Redis的优惠券秒杀问题(六)超卖问题、一人一单问题

news2025/7/19 21:01:55

Redis的优惠券秒杀问题(六)超卖问题、一人一单问题 

超卖问题

问题描述

使用Jmeter进行压测 

发生超卖问题原因分析 

解决方案 

悲观锁与乐观锁

1. 版本号 

2. CAS法 

CAS三大问题(题外话!)

CAS三大问题的解决方案

代码实现 

一人一单问题

问题描述 

流程设计

解决方案 

代码实现 

代码中技术点分析 

1. intern()

2. 事务失效问题 currentProxy()


Redis的优惠券秒杀问题(六)超卖问题、一人一单问题 

超卖问题

问题描述

接上一篇文章。在上一篇文章中,我们实现了秒杀下单的业务,有请求过来,只要库存充足,就进行减库存,生成订单的操作。

但是这样子在多线程高并发的场景下,一定会出现问题。

使用Jmeter进行压测 

我们可以用 jmeter工具 复现一下场景,具体配置如下 

配置authorization 

运行程序,登入用户后,打开F12,获取authorization 的值

启动 Jmeter,进行压测,我们这里开了200个线程

我们这里设定有100个库存,所以正常的情况应该会有一半的线程(100个)的HTTP请求出现异常,但是这里显然不是! 

查看数据库 tb_sckill_voucher表

stock为负数超卖问题发生! 

订单表 tb_voucher_order表 也是如此 

发生超卖问题原因分析 

解决方案 

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁!!!

所以我们现在要研究是就是要加什么类型的锁?要怎么加锁?在哪里加锁?

悲观锁与乐观锁

悲观锁添加同步锁,让线程串行执行
  优点简单粗暴
  缺点性能一般
乐观锁不加锁,在更新时判断是否有其它线程在修改
  优点性能好
  缺点存在成功率低的问题

先提一下,并不是说有一种锁叫乐观锁、叫悲观锁,“悲观”、“乐观”只是用来形容一种思想,一种方式!!!

相较于悲观锁而言, 乐观锁机制采取了更加宽松的加锁机制。自然性能方面会优于悲观锁!

在这个问题中我们用乐观锁来解决!最常见的方式有两个“版本号”、“CAS”

1. 版本号 

但是,显然,在之前数据库设计的时候,没有version这个字段

我们如果想要用这个方案也可以,但是比较麻烦!

2. CAS法 

CAS是乐观锁的一种实现,CAS全称是比较和替换,CAS的操作主要由以下几个步骤组成

  1. 先查询原始值
  2. 操作时比较原始值是否修改
  3. 如果修改,则操作失败,禁止更新操作,如果没有发生修改,则更新为新值

伪代码 

do{
    备份旧数据;
    基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))

CAS三大问题(题外话!)

扯点别的,CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题:

  1. ABA问题
  2. 循环时间长开销大
  3. 只能保证一个共享变量的原子操作

CAS三大问题的解决方案

CAS的三个问题及解决方案_渣一个的博客-CSDN博客_业务层cas 解决死循环icon-default.png?t=M85Bhttps://blog.csdn.net/weichi7549/article/details/107734843

代码实现 

@Resource
private ISeckillVoucherService seckillVoucherService;

@Resource
private RedisIdWorker redisIdWorker;

@Override
public Result seckillVoucher(Long voucherId) {
    // 1. 查询优惠券
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    LocalDateTime nowTime = LocalDateTime.now();

    // 2. 判断秒杀是否开始
    if (nowTime.isBefore(voucher.getBeginTime())) {
        return Result.fail("活动未开始!");
    }

    // 3. 判断秒杀是否结束
    if (nowTime.isAfter(voucher.getEndTime())) {
        return Result.fail("活动已结束!");
    }

    // 4. 判断库存
    if (voucher.getStock() < 1) {
        return Result.fail("已买完!");
    }

    // 5. 减库存
    boolean success = seckillVoucherService.update()
            .setSql("stock = stock - 1")
            .eq("voucher_id", voucherId).gt("stock", 0)  // CAS方案(乐观锁)!
            .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);
}

核心是我们在减库存的时候,判断一下stock是否还大于0。

.gt("stock", 0) 

我们这边是只要库存是大于0的就都可以购买,所以对于这个点,乐观锁的判断可以适当放宽,只对库存为0时的“减库存”操作加锁,对应的SQL:

update tb_seckill_voucher 
set stock = stock - 1 
where voucher_id = 18 and stock > 0

一人一单问题

问题描述 

什么是一人一单问题?简单的来说就是模拟为了防止黄牛”屯“货而设计的,每一个用户ID,只能下一单!如下图,同一个用户下了很多单!!!

所以我们要修改秒杀业务,要求同一个优惠券,一个用户只能下一单 

流程设计

解决方案 

我们先获取一下用户的id,如果是相同的用户id“同时”执行到这里,只能允许一个进入该逻辑,执行减库存,生成订单的逻辑!其它的必须在此阻塞

