事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。事务只和 DML语句(insert、update、delete)有关。

什么是事务?

将一组SQL语句放在一个批次中去执行。要么全部成功,要么全部失败!

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_java


 ✦ 解析:A给B转账200元,第一步执行:A -200。 第二步执行:B +200,如果第一步执行完毕,还没来得及执行第二步,此时银行系统故障,则结果未  A 少了200元,但是B没有收到钱,所以就需要事务,将扣除你的100元再返回给你,就是这么简单。

事务控制语句

a. 开关事务

  • 要显式地开启一个事务务须使用命令 begin 或 start transaction
  • 或者执行命令 set autocommit = 0,禁止mysql事务的自动提交
-- 1.关闭事务
set autocommit = 0;

-- 2.开启事务(mysql默认,即执行sql语句后就会马上执行 commit 操作)
set autocommit = 1;

-- 3.查看当前库是否开启了事务自动提交   ON:开启(默认)    OFF:关闭中
show variables like 'autocommit%';

注意:autocommit参数是针对连接的,在一个连接中修改了该参数,不会对其他连接产生影响。

b. 手动提交事务

手动优先原则。即使设置为自动提交 autocommit = 1,但仍可以手动介入,并以手动为准。

-- 1.开启一个事务
start transaction; 或者 begin;

-- 2.提交事务,持久化(成功)
commit; 或 commit work;

-- 3.回滚事务:回到事务前的数据状态(失败)
rollback; 或 rollback work;

c. 事务块的保存点

场景:如在某个事务块内执行了很多sql语句,突然有一条语句写错了,只能利用rollback语句,回滚全部事务,回到未开启前的数据状态,sql 全部都要重新执行

保存点(savepoint):开启事务后在执行语句后面打几个点,调用rollback语句可以回滚到指定的点。而不是回到事务开始前的样子。

-- 1.允许在事务中创建一个保存点,一个事务中可以有多个保存点
savepoint [保存点的名称];

-- 2.把事务回滚到标记点
rollback to savepoint [保存点的名称];

-- 3.删除一个事务的保存点
release savepoint [保存点的名称];

事务场景演示

例子:假设A余额1000元,B余额0元,执行A给B转账100元 操作,user表数据如下:

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_database_02

第一步:关闭自动提交      由于mysql 默认是开启事务自动提交的,先关闭自动提交

手动优先原则。可以不关闭自动提交,使用begin或start transaction 开启一个事务后,自动提交将被暂时禁用,直到使用commit 或 rollback 结束事务。事务结束后,回到启用状态

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_数据库_03

第二步:开启事务       使用begin 或 start transaction 语句开启事务,并执行转账语句

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_mysql的事务是为了解决什么问题的_04

!! 注意:此时虽然已转账成功了,并且数据已发生变化,但我们没有执行 commit 提交,意味着这个事务还没结束,如果我想取消这次转账呢?

第三步:回滚事务    使用 rollback

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_database_05

第四步:提交事务    一旦执行commit,数据将被永久保存到数据库中

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_database_06

  • 从 begin 到  commit  命令之间的一系列操作为一个完整的事务周期。
  • 手动开启一个事务,若不执行commit 命令,终止 MySQL 会话后,数据库将会自动执行回滚
  • 只有当执行 commit 命令后,数据的改变才会持久化的保存到数据库中
  • 如自动提交未关闭,begin手动开启一个事务后,自动提交将被暂时禁用,直到使用commit 或 rollback 结束事务。自动提交会恢复开启状态。

☁ 扩展:理解和使用 savepoint 保存点

现在数据,A有1000元,B有0元,我们进行A给B转账,每次转账100,分2次执行

第一步:执行第一次转账成功后(A有900元,B有100元),设置一个保存点,如下

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_java_07

第二步:继续执行第二次转账成功(A有800元,B有200元)

 

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_database_08

第三步:回滚数据,撤销第二次的转账

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_java_09

 

衡量事务四大特性
 

▎  原子性 (Atomicity)     

一个事务中的所有操作,要么都成功,要么都失败!

定义:如果事务中某条语句执行失败了,前面已经执行成功的语句,会回滚到未执行前的状态,就像这个事务从来没有执行过一样。

✧ 实现原理:undo log(回滚日志)

undo log作用:提供回滚 和 多个行版本控制(MVCC),是事务原子性和隔离性实现的基础。

