黑马点评-乐观锁/悲观锁/synchronized/@Transactional

文章目录

          • 全局ID生成器
          • 超卖
            • 乐观锁
          • 一人一单
            • 悲观锁

当我们确认订单时,系统需要给我们返回我们的订单编号。这个时候就会出现两个大问题。

1.订单id采用数据库里的自增的话,安全性降低。比如今天我的订单是10,我明天的订单是100,那么我就可以知道在昨天总共有多少订单,从而给恶意用户钻空子。

2.订单数很多时,订单id增长到几百上千万,单张表无法存储大量数据,那就需要将这些数据分到多张表,同时要重新设计订单id,避免出现相同ID。

所以,这里我们使用全局ID生成器

全局ID生成器

在分布式系统下用来生成全局唯一ID的工具。

其基本核心就是ID的生成:

在这里插入图片描述

符号位:正负数

时间戳:当前时间减初始时间

序列号:基于redis自增INCR命令

对存储在指定键中的整数值进行原子性递增的核心命令.

当key不存在时,redis自动创建一个新key,并设置其value为0.然后执行incr操作,将value递增为1并返回。

key存在时,直接将value递增。

@Component
public class RedisIdWorker {/*** 开始时间戳*/private static final long BEGIN_TIMESTAMP = 1640995200L;//2022年1月1日0点0分0秒/*** 序列号的位数*/private static final int COUNT_BITS = 32;private StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}public long nextId(String keyPrefix) {// 1.生成时间戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long timestamp = nowSecond - BEGIN_TIMESTAMP;// 2.生成序列号// 2.1.获取当前日期,精确到天String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// 2.2.自增长long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);// 3.拼接并返回return timestamp << COUNT_BITS | count;}
}

超卖

在简单的优惠券秒杀下单中,我们的基本步骤:

1.根据优惠券id查询是否存在

2.确认抢购时间在时间范围内

3.确认优惠券库存>0

4.根据RedisIdWorker生成订单id,优惠券数量减1

在这里插入图片描述

这在简单的场景下是没有问题的,但是在实际场景中,我们要考虑到多线程导致的超卖问题。

现在有100张优惠券,有200人来抢

理论上来说,应卖出100张,有100人抢到,但事实却是多卖出了9张

在这里插入图片描述

当涉及多线程时,各个线程的运行顺序我们是无法肯定的。

当线程1查询库存为1时,线程2插进来了,也查到为1,线程1按照逻辑扣减库存,线程2也按照逻辑扣除库存,这样就导致最终库存为-1。更多个线程,可能导致库存更低,这就是超卖。

在这里插入图片描述

乐观锁

悲观锁和乐观锁都只是一种思想!

乐观锁先操作,提交时再检查冲突

认为并发操作很少发生冲突,只在提交操作时检查是否冲突,比如CAS操作,数据库的乐观锁和Java中的Atomic类。

举个例子:

1.购物车结算时才检查库存(默认没人抢购)

2.或者在网上订票,系统显示还有1个座位,你点击预订,系统会先让你填写信息,然后提交的时候检查是否还有座位。如果有,预订成功;如果没有,提示你重新选择

这里就以乐观锁为核心解决方法:判断之前查询到的数据是否有被修改过。

  • 版本号法

    给优惠券再设置一个字段“版本号”,初始值为1,每次被修改就加1。

    这样每个线程在查询到库存和版本号时,要想修改数据,必须在当前版本号基础上实现,否则不成功。

    在这里插入图片描述

  • CAS法

    本质还是版本思想,做了简化,每个线程在查询到库存后,要想修改数据,必须在当前库存基础上实现,否则不成功。

    在这里插入图片描述

但是乐观锁同样存在问题,当其他线程发现数据被修改后,他就不再执行,导致优惠券没有卖完。

所以这里其他线程只需要在将修改条件改为stock>0。只要有库存,我就可以减。

这样会不会恍然中带点疑惑:这跟最初有什么区别?都是判断库存是否>0。

NO,最初的问题出现在先判断,再修改;而现在是要修改的时候才做判断

