Redis:原理速成+项目实战——Redis实战9(秒杀优化)

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:Redis:原理速成+项目实战——Redis实战8(基于Redis的分布式锁及优化)
📚订阅专栏:Redis:原理速成+项目实战
希望文章对你们有所帮助

简单回顾一下之前实现秒杀的思路,其实无非就是2点:
1、库存够不够,该用户有没有买过
2、操作数据库(扣库存、创建订单)

之前的业务由于涉及了大量数据库的操作,所以性能并不是太好。

秒杀优化

  • 异步秒杀思路
  • Redis实现秒杀资格的判断
    • 分析
    • 实现
  • 基于阻塞队列实现秒杀异步下单
  • 总结及发现问题

异步秒杀思路

之前的业务,可以用下图来表示:
在这里插入图片描述
可以发现,Tomcat中顺序执行的操作里面有4个需要对数据库进行查询或修改操作,而MySQL本身的并发能力就是很差的,时间是所有时间之和,这种解决方式并不好。
因此,我们需要将操作数据库的步骤分给多个线程来做,而库存量判断、是否具有购买资格的判断,我们也可以将其交给Redis,从而提高效率。
既然是将步骤分给多个线程来做,我们要开辟线程,并且要使得开辟的线程能够正确执行业务:

在这里插入图片描述
整个业务的流程被分开了,所需要的时间不再是所有的过程时间之和(完成下单的操作是异步执行的),而且整个业务基于Redis,将会大大提升业务的性能。
但这需要考虑2个难点问题:如何在Redis里面完成判断库存以及一人一单的校验?如何基于阻塞队列实现秒杀异步下单?

Redis实现秒杀资格的判断

分析

要在Redis里面判断库存量以及一人一单,我们肯定需要将优惠券的库存信息以及相关订单信息缓存到Redis中去,我们需要选取合适的数据结构来保存这两个信息。
库存很容易,因为只包含了库存量这个信息,直接使用String类型进行存储即可。
但要实现一人一单,我们要判断这个优惠券被哪些用户购买过。所以这个数据结构需要能够保存多个值,而又因为一人一单,所以我们要保存的用户的id显然是不能重复的。所以,set是很适合的。
因此业务流程可以很容易知道:
在这里插入图片描述
可以发现业务的步骤还是有很多步的,因此我们要保证步骤的原子性,因此上述的内容应当用Lua脚本来写。

实现

需求:

1、新增秒杀优惠券的同时,将优惠券的信息保存到Redis中
2、基于Lua脚本,判断秒杀库存、一人一单、决定用户是否抢购成功
3、若成功,将优惠券id与用户id进行封装,再存入阻塞队列
4、开启线程任务,不断从阻塞队列中获取信息,实现异步下单

1、在VoucherServiceImpl类中新增优惠券,同时保存到Redis中:

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    @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中, SECKILL_STOCK_KEY = "seckill:stock:"
        stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
    }

打开postman,进行测试:
在这里插入图片描述
添加成功:
在这里插入图片描述
2、在resources下创建seckill.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 判断库存是够充足
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2 库存不足,返回1
    return 1
end
--3.2 判断用户是否下单,即判断用户id是不是这个set集合的成员
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.2 存在,说明重复下单
    return 2
end
-- 3.4 扣库存
redis.call('incrby', stockKey, -1)
-- 3.5 下单(保存用户)
redis.call('sadd', orderKey, userId)
return 0

3、在VoucherOrderServiceImpl中修改seckillVoucher,修改业务,先调用Lua脚本执行,返回0即可将下单信息存入阻塞队列(存入阻塞队列的代码编写较为麻烦,暂时放着):

    //秒杀优化,调用Lua的代码
    @Override
    public Result seckillVoucher(Long voucherId) {
        //获取用户
        Long userId = UserHolder.getUser().getId();
        //执行Lua脚本,这里使用了静态代码块
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(), //这里我们没有key传,只需要传送一个空集合即可
                voucherId.toString(), userId.toString()//传其他类型的参数
        );
        //判断结果是否为0
        int r = result.intValue();
        if(r != 0){
            //不为0,没有购买资格
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        }
        //为0,有购买资格,将下单信息保存到阻塞队列
        long orderId = redisIdWorker.nextId("order");
        //TODO 保存阻塞队列,这边先空着,下一部分单独编写

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

打开postman进行测试,连续下单两次,证明下单资格判断的可行性:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当然数据库中的数据还是没有改变的, 异步下单还没做。

