乐观锁与悲观锁各自适用场景是什么?

2015-04-09 14:08:48 +08:00
 ajianrelease
悲观锁貌似没法解决更新丢失的问题。见下面的例子,两个用户张三,李四,他们两人可以更新同一条数据库记录。假设记录为(sex,age) = (‘male’, 25)。在张三的查询和修改的时间间隔内,李四更新了记录,而张三对这种情况不知情,最后导致李四的更新丢失了。无论加不加悲观锁,都解决不了这种问题。我的问题是

1)对于这种并发写冲突,是不是只能用乐观锁(给表加一个版本号字段)来防止更新丢失?
2)那select ... for update这种悲观锁在什么场景下使用,悲观锁的使用应该是为了解决并发写冲突,但貌似它又不能解决更新丢失问题,感觉有点鸡肋啊,亦或是我理解有误.

有一篇相关文章,参见http://www.douban.com/note/204830640/

12518 次点击
所在节点    MySQL
19 条回复
cloudhunter
2015-04-09 14:55:54 +08:00
1)可以看看事务隔离级别的定义
2)select ... for update,我们用它主要为了保证业务的强一致性,对某一行的记录加锁,然后让分布式的更新或读取变成串行。
bsbgong
2015-04-09 15:03:54 +08:00
两种锁都是为了解决并发情况下的写冲突。
用那种机制,取决于你的场景。要记住,总目标和原则都是:提高写效率。
悲观锁是early lock,乐观锁是late lock。因此:
1. 对于数据更新频繁的场合,悲观锁效率更高
2. 对于数据更新不频繁的场合,乐观锁效率更高
caoyue
2015-04-09 15:25:35 +08:00
我的理解是,数据库只能保证操作的结果正确而不能替你做业务逻辑上的「正确」
题目所说的场景可以将读和写作为一个事务,同时选择合适的隔离级别。
当然更高的隔离级别也意味着更低的并发
如楼上所说,两种方式开销都不小,具体怎么实现取决于楼主的场景
ajianrelease
2015-04-09 15:57:51 +08:00
@cloudhunter
@bsbgong
@caoyue
上面的场景怎么将读和写做为一个事务呢?貌似没法做到吧。
第一步:用户A先调用服务端的读api,服务端读后就返回给了用户,即使读的时候加锁,那返回给用户响应时也已经把锁释放掉了。
第二步:用户A收到响应后,编辑数据,然后再调用服务端的写api,完成更新。
如果在这两步之间,用户B更新了数据,那用户A执行第二步后,B的更新就丢掉了。
以知乎为例,知乎上的问题是所有人都能编辑的,如果有两个用户像上面这么操作,那用户B的编辑就被干掉了
200cc
2015-04-09 16:36:09 +08:00
这个业务的目的是什么?
(1) 只保留A的修改
(2) 只保留B的修改
(3) A可以覆盖B的修改. 但在日志里面能体现A,B的修改记录.
caoyue
2015-04-09 16:50:50 +08:00
@ajianrelease
对数据库了解不多,个人感觉哈 :)
1. 一般场景下竞争冲突的时候不多。除非完全不可接受的情况,使用乐观锁比较常见。
2. 有些 ORM 是内建乐观锁支持的
3. 这种情况下要使用悲观锁,可以在更新前做一次 select for update
4. 互联网应用有时候对数据一致性的要求没有那么高
5. 有些场景会用到多级缓存缓解这些问题
6. 如果是数据正确性很重要比如金融业务之类,加上操作日志是更保险的做法
ajianrelease
2015-04-09 17:06:32 +08:00
@200cc 目的是他们两个的更改都要保留。即最后结果希望是(‘female’, 30),也就是说A在更新时要提示他其它用户已经更改过了,所以需要乐观锁。
bsbgong
2015-04-09 18:32:36 +08:00
@ajianrelease
乐观锁是假定读取的数据,在写之前不会被更新。适用于数据更新不频繁的场景。
Dynamodb支持乐观锁。

悲观锁也是类似,mysql支持悲观锁。
当你执行select xx from xx where xx for update后,在另一个事务中如果对同一张表再次执行select xx from xx where xx for update,那么第二个事务会一直等到第一个事务结束才会被触发,也就是一直处于阻塞的状态,无法查询。可以看到在数据更新不频繁的时候,悲观锁效率很低。

