这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战
前言
RocketMQ的重试机制很好的保证了消息至少被消费一次,也使得RocketMQ丢消息的概率大大减小。而这样做的代价就是消息可能会被多次消费,正如第一篇文章所说的问题,那我们该如何避免呢?在高并发概念下,经常会提到这样一个概念就是幂等。在这里,我们同样是考虑幂等问题,只不过这里是由消息队列引起的消息幂等。
消息幂等
当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并且多次消费并未对业务系统产生任何负面影响,那么这整个过程就可实现消息幂等。
例如,在支付场景下,消费者消费扣款消息,对一笔订单执行扣款操作,扣款金额为100元。如果因网络不稳定等原因导致扣款消息重复投递,消费者重复消费了该扣款消息,但最终的业务结果是只扣款一次,扣费100元,且用户的扣款记录中对应的订单只有一条扣款流水,不会多次扣除费用。那么这次扣款操作是符合要求的,整个消费过程实现了消费幂等。
处理方法
其实脑子里第一时间就是用RocketMQ自带的Message ID来进行幂等处理。但是,RocketMQ的重试机制会导致同一条消息重复发送,Message ID是不会变的。比如
而且,就算是不同Message ID的消息,消息体内的业务数据依旧可以是相同的。所以使用Message ID是无法做到真正安全的幂等处理。所以不建议以Message ID作为处理依据。最好的方式是以业务唯一标识作为幂等处理的关键依据。RocketMQ收发消息,主要是封装成Message来操作,所以我们可以通过Message的方法setKey(bizID)来给消息设置业务码,业务码就是业务唯一标识。这样在消费消息是,只需要getKey()就可以拿到业务唯一标识,再根据业务唯一标识的Key做幂等处理,这样就可以实现消息幂等了。
/**
* 消息类. 一条消息由主题, 消息体以及可选的消息标签, 自定义附属键值对构成.
*
* <p> <strong>注意:</strong> 我们对每条消息的自定义键值对的长度没有限制, 但所有的自定义键值对, 系统键值对序列化后, 所占空间不能超过32767字节. </p>
*/
public class Message implements Serializable {
private static final long serialVersionUID = -1385924226856188094L;
/**
* <p> 系统属性 </p>
*/
Properties systemProperties;
/**
* 获取业务码
*
* @return 业务码
*/
public String getKey() {
return this.getSystemProperties(SystemPropKey.KEY);
}
/**
* 设置业务码
*
* @param key 业务码
*/
public void setKey(String key) {
this.putSystemProperties(SystemPropKey.KEY, key);
}
}