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

Redis做聚合查询其实挺方便,性能又快还能节省资源,真心值得试试看

最近我在做一个用户行为分析的需求,需要快速统计出不同渠道来源的用户在一天内的活跃次数、最早和最晚的活跃时间,一开始的思路很直接:用关系型数据库,比如MySQL,写个SQL语句,GROUP BY一下渠道,再用COUNTMINMAX函数就搞定了,但转念一想,这数据量可不小,而且需要实时查看,频繁对线上数据库做这种聚合查询,怕是会影响正常业务,自己也觉得有点“心虚”。

正发愁呢,旁边一位经验丰富的同事看了一眼,说:“这种简单的计数和极值统计,你用Redis试试呗,又快又省资源。” 我当时第一反应是:“Redis?那不是个缓存吗?做键值存储是厉害,还能做聚合查询?” 同事笑了笑,让我自己试试看,这一试,还真发现了一片新天地。

Redis的哈希与有序集合联手

我琢磨了一下需求,关键是要按渠道分组,然后对每个渠道下的时间戳进行统计,Redis虽然没有像SQL那样直接的GROUP BY语句,但它有非常高效的数据结构,我是这么做的:

  1. 存储数据:每当有一个用户活跃事件产生时,我除了记录详细信息到数据库(用于后续复杂分析),还会向Redis发送两条命令,我用一个大的哈希(Hash)来存储每个渠道的总活跃次数,键名类似 user_activity:channel:[渠道ID]:count,直接使用HINCRBY命令对这个哈希里的对应字段进行累加,这一步替代了SQL中的COUNT。 对于最早和最晚活跃时间,我利用了有序集合(Sorted Set),我为每个渠道创建一个有序集合,键名如 user_activity:channel:[渠道ID]:timestamps,然后将每次活跃的时间戳(比如用Unix时间戳)作为分数(score),和一个唯一的标识符(比如用户ID加随机数,确保不重复)作为成员(member)ZADD进去,有序集合天生就能维护成员的分数顺序。

  2. 执行查询:当需要查询统计结果时,操作就非常快了。

    • 总次数:直接HGET那个哈希键的对应字段,时间复杂度O(1),瞬间完成。
    • 最早活跃时间:对那个渠道的有序集合使用ZRANGE key 0 0 WITHSCORES,取出分数最小的那个成员(即最早时间戳)。
    • 最晚活跃时间:使用ZREVRANGE key 0 0 WITHSCORES,取出分数最大的那个成员(即最晚时间戳)。

这样一来,我根本不需要像在MySQL里那样扫描大量的记录行,所有的聚合结果都是在数据写入时通过HINCRBYZADD预先“计算”好的,查询只是简单地获取已经计算好的结果或者取一下有序集合的头尾元素,效率极高,这些操作都是内存操作,速度自然比磁盘I/O快好几个数量级。

Redis做聚合查询其实挺方便,性能又快还能节省资源,真心值得试试看

性能与资源节省的切身感受

实际用上去之后,感受最深的有两点:

一是速度真的快,之前那个复杂的SQL查询,在数据量大的时候,即使有索引,执行起来也要几百毫秒甚至上秒级,而改用Redis后,每次查询的响应时间都在毫秒级别,几乎是“秒出”结果,这对于需要实时展示数据的后台管理系统来说,体验提升太大了。

二是确实省资源,正如我最初担心的,原来的聚合查询虽然能出结果,但时不时会看到数据库的监控图表上出现一个小小的“波峰”,那就是我的查询在“作怪”,虽然单次影响不大,但频繁执行终究是个隐患,迁移到Redis后,主数据库的压力明显减小了,Redis本身就是为了高并发、低延迟场景设计的,处理这些请求游刃有余,相当于把聚合计算的压力从核心业务数据库上剥离了出来,这种架构上的解耦,让整个系统更稳健了。

Redis做聚合查询其实挺方便,性能又快还能节省资源,真心值得试试看

不只是计数:更多聚合场景

尝到甜头后,我开始研究Redis还能做哪些聚合,我发现,除了我用的计数和极值,通过不同的数据结构和命令组合,还能实现不少功能:

  • 去重计数:比如统计一天内活跃用户数(UV),可以用SET直接存储用户ID,利用集合的去重特性,最后用SCARD命令获取集合大小,如果数据量巨大担心内存,还可以用HyperLogLog,它可以用极小的空间完成近似去重计数,虽然略有误差,但对于UV统计这种场景完全够用。
  • 统计平均值:这需要稍微变通一下,可以像我一样,用一个哈希同时存储sum(总和)和count(次数),每次写入时,同时更新这两个字段,查询时,分别取出sumcount,然后在应用层做个除法,就得到平均值了,虽然需要两次查询,但依然很快。
  • 排行榜:这简直就是有序集合的“主场”。ZREVRANGE直接就能出Top N,ZRANK能查某个用户的排名,非常方便。

一点提醒和总结

Redis做聚合也不是万能的,它适合的是实时性要求高、聚合逻辑相对简单(如计数、求和、排序、去重)、数据量可以放在内存中的场景,如果聚合条件非常复杂,涉及多表关联,或者需要完整的SQL分析功能,那还是关系型数据库或者专业的OLAP数据库更合适。

因为数据主要在内存中,需要合理设置过期时间,或者规划好数据持久化与备份策略,防止数据丢失。

这次经历让我彻底改变了对Redis的看法,它不仅仅是一个简单的缓存工具,更是一个高性能的数据结构服务器,在合适的场景下,用它来做实时聚合查询,就像开启了一个隐藏的“性能外挂”,既能享受到飞一样的速度,又能为核心数据库减负,真心值得大家在遇到类似需求时试试看,很多时候,技术选型的优化,不在于用多么高深的新技术,而在于把现有的、熟悉的工具用到极致。