用法:在需要限流的接口controller方法上添加@Limit@Limit(key = "login", period = 60, count = 20, name = "登录接口", prefix = "limit")
在需要限流的接口controller方法上添加@Limit
@Limit(key = "login", period = 60, count = 20, name = "登录接口", prefix = "limit")
原理:使用自定义注解,在切面中获取限流器的信息,配合redis的lua脚本,实现如果在规定时间内的请求次数超过限制,就不继续执行controller方法
使用自定义注解,在切面中获取限流器的信息,配合redis的lua脚本,实现如果在规定时间内的请求次数超过限制,就不继续执行controller方法
详细实现:
注解:package com.jswdwsx.common.annotation;
import com.jswdwsx.common.enums.LimitType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {
// 资源名称,用于描述接口功能
String name() default "";
// 资源 key
String key() default "";
// key prefix
String prefix() default "";
// 时间的,单位秒
int period();
// 限制访问次数
int count();
// 限制类型
LimitType limitType() default LimitType.CUSTOMER;
}
package com.jswdwsx.common.enums;
public enum LimitType {
// 传统类型
CUSTOMER,
// 根据 IP 限制
IP;
}
package com.jswdwsx.common.annotation; import com.jswdwsx.common.enums.LimitType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Limit { // 资源名称,用于描述接口功能 String name() default ""; // 资源 key String key() default ""; // key prefix String prefix() default ""; // 时间的,单位秒 int period(); // 限制访问次数 int count(); // 限制类型 LimitType limitType() default LimitType.CUSTOMER; }
package com.jswdwsx.common.enums; public enum LimitType { // 传统类型 CUSTOMER, // 根据 IP 限制 IP; }
切面:/**
* 接口限流
*/
@Slf4j
@Aspect
@Component
public class LimitAspect {
private final RedisTemplate<String, Serializable> limitRedisTemplate;
@Autowired
public LimitAspect(RedisTemplate<String, Serializable> limitRedisTemplate) {
this.limitRedisTemplate = limitRedisTemplate;
}
@Pointcut("@annotation(com.jswdwsx.common.annotation.Limit)")
public void pointcut() {
// do nothing
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Limit limitAnnotation = method.getAnnotation(Limit.class);
LimitType limitType = limitAnnotation.limitType();
String name = limitAnnotation.name();
String key;
String ip = IPUtil.getIpAddr(request);
int limitPeriod = limitAnnotation.period();
int limitCount = limitAnnotation.count();
switch (limitType) {
case IP:
key = ip;
break;
case CUSTOMER:
key = limitAnnotation.key();
break;
default:
key = StringUtils.upperCase(method.getName());
}
ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix() + "_", key, ip));
String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
log.info("IP:{} 第 {} 次访问key为 {},描述为 [{}] 的接口", ip, count, keys, name);
if (count != null && count.intValue() <= limitCount) {
return point.proceed();
} else {
throw new LimitAccessException("接口访问超出频率限制,请在" + limitPeriod + "秒后重试");
}
}
/**
* 限流脚本
* 调用的时候不超过阈值,则直接返回并执行计算器自加。
*
* @return lua脚本
*/
private String buildLuaScript() {
return "local c" +
"\nc = redis.call('get',KEYS[1])" +
"\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
"\nreturn c;" +
"\nend" +
"\nc = redis.call('incr',KEYS[1])" +
"\nif tonumber(c) == 1 then" +
"\nredis.call('expire',KEYS[1],ARGV[2])" +
"\nend" +
"\nreturn c;";
}
}
/** * 接口限流 */ @Slf4j @Aspect @Component public class LimitAspect { private final RedisTemplate<String, Serializable> limitRedisTemplate; @Autowired public LimitAspect(RedisTemplate<String, Serializable> limitRedisTemplate) { this.limitRedisTemplate = limitRedisTemplate; } @Pointcut("@annotation(com.jswdwsx.common.annotation.Limit)") public void pointcut() { // do nothing } @Around("pointcut()") public Object around(ProceedingJoinPoint point) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); Limit limitAnnotation = method.getAnnotation(Limit.class); LimitType limitType = limitAnnotation.limitType(); String name = limitAnnotation.name(); String key; String ip = IPUtil.getIpAddr(request); int limitPeriod = limitAnnotation.period(); int limitCount = limitAnnotation.count(); switch (limitType) { case IP: key = ip; break; case CUSTOMER: key = limitAnnotation.key(); break; default: key = StringUtils.upperCase(method.getName()); } ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix() + "_", key, ip)); String luaScript = buildLuaScript(); RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class); Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod); log.info("IP:{} 第 {} 次访问key为 {},描述为 [{}] 的接口", ip, count, keys, name); if (count != null && count.intValue() <= limitCount) { return point.proceed(); } else { throw new LimitAccessException("接口访问超出频率限制,请在" + limitPeriod + "秒后重试"); } } /** * 限流脚本 * 调用的时候不超过阈值,则直接返回并执行计算器自加。 * * @return lua脚本 */ private String buildLuaScript() { return "local c" + "\nc = redis.call('get',KEYS[1])" + "\nif c and tonumber(c) > tonumber(ARGV[1]) then" + "\nreturn c;" + "\nend" + "\nc = redis.call('incr',KEYS[1])" + "\nif tonumber(c) == 1 then" + "\nredis.call('expire',KEYS[1],ARGV[2])" + "\nend" + "\nreturn c;"; } }