去重表与消费幂等是什么
消息投递通常是至少一次,重复不可避免,因此消费方必须幂等。最常见的实现是用去重表:把已处理的消息标识写进一张带唯一约束的表,重复投递时唯一键冲突即判定为已处理、直接跳过。关键在于去重记录与业务更新要放在同一个本地事务里,否则中间失败会造成「业务已改但标记没写」或「标记已写但业务没改」,两种情况都会破坏幂等。除消息外,这个模式也适用于回调、重试任务与人工补单。
基本信息
| 核心机制 | 唯一约束记录已处理标识,重复投递触发冲突即跳过 |
|---|---|
| 原子性 | 去重记录与业务更新在同一事务中提交或回滚 |
| 去重键 | 业务单号、消息 ID 或幂等键,需全局唯一且稳定 |
| 保留窗口 | 按最大重投递延迟设置并定期归档,避免无限增长 |
| 替代方案 | 业务表上的幂等键唯一索引;外部系统不支持事务时用状态机前置判断 |
典型使用场景
- 消费 MQ 消息:订单支付成功事件重复投递时只处理一次
- 接收 Webhook 回调:第三方重试导致同一事件多次到达
- 支付与退款:资金类操作必须防止重复扣款或重复退款
- 定时任务补偿:任务重启后重跑时按业务单号去重
- 批量导入:文件重复上传时不产生重复数据
- 人工修复工具:重投工具多次点击也不会造成重复业务发生
同事务去重示例
同事务去重示例
-- 去重表:msg_id 上建唯一索引
CREATE TABLE msg_dedupe (
msg_id VARCHAR(64) PRIMARY KEY, -- 消息唯一标识
created_at DATETIME NOT NULL, -- 首次处理时间,用于归档
biz_no VARCHAR(64) NOT NULL -- 业务单号,便于排查
);
BEGIN;
-- 唯一键冲突则回滚,视为已处理,直接返回成功
INSERT INTO msg_dedupe(msg_id, created_at, biz_no) VALUES (?, NOW(), ?);
UPDATE account SET balance = balance - 100 WHERE id = ?;
COMMIT; -- 两步必须同一事务,否则幂等仍会被破坏设计检查
| 检查项 | 要求 | 缺失后果 |
|---|---|---|
| 事务范围 | 去重记录与业务更新同事务 | 重复处理或漏处理,幂等失效 |
| 唯一约束 | 数据库中真实存在的唯一索引 | 并发插入两条记录,去重形同虚设 |
| 保留窗口 | 覆盖最大重投递延迟并定期归档 | 窗口过短漏拦,过长则表无限膨胀 |
| 并发控制 | 依赖数据库唯一约束而非先查后插 | 并发下先查后插仍会重复处理 |
| 观测 | 统计拦截次数与唯一键冲突率 | 重复投递量突增无人发现 |
去重表要设置保留窗口并定期归档:窗口过短会漏拦延迟重投的消息,窗口过长则存储与索引成本持续上升,需要按实际重投递延迟来定。
并发安全靠数据库唯一约束而不是先查后插:先查后插在并发下仍有竞态窗口,只有唯一索引能真正兜住重复。