17-时间戳顺序并发控制

17-时间戳顺序并发控制

并发控制介绍了两种思路:

  • 二阶段锁
    • 悲观方法:在问题出现之前, 采取措施阻止问题的发生
    • 缺点
      • 锁会影响性能,锁意味着等待,
  • Time stamp Ordering(T/0)
    • 时间戳顺序并发控制 :根据时间戳确定顺序,决定出问题如何处理

image-20240731182504182

如果事务\(TS(T_i)\)发生时间小于\(TS(T_j)\),数据库要保证\(T_i\) 一定比\(T_j\) 先发生.

image-20240731182741835

时间戳如何而来?

  • 时间戳要求单调递增

时间戳多种实现策略:

  • 全局系统时钟
    • 问题:电脑的时间不是完全精确的。之所以表现精确是因为网络矫正,在线同步
  • 全局逻辑计数器
    • 问题:分布式也存在同步问题
  • 混合方法(Hybrid)

image-20240731182954687

首先介绍 基础时间戳协议,其次介绍进阶版本 OCC,最后研究隔离级别

image-20240731183128539

BASIC T/O

image-20240731183212463

时间戳方法不需要给object加锁。

每一行记录都要附上两个时间戳:

  • 读时间戳:上一次被修改的事务的时间戳

  • 写时间戳:上一次被读的事务的时间戳

    每一次操作都要检查时间戳

  • 宗旨是不能操作未来的数据

image-20240731183510057

读的流程如下:

如果发现事务的时间戳 比目标object X的时间戳还要大,说明 object X 被一个时间戳大的事务修改过。(比如一个事务先发生,但是去做其他的事情了,在这个期间,记录X另外一个刚开始的事务修改了。此时就会发生上述现象,这就叫做来自未来的数据)

  • 此时该事务要回滚,更换一个新的时间戳重新开始

反之:如果该事务的时间戳比较object x的时间戳要新

  • 允许该事务\(T_i\)读记录X
  • 更新X对应的读时间戳
  • 将记录X拷贝一个副本,方便事务\(T_i\)后续再读(副本就是版本)

image-20240731184207245

写的流程如下:

如果 事务\(T_i\)的时间戳 小于记录X的读时间戳和写时间戳:

  • 根据不能操作未来数据的原则,直接回滚

反之:如果 事务\(T_i\)的时间戳 大于记录X的读时间戳和写时间戳

  • 允许事务\(T_i\) 修改X,同时更新X的写时间戳
  • 同时,copy一个副本去保证事务\(T_i\)的后续读

从上面可以得出时间戳算法的基本操作原则:事务不能对时间戳大于自己的记录进行操作,只能操作时间戳小的。只能更新过去的数据。

上面简单的规则就能保证对于并发的事务,产生一个可执行的、正确的调度?

案例:

下图有两条记录:A和B,后面是读时间戳和写时间戳。假设事务T1时间戳是1,事务T2的时间戳是2。T1先开始。

时间线

  • T1 :读B,因为T1时间戳更新,所以可以读。 同时更新B的读时间戳为1

  • T2 : 读B,T2时间戳为2,比B的时间戳要大,所以可以读。更新B的时间戳为2

  • T2: 写B,T2时间戳新,更新B的时间戳为2

  • T1 读A,更新A读时间戳为1

  • T2 读A,更新A读时间戳为2

  • T1 再次读A,因为T1的时间戳 小雨 A的写时间戳,所以可以读。此时更新读时间戳,因为读时间戳比T1的时间戳更新,所以A的读时间戳并未改变

案例1 两个事务没有发生冲突,所以 时间戳算法没有起作用/

初始状态:

image-20240731190555030

最终状态:
image-20240731190646486

案例2:

时间线

  • T1读A,更新A的读时间戳
  • T2写A,因为T2的时间戳 大于 A的读时间戳和写时间戳,所以可以读A,更新A的时间戳
  • T1写A,因为A的时间戳 小雨 A的读、写时间戳,不能写未来的数据,所以 A回滚

开始状态:

image-20240731190724112

T1写A触发时间戳算法,A回滚

image-20240731191049150

上述案例2,按照T O的方法是要回滚的,但是在T1 写A这步是可以优化的。即T先写A,然后保证T1发生在T2之间,相当于T1写的数据会被T2进行覆盖。

这个可以优化的点,称之为 托马斯规则。

当T1的时间戳 小于X的写时间戳,说明X被一个新的事务进行写操作了,托马斯规则说,针对这种情况也可以写,可以先写,然后被新的事务再覆盖(反正未来也会被覆盖掉)。

