二非事务型MQ的最终一致性事务方案

二 非事务型MQ的最终一致性事务方案

二 非事务型MQ的最终一致性事务方案

2.1 非事务型MQ的方案及流程

对于非事务型MQ,使用该消息中间件实现最终一致性事务的方案,参照第一部分的设计思路:
image-20230511153852987

2.2 开发案例

本文以一个简单的关于user用户的业务,为开发案例,进行展示开发过程。

step1 定义本地的事务消息表

CREATE TABLE `mq_trans_message` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `tenant_code` varchar(64) NOT NULL DEFAULT 'system' COMMENT '租户唯一编码,system-系统级别,跨租户共享',
  `topic` varchar(128) NOT NULL DEFAULT '' COMMENT 'topic',
  `tag` varchar(128) NOT NULL DEFAULT '' COMMENT 'tag',
  `message_key` varchar(256) NOT NULL DEFAULT '' COMMENT '消息key',
  `message` text NOT NULL COMMENT '消息',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='事务消息记录表';

主要信息包括事务消息的id、topic、tag、message-key消息的key、message消息体、create/updateTime消息的创建和更新时间。

step2 本地事务流程

首先创建controller控制器,响应request请求:

(controller层调用service层,service层实现interface接口,并提供@Transactional注解的事务方法)

@RestController
public class HelloController {

    @Autowired
    private UserService userService;

    @GetMapping("/trans/test")
    public Boolean transTest() throws Exception {
      //调用本地的事务方法
        return userService.transMessageSuccess();

    }
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements UserService {

    private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private MqTransMessageService mqTransMessageService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean save(UserEntity userEntity) {
        return super.insert(userEntity);
    }

  /**…………………………………………………………………………………………service层,提供的本地事务方法…………………………………………………………………………………… */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean transMessageSuccess() {
        //保存用户信息  --1 本地事务操作一:saveUser存入数据库,自身调用,save方法的注解失效,但是会加入当前事务
        saveUser();
        LOGGER.info("begin send trans message");
      //2 本地事务操作二:将事务消息存入本地数据库
        mqTransMessageService.transSendMsg(MqConstant.Top.USER_ORDER_TOPIC, MqConstant.Tag.USER_TAG,
            "{\"userName\": \"WillJoSuccess\"}");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            LOGGER.error(e.getMessage());
        }
        LOGGER.info(" end send tran message");
        return Boolean.TRUE;
    }

当前mqTransMessageService.transSendMsg方法,只是将事务消息存入本地事务信息数据库、并存入MessageQueue.PriorityQueue中(spring启动,while执行本地消息发送到RocketMQ上,减少定时任务的压力)

-----关于事务:此处当前方法没有被事务注解,但是由于是不同类之间被@Transactional的事务方法调用,因此当前方法默认会加入事务中,保证事务的一致性。

@Service
public class MqTransMessageServiceImpl extends
        ServiceImpl<MessageMapper, MqTransMessageEntity> implements
        MqTransMessageService {

    @Autowired
    private MessageMapper messageMapper;

    private static final int MAX_MESSAGE_NUM = 1000;

  //当前方法未用@Transactional注释,但是仍旧会加入存在的事务
    @Override
    public Boolean transSendMsg(String topic, String tag, String content) {
        if (StringUtils.isBlank(topic)) {
            throw new IllegalArgumentException("topic 不能为空");
        }
        if (StringUtils.isBlank(content)) {
            throw new IllegalArgumentException("content 不能为空");
        }
        MqTransMessageEntity msg = new MqTransMessageEntity();
        msg.setTopic(topic)
                .setTag(tag)
                .setMessage(content)
                .setCreateTime(new Date());
        //将MqTransMessageEntity存入本地数据库表
        super.insert(msg);
        //将MqTransMessageEntity存入MessageQueue.PriorityQueue中
        return MessageQueue.putInPriorityQueue(msg);

    }
}

上述过程,完成本地事务的核心步骤,如下:

image-20230612175243620

step3 本地事务消息发送到MQ(优化)

在本地事务执行过程中,由于事务信息不停的加入本地事务消息数据库中,由后台任务定时的发送消息到MQ上,为了减少后台任务的工作量、加快传递本地事务消息,会配置一个自动执行的方法,执行while循环,将本地事务消息发送给MQ。如果没有发送成功的,交给后台定时任务托底。

首先,在项目中自定义一个对象

public class MessageQueue {


    /**
     * 优先级最高的
     */
    public static BlockingQueue<MqTransMessage> priorityQueue = new LinkedBlockingDeque<>();

    /**
     * 延迟队列
     */
    public static DelayQueue<MqTransMessageDelay> delayQueue = new DelayQueue<>();

