资源限流 + 本地分布式多重锁——高并发性能挡板,隔绝无效流量请求
前言
在高并发分布式下,我们往往采用分布式锁去维护一个同步互斥的业务需求,但是大家细想一下,在一些高TPS的业务场景下,让这些请求全部卡在获取分布式锁,这会造成什么问题?
瞬时高并发压垮系统
众所周知,一个 SpringBoot 应用的同一时间在运行的请求是有限的,因为 SpringBoot 处理请求底层也是个线程池。我截图个 Hippo4j 监控到的 SpringBoot Tomcat 容器线程池举例。

通过上图得知,SpringBoot Tomcat 容器默认情况下,同一时间最多能处理 200 个请求。如果要应对上千万的 TPS 明显是不可能的。
如果我们直接上分布式锁来维护那么一个同步互斥的业务需求,大量请求会因为分布式锁的申请而发生阻塞,导致请求无法快速处理。这会导致后续请求长时间被阻塞,使系统陷入假死状态。无论请求的数量有多大,系统都无法返回响应。此外,随着请求的积累,还存在内存溢出的风险。更糟糕的是,如果 SpringBoot Tomcat 的线程池被分布式锁占用,查询请求也将无法得到响应。系统直接嘎了....
无效请求浪费资源
假如一趟列车有几十万人抢票,但是真正能购票的用户可能也就几千人。也就意味着哪怕几十万人都去请求这个分布式锁,最终也就几十万人中的几千人是有效的,其它都是无效获取分布式锁的行为。这样子就给 Redis 申请分布式锁带来巨大的开销压力
那么针对上述两个问题,我们该如何优雅解决?且看下文解析
资源限流算法
1. 什么是限流
限流(Rate Limiting)是一种应用程序或系统资源管理的策略,用于控制对某个服务、接口或功能的访问速率。它的主要目的是防止过度的请求或流量超过系统的处理能力,从而保护系统的稳定性、可靠性和安全性。
通过限制访问速率,限流可以防止以下问题的发生:
- 过度使用资源:限流可以防止某个用户或客户端过度使用系统资源,从而保护服务器免受过载的影响。
- 防止垃圾请求:限流可以过滤掉恶意或无效的请求,例如恶意攻击、爬虫或垃圾邮件发送等。
- 维护服务质量:通过限制访问速率,可以确保每个请求都能够得到适当的处理和响应时间,从而提高服务质量和用户体验。
- 控制成本:限流可以帮助控制系统资源的使用,避免因为过多的请求而导致不必要的成本增加。
上文也讲到过,一个列车可能就几百上千人能购买成功,但可能会有远超过这个量级的用户进行抢票,在真正执行抢票逻辑前,可以通过限流算法进行限制,只让少量用户操作购票流程。
2. 常见限流算法
限流可以通过多种算法方式实现,比如:
- 固定窗口算法(Fixed Window Algorithm):该算法将时间划分为固定大小的窗口,例如每秒、每分钟或每小时。在每个窗口内,限制请求的数量不得超过预设的阈值。这种算法简单直观,但可能存在突发请求超过阈值的问题。
- 滑动窗口算法(Sliding Window Algorithm):该算法将时间划分为连续的时间片段,例如每秒划分为多个小的时间片段。每个时间片段都有自己的请求计数器,记录在该时间片段内的请求数量。当请求到达时,会逐渐删除过时的时间片段,并根据当前时间片段内的请求数量判断是否超过阈值。这种算法可以更好地处理突发请求。
- 令牌桶算法(Token Bucket Algorithm):该算法模拟了一个令牌桶,桶中以固定速率生成令牌。每个令牌代表一个请求的许可。当请求到达时,需要先从令牌桶中获取令牌,如果桶中没有足够的令牌,则请求被限制。这种算法可以平滑地控制请求的速率。
- 漏桶算法(Leaky Bucket Algorithm):该算法类似于一个漏桶,请求以固定速率进入漏桶。如果漏桶已满,则多余的请求将被丢弃或延迟处理。这种算法可以稳定请求的处理速率,防止突发请求对系统造成压力。
这些算法都有不同的特点和适用场景,选择适合的限流算法取决于应用程序的需求和预期的限流效果。在实际应用中,也可以根据具体情况结合多种算法来实现更复杂的限流策略。
这些算法网上介绍的较为完善,大家可以搜索相关文章详细了解,这里不过多赘述。
实际业务学习
假设我们现在需要设计一个架构来满足国庆假期热门列车的车票售卖业务
业务分析
对于五一、国庆以及过年这些节日来说,一些热门列车的 TPS 少说有几十万 TPS。如果仅仅采用所有请求都进行分布式锁竞争去同步互斥进行购座下单的设计,直接就会导致前面提到的瞬时高并发压垮系统问题,那这块的分布式锁逻辑是不是可以优化呢?比如不让所有抢购列车的用户去申请分布式锁,而是让少量用户去请求获取分布式锁。这样优化的话,可以极大情况节省 Redis 申请分布式锁的开销压力。
优化思路
我们可以采用双重判定锁的思路,在竞争分布式锁前判断它有没有资格去竞争先,只要没有资格竞争的就一边凉快儿去,只有剩下那些具备竞争资格的请求才能到达下一步竞争环境,大家想想,对于当前业务场景来说,如果把车票当作一个令牌,在竞争锁前先让他们去抢这些令牌,只有抢到令牌的人才能进行竞争分布式锁同步互斥下单操作,那么你看,几十万的TPS不就变成了几千个TPS了嘛,这样优化的话,可以极大情况节省 Redis 申请分布式锁的开销压力。
伪代码实现
相信大家已经明白了精髓,这里我就不贴多详细的代码了,精华往往一点即通~以下是简要的伪代码:
if(令牌容器在缓存中失效){重新读取令牌资源,并放入缓存中充当令牌容器
}
String token = Lua脚本实现查询余额大于0就返回,并余额减一,确保两操作的原子性
if(token != null){RLock lock = redissonClient.getFairLock(lockKey);lock.lock();try {// 执行购票流程return executePurchaseTickets(requestParam);} finally {// 释放分布式公平锁lock.unlock();}
}
不知道上述讲解大家对于分布式锁的运用设计有没有新的思路呢?但是还没有结束噢,下面我们再来深入一下
本地分布式多重锁
优化思路
类似于这种有加分布式锁逻辑的,大多数都是集群化部署,是否需要考虑封装下加锁逻辑呢?,比如线程先去竞争单个服务的内部锁,竞争成功再去竞争分布式锁,从而减少redis的压力,其实本质上就是一个逐级打怪的过程,我先在蛇窝里当上蛇头了,代表所有蛇去龙穴里去和其他的蛇头竞争龙头,那么经过这么一轮的再度过滤,竞争的分布式锁的TPS是不是就更小了呢?
1. 构建本地分布式多重锁
接口的实现逻辑需要再次重构下,从单分布式锁的获取变为多种锁的组合获取。
private final ConcurrentHashMap<String, ReentrantLock> localLockMap = new ConcurrentHashMap<>();@Override
@Transactional(rollbackFor = Throwable.class)
public TicketPurchaseRespDTO purchaseTicketsV2(PurchaseTicketReqDTO requestParam) {// .....// 构建锁唯一 KeyString lockKey = environment.resolvePlaceholders(String.format(LOCK_PURCHASE_TICKETS, requestParam.getTrainId()));// 根据锁唯一 Key 获取本地锁,通过 ConcurrentHashMap 保证并发读写数据安全ReentrantLock localLock = localLockMap.computeIfAbsent(lockKey, key -> new ReentrantLock(true));// 先获取本地公平锁,因为咱们上面创建锁指定了公平模式 new ReentrantLock(true)localLock.lock();try {// 获取到本地公平锁后,开始获取分布式公平锁RLock lock = redissonClient.getFairLock(lockKey);lock.lock();try {// 执行购票流程return executePurchaseTickets(requestParam);} finally {// 释放分布式公平锁lock.unlock();}} finally {// 释放本地公平锁localLock.unlock();}
}
从实现咱们上述功能来说,这个代码已经没问题了。但是,仔细思考下,是否还有一些潜在逻辑是没考虑到的?
2. 本地锁内存安全思考
上面这个程序安全么?在看到这里时,大家思考下。
结论是不安全的,可能会有内存溢出的风险。问题就出在本地锁存储容器上。
我们通过 ConcurrentHashMap 存储每个列车的本地锁,作为申请分布式锁之前的一层性能挡板,隔绝无效流量请求 Redis。但是大家发现没有,这个 ConcurrentHashMap 是只能存储,但是没有任何过期策略。这样会导致一个问题就是应用长时间不发布,越来越多的列车数据存储在容器中,直到内存溢出为止。
怎么实现一个线程安全以及内存安全的本地锁容器?伪代码如下,大家仅作为参考即可。
通过 Caffeine 创建本地安全锁容器,Caffeine 的 expireAfterWrite 方法代表,放入元素过期的时间是什么。比如咱们以下案例中配置的一天过期,代表一个列车的本地公平锁创建一天后失效。
private final Cache<String, ReentrantLock> localLockMap = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).build();@Override
@Transactional(rollbackFor = Throwable.class)
public TicketPurchaseRespDTO purchaseTicketsV2(PurchaseTicketReqDTO requestParam) {// ......String lockKey = environment.resolvePlaceholders(String.format(LOCK_PURCHASE_TICKETS, requestParam.getTrainId()));// getIfPresent 类似于 HashMap 中 get 方法ReentrantLock localLock = localLockMap.getIfPresent(lockKey);// 不存在的话执行加载流程if (localLock == null) {// Caffeine 不像 ConcurrentHashMap 做了并发读写安全控制,这里需要咱们自己控制synchronized (TicketService.class) {// 双重判定的方式,避免重复创建if ((localLock = localLockMap.getIfPresent(lockKey)) == null) {// 创建本地公平锁并放入本地公平锁容器中localLock = new ReentrantLock(true);localLockMap.put(lockKey, localLock);}}}localLock.lock();try {RLock lock = redissonClient.getFairLock(lockKey);lock.lock();try {return executePurchaseTickets(requestParam);} finally {lock.unlock();}} finally {localLock.unlock();}
}
文末总结
希望通过以上两个优化方向的讲解,能给大家对分布式锁的设计带来新的思路,最后再给大家引用一位大佬的话:
技术设计中不存在“银弹”。选择技术选型往往会有得失,多方面权衡后选择出一个适合项目的使用即可。
一起加油吧!陌生的程序人

相关文章:
资源限流 + 本地分布式多重锁——高并发性能挡板,隔绝无效流量请求
前言 在高并发分布式下,我们往往采用分布式锁去维护一个同步互斥的业务需求,但是大家细想一下,在一些高TPS的业务场景下,让这些请求全部卡在获取分布式锁,这会造成什么问题? 瞬时高并发压垮系统 众所周知…...
day52【子序列】300.最长递归子序列 674.最长连续递增序列 718.最长重复子数组
文章目录 300.最长递增子序列674.最长连续递增序列718.最长重复子数组 300.最长递增子序列 题目链接:力扣链接 讲解链接:代码随想录链接 题意:给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 子序列 是由数组派生而…...
计算机视觉 计算机视觉识别是什么?
计算机视觉识别(Computer Vision Recognition)是计算机科学和人工智能领域中的一个重要分支,它致力于使计算机系统能够模拟和理解人类视觉的过程,从而能够自动识别、分析和理解图像或视频中的内容。这一领域的发展旨在让计算机具备…...
Make.com实现多个APP应用的自动化的入门指南
Make.com是一款基于云的自动化平台,可帮助用户将多个应用程序连接在一起,并通过设置自动化流程来简化日常任务。Make.com提供丰富的API集成,支持连接各种流行的应用程序,包括社交媒体、电子商务、CRM等。 使用Make.com实现多个AP…...
LLMs之HFKR:HFKR(基于大语言模型实现异构知识融合的推荐算法)的简介、原理、性能、实现步骤、案例应用之详细攻略
LLMs之HFKR:HFKR(基于大语言模型实现异构知识融合的推荐算法)的简介、原理、性能、实现步骤、案例应用之详细攻略 目录 HFKR的简介 异构知识融合:一种基于LLM的个性化推荐新方法...
多模态 多引擎 超融合 新生态!2023亚信科技AntDB数据库8.0产品发布
9月20日,以“多模态 多引擎 超融合 新生态”为主题的亚信科技AntDB数据库8.0产品发布会成功举办,从技术和生态两个角度全方位展示了AntDB数据库第8次大型能力升级和生态建设成果。浙江移动、用友、麒麟软件、华录高诚、金云智联等行业伙伴及业界专家共同…...
elasticsearch无法访问9200端口
近期部署elasticsearch后,启动时发现一直报如下错误: curl: (7) Failed connect to localhost:9200; Connection refused 部署的版本为elasticsearch-7.13.2,排查原因是因为开启了ssl认证。 解决方法: 在/opt/software/elasticsearch-7.13.2/config下…...
【Linux】进程等待
文章目录 进程等待进程等待必要性实验(见见猪跑)进程等待的方法wait方法waitpid**方法**宏的使用方法获取子进程status 阻塞VS非阻塞概念对比非阻塞有什么好处 具体代码实现进程的阻塞等待方式:进程的非阻塞等待方式:让父进程做其他任务 进程等待 进程等待必要性 之前讲过&am…...
电视「沉浮录」:跌出家电“三大件”?
【潮汐商业评论/原创】 “这年头谁还看电视,家里电视近一年都没打开过了,我明天就打算把它二手卖掉。”想到已落灰许久的电视机,Andy打开了二手平台。 “要不是这几年孩子网课多,我是真没考虑换新电视,家里用了8年的…...
前端实现调用打印机和小票打印(TSPL )功能
Ⅰ- 壹 - 使用需求 前端 的方式 点击这个按钮,直接让打印机打印我想要的东西 Ⅱ - 贰 - 小票打印 目前比较好的方式就是直接用 TSPL 标签打印指令集, 基础环境就不多说了,这个功能的实现就是利用usb发送指令,现在缺少个来让我们能够和usb沟通的工具,下面这就是推…...
串口通信(6)应用定时器中断+串口中断实现接收一串数据
本文为博主 日月同辉,与我共生,csdn原创首发。希望看完后能对你有所帮助,不足之处请指正!一起交流学习,共同进步! > 发布人:日月同辉,与我共生_单片机-CSDN博客 > 欢迎你为独创博主日月同…...
【WinForm详细教程六】WinForm中的GroupBox和Panel 、TabControl 、SplitContainer控件
文章目录 1.GroupBox和Panel2.TabControl3.SplitContainer 1.GroupBox和Panel GroupBox:是一个分组容器,提供一个框架将相关的控件组织在一起,它有标题、边框,但没有滚动条。 Panel:也是一个容器控件,用来…...
gradle与maven
Gradle 和 Maven 都是流行的构建工具,通常用于构建和管理 Java 和 Android 项目。它们都可以自动下载依赖库、编译代码、运行测试、打包和发布等。 以下是对 Gradle 和 Maven 的介绍: Gradle: Gradle 是一个基于 Groovy 和 Kotlin 的构建自…...
2.Docker基本架构简介与安装实战
1.认识Docker的基本架构 下面这张图是docker官网上的,介绍了整个Docker的基础架构,我们根据这张图来学习一下docker的涉及到的一些相关概念。 1.1 Docker的架构组成 Docker架构是由Client(客户端)、Docker Host(服务端)、Registry(远程仓库)组成。 …...
拓世法宝 | 数字经济崛起,美业如何抓住流量风口?
爱美之心,人皆有之。无论男女,都会很自然地对美好事物燃起兴致,跟高颜值相关的事物总能聚集注意力。例如直播平台里的美女网红收割流量赚得盆满钵满,面庞俊俏的年轻偶像吸引万千粉丝,还有“央视最美记者”王冰冰、“最…...
Scala 泛型编程
1. 泛型 Scala 支持类型参数化,使得我们能够编写泛型程序。 1.1 泛型类 Java 中使用 <> 符号来包含定义的类型参数,Scala 则使用 []。 class Pair[T, S](val first: T, val second: S) {override def toString: String first ":" sec…...
索引失效的场景有哪些?
虽然你这列上建了索引,查询条件也是索引列,但最终执行计划没有走它的索引。下面是引起这种问题的几个关键点。 列与列对比 某个表中,有两列(id和c_id)都建了单独索引,下面这种查询条件不会走索引 select…...
Java进阶04 final关键字、abstract抽象、interface接口、JDK8与JDK9中接口的区别、内部类和匿名类
文章目录 一、final关键字二、abstract关键字三、接口interface四、JDK8和JDK9中接口的区别五、内部类 一、final关键字 final可以修饰类、方法、变量 用final修饰类 表示此类不能被继承 用final修饰方法 表示方法不可以被重写 用final修饰变量 既可以修饰成员变量也可以修饰…...
Python的web自动化学习(五)Selenium的隐式等待(元素定位)
引言: WebDriver隐式等待是一种全局性的等待方式,它会在查找元素时设置一个固定的等待时间。当使用隐式等待时,WebDriver会在查找元素时等待一段时间,如果在等待时间内找到了元素,则立即执行下一步操作;如果…...
20231102从头开始配置cv180zb的编译环境(欢迎入坑,肯定还有很多问题等着你)
20231102从头开始配置cv180zb的编译环境(欢迎入坑,肯定还有很多问题等着你) 2023/11/2 11:31 (欢迎入坑,本篇只是针对官方的文档整理的!只装这些东西你肯定编译不过的,还有很多问题等着你呢&…...
告别黑屏和错位!Uniapp视频轮播最佳实践:巧用v-if与swiper事件实现无缝切换
Uniapp视频轮播组件深度优化:从黑屏错位到无缝体验的全链路解决方案 在移动应用开发中,视频轮播组件已经成为提升用户参与度的关键元素。然而,当Uniapp开发者尝试在swiper组件中嵌入视频时,常常会遇到视频位置偏移、黑屏闪现、自动…...
Linux系统学习:38张思维导图构建核心知识体系
1. Linux学习思维导图概述作为一名从嵌入式开发转战云计算的老兵,我深知系统化学习Linux的重要性。最近整理硬盘时翻出一套珍藏多年的学习资料——38张涵盖Linux核心知识体系的思维导图,这些图纸曾帮助我顺利通过RHCE认证,也指导过团队新人快…...
FastAPI系列 4 - 模块化路由的艺术:APIRouter实战指南
1. 为什么需要模块化路由? 第一次用FastAPI开发电商后台时,我把所有路由都堆在main.py里。三个月后这个文件膨胀到2000多行代码,每次修改用户认证逻辑都要在订单处理和商品列表的代码块之间来回翻找。这种经历让我深刻理解了为什么APIRouter会…...
Vue 组态化管道流动效果:从零构建现代化流体模拟系统
1. 为什么需要管道流动模拟系统 在工业自动化和教学演示领域,可视化管道系统是一个常见需求。想象一下化工厂的液体输送管道、城市供水系统或者实验室的流体实验装置,这些场景都需要直观展示流体在管道中的流动状态。传统做法是使用静态图片或简单动画&a…...
程序员体检报告暗语:甲状腺结节=加班等级说明书
一、当体检报告出现“甲状腺结节”翻开软件测试工程师的体检报告,“甲状腺结节”已成为高频词。医学定义中,甲状腺结节是甲状腺细胞异常增生形成的肿块,随吞咽移动,临床检出率超20%(数据来源:2023年《中国甲…...
【限时解禁】Cuvil编译器v0.9.3内部架构设计图(含Python动态类型静态化映射表),仅开放72小时
第一章:Cuvil 编译器在 Python AI 推理中的应用Cuvil 是一款面向 AI 工作负载的轻量级领域专用编译器,专为优化 Python 生态中基于 PyTorch 和 ONNX 的模型推理而设计。它通过静态图重写、算子融合与硬件感知调度,在不修改用户代码的前提下&a…...
探索偏心轮飞剪的 Codesys 程序奥秘:基于偏心轮加滑块机构
偏心轮 飞剪 电子凸轮 codesys程序源码 适用于偏心轮加滑块机构 在自动化控制领域,偏心轮飞剪系统凭借其独特的运动特性和高效的切割能力,在众多生产场景中发挥着关键作用。今天咱们就深入探讨基于偏心轮加滑块机构的偏心轮飞剪的 Codesys 程序源码&…...
新手入门:在快马平台动手实现你的第一个ui-ux-pro-max设计页面
作为一个刚接触前端设计的新手,最近在InsCode(快马)平台尝试做了一个UI-UX-Pro-Max级别的登录注册页面,整个过程意外地顺利。这里记录下我的实践过程,希望能帮到同样想入门的朋友。 从零搭建页面框架 先用HTML搭建基础结构,包含表…...
MOS管选型实战指南
MOS管(金属氧化物半导体场效应晶体管)是现代电力电子和开关电路的核心元件。选型失误的后果往往是灾难性的——效率低下、发热严重、驱动振荡、甚至炸管冒烟。相比电阻电容,MOS管的选型需要权衡的维度更多:电压、电流、导通电阻、开关速度、驱动电压、热阻、体二极管特性……...
技术洞察:zyfun如何重构跨平台视频播放体验
技术洞察:zyfun如何重构跨平台视频播放体验 【免费下载链接】zyfun 跨平台桌面端视频资源播放器,免费高颜值. 项目地址: https://gitcode.com/gh_mirrors/zy/zyfun 在数字娱乐快速发展的今天,跨平台视频播放器面临着系统兼容性、性能优化和用户体…...
