zoukankan      html  css  js  c++  java
  • mysql 学习

    本篇已收录在 MySQL 是怎样运行的 学习笔记系列

    MVCC(多版本并发控制)原理

    版本链

    对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL的UNIQUE键时都不会包含row_id列):

  • trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
  • 比方说我们的表hero现在只包含一条记录:

    假设插入该记录的事务id为80,那么此刻该条记录的示意图如下所示:

    假设之后两个事务id分别为100、200的事务对这条记录进行UPDATE操作,操作流程如下:

    每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样:

    对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id,这个信息很重要,我们稍后就会用到。

    ReadView

    对于使用 READ UNCOMMITTED 隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了;

    对于使用 SERIALIZABLE 隔离级别的事务来说, InnoDB 规定使用加锁的方式来访问记录

    对于使用 READ COMMITTEDREPEATABLE READ 隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的

    为此 InnoDb 提出了一个 ReadView 的概念. 这个 readview 包含四部分内容:

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

    在 MySQL 中,READ COMMITTED和REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同。我们还是以表 hero 为例来,假设现在表 hero 中只有一条由事务 id 为 80 的事务插入的一条记录:

    接下来看一下 READ COMMITTED 和 REPEATABLE READ 所谓的生成 ReadView 的时机不同到底不同在哪里。

    READ COMMITTED —— 每次读取数据前都生成一个ReadView:

    比方说现在系统里有两个事务id分别为100、200的事务在执行:

    此刻,表hero中number为1的记录得到的版本链表如下所示:

    假设现在有一个使用READ COMMITTED隔离级别的事务开始执行:

    # 使用READ COMMITTED隔离级别的事务
    BEGIN;
    
    # SELECT1:Transaction 100、200未提交
    SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'
    

    然后再到事务id为200的事务中更新一下表hero中number为1的记录:

    # Transaction 200
    BEGIN;
    
    # 更新了一些别的表的记录
    ...
    
    UPDATE hero SET name = '赵云' WHERE number = 1;
    
    UPDATE hero SET name = '诸葛亮' WHERE number = 1;
    

    然后再到刚才使用READ COMMITTED隔离级别的事务中继续查找这个number为1的记录,如下:

    # 使用READ COMMITTED隔离级别的事务
    BEGIN;
    
    # SELECT1:Transaction 100、200均未提交
    SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'
    
    # SELECT2:Transaction 100提交,Transaction 200未提交
    SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'张飞'
    

    使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。

    REPEATABLE READ —— 在第一次读取数据时生成一个ReadView

    对于使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。我们还是用例子看一下是什么效果。

    比方说现在系统里有两个事务id分别为100、200的事务在执行:

    # Transaction 100
    BEGIN;
    
    UPDATE hero SET name = '关羽' WHERE number = 1;
    
    UPDATE hero SET name = '张飞' WHERE number = 1;
    
    # Transaction 200
    BEGIN;
    
    # 更新了一些别的表的记录
    ...
    
    

    假设现在有一个使用REPEATABLE READ隔离级别的事务开始执行:

    # 使用REPEATABLE READ隔离级别的事务
    BEGIN;
    
    # SELECT1:Transaction 100、200未提交
    SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'
    

    这个SELECT1的执行过程如下:

  • 在执行SELECT语句时会先生成一个ReadView,ReadView的m_ids列表的内容就是[100, 200],min_trx_id为100,max_trx_id为201,creator_trx_id为0。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是'张飞',该版本的trx_id值为100,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
  • 下一个版本的列name的内容是'关羽',该版本的trx_id值也为100,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列name的内容是'刘备',该版本的trx_id值为80,小于ReadView中的min_trx_id值100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为'刘备'的记录。
  • 之后,我们把事务id为100的事务提交一下,就像这样:

    # Transaction 100
    BEGIN;
    
    UPDATE hero SET name = '关羽' WHERE number = 1;
    
    UPDATE hero SET name = '张飞' WHERE number = 1;
    
    COMMIT;
    

    然后再到事务id为200的事务中更新一下表hero中number为1的记录:

    # Transaction 200
    BEGIN;
    
    # 更新了一些别的表的记录
    ...
    
    UPDATE hero SET name = '赵云' WHERE number = 1;
    
    UPDATE hero SET name = '诸葛亮' WHERE number = 1;
    

    此刻,表hero中number为1的记录的版本链就长这样:

    然后再到刚才使用REPEATABLE READ隔离级别的事务中继续查找这个number为1的记录,如下:

    # 使用REPEATABLE READ隔离级别的事务
    BEGIN;
    
    # SELECT1:Transaction 100、200均未提交
    SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'
    
    # SELECT2:Transaction 100提交,Transaction 200未提交
    SELECT * FROM hero WHERE number = 1; # 得到的列name的值仍为'刘备'
    

    这个SELECT2的执行过程如下:

  • 因为当前事务的隔离级别为REPEATABLE READ,而之前在执行SELECT1时已经生成过ReadView了,所以此时直接复用之前的ReadView,之前的ReadView的m_ids列表的内容就是[100, 200],min_trx_id为100,max_trx_id为201,creator_trx_id为0。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是'诸葛亮',该版本的trx_id值为200,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
  • 下一个版本的列name的内容是'赵云',该版本的trx_id值为200,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列name的内容是'张飞',该版本的trx_id值为100,而m_ids列表中是包含值为100的事务id的,所以该版本也不符合要求,同理下一个列name的内容是'关羽'的版本也不符合要求。继续跳到下一个版本。
  • 下一个版本的列name的内容是'刘备',该版本的trx_id值为80,小于ReadView中的min_trx_id值100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c为'刘备'的记录。
  • 也就是说两次SELECT查询得到的结果是重复的,记录的列c值都是'刘备',这就是可重复读的含义。如果我们之后再把事务id为200的记录提交了,然后再到刚才使用REPEATABLE READ隔离级别的事务中继续查找这个number为1的记录,得到的结果还是'刘备',具体执行过程大家可以自己分析一下。

    小结

    READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。

  • 相关阅读:
    【内存管理】STL空间配置器
    【关键帧提取】基于CSD特征
    【内存管理】内存池技术
    【CUDA学习】计时方法
    【内存管理】简单的分配程序(续1)
    【背景建模】基于时空特征(续1)
    【OpenCV学习】准确定位帧
    【背景建模】基于时空特征(续2)
    【内存管理】简单的分配程序
    32位asp.net应用程序在64位Windows上运行时的问题一例
  • 原文地址:https://www.cnblogs.com/it-dennis/p/12675100.html
Copyright © 2011-2022 走看看