数据库隔离等级(翻译)

译者按:

原来用MySQL,也知道有Isolation Level这东西,但是官方文档把我劝退了, 而且再加上本身参与的项目大都不需要特别关注这方面的调整,所以对此一直都是一知半解。今天看了这篇文章,瞬间觉得这个当初觉得很晦涩的问题一下子变的很直观,现在翻译过来,供那些跟我一样欠着这个`技术债`的朋友把这个坑填了。

作者原文链接贴在这里了,本人翻译水平有限,有条件的还是建议读原文。

https://medium.com/geekculture/transaction-isolation-levels-f438f861e48a

Let’s have a quick recap of ACID properties.

让我们快速复习一下ACID属性

A --- 原子性: 这表示所有事务中的指令都应该以原子操作形式呈现。简单来说,原子性就是不可分割,所以事务执行起来就像是一条指令。

比如, A想给B转500块钱。我们把它作为一个事务。原子性保证A账户的扣款操作和B账户的转入操作要组合成一步。想象一下如果没有原子性,A的账户被扣款了,然后事务失败了,B的账户并没有转入这笔钱。

C --- 一致性: 这是用来保证数据库永远处于一致的状态。还是拿上面的例子来说,A的账户里有1000块钱,B的账户里有2000块钱。A转给B500块钱,这时我们期望A的账户余额为500,B的账户余额为2500。这里我们可以把A和B的账户余额之和看成一个状态,这个状态在事务执行前后要保持一致。

I --- 隔离性: 这个特性是用来保证多个事务同时进行的时候,它们不会相互影响。简单来说,就是这种并行事务的执行结果与顺序执行的结果一致。

举个例子,说有两个同时执行的事务, A转钱给B, A转钱给C, 都是500块钱。此时会有几种可能会引起问题的场景,比如, 事务1读取A的账户余额是1000, 同时事务2读取A的账户余额也是1000。两个事务都在A的账户里扣500块钱分别转给B和C。但是这里有个问题,就是两个事务都会将A的账户欲额更新为500。所以A的账户余额最终是500。这就违背了一致性,因为A的账户余额最终应该是0。我们有几种隔离等级,其严格程度各不相同, 这将是我们这篇文章的重点。

D --- 持续性: 此特性保证一旦事务成功提交,我们不会丢失它的状态,并且这个状态是永久的。比如A的账户里有1000块钱,然后他给B转了500块钱。从这时起,往后的任何一个时间点,我们查询A账户的余额,我们将的到最新的余额,并且不会丢失这些细节。

让我们来聊一聊隔离等级和为什么这些隔离等级在并发环境中是首要需求。

并发环境所存在的问题:

1. 脏读

这里说的脏是指那些不存在的错误或者并非有意暴露出来的数据。

假如我们有两个事务, T1和T2, 同时在运行。现在如果T1插入或者更新了几行数据,并且T2在T1提交之前读取了这几行被修改的数据。T2实际上就执行了一个脏读,因为T1可能会回滚并且永远不再提交。所以T2读取的数据实际上压根就不存在。

​​​​​​​

例子
事务执行前A的账户余额 = 1000元

事务1开始
事务1读取A的账户余额=1000元
事务1设置A的账户余额=500元 (比如向B转账等等)
事务2开始
事务2读取A的账户余额=500元 [脏读]
事务1回滚

此时,事务2读取A的账户余额为500元,这就是脏读, 因为那个从A账户往B账户转账的事务1已经回滚,然后,事务2就读取了错误的值。考虑一下这种情况的后果,如果事务2的原有意图是向C转800块钱。它此时将认为A的账户余额只有500元,因此它将返回一个余额不足的错误,虽然实际上此时A的账户里有1000块钱(因为事务1回滚了)。

2. 脏写:

Synonymous to Dirty reads, Dirty writes can happen when T1 is going on, and T2 writes some value. This means when the T1 commits, it will also commit T2's change as well, which T2 would have rolled back.
This will lead to unintentional write to the DB.

与脏读相似,脏写是指在事务1的执行过程中,事务2写入了一些数据。这意味着当事务1提交的时候,它同样提交了事务2所做的修改,而事务2有可能在后续的过程中回滚。这将导致非本意的写入。

