数据库(五),事务

为什么需要事务呢?

数据库(二),数据库起源里面我们提到了事务。

数据库除了对查询等操作进行了抽象,另外一个重要的功能就是事务了。为什么需要事务呢?因为我们在操作数据的时候,可能遇到多个线程同时操作数据的问题,也可能遇到突然数据库故障了的问题,这些都可能造成数据的不一致。所以事务要保证的就是一致性

保证一致性的第一重意思是,这是为了应对多个连接同时连到数据库的时候。因为我们可能为每个连接分配一个线程,而这些线程有可能同时操作同一块数据,这样将会发生不一致。所以我们只好在写的时候加上,也就是强行保证只有一个线程可以访问到这块数据。

另外我们还会遇到数据库崩溃的问题,所以我们要求一个事务一定是原子的,也就是 要么全部发生, 要么根本不发生。比如Bob给Smith转100块,要么Bob有100块,要么Smith有100块,不存在中间状态。

对于单机事务而言,需要保证

  • 原子性
  • 一致性
  • 隔离性
  • 持久性

也就是所谓的ACID,下面我们依次介绍他们是怎么实现的。


image.png

原子性

Undo日志

所谓原子性指的是要么同时成功,要么同时失败。比如Bob账户里面有100块,而Smith账户里面有0元,现在我们希望Bob转100块给Smith。

所谓原子性就是要么Bob成功转给了Smith100块,此时Bob有0元、Smith有100块。要么失败了,Bob仍然有100块,Smith为0元。不会存在Bob把钱转出去了,而Smith却没有拿到钱的情况。

现在我们来想想要实现这个事务,应该怎么做

  • 锁定Bob账户

  • 锁定Smith账户

  • 查看Bob是否有100块钱,如果有,则从账号里面减少100块

  • 给Smith账户里面增加100块

  • 依次解锁Bob和Smith

image.png

但是执行事务不会永远是一帆风顺的,可能出现意外,比如Bob或者Smith账户不存在怎么办?没关系,我们可以回滚到上一个状态。

但是数据库不可能把每个状态都记录下来,这就需要我们在转账之前把之前的状态记录下来。

比如我们看刚刚那个转账操作的中间状态

  1. Bob:100,Smith:0
  2. Bob:0,Smith :0 (此时正在转账)
  3. Bob : 0 , Smith :100(转账成功)

我们可以在插入两个undo段,他们记录在日志中。

  1. Bob:100,Smith:0
  2. Bob:0,Smith :0 (此时正在转账)
    • 上一个状态为:Bob:100,Smith:0
  3. Bob : 0 , Smith :100(转账成功)
    • 上一个状态为: Bob:0,Smith :0

这样如果要回滚,只需要回溯日志即可实现。这

另外还有一种可能就是事务并没有进行完,系统就崩溃了怎么办?那系统重启之后就得做恢复操作啊。那怎么恢复了,同样也是通过日志。我们可以在进行真正的操作之前,需要把要做的事写下来,

我们会在事务开始之前写下:

Bob原有100元,Smith原有0元

如果事务执行到一半就断电,那么重启之后我们就可以按照日志来恢复,然后仍然是** Bob有100元,Smith有0元。即使恢复100次,仍然是这个结果,这就叫幂等性**,所以恢复过程中也断电了,我们仍然可以按照日志来进行恢复。

现在还有个一问题没有解决,那就是怎么知道一个事务没有完成呢?

同样我们可以通过记录日志的方式来完成。比如我们在记录的时候,不但把余额记上,还把事务开始了和结束这两个动作打上标记。

比如

[开始事务T1]
[事务T1:Bob原有100]
[事务T2:Smith原有0]
[提交事务T1]

这样,如果在日志中看到了提交事务T1或者回滚事务T1,我们就知道这个事务已经结束了。如果只看到开始事务T1,那就得恢复。比如下面这个就得恢复

[开始事务T1]
[事务T1:Bob原有100]
[事务T2:Smith原有0]

而且,在恢复之后,需要在日志文件中加上一行回滚事务T1,这样下次恢复就不用再考虑T1这个事务呢,因为现在早已经回到上一个状态去了呢。

Undo日志写入文件的时机

上面的讨论其实我们都故意忽略了一个问题,那就是Undo日志也需要加载到内存中才能读写,但是如果日志还没写好就断电了怎么办?

其实我们只要掌握好把日志写入文件的时机就OK了。

最容易想到的就是在一开始就把日志写入文件,就好比写作文前把草稿打好,后面只管按着草稿誊抄一遍就可以了。

然而,现实是,一开始的时候,我们都不知道程序要操作哪个字段,怎么记录日志呢,当然也不能写入文件呢。所以肯定是一边在内存中操作Undo日志,一边找时机写入磁盘中。

比如上面的转账操作,我们其实可以这样来修改和写日志。

操作 数据缓冲区 日志缓冲区
开始事务T1 [开始事务T1]
Bob = Bob - 100 Bob新余额为0 [事务T1,Bob原有余额为100]
把日志写入文件 注意,日志写入文件后,缓冲区会清空
把Bob余额写入文件
Smith = Smith + 100 [事务T1,Smith原有余额0]
把日志写入文件 注意,日志写入文件后,缓冲区会清空
把Smith余额写入文件 Smith新余额为100
提交事务T1 [提交事务T1]
把日志写入文件 注意,日志写入文件后,缓冲区会清空

总结一下就是,

  • 当余额发生改变的时候,记录之前的余额

  • 在余额要写入硬盘之前,需要把日志先写入文件,然后日志缓冲区会清空。

  • 提交事务的日志一定是在所有余额都写入硬盘之后才写入

也就是说事务过程中,余额发生改变,在余额正式写入了硬盘以后,相当于木已成舟,所以我们也需要把日志写入硬盘。

当所有余额都稳稳当当的落到磁盘上了,我们自然也应该把日志落到磁盘上

那么我们可以攻防演练一下。

如果Bob的余额写到了硬盘,但是Smith还没修改。此时日志中落盘的只有Bob原有的余额也就是:

[开始事务T1]
[事务T1:Bob原有100]

恢复的时候,发现事务没有结束,所以还会把Bob的余额给恢复了。

同理,如果Bob和Smith的余额都落盘了,但是没有提交事务,此时日志是

[开始事务T1]
[事务T1:Bob原有100]
[事务T2:Smith原有0]

依然可以恢复两个账户的余额。

即使两个账户的最新余额都落盘了,也提交了事务,但是只要在日志写入磁盘之前崩溃,则Undo日志还是

[开始事务T1]
[事务T1:Bob原有100]
[事务T2:Smith原有0]

同样会把余额恢复成原样。

原子性做不到的地方

现在可算是把原子性说完了,但是只有原子性是不够的,为什么呢?因为它无法保证多个线程访问数据时的一致性。

比如在第2步的时候,另一个事务把把smith账户加到了300块钱,

  1. Bob:100,Smith:0
  2. Bob:0,Smith :0 ------------->Bob:0,Smith :300(另一个事务干的)
    • 上一个状态为:Bob:100,Smith:0
  3. Bob : 0 , Smith :100(转账成功)
    • 上一个状态为: Bob:0,Smith :0

如果有另一个事务在进行到步骤2的时候把smith账户加到了300块钱,此时如果回滚,会把smith改为0,那加上的300块就丢失了。 那么我们还需要一致性。


image.png

一致性

上一章我们提到了如果在事务中间,有另一个事务突然插手对数据进行修改,则如果出现回退,将会出现数据不一致的问题。

那怎么解决这个问题呢?如果我们一个事务对数据操作完了以后,另一个事务再进入,这样就不会发生争抢和数据不一致了。所以核心就在于加锁

比如

  •  Lock Bob , Smith
    
  1. Bob:100,Smith:0
  2. Bob:0,Smith :0 ------------->Bob:0,Smith :300(另一个事务干的)
    • 上一个状态为:Bob:100,Smith:0
  3. Bob : 0 , Smith :100(转账成功)
    • 上一个状态为: Bob:0,Smith :0
  • unLock Bob and Smith

在事务的开始和结束分别进行加锁和解锁。这样,其他的事务并不可知事务内部的事情。只有在事务单元内部完全成功了以后才对外可见。

到现在我们“仿佛”已经解决了并发、一致两个大问题了,但是新的问题也来了,加锁以后,其他的事务无法对数据进行访问,那么系统的并发度是上不来的,这就是下面的隔离性要解决的问题。

image.png

隔离性

所谓隔离性,其实是以性能作为理由,在破坏一致性。何以见得?因为如果要保证强一致性,最好的方法就是不管读写,统统排队进行,这样一定不会出现数据不一致的情况。

然而此时就做不到高的并发,性能也就上不去。所以我们只要做一些妥协,比如只加写锁,不加读锁。

我们首先需要看看,两个事务单元对同一个数据,有哪几种并发模式,然后定义不同的隔离级别,看每种隔离级别可以实现哪些并发模式。