逻辑格式的日志,在执行undo时,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,这一点是不同于redo log的。

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_java_10

解析

当事务对数据库进行修改时,InnoDB会生成对应的undo log日志。如果事务执行失败或者调用rollback,导致事务回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子;

每对一条记录做一次改动,就会产生1条或者2条 undo log。一个事务中可能有多个增删改SQL语句,一个SQL语句可能会产生多条 undo log,一个事务中的这些 undo log 会被从 0 开始递增编号,这个编号称为 undo no。

回滚原理

undo log是逻辑日志,记录的是sql执行相关的信息,当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:

  • 对于insert,回滚时会执行delete;
  • 对于delete,回滚时会执行insert;
  • 对于update,回滚时会执行一个相反的update;

InnoDB存储引擎中,undo log类型

  • insert undo log
  • update undo log

insert undo log是指在insert 操作中产生的undo log,因为insert操作的记录,只对事务本身可见,对其他事务不可见。故该undo log可以在事务提交后直接删除,不需要进行purge操作。

update undo log记录的是对delete 和update操作产生的undo log,该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。

undo log什么时候释放?

答:当事务提交后,undo log并不能立马被删除,提交时放入undo log链表,由purge线程判断是否有其他事务在使用undo段中表的上一个事务之前的版本信息,决定是否可清理undo log

▎  一致性 (Consistency)  

数据前后的完整性要保持一致

定义:指事务执行结束后,数据库的完整性约束未被破坏,数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的总和应该不变),保证数据的正确和一致性
 

☛ 实现一致性的措施包括

  • 数据库层面:必须要实现A(原子性)、I(隔离性)、D(持久性) 三大特性,才有可能实现一致性
  • 应用层面:例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致

➳ 结论:ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段

 

▎ 

多个事务并发操作时,事务之间互不干扰,处于隔离状态

定义:当多个用户同时访问数据库时,比如操作同一张表,数据库为每一个用户开启的事务具有隔离性,不会被互相干扰,各搞各的互不影响!

问题一:事务并发,带来什么问题?(脏读、不可重复读、幻读)

1. 脏读 (dirty read):当前事务A中可以读到事务B未提交的数据

  • 张三余额100,事务A、B 同时开启事务
  • 事务B执行修改,将张三的余额从100修改至200
  • 在B未提交事务前,A此时查询张三余额,得到的结果是:200

时间顺序

事务A

事务B


开始事务

开始事务


 修改张三余额 由原100 修改为 200元


查询张三余额,结果为:200【脏读】



提交事务

提交事务

➳ 结论:事务A、B交替执行,事务A被事务B干扰到了,事务A读取到事务B未提交的数据!

2. 不可重复读 (unrepeatable read):事务A中先后两次读取同一个数据,两次读取的结果不一样

脏读与不可重复读的区别:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。

  • 事务A先查询张三余额,结果为100
  • 事务B执行修改,将张三的余额从100修改至200,并提交了事务!
  • 事务A再次查询张三余额,发现变为了200

时间顺序

事务A

事务B


开始事务

开始事务


查询张三余额,结果为:100元(初始值)





修改张三余额,由原100修改为200元



提交事务


查询张三余额,结果为:200元【不可重复读】


提交事务

➳ 结论:事务A再次被事务B干扰到了,相同的查询语句,却返回了不同的数据。

3. 幻读 :select某记录是否存在,不存在则插入记录,执行 insert 时发现记录已存在,无法插入

不可重复读与幻读的区别:前者是数据变了,后者是数据的行数变了

  • 事务A:检测表中是否有 id 为 1 的记录,没有则插入,这是我们期望的正常业务逻辑
  • 此时事务B 先插入一条数据:{ id = 1 , name ="王五" },并提交了事务。
  • 接着,事务A执行插入语句{ id = 1 , name ="张三" },提示报错:表的主键冲突
  • 事务A再次查询user表记录,发现表里还是没有数据,但插入又提示报错

时间顺序

事务A

事务B


开始事务

开始事务


查询user表记录,结果:空(没有数据)





insert 一条数据(id = 1, name="王五")


执行 insert 语句(id = 1, name="张三")

提交事务


插入失败,报错:

error 1062:Duplicate entry “1” for key ‘primary’


查询user表记录,结果:空(没有数据)


