场景。
1) Spring MVC下Controller是多线程的,所有调用controller方法之前会进入切面。
@Around("within(@org.springframework.stereotype.Controller *) && @annotation(limit)")
2)获得请求的真实ip,(nginx代理之前的ip)
public class NginxUtils {
public static String getRealIp(HttpServletRequest request){
String ip = request.getHeader("X-Forwarded-For");
if(ip == null || ip.equals("") || "unknown".equalsIgnoreCase(ip)){
ip = request.getHeader("X-Real-IP");
}
if(ip == null || ip.equals("") || "unknown".equalsIgnoreCase(ip)){
ip = request.getHeader("Proxy-Client-IP");
}
if(ip == null || ip.equals("") || "unknown".equalsIgnoreCase(ip)){
ip = request.getRemoteAddr();
}
return ip;
}
}
3)构造redis中需要存的key。
String ip = NginxUtils.getRealIp(request);
String url = request.getRequestURI();
String key = "req_limit_" + url + "_" + ip;
BoundValueOperations<String, String> ops = stringRedisTemplate.boundValueOps(key);
String countString = ops.get();
if(countString == null || countString.equals("")){
logger.info("ip:" + ip + ", first request within 1s");
ops.set("0", 1000, TimeUnit.MILLISECONDS);
}
long count = Long.valueOf(ops.get());`这里可能会为null, 因为多线程导致有请求执行到这里时,刚好key的超时时间到了`
if(count > 10){
logger.info("url:\t" + url + ", ip:\t" + ip + " limit request");
map.put("success", false);
map.put("message", "the ip: " + ip + " is up to the limit " + 10 + " within " + "1s");
return map;
}
ops.increment(1);`执行incr,或创建一个key, 并且增加1, 此时ttl为 -1`
4)解决方案, 在incr之前,判断是否过期了,过期则重新设置ttl。
Long milSeconds = stringRedisTemplate.getExpire(key, TimeUnit.MILLISECONDS);
if(milSeconds == null || milSeconds <= 0){
ops.set("0", 1000, TimeUnit.MILLISECONDS);
}
ops.increment(1);```