资源限流 + 本地分布式多重锁——高并发性能挡板,隔绝无效流量请求

news2025/7/9 14:32:38

前言

在高并发分布式下,我们往往采用分布式锁去维护一个同步互斥的业务需求,但是大家细想一下,在一些高TPS的业务场景下,让这些请求全部卡在获取分布式锁,这会造成什么问题?

瞬时高并发压垮系统

众所周知,一个 SpringBoot 应用的同一时间在运行的请求是有限的,因为 SpringBoot 处理请求底层也是个线程池。我截图个 Hippo4j 监控到的 SpringBoot Tomcat 容器线程池举例。

image.png

通过上图得知,SpringBoot Tomcat 容器默认情况下,同一时间最多能处理 200 个请求。如果要应对上千万的 TPS 明显是不可能的。

如果我们直接上分布式锁来维护那么一个同步互斥的业务需求,大量请求会因为分布式锁的申请而发生阻塞,导致请求无法快速处理。这会导致后续请求长时间被阻塞,使系统陷入假死状态。无论请求的数量有多大,系统都无法返回响应。此外,随着请求的积累,还存在内存溢出的风险。更糟糕的是,如果 SpringBoot Tomcat 的线程池被分布式锁占用,查询请求也将无法得到响应。系统直接嘎了....

无效请求浪费资源

假如一趟列车有几十万人抢票,但是真正能购票的用户可能也就几千人。也就意味着哪怕几十万人都去请求这个分布式锁,最终也就几十万人中的几千人是有效的,其它都是无效获取分布式锁的行为。这样子就给 Redis 申请分布式锁带来巨大的开销压力

那么针对上述两个问题,我们该如何优雅解决?且看下文解析

资源限流算法

1. 什么是限流

限流(Rate Limiting)是一种应用程序或系统资源管理的策略,用于控制对某个服务、接口或功能的访问速率。它的主要目的是防止过度的请求或流量超过系统的处理能力,从而保护系统的稳定性、可靠性和安全性。

通过限制访问速率,限流可以防止以下问题的发生:

  1. 过度使用资源:限流可以防止某个用户或客户端过度使用系统资源,从而保护服务器免受过载的影响。
  2. 防止垃圾请求:限流可以过滤掉恶意或无效的请求,例如恶意攻击、爬虫或垃圾邮件发送等。
  3. 维护服务质量:通过限制访问速率,可以确保每个请求都能够得到适当的处理和响应时间,从而提高服务质量和用户体验。
  4. 控制成本:限流可以帮助控制系统资源的使用,避免因为过多的请求而导致不必要的成本增加。

上文也讲到过,一个列车可能就几百上千人能购买成功,但可能会有远超过这个量级的用户进行抢票,在真正执行抢票逻辑前,可以通过限流算法进行限制,只让少量用户操作购票流程。

2. 常见限流算法

限流可以通过多种算法方式实现,比如:

  1. 固定窗口算法(Fixed Window Algorithm):该算法将时间划分为固定大小的窗口,例如每秒、每分钟或每小时。在每个窗口内,限制请求的数量不得超过预设的阈值。这种算法简单直观,但可能存在突发请求超过阈值的问题。
  2. 滑动窗口算法(Sliding Window Algorithm):该算法将时间划分为连续的时间片段,例如每秒划分为多个小的时间片段。每个时间片段都有自己的请求计数器,记录在该时间片段内的请求数量。当请求到达时,会逐渐删除过时的时间片段,并根据当前时间片段内的请求数量判断是否超过阈值。这种算法可以更好地处理突发请求。
  3. 令牌桶算法(Token Bucket Algorithm):该算法模拟了一个令牌桶,桶中以固定速率生成令牌。每个令牌代表一个请求的许可。当请求到达时,需要先从令牌桶中获取令牌,如果桶中没有足够的令牌,则请求被限制。这种算法可以平滑地控制请求的速率。
  4. 漏桶算法(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) {
	// .....
	// 构建锁唯一 Key
    String 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();
    }
}

文末总结

