Redis:原理速成+项目实战——Redis实战9(秒杀优化)
👨🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:Redis:原理速成+项目实战——Redis实战8(基于Redis的分布式锁及优化)
📚订阅专栏:Redis:原理速成+项目实战
希望文章对你们有所帮助
简单回顾一下之前实现秒杀的思路,其实无非就是2点:
1、库存够不够,该用户有没有买过
2、操作数据库(扣库存、创建订单)
之前的业务由于涉及了大量数据库的操作,所以性能并不是太好。
秒杀优化
- 异步秒杀思路
- Redis实现秒杀资格的判断
- 分析
- 实现
- 基于阻塞队列实现秒杀异步下单
- 总结及发现问题
异步秒杀思路
之前的业务,可以用下图来表示:
可以发现,Tomcat中顺序执行的操作里面有4个需要对数据库进行查询或修改操作,而MySQL本身的并发能力就是很差的,时间是所有时间之和,这种解决方式并不好。
因此,我们需要将操作数据库的步骤分给多个线程来做,而库存量判断、是否具有购买资格的判断,我们也可以将其交给Redis,从而提高效率。
既然是将步骤分给多个线程来做,我们要开辟线程,并且要使得开辟的线程能够正确执行业务:
整个业务的流程被分开了,所需要的时间不再是所有的过程时间之和(完成下单的操作是异步执行的),而且整个业务基于Redis,将会大大提升业务的性能。
但这需要考虑2个难点问题:如何在Redis里面完成判断库存以及一人一单的校验?如何基于阻塞队列实现秒杀异步下单?
Redis实现秒杀资格的判断
分析
要在Redis里面判断库存量以及一人一单,我们肯定需要将优惠券的库存信息以及相关订单信息缓存到Redis中去,我们需要选取合适的数据结构来保存这两个信息。
库存很容易,因为只包含了库存量这个信息,直接使用String类型进行存储即可。
但要实现一人一单,我们要判断这个优惠券被哪些用户购买过。所以这个数据结构需要能够保存多个值,而又因为一人一单,所以我们要保存的用户的id显然是不能重复的。所以,set是很适合的。
因此业务流程可以很容易知道:
可以发现业务的步骤还是有很多步的,因此我们要保证步骤的原子性,因此上述的内容应当用Lua脚本来写。
实现
需求:
1、新增秒杀优惠券的同时,将优惠券的信息保存到Redis中
2、基于Lua脚本,判断秒杀库存、一人一单、决定用户是否抢购成功
3、若成功,将优惠券id与用户id进行封装,再存入阻塞队列
4、开启线程任务,不断从阻塞队列中获取信息,实现异步下单
1、在VoucherServiceImpl类中新增优惠券,同时保存到Redis中:
@Resourceprivate StringRedisTemplate stringRedisTemplate;@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);//保存秒杀库存到Redis中, SECKILL_STOCK_KEY = "seckill:stock:"stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());}
打开postman,进行测试:
添加成功:
2、在resources下创建seckill.lua,决定用户能否抢券成功:
-- 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) then-- 3.2 库存不足,返回1return 1
end
--3.2 判断用户是否下单,即判断用户id是不是这个set集合的成员
if(redis.call('sismember', orderKey, userId) == 1) then-- 3.2 存在,说明重复下单return 2
end
-- 3.4 扣库存
redis.call('incrby', stockKey, -1)
-- 3.5 下单(保存用户)
redis.call('sadd', orderKey, userId)
return 0
3、在VoucherOrderServiceImpl中修改seckillVoucher,修改业务,先调用Lua脚本执行,返回0即可将下单信息存入阻塞队列(存入阻塞队列的代码编写较为麻烦,暂时放着):
//秒杀优化,调用Lua的代码@Overridepublic Result seckillVoucher(Long voucherId) {//获取用户Long userId = UserHolder.getUser().getId();//执行Lua脚本,这里使用了静态代码块Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(), //这里我们没有key传,只需要传送一个空集合即可voucherId.toString(), userId.toString()//传其他类型的参数);//判断结果是否为0int r = result.intValue();if(r != 0){//不为0,没有购买资格return Result.fail(r == 1 ? "库存不足" : "不能重复下单");}//为0,有购买资格,将下单信息保存到阻塞队列long orderId = redisIdWorker.nextId("order");//TODO 保存阻塞队列,这边先空着,下一部分单独编写//返回订单idreturn Result.ok(orderId);}
打开postman进行测试,连续下单两次,证明下单资格判断的可行性:
当然数据库中的数据还是没有改变的, 异步下单还没做。
如果要进行压力测试的话,大家要自己构建出几百个用户,然后这些用户分别占一个线程进行下单,用jmeter进行测试,完成这些测试还是很繁琐的,但是因为这些操作都是基于Redis的,容易知道吞吐率肯定是变大了不少的。
基于阻塞队列实现秒杀异步下单
对于用户资格的判断已经完成了,假设用户具有秒杀的资格,这时候我们只需要独立开辟一个线程,去异步实现下单。因为用户只要具有这个资格,直接返回订单的id从而让用户去执行付款即可,而将订单的信息存入数据库并不需要严格的时效性,因此业务可行:
接下来,开启线程任务,不断从阻塞队列中获取信息,实现下单,这是整体性能变高的关键。
代码的实现,我们之所以选择阻塞队列,是因为阻塞队列具有一个重要的性质:当线程尝试获取阻塞队列的元素时,若队列中没有元素,该线程就会被阻塞,直到队列中获取到这个元素了。
另外代码中因为涉及了开辟独立线程去实现异步下单,因此我们需要准备好线程池与线程任务。
最终业务的全部代码实现如下:
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {//注入秒杀优惠券的service@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate RedissonClient redissonClient;public static final DefaultRedisScript<Long> SECKILL_SCRIPT;static {SECKILL_SCRIPT = new DefaultRedisScript<>();SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));//设置脚本位置SECKILL_SCRIPT.setResultType(Long.class);//配置返回值}//阻塞队列:当线程尝试从这个队列中获取元素,如果没有元素,那么该线程就会被阻塞,直到队列中获取到元素private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);//线程池private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();//线程任务,用户随时都能抢单,所以应该要在这个类被初始化的时候马上开始执行@PostConstruct //该注解表示在当前类初始化完毕以后立即执行private void init(){SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());}private class VoucherOrderHandler implements Runnable{@Overridepublic void run() {//不断从队列中取订单信息while (true){try {VoucherOrder voucherOrder = orderTasks.take();//创建订单,流程里面无须再加锁,加个锁就是做个兜底handleVoucherOrder(voucherOrder);} catch (Exception e) {//e.printStackTrace();log.error("创建订单异常", e);}}}}IVoucherOrderService proxy;//秒杀优化,调用Lua的代码@Overridepublic Result seckillVoucher(Long voucherId) {//获取用户Long userId = UserHolder.getUser().getId();//执行Lua脚本,这里使用了静态代码块Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(), //这里我们没有key传,只需要传送一个空集合即可voucherId.toString(), userId.toString()//传其他类型的参数);//判断结果是否为0int r = result.intValue();if(r != 0){//不为0,没有购买资格return Result.fail(r == 1 ? "库存不足" : "不能重复下单");}//为0,有购买资格,将下单信息保存到阻塞队列long orderId = redisIdWorker.nextId("order");//TODO 保存阻塞队列//先将用户id与订单id封装起来VoucherOrder voucherOrder = new VoucherOrder();voucherOrder.setId(orderId);voucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);//放入阻塞队列orderTasks.add(voucherOrder);//获取代理对象proxy = (IVoucherOrderService) AopContext.currentProxy();//返回订单idreturn Result.ok(orderId);}private void handleVoucherOrder(VoucherOrder voucherOrder) {//获取用户,用户id不能再从UserHolder中取了,因为现在是从线程池获取的全新线程,不是主线程Long userId = voucherOrder.getUserId();//创建锁对象RLock lock = redissonClient.getLock("lock:order:" + userId);//获取锁boolean isLock = lock.tryLock();//判断是否获取锁成功if(!isLock){log.error("不允许重复下单");//理论上不会发生return;}try {//这是主线程的一个子线程,无法直接获得代理对象,代理对象需要在主线程中获取,并设置成成员变量使得该子线程能够获取//createVoucherOrder的方法体直接修改,不再需要重新根据id创建订单,而是直接将订单传进去proxy.createVoucherOrder(voucherOrder);} finally {lock.unlock();}}@Transactionalpublic void createVoucherOrder(VoucherOrder voucherOrder) {Long userId = voucherOrder.getUserId();//查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();//判断是否存在if (count > 0) {log.error("不可重复购买");return;}//扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0).update();if (!success) {log.error("库存不足");return;}//不需要再创建订单,直接savesave(voucherOrder);}
}
总结及发现问题
1、秒杀业务的优化思路
(1)先用Redis完成库存量以及一人一单判断,完成抢单
(2)下单的业务(操作数据库的业务)放入阻塞队列,并用独立线程完成异步下单
2、基于阻塞队列的异步秒杀存在的问题
(1)内存限制问题:我们使用了JDK的阻塞队列,占用的是JVM内存,若不加以限制,会有很多的订单对象创建线程并且将大量信息放入阻塞队列,可能会内存溢出
(2)数据安全问题:要用于下单的业务信息都存到了内存中,万一服务宕机,那么用户完成了抢单,但是数据库却没有做出相应的修改,将会导致数据不一致
解决的方法将在下一节进行分析
相关文章:
Redis:原理速成+项目实战——Redis实战9(秒杀优化)
👨🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习 🌌上期文章:Redis:原理速成项目实战——Redis实战8(基于Redis的分布式锁及优化) 📚订阅专栏&…...
【I2多语言】多语言快速上手
简介 官方API:http://www.inter-illusion.com/assets/I2LocalizationManual/I2LocalizationManual.html意义:更改游戏语言(多语言支持) 快速上手 插件安装: 直接拖拽进Unity即可 创建语言源(Creating a …...
【野火i.MX6ULL开发板】开发板连接网络(WiFi)与 SSH 登录、上电自动登录、设置静态IP、板子默认参数
0、前言 参考之前自己写的: http://t.csdnimg.cn/g60P8 参考资料: [野火]《Linux基础与应用开发实战指南——基于i.MX6ULL开发板》_20230323 从野火官网下载 参考博客: http://t.csdnimg.cn/8uh4O 参考官方文档: https://doc.…...
【数据库原理】(10)数据定义功能
SQL 数据定义功能包括定义模式、定义表、定义索引和定义视图,其语句如表所示。 一.创建、删除模式 1.创建模式 (Create Schema) 用途:创建模式是为了在数据库中定义一个新的命名空间,它可以包含多个数据库对象。 语法: CREATE SCHEMA &…...
GnuTLS recv error (-110): The TLS connection was non-properly terminated.
bug 解决方案:参考 GnuTLS recv error (-110): The TLS connection was non-properly terminated. 解决方案: apt-get install gnutls-bin git config --global http.sslVerify false git config --global http.postBuffer 1048576000参考...
hive sql 和 spark sql的区别
Hive SQL 和 Spark SQL 都是用于在大数据环境中处理结构化数据的工具,但它们有一些关键的区别: 底层计算引擎: Hive SQL:Hive 是建立在 Hadoop 生态系统之上的,使用 MapReduce 作为底层计算引擎。因此,它的…...
SparkStreaming基础解析(四)
1、 Spark Streaming概述 1.1 Spark Streaming是什么 Spark Streaming用于流式数据的处理。Spark Streaming支持的数据输入源很多,例如:Kafka、Flume、Twitter、ZeroMQ和简单的TCP套接字等等。数据输入后可以用Spark的高度抽象原语如:map、…...
HTML---JavaScript操作DOM对象
目录 文章目录 本章目标 一.DOM对象概念 二.节点访问方法 常用方法: 层次关系访问节点 三.节点信息 四.节点的操作方法 操作节点的属性 创建节点 删除替换节点 五.节点操作样式 style属性 class-name属性 六.获取元素位置 总结 本章目标 了解DOM的分类和节点间的…...
ChatGPT扩展系列之网易数帆ChatBI
在当今数字化快速发展的时代,数据已经成为业务经营与管理决策的核心驱要素。无论是跨国大企业还是新兴创业公司,正确、迅速地洞察数据已经变得至关重要。然而,传统的BI工具往往对用户有一定的技术门槛,需要熟练的操作技能和复杂的查询语句,这使得大部分的企业员工难以深入…...
1.10号io网络
信号量(信号灯集) 1> 信号灯集主要完成进程间同步工作,将多个信号灯,放在一个信号灯集中,每个信号灯控制一个进程 2> 每个灯维护了一个value值,当value值等于0时,申请该资源的进程处于阻…...
基于JAVA+SpringBoot的高校学术报告系统
✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取项目下载方式🍅 一、项目背景介绍: 智慧高校学术报告系统…...
单机部署Rancher
上次已经安装完毕了k8s了,但是想要界面化的管理,离不开界面工具,首推就是rancher,本文介绍安装rancher的安装,也可以将之前安装的k8s管理起来。 已经安装完毕docker和docker-ce的可以直接从第三部分开始。 一、基础准…...
linux 命令
ps: 命令用来查看系统上的进程信息。 查看内存 cat /proc/进程id/maps...
MySQL数据库进阶|SQL优化|开发手册
系列专栏:MySQL数据库进阶 前言 在看此篇前,建议先阅读MySQL索引,对索引有个基本了解:MySQL数据库进阶-索引-CSDN博客 在进行SQL优化前,我们必须先了解SQL查询的性能分析,为什么这条SQL慢,慢在…...
一文了解Git(所有命令)附带图片
我是南城余!阿里云开发者平台专家博士证书获得者! 欢迎关注我的博客!一同成长! 一名从事运维开发的worker,记录分享学习。 专注于AI,运维开发,windows Linux 系统领域的分享! 其他…...
Hex2Bin转换软件、Bootloader 、OTA加密升级 、STM32程序加密、其他MCU同样适用
说明:这个工具可以将 Hex 文件 转换为 Bin 格式文件,软件是按自己开发 STM32 OAT 功能需求开发的一款辅助 上位机软件。 文中的介绍时 bootloader boot 文档在补充完善中... 有兴趣的朋友可留言探讨。 1. 软件功能: 1.生成 bin&#x…...
Hadoop之mapreduce参数大全-6
126.指定 Map 任务运行的节点标签表达式 mapreduce.map.node-label-expression 是 Hadoop MapReduce 框架中的一个配置属性,用于指定 Map 任务运行的节点标签表达式。节点标签是在 Hadoop 集群中为节点分配的用户定义的标签,可用于将 Map 任务限制在特定…...
Vue开发中,在实现单页面应用(SPA)前端路由时的hash模式和history模式的区别及详细介绍
文章目录 一、前言二、hash模式hashchange 事件: 三、history模式方法:1、history.go():2、history.back():3、history.forward():4、History.replaceState()5、History.pushState()popState 事件 四、nginx配置五、原…...
功能强大的免费SSL证书
一、数据加密的重要性 免费SSL证书的核心作用在于对网站的数据传输进行加密处理。当一个网站部署了SSL证书后,它能够将HTTP协议升级至HTTPS,这意味着所有在客户端(如浏览器)与服务器之间传输的信息都将被高强度的加密算法所保护。…...
在Vue中使用Web Worker详细教程
1.什么是Web Worker? Web Worker 是2008年h5提供的新功能,每一个新功能都是为了解决原有技术的的痛点,那么这个痛点是什么呢? 1.1 JavaScript的单线程 JavaScript 为什么要设计成单线程? 这与js的工作内容有关:js只…...
四、C#高级特性(动态类型与Expando类)
在C#中,动态类型和ExpandoObject类是两个与运行时类型系统相关的特性,它们提供了更灵活的数据处理能力。 动态类型 动态类型是一种特殊的类型,允许你在运行时解析和操作对象的成员,而不需要在编译时知道这些成员的细节。使用动态…...
贪心算法的“左最优“与“右最优“及其对应的堆处理和预处理方法
1 答疑 1.1 什么是贪心算法的"左最优"与"右最优" "左最优"和"右最优"是贪心算法中的两种策略: 左最优 (Leftmost Greedy): 在每一步选择中,总是选择最左边(最早出现的)可行的选项。 右…...
【Docker】容器的相关命令
上一篇:创建,查看,进入容器 https://blog.csdn.net/m0_67930426/article/details/135430093?spm1001.2014.3001.5502 目录 1. 关闭容器 2.启动容器 3.删除容器 4.查看容器的信息 查看容器 1. 关闭容器 从图上来看,容器 aa…...
Android BUG 之 Error: Activity class {} does not exist
项目场景: 更换包名,运行报错 问题描述 原因分析: 在替换包名的时候要确认,配置文件跟build中的保持一致,在更换后还要将旧包的缓存数据清理掉 解决方案: 1 替换后删除 app 下的build 文件夹 2 Rebuild Pr…...
听劝,年度规划有它真的很必要!
2024年的时间进度条已走过一周,完成全年的1/52。 新年的flag悄然立下:愿逆风如解意,税后八个亿。 在不确定的世界中,发财暴富终归是确定的目标。 相比2023年的卷,年底的即兴生活正在悄悄上演,上一秒还在…...
leetcode滑动窗口问题总结 Python
目录 一、理论 二、例题 1. 最长无重复字符串 2. 长度最小的子数组 3. 字符串的排列 4. 最小覆盖子串 5. 滑动窗口最大值 一、理论 滑动窗口是一类比较重要的解题思路,一般来说我们面对的都是非定长窗口,所以一般需要定义两个指针 left 和 right&…...
秒变办公达人,只因用了这5款在线协同文档app!
在日常工作中,我们不可避免地需要处理各种文档,有时你可能会为如何高效地管理这些文档而感到烦恼,或是不知道如何挑选合适的在线文档工具? 不用担心!在这篇文章中,我们将介绍5个好用的在线文档工具App&…...
镜头选型和计算
3.5 补充知识 一、单像元分辨率(单像素精度) 单像素精度是表示视觉系统综合精度的指标,表示一个像元对应检测目标的实际物理尺寸,是客户重点关注的 视觉系统参数; 计算公式1:单像素精度视野范围FOV/相机分辨…...
2024--Django平台开发-Django知识点(四)
1.知识回顾 创建项目:新项目、别人项目、新版版、老版本 项目目录(v1.0版本) 路由系统 常见路由编写加粗样式 /index/ 函数 /index/<str:v1> 函数 re_path(ryy/(\d{4})-(\d{2})-(\d{2})/, views.yy), re_path(ryy/(?…...
可狱可囚的爬虫系列课程 09:通过 API 接口抓取数据
前面已经讲解过 Requests 结合 BeautifulSoup4 库抓取数据,这种方式在抓取数据时还是比较方便快捷的,但是这并不意味着所有的网站都适合这种方式,并且这也不是抓取数据的最快方式,今天我们来讲一种更快速的获取数据的方式…...
怎么做体育直播网站/前端性能优化
以下列出mysql函数的使用,并不完全,涉及到多少写多少。length(str):返回字符串(str)的字符长度。一个汉字算三个字符,一个数字或字母算一个字符。select length(测试); --6select length(123abc); --6char_length(str):…...
花的网站建设规划书/百度推广多少钱一天
作者部门:数字地球重点实验室近日,中科院空天信息研究院数字地球重点实验室王超研究员团队首次公开了合成孔径雷达(SAR)图像船舶检测数据集。该数据集来自于多源、多模式SAR图像。基于此数据集,该团队实现了复杂背景下的商船检测与分类一体化…...
三合一网站选什么系统/seo网络优化软件
变形转换 属性:transfrom 1.Translate----位移 概念 该属性值可以让元素从当前位置根据X轴Y轴,在水平和垂直方向进行移动。 注意: 在浏览器中一般只要说到x,y轴,那么基本是都是以下面方式计算原点和方向 原点: 元素…...
佛山自己网站建设/中国目前最好的搜索引擎
转 线性代数的本质–对线性空间、向量和矩阵的直觉描述 线性代数课程,无论你从行列式入手还是直接从矩阵入手,从一开始就充斥着莫名其妙。 比如说,在全国一般工科院系教学中应用最广泛的同济线性代数教材(现在到了第四版&#…...
北京建设高端网站的/精准ip地址查询工具
C TCP Server 用C写网络程序需要winsock2.h这个头文件,还有添加一个ws2_32.lib的lib库. VC2005添加Lib库的方法1 项目- Server属性- 配置属性- 连接器- 附加依赖项2 #pragma comment(lib,"ws2_32.lib")3 查找ws2_32.lib文件项目- 添加现有项,将ws2_32.lib作为文件…...
做网站的学什么代码/wix网站制作
乒乓操作是一个主要用于数据流控制的处理技巧,典型的乒乓操作如下图所示。 外部输入数据流通过“输入数据选择控制”模块送入两个数据缓冲区中,数据缓冲模块可以为任何存储模块,比较常用的存储单元为双口RAM(Dual RAM)、SRAM、SDRAM、FIFO等。…...