文章目录
- 1. 各个隔离级别的演示
- 事务隔离级别 —— 读未提交
- 事务隔离级别—— 读提交
- 事务隔离级别 —— 可重复读
- 事务隔离级别 —— 串行化
- 脏读 不可重复读 幻读的理解
- 2. MVCC机制
- 读写
- 3个记录隐藏列字段
- undo日志
- 模拟MVCC
- read view 理论
- 3. 读提交与 可重复读的区别
- 两者本质区别
1. 各个隔离级别的演示
事务隔离级别 —— 读未提交
输入 select @@global.tx_isolation; 查询当前的隔离级别
发现 其隔离级别为 串行化
先将全局隔离级别 修改为 读未提交
再次查询发现 修改成功,全局隔离级别 已经被修改为 读未提交
在终端1中,启动事务后,插入王五的数据,此时在终端2中也可以查到王五的数据
再次在终端1的事务中 更新王五的名字为qwe, 此时终端2的事务中 依旧能查到 王五的名字已经修改为qwe
一个事务在执行中,读到另一个执行中的事务的更新 但是还没有commit的数据,这种现象叫做 脏读
事务隔离级别—— 读提交
将全局隔离级别 改为 读 提交
在终端1 启动事务后,插入 田七的数据到表中 ,此时在终端2启动事务的表中 是查询不到田七的数据
在终端1中再次修改id值为2的名字为liubei,此时在终端2中依旧是没有对id值为2的名字做出修改
当终端1的事务 进行 提交后 ,终端2中的事务才能查询到 表做出的修改数据
当一个事务,并未commit,就造成 同一个事务内,同样的读取,在不同的时间段,读取到不同的值
这种现象叫做 不可重复读
事务隔离级别 —— 可重复读
首先要保证终端1和终端2的全局隔离级别 和 会话隔离级别 都为 可重复读
输入 select @@global.tx_isolation; 查询当前会话的隔离级别
发现当前为 read -committed 表示 读提交
将会话的隔离级别 修改为 可重复读
查询当前全局隔离级别 也是 为 可重复读
启动事务后,在终端1中,插入王五的数据时,在终端2中,是是不见对应王五的数据的
终端1就算进行了提交,在终端2中也是看不见王五数据的
在终端2中,进行commit提交后,发现才可以查看到 account表中 修改的数据
可重复读对于删除 也是如此,当在终端1中 删除id值为2的数据时,终端2中是没有数据变化
只有当终端2进行提交,才可以看到终端1对表做出 的删除数据的操作
事务隔离级别 —— 串行化
保证mysql让所有的事务 进行串行化,就可以保证mysql 绝对的完整性
是一个安全的方案,但并不是一个高效的方案
保证终端1和终端2的全局隔离级别 和 会话隔离级别都为 串行化
设置 全局隔离级别 修改为 串行化
并查看当前全局隔离级别
将会话隔离级别 修改为 串行化
并查看当前会话 隔离级别
在终端1中,当想进行删除id值为1的数据时,发现卡住了
在终端2中,当事务进行提交,终端1中的删除id值为1的数据 的操作 就可以正常生效了
脏读 不可重复读 幻读的理解
一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读
(如在终端1中插入数据,而在终端2中是可以查询到数据的存在的)
同一个事务内,同样的读取,在不同的时间段,读取到了不同的值,这种现象叫做不可重复读
(如在终端1中查询数据,数据A是属于 50-100 范围内的,并且已经检测到数据A存在于50-100范围内, 但终端2在检测后修改数据A的范围 在100 -200 ,就会导致 终端1中继续检测时,发现 数据A还会存在于100-200 范围内)
一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据
会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读
情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读
(少数的mysql版本 虽然在可重复读 情况下,但依旧无法避免 终端2 能够查询到终端1的数据
正常来说在 可重复读情况下,终端2是不可以接收到来自终端1的数据的)
可以发现 读未提交 是 会触发 脏读、不可重复读、幻读 的情况的,所以隔离级别很低
其次 是 读已提交是不会触发 脏读 ,但是会触发 不可重复读、幻读的情况的
可重复读 与 可串行化 都不会 触发 脏读 不可重复读 、幻读 的情况
2. MVCC机制
一个数据库在并发访问时,具体场景有:
读-读并发,不存在任何问题,也不需要并发控制(没有人修改)
写-写并发,有线程安全问题,可能会存在更新丢失的问题
读-写并发,有线程安全问题,可能会造成事务隔离问题,出现脏读、幻读、不可重复读问题
读写
多版本并发控制(MVCC) 是一种用来解决 读写冲突的无锁并发控制
事务一定是有先有后 到达的,如何区分事务的先后问题
每一个事务 都有自己的事务ID
可以根据事务ID的大小,来决定事务到来的先后顺序
mysqld 可能会面临处理多个事务的情况,事务也有自己的生命周期
(其生命周期指的是 其要被创建、投递到等待队列中、拿出来等待执行、执行错误进行回滚、执行完毕要被消除)
所以mysqld 要对多个事务进行管理,而管理的本质是 先描述 在组织
就可以把 事务 看作 mysql中的一个结构体对象或者类对象,而事务ID是在结构体内部的
3个记录隐藏列字段
DB_TRX_ID : 6 byte 最近修改或插入的事务ID ,记录创建这条记录或最后一次修改该记录的事务ID
( 以插入举例,最近一次 插入记录的事务 是谁插入的,对应的事务ID是谁,把对应的事务ID放入表中)
DB_ROLL_PTR: 7byte ,回滚指针 指向这条记录的上一个版本
DB_ROW_ID: 6 byte 隐含的自增ID ,如果表中没有主键,InnoDB 会自动 以 DB_ROW_ID 产生一个聚簇索引
创建一张表 student,其中内部并不包含主键,只有一个不为空的name和不为空的age
将张三的数据 插入到 student表中
DB_TRX_ID 为 null ,说明没有事务进行插入、删除或者修改
DB_ROW_ID 为1 ,说明表中没有创建主键,所以使用隐含的主键
DB_ROLL_PTR 为null, 说明没有执行任何操作,找不到启动事务后的上一次操作
undo日志
mysql内有一大堆的日志缓冲区,就可以将其放入buffer pool中,被称为 undo log
undo log 是应用层 由mysql维护的一段内存缓冲区,用于保存日志数据
模拟MVCC
想要将上述创建好的student表中 张三数据做出修改, 事务10 通过使用 update 将张三 修改为 李四
msyql 判断要对事务进行修改,就需要先给 记录 加锁
修改前,需要先将记录拷贝到 undo log 中,此时undo log 中就有了一行副本数据
因为把老版本在 undo log 中保存了一份,所以在DB_ROLL_PTR 中填入对应老版本的地址 0xaa
该记录就指向了 undo log 中的历史版本
最终把 name 从张三 改到 李四
又因为 是事务10对表中数据做出修改,所以 对应的 DB_TRX_ID 变为10
当事务提交后,就在该记录下释放锁
如果又来了一个事务11,要把表中的记录 做出修改,把age 从 28 改为 30
msyql 判断要对事务进行修改,就需要先给 记录 加锁
修改前,需要先将记录拷贝到 undo log 中,此时undo log 中就又有了一行副本数据
因为把老版本在 undo log 中保存了一份,所以在DB_ROLL_PTR 中填入对应老版本的地址 0xbb
该记录就指向了 undo log 中的历史版本
又因为 是事务10对表中数据做出修改,所以 对应的 DB_TRX_ID 变为11、age 变为 30
当事务提交后,就在该记录下释放锁
最终形成一个多版本的数据,就称为 MVCC 多版本控制
把上述的一个一个的版本,称之为一个一个的快照
当前读:读取最近的记录
快照读:读取历史版本,不读取最新数据
增删改 都叫做当前读
select 有可能是当前读、也有可能是 快照读
读写并发:当使用upate对数据做修改时, 写 的是 当前最新的数据,而读的是 历史版本的数据
不会出现访问同一个的位置 ,就不用加锁,可以并发进行读写操作
串行化的 读写 访问的都是 当前数据,所以读写必须都加锁
若select 采用快照读,即读取历史版本的数据 ,是不受加锁限制的,就可以并发可以执行
既提高了效率,又为未来实现隔离性提供底层支持
read view 理论
read view 就是 事务进行快照读 操作的时候 产生的 读视图
在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照
记录并维护当前活跃事务的ID
(每个事务开启时,都会被分配一个ID,这个ID是递增的,所以最新事务,ID值越大)
read view 在mysql中就是一个类,本质是用来可见性判断的
当某个事务执行 快照读时,对该记录 创建一个read view 读视图,用它作为条件,用来判断当前事务能够看到那个版本的数据
,既可能是当前最新的数据,也有可能是 undo log 里面某个版本的数据
在read view类中 有四个重要字段
mids 表示 一张列表,用来维护read view 生成时刻,系统正在活跃的事务ID
(事务是并发的,当一个事务在运行时,其他事务也可能在运行,所以获取正在活跃的事务ID)
up_limit_id 表示 mids列表中 事务ID最小的ID
low_limit_id 表示 read view 生成时刻系统尚未分配的下一个事务ID,也就是目前已经出现的事务ID的最大值+1
creator_trx_id 表示 创建这个read view 的事务ID
若版本链某一条记录中的事务ID 与自己的creator_trx_id 相同
说明现在正在查看的记录,就是自己创建的,所以就应该看到对应的事务
up_limit_id 表示正在活跃的事务ID中最小的
若遍历对应的版本链时,发现对应的事务ID 比 我所能看到的最小的事务ID 还要小
说明 DB_TRX_ID 对应的事务早就已经 结束提交 了
所以我应该看到 该事务
low_limit_id 表示 read view 生成时刻系统尚未分配的下一个事务ID
若遍历版本链,发现所记录某一条的事务ID 比我自己事务中的 low_limit_id 还要大
说明 该事务 还没有跑起来
假设有 7 8 9 10 号事务ID,在快照前, 7 和 9 号事务ID 先提交了
所以 m_ids 中 就为 8 和 10 号 事务ID
所以快照到的事务ID 可以不连续
即 DB_TRX_id 不在 mid_s列表中,说明已经提交了,就可以看到
如果在,则说明该事务和自己的事务一样都是活跃事务,没有提交,就不应该看到
read view 是事务可见性的一个类,不是事务创建出来的时候,就会有read view
而是当这个事务(该事务已经存在), 首次进行快照读的时候,mysql 会形成 read view
3. 读提交与 可重复读的区别
RR (repeatable Read) 表示 可重复读
RC(Read Committed) 表示 读提交
select * from user lock in share mode 以加共享锁方式进行读取,对应的是当前读
若不加 lock in share mode 则为对照读
查询当前隔离级别 发现为 串行化
将隔离级别设置为可重复读
向user表中插入id值为1的数据
输入 select * from user 查看当前user表中的数据
由于select * from user 是快照读,读取的是历史版本的数据
所以即使当终端1将id值为1的数据 的age修改为18
终端2中 进行从查询 表中 id值为1的数据 的age 依旧为15
所以将其改成 select * from user lock in share mode , 此时就按照当前读
再次查询表中id值为1的数据的age 为18
假设终端1中的事务为 事务A ,终端2中的事务 为 事务B
事务B进行快照读时,mysql就已经形成了一个 read view
事务B 快照对象里面的值 认为 事务A 是跟它一块运行的
事务B 就认为 事务A 在自己的 mid_s 列表中,没有提交,就看不到
先在终端1进行修改age, 进行提交后再查询
此时 终端2才进行查询
则发现即使是快照读 ,表中数据也是修改后的,与 当前读 到的数据是相同的
假设终端1中的事务为 事务A ,终端2中的事务 为 事务B
由于事务B在 事务A进行操作时,并没有进行快照读,从而没有生成对应的 read view
只有到 事务A 进行提交后 , 事务B 才进行快照读 ,生成 read view
此时 事务B中 mid_s列表 中 不存在 事务A的值,说明 已经提交了 ,就可以看到了
先在终端1进行修改age, 进行提交后再查询
此时 终端2才进行查询
则发现即使是快照读 ,表中数据也是修改后的,与 当前读 到的数据是相同的
假设终端1中的事务为 事务A ,终端2中的事务 为 事务B
由于事务B在 事务A进行操作时,并没有进行快照读,从而没有生成对应的 read view
只有到 事务A 进行提交后 , 事务B 才进行快照读 ,生成 read view
此时 事务B中 mid_s列表 中 不存在 事务A的值,说明 已经提交了 ,就可以看到了
两者本质区别
RR (repeatable Read) 表示 可重复读
RC(Read Committed) 表示 读提交
由于read view 形成时机的不同,从而造成 RC、RR 级别下 快照读的结果的不同
RR级别下 的某个事务 的对某条记录的第一次快照读会创建 一个快照及 read view
将系统活跃的其他事务记录起来
只有当首次快照读时,才会产生 read view ,所以之后的快照读 使用的都是 同一个 read view ,对之后的修改不可见
(此时更新数据,全被放入 read view的mid_s列表中,说明没有提交 不可见)
在RC级别下, 事务中,每次快照读都会重新生成一个 快照 和 read view
所以一个终端中的事务 就可以看到另一个终端中的事务提交的更新