MySQL 06_MySQL事务详解

一、数据库事务概述

1.1 事务的基本概念

事务(transaction) 是一组逻辑操作单元,使数据从一种状态变换到另一种状态。通常一个事务对应一个完整的业务(例如:银行账户转账业务,该业务就是一个最小的工作单元)。

事务处理的原则:保证事务的所有操作都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的操作都被 提交(commit) 执行,那么这些修改就永久地保存下来;要么数据库管理系统将放弃所作的所有修改,整个事务 回滚(rollback) 到最初状态。也就是说一个事务中的所有操作要么全部成功执行, 要么完全不执行。

在数据库中 事务(transaction) 可以把多个SQL(操作)给打包到一起(组成一个工作单元),即将多个SQL语句变成一个整体,mysql 引擎要么会全部执行这一组 sql 语句,要么全部都不执行。

在数据库系统中,一个完整的业务(事务)需要批量的DML(insert、update、delete)语句共同联合完成,事务只和DML语句有关,或者说DML语句才有事务。这个和业务逻辑有关,业务逻辑不同,DML语句的个数不同。

1.2 事务的ACID特性

1、原子性(atomicity)

原子性(atomicity) 是指事务是一个不可分割的工作单位,事务中的所有操作要么全部提交成功执行,要么全部失败回滚,没有中间状态.。

原子性主要是通过 事务日志中的回滚日志(undo log) 来实现的,当事务对数据库进行修改时,InnoDB 会根据操作生成相反操作的 undo log,比如说对 insert 操作,会生成 delete 记录,如果事务执行失败或者调用了 rollback,就会根据 undo log 的内容恢复到执行之前的状态。

事务的原子性,也是事务的核心特性,是事务的初心。

2、一致性(consistency)

一致性(consistency) 是指事务执行前后,数据从一个合法性状态变换到另外一个合法性状态,即使中途发生了异常,也不会因为异常引而破坏数据库的完整性约束,比如唯一性约束等,这种状态是语义上的而不是语法上的,跟具体的业务有关。

3、持久性(durability)

持久性(durability) 是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,即使数据库宕机也不会丢失(通过事务日志中的重做日志 redo log来保证),接下来的其它操作和数据库故障不应该对其有任何影响。

持久性是通过事务日志来保证的,日志包括了重做日志和回滚日志;当通过事务对数据进行修改的时候:

  • 事务修改之前,会先将数据库的变化信息记录到重做日志redo log 中,如果数据库宕机, 恢复后会读取 redo log 中的记录来恢复数据;
  • 然后再对数据库中对应的行进行修改;

这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。

4、隔离型(isolation)

隔离型(isolation) 是指一个事务的执行不能被其它事务干扰,即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事务之间不能互相干扰。

一个服务器,可以同时给多个客户端提供服务,多个客户端是并发执行的关系,多个客户端就会有多个事务,多个事务同时去操作一个表的时候,特别容易出现互相影响的问题。

  • 如果隔离性越高,就意味着事务之间的并发程度越低,执行效率越慢,但是数据准确性越高;
  • 如果隔离性越低,就意味着事务之间的并发程度越高,执行效率越快,但是数据准确性越低;

隔离性 通过事务的隔离级别来定义,并用锁机制来保证写操作的隔离性,用 MVCC 来保证读操作的隔离性。

事务的 隔离性通过锁实现,而事务的 原子性一致性持久性 则是通过 事务日志实现redo logundo log 共同保障了事务的持久性、一致性和原子性。

1.3 事务的状态

1、活动的(active)

事务对应的数据库操作正在执行过程中时,就说该事务处在活动的状态。

2、部分提交的(partially committed)

当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,就说该事务处在部分提交的状态。

3、提交的(committed)

当一个处在部分提交的状态的事务将修改过的数据都同步到磁盘上之后,就可以说该事务处在了提交的状态。

4、失败的(failed)

当事务处在活动的或者部分提交的状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,就说该事务处在失败的状态。

5、中止的(aborted)

如果事务执行了一部分而变为 失败的状态,那么就需要把已经修改的事务中的操作还原到事务执行前的状态。换句话说,就是要撤销失败事务对当前数据库造成的影响。这个撤销的过程称之为 回滚。当回滚操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,就说该事务处在了中止的状态。

