如何保证分布式事务

一、概述

最近几年,都提倡将复杂的应用系统,按照业务或者功能拆分成多个职能单一的微服务(只处理自己的业务或者功能),各个服务之间通过PRC或者Restful通信,而且微服务开发效率高、部署简单、代码好维护、稳定性高、扩展性强、松耦合、可以使用不同语言开发等优点,但是也存在测试难度提升、管理复杂、调用复杂、分布式事务等劣势。
对于事务,之前的单体应用,不需要考虑跨模块回滚事务的问题,但是在分布式系统中,我们需要保证事务最终一致。
现有分布式事务的主要实现方式:

XA 方案
TCC 方案
可靠消息最终一致性方案
最大努力通知方案

本文只讨论通过RocketMQ事务消息来实现分布式事务的最终一致。

具体方案:A系统在执行本地事务之前,先发送一条prepare消息,然后RocketMQ broker轮询这条prepare消息是否可以提交提交(或者回归,或者继续等待),当A系统本地事务执行成功之后,broker将提交这条消息;B系统监听到这条消息之后,开始执行本地事务。

潜在的问题:
1、发送prepare消息失败:那么A系统本地事务就不需要执行,直接丢弃这条消息。
2、B系统在收到消息之后,重试多次之后依旧失败:只能人工干预,回滚数据,没有完美的设计方案。
3、prepare消息每隔多长时间轮询一次,prepare消息是否可以提交、回归、继续等待:默认每隔1分钟轮询一次,可以在broker.config配置轮询时间。
4、最大轮询次数:默认15次,超过上限之后,丢弃消息。

二、具体实现

在上一篇文章的基础之上(RocketMq顺序消息解决方案),来写这次的demo

1、OrderConstant

package com.cn.dl.springbootdemo.constant;


public interface OrderConstant {
    String ORDER_TOPIC = "order_msg";

    String ORDER_CONSUMER_GROUP = "order_msg_consumer";

    String REPAY_TRANSACTION_GROUP = "repay_transaction_group";

    String REPAY_MSG_TOPIC = "repay_msg_topic";

    String REPAY_MSG_GROUP = "repay_msg_group";

    String RANDOM_INT = "random_int";
}

2、RepayTransactionProducer

package com.cn.dl.springbootdemo.mq;

import com.cn.dl.springbootdemo.bean.Repay;
import com.cn.dl.springbootdemo.constant.OrderConstant;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * Created by yanshao on 2020-11-05.
 */
@Component
public class RepayTransactionProducer {

    private static final Logger logger = LoggerFactory.getLogger(RepayTransactionProducer.class);


    @Resource
    private RocketMQTemplate rocketMQTemplate;

    public void sendRepayTransactionMsg(Repay repay){
        Message<? extends Repay> message = MessageBuilder.withPayload(repay)
                .setHeader(RocketMQHeaders.TRANSACTION_ID,repay.getRepayId())
                .build();
        TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
                OrderConstant.REPAY_TRANSACTION_GROUP,OrderConstant.REPAY_MSG_TOPIC,message,new String[]{"test","demo"});
        logger.info("send repay transaction msg. repay: {},sendResult: {}",repay,sendResult);
    }
}

3、RepayTransactionListener

package com.cn.dl.springbootdemo.mq;

import com.cn.dl.springbootdemo.constant.OrderConstant;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;

/**
 * Created by yanshao on 2020-11-05.
 */
@Component
@RocketMQTransactionListener(txProducerGroup = OrderConstant.REPAY_TRANSACTION_GROUP)
public class RepayTransactionListener implements RocketMQLocalTransactionListener {

    private static final Logger logger = LoggerFactory.getLogger(RepayTransactionListener.class);

    /**
      发送prepare消息成功后会这样这个方法,只执行一次
      @param message 消息头、消息体
      @param args 接受额外的参数
      @return 返回事务状态,COMMIT:提交  ROLLBACK:回滚  UNKNOW:回调
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object args) {
        logger.info("execute local transaction transactionId: {},args: {}"
                ,message.getHeaders().get(RocketMQHeaders.TRANSACTION_ID),args);
        return RocketMQLocalTransactionState.UNKNOWN;
    }

    /**
     * 判断本地事务的执行状态,如果本地执行成功,返回COMMIT,如果本地事务还在进行中或者状态未知,
     * 返回UNKNOW,如果本地事务执行失败,返回ROLLBACK
     *
     * 默认执行15次,超过上限则丢弃消息
     * @param message
     * @return
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        logger.info("check local transaction transactionId: {}",message.getHeaders().get(RocketMQHeaders.TRANSACTION_ID));
        return RocketMQLocalTransactionState.COMMIT;
    }
}

4、RepayMsgConsumer

package com.cn.dl.springbootdemo.mq;

import com.alibaba.fastjson.JSONObject;
import com.cn.dl.springbootdemo.bean.Repay;
import com.cn.dl.springbootdemo.constant.OrderConstant;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;

/**
 * Created by yanshao on 2020-11-05.
 */
@Component
@RocketMQMessageListener(
        topic = OrderConstant.REPAY_MSG_TOPIC,
        consumerGroup = OrderConstant.REPAY_MSG_GROUP
)
public class RepayMsgConsumer implements RocketMQListener<MessageExt> {

