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

用Redis怎么搞复杂数据查询,感觉还挺有意思的那些方法和思路

用Redis搞复杂数据查询,如果还想着像用MySQL那样写个SELECT * FROM table WHERE ...,那肯定就走错路了,Redis的乐趣就在于逼着你换一种“算计”数据的方式,核心思路不是等查询来了再去翻箱倒柜,而是事先把数据“摆放”成各种容易拿取的形状,让查询变成一个极其简单的“抓取”动作,这就像你不是在杂乱的工具箱里找一把特定的螺丝刀,而是有一个挂满了各种螺丝刀的墙面,伸手就能拿到。

第一个有意思的思路:用有序集合(Sorted Set)玩转排行榜和分段查询。

这大概是Redis最经典的用法之一了,有序集合里每个成员都有一个分数(score),Redis会自动根据分数排序,这天生就是为排行榜准备的,比如游戏积分榜、热搜榜,但有意思的扩展在于,你不仅能查Top N,还能做“分段统计”。

来源中提到一个例子,统计在线用户数,你可以把用户ID作为成员,把最后一次活跃的时间戳作为分数,塞进一个有序集合,当你想知道“最近10分钟内活跃的用户有多少”时,根本不需要遍历所有用户,只需要用ZCOUNT命令,查询当前时间戳减去10分钟到当前时间戳这个分数区间内有多少成员就行了,速度极快,这其实就是一种对时间的范围查询。

再比如,电商里想快速知道价格在100元到200元之间的商品有多少,也可以把商品ID和价格放进有序集合,用ZCOUNTZRANGEBYSCORE一下就出来了,这种“预排序”的思想,把查询时的计算成本完全转移到了写入时,用空间换来了惊人的时间效率。

第二个更有趣的思路:利用集合(Set)和位图(Bitmap)做多维度的标签筛选。

这是我觉得Redis最“骚”的操作之一,想象一个用户系统,每个用户有多个标签,90后”、“男性”、“喜欢看电影”、“在北京”,现在要找出同时满足“90后”、“男性”、“喜欢看电影”这三个标签的所有用户。

传统数据库可能得JOIN好几张表,数据量一大就慢,Redis的做法很巧妙:

  1. 为每一个标签创建一个集合(Set),比如一个叫tag:90后的集合,里面是所有90后用户的ID。
  2. 同样,创建tag:男性tag:喜欢看电影这些集合。

当需要查询时,直接使用集合的交集操作SINTER,命令就是SINTER tag:90后 tag:男性 tag:喜欢看电影,结果瞬间就出来了,就是同时拥有这三个标签的用户ID集合,这种操作的速度几乎与集合大小无关,非常恐怖。

用Redis怎么搞复杂数据查询,感觉还挺有意思的那些方法和思路

但集合还有个更节省内存的“升级版”——位图(Bitmap),你可以把每个用户ID映射为一个数字偏移量(比如用户ID是自增的,就直接用ID值),每个标签不再是一个存储着大量用户ID的集合,而是一个很长的二进制位序列(位图)。

如果用户1是90后,就在tag:90后这个位图的第1个位上标记为1,用户2如果是,就在第2个位上标记为1,以此类推,查询时,不再是求集合交集,而是对三个位图进行“按位与”(BITOP AND)操作,得到的新位图中,值为1的位,对应的用户就是满足条件的,位图在存储海量用户的是/否状态时,能节省巨大的内存,比如1亿用户的一个标签状态,只需要大概12MB内存,这种用二进制位来“打标签”进行多维度筛选的思路,非常精巧和高效。

第三个思路:通过设计键名(Key)本身来建立索引。

Redis的键不只是一个标识,它本身可以携带信息,你可以通过精心设计键的命名方式,来模拟一些简单的查询。

来源中常举的例子是存储用户会话(Session),你不仅可以简单地用session:[sessionId]来存,还可以用session:user123:firefox这样的键名,意思是“用户123在Firefox浏览器上的会话”,这样,如果你想清理某个用户的所有会话,虽然Redis不支持按前缀后缀模糊删除,但你可以用SCAN命令迭代匹配session:user123:*这样的模式,找到所有相关键再删除,这相当于在键名里内置了一个针对用户ID的二级索引。

用Redis怎么搞复杂数据查询,感觉还挺有意思的那些方法和思路

再比如,存储文章数据,键名可以是article:1000,但同时,如果你想按发布时间排序,可以另外用一个有序集合,成员是文章ID,分数是发布时间戳,如果你想按作者分类,可以为每个作者维护一个集合,里面是他所有文章的ID,这些额外的数据结构,都是不同类型的“索引”,写入文章时,你需要在article:1000这个哈希键里存一份,同时也要把ID加到全局文章ID列表、按时间排序的有序集合、按作者分类的集合里,这叫“反规范化”,写入时麻烦一点,但换来了读取时多种维度下的极致速度。

第四个思路:用Lua脚本保证复杂操作的原子性。

当你上面的技巧玩得越来越复杂,可能会遇到一个问题:一个业务操作需要按顺序执行好几个Redis命令,先ZADD更新排行榜分数,再HINCRBY更新用户总积分,最后PUBLISH发个消息通知,在分布式环境下,这几个命令之间可能会被其他客户端的命令插入,导致数据不一致。

这时,Redis的Lua脚本就派上用场了,你可以把这一连串操作写成一个Lua脚本,一次性提交给Redis服务器执行,Redis保证一个Lua脚本在执行时是原子性的,中间不会被任何其他命令打断,这就好比你把一整件事的说明书直接交给了仓库管理员,他照着清单一步到位地拿取和修改货物,而不是你在一旁一句一句地指挥,避免了他听错或者被其他人打断的风险,这让在Redis上构建复杂的业务逻辑变得可靠。

这些方法和思路的核心趣味在于:

放弃“通用查询”的幻想,拥抱“专用数据结构”,就像玩乐高,你不是拿着一块万能橡皮泥去捏出形状,而是用各种现成的、形状特异的基础积木(字符串、哈希、列表、集合、有序集合),在设计和搭建阶段就想好它们如何组合,来应对未来所有已知的“查询”需求,这是一种充满设计感和预见性的编程乐趣,当你用一个ZINTERSTORE命令秒杀了别人需要复杂SQL和索引优化才能搞定的多维度聚合查询时,那种感觉确实挺有意思的。