智能Bi项目-限流优化

系统优化之前的今天快速的过了一遍,对于限流这方面学习之后做个笔记记录一下。

目的

在这个项目里,限流的目的在于避免一个用户在同一时间内多接口发起多次请求,如果用户直接起多个线程来执行分析功能,在用户给的数据很大的情况下Api余额也会没的很快,所以在这里给他做个限流是有必要的,也是为了网站的安全。

常见算法

常见的限流算法有4种,分别是固定窗口、滑动窗口、漏桶算法和令牌算法。

固定窗口

固定窗口算法就是给一个固定长度的时间段,在这个时间段里你最多执行n次请求,这个算法实现起来非常简单,但容易出现流量突刺的问题,假如你的时间段是10s,阈值是10个请求,我在第9s的时候来了10个请求,然后又在第11s来了10个请求,在短短2s间隔就一下子来了20个请求,总体来看它就已经不符合10s 10个请求的限制了。

滑动窗口

为了解决固定窗口的临界值问题,就有了滑动窗口,滑动窗口算法我的理解在看,就是这个窗口会随着一个时间周期移动,比如我们的窗口长度还是10s,阈值10个请求,然后每隔2s这个窗口就会往前移动,检查的时间段就从0-10变成了2-12,这样到第9s的时候,窗口移动到了8,而9和11s会在同一个窗口内,因此11s的请求就会被限制了。但是这个算法会拒绝限流之后的请求,应用到实际产品中的话对用户是不太友好的。

漏桶算法

这个算法就不会暴力的拒绝限流后的请求了,漏桶算法的话,我把它理解成一个桶,对于进入桶里的速度我们是不确定的,但是我们可以规定它流出桶的速度,转换成业务的话,就是业务请求的速率不确定,但是可以控制请求服务的执行速率,假如我的桶一下子能容纳10个请求,一下子来了11个请求,那么第11个请求就会被拒绝掉,如果这11个请求不是一次性来,那么可能在第11个请求进来的时候前面有请求执行完毕流出桶,就能够进入了。但是由于流出速度限制住了,当流量一下子很大的时候这个算法会处理的比较慢。

令牌桶算法

令牌桶的做法是我每隔一段时间就往桶里放一定数量的令牌,当请求进来时会来桶里获取令牌,如果有令牌则请求正常执行,如果桶里没有令牌那么这个请求就会被限制,发放令牌的时间和数量设计的恰当的话,就能够实现限流的同时也能在大流量的时候保证一定的速度。GuavaRateLimiter就是基于该算法实现的。

限流实现

限流有本地限流和分布式限流,GuavaRateLimiter就适用于本地限流,分布式限流可以采用Redission或者sentinel进行限流。

Redission

项目源码中采用了Redission进行限流,Redission里有限流的工具类,实现限流的方式很简单:以用户Id来定义限流器,设置好令牌的发放数量和间隔后调用tryAcquire方法来尝试获取令牌,返回false则代表令牌此时消耗完,对该请求进行限制:

java
复制代码
@Resource private RedissonClient redissonClient; /** * 限流操作 * * @param key 区分不同的限流器,比如不同的用户 id 应该分别统计 */ public void doRateLimit(String key) { // 创建一个名称为user_limiter的限流器,每秒最多访问 2 次 RRateLimiter rateLimiter = redissonClient.getRateLimiter(key); rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS); // 每当一个操作来了后,请求一个令牌 boolean canOp = rateLimiter.tryAcquire(1); if (!canOp) { throw new BusinessException(ErrorCode.TOO_MANY_REQUEST); } }

Guava RateLimiter

我上网查了查guava的使用教程,它创建限流器的方法是RateLimiter.create,我们可以在该方法中指定每秒发放多少个令牌,但这样创建的话好像我们不知道到底是哪个用户的限流器,因此需要一个Map结构来构建用户Id-限流器的对应关系,为了线程安全,采用ConcurrentHashMap来进行存储,为了避免内存泄漏,要及时的清除哈希表里久久未操作的用户限流器,清除可以采用定时任务的方式进行,那么怎么知道清理哪些呢,我们可以按照LRU算法来找出长时间未活动的用户,只需要给限流器带上一个时间戳即可,因此我们的对应关系就变成了用户Id-带时间戳的限流器,这样我们的GuavaLimiterManager就如下所示:

java
复制代码
@Service public class GuavaLimiterManager { // 存储每个用户的 RateLimiter 和最后活动时间 private final Map<String, RateLimiterWithTimestamp> userRateLimiters = new ConcurrentHashMap<>(); // 每个用户每秒最多允许 2 次请求 private static final double REQUESTS_PER_SECOND = 2.0; // 用户最大空闲时间(单位:秒) private static final long MAX_IDLE_TIME_SECONDS = 60; // 1 分钟 /** * 检查用户是否允许发送请求 * * @param userId 用户 ID * @return true 表示允许,false 表示限流 */ public boolean allowRequest(String userId) { // 使用 computeIfAbsent 原子操作 RateLimiterWithTimestamp rateLimiterWithTimestamp = userRateLimiters.computeIfAbsent( userId, k -> new RateLimiterWithTimestamp(RateLimiter.create(REQUESTS_PER_SECOND)) ); // 更新最后活动时间 rateLimiterWithTimestamp.updateTimestamp(); RateLimiter rateLimiter = rateLimiterWithTimestamp.getRateLimiter(); boolean res = rateLimiter.tryAcquire(); if (res){ System.out.println("成功处理"); return true; }else { System.out.println("令牌消耗完,等待中"); return false; } } /** * 清理长时间未活动的用户 */ @Scheduled(fixedRate = 30, timeUnit = TimeUnit.SECONDS) // 每隔 30 秒执行一次 public void cleanUpInactiveUsers() { long currentTime = System.currentTimeMillis(); userRateLimiters.entrySet().removeIf(entry -> { long idleTime = currentTime - entry.getValue().getLastActivityTimestamp(); return idleTime > MAX_IDLE_TIME_SECONDS * 1000; // 转换为毫秒 }); System.out.println("清理完成,当前用户数量: " + userRateLimiters.size()); } /** * 封装 RateLimiter 和最后活动时间 */ private static class RateLimiterWithTimestamp { private final RateLimiter rateLimiter; private long lastActivityTimestamp; public RateLimiterWithTimestamp(RateLimiter rateLimiter) { this.rateLimiter = rateLimiter; this.lastActivityTimestamp = System.currentTimeMillis(); } public RateLimiter getRateLimiter() { return rateLimiter; } public long getLastActivityTimestamp() { return lastActivityTimestamp; } public void updateTimestamp() { this.lastActivityTimestamp = System.currentTimeMillis(); } } }

这里的代码和上面的一些思路是Ai给出来的,所以要善以利用Ai捏。

不过我在测试的时候发现一个问题,就是我规定的每秒放2个令牌,然后我用两个for循环去调用allowRequest方法,第一个for循环请求次数是2,第二个for循环请求是5,代码如下:

java
复制代码
String start = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); String userId = "1"; for (int i = 0; i < 2; i++) { System.out.println("处理请求"+i); guavaLimiterManager.allowRequest(userId); } Thread.sleep(1000); for (int i = 0; i < 5; i++) { System.out.println("处理请求"+i); guavaLimiterManager.allowRequest(userId); } String end = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); System.out.println(start + " -> " + end);