事务状态变迁图:

Tips: 在事物进行过程中,未结束之前,DML语句是不会更改底层数据,只是将历史操作记录一下,在内存中完成记录。只有在事物结束的时候,而且是成功的结束的时候,才会修改底层硬盘文件中的数据。

二、事务的使用(提交流程)

2.1 事务的提交流程

在一次更改数据的事务提交中,相关操作 及 事务日志记录 流程为:开启事务 -> 找目标记录 -> 生成 undo log 记录旧值 -> 修改数据 -> 生成 redo log 并刷盘 -> 提交事务,生成bin log并刷盘 -> 事务完成,redo log 状态为已提交

  • 分配事务ID ,开启事务,获取锁,没有获取到锁则等待;
  • 执行器先通过存储引擎找到的数据页,如果缓冲池有则直接取出,没有则去主键索引上取出对应的数据页放入缓冲池;
  • 在数据页内找到记录,取出数据生成 undo log 记录旧值,更改后写入内存;
  • 生成 redo log 到内存,redo log 状态为 prepare;
  • 准备提交事务,将 redo log 和 undo log 写入文件并调用 fsync(刷盘);
  • 提交事务,server 层生成 bin log 并写入文件调用 fsync(刷盘);
  • 事务完成,将 redo log 的状态改为 commited,释放锁;

2.2 显式事务

显示事务 是使用 START TRANSACTION 或者 BEGIN 显式开启的事务。

START TRANSACTION 语句相较于 BEGIN 特别之处在于,它后边能跟随几个修饰符:

  • READ ONLY:标识当前事务是一个只读事务,也就是属于该事务的数据库操作只能读取数据,而不能修改数据;
  • READ WRITE:标识当前事务是一个读写事务,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据;
  • WITH CONSISTENT SNAPSHOT:启动一致性读;

显式开启事务的语法:

1
2
3
4
5
6
7
-- 开启事务
start transaction;

-- 若干条执行sql(DML),一系列事务中的操作(主要是DML,不含DDL)

-- 提交/回滚事务
commit/rollback;

显示在开启事务之后, 事务中的sql不会立即去执行, 只有等到commit操作后才会统一执行(保证原子性)。

提交事务:当提交事务后,对数据库的修改是永久性的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
mysql> start transaction;  # 手动开启事务
mysql> update account set money = money - 2000 where name = '张三';
mysql> update account set money = money + 2000 where name = '李四';
mysql> commit;  # commit之后即可改变底层数据库数据
mysql> select * from account;
+----+--------+----------+
| id | name   | money    |
+----+--------+----------+
|  1 | 张三   |  8000.00 |
|  2 | 李四   | 12000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

回滚操作(事务失败): 即撤销正在进行的所有没有提交的修改。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
mysql> start transaction;
mysql> update account set money = money - 2000 where name = '张三';
mysql> update account set money = money + 2000 where name = '李四';
mysql> rollback;
mysql> select * from account;
+----+--------+----------+
| id | name   | money    |
+----+--------+----------+
|  1 | 张三   | 10000.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+

将事务回滚到某个保存点:

1
ROLLBACK TO [SAVEPOINT]

其中关于SAVEPOINT相关操作有:

1
2
3
4
5
# 在事务中创建保存点,方便后续针对保存点进行回滚,一个事物中可以存在多个保存点。
SAVEPOINT 保存点名称;

# 删除某个保存点
RELEASE SAVEPOINT 保存点名称;

Tips:

  • 显式的的使用 START TRANSACTION 或者 BEGIN 语句开启一个事务时,在本次事务提交或者回滚前会暂时关闭掉自动提交事务的功能;
  • 把系统变量 autocommit 的值设置为OFF;

2.3 隐式事务

除了使用显示的方式提交事务外,在直接执行SQL语句(增删改操作的DML 和 DDL)时,也会自动提交事务, 这种机制的事务称为 隐式事务

