RocketMQ常见问题及实现分布式事务时注意事项

RocketMQ常见问题及实现分布式事务时关注问题

  • RocketMQ实现分布式事务时,需关注的问题
    • 1、MQ半消息回查,若得不到该消息是提交还是回滚,会一直隔一段时间就查询一次吗?
    • 2、二阶段异常,需要回滚怎么处理?
    • 3、消费者消费失败后,会重试吗,多次重试后还是失败会怎么样?
  • RocketMQ常见的一些疑问或问题
    • 1、如何防止消息丢失的问题
    • 2、如何防止消息的重复消费问题(幂等性问题)
    • 3、消息如何保证消费顺序的问题
    • 4、消息积压、阻塞怎么解决?

RocketMQ实现分布式事务时,需关注的问题

1、MQ半消息回查,若得不到该消息是提交还是回滚,会一直隔一段时间就查询一次吗?

答:为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionalMessageCheckListener 类来修改这个行为。
RocketMQ官网解释.

2、二阶段异常,需要回滚怎么处理?

答:这个问题我个人觉得是SEATA和RocketMQ的主要区别:
SEATA在解决分布式事务时,SEATA主要是有四种模式来解决分布式事务,通常用的较多的时AT模式和TCC模式:
1、AT模式涉及到一个全局锁的概念,通过全局锁,来保证二阶段回滚时,一阶段的数据也可以回滚,所以在数据库层性能多少会受到影响
2、TCC模式主要是我们自己控制 资源预留、确认提交、回滚资源释放,也可以回滚一阶段的数据,不过业务侵入度较高
RocketMQ:当二阶段(消费者)逻辑异常时,只能够保证消费者端事务回滚、无法正常消费消息,但是生产者端是无法进行回滚操作的。所以在开发时,通常将容易出现异常的情况放在一阶段(生产者端,例:跨行转账的扣款,下单成功送积分的下单等等),尽可能保证消费者进行消费时,不会因为业务逻辑不同导致无法正常消费,从而产生的数据不一致的问题。

那,如果真的由于特殊原因,消息一直不能被正确处理,那怎么办 ?
我们考虑两种方式来解决这个问题。
第一,在代码中设置消息重试次数,如果达到指定次数,就发邮件或者短信通知业务方人工介入处理

@Component
public class OrderListener implements MessageListenerConcurrently {

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
        logger.info("消费者线程监听到消息。");
        for (MessageExt message:list) {
            if (!processor(message)){
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }

    /**
     * 消息处理,第3次处理失败后,发送邮件通知人工介入
     * @param message
     * @return
     */
    private boolean processor(MessageExt message){
        String body = new String(message.getBody());
        try {
            logger.info("消息处理....{}",body);
            int k = 1/0;
            return true;
        }catch (Exception e){
            if(message.getReconsumeTimes()>=3){
                logger.error("消息重试已达最大次数,将通知业务人员排查问题。{}",message.getMsgId());
                sendMail(message);
                return true;
            }
            return false;
        }
    }
}

第二,等待消息重试最大次数后,进入死信队列。
消息重试最大次数默认是16次,我们也可以在消费者端设置这个次数。
consumer.setMaxReconsumeTimes(3);//设置消息重试最大次数
死信队列的主题名称是 %DLQ% + 消费者组名称,比如在订单数据中,我们设置了消费者组名:
String consumerGroup = “order-consumer-group”;
那么这个消费者,对应的死信队列主题名称就是%DLQ%order-consumer-group
可以通过程序代码监听这个死信队列的主题,来通知人工介入处理或者直接在控制台查看处理了。通过幂等性消费和对死信消息的处理,基本上就能保证消息一定会被处理。

3、消费者消费失败后,会重试吗,多次重试后还是失败会怎么样?

答:这个问题其实在第二个问题中已经给出答案了,消息重试最大次数默认是16次,我们也可以在消费者端设置这个次数,当消息消费到达重试最大次数后,进入死信队列

RocketMQ常见的一些疑问或问题

1、如何防止消息丢失的问题

消息生产者:消息发送成功也不意味着它是可靠的。要确保不会丢失任何消息,还应启用同步Master服务器或同步刷盘,即SYNC_MASTER或SYNC_FLUSH。
(1.2 消息发送失败处理方式
Producer的send方法本身支持内部重试,重试逻辑如下:

至多重试2次。
如果同步模式发送失败,则轮转到下一个Broker,如果异步模式发送失败,则只会在当前Broker进行重试。这个方法的总耗时时间不超过sendMsgTimeout设置的值,默认10s。
如果本身向broker发送消息产生超时异常,就不会再重试。
以上策略也是在一定程度上保证了消息可以发送成功。如果业务对消息可靠性要求比较高,建议应用增加相应的重试逻辑:比如调用send同步方法发送失败时,则尝试将消息存储到db,然后由后台线程定时重试,确保消息一定到达Broker。)

1 )多 Master,每个 Master 带有 Slave;
2 )主从之间设置成 SYNC_MASTER;
3 ) Producer 用同 步方式写;
4 )刷盘策略设置成 SYNC FLUSH。
就可以消除单点依赖,即使某台机器出现极端故障也不会丢消息 。

