可靠消息最终一致性分布式事务

系列文章目录

提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加
可靠消息最终一致性分布式事务


提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:这里可以添加本文要记录的大概内容:

在当今的分布式系统中,确保数据的一致性和可靠性是至关重要的。而传统的分布式事务处理方案,如两阶段提交(2PC),虽然可以保证原子性,但在面对高并发和分布式环境时,往往会面临性能和扩展性的问题。因此,可靠消息最终一致性分布式事务(Reliable Message Eventually Consistent Distributed Transactions,简称 RMECT)应运而生。
RMECT 是一种基于消息队列的事务处理方案,它通过将事务的提交和消息的可靠传输分离,实现了最终一致性。在 RMECT 中,事务的发起方首先执行本地事务,并将事务的结果作为消息发送到消息队列中。然后,事务的参与方从消息队列中接收消息,并根据消息中的事务结果执行相应的操作。通过这种方式,RMECT 可以在不牺牲性能和扩展性的前提下,保证数据的最终一致性。
在本博客中,我们将深入探讨可靠消息最终一致性分布式事务的原理、实现以及应用场景。我们将介绍 RMECT 的核心概念和架构,以及如何使用消息队列来实现可靠的消息传输。我们还将探讨 RMECT 在实际应用中的优势和挑战,并提供一些实际的案例分析。
通过阅读本博客,您将对可靠消息最终一致性分布式事务有更深入的了解,并了解如何在实际应用中应用这种事务处理方案来确保数据的一致性和可靠性。无论您是分布式系统的开发者、架构师还是对分布式事务感兴趣的读者,本博客都将为您提供有价值的信息和见解。让我们一起探索可靠消息最终一致性分布式事务的世界,为构建高可靠的分布式系统奠定坚实的基础!


提示:以下是本篇文章正文内容,下面案例可供参考

一、实现原理

RocketMQ实现可靠消息最终一致性的原理是通过发送方在业务执行开始时向消息队列中投递“半消息”来实现的。“半消息”是指暂时不会真正投递的消息,当发送方将消息成功发送给MQ服务端且未收到该消息的二次确认结果时,该消息的状态为“暂时不可投递”状态(即状态未知),即为“半消息”。
如果由于网络闪断、生产者应用重启等原因导致事务消息的二次确认丢失,MQ服务端会通过扫描发现某条消息长期处于“半消息”状态,此时MQ服务端会主动向生产者查询该消息的最终状态是处于提交(commit)还是回滚(Rollback),这个过程称为消息回查。

实现流程

  1. 事务发起方(发送方)在执行本地事务之前,先发送一条预备消息(Prepare Message)到 RocketMQ 消息队列。
  2. RocketMQ 确认收到预备消息,并将其存储在消息队列中。
  3. 事务发起方执行本地事务,如果本地事务执行成功,则发送一条确认消息(Commit Message)给 RocketMQ;如果本地事务执行失败,则发送一条取消消息(Rollback Message)给 RocketMQ。
  4. 如果事务参与方未收到消息或者执行事务失败,且RocketMQ未删除保存的消息数据,则RocketMQ会回查事务发起方的接口,查询事务状态,以此确认是再次提交事务还是回滚事务。
  5. 事务发起方查询本地数据库,确认事务是否是执行成功的状态。
  6. 事务发起方根据查询到的事务状态,向RocketMQ发送提交事务或者回滚事务的消息。
  7. 如果第六步中,事务发起方向RocketMQ发送的是提交事务的消息,则RocketMQ会向事务参与方投递消息。
  8. 如果第七步中,事务发起方向RocketMQ发送的是回滚事务的消息,则RocketMQ不会向事务参与方投递消息,并且会删除内部存储的消息数据。
  9. 如果RocketMQ向事务参与方投递的是执行本地事务的消息,则事务参与方会执行本地事务,向本地数据库中插入、更新、删除数据。
    10.如果RocketMQ向事务参与方投递的是查询本地事务状态的消息,则事务参与方会查询本地数据库中事务的执行状态。

