SpringCloudAlibaba - RocketMQ 分布式事务消息的实现

本文详细介绍了如何在SpringCloud和RocketMQ环境中实现分布式事务消息,通过内容中心和用户中心的示例,展示了如何处理服务异常导致的数据一致性问题。通过代码实例和流程解析,读者将理解半消息、二次确认及事务三种状态在实际应用中的作用。
摘要由CSDN通过智能技术生成

前言

RocketMQ提供了事务消息去解决程序异常回滚但消息已发出的问题,如服务A插入一条数据后服务B需要对用户数据进行修改,而服务A发出消息后程序发生异常导致数据插入回滚,而服务B监听到消息又对数据进行了修改,导致数据出现问题


环境

Spring Cloud Hoxton.SR9 + Spring Cloud Alibaba 2.2.6.RELEASE + RocketMQ 4.7.0


分布式事务消息流程

流程图

在这里插入图片描述

流程解析

  • 第1步:生产者向MQ Server发送半消息(特殊消息,会被存储到MQ Server且标记为暂时不能投递),消费者不会接收到这条消息
  • 第2 3步:当半消息发送成功后生产者就去执行本地事务
  • 第4步:生产者根据本地事务的执行状态向MQ Server发送二次确认请求,如果MQ Server收到的是commit就将半消息标记为可投递,消费者即可消费到该消息,如果接收到是rollback就将这条半消息删除
  • 第5步:如果第四步的二次确认没有能够成功发送到MQ Server,经过一段时间后,MQ Server会向生产者发送回查消息去获取本地事务的执行状态
  • 第6步:生产者检查本地事务执行状态
  • 第7步:生产者根据本地事务的执行结果告诉MQ Server应该commit还是rollback,如果是commit则像消费者投递消息,如果是rollback则丢弃消息

注:
1234步是一种二次确认的机制,生产者把消息发送到MQ,MQ做了标记不让去消费这条消息,生产者去执行本地事务,完成后根据执行状态去投递或丢弃消息
567步是MQ没有收到二次确认做的容错处理


事务消息三种状态

  • Commit:提交事务消息,消费者可以消费此消息
  • Rollback:回滚事务消息,broker会删除该消息,消费者不能消费
  • UNKNOWN: broker需要回查确认该消息的状态

具体实现

实现代码

问题场景:内容中心插入一条数据后用户中心需要对用户数据进行修改,而内容中心发出消息后程序发生异常导致数据插入回滚,而用户中心监听到消息又对数据进行了修改导致数据的不一致,下面将用RocketMQ的分布式事务消息验证下该场景的处理方式


内容中心

  • 表结构
CREATE TABLE `test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='插入数据测试表'

CREATE TABLE `rocketmq_transaction_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `transaction_Id` varchar(45) COLLATE utf8_unicode_ci NOT NULL COMMENT '事务id',
  `log` varchar(45) COLLATE utf8_unicode_ci NOT NULL COMMENT '日志',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='RocketMQ事务日志表'
  • TestRocketController.java
@PostMapping("test1")
public Test test1() {
    return testService.insertTest();
}
  • TestService.java
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import lombok.RequiredArgsConstructor;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TestService {

    private final TestMapper testMapper;
    private final RocketMQTemplate rocketMQTemplate;
    private final RocketmqTransactionLogMapper rocketmqTransactionLogMapper;

    public Test insertTest() {
        Test test = Test.builder()
                .title("世事短如春梦,春梦了无痕,譬如春梦,黄粱未熟蕉鹿走")
                .build();

        /**
         * 发送半消息 对应步骤一
         * 参数1:Topic
         * 参数2:消息体
         *      可设置header,可用作参数传递
         * 参数2:arg 可用作参数传递
         */
        rocketMQTemplate.sendMessageInTransaction(
                "add-test",
                MessageBuilder.withPayload(test)
                              .setHeader(RocketMQHeaders.TRANSACTION_ID, UUID.randomUUID().toString())
                              .build(),
                 test
        );

        return test;
    }

    /**
     * 插入数据且记录事务日志
     */
    @Transactional(rollbackFor = Exception.class)
    public void insertTestDataWithRocketMqLog(Test test, String transactionId) {
        this.insertTestData(test);

        rocketmqTransactionLogMapper.insertSelective(
                RocketmqTransactionLog.builder()
                        .transactionId(transactionId)
                        .log("插入了一条Test数据...")
                        .build()
        );
    }

    /**
     * 插入测试数据
     * @param test
     */
    @Transactional(rollbackFor = Exception.class)
    public void insertTestData(Test test) {
        testMapper.insertSelective(test);
    }
}
  • TestMapper.java
public interface TestMapper extends Mapper<Test> {
}
  • RocketmqTransactionLogMapper.java
public interface RocketmqTransactionLogMapper extends Mapper<RocketmqTransactionLog> {
}
  • Test.java
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
@Table(name = "test")
public class Test {

