Spring 事务

发布时间 2023-10-19 15:52:58作者: LARRY1024

Spring 事务

Spring 本身并不实现事务,Spring 事务的本质还是底层数据库对事务的支持,没有数据库事务的支持,Spring 事务就不会生效。

Spring 事务 提供一套抽象的事务管理,并且结合 Spring IOC 和 Spring AOP ,简化了应用程序使用数据库事务,通过声明式事务,可以做到对应用程序无侵入的实现事务功能。

以 JDBC 操作数据,使用事务的步骤为:

  • 获取链接

    Connection conn = DriverManager.getConnection(URL, USER, PASSWD);

  • 开启事务

    conn.setAutoCommit(false);

  • 执行 CRUD 语句

  • 提交或回滚事务

    conn.commit(); 或者 conn.rollback();

  • 关闭链接

    conn.close();

使用 Spring 事务之后,就只需要关注第 3 步的实现即可,其他的步骤,都是由 Spring 完成。Spring 事务的本质其实就是数据库对事务的支持,Spring 只提供统一事务管理接口,具体实现都是由各数据库自己实现。

Spring 支持两种事务方式,分别是编程式事务和声明式事务。

  • 编程式事务:在代码中硬编码(不推荐使用),通过 TransactionTemplate 或者 TransactionManager 手动管理事务,实际应用中很少使用,但是对于我们理解 Spring 事务管理原理有帮助。

  • 声明式事务:在 XML 配置文件中配置或者直接基于注解(推荐使用),实际是通过 AOP 实现(基于@Transactional 的全注解方式使用最多)

编程式事务管理

【示例】使用 TransactionTemplate 来管理事务:

class TestTransaction {
    @Autowired
    private TransactionTemplate transactionTemplate;

    public void testTransaction() {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                try {
                    ... //  业务代码
                } catch (Exception e) {
                    // 回滚
                    transactionStatus.setRollbackOnly();
                }
            }
        });
    }
}

【示例】使用 TransactionManager 来管理事务:

class TestTransaction {
    @Autowired
    private PlatformTransactionManager transactionManager;

    public void testTransaction() {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            ... // 业务代码
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
        }
    }
}

在编程式事务中,必须在每个业务操作中包含额外的事务管理代码,就导致代码看起来非常的臃肿,但对理解 Spring 的事务管理模型非常有帮助。

声明式事务管理

声明式事务管理建立在 AOP 之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。显然声明式事务管理要优于编程式事务管理,这正是 Spring 倡导的非侵入式的编程方式。

唯一不足的地方就是声明式事务管理的粒度是方法级别,而编程式事务管理是可以到代码块的,但是可以通过提取方法的方式完成声明式事务管理的配置。

事务管理模型

事务管理器:TransactionManager

Spring 将事务管理的核心抽象为一个事务管理器(TransactionManager),它的源码只有一个简单的接口定义,属于一个标记接口:

public interface TransactionManager {
}

TransactionManager 有两个子接口,分别是:编程式事务接口 ReactiveTransactionManager 和声明式事务接口 PlatformTransactionManager。

声明式事务接口:PlatformTransactionManager

PlatformTransactionManager 接口定义了 3 个接口方法:

interface PlatformTransactionManager extends TransactionManager {
    // 根据事务定义获取事务状态
    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
    // 提交事务
    void commit(TransactionStatus status) throws TransactionException;
    // 事务回滚
    void rollback(TransactionStatus status) throws TransactionException;
}

通过 PlatformTransactionManager 这个接口,Spring 为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager) 等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

事务定义

事务定义接口 TransactionDefinition,的源码如下:

public interface TransactionDefinition {
    // 事务的传播行为
    default int getPropagationBehavior() {
        return PROPAGATION_REQUIRED;
    }
    // 事务的隔离级别
    default int getIsolationLevel() {
        return ISOLATION_DEFAULT;
    }
    // 事务超时时间
    default int getTimeout() {
        return TIMEOUT_DEFAULT;
    }
    // 事务是否只读
    default boolean isReadOnly() {
        return false;
    }
}

