基于 Spring 的本地事务管理

1. 什么是本地事务?

本地事务是指在单一数据库系统内执行的一组操作,这些操作要么全部完成,要么全部不执行,是一个不可分割的工作单元。本地事务具有ACID特性:

  • 原子性(Atomicity):事务中的所有操作都作为一个整体提交或回滚;如果事务的一部分失败,则整个事务都会被撤销。
  • 一致性(Consistency):事务将数据库从一个一致状态转换到另一个一致状态,确保数据的完整性和规则得到遵守。
  • 隔离性(Isolation):多个并发事务之间相互隔离,一个事务的中间状态对其他事务是不可见的。
  • 持久性(Durability):一旦事务成功提交,它对数据库的更改就是永久性的,即使系统出现故障。

2. 使用JDBC进行本地事务管理

2.1. 基本步骤

  1. 获取数据库连接:使用DriverManager.getConnection()方法获取数据库连接。
  2. 关闭自动提交:调用connection.setAutoCommit(false)关闭自动提交模式。
  3. 执行SQL操作:在同一个连接上执行多个SQL语句。
  4. 提交或回滚事务:根据操作结果调用connection.commit()或connection.rollback()。
  5. 关闭连接:释放数据库连接资源。

2.2. 示例代码

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

public class JdbcTransactionExample {
    private static final String DB_URL = "jdbc:mysql://localhost:3306/test";
    private static final String USER = "root";
    private static final String PASS = "root";

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement stmt1 = null;
        PreparedStatement stmt2 = null;

        try {
            // 1. 获取数据库连接
            conn = DriverManager.getConnection(DB_URL, USER, PASS);
            // 2. 关闭自动提交
            conn.setAutoCommit(false);

            // 3. 执行SQL操作
            String sql1 = "INSERT INTO users (name, email) VALUES (?, ?)";
            stmt1 = conn.prepareStatement(sql1);
            stmt1.setString(1, "John Doe");
            stmt1.setString(2, "john.doe@example.com");
            stmt1.executeUpdate();

            // 模拟异常
            // int a = 0/0;

            String sql2 = "UPDATE accounts SET balance = balance - 100 WHERE user_id = ?";
            stmt2 = conn.prepareStatement(sql2);
            stmt2.setInt(1, 1);
            stmt2.executeUpdate();

            // 4. 提交事务
            conn.commit();
            System.out.println("Transaction committed successfully.");
        } catch (SQLException e) {
            if (conn != null) {
                try {
                    // 5. 回滚事务
                    conn.rollback();
                    System.out.println("Transaction rolled back.");
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
            e.printStackTrace();
        } finally {
            // 6. 关闭连接
            try {
                if (stmt1 != null) stmt1.close();
                if (stmt2 != null) stmt2.close();
                if (conn != null) conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

3. 使用Spring框架进行本地事务管理

3.1. 编程式事务管理

编程式事务管理是指在代码中显式地控制事务的开始、提交和回滚。这种方式提供了更大的灵活性,但也会增加代码的复杂性。

3.1.1. 使用TransactionTemplate进行编程式事务管理

TransactionTemplate是Spring提供的一个类,用于简化编程式事务管理。它封装了事务管理的逻辑,使得开发者可以更方便地控制事务。

3.1.1.1. 配置TransactionTemplate

首先,需要配置一个TransactionTemplate bean。通常在Spring Boot项目中,可以通过DataSourceTransactionManager来配置。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;

import javax.sql.DataSource;

@Configuration
public class TransactionConfig {

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        // 设置事务隔离级别
//        transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        // 设置事务只读
//        transactionTemplate.setReadOnly(true);
        // 设置传播行为
//        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        // 设置超时时间为30秒
//        transactionTemplate.setTimeout(30); 
        return transactionTemplate;
    }
}

注意: 在Spring

Boot中,PlatformTransactionManager、TransactionTemplate通常会被JPA、JDBC、MyBatis、Hibernate等常见框架自动配置和注入,所以通常不需要手动配置PlatformTransactionManager、TransactionTemplate。

3.1.2. 使用TransactionTemplate管理事务

在需要事务管理的方法中,注入TransactionTemplate并使用它来控制事务。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private TransactionTemplate transactionTemplate;

    public void registerUser(User user) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try {
                    // 1. 保存用户信息
                    userRepository.save(user);

                    // 设置保存点
//                    Object savepoint = status.createSavepoint();

                    // ... 其他操作

                    // 操作失败,回滚到保存点
//                     status.rollbackToSavepoint(savepoint);

                    // 2. 更新账户余额
                    Account account = accountRepository.getById(user.getId());
                    account.setBalance(account.getBalance() - 100);
                    accountRepository.updateById(account);

                    // 释放保存点
//                    status.releaseSavepoint(savepoint);
                } catch (Exception e) {
                    // 3. 设置事务回滚
                    status.setRollbackOnly();
                    e.printStackTrace();
                }
            }
        });
    }
}

3.1.2 使用PlatformTransactionManager进行编程式事务管理

除了TransactionTemplate,还可以直接使用PlatformTransactionManager来管理事务。

3.1.2.1 配置PlatformTransactionManager

与前面的配置相同,确保已经配置了PlatformTransactionManager。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
public class TransactionConfig {

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

注意: 在Spring
Boot中,PlatformTransactionManager通常会被JPA、JDBC、MyBatis、Hibernate等常见框架自动配置和注入,所以通常不需要手动配置PlatformTransactionManager。

3.1.2.2 使用PlatformTransactionManager管理事务

在需要事务管理的方法中,注入PlatformTransactionManager并使用它来控制事务。

import org.springframework.beans.factory.annotation.Autowired;
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;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private PlatformTransactionManager transactionManager;

    public void registerUser(User user) {
        // 1. 定义事务属性
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        // 设置传播行为
//        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        // 设置隔离级别
//        def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        // 设置是否只读
//        def.setReadOnly(false); 

        // 2. 获取事务状态
        TransactionStatus status = transactionManager.getTransaction(def);

        try {
            // 3. 保存用户信息
            userRepository.save(user);

            // 创建保存点
//            Object savepoint = status.createSavepoint();
            // 回滚到保存点
//            status.rollbackToSavepoint(savepoint);

            // 4. 更新账户余额
            Account account = accountRepository.getById(user.getId());
            account.setBalance(account.getBalance() - 100);
            accountRepository.updateById(account);

            // 释放保存点
//            status.releaseSavepoint(savepoint);

            // 5. 提交事务
            transactionManager.commit(status);
        } catch (Exception e) {
            // 6. 回滚事务
            transactionManager.rollback(status);
            e.printStackTrace();
        }
    }
}

3.1.3 TransactionTemplate 和 PlatformTransactionManager 区别和联系

特性/功能 TransactionTemplate PlatformTransactionManager
用途 提供模板化的编程方式来管理事务,简化事务管理代码。 定义事务管理的基本接口,提供事务管理的核心功能。
编程模型 编程式事务管理。 可以用于编程式和声明式事务管理。
使用方式 直接在代码中使用 TransactionTemplate 来管理事务。 通常通过 TransactionTemplate@Transactional 注解使用。
配置属性 可以设置隔离级别、传播行为、超时时间、只读属性等。 通过 TransactionTemplate 配置,也可以通过其他方式配置。
回滚规则 通过 TransactionCallback 手动处理异常来控制回滚。 通过 TransactionAttribute 配置回滚规则。
保存点 支持保存点(savepoints),可以部分回滚事务。 支持保存点(savepoints),可以部分回滚事务。
事务状态管理 使用 TransactionStatus 对象来管理事务状态。 使用 TransactionStatus 对象来管理事务状态。
依赖 依赖于 PlatformTransactionManager 是事务管理的核心接口,TransactionTemplate 依赖于它。
灵活性 提供更高的灵活性,适合复杂的事务逻辑。 提供基本的事务管理功能,适合大多数情况。

3.2. 声明式事务管理

Spring框架提供了强大的声明式事务管理,简化了事务处理代码。通过使用@Transactional注解,可以方便地定义方法级别的事务边界。

