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

异步前

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

可以看到异步优化前 ,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内存也会增多,同时如果服务宕机,阻塞队列中的数据也会丢失,因此也存在数据安全的问题。

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

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

相关文章

LabVIEW图像分段线性映射

介绍了如何使用LabVIEW对图像进行分段线性映射处理&#xff0c;通过对特定灰度值区间进行不同的线性映射调整&#xff0c;以优化图像的显示效果。案例中详细展示了如何配置和使用LabVIEW中的图像处理工具&#xff0c;包括设置分段区间、计算映射参数和应用映射函数等步骤。 实…

STM32智能医疗监测系统教程

目录 引言环境准备智能医疗监测系统基础代码实现&#xff1a;实现智能医疗监测系统 4.1 数据采集模块 4.2 数据处理与分析 4.3 通信系统实现 4.4 用户界面与数据可视化应用场景&#xff1a;医疗监测与优化问题解决方案与优化收尾与总结 1. 引言 智能医疗监测系统通过STM32嵌…

Python爬取股票信息-并进行数据可视化分析,绘股票成交量柱状图

为了使用Python爬取股票信息并进行数据可视化分析&#xff0c;我们可以使用几个流行的库&#xff1a;requests 用于网络请求&#xff0c;pandas 用于数据处理&#xff0c;以及 matplotlib 或 seaborn 用于数据可视化。 步骤 1: 安装必要的库 首先&#xff0c;确保安装了以下P…

virtualbox窗口和win10窗口的切换

1、问题&#xff1a; 从windows切换到虚拟机可以用快捷键 ALTTAB&#xff0c;但是从虚拟机到windows使用 ALTTAB 无法成功切换 2、解决方法&#xff1a; 注意&#xff1a;发现设置为ctrlAlt会导致打开终端快捷键&#xff08;CtrlAltT&#xff09;失效&#xff0c;建议这里设置…

【C++】开源:地图投影和坐标转换proj库配置使用

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍地图投影和坐标转换proj库配置使用。 无专精则不能成&#xff0c;无涉猎则不能通。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&a…

mars3d加载wms服务或者wmts服务注意事项

1.wms只支持4326、3857、4490的标准切片&#xff0c;其他坐标系不支持 Mars3D三维可视化平台 | 火星科技 2.wmts同理&#xff0c;Mars3D三维可视化平台 | 火星科技 3.对应级别tilematrix找到的瓦片tilerow&tilecol这两个参数使用常见报错无效参考&#xff1a; 【Mars3d】…

VSCode设置字体大小

方法1&#xff1a;Ctrl 和 Ctrl -&#xff0c;可以控制整个VSCode界面的整体缩放&#xff0c;但是不会调整字体大小 方法2&#xff1a;该方法只能设置编辑器界面的字号&#xff0c;无法改变窗口界面的字号。 &#xff08;1&#xff09;点开左下角如下图标&#xff0c;进入…

商贸物流大脑:大模型+数据要素赋能智慧物流数据平台

项目背景与意义 物流行业快速发展&#xff0c;数据量急剧增加&#xff0c;随着电子商务、智能制造等领域的快速发展&#xff0c;物流行业面领着前所未有的挑战和机遇&#xff0c;如效率低下、资源配置不均、信息不透明等问题。随着全球化和电子商务的快速发展&#xff0c;数据…

【Linux进阶】文件系统8——硬链接和符号连接:ln

在Linux下面的链接文件有两种&#xff0c; 一种是类似Windows的快捷方式功能的文件&#xff0c;可以让你快速地链接到目标文件&#xff08;或目录)&#xff1b;另一种则是通过文件系统的inode 链接来产生新文件名&#xff0c;而不是产生新文件&#xff0c;这种称为硬链接&…

Vue + SpringBoot:el-upload组件单文件、多文件上传实战解析

