当前位置: 首页 > news >正文

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

秒杀优化-异步秒杀思路

未优化的思路

当用户发起请求,此时会请求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@Transactionalpublic 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);
//存入redisstringRedisTemplate.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.库存不足,返回1return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then-- 3.3.存在,说明是重复下单,返回2return 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脚本,判断是否抢购成功,如果抢购成功,要放入堵塞队列中

@Overridepublic 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();//订单idvoucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);voucherOrder.setId(orderId);//将订单信息放入阻塞队列orderTakes.add(voucherOrder);return Result.ok(orderId);}

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

//从阻塞队列里面取订单信息private class voucherOrderHander implements Runnable {@Overridepublic 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,保证该对象被创建时,线程也能顺利创建,这里用了线程池来提交线程任务

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

 完整代码实现

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Autowiredprivate IVoucherOrderService voucherOrderService;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate 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);}@PostConstructpublic void init() {SECKILL_ORDER_EXECUTOR.execute(new voucherOrderHander());}//从阻塞队列里面取用户信息private class voucherOrderHander implements Runnable {@Overridepublic 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();}}}@Overridepublic 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();//订单idvoucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);voucherOrder.setId(orderId);//将订单信息放入阻塞队列orderTakes.add(voucherOrder);return Result.ok(orderId);}
@Transactionalpublic 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);}

相关文章:

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

秒杀优化-异步秒杀思路 未优化的思路 当用户发起请求&#xff0c;此时会请求nginx&#xff0c;nginx会访问到tomcat&#xff0c;而tomcat中的程序&#xff0c;会进行串行操作&#xff0c;分成如下几个步骤 1、查询优惠卷 2、判断秒杀库存是否足够 3、查询订单 4、校验是否是一…...

Python爬虫-IP隐藏技术与代理爬取

前言 在进行爬虫程序开发和运行时&#xff0c;常常会遇到目标网站的反爬虫机制&#xff0c;最常见的就是IP封禁&#xff0c;这时需要使用IP隐藏技术和代理爬取。 一、IP隐藏技术 IP隐藏技术&#xff0c;即伪装IP地址&#xff0c;使得爬虫请求的IP地址不被目标网站识别为爬虫。…...

二刷力扣--链表

链表 链表类型&#xff1a; 单链表&#xff08;可以访问后面的一个节点&#xff09; 双链表&#xff08;可以访问前后节点&#xff09; 循环链表&#xff08;最后一个节点指向首节点&#xff09; 在Python中定义单链表节点&#xff1a; class ListNode:def __init__(self, v…...

返回值加const ,为了不拷贝得到成员的值,但被赋值的左值也要const

1. getA 函数返回值 什么都不加&#xff0c;也改不了c里面a的指针指向 why&#xff1f;返回成员变量时&#xff0c;会复制一下。 返回成员变量时&#xff0c;一般会赋值一下没有RVO_地摊书贩的博客-CSDN博客 2. getA 函数返回值 加了引用&#xff0c; 就没有复制 3. getA 函数…...

本地如何使用HTTPS进行调试

在现代前端开发中&#xff0c;HTTPS已经成为不可或缺的一部分&#xff0c;因为它在保护用户数据和确保网站安全性方面发挥着关键作用。然而&#xff0c;有时在本地开发过程中启用HTTPS可能会变得有些复杂。在本文中&#xff0c;我们将介绍如何轻松地在本地进行HTTPS调试&#x…...

观察者模式:对象之间的订阅机制

欢迎来到设计模式系列的第十三篇文章&#xff01;在之前的文章中&#xff0c;我们学习了许多常用的设计模式&#xff0c;今天我们将介绍观察者模式&#xff0c;它是一种行为型设计模式&#xff0c;用于定义对象之间的一对多依赖关系&#xff0c;当一个对象的状态发生变化时&…...

【1462. 课程表 IV】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 你总共需要上 numCourses 门课&#xff0c;课程编号依次为 0 到 numCourses-1 。你会得到一个数组 prerequisite &#xff0c;其中 prerequisites[i] [ai, bi] 表示如果你想选 bi 课程&#xff0c;你…...

Kerberos 身份验证

简介 Kerberos 是一种由 MIT&#xff08;麻省理工大学&#xff09;提出的一种基于加密 Ticket 的身份认证协议。它旨在通过使用密钥加密技术为客户端/服务器应用程序提供强身份验证&#xff0c;用于验证用户或主机的标识。。 适用范围&#xff1a;Windows Server 2022、Window…...

R语言贝叶斯METROPOLIS-HASTINGS GIBBS 吉布斯采样器估计变点指数分布分析泊松过程车站等待时间...

原文链接&#xff1a;http://tecdat.cn/?p26578 指数分布是泊松过程中事件之间时间的概率分布&#xff0c;因此它用于预测到下一个事件的等待时间&#xff0c;例如&#xff0c;您需要在公共汽车站等待的时间&#xff0c;直到下一班车到了&#xff08;点击文末“阅读原文”获取…...

通付盾入选2023年度“上市苗圃工程”重点企业

