阅读 5797

『MySQL』深入理解事务的来龙去脉

MySQL系列文章:

『MySQL』搞懂 InnoDB 锁机制 以及 高并发下如何解决超卖问题

「MySQL」高性能索引优化策略

『MySQL』揭开索引神秘面纱

『MySQL』MySQL执行流程

一张思维导图看完系列文章:

距离上一篇MySQL的文章已经过去一个月了,终于有时间来写写关于MySQL的事务了。本文内容默认是针对 MySQL InnoDB 引擎。

先来一张思维导图读全文内容:

1. 为什么需要有事务

了解事务之前,先来看看数据库为什么需要有事务,假设没有事务会有什么影响?

举一个转账的例子,假设你朋友向你借10000元,你打开APP,乐呵呵的把钱转了,你的卡里已经少了10000元,但是你打电话给朋友时,你朋友说没有收到啊,你这时候肯定卖银行怎么不靠谱,没到账怎么把我卡里的钱给扣了。

我们来捋一捋上述银行发生的过程,简单的分三步:

A发起转账10000给B -> A银行卡减10000元 -> B银行卡增加10000元。

上述案例是第三步出现了问题,如果有事务,则不会发生案例中的事情,可以理解为事务就是这三个步骤是一根绳子上的蚂蚱,要么都成功,要么都失败。

所以数据库引入事务的主要目的是事务会把数据库会从一种一致状态转换到另一种一致状态,数据库提交工作时可以确保要么所有修改都保存,要么所有修改都不保存。

了解事务,还需要了解事务的理论依据ACID,也可以说事务的几个特性。

1.1 ACID

1.1.1 A(Atomicity) 原子性

还是上面转账的例子,原子性强调转账从A-B的三个步骤必须要么都成功,要么都不成功。

原子性是整个数据库事务是不可分割的工作单位,只有事务中的所有的数据库操作都执行成功,才算整个事务成功。事务中任何一个SQL执行失败,已经执行成功的SQL语句也必须撤销,回到执行事务的之前的状态。

1.1.2 C(Consistency) 一致性

一致性是指事务将数据库从一种一致性状态变为下一种一致性状态。在事务开始之前和之后,数据库的完整性约束没有被破坏。

上面转账的例子,无论转账成功或者失败,A和B加起来变化就是10000元,不会多也不会少。

1.1.3 I(Isolation) 隔离性

隔离性要求每个读写事务对其他事务的操作对象能相互分离。

比如A转账的银行是工商银行,那么别人在工商银行转账不能干扰A的转账行为。

1.1.4 D(Durability) 持久性

持久性指事务一旦提交,其结果就是永久性的。

2. 事务的实现

事务的实现就是如何实现ACID特性,下面一图下概况下:

由上图看,事务的实现通过 redo_log 和 undo_log, 以及锁实现,锁实现事务的隔离,上一篇已经讲解InnoDB的锁,需要了解的朋友可以查看上一篇文章。

redo_log 实现持久化和原子性,而undo_log实现一致性,二种日志均可以视为一种恢复操作,redo_log是恢复提交事务修改的页操作,而undo_log是回滚行记录到特定版本。二者记录的内容也不同,redo_log是物理日志,记录页的物理修改操作,而undo_log是逻辑日志,根据每行记录进行记录。

2.1 redo log 重做日志

redo_log 重做日志上面已经提到实现持久化和原子性,重做日志由两部分组成,一是内存中的重做日志缓存(redo log buffer),这部分是容易丢失的。二是重做日志文件(redo log file),这部分是持久的。

知道redo_log是什么?还需要了解其更新流程以及redo log存的是什么内容和恢复机制。

2.1.1 更新流程

先来了解第一个问题,redo log的更新流程如下图,以一次Update 操作为例。

  • 执行update操作。
  • 先将原始数据从磁盘读取到内存,修改内存中的数据。
  • 生成一条重做日志写入redo log buffer,记录数据被修改后的值。
  • 当事务提交时,需要将redo log buffer中的内容刷新到redo log file。
  • 事务提交后,也会将内存中修改数据的值写入磁盘。

为了确保每次日志都写入重做日志文件,InnoDB存储引擎会调用一次fsync操作。

2.1.2 存储格式内容

了解redo log存储格式和内容之前,先来对比一下跟binlog二进制日志由什么不同,binlog主要是主从复制和进行POINT-IN-TIME的恢复,想必大家对它不陌生。

binlog只有在事务提交的时候才会写入,且是数据库的上层产生的。redo log是Innodb引擎层产生的。

对比一二者的写入方式:

binlog是每次事务才写入,所以每个事务只会有一条日志,记录的逻辑日志,也可以说记录的就是SQL语句。

redo log是事务开始就开始写入,*T1表示事务提交。记录的是物理格式日志,即每个页的修改。

redo log默认是以block(块)的方式为单位进行存储,每个块是512个字节。不同的数据库引擎有对应的重做日志格式,Innodb的存储管理是基于页的,所以其重做日志也是基于页的。

redo log格式:

  • redo_log_type 重做日志类型
  • space 表空间的ID
  • page_no 页的偏移量
  • redo_log_body 存储内容

执行一条插入语句,重做日志大致为:

INSERT INTO user SELECT 1,2;
           |
page(2,3), offset 32, value 1,2 # 主键索引
page(2,4), offset 64, value 2   # 辅助索引
复制代码

可以看到重做日志存储的格式有点看不太懂,看不懂没有关系,主要是告诉大家,重做日志存储物理格式日志,也就是基于存储页的修改。

2.1.3 恢复机制

再来了解一下 redo log的恢复机制:

上图概况了重做日志的恢复机制,先来解释一下图中出现的 LSN 是什么?

LSN(Log Sequence Number) 日志序列号,Innodb里,LSN占8个字节,且是单调递增的,代表的含义有: 重做日志写入的总量、checkpoint的位置、页的版本。

假设在LSN=10000的时候数据库出现故障,磁盘中checkpoint为10000,表示磁盘已经刷新到10000这个序列号,当前redolog的checkpoint是13000,则需要恢复10000-13000的数据。

再来想想,redo log为什么可以实现事务的原子性和持久性。

  • 原子性,是redo log记录了事务期间操作的物理日志,事务提交之前,并没有写入磁盘,保存在内存里,如果事务失败,数据库磁盘不会有影响,回滚掉事务内存部分即可。
  • 持久性,redo log 会在事务提交时将日志存储到磁盘redo log file,保证日志的持久性。

2.2 undo log

redo log一旦提交意味着持久化了,但是有时候需要对其进行rollback操作,那就需要undo log。

undo log是逻辑日志,只是将数据库逻辑的恢复到原来的样子。并不能将数据库物理地恢复到执行语句或者事务之前的样子。虽然所有的逻辑修改均被取消了,但是数据结构和页本身在回滚前后可能不一样了。

既然是逻辑日志,可以理解为它存储的是SQL, 在事务中使用的每一条 INSERT 都对应了一条 DELETE,每一条 UPDATE 也都对应一条相反的 UPDATE 语句。

undo log 存放在数据库内部的一个特殊段(segment)中,也叫undo段,存在于共享表空间中。

undo log实现了事务的一致性,可以通过undo log恢复到事务之前的逻辑状态,保证一致性。

undo log 还可以实现MVCC(Multi-Version Concurrency Control ,多版本并发控制),多版本并发控制其实可以通过 undo log 形成一个事务执行过程中的版本链,每一个写操作会产生一个版本,数据库发生读的并发访问时,读操作访问版本链,返回最合适的结果直接返回。从而读写操作之间没有冲突,提高了性能。

3. 事务控制语句

上图列出了事务的一些控制语句,start transaction/begin、commit、rollback相信大家都有用过。

savepoint identifier 可以创建事务的一个保存点,执行回滚操作时可以回滚到指定保存点,不需要回滚整个事务。

打个比例,假设你去旅游到目标地需要三个行程,第一程 深圳到广州高铁,第二程 从广州飞到雅加达,第三程 雅加达飞到某岛。 如果再第三程 飞机取消行程,事务要回滚,如果要你再会深圳,你肯定会心理一万个草泥马。因为再进入事务,第一步和第二步是不变的,所以不需要回滚,直接回滚第三步即可。

set transaction 修改事务隔离级别,比如修改会话级别的事务:

set session transaction isolation level read committed;

4. 事务隔离级别

事务的隔离性是通过锁来实现,上一篇也提到事务的隔离级别,这篇简单回顾一下。

四种隔离级别,按READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE顺序,隔离级别是从低到高,InnoDB默认是REPEATABLE-READ级别,此级别在其余数据库中是会引起幻读问题,InnoDB采用Next-Key Lock锁算法避免了此问题,什么是幻读问题,请参考上一篇文章。

隔离级别越低,则事务请求的锁和保持锁的时间就越短。

4.1 READ-UNCOMMITTED

READ-UNCOMMITTED 中文叫未提交读,即一个事务读到了另一个未提交事务修改过的数据,整个过程如下图:

如上图,SessionA和SessionB分别开启一个事务,SessionB中的事务先将id为1的记录的name列更新为'lisi',然后Session 中的事务再去查询这条id为1的记录,那么在未提交读的隔离级别下,查询结果由'zhangsan'变成了'lisi',也就是说某个事务读到了另一个未提交事务修改过的记录。但是如果SessionB中的事务稍后进行了回滚,那么SessionA中的事务相当于读到了一个不存在的数据,这种现象也称为脏读。

可见READ-UNCOMMITTED是非常不安全。

4.2 READ COMMITTED

READ COMMITTED 中文叫已提交读,或者叫不可重复读。即一个事务能读到另一个已经提交事务修改后的数据,如果其他事务均对该数据进行修改并提交,该事务也能查询到最新值。如下图:

在第4步 SessionB 修改后,如果未提交,SessionA是读不到,但SessionB一旦提交后,SessionA即可读到SessionB修改的内容。

从某种程度上已提交读是违反事务的隔离性的。

4.3 REPEATABLE READ

REPEATABLE READ 中文叫可重复读,即事务能读到另一个已经提交的事务修改过的数据,但是第一次读过某条记录后,即使后面其他事务修改了该记录的值并且提交,该事务之后再读该条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据。如下图:

InnoDB默认是这种隔离级别,SessionB无论怎么修改id=1的值,SessionA读到依然是自己开启事务第一次读到的内容。

4.4 SERIALIZABLE

SERIALIZABLE 叫串行化, 上面三种隔离级别可以进行 读-读 或者 读-写、写-读三种并发操作,而SERIALIZABLE不允许读-写,写-读的并发操作。 如下图:

SessionB 对 id=1 进行修改的时候,SessionA 读取id=1则需要等待 SessionB 提交事务。可以理解SessionB在更新的时候加了X锁。

5. 分布式事务

分布式事务指允许多个独立的事务资源参与到一个全局的事务中。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚。

5.1 InnoDB 分布式事务

InnoDB 是支持分布式事务,由一个或多个资源管理器(Resource Managers),一个事务管理器(Transaction Manager),以及一个应用程序(Application Program)组成。

  • 资源管理器(Resource Managers),提供访问事务资源的方法,一般一个数据库就是一个资源管理器。
  • 事务管理器(Transaction Manager),协调参与全局事务中的各个事务,需要和参与全局事务的所有资源管理器进行通信。
  • 应用程序(Application Program) 定义事务的边界,指定全局事务中的操作。

如下图:

应用程序向一个或多个数据库执行事务操作,事务管理器进行管理事务,通过二段式提交,第一阶段所有参与的全局事务的节点都开始准备,告诉事务管理器都准备好了,可以提交了。第二阶段,事务管理器告诉每一个资源管理器是执行Commit 还是 Rollback。如果任何一个节点显示不能提交,则所有的节点被告知需要回滚。

5.2 TCC分布式事务

InnoDB的分布式是数据库实现的,看看数据库外如何分布式事务,比较常见的是TCC分布式事务。

上图描述了TCC分布式事务的流程,假设电商业务中,支付后需要修改库存,积分,物流仓储的数据,如果一个失败则全部回滚。

TCC分布式事务,有三个阶段,Try,Confirm, Cancel。也就是说每个参与事务的服务都需要实现这三个接口,库存、积分、仓储都需要实现这三个接口。

第一阶段,Try,业务应用调取各个服务的Try接口,告诉他们给我预留一个商品,有人要购买,可以理解为冻结,每一步都不执行成功,只是标记更新状态。

第二阶段,Confirm,确认阶段,即事务协调器调取每个服务Confirm执行事务操作,如果某一个服务的Confirm失败,则有第三个阶段。如果成功则结束事务。

第三个阶段,Cancel,如果在第二个阶段有一个事务提交失败,则事务协调器调取所有业务的Cancel接口,回滚事务,将第一阶段冻结的商品恢复。

思考题:

在MQ中间件连接的上游服务和下游服务中如何实现分布式事务了?

欢迎大家留言讨论。

更多MySQL相关文章和讨论,请关注公众号:『 天澄技术杂谈 』