16-二阶段锁

16-二阶段锁

智能即压缩:用少数的规律来汇总足够多的数据。这种少数的规律以函数的形式体现。用一个函数来表示这些规律。只要函数足够复杂,其可解释性必要要变弱。从数据中提取规律是一个信息压缩的过程。

本节基于事务的理论,如何实现事务。二阶段锁是第一种实现的方案。

如下是一个 写读的冲突。

前面讲的理论都是 从结果上分析事务是否可以等效成串行化执行(马后炮)。如何用程序来实现?

image-20240730193324780

image-20240730193719794

加锁是一种方案。

image-20240730193835897

image-20240730193850138

本结首先介绍

  • 锁的类型
  • 二阶段锁
  • 死锁的检测和预防
  • 层级锁

latches:保护B+树的节点。

locks保护的是数据库的内容。宏观的概念。不懂

image-20240730193946142

锁的最基本类型:共享锁和拍他锁。 共享锁就是读锁,排他锁就是写锁。

共享锁和拍他锁的兼容情况。兼容性矩阵。

image-20240730194118280

带锁的情况下,事务如何执行?

  • 事务申请锁
  • 锁管理器授权或者 阻塞锁
  • 释放锁

锁管理器内部记录着锁的当前状态

image-20240730194220991

如下案例:加锁之后还是不可串行化。没有改变本来不可串行化的问题。

虽然加了锁,但是T2写了A,T1再次读A的时候,数值还是会发生改变

image-20240725211756452

image-20240730194533117

于是有人改进了,提出了二阶段锁,用于并发控制。并发控制的协议或者方法。

image-20240725211920251

用这种方法不需要提前知道 transcation要干什么?执行过程中就可以避免不可串行化的问题

其步骤如下:

image-20240725212018762

增长阶段:

  • 只能不断加锁

收缩阶段

  • 只能开锁。不能再加锁了

通过上面两个规定,就可以解决前面的不可串行化的问题。解决了啥问题?

image-20240725212218103

如下就不正确。

image-20240725212317786

如何解决自动加锁,将冲突转化为串行化问题

用二阶段锁 解决刚才的问题:
由于增长阶段不能解锁,所以T2的申请就拒绝了。

image-20240730195100496

二阶段锁可以将冲突串行化,出来的依赖图是不会有环的

但是二阶段锁会导致级联回滚。

image-20240725212510371

如下事务,是严格执行二阶段锁的,T2是基于T1的临时版本之上进行的读写操作。如果T1回滚了,那么T2也要回滚。

执行了二阶段锁,还是有W-R冲突。

image-20240730195437230

2PL和MVCC是解决隔离性的两种思路

上面的问题是在t1的临时版本上,做了操作,结果t1回滚了,基于t1修改进行的事务都是要回滚的。

一个简单的解决方案:牺牲性能也可以。

  • 上面案例是因为T2在T1的临时版本上做了操作,所以就让t2不要基于T1的临时版本上修改。所以解决方案是 一个事务执行完毕之后(commit之后)再进行解锁

这样就完全串行化了。

传统的二阶段锁会有如下问题:

  • 上述的问题叫做脏读。读到了一些还没有commit的数据
    • 解决方案是严格二阶段锁 SS2PL
  • 会导致死锁

image-20240725212906456

为了解决 2阶段脏读问题,于是引入了严格的二阶段锁

解决方案:严格的2PL

image-20240725213106205

commit之后,将锁全部释放

image-20240725213139268

严格的含义:目标数据一直到txn结束之后才能进行修改。

优点:

  • 不会产生级联回滚
  • 回滚事务 直接回滚到最开始的状态(?不懂),因为其修改事务的时候,绝对不会有其他的事务来操作目标表格

案例:

image-20240725213238313

T2:算一下 A和B的和

不用二阶段锁,随意并发,会造成不一致的情况

image-20240725213355072

保证前后一致性。如果使用了2阶段锁:如下图示

可以保证一执行,但是会造成级联回滚的问题。

image-20240725213452138

强严格二阶段锁

image-20240725213609533

事务所有有可能的调度中选择一个子集来穿行话。

是否能将所有的冲突都串行化?应该是所有冲突最差的解决方案就是串行化。能不串行化就不串行化。

真正串行化执行的是很少的调度。

冲突串行化

视图串行化:

不会级联回滚的

SSPL 满足了基于冲突的串行化,又满足了级联回滚。

image-20240725213753856

死锁问题

业务逻辑:T1先操作A,再操作B;T2先操作B,再操作A;

业务逻辑写死了。没办法解锁了,永远互相循环等待。属于卡了二阶段锁的bug了

二阶段锁的一个问题,死锁。最简单的解决方案:让一个事务失败。退出,选择哪个退出也是一个问题

ss2PL也没有办法避免死锁

image-20240730201149479

锁的等待成了一个环

image-20240725214047963

针对死锁,一般有两个解决方案:

  • 死锁周期行检测
  • 死锁预防

死锁检测:

