谈谈乐观锁和悲观锁


什么是乐观什么是悲观

有一类人开车小心翼翼,从不超速、违章、酒驾,他总是很悲观,总担心会发生车祸,所以偶尔会发生车祸。另一类人麻痹大意,超速、酒驾各种胡来,他总是很悲观,总感觉我反正就这一次,我不可能运气这么差吧? 

然而结果是什么呢?悲观的司机小心使得万年船,虽然我开的慢,可能上班会迟到,但是我小心驶得万年船,我总能平安无事。乐观的司机总感觉坏事不会发生到自己头上,飙车的时候虽然很爽,但是一旦出事你就只有说拜拜了。

 

丢失更新

在将之前我先来介绍一个概念就是丢失更新,因为他是理解乐观锁和悲观锁的关键。

例如现在订单有3中状态,0未支付、1已支付、2已取消。用户下了一个订单之后,用手机和网页查询到订单状态都是未支付0。现在我们用手机支付这个订单,我们数据库会进行如下操作来修改订单状态:

update order
set type = 1
where oid = 001

不考虑任何锁,我们在支付的同时,又从网页上对订单进行了取消:

update order
set type = 2
where oid = 001

现在会造成一种什么情况?就是我用手机支付成功后,订单开始进入发货流程了,但是立马用网页对他进行了取消,因为我们手机和网页看到的订单状态都是0未支付。这就会造成你又给我发了货,然后我取消订单你又要给我退钱,这覆盖其他事务的更新操作,就是丢失更新。

悲观锁

悲观锁顾名思义,在处理问题的时候总是很悲观。每次修改数据的时候,总怕是担惊受怕,怕被别人修改了。所以就会对数据进行加锁,告诉其他人:我正在处理数据,你们都不许操作,等我操作完了你们再动!

传统的关系型数据库也是为我们提供了悲观锁的实现如 for update 。在一个事物中,我们对一个select语句最后加上for update(如果触发索引,是行级锁;否则就会锁表),之后其他任何人要对数据进行操作,都会被阻塞,直到我事物提交后释放锁。

以我们上面的修改订单为例:手机端进行支付操作,电脑端进行取消订单。进行支付和取消订单都会查询一下订单状态,这时候我们使用for update进行加锁。

select type
from order
where oid = 001 for update

这时候只要手机端或者电脑端进行了for update,另一端只有等待事物提交之后才能获取订单状态。这样就不会出现两端获取到的订单状态都是都是0未支付,而一端覆盖另一端的更新状态。

乐观锁

乐观锁恰好和悲观锁相反,悲观锁总是认为我修改数据的时候会发生丢失更新,犹如惊弓之鸡。乐观锁总感觉我每次修改数据的时候别人不会修改,所以并没有加如同悲观锁那么重的锁,只不过我更新的时候,检查一下这段时间有没有人去更新这个数据。

例如我们用svn服务器下载文件到本地,然后对他进行修改,然后提交svn。如果有很多个同时进行这个工作,按道理肯定会发生丢失更新吧。但是svn很巧妙的解决了这个问题,采用版本机制。你从svn下载一个文件,加入,这个文件当前的版本号是1,你在本地做了修改,然后提交。提交的时候,把你原始的版本号一起提交到svn,svn在修改的时候,首先对比一下你提交过来的版本号和当前服务器中该文件的版本号是否一致。如果不一致说明你在本地修改的时候,有其他人提交了修改,这时候驳回你的修改,给你冲突提示。如果一致进行替换,并将版本号+1,这样的机制有什么好处?我们来想下,由于我们采用的是版本号机制,这样我们在本地修改的时候,服务器中的数据并没有锁定,人和人都可以并发的修改,并增加版本号。

一些思考


但是我们要知道,数据库中实现乐观锁的数据版本,没你想的那么容易,刚才都只是在高谈阔论,没你想的那么容易,很多人会说,不就加一个版本号吗?看我的

update account 
set money = money-100, version = version + 1 
where aid = 001 and version = oldversion

然后返回受影响的行数进行判断,完全正确,没问题!但是如果再问一个复杂的,如果我要你查询余额,对余额大于10000的账户,增加100个积分.你查询了余额,修改的却不是余额,你该怎么设计。其实是一样的,修改积分表的时候,把余额的老本版号带过来,如果一致进行积分修改,否则不修改积分,对比新的余额版本号如下:

update jifenTable 
set jifen = jifen + 100 
where jid = 001 and exists(select 1 from account where aid = 001 and version = oldversion)

好了,看到了没,乐观锁有一个很大的问题 
1. 代码复杂,设计上有挑战
2. 对于客户端版本号不一致的冲突提示

而对于悲观锁,只需要一个`select for update`,简单直观,不容易出错,乐观锁设计不当,容易出错。

乐观锁还有一个更大的问题,那就是,有些时候,客户端允许容忍一定的系统响应慢,也不容忍操作半天,居然给我提示一个操作不成功。但是悲观锁总会成功,而乐观锁,存在失败的可能性,如果说改乐观锁的处理并发很大,采用了乐观锁,将会死一大片,可能80%的操作都会失败。假设我开了一个全国连锁的超市都用的一个收款码,有100个人同时对该账户进行付款,这一瞬间内,有80个人失败,20个人成功,我们的系统如同噩梦一样。

猜你喜欢

转载自blog.csdn.net/qq_25448409/article/details/82734548