如果要进行压力测试的话,大家要自己构建出几百个用户,然后这些用户分别占一个线程进行下单,用jmeter进行测试,完成这些测试还是很繁琐的,但是因为这些操作都是基于Redis的,容易知道吞吐率肯定是变大了不少的。

基于阻塞队列实现秒杀异步下单

对于用户资格的判断已经完成了,假设用户具有秒杀的资格,这时候我们只需要独立开辟一个线程,去异步实现下单。因为用户只要具有这个资格,直接返回订单的id从而让用户去执行付款即可,而将订单的信息存入数据库并不需要严格的时效性,因此业务可行:
在这里插入图片描述
接下来,开启线程任务,不断从阻塞队列中获取信息,实现下单,这是整体性能变高的关键。
代码的实现,我们之所以选择阻塞队列,是因为阻塞队列具有一个重要的性质:当线程尝试获取阻塞队列的元素时,若队列中没有元素,该线程就会被阻塞,直到队列中获取到这个元素了。
另外代码中因为涉及了开辟独立线程去实现异步下单,因此我们需要准备好线程池与线程任务。
最终业务的全部代码实现如下:

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

    //注入秒杀优惠券的service
    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;
    
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    public static final DefaultRedisScript<Long> SECKILL_SCRIPT;
    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));//设置脚本位置
        SECKILL_SCRIPT.setResultType(Long.class);//配置返回值
    }

    //阻塞队列:当线程尝试从这个队列中获取元素,如果没有元素,那么该线程就会被阻塞,直到队列中获取到元素
    private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
    //线程池
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    //线程任务,用户随时都能抢单,所以应该要在这个类被初始化的时候马上开始执行
    @PostConstruct  //该注解表示在当前类初始化完毕以后立即执行
    private void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    private class VoucherOrderHandler implements Runnable{

        @Override
        public void run() {
            //不断从队列中取订单信息
            while (true){
                try {
                    VoucherOrder voucherOrder = orderTasks.take();
                    //创建订单,流程里面无须再加锁,加个锁就是做个兜底
                    handleVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    //e.printStackTrace();
                    log.error("创建订单异常", e);
                }
            }
        }
    }

    IVoucherOrderService proxy;

    //秒杀优化,调用Lua的代码
    @Override
    public Result seckillVoucher(Long voucherId) {
        //获取用户
        Long userId = UserHolder.getUser().getId();
        //执行Lua脚本,这里使用了静态代码块
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(), //这里我们没有key传,只需要传送一个空集合即可
                voucherId.toString(), userId.toString()//传其他类型的参数
        );
        //判断结果是否为0
        int r = result.intValue();
        if(r != 0){
            //不为0,没有购买资格
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        }
        //为0,有购买资格,将下单信息保存到阻塞队列
        long orderId = redisIdWorker.nextId("order");
        //TODO 保存阻塞队列
        //先将用户id与订单id封装起来
        VoucherOrder voucherOrder = new VoucherOrder();
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);
        //放入阻塞队列
        orderTasks.add(voucherOrder);
        //获取代理对象
        proxy = (IVoucherOrderService) AopContext.currentProxy();

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

    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        //获取用户,用户id不能再从UserHolder中取了,因为现在是从线程池获取的全新线程,不是主线程
        Long userId = voucherOrder.getUserId();
        //创建锁对象
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //获取锁
        boolean isLock = lock.tryLock();
        //判断是否获取锁成功
        if(!isLock){
            log.error("不允许重复下单");//理论上不会发生
            return;
        }
        try {
            //这是主线程的一个子线程,无法直接获得代理对象,代理对象需要在主线程中获取,并设置成成员变量使得该子线程能够获取
            //createVoucherOrder的方法体直接修改,不再需要重新根据id创建订单,而是直接将订单传进去
            proxy.createVoucherOrder(voucherOrder);
        } finally {
            lock.unlock();
        }
    }

    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        //查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        //判断是否存在
        if (count > 0) {
            log.error("不可重复购买");
            return;
        }
        //扣减库存
        boolean success = seckillVoucherService.update().
                setSql("stock = stock - 1").
                eq("voucher_id", voucherOrder.getVoucherId()).
                gt("stock", 0).
                update();
        if (!success) {
            log.error("库存不足");
            return;
        }
        //不需要再创建订单,直接save
        save(voucherOrder);
    }
}

总结及发现问题