    /**
     * id
     */
    @Id
    @GeneratedValue(generator = "JDBC")
    private Integer id;

    /**
     * 标题
     */
    private String title;

}
  • RocketmqTransactionLog.java
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
@Table(name = "rocketmq_transaction_log")
public class RocketmqTransactionLog {
    /**
     * id
     */
    @Id
    @GeneratedValue(generator = "JDBC")
    private Integer id;

    /**
     * 事务id
     */
    @Column(name = "transaction_Id")
    private String transactionId;

    /**
     * 日志
     */
    private String log;
}
  • AddTestTransactionListener.java
import lombok.RequiredArgsConstructor;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import java.util.Objects;

/**
 * 事务监听
 */
@RocketMQTransactionListener
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AddTestTransactionListener implements RocketMQLocalTransactionListener {

    private final TestService testService;
    private final RocketmqTransactionLogMapper rocketmqTransactionLogMapper;

    /**
     * 执行本地事务,对应步骤三
     * @param message
     * @param o
     * @return
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {

        MessageHeaders headers = message.getHeaders();

        String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);

        try {
            testService.insertTestDataWithRocketMqLog((Test) o, transactionId);
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    /**
     * 本地事务回查,对应步骤六
     * @param message
     * @return
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        MessageHeaders headers = message.getHeaders();
        String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);

        // 根据记录的事务回查
        RocketmqTransactionLog transactionLog = rocketmqTransactionLogMapper.selectOne(
                RocketmqTransactionLog.builder()
                        .transactionId(transactionId)
                        .build()
        );

        // 本地事务执行成功
        if (Objects.nonNull(transactionLog)) {
            return RocketMQLocalTransactionState.COMMIT;
        }

        // 本地事务执行失败
        return RocketMQLocalTransactionState.ROLLBACK;
    }
}

用户中心

  • TestRocketConsumer.java
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Service;

@Service
@Slf4j
@RocketMQMessageListener(consumerGroup = "consumer-group", topic = "add-test")
public class TestRocketConsumer implements RocketMQListener<Test> {
    @Override
    public void onMessage(Test test) {
        // TODO 业务处理
        try {
            log.info("监听到主题为'add-test'的消息:" + new ObjectMapper().writeValueAsString(test));
            log.info("可以开始处理业务啦啦啦");
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }
}

测试

  • 如图所示,在执行数据插入后还未向MQ Server发送本地事务的执行状态时,模拟服务宕机,将服务kill

在这里插入图片描述

  • Kill内容中心进程

在这里插入图片描述

  • 此时未向MQ Server发送本地事务的执行状态,MQ Server中的消息不会投递到用户中心,用户中心未收到消息不会进行后续的业务处理,如下所示,重启应用后进入本地事务回查

在这里插入图片描述

  • 本地事务回查后用户中心正常监听到消息进行业务处理

在这里插入图片描述

  • 至此,已完成RocketMQ分布式事务消息的实现

项目源码


- End -
- 个人学习笔记 -
- 仅供参考 -

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Cloud Alibaba 包含了五个核心组件:Nacos、Sentinel、Dubbo、RocketMQ 和 Seata。下面分别介绍它们的底层实现原理。 1. Nacos Nacos 是阿里巴巴开源的服务发现、配置管理和动态 DNS 服务。它的底层实现原理是基于 Raft 算法实现的一致性协议,保证了数据的一致性和高可用性。Nacos 将注册中心、配置中心和命名服务合并在一个平台上,方便开发人员进行服务的管理。 2. Sentinel Sentinel 是阿里巴巴开源的微服务流量控制组件,提供实时监控、流量控制、熔断降级等功能。它的底层实现原理是基于令牌桶算法和滑动窗口算法实现的流量控制,通过统计请求的 QPS、RT、异常比例等指标,实现对服务的流量控制和熔断降级。 3. Dubbo Dubbo 是阿里巴巴开源的高性能 RPC 框架,提供了服务治理、负载均衡、容错机制等功能。它的底层实现原理是基于 Netty 实现的高性能网络通信,通过序列化、反序列化、协议编解码等技术实现跨语言的 RPC 调用。 4. RocketMQ RocketMQ 是阿里巴巴开源的分布式消息中间件,提供了高吞吐量、低延迟、可靠性等特性。它的底层实现原理是基于消息队列的发布/订阅模式实现的,通过消息队列的缓存、异步处理、水平扩展等技术实现高效的消息传递和处理。 5. Seata Seata 是阿里巴巴开源的分布式事务解决方案,提供了分布式事务的一致性协议和分布式事务管理器。它的底层实现原理是基于两阶段提交协议实现的,通过对分布式事务的预处理、提交和回滚等操作实现分布式事务的一致性和可靠性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Maggieq8324

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值