背景
限流是一种用于控制系统资源利用率或确保服务质量的策略。
在Web应用中,限流通常用于控制接口请求的频率,防止过多的请求导致系统负载过大或者防止恶意攻击。
此篇文章将详细介绍SpringBoot项目中使用自定义注解和切面优雅的实现固定窗口限流,滑动窗口限流,漏桶限流,令牌桶限流。
基本配置类
一、项目结构
1、pom文件配置
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
2、基础的类
(1)自定义注解
com.limit.rule.annotation.LimitRule
package com.limit.rule.annotation;
import com.limit.rule.enums.LimitRuleEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author shilei
*/
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LimitRule {
LimitRuleEnum rule();
String resource();
String limitCount() default "0";
String time() default "0";
String rate() default "100";
String capacity() default "9999999999";
String message() default "接口请求频繁,请稍后再试";
}
(2)自定义切面
com.limit.rule.aspect.LimitRuleAspect
package com.limit.rule.aspect;
import com.limit.rule.annotation.LimitRule;
import com.limit.rule.core.LimitService;
import com.limit.rule.enums.LimitRuleEnum;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.lang.reflect.Method;
import java.util.Map;
/**
* @author shilei
*/
@Aspect
@Slf4j
@Data
public class LimitRuleAspect {
@Autowired
private Map<String, LimitService> limitServiceMap;
@Autowired
private Map<String, DefaultRedisScript<Long>> scriptMap;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Before("@annotation(limitRule)")
public void limitRule(JoinPoint joinPoint, LimitRule limitRule) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
log.info("methodName:{}触发限流, 限流规则:{}", method.getName(), limitRule.rule().name());
LimitRuleEnum rule = limitRule.rule();
LimitService limitService = limitServiceMap.get(rule.getLimitBean());
DefaultRedisScript<Long> script = scriptMap.get(rule.getScriptName());
limitService.handle(limitRule, redisTemplate, script);
}
}
(3) 自定义异常
com.limit.rule.exception.LimitException
package com.limit.rule.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author shilei
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class LimitException extends RuntimeException {
private String code;
private String message;
public LimitException(String message) {
super();
this.message =message;
}
}
(4)自定义枚举
com.limit.rule.enums.LimitRuleEnum
package com.limit.rule.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author shilei
*/
@Getter
@AllArgsConstructor
public enum LimitRuleEnum {
/**
* 计数器算法
*/
COUNTER("counterLimitServiceImpl", "counterLimitScript"),
/**
* 滑动窗口算法
*/
ROLLING_WINDOW("rollingWindowLimitServiceImpl", "rollingWindowLimitScript"),
/**
* 漏桶算法
*/
LEAKY_BUCKET("leakyBucketLimitServiceImpl", "leakyBucketLimitScript"),
/**
* 令牌桶算法
*/
TOKEN_BUCKET("tokenBucketLimitServiceImpl", "tokenBucketLimitScript"),
;
private final String limitBean;
private final String scriptName;
}
3、在resources下定义lua脚本
(1)固定窗口限流:counterLimit.lua
--获取KEY
local key = KEYS[1]
-- 限流时间/缓存时间
local time = tonumber(ARGV[1])
redis.log(redis.LOG_NOTICE, time)
--当前请求数
local currentCount = redis.call('incr', key)
if (tonumber(currentCount) == 1) then
redis.call('expire', key, time)
end
return currentCount
(2)滑动窗口限流:rollingWindowLimit.lua
--获取KEY
local key = KEYS[1];
--获取ARGV内的参数
-- 缓存时间
local expire = tonumber(ARGV[1]);
-- 当前时间
local currentMs = tonumber(ARGV[2]);
-- 最大次数
local count = tonumber(ARGV[3]);
--窗口开始时间
local windowStartMs = currentMs - expire * 1000;
--获取key的次数
local current = redis.call('zcount', key, windowStartMs, currentMs);
--如果key的次数存在且大于预设值直接返回当前key的次数
if current and tonumber(current) > count then
return tonumber(current);
end
-- 清除上一个窗口的数据
redis.call("ZREMRANGEBYSCORE", key, 0, windowStartMs);
-- 添加当前成员
redis.call("zadd", key, tostring(currentMs), currentMs);
redis.call("expire", key, expire);
--返回key的次数
return tonumber(current)
(3)漏桶限流:leakyBucketLimit.lua
-- 限流器的键名
local key = KEYS[1]
-- 漏桶的容量
local capacity = tonumber(ARGV[1])
-- 漏桶的速率
local rate = tonumber(ARGV[2])
-- 当前时间戳
local current_time = tonumber(ARGV[3])
-- 获取漏桶中的水滴数量
local water = tonumber(redis.call('get', key) or "0")
-- 上次漏水的时间戳
local lastLeakTime = tonumber(redis.call('get', key .. ':last_leak_time') or current_time)
-- 计算当前时间与上次漏水的时间间隔
local elapsed = math.max(0, current_time - lastLeakTime)
-- 根据时间间隔计算漏水数量,并更新漏桶中的水滴数量
water = water - math.floor(rate*(elapsed/1000))
-- 水滴数量不会低于0
if water < 0 then
water = 0
end
-- 新的请求加入漏桶中
water = water + 1
if water > capacity then
-- 漏桶已满,拒绝请求
return 0
else
-- 接受请求,更新漏桶中的水滴数量
redis.call('set', key, water)
-- 更新上次漏水的时间戳
redis.call('set', key .. ':last_leak_time', current_time)
return 1
end
(4)令牌桶限流:tokenBucketLimit.lua
--获取key
local bucket_key = KEYS[1]
--每秒生成的令牌数
local rate = tonumber(ARGV[1])
-- 桶容量
local bucket_count = tonumber(ARGV[2])
--当前时间
local current_time = tonumber(ARGV[3])
-- 检查令牌桶的存在性,不存在则创建
if redis.call('exists', bucket_key) == 0 then
redis.call('hset', bucket_key, 'token_rate', rate)
redis.call('hset', bucket_key, 'token_count', bucket_count)
redis.call('hset', bucket_key, 'token_time', current_time)
end
-- 读取当前令牌桶状态
local token_rate = tonumber(redis.call('hget', bucket_key, "token_rate"))
local token_count = tonumber(redis.call('hget', bucket_key, "token_count"))
local token_time = tonumber(redis.call('hget', bucket_key, "token_time"))
-- 更新时间戳
if current_time > token_time then
local time_delta = current_time - token_time
local tokens_to_add = math.floor(time_delta * (token_rate / 1000))
bucket_count = math.min(token_count + tokens_to_add, bucket_count)
redis.call('hset', bucket_key, 'token_count', bucket_count)
redis.call('hset', bucket_key, 'token_time', current_time)
end
-- 判断请求令牌是否足够
if bucket_count < 1 then
-- 不足
return 0
else
bucket_count = bucket_count - 1
redis.call('hset', bucket_key, 'token_count', bucket_count)
-- 足够
return 1
end
4、定义处理限流的抽象接口
com.limit.rule.core.LimitService
package com.limit.rule.core;
import com.limit.rule.annotation.LimitRule;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author shilei
*/
public interface LimitService {
RedisSerializer redisSerializer = new StringRedisSerializer();
/**
* 限流处理
*
* @param limitRule @see LimitRule
* @param redisTemplate redisTemplate
* @param script lua脚本
*/
void handle(LimitRule limitRule, RedisTemplate<String, Object> redisTemplate , DefaultRedisScript<Long> script);
}
5、限流策略的实现
(1)固定窗口限流策略
com.limit.rule.core.CounterLimitServiceImpl
package com.limit.rule.core;
import com.limit.rule.annotation.LimitRule;
import com.limit.rule.exception.LimitException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* @author shilei
*/
@Slf4j
public class CounterLimitServiceImpl implements LimitService {
@Override
public void handle(LimitRule limitRule, RedisTemplate<String, Object> redisTemplate, DefaultRedisScript<Long> script) {
List<String> keys = Collections.singletonList(limitRule.resource());
Long increment = redisTemplate.execute(script, redisSerializer, redisSerializer, keys, limitRule.time());
if (Objects.nonNull(increment) && increment.intValue() > Integer.parseInt(limitRule.limitCount())) {
throw new LimitException(limitRule.message());
}
}
}
(2)滑动窗口限流策略
com.limit.rule.core.RollingWindowLimitServiceImpl
package com.limit.rule.core;
import com.limit.rule.annotation.LimitRule;
import com.limit.rule.exception.LimitException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.Objects;
/**
* @author shilei
*/
public class RollingWindowLimitServiceImpl implements LimitService{
@Override
public void handle(LimitRule limitRule, RedisTemplate<String, Object> redisTemplate, DefaultRedisScript<Long> script) {
Long count = redisTemplate.execute(script, redisSerializer, redisSerializer, Collections.singletonList(limitRule.resource()),
limitRule.time(), String.valueOf(System.currentTimeMillis()), limitRule.limitCount());
if (Objects.nonNull(count) && count.intValue() > Integer.parseInt(limitRule.limitCount())) {
throw new LimitException(limitRule.message());
}
}
}
(3)漏桶限流策略
com.limit.rule.core.LeakyBucketLimitServiceImpl
package com.limit.rule.core;
import com.limit.rule.annotation.LimitRule;
import com.limit.rule.exception.LimitException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.Objects;
/**
* @author shilei
*/
public class LeakyBucketLimitServiceImpl implements LimitService{
@Override
public void handle(LimitRule limitRule, RedisTemplate<String, Object> redisTemplate, DefaultRedisScript<Long> script) {
Long count = redisTemplate.execute(script, redisSerializer, redisSerializer, Collections.singletonList(limitRule.resource()),
limitRule.capacity(), limitRule.rate(), String.valueOf(System.currentTimeMillis()));
if (Objects.nonNull(count) && count.intValue() == 0) {
throw new LimitException(limitRule.message());
}
}
}
(4)令牌桶限流策略
com.limit.rule.core.TokenBucketLimitServiceImpl
package com.limit.rule.core;
import com.limit.rule.annotation.LimitRule;
import com.limit.rule.exception.LimitException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.Objects;
/**
* @author shilei
*/
public class TokenBucketLimitServiceImpl implements LimitService {
@Override
public void handle(LimitRule limitRule, RedisTemplate<String, Object> redisTemplate, DefaultRedisScript<Long> script) {
Long count = redisTemplate.execute(script, redisSerializer, redisSerializer, Collections.singletonList(limitRule.resource()),
limitRule.rate(), limitRule.capacity(), String.valueOf(System.currentTimeMillis()));
if (Objects.nonNull(count) && count.intValue() == 0) {
throw new LimitException(limitRule.message());
}
}
}
6、配置自动装配的类
(1)redis自动装配配置
com.limit.rule.config.RedisConfig
package com.limit.rule.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author shilei
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
(2) lua脚本自动装配
com.limit.rule.config.ScriptAutoConfig
package com.limit.rule.config;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
/**
* @author shilei
*/
@Configuration
@AutoConfigureBefore(RedisConfig.class)
public class ScriptAutoConfig {
@Bean
public DefaultRedisScript<Long> rollingWindowLimitScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setResultType(Long.class);
ClassPathResource classPathResource = new ClassPathResource("lua/rollingWindowLimit.lua");
script.setLocation(classPathResource);
return script;
}
@Bean
public DefaultRedisScript<Long> counterLimitScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setResultType(Long.class);
ClassPathResource classPathResource = new ClassPathResource("lua/counterLimit.lua");
script.setLocation(classPathResource);
return script;
}
@Bean
public DefaultRedisScript<Long> leakyBucketLimitScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setResultType(Long.class);
ClassPathResource classPathResource = new ClassPathResource("lua/leakyBucketLimit.lua");
script.setLocation(classPathResource);
return script;
}
@Bean
public DefaultRedisScript<Long> tokenBucketLimitScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setResultType(Long.class);
ClassPathResource classPathResource = new ClassPathResource("lua/tokenBucketLimit.lua");
script.setLocation(classPathResource);
return script;
}
}
(3)限流策略自动装配
com.limit.rule.config.LimitAutoConfig
package com.limit.rule.config;
import com.limit.rule.aspect.LimitRuleAspect;
import com.limit.rule.core.*;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author shilei
*/
@Configuration
@AutoConfigureBefore(ScriptAutoConfig.class)
public class LimitAutoConfig {
@Bean
public LimitService counterLimitServiceImpl() {
return new CounterLimitServiceImpl();
}
@Bean
public LimitService leakyBucketLimitServiceImpl() {
return new LeakyBucketLimitServiceImpl();
}
@Bean
public LimitService rollingWindowLimitServiceImpl() {
return new RollingWindowLimitServiceImpl();
}
@Bean
public LimitService tokenBucketLimitServiceImpl() {
return new TokenBucketLimitServiceImpl();
}
@Bean
public LimitRuleAspect limitRuleAspect() {
return new LimitRuleAspect();
}
}
(4)META-INF下配置需要自动装配的类
springBoot3.x版本META-INF下新建spring目录,然后在spring目录下新建org.springframework.boot.autoconfigure.AutoConfiguration.imports文件配置如下
com.limit.rule.config.RedisConfig
com.limit.rule.config.ScriptAutoConfig
com.limit.rule.config.LimitAutoConfig
springBoot2.x版本META-INF下新建spring.factories文件配置如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.limit.rule.config.RedisAutoConfig,\
com.limit.rule.config.ScriptAutoConfig,\
com.limit.rule.config.LimitAutoConfig
7、编写测试接口
package com.client.utils.controller.limit;
import com.client.utils.vo.Result;
import com.limit.rule.annotation.LimitRule;
import com.limit.rule.enums.LimitRuleEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* @author shilei
*/
@RestController
@RequestMapping("/limit/rule")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "限流验证", description = "限流验证")
public class LimitRuleController {
@GetMapping("/counter")
@Operation(summary = "固定窗口限流测试")
@LimitRule(resource = "counter", rule = LimitRuleEnum.COUNTER, limitCount = "2", time = "10", message = "错误")
public Result<Void> counter() {
return Result.success();
}
@GetMapping("/rolling/window")
@Operation(summary = "滑动窗口限流测试")
@LimitRule(resource = "rollingWindow", rule = LimitRuleEnum.ROLLING_WINDOW, limitCount = "2", time = "10")
public Result<Void> rollingWindow() {
return Result.success();
}
@GetMapping("/leaky/bucket")
@Operation(summary = "漏桶限流测试")
@LimitRule(resource = "leakyBucket", rule = LimitRuleEnum.LEAKY_BUCKET, capacity = "10", rate = "2")
public Result<Void> leakyBucket() {
return Result.success();
}
@GetMapping("/token/bucket")
@Operation(summary = "令牌桶限流测试")
@LimitRule(resource = "tokenBucket", rule = LimitRuleEnum.TOKEN_BUCKET, capacity = "10", rate = "1")
public Result<Void> tokenBucket() {
return Result.success();
}
}