【Redis】使用阻塞队列+Redis优化秒杀业务

news2024/11/28 8:47:05

【Redis】使用阻塞队列+Redis优化秒杀业务

文章目录

  • 【Redis】使用阻塞队列+Redis优化秒杀业务
    • 1. 为什么要优化
    • 2. 怎么优化
      • 2.1 查询优惠卷
      • 2.2 判断秒杀库存
      • 2.3 校验一人一单
      • 2.4 减库存
      • 2.5 创建订单
      • 2.6 保证redis操作的原子性
    • 3. 确认优化方案
    • 4. 实现优化方案
      • 4.1 编写lua脚本
      • 4.2 定义阻塞队列和线程池
      • 4.3 定义内部类实现 `Runnable` 接口处理阻塞队列
      • 4.4 定义方法执行下单任务
      • 4.5 主方法实现
    • 5. 怎样才算优化成功

在对业务进行优化之前,我们需要了解以下几点:

  • 为什么要优化
  • 怎么优化
  • 怎么才算优化成功

我们下面也围绕这几点来讲述。


1. 为什么要优化

假设一个场景:

一个电商平台,商家推出热门产品的限量优惠券,一人只能下一单。

最简单的业务流程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D1ZIZwAe-1673864380249)(C:\Users\zhuhuanjie\AppData\Roaming\Typora\typora-user-images\image-20230116164809024.png)]

首先查询优惠券再判断秒杀库存然后查询订单…一步接着一步,整个业务的响应时间就是每步操作所花时间之和,我们将这种形式称为 同步。而且基本每个操作都要查询数据库。我们也知道查询数据库的时间不算快并且当并发量比较大时对数据库也不友好。所以我们需要对其进行优化。


2. 怎么优化

怎样优化原来的业务,我认为首先就得画出原来业务的流程图,根据流程图具体分析哪一步可以进行优化。我们的流程图已经在上面给出。

2.1 查询优惠卷

这里的优惠券查询直接查询了数据库,我们直到数据库的查询效率是不如redis的,所以我们可以将优惠券的信息添加到redis中(在发布优惠券的时候就要添加到redis中),每次都查询redis的数据,这样就提高了查询效率。


2.2 判断秒杀库存

和查询优惠券一样,我们已经已经说了要将优惠券信息添加到redis中,那么我们就可以根据从redis中查询出的数据进行秒杀判断。那么问题来了,我们添加到redis的优惠券信息到底是啥?是一整个优惠券对象吗?不是,而是优惠券的库存数量。至于key的值,完全根据实际业务决定。下面给出redis存储优惠券的示例格式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eblWYY14-1673864380250)(C:\Users\zhuhuanjie\AppData\Roaming\Typora\typora-user-images\image-20230116170618643.png)]


2.3 校验一人一单

校验一人一单操作的优化不要我说也知道该干嘛了吧,没错!放到redis中查询,那么我们把什么数据放到redis中存储呢?**这里是一个重点!**选择的一个合适的存储格式及其重要。因为我们要求一人只能下一单,我们应该能够想到Set集合,set集合的特性不用我多说。set集合中存储下单用户的id,我们就以set集合的形式存入redis中,key可以由优惠卷的id组成,存储格式如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HpLvX4wC-1673864380251)(C:\Users\zhuhuanjie\AppData\Roaming\Typora\typora-user-images\image-20230116171340741.png)]


2.4 减库存

校验一人一单发现该用户没下过单,我们就可以去减库存,减库存的操作我们也放到redis中执行,我们可以使用redis自增的操作来实现扣减库存的操作。


2.5 创建订单

我们原来的业务流程是 同步 的,我们可以将它改造成 异步 的,这样就能够大大节省时间。而创建订单的操作正适合改造成异步操作。我们将订单对象放入一个阻塞队列中,让独立线程去处理阻塞队列中的订单对象。


2.6 保证redis操作的原子性

2.1至2.5的操作都是分别对redis进行操作,在并发的情况下万一某一步的redis操作因为某些原因阻塞了,容易出现线程安全问题。为避免线程安全问题的发生,我们应该确保redis操作的原子性。这里可以选择使用lua脚本来却本redis操作的原子性。


3. 确认优化方案

在上面我们已经分析了各个流程当中可优化的点,那么我们接下来就可以根据上面的分析来重新设计业务流程图,优化之后的业务流程图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Obp2BFnT-1673864380251)(C:\Users\zhuhuanjie\AppData\Roaming\Typora\typora-user-images\image-20230116173923903.png)]