例子
事务1开始
事务1读取A的账户余额=1000元
事务1设置A的账户余额=500元(例如向B转账500元)
事务2开始
事务2读取A的账户余额=500元(因为事务1已经更新了它)
事务2设置A的账户余额=300元(例如向C转账200元) [脏写]
事务1提交. (提交A的账户余额 = 300)
事务2回滚. (意味着A对C的200元转账没有发生).

此时,因为A对C的转账没有发生,所以只有A对B的500元转账, A的账户余额应该是500, 但是因为脏写,A的账户余额被错误的设置成了300

3. 不可重复读:

这通常发生在事务尝试多次读取数据库时, 其直接表现就是每次获取的结果都不一样。比如事务1读取某数据行2次,在这两次读取当中,事务2更新了该行。

例子:
事务1开始
事务1读取A账户余额=1000[Read 1]
事务2开始
事务2读取A账户余额=1000
事务2设置A账户余额=500 (例如A向B转账500)
事务2提交
事务1读取A账户余额=500. [Read 2] — 问题出现了.

就像它名字表达的那样,当一个事务进行重复的读取,它得到的值是不一样的。

4. 幻读:

就像这个名字所表达的,它意味着一些幻象读取。这可能发生在事务1查询出若干行数据,但是同时事务2插入了新的符合这次查询条件的数据行。然后当事务1再次查询时,它将读取到新增的行(幻读)。

例子
事务1开始
事务1查询: select * from Table where X>2 → 假如返回了100行数据.
事务2开始
事务2插入一行数据 X=150
事务1重复相同的查询, 但是这次返回了101行数据.

我们可能会遇到上面提到的这些并发问题,相应的我们有4个隔离等级来应对这些问题。

在正式介绍隔离等级之前,让我们先来了解一下数据库中的锁

  1. 读锁(共享锁): 如果事务1获得了某行数据的写锁,事务2仍然可以读取这行数据。这意味这事务1和事务2可以同时读取该行数据。同样因为事务1持有读锁,并且“读取者不会阻塞写入者", 所以事务2在获取写锁后仍然可以更新这行数据。
  2. 写锁(独占锁): 如果事务1或得了某行数据的写锁,那么事务2就不能读取或者写入该行(写入者阻塞读取者)。这意味着如果事务获取了某行数据的写锁,那么其他事务就不能再读写该行。

隔离等级:

1. 可读到未提交数据:

此等级不提供任何隔离性,因为它允许读取到未提交的数据。在这个等级下,以上所有的提到的并发问题都有可能发生。

2. 可读到已提交数据:

只有已提交的数据才可以被读到。让我们看一下它能解决哪些问题

脏读: 解决.

事务1开始
事务1读取A的账户余额=1000
事务1设置A的账户余额=500 (例如向B转账)
事务2开始
事物2读取A的账户余额= ? [阻塞] (将不会被执行,直到事务1结束)

因为事务只能读取已提交的数据,所以它有效的防止了脏读

如何起效的?

当事务1读取A的账户余额时, 它请求读/共享锁。

然后当事务1写/更新A的账户时, 它获取写锁。

现在,当其他事务想读取这些被写锁保护的数据时,这些操作会被阻塞,其他事务会等待写锁释放,也就是事务1完成。

所以一旦事务1提交或者回滚,写锁被释放,事务2也将被解锁,并且读取到A的账户余额为500, 这是正确的值,不是脏数据

同样的,事务2不能更新A的账户,直到事务1提交或者回滚,所以它也可以防止脏写的发生。

不可重复读? 没有效果

事务1开始
事务1读取A账户余额=1000 [Read-1]
事务2开始
事务2读取A账户余额=1000
(事务1读取完数据立刻释放所获取的读锁, 此时该行数据被解锁)

事务2设置A账户余额=500
事务2提交(释放写锁)
事务1读取A账户余额=500 [Read-2] [不同的结果] [不可重复]

