Redis实战案例12-添加秒杀券实现秒杀下单及相关问题解决

news2024/11/14 21:47:11

1. 添加优惠券

在这里插入图片描述

该项目没有后台管理的界面,所以采用postman发送请求

http://localhost:8081/voucher/seckill

注意end时间要大于当前系统时间

{
  "shopId": 2,
  "title": "100元代金券",
  "subTitle": "周一至周五均可使用",
  "rules": "全场通用\\n无需预约\\n可无限叠加\\不兑现、不找零\\n仅限堂食",
  "payValue": 8000,
  "actualValue": 10000,
  "type": 1,
  "stock": 100,
  "beginTime": "2023-07-05T10:09:17",
  "endTime": "2023-07-05T23:09:04"
}

在这里插入图片描述

2. 用户实现秒杀下单

在这里插入图片描述
在这里插入图片描述

controller层

@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
    @Resource
    private IVoucherOrderService voucherOrderService;

    @PostMapping("seckill/{id}")
    public Result seckillVoucher(@PathVariable("id") Long voucherId) {
        return voucherOrderService.seckillVoucher(voucherId);
    }
}

service层

public interface IVoucherOrderService extends IService<VoucherOrder> {

    Result seckillVoucher(Long voucherId);
}

实现类

@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. 查询优惠券(MybatisPlus运用)
        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. 扣减库存(MybatisPlus运用)
        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);
        //7. 返回订单id
        return Result.ok(orderId);
    }
}

3. 问题分析和优化

超卖问题
多线程并发安全问题,在之前代码的基础上采用JMeter测试,发现出现超卖的情况,库存出现了负数;

正常的线程执行,顺序执行不会出现问题;
在这里插入图片描述
但是线程的执行是并发抢占式的,所以很可能会变成交叉执行,如下图的情况;
只要在线程1进行数据库操作之前,有线程已经做了查询的操作,那就存在超卖的风险;

boolean success = seckillVoucherService.update()
                            .setSql("stock = stock - 1")
                            .eq("voucher_id", voucherId).update();

在这里插入图片描述

最直观解决方案: 加锁,有两种锁,悲观锁和乐观锁;
在这里插入图片描述
在这里插入图片描述

对版本号法的一种简化,stock和version都要做相同的判断,那就直接简化为判断stock即可,只要在做修改的时候判断stock是否和自己查询的stock相同就行;

在这里插入图片描述

采用CAS法对代码进行优化

//5. 扣减库存(MybatisPlus运用)
boolean success = seckillVoucherService.update()
        .setSql("stock = stock - 1")// set stock = stock - 1
        .eq("voucher_id", voucherId).eq("stock", voucher.getStock())// where id = ? and stock = ?
        .update();

重点: 采用JMeter测试,发现没有出现超卖的情况,但是出现了很多优惠券卖不出的情况,可以极端假设有100个线程同时查询完了库存,线程1进行数据库的修改之后,stock的值变成了99,此时另外99个线程进行对比的时候会发现stock的值与它们之前查询的stock的值不相同,此时就会走下面代码;

if(!success){
    // 扣减失败
    return Result.fail("已经抢完啦~");
}

修改的方案变成stock>0而不是判断之前的stock和现在stock是否相等;

 boolean success = seckillVoucherService.update()
         .setSql("stock = stock - 1")// set stock = stock - 1
         .eq("voucher_id", voucherId).gt("stock", 0)// where id = ? and stock = ?
         .update();

在这里插入图片描述

4. 一人一单问题

在这里插入图片描述

关键点
在实现一人一单的过程中,锁对象如果采用下面的代码,会出现问题,因为toString()方法的源码中产生的都是新的对象,所以不能这么写;
userId.toString().intern()方法可以确保返回的字符串对象是常量池中的对象,从而确保在多线程中使用相同的字符串对象。intern()方法会将字符串添加到常量池中(如果常量池中不存在该字符串),并返回常量池中的对象引用;
因此,改为synchronized(userId.toString().intern())来确保在多线程中使用相同的字符串对象。这样做可以避免不必要的锁竞争;
但是,intern()方法的使用可能会增加内存的消耗,因为它会将字符串对象添加到常量池中,尤其是当有大量不同的字符串被调用时。所以在使用intern()方法时应该谨慎使用,确保在适当的情况下使用它;

synchronized (userId.toString())
public static String toString(long i) {
        if (i == Long.MIN_VALUE)
            return "-9223372036854775808";
        int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
        char[] buf = new char[size];
        getChars(i, size, buf);
        return new String(buf, true);
    }

修改为

synchronized (userId.toString().intern())

