mysql系列之MVCC

mysql系列之MVCC

多版本并发控制 技术的英文全称是 Multiversion Concurrency Control ,简称 MVCC

undo log是 MVCC 实现的重要依赖。

undo log

mysql日志中比较重要的log

  • redo log
  • undo log
  • bin log

redolog 用于数据灾后恢复: WAL ,redo log 记录物理页的修改。

undo log 记录逻辑日志,事务每次修改innodb会记录undo record,支持事务回滚。当 delete 一条记录时,undo log 中会记录一条对应的 insert 记录,反之亦然,当 update 一条记录时,它记录一条对应相反的 update 记录,如果 update 的是主键,则是对先删除后插入的两个事件的反向逻辑操作的记录。这样,在事务回滚时,我们就可以从 undo log 中反向读取相应的内容,并进行回滚,同时,我们也可以根据 undo log 中记录的日志读取到一条被修改后数据的原值。

正是依赖 undo log,innodb 实现了 ACID 中的 C — Consistency 即一致性。

innodb 通过段的方式来管理 undo log,每一条记录占用一个 undo log segment,每 1024 个 undo log segment 被组织为一个回滚段(rollback segment) mysql 5.6 版本以后可以通过 innodb_undo_logs 配置项设置系统支持的最大回滚段个数,默认为 128。


事务的隔离级别

https://mp.weixin.qq.com/s?__biz=MzU4MzU4NzI5OA==&mid=2247483843&idx=3&sn=f66864ab3ecbb4461fbabec7a6ee8f19&chksm=fda7854ecad00c5897409cd219a78d8d3b89c4826953a43aa0d1234a204321a619ae9509e172&scene=21#wechat_redirect

隔离主要隔离的是事务,一个事务要和其他事务隔离。

SET TRANSACTION语句改变单个会话或者所有新进连接的隔离级别 ,默认是为下一个未开始的事务设置隔离级别,如果使用global,则后所有新连接都使用该隔离级别;session 表示当前连接使用设定的隔离级别。

  • read uncommitted 读取未提交
  • read committed 读已提交
  • repeatable read 可重复度
  • serializable 串行

read uncommitted

脏读,实际中很少用

read committed

只能看到别的事务已经commit的数据,是大多是数据库的默认隔离级别。

存在不可重复读的问题 - 同一事务执行期间,可能会有新的commit,因此多次select 可能会返回不通数据

Repeatable Read

Mysql 默认的隔离级别,保证同一事务期间看到同样的数据。但会有幻读的问题。幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行

Serializable

最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题 - 读的数据行家共享锁。

幻读

https://mp.weixin.qq.com/s?src=11&timestamp=1604415769&ver=2684&signature=sbZ5QV8KqQjg8GmbUumWaJVmm*k4LOL-GbEddZqhDvtChsCF*e*kzig3eaulsYMwx5JDPkJy0xURxxkpMlmzAdLqaDRfQqJjpbxOerNq2CcMKQABnfAT*K-3s1YhODZa&new=1

不可重复读指的是,在一个事务开启过程中,当前事务读取到了另一事务提交的修改。 幻读则指的是,在一个事务开启过程中,读取到另一个事务提交导致的数据条目的新增或删除。

Next-key(锁) - 当前读

包含两部分行锁和间隙锁, 记录锁在索引上;间隙锁加在索引之间。 将当前数据的与上一条数据和下一条数据之间的间隙锁定。

Screen Shot 2020-11-03 at 11.20.37 PM

根据number列,我们可以分为几个区间:(无穷小,2),(2,4),(4,5),(5,5),(5,11),(11, 无穷大)。区间(2,4)分别对应的临界记录是(id=1,number=2),(id=3,number=4),这两条记录中间可 以插入(id=2,number=3)等记录,那么就认为(id=1,number=2)与(id=3,number=4)之间存在间隙。

间隙锁定: 向左寻找最靠近检索条件的记录值A,作为左区间,向右寻找最靠近检索条件的记录值B作为右区 间,即锁定的间隙为(A,B)。 where number=5的话,那么间隙锁的区间范围为(4,11);

作用: 防止间隙内有新数据插入,防止已存在的数据更新成间隙内的数据

条件: 必须在RR隔离级别,检索条件必须有index,否则表锁。

session 1:
start transaction ;
select * from news where number=4 for update ; // 当前读

session 2:
start transaction ;
insert into news value(2,4);#(阻塞)
insert into news value(2,2);#(阻塞)
insert into news value(4,4);#(阻塞)
insert into news value(4,5);#(阻塞)
insert into news value(7,5);#(执行成功)
insert into news value(9,5);#(执行成功)
insert into news value(11,5);#(执行成功)

当检索number =4, 左边最近值2,右边最近为5, session 1 间隙锁范围 (2,4)-(4,5)

next-key 解决了当前读的幻读问题,问题是并发低。

MVCC - 快照读/一致性读

事务每次取数据的时候都会取创建版本小于当前事务版本的数据,以及过期版本大于当前版本的数据。

将历史数据存一份快照,所以其他事务增加与删除数据,对于当前事务来说是不可见的。

关于当前读和一致性读

快照读

https://time.geekbang.org/column/article/70562

普通的 select 就是快照读, 不加锁使用MVCC