3.2.1 声明式事务使用

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private AccountRepository accountRepository;

    @Transactional
    public void registerUser(User user) {
        // 1. 保存用户信息
        userRepository.save(user);

        // 2. 更新账户余额
        Account account = accountRepository.getById(user.getId());
        account.setBalance(account.getBalance() - 100);
        accountRepository.updateById(account);

        // 如果抛出运行时异常,事务将回滚
    }
}

常用的 @Transactional 参数及其说明

  1. propagation
  • 类型: Propagation
  • 默认值: Propagation.REQUIRED
  • 描述: 定义了事务的传播行为,即当前事务与新事务之间的关系。
  • 可选值:
    • REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
    • SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
    • MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
    • REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则挂起当前事务。
    • NOT_SUPPORTED: 以非事务方式执行操作,如果当前存在事务,则挂起当前事务。
    • NEVER: 以非事务方式执行操作,如果当前存在事务,则抛出异常。
    • NESTED: 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则创建一个新的事务。
  1. isolation
  • 类型: Isolation
  • 默认值: Isolation.DEFAULT
  • 描述: 定义了事务的隔离级别,决定了事务之间如何相互影响。
  • 可选值:
    • DEFAULT: 使用底层数据库的默认隔离级别。
    • READ_UNCOMMITTED: 最低的隔离级别,允许读取未提交的数据。
    • READ_COMMITTED: 只能读取已经提交的数据,不能读取未提交的数据。
    • REPEATABLE_READ: 在同一个事务中,多次读取同一数据的结果是一致的。
    • SERIALIZABLE: 最高的隔离级别,事务串行执行,完全隔离。
  1. timeout
  • 类型: int
  • 默认值: -1
  • 描述: 事务的超时时间,单位为秒。如果事务在这个时间内没有完成,则自动回滚。
  1. readOnly
  • 类型: boolean
  • 默认值: false
  • 描述: 标记当前事务是否为只读事务。对于某些数据库,标记为只读可以提高性能。
  1. rollbackFor
  • 类型: Class[]
  • 默认值: {}
  • 描述: 指定哪些异常需要触发事务回滚。默认情况下,运行时异常(RuntimeException)和错误(Error)会触发回滚,而检查型异常(Exception)不会触发回滚。
  1. noRollbackFor
  • 类型: Class[]
  • 默认值: {}
  • 描述: 指定哪些异常不需要触发事务回滚。即使这些异常被抛出,事务也不会回滚。

3.2.2 @Transactional失效场景

该部分转载于 一口气怼完12种@Transactional的失效场景

3.2.2.1 代理不生效

声明式事务是基于 AOP(面向切面编程)代理机制来拦截方法调用,并在方法执行前后进行事务管理。Spring 生成代理的方式主要有两种:JDK
动态代理和CGLIB 代理。

PlatformTransactionManager 实现
选择PlatformTransactionManager
管理JDBC事务
管理JPA事务
管理JTA事务
PlatformTransactionManager
获取PlatformTransactionManager
DataSourceTransactionManager
JpaTransactionManager
JtaTransactionManager
启动Spring应用
扫描带有@Transactional的类和方法
解析@Transactional注解
使用AnnotationTransactionAttributeSource解析注解属性
创建AOP代理
目标类是否实现了接口?
使用JDK动态代理
使用CGLIB代理
创建JdkDynamicAopProxy
创建ObjenesisCglibAopProxy
代理对象拦截方法调用
调用TransactionInterceptor
获取TransactionAttribute
获取TransactionStatus
开启事务
执行目标方法
方法是否正常返回?
提交事务
回滚事务
方法执行完成
  1. JDK 动态代理:
  • 基本概念:
    • 基于接口的代理:JDK 动态代理是基于 Java 的反射机制实现的,它只能代理实现了接口的类。
    • 生成代理类:在运行时动态生成一个实现了相同接口的代理类。
  • 适用场景:
    • 目标类实现了接口:只有当目标类实现了至少一个接口时,才能使用 JDK 动态代理。
  • 优点:
    • 性能较好:由于基于标准的 Java 反射机制,性能相对较高。
    • 符合面向接口编程:符合 Java 的面向接口编程原则。
  • 缺点:
    • 仅限接口:只能代理实现了接口的类,无法代理没有实现接口的类。
  1. CGLIB 代理:
  • 基本概念:
    • 基于类的代理:CGLIB(Code Generation Library)代理是通过生成目标类的子类来实现的,因此可以代理没有实现接口的类。
    • 生成代理类:在运行时动态生成一个继承自目标类的代理类。
  • 适用场景:
    • 目标类没有实现接口:当目标类没有实现任何接口时,只能使用 CGLIB 代理。
    • 需要代理类本身:某些情况下需要代理类本身而不是接口。
  • 优点:
    • 无需接口:可以代理没有实现接口的类。
    • 灵活性高:可以代理类本身。
  • 缺点:
    • 性能稍差:由于需要生成子类,性能相对 JDK 动态代理稍差。
    • 无法代理 final 类:CGLIB 无法代理 final 类和 final 方法。
  1. Spring 如何选择代理方式:
    Spring 会根据以下条件自动选择使用 JDK 动态代理还是 CGLIB 代理:
  • 默认行为:
    • 如果目标类实现了接口,默认使用 JDK 动态代理
    • 如果目标类没有实现接口,默认使用 CGLIB 代理
  • 配置选项:
    • proxy-target-class 属性:可以通过配置 proxy-target-class 属性来强制使用 CGLIB 代理。
      • proxy-target-class=true:强制使用 CGLIB 代理。
      • proxy-target-class=false:强制使用 JDK 动态代理(如果可能)。

