当前位置: 首页
AI教程
扛住关注功能高并发:Redis ZSet+Lua+MQ异步落库一整套方案

扛住关注功能高并发:Redis ZSet+Lua+MQ异步落库一整套方案

时间:2026-08-04
转载

高并发关注功能采用RedisZSet存储关注关系,利用Lua脚本保证原子操作;通过MQ异步落库,并按Tag分类消费;消费端引入令牌桶限流保护数据库,有效实现削峰填谷,显著提升系统稳定性与高并发处理能力。

关注功能高并发有多难扛?2026年8月实测,适用于 Redis 6.0 / RocketMQ 4.9。本文完整梳理了「Redis 缓冲 → ZSet 存关系 → Lua 保原子 → MQ 异步落库 → 令牌桶削峰」整套方案,照着这套思路做高并发设计,能少走不少弯路。

一、直接写库,关注接口为什么扛不住

先说结论:关注功能的高并发瓶颈,从来不在功能本身,而在你把高频写操作直接怼给了数据库。

关注这个动作拆开看,一次请求至少要干三件事:往关系表插入一条记录、给对方的粉丝数+1、给自己维护的关注列表加一个成员。很多项目第一版就是这么干的——一个事务里同步写完这三步。用户量一上来,问题全暴露了:

  • 关系表频繁 insert,索引不断膨胀,写入越来越慢;
  • 高并发下「插入+计数」不是原子的,粉丝数可能对不上账;
  • 数据库 IO 是有上限的,瞬时洪峰一来直接打穿。

经典的故障案例就是「关注接口双写 Redis + DB」:请求来了先写 MySQL 再写 Redis,并发一上来,两边数据就开始打架——有人取关成功了,Redis 里关系还在;有人关注重复提交,计数翻倍。\

说白了,关系数据本身不难存,难的是让数据库别被高频写压垮。所以整套方案的核心思路只有一个:把「高频写」前置到 Redis,用 MQ 把落库这件事切成异步,数据库只吃它扛得住的量。

二、Redis 缓冲层:把关系放进 ZSet

Redis ZSet(Sorted Set,有序集合):Redis 的一种数据结构,每个成员带一个 score 分值,成员按 score 自动排序。你可以理解为「带时间戳的收藏夹」——既能去重判断存没存过,又能按时间倒序翻页。

关注关系选 ZSet 而不是 Set,就一个原因:关注列表天然需要按时间排序。「最近关注了谁」是社交产品里最常见的查询,Set 只能存成员、排不了序,ZSet 用关注时间当 score,ZRANGE 一查就是按时间排好的列表。

存储模型是双向两份:

following:{用户ID} member=被关注人ID,score=关注时间戳
follower:{用户ID} member=粉丝ID,score=关注时间戳

关注时间做 score,取出来天然就是一条时间线,就像朋友圈按发布时间排。注意看这张概念图里的时间圆点——score 就是那条看不见的排序轨道:

ZSet 里 score 是排序轨道,时间戳当 score 就能按时间查

这样设计的好处是:

  • 插入高效:ZADD 复杂度 O(log N),时间戳做 score,天然有序;
  • 查询高效:ZRANGE 分页查关注/粉丝列表、ZSCORE 判断是否已关注,都是毫秒级;
  • 取关干净:ZREM 一条命令删掉关系,两个 key 一起维护。

关注功能高并发架构图:请求进入 Redis 缓冲层,Lua 原子更新 ZSet,异步发送 MQ 由消费端令牌桶限流后落库

上面这张图就是完整链路:关注接口收到请求 → Lua 脚本原子更新 Redis → 发 MQ → 消费者令牌桶限流 → 异步落库。下面逐个环节拆。

三、Lua 脚本:高并发下 Redis 更新也要原子

Lua 脚本:Redis 内置的脚本语言,脚本会整段在 Redis 服务端原子执行——执行期间不会被其他命令插入。你可以理解为「Redis 内的一次性原子事务」。

很多人以为写了 Redis 就万事大吉,其实高并发下 Redis 的「检查+更新」也必须用 Lua 串成一步。看这段伪代码,是不是很眼熟:

-- 关注:判断+插入+计数,三条命令
local exists = redis.call('zscore', KEYS[1], ARGV[1])
if exists then return 1 end  -- 已关注
redis.call('zadd', KEYS[1], ARGV[2], ARGV[1])
redis.call('hincrby', 'follow:count', ARGV[1], 1)
return 0

如果这三步分开执行,并发下就会出问题:两个请求同时判断「没关注」,然后各插一次、计数加两次。生产环境就踩过这个坑——上线当天关注数虚高,排查半天发现就是计数器被并发叠加了。\

用 Lua 把「判断是否已关注 → 插入 ZSet → 更新计数」合并成一步,Redis 单线程保证整段脚本不会被打断,原子性天然解决。关注和取关各写一个脚本,eval 一次调用就完成,性能比拆分命令还好。

这个「合并成一步」就是 Lua 原子性的精髓——三条散落的命令,被串成一把钥匙上的三个齿,要么一起动,要么都不动:

Lua 脚本 = 把「判断、更新、计数」锁进一把钥匙,原子完成

四、发 MQ 异步落库:打 Tag 精确消费

MQ(Message Queue,消息队列):一个中转消息的中间件,生产者发消息、消费者异步取消息,两边解耦。你可以理解为「业务之间递纸条的收发室」——不是当面交接,而是放进信箱异步处理。

Redis 只是缓冲层,不是持久层。关系最终要落到数据库,但绝不能同步落。正确做法是:Redis 更新成功后,立即发一条消息到 MQ,接口直接返回「关注成功」,剩下的落库交给消费者慢慢做。

这里有个细节值得展开——发消息要打 Tag。以 RocketMQ 为例,同一个 Topic 下按业务场景打不同标签:

// 生产端:关注/取关打不同 Tag
Message msg = new Message("FOLLOW_TOPIC",   // Topic
    isFollow ? "FOLLOW" : "UNFOLLOW",       // Tag 标签
    userId + ":" + targetId                 // 业务数据
);
producer.send(msg);

消费者订阅时按 Tag 过滤,只处理自己关心的消息:

consumer.subscribe("FOLLOW_TOPIC", MessageSelector.byTag("FOLLOW || UNFOLLOW"));

为什么要打 Tag?三个理由:消费端能精确过滤,不用消费全部消息再业务层判断;方便分场景限流,关注和取关可以配置不同的处理速率;排查问题时定位快,一看 Tag 就知道这条消息是什么业务。

消息队列削峰的核心价值就在这:接口侧把瞬时洪峰甩给 MQ,MQ 像一个水库,把洪峰的水蓄住,再按下游能承受的流速放出去。这就是常说的削峰填谷。

你看这张图就明白了——上面是劈头盖脸的暴雨(洪峰),下面是稳稳的一条小溪(落库速率),中间的 MQ 就是那个水库闸门:

MQ 是水库,接口是暴雨,数据库是小溪——削峰填谷全靠这个闸门

五、消费端令牌桶:数据库只吃它吃得下的量

令牌桶(Token Bucket):一种限流算法,系统按固定速率往桶里放令牌,请求要先拿到令牌才能处理,桶满了就丢弃新令牌。你可以理解为「景区按固定频率放人进门的闸机」——人再多,进场的速率是定死的。

到消费端这一步,MQ 里已经堆了一大堆落库消息。如果消费者一股脑全拉出来写库,等于把洪峰从接口侧平移到了数据库侧,前面白干了。所以消费端必须限速,这就是令牌桶上场的地方。

这里用的是 Redis 实现的分布式令牌桶:消费者每处理一条消息前,先 eval 一个 Lua 脚本拿令牌,拿到才继续,拿不到就稍等重试。核心脚本长这样:

local bucket = 'token:' .. KEYS[1] -- 桶名
local tokens = tonumber(redis.call('get', bucket) or '0')
local capacity = tonumber(ARGV[1]) -- 桶容量
if tokens >= 1 then
    redis.call('decr', bucket)
    return 1 -- 拿到令牌
end
return 0 -- 桶空,稍后再试

配合一个定时任务按固定速率补令牌,消费速率就被稳稳控住了。数据库的写入 TPS 从「不可控的洪峰」变成「恒定的 500」,压力直接可量化。