正常来说第一个for循环肯定是2个成功处理的,但实际上第二个请求没有成功获取到令牌,而第二个for循环的第2个请求又成功处理了,且后面的都打印出令牌消耗完,经过上网查资料,我推测应该是一开始可能没有放够2块令牌,而一下子突发的两个请求只有第一个请求拿到了令牌,为了防止突发请求,Guava RateLimiter有一个预热机制,即在预热时间内逐渐发放令牌,RateLimiter.create(2,1,TimeUnit.SECONDS)表示1秒时间预热以达到每秒2个令牌的速度,不过即使这样设置了也好像没用,第二个请求还是令牌消耗完,除非我在第一个for循环里直接让他休眠1s让它能够获得令牌,不过第二个for循环正确输出了出来的话,那应该上面的代码是没什么问题的了。

如果代码中有错或者我后面的想法有问题的,欢迎各位大佬指出

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
shark
作者分享
做了一个月的小程序,终于准备到即将交付的时候了,这段时间里可真是被微信小程序的各种限制折磨的不要不要的,突出一个为难开发者,有几次改代码改到了两点多才改完!小程序的后端技术栈和鱼皮的智能Bi项目采用了差不多的技术栈,通过注解+aop来对接口进行令牌桶限流,用rabbitmq来缓解调用Ai服务的压力,异步生成报告,对于订单采用了自定义线程池结合async+scheduled注解实现定时任务的异步执行,每分钟遍历删除库中的订单,还有些功能等真正交付后再做下总结。唉,不知道这个项目再加上智能图库项目能不能让我这种0相关工作经验的双非往届生有面试机会和找到工作呢,不想再次尝试付出与回报不成正比的滋味了😭😭
8
今天收到了甲方的第一笔付款💰,没想到毕业一年半居然能用自己专业技能接个活赚了点外快,还是有点小高兴的。 这两天的学习状态不太行,我好像总会在经历了持续一段时间的学习后会有两三天学不进的状态🤧,不知道大伙会不会有这种状态,虽然我很想克服这种状态,但心里就好像有东西堵在那影响着学不进去(不知道是不是焦虑),希望明天能恢复状态吧。 转眼到了3月份,接的活的时间也剩下半个月了,下半部分就要开发调用Ai接口的代码和微信支付(感觉好像挺麻烦?)的代码了,同时也需要开始准备看面试题了,想问下各位是怎么使用面试鸭来高效刷题的呢,这两天(可能跟我的状态有关)看面试题总是有种似记住但又没全记住的感觉,上次背八股还是22年秋招的时候,对于一些知识可能还有记得一些,但由于我是0开发经验社招,不知道重点要看哪些好,希望各位大佬能给小弟一点建议🙏
4
这几天没有打卡,不是摆烂了,而是前几天有个朋友推了个活给我问我说做不做,是做一个微信小程序,并且需要调用Ai的接口,他因为工作太忙没有空接这个活,而我本来正在学习Bi项目,打算学完之后写简历刷面试题投简历,所以一开始还在犹豫,觉得自己23届毕业,虽说大学自学了几年Java,但是0相关工作经验,有点没底气,不过后来我拉到了另一个同学来帮忙,那个同学在大厂实习过,有过企业开发的经验,他也乐意加入一起做项目,于是最后便接下了这个活(主要是有钱可以拿)。由于自己是前端苦手,所以也是一边问Ai一边改页面的布局,改到今天觉得差不多了,甲方也觉得ok,后面就准备和同学一起写后端的接口了,在这几天也对着项目需求跟同学讨论了许多功能和结构上的设计,也算是一种积累经验的过程了,希望这一个月的工期内能把项目做好上线,最好是能写到简历上吧,孩子真的很想要一份工作!!这一个月干的活希望不要是白忙活,不然我真的要道心破碎了😭😭😭
7
智能BI项目-调用硅基流动的DeepSeek
19
智能图库模块-协同编辑
6
下载 APP