引言
现在数据库的实现已经很成熟了,基本已经能够满足我们日常的工作和学习所需,但是有的时候,人还是要“犯贱”一下嘛,理解数据库系统是怎么保证我们的事务操作的一致性(当然也有不一致的情况,大不了全部回滚嘛)
文章有点长,慢慢食用~
废话不多说,本篇文章从以下三个方面,深入理解数据库系统的锁的机制,如果有不正确的地方,还请留言告知
三级封锁
两段封锁
多粒度封锁
锁的由来
数据库(DB),数据库系统(DBMS) 相信大家都比我还清楚,不清楚的可以去找度娘...
举个栗子
人物:小明 (常出现于初中作文、英语、数学题、物理题、化学题, 这次也有幸在我们的数据库出现)
任务:开商店没错,就是我
情景一
-- 时间:电脑还未普及的年代
需求:记账
解决方案:
给他一支笔,一个本子(有条件就给专门记账本,没条件给一张纸);
记录内容:日期,天气,时间,购买人,购买物品,数量,单价,实收;
(好了,够他玩一阵子了)缺点:适应现阶段的需求
情景二
-- 时间:电脑普及
需求:电脑记账(对的,你没看错,小明就是这么潮)
解决方案:
1.创建一个记账.txt,好了,拿去玩吧;
2.使用数据库,创建字段,进行录入每笔交易记录;小明现在就一个店铺,一台电脑,所以,他自己去玩吧...
情景三
--- 时间:电脑不值钱,并且每件物品的库存总数变化要在DB中有所展示
需求:连锁商店自动化记账,并且录入一个数据库(假设当前每个店铺只有一台电脑)
解决方案1:
给每个连锁店的电脑安装数据库,并且每台电脑之间的数据互不相通;
每台电脑各自记录,并将当前数据记录到各自的DB中;
固定时间,进行店铺汇总;
解决方案2:
建立云端的数据库,每个连锁店铺的电脑对云端的数据库进行读写;
可以实时读写,即李梅买了一桶油,付钱完成,就与远端服务器进行链接并进行记录;也可以先记录到本地,每天固定时间,各个店铺对远端服务器上面的DB进行事务操作;
情景四
-- 时间:小明连锁商店变成了连锁超市
需求:每间店铺内的结算电脑不只一台
解决方案:
暂时不是本片文章讨论范围,以后会在其他文章内进行给出,敬请谅解
好了,至此,已经可以大致了解了小明同学一路的创业之路,我们要好好向他学习成功的经验,要从他....
(咳咳,走错片场了....┭┮﹏┭┮)
下面我们来逐步分析各个情景:
1.对于情景一、二,我们提供的解决方案基本可以满足小明同学的需求了,至于他有什么界面美观呀,操作便捷呀之类的需求,一概无视,我是程序员,又不是美工;[・`Д´・ ]
2.对情景三而言,问题比较严肃。我们需要对每台电脑同步数据到云端的数据库,进行控制,否则会出现写入异常问题,特别是对总数进行增减。
假设现在有两台电脑
1.T1,T2
2.DB中A的数量为100
他们都需要对数据库中的商品A进行数量修改
T1 今天卖出了30件
T2今天进货60件,卖出去70件
操作如表1
Time | T1 | T2 | A |
---|---|---|---|
time1 | Read(A) | --- | T1-A: 100 T2-A: 0 DB-A: 100 |
time2 | ---- | Read(A) | T1-A: 100 T2-A: 100 DB-A: 100 |
time3 | Write(A - 30) Commit(A) | --- | T1-A: 70 T2-A: 100 DB-A: 70 |
time3 | ---- | Write(A + 60) | T1-A: 70 T2-A: 160 DB-A: 70 |
time4 | ---- | Write(A - 70) | T1-A: 70 T2-A: 90 DB-A: 70 |
time5 | ---- | Commit(A) | T1-A: 70 T2-A: 90 DB-A: 90 |
显然,在不经控制的时候,T1和T2对数据的操作是存在很大的问题的,那么我们该如何解决呢?
...
串行,先执行T1,当T1执行完毕后,再执行T2,或者两者交换顺序。
这是一个不错的解决方法,但是对于小明这样的,动不动就喜欢开连锁店的人而言,你可以顺序执行10家,100家,但是对于1000家,10000家呢?(不要以为小明不会开这么多店铺,他可是小明呀o(╥﹏╥)o)
我们这个时候要实现串行怎么办呢?要保证一台电脑执行完了,在执行另一台电脑。这就是下面我们将要提到的两段封锁机制。
在此之前,我们要明白我们为什么要串行执行任务,是因为串行执行可以保证数据库A的值安全;
但是为什么串行就可以保证数据库A的值是安全的呢?
这里我们将对数据的操作可以分为Read和Write两个操作;
在表1中,T1读取了A的值,还没来得及对A进行写的时候,T2也对A进行了读取;
那我们要阻止在T2读取数据A的时候,T1对A进行写;或者在T1对数据A写的时候,不允许T2进行读写;
是不是晕了?没事,吸两口,就能懂,很简单的
吸两口
有了上述的要求,那我们是不是可以创建锁,对A进行加锁,
当读取的时候添加读锁(共享锁,share,这个名字真的不是我随意取的);
当写入的时候添加写锁(排它锁,x锁,这个名字也不是我随意取的)
我们要求:
在读取的时候,别的事务不能对其进行写操作,但是可以读操作;
在写取的时候,别的事务不能对其进行读、写操作;
具体的情况我们可以用表2来展示
锁 | S(share-lock) | X(x-lock) | no-lock |
---|---|---|---|
S(share-lock) | |||
X(x-lock) |
好了,我们对造成表1的原因进行的分析,并进行了限制,现在我们再来模拟一下表1中的执行(这里我们假设锁的释放为用完即释放,Eg:S-lock(A) -> Read(A) -> S-Unlock(A))
表3
Time | T1 | T2 | A |
---|---|---|---|
time1 | S-lock(A) | --- | T1-A: 0 T2-A: 0 DB-A: 100 |
time2 | Read(A) | --- | T1-A: 100 T2-A: 0 DB-A: 100 |
time3 | S-unlock(A) | --- | T1-A: 100 T2-A: 0 DB-A: 100 |
time4 | --- | S-lock(A) | T1-A: 100 T2-A: 0 DB-A: 100 |
time5 | ---- | Read(A) | T1-A: 100 T2-A: 100 DB-A: 100 |
time6 | --- | S-unlock(A) | T1-A: 100 T2-A: 100 DB-A: 100 |
time7 | X-lock(A) | --- | T1-A: 100 T2-A: 100 DB-A: 100 |
time8 | Write(A - 30) Commit(A) | --- | T1-A: 70 T2-A: 100 DB-A: 70 |
time9 | X-unlock(A) | --- | T1-A: 100 T2-A: 100 DB-A: 100 |
time10 | --- | X-unlock(A) | T1-A: 70 T2-A: 100 DB-A: 70 |
time11 | ---- | Write(A + 60) | T1-A: 70 T2-A: 160 DB-A: 70 |
time12 | ---- | Write(A - 70) | T1-A: 70 T2-A: 90 DB-A: 70 |
time13 | ---- | Commit(A) | T1-A: 70 T2-A: 90 DB-A: 90 |
time14 | --- | X-unlock(A) | T1-A: 70 T2-A: 90 DB-A: 90 |
WTF为啥还不行...
Why?
看样子,仅仅只是对数据项添加对应的锁是不行的,那是锁的问题吗?还是加锁的机制有问题?
(装逼时刻)我明确的告诉你,锁是没错的,我们要在加锁的时候,采用一定的策略,这样就能实现我们所要达成的问题
接下来,我们再考虑一下,什么策略可以保证各个计算机在并行对数据库进行操作的时候,可以确保其结果和某一个串行的执行顺序一致?
可能有人会说,为什么是某一种串行,因为就现在小明连锁店的情况而言,可能只需要对DB中的数据进行加减即可;但是不能保证每次的执行的事务都是加减法吧。2333
咱们引入一个概念串行调度
多个事务进行操作时,如果以事务为单位,多个事务依次执行
可串行化
对于一个并发事务集来说,如果一个调度与同一事务集中某一个串行调度等价,则称该调度为可串行化
讲人话
简单的来讲就是,在并行调度的时候,我们要确保并发执行的操作事务,与某一种事务串行的结果一致;
至于课串行化,就是在并行调度的过程中,对原子事务进行交换,且不改变原并行调度执行结果,可以得到一个串行调度的队列
有了以上的知识,明确了我们需要一种机制来保证事务的并发执行与串行调度一致,介绍以下封锁机制
两段封锁
首先搬出各大书中对其的定义(定义嘛,看不懂直接跳过)
扩展阶段:申请并获得各种类型的锁。此阶段只能申请事务中需要的锁,但是不能释放锁
收缩阶段:释放所有申请的锁。此阶段只能释放该事务申请的锁,且不能再申请锁
简单的来讲就是,在一个事务中所有的封锁操作必须出现在第一个释放锁的操作之前
Eg:
T Slock(A) Slock(B) Slock(C) 第一阶段 扩展阶段
T Unclock(B) Unclock(C) Unclock(A) 第二阶段 收缩阶段
不正确的例子Slock(A) Slock(B) Unclock(B) Slock(C) Unclock(C) Unclock(A)
thinking...
下面我们通过表4来演示一遍两段封锁
Time | T1 | T2 | A |
---|---|---|---|
time1 | S-lock(A) | --- | T1-A: 0 T2-A: 0 DB-A: 100 |
time2 | Read(A) | --- | T1-A: 100 T2-A: 0 DB-A: 100 |
time3 | X-lock(A) | --- | T1-A: 100 T2-A: 0 DB-A: 100 |
time4 | --- | S-lock(A) | T1-A: 100 T2-A: 0 DB-A: 100 |
time5 | --- | Wait() | T1-A: 100 T2-A: 0 DB-A: 100 |
time6 | S-unlock(A) | Wait() | T1-A: 100 T2-A: 0 DB-A: 100 |
time7 | Write(A-30) | Wait() | T1-A: 70 T2-A: 0 DB-A: 100 |
time8 | Commit() | Wait() | T1-A: 70 T2-A: 0 DB-A: 70 |
time9 | X-lock(A) | Wait() | T1-A: 70 T2-A: 0 DB-A: 70 |
time10 | --- | Read(A) | T1-A: 70 T2-A: 70 DB-A: 70 |
time11 | --- | X-lock(A) | T1-A: 70 T2-A: 70 DB-A: 70 |
time12 | --- | Write(A+60) | T1-A: 70 T2-A: 130 DB-A: 70 |
time13 | --- | Write(A-70) | T1-A: 70 T2-A: 60 DB-A: 70 |
time14 | --- | Commit() | T1-A: 70 T2-A: 60 DB-A: 60 |
time15 | --- | S-unlock(A) | T1-A: 70 T2-A: 60 DB-A: 60 |
time16 | --- | X-unlock(A) | T1-A: 70 T2-A: 60 DB-A: 60 |
终于对了,按照两段封锁协议,T1和T2的并行操作没有问题
但是,However他们只是对一个数据进行修改呀,要是对DB中多个数据进行修改呢?
假设:DB中现有三个商品分别为A:30 B:60 C:90
T1: A: -10 B: + 30 C:-7
T2: C:-10 A: -20 B: + 20
我们依旧采用两段封锁来做实验,如表5
Time | T1 | T2 |
---|---|---|
time1 | S-lock(A) | --- |
time2 | Read(A) | S-lock(C) |
time3 | X-lock(A) | Read(C) |
time4 | S-lock(B) | S-lock(A) |
time5 | Write(A-10) | Wait(A) |
time6 | X-lock(B) | --- |
time7 | Write(B+30) | --- |
time8 | S-lock(C) | --- |
time9 | Write(C) | --- |
time10 | --- | --- |
额,死锁了...
由于两段封锁协议的定义,T1在未对C进行加锁和操作前,不能对A,B进行释放;同样的,T2在没有对A,B进行加锁操作前,不能释放C,这就导致了死锁。
由此可知,两段封锁协议,虽然可以从并发调度的角度上面将事务进行串行化,以保证其结果满足一种串行调度,但是仍然无法有效的排除死锁问题。
三级封锁
已经理解了两段加锁,对于现在要讲的三级封锁,理解上就会很容易
一级封锁协议:事务T在对数据D进行写操作之前,必须对D加X锁,保持加锁状态直到事务操作结束;
二级封锁协议:事务T在读取数据D之前必须先对D加S锁,在读完之后即刻释放加在D上的S锁;与一级封锁协议一起构成二级封锁协议
三级封锁协议:事务T在对数据D读之前必须先对D加S锁,直到事务结束才能释放加在D上的S锁;与一级和二级封锁协议构成三级封锁协议
作者:jufengliushao
链接:https://www.jianshu.com/p/59d87e5fd63d