消息队列幂等方案

如果一个消息队列对某个消息发送了两次,导致入库两条相同的数据,如何避免

也即如何保证幂等性,接下来讨论的前提是全局唯一 ID

唯一索引

最简单的方案,先查 ID 在数据库里有没有,如果没有则入库

1
2
3
4
vo = select(id);
if (vo == null){
save(信息)
}

先查询,再判断,再保存,在多线程的情况下,还是拦不住

可以在数据表中加唯一索引方案解决,在数据库层面抛异常,来保证幂等

如果要保证少抛异常,看接下来的方案

Redis

为了保证在并发条件下,也只有一个请求到数据库,可以用锁的方式

以 id 为 key 加锁,如果加锁成功,则放行

1
2
3
4
flag = addRedisLock(id, 过期时间);
if (flag){
save(信息)
}

如果此时 Redis 锁成功了,但是还没 save,Redis 重启了,理论上是可以重试的,但是由于 Redis 锁的存在,导致不会走到 save 的逻辑

  • 人工介入,删除 Redis 对应的 key

  • 由于设置了过期时间,等过期时间过了之后的请求理论上还是可以拿到锁,所以加一个重试机制

1
2
3
4
flag = addRedisLock(id, 过期时间, 获取不到则等待);
if (flag){
save(信息)
}

由于有锁等待的逻辑,如果两个请求过来,还是有可能都放到 Redis 里,save 方法还是会走两遍,所以还需要加一个查询数据库的逻辑

1
2
3
4
5
6
7
8
9
10
flag = addRedisLock(id, 过期时间, 获取不到则等待);
if (flag){
vo = select(id);
if (vo == null){
save(信息);
}
}else {
// 等待结束后还是未获取到锁,发送预警
monitor(预警信息);
}

MySQL

最开始加唯一索引需求是根据业务表来做的,如果出现问题就让它抛出主键异常

如何设计一个通用技术组件,不依赖于业务场景

1
2
3
4
vo = select for update(id);
if (vo == null){
save(message);
}

也是加锁,但是这样性能就太差了

设计一个 消息消费记录表 专门为了解决幂等问题而存在的

1
2
3
if (保存数据到消息消费记录表){ // 出现主键冲突就返回 false
save(message);
}

先判断,再保存,非原子性,多线程情况下也不行

加入事务

1
2
3
4
5
begin transaction;
if (保存数据到消息消费记录表){ // 出现主键冲突就返回 false
save(message);
}
end transaction;

一般来说,能不用事务就不使用事务,通过最终一致性来保证数据完整性

所以可以在 消费记录表 中加一个状态字段,状态有两个取值:消费中,消费完成

同时把唯一索引改成 消息唯一标识 + 状态

首先 MQ 发起请求,数据往消费记录表中插入,状态是“消费中”
如果插入成功,说明第一次消费,进入到业务逻辑中

  • 如果业务逻辑执行成功,则更新消费记录表对应数据为“消费完成”
  • 如果业务执行失败,删除消费记录表对应的数据,把消息扔回 MQ,等待下次重试
    如果插入失败,说明重复消费,直接丢弃

其实插入失败这里还应该判断状态是否是消费成功的,如果是消费成功的,才应该丢弃


消息队列幂等方案
http://showyoubug.cn/2024/06/14/消息队列幂等方案/
作者
Dong Su
发布于
2024年6月14日
许可协议