事务的传播机制

TransactionDefinition 一共定义了 7 种事务传播行为

PROPAGATION_REQUIRED

这也是 @Transactional 默认的事务传播行为,指的是:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。更确切地意思是:

  • 如果外部方法没有开启事务,Propagation.REQUIRED 修饰的内部方法会开启自己的事务,且开启的事务相互独立,互不干扰。

  • 如果外部方法开启事务并且是 Propagation.REQUIRED 的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务都需要回滚。

Class A {
    @Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
    public void aMethod {
        // do something
        B b = new B();
        b.bMethod();
    }
}

Class B {
    @Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
    public void bMethod {
       // do something
    }
}

aMethod 调用了 bMethod,只要其中一个方法回滚,整个事务均回滚。

PROPAGATION_REQUIRES_NEW

PROPAGATION_REQUIRES_NEW 会创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法都会开启自己的事务,且开启的事务与外部的事务相互独立,互不干扰。

Class A {
    @Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
    public void aMethod {
        //do something
        B b = new B();
        b.bMethod();
    }
}

Class B {
    @Transactional(propagation=Propagation.REQUIRES_NEW)
    public void bMethod {
       //do something
    }
}

如果 aMethod() 发生异常回滚,bMethod() 不会跟着回滚,因为 bMethod() 开启了独立的事务。但是,如果 bMethod() 抛出了未被捕获的异常并且这个异常满足事务回滚规则,aMethod() 同样也会回滚。

PROPAGATION_NESTED

如果当前存在事务,就在当前事务内执行;否则,就执行与 PROPAGATION_REQUIRED 类似的操作。

PROPAGATION_MANDATORY

如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

PROPAGATION_SUPPORTS

如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

PROPAGATION_NOT_SUPPORTED

以非事务方式运行,如果当前存在事务,则把当前事务挂起。

PROPAGATION_NEVER

以非事务方式运行,如果当前存在事务,则抛出异常。

事务隔离级别

TransactionDefinition 中一共定义了 5 种事务隔离级别:

  • ISOLATION_DEFAULT:使用数据库默认的隔离级别,MySql 默认采用的是 REPEATABLE_READ,也就是可重复读。

  • ISOLATION_READ_UNCOMMITTED:最低的隔离级别,可能会出现脏读、幻读或者不可重复读。

  • ISOLATION_READ_COMMITTED:允许读取并发事务提交的数据,可以防止脏读,但幻读和不可重复读仍然有可能发生。

  • ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被自身事务所修改的,可以阻止脏读和不可重复读,但幻读仍有可能发生。

  • ISOLATION_SERIALIZABLE:最高的隔离级别,虽然可以阻止脏读、幻读和不可重复读,但会严重影响程序性能。

通常情况下,我们采用默认的隔离级别 ISOLATION_DEFAULT 就可以了,也就是交给数据库来决定,可以通过 SELECT @@transaction_isolation; 命令来查看 MySql 的默认隔离级别,结果为 REPEATABLE-READ,也就是可重复读。

事务的只读属性

如果一个事务只是对数据库执行读操作,那么该数据库就可以利用事务的只读属性,采取优化措施,适用于多条数据库查询操作中。

MySQL 默认对每一个连接都启用了 autocommit 模式,在该模式下,每一个发送到 MySQL 服务器的 SQL 语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务。

当我们给方法加上了 @Transactional 注解,那这个方法中所有的 SQL 都会放在一个事务里。否则,每条 SQL 都会单独开启一个事务,中间被其他事务修改了数据,都会实时读取到。有些情况下,当一次执行多条查询语句时,需要保证数据一致性时,就需要启用事务支持。否则上一条 SQL 查询后,被其他用户改变了数据,那么下一个 SQL 查询可能就会出现不一致的状态。

事务的回滚策略

默认情况下,事务只在出现运行时异常(Runtime Exception)时回滚,以及 Error,出现检查异常(checked exception,需要主动捕获处理或者向上抛出)时不回滚。

如果我们想要回滚特定的异常类型的话,可以这样设置:

@Transactional(rollbackFor= MyException.class)

Spring Boot 对事务的支持

@Transactional 的作用范围

  • :表明类中所有 public 方法都启用事务。

  • 方法:最常用的一种。

  • 接口:不推荐使用。

注意事项

  • 要在 public 方法上使用 @Transactional ,在AbstractFallbackTransactionAttributeSource 类的 computeTransactionAttribute 方法中有个判断,如果目标方法不是 public,则 TransactionAttribute 返回null,即不支持事务。

  • 避免同一个类中调用带有 @Transactional 注解的方法,这样会导致事务失效。

事务失效场景

image

访问权限问题

如下代码中,add 方法的访问权限被定义成了 private,这样会导致事务失效,spring要求被代理方法必须是public的。

【错误示例】

@Service
public class UserService {
    @Transactional
    private void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }
}

总结:如果我们自定义的事务方法(即目标方法),它的访问权限不是 public,而是 private、default 或 protected的 话,spring 则不会提供事务功能。

方法用 final 修饰

有时候,某个方法不想被子类重新,这时可以将该方法定义成 final 的。普通方法这样定义是没问题的,但如果将事务方法定义成 final 会导致事务失效。

【错误示例】

@Service
public class UserService {
    @Transactional
    private final void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }
}

spring 事务底层使用了 aop,也就是通过 jdk 动态代理或者 cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用 final 修饰了,那么在它的代理类中,就无法重写该方法,而导致无法支持事务功能。

注意,如果某个方法是 static 的,同样无法通过动态代理,变成事务方法。

方法内部调用

有时候我们需要在同一个类中的方法中,调用另外一个事务方法,这样也会导致事务失效。

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    @Transactional
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }

    @Transactional
    public void updateStatus(UserModel userModel) {
        doSameThing();
    }
}

我们看到在事务方法 add 中,直接调用事务方法 updateStatus。updateStatus 方法拥有事务的能力是因为 spring aop 生成代理了对象,但是这种方法直接调用了 this 对象的方法,所以 updateStatus 方法不会生成事务。

由此可见,在同一个类中的方法直接内部调用,会导致事务失效。

解决思路

新加一个Service方法

第一种思路,只需要新加一个Service方法,把 @Transactional 注解加到新 Service 方法上,把需要事务执行的代码移到新方法中。

@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceB serviceB;

   public void save(User user) {
         queryData1();
         queryData2();
         serviceB.doSave(user);
   }
}

@Servcie
public class ServiceB {
    @Transactional(rollbackFor=Exception.class)
    public void doSave(User user) {
       addData1();
       updateData2();
    }

}

在该Service类中注入自己

如果不想再新加一个 Service 类,在该 Service 类中注入自己也是一种选择。

具体代码如下:

@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceA serviceA;

   public void save(User user) {
         queryData1();
         queryData2();
         serviceA.doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
}

未被 spring 管理

使用 spring 事务的前提是:对象要被 spring 管理,需要创建 bean 实例。

通常情况下,我们通过 @Controller@Service@Component@Repository 等注解,可以自动实现 bean 实例化和依赖注入的功能。

如果对象没有被 spring 管理,例如,Service 类没有加 @Service 注解,事务也不会生效,例如:

//@Service
public class UserService {

    @Transactional
    public void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }
}

多线程调用

【错误示例】

@Slf4j
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {
    @Transactional
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

从上面的例子中,我们可以看到事务方法 add 中,调用了事务方法 doOtherThing,但是事务方法 doOtherThing 是在另外一个线程中调用的。

这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想 doOtherThing 方法中抛了异常,add 方法也回滚是不可能的。

spring 的事务是通过数据库连接来实现的,当前线程中保存了一个 map,key 是数据源,value 是数据库连接。

private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");

对于同一个事务,一定是同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程下,获取的数据库连接肯定是不一样的,所以是不同的事务。

数据库引擎不支持事务

在 mysql 5 之前,默认的数据库引擎是 MyISAM。它的好处:索引文件和数据文件是分开存储的,对于查多写少的单表操作,性能比 innodb 更好。

MyISAM 引擎不支持事务,如果操作的表是基于不支持事务的引擎,也会导致事务注解失效。

未开启事务

springboot 默认会通过 DataSourceTransactionManagerAutoConfiguration 类,默认开启事务支持。

如果是传统的 spring 项目,需要配置事务管理器,以开启事务。

<!-- 配置事务管理器 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>
<tx:advice id="advice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>
<!-- 用切点把事务切进去 -->
<aop:config>
    <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/>
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>

事务不回滚

错误的传播特性

我们在使用 @Transactional 注解时,是可以通过 propagation 参数指定事务的传播特性,spring 目前支持 7 种传播特性:

  • REQUIRED:如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。

  • SUPPORTS:如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。

  • MANDATORY:如果当前上下文中存在事务,否则抛出异常。

  • REQUIRES_NEW:每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。

  • NOT_SUPPORTED:如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。

  • NEVER:如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。

  • NESTED:如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

【错误示例】

@Service
public class UserService {

    @Transactional(propagation = Propagation.NEVER)
    public void add(UserModel userModel) {
        saveData(userModel);
        updateData(userModel);
    }
}

我们可以看到 add 方法的事务传播特性定义成了 Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。

目前只有这三种传播特性才会创建新事务:REQUIREDREQUIRES_NEWNESTED

在事务中捕获了异常

如果在事务中,通过 try-catch 手动捕获了异常,则会导致不会回滚。

【错误示例】

@Slf4j
@Service
public class UserService {

    @Transactional
    public void add(UserModel userModel) {
        try {
            saveData(userModel);
            updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

如果想要 spring 事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则 spring 认为程序是正常的。

在事务中抛出了不支持的异常

即使在事务中没有手动捕获异常,但如果新抛出的异常不正确,spring 事务也不会回滚。

【错误示例】

@Slf4j
@Service
public class UserService {

    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
             saveData(userModel);
             updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}

因为 spring 事务,默认情况下只会回滚 RuntimeException(运行时异常)和 Error(错误),对于普通的 Exception(非运行时异常),它不会回滚。

自定义了回滚异常

在使用 @Transactional 注解声明事务时,有时我们想自定义回滚的异常,spring 也是支持的。可以通过设置 rollbackFor 参数,来完成这个功能。

【错误示例】

@Slf4j
@Service
public class UserService {

    @Transactional(rollbackFor = BusinessException.class)
    public void add(UserModel userModel) throws Exception {
       saveData(userModel);
       updateData(userModel);
    }
}

如果在执行上面这段代码时,程序抛出了 SqlException、DuplicateKeyException 等异常时,事务就不会回滚。因为上述代码只会对 BusinessException 异常回滚,当报错的异常不属于 BusinessException 时,事务就不会回滚。

虽然 rollbackFor 参数有默认值,但是实际使用时,最好重新指定该参数。

因为如果使用默认值,一旦程序抛出了 Exception,事务就不会回滚。因此,建议一般情况下,将该参数设置成:Exception 或 Throwable。

嵌套事务回滚过多

【错误示例】

public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        roleService.doOtherThing();
    }
}

@Service
public class RoleService {

    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

这种情况使用了嵌套的内部事务,原本是希望调用 roleService.doOtherThing 方法时,如果出现了异常,只回滚 doOtherThing 方法里的内容,不回滚 userMapper.insertUser 里的内容,即回滚保存点。

而实际上,insertUser也会被回滚。

因为 doOtherThing 方法出现了异常,没有手动捕获,会继续往上抛,到外层 add 方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。

解决办法

可以将内部嵌套事务放在 try / catch 中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。

【示例代码】

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        try {
            roleService.doOtherThing();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

参考: