mvcc与幻读

官方定义

在一次查询的结果集里出现了某一行数据,但是该数据并未出现在更早的查询结果集里。例如,在一次事务里进行了两次查询,在这两次之间另一个事务插入某一行或更新某一行数据后提交, 并且数据可以匹配查询的where条件
在不同的隔离级别中,串行化读可以避免幻读,而其他隔离级别都允许出现幻读.

演示

执行前user表
mysql> select * from user;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | 小李 |   10 |
|  2 | 小王 |   20 |
+----+------+------+
2 rows in set (0.00 sec)
sql执行顺序
事务A事务B
0begin;begin;
1select * from user where age > 10
2insert into user(name, age) value(‘小张’, 30);
3commit;
4select * from dept where age > 10
5update dept set name = ‘改’ where age > 10;
6select * from dept where age > 10;
7commit;
执行过程
  • 事务A
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where age > 10;
+----+------+------+
| id | name | age  |
+----+------+------+
|  2 | 小王 |   20 |
+----+------+------+
1 row in set (0.00 sec)
  • 事务B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into user(name, age) values('小张', 30);
Query OK, 1 row affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)
  • 事务A
mysql> select * from user where age > 10;
+----+------+------+
| id | name | age  |
+----+------+------+
|  2 | 小王 |   20 |
+----+------+------+
1 row in set (0.00 sec)
  • 事务A
mysql> update user set name = '改' where age > 10;
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2  Changed: 2  Warnings: 0
  • 事务A
mysql> select * from user where age > 10;
+----+------+------+
| id | name | age  |
+----+------+------+
|  2 ||   20 |
|  3 ||   30 |
+----+------+------+
2 rows in set (0.00 sec)
执行后user表
mysql> select * from user;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | 小李 |   10 |
|  2 ||   20 |
|  3 ||   30 |
+----+------+------+
3 rows in set (0.00 sec)

基础概念

快照读和当前读
  • 快照读
    当执行普通select操作时Innodb默认会执行快照读, 读的是数据库记录的快照版本
  • 当前读
    对于会对数据修改的操作(update、insert、delete)都是采用当前读, 读取的是最新版本, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题

select可以加锁实现当前读

select * from user where age > 10 lock in share mode;
-- 或
select * from user where age > 10 for update;
锁类型
  • 共享锁
    又称读锁, S锁, 事务T1对数据A加上共享锁, 那么事务T2可以读数据A, 不能修改数据A, 事务T2可以对数据A再加共享锁, 不能加排它锁
  • 排他锁
    又称写锁, X锁, 事务T1对数据A加上排它锁, 那么事务T2不能读数据A, 不能修改数据A, 事务T2不能对数据A再加任何锁
锁级别
  • 行锁
  • 表锁
  • 间隙锁
  • next-key锁
redo 和 undo
  • redo
    redo log是重做日志,是记录物理数据变化的日志, 使用数据库DML对数据的修改操作,都会产生redo log,它可以保证事务的持久性
  • undo
    undo log是回滚日志,主要记录的是数据的逻辑变化,它是逻辑日志
    • 回滚
      可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录, 当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。
    • mvcc
      当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取

MVCC

版本链

对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列

  • trx_id: 每次对某条记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列
  • roll_pointer: 每次对某条记录进行改动时,这个隐藏列会存一个指针,可以通过这个指针找到该记
    录修改前的信息
ReadView

ReadView中主要包含当前MySQL中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids(一个数组), 以及下面几个

  • m_ids,就是说此时有哪些事务在MySQL里执行还没提交的
  • min_trx_id: m_ids中最小的值
  • max_trx_id: MySQL下一个要生成的事务ID,就是最大事务ID
  • creator_trx_id: 当前事务的ID
