Lecture #18:TimeStamp Ordering Concurrency Control
Lecture18目录:
- 时间戳并发控制
- 乐观并发控制(OCC)
- OCC三个阶段
- 读取阶段
- 验证阶段
- 写入阶段
- 执行
- 验证阶段
- 向前验证
- 动态数据库和幻读问题
- 重新执行扫描
- 谓词锁定
- 索引锁定
- 四个隔离级别
时间戳并发控制
时间戳排序是一种乐观型并发控制协议, DBMS假设事务冲突很少发生。DBMS不在要求事务在被允许读写之前必须获取锁。 而是分配一个时间戳来确定事务的可串行化顺序。
这个时间戳总是递增的。 不建议用时间来作为时间戳, 因为会有夏令(夏令是什么我不知道), 导致时间戳不准。 同样如果利用计数器, 那么分布式情况下不好维持全局唯一。 一般是有一个时间戳管理系统来分配。 一个事务可以分配多个时间戳, 并且时间戳什么时候分配也是一个问题。 本节课主要讨论的是一个事务只有一个时间戳。
一般情况下采用两者结合。
乐观并发控制(OCC)
乐观并发控制(OCC)是乐观并发控制协议的一种,也是最主要的协议。 它使用时间戳来验证事务。当冲突数量较少时, OCC效果最佳。
乐观并发控制协议, 基于时间戳排序, 当事务启动时, 数据库系统会为其分配一个私有工作区。 并且无论他们何时读取或写入任何内容, 读取都将始终复制到此私有工作区中。他们进行的任何写入也将位于该事务独有的此私有工作区中, 没有其他事务能够看到这个工作区。这意味着, 当执行写操作时, 我不需要获取全局数据库上的锁, 我将操作我的操作区中的元组。
OCC三个阶段
读取阶段
此阶段, 事务实际上可以读取和写入数据库。 但是, 无论何时读取和写入, 都是从全局数据库复制到该事务的私人工作区。 然后以后每次读取, 都是从私有工作区中读取。 确保可重复读。
验证阶段
然后一旦事务想要提交, 那么就进入验证阶段。 这个阶段数据库会为你分配事务的时间戳。 然后查看是否与其他事务冲突, 这些其他事务可能是过去的, 可能是正在运行的。
写入阶段
过了验证阶段, 就进入了写入阶段。 此阶段会将要写入的元组写入到全局数据库, 然后该元组就有一个写入时间戳, 来跟踪写入该元组的事务。
执行
有两个事务T(1), T(2)。 然后数据库里面有一个原始的元组A:
接下来T1Begin, 然后T1 R(A)。
然后T2进入, R(A):
T2R(A)后进入验证阶段, T2被分配一个时间戳, 假如此时分配到了1. 那么T2被分配1时间戳。 因为T2没有修改任何内容, 那么它的工作区里面的内容不会合并到全局数据库中。
T2COMMIT后, T1W(A), 此时T1还没有进入验证阶段,所以他还没有被分配时间戳, 所以他修改的数据的跟踪时间戳W-TS是无穷大。
然后T1 R(A), 读取到自己工作取得A = 456。 然后进入验证阶段, 此时被分配时间戳2, 就要把工作区中的无穷大时间戳元组都改为时间戳2。
因为有修改, 并且与其他事务没有冲突(冲突后面详细解释), 所以可以进入写入阶段。 进入写入阶段就是把工作区中的数据写入到全局数据库。 然后完成COMMIT。
验证阶段
上面有一个细节, 就是验证阶段怎么直到能不能进入吸入阶段。 所以, 验证阶段的目标就是确定事务是否允许提交, 从而保证我们只允许可序列化的调度。
这里有两种方法, 一种是向后验证,一种是向前验证。 这两种的方法在于, 我是回顾过去, 查看我已经提交的事务和我的事务是否错过了更新, 还是我要展望未来, 这意味着我要知道未来的其他事务。 这里的未来指的是逻辑上的未来。
这里向前验证是指检查正在提交的事务, 其读写集是否与仍在运行但尚未提交的任何活动事务的读写集相交。
向后验证则是查看是否有事务运行了我本应该看到但实际没有看到的更改?
((TODO),这里不好理解, 需要结合后面的例子才能稍微理解一些。)
下面展开向前验证:
向前验证
对于向前验证, 我们将在每个事务进入验证阶段时, 为其分配一个时间戳。 然后, 如果我们的事务要提交, 则对于每个同时运行的其他事务, 必须满足下面三个条件之一:
这里我们要明确一件事情, 就是我们再进行向前验证的时候, 实际上我们直到许多事情: 我们知道当前正在进行的事务有哪些, 知道这些事务的工作区, 知道他们的读集合, 知道他们的写集合,及修改。
然后, 我们要利用这些东西去让该事务与其他事务依次判断, 如果与对比的事务满足三个条件之一, 那么就判断成功。 如果与所有的其他事务都判断成功, 那么就说明这个事务能进行提交。
第一种情况非常简单, 就是如果T1已经完成了提交, T2才开始运行, 那么就像相当于串行执行, 两者不会存在冲突。
第二种情况是, 如果T1已经完成了写入阶段, T2还在运行,此时T2还有可能读取和写入。 那么T1如果没有修改T2读取的任何对象, 那么就不存在冲突。 T1可以安全提交。
简单说就是T1有一个写入集合, T2有一个读取集合, 如果两个集合不相交, 那么就没有冲突。 因为T1已经完成了写入, 如果T2的读取对象有T1修改的,那么T2就是读取旧的数据了, 逻辑上T2应该在T1提交之后读取, 读取的时T1写入的数据,但是现在读取的时旧的了, 就违背了时间戳排序。
这个例子:
一开始T1在自己的工作区中R(A),W(A)。 然后T2在自己的工作区R(A), 他读取的是A = 123。 然后T1进入验证阶段。 因为T1进入验证阶段时T2还在运行, 并且此时T1的写入和T2的读取有交集, 所以不允许T1提交。 从另一个方向观察就可以发现T2本应该读取到T1 W(A)后的数据456, 但是之读取到了123。
T1先提交, 时间戳应该是1, T2后提交时间戳应该是2。 所以T1应该看到1之前的0, T2应该看到1。 但是T2其实看到的是0.
如果换一种交互顺序就可以:
在这个里面, T2先验证, T2验证的时候, T2没有写如果, 所以写入集合为空。 显然T2的写入集合和T1的读取集合相交一定为空。 所以T2完成验证可以提交。 然后T1验证, 因为T2此时已经完成了验证, 所以T1也可以验证成功。
第三种情况就是T1正在提交阶段, 如果T1的写入集合与T2的写入集合交集为空, 并且与T2的读取集合交集为空。 那么就可以提交。
在这里T1先验证, 因为T2只有R(B)。 所以交集都为空, 可以提交。 然后验证T2, 因为T1已经提交, 所以可以提交。
动态数据库和幻读问题
幻读问题就是我一开始读取整个数据库的某个表, 然后有另一个事务在insert数据向该表。 然后我就读取到了看到了一条新的原本不存在的数据。
这个是OCC和两阶段锁无法保证的。 对于两阶段锁来说, 只有存在的数据才能锁, 不存在的数据无法锁, 所以这个新出现的数据两阶段锁无能为力。 对于OCC来说(为什么不行, OCC不是在自己的工作区进行读取吗, 这里当作OCC的工作区保存的数据, 因为要读进来, 所以在读的过程中出现了幻读。 TODO)
OCC和两阶段锁都只能工作在静态的表中。
其中一种方法是锁定一切。就是把整个表, 都锁定。但是这样很慢, 所以只考虑接下来的三种方法(但是用的可能并不是非常少。在四种方法中排第二,Andy的幻灯片给出了Less Common的评价,仅次于索引锁定):
重新执行扫描
如果记录了每次查询的Where语句, 那么当事务提交时。 无论使用OCC还是两阶段锁, 都需要冲洗执行查询中的扫描部分, 看看是否有新的结果产生。
谓词锁定
假设对于事务1的扫描查询, 在多维空间中存在一个由Where子句指定的有界区域。 该区域对应可能存在的元组。 然后多个多维空间如果有重叠, 那么他们逻辑上访问的是相同的元组。 (TODO不理解,Andy说只有四个数据库是使用这种方法,并且是NP。 这种方法实现起来很困难, 先不理解了。)
索引锁定
利用索引来管理一个范围内的锁, 检查这些范围内是否存在并发冲突。
对于索引锁定, 可以是键值锁, 锁管理器会记录这个B+树的特定范围内的某个键值锁。
也可使用间隙锁,跟踪键值中间的间隙,对其加锁。(TODO, 为什么加一个锁,就能防止幻读了?)
有了这些细粒度的锁, 还可以在内部节点添加更粗粒度的锁, 锁住页面,或者多个元组, 这个锁是意向排他锁,对于意向排他锁, 我可以获取内部的更细力度的锁, 但是对于内部的锁, 只可以获取一个排他锁。
四个隔离级别
四个隔离级别就是:读未提交、读提交、可重复读(快照隔离)、串行化