当前位置: 首页 > news >正文

细节补充第一篇:RocketMQ 的使用

本文是秒杀系统系列的细节补充,聚焦 RocketMQ 在实战中的核心问题:三个端到底是什么?异步削峰从 3000 QPS 到 800 TPS 是怎么做到的?处理流控的到底是谁?TPS 是越高越好吗?

一、RocketMQ 的三个端到底是什么?

【疑问】

都说 RocketMQ 有生产者、服务端、消费者三个部分。那“服务端”是啥?是和我代码写在一起的一段逻辑,还是一个独立的服务?它有端口号吗?生产者消费者又是怎么跟它通信的?

【回复】

这个问题问到根上了。很多人用了半年 RocketMQ,都以为它就是几行@RocketMQMessageListener注解的事。

真相是:服务端(Broker)是一个完全独立的 Java 进程,有自己的端口号(默认 10911),需要单独部署。

三个端的真实关系是这样的:

生产者(你的 Spring Boot 应用) │ │ TCP 长连接 ↓ 服务端(Broker,独立进程,端口 10911) │ │ TCP 长连接(长轮询) ↓ 消费者(你的 Spring Boot 应用)

生产者消费者,是你代码里引入的 RocketMQ 客户端库(一个 jar 包)。它运行在你的 Spring Boot 进程里,没有独立端口号,但它内部有自己的线程池、连接池,专门负责和服务端通信。

服务端是你需要单独部署的。如果是 Docker 部署,就是一个独立容器;如果是裸机部署,就是一个 Java 进程。配置文件叫broker.conf,里面可以配置 Topic 的默认队列数、持久化策略等。

关键认知:三者之间的每一次交互——发消息、拉消息、心跳检测——都是真实的 TCP 网络请求。只是因为用了长连接,建连的开销只发生一次,后续通信都非常快。

二、异步削峰从 3000 QPS 到 800 TPS 是怎么做到的?

【疑问】

你说秒杀接口能扛 3200 QPS,但数据库只能扛 800 TPS。这中间的 2400 请求去哪了?不是说数据库行锁只能串行吗,那 800 TPS 又是怎么算出来的?

【回复】

这个问题,我用一个真实的时间账本来回答。

同步下单为什么必死?

假设秒杀接口同步写数据库,一次下单要做什么?

阶段耗时说明
Redis Lua 扣减库存10ms内存操作,很快
获取数据库连接60ms连接池只有 200,3000 并发下大量排队
数据库事务执行40ms同一商品行锁,UPDATE 串行执行
网络传输等10ms
总耗时120ms

这就是我压测同步版本得出的真实数据:120ms

瓶颈在哪?获取连接等了 60ms,事务执行等了 40ms。这两项加起来 100ms,占了总时间的 83%。

异步削峰后发生了什么?

引入 RocketMQ 后,接口逻辑变成:

Redis Lua 扣减库存(10ms) ↓ 发送 RocketMQ 事务消息(5ms) ↓ 立即返回“排队中” (网络开销 10ms)

接口总耗时:25ms。

经优化,节省了 95ms。原本 100ms 的同步操作数据库,变成了异步操作,只留下了发 MQ 的 5ms 开销,压力从数据库转移到了后台消费者那里。

3000 QPS 和 800 TPS 是怎么关联的?

那 40ms 的事务操作并没有消失,只是从“接口里同步执行”变成了“消费者异步执行”。

但这里有个关键设计:消费者的第一件事不是扣 MySQL 库存,而是只插入一条预订单记录。库存扣减在 Redis 里已经完成了,MySQL 库存等到用户真正支付后才扣。

消费者配置:

  • 消费线程数:8
  • 每条消息处理时间:10ms(只 INSERT 一条预订单,含网络往返和磁盘写入)

计算公式

消费者 TPS = 并发线程数 / 每条消息处理时间 = 8 / 0.01s = 800 TPS

所以:

  • 接口层:3200 QPS(Redis 扛住了)
  • 数据库层(消费者插入预订单):800 TPS,3000 条消息约 3.75 秒落库
  • 数据库层(支付后扣库存):由行锁控制,但支付是非瞬时行为,天然分散

这就是削峰填谷的本质:先用 Redis 挡住峰值,后用 MQ 排队,消费者只做最轻量的插入操作,把重操作留到支付环节。

耗时数据来源:同步版本和异步版本的接口耗时,通过 Arthas 的trace命令做了方法级耗时追踪,同时用 JMeter 聚合报告做端到端验证,两者结论一致。trace com.pangxuan.service.impl.SeckillOrderServiceImpl executeSeckill显示 Redis 扣减约 10ms,MQ 发送约 5ms,网络开销约 10ms。