希望通过以上两个优化方向的讲解,能给大家对分布式锁的设计带来新的思路,最后再给大家引用一位大佬的话:

技术设计中不存在“银弹”。选择技术选型往往会有得失,多方面权衡后选择出一个适合项目的使用即可。

一起加油吧!陌生的程序人

image.png

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

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

相关文章

C#开发的OpenRA游戏之选择建筑物显示状态信息

C#开发的OpenRA游戏之选择建筑物显示状态信息 前面已经分析了金钱系统,有了钱之后,就可以放任地建造万物了。 在建造的过程中,当你选择建筑物时,就可以看到当前建筑物的健康信息和制造当前物品的进度,如下图所示: 从上图可以看到最上面的进度条是这个建筑物的健康程度,…

Java 代码读取自定义的配置文件里面的东西

目录 1 问题2 实现 1 问题 Java 代码读取自定义的配置文件里面的东西&#xff0c;在代码里面进行使用。 也就是随便一个配置文件&#xff0c;如何在代码里面进行读取 2 实现 将配置文件&#xff0c;随便起一个名字&#xff0c;将这个配置文件写到 resource 这个目录下&#…

Java调用HTTPS接口,绕过SSL认证

1&#xff1a;说明 网络编程中&#xff0c;HTTPS&#xff08;Hypertext Transfer Protocol Secure&#xff09;是一种通过加密的方式在计算机网络上进行安全通信的协议。网络传输协议&#xff0c;跟http相比更安全&#xff0c;因为他加上了SSL/TLS协议来加密通信内容。 Java调…

主播直播美颜SDK:性能优化策略

当下&#xff0c;主播直播美颜SDK成为了越来越多主播的利器。这些SDK可以实时美化主播的外貌&#xff0c;提高视觉吸引力&#xff0c;但同时也需要处理大量的图像数据。因此&#xff0c;性能优化成为了不可或缺的一环。本文将探讨主播直播美颜SDK的性能优化策略&#xff0c;以确…

【word技巧】word文档如何转换为ppt文件?

大家是否会遇到需要将word文档转换为ppt文件的情况&#xff1f;除了反反复复粘贴复制以外&#xff0c;还有其他方法可以转换文件格式&#xff0c;今天给大家分享word转换ppt方法。 首先我们先将word文件打开大纲模式 然后我们将文中的大标题设置为1级标题&#xff0c;副标题设…

【C++初阶】——初始化列表static成员友元内部类匿名对象

个人主页点击直达&#xff1a;小白不是程序媛 C系列专栏&#xff1a;C头疼记 代码仓库&#xff1a;Gitee 目录 前言 再谈构造函数 初始化列表 explicit关键字 匿名对象 static成员 友元 友元函数 友元类 内部类 再次理解类和对象 前言 前几篇文章我们对于分别对C类…

未来的无人驾驶时代会对我们带来哪些影响?

未来的无人驾驶时代&#xff1a;无人驾驶技术发展现状、未来趋势以及迈入无人驾驶时代后会给我们带来哪些积极影响 过去&#xff0c;说起无人驾驶汽车&#xff0c;就像是科幻小说中的情节。但时至今日&#xff0c;这种情况已经改观&#xff1a;谷歌、特斯拉、福特等多家大型公…

docker打包container成image,然后将image上传到docker hub

第一步&#xff1a;停止正在运行的容器 docker stop <container_name> eg: docker stop xuanjie_mlir 第二步&#xff1a;将对应的container打包成image docker commit <container_id> <镜像名&#xff1a;版本> eg&#xff1a;docker commit 005672e6d97a…

开放式耳机能保护听力吗,开放式耳机跟骨传导耳机哪个更好?

如果从严格意义上来讲的话&#xff0c;开放式耳机中的骨传导耳机是能保护听力&#xff0c;现如今的开放式耳机是一个统称&#xff0c;将所有不入耳的类目全部规划到一块。因此在开放式耳机中存在着一些耳机是只能够保持周边环境音&#xff0c;而不是保护听力的。 下面让我来给…

基于单片机设计的电子柜锁

