基于redis实现优惠劵秒杀下单功能(结合黑马视频总结)

news2024/9/20 22:40:41

 基础业务逻辑

 初步实现

@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("库存不足!");
    }
    //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);
 
}

当多个线程同时进行时可能出现超卖问题,同一个用户可能出现重复下单问题

可以使用乐观锁或者悲观锁解决

 乐观锁又分为两种版本号法和CAS法

在每次扣减库存时对版本号也进行修改,假设线程1来查询库存和版本号,都为1,接着判断库存是否大于0,判断成立,执行扣减语句,set stock = stock - 1 ,version = version + 1 where id= 10 and version = 1;如果在执行这条sql语句时version依旧 = 1也就说明和查询到的version一样,没有人修改过版本号,也就没有人扣减库存,执行完后 stock = 0,version = 2. 假设线程2在线程1查询完之后也来查询库存和版本号此时stock = 1,version = 1.此时线程1 还未执行扣减库存操作,所以stock和version都没变,线程2进行stock判断 大于0执行扣减库存操作,,set stock = stock - 1 ,version = version + 1 where id= 10 and version = 1.此时这条sql语句执行失败,因为version已经被线程1修改为2所以执行失败,不会扣减库存 。

用stock修改变化代替version,因为每次查询version时也会查询stock,每次更新version时也会修改stock。只需要将版本号法中的version变为stock就可以

只需要将上面扣减库存代码做以下修改就可,如果使用方案一会发现失败率很大,从业务逻辑上想,不需要使查询的stock完全一样,只需要满足stock>0就可以


        //扣减库存
        //修改方案一 失败率太大
         boolean updated = seckillVoucherService.update().
         setSql("stock = stock - 1") //set stock = stock - 1
         .eq("voucher_id", voucherId).eq("stock", voucher.getStock()) //where voucher_id = ? and stock = ?
         .update();

        //修改方案二
        boolean update = seckillVoucherService.update()
            .setSql("stock= stock -1")
            .eq("voucher_id", voucherId).update().gt("stock",0); //where id = ? and stock > 0

一人一单业务逻辑

初步实现

@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("库存不足!");
    }
    // 5.一人一单逻辑
    // 5.1.用户id
    Long userId = UserHolder.getUser().getId();
    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")
            .eq("voucher_id", voucherId).update();
    if (!success) {
        //扣减库存
        return Result.fail("库存不足!");
    }
    //7.创建订单
    VoucherOrder voucherOrder = new VoucherOrder();
    // 7.1.订单id
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
 
    voucherOrder.setUserId(userId);
    // 7.3.代金券id
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);
 
    return Result.ok(orderId);
 
}

 我们将一人一单和创建订单的代码统一抽取成一个方法createVoucherOrder并且加锁,这样就可以初步实现一人一单。

@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {
 
    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);
}

 但是将锁加在方法上锁的粒度太大,会导致效率低,我们的目的是一个用户只能下一单,所以我们的锁对象是userid就可以。如果在方法内部加锁的话还有一个问题,就是锁释放了,但是事务还没提交新创建的订单可能还未写入数据库这就导致其它线程又可以进来执行查询订单操作可能没有查询到又会创建订单,导致线程安全问题,所以我们要将整个函数锁起来 ,确保事务提交后锁才会释放。

@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()){
        //获取代理对象(事务)
        IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
    return proxy.createVoucherOrder(orderId);
}
}

toString底层是会创建一个新的对象返回,所以我们要调用inter()方法拿到线程池中的那个原始对象,否则即使是同一个userid每次也会返回一个新的对象。

如果直接调用creatVoucherOrder方法时本质是this在调用,spring只有通过代理对象调用带有@Transactional的方法时事务才会生效,所以我们需要通过上下文来获取当前对象的代理对象Spring 中只有通过代理对象调用带有 @Transactional 注解的方法时,事务才会生效。这是因为 Spring 实现声明式事务管理主要依赖于面向切面编程(AOP)机制,特别是动态代理技术。当一个类的方法被标记为 @Transactional,Spring 不会在原始类实例上直接添加事务行为,而是创建一个代理对象来包裹原始类实例。这个代理对象负责在方法调用前后插入必要的事务管理逻辑,如开启事务、提交事务或回滚事务。

以下是几个关键点说明为何必须通过代理对象调用:

代理对象的作用:

代理对象是对原始对象(即实现了 @Transactional 方法的服务类实例)的包装,它继承或实现与原始对象相同的接口或类。