提交事务

➳ 结论:幻读,并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。

!! 注意:目前网上的众多解释幻读的概念,都是说在一个事务内,执行两次select查询的结果不一样,即 select 1 得到 10 条记录,select 2 得到 11 条记录,这其实并不是幻读,这是不可重复读的一种,而且只会在 读未提交、读已提交 级别下出现,而在 mysql 默认的 可重复读 隔离级别是不会出现的!

问题二:如何解决事务并发的问题?

隔离性最终的目的是:多个事务同时并发的情况下,事务之间互不干扰。

隔离性最简单最直接的实现方式就是串行执行。让事务一个一个执行,前面的事务还没执行完,后面的事务就都等待。但这样的实现方式很明显并发效率不高,并不适合在实际环境中使用。

——那有没有更好的方式呢?

☛ 实现隔离机制

  • 读写锁:在读取数据前,对其加锁,阻止其他事务对数据进行修改。
  • MVCC多版本并发控制 :一次性快照读。不加任何锁, 通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot), 并用该快照提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本。

一、事务隔离级别

MySql使用不同的锁策略 / MVCC来实现四种不同的隔离级别。RR、RC的实现原理跟MVCC有关,RU和Serializable跟锁有关。

为解决事务并发问题,实现不同程度的并发控制,SQL的标准制定者提出了不同的隔离级别

隔离级别

脏读

不可重复读

幻读

Read Uncommitted

(读未提交)

存在

存在

存在

Read Committed

(读已提交)

-

存在

存在

Repeatable Read

(可重复读)默认

-

-

存在

Serializable

(可串行化)

-

-

-

二、数据库隔离级别配置相关命令

MySQL 8.0版本使用 transaction_isolation,低版本使用 tx_isolation 

***************************** 查看隔离级别 *****************************

-- 方式一:输入以下命令查看事务隔离级别,(其中transaction_isolation就是隔离级别)
mysql> show variables like 'transaction%';
+----------------------------------+-----------------+
| Variable_name                    | Value           |
+----------------------------------+-----------------+
| transaction_alloc_block_size     | 8192            |
| transaction_allow_batching       | OFF             |
| transaction_isolation            | REPEATABLE-READ |
| transaction_prealloc_size        | 4096            |
| transaction_read_only            | OFF             |
| transaction_write_set_extraction | XXHASH64        |
+----------------------------------+-----------------+

-- 方式二:使用sql查看(该语句只能使用命令窗口)
--  SELECT @@global.transaction_isolation;  查看全局
--  SELECT @@session.transaction_isolation;  查看当前会话
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+





***************************** 设置隔离级别 *****************************

-- 语法:set [会话 | 全局] transaction isolation level [隔离级别]

-- 设置当前会话,隔离级别为:读未提交 read uncommitted
set session transaction isolation level read uncommitted ;

-- 设置全局会话,隔离级别为:读未提交 read uncommitted
set global transaction isolation level read uncommitted ;

三、命令演示:各个隔离级别事务之间的数据变化

!! 注意:每演示一个级别前,记得先修改数据库为对应的隔离级别!

✧ 读未提交(Read Uncommitted)

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_database_11

➳ 结论:读未提交,一个事务会读到其他事务未提交的数据,即存在脏读问题。事务B都没执行commit 提交到数据库,就被事务A就读到了其修改的数据。

✧ 读已提交(Read Committed)     解决:脏读

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_database_12

➳ 结论:读已提交。当前事务A只能读取到事务B已经commit 提交的数据。
 

✧ 可重复读(Repeatable Read)     解决:脏读、不可重复读

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_acid_13

 ➳ 结论:可重复读。只要当前事务A未结束,则A在事务期间每次读的数据都跟第一次读到的结果一致,即便其他事务已经修改了数据,并commit提交到了数据库。

☛ RR级别存在幻读

场景一:

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_数据库_14

流程解析:

  1. 事务A,检测表中是否有记录,没有则插入id为1的纪录,这是我们期望的正常业务逻辑。
  2. 事务B,在A插入前,先插入一条数据,并提交事务,目的在于扰乱A的正常的事务执行。
  3. 在 RR 隔离级别下,step1、step2 是会正常执行的,step3 则会报错主键冲突!

A发生了幻读,因为A在 step1 中读取的数据状态并不能支撑后续的业务操作。