二、可靠消息最终一致性分布式事务和Hmily实现的最终一致性分布式事务的区别

  • 可靠消息最终一致性分布式事务:可靠消息最终一致性分布式事务通常使用消息队列来实现,它的核心思想是将事务的操作拆分成两个阶段:准备阶段和提交阶段。在准备阶段,事务发送一个准备消息到消息队列,并执行本地事务。如果本地事务执行成功,则发送一个提交消息到消息队列;如果本地事务执行失败,则发送一个回滚消息到消息队列。在提交阶段,事务监听消息队列,如果收到提交消息,则执行提交操作;如果收到回滚消息,则执行回滚操作。通过这种方式,可靠消息最终一致性分布式事务保证了事务的可靠性和消息的可靠传输。
  • 最终一致性分布式事务:最终一致性分布式事务通常使用分布式锁或者事务协调器来实现,它的核心思想是通过多个节点之间的协调来保证事务的最终一致性。在最终一致性分布式事务中,各个节点可以在本地执行事务,然后通过分布式锁或者事务协调器来保证各个节点的事务最终能够达到一致的状态。
  • 应用场景不同:可靠消息最终一致性分布式事务适用于需要保证事务可靠性和消息可靠传输的场景,例如银行转账、电商支付等。最终一致性分布式事务适用于对事务的可靠性要求不高,但需要保证最终一致性的场景,例如缓存更新、数据同步等。

三、可靠消息最终一致性分布式事务实现步骤

接下来我们将模拟商城业务中的下单扣减库存场景。订单微服务和库存微服务分别独立开发和部署。

环境准备

1.orders订单数据表,orders数据表存储于tx-msg-orders订单数据库。

