基于Redis和阻塞队列的 异步秒杀业务

news2025/2/25 11:51:37

异步前

之前的秒杀业务的查询优惠券、查询订单、减库存、创建订单都要查询数据库,而且有分布式锁,使得整个业务耗时长,对此采用异步操作处理,异步操作类似于餐厅点餐,服务员负责点菜产生订单、厨师负责根据订单后厨做饭,整个流程由服务员和厨师两个线程完成,此为异步

可以看到异步优化前 ,1000个请求的耗时均值497ms

异步优化方案

 

将判断秒杀库存和校验一人一单的操作放在redis进行,优惠券库存信息也放入redis以减少读取数据库的压力,采用set集合存储购买过优惠券的用户的id,set集合有元素不重复的特性,可以自动实现一人一单

整体业务逻辑如下:

Redis实现库存和秒杀资格判断(需求1和2)

优惠券信息保存到redis

修改添加秒杀券的代码,在添加秒杀券的同时把信息也保存到redis中

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

    }

添加秒杀券,信息成功添加到redis中,秒杀券id是13,库存是100,如下图所示:

lua脚本查询redis中库存和一人一单购买资格

seckill.lua

---
--- Created by 懒大王Smile.
--- DateTime: 2024/7/6 10:47
---
-- 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
    return 1
end
--3.2判断用户是否下单   若set集合中存在该用户id,则说明已下过单,返回1
if (tonumber(redis.call('sismember',orderKey,userId))==1) then
    return 2
end

--3.4扣库存
redis.call('incrby',stockKey,-1)
--3.5保存用户到set
redis.call('sadd',orderKey,userId)
return 0


 VoucherOrderServiceImpl.java

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

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private ISeckillVoucherService SeckillVoucherService;

    @Autowired
    private RedissonClient redissonClient;

    @Resource
    private RedisIdWorker redisIdWorker;

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

    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        //执行lua脚本判断有无购买资格
        Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString()
        );
        int i = result.intValue();
        if (i!=0){
            return Result.fail(i==1?"优惠券库存不足":"不能重复下单");
        }
        long orderId = redisIdWorker.nextId("order:");
        //生成了orderId,把订单信息保存到阻塞队列,由另一个线程专门根据订单信息去数据库做增删改查,这就实现了异步

        //TODO

        return Result.ok(orderId);
    }
}

运行效果

同一用户两次下单id为13的秒杀券,第一次成功,第二次失败,如下图: 

再次查看redis中voucherId=13的秒杀券,库存减1,且该对该秒杀券下单成功的用户已经存入set集合,userId=1010

优化后 模拟大量用户抢购秒杀券 的测试

 优化后,1000个请求的耗时均值为178ms,相比最初的497ms减少很多

阻塞队列实现异步秒杀下单(需求3和4)

阻塞队列
当线程尝试从队列中获取元素时,若队列无元素,则线程会阻塞,直到队列中有元素才会被唤醒

前面实现了redis秒杀券资格判断,若该用户有资格,则其userId存入redis订单中,且redis中秒杀券库存自减

订单加入阻塞队列

//定义阻塞队列
private BlockingQueue<VoucherOrder> orderTasks=new ArrayBlockingQueue<>(1024*1024);
//订单加入阻塞队列
        //创建订单
        VoucherOrder order = new VoucherOrder();
        order.setVoucherId(voucherId);
        //TODO 生成了orderId,把订单信息保存到阻塞队列,由另一个线程专门根据订单信息去数据库做增删改查,这就实现了异步
        long orderId = redisIdWorker.nextId("order:");
        order.setId(orderId);
        order.setUserId(userId);

        //添加到阻塞队列
        orderTasks.add(order);

从阻塞队列中获取订单然后操作数据库

这里定义线程池,让线程去从阻塞队列中获取订单,实现异步操作数据库

//定义线程池,负责从阻塞队列中获取订单然后异步下单
    private static final ExecutorService SECKILL_ORDER_EXECUTOR= Executors.newSingleThreadExecutor();


//定义线程  这是个内部类
    private class VoucherOrderHandler implements Runnable{
        @Override
        public void run() {
            while (true){
                try {
                    //获取队列中的订单
                    VoucherOrder order = orderTasks.take();

                    //创建订单
                    handleVoucherOrder(order);

                } catch (InterruptedException e) {
                    log.error("订单处理异常",e);
                }

            }
        }
    }