隐式提交数据的情况:

  • 数据定义语言(Data definition language,缩写为:DDL)
  • 任何一条DML语句(insert、update、delete)的执行,都标志事务的开启;
  • 事务控制或关于锁定的语句:
    • 当在一个事务还没提交或者回滚时就又使用 START TRANSACTION 或者 BEGIN 语句开启了另一个事务时,会隐式的提交上一个事务;
    • 当前的 autocommit 系统变量的值为 OFF,手动把它调为ON时,也会隐式的提交前边语句所属的事务;
    • 使用 LOCK TABLESUNLOCK TABLES 等关于锁定的语句也会隐式的提交前边语句所属的事务;

三、事务并发异常

在实际生产环境下,可能会出现大规模并发请求的情况,如果没有妥善的设置事务的隔离级别,就可能导致一些异常情况的出现,最常见的几种异常为 脏写( Dirty Write )脏读(Dirty Read)幻读(Phantom Read)不可重复读(Unrepeatable Read),其严重级别为: 脏写 > 脏读 > 不可重复读 > 幻读

3.1 脏写( Dirty Write )

脏写( Dirty Write ) 也可称为 更新丢失(Update Lost) 是指当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其它事务的存在,就会发生丢失更新问题(最后的更新覆盖了由其它事务所做的更新)。 对于两个事务 Session A、Session B,如果事务Session A修改了另一个未提交事务Session B修改过的数据,那就意味着发生了脏写。

3.2 脏读(Dirty Read)

脏读(Dirty Read) 是指一个事务读取到了另外一个事务没有提交的数据(读写的是同一份数据)。

具体说就是当一个 事务B 正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,与此同时另外一个 事务A 也访问这个数据,然后使用了这个数据,因为这个数据是还没有提交的数据,那么事务A 读到的这个数据就是脏数据,依据脏数据所做的操作可能是不正确的。 对于两个事务 Session A、Session B,Session A读取了已经被 Session B更新但还没有被提交的字段。之后若 Session B回滚,Session A读取的内容就是临时且无效的。

3.3 不可重复读(Unrepeatable Read)

不可重复读(Unrepeatable Read) 在同一事务中,多次读取同一数据返回的结果有所不同。换句话说就是,后续读取到另一会话事务已提交的更新数据。

可重复读 就是在同一事务中多次读取数据时,能够保证所读数据一样,也就是后续读取不能读到另一会话事务已提交的更新数据。 对于两个事务Session A、Session B,Session A读取了一个字段,然后 Session B更新了该字段。 之后Session A再次读取同一个字段,值就不同了,那就意味着发生了不可重复读。

Tips: 那对于先前已经读到的记录,之后又读取不到这种情况 发生了不可重复读的现象。

3.4 幻读(Phantom Read)

幻读(Phantom Read) 是指同一事务中,用同样的操作读取两次,得到的记录数不相同。

幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行;同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据;那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。 对于两个事务Session A、Session B, Session A 从一个表中读取了一个字段, 然后 Session B 在该表中插入了一些新的行。之后, 如果 Session A再次读取同一个表, 就会多出几行,那就意味着发生了幻读。

Tips: 幻读强调的是指同一个事务中按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录。

3.5 幻读和不可重复读的区别

  • 不可重复读 的重点是修改:在同一事务中,同样的条件,第一次读的数据和第二次读的数据不一样(因为中间有其它事务提交了修改);
  • 幻读 的重点在于新增或者删除:在同一事务中,同样的条件,第一次和第二次读出来的记录数不一样(因为中间有其它事务提交了插入/删除);

四、事务隔离级别

4.1 SQL中的四种隔离级别

SQL标准中设立了4个隔离级别:

  • READ UNCOMMITTED:读未提交,不做任何限制(读写均不使用锁), 事务之间都是随意并发执行的;在该隔离级别,并发程度最高、隔离性最差,所有事务都可以看到其它未提交事务的执行结果。不能避免脏读、不可重复读、幻读。
  • READ COMMITTED:读已提交,对写操作加锁,并发程度降低,隔离性提高。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。可以避免脏读,但不可重复读、幻读问题仍然存在。
  • REPEATABLE READ:可重复读,写加锁、读加锁、隔离性再次提高,并发程度再次降低。事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。这是 MySQL的默认隔离级别

