在写x联帮项目时遇到一个问题:编写获取社团列表接口时,需要加入Redis缓存,但是给分页结果和多条件模糊查询结果添加Reids缓存时应该如何实现呢?于是去搜索了一些实现方案。
传统分页
传统分页做缓存是直接查找出来,按页放入缓存,这种方式有许多缺点:
缓存不能及时更新,一旦数据发生变化,所有之前的分页缓存都会失效;
同时这种方案无法满足按条件存入缓存。
按照条件分页
如果是按照不同的分页条件来缓存多个key,比如分页查询社团:pageNumber=1&condition=10
缓存的key就可以设置成:community:page:1:condition:10
同样这种方法也存在着一些问题:
缓存的value存在冗余,例如:community:page:1:condition:10中就可能包括了community:page:1:condition:5中的内容(数据没有发生变化的情况下)
当改变查询条件为community:page:1:condition:5时,就会导致缓存未命中,降低缓存命中率;
为了保证数据一致性,需要清理缓存的时候,很难处理,redis的keys命令会对性能造成很大影响,从而导致redis很大的延迟。在生产环境中,需要手动拼接缓存key,如果条件很多,不知道会拼到哪一个条件为止(虽然我的项目没有那么多条件)。
放弃数据一致性,当设置了过期时间时,可能会出现查询第一页命中了缓存,查询第二页的时候未命中缓存,但数据发生了变化,第二页查询返回和第一页相同的结果。
基于Redis提供的ZSet(Sorted Set)数据结构
ZSet结构主要存储有序集合。指令描述以及该指令在分页实现中的作用:
·ZADD:SortedSet(以下简称S Set)的添加元素指令ZADD key score member[[score,member]…]会给每个添加的元素member绑定一个用于排序的值score,S Set就会根据score值的大小对元素进行排序。开发中通常将数据的时间属性做为score用于排序,也可以根据具体业务场景去选择排序的目标。(例如此项目中通过活动点击量设置社团热度,社团根据热度进行排名,热度排名可以做为score)
·ZREVRANGE:S Set中的指令ZREVRANGE key start stop可以返回指定区间内的成员,可以用来做分页
·ZREM:S Set的指令ZREM key member可以根据key移除指定的成员,从而满足删评论的需求。
下面是分页实现演示图,将20230418数据插入后新的查询情况。
Redis中的List也可以实现分页,但List无法实现自动排序,并且S Set还可以根据score进行数据筛选,获取目标区间内的数据。但是当遇到需要插入重复数据的情况,可能就需要使用List来实现分页。
多条件模糊查询
Redis是key-value类型的内存数据库,通过key直接取数据虽然很方便,但是没有提供像mysql那样的sql条件查询支持。因此想要实现模糊条件查询需要借助Redis提供的结构和功能。
Redis的模糊条件查询是基于Hash实现的,具体实现是将数据某些条件值作为hash的key值,将数据本身作为value进行存储。然后通过Hash提供的HSCAN指令去遍历所有的key进行筛选,得到符合条件的所有key值(hscan可以进行模式匹配)。我们通常可以将符合条件的key全部放入到一个Set或List中。通过这种方式就可以根据得到的key值取到相应的value。
HSCAN提供的模式匹配方法,本质上是基于遍历实现的,对于开发有些基本了解就知道遍历的效率并不高,数据量非常大的情况下甚至可以说非常低效,每一次匹配都需要遍历所有的key。
Redis分页+多条件模糊查询组合实现
在实际应用中,单独使用ZSet实现分页已经可以解决大部分问题,并且有着不错的性能,但如果我们所分页的数据往往会伴随一些动态的筛选条件,此时ZSet就不能满足我们的业务需求。
两种解决方案:1.如果数据已经存储在了持久化数据库中,可以每次在数据库中做好条件查询再将数据放入Redis中进行分页。2.在Redis中实现多条件模糊查询并分页。第一种方案可以保证一定的数据一致性,但缺点在于有时数据不一定都在持久化数据库中,在一些场景下需要展现更好的并发性及高响应,就需要将数据先放置在缓存数据库中,等到某个时间或满足某种条件时再持久化到数据库中(当然,目前我们的项目还没有那么高端)。此时第一种方案就不能很好的满足业务需求,就需要使用第二种方案。
实现思路
首先采用多条件模糊查询的方式,将涉及到的条件字段作为hash的field,数据内容作为对应的value进行存储(我们的项目以json格式存储,方便反序列化)。需要实现约定好查询的格式,拿到匹配串后先去Redis中查询是否存在以该匹配串为key的ZSet,如果没有则通过Redis提供的HSCAN遍历所有hash的field,得到符合条件的field并放入一个ZSet集合,将集合的key设置为条件匹配串。如果已经存在了,则直接对ZSet进行分页查询即可。
由于在缓存数据库中没有找到符合的ZSet集合,将根据匹配串生产一个新的集合用于分页。下图为实现流程
虽然我们可以通过上面这种方式实现多条件模糊查询+分页的功能,但是实际环境中,我们不能无限制生产新的集合,因为匹配串有多种,会给缓存带来巨大压力(我们的项目云服务器分配给Redis只有200M内存,别问,问就是用不起好的服务器)。因此需要在生成集合时设定过期时间,到期自动销毁,同时对于命中的集合将更新其过期时间,以保证热点数据持久性。
同时,数据的实时性也是一个问题,因为集合是在生成集合时的Hash内容决定的,对于新插入到Hash的数据,集合是无法感知的,因此有两种解决方案:一种是插入到Hash的同时再插入到其他相应的集合中,保证数据一直是最新的,这种方式就需要增加特殊的前缀用于识别需要插入到哪些集合中;第二种方式则是定时更新,相比第一种方式较为省力,但实时性不如第一种。具体选择哪种则需要根据具体的业务场景。
分享完毕