    public static boolean putInPriorityQueue(MqTransMessageEntity mqTransMessageEntity) {
        return priorityQueue.add(MqTransMessage.instance(mqTransMessageEntity));
    }

    public static boolean putInDelayQueue(MqTransMessage transMessage) {
        transMessage.setFailCount(transMessage.getFailCount()+1);
        return delayQueue.add(MqTransMessageDelay.instance(transMessage));
    }


}

该对象是step3的本地事务方法中添加入MessageQueue这一内存的。

image-20230529164405195

然后,定义两个监听器类,监听到在spring项目启动时,自动执行onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) 方法

public class TransMessageRunner implements ApplicationListener<ApplicationReadyEvent> {

    private static final Logger logger = LoggerFactory.getLogger(TransMessageRunner.class);

    @Autowired
    private RocketMqProducerService rocketMqProducerService;

    @Autowired
    private MqTransMessageService mqTransMessageService;

    /**
     * 事务最大等待时间,单位为秒
     */
    public static final int TRANS_MAX_WAITING_TIME = 30;




    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
        System.out.println("run message send thread");
        new Thread(() -> {
            while (true) {
                MqTransMessage message = null;
                try {
                  //step3.1 获取优先队列中的message对象
                    message = MessageQueue.priorityQueue.take();
                } catch (InterruptedException e) {

                }
                if (Objects.isNull(message)) {
                    continue;
                }
                SendResult sendResult = null;
                try {
                    String key = MessageFormat.format(MessageLock.LOCK_PREFIX, message.getId());
                    //key.intern()获取jvm中的唯一对象
                    synchronized (key.intern()) {
                      //step3.2 获取本地数据库中的事务消息
                        // 查询数据库确保是有值
                        MqTransMessageEntity mqTransMessageEntity = mqTransMessageService.selectById(message.getId());
                      //step3.3.1 如果本地数据库消息null
                        if (Objects.isNull(mqTransMessageEntity)) {
                            // 事务数据库表无值,有三种可能姓 ,一种是事务没结束,一种事务没成功,或者已经被定时任务发送了
                            long time = System.currentTimeMillis() - message.getCreateTime().getTime();
                            if(time / 1000 > TRANS_MAX_WAITING_TIME) {
                                // 超过30秒还是查不到,就直接丢弃了,后面有定时任务兜底
                                logger.info(" due to over 30 second, discard message for messageId={}", message.getId());
                            } else {
                                // 放到延迟队列处理
                                logger.info(" add message to delayQueue  for messageId={}", message.getId());
                                MessageQueue.putInDelayQueue(message);
                            }
                            continue;
                        } 
                      //step3.3.2 如果本地数据库消息不为null
                      else {
                            //查询事务库表有值,然后rocketMQ同步发送信息到broker,获取发送结果sendResult
                            sendResult = rocketMqProducerService.synSend(message.getTopic(), message.getTag(),
                                    message.getMessage());
                            if (Objects.nonNull(sendResult) && SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
                              //执行成功,从数据库删除
                                mqTransMessageService.deleteById(message.getId());
                            } else {
                              //执行失败,继续放入优先队列执行
                                // 网路抖动等原因,继续放在优先队列进行发送
                                MessageQueue.priorityQueue.put(message);
                            }
                        }

                    }

                } catch (Exception e) {
                    logger.warn("mq send fail,message={}",e.getMessage(),e);
                    MessageQueue.putInDelayQueue(message);
                }


            }
        },"transMessage").start();
    }
}

上述监听器的逻辑如下:

step3.1 获取优先队列中的message对象
   String key = MessageFormat.format(MessageLock.LOCK_PREFIX, message.getId());
                    //key.intern()获取jvm中的唯一对象
                    synchronized (key.intern())-------加锁-注意此处加锁逻辑
step3.2 获取本地数据库中的事务消息
step3.3.1 如果本地数据库消息null
// 事务数据库表无值,有三种可能姓 ,一种是事务没结束,一种事务没成功,或者已经被定时任务发送了
 case1:超过30秒还是查不到,就直接丢弃了,后面有定时任务兜底
 case2:放到延迟队列处理 MessageQueue.putInDelayQueue(message)
                      
step3.3.2 如果本地数据库消息不为null
1 然后rocketMQ同步发送信息到broker
case1:执行成功,从数据库删除
case2:执行失败,继续放入优先队列执行MessageQueue.priorityQueue.put(message)

这里主要是加快本地事务消息传递到MQ,保证本地消息传送的即时性。

然后,还有一个延迟消息的监听器,不断的把延迟消息重新放入优先队列中

public class TransDelayMessageRunner implements ApplicationListener<ApplicationReadyEvent> {

