一直以来对数据库的事务隔离机制的理解总是停留在表面,其内容也是看一遍忘一边。这两天决定从原理上理解它,整理成自己的知识。查阅资料的过程中发现好多零碎的概念如果串起来足够写一本书,所以在这里给自己梳理一个脉络,具体的内容参考引文或在网上搜一下。由于平时接触最多的是MySQL,所以文章中某些部分是MySQL特有的特性,请读者注意。
数据库并发操作会引发的问题:
多个事务同时访问数据库时候,会发生下列5类问题,包括3类数据读问题(脏读,不可重复读,幻读),2类数据更新问题(第一类丢失更新,第二类丢失更新):脏读(dirty read):A事务读取B事务尚未提交的更改数据,并在这个数据基础上操作。如果B事务回滚,那么A事务读到的数据根本不是合法的,称为脏读。在oracle中,由于有version控制,不会出现脏读。数据库在并发操作下会出现上述这些问题,要解决它就要想办法在执行可能引发问题的操作之前将该操作阻塞住,让它等到合适的时机再执行。那么如何挑选合适的时机阻塞操作的执行,又如何保证在调度过程执行完成后其执行结果与串行执行操作的结果相同呢?
三级封锁协议
数据库想要在“合适”的时机阻塞住数据库操作,那么首先要定义好怎么样的时机算是“合适”,因为各个系统支持的业务千差万别,对数据的实时性和有效性的要求也不同。于是数据库理论中就提出了封锁级别的概念,对不同的同步要求采用不同的封锁级别。
三级封锁协议内容如下:
一级封锁协议:事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。 一级封锁协议可以防止丢失修改,并保证事务T是可恢复的。使用一级封锁协议可以解决丢失修改问题。在一级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的,它不能保证可重复读和不读“脏”数据。 二级封锁协议:一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,读完后方可释放S锁。 二级封锁协议除防止了丢失修改,还可以进一步防止读“脏”数据。但在二级封锁协议中,由于读完数据后即可释放S锁,所以它不能保证可重复读。 三级封锁协议 :一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。 三级封锁协议除防止了丢失修改和不读“脏”数据外,还进一步防止了不可重复读。事务隔离级别:
三级封锁协议反映在实际的数据库系统上,就是四级事务隔离机制。总的来说,四种事务隔离机制就是在逐渐的限制事务的自由度,以满足对不同并发控制程度的要求。以下就是数据库的四种隔离级别:
Read Uncommitted、Read Committed、Repeatable Read、Serializable
其对各个并发问题的制约强度见下表:
√: 可能出现 ×: 不会出现
脏读 | 不可重复读 | 幻读 | |
Read uncommitted | √ | √ | √ |
Read committed | × | √ | √ |
Repeatable read | × | × | √ |
Serializable | × | × | × |
四种级别对并发问题的解决由弱到强,相应的系统性能由强到弱,MySQL的默认级别是Repeatable Read。
Read Uncommitted
在Read Uncommitted策略下,数据库遵循一级封锁协议,只对修改数据的并发操作做限制。一个事务不能修改其他事务正在修改的数据,但可以读取到其他事务中尚未提交的修改,这些修改如果未被提交,将会成为脏数据。
Read committed
在Read committed策略下,数据库遵循二级封锁协议,只允许读取已经被提交的数据,反过来讲,如果一个事务修改了某行数据且尚未提交,而第二个事务要读取这行数据的话,那么是不允许的。在MySql的InnoDB下,虽然这种操作不被允许,但MySQL不会阻塞住数据的查询操作,而是会查询出数据被修改之前的备份,返回给客户端。MySQL的这种机制称为MVCC(多版本并发控制),就是说数据库在事务并发的过程中对数据维护多个版本,使得不同的事务对不同的数据版本进行读写(MVCC的实现参见引用中的文章)。这样的机制反映在应用中就是,在任何时候对数据库查询总是可以得到数据库中最近提交的数据。为被提交的脏数据被隔离起来,无法被查询到,即防止脏读发生。
Repeat Read
Repeat Read又比Read Committed更加严格一点,但仍然是在二级封锁协议的范畴,只是读取过程受到更多MVCC的影响。在Read Committed下,允许一个事务中多次相同查询得到不同的结果,就是所谓的不可重复读问题。这在一些应用中是允许的,所以oracle、SQL server上默认这一隔离级别,但MySQL没有,它默认Repeat Read级别。在这一级别下,有赖于MVCC,同一个事务中的查询只能查到版本号不高于当前事务版本的数据,即事务只能看到该事务开始前或者被该事物影响的数据。反过来说,这一级别下,不允许事务读取在该事务开始后新提交的数据。即防止了不可重复读的发生。
依靠上面的机制,已经做到了在事务内数据内容的不变,但是不能保证多次查询得到的数据数量一致。因为在一个事务执行的过程中别的事务完全可以执行数据插入,当插入了刚好符合查询条件的数据时,就会引发数据查询结果集增加,引发幻读。还有一种情况就是,如果一个事务想插入一条数据,而另一个事务已经插入了含有相同主键的数据,那么当前事务也会被阻塞,并最终执行失败,虽然当前事务根本无法查询到这一条数据,这也是一种幻读。InnoDB提供的间隙锁机制可以在一定程度上防止幻读的发生,具体介绍见最后一篇引文。
Serializable
最后,最强事务隔离机制Serializable,它遵循三级封锁协议,使得所有的事务必须串行化执行,只要有事务在对表进行查询,那么在此事务提交前,任何其他事务的修改都会被阻塞。这解决了一切并发问题,但会造成大量的等待、阻塞甚至死锁,使系统性能降低。
要注意,在任何一种隔离机制下,都是不允许一个事务删除或修改另一个事务影响过而未提交的数据的。因为事务增、删、改数据以后,会在该行加上排它锁,排它锁会阻塞其他事务再次对该行数据操作。也正是由于排它锁的存在,这四种隔离机制都不会出现任何一种更新丢失的现象,因为一条信息根本不允许第二个事务进行修改。
两段锁协议
数据库在调度并发事务时遵循“两段锁”协议,“两段锁”协议是指所有事务必须分两个阶段对数据项进行加锁和解锁
扩展阶段:在对任何数据项的读、写之前,要申请并获得该数据项的封锁。收缩阶段:每个事务中,所有的封锁请求必须先于解锁请求。在数学上可以证明,遵循两段锁的调度可以保证调度结果与串行化调度相同。这样的机制保证了数据库并发调度与串行调度的等价。
*注:
参考资料:
http://blog.csdn.net/fg2006/article/details/6937413
http://blog.csdn.net/chen77716/article/details/6742128
http://www.bitsCN.com/database/201304/201415.html
http://snailxr.iteye.com/blog/1143615
http://blog.sina.com.cn/s/blog_711b11fd0101bhks.html
http://blog.sina.com.cn/s/blog_499740cb0100ugs7.html