你的位置:首页 > 数据库

[数据库]并发事务的丢失更新及数据锁机制


    在事务的隔离级别内容中,能够了解到两个不同的事务在并发的时候可能会发生数据的影响。细心的话可以发现事务隔离级别章节中,脏读、不可重复读、幻读三个问题都是由事务A对数据进行修改、增加,事务B总是在做读操作。如果两事务都在对数据进行修改则会导致另外的问题:丢失更新。这是本博文所要叙述的主题,同时引出并发事务对数据修改的解决方案:锁机制。

1、丢失更新的定义及产生原因。

   丢失更新就是两个不同的事务(或者Java程序线程)在某一时刻对同一数据进行读取后,先后进行修改。导致第一次操作数据丢失。可以用图表表示如下:

时间点事务A                                 事务B                                             
1 start transcaction; 
2 start transcaction;
3update t_customer ts set ts.name='张三' where ts.id='10'; 
4 update t_customer t set t.name='李四',t.age=20 where ts.id='10'
5commit; 
6 commit;

 

 

 

 

 

 

   

     

 

       假如原来t_customer表内id为10的行,是一条{id:10,name:"王五",age:15} 的数据,经过事务A修改后变成{id:10,name:"张三",age:15}。事务B提交后,该数据变成了{id:10,name:"李四",age:20}。由事务A所执行的操作在事务B的提交后,数据被冲掉了。这个现象就叫做丢失更新。

     备注:事实上Mysql数据库会在事务里面默认添加写锁,上面的现象是没法重现的。了解即可。丢失更新就是由并发修改数据造成的。如下图所示:

    

 

2、如何避免丢失更新的发生。

  2.1 避免丢失更新有两种方法:

            (1) 不使用事务。[最不可能的方法]

            (2)使用数据库锁机制防止丢失更新 

 

  2.2 数据库的锁

          解决丢失更新的方法有好几个,先来了解下数据库里面的"锁"。从数据库功能上面来看,数据库设计上分为两种锁:读锁(共享锁)和写锁(排它锁)。

            数据库在设计这两种锁的时候,这两种锁间的关系如下:读锁与读锁可以共存,读锁与写锁互斥,写锁与写锁互斥。(这种设计跟Java线程锁机制是一样的)。

      使用数据库添加读锁和写锁的方法很简单 , 但需要注意的是锁必须在事务内进行声明。在事务外声明的锁将不具备效应。

      为表添加读锁的方法:select * from t_account lock in share mode; (读锁与他人共享读操作,很容易导致死锁。)

      为表添加写锁的方法:select * from t_account for update;

         

       在JDBC这块,事务一般通过sql脚本去控制处理。因此可以在sql脚本处添加上锁去进行控制。(后面会整理通过ORM框架:hibernate对事务进行控制)

  2.3 通过数据库的锁机制解决丢失更新

       解决丢失更新,主要使用两种方法:

             1. 使用排它锁。经过上面基于数据库锁的介绍可知,丢失更新可以使用写锁(排它锁)进行控制。因为排它锁添加到某个表的时候,事务未经提交,其他的事务根本没法获取修改权,因此排它锁可以用来控制丢失更新。需要说明的是有时候,当知道某一行会发生并发修改的时候,可以把锁定的范围缩小。例如使用select * from t_account t wheret.id='1' for update; 这样能够比较好地把控上锁的粒度,这种基于行级上锁的方法叫"行级锁"。

    2. 使用乐观锁。

           乐观锁的原理是:认为事务不一定会产生丢失更新,让事务进行并发修改,不对事务进行锁定。发现并发修改某行数据时,乐观锁抛出异常。让用户解决。可以通过给数据表添加自增的version字段或时间戳timestamp。进行数据修改时,数据库会检测version字段或者时间戳是否与原来的一致。若不一致,抛出异常。

  

时间点事务A事务Bversion值
   1
1提出修改 :update t_account t set t.money=t.money+100 where t.name ='a'提出修改 :update t_account t set t.money=t.money+100 where t.name ='a'1
2commit; 校验事务A与version值,version字段值都为"1",通过提交内容,version字段值更新为2
3 commit;

校验事务B与version值,事务B提交前的version字段值为1,但当前version值为2,禁止事务B提交.抛出异常让用户处理

 

 

 

 

 

 

 

    

       时间戳与version的判断都一样,时间戳记录的是当前时间。提交前数据库会校验提交前的时间戳是否与当前的一致。若一致则更新时间戳。

      以上为本次对数据库并发更新的总结,后面补充在ORM框架下如何上锁。