add by zhj: 本文主要谈的是乐观并发控制,虽然乐观并发控制不太适用于并发写冲突很频繁的场景下,因为这样会导致事务回滚,需要用户重试retry,
但是如果不用乐观并发控制的话,貌似也没有其它什么好的办法了,悲观锁并不能解决更新丢失的问题,比如本文中的例子,我们也可以想想Git
遇到这种情况时是怎么处理的,其实Git也会像本文一样处理。为什么说悲观锁也不能完全解决更新丢失的问题呢?我们看下面的例子,两个用户
张三,李四,他们两人可以更新同一条数据库记录,假设记录为(sex,age) = (‘male’, 25),下面的操作之后,李四的更新还是丢失了。这个例子其实
跟下面讲的甲乙丙的例子差不多,都是会导致更新丢失的。解决办法就是用乐观并发控制。
时间 |
用户张三 |
用户李四 |
T1 |
通过客户端查看记录 |
|
T2 |
服务端收到请求后,返回(‘male’, 25) |
|
T3 |
通过客户端查看记录 |
|
T4 |
服务端收到请求后,返回(‘male’, 25) |
|
T5 |
在客户端编辑记录为(‘male’, 30),并提交服务端 |
|
T6 |
服务端收到后更新成功 |
|
T7 |
在客户端编辑记录为(‘female’, 25),并提交服务端 |
|
T8 |
服务端收到后更新成功 |
|
T9 |
另,本文作者认为乐观并发控制与MVCC是一回事,在此,我个人的观点还是乐观并发控制完全无锁,而MVCC一般与锁机制结合使用,至少在MySQL中是这样的,当修改数据时,会加X锁。
原文:http://www.jayxu.com/2012/03/13/13326/
今天酷壳上发布了一篇网友投稿的讨论MVCC的文章。写得很浅显,很明白。MVCC是每个接触数据库尤其是分布式互联网应用的开发人员应知应会的内容,而架构师更应该知道如何在悲观锁和乐观锁之间进行平衡与选择,这里不做展开,只想补充以下内容,来自于之前和现在的项目经验:
乐观并发控制在web应用还有一种应用场景就是在前端页面事务无法控制到的位置通过version检查避免脏数据的覆盖操作。比如在悲观锁环境下,当多个用户在各自的浏览器上修改同一份数据的不同域时,由于事务延伸不到客户的浏览器上,因此当他们提交时,服务器、数据库会认为是多份独立的事务提交,将相继全部成功,最终导致最后提交的有效,前几次提交的数据都被最后一次提交的数据覆盖,形象一点:
数据库原始数据
+----+------+--------+-----+--------+
| id | name | gender | age | height |
+----+------+--------+-----+--------+
| 1 | Jay | male | 29 | 1.88 |
+----+------+--------+-----+--------+
此时三个用户都打开了“编辑用户信息”页面,拿到了相同的数据,但是
- 甲将性别改成“unknown”
- 乙将年龄改成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里是一种可行的做法