关于Spring Cloud基于RocketMQ可靠消息最终一致性实现分布式事务

1. 安装搭建 RocketMQ 服务器

搭建单机 Rocketmq 服务器笔记:
关于Linux中安装Rocketmq说明

搭建双主双从同步复制 Rocketmq 服务器笔记:
关于LInux中RocketMQ双主双从同步复制集群说明

2. 基于 RocketMQ 可靠消息的分布式事务方案原理

关于RocketMQ原理与应用说明

3. 基于 RocketMQ 可靠消息的分布式事务方案实现

3.1 准备工作

3.1.1 创建tx_table表

Rocketmq收到事务消息后,会等待生产者提交或回滚该消息。如果无法得到生产者的提交或回滚指令,则会主动向生产者询问消息状态,称为回查。

在 order 项目中,为了让Rocketmq可以回查到事务的状态,需要记录事务的状态,所以我们添加一个事务的状态表来记录事务状态。

CREATE TABLE tx_table(
	`xid` char(32) PRIMARY KEY COMMENT '事务id',
	`status` int COMMENT '0-提交,1-回滚,2-未知',
	`created_at` BIGINT UNSIGNED NOT NULL COMMENT '创建时间'
);

3.1.2 创建order-topic

使用 order-topic 来收发消息,在 Rocketmq 服务器上创建这个 Topic:在这里插入图片描述

3.1.3 添加RocketMQ 依赖

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

3.1.4 修改application.yml配置

rocketmq:
  name-server: 192.168.126.126:9876
  producer:
    group: order-group

3.2 添加生产者代码

3.2.1 添加TxMapper访问事务状态表

事务状态保存到tx_table 表,在 TxMapper 接口和 TxMapper.xml 中添加事务状态数据的读写方法。

本地事务执行后要保存事务信息(事务id、事务状态)到数据库,以便之后进行事务回查,首先创建封装事务信息的类 TxInfo :

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TxInfo {
    private String xid;
    private Long created;
    private Integer status;
}

TxMapper 接口:

public interface TxMapper extends BaseMapper<TxInfo> {
	@select("SELECT COUNT(1) FROM tx_table WHERE xid=#{xid}")
    Boolean exists(String xid);
}

3.2.2 TxOrderService 发送事务消息

TxAccountMessage 封装发送给账户服务的数据:用户id和扣减金额。另外还封装了事务id。

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TxAccountMessage {
    Long userId;
    BigDecimal money;
    String xid;
}

在业务方法 create() 中不直接保存订单,而是发送事务消息。

消息发出后,会触发 TxListener 执行本地事务,它执行时会回调这里的 doCreate()方法完成订单的保存。

@Slf4j
@Primary
@Service
public class TxOrderService implements OrderService {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private TxMapper txMapper;
    @Autowired
    EasyIdGeneratorClient easyIdGeneratorClient;

    /*
    创建订单的业务方法
    这里修改为:只向 Rocketmq 发送事务消息。
     */
    @Override
    public void create(Order order) {
        // 产生事务ID
        String xid = UUID.randomUUID().toString().replace("-", "");

        //对事务相关数据进行封装,并转成 json 字符串
        TxAccountMessage sMsg = new TxAccountMessage(order.getUserId(), order.getMoney(), xid);
        String json = JsonUtil.to(sMsg);

        //json字符串封装到 Spring Message 对象
        Message<String> msg = MessageBuilder.withPayload(json).build();

        //发送事务消息
        rocketMQTemplate.sendMessageInTransaction("order-topic:account", msg, order);
        log.info("事务消息已发送");
    }

    //本地事务,执行订单保存
    //这个方法在事务监听器中调用
    @Transactional
    public void doCreate(Order order, String xid) {
        log.info("执行本地事务,保存订单");

        // 从全局唯一id发号器获得id
        Long orderId = easyIdGeneratorClient.nextId("order_business");
        order.setId(orderId);

        orderMapper.create(order);

        log.info("订单已保存! 事务日志已保存");
    }
}

3.2.3 TxListener 事务监听器

发送事务消息后会触发事务监听器执行。

事务监听器有两个方法:

  • executeLocalTransaction(): 执行本地事务
  • checkLocalTransaction(): 负责响应Rocketmq服务器的事务回查操作
@Slf4j
@Component
@RocketMQTransactionListener
public class TxListener implements RocketMQLocalTransactionListener {
    @Autowired
    private TxOrderService orderService;
    @Autowired
    private TxMapper txMapper;

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
        log.info("事务监听 - 开始执行本地事务");

