消息的幂等性
幂等性是什么
幂等是一个数学概念,用函数表达式来描述是这样的:f(x) = f(f(x)),例如求绝对值函数。
放在业务中,幂等就是i同一条消息被重复消费多次,业务结果只会被执行一次,不会引发副作用(如重复扣库存、重复发优惠券、重复扣款等),一句话就是指同一个业务,执行一次或多次对业务状态的影响是一致的。
为什么要保证幂等性
但数据的更新往往不是幂等的,如果重复执行可能造成不一样的后果。比如:
- 取消订单,恢复库存的业务。如果多次恢复就会出现库存重复增加的情况
- 退款业务。重复退款对商家而言会有经济损失。
所以,我们要尽可能避免业务被重复执行。
但是RabbitMQ 或任何 MQ 都不能 100% 保证只投递一次或只消费一次,可能因为:
- 消息重复投递
- 消息可能重新进入队列
- 消息可能重复消费
等等原因….
所以在业务层保证幂等性显得尤为重要。
如何保证幂等性
数据库唯一约束
如果从MQ拿到数据是要存到数据库,那么可以根据数据(创建一个UUID)创建唯一约束;这样的话,同样的数据从MQ发送过来之后,当插入数据库的时候,会报违反唯一约束的异常,不会插入成功的。
- 每条消息携带唯一 ID(如
messageId、订单号) - 数据库建表并为
messageId设置唯一约束:
1 | CREATE TABLE message_log ( |
- 消费者处理消息时,尝试插入
messageId:
- 插入成功:说明是第一次处理 → 执行业务
- 插入失败:违反唯一约束 → 表示已处理,跳过
消息唯一id
在生产者发送消息时,创建一个全局唯一id,在消费的时候,都先去redis里面查询是否有这个id,如果没有就执行业务插入id,如果有了,就跳过不执行业务。
- Redis 的
setIfAbsent是原子操作,线程安全 - 可设置过期时间,释放空间
- 更适合高并发、轻量级幂等
二者的共性及区别
- 其实两种实现的基础都是在一个唯一id上,所以他们的共性就是在生成id上,这里我提供一种id的生成方式。
共性
我们翻阅源码,发现在消息转换器的底层中有这样一段:
消息转换器的底层其实自动生成过一个id,我们只需要把CreateMessageIds的值设置为true即可使用。
所以我们将之前的消息转换器改造一下:
1 |
|
首先我们先不改变消息转换器,发送一条消息,然后改造之后再发送一条同样的消息。
现在来测试一下:
发现了改造之后的消息,携带了一个id!!
这样唯一id的生成就搞定了。
区别
总结如下:
| 比较项 | 使用唯一 ID(Redis 去重) | 数据库唯一约束 |
|---|---|---|
| 核心机制 | 判断消息 ID 是否存在于 Redis | 利用数据库插入唯一值是否成功 |
| 幂等性实现方式 | 内存缓存去重 | 主键/唯一索引去重 |
| 性能 | 高性能,适合高并发场景 | 有 IO 开销,适合稳定场景 |
| 数据可追溯 | 不可查历史 | 可查历史处理记录 |
| 存储压力 | 小(可设置过期) | 大(需定期清理) |
| 事务一致性 | Redis 与业务逻辑分离 | 可与业务逻辑在同一事务 |
| 风险容错 | Redis 宕机可能丢幂等状态 | 数据库更稳定 |
总结
实现消息的幂等性,我们可以有两种策略:
- 使用数据库的唯一约束实现,从MQ中拿到消息是要将数据插入数据库,这个过程中可以把证明消息唯一的id设置为数据库的唯一约束;同样的数据插入数据库时,如果这个唯一约束已经存在,那就无法进行,如果时第一次,就可以顺利插入,从而实现了消息的幂等性。
- 使用消息唯一id搭配redis,生产者每次发送消息时,生成一个全局唯一id;执行业务逻辑之前,我们都先查看redis中是否存在有这个唯一id;如果有,证明业务逻辑已经执行过,跳过不执行,反之执行,即可。