三、处理流控的到底是谁?

【疑问】

你说消费者是“匀速消费”的,但消费线程是 8 个,消息有 3000 条,凭什么它们不会一拥而上,又把连接池打满?到底是谁在控制速度?

【回复】

这个问题我刚开始也想错了。我以为消息队列“流控机制”就是 8 个消费线程并发,同一时间只有 8 个消息被消费。后来才发现,真正限速的不是 MQ 的并发线程数,而是消费者的“拉取-消费”机制和数据库的行锁。

拉取和消费是两个独立的线程池

很多人(包括最初的我)以为消费线程既负责拉消息又负责处理。不是的。

消费者的真实结构是这样的:

RocketMQ 消费者客户端(运行在你的 Spring Boot 进程里) │ ├── 拉取线程池(1~2 个线程) │ └── 负责从 Broker 批量拉取消息 │ ├── ProcessQueue(本地缓冲队列,与 MessageQueue 一一对应) │ └── 消费线程池(8 个线程,你配置的) └── 从 ProcessQueue 取消息,调用 onMessage 方法

拉取线程只管一件事:从 Broker 批量拿消息,丢到 ProcessQueue 里。
消费线程只管一件事:从 ProcessQueue 取消息,执行业务逻辑。

它们是异步并行的,互不阻塞。

真正限速的三大机制

第一层:ProcessQueue 的锁

一个 ProcessQueue 同一时刻只能被一个消费线程持有。配置了 8 个 MessageQueue,就有 8 个 ProcessQueue,最多 8 个线程同时工作。

这就像一个餐厅有 8 个灶台,雇了 8 个厨师,刚好每人一个灶台,全部可以同时做菜。

第二层:数据库连接池

消费线程执行onMessage时,需要从数据库连接池借连接。数据库连接池上限 200,消费线程只有 8 个,连接完全够用,不会成为瓶颈。

第三层:数据库行锁(在本方案中不是瓶颈)

因为消费者只做 INSERT 预订单,不同订单之间完全不冲突,不存在行锁竞争。真正的行锁出现在支付后的库存扣减环节,但那已经是另一个低并发场景了。

所以,消费者能“匀速”,核心是 ProcessQueue 的锁机制 + 线程数配置,共同制造了一个稳定的并发处理节奏。

四、3000 条消息是怎么被一步步削峰的?

【疑问】

能给一个完整的 3000 条消息从发送到消费的完整流程吗?每一步发生了什么?

【回复】

第一幕:生产者发送(秒杀接口)

3000 个秒杀请求 ↓ Redis Lua 扣减库存(10ms/条) ↓ rocketMQTemplate.sendMessageInTransaction() ↓ 发送 3000 条消息到 Topic: seckill-order-topic ↓ 接口返回“排队中”(总耗时 25ms)

第二幕:Broker 存储

Broker 收到 3000 条消息 ↓ 轮询写入 8 个 MessageQueue ├── Queue-0: 375 条 ├── Queue-1: 375 条 ├── ... └── Queue-7: 375 条 ↓ 持久化到磁盘(同步刷盘/异步刷盘) ↓ 等待消费者来拿

第三幕:消费者拉取

消费者客户端启动 ↓ 负载均衡:8 个消费线程分配 8 个队列,每个线程独占一个队列 ↓ 拉取线程(1~2个): ├── 从 Queue-0 拉取 32 条 → 放入 ProcessQueue-0 ├── 从 Queue-1 拉取 32 条 → 放入 ProcessQueue-1 ├── ... └── 从 Queue-7 拉取 32 条 → 放入 ProcessQueue-7 ↓ 第一批 256 条消息进入本地缓冲

第四幕:消费者处理

ProcessQueue 加锁: ├── ProcessQueue-0 → 线程1 获得锁 → 逐条消费 ├── ProcessQueue-1 → 线程2 获得锁 → 逐条消费 ├── ... └── ProcessQueue-7 → 线程8 获得锁 → 逐条消费 每个线程里只做一件事: @Transactional public void createPreOrder(Message msg) { INSERT INTO pre_order (...) VALUES (...); -- 10ms } 事务耗时:10ms 8 个线程并发 TPS = 8 / 0.01s = 800 3000 条消息总耗时 ≈ 3.75 秒

第五幕:超时补偿(如果用户不支付)

