一、前言
事务一直以来是一个玄之又玄的东西,非常难以理解。难以理解倒不是因为事务本身有多难,而是事务这个概念被各种刻意包装,以至于让人晕头转向,摸不着头脑。例如各种抽象的概念,一致性、持久性、原子性、持久性、读未提交、读已提交、可重复读、序列化,Spring也抽象了事务的传播属性,数据库本身又有各种锁,于是我们就晕头了。今天我们就来扒一扒事务,看看事务华丽外衣下的本质。本篇博客结合沈询的分享,加上自己的一些总结。
二、事务要解的最终问题——“一致性”
一致性是一个名词,其实这里实际上应该当成一个动词来理解,多个参与者达成一致,达成了共识,相互之间不扯皮。这样说可能比较抽象,举个生活中的例子。
一天,老王和老王老婆,同时去银行取钱,老王查了银行卡账号有1w块钱,于是取了出来,假设老王老婆和老王同时查询,也看到有1w块钱,于是点击取款,最后发现没钱可取了,于是要扯皮了,这就不一致了。
用一句通俗的话来描述事务的一致性:所有事务的参与方,眼所见,便是心所得。上面的例子中,老王老婆看到了卡上有1w,却没法取,于是就要扯皮了。这就是严苛的一致性所定义的内容。
下面我们来具体分析一下上面的例子。
首先引出事务单位的概念,事务单元就是完成一个具体的业务的最小单元,上面的例子中包含两个事务单元。按照正常的时间线,要想不扯皮,应该看到如下执行顺序。
但是扯皮的事情发生了,两个事务单元并行了,于是出现如下的执行顺序。事务单元二左移,与事务单元一并行。
上面的场景似曾相识,其实就是和线程安全所描述的内容一摸一样,《一篇文章看懂Java并发和线程安全》,要想不扯皮,必须让访问的共享资源互斥,用Java代码可以描述成如下的代码:
public class Consistency {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
int balance = query();// 查询余额
if (balance > 0) {
drawingOutCash();// 取出现金
}
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
要想强一致,所有的事务单元串行着执行,这就是事务隔离级别中的SERIALIZABLE,于是就引出了事务的隔离级别。
三、事务的隔离级别
强一致,必须让所有事物单元串行执行,这便是隔离级别中的SERIALIZABLE(序列化),但是这样系统的性能是可想而知的,几乎不可用,于是需要放宽对锁的要求,所以出现了其他的隔离级别。事务的隔离级别是以性能为由对一致性的破坏,它的出现是为了破坏一致性,而不是维持一致性。
事务单元与事务单元的关系只有四种:读读、读写、写读、写写
SERIALIZABLE(序列化)
要想进一步提升性能,于是出现了读写锁,这里就出现了两种隔离级别:REPEATABLE_READ(可重复读)和READ_COMMITED(读已提交)
REPEATABLE_READ(可重复读):
读锁不能被升级为写锁,那么对共享资源的写,就进不来,这样“读读”是可并行的,这样会出现幻读,因为在这个级别,表是不会被看做是共享资源的,所以可以insert
READ_COMMITED(读已提交):
读锁可以被升级为写锁,那么当对共享资源正在读时,可以被写请求升级为写锁,那么这样“读读”、“读写”可以并行,于是出现了幻读、不可重复读等等现象
READ_UNCOMMITTED
只加写锁,读不用申请锁,这样“读读”、“读写”、“写读”都可以并行,但“写写”还不能并行,于是所有的写都是串行,于是就有了脏读、不可重复读、幻读等等。
这就是事务隔离级别的真相,事务隔离级别越低,并行度越好,一致性越低。
四、一句话总结
事务的一致性和线程安全所面对的问题一模一样,要想维持一致性,需要保证两点:共享变量的可见性、临界区代码访问的顺序性。
快乐源于分享。
此博客乃作者原创, 转载请注明出处