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);}

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

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

相关文章

关于try..catch..finally..throw

try 每个try块必须与至少一个catch或finally块&#xff0c;否则会抛出SyntaxError错误。 单独使用try块进行验证&#xff1a; try { throw new Error(Error while executing the code); } 1 2 3 ⓧ Uncaught SyntaxError: Missing catch or finally after try 1 try…catch …

大厂超全安全测试--关于安全测试的分类及如何测试

安全测试&#xff08;总结&#xff09; 1.jsonNP劫持 &#xff08;其实json劫持和jsonNP 劫持属于CSRF跨站请求伪造&#xff09;的攻击范畴&#xff0c;解决方法和CSRF一样 定义&#xff1a;构造带有jsonp接口的恶意页面发给用户点击&#xff0c;从而将用户的敏感信息通过js…

Jtti:Ubuntu下如何迁移Thunderbird邮件

要迁移Thunderbird邮件&#xff0c;您需要复制和转移Thunderbird的邮件文件和配置文件。以下是一般的迁移步骤&#xff1a; 注意&#xff1a;在进行迁移之前&#xff0c;请确保Thunderbird已经安装在目标Ubuntu系统上。 备份旧系统的Thunderbird数据&#xff1a; 在源系统上打开…

MySQL——主从复制

简介 在实际的生产中&#xff0c;为了解决Mysql的单点故障已经提高MySQL的整体服务性能&#xff0c;一般都会采用「主从复制」。 主从复制开始前有个前提条件&#xff1a;两边的数据要一样&#xff0c;主必须开启二进制日志 dump thread 线程 基于位置点从是否需要开启二进…

【面试刷题】——什么是面向过程 什么是面向对象

"面向过程"和"面向对象"是两种不同的编程范式&#xff0c;它们描述了程序的设计和组织方式。 面向过程编程&#xff08;Procedural Programming&#xff09;&#xff1a; 面向过程编程是一种以过程或函数为中心的编程范式。在面向过程编程中&#xff0c;…

计算机组成原理——基础入门总结(一)

本帖更新一些关于计算机组成原理的重点内容。由于博主考研时并不会考这门课&#xff0c;但是考虑到操作系统中又很多重要晦涩的概念涉及很多诸如内存、存储器、磁盘、cpu乃至各种寄存器的知识&#xff0c;此处挑选一些核心的内容总结复盘一遍——实现声明&#xff1a;本帖的内容…

Python统计pdf中英文单词的个数

之前的文章提供了批量识别pdf中英文的方法,详见【python爬虫】批量识别pdf中的英文,自动翻译成中文上。以及自动pdf英文转中文文档,详见【python爬虫】批量识别pdf中的英文,自动翻译成中文下。    本文实现python统计pdf中英文字符的个数。 文章目录 一、要统计字符的pdf…

第16篇ESP32 platformio_arduino框架 wifi联网_连接WiFi热点并连接tcp server收发数据进行通讯

第1篇:Arduino与ESP32开发板的安装方法 第2篇:ESP32 helloword第一个程序示范点亮板载LED 第3篇:vscode搭建esp32 arduino开发环境 第4篇:vscodeplatformio搭建esp32 arduino开发环境 ​​​​​​第5篇:doit_esp32_devkit_v1使用pmw呼吸灯实验 第6篇:ESP32连接无源喇叭播…

许可分析 license分析 第十一章

许可分析是指对软件许可证进行详细的分析和评估&#xff0c;以了解组织内部对软件许可的需求和使用情况。通过许可分析&#xff0c;可以帮助组织更好地管理和优化软件许可证的使用。以下是一些可能的许可分析方法和步骤&#xff1a; 软件许可证监管合规性&#xff1a;严格遵守软…

Linux 文件创建、查看

