数据库事务隔离级别的实现-锁机制

来源:互联网 发布:淘宝宝贝详情怎么设置 编辑:程序博客网 时间:2024/05/01 04:25

一 数据库事务处理中可能的异状 

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

1丢失更新  

如果两个事务都要更新数据库同一个记录X,x=100

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

 

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


2脏读(未提交读) 

一个事务读到另一个事务还没有提交的数据。 如:

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

       

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

3不可重复读

一个事务在自己没有更新数据库数据的情况,同一个查询操作执行两次或多次的结果不一致,即不可重复读。原因是其他事务对该记录做了修改。如:

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

 

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

4幻读(Phantom Read)

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

二 事务隔离级别实现机制-锁

1 数据库锁

封锁是实现并发控制的一个非常重要的技术。所谓封锁就是事务T在对某个数据对象例如表、记录等操作(读取、修改)之前,先向系统发出请求,对其加锁。加锁后事务T就对该数据对象有了一定的控制,在事务T释放它的锁之前,其它的事务不能修改此数据对象。 基本的封锁类型有两种:排它锁(Exclusive locks 简记为X锁)和共享锁(Share locks 简记为S锁)。
      排它锁又称为写锁。若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其它事务在T释放A上的锁之前不能再读取和修改A。 
      共享锁又称为读锁。若事务T对数据对象A加上S锁,则其它事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其它事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
 

 

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


2 封锁协议

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

(1)一级封锁协议 (对应隔离级别:read uncommited)   

     一级封锁协议:

1)事务对当前被读取的数据(读取的瞬间,或者是读取前一瞬间)共享锁,一旦读完该行,立即 释放该行的共享锁从数据库的底层实现更深入的来理解,既是,数据库会对游标当前的数据上加共享锁 , 但是当游标离开当前行的时候,立即释放该行的共享锁,由此导致不可重复读)。  

2)事务对需要修改的数据(修改的瞬间,或者是说修改前一瞬间) 对其加共享锁(加锁后其他事务不能更改,但是可以读取,由此导致“脏读”),直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。

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

(2)二级封锁协议 (对应隔离级别:read commited) 

     二级封锁协议:

1)事务对当前被读取的数据(读取的瞬间,或者是读取前一瞬间)共享锁,一旦读完该行,立即 释放该行的共享锁。  

2)事务对需要修改的数据(修改的瞬间,或者是说修改前一瞬间) 对其加排他锁,直到事务结束才释放,防止其他事务读取未提交的数据,这样,也就避免了 “脏读” 的情况。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。

     二级封锁协议除防止了“脏读”数据,但是不能避免 丢失更新,不可重复读,幻读 。

     但在二级封锁协议中,由于读完数据后立即 释放共享锁,所以它不能避免不可重复读 。同时它也不能避免 丢失更新 :如果事务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 和mysql中,需要加 SELECT …… FOR UPDATE 语句,凡是该事务读到的数据,都被该事务占据了排他锁,直到事务结束,防止其他事务在本事务结束前获取相同数据,这样就防止了 丢失更新 ! 

(3)三级封锁协议 (对应隔离级别:repeatable read)

      三级封锁协议:

1)事务对当前被读取的数据(读取的瞬间,或者是读取前一瞬间)共享锁,直到事务结束才释放 ,这样保证了可重复读(既其他的事务能读取该数据,但是不能修改该数据)。  

2)事务对需要修改的数据(修改的瞬间,或者是说修改前一瞬间) 对其加排他锁,直到事务结束才释放,防止其他事务读取未提交的数据,这样,也就避免了 “脏读” 的情况。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。

      三级封锁协议除防止了“脏”数据 和不可重复读 。但是这种情况不能避免 幻读 和 丢失更新 的情况。在事务 A 没有完成之前,事务 B 可以新增数据,那么 当事务 A 再次读取的时候,事务B 新增的数据会被读取到,这样,在该封锁协议下,幻读 就产生了。 如果事务A 和 事务B 同时读取了资源X=100,同样,如果事务A先对X进行 更新X=X+100,等待事务A执行完成X=200,那么事务B 获得X的排他锁,进行更新 X=X+200,然后提交 X=300,同样A的更新被B所覆盖!

