事务隔离级别与MVCC

125 阅读7分钟

脏读、幻读和不可重复读的概念

  1. 脏读:一个事务中访问到了另外一个事务未提交的数据。 当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
  1. 不可重复读:一个事务查询同一条记录2次,得到的结果不一致。 是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
  1. 幻读:一个事务查询2次,得到的记录条数不一致。 幻读是不可重复读的一种特殊场景。第一个事务在进行第一次和第二次查询的间隔时间内,第二个事务添加或删除了对应的数据,第一个事务两次查询的条数不一致,就好像发生了幻觉一样。

事务隔离级别

image.png

  1. 读未提交(read uncommitted) 是指,一个事务还没提交时,它做的变更就能被别的事务看到。通俗理解,别人改数据的事务尚未提交,我在我的事务中也能读到。
  2. 读提交(read committed) 是指,一个事务提交之后,它做的变更才会被其他事务看到。通俗理解,别人改数据的事务已经提交,我在我的事务中才能读到。
  3. 可重复读(repeatable read) 是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。通俗理解,别人改数据的事务已经提交,我在我的事务中也不去读。MySQL InnoDB存储引擎默认支持的隔离级别是可重复读。
  4. 串行化(serializable ),顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。通俗理解,我的事务尚未提交,别人就别想改数据。
  • 若隔离级别是 “读未提交”,则V1的值就是2。这时候事务B虽然还没有提交,但是结果已经被A看到了。因此V2、V3也都是2。
  • 若隔离级别是 “读提交”,则V1是1,V2的值是2。事务B的更新在提交后才能被A看到。所以,V3的值也是2。
  • 若隔离级别是 “可重复读”,则 V1、V2 是 1,V3 是 2。之所以V2还是 1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。
  • 若隔离级别是 “串行化”,则在事务A执行“查询得到值1”的时候会加读锁。直到事务A提交后,事务B才可以继续执行。所以从A的角度看,V1、V2值是 1,V3 的值是 2。

MVCC

MVCC英文全称为Multi-Version Concurrency Control,翻译为中文即多版本并发控制。通过读取指定版本的历史记录,并通过一些手段保证读取的记录值符合事务所处的隔离级别,在不加锁的情况下解决读写冲突。 🔗

对于使用InnoDB存储引擎的表来说,聚集索引记录中都包含下面2个必要的隐藏列:
trx_id:一个事务每次对某条聚集索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
roll_pointer:每次对某条聚集索引记录进行改动时,都会把旧的版本写入undo日志中。这个隐藏列就相当于一个指针,通过他找到该记录修改前的信息。
image.png

为了判断版本链中哪个版本对当前事务是可见的,MySQL设计出了ReadView的概念。4个重要的内容如下:

  • m_ids:在生成ReadView时,当前系统中活跃的事务id列表
  • min_trx_id:在生成ReadView时,当前系统中活跃的最小的事务id,也就是m_ids中的最小值
  • max_trx_id:在生成ReadView时,系统应该分配给下一个事务的事务id值
  • creator_trx_id生成该ReadView的事务的事务id

当对表中的记录进行改动时,执行insertdeleteupdate这些语句时,才会为事务分配唯一的事务id,否则一个事务的事务id值默认为0。
max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比如现在有事务id为1,2,3这三个事务,之后事务id为3的事务提交了,当有一个新的事务生成ReadView时,m_ids的值就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。

mvcc判断版本链中哪个版本对当前事务是可见的过程

执行过程如下:

  • 如果被访问版本的trx_id = creator_id,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问;
  • 如果被访问版本的trx_id < min_trx_id,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问;
  • 被访问版本的trx_id >= max_trx_id,表明生成该版本的事务在当前事务生成ReadView后才开启,该版本不可以被当前事务访问;
  • 被访问版本的trx_id是否在m_ids列表中
    • 是,创建ReadView时,该版本还是活跃的,该版本不可以被访问。顺着版本链找下一个版本的数据,继续执行上面的步骤判断可见性,如果最后一个版本还不可见,意味着记录对当前事务完全不可见 ;
    • 否,创建ReadView时,生成该版本的事务已经被提交,该版本可以被访问。

MVCC实现读提交

MVCC实现读提交(Read Committed),每次读取数据前都生成一个ReadView。

建立数据表:

CREATE TABLE `girl` (
  `id` int(11) NOT NULL,
  `name` varchar(255),
  `age` int(11),
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

下面是3个事务执行的过程,一行代表一个时间点:

image.png

image.png

时间点为5时select语句的执行过程:

  1. 系统中有两个事务id分别为100,200的事务正在执行。
  2. 执行select时生成一个ReadView,mids = [100,200]min_trx_id = 100max_trx_id = 201creator_trx_id = 0(select事务没有执行更改操作,事务id默认为0)。
  3. 最新版本的name列为西施,该版本trx_id值为100,在mids列表中,不符合可见性要求,根据roll_pointer跳到下一个版本。
  4. 下一个版本的name列王昭君,该版本的trx_id值为100,也在mids列表内,因此也不符合要求,继续跳到下一个版本。
  5. 下一个版本的name列为貂蝉,该版本的trx_id值为10,小于min_trx_id,因此最后返回的name值为貂蝉。

image.png

时间点为8时select语句的执行过程:

  1. 系统中有一个事务id为200的事务正在执行(事务id为100的事务已经提交)。
  2. 执行select语句时生成一个ReadView,mids = [200]min_trx_id = 200max_trx_id = 201creator_trx_id = 0
  3. 最新版本的name列为杨玉环,该版本trx_id值为200,在mids列表中,不符合可见性要求,根据roll_pointer跳到下一个版本。
  4. 下一个版本的name列为西施,该版本的trx_id值为100,小于min_trx_id,因此最后返回的name值为西施。
  5. 当事务id为200的事务提交时,查询得到的name列为杨玉环。