消息的幂等性
T00 Lv2

幂等性是什么

幂等是一个数学概念,用函数表达式来描述是这样的:f(x) = f(f(x)),例如求绝对值函数。

放在业务中,幂等就是i同一条消息被重复消费多次,业务结果只会被执行一次,不会引发副作用(如重复扣库存、重复发优惠券、重复扣款等),一句话就是指同一个业务,执行一次或多次对业务状态的影响是一致的。

为什么要保证幂等性

但数据的更新往往不是幂等的,如果重复执行可能造成不一样的后果。比如:

  • 取消订单,恢复库存的业务。如果多次恢复就会出现库存重复增加的情况
  • 退款业务。重复退款对商家而言会有经济损失。

所以,我们要尽可能避免业务被重复执行。

但是RabbitMQ 或任何 MQ 都不能 100% 保证只投递一次或只消费一次,可能因为:

  1. 消息重复投递
  2. 消息可能重新进入队列
  3. 消息可能重复消费

等等原因….

所以在业务层保证幂等性显得尤为重要。

如何保证幂等性

数据库唯一约束

如果从MQ拿到数据是要存到数据库,那么可以根据数据(创建一个UUID)创建唯一约束;这样的话,同样的数据从MQ发送过来之后,当插入数据库的时候,会报违反唯一约束的异常,不会插入成功的。

  1. 每条消息携带唯一 ID(如 messageId、订单号)
  2. 数据库建表并为 messageId 设置唯一约束:
1
2
3
4
CREATE TABLE message_log (
message_id VARCHAR(64) PRIMARY KEY,
...
);
  1. 消费者处理消息时,尝试插入 messageId
  • 插入成功:说明是第一次处理 → 执行业务
  • 插入失败:违反唯一约束 → 表示已处理,跳过

消息唯一id

在生产者发送消息时,创建一个全局唯一id,在消费的时候,都先去redis里面查询是否有这个id,如果没有就执行业务插入id,如果有了,就跳过不执行业务。

  • Redis 的 setIfAbsent 是原子操作,线程安全
  • 可设置过期时间,释放空间
  • 更适合高并发、轻量级幂等

二者的共性及区别

  • 其实两种实现的基础都是在一个唯一id上,所以他们的共性就是在生成id上,这里我提供一种id的生成方式。

共性

我们翻阅源码,发现在消息转换器的底层中有这样一段:image

消息转换器的底层其实自动生成过一个id,我们只需要把CreateMessageIds的值设置为true即可使用。

所以我们将之前的消息转换器改造一下:

1
2
3
4
5
6
7
8
@Bean
public MessageConverter messageConverter(){
// 1.定义消息转换器
Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
// 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
jjmc.setCreateMessageIds(true);
return jjmc;
}

首先我们先不改变消息转换器,发送一条消息,然后改造之后再发送一条同样的消息。

现在来测试一下:

发现了改造之后的消息,携带了一个id!!

image

这样唯一id的生成就搞定了。

区别

总结如下:

比较项 使用唯一 ID(Redis 去重) 数据库唯一约束
核心机制 判断消息 ID 是否存在于 Redis 利用数据库插入唯一值是否成功
幂等性实现方式 内存缓存去重 主键/唯一索引去重
性能 高性能,适合高并发场景 有 IO 开销,适合稳定场景
数据可追溯 不可查历史 可查历史处理记录
存储压力 小(可设置过期) 大(需定期清理)
事务一致性 Redis 与业务逻辑分离 可与业务逻辑在同一事务
风险容错 Redis 宕机可能丢幂等状态 数据库更稳定

总结

实现消息的幂等性,我们可以有两种策略:

  1. 使用数据库的唯一约束实现,从MQ中拿到消息是要将数据插入数据库,这个过程中可以把证明消息唯一的id设置为数据库的唯一约束;同样的数据插入数据库时,如果这个唯一约束已经存在,那就无法进行,如果时第一次,就可以顺利插入,从而实现了消息的幂等性。
  2. 使用消息唯一id搭配redis,生产者每次发送消息时,生成一个全局唯一id;执行业务逻辑之前,我们都先查看redis中是否存在有这个唯一id;如果有,证明业务逻辑已经执行过,跳过不执行,反之执行,即可。
Powered by Hexo & Theme Keep
Total words 55.8k Unique Visitor Page View