02.读已提交
Read Committed 读已提交
最基本的事务隔离级别是读已提交(Read Committed),它提供了两个保证:
- 从数据库读时,只能看到已提交的数据(没有脏读(dirty reads))。
- 写入数据库时,只会覆盖已经写入的数据(没有脏写(dirty writes))。
这是大多数数据库系统的默认隔离级别比如 Sql Server, Oracle 等,但不是 MySQL 默认的。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的 Commit,所以同一查询可能返回不同结果。
没有脏读
设想一个事务已经将一些数据写入数据库,但事务还没有提交或中止。另一个事务可以看到未提交的数据吗?如果是的话,那就叫做脏读(dirty reads)。在读已提交隔离级别运行的事务必须防止脏读。这意味着事务的任何写入操作只有在该事务提交时才能被其他人看到(然后所有的写入操作都会立即变得可见)。如下图所示用户 1 设置了 x = 3,但用户 2 的 get x 仍旧返回旧值 2,而用户 1 尚未提交。
为什么要防止脏读,有几个原因:
-
如果事务需要更新多个对象,脏读取意味着另一个事务可能会只看到一部分更新。例如用户看到新的未读电子邮件,但看不到更新的计数器。这就是电子邮件的脏读。看到处于部分更新状态的数据库会让用户感到困惑,并可能导致其他事务做出错误的决定。
-
如果事务中止,则所有写入操作都需要回滚。如果数据库允许脏读,那就意味着一个事务可能会看到稍后需要回滚的数据,即从未实际提交给数据库的数据。想想后果就让人头大。
没有脏写
如果两个事务同时尝试更新数据库中的相同对象,会发生什么情况?我们不知道写入的顺序是怎样的,但是我们通常认为后面的写入会覆盖前面的写入。但是,如果先前的写入是尚未提交事务的一部分,又会发生什么情况,后面的写入会覆盖一个尚未提交的值?这被称作脏写(dirty write)。在读已提交的隔离级别上运行的事务必须防止脏写,通常是延迟第二次写入,直到第一次写入事务提交或中止为止。
通过防止脏写,这个隔离级别避免了一些并发问题:
- 如果事务更新多个对象,脏写会导致不好的结果。譬如以一个二手车销售网站为例,Alice 和 Bob 两个人同时试图购买同一辆车。购买汽车需要两次数据库写入:网站上的商品列表需要更新,以反映买家的购买,销售发票需要发送给买家。销售是属于 Bob 的(因为他成功更新了商品列表),但发票却寄送给了爱丽丝(因为她成功更新了发票表)。读已提交会阻止这样这样的事故。
- 但是,提交读取并不能防止两个计数器增量之间的竞争状态。在这种情况下,第二次写入发生在第一个事务提交后,所以它不是一个脏写。这仍然是不正确的,但是出于不同的原因,在“防止更新丢失”中将讨论如何使这种计数器增量安全。
实现读已提交
读已提交是一个非常流行的隔离级别。这是 Oracle 11g,PostgreSQL,SQL Server 2012,MemSQL 和其他许多数据库的默认设置。最常见的情况是,数据库通过使用行锁(row-level lock)来防止脏写:当事务想要修改特定对象(行或文档)时,它必须首先获得该对象的锁。然后必须持有该锁直到事务被提交或中止。一次只有一个事务可持有任何给定对象的锁;如果另一个事务要写入同一个对象,则必须等到第一个事务提交或中止后,才能获取该锁并继续。这种锁定是读已提交模式(或更强的隔离级别)的数据库自动完成的。
如何防止脏读?一种选择是使用相同的锁,并要求任何想要读取对象的事务来简单地获取该锁,然后在读取之后立即再次释放该锁。这能确保不会读取进行时,对象不会在脏的状态,有未提交的值(因为在那段时间锁会被写入该对象的事务持有)。但是要求读锁的办法在实践中效果并不好。因为一个长时间运行的写入事务会迫使许多只读事务等到这个慢写入事务完成。这会损失只读事务的响应时间,并且不利于可操作性:因为等待锁,应用某个部分的迟缓可能由于连锁效应,导致其他部分出现问题。
出于这个原因,大多数数据库对于写入的每个对象,数据库都会记住旧的已提交值,和由当前持有写入锁的事务设置的新值当事务正在进行时,任何其他读取对象的事务都会拿到旧值只有当新值提交后,事务才会切换到读取新值。
不可重复读与幻读
在提交读级别中,数据库将保证如果一个事务没有完全执行成功(commit 完成),事务中的操作对其他的事务是不可见的。在该隔离级别下,虽然杜绝了脏读的发生,但是还是存在着不可重复读以及幻读的问题。不可重复读发生在事务 T1 读取了一行数据,事务 T2 接着修改或者删除了该行数据(已提交),当 T1 事务再次读取同一行数据的时候,发现数据已经被修改或者被删除。示例如下图:
-- 会话 1 中操作
start transaction;
select * from xxx where id = 1; -- 此时数据状态为 a
-- 注意次会话中开启事务,未提交
-- 切换至会话 2 操作
update xxx set xxx=newValue where id = 1; -- 更新数据至新的状态 b
-- 再次切换至会话 1 操作
select * from xxx where id = 1;
-- 此时查询出的数据状态有两种可能:新状态 b、老状态 a
-- 所谓的连续读:同一个事物中的两次读操作,数据状态保持一致
幻读则发生在事务 T1 读取了满足某条件的一个数据集,事务 T2 此时插入了一行或者多行满足 T1 查询条件的的数据并提交,当 T1 再次采用相同的条件进行读取时,得到了与第一次不同的结果集。示例如下:
-- 会话 1
start transaction;
select * from xxx;
-- 此时查询表为空,且事务未提交
-- 会话 2
start transaction;
insert into xxx values(1); -- 新增一条记录
commit;
-- 会话 1
select * from xxx;
-- 此时查询表仍为空,表示满足[可重复读]特性
update xxx set age=99 where id=1; -- 更新会话 2 中插入记录(此时会话 1 并不可见)
-- 更新 1 条记录
select * from xxx;
commit;