当前位置: 首页 > news >正文

黑马点评-乐观锁/悲观锁/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.xdnf.cn/news/631351.html

相关文章:

  • java刷题(6)
  • Netty学习专栏(三):Netty重要组件详解(Future、ByteBuf、Bootstrap)
  • RPG游戏设计战斗篇——战法牧协同作战体系研究
  • itextpdf根据模板生成pdf导出pdf遇到的问题
  • 【商业分析】充分了解“特性”和“功能”的区别,加强资源的聚焦度。
  • Java中的String的常用方法用法总结
  • Linux基础命令详解:touch、cat、more 的使用技巧与实战
  • Dynamics 365 简介
  • Python爬虫开发基础案例:构建可复用的名言采集系统
  • 【信息系统项目管理师】第24章:法律法规与标准规范 - 27个经典题目及详解
  • 力扣48 .旋转图像 (最简单的方法)
  • 【VBA 常用对象总结】掌握核心对象的属性和方法
  • [原创](计算机数学)(Introduction Linear Algebra)(P25): 为什么Cyclic Differences无法构成三维空间?
  • 无需会员可一键转换
  • Spring Security探索与应用
  • 《2.2.1顺序表的定义|精讲篇》
  • RK3588 buildroot QT 悬浮显示(OSD)
  • 大学生科创项目在线管理系统设计与实现
  • 数据库blog6_商业数据库下载知识
  • AI知识库
  • 【项目需求分析文档】:在线音乐播放器(Online-Music)
  • vFile文件的精读
  • NVMe高速传输之摆脱XDMA设计2
  • 【批量文件夹重命名】如何按照Excel表格对应的关系,批量一对一的重命名文件夹,文件夹按照对应映射关系一对一改名
  • 使用ps为图片添加水印
  • 常见实验室器材采购渠道分享
  • 《岁月深处的童真》
  • 基于python的百度迁徙迁入、迁出数据分析(城市版)
  • 滚珠导轨在航空航天领域具体应用是什么?
  • 如何优化 MySQL 存储过程的性能?