A:“见鬼了,表里不是没有记录吗???” ,于是A又执行了 step4,发现和 setp1 读取的结果是一样的(RR下的 MVCC机制)。A无论读取多少次,都查不到 id = 1 的记录,但它的确无法插入这条通过读取,而认定不存在的记录(此数据已被B插入)

串行化隔离级别下,step1 执行时是会隐式的添加 行(X)锁 / gap(X)锁的,从而 step2 会被阻塞,step3 会正常执行,待A提交后,B才能继续执行(主键冲突执行失败),对于A来说他前期读取的结果是可以支撑其后续业务的。

场景二:RR级别防止幻读:加排他锁或共享锁

!! 原理加锁读时不会采用MVCC!由于锁的特性,其他事务也无法修改;保证读取最新!

RR级别解决幻读,需显式加锁,因为RR级别下的事务对被读取的数据不加锁——快照读;加入for update后,对当前读取的数据加(X)行锁,变为当前读;   注意:X 表示排他锁

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_java_15

流程解析:

  1. 事务A,查询user表所有数据。(step1)
  2. 事务B,插入一条数据,并提交事务。(step2)
  3. 事务A,再次查询user表数据,结果与第一次查询一致,实际库中记录已改变(step3)
  4. 利用for update,对该记录加排他锁,并转为当前读,期间其他用户阻塞等待(step4)

场景三:RR级别防止幻读:不存在的记录是否会加锁?

!! 注意:InnoDB行锁,锁定的是记录所对应的索引,如数据存在加x行锁,不存在加间隙锁

假设现在 user表数据为空,没有任何记录,如下:

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_数据库_16

☛ 演示 id = 1 的记录不存在的场景:

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_acid_17

流程解析:

  1. 事务A,查询 id =1 的记录,利用for update,对该 “记录” 加锁
  2. 事务B,插入一条数据失败,原因:等待 step1中事务A释放该“记录”的锁,等待超时

注意:InnoDB 的行锁(gap锁是范围行锁,一样的)锁定的是记录所对应的索引。

即便数据id=1 不存在,当前事务也会获得一把记录锁,InnoDB的行锁,锁定的是记录所对应的索引。如数据存在就加(X),不存在加 next-lock key / gap 锁(范围行锁),其他事务则无法插入此索引的记录,故杜绝了幻读。 

select .. for update   

对读取的行加 X 排它锁,其他事务不能对该行再加任何锁。(串行化隔离级别下会隐式做此操作)

select .. lock in share mode 

对读取的行加 S 共享锁。其他事务可对该行继续加 S 锁,但是不允许添加 X 锁,否则会被阻塞住

加锁读在查询时会对查询的数据加锁(共享锁或排它锁)。由于锁的特性,当某事务对数据进行加锁读后,其他事务无法对数据进行写操作,因此避免脏读和不可重复读。

避免幻读,则需通过next-key lock。行锁的一种,实现相当于record lock(记录锁) + gap lock(间隙锁);其特点是不仅会锁住记录本身(record lock的功能),还会锁定一个范围(gap lock的功能)因此,加锁读同样可以避免脏读、不可重复读和幻读,保证隔离性。

 

✧ 串行化(Serializable)     解决:脏读、不可重复读、幻读

可串行化强制事务串行,并发效率很低,只有当对数据一致性要求极高且可以接受没有并发时使用,因此使用也较少。

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_acid_18

 ➳ 结论:串行化。当前A不结束事务,则在A的事务期间,事务B对数据执行写操作,会进入阻塞状态,直到A结束事务。事务B对表的写操作,在等事务A的读操作。读写都不允许并发。

 

四、保证隔离性:锁机制

RR、RC的实现原理跟MVCC有关,RU 和 Serializable 跟锁有关

★ 读未提交(Read Uncommitted)    采取的是读不加锁原理

  • 事务读不加锁,不阻塞其他事务的读和写
  • 事务写阻塞其他事务写,但不阻塞其他事务读;

★ 串行化(Serializable)      读加共享锁,写加排他锁, 读写互斥

  • 所有SELECT语句会隐式转化为 select...for share,即加共享锁。
  • 读加共享锁,写加排他锁,读写互斥。如果有未提交的事务正在修改某些行,所有select这些行的语句都会阻塞。

五、保证隔离性:MVCC一次性快照读

