悲观锁与乐观锁的实现(详情图解)

1、前言

  • 在了解悲观锁和乐观锁以前,咱们先了解一下什么是锁,为何要用到锁?mysql

  • 技术来源于生活,锁不只在程序中存在,在现实中咱们也随处可见,例如咱们上下班打卡的指纹锁,保险柜上的密码锁,以及咱们咱们登陆的用户名和密码也是一种锁,生活中用到锁能够保护咱们人身安全(指纹锁)、财产安全(保险柜密码锁)、信息安全(用户名密码锁),让咱们更放心的去使用和生活,由于有锁,咱们不用去担忧我的的财产和信息泄露。web

  • 而程序中的锁,则是用来保证咱们数据安全的机制和手段,例如当咱们有多个线程去访问修改共享变量的时候,咱们能够给修改操做加锁(syncronized)。当多个用户修改表中同一数据时,咱们能够给该行数据上锁(行锁)。所以,当程序中可能出现并发的状况时,咱们就须要经过必定的手段来保证在并发状况下数据的准确性,经过这种手段保证了当前用户和其余用户一块儿操做时,所获得的结果和他单独操做时的结果是同样的算法

  • 没有作好并发控制,就可能致使脏读、幻读和不可重复读等问题,以下图所示:
    在这里插入图片描述
    因为并发操做,若是没有加锁进行并发控制,数据库的最终的一条数据可能为3也有可能为5,致使数值不许确sql

2、悲观锁和乐观锁

首先咱们须要清楚的一点就是不管是悲观锁仍是乐观锁,都是人们定义出来的概念,能够认为是一种思想。数据库

2.一、悲观锁

悲观锁(Pessimistic Lock): 就是很悲观,每次去拿数据的时候都认为别人会修改。因此每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放,悲观锁中的共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程编程

可是在效率方面,处理加锁的机制会产生额外的开销,还有增长产生死锁的机会。另外还会下降并行性,若是已经锁定了一个线程A,其余线程就必须等待该线程A处理完才能够处理安全

数据库中的行锁,表锁,读锁(共享锁),写锁(排他锁),以及syncronized实现的锁均为悲观锁并发

在这里插入图片描述
悲观并发控制其实是“先取锁再访问”的保守策略,为数据处理的安全提供了保证,
在这里插入图片描述svg

2.二、乐观锁

乐观锁(Optimistic Lock): 就是很乐观,每次去拿数据的时候都认为别人不会修改。因此不会上锁,可是若是想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。若是修改过,则从新读取,再次尝试更新,循环上述步骤直到更新成功(固然也容许更新失败的线程放弃操做),乐观锁适用于多读的应用类型,这样能够提升吞吐量性能

相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。通常的实现乐观锁的方式就是记录数据版本(version)或者是时间戳来实现,不过使用版本记录是最经常使用的。

在这里插入图片描述
乐观控制相信事务之间的数据竞争(data race)的几率是比较小的,所以尽量直接作下去,直到提交的时候才去锁定,因此不会产生任何锁和死锁。

3、锁的实现

悲观锁阻塞事务、乐观锁回滚重试:它们各有优缺点,不要认为一种必定好于另外一种。像乐观锁适用于写比较少的状况下,即冲突真的不多发生的时候,这样能够省去锁的开销,加大了系统的整个吞吐量。但若是常常产生冲突,上层应用会不断的进行重试,这样反却是下降了性能,因此这种状况下用悲观锁就比较合适。

3.1 悲观锁的实现方式

场景:

有用户A和用户B,在同一家店铺去购买同一个商品,可是商品的可购买数量只有一个

下面是这个店铺的商品表t_goods结构和表中的数据:
在这里插入图片描述
在不加锁的状况下,若是用户A和用户B同时下单,就会报错。

悲观锁的实现,每每依靠数据库提供的锁机制,在数据库中,咱们如何用悲观锁去解决这个事情呢?

  1. 加入当用户A对下单购买商品(臭豆腐)的时候,先去尝试对该数据(臭豆腐)加上悲观锁
  2. 加锁失败:说明商品(臭豆腐)正在被其余事务进行修改,当前查询须要等待或者抛出异常,具体返回的方式须要由开发者根据具体状况去定义
  3. 加锁成功:对商品(臭豆腐)进行修改,也就是只有用户A能买,用户B想买(臭豆腐)就必须一直等待。当用户A买好后,用户B再想去买(臭豆腐)的时候会发现数量已经为0,那么B看到后就会放弃购买
  4. 在此期间若是有其余对该数据(臭豆腐)作修改或加锁的操做,都会等待咱们解锁后或者直接抛出异常

在这里插入图片描述

那么如何加上悲观锁呢?咱们能够经过如下语句给id=2的这行数据加上悲观锁,首先关闭MySQL数据库的自动提交属性。由于MySQL默认使用autocommit模式,也就是说,当咱们执行一个更新操做后,MySQL会马上将结果进行提交,(sql语句:set autocommit=0)

