欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 文化 > 黑马点评-乐观锁/悲观锁/synchronized/@Transactional

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

2025/6/6 15:30:55 来源:https://blog.csdn.net/Ggjvhsjsj/article/details/148192455  浏览:    关键词:黑马点评-乐观锁/悲观锁/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<

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词