当外部代码通过代理对象调用方法时,实际上是调用了代理对象上的方法。代理对象的方法内部会先执行与事务相关的前置处理(如开启事务),然后调用原始对象对应方法的实际逻辑,最后执行与事务相关的后置处理(如根据方法执行结果决定提交或回滚事务)。

直接调用的问题:

如果在服务类内部(即同一个类中)的一个方法直接调用另一个被 @Transactional 注解的方法,由于这种调用不经过代理对象,所以不会触发事务管理逻辑。Spring AOP 是基于方法调用的切面编织,同一类内部方法调用属于“自我调用”,不会触发代理方法的介入。

若要解决此类问题,可以采用以下策略之一:

将事务方法移动到另一个类中,使得方法间调用成为不同类之间的调用,从而可以通过代理对象调用。

如代码所示,主动获取当前代理对象并使用代理对象来调用事务方法,确保事务逻辑得以执行。

因此,为了确保 @Transactional 注解的有效性,应确保对事务方法的调用是通过 Spring 创建的代理对象进行的。这样,Spring 才能正确地应用事务边界管理和相应的事务行为。

要想获取当前类的代理对象需要在启动类中添加下面的注解让这个代理类暴露,我们才能获取到

@EnableAspectJAutoProxy(exposeProxy = true)
       <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.7</version>
        </dependency>

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

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

相关文章

YOLOv8改进 | 注意力机制 | 反向残差注意力机制【内含创新技巧思维】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 专栏目录 &#xff1a;《YOLOv8改进有效…

【Docker系列】Docker 镜像管理:删除无标签镜像的技巧

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

【Webpack 踩坑】CSS加载缓慢

问题&#xff1a;使用webpack5&#xff0c;单独index.scss在assets/css目录下&#xff0c;但是不管是production还是development环境下&#xff0c;都会出现dom加载完后再渲染样式 本意是想要将样式单独打包到一个文件夹&#xff0c;还有压缩css 于是用了mini-css-extract-plug…

【LeetCode】219.存在重复元素II

1. 题目 2. 分析 3. 代码 class Solution:def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:num2index defaultdict(list)for idx,num in enumerate(nums):num2index[num].append(idx)for key, val in num2index.items():if len(val) > 2:for i i…

书生大模型实战营--L1关卡-XTuner 微调个人小助手认知

一、为什么要做模型微调&#xff0c;有些场景下大模型无法更好的回答用户问题 二、准备模型以及训练语料 准备工作详细参考&#xff0c;这里有很详细的介绍 https://github.com/InternLM/Tutorial/blob/camp3/docs/L1/XTuner/readme.md 三、微调模型后的回答&#xff0c;符合…

python 爬虫入门实战——爬取维基百科“百科全书”词条页面内链

1. 简述 本次爬取维基百科“百科全书”词条页面内链&#xff0c;仅发送一次请求&#xff0c;获取一个 html 页面&#xff0c;同时不包含应对反爬虫的知识&#xff0c;仅包含最基础的网页爬取、数据清洗、存储为 csv 文件。 爬取网址 url 为 “https://zh.wikipedia.org/wiki/…

历届奥运会奖牌数据(1896年-2024年7月)

奥运会&#xff0c;全称奥林匹克运动会&#xff08;Olympic Games&#xff09;&#xff0c;是国际奥林匹克委员会主办的世界规模最大的综合性体育赛事&#xff0c;每四年一届&#xff0c;会期不超过16天。这项历史悠久的赛事起源于古希腊&#xff0c;现代奥运会则始于1896年的希…

opencascade AIS_ViewCube源码学习小方块

opencascade AIS_ViewCube 小方块 前言 用于显示视图操控立方体的交互对象。 视图立方体由多个部分组成&#xff0c;负责不同的相机操作&#xff1a; 立方体的各个面代表主视图&#xff1a;顶部、底部、左侧、右侧、前侧和后侧。 边表示主视图之一的旋转45度。 顶点表示主视…

3143. 正方形中的最多点数 Medium

给你一个二维数组 points 和一个字符串 s &#xff0c;其中 points[i] 表示第 i 个点的坐标&#xff0c;s[i] 表示第 i 个点的 标签 。 如果一个正方形的中心在 (0, 0) &#xff0c;所有边都平行于坐标轴&#xff0c;且正方形内 不 存在标签相同的两个点&#xff0c;那么我们称…

ChatGPT协助撰写研究论文的11种方法【全集】

