事务
基本要素
-
原子性
-
一致性
-
隔离性
- 同一时间只允许同一事务请求同一数据,不同事务之间彼此没有任何干扰
-
持久性
- 事务完成后,事务对数据库的所有更新将会被保存到数据库
事务的并发问题
脏读
-
概念
- 事务A读取事务B未提交的数据,结果事务B回滚,那么事务A则读到的为脏数据
-
解决办法
- 事务A只读取其他事务已提交的数据,每个 select 语句都有自己的一份快照,而不是一个事务一份,所以在不同的时刻,查询出来的数据可能是不一致的,所以说RC级别下的快照是每个select都一份,而RR情景下是事务开始就会读取快照,持续在整个事务期间内,
-
对应隔离级别
不可重复读
-
概念
- 事务A对数据进行多次读取操作,事务B对事务A读取的内容进行了更新操作并提交,此时事务A在这个期间读到的数据可能出现内容不一致的情况
-
解决办法
- 可重复读隔离级别下,事务A发起时,事务在启动的时候就”拍了个快照“。注意,这个快照是基于整个库的,后续的读操作都是快照读,都是读己之所得,所以不会读到其他事务所修改的内容,也就无法产生不可重复读
-
对应隔离级别
幻读
-
概念
- 事务A对某个范围的区间数据进行修改时,事务B新增或删除了这个范围内的数据并提交,从而导致事务A针对这个范围内的数据条数进行统计时发现多了一条未修改的数据
-
解决办法
- repeatable read隔离级别下,只有快照读会产生幻读现象,当前读已经通过gap锁的引入消除了幻读现象
-
对应隔离级别
- (1)串行化(serializable)
- (2)repeatable read隔离级别下,一个事务中只使用当前读,或者只使用快照读都能避免幻读
不可重复读和幻读的比较
- 总结:不可重复读针对的是重复读中数据内容的异常场景,幻读是针对的数据条数的异常场景,所以说不可重复读是针对修改的场景,幻读是针对新增和删除的场景造成的数据条数的改变
当前读和快照读的比较
-
快照读
-
实际场景
- 解决了不可重复读
-
概念
-
好处
- 并发读写时能够做到读操作不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库的读写能力
- 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
-
MySQL锁的组合
- MVCC+悲观锁(NextKey-LOCK):MVCC解决脏读、不可重复读、幻读,悲观锁解决写写冲突(丢失更新)
- MVCC + 乐观锁:MVCC解决脏读、不可重复读、幻读,乐观锁解决写写冲突(丢失更新)
-
MVCC实现方式
- 通过Read View (三个隐式字段)+undo log实现

MVCC具体是如何实现的呢?
实际上,InnoDB会在每行记录后面增加三个隐藏字段:
(1)DB_ROW_ID:行ID,随着插入新行而单调递增,如果有主键,则不会包含该列。
(2)DB_TRX_ID:记录插入或更新该行的事务的事务id,注意是该条记录链表的链头节点的事务id,也就是最新的版本的事务id

(3)DB_ROLL_PTR:回滚指针。指向undo.log记录,每次对某条记录进行修改时,改列会保存一个指针,可以通过这个指针找到记录修改前的信息。当某条记录被修改多次时,该行记录会存在多个版本,并通过回滚指针形成版本链
以RR为例,每开启一个事务,会分配一个事务id,在该事务执行第一个Select语句时,会生成一个当前时间点的ReadView(快照),主要包含如下属性:
(1)trx_ids:生成快照时,系统当前存在的活跃事务id,就是未提交的事务
(2)up_limit_id:trx_ids的最小值,trx_id低于该值的都能被看到
(3)low_limit_id:当前事务id的最大值,生成ReadView时候要系统分配给下一个事务的id值,例如当前事务id的最大值为5,那么low_limit_id的值就为5+1=6
(4)creator_trx_id:生成该事务的事务id
有了这个ReadView(快照),下次访问的时候就能够按照如下步骤判断该版本的数据是否可见
(1)如果访问版本trx_id与当前事务id相同,则代表当前事务访问他修过的记录,允许看到
(2)如果trx_id小于up_limit_id的值。表明生成该数据的事务在当前事务之前就已提交,允许看到
(3)如果trx_id大于low_limit_id的值。表明生成该数据的事务在当前事务之后才开启,所以该版本下的数据不允许访问
(4)如果trx_id的值再up_limit_id和low_limit_id之间,再看trx_id是否在trx_ids中,不存在则允许访问,存在则不允许
判断时候,会从版本链的后往前判断,直到满足条件的trx_id并进行展示,如下图代码所示

