如何正确的姿态实现限流特技?
为什么需要实现限流?
由于业务应用系统的负载能力有限,为了防止非预期的请求对系统压力过大而拖垮业务应用系统,必须采取流量控制措施。
扩展:如果需求是如何实现控制用户访问次数,比如说限时秒杀,防止同一个用户在指定的时间内操作次数过多,是否可以使用同样的方式呢?
话不多说,直接上代码,结合代码会更加的生动形象。
【代码实现方式: 自定义注解 + 拦截器 + redis结合lua 实现高效限流】
第一步:
自定义限流的注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExtLimit {
/**限制次数*/
int limitNum() default 0;
/**过期时间*/
int expireTime() default 0;
}
上面两个参数联系起来就是在指定的时间内,能访问的次数是多少,一旦超过次数,不好意思,你需要等等了。
第二步:
注解定义好了,我们该如何使用这个注解,我们那就定义一个拦截器吧,检测到是这个注解ExtLimit 的,我们就把你拦下来,验证你一番再说。
public class ExtAnnotationInterceptor extends BaseResponse implements HandlerInterceptor{
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
ConfigurationUtil configurationUtil;
@Autowired
ErrorCodeMsgUtil errorCodeMsgUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 判断接口是否有注解ExtLimit
ExtLimit methodAnnotation = method.getAnnotation(ExtLimit.class);
extLimitAnnotationHandler(response, method, methodAnnotation);
return true;
}
//注解@ExtLimit处理
public boolean extLimitAnnotationHandler(HttpServletResponse response,Method method,ExtLimit methodAnnotation) throws Exception{
if(methodAnnotation == null) {
return true;
}
//2. 获取限流参数
int limitNum = methodAnnotation.limitNum();
int expireTime = methodAnnotation.expireTime();
if(limitNum == 0 || expireTime == 0){
return true;
}
//3. 调用redis lua脚本实现限流
String executeStr = stringRedisTemplate.execute(RedisLuaScriptUtil.LIMIT_REDIS_SCRIPT, Collections.singletonList(configurationUtil.getServerName()+":"+method.getName()),String.valueOf(limitNum),String.valueOf(expireTime));
//4.在指定的时间内超过限制次数返回结果
if(Constants.FALSE.equals(executeStr)){
//表示直接响应一个json格式的信息
ResponseUtil.sendJsonMessage(response, setResultFail(errorCodeMsgUtil.getLimitErrorCode(),errorCodeMsgUtil.getLimitErrorMsg()));
return false;
}
return true;
}
}
注释【3. 调用redis lua脚本实现限流】,是redis结合lua实现的一个在指定时间内的计数RedisLuaScriptUtil是个工具类,如下:
public class RedisLuaScriptUtil {
/**对接口做限流操作,采用redis+lua,时间单位:s*/
public static final RedisScript<String> LIMIT_REDIS_SCRIPT = new DefaultRedisScript<String>(
"local key = KEYS[1] "
+ "local limit = ARGV[1] "
+ "local expireTime = ARGV[2] "
+ "if redis.call('EXISTS',key) == 1 then "
+ "if tonumber(redis.call('GET',key)) >= tonumber(limit) then "
+ "return 'false'"
+ "else "
+ "redis.call('INCRBY',key,'1') "
+ "return 'true'"
+ "end "
+"else "
+ "redis.call('SET',key,'1','EX',expireTime) "
+ "return 'true'"
+"end "
,String.class);
/**对接口幂等性做限制,由单独的token服务生成是分布式全局id*/
public static final RedisScript<String> IDEMPOTENCY_REDIS_SCRIPT = new DefaultRedisScript<String>(
"local IdempotencyToken = KEYS[1] "
+ "if redis.call('EXISTS',IdempotencyToken) == 1 then "
+ "redis.call('DEL',IdempotencyToken) "
+ "return 'true' "
+ "else "
+ "return 'false'"
+ "end ",
String.class);
/**分布式加锁*/
public static final RedisScript<String> DISTRIBUTED_LOCK_SCRIPT = new DefaultRedisScript<String>(
"local distributedKey = KEYS[1] "
+ "local distributedValue = ARGV[1] "
+ "local expireTime = ARGV[2] "
+ "if redis.call('SETNX',distributedKey,distributedValue) == 1 then "
+ "redis.call('EXPIRE',distributedKey,expireTime) "
+ "return 'true'"
+ "else "
+ "return 'false' "
+ "end",
String.class);
/**分布式释放锁*/
public static final RedisScript<String> DISTRIBUTED_UNLOCK_SCRIPT = new DefaultRedisScript<String>(
"local distributedKey = KEYS[1] "
+ "local distributedValue = ARGV[1] "
+ "if redis.call('GET',distributedKey) == distributedValue then "
+ "redis.call('DEL',distributedKey) "
+ "return 'true'"
+ "else "
+ "return 'false'"
+ "end ",
String.class);
}
以上,是整个实现限流的流程,接下来简单的说下扩展:
扩展:如果需求是如何实现控制用户访问次数,比如说限时秒杀,防止同一个用户在指定的实现操作次数过多,是否可以使用同样的方式呢?
如果防止客户端频繁的操作,我们仅仅只需要将上面一段代码中的Collections.singletonList(configurationUtil.getServerName()+":"+method.getName()):
String executeStr = stringRedisTemplate.execute(RedisLuaScriptUtil.LIMIT_REDIS_SCRIPT, Collections.singletonList(configurationUtil.getServerName()+":"+method.getName()),String.valueOf(limitNum),String.valueOf(expireTime));
只需要method.getName() 换成客户端ip即可,这样是不是简单的控制了一个ip来源的访问次数,其实还有很多的解决方案,大家自行扩展即可。