1、秒杀业务的优化思路

(1)先用Redis完成库存量以及一人一单判断,完成抢单
(2)下单的业务(操作数据库的业务)放入阻塞队列,并用独立线程完成异步下单

2、基于阻塞队列的异步秒杀存在的问题

(1)内存限制问题:我们使用了JDK的阻塞队列,占用的是JVM内存,若不加以限制,会有很多的订单对象创建线程并且将大量信息放入阻塞队列,可能会内存溢出
(2)数据安全问题:要用于下单的业务信息都存到了内存中,万一服务宕机,那么用户完成了抢单,但是数据库却没有做出相应的修改,将会导致数据不一致

解决的方法将在下一节进行分析

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Unity中URP下深度图的线性转化

文章目录 前言一、_ZBufferParams参数有两组值二、LinearEyeDepth1、使用2、Unity源码推导&#xff1a;3、使用矩阵推导&#xff1a; 三、Linear01Depth1、使用2、Unity源码推导3、数学推导&#xff1a; 前言 在之前的文章中&#xff0c;我们实现了对深度图的使用。因为&#…

uniapp最简单的底部兼容安全区域显示

效果图&#xff1a; 1.html写上动态padding-bottom <view class"button-wrap" :style"padding-bottom:bottomPaddingrpx"><view class"com-btn cencel-btn">取消</view><view class"com-btn confirm-btn " cl…

了解虾皮平台的免费采集软件:数据抓取、分析与市场优势

在电商领域&#xff0c;市场分析和选品策略制定是卖家们取得成功的关键。虾皮&#xff08;Shopee&#xff09;平台提供了免费采集软件&#xff0c;帮助卖家从平台上抓取商品数据&#xff0c;以便进行市场分析、选品策略制定等。这类软件具备数据抓取、数据分析、批量处理、实时…

一文解决新手所有python环境变量报错问题

问题描述: cmd控制台输入python或pip后会出现下面情况 首先确保安装程序时勾选了安装pip pip的所在目录&#xff0c;可以打开该目录查看是否存在 如果还有问题&#xff0c;确保环境变量配置了python的路径 具体操作步骤 此处的用户环境变量是只针对当前系统用户有效&a…

固乔快递查询助手:一键批量查询,省时省力

在快递行业飞速发展的今天&#xff0c;高效、准确地掌握快递信息对于电商卖家、物流公司管理者以及经常需要查询快递信息的消费者来说至关重要。固乔快递查询助手正是为此而生&#xff0c;它的批量查询功能为用户带来前所未有的便利和高效。 首先&#xff0c;如何下载固乔快递查…

「超级细菌」魔咒或将打破,MIT 利用深度学习发现新型抗生素

作者&#xff1a;加零 编辑&#xff1a;李宝珠、三羊 MIT 利用图神经网络 Chemprop 识别潜在抗生素&#xff0c;特异性杀死鲍曼不动杆菌。 自然界中充满了各种各样的微生物&#xff0c;例如结核杆菌&#xff08;导致肺结核&#xff09;、霍乱弧菌&#xff08;导致霍乱&#…

Vue 自定义仿word表单录入之单选按钮组件

因项目需要&#xff0c;要实现仿word方式录入数据&#xff0c;要实现鼠标经过时才显示编辑组件&#xff0c;预览及离开后则显示具体的文字。 鼠标经过时显示 正常显示及离开时显示 组件代码 <template ><div class"pager-input flex border-box full-width fl…

程序员试用期转正工作总结

一、试用期工作总结 在公司的三个月试用期中&#xff0c;我完成了以下工作&#xff1a; 完成了XX个功能模块的开发&#xff0c;包括XX模块、XX模块和XX模块。参与了XX个项目的开发和上线&#xff0c;其中XX项目、XX项目和XX项目是我主导的。优化了现有系统的性能&#xff0c;特…

CMU15-445-Spring-2023-Project #2 - 前置知识(lec07-010)

Lecture #07_ Hash Tables Data Structures Hash Table 哈希表将键映射到值。它提供平均 O (1) 的操作复杂度&#xff08;最坏情况下为 O (n)&#xff09;和 O (n) 的存储复杂度。 由两部分组成&#xff1a; Hash Function和Hashing Scheme&#xff08;发生冲突后的处理&…

【Python】使用tkinter设计开发Windows桌面程序记事本(2)

