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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 悲观锁与乐观锁的实现(详情图解)

三、锁的实现


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


3.1 悲观锁的实现方式


场景:


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

下面是这个店铺的商品表t_goods结构和表中的数据:

image.png


在不加锁的情况下,如果用户A和用户B同时下单,就会报错。


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


加入当用户A对下单购买商品(臭豆腐)的时候,先去尝试对该数据(臭豆腐)加上悲观锁

加锁失败:说明商品(臭豆腐)正在被其他事务进行修改,当前查询需要等待或者抛出异常,具体返回的方式需要由开发者根据具体情况去定义

加锁成功:对商品(臭豆腐)进行修改,也就是只有用户A能买,用户B想买(臭豆腐)就必须一直等待。当用户A买好后,用户B再想去买(臭豆腐)的时候会发现数量已经为0,那么B看到后就会放弃购买

在此期间如果有其他对该数据(臭豆腐)做修改或加锁的操作,都会等待我们解锁后或者直接抛出异常

image.png


那么如何加上悲观锁呢?我们可以通过以下语句给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

image.png


事务B:

我们是可以看到,事务B会一直等待事务A释放锁。如果事务A长期不释放锁,那么最终事务B将会报错,报错如下:Lock wait timeout exceeded; try restarting transaction,表示语句已被锁住

image.png


现在我们让事务A执行命令去修改数据,让臭豆腐的数量减一,然后查看修改后的数据,最后commit,结束事务

image.png



我们可以看到当我们事务A执行完成之后,臭豆腐的库存只有0个了,这个时候我们用户B再来购买这个臭豆腐的时候就会发现,最后一个臭豆腐已经被用户A购买完了,那么用户B只能放弃购买臭豆腐了。


通过悲观锁我们可以解决因为商品库存不足,导致的商品超出库存的售卖。


3.1 乐观锁的实现方式


对于上面的应用场景,我们应该怎么用乐观锁去解决呢?在上面的乐观锁中,我们有提到使用版本号(version)来解决,所以我们需要在t_goods加上版本号,调整后的sql表结构如下:

image.png


具体操作步骤如下:

1、首先用户A和用户B同时将臭豆腐(id=2)的数据查出来

2、然后用户A先买,用户A将(id=1和version=0)作为条件进行数据更新,将数量-1,并且将版本号+1。此时版本号变为1。用户A此时就完成了商品的购买

3、 用户B开始买,用户B也将(id=1和version=0)作为条件进行数据更新

4、更新完后,发现更新的数据行数为0,此时就说明已经有人改动过数据,此时就应该提示用户B重新查看最新数据购买


image.png


1、首先我们开启两个会话窗口,输入查询语句:select num from t_goods where id = 2

事务A:


image.png

事务B:

image.png


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


2、此时事务A进行更新数据的操作,然后在查询更新后的数据

image.png

这个时候我们可以看到事务A更新成功,并且库存-1 版本号+1成功


2、此时事务B进行更新数据的操作,然后在查询更新后的数据

image.png

可以看到最终修改的时候失败,数据没有改变。此时就需要我们告知用户B重新处理


3.1.1 CAS


说到乐观锁,就必须提到一个概念:CAS

什么是CAS呢?Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置。

1、比较:读取到了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动)。

2、设置:如果是,将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操作,那么下个线程去提交修改的时候,会带上版本号去判断,如果版本修改了,那么线程重试或者提示错误信息~


四、如何选择


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


但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。


注意点:


1、乐观锁并未真正加锁,所以效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。


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


五、总结


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



相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
消息中间件 存储 前端开发
[笔记]C++并发编程实战 《四》同步并发操作(三)
[笔记]C++并发编程实战 《四》同步并发操作(三)
|
23天前
|
算法 Java 关系型数据库
何为乐观锁和悲观锁
何为乐观锁和悲观锁
|
6月前
|
存储 人工智能 关系型数据库
10个行锁、死锁案例⭐️24张加锁分析图🚀彻底搞懂Innodb行锁加锁规则!
10个行锁、死锁案例⭐️24张加锁分析图🚀彻底搞懂Innodb行锁加锁规则!
|
5月前
|
Java Linux
如何实现无锁并发:深入理解CAS原理
如何实现无锁并发:深入理解CAS原理
186 2
|
6月前
|
Java
18.AtomicReference、AtomicStampReference底层原理。多个变量更新怎么保证原子性?CAS的ABA问题怎么解决?
18.AtomicReference、AtomicStampReference底层原理。多个变量更新怎么保证原子性?CAS的ABA问题怎么解决?
51 0
18.AtomicReference、AtomicStampReference底层原理。多个变量更新怎么保证原子性?CAS的ABA问题怎么解决?
|
算法 Java 数据库
数据库系列课程(19)-乐观锁与悲观锁的优缺点
数据库系列课程(19)-乐观锁与悲观锁的优缺点
76 0
|
存储 前端开发 Unix
[笔记]C++并发编程实战 《四》同步并发操作(二)
[笔记]C++并发编程实战 《四》同步并发操作(二)
|
存储 前端开发 安全
[笔记]C++并发编程实战 《四》同步并发操作(一)
[笔记]C++并发编程实战 《四》同步并发操作
|
存储 前端开发 调度
[笔记]C++并发编程实战 《四》同步并发操作(四)
[笔记]C++并发编程实战 《四》同步并发操作(四)
105 0
|
缓存 数据处理 数据库
悲观锁和乐观锁的区别和应用场景
悲观锁和乐观锁是并发控制中常用的两种锁机制,用于解决多线程环境下的数据一致性问题。它们在应对并发访问时采取了不同的策略,有不同的特点和适用场景。
1336 0