事务的四个隔离级别,读未提交、读已提交、可重复读和串行化,先来说说读未提交和串行化的实现方式:
- 读未提交:在并发事务访问时,事务可以看到其他事务未提交的数据,也就是不采取任何措施,就实现了读未提交的效果。
- 串行化:读的时候加共享锁,其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。
所以只有读已提交和可重复读是通过MVCC实现的,MVCC(Multi-Version Concurrency Control)是多版本并发控制的缩写,对于每个数据行,都会维持多个版本。
MVCC有两个重要知识点:
- 版本链:数据库中的每行数据包含隐藏字段事务id(trx_id)和回滚指针(roll_pointer),事务id记录修改该数据的事务id,回滚指针记录上一版本地址(在undo log中记录)。多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链,通过版本链,我们可以访问到前面版本的数据。
- Read View:Read View是事务执行SQL语句时产生的读视图。Read View会记录创建该Read View的事务id(creator_trx_id),当前活跃的事务(即未提交的事务)列表(m_ids),该列表包含创建该Read View的事务id,活跃事务中最小的事务id(min_limit_id),系统中应该分配给下一个事务的id(max_limit_id),即全局最大的事务id+1。
根据Read View,就可以判断数据的哪些版本是可读的,具体如下:
- 数据行事务id < Read View中活跃事务最小id:表明该数据是已提交的事务改动的,是可见的。
- Read View中活跃事务最小id <= 数据行事务id < Read View中准备分配给下一个事务的id:如果数据行事务id等于创建该Read View的事务id,说明该数据是当前事务自己修改的,是可见的。如果活跃列表中包含数据行事务id,说明是未提交事务修改的数据,不可见。如果活跃列表中不包含数据行事务id,说明是已经提交的事务修改的数据,可见。
- 数据行事务id >= Read View中准备分配给下一个事务的id:说明是未来的事务修改的数据,不可见。
对于不可见的数据版本,可以通过版本链找到之前的版本,如果之前的也不可见,就继续往前找,直到找到可见的数据版本。
MVCC实现可重复读:事务开启后第一次执行select时生成Read View。可重复读的含义同一事务内多次读取同一数据,数据不能发生变化,比如事务1读数据,事务2修改数据,事务1再次读取数据,数据不能发生变化。那么在MVCC下,事务1第一次读时生成的Read View中记录了活跃的事务2(事务2后续还要修改数据,所以肯定未提交,是活跃的),第二次读取时发现当前数据版本被活跃事务修改,是不可见的,就会向前寻找未被事务2修改的可见的数据版本,保证了数据的可重复读。
MVCC实现读已提交:事务中每次执行select时都生成Read View。读已提交的含义是事务只能读取已经提交的事务修改的数据。在MVCC中,事务每次读取数据时都生成一个新的Read View,更新活跃事务列表,已提交的数据的事务id不在活跃列表中了,那么数据自然是可见的。
MVCC解决幻读问题:可重复读隔离级别下,只会在事务开启后的第一次查询时生成Read View,并使用这个Read View直到事务提交。所以在生成 Read View之后其它事务所做的更新、插入记录版本对当前事务并不可见,防止了“幻读”。
此外,MySQL还可以使用临键锁Next-key-Lock 防止幻读。执行 select...for update/lock in share mode、insert、update、delete 等当前读时,会锁定读取到的记录和它们的间隙,防止其它事务在查询范围内插入数据,不能插入自然就不会幻读。