rocketmq-spring-boot-starter实现分布式事务步骤

1、RocketMq服务安装
(1)、下载
http://rocketmq.apache.org/release_notes/release-notes-4.6.0/
rocketmq-spring-boot-starter实现分布式事务步骤_第1张图片

(2)、安装
下载二进制版本,解压到任意目录即可。
需要注意的是:无论是windows环境还是linux环境,安装路径中都不能有空格否则启动mqnamesrv.cmd时会报 “找不到或无法加载到主类”的错。且jdk安装目录也不能有空格,否则启动mqbroker时也会报 “找不到或无法加载到主类”的错。
Linux和windows上都能运行,解压即可(以windows为例):

(3)配置环境变量
ROCKETMQ_HOME(linux中也要配置环境变量)
rocketmq-spring-boot-starter实现分布式事务步骤_第2张图片
(4)启动mqnamesrv服务
在命令提示符(cmd)窗口中执行命令 mqnamesrv.cmd 启动日志如果有 The Name Server boot success 打印则表示NameServer 启动成功

(5)启动mqbroker服务
重新开启一个命令提示符(cmd)窗口,执行命令 mqbroker.cmd -n localhost:9876,注意只要没有报错日志应该就是启动成功了,如果启动成功则不会打印任何日志,不要关闭命令提示符(cmd)窗口。需要注意的是必须先启动 NameServer 再启动 Broker,Broker 要在 NameServer 上注册。

(6)关闭broker
首先,执行命令 mqshutdown.cmd broker 关闭 Broker,如果有 Broker 运行则会打印关闭的 Broker 所在线程

(7)关闭
其次,执行命令mqshutdown.cmd namesrv 关闭 NameServer,如果有 NameServer 运行则会打印关闭的 NameServer 所在线程

(8)RocketMQ Console安装(视化管理控制台)
下载 rocketmq-externals
修改配置文件 rocketmq-externals-master/rocketmq-console/src/main/resources/application.propertis

rocketmq.config.namesrvAddr=localhost:9876

运行rocketmq-console项目
java -jar rocketmq-console-ng-1.0.1.jar
打开浏览器,输入 localhost:8080,可以看到如下界面
rocketmq-spring-boot-starter实现分布式事务步骤_第3张图片
2、producer开发
(1)、引入jar包

		<dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.0.4</version>
        </dependency>

(2)配置properties

rocketmq.name-server=localhost:9876
#这个group只在启动的时候用,在代码中没用到
rocketmq.producer.group=producer-mq-msg

(3)编写事务消息发送方法及本地事务方法

package com.esc.payservice.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.esc.payservice.dao.AccountMapper;
import com.esc.payservice.model.Account;
import com.esc.payservice.model.AccountExample;
import com.esc.payservice.service.IPayService;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;

@Service
public class MqPayService implements IPayService {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public String payMoney() {

        /**
         * 步骤一:
         * 功能:发送事务消息
         * txProducerGroup:消息生产者组
         * destination:消息发送目的地,即topic(生产者和消费者topic相同才能消费到消息)
         * message:消息
         * arg:参数
         */

        Account modAccount = new Account();
        modAccount.setAccountNo("1234567890");
        modAccount.setBalance(new BigDecimal("700.00"));//通知1234567890账户新增700元
		//将要发送的对象转换成消息需要的json字符串格式
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("modAmount", modAccount);
        jsonObject.toString();
        Message<String> message = MessageBuilder.withPayload(jsonObject.toString()).build();
        rocketMQTemplate.sendMessageInTransaction("produce_pay", "topic_mqmsg",message,null);

        return "success";
    }

    public void updateAmount(Account modAccount){
        /**
         * 步骤三
         * 功能:更新本地账户
         * 描述:事务消息发送成功后,mq收到消息并应答,开始执行本地事务
         */

        AccountExample example = new AccountExample();
        AccountExample.Criteria criteria = example.createCriteria();
        criteria.andAccountNoEqualTo("9876543210");//本地账户

        //查询本地账户
        List<Account> accountList = accountMapper.selectByExample(example);

        //扣减本地账户金额
        Account record = new Account();
        record.setBalance(accountList.get(0).getBalance().subtract(modAccount.getBalance()) );//本地账户9876543210扣减消息中的发送的金额

        //更新本地账户
        AccountExample example2 = new AccountExample();
        AccountExample.Criteria criteria2 = example2.createCriteria();
        criteria2.andIdEqualTo(accountList.get(0).getId());
        accountMapper.updateByExampleSelective(record, example2);
    }
}

注意:sendMessageInTransaction方法中的topic名称要与服务消费方中的topic名称一致
(4)编写MQ监听器,监听生产者发送消息后mq的回调以及mq回调检查本地事务的执行情况

package com.esc.payservice.listener.mqlistener;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.esc.payservice.model.Account;
import com.esc.payservice.service.IPayService;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

/**
 * 功能:本地事务监听器
 * 描述:mq收到事务消息后的应答回调以及mq回调检查本地事务执行情况
 * @Component:让监听器成为容器中的一个组件
 * @RocketMQTransactionListener:监听器,监听某个组的生产者发送消息后的回调
 *
 */
@Component
@RocketMQTransactionListener(txProducerGroup = "produce_pay")
public class ProduceMqMsgListner implements RocketMQLocalTransactionListener {

    @Autowired
    private IPayService mqPayService;

