基于RocketMQ的分布式事务

2 篇文章 0 订阅
2 篇文章 0 订阅

RocketMQ消息队列

基于RocketMQ的分布式事务

在介绍RocketMQ的分布式事务之前,先来了解下什么事分布式事务?

一、分布式事务

  • 简介

在分布式系统中,不止使用一个数据库,比如订单系统使用db_order数据库,产品系统使用的是db_product数据库,在订单系统中只能保证订单相关操作的事务,在产品系统中只能保证产品相关操作的事务。比如:如果在订单系统中进行生成订单、扣减库存的业务,如果出现异常,那么创建订单的事务会回滚,而扣减库存的事务则不会,因为本地事务是不能夸数据库的。跨库的事务就属于分布式事务。

把分布式系统中两个相关操作看成是一个单元,比如创建订单和修改库存的操作,该单元要么一起成功,要么一起失败,这就是分布式事务。

  • 关于分布式事务你不得不知的两个理论:

1、CAP定理
CAP原则又称CAP定理,指的是在一个分布式系统中,WEB服务无法同时满足以下3个特性:

一致性(Consistency) : 在分布式系统中数据一旦更新,所有数据变动都是同步的

可用性(Availability) : 好的响应性能,每个操作都必须有预期的响应结束

分区容错性(Partition tolerance) : 在网络分区的情况下,即使出现单个节点无法可用,系统依然正常对外提供服务

首先在分布式系统中,横向扩展策略依赖于数据分区,所以一般会在一致性和可用性上做出牺牲。

2、BASE理论
BASE理论中的三个特性:

Basically Available(基本可用)

Soft state(软状态)

Eventually consistent(最终一致性)

三个特性分别指的是:

(1)基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性——但请注意,这绝不等价于系统不可用。

(2)软状态,和硬状态对应,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统不同节点的数据副本之间进行数据同步的过程存在延时。

(3)最终一致性强调的是系统所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要试试保证系统数据的强一致性。

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consist ency)。
————————————————
版权声明:本文为CSDN博主「坏菠萝」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/abcwanglinyong/article/details/82116669

二、分布式事务解决方案

分布式事务解决方案有很多种,这里针对RocketMQ本身介绍下两阶段提交(2PC)。因为本身RocketMQ的分布式事务消息就是基于消息中间件模拟的两阶段提价(2PC)。

  • 主要分为以下几个步骤
  1. 系统A先向消息中间件发送一条预备消息,消息中间件保存还该消息后向系统A发送确认消息

  2. 系统A接收到MQ的确认消息后,执行本地事务

  3. 系统A根据本地事务执行结果再向MQ发送提交信息,以提交二次确认

  4. MQ收到二次确认消息后,不预备消息标记为可投递,订阅者最终讲接收到该消息

  • 在这过程中是如何进行回滚操作?
  1. 在本地事务未执行之前,也就是上面的1和2出错的话,不会进入后面的阶段,也就不会有问题
  2. 第3步出错系统A会实现一个消息回查接口,MQ服务端在等不到系统A反馈时会轮询该消息回查接口,检查系统A的本地事务执行结果。如果事务成功执行则进入下个阶段,否则回滚到第一步中。
  3. 第4布出错,此时系统A的本地事务已经提交成功,MQ服务端通过回查接口能够检查到该事务执行成功,那么由MQ服务端将预备消息标记为可投递,从而完成消息事务的处理。

至此可实现跨系统是分布式事务了。

整体的分布式事务被拆分成一个消息事务(系统A的本地事务+发消息)+系统B的本地事务,系统B的操作由消息驱动,这样系统A和系统B的事务便绑定在一起。

  • RocketMQ整体交互流程图如下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jR0Ucv9O-1580922189411)(C:\Users\Administrator\Desktop\9873681-1e6612b094cb561d.png)]

  1. 事务发起方首先发送准本预备消息到MQServer
  2. MQServer向事务发起方ACK确认消息发送成功
  3. 事务发起方接收到确认消息后执行事务
  4. 事务发起方根据本地事务的执行结果返回commit或rollback给MQserver。如果发送的是rollback,则MQ将删除该预备消息不进行下发;否则MQ会把该预备消息发送给Consumer
  5. 如果在执行本地事务过程中该应用挂了或者超时,第4步提交的二次确认消息最终没有到达MQServer,MQServer将在经过一定时间后对该消息发起消息回查,通过不停的询问同组的其他的Producer来获取状态
  6. 发送方接受到回查消息后查询对应消息的本地事务执行结果
  7. 根据回查的本地事务的最终执行结果再次提交二次确认
  8. 消费端的消息成功机制是由MQ保证的

