限流算法
一. sentx + incr
谈起限流算法,记得之前做过一个限制用户获取手机验证码次数的需求,同一个手机号24小时最多只能获取20次手机验证码,防止接口被盗刷。当时使用当前时间(yyyy-MM-dd) + 当前用户唯一标识作为 key,使用setnx + incr实现,当value的值达到20,拒绝发送短信。时间思路和代码都比较简单,不在此赘述了。
这种方案有一个致命的缺陷,如果限制间隔10分钟用户只能请求5次,假设再1-10 分钟请求9次,10-11分钟请求9次,那么此用户在1-11分钟请求了18次,与需求相违背。不可取。
二. zset 滑动窗口
redis提供了sorted set 数据结构,即zset,zadd key score member ,它支持按照score对set中的元素排序,那么我们可以将当前请求的请求时间时间戳作为score,使用uuid作为value(member), 那么我们要统计某个时间段内的访问书数据个数就易如反掌。
@Target({ElementType.METHOD, ElementType.TYPE}) // 可以考虑应用到类上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LimitFlow {
/**
* 限流间隔
* @return
*/
int interval() default 60 * 1000;
/**
* 在interval内可以执行的次数
* @return
*/
int limit() default 10;
}
@Aspect
@Component
public class LimitFlowAspect {
@Pointcut("@annotation(com.cph.aspect.LimitFlow)")
private void pointCutMethodController() {
}
/**
* 环绕通知前后增强
*/
@Around(value = "pointCutMethodController()")
public Object doAroundService(ProceedingJoinPoint joinPoint) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
LimitFlow limitFlow = method.getAnnotation(LimitFlow.class);
if (limitFlow != null) {
int interval = limitFlow.interval();
int limit = limitFlow.limit();
if(!RedisUtils.limitFlow("limit", limit, interval)){
return new CommonResult(500, "操作频繁,请稍后再试", null, null);
}
}
Object result = joinPoint.proceed();
return result;
}
}
/**
* 限流
* @param key
* @param limit
* @param interval
*/
public static Boolean limitFlow(String key, int limit, int interval){
Long currentTime = new Date().getTime();
if (redisTemplate.hasKey(key)) {
Integer count = redisTemplate.opsForZSet().rangeByScore("limit", currentTime - interval, currentTime).size();
if( count > limit) return false;
}
redisTemplate.opsForZSet().add(key, UUID.randomUUID().toString(), currentTime);
return true;
}
滑动窗口解决了 一. sentx + incr 某个时间段内的访问数据的问题。
三. 令牌桶算法
每个请求执行之前都需要从桶中获取令牌,只有获取到令牌的请求才可以执行,否则拒绝执行。使用redis中的list实现,启动一个定时任务定期向令牌桶放入令牌,桶满了则丢弃。当请求来的时候获取令牌,当令牌桶为空则证明无法获取令牌,拒绝执行。
public Response limitFlow2(Long id){
Object result = redisTemplate.opsForList().leftPop("limit_list");
if(result == null){
return Response.ok("当前令牌桶中无令牌");
}
return Response.ok(articleDescription2);
}
@Scheduled(fixedDelay = 10_000,initialDelay = 0)
public void setIntervalTimeTask(){
redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString());
}
四. 漏斗算法
漏斗算法类似于将请求装在一个漏斗中,按照固定速率进行消费,漏斗满了之后溢出的请求拒绝服务。
五. Redission
redission 的 RRateLimiter 使用 lua脚本 + 令牌桶算法实现。
RedissonClient redissonClient = Redisson.create();
RRateLimiter rateLimiter = redissonClient.getRateLimiter("xindoo.limiter");
rateLimiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.HOURS);
rateLimiter.acquire(1); // 申请1份许可,直到成功
boolean res = rateLimiter.tryAcquire(1, 5, TimeUnit.SECONDS); // 申请1份许可,如果5s内未申请到就放弃