    /**
     * 步骤二:
     * 描述:mq收到事务消息后,开始执行本地事务
     * @Transactional:开启本地事务
     * @param message
     * @param o
     * @return
     */
    @Override
    @Transactional
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
        try {
            String jsonString = new String((byte[])message.getPayload());//转换成String
            JSONObject jsonObject = JSONObject.parseObject(jsonString);
            //消息中的账户为消费方中的账户
            Account account = JSONObject.parseObject(jsonObject.getString("modAmount"), Account.class);
            //更新本地账户时,只用到了消息中的金额
            mqPayService.updateAmount(account);
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e){
            return RocketMQLocalTransactionState.UNKNOWN;
        }
    }

    /**
     * 步骤四
     * 描述:mq回调检查本地事务执行情况
     * @param message
     * @return
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        //此处只是模拟,需根据实际情况编写检查本地事务执行情况方法
        if(System.currentTimeMillis() % 3 == 0){
            return RocketMQLocalTransactionState.COMMIT;
        }else{
            return RocketMQLocalTransactionState.UNKNOWN;
        }
    }
}

注意:
(1)、类上需添加@component和@RocketMQTransactionListener注解
(2)、@RocketMQTransactionListener中需指定txProducerGroup属性,且属性值与上面(3)中发送事务消息时用到的txProducerGroup值一致
(3)、回调执行本地事务的方法上要加上@Transactional注解,以执行本地事务

到此,producer的主要方法编写完成

3、consumer开发
(1)、引入jar包

		<dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.0.4</version>
        </dependency>

(2)配置properties

rocketmq.name-server=localhost:9876
#这个group只在启动的时候用,在代码中没用到
rocketmq.producer.group=consumer-mq-msg

(3)、编写consumer监听器

package com.esc.bank.listener.mqlistener;

import com.alibaba.fastjson.JSONObject;
import com.esc.bank.model.Account;
import com.esc.bank.service.IBankService;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 功能:mq消息监听器
 * 描述:监听来自topic_mqmsg的topic的消息,此topic要与生产者发送的topic一致
 * @Component:注册成为容器中的一个组件
 * @RocketMQMessageListener:消息监听器,监听来自topic_mqmsg的消息,并定义消费者自己的组
 */
@Component
@RocketMQMessageListener(topic = "topic_mqmsg",consumerGroup = "consumer_bank")
public class ConsumerMqMsgListener implements RocketMQListener<String> {

    @Autowired
    private IBankService mqBankService;
    
    /**
     * 监听消息,并执行本地事务
     * @param s
     */
    @Override
    public void onMessage(String s) {
        JSONObject jsonObject = JSONObject.parseObject(s);
        final Account modAmount = JSONObject.parseObject(jsonObject.getString("modAmount"), Account.class);
        mqBankService.mqModAmount(modAmount);
    }
}

注意:
(1)、类上需添加@Component、@RocketMQMessageListener
(2)、@RocketMQMessageListener需要topic和consumerGroup属性,且topic的属性值必须与producer中配置的topic值一致

(3)、编写consumer本地事务方法

package com.esc.bank.service.impl;

import com.esc.bank.dao.AccountMapper;
import com.esc.bank.model.Account;
import com.esc.bank.model.AccountExample;
import com.esc.bank.service.IBankService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class MqBankService implements IBankService {

    @Autowired
    private AccountMapper mapper;
    
    /**
     * @Transactional:开启本地事务
     * @param account
     * @return
     */
    @Override
    @Transactional
    public String mqModAmount(Account account) {
        AccountExample example = new AccountExample();
        AccountExample.Criteria criteria = example.createCriteria();
        criteria.andAccountNoEqualTo(account.getAccountNo());

        List<Account> accountList = mapper.selectByExample(example);

        Account record = new Account();
        record.setBalance(accountList.get(0).getBalance().add(account.getBalance()));
        AccountExample example2 = new AccountExample();
        AccountExample.Criteria criteria2 = example2.createCriteria();
        criteria2.andIdEqualTo(accountList.get(0).getId());
        mapper.updateByExampleSelective(record,example2);
        return "success";
    }
}

注意:本地事务方法上也需要添加@Transactional注解。

至此,producer和consumer的主要步骤基本编写完成,加上调用方法以及其他配置就可以使用了。
运行项目前,需先运行manamesrv和mqbroker服务,再运行producer和consumer项目

4、原理
rocketmq-spring-boot-starter实现分布式事务步骤_第4张图片
执行流程如下:

为方便理解我们以注册送积分的例子来描述 整个流程。

Producer 即MQ发送方,本例中是用户服务,负责新增用户。MQ订阅方即消息消费方,本例中是积分服务,负责新增积分。

1、Producer 发送事务消息

Producer (MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预备状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的。

本例中,Producer 发送 ”增加积分消息“ 到MQ Server。

2、MQ Server回应消息发送成功

MQ Server接收到Producer 发送给的消息则回应发送成功表示MQ已接收到消息。

3、Producer 执行本地事务

Producer 端执行业务代码逻辑,通过本地数据库事务控制。

本例中,Producer 执行添加用户操作。

4、消息投递

若Producer 本地事务执行成功则自动向MQServer发送commit消息,MQ Server接收到commit消息后将”增加积分消息“ 状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息;

若Producer 本地事务执行失败则自动向MQServer发送rollback消息,MQ Server接收到rollback消息后 将删除”增加积分消息“ 。

MQ订阅方(积分服务)消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里ack默认自动回应,即程序执行正常则自动回应ack。

5、事务回查

如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他 Producer来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息。

以上主干流程已由RocketMQ实现,对用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。

你可能感兴趣的:(rocketmq-spring-boot-starter实现分布式事务步骤)