    private static final Logger logger = LoggerFactory.getLogger(TransDelayMessageRunner.class);

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        new Thread(() -> {
            while (true) {
                try {
                    /** 将延迟队列delayQueue中的信息重新添加入优先队列priorityQueue  */
                    MqTransMessageDelay messageDelay = MessageQueue.delayQueue.take();
                    logger.info("delay message poll ,message={}", messageDelay);
                    MessageQueue.priorityQueue.put(messageDelay);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }

        },"delayMessage").start();
    }
}

注意:

当前两个监听器类,需要添加到spring容器中。本项目中,定制@Enable注解,会在项目启动时,加载这些类。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({
        RocketMqProperties.class,
        RocketMqConsumerRunner.class,
        RocketMqProducerService.class, //包装了RocketMQ的发送消息的服务类
        RocketMqProperties.class,
        RocketMqFactoryBeanConfig.class,
        TransMessageRunner.class,  //延迟队列---发送本地事务消息到MQ
        TransDelayMessageRunner.class, //优先队列
        MonitorQueue.class
})
public @interface EnableRocketMq {

}

step4 后台任务发送事务消息到MQ

@Component
public class MqTransMessageTask {

    private static final Logger logger = LoggerFactory.getLogger(MqTransMessageTask.class);
    @Autowired
    private MqTransMessageService messageService;

    @Autowired
    private RocketMqProducerService rocketMqProducerService;

    /**
     * 每次获取消息数量
     */
    private static final int MAX_MESSAGE_NUM = 1000;

//spring的定时任务注解,需要再启动类上@EnableScheduling
    @Scheduled(fixedDelay = 5 * 1000)
    public void sendMessage() {
//        logger.info("====开始执行任务=====");
        List<MqTransMessageEntity> list = messageService.list(MAX_MESSAGE_NUM);
        LinkedBlockingDeque<Long> successIds = new LinkedBlockingDeque<>();
        // 如果执行期间宕机,那么这里会导致消息重发,单消费端必须要保证幂等
        list.parallelStream().forEach(messageEntity -> {
          //消息锁,构建string的key
            String key = MessageFormat.format(MessageLock.LOCK_PREFIX, messageEntity.getId());
            synchronized (key.intern()) {
                SendResult sendResult = rocketMqProducerService
                        .synSend(messageEntity.getTopic(), messageEntity.getTag(),
                                messageEntity.getMessage());
                if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
                    successIds.add(messageEntity.getId());
                }
            }


        });
        // 发送成功删除
        if (!CollectionUtils.isEmpty(successIds)) {
            messageService.del(successIds);
        }


    }

}

这里使用list.parallelStream().forEach的循环操作,对从本地消息数据库中查到的messageList,进行逐个发送。

step5 消费者消费消息

image-20230529171457111

对于消费者,也可以通过ApplicationRunner的run方法,在spring容器启动时候,自动执行

@Slf4j
public class RocketMqConsumerRunner implements ApplicationRunner {

    @Autowired
    private ApplicationContext context;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 需要执行的逻辑代码,当spring容器初始化完成后就会执行该方法。
        RocketMqConsumer consumer = context.getBean(RocketMqConsumer.class);
        consumer.start();
    }
}

当前启动类,也需要通过加入容器,这里仍旧采用自定义注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({
        RocketMqProperties.class,
        RocketMqConsumerRunner.class,

在消费消息时,需要执行如下的事务方法:

1 查询是否做过当前操作;
2 扣减库存;
3 在本地事务消息表中,记录操作已经做过(redis或者mysql);
4 发送ack;

针对消息的消费,有并发和有序消费,此处以并发消费为例:

 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(consumerGroup);
            //设置名称服务器地址
            consumer.setNamesrvAddr(this.configuration.getNamesrvAddr());
            consumer.subscribe(topic, tag);  //设置consumer订阅消息的topic和tag
            //注册消费回调
            consumer.registerMessageListener((MessageListenerConcurrently) (msgList, context) -> {
                try {
                    for (MessageExt msg : msgList) {
                        MessageListener listener = (MessageListener) entry.getValue();
                        MqAction action = listener.consume(msg, context);
                        switch (action) {
                            case ReconsumeLater:
                                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                            default:
                        }
                    }
                } catch (Exception e) {
                    LOGGER.error("消费失败", e);
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            });

此处,待完善。应该在MessageListenerConcurrently的监听器中,执行本地消费消息的事务逻辑。

public interface MessageListenerConcurrently extends MessageListener {
    ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> var1, ConsumeConcurrentlyContext var2);
}
posted @ 2023-06-12 18:35  LeasonXue  阅读(48)  评论(0编辑  收藏  举报