-
MySQL 中的事务
- 前言
- 原子性
- 一致性
- 持久性
-
并发事务存在的问题
- 脏读
- 幻读
- 不可重复读
-
隔离性
- 事务的隔离级别
-
事务隔离是如何实现
- 可重复读 和 读提交
- 串行化
- 读未提交
MySQL 中的事务
前言
A(Atomic,原子性:指的是整个数据库事务操作是不可分割的工作单元,要么全部执行,要么都不执行;
-
如果事务执行过程中,每个操作失败了,系统可以撤销事务,系统可以撤销事务,返回系统初始化的状态。
数据的完整性: 实体完整性、列完整性(如字段的类型、大小、长度要符合要求)、外键约束等;
D(durability, 持久性: 指的是一旦数据提交,对数据库中数据的改变就是永久的。即使发生宕机,数据库也能恢复。
原子性
在对数据库进行修改的时候,就会记录 undo log
,这样当事务执行失败的时候,就能使用这些 undo log
恢复到修改之前的样子。
undo log 进行事务的回滚,实际上做的是和之前相反的工作,对于每个 INSERT,InnoDB 会生成一个 DELETE ;对于 DELETE 操作,InnoDB 会生成一个 INSERT。通过反向的操作来实现事务数据的回滚操作。实现事务的原子性。
一致性
一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。
持久性
redo log
用来从保证事务的持久性。redo log 简单点讲就是 MySQL 异常宕机后,将没来得及提交的事物数据重做出来。
redo log 包括两部分:一个是内存中的日志缓冲(
redo log buffer
,另一个是磁盘上的日志文件(redo log file
。redo log buffer,后续某个时间点再一次性将多个操作记录写到
redo log file
。这种 先写日志,再写磁盘 的技术就是 MySQL 里经常说到的 WAL(Write-Ahead Logging 技术。redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为
crash-safe
。并发事务存在的问题
-
幻读:前后读取的记录数量不一致。
脏读:读到其他事务未提交的数据;
脏读
幻读
The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row。
简单的讲就是,幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
不可重复读
在一个事务内,多次读同一个数据。在这个事务还没有结束时,另一个事务也访问该同一数据并修改数据。那么,在第一个事务的两次读数据之间。由于另一个事务的修改,那么第一个事务两次读到的数据可能不一样,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读,即原始读取不可重复。
幻读和不可重复读的的区别
隔离性
事务的隔离级别
MySQL 中标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。
-
读提交:一个事务提交之后,它的变更才能被其他的事务看到;
-
串行化:这是事务的***别,顾名思义就是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读提交 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
串行化 | 不可能 | 不可能 | 不可能 |
可以看到,只有串行化的隔离级别解决了【脏读,不可重复读,幻读】这 3 个问题。
读提交 和 可重复读
create table user
(
id int auto_increment primary key,
username varchar(64 not null,
age int not null
;
insert into user values(2, "小张", 1;
来分析下下面的栗子
读未提交
读提交
可重复读
虽然事务1提交了,但是 V2 还是在事务2 中没有提交,根据可重复读的要求,一个事务执行的过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,所以 V2 也是 1。
串行化
事务隔离是如何实现
在了解了四种隔离级别,下面来聊聊这几种隔离级别是如何实现的。
Read View。
Read View 是一个数据库的内部快照,用于 InnoDB 中 MVCC 机制。
可重复读 和 读提交
undo log 日志版本链和 Read View
。
undo log
日志版本链undo log 是一种逻辑日志,当一个事务对记录做了变更操作就会产生
undo log
,里面记录的是数据的逻辑变更。
trx_id:每次对某条聚簇索引记录进行改动时,都会把对应的事务 id 赋值给 trx_id 隐藏列;
每次事务更新的时候,undo log
就会用 trx_id 记录下当前事务的事务 ID,同时记录下当前更新的数据,通过 roll_pointer 指向上个更新的旧版本数据,这样就形成了一个历史的版本链。
Read View
undo log 版本链会将历史事务进行快照保存,并且根据事务的版本大小,通过指针串联起来,对于 可重复读 和 读提交 这两种事务隔离级别,只需要在
undo log
中选择合适的事务版本进行数据读取,就能实现对应的读取隔离效果。undo log 版本链中,那个事务版本对当前事务可见,InnoDB 中通过
Read View
来解决,作用是事务执行期间用来定义“我能看到什么数据”。Read View 中有四个重要的字段
-
Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。
-
Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;
-
Read View 的事务的事务 ID。
Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。
Read View 可以在理解为一个数据的快照,可重复读
隔离级别会在每次启动的事务的时候生成一个 Read View
记录下当前事务启动瞬间,当前所有活跃的事务 ID。
Read View 的事务的事务 ID,会在 Read View
中根据事务 ID 大小,判断当前事务落在了那个区域,然后判断当前事务 ID 对应的数据快照是否可读。
-
未开始事务:
Read View
中应该给下一个事务的 ID,这部分的数据是不可见;
1、如果当前事务 ID 在未提交事务集合中,表示这个版本是由还没提交的事务生成的,不可见;
总结下来就是
Read View 时的事务 ID 会判断当前事务落在了 Read View
中那个区域中;
3、如果可读通过 undo log
版本链找到对应事务的快照数据,这就是目前该事物能够读到的数据;
undo log 版本链找到上个事务的版本,持续重复 1~3 的步骤,直到找到版本链中最后一个数据,如果最后一个版本的数据也是不可见,那就表示当前查询找不到记录。
可重复读 和 读提交
事务隔离级别的区别就在于创建 Read View
的时机不同,可重复读
事务隔离级别会在每次启动事务的时候创建 Read View
,读提交
会在每次查询的时候创建 Read View
。
可重复读事务隔离级别在事务开始创建了 Read View
,就能保证事务中的看到的数据一致了,而读提交
事务隔离级别在每次查询的时候,创建 Read View
,就能在每次查询的时候读到已经提交的事务数据。
可重复读隔离级别的读取过程
其中 V1 的查询结果是 3,V2 的查询结果是 1。
1、事务 1 开始前,系统里面只有一个活跃事务 ID 是 99;
3、三个事务开始前,id = 2
的 age 为 1 这一行数据的 trx_id 是 90。
Read View 分别是
id = 2 的 age 为 2。
id = 2 的 age 为 3。
Read View 中的未提交事务集合中,所以数据不可见,需要根据版本链寻找上一个版本。
trx_id=101,处于当前 Read View
的未开始事务中,所以数据不可见;
trx_id=102,同样处于当前 Read View
的未开始事务中,所以数据不可见;
trx_id=90,处于当前 Read View
的已提交事务中,所以数据可见;
1、版本未提交,不可见;
3、版本已提交,而且是在视图创建前提交的,可见。
数据更新
如果事务2在数据更新之前先去查询一次,那么看到的数据就是id = 2
的 age 为 1,但是更新数据,就不能在老的数据中更新了,否则事务3的更新数据就会丢失了。
这样更新的时候使用当前读,就保证了事务2拿到的最新的数据,所以更新完成之后的查询 id = 2
的 age 就为 3 了。
lock in share mode 或 写锁(X锁,排他锁)for update
。
select age from user where id=2 lock in share mode;
select age from user where id=2 for update;
如果当前事务3 的事务还没提交,这时候,事务2就开始了写入
什么是两阶段锁协议:在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:
在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。
串行化
读写都需要加锁,读的时候加读锁,写的时候加写锁。
读未提交
对写仍需要锁定,策略和读已提交类似,避免脏写。
可重复读解决了幻读吗
针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
select ... for update 等语句),是通过 next-key lock
(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update
语句的时候,会加上 next-key lock
,如果有其他事务在 next-key lock
锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
读操作,可重复读隔离级别会使用 MVCC 针对当前事务生成事务快照,通过对比事务版本,就能保证事务中的快照读。
Next-Key,MySQL 把行锁和间隙锁合并在一起,解决了并发写和幻读的问题,这个锁叫做 Next-Key
锁。
Next-Key 算法中,对于索引的扫描,不仅仅是锁住扫描到的索引,而且还锁着这些索引覆盖的范围。因此对于范围内的插入都是不允许的,这样就能避免幻读的发生。
create table user
(
id int auto_increment primary key,
username varchar(64 not null,
age int not null
;
insert into user values(2, "小张", 1;
insert into user values(4, "小明", 1;
insert into user values(6, "小红", 1;
insert into user values(8, "小白", 1;
上面的这两个事务,事务1 中的 select * from where age>4 for updated
会加上一个间隙锁 (4,6]
这样事务 2 中的插入就需要等待事务1中锁释放才能提交修改。
不过可重读真的就完全避免了幻读吗?下面来看两种异常的情况。
场景1
for updated 当前读,这就会出现读取的结果和第一次读取结果的不一致。
场景2
上面两种场景的最终原因,就是后面的查询间接或直接的使用了当前读,造成了数据的不一致,所以只需要在最开始的查询加上 for updated
,就能避免幻读的出现了。
总结
-
可重复读:MySQL 中默认的事务隔离级别,一个事务执行的过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,在此隔离级别下,未提交的变更对其它事务也是不可见的,此隔离级别基本上避免了幻读;
读未提交:一个事务还没提交时,它的变更就能被别的事务看到,读取未提交的数据也叫做脏读;
2、可重复读 是 MySQL 中默认的事务隔离级别;
undo log 日志版本链和 Read View
;
5、读未提交,读取最新的数据,读不用加锁,不用遍历版本链,直接读取最新的数据,不管这条记录是不是已提交。不过这种会导致脏读。对写仍需要锁定,策略和读已提交类似,避免脏写;
针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
select ... for update 等语句),是通过 next-key lock
(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update
语句的时候,会加上 next-key lock
,如果有其他事务在 next-key lock
锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
参考
【高性能MySQL(第3版】https://book.douban.com/subject/23008813/
【MySQL 实战 45 讲】https://time.geekbang.org/column/100020801
【MySQL技术内幕】https://book.douban.com/subject/24708143/
【MySQL学习笔记】https://github.com/boilingfrog/Go-POINT/tree/master/mysql
【MySQL总结--MVCC(read view和undo log)】https://blog.csdn.net/huangzhilin2015/article/details/115195777
【深入理解 MySQL 事务:隔离级别、ACID 特性及其实现原理】https://blog.csdn.net/qq_35246620/article/details/61200815
【分布式事务】https://mp.weixin.qq.com/s/MbPRpBudXtdfl8o4hlqNlQ