【高并发】超卖&一人一单问题

一、超卖问题

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. 基于悲观锁的实现

    @Override
    public 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
            // 生成唯一 id
            long id = redisIdWorker.nextId("order");
            voucherOrder.setId(id);
            voucherOrder.setVoucherId(voucherId);
            voucherOrder.setUserId(1L);
            save(voucherOrder);
            return Result.ok();
    }

导入依赖:

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

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

@EnableAspectJAutoProxy(exposeProxy = true) // 表示开启暴露代理对象
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {

    public static void main(String[] args) {
        SpringApplication.run(HmDianPingApplication.class, args);
    }

}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值