RR、RC的实现原理跟MVCC有关,RU和Serializable跟锁有关

在了解mvcc多版本并发控制前,我们先了解两个概念,什么是一致性锁定读 和 一致性非锁定读

☛ 一致性锁定读

所谓一致性锁定读,就是加锁读,对读取的行进行加锁。读期间其他事务无法对该数据行进行修改;保证读取最新和数据逻辑的一致性!

InnoDB 引擎对读操作支持两种加锁方式: select .. for update  和 select .. lock in share mode ,加 X排它锁 或加 S共享锁,保证读取到的行是最新的。! 注意:加锁读时不会采用MVCC!

☛ 一致性非锁定读

每行记录可能存在多个历史版本,多版本之间串联起来形成了一条版本链,这样不同时刻启动的事务可以无锁地访问到不同版本的数据。

一致性非锁定读就是读快照。快照:当前行数据的历史版本,每行记录可能存在多个历史版本,也就是多份快照数据——行多版本技术

由于一个行记录可能存在多个快照(历史版本),从而引发一系列的并发问题,如何解决这些并发问题,就是所谓的多版本并发控制(MVCC)。非锁定读机制极大地提高了数据库的并发性。

!!只有在事务隔离级别RR、RC下,InnoDB 存储引擎才会使用 一致性非锁定读

✎ 一致性非锁定读:RC表现形式

  • 在 读已提交 事务隔离级别下,总是读取行的最新版本;如果行被锁定了,非一致性读不会因此去等待行上锁的释放,而是去读取该行版本的最新的一个快照。

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_database_19

 ✎ 一致性非锁定读:RR表现形式

  • 在 可重复读 事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时,第一次读到的行数据版本

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_acid_20

通俗理解:假设事务A第一次读到的是 快照数据1,接着事务B修改数据并提交事务,产生了新的快照数据,事务A第二次读的时候,还是读取快照数据1,而不是去读最新的快照数据

5.1 undo log 版本链

InnoDB并不会去开辟空间存储多个版本的行记录 ,mvcc的实现是借助 undo log 来完成的

B+ 索引树上对应的记录只会有一个最新版本,只不过 InnoDB 可以根据 undo log 得到数据的历史版本,从而实现多版本控制。

 

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_java_21

 

★ 5.1.1 实现原理 

InnoDB 存储引擎中的每条行记录都维护了两个隐藏的字段

  •  trx_id :最近更新这条行记录的事务id
  •  roll_pointer :指向之前生成的 undo log

步骤一:假设 id=100 的事务 A 插入一条记录,则 trx_id = 100,roll_pointer 指向空的undo log,因为这是一条新的数据,不存在历史版本

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_database_22

步骤二:接着,id = 200 的事务 B 修改了这条行记录,把 age 从18修改为20,于是这条行记录的 trx_id 就变成了 200, roll_pointer 就指向事务 A 生成的 undo log 

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_acid_23

步骤三:最后,id = 300 的事务 C 也来修改这条行记录,把age从20修改为30,如下:

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_database_24

结论 :每次修改记录都会更新 trx_id 和 roll_pointer 字段的值,该条数据的历史版本对应的undo log 会通过 roll_pointer 指针串联起来,从而形成一个版本链

★ 5.1.2 回滚日志(undo log) 类型

!! 注意:select操作不会生成 undo log,不管有无提交事务,一旦执行inser/update/delete,都会产生一条undo log日志。

在 InnoDB 存储引擎中,undo log 只分为两种:

  • insert undo log:在 insert 操作中产生的 undo log
  • update undo log:对 delete 和 update 操作产生的 undo log

MVCC 机制,主要靠 update undo log 实现,对于insert操作产生的insert undo log, 只对事务本身可见。所以也不存在并发的问题。故该undo log可以在事务提交后直接删除。

update undo log记录的是对delete 和update产生的undo log,该undo log可能需提供MVCC机制,故事务提交后不会立马删除。而是放入undo log链表中,等待purge线程判断是否可删除

5.2 ReadView (读视图) 机制

作用:用来判断当前事务能够看见哪些版本的

主要由以下部分组成:

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_数据库_25

★ 5.2.1 ReadView生成时间

在InnoDB中,begin/start一个事务并不会立即分配事务id,而是真正执行了操作才会分配事务id。例如事务A和事务B根据下图时间线执行:

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_java_26

