小心:防止数据库数据写覆盖

  常常在项目中见到这样的DML语句:
  1. UPDATE table1 set column1=2 where id=12345
这么简单的语句,有什么稀奇的地方呢?我来模拟一个场景,用户A在页面上查询到这条ID=12345的记录,他看到column1的值是1,他想更新到2 。于是他点击了更新,同时用户B也看到了这条记录,他看到这条记录的column1的值也是1,他想更新到3,于是他也点击更新。这个时候问题来了,由于用户A心里想的是2,这个时候他更新完毕之后,查询出来看到的值却是3.这时估计用户A感觉灵异事件发生了...
  
  上面是一个很简单的场景,而且在项目中也经常遇到,这就是数据写覆盖问题。刚出来工作的同学最容易犯这个问题,我这里提供几个方法权当供大家参考:
  ①利用语句级SQL执行的原子性
    假设一个总额字段 total初始值是20,如果以后每次总额增加10元,我们就要更改这个字段。
    容易犯的错误:
    首先,
  1. select total from table1 where id=12345
     然后在程序中
  1. var temp=total+10
     最后一步
  1. UPDATE table1 set total=#temp# where id=12345
   解决方案:
  1. UPDATE table1 set total=total+10 where id=12345
   这里直接利用语句级的原子性,不用先查询再做加法。

 ②悲观锁:利用for update行锁(先查询再更新)
   同样是上面的例子。
   解决方案:
    首先,开启事务
  1. select total from table1 where id=12345 for update
    然后在程序中
  1. var temp=total+10
    之后更新
  1. UPDATE table1 set total=#temp# where id=12345
   最后,关闭事务。
   注意,这里几条基于一定要在事务中执行。

 ③乐观锁:更新时检查版本标志
    新增一个字段version,每次update都要对这个version加1,更新的时候检查这个version是否变化,如果已经变化了,表示被更新过了。
    首先
  1. select total,version,id from table1 where id=12345
   假设这里version=20,
   
然后在程序中
  1. var temp=total+10
    之后更新
  1. UPDATE table1 set total=#temp#,version=version+1 where id=12345 and version=20
   检查更新语句的返回值是否等于1,如果不等于1表示更新不成功,根据实际业务情况,可以直接反馈给用户,或者再做其他重试处理。


上面三种方法一般都有自己的使用场合:
  ①第一种方法,一般适合某个字段需要根据更新前的状态动态增加或者减少等做出变化的情况,这类需求处理起来无往不利。
  ② 第二种方法适用于用户更新一定要保证成功的情况,即更改及有效的情况,如果并发更新程序很高,一般采用这种方式比较妥当,可以减少更新失败的情况。
  ③第三种方法,不需要对数据进行事先锁定,更新只需要检查版本标志,即可知道是否已经被别人更新过了,适合于对更新成功要求不高的用户,即可以通过简单提示,然后让用户重新查询再修改的情况。如果并发程度很高,这种方法失败的几率也很高,所以一般不太适合。

  当然,这些都要根据实际情况来决定,一般系统中会综合运用上诉的几种方法来解决问题。总之一句话,写覆盖的问题大家一定要引起重视,不然出现了问题,你也只能按灵异事件处理了,因为并发问题,有的数据怎么来的都不知道。
   
  早先我也曾提到过这里的问题:《保持业务数据同步》

posted @ 2008-12-15 11:15  lovingprince  阅读(300)  评论(0编辑  收藏  举报