Spring 事务
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
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
注解的方法,这样会导致事务失效。
事务失效场景
访问权限问题
如下代码中,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:如果当前上下文中存在事务,则嵌套事务执行;如果不存在事务,则新建事务。
总结起来,区别如下:
传播类型 | 当前上下文有事务 | 当前上下文无事务 | |
---|---|---|---|
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,这种类型的传播特性不支持事务,如果有事务则会抛异常。
目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。
在事务中捕获了异常
如果在事务中,通过 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);
}
}
}
参考: