一、可重复读
事务启动创建视图,执行期修改数据,事务看到和启动时一样。隔离级别下执行事务,与世无争,不受外界影响。
例1:事务更新,另外事务拥有这行锁,会被锁住,等到锁更新时,读的是?
mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL,
`k` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);
注意事务启动时机。begin/start transaction 不是起点,
1、启动方式:
1、一致性视图,执行第一个快照读语句时创建;2、start transaction with consistent snapshot创建。默认 auto commit=1
C :没显式用 begin/commit,update就是事务,完成自动提交。
B :更新之后查询 ; k=3
A :B 的查询之后查询。k=1
2、两个“视图”概念:
1、view:查询语句定义的虚拟表,调用时查询生成结果。create view
2、一致性读视图consistent read view(MVCC 时用),支持 读提交RC(Read Committed)和 可重复读RR(Repeatable Read)实现。
没有物理结构,作用:事务执行期间定义“能看到什么”
3、“快照”在 MVCC 里是怎么工作的?
快照基于整库。库有 100G,不是启动拷贝 100G
实现:唯一事务 ID(transaction id),按申请顺序递增。
每行数据更新生新版本,把 transaction id 赋值给这个数据版本的事务 ID(row trx_id), 。一行记录有多个版本 (row),每个有自己 row trx_id。
V4,k =22,row trx_id = 25。更新生成 undo log(回滚日志):三个虚线箭头就是
V1、V2、V3 不是物理上真实存在,每次需要根据当前和 undo log 计算。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。
启动后才生成不认(自己更新自己认),找到上个版本,如“上一个版本”不可见,继续找。
InnoDB 保存事务启动瞬间(启动没提交事务 ID)数组形式:ID 最小值为低水位,最大值加 1 为高水位。
数组+高水位 = 一致性视图(read-view);数组把 row trx_id 分成了不同情况:
一个数据版本的 row trx_id几种可能:
1.绿色,已提交/自己生成,可见;
2.红色,将来生成,不可见;
3.黄色
a.row trx_id 在数组中,表示没提交事务生成,不可见;
b.row trx_id 不在数组中,表示已提交事务生成,可见。
事务低水位是 18,访问这行数据时, V4 通过 U3 计算出 V3,在它看值=11。
之后更新,版本属于 2 或 3(a) ,跟这个事务看到内容无关,事务的快照“静态”。
InnoDB 利用“所有数据都有多个版本”,实现“秒级创建快照”。
二、读题交
数据版本,对于事务视图四种情况:
1. 未提交,不可见;
2. 视图创建后提交,不可见;
3. 创建前提交,可见。
4. 自己更新,可见
下图 1 中的三个事务,事务 A 语句返回的结果,为什么是 k=1:
1. A 开始前,活跃事务 ID 99;
2. A、B、C 版本100、101、102,系统里只有这四个事务;
3.三个事务开始前,(1,1)这一行数据的 row trx_id 是 90。
事务 A 的视图数组就是 [99,100], 事务 B 的视图数组是[99,100,101], 事务 C 的视图数组是 [99,100,101,102]。
第一更新事务 C, (1,1) 成 (1,2)。 row trx_id 是 102,90 成历史版本。
第二更新事务 B, (1,2) 成 (1,3)。row trx_id是 101,102 成历史版本。
A 查时,B 没提交,(1,3) 已成当前版本。对 A 不可见。
101(找 (1,3) 时)、102比高水位大,红色区域,不可见;
(1,1),它的 row trx_id=90,比低水位小,绿色区域,可见。
A 什么时候查询,结果都是一致的,为一致性读。
去掉数字对比后,用时间先后判断
(1,3) 还没提交,属于情况 1,不可见;
(1,2) 虽然提交了,但是是在视图数组创建之后提交的,属于情况 2,不可见;
(1,1) 是在视图数组创建之前提交的,可见。
三、当前读
“当前读”(current read):更新先读后写,读当前值
1、更新:事务 B 的 update 语句,如果按照一致性读,好像结果不对?
图 5 中, B 先生成,C 后提交,应该看不见 (1,2) 吗,怎算出 (1,3) 来?
如B 更新前查,k =1。更新不在历史版本上,在(1,2)基础上操作。
B 查询时,自己版本号101,最新数据版本号也是 101,自己更新,直接使用, k = 3。
2、查询:除 update 外,select 加锁,也是当前读。
A 的查询语句 select * from t where id=1 加上 lock in share mode 或 for update,读到版本号101数据,k = 3。下面分别加读锁(S 锁,共享锁)和写锁(X 锁,排他锁)。
mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;
C 不是马上提交,变成C’,会怎样?
(1,2) 版本生成了,是当前最新版本。两阶段锁协议上场。C’没提交,就是(1,2) 写锁没释放。 B 当前读,读最新版本,必须加锁,等到C’释放,才能当前读。
3、可重复读怎么实现的?
核心是一致性读(consistent read);更新只能用当前读。当前行锁被占用,进入锁等待。
区别:
可重复读:开始创建一致性视图,之后共用;
读提交:每执行前都会重新算出新视图。
事务 A 、B 查到 k=2、3
“start transaction with consistent snapshot; 等效于普通start transaction。
小结
InnoDB 行数据有多个版本,每个数据版本有自己 row trx_id,每个事务或者语句有自己一致性视图。一致性读(普普通查询语句)会根据 row trx_id 和一致性视图确定数据版本的可见性。
1)可重复读:查询只承认在事务启动前提交数据;
2)读提交:查询只承认语句启动前提交数据;
3)当前读:读取已经提交最新版本。
为什么表结构不支持“可重复读”?表结构没有对应行数据和row trx_id,只能遵循当前读逻辑。ps:MySQL 8.0 把表结构放 InnoDB 字典里,以后会支持表结构可重复读。
问题
表结构和初始化语句为试验环境,可重复读。所有“ c 和 id 值相等的行” c 值清零,但发现改不掉情况。构造出情况,说明其原理。实际中有没有可能碰到这种情况?
mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, c) values(1,1),(2,2),(3,3),(4,4);
RR下,update前,所有c值修改,update t set c = id + 1。
所谓“乐观锁”。基于version字段对row cas式更新,version被其他事务抢先更新,自己事务中更新失败,trx_id没变成自身事务id,select还是旧值,出现“明明值没变可就是更新不了”。
解决方案:每次cas更新不管成功失败,结束当前事务。失败重起新事务,查询更新。
重起新事务:是否成功标准:affected_rows 是否等于预期值。
预期值本是4,实际业务中,匹配唯一主键,预期值是1。
MySQL一致性视图用这种宽松的update机制。其他大多内部实现cas,失败后下一步有区别。
评论1
transaction id=98事务A select(Q2)之前更新字段,A发现字段row trx_id=98,比自己up_limit_id,A不能获取到transaction id=98更新后值。
评论2
A、B两个用户,互相喜欢,则成为好友。like表,friend表,like:user_id、liker_id设置为复合唯一索引uk_user_id_liker_id。语句执行顺序是这样的:
以A喜欢B为例:
1、先查询对方有没有喜欢自己(B有没有喜欢A)
select * from like where user_id = B and liker_id = A
2、如果有,则成为好友:insert into friend
3、没有,则只是喜欢关系:insert into like
如A、B同时喜欢对方,会出现不会成为好友的问题。因为上面第1步,双方都没喜欢对方。1步用排他锁也不行,记录不存在,行锁不生效。mysql锁层如何处理?
like增加列,userid 喜欢 likeid=1,2相反。再向like表插入,比较userid与likeid ascii值大小,小的放前面,如果userid与likeid交换,列r值为2。
互相喜欢时,后插入like表的唯一联合主键索引就会提示冲突,插入失败,这个时候将他们插入firend表即可
评论3
开启事务时,保存活跃事务的数组(A),获取高水位(B)。A和B之间会不会产生新的事务?如果产生,对于当前可见,不管有没有提交?
锁保护下做,原子操作,期间不能创建事务。
评论4
A B启动时up_limit_id=99
B update 每行row_trx_id=101
A update 每row_trx_id=100
select(2)是RR,找row_trx_id<=101版本返回,优先找到版本为100的,没有取到自己的更新。
不对!A的update被行锁锁住的,B结束才释放,不存在B update后,A更新。
评论5(重点总结)
1.innodb支持RC和RR隔离级别实现是用的一致性视图(consistent read view)
2.事务启动快照,基于整个库.
事务内,整个库修改对于该事务都是不可见的(对于快照读的情况)
3.事务如何实现的MVCC呢?
(1)每个事务都ID,叫transaction id(严格递增)
(2)启动时,找到已提交最大事务ID记为up_limit_id。
(3)更新时,比如id=1改为了id=2.把id=1和该行之前row trx_id写到undo log里, 修改这条语句的transaction id记在该行行头
(4)查时, up_limit_id>=transaction id,可以看.up_limit_id<transaction id,undo log里取。去undo log查找数据的时候,也需要做比对,必须up_limit_id>transaction id,才返回数据
4.什么是当前读,由于当前读都是先读后写,只能读当前的值,所以为当前读.会更新事务内的up_limit_id为该事务的transaction id
5.为什么rr能实现可重复读而rc不能,分两种情况
(1)快照读的情况下,rr不能更新事务内up_limit_id,
而rc每次会把up_limit_id更新为快照读之前最新已提交事务的transaction id,则rc不能可重复读
(2)当前读的情况下,rr是利用record lock+gap lock来实现的,而rc没有gap,rc不能可重复读
评论6
事务A和B, A更新c=0 条件:
1、update已成功, 没其他活动事务修改,条件为id in 1,2,3,4或c in 1,2,3,4, 否则被锁阻塞;
2、A再次执行查询结果却一样
答:B把id或者c给修改了,已提交, A“当前读”没有匹配的条件; A查询在B更新(且提交)后
1,A select * from t;
2, B update t set c = c + 4; // 只要c或者id大于等于5就行; 当然这行也可以和1调换, 不影响
3, 事务B commit;
4, 事务A update t set c = 0 where id = c; // 当前读; 此时已经没有匹配的行
5,事务A select * from t;