关键点
Spring提交事务是在函数结束之后,也就意味着锁释放之后事务才会去做提交操作,此时其它线程可能会趁虚而入;
所以要将锁放在函数外边;
示例代码:

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

    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1. 查询优惠券(MybatisPlus运用)
        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("已经抢完啦~");
        }
        
        // 一人一单
        // 用户id
        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()) {
            return createVoucherOrder(voucherId);
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        // 一人一单
        // 用户id
        Long userId = UserHolder.getUser().getId();
        // 查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            return Result.fail("已经抢过咯~");
        }
        //5. 扣减库存(MybatisPlus运用)
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")// set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock", 0)// where id = ? and stock = ?
                .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
        voucherOrder.setUserId(userId);
        //6.3 代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //7. 返回订单id
        return Result.ok(orderId);
    }
}

关键点
建议补充知识点:事务失效的几种情况
事务管理问题:createVoucherOrder方法上使用了@Transactional注解,但是在seckillVoucher方法中调用该方法时,并未通过代理方式调用,因此事务并不会起作用;

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

其中的返回值也可以是

return this.createVoucherOrder(voucherId);

所以,这是一个内部方法调用,而不是通过代理对象调用。在这种情况下,Spring事务管理器无法正确地拦截该方法调用并应用事务;
所以我们需要获取代理对象;

// 一人一单
// 用户id
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
    // 获取代理对象
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
}

并且导入相关依赖

<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);
    }

}

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

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

相关文章

c++查漏补缺

c语言的struct只能包含变量&#xff0c;而c中的class除了包含变量&#xff0c;还可以包含函数。 通过结构体定义出来的变量还是变量&#xff0c;而通过类定义出来有了新的名称&#xff0c;叫做对象。C语言中&#xff0c;会将重复使用或具有某项功能的代码封装成一个函数&#x…

【剑指offer】8. 斐波那契数列(java)

文章目录 斐波那契数列描述输入描述&#xff1a;返回值描述&#xff1a;示例1示例2示例3思路非递归递归 完整代码 斐波那契数列 描述 大家都知道斐波那契数列&#xff0c;现在要求输入一个正整数 n &#xff0c;请你输出斐波那契数列的第 n 项。 斐波那契数列是一个满足 f …

PHP学生工作平台管理系统mysql数据库web结构apache计算机软件工程网页wamp

一、源码特点 PHP学生工作平台管理系统 是一套完善的web设计系统&#xff0c;对理解php编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为PHP APACHE&#xff0c;数据 库为mysql5.0&#xff0c;使用php语言开发…

linux 如何挂载fat32格式u盘,如何挂载NTFS 文件系统的硬盘

linux系统默认可以识别fat32u盘&#xff0c;对ntfs格式u盘不能识别 具体挂载方式如下 1、插入u盘 2、mkdir /mnt/usb 此命令用于创建挂载u盘的目录&#xff0c;只需创建一次就可以&#xff0c;若已经存在则不需要再次创建 3、fdisk -l 找到u盘路径 上图显示的sdb1,sdb2,sdb5…

Gradio,我们可以为我们的模型创建Web界面

Gradio是一个Python库&#xff0c;允许我们快速为机器学习模型创建可定制的接口。 使用Gradio&#xff0c;我们可以为我们的模型创建Web界面&#xff0c;而无需编写任何HTML&#xff0c;CSS或JavaScript。 Gradio旨在与广泛的机器学习框架配合使用&#xff0c;包括TensorFlow&a…

IOU发展历程学习记录

概述 IOU的出现主要最先运用在预测bbox框和target bbox框之间的重叠问题&#xff0c;为NMS提供相应的数值支撑。另外在bbox框的回归问题上&#xff0c;由于L1 Loss存在如下问题&#xff1a;当损失函数对x的导数为常数&#xff0c;在训练后期&#xff0c;x很小时&#xff0c;若…

GEE:基于MODIS土地覆盖类型“混交林”的净初级生产力(NPP)的区域统计

作者:CSDN @ _养乐多_ 本文将介绍如何使用Google Earth Engine(GEE)平台提取特定地区的净初级生产力(NPP)的统计信息,并在地图上可视化。通过加载MODIS数据集,并使用GEE提供的函数和方法,能够高效地计算特定地区的净初级生产力的平均值。 文章目录 一、代码详解二、代…

大模型的数据供血系统-向量数据库常识科普

1. 数据库行业有了新动向 对于传统数据库研发运维来说&#xff0c;数据库行业上次有概念创新&#xff0c;还是十几年前的NoSQL…… 在AI大行业发展的推进下&#xff0c;向量数据库成为了最新兴的数据库技术趋势&#xff0c;业内多家开源向量数据库都拿到了高额融资&#xff0c;…

《网络是怎样连接的》-户根勤

