0
点赞
收藏
分享

微信扫一扫

Windows 本地直接使用 SSH,SFTP 以及 SFTP下载文件到 Windows/mac 本地或上传(没有客户端时)

夹胡碰 2024-04-30 阅读 6
mysql

多版本并发控制(MVCC)

前置知识:事务

并发问题

脏读

事务A中读取到事务B中未提交的数据。具体来说,在t1时刻,事务A中读取到事务B中未提交的数据,并将该数据用于计算得到一个结果值;而在t2时刻,事务B执行回滚,那么事务A中得到的结果值就没有任何意义,这种现象就称为脏读。

脏读
# 脏读问题演示

# 事务1: 设置全局的隔离级别为读未提交, 设置完成后重新登录MySQL
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
# 检查隔离级别
SELECT @@TRANSACTION_ISOLATION;

# 事务1: 修改一条数据
BEGIN;
UPDATE `user` SET name = 'root' WHERE id = 1;

# 事务2: 查看数据
BEGIN;
SELECT * FROM `user` WHERE id = 1;

# 此时能够观测到事务2中读取到事务1中的修改, 但是事务1并未提交

# 将隔离级别改回默认级别, 重启控制台
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
# 检查隔离级别
SELECT @@TRANSACTION_ISOLATION;
不可重复读

事务A在一次事务的执行过程中,两次读取结果不相同。具体来说,在t1时刻,事务A读取数据x1;在t2时刻,事务B将数据修改为x2并提交事务;在t3时刻,事务A再次读取数据得到x2;站在事务A的角度,可能在t1时刻到t3时刻并没有对数据进行修改,却得到两个不同的值(都是正确值),这种现象称为不可重复读。

幻读

解决了不可重复读的问题,即在一次事务中,可以保证对数据的两次读取的结果是相同的。但是会出现幻读的问题。幻读问题是指事务A在查询时明明没有查到该数据,但是却无法插入成功。具体来说,在t1时刻,事务A查询是否有id=1的数据,发现没有;在t2时刻,事务B插入一条id=1的数据并提交;在t3时刻,事务A再次查询是否有id=1的数据,还是发现没有(因为可重复读),因此事务A打算插入一条id=1的数据,但是此时插入失败并报错,这时事务A就会纳闷,明明查询id=1的结果是没有,但是插入又报错说id=1的数据已经存在,这种现象称为幻读。

# 事务1: 查看数据
BEGIN;
SELECT * FROM `user`;

# 事务2: 查看数据, 新增数据, 并提交事务
BEGIN;
SELECT * FROM `user`;
# 注: insert操作会加锁, 如果在事务1未提交前, 事务2也执行了insert操作, 那么事务2会被阻塞
INSERT INTO `user`(id, name) VALUES(7, 'why2');
COMMIT;

# 事务1: 再次查看数据
# 在RR级别下, 由于可重复读机制, 因此事务1看不到事务2提交的数据, 但是插入失败
# 在RC级别下, 可以看到新插入的数据, 因此行数更多(幻读)
SELECT * FROM `user`;
# 事务1: 尝试插入相同的数据, 此时报错(这里演示RR级别下的错误, RC级别下能看到有id=7的数据)
INSERT INTO `user`(id, name) VALUES(7, 'why1');
# 错误: ERROR 1062 (23000): Duplicate entry '7' for key 'user.PRIMARY'

隔离级别

三个问题,对应四种方案,分别是解决0个问题(读未提交)、解决1个问题(读已提交)、解决2个问题(可重复读)、解决3个问题(串行化)。

串行是一切并行问题的终点,没有并行就不会有并行导致的伴生问题。

Oracle 数据库的默认隔离级别是 RC,MySQL 数据库的默认隔离级别是 RR。

事务的隔离级别

隔离级别之间的关系

可重复读并不是建立在读已提交的基础上,相反,这些隔离级别是互斥的。想要实现可重复读,那么必然会丢弃读已提交的性质。想要实现读已提交的数据,那么就没办法实现可重复读的性质。

操作指令

# 查看内置的变量值
SELECT @@autocommit;

# 开启事务: 方式一
SET @@autocommit=0;
# 开启事务: 方式二
BEGIN;

# 提交事务
COMMIT;

# 回滚事务
ROLLBACK;

# 查看事务的隔离级别
SELECT @@TRANSACTION_ISOLATION;

# 设置事务的隔离级别
# 需要退出当前会话窗口
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

MVCC 是什么

并发可能遇到三种场景:读-读、读-写、写-写

对于读-读场景,不存在并发问题;而对于后面两种场景,则存在并发问题,这些并发问题都可以通过加锁来解决。但是对于读-写冲突的场景,存在着比加锁性能更高的解决方案,即 MVCC(多版本并发控制)。因此,MVCC 是针对并发场景下的读写冲突问题,对于读操作不采用加锁的一种优化方案。对于写操作还是需要采用加锁。

MVCC 的实现原理

MySQL 中的 MVCC 是通过 4个隐藏字段、undo 日志和 read view 的共同作用实现。在读操作时,生成一个read view

隐藏字段

  • db_row_id:自增的隐藏主键,如果没有表结构没有指定主键,则会生成该字段
  • db_trx_id:最近修改的事务id,记录该条记录最近一次被哪个事务修改
  • db_roll_ptr:上一条记录的地址,本质上是next指针
  • deleted_bit:逻辑删除标志位

三个隐藏字段

undo log

undo log 用于记录数据被修改前的信息,用于事务回滚或MVCC(多版本并发控制)。

与 redo log 记录实际执行的物理操作不同,undo log 并不是记录实际执行的数据操作,甚至是记录与当前操作相反的操作(逆操作),例如当前实际执行的是delete 操作,那 undo log 中需要对该操作进行还原,因此记录 insert 操作。

  • insert undo log:保存插入记录的主键值,回滚时只需要删除该主键对应的记录即可

  • update undo log:保存记录的旧值,回滚时将记录更新回旧值

  • delete undo log:理论上将记录的旧值保存下来,回滚的时候重新插入记录即可(主键变化怎么办呢?)。但实际为了效率并没有这么处理。

    设置删除标志位(隐藏字段),InnoDB 引擎通过专门的 purge 线程来清理这些删除标志位被设置true的记录。这种情况下如何实现回滚呢?

undo log 在事务提交之后,并不会立即删除,因为这些日志还可能用于 MVCC。undo log 采用 segment(段)的方式进行管理和记录,存放在 rollback segment (回滚段)中,内部包含 1024 个 rollback segment。这里是什么的内部?

简洁点来说,undo log 日志应该体现 insert、update、delete 操作,但是 insert 插入的是新数据,那么该行数据对应的 undo log 版本链就只有一个结点,因此改行数据没有 undo log。

undo log 的本质就是一个链表

read view(读视图)

ReadView的生成原理(快照读的原理)

image-20230517155919738

版本链数据访问规则:

image-20230517160407270

image-20230517161029010

image-20230517161536302

快照读(snapshot read):

  • 在 RC(read committed)级别下,每次 snapshot read 都会生成 read view
  • 在 RR(repeatable read)级别下,事务内的第一次snapshot read会生成read view,后面的snapshot read都会读取第一次read view中的数据,从而实现repeatable read。

当前读(current read):加锁读取当前数据库中保存的数据。

举报

相关推荐

0 条评论