代码实现 

@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. 查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        LocalDateTime nowTime = LocalDateTime.now();

        // 2. 判断秒杀是否开始
        if (nowTime.isBefore(voucher.getBeginTime())) {
            return Result.fail("活动未开始!");
        }

        // 3. 判断秒杀是否结束
        if (nowTime.isAfter(voucher.getEndTime())) {
            return Result.fail("活动已结束!");
        }

        // 4. 判断库存
        if (voucher.getStock() < 1) {
            return Result.fail("已买完!");
        }

        Long userId = UserHolder.getUser().getId();
        // 如果字符常量池中已经包含一个等于此String对象的字符串,则返回常量池中字符串的引用
        // 对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true
        synchronized (userId.toString().intern()) {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        // 一人一单
        Long userId = UserHolder.getUser().getId();

        Integer 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).gt("stock", 0)  // CAS方案(乐观锁)!
                .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);
        return Result.ok(orderId);
    }
}

代码中技术点分析 

1. intern()

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

intern() 方法返回字符串对象的规范化表示形式

对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true

String.intern()是一个Native方法,它的作用是:如果字符常量池中已经包含一个等于此String对象的字符串,则返回常量池中字符串的引用(不会新new一个),否则,将新的字符串放入常量池,并返回新字符串的引用。

在该业务场景中,会有多个线程会同时执行该逻辑。会生成多个userId,这里是要给相同的userId加锁!但是toString()方法会生成一个新的字符串对象

如果不使用 intern() ,尽管这些字符串的值相同,它们的内存地址也会不同!!!所以并不会把它们认定为相同的字符串!

2. 事务失效问题 currentProxy()

如果我们这里不生成其代理对象,则会导致事务失效

synchronized (userId.toString().intern()) {
    return createVoucherOrder(voucherId);
}

在Spring中,事务的实现方式,是对当前类(VoucherOrderServiceImpl)做了动态代理用其代理对象去做事务处理!

但是这里如果是上述代码, 实际上是this.createVoucherOrder(voucherId),这个this指的是VoucherOrderServiceImpl,是非代理对象,是没有事务功能!!!所以如果代码这样子写,@Transactional标注的事务会失效!!!

解决办法 

synchronized (userId.toString().intern()) {
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
}

获取到当前对象的代理对象 

AopContext.currentProxy() 

然后再用该代理对象来调用方法

proxy.createVoucherOrder(voucherId);  

这么改完之后还要在启动类上面加上注解,用来暴露代理对象

@EnableAspectJAutoProxy(exposeProxy = true)    // 默认是关闭的

@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/17313.html

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

相关文章

误差和梯度下降

Datawhale开源学习&#xff0c;机器学习课程&#xff0c;项目地址&#xff1a;https://github.com/datawhalechina/leeml-notes 之前讲了线性模型&#xff0c;提到了误差&#xff0c;那么误差来自哪里&#xff1f;本节内容将介绍「偏差」、「方差」对模型拟合度的影响&#xff…

西电计组II 实验1

西电计组II 实验1 文章目录西电计组II 实验18086汇编 IO操作环境搭建8086汇编 helloworldassumesegmentdb编译链接lstmapobjexesymdobint 21H 软件中断程序设计要求全局变量函数设计putchargetcharprintnewlineinputmemsetexithexbinarycircle程序入口完整代码8086汇编 IO操作 …

wav to image 的数据集制作代码

🍿*★,*:.☆欢迎您/$:*.★* 🍿 目录 背景 正文 总结 背景描述

python+django网吧会员管理系统

系统项目截图 本网吧管理系统主要包括三大功能模块&#xff0c;即管理员、会员、网管。 &#xff08;1&#xff09;管理员模块&#xff1a;首页、个人中心、会员管理、网管管理、商品类型管理、商品信息管理、购买商品信息管理、呼叫网管管理、电脑信息管理、用户上机管理、用户…

汇编语言外中断

外中断 文章目录外中断1.外中断概念2.PC机键盘的处理过程1.外中断概念 CPU在计算机系统中&#xff0c;除了能够执行指令&#xff0c;进行运算以外&#xff0c;还应该能够对外部设备进行控制&#xff0c;接收它们的输入&#xff0c;向它们进行输出&#xff08;I/O能力&#xff0…

如何把家装修出温馨的感觉?极家好不好

如何把家装修出温馨的感觉&#xff1f;极家好不好&#xff1f;想要让家变成理想的样子&#xff01;如何进行&#xff01; 第一步&#xff1a;找一个靠谱的装修团队&#xff0c;特别重要的是项目经理&#xff0c;极家好不好这个真的真的真的很重要‼️ 一个好的装修团队&#xf…

Windbg可以看到Visual Studio中看不到的有效函数调用堆栈

