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

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(秒杀优化)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位大四、研0学生&#xff0c;正在努力准备大四暑假的实习 &#x1f30c;上期文章&#xff1a;Redis&#xff1a;原理速成项目实战——Redis实战8&#xff08;基于Redis的分布式锁及优化&#xff09; &#x1f4da;订阅专栏&…...

【I2多语言】多语言快速上手

简介 官方API&#xff1a;http://www.inter-illusion.com/assets/I2LocalizationManual/I2LocalizationManual.html意义&#xff1a;更改游戏语言&#xff08;多语言支持&#xff09; 快速上手 插件安装&#xff1a; 直接拖拽进Unity即可 创建语言源&#xff08;Creating a …...

【野火i.MX6ULL开发板】开发板连接网络(WiFi)与 SSH 登录、上电自动登录、设置静态IP、板子默认参数

0、前言 参考之前自己写的&#xff1a; http://t.csdnimg.cn/g60P8 参考资料&#xff1a; [野火]《Linux基础与应用开发实战指南——基于i.MX6ULL开发板》_20230323 从野火官网下载 参考博客&#xff1a; http://t.csdnimg.cn/8uh4O 参考官方文档&#xff1a; https://doc.…...

【数据库原理】(10)数据定义功能

SQL 数据定义功能包括定义模式、定义表、定义索引和定义视图,其语句如表所示。 一.创建、删除模式 1.创建模式 (Create Schema) 用途&#xff1a;创建模式是为了在数据库中定义一个新的命名空间&#xff0c;它可以包含多个数据库对象。 语法&#xff1a; CREATE SCHEMA &…...

GnuTLS recv error (-110): The TLS connection was non-properly terminated.

bug 解决方案&#xff1a;参考 GnuTLS recv error (-110): The TLS connection was non-properly terminated. 解决方案&#xff1a; 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 都是用于在大数据环境中处理结构化数据的工具&#xff0c;但它们有一些关键的区别&#xff1a; 底层计算引擎&#xff1a; Hive SQL&#xff1a;Hive 是建立在 Hadoop 生态系统之上的&#xff0c;使用 MapReduce 作为底层计算引擎。因此&#xff0c;它的…...

SparkStreaming基础解析(四)

1、 Spark Streaming概述 1.1 Spark Streaming是什么 Spark Streaming用于流式数据的处理。Spark Streaming支持的数据输入源很多&#xff0c;例如&#xff1a;Kafka、Flume、Twitter、ZeroMQ和简单的TCP套接字等等。数据输入后可以用Spark的高度抽象原语如&#xff1a;map、…...

HTML---JavaScript操作DOM对象

目录 文章目录 本章目标 一.DOM对象概念 二.节点访问方法 常用方法&#xff1a; 层次关系访问节点 三.节点信息 四.节点的操作方法 操作节点的属性 创建节点 删除替换节点 五.节点操作样式 style属性 class-name属性 六.获取元素位置 总结 本章目标 了解DOM的分类和节点间的…...

ChatGPT扩展系列之网易数帆ChatBI

在当今数字化快速发展的时代,数据已经成为业务经营与管理决策的核心驱要素。无论是跨国大企业还是新兴创业公司,正确、迅速地洞察数据已经变得至关重要。然而,传统的BI工具往往对用户有一定的技术门槛,需要熟练的操作技能和复杂的查询语句,这使得大部分的企业员工难以深入…...

1.10号io网络

信号量&#xff08;信号灯集&#xff09; 1> 信号灯集主要完成进程间同步工作&#xff0c;将多个信号灯&#xff0c;放在一个信号灯集中&#xff0c;每个信号灯控制一个进程 2> 每个灯维护了一个value值&#xff0c;当value值等于0时&#xff0c;申请该资源的进程处于阻…...

基于JAVA+SpringBoot的高校学术报告系统

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 智慧高校学术报告系统…...

单机部署Rancher

上次已经安装完毕了k8s了&#xff0c;但是想要界面化的管理&#xff0c;离不开界面工具&#xff0c;首推就是rancher&#xff0c;本文介绍安装rancher的安装&#xff0c;也可以将之前安装的k8s管理起来。 已经安装完毕docker和docker-ce的可以直接从第三部分开始。 一、基础准…...

linux 命令

ps: 命令用来查看系统上的进程信息。 查看内存 cat /proc/进程id/maps...

MySQL数据库进阶|SQL优化|开发手册

系列专栏&#xff1a;MySQL数据库进阶 前言 在看此篇前&#xff0c;建议先阅读MySQL索引&#xff0c;对索引有个基本了解&#xff1a;MySQL数据库进阶-索引-CSDN博客 在进行SQL优化前&#xff0c;我们必须先了解SQL查询的性能分析&#xff0c;为什么这条SQL慢&#xff0c;慢在…...

一文了解Git(所有命令)附带图片