4. 实现优化方案

4.1 编写lua脚本

根据给出的lua脚本流程图编写lua脚本:

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

-- 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)
return 0

4.2 定义阻塞队列和线程池

//阻塞队列
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
//线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

4.3 定义内部类实现 Runnable 接口处理阻塞队列

定义一个内部类,编写“下单任务”去执行创建订单的操作。线程池中的独立线程去执行这个任务,从阻塞队列中取出订单,然后创建订单。

 private class VoucherOrderHndler implements Runnable {

    @Override
    public void run() {
        while (true) {
            try {
                //1.获取队列中的订单信息
                VoucherOrder voucherOrder = orderTasks.take();
                //2.创建订单
                handleVoucherOrder(voucherOrder);
            } catch (Exception e) {
                log.error("处理订单异常", e);
            }
        }
    }
}

handleVoucherOrder(voucherOrder) 方法定义在外部类上,具体业务如下:

虽然我们使用lua脚本已经确保了一人一单,但是我们还可以使用 redisson 去加锁进行兜底。然后我们使用代理对象去调用创建订单的方法。

但是我们这里使用线程池中的独立线程去处理订单,该线程和我们主方法中使用的线程不是同一个,所以在该方法中我们通过 IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); 我们无法获得主方法中的代理对象,所以该方法中的 proxy 对象,是我们在外部类中通过成员变量声明并在主方法中赋值的,具体操作继续往下看。

private void handleVoucherOrder(VoucherOrder voucherOrder) {
    Long userId = voucherOrder.getUserId();
    RLock lock = redissonClient.getLock("lock:order:" + userId);
    if (!lock.tryLock()) {
        //获取锁失败
        log.error("不允许重复下单");
    }
    try {
        //获取代理对象(事务)
        proxy.createVoucherOrder(voucherOrder);
    } finally {
        //释放锁
        lock.unlock();
    }
}

createVoucherOrder(voucherOrder) 的方法如下所示:

@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
    //4.一人一单
    Long userId = voucherOrder.getUserId();

    //4.1.查询是否已经有订单
    int count = lambdaQuery().eq(VoucherOrder::getUserId, userId).eq(VoucherOrder::getVoucherId, voucherOrder.getVoucherId()).count();
    if (count > 0) {
        //已有订单
        log.error("用户已经购买过一次!");
        return;
    }

    //5.扣减库存
    //5.1.写法一
    LambdaUpdateWrapper<SeckillVoucher> updateWrapper = new LambdaUpdateWrapper<>();
    updateWrapper.setSql("stock=stock-1").eq(SeckillVoucher::getVoucherId, voucherOrder.getVoucherId()).gt(SeckillVoucher::getStock, 0);
    boolean success = seckillVoucherService.update(updateWrapper);

    if (!success) {
        //扣减失败
        log.error("库存不足");
        return;
    }

    //创建订单
    save(voucherOrder);
}

4.4 定义方法执行下单任务

我们要实现上面定义的下单任务在外部类刚初始化完成就执行。我们需要使用到 @PostConstruct 注解,该注解的功能是在类初始化后就执行。

//这个类刚初始化后就去执行这个任务
@PostConstruct
private void init() {
    SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHndler());
}

4.5 主方法实现

seckillVoucher(Long voucherId) 就是真正处理秒杀业务的方法。

private static final DefaultRedisScript<Long> SECKILL_SCRIPT;

static {
    SECKILL_SCRIPT = new DefaultRedisScript<>();
    SECKILL_SCRIPT.setResultType(Long.class);
    SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
}

private IVoucherOrderService proxy;

@Override
public Result seckillVoucher(Long voucherId) {
    //获取用户id
    Long userId = UserHolder.getUser().getId();
    //1.执行lua脚本
    Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,
            Collections.emptyList(),
            voucherId.toString(), userId.toString());
    //2.判断结果是否为0
    int r = result.intValue();
    if (r != 0) {
        //2.不为0,没有购买资格
        return Result.fail(r == 1 ? "库存不足" : "不能重读下单");
    }
    //2.2 为0,有购买资格,把下单信息保存到阻塞队列
    VoucherOrder voucherOrder = new VoucherOrder();
    //2.3 订单id
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    voucherOrder.setUserId(UserHolder.getUser().getId());
    voucherOrder.setVoucherId(voucherId);
    //2.4放入阻塞队列
    orderTasks.add(voucherOrder);

    //3.获取代理对象
    proxy = (IVoucherOrderService) AopContext.currentProxy();
    return Result.ok(orderId);
}

