基于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,一经查实,立即删除!

相关文章

[leetcode hot 150]第二十三题,合并K个升序链表

题目&#xff1a; 给你一个链表数组&#xff0c;每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中&#xff0c;返回合并后的链表。 示例 1&#xff1a; 输入&#xff1a;lists [[1,4,5],[1,3,4],[2,6]] 输出&#xff1a;[1,1,2,3,4,4,5,6] 解释&#xff1a…

解析Java中1000个常用类:Dictionary类,你学会了吗?

在线工具站 推荐一个程序员在线工具站:程序员常用工具(http://cxytools.com),有时间戳、JSON格式化、文本对比、HASH生成、UUID生成等常用工具,效率加倍嘎嘎好用。程序员资料站 推荐一个程序员编程资料站:程序员的成长之路(http://cxyroad.com),收录了一些列的技术教程…

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

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

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

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

WHAT - React useReducer vs Redux

useReducer 和 Redux 都是用于管理应用程序状态的工具&#xff0c;但它们有几点不同之处&#xff1a; useReducer React 内置钩子&#xff1a; useReducer 是 React 提供的一个内置 Hook&#xff0c;用于在函数式组件中管理局部状态。可以通过定义一个 reducer 函数来处理状态…

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

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

555定时器

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

MyBatis 的知识要点,面试多半会被问到的知识点

1、什么是 MyBatis? MyBatis 是一款优秀的支持自定义 SQL 查询、存储过程和高级映射的持久层框架&#xff0c;消除了 几乎所有的 JDBC 代码和参数的手动设置以及结果集的检索 。 MyBatis 可以使用 XML,或注解进 行配置和映射&#xff0c;MyBatis 通过将参数映射到配置的 SOL,形…

【设计模式之美】策略模式方法论:解耦策略的定义、创建和使用

文章目录 一. 策略的定义-封装策略&#xff0c;面向接口二. 策略的创建-创建策略工厂1. 对于无状态策略2. 对于有状态策略 三. 策略的使用&#xff1a;动态选择四. 避免分支判断-策略的优雅1. 对于无状态的策略2. 对于有状态的策略 策略模式是定义一族算法类&#xff0c;将每个…

雅思词汇及发音积累 2024.7.6

地理方位 1.right 右边 2.left 左边 3.in front of 在前面 4.behind/rear 在后面 5.next to 在旁边 6.at the end of 在末端 7.opposite to 在对面 8.be far from 距离某处很远 9.be nearby 距离某处很近 10.go back/back/back up 向回走 11.go up/down 向上&#xff08;北&…

数据结构(3.3)——栈的链式存储结构

链栈的定义 采用链式存储的栈成为链栈&#xff0c;链栈的优点是便于多个栈共享存储空间和提高其效率&#xff0c;且不存在栈满上溢的情况。通常采用单链表实现。 typedef struct Linknode {int data; // 数据域struct Linknode* next; // 指针域 } LiStack; // 栈类…

常见的块元素、行内元素以及行内块元素,三者有何不同?

在HTML中&#xff0c;元素可以分为块级元素&#xff08;block-level elements&#xff09;、行内元素&#xff08;inline elements&#xff09;和行内块元素&#xff08;inline-block elements&#xff09;。它们之间的主要区别如下&#xff1a; 块级元素&#xff08;block-le…

【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;索引可…

centos 7系统升级内核(ELRepo仓库)、小版本升级、自编译内核

使用ELRepo仓库 ELRepo是一个第三方仓库&#xff0c;提供了最新的linux内核版本。 安装ELRepo密钥&#xff1a; rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org 安装ELRepo仓库&#xff1a; rpm -Uvh http://www.elrepo.org/elrepo-release-7.0-2.el7.elre…

Spring Boot与GraphQL的集成

Spring Boot与GraphQL的集成 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们将探讨如何在Spring Boot应用中集成GraphQL&#xff0c;这是一种强大的API…

Vue2中跨组件共享公共属性的方法、优缺点与实现

一、vuex&#xff08;最常用&#xff09; 优缺点 优点&#xff1a;集中管理状态&#xff0c;组件间解耦&#xff0c;易于调试和测试。缺点&#xff1a;学习成本较高&#xff0c;对于小项目可能过于复杂。 适用场景 大型、复杂的单页面应用&#xff08;SPA&#xff09;。需要全局…

Apache Seata配置管理原理解析

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

Linux系统基础命令行指令——Ubuntu

基础指令 更新指令 sudo apt update sudo apt upgrade 切换超级管理员 su root 切换路径 //相对、绝对 cd 路径回上一级路径 cd ..cd ../.. 退两级路径 查看当前目录 pwd查看指定路径内容 ls //常见搭配 ls -al 创建目录 mkdir 路径 创建文件 touc…