乐观锁与悲观锁的生动举栗讲解

一、并发控制

当程序中可能出现并发的情况时,就需要通过一定的手段来保证在并发情况下数据的准确性,通过这种手段保证了当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的。这种手段就叫做并发控制。并发控制的目的是保证一个用户的工作不会对另一个用户的工作产生不合理的影响。

没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题。

img

常说的并发控制,一般都和数据库管理系统(DBMS)有关。在DBMS中的并发控制的任务,是确保在多个事务同时存取数据库中同一数据时,不破坏事务的隔离性和统一性以及数据库的统一性。

实现并发控制的主要手段大致可以分为乐观并发控制和悲观并发控制两种。

首先要明确:无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。其实不仅仅是关系型数据库系统中有乐观锁和悲观锁的概念,像hibernate、tair、memcache等都有类似的概念。所以,不应该拿乐观锁、悲观锁和其他的数据库锁等进行对比。


二、悲观锁

​总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

举个栗子:

  1. 一间厕所在一个时间点内只能一个人使用(互斥锁),如果同时两个有“大事”要处理的人,他们一起冲到了厕所门口(当前厕所现在是没人的)
  2. 那个先冲进去厕所的人A,就会把门给关上(A上悲观锁:在查询数据成功后立马上锁)
  3. 上了锁之后,他才开始安心的处理他的大事(A更新数据)
  4. 而另一个人B,面对上锁的厕所,没有别的办法,也只能选择等待(B阻塞)
  5. 在A处理完之后,终于开锁了(A解除锁)
  6. 看到A出来,B立马就冲了进去,也上了锁,处理自己的大事(B查询数据,上锁,更新数据,解除锁)

上面的方法保证了每个人都能够公平竞争的使用厕所(谁先到谁就先处理数据),但是关门锁门的操作,让其他人只能等待某条数据处理完后,才能再去处理。这样就导致了数据处理的低效率

下面以淘宝下单过程中扣减库存的需求说明一下悲观锁的使用:
img
想要了解mysql的锁机制,可阅读这篇文章: 详解mysql的for update

而悲观锁还有一个极端的情况:你想在麦当劳里借个洗手间用用,但是你不知道洗手间在具体哪个位置,这时,你就超级过分地把整家麦当劳给锁了(表锁)再去慢慢找你的厕所。这样一来,不只是想来蹭厕所的客人,大量来点餐(进行其他数据操作)的客人也不能点了,好了麦当劳凉了你也凉了。


三、乐观锁

总是假设最好的情况,每次去读数据的时候都认为别人不会修改,所以不会上锁, 但是在更新的时候会判断一下在此期间有没有其他线程更新该数据, 可以使用版本号机制和CAS算法实现。 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。

1、版本号机制

乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。

举个栗子:

  1. 开课啦,教室里的学生们都是无比积极的
  2. 当老师提出第1个问题(版本号为1),就有几个学生争先恐后的举手想要回答问题(想要处理数据)
  3. 这时候,老师便让最先举手的那个学生回答第1题(版本号为1)
  4. 被点名的学生起来回答问题(某条数据处理成功进行),回答成功后,进入第2题(版本号改为了2)。
  5. 其余几个举手想回答第1题(想要处理版本为1)的学生知道看到老师现在提问的已经变成了第2题(版本号为2,跟自己的版本号对不上),就放下手了(想要处理的数据直接按照失败处理)。

这里需要注意一个点:学生的名字都应该是唯一的(版本号不重复),如果版本号重复,那么就失去意义了

当然,上面的例子是可以正常的处理事务,但会出现一个情况:很多近乎同时举手想要回答同一道题的学生都没被老师点名,这样就太打击学生的积极性了,怎么样才能够减小这种对学生的打击呢,最大程度的发挥学生的积极性呢?( 怎么减小乐观锁粒度, 最大程度的提升吞吐率,提高并发能力!)

假设老师采取另一种方式:只要你举手想要回答第n题,我只要还有回答时间,还有几次答同一道题的机会,就接着让你来回答,这样每个举手的学生就都能作答了。

问题表question有很多个问题(数据),第1道题在表中的question_id为1。每道题都有自己的剩余回答机会answer_num,如果被回答完,那么就不能再让人回答了。用sql表示如下:

update question set answer_num = answer_num - 1 where question_id = 1 and answer_num -1 > 0

以上update语句,在执行过程中,会在一次原子操作中自己查询一遍question_num(剩余问题)的值,并将其扣减掉1。


2、CAS算法

CAS(Compare-And-Swap,比较和互换),是一种有名的无锁算法。
CAS 算法是硬件对于并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。

无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步。
CAS算法涉及到3个操作数。

  • 需要读写的内存值V。
  • 进行比较的值A。
  • 拟写入的新值B。

当且仅当V的值等于A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

举个栗子:

  1. 桌上有 3 瓶雪碧(内存值为3)
  2. A想要开雪碧派对,确认了有 3 瓶雪碧(A读取时,内存值为 3 ),就出去打电话约朋友聚会(A事务进行较为缓慢)。
  3. 这期间,B想喝雪碧,看到了那 3 瓶雪碧(B读取时,内存值为3)
  4. 然后 B喝掉了 1 瓶,雪碧变成了 2 瓶(B事务执行完成,内存值变成了2)
  5. A约好朋友回来,确认雪碧变成了2瓶(内存值2,跟A读取时比较,发生变化了),雪碧派对开不成了,只能跟朋友说不聚了。(A事务执行失败,回滚)

B顺利执行了自己的事务,A执行自己事务失败回滚


3、CAS算法的ABA问题

CAS也并不完美,它存在**"ABA"问题**,假若一个变量初次读取是A,在compare阶段依然是A,但其实可能在此过程中,它先被改为B,再被改回A,而CAS是无法意识到这个问题的。CAS只关注了比较前后的值是否改变,而无法清楚在此过程中变量的变更明细,这就是所谓的ABA漏洞。

炒个上面的栗子:

  1. 桌上有 3 瓶雪碧(内存值为3)
  2. A想要开雪碧派对,确认了有 3 瓶雪碧(读取时,内存值为 3 ),就出去打电话约朋友聚会(事务进行较为缓慢)。
  3. 这期间,B想喝雪碧,看到了那 3 瓶雪碧(读取时,内存值为3)
  4. 然后B喝掉了 1 瓶,雪碧变成了 2 瓶(事务执行完成,内存值变成了2)
  5. 巧合发生了,B喝的雪碧得奖“再来一瓶”,B就立马换回来 1 瓶,然后放回了桌上,雪碧又变成了 3 瓶(内存值又被变成了3)
  6. A约好朋友回来,确认雪碧还是有3瓶(内存值还是3,跟读取时没有变化),就拿去开雪碧派对了(顺利执行自己的事务)。

虽然A和B都顺利执行了自己的事务,但这过程却存在了问题:

  • B白喝一瓶雪碧没给钱
  • A损失了一个“再来一瓶”

随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被应用到生产环境中了,尤其是并发量比较大的业务场景。

参考文章:什么是乐观锁,什么是悲观锁

猜你喜欢

转载自blog.csdn.net/weixin_38125045/article/details/106939013