就像上面所展示的, 虽然事务1保证它所读取的都是已提交的数据,但是其他事务仍然可以更新事务1所读取的行,当事务1再次读取这些行时,它们的值就变得不同了。

为什么会这样:

事务1获取读锁,然后读取行,一旦读取完毕,事务1会尽快释放该锁。

事务2此时可以执行更新行的操作,因为这行的锁已经被释放了。

因此,事务1在尝试再次读取这行的时候,所得到的值就与第一次不一样了。

幻读? 还是存在.

事务1开始
事务1查询 select * from Tbl where X>100 → 3 rows
事务2开始

事务2插入一行数据X=150
事务2提交
事务1查询 select * from Tbl where X>100 → 4rows [幻读].

就像上面所展示的,即使事务1保证只读取已提交的数据,其他事务仍然可以插入新的行,而这些插入的行可能会影响事务1的查询结果

为什么会这样:
事务1针对它要更新的行请求一个写锁

但是就像上面所解释的,其他事务仍然可以更新其他未加锁的行。

3. 可重复读隔离等级:

该等级的隔离在读取已提交数据等级的基础上增加了另一层隔离,从而进一步防止可重复读的问题发生。

之所以可以达到该效果,完全是依赖于“读取者可以阻塞写入者”原则,该原则与通常所用的读锁的行为正好相反

作用原理:

在上问提到的读取提交数据隔离等级中,事务1读取数据之前获取读锁,在读取完成之后会尽快释放该锁,然后事务2就可以获取写锁来更新该行数据。

如果事务1不会立即释放读锁,并且这个读锁可以阻止其他事务获取写锁呢?

这样的话,事务1在读取过程中(无论读多少次)其他事务都无法更新该行,因此就不会出现不可重复读的问题

事务1开始
事务1读取A的账户余额=1000 [Read-1] {该行的读锁不会在此操作后释放}
事务2开始
事务2读取A的账户余额=1000 (因为事务1获取的是读所,所以此时事务2可以读取该行)
事务2更新A的账户余额=500 [阻塞][写入需要获取写锁]
事务1再次读取A的账户余额=1000 [Read-2] [同样的结果]

需要注意的点:
事务1读取 → 获取读锁.
事务2读取 → 允许, 因为读锁是共享锁,多个读操作是允许的.
事务2写入 → 不允许, 因为该行被锁住了, 并且事务2会处于X-WAIT状态下.
X代表写(或者互斥独占锁) — Wait表示等待获取写锁.

所以事务2无法获取写锁来更新该行,当事务1再次读取该行的时候它仍然可以获取到相同的结果

一旦事务1完成了,读锁会被释放,然后事务2就可以获取写锁来进行更新了

幻读? 依然存在.

该机制可以阻止其他事务修改你所读取的行,但是它不能防止幻读,其他事务仍然可以插入新行

事务1开始
事务1查询 select * from Tbl where X>100 → 3 rows (获取到3行的读锁)
事务2开始
事务2插入一行新数据X=150
事务2提交
事务1查询 select * from Tbl where X>100 → 4rows [幻读].

就像上面所说的,尽管事务2不能更新那些被事务1所查询的行,但是事务2仍然可以插入新行,所以幻读问题依然存在

4. 序列化读隔离等级:

这是最严格的隔离等级,即使是幻读的问题也可以被屏蔽掉

运作机制:

当事务1查询某个范围内的数据时,它会请求一种不同类型的锁,这个锁的作用对象将会是事务1所查询的范围。

与S代表读锁,X代表写锁不同, 这种锁被称为范围锁(范围S-S是它的状态)。

所以当事务1查询一个范围的时候,该范围是被锁定的。

如果事务2想插入一行该范围内的数据,事务2将会被阻塞直到事务1完成并且释放该范围锁

但是事务2是可以读取该范围内的行的,因为范围锁允许共享读取,但是禁止写入

事务1开始
事务1查询 select * from Tbl where X>100 → 100 rows
事务2开始
事务2插入一行X=150 [阻塞]
事务1再次查询 select * from Tbl where X>100 → 100 rows [相同结果].

因此我们可以得出结论,该等级的隔离可以解决上述所有并发问题

总结:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值