Tips: InnoDB 和 Falcon 存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决快照读(普通 select 语句)幻读问题,InnoDB 还通过间隙锁解决当前读(select … for update,update,delete 等语句)幻读问题。

  • SERIALIZABLE:可串行化,严格执行串行化、并发程度最低、隔离性最高,执行速度最慢。确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其它事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。

4.2 设置/查看 事务隔离级别

方式一: 在my.ini文件中使用transaction-isolation选项来设置服务器的缺省事务隔离级别。 该选项值可以是:

  • READ-UNCOMMITTED
  • READ-COMMITTED
  • REPEATABLE-READ
  • SERIALIZABLE

例如:

1
2
[mysqld]
transaction-isolation = READ-COMMITTED

方式二:通过命令动态设置隔离级别 隔离级别也可以在运行的服务器中动态设置,应使用 SET TRANSACTION ISOLATION LEVEL 语句。

语法模式为:

1
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL <isolation-level>

其中的可以是:

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

例如:

1
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

产看隔离级别

1
2
3
4
5
6
7
8
# 查看隔离级别,MySQL5.7.20 之前
SHOW VARIABLES LIKE 'tx_isolation';

# 查看隔离级别,MySQL5.7.20版本及之后
SHOW VARIABLES LIKE 'transaction_isolation';

# 不同MySQL版本中都可以使用的
SELECT @@transcation_isolation;

4.3 隔离级别的作用范围

事务隔离级别的作用范围分为两种:

  • 全局级:对所有的会话有效
  • 会话级:只对当前的会话有效

例如,设置会话级隔离级别为 READ COMMITTED

1
2
3
mysql> SET TRANSACTION ISOLATION LEVEL READ COMMITTED
# 或:
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED

设置全局级隔离级别为 READ COMMITTED

1
mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED

五、MySQL事务日志 (redo log) 和 (undo log)

5.1 事务4种特性的实现机制

事务的 4种特性:原子性一致性隔离性持久性 的实现机制为:

  • 事务的 隔离性 由 锁机制实现;
  • 而事务的 原子性一致性持久性 由事务的 redo 日志undo 日志 来保证:
    • redo log 称为重做日志,提供再写入操作来恢复提交事务修改的页操作,用来保证事务的持久性;
    • undo log 称为回滚日志,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。

有的DBA或许会认为 undo logredo log 的逆过程,其实不然。redo logundo log 都可以视为是一种 恢复操作,但是:

  • redo log:是存储引擎层(innodb)生成的日志,记录的是 “物理级别” 上的页修改操作,比如页号x、偏移量y 写入了 “zzz"数据。redo log 主要为了保证数据的可靠性
  • undo log:是存储引擎层(innodb)生成的日志,记录的是逻辑操作日志,比如对某一行数据进行了INSERT语句操作,那么undo log就记录一条与之相反的DELETE操步。undo log 主要用于事务的回滚(undo log记录的是每个修改操作的逆操作)和一致性非锁定读(undo log回滚行记到某种特定的版本—MVCC,即多版本并发控制)

5.2 重做日志(redo log)详解

1、为什么需要 redo log

MySQL系统服务再进行数据的增删改查等操作其本质上都是在访问页面(包括读页面、写页面、创建新页面等操作),在数据库进行页面读操作的时候,首先会判断该页面是否在缓冲池中,如果存在就直接操作缓冲池中的数据页,如果不存在,就会通过内存或磁盘将页面存放到缓冲池中再进行操作。

缓冲池可以帮助数据库消除CPU和磁盘之间的鸿沟,通过 checkpoint机制 可以保证数据的最终落盘(写入磁盘持进行久化存储),然而由于checkpoint并不是每次变更的时候就触发的,而是由master线程隔一段时间去处理的。所以最坏的情况就是事务提交后,刚写完缓冲池,数据库宕机了,那么这段被修改过的数据就是丢失的(还没有写入磁盘),无法恢复。

事务的持久性 要求对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。

如何避免因数据库服务异常导致缓存中修改的数据未刷入磁盘而丢失呢?

一个简单的做法是 在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法使得失去了使用缓存池减少IO的意义:

  • 修改量与刷新磁盘工作量严重不成比例,这将导致频繁的IO操作;
  • 随机IO刷新较慢;

