Spring声明式事务
所有的事物都由PlatformTransactionManager
负责管理。事务由TransactionStatus
负责表示。之所以抽象出这两个玩意儿,是因为JavaEE除了提供JDBC事务外,还支持分布式事务JTA(Java Transation API)。然而分布式事务非常慢,所以使用率不高。Spring为了同时支持JDBC和JTA两种事物类型,就抽象出了上面的两个玩意儿。
Spring事务注解
注解 | 翻译 | 作用 |
---|---|---|
@EnableTransactionManagement | 启动事务管理 | 用在启动类AppConfig 上,启用声明式事物,其原理仍然是AOP代理,即通过自动创建Bean的Proxy实现,因此,启动类上添加了这个注解后,不需要再添加@EnableAspectJAutoProxy |
@Transactional | 交易的,事务的 | 用在需要事务支持的方法上或者Bean的class 处,表示所有public 方法都具有事务支持。需要回滚事务,只需要抛出RuntimeException 。更多内容本小节**@Transactional注解参数**。 |
@Transactional注解参数
-
如果要针对Checked Exception回滚事务,需要在
@Transactional
注解中写出来,如IOException.class
,使用方法形如@Transactional(rollbackFor = {RuntimeException.class, IOException.class}) public buyProducts(long productId, int num) throws IOException { ... }
上述代码表示在抛出
RuntimeException
或IOException
时,事务将回滚。为了简化代码,我们强烈建议业务异常体系从RuntimeException
派生,这样就不必声明任何特殊异常即可让Spring的声明式事务正常工作:public class BusinessException extends RuntimeException { ... } public class LoginException extends BusinessException { ... } public class PaymentException extends BusinessException { ... }
-
定义事务的传播级别
@Transactional(propagation = Propagation.REQUIRES_NEW) public Product createProduct() { ... }
Spring事务传播级别
级别 | 翻译 | 作用 |
---|---|---|
REQUIRED | 要求;要求具备;规定 | 默认传播级别**(满足大部分需求)** |
SUPPORTS | 支持 | 表示如果有事务,就加入到当前事务,如果没有,那也不开启事务执行。这种传播级别可用于查询方法,因为SELECT语句既可以在事务内执行,也可以不需要事务;(少数情况用到) |
MANDATORY | 强制的;法定的 | 表示必须要存在当前事务并加入执行,否则将抛出异常。这种传播级别可用于核心更新逻辑,比如用户余额变更,它总是被其他事务方法调用,不能直接由非事务方法调用; |
REQUIRES_NEW | 需要;要求具备_新 | 表示不管当前有没有事务,都必须开启一个新的事务执行。如果当前已经有事务,那么当前事务会挂起,等新事务完成后,再恢复执行;(少数情况用到) |
NOT_SUPPORTED | 不支持的 | 表示不支持事务,如果当前有事务,那么当前事务会挂起,等这个方法执行完成后,再恢复执行; |
NEVER | 从不;绝不 | 和NOT_SUPPORTED 相比,它不但不支持事务,而且在监测到当前有事务时,会抛出异常拒绝执行; |
NESTED | 相互套叠 | 表示如果当前有事务,则开启一个嵌套级别事务,如果当前没有事务,则开启一个新事务。 |
上面加黑的部分会用到,其它的基本不会用到,因为把事务搞的越复杂,不仅逻辑跟着复杂,而且速度也会越慢。
Spring如何传播事务
Spring使用声明式事务,最终也是通过执行JDBC事务来实现功能的,那么,一个事务方法,如何获知当前是否存在事务?
答案是使用ThreadLocal。Spring总是把JDBC相关的Connection
和TransactionStatus
实例绑定到ThreadLocal
。如果一个事务方法从ThreadLocal
未取到事务,那么它会打开一个新的JDBC连接,同时开启一个新的事务,否则,它就直接使用从ThreadLocal
获取的JDBC连接以及TransactionStatus
。
因此,事务能正确传播的前提是,方法调用是在一个线程内才行。如果像下面这样写:
@Transactional
public User register(String email, String password, String name) { // BEGIN TX-A
User user = jdbcTemplate.insert("...");
new Thread(() -> {
// BEGIN TX-B:
bonusService.addBonus(user.id, 100);
// END TX-B
}).start();
} // END TX-A
在另一个线程中调用BonusService.addBonus()
,它根本获取不到当前事务,因此,UserService.register()
和BonusService.addBonus()
两个方法,将分别开启两个完全独立的事务。
换句话说,事务只能在当前线程传播,无法跨线程传播。
那如果我们想实现跨线程传播事务呢?原理很简单,就是要想办法把当前线程绑定到ThreadLocal
的Connection
和TransactionStatus
实例传递给新线程,但实现起来非常复杂,根据异常回滚更加复杂,不推荐自己去实现。
小结
Spring提供的声明式事务极大地方便了在数据库中使用事务,正确使用声明式事务的关键在于确定好事务边界,理解事务传播级别。