常规分页查询缓存方案

我们都知道,通过缓存查询的结果,可以极大的提升系统的服务能力,以及降低底层服务或者是数据库的压力。

对于有分页条件的缓存,我们也可以按照不同的分页条件来缓存多个key,比如分页查询产品列表,page=1&limit=10和page=1&limit=5这两次请求可以这样缓存查询结果

1
2
productList:page:1:limit:10
productList:page:1:limit:5

这个是一种常见方案,但是存在着一些问题:

  • 缓存的value存在冗余,productList:page:1:limit:10缓存的内容其实是包括了productList:page:1:limit:5中的内容(缓存两个key的时候,数据未发生变化的情况下)
  • 仅仅是改变了查询条件的分页条件,就会导致缓存未命中,降低了缓存的命中率
  • 为了保证数据一致性,需要清理缓存的时候,很难处理,redis的keys命令对性能影响很大,会导致redis很大的延迟,生产环境一般来说禁止该命令。自己手动拼缓存key,你可能根本不知道拼到哪一个page为止。
  • 放弃数据一致性,通过设置失效时间来自动失效,可能会出现查询第一页命中了缓存,查询第二页的时候未命中缓存,但此时数据已经发生了改变,导致第二页查询返回的和第一页相同的结果。

以上,在分页条件下这样使用常规方案总感觉有诸多困扰,诸多麻烦,那是不是就应该放弃使用缓存?

基于SortedSet的分页查询缓存方案

首先想到的解决方法是使用List不再根据分页条件使用多个key,而是使用一个key,也不分页将全部的数据缓存到redis中,然后按照分页条件使用range(key,start,limit)获取分页的结果,这个会导致一个问题,当缓存失效时,并发的写缓存会导致出现重复数据.而且评论如果被删除,就需要更新Redis中的数据,LREM命令可以做到删除,但是LRANGE 是一个耗时命令 O(N)。

所以想到通过使用zset来处理并发时的重复数据.代码逻辑如下:

  1. range(key,start,limit)按照分页条件获取缓存,命中则直接返回
  2. 缓存未命中,查询数据库当前页和后面几页的数据。
  3. add(key,valueScoreMap<value,score>)写入缓存,expire设置缓存时间
  4. 当需要清理缓存时,直接删除key,如果是因为数据新增和删除,可以add(key,value,score)remove(key,value)

redis中会按照score分值升序排列map中的数据,一般的,score分值是sql语句的order by filedA的filedA的值,这样能保证数据一致性。

如果涉及到复杂的查询,基本可以确认不适合采用redis,更加适用其他的搜索引擎,比如es。比如像购物网站中商品一样,可以根据最新时间分页,也可以根据销量分页,或者说可以根据热度做分页.

参考

Redis分页查询缓存方案

Redis 分页排序查询