上一篇&#xff1a;【Python】使用tkinter设计开发Windows桌面程序记事本&#xff08;1&#xff09;-CSDN博客 下一篇&#xff1a; 作者发炎 此代码模块是继承上一篇文章的代码模块的基础上开始设计开发的。 如果不知道怎么新建"记事本项目"文件夹&#xff0c;请参…

【高等数学之泰勒公式】

一、从零开始 1.1、泰勒中值定理1 什么是泰勒公式?我们先看看权威解读: 那么我们从古至今到底是如何创造出泰勒公式的呢? 由上图可知&#xff0c;任一无穷小数均可以表示成用一系列数字的求和而得出的结果&#xff0c;我们称之为“无穷算法”。 那么同理我们想对任一曲线来…

C2-4.2.2 决策树-纯度+信息熵+信息增益

C2-4.2.2 决策树-纯度信息熵信息增益 1、首先了解他的应用背景——决策树 其实说白了&#xff0c;就是一个二叉树 2、纯度 我们举一个买黄金的例子吧&#xff01;黄金有999 和 9999 。 他们是有区别的&#xff0c;代表着黄金的纯度&#xff08;相对杂质而言&#xff09;&…

JMS消息发送

目录 概述1.搭建 JMS 环境2.使用JmsTemplate 发送消息3.接收JMS 消息 概述 JMS是一个Java标准&#xff0c;定义了使用消息代理(message broker)的通用API,在2001年提出。长期以来&#xff0c;JMS一直是Java 中实现异步消息的首选方案。在JMS 出现之前每个消息代理都有其私有的…

MongoDB 启动提示错误code=killed, signal=ABRT

1.停止MongoDB sudo systemctl stop mongod 2.检查数据损坏 sudo mongod --repair --dbpath /var/lib/mongodb 3.赋权限 chown -R mongodb:mongodb /var/lib/mongodb chown mongodb:mongodb /tmp/mongodb-27017.sock 如果不赋权限&#xff0c;启动的时候则会提示 4.启动Mo…

python 工作目录 与 脚本所在目录不一致

工作目录&#xff1a;执行脚本的地方 我以为工作目录会是当前执行脚本的目录位置&#xff0c;但其实不是&#xff0c;例如&#xff1a; 图中红色文件为我执行的脚本文件&#xff0c;但是实际的工作目录是PYTHON LEARNING 可以用如下代码查询当前工作目录&#xff1a; import os…

2024年甘肃省职业院校技能大赛 “信息安全管理与评估”赛项样题卷①

2024年甘肃省职业院校技能大赛 高职学生组电子与信息大类信息安全管理与评估赛项样题 第一阶段&#xff1a;第二阶段&#xff1a;模块二 网络安全事件响应、数字取证调查、应用程序安全第二阶段 网络安全事件响应第一部分 网络安全事件响应第二部分 数字取证调查第三部分 应用程…

redis可视化工具 RedisInsight

redis可视化工具 RedisInsight 1、RedisInsight是什么2、下载RedisInsight3、使用RedisInsight4、其他redsi可视化工具 1、RedisInsight是什么 RedisInsight 是一个用于管理和监控 Redis 数据库的图形用户界面&#xff08;GUI&#xff09;工具。它是由 Redis Labs 开发的&…

Linux驱动学习—输入子系统

1、什么是输入子系统&#xff1f; 输入子系统是Linux专门做的一套框架来处理输入事件的&#xff0c;像鼠标&#xff0c;键盘&#xff0c;触摸屏这些都是输入设备&#xff0c;但是这邪恶输入设备的类型又都不是一样的&#xff0c;所以为了统一这些输入设备驱动标准应运而生的。…

CHS_01.2.1.1+2.1.3+进程的概念、组成、特征

CHS_01.2.1.12.1.3进程的概念、组成、特征 进程进程的概念 进程的组成——PCB进程的组成——PCB进程的组成——程序段、数据段知识滚雪球&#xff1a;程序是如何运行的&#xff1f;进程的组成进程的特征 知识回顾与重要考点 从这个小节开始 我们会正式进入第二章处理机管理相关…

【前端】使用javascript开发一个在线RGB颜色转换

欢迎来到《小5讲堂》 大家好&#xff0c;我是全栈小5。 这是是《前端》序列文章&#xff0c;每篇文章将以博主理解的角度展开讲解&#xff0c; 特别是针对知识点的概念进行叙说&#xff0c;大部分文章将会对这些概念进行实际例子验证&#xff0c;以此达到加深对知识点的理解和掌…