5. 怎样才算优化成功

判断优化是否成功的一个最为直观的指标就是响应时间,我们可以通过jMeter工具去进行并发测试,得到优化后的聚合报告,并与优化之前的聚合报告相比较。如果优化后的聚合报告中各指标基本都优于优化前的指标,那么就算优化成功了。

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

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

相关文章

jvm系列(3)--运行时数据区Runtime

目录运行时数据区概述及线程前言运行时数据区结构运行时数据区与内存线程的内存空间Runtime类线程JVM 线程JVM 系统线程程序计数器(PC寄存器)PC寄存器介绍PC寄存器的作用举例两个面试题CPU 时间片本地方法接口本地方法举例为什么要使用 Native Method&#xff1f;与Java环境外交…

Tomcat打破双亲委派模型

tomcat的类加载器结构tomcat的类加载&#xff08;loadClass&#xff09;过程和原本的双亲委派模型思路差不多&#xff0c;先看有没有加载过。先在本地 Cache 查找该类是否已经加载过&#xff0c;也就是说 Tomcat 的类加载器是否已经加载过这个类。如果 Tomcat 类加载器没有加载…

【学习笔记】【Pytorch】张量(Tensor)的基础操作

【学习笔记】【Pytorch】张量&#xff08;Tensor&#xff09;的基础操作一、创建张量1.使用数据创建张量2.无需数据的创建选项3.torch.Tensor与torch.tensor的区别4.PyTorch中张量的创建方法的选择二、张量的属性1.张量的 torch.dtype2.张量的 torch.device3.张量的 torch.layo…

阿维塔冲击年10万台订单,第二款车型Q2发布

1月13日&#xff0c;阿维塔科技在重庆总部召开渠道合作伙伴大会。今年&#xff0c;阿维塔计划推出&#xff1a; •阿维塔11后驱版本 •并发布第二款产品&#xff0c;代号E12&#xff0c;定位中大型轿车。阿维塔今年计划冲击10万辆订单目标。在当前CHN平台的基础上&#xff0c;阿…

Openresty记录笔记

最近由于项目需要学习了安全代理的相关知识&#xff0c;其实刚开始的时候是非常需要一个入门的介绍&#xff0c;大概说明下这个到底是个什么东西&#xff0c;能干啥&#xff0c;简单的原理是什么&#xff0c;为此我记录下我看完用完的心得&#xff0c;记录成笔记。 一般我们代码…

Redis 持久化详解

目录一、简介二、RDB持久化2.1、SAVE2.2、BGSAVE2.3、SAVE选项2.4、RDB文件结构2.5、RDB文件载入三、AOF持久化3.1、开启AOF功能3.2、配置AOF文件的冲洗频率3.3、AOF重写3.3.1、BGREWRITEAOF命令&#xff08;手动&#xff09;3.3.2、AOF重写配置选项&#xff08;自动&#xff0…

Android | Service

Android Service Service 概念 实现程序后台运行的解决方案&#xff0c;一种可在后台执行长时间运行操作而不提供界面的应用组件。Service 的运行不依赖于任何用户界面&#xff0c;即使程序被切换到后台&#xff0c;或者用户打开了另外一个应用程序&#xff0c;Service 仍然能…

Vue3——第十五章(计算属性:computed)

一、基础示例 模板中的表达式虽然方便&#xff0c;但也只能用来做简单的操作。如果在模板中写太多逻辑&#xff0c;会让模板变得臃肿&#xff0c;难以维护。推荐使用计算属性来描述依赖响应式状态的复杂逻辑。 在这里定义了一个计算属性 publishedBooksMessage。computed() 方…

【设计模式】创建型模式·原型模式

设计模式学习之旅(五) 查看更多可关注后查看主页设计模式DayToDay专栏 一. 概述 用一个已经创建的实例作为原型&#xff0c;通过复制(克隆)该原型对象来创建一个和原型对象相同的新对象。 原型模式包含如下角色&#xff1a; 抽象原型类&#xff1a;规定了具体原型对象必须实现…

Java基础(二)