touch、cat、more命令 ①touch命令——创建文件 ②cat命令——查看文件内容全部显示 这是txt.txt文件内容 使用cat命令查看 ③more命令——查看文件内容支持翻页 在查看的过程中&#xff0c;通过空格翻页&#xff0c;通过q退出查看

MySql(随记)

一条MySql执行过程 首先Mysql的架构分为两层&#xff0c;Server层和存储引擎层。 Server层&#xff1a;MySql大多数核心功能&#xff0c;主要包括&#xff0c;连接器&#xff0c;查询缓存&#xff0c;解释器&#xff0c;预处理器&#xff0c;优化器&#xff0c;执行器等 存储引…

C语言练习题解析(2)

&#x1f493;博客主页&#xff1a;江池俊的博客⏩收录专栏&#xff1a;C语言刷题专栏&#x1f449;专栏推荐&#xff1a;✅C语言初阶之路 ✅C语言进阶之路&#x1f4bb;代码仓库&#xff1a;江池俊的代码仓库&#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐ 文…

Anaconda bug

报错如下&#xff1a; DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): repo.anaconda.com:443 DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): repo.anaconda.com:443 DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1):…

蓝桥杯2023年第十四届省赛真题-更小的数--题解

目录 蓝桥杯2023年第十四届省赛真题-更小的数 题目描述 输入格式 输出格式 样例输入 样例输出 提示 【思路解析】 【代码实现】 蓝桥杯2023年第十四届省赛真题-更小的数 时间限制: 3s 内存限制: 320MB 提交: 895 解决: 303 题目描述 小蓝有一个长度均为 n 且仅由数字…

C2基础设施威胁情报对抗策略

威胁情报是指在信息安全和安全防御领域&#xff0c;收集、分析和解释与潜在威胁相关的信息&#xff0c;以便预先发现并评估可能对组织资产造成损害的潜在威胁&#xff0c;是一种多维度、综合性的方法&#xff0c;其通过信息的收集、分析和研判&#xff0c;帮助组织了解可能对其…

准备篇(二)Python 教程

Part 1 Python 基础语法区分输入与输出注释文本列表if 语句for 语句range() 函数走向编程的第一个例子Part 2 函数 和 数据结构函数数据结构del 语句列表详解元组集合字典循环的技巧Part 3 输入与输出读写文件打开文件 open()读文件写文件

C++---继承

继承 前言继承的概念及定义继承的概念继承定义继承关系和访问限定符 基类和派生类对象赋值转换继承中的作用域派生类的默认成员函数继承与友元继承与静态成员**多重继承**多继承下的类作用域菱形继承虚继承使用虚基类 支持向基类的常规类型转换 前言 在需要写Father类和Mother…

征战开发板从无到有(三)

接上一篇&#xff0c;翘首已盼的PCB板子做好了&#xff0c;管脚约束信息都在PCB板上体现出来了&#xff0c;很满意&#xff0c;会不会成为爆款呢&#xff0c;嘿嘿&#xff0c;来&#xff0c;先看看PCB裸板美图 由于征战开发板电路功能兼容小梅哥ACX720&#xff0c;大家可以直…

【MySQL】 MySQL数据库基础

文章目录 &#x1f431;‍&#x1f453;数据库的操作&#x1f4cc;显示当前的数据库&#x1f4cc;创建数据库&#x1f388;语法&#xff1a;&#x1f388;语法说明&#x1f388;示例&#xff1a; &#x1f334;使用数据库&#x1f38b;删除数据库&#x1f431;‍&#x1f3cd;语…

Stable DIffusion 炫酷应用 | AI嵌入艺术字+光影光效

目录 1 生成AI艺术字基本流程 1.1 生成黑白图 1.2 启用ControlNet 参数设置 1.3 选择大模型 写提示词 2 不同效果组合 2.1 更改提示词 2.2 更改ControlNet 2.2.1 更改模型或者预处理器 2.2.2 更改参数 3. 其他应用 3.1 AI光影字 本节需要用到ControlNet&#xff0c;可…