        // 监听器中得到的 message payload 是 byte[]
        String json = new String((byte[]) message.getPayload());
        String xid = JsonUtil.getString(json, "xid");

        log.info("事务监听 - "+json);
        log.info("事务监听 - xid: "+xid);

        RocketMQLocalTransactionState state;
        int status = 0;
        Order order = (Order) o;

        try {
            orderService.doCreate(order, xid);

            log.info("本地事务执行成功,提交消息");
            state = RocketMQLocalTransactionState.COMMIT;
            status = 0;
        } catch (Exception e) {
            e.printStackTrace();
            log.info("本地事务执行失败,回滚消息");
            state = RocketMQLocalTransactionState.ROLLBACK;
            status = 1;
        }

        TxInfo txInfo = new TxInfo(xid, System.currentTimeMillis(), status);
        txMapper.insert(txInfo);

        return state;
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        log.info("事务监听 - 回查事务状态");

        // 监听器中得到的 message payload 是 byte[]
        String json = new String((byte[]) message.getPayload());
        String xid = JsonUtil.getString(json, "xid");

        TxInfo txInfo = txMapper.selectById(xid);
        if (txInfo == null) {
            log.info("事务监听 - 回查事务状态 - 事务不存在:"+xid);
            return RocketMQLocalTransactionState.UNKNOWN;
        }

        log.info("事务监听 - 回查事务状态 - "+ txInfo.getStatus());

        switch (txInfo.getStatus()) {
            case 0: return RocketMQLocalTransactionState.COMMIT;
            case 1: return RocketMQLocalTransactionState.ROLLBACK;
            default: return RocketMQLocalTransactionState.UNKNOWN;
        }
    }
}

3.3 添加消费者代码

3.3.1 修改application.yml 配置

rocketmq:
  name-server: 192.168.126.129:9876

3.3.2 TxConsumer 接收事务消息

接收的消息转换成 TxAccountMessage 对象,这里先创建这个类:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TxAccountMessage {
    Long userId;
    BigDecimal money;
    String xid;
}

TxConsumer 实现消息监听,收到消息后完成扣减金额业务:

@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "account-consumer-group", topic = "order-topic", selectorExpression = "account")
public class TxConsumer implements RocketMQListener<String> {
    @Autowired
    private AccountService accountService;

    @Override
    public void onMessage(String msg) {
        TxAccountMessage txAccountMessage = JsonUtil.from(msg, new TypeReference<TxAccountMessage>() {});
        log.info("收到消息: "+txAccountMessage);

        accountService.decrease(txAccountMessage.getUserId(), txAccountMessage.getMoney());
    }
}

3.3.3 AccountServiceImpl 添加事务注解

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountMapper accountMapper;

    @Transactional
    @Override
    public void decrease(Long userId, BigDecimal money) {
        accountMapper.decrease(userId,money);
    }
}

3.4 order 本地事务失败测试

@Slf4j
@Primary
@Service
public class TxOrderService implements OrderService {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private TxMapper txMapper;
    @Autowired
    EasyIdGeneratorClient easyIdGeneratorClient;

    /*
    创建订单的业务方法
    这里修改为:只向 Rocketmq 发送事务消息。
     */
    @Override
    public void create(Order order) {
        // 产生事务ID
        String xid = UUID.randomUUID().toString().replace("-", "");

        //对事务相关数据进行封装,并转成 json 字符串
        TxAccountMessage sMsg = new TxAccountMessage(order.getUserId(), order.getMoney(), xid);
        String json = JsonUtil.to(sMsg);

        //json字符串封装到 Spring Message 对象
        Message<String> msg = MessageBuilder.withPayload(json).build();

        //发送事务消息
        log.info("开始发送事务消息");
        rocketMQTemplate.sendMessageInTransaction("order-topic:account", msg, order);
        log.info("事务消息已发送");
    }

    //本地事务,执行订单保存
    //这个方法在事务监听器中调用
    @Transactional
    public void doCreate(Order order, String xid) {
        log.info("执行本地事务,保存订单");

        // 从全局唯一id发号器获得id
        Long orderId = easyIdGeneratorClient.nextId("order_business");
        order.setId(orderId);

        orderMapper.create(order);

        if (Math.random() < 0.5) {
            throw new RuntimeException("模拟异常");
        }

        log.info("订单已保存! 事务日志已保存");
    }
}

3.5 account 本地事务失败测试

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountMapper accountMapper;

    @Transactional
    @Override
    public void decrease(Long userId, BigDecimal money) {
        accountMapper.decrease(userId,money);

        if (Math.random() < 0.5) {
            throw new RuntimeException("模拟异常");
        }
    }
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值