    private static final Logger logger = LoggerFactory.getLogger(RepayTransactionListener.class);


    @Override
    public void onMessage(MessageExt messageExt) {
        String msg = null;
        try {
            msg = new String(messageExt.getBody(), StandardCharsets.UTF_8.name());
            Repay repay = JSONObject.parseObject(msg, Repay.class);
            logger.info("repay msg consumer. repay: {}",repay);
        }catch (Exception e){
            logger.error("repay msg consumer exception. msgId: {}",messageExt.getMsgId());
            throw new RuntimeException("repay msg consumer exception!");
        }
    }

}

5、Repay

package com.cn.dl.springbootdemo.bean;

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

import java.io.Serializable;
import java.math.BigDecimal;

/**
 * Created by yanshao on 2020-11-05.
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Repay implements Serializable {
    private static final long serialVersionUID = -5915842247156810870L;

    /**
     * 还款唯一ID
     */
    private String repayId;
    /**
     * 用户ID
     */
    private String customerId;
    /**
     * 还款金额
     */
    private BigDecimal repayAmount;
    /**
     * 备注
     */
    private String remark;
}

6、测试入口

    @Resource
    private RepayTransactionProducer repayTransactionProducer;
    @Test
    public void sendRepayTransaction() throws InterruptedException {
        for(int i=0; i<1; i++){
            Repay repay = Repay.builder()
                    .customerId("customerId" + i)
                    .repayId("repayId" + i)
                    .repayAmount(new BigDecimal(i * 1000))
                    .build();
            repayTransactionProducer.sendRepayTransactionMsg(repay);
        }

        TimeUnit.SECONDS.sleep(30000);
    }

7、执行结果

8、测试丢弃和继续等待两种状态,修改RepayTransactionProducer、RepayTransactionListener

public void sendRepayTransactionMsg(Repay repay){
        Message<? extends Repay> message = MessageBuilder.withPayload(repay)
                .setHeader(RocketMQHeaders.TRANSACTION_ID,repay.getRepayId())
                //为了测试,这里增加随机树,默认本地是否执行失败等情况
                .setHeader(OrderConstant.RANDOM_INT,new Random().nextInt(10))
                .build();

        TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
                OrderConstant.REPAY_TRANSACTION_GROUP,
                OrderConstant.REPAY_MSG_TOPIC,
                message,
                new String[]{"test","demo"});
        logger.info("send repay transaction msg. repay: {},sendResult: {}",repay,sendResult);
    }
@Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        int randomInt = Integer.parseInt(String.valueOf(message.getHeaders().get(OrderConstant.RANDOM_INT)));
        if(randomInt < 3){
            logger.info("unknown transaction transactionId: {}",message.getHeaders().get(RocketMQHeaders.TRANSACTION_ID));
            return RocketMQLocalTransactionState.UNKNOWN;
        } else if(randomInt % 2 == 0){//如果随机数大于2 && 为偶数,丢弃消息
            logger.info("rollback transaction transactionId: {}",message.getHeaders().get(RocketMQHeaders.TRANSACTION_ID));
            return RocketMQLocalTransactionState.ROLLBACK;
        } else {
            logger.info("commit transaction transactionId: {}",message.getHeaders().get(RocketMQHeaders.TRANSACTION_ID));
            return RocketMQLocalTransactionState.COMMIT;
        }
    }

