redis实战-redis实现异步秒杀优化

news2024/11/25 6:30:00

秒杀优化-异步秒杀思路

未优化的思路

当用户发起请求,此时会请求nginx,nginx会访问到tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤

1、查询优惠卷

2、判断秒杀库存是否足够

3、查询订单

4、校验是否是一人一单

5、扣减库存

6、创建订单

 在这六步操作中,又有很多操作是要去操作数据库的,而且还是一个线程串行执行, 这样就会导致我们的程序执行的很慢

 优化方案

我们将耗时比较短的逻辑判断放入到redis中,比如是否库存足够,比如是否一人一单,这样的操作,只要这种逻辑可以完成,就意味着我们是一定可以下单完成的,我们只需要进行快速的逻辑判断,根本就不用等下单逻辑走完,我们直接给用户返回成功, 再在后台开一个线程,后台线程慢慢的去执行queue里边的消息,即不追求时效性,让用户先成功下单,后续再完善数据库数据

 

整体思路

用户下单之后,判断库存是否充足只需要到redis中去根据key找对应的value是否大于0即可,如果不充足,则直接结束,如果充足,继续在redis中判断用户是否可以下单,如果set集合中没有这条数据,说明他可以下单,如果set集合中没有这条记录,则将userId和优惠卷存入到redis中,并且返回0,整个过程需要保证是原子性的,我们可以使用lua来操作

当以上判断逻辑走完之后,我们可以判断当前redis中返回的结果是否是0 ,如果是0,则表示可以下单,则将之前说的信息存入到到queue中去,然后返回,然后再来个线程异步的下单,前端可以通过返回的订单id来判断是否下单成功。

难点

  • 怎么在redis中去快速校验一人一单,还有库存判断
  • 由于我们校验和tomct下单是两个线程,那么我们如何知道到底哪个单他最后是否成功,或者是下单完成,为了完成这件事我们在redis操作完之后,我们会将一些信息返回给前端,同时也会把这些信息丢到异步queue中去,后续操作中,可以通过这个id来查询我们tomcat中的下单逻辑是否完成了。

代码实现

需求:

  • 新增秒杀优惠券的同时,将优惠券信息,优惠券id和库存信息保存到Redis中

  • 基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功

  • 如果抢购成功,将优惠券id和用户id封装后存入阻塞队列

  • 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能

 新增优惠券,将优惠券信息入库并写入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().setIfAbsent(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
    }

 判断秒杀库存、一人一单,决定用户是否抢购成功,考虑到操作的原子性,采用lua脚本完成这一连串的操作

---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by Lenovo.
--- DateTime: 2023/9/5 20:57
---
-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
---- 1.3.订单id
local orderId = ARGV[3]

-- 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)
---- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0

执行lua脚本,判断是否抢购成功,如果抢购成功,要放入堵塞队列中

@Override
    public Result seckillVoucher(Long voucherId) {

        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //判断是否开始,开始时间如果在当前时间之后就是尚未开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀尚未开始");
        }
        //判断是否结束,结束时间如果在当前时间之前就是已经结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀已经结束");
        }
        Long userId = UserHolder.getUser().getId();
        long orderId = new RedisIdWorker(stringRedisTemplate).nextId("order");
        Long execute = stringRedisTemplate.execute(SILLL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString(), String.valueOf(orderId)
        );
        int r = execute.intValue();
        if (r != 0) {
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        }
        VoucherOrder voucherOrder = new VoucherOrder();
        //订单id
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);
        voucherOrder.setId(orderId);
        //将订单信息放入阻塞队列
        orderTakes.add(voucherOrder);
        return Result.ok(orderId);
    }

定义线程内部类,不断从堵塞队列中读取订单

//从阻塞队列里面取订单信息
    private class voucherOrderHander implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    VoucherOrder take = orderTakes.take();
                    handleVoucherOrder(take);
                } catch (Exception e) {
                    log.error("异常信息如下", e);
                }
            }
        }

获取订单信息的具体方法,这里依然加了分布式锁,是为了保险起见

 private void handleVoucherOrder(VoucherOrder take) {
            Long userId = take.getId();
            //创建锁对象
            RLock lock = redissonClient.getLock("lock:order:" + userId);
            //尝试获取锁
            boolean isLock = lock.tryLock();
            //获取锁失败
            if (!isLock) {
                log.error("不允许重复下单");
                return;
            }
            try {
                voucherOrderService.createVoucherOrder(take);
            } finally {
                //释放锁
                lock.unlock();
            }
        }
    }

这里又有一个问题,就是我们订单信息入库应该是在该类对象被创建的时候就要开启线程在堵塞队列等待读取是否有订单信息,然后顺利入库,所以我们用了aop的@PostConstruct,保证该对象被创建时,线程也能顺利创建,这里用了线程池来提交线程任务

