Mysql复习计划(五)- 事务和事务日志
一. 事务
事务的基本概念:一组逻辑操作单元,使数据从一种状态变换到另一种状态。
事务的ACID特性:
- 原子性:原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚。
- 一致性:一致性是指事务执行前后,数据从一个合法性状态变换到另外一个 合法性状态
- 隔离性:事务的隔离性是指一个事务的执行 。
- 持久性:持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的。(持久性是通过事务日志来保证的。日志包括了重做日志和回滚日志 )
总结下就是:ACID作为事务的四大特性,原子性是基础,隔离性是手段,一致性是约束条件,目的是保持持久性。
事务的状态:
- 活动的:事务对应的数据库操作正在执行过程中。
- 部分提交的:当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,我们就说该事务处在部分提交的状态。
- 失败的:当事务处在活动的或者部分提交的状态时,可能遇到了某些错误而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在失败的状态。
- 中止的:如果事务执行了一部分而变为失败的状态,那么就需要把已经修改的事务中的操作还原到事务执行前的状态。即回滚,回滚完毕后,处于中止状态。
- 提交的:当一个处在部分提交状态的事务将修改过的数据同步到磁盘上后,此时处于提交状态。
如图:
1.1 事务的使用
显式事务的使用:
# 1. 步骤1,声明开启事务
# 方式1
begin;
# 方式2
start transaction [read only | read write | with consistent snapshot];
# 2.进行数据库DML操作
# 3.事务提交或者回滚
commit;
# 回滚
ROLLBACK;
# 将事务回滚到某个保存点。
ROLLBACK TO [SAVEPOINT];
READ ONLY
:标识当前事务是一个只读事务,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。READ WRITE
:标识当前事务是一个读写事务,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。WITH CONSISTENT SNAPSHOT
:启动一致性读。
隐式事务的使用,关键字:autocommit
show VARIABLES like 'autocommit';
默认开启,此时一条完整独立的SQL语句,其本身就是一个独立的事务。
注意:虽然事务默认是开启的,哪怕会自动提交事务,但是一旦我们声明了start transaction
后,后面的DML操作就不会自动提交数据,而是需要通过commit
命令来生效。
隐式提交数据的情况:
- 数据定义语言(
DDL
):指的是当我们使用create/alert/drop
等语句去修改数据库对象的时候,就会隐式的提交。
begin;
select .... # 事务M中的语句A
update .... # 事务M中的语句B
create table ... # 该语句会隐式的提交前边语句所属的事务M
- 隐式使用或修改mysql数据库的表:当使用
Alert user、Create user、Drop user、Grant、Rename user、revoke、set password
等操作时候,也会提交前面语句所属事务。 - 事务控制或者关于锁定的语句,三种情况:
1.当我们在一个事务还没提交或者回滚时就又使用
START TRANSACTION
或者BEGIN
语句开启了另一个事务时,会隐式的提交上一个事务
2.当前的autocommit
系统变量的值为OFF
,我们手动把它调为ON
时,也会隐式的提交前边语句所属的事务。
3.使用Lock Tables、Unlock Tables
等关于锁定的语句也会提交前边语句所属的事务。
1.2 数据并发问题
1.2.1 脏写
对于两个事务 Session A、Session B,如果事务Session A 修改了另一个未提交事务Session B 修改过的数据,那就意味着发生了脏写。
以上面示意图为例,当SessionB发生回滚之后,SessionA做的更新也就将不复存在,这种现象就叫脏写。
1.2.2 脏读
Session A 读取了已经被 Session B 更新但还没有被提交的字段。之后若 Session B 回滚 ,Session A 读取的内容就是临时且无效的。
1.2.3 不可重复读
Session A 读取了一个字段,然后 Session B 更新了该字段。 之后Session A 再次读取同一个字段,值就不同了。那就意味着发生了不可重复读。
1.2.4 幻读
Session A 从一个表中读取了一个字段, 然后 Session B 在该表中插入了一些新的行并满足SessionA当中的查询条件。之后如果 Session A 再次读取同一个表, 就会多出几行。那就意味着发生了幻读。
这几种并发问题的严重程度,从大到小进行排序:
- 脏写 > 脏读 > 不可重复读 > 幻读
1.3 事务隔离级别
Mysql中事务隔离级别一共有4种:
- 读未提交(
READ UNCOMMITTED
):在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读。 - 读已提交(
READ COMMITTED
):一个事务只能看见已经提交事务所做的结果,可以避免脏读。 - 可重复读(
REPEATABLE READ
):事务A在读到一条数据之后,此时事务B对该数据进行了修改并提 交,那么事务A再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读。Mysql的默认隔离级别。
- 可串行化(
SERIALIZABLE
):确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止 其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。
用图表做结论就是:
Mysql中默认的隔离级别是可重复读,我们也可以手动修改下事务的隔离级别。
# Mysql 5.7.20版本之前事务的查看方式
show variables like 'tx_isolation';
# 在这版本之后的Mysql,事务查看方式
show variables like 'transaction_isolation';
结果如下:
如何设置事务的隔离级别
第一种方式:
set [global | session] transaction isolation level [隔离级别]
这里的隔离级别指的是:
1.READ UNCOMMITTED
2.READ COMMITTED
3.REPEATABLE READ
4.SERIALIZABLE
第二种方式:
set [global | session] transaction_isolation = [隔离级别]
这里的隔离级别指的是:
1.READ-UNCOMMITTED
2.READ-COMMITTED
3.REPEATABLE-READ
4.SERIALIZABLE
备注:关于设置变量的时候,使用global
和session
的区别
- global:全局范围内影响。但是对于已经存在的会话是无效的。
- session:当前会话范围内影响。对当前会话的后续所有事务有效。
二. 事务日志
事务的隔离性由锁机制来实现,而事务的原子性、一致性和持久性则交给事务的redo
日志和undo
日志来保证。
首先,来看下两种日志的作用分别是什么:
Redo Log
:重做日志,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性。Undo Log
:回滚日志,回滚行记录到某个特定的版本,用来保证事务的原子性和一致性。
其次,两种日志都是存储引擎层InnoDB
生成的日志。看看两种日志的区别:
Redo Log
:记录的是物理级别上的页修改操作。比如页号、偏移量、写入了什么数据等。Undo Log
:记录的是逻辑操作日志,比如对某一行数据进行了插入操作,那么Undo Log
会记录一条与之相反的删除操作。用于事务的回滚。
2.1 Redo 日志
背景:InnoDB
存储引擎以页为单位来管理存储空间, 真正访问页之间,需要把磁盘上的页缓存到内存中的Buffer Pool
之后才可以访问。所有的变更都必须先更新缓冲池中的数据,然后缓冲池的脏页(内存中修改了,但是磁盘还没更新)会以一定的频率刷入到磁盘中(checkpoint
机制)。
为什么需要Redo
日志呢?先来看下背景:
问题1:
SQL修改量与刷新磁盘工作量严重不成比例。
有些时候我们可能仅仅修改了某页中的一条数据。但是InnoDB是以页为单位来进行磁盘IO的。因此在提交事务进行磁盘更新的时候,不得不将该数据所在的整个页进行刷新到磁盘中。这样的操作没必要。
问题2:
随机IO刷新较慢。
一个事务可能包含多个语句,倘若事务修改的这些页面并不相邻,意味着进行事务提交的时候,将缓冲池中的页刷新到磁盘这个动作的时候,需要进行很多的随机IO,也会带来性能问题。
所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好,这也就是需要Redo
日志的原因。
而InnoDB
引擎的事务采用了WAL
技术(Write Ahead Logging
),即先写日志,在写磁盘, 只有日志写入成功,才算事务提交成功。这里的日志也就是RedoLog
。
RedoLog
的优点:
- 降低了刷盘频率。
- 占用的存储空间比较小。
RedoLog
是顺序写入磁盘,并且在事务执行过程中,RedoLog
不断记录。
2.1.1 Redo 日志的组成
Redo日志主要分为两个部分:
部分一:重做日志缓冲(redo log buffer
),保存在内存中,容易丢失。
在Mysql服务器启动的时候,就会申请一块连续的内存空间,其划分为若干个连续的redo log block
,一个块占用512kb
,如图:
部分二:重做日志文件(redo log file
),保存到磁盘中,是持久的。 例如数据库data
文件下的这两个文件就是重做日志文件:
这里设置到这么几个参数:
innodb_log_files_in_group
:知名redo log file
的个数,命名格式如:ib_logfile0,ib_logfile1
,默认是2个。
innodb_log_file_size
:单个redo log
文件的大小,默认是48MB
。
上述两个文件,他们属于同一个日志文件组,一个文件组中的日志写入也是有顺序的,按照序号的大小,从小到大开始写:
2.1.2 Redo 的流程和刷盘策略
以更新事务为例:
从上面的流程我们得知:
redo log
并不是直接写入到磁盘中,而是先写入缓冲池里。- 再以一定的频率做刷盘,将数据写入到
redo log file
中。
InnoDB
存储引擎给出了三种刷盘策略,通过设置innodb_flush_log_at_trx_commit
参数,共有三种值:
-
值为0:表示每次事务提交时不进行刷盘操作。(系统默认
masterthread
每隔1s进行一次重做日志的同步)
-
值为1:表示每次事务提交时都将进行同步刷盘操作。也是InnoDB的默认刷盘行为。
-
值为2:表示每次事务提交时都只把
redo log buffer
内容写入page cache
,不进行同步。由os
自己决定什么时候同步到磁盘文件。
总结下就是:
- 第一种,全权交给主线程来每隔一秒来刷新,可能出现数据丢失(Mysql服务器挂了)。
- 第二种,事务提交一次,刷盘一次,不会丢失数据。
- 第三种,交给操作系统来同步,也会出现数据丢失(操作系统挂了)。
2.1.3 写入redo log buffer 的过程
首先,Mysql中对底层页的一次原子访问的过程叫做Mini-Transaction
。简称mtr
。
一个事务可能由若干语句组成,一个语句又可能由若干mtr
组成,而每个mtr
过程又可以包含多条redo
日志。示意图如下:
回到我们说写入redo log buffer
的过程。写入的顺序是有序的,在2.1.1小节当中,我们得知有个redo log block
的概念,多个连续的redo log block
也就是组成了重做日志缓冲池。写入的示意图如下:
注意:
- 不同的事务之间可能是并发执行的,因此在每个
block body
中写入redo log
的时候,可能是交替写入的。
2.2 Undo 日志
上面提到,Redo 日志是事务持久性的一个保证,中心思想:先写日志再写磁盘。 而Undo
日志是事务原子性的保证,同样的,在事务中更新数据也有个前置动作,先写入undo log
。
注意:undo log
的生成也会产生redo log
。因为undo log
也需要持久性的保护。
undo
日志用在什么地方呢?
- 某个事务执行到一半时发生了系统错误或者断电。
- 在事务执行过程中,通过输入
Rollback
语句来结束当前事务的执行。
那么此时,我们需要将数据改回原本的样子,这个过程就叫做回滚,也符合原子性的要求。从一个持久性状态到另一个持久性状态。即不包括这种中间态。
undo
日志作用:
- 回滚数据。
- MVCC(下文会说)。
2.2.1 Undo 日志存储结构
InnoDB
对undoLog
的管理采用段的方式,即回滚段(rollback segment
)。每个回滚段记录了1024个undo log segment
。在每个undo log segment
中再进行undo页
的申请。
undo页
:
- 当我们开启事务,需要写
undoLog
的时候,就得去undo log segment
中申请一块空闲位置,即申请undo页
。 - 在这个申请到的
undo页
中再进行回滚日志的写入。 undo页
具有重用性,事务提交的时候,并不会立刻删除对应的undo页
。而是将其放入到一个链表中。- 若
undo页
的使用空间小于四分之三
,则当前的undo
页可以被重用,即不会被回收。而是将其分配给下一个事务来使用。
回滚段和事务之间的关系:
- 每个事务只会使用一个回滚段,一个回滚段在同一时刻可能会服务于多个事务。
- 在事务进行的过程中,当数据被修改时,原始的数据会被复制到回滚段。
回滚段中的数据分类:
- 未提交的回滚数据(
uncommitted undo information
) :该数据关联的事务并未提交,因此用于实现读一致性,并且该数据是不能够被其他事务的数据所覆盖。 - 已经提交但未过期的回滚数据(
committed undo information
):该数据关联的事务已提交,但是受到undo retention
参数的保持时间的影响。 - 事务已经提交并过期的数据(
expired undo information
):事务已提交,并且超过了保持时间,属于过期数据,这部分的数据,在回滚段满了之后,会优先被覆盖。
undoLog
类型分为两种:
insert undo log
:insert
操作中产生的日志,只对事务本身可见,对其他事务不可见(隔离性)。因此该类型日志可以再事务提交之后直接删除,不需要进行purge
操作。update undo log
:针对delete
和update
操作产生的日志,可能需要提供MVCC机制。不能在事务提交的时候就删除,提交的时候放入到undoLog
链表,等待purge
线程来删除。
2.2.2 Undo 流程
以下是undo+redo
事务的简化流程:
需求如下:一共2条数据,A=1,B=2
。事务中作出更改:A=3,B=4
。那么流程简化如下:
1. begin;
2. 记录 A=1 到 undo log。
3. update A=3。
4. 记录 A=3 到 redo log。
5. 记录 B=2 到 undo log。
6. update B=4。
7. 记录 B=4 到 redo log。
8. 将 redo log刷新到磁盘。
9. commit;
只有Buffer Pool
的流程:
有了事务日志之后: