隔离级别
数据库事务的知识
数据库事务具有以下4 个基本特征, 也就是著名的ACID 。
- Atomic (原子性):事务中包含的操作被看作一个整体的业务单元, 这个业务单元中的操作要么全部成功,要么全部失败,不会出现部分失败、部分成功的场景。
- Consistency (一致性):事务在完成时,必须使所有的数据都保持一致状态,在数据库中所有的修改都基于事务,保证了数据的完整性。
- Isolation (隔离性): 这是我们讨论的核心内容,正如上述,可能多个应用程序线程同时访问同一数据,这样数据库同样的数据就会在各个不同的事务中被访问,这样会产生丢失更新。为了压制丢失更新的产生,数据库定义了隔离级别的概念,通过它的选择,可以在不同程度上压制丢失更新的发生。因为互联网的应用常常面对高并发的场景,所以隔离性是需要掌握的重点内容。
- Durability (持久性):事务结束后,所有的数据会固化到一个地方,如保存到磁盘当中,即使断电重启后也可以提供给应用程序访问。
详解隔离级别
- 未提交读
未提交读( read uncommitted )是最低的隔离级别,其含义是允许一个事务读取另外一个事务没有提交的数据。未提交读是一种危险的隔离级别,所以一般在我们实际的开发中应用不广, 但是它的优点在于并发能力高,适合那些对数据一致性没有要求而追求高并发的场景,它的最大坏处是出现脏读。
- 读写提交
读写提交( read committed )隔离级别, 是指一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据。
在T3 时刻事务2 读取库存的时候, 因为事务l 未提交事务,所以读出的库存为l , 于是事务2认为当前可扣减库存; 在T4 时刻,事务l 己经提交事务,所以在TS 时刻, 它扣减库存的时候就发现库存为0 ,于是就无法扣减库存了。这里的问题在于事务2 之前认为可以扣减,而到扣减那一步却发现已经不可以扣减,于是库存对于事务2 而言是一个可变化的值,这样的现象我们称为不可重复读。 - 可重复读
可重复读的目标是克服读写提交中出现的不可重复读的现象,因为在读写提交的时候,可能出现一些值的变化, 影响当前事务的执行,如上述的库存是个变化的值,这个时候数据库提出了可重复读的隔离级别。
- 串行化
串行化( Serializabl巳)是数据库最高的隔离级别,它会要求所有的SQL 都会按照顺序执行,这样就可以克服上述隔离级别出现的各种问题,所以它能够完全保证数据的一致性。
使用合理的隔离级别
通过上面的讲述,读者应该对隔离级别有了更多的认识,使用它能够在不同程度上压制丢失更新,于是可以总结成所示的一张表。
对于隔离级别,不同的数据库的支持也是不一样的。例如, Oracle 只能支持读写提交和串行化,而MySQL 则能够支持4 种,对于Oracle 默认的隔离级别为读写提交, MySQL 则是可重复读,这些需要根据具体数据库来决定。
传播行为
传播行为是方法之间调用事务采取的策略问题。在绝大部分的情况下,我们会认为数据库事务要么全部成功, 要么全部失败。但现实中也许会有特殊的情况。例如,执行一个批量程序,它会处理很多的交易,绝大部分交易是可以顺利完成的,但是也有极少数的交易因为特殊原因不能完成而
发生异常,这时我们不应该因为极少数的交易不能完成而回滚批量任务调用的其他交易,使得那些本能完成的交易也变为不能完成了。此时,我们真实的需求是,在一个批量任务执行的过程中,调用多个交易时,如果有一些交易发生异常,只是回滚那些出现异常的交易,而不是整个批量任务,这样就能够使得那些没有问题的交易可以顺利完成,而有问题的交易则不做任何事情。
传播行为的定义
在Spring 事务机制中对数据库存在7 种传播行为,它是通过枚举类 定义的。下面先来研究它的源码,如代码所示:
public enum Propagation {
/**
* 需要事务,它是默认传播行为,如果当前存在事务,就沿用当前事务,
去否则新建一个事务运行子方法
*/
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),
/**
在当前方法调用子方法时,如果子方法发生异常,
只因滚子方法执行过的SQL ,而不回滚当前方法的事务
*/
NESTED(TransactionDefinition.PROPAGATION_NESTED);
private final int value;
Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
RQUIRED
、REQUIRES_NEW
和NESTED
这3 种最常用的传播行为NESTED
传播行为和RQUIRES_NEW
还是有区别的。NESTED
传播行为会沿用当前事务的隔离级别和锁等特性,而RQUIRES NEW
则可以拥有自己独立的隔离级别和锁等特性, 这是在应用中需要注意的地方。
@Transactional 调用失效问题
@Transactional
在某些场景下会失效,这是要注意的问题,在同一个类调用,Spring
数据库事务的约定, 其实现原理是AOP
, 而AOP
的原理是动态代理, 在自调用的过程中, 是类自身的调用,而不是代理对象去调用, 那么就不会产生AOP
。