悲观锁 乐观锁

在没有加锁的秒杀场景下 每秒打进来的请求是巨大的 高并发场景下 我们发现不仅异常率高的可怕 库存竟然还变成了负数 这产生的结果肯定是很大损失的 那为什么会出现超卖问题呢

我们假设有下面两个线程

线程1查询库存,发现库存充足,创建订单,然后准备对库存进行扣减,但此时线程2和线程3也进行查询,同样发现库存充足,然后线程1执行完扣减操作后,库存变为了0,线程2和线程3同样完成了库存扣减操作,最终导致库存变成了负数!这就是超卖问题的完整流程

因此超卖产生了 那我们应该如何解决呢 很简单 直接加锁不就好了 加个互斥锁 一个一个来 那么会出现所有人都在堵塞 秒杀变成小时杀了 肯定不行 所以我先介绍两种锁的机制

  1. 悲观锁,认为线程安全问题一定会发生,因此操作数据库之前都需要先获取锁,确保线程串行执行。常见的悲观锁有:synchronized、lock
  2. 乐观锁,认为线程安全问题不一定发生,因此不加锁,只会在更新数据库的时候去判断有没有其它线程对数据进行修改,如果没有修改则认为是安全的,直接更新数据库中的数据即可,如果修改了则说明不安全,直接抛异常或者等待重试。常见的实现方式有:版本号法、CAS操作、乐观锁算法

接下来我们详细分析乐观锁的实现方式:

版本号机制

版本号机制是乐观锁最常见的实现方式。每条数据都有一个版本号,每次更新数据时版本号加1。当线程A要更新数据时,先检查当前版本号是否与自己获取时的版本号一致,如果一致则更新,否则说明数据已被其他线程修改,更新失败。

例如我们可以在商品表中增加一个version字段:

UPDATE product SET stock = stock - 1, version = version + 1 
WHERE id = #{id} AND version = #{version}

CAS (Compare And Swap)

CAS是乐观锁的另一种实现方式,它包含三个操作数:内存位置、预期原值和新值。执行CAS操作时,将内存位置的值与预期原值比较,如果相匹配,则将内存位置的值更新为新值。否则,不做任何操作。

public boolean decreaseStock(Long productId, Integer version) {// 查询商品当前库存和版本号Product product = productMapper.selectById(productId);if (product.getStock() <= 0) {return false; // 库存不足}// 使用CAS更新库存和版本号int result = productMapper.decreaseStockWithVersion(productId, product.getVersion(), product.getVersion() + 1);return result > 0;
}

两种锁的适用场景

悲观锁适用于:

  • 并发写入多、临界资源争抢激烈的场景
  • 读少写多的场景
  • 要求数据强一致性的场景

乐观锁适用于:

  • 并发写入少、冲突较少的场景
  • 读多写少的场景
  • 允许短时间数据不一致的场景

乐观锁解决超卖问题

首先我们要为 tb_seckill_voucher 表新增一个版本号字段 version ,线程1查询完库存,在进行库存扣减操作的同时将版本号+1,线程2在查询库存时,同时查询出当前的版本号,发现库存充足,也准备执行库存扣减操作,但是需要判断当前的版本号是否是之前查询时的版本号,结果发现版本号发生了改变,这就说明数据库中的数据已经发生了修改,需要进行重试(或者直接抛异常中断)

       **boolean flag = seckillVoucherService.update(new LambdaUpdateWrapper<SeckillVoucher>().eq(SeckillVoucher::getVoucherId, voucherId).gt(SeckillVoucher::getStock, 0).setSql("stock = stock -1"));**

注意到这里**.gt(SeckillVoucher::getStock, 0)而不是eq(SeckillVoucher::getStock,voucher.getStock())**

其实是因为乐观锁的弊端 可能锁住正常订单 例如大家一起获取库存100 一个线程执行成功 其他线程扣减时 发现库存为99 直接不干了 多个线程直接断了 因此我们直接写成第一种即可

一人一单超卖

