如何保证消息幂等
本文内容
1. 对幂等的理解
幂等是业务的一个特性,在 MQ 中,对于不满足幂等性的业务,在消息重复消费时,会导致数据不一致、或数据错乱的现象。
例如一个支付业务,消费者需要消费扣款的消息,如果该消息出现了重复消费的情况,但最终的业务结果是只扣款了一次,那么就说这个扣款业务具有 幂等性。
再用我们常见的 HTTP 为例,GET、PUT、DELETE 方法都是幂等的,因为它们不管执行多少次,都和执行一次效果一样,对业务结果都是无影响的。而 POST 就不是幂等的,它会创建多个资源。
在实际业务中,大多都是对 DB 的操作,在 CRUD 中:
READ 和 DELETE 是幂等的;
CREATE 不是幂等的;
而 UPDATE 操作可能是幂等的,也可能不是幂等的。比如:
UPDATE user SET age = 1 WHERE id = 1; # 幂等 UPDATE user SET age = age + 1 WHERE id = 1; # 不幂等
2. 为何会出现消息不幂等的情况?
在 MQ 中,为了保证 消息的可靠传输,一般都会有 失败重传机制,比如:
当生产者发现消息发送失败时,生产者会进行消息重发;
比如 发送的消息因网络延时,导致生产者未收到服务端的 ACK,导致 消息重发,而一段时间后延时的消息又到达了,导致消息重复,这种情况消息的 offset 是不同的。
当消费者消费失败时,服务端会再次投递该消息给消费者。
比如 消费者消费完消息后,还没来得及回复 ACK,机器就宕机了,重启后服务端会再次将该消息投递给消费者,这种情况消息的 offset 是相同的。
出现了重复消息,自然而然就会有幂等性问题了。
3. 如何解决消息不幂等?
首先要明确一点,有些业务天然就不需要考虑幂等性问题,允许重复调用(即允许重试)。这些业务还可以通过合理的重试机制来提高可靠性。
那在需要考虑幂等性问题的业务中,应该如何保证消息幂等呢?通常有以下几种解决方案:
- 利用数据库;
- 设置全局唯一标识 ID;
3.1 利用数据库
利用数据库解决幂等性问题,可以有两种方案:
- 使用唯一索引去重;
- 设计一个去重表。
比如,业务是向数据库中插入数据,那么就可以使用唯一索引来保证幂等,重复消费时会插入失败,捕获异常处理即可。
同理,如果业务是操作 Redis 的 Set,那么也可以天然的保证幂等。
如果是不具有幂等性的更新数据(UPDATE user SET age = age + 1 WHERE id = 1;
),此时可以使用第二种方案,我们 新建一张去重表,将 id 和时间戳做一个唯一索引进行约束。当消费者要更新数据库时,先往去重表进行写入,只有第一条消息会插入成功,后面的都会失败,在失败时捕获异常处理一下即可保证幂等。
3.2 设置全局唯一标识 ID
我们可以用 业务唯一标识 ID 作为 Message Key 放入消息中,消费者在消费消息时,首先需要取出该 Key,然后根据业务来做具体的幂等处理。
为了增加幂等处理的速度,可以将这个 Message Key 存入 Redis,通过 Redis 来快速判断该 Key 是否存在,即是否为重复消费。