@PostConstruct
    public void init() {
        SECKILL_ORDER_EXECUTOR.execute(new voucherOrderHander());
    }

 完整代码实现

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Autowired
    private ISeckillVoucherService seckillVoucherService;
    @Autowired
    private RedisIdWorker redisIdWorker;
    @Autowired
    private IVoucherOrderService voucherOrderService;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedissonClient redissonClient;
    private static final DefaultRedisScript<Long> SILLL_SCRIPT;
    BlockingQueue<VoucherOrder> orderTakes = new ArrayBlockingQueue<>(1024 * 1024);
    //异步处理线程池
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

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

    @PostConstruct
    public void init() {
        SECKILL_ORDER_EXECUTOR.execute(new voucherOrderHander());
    }

    //从阻塞队列里面取用户信息
    private class voucherOrderHander implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    VoucherOrder take = orderTakes.take();
                    handleVoucherOrder(take);
                } catch (Exception e) {
                    log.error("异常信息如下", e);
                }
            }
        }

        private void handleVoucherOrder(VoucherOrder take) {
            Long userId = take.getId();
            //创建锁对象
            RLock lock = redissonClient.getLock("lock:order:" + userId);
            //尝试获取锁
            boolean isLock = lock.tryLock();
            //获取锁失败
            if (!isLock) {
                log.error("不允许重复下单");
                return;
            }
            try {
                voucherOrderService.createVoucherOrder(take);
            } finally {
                //释放锁
                lock.unlock();
            }
        }
    }

    @Override
    public Result seckillVoucher(Long voucherId) {

        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //判断是否开始,开始时间如果在当前时间之后就是尚未开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀尚未开始");
        }
        //判断是否结束,结束时间如果在当前时间之前就是已经结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀已经结束");
        }
        Long userId = UserHolder.getUser().getId();
        long orderId = new RedisIdWorker(stringRedisTemplate).nextId("order");
        Long execute = stringRedisTemplate.execute(SILLL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString(), String.valueOf(orderId)
        );
        int r = execute.intValue();
        if (r != 0) {
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        }
        VoucherOrder voucherOrder = new VoucherOrder();
        //订单id
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);
        voucherOrder.setId(orderId);
        //将订单信息放入阻塞队列
        orderTakes.add(voucherOrder);
        return Result.ok(orderId);
    }
@Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 用户已经购买过了
            log.error("用户已经购买过了");
            return;
        }

        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣减失败
            log.error("库存不足");
            return;
        }
        save(voucherOrder);

    }

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

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

相关文章

vue中如何给特殊字段设置插槽

大纲: <template><div><div><span>卡号</span><el-input type"text" v-model"cardNo" clearable placeholder"请输入卡号" /><el-button type"primary" plain icon"el-icon-search"…

【Python】从入门到上头— 多进程与分布式进程(10)

文章目录 前言一.多进程1.fork()系统调用2.OS模块3.multiprocessing模块4.进程池&#xff08;multiprocessing Pool模块&#xff09;5.子进程&#xff08;subprocess模块&#xff09;6.进程间通信7.小结7.1.学习小结7.2.Python分布式进程报错&#xff1a;pickle模块不能序列化l…

Axure RP 10汉化版下载 Axure RP 10 mac授权码

Axure RP10汉化版是最强大的计划&#xff0c;原型设计和交付给开发人员的方法&#xff0c;而无需编写代码。能够制作逼真的&#xff0c;动态形式的原型。 Axure RP 10汉化版下载 Axure RP 10 mac授权码 RP 10有什么新功能&#xff1f; 1.显示动态面板 使用Axure RP 10&…

【FAQ】安防监控/视频汇聚/云存储/智能视频分析平台EasyCVR显示CPU过载,如何解决?

视频云存储/安防监控/视频汇聚平台EasyCVR基于云边端智能协同&#xff0c;支持海量视频的轻量化接入与汇聚、转码与处理、全网智能分发、视频集中存储等。安防视频监控系统EasyCVR拓展性强&#xff0c;视频能力丰富&#xff0c;具体可实现视频监控直播、视频轮播、视频录像、云…

环境变量与Path环境变量

“环境变量”和“path环境变量”其实是两个东西&#xff0c;这一点大家一定要区分开&#xff0c;不要混为一谈。 “环境变量”是操作系统工作环境设置的一些选项或属性参数。每个环境变量由变量名和文件路径组成的&#xff0c;可以设置很多个环境变量。 我们一般使用环境变量…

把数组b中的值拷贝给数组a numpy.copyto(a,b)

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 把数组b中的值拷贝给数组a numpy.copyto(a,b) [太阳]选择题 请问关于以下代码最后输出结果的是&#xff1f; import numpy as np to np.array([1, 2]) print("【显示】to ",to) …

GLTF扩展使用及开发指南

glTF 扩展扩展了基本 glTF 模型格式。 扩展可以引入新的属性&#xff08;包括引用外部数据的属性&#xff0c;并且扩展可以定义这些数据的格式&#xff09;、新的参数语义、保留的 ID 和新的容器格式。 扩展是针对特定版本的 glTF 编写的&#xff0c;并且可能会在更高版本的 gl…

高忆管理:央行利好提振 A股三大指数收红