一个数据如果之后都没有人读,但是后面会有人修改,那我在自己的事务内就可以当成我完成了修改。 (存疑)

image-20240731191401166

用托马斯规则修改案例2

image-20240731191932410

image-20240731191952858

如果不考虑托马斯规则,基础TO算法可以产生一个冲突串行化的调度,

  • 它的优点是没有死锁,因为不需要加锁等待
  • 问题是:
    • 长的事务可能会一直处于饥饿状态
      • 如果一个事务几百条sql,那么不断的回滚,可不就一直饥饿吗

一个事务是可恢复的 当切仅当 它修改的数据都是在已经提交的事务的基础之上去修改。

否则,数据库很难保证当数据库发生崩溃之后再给你恢复过来

image-20240731192625638

案例解释:可恢复调度

时间线:

  • T1写A
  • T2 读A,因为T2更新,所以可以读老数据,所以可以读A
  • 但是T1一直到T2commit之后,回滚了。此时T2就是不可恢复的,因为T2已经commit了。
    • 数据库崩溃的时候,先恢复T1才能恢复T2,因为T2读的数据是T1写进去的。T1都没有了,那么T2怎没恢复?(我不理解)

image-20240731203557888

上述说明了BASIC TO 会导致不能恢复的事务。

TO 还有性能问题:

  • TO算法在读或者写的数据的数据,要往本地copy,代价很大
  • 长事务有可能会饥饿

由基础TO进阶而来的一些思考:为了优化基础TO算法,引出了OCC

image-20240731204040271

如果你可以确认事务之间的重复比较少 并且 大多数的事务比较短,那么这种无锁的方法相比较有锁的方法要好。

一个更好的方法是在针对一些具体的confilct的例子上做优化。大模型+小模型的方法

解决方案:OCC 乐观的并发控制方法

image-20240731204333956

OCC也是基于TO算法的。其基本思想如下:

数据库为每一个事务创立了一个私有本地空间

  • 任何事物读取的数据,都可以copy到这个私有本地空间。
  • 修改操作直接在该私有空间中进行,不直接写入数据库底层

当一个txn提交之后,数据库会比较你提交的数据和别的事务进行比较,如果没有冲突,会让你提交。如果有问题,怎么解决?
具体来讲:

OCC方法有三个阶段:

  • 读阶段:对数据库来说的是只读,不写,comit之后再写
    • 将事务读取的数据写入私有空间
  • 校验阶段
    • 要提交的时候,跟别的事务相比较,有没有冲突 (跟git很像)
  • 写阶段:对于数据库来说,是写入
    • 真正的将私有空间要记录的东西写入数据库;如果有冲突,直接回滚

image-20240731205802480

案例1:

时间线:注意T1和T2开始都没有时间戳

  • T1读A 复制到私有空间T1 workspace
  • T2开始读,复制到T2的私有空间。注意这里是检验之后,才给事务复制时间戳
  • 校验通过,T2将其写入数据库

开始状态:

image-20240731205848230

时间线:

  • T1发来一个写的SQL
  • T1进入校验,给T2 一个时间戳,校验,写入数据库

image-20240731210114937

image-20240731210154131

接下来,详细介绍三个步骤的具体过程:

写阶段

  • 从数据库中读取数据写入本地数据库,方便后续重复读取

image-20240731210240915

注意:长事务要修改的行很多,那么性能是不是很大?所以说应该是有试用范围的

校验阶段:

  • 当事务调用commit时候,数据库会校验该数据和其他的事务是否有冲突,校验原则如下:
    • 数据库要保证串行化(小时间戳先发生,大时间戳后发生)
    • 检验当前事务和其他的事务是否存在 读写冲突和写写冲突 单向不要成环(不懂)

校验有两种实现方案

  • 前向校验
  • 后向校验

image-20240731210535256

后向校验:

  • 2号事务向历史上已经提交过的事务进行校验 ,看看和历史提交的事务有没有成环的冲突。如果没有,就直接提交;如果有,T2就自杀

image-20240731211124901

前向校验:

  • 未来的事务还未发生,所以校验的是和未来事务交叠的部分。没发生的无法校验image-20240731211207872

未来的事务没有commit,T2也没有commit,此时如果有冲突,就可以灵活的选择kill掉哪个

image-20240731211310011

每一个事务的时间戳是校验阶段开始的时候才会赋予的。

比较当前事务与未来事务已经发生的部分。

如果\(T_i\)的时间遭遇战\(T_j\), 有如下三种情况:

第一种情况:

  • \(T_i\)完成三个阶段之后,\(T_j\)才发生。 这两个事务是完全串行化的。这个就不需要等觉串行化,因为它是真的串行化。

image-20240731212819281

第二种情况

  • Ti再Tj进行写阶段之前完成, Ti进入validation阶段,Tj开始写
  • Ti要提交的数据没有被Tj读过

image-20240731212919707

案例:

在T1 validate阶段,因为T2 在T1validat之前读了A,但是T1 在T2读之前读并且写了A。等于数据库中的数据copy了两份。现在T1要提交,假设提交上去,那么T2读的A就是错误的,此时T1要回滚。

image-20240731213257306

T2 早于T1进入validate阶段,

T1进入validate的时候,T2还没有write。T1的write set和T2 的read set没有交集(这里没有讲清楚)

image-20240731213439194

第三种情况:

  • Ti完成读阶段,之前Tj也已经完成了读阶段
  • Ti写的东西和Tj读的东西不能有交集
  • Ti写的东西和Tj写的东西不能有交集

image-20240731213652337

案例解释:

T1validate更早。

初始状态:

image-20240731213923739

站在T1validate的时间点上,T2读的A是提交的A,就没有问题。(这里没有讲清楚)

image-20240731214005170

image-20240731214117414

OCC的第三大步骤:

写阶段

  • 要锁全表,怕并发问题

image-20240731214142706

OCC的思考:

OCC的适用场景

  • 冲突比较少
  • 所有的事务都是只读是最好的,跟二阶段锁相比,性能提高很多
  • 或者 事务要提交的数据之间没有交集,这就没什么好校验的,速度当然快

当数据库非常大的时候,查询是非倾斜的,查询是均匀的话,这时候冲突的概率非常小,比较适合OCC

image-20240731214252869

OCC的问题:

  • 复制到本地,开销很大
  • 校验逻辑复杂。write 阶段由于锁表,不能并发可能成为瓶颈
  • OCC出问题是当所有的事务活都干完了,才知道有问题,所以只要一出问题,干的活全部浪费了。浪费很大。2阶段一旦发生死锁,直接就干掉了。浪费的比较少

隔离级别

二阶段和TO都 可以产生等效串行化的调度序列,这两种东西防止不了幻读。

前面写讨论的都是update的问题,没有讨论过insertion和delete时候的问题。

案例

脏读问题:

  • T1事务 读了两次 people表中的最大值,但是第二次读的数据已经被修改了

之前的二阶段锁和OCC都没有考虑到这个问题,这叫幻读,读到了第一次不存在的东西

image-20240731220939536

研究二阶段锁和OCC为什么预防不了上述的问题:

加了锁,只能锁现存的东西,控制不了你插入新的东西

image-20240731221055577

幻读的解决方案

image-20240731221117662

方法1:重新扫描

image-20240731221237646

方案2: predicate locling 谓词锁

  • 给select语句后,where的谓词加共享锁
  • 给update、insert、delete 语句,where之后加 拍他锁

image-20240731221316770

加锁之后,你想写入,你插不进去

image-20240731221453911

方法3:索引锁

谓词中有索引,锁索引页

image-20240731221540221

就是不让你插入进去或者删除。如果没有索引,那么就加表锁。

image-20240731221601476

mysql 的方案是间隙锁。

前面的工作都是致力于将一个并发的事务变成可串行化的

image-20240731221809064

serializable是一个很高的隔离级别。有时候并不需要,有些业务允许并发发生错误。

所以有了隔离级别的想法。

image-20240731222016853

控制事务之间隔离的程度。

  • 脏读问题:读未提交
  • 不可重复读:两次读的数据不一样
  • 幻读:两次读的数据发生了变化

隔离级别:

image-20240731222114438

隔离级别问题矩阵:

image-20240731222155255

隔离级别的实现:

image-20240731222222278

串行化:严格二阶段锁

可重复读:放开索引锁

读已经提交:S锁实时放开

读未提交:不增加S锁

逐渐的退化,性能会越来越高

image-20240731222337839

sql92 标准规定如何设定隔离级别。

不是所有的数据库都支持上述四种级别,也不仅仅只有这4种级别

统计:当前数据库默认隔离级别和最高隔离级别

image-20240731222440594

MVCC会研究快照隔离

从用户角度调查哪个隔离级别用的最多。

image-20240731222540611

92标准还规定了其他

image-20240731222648158

总结

image-20240731222748316

下一节课研究MVCC,多版本控制

posted @   金字塔下的蜗牛  阅读(103)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示