近日&#xff0c;2023年度苏州工业园区企业上市苗圃工程认定名单公示&#xff0c;江苏通付盾科技有限公司成功入选园区“上市苗圃工程”重点企业。 2023年第一批次苗圃企业认定结果&#xff1a; 企业上市苗圃工程 上市企业是衡量地方综合经济实力的重要标尺&#xff0c;也是区…...

SpringMVC之文件上传下载

SpringMVC是一个基于Java的Web框架&#xff0c;它提供了一套用于构建Web应用程序的开发模型。在SpringMVC中&#xff0c;文件上传和下载是常见的功能之一。 SpringMVC文件上传和下载的介绍&#xff1a; 介绍文件上传&#xff1a; 在SpringMVC中&#xff0c;文件上传功能可以通…...

嵌入式IDE(2):KEIL中SCF分散加载链接文件详解和实例分析

在上一篇文章IAR中ICF链接文件详解和实例分析中&#xff0c;我通过I.MX RT1170的SDK中的内存映射关系&#xff0c;分析了IAR中的ICF链接文件的语法。对于MCU编程所使用的IDE来说&#xff0c;IAR和Keil用得比较多&#xff0c;所以这一篇文章就来分析一下Keil的分散文件.scf(scat…...

Linux防火墙常用操作及端口开放

Linux防火墙常用操作及端口开放 1.查看防火墙状态 firewall-cmd --state 2.开启防火墙 systemctl start firewalld.service 3.开启指定端口 firewall-cmd --zonepublic --add-port3306/tcp --permanent firewall-cmd --zonepublic --add-port6379/tcp --permanent 显示success表…...

[JAVAee]Linux上的javax.mail报错

我们把在window写的项目部署到Linux上的Tomcat时,如果发现使用不了了,该如何找到错误呢?找到报错的地方在哪呢? 在Linux环境下来到Tomcat目录下的logs目录,输入: tail -f catalina.out -n 500 tail 就是把文件的末尾几行读取到终端上,并会持续刷新 -f 循环读取 catalina.ou…...

开学季|校园迎新哪家强?VR全景来导航

九月开学迎新季&#xff0c;各大高校的迎新活动开展的如火如荼&#xff0c;随着科技的不断进步&#xff0c;高校为了更好的开展迎新活动&#xff0c;让新生们尽快熟悉新的校园和生活&#xff0c;会利用VR全景技术带领着新生进行校园游览&#xff0c;给予新生们巨大便利的同时&a…...

el-checkbox-group限制勾选数量

<!--* Description: 视频监控 页面* Author: mhf* Date: 2023-08-15 13:26:33 --> <template><div class"videoSurveillance"><el-row :gutter"24"><el-col :span"4"><div class"videoSurveillance-left&…...

【JavaScript】WebAPI入门到实战

文章目录 一、WebAPI背景知识1. 什么是WebAPI&#xff1f;2. 什么是API&#xff1f; 二、DOM基本概念三、获取元素三、事件初识1. 点击事件2. 键盘事件 四、操作元素1. 获取/修改元素内容2. 获取/修改元素属性3. 获取/修改表单元素属性4. 获取/修改样式属性 五、操作节点1. 新增…...

奥康的高尔夫鞋,圈不住投资者的心

文 | 螳螂观察 作者 | 青月 鞋服行业终于熬过了“寒冬”&#xff0c;2023年行业景气度开始逐步回暖。 东方财富Choice数据显示&#xff0c;截至8月17日&#xff0c;已有28家鞋帽服装类上市公司发布了2023年中期业绩预告或快报&#xff0c;其中&#xff0c;9家预增&#xff0…...

vue2配置环境变量并且nginx运行成功

需求&#xff1a;我在vue项目配置了生产环境和开发环境&#xff0c;之后通过proxy代理的方式把地址转发到真实的服务器地址上用于请求接口&#xff0c;之后把项目打包后上传到nginx上&#xff0c;之后接口报错404&#xff0c;但是本地运行是可以访问的&#xff0c;找了很久终于…...

Java+Swing形成GUI图像界面

一、Swing 简介 Swing 主要用来开发 GUI 程序,GUI(Graphical User Interface)即图形用户界面。Java 中针对 GUI 设计提供了丰富的类库,这些类分别位于 java.awt 和 java.swing 中,简称 AWT 和 Swing ;其中,AWT(Abstract Window Toolkit)是抽象窗口工具包,是 Java 平…...

编辑距离 -- 动规

72. 编辑距离 给出动规的两种常见实现形式&#xff1a;自顶向下、自底向上&#xff0c;前者一般借助递归函数备忘录实现&#xff0c;后者通常基于dp数组实现。 class MinDistance:"""72. 编辑距离https://leetcode.cn/problems/edit-distance/""&quo…...

douyin【商品抢购js脚本】

文章目录 前言订阅须知知识点源码前言 脚本主要用来实现抢购douyin商城、直播间秒杀商品等一系列商品 订阅须知 订阅后,只提供js源代码,不提供教学,请根据源码自行抓包知识点 1、在查询串插入一个固定的键rstr   2、对查询串进行按键排序并取值,对空格和+进行转义为a …...

