MySQL-MVCC

问题

多事务同时访问数据库中的相同数据时

  • 读 + 读:多个事务对相同数据全部是读操作时,不会产生任何并发问题
  • 读 + 写:多个事务即存在读又存在写时,可能会产生脏读不可重复读幻读的问题
  • 写 + 写:多个事务同时修改数据,可能产生数据丢失(回滚丢失、覆盖丢失)

解决方案

  • 读 + 读:不用解决
  • 读 + 写:常规一般会对要操作的数据加锁来解决并发读写可能产生的问题,MySQL 的 InnoDB 实现了 MVCC 来更好地处理读写冲突,可以做到即使存在并发读写,也不用加锁,实现“非阻塞并发读”
  • 写 + 写:通过加锁(乐观锁/悲观锁)来解决

概念

MVCC(Multi Version Concurrency Control)即多版本并发控制,是指在数据库中为了实现高并发的数据访问,对数据进行多版本处理,并通过事务的可见性来保证事务能看到自己应该看到的数据版本。

简单说,MVCC多版本并发控制是指:维持一个数据的多个版本,使得读写操作没有冲突,这只是一个理想概念。MySQL 通过快照读实现了 MVCC 理想模型的其中一个具体非阻塞读功能。

  • 当前读
    读取的数据是最新版本,读取数据时还要保证其他并发事务不会修改当前的数据,当前读会对读取的记录加锁。比如:select ... lock in share mode(共享锁)、select ... for update | insert | delete(排它锁)
  • 快照读
    基于 MVCC 实现的读,不对读操作加任何锁,读取的时候根据版本链和 Read View 进行可见性判断,所以读取的数据不一定是数据库中的最新值。注意,在串行化隔离级别下,读操作也会加锁,会退化成当前读

实现原理

MySQL 中 MVCC 主要是通过行记录中的隐藏字段、版本链、ReadView来实现的

隐藏字段

MySQL中,在每一行记录中除了自定义的字段,还有一些隐藏字段

  • ROW_ID:当数据库表没有定义主键时,InnoDB 会以 ROW_ID 为主键生成一个聚簇索引
  • TRX_ID:事务ID记录了新增/最近修改这条记录的事务ID,事务ID是自增的
  • ROLL_POINTER:回滚指针指向当前记录的上一个版本(在Undo Log中)

版本链

在修改数据的时候,会向 Redo log 中记录修改的页内容(用于恢复数据),也会向 Undo log 中记录数据库原来的快照。Undo log 有两个作用,回滚事务和实现MVCC

事务(trx_id=100)执行了 insert into t_user values(1, '张三', 20);


事务(trx_id=102)执行了update t_user set name = '李四' where id = 1;

事务(trx_id=103)执行了update t_user set name = '王五' where id = 1;

ReadView

多个事务对同一行数据修改后,这行记录除了最新的数据,在 Undo log 中还有多个版本的快照。那其他事务查询时能查到最新版本的数据吗?

ReadView 就是 MVCC 在对数据进行快照时,会产生的一个“读视图

ReadView 中有4个比较重要的变量

  • m_ids:活跃事务ID列表,当前系统中所有活跃的(没提交的)事务的事务ID列表
  • mix_trx_id:m_ids中最小的事务ID
  • max_trx_id:生成 ReadView 时,系统应该分配给下一个事务的ID,也就是 m_ids 中的最大事务ID + 1
  • creator_trx_id:生成该 ReadView 的事务的事务ID

ReadView 可见性算法:

  • trx_id == creator_trx_id 时,说明版本链中的这个版本是当前事务修改的,所以该快照记录对当前事务可见
  • trx_id < min_trx_id 时,说明版本链中的这条记录已经提交了,所以该快照记录对当前事务可见
  • trx_id > max_trx_id 时,说明快照记录对当前事务不可见
  • min_trx_id <= trx_id < max_trx_id 时,如果版本链中记录的 trx_id 在活跃事务ID列表 m_ids 中,说明生成 ReadView 时,修改记录的事务还没有提交,所以该快照记录对当前事务不可见;否则该快照记录对当前事务可见

当事务对行记录进行快照读时 select * from t_user where id=1;,在版本链的快照中,从最新的一条记录开始,一次判断这4个条件,直到某一版本的快照读对当前事务可见,否则继续比较上一版本的记录

MVCC 只在 RC(解决脏读)和 RR(解决不可重复读)隔离级别下生效。在 RC 隔离级别下,每一次快照读都会生成一个最新的 ReadView;在 RR 隔离级别下,只有事务中第一次快照读会生成 ReadView,之后的快照读都使用第一次生成的 ReadView。

MVCC案例

前提条件:事务(trx_id=100)向表中插入一条记录 insert into t_user values(1, '张三', 20);并提交事务

时间顺序 事务101 事务102 事务103
t1 begin
t2 select * from t_user where id=1;
t3 begin
t4 select * from t_user where id = 1;
t5 begin
t6 select * from t_user where id = 1;
t7 update t_user set name = '李四' where id = 1
t8 select * from t_user where id = 1
t9 select * from t_user where id = 1
t10 commit
t11 select * from t_user where id = 1
t12 update t_user set name = '王五' where id = 1
t13 commit
t14 select * from t_user where id = 1

