Java开发中,高并发一直都是我们比较关注的,网上针对高并发有各种各样的应对方案,有加缓存、服务降级、接口限流等等等,今天我们主要讲接口限流方面的东西。
接口限流其实主要就是分布式限流,分布式限流最关键的是讲限流服务做成原子化,而解决方案可以使用redis+lua或者nginx+lua。
今天我主要讲的是redis+lua进行限流:
首先在项目中自定义了一个Limit的注解,当接口加上这个@Limit注解时,代表该接口需要限流,然后使用了AOP切加了该注解的接口,在AOP方法中使用redis+lua做接口限流
Limit注解:
@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;
}
切面类以及Lua脚本,都写在同一个方法里了
@Aspect
@Component
public class LimitAspect {
private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class);
private final RedisTemplate<String, Serializable> limitRedisTemplate;
@Autowired
public LimitAspect(RedisTemplate<String, Serializable> limitRedisTemplate) {
this.limitRedisTemplate = limitRedisTemplate;
}
@Pointcut("@annotation(cc.mrbird.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;
int limitPeriod = limitAnnotation.period();
int limitCount = limitAnnotation.count();
switch (limitType) {
case IP:
key = IPUtils.getIpAddr(request);
break;
case CUSTOMER:
key = limitAnnotation.key();
break;
default:
key = StringUtils.upperCase(method.getName());
}
ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix() + "_", key + "_" + request.getRequestedSessionId()));
String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
logger.info("第{}次访问key为 {},描述为 [{}] 的接口", count, keys, name);
if (count != null && count.intValue() <= limitCount) {
return point.proceed();
} else {
throw new LimitAccessException("接口访问超出频率限制");
}
}
/**
* 限流脚本
* 调用的时候不超过阈值,则直接返回并执行计算器自加。
*
* @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;";
}
}
测试接口:
@RestController
public class TestController {
private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger();
/**
* 测试限流注解,下面配置说明该接口 60秒内最多只能访问 10次,保存到redis的键名为 limit_test,
* 即 prefix + "_" + key,也可以根据 IP 来限流,需指定limitType = LimitType.IP
*/
@Limit(key = "test", period = 60, count = 10, name = "resource", prefix = "limit")
@GetMapping("/test")
public int testLimiter() {
return ATOMIC_INTEGER.incrementAndGet();
}
}
测试结果:
当第十一次请求过来以后,提示接口访问超出频率限制。这个只是限制,其实可以将超出的请求都放入到队列中去,然后等已经来的接口处理完请求以后再让队列中的请求再次请求接口