4种可能

同样我们以一个例子来说明

现在 T1 :Bob要给Smith 100块,然后T2 : Smith要给Joe 100块。

这就是两个事务,如下图所示,为了保证一致性,Smith账户会被两个事务单元锁定。也就是两个事务有共享数据,Bob在给Smith转钱的时候,另一个事务无法对Smith账户进行操作了,并发就上不去。

image.png

此时两个事务单元T1,T2之间只有读写并发、写读并发、读读并发、写写并发4种可能。

  • 写写并行

    什么时候能写写并行,只有当两个事务的数据完全没有重叠的情况下,比如如下的情况。

image.png

因为没有共享数据,所以完全可以写写并行,也就是写写都不加锁。

  • 读读并行

    也就是读操作不加锁,这样读与读可以并行操作,因为读不会修改数据,所以读读可以放心的并行,而不用担心一致性的问题。


    image.png
  • 读写并行

    也就是读的时候,可以并发写。我们知道,写操作会修改数据,但是写是加锁的,所以我们无法读到写未提交的结果。所以虽然两次读到的数据是不一样的,不可重复读,但是每次读到的数据都是正确的,不存在不一致。

  • 写读并行

    也就是写的时候,还可以并发读。因为数据是在不断改变的,很可能读到中间的状态,如果系统在此时崩溃了,重启的时候会恢复到修改前的值,此时自然会出现错乱。
    那么我们是否无法实现写读并行了吗?并不是,可以通过Copy on Write。具体怎么做呢?每次写操作之前都把数据复制一份到log里面,在log里面进行修改。

    其实就是把原来的数据复制一份,然后修改。这样读操作作用的就是原来的数据,而写作用的是备份的数据,互不干扰。

image.png

这种方法又叫(MVCC,Multi Version content control,多版本内容控制)。那么多版本是什么意思。

我们知道数据被复制出去了一份以后,可能会被修改多次,那么下一次读应该读修改后哪个版本的数据呢?这个时候,我们可以在日志里面加上版本号。比如说,现在写入的数据版本号是10,如果要读取版本号为5的数据,则可以往前一直找,直到找到对应的位置。

所以如果读发生在写操作之后,读的版本号一定要大于写的版本号。这样就可以保证读到想要的数据。

四种隔离级别

上面讲了两个事务单元针对一块数据其实有4种并发的可能,接下来我们继续讨论隔离级别。不同的隔离级别可以实现读写并行、写读并行、读读并行、写写并行的一种或者几种。

  • 串行化:

    就是读的时候不允许写,写的时候不允许读,这样可以保证数据强一致,但是性能最低。SQLite默认采用这种方式。

image.png
  • 可重复读,也就是只能实现读读并行读写、写读、写写等不能实现。

    所以在两个都是读的时候,不加读锁,其他情况均需要加锁。

    MySQL默认是这种方式。

  • 读已提交(Read Committed):
    此时当数据被加上读锁了以后,一个写进来,写锁替换掉读锁,也就是可以将读锁升级为写锁。

    那么如果事务T1读取了数据,然后事务T2把这个数据修改了,因为事务T2也是加锁的,所以它会提交,那么事务T1再读取这个数据时,原来的数据已经发生变化了。这就是不可重复读。

    image.png

    此时可以做到读写并行、读读并行,做不了写读并行

    Oracle , PostgreSQL, SQL Server都是使用的这种模式。

  • 读未提交:顾名思义,就是可以读到未提交的内容

    最低级别的隔离,此时只加上写和读是不加锁的。因为数据是在不断改变的,很可能读到中间的状态,如果系统在此时崩溃了,重启的时候会恢复到修改前的值,此时自然会出现错乱。

image.png

要解决写读并行的问题,可以使用上面说过的Copy on write,这种方法最大的好处在于可以保证写读并行,同时隔离级别还很高

image.png

持久性

现在我们来讨论最后ACID的持久性,也就是只要事务提交了,不管是崩溃还是出错,数据一定要写到磁盘上

那么数据什么情况下会丢失呢?

  • 还有就是内存如果掉电,里面的数据就必然丢失,持久性得不到保证。但是如果每一次提交操作完成以后,都将内存中的数据同步到硬盘上,则会造成频繁写硬盘,性能将下降。所以持久性和延迟无法兼得

    我们只要进行折中,比如只要把数据提交到内存,就立刻返回成功,然后将一段时间的请求打包送到磁盘上。这样就避免了每次提交都写磁盘

image.png

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容