对比规则
  1. trx_id小于等于min_trx_id, 表示这个版本是已提交的事务或者是当前事务自己生成的,可见.
  2. trx_id大于等于max_trx_id, 表示这个版本是由将来启动的事务生成的,不可见.
  3. trx_id大于min_trx_id且小于max_trx_id
    3.1. trx_id在m_ids中, 表示这个版本是由还没提交的事务生成的,不可见
    tr_id等于creator_trx_id, 表示这个版本是当前事务自己生成的,可见
    3.2. trx_id不在m_ids中, 表示这个版本是已经提交了的事务生成的,可见

如果某个版本的数据对当前事务不可见的话,那就顺着版本链继续去找下一个版本的数据记录,一直到版本链中的最后一个版本数据,如果最后一个版本的数据也不可见的话,那么也就意味着该条记录对该事务不可见,查询结果就不包含该记录

mvcc如何实现不同隔离级别

可重复读

事务A的事务id = 101, 事务B的事务id = 102, user表内数据由之前事务id = 100的事务插入, 已经提交

事务A条件查询
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where age > 10;
+----+------+------+
| id | name | age  |
+----+------+------+
|  2 | 小王 |   20 |
+----+------+------+
1 row in set (0.00 sec)
  • 版本链
    在这里插入图片描述
  • 执行流程
  1. select时会首先生成一个ReadView,此时m_ids数组列表就是[101]
  2. 根据条件age>10, 查到了id=2的记录, 现在该记录只有一个版本
  3. 最新版本链内的记录, trx_id=100, 小于min_trx_id, 该记录对事务A可见
事务B进行新增
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into user(name, age) values('小张', 30);
Query OK, 1 row affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)
  • 事务B新增后的版本链
    在这里插入图片描述
事务A再次条件查询
mysql> select * from user where age > 10;
+----+------+------+
| id | name | age  |
+----+------+------+
|  2 | 小王 |   20 |
+----+------+------+
1 row in set (0.00 sec)
  • 版本链
    在这里插入图片描述
  • 执行流程
  1. 因为隔离级别是可重复读, 所以第二次查询还是用的第一次的ReadView, m_ids是[101]
  2. 根据条件查到id = 2和id = 3两条记录, id = 2是可见的
  3. id = 3的trx_id = 102, 大于max_trx_id, 所以id = 3的对事务A不可见
事务A根据条件修改
mysql> update user set name = '改' where age > 10;
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2  Changed: 2  Warnings: 0
  • 版本链在这里插入图片描述
  • 执行流程
  1. update操作是当前读, 总会读到最新的数据, 和ReadView以及trx_id没关系
  2. 事务A进行update的时候, 会读到两条记录
  3. 事务A修改后, id = 2和id = 3的记录, 他们最新版本的trx_id都变成101了, 也就是事务A的id
事务A修改完再次条件查询
mysql> select * from user where age > 10;
+----+------+------+
| id | name | age  |
+----+------+------+
|  2 ||   20 |
|  3 ||   30 |
+----+------+------+
2 rows in set (0.00 sec)
  • 版本链
    在这里插入图片描述
  • 执行流程
  1. 可重复读, m_ids依然是[101]
  2. 根据条件查到id = 2和id = 3两条记录
  3. 判断是否可见, trx_id存在于m_ids中, 且trx_id等于creator_trx_id, 对事务A可见
读已提交

读已提交也是用mvcc实现的, 唯一不同的是, 读已提交每次select的时候, 都会重新生成一个ReadView, 相应的trx_max, min_trx_id等参数都会取最新的.

如何避免幻读

next-key锁

next-key锁 = 行锁+间隙锁

间隙锁

gap间隙锁只有在可从重复读和串行化的隔离级别中才会有, 锁定范围空间的数据

使用方式
select * from user where age > 10 lock in share mode
-- 或
select * from user where age > 10 for update
总结
隔离级别脏读不可重复读幻读
读未提交
读已提交
可重复读
串行化

参考:
https://juejin.cn/post/6916500638457298958
https://juejin.cn/post/6844903799534911496

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值