隔离级别主要用于ACID中的“I”属性,即隔离。让我们快速回顾一下ACID属性。
A-原子性:它表示事务中的所有指令都应该以原子方式发生。原子性仅仅意味着它不能被分解,因此事务的执行就像它是一个单独的步骤。
例如:A想转500块钱给B,称之为事务。原子性保证了A的余额将被扣除500,B的余额将增加500。这个增加和扣除都是在一个步骤里发送。想象一下如果不是原子的话会发生混乱。例如,A的钱被扣除了,然后交易失败了,B账户未收到转账。
C-一致性:这确保数据库始终处于一致或有效状态。
使用上面相同的例子,假设在交易之前,A的余额是1000元,B的余额是2000元。交易事务完成后,希望看到A的余额=500,B=2500。如果我们思考一下,这两个状态都是有效的。不能让A的余额更新到500,而B的余额不是2500。 因此,在这里,他们的余额的总和可以被认为是一种状态,它应该保持一致,在交易之前和之后。
I-隔离性:当多个事务同时运行时,此属性可确保这些事务不受彼此的影响,从而防止出现问题。也就是说,如果事务按顺序运行,结果应该与并行获得的结果相同。
举个例子,假设有两个并发的事务正在进行: A→B和A→C在每笔交易中转移500。 在多种情况下,并发可能会导致一些问题,比如T1将A的余额读取为1000,同时T2将A的余额读取为1000。分别扣除500发给B和C这导致了问题,因为现在两者都将更新A的余额为500。 由于A的余额现在是500卢比,这与一致性相矛盾,因为在这两笔交易之后A的预期余额应该是0。
我们有几个隔离级别,取决于我们希望事务执行有多严格,这些是本文的主要重点。
D-持久性:它确保一旦事务提交,我们不应该失去它的状态,并且必须被持久化。例如,A有1000元,它把500转给了B。现在,每次我们查询A的余额时,我们应该得到最新的值,不能丢失这些细节。
让我们讨论一下隔离级别,以及为什么在并发环境中首先需要这些级别。
并发环境中的问题:
1、脏读:
脏读指的是错误的或无意的数据,这些数据可能从未存在于数据库中。
假设我们有两个事务T1和T2并发运行。 现在,如果T1插入/更新了一些行,T2在T1提交之前读取这些行。
T2在这里执行了脏读取,因为T1可能决定回滚/中止,并且永远不会提交,所以T2读取的内容永远不存在。
例如:
在事务之前A的余额=1000
T1事务开始
T1读取A的余额=1000
T1更新A的余额=500(可能是转给B了)
T2开始
T2读取A的余额=500【脏读】
T1回滚
这里T2读A的余额=500是脏读,由于T1事务转500从A->B被终止了,同时T2读到了错误的A余额。
考虑另一个错乱,如果T2想转800给C。看到A只有500将返回“余额不足”错误,即使此时A实际余额有1000(假设T1没发生/回滚了)。
2、脏写
类似脏读,脏写可能发生在T1进行时,T2写入某个值。这意味着当T1提交时,它还将提交T2的更改,T2将回滚这些更改。 导致数据库无意的写入。
例如:
T1事务开始
T1读取A的余额=1000
T1更新A的余额=500【可能转给B了】
T2事务开始
T2读取A的余额=500【由于T1已经更新了】
T2更新A余额=300【可能转给C200了】【脏写】
T1提交事务【提交A余额=300】
T2回滚【意味着A->C转的200事务并未发生】
所以在这里,因为A→C从未发生过,只有A→B的500,所以预期的A的余额= 500,但由于脏写,A的余额被错误地更新为300。
3、不可重复读:
当一个事务尝试多次读取数据库行并且每次都得到不同的结果时,就会发生这种情况。例如,如果T1在两次不同的时间读取DB行,并且在两次读取之间,T2更新该行。
考虑如下例子:
T1事务开始
T1读取A的余额=1000【第一次读】
T2事务开始
T2读取A的余额=1000
T2写入A的余额=500【假设A->B转了500】
T2事务提交
T1读取A的余额=500【第二次读】问题出现
因此,顾名思义,当事务进行重复读取时,它会得到不同的值。
幻读:
顾名思义,它意味着一些诡异的阅读发生了。如果T1查询某个范围的行(比如N行),则会发生这种情况,同时T2插入了与T1相同查询条件匹配的额外行。 然后,如果T1再次搜索,它将获得额外的行(幻读)。
例如:
T1事务开始
T1查询:select * from Table where X > 2【假设返回100行】
T2事务开始
T2插入一行X=150
T1执行相同的查询,这次返回的结果是101行。
因此,如上所述,我们可以有上述类型的并发问题,有4个隔离级别来处理这些问题。
在讨论隔离级别之前,让我们先了解数据库的锁。
1、读(共享)锁:如果T1在一行上拿到读锁,T2仍然可以读该行。
这意味着T1和T2都可以在同一行上读(共享锁)。而且,由于T1持有读锁,并且“读不阻塞写”,T2仍然可以通过获取写锁来更新该行。
2、写(独占)锁:如果T1持有一行的写锁,则T2不能读或写该行。(写锁阻塞读锁)。 这意味着如果在一行上设置了写锁,则没有其他事务可以读/写该行。
隔离级别:
1、读未提交隔离级别:
这提供了0%的隔离,因为它也允许读取未提交的数据。 在这样的隔离级别上,所有上述并发问题都存在。
2、读提交隔离级别:
它提供了隔离,只允许读取已提交的数据。 让我们看看它能解决哪些问题。
解决:脏读
T1事务开始
T1读取A的余额=1000
T1更新A余额=500【可能转给B了】
T2事务开始
T2读取A的余额=?【受阻塞】【只能T1事务提交后才能执行】
因此,由于事务只能读取已提交的数据,因此可以防止脏读取。
实现原理
当T1读取A的余额,它拿到了共享/读锁。
然后T1写/更新A的余额,并拿到写锁。
现在当另一个事务试图读取已经在写锁状态的值就不允许的,要等到写锁释放或T1事务完成。
因此一旦T1提交或回滚,写锁释放T2将不受阻塞,并读取A的余额=500,这是正确的值不是脏读。类似地,T2不能更新A的余额,直到T1已经提交/回滚,所以它也可以防止脏写。
不可重复读吗?还会发生
T1事务开始
T1读取A的余额=1000【第一次读】
T2事务开始
T2读取A的余额=1000【读锁是可共享的】
(T1读和释放读锁,该行现在是非锁定的)
T2更新A的余额=500【可以写入因为此时可以拿到写锁】
T2提交【释放写锁,现在改行读写锁都释放了】
T1读取A的余额=500【第二次读】【结果不同不可重复读】
如上所示,尽管T1确保它总是读取提交的值,但其他事务仍然可以更新这一行,这意味着如果T1再次读取同一行,它将得到不同的结果。
这是怎么发生的:
T1拿到读锁,读完成后释放读锁。
T2可以执行更新操作由于没有锁定。
因此当T1试图再次读取同一行时,读取到不同的值。
幻读?还是存在
T1事务开始
T1查询:select * from Tbl where X > 100【结果为3行】
T2事务开始
T2插入1行数据X=150
T2提交
T1再次查询select * from Tbl where X > 100【结果4行】【发生幻读】
如上所示,尽管T1确保它总是读取提交的值,但其他事务可以插入其他行,这可能会影响T1的读取计数。
发生原因:
T1再次对它正在更新的行获取写锁。 然而,如上所述,其他事务仍然可以更新其他未锁定的行。
3、可重复读取隔离级别:
这在读提交之上增加了另一个隔离层,以进一步防止可重复读问题。
这是通过“读锁可以阻塞写锁”原则实现的,这与一般的读锁行为相违背。
实现原理:
正如上面所讨论的读提交隔离级别,T1获得读锁并在读完成后尽快释放它,然后T2可以获得写锁来更新它。
如果T1持有的读锁没有尽快释放,并且它也阻止了其他事务获得写锁,该怎么办?然后,当T1读取(任意次数)时,没有其他事务可以更新该行,从而防止不可重复读取问题。
T1事务开始
T1读取A的余额=1000【第一次读,拿到读锁】
T2事务开始
T2读取A的余额=1000【可以读,由于读锁共享的】
T2更新A的余额=500【阻塞等待锁】
T1读取A的余额=1000【第二次读取结果相同】
请注意:
T1读取->拿到读锁
T2读取->允许,由于读=共享锁。多读是允许的。
T2写->不允许,由于行被锁定,T2将处X-WAIT等待状态
X意思是写【互斥锁】等待写锁。
因此T2不能获取锁来更新这一行,因此当T1再次读取同一行时,它仍然会得到相同的结果。 一旦T1完成,锁就被释放,然后T2就可以获得这个锁来更新。
幻读?还是存在
它不允许对它感兴趣的行进行任何更新。然而,它不能阻止任何幻读,其他事务仍然可以插入新行。
T1事务开始
T1查询:select * from Tbl where X>100 → 3 rows【拿到3行的读锁】
T2事务开始
T2插入一行X=150
T2提交
T1查询: Queries select * from Tbl where X>100 【4行结果发生幻读】
如上所述,虽然T2不能更新T1查询的任何行,但由于T2可以插入新行,幻读问题仍然存在。
4、串行读取隔离级别:
这是最严格的隔离级别,也防止了幻像读取问题。
实现原理:
当T1查询一个范围或记录时,它会获得一种不同类型的锁,这表明它属于这个范围。 这个锁被称为范围锁(范围S-S是它的状态),而不是S代表读锁,X代表写锁。 所以当T1查询一个范围时,所有的行都是范围锁定的。 如果T2尝试插入新行,这可能会影响到这个范围,那么T2将被阻塞,直到T1完成并释放范围锁。 但是,T2可以读取这些行,因为范围锁允许共享读,但阻止某些写操作。
T1事务开始
T1查询:Queries select * from Tbl where X>100 → 【结果100行】
(范围S-S锁定保持100行)
T2事务开始
T2插入一行数据:X=150【阻塞】
T1查询:Queries select * from Tbl where X>100 → 【结果还是100行】
总结:
由此我们可以推断,它解决了上述所有并发问题。