首先直接上答案,数据的隔离级别从低到高依次为:读未提交,读已提交,可重复度,串行化。
然后面试官肯定会接着问你,各种隔离界别有什么问题,还是直接上答案:读未提交会有脏读的问题,读已提交会有不可重复读的问题,可重复度会有幻读的问题,串行化倒是没有这种问题,但效率不高。
这种回答肯定是不行的,你要详细解释解释,显得你很懂!
隔离级别细节
读未提交
当有两个并行的事务,在这种隔离级别下,A事务会读到B事务提交前做的更改,基本相当于两个事务没做隔离。
| 时间点 | A事务操作 | B事务操作 |
|---|---|---|
| T0 | begin | begin |
| T1 | select price where goods_id=1ret:1 | |
| T2 | update price= 2 where goods_id=1 | |
| T3 | select price where goods_id=1ret:2 | |
| T4 | rollback |
如上述表格,T2时刻B事务做了一个更改,T3时刻A事务就读到了这个更改,T4时刻B事务回滚,所以B事务的更改等于从来没存在过,而A事务却读到了这个更改,就是说A事务读到了脏数据,这种现象称为脏读。如果A事务使用读到的脏数据用于后续计算,肯定是不对的。所以生产很少使用这种隔离级别。
读已提交
既然脏读是因为A事务读取了B事务未提交的更改,那么好,让A事务读不到B事务未提交的更改不就行了。A事务不能读取B事务未提交的更改,只能在B事务提交后才能读取B事务的更改,这个就是读已提交。
| 时间点 | A事务操作 | B事务操作 |
|---|---|---|
| T0 | begin | begin |
| T1 | select price where goods_id=1ret: 1 | |
| T2 | update price = 2 where goods_id=1 | |
| T3 | select price where goods_id=1ret: 1 | |
| T4 | commit | |
| T5 | select price where goods_id=1ret: 2 |
如上述表格T2时刻,B事务进行了更新,但T3时刻A事务没读取到B事务的更新,而在T5时刻B事务提交后,A事务读取到了B事务的更改。很明显脏读问题不存在了,A事务每次读取的都是正确的数据。但是,这种隔离级别下会出现一次事务中,两次相同的查询返回不同结果的问题,比如T3和T5时刻A事务进行了两次读取,返回了不同的数据,这种就是不可重复读的问题。
可重复度读
既然上个隔离界别读已提交的问题是不可重复读,那下个隔离级别肯定是可重复读了,这名字也是够随意。即在A事务中读同一条很多次,不管B事务是否更新过这条数据,不管B事务是否提交过,A事务的读取操作总是返回相同的结果,如下图。
| 时间点 | A事务操作 | B事务操作 |
|---|---|---|
| T0 | begin | begin |
| T1 | select price where goods_id=1ret: 1 | |
| T2 | update price = 2 where goods_id=1 | |
| T3 | select price where goods_id=1ret: 1 | |
| T4 | commit | |
| T5 | select price where goods_id=1ret: 1 |
可重复读存在幻读的问题,即当B事务进行insert操作并提交后,A事务查询某个范围的返回的数据可能会改变(在MySQL中幻读问题在可重复度的隔离级别下也解决了)。不可重复读和幻读的区别在于前者描述的是别的事务update操作对当前事务的影响,而后者描述的是别的事务的insert操作对当前事务的影响。
串行化
串行化是指各个事务串行执行,互不干扰,实际生产中因为性能问题较少使用。
MySQL可重复读隔离级别的实现
你觉得这就完了?不,下边面试官肯定会问,mysql默认隔离级别是什么?你告诉他mysql默认的隔离级别是可重复读。
接下来就到关键点了:mysql是怎么实现可重复读的?这个就涉及到MySQL的两种读取方式:快照读和当前读。
快照读
事务中一般的select语句都是快照读,即读到的数据不是最新的。在可重复读的隔离级别下,读取到的是在当前事务开始前已经提交到数据库中的数据,而当前事务开始后由别的事务后续提交到数据库中的数据是读取不到的。快照读的实现依赖于MySQL的多版本并发控制(MVCC)。
具体而言,MySQL中每条记录都会两个隐藏的字段trx_id和rollback_pointer。trx_id记录了最近更新这条记录的事务的事务ID,rollback_pointer指向了回滚段里这条记录上个版本的数据,同时回滚段里上个版本的数据也指向了上上个版本的数据,构成一个链表,逻辑上如下图:
基于这种结构,MySQL中有一个一致性视图的概念(ReadView),主要有下边四个部分内容构成:
- m_ids:构建ReadView时处于活动状态的事务的id的集合。
- min_trx_id:m_ids中最小的事务ID。
- max_trx_id:区别于min_trx_id,它表示的含义不是m_ids中最大的事务ID,而是接下来的事务会分配的事务ID。
- creator_trx_id:当前事务的事务ID。
MySQL在可重复读的隔离级别下,每次开启一个事务时,会生成一个ReadView。当事务中发生简单select的操作即快照读时,对读取的记录会做如下操作:
- 如果记录的trx_id = creator_id,表示这条记录最近是本事务修改的,自己做的事情肯定是要认的,本条记录作为读取结果的一部分。
- 如果记录的trx_id < min_trx_id,表示这条记录的更改事务在本事务开启前已经提交,本条记录作为读取结果的一部分。
- 如果记录的trx_id >= max_trx_id,表示修改这条记录的事务是在本事务之后开启的,需要通过rollback_pointer找到一个可用版本,如果能找到的话,作为读取结果的一部分。
- 如果记录的trx_id >= min_trx_id 并且 记录的trx_id <max_trx_id,那么判断trx_id是否属于m_ids。如果属于,表示本事务开启时,修改本条记录的事务正处于活动状态,需要通过rollback_pointer找到一个可用的版本;如果不属于,说明本事务开启时修改本条记录的事务已经提交,本条记录可以作为读取结果的一部分。
总结而言,MySQL在可重复度的隔离级别下,每次事务开启时生成一个ReadView,在事务中的快照读,根据ReadView保证读取的数据都是在本事务开启前已经提交的。从而,不管数据怎么被别的事务改变,快照读总是可以读取到一致的数据。并且因为这种实现机制,保证了MySQL在可重复的读的隔离级别下,读取不到事务开启后才新增的数据,规避的幻读的问题。
另外提一句,MySQL在读已提交的隔离级别下,读已提交的实现方式是,事务中每次快照读之前都会生成一个ReadView。
当前读
相对于快照读,select……for update,select …… lock in share mode,update, delete这些操作都属于当前读。既然读的是最新的数据,那么一个事务中两次读取怎么保证一致呢?这里其实是通过锁实现的。当在一个事务中进行当前读的操作时,会对相关数加上锁,另外的事务对相关事务进行更改或者插入的操作时,会发现相关数据被上了锁,会blocked住。所以在第一个事务中多次当前读不会发生数据不一致的情况,因为数据被锁定,别的事务没法进行相关的更改操作。
这里的锁主要有两种,一种是行锁Record Lock,锁定一条记录;另一种是间隙锁Gap Lock,锁定一个索引的一个间隙,锁释放前另外的事务在该间隙的插入操作都会blocked主。MySQL通过这两种锁保证了在当前读的情况下不会发生不可重复度和幻读。暂时到这,关于当前读的锁下次再仔细研究。
题外话:事务的四大特性ACID
原子性:指事务中所有操作需要是一个不可分割的整体,要不都执行成功,要不都执行失败。
隔离性:指事务之间不应该互相影响而发生意想不到不到的结果,隔离的严格程度就是我们说的隔离界别。
持久性:指事务一旦提交成功,后边不管发生什么事故,本次提交的结果不应该丢失。
一致性:指事务的执行应该让业务从一个一致性状态转移到另一个一致性状态,而这个一致性状态更多是和业务相关,所以事务的一致性不仅需要通过数据库保证,也需要通过业务代码去保证。