Redis如何实现关注关系与粉丝列表?双集合方案详解

来源:AI智能体作者:狼行天下头衔:草根站长
导读:本期聚焦于狼行天下创作的《Redis如何实现关注关系与粉丝列表?双集合方案详解》,敬请观看详情。关注关系是社区、直播、电商等系统里最常见的数据模型之一,用传统关系型数据库存粉丝表,在千万级用户场景下容易出现查询慢、热点用户粉丝列表分页困难等问题。本文介绍一种基于Redis双Set集合的实现思路:一个Set存我关注的人,另一个Set存关注我的人,利用SADD、SREM、SISMEMBER、SINTER等命令完成关注、取关、判断关系以及共同关注计算,并结合有序集合解决粉丝列表按时间排序的需求。文中还会对比数据库方案与缓存方案的优缺点,分析缓存与数据库双写的一致性处理方式,给出可直接使用的代码示例和注意事项,帮助你搭建高性能的关注关系模块。

关注关系几乎是所有内容型产品的标配功能:微博的关注与粉丝、B站的UP主粉丝团、直播间的关注主播、电商店铺的收藏关注,底层都是同一套数据模型。这类数据的特点很鲜明:写多读也多、查询模式固定、数据量随用户规模呈爆炸式增长。一个千万粉丝的头部博主,粉丝列表如果直接落在MySQL里分页查询,深分页的性能会非常糟糕。所以绝大多数中大型系统都会把关注关系的核心读写路径放到Redis里,本文就来详细拆解这套方案的设计与实现。

Redis如何实现关注关系与粉丝列表?双集合方案详解

一、数据结构设计:双Set方案

关注关系本质上是两张表:正表(我关注了谁)和反表(谁关注了我)。在Redis里对应的也是两个集合。约定两个Key的设计规则:follow:{userId}存放用户关注的人,fans:{userId}存放关注该用户的粉丝。当用户A关注用户B时,需要同时执行两个SADD操作;取消关注时对应执行两个SREM。这种双向写入保证了任意方向的查询都能O(1)复杂度命中。

选用Set而不是Hash或List是有原因的。Set天然去重,重复关注不会产生脏数据;SISMEMBER判断关系的时间复杂度是O(1),非常适合“我是否关注了TA”这类高频查询;SINTER可以直接求两个用户的共同关注,这是List和Hash都做不到的。如果使用ZSet替代Set,还能额外获得按关注时间排序的能力,后面会展开讲。

// 用户A关注用户B
func Follow(rdb *redis.Client, uidA, uidB int64) error {
	pipe := rdb.Pipeline()
	pipe.SAdd(ctx, fmt.Sprintf("follow:%d", uidA), uidB)
	pipe.SAdd(ctx, fmt.Sprintf("fans:%d", uidB), uidA)
	_, err := pipe.Exec(ctx)
	return err
}

// 判断A是否关注了B
func IsFollowing(rdb *redis.Client, uidA, uidB int64) (bool, error) {
	return rdb.SIsMember(ctx, fmt.Sprintf("follow:%d", uidA), uidB).Result()
}

有一个细节需要注意:判断“是否关注”时要查follow集合,判断“是否是我的粉丝”时要查fans集合,不要混用。有些初学者习惯用SISMEMBER fans:{A} B来回答“A是否关注了B”,这在单向关注的产品里语义就反了。另外双向写入必须放在同一个Pipeline或事务里,避免出现A的关注列表有B、但B的粉丝列表没有A这种不一致状态。

二、核心接口的实现

关注系统对外通常暴露四类接口:关注、取关、粉丝列表分页、共同关注。前两个上面已经给出示例,这里重点说粉丝列表分页。Set本身是无序的,SMEMBERS虽然能拿全量但无法分页,Redis 6.2之后提供了SSCAN的游标遍历,可以配合COUNT参数做分页,但SSCAN返回的顺序不保证稳定,翻页时可能出现重复或遗漏。如果业务对顺序没有要求(比如粉丝广场随机展示),SSCAN足够用;如果要求按关注时间倒序展示“最新粉丝”,就应该换用ZSet。

