在分布式系统中,上游发来的消息队列必然会存在重复的数据,这是不可避免的。
发生原因 消息生产方发送数据时可能会重发
Producer在发送消息时比如遇到网络问题时,发送后因超时得不到服务器的ack,从而进行重发。如果发送的消息内容是银行扣款,那么发生的问题可想而知。
有人会问为啥中间件不能帮我们做排重?
中间件排重会有以下问题:
- 性能消耗严重
对于kafka来说消息量都是千万以上,那么排重意味着要对这些数据进行多次查询,肯定是不现实的。同时排重的时间跨度也是有可能造成数据存储成本增高。
MQTT协议给出服务质量标准
- At most once: 至多一次。消息在传递时,最多会被送达一次。换一个说法就是,没什么消息可靠性保证,允许丢消息。一般都是一些对消息可靠性要求不太高的监控场景使用,比如每分钟上报一次机房温度数据,可以接受数据少量丢失。
- At least once: 至少一次。消息在传递时,至少会被送达一次。也就是说,不允许丢消息,但是允许有少量重复消息出现。
- Exactly once:恰好一次。消息在传递时,只会被送达一次,不允许丢失也不允许重复,这个是最高的等级。
名称 | 质量标准 |
---|---|
Kafka | At least once |
RocketMQ | At least once |
RabbitMQ | At least once |
既然不能在中间件服务解决重复的消息 那么只能在消费端解决
这里说个专业名词:
幂等(Idempotence) 本来是一个数学上的概念,它是这样定义的:如果一个函数 f(x) 满足:f(f(x)) = f(x),则函数 f(x) 满足幂等性。
这个概念被拓展到计算机领域,被用来描述一个操作、方法或者服务。一个幂等操作的特点是,其任意多次执行所产生的影响均与一次执行的影响相同。
那么回到一开始提到的问题:银行转账多次收到扣款的问题。如果我们保证了消费的业务代码是幂等的,那么无论多少重复的消息过来,依然与第一次执行的发生的效果一致,这个问题就迎刃而解。
那怎么实现幂等性呢?一般有以下几种办法。
- 使用数据库唯一索引来实现幂等
通俗点说就是每次操作都有相同的一个操作id,在操作之前做查询,如果该操作id执行过,就避免再次执行。
比如在数据库对这个唯一约束增加唯一索引,多次插入相同的数据时会直接抛出异常“org.springframework.dao.DuplicateKeyException”。或者直接使用“INSERT IF NOT EXIST”来避免多次插入数据。
- 使用Token 机制或者 GUID(全局唯一 ID)机制
和数据库生成唯一标识的概念是一致的,如果消息量过多都放入数据库中去判断的话,可能会造成数据库奔溃。
那么在消息消费时,先判断该消息是否已经消费过,如果没有消费过,才放入到下一步进行持久化操作。
但是在分布式系统中,多个节点可能无法互相感知各自的消费消息,可能会出现这样的情况:
- t0 时刻:Consumer A 收到条消息,检查消息执行状态,发现消息未处理过,开始执行“账户增加 100 元”;
- t1 时刻:Consumer B 收到条消息,检查消息执行状态,发现消息未处理过,因为这个时刻,Consumer A 还未来得及更新消息执行状态。
那么就必须要使用第三方缓存比如redis等来做分布式锁,或者使用分布式事务。