DROP TABLE IF EXISTS `orders`;
CREATE TABLE `order` (
 `id` bigint(20) NOT NULL COMMENT '主键',
 `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
 `order_no` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '订单编号',
 `product_id` bigint(20) NULL DEFAULT NULL COMMENT '商品id',
 `pay_count` int(11) NULL DEFAULT NULL COMMENT '购买数量',
 PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
CREATE TABLE `tx_log` (
 `tx_no` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '分布式事务全局序列号',
 `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
 PRIMARY KEY (`tx_no`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

2.stock库存数据表

DROP TABLE IF EXISTS `stock`;
CREATE TABLE `stock` (
 `id` bigint(20) NOT NULL COMMENT '主键id',
 `product_id` bigint(20) NULL DEFAULT NULL COMMENT '商品id',
 `total_count` int(11) NULL DEFAULT NULL COMMENT '商品总库存',
 PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;


3.tx_log事务记录表

-- ----------------------------
-- Table structure for tx_log
-- ----------------------------
DROP TABLE IF EXISTS `tx_log`;
CREATE TABLE `tx_log` (
 `tx_no` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '分布式事务全局序列号',
 `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
 PRIMARY KEY (`tx_no`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;

4.安装NameServer,拉取镜像

docker pull rocketmqinc/rocketmq

5.创建数据存储目录

mkdir -p /docker/rocketmq/data/namesrv/logs /docker/rocketmq/data/namesrv/store

6.启动NameServer

docker run -d \
--restart=always \
--name rmqnamesrv  \
-p 9876:9876 \
-v /docker/rocketmq/data/namesrv/logs:/root/logs \
-v /docker/rocketmq/data/namesrv/store:/root/store \
-e "MAX_POSSIBLE_HEAP=100000000" \
rocketmqinc/rocketmq \
sh mqnamesrv

6.编辑border配置

vim /docker/rocketmq/conf/broker.conf
# 所属集群名称,如果节点较多可以配置多个
brokerClusterName = DefaultCluster 
#broker名称,master和slave使用相同的名称,表明他们的主从关系 
brokerName = broker-a 
#0表示Master,大于0表示不同的
slave brokerId = 0 
#表示几点做消息删除动作,默认是凌晨4点 
deleteWhen = 04 
#在磁盘上保留消息的时长,单位是小时 
fileReservedTime = 48 
#有三个值:SYNC_MASTER,ASYNC_MASTER,SLAVE;同步和异步表示Master和Slave之间同步数据的机 制;
brokerRole = ASYNC_MASTER 
#刷盘策略,取值为:ASYNC_FLUSH,SYNC_FLUSH表示同步刷盘和异步刷盘;SYNC_FLUSH消息写入磁盘后 才返回成功状态,ASYNC_FLUSH不需要;
flushDiskType = ASYNC_FLUSH 
# 设置broker节点所在服务器的ip地址 
brokerIP1 = 192.168.66.100
#剩余磁盘比例 
diskMaxUsedSpaceRatio=99

7.启动broker

docker run -d --restart=always --name rmqbroker --link rmqnamesrv:namesrv -p 10911:10911 -p 10909:10909 --privileged=true -v /docker/rocketmq/data/broker/logs:/root/logs -v /docker/rocketmq/data/broker/store:/root/store -v /docker/rocketmq/conf/broker.conf:/opt/rocketmq-4.4.0/conf/broker.conf -e "NAMESRV_ADDR=namesrv:9876" -e "MAX_POSSIBLE_HEAP=200000000" rocketmqinc/rocketmq sh mqbroker -c /opt/rocketmq-4.4.0/conf/broker.conf

8.关闭防火墙(或者开放端口)

#关闭防火墙 
systemctl stop firewalld.service 
#禁止开机启动 
systemctl disable firewalld.service

9.部署RocketMQ的管理工具

#创建并启动容器 
docker run -d --restart=always --name rmqadmin -e "JAVA_OPTS=-Drocketmq.namesrv.addr=192.168.66.100:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false" -p 8080:8080 pangliang/rocketmq-console-ng

项目准备

1.创建Maven父工程,创建订单微服务子工程,这里自行创建
2.引入依赖

<dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.rocketmq</groupId>
      <artifactId>rocketmq-spring-boot-starter</artifactId>
      <version>2.0.2</version>
    </dependency>
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>
  </dependencies>

3.编写配置文件

server:
  port: 9090
spring:
  application:
   name: tx-msg-stock
  datasource:
   url: jdbc:mysql://192.168.66.100:3306/tx-msg-order?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&autoReconnect=true&failOverReadOnly=false&useSSL=false
   username: root
   password: 123456
   driver-class-name: com.mysql.cj.jdbc.Driver
################ RocketMQ 配置 ##########
rocketmq:
  name-server: 192.168.66.100:9876
  producer:
   group: order-group

4.编写主启动类

/**
 * 订单微服务启动成功
 */
@Slf4j
@MapperScan("com.itbaizhan.order.mapper")
@SpringBootApplication
public class OrderMain9090 {
  public static void main(String[] args) {
    SpringApplication.run(OrderMain9090.class,args);
    log.info("************* 订单微服务启动成功  *******");


   }
}

5.代码生成

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import java.util.Arrays;
import java.util.List;
public class CodeGenerator {
  public static void main(String[] args) {
    FastAutoGenerator.create("jdbc:mysql://192.168.66.102:3306/tx-msg-order", "root", "123456")
         .globalConfig(builder -> {
          builder.author("itbaizhan") // 设置作者
               .commentDate("MM-dd") // 注释日期格式 
               .outputDir(System.getProperty("user.dir") +"/rocketmq-msg/orders"+ "/src/main/java/") 
               .fileOverride(); //覆盖文件
         })
        // 包配置
         .packageConfig(builder -> {
          builder.parent("com.itbaizhan.orders") // 包名前缀
               .entity("entity") //实体类包名
               .mapper("mapper") //mapper接口包名
               .service("service"); //service包名
         })
         .strategyConfig(builder -> {
          // 设置需要生成的表名
          builder.addInclude(Arrays.asList("orders","tx_log"))
              // 开始实体类配置
               .entityBuilder()
              // 开启lombok模型
               .enableLombok()
              //表名下划线转驼峰
               .naming(NamingStrategy.underline_to_camel)
              //列名下划线转驼峰
               .columnNaming(NamingStrategy.underline_to_camel);
         })
         .execute();


   }
}

6.创建TxMessage类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TxMessage implements Serializable {


  private static final long serialVersionUID = -4704980150056885074L;


  /**
   * 商品id
   */
  private Long productId;


  /**
   * 商品购买数量
   */
  private Integer payCount;


  /**
   * 全局事务编号
   */
  private String txNo;
}



7.编写OrderService接口

/**
   * 添加订单
   * @param productId 商品id
   * @param payCount 购买数量
   */
  void save(Long productId,Integer payCount);


  /**
   * 提交订单同时保存事务信息
   */
  void submitOrderAndSaveTxNo(TxMessage txMessage);


  /**
   * 提交订单
   * @param productId 商品id
   * @param payCount 购买数量
   */
  void submitOrder(Long productId, Integer payCount);

8.编写OrderService接口实现

import com.alibaba.fastjson.JSONObject;
import com.itbaizhan.order.entity.Order;
import com.itbaizhan.order.entity.TxLog;
import com.itbaizhan.order.mapper.OrderMapper;
import com.itbaizhan.order.mapper.TxLogMapper;
import com.itbaizhan.order.service.IOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itbaizhan.order.tx.TxMessage;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.UUID;


/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author itbaizhan
 * @since 05-20
 */
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {




  @Resource
  RocketMQTemplate rocketMQTemplate;


  @Resource
  private TxLogMapper txLogMapper;


  /**
   * 添加
   * @param productId 商品id
   * @param payCount 购买数量
   */
  @Override
  public void save(Long productId, Integer payCount) {
    Order order = new Order();
    // 订单创建时间
    order.setCreateTime(LocalDateTime.now());
    // 生产订单编号
    order.setOrderNo(UUID.randomUUID().toString().replace("-",""));
    // 商品id
    order.setProductId(productId);
    // 购买数量
    order.setPayCount(payCount);
    baseMapper.insert(order);
   }
  @Override
  @Transactional(rollbackFor = Exception.class)
  public void submitOrderAndSaveTxNo(TxMessage txMessage) {
    TxLog txLog = txLogMapper.selectById(txMessage.getTxNo());
    if(txLog != null){
      log.info("订单微服务已经执行过事务,商品id为:{},事务编号为:{}",txMessage.getProductId(), txMessage.getTxNo());
      return;
     }
    //生成订单
    this.save(txMessage.getProductId(),txMessage.getPayCount());
    //生成订单
    txLog = new TxLog();
    txLog.setTxNo(txMessage.getTxNo());
    txLog.setCreateTime(LocalDateTime.now());
    //添加事务日志
    txLogMapper.insert(txLog);
   }
  /**
   *  提交订单
   * @param productId 商品id
   * @param payCount 购买数量
   */
  @Override
  public void submitOrder(Long productId, Integer payCount) {
    //生成全局分布式序列号
    String txNo = UUID.randomUUID().toString();
    TxMessage txMessage = new TxMessage(productId, payCount, txNo);
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("txMessage", txMessage);
    Message<String> message = MessageBuilder.withPayload(jsonObject.toJSONString()).build();
    //发送事务消息  且该消息不允许消费   tx_order_group: 指定版事务消息组
    rocketMQTemplate.sendMessageInTransaction("tx_order_group", "topic_txmsg", message, null);
   }
}

10.执行本地的业务代码

import com.alibaba.fastjson.JSONObject;
import com.itbaizhan.order.entity.TxLog;
import com.itbaizhan.order.mapper.TxLogMapper;
import com.itbaizhan.order.service.IOrderService;
import com.itbaizhan.order.service.ITxLogService;
import com.itbaizhan.order.tx.TxMessage;
import lombok.extern.slf4j.Slf4j;
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.messaging.Message;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;


import javax.annotation.Resource;




/**
 * @author itbaizhan
 * @version 1.0.0
 * @description 监听事务消息
 */


@Slf4j
@Component
@RocketMQTransactionListener(txProducerGroup = "tx_order_group")
public class OrderTxMessageListener implements RocketMQLocalTransactionListener {


  @Autowired
  private IOrderService orderService;


  @Resource
  private TxLogMapper txLogMapper;


  /**
   * RocketMQ的Producer本地事务:先执行本地的业务代码(使用Spring的事件管理),判断是否成功。
   * 成功返回: RocketMQLocalTransactionState.COMMIT,失败返回:RocketMQLocalTransactionState.ROLLBACK
   */
  @Override
  @Transactional(rollbackFor = Exception.class)
  public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object obj) {
    try {
      log.info("订单微服务执行本地事务");
      TxMessage txMessage = this.getTxMessage(msg);
      //执行本地事务
      orderService.submitOrderAndSaveTxNo(txMessage);
      //提交事务
      log.info("订单微服务提交事务");
      // COMMIT:即生产者通知Rocket该消息可以消费
      return RocketMQLocalTransactionState.COMMIT;
     } catch (Exception e) {
      e.printStackTrace();
      //异常回滚事务
      log.info("订单微服务回滚事务");
      // ROLLBACK:即生产者通知Rocket将该消息删除
      return RocketMQLocalTransactionState.ROLLBACK;
     }
   }
  private TxMessage getTxMessage(Message msg) {
    String messageString = new String((byte[]) msg.getPayload());
    JSONObject jsonObject = JSONObject.parseObject(messageString);
    String txStr = jsonObject.getString("txMessage");
    return JSONObject.parseObject(txStr, TxMessage.class);
   }
}

11.编写OrderService接口

/**
   * 添加订单
   * @param productId 商品id
   * @param payCount 购买数量
   */
  void save(Long productId,Integer payCount);
  /**
   * 提交订单同时保存事务信息
   */
  void submitOrderAndSaveTxNo(TxMessage txMessage);
  /**
   * 提交订单
   * @param productId 商品id
   * @param payCount 购买数量
   */
  void submitOrder(Long productId, Integer payCount);

12.编写OrderService接口实现

import com.alibaba.fastjson.JSONObject;
import com.itbaizhan.order.entity.Order;
import com.itbaizhan.order.entity.TxLog;
import com.itbaizhan.order.mapper.OrderMapper;
import com.itbaizhan.order.mapper.TxLogMapper;
import com.itbaizhan.order.service.IOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itbaizhan.order.tx.TxMessage;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.UUID;


/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author itbaizhan
 * @since 05-20
 */
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {




  @Resource
  RocketMQTemplate rocketMQTemplate;


  @Resource
  private TxLogMapper txLogMapper;


  /**
   * 添加
   * @param productId 商品id
   * @param payCount 购买数量
   */
  @Override
  public void save(Long productId, Integer payCount) {
    Order order = new Order();
    // 订单创建时间
    order.setCreateTime(LocalDateTime.now());
    // 生产订单编号
    order.setOrderNo(UUID.randomUUID().toString().replace("-",""));
    // 商品id
    order.setProductId(productId);
    // 购买数量
    order.setPayCount(payCount);
    baseMapper.insert(order);
   }


  @Override
  @Transactional(rollbackFor = Exception.class)
  public void submitOrderAndSaveTxNo(TxMessage txMessage) {
    TxLog txLog = txLogMapper.selectById(txMessage.getTxNo());
    if(txLog != null){
      log.info("订单微服务已经执行过事务,商品id为:{},事务编号为:{}",txMessage.getProductId(), txMessage.getTxNo());
      return;
     }


    //生成订单
    this.save(txMessage.getProductId(),txMessage.getPayCount());


    //生成订单
    txLog = new TxLog();
    txLog.setTxNo(txMessage.getTxNo());
    txLog.setCreateTime(LocalDateTime.now());
    //添加事务日志
    txLogMapper.insert(txLog);
   }


  /**
   *  提交订单
   * @param productId 商品id
   * @param payCount 购买数量
   */
  @Override
  public void submitOrder(Long productId, Integer payCount) {
    //生成全局分布式序列号
    String txNo = UUID.randomUUID().toString();
    TxMessage txMessage = new TxMessage(productId, payCount, txNo);
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("txMessage", txMessage);
    Message<String> message = MessageBuilder.withPayload(jsonObject.toJSONString()).build();
    //发送事务消息  且该消息不允许消费   tx_order_group: 指定版事务消息组
    rocketMQTemplate.sendMessageInTransaction("tx_order_group", "topic_txmsg", message, null);
   }



}

13.订单微服务监听事务消息,执行本地的业务代码

package com.itbaizhan.order.message;


import com.alibaba.fastjson.JSONObject;
import com.itbaizhan.order.entity.TxLog;
import com.itbaizhan.order.mapper.TxLogMapper;
import com.itbaizhan.order.service.IOrderService;
import com.itbaizhan.order.service.ITxLogService;
import com.itbaizhan.order.tx.TxMessage;
import lombok.extern.slf4j.Slf4j;
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.messaging.Message;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;


import javax.annotation.Resource;




/**
 * @author itbaizhan
 * @version 1.0.0
 * @description 监听事务消息
 */


@Slf4j
@Component
@RocketMQTransactionListener(txProducerGroup = "tx_order_group")
public class OrderTxMessageListener implements RocketMQLocalTransactionListener {


  @Autowired
  private IOrderService orderService;


  @Resource
  private TxLogMapper txLogMapper;


  /**
   * RocketMQ的Producer本地事务:先执行本地的业务代码(使用Spring的事件管理),判断是否成功。
   * 成功返回: RocketMQLocalTransactionState.COMMIT,失败返回:RocketMQLocalTransactionState.ROLLBACK
   */
  @Override
  @Transactional(rollbackFor = Exception.class)
  public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object obj) {
    try {
      log.info("订单微服务执行本地事务");
      TxMessage txMessage = this.getTxMessage(msg);
      //执行本地事务
      orderService.submitOrderAndSaveTxNo(txMessage);
      //提交事务
      log.info("订单微服务提交事务");
      // COMMIT:即生产者通知Rocket该消息可以消费
      return RocketMQLocalTransactionState.COMMIT;
     } catch (Exception e) {
      e.printStackTrace();
      //异常回滚事务
      log.info("订单微服务回滚事务");
      // ROLLBACK:即生产者通知Rocket将该消息删除
      return RocketMQLocalTransactionState.ROLLBACK;
     }


   }


  private TxMessage getTxMessage(Message msg) {
    String messageString = new String((byte[]) msg.getPayload());
    JSONObject jsonObject = JSONObject.parseObject(messageString);
    String txStr = jsonObject.getString("txMessage");
    return JSONObject.parseObject(txStr, TxMessage.class);
   }
}

14.网络异常消息处理

/**
   * 因为网络异常或其他原因时,RocketMQ的消息状态处于UNKNOWN时,调用该方法检查Producer的本地事务是否已经执行成功,
   * 成功返回: RocketMQLocalTransactionState.COMMIT,失败返回:RocketMQLocalTransactionState.ROLLBACK
   */
  @Override
  public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
    log.info("订单微服务查询本地事务");
    TxMessage txMessage = this.getTxMessage(msg);
    // 获取订单的消息
    Integer exists = txLogService.isExistsTx(txMessage.getTxNo());
    if (exists != null) {
      // COMMIT:即生产者通知Rocket该消息可以消费
      return RocketMQLocalTransactionState.COMMIT;
     }
    // UNKNOWN:即生产者通知Rocket继续查询该消息的状态
    return RocketMQLocalTransactionState.UNKNOWN;
   }
  private TxMessage getTxMessage(Message msg) {
    String messageString = new String((byte[]) msg.getPayload());
    JSONObject jsonObject = JSONObject.parseObject(messageString);
    String txStr = jsonObject.getString("txMessage");
    return JSONObject.parseObject(txStr, TxMessage.class);
   }

15.创建库存微服务,引入依赖

 <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.rocketmq</groupId>
      <artifactId>rocketmq-spring-boot-starter</artifactId>
      <version>2.0.1</version>
    </dependency>
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>
  </dependencies>

16.编写配置文件

server:
  port: 6060
spring:
  application:
   name: tx-msg-stock
  datasource:
   url: jdbc:mysql://192.168.66.100:3306/tx-msg-stock?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&autoReconnect=true&failOverReadOnly=false&useSSL=false
   username: root
   password: 123456
   driver-class-name: com.mysql.cj.jdbc.Driver
################ RocketMQ 配置 ##########
rocketmq:
  name-server: 192.168.66.100:9876

17.编写主启动类

package com.itbaizhan.stock;


import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


/**
 * @author itbaizhan
 * @version 1.0.0
 * @description 库存微服务启动类
 */
@MapperScan("com.itbaizhan.stock.mapper")
@Slf4j
@SpringBootApplication
public class StockServerStarter {
  public static void main(String[] args) {
    SpringApplication.run(StockServerStarter.class, args);
    log.info("**************** 库存服务启动成功 ***********");
   }
}



18.代码生成

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import java.util.Arrays;
import java.util.List;
public class CodeGenerator {
  public static void main(String[] args) {
    FastAutoGenerator.create("jdbc:mysql://192.168.66.102:3306/tx-msg-stock", "root", "123456")
         .globalConfig(builder -> {
          builder.author("itbaizhan") // 设置作者
               .commentDate("MM-dd") // 注释日期格式 
               .outputDir(System.getProperty("user.dir") +"/rocketmq-msg/stock"+ "/src/main/java/") 
               .fileOverride(); //覆盖文件
         })
        // 包配置
         .packageConfig(builder -> {
          builder.parent("com.itbaizhan.stock") // 包名前缀
               .entity("entity") //实体类包名
               .mapper("mapper") //mapper接口包名
               .service("service"); //service包名
         })
         .strategyConfig(builder -> {
          // 设置需要生成的表名
          builder.addInclude(Arrays.asList("stock","tx_log"))
              // 开始实体类配置
               .entityBuilder()
              // 开启lombok模型
               .enableLombok()
              //表名下划线转驼峰
               .naming(NamingStrategy.underline_to_camel)
              //列名下划线转驼峰
               .columnNaming(NamingStrategy.underline_to_camel);
         })
         .execute();
   }
}


19.编写库存接口

public interface StockService {


  /**
   * 根据id查询库存
   * @param id
   * @return
   */
  Stock getStockById(Long id);


  /**
   * 扣减库存
   */
  void decreaseStock(TxMessage txMessage);
}

20.库存接口实现类

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author itbaizhan
 * @since 05-20
 */
@Slf4j
@Service
public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> implements IStockService {
  @Resource
  private StockMapper stockMapper;
  @Resource
  private TxLogMapper txLogMapper;
  @Transactional
  @Override
  public void decreaseStock(TxMessage txMessage) {
    log.info("库存微服务执行本地事务,商品id:{}, 购买数量:{}", txMessage.getProductId(), txMessage.getPayCount());
    //检查是否执行过事务
    TxLog txLog = txLogMapper.selectById(txMessage.getTxNo());
    if(txLog != null){
      log.info("库存微服务已经执行过事务,事务编号为:{}", txMessage.getTxNo());
     }
    // 根据商品id查询库存
    QueryWrapper<Stock> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("product_id",txMessage.getProductId());
    Stock stock = stockMapper.selectOne(queryWrapper);
    if(stock.getTotalCount() < txMessage.getPayCount()){
      throw new RuntimeException("库存不足");
     }
    // 减库存
    stock.setTotalCount(stock.getTotalCount()-txMessage.getPayCount());
    stockMapper.updateById(stock);
    //生成订单
    txLog = new TxLog();
    txLog.setTxNo(txMessage.getTxNo());
    txLog.setCreateTime(LocalDateTime.now());
    //添加事务日志
    txLogMapper.insert(txLog);
   }
}

21.库存微服务消费者实现,用于消费RocketMQ发送过来的事务消息,并且调用StockService中的decreaseStock(TxMessage)方法扣减库存。

/**
 * @author binghe
 * @version 1.0.0
 * @description 库存事务消费者
 */
@Component
@Slf4j
@RocketMQMessageListener(consumerGroup = "tx_stock_group", topic = "topic_txmsg")
public class StockTxMessageConsumer implements RocketMQListener<String> {
  @Autowired
  private IStockService stockService;
  @Override
  public void onMessage(String message) {
    log.info("库存微服务开始消费事务消息:{}", message);
    TxMessage txMessage = this.getTxMessage(message);
    stockService.decreaseStock(txMessage);
   }
  private TxMessage getTxMessage(String msg){
    JSONObject jsonObject = JSONObject.parseObject(msg);
    String txStr = jsonObject.getString("txMessage");
    return JSONObject.parseObject(txStr, TxMessage.class);
   }
}
 


总结

提示:这里对文章进行总结:
总的来说,RMECT 是一种灵活、高性能、高可靠的分布式事务处理方案,它通过将事务的提交和消息的可靠传输分离,实现了最终一致性。在实际应用中,需要根据具体的业务场景和需求,选择合适的消息队列和事务处理框架,以实现可靠的消息最终一致性分布式事务。

  • 20
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值