1. MySQL事务基础

1.1. 并发事务带来的问题

数据库会存在并发执行多个事务,多个事务可能并发的对相同的数据库进行CURD操作导致并发问题。并发带来的问题包括数据丢失(脏写)、脏读、不可重复读、幻读。
MySQL事务.jpg

1.1.1. 更新丢失(脏写)

在两个或两个以上事务选择数据库中的同一行数据时,并基于最初选定的值更新该行数据时,因为每个事务之间都无法感知彼此的存在,所以会出现最后的更新操作覆盖之前由其他事务完成的更新操作情况。对于同一行数据,一个事务对该行数据的更新操作覆盖了其他事务对该行的数据更新操作
举例:张三账户余额有100元,事务A和事务B两个事务并发操作对张三账户余额增加,事务A将张三账户余额增加100元,事务B将张三账户余额增加200元。起初事务A和事务B同时读取到张三的账户余额是100元,然后事务A和事务B分别更新张三的银行账户余额,事务A提交后账户余额=100元+100元,事务B提交后账户余额=100元+200元,假设事务A比事务B先提交,那么最终张三账户的余额是300元。也就是说事务B覆盖了事务A的更新操作
MySQL事务-第 2 页.jpg

1.1.2. 脏读

事务A正在修改一条数据,在提交之前,有一个事务B来查询这条被修改的数据。如果没有对这两个事务进行控制,那么事务B会查询到事务A没有提交的数据(因为事务A还没有提交,就被事务B查询到了,所以称这个没有提交的数据未脏数据)
示例:
事务A和事务B两个事务,事务A向张三账户转账100元,事务B查询张三余额。事务A执行转账操作,在事务A未提交时,事务B查询到张三账户余额多了100元,后来事务A由于一些原因导致回滚。注意此时事务B读取到的是事务A没有提交的数据,也就是查询到了脏数据

1.1.3. 不可重复读

在一个事务中,根据同一个查询条件,两次及以上读取到的数据不一致,我们称这种情况叫脏读。
示例:
事务A和事务B,事务B第一次查询张三账户余额为100元,此时事务A向张三账户转账100元,然后事务B再次查询张三余额时发现张三账户余额多了100元,导致事务B两次查询张三账户余额结果不一样

1.1.4. 幻读

事务A按照同样的查询条件多次查询数据库时,在多次的查询间隙,事务B往数据库插入了满足事务A查询条件的数据。导致事务A多次查询的数据结果不一样,这种现象叫幻读。也就是说一个事多次读取相同查询条件的数据时,读取到的结果不一致
MySQL事务-第 3 页.jpg

1.1.5. 不可重复读和幻读的区别

我们很容易把幻读和不可重复读搞混淆,不可重复读和幻读的区别主要有以下几点

  1. 不可重复读的重点在于数据的更新和删除,而幻读的重点在于数据插入操作
  2. 使用锁机制实现事务隔离级别时,在“可重复读隔离级别”中,SQL语句第一次读取到数据后,会将对应的数据加锁,使得其他事务不能修改和删除这些数据,此时可以实现可重复读。但是这种方式无法对新插入的数据加锁,如果事务A读取了数据,此时事务B是可以往表中插入数据的,这样就会导致事务A读取了新插入的数据。
  3. 幻读无法通过行级锁来避免,需要使用串行化的事务隔离级别,但是这种事务隔离级别会极大降低数据库的并发性能
  4. 其实不可重复度和幻读最大的区别在于如果通过锁机制解决问题,除了使用悲观锁来避免不可重复读和幻读外,我们也可以使用乐观锁来处理。例如:MySQL 为了提高整体性能,使用了基于乐观锁的MVCC(多版本控制)机制来避免不可重复读和幻读

1.2. MySQL 事务隔离级别

MySQL 中的InnoDB存储引擎提供了四种事务隔离级别,分别为读未提交、读已提交、可重复读、串行化

隔离级别 读数据一致性 脏读 不可重复读 幻读
未提交读 最低级别,只保证不读取物理上损坏的数据
已提交读 语句级
可重复读 事务级
可串行化 最高级别,事务级
  1. 未提交读(read-uncommitted)

在一个事务中,可以读取到其他事务未提交的数据变化,这种读取其他会话还没提交的事务,叫做脏读现象,在生产环境中切勿使用。

  1. 已提交读(read-committed)

在一个事务中,可以读取到其他事务已经提交的数据变化,这种读取也就叫做不可重复读,因为两次同样的查询可能会得到不一样的结果。

  1. 可重复读(repetable-read)

MySQL默认隔离级别,在一个事务中,直到事务结束前,都可以反复读取到事务刚开始时看到的数据,并一直不会发生变化,避免了脏读、不可重复读现象,但是它还是无法解决幻读问题。

  1. 可串行化(serializable)

这是最高的隔离级别,它强制事务串行执行,避免了前面说的幻读现象,简单来说,它会在读取的每一行数据上都加锁,所以可能会导致大量的超时和锁争用问题。

1.3. MySQL锁分类

锁是一种协调多个进程或者多个线程对同一个资源的访问机制,MySQL中的锁可以从以下几个方面进行分类
MySQL事务-第 4 页.jpg

1.4. 死锁的产生

锁虽然能解决并发问题,但是如果用的不对,就有可能造成死锁。发生死锁的必要条件有四个,分别是互斥条件、不可剥夺条件、请求与保护条件、循环等待条件。只有这4个条件都满足时才会发生死锁
MySQL事务-第 5 页.jpg

1.4.1. 互斥条件

在一段时间内,某个资源只能被一个进程占用。此时如果有其他进程请求该资源时,则只能等待

1.4.2. 不可剥夺条件

某个进程在获得资源后,在使用完毕之前,不能被其他进程强行夺走,只能由获得资源的进程主进程释放

1.4.3. 请求与保持条件

进程已经获得了至少一个资源,又要请求其他资源,但请求的资源已经被其他进程占有,此时请求的进程就会被阻塞,并且不会释放自己已获得的资源

1.4.4. 循环等待条件

系统中进程之间相互等待,同时各自占用的资源又会被下一个进程所请求。例如有进程A、进程B、进程C三个进程,进程A请求的资源被进程B占有,进程B请求的资源被进程C占有,进程C请求的资源被进程A占有,于是形成了循环等待条件

1.5. 如何避免死锁

有四种处理死锁的方法,分别是预防死锁、避免死锁、检测死锁、解除死锁

1.5.1. 预防死锁

处理死锁最直接的方法就是破坏造成死锁的四个必要条件中的一个或者多个,以防止死锁的发生

1.5.2. 避免死锁

在系统资源分配过程中,使用某种策略或者方法防止系统进入不安全的状态,从而避免死锁的发生

1.5.3. 检测死锁

检测死锁这种方法允许系统在运行过程中发生死锁,但是能够检测死锁的发生,并采取适当的措施清除死锁

1.5.4. 解除死锁

当检测出死锁后,采用适当的策略和方法将进程从死锁状态解脱出来,在实际工作中通常采用有序资源分配法和银行家算法这两种方式来避免死锁。

2. MySQL 事务实现原理

MySQL事务的实现离不开Redo Log和Undo Log,事务的隔离性是由锁和MVCC 机制实现的,原子性和持久性由Redo Log 实现,一致性由Undo Log 实现

2.1. Redo Log

2.1.1. Redo Log 概念

Redo Log 被称为重做日志,它是InnoDB存储引擎中产生的,用来保证事务的原子性和持久性。Redo Log 主要记录的是物理日志,也就是对磁盘上的数据进行修改操作。Redo Log 通常包含两部分,一部分是内存中的日志缓存,称为Redo Log Buffer,这部分日志比较容易丢失,另一部分是存在磁盘上的重做日志文件,称为Redo Log File,这部分日志持久化到磁盘上的,不容易丢失

2.1.2. Redo Log 基本原理

Redo Log 能够保证事务的原子性和持久性,在MySQL发生故障时,尽力避免内存的脏页数据写入数据库表的IBD文件。在MySQL重启时,可以根据Redo Log恢复事务已经提交但还未写入IBD文件的数据,从而对事务提交的数据进行持久化操作
示例:在订单业务中,用户提交订单时,系统会创建一条订单记录到订单表中,那么MySQL中Redo Log 写入过程如下图所示:
MySQL事务-第 6 页.jpg
MySQL 在提交事务时,会将写入数据先写入Redo Log Buffer,而后Redo Log Buffer 会根据Redo Logo 的刷盘规则写入Redo Log file文件中。当MySQL 发生故障重启时,会将Redo Log中的数据恢复到IBD文件中。
Redo Log Buffer是存放在内存中的数据,Redo Log 文件是存放在磁盘的数据,因为内存的速度快,所以先写入内存,在从内存中写入磁盘,提高吞吐量

2.1.3. Redo Log 刷盘规则

在MySQL额InnoDB存储引擎中,当我们向表中insert一条数据时,MySQL不会直接将数据写入数据库表中,而是先把数据写如到Redo Log Buffer中,而后根据刷盘规则把数据写入Redo Log file中,最后从Redo Log file中把数据写入IBD文件中(表中的数据)。
MySQL事务-第 7 页.jpg
Log Buffer 写入Redo Log Buffer 到Redo Log 文件时需要经过内核空间OS Buffer,这是因为在打开日志文件时,没有使用O_DIRECT 标志位,而O_DIRECT 标志位可以不经过系统内核空间OS Buffer直接向磁盘写数据

那么Redo Log Buffer中的数据是通过什么样的频率刷盘到Redo Log文件呢?在InnoDB存储引擎中,提供了四种刷盘机制

  1. 开启事务,发出提交事务指令后是否刷新日志(从Redo Log Buffer 写入数据到磁盘中的Redo Log 文件)由变量innodb_flush_log_at_trx_commit决定
  2. 默认每秒刷新一次,刷新日志的频率由变量innodb_flush_log_at_timeout的值来调整刷新频率
  3. 当Log Buffer 中已经使用的内存超过一半时,触发刷盘操作
  4. 当事务中存在checkpoint(检查点)时,在一定程度上代表了刷写到磁盘时日志所处的LSN的位置

innodb_flush_log_at_trx_commit 变量不同取值区别
innodb_flush_log_at_trx_commit变量的取值由0,1,2,默认位1。

  1. 变量设置位0,则每次提交事务时,不会将Log Buffer中的日志写入OS Buffer,而是通过一个单独的线程,每秒写入OS Buffer 并调用fsync()函数写入磁盘的Redo Log 文件。这种方式不是实时写入磁盘的,而是每隔1秒钟写入一次,如果系统崩溃,可能会丢失1s的数据
  2. 变量设置为1,则每次提交事务时都会将Log Buffer中的日志写入OS Buffer,并且会调用fsync()函数将日志数据写入磁盘的Redo Lo 文件中。这种方式会实时的写入磁盘,在系统崩溃时数据不会丢失,但是性能比较差。是变量innodb_flush_log_at_trx_commit 的默认值
  3. 变量设置为2,则每次提交事务时,都只是将数据写入OS Buffer,之后每隔1s,通过fsync()函数将OS Buffer中的数据同步写入Redo Log 文件中。

注意在MySQL中,有一个变量innodb_flush_log_at_timeout 的值为1,这个变量表示刷新日志频率。另外在InnoDB存储引擎中,刷新数据页到磁盘和刷新Undo Log页到磁盘就只有一种检查点规则

2.1.4. Redo Log 写入机制

Redo Log 主要记录的是物理日志,其文件内容是以顺序循环的方式写入的,一个文件写满时会写入另一个文件,最后一个文件写满时,会向第一个文件写数据,并且是覆盖写
MySQL事务-第 8 页.jpg

  1. Write Pos 是数据库表中当前记录所在位置,随着不断地向数据库表中写数据,这个位置会向后移动,当移动到最后一个文件的最后一个位置时,又会回到第一个文件的开始位置进行写操作
  2. CheckPoint 是当前要擦除的位置,这个位置也是向后移动的,移动到最后一个文件的最后一个位置时,也会回到第一个文件的开始位置进行擦除。只不过在擦除记录之前,需要把记录更新到数据库文件中
  3. Write Pos和CheckPoint 之间的间隔,这个间隔表示还可以记录新的操作。如果Write Pos 移动的速度较快,追上了CheckPoint,则表示数据已经写满不能再向Redo Log 文件中写数据了,此时需要停止写入数据,擦除一些记录


2.1.5. Redo Log 的LSN 机制

2.2. Undo Log

2.2.1. Undo Log 概念

Undo Log MySQL事务的实现中主要起到两个作用,回滚事务和多版本并发事务,也就是常说的MVCC机制

MySQL在启动事务之前,会先将要修改的数据记录存储到Undo Log 中,如果数据库的事务回滚、崩溃等,可以利用Undo Log 对数据库中未提交的事务进行回滚操作,从而保证数据的一致性

Undo Log 会在事务开始产生前,当事务提交时,并不会立刻删除相应的Undo Log。此时InnoDB存储引擎会将当前事务对应的Undo Log放入待删除的列表,然后通过一个后台线程purge thread 进行删除处理

Undo Log和Redo Log 不同,Undo Log 记录的是逻辑日志,当数据库执行一条insert语句时,Undo Log 会记录一条对应的delete语句。当数据库执行一条delete语句,Undo Log 会记录一条对应的insert语句。当数据库执行一条update语句,Undo Log 会记录一条相反的update语句

当执行事务回滚操作或者数据库崩溃时,可以从Undo Log 中读取相应的数据记录进行回滚操作

MySQL 中的多版本并发控制也是通过Undo Log 实现的,当select 语句查询的数据被其他事务锁定时,可以从Undo Log中分析出当前数据库之前的版本,从而向客户端返回之前版本的数据

需要注意的是,因为MySQL事务执行过程中产生的Undo Log 也需要进行持久化操作,所以Undo Log 也会产生Redo Log。由于Undo Log 的完整性和可靠性需要Redo Log 来保证,因此数据库崩溃时需要先做Redo Log数据恢复,然后再做Undo Log回滚

2.2.2. Undo Log 存储方式

在InnoDB 存储引擎对于Undo Log的存储采用段的方式进行管理,在InnoDB存储引擎的数据文件中存在一种叫作rollback segement 的回滚段,这个回滚段内部有1024个undo log segement 段

Undo Log 默认存放哎共享数据表空间中,默认未ibdata1文件中。如果开启了innodb_file_per_table参数,就会将Undo Log 存放在每张表的.ibd文件中

默认情况下,InnoDB 存储引擎会将回滚段全部写在同一个文件中,也可以通过innodb_undo_tablespaces变量将回滚段平均分配到多个文件中。innodb_undo_tablespaces变量的默认值未0,表示将rollback segment 回滚段全部写到同一个文件中

2.2.3. Undo Log 基本原理

Undo Log 写入磁盘时和Redo Log 一样,默认情况下都需要通过内核OS Buffer 写入磁盘。
和Redo Log 一样,如果设置了O_DIRECT 标志位,就可以不经过内核OS Buffer写入磁盘,而是直接写入磁盘
MySQL事务-第 9 页.jpg

MySQL 数据库事务提交之前,InnoDB 存储引擎会将数据表中修改前的数据保存到Undo Log Buffer,然后再持久化到磁盘的Undo Log 文件中。当数据库发生故障重启或者事务回滚时,InnoDB 存储引擎会读取Undo Log 中的数据,将事务还没有提交的数据回滚到最初状态。

2.2.4. Undo Log 实现MVCC机制

Undo Log 除了实现事务回滚操作外,还有一个作用就是实现多版本并发控制,也就是MVCC机制。在事务提交之前,向Undo Log 保存事务当前的数据,这些保存到Undo Log 中的旧版本数据可以作为快照供其他并发事务进行快照读取。

Undo Log 的回滚段中,undo logs 分为insert undo log 和 update undo log

  1. insert undo log:事务对插入新记录产生的Undo Log,只有在事务回滚时需要,在事务提交之后可以丢弃
  2. update undo log:事务对记录进行删除和更新操作时产生的Undo Log,不仅在事务回滚时需要,在一致性读取时也需要,因此不能随便删除,只有当数据库所使用的快照不涉及该日志时,对应的回滚日志才会被purge线程删除

MySQL在实现MVCC时,InnoDB存储引擎在数据库每行数据的后面添加3个字段,6字节的事务id(DB_TRX_ID)字段、7字节的回滚指针(DB_ROLL_PTR)字段、6字节的DB_ROW_ID字段

  1. 6字节的事务ID(DB_TRX_ID)字段

用来标识最近一次修改(insert、update)本行的事务id,如果是delete操作,在InnoDB存储引擎内部也属于一次update操作,即更新行中的一个特殊位,将行标识位已删除,并非真正删除

  1. 7字节的回滚指针(DB_ROLL_PTR)字段

指向上一个版本的记录行,能够从最新版本的记录逐级向上,找到要查找的行版本记录

  1. 6字节的DB_ROW_ID字段

包含一个随着新数据行的插入操作而递增的行id,当由InnoDB在存储引擎自动产生聚集索引时,聚集索引会包含这个行id,否则这个行id不会出现在任何索引中。

示例说明MVCC机制:
事务A和事务B两个事务,事务A对商品数据库表中的库存字段进行更新,同时事务B读取商品的信息,Undo Log 实现的MVCC 机制流程如下:
手动开启事务A,更新商品表中id=1的数据,在更新之前首先会把更新前id=1的数据写入Undo Log Buffer中。在事务A没有提交之前,事务B手动开启事务查询商品表中id=1的数据,此时事务B查询的数据实际是Undo Log Buffer中的数据,Undo Log Buffer中的数据是修改前的数据。
MySQL事务-第 10 页.jpg

3. Bin Log

3.1. Bin Log 基本概念

Bin Log 是一种记录MySQL 数据库表结构变更以及表数据变更的二进制日志。BinLog 中不会记录诸如select、show 这类查询操作日志,同时BinLog 是以事件形式记录相关变更操作的,斌且包含语句执行所消耗的事件,BinLog 由以下两个最重要的使用场景。

  1. 主从复制:在数据库上开启BinLog,主数据库把BinLog发送给从数据库,从数据库获取BinLog后通过I/O线程将日志写到中继日志,也就是Relay Log中。然后通过SQL线程将Relay Log中的数据同步至从数据库,从而达到主从数据库的一致性
  2. 数据恢复:当MySQL 数据库发生故障或者奔溃时,可以通过BinLog 进行数据恢复。如:可以使用mysqlbinlog 等工具进行数据恢复

3.2. BinLog 记录模式

BinLog 主要由Row、Statement、Mixed三种记录模式

  1. Row 模式

Row 模式下的BinLog 文件会记录每一行数据被修改的情况,然后再MySQL从数据库中对相同的数据进行修改

Row 模式的优点是能够非常清楚地记录每一行数据地修改,完全实现主从数据库地同步和数据恢复

Row 模式地缺点是如果主数据库中发生批量操作,尤其是大批量地操作,会产生大量的二进制日志。比如:使用alter table操作修改拥有大量数据的数据表结构时,会使二进制日志的内容暴涨,产生大量的二进制日志,从而大大影响主从数据库的同步性能

  1. Statement 模式

Statement 模式下的BinLog 文件会记录每一条修改数据的SQL语句,MySQL从数据库在复制SQL语句的时候,然后在从数据库上执行SQL 进程解析出来的SQL语句

Statement 模式的优点是由于不记录数据库的修改细节,只是记录数据表结构和数据变更的SQL语句,因此产生的二进制日志数据量比较小,这样能够减轻磁盘的I/0操作,提升数据存储和恢复能力

Statment 模式的缺点是在某些情况下,可能会导致主从数据库中的数据不一致。例如:在MySQL主数据库中使用了last_insert_id()和now()等函数,会导致MySQL主从数据库不一致

  1. Mixed 模式

Mixed 模式下的BinLog 是Row模式和Statment 模式的混用。在这种模式下,一般会使用Statment模式保存BinLog,如果存在Statment 模式无法复制的操作,例如在MySQL主数据库中使用last_insert_id()和now()等函数,MySQL 会使用Row模式保存BinLog。Mixed模式会根据MySQL 执行的SQL语句选择合适的写入记录模式

3.3. Bin Log 写入机制

