MySQL的事务隔离级别及其原理

一、MySQL事务的四种隔离级别

读未提交(READ UNCOMMITTED),可以读到其他事务中未commit的数据
读已提交(READ COMMITTED),只能读到其他事务已commit的数据
可重复读(REPEATABLE READ),只能读取到本事务启动之前其他事务已commit的数据
序列化串行(SERIALIZABLE),所有事务串行化,不允许并行执行
isolation levels
通常在关于事务隔离级别的文章中,会提到读脏读、不可重复读、幻读三个数据一致性的问题,而又有很多文章提到只有序列化串行级别才能解决幻读,但是实际上在MySQL中,可重复读级别就能够解决全部三个问题。接下来将对可重复读的原理进行深入讲解。

二、两个关键的数据结构

Undo Log

MySQL中使用一种名为MVCC的技术实现了读已提交和可重复读,而MVCC的多个版本的数据正是基于Undo页,Undo页结构如下所示:undo页结构
MySQL中每一个record中,都会有额外的DB_TRX_ID字段记录了写入或修改该行数据的事务ID,DB_ROLL_PTR字段记录了指向修改之前的数据的指针,所以在MySQL运行期间对每一行数据的修改记录,都会被记录在undo页中。

ReadView

在可重复读的隔离级别下,每个事务在begin时,都会创建一个ReadView对象,记录了事务begin那一刻所有正在执行的事务,如下图所示(源代码链接):
ReadView
m_low_limit_id 是当前正在执行的事务的ID的最大值
m_up_limit_id 是当前正在执行的事务的ID的最小值
m_creator_trx_id 是创建该ReadView的事务的ID
m_ids 是所有正在执行的事务的ID的列表

三、源码分析

初始化阶段

为事务创建一个新的ReadView对象,将此时系统中活跃的事务id记录到对象中

Created with Raphaël 2.3.0 row0sel.cc - row_search_mvcc(...) 1. trx0trx.cc - ReadView *trx_assign_read_view(trx_t *trx) 2. read0read.cc - void MVCC::view_open(ReadView *&view, trx_t *trx) 3. read0read.cc - void ReadView::prepare(trx_id_t id)

void ReadView::prepare(trx_id_t id)方法的源码如下,将当前系统中的事务信息记录到m_creator_trx_idm_up_limit_idm_low_limit_idm_ids四个变量中,用于进行数据在事务中可见性的检查。
在这里插入图片描述

可见性检查阶段

在sql执行并且启用MVCC功能时,会根据当前事务的ReadView对每行sql涉及到的数据的可见性进行检查。只有行上的事务id小于当前事务id,并且不在记录的活跃事务id列表中,说明该行是在ReadView初始化之前提交的,对当前事务可见。

Created with Raphaël 2.3.0 row0pread.cc - dberr_t Parallel_reader::Ctx::traverse_recs(PCursor *pcursor, mtr_t *mtr) 1. row0pread.cc - bool Parallel_reader::Scan_ctx::check_visibility(const rec_t *&rec, ulint *&offsets, mem_heap_t *&heap, mtr_t *mtr) 2. read0types.h - bool changes_visible(trx_id_t id, const table_name_t &name) 3. row0vers.cc - dberr_t row_vers_build_for_consistent_read(...) 4. trx0rec.cc - bool trx_undo_prev_version_build(...)

步骤1的Parallel_reader::Scan_ctx::check_visibility方法的源代码如下,该方法的入参中的rec对象,是上面提到的Row in Buffer Pool的数据行,当该行数据对当前事务不可见时,会调用步骤3的dberr_t row_vers_build_for_consistent_read方法,查找Undo Log中是否有对当前事务可见的数据。如果从Undo Log中依然获取不到有效的数据,则返回false,表示该行数据对当前事务不可见。
在这里插入图片描述
步骤2的bool changes_visible(trx_id_t id, const table_name_t &name)方法的源码如下,只有当数据行上的事务id等于ReadViewm_creator_trx_id、或者小于m_up_limit_id,或者不存在于m_ids时,才对当前事务可见。
在这里插入图片描述
步骤3的dberr_t row_vers_build_for_consistent_read方法的源码如下,该方法中的for循环会一直向前获取数据的老版本。如果获取的老版本数据为null,则说明已经循环到了最早的数据版本,则跳出for循环并结束方法。
在这里插入图片描述
每获取到一次老版本的数据,便对其进行可见性的检查,当该数据可见时直接跳出for循环并结束方法。
在这里插入图片描述

四、不同隔离级别中数据可见性的原理

由上述内容,我们了解到MySQL中数据可见性的检查是基于ReadView和Undo Log实现的,也就是所谓的MVCC多版本并发控制的具体实现。而不同的隔离级别正是通过用不同的策略使用ReadView,以达到不同数据可见性。

  • 读未提交(READ UNCOMMITTED),由上述源码分析可以看到,该隔离级别不会通过ReadView进行可见性检查,即直接使用Row in Buffer Pool的行数据
  • 读已提交(READ COMMITTED),事务中执行每条sql时,都会创建新的ReadView,因此该隔离级别每次sql执行时,都能读到最新的已提交的数据。
  • 可重复读(REPEATABLE READ),只会在事务启动时创建新的ReadView,因此只能读取到事务启动之前已提交的数据
  • 序列化串行(SERIALIZABLE),在执行sql时会获取相关表的锁,并且直到事务结束时才释放,因此该隔离级别下每张表同时只能有一个事务在使用

由此可以发现,在“可重复读”隔离级别下,就可以解决事务中读未提交、不可重复读两种问题,部分解决幻读问题。因为在事务执行时,其他事务提交的新的插入的数据行,其行上的事务id存在于该事务的ReadView中,所以会被判定为不可见,即达到了避免幻读的功能。

但是MySQL的MVCC的实现并不能完全保证数据的可见性,MySQL同时还使用了行级锁来确保每行数据修改的原子性,以避免多个事务同时操作一行数据。
因为在执行update、delete时,会跳过ReadView的检查机制,直接读取最新的数据,然后将其修改为当前事务的数据,即Row in Buffer Pool的行数据的事务id变为当前事务的id。而如果在这之前有其他事务插入了新行,则新的行数据的Row in Buffer Pool的版本的事务id会变为当前事务的id,当事务再次查询数据时,便会发生幻读。
为避免这种情况,可以使用for update对范围查询的内容进行加锁,实际存在的行会被占用写锁,不存在的行会被占用gap锁和next-key锁,以避免其他事务不会插入行数据到不存在的行的位置中,从而避免上述幻读情况的发生。

  • 1
    点赞
  • 2
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

银河舰长

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值