1.标识符标识符&#xff1a;由数字、字符、下划线、$组成&#xff08;不能以数字、下划线开头&#xff09;java严格区分大小写2.命名规范包名&#xff1a;多单词组成时所有字母全部小写类名、接口名&#xff1a;多单词组成时&#xff0c;所有单词首字母大写变量名、方法名&…

屏幕录制工具哪个好用?分享3款相见恨晚的软件

在我们的日常生活中&#xff0c;我们经常使用截图和手机屏幕记录功能来记录一些重要的内容。然而&#xff0c;录制的图片清晰度很低&#xff0c;或者需要不断的截图&#xff0c;这很容易出错一些重要的内容&#xff0c;这个时候就需要进行录屏了。那么电脑上的屏幕录制工具哪个…

group by详解

group by功能 在SQL中group by主要用来进行分组统计&#xff0c;分组字段放在group by的后面&#xff1b;分组结果一般需要借助聚合函数实现。 group by语法结构 1、常用语法 语法结构 SELECT column_name1,column_name2, … 聚合函数1,聚合函数2 , … FROM table_name GROUP…

电脑删除了大文件怎么恢复?看看这四种方法

电脑能够帮助我们存储大量的文件&#xff0c;比如视频、文档、音频等&#xff0c;但是随着时间的流逝&#xff0c;有些文件所存在的意义也变得毫无价值了&#xff0c;这时候很多小伙伴都会选择删除操作&#xff0c;可是由于电脑磁盘内容过多&#xff0c;容易面临重要文件被误删…

硬件仿真加速器与原型验证平台

基于软件仿真工具对于动辄几百万门的ASIC验证而言&#xff0c;几乎显得力不从心。不管是从成本还是从性能的角度来看&#xff0c;使用硬件仿真器或者基于FPGA的原型验证平台&#xff0c;几乎是验证工程师的不二法门。因为基于硬件的环境能够极大的提高验证的速度&#xff0c;增…

Promise(基础)

Promise是什么 1.promise是一门新的技术&#xff08;ES6规范&#xff09; 2.Promise是JS中一编程的解决方案&#xff08;旧的解决方案是单纯的使用回调函数&#xff09; 3.promise一个构造函数&#xff0c;promise队形用来封装一个一步操作并可以获取其成功/失败的结果值。 注…

sparksql案例实操

sparksql案例实操解决语句如下 select * from( select , rank()over(partition by area order by clickCnt desc) from(select area, product_name, count()as clickCnt from( select a.*, p.product_name, c.area, c.city_name from user_visit_action a join product_info p…

Dubbo与Spring集成

Dubbo框架常被当作第三方框架集成到应用中&#xff0c;当Spring集成Dubbo框架后&#xff0c;为什么在编写代码的时候&#xff0c;只用了DubboReference注解就可以调用提供方的服务了呢&#xff1f;这篇笔记就是分析Dubbo框架是怎么与Spring结合的。 现状integration层代码编写…

关于嵌入式学习和规划,求指点?

在知乎上收到的一个提问问题&#xff1a;各位大佬好&#xff0c;我先说说基本情况&#xff0c;28岁&#xff0c;北京&#xff0c;嵌入式软开&#xff0c;军工行业。硕士毕业一年半。工作不忙收获很少&#xff0c;造成我自己特别迷茫&#xff0c;没有了方向&#xff0c;自己学没…

【C++】Hash闭散列

目录 一、哈希的概念 1.1 哈希冲突 1.2 哈希函数 1.3 装载因子 二、闭散列 2.1 线性探测 2.2 Insert 插入 2.3 Find 查找 2.4 Erase删除 2.5 插入复杂类型 2.6 二次探测 三、源代码与测试用例 3.1 hash源代码 3.2 测试用例 一、哈希的概念 在前面学习了二叉搜索…

多巴胺聚乙二醇多巴胺,Dopamine-PEG-Dopamine简介,多巴胺是具有正性肌力活动的单胺化合物

产品名称&#xff1a;多巴胺聚乙二醇多巴胺&#xff0c;双多巴胺聚乙二醇&#xff08;Dopamine-PEG-Dopamine&#xff09; 中文别名&#xff1a;多巴胺PEG多巴胺&#xff0c;双多巴胺聚乙二醇 英文名称&#xff1a;Dopamine-PEG-Dopamine 存储条件&#xff1a;-20C&#xff0…