Springboot实战——黑马点评之秒杀优化

news/2024/9/28 17:04:19

Springboot实战——黑马点评之 秒杀优化

1 秒杀优化

先来复习以下,秒杀优惠券业务的现有实现逻辑:


以上流程图中的操作串行执行,效率极低。
其中 判断秒杀库存 以及 校验一人一单 属于对数据库的读取,耗时较少;扣减库存 以及 创建订单 属于对数据库的写操作,耗时相对较久。

提升效率的方法我们可以考虑两个方面:
1)引入并发(开启多线程):主线程负责读取操作,如果读取检验资格通过,则开启另外的线程负责写操作
2)引入Redis缓存:可以将订单信息以及秒杀券信息存入Redis,在Redis中检验资格后,将符合资格的优惠券id+用户id+订单id存入阻塞队列,单独开启第二线程来读取阻塞队列执行写操作,即刻给用户返回下单订单号。

1.1 引入Redis进行资格检验

资格检验分为 检查库存是否充足 以及 用户是否下单过该优惠券 两个操作,如果引入Redis来实现,要考虑:

  • 秒杀券库存导入Redis,并且要数据及时更新同步,即 在检验资格通过后需要将Redis中的券库存-1
  • 下单记录:使用的数据结构需要满足1 集合;2 元素唯一性
  • 使用Redis中的set类型来缓存下单该优惠券的用户id集合,并且要保证数据及时更新同步,即 在检验资格通过后需要向set中添加用户id


以上所考虑的几点还需要保证操作的原子性,所以使用Redis的Lua脚本来实现。
Lua脚本需要的ARGV参数列表中有两个待定参数,分别是优惠券id 以及 用户id,其他的业务逻辑均调用Redis命令即可实现

-- 1. 参数列表
-- 1.1. 优惠券id 用于查询优惠券库存时的关键字
local voucherId = ARGV[1]
-- 1.2. 用户id 用于将查询下单用户对比
local useId = ARGV[2]-- 2. 数据key
-- 2.1. 库存key + 业务前缀 拼接 优惠券id
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. 判断用户是否下单 SISMEMBER orderkey userId
if(redis.call('sismember', orderKey, userId) == 1) then-- 3.3 如果存在该用户,说明是重复下单,返回错误码 2return 2
end--3.4 扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
--3.5 下单(插入用户id) sadd orderKey userId
redis.call('sadd', orderKey, userId)return 0

如果库存不足则返回1(Long),如果该用户重复下单则返回2(Long),如果资格检验通过则返回0

如果资格检验通过,则需要保证该有效订单被阻塞队列拿到,后续阻塞式执行成功,所以将“凭证”(封装好用户id、券id、订单id的订单实例)传入阻塞队列,等待异步线程阻塞式读取处理下单业务。

// 这里直接封装成订单实例
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setVoucherId(voucherId);
voucherOrder.setUserId(userId);
voucherOrder.setId(redisIdWorker.nextId("order"));
// 放入阻塞队列 blockingqueue
orderTasks.add(voucherOrder);

1.2 开启异步线程写数据库

需要准备以下几个数据结构:

  • 阻塞队列:当一个线程尝试从该队列中获取元素时,当查询到队列为空时会阻塞等待,直到队列中插入元素后被唤醒,不会导致线程空转消耗CPU资源。
  • 异步线程实现下单即 开启异步独立线程来阻塞式执行下单业务,所以需要准备1 线程池 2 线程任务
  • 线程池常量(单线程线程处理器),用于提交异步任务
// 线程池/线程处理器 此处创建的是单线程处理器
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){//...这里执行下单的具体业务//1. 获取队列中的订单信息try {VoucherOrder voucherOrder = orderTasks.take();// 2. 下单业务handleVoucherOrder(voucherOrder);} catch (Exception e) {log.error("处理订单异常",e);}}}
}

1.3 以上异步实现的弊端

1)内存限制问题:阻塞队列是有JDK内部的,底层使用的是JVM内存,如果有大量的订单信息被存入阻塞队列,将会带来较大内存负担,内存溢出
2)数据安全问题:JVM内存没有持久化机制。如果服务宕机,内存中的订单信息消失,用户支付状态与后台保存订单状态不一致;或者是 从阻塞队列中取出订单信息后尚未来得及处理下单逻辑,服务宕机了,将会造成订单丢失的问题。

2 Redis消息队列实现异步秒杀

使用Redis消息队列的两个优势:
1)Redis的消息队列是独立于JVM之外的数据结构,不受JVM内存的限制
2)Redis的消息队列可对消息作持久化,保证数据安全性,且封装有消息确认机制,确保了消息至少被消费一次

