MYSQL有四大隔离级别,分别是:读未提交,读已提交,可重复读,串行化。
读未提交,就是在并发情况下,A事务读到了B事务未提交的数据,如果B事务最终回滚操作,事务A读取的数据就是错乱的,而且很难追踪,则会产生脏读的现象。读未提交是限制性最弱的隔离级别,带来的问题也最多,也有可能产生不可重复读和幻读的问题,所以很少实际应用。
读已提交,就是一个事务读取到的数据是其他事务已经提交的数据,此隔离级别虽然解决了脏读的问题,但是有不可重复的问题存在。
不可重复读,就是在并发情况下,A事务首次读取到的数据为1,未提交,接着B事务把数据1改为了2,并提交了事务,接着A事务再次读取数据,数据就变成了2,这种想象就是不可重复读问题。
可重复读,根据名字就能知道,他的出现解决了不可重复读的问题,是mysql的InnoDB存储引擎下的默认隔离级别。他虽然解决了脏读和不可重复读的问题,但还是会有幻读的问题存在。
可重复读是如何解决不可重复读的问题呢?是通过MVCC解决的。
MVCC主要是通过undo log和read view来实现。在MySQL中,在表的每一条数据后面都隐藏着几个字段:一个是事务ID,记录最近操作这条数据的事务ID;一个是回滚指针,指向这条数据的上一个版本,如果是新插入的数据,此记录为空;还一个是隐含的自增ID,当数据表没有设置主键和没有字段的值是唯一记录时,mysql会采用这个隐含的自增ID作为主键。
undo log,也被称为回滚日志,用于记录数据被修改前的信息。作用是记录回滚信息和快照读。如果一个事务是插入新记录,产生的undo log只在事务回滚时需要,在事务提交后可以被立即丢弃;如果一个事务是修改或者删除,产生的undo log不仅会在回滚时需要,在快照读的时候也需要,所以不能轻易被删除,只有在快照读和回滚时不涉及该日志时,就会被purge线程清理。在undo 日志中,会将一条数据不同的版本根据回滚指针串联起来,形成一个链表。
read view,就是事务进行快照读操作时产生的读视图。RC级别下,同一事务里,每次查询都会产生一个新的视图,这样很有可能造成在同一个事务里前后读取到的数据不一致的问题,RR级别下,同一事务下,只会获取一次视图,之后的同样查询会沿用此视图,从而保证了数据都是一样的。read view 遵循一个可见性算法来做判断,判断当前事务可见哪些版本的数据。可以先来了解read view的几个属性:一个是m_ids,是一个List数据结构,用来存放当前系统中所有活跃的读写事务ID;一个是min_limit_id,指的是当前系统中所有活跃的读写事务ID中最小的事务ID;一个是max_limit_id,指的是当前系统中所有活跃的读写事务ID中最大的事务ID;一个是creator_trx_id,指的是创建当前视图的事务ID。
readview大概的算法流程:
如果数据的事务ID < 最小的活跃事务ID,说明该版本数据是在生成视图前生成的,并已经提交,该版本数据会被当前事务访问(对当前事务不可见)。
如果数据的事务ID > 会等于最大的活跃事务ID,说明该版本数据是在生成视图后生成的,该版本数据不会被当前事务访问(对当前事务不可见)。
如果 最小的活跃事务ID <= 数据的事务ID <= 最大的活跃事务ID,这种情况下,有三种情况:
-
如果所有的活跃事务ID中包含这个数据的事务ID,说明读视图生产的时候,这个数据事务还没有提交,但是如果这个数据的事务ID等于创建这个读视图的事务ID,说明这个数据时自己生产的,因此可见。
-
如果所有的活跃事务ID中包含这个数据的事务ID,但这个数据的事务ID不等于创建视图事务的ID,视图生产时这个数据的事务还没有提交,不是自己生产的,所以这个数据对于当前的事务是不可见的。
-
如果所有的活跃事务ID中不包含这个数据的事务ID,说明这个数据的事务是在视图生产前就已经提交了,此数据是对当前事务可见的。
根据这个算法获取到快照读的可见数据。
这里借鉴@SnailMann 的一张流程图:
幻读,就是A事务读取表中某字段小于5的数据为3条,该事务没有提交,此时,B事务插入了一条符合A事务的查询条件的数据,并且提交了事务,A事务再次查询时会得到4条。这种情况在没有使用MVCC时会发生,如果使用了MVCC就不会发生。如果B事务提交后,A事务更新了该字段所有小于5的数据操作,之后再去获取该字段小于5的数据,还是会得到4条,所以,幻读问题又发生了。
MVCC没有彻底解决掉脏读的问题,只有当前操作都是快照读时,才不会产生幻读的问题,如果既有快照读又有当前读,就会有幻读的问题。
MVCC解决了快照读情况下的幻读问题,却没有解决当前读情况下的幻读问题。
RR级别下的当前读情况下如何解幻读?加锁!
next-key 锁由两部分:行锁和间隙锁。例如行锁要锁住id=5的行,前一个值是id=1的行,name该行对应的间隙锁就是(1,5],在RR级别下,默认的加锁类型就是next-key,以此来防止当前读情况下导致幻读的问题。但是也带来了性能上的问题。
可串行化是最高的隔离级别,同时代价也是最大的,性能会很低,一般很少用。在该级别下事务是按照顺序执行的,所以避免的脏读、不可重复读和幻读的问题。因为该级别锁定的是整个范围的键,并一直持有本锁,知道事务完成。