【Mysql】丢失更新

  • 第一类丢失更新:一个事务的回滚覆盖了另一个事务提交的update。
  • 第二类丢失更新:一个事务提交的update覆盖了另一个事务提交的update。
    1. 第一类丢失更新 

          A事务撤销时,把已经提交的B事务的更新数据覆盖了。这种错误可能造成很严重的问题,通过下面的账户取款转账就可以看出来:

      时间

      取款事务A

      转账事务B

      T1

      开始事务

       

      T2

       

      开始事务

      T3

      查询账户余额为1000元    

       

      T4

       

      查询账户余额为1000元

      T5

       

      汇入100元把余额改为1100元

      T6

       

      提交事务

      T7

      取出100元把余额改为900元

       

      T8

      撤销事务

       

      T9

      余额恢复为1000 元(丢失更新)

       

         

      A事务在撤销时,“不小心”将B事务已经转入账户的金额给抹去了。 

          第二类丢失更新 

      A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失:   

      时间

      转账事务A

      取款事务B

      T1

       

      开始事务

      T2

      开始事务

                               

      T3

                     

      查询账户余额为1000元    

      T4

      查询账户余额为1000元

                               

      T5

       

      取出100元把余额改为900元

      T6

       

      提交事务           

      T7

      汇入100元

       

      T8

      提交事务

       

      T9

      把余额改为1100 元(丢失更新)

       

        

          上面的例子里由于支票转账事务覆盖了取款事务对存款余额所做的更新,导致银行最后损失了100元,相反如果转账事务先提交,那么用户账户将损失100元。


    2. 例子如下
    mysql> select * from test1;
    +------+-------+
    | id   | score |
    +------+-------+
    |   -9 |    60 |
    +------+-------+
    1 row in set (0.00 sec)

    1. session1 session2
      begin;


      begin;
      select score into @score from test1 where id=-9;


      select score into @score from test1 where id=-9;
      update test1 set score=@score -10 where id=-9;

      commit;


      update test1 set score=@score +10 where id=-9;

      commit;

      mysql> select * from test1;
      +------+-------+
      | id   | score |
      +------+-------+
      |   -9 |    70 |
      +------+-------+

      mysql> select * from test1;
      +------+-------+
      | id   | score |
      +------+-------+
      |   -9 |    70 |
      +------+-------+
      1 row in set (0.00 sec)
      (丢失更新,为50被70覆盖了)
      字没用,为了对其表格       









  • 解决
    1. 悲观锁解决丢失更新的问题


      举个例子:

      begin;
      SET @a = (SELECT restnum FROM book WHERE id =1)
      SET @b = function(@a)      //抽象为某个业务逻辑操作 
      UPDATE  book SET restnum=@b WHERE  id =1 
      commit;
      类似于上面的代码套进事务中,其实在传统数据库的REPEATABLE_READ下是安全的;如果数据库使用了更新锁(U锁),会在第一个读操作上直接加排它锁,避免了丢失更新(否则会死锁)。但是现代数据库引擎都是基于MVCC实现的,即使数据被排它锁锁住,第一次读也不会被阻塞而是读到旧数据,到第二步写的时候就会写入旧数据计算出的结果。


      因此在REPEATABLE_READ级别下,我们需要手动加锁。正确的写法是:
      begin;    //注意要关闭auto commit
      SET @a = (SELECT restnum FROM book WHERE id =1 FOR UPDATE)
      SET @b = function(@a)      //抽象为某个业务逻辑操作 
      UPDATE  book SET restnum=@b WHERE  id =1 
      commit;
      如上,SELECT…FOR UPDATE 可以直接在数据上加排他锁(如果where指定了主键的话会锁行,否则会锁表),直到该事务结束(提交或回滚)。
      与此类似的语句还有SELECT … LOCK IN SHARE MODE是共享锁,也就是传统的S锁。显然,此处如果使用共享锁会发生典型的死锁。
      MySql的SERIALIZABLE隔离级别就是在REPEATABLE READ的基础上,把所有纯SELECT操作加上LOCK IN SHARE MODE。也就是把SELECT从快照读变成了当前读,因此不要天真的以为真的会“序列化”执行。



      乐观锁解决丢失更新的问题

      在实际使用中,我们其实很少使用悲观锁,因为阻塞可能会造成未知的性能问题。使用乐观锁,通常是通过版本号或者时间戳,在最后提交时进行比对:
      SELECT (restnum,version) FROM book WHERE id =1)
      ...... //业务逻辑操作 
      UPDATE  book SET restnum=@xx WHERE  id =1 AND version = @version
      当然,理想状态下,最好能直接用一个UPDATE语句实现,因为UPDATE会加排他锁(或者说是更新锁→排它锁),也就是不会出现上面的问题了。


请使用浏览器的分享功能分享到微信等