我是南城余&#xff01;阿里云开发者平台专家博士证书获得者&#xff01; 欢迎关注我的博客&#xff01;一同成长&#xff01; 一名从事运维开发的worker&#xff0c;记录分享学习。 专注于AI&#xff0c;运维开发&#xff0c;windows Linux 系统领域的分享&#xff01; 其他…...

Hex2Bin转换软件、Bootloader 、OTA加密升级 、STM32程序加密、其他MCU同样适用

说明&#xff1a;这个工具可以将 Hex 文件 转换为 Bin 格式文件&#xff0c;软件是按自己开发 STM32 OAT 功能需求开发的一款辅助 上位机软件。 文中的介绍时 bootloader boot 文档在补充完善中... 有兴趣的朋友可留言探讨。 1. 软件功能&#xff1a; 1.生成 bin&#x…...

Hadoop之mapreduce参数大全-6

126.指定 Map 任务运行的节点标签表达式 mapreduce.map.node-label-expression 是 Hadoop MapReduce 框架中的一个配置属性&#xff0c;用于指定 Map 任务运行的节点标签表达式。节点标签是在 Hadoop 集群中为节点分配的用户定义的标签&#xff0c;可用于将 Map 任务限制在特定…...

Vue开发中,在实现单页面应用(SPA)前端路由时的hash模式和history模式的区别及详细介绍

文章目录 一、前言二、hash模式hashchange 事件&#xff1a; 三、history模式方法&#xff1a;1、history.go()&#xff1a;2、history.back()&#xff1a;3、history.forward()&#xff1a;4、History.replaceState()5、History.pushState()popState 事件 四、nginx配置五、原…...

功能强大的免费SSL证书

一、数据加密的重要性 免费SSL证书的核心作用在于对网站的数据传输进行加密处理。当一个网站部署了SSL证书后&#xff0c;它能够将HTTP协议升级至HTTPS&#xff0c;这意味着所有在客户端&#xff08;如浏览器&#xff09;与服务器之间传输的信息都将被高强度的加密算法所保护。…...

在Vue中使用Web Worker详细教程

1.什么是Web Worker? Web Worker 是2008年h5提供的新功能&#xff0c;每一个新功能都是为了解决原有技术的的痛点&#xff0c;那么这个痛点是什么呢&#xff1f; 1.1 JavaScript的单线程 JavaScript 为什么要设计成单线程&#xff1f; 这与js的工作内容有关&#xff1a;js只…...

[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?

&#x1f9e0; 智能合约中的数据是如何在区块链中保持一致的&#xff1f; 为什么所有区块链节点都能得出相同结果&#xff1f;合约调用这么复杂&#xff0c;状态真能保持一致吗&#xff1f;本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里&#xf…...

OpenLayers 可视化之热力图

注&#xff1a;当前使用的是 ol 5.3.0 版本&#xff0c;天地图使用的key请到天地图官网申请&#xff0c;并替换为自己的key 热力图&#xff08;Heatmap&#xff09;又叫热点图&#xff0c;是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...

以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:

一、属性动画概述NETX 作用&#xff1a;实现组件通用属性的渐变过渡效果&#xff0c;提升用户体验。支持属性&#xff1a;width、height、backgroundColor、opacity、scale、rotate、translate等。注意事项&#xff1a; 布局类属性&#xff08;如宽高&#xff09;变化时&#…...

ssc377d修改flash分区大小

1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...

CentOS下的分布式内存计算Spark环境部署

一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架&#xff0c;相比 MapReduce 具有以下核心优势&#xff1a; 内存计算&#xff1a;数据可常驻内存&#xff0c;迭代计算性能提升 10-100 倍&#xff08;文档段落&#xff1a;3-79…...

汽车生产虚拟实训中的技能提升与生产优化​

在制造业蓬勃发展的大背景下&#xff0c;虚拟教学实训宛如一颗璀璨的新星&#xff0c;正发挥着不可或缺且日益凸显的关键作用&#xff0c;源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例&#xff0c;汽车生产线上各类…...

STM32F4基本定时器使用和原理详解

STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...

1.3 VSCode安装与环境配置

进入网址Visual Studio Code - Code Editing. Redefined下载.deb文件&#xff0c;然后打开终端&#xff0c;进入下载文件夹&#xff0c;键入命令 sudo dpkg -i code_1.100.3-1748872405_amd64.deb 在终端键入命令code即启动vscode 需要安装插件列表 1.Chinese简化 2.ros …...

Java多线程实现之Callable接口深度解析

Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...

k8s业务程序联调工具-KtConnect

概述 原理 工具作用是建立了一个从本地到集群的单向VPN&#xff0c;根据VPN原理&#xff0c;打通两个内网必然需要借助一个公共中继节点&#xff0c;ktconnect工具巧妙的利用k8s原生的portforward能力&#xff0c;简化了建立连接的过程&#xff0c;apiserver间接起到了中继节…...