定时任务每 30 秒扫描一次 ↓ 查出 pre_order 中 status=待支付 且超过 30 分钟的记录 ↓ DELETE FROM pre_order WHERE id = ? ↓ 补偿 Redis 库存: INCR seckill:stock:{detailId} ↓ 通知用户“订单超时,库存已释放”

注意:这里的补偿是主动调用的,不是依赖 Redis 自动过期。Redis 的 Key 虽然有 TTL,但秒杀库存是核心数据,不能等它自动过期——必须由定时任务精确控制补偿时机。

最终结果

3000 条消息,8 个队列并发消费 每条 10ms,所以: 总耗时 = 375(每队列消息数)× 10ms = 3.75 秒 数据库 TPS = 8 / 0.01s = 800 TPS 消费者完全不构成瓶颈。 真正的瓶颈在 Redis 扣减(10ms/条 → 单机 Redis 约 10 万 QPS,也绰绰有余)。

五、TPS 是越高越好吗?(关键权衡)

【疑问】

既然修改并发队列和消费线程能提高 TPS,那我把线程调到 100,TPS 飞到天上去了,不是更好吗?

【回复】

不是。TPS 不是越高越好,而是“匹配业务需求”最好。

你把消费线程调到 100 会发生什么?

配置 100 个消费线程 ↓ 100 个线程并发消费 ↓ 插入预订单(不同订单,无行锁冲突) ↓ TPS = 100 / 0.01s = 10000 TPS

数字确实飞上天了,但代价是什么?

代价一:CPU 上下文切换爆炸

你现在是 8 核 CPU,100 个线程在跑。每个线程执行 10ms 的数据库操作,其中 9ms 在等 IO。100 个线程中同时有约 90 个在等待 IO,10 个在执行计算。

操作系统为了让这 100 个线程“看起来”都在执行,需要不断切换 CPU 的上下文。每次切换都有开销(保存寄存器、加载新线程状态等)。

8 核跑 8 线程:切换开销几乎为 0 8 核跑 100 线程:每秒上下文切换可能达到几十万次 CPU 大量时间花在“切换”上,而不是“干活”

结果:TPS 可能从 10000 降到 3000,甚至比 8 线程还差。

代价二:数据库连接池压力

你连接池配了 200。100 个线程同时执行,需要 100 个连接。虽然 100 < 200,但你还有其他业务请求也要用连接。

连接池 200 消费线程 100 → 占 100 个连接 其他业务请求 → 剩 100 个连接可用 如果是高峰,连接池可能被打满, 其他业务(如查询课程、用户登录)全部挂掉。

代价三:Redis 主线程压力

虽然消费者不扣 Redis,但插入预订单时可能有一些 Redis 查询操作(如查用户信息、校验幂等)。100 个线程并发访问 Redis,虽然 Redis 单线程扛得住,但网络带宽和连接数也会成为瓶颈。

所以 TPS 的“最优值”是什么?

TPS 最优值 = 刚好满足业务需求,同时不给系统其他部分添麻烦的值。

秒杀接口 QPS:3200 Redis 扣减后,发到 MQ 的消息量:假设 80% 扣减成功 → 2560 条 消费者消费速度:800 TPS 全部消费完成时间:2560 / 800 ≈ 3.2 秒

3.2 秒消费完 2560 条消息,对用户体验来说完全够了。预订单创建后,前端 3 秒轮询一次就能查到订单,用户感知不到延迟。

如果调到 100 线程,消费时间缩短到 0.25 秒

有意义吗?没有。

因为前端轮询间隔是 3 秒,你 0.25 秒消费完和 3.2 秒消费完,用户感知是一样的——都是第一次轮询就看到订单。

多出来的 92 个线程,就是在浪费 CPU、浪费连接、浪费内存。

面试时怎么回答这个问题

面试官:“TPS 是越高越好吗?你为什么不把线程调到 100?”

:“不是越高越好,而是匹配业务的需求和系统的物理资源。我设置 8 个线程是因为:

  1. 业务需求:3000 条消息,8 线程 800 TPS,3.75 秒消费完,前端 3 秒轮询一次刚好够用。再快用户也感知不到。
  2. 物理资源:我的服务器是 8 核,8 个线程刚好最大化利用 CPU,不产生上下文切换开销。
  3. 保护其他业务:连接池总共 200,如果消费占 100 个连接,其他正常查询、登录就受影响。

TPS 不是越高越好,匹配业务窗口期、匹配物理资源、不给其他模块添堵,才是好的设计。”

六、总结:这张图记下来,面试够用了