学境思源&#xff0c;一键生成论文初稿&#xff1a; AcademicIdeas - 学境思源AI论文写作 当我们使用 ChatGPT 时&#xff0c;原本那些需要花费数小时、数天、有时甚至更长时间的任务现在只需几分钟甚至更短时间。 今天的分享&#xff0c;我们将谈谈 ChatGPT 在研究论文方面可…

右键空白处自定义菜单

效果 建立二级菜单 winr输入 regedit 路径复制到地址栏&#xff0c;回车即可定位。 计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\CommandStore\shell\如果没有地址栏 在shell上右键&#xff0c;新建项,名字随意但最好是英文&#xff08;…

免费开源!PDF加盖骑缝章小工具PDFQFZ

PDFQFZ是一款免费开源的PDF加盖骑缝章小工具&#xff0c;主要用于给多页合同等PDF文件添加骑缝章&#xff0c;以达到防伪效果。该工具支持对PDF文件或文件夹进行随机分割、印章设定、骑缝章等操作&#xff0c;生成加盖骑缝章的PDF文件。 主要功能&#xff1a; 支持加密PDF文件…

Python安装教程(Window环境)

1 Python安装 1.1 Python官网下载 1&#xff09;在Python官网选择Python版本&#xff08;这里选择Python 3.12.4&#xff09;&#xff0c;点击对应的【Download】按钮下载。2&#xff09;根电脑的操作系统类型选择&#xff08;这里电脑64位操作系统&#xff09;&#xff0c;点…

【商品支付】漏洞详解!

产生原因&#xff1a; 订单金额的验证:价格未保存在数据库中未校验商品价格与数据库中是否匹配 不安全的传输&#xff1a; 订单相关信息未进行加密传输(话说你加密的地方一定是重要地方&#xff0c;你不加密&#xff0c;你就是有病&#xff0c;哈哈哈) 验证规…

(超全)Kubernetes 的核心组件解析

引言 在现代软件开发和运维的世界中&#xff0c;容器化技术已经成为一种标志性的解决方案&#xff0c;它为应用的构建、部署和管理提供了前所未有的灵活性和效率。然而&#xff0c;随着应用规模的扩大和复杂性的增加&#xff0c;单纯依靠容器本身来管理这些应用和服务已不再足够…

gitlab 服务器安装

阿里云盘快传 百度链接 链接&#xff1a;https://pan.baidu.com/s/1Gn5bWHi45Dcpe1RH1S06dw 提取码&#xff1a;yai2 然后就是有一台服务器 cd /mkdir gitlab上传下载好的东西rpm -ivh gitlab-ce-10.8.4-ce.0.el7.x86_64.rpm 这里可以tab提示vim /etc/gitlab/gitlab.rb我建议…

【JavaEE精炼宝库】 网络编程套接字——UDP业务逻辑 | TCP流套接字编程及业务逻辑实现

文章目录 一、UDP业务逻辑实现二、TCP流套接字编程2.1 API 介绍&#xff1a;2.1.1 ServerSocket&#xff1a;2.1.2 Socket&#xff1a; 2.2 Java流套接字通信模型&#xff1a;2.3 代码示例&#xff1a;2.3.1 TCP Echo Server&#xff1a;2.3.2 TCP Echo Client&#xff1a;2.3.…

【刷题汇总 -- 爱吃素、相差不超过k的最多数、最长公共子序列(一)】

C日常刷题积累 今日刷题汇总 - day0261、爱吃素1.1、题目1.2、思路1.3、程序实现 2、相差不超过k的最多数2.1、题目2.2、思路2.3、程序实现 -- 滑动窗口 3、最长公共子序列(一)3.1、题目3.2、思路3.3、程序实现 -- dp 4、题目链接 今日刷题汇总 - day026 1、爱吃素 1.1、题目…

自写Json转换工具

前面写了简单的API测试工具ApiTools&#xff0c;返回的json有时需要做很多转换&#xff0c;于是开发了这个工具。 功能包括 1、json字符串转为表格&#xff0c;可以直观的展示&#xff0c;也可以复制&#xff0c;并支持转换后的表格点击列头进行排序&#xff0c;比较方便地定…

鸿蒙系统开发【应用接续】基本功能

应用接续 介绍 基于ArkTS扩展的声明式开发范式编程语言编写的一个分布式视频播放器&#xff0c;主要包括一个直播视频播放界面&#xff0c;实现视频播放时可以从一台设备迁移到另一台设备继续运行&#xff0c;来选择更合适的设备继续执行播放功能以及PAD视频播放时协同调用手…