文章目录 单文件上传后端前端 多文件上传后端前端 单文件上传 后端 PostMapping("/uploadDxfFile") public R uploadDxfFile(RequestParam(value "file", required true) MultipartFile multipartFile) throws Exception {// 文件校验工作if (multipar…

【C语言小知识】缓冲区

缓冲区 当我们使用printf()将数据显示在屏幕上&#xff0c;或者使用scanf()函数将数据输入到电脑里&#xff0c;我们是否会产生些许疑问&#xff1f;为何输入的字符会直接显示到屏幕上等等。这里需要介绍一个C语言中的一个关键概念——缓冲区。 当我们使用老式系统进行运行代码…

机器学习之保存与加载

前言 模型的数据需要存储和加载&#xff0c;这节介绍存储和加载的方式方法。 存和加载模型权重 保存模型使用save_checkpoint接口&#xff0c;传入网络和指定的保存路径&#xff0c;要加载模型权重&#xff0c;需要先创建相同模型的实例&#xff0c;然后使用load_checkpoint…

创维汽车开展年中总结会:创新创造·勇开拓 智慧经营·攀高峰

2024年7月3日&#xff0c;回顾上半年的工作成果&#xff0c;总结经验教训&#xff0c;明确下半年的发展方向和重点任务&#xff0c;创维汽车于山西省晋中市榆次区山西联合创维体验中心开展年中总结会。 创维集团、创维汽车创始人黄宏生&#xff1b;开沃集团联合创始人、首席执…

每日一更 EFK日志分析系统

需要docker和docker-compose环境 下面时docker-compose.yaml文件 [rootnode1 docker-EFK]# cat docker-compose.yaml version: 3.3services:elasticsearch:image: "docker.elastic.co/elasticsearch/elasticsearch:7.17.5"container_name: elasticsearchrestart: …

process.env 管理 Vue 项目的环境变量(Vue项目中环境变量的配置及调用)

简述&#xff1a;在构建 Vue 应用时&#xff0c;管理配置是开发中的一个重要部分。不同的环境&#xff08;如开发、测试和生产&#xff09;往往需要不同的配置&#xff0c;例如 API、 基础 URL、第三方服务的密钥等。使用环境变量可以帮助我们更好地管理这些配置。这里将介绍如…

2 极/2 零 (2P2Z) 补偿器

极/2 零 &#xff08;2P2Z&#xff09; 补偿器是模拟 II 型控制器的数字实现。它是一种滤波器&#xff0c;通过考虑两个极点和一个零点&#xff0c;将特定的增益和相位升压引入系统。您必须战略性地选择每个极点和零点的频率位置&#xff0c;这将有助于实现所需的系统性能。在该…

当需要对大量数据进行排序操作时,怎样优化内存使用和性能?

文章目录 一、选择合适的排序算法1. 快速排序2. 归并排序3. 堆排序 二、数据结构优化1. 使用索引2. 压缩数据3. 分块排序 三、外部排序1. 多路归并排序 四、利用多核和并行计算1. 多线程排序2. 使用并行流 五、性能调优技巧1. 避免不必要的内存复制2. 缓存友好性3. 基准测试和性…

手把手教你从零开始构建 AI 视频生成模型

在 GitHub 上发现一篇教程&#xff0c;作者详细介绍了如何使用 Python 语言&#xff0c;从零开始构建一个文本到视频生成模型。 涵盖了从理解理论概念到架构编码&#xff0c;最终实现输入文本提示即可生成视频的全过程。 相关链接 GitHub&#xff1a;github.com/FareedKhan-…

PD协议诱骗芯片,XSP08Q,XSP16应用笔记

XSP08Q是3C数码或小家电产品的Type-C接口控制芯片&#xff0c;它负责和PD充电器通讯&#xff0c;获取充电器的快充电压档位&#xff0c;如5V4A&#xff0c;9V3A&#xff0c;12V2A&#xff0c;15V3A&#xff0c;20V5A等等。 XSP08Q支持PD协议&#xff0c;BC1.2协议&#xff0c;Q…

Rakis: 免费基于 P2P 的去中心化的大模型

是一个开源的&#xff0c;完全在浏览器中运行的去中心化 AI 推理网络&#xff0c;用户无需服务器&#xff0c;打开即可通过点对点网络使用 Llama-3、Mistral、Gemma-2b 等最新开源模型。 你可以通过右上角的 Scale Worker &#xff0c;下载好模型后挂机就能作为节点加入到这个…