基于 Spring 的本地事务管理_第1张图片

3.2.2.1.1 将注解标注在接口方法上

失效原因:

  • @Transactional是支持标注在方法与类上的。一旦标注在接口上,对应接口实现类的代理方式如果是CGLIB,将通过生成子类的方式生成目标类的代理,将无法解析到@Transactional,从而事务失效。

解决方案:

  • 将@Transactional标注在接口方法上,将接口实现类的代理方式改为JDK动态代理。
  • 将@Transactional标注在方法与类上的。
3.2.2.1.2 被final、static关键字修饰的类或方法

失效原因:

  • final 方法:对于 final 方法,由于它们不能被子类重写,JDK 动态代理和 CGLIB 代理都无法生成有效的代理逻辑来拦截这些方法。
  • static 方法:AOP 代理机制依赖于实例级别的拦截,而 static 方法属于类级别,不是实例的一部分。因此,Spring 的 AOP 代理无法拦截
    static 方法。

解决方案:

  • 将 final、static 修饰的类方法改为非final、static 修饰。
3.2.2.1.3 类方法内部调用

失效原因:

  • 当一个类中的方法直接调用另一个带有 @Transactional 注解的方法时,这个调用是通过 this 引用进行的,而不是通过代理对象。因此,AOP
    代理无法拦截到这个调用,事务管理逻辑也不会被触发。

解决方案:

  • 将业务逻辑移到非内部调用的方法中:将实际的业务逻辑移到另一个服务类或方法中,避免在同一类中自调用带有 @Transactional
    注解的方法。
  • 自我注入的方式:通过构造函数注入或字段注入自身实例,确保调用的是代理对象而不是直接通过 this 引用调用。
  • 使用代理对象调用:通过AopContext.currentProxy()获取代理对象

AspectJ 解决事务失效:

  1. 引入依赖
<dependency>
     <groupId>org.aspectjgroupId>
     <artifactId>aspectjweaverartifactId>
dependency>
  1. 暴露代理对象
    在启动类上添加注解@EnableAspectJAutoProxy(exposeProxy=true),暴露代理对象:
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
public class AppConfig {
}
  1. 使用代理对象
    内部调用失效示例:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TransactionalService {

    @Autowired
    private AnotherService anotherService;

    @Transactional
    public void methodA() {
        // 一些数据库操作
        System.out.println("Method A is running");
        methodB(); // 内部方法调用
    }

    @Transactional
    public void methodB() {
        // 一些数据库操作
        System.out.println("Method B is running");
    }
}

使用代理对象调用示例:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TransactionalService {

    @Autowired
    private AnotherService anotherService;

    @Transactional
    public void methodA() {
        // 一些数据库操作
        System.out.println("Method A is running");
         ((TransactionalService) AopContext.currentProxy()).methodB(); // 使用AopContext获取代理对象
    }

    @Transactional
    public void methodB() {
        // 一些数据库操作
        System.out.println("Method B is running");
    }
}
3.2.2.1.4 当前类没有被Spring管理