innodb 有个全局的事务ID, 新事务都会记录这个唯一id,并且该ID是增长的; 新事务创建时,事务系统会将当前未提交的所有事务ID组成数组传给新事务。

每当trx 更新数据是,写入undo log 并将该行记录的隐藏字段db_trx_id 更新为当前事务ID。 当有另一个事务select 读取,如果读到改行db_trx_id不为空并且和当前事务ID 不同,说明这行数据是另一个事务修改的, 但是怎么判断这个change是当前事务开启前提交的还是开启后提交的?

Screen Shot 2020-11-03 at 11.41.28 PM

如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;

如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;

如果落在黄色部分,那就包括两种情况a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

如果这行数据的db_trx_id 大于当前事务ID,说明这行数据是当前事务开启后提交的,通过DB_ROLL_PTR 找到对应的undo log 来进行逻辑上的回溯 拿到当前事务开始前的原数据。

当前读

isnert, update, delete ,select * from t for update (lock in share mode) 为当前读,需要加锁

  • LOCK IN SHARE MODE 锁定当前查询的行,不允许其他事务对行进行写操作,但其他事务可以进行读操作。
  • FOR UPDATE 锁定行,阻止其他事物对该行的任何读写操作。

MVCC - (类似Copy-On-Write)

并发的演进思路

  • 普通锁,只能串行执行;
  • 读写锁,可以实现读读并发;
  • 数据多版本并发控制,可以实现读写并发。

MVCC - 事务并发, 并发控制可以通过加锁的方式来做,但加锁导致性能问题,MVCC类似于乐观锁,思想就是保存数据的历史版本,通过对数据行的多个版本管理来实现数据库的并发控制。可以认为 多版本并发控制(MVCC)是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低并且没有死锁的问题。

MVCC可以帮我们解决幻读的问题,除此还可以解决不可重复读的问题。

具体的实现是,在数据库的每一行中,添加额外的三个字段:

  1. DB_TRX_ID — 记录插入或更新该行的最后一个事务的事务 ID
  2. DB_ROLL_PTR — 指向改行对应的 undolog 的指针
  3. DB_ROW_ID — 单调递增的行 ID,他就是 AUTO_INCREMENT 的主键 ID
Screen Shot 2020-11-04 at 3.36.44 PM

对于正常的 select 查询 innodb 实际上进行的是快照读,即通过判断读取到的行的 DB_TRX_ID 与 DB_ROLL_PTR 字段指向的 undo log 回溯到事务开启前或当前事务最后一次更新的数据版本,从而在这样的场景下避免了可重复读与幻读的问题。

每当一个事务更新一条数据时,都会在写入对应 undo log 后将这行记录的隐藏字段 DB_TRX_ID 更新为当前事务的事务 ID,用来表明最新更新该数据的事务是该事务。

当另一个事务去 select 数据时,读到该行数据的 DB_TRX_ID 不为空并且 DB_TRX_ID 与当前事务的事务 ID 是不同的,这就说明这一行数据是另一个事务修改并提交的。

如何判断这行数据究竟是在当前事务开启前提交的还是在当前事务开启后提交的呢?

如果这一行数据的 DB_TRX_ID 在 TRX_ID 集合中(未提交的事务id集合)或大于当前事务的事务 ID,那么就说明这行数据是在当前事务开启后提交的,否则说明这行数据是在当前事务开启前提交的。对于当前事务开启后提交的数据,当前事务需要通过隐藏的 DB_ROLL_PTR 字段找到 undo log,然后进行逻辑上的回溯才能拿到事务开启时的原数据。 这个通过 undo log + 数据行获取到事务开启时的原始数据的过程就是“快照读”。

MVCC与不可重复读、幻读

对于正常的 select 查询 innodb 实际上进行的是快照读,即通过判断读取到的行的 DB_TRX_ID 与 DB_ROLL_PTR 字段指向的 undo log 回溯到事务开启前或当前事务最后一次更新的数据版本,从而在这样的场景下避免了可重复读与幻读的问题。

多版本并发控制(MVCC) 在一定程度上实现了 读写并发 ,它只在 可重复读(REPEATABLE READ)提交读(READ COMMITTED) 两个隔离级别下工作。其他两个隔离级别都和 MVCC 不兼容,因为 未提交读(READ UNCOMMITTED) ,总是读取最新的数据行,而不是符合当前事务版本的数据行。而 可串行化(SERIALIZABLE) 则会对所有读取的行都加锁。


https://mp.weixin.qq.com/s/rKYpQp4JPZL2q5p41f6V6Q

https://mp.weixin.qq.com/s?__biz=MzU4MzU4NzI5OA==&mid=2247483843&idx=3&sn=f66864ab3ecbb4461fbabec7a6ee8f19&chksm=fda7854ecad00c5897409cd219a78d8d3b89c4826953a43aa0d1234a204321a619ae9509e172&scene=21#wechat_redirect

https://mp.weixin.qq.com/s/iTtVdEOjJY2vnJoTDhcRAA

https://mp.weixin.qq.com/s?src=11&timestamp=1604416369&ver=2684&signature=9cCb3zA3o*zb15okxcukLypwTbjMYFM4zhuYCu4w8aWW86HN6Mjf05uQCGqpRCqD3rlM43MHuf852MxmeDpdM7DajvwkcAbYEBT*AR6aoNmLi7aHGtNPAGTXvkD8WT9x&new=1