三、RocketMQ事务消息实例

建议大家使用MQ的时候要选择MQ版本4.3以上的,而且pom文件引入的rocketmq-client版本号要与你服务器上的版本号一致,否则可能会出现No route info of this topic这样的异常信息,被这个坑惨了

  • 事务消息生产者

    /**
     * @Auther: Yonggang Shi
     * @Date: 2020/02/03 17:11
     * @Description:
     */
    public class TransactionProducer {
    
        private static  Logger logger = LoggerFactory.getLogger(TransactionProducer.class);
    
        public static void main(String[] args) throws Exception {
    
            TransactionMQProducer producer = new TransactionMQProducer("transaction_producer_group");
    
            producer.setNamesrvAddr(Consts.MQ_ADDR);
            ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), (Runnable r) ->
            {
                Thread thread = new Thread(r);
                thread.setName("client-transaction-msg-check-thread");
                return thread;
            });
            //设置本地事务执行的线程池
            producer.setExecutorService(executorService);
            producer.setTransactionListener(new TransactionListener() {
                @Override
                public LocalTransactionState executeLocalTransaction(Message message, Object o) {
                    //本地事务处理逻辑
                    logger.info("本地事务执行。。。");
                    logger.info("消息标签:"+new String(message.getTags()));
                    logger.info("消息内容:"+new String(message.getBody()));
                    String tag = message.getTags();
                    if (tag.equals("Transaction1")){
                        //消息的标签如果是Transaction1,则返回事务失败标记
                        logger.error("模拟本地事务执行失败");
                        return LocalTransactionState.ROLLBACK_MESSAGE;
                    }
                    logger.info("模拟本地事务成功");
                    return LocalTransactionState.COMMIT_MESSAGE;
                }
    
                @Override
                public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
                    //消息回查接口
                    logger.info("服务器调用消息回查接口");
                    logger.info("消息标签:"+new String(messageExt.getTags()));
                    logger.info("消息内容:"+new String(messageExt.getBody()));
                    return LocalTransactionState.COMMIT_MESSAGE;
    
                }
            });
            producer.start();
            for (int i =0 ;i<2;i++){
                Message message=new Message("TopicTransaction","Transaction"+i,("Hello Rocakmq transaction").getBytes());
                SendResult sendResult =producer.sendMessageInTransaction(message,null);
                logger.info(String.valueOf(sendResult));
                logger.info("");
                TimeUnit.MICROSECONDS.sleep(10);
            }
            for (int i =0 ;i<100;i++){
                Thread.sleep(1000);
            }
            producer.shutdown();
        }
    }
    
    
    

    与普通生产者不同的地方是,这里需要调用setTransactionListener方法,通过自己实现TransactionListener接口的executeLocalTransaction执行本地事务和checkLocalTransaction消息回查方法

    执行结果说明有个事务消息挂了,实际上发送过去的就只有一条

在这里插入图片描述

  • 事务消息消费者

    /**
     * @Auther: Yonggang Shi
     * @Date: 2020/02/03 22:11
     * @Description:
     */
    public class TransactionConsumer {
    
        private static   Logger logger = LoggerFactory.getLogger(TransactionConsumer.class);
    
        public static void main(String[] args) throws MQClientException {
            DefaultMQPushConsumer consumer =new DefaultMQPushConsumer("transaction_consumer_group");
            consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
            consumer.setNamesrvAddr(Consts.MQ_ADDR);
            consumer.subscribe("TopicTransaction","*");
            consumer.registerMessageListener(new MessageListenerConcurrently() {
                private Random random = new Random();
                @Override
                public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                    for (MessageExt msg: list) {
                        logger.info("消息消费者接收到消息"+msg);
                        logger.info("接收到的消息标签:"+new String(msg.getTags()));
                        logger.info("接收到消息内容:"+new String(msg.getBody()));
                    }
                    try {
                        //模拟业务处理
                        TimeUnit.SECONDS.sleep(random.nextInt(5));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        return  ConsumeConcurrentlyStatus.RECONSUME_LATER;
                    }
    
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
            });
            consumer.start();
        }
    }
    
    

    执行事务消息消费者,仅消费了Transaction0,说明本地事务消息失败的没有被发过来

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Eh4tApj-1580922189414)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1580798597842.png)]

