需求描述

  1. 项目中有许多接口,现在我们需要实现一个功能对接口调用情况进行统计,主要功能如下:

    • 需求一:实现对每个接口,每天的调用次数做记录;
    • 需求二:如果某次调用抛出了异常信息,则记录下异常信息;
    • 需求三:限流,限制单个IP一天内对一个接口的调用次数。

概要设计

  1. 因为需要对每个接口的调用情况进行统计,所以选择AOP来实现,将Controller层抽象为一个切面

    • @Before 执行业务操作前进行限流判断;

    • @AfterReturn 如果正常返回则调用次数加1;

    • @AfterThrowing 如果抛出异常则记录异常信息。

    如果将这些信息写入数据库的话会对每个接口带来额外的操作数据库的开销,影响接口响应时间,且此类记录信息较多,所以此处选择Redis将这些信息缓存下来。

  2. 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限流的更多相关文章

  1. 基于redis+lua实现高并发场景下的秒杀限流解决方案

    转自:https://blog.csdn.net/zzaric/article/details/80641786 应用场景如下: 公司内有多个业务系统,由于业务系统内有向用户发送消息的服务,所以通过统 ...

  2. 基于aop的redis自动缓存实现

    目的: 对于查询接口所得到的数据,只需要配置注解,就自动存入redis!此后一定时间内,都从redis中获取数据,从而减轻数据库压力. 示例: package com.itliucheng.biz; ...

  3. 【分布式架构】--- 基于Redis组件的特性,实现一个分布式限流

    分布式---基于Redis进行接口IP限流 场景 为了防止我们的接口被人恶意访问,比如有人通过JMeter工具频繁访问我们的接口,导致接口响应变慢甚至崩溃,所以我们需要对一些特定的接口进行IP限流,即 ...

  4. spring boot:用redis+lua实现基于ip地址的分布式流量限制(限流/简单计数器算法)(spring boot 2.2.0)

    一,限流有哪些环节? 1,为什么要限流? 目的:通过对并发请求进行限速或者一个时间单位内的的请求进行限速,目的是保护系统可正常提供服务,避免被压力太大无法响应服务. 如果达到限制速率则可以采取预定的处 ...

  5. 基于Redis实现分布式应用限流--转

    原文地址:https://my.oschina.net/giegie/blog/1525931 摘要: 限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限 ...

  6. 基于令牌桶算法实现的SpringBoot分布式无锁限流插件

    本文档不会是最新的,最新的请看Github! 1.简介 基于令牌桶算法和漏桶算法实现的纳秒级分布式无锁限流插件,完美嵌入SpringBoot.SpringCloud应用,支持接口限流.方法限流.系统限 ...

  7. 微服务架构 | 5.2 基于 Sentinel 的服务限流及熔断

    目录 前言 1. Sentinel 基础知识 1.1 Sentinel 的特性 1.2 Sentinel 的组成 1.3 Sentinel 控制台上的 9 个功能 1.4 Sentinel 工作原理 ...

  8. 一个轻量级的基于RateLimiter的分布式限流实现

    上篇文章(限流算法与Guava RateLimiter解析)对常用的限流算法及Google Guava基于令牌桶算法的实现RateLimiter进行了介绍.RateLimiter通过线程锁控制同步,只 ...

  9. Redis限流

    在电商开发过程中,我们很多地方需要做限流,有的是从Nginx上面做限流,有的是从代码层面限流等,这里我们就是从代码层面用Redis计数器做限流,这里我们用C#语言来编写,且用特性(过滤器,拦截器)的形 ...

随机推荐

  1. Java之字符流读写文件、文件的拷贝

    字符流读数据 – 按单个字符读取 创建字符流读文件对象: Reader reader = new FileReader("readme.txt"); 调用方法读取数据: int d ...

  2. layDate——设置最大日期不能超过当前日期

    例如,当前年份是2018年,实现效果如下,2018年之后年份不可操作: 具体代码实现: layui.use([ 'laydate'], function () { var laydate = layu ...

  3. springboot与docker整合

    一.springboot与docker整合 a.创建Dockerfile FROM java MAINTAINER "Wing"<1561815137@qq.com> ...

  4. Hadoop 学习笔记之 Incompatible namespaceIDs问题

    Hadoop重新格式化后,仍然datanode启动失败,查看log: org.apache.hadoop.hdfs.server.datanode.DataNode: Java.io.IOExcept ...

  5. 理解Android中的注解与反射

    反射 Java反射(Reflection)定义 Java反射机制是指在运行状态中 对于任意一个类,都能知道这个类的所有属性和方法:对于任何一个对象,都能够调用它的任何一个方法和属性: 这样动态获取新的 ...

  6. mac下安装rabbitmq

    使用homebrew安装rabbitmq,命令如下: brew install rabbitmq 安装的位置如下/usr/local/Cellar/rabbitmq/3.7.18 进入到sbin目录下 ...

  7. python程序调用C/C++代码

    这篇用来记录在些模拟Canoe生成CAN数据桢工具时遇到的问题, 生成CAN数据桢,主要分为两个关注点: 1.如何从can信号名获取到can信号的ID长度以及信号的起始位,并将信号值按照一定的规则填写 ...

  8. 浅谈json web token及应用

    Json Web Token (JWT),是一个非常轻巧的规范,这个规范允许在网络应用环境间客户端和服务器间较安全的传递信息.该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO) ...

  9. tcpdump使用详情及案例

    转载http://starsliao.blog.163.com/blog/static/89048201062333032563/ TCPdump抓包命令tcpdump是一个用于截取网络分组,并输出分 ...

  10. Win10实现多用户同时进行远程桌面连接的解决方案

    Win10无法实现多用户远程桌面连接的解决方案以下方法解决Win10无法同时多用户登录.推荐:win10正式版下载 以下为必要步骤需先进行组策略设置,完成后再选择方法1或2(推荐方法2): 远程桌面连 ...