一、前言 随着现代社会的不断发展&#xff0c;电子柜锁的应用越来越广泛。传统的机械柜锁存在一些不便之处&#xff0c;例如钥匙容易丢失、密码容易泄露等问题。设计一款基于单片机的电子柜锁系统成为了一个有趣而有意义的项目。 该电子柜锁系统通过电磁锁作为柜锁的开关&…

esp32 WiFi连接失败后,可打印错误码分析连接失败的原因

esp32 WiFi连接失败后,可打印错误码分析连接失败的原因; 在事件中添加红色选框中的代码: WiFi连接失败错误码如下所示: REASON_UNSPECIFIED = 1,REASON_AUTH_EXPIRE = 2,REASON_AUTH_LEAVE = 3,REASON_ASSOC_EXPIRE …

基于uniapp与uview做一个按拼音首字母排序的通讯录页面

效果图&#xff1a; 第一步导入pinyin库并应用&#xff0c;用于区分汉字的拼音首字母 npm i pinyin import pinyin from "pinyin" 完整算法&#xff1a; function getListByPinyinFirstLetter(data) {const newList {};for (const item of data) {let firstLett…

AI全栈大模型工程师(十四)数据连接封装

文章目录 二、数据连接封装2.1 文档加载器:Document Loaders2.2 文档处理器2.3 文档向量化:Text Embeddings2.4 向量的存储(与索引):Vectorstores2.5 向量检索:Retrievers后记二、数据连接封装 2.1 文档加载器:Document Loaders !pip install pypdffrom langchain.docu…

MICCAI2023论文多模态论文速读-1

文章目录 1.Attentive Deep Canonical Correlation Analysis for Diagnosing Alzheimer’s Disease Using Multimodal Imaging Genetics2.Bidirectional Mapping with Contrastive Learning on Multimodal Neuroimaging Data3.CoLa-Diff: Conditional Latent Diffusion Model f…

ruoyi系统改造

前端启动报错&#xff1a;Error: error:0308010C:digital envelope routines::unsupported 修改ruoyi-ui/package.json&#xff0c;添加export NODE_OPTIONS–openssl-legacy-provider && "scripts": {"dev": "export NODE_OPTIONS--openssl…

C#Onnx模型信息查看工具

效果 Netron效果 项目 代码 using Microsoft.ML.OnnxRuntime; using System; using System.Collections.Generic; using System.Text; using System.Windows.Forms;namespace Onnx_Demo {public partial class frmMain : Form{public frmMain(){InitializeComponent();}string…

性能测试怎么入门?一文7个知识点带你成功入门

一、相关概念 1、性能测试相关&#xff1a;负载测试&#xff0c;性能测试&#xff0c;压力测试&#xff0c;稳定性测试&#xff0c;全链路测试等 2、性能指标&#xff1a;吞吐率&#xff0c;tps&#xff0c;并发用户数&#xff0c;吞吐量&#xff0c;响应时间等 二、性能测试…

colly库的爬虫程序

使用colly库的爬虫程序&#xff0c;该程序将使用Go语言爬取内容。 package main ​ import ("fmt""log" ​"github.com/gocolly/colly" ) ​ func main() {// 创建一个Colly爬虫实例c : colly.NewCrawler() ​// 设置服务器c.OnRequest(func(r …

我做云原生的那几年

背景介绍 在2020年6月&#xff0c;我加入了一家拥有超过500人的企业。彼时&#xff0c;前端团队人数众多&#xff0c;有二三十名成员。在这样的大团队中&#xff0c;每个人都要寻找自己的独特之处和核心竞争力。否则&#xff0c;你可能会沉没于常规的增删改查工作中&#xff0…

Redis配置多个端口记录

一、背景&#xff1a; 使用Redis做WEB系统缓存&#xff0c;如登录信息、数据字典 等 键值对信息&#xff1b;存在多个测试环境及开发连接使用默认的6379端口&#xff0c;易造成Key重复&#xff0c;缓存紊乱&#xff0c;网络堵塞&#xff1b; 额外增开6380、6381端口&#xff0c…