另一个解决的思路:

  • 只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。
  • 所以只需要把修改了哪些数据记录一下(数据落到磁盘)就好,如果数据库系统异常导致,系统在后台自动的再根据这个记录重新操作一遍即可。这个记录修改数据的记录就叫做 重做日志(redo log)

InnoDB引擎的事务采用了 日志先行WAL技术( Write-Ahead Logging ),这种技术的思想就是先写日志,再写磁盘,只有日志写入(磁盘)成功,才算事务提交成功,这里的日志就是redo log。当发生宕机且数据未刷到磁盘的时候,可以通过redo log来恢复,保证ACID中的D,这就是redo log的作用。

Tips: Write-Ahead Log(预先日志持久化) —— 在持久化一个数据页之前,先将内存中相应的日志页持久化。

REDO日志的好处:

  • redo日志降低了刷盘频率
  • redo日志占用的空间非常小

REDO日志的特点:

  • redo日志是顺序写入磁盘的
  • 事务执行过程中,redo log不断记录

2、redo log 的组成

redo log可以简单分为以下两个部分:

  • 重做日志的缓冲 (redo log buffer) ,保存在内存中,是易失的;
  • 重做日志文件 (redo log file),保存在硬盘中,是持久的;

redo log buffer 大小参数设置:innodb_log_buffer_size,默认16M,最大值是4096M,最小值为1M。

redo log file日志文件: Redo日志文件有很多个,一般以日志文件组的形式出现,文件统一命名,格式是ib_logfile+数字,从0开始 日志文件组中每个文件大小相同,存放在MySQL的数据目录中,比如可以配置为一组4个文件,每个文件的大小是1GB,整个redo log日志文件组可以记录4G的内容。 Redo日志文件采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示。

Tips: 在MySQL的数据目录中,默认有两个文件ib_logfile0和ib_logfile1,每次刷盘都是将数据刷新到这两个文件内。

3、redo log 的整体流程

在MySQL 的 InnoDB 存储引擎中,事务日志通过重做日志(redo log) 和 InnoDB 存储引擎的日志缓冲(InnoDB Log Buffer)实现。

  • 事务开启时,事务中的操作,都会先写入存储引擎的日志缓冲中,在事务提交之前,这些缓冲的日志都需要提前刷新到磁盘上持久化,这就是 日志先行(Write-Ahead Logging)技术
  • 当事务提交之后,在Buffer Pool中映射的数据页才会慢慢刷新到磁盘,这个过程中如果数据库崩溃或者宕机,那么当系统重启进行恢复时,就可以根据redo log中记录的日志,把数据库恢复到崩溃前的一个状态,这类未完成的事务,可以继续提交,也可以选择回滚,这基于恢复的策略而定。

Steps: 第1步:如果原始数据不在内存缓存页中,则先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝; 第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值; 第3步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式; 第4步:定期将内存中修改的数据刷新到磁盘中;

在MySQL系统启动的时候,就已经为redo log分配了一块连续的存储空间,以顺序追加的方式记录 redo Log,通过顺序IO来改善性能,所有的事务共享redo log的存储空间,它们的 redo log 按语句的执行顺序,依次顺序的记录在一起。

4、redo log 的刷盘策略

redo log buffer 刷盘到 redo log file 的过程并不是真正的刷到磁盘中去,只是刷入到文件系统缓存(page cache)中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己来决定(比如page cache足够大了)。那么对于InnoDB来说就存在一个问题,如果交给系统来同步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。

针对这种情况,InnoDB给出 innodb_flush_log_at_trx_commit参数,该参数控制 commit提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:

  • 0:表示每次事务提交时不进行刷盘操作(系统默认master thread每隔1s进行一次重做日志的同步);
  • 1:表示每次事务提交时都将进行同步,刷盘操作(默认值);
  • 2:表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步,由OS自己决定什么时候同步到磁盘文件;