第一章&#xff1a;浏览器生成消息-探索浏览器内部 主要讲HTTP消息、DNS和委托协议栈发送消息。 第二章&#xff1a;用电信号传输TCP/IP数据-探索协议栈和网卡 主要讲套接字的创建、连接、通信、断开和删除四个阶段&#xff1b;IP与以太网的包收发阶段&#xff1b;UDP协议的收…

使用LocalThread获取当前线程的用户ID错误

说明&#xff1a;LocalThread是线程变量&#xff0c;可以往该线程变量中填充我们项目用户的ID&#xff0c;可以在其他的业务代码中直接获取&#xff0c;十分方便&#xff0c;详细参考&#xff1a;http://t.csdn.cn/k75rs LocalThread使用 第一步&#xff1a;创建类 创建一个…

北京市自动驾驶出行服务商业化试点启动,无人驾驶会是未来吗?

北京市高级级别自动驾驶示范区工作办公室公告称&#xff0c;智能网联乘用车“车内无人”商业化试点正式启动。根据最新修订的《北京市智能网联汽车政策先行区自动驾驶出行服务商业化试点管理细则&#xff08;试行&#xff09;》&#xff0c;企业在满足相关要求后&#xff0c;可…

如何用https协议支持小程序

步骤一&#xff1a;下载SSL证书 登录数字证书管理服务控制台。在左侧导航栏&#xff0c;单击SSL 证书。在SSL证书页面&#xff0c;定位到目标证书&#xff0c;在操作列&#xff0c;单击下载。 在服务器类型为Nginx的操作列&#xff0c;单击下载。 解压缩已下载的SSL证书压缩…

English Learning - L3 作业打卡 Lesson8 Day58 2023.7.3 周一

English Learning - L3 作业打卡 Lesson8 Day58 2023.7.3 周一 引言&#x1f349;句1: And this is when I learned that our borders and our obstacles can only do two things: one, stop us in our tracks or two, force us to get creative.成分划分弱读连读爆破语调 &…

无线基站与无线频谱资源

文章目录 基站的主要组成天线馈线&#xff08;电缆线&#xff09;RRU&#xff08;射频拉远单元&#xff0c;Remote Radio Unit&#xff09;BBU&#xff08;室内基带处理单元&#xff0c;Building Base band Unit&#xff09;AAU&#xff08;有源天线单元&#xff0c;Active Ant…

Summer test

目录 第一个只出现一次的字符判定字符是否唯一 第一个只出现一次的字符 原题链接&#xff1a;第一个只出现一次的字符 int FirstNotRepeatingChar(char* str ) {int arr[200] {0};int len strlen(str);int i0;for(i0;i<len;i){arr[str[i]];}for(i0;i<len;i){if(arr[s…

[ABC218G] Game on Tree 2 树上游戏

[ABC218G] Game on Tree 2 树上游戏 文章目录 [ABC218G] Game on Tree 2 树上游戏题面翻译输入格式输出格式样例 #1样例输入 #1样例输出 #1 样例 #2样例输入 #2样例输出 #2 样例 #3样例输入 #3样例输出 #3 题目大意分析水法code 正解code 题面翻译 给定一棵树&#xff0c;以及…

leetcode 106. 从中序与后序遍历序列构造二叉树

2023.7.8 让我很难受的一道题&#xff0c;个人感觉难度不止中等。 首先要知道的是知道了前序/后序 中序 之后&#xff0c;是可以构造出相应且唯一的二叉树的。 本道题的思路通过递归的方式根据中序遍历数组和后序遍历数组构建二叉树&#xff0c;并返回根节点。递归的结束条…

【通览一百个大模型】Anthropic LLM(Anthropic)

【通览一百个大模型】Anthropic LLM&#xff08;Anthropic&#xff09; 作者&#xff1a;王嘉宁&#xff0c;本文章内容为原创&#xff0c;仓库链接&#xff1a;https://github.com/wjn1996/LLMs-NLP-Algo 订阅专栏【大模型&NLP&算法】可获得博主多年积累的全部NLP、大…

Ubuntu安装VMtools实现与主机之间复制粘贴

目录 一、安装 VMware Tools 二、Ubuntu命令 一、安装 VMware Tools 右键点击你创建的系统&#xff0c;然后出现菜单下滑找到安装 VMware Tools&#xff08;T&#xff09; 这个点击安装&#xff1b; 右键点击你创建的系统&#xff0c;然后出现菜单下滑找到设置; 然后弹出虚…

USB转串口那些事儿—电源与防倒灌设计

USB转串口芯片和串口负载&#xff08;MCU、CPU、其他串口外设等&#xff09;的供电方式可以分为2个大类&#xff1a;统一供电和独立供电。 一、供电说明 统一供电是指USB芯片和串口负载使用同一电源&#xff0c;上下电同步&#xff0c;此时不会存在彼此之间电流倒灌的问题。 …