限流方案是否支持突发实现复杂度适用场景
固定窗口否,瞬间可能双倍低粗粒度限流
滑动窗口部分中要求平滑
令牌桶是,桶存突发量中削峰填谷、消费端限速
漏桶否,恒定速率低强制匀速

对比下来,消费端削峰选令牌桶最合适:平时流量平稳时桶是满的、不排队;活动洪峰时桶蓄不住的部分直接丢弃或排队,数据库永远只看到固定速率。实际调参时,桶容量按「数据库能承受的最大 TPS × 3」设置,既给了突发余量,又不至于把库打爆。

FAQ

六、总结:这套方案的核心思路

回顾一下整套链路:关注功能高并发设计,核心就一句话——Redis 当缓冲层挡住高频写,ZSet 存关系保排序,Lua 脚本把判断和更新合成原子一步,MQ 异步承接落库并打 Tag 分场景,消费端令牌桶把数据库压在恒定速率。五个环节各司其职,缺一个都容易在洪峰时出岔子。

以踩过坑的经验来看,新手最容易漏的是第二环——以为 Redis 天然原子,结果计数器被并发打爆。原子性不会因为数据进了 Redis 就自动有,该用 Lua 还得用。

关注接口高并发设计自查清单:

  • Redis 缓冲:关系写入全部前置 Redis;
  • ZSet 双向存储:following / follower 各一份;
  • Lua 原子:判断+插入+计数合并一个脚本;
  • MQ 异步:发消息打 Tag,接口立即返回;
  • 令牌桶限流:消费端限速落库,保护数据库。

这套方案同样适用于点赞、收藏、订阅这类「高频写关系」功能——把场景换一下,思路是通的。这套关注功能高并发方案在你那边落地时,建议先把量级估清楚:预估并发 1 万以内,简化到「Redis + 定时任务」也能扛;真正到了活动级洪峰,再上 MQ + 令牌桶不迟。如果想深入,推荐看 Redis 官方文档的 Lua 脚本章节,和 RocketMQ 的消息过滤文档(搜:Redis eval 文档、RocketMQ Tag 过滤)。

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
CAD零基础入门教程:坐标输入、图层管理与基础绘图命令

CAD零基础入门教程:坐标输入、图层管理与基础绘图命令

本文面向CAD零基础学习者,系统讲解坐标输入、图层管理与基础绘图命令的核心用法。通过分步实操与常见问题排查,帮助新手建立精确绘图习惯,掌握规范出图的基础能力。

时间:2026-09-01 16:53
CAD从入门到项目交付:绘图、标注、图块与实战工作流

CAD从入门到项目交付:绘图、标注、图块与实战工作流

掌握CAD的核心在于建立“画得准、标得清、复用快、交付稳”的工作流。本文提供从环境设置、高频命令组合、标注规范、图块标准化到项目分阶段交付的完整路径,帮助初学者避免常见返工陷阱,独立完成可检查、可复用、可打印的工程图纸。

时间:2026-09-01 16:52
Claude Code 登录指南:个人、Teams 与企业账号区分与授权步骤

Claude Code 登录指南:个人、Teams 与企业账号区分与授权步骤

本文详细解析 Claude Code 登录前的账号类型区分方法,涵盖个人订阅、Teams 席位与企业 Enterprise 席位的授权路径差异。提供终端登录命令、环境变量排查及常见异常处理步骤,帮助用户快速完成正确授权并避免登录路径混淆。

时间:2026-09-01 14:27
Claude Code 文件修改前的权限模式配置与命令审批指南

Claude Code 文件修改前的权限模式配置与命令审批指南

本文详细介绍Claude Code在修改文件前的权限模式配置方法,包括defaultMode可选值、permissions allow与deny规则设置、多层级配置文件管理以及 status验证技巧,帮助开发者安全高效地使用AI编程助手。

时间:2026-09-01 14:12
Claude Code接入VS Code后先测扩展和终端命令

Claude Code接入VS Code后先测扩展和终端命令

在VS Code中接入Claude Code后,建议优先验证扩展面板与集成终端两条入口。本文提供标准检查顺序、关键命令与常见故障排查路径,帮助你快速确认环境就绪,避免后续开发受阻。

时间:2026-09-01 14:10
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全