悲观锁加锁sql语句: select num from t_goods where id = 2 for update

咱们经过开启mysql的两个会话,也就是两个命令行来演示:

事务A:
咱们能够看到数据是马上立刻就能够查询出来,num=1
在这里插入图片描述
事务B:
咱们是能够看到,事务B会一直等待事务A释放锁。若是事务A长期不释放锁,那么最终事务B将会报错,报错以下:Lock wait timeout exceeded; try restarting transaction,表示语句已被锁住
在这里插入图片描述
如今咱们让事务A执行命令去修改数据,让臭豆腐的数量减一,而后查看修改后的数据,最后commit,结束事务

在这里插入图片描述

咱们能够看到当咱们事务A执行完成以后,臭豆腐的库存只有0个了,这个时候咱们用户B再来购买这个臭豆腐的时候就会发现,最后一个臭豆腐已经被用户A购买完了,那么用户B只能放弃购买臭豆腐了。
在这里插入图片描述
经过悲观锁咱们能够解决由于商品库存不足,致使的商品超出库存的售卖。

3.1 乐观锁的实现方式

对于上面的应用场景,咱们应该怎么用乐观锁去解决呢?在上面的乐观锁中,咱们有提到使用版本号(version)来解决,因此咱们须要在t_goods加上版本号,调整后的sql表结构以下:
在这里插入图片描述
具体操做步骤以下:
一、首先用户A和用户B同时将臭豆腐(id=2)的数据查出来
二、而后用户A先买,用户A将(id=1和version=0)做为条件进行数据更新,将数量-1,而且将版本号+1。此时版本号变为1。用户A此时就完成了商品的购买
三、 用户B开始买,用户B也将(id=1和version=0)做为条件进行数据更新
四、更新完后,发现更新的数据行数为0,此时就说明已经有人改动过数据,此时就应该提示用户B从新查看最新数据购买

在这里插入图片描述

一、首先咱们开启两个会话窗口,输入查询语句:select num from t_goods where id = 2
事务A:
在这里插入图片描述

事务B:
在这里插入图片描述

这个时候事务A和事务B同时获取相同的数据

二、此时事务A进行更新数据的操做,而后在查询更新后的数据
在这里插入图片描述
这个时候咱们能够看到事务A更新成功,而且库存-1 版本号+1成功

二、此时事务B进行更新数据的操做,而后在查询更新后的数据
在这里插入图片描述
能够看到最终修改的时候失败,数据没有改变。此时就须要咱们告知用户B从新处理

3.1.1 CAS

说到乐观锁,就必须提到一个概念:CAS
什么是CAS呢?Compare-and-Swap,即比较并替换,也有叫作Compare-and-Set的,比较并设置。
一、比较:读取到了一个值A,在将其更新为B以前,检查原值是否仍为A(未被其余线程改动)。
二、设置:若是是,将A更新为B,结束。[1]若是不是,则什么都不作。
上面的两步操做是原子性的,能够简单地理解为瞬间完成,在CPU看来就是一步操做。
有了CAS,就能够实现一个乐观锁,容许多个线程同时读取(由于根本没有加锁操做),可是只有一个线程能够成功更新数据,并致使其余要更新数据的线程回滚重试。 CAS利用CPU指令,从硬件层面保证了操做的原子性,以达到相似于锁的效果。

Java中真正的CAS操做调用的native方法
由于整个过程当中并无“加锁”和“解锁”操做,所以乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已,可是CAS有一个问题那就是会产生ABA问题,什么是ABA问题,以及如何解决呢?

ABA 问题:
若是一个变量V初次读取的时候是A值,而且在准备赋值的时候检查到它仍然是A值,那咱们就能说明它的值没有被其余线程修改过了吗?很明显是不能的,由于在这段时间它的值可能被改成其余值,而后又改回A,那CAS操做就会误认为它历来没有被修改过。这个问题被称为CAS操做的 "ABA"问题。

ABA 问题解决:
咱们须要加上一个版本号(Version),在每次提交的时候将版本号+1操做,那么下个线程去提交修改的时候,会带上版本号去判断,若是版本修改了,那么线程重试或者提示错误信息~

4、如何选择

悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种必定好于另外一种。像乐观锁适用于写比较少的状况下,即冲突真的不多发生的时候,这样能够省去锁的开销,加大了系统的整个吞吐量。

但若是常常产生冲突,上层应用会不断的进行重试,这样反却是下降了性能,因此这种状况下用悲观锁就比较合适。

注意点:

一、乐观锁并未真正加锁,因此效率高。一旦锁的粒度掌握很差,更新失败的几率就会比较高,容易发生业务失败。

二、悲观锁依赖数据库锁,效率低。更新失败的几率比较低。

5、总结

这篇文章讲解了悲观锁与乐观锁的区别,以及实现场景,不论是悲观锁仍是乐观锁都是人们定义出来的概念,是一种思想,如何有有疑问或者问题的小伙伴能够在下面进行留言,小农看到了会第一时间回复你们,谢谢,你们加油~