数据库事务隔离级别与锁机制实现(转载)

写在前面

  数据库并发处理是一个非常重要的问题,其实现本质上同应用程序一级的并发控制原理是一致的,也是通过加锁解决。而事务隔离级别相当于一段独立的代码块,具有原子性、一致性、隔离性、持久性4个特征。本来想自己写一篇总结,但看到有一篇文章总结得非常好,就直接转载过来。

  以下内容转载自:宋杰的站点 » 【转载】数据库事务隔离级别和锁实现机制

  原文的版式我修改了一下,有些跟核心内容无关的我删去了,某一两处内容我觉得不对的在后面加了备注。

数据库事务处理中出现的数据不一致的情况

在多个事务并发做数据库操作的时候,如果没有有效的避免机制,就会出现种种问题。大体上有四种问题,归结如下:

丢失更新

如果两个事务都要更新数据库一个字段x,x=100

事务A 事务B
读取X=100 读取X=100
写入x=X+100 写入x=X+200
事务结束x=200 事务结束x=300
最后x=300

最后x=300

两个不同事务同时获得相同数据,然后在各自事务中同时修改了该数据,那么先提交的事务更新会被后提交事务的更新给覆盖掉,这种情况事务A的更新就被覆盖掉了、丢失了。

脏读(未提交读)

防止一个事务读到另一个事务还没有提交的记录。 如:

事务A 事务B
写入x=X+100(x=200)
读取X=200(读取了事务B未提交的数据)
事务回滚x=100
事务结束x=100
事务结束

事务读取了未提交的数据,事务B的回滚,导致了事务A的数据不一致,导致了事务A的脏读 !

不可重复读

一个事务在自己没有更新数据库数据的情况,同一个查询操作执行两次或多次的结果应该是一致的;如果不一致,就说明为不可重复读。

还是用上面的例子

事务A 事务B
读取X=100 读取X=100
读取X=100 写入x=X+100
事务结束,x=200
读取X=200(此时,在同一个事务A中,读取的X值发生了变化!)
事务结束

这种情况事务A多次读取x的结果出现了不一致,即为不可重复读 。

幻读(Phantom Read)

事务A读的时候读出了15条记录,事务B在事务A执行的过程中 增加 了1条,事务A再读的时候就变成了 16 条,这种情况就叫做幻影读。

不可重复读说明了做数据库读操作的时候可能会出现的问题。

事务隔离级别通过锁的实现机制

两个锁:

  1. 排他锁 被加锁的对象只能被持有锁的事务读取和修改,其他事务无法在该对象上加其他锁,也不能读取和修改该对象
  2. 共享锁 被加锁的对象可以被持锁事务读取,但是不能被修改,其他事务也可以在上面再加共享锁。

特别的,对共享锁: 如果两个事务对同一个资源上了共享锁,事务A 想更新该数据,那么它必须等待 事务B 释放其共享锁。

在运用 排他锁 和 共享锁 对数据对象加锁时,还需要约定一些规则,例如何时申请 排他锁 或 共享锁、持锁时间、何时释放等。称这些规则为封锁协议(Locking Protocol)。对封锁方式规定不同的规则,就形成了各种不同的封锁协议。

封锁协议

一级封锁协议 (对应 read uncommited)

一级封锁协议是:事务 在对需要修改的数据上面(就是在发生修改的瞬间) 对其加共享锁(其他事务不能更改,但是可以读取-导致“脏读”),直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。

一级封锁协议不能避免 丢失更新,脏读,不可重复读,幻读!

二级封锁协议 (对应read commited)

二级封锁协议是:1)事务 在对需要更新的数据 上(就是发生更新的瞬间) 加 排他锁 (直到事务结束) , 防止其他事务读取未提交的数据,这样,也就避免了 “脏读” 的情况。2)事务 对当前被读取的数据 上面加共享锁(当读到时加上共享锁),一旦读完该行,立即 释放该 该行的共享锁 – 从数据库的底层实现更深入的来理解,既是,数据库会对游标当前的数据上加共享锁 , 但是当游标离开当前行的时候,立即释放该行的共享锁。
二级封锁协议除防止了“脏读”数据,但是不能避免 丢失更新,不可重复读,幻读 。

