限流组件开发与学习
为了让羊毛党利用脚本等工具快速多次抽奖印象活动体验,我们可以写一个限流的组件,来阻止短时间内大量请求。
首先来定义这个注解:RateLimiterAccessInterceptor
注解 RateLimiterAccessInterceptor
1 | (RetentionPolicy.RUNTIME) |
这个注解用于标识需要进行限流的方法。该注解包含以下属性:
- key: 用于指定哪个字段作为限流的标识符,默认值为
"all"
,表示对所有请求统一限流。 - permitsPerSecond: 每秒允许的请求次数,用于配置限流的频率。
- blacklistCount: 黑名单拦截的阈值,当某个标识符的请求次数超过该值后,将其加入黑名单。默认值为
0
,表示不启用黑名单功能。 - fallbackMethod: 当请求被限流或拦截后,执行的回调方法名称。
切面类 RateLimiterAOP
RateLimiterAOP
是一个切面类,负责拦截标注了 RateLimiterAccessInterceptor
注解的方法,并实现具体的限流逻辑。
主要成员变量:
- rateLimiterSwitch: 通过
@DCCValue
注解从配置中心获取的限流开关,用于控制限流功能的开启和关闭。 - loginRecord: 使用 Guava 的
Cache
实现,每个key
对应一个RateLimiter
,用于控制请求频率。记录的有效期为 1 分钟。 - blacklist: 使用 Guava 的
Cache
实现,用于记录被加入黑名单的标识符,记录的有效期为 24 小时。
1 | 4j |
切点定义:
定义了一个切点 aopPoint
,匹配所有标注了 RateLimiterAccessInterceptor
注解的方法。
1 | 4j |
环绕通知 doRouter
:
RateLimiter是什么类?
RateLimiter
类介绍:
- 来源:
RateLimiter
是 Google Guava 库中的一个类,位于com.google.common.util.concurrent
包中。 - 功能:实现了基于令牌桶算法的限流器,用于控制代码执行的速率,防止系统被过多请求压垮。
主要特性:
- 令牌桶算法:
RateLimiter
采用令牌桶算法,通过以固定速率生成令牌,控制请求的速率。- 每个请求在执行前需要从桶中获取一个令牌,如果令牌可用,则允许请求执行;否则,拒绝请求。
- 两种获取令牌的方式:
- 阻塞获取:
acquire()
方法会阻塞,直到获取到令牌。 - 非阻塞获取:
tryAcquire()
方法会立即返回,表示是否成功获取到令牌。
- 阻塞获取:
- 速率控制:
- 可以动态调整令牌生成速率,适应不同的限流需求。
主要方法:
RateLimiter.create(double permitsPerSecond)
:- 创建一个以指定速率(每秒发放的令牌数)生成令牌的
RateLimiter
实例。因此,我们在用@RateLimiterAccessInterceptor
注解一个方法的时候,需要定义这个permitsPerSecond
字段,他决定了隔多少秒可以抽一次奖。
- 创建一个以指定速率(每秒发放的令牌数)生成令牌的
boolean tryAcquire()
:- 尝试立即获取一个令牌,如果成功返回
true
,否则返回false
。 - 非阻塞方式,适用于需要快速判断是否允许请求的场景。
- 尝试立即获取一个令牌,如果成功返回
void acquire()
:- 阻塞直到获取到一个令牌,适用于需要严格限制执行速率的场景。
核心逻辑
这是核心的限流逻辑实现。以下是详细流程:
- 限流开关检查:
- 如果
rateLimiterSwitch
未配置或值为"close"
,则不进行限流,直接执行目标方法pjp.proceed()
。
- 如果
- 获取限流标识符:
- 从注解中获取
key
属性值。 - 通过
getAttrValue
方法,从目标方法的参数中提取出key
对应的值keyAttr
。 - 如果
keyAttr
为"all"
,则表示对所有请求统一限流;否则,对特定标识符(如userId
)进行限流。
- 从注解中获取
- 黑名单检查:
- 如果
blacklistCount
不为0
,并且keyAttr
已经在黑名单中且超过了blacklistCount
,则直接调用回调方法fallbackMethod
,拒绝此次请求。
- 如果
- 限流检查:
- 从
loginRecord
中获取对应key
的RateLimiter
实例,如果不存在则创建一个新的RateLimiter
,并放入缓存中。loginRecord
中的RateLimiter
会在 1 分钟后过期,若该用户在此期间没有新的请求,其RateLimiter
会被移除。 - 调用
rateLimiter.tryAcquire()
尝试获取一个许可。如果获取失败,表示当前请求超出限流频率:- 如果
blacklistCount
不为0
,即配置的黑名单阈值不为0
。则记录此次超频行为,更新黑名单计数。 - 调用回调方法
fallbackMethod
,拒绝此次请求。
- 如果
- 从
- 允许请求:
- 如果成功获取到许可,则执行目标方法
pjp.proceed()
,允许此次请求通过。
- 如果成功获取到许可,则执行目标方法
1 | 4j |
辅助方法:
- fallbackMethodResult:通过反射调用用户配置的回调方法。当请求被限流或拦截后,执行此方法以返回预定义的响应。
1 | /** |
- getAttrValue:根据
key
从目标方法的参数中提取对应的值。支持通过反射获取对象属性值,适应不同参数类型。
1 |
|
- getValueByName 和 getFieldByName:通过反射获取对象的指定属性值,支持获取父类属性。
1 | /** |
1 |
|
实际使用
1 | "userId",fallbackMethod = "drawRateLimiterError",permitsPerSecond = 1.0d,blacklistCount = 1) (key = |
Redis版本
我们可以将缓存在Guava中的BlackList和loginRecord缓存在redis中,并模拟出RateLimiter的令牌桶策略:
使用Redis实现类似RateLimiter的令牌桶逻辑如下:
令牌桶的构建和更新
- 获取限流速率
permitsPerSecond
。 - 构建令牌桶的 Redis 键
bucketKey
。 - 从 Redis 获取对应的令牌桶状态
bucketMap
,包含tokens
和last_refill_time
。 - 如果
bucketMap
为null
或为空,初始化令牌数为1
,设置last_refill_time
为当前时间,并设置过期时间为1 小时
。 - 否则,从
bucketMap
中获取当前的storedTokens
和last_refill_time
。 - 计算自上次补充令牌以来经过的毫秒数
elapsedMs
。 - 根据限流速率计算新产生的令牌数
newTokens
。 - 将新令牌数添加到
storedTokens
,并限制令牌数不超过1
(令牌桶容量),这样不管中间间隔了多少时间,都不会令令牌累计。 - 更新
last_refill_time
为当前时间。
判断令牌是否足够
- 如果
storedTokens
不足(即<= 0
),表示请求超过了限流频率。 - 如果配置了黑名单阈值且
keyAttr
不是"all"
,则将keyAttr
的黑名单计数器递增1
。 - 如果是第一次违反限流(
newVal == 1
),设置黑名单键的过期时间为24 小时
。 - 更新令牌桶的状态(虽然
storedTokens
已经不够,但依然更新是为了保持数据一致性)。 - 记录日志并调用回调方法
fallbackMethod
,拦截请求。
扣减令牌并允许请求
- 如果有足够的令牌(
storedTokens > 0
),则扣减1
个令牌。说明这次是允许访问的 - 更新令牌桶的状态。
- 允许请求继续执行目标方法。
1 | "aopPoint() && @annotation(rateLimiterAccessInterceptor)") ( |