MySQL 事务在提交的时候,会记录事务日志和二进制日志,也就是Redo Log 和 BinLog。那么对于事务日志和二进制日志,MySQL 会先记录哪一种呢?
Redo Log 是InnoDB存储引擎特有的日志,BinLog是MySQL本身就有的上层日志,并且会先于InnoDB 的事务日志被写入,因此在MySQL中,二进制日志会先于事务日志被写入
MySQL在写入BinLog二进制日志时,会按照如下规则进行写操作

  1. 根据记录模式(Row、Statment、Mixed)和操作(create、drop、alter、insert、update等)触发事件生成日志事件(事件触发执行机制)
  2. 将事务执行过程中产生的日志事件写入相应的缓冲区。注意每个事务线程都有一个缓冲区。日志事件保存在数据结构binlog_cache_mngr中,这个数据结构中有两个缓冲区:一个是stmt_cache,用于存放不支持事务的信息,另一个是trx_cache,用于存放支持事务的信息
  3. 事务在Commit阶段会将产生的日志事件写入磁盘的BinLog 文件中。因为不同的事务会以串行的方式将日志事件写入BinLog文件中,所以一个事务中包含的事务事件信息在BinLog文件中是连续的,中间不会插入其他事务的日志事件

所以,一个事务的BinLog 是完整的,并且中间不会插入其他事务的BinLog

3.4. BinLog和Redo Log区别

BinLog和Redo Log 在一定程度上可以恢复数据,但二者还有有这本质的区别。

  1. BinLog是MySQL本身拥有的,不管使用何种存储引擎,BinLog都存在,而Redo Log 是InnoDB 存储引擎特有的,只有InnoDB存储引擎才会有Redo Log
  2. BinLog是一种逻辑日志,记录的是对数据库的所有修改操作,而Redo Log 是一种物理日志,记录的是每个数据页的修改
  3. Redo Log具有幂等性,多次操作的前后结果是一致的,而BinLog不具有幂等性,记录的是所有影响数据库的操作。例如插入一条数据后再将其删除,则Redo Log前后的状态未发生变化,而BinLog就会记录插入操作和删除操作
  4. BinLog开启事务时,会将每次提交的事务一次性写入内存缓冲区,如果没有开启事务,则每次成功执行插入、更新、删除语句时,就会将对应的事务信息写入内存缓冲区。Redo Log是在数据准备修改之前将数据写入Redo Log Buffer中,然后再Buffer中修改数据。而且再提交事务时,先将Redo Log写入缓冲区,写入完成后再提交事务
  5. BinLog 只会再事务提交时,一次性写入BinLog,其日志的记录方式与事务的提交顺序有关,并且一个事务的BinLog中间不会插入其他的事务BinLog。而Redo Log记录的是物理页的修改,最后一个提交的事务会覆盖之前所有未提交的事务记录,并且一个事务Redo Log中间会插入其他的事务Redo Log
  6. BinLog 是追加写入,写完一个日志文件再写下一个日志文件,不会覆盖使用,而Redo Log 是循环写入,日志空间大小是固定的,会覆盖使用
  7. BinLog一般用于主从复制和数据恢复,并且不具备奔溃自动恢复能力。Redo Log 是再服务器发生故障后重启MySQL,用于恢复事务已经提交但没有写入数据表的数据

4. MySQL事务流程

4.1. MySQL事务执行流程

MySQL 在事务执行的过程中,会记录相应SQL 语句的Undo Log和Redo Log,然后在内存中更新数据并形成数据脏页。接下来Redo Log会根据一定的规则触发刷盘操作,Undo Log 和数据脏页则通过检查点机制刷盘。事务提交时,会将当前事务相关的所有Redo Log 刷盘,只有当前事务的相关所有Redo Log 刷盘成功,事务才算提交成功
MySQL事务-第 11 页.jpg

4.2. MySQL事务恢复流程

如果在事务提交之前,MySQL崩溃或者宕机,此时会先使用Redo Log 恢复数据,然后使用Undo Log 回滚数据。如果在事务提交之后MySQL奔溃或者宕机,此时会使用Redo Log 恢复数据。事务恢复流程如下:
MySQL事务-第 12 页.jpg
在MySQL 发生崩溃或者宕机时,需要重启MySQL,在重启MySQL之后会获取日志检查点信息,随后根据日志检查点信息使用Redo Log 恢复数据。如果在MySQL 崩溃或者宕机时,事务没有提交则使用Undo Log 回滚事务,如果事务已经提交则使用Redo Log 恢复数据