在上一讲的内容中,我们详细介绍了微服务架构带来的分布式事务问题,并对解决该问题的三种方案进行了简单总结。这三种解决方案分别对应两种解决思路:
尽量避免分布式事务问题的产生;
采用分布式事务中间件和分布式数据库来解决。
这两种技术的实现都借鉴了传统的事务理论和底层实现,所以第一个模块我们将围绕事务的原理以及它的具体实现技术详细展开。
这一讲我们先从事务的概念和本质入手,重点介绍 ACID 特性,然后介绍数据库的各种常见故障类型,最后举例说明事务的具体实现技术。
什么是事务?
事务(Transaction)是指由数据库的使用者定义的一组数据库操作序列,由于业务规则限制,要求这些操作序列必须作为一个整体不可分割,要么全部执行成功,要么全部执行失败。
事务可以由用户显示定义,也可以由数据库系统隐式开启并自动提交。在 MySQL 中默认是自动提交事务,不需要显示定义,也就是每条 SQL 语句在执行时自动开启事务,执行完毕后会自动提交,并永久保存。
当用户需要自己控制多条语句作为一个事务时,可以通过控制语句修改 MySQL 的默认值,变更为显示定义,这样就可以自己控制事务的范围了。控制语句语法如下:
SET autocommit = 0|OFF;复制
显示定义后,语句必须以 BEGIN TRANSACTION 开始,以 COMMIT 或 ROLLBACK 结束,中间可以是多条 SQL 语句,也可以是一条 SQL 语句。格式如下所示:
BEGIN TRANSACTION
SQL 语句1;
SQL 语句2;
……
COMMIT或ROLLBACK复制
COMMIT 执行完之后,该事务内所有 SQL 语句对数据的修改都会变更到物理磁盘文件中。
如果执行 ROLLBACK,则表示 SQL 语句执行的过程中发生异常,即使 SQL 语句 1 对数据的修改成功,也将会被撤销,所有的数据恢复到开始时的状态。
数据库的常见故障 & 事务可以解决的故障
数据库发生故障是不可避免的,按照故障的严重程度从大到小可以分为 4 种。
(1)灾难性故障
这类问题需要利用跨地域的机房冗余灾难备份,事务无法解决。比如 2008 年汶川大地震这种灾难发生,当地银行的服务器肯定会瘫痪,这就需要利用异地机房的数据在事后进行数据的灾难恢复。大的银行的核心数据库系统,都是采用两地三中心或三地五中心。
(2)介质故障
比如硬盘故障,这不属于事务保护的范围。
(3)系统故障
典型的断电引发的操作系统崩溃或者数据库系统崩溃。在没有事务出现之前,当出现系统故障的时候,容易出现如下问题:
数据无法恢复、出现数据丢失等现象;
数据没有丢失但是会出现数据错乱、脏数据。
利用事务的恢复机制和恢复策略,可以有效解决上述两种问题。
(4)数据输入错误
这类问题数据库无法保证,需要用户程序来保证。一般情况下,应用程序已经进行了校验和逻辑判断,使得数据库的数据输入错误已经很少。
事务的本质和 ACID 特性是什么?
事务的本质到底是什么?我个人觉得应该从两方面来看:宏观整体视角和内部微观视角。
从宏观整体视角来看,事务实际上是数据库系统为应用程序开发者提供和使用的一种能力。
从内部微观的视角来看,事务封装了一系列对数据库的读写操作,这些基本操作可以组合成读读、读写、写读、写写四种类型,封装完之后不论内部如何,都将作为一个整体来执行。
事务的本质最重要的体现就是事务的 ACID 特性。
在前文中,我们已经简单地讲了 ACID 特性,是指原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。下面再详细讲讲这几个特性。
1.原子性(all-or-nothing)
我们应对未来不确定的情况一般会有两手准备,对应到事务中就是:当多条 SQL 语句执行,如果部分执行成功,其他也一定需要成功;如果部分执行失败,其他成功部分也必须要撤销。也就是不论中间发生什么异常故障,最终就是两个极端结果中的一个,整体成功提交(Commit)或者整体失败回退(Rollback)。
2.持久性,又称永久性(Permanence)
持久性是作为事务必须有的核心特性,原因就在于当突然发生断电的场景,数据库系统可能会崩溃,这个时候数据虽然已经在内存中更新成功,但是并没有及时地回写到稳定的磁盘存储中(也称为刷脏页),此时很容易发生更新成功而数据丢失的现象。这是一个很严重的问题,假设突然断电,导致用户充值一个亿突然间凭空消失,会是怎样的局面。
所以,从用户的角度来看,用户发起一条数据更新操作,返回修改成功之后,就表示数据库已经为用户提供承诺,后续不论发生任何故障情况,该数据一定会永久保持不变。显然,没有持久性保证的语句执行,是不满足这个承诺的。
3.隔离性
事务的隔离性就是多个事务之间是隔离的,至少从用户的视角来看,一个事务的执行过程不会受到另外一个事务的执行的影响。
如果实际执行中事务 A 和事务 B 完全是串行执行那么肯定不会相互影响,但是这样的串行执行会降低并发度和执行效率。而实际情况是事务 A 和事务 B 的各项操作之间都是交叉执行的,而且有可能某个事务在执行了一半之后数据库发生系统故障,强行停止了其后续的操作,所以也就要求必须撤销其执行成功的部分操作。所以多个事务的多个语句并行交叉执行,加上各种故障的影响,为了避免事务间的影响,就需要隔离性的保障。
4.一致性
如果数据库系统没有事务的保证,一定会出现数据丢失或者不一致等问题,导致数据的安全和正确性无法保障。
事务的 ACID 四个特性之间的关系是相辅相成,缺一不可的,如下图所示:
一致性是事务要达成的总目标,要求保证用户视角下数据的正确性和安全性,具体如何保证,就需要其他的三个特性提供内部支持;
原子性是针对单个事务来讲,要么全部成功提交要么全部回滚,它需要持久性提供保证;
隔离性是针对并发执行的多个事务的交叉数据执行,需要保证互相不影响并且执行正确,它也需要持久性提供保证;
持久性是针对所有正常的或是故障的情况,只要事务提交,都要求满足数据被永久的存储在非易失存储介质上。
除 ACID 之外,事务还具有哪些其他的属性?
除了 ACID 之外,你是不是觉得事务已经没有其他的属性了?事务完全等同于 ACID?答案其实是否定的。事务除了 ACID 这四个最重要的特性之外,还有几个其他的属性:可恢复性、严格性、可串行化。
这几个属性都是在涉及多个事务的并发操作中产生的,由于针对多个事务的并发操作比较复杂,会涉及事务的隔离级别和锁机制,有时候会出现各种数据异常情况,因此我们在后面的几讲中专门去讲解。
汇总一下事务的所有属性,总结如下:
事务的特性,是采用什么技术实现的?
事务的实现技术主要包括两方面:
数据库恢复技术;
数据库并发控制技术。
就像关于事务的这句话,总结得很好:
事务是并发控制的基本单位,也是故障恢复的基本单位。
这里我们重点讲解一下数据库恢复技术。
1.数据库恢复技术
数据库恢复技术是为了保证数据的原子性和持久性,在数据库发生故障的时候能够恢复到正确的状态,避免出现各种数据异常或者数据丢失的现象。常用的数据恢复技术就是 Redo 日志和 Undo 日志。
以 MySQL 的 InnoDB 存储引擎为例,如上图所示,我们讲解一下 MySQL 服务器处理一个 SQL 更新语句的整个流程,这个流程可以帮助我们理解 Undo 日志和 Redo 日志的作用,从而就能深刻地理解为什么事务能够解决系统故障。
1.MySQL 客户端在数据库连接池中获取一个连接,将 SQL 更新语句发送到 MySQL 服务器端。
2.服务器端线程获取到该条 SQL 更新语句后,首先通过 SQL 解析器进行语法解析,生成解析树。
3.查询优化器负责寻找最佳的查询方案,选择合适的索引,生成执行计划。
4.然后执行器根据这个执行计划来调用存储引擎获取查询结果。
5.执行器去存储引擎中查询数据,如果 Buffer Pool 缓存中存在,则直接返回结果。
6.如果缓存中不存在,需要从数据库磁盘文件中获取数据,将磁盘中的数据加载到内存的 BufferPool 中,然后对该记录加独占锁。
7.在更新数据之前需要把更新前的数据写入 Undo 日志文件中,这个步骤很关键,这样做的目的也是为了在后面出现故障的时候,事务可以回滚,恢复之前的数据。
8.更新内存 BufferPool 中的数据,注意此时修改的数据还没有刷新到磁盘,后续有后台线程异步去刷脏页到磁盘。
9.为了避免数据库发生系统故障,比如突然断电等异常情况,需要在修改数据时将操作同步记录到 RedoLog Buffer 中。
注意:在未将 RedoLog Buffer 数据刷新到磁盘 Redo Log 文件时,如果系统发生故障,虽然此时内存 BufferPool 中数据已经修改了(脏页),但是磁盘文件中数据还是旧的值,此时故障只会导致事务提交失败,不影响数据的正确性。
10.当准备提交事务时,需要先将缓存区 RedoLog Buffer 中数据刷新到磁盘 Redo Log 上,此时 Redo Log 还不是提交状态。同时需要将操作记录到 Binlog 日志中,在 Binlog 日志记录成功后,将刚刚写入的 Redo Log 的状态修改成提交,到此整个更新就完成了。
注意:此时事务已经提交,Redo Log 和 Binlog 日志数据都存在于磁盘上,BufferPool 缓存中的数据因为是随机写,不如 Redo Log 的顺序写性能高,所以可能还没有及时刷新到磁盘上,一旦机器断电故障 BufferPool 中修改的数据就会丢失,而有了 Redo Log 的保障就可以利用其来重放日志恢复丢失的数据。
2.并发控制技术
我们知道了事务的原子性和持久性,是通过故障恢复技术来实现的,接着来介绍一下事务的并发控制技术,它主要解决的是事务的隔离性问题。
因为涉及了多个事务之间的协作问题,所以事务的并发控制技术比较复杂。
当大量用户同时访问数据、更新数据的时候,就涉及多个事务的并发执行,由于每个事务内部有多个语句和多个基本的读写操作,实际内部是多个读写基本操作之间并发交叉执行。
事务的并发控制管理器必须要保证,实际内部的基本读写操作以某种合适的顺序来执行,使得这种顺序执行的最终结果与按照事务的顺序来执行产生的结果相同。
既要保证并发事务执行的结果正确,又要保证执行的高效,就需要各种并发控制协议和算法来实现了。关于这部分详细内容我们将在本模块的第 6 ~ 8 讲中详细讲解。
总结
这讲的内容涉及大量的底层原理,虽然学起来可能有些烧脑,但是我相信一句话:坚持做难并且正确的事,你一定会获得比别人更大的收获。
今天给你留几个扩展本讲内容的思考题:
Binlog 日志和 Redolog 日志有什么区别?
如何保证 Binlog 日志和 Redolog 日志的一致性?
希望在评论区写出你思考的答案,如果对本讲内容有不清楚的,或不赞同的观点,也欢迎在评论区留言与我讨论。
下一讲我会讲解“汇总分析 5 种常见数据读写异常”,期待与你再次相见。
相关文章推荐:
01 | 如何实现自我基础设施新重建-从掌握一致性与分布式事务开始
03 | 分布式事务的产生原因及常见的解决方案(NewSQL、Distributed SQL等)