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

异步前

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

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

异步优化方案

 

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

整体业务逻辑如下:

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

优惠券信息保存到redis

修改添加秒杀券的代码,在添加秒杀券的同时把信息也保存到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().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) thenreturn 1
end
--3.2判断用户是否下单   若set集合中存在该用户id,则说明已下过单,返回1
if (tonumber(redis.call('sismember',orderKey,userId))==1) thenreturn 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 {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate ISeckillVoucherService SeckillVoucherService;@Autowiredprivate RedissonClient redissonClient;@Resourceprivate 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);}@Overridepublic 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,把订单信息保存到阻塞队列,由另一个线程专门根据订单信息去数据库做增删改查,这就实现了异步//TODOreturn 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{@Overridepublic void run() {while (true){try {//获取队列中的订单VoucherOrder order = orderTasks.take();//创建订单handleVoucherOrder(order);} catch (InterruptedException e) {log.error("订单处理异常",e);}}}}//spring提供的注解  作用:类初始化后就执行VoucherOrderHandler方法
//向线程池提交一个线程@PostConstructprivate 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 {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate ISeckillVoucherService SeckillVoucherService;@Autowiredprivate RedissonClient redissonClient;@Resourceprivate RedisIdWorker redisIdWorker;//阻塞队列  当线程尝试从队列中获取元素时,若队列无元素,则线程会阻塞,直到队列中有元素才会被唤醒private BlockingQueue<VoucherOrder> orderTasks=new ArrayBlockingQueue<>(1024*1024);//线程池,负责从阻塞队列中获取订单然后异步下单private static final ExecutorService SECKILL_ORDER_EXECUTOR= Executors.newSingleThreadExecutor();//spring提供的注解  作用:类初始化后就执行VoucherOrderHandler方法//向线程池提交一个线程@PostConstructprivate void init(){SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());}//线程  内部类private class VoucherOrderHandler implements Runnable{@Overridepublic 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);}@Overridepublic 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对该类做了动态代理,用动态代理的对象提交的事务@Transactionalpublic 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.mzph.cn/pingmian/41925.shtml

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

相关文章

IDEA越用越卡?教你轻松解决IDEA内存占用过高问题

大家好&#xff0c;我是瑶山&#xff0c;最近IDEA越用越卡了&#xff0c;刚刚内存卡爆&#xff0c;带着整个电脑也卡的飞起&#xff0c;只能重启了电脑。 虽然重启后又恢复到了流畅&#xff0c;但是问题还是如鲠在喉&#xff0c;痛定思痛&#xff0c;还是决定处理下&#xff01…

基于SpringBoot+Vue的招生管理系统(带1w+文档)

