15-并发控制理论
并发控制横跨了多个层级:
- operator Execution 操作执行
- Access Methods 读表
- buffer Pool Manager 缓存池
日志恢复
- buffer Pool Manager 缓存池
- Disk 磁盘管理
Motivation:
- 当多人修改数据库同一条数据,就会出现竞争问题
- 把100块钱从A账户转移到B账户,如果A账户扣100之后,机房断电了。当断电恢复之后,当前的数据库状态应该是什么?(如果没有恢复,状态肯定是错误)
上面一个涉及丢失更新的问题 Lost Updates ,用并发控来解决。下面一个问题,用持久性来解决,用恢复来解决
并发控制和恢复是数据库非常重要的功能。他们是实现事务ACID特性的基础。
事务(Transcation 简称:txn) 是多个数据库操作组合成的一个不可分割的、同时成功或失败的工作单元。txn是数据库操作的基本单元,一个txn包含一个或者多个动作,
在操作数据的时候,如果没有显示开txn,数据库会隐式开txn。在数据库中一条sql也是一个txn。
批: 关于存储过程的理解:
- 存储过程,从表现上来看,也可以看成是一个大的事务。要么执行成功,要么执行失败。存储过程可以看成一个受到限制的函数。这也是其和其他图灵完备的语言,如C++,python函数的区别。
转账包含3个动作: - 检查用户的账户是否有100美元
- 用户的账户减去100美元
- 目标账户增加100美元
这三条语句要么都成功,要么都失败。关于转账或者其他txn要实现的功能,其实用户完全可以自己编程实现。但是数据库提供了事务,保证了正确性。简化了这个操作,使得用户只关心目标。不用关心目标之外的其他影响。 产品和用户的界限越来越模糊,并没有明确的界限。
最简单的实现txn思路:
- 一次只执行一条(缺点:不能并发跑,影响数据库的性能)
- 将整个数据复制一份,备份一份。在复制一份上进行修改,如果执行成功,就持久化,否则不操作(数据量太大)
ps:上面最简单的思路其实提供了两个并发拓展的方向,一个是从时间上角度,去实现并发控制,比如2阶段;一种是空间角度,去实现并发,比如多版本控制。
并发:在同一时间间隔内有多个事件或活动发生,即在同一个时间段内,多个事务交替执行。 并发的好处:
- 单条请求, 响应时间更短
- 对系统更高的利用效率
并发的要求
- 正确性
- 公平
并发是为了提高txn的并行度,它的正确性有一个基准:即串行执行txn的结果。和串行执行txn结果一致即是正确。
并发必须进行控制,如果不进行控制,可能会导致数据永久性损坏(比如写入脏数据)。
多个txn能否并发,如何并发?如何交叉执行。这就是接下来要解决的问题,并发需要寻找一些标准。
数据库不知道sql语句本身代表的业务的含义,所以数据库要判断txn之间能否交叉运行,如何交叉运行,要根据sql本身去分析。接下来,需要对txn进行简化,抽取txn最本质的特性,继而对其进行分析。
- 用 data object 代表操作的目标
- 将事务的行为 简化成 读写操作
- 事务以BEGIN命令标示开始
- 事务的结束或者结果只有两种形态:COMMIT 或者 ABORT(ROLLBACK)。其中commit表示txn执行成功,提交;rollback表示txn回滚(部分系统用abort表示回村)。
- 回滚指令可是程序本身发出的,也可以是数据库发出的
回滚有可能是程序本身发出的,也有可能是数据库本身发出的。
比如当前的txn和其他的txn产生了dead clock,那么数据库会主动打断当前txn的进程,比如当前的txn和其他的txn产生了dead clock,那么数据库会主动打断当前txn的进程
并发要保证正确性,接下来介绍正确性的四个标准:ACID,即 数据库执行txn怎么才算正确执行了
这四个标准也是txn的四个性质,也可以看做并发的四个限制。
原子性:要么执行成功,要么失败。
一致性:数据在执行txn之前和之后,状态要保持一致。数据来自于真实世界,反应真实世界,所以执行前后也要满足真实世界的逻辑。比如 A向B账户赚钱,A账户和B账户的钱 转账前后要保持一致。
隔离性:互不干扰。从结果上看,多个事务在并发执行的过程中所得到的结果,和串行执行得到的结果是一致;从用户角度来看,用户执行的操作和其他事务隔离。
持久性:一个txn一旦执行成功,其对数据的影响是持久的,不会丢失。 无论是否停电或者是否发生其他非可抗因素,一旦txn提交,那么其对数据的改变就是永久的。
通俗的版本:
今天要研究四个方面如何实现?
首先是原子性。
一个txn只有两种结局:成功,终止(被回滚)。
原子性由数据库保证,事务是数据库提供的服务。
- 在用户看来, 你的指令要么全部执行,要么一个也不执行。
两个场景发生之后,数据库当前这批数据的状态应该是什么? 事务要回滚到最原始的状态
- 案例1:从账户A中取了100还没有转移到账户B中,数据库把事务终止了
- 案例2:从账户A中取了100还没有转移到账户B中,停电了
实现原子性的手段:
-
日志
- 执行一步,同时在日志中,记录如果回滚,该如何操作,即回滚日志,Undo log
- 一般是在内存和磁盘中都会存日志
- 日志好像是飞机的黑匣子一样
- 日志不光有回滚txn的作用,还有审计和提高执行效率的作用。
- 审计:如果数据库出问题,查看是哪些操作触发了审计。
- 提高效率:即先记录,在当时并不是真的执行,而后再在合适的时候,执行。 lazy evaluation
-
增量备份:只备份你修改的page。今天这个技术已经被淘汰了。
一致性:
txn执行前后,要满足基本的真实世界的逻辑。事务的执行会改变系统状态,一致性是指事务的执行会保证数据库从一个正确(一致)的状态转移到另一个正确的状态。
- 比如数据库可以存储负数,但银行业务中用户的存款不能出现负数,存在负存款账户的数据库状态是不正确的状态
一致性分为: - 数据库一致性
- 事务一致性
数据库一致性
- 数据库是对真实世界的建模,其必须满足完整性约束。(什么是完整性约束?)
- 未来的事务操作 要能看到 历史的事务的影响(?)
- 如果在txn执行之前,数据库满足一致性,那么在txn执行之后,数据库也应该满足一致性(数据库满足一致性是什么意思?)
事务的一致性:要靠业务来保证,数据库无法保证(一致性其实完全可以由用户编程实现保证)。
PS:关于事务的并发,可以建模成一个带约束的优化问题:
- 初版: 最大化事务的并发量 在 公平性和正确性的限制下
- 二版: 最大化事务的并发两 在 ACID的限制下
隔离性
事务的隔离性是指多个事务在并发执行的过程中所得到的结果,和串行执行得到的结果是一致的,保证了事务并发处理中事务的正确
理想的隔离:在一个时间只运行一个txn。在后面我们会看到:并发控制是实现隔离性的主要手段。
为什么要实现隔离性?
- 为了提高并发度,同时也方便了使用者,写sql的人。串行处理不存在事务之间的相互干扰,但是不能充分利用计算资源,效率低下。
- 用户在进行转账操作的时候,直接写转账的sql,能不能操作成功,数据库来做检查。(数据库做隔离性,就是为了用户能更好的实现业务)
并发控制可以提升事务并发性,但是要解决冲突问题
所以,我们需要一种方法来交替穿插的跑txn。这里面有一个基准:数据库角度肯定是多个txn一起执行的,但是对用户来说要像串行跑一样。
接着从实际的执行效果,对txn进行分类,看看那些类可以进行直接跑,哪些类可以转化为串行跑。(比如如果多个事务都只有读操作,这个完全可以并发)
并发控制协议是决定多个txn如何交替,按照什么顺序执行?什么时候让txn失败
有两大流派:
- 悲观派:事情还未发生之前做控制,让坏的事情不要发生,即事前控制,比如 后续介绍的两阶段锁
- 乐观派:事情已经发生了,再去消除影响,即事后控制。先发展,后治理。矫枉必过正。出问题了再去让出问题的txn回滚掉,比如 基于时间戳并发控制
乐观比悲观好。实际中,应该是乐观悲观并用。
如何实现并发?
从结果分析:如何才算正确的输出。因为无论如何执行,要保证结果都是正确的。正确的基准是什么?是串行执行的结果。
只要是和串行执行的结果相符合,那么就是正确的执行。并发的目标就是同时运行多个事务但是和串行的效果是一样的。
并没有说,T1一定要比T2先执行,或者 T2一定要比T1先执。但是运行的结果一定要和串行运行的结果相同。
从结果来看,上述两个事务顺序无论谁先执行结果都是正确的。上述叫真正顺序的执行。
如果是真的并发过来的,那么怎么让它交叠的去跑?interleave(交插)
交替跑是一个优化问题,优化目标是最大并发量,约束条件是什么?是公平,正确。
交叠跑:
- 最大化系统并发量
- 更好的利用多核cpu(系统可用性更高,能更加充分的利用系统)
如果一个txn出错了,那么它不会影响别的txn
如果对并发不进行控制,如下就是随机跑的案例。(这页PPT有问题)
如下这种执行顺序就是错误的,存在一致性问题。
转换成数据库的操作,数据库在干什么?
对数据库而言,所有的操作落实下去无非是读写两个操作。所以后续用读写操作为工具,来分析并发中可能遇到的种种问题,而后对种种问题进行解决。
使用读操作和写操作来描述事务的执行过程。后面分析问题的形式就要发生变化了。变成读写角度了。
我们可以从人为的角度,判定txn的执行是否存在问题,那么我们如何编写程序,让数据库来识别出txn的执行是否存在问题?
以真正串行的调度作为基准,如果实际txn的执行可以等价成串行,那么没有问题。
那么如何等效?
ps:这里面几个改变,serial schedule(串行化调度),Equivalent Schedules(等价调度),Serializable Schedule(可串行化调度)的概念
-
串行调度(Serial Schedule): 事务串行执行。所谓 Serial Schedule,就是 针对不同的事务,完全串行执行,没有交叉执行。
-
等价调度(Equivalent Schedules):
- 针对同一个事务的两个调度,如果调度1和调度2对数据库的影响是一致的,那么这两个调度是等价的
-
可串行化调度(Serializable Schedule)
- 给定一个并发调度S ,存在一个串行调度S’, 在任何数据库状态下,按照调度S 和调度S’执行后所产生的结果都是相同的 。
- 此时调度S 被称之为可串行化调度(serializable schedule)。
4.可串行化调度的数量十分巨大,且难以校验,数据库中一般通过找到可串行化调度的子集(充分条件),即找到能够提前确认是可串行调度的并发调度,进而提升调度效率。
可串行化调度 。如果每一个txn都保证一致性,那么可串行化调度也保证了一致性。
可串行化是一个不太好理解的概念。
更大的灵活性意味着更大的并行度。
- 如何判断一个调度是否可串行化?如何实现可串行化调度?
- 验证代价较大,一般找一个容易验证的可串行化调度方案,一般有如下几种方案:
- 终态可串行化:判读最终执行结果的状态
- 冲突可串行化:判断操作之间是否冲突
- 视图可串行化
3.下面重点介绍的是冲突串行化
冲突操作的定义:两个操作是冲突的,其需要满足如下条件:
- 两个操作来自不同的事务
- 针对在相同的object上,至少有一个是写
根据上面的定义,冲突有如下三种:(如下三种都是至少有一个写)
- 读写冲突
- 写读冲突
- 写写冲突
接下来,逐一的看:
读写冲突:一个事务读了两次,另外一个事务T2 在读的中间写了一次,此时出现的前后不一致现象称之为 读写冲突,又叫不可重复读。
读写冲突 明显破坏了隔离性,第一次读和第二次读不一样。
写读冲突:(先写后读,而后撤销写操作)一个事务读取了未提交的版本修改叫脏读
写写冲突:结果交叉。
交叉之后,串行的执行,会出问题。
串行的结果,要么最后A和B是T1写的结果;要么是T2写的结果;这两者任意一个结果都是正确的。不能是两者混着
研究冲突是为了更好的串行化。
可串行化有两种方式:
- Conflict Serializability(冲突可串行化)
- view Serializability
冲突可串行化调度,是从冲突的角度出发,针对一个调度S 去发现其等价的串行调度S’来确定S 是一个可串行化调度
- 一次操作交换定义为交换事务调度序列中相邻的两个操作,一个交换操作可以将一个调度A变成另外一个调度B。
当交换不会影响两个调度的一致性时,定义该交换得到的两个调度是等价的,该交换为等价交换
- 等价操作
- 交换连续两个相同数据读取操作的顺序:RT1(A) RT2(A)
- 交换连续两个不同数据读写操作的顺序:RT1(A) WT2(B);WT1(A) RT2(B);WT1(A) WT2(B)
- 非等价操作
- 交换连续两个相同数据的读写、写写操作: RT1(A) WT2(A);WT1(A) RT2(A);WT1(A) WT2(A)
两个调度是冲突等价的,当且仅当:(无法翻译)
- 如果
一些冲突操作可以转换为 串行化操作
ex:不懂这里
S是冲突等效的,当你可以将s变换成串行调度通过交换不同txn的连续非冲突操作。
判断如下是否能可以转化成可串行化序列
为什么可以滑动?因为不同的action是不存在。
在时间上不同的操作可以滑动位置。(ps:滑动位置只是说明可以串行化,那么实际执行也是串行化吗?)
一个反例:
一旦有重复,写些冲突,不能等效交换。没办法转化成真正串行的。
如果有冲突,不能等效交换位置。
如下也不能交换位置:
除了如上的三种冲突,都是可以交换位置的。
当存在多个txn的时候,交换操作就会变得很难,所以需要新的算法。
引出依赖图这个工具:
构建依赖图。(感觉拓扑排序可能在这里有用)
如果依赖图有环,永远无法等效成串行化。构建依赖图,判断是否有环。
- 对于有向无环图的判定,可以使用经典的拓扑排序算法。若图G 无环,使用拓扑排序可以得到图G 的一个拓扑序列C。
- 拓扑序列C 为该调度S’等价的串行调度S
如果两个调换位置,那么就和串行化之后结果不同的。
因为图中不存在环,所以可以先执行T2,再执行T2,再执行T3。
如上三个可串行化。不是真串行化,是执行效果相当于串行化。
如下是一个反例:永远没有通过滑动的办法,将如下操作等效为串行化。
好像给了一个依赖图无法包含的问题。(我不懂这个案例放在这里的含义)
前面的是基于冲突的,下面是基于观察的。
依赖图显示不可以,但是根据实际的业务发现是可串行的。依赖图是有局限性的,依赖图会错杀一些真正可串行化的情况
基于人类 观察的可串行化 比 基于冲突的 局限性更少。但是无法实现。
两种都没办法做到100%的不误杀,因为算法没办法理解业务。多少都会存在错杀的情况。
实际中,基于冲突的用的更多,因为更好实现。错杀的特殊情况有些可以形成案例,摘出来,单独判断。(大模型套小模型,大模型+规则)
大部分数据库对于只读txn,那么就不需要判断读写依赖,写读依赖。直接并发就可以了。
多种调度并发执行分类:
- 真正串行的是一小部分,稍微多点的是 冲突可串行化。再多的是观察可串行化。
持久性:
持久性:一旦某个事务提交以后,即使数据库发生故障,该事务的执行结果也不会丢失,可以被正确地恢复,仍然对后续事务可见。
实现方法:
- Redo log 重做日志
- shadow page 技术
** 回顾 & 总结**
-
并发控制和恢复是数据库最重要的功能之一。mysql是免费开源的数据库支持事务。
-
事务可以极大的减轻业务的实现复杂度。
-
并发控制是自动实现的。并发控制要保证:并发执行的效果和单独的串行执行效果相同
-
google专家建议:大部分情况下,能用事务尽量用事务,但是某些地方可能直接coding性能更好,因为数据库不理解业务。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?