2.1 Redis实现消息队列的三种方式

  • 基于List结构:

    使用BPOP来阻塞式从Redis的list数据结构中获取队首元素,本质上原理和JDK的阻塞队列一样的。





    这样实现的弊端
    1)无法避免服务宕机导致的消息丢失
    2)只支持单消费者

  • 基于PubSub结构
    支持多消费者了,支持多生产、多消费


    这样实现的弊端
    1)不支持数据持久化,发送消息时如果消息无人订阅,消息不会永久存储在Redis中
    2)消息堆积有上限,消费者接收数据有缓存区,如果消息缓存超额,则会造成数据丢失了
    3)无法避免消息丢失

  • 基于Stream数据类型




    如果基于Stream数据类型来实现异步下单业务,则会出现消息漏读问题

  • 基于Stream的消费者组
    消费者组:将多个消费者划分到一个组中,该组监听同一个队列
    这样设计有以下几个特点:
    1)消息分流:同组内的消费者用来“竞争”同一个队列中的消息,与单消费者相比,加快了处理消息的速度,且消息可回溯
    2)消息标示:消费者组会维护一个标示,记录最后一个被处理的消息,如果服务宕机重启,能从标示之后读取消息,确保每一个消息都被消费成功,避免像单消费者出现漏读消息的问题
    3)消息确认:消费者组当获取到一个消息时,会将消息插入Pending-list中,标志该消息尚未处理,当处理结束后,会通过XACK来确认消息已处理,然后从pending-list中移除

    当消费者获取到消息时,消息会自动放入pending-list中,等待消费者处理完毕后发出XACK确认后才将其移除


    消费者1和2相继从s1队列中读取未读取过的第一条消息,与此同时这些消息均被放入了Pending-list中,等待消息确认Ack

2.2 Stream队列实现异步下单

所以可以将原有的异步下单功能替换成用Stream队列实现:
循环从Stream队列中读取订单信息 -> 消费者组以最后一个被获取的消息标识($),读取队列中还没被消费的消息,并设置2秒内阻塞式(|block)读取 -> 如果阻塞等待时间内并未拿到最新消息则continue -> 如果阻塞等待时间内获取到新消息,则按下单业务将其处理
捕获异常 -> 意味着此时pending-list中存在已被消费但未被处理完毕的消息 -> 循环从pending-list中获取第0号消息(非阻塞式,0)来尝试继续处理 -> 如果获取到尚未处理过的消息,则按正常下单业务继续处理 -> 如果没有异常中止的消息则结束异常捕获业务
如果捕获异常过程中又遇到异常 -> 继续循环读取pending-list

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/65831.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

PARTVI-Oracle数据库管理与开发-数据库管理员的概念

18. 数据库管理员的概念 18.1. 数据库管理员的职责 数据库管理员(DBA)的主要责任是使企业数据对其用户可用。DBA必须与开发人员紧密合作,确保他们的应用程序有效地使用数据库,并与系统管理员合作,确保物理资源充足且使用高效。Oracle DBA负责了解Oracle数据库架构以及数据…

实验作业1

实验一 任务一 源代码#include<stdio.h> int main() {printf(" o \n");printf("<H>\n");printf("I I\n");printf(" o \n");printf("<H>\n");printf("I I\n");return 0; }效果 源代码#incl…

01. 感知层环境安装

1. 软件以及驱动的安装安装ZigBee无线网络节点开发平台 IAR Embedded Workbench(简称EW) 安装串口驱动(CH340芯片)。点击安装64位的。后续就可以使用串口对开发板进行调试。 仿真器驱动程序(用来烧录代码)的安装。 安装串口工具(XCOM)。2. IAR创建工程打开安装的IAR软件,点击…

黑马PM-内容项目-需求分析

需求分析的定义需求分析的时机需求分析的步骤

带笔TP gt9xx调试

一.添加驱动把供应商提供的驱动替换掉sdk里面默认的驱动drivers/input/touchscreen/gt9xx 二.dts配置:&i2c3 {status = "okay";pinctrl-names = "default";pinctrl-0 = <&i2c3m0_xfer>;gt9xx: gt9xx@5d {compatible = "goodix,gt9xx&q…

C10-06-Burp简单使用

一 浏览器代理设置免责声明 本文仅是个人对该工具的学习测试过程记录,不具有恶意引导意向。 本文工具仅面向合法授权的企业安全建设行为,如您需要测试本工具的可用性,请自行搭建靶机环境。 在使用本工具进行检测时,您应确保该行为符合当地的法律法规,并且已经取得了足够的…

修改网页内容

修改网页内容 控制台输入代码 document.body.isContentEditable=true document.body.isContentEditable=true

青训营 X 豆包MarsCode

如果有人对前端/后端/AI 技术以及字节感兴趣并想系统学习来自大厂课程的话,可以试试青训营!青训营是字节跳动稀土掘金社区发起的技术系列培训 & 人才选拔项目,旨在培养优秀且具有职业竞争力的开发工程师。课程全程免费,不收取任何费用!考核方式:通过发布的刷题任务即…