@Transactionalpublic Result createVoucherOrder(Long voucherId) {// 5.一人一单Long userId = UserHolder.getUser().getId();// 6.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock = stock - 1.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0.update();if (!success) {// 扣减失败return Result.fail("库存不足!");}// 7.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 7.1.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 7.2.用户idvoucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回订单idreturn Result.ok(orderId);}}
一人一单
悲观锁

悲观锁:提前加锁

认为并发操作一定会发生冲突,因此每次访问数据时都会加锁,比如synchronized和ReentrantLock

举个例子:出门时锁门(默认有小偷)

上面的解决中,还存在一个问题:一个用户不可以买多张优惠券

那如果我们直接简单的判断该用户是否下过单来处理的话:

// 5.1.查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 5.2.判断是否存在if (count > 0) {// 用户已经购买过了log.error("不允许重复下单!");return;}// 6.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock = stock - 1.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0.update();if (!success) {// 扣减失败log.error("库存不足!");return;}// 7.创建订单save(voucherOrder);

假设用户A同时发起多个请求,每个请求都执行这段代码。这时候,可能会出现多个线程同时通过第5.1步的查询(count=0),然后都进入扣减库存和创建订单的步骤,导致用户A创建了多个订单,违反了“一人一单”的要求。

为什么会这样?

两个线程同时执行查询时,此时数据库中还没有该用户的订单,所以两个线程都认为可以继续执行。然后它们都会去扣减库存,假设库存足够,两个线程都成功扣减,然后各自创建订单。

所以我们最终的解决方法就是再加上一个悲观锁:

一个用户加一把锁(确保不会重复下单),不同用户加不同锁

@Transactionalpublic Result createVoucherOrder(Long voucherId) {// 5.一人一单Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {// 5.1.查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 5.2.判断是否存在if (count > 0) {// 用户已经购买过了return Result.fail("用户已经购买过一次!");}// 6.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock = stock - 1.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0.update();if (!success) {// 扣减失败return Result.fail("库存不足!");}// 7.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 7.1.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 7.2.用户idvoucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回订单idreturn Result.ok(orderId);}}

synchronized (userId.toString().intern())

这里为什么通过用户ID来加锁,为什么是userId.toString().intern()?

synchronized:实现线程同步,确保同一时刻只有一个线程可以执行某个代码块或方法。

toString()userId转换为字符串,虽然是同一个userId,但是会新生成不同的字符串对象。

public static String toString(long i) {int size = stringSize(i);if (COMPACT_STRINGS) {byte[] buf = new byte[size];getChars(i, size, buf);return new String(buf, LATIN1);} else {byte[] buf = new byte[size * 2];StringUTF16.getChars(i, size, buf);return new String(buf, UTF16);}}

intern()方法会返回该字符串在常量池中的引用,确保相同值的字符串引用同一个对象,从而正确同步。

  • 如果常量池已存在相同值的字符串,直接返回该引用;
  • 如果不存在,将该字符串加入常量池后再返回引用。

不过又发现一个问题:这里用户加锁-操作-释放锁,但如果此时事务还没有提交上去,其他线程来了,依然可能出现并发问题。

在这里插入图片描述

我们希望整个事务提交上去后再释放锁

也就是给这个函数加上锁。

当函数1(无事务)调用这个函数2时(有事务),事务是否还生效?

在这里插入图片描述

事务

当我们在一个类的方法上使用 @Transactional注解时,Spring会为该类创建一个代理对象。这个代理对象在调用方法时会处理事务的开启、提交或回滚等操作。

如果在一个类内部的方法A调用另一个有@Transactional注解的方法B,这时候方法A调用的是实际的实例方法,而不是通过代理对象调用的。因此,事务不会生效,因为代理对象没有被使用到。

解决:

在这里插入图片描述

在这里插入图片描述

不断学习中,感谢大家的观看>W<

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

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

相关文章

python下通过wmic设置程序的优先级~~~

在开发过程中&#xff0c;经常会碰到需要设置程序优先级&#xff0c;这时候可以手动到任务管理器中调整&#xff0c;但是这多多少少有些不方便&#xff0c;那么这时候我们就可以通过subprocess调用wmic命令来实现&#xff0c;方法如下: step 1 必要的引用: import subprocess…

在Mac中使用pyenv管理Python版本:从安装到虚拟环境的全流程指南

# 在Mac中使用pyenv管理Python版本&#xff1a;从安装到虚拟环境的全流程指南 ## 一、为什么选择pyenv&#xff1f; 在开发过程中&#xff0c;不同项目往往需要不同的Python版本&#xff08;如3.8 vs 3.10&#xff09;&#xff0c;而系统默认的Python环境难以满足灵活切换的需…

FFT Shift

在频域图像处理中,交换四个象限实现FFT移位(也称为FFT Shift)是一种将频域图像的低频成分移动到中心的标准化操作。 1. 为什么需要FFT移位? 原始FFT输出特性: 二维FFT的直接计算结果中: 低频分量(图像的整体亮度和平滑部分)位于频谱图的四个角落 高频分量(边缘、细节…

python打卡day34@浙大疏锦行

知识点回归&#xff1a; CPU性能的查看&#xff1a;看架构代际、核心数、线程数GPU性能的查看&#xff1a;看显存、看级别、看架构代际GPU训练的方法&#xff1a;数据和模型移动到GPU device上类的call方法&#xff1a;为什么定义前向传播时可以直接写作self.fc1(x) ①CPU性能查…

Windows 配置 ssh 秘钥登录 Ubuntu

在 Windows 上推送 SSH 公钥到远程服务器&#xff08;类似于 Linux 上的 ssh-copy-id&#xff09;可以通过以下几种方法实现&#xff1a; ** 手动复制公钥内容** 查看本地公钥内容&#xff1a;type $env:USERPROFILE\.ssh\id_rsa.pub登录远程服务器&#xff0c;将公钥内容粘贴…

SAP全面转向AI战略,S/4HANA悄然隐身

在2025年SAP Sapphire大会上&#xff0c;SAP首席执行官Christian Klein提出了一个雄心勃勃的愿景&#xff1a;让人工智能&#xff08;AI&#xff09;无处不在&#xff0c;推动企业数字化转型。SAP的AI战略核心是将AI深度融入其业务应用生态&#xff0c;包括推出全新版本的AI助手…

Athena 执行引擎:在线服务计算的效率王者

引言 在在线服务领域&#xff0c;计算任务呈现出独特的特性&#xff1a;一方面&#xff0c;数据量通常不会过于庞大&#xff0c;因为在线服务对耗时和响应速度有着严苛要求&#xff1b;另一方面&#xff0c;计算任务具有可控性&#xff0c;其大多并非由用户实时输入动态生成&a…

传奇各种怪物一览/图像/爆率/产出/刷新地/刷新时间/刷怪时间

名称图像显示名等级血量攻击可召唤产出刷新蝙蝠蝙蝠530-22,0,0可诱惑回城卷(1.00%) 金币(1.00%*500)鸡鸡551-1,0,0可诱惑鸡肉(100.00%)比奇省(29550,62550)5分钟35只 比奇省(35025,20025)5分钟25只 比奇省(34025,31025)5分钟25只 比奇省(40525,24025)5分钟25只 比奇省(28025,26…

MySQL--day7--聚合函数

&#xff08;以下内容全部来自上述课程&#xff09; 聚合函数 1. 介绍 聚合函数作用于一组数据&#xff0c;并对一组数据返回一个值。 聚合函数类型 AVG&#xff08;&#xff09;SUM&#xff08;&#xff09;MAX&#xff08;&#xff09;MIN&#xff08;&#xff09;COU…

[Java] 封装

目录 1. 什么是封装 2. 访问修饰符 3. 封装的好处 4. 封装的步骤 5. 包 5.1 什么是包 5.2 导入包中的类 5.3 自定义包 5.4 常用的包 6. static关键字 6.1 static修饰成员变量 6.2 static修饰成员方法 6.3 Static修饰成员变量初始化 7. 代码块 7.1 普通代码块 …

Axure元件动作五:设置列表选中项

亲爱的小伙伴,在您浏览之前,烦请关注一下,在此深表感谢!如有帮助请订阅专栏! Axure产品经理精品视频课已登录CSDN可点击学习https://edu.csdn.net/course/detail/40420 演示视频: Axure设置列表选中项 课程主题:设置列表选中项 主要内容:下拉列表选项、值、变量值、焦…

Spring框架--IOC技术

一、Spring框架的介绍 1、Spring框架的概述 Spring 是一个开放源代码的设计层面框架&#xff0c;它解决的是业务逻辑层和其他各层的松耦合问题&#xff0c;因此它将面向接口的编程思想贯穿整个系统应用。Spring是于2003年兴起的一个轻量级的Java开发框架&#xff0c;由 Rod Jo…

Flannel后端为UDP模式下,分析数据包的发送方式——tun设备(三)

在分析 Kubernetes 环境中 Flannel UDP 模式的数据包转发时&#xff0c;我们提到 flannel.1 是一个 TUN 设备&#xff0c;它在数据包处理中起到了关键作用。 什么是 TUN 设备&#xff1f; TUN 设备&#xff08;Tunnel 设备&#xff09;是 Linux 系统中一种虚拟网络接口&#x…

2025深圳国际无人机展深度解析:看点、厂商与创新亮点

2025深圳国际无人机展深度解析&#xff1a;看点、厂商与创新亮点 1.背景2.核心看点&#xff1a;技术突破与场景创新2.1 eVTOL&#xff08;飞行汽车&#xff09;的规模化展示2.2 智能无人机与无人值守平台2.3 新材料与核心零部件革新2.4 动态演示与赛事活动 3.头部无人机厂商4.核…

【Jitsi Meet】(腾讯会议的平替)Docker安装Jitsi Meet指南-使用内网IP访问

Docker安装Jitsi Meet指南-使用内网IP访问 下载官方代码配置环境变量复制示例环境文件并修改配置&#xff1a;编辑 .env 文件&#xff1a; 修改 docker-compose.yml 文件生成自签名证书启动服务最终验证 腾讯会议的平替。我们是每天开早晚会的&#xff0c;都是使用腾讯会议。腾…

使用Spring Boot和Spring Security结合JWT实现安全的RESTful API

使用Spring Boot和Spring Security结合JWT实现安全的RESTful API 引言 在现代Web应用中&#xff0c;安全性是至关重要的。Spring Boot和Spring Security提供了强大的工具来保护我们的应用程序&#xff0c;而JWT&#xff08;JSON Web Token&#xff09;则是一种轻量级的认证和…

对神经正切核的理解和推导(1)

声明&#xff1a; 本文是对Neural Tangent Kernel &#xff08;NTK&#xff09;基础推导 - Gearlesskai - 博客园文章内容的理解与推导&#xff0c;里面很多东西对我这种新手来说不太好理解&#xff0c;所以我力求通过这种方式理解文章的大部分内容。希望我的理解可以帮助你更…

基于 STC89C52 的养殖场智能温控系统设计与实现

摘要 本文提出一种基于 STC89C52 单片机的养殖场环境温度智能控制系统,通过集成高精度温度传感器、智能执行机构及人机交互模块,实现对养殖环境的实时监测与自动调控。系统具备温度阈值设定、超限报警及多模式控制功能,可有效提升养殖环境稳定性,降低能耗与人工成本。 一…

微信小程序调试

一、寻找答案 1. 创建小程序 https://zhuanlan.zhihu.com/p/1906013675883561860 2. 若有后端接口&#xff0c;需要调试 https://blog.csdn.net/animatecat/article/details/126949749 3. 比较细教程, 搭建修改配置 https://zhuanlan.zhihu.com/p/1893281527112136235 4. 查找…

使用DeepSeek实现数据处理

一、核心能力全景图 Ctrl+/ 唤醒智能助手,支持以下数据处理场景: 🧹 数据清洗与预处理📈 统计分析与可视化🤖 机器学习建模🚀 大数据性能优化📊 自动化报告生成⚡ 实时流数据处理二、高频场景实战(附魔法口令) 场景1:数据清洗自动化(Python示例) 口令: 处…