Spring事务传播机制
Spring 七种传播机制 | |
---|---|
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED) | 支持当前事务,如果有事务加入当前事务,没有则会创建一个新的事务 |
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS) | 支持当前事务,如果有事务加入当前事务,如果没有事务的话以非事务方式执行 |
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY) | 支持当前事务,当前如果没有事务抛出异常 |
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW) | 不管是否有事务创建一个新的事务并挂起当前事务 |
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED) | 以非事务方式执行,如果当前存在事务则将当前事务挂起 |
NEVER(TransactionDefinition.PROPAGATION_NEVER) | 以非事务方式进行,如果存在事务则抛出异常 |
NESTED(TransactionDefinition.PROPAGATION_NESTED) | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。 |
注意:事务的传播在类内部方法中调用时是失效的
以PROPAGATION_REQUIRED为例子讲一下传播机制中的当前事务和新建事务的概念。
假设有以下几个类
ContollerA
ServiceA,save方法被@Transactional(propagation = Propagation.REQUIRED)修饰
ServiceB,delete方法被@Transactional(propagation = Propagation.REQUIRED)修饰
方法调用链为ControllerA -> ServiceA#save() -> ServiceB#delete()
那么时序图如下图1-1所示。
对于ServiceA的save方法来说,由于Controller调用的时候没有开启任何事务,因此,Spring内部识别到REQUIRED传播机制则会主动开启一个事务。
对于ServiceB的delete方法来说,由于save方法已经开启了一个事务,那么对于delete方法来说当前已经存在了一个事务,那么Spring就不会再为delete方法开启一个事务,而是直接把delete方法加入save方法前开启的事务中。
最后save方法和delete方法执行完之后,Spring则会内部把事务提交上去。
Spring事务失效场景
-
数据库引擎不支持事务,比如MyISAM
-
入口方法不是public,这一点由Spring的AOP特性决定的,理论上而言,不public也能切入,但spring可能是觉得private自己用的方法,应该自己控制,不应该用事务切进去吧)。另外private 方法, final 方法 和 static 方法不能添加事务,加了也不生效
-
Spring事务管理默认只支持运行期异常进行回滚(至于为什么spring要这么设计:因为spring认为Checked的异常属于业务的,coder需要给出解决方案而不应该直接扔该框架)
-
没有启用事务和切面
-
类是否被正确代理
-
业务和事务要在同一个线程,否则事务也不生效,比如
@Transactional @Override public void save(User user1, User user2) { new Thread(() -> { saveError(user1, user2); System.out.println(1 / 0); }).start(); }
-
默认情况下,只有来自外部的方法调用才会被AOP代理捕获,也就是,类内部方法调用本类内部的其他方法并不会引起事务行为,即使被调用方法使用@Transactional注解进行修饰。
Spring 事务四大特性
- 原子性 : 操作要么全部成功,要么全部失败回滚
- 一致性 : 事务执行前和执行后处于一致性状态。例如,转账前A、B共5000元,A、B之间转账后,两者之和仍应该是5000元。
- 隔离性 : 事务之间互不干扰
- 持久性 : 事务一旦提交,数据的改变是永久性的,即使这时候数据库发生故障,数据也不会丢失。
事务隔离级别
-
未提交读(READ UNCOMMITED)
在未提交读级别下,事务中的修改,即使没有提交,对其他事务也是可见的,事务可以读取未提交的数据,这也被称为脏读
-
提交读(READ COMMIT)
一个事务开始时,只能看见已经提交的事务所做的修改,换句话说,一个事务直到提交之前,所做的任何修改对其他事务都是不可见的,这个级别有时候也被称为不可重复读,因为两次执行同样的查询可能会得到不同的结果
-
可重复读 (REPEATABLE READ)
可重复读解决了脏读的问题,该级别保证了统一个事务中多次读取同样记录的结果是一致的。但是理论上,可重复读隔离级别无法解决幻读问题,所谓幻读,指的是当某个事务在读取某个范围内的记录时,会产生幻行,很多DB通过MVCC机制解决这个问题
-
序列化 (SERIALIZABLE)
序列化级别时最高的隔离级别,它通过强制事务串行执行,避免了前面所有的所有问题
产生的问题
- 脏读,在未提交读隔离级别出现
- 不可重复读,在提交读隔离级别出现
- 幻读,可重复读隔离级别通过MVCC解决
Spring 手动开启事务
使用场景,外部类需要调用父类的方法,然后父类会调用子类实现的抽象方法,父类不需要事务,但是子类实现的方法需要事务,因为外部类是调用父类的方法,在子类添加@Transactional注解无法生效,例如
@Scheduled(fixedRate = 1000 * 60 * 3)
public void importPaidOrders() {
// 外部调用
paidOrdersImporter.importOrders(tmb,now, TypeTkStatus.RELATION_PAID);
}
// 父类方法
public void importOrders(String startTime,String endTime, TypeTkStatus tkStatus){
doDealWithOrder(...);
}
// 抽象方法
abstract void doDealWithOrder(
Orders orders,
Orders previousOrder,
Long parentUserId,
Long parentPUserId,
User user,
User oneLevelUser
);
// 子类实现
public void doDealWithOrder(
Orders orders,
Orders previousOrder,
Long parentUserId,
Long parentPUserId,
User user,
User oneLevelUser
){
...
}
这个时候可以手动开启事务,如何手动开启?
// 注入DataSourceTransactionManager
@Autowired
DataSourceTransactionManager dataSourceTransactionManager;
// 注入TransactionDefinition
@Autowired
TransactionDefinition transactionDefinition;
@Override
@Transactional
void doDealWithOrder(Orders orders, Orders previousOrder, Long parentUserId, Long parentPUserId,
User user, User oneLevelUser) {
// 开启事务
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
try {
// business logic
...
// 提交事务
dataSourceTransactionManager.commit(transactionStatus);
}catch (Exception ex){
logger.error("paid orders exception is : {}",ex);
// 回滚事务
dataSourceTransactionManager.rollback(transactionStatus);
}
}