做最漂亮的网站/网络推广公司

做最漂亮的网站,网络推广公司,上海网站建设的英文,网站的建设运营收费是哪些一、业务场景介绍 优惠券、门票等限时抢购常常出现在各类应用中,这样的业务一般为了引流宣传而降低利润,所以一旦出现问题将造成较大损失,那么在业务中就要求我们对这类型商品严格限时、限量、每位用户限一次、准确无误的创建订单&#xff0c…

一、业务场景介绍

        优惠券、门票等限时抢购常常出现在各类应用中,这样的业务一般为了引流宣传而降低利润,所以一旦出现问题将造成较大损失,那么在业务中就要求我们对这类型商品严格限时、限量、每位用户限一次、准确无误的创建订单,这样的要求看似简单,但在分布式系统中,要求我们充分考虑高并发下的线程安全问题,今天我们来看一下两种解决思路。

二、基于Redisson分布式锁的秒杀方案

        这里我们就不进行自定义redis锁了,Redisson 基于 Redis 实现了 Java 驻内存数据网格(In-Memory Data Grid),它不仅提供了对 Redis 原生命令的封装,还提供了一系列高级的分布式数据结构和服务,促进使用者对 Redis 的关注分离,让开发者能够更专注于业务逻辑,所以我们直接使用Redisson,但底层源码还是需要我们去自己学习掌握的。

1.流程概览

        其实单看流程图我们就能发现这一连串的串行逻辑就会非常影响效率,我们先留着这个问题后面优化 。

2.具体实现

    @Overridepublic Result generate(Long voucherId) {//查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);//活动是否开始/结束if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("活动未开始!");}if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("活动已结束!");}//库存表是否充足if (voucher.getStock()<1) {return Result.fail("库存不足!");}Long userId = UserHolder.getUser().getId();//只锁同一个id//创建锁对象RLock lock = redissonClient.getLock("lock:order:" + userId);//获取锁,防止同一用户的并发请求boolean isLock = lock.tryLock();//默认不等待,30秒过期if (!isLock) {//获取锁失败return Result.fail("网络繁忙!");}//拿到spring事务代理,这里为了简单解决事务自调用直接去拿代理可能造成问题,建议将事务方法重构至另一服务类并注入try {IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//释放锁lock.unlock();}}@Transactional//要锁住事物,防止事物在锁释放后才提交导致其他线程进入public Result createVoucherOrder(Long voucherId) {Long userId = UserHolder.getUser().getId();//一人一单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if (count >0) {return Result.fail("您最多只可购买一单!");}//扣减库存boolean flag = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).gt("stock",0).update();if (!flag){//高并发下已经被其他用户线程扣减return Result.fail("库存不足2!");}//创建订单VoucherOrder voucherOrder = new VoucherOrder();//唯一IDlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//用户idvoucherOrder.setUserId(userId);//代金券IdvoucherOrder.setVoucherId(voucherId);save(voucherOrder);//返回订单IDreturn Result.ok(orderId);}

3.测试分析

 接下来我们登录数据库中所有的用户并记录Authorization

@SpringBootTest
@Component
public class SecKill {@Autowiredprivate IUserService userService;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Testvoid userLogin() throws IOException {// 定义保存 token 的文件路径String filePath = "D:\\tokens.txt";// 使用 BufferedWriter 写入文件try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath, true))) { // 追加模式for (User user : userService.list()) {String phone = user.getPhone();HttpSession session = null;userService.sendCode(phone, session);String code = stringRedisTemplate.opsForValue().get("login:code:" + phone);LoginFormDTO loginFormDTO = new LoginFormDTO();loginFormDTO.setCode(code);loginFormDTO.setPhone(phone);String token = userService.logIn(loginFormDTO, session);// 将 token 写入文件writer.write(token);writer.newLine(); // 换行writer.flush(); // 刷新缓冲区,确保数据写入文件}} catch (IOException e) {e.printStackTrace();}}
}

然后我们设置优惠券数量为200,通过jmeter(一款测试工具,大家自行学习如何使用)模拟数据库中1000多个用户总计每秒1000的高并发请求

 从聚合报告中可以看到虽然80%的异常率确实满足了我们对优惠券的限量要求,通过查看数据库订单和库存也不存在问题,但是我们可以看到我们的平均响应时间在高并发下达到了344ms,吞吐量只有1200左右,如果面临更高的并发难免因性能局限出现问题。

三、基于消息队列的异步秒杀

1.问题分析

正如我们一开始发现的,每个请求来到服务器都需要执行一串的数据库读写操作,而写操作耗时是比较久的,可是当我们确定用户抢单成功后只要能确保订单最终写入即可,无需让其阻塞请求,所以我们其实可以将读写操作分离开。

我们可以利用读操作完成下单资格的各种校验,校验成功即可对请求做出响应,那么后续写订单操作怎么完成呢?我们需要根据校验成功的记录完成写操作,那谁来完成校验成功的记录呢,这样记录是不是又和原来的读写串行一样了呢?

2.工具对比

首先我们的目的是加快请求响应效率,减轻数据库压力,其实我们需要的就是一个中间工具做到能够快速存储校验成功的记录并有限制的可控的逐渐将存储起来的记录转发给数据库让其创建订单,能做到上述要求的工具有很多,这里简单对比以下三种供大家参考。

特性/技术阻塞队列RedisMQ消息中间件(如RabbitMQ、Kafka)
系统解耦低,主要用于单机环境中,支持集群部署高,天然用于系统解耦
异步通信支持,但需要手动实现通过发布/订阅模式实现专为异步通信设计
削峰填谷临时存储请求,能力有限缓存请求,需合理设计策略缓存大量请求,后端按速率消费
可靠性和持久性依赖具体实现,需额外持久化支持持久化,可靠性较高高可靠性和持久性,支持消息确认
性能和吞吐量受限于单机处理能力性能较高,支持集群最高,适用于大规模分布式系统
功能丰富性单一,主要用于线程间通信支持多种数据结构和操作支持多种消息协议、路由机制等
开发和维护成本低,但需手动实现异步逻辑中等,易于实现和使用高,需学习和理解相关协议和机制
适用场景小规模、单机环境中小规模、集群部署大规模分布式系统、复杂路由

 3.流程概览

        由于阻塞队列局限较大,MQ中间件比较简单,这里我们以Redis中的stream为例(除此之外,list和PubSub也能实现,但是局限较大)实现异步秒杀。

对于红框部分,为了确保原子性,我们借助lua脚本完成,这样一来我们就将MySQL的读写操作分离开来,请求响应中只需要读取验证,用redis更高效的io操作完成简单记录,随后异步逐渐处理MySQl的订单写入。

4.具体实现

  • lua脚本
    ---
    --- Generated by EmmyLua(https://github.com/EmmyLua)
    --- Created by cds.
    --- DateTime: 2025/3/23 13:03
    ---
    --1.参数列表
    --1.1优惠券id
    local voucherId=ARGV[1]
    --1.2用户id
    local userId=ARGV[2]
    --1.3订单id
    local orderId=ARGV[3]--2.数据key
    --2.1库存key
    local stockKey='seckill:stock:' .. voucherId
    --2.2订单key
    local orderKey='seckill:order:' .. voucherId--脚本业务
    --判断库存是否充足
    if (tonumber(redis.call('get',stockKey))<=0) then--库存不足返回1return 1
    end
    --判断用户是否下单
    if (redis.call('sismember',orderKey,userId)==1) then--下过单返回2return 2
    end
    --扣库存
    redis.call('incrby',stockKey,-1)
    --下单
    redis.call('sadd',orderKey,userId)
    --发送消息到消息队列  xadd stream.orders * k1 v1 k2 v2 ..
    redis.call('xadd','stream.orders','*','userId',userId,'voucherId',voucherId,"id",orderId)return 0
  •  具体业务
        @Autowiredprivate IVoucherOrderService proxy;//初始化lua脚本信息private static final DefaultRedisScript<Long> SECKILL_SCRIPT;static {SECKILL_SCRIPT =new DefaultRedisScript<>();SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));SECKILL_SCRIPT.setResultType(Long.class);}//异步单例线程private static final ExecutorService SECKILL_ORDER_EXECTUOR= Executors.newSingleThreadExecutor();//在spring的Bean初始化并注入后开始@PostConstructprivate void init(){SECKILL_ORDER_EXECTUOR.submit(new VoucherOrderHandler());}//线程任务private class VoucherOrderHandler implements Runnable {String queueName = "stream.orders";@Overridepublic void run() {while (true) {try {//获取消息队列中的订单信息  XREAD GROUP group1 c1 count 1 block 2000 streams stream.orders >List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("group1", "c1"),StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),StreamOffset.create(queueName, ReadOffset.lastConsumed()));//判断消息是否获取成if (list == null || list.isEmpty()) {//获取失败 没有消息,继续循环continue;}//获取成功,可以下单//解析消息中的订单信息MapRecord<String, Object, Object> record = list.get(0);Map<Object, Object> value = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);handleVoucherOrder(voucherOrder);//ACK确认 SACK stream.orders group1 idstringRedisTemplate.opsForStream().acknowledge(queueName, "group1", record.getId());} catch (Exception e) {log.error("创建订单异常{}", e.getMessage());//有异常去pendingList拿handlePendingList();}}}private void handlePendingList() {while (true) {try {//获取pending-list队列中的订单信息  XREAD GROUP group1 c1 count 1 block 2000 streams stream.orders 0List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("group1", "c1"),StreamReadOptions.empty().count(1),StreamOffset.create(queueName, ReadOffset.from("0")));//判断消息是否获取成if (list == null || list.isEmpty()) {//获取失败 pending-list没有消息,结束循环break;}//获取成功,可以下单//解析消息中的订单信息MapRecord<String, Object, Object> record = list.get(0);Map<Object, Object> value = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);handleVoucherOrder(voucherOrder);//ACK确认 SACK stream.orders group1 idstringRedisTemplate.opsForStream().acknowledge(queueName, "group1", record.getId());} catch (Exception e) {log.error("创建订单异常{}", e.getMessage());try {Thread.sleep(20);} catch (InterruptedException ex) {throw new RuntimeException(ex);}}}}}private void handleVoucherOrder(VoucherOrder voucherOrder) {Long userId = voucherOrder.getUserId();//创建锁对象RLock lock = redissonClient.getLock("lock:order:" + userId);//获取锁boolean isLock = lock.tryLock();//默认不等待,30秒过期if (!isLock) {//获取锁失败log.info("请勿重复购买!");return;}//拿到spring事务代理try {proxy.createVoucherOrder(voucherOrder);} finally {//释放锁lock.unlock();}}//这部分的检验是以防stream消息队列里出现问题导致重复save操作@Transactional//要锁住事物public 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 flag = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0).update();if (!flag) {log.info("库存不足!");return;}//创建订单save(voucherOrder);}}@Overridepublic Result secKill(Long voucherId) {//查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);//活动是否开始/结束if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("活动未开始!");}if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("活动已结束!");}//库存表是否充足if (voucher.getStock() < 1) {return Result.fail("库存不足!");}//获取用户Long userId = UserHolder.getUser().getId();//1执行lua脚本//唯一IDlong orderId = redisIdWorker.nextId("order");Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(), userId.toString(), String.valueOf(orderId));int r = result.intValue();//2判断lua脚本返回值0if (r != 0) {//2.1不为零无资格return Result.fail(r == 1 ? "库存不足!" : "不能重复下单!");}return Result.ok(orderId);}

5.测试分析 

我们再次使用jmeter进行同样的测试,但这次我们需要提前将库存信息同步到redis 

可以看到经过优化的秒杀业务吞吐量大大增加,平均响应时间降低到30ms左右,得到了十倍左右的提升,大大增加了响应处理效率

 redis订单记录

redis消息队列记录

如果去控制台观察日志可以发现,删改请求少量穿插在中间,大部分聚集在查询校验结束的末尾,读操作基本都聚集在最前面,DB操作得到有效控制,这就是异步写入处理的体现

好了,本次分享到这里结束,谢谢阅读!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MiniMax GenAI 可观测性分析:基于阿里云 SelectDB 构建 PB 级别日志系统

“阿里云SelectDB作为MiniMax日志存储服务的核心支撑&#xff0c;为在线和离线业务提供了高效、稳定的查询与聚合分析能力。其支持实时物化视图、租户资源隔离、冷热分离等企业级特性&#xff0c;不仅有效解决了日志场景下PB级别数据查询的性能瓶颈&#xff0c;还通过智能化的资…

【YOLO V3】目标检测 Darknet 训练自定义模型

【YOLO V3】目标检测 Darknet 训练自定义模型 前言整体思路环境检查与依赖配置克隆 YOLOv3 Darknet 并编译Clone Darknet 项目文件修改 Makefile 文件修改模型保存频率项目编译 准备数据集配置训练文件数据集&#xff1a;datasets &#xff08;自制&#xff09;权重文件 yolov3…

DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加导出数据功能

前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加导出数据功能📚页面效果📚指令输入�…

《Python实战进阶》第31集:特征工程:特征选择与降维技术

第31集&#xff1a;特征工程&#xff1a;特征选择与降维技术 摘要 特征工程是机器学习和数据科学中不可或缺的一环&#xff0c;其核心目标是通过选择重要特征和降低维度来提升模型性能并减少计算复杂度。本集聚焦于特征选择与降维技术&#xff0c;涵盖过滤法、包裹法、嵌入法等…

Excel第41套全国人口普查

2. 导入网页中的表格&#xff1a;数据-现有链接-考生文件夹&#xff1a;网页-找到表格-点击→变为√-导入删除外部链接关系&#xff1a;数据-点击链接-选中连接-删除-确定&#xff08;套用表格格式-也会是删除外部链接&#xff09;数值缩小10000倍&#xff08;除以10000即可&am…

WPS宏开发手册——使用、工程、模块介绍

目录 系列文章前言1、开始1.1、宏编辑器使用步骤1.2、工程1.3、工程 系列文章 使用、工程、模块介绍 JSA语法 第三篇练习练习题&#xff0c;持续更新中… 前言 如果你是开发人员&#xff0c;那么wps宏开发对你来说手拿把切。反之还挺吃力&#xff0c;需要嘻嘻&#xf…

EtherCAT转CANopen配置CANopen侧的PDO映射

EtherCAT转CANopen配置CANopen侧的PDO映射 在工业自动化领域&#xff0c;EtherCAT和CANopen是两种广泛应用的通信协议。它们各自具有独特的优势&#xff0c;但在某些应用场景下&#xff0c;需要将这两种协议进行转换以实现设备间的高效数据交换。本文将详细介绍如何在使用Ethe…

【QT】Qt creator快捷键

Qt creator可以通过以下步骤快捷键査看调用关系&#xff1a; 1.打开代码文件。 2.将光标放在你想要查看调用关系的函数名上。 3.按下键盘快捷键 CtrlshiftU。 4.弹出菜单中选择“调用路径”或“被调用路径” 5.在弹出的窗口中可以查看函数的调用关系 折叠或展开代码快捷键&…

【RHCE】LVS-NAT模式负载均衡实验

目录 题目 IP规划 配置IP RS1 RS2 RS3 LVS client 配置RS 配置LVS 安装lvs软件 启动ipvsadm服务 lvs规则匹配 ipvsadm部分选项 客户端测试 总结 题目 使用LVS的 NAT 模式实现 3 台RS的轮询访问&#xff0c;IP地址和主机自己规划。 IP规划 主机IP地址RS1-nat模…

排序算法(插入,希尔,选择,冒泡,堆,快排,归并)

1.插入排序 插入排序的主要思想是额外申请一个空间cur&#xff0c;让cur一开始等于数组的第1号位置,设置i1&#xff0c;让i-1的元素与其比较&#xff0c;如果arr[i-1]>arr[i]&#xff0c;就让arr[i1] arr[i]&#xff0c;当进行到最后一次对比结束&#xff0c;i-1,再让arr[…

LiteIDE中配置golang编译生成无CMD窗口EXE的步骤

LiteIDE中配置golang编译生成无CMD窗口EXE的步骤 一、环境配置1、设置GOROOT‌2、配置GOPATH‌ 二、项目编译参数设置1、新建/打开项目‌2、修改编译配置‌3、其他优化选项&#xff08;可选&#xff09;‌ 三、构建与验证1、编译生成EXE‌2、验证无窗口效果‌ 四、注意事项 一、…

Maya基本操作

基本操作 按住ALT键&#xff0c;左键旋转视角&#xff0c;中键平移视角&#xff0c;右键放大缩小视角。 按空格键切换4格视图。 导入FBX格式文件后&#xff0c;无贴图显示。 按6键开启。着色纹理显示 坐标轴相关 修改菜单-左键最上面的虚线。固定修改选项窗口。 选中物体…

Windows打开ftp局域网共享

前提是windows已经设置好开机账号密码了&#xff0c;否则教程不适用 第一先打开电脑ftp共享配置 点击保存即可 2.设置要共享到其他电脑的文件路径&#xff08;如果你要共享整个盘你就设置整个盘&#xff0c;如果是共享盘中某文件就设置某文件&#xff0c;这里是某文件&#x…

overleaf中会议参考文献使用什么标签:inproceedings

overleaf中会议参考文献使用什么标签 会议论文在LaTeX文献条目中应使用 @inproceedings 标签,而非 @article。根据你提供的内容,修正后的格式如下: @inproceedings{asai2023self, author = {Asai, Akari and Wu, Zeqiu and Wang, Yizhong and Sil, Avirup and Hajishirzi,…

一文详解redis

redis 5种数据类型 string 字符串是 Redis 里最基础的数据类型&#xff0c;一个键对应一个值。 设置值 SET key value例如&#xff1a; SET name "John"获取值 GET key例如&#xff1a; GET namelist 列表是简单的字符串列表&#xff0c;按插入顺序排序。 在列…

第16章:基于CNN和Transformer对心脏左心室的实验分析及改进策略

目录 1. 项目需求 2. 网络选择 2.1 UNet模块 2.2 TransUnet 2.2.1 SE模块 2.2.2 CBAM 2.3 关键代码 3 对比试验 3.1 unet 3.2 transformerSE 3.3 transformerCBAM 4. 结果分析 5. 推理 6. 下载 1. 项目需求 本文需要做的工作是基于CNN和Transformer的心脏左心室…

【AI】知识蒸馏-简单易懂版

1 缘起 最近要准备升级材料&#xff0c;里面有一骨碌是介绍LLM相关技术的&#xff0c;知识蒸馏就是其中一个点&#xff0c; 不过&#xff0c;只分享了蒸馏过程&#xff0c;没有讲述来龙去脉&#xff0c;比如没有讲解Softmax为什么引入T、损失函数为什么使用KL散度&#xff0c;…

2024年数维杯数学建模B题生物质和煤共热解问题的研究解题全过程论文及程序

2024年数维杯数学建模 B题 生物质和煤共热解问题的研究 原题再现&#xff1a; 随着全球能源需求的不断增长和对可再生能源的追求&#xff0c;生物质和煤共热解作为一种潜在的能源转化技术备受关注。生物质是指可再生能源&#xff0c;源自植物和动物的有机物质&#xff0c;而煤…

Spring相关API

1是相对路径 2 是绝对路径 3 在注解时使用

Netty源码—客户端接入流程

1.关于Netty客户端连接接入问题整理 一.Netty是在哪里检测有新连接接入的&#xff1f; 答&#xff1a;boss线程第一个过程轮询出ACCEPT事件&#xff0c;然后boss线程第二个过程通过JDK底层Channel的accept()方法创建一条连接。 二.新连接是怎样注册到NioEventLoop线程的&#x…