08 | 事务到底是隔离的还是不隔离的?(评论2没理解)

一、可重复读

事务启动创建视图,执行期修改数据,事务看到和启动时一样。隔离级别下执行事务,与世无争,不受外界影响

例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);

图 1 事务 A、B、C 的执行流程

注意事务启动时机。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

图 2 行状态变更图

 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]

图 4 事务 A 查询数据逻辑图

第一更新事务 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) 来?

图 5 事务 B 更新逻辑图  

如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’,会怎样?

图 6 事务 A、B、C'的执行流程

(1,2) 版本生成了,是当前最新版本。两阶段锁协议上场。C’没提交,就是(1,2) 写锁没释放。 B 当前读,读最新版本,必须加锁,等到C’释放,才能当前读。

图 7 事务 B 更新逻辑图(配合事务 C')

3、可重复读怎么实现的?

核心是一致性读(consistent read);更新只能用当前读。当前行锁被占用,进入锁等待。

区别:

可重复读开始创建一致性视图,之后共用

读提交:每执行前都会重新算出新视图

事务 A 、B 查到 k=2、3

“start transaction with consistent snapshot; 等效于普通start transaction。

图 8 读提交隔离级别下的事务状态图

小结

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;

你可能感兴趣的:(08 | 事务到底是隔离的还是不隔离的?(评论2没理解))