一、事务基本知识
1. 事务基本概述
1.1 存储引擎支持情况
通过 SHOW ENGINES
命令来查看MySQL支持的存储引擎有哪些,以及这些存储引擎是否支持事务。
从下图可知,在MySQL中只有InnoDB支持事务。
1.2 基本概念
事务:一组操作逻辑单元,使数据从一个状态变为另一个状态。
事务执行的原则:保证事务中的操作作为一个工作单元来完成,要么整个事务被提交,要么放弃所有的修改,回滚到最初的状态。
1.3 事务的ACID特性
-
原子性(atomicity):
原子性指事务是不可分割的工作单位,要么全部提交,要么全部失败回滚。
-
一致性(consistency):
**一致性是指事务执行前后,数据从一个合法状态变换到另一个合法状态。**这里说的合法状态是指语义上的,跟具体的业务相关。
举例1:A账户有200元,转账300元出去,此时A账户余额为-100元,此时就说数据是不一致的,因为余额小于0不是合法状态。
举例2:A账户有200元,转账50给B账户,A账户的钱被扣了,但是B账户因为各种意外导致余额并没有增加,此时就说数据是不一致的,因为总金额变小了不是合法的状态。
举例3:在数据库表中我们将姓名设置成了唯一性约束,这时在事务执行过程中导致了姓名不唯一,这是也说数据不一致,因为姓名重复不是合法的状态。
从上面可以看到,状态是否合法是由我们自己定义的,事务在执行前后都应该是合法状态。
-
隔离性(isolation):
隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间并不会相互干扰。
-
持久性(durability):
**持久性是指一个事务一旦被提交,它对数据库中的数据的改变就是永久性的。**接下来的其他操作或着故障都不应该对其有影响。
持久性是通过事务日志来保证的,日志包括重做日志和回滚日志。当我们通过事务对数据进行修改时,会将数据库的变化信息记录到重做日志中,然后再对数据库对应的数据进行修改。
ACID是事务的四大特性,在这四个特性中,原子性是基础,隔离性是手段,一致性是约束条件,持久性是目的。
1.4 事务的状态
事务是由一个或多个数据库操作构成的,MySQL根据这些操作执行的不同阶段把事务大致分为了几个状态。
-
活动的(active)
事务对应的数据库操作正在执行过程中,我们称该事务处在活动中的状态。
-
部分提交的(partially committed)
当事务的操作都执行完成了,但由于操作都在内存中执行,并没有刷新到磁盘中时,我们称该事务处在部分提交的状态。
-
失败的(failed)
当事务处在活动或者部分提交的阶段时,可能遇到某些错误(数据库自身的错误,操作系统错误,断电错误等)而无法继续执行,或者人为停止当前事务的执行,我们称该事务处在失败的状态。
-
中止的(aborted)
如果事务执行一部分而变为了失败的状态,那么就需要将已经修改的事务的操作还原到事务执行前的状态。也就是要撤销失败事务对当前数据库的影响,我们称撤销过程为回滚。当回滚完成后,我们称该事务处于中止状态。
-
提交的(committed)
当一个处于部分提交的事务将修改的数据同步到磁盘后,我们称该事务处在提交的状态。
2. 如何使用事务
使用事务有两种方式:显式事务和隐式事务。
2.1 显式事务
步骤一:使用 start transaction
或者 begin
,来开启一个事务。
start transaction
语句相较于 begin
,后面能跟修饰词:
read only
:表示当前事务只是一个可读事务,也就是说这个事务里面的数据库操作只能是读取数据,而不能是修改数据。read write
:表示当前事务是一个读写事务,也就是这个事务里面的数据库操作既能读取数据,也能修改数据。with consistent snapot
:启动一致性读。
事务的默认访问模式是读写模式。
步骤二:一系列事务中的操作
步骤三:提交事务或中止事务
提交事务:commit
中止事务:使用rollback
撤销事务 或者 使用 rollback to [SAVEPOINT]
撤销到某个保存点
关于SAVEPOINT相关操作有:
#创建保存点
SAVEPOINT 保存点名称
#删除保存点
RELEASE SAVEPOINT 保存点名称
2.2 隐式事务
MySQL默认情况下每条DML操作都是一个独立的事务,当DML语句执行完后,也会随之提交。可以通过变量 autocommit 来查看:
SHOW VARIABLES LIKE 'autocommit';
默认为打开的状态,可以通过set autocommit = false
来关闭自动提交事务。
当autocommit为关闭状态时,配合commit就能够实现自定义事务的功能。
SET autocommit = FALSE;
UPDATE accounts SET money = money - 50 WHERE NAME = 'AA';
UPDATE accounts SET money = money + 50 WHERE NAME = 'BB';
COMMIT;
当我们使用start transaction
或者 begin
显式开启一个事务时,即使 autocommit 为 true,也不会自动提交DML语句。
2.3 隐式提交事务的情况
-
数据库定义语言(DDL)
当我们使用CREATE,ALTER,DROP等语句去修改数据库对象时,会隐式地提交前边的语句或者上一个事务。
BEGIN; SELECT ... UPDATE ... ... CREATE TABLE ... #此语句会隐式提交前边语句
-
事务控制或者关于锁的语句
当一个事务还没有提交或者回滚时,又开启另一个事务,会导致上一个事务被自动提交。
BEGIN; SELECT ... UPDATE ... ... BEGIN; #此语句会隐式提交上一个事务
当前的autocommit为off,我们手动调为on时,会自动提交前边语句所属的事务。
使用
LOCK TABLES
、UNLOCK TABLES
等关于锁的语句时也会隐式提交前边语句所属的事务。 -
加载数据的语句
使用
LOAD DATA
语句来批量往数据库中导入数据时,也会隐式提交前边语句所属的事务。 -
关于MySQL复制的语句
使用
START SLAVE
、STOP SLAVE
、RESET SLAVE
等语句时会隐式提交前边语句所属的事务。 -
其他一些语句
使用
ANALYZE TABLE
、CACHE INDEX
、FLUSH
等语句也会隐式提交前边所属的事务。
3. 事务的分类
- 扁平事务:也就是最基本的事务,由start transaction或者begin开始,由commit或者rollback结束。
- 带有保存点的扁平事务:顾名思义,也就是带有保存点savepoint。
- 链事务:指一个事务由子事务链式组成。
- 嵌套事务:
- 分布式事务:
4. 事务的隔离级别
在某一时间内,可能有多个客户端和MySQL连接,每个客户端与服务器连接上后,就可以称为一个会话。每个客户端可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是说服务器可能需要同时处理多个事务,此时在并发情况下,就可能出现问题。
理论上来说,当某个事务在对某个数据进行访问时,其他事务就应该进行排队,但是这样对性能影响太大了。我们既想保持事务的隔离性,又想让服务器在处理访问相同数据的多个事务时性能高点,就需要进行一定的取舍了。
4.1 并发情况下的问题
1. 脏写
事务A修改了未提交的事务B修改过的数据,那有可能发生脏写。
以下面的例子为例,事务B将studentno为1的name改为了李四,但是并没有提交,此时事务A再将这条数据修改成了张三,并且提交了,这时数据库中关于这条记录的name字段为张三,但是如果事务B进行了回滚,则name字段又会变为李四,相当于事务A没有压根就更新过,也就是发生了脏写。
2. 脏读
事务A读取了已经被事务B更新但是没有提交的数据,之后若事务B进行了回滚,则事务A读取到的数据就是临时且无效的。
3. 不可重复读
事务A读取了一个字段,然后事务B更新了这个字段,之后事务A再次读取这个字段,发现值不一样了。
4. 幻读
事务A读取了一个字段,然后事务B新插入了几行,之后事务A再次读取同一张表,发现多了几行,这就意味着发生了幻读。
注意和不可重复读进行区别,不可重复读是更新字段导致的,而幻读是增加数据导致的。
4.2 SQL标准的四种隔离级别
对于上面并发情况下可能出现的问题,我们可以按照严重性来进行排个序:脏写 > 脏读 > 不可重复读 > 幻读。
考虑到性能原因,不太可能做到完全的隔离性,而是会舍弃一部分隔离性来换取一部分性能。当然隔离性越低,性能会越好,但是产生的问题也就越多。
在SQL中设立了4个隔离级别:
READ UNCOMMITTED
:**读未提交,**在该隔离级别中,事务可以看到未提交的执行结果。不能避免脏读、不可重复读、幻读。
READ COMMITTED
:读已提交,在该隔离级别中,事务只能看到已经提交的执行结果,这是大多数数据库的默认隔离级别(不是MYSQL默认)。可以避免脏读,不能避免不可重复读,幻读。
REPEATABLE READ
:**可重复读,**在该隔离级别中,事务多次读取同一数据,所获得的结果都是相同的,即使中间被其他事务修改了。可以避免脏读,不可重复读,但不可避免幻读。这是MySQL的默认隔离级别。
SERIALIZABLE
:**可串行化,**确保事务可以多次从表中读取到相同的行。在这个隔离级别下,禁止其他事务进行对该表进行插入,删除和更新操作,所有并发问题都可以避免,但是性能十分低下。能避免脏读、不可重复读、幻读。
由于脏写太严重了,因此所有的隔离级别都必须解决这个问题。
4.3 MySQL支持的四种隔离级别
不同的数据库厂商对SQL标准规定的四种隔离级别的支持不太一样,MySQL虽然支持四种隔离级别,但是和SQL标准规定的各级隔离级别还是有些出入的。MySQL在 可重复读的隔离级别下,是解决了幻读问题的。
**查看MySQL的隔离级别:**默认为REPEATABLE-READ(可重复读)
#MySQL5.7.20 之前的查看方式
SHOW VARIABLES LIKE 'tx_isolation'
#MySQL5.7.20之后通过transaction_isolation来替换tx_isolation
SHOW VARIABLES LIKE 'transaction_isolation'
#或者使用系统变量
SELECT @@transaction_isolation;
设置隔离级别:
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
二、MySQL的事务日志
上面讲到事务有4种特性:原子性,一致性,隔离性和持久性。
- 隔离性由锁机制实现
- 原子性,一致性,持久性由事务的redo日志和undo日志来实现。
redo log:
-
称为重做日志,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性。
-
记录的是“物理级别”上的页修改操作,比如页号xxx,偏移量yyy,写入了zzz数据,主要用于保证数据的可靠性。
undo log:
- 称为回滚日志,回滚记录到某个特定版本,用来保证事务的原子性和一致性。
- 记录的是逻辑操作日志,比如对某一行进行了INSERT语句操作,那么undo log就记录一条与之相反的DELETE操作,主要用于事务的回滚,也就是说undo log记录的是每个操作的逆操作。
1. redo 日志
InnoDB存储引擎是以页为单位来进行磁盘和内存的交互的。当需要访问页面时,首先需要从磁盘中的页缓冲到内存中的Buffer Pool之后才能访问,当进行更新时,首先更新缓冲池中的数据,然后再以一定的频率持久化到磁盘中。缓冲池的作用是为了来优化CPU和磁盘之间的速度差异。
1.1 为什么需要redo日志
虽然缓冲池可以帮助消除CPU和磁盘之间的鸿沟,checkpoint机制可以保证数据的最终落盘,然而由于checkpoint并不是每次变更后就触发的,而是master线程每隔一段时间去处理的,所有有可能存在事务刚提交后,刚写完缓冲池,数据库就宕机了,那么这段数据就是丢失的,无法恢复。
那么如何保证这个持久化呢?
一个简单的做法就是:在事务提交之前把该事务所修改的所有页面都刷新到磁盘中,但是这样存在一些问题:
-
修改量与刷新磁盘的工作量严重不成比例:
有时候我们仅仅只是修改了页面的一个字节,但是由于在InnoDB中是以页为单位来进行磁盘IO的,也就是说我们在该事务提交时不得不将一个完整的页面刷新到磁盘中,一个页面的大小为16KB,只修改一个字节就要刷新16KB的数据到磁盘中显然是小题大做了。
因此产生了redo日志这一解决办法:
事实上我们的目的是为了让提交的事务对数据库能够永久生效,即使系统崩溃,在重启后也能够把这种修改恢复回来,所以其实没必要在每次事务提交时就将修改的页面刷新到磁盘中,只需要记录一下修改了哪些东西就好了。
InnoDB引擎的事务采用了WAL技术(Write-Ahead Logging),这种技术的思想就是先写日志,再写磁盘,只有日志写入成功,才算事务提交成功,这里的日志就是redo log。当发生宕机时且数据未刷到磁盘时,可以通过redo log来进行恢复,保证ACID中的D(持久化)。
1.2 redo日志的好处
好处:
- redo日志降低了刷盘效率
- redo日志占用空间非常小
特点:
-
redo日志是顺序写入磁盘的
每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的。
-
事务执行过程中,redo log不断记录
redo log和bin log的区别,redo log是存储引擎层产生的,而bin log是数据库层产生的。假设一个事务,对表做了10万行记录的插入,在这个过程中,会一直不断万redo log顺序记录日志,而bin log并不是这样,而是直到这个事务提交了,才一次性写入到bin log中。
1.3 redo的组成
Redo log可以分为两个简单的部分:
-
redo log 的缓冲(redo log buffer):保存在内存中,容易丢失。
在服务器启动时就申请了一大片内存空间作为redo log的缓冲区,这片内存空间被划分成若干个连续的redo log block,一个redo log block占用512个字节。
-
redo log 的文件(redo log File):保存在磁盘中,是持久的。
1.4 redo log的刷盘策略
redo log的写入流程是这样的:首先先写入redo log buffer,然后再刷入文件系统(page cache)中去,最后真正的写入磁盘是交由操作系统自己来决定的。这里提到的page cache是操作系统为了提高文件写入效率而做的一个优化。
那这样也就是说,redo log的写入过程中有几个地方需要注意:一个是写入到redo cache的过程,一个是从redo cache写入到page cache的过程,一个是从page cache写入到磁盘的过程。这几个过程如果发生了系统宕机,那么都会导致数据丢失。
针对这些情况,InnoDB给出了 innodb_flush_log_at_trx_commit
参数,该参数控制commit提交事务时,如何将redo log buffer中的日志刷新到redo log File中,它支持三种策略:
- 设置为0:表示每次事务提交时不进行刷盘操作(系统默认master thread每隔1s进行一次重做日志的同步)
- 设置为1:表示每次事务提交时都将进行同步, 刷新到磁盘中(默认值)
- 设置为2:表示每次事务提交时只把redo buffer内容写入到page buffer中,接下来不进行同步,由操作系统每隔1s进行同步。
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'
从上面可以看出,对数据的保护程度:1 > 2 > 0,但是相应的性能:0 > 2 > 1,因此这里也涉及到了一个调优的点:可以适当将innodb_flush_log_at_trx_commit设置成2,会比1性能要好点。
流程图:
1.5 写入redo log buffer的过程
1. Mini-Transaction
MySQL把对底层页面的一次原子访问的过程称为一个Mini-Transaction,简称mtr。比如,插入一条数据就是一个Mini-Transaction,一个所谓的mtr可以包含一组redo日志,在进行数据恢复时这一组redo日志作为一个不可分割的整体。
一个事务可以包含若干个语句,一条语句其实是由若干个mtr组成,每个mtr又可以包含若干条redo日志,用画图表示:
2. redo 日志写入log buffer
redo log buffer是由 若干个redo log block构成的。当向redo log buffer写入数据时,是先从前边的block开始写,用完之后才往下一个block继续写。在这个过程中,需要解决这么一个问题:当我们写数据时,应该写在哪个block的哪个偏移量,所以Innodb提供了一个称为buf_free的全局变量,该变量指明后续写入的redo日志应该写在log buffer的哪个位置。
1.6 redo log File
1. 相关参数设置
innodb_log_group_home_dir
:指定了redo log文件组的所在位置,默认值是./,表示在数据库的数据目录下。MySQL下的默认数据目录(var/lib/mysql)下默认有两个名为ib_logfile0和ib_logfile1的文件,log buffer中的数据默认就是刷新到这两个磁盘文件中的。innodb_log_files_in_group
:指明了redo log File的个数,命名方式为ib_logfile0,ib_logfile1 …ib_logfilen。默认是2个,最多100个。inndb_flush_log_at_trx_commit
:控制redo log刷新到磁盘的策略,默认为1。innodb_log_file_size
:单个redo log文件的大小,默认值是48M,最大值是512G。
2. 日志文件组
从上面可知,磁盘中的redo log file并不只有一个,而是以一个日志文件组的形式出现的。在将redo日志写入日志文件时,是从ib_logfile0开始写,写满后接着ib_logfile1写,以此类推,那要是写到了最后一个文件怎么办,那就重新转到ib_logfile0继续写。
.但是重新回到ib_logfile0写有可能覆盖掉之前写的日志,因此还需要两个属性:write pos 和 checkpoint
- write pos:记录当前写的位置,一边写一边后移
- checkpoint:当前要擦除的位置,一边擦一边后移
因此write pos 和 checkpoint之间还空着的部分就是用来写入新的redo log记录的。
如果write pos追上了checkpoint,表示日志文件组满了。这时候就不能够再写入新的redo log记录了,需要先清空一些记录,把checkpoint推进一下。
2. undo 日志
redo log是事务持久化的保证,undo log是事务原子性的保证。
undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护。
2.1 undo日志的作用
1. 回滚数据
undo 日志只能是逻辑上恢复到原来的样子,只是说修改被逻辑上取消了,但是数据结构和页本身即使回滚回去可能还是无法恢复成原来一模一样的状态。
这是因为在多用户并发系统中,当一个事务在修改页中的某条数据时,同时有多个事务也在修改同一页的另外数据,因此不能将一个页回滚到事务原来的样子,这样会影响到其他事务的工作。
2. MVCC
在InnoDB存储引擎中MVCC的实现是通过undo 日志来实现的。当用户读取一条记录时,若该记录已经被其他事务占用了,那么当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取。
2.2 undo 日志的存储结构
1. 回滚段和undo页
2. 回滚段和事务
3. 回滚段中的数据分类
这里先挖坑。。。
2.3 undo的类型
在InnoDB存储引擎中,undo log可以分为:
-
insert undo log
指在insert操作中产生的undo日志,由于insert操作的记录只对事务本身可见,对其他事务并不可见(这是事务隔离性的要求),因此该undo log可以在事务提交后直接删除,不需要进行purge操作。
-
update undo log
update undo log记录的是update和delete产生的undo日志,该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。
2.4 undo log的生命周期
1. 简要生成过程
2. 详细生成过程
对于InnoDB存储引擎来说,每个行记录除了记录本身的数据外,还有几个隐藏列:
DB_ROW_ID
:如果没有显式为表定义主键,并且表中也没有唯一索引,那么InnnoDB会自动为表添加一个row_id的隐藏列作为主键。DB_TRX_ID
:每个事务都会分配一个事务ID,当对某条记录发生变更时,就会将这个事务的事务ID写入DB_TRX_ID中。DB_ROLL_PTR
:回滚指针,本质上就是指向undo log的指针。
。。。