「MySQL」深入理解事务的来龙去脉
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 存储内容
执行一条插入语句,重做日志大致为:
可以看到重做日志存储的格式有点看不太懂,看不懂没有关系,主要是告诉大家,重做日志存储物理格式日志,也就是基于存储页的修改。
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 接口,回滚事务,将第一阶段冻结的商品恢复。
原文链接:https://juejin.cn/post/6844903827611582471
最后,小编还给大家整理了一份面试宝典,有需要的小伙伴添加小助理 vx:
mxzFAFAFA 即可!!
版权声明: 本文为 InfoQ 作者【学Java关注我】的原创文章。
原文链接:【http://xie.infoq.cn/article/fd19f2ba63605c5d3e21ec2a4】。文章转载请联系作者。
评论 (1 条评论)