黑马点评-实现安全秒杀优惠券(使并发一人一单,防止并发超卖)

news2025/6/9 17:03:14

一.实现优惠券秒杀

1.最原始代码:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional
    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("库存不足!");
        }
        //5,扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId).update();
        if (!success) {
            //扣减库存
            return Result.fail("库存不足!");
        }
        //6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 6.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 6.2.用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        // 6.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder); // 写入数据库

        return Result.ok(orderId);

    }
}

出现的问题:在高并法的情况下存在多买的情况,导致商品成为负数 

代码出现问题的原理图:

2.使用乐观锁解决超卖的问题

在修改时,查看之前查出来的数据和现在的数据库里面的优惠券的数量是否相同,相同就表示期间没有修改,则我们可以执行减库存的操作。

代码修改: 

// 把上面的sql语句改成这个
boolean success = seckillVoucherService.update()
      .setSql("stock= stock -1")
      .eq("voucher_id", voucherId)
      .eq("stock",voucher.getStock()) //  添加乐观锁,但是成功率太低
      .update();

出现的问题:200个用户抢100个优惠券这100个优惠券还有剩余。 

3.优化乐观锁

在修改时,其实我们只用查看库存剩余的券的数量是否大于0就可以执行减库存了,不需要向上面一样写的这么严格。

 代码修改:

// 把上面的sql语句改成这个
boolean success = seckillVoucherService.update()
      .setSql("stock= stock -1")
      .eq("voucher_id", voucherId)
      .gt("stock",0) //  优化乐观锁
      .update();

二.实现一人一单

1.有问题的代码

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional
    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("库存不足!");
        }
        //5,扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId)
                .eq("stock",voucher.getStock()) //  添加乐观锁,但是成功率太低
                .update();
        if (!success) {
            //扣减库存
            return Result.fail("库存不足!");
        }
        // 6. 一人一单
        Long userId = UserHolder.getUser().getId();
        // 6.1. 查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 6.2. 判断是否存在
        if(count > 0){
            // 用户已经购买过了
            return Result.fail("用户已经购买过一次!");
        }
        //7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.用户id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder); // 写入数据库

        return Result.ok(orderId);

    }
}

出现的问题:在高并发的情况下还是会出现一个人可以下多单

出现问题的原理图:

2.使用悲观锁:

更改步骤5之后的逻辑,添加synchronized

@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {

        // 5. 一人一单
	    Long userId = UserHolder.getUser().getId();
        // 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.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.用户id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        // 7.返回订单id
        return Result.ok(orderId);
}

但是这样添加锁,锁的粒度太粗了,在使用锁过程中,控制 锁粒度 是一个非常重要的事情,因为如果锁的粒度太大,会导致每个线程进来都会锁住,所以我们需要去控制锁的粒度。

3.优化悲观锁的使用

思路:我们对用户的id进行上锁。但是,注意这里toString.intern,这里和tostring的底层原理有关使用了这个相当于创建了一个新的对象,所以同一id不会被锁住,加了intern就会去string的常量池里面去找,就不会创建新的了

@Transactional
public  Result createVoucherOrder(Long voucherId) {
	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.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.用户id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        // 7.返回订单id
        return Result.ok(orderId);
    }
}

4.再次优化 

但是这里我们还需要注意,这里是先释放锁在提交事务的,可能会出现问题,所以我们要把函数里面的锁去掉,在调用的地方加锁即可

Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
    return this.createVoucherOrder(voucherId);
}

5.继续优化,前面的修改会导致事务失效

所以我们要获取代理对象并用代理对象调用 createVoucherOrder 方法

关于事务失效可以查看这篇博客 点击进入

如果还是不清楚可以查看一人一单视频的最后一点 

synchronized (userId.toString().intern()) {
    // 获取代理对象(事务)
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
}
// 这里还需要在启动类上面添加这个注解,去暴露代理对象
@EnableAspectJAutoProxy(exposeProxy = true)

出现的问题:在多节点部署服务器的时候,还是会出现一人多单的问题,因为每个jvm的锁的监视器不同导致,无法锁住导致一人多单,这里就需要分布式锁

 出现问题的原理图:

6.使用分布式锁解决集群部署的并发问题

解决问题的原理图:

具体实现可以查看这篇博客  点击进入

7.使用异步的思路解决高并发

实现异步的思路的流程图:

① 新增秒杀优惠券的同时,将优惠券信息保存到Redis
// 改造添加优惠券代码
@Transactional
public void addSeckillVoucher(Voucher voucher) {
    // 保存优惠券
    save(voucher);
    // 保存秒杀信息
    SeckillVoucher seckillVoucher = new SeckillVoucher();
    seckillVoucher.setVoucherId(voucher.getId());
    seckillVoucher.setStock(voucher.getStock());
    seckillVoucher.setBeginTime(voucher.getBeginTime());
    seckillVoucher.setEndTime(voucher.getEndTime());
    seckillVoucherService.save(seckillVoucher);
    // 保存秒杀库存到Redis中
    stringRedisTemplate.opsForValue().set(RedisConstants.SECKILL_STOCK_KEY + voucher.getId(),voucher.getStock().toString());
}
② 基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功

lua脚本实现思路的流程图:

-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
local orderId = ARGV[3]

-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId

-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
public class VoucherOrderServiceImpl {
    private BlockingQueue<VoucherOrder> orderTasks =new ArrayBlockingQueue<>(1024 * 1024);
    private IVoucherOrderService proxy; // 保存这个类的代理对象
    
    public Result seckillVoucher(Long voucherId) {
        //获取用户
        Long userId = UserHolder.getUser().getId();
        // 1.执行lua脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString()
        );
        int r = result.intValue();
        // 2.判断结果是否为0
        if (r != 0) {
            // 2.1.不为0 ,代表没有购买资格
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        }
        // 2.2. 为0,有购买资格,把下单信息保存到阻塞队列
        VoucherOrder voucherOrder = new VoucherOrder();
        // 2.3.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 2.4.用户id
        voucherOrder.setUserId(userId);
        // 2.5.代金券id
        voucherOrder.setVoucherId(voucherId);
        // 2.6.放入阻塞队列
        orderTasks.add(voucherOrder);
        // 3.获取代理对象,保证后面线程池中使用代理对象去完成消费阻塞队列里面的消息
        proxy = (IVoucherOrderService)AopContext.currentProxy();
        // 4.返回订单id
        return Result.ok(orderId);
    }
}
④ 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
@Slf4j
public class VoucherOrderServiceImpl {
    //异步处理线程池
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
    private IVoucherOrderService proxy;

    @PostConstruct // 当前类初始化完毕就执行
    private void init() {
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    private class VoucherOrderHandler implements Runnable {
        public void run() {
            while (true) {  // 一直去阻塞队列获取消息
                try {
                    // 1.获取队列中的订单信息
                    VoucherOrder voucherOrder = orderTasks.take();
                    // 2.创建订单
                    handleVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                }
            }
        }
    }

    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        // 1.获取用户
        Long userId = voucherOrder.getUserId();
        // 2.创建锁对象
        RLock redisLock = redissonClient.getLock("lock:order:" + userId);
        // 3.尝试获取锁
        boolean isLock = redisLock.lock();
        // 4.判断是否获得锁成功
        if (!isLock) {
            // 获取锁失败,直接返回失败或者重试
            log.error("不允许重复下单!");
            return;
        }
        try {
            //注意:由于是spring的事务是放在threadLocal中,此时的是多线程,事务会失效
            proxy.createVoucherOrder(voucherOrder);
        } finally {
            // 释放锁
            redisLock.unlock();
        }
    }

    @Transactional
    public  void createVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).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", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣减失败
            log.error("库存不足");
            return ;
        }
        // 7.创建订单
        save(voucherOrder);

    }
}

 存在的问题:上面的阻塞队列在jvm中,会存在内存限制的问题,如果服务器宕机了,就会导致数据全部消失。

然后我们可以使用redis中的stream这个数据结构作为消息队列进行优化,后面优化的代码就不写了,因为要使用消息队列的话还是建议使用 rabbitmq 这种开源的消息队列组件就行了,这里我们只需要懂思想了。

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

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

