带查询条件的分页列表缓存策略目标:快速根据查询条件拿到分页

目标:快速根据查询条件拿到分页的list数据

使用应用情景如下:

查询条件可分为以下几类:

  • type status 等 可选值为有限集合,使用 “=” 来查询
  • title name等 输入值不确定,使用 “like” 来查询

分页:

  • pageNumber
  • pageSize

数据存储在mysql

首先确定缓存是否是最佳的方案

可用做缓存key生成条件的属性为3种:

  1. 枚举类值 type
  2. 模糊搜索类值 title
  3. 分页参数 pageNumber pageSize

缓存的value:

  1. id
  2. 整个Record对象

如用以上3种可选属性排列组合,无论选取哪几种,都会面临缓存频繁清除和缓存命中率低的问题,如直接拼接全部查询条件+分页参数作为key,则

  • 数据库记录增,删,改,都需要失效缓存,缓存清除的频率非常高;
  • 因为有用户输入字段,一旦涉及到用户输入文本,那么该字段的输入值差异会比较大,导致缓存中的key数量非常多,缓存的命中率会很低

如不使用分页参数,则要缓存条件匹配的所有记录,数量的压力是一方面,另一方面,当数据库记录发生变化时,不管是增删改哪一种,每一个缓存要判断改动数据是否命中,然后更新,更新逻辑十分复杂

所以结论是,此种情况下,要提升接口响应速度,缓存方案不可行

目前成熟解决方案:搜索引擎(Elasticsearch)

Elasticsearch(ES)是一个基于 Lucene 构建的开源分布式搜索分析引擎,可以近实时的索引、检索数据。具备高可靠、易使用、社区活跃等特点,在全文检索、日志分析、监控分析等场景具有广泛应用。

方案:数据库中的数据可同步存入到Elasticsearch中,当更新或者删除数据时,同时更新Elasticsearch中数据,列表查询时可从Es中查询

未命名文件.png

列表缓存是否一无是处

具体问题具体分析

场景:无查询条件,只要求分页的list数据的快速查询
则一种可用设计方案如下:

redsi.png

缓存实现选择redis, 缓存分两块:

  1. 全部数据id的缓存,使用zset, key为数据id, score为用来排序的字段(id,rank,时间戳等)
  2. 对象的缓存,可序列化后使用String类型

分页查询时,首先在zset中查出当前要获取页的所有id,再根据这一批id去获取相应的Record,如有Record不在缓存中,再去数据库中使用where id in 查出这些数据,然后更新缓存

对象的缓存的list getById 都可以使用

需要注意的是zset中必须有全部id,并且score要同步数据库中被选中排序的那个字段的变化

缓存设计的三个要点:

  1. 初始化
  2. 更新
  3. 清除

从这三个点来考虑上述缓存设计:

初始化:

ids Record
可进行缓存预热,在服务启动前,就把全部ids加载到缓存中,或在第一次list查询时加载 同样,可把score比较大的一批预热进缓存,这些数据在最前面,属于热数据,或在第一次获取不到时加载

更新:

ids Record
数据新增or删除or用作score的字段变化时 数据更新时

清除

ids Record
不清除 数据删除时

通过以上分析,可以看出这种设计命中率比较高,缓存更新策略也比较简单明了

本地缓存 vs 集中式缓存

cache对比.png

案例分析:服务有多个节点,使用spring cache做本地缓存,获取数据的方法上使用@Cacheable来缓存数据,有一个单独的方法来清理缓存,该方法使用@CacheEvict注解,不做实际操作,专门用来清缓存,调用的时机在数据保存的时候,看起来好像没有问题,在数据更新的时候清理掉缓存

那么问题在哪儿呢?

该服务有多个节点,保存数据的请求只会打到某一个节点上,也就是说,只有收到保存请求的节点会调用清理缓存的方法,而其他节点并没有清缓存,所以导致了几个节点的缓存不一致