事务
事务是数据库区别于文件系统的重要特性之一,当我们有了事务就会让数据库始终保持一致性
,同时我们还能通过事务的机制恢复到某个时间点
,这样可以保证已提交到数据库的修改不会因为系统崩溃而丢失
1. 概述
1.1 存储引擎支持情况
SHOW ENGINES
命令来查看当前 MySQL 支持的存储引擎都有哪些,以及这些存储引擎是否支持事务
能看出在 MySQL 中,只有 InnoDB 是支持事务的
1.2 基本概念
事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态
事务处理的原则:保证所有事务都作为 一个工作单元
来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交( commit
),那么这些修改就 永久
地保存下来;要么数据库管理系统将 放弃
所做的所有 修改
,整个事务回滚( rollback
)到最初状态
1.3 ACID 特性
-
原子性(atomicity):
原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚
-
一致性(consistency):
一致性是指事务执行前后,数据从一个
合法性状态
变换到另外一个合法性状态
。这种状态是语义
上的而不是语法上的,跟具体的业务有关 -
隔离型(isolation):
隔离性是指一个事务的执行
不能被其他事务干扰
,即一个事务内部的操作及使用的数据对并发
的其他事务是隔离的,并发执行的各个事务之间不能互相干扰 -
持久性(durability):
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是
永久性的
持久性是通过
事务日志
来保证的。日志包括了重做日志
和回滚日志
。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志
中,然后再对数据库中对应的行进行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性
1.4 事务的状态
-
活动的(active):
当事务对应的数据库操作正在执行过程中,则该事务处于活动状态
-
部分提交的(partially committed):
当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并
没有刷新到磁盘
时,我们就说该事务处在部分提交的
状态 -
失败的(failed):
当事务处在
活动的
或者部分提交的
状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在失败的
状态 -
中止的(aborted):
如果事务执行了一部分而变为
失败的
状态,那么就需要把已经修改的事务中的操作还原到事务执行前的状态。换句话说,就是要撤销失败事务对当前数据库造成的影响。我们把这个撤销的过程称之为回滚
。当回滚
操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了中止的
状态 -
提交的(committed):
当一个处在
部分提交的
状态的事务将修改过的数据都同步到磁盘
上之后,我们就可以说该事务处在了提交的
状态
图中可见,只有当事务处于提交的
或者中止的
状态时,一个事务的生命周期才算是结束了。对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,对于处于中止状态的事务,该事务对数据库所做的所有修改都会被回滚到没执行该事务之前的状态
1.5 事务的分类
- 扁平事务 (Flat Transactions)
- 扁平事务是事务类型中最简单的一种,在实际生产环境中可能是使用最频繁的事务。在扁平事务中,所有操作都处于同一层次,其由
BEGIN WORK
开始,由COMMIT WORK
或ROLLBACK WORK
结束。所有的操作要么都执行,要么都回滚
- 扁平事务是事务类型中最简单的一种,在实际生产环境中可能是使用最频繁的事务。在扁平事务中,所有操作都处于同一层次,其由
- 带有保存点的扁平事务 (Flat Transactions with Savepoints)
- 除了支持扁平事务的操作外,带有保存点的扁平事务允许在事务执行过程中回滚到同一事务中较早的一个状态。保存点用来通知事务系统应该记住事务当前的状态,以便当之后发生错误时,事务能回到保存点当时的状态
- 链事务 (Chained Transactions)
- 链事务可以视为保存点模式的一种变种。链事务的思想是:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务
- 嵌套事务 (Nested Transactions)
- 嵌套事务是一个层次结构框架,由一个顶层事务控制着各个层次的事务。顶层事务之下嵌套的事务被称为子事务,其控制每一个局部的变换
- 分布式事务 (Distributed Transactions)
- 分布式事务通常是一个分布式环境下运行的扁平事务,需要根据数据所在位置访问网络中的不同节点。例如,一个用户在 ATM 机上进行银行的转账操作,可能涉及到多个数据库节点
2. 如何使用事务
2.1 显式事务
步骤 1:
START TRANSACTION
或者 BEGIN
,作用是显式开启一个事务
mysql> BEGIN;
#或者
mysql> START TRANSACTION;
START TRANSACTION
语句相较于 BEGIN
特别之处在于后边能跟随几个 修饰符
:
-
READ ONLY
:标识当前事务是一个只读事务
,也就是属于该事务的数据库操作只能读取数据,而不能修改数据补充:只读事务中只是不允许修改那些其他事务也能访问到的表中的数据,对于临时表来说(我们使用 CREATE TMEPORARY TABLE 创建的表),由于它们只能再当前会话中可见,所以只读事务其实也是可以对临时表进行增、删、改操作的
-
READ WRITE(默认)
:标识当前事务是一个读写事务
,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据 -
WITH CONSISTENT SNAPSHOT
:启动一致性读
比如:
START TRANSACTION READ ONLY; # 开启一个只读事务
START TRANSACTION READ ONLY, WITH CONSISTENT SNAPSHOT # 开启只读事务和一致性读
START TRANSACTION READ WRITE, WITH CONSISTENT SNAPSHOT # 开启读写事务和一致性读
注意:
READ ONLY
和READ WRITE
是用来设置所谓的事务访问模式
的,就是以只读还是读写的方式来访问数据库中的数据,一个事务的访问模式不能同时设置为只读
和读写
的,所以不能同时把READ ONLY
和READ WRITE
放到START TRANSACTION
语句后边- 如果我们不显式指定事务的访问模式,那么该事务的访问模式就是
读写
模式
步骤 2:
一系列事务中的操作(主要是 DML,不含 DDL)
步骤 3:
提交事务或中止事务(即回滚事务)
# 提交事务。当提交事务后,对数据库的修改是永久性的。
mysql> COMMIT;
# 回滚事务。即撤销正在进行的所有没有提交的修改
mysql> ROLLBACK;
# 将事务回滚到某个保存点。
mysql> ROLLBACK TO [SAVEPOINT]
savepoint
# 在事务中创建保存点,方便后续针对保存点进行回滚。一个事务中可以存在多个保存点 SAVEPOINT 保存点名称;
# 删除某个保存点 RELEASE SAVEPOINT 保存点名称;
2.2 隐式事务
MySQL 中有一个系统变量 autocommit
:
SHOW VARIABLES LIKE 'autocommit';
当然,如果我们想关闭这种 自动提交
的功能,可以使用下边两种方法之一:
-
显式的的使用
START TRANSACTION
或者BEGIN
语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能 -
把系统变量
autocommit
的值设置为OFF
,就像这样:SET autocommit = OFF; #或 SET autocommit = 0;
2.3 隐式提交数据的情况
- 数据定义语言(Data definition language,缩写为:DDL)
数据库对象,指的就是数据库、表、视图、存储过程
等结构。当我们 CREATE
、ALTER
、DROP
等语句去修改数据库对象时,就会隐式的提交前边语句所属于的事务。即:
BEGIN;
SELECT ... # 事务中的一条语句
UPDATE ... # 事务中的一条语句
... # 事务中的其他语句
CREATE TABLE ... # 此语句会隐式的提交前边语句所属于的事务
- 隐式使用或修改 MySQL 数据库中的表
当我们使用 ALTER USER
、CREATE USER
、DROP USER
、GRANT
、RENAME USER
、REVOKE
、SET PASSWORD
等语句时也会隐式的提交前边语句所属于的事务
- 事务控制或关于锁定的语句
-
当我们在一个事务还没提交或者回滚时就又使用
START TRANSACTION
或者BEGIN
语句开启了另一个事务时,会隐式的提交上一个事务。即:BEGIN; SELECT ... # 事务中的一条语句 UPDATE ... # 事务中的一条语句 ... # 事务中的其他语句 BEGIN; # 此语句会隐式的提交前边语句所属于的事务
-
当前的
autocommit
系统变量的值为OFF
,我们手动把它调为ON
时,也会隐式的提交前边语句所属的事务 -
使用
LOCK TABLES
、UNLOCK TABLES
等关于锁定的语句也会隐式的提交前边语句所属的事务
- 加载数据的语句
使用 LOAD DATA
语句来批量往数据库中导入数据时,也会隐式的提交前边语句所属的事务
- 关于 MySQL 复制的一些语句
使用 START SLAVE
、STOP SLAVE
、RESET SLAVE
、CHANGE MASTER TO
等语句会隐式的提交前边语句所属的事务
- 其他的一些语句
使用 ANALYZE TABLE
、CACHE INDEX
、CAECK TABLE
、FLUSH
、LOAD INDEX INTO CACHE
、OPTIMIZE TABLE
、REPAIR TABLE
、RESET
等语句也会隐式的提交前边语句所属的事务
3. 事务隔离级别
MySQL 是一个客户端/服务器
架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称为一个会话( Session
)。每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理多个事务。事务有 隔离性
的特性,理论上在某个事务 对某个数据进行访问
时,其他事务应该进行排队
,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样对 性能影响太大
,我们既想保持事务的隔离性,又想让服务器在处理访问同一数据的多个事务时 性能尽量高些
,那就看二者如何权衡取舍了
3.1 数据准备
CREATE TABLE student (
studentno INT,
name VARCHAR(20),
class varchar(20),
PRIMARY KEY (studentno)
) Engine=InnoDB CHARSET=utf8;
然后向这个表里插入一条数据:
INSERT INTO student VALUES(1, '小谷', '1班');
现在表里的数据就是这样的:
mysql> select * from student;
+-----------+--------+-------+
| studentno | name | class |
+-----------+--------+-------+
| 1 | 小谷 | 1班 |
+-----------+--------+-------+
1 row in set (0.00 sec)
3.2 数据并发问题
针对事务的隔离性和并发性,我们怎么做取舍呢?先看一下访问相同数据的事务在不保证串行执行 (也就是执行完一个再执行另一个)的情况下可能会出现哪些问题:
(1)脏写(Dirty Write)
对于两个事务 Session A、Session B。如果事务 Session A 修改了
另一个事务 Session B 修改过
但 未提交
的数据,那就意味着发生了 脏写
,示意图如下:
Session A 和 Session B 各开启了一个事务,Sesssion B 中的事务先将 studentno 列为 1 的记录的 name 列更新为'李四',然后 Session A 中的事务接着又把这条 studentno 列为 1 的记录的 name 列更新为'张三'。如果之后 Session B 中的事务进行了回滚,那么 Session A 中的更新也将不复存在,这种现象称之为脏写。这时 Session A 中的事务就没有效果了,明明把数据更新了,最后也提交事务了,最后看到的数据什么变化也没有。这里大家对事务的隔离性比较了解的话,会发现默认隔离级别下,上面 Session A 中的更新语句会处于等待状态,这里只是跟大家说明一下会出现这样的现象
(2)脏读(Dirty Read)
对于两个事务 Session A、Session B。Session A 读取
了已经被 Session B 修改过
但还 未提交
的字段。 之后若 Session B 回滚
,Session A 读取
的内容就是 临时且无效
的
Session A 和 Session B 各开启了一个事务,Session B 中的事务先将 studentno 列为 1 的记录的 name 列更新为'张三',然后 Session A 中的事务再去查询这条 studentno 为 1 的记录,如果读到列 name 的值为'张三',而 Session B 中的事务稍后进行了回滚,那么 Session A 中的事务相当于读到了一个不存在的数据,这种现象就称之为 脏读
(3)不可重复读(Non-Repeatable Read)
对于两个事务 Session A、Session B。Session A 读取
了一个字段,然后 Session B 更新
了该字段。 之后 Session A 再次读取
同一个字段,值就不同
了。那就意味着发生了不可重复读
我们在 Session B 中提交了几个 隐式事务
(注意是隐式事务,意味着语句结束事务就提交了),这些事务都修改了 studentno 列为 1 的记录的列 name 的值,每次事务提交之后,如果 Session A 中的事务都可以查看到最新的值,这种现象也被称之为 不可重复读
(4)幻读(Phantom)
对于两个事务 Session A、Session B。 Session A 从一个表中 读取
了一个字段,然后 Session B 在该表中 插入了一些新的行
。之后,如果 Session A 再次读取
同一个表,就会多出几行。那就意味着发生了幻读
Session A 中的事务先根据条件 studentno > 0 这个条件查询表 student,得到了 name 列值为'张三'的记录;之后 Session B 中提交了一个 隐式事务
,该事务向表 student 中插入了一条新记录;之后 Session A 中的事务再根据相同的条件 studentno > 0 查询表 student,得到的结果集中包含 Session B 中的事务新插入的那条记录,这种现象也被称之为幻读
。我们把新插入的那些记录称之为幻影记录
注意1:
有的同学会有疑问,那如果 Session B 中删除了一些符合
studentno > 0
的记录而不是插入新记录,那 Session A 之后再根据studentno > 0
的条件读取的记录变少了
,这种现象算不算幻读
呢?这种现象不属于幻读
,幻读强调的是一个事务按照某个相同条件多次读取
记录时,后读取时读到了之前没有读到的记录注意2:
那对于先前已经读到的记录,之后又读取不到这种情况,算啥呢?这相当于对每一条记录都发生了
不可重复读
的现象。幻读只是重点强调了读取到了之前读取没有获取到的记录
3.3 SQL 中的四种隔离级别
上面介绍了几种并发事务执行过程中可能遇到的一些问题,这些问题有轻重缓急之分,我们给这些问题按照严重性来排一下序:
脏写 > 脏读 > 不可重复读 > 幻读
我们愿意舍弃一部分隔离性来换取一部分性能在这里就体现在:设立一些隔离级别,隔离级别越低,并发问题发生的就越多。 SQL标准
中设立了 4 个 隔离级别
:
READ UNCOMMITTED
:读未提交,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读READ COMMITTED
:读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。可以避免脏读,但不可重复读、幻读问题仍然存在REPEATABLE READ
:可重复读,事务 A 在读到一条数据之后,此时事务 B 对该数据进行了修改并提交,那么事务 A 再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。这是 MySQL 的默认隔离级别SERIALIZABLE
:可串行化,确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读
SQL标准
中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的问题,具体情况如下:
脏写
怎么没涉及到?因为脏写这个问题太严重了,不论是哪种隔离级别,都不允许脏写的情况发生
不同的隔离级别有不同的现象,并有不同的锁和并发机制,隔离级别越高,数据库的并发性能就越差,4 种事务隔离级别与并发性能的关系如下:
3.4 MySQL 支持的四种隔离级别
不同的数据库厂商对 SQL 标准中规定的四种隔离级别支持不一样。比如,Oracle
就只支持 READ COMMITTED(默认隔离级别)
和SERIALIZABLE
隔离级别。MySQL 虽然支持 4 种隔离级别,但与 SQL 标准中所规定的各级隔离级别允许发生的问题却有些出入,MySQL 在REPEATABLE READ
隔离级别下,是可以禁止幻读问题的发生的
MySQL 的默认隔离级别为 REPEATABLE READ
,我们可以手动修改一下事务的隔离级别
# 查看隔离级别,MySQL 5.7.20的版本之前:
mysql> SHOW VARIABLES LIKE 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)
# MySQL 5.7.20版本之后,引入 transaction_isolation 来替换 tx_isolation
# 查看隔离级别,MySQL 5.7.20的版本及之后:
mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.02 sec)
#或者不同MySQL版本中都可以使用的:
SELECT @@transaction_isolation;
3.5 设置事务的隔离级别
通过下面的语句修改事务的隔离级别:
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL 隔离级别;
#其中,隔离级别格式:
> READ UNCOMMITTED
> READ COMMITTED
> REPEATABLE READ
> SERIALIZABLE
或者:
SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = '隔离级别'
#其中,隔离级别格式:
> READ-UNCOMMITTED
> READ-COMMITTED
> REPEATABLE-READ
> SERIALIZABLE
关于设置时使用 GLOBAL 或 SESSION 的影响:
-
使用
GLOBAL
关键字(在全局范围影响):SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE; #或 SET GLOBAL TRANSACTION_ISOLATION = 'SERIALIZABLE';
则:
- 当前已经存在的会话无效
- 只对执行完该语句之后产生的会话起作用
-
使用
SESSION
关键字(在会话范围影响):SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; #或 SET SESSION TRANSACTION_ISOLATION = 'SERIALIZABLE';
则:
- 对当前会话的所有后续的事务有效
- 如果在事务之间执行,则对后续的事务有效
- 该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务
如果在服务器启动时想改变事务的默认隔离级别,可以修改启动参数 transaction_isolation
的值。比如,在启动服务器时指定了 transaction_isolation = SERIALIZABLE
,那么事务的默认隔离界别就从原来的 REPEATABLE-READ
变成了 SERIALIZABLE
数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱
4. 事务日志
事务有 4 种特性:原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢?
- 事务的隔离性由
锁机制
实现 - 而事务的原子性、一致性和持久性由事务的
redo
日志和undo
日志来保证- redo log 称为
重做日志
,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性 - undo log 称为
回滚日志
,回滚行记录到某个特定版本,用来保证事务的原子性、一致性
- redo log 称为
有的 DBA 或许会认为 undo 是 redo 的逆过程,其实不然。redo 和 undo 都可以视为是一种 恢复操作
,但是:
redo log
: 是存储引擎层 (innodb) 生成的日志,记录的是"物理级别"
上的页修改操作,比如页号xxx,偏移量yyy写入了'zzz'数据。主要为了保证数据的可靠性undo log
: 是存储引擎层 (innodb) 生成的日志,记录的是逻辑操作
日志,比如对某一行数据进行了 INSERT 语句操作,那么 undo log 就记录一条与之相反的 DELETE 操作。主要用于事务的回滚
(undo log 记录的是每个修改操作的逆操作
) 和一致性非锁定读
(undo log 回滚行记录到某种特定的版本——MVCC,即多版本并发控制)
4.1 redo 日志
InnoDB 存储引擎是以页为单位
来管理存储空间的。在真正访问页面之前,需要把在磁盘上
的页缓存到内存中的Buffer Pool
之后才可以访问。所有的变更都必须先更新缓冲池
中的数据,然后缓冲池中的脏页
会以一定的频率被刷入磁盘 (checkPoint
机制),通过缓冲池来优化 CPU 和磁盘之间的鸿沟,这样就可以保证整体的性能不会下降太快
(1)为什么需要 redo 日志
一方面,缓冲池可以帮助我们消除 CPU 和磁盘之间的鸿沟,checkpoint 机制可以保证数据的最终落盘,然而由于 checkpoint 并不是每次变更的时候就触发
的,而是master线程隔一段时间去处理的。所以最坏的情况就是事务提交后,刚写完缓冲池,数据库宕机了,那么这段数据就是丢失的,无法恢复
另一方面,事务包含持久性
的特性,就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失
那么如何保证这个持久性呢?
-
在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法有些问题:
-
修改量与刷新磁盘工作量严重不成比例
有时候我们仅仅修改了某个页面中的一个字节,但是我们知道在 InnoDB 中是以页为单位来进行磁盘 IO 的,也就是说我们在该事务提交时不得不将一个完整的页面从内存中刷新到磁盘,我们又知道一个默认页面时 16KB 大小,只修改一个字节就要刷新 16KB 的数据到磁盘上显然是小题大做了
-
随机 IO 刷新较慢
一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,假如该事务修改的这些页面可能并不相邻,这就意味着在将某个事务修改的 Buffer Pool 中的页面
刷新到磁盘
时,需要进行很多的随机IO
,随机IO 比 顺序IO 要慢,尤其对于传统的机械硬盘来说
-
-
我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,再重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好。比如,某个事务将系统表空间中 第 10 号页面中偏移量为 100 处的那个字节的值 1 改成 2 。我们只需要记录一下:将第0号表空间的10号页面的偏移量为100处的值更新为 2
InnoDB 引擎的事务采用了 WAL 技术 (Write-Ahead Logging
),这种技术的思想就是先写日志,再写磁盘,只有日志写入成功,才算事务提交成功,这里的日志就是 redo log
。当发生宕机且数据未刷到磁盘的时候,可以通过 redo log
来恢复,保证ACID中的D,这就是 redo log
的作用
(2)好处和特点
好处:
- redo 日志降低了刷盘频率
- redo 日志占用的空间非常小
存储表空间 ID、页号、偏移量以及需要更新的值,所需的存储空间是很小,刷盘快
特点:
-
redo 日志是顺序写入磁盘的
在执行事务的过程中,每执行一条语句,就可能产生若干条 redo 日志,这些日志是按照
产生的顺序写入磁盘的
,也就是使用顺序 ID,效率比随机IO快 -
事务执行过程中,redo log不断记录
redo log 跟 bin log 的区别:redo log 是
存储引擎层
产生的,而 bin log 是数据库层
产生的。假设一个事务,对表做10万行的记录插入,在这个过程中一直不断的往 redo log 顺序记录,而 bin log 不会记录,直到这个事务提交,才会一次写入到 bin log文件中
(3)组成
redo log 可以简单分为以下两个部分:
重做日志的缓冲 (redo log buffer)
,保存在内存中,是易失的
在服务器启动时就会向操作系统申请了一大片称之为 redo log buffer 的 连续内存
空间,翻译成中文就是 redo 日志缓冲区。这片内存空间被划分为若干个连续的redo log block
。一个 redo log block
占用512字节
大小
参数设置:innodb_log_buffer_size:
redo log buffer
大小默认 16M
,最大值是 4096M,最小值为 1M
重做日志文件 (redo log file)
,保存在硬盘中,是持久的
redo 日志文件如图所示,其中ib_logfile0
和ib_logfile1
即为 redo 日志
(4)整体流程
以一个更新事务为例,redo log
流转过程,如下图所示:
- 先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝
- 生成一条重做日志并写入
redo log buffer
,记录的是数据被修改后的值- 当事务 commit 时,将
redo log buffer
中的内容刷新到redo log file
,对redo log file
采用追加写的方式- 定期将内存中修改的数据刷新到磁盘中
Write-Ahead Log(预先日志持久化):在持久化一个数据页之前,先将内存中相应的日志页持久化
(5)刷盘策略
redo log
的写入并不是直接写入磁盘的,InnoDB 引擎会在写 redo log
的时候先写 redo log buffer
,之后以 一定的频率
刷入到真正的 redo log file
中。这里的一定频率怎么看待呢?这就是我们要说的刷盘策略
注意,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 线程每隔 1 s进行一次重做日志的同步)设置为 1
:表示每次事务提交时都将进行同步,刷盘操作(默认值)设置为 2
:表示每次事务提交时都只把redo log buffer
内容写入page cache
,不进行同步。由操作系统自己决定什么时候同步到磁盘文件
除了后台线程每秒1次
的轮询操作,还有一种情况,当 redo log buffer
占用的空间即将达到innodb_log_buffer_size
(这个参数默认是16M)的一半的时候,后台线程会主动刷盘
(6)不同刷盘策略演示
innodb_flush_log_at_trx_commit = 1
只要事务提交成功,
redo 1og
记录就一定在硬盘里,不会有任何数据丢失。如果事务执行期间 MySQL 挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。可以保证ACID的D,数据绝对不会丢失,但是效率最差的。建议使用默认值,虽然操作系统宕机的概率理论小于数据库宕机的慨率,但是一般既然使用了事务,那么数据的安全相对来说更重要些
innodb_flush_log_at_trx_commit = 2
只要事务提交成功,
redo log buffer
中的内容只写入文件系统缓存(page cache
)。如果仅仅只是 MySQL 挂了不会有任何数据丢失,但是操作系统宕机可能会有1秒数据的丢失,这种情况下无法满足ACID中的D。但是数值2肯定是效率最高的
innodb_flush_log_at_trx_commit = 0
master 线程中每1秒进行一次重做日志的 fsync 操作,因此实例 crash 最多丢失1秒钟内的事务。数值为0的话,是一种折中的做法,它的 IO 效率理论是高于1的,低于2的,这种策略也有丢失数据的风险,也无法保证D
注:master 线程,是负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性
(7)checkpoint
在整个日志文件组中还有两个重要的属性,分别是 write pos、checkpoint
write pos
是当前记录的位置,一边写一边后移checkpoint
是当前要擦除的位置,也是往后推移
每次刷盘 redo log
记录到日志文件组中,write pos
位置就会后移更新。每次 MySQL 加载日志文件组恢复数据时,会清空加载过的 redo log
记录,并把 checkpoint
后移更新。write pos
和 checkpoint
之间的还空着的部分可以用来写入新的 redo log
记录
如果 write pos
追上 checkpoint
,表示日志文件组
满了,这时候不能再写入新的 redo log
记录,MySQL 得停下来,清空一些记录,把 checkpoint
推进一下
4.2 undo 日志
redo log
是事务持久性的保证,undo log
是事务原子性的保证。在事务中 更新数据
的 前置操作
其实是要先写入一个 undo log
(1)基本介绍
事务需要保证 原子性
,也就是事务中的操作要么全部完成,要么什么也不做。但有时候事务执行到一半会出现一些情况,比如:
- 情况一:事务执行过程中可能遇到各种错误,比如
服务器本身的错误
,操作系统错误
,甚至是突然断电
导致的错误 - 情况二:程序员可以在事务执行过程中手动输入
ROLLBACK
语句结束当前事务的执行
以上情况出现,我们需要把数据改回原先的样子,这个过程称之为 回滚
,这样就可以造成一个假象:这个事务看起来什么都没做,所以符合 原子性
要求
每当我们要对一条记录做改动时(这里的改动
可以指 INSERT
、DELETE
、UPDATE
),都需要"留一手"一一把回滚时所需的东西记下来。比如:
- 你
插入一条记录
时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉
就好了。(对于每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
可能有误解
:undo log
用于将数据库物理地恢复到执行语句或事务之前的样子。但事实并非如此。undo log
是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子。所有修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同 -
这是因为在多用户并发系统中,可能会有数十、数百甚至数千个并发事务。数据库的主要任务就是协调对数据记录的并发访问。比如,一个事务在修改当前一个页中某几条记录,同时还有别的事务在对同一个页中另几条记录进行修改。因此,不能将一个页回滚到事务开始的样子,因为这样会影响其他事务正在进行的工作
-
-
MVCC
undo log
的另一个作用是 MVCC,即在 InnoDB 存储引擎中 MVCC 的实现是通过undo log
来完成。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo log
读取之前的行版本信息,以此实现非锁定读取
(3)类型
在 InnoDB 存储引擎中,undo log
分为:
-
insert undo log
insert undo log
是指insert
操作中产生的undo log
。因为insert
操作的记录,只对事务本身可见,对其他事务不可见(这是事务隔离性的要求),故该undo log
可以在事务提交后直接删除。不需要进行purge操作 -
update undo log
update undo log
记录的是对delete
和update
操作产生的undo log
。该undo log
可能需要提供 MVCC 机制,因此不能在事务提交时就进行删除。提交时放入undo log
链表,等待purge线程进行最后的删除
(4)小结
undo log
是逻辑日志,对事务回滚时,只是将数据库逻辑地恢复到原来的样子redo log
是物理日志,记录的是数据页的物理变化,undo log
不是redo log
的逆过程