内部创建一个 图: 数据库就周期性的去监测这个图,看是否有环?如果有环,就把想办法把环破坏掉。注意,这个不是依赖图,是等待图。

image-20240725214111981

根据左边的事务,画出了依赖图:

image-20240725214243964

在锁增长的阶段不允许解锁。

解决方案:选择一个倒霉蛋让它回滚,这样其他就可以进行下去了。“victim”: 倒霉蛋

干掉的事务直接终止或者重启。

image-20240725214312887

处理死锁这部分开销 成本要控制好;太快不行,太慢也不行。成本要权衡。

选择牺牲的那个事务也要考虑:基本上是代价最小原则:

  • Age,比如 已经执行多长时间,快执行完了,就不能干掉
  • 比如事务已经执行的条数,有的已经执行100多条了,有的执行了2条
  • 看这个事务已经加了多少把锁了, 不一定越多越好
  • 多少其他的事务因为这个事务回滚过

考虑事务被回滚多少次,不能老是一个,比如你转账总是不成功,这时候会导致饥饿现象。

image-20240725220414565

选择好了牺牲品之后,如何回滚它?

  • 全部回滚
  • 部分回滚 判断是哪个sql语句造成的回滚,只回滚那几条sql语句即可

image-20240725220428813

借助图理论去检测是否存在死锁。

死锁预防

image-20240725220517755

根据时间戳,给定各种事务分配优先级别。

  • 越早开始,优先级越高

死锁预防有两个策略:

  • wait-die
    • 如果老的事务申请锁,但是锁被年轻的事务所持有的话,那么老的事务就去等待
    • 如果反过来,年轻的等老的,就自杀,重开。因为比你年轻,所以让着你
  • Wound-wait
  • 如果是一个老 的事务想获取一把锁,这把锁是一个年轻的事务所持有,老的事务直接把这个锁抢过来
  • 如果反过来,如果年轻的想获取锁,发现锁被年老的锁持有,那么年轻的等待老的事务

