单体与分布式事务

事务简介:

事务(Transaction)是数据库管理系统(DBMS)中的一个核心概念,它代表了一组数据库操作,这些操作被视为一个单一的、不可分割的工作单元。事务的概念主要用于确保数据库的一致性和完整性,尤其是在并发访问和系统故障的情况下。

事务具有以下四个特性,通常被称为ACID特性:
  1. 原子性(Atomicity):事务中的所有操作要么全部成功执行,要么全部不执行。如果事务中的任何操作失败,整个事务将回滚到事务开始前的状态。
  2. 一致性(Consistency):事务执行前后,数据库必须从一个一致性状态转移到另一个一致性状态。这意味着事务不能违反数据库的完整性约束。
  3. 隔离性(Isolation):并发执行的多个事务之间应该相互隔离,一个事务的执行不应该影响其他事务的执行。隔离级别决定了事务之间的可见性和影响程度。
  4. 持久性(Durability):一旦事务提交,其结果必须是永久性的,即使系统发生故障,提交的数据也不会丢失。
事务的隔离级别
  1. 读未提交(Read Uncommitted):最低的隔离级别,允许一个事务读取另一个事务未提交的数据。可能会导致脏读、不可重复读和幻读。
  2. 读已提交(Read Committed):一个事务只能读取另一个事务已经提交的数据。可以避免脏读,但可能会导致不可重复读和幻读。
  3. 可重复读(Repeatable Read):在一个事务中,相同的查询总是返回相同的结果,即使其他事务对数据进行了修改。可以避免脏读和不可重复读,但可能会导致幻读。
  4. 串行化(Serializable):最高的隔离级别,事务完全串行执行,避免了脏读、不可重复读和幻读,但性能较差。
事务的并发控制

在并发环境下,多个事务可能同时访问和修改相同的数据,这可能导致数据不一致的问题。数据库系统通过锁机制、多版本并发控制(MVCC)等技术来实现并发控制,确保事务的隔离性和一致性。

本地事务(Local Transaction)

  • 本地事务是指在单个数据库实例中执行的事务。
  • 事务的所有操作都在同一个数据库中进行,不需要跨多个数据库实例。

image-20241020210514797

