文章初衷
最开始听到隔离级别这个词是在大学上数据库课的时候老师讲到,当时听得一头雾水最终也没理解到,就不了了之了。到毕业开始找工作,每次面试前的知识点整理,总能看到隔离级别这个词,但是每次都停留在背书式的记住。一直对隔离级别这个词感觉很陌生甚至有点抗拒。最近公司在做技术分享,我想分享一下MVCC的理解,在查资料的过程中发现MVCC 跟 隔离级别、undo log等知识点关联还是很密切的,所以也顺便系统的去学习一下隔离级别。
开始正题
我对隔离级别的理解:事务之间互相影响的程度。比如在A事务中需要查询和修改用户余额字段,现在进来了一个B事务也改动了用户余额的字段,隔离级别就规定B事务的改动对A事务的影响程度。
不同的隔离级别导致 A、B两个事务之间的操作有不同的影响。
隔离级别可以理解为处理多事务并发操作数据时的不同策略。下面先列出多事务并发操作可能引起的问题。
ANSI/ISO SQL 定义了 4 种标准隔离级别。
下面根据隔离程度从低到高分别说明。
读未提交(Read uncommitted)
这个隔离级别规定:在查询数据时能读取到未提交事务中修改了的数据结果。
字面上理解起来可能比较难,我们直接来看例子:
时间点 | 事务A | 事务B |
---|---|---|
T1 | 开启事务 | |
T2 | 开启事务 | |
T3 | 查询余额,结果是100 | |
T4 | 修改余额为150 | |
T5 | 查询余额,结果是150 | |
T6 | 提交事务 | |
T7 | 提交事务 |
上面例子可以看到,事务B在修改了余额后,事务A能马上查询到修改后的结果。这就是 读未提交 的效果,结合上面这个例子去理解的话,读未提交就是:事务A能 读 事务B 未提交 的数据。
这个隔离级别会产生一个问题。假如事务B最终没有提交事务,而是回滚了。可以看一下下面这个例子:
时间点 | 事务A | 事务B |
---|---|---|
T1 | 开启事务 | |
T2 | 开启事务 | |
T3 | 查询余额,结果是100 | |
T4 | 修改余额为150 | |
T5 | 查询余额,结果是150 | |
T6 | 事务回滚 | |
T7 | 查询余额,结果是100 | |
T8 | 提交事务 |
由于事务B最终回滚了,事务A在 时间点T4 拿到的余额150是一个脏数据,我们称这个问题为:脏读。
总结来说,在读未提交隔离级别下,数据的改动能实时的被查询出来,这带来的问题是会出现 脏读。
读已提交(Read commited)
理解了上面的读未提交,再来看读已提交的话应该就比较好理解了。
读已提交隔离级别规定:只有在事务提交后,事务中修改了的数据才能被其他事务查询到。
我们还是来看一下例子:
时间点 | 事务A | 事务B |
---|---|---|
T1 | 开启事务 | |
T2 | 开启事务 | |
T3 | 查询余额,结果是100 | |
T4 | 修改余额为150 | |
T5 | 查询余额,结果是100 | |
T6 | 提交事务 | |
T7 | 查询余额,结果是150 | |
T8 | 提交事务 |
根据上面这个例子可以看到,事务B 在 时间点T4 修改了余额但未提交事务,事务A 在 T5 查询余额时,是查不到事务B修改过的余额。在事务B 提交事务后,事务A再去查询余额,就能拿到事务B 修改后的余额。
结合这个例子去理解的话,读已提交就是:事务A能 读 事务B 已提交 的数据。
这个隔离级别不会出现脏读,但会有另外的问题。可以看回上面的例子,事务A 对余额做了多次查询,却得到了不同的结果,在我们实现部分业务逻辑时这可能是一个不正常的现象。因为重复读同一条数据会得到不同的结果,所以我们称这个问题为:不可重复读。
总结来说,在读已提交隔离级别下,数据的改动在事务提交后能实时的被查询出来,这带来的问题是会出现 不可重复读。
可重复读(Repeatable read)
这个隔离级别名称我认为是比较抽象的。由于这个隔离级别解决了 读已提交 产生的不可重复读问题,所以这个隔离级别就叫 可重复读。
可重复读隔离级别规定:事务只能查询到事务开始之前已提交的数据。
我们还是通过具体例子来说明:
时间点 | 事务A | 事务B | 事务C |
---|---|---|---|
T1 | 开启事务 | ||
T2 | 开启事务 | ||
T3 | 查询余额,结果是100 | ||
T4 | 修改余额为150 | ||
T5 | 查询余额,结果是100 | ||
T6 | 提交事务 | ||
T7 | 查询余额,结果是100 | ||
T8 | 开始事务 | ||
T9 | 查询余额,结果是150 | ||
T10 | 提交事务 | ||
T11 | 提交事务 |
事务B 在事务A 开始之后才开始,所以事务B的更新操作不会影响到事务A 的查询,即使事务B提交了,事务A还是拿到余额100的结果。事务C 在事务B 提交之后才开始,所以事务C 能查到事务B 修改后的结果。
可以看到事务A每次查询都能得到同样的结果,这解决了 读已提交 的不可重复读问题。
但这个隔离级别还是存在另外一个问题:幻读。我认为 幻读 这个问题是最难理解的,我们先来看一下例子:
假设我们现在表中有两条数据:
id | account | balance |
---|---|---|
1 | 张三 | 1000 |
2 | 李四 | 2000 |
然后我们进行以下操作
时间点 | 事务A | 事务B |
---|---|---|
T1 | 开启事务 | |
T2 | 开启事务 | |
T3 | 新增一个王五的账号,余额3000 | |
T4 | 提交事务 | |
T5 | 查询列表,返回 张三、李四 两条数据 | |
T6 | 更新王五的余额 | |
T7 | 查询列表,返回 张三、李四、王五 三条数据 | |
T8 | 提交事务 |
上面例子时间点 T5,T6,T7 看上去很诡异,但是实际操作的确也是得到这样的结果。我是这样理解的:
- 事务A 在 T5 查询不到王五的数据,那是因为事务B在事务A之后开始,这里满足 可重复读 隔离级别的规则。
- T6 能更新王五这条数据应该是最让人迷惑的。但其实我们细想一下,这里是事务A 的更新操作,而我们之前谈到的隔离级别都是讲的select操作。在这里事务A 通过select语句的确是查询不到事务A 的数据,但是update 操作根据where其实是能找到这一条数据的。可以理解为,select的查询与update中where查询是不一样逻辑。
- T7 这时候能查到王五这一条数据,是因为T6 对王五做了更新操作,导致王五这条数据存在一个事务A操作过的版本。可重复读隔离级别下,是允许查询到当前事务中修改过的数据,所以事务A 这里查得到王五这条数据也是符合可重复读隔离级别的。
上面这个例子,一开始我们查询列表只有2条数据,后来又查到3条数据,这就是我们上面提到的 幻读。
幻读 与 不可重复读 有那么一点相像,两者都是在用相同条件多次查询时得到不同的结果。我的理解是,不可重复读更侧重于数据内容的变更,而幻读侧重于由于事务内的一些操作导致本来查询不到的数据变成对当前事务可见。
串行化(Serializable)
这是最严谨的隔离级别,为了不因为多事务并行执行导致数据的不一致,直接规定事务串行去执行。就是上一个事务未结束,下一个事务不会开始。串行化比较好理解,就不做举例了。
前面提到的3种问题(脏读、不可重复度、幻读)在串行化隔离级别下,都不会发生,所以说这个是最严谨的隔离级别。
隔离级别越高,需要消耗的性能越多。
MySQL InnoDB引擎默认使用 可重复读 隔离级别
这里额外说一点,读已提交、可重复读 这两个隔离级别的数据其实是比较特别的。我们再拿可重复读中的例子来看一下:
时间点 | 事务A | 事务B | 事务C |
---|---|---|---|
T1 | 开启事务 | ||
T2 | 开启事务 | ||
T3 | 查询余额,结果是100 | ||
T4 | 修改余额为150 | ||
T5 | 查询余额,结果是100 | ||
T6 | 提交事务 | ||
T7 | 查询余额,结果是100 | ||
T8 | 开始事务 | ||
T9 | 查询余额,结果是150 | ||
T10 | 提交事务 | ||
T11 | 提交事务 |
这个例子中,事务A查到的余额一直是100,而事务B在修改余额后,事务B查询余额结果会是150。不知道大家在这里会不会产生疑问,同一个账户的余额,事务A查的到是100,事务B查到的是150,那数据库中这个账户的余额字段存的究竟是存的100还是150?还是100和150余额同时存在于数据库?这其实是一个叫做 多版本并发控制 的知识点,又称作 MVCC(Multi-Version Concurrency Control)。上面例子中,余额为100和150 的数据是同时存在的,这就是多版本的意思。具体的 MVCC介绍留到下一篇文章再说了。