失效原因:

  • 如果类不是由 Spring 容器管理的(例如通过 new 关键字直接实例化),那么即使该类的方法上有 @Transactional 注解,事务管理也不会生效。

解决方案:

  • 将类注册到 Spring 容器中,使其成为 Spring 管理的 bean。
3.2.2.2 框架或底层不支持的功能

这类失效场景主要聚焦在框架本身在解析@Transactional时的内部支持。如果使用的场景本身就是框架不支持的,那事务也是无法生效的。

3.2.2.2.1 非public修饰的方法

失效原因:

  • 默认情况下,Spring 的 AOP 代理只拦截 public 方法。对于非 public 方法(如 private、protected
    或包级私有方法),由于方法不可见,代理机制无法拦截这些方法,因此事务管理不会生效。

解决方案:

  • 将方法设置为 public。
3.2.2.2.2 多线程调用

在 TransactionAspectSupport.prepareTransactionInfo 方法中
基于 Spring 的本地事务管理_第2张图片
txInfo.bindToThread() 无论是否创建了新的事务,都会将TransactionInfo对象绑定到当前线程。这是为了确保事务信息栈的完整性,即使没有创建新事务。

3.2.2.2.3 数据库本身不支持事务

某些NoSQL数据库或特定版本的关系型数据库,比如Mysql的Myisam存储引擎是不支持事务的。Spring
的声明式事务管理将无法正常工作,因为这些数据库缺乏必要的事务特性(如ACID属性)。

3.2.2.2.4 未开启事务

在MVC项目中还需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。

3.2.2.3 错误使用@Transactional
3.2.2.3.1 错误的传播机制

失效原因:

  • 传播行为不匹配:如果使用了不合适的传播行为,例如 SUPPORTS 或 NOT_SUPPORTED,可能会导致事务管理失效。这些传播行为在某些情况下不会创建或加入事务。
  • 方法调用链中的传播行为冲突:在方法调用链中,不同方法可能有不同的传播行为,如果这些行为不兼容,可能会导致事务管理失效。

解决方案:

  • 使用合适的传播行为:确保在方法调用链中使用正确的传播行为,以匹配事务的预期行为。
3.2.2.3.2 rollbackFor属性设置错误

失效原因:

  • rollbackFor 属性用于指定哪些异常会触发事务回滚。默认情况下,Spring 只会在遇到未检查异常(即继承自 RuntimeException
    的异常)时回滚事务。对于已检查异常(即不继承自 RuntimeException 的异常),你需要显式地指定它们以触发回滚。

解决方案:

  • 使用 rollbackFor 属性:在方法上添加 rollbackFor 属性,并指定需要触发回滚的异常类型。
3.2.2.3.3 异常被内部 try-catch

失效原因:

  • 当异常在方法内部被捕获并处理时,Spring 的事务管理机制无法感知到异常的发生,因此不会触发回滚操作。这会导致事务管理失效,即使你正确配置了
    rollbackFor 属性。

解决方案:

  • 避免在方法内部处理异常:避免在方法内部处理异常,或者将异常抛出给上层调用者,让上层调用者处理异常。
3.2.2.3.4 嵌套事务

预期场景:

  • 外层事务不因内层事务的异常而回滚。

失效原因:

  • Spring 的事务管理不支持嵌套事务。当一个方法开始一个新的事务时,如果该方法内部再次调用另一个方法,则该方法将不会创建一个新的事务,而是将使用外层方法的事务。

解决方案:

  • 使用 Propagation.REQUIRES_NEW:在内层方法上使用 Propagation.REQUIRES_NEW
    传播行为,将创建一个新的事务,而不管外层方法是否已经创建了事务。这样,内层事务与外层事务相互独立,内层事务的异常不会影响外层事务。
  • 使用 try-catch:在外层方法调用内层方法时,在外层方法中捕获异常,并在异常处理中回滚事务。
3.2.2.4 总结

基于 Spring 的本地事务管理_第3张图片

你可能感兴趣的:(java,数据库)