事务隔离级别新见解!

前言

我前段时间在写代码的时候,常常考虑并发问题,对事物的安全性、隔离级别须要更深的了解,因此翻看了网上绝大部分关于事务的文章。可是看了以后仍是有些疑惑,例如事务的四种隔离级别,虽然有些文章举出了生动的例子,但并无提到编程中的如何选择使用。java

大部分介绍事务的文章,都是介绍什么事务隔离级别的、各类锁的概念,好像举得概念越多,就显示做者了知识更丰富同样,然而并无实际编程的例子,就像英文教科书般将本该实际运用的东西变成一种学术,就算看懂讲是什么东西也没办法使用。这种教科书式、百科式的文章贻害不浅,所以我才写这篇文章。sql

事务隔离级别

简单来讲就是要么一块儿过,要么所有取消,保证数据的完整性,在普通状况下,事就是这么简单,提交回滚罢了。然而遇到并发问题,数据安全问题,这点了解是不够的。
数据库

有4种隔离级别,为何是4种而没有5种、6种?多是研究数据库的鼻祖们总结最后得出来的吧。那么下面我要先引用网上绝大部分关于这4种级别的介绍,如下为网上摘抄。编程

在介绍4种事隔离级别前,须要三个概念:安全

1. 脏读:一个事务读取到另外一个事务还没有提交的数据。并发

2. 不可重复读:同一事务,两次读取同一数据,获得不一样的结果。测试

3. 幻读:同一事务,用相同的条件读取两次,获得的结果集数据条数不一样(数据条数多了或者少了)。spa

而后为了解决这些个问题,数据库有了4种隔离级别:线程

1. TRANSACTION_READ_UNCOMMITTED:防止更新丢失,容许脏读、不可重复读、幻读。事务

2. TRANSACTION_READ_COMMITTED:防止脏读,容许不可重复读、幻读,这也是多数数据库默认的隔离级别。

3. TRANSACTION_REPEATABLE_READ:防止脏读、不可重复读,容许幻读。

4. TRANSACTION_SERIALIZABLE:防止脏读、不可重复读,幻读。(事务彻底串行化执行,事务一个一个按顺序依次执行,能够不会产生并发问题。)

还附带了一张表:


丢失更新 脏读
不可重复读 幻读
未提交读
N Y Y Y
已提交读
N N Y Y
可重复读
N N N Y
串行化
N N N N

还有一些文章对隔离级别的理解:“级别越高越能保证数据完整性一致性”,“一个级别解决一个问题”,有的还画出隔离级别与并发的关系坐标图。。。

更合理的级别划分

网上几乎全部文章都是这么写,其实有些数据库官方的文档也是这么解释的。这么解释从某个角度是正确的,可是我就有些疑问了,“READ_UNCOMMITTED”这个隔离级别不就是废物了么?为何Oracle和SQLservice的默认隔离级别是READ_COMMITTED,Mysql的默认隔离级别是REPEATABLE_READ,Oracle这种商业的数据库默认隔离级别安全性比开源的数据库还要低?java项目使用的Hibernate为的是跨数据库,能从Oracle和Mysql之间切换,那么切换后默认事隔离级别变了不会影响安全性么?解决幻读必定要使用SERIALIZABLE么,怎么我看不少项目基本上都没用这个呢?等等各类疑问。

经过结合实际案例研究,我认为隔离级别应该这么划分:

REPEATABLE_READ < READ_COMMITTED < READ_UNCOMMITTED < SERIALIZABLE

并且,我要说的是我这种隔离级别的划分才是合理的!可能你从没见过有人这么定义隔离级别,并且官方都不这么定义的!我只用事实说话,下面我结合实际编程的例子,分析为何隔离级别为何按我那样划分才合理。

举个简单的例子:注册。

咱们在作注册这个功能的时候,一般要进行这么个步骤:

在这个需求下,是不能简单说使用哪一种隔离级别好的,这是分开几个步骤,并非一瞬间,有可能在这几个步骤之间,其余操做了数据库,因此必须结合时间顺序!

场景1:

假如使用REPEATABLE_READ ,例如Mysql,它在实现该隔离级别是用MVCC,即读取记录的时候过滤时间戳,即便在当前线程的过程当中其余对数据库进行增长、修改或删除,也是看不到的,达到了数据的一致性。然而这个”一致性“在这个场景并无好处,由于若线程1和线程2都注册同一个用户名,线程2在时间1的时候提交了数据线程1看不到,会认为该用户名还没注册,容许用户注册,当插入数据库的时候,因为unique约束报错了。

在这种状况下,使用READ_COMMITTED ,能看到其余提交了的数据,明显更具备准确的检验结果!

场景2:

线程2的提交时间变成了时间3,此时READ_COMMITTED 就没什么做用了,这种场景下下READ_UNCOMMITTED 就发挥做用了。READ_UNCOMMITTED 实际上是一个更增强大的隔离级别,连其余未提交的东西都能看到,尤为在一些简单的逻辑上,插入数据库紧接着下一步就是提交无误,这种状况下是比较适合使用READ_UNCOMMITTED 这个隔离级别的。

通过我在Mysql测试当隔离级别为READ_UNCOMMITTED 的时候,同时具备READ_COMMITTED 和READ_UNCOMMITTED 的能力,即能看到其余线程提交了的数据和未提交的数据!

场景3:

此场景中两个事物在检查用户名是否存在的时候,数据库确实没有记录,连未提交的也没有,它们在插入数据库的时候仅仅相差了一步!在Mysql中的测试状况下是,线程1和2都会进行插入记录这个动做,可是因为有惟一约束,线程1较晚插入会受到阻塞而不是报错,由于它要看另一个线程是打算提交仍是回滚,若是回滚,线程1将继续执行,若是线程2提交了,线程1报错。

这种状况除了使用SERIALIZABLE隔离级别,其余隔离级别都不能防止另外一个线程报错,然而SERIALIZABLE隔离级别是彻底阻塞,如该场景线程2先开启,线程1连检查用户名是否存在这一步都会阻塞,但其余查询业务也同样受到阻塞,从效率来讲来是很是很差的。

举这个例子,是想借此介绍下各个隔离级别在同一个场景下的表现,而不是探讨怎么解决该例子的问题。我举的这个例子,“用户名”有惟一约束,这已是保证数据正确性的终极防线了,检查用户名是否存在的做用,只是在页面注册的时候用Ajax提早检查,增长用户体验而已,即便没有数据的安全性也是能达到的,因此我以为这个功能使用REPEATABLE_READ、READ_COMMITTED或READ_UNCOMMITTED都是能够的

并发问题

了解了隔离级别的真正意义后,咱们要探讨下并发问题,由于事物隔离级别也是由于并发而产生的。上面我举了一个注册的例子,具备并发问题,可是问题并不严重,由于“用户名”这个字段有unique约束,即便不作插入前的校验步骤,也不会致使出现两个相同的用户名这种错误。

那么如今来讨论一个没有约束的例子,没有约束的话,就必须靠解决并发问题来保证数据安全性。

例子:买票

咱们在作这个功能的时候,须要进行这么个步骤:

因为数据库没有正整数类型,没有约束来防止数据出错,若是出现注册那个例子中的场景3,是会致使余票变为负数。

那么,其余三个事物隔离级别都没有100%的做用,是否是得出杀手锏SERIALIZABLE了?答案是否的,由于使用SERIALIZABLE隔离级别会彻底阻塞其余事物,会致使当有一我的进行购票的时候,其余对该涉及的表的查询都要等待,这种状况怎么能忍?除非只有该对涉及的表有操做,但这种状况几乎是不可能的,即便有也不能保证未来业务扩充。

解决办法1:利用synchronized让方法同步执行,是解决这个并发场景的常见办法。

解决办法2:Hibernate乐观锁,也就是@Version注解。在存放着“余票”字段的表中,增长一个int型字段,同时使用@Version注解,这个字段就表示版本号,每次修改版本号就会递增。当前线程持久化时若是检测到版本号变化,即有其余线程修改过该记录,将会抛异常,咱们能够在抛异常的时候作一些其余措施,或者什么都不作。

结语

我对事物隔离级别的见解,还有解决并发问题的思路可能比较浅薄,但仍是但愿这篇文章能帮到你们,同时欢迎留言探讨共同进步!

相关文章
相关标签/搜索