但在二级封锁协议中,由于读完数据后立即 释放共享锁,所以它不能避免可重复读 ,同时它也不能避免 丢失更新 ,如果事务A、B同时获取资源X,然后事务A先发起更新记录X,那么 事务B 将等待事务 A 执行完成,然后获得记录X 的排他锁,进行更改。这样事务 A 的更新将会被丢失。 具体情况如下:

事务A 事务B
读取X=100(同时上共享锁) 读取X=100(同时上共享锁)
读取成功(释放共享锁) 读取成功(释放共享锁)
UPDATE X=X+100 (上排他锁)
UPDATING A(等待事务A释放对X的排他锁)
事务成功(释放排他锁)X=200
UPDATE X=X+200(成功上排他锁)
事务成功(释放排他锁)X=300

由此可以看到,事务A的提交被事务B覆盖了,所以不能防止 丢失更新。

如果要避免 丢失更新,我们需要额外的操作, 对凡是读到的数据加 共享锁 和排他锁 ,这个往往需要程序员自己编程实现,比如在Oracle 中,需要加 SELECT FOR UPDATE 语句,表明,凡是该事务读到的数据,额外的加上排他锁,防止其他数据同一时间获取相同数据,这样就防止了 丢失更新 !

三级封锁协议 (对应reapetable read )

三级封锁协议是:二级封锁协议加上事务 在读取数据的瞬间 必须先对其加 共享锁 ,但是 直到事务结束才释放 ,这样保证了可重复读(既是其他的事务职能读取该数据,但是不能更新该数据)。

三级封锁协议除防止了“脏”数据,不可重复读和丢失更新。但是这种情况不能避免 幻读。由于该封锁协议只是对某行数据加锁,因此在事务 A 没有完成之前,事务 B 可以新增数据(因为不是对锁住的这一行操作),那么 当事务 A 再次读取的时候,事务B 新增的数据会被读取到,这样,在该封锁协议下,幻读 就产生了。

注:原文说该封锁协议不能避免丢失更新,但我认为是可以避免的。因为无论共享锁还是排它锁,在该事务隔离等级下,其持有时间都是整个事务周期,所以无论读还是写某一行记录都是独占的,不可能存在事务A和B并发读取数据的情况,同时只有一个事务能读取并修改数据。因此应该不会造成丢失更新。

进阶:repeatable read 导致死锁的情况(即便是 不同的资源在相同的顺序下获取)。 比如 事务1 读取 A,同时 事务2 也读取 A,那么事务1和事务2 同时对 A 上了共享锁,然后事务1 要UPDATE A,而此时 事务2 也要 UPDATE A,这个时候 事务1 等待 事务2 释放其在 A 上的共享锁,然后 事务2 要等待 事务1 释放其在 A 上的共享锁,这样,事务1 和 事务2 相互等待,产生死锁!(SQL Server/DB2 里面有 UPDATE LOCK 可以解决这种情况,具体的思路是,在 repeatable read 的情况下,将读取的数据 上的 UPDATE 锁,介于 共享锁 和 排他锁之间的一种锁,该锁的作用是 当出现上面这种情况后,事务1 和 事务2 对 A 上的是 UPDATE 锁,那么谁先 要修改 A,那么该事务就会将 UPDATE 锁可以顺利升级为 排他锁对该数据进行修改!)

最强封锁协议(对应Serialization)

四级封锁协议是对三级封锁协议的增强,其实现机制也最为简单,直接对 事务中 所 读取 或者 更改的数据所在的表加表锁,也就是说,其他事务不能 读写 该表中的任何数据。这样所有的 脏读,不可重复读,幻读 ,都得以避免!