基于SpringBootVue的招生管理系统(带1w文档&#xff09; 通过招生管理系统的研究可以更好地理解系统开发的意义&#xff0c;而且也有利于发展更多的智能系统&#xff0c;解决了人才的供给和需求的平衡问题&#xff0c;招生管理系统的开发建设&#xff0c;由于其开发周期短&…

在 PostgreSQL 中,如何处理大规模的文本数据以提高查询性能?

文章目录 一、引言二、理解 PostgreSQL 中的文本数据类型三、数据建模策略四、索引选择与优化五、查询优化技巧六、示例场景与性能对比七、分区表八、数据压缩九、定期维护十、总结 在 PostgreSQL 中处理大规模文本数据以提高查询性能 一、引言 在当今的数据驱动的世界中&…

555定时器

硬件大杂烩 1. 555定时器内部结构 各引脚定义作用 引脚1: GND (地)&#xff0c;功能&#xff1a;接地&#xff0c;作为低电平(0V)。 引脚2: TRIG (触发)&#xff0c;功能&#xff1a;当此引脚电压降至1/3VCC (或由控制端决定的阈值电压)时&#xff0c;输出端给出高电平。 引…

【CUDA】 由GPGPU控制核心架构考虑CUDA编程中线程块的分配

GPGPU架构特点 由于典型的GPGPU只有小的流缓存&#xff0c;因此一个存储器和纹理读取请求通常需要经历全局存储器的访问延迟加上互连和缓冲延迟&#xff0c;可能高达数百个时钟周期。与CPU通过巨大的工作集缓存而降低延迟不同&#xff0c;GPU硬件多线程提供了数以千计的并行独…

YOLOv8改进 添加轻量级注意力机制ELAttention

一、ELA论文 论文地址:2403.01123 (arxiv.org) 二、Efficient Local Attention结构 ELA (Efficient Local Attention) 被用于处理自然语言处理任务中的序列数据。它旨在提高传统注意力机制的效率,并减少其计算和存储成本。 在传统的注意力机制中,计算每个输入位置与所有其…

MYSQL 四、mysql进阶 6(索引的创建与设计原则)

一、索引的声明和使用 1.1 索引的分类 MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索引等。 从 功能逻辑 上说&#xff0c;索引主要有 4 种&#xff0c;分别是普通索引、唯一索引、主键索引、全文索引。 按照 物理实现方式 &#xff0c;索引可…

Apache Seata配置管理原理解析

本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 Apache Seata配置管理原理解析 说到Seata中的配置管理&#xff0c;大家可能会想到Seata中适配…

47.HOOK引擎优化支持CALL与JMP位置做HOOK

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 上一个内容&#xff1a;46.修复HOOK对代码造成的破坏 以 46.修复HOOK对代码造成的破坏 它的代码为基础进行修改 优化的是让引擎支持从短跳JMP&#xff08;E9&…

美光科技在2024年1γ工艺技术在10纳米级别启动EUV试产

美光科技&#xff08;Micron&#xff09;在2024年针对其1γ&#xff08;1-gamma&#xff09;工艺技术在10纳米级别启动EUV&#xff08;极紫外光刻&#xff09;试产&#xff0c;这标志着存储行业巨头在EUV采用上的重要一步&#xff0c;尽管相比英特尔和台积电等其他半导体制造商…

【PWN · ret2shellcode | sandbox-bypass | 格式化字符串】[2024CISCN · 华东北赛区]pwn1_

一道栈ret2shellcodesandbox&#xff08;seccomp&#xff09;格式化字符串的题目 前言 ret2shellcode&#xff0c;已经不是简单的放到栈上、ret这样一个简单的过程。套一层seccomp的沙箱&#xff0c;打ORW又遇到open受限等等&#xff0c;考虑的蛮多。过程中收获最多的可以说是…

Hugging face Transformers(2)—— Pipeline

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

【系统架构设计师】计算机组成与体系结构 ⑩ ( 磁盘管理 | 磁盘移臂调度算法 | 先来先服务算法 | 最短寻道时间优先 | 扫描算法 | 循环扫描算法 )

文章目录 一、磁盘移臂调度算法1、磁盘移臂调度算法简介2、先来先服务算法3、最短寻道时间优先4、扫描算法5、循环扫描算法 二、最短寻道时间优先算法示例 一、磁盘移臂调度算法 1、磁盘移臂调度算法简介 磁盘 数据块读取 的 性能 主要由 寻道时间旋转延时 决定 ; 旋转延时 …

ROS 2官方文档(基于humble版本)学习笔记(四)

ROS 2官方文档&#xff08;基于humble版本&#xff09;学习笔记&#xff08;四&#xff09; 2.客户端库使用colcon构建包&#xff08;package&#xff09;创建工作空间&#xff08;workspace&#xff09;构建工作空间执行测试&#xff08;tests&#xff09;导入环境&#xff08…

第十四届蓝桥杯省赛C++B组G题【子串简写】题解(AC)

题目大意 给定字符串 s s s&#xff0c;字符 a , b a, b a,b&#xff0c;问字符串 s s s 中有多少个 a a a 开头 b b b 结尾的子串。 解题思路 20pts 使用二重循环枚举左端点和右端点&#xff0c;判断是否为 a a a 开头 b b b 结尾的字符串&#xff0c;是则答案加一…

Stable Diffusion:最全详细图解

Stable Diffusion&#xff0c;作为一种革命性的图像生成模型&#xff0c;自发布以来便因其卓越的生成质量和高效的计算性能而受到广泛关注。不同于以往的生成模型&#xff0c;Stable Diffusion在生成图像的过程中&#xff0c;采用了独特的扩散过程&#xff0c;结合深度学习技术…

2024亚太杯数学建模竞赛(B题)的全面解析

你是否在寻找数学建模比赛的突破点&#xff1f;数学建模进阶思路&#xff01; 作为经验丰富的数学建模团队&#xff0c;我们将为你带来2024亚太杯数学建模竞赛&#xff08;B题&#xff09;的全面解析。这个解决方案包不仅包括完整的代码实现&#xff0c;还有详尽的建模过程和解…

【C++:类的基础认识和this指针】

C的类与C语言的struct结构体有啥区别&#xff1f; 默认的访问限定符不同 类的简要 关键字&#xff1a;class{}里面是类的主体&#xff0c;特别注意&#xff1a;{}后面的&#xff1b;不可以省略类中的变量叫做成员变量&#xff0c;类中的函数叫做成员函数类中访问有三种访问权限…

单/多线程--协程--异步爬虫

免责声明:本文仅做技术交流与学习... 目录 了解进程和线程 单个线程(主线程)在执行 多线程 线程池 协程(爬虫多用) 假异步:(同步) 真异步: 爬虫代码模版 异步-爬虫 同步效果--19秒 异步效果--7秒 了解进程和线程 ​ # --------------------> # ------> # …

MinIO:开源对象存储解决方案的领先者

MinIO:开源对象存储解决方案的领先者 MinIO 是一款开源的对象存储系统&#xff0c;致力于提供高性能、可伸缩、安全的数据存储解决方案。 官方解释&#xff1a;MinIO 是一个基于Apache License v2。0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口&#xff0c;非常适…