✦ 解析 :虽然事务A先begin,但它执行select时能看到事务B提交的数据,因为事务在begin的时候没有真正开始一个事务,事务A的 readview 是在select 时生成的,此时事务B对数据修改的版本快照,按规则来说:trx_id < min_trx_id,属于已提交事务生成,所以对于事务A来说可见。

▶ 流程示例:事务并发操作时读取的ReadView变化

  1. 事务A 插入一条 id=1的数据 (已提交事务)
  2. 事务B第一次查询 id =1  (查询 1中事务A已提交的数据)
  3. 事务C修改 id =1的数据
  4. 事务B第二次查询 id =1  (查询 3中事务C修改了但未提交的数据)
  5. 事务C第一次查询 id =1  (查询 3中自己修改的数据)

注意:事务A已提交事务,事务B、C同时并发执行,但不提交

步骤一: 数据插入并提交数据

假设表中已被事务 A(id = 100)插入了一条行记录,并完成commit提交

此时有两个事务 B (id = 200) 和 C (id = 300) 过来并发执行

  • 事务B:查询id = 1的记录
  • 事务C:修改这行数据

注:这两个事务都执行了相应的操作,但都还没有提交事务,如图:

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_acid_27


 

步骤二:生成ReadView

事务B开始执行select, 此时会生成一个 ReadView,数据如下:(因为事务B、C并发begin开始事务,所以两者都存在活跃事务ids中)

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_mysql的事务是为了解决什么问题的_28

步骤三:事务B第一次查询   查询事务A已提交的数据

事务B执行第一次查询,会把这行数据的 trx_id 和快照中的 min_trx_id

Row. trx_id  < ReadView. min_trx_id

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_数据库_29

步骤四:事务C修改,更新隐藏字段 trx_id、roll_pointer

接着事务C执行修改这条记录,把 age=18 修改为20,故这行记录的 trx_id 更新为300, 同时 roll_pointer 指向了事务 C 修改之前生成的 undo log,如图:

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_mysql的事务是为了解决什么问题的_30

步骤五:事务B第二次查询    查询事务C修改了但未提交的数据

事务B执行第二次查询,发现该行数据的 trx_id=300,大于ReadView 快照中的最小活跃事务min_trx_id (200),且小于下一个事务自增id  max_trx_id (301)

最小活跃事务200——数据行的trx_id 300——下一个事务自增id 301

结论:说明更新了这行数据的事务,可能存在当前快照的活跃事务 m_ids {200、300} 中

故事务 B 会去判断所有活跃事务m_ids 里是否存在 trx_id=300 的事务,存在说明 id = 300 的事务是跟自己(事务 B)在同一时间并发执行的,则该 age=20的数据不能被事务B所查询到:

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_acid_31

——那么该如何查询? 

根据RR或RC级别,事务C虽修改了数据但未提交事务,事务B是不能查到事务C其修改的值

事务B会根据undo log 版本链排查,根据该行数据的 roll_pointer指针往下找,就会找到最近的一条 trx_id=100 的 undo log,而自己的 id 是 200,说明 trx_id = 100 的 undo log 版本是在事务 B 开启之前就已经提交了的。故事务 B 的这次查询读到的就是该版本undo log的数据,即 age = 18。

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_数据库_32

步骤六:事务C第一次查询    查询自己修改的数据

步骤六之前的例子只涉及到了 ReadView 中的前三个字段,而 creator_trx_id

事务C查询 id=1的记录,会生成一份ReadView,发现 trx_id=300 与 ReadView 里的 creator_trx_id=300 一样,说明这是事务C刚刚自己修改的,是可以被查询到的,即age=20

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_mysql的事务是为了解决什么问题的_33

答:RC级别下可以读到,RR级别不可以(加锁读除外)

一、图解演示:RC级别

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_acid_34

解析:事务D修改数据提交事务后,事务C执行select查询,重新生成了一份ReadView快照,发现最近更新了id=1这条记录的 trx_id =400,大于最小活跃事务min_trx_id (200) ,且小于下一个事务自增的max_trx_id (401) 

当前快照中最小活跃事务id  <= 最近更新该行数据的事务id 

