02. 读已提交
Read Committed 读已提交
最基本的事务隔离级别是读已提交(Read Committed
- 从数据库读时,只能看到已提交的数据(没有脏读(dirty reads
) ) 。 - 写入数据库时,只会覆盖已经写入的数据(没有脏写(dirty writes
) ) 。
这是大多数数据库系统的默认隔离级别比如
没有脏读
设想一个事务已经将一些数据写入数据库,但事务还没有提交或中止。另一个事务可以看到未提交的数据吗?如果是的话,那就叫做脏读(dirty reads
为什么要防止脏读,有几个原因:
-
如果事务需要更新多个对象,脏读取意味着另一个事务可能会只看到一部分更新。例如用户看到新的未读电子邮件,但看不到更新的计数器。这就是电子邮件的脏读。看到处于部分更新状态的数据库会让用户感到困惑,并可能导致其他事务做出错误的决定。
-
如果事务中止,则所有写入操作都需要回滚。如果数据库允许脏读,那就意味着一个事务可能会看到稍后需要回滚的数据,即从未实际提交给数据库的数据。想想后果就让人头大。
没有脏写
如果两个事务同时尝试更新数据库中的相同对象,会发生什么情况?我们不知道写入的顺序是怎样的,但是我们通常认为后面的写入会覆盖前面的写入。但是,如果先前的写入是尚未提交事务的一部分,又会发生什么情况,后面的写入会覆盖一个尚未提交的值?这被称作脏写(dirty write
通过防止脏写,这个隔离级别避免了一些并发问题:
- 如果事务更新多个对象,脏写会导致不好的结果。譬如以一个二手车销售网站为例,
Alice 和Bob 两个人同时试图购买同一辆车。购买汽车需要两次数据库写入:网站上的商品列表需要更新,以反映买家的购买,销售发票需要发送给买家。销售是属于Bob 的(因为他成功更新了商品列表) ,但发票却寄送给了爱丽丝(因为她成功更新了发票表) 。读已提交会阻止这样这样的事故。
- 但是,提交读取并不能防止两个计数器增量之间的竞争状态。在这种情况下,第二次写入发生在第一个事务提交后,所以它不是一个脏写。这仍然是不正确的,但是出于不同的原因,在“防止更新丢失”中将讨论如何使这种计数器增量安全。
实现读已提交
读已提交是一个非常流行的隔离级别。这是
如何防止脏读?一种选择是使用相同的锁,并要求任何想要读取对象的事务来简单地获取该锁,然后在读取之后立即再次释放该锁。这能确保不会读取进行时,对象不会在脏的状态,有未提交的值(因为在那段时间锁会被写入该对象的事务持有
出于这个原因,大多数数据库对于写入的每个对象,数据库都会记住旧的已提交值,和由当前持有写入锁的事务设置的新值当事务正在进行时,任何其他读取对象的事务都会拿到旧值只有当新值提交后,事务才会切换到读取新值。
不可重复读与幻读
在提交读级别中,数据库将保证如果一个事务没有完全执行成功(
-- 会话 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
-- 所谓的连续读:同一个事物中的两次读操作,数据状态保持一致
幻读则发生在事务
-- 会话 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;