持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第23天,点击查看活动详情
并发控制覆盖范围横跨执行-读表-缓存池多个层级,因此非常复杂,需要单开一个大块来讲
事务
一个事务是一段连续的执行操作,例如一段SQL,在我们所学的关系型数据库中,事务就是由SQL组成的,哪怕只有一条SQL语句也是一个事务。事务在DBMS中的执行必须是原子的,也就是说在DBMS眼里看来,要么事务执行完,要么事务没执行。
我们一定要把事务的概念假如DBMS的话,理所应当的我们能够想到一个最简单的设计:
- 事务之间是串行的,事务们一个个的被执行,彼此之间毫无关系
- 当事务开始时,DBMS复制整个数据库文件,并对这个新文件进行事务更改。如果事务成功,则新文件将成为当前数据库文件。如果事务失败,DBMS将丢弃新文件,并且未保存任何事务更改。
上面就是最初的DBMS事务设计,也叫strawman system。显而易见的,这种设计完全没有利用好cpu和内存,当前来说,更好的解决办法是允许并行的处理相互独立的事务,我们的要求是
- 并行处理的事务之间没有冲突
- 没有事务总是无法被处理,总体是公平的
为了达到这些要求,我们要精心设计事务的处理方法,首先我们要对事务的并行执行有一个大致概念,首先任意事务之间随意交叉执行可能导致永久的不一致性,这是很明显的,并且数据库是有局限的,数据库只知道执行事务,不知道事务所表达的含义,因此无法通过语义调度事务。在设计并发控制方法之前,我们要对事务有一个良好的抽象。
抽象事务
这样讲可能太抽象了,具体的说,因为数据库只关心对数据做哪些读和写的操作,因此对DBMS来说,事务就是一段连续的读和写操作,一个事务开始的标识是 BEGIN 指令。
利用 COMMIT 或者 ABORT 指令停止事务:
- 如果是
COMMIT,DBMS知道这一段操作执行完成了,如果没冲突,则将更改的数据写入,如果冲突,则放弃这些更改的数据。 - 如果是
ABORT,DBMS知道这表示自主引发的事务中断,所有的更改会被放弃,仿佛事务没发生过。
ABORT有三种引发方法:
-
显式写出
-
事务引发
-
DBMS主动触发
DBMS并发性设计的好不好,有一个ACID标准,具体内容是:
-
原子性:事务中的所有操作都会发生,或者不发生
-
一致性:如果每个事务一致,并且数据库的状态在开始时与最终时一致
-
隔离性:一个事务的执行与其他事务的执行向隔离
-
持久性:如果事务提交,其更改的内容能够被持久化
ACID也就是Atomic、Consistency、Isolation、Durablity的缩写
原子性
DBMS保证事务是原子的。事务要么执行其所有操作,要么不执行任何操作。有两种方法可以做到这一点:
日志
DBMS在内存和磁盘上维护当前执行的事务的所有操作,以便可以随时撤消或者中止事务。
出于便于审计和高效率的原因,几乎所有现代数据库系统都使用日志记录。
复制
上面也解释了复制整个数据库是完全不合理的,但是复制本身是一个可行的方法,我们可以只复制事务修改的page,只有当事务提交时,这些page才被写回,这种方法数据恢复的比日志方法更快,但是维护开销更大,所以几乎不用这种方法。
一致性
事务的一致性比较抽象,我们可以理解为在更高的层面上,数据库逻辑一致。事务执行前和执行后,整个数据库的逻辑不改变,比如A向B转账100元,那么事务的逻辑就是把A的总值减100,把B的总值加100。数据库在事务执行前的状态逻辑+事务逻辑=事务执行后的状态逻辑。此时我们说这个数据库和事务都是具有一致性的。
数据库一致
数据库准确地表示它正在建模的真实世界实体,并遵循完整性约束。(例如,一个人的年龄不能为负)。此外,将来的事务应该在数据库中看到过去提交的事务的影响。
事务一致
如果数据库在事务开始之前是一致的,那么在事务开始之后也是一致的。确保事务一致性是应用程序的责任。
隔离性
DBMS提供了事务在系统中单独运行的假象。他们看不到并发事务的影响。这相当于一个以串行顺序(即一次一个)执行事务的系统。但为了获得更好的性能,DBMS必须在保持隔离假象的同时交错并发事务的操作。
如何让一段并行执行的事务在逻辑上确保和一段串行执行的一致呢?我们提出一个概念:并发控制协议,它表示DBMS如何安排并发操作之间的交叉执行。有两种协议
- 悲观:问题很多,不要让并发冲突出现
- 乐观:冲突很少,等冲突发生了之后再处理
假设A和B各有1000块钱,事务T1是给A+100、给B-100,事务T2是把A乘1.06、把B乘1.06,那么对T1和T2来说,隔离性体现在T1执行时,即使T2在并行执行,T1也不会在处理数据的时候发生数据突然更改的情况,也就是看不到并发事务的影响的含义。
DBMS执行操作的顺序称为执行计划。并发控制协议的目标是生成与某些串行执行等效的执行计划,这里有一些概念:
- 串行计划:不同事务连续执行的计划
- 等效计划:对于任何数据库状态,如果执行第一个调度的效果与执行第二个调度的效果相同,则这两个调度是等效的
- 可序列化计划:可序列化计划是一个与串行计划等效,且通过挪动读写顺序,可以变成形式上与串行计划也相等的计划
还有一个概念:冲突,如果两个操作针对不同的事务,在同一对象上执行,并且至少有一个操作是写操作,则会发生冲突。有三种冲突
-
读写冲突:多次读取同一对象时,事务无法获得相同的值
-
写读冲突:事务在写之后放弃了更改,但是放弃之前就有事务读取了更改后的值
-
写写冲突:一个事务覆盖另一个并发事务的未提交数据
三种冲突的位置是不能交换的,其它的操作都可以交换顺序,交换不冲突的操作不改变等效性。
序列化
如果两个计划涉及相同事务的相同操作,并且每对冲突操作在两个计划中的排序方式相同,则这两个计划是冲突等价的。如果某个计划的冲突等价于某个串行计划,则该计划是可冲突序列化的。序列化有两种类型:(1)冲突和(2)视图。其中冲突在DBMS里应用广泛,而视图因为没有可行性不在任何DBMS里应用。
我们要验证并行事务是否为可冲突序列化的,可以一个个挪动不冲突的操作来看看最后是否和串行计划相等,但是这开销太大了,因此验证可冲突序列化的更好方法是使用依赖关系图(优先级图)
在依赖关系图中,每个事务都是图中的一个节点。如果来自的操作与来自的操作冲突,并且发生在之前,则它们之间就会有一条有向边,如果依赖关系图是非循环的,则调度是可冲突序列化的
依赖图并不是万能的,数据库并不能通过依赖图完全判断事务是否为可串行的。。
视图
视图可串行化是一个较弱的可串行化概念,它允许所有具有冲突串行化和“盲写”(即在不首先读取值的情况下执行写)的计划。因此,它允许比冲突序列化更多的计划,但很难有效实施。这是因为DBMS不知道应用程序将如何“解释”值。
如果认真观察业务逻辑,能发现很多可串行化的操作并不是完全和冲突相对应的。特殊的例子可能会在应用层单独处理,这时可能需要抛弃事务这个抽象。但是大多数时候使用事务这个抽象会让我们的业务更简单。
持久化
在崩溃或重启后,提交的事务的所有更改都必须是持久的(即持久的)。DBMS可以使用日志记录或影子分页来确保所有更改都是持久的。