5、redo log 相关参数设置

  • innodb_log_group_home_dir:指定 redo log 文件组所在的路径,默认值为./,表示在数据库的数据目录下。MySQL的默认数据目录(var/lib/mysql)下默认有两个名为ib_logfile0和ib_logfile1的文件,log buffer中的日志默认情况下就是刷新到这两个磁盘文件中。此redo日志文件位置还可以修改。
  • innodb_log_files_in_group:指明redo log file的个数,命名方式如:ib_logfile0,ib_logfile1… ib_logfilen。默认2个,最大100个。
  • innodb_flush_log_at_trx_commit:控制 redo log 刷新到磁盘的策略,默认为1。
  • innodb_log_file_size:单个 redo log 文件设置大小,默认值为 48M ,最大值为512G,注意最大值指的是整个 redo log 系列文件之和,即(innodb_log_files_in_group * innodb_log_file_size )不能大于最大值512G。

5.3 回滚日志(undo log)

1、回滚日志(undo log)简介

redo log是事务持久性的保证,undo log 是事务原子性的保证,在事务中更新数据的前置操作其实是要先写入一个 undo log

事务需要保证原子性,也就是事务中的操作要么全部完成,要么什么也不做。但有时候事务执行到一半会出现一些情况,比如:

  • 事务执行过程中可能遇到各种错误,比如服务器本身的错误,操作系统错误,甚至是突然断电导致的错误;
  • 程序员可以在事务执行过程中手动输入 rollback语句结束当前事务的执行;

以上情况出现时,需要把数据改回原先的样子,这个过程称之为 回滚(rollback),这样就可以造成一个假象:这个事务看起来什么都没做,所以符合原子性要求。

所以,当要对一条记录做改动时(这里的改动可以指INSERT、 DELETE、UPDATE),MySQL都需要 “留一手” —― 把回滚时所需的东西记下来。比如:

  • 插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了(对于每个 INSERT语句,InnoDB存储引擎会记录一个对应的 DELETE 操作 );
  • 删除了一条记录,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了(对于每个 DELETE,InnoDB存储引擎会记录一个 INSERT)
  • 修改了一条记录,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了(对于每个UPDATE,InnoDB存储引擎会记录一个相反的UPDATE,将修改前的行放回去)

MySQL把这些为了回滚而记录的这些内容称之为 撤销日志 或者 回滚日志(即undo log)

注意,由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo日志。

此外,undo log 会产生redo log,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护。

2、undo log的作用

作用1:回滚数据 用户对undo日志可能有误解:undo用于将数据库物理地恢复到执行语句或事务之前的样子。但事实并非如此。undo是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子。所有修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同。

这是因为在多用户并发系统中,可能会有数十、数百甚至数千个*发事务。数据库的主要任务就是协调对数据记录的并发访问。比如,一个事务在修改当前一个页中某几条记录,同时还有别的事务在对同一个页中另几条记录进行修改。因此,不能将一个页回滚到事务开始的样子,因为这样会影响其它事务正在进行的工作。

作用2:MVCC(详情看第16章) undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来完成。当用户读取一行记录时,若该记录已经被其它事务占用,当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取。

Tips: 单个事务的回滚,只会回滚当前事务做的操作,并不会影响到其它的事务做的操作。

3、undo log的存储结构

回滚段与undo页:InnoDB对undo log的管理采用段的方式,也就是回滚段(rollback segment)。每个回滚段记录了1024个undo log segment,在每个undo log segment段中进行undo页的申请。

当在MySQL中开启一个事务需要写undo log的时候,就得先去undo log segment中去找到一个空闲的位置,当有空位的时候,就去申请undo页,在这个申请到的undo页中进行undo log的写入。

mysql默认一页的大小是16k。

为每一个事务分配一个页,是非常浪费的(除非事务非常长),假设应用的TPS(每秒处理的事务数目)为1000,那么1s就需要1000个页,大概需要16M的存储,1分钟大概需要1G的存储。如果照这样下去除非MySQL清理的非常勤快,否则随着时间的推移,磁盘空间会增长的非常快,而且很多空间都是浪费的。

于是undo页就被设计得 可以重用 了,当事务提交时,并不会立刻删除undo页。因为重用,所以这个undo页可能混杂着其它事务的undo log。undo log在commit后,会被放到一个链表中,然后判断undo页的使用空间是否小于3/4,如果小于3/4的话,则表示当前的undo页可以被重用,那么它就不会被回收,其它事务的undo log可以记录在当前undo页的后面。由于undo log是离散的,所以清理对应的磁盘空间时,效率不高。