//spring提供的注解  作用:类初始化后就执行VoucherOrderHandler方法
//向线程池提交一个线程
    @PostConstruct
    private void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }


    private void handleVoucherOrder(VoucherOrder order) {
        Long userId = order.getUserId();

        RLock redisLock = redissonClient.getLock("lock:order:" + userId);

        boolean tryLock = redisLock.tryLock();

        //判断锁是否获取成功
        if (!tryLock){
            log.error("不允许重复下单");
            return ;
        }
        try {
            proxy.createVoucherOrder(order);
            //使用动态代理类的对象,事务可以生效
        } finally {
            redisLock.unlock();
        }
    }

完整代码

VoucherOrderServiceImpl.java

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

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private ISeckillVoucherService SeckillVoucherService;

    @Autowired
    private RedissonClient redissonClient;

    @Resource
    private RedisIdWorker redisIdWorker;


    //阻塞队列  当线程尝试从队列中获取元素时,若队列无元素,则线程会阻塞,直到队列中有元素才会被唤醒
    private BlockingQueue<VoucherOrder> orderTasks=new ArrayBlockingQueue<>(1024*1024);

    //线程池,负责从阻塞队列中获取订单然后异步下单
    private static final ExecutorService SECKILL_ORDER_EXECUTOR= Executors.newSingleThreadExecutor();


    //spring提供的注解  作用:类初始化后就执行VoucherOrderHandler方法
    //向线程池提交一个线程
    @PostConstruct
    private void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }


    //线程  内部类
    private class VoucherOrderHandler implements Runnable{
        @Override
        public void run() {
            while (true){
                try {
                    //获取队列中的订单信息
                    VoucherOrder order = orderTasks.take();
                    //创建订单
                    handleVoucherOrder(order);

                } catch (InterruptedException e) {
                    log.error("订单处理异常",e);
                }

            }
        }
    }


    //代理对象
    //因为异步之后,子线程不能获取代理对象无法实现事务,所以要定义为全局变量,在主线程中就获取代理对象给子线程用
    IVoucherOrderService proxy;

    private void handleVoucherOrder(VoucherOrder order) {

        Long userId = order.getUserId();

        RLock redisLock = redissonClient.getLock("lock:order:" + userId);

        boolean tryLock = redisLock.tryLock();

        //判断锁是否获取成功
        if (!tryLock){
            log.error("不允许重复下单");
            return ;
        }
        try {
            //锁加到这里,事务提交后才释放锁
            proxy.createVoucherOrder(order);
            //使用动态代理类的对象,事务可以生效
        } finally {
            redisLock.unlock();
        }
    }


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


    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();

        //执行lua脚本判断有无购买资格
        Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString()
        );
        int i = result.intValue();
        if (i!=0){
            return Result.fail(i==1?"优惠券库存不足":"不能重复下单");
        }


        //创建订单
        VoucherOrder order = new VoucherOrder();
        order.setVoucherId(voucherId);
        //TODO 生成了orderId,把订单信息保存到阻塞队列,由另一个线程专门根据订单信息去数据库做增删改查,这就实现了异步
        long orderId = redisIdWorker.nextId("order:");
        order.setId(orderId);
        order.setUserId(userId);

        //添加到阻塞队列
        orderTasks.add(order);

        //获取事务的动态代理对象,需要在启动类加注解暴漏出对象
        proxy = (IVoucherOrderService)AopContext.currentProxy();//拿到动态代理对象

        return Result.ok(orderId);
    }


    //TODO spring对该类做了动态代理,用动态代理的对象提交的事务
    @Transactional
    public void createVoucherOrder(VoucherOrder order) {
        //一人一单,根据优惠卷id和用户id去数据库查询是否已经存在该优惠卷
        Long id = order.getUserId();
            //为用户id加锁而不是对整个createVoucherOrder方法加锁,减小锁范围,提升性能,这样每个用户就有不同的锁
            //锁加在函数内部,锁内的代码执行完后就会释放锁,而事务的提交是在整个方法执行后提交的,也就是事务的提交在锁释放之后。
            //但是锁释放后其他线程就可以进来,此时事务可能还没有提交,可能出现并发问题,重复购买
            //所以要扩大锁的范围,把锁加到seckillVoucher方法后面,在事务提交后才能释放锁!

            int count = query().eq("user_id", id).eq("voucher_id", order).count();
            if (count >=1) {
                //count==1说明用户拥有了一个优惠券
                log.error("不能重复下单");
                return;
//                return Result.fail("不能重复购买优惠卷");
            }

            //4.扣减库存  防止超卖,加乐观锁,扣减库存前再查询一次库存判断
//        boolean b = SeckillVoucherService.update()
//                .setSql("stock=stock-1").
//                eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update();
            //使用setSql方法设置了更新语句"stock=stock-1",接着使用eq方法添加了两个条件:"voucher_id"等于voucherId和"stock"等于voucher.getStock()
            //条件1:voucher_id=voucherId指当前操作的优惠卷的id=数据库中的优惠卷id,即通过优惠卷id指明了要修改哪个优惠卷的库存
            //条件2:stock=voucher.getStock,说明该线程修改库存期间没有其他线程来插队修改库存,那么数据是安全的
            //TODO !!!注意!这种操作在并发情况下可能导致用户在优惠卷库存充足的情况下抢购优惠卷失败,也就是即使有库存也会抢购失败,此时可以判断库存是否充足,重新抢购

            //修改如下:最后库存判断,只要>0就可以修改
            boolean b = SeckillVoucherService.update()
                    .setSql("stock=stock-1").
                    eq("voucher_id", order).gt("stock", 0)
                    .update();
            if (!b) {
//                return Result.fail("库存不足");
                log.error("库存不足");
                return;
            }
            save(order);
    }
}

总结

所谓异步,就是把主线程的任务分给多个线程执行,提高业务执行速度

内存安全限制:我们使用的阻塞队列是JDK自带的,它基于JVM内存,如果阻塞队列中元中的元素过多,占用的JVM内存也会增多,同时如果服务宕机,阻塞队列中的数据也会丢失,因此也存在数据安全的问题。

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

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

相关文章

破解宇宙终极奥秘,战胜昊天无上束缚

在幽邃的暗夜下&#xff0c;细品着夫子与昊天跨越千年的智勇交锋&#xff0c;我的思绪不禁飘向了更加深远的宇宙边际&#xff0c;回响起那些关于人类如何挑战天命、战胜上天的过往。 宇宙奥秘 在浩瀚无垠的宇宙深渊中&#xff0c;隐藏着一段超越凡尘的规则。昊天&#xff0c;…

文件上传(本地、OSS)

什么是文件上传&#xff1a;将文件上传到服务器。 文件上传-本地存储 前端 <template> <div><!-- 上传文件需要设置表单的提交方式为post&#xff0c;并设置enctype属性、表单项的type属性设置为file --><form action"http://localhost:8080/wedu/…

Python 异步编程介绍与代码示例

Python 异步编程介绍与代码示例 一、异步编程概述 异步编程是一种编程范式&#xff0c;它旨在处理那些需要等待I/O操作完成或执行耗时任务的情况。在传统的同步编程中&#xff0c;代码会按照顺序逐行执行&#xff0c;直到遇到一个耗时操作&#xff0c;它会阻塞程序的执行直到…

Hugging face Transformers(2)—— Pipeline

Hugging Face 是一家在 NLP 和 AI 领域具有重要影响力的科技公司&#xff0c;他们的开源工具和社区建设为NLP研究和开发提供了强大的支持。它们拥有当前最活跃、最受关注、影响力最大的 NLP 社区&#xff0c;最新最强的 NLP 模型大多在这里发布和开源。该社区也提供了丰富的教程…

gptoolbox matlab工具箱cmake 调试笔记

一、问题描述 起因&#xff1a;在matlab中运行Offset surface of triangle mesh in matlab的时候报错&#xff1a; 不支持将脚本 signed_distance 作为函数执行: E:\MATLAB_File\gptoolbox\mex\signed_distance.m> 出错 offset_bunny (第 22 行) D signed_distance(BC,V,F…

Vim编辑器与Shell命令脚本

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 目录 一、Vim文本编辑器 二、编写Shell脚本 三、流程控制语句 四、计划任务服务程序 致谢 一、Vim文本编辑器 “在Linux系统中一切都是文件&am…

Java技术栈总结:kafka篇

一、# 基础知识 1、安装 部署一台ZooKeeper服务器&#xff1b;安装jdk&#xff1b;下载kafka安装包&#xff1b;上传安装包到kafka服务器上&#xff1a;/usr/local/kafka;解压缩压缩包&#xff1b;进入到config目录&#xff0c;修改server.properties配置信息&#xff1a; #…

绝区叁--如何在移动设备上本地运行LLM

随着大型语言模型 (LLM)&#xff08;例如Llama 2和Llama 3&#xff09;不断突破人工智能的界限&#xff0c;它们正在改变我们与周围技术的互动方式。这些模型早已集成到我们的手机中&#xff0c;但到目前为止&#xff0c;它们理解和处理请求的能力还非常有限。然而&#xff0c;…

2024年7月6日 (周六) 叶子游戏新闻

自动电脑内部录音器AutoAudioRecorder: 是一款免费的自动音频录制软件&#xff0c;可直接将电脑内部所有的声音录制成 mp3/wav 文件&#xff0c;包括音乐、游戏直播、网络会议、聊天通话等音频源。 卸载工具 HiBitUninstaller: Windows上的软件卸载工具 《不羁联盟》制作人&…

数据库测试|Elasticsearch和ClickHouse的对决

