Redis实战篇《黑马点评》5

ops/2025/2/23 8:02:52/

5.秒杀优化

5.1异步秒杀思路

  • 我们先来回顾一下下单流程

  • 当用户发起请求,此时会先请求Nginx,Nginx反向代理到Tomcat,而Tomcat中的程序,会进行串行操作,分为如下几个步骤

    1. 查询优惠券
    2. 判断秒杀库存是否足够
    3. 查询订单
    4. 校验是否一人一单
    5. 扣减库存
    6. 创建订单
  • 在这六个步骤中,有很多操作都是要去操作数据库的,而且还是一个线程串行执行,这样就会导致我们的程序执行很慢,所以我们需要异步程序执行,那么如何加速呢?

  • 优化方案:我们将耗时较短的逻辑判断放到Redis中,例如:库存是否充足,是否一人一单这样的操作,只要满足这两条操作,那我们是一定可以下单成功的,不用等数据真的写进数据库,我们直接告诉用户下单成功就好了。然后后台再开一个线程,后台线程再去慢慢执行队列里的消息,这样我们就能很快的完成下单业务。

  • 但是这里还存在两个难点

    1. 我们怎么在Redis中快速校验是否一人一单,还有库存判断
    2. 我们校验一人一单和将下单数据写入数据库,这是两个线程,我们怎么知道下单是否完成。
      • 我们需要将一些信息返回给前端,同时也将这些信息丢到异步queue中去,后续操作中,可以通过这个id来查询下单逻辑是否完成
  • 我们现在来看整体思路:当用户下单之后,判断库存是否充足,只需要取Redis中根据key找对应的value是否大于0即可,如果不充足,则直接结束。如果充足,则在Redis中判断用户是否可以下单,如果set集合中没有该用户的下单数据,则可以下单,并将userId和优惠券存入到Redis中,并且返回0,整个过程需要保证是原子性的,所以我们要用Lua来操作,同时由于我们需要在Redis中查询优惠券信息,所以在我们新增秒杀优惠券的同时,需要将优惠券信息保存到Redis中

  • 完成以上逻辑判断时,我们只需要判断当前Redis中的返回值是否为0,如果是0,则表示可以下单,将信息保存到queue中去,然后返回,开一个线程来异步下单,其阿奴单可以通过返回订单的id来判断是否下单成功

5.2Redis完成秒杀资格判断

  • 需求:
    1. 新增秒杀优惠券的同时,将优惠券信息保存到Redis中
    2. 基于Lua脚本,判断秒杀库存、一人一单,决定用户是否秒杀成功
  • 步骤一:修改保存优惠券相关代码
@Override
@Transactional
public 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);// 保存秒杀优惠券信息到Reids,Key名中包含优惠券ID,Value为优惠券的剩余数量stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString()); 
}
  • 使用PostMan发送请求,添加优惠券
    请求路径:http://localhost:8080/api/voucher/seckill
    请求方式:POST
    {"shopId":1,"title":"9999元代金券","subTitle":"365*24小时可用","rules":"全场通用\\nApex猎杀无需预约","payValue":1000,"actualValue":999900,"type":1,"stock":100,"beginTime":"2022-01-01T00:00:00","endTime":"2022-12-31T23:59:59"
    }
  • 添加成功后,数据库中和Redis中都能看到优惠券信息
  • 步骤二:编写Lua脚本
    lua的字符串拼接使用..,字符串转数字是tonumber()