四、分布式事务实现新用户注册送积分

背景介绍:新用户注册赠送积分。这里的流程就是把用户表格积分表分别放在不同的库,实现两者的跨库事务操作。

这里我们主要介绍下核心代码,完整的源码可以follow我的大型仓库 https://github.com/rainandsunshine/Poet.git

  • 下面分为两块,一个是配置双数据源,单个数据源不能实现跨库操作;二是RocketMQ的分布式事务在具体业务中如何实现。
  1. 配置双数据源,也就是一个系统里面连接两个库。这个项目使用的是JdbcTemplate作为持久层的开发,在SpringBoot中直接新建个配置类,给数据源都绑定好。

    /**
     * @Auther: Yonggang Shi
     * @Date: 2020/02/04 22:58
     * @Description: 双数据源配置
     */
    @Configuration
    public class DataSourceConfig {
        @Bean(name = "testDataSource")
        @Primary
        @Qualifier("testDataSource")
        @ConfigurationProperties(prefix="spring.datasource.hikari.mysql")
        public DataSource testDataSource() {
            return DataSourceBuilder.create().build();
        }
    
        @Bean(name = "formalDataSource")
        @Qualifier("formalDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.formal.mysql")
        public DataSource formalDataSource() {
            return DataSourceBuilder.create().build();
        }
    
        @Bean(name="testJdbcTemplate")
        public JdbcTemplate testJdbcTemplate (
                @Qualifier("testDataSource")  DataSource testDataSource ) {
            return new JdbcTemplate(testDataSource);
        }
    
        @Bean(name = "formalJdbcTemplate")
        public JdbcTemplate formalJdbcTemplate(
                @Qualifier("formalDataSource") DataSource formalDataSource){
            return new JdbcTemplate(formalDataSource);
        }
        /*
         * @Description:用户DAO层bean, 通过参数注入对应的jdbctemplate,实现对库绑定
         * @Param: [jdbcTemplate]
         * @Return: cn.loveyx815.rocketmq.mqtransaction.dao.UserDao
         * @Author: Yonggang Shi
         * @Date: 2020/2/5/005 下午 11:57
         */
        @Bean(name="userDao")
        public UserDao getUserDao(@Qualifier("testJdbcTemplate") JdbcTemplate jdbcTemplate){
            UserDao userDao =new UserDao();
            userDao.setJdbcTemplate(jdbcTemplate);
            return  userDao;
        }
        /*
         * @Description:积分DAO层bean, 通过参数注入对应的jdbctemplate,实现对库绑定
         * @Param: [jdbcTemplate]
         * @Return: cn.loveyx815.rocketmq.mqtransaction.dao.UserDao
         * @Author: Yonggang Shi
         * @Date: 2020/2/5/005 下午 11:57
         */
        @Bean(name="pointDao")
        public PointDao getPointDao(@Qualifier("formalJdbcTemplate") JdbcTemplate jdbcTemplate){
            PointDao pointDao =new PointDao();
            pointDao.setJdbcTemplate(jdbcTemplate);
            return  pointDao;
        }
    }
    
    

    配置多数据源一定有个主要的数据源,不然程序加载就不能识别默认的,导致报错。@Primary注解加在你想加的DataSource上。

    先注入两个DataSource的bean后,再分别注入JdbcTemplate中,最后把持久层的userDAO和pointDAO分贝注入不同的Jdbctemplate的bean,这样就可以实现多数经验绑定了。

  2. 下面就是介绍事务消息生产者和消费者

    除了那些基本的配置之外,主要的是在事务消息可以实现分布式事务,基于2PC(二阶段提交)前文已经介绍过了。

    特别的地方就是在消息生产者生产的时候需要添加个本地事务监听器,用来监听本地事务执行状态,然后再发送消息。

    而消费者也需要自己实现MessageListenerConcurrently接口的方法,可以在消费消息的时候做一些业务处理

  • 消息监听器TransactionMessageListener

    /**
     * @Auther: Yonggang Shi
     * @Date: 2020/02/04 18:50
     * @Description: 事务消息监听器,用作消费者消费的监听逻辑实现
     */
    @Component
    public class TransactionMessageListener implements MessageListenerConcurrently {
        private Logger logger = LoggerFactory.getLogger(this.getClass());
    
        @Resource
        private PointService pointService;
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
            try {
                for (MessageExt message:list){
                    logger.info("消息消费者接收到消息:"+message);
                    logger.info("接收到消息内容:"+new String (message.getBody()));
                    //从消息体中获取积分消息对象
                    UserPointMessage userPointMessage= JSON.parseObject(message.getBody(),UserPointMessage.class);
                    if (userPointMessage!=null){
                        Point point = new Point();
                        point.setUserId(userPointMessage.getUserId());
                        point.setAmount(userPointMessage.getAmount());
                        //保存用户积分记录并提交本地事务
                        pointService.savePoint(point);
                    }
                }
            }catch (Exception e){
                logger.error("消息消费出错"+e);
                return  ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
            //正常消费成功
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    }
    
    
  • 本地事务监听器UserLocalTransactionListener

    /**
     * @Auther: Yonggang Shi
     * @Date: 2020/02/04 18:25
     * @Description: 本地事务监听器,用作生产者生产消息的逻辑
     */
    @Component
    public class UserLocalTransactionListener implements TransactionListener {
        private Logger logger = LoggerFactory.getLogger(this.getClass());
    
        @Autowired
        private UserService userService;
        @Override
        public LocalTransactionState executeLocalTransaction(Message message, Object o) {
            //本地事务处理逻辑
            logger.info("本地事务执行。。。");
            logger.info("消息标签:"+new String(message.getTags()));
            logger.info("消息内容:"+new String(message.getBody()));
            //从消息体重获取积分消息对象
            UserPointMessage userPointMessage = JSON.parseObject(message.getBody(), UserPointMessage.class);
            //保存用户记录并提交本地事务
            userService.saveUser(userPointMessage.getUserId(),userPointMessage.getUserName());
            return LocalTransactionState.COMMIT_MESSAGE;
        }
    
        @Override
        public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
            //消息回查接口
            logger.info("服务器调用消息回查接口");
            logger.info("消息标签:"+new String(messageExt.getTags()));
            logger.info("消息内容:"+new String(messageExt.getBody()));
            //从消息体重获取积分消息对象
            UserPointMessage userPointMessage = JSON.parseObject(messageExt.getBody(),UserPointMessage.class);
            if (userPointMessage!= null){
                String userId = userPointMessage.getUserId();
                if (userService.getById(userId) != null){
                    logger.info("本地插入用户表成功!");
    //                表示本地事务执行成功
                    return LocalTransactionState.COMMIT_MESSAGE;
                }
            }
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }
    
    

    通过使用@Component注解来标识这两个监听器注入Spring容器,然后在生产者、消费者配置类中分别引用这两个监听器bean。

  • 生产者

    /**
     * @Auther: Yonggang Shi
     * @Date: 2020/02/04 17:55
     * @Description: 消息生产者
     */
    public class TransactionSpringProducer {
        private Logger logger = LoggerFactory.getLogger(getClass());
        private String producerGroupName;
        private String nameServerAdd;
        private int corePoolSize = 1;
        private int maximumPoolSize = 5;
        private long keepAliveTime = 100;
        private TransactionMQProducer producer;
        private TransactionListener transactionListener;
        public TransactionSpringProducer(String producerGroupName,String nameServerAdd,int corePoolSize,int maximumPoolSize,long keepAliveTime,TransactionListener transactionListener){
            this.corePoolSize=corePoolSize;
            this.keepAliveTime=keepAliveTime;
            this.maximumPoolSize=maximumPoolSize;
            this.nameServerAdd=nameServerAdd;
            this.producerGroupName=producerGroupName;
            this.transactionListener=transactionListener;
        }
    
        public  void init() throws Exception{
            logger.info("开始启动消息生产者服务。。。");
    
            producer = new TransactionMQProducer(producerGroupName);
            producer.setNamesrvAddr(nameServerAdd);
            ExecutorService executorService = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime, TimeUnit.SECONDS,new ArrayBlockingQueue<>(2000),(Runnable r )->{
                Thread thread = new Thread(r);
                thread.setName("client-transaction-msg-check-thread");
                return thread;
            });
            producer.setExecutorService(executorService);
            producer.setTransactionListener(transactionListener);
            producer.start();
            logger.info("消息生产者已启动!!!");
        }
    
        public void destory(){
            logger.info("开始关闭消息生产服务。。");
            producer.shutdown();
            logger.info("生产者服务已关闭");
        }
        public DefaultMQProducer getProducer(){
            return producer;
        }
    
    
    }
    
  • 消费者

    /**
     * @Auther: Yonggang Shi
     * @Date: 2020/02/04 18:41
     * @Description: 消费者
     */
    public class TransactionSpringConsumer {
    
        private Logger logger = LoggerFactory.getLogger(this.getClass());
    
        private String consumerGropuName;
        private String nameServerAddr;
        private String topicName;
        private DefaultMQPushConsumer consumer;
        private MessageListenerConcurrently messageListener;
    
        public TransactionSpringConsumer(String consumerGropuName,String nameServerAddr,String topicName,MessageListenerConcurrently messageListener){
            this.consumerGropuName=consumerGropuName;
            this.messageListener=messageListener;
            this.nameServerAddr=nameServerAddr;
            this.topicName=topicName;
        }
    
        public void init () throws  Exception{
            logger.info("开始启动消息消费者服务。。。");
            consumer=new DefaultMQPushConsumer(consumerGropuName);
            consumer.setNamesrvAddr(nameServerAddr);
            consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
            consumer.subscribe(topicName,"*");
            consumer.registerMessageListener(messageListener);
            consumer.start();
            logger.info("消息消费者服务启动成功");
        }
    
        public void destory(){
            logger.info("开始关闭消息消费者服务。。");
            consumer.shutdown();
            logger.info("消费者服务已关闭");
        }
    
        public  DefaultMQPushConsumer getConsumer(){
            return  consumer;
        }
    }
    
    
  • 生产者、消费者配置类

    /**
     * @Auther: Yonggang Shi
     * @Date: 2020/02/04 18:41
     * @Description: 消费者
     */
    public class TransactionSpringConsumer {
    
        private Logger logger = LoggerFactory.getLogger(this.getClass());
    
        private String consumerGropuName;
        private String nameServerAddr;
        private String topicName;
        private DefaultMQPushConsumer consumer;
        private MessageListenerConcurrently messageListener;
    
        public TransactionSpringConsumer(String consumerGropuName,String nameServerAddr,String topicName,MessageListenerConcurrently messageListener){
            this.consumerGropuName=consumerGropuName;
            this.messageListener=messageListener;
            this.nameServerAddr=nameServerAddr;
            this.topicName=topicName;
        }
    
        public void init () throws  Exception{
            logger.info("开始启动消息消费者服务。。。");
            consumer=new DefaultMQPushConsumer(consumerGropuName);
            consumer.setNamesrvAddr(nameServerAddr);
            consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
            consumer.subscribe(topicName,"*");
            consumer.registerMessageListener(messageListener);
            consumer.start();
            logger.info("消息消费者服务启动成功");
        }
    
        public void destory(){
            logger.info("开始关闭消息消费者服务。。");
            consumer.shutdown();
            logger.info("消费者服务已关闭");
        }
    
        public  DefaultMQPushConsumer getConsumer(){
            return  consumer;
        }
    }
    
    

    这样就已经完成了分布式事务生产消费的工作,还有相关的service和dao代码就不贴了,这里 都有!

  • 单元测试分布式事务生产消费

    /**
     * @Auther: Yonggang Shi
     * @Date: 2020/02/04 23:24
     * @Description:
     */
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = {RocketmqApplication.class})
    public class MQConfigTest {
        @Autowired
        private UserService userService;
    
    
    
        @Test
        public void newUser() throws Exception{
            userService.newUserAndPoint("分布式事务测试",100);
            Thread.sleep(5000);
        }
    }
    

    结果如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-33Npo5BI-1580922189416)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1580921850968.png)]

我们再 看下两个库是否也更新了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4FtAkAIT-1580922189417)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1580922010823.png)]

至此分布式事务已完成实现

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值