2、如何防止消息的重复消费问题(幂等性问题)

RocketMQ官网幂等性问题解释.
这个问题官方也指明了实际使用中,可能会存在相同的消息有两个不同msgId的情况(消费者主动重发、因客户端重投机制导致的重复等),这种情况就需要使业务字段进行重复消费
解决方式:
一、借助关系数据库的唯一ID进行去重。(官网推荐,即使分布式环境也不会出现问题,并发很高的话,可能数据库压力会大些)
二、通过Redis的increment自增操作,通过这一原子性操作来生成唯一ID
三、通过雪花算法生成唯一ID
四、通过UUID生成唯一ID,缺点生成的ID过于随机

重复消费通常这样的解决方案:
1、消息body需要有一个唯一业务ID存在,比如:订单ID、商品ID,这样消费端当进行状态变更时(或者消息表的状态变更时),可以根据乐观锁的更新,来保证幂等。
通过redis来保证幂等在大部分场景下是没问题的,如果是服务器突然宕机、或者主从发生切换,导致部分数据丢失,是有几率出现重复消费的问题,所以通过数据库持久化来保证幂等是一个比较好的选择。

扩展:这里和大家简单聊一下雪花算法

网上有部分朋友提出雪花算法会有重复ID出现的情况,这问题是什么原因导致的呢
雪花算法原理可参看: 雪花算法原理解析.
通过对原理的认识,我们可以得出当workerId和datacenterId相同,且是多例模式时,会出现ID重复的情况,我这边也进行了代码测试:

@Component
public class SnowFlakeUtil {
    private long workerId = 0L;
    private long datacenterId = 1L;
    private Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);

    @PostConstruct
    public void init() {
        try {
            workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
        } catch (Exception e) {
            workerId = NetUtil.getLocalhostStr().hashCode();
        }

    }

    public synchronized long snowflakeId(long workerId, long datacenterId) {
        Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
        return snowflake.nextId();
    }

    public synchronized long snowflakeId() {
        return snowflake.nextId();
    }
}
package com.fss.test;

import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import com.fss.utils.SnowFlakeUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

import javax.annotation.Resource;
import java.util.HashSet;
import java.util.concurrent.*;

/**
测试类
*/
@Slf4j
class SnowFlakeUtilTest extends BaseTest {

    @Resource
    private SnowFlakeUtil snowFlakeUtil;

    @Resource
    private SnowFlakeUtil snowFlakeUtil1;
    @Resource
    private SnowFlakeUtil snowFlakeUtil2;
    @Resource
    private SnowFlakeUtil snowFlakeUtil3;

    /**
     * 因为snowFlakeUtil中的snowflakeId方法是synchronized修饰,所以当单机部署下单例模式下生成snowflakeId 是不会出现问题的
     */
    @Test
    void insert() {
        int num = 10000000;
        HashSet<Long> set = new HashSet<Long>(new Double(num / 0.75).intValue() + 1);
        boolean flag = true;
        log.info("开始执行...");
        for (int i = 0; i < num; i++) {
            if (i % 1000 == 0) {
                log.info("当前i为:" + i);
            }
            flag = set.add(snowFlakeUtil.snowflakeId());
            if (!flag) {
                throw new RuntimeException("存在重复的key");
            }
        }
    }

    /**
     * 虽然是多线程生成snowflakeId,但是synchronized对对象加锁,故也不会出现问题
     */
    @Test
    void manyThread() {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(20, 20, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());

        int num = 10000000;
        HashSet<Long> set = new HashSet<>(new Double(num / 0.75).intValue() + 1);

        log.info("开始执行...");
        for (int i = 0; i < 10000000; i++) {
            if (i % 10000 == 0) {
                log.info("当前i为:" + i);
            }

            threadPoolExecutor.execute(() -> {
                boolean flag = set.add(snowFlakeUtil.snowflakeId());
                if (!flag) {
                    throw new RuntimeException("存在重复的key");
                }
            });
        }
    }