很容易发现 在判断订单前加上逻辑即可

 int count = this.count(new LambdaQueryWrapper<VoucherOrder>().eq(VoucherOrder::getUserId, ThreadLocalUtls.getUser().getId()));if (count >= 1) {// 当前用户不是第一单return Result.fail("用户已购买");}

通过测试,发现并没有达到我们想象中的目标,一个人只能购买一次,但是发现一个用户居然能够购买8次。这说明还是存在超卖问题

问题原因:出现这个问题的原因和前面库存为负数数的情况是一样的,线程1查询当前用户是否有订单,当前用户没有订单准备下单,此时线程2也查询当前用户是否有订单,由于线程1还没有完成下单操作,线程2同样发现当前用户未下单,也准备下单,这样明明一个用户只能下一单,结果下了两单,也就出现了超卖问题

解决方案:一般这种超卖问题可以使用下面两种常见的解决方案

  1. 悲观锁
  2. 乐观锁

悲观锁解决超卖问题

    /*** 抢购秒杀券** @param voucherId* @return*/@Transactional@Overridepublic Result seckillVoucher(Long voucherId) {// 1、查询秒杀券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2、判断秒杀券是否合法if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 秒杀券的开始时间在当前时间之后return Result.fail("秒杀尚未开始");}if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 秒杀券的结束时间在当前时间之前return Result.fail("秒杀已结束");}if (voucher.getStock() < 1) {return Result.fail("秒杀券已抢空");}// 3、创建订单Long userId = ThreadLocalUtls.getUser().getId();synchronized (userId.toString().intern()) {// 创建代理对象,使用代理对象调用第三方事务方法, 防止事务失效IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(userId, voucherId);}}/*** 创建订单** @param userId* @param voucherId* @return*/@Transactionalpublic Result createVoucherOrder(Long userId, Long voucherId) {
//        synchronized (userId.toString().intern()) {// 1、判断当前用户是否是第一单int count = this.count(new LambdaQueryWrapper<VoucherOrder>().eq(VoucherOrder::getUserId, userId));if (count >= 1) {// 当前用户不是第一单return Result.fail("用户已购买");}// 2、用户是第一单,可以下单,秒杀券库存数量减一boolean flag = seckillVoucherService.update(new LambdaUpdateWrapper<SeckillVoucher>().eq(SeckillVoucher::getVoucherId, voucherId).gt(SeckillVoucher::getStock, 0).setSql("stock = stock -1"));if (!flag) {throw new RuntimeException("秒杀券扣减失败");}// 3、创建对应的订单,并保存到数据库VoucherOrder voucherOrder = new VoucherOrder();long orderId = redisIdWorker.nextId(SECKILL_VOUCHER_ORDER);voucherOrder.setId(orderId);voucherOrder.setUserId(ThreadLocalUtls.getUser().getId());voucherOrder.setVoucherId(voucherOrder.getId());flag = this.save(voucherOrder);if (!flag) {throw new RuntimeException("创建秒杀券订单失败");}// 4、返回订单idreturn Result.ok(orderId);}}

这里有很多值得注意的小问题

  1. 锁的范围尽量小。synchronized尽量锁代码块,而不是方法,锁的范围越大性能越低
  2. 锁的对象一定要是一个不变的值。我们不能直接锁 Long 类型的 userId,每请求一次都会创建一个新的 userId 对象,synchronized 要锁不变的值,所以我们要将 Long 类型的 userId 通过 toString()方法转成 String 类型的 userId,toString()方法底层(可以点击去看源码)是直接 new 一个新的String对象,显然还是在变,所以我们要使用 intern() 方法从常量池中寻找与当前 字符串值一致的字符串对象,这就能够保障一个用户 发送多次请求,每次请求的 userId 都是不变的,从而能够完成锁的效果(并行变串行)
  3. 我们要锁住整个事务,而不是锁住事务内部的代码。如果我们锁住事务内部的代码会导致其它线程能够进入事务,当我们事务还未提交,锁一旦释放,仍然会存在超卖问题
  4. Spring的@Transactional注解要想事务生效,必须使用动态代理。Service中一个方法中调用另一个方法,另一个方法使用了事务,此时会导致@Transactional失效,所以我们需要创建一个代理对象,使用代理对象来调用方法。

让代理对象生效的步骤:

①引入AOP依赖,动态代理是AOP的常见实现之一

    <dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency>

②暴露动态代理对象,默认是关闭的

@EnableAspectJAutoProxy(exposeProxy = true)

乐观锁解决

ALTER TABLE tb_voucher_order
ADD CONSTRAINT UNIQUE (user_id)

乐观锁解决方案其实更加优雅。我们可以通过在数据库表中添加唯一约束来防止一人多单的问题。通过在user_id字段上添加唯一约束,当多个线程尝试为同一用户创建订单时,数据库会自动拒绝重复记录,只有第一个提交的事务能够成功。这种方式比使用悲观锁性能更好,因为它不需要额外的加锁操作,而是利用了数据库自身的特性来保证数据一致性。

要记得捕获异常返回前端

try {save(order); // 可能会抛出 DuplicateKeyException} catch (DuplicateKeyException e) {return Result.fail("你已经抢过了");}

这样即可

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/88832.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/88832.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/88832.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何使用Cisco DevNet提供的免费ACI学习实验室(Learning Labs)?(Grok3 回答)

Cisco DevNet 提供的免费 ACI&#xff08;Application Centric Infrastructure&#xff09;学习实验室&#xff08;Learning Labs&#xff09;是帮助用户学习和实践 Cisco ACI 技术&#xff08;包括 APIC 控制器&#xff09;的优秀资源&#xff0c;适合网络工程师、开发者和准备…

Combine的介绍与使用

目录一、Combine 框架介绍二、核心概念三、基础使用示例3.1、创建 Publisher & 订阅3.2、操作符链式调用3.3、Subject 使用&#xff08;手动发送值&#xff09;3.4、网络请求处理3.5、组合多个 Publisher3.6、错误处理四、核心操作符速查表 Operator五、UIKit 绑定示例六、…

【Java笔记】七大排序

目录1. 直接插入排序2. 希尔排序3. 选择排序4. 堆排序(重要)5. 冒泡排序6. 快速排序&#xff08;重要&#xff09;6.1 Hoare 法6.1.1 Hoare 法优化6.2 挖坑法&#xff08;重点&#xff09;6.3 快速排序的非递归写法7. 归并排序海量数据的排序问题8. 总结1. 直接插入排序 时间复…

H.264编解码(NAL)

在我们的日常生活中&#xff0c;比如有缓存电影或者是发送视频的需求。如果没有视频压缩&#xff0c;一部手机只能存几分钟视频&#xff0c;1TB 硬盘也装不下几部电影&#xff0c;用 4G 网络发一段 1 分钟视频&#xff0c;可能需要几十分钟&#xff08;甚至传不完&#xff09;&…

新手向:Python自动化办公批量重命名与整理文件系统

本文将详细介绍如何使用Python实现一个强大的文件批量重命名与整理工具&#xff0c;帮助开发者自动化这一繁琐过程。本教程面向Python初学者&#xff0c;通过一个完整的项目案例&#xff0c;讲解文件系统操作的核心技术。我们将构建的工具将具备以下功能&#xff1a;基于正则表…

C++ 左值右值、左值引用右值引用、integral_constant、integral_constant的元模板使用案例

C 左值右值、左值引用右值引用、integral_constant、integral_constant的元模板使用案例一、左值右值1.左值2.右值二、左值引用右值引用1.左值引用2.右值引用总结三、integral_constant四、integral_constant的元模板使用案例1.求最大整数2.内存对齐alignof关键字元模板计算内存…

c++算法一

1.双指针总结&#xff1a;1.复写0这道题&#xff0c;告诉我们要正难其反&#xff0c;我们从后向前进行重写&#xff0c;删除某些数字的时候&#xff0c;我们可以从前向后遍历&#xff0c;但是增加一些数字的时候会对后面的数据进行覆盖&#xff0c;所以要从后向前进行2.快乐数涉…

LeetCode-283. 移动零(Java)

283. 移动零 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 请注意 &#xff0c;必须在不复制数组的情况下原地对数组进行操作。 示例 1: 输入: nums [0,1,0,3,12] 输出: [1,3,12,0,0] 示例 2: 输入: n…

【数据库】慢SQL优化 - MYSQL

一、数据库故障的关键点 引起数据库故障的因素有操作系统层面、存储层面&#xff0c;还有断电断网的基础环境层面&#xff08;以下称为外部因素&#xff09;&#xff0c;以及应用程序操作数据库和人为操作数据库这两个层面&#xff08;以下称内部因素&#xff09;。这些故障中外…

桶排序算法深度剖析

&#x1f50d; 桶排序算法深度剖析 &#x1f3af; 核心原理图解⚙️ 完整算法流程 #mermaid-svg-LyB6SpaZ132X4Wtn {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-LyB6SpaZ132X4Wtn .error-icon{fill:#552222;}#mer…

对S32K144做的BMS安装快速开发Simulink库及BMS例程介绍

前言 本章介绍BMS硬件功能及SimuLink库为主&#xff0c;捎带介绍一些例程内容 注意&#xff1a;例程所用的协议均是自定义的 自做的SimuLink库也会不定期更新 BMS例程的内容不定期维护添加 当前的BMS没有主动均衡功能&#xff0c;这个有考虑后期加上&#xff0c;当前还处于…

urlencode、html实体编码、unicode

目录 urlencode html实体编码 Unicode编码 urlencode URL编码也称为百分号编码&#xff0c;用于将URL中的特殊字符转换为安全传输的格式。英文数字一般不编码 特点&#xff1a; 使用%后跟两个十六进制数字表示字符 空格编码为或%20 保留字符&#xff08;; / ? : & …

【HarmonyOS】元服务概念详解

【HarmonyOS】元服务概念详解 最近几年&#xff0c;我们手里的设备越来越多——手机、平板、手表、车机……光是管理这些设备上的APP就够头疼了&#xff1a;下载要流量、安装占内存、换个设备又得重新弄一遍。有没有更简单的方式&#xff1f;HarmonyOS推出的“元服务”&#xf…

vscode/cursor怎么自定义文字、行高、颜色

JetBrains Mono: A free and open source typeface for developers | JetBrains: Developer Tools for Professionals and Teams 首先下载上面的文字&#xff0c;然后右键全选&#xff0c;安装 然后重启cursor 下载插件Apc Customize UI 点击设置 把下面的代码复制进去&…

JavaScript 与 C语言基础知识差别

一&#xff0c; 变量声明对比 C语言&#xff1a; int age 20; // 必须指定类型 float price 9.99; char grade A; const double PI 3.14; // 常量JavaScript&#xff1a; let age 20; // 数字 var price 9.99; // 现在不用&#xff0c;有缺点 co…

无缝矩阵支持音频分离带画面分割功能的全面解析

一、技术原理与实现方式1. 音频分离技术核心功能&#xff1a;HDMI无缝矩阵通过硬件或软件实现音频加嵌与分离功能&#xff0c;支持多设备音频的独立处理与增强。实现方式&#xff1a;音频加嵌&#xff1a;将外部音频信号&#xff08;如麦克风、调音台&#xff09;嵌入HDMI信号中…

AI创作系列第18篇:海狸IM移动端UI统一大升级 - 从混乱到规范的技术重构之路

AI创作系列第18篇&#xff1a;海狸IM移动端UI统一大升级 - 从混乱到规范的技术重构之路本文是海狸IM AI创作系列的第18篇文章&#xff0c;记录7月11日-13日周末期间对移动端的UI统一升级工作。这次重构不是功能性的&#xff0c;而是架构性的 - 我们重新设计了整个UI架构&#x…

八、nginx搭建,实现vue跳转nginx跳转gateway

基本的调用链路: vue调用nginx,nginx反向代理gateway,gateway看用户是否登录,没有登录的话,就创建验证码并先输入密码后获取token。 截止现在我们创建了两个项目能够通过feign调用,并且创建好了gateway,且能调用对应的项目。 这一章节,我们搭建好nginx,通过反向代理,…

C++ 中常见的字符串定义方式及其用法

引言 最近在学习C&#xff0c;下面将从基础到进阶的顺序&#xff0c;列出一些 C 中常见的字符串定义方式及其用法&#xff0c;包含完整代码和详细注释&#xff0c;加深对代码的理解。 C 风格字符串&#xff08;char*或 char[]&#xff09; 定义方式 #include <iostream>i…

下一代防火墙-防范DOS攻击、IPS防护、web防护实验

一、实验拓扑二、实验设备1.山石网科系列下一代防火墙2.三层交换机一台3.windows两台4.各种工具&#xff0c;如hyenae、小旋风服务器、永恒之蓝等等三、实验目的1.掌握网络攻击防护策略配置2.通过下一代防火墙来防护服务器免受DOS攻击四、防范Dos攻击实验1.将一台windows配置为…