写在开头:在最开始写这篇笔记的时候我是不知道MVCC(多版本并发控制)的,所以我一边写,一边充满了困惑。比如这个RR的实现,在读的时候加S锁直到事务提交,但是我尝试了很多遍发现我无论使用Oracle还是Mysql,读数据都不会阻塞写数据,这一度让我很困惑。不是说了加S锁,就没有办法加X锁所以其他事务没办法修改数据,从而实现了可重复读嘛。。。。直到我认识到这是实现数据库并发控制的不同的两种不同方式,我才恍然大悟。那么先介绍基于事务锁的并发控制
隔离性分为四个级别:
- 读未提交:(Read Uncommitted)
- 读已提交(Read Committed) 大多数数据库默认的隔离级别
- 可重复读(Repeatable-Read)
- 序列化(serializable)
隔离是有代价的,对于基于事务锁的并发控制而言,隔离度越高,并行度就越低。但事务的一致性会越高。通过选择合适的数据一致性从而达到尽可能高的并行度,最大程度发挥系统的性能。
读未提交
数据库支持并发读写的话,是有很多问题要考虑的,如果我们不进行任何并发控制,会出现下面的情况。
比如一个收入表,本来有100块钱,小红买了一个商品,进账10块钱。小李,也买了个商品,也进账30块钱。此时卖出了两件商品,收入表应该增加到140块钱。但是由于此时并发没有任何限制。可能出现不准确的情况。
小红 | 小李 |
---|---|
读取收入表A:100 | - |
- | 读取收入表A:100 |
A=A+10 | - |
写回收入表A:110 | - |
- | A=A+30 |
- | 写回收入表A:130 |
此时小红的增加记录,就可能会被小李覆盖掉。
那怎么办呢。对于任何一种数据库,这种丢失修改低级的错误是绝不允许出现的。
怎么办呢。
写数据的时候,加上排他锁(X锁),读数据不加锁。
在写某行数据的时候,其他进程对该行数据不能有写操作
通过加上X锁,流程就变成了这个样子:
小红 | 小李 |
---|---|
获取A的X锁:==成功== | --- |
读取收入表A:100 | - |
- | 获取A的X锁:==失败==(锁等待) |
A=A+10 | 等待中.. |
写回收入表A:110 | 等待中.. |
结束事务,释放A的X锁 | 等待中.. |
- | 获取A的X锁:==成功== |
- | A=A+30 |
- | 写回收入表A:150 |
- | 释放A的X锁 |
这就解决了这种低级错误问题
也是数据库的最低隔离级别:读未提交:(Read Uncommitted)
mysql中支持Read Uncommitted,但是Oracle无法设置该隔离级别。
Read Uncommitted故名思意,就是可以读取到未提交的数据,也就是脏数据。
如何会出现读取到脏数据的情况呢?如下所示:
小红 | 小李 |
---|---|
获取A的X锁:==成功== | - |
读取收入表A:100 | - |
A=A+10 | - |
写回收入表A:110 | - |
- | 读取A的数据:110 |
RollBack,A恢复为100 | - |
- | 小李读取到了错误的数据... |
这里介绍一下从sqlserver官网引用的一段话:
Transactions running at the READ UNCOMMITTED level do not issue shared locks to prevent other transactions from modifying data read by the current transaction. READ UNCOMMITTED transactions are also not blocked by exclusive locks that would prevent the current transaction from reading rows that have been modified but not committed by other transactions. When this option is set, it is possible to read uncommitted modifications, which are called dirty reads. Values in the data can be changed and rows can appear or disappear in the data set before the end of the transaction.
翻译过来就是:
Read Uncommitted事务隔离级别没有使用共享锁去阻止其他事务修改当前事务读取的数据。READ UNCOMMITTED事务也不会使得独占锁阻塞当前事务读取已被修改但未被其他事务提交的行。设置此隔离级别,可以读取未提交的修改,称为脏读——在事务结束前,数据中的值可能被改变,行也可能出现或者消失。
说的很清楚,在READ UNCOMMITTED级别下,A事务进行update,B事务是无法对同一行进行update的,但是读,是没问题的,而A可能commit,也可能rollback,所以这列B可能读到了脏数据。
所以READ UNCOMMITTED写加了X锁,在事务结束之后释放,读不加锁。
不过话说回来,脏读这样的低一致性基本上没有任何系统支持把。。。所以一般是没人设置此隔离级别的。
读已提交
Read Committed可以解决脏读问题,他是如何做到的呢。我们上面已经看到了,脏读之所以发生是因为读操作没有加锁,所以在事务A写数据的过程中,事务B是可以进行读取的。所以呢,我们可以给读操作也加一个锁,叫做共享锁(S锁)。如果某行数据加了X锁,就没法加S锁;加了S锁,就没法加X锁。但是加了S锁,另一个事务还是可以加S锁的。这样呢,读数据的并发行还是不受影响影响的。
此时的流程变成了来这个样子:
小红 | 小李 |
---|---|
获取A的X锁:==成功== | - |
读取收入表A:100 | - |
A=A+10 | - |
写回收入表A:110 | - |
- | 获取A的S锁:==失败== |
RollBack,A恢复为100 | - |
提交事务,释放A的S锁 | - |
- | 获取A的S享锁:==成功== |
- | 读取A的值:100 |
- | 释放A的S锁 |
通过S锁,我们只能读取到已提交的数据,解决了脏读问题。
这里需要注意的是小李获取到的S锁是在读取操作结束后立刻释放的,而不是在事务提交时刻才释放,因此,等待在该对象上进行写事务就可以较早获得锁继续执行(而不是在小李事务提交之后才能获得X锁)。这样,数据库系统整体得到了较高的并行度。
不过呢,这种读取操作结束之后立刻释放S锁的做法,可能会导致另一个问题。什么问题呢?往下看(小红的处理是在一个事务中):
小红 | 小李 |
---|---|
获取A的S锁:==成功== | - |
读取A:100 | - |
释放A的S锁 | - |
计算C=A+50=150 | - |
- | 获取A的X锁:==成功== |
- | 读取A的值:100 |
- | A=A+30 |
- | 写回A的值:130 |
- | 释放A的X锁 |
获取A的S锁:==成功== | - |
读取A:130 | - |
释放A的S锁 | - |
计算C=A+50=180 | - |
这个时候小红有点懵逼...哎,同一个事务里面,我第一此计算C的值是150,第二次计算C的值是180.到底怎么搞呢,这数据出现了不一致啊。
可重复读
可重复读,顾名思义,在一个事务中,多次读取某一行数据的值,数据是一致的,不会出现第一次读和第二次读数据不一样的情况。那么他是如何做到的呢?
前面已经提及过了,RC隔离级别是读取操作结束后立刻释放S锁。这样换来了更高的并发性,但是也正是因为这种特性,导致了不可重复度。要实现可重复度也很简单,在事务提交时才释放S锁。如下所示:
小红 | 小李 |
---|---|
事务开始 | - |
获取A的S锁:==成功== | - |
读取A:100 | - |
计算C=A+50=150 | - |
做其他事情... | - |
- | 获取A的X锁:==失败== |
做其他事情... | - |
再次读取A:100 | - |
计算C=A+50=100 | - |
提交事务 | - |
释放A的S锁 | - |
- | 获取A的X锁:==成功== |
- | 读取A的值:100 |
- | A=A+30 |
- | 写回A的值:130 |
- | 释放A的X锁 |
这样呢,小红在一个事务中多次读取并计算C的值,得到的总是相同的结果,实现了可重复度。事务就获得了更高的一致性。但是由于S锁也要等到事务结束才释放,数据库系统整体的并发度进一步降低了。
看似在RR隔离级别下,已经解决了所有的问题,但是啊。锁只能加在已经存在的行上面,不能加在还未出现的行上面对吧。
比如做范围查找:
select * from student where age between 18 and 22;
小红 | 小李 |
---|---|
select * from student where age = 18; | - |
读取到5行记录 | - |
获取上述5行的X锁:==成功== | - |
更新上述5行记录 score = A | - |
- | 插入一行age=18,score=B的记录 |
select * from student where age = 18; | - |
多了一行未修改的记录... | - |
小红第一次查,发现有5行数据,代表有5个学生满足在18至22岁之间。此时呢,小李往student表中插入了一行学生数据,该学生为20岁,然后提交。小红再次查,发现有6行数据了。这下小红懵逼了。感觉出现了幻觉。。这就是所谓的幻读。
序列化
小红 | 小李 |
---|---|
开启事务 | - |
A操作 | - |
B操作 | - |
C操作 | 增删改查被阻塞 |
D操作 | 阻塞中 |
事务提交 | 阻塞中 |
- | 操作成功 |
如何解决幻读问题呢?那就只能序列化串行执行了。。实现了最高的数据一致性,但也导致了最低的并发度。。。