常见Web安全技术总结!474页Web安全从入门到精通(附PDF)

Web安全范围比较大&#xff0c;知识点比较杂&#xff0c;很多朋友都无从下手&#xff0c;这不可怕&#xff0c;可怕的是乱下手&#xff0c;其实往往基础才是决定你是否能走远的关键。 为了帮助大家入门网安&#xff0c;给大家推荐一份《新手Web安全入门到精通》&#xff0c;共…...

Prometheus 监控指南:如何可靠地记录数字时间序列数据

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f405;&#x1f43e;猫头虎建议程序员必备技术栈一览表&#x1f4d6;&#xff1a; &#x1f6e0;️ 全栈技术 Full Stack: &#x1f4da…...

rsync远程同步+inotify监控

目录 一、Rsync 简介 1、rsync是什么 2、备份的方式 3、rsync同步方式 4、常用rsync命令 5、配置源的两种表达方法 二、rsync实验 1、本地复制 ​编辑​编辑 2、异地复制 2.1 rsync服务器配置 2.2 rsync客户端配置 2.2.1 普通同步 2.2.2 免密同步 2.2.3 --delet…...

【面试经典150 | 数组】移除元素

文章目录 写在前面Tag题目来源题目解读解题思路方法一&#xff1a;原地操作 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并附带一些对于本题涉及到的数据结构等…...

玩转Mysql系列 - 第21篇:什么是索引?

这是Mysql系列第21篇。 本文开始连续3篇详解mysql索引&#xff1a; 第1篇来说说什么是索引&#xff1f; 第2篇详解Mysql中索引的原理 第3篇结合索引详解关键字explain 本文为索引第一篇&#xff1a;我们来了解一下什么是索引&#xff1f; 路人在搞计算机之前&#xff0c;…...

预处理指令

// The include directive instructs the preprocessor to paste the text of the given file into the current file. // 粘贴指定文件的内容 #include // 定义宏PI #define PI 3.1415926 // 取消定义PI #undef PI条件编译(Conditional Compilation) // 检查xxx是否已被定义为…...

强大的JTAG边界扫描(1):基本原理介绍

文章目录 1. 什么是边界扫描&#xff1f;2. JTAG硬件接口3. 边界扫描相关的软硬件4. 学习资料5. 总结 我是怎么了解到边界扫描的呢&#xff1f; 这就要从我淘到一块FPGA板卡的事情说起了。 前段时间我在某二手平台上淘了一块FPGA板子&#xff0c;它长这样&#xff1a; 板子的…...

【C++】源文件.cpp和头文件.h分离编程

优势介绍 将C代码分为头文件&#xff08;.h&#xff09;和源文件&#xff08;.cpp&#xff09;的做法有以下几个好处&#xff1a; 模块化和代码组织&#xff1a;将函数和类的声明&#xff08;包括函数原型、类的成员和属性等&#xff09;放在头文件中&#xff0c;将函数和类的…...

诛仙3官方网站时竹任务荧灵怎么做/百度app下载最新版

oracle的order by 中文排序原理 写SQL时&#xff0c;发现oracle的order by中文排序并不是完全按照拼音排序的 21鄞&#xff08;yin&#xff09;通集团PPN001 和 21舟山交投PPN002 这两个名称&#xff0c;按照拼音来说&#xff0c;应该Y在Z前面&#xff0c;百思不得其解&#x…...

织梦系统如何做网站地图/软文发稿公司

在数组中就地移除value的元素&#xff0c;并且不用管移除后的顺序稳定性。 大致思路&#xff1a; 如果要前移来删除数组元素的话要O(n^2)。 这里的方法是——两个指针&#xff08;发现两个指针在“就地处决”真的很重要诶...&#xff09;&#xff0c;一个一直指向最后一个元…...

连云港网站建设公司/成都seo网络优化公司

1、%s&#xff1a;字符型 %s&#xff1a;正常输出字符 %6s&#xff1a;输出6位字符&#xff0c;不足6位字符的左边用空格符补充&#xff0c;超过6位字符的输出全部字符&#xff08;%-6d为右边用空格补充&#xff09; %.2s&#xff1a;截取前2位字符&#xff0c;不足2位的左边…...

列举五种常用的网站推广方法/太原百度公司地址

现象&#xff1a; 某天点了下File---->Invalidate Caches/Restart&#xff0c;重新打开工程后&#xff0c;发现工具栏build下clean project,Generate Signed APK…啥都没了。网上找了好久&#xff0c;莫得办法&#xff0c;只能卸了重装&#xff0c;结果还是那个叼样。 解决办…...

遂昌建设局网站/seo小白入门

上一篇讲了如何在Site Definition中引入Maste Page&#xff0c;作为实际操作这是必要的步骤&#xff0c;但仅仅这样是肯定不够的&#xff0c;因为我们不可能不对Master Page的外观进行设计就部署并使用它了。而外观的设计又必须要涉及到CSS等资源的引入与使用。这里我们就继续上…...

南通政府网站建设/网上网络推广

在 webpack-dev-server 的配置中添加disableHostCheck: true转载于:https://www.cnblogs.com/xiaoroad/p/8967353.html...