当前位置:首页 > 问答 > 正文

Redis里怎么按条件拿指定数量数据,返回条数控制问题探讨

在Redis的实际使用中,我们经常会遇到一种需求:从一个大的数据集合里,按照一定的条件筛选出我们想要的数据,并且还要控制最终返回结果的数量,从所有用户中找出最近登录的100个VIP用户,或者从一批商品键里找出价格低于50元的前20个商品,这个需求在关系型数据库里用一句SQL的WHERE和LIMIT就能轻松搞定,但在Redis这种键值数据库中,由于没有原生的、复杂的查询语句,实现起来就需要动一番脑筋,下面我们就来探讨几种常见的方法和它们各自会遇到的问题。

最直接也最不推荐的方法,就是通过KEYS命令匹配出所有符合模式的键,然后再进行筛选,你想找所有以"user:profile:"开头,并且包含"vip"标识的键,有人可能会想先用KEYS user:profile:*vip*拿到所有键名,然后在客户端程序里进行排序或截取,但Redis官方和所有技术社区都严重警告,KEYS命令在生产环境中是禁用的,因为它会遍历数据库中的所有键,当数据量很大时,会严重阻塞Redis服务器,导致其他所有请求超时,相当于一次服务攻击,尽管思路简单,但这种方法在现实中是不可行的,必须放弃。

Redis里怎么按条件拿指定数量数据,返回条数控制问题探讨

为了避免KEYS命令的性能灾难,大家通常会使用SCAN命令族(包括SCAN, SSCAN, HSCAN, ZSCAN),SCAN命令采用游标方式增量式地遍历集合,不会长时间阻塞服务器,我们可以用SCAN 0 MATCH user:profile:*vip*来逐步获取所有匹配的键,SCAN命令本身只提供基于键名模式的过滤(MATCH),它无法检查键所对应的值的内容,也就是说,如果你要根据哈希键(Hash)里的某个字段值(如用户等级)来筛选,单靠SCAN是做不到的,你需要先用SCAN拿到一批键名,然后再用像HMGET这样的命令逐个取出这些键的字段值,在客户端判断是否满足条件,这个过程涉及到多次网络往返(如果Redis和客户端不在同一台机器上,网络开销很大)和多次Redis命令执行,当数据量很大时,效率依然很低,返回指定数量的结果可能需要遍历整个数据集。

有没有更高效的方法呢?这就要提到Redis的有序集合(Sorted Set)了,这是实现“按条件查询并控制返回数量”最强大、最高效的数据结构,它的核心思想是“预先排序和索引”,你可以将你的查询条件转化为一个分数(score),上述找最近登录的VIP用户的例子,“最近登录”这个条件可以用登录时间戳作为分数,你把用户ID和登录时间戳一起加入到有序集合中(例如ZADD recent_login <timestamp> <userid>),这样,所有用户已经按照登录时间从远到近排好序了。

Redis里怎么按条件拿指定数量数据,返回条数控制问题探讨

查询“最近登录的100个VIP用户”就变成了两步:

  1. 使用ZREVRANGEBYSCORE命令(或者更新的ZRANGE命令带BYSCORE和REV选项),从当前时间开始,取分数最大(即最新)的前N个(比如200个)用户ID,这里N可以设得比最终需要的100个稍大一些,留出筛选余地。
  2. 在客户端程序中,用第一步得到的用户ID,去查询这些用户对应的详细信息(比如存在Hash里的VIP等级),然后过滤出VIP用户,最后截取前100个。

这种方法之所以高效,是因为有序集合底层是跳表(Skip List)和哈希表实现的,按分数范围取数据的复杂度是O(log(N) + M),N是集合大小,M是返回的元素数量,速度非常快,主要的性能损耗发生在第二步的批量查询和客户端过滤,但相比用SCAN全表扫描,性能已经有了天壤之别,这种方法的挑战在于,你需要维护这个有序集合,每当有用户登录时,就要更新其对应的时间戳分数,这是一种用“空间换时间”和“写入换读取”的思路。

如果查询条件更复杂,比如既要看登录时间,又要看用户等级,甚至还要看地理位置,单靠一个有序集合就力不从心了,这时,业界常见的做法是使用Redis的另一种数据结构——RedisSearch模块(这是一个官方支持的模块,需要单独安装),RedisSearch提供了类似于搜索引擎的倒排索引功能,可以让你对存储在Hash中的多个字段进行复杂的联合查询,并且也支持分页(LIMIT)和排序,你可以像写简易SQL一样,执行如@login_time:[-inf +inf] @vip:true这样的查询,并指定返回0到99条结果,这对于复杂查询场景是终极解决方案,但缺点是需要在Redis服务器上安装额外模块,并且需要建立索引,有一定学习成本。

还有一种情况是对于集合(Set)或列表(List)的操作,比如从一个很大的用户粉丝列表(Set)中随机抽取10个粉丝,这时可以使用SRANDMEMBER key 10命令,它可以高效地随机返回指定数量的成员,但如果要的不是随机,而是有特定顺序(如按加入时间最早),并且列表很大,List结构的LRANGE命令虽然可以按索引范围取,但确定符合条件的数据的起始索引可能同样困难。

在Redis中按条件获取指定数量数据,核心思路是根据条件的复杂度和性能要求来选择方案:绝对避免使用KEYS命令;对于简单键名模式,使用SCAN但需注意其无法过滤值内容;对于单条件排序查询,优先使用有序集合(Sorted Set),通过将条件映射为分数来获得极高性能;对于多条件复杂查询,考虑使用RedisSearch模块;对于集合的随机抽样,有对应的原生命令,最关键的是,要理解Redis是数据结构服务器,在设计数据存储时,就要提前想好未来的查询方式,通过精心设计数据结构来避免低效的遍历,这才是解决返回条数控制问题的根本。

Redis里怎么按条件拿指定数量数据,返回条数控制问题探讨