应用的幂等是在分布式系统设计时必须要考虑的一个关键点。如果对幂等没有额外的考虑,那么在业务出现处理失败的情况时,可能出现重复消费相同的消息,从而导致出现不符合业务预期的情况。为了避免上述异常,消息队列的消费者在接收到消息后,有必要根据业务上的唯一 Key 对消息做幂等处理。
什么是消息幂等
定义
当业务多次消费到同一条消息的结果与消费一次的结果是相同的,同时多次消费同一条消息并未对业务系统产生任何负面影响,那么这个消费者的处理过程就是幂等的。
场景示例
举个例子,在银行支付系统的场景下,当消费端消费到扣款消息后,系统将对订单执行扣款操作,扣款金额为1美元。如果因由于网络不稳定等一些原因导致扣款消息再次被消费到,如果最终的业务结果是只扣款一次,扣费1美元,且用户的扣款记录中对应的订单只有一条扣款流水,并没有多次被多次扣费,那么这次扣款操作是符合要求的,整个消费过程实现了消费幂等。
适用场景
发送消息引起消息重复场景
生产者在发送一条消息后,服务端接收成功并完成持久化,如果此时出现了网络闪断或者客户端重启等异常导致服务端对客户端应答失败,此时生产者由于没有收到服务端的确认消息,从而尝试再次发送消息,这时会导致后续消费者会收到两条内容相同但 Message ID 不同的消息。
消费消息引起消息重复场景
消费端消费到了消息并完成业务处理,当消费端给服务端返回 ACK 的时候,此时网络发生异常。当消费端再次来消费消息时,会再次消费到已被处理过的消息,消费者收到了两条内容相同并且 Message ID 也相同的消息。
处理方案
根据以上两种场景可以看出消息重复会出现以下两种情况:
可能出现不同的 Message ID 对应的消息内容相同。
可能是相同的 Message ID 同时消息内容相同。
所以不建议以 Message ID 作为处理依据,建议以业务的唯一标识作为幂等处理的依据。例如支付场景可以将订单号,作为幂等处理的依据。消费到消息后,取出业务中的订单号,业务根据订单号进行判断是否被处理。
代码示例
public static class Order {
public String orderId;
public String orderData;
}
生产端:
Producer<Order> producer = client.newProducer(Schema.AVRO(Order.class)).create();
producer.newMessage().value(new Order("orderid-12345678", "orderData")).send();
消费端:
Consumer<Order> consumer = client.newConsumer(Schema.AVRO(Order.class)).subscribe();
Order order = consumer.receive().getValue();
String key = order.orderId;
获取到业务的唯一标识 orderId 后,对其进行去重操作。
常见的去重方式
利用数据库进行去重
业务上的幂等操作可以添加一个过滤的数据库,例如设置一个去重表,也可以在数据库中通过唯一索引来去重。
举一个例子,现在要根据订单流转的消息在数据库中写一张订单 Log 表,可以把订单 ID 和修改时间戳做一个唯一索引进行约束。
当消费端消费消息出现相同内容的消息,会多次去订单 Log 表中进行写入,由于添加了唯一索引,除了第一条之外,后面的都会失败,这就从业务上保证了幂等,即使消费多次,也不会影响最终的数据结果。
设置全局唯一消息 ID 或者任务 ID
调用链 ID 也可以应用在这里。在消息生产的时候向每条消息中添加一个唯一 ID,消息被消费后,在缓存中设置一个 Key 为对应的唯一 ID,代表数据已经被消费,当消费端去消费时,就可以根据这条记录,来判断是否已经处理过。
本页内容是否解决了您的问题?