回滚段与事务

  • 每个事务只会使用一个回滚段,一个回滚段在同一时刻可能会服务于多个事务。
  • 当一个事务开始的时候,会制定一个回滚段,在事务进行的过程中,当数据被修改前,原始的数据会被复制到回滚段。
  • 在回滚段中,事务会不断填充盘区,直到事务结束或所有的空间被用完。如果当前的盘区不够用,事务会在段中请求扩展下一个盘区,如果所有已分配的盘区都被用完,事务会覆盖最初的盘区或者在回滚段允许的情况下扩展新的盘区来使用。
  • 回滚段存在于undo表空间中,在数据库中可以存在多个undo表空间,但同一时刻只能使用一个undo表空间。
  • 当事务提交时,InnoDB存储引擎会做以下两件事情:
    • 将undo log放入列表中,以供之后的purge操作
    • 判断undo log所在的页是否可以重用,若可以分配给下个事务使用

回滚段中的数据分类

  • 未提交的回滚数据(uncommitted undo information):该数据所关联的事务并未提交,用于实现读一致性,所以该数据不能被其它事务的数据覆盖。
  • 已经提交但未过期的回滚数据(committed undo information):该数据关联的事务已经提交,但是仍受到undo retention参数的保持时间的影响。
  • 事务已经提交并过期的数据(expired undo information):事务已经提交,而且数据保存时间已经超过undo retention参数指定的时间,属于已经过期的数据。当回滚段满了之后,会优先覆盖事务已经提交并过期的数据。

事务提交后并不能马上删除undo log及undo log所在的页,这是因为可能还有其它事务需要通过undo log来得到记录之前的版本,故事务提交时将undo log放入一个链表中,是否可以最终删除undo log及undo log所在页由purge线程来判断。

4、undo log的类型 和 删除

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

  • insert undo log insert undo log是指在insert操作中产生的undo log。因为insert操作的记录,只对事务本身可见,对其它事务不可见(这是事务隔离性的要求),故该undo log可以在事务提交后直接删除,不需要进行purge操作。
  • update undo log update undo log记录的是对delete和updale操作产生的undo log。该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。

5、undo log 的生命周期

Redo Log和Undo Log的生成过程

对于InnoDB引擎来说,每行记录除了记录本身的数据之外,还有几个隐藏的列:

  • DB_ROW_ID: 如果定义表没有显示的定义主键,并且表中也没有定义唯一索引,那么InnoDB会自动为表添加一个 row_id的隐藏列作为主键;
  • DB_TRX_ID:每个事务都会分配一个 事务ID, 当对某条记录发生变更事,就会将这个事务的ID 写入trx_id中;
  • DB_ROLL_PTR:回滚指针,本质上就是指向 undo log 的指针。

当执行INSERT时:

1
2
begin; 
INSERT INTO user (name) VALUES ("tom");

插入的数据都会生成一条insert undo log,并且数据的回滚指针会指向它,undo log 会记录undo log的序号、插入主键的列和值,那么在进行rollback的之后,通过主键直接把对应的数据删除即可;

当执行UPDATE时: 对于更新的操作会产生update undo log,并且会分更新主键和不更新主键的

1
2
3
# 不更新主键
begin; 
INSERT INTO user (name) VALUES ("mac");

1
2
3
# 更新主键
begin; 
UPDATE user SET id=2 WHERE id=1;

上面的例子来说,假设执行rollback,那么对应的流程应该是这样:

  • 1、通过undo no=3的日志把id=2的数据删除
  • 2、通过undo no=2的日志把id=1的数据的deletemark还原成0
  • 3、通过undo no=1的日志把id=1的数据的name还原成Tom
  • 4、通过undo no=0的日志把id=1的数据删除

5.4 事务日志小结

  • undo log是逻辑日志,对事务回滚时,只是将数据库逻辑地恢复到原来的样子。
  • redo log是物理日志,记录的是数据页的物理变化,undo log不是redo log的逆过程。