今天酷壳上发布了一篇网友投稿的讨论MVCC的文章。写得很浅显,很明白。MVCC是每个接触数据库尤其是分布式互联网应用的开发人员应知应会的内容,而架构师更应该知道如何在悲观锁和乐观锁之间进行平衡与选择,这里不做展开,只想补充以下内容,来自于之前和现在的项目经验:

乐观锁在web应用还有一种应用场景就是在前端页面事务无法控制到的位置通过version检查避免脏数据的覆盖操作。比如在悲观锁环境下,当多个用户在各自的浏览器上修改同一份数据的不同域时,由于事务延伸不到客户的浏览器上,因此当他们提交时,服务器、数据库会认为是多份独立的事务提交,将相继全部成功,最终导致最后提交的有效,前几次提交的数据都被最后一次提交的数据覆盖,形象一点:

数据库原始数据

+----+------+--------+-----+--------+
| id | name | gender | age | height |
+----+------+--------+-----+--------+
| 1  | Jay  | male   | 29  | 1.88   |
+----+------+--------+-----+--------+

此时三个用户都打开了“编辑用户信息”页面,拿到了相同的数据,但是

  • 甲将性别改成“unknown” 8-O
  • 乙将年龄改成28
  • 丙将身高改成1.85

假设他们依次提交,最终的结果将是

+----+------+--------+-----+--------+
| id | name | gender | age | height |
+----+------+--------+-----+--------+
| 1  | Jay  | male   | 29  | 1.85   |
+----+------+--------+-----+--------+

丙的提交生效,其他人的修改被脏数据覆盖

如果使用乐观锁version机制,情况会有很大不同

数据库原始数据

+----+------+--------+-----+--------+---------+
| id | name | gender | age | height | version |
+----+------+--------+-----+--------+---------+
| 1  | Jay  | male   | 29  | 1.88   | 1       |
+----+------+--------+-----+--------+---------+

还是按照上面的场景修改数据,还是按照上面的顺序提交,服务器会将version字段返回至客户端,客户端提交时也会带上version信息:

  • 甲提交,数据更新为
    +----+------+--------+-----+--------+---------+
    | id | name | gender | age | height | version |
    +----+------+--------+-----+--------+---------+
    | 1  | Jay  | unknown| 29  | 1.88   | 2       |
    +----+------+--------+-----+--------+---------+
  • 乙提交,由于version已经更新为2,数据库认为有冲突(其实MVCC的version和传统的VCS是一样的,会有冲突发生),更新失败,抛异常,服务器提示“数据已被更新,请刷新后重试”,乙刷新获得甲更新后的数据,修改年龄为28,提交,数据更新为
    +----+------+--------+-----+--------+---------+
    | id | name | gender | age | height | version |
    +----+------+--------+-----+--------+---------+
    | 1  | Jay  | unknown| 28  | 1.88   | 3       |
    +----+------+--------+-----+--------+---------+
  • 丙提交,和乙一样,被告知“数据已被更新,请刷新后重试”,刷新,更新,提交,数据更新为
    +----+------+--------+-----+--------+---------+
    | id | name | gender | age | height | version |
    +----+------+--------+-----+--------+---------+
    | 1  | Jay  | unknown| 28  | 1.85   | 4       |
    +----+------+--------+-----+--------+---------+

这应该是大家所预期的结果。而如果使用悲观锁要解决这个问题,只能在服务器端做额外的处理,辨识此次更新的字段,然后更新前查询一次数据库,获得最新的数据,仅更新发生变化的字段,然后提交。或者,页面就应该避免多字段大表单的提交,把每次可更新的内容进行拆分,比如现在几乎所有的SNS的用户信息更新页面都会按“基本信息”、“学校信息”、“就业信息”等分段保存(当然这么做的原因有很多,比如用户体验,对于一个长长的大表格,用户更能接受“少量多次”的提交方式,而且如果在保存前出现浏览器崩溃、死机等意外情况,未保存而需要重填的数据量不会很大。脏数据覆盖问题只是其中一个)

最后补充两点实战经验

  • 如果需要在Hibernate中使用MVCC,直接在entity中定义一个int类型的字段,然后使用@Version修饰该字段
  • 在真实环境中,若使用MVCC并且允许用户重复更新,每次页面提交后,应该将数据库最新的version值传回客户端。如果使用REST,直接放在response的header里是一种可行的做法