目录 1、Visual Studio中看不到有效的函数调用堆栈 2、使用Windbg调试运行主程序&#xff0c;看到了有效的函数调用堆栈 3、根据函数名和行号去查看对应的C源码&#xff0c;定位问题 4、总结 VC常用功能开发汇总&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持…

Frechet distance距离计算原理及python实现

Frechet distance概念 弗雷彻距离(Frechet distance)定义了一种考虑位置和点的次序的计算两条曲线相似度的方法&#xff0c;常用于时间序列相似度度量和轨迹序列相似度度量。该指标的算法可用the walking dog problem来描述&#xff1a;想象一个人用牵引带遛狗的同时&#xff0…

“碳”零排放是什么意思

在气候变化问题上拖拖拉拉之后&#xff0c;澳大利亚联邦政府终于承诺到 2050 年实现净零排放&#xff0c;世界其他大部分地区也是如此。几乎所有发达经济体现在都加强了 2030 年目标&#xff0c;并承诺在本十年将排放量大致减半。 需要记住的重要一点是&#xff0c;如果没有本…

abbyy finereader2023泰比文字识别PDF编辑软件

近年来&#xff0c;随着盲人数字阅读的普及推广&#xff0c;PDF格式的电子书越来越受到大家的关注和喜爱&#xff0c;但受读屏软件功能的限制&#xff0c;扫描版的PDF电子书是无法直接阅读的&#xff0c;这就需要将其转换为可阅读的文档格式&#xff0c;可对于大多数视障读者来…

Linux基本指令1

系统内核&#xff1a;Centos 7.6 64位操作系统(OS, operating system)是什么&#xff1f;世界上第一台计算机诞生的时候是没有操作系统的&#xff0c;但是这个计算机操作起来效率特别低&#xff0c;难度非常高。使用对象只有科学家。操作系统的意义就在于降低操作难度&#…

数据库高级 V

数据库高级 V 1.JVM内存结构,JVM调优,GC常用算法 如何调整堆内存大小,以及调整各年代之间的比例,更换GC 修改JVM堆大小方式: 找到Idea安装目录下的-->bin-->idea.exe.vmoptions -server -Xms128m //堆初始大小 -Xmx512m //最大堆内存 -XX:ReservedCodeCacheSize240m -XX…

[附源码]计算机毕业设计JAVA花卉销售管理系统

[附源码]计算机毕业设计JAVA花卉销售管理系统 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybati…

Linux umask命令详解,Linux修改文件默认访问权限

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 umask命令一、查看umask值二、临时修改umask值三、永久修改umask值四、文件和目录的默认权限五、权限数值对照表六、常用umask值及对应权限七…

最近公共祖先(朴素法、倍增法、(递归法))

目录 一、前言 二、题一&#xff1a;二叉树的最近公共祖先 1、上链接 2、基本思路 &#xff08;1&#xff09;朴素法 &#xff08;2&#xff09;LCA倍增法。 3、朴素法代码 &#xff08;1&#xff09;C&#xff08;AC&#xff09; &#xff08;2&#xff09;python&am…

1.2 极限的性质【极限】

1.2 极限的性质【极限】 1.2.1 唯一性 极限的唯一性 引入 假设警察逮捕罪犯&#xff0c;把犯人追到了悬崖边上&#xff0c;那么犯人只能在悬崖边束手就擒&#xff0c;这个时候悬崖边是犯人逃跑的极限位置&#xff0c;别无去处&#xff0c;位置唯一。 考试或比赛的时候都努…

web前端开发技术纯静态 (12306官网 1页)

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材&#xff0c;DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | 公司官网网站 | 企业官网 | 酒店官网 | 等网站的设计与制 | HTML期末大学生网页设计作业&#xff0c;Web大学生网页 HTML&#xff1a;结构 CSS&#…

WebDriverManager自动管理浏览器Driver包

WebDriverManager是什么&#xff1f; WebDriverManager是一个开源 Java 库&#xff0c;它以全自动方式管理&#xff08;即下载、设置和维护&#xff09; Selenium WebDriver所需的驱动程序&#xff08;例如&#xff0c;chromedriver、geckodriver、msededriver 等&#xff09;…

【ASM】字节码操作 工具类与常用类 Printer、ASMifier、Textifier 介绍

文章目录 1.概述2. Printer2.1 class info2.2 fields2.3 constructors2.4 methods3. ASMifier与Textifier3.1 如何使用3.2 从命令行使用3.3 visit方法3.4 从代码中使用1.概述 在上一篇文章中:【ASM】字节码操作 工具类与常用类 TraceClassVisitor 介绍 我们知道了如何使用Tra…

nodejs+vue+elementui个人图书分享共享网站

本面向图书共享系统主要包括两大功能模块&#xff0c;即用户功能模块和管理员功能模块。 &#xff08;1&#xff09;管理员模块&#xff1a;首页、个人中心、图书分类管理、图书信息管理、用户管理、用户分享管理、联系我们、社区交流、系统管理。 &#xff08;2&#xff09;用…