前言 数据库作为产品架构的重要组成部分&#xff0c;一直是技术人员做产品选型的考虑因素之一。 ClkLog会经常遇到小伙伴问支持兼容哪几种数据库&#xff1f;为什么是选择ClickHouse而不是这个或那个。 由于目前市场上主流的数据库有许多&#xff0c;这次我们选择其中一个比较典…

【密码学】密码学体系

密码学体系是信息安全领域的基石&#xff0c;它主要分为两大类&#xff1a;对称密码体制和非对称密码体制。 一、对称密码体制&#xff08;Symmetric Cryptography&#xff09; 在对称密码体制中&#xff0c;加密和解密使用相同的密钥。这意味着发送方和接收方都必须事先拥有这…

医院产科信息化管理系统源码,智慧产科管理系统,涵盖了从孕妇到医院初次建档、历次产检、住院分娩、统计上报到产后42天全部医院服务的信息化管理。

医院产科信息化管理系统源码&#xff0c;智慧产科管理系统&#xff0c;产科专科电子病历系统 技术架构&#xff1a;前后端分离Java&#xff0c;Vue&#xff0c;ElementUIMySQL8.0.36 医院产科信息化管理系统&#xff0c;通过构建专科病例系统实现临床保健一体化&#xff0c;涵…

线程池理解及7个参数

定义理解 线程池其实是一种池化的技术实现&#xff0c;池化技术的核心思想就是实现资源的复用&#xff0c;避免资源的重复创建和销毁带来的性能开销。线程池可以管理一堆线程&#xff0c;让线程执行完任务之后不进行销毁&#xff0c;而是继续去处理其它线程已经提交的任务。 …

【pytorch18】Logistic Regression

回忆线性回归 for continuous:y xwbfor probability output:yσ(xwb) σ:sigmoid or logistic 线性回归是简单的线性模型&#xff0c;输入是x&#xff0c;网络参数是w和b&#xff0c;输出是连续的y的值 如何把它转化为分类问题?加了sigmoid函数&#xff0c;输出的值不再是…

springboot服务启动读取不到application.yml中的nacos.config信息

我的版本&#xff1a; 可以添加bootstrap.yml文件&#xff0c;在里面添加nacos.config的配置信息 也可以添加VM参数 -Dspring.cloud.nacos.discovery.server-addr -Dspring.cloud.nacos.config.server-addr -Dspring.cloud.nacos.config.namespace -Dspring.cloud.nacos.discov…

Java实现登录验证 -- JWT令牌实现

目录 1.实现登录验证的引出原因 2.JWT令牌2.1 使用JWT令牌时2.2 令牌的组成 3. JWT令牌&#xff08;token&#xff09;生成和校验3.1 引入JWT令牌的依赖3.2 使用Jar包中提供的API来实现JWT令牌的生成和校验3.3 使用JWT令牌验证登录3.4 令牌的优缺点 1.实现登录验证的引出 传统…

LeetCode刷题之搜索二维矩阵

2024 7/5 一如既往的晴天&#xff0c;分享几张拍的照片嘿嘿&#xff0c;好几天没做题了&#xff0c;在徘徊、踌躇、踱步。蝉鸣的有些聒噪了&#xff0c;栀子花花苞也都掉落啦&#xff0c;今天给他剪了枝&#xff0c;接回一楼来了。ok&#xff0c;做题啦&#xff01; 图1、宿舍…

EDA 2023 年世界国家suicide rate排名

文章目录 前言:关于数据集列 导入模块导入数据数据预处理探索性数据分析按性别划分的自杀率 [箱线图]相关矩阵热图自杀率最高的 15 个国家变化百分比最高的 15 个国家/地区2023 年世界地图上自杀率的国家 结尾: 前言: 随着社会的不断发展和变迁&#xff0c;人们对于各种社会问…

154. 寻找旋转排序数组中的最小值 II(困难)

154. 寻找旋转排序数组中的最小值 II 1. 题目描述2.详细题解3.代码实现3.1 Python3.2 Java 1. 题目描述 题目中转&#xff1a;154. 寻找旋转排序数组中的最小值 II 2.详细题解 该题是153. 寻找旋转排序数组中的最小值的进阶题&#xff0c;在153. 寻找旋转排序数组中的最小值…

2024 年第十四届亚太数学建模竞赛(中文赛项)浅析

需要完整B题资料&#xff0c;请关注&#xff1a;“小何数模”&#xff01; 本次亚太(中文赛)数学建模的赛题已正式出炉&#xff0c;无论是赛题难度还是认可度&#xff0c;该比赛都是仅次于数模国赛的独一档&#xff0c;可以用于国赛前的练手训练。考虑到大家解题实属不易&…