前言
首先要推荐这个数据库的文章,感觉写的真好:
如果有人问你数据库的原理,叫他看这篇文章
简介
引用原文中关于数据库四种隔离级别的介绍。
现代数据库不会使用纯粹的隔离作为默认模式,因为它会带来巨大的性能消耗。SQL一般定义4个隔离级别:
串行化(Serializable,SQLite默认模式):最高级别的隔离。两个同时发生的事务100%隔离,每个事务有自己的『世界』。
可重复读(Repeatable read,MySQL默认模式):每个事务有自己的『世界』,除了一种情况。如果一个事务成功执行并且添加了新数据,这些数据对其他正在执行的事务是可见的。但是如果事务成功修改了一条数据,修改结果对正在运行的事务不可见。所以,事务之间只是在新数据方面突破了隔离,对已存在的数据仍旧隔离。
举个例子,如果事务A运行”SELECT count(1) from TABLE_X” ,然后事务B在 TABLE_X 加入一条新数据并提交,当事务A再运行一次 count(1)结果不会是一样的。
这叫幻读(phantom read)。读取已提交(Read committed,Oracle、PostgreSQL、SQL Server默认模式):可重复读+新的隔离突破。如果事务A读取了数据D,然后数据D被事务B修改(或删除)并提交,事务A再次读取数据D时数据的变化(或删除)是可见的。
这叫不可重复读(non-repeatable read)。读取未提交(Read uncommitted):最低级别的隔离,是读取已提交+新的隔离突破。如果事务A读取了数据D,然后数据D被事务B修改(但并未提交,事务B仍在运行中),事务A再次读取数据D时,数据修改是可见的。如果事务B回滚,那么事务A第二次读取的数据D是无意义的,因为那是事务B所做的从未发生的修改(已经回滚了嘛)。
这叫脏读(dirty read)。多数数据库添加了自定义的隔离级别(比如 PostgreSQL、Oracle、SQL Server的快照隔离),而且并没有实现SQL规范里的所有级别(尤其是读取未提交级别)。
默认的隔离级别可以由用户/开发者在建立连接时覆盖(只需要增加很简单的一行代码)。
Oracle支持两种事务隔离级别:READ COMMITTED(默认事务隔离级别),SERIALIZABLE。
MySQL支持四种事务隔离级别,其中REPEATABLE READ为默认事务隔离级别。
实验
看到这个介绍简直也是一头雾水。直接做实验来理解吧。
0#准备工作
建立一个balance表,里面有一列数据money,咱们就玩id=1这一行的money,初始值为2960。
然后打开两个远程登录的窗口,都打开MySQL数据库,开始实验。
从隔离级别低到高,分别是:读取未提交、读取已提交、可重复读、串行化。
1#读取未提交
- 查看窗口1的会话隔离级别,发现MySQL的默认隔离级别是Repeatable read。
select @@tx_isolation;
- 设置会话隔离级别为实验的read uncommitted:
set session transaction isolation level read uncommitted;
- 在窗口1开始一个事务并查看money。
-- 窗口1
begin;
select money from balance where id = 1;
- 然后在窗口2开始一个事务并修改money。
-- 窗口2
begin;
update balance set money = money - 1000 where id = 1;
select money from balance where id = 1;
可见在窗口2这个事务中money已经被修改,但是还没提交。
- 回到窗口1,再查看money
-- 窗口1
select money from balance where id = 1;
可见,虽然窗口2的事务还没提交,但是窗口1的事务已经可以读到还没提交的数据,所以这就叫做 读取未提交 。可以看到,两个事务的隔离性很低,这是四种隔离级别中最低的级别。
- 那么,如果窗口2的事务发生错误,将数据回滚,money变回原来的值,实际上money不应该发生变化,可是咱们的窗口1的事务还是读到了 错误 的 回滚前 的1960,这就叫 脏读 。
-- 窗口2
rollback;
select money from balance where id = 1;
commit;
money变回2960了。
- 所以,如果窗口2的事务代表转账,money从2960转走1000变为1960,然后转账出错,回滚回2960。
- 数据库的隔离级别如果是read uncommitted的话,其他的事务(窗口1 的事务)就有可能再中间读到1960这个错误值。这就叫 脏读啊亲。
2#读取已提交
- 先将窗口1的事务结束掉(commit),然后设置隔离级别为read committed。再开始一个新事务,读取money。
-- 窗口1
commit;
set session transaction isolation level read committed;
begin;
select money from balance where id = 1;
- 窗口2开始新事务修改money
-- 窗口2
begin;
update balance set money = money - 1000;
select money from balance where id = 1;
- 回到窗口1,再读money
-- 窗口1
select money from balance where id = 1;
-
哈哈,这回窗口1中的money没被修改了吧。
-
然后将窗口2的修改提交。
-- 窗口2
commit;
select money from balance where id = 1;
- 回到窗口1再读money,可以读到已提交的money了。
-- 窗口1
select money from balance where id = 1;
commit;
- 窗口1的事务可以读到窗口2的已提交的事务,这就叫 读取已提交 。
3#可重复读
- 所以如果要再提高隔离性,那是怎么样呢?那就是窗口2的事务就算提交了数据修改,我窗口1的事务也不管,还是读取到原来的数据。
-- 窗口1
-- 设置隔离级别为repeatable read
set session transaction isolation level repeatable read;
begin;
select money from balance where id = 1;
-- 窗口2
-- 修改money并提交
begin;
update balance set money = money - 1000;
select money from balance where id = 1;
commit;
-- 窗口1
-- 窗口1的事务还是视而不见。
select money from balance where id = 1;
- 窗口2的事务已经提交了,数据库的数据money已经真正被修改了,可是窗口1的事务还是视而不见,仍然隔离了,读取的数值仍然不变,重复读的数值不会变,这就是 可重复读 。
4#串行化
- 难道还有更变态的更强的隔离级别,答案是肯定的,那就是 串行化 。串行化是怎么再增强隔离性的呢?回到一开始文章中对 可重复读 和 串行化 的解释。
-
可重复读:
每个事务有自己的『世界』,除了一种情况。如果一个事务成功执行并且添加了新数据,这些数据对其他正在执行的事务是可见的。但是如果事务成功修改了一条数据,修改结果对正在运行的事务不可见。所以,事务之间只是在新数据方面突破了隔离,对已存在的数据仍旧隔离。
-
串行化:
最高级别的隔离。两个同时发生的事务100%隔离,每个事务有自己的『世界』。
-
也就是说,如果是在可重复读的情况下,插入新数据这个事情是没有被隔离的,但是在串行化的情况下,插入新数据也被隔离了。好吧,还是很抽象,还是实验最好。
-- 窗口1
-- 先是继续用可重复读的隔离级别。
begin;
select count(*) from balance;
好的,表里有579条数据。然后我们在窗口2的事务中插入新数据。
-- 窗口2
-- 在窗口2的事务中插入新数据。
begin;
insert into balance (money) values (999);
commit;
select count(*) from balance;
好了,数据表中的记录数已经达到了580条。那么回到窗口1,按照可重复读的定义,应该是580条记录,然后串行化的设置才是579条记录。可是!
- 真是万万没想到啊,这不是打自己脸吗,啪啪响。上网搜了一下,找到这么一句话:
REPEATABLE READ:在mysql中,不会出现幻读。mysql的实现和标准定义的RR隔离级别有差别。
好吧有兴趣的同学可以看看这位大神的详细解释:MySQL_REPEATABLE-READ事务隔离级别 && 幻读。