之前接触redis应用有业务加锁,数据库缓存,分布式锁,今天介绍的是用redis限流。
实现逻辑:通过AOP实现,在方法上加注解。
主要参考文章:https://blog.csdn.net/a309220728/article/details/84937630
不足之处是,自定义注解中,访问时间是固定的一分钟,实际入参只有访问次数。其实可以把时间也作为入参,实现更加灵活的配置。
代码:
自定义注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Order(Ordered.HIGHEST_PRECEDENCE)
public @interface RequestLimit {
/**
* 允许访问的次数,默认值MAX_VALUE
*/
int count() default Integer.MAX_VALUE;
/**
* 时间段,单位为毫秒,默认值一分钟
*/
long time() default 60000;
}
AOP切面
@Component
@Aspect
public class RequestLimitAop {
private Logger log = LogManager.getLogger(RequestLimitAop.class);
public RequestLimitAop() {
// TODO Auto-generated constructor stub
}
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Before("within(com.anji.allways..*) && @annotation(limit)")
public void requestLimit(JoinPoint joinPoint, RequestLimit limit) throws ServiceException {
try {
Object[] args = joinPoint.getArgs();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String ip = getIpAddress(request);
String url = request.getRequestURL().toString();
String key = "req_limit_".concat(url).concat("_").concat(ip);
boolean checkResult = checkWithRedis(limit, key);
if (!checkResult) {
log.debug("requestLimited," + "[用户ip:{}],[访问地址:{}]超过了限定的次数[{}]次", ip, url, limit.count());
throw new ServiceException("请求过于频繁,超出限制!");
}
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
log.error("RequestLimitAop.requestLimit has some exceptions: ", e);
}
}
@AfterThrowing(pointcut = "within(com.anji.allways..*) && @annotation(loggerManage)", throwing = "ex")
public void addAfterThrowingLogger(JoinPoint joinPoint, LoggerManage loggerManage, Exception ex) {
log.error("执行 " + loggerManage.description() + " 异常", ex);
log.error("发现异常!方法:" + joinPoint.getSignature().getName() + "--->异常", ex);
// 这里输入友好性信息
if (!StringUtils.isEmpty(ex.getMessage())) {
log.error("异常", ex.getMessage());
writeContent("500", ex.getMessage());
} else {
writeContent("500", "十分抱歉,出现异常!程序猿小哥正在紧急抢修...");
}
}
/**
* 以redis实现请求记录
* @param limit
* @param key
* @return
*/
private boolean checkWithRedis(RequestLimit limit, String key) {
long count = stringRedisTemplate.opsForValue().increment(key, 1);
if (count == 1) {
stringRedisTemplate.expire(key, limit.time(), TimeUnit.MILLISECONDS);
}
if (count > limit.count()) {
return false;
}
return true;
}
/**
* 获取用户真实IP地址,不使用request.getRemoteAddr()的原因是有可能用户使用了代理软件方式避免真实IP地址, 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值
* @return ip
*/
private String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
log.info("x-forwarded-for ip: " + ip);
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if (ip.indexOf(",") != -1) {
ip = ip.split(",")[0];
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
log.info("Proxy-Client-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
log.info("WL-Proxy-Client-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
log.info("HTTP_CLIENT_IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
log.info("HTTP_X_FORWARDED_FOR ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
log.info("X-Real-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
log.info("getRemoteAddr ip: " + ip);
}
log.info("获取客户端ip: " + ip);
return ip;
}
/**
* 将内容输出到浏览器
* @param content
* 输出内容
*/
public static void writeContent(String code, String content) {
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "text/json;charset=UTF-8");
response.setHeader("icop-content-type", "exception");
PrintWriter writer = null;
JsonGenerator jsonGenerator = null;
try {
writer = response.getWriter();
jsonGenerator = (new ObjectMapper()).getFactory().createGenerator(writer);
jsonGenerator.writeObject(new BaseResponseModel(code, content));
} catch (IOException e1) {
e1.printStackTrace();
} finally {
writer.flush();
writer.close();
}
}
}
添加注解
@RequestMapping("/findItemPro")
@ResponseBody
@RequestLimit(count = 2)
@LoggerManage(description="查询零件属性")
public BaseResponseModel<Object> findItemPro(@RequestBody BaseRequestModel request) throws Exception {
BaseResponseModel<Object> response = new BaseResponseModel<Object>();
try {
return itemPropertyService.findItemProperty(request);
} catch (Exception e) {
e.printStackTrace();
response.setRepCode(RespCode.USER_QUERYNAME_ERROR);
response.setRepMsg(RespMsg.USER_QUERYNAME_ERROR_MSG);
}
return response;
}