-- 订单id
local voucherId = ARGV[1]
-- 用户id
local userId = ARGV[2]
-- 优惠券key
local stockKey = 'seckill:stock:' .. voucherId
-- 订单key
local orderKey = 'seckill:order:' .. voucherId
-- 判断库存是否充足
if (tonumber(redis.call('get', stockKey)) <= 0) thenreturn 1
end
-- 判断用户是否下单
if (redis.call('sismember', orderKey, userId) == 1) thenreturn 2
end
-- 扣减库存
redis.call('incrby', stockKey, -1)
-- 将userId存入当前优惠券的set集合
redis.call('sadd', orderKey, userId)
return 0

  • 修改业务逻辑
    @Override
    public Result seckillVoucher(Long voucherId) {//1. 执行lua脚本Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(), voucherId.toString(),UserHolder.getUser().getId().toString());//2. 判断返回值,并返回错误信息if (result.intValue() != 0) {return Result.fail(result.intValue() == 1 ? "库存不足" : "不能重复下单");}long orderId = redisIdWorker.nextId("order");//TODO 保存阻塞队列//3. 返回订单idreturn Result.ok(orderId);
    }

  • 现在我们使用PostMan发送请求,redis中的数据会变动,而且不能重复下单,但是数据库中的数据并没有变化
  • 5.3基于阻塞队列实现秒杀优化

  • 修改下单的操作,我们在下单时,是通过Lua表达式去原子执行判断逻辑,如果判断结果不为0,返回错误信息,如果判断结果为0,则将下单的逻辑保存到队列中去,然后异步执行
  • 需求
    1. 如果秒杀成功,则将优惠券id和用户id封装后存入阻塞队列
    2. 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
    • 步骤一:创建阻塞队列
      阻塞队列有一个特点:当一个线程尝试从阻塞队列里获取元素的时候,如果没有元素,那么该线程就会被阻塞,直到队列中有元素,才会被唤醒,并去获取元素
      阻塞队列的创建需要指定一个大小
      private final BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);

    • 那么把优惠券id和用户id封装后存入阻塞队列
      @Override
      public Result seckillVoucher(Long voucherId) {Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(), voucherId.toString(),UserHolder.getUser().getId().toString());if (result.intValue() != 0) {return Result.fail(result.intValue() == 1 ? "库存不足" : "不能重复下单");}long orderId = redisIdWorker.nextId("order");//封装到voucherOrder中VoucherOrder voucherOrder = new VoucherOrder();voucherOrder.setVoucherId(voucherId);voucherOrder.setUserId(UserHolder.getUser().getId());voucherOrder.setId(orderId);//加入到阻塞队列orderTasks.add(voucherOrder);return Result.ok(orderId);
      }

      步骤二:实现异步下单功能

    • 先创建一个线程池
      private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
    • 创建线程任务,秒杀业务需要在类初始化之后,就立即执行,所以这里需要用到@PostConstruct注解
      @PostConstruct
      private void init() {SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
      }private class VoucherOrderHandler implements Runnable {@Overridepublic void run() {while (true) {try {//1. 获取队列中的订单信息VoucherOrder voucherOrder = orderTasks.take();//2. 创建订单handleVoucherOrder(voucherOrder);} catch (Exception e) {log.error("订单处理异常", e);}}}
      }
    • 编写创建订单的业务逻辑
      private IVoucherOrderService proxy;
      private void handleVoucherOrder(VoucherOrder voucherOrder) {//1. 获取用户Long userId = voucherOrder.getUserId();//2. 创建锁对象,作为兜底方案RLock redisLock = redissonClient.getLock("order:" + userId);//3. 获取锁boolean isLock = redisLock.tryLock();//4. 判断是否获取锁成功         if (!isLock) {log.error("不允许重复下单!");return;}try {//5. 使用代理对象,由于这里是另外一个线程,proxy.createVoucherOrder(voucherOrder);} finally {redisLock.unlock();}
      }
    • 查看AopContext源码,它的获取代理对象也是通过ThreadLocal进行获取的,由于我们这里是异步下单,和主线程不是一个线程,所以不能获取成功
      private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal("Current AOP proxy");
    • 但是我们可以将proxy放在成员变量的位置,然后在主线程中获取代理对象
      @Override
      public Result seckillVoucher(Long voucherId) {Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(), voucherId.toString(),UserHolder.getUser().getId().toString());if (result.intValue() != 0) {return Result.fail(result.intValue() == 1 ? "库存不足" : "不能重复下单");}long orderId = redisIdWorker.nextId("order");//封装到voucherOrder中VoucherOrder voucherOrder = new VoucherOrder();voucherOrder.setVoucherId(voucherId);voucherOrder.setUserId(UserHolder.getUser().getId());voucherOrder.setId(orderId);//加入到阻塞队列orderTasks.add(voucherOrder);//主线程获取代理对象proxy = (IVoucherOrderService) AopContext.currentProxy();return Result.ok(orderId);
      }

      5.4小结

    • 秒杀业务的优化思路是什么?

      1. 先利用Redis完成库存容量、一人一单的判断,完成抢单业务
      2. 再将下单业务放入阻塞队列,利用独立线程异步下单
    • 基于阻塞队列的异步秒杀存在哪些问题?

      1. 内存限制问题:
        • 我们现在使用的是JDK里的阻塞队列,它使用的是JVM的内存,如果在高并发的条件下,无数的订单都会放在阻塞队列里,可能就会造成内存溢出,所以我们在创建阻塞队列时,设置了一个长度,但是如果真的存满了,再有新的订单来往里塞,那就塞不进去了,存在内存限制问题
      2. 数据安全问题:
        • 经典服务器宕机了,用户明明下单了,但是数据库里没看到

http://www.ppmy.cn/ops/160714.html

相关文章

短视频脚本文案策划 总结

1、旁白&#xff1a;第三人称视角&#xff0c;依靠浑厚的声音奠定影片的基调&#xff08;企业宣传片主要手法和元素&#xff09;&#xff08;容易不接地气&#xff09;&#xff08;要有故事感的旁白&#xff09; 2、独白&#xff1a;第一人称视角&#xff0c;主角或其他视角的内…

ubuntu新系统使用指南

1. 更新源 2. 配置rime 输入法 3. 下载常用工具 deepin-log-viewer 功能&#xff1a;Deepin 日志查看器。 用途&#xff1a;用于查看系统日志、应用程序日志等&#xff0c;帮助用户排查问题。 deepin-movie 功能&#xff1a;Deepin 视频播放器。 用途&#xff1a;用于播…

以ChatGPT为例解析大模型背后的技术

目录 1、大模型分类 2、为什么自然语言处理可计算&#xff1f; 2.1、One-hot分类编码&#xff08;传统词表示方法&#xff09; 2.2、词向量 3、Transformer架构 3.1、何为注意力机制&#xff1f; 3.2、注意力机制在 Transformer 模型中有何意义&#xff1f; 3.3、位置编…

柠檬水找零(力扣860)

这道题的贪心很简单&#xff0c;就是体现在对于20元的找零上。根据题意&#xff0c;20元有两种找零方式&#xff1a;1.找一张5元和一张10元&#xff1b;2.找3张5元。但是5元比较万能&#xff0c;因为无论是10还是20都需要用5元来找零&#xff0c;所以我们优先考虑第一种找零方式…

【护网行动-红蓝攻防】第一章-红蓝对抗基础 认识红蓝紫

1.实战攻防演练 1.1为什么要进行实战攻防演练&#xff1f; 军事上的演练&#xff0c;是除了实战以外最能检验军队战斗力的一种考核方式&#xff0c;他可以模拟面对外部势力的攻击时候&#xff0c;如何更好的去维护国家和主权的安全。同样的&#xff0c;在网络上面&#xff0c;…

windwos与linux环境下Iperf3带宽测试工具的安装、使用

目录 一、前言 二、windows 2.1下载 2.2安装 2.3使用 2.3.1服务端 2.3.2客户端 2.3.3输出内容 1.客户端 2.服务端 2.4.相关命令 三、linux 3.1安装 3.2使用 1.服务端 2.客户端 3.输出内容 1.客户端 2.服务端 一、前言 在数字化浪潮下&#xff0c;网络性能…

Spring MVC中环境配置的实战应用

在现代的Spring MVC应用中&#xff0c;环境配置是一个非常重要的环节。通过合理配置环境&#xff0c;我们可以轻松地在开发环境、测试环境和生产环境之间切换&#xff0c;而无需修改代码。本文将通过一个具体的实例&#xff0c;展示如何在Spring MVC中设置环境配置&#xff0c;…

6. 【.NET 8 实战--孢子记账--从单体到微服务--转向微服务】--微服务基础工具与技术--Ocelot 网关--概念与简单入门

网关是一种位于客户端和后端服务之间的服务&#xff0c;充当所有客户端请求的单一入口。它的主要职责是接收所有的API调用&#xff0c;汇总各类请求&#xff0c;将其路由到适当的后端服务&#xff0c;并将响应返回给客户端。网关不仅仅是一个简单的反向代理&#xff0c;它还能够…