消息幂等(去重)通用解决方案
消息中间件是分布式系统常用的组件,无论是异步化、解耦、削峰等都有广泛的应用价值。我们通常会认为,消息中间件是一个可靠的组件——这里所谓的可靠是指,只要我把消息成功投递到了消息中间件,消息就不会丢失,即消息肯定会至少保证消息能被消费者成功消费一次,这是消息中间件最基本的特性之一。
一个消息M发送到了消息中间件,消息投递到了消费程序A,A接受到了消息,然后进行消费,但在消费到一半的时候程序重启了,这时候这个消息并没有标记为消费成功,这个消息还会继续投递给这个消费者,直到其消费成功了,消息中间件才会停止投递。
然而这种可靠的特性导致,消息可能被多次地投递。举个例子,还是刚刚这个例子,程序A接受到这个消息M并完成消费逻辑之后,正想通知消息中间件“我已经消费成功了”的时候,程序就重启了,那么对于消息中间件来说,这个消息并没有成功消费过,所以他还会继续投递。这时候对于应用程序A来说,看起来就是这个消息明明消费成功了,但是消息中间件还在重复投递。
这在RockectMQ的场景来看,就是同一个messageId的消息重复投递下来了。
简单的消息去重解决方案
例如:假设我们业务的消息消费逻辑是:插入某张订单表的数据,然后更新库存:
insert into t_order values .....
update t_inv set count = count-1 where good_id = 'good123';
select * from t_order where order_no = 'order123'
if(order != null) {
return ;//消息重复,直接返回
}
保证消息幂等性的核心方法是结合唯一标识符、业务层面校验和系统级去重机制,确保即使消息重复处理也不会影响最终状态。
主要方法
-
唯一标识符机制
- 消息ID生成:在生产者端为每条消息生成全局唯一ID(如UUID或业务流水号),消费者端通过存储系统(数据库、Redis等)记录已处理的ID,重复ID直接忽略。 12
- 业务唯一标识:在消息体中嵌入业务唯一键(如订单号、交易流水号),消费者处理前检查业务状态,避免重复操作。 23
- 消息指纹:通过哈希算法(如MD5/SHA)生成内容指纹,用于检测重复消息。 1
-
幂等性操作设计
- 数据库约束:利用数据库唯一索引或乐观锁(版本号/时间戳),确保数据更新或插入操作仅执行一次。例如,订单表对订单ID设置唯一约束。
- 分布式锁:在处理消息前尝试获取以消息ID为键的分布式锁,避免并发重复处理。
并发重复消息
并发去重的解决方案之一
要解决上面并发场景下的消息幂等问题,一个可取的方案是开启事务把select 改成 select for update语句,把记录进行锁定。
select * from t_order where order_no = 'THIS_ORDER_NO' for update //开启事务 if(order.status != null) {
return ;//消息重复,直接返回
}
Exactly Once(在消息中间件里,有一个投递语义的概念,而这个语义里有一个叫”Exactly Once”,即消息肯定会被成功消费,并且只会被消费一次)
基于关系数据库事务插入消息表
假设我们业务的消息消费逻辑是:更新MySQL数据库的某张订单表的状态:
update t_order set status = 'SUCCESS' where order_no= 'order123';
要实现Exaclty Once即这个消息只被消费一次(并且肯定要保证能消费一次),我们可以这样做:在这个数据库中增加一个消息消费记录表,把消息插入到这个表,并且把原来的订单更新和这个插入的动作放到同一个事务中一起提交,就能保证消息只会被消费一遍了。
- 开启事务
- 插入消息表(处理好主键冲突的问题)
- 更新订单表(原消费逻辑)
- 提交事务