相关文章

解决论文中字体未嵌入的问题

文章总览&#xff1a;YuanDaiMa2048博客文章总览 解决论文中字体未嵌入的问题 问题描述解决方案&#xff1a;使用 Adobe PDF 打印机嵌入字体&#xff08;WPS版&#xff09;步骤一&#xff1a;打开 PDF 文件步骤二&#xff1a;选择打印到 Adobe PDF步骤三&#xff1a;修改 Adobe…

leetcode 131. Palindrome Partitioning

目录 一、题目描述 二、方法1、回溯法每次暴力判断回文子串 三、方法2、动态规划回溯法 一、题目描述 分割回文子串 131. Palindrome Partitioning 二、方法1、回溯法每次暴力判断回文子串 class Solution {vector<vector<string>> res;vector<string>…

审计报告附注救星!实现Word表格纵向求和+横向计算及其对应的智能校验

在审计工作中&#xff0c;Word附注通常包含很多表格。为了确保附注数字的准确性&#xff0c;我们需要对这些表格进行数字逻辑校验&#xff0c;主要包含两个维度&#xff1a;在纵向上验证合计项金额是否正确&#xff1b;在横向上检查“年末金额年初金额本期增加-本期减少”的勾稽…

人工智能数学基础实验(四):最大似然估计的-AI 模型训练与参数优化

一、实验目的 理解最大似然估计&#xff08;MLE&#xff09;原理&#xff1a;掌握通过最大化数据出现概率估计模型参数的核心思想。实现 MLE 与 AI 模型结合&#xff1a;使用 MLE 手动估计朴素贝叶斯模型参数&#xff0c;并与 Scikit-learn 内置模型对比&#xff0c;深入理解参…

告别延迟!Ethernetip转modbustcp网关在熔炼车间监控的极速时代

熔炼车间热火朝天&#xff0c;巨大的热风炉发出隆隆的轰鸣声&#xff0c;我作为一名技术操控工&#xff0c;正密切关注着监控系统上跳动的各项参数。这套基于EtherNET/ip的监控系统&#xff0c;是我们车间数字化改造的核心&#xff0c;它将原本分散的控制单元整合在一起&#x…

Visual Studio Code插件离线安装指南:从市场获取并手动部署

Visual Studio Code插件离线安装指南&#xff1a;从市场获取并手动部署 一、场景背景二、操作步骤详解步骤1&#xff1a;访问官方插件市场步骤2&#xff1a;定位目标版本步骤3&#xff1a;提取关键参数步骤4&#xff1a;构造下载链接步骤5&#xff1a;下载与安装 三、注意事项 …

计算机视觉---YOLOv1

YOLOv1深度解析&#xff1a;单阶段目标检测的开山之作 一、YOLOv1概述 提出背景&#xff1a; 2016年由Joseph Redmon等人提出&#xff0c;全称"You Only Look Once"&#xff0c;首次将目标检测视为回归问题&#xff0c;开创单阶段&#xff08;One-Stage&#xff09…

爬虫核心概念与工作原理详解

爬虫核心概念与工作原理详解 1. 什么是网络爬虫&#xff1f; 网络爬虫&#xff08;Web Crawler&#xff09;是一种按照特定规则自动抓取互联网信息的程序或脚本&#xff0c;本质是模拟人类浏览器行为&#xff0c;通过HTTP请求获取网页数据并解析处理。 形象比喻&#xff1a;如…

vue3前端后端地址可配置方案

在开发vue3项目过程中&#xff0c;需要切换不同的服务器部署&#xff0c;代码中配置的服务需要可灵活配置&#xff0c;不随着run npm build把网址打包到代码资源中&#xff0c;不然每次切换都需要重新run npm build。需要一个配置文件可以修改服务地址&#xff0c;而打包的代码…

digitalworld.local: FALL靶场

digitalworld.local: FALL 来自 <digitalworld.local: FALL ~ VulnHub> 1&#xff0c;将两台虚拟机网络连接都改为NAT模式 2&#xff0c;攻击机上做namp局域网扫描发现靶机 nmap -sn 192.168.23.0/24 那么攻击机IP为192.168.23.182&#xff0c;靶场IP192.168.23.4 3&…

MySQL---库操作

mysql> create database if not exists kuku3; 1.库操作的语法 create database [if not exists] db_name [create_specification [, create_specification] ...] create_specification: [default] character set charset_name [default] collate collation_name详细解释…

动态规划算法:字符串类问题(2)公共串

0 前言 上节课我们已经讲述了使用动态规划求取回文串长度与数量的方法&#xff08;和本节课关系不大&#xff0c;感兴趣可以去看字符串类问题&#xff08;1&#xff09;回文串&#xff09;&#xff0c;这节课我们继续探索字符串问题中的动态规划问题。 进入本篇文章前&#x…

深度解析Vue项目Webpack打包分包策略 从基础配置到高级优化,全面掌握性能优化核心技巧

深度解析Vue项目Webpack打包分包策略 从基础配置到高级优化&#xff0c;全面掌握性能优化核心技巧 一、分包核心价值与基本原理 1.1 为什么需要分包 首屏加载优化&#xff1a;减少主包体积&#xff0c;提升TTI&#xff08;Time to Interactive&#xff09;缓存利用率提升&am…

spring-boot-starter-data-redis应用详解

一、依赖引入与基础配置 添加依赖 在 pom.xml 中引入 Spring Data Redis 的 Starter 依赖&#xff0c;默认使用 Lettuce 客户端&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis<…

5060显卡驱动PyCUDA开发环境搭建

5060显卡驱动PyCUDA开发环境搭建 本文手把手讲解了RTX5060ti显卡从上手尝试折腾&#xff0c;到在最新Ubuntu LTS版本上CUDA开发环境搭建成功的详细流程。 1.1 开机后Ubuntu2404LTS不识别显卡 1.1.1 显卡硬件规格要求 笔者下单的铭瑄电竞之心&#xff0c;安装规格是PCIe …

redis搭建最小的集群,3主3从

create.sh脚本用于快速部署一个Docker化的Redis集群。首先&#xff0c;脚本创建了一个自定义的Docker网络redis-net&#xff0c;并指定了子网以防止IP变动。接着&#xff0c;脚本设置了宿主机的公网IP&#xff0c;并生成了六个Redis节点的配置文件&#xff0c;每个配置文件都启…

【sylar-webserver】10 HTTP模块

HTTP 解析 这里使用 nodejs/http-parser 提供的 HTTP 解析器。 HTTP 常量定义 HttpMethod HttpStatus /* Request Methods */ #define HTTP_METHOD_MAP(XX) \XX(0, DELETE, DELETE) \XX(1, GET, GET) \XX(2, HEAD, HEAD) …

海康NVR录像回放SDK原始流转FLV视频流:基于Java的流媒体转码(无需安装第三方插件ffmpeg)

wlinker-video-monitor 代码地址&#xff1a;https://gitee.com/wlinker/wlinker-video-monitor 背景与需求 在安防监控、智能楼宇等场景中&#xff0c;海康威视设备作为行业主流硬件&#xff0c;常需要将录像回放功能集成到Web系统中。然而&#xff0c;海康设备的原始视频流…

运维Linux之Ansible详解学习(更新中)

什么是Ansible Ansible 是一款新出现的自动化运维工具&#xff0c;基于 Python 开发。以下是对它的详细介绍&#xff1a; 功能特点&#xff1a;集合了众多运维工具的优点&#xff0c;能实现批量系统配置、批量程序部署、批量运行命令等功能。它是基于模块工作的&#xff0c;本…

深入浅出IIC协议 - 从总线原理到FPGA实战开发 -- 第三篇:Verilog实现I2C Master核

第三篇&#xff1a;Verilog实现I2C Master核 副标题 &#xff1a;从零构建工业级I2C控制器——代码逐行解析与仿真实战 1. 架构设计 1.1 模块分层设计 三层架构 &#xff1a; 层级功能描述关键信号PHY层物理信号驱动与采样sda_oe, scl_oe控制层协议状态机与数据流控制state…