数据库事务的概念及其实现原理
转载自:
目录- 1. 认识事务
- 1.1 为什么需要数据库事务
- 1.2 什么是数据库事务
- 1.3 事务如何解决问题
- 1.4 事务的ACID特性以及实现原理概述
- 2.并发异常与并发控制技术
- 2.1 常见的并发异常
- 2.2 事务的隔离级别
- 2.3 事务隔离性的实现——常见的并发控制技术
- 2.3.1 基于封锁的并发控制
- 2.3.2 基于时间戳的并发控制
- 2.3.3 基于有效性检查的并发控制
- 2.3.4 基于快照隔离的并发控制
- 2.3.5 关于并发控制技术的总结
- 3. 故障与故障恢复技术
- 3.1 为什么需要故障恢复技术
- 3.2 事务的执行过程以及可能产生的问题
- 3.3 日志的种类和格式
- 3.4 日志恢复的核心思想
- 3.5 事务故障中止/正常回滚的恢复流程
- 3.6 系统崩溃时的恢复过程(带检查点)
- 3.6.1 一个系统崩溃恢复的例子
- 4. 总结
- 5. 参考资料
事务是数据库系统中的重要概念,了解这一概念是以正确的方式开发和数据库交互的应用程序的前提。但是很多开发者对于事务的认识相对片面和肤浅,仅仅把它等同于ACID,不了解数据库系统引入事务的真正动机,ACID对于事务意味着什么以及最重要的:数据库系统是如何保证事务的ACID特性的?
因为最近在导师手下做一个微服务和分布式事务相关的课题项目,作为必要的前置准备,需要加深下对单机事务的认识并理解其实现原理。这篇文章是在我给组内的小伙伴分享的PPT的基础上改动而成,旨在帮助大家建立关于事务的相对体系的认识。由于当时正好赶上考试周,所以写的比较仓促,里面肯定有理解不到位甚至出错的地方,希望看到的小伙伴能够我指出来。同时还需强调几点:
- 以下所有内容都是针对单机事务而言,不涉及分布式事务相关的东西!
- 关于事务原理的讲解不针对具体的某个数据库实现,所以某些地方可能和你的实践经验不符。
1. 认识事务
转账是生活中常见的操作,比如从A账户转账100元到B账号。站在用户角度而言,这是一个逻辑上的单一操作,然而在数据库系统中,至少会分成两个步骤来完成:
- 1.将A账户的金额减少100元
- 2.将B账户的金额增加100元。
在这个过程中可能会出现以下问题:
- 1.转账操作的第一步执行成功,A账户上的钱减少了100元,但是第二步执行失败或者未执行便发生系统崩溃,导致B账户并没有相应增加100元。
- 2.转账操作刚完成就发生系统崩溃,系统重启恢复时丢失了崩溃前的转账记录。
- 3.同时又另一个用户转账给B账户,由于同时对B账户进行操作,导致B账户金额出现异常。
为了便于解决这些问题,需要引入数据库事务的概念。
1.2 什么是数据库事务
对于上面的转账例子,可以将转账相关的所有操作包含在一个事务中
BEGIN TRANSACTION
A账户减少100元
B账户增加100元
COMMIT
- 1.当数据库操作失败或者系统出现崩溃,系统能够以事务为边界进行恢复,不会出现A账户金额减少而B账户未增加的情况。
- 2.当有多个用户同时操作数据库时,数据库能够以事务为单位进行并发控制,使多个用户对B账户的转账操作相互隔离。
事务使系统能够更方便的进行故障恢复以及并发控制,从而保证数据库状态的一致性。
1.4 事务的ACID特性以及实现原理概述
2.1 常见的并发异常
-
事务具有隔离性,理论上来说事务之间的执行不应该相互产生影响,其对数据库的影响应该和它们串行执行时一样。
-
然而完全的隔离性会导致系统并发性能很低,降低对资源的利用率,因而实际上对隔离性的要求会有所放宽,这也会一定程度造成对数据库一致性要求降低
-
SQL标准为事务定义了不同的隔离级别,从低到高依次是
- 读未提交(READ UNCOMMITTED)
- 读已提交(READ COMMITTED)
- 可重复读(REPEATABLE READ)
- 串行化(SERIALIZABLE)
事务具有隔离性,理论上来说事务之间的执行不应该相互产生影响,其对数据库的影响应该和它们串行执行时一样。
然而完全的隔离性会导致系统并发性能很低,降低对资源的利用率,因而实际上对隔离性的要求会有所放宽,这也会一定程度造成对数据库一致性要求降低
SQL标准为事务定义了不同的隔离级别,从低到高依次是
事务的隔离级别越低,可能出现的并发异常越多,但是通常而言系统能提供的并发能力越强。
不同的隔离级别与可能的并发异常的对应情况如下表所示,有一点需要强调,这种对应关系只是理论上的,对于特定的数据库实现不一定准确,比如mysql
的Innodb存储引擎通过Next-Key Locking技术在可重复读级别就消除了幻读的可能。
所有事务隔离级别都不允许出现脏写,而串行化可以避免所有可能出现的并发异常,但是会极大的降低系统的并发处理能力。
2.3 事务隔离性的实现——常见的并发控制技术
核心思想:对于并发可能冲突的操作,比如读-写,写-读,写-写,通过锁使它们互斥执行。
锁通常分为共享锁和排他锁两种类型
- 1.共享锁(S):事务T对数据A加共享锁,其他事务只能对A加共享锁但不能加排他锁。
- 2.排他锁(X):事务T对数据A加排他锁,其他事务对A既不能加共享锁也不能加排他锁
基于锁的并发控制流程:
-
事务根据自己对数据项进行的操作类型申请相应的锁(读申请共享锁,写申请排他锁)
-
申请锁的请求被发送给锁管理器。锁管理器根据当前数据项是否已经有锁以及申请的和持有的锁是否冲突决定是否为该请求授予锁。
-
若锁被授予,则申请锁的事务可以继续执行;若被拒绝,则申请锁的事务将进行等待,直到锁被其他事务释放。
可能出现的问题:
-
死锁:多个事务持有锁并互相循环等待其他事务的锁导致所有事务都无法继续执行。
-
饥饿:数据项A一直被加共享锁,导致事务一直无法获取A的排他锁。
对于可能发生冲突的并发操作,锁使它们由并行变为串行执行,是一种悲观的并发控制。
2.3.2 基于时间戳的并发控制
核心思想:事务对数据的更新首先在自己的工作空间进行,等到要写回数据库时才进行有效性检查,对不符合要求的事务进行回滚。
基于有效性检查的事务执行过程会被分为三个阶段:
-
读阶段:数据项被读入并保存在事务的局部变量中。所有write操作都是对局部变量进行,并不对数据库进行真正的更新。
-
有效性检查阶段:对事务进行有效性检查,判断是否可以执行write操作而不违反可串行性。如果失败,则回滚该事务。
-
写阶段:事务已通过有效性检查,则将临时变量中的结果更新到数据库中。
有效性检查通常也是通过对事务的时间戳进行比较完成的,不过和基于时间戳排序的规则不一样。
该方法允许可能冲突的操作并发执行,因为每个事务操作的都是自己工作空间的局部变量,直到有效性检查阶段发现了冲突才回滚。因而这是一种乐观的并发策略。
2.3.4 基于快照隔离的并发控制
以上只是对常见的几种并发控制技术进行了介绍,不涉及特别复杂的原理的讲解。之所以这么做一是要真的把原理和实现细节讲清楚需要涉及的东西太多,篇幅太长,从作者和读者角度而言都不是一件轻松的事,所以只对其实现的核心思想和实现要点进行了简单的介绍,其他部分就一笔带过了。二是并发控制的实现的方式太过多样,基于封锁的实现就有很多变体,mvcc多版本并发控制的实现方式就更是多样,而且很多时候会和其他并发控制方式比如封锁的方式结合起来使用。
3. 故障与故障恢复技术
数据库运行过程中可能会出现故障,这些故障包括事务故障和系统故障两大类
- 事务故障:比如非法输入,系统出现死锁,导致事务无法继续执行。
- 系统故障:比如由于软件漏洞或硬件错误导致系统崩溃或中止。
这些故障可能会对事务和数据库状态造成破坏,因而必须提供一种技术来对各种故障进行恢复,保证数据库一致性,事务的原子性以及持久性。数据库通常以日志的方式记录数据库的操作从而在故障时进行恢复,因而可以称之为日志恢复技术。
3.2 事务的执行过程以及可能产生的问题
-
:描述一次数据库写操作,T是执行写操作的事务的唯一标识,X是要写的数据项,V1是数据项的旧值,V2是数据项的新值。 -
:对数据库写操作的撤销操作,将事务T的X数据项恢复为旧值V1。在事务恢复阶段插入。 -
: 事务T开始 -
: 事务T提交 -
: 事务T中止
关于日志,有以下两条规则
- 1.系统在对数据库进行修改前会在日志文件末尾追加相应的日志记录。
- 2.当一个事务的commit日志记录写入到磁盘成功后,称这个事务已提交,但事务所做的修改可能并未写入磁盘
3.4 日志恢复的核心思想
-
从后往前扫描日志,对于事务T的每个形如
的记录,将旧值V1写入数据项X中。 -
往日志中写一个特殊的只读记录
,表示将数据项恢复成旧值V1,
这是一个只读的补偿记录,不需要根据它进行undo。 -
一旦发现了
日志记录,就停止继续扫描,并往日志中写一个
日志记录。
3.6 系统崩溃时的恢复过程(带检查点)
恢复前的日志如下,写入最后一条日志记录后系统崩溃
<T0 start>
<T2 commit>
<T1 start>
<checkpoint {T0,T1}> //之前T2已经commit,故不用重做
<T1 commit>
<T2 start>
<T0 abort> //T0回滚完成,插入该记录后系统崩溃
4. 总结
《数据库系统概论》
《数据库系统概念》