jdbc事务

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class LocalTransactionExample {

    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/bankdb";
        String user = "root";
        String password = "password";

        Connection connection = null;
        PreparedStatement debitStmt = null;
        PreparedStatement creditStmt = null;

        try {
            // 1. 获取数据库连接
            connection = DriverManager.getConnection(url, user, password);

            // 2. 关闭自动提交,开始事务
            connection.setAutoCommit(false);

            // 3. 准备SQL语句
            String debitSql = "UPDATE Accounts SET Balance = Balance - ? WHERE AccountID = ?";
            String creditSql = "UPDATE Accounts SET Balance = Balance + ? WHERE AccountID = ?";

            debitStmt = connection.prepareStatement(debitSql);
            creditStmt = connection.prepareStatement(creditSql);

            // 4. 设置参数并执行扣款操作
            debitStmt.setDouble(1, 100.00);
            debitStmt.setInt(2, 1);
            debitStmt.executeUpdate();

            // 5. 设置参数并执行存款操作
            creditStmt.setDouble(1, 100.00);
            creditStmt.setInt(2, 2);
            creditStmt.executeUpdate();

            // 6. 提交事务
            connection.commit();
            System.out.println("Transaction committed successfully.");

        } catch (SQLException e) {
            // 7. 如果发生异常,回滚事务
            if (connection != null) {
                try {
                    connection.rollback();
                    System.out.println("Transaction rolled back due to error: " + e.getMessage());
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
        } finally {
            // 8. 关闭资源
            try {
                if (debitStmt != null) debitStmt.close();
                if (creditStmt != null) creditStmt.close();
                if (connection != null) connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

Spring事务

  • 声明式事务
  • 编程式事务

声明式事务

示例:

@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private ProductRepository productRepository;

    @TransactionalPropagation。)
    public void createOrderWithProduct(Order order, Product product) {
        // 插入订单
        orderRepository.insertOrder(order);

        // 插入产品
        productRepository.insertProduct(product);
    }
}
事务的传播机制

事务的传播机制(Transaction Propagation)是指在多个事务方法相互调用时,如何处理这些方法之间的事务关系。Spring 框架提供了多种事务传播机制,用于定义事务方法在调用其他事务方法时的行为。以下是 Spring 中常用的事务传播机制:

1. REQUIRED(默认)
  • 描述:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  • 适用场景:大多数情况下使用,适用于需要事务支持的方法。
2. REQUIRES_NEW
  • 描述:总是创建一个新的事务,如果当前存在事务,则挂起当前事务。
  • 适用场景:适用于需要独立事务的方法,即使外部方法失败,内部方法的事务仍然可以提交。
3. SUPPORTS
  • 描述:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
  • 适用场景:适用于不需要事务支持的方法,但如果外部方法有事务,则加入该事务。
4. NOT_SUPPORTED
  • 描述:以非事务方式执行,如果当前存在事务,则挂起当前事务。
  • 适用场景:适用于不需要事务支持的方法,并且希望在事务环境中以非事务方式执行。
5. MANDATORY
  • 描述:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  • 适用场景:适用于必须在事务环境中执行的方法。
6. NEVER
  • 描述:以非事务方式执行,如果当前存在事务,则抛出异常。
  • 适用场景:适用于不允许在事务环境中执行的方法。
7. NESTED
  • 描述:如果当前存在事务,则在嵌套事务中执行;如果当前没有事务,则创建一个新的事务。嵌套事务可以独立于外部事务提交或回滚。
  • 适用场景:适用于需要嵌套事务的场景,外部事务的回滚不会影响嵌套事务的提交。
事务失效的情况

@Transactional 注解是 Spring 框架中用于声明式事务管理的重要工具,但在某些情况下,@Transactional 注解可能会失效,导致事务管理无法按预期工作

1. 内部方法调用

问题描述

  • 当一个事务方法调用同一个类中的另一个事务方法时,内部方法的事务注解可能会失效。
  • 这是因为 Spring 的事务管理是通过代理模式实现的,代理对象只能拦截外部方法调用,而不能拦截内部方法调用。

示例代码

@Service
public class MyService {

    public void methodA() {
        // 外部调用
        methodB();
    }

    @Transactional
    public void methodB() {
        // 事务方法
    }
}

解决方案

  • 使用 AopContext.currentProxy() 获取当前代理对象,然后通过代理对象调用内部方法。
@Service
public class MyService {

    public void methodA() {
        // 通过代理对象调用内部方法
        ((MyService) AopContext.currentProxy()).methodB();
    }

    @Transactional
    public void methodB() {
        // 事务方法
    }
}
2. 非公开方法

问题描述

  • @Transactional 注解只能应用于公开(public)方法。如果将注解应用于非公开方法(如 private、protected 方法),事务管理将失效。

示例代码

@Service
public class MyService {

    @Transactional
    private void methodB() {
        // 事务方法
    }
}

解决方案

  • 将方法声明为公开(public)。
@Service
public class MyService {

    @Transactional
    public void methodB() {
        // 事务方法
    }
}
3. 异常未被捕获

问题描述

  • 如果事务方法中抛出的异常未被捕获,并且异常类型不是 RuntimeExceptionError,事务将不会回滚。

示例代码

@Service
public class MyService {

    @Transactional(rollbakcFor ="exception.class")
    public void methodB() throws Exception {
        // 业务逻辑
        throw new Exception("Some error");
    }
}

解决方案

  • 确保抛出的异常是 RuntimeExceptionError,或者在 @Transactional 注解中指定回滚异常类型。
@Service
public class MyService {

    @Transactional(rollbackFor = Exception.class)
    public void methodB() throws Exception {
        // 业务逻辑
        throw new Exception("Some error");
    }
}
4.异常被捕获并处理

问题描述

  • 如果在事务方法中捕获了异常并进行了处理,事务将不会回滚。

示例代码

@Service
public class MyService {

    @Transactional
    public void methodB() {
        try {
            // 业务逻辑
            throw new RuntimeException("Some error");
        } catch (Exception e) {
            // 异常处理
            System.out.println("Exception caught: " + e.getMessage());
        }
    }
}

解决方案

  • 如果需要回滚事务,可以在捕获异常后重新抛出 RuntimeExceptionError,或者手动调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
@Service
public class MyService {

    @Transactional
    public void methodB() {
        try {
            // 业务逻辑
            throw new RuntimeException("Some error");
        } catch (Exception e) {
            // 异常处理
            System.out.println("Exception caught: " + e.getMessage());
            // 手动回滚事务
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }
}
5. 事务管理器未配置

问题描述

  • 如果没有正确配置事务管理器,@Transactional 注解将无法生效。

解决方案

  • 确保在 Spring 配置文件中正确配置了事务管理器,并启用了注解驱动的事务管理。
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<tx:annotation-driven transaction-manager="transactionManager"/>
6. 非 Spring 管理的 Bean

问题描述

  • 如果 Bean 不是由 Spring 容器管理的(例如,通过 new 关键字创建的对象),@Transactional 注解将无法生效。

示例代码

public class MyService {

    @Transactional
    public void methodB() {
        // 事务方法
    }
}

public class MainApp {
    public static void main(String[] args) {
        MyService myService = new MyService();
        myService.methodB(); // @Transactional 注解失效
    }
}

解决方案

  • 确保 Bean 是由 Spring 容器管理的,通过 @AutowiredApplicationContext.getBean() 获取 Bean。
@Service
public class MyService {

    @Transactional
    public void methodB() {
        // 事务方法
    }
}

public class MainApp {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        MyService myService = context.getBean(MyService.class);
        myService.methodB(); // @Transactional 注解生效
    }
}
7. 事务传播机制不匹配

问题描述

  • 如果事务传播机制不匹配,可能会导致事务管理失效。例如,一个方法声明为 REQUIRES_NEW,但在调用链中没有正确处理事务传播。

示例代码

@Service
public class MyService {

    @Transactional
    public void methodA() {
        methodB();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // 业务逻辑
    }
}

解决方案

  • 确保事务传播机制与业务逻辑需求匹配,并正确处理事务传播。

编程式事务

编程式事务(Programmatic Transaction)和声明式事务(Declarative Transaction)是两种不同的事务管理方式,它们在实现方式、灵活性、代码复杂度和适用场景上存在显著差异。以下是对这两种事务管理方式的详细比较:

1. 实现方式

编程式事务

  • 实现方式:通过编写代码显式地管理事务的开始、提交和回滚。
  • 示例:使用 PlatformTransactionManagerTransactionTemplateTransactionalOperator 等工具类。
TransactionDefinition definition = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(definition);

try {
    // 业务逻辑
    transactionManager.commit(status);
} catch (Exception e) {
    transactionManager.rollback(status);
    throw e;
}

声明式事务

  • 实现方式:通过注解(如 @Transactional)或 XML 配置声明事务管理。
  • 示例:在方法或类上使用 @Transactional 注解。
@Transactional
public void performTransaction() {
    // 业务逻辑
}
2. 灵活性

编程式事务

  • 灵活性:非常高,可以根据业务逻辑动态决定事务的提交或回滚,支持复杂的事务逻辑和嵌套事务。
  • 适用场景:适用于需要高度定制和复杂事务逻辑的场景。

声明式事务

  • 灵活性:较低,事务管理逻辑固定,无法根据业务逻辑动态调整。
  • 适用场景:适用于简单的事务管理场景,事务逻辑相对固定。
3. 代码复杂度

编程式事务

  • 代码复杂度:较高,需要编写大量事务管理代码,代码量较大,维护成本较高。
  • 示例
让我们通过一个具体的例子来说明编程式事务的灵活性。假设我们正在开发一个电子商务平台,用户可以购买商品,并且我们提供优惠券功能。在用户使用优惠券购买商品时,我们需要执行以下步骤:

检查优惠券是否有效。
从用户的账户中扣除相应的金额。
更新库存。
如果优惠券有使用次数限制,更新优惠券的使用次数。
  
这些步骤需要在一个事务中完成,以确保数据的一致性。如果任何一步失败,整个购买过程应该回滚,用户不应该被扣款,库存也不应该减少。

下面是一个使用编程式事务来实现这个场景的示例代码:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;

@Service
public class PurchaseService {

    private final JdbcTemplate jdbcTemplate;
    private final TransactionTemplate transactionTemplate;

    @Autowired
    public PurchaseService(DataSource dataSource, PlatformTransactionManager transactionManager) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public void processPurchase(String userId, int productId, String couponCode) {
        // 使用编程式事务执行购买过程
        transactionTemplate.execute(status -> {
            try {
                // 1. 检查优惠券是否有效
                boolean isValid = checkCoupon(couponCode);
                if (!isValid) {
                    throw new RuntimeException("无效的优惠券");
                }

                // 2. 从用户账户中扣除金额
                deductAmount(userId, productId);

                // 3. 更新库存
                updateInventory(productId);

                // 4. 更新优惠券使用次数
                updateCouponUsage(couponCode);

                // 如果一切顺利,返回null以提交事务
                return null;
            } catch (Exception e) {
                // 如果出现异常,事务将回滚
                throw e;
            }
        });
    }

    private boolean checkCoupon(String couponCode) {
        // 实现优惠券有效性检查逻辑
        // 例如:检查优惠券是否存在,是否过期等
        return true;
    }

    private void deductAmount(String userId, int productId) {
        // 实现从用户账户扣除金额的逻辑
        // 例如:更新用户账户余额
    }

    private void updateInventory(int productId) {
        // 实现更新库存的逻辑
        // 例如:减少商品库存数量
    }

    private void updateCouponUsage(String couponCode) {
        // 实现更新优惠券使用次数的逻辑
        // 例如:增加优惠券的使用次数
    }
}
在这个例子中,我们使用 TransactionTemplate 来管理事务。processPurchase 方法包含了购买商品的所有步骤,这些步骤都在 transactionTemplate.execute 方法的 lambda 表达式中执行。如果在执行过程中抛出异常,事务将自动回滚,确保所有操作要么全部成功,要么全部失败。

这个例子展示了编程式事务的灵活性,允许我们在运行时根据业务逻辑的需要动态地控制事务的边界。通过编程式事务,我们可以精确地控制事务的开始和结束,以及在出现异常时的回滚行为。

声明式事务

  • 代码复杂度:较低,事务管理代码量少,代码简洁,易于维护。
  • 示例
@Transactional
public void performTransaction() {
    // 业务逻辑
}
4. 适用场景

编程式事务

  • 适用场景
    • 复杂事务逻辑:需要根据不同的条件动态决定事务的提交或回滚。
    • 嵌套事务:需要在事务中嵌套其他事务,并且希望子事务独立于父事务提交或回滚。
    • 异常处理:需要根据不同的异常类型进行不同的处理。
    • 事务超时和隔离级别:需要动态设置事务的超时时间和隔离级别。
    • 事务传播机制:需要根据业务逻辑动态选择事务传播机制。

声明式事务

  • 适用场景
    • 简单事务管理:事务逻辑相对固定,不需要复杂的动态控制。
    • 快速开发:事务管理代码量少,开发效率高。
    • 团队协作:代码简洁,易于理解和维护,适合团队协作开发。
5. 性能

编程式事务

  • 性能:由于需要显式管理事务,可能会引入一定的性能开销,尤其是在高并发场景下。

声明式事务

  • 性能:由于事务管理逻辑由框架自动处理,性能开销较小,适合高并发场景。
6. 异常处理

编程式事务

  • 异常处理:可以灵活处理不同类型的异常,并根据异常类型决定是否回滚事务。
try {
    // 业务逻辑
    transactionManager.commit(status);
} catch (DataValidationException e) {
    // 记录日志,但不回滚事务
    log.error("Data validation error: " + e.getMessage());
} catch (Exception e) {
    transactionManager.rollback(status);
    throw e;
}

声明式事务

  • 异常处理:默认情况下,只有 RuntimeExceptionError 会导致事务回滚,可以通过 rollbackFor 属性指定其他异常类型。
@Transactional(rollbackFor = Exception.class)
public void performTransaction() {
    // 业务逻辑
}
总结
特性编程式事务声明式事务
实现方式通过编写代码显式管理事务通过注解或 XML 配置声明事务管理
灵活性非常高,支持复杂事务逻辑和嵌套事务较低,事务管理逻辑固定
代码复杂度较高,需要编写大量事务管理代码较低,代码简洁,易于维护
适用场景复杂事务逻辑、嵌套事务、异常处理等简单事务管理、快速开发、团队协作
性能可能引入一定的性能开销性能开销较小,适合高并发场景
异常处理可以灵活处理不同类型的异常默认处理 RuntimeExceptionError

通过理解和正确使用编程式事务和声明式事务,可以更好地管理事务逻辑,确保系统的可靠性和稳定性。在实际应用中,可以根据业务需求选择合适的事务管理方式。

分布式事务

分布式事务产生背景

业务服务化拆分

  • 在业务发展初期,“一块大饼”的单业务系统架构,能满足基本的业务需求。但是随着业务的快速发展,系统的访问量和业务复杂程度都在快速增长,单系统架构逐渐成为业务发展瓶颈,解决业务系统的高耦合、可伸缩问题的需求越来越强烈。
  • 按照面向服务架构(SOA)的设计原则,将单业务系统拆分成多个业务系统,降低了 各系统之间的耦合度,使不同的业务系统专注于自身业务,更有利于业务的发展和系统容量的伸缩。
  • 业务系统按照服务拆分之后,一个完整的业务往往需要调用多个服务,如何保证多个服务间的数据一致性成为一个难题。

数据库拆分

  • 业务数据库起初是单库单表,但随着业务数据规模的快速发展,数据量越来越大,单库单表逐渐成为瓶颈。所以我们对数据库进行了水平拆分,将原单库单表拆分成数据库分片。
  • 如下图所示,分库分表之后,原来在一个数据库上就能完成的写操作,可能就会跨多个数据库,这就产生了跨数据库事务问题。

image-20241020214322879

Seata 是什么

https://seata.apache.org/zh-cn/docs/overview/what-is-seata

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服 务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction service 全局事务服务)

image-20241024224530545

官网:https://seata.io/zh-cn/index.html

源码: https://github.com/seata/seata

常见分布式事务解决方案

  • seata阿里分布式事务框架
  • 消息队列 MQ
  • saga
  • XA

他们有一个共同点,都是“两阶段(2PC)”。“两阶段”是指完成整个分布式事务,划分成两个步聚完成。
实际上,这四种常见的分布式事务解决方案,分别对应着分布式事务的四种模式:AT、TCC、Saga、XA;
四种分布式事务模式,都有各自的理论基础,分别在不同的时间被提出:每种模式都有它的适用场景,同样每个模式也都诞生有各自的代表产品;而这些代表产品,可能就是我们常见的(全局事务、基于可靠消息、最大努力通知、TCC)。

下面,我们会分别来看4种模式(AT、TCC、Saga、XA)的分布式事务实现。
在看具体实现之前,先讲下分布式事务的理论基础。

分布式事务理论基础

解决分布式事务,也有相应的规范和协议。分布式事务相关的协议有2PC、3PC。
由于三阶段提交协议3PC非常难实现,目前市面主流的分布式事务解决方案都是2PC协议。这就是开始提及的常见分布式事务解决方案里面,那些列举的都有一个共同点“两阶段”的内在原因。

有些文章分析2PC时,几乎都会用TCC两阶段的例子,第一阶段try,第二阶段完成confirm或cancel。其实2PC并不是专为实现TCC设计的,2PC具有普适性—协议一样的存在,目前绝大多数分布式解决方案都是以两阶段提交协议2PC为基础的。

TCC(Try-Confirm-Cancel)实际上是服务化的两阶段提交协议.

2PC两阶段提交协议:

2PC(两阶段提交,Two-Phase Commit) ,顾名思义,分为两个阶段:Prepare和Commit

Prepare:提交事务请求:流程如下:
image-20241020222818723
  1. 询问: 协调者向所有参与者发送事务请求,询问是否可执行事务操作,然后等待各个参与者的响应。

  2. 执行:各个参与者接收到协调者事务请求后,执行事务操作(例如更新一个关系型数据库表中的记录),并将Undo和Redo信息记录事务日志中。

  3. 响应 如果参与者成功执行了事务并写入Udo和Redo信息,则向协调者返回YES响应,否则返回NO响应。当然,参与者也可能宕机,从而不会返回响应

Commit:提交:流程如下:
image-20241020224535355
  1. commit请求: 协调者向所有参与者发送Commit请求。
  2. 事务提交: 参与者收到Commit请求后,执行事务提交,提交完成后释放事务执行期占用的所有资源。
  3. 反馈结果: 参与者执行事务提交后向协调者发送ACk响应。
  4. 完成事务: 接收到所有参与者的Ack响应后,完成事务提交。

image-20241020224007272

中断事务

在执行Prepare步骤过程中,如果某些参与者执行事务失败、宕机或与协调者之间的网络中断,那么协调者就无法收到所有参与者的YES响应,或者某个参与者返回了No响应,

此时,协调者就会进入回退流程,对事务进行回退。

image-20241020225818011
  1. rollback请求: 协调者向所有参与者发送Rollback请求。
  2. 事务回滚: 参与者收到Rollback后,使用Prepare阶段的Undo日志执行事务回滚,完成后释放事务执行期占用的所有资源。
  3. 反馈结果 :参与者执行事务回滚后向协调者发送Ack响应。
  4. 中断事务: 接收到所有参与者的Ack响应后,完成事务中断

image-20241020225504488

2PC的问题
  1. 同步阻塞 : 参与者在等待协调者的指令时,其实是在等待其他参与者的响应,在此过程中,参与者是无法进行其他操作的,也就是阻塞了其运行。倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信息,那么会导致参与者一直阻塞下去。
  2. 单点故障: 在2PC中,一切请求都来自协调者,所以协调者的地位是至关重要的,如果协调者宕机,那么就会使参与者一直阻塞并一直占用事务资源。如果协调者也是分布式,使用选主方式提供服务,那么在一个协调者挂掉后,可以选取另一个协调者继续后续的服务,可以解决单点问题。但是,新协调者无法知道上一个事务的全部状态信息(例如已等待Prepare响应的时长等),所以也无法顺利处理上一个事务。
  3. 数据不一致: Commit事务过程中Commit请求/Rollback请求可能因为协调者宕机或协调者与参与者网络问题丢失,那么就导致了部分参与者没有收到Commit/Rollback请求,而其他参与者则正常收到执行了Commit/Rollback操作,没有收到请求的参与者则继续阻塞。这时,参与者之间的数据就不再一致了。
    当参与者执行Commit/Rollback后会向协调者发送Ack,然而协调者不论是否收到所有的参与者的Ack,该事务也不会再有其他补救措施了,协调者能做的也就是等待超时后像事务发起者返回一个“我不确定该事务是否成功”。
  4. 环境可靠性依赖: 协调者Prepare请求发出后,等待响应,然而如果有参与者宕机或与协调者之间的网络中断,都会导致协调者无法收到所有参与者的响应,那么在2PC中,协调者会等待一定时间,然后超时后,会触发事务中断,在这个过程中,协调者和所有其他参与者都是出于阻塞的。这种机制对网络问题常见的现实环境来说太苛刻了。
4种模式(AT,TCC,Saga,XA)的分布式事务实现
AT模式(auto transcation)

AT模式是一种无侵入的分布式事务解决方案。阿里seata框架,实现了该模式。Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。
在AT模式下,用户只需关注自己的“业务SQL”,用户的“业务SQL”作为一阶段,Seata框架会自动生成事务的二阶段提交和回滚操作。

image-20241021163108458

AT模式如何做到对业务的无侵入:

  • 一阶段:
    在一阶段,Seata会拦截“业务SQL”,首先解析SQL语义,找到“业务SQL"要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务SQL"更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

image-20241021164254599

以上述场景为例:

image-20241021164510777

  • 二阶段提交:
    二阶段如果是提交的话,因为“业务SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

image-20241021164718799

  • 二阶段回滚:
    二阶段如果是回滚的话,Seata就需要回滚一阶段已经执行的“业务SQL”,还原业务数据。回滚方式便是“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据"和“after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

image-20241021165141231

AT模式的一阶段、二阶段提交和回滚均由Seata框架自动生成,用户只需编写“业务SQL”,便能轻松接入分布式事务,AT模式是一种对业务无任何侵入分布式事务解决方案。

  • 总结:

    性能:高

    模式:AP,存在数据不一致的中间状态

    难易程度:简单,靠SEATA自己解析反向SQL并回滚

    使用要求:

    所有服务与数据库必须要自己拥有管理权,因为要创建UNDO LOG表

    最好是MySQL,也支持PostgreSQLOracleOceanBase,H2

    应用场景:高并发互联网应用,允许数据出现短时不一致,可通过对账程序或补录来保证最终一致性。

    在分布式事务的背景下,"AP"通常指的是"Availability and Partition Tolerance",这是CAP定理中的两个要素。CAP定理指出,在设计分布式系统时,不可能同时满足以下三个特性:一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)。在分布式事务的解决方案中,"AP"模式通常意味着系统更侧重于可用性和分区容错性,而不是强一致性。
    
    在Seata的AT模式中,"AP"设计可能意味着在某些情况下,为了保持系统的高可用性和分区容错性,可能会牺牲一定的数据一致性。Seata的AT模式是一种无侵入的分布式事务解决方案,它通过记录业务数据的变更日志来实现事务的最终一致性。在AT模式下,数据的变更是实际提交到数据库的,如果后续的分支事务出现问题,回滚操作依赖于日志来实现数据的最终一致性,而不是像XA模式那样,通过数据库本身的事务管理来保证强一致性。
    
TCC模式

TCC模式需要用户根据自己的业务场景实现Try、Confirm和Cancel三个操作;事务发起方在一阶段执行Try方式,在二阶段提交执行Confirm方法,二阶段回滚执行Cancel方法。

  • 特点:
    • 侵入性比较强,并且得自己实现相关事务控制逻辑
    • 在整个过程基本没有锁,性能更强

image-20241021181942538

场景举例:

image-20241021182622969

可靠消息最终一致性方案:

image-20241021183453128

  • 总结:

    性能:好

    模式:AP,存在数据不一致的中间状态

    难易程度:复杂,SEATA TC只负责全局事务的提交与回滚指令,具体的回滚处理全靠程序员自己实现。

    使用要求:

    所有服务与数据库必须要自己拥有管理权

    支持异构数据库,可以使用不同选型实现

    应用场景:高并发互联网应用,允许数据出现短时不一致,可通过对账程序或补录来保证最终一致性。

SAGA

Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

image-20241024234724506

特点:

性能:不一定,取决于三方服务

模式:AP,存在数据不一致的中间状态

难易程度:复杂,提交与回滚流程全靠程序员编排

示例状态图:

image-20241025001423835

使用要求:

在当前架构引入状态机机制,类似于工作流

上手难度大

无法保证隔离性

应用场景:

需要与第三方交互时才会考虑,例如:调用支付宝支付接口->出库失败->调用支付宝退款接口

XA 模式

XA 模式是从 1.2 版本支持的事务模式。XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。Seata XA 模式是利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。

image-20241025001957757

一个简化的例子,说明在订单服务和库存服务中,XA 模式是如何工作的:

  1. 全局事务的开始
  • 用户发起一个购买请求,订单服务首先向全局事务管理器(TC)申请开启一个新的全局事务。
  1. 分支事务的注册
  • 订单服务和库存服务各自向 TC 注册自己的分支事务,每个分支事务都会被分配一个分支事务 ID。
  1. 执行阶段
  • 订单服务
    • 执行 XA start,开启一个 XA 分支事务。
    • 执行业务 SQL,创建订单。
    • 执行 XA end,结束当前 XA 分支事务,但不提交。
  • 库存服务
    • 执行 XA start,开启另一个 XA 分支事务。
    • 执行业务 SQL,扣减库存。
    • 执行 XA end,结束当前 XA 分支事务,但不提交。
  1. 完成阶段
  • 全局提交或回滚
    • 如果订单创建和库存扣减都成功,订单服务向 TC 发起全局提交请求。
    • 如果任何一个步骤失败,订单服务向 TC 发起全局回滚请求。
  • 分支提交或回滚
    • 如果 TC 收到全局提交请求,它会向订单服务和库存服务的分支事务发送 XA commit 命令。
    • 如果 TC 收到全局回滚请求,它会向订单服务和库存服务的分支事务发送 XA rollback 命令。
  1. 事务结束
  • 所有分支事务完成提交或回滚后,全局事务结束。

注意:

使用 XA 模式时,需要注意的是,在 XA prepare 阶段,分支事务会锁定资源,直到收到 TC 的 XA commit 或 XA rollback 指令。这意味着在这段时间内,其他事务可能无法访问这些资源,从而导致性能问题。因此,虽然 XA 模式可以保证强一致性,但在高并发场景下可能不是最优选择。

特点
性能:低

模式:CP,强一致性

难易程度:简单,基于数据库自带特性实现,无需改表

使用要求:

使用支持XA方案的关系型数据库(主流都支持)

应用场景:金融行业,并发量不大,但数据很重要的项目

Seata的三大角色

TC (Transaction Coordinator) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。

image-20241024224530545

快速开始

AT模式流程图

image-20241024233123416

Seata Server(TC)环境搭建

https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html

下载地址

版本说明

image-20241021221528186
Server端存储模式(store.mode)支持三种:
  • file:单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高
  • db(5.7+):高可用模式,全局事务会话信息通过db共享,相应性能差些
  • redis:Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置适合当前场景的redis持久化配置

资源目录:https://github.com/apache/incubator-seata/tree/v1.4.2/script

  • client

    存放client端sql脚本,参数配置

  • config-center

    各个配置中心参数导入脚本,config.txt(包含server和client,原名nacos-config.txt)为通用参数文件

  • server

    server端数据库脚本及各个容器配置

部署方案:

db存储模式+Nacos(注册&配置中心)部署高可用模式

1.修改store.mode

启动包: seata–>conf–>file.conf,修改store.mode=“db”

源码: 根目录–>seata-server–>resources–>file.conf,修改store.mode=“db”

image-20241021224914615

2.修改数据库配置

image-20241021225318895

3. 建库建表

​ 在seata提供的资源目录查找—seata—>script---->server—>db----->mysql.sql

4. 修改nacos配置
  • 文件位置

    • image-20241022214614427
  • register

    • image-20241022221112645
  • config

    • 这里注意修改type模式,以及在nacos中创建dataId,以及分组
    • image-20241022222243222
  • script目录下的config.txt文件并修改成持久化数据库的节点,需要修改位置已截图,将下方txt中的内容复制到seataServer.properties中。

    • image-20241022222156202
    • image-20241022222129434
  • 如果低版本seata 无上面config的dataId 配置则找到脚本文件执行下方脚本

    • image-20241022223300175

    • 执行脚本指令:

    • sudo sh ./nacos-config.sh -h 116.196.119.230 -p 8848 -g SEATA_GROUP
      
    • 参数解读:

    • image-20241022223635143

5. 启动分布式事务seata
  • ./seata-server.sh -h 116.196.119.230 -p 8091 -m db
    ./seata-server.sh -h 127.0.0.1 -p 8091 -m db
    

    image-20241023225152702

    启动成功:

    image-20241023225047037

代码实操:
1.依赖:

涉及到分布式事务的服务都添加

 <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
 </dependency>
2.创建数据库表:

同样涉及到分布式事务的服务都添加

-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
3.指定seata配置
seata:
  enabled: true
  enable-auto-data-source-proxy: true
  config:
    type: nacos
    nacos:
      server-addr: 116.196.119.230:8848
      group: SEATA_GROUP
      username: nacos
      password: nacos
      data-id: seataServer.properties
  registry:
    type: nacos
    nacos:
      application:  seata-server
      server-addr: 116.196.119.230:8848
      group: SEATA_GROUP
      username: nacos
      password: nacos
  tx-service-group: default_tx_group
 //  tx-service-group 动态切换集群
4.代码测试
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderDao orderDao;

    @Autowired
    private StockFeignController stockFeignController;

    @Transactional
    @GlobalTransactional
    @Override
    public boolean add(Order newOrder) {
        // 增加订单信息
        int result = orderDao.add(newOrder);
        Stock stock = new Stock();
        stock.setProductId(newOrder.getProductId());
        // 远程调用库存服务扣件库存
        stockFeignController.reduce(stock);

        int i = 1 / 0;
        return result > 0;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值