使用springboot集成redis实现一个简单的限流功能。
实现简单的限流可以通过自定义注解来实现,限流可以分为不同的策略,如针对接口的全局性限流、针对ip的限流,限制1分钟内访问的次数。
实例
限流方式的枚举类
public enum LimitType {
/**
* 默认策略全局限流
*/
DEFAULT,
/**
* 根据请求IP进行限流
*/
IP
}
限流注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
/**
* 限流key前缀
*/
String keyPrefix() default "rate_limiter:";
/**
* 限流时间,单位秒
*/
int time() default 60;
/**
* 限流次数
*/
int count() default 100;
/**
* 限流类型
*/
LimitType limitType() default LimitType.DEFAULT;
}
RedisTemplate类,在 Spring Boot 中,默认的 RedisTemplate 有一个小坑,就是序列化用的是 JdkSerializationRedisSerializer,直接用这个序列化工具将来存到 Redis 上的 key 和 value 都会莫名其妙多一些前缀,这就导致你用命令读取的时候可能会出错,此时只能继续使用 RedisTemplate 将之读取出来。
用redis实现限流会用到lua脚本,使用lua脚本的时候就会出现上面的问题,所有需要修改RedisTempplate的序列化方案。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate
其中key、value都使用了jackson序列化方式来解决。redis中的一些原子操作可以借助lua脚本来实现,可以在resources目录下新建lua目录来存放lua脚本文件,内容如下
local key = KEYS[1]
local count = tonumber(ARGV[1])
local time = tonumber(ARGV[2])
local current = redis.call('get', key)
if current and tonumber(current) > count then
return tonumber(current)
end
current = redis.call('incr', key)
if tonumber(current) == 1 then
redis.call('expire', key, time)
end
return tonumber(current)
其中redis.call 就是执行具体的 redis 指令。具体执行流程如下:
限流切面
@Aspect
@Component
public class RateLimiterAspect {
private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedisScript redisScript;
@Before("@annotation(rateLimiter)")
public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
String keyPrefix = rateLimiter.keyPrefix();
int time = rateLimiter.time();
int count = rateLimiter.count();
String redisKey = getRedisKey(rateLimiter, point);
List keys = Collections.singletonList(redisKey);
// try {
Long number = redisTemplate.execute(redisScript, keys, count, time);
if (number == null || number.intValue() > count) {
throw new ServiceException("访问过于频繁,请稍候再试");
}
log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), keyPrefix);
// } catch (Exception e) {
// throw new RuntimeException("服务器限流异常,请稍候再试");
// }
}
public String getRedisKey(RateLimiter rateLimiter, JoinPoint point) {
StringBuffer stringBuffer = new StringBuffer(rateLimiter.keyPrefix());
if (rateLimiter.limitType() == LimitType.IP) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
stringBuffer.append(IPUtil.getIpAddress(request)).append("-");
}
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class> targetClass = method.getDeclaringClass();
stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
return stringBuffer.toString();
}
}
拦截了所有加了@Ratelimiter注解的方法,在前置通知中对注解进行处理。
全局异常处理类
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ServiceException.class)
public Map globalException(ServiceException e) {
HashMap map = new HashMap<>();
map.put("status", 500);
map.put("message", e.getMessage());
return map;
}
@ExceptionHandler(value = Exception.class)
public Map jsonCommonErrorHandler(HttpServletRequest req, Exception e) {
HashMap map = new HashMap<>();
map.put("status", 500);
map.put("message", e.getMessage());
return map;
}
}
测试
@GetMapping("/hello")
@RateLimiter(time = 5, count = 3, limitType = LimitType.IP)
public String hello() {
return "hello";
}
@GetMapping("/hello2")
@RateLimiter(time = 5, count = 3, limitType = LimitType.DEFAULT)
public String hello2() {
return "hello2";
}