我前段时间在写代码的时候,常常考虑并发问题,对事物的安全性、隔离级别须要更深的了解,因此翻看了网上绝大部分关于事务的文章。可是看了以后仍是有些疑惑,例如事务的四种隔离级别,虽然有些文章举出了生动的例子,但并无提到编程中的如何选择使用。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注解,这个字段就表示版本号,每次修改版本号就会递增。当前线程持久化时若是检测到版本号变化,即有其余线程修改过该记录,将会抛异常,咱们能够在抛异常的时候作一些其余措施,或者什么都不作。
我对事物隔离级别的见解,还有解决并发问题的思路可能比较浅薄,但仍是但愿这篇文章能帮到你们,同时欢迎留言探讨共同进步!