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

用Redis咋能快查关注数,简单又高效的思路分享

说到用Redis快查关注数,比如查一个用户有多少粉丝,或者关注了多少人,这确实是Redis最典型的应用场景之一,核心思路就一句话:别去数据库里现算,直接把数存好,用的时候一读就行。 下面我详细拆解一下怎么把这个思路落地,保证既简单又高效。

最核心、最直接的办法:用String类型存一个数字

这是最直观的,比如用户A的粉丝数,我就在Redis里存一个键值对,键的名字得有规律,follower_count:user:A,对应的值就是粉丝数量,15203

  • 怎么用:当有人关注用户A时,程序干两件事:1. 在数据库里建立关注关系,2. 立刻给Redis里的 follower_count:user:A 这个值加1(使用Redis的 INCR 命令),取消关注就减1(DECR 命令),查数量的时候,直接用 GET follower_count:user:A,这个速度是微秒级的,快得没影。
  • 优点:极其简单,速度最快,理解起来没任何难度。
  • 需要注意的点:这个数字可能会“不准”,因为极端情况下,可能数据库更新成功了,但Redis增减失败了(比如网络闪断),所以它适合对数字的“绝对精确性”要求不是百分百苛刻的场景,比如微博的粉丝数,差几个根本无所谓,如果非要绝对精确,可以在系统低峰期,定期从数据库同步一次正确的数量到Redis,纠正一下。

当需要知道“谁”关注了,而不仅仅是“多少”个:用Set集合

我们不光想知道粉丝数量,还想知道具体是哪些人关注了我,或者判断某个人B是不是已经关注了A,这时候,String类型就不够了,得用Redis的Set(集合)类型。

  • 怎么用:为每个用户创建一个Set,比如用户A的粉丝集合,键可以叫 followers:user:A,当用户B关注A时,就把B的用户ID(user:B)加入到这个集合里(使用 SADD 命令),取消关注就从集合里移除(SREM 命令)。
  • 查什么
    1. 查总数:直接用 SCARD followers:user:A 命令,这个命令能瞬间返回集合的元素个数,也就是粉丝数,效率同样极高。
    2. 查具体名单:用 SMEMBERS followers:user:A 能取出所有粉丝ID,但要注意,如果粉丝量巨大(比如百万级),这个命令可能会慢,而且一次返回大量数据有风险,更常见的做法是分页查,用 SSCAN 命令。
    3. 判断是否关注:用 SISMEMBER followers:user:A user:B,能立刻知道B是不是A的粉丝。
  • 优点:功能强大,除了计数,还能做很多关系判断。
  • 缺点:比单纯的String耗内存,如果用户粉丝量极大(千万甚至上亿),这个Set会非常大,占用很多Redis内存,操作起来也可能变慢,这时候就需要更高级的招了。

对付海量数据:用HyperLogLog做近似计数

用Redis咋能快查关注数,简单又高效的思路分享

这是一种“杀手级”的应用,如果你的场景是:我需要统计的量非常大(比如一个明星大V的粉丝数上亿),并且我可以接受这个数字有微小的误差(比如误差在0.81%以内),但我的首要目标是极致节省内存,那么HyperLogLog(简称PF)就是为你准备的。

  • 怎么用:和String类似,为每个用户分配一个PF结构,键比如叫 pf_followers:user:A,当有新人关注时,就用 PFADD pf_followers:user:A user:B 命令,查询总数用 PFCOUNT pf_followers:user:A
  • 优点:内存占用小得惊人,官方说法是,统计1亿个不重复元素,只需要大概12KB内存,而且无论你要统计的元素有多少,它占用的内存都是固定的,这对于省成本来说太友好了。
  • 缺点:就一条,数字是“近似”的,不精确,所以它只适用于那种“展示个大概规模”的场景,比如页面侧边栏显示“10万+粉丝”,具体是100,102还是100,201,不重要。

总结一下怎么选

就是看你的具体需求:

  • 只要数字,追求最快最直接 -> 用 String,配合 INCR/DECR,记得处理好可能的数据不一致问题。
  • 既要数字,还要知道是谁,关系判断 -> 用 Set,配合 SADD/SCARD/SISMEMBER,适合粉丝量不是天文数字的场景。
  • 海量数据统计,允许一点点误差,死磕内存节省 -> 用 HyperLogLog,配合 PFADD/PFCOUNT

一个非常重要的实战技巧:分桶计数

用Redis咋能快查关注数,简单又高效的思路分享

这个技巧来自一篇名为《Redis 设计与实现》的经典资料,用来解决“超级大V”的计数问题,比如一个用户有5亿粉丝,你用Set存5亿个ID,内存肯定爆炸,即使用String,频繁的 INCR 操作在极高并发下也可能有压力。

“分桶”的思路很巧妙:我不只存一个总数,而是把计数分散到多个小的Redis键里。

用户A的粉丝数,我不再只用一个 follower_count:user:A,而是用100个(这个数量可以调整): follower_count_bucket:user:A:1 follower_count_bucket:user:A:2 ... follower_count_bucket:user:A:100

当有新粉丝关注时,我随机选一个桶(比如第57号桶),给这个桶里的数字加1,查总数的时候,我需要把这100个桶的值全部 GET 出来,在程序里加一下。

  • 为什么这样更好?
    1. 分散写入压力:原来所有并发都争抢一个键,现在分散到100个键上,大大降低了单个键的竞争,提升了并发处理能力。
    2. 控制单键大小:避免了单个键的Value值过大(虽然String类型值可以很大,但小一点总归好)。
    3. 平衡了读写:写操作(INCR)因为分散而变快了,读操作(GET 100个键)虽然变复杂了,但依然是O(1)操作,只是多了网络开销,由于读操作通常可以通过缓存等手段优化,而写操作是实打实的压力,所以这个权衡往往是值得的。

就是用Redis快查关注数的主流思路,核心就是利用Redis内存操作的极致速度,把计算结果提前存好,用空间换时间,具体用哪种结构,取决于你对数据精确度、功能丰富度和内存成本之间的权衡。