为什么需要限流
在高并发环境下,为了缓解数据库,服务器的压力,往往需要对一些接口进行限制操作。比如某个接口10s
内只能调用5
次,需要怎么做呢?
这里我有一个思路,利用Redis
的incr
命令,每次调用接口,原子自增 1
,当自增的值大于设定的次数时,就不让调用接口(返回接口调用次数已经用完之类的)。
Lua脚本
Lua
脚本的多个命令操作可以实现原子性,下面为相关的Lua
脚本命令。
--获取key
local key1 = KEYS[1]
--原子自增操作
local val = redis.call('incr',key1)
--返回key的剩余生存时间
local ttl = redis.call('ttl',key1)
--获取java代码中的参数
--(Long) redisTemplate.execute(getRedisScript, keyList, expireTimes, limitTimes);
local expire = ARGV[1]
local times = ARGV[2]
--日志打印
redis.log(redis.LOG_DEBUG,tostring(times))
redis.log(redis.LOG_DEBUG,tostring(expire))
redis.log(redis.LOG_NOTICE, "incr "..key1.." "..val);
--给指定的key设置生存时间
if val == 1 then
redis.call('expire',key1,tonumber(expire))
else
if ttl == -1 then
redis.call('expire', key1, tonumber(expire))
end
end
--超过限制次数,返回false
if val > tonumber(times) then
return 0
end
return 1
限流注解
代码语言:javascript复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
/**
* 限流key
* @return
*/
String key() default "rate:limiter";
/**
* 单位时间限制通过的请求数
* @return
*/
long limit() default 10;
/**
* 过期时间,单位s
* @return
*/
long expire() default 1;
/**
* 提示
* @return
*/
String message() default "false";
}
AOP编程
代码语言:javascript复制@Aspect
@Component
@Slf4j
public class RateLimiterHandler {
@Autowired
private RedisTemplate redisTemplate;
private DefaultRedisScript<Long> getRedisScript;
@PostConstruct
public void init() {
getRedisScript = new DefaultRedisScript<>();
getRedisScript.setResultType(Long.class);
getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
log.info(">>>>>>>>>>>>>>>>RateLimiterHandler[分布式限流处理器] lua脚本加载完成");
}
@Pointcut("@annotation(com.lvshen.demo.redis.ratelimiter.RateLimiter)")
public void rateLimiter() {}
@Around("rateLimiter()")
public Object around(ProceedingJoinPoint point) throws Throwable {
log.info(">>>>>>>>>>>>>>>>RateLimiterHandler[分布式限流处理器]开始执行限流操作");
/*Signature signature = point.getSignature();
if (!(signature instanceof MethodSignature)) {
throw new IllegalArgumentException("the Annotation @RateLimiter must used on method!");
}*/
MethodSignature signature = (MethodSignature) point.getSignature();
/**
* 获取注解参数
*/
Method method = point.getTarget().getClass().getMethod(signature.getName(), signature.getMethod().getParameterTypes());
RateLimiter rateLimiter = method.getAnnotation(RateLimiter.class);
String limitKey = rateLimiter.key();
Preconditions.checkNotNull(limitKey);
long limitTimes = rateLimiter.limit();
long expireTimes = rateLimiter.expire();
log.info(">>>>>>>>>>>>>RateLimiterHandler[分布式限流处理器]参数值为-limitTimes={},limitTimeout={}", limitTimes, expireTimes);
String message = rateLimiter.message();
if (StringUtils.isBlank(message)) {
message = "false";
}
//执行lua脚本
List<String> keyList = Lists.newArrayList();
keyList.add(limitKey);
Long result = (Long) redisTemplate.execute(getRedisScript, keyList, expireTimes, limitTimes);
StringBuffer sb = new StringBuffer();
if ((result == 0)) {
String msg = sb.append("超过单位时间=").append(expireTimes).append("允许的请求次数=").append(limitTimes).append("[触发限流]").toString();
log.info(msg);
return message;
}
//执行原方法
return point.proceed();
}
}
测试Controller
代码语言:javascript复制@RestController
@Slf4j
public class TestLimiterController {
private static final String MESSAGE = "{"code":"400","msg":"FAIL","desc":"触发限流"}";
AtomicInteger atomicInteger = new AtomicInteger();
/**
* 10s限制请求5次
* @param request
* @return
* @throws Exception
*/
@ResponseBody
@RequestMapping("ratelimiter")
@RateLimiter(key = "ratedemo:1", limit = 5, expire = 10, message = MESSAGE)
public String test(HttpServletRequest request) throws Exception {
int count = atomicInteger.incrementAndGet();
return "正常请求" count;
}
}
测试结果
用postman
测试
请求第一次
…
请求第五次
当请求第六次时,出发限流
这只是简单的限流设计,实际使用可能还需要考虑好很多方面。不过现在Redis4.0
提供了一个Redis
限流模块,Redis-Cell
,该模块使用了漏斗算法
。并提供了原子的限流指令。有兴趣的可以自行研究下。