问题
多事务同时访问数据库中的相同数据时
- 读 + 读:多个事务对相同数据全部是读操作时,不会产生任何并发问题
- 读 + 写:多个事务即存在读又存在写时,可能会产生脏读、不可重复读、幻读的问题
- 写 + 写:多个事务同时修改数据,可能产生数据丢失(回滚丢失、覆盖丢失)
解决方案
- 读 + 读:不用解决
- 读 + 写:常规一般会对要操作的数据加锁来解决并发读写可能产生的问题,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的快照对当前事务可见
网友评论