基于AOP和Redis实现对接口调用情况的监控及IP限流
需求描述
- 项目中有许多接口,现在我们需要实现一个功能对接口调用情况进行统计,主要功能如下:
- 需求一:实现对每个接口,每天的调用次数做记录;
- 需求二:如果某次调用抛出了异常信息,则记录下异常信息;
- 需求三:限流,限制单个IP一天内对一个接口的调用次数。
概要设计
因为需要对每个接口的调用情况进行统计,所以选择AOP来实现,将Controller层抽象为一个切面
@Before 执行业务操作前进行限流判断;
@AfterReturn 如果正常返回则调用次数加1;
@AfterThrowing 如果抛出异常则记录异常信息。
如果将这些信息写入数据库的话会对每个接口带来额外的操作数据库的开销,影响接口响应时间,且此类记录信息较多,所以此处选择Redis将这些信息缓存下来。
Redis设计
- 对于需求一,我们需要记录三个信息:1、调用的接口名;2、调用的日期(精确到天);3、调用次数。所以此处Redis的key使用Hash结构,数据结构如下:key = 接口URI、key = 调用日期(到天)、value = 调用次数(初始值为1,没一次调用后自增1)。
- 对于需求二,需要记录的信息有:1、调用的接口名;2、异常发生时间(精确到毫秒);3、异常信息。因为需求一的key已经设置成了接口URI,所以此处选择使用URI + 后缀“_exception”的形式来代表异常信息的key。所以此需求Redis的数据结构设计如下(仍然使用Hash结构):key = URI + “_exception”、key = 异常发生时间(精确到毫秒)、value = 异常信息。
- 对于需求三,我们需要记录的信息有:1、调用的接口名;2、ip地址;3、调用时间;4、调用次数。此需求需要记录的信息较多,但是我们可以将信息1、信息2、信息3组合起来拼接成一个唯一的key即可,将调用时间的维度精确到天且设置key的过期时间为一天,这样的一个key即可代表单个IP一天时间内访问了哪些接口。所以Redis的数据结构设计如下:key = URI + ip +date(精确到天)、value = 调用次数。
代码实现
/**
* 接口调用情况监控
* 1、监控单个接口一天内的调用次数
* 2、如果抛出异常,则记录异常信息及发生时间
* 3、对单个IP进行限流,每天对每个接口的调用次数有限
*
* @author csh
* @date 2019/10/30
*/
@Aspect
@Component
public class ApiCallAdvice {
@Resource
private RedisTemplate redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final String FORMAT_PATTERN_DAY = "yyyy-MM-dd";
private static final String FORMAT_PATTERN_MILLS = "yyyy-MM-dd HH:mm:ss:SSS";
/**
* 真正执行业务操作前先进行限流的验证
* 限制维度为:一天内单个IP的访问次数
* key = URI + IP + date(精确到天)
* value = 调用次数
*/
@Before("execution(* com.pagoda.erp.platform.controller.*.*(..))")
public void before() {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//获取请求的request
HttpServletRequest request = attributes.getRequest();
String uri = request.getRequestURI();
String date = dateFormat(FORMAT_PATTERN_DAY);
String ip = getRequestIp(request);
if (StringUtils.isEmpty(ip)) {
throw new BusinessException("IP不能为空。");
}
// URI+IP+日期 构成以天为维度的key
String ipKey = uri + "_" + ip + "_" + date;
if (redisTemplate.hasKey(ipKey)) {
if (Integer.parseInt(redisTemplate.opsForValue().get(ipKey).toString()) > 10000) {
throw new BusinessException("访问失败,已超过访问次数。");
}
redisTemplate.opsForValue().increment(ipKey, 1);
} else {
stringRedisTemplate.opsForValue().set(ipKey, "1", 1L, TimeUnit.DAYS);
}
}
/**
* 如果有返回结果,代表一次调用,则对应接口的调用次数加一,统计维度为天
* (Redis使用Hash结构)
* key = URI
* key = date (精确到天)
* value = 调用次数
*/
@AfterReturning("execution(* com.pagoda.erp.platform.controller.*.*(..))")
public void afterReturning() {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//获取请求的request
HttpServletRequest request = attributes.getRequest();
String uri = request.getRequestURI();
String date = dateFormat(FORMAT_PATTERN_DAY);
if (redisTemplate.hasKey(uri)) {
redisTemplate.boundHashOps(uri).increment(date, 1);
} else {
redisTemplate.boundHashOps(uri).put(date, 1);
}
}
/**
* 如果调用抛出异常,则缓存异常信息(Redis使用Hash结构)
* key = URI + “_exception”
* key = time (精确到毫秒的时间)
* value = exception 异常信息
*
* @param ex 异常信息
*/
@AfterThrowing(value = "execution(* com.pagoda.erp.platform.controller.*.*(..))", throwing = "ex")
public void afterThrowing(Exception ex) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String uri = request.getRequestURI() + "_exception";
String time = dateFormat(FORMAT_PATTERN_MILLS);
String exception = ex.getMessage();
redisTemplate.boundHashOps(uri).put(time, exception);
}
private String getRequestIp(HttpServletRequest request) {
// 获取请求IP
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
ip = "" + request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
ip = "" + request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
ip = "" + request.getRemoteAddr();
}
return ip;
}
private String dateFormat(String pattern) {
SimpleDateFormat dateFormat = new SimpleDateFormat(pattern);
return dateFormat.format(new Date());
}
}
参考资料
基于AOP和Redis实现对接口调用情况的监控及IP限流的更多相关文章
- 基于redis+lua实现高并发场景下的秒杀限流解决方案
转自:https://blog.csdn.net/zzaric/article/details/80641786 应用场景如下: 公司内有多个业务系统,由于业务系统内有向用户发送消息的服务,所以通过统 ...
- 基于aop的redis自动缓存实现
目的: 对于查询接口所得到的数据,只需要配置注解,就自动存入redis!此后一定时间内,都从redis中获取数据,从而减轻数据库压力. 示例: package com.itliucheng.biz; ...
- 【分布式架构】--- 基于Redis组件的特性,实现一个分布式限流
分布式---基于Redis进行接口IP限流 场景 为了防止我们的接口被人恶意访问,比如有人通过JMeter工具频繁访问我们的接口,导致接口响应变慢甚至崩溃,所以我们需要对一些特定的接口进行IP限流,即 ...
- spring boot:用redis+lua实现基于ip地址的分布式流量限制(限流/简单计数器算法)(spring boot 2.2.0)
一,限流有哪些环节? 1,为什么要限流? 目的:通过对并发请求进行限速或者一个时间单位内的的请求进行限速,目的是保护系统可正常提供服务,避免被压力太大无法响应服务. 如果达到限制速率则可以采取预定的处 ...
- 基于Redis实现分布式应用限流--转
原文地址:https://my.oschina.net/giegie/blog/1525931 摘要: 限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限 ...
- 基于令牌桶算法实现的SpringBoot分布式无锁限流插件
本文档不会是最新的,最新的请看Github! 1.简介 基于令牌桶算法和漏桶算法实现的纳秒级分布式无锁限流插件,完美嵌入SpringBoot.SpringCloud应用,支持接口限流.方法限流.系统限 ...
- 微服务架构 | 5.2 基于 Sentinel 的服务限流及熔断
目录 前言 1. Sentinel 基础知识 1.1 Sentinel 的特性 1.2 Sentinel 的组成 1.3 Sentinel 控制台上的 9 个功能 1.4 Sentinel 工作原理 ...
- 一个轻量级的基于RateLimiter的分布式限流实现
上篇文章(限流算法与Guava RateLimiter解析)对常用的限流算法及Google Guava基于令牌桶算法的实现RateLimiter进行了介绍.RateLimiter通过线程锁控制同步,只 ...
- Redis限流
在电商开发过程中,我们很多地方需要做限流,有的是从Nginx上面做限流,有的是从代码层面限流等,这里我们就是从代码层面用Redis计数器做限流,这里我们用C#语言来编写,且用特性(过滤器,拦截器)的形 ...
随机推荐
- java 集成Redis 一主多从
1.测试代码如下: public static void main(String[] args) { Set<String> sentinels = new HashSet<Stri ...
- 数据库系统概论——从E-R模型到关系模型
E-R模型和关系模型都是现实世界抽象的逻辑表示 E-R模型并不被 DBMS直接支持,更适合对现实世界建模 关系模型是 DBMS直接支持的数据模型 基本 E-R图中的元素包括实体集.联系集.属性 椭圆框 ...
- pyinstaller 打包exe程序读不到配置文件No such file
挺久没更新博客的,一来之前是觉得才疏学浅,记录下来的太简单没人看.二来时间上不是很充裕(不是借口,有时间打游戏,没时间总结) 偶然有一次发现同事在搜索解决问题的时候正在看我博客的解决思路,很奇妙的感觉 ...
- 01 python安装与初识
一.简要概述 python学习时设计要大于开发. 二.编程语言 1.分类 编程语言分为高级语言和低级语言.高级语言如python.c#,Java.PHP等,低级语言(基础语言)如C.汇编语言. 2.机 ...
- idea tomcat提示Unable to ping server at localhost:1099
idea启动tomcat报错Unable to ping server at localhost:1099 是 IDEA配置的jdk版本 与 tomcat的jdk版本不配导致的
- 02-22 决策树C4.5算法
目录 决策树C4.5算法 一.决策树C4.5算法学习目标 二.决策树C4.5算法详解 2.1 连续特征值离散化 2.2 信息增益比 2.3 剪枝 2.4 特征值加权 三.决策树C4.5算法流程 3.1 ...
- idea2019版与maven3.6.2版本不兼容引发的血案
昨天遇到了点问题解决浪费了一些时间(导致更新内容较少)回顾下问题 项目出现Unable to import maven project: See logs for details 翻了好多博客 莫名的 ...
- VirtualBox for Mac 6.0.14 开源免费虚拟机方案
VirtualBox for mac是一款开源虚拟机软件,你可以利用该软件在Mac OS平台上运行Windows软件,即可以在一定程度上弥补Mac OS平台软件不足的劣势,玩家也可以获得Windows ...
- [JZOJ5459]【NOIP2017提高A组冲刺11.7】密室
Description 小X 正困在一个密室里,他希望尽快逃出密室.密室中有N 个房间,初始时,小X 在1 号房间,而出口在N 号房间.密室的每一个房间中可能有着一些钥匙和一些传送门,一个传送门会单向 ...
- MacOS访达增强工具-TotalFinder
TotalFinder 是Mac上最好用的Finder增强工具,TotalFinder 提供了多标签式浏览.拷贝路径.剪切文件.显示隐藏文件.双栏窗口模式.彩色标签等功能 彩色的标签 将彩色带回El ...