相反,当数据更新频繁的时候,乐观锁的效率很低,因为基本上每次写的时候都要重复读写两次以上。

根据你的描述,应该使用乐观锁(加一个版本号字段)。
A更新成功之后,ver++
B在尝试更新的时候,发现欲更新的记录的的ver跟数据库对应记录的ver不一致。于是重新读取该记录,也就是A更新之后的记录。
至于重新读取之后是怎样提示用户,就是你UX的设计问题了,跟数据库这边无关。
A的更新是保存到了数据库的。B要再更新,必须基于A的更新之上。
ryd994
2015-04-09 19:28:51 +08:00
@ajianrelease 关于4楼,我的理解是:
如果加悲观锁的话,张三读完之后就不释放锁,直到写入完成再释放。也就是说后来的李四从第一步就不能开始。其实也能解决问题,但是性能就很悲剧了。
ajianrelease
2015-04-09 20:25:23 +08:00
@bsbgong 多谢,回答的很详细。既然悲观锁在数据更新方面不如乐观锁效率高,那全用乐观锁就行了呗,悲观锁还有什么用呢?能否举出一个使用悲观锁而不使用乐观锁的例子啊
ajianrelease
2015-04-09 20:27:04 +08:00
@ryd994 恩,你说的这种方法貌似不适用我举的这个例子。因为张三读完后,服务端要给他返回数据的,返回数据时,事务已经结束了,锁不可能还在啊。
clino
2015-04-09 20:40:04 +08:00
@bsbgong "对于数据更新频繁的场合,悲观锁效率更高" 这个不理解,感觉应该是也是乐观锁效率更高吧?
ryd994
2015-04-09 21:42:33 +08:00
@ajianrelease 用两种命令,一种是读,一种是读加锁。没人规定返回数据了服务器程序就要结束。用fcgi的话完全可以自己做一个锁池解决。当然,这样蛮考验性能的。
mfaner
2015-04-10 11:12:12 +08:00
客户端只提交数据变化应该就能用悲观锁了吧,比如转帐。
你这种情况悲观锁不是没锁住吗,除非事务边界延伸到客户端...好吧其实我什么都不懂。
snnn
2015-04-10 13:05:56 +08:00
悲观锁是提前加锁,楼主是不是搞反了啊?

并发控制协议需要关注的3个问题:

冲突何时发生?
冲突如何被检测到? 
冲突如何解决?
并发控制协议总的来说,可以分为乐观和悲观两种。

两种版本管理模式:

1、eager version management:直写型。必须要有undo log。

2、lazy version management:延迟更新。所有的修改在提交前都是针对当前事务私有的。

加锁的时机:

ETL:encounter-time locking。事务第一次访问这个地址时加锁。

CTL:commit-time locking。事务提交时加锁。

ETL可以支持eager version management和lazy version management。

CTL只能支持lazy version management。不能支持eager version management是很显然的,因为如果直写,但是事务提交时才加锁,那就乱套了。

我们平时所说的悲观锁是指encounter-time locking。无论是ETL还是CTL,都能用来实现可串行化的隔离度。
dingyaguang117
2015-04-10 17:55:28 +08:00
加锁不就是为了防止更新丢失的么, 为什么会防止不了呢
悲观锁:从一开始都不让其他事务读
乐观锁:只要有人在我读之后改过,我就放弃了
这两种都能防止呀
ajianrelease
2015-04-11 01:02:23 +08:00
@ryd994 奥,可以这样啊,学习了。那这样感觉很不靠谱啊,如果用户读加锁,服务端返回数据后还保持锁,那用户不修改数据,那锁不就一直在那吗?现实中,你使用过锁池这种东西吗?或者你见面其它人用吗?
ajianrelease
2015-04-11 01:08:23 +08:00
@snnn 说实话,你说的对我来说有点高深了,我得好好理解一下
ryd994
2015-04-11 05:23:42 +08:00
@ajianrelease 我没用过,但是见过有人用
如果怕用户改一半跑了可以做超时
但是无论怎么样,在与人交互的情况下,用悲观锁的性能肯定是很悲观的

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/182569

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX