Isolation:隔离级别和脏读、脏写、不可重复读、更新丢失和幻读

隔离性

隔离性是指,多个用户的并发事务访问同一个数据库时,一个用户的事务不应该被其他用户的事务干扰,多个并发事务之间要相互隔离。

参考整理:https://zhuanlan.zhihu.com/p/69380112

隔离级别

  1. 读未提交(Read uncommitted):这种事务隔离级别下,select语句不加锁。此时,可能读取到不一致的数据,即“读脏 ”。这是并发最高,一致性最差的隔离级别。

  2. 读已提交(Read committed):从数据库读取时,只能看到已提交的数据(不脏读)。写入数据库时,只会覆盖已经提交的写入数据(不脏写)。可能会造成不可重复读。 大多数数据库的默认级别就是Read committed,比如Sql Server ,Oracle。

  3. 可重复读(Repeatable read):事务始终可以看到事务开始时数据库中的所有数据。即使这些数据随后被其他事务更改,这个事务也只能看到该特定时间点的旧数据。MySql默认隔离级别。可避免 脏读 、不可重复读 的发生。可能造成幻读。

  4. 串行化(Serializable ):就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待。可避免 脏读、不可重复读、幻读 的发生,但性能最低。

脏读、脏写

设想一个事务已经将一些数据写入数据库,但还没有提交。另一个事务可以看到未提交的数据吗?如果是的话,这就叫做脏读(Dirty Reads)。

如果两个事务尝试更新数据库中的相同数据,会发生什么情况?通常后面的写入会覆盖前面的写入。但是,如果先前的写入尚未提交,后面的写入又覆盖了这个尚未提交的值,会怎么样呢?这就叫做脏写

为了防止脏读,每次写入前,数据库都会记住旧值。 当前事务尚未提交时,其他事务的读取都会拿到旧值。当前事务提交后,其他事务才能读取到新值。

为了防止脏写,数据库一般用行锁。当事务想要修改特定的行时,必须先获得该行的锁。一次只有一个事务可持有任何给定行的锁。如果另一个事务要写入同一行,就必须等到第一个事务提交或回滚后。

使用for update进行上锁,比如:select * from table_name where id =1 for update ;

不可重复读(读偏差)

小明有两个账户,账户A和账户B,每个存有500元,一共1000元。现在发起一笔转账,从账户A转100元到账户B。

发起转账交易后,小明马上查询两个账户的余额。不幸的是,对账户B的查询发生在转账事务提交前(显示500元),对账户A的查询发生在转账事务提交后(显示400元)—— 似乎有100元不翼而飞!

这种异常叫做读偏差(也叫不可重复读,Non-Repeatable Reads)。尽管听上去有些吹毛求疵,因为这看起来不会长期持续。如果用户几秒钟后再次刷新页面,就会看到正确的帐户余额。但是有些情况下,仍不能容忍这种暂时的不一致,比如:

  1. 需要运行一个查询,扫描数据库中的所有账户。这个查询将会持续一段时间,在这个时间段又有多个转账事务被提交,那么我们始终无法读取到正确的数据。

  2. 备份操作需要复制整个数据库,这可能需要花费数小时才能完成。备份进行时,数据库依然接受写入操作。最终备份数据库中会包含一些旧的值和一些新的值。如果将来从这样的备份中恢复数据,那么不一致(比如消失的钱)就会变成永久的。

很明显,造成不可容忍的读偏差的原因是——只读操作持续的时间太长了,而这段时间又提交了新的写入。

简单来说不可重复读就是读的前后两次结果不一致

丢失更新

两个用户同时编辑wiki页面,每个用户通过读取整个页面、编辑内容、然后发送整个页面到服务器的方式来保存其更改,覆写数据库中当前的任何内容。

到目前为止的读已提交可重复读级别,可以保证只读事务在并发写入时可以看到什么,却忽略了两个事务并发写入的问题。如果两个写入事务同时执行,则其中一个的修改可能会丢失,因为第二个写入的内容并没有包括第一个事务的修改——这就是丢失更新(Lost Update)。

这是一个普遍的问题,目前的各种解决方案如下:

原子写

UPDATE counter SET value = value + 1 WHERE id = 'whatever';

for update显式锁定

BEGIN TRANSACTION;
# for update就是为这行数据加了锁,提交或回滚后释放
SELECT * FROM users WHERE id = 'Eddie' FOR UPDATE;
# 拿到数据后,应用程序做校验,然后...
UPDATE users SET money = '99999999' WHERE id = 'Eddie';
COMMIT;

比较并设置(CAS, Compare And Set)

UPDATE wiki_pages SET content = '新内容' WHERE id = '007' AND content = '旧内容';

根据数据库的实现,这可能也不安全

如果数据库允许WHERE子句从旧快照中读取,则此语句也无法保证防止丢失更新

在应用层,还有一些其他的解决办法,比如为并发写入创建多个冲突的版本,并使用应用代码或特殊数据结构解决或合并这些版本。或者直接以最后写入为准(这并没有解决丢失更新,而是允许丢失更新)。(git上解决version冲突)

幻读(写偏差)

幻读(Phantom Reads)就是读的时候发生了插入或删除,事务A根据相同条件第二次查询到事务B提交的新增数据,两次数据结果集不一致,好像出现了幻觉一样。幻读发生在范围查询中。

read commited级别下的事务在每次查询的开始都会生成一个独立的ReadView,所以才不可重复读,repeatable read级别在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView,所以可重复读,但ReadView只是针对查询操作,如果发生了插入或删除操作是会影响到查询操作,所以才出现了幻读。


   转载规则


《Isolation:隔离级别和脏读、脏写、不可重复读、更新丢失和幻读》 锦泉 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录