用ZSet实现时,member存粉丝ID,score存关注时间戳,ZRANGEBYSCORE或者ZRANGE配合REV参数就能实现稳定的时间序分页,用上一页最后一条的score做游标即可,天然避免深分页问题。共同关注用SINTER可以直接算出我和某个用户共同关注的人,如果要扩展到“你关注的人里也关注了TA”,则需要组合SINTERSTORE先把中间结果落到临时Key,再做后续判断,记得用完之后DEL掉临时Key,并给它设置过期时间兜底。

// ZSet方案:按时间倒序取粉丝列表,lastTs为上一页末尾的时间戳
func FansByTime(rdb *redis.Client, uid int64, lastTs float64, size int64) ([]string, error) {
	key := fmt.Sprintf("fans:z:%d", uid)
	return rdb.ZRevRangeByScore(ctx, key, &redis.ZRangeByScore{
		Max:    fmt.Sprintf("(%f", lastTs), // 严格小于lastTs
		Offset: 0,
		Count:  size,
	}).Result()
}

// 共同关注
func CommonFollow(rdb *redis.Client, uidA, uidB int64) ([]string, error) {
	return rdb.SInter(ctx,
		fmt.Sprintf("follow:%d", uidA),
		fmt.Sprintf("follow:%d", uidB)).Result()
}

分页游标用score而不用offset,这一点很重要。粉丝量达到百万级时,offset方式的ZRANGE会随着翻页深度线性变慢,而score游标始终只扫size个元素,性能恒定。前端需要把每页最后一条的时间戳带回作为下一页参数,接口设计上多一个cursor字段即可平滑兼容。

三、缓存与数据库的一致性处理

Redis再快也是内存存储,宕机或主从切换时可能丢数据,所以关注关系通常仍然要落库作为最终存储,Redis承担加速层角色。推荐的写入顺序是:先更新MySQL,再更新Redis。如果反过来先写缓存再写库,一旦写库失败,缓存里就存在一条持久化的假关注,用户看到关注成功但数据库查无此记录,排查起来非常麻烦。先写库再写缓存,最坏情况只是缓存少了一条数据,可以通过补偿任务修复。

缓存更新失败的问题可以用几个手段兜底:给关注相关的Key设置合理过期时间,让不一致窗口有上限;后台定时任务对账,抽样比对数据库与Set的基数差异,发现差异后以数据库为准修复缓存;写操作使用重试队列,缓存操作失败时投递到消息队列异步重试。还有一种更彻底的方案是只写数据库,缓存全部通过订阅binlog(比如用Canal)来异步刷新,业务代码完全不用关心缓存一致性,代价是架构复杂度上升,适合团队已有binlog订阅基础设施的情况。

大V粉丝量的问题也值得提前考虑。百万粉丝的大Key在做迁移、过期删除时可能阻塞Redis,建议粉丝集合按粉丝ID取模拆分成固定数量的子Key,例如fans:{uid}:0到fans:{uid}:15共16片,读取时先根据粉丝ID定位分片再查询。拆分会增加判断关系的成本,需要权衡使用,普通用户不拆、超过阈值才拆的动态策略是比较常见的折中做法。

四、方案对比与选型建议

纯数据库方案的优点是数据强一致、支持任意维度的复杂查询和统计,缺点是高并发下扛不住,深分页慢,热点用户的粉丝查询容易拖垮库。纯Redis方案性能极好,接口简洁,但存在数据丢失风险且不支持模糊统计。生产环境的主流选择是数据库为真源、Redis为读加速的混合架构,关注和取关这类写操作双写,关系判断和列表读取全部走缓存,命中率高的场景下数据库压力可以忽略不计。

还有一个容易被忽略的边界:互相关注(好友关系)。实现上只需要在关注成功后判断SISMEMBER follow:{B} A是否成立,成立则双方互关,可以额外维护一个friend:{uid}集合,或者直接在业务层用两次判断实现。取关时同样要同步清理互关状态。把互关判断做在写路径上而不是读路径上,可以避免每次查询好友列表时的双倍请求量。整体来看,这套双集合加ZSet的方案在绝大多数社交场景里都够用,真正的难点不在数据结构,而在于缓存一致性、大Key治理这些工程细节上,把这两块设计好,关注模块就能稳定支撑高并发了。

Redis关注关系粉丝列表Set集合修改时间:2026-09-15 14:50:45

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260915/57339.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。