如果要避免 丢失更新,我们可以指定对要读取的数据上排他锁直到事务结束,这个往往需要程序员自己编程实现,比如在Oracle 和mysql中,需要加 SELECT …… FOR UPDATE 语句,凡是该事务读到的数据,都被该事务占据了排他锁,直到事务结束,防止其他事务在本事务结束前获取相同数据,这样就防止了 丢失更新 !

      进阶: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 锁可以顺利升级为 排他锁对该数据进行修改!)

 

(4)最强封锁协议(对应隔离级别:Serialization)

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

 

三 附Oracle 事务一致性原则

        事务是定义和维护一致性的单位,封锁就是要保证这种一致性。如果   
对封锁的要求高会增加开销,降低并发性和效率;有的事务并不严格要求   
结果的质量(如用于统计的事务),如果加上严格的封锁则是不必要和不   
经济的。因此有必要进行进一步的分析,考察不同级别的一致性对数据库   
数据的质量及并行能力的影响。   
        一致性级别定义为如下的几个条件:   
    (1)   事务不修改其它任何事务的脏数据。脏数据是被其它事务修改过,   
但尚未提交的数据。   
    (2)   在事务结束前不对被修改的资源解锁。   
    (3)   事务不读其它任何事务的脏数据。   
    (4)   在读前对数据加共享锁(RS)和行排它锁,直至事务结束。   
            *   满足条件1的事务叫第0级事务。   
            *   满足条件1和2的事务叫第1级一致性事务。   
            *   满足条件1、2和3的事务为2级一致性事务。ORACLE的读一致性保   
证了事务不读其它事务的脏数据。   
            *   满足条件1、2、3和4的事务叫第3级一致性事务。   
        由ORACLE的三个性质:自动加隐式锁、在事务结束时释放锁和读一致   
性,使ORACLE成为自动满足以上的0、1和2级一致性事务。因此,ORACLE   
自动防止了脏读(写-读依赖)。但是,ORACLE不能自动防止丢失修改(写   
-写依赖),读的不可重复性(读-写依赖),彻底解决并发性中的问题还   
需满足第4个条件(3级一致性事务),这需要程序员根据实际情况编程。   
方法如下: 
        *   如果想在一段时间内使一些数据不被其它事务改变,且在本事务内   
            仅仅查询数据,则可用SET   TRANSACTION   READ   ONLY   语句达到这一   
            目的。   
        *   如果想在一事务内修改一数据,且避免丢失修改,则应在读这一数   
            据前用SELECT   FOR   UPDATE对该数据加锁。   
        *   如果想在一事务内读一数据,且想基于这一数据对其它数据修改,   
            则应在读数据前对此数据用SELECT   FOR   UPDATE加锁。对此种类型   
            的应用,用这条SQL语句加锁最恰当。   
        *   如果想避免不可重复读现象,可在读前用SELECT   FOR   UPDATE对数   
            据加锁,或用SET   TRANSACTION   READ   ONLY设置只读事务。  


四 特殊情况 

1) Read-Commit 的行锁导致其他事务一直被 hanging on的情况!

假设我们有 VARIANT 表, Trasaction A 对 Variant 中的字段 VariantName 1 进行了修改,但是事务未提交(假设,该事务将执行1个小时),此时 Trasaction B 要读VariantName(查询某一个VariantName 2 ),此时它会一直被Transaction A 阻塞,直到Transaction A 提交对 VariantName 的修改后,Transaction B才会得到VariantName 2 的查询结果,这样Transaction B最长可被阻塞1个小时!

这里,虽然Transaction A是针对 VariantName 1 上的修改,而 Transaction B 是读取 VariantName 2 , 对应的Variant Name不一样,但是此时,Transaction B并不知道 Transaction A 的结果(对Transaction B而言,它不清楚Transaction A提交的结果是什么),为了避免“脏读”,Transaction B会等待 Transaction A执行完事务以后,完成它对VariantName的修改后,才返回结果!

 

所以,在一个事务中,我们应该尽量把 SELECT Queries 放到最前面,把所有的 Update 放到最后面,避免不必要的等待!

 

特别的,如果上面这种情况,VariantName是Unique Index或者是Primary Key, 这个时候,Transaction B不会被Transaction A 阻塞!因为 Transaction B 知道 Transaction A提交的更改不会影响 他获取的VariantName 2 因为Transaction B 知道 VariantName 2 是唯一的!

0 0
原创粉丝点击