环境
MacBook Pro
序言
MySQL 技术内幕 InnoDB 存储引擎 第二版
事务
事务的四个特性:
① 原子性
② 隔离性
③ 一致性
④ 持久性
其中,隔离性
,是由锁
来保证的。
原子性
、一致性
、持久性
则是通过redo log
和undo log
来完成。
名称 | 描述 | 记录格式 | 读取方式 |
---|---|---|---|
redo log | 重做日志;用来保证事务的原子性和持久性 | 物理日志,记录页的物理修改操作 | 基本上是顺序读 |
undo log | 用来保证事务的一致性 | 逻辑日志;根据每行记录进行记录 | 随机读 |
Q : undo
是redo
的逆操作吗?
A: 其实不是;redo和undo的作用都可以视为一种恢复操作;
redo
恢复提交事务修改的页操作;
undo
回滚行记录到某个特定版本;
redo log 会产生脏页,这种脏页时间长了,量多了后,就会让MySQL服务器“抖一下”。
刷脏页有四种场景:
- redo log 写满了,要刷脏页;这种情况需要尽量避免。因为出现这种情况,整个系统不再接收更新了,所有的更新都会被堵住,从监控上看,就是更新跌为0。
- 内存不够用了,要先将脏页写到磁盘中。这种情况其实是常态。InnoDB用缓冲池管理内存,缓冲池内存页有三种状态: ① 还没有使用 ② 使用了并且是干净页。③使用了并且是脏页。对于长时间运行的数据库来说,未被使用的页很少。
刷脏页虽然是常态,但是出现下面情况,都将影响性能:
- 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长;
- 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的。
所以,InnoDB 需要有控制脏页比例的机制,来尽量避免上面的这两种情况。
- MySQL 认为系统“空闲”的时候。
- MySQL 正常关闭的情况。这时候,MySQL 会把内存的脏页都 flush 到磁盘上,这样下次 MySQL 启动的时候,就可以直接从磁盘上读数据,启动速度会很快。
事务的分类
- 扁平事务
- 带有保存点的扁平事务
- 链事务
- 嵌套事务
- 分布式事务
扁平事务
在扁平事务中所有的操作都处在同一层次;
由begin work
开始,commit work
或rollback work
结束;
该事务也是我们最常用的事务
带有保存点的扁平事务
出现的原因
:某些事务可能在执行过程中出现的错误并不会导致所有的操作都无效,放弃整个事务不合乎要求,开销也太大。
保存点
:用来通知系统应用记住事务当前的状态,以便当之后发生错误时,事务能回到保存点当时的状态。
链事务
可以视为保存点
模式的一个变种。
出现的原因
:带有保存点的扁平事务,当系统发生崩溃时,所有的保存点都将消失,因为其保存点是意失的,而非持久的。这意味着当进行恢复时,事务需要从开始处重写执行,而不能从最近的一个保存点继续执行。
链事务:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。注意,提交事务操作和开始下一个事务操作将合并为一个原子操作。这意味着下一个事务将看到上一个事务的结果。
链事务与带保存点的扁平事务区别?
A:后者可以回滚到任意正确的保存点。而链事务中的回滚仅限当前事务,即只能恢复到最近的一个保存点。对于锁的处理也不一样:链事务
在执行commit后即释放了当前事务所持有的锁,而带有保存点的扁平事务
不影响迄今为止所持有的锁。
分布式事务
通常是一个分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中不同节点。
InnoDB存储引擎支持的事务: 扁平事务、带有保存点的事务,链事务,分布式事务
重做日志
redo
重做日志,是ACID
中的D
,持久化;
其由两部分组成:
- 内存中的重做日志缓存
- 重做日志文件
InnoDB对事务的提交:必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的
commit
操作完成才算完成。
这里的日志是重做日志,其由两部分组成:redo log和undo log
默认情况下,InnoDB
每次将重做日志缓存
写入重做日志文件
后,都会调用一次fsync
操作。
当然,支持参数来控制重做日志
刷新到磁盘的策略;
innodb_flush_log_at_arx_commit | 描述 |
---|---|
0 | 事务提交时不进行写入重做日志操作,这个操作仅在master thread中完成;而master thread会每1秒进行一次fsync 操作 |
1 | 默认值;每次将重做日志缓存 写入重做日志文件 后,都会调用一次fsync 操作 |
2 | 事务提交时,将重做日志写入重做日志文件,但仅写入文件系统缓存中,不进行fsync |
二进制日志(binlog)
作用:用来进行point-in-time
的恢复和主从复制环境的建立。
binlog
和重做日志
的区别:
- 重做日志是InnoDB 存储引擎层产生,而二进制日志是MySQL数据上层产生;
- 二进制日志是一种逻辑日志,其记录的是对应的SQL语句。而重做日志是物理格式日志,其记录的是对于每个页的修改。
- 二进制日志只在事务提交完成后,进行一次写入。而重做日志在事务进行中不断被写入,这表现为日志并不是随事务提交的顺序进行写入
undo
因为事务有回滚的需求,所以在对数据库进行修改时,InnoDB
存储引擎不但会产生redo
,还会产生一定量的undo
.
作用:
① 用于事务的回滚
② 用于MVCC
的控制
undo
存储在数据库内部的一个特殊段segment
中,这个段称为undo
段。位于共享表空间中。
回滚
举例说明:
用户执行一个insert
10w条记录的事务。因为undo
的缘故,这个事务会导致分配一个新的段,即表空间变大。
现在用户执行rollback
进行回滚,会将插入的事务进行回滚,但是表空间的大小并不会因此而收缩。
当InnoDB
回滚时,它实际上做的是与先前相反的工作:
① 对于每个insert
操作,InnoDB
存储引擎会完成一个delete操作。
② 对于每个delete
操作,其会执行一个insert
操作;
③ 对于每个update
操作,其会执行一个相反的update
,将修改前的行返回去。
MVCC
举例:
当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo
读取之前的行版本信息,以此实现非锁定读取。
undo的结构:
从上图的结构我们知道:
InnoDB存储引擎有rollback segment
,每个回滚段中记录了1024
个undo log segment
,而在每个undo log segment
段中进行undo
页的申请。
共享表空间偏移量为
5
,(0,5)记录了所有rollback segment header
所在的页。该页的类型FIL_PAGE_TYPE_SYS
;
假设只有一个rollback segment
,那么支持同时在线的事务为1024
;
特别注意:
- 事务在
undo log segment
分配页并写入undo log
的这个过程同样需要写入重做日志
中。当事务提交时,存储引擎会做两件事情:
① 将undo log
放入链表中,以供purge
操作。
② 判断undo log
所在的页是否可以重用。 - 事务提交后,并不会马上删除
undo log
及undo log 所有的页。
不马上删除是因为,可能有其他事务需要通过
undo
来访问行记录之前的版本信息。
故事务提交时,将undo log
放入一个链表中,是否可以最终删除undo log
和undo log所在的页
由purge
线程来判断。
undo log 格式
undo log分为:
① insert undo log
② update undo log
因为insert
操作的记录,只对事务本身可见(事务原子性
),故该undo log
可以在事务提交后直接删除。不需要purge
操作。
而update undo log
记录的是delete
和update
操作产生的undo log
,为了支持MVCC
机制,提交事务时,不会删除。而是放入undo log
链表中等待purge
线程进行最后的删除。
最后我们举例来说明
① 插入insert
的情况
insert into t select 1, '1'
这种情况会产生一个类型TRX_UNDO_INSERT_REC
的undo log
;
提交事务后,该undo页
会被放入cache
中,以便下次事务需要时直接使用。
② 删除delete
的情况
delete form t where a=1;
这种情况会产生一个类型TRX_UNDO_DEL_MARK_REC
的undo log
;
提交事务后,undo
页会放入cache
列表中以供下次使用;
delete操作并不直接删除记录,而只是将记录标记为已删除,记录的最终删除是在
purge
操作中完成的。
③ 更新update
的情况
# 更新非主键的情况
update t set b = '2' where a = 1;
会产生一个TRX_UNDO_UPD_EXIST_REC
的undo log
。
# 更新主键的情况
update t set a = 2 where a = 1;
如果更新的是主键,那么操作会分两步完成:
① 将原主键记录标记为已删除,因此会产生已类型为TRX_UNDO_DEL_MARK_REC
的undo log
;
② 插入一条新记录,因此会产生一个类型为TRX_UNDO_INSERT_REC
的undo log
;
A. 从表中删除一行记录
TRX_UNDO_DEL_MARK_REC (将主键记入日志)
在删除一条记录时,并不是真正的将数据从数据库中删除,只是标记为已删除.这样做的好处是Undo Log中不用记录整行的信息.在undo时操作也变得很简单.
B. 向表中插入一行记录
TRX_UNDO_INSERT_REC (仅将主键记入日志)
TRX_UNDO_UPD_DEL_REC (将主键记入日志) 当表中有一条被标记为删除的记录和要插入的数据主键相同时, 实际的操作是更新这个被标记为删除的记录。
C. 更新表中的一条记录
TRX_UNDO_UPD_EXIST_REC (将主键和被更新了的字段内容记入日志)
TRX_UNDO_DEL_MARK_REC 和 TRX_UNDO_INSERT_REC,当更新主键字段时,实际执行的过程是删除旧的记录然后,再插入一条新的记录。
purge
delete和update操作可能并不直接删除旧数据。
# 表t 列a有聚集索引,列b上有辅助索引
delete from t where a = 1;
上面的SQL,InnoDB仅是将主键列等于1的记录delete flag
设置为1
,记录并没有删除;
purge
用于最终完成delete
和update
操作,这么设计的目的是为了支持MVCC
;
那么什么时候删除呢?若该行记录已不被任何其他事务引用,那么就可以进行真正的delete
操作。
流程:
根据事务提交的顺序,将undo log
进行链接,形成一个history
列表;
- 在执行
purge
过程中,InnoDB
存储引擎首先从history
列表中找到第一个需要被清理的记录,这里记为trx1
; - 清理之后引擎会在
trx1
的undo log所在的页中继续寻找是否存在可以被清理的记录,这里会找到事务trx3
,接着又找到trx5
,但是发现trx5
被其他事务所引用而不能清理。 - 然后再次去
history
列表中查找,发现这是最尾端的记录trx2
,接着找到trx2
所在的页,然后依次再把事务trx6
、trx4
的记录进行清理。 - 由于
undo page2
中所有的页都被清理了,因此该undo page
可以被重用。
小结:
1、InnoDB存储引擎这种先找history list
中找到undo log
,然后再从undo page
中找undo log
的设计模式是为了避免大量的随机读取操作,从而提高purge
的效率。
2、为了节省存储空间,一个页上允许多个事务的undo log
的存在。即undo page
可以被多个不同事务的undo log
重用
group commit
事务的提交过程:
① 修改内存中事务对应的信息,并且将日志写入重做日志缓存中。
② 调用fsync
来确保日志都从重做日志缓存中写入了磁盘。
我们都知道fsync
这个系统操作时IO
操作,非常耗时,如果每次事务提交都执行的话,那么数据库的性能将大大折扣。。。
因此InnoDB
引入了group commit
:
即:当某个事务在执行过程②
时,其他事务可以进行过程①
,这样就可以在过程②
执行时,可以将多个事务日志通过一次fsync
刷新到磁盘。
后面到了MySQL5.6
版本中,将事务提交分为几个步骤:
首先,在MySQL
上层进行提交时会按顺序将其(事务
)放入一个队列中,队列中的第一个事务称为leader
,其他事务称为follower
,leader
控制着follower
的行为;
① flush
阶段:将每个事务的二进制日志写入内存;
② sync
阶段:将内存中二进制日志刷新到磁盘,若队列中有多个事务,那么仅一次fsync
操作就完成了二进制日志的写入;这就是BLGC
③ commit
阶段:leader
根据顺序调用存储引擎层事务的提交,而InnoDB引擎层
本来就支持group commit
,因此性能非常好;
上面的步骤可以看出,①阶段和②阶段是用来保证binlog
提交的顺序;
这也是BLGC
的由来:binary log group commit
;
特别注意:Flush
和sync
阶段,是先保证队列中的事务都执行完毕后,才会执行commit
阶段;
当有一组事务进行
commit
时,其他事务可以进行flush
阶段,从而使group commit
不断生效;
在没有BLGC之前,MySQL为了保证二进制日志bin log
和InnoDB引擎层事务顺序一致,会使用prepare_commit_mutex
这个锁,
但是使用锁之后,那么引擎层面的group commit
失效;
Q:为什么要保证MySQL上层二进制日志的写入顺序和InnoDB层的事务提交顺序一致呢?
A:这是因为备份和恢复的需要:
若通过在线备份进行数据库恢复来重新建立replication,并假设有三个事务1,事务2,事务3;
他们bin log的顺序是 1 2 3,InnoDB 层事务提交顺序是2 3 1
那么InnoDB 引擎会检测事务T3在上下层两层(MySQL数据库上层和InnoDB层)都完成了提交,不需要再进行恢复,
此时,事务T1的数据就会丢失;
这也说明,数据恢复,是按照事务的顺序来的;
事务隔离级别
InnoDB存储引擎提供的事务隔离级别有:
事务隔离级别 | 支持级别 |
---|---|
read uncommitted | 脏读、不可重复读、幻读都会出现 |
read committed | 解决脏读;使用行锁 |
repeatable read | 解决幻读和不可重复度;使用next-key lock 行锁加范围锁 |
serializable | 用于分布式 |
读未提交
:别人改数据的事务尚未提交,我在我的事务中也能读到。
读已提交
:别人改数据的事务已经提交,我在我的事务中才能读到。
可重复读
:别人改数据的事务已经提交,我在我的事务中也不去读。
串行
:我的事务尚未提交,别人就别想改数据。
这4种隔离级别,并行性能依次降低,安全性依次提高。
不可重复读和幻读的区别?
其实幻读
只是不可重复读
的一个特殊情况,而且MySQL
官方文档,将它们都归为幻象问题
;
引起不可重复读发生的操作是:update
和delete
;
而幻读的操作是:insert
;
我个人倒是觉得,他两是一类问题;
分布式事务
InnoDB
是如何支持分布式事务的呢?
InnoDB
存储引擎提供了对XA
事务的支持,并通过XA
事务来支持分布式事务
的实现;
XA事务由一个或多个资源管理器、一个事务管理器,一个应用程序组成;
资源管理器:提供访问事务资源的方法;通常就是一个数据库;
事务管理器:协调参与全局事务中的各个事务。需要和参与全局事务的所有资源管理器进行通讯。
应用程序:定义事务的边界,指定全局事务中的操作;
在MySQL
分布式事务中,资源管理器就是MySQL
数据库,事务管理器为连接MySQL
服务器的客户端;
什么是分布式事务?
是指允许多个独立的事务资源参与到一个全局的事务中。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚。
说明:
① 事务资源通常是关系型数据库系统
② 必须设置隔离级别为serializable
分布式事务使用的是两段提交的方式。
第一阶段:
所有参与全局事务的节点都开始准备,告诉事务管理器它们准备好了。
第二阶段:
事务管理器
告诉资源管理器执行commit
或rollback
;
如果任何一个节点显示不能提交,则所有的节点都被告知需要回滚。
可以看出,和本地事务相比,分布式事务多了一次prepare
操作,待收到所有节点的同意信息后,再进行commit
或者rollback
操作。
内部XA事务
上面介绍的都是外部分布式事务;但是数据库内部还存在存储引擎
和插件
之间或者存储引擎
和存储引擎之间
的事务。
比如:binlog和InnoDB存储引擎之间。由于复制的需要;
上图中,如果执行完①、②后在步骤③之前,发送宕机,那么主从就会不一致;
为了解决这个问题MySQL
数据库在binlog
和InnoDB
存储引擎之间采用了XA
事务;
当事务提交时,InnoDB
引擎会先做一个prepare
的操作,将事务的xid
写入,接着写入二进制的日志,
如果在InnoDB
存储引擎提交前,MySQL
发送宕机了,那么MySQL数据库在重启后会先检查准备的uxid
事务是否已经提交,如果没有,则在存储引擎层在进行一次提交。
如下图:
MySQL提供的事务控制语句
在MySQL
命令行的默认设置下,事务都是自动提交的,即执行SQL
语句后就会马上执行COMMIT
操作。因此要显式地开启一个事务需要使用命令BEGIN
, START TRANSACTION
, 或者执行命令SET AUTOCOMMIT=0
, 禁用当前会话的自动提交。
显示开启一个事务
-
START TRANSACTION
|BEGIN
显式地开启一个事务。 -
COMMIT(COMMIT WORK)
提交事务。 -
ROLLBACK (ROLLBACK WORK)
回滚事务 -
SAVEPOINT identity
允许在事务中创建一个保存点,一个事务中可以有多个SAVEPOINT
。 -
RELEASE SAVEPOINT identity
删除一个事务的保存点,当没有一个保存点执行这语句时,会抛出一个异常。 -
ROLLBACK TO [SAVEPOINT] identity
和SAVEPOINT
命令一起使用。可以把事务回滚到标记点,而不回滚在此标记点之前的任何工作。6.1 例如,可以发出两个
UPDATE
语句,后面跟一个SAVEPOINT
, 然后又是两条DELETE
语句。如果执行DELETE
语句期间出现了某种异常情况,并且捕获到这个异常,同时发出了ROLLBACK TO SAVEPOINT
命令,事务就会回滚到指定的SAVEPOINT
,撤销DELETE
完成的所有工作,而UPDATE
语句完成的工作不受影响。 -
SET TRANSACTION
设置事务的隔离级别。
这里需要重点说下一个参数:completion_type
;
commit work
与completion_type
的关系,commit work
是用来控制事务结束后的行为,是chain
还是release
的,可以通过参数completion_type
来控制,默认为0(NO_CHAIN
),表示没有任何操作 与commit
效果一样。
当completion_type=1的时候,COMMIT WORK
等同于COMMIT AND CHAIN
, 表示马上自动开启一个相同隔离级别的事务。
当completion_type=2的时候,COMMIT WORK
等同于COMMIT AND RELEASE
, 在事务提交后会自动断开与服务器的连接。
关于长事务需要注意的地方
开发中,习惯性的会把set autocommit=0
,关闭MySQL
自动提交,这很容易导致出现长事务,建议使用set autocommit=1
,但是这样有些开发人员会纠结:多一次交互
,即:又得重写begin
开启一个事务。
有这种顾虑的情况下,可以考虑使用commit work and chain
语法。
也就是通过参数completion_type=1
,再使用commit work
来达到效果:
mysql> set names utf8;
Query OK, 0 rows affected (0.00 sec)
mysql> create table t(a int, primary key (a))engine=innodb;
Query OK, 0 rows affected (0.29 sec)
mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 1 |
+--------------+
1 row in set (0.00 sec)
mysql> set @@completion_type=1;
Query OK, 0 rows affected (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t select 1;
Query OK, 1 row affected (0.00 sec)
Records: 1 Duplicates: 0 Warnings: 0
mysql> commit work;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t select 2;
Query OK, 1 row affected (0.00 sec)
Records: 1 Duplicates: 0 Warnings: 0
mysql> insert into t select 2;
ERROR 1062 (23000): Duplicate entry '2' for key 'PRIMARY'
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)
# 回滚之后只有1这个记录,而没有2这个记录
mysql> select * from t;
+---+
| a |
+---+
| 1 |
+---+
1 row in set (0.00 sec)