资讯动态

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

发布时间:2026/9/10 0:27:15 来源:尧图企业网站定制
本文是秒杀系统系列的细节补充聚焦 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连接池只有 2003000 并发下大量排队数据库事务执行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 QPSRedis 扛住了数据库层消费者插入预订单800 TPS3000 条消息约 3.75 秒落库数据库层支付后扣库存由行锁控制但支付是非瞬时行为天然分散这就是削峰填谷的本质先用 Redis 挡住峰值后用 MQ 排队消费者只做最轻量的插入操作把重操作留到支付环节。耗时数据来源同步版本和异步版本的接口耗时通过 Arthas 的trace命令做了方法级耗时追踪同时用 JMeter 聚合报告做端到端验证两者结论一致。trace com.pangxuan.service.impl.SeckillOrderServiceImpl executeSeckill显示 Redis 扣减约 10msMQ 发送约 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那我把线程调到 100TPS 飞到天上去了不是更好吗【回复】不是。TPS 不是越高越好而是“匹配业务需求”最好。你把消费线程调到 100 会发生什么配置 100 个消费线程 ↓ 100 个线程并发消费 ↓ 插入预订单不同订单无行锁冲突 ↓ TPS 100 / 0.01s 10000 TPS数字确实飞上天了但代价是什么代价一CPU 上下文切换爆炸你现在是 8 核 CPU100 个线程在跑。每个线程执行 10ms 的数据库操作其中 9ms 在等 IO。100 个线程中同时有约 90 个在等待 IO10 个在执行计算。操作系统为了让这 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 最优值 刚好满足业务需求同时不给系统其他部分添麻烦的值。秒杀接口 QPS3200 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 个线程是因为业务需求3000 条消息8 线程 800 TPS3.75 秒消费完前端 3 秒轮询一次刚好够用。再快用户也感知不到。物理资源我的服务器是 8 核8 个线程刚好最大化利用 CPU不产生上下文切换开销。保护其他业务连接池总共 200如果消费占 100 个连接其他正常查询、登录就受影响。TPS 不是越高越好匹配业务窗口期、匹配物理资源、不给其他模块添堵才是好的设计。”六、总结这张图记下来面试够用了秒杀接口3200 QPS │ ▼ RedisLua 原子扣减 │ ┌─────┴─────┐ │ 扣减成功 │ 扣减失败 ▼ ▼ RocketMQ 事务消息 返回“已售罄” │ ▼ Broker8 个 MessageQueue │ ┌───────┴───────┐ ▼ ▼ 拉取线程批量拉取 ProcessQueue 本地缓冲 │ │ └───────┬───────┘ ▼ 消费线程池8 个线程 │ └── INSERT pre_order10ms并发不冲突 │ ▼ 预订单落库800 TPS │ ┌───────┴───────┐ ▼ ▼ 用户支付 超时未支付 │ │ ▼ ▼ UPDATE stock 扣库存 定时任务删除预订单 │ │ ▼ ▼ 订单完成 补偿 Redis 库存四个关键数字接口层3200 QPSRedis 扛的消息队列3000 条排队消费者插入预订单800 TPS8 线程10ms/条3.75 秒落库支付后扣库存由行锁控制但支付天然分散一句话总结RocketMQ 在秒杀系统里的作用就是把 Redis 扛住的 3200 QPS 瞬时流量用消息队列缓冲后交给 8 个消费者线程。消费者只做最轻量的预订单插入10ms 一条TPS 稳定在 800 左右3 秒多全部落库。MySQL 库存扣减被移到了支付环节避免了秒杀瞬间的写压力。TPS 不是越高越好匹配业务窗口期、匹配物理资源、不给其他模块添堵才是好的设计。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价