目标:快速根据查询条件拿到分页的list数据
使用应用情景如下:
查询条件可分为以下几类:
- type status 等 可选值为有限集合,使用 “=” 来查询
- title name等 输入值不确定,使用 “like” 来查询
分页:
- pageNumber
- pageSize
数据存储在mysql
首先确定缓存是否是最佳的方案
可用做缓存key生成条件的属性为3种:
- 枚举类值 type
- 模糊搜索类值 title
- 分页参数 pageNumber pageSize
缓存的value:
- id
- 整个Record对象
如用以上3种可选属性排列组合,无论选取哪几种,都会面临缓存频繁清除和缓存命中率低的问题,如直接拼接全部查询条件+分页参数作为key,则
- 数据库记录增,删,改,都需要失效缓存,缓存清除的频率非常高;
- 因为有用户输入字段,一旦涉及到用户输入文本,那么该字段的输入值差异会比较大,导致缓存中的key数量非常多,缓存的命中率会很低
如不使用分页参数,则要缓存条件匹配的所有记录,数量的压力是一方面,另一方面,当数据库记录发生变化时,不管是增删改哪一种,每一个缓存要判断改动数据是否命中,然后更新,更新逻辑十分复杂
所以结论是,此种情况下,要提升接口响应速度,缓存方案不可行
目前成熟解决方案:搜索引擎(Elasticsearch)
Elasticsearch(ES)是一个基于 Lucene 构建的开源分布式搜索分析引擎,可以近实时的索引、检索数据。具备高可靠、易使用、社区活跃等特点,在全文检索、日志分析、监控分析等场景具有广泛应用。
方案:数据库中的数据可同步存入到Elasticsearch中,当更新或者删除数据时,同时更新Elasticsearch中数据,列表查询时可从Es中查询
列表缓存是否一无是处
具体问题具体分析
场景:无查询条件,只要求分页的list数据的快速查询
则一种可用设计方案如下:
缓存实现选择redis, 缓存分两块:
- 全部数据id的缓存,使用zset, key为数据id, score为用来排序的字段(id,rank,时间戳等)
- 对象的缓存,可序列化后使用String类型
分页查询时,首先在zset中查出当前要获取页的所有id,再根据这一批id去获取相应的Record,如有Record不在缓存中,再去数据库中使用where id in 查出这些数据,然后更新缓存
对象的缓存的list getById 都可以使用
需要注意的是zset中必须有全部id,并且score要同步数据库中被选中排序的那个字段的变化
缓存设计的三个要点:
- 初始化
- 更新
- 清除
从这三个点来考虑上述缓存设计:
初始化:
ids | Record |
---|---|
可进行缓存预热,在服务启动前,就把全部ids加载到缓存中,或在第一次list查询时加载 | 同样,可把score比较大的一批预热进缓存,这些数据在最前面,属于热数据,或在第一次获取不到时加载 |
更新:
ids | Record |
---|---|
数据新增or删除or用作score的字段变化时 | 数据更新时 |
清除
ids | Record |
---|---|
不清除 | 数据删除时 |
通过以上分析,可以看出这种设计命中率比较高,缓存更新策略也比较简单明了
本地缓存 vs 集中式缓存
案例分析:服务有多个节点,使用spring cache做本地缓存,获取数据的方法上使用@Cacheable来缓存数据,有一个单独的方法来清理缓存,该方法使用@CacheEvict注解,不做实际操作,专门用来清缓存,调用的时机在数据保存的时候,看起来好像没有问题,在数据更新的时候清理掉缓存
那么问题在哪儿呢?
该服务有多个节点,保存数据的请求只会打到某一个节点上,也就是说,只有收到保存请求的节点会调用清理缓存的方法,而其他节点并没有清缓存,所以导致了几个节点的缓存不一致