[TOC] #### 1. 前言 --- 上一讲我们知道了:RabbitMQ 可能会投递重复消息,这一讲我们来解决它,但在讲方案之前,先看一个事故 线上电商系统,用户下单购买了一件商品,价格 100 元,消息被重复投递了两次 消费者的处理逻辑: ```php $callback = function ($msg) { $data = json_decode($msg->body, true); // 扣库存 DB::table('goods')->where('id', $data['goods_id'])->decrement('stock', 1); // 加积分 DB::table('users')->where('id', $data['user_id'])->increment('points', 100); $msg->ack(); }; ``` 第一条消息:扣了 1 件库存,加了 100 积分,第二条消息:又扣了 1 件库存,又加了 100 积分 用户买了一件商品,库存扣了两件,积分多了 100(用户赚麻了,老板亏麻了) 为什么 RabbitMQ 保证了可靠还会这样 ? 上一讲说过,重复消息的来源有很多:Producer 重发、ACK 丢失、消费者重启 RabbitMQ 的职责是把消息送到,至于「这条消息之前处理过了」RabbitMQ 不知道 它不知道你的业务逻辑,不知道「扣库存」应该只扣一次 所以你需要自己保证:同一条消息不管收到几次,结果都一样,这就是 幂等性 #### 2. 幂等性 --- 用一句话解释:同一条消息执行一次和执行十次,结果一样 比如: + 扣库存 1 件,执行 1 次和 10 次,都是扣 1 件 ✅ 幂等 + 发送短信,执行 1 次和 10 次,用户收到 10 条 ❌ 不幂等 怎么实现幂等性 ? 方案一:唯一业务 ID 每条消息带一个唯一标识,比如订单号,消费者处理之前,先查一下这个标识有没有处理过 + 简单直接,适合大多数场景,缺点是需要额外建一张去重表 ```php $callback = function ($msg) { $data = json_decode($msg->body, true); $orderId = $data['order_id']; // 检查是否已经处理过 $exists = DB::table('processed_messages')->where('message_id', $orderId)->exists(); if ($exists) { echo "消息 {$orderId} 已处理过,跳过\n"; $msg->ack(); return; } // 处理业务 processOrder($data); // 记录已处理 DB::table('processed_messages')->insert([ 'message_id' => $orderId, 'processed_at' => date('Y-m-d H:i:s') ]); $msg->ack(); }; ``` 方案二:数据库去重(利用唯一索引) 如果业务本身有唯一约束,可以直接利用,比如「扣库存」,可以用订单号做唯一索引: + 插入的时候如果重复,数据库会报唯一键冲突,直接跳过就行 + 不需要额外的去重表,利用业务本身的约束 ```sql CREATE TABLE order_stock_log ( order_id BIGINT UNIQUE, goods_id INT, quantity INT, created_at DATETIME ); ``` 方案三:状态机判断 如果订单有状态流转,可以用状态来判断,比如订单状态是 待付款 → 已付款 → 已发货: ```php $callback = function ($msg) { $data = json_decode($msg->body, true); // 只有待付款的订单才能变成已付款 $affected = DB::table('orders') ->where('id', $data['order_id']) ->where('status', 'unpaid') ->update(['status' => 'paid']); if ($affected === 0) { echo "订单状态不对,跳过\n"; $msg->ack(); return; } // 后续业务逻辑 processAfterPayment($data); $msg->ack(); }; ``` 如果消息重复,第二次 update 时 status 已经不是 unpaid 了,affected 为 0,直接跳过 利用数据库的行锁和状态判断,天然防重复 三种方案怎么选 ? 实际项目中,这三种方案经常组合使用,核心思路就一个:处理之前先判断「我处理过没有」 | 方案 | 适用场景 | 优点 | 缺点 | | ------------ | ------------ | ------------ | ------------ | | 唯一业务 ID | 通用场景 | 简单直接 | 需要去重表 | | 数据库去重 | 业务有唯一约束 | 不需要额外表 | 只适合特定场景 | | 状态机判断 | 有状态流转的业务 | 天然防重复 | 需要设计好状态 | #### 3. 本文小结 --- 幂等性的核心思想:同一条消息执行一次和执行十次,结果一样 三种常用方案: + 唯一业务 ID + 去重表 + 数据库唯一索引 + 状态机判断 RabbitMQ 不会帮你做幂等,这是业务层的事情,但只要你做好了幂等,重复消息就不会造成业务问题 下一讲,我们来聊一个排查问题的工具:消息追踪