### ### 分布式事务一致性保障机制 在分布式系统中,事务的一致性和可靠性通常通过以下几种机制来实现: #### 1. 两阶段提交(2PC) 两阶段提交是一种经典的分布式事务协议,它通过协调者(Transaction Manager)和参与者(Resource Manager)之间的交互来确保事务的原子性和一致性。2PC 分为两个阶段: - **准备阶段(Prepare Phase)**:协调者询问所有参与者是否可以提交事务,每个参与者必须准备好事务并锁定资源。 - **提交阶段(Commit Phase)**:如果所有参与者都准备就绪,协调者发出提交命令;否则,发出回滚命令。 尽管 2PC 能够保证事务的强一致性,但它存在单点故障和阻塞问题,协调者故障可能导致整个事务无法继续执行,且在准备阶段需要等待所有参与者的响应,可能导致系统性能下降[^4]。 #### 2. 三阶段提交(3PC) 三阶段提交是对 2PC 的改进,旨在减少阻塞和提高容错能力。3PC 引入了超时机制和更多的状态转换,分为三个阶段: - **CanCommit**:协调者询问参与者是否可以提交事务。 - **PreCommit**:如果所有参与者都同意,协调者通知参与者进行预提交。 - **DoCommit**:协调者最终决定提交或回滚事务。 3PC 通过引入超时机制避免了部分阻塞问题,但仍然存在较高的通信开销和复杂的状态管理[^3]。 #### 3. TCC(Try-Confirm-Cancel) TCC 是一种补偿事务模式,适用于对性能和可用性要求较高的场景。它分为三个阶段: - **Try**:资源的初步检查和预留。 - **Confirm**:业务操作的执行。 - **Cancel**:如果任何阶段失败,执行补偿操作以回滚事务。 TCC 的优势在于其灵活性和高性能,但需要业务逻辑显式支持补偿机制,开发复杂度较高[^2]。 #### 4. 最大努力通知(Best Effort Notification) 最大努力通知是一种最终一致性的事务处理方式,适用于对一致性要求不高的场景。它通过异步通知的方式,尝试多次提交事务,直到所有参与者确认成功或达到最大重试次数。这种方式实现简单,但无法保证强一致性[^2]。 #### 5. 可靠消息最终一致性(如 RocketMQ) 可靠消息最终一致性方案通过消息队列来保证分布式事务的最终一致性。例如,RocketMQ 提供了事务消息机制,允许生产者在本地事务执行成功后再提交消息,确保消息发送与本地事务的原子性。消费者端也可以通过事务回查机制来确认消息的最终状态,从而实现跨服务的事务一致性[^1]。 #### 6. Seata(AT 模式) Seata 是一个开源的分布式事务解决方案,支持多种事务模式,其中 AT(Auto Transaction)模式通过全局事务协调器(TC)与本地事务日志的结合,实现了对分布式事务的自动管理。AT 模式通过以下方式保证一致性: - 在事务开始时,记录数据库的前后镜像(undo log)。 - 在提交阶段,删除 undo log;在回滚阶段,根据 undo log 恢复数据。 Seata 的 AT 模式对业务代码无侵入,适用于基于数据库操作的场景,具有较高的可用性和一致性保障[^5]。 #### 7. SAGA 模式 SAGA 是一种长事务模式,适用于需要跨多个服务执行的业务流程。它将整个事务拆分为多个本地事务,每个本地事务提交后立即释放资源。如果某一步失败,则通过补偿事务回滚前面的操作。SAGA 模式具有良好的性能和可扩展性,但需要开发者自行实现补偿逻辑[^3]。 --- ### ### 项目中的实现建议 在实际项目中,选择合适的分布式事务方案应基于业务场景、性能要求和系统架构: - **对于高并发、低延迟的场景**,推荐使用 TCC 或 Seata 的 AT 模式,它们在保证一致性的同时,具备较好的性能。 - **对于最终一致性要求较高的场景**,如订单、支付等业务,可以采用 RocketMQ 的事务消息机制,结合本地事务表实现可靠消息最终一致性。 - **对于复杂业务流程**,如供应链、物流等,SAGA 模式提供了良好的灵活性和可扩展性,适合长周期事务处理。 --- ### 示例:基于 RocketMQ 的事务消息实现 ```java // 生产者端 Message msg = new Message("OrderTopic", "ORDER_MSG".getBytes()); SendResult sendResult = rocketMQTemplate.getProducer().sendMessageInTransaction(msg, null); // 消费者端 @RocketMQMessageListener(topic = "OrderTopic", consumerGroup = "order-group") public class OrderConsumer implements RocketMQListener<String> { @Override public void onMessage(String message) { // 处理订单创建逻辑 try { // 执行本地事务 createOrder(); } catch (Exception e) { // 回查机制处理 } } } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

燕少༒江湖

给我一份鼓励!谢谢!

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

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

打赏作者

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

抵扣说明:

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

余额充值