    /**
     * 我本以为这种情况下会出现问题,后面run了几次发现不会抛出异常,仔细想了下,才想起Spring生成bean都是单例模式的,所以虽然定义了多个SnowFlakeUtil实例,
     * 但是实际上指向的实例还是同一个,故也不会出现问题
     */
    @Test
    void manySameSnowFlakeUtillBySpring() {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(15, 15, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
        int num = 10000000;
        HashSet<Long> set = new HashSet<>(new Double(num * 5 / 0.75).intValue() + 1);

        log.info("开始执行...");
        for (int i = 0; i < num; i++) {
            if (i % 10000 == 0) {
                log.info("当前i为:" + i);
            }

            threadPoolExecutor.execute(() -> {
                boolean flag = set.add(snowFlakeUtil1.snowflakeId());
                if (!flag) {
                    throw new RuntimeException("存在重复的key");
                }
                flag = set.add(snowFlakeUtil2.snowflakeId());
                if (!flag) {
                    throw new RuntimeException("存在重复的key");
                }
                flag = set.add(snowFlakeUtil3.snowflakeId());
                if (!flag) {
                    throw new RuntimeException("存在重复的key");
                }
            });
        }
    }

    /**
     * 创建多个workerId和datacenterId相同的实例,通过多线程生成snowflakeId,此时就会出现ID重复的问题。
     * 这个在单机架构下多个服务器和分布式多服务器,若workerId和datacenterId相同,就有可能会出现ID重复的问题
     */
    @Test
    void manySameSnowFlakeUtill() {
        Snowflake snowflake1 = IdUtil.createSnowflake(0, 1);
        Snowflake snowflake2 = IdUtil.createSnowflake(0, 1);
        Snowflake snowflake3 = IdUtil.createSnowflake(0, 1);
        Snowflake snowflake4 = IdUtil.createSnowflake(0, 1);
        Snowflake snowflake5 = IdUtil.createSnowflake(0, 1);
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(15, 15, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
        int num = 10000000;
        HashSet<Long> set = new HashSet<>(new Double(num * 5 / 0.75).intValue() + 1);

        log.info("开始执行...");
        for (int i = 0; i < num; i++) {
            if (i % 10000 == 0) {
                log.info("当前i为:" + i);
            }

            threadPoolExecutor.execute(() -> {
                boolean flag = set.add(snowflake1.nextId());
                if (!flag) {
                    throw new RuntimeException("存在重复的key");
                }
                flag = set.add(snowflake2.nextId());
                if (!flag) {
                    throw new RuntimeException("存在重复的key");
                }
                flag = set.add(snowflake3.nextId());
                if (!flag) {
                    throw new RuntimeException("存在重复的key");
                }
                flag = set.add(snowflake4.nextId());
                if (!flag) {
                    throw new RuntimeException("存在重复的key");
                }
                flag = set.add(snowflake5.nextId());
                if (!flag) {
                    throw new RuntimeException("存在重复的key");
                }
            });
        }
    }

}

那么在单机架构下多个服务器和分布式多服务器下,想使用雪花算法生成唯一ID的解决方式是什么呢?
一、workId使用服务器hostName生成,dataCenterId使用IP生成,这样可以最大限度防止10位机器码重复,但是由于两个ID都不能超过32,只能取余数,还是难免产生重复,但是实际使用中,hostName和IP的配置一般连续或相近,只要不是刚好相隔32位,就不会有问题(不是十分严谨,还有几率重复)
二、启动方式: java -jar -Xms256m -Xmx512m -Dserver.workId=1 -Dserver.datacenterId=1 /home/admin/jars/test.jar ,通过执行workId和datacenterId的方式避免重复,不同的服务器 -Dserver.workId -Dserver.datacenterId设置为不同的值。(部署较为麻烦,还需要更更改对应的脚本文件)
三、(待续更)

3、消息如何保证消费顺序的问题

1、全局消息顺序:可以将topic的队列设置为1,这样所有消息发送根据写入顺序发送到broker,消费者进行消费自然也是顺序的(原因:同一个消费队列某个时刻只能被一个消费者消费,一个消费者可以同时消费多个消息队列).
2、局部消息顺序:topic仍可以设置为多个队列,发送者通过messageLisstenerOrderLy的方式去发送消息,发送消息时根据ID进行hash运算得到数字和队列数值取余数,得到目标队列进行发送即可,这样同一个业务ID就可以保证局部的顺序。消费者通过MessageListenerOrderly,去顺序的接受消息进行消费即可。

4、消息积压、阻塞怎么解决?

1、消费者若是宕机了,或者迭代修改代码导致消费过慢,可以先增加消费者,让消费者机器数达到和消息队列一致。保证每个消息队列可以只被一个消费者消费,增加消费端的消费速率。
2、排查原因、优化代码,提高单个消费者的消费能力。
3、若可能是长期会是大流量过来,可以增加该topic的队列数值,增加消费者机器,来保证消费速率,如果是瞬时流量,后续仍是之前的低频状态,可以写个临时程序,将该topic的消息通过临时消费者转发到新的topic上,然后,恢复之前的消费者,就可以保证后续的消息可以正常被消费,而新的topic要设置较大的消费队列,临时启用多个消费者针对新topic进行处理。

你可能感兴趣的:(分布式事务,java,分布式,队列,spring,intellij,idea)