【利用rocketmq half message,实现分布式事务管理】

  • 目前业界比较主流的分布式事务解决方法大概可以分为两种

    • 强一致性(主要解决方法代表有 2PC 、 Tcc 适用于 金融交易场景)
    • 最终一致性(主要解决方法代表有 RocketMQ事务消息 适用于常见的积分订单场景,1、比如创建订单 2、如果订单创建成功 3、增加买家积分 不管中途发生了什么 只要订单成功,那么买家的积分就一定要增加。保证最终一致性)
  • 术语简介

    • HALF MESSAGE : 事务消息 也称半消息 标识该消息处于"暂时不能投递"状态,不会被Comsumer所消费,待服务端收到生成者对该消息的commit或者rollback响应后,消息会被正常投递或者回滚(丢弃)消息
    • RMQ_SYS_TRANS_HALF_TOPIC :半消息在被投递到Mq服务器后,会存储于Topic为RMQ_SYS_TRANS_HALF_TOPIC的消费队列中
    • RMQ_SYS_TRANS_OP_HALF_TOPIC : 在半消息被commit或者rollback处理后,会存储到Topic为RMQ_SYS_TRANS_OP_HALF_TOPIC的队列中,标识半消息已被处理
  • 执行流程图
    在这里插入图片描述

  • 流程说明

    1、首先事务发起者 给RocketMQ发送一个半消息
    
    2、RocketMQ响应事务发起者 半消息发送成功
    
    3、事务发起者提交本地事务
    
    4、根据本地事务运行结果 响应RocketMQ 半消息是commit还是rollback
    
    5、如果没有收到第4步通知,则RocketMQ回查事务发起者。
    
    6、事务发起者收到回查通知检查本地消息状态
    
    7、将回查结果返回RocketMQ 根据结果commit/rollback半消息
    
    8、如果broker收到commit 则将半消息从 trans_half队列提交到真正的业务队列中。如果收到rollback或者半消息过期 则提交到trans_op_half队列中。
    
    9、如果半消息被commit 则消息订阅方法能读取消费该消息,只要保证下游消费失败重试,即可保证消息最终一致性。
    
    分析一下 可能遇到的场景
    
    1、半消息发送成功,本地事务运行失败。rollback半消息,下游业务无感知,正常。
    
    2、半消息发送成功,本地事务运行成功。但是第4步通知broker由于网络原因发送失败,但是broker有轮询机制,根据唯一id查询本地事务状态,从而提交半消息。
    
    通过以上几步就实现了RocketMQ的事务消息。
    
  • 代码实现

    @Data
    public class Order {
     
        /**
         * 订单号
         */
        private String orderNo;
     
        /**
         * 买家id
         */
        private Integer buyerId;
     
        /**
         * 支付状态 0 已支付 1 未支付 2 已超时
         */
        private Integer payStatus;
     
        /**
         * 下单日期
         */
        private Date createDate;
     
        /**
         * 金额
         */
        private Long amount;
    }
    
    @Data
    public class PointRecord {
     
        /**
         * 订单号
         */
        private String orderNo;
     
        /**
         * 用户id
         */
        private Integer userId;
     
    }
    
    @Service("payService")
    @Slf4j
    public class PayService {
     
        @Autowired
        private OrderMapper orderMapper;
     
        @Autowired
        private PointRecordMapper pointRecordMapper;
     
        /**
         * 支付功能:
         *  如果支付成功 则下游业务 也就是积分服务对应的账号需要增加积分
         *  如果支付失败,则下游业务无感知
         */
        @Transactional(rollbackFor = Exception.class)
        public void pay(String orderNo, Integer buyerId) {
            // 1、构造积分添加记录表
            PointRecord record = new PointRecord();
            record.setOrderNo(orderNo);
            record.setUserId(buyerId);
            // 2、存入数据库
            pointRecordMapper.insert(record);
             // 3、修改订单状态 为已支付
            Order order = new Order();
            order.setOrderNo(orderNo);
            order.setBuyerId(buyerId);
            //4、 更新订单信息
            orderMapper.updateOrder(order);
     
            log.info("执行本地事务,pay() ");
        }
     
        public Boolean checkPayStatus(String orderNo) {
            // 根据判断是否有PointRecord这个记录来 确实是否支付成成功 用于事务回查判断本地事务是否执行成功
            return Objects.nonNull(pointRecordMapper.getPointRecordByOrderNo(orderNo));
        }
     
    }
    
    @Component
    @Slf4j
    public class TransactionProducer implements InitializingBean {
     
        private  TransactionMQProducer producer;
     
        @Autowired
        private RocketMQProperties rocketMQProperties;
     
        @Autowired
        private TransactionListener transactionListener;
     
     
        /**
         * 构造生产者
         * @throws Exception
         */
        @Override
        public void afterPropertiesSet() throws Exception {
     
            producer = new TransactionMQProducer(rocketMQProperties.getTransactionProducerGroupName());
            producer.setNamesrvAddr(rocketMQProperties.getNamesrvAddr());
            ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("transaction-thread-name-%s").build();
            ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60,
                    TimeUnit.SECONDS, new ArrayBlockingQueue<>(30), threadFactory);
            producer.setExecutorService(executor);
     
            producer.setTransactionListener(transactionListener);
     
            producer.start();
     
        }
     
        /**
         * 真正的事物消息发送者
         */
        public void send() throws JsonProcessingException, UnsupportedEncodingException, MQClientException {
     
            ObjectMapper objectMapper = new ObjectMapper();
     
            // 模拟接受前台的支付请求
            String orderNo = UUID.randomUUID().toString();
            Integer userId = 1;
            // 构造发送的事务 消息
            PointRecord record = new PointRecord();
            record.setUserId(userId);
            record.setOrderNo(orderNo);
     
            Message message = new Message(rocketMQProperties.getTopic(), "", record.getOrderNo(),
                    objectMapper.writeValueAsString(record).getBytes(RemotingHelper.DEFAULT_CHARSET));
     
            producer.sendMessageInTransaction(message, null);
     
            log.info("发送事务消息, topic = {}, body = {}", rocketMQProperties.getTopic(), record);
        }
    }
    
    @Component
    @Slf4j
    public class PointTransactionListener implements TransactionListener {
     
        @Autowired
        private PayService payService;
     
        /**
         * 根据消息发送的结果 判断是否执行本地事务
         * @param message
         * @param o
         * @return
         */
        @Override
        public LocalTransactionState executeLocalTransaction(Message message, Object o) {
            // 根据本地事务执行成与否判断 事务消息是否需要commit与 rollback
            ObjectMapper objectMapper = new ObjectMapper();
            LocalTransactionState state = LocalTransactionState.UNKNOW;
            try {
                PointRecord record = objectMapper.readValue(message.getBody(), PointRecord.class);
                payService.pay(record.getOrderNo(), record.getUserId());
                state = LocalTransactionState.ROLLBACK_MESSAGE;
            } catch (UnsupportedEncodingException e) {
                log.error("反序列化消息 不支持的字符编码:{}", e);
                state = LocalTransactionState.ROLLBACK_MESSAGE;
            } catch (IOException e) {
                log.error("反序列化消息失败 io异常:{}", e);
                state = LocalTransactionState.ROLLBACK_MESSAGE;
            }
            return state;
        }
     
        /**
         * RocketMQ 回调 根据本地事务是否执行成功 告诉broker 此消息是否投递成功
         * @param messageExt
         * @return
         */
        @Override
        public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
            ObjectMapper objectMapper = new ObjectMapper();
            LocalTransactionState state = LocalTransactionState.UNKNOW;
            PointRecord record = null;
            try {
                record = objectMapper.readValue(messageExt.getBody(), PointRecord.class);
            } catch (IOException e) {
                log.error("回调检查本地事务状态异常: ={}", e);
     
            }
            try {
                //根据是否有transaction_id对应转账记录 来判断事务是否执行成功
                boolean isCommit = payService.checkPayStatus(record.getOrderNo());
                if (isCommit) {
                    state = LocalTransactionState.COMMIT_MESSAGE;
                } else {
                    state = LocalTransactionState.ROLLBACK_MESSAGE;
                }
            } catch (Exception e) {
                log.error("回调检查本地事务状态异常: ={}", e);
            }
            return state;
     
        }
    }
    

本地事务失效场景:

- 1、数据库引擎不支持事务,如mysql的MyISAM
- 2、类没有被spring管理
- 3、方法不是public修饰
- 4、非事务方法调用事务方法
- 5、设置事务传播行为 - 不已事务运行,propagation 的属性值设置为:Propagation.NOT_SUPPORTED、Propagation.NEVER
- 6、异常被捕获
- 7、rollbackFor,未指定到对应异常
  • 参考:https://blog.csdn.net/weixin_44688301/article/details/116783136
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值