秒杀接口(3200 QPS) │ ▼ Redis(Lua 原子扣减) │ ┌─────┴─────┐ │ 扣减成功 │ 扣减失败 ▼ ▼ RocketMQ 事务消息 返回“已售罄” │ ▼ Broker(8 个 MessageQueue) │ ┌───────┴───────┐ ▼ ▼ 拉取线程批量拉取 ProcessQueue 本地缓冲 │ │ └───────┬───────┘ ▼ 消费线程池(8 个线程) │ └── INSERT pre_order(10ms,并发不冲突) │ ▼ 预订单落库(800 TPS) │ ┌───────┴───────┐ ▼ ▼ 用户支付 超时未支付 │ │ ▼ ▼ UPDATE stock 扣库存 定时任务删除预订单 │ │ ▼ ▼ 订单完成 补偿 Redis 库存

四个关键数字

  • 接口层:3200 QPS(Redis 扛的)
  • 消息队列:3000 条排队
  • 消费者插入预订单:800 TPS(8 线程,10ms/条,3.75 秒落库)
  • 支付后扣库存:由行锁控制,但支付天然分散

一句话总结

RocketMQ 在秒杀系统里的作用,就是把 Redis 扛住的 3200 QPS 瞬时流量,用消息队列缓冲后交给 8 个消费者线程。消费者只做最轻量的预订单插入,10ms 一条,TPS 稳定在 800 左右,3 秒多全部落库。MySQL 库存扣减被移到了支付环节,避免了秒杀瞬间的写压力。TPS 不是越高越好,匹配业务窗口期、匹配物理资源、不给其他模块添堵,才是好的设计。

http://www.jsqmd.com/news/809264/

相关文章:

  • 手持式超声波流量计十大品牌:轻便与精准可否兼得? - 仪表人叶工
  • 一句话就能“劫持”你的AI?DZS 分层式自适应提示词注入攻击的防御机制框架 (HAA)来了!
  • Entroly:AI编程助手成本优化工具,让AI看见100%代码仅用5%token
  • SWD协议实战:从波形解析到寄存器读写全流程拆解
  • 2026 五家正规机构实测对比,上海包包回收哪家好? - 奢侈品回收测评
  • 2023B卷,荒岛求生
  • 观察使用Taotoken后月度账单明细与API调用成功率的变化
  • 如何深度优化PowerToys:专业中文界面的完整实战指南
  • 2026国内果酒TOP5!云南等地企业广受好评 - 十大品牌榜
  • 【SketchUp 2024】渲染前必调:六大样式设置详解,从边线优化到水印天空实战
  • 终极指南:如何使用RPG Maker Decrypter快速解密游戏资源
  • Windows安卓应用安装神器:APK Installer完全使用指南
  • AI写专著新突破!AI专著生成工具,3天完成20万字专著创作不是梦!
  • 观察Taotoken用量看板如何帮助我精细化控制API调用成本
  • 实战复盘:我是如何通过一个SSRF漏洞,利用Gopher协议拿下内网Redis的
  • 青岛鼎力信达起重设备租赁:靠谱的青岛吊车出租公司 - LYL仔仔
  • 揭秘Happy Island Designer:解锁你的岛屿设计超能力
  • 常州黄金回收哪里更透明?福正美用数据告诉你答案 - 福正美黄金回收
  • R语言数据分析革命:gptstudio集成GPT实现智能编程辅助
  • 技术解析:从多目标优化视角看多任务学习的帕累托最优解
  • 自动驾驶卡车软件平台:技术架构、核心玩家与商业化挑战
  • 从零构建Telegram群管机器人:Pyrogram+Telethon双框架实战指南
  • 如何为国际学校、教育集团选择校服定制供应商?评估整体解决方案的五大能力与四步流程 - 速递信息
  • 故障率降至0.1%:医用硅胶单向阀定制案例解析 - 速递信息
  • 京东物流第一季营收606亿:经调整净利10.5亿 拟斥资12亿美元回购
  • 纯铝排 导电铝排 铝排母线 6101铝排 接地扁铝厂家实测盘点:从工地配电到冷库的靠谱选择 - 奔跑123
  • ESP32-CAM图片上传踩坑实录:从Arduino环境配置到巴法云HTTP POST成功,我遇到的5个问题及解决办法
  • 当你的电脑被重复照片淹没时,这款智能工具如何拯救你的存储空间
  • 2026年乌鲁木齐太阳能路灯工程采购指南:本地源头工厂如何助力市政快速交付 - 优质企业观察收录
  • 别再死磕BERT了!用PyTorch从零搭建BiLSTM-CRF模型,搞定中文NER任务(附完整代码)