总体:不要不想等。优先级总是有高有低的,要么高的等低的,要么低的等高的,如果反过来等,直接干掉一个,打破了死锁成立的条件。(

我还是不懂

如果年老的等年轻的,到底是干掉还是继续等?这准则到底咋用呀?

image-20240725220541112

年轻的等年老的,年轻的直接自杀,重新投胎

老的发现一把锁被年轻的持有,直接抢过来 ,优先级更高的抢过来

高优先级别的等待低优先级的,高的直接把低的干掉

尊老锁,爱幼锁

先begin的优先级别高。

如下第一个例子:T1级别高,T1等待T2。T2拥有锁,对于Wait-Die,老的等;对于Woind-wait,年轻的被杀了。老的获得锁

案例2:

T1级别高,先获取锁;T2 级别低,需要获取锁;根据Wait-die规则,T2直接自杀,重开,根据Wound-wait,T2 等待。

image-20240725221215507

死锁预防有两个问题:

  • 为什么这种策略可以预防死锁?
    • 要么年轻的等老的,要么老的等年轻的,,不会互相等待,相当于干掉了死锁的成立条件,不会成环
  • 如果年轻的被干掉了,那么新优先级是什么?
    • 给新的时间戳还是第一次执行的时间戳?如果一致更新优先级,那这个事务可能会一直处于不执行状态,出现饥饿现象。反之,则时间戳保持不变,则总会混成老的。

image-20240725221502517

mysql应该是一种,会检测死锁。

一些观察;

之前的锁就一种粒度:行锁。

如果一个事务想更新十亿条数据,那么要增加10亿个行锁

一次性获取太多锁,那么获取锁,解开锁这本身就是一个很大的开销,如何降低开销?

ps:数据库是为了解决了一个问题,而后引出了一个新的问题。螺旋式上升,随着问题的解决,不确定性在不断的减少。其实:交易系统也应该是这样。大模型套小模型+规则

image-20240725221545720

引出了锁的粒度问题,继而引出了层次锁。

如果数据库想获取一些锁的话,数据库可以辅助做一些粒度上的调整。

image-20240725221745542

锁的属性,锁的行,锁的页,锁的表?锁的域。也就是说数据库可以设置多种粒度的锁,辅助事务的执行。

万门的情况下,数据库应该保证加锁的数量相比较事务实际需要的要足够的少。

加锁粒度也有一个权衡, 在加锁的开销和并行中有一个权衡

  • 比如 更新两行数据,将整个表给锁了,那么会影响其他事务,所以数据库要做权衡。更少锁的,更大的粒度 还是 更多的锁,更小的粒度
  • 锁的数量要少,就用大粒度锁;锁的粒度要多,那么就用小的粒度

image-20240725221857160

层级锁

一个表锁解决了所有行锁的问题。一般最细的粒度是行。表粒度上锁,意味着该表下所有的行和属性都被锁住了,这样对其他行做操作是不允许的。

image-20240725221942915

案例:如果我想对table1加锁,就需要判定该表的每一行是否加锁了?如果有一行被被人上了锁,那么我就不能对该表加锁了 。

引出的问题:

如何快速判断一个表中某一行是否被加了锁?给表 打上标记,感觉和索引类似,比如当给行加锁的时候,你可以在表上打一些标记, 告诉别人我虽然没有锁整张表,但是里面某些行我加了锁。这样如果有人想上表锁,那么就不需要进行行遍历了。

意向锁

如上问题引出了意向锁。

意向锁允许更高层级的节点(比行层级更高的比如表)加一些锁,而不用check所有的子节点。它并没有真正的锁表,只是一些意向,排除一些选择。

image-20240725222207826

意向锁有三种:

  • IS:下面的行有被加共享锁的情况 S锁。表没加锁,但是下面有记录加了S锁
  • IX:同上,下面有记录加了排他锁。
  • SIX:表的某些行被加了排他锁, 但是整张表又被加了共享锁。用共享锁锁了整张表,用排他锁锁了底下的一些行

image-20240725222252173

三种意向锁和之前介绍的s和x锁,构成了更加复杂的锁兼容矩阵

每一行代表的是T1持有某些锁,列代表的是T2想加什么锁。(还是不理解?)

image-20240725222435028

复杂的加锁方案的阐述:

  • 每一个事务需要持有合适的锁在高的层级
  • 对于下面层级想加S或者IS的话,那么对于上面层级必须要挂一个IS锁。想对行加S和IS锁,表必须加IS锁
  • 对某些行加X、IX,SIX锁,那么表一定要挂IX锁
  • 即对行加锁的时候,对表也要加锁,方便后来的事务检索

image-20240725222540630

还是要解决最开始的问题:表层级加锁,那么如何快速判断其行是否加锁?

上下层级约束,在下层级有某些锁,则上层级锁必须增加某些标记

image-20240730220518823

例子:

读tuple1 加S锁,表加IS锁。

image-20240730220548868

更新tuple2,写操作,所以要先对表的层级加IX锁。根据兼容矩阵,IX和IS是可以兼容的,

image-20240730220708967

案例2:

三个事务并发

T1: 遍历表,更新部分的tuple(ex:带着谓词去找)

T2:点查询

T3:全表扫描,没有更新操作

image-20240725222816585

在做读表操作之前,要先对R表挂SIX锁(为什么是这个)

image-20240730224434368

image-20240725223014283

点查询:

表的层级

image-20240725223112373

全表扫描:在表的层级挂一个s锁。SIX和IS是可以共存的。

第三业务来了,全表扫描,全表扫描 直接在表的层级挂一个S锁,问题是SIX和S不能共存,被SIX中的IX卡住了,因为IX涉及修改操作。 所以其需要等待,等待T1业务完毕之后,可以去做。image-20240725223134849

T3 不用逐一核查,在表的层级直接核验,有SIX,所以直接就等。达到提升性能的目的。

image-20240730224822721

层级锁在工程上非常好用, 极大的避免了加锁太多的情况。针对问题:在底层挂了一个锁,另外一个事务需要整个扫描,确认能不能在上层加锁。于是发明了意向锁,意向锁相当于在上层增加了一些标记。

IS:需要对表下层行加S锁,所以要对表加IS锁。

image-20240725223304257

锁粒度的优化

  • 如果底层增加了太多的锁,多到一定比例,就不会再加了,直接换成表锁。

这可以减少锁管理器的工作量。

image-20240725223353512

工程上实际的加锁情况:

  • 工程实践不需要手动加锁,都是自动加锁,数据库根据用户的操作自动加了,比如想update 某一行,自动加锁。

image-20240725223904381

有些用户就想加锁,所以数据库也开放了增加显示锁接口。数据库也提供了这种语法。

image-20240725224002987

显示加行锁的操作:

for update:选出来的数据增加排他锁,后面要更新。自动升级为拍他锁。

image-20240725224021180

有一些特殊的业务需要手动加锁。比如数据库备份,不想备份表A的时候,表B在更新。备份表B的时候,表A在更新。此时要对所有的表加S锁。

image-20240725224157033

总结:

  • 二阶段锁几乎应用在所有的数据库中。 比如 mysql就是二阶段锁,已经支持MVCC。但是二阶段锁的应用范围有限,大多数可能是通过时间戳序列完成的。通过二阶段锁达成事务的可串行化。
  • 通过二阶段锁,自动的生成正确的事务调度,通过二阶段锁,放心的让事务并发,可以避免所有并发出现的问题

通过二阶段锁实现事务的可串行化,自动的生成事务并发日程

通过二阶段锁放心的让事务进行加锁

下一节课,研究基于时间戳顺序的并发控制

image-20240730230131564

两阶段锁的线索:

为什么引入加锁机制?程序化检查

普通的加锁存在什么问题?

如何加锁? 二阶段锁

普通二阶段锁的问题? 严格二阶段锁

锁必须分层级,引出了层级了锁

第二种实现思路:基于时间戳

第三种:MVVC

posted @   金字塔下的蜗牛  阅读(63)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
点击右上角即可分享
微信分享提示