说明可以通过当前快照得到是否可见,继续判断:trx_id =400是否存在活跃事务m_ids

  • 存在:说明该条记录是已开始但没提交的事务修改的,当前事务C不可见
  • 不存在:说明这条记录对于事务C来说,是已提交的事务生成的,当前事务C可见

➳ 结论:根据RC级别机制,事务C可以读取到事务D所做的修改 age=30的记录

二、图解演示:RR级别

mysql的事务是为了解决什么问题的 mysql实现事务的原理(mvcc)_数据库_35

解析:事务D修改数据提交事务后,事务C查询这行记录时,发现最近更新了id=1这条记录的 trx_id =400,大于下一个事务自增的max_trx_id (301) ,这说明事务C执行期间,有另外一个事务更新了数据(可理解为 "将来" 的事务操作的),所以该条数据对于事务C不可见。

➳ 结论:根据RR级别机制,事务C不可以读取到事务D所做的修改 age=30的记录

5.3  通过 undo log 版本链和 ReadView 机制

对于RR、RC这两个隔离级别,最根本的不同之处,在于它们生成 ReadView 的时机不同

  • 可以保证一个事务不会读到并发执行的另一个事务的更新
  • 可以保证一个事务只可以读到该事务自己修改的数据或该事务开始之前的数据

六、隔离性总结

InnoDB实现的RR,通过锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)等,实现了一定程度的隔离性,可满足大多数场景的需要

1、RR 级别作为 mysql 事务默认隔离级别,是事务安全与性能的折中,可能也符合二八定律(20%的事务存在幻读的可能,80%的事务没有幻读的风险)。

2、serializable 级别悲观的认为幻读时刻都会发生,故会自动的隐式的对事务所需资源加排它锁,其他事务访问此资源会被阻塞等待,故事务是安全的,但需要认真考虑性能。

3、InnoDB 的行锁锁定的是索引,而不是记录本身,故某索引相同的记录都会被加锁,会造成索引竞争,建议尽可能的使用主键或唯一索引对记录加锁。索引映射的记录如果存在,加行锁,如果不存在,则会加 next-key lock / gap 锁 / 间隙锁

 

▎ 

事务一旦提交则不可逆,被持久化到数据库中

定义:事务一旦被提交,对数据库中的改变就是永久性的,即便是在数据库发生了故障,也不会对其数据有任何影响。

✧ 实现原理:redo log(重做日志)

重做日志(Redo Log)用来实现事务的 持久性(Duration),记录每次操作上页的物理修改。

☛ 缘起:InnoDB作为MySQL的存储引擎,数据存放磁盘,每次读写数据都需磁盘IO,效率很低。

⚘ 结果:InnoDB提供了缓存(Buffer Pool),作为访问数据库的缓冲

  • 读:先从Buffer Pool中读取,如Buffer Pool中没有,则从磁盘读取后再放入Buffer Pool
  • 写:先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(此过程称为刷脏)

➳ 结论:Buffer Pool的存在提高了读写数据的效率,但也存在新的问题:如果MySQL宕机,而此时缓存中修改的数据还没刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。

☑ 解决:redo log,当数据修改时,既修改Buffer Pool中的数据,还会记录到redo log;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log日志,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。

☁ 思考 :既然redo log也需在事务提交时将日志写入磁盘,为何比直接将Buffer Pool中修改的数据写入磁盘 (即刷脏) 要快呢?

主要有以下两方面的原因

(1)刷脏是随机IO,每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。

(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。

1、什么时候产生?

事务开始后就产生redo log,redo log的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo log文件中。

2、什么时候释放?

对应事务的脏页写入到磁盘后,重做日志占用的空间就可以重用(被覆盖)

MySQL 事务使用注意

1)  事务尽可能简短

事务的开启到结束会在数据库系统中保留大量资源,以保证事务的ACID特性。在多用户系统中,较大的事务会占用系统的大量资源,从而影响软件的运行性能,甚至导致系统崩溃。

2)  事务中访问的数据量尽量最少

当并发执行事务处理时,事务操作的数据量越少,事务之间对相同数据的操作就越少。

3)  查询数据时尽量不要使用事务

查询操作并不会更新数据库的数据,应尽量不使用事务查询数据,避免占用过量的系统资源。

4)  在事务处理过程中尽量不要出现等待用户输入的操作

在处理事务的过程中,如需等待用户输入数据,则事务会长时间地占用资源,可能造成系统阻塞。