【高并发】超卖一人一单问题
创始人
2024-03-25 11:50:44
0

一、超卖问题

1. 超卖场景

高并发场景下用户下单,存在如下所示的超卖问题,其产生的主要原因是一个线程刚读出库存值,还没进行修改时,另一个线程也读出来该库存值,从而导致这两个线程在进行下单时,对同一个值减了1。

在这里插入图片描述

2. 解决方案

1. 悲观锁

认为线程安全问题一定会发生,线程串行执行

  • 优点:简单粗暴
  • 缺点:性能一般

2. 乐观锁

认为线程安全问题不一定会发生,在更新数据时判断有没有线程对数据做了修改

  • 优点:性能好
  • 缺点:存在成功率低的问题,多个线程同时访问,查询到同一个值,只要有一个线程进行了修改,其他的线程就都失败了

在这里插入图片描述

3. 乐观锁的实现

难点在于判断数据是否被修改过,判断方式有:

1. 版本号法

给数据加一个版本号
在这里插入图片描述

在这里插入图片描述

先查询到版本号和库存,更新的时候判断版本号和查询时的版本号是否相同,不相同说明这段时间库存已经被更新过了,此时更新库存失败。

2. CAS 法

库存和版本号执行相同操作,用数据本身是否有变化进行判断。先查询库存数据的值,更新时再查一遍看看库存数据有没有变化,有变化就不更新了。

在这里插入图片描述

使用 CAS 法 解决超卖问题
        // 5. 扣除库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).eq("stock", voucher.getStock()) // 乐观锁,保证当前线程执行的时候没有其他线程修改过库存.update();

解决失败率高的问题: 只要库存大于 0 就卖

        // 5. 扣除库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0) // 对乐观锁进行改进,只要防止出现负数就行.update();

二、一人一单

1. 场景

只容许一人下一单,要对单个用户访问的高并发情况加锁

2. 基于悲观锁的实现

    @Overridepublic Result seckillVoucher(Long voucherId) {// 1. 查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2. 判断秒杀是否开始if(voucher.getBeginTime().isAfter(LocalDateTime.now())){return Result.fail("秒杀还未开始");}// 3. 判断秒杀是否已经结束if(voucher.getEndTime().isBefore(LocalDateTime.now())){return Result.fail("秒杀已经结束");}// 4. 判断库存是否充足if(voucher.getStock() < 1){return Result.fail("库存不足");}// 线程安全 —— 先获取锁,完成事务,释放锁Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) { // 对用户 id 加锁,保证字符串值相同就会被锁定IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy(); // 拿到当前对象的代理对象return proxy.createVoucherOrder(voucherId); // 当前没有事务功能,代理对象才有事务功能}}@Transactional// 实现一人一单 —— 悲观锁 —— 以用户 id 加锁 —— 处理同一个用户的并发安全问题,防止一个用户并发买很多单public Result createVoucherOrder(Long voucherId){Long userId = UserHolder.getUser().getId();// 查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if (count > 0) {return Result.fail("您已经买过一次了,不能再买了");}// 5. 扣除库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId)//.eq("stock", voucher.getStock()) // 乐观锁,保证当前线程执行的时候没有其他线程修改过库存.gt("stock", 0) // 对乐观锁进行改进,只要防止出现负数就行.update();if (!success) {return Result.fail("库存不足");}// 6. 创建订单VoucherOrder voucherOrder = new VoucherOrder();// 订单的参数 —— 订单 id \ 代金券 id \ 用户 id// 生成唯一 idlong id = redisIdWorker.nextId("order");voucherOrder.setId(id);voucherOrder.setVoucherId(voucherId);voucherOrder.setUserId(1L);save(voucherOrder);return Result.ok();}

导入依赖:

        org.aspectjaspectjweaver

加入允许暴露代理对象的依赖:

@EnableAspectJAutoProxy(exposeProxy = true) // 表示开启暴露代理对象
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {public static void main(String[] args) {SpringApplication.run(HmDianPingApplication.class, args);}}

相关内容

热门资讯

每周股票复盘:广电网络(600... 截至2025年12月26日收盘,广电网络(600831)报收于4.2元,较上周的4.36元下跌3.6...
每周股票复盘:新疆火炬(603... 截至2025年12月26日收盘,新疆火炬(603080)报收于22.85元,较上周的22.73元上涨...
每周股票复盘:瀚川智能(688... 截至2025年12月26日收盘,瀚川智能(688022)报收于15.3元,较上周的14.42元上涨6...
每周股票复盘:中粮糖业(600... 截至2025年12月26日收盘,中粮糖业(600737)报收于17.27元,较上周的17.18元上涨...
每周股票复盘:马钢股份(600... 截至2025年12月26日收盘,马钢股份(600808)报收于4.22元,较上周的3.82元上涨10...
每周股票复盘:内蒙一机(600... 截至2025年12月26日收盘,内蒙一机(600967)报收于16.34元,较上周的16.05元上涨...
富达基金投顾业务负责人戴旻:封... 由三亚市人民政府主办,《财经》杂志、财经网、《财经智库》、三亚中央商务区管理局、三亚经济研究院承办的...
海南大谷国际园区董事长张焱:以... 由三亚市人民政府主办,《财经》杂志、财经网、《财经智库》、三亚中央商务区管理局、三亚经济研究院承办的...
每周股票复盘:汉马科技(600... 截至2025年12月26日收盘,汉马科技(600375)报收于5.87元,较上周的5.92元下跌0....
每周股票复盘:中天科技(600... 截至2025年12月26日收盘,中天科技(600522)报收于18.4元,较上周的18.37元上涨0...