这篇文章以一个问题开始, 如果你知道答案的话就可以跳过不看啦 @(o・ェ・)@
Q: 在一个批量任务执行的过程中, 调用多个子任务时, 如果有一些子任务发生异常, 只是回滚那些出现异常的任务, 而不是整个批量任务, 请问在 Spring 中事务需要如何配置才能实现这一功能呢?
隔离级别
隔离性 (Isolation) 作为事务特性的一个关键特性, 它要求每个读写事务的对象对其他事务的操作对象能相互分离, 即该事务提交前对其他事务都不可见, 在数据库层面都是使用锁来实现.
事务的隔离级别从低到高有以下四种:
READ UNCOMMITTED(未提交读): 这是最低的隔离级别, 其含义是允许一个事务读取另外一个事务没有提交的数据. READ UNCOMMITTED 是一种危险的隔离级别, 在实际开发中基本不会使用, 主要是由于它会带来 脏读 问题.
时间 | 事务 1 | 事务 2 | 备注 |
---|---|---|---|
1 | 读取库存为 100 | --- | --- |
2 | 扣减库层 50 | --- | 剩余 50 |
3 | 扣减库层 50 | --- | |
4 | 提交事务 | 库存保存为 0 | |
5 | 回滚事务 | --- | 事务回滚为 0 |
脏读对于要求数据一致性的应用来说是致命的, 目前主流的数据库的隔离级别都不会设置成 READ UNCOMMITTED. 不过脏读虽然看起来毫无用处, 但是它主要优点是并发能力高, 适合那些对数据一致性没有要求而追求高并发的场景.
READ COMMITTED(读写提交): 它是指一个事务只能读取另外一个事务已经提交的数据, 不能读取未提交的数据. READ COMMITTED 会带来 不可重复读 的问题:
时间 | 事务 1 | 事务 2 | 备注 |
---|---|---|---|
1 | 读取库存为 1 | ||
2 | 扣减库存 | 事务未提交 | |
3 | 读取库存为 1 | ||
4 | 提交事务 | 库存变成 0 | |
5 | 扣减库存 | 库存为 0,无法扣减 |
不可重复读和脏读的区别是: 脏读读取到的是未提交的数据, 而不可重复读读到的确实已经提交的数据, 但是违反了数据库事务一致性的要求.
一般来说, 不可重复读的问题是可以接受的, 因为其读到的是已经提交的数据, 本身并不会带来很大的问题. 因此, 很多数据库如 (ORACLE,SQL SERVER) 将其默认隔离级别设置为 READ COMMITTED, 允许不可重复读的现象.
REPEATABLE READ (可重复读): 可重复读的目标是为了克服 READ COMMITED 中出现的不可重复读, 它指在同一个事务内的查询都是与事务开始时刻一致, 以上表为例, 在 REPEATABLE READ 隔离级别下它会发生如下变化:
时间 | 事务 1 | 事务 2 | 备注 |
---|---|---|---|
1 | 读取库存为 1 | ||
2 | 扣减库存 | 事务未提交 | |
3 | 读取库存 | 不允许读取,等待事务 1 提交
| |
4 | 提交事务 | 库存变成 0 | |
5 | 读取库存 | 库存为 0,无法扣减 |
REPEATABLE READ 虽然解决了不可重复读问题, 但是他又会带来 幻读 问题, 幻读是指, 在一个事务中, 第一次查询某条记录, 发现没有, 但是, 当试图更新这条不存在的记录时, 竟然能成功, 并且, 再次读取同一条记录, 它就神奇地出现了.
时间 | 事务 A | 事务 2 | 备注 |
---|---|---|---|
1 | begin | begin | |
2 | 读取 id 为 100 的数据 | 没有数据 | |
3 | 插入 id 为 100 的数据 | ||
4 | 提交事务 | ||
5 | 读取 id 为 100 的数据 | 没有数据 | |
6 | 更新 id 为 100 的数据 | 成功 | |
7 | 读取 id 为 100 的数据 | 读取成功 | |
8 | 提交事务 |
事务 B 在第 2 步第一次读取 id=99 的记录时, 读到的记录为空, 说明不存在 id=99 的记录. 随后, 事务 A 在第 3 步插入了一条 id=99 的记录并提交. 事务 B 在第 5 步再次读取 id=99 的记录时, 读到的记录仍然为空, 但是, 事务 B 在第 6 步试图更新这条不存在的记录时, 竟然成功了, 并且, 事务 B 在第 8 步再次读取 id=99 的记录时, 记录出现了.
SERIALIZABLE(串行化): 数据库最高的隔离级别, 它要求所有的 SQL 都会按照顺序执行, 这样可以克服上述所有隔离出现的各种问题, 能够完全包住数据的一致性.
Spring 中配置隔离级别
在 Spring 项目中配置隔离级别只需要做如下操作
- @Transactional(isolation = Isolation.SERIALIZABLE)
- public int insertUser(User user){
- return userDao.insertUser(user);
- }
上面的代码中我们使用了串行化的隔离级别来包住数据的一致性, 这使它将阻塞其他的事务进行并发, 所以它只能运用在那些低并发而又需要保证数据一致性的场景下.
隔离级别字典:
- DEFAULT(-1), ## 数据库默认级别
- READ_UNCOMMITTED(1),
- READ_COMMITTED(2),
- REPEATABLE_READ(4),
- SERIALIZABLE(8);
传播行为
在 Spring 中, 当一个方法调用另外一个方法时, 可以让事务采取不同的策略工作, 如新建事务或者挂起当前事务等, 这便是事务的传播行为.
定义
在 Spring 的事务机制中对数据库存在 7 种传播行为, 通过枚举类 Propagation 定义.
- public enum Propagation {
- /**
- * 需要事务, 默认传播性行为.
- * 如果当前存在事务, 就沿用当前事务, 否则新建一个事务运行子方法
- */
- REQUIRED(0),
- /**
- * 支持事务, 如果当前存在事务, 就沿用当前事务,
- * 如果不存在, 则继续采用无事务的方式运行子方法
- */
- SUPPORTS(1),
- /**
- * 必须使用事务, 如果当前没有事务, 抛出异常
- * 如果存在当前事务, 就沿用当前事务
- */
- MANDATORY(2),
- /**
- * 无论当前事务是否存在, 都会创建新事务允许方法
- * 这样新事务就可以拥有新的锁和隔离级别等特性, 与当前事务相互独立
- */
- REQUIRES_NEW(3),
- /**
- * 不支持事务, 当前存在事务时, 将挂起事务, 运行方法
- */
- NOT_SUPPORTED(4),
- /**
- * 不支持事务, 如果当前方法存在事务, 将抛出异常, 否则继续使用无事务机制运行
- */
- NEVER(5),
- /**
- * 在当前方法调用子方法时, 如果子方法发生异常
- * 只回滚子方法执行过的 SQL, 而不回滚当前方法的事务
- */
- NESTED(6);
- ......
- }
日常开发中基本只会使用到 REQUIRED(0) , REQUIRES_NEW(3) , NESTED(6) 三种.
NESTED 和 REQUIRES_NEW 是有区别的. NESTED 传播行为会沿用当前事务的隔离级别和锁等特性, 而 REQUIRES_NEW 则可以拥有自己独立的隔离级别和锁等特性.
NESTED 的实现主要依赖于数据库的保存点 (SAVEPOINT) 技术, SAVEPOINT 记录了一个保存点, 可以通过 ROLLBACK TO SAVEPOINT 来回滚到某个保存点. 如果数据库支持保存点技术时就启用保存点技术; 如果不支持就会新建一个事务去执行代码, 也就相当于 REQUIRES_NEW .
Transactional 自调用失效
如果一个类中自身方法的调用, 我们称之为自调用. 如一个订单业务实现类 OrderServiceImpl 中有 methodA 方法调用了自身类的 methodB 方法就是自调用, 如:
- @Transactional
- public void methodA(){
- for (int i = 0; i < 10; i++) {
- methodB();
- }
- }
- @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
- public int methodB(){
- ......
- }
在上面方法中不管 methodB 如何设置隔离级别和传播行为都是不生效的. 即自调用失效.
这主要是由于 @Transactional 的底层实现原理是基于 AOP 实现, 而 AOP 的原理是动态代理, 在自调用的过程中是类自身的调用, 而不是代理对象去调用, 那么就不会产生 AOP, 于是就发生了自调用失败的现象.
要克服这个问题, 有 2 种方法:
编写两个 Service, 用一个 Service 的 methodA 去调用另外一个 Service 的 methodB 方法, 这样就是代理对象的调用, 不会有问题;
在同一个 Service 中, methodA 不直接调用 methodB, 而是先从 Spring IoC 容器中重新获取代理对象 `OrderServiceImpl., 获取到后再去调用 methodB. 说起来有点乱, 还是 show you the code.
- public class OrderServiceImpl implements OrderService,ApplicationContextAware {
- private ApplicationContext applicationContext = null;
- @Override
- public void setApplicationContext(ApplicationContext applicationContext) {
- this.applicationContext = applicationContext;
- }
- @Transactional
- public void methodA(){
- OrderService orderService = applicationContext.getBean(OrderService.class);
- for (int i = 0; i < 10; i++) {
- orderService.methodB();
- }
- }
- @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
- public int methodB(){
- ......
- }
- }
上面代码中我们先实现了 ApplicationContextAware 接口, 然后通过 applicationContext.getBean() 获取了 OrderService 的接口对象. 这个时候获取到的是一个代理对象, 也就能正常使用 AOP 的动态代理了.
回到最开始的那个问题, 看完这篇文章是不是有答案了呢?
来源: http://www.tuicool.com/articles/MBR3QnV