对于删除,其实是一种特殊的更新,InnoDB用一个特殊的值delete_bit标识是否删除,判断时查看是否被标记,如果是则跳过该版本。再判断下个版本
-
当前读
-
实际场景
-
解决了幻读(SQL 标准中规定的 RR 并不能消除幻读,但是 MySQL 的 RR 可以,靠的就是 Gap 锁,在 RR 级别下,Gap 锁是默认开启的,而在 RC 级别下,Gap 锁是关闭的)
-
概念
- 读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁,当然,获得什么锁取决于当前事务的隔离级别、语句的执行计划、查询条件等因素
select * from .... where ... lock in share mode
update .... set .. where ...
delete from. . where ..
insert..
-
实现方式
-
结论
- 纯当前读和纯快照读都能够解决RR下的幻读,但是快照读下使用了当前读则无法解决幻读,因为当前读会更新快照数据,导致幻读的异常
例如:

幻读/不可重复读的危害
不可重复读
假设存在一个表A,其中包含用户信息和存款字段,现在有个业务场景需要针对表A中存款大于<1000的用户发个纸巾,针对存款在1000-2000的用户发个礼品茶杯,此时这个场景就可能出现下图这给一个用户重复发奖的场景
时刻 | 事物A | 事物B |
T1 | 查询表A中存款>500的用户送纸,查出用户小明 | |
T2 | | 小明存了1000块 |
T3 | 查询表A存款>1000的用户送茶杯,也查出小明 | |
可以看出,我们给小明即送出了纸,也送出了茶杯,可以看出在不可重复读的场景,验证违背了数据的一致性(完整性约束),我们可以看出我们产生异常的主要原因是同一事物中,同一记录返回了不同的结果
搜索不可重复读,基本都是类似下面这个描述
不可重复读,是指在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据。
这个描述有错吗?没错。错在后半句这个描述只是一个子集。正确的描述应该为
不可重复读,是指在数据库访问中,一个事务范围内两个查询的相同记录却返回了不同数据。
原文链接:https://blog.csdn.net/qq_24054301/article/details/123086098
幻读
假设存在一个业务场景,还是结合上述场景,存在一个表A,其中包含用户信息和存款字段,其中多了一个积分字段,要求送完茶杯之后扣除积分,送纸无需扣除
此时流程可能如下
时刻 | 事物A | 事物B |
T1 | 查询表A中存款>500的用户送纸,查出用户小明 | |
T2 | | 小明存了1000块 |
T3 | 查询表A存款>1000的用户,未查出用户 | |
T4 | 扣减update 存款>1000的用户,此时扣了小明的积分 | |
问题很明显:只给小明发了纸,但是却扣减了小明的积分
幻读的业务问题就很明显了,当你select出了n条数据去进行业务处理,此时针对n条用户处理完成后执行更新操作,此时可能会造成更新大于或者小于n条数据,造成不符合业务预期
实际开发中,如何避免幻读?
如何避免幻读:
- 事物只执行快照读,不进行当前读(update/insert/delete/上锁)操作
- 事物如果要执行当前读操作,先进行当前读(update/insert/delete/上锁),因为这个场景可以产生间隙锁保护数据
四种隔离级别

思维导图

参考链接:
https://zhuanlan.zhihu.com/p/106883029?from_voters_page=true
https://blog.csdn.net/w892824196/article/details/109701966
https://blog.csdn.net/qq_44766883/article/details/105879308
https://www.jianshu.com/p/8845ddca3b23