上证指数日K线图 受央行利好音讯刺激&#xff0c;9月11日A股商场震荡走强&#xff0c;三大指数收红&#xff0c;北上资金日内“去而复返”&#xff0c;由净流出转为净流入。 因为上周末音讯面略显平平&#xff0c;9月11日A股开盘较为分解&#xff0c;三大指数涨跌不一&#x…

518企业年会抽奖软件,支持撤消、轮空缺席弃奖

518抽奖软件简介 518抽奖软件&#xff0c;518我要发&#xff0c;超好用的年会抽奖软件&#xff0c;简约设计风格。 包含文字号码抽奖、照片抽奖两种模式&#xff0c;支持姓名抽奖、号码抽奖、数字抽奖、照片抽奖。(www.518cj.net) 操作入口 主窗口上按 CtrlZ&#xff0c;打开…

Linux入门-网络基础|网络协议|OSI七层模型|TCP/IP五层模型|网络传输基本流程

文章目录 一、网络基础 二、网络协议 1.OSI七层模型 2.TCP/IP五层&#xff08;或四层&#xff09;模型 三、网络传输基本流程 1.网络传输流程图 2.数据包封装和分用 四、网络中的地址管理 1.IP地址 2.MAC地址 一、网络基础 网络发展最初是独立模式&#xff0c;即计算…

系统学习Linux-防火墙(Firewall)

目录 防火墙的作用 防火墙分类 防火墙性能 硬件防火墙 软件防火墙 扩展&#xff1a;Web应用防火墙(WAF) 硬件防火墙与软件防火墙比较 iptables netfilter/iptables功能 iptables概念 iptables工作一句——规则&#xff08;rules&#xff09; iptables中链的概念 i…

【面试必刷TOP101】反转链表 链表内指定区间反转

目录 题目&#xff1a;反转链表_牛客题霸_牛客网 (nowcoder.com) 题目的接口&#xff1a; 解题思路&#xff1a; 代码&#xff1a; 过啦&#xff01;&#xff01;&#xff01; 题目&#xff1a;链表内指定区间反转_牛客题霸_牛客网 (nowcoder.com) 题目的接口&#xff1…

肖sir__mysql之安装__002

mysql之安装 1、rpm -qa|grep mysql 2.删除mysql的方法&#xff1a; 第一种&#xff1a;yum remove mysql * &#xff08;*表示所有&#xff09; 第二种&#xff1a;rpm -e --nodeps 包名 强制删除mysql包 3、删除干净mysql文件&#xff0c;在进行安装 yum install mysql 安装…

MAML在隐式神经表示中的应用

论文 Learned Initializations for Optimizing Coordinate-Based Neural Representations &#x1f383;Abstract1. Introduction2. Related Work3. Overview ⭐4. Results5. Conclusion6. AcknowledgementsA. Implementation details Implicit Neural Representations for Ima…

Linux IO函数

read/write函数&#xff1a; 1.read #include <unistd.h> ssize_t read(int fd, void *buf, size_t count); 参数&#xff1a; -fd&#xff1a;文件描述符&#xff0c;open得到的&#xff0c;通过这个文件描述符操作某个文件 -buf&#xff1a;需要读取数据存放的地方…

3D虚拟情景实训教学系统在法律课堂中的应用

3D虚拟情景实训教学系统是一种基于虚拟现实技术的教育工具&#xff0c;它通过模拟真实的法律场景&#xff0c;让学生能够身临其境地体验法律实践。这种教学方式可以让学生更加深入地理解法律原则和规则&#xff0c;提高他们的法律素养和实践能力。 在传统的法律课堂中&#xff…

一文了解国自然热点“超级增强子”的重要标记——H3K27ac

2023国自然结果已经揭晓&#xff0c;“超级增强子”&#xff08; Super enhancer, SE&#xff09;作为国自然新热点&#xff0c;2023年项目为32个。2019-2023年来总累计项目143项&#xff0c;但累计项目金额达6033万。此外&#xff0c;Pubmed数据统计显示5年间SE影响因子大于10…

二叉树【树的基本概念】

全文目录 树树的概念树的相关概念树的表示树的实际应用 二叉树二叉树的概念二叉树的特殊类型二叉树的性质二叉树的存储结构顺序存储链式存储 堆堆的概念向下调整算法向上调整算法堆的插入堆的删除堆的构建时间复杂度计算 堆排序TOP-K问题 树 树的概念 树是一种非线性的数据结…

华为CD32键盘使用教程

华为CD32键盘使用教程 用爱发电写的教程&#xff01; 最后更新时间&#xff1a;2023.9.12 型号&#xff1a;华为有线键盘CD32 基本使用 此键盘在不安装驱动的情况下可以直接使用&#xff0c;但是不安装驱动指纹识别是无法使用的&#xff01;并且NFC功能只支持华为的部分电脑…

固定资产预算怎么管理的

在现代企业管理中&#xff0c;固定资产预算的管理是一项至关重要的任务。它不仅关系到企业的经济效益&#xff0c;更关系到企业的长远发展。那么&#xff0c;如何进行有效的固定资产预算管理呢&#xff1f; 明确固定资产预算的目标和原则  我们需要明确固定资产预算的目标和…