如何避免RocketMQ的重试机制导致的数据重复

434 阅读3分钟

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

前言

RocketMQ的重试机制很好的保证了消息至少被消费一次,也使得RocketMQ丢消息的概率大大减小。而这样做的代价就是消息可能会被多次消费,正如第一篇文章所说的问题,那我们该如何避免呢?在高并发概念下,经常会提到这样一个概念就是幂等。在这里,我们同样是考虑幂等问题,只不过这里是由消息队列引起的消息幂等。

消息幂等

当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并且多次消费并未对业务系统产生任何负面影响,那么这整个过程就可实现消息幂等。
例如,在支付场景下,消费者消费扣款消息,对一笔订单执行扣款操作,扣款金额为100元。如果因网络不稳定等原因导致扣款消息重复投递,消费者重复消费了该扣款消息,但最终的业务结果是只扣款一次,扣费100元,且用户的扣款记录中对应的订单只有一条扣款流水,不会多次扣除费用。那么这次扣款操作是符合要求的,整个消费过程实现了消费幂等。

处理方法

其实脑子里第一时间就是用RocketMQ自带的Message ID来进行幂等处理。但是,RocketMQ的重试机制会导致同一条消息重复发送,Message ID是不会变的。比如 image.png
而且,就算是不同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);
    }
}