版本链

在时间点 t1~t6 时,整个版本链中只有一个快照, trx_id = 100


在时间点 t7~t11 时,整个把版本链有两个快照 trx_id = 102、100


在时间点 t12~t14 时,整个把版本链有两个快照 trx_id = 103、102、100

事务隔离级别为RC(读已提交)

当前事务隔离级别为RC时,每个事务每次查询对应生成的 ReadView

  • t2、t4、t6
    这三个时间点,版本链中都只有一个快照(trx_id=100),
    因为 trx_id(100) < min_trx_id(101),符合算法2,所以 trx_id = 100 这个快照对当前事务可见
  • t8
    版本链中有两个快照 trx_id=102 -> trx_id=100,从版本链中最新的开始依次判断
    对于 trx_id = 102 快照,min_trx_id(101) <= trx_id(102) < max_trx_id(104),且 trx_id(102) 在 trx_list(101,102,103) 中,说明当前事务生成 ReadView 时,修改该记录的事务仍在活跃事务(还未提交),根据算法4,trx_id=102的快照对当前事务不可见
    对于 trx_id = 100,因为 trx_id(100) < min_trx_id(101),符合算法2,所以 trx_id = 100这个快照对当前事务可见
  • t9
    版本链中有两个快照 trx_id=102 -> trx_id=100,从版本链中最新的开始依次判断
    对于 trx_id = 102 的快照,因为 trx_id(102) = creator_trx_id(102) 符合算法1,所以 trx_id = 102 的快照对当前事务可见
  • t11
    版本链中有两个快照 trx_id=102 -> trx_id=100,从版本链中最新的开始依次判断
    对于 trx_id = 102 快照,min_trx_id(101) <= trx_id(102) < max_trx_id(104),且 trx_id(102) 不在 trx_list(101,103) 中,说明当前事务生成 ReadView 时,修改该记录的事务不是活跃事务(已提交),根据算法4,trx_id=102的快照对当前事务可见
  • t14
    版本链中有三个快照 trx_id = 103 -> trx_id=102 -> trx_id=100,从版本链中最新的开始依次判断
    对于 trx_id = 103 的快照,min_trx_id(101) <= trx_id(103) < max_trx_id(104),且 trx_id(103) 不在 trx_list(101)中,说明当前事务生成 ReadView 时,修改该记录的事务不是活跃事务(已提交),根据算法4,trx_id=103的快照对当前事务可见

事务隔离级别为RR(可重复读)

当前事务隔离级别为RR时,每个事务每次查询对应生成的 ReadView


在 RR 隔离级别下,只有事务中第一次快照读会生成 ReadView,之后的快照读都使用第一次生成的 ReadView,所以 事务101在 t8、t14时刻查询时,使用的 ReadView 跟 t2 时刻一样;事务102在 t9 时刻查询使用 ReadView 跟 t4 时刻一样;事务103在 t11 时刻查询使用的 ReadView 跟 t6 时刻一样。

  • t2、t4、t6
    这三个时间点,版本链中都只有一个快照(trx_id=100),
    因为 trx_id(100) < min_trx_id(101),符合算法2,所以 trx_id = 100 这个快照对当前事务可见
  • t8
    版本链中有两个快照 trx_id=102 -> trx_id=100,从版本链中最新的开始依次判断
    对于 trx_id = 102 快照,trx_id(102) >= trx_id(102),根据算法3,trx_id=102的快照对当前事务不可见
    对于 trx_id = 100,因为 trx_id(100) < min_trx_id(101),符合算法2,所以 trx_id = 100这个快照对当前事务可见
  • t9
    版本链中有两个快照 trx_id=102 -> trx_id=100,从版本链中最新的开始依次判断
    对于 trx_id = 102 的快照,因为 trx_id(102) = creator_trx_id(102) 符合算法1,所以 trx_id = 102 的快照对当前事务可见
  • t11
    版本链中有两个快照 trx_id=102 -> trx_id=100,从版本链中最新的开始依次判断
    对于 trx_id = 102 快照,min_trx_id(101) <= trx_id(102) < max_trx_id(104),且 trx_id(102) 在 trx_list(101,102,103) 中,说明当前事务生成 ReadView 时,修改该记录的事务是活跃事务(还未提交),根据算法4,trx_id=102的快照对当前事务不可见
  • t14
    版本链中有三个快照 trx_id = 103 -> trx_id=102 -> trx_id=100,从版本链中最新的开始依次判断
    对于 trx_id = 103 的快照,trx_id(103) >= max_trx_id(102),根据算法3,trx_id=103的快照对当前事务不可见
    对于 trx_id = 102 的快照,trx_id(102) >= max_trx_id(102),根据算法3,trx_id=102的快照对当前事务不可见
    对于 trx_id = 100 的快照,trx_id(100) < min_trx_id(101),根据算法2,trx_id=100的快照对当前事务可见

你可能感兴趣的:(MySQL-MVCC)