需求描述

  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. 【ASP.NET基础--MVC】MVC视图基础语法学习

    初步接触.net MVC的视图语法,很多东西都不太熟悉,感觉跟之前的aspx以及html都有一些区别,最近看别人的代码,一边看一边研究,现把学到的东西在这里记录一下,以便日后翻阅. 第一部分:基础知识 ...

  2. Angular: If ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as ‘standalone’ in ngModelOptions.

    在Angular中,动态生成的Html控件,如果没有name属性并且在ts中要操作Model的内容.就会引发如题的错误. 解决方案两个: 加上name的属性 设置ngModelOptions   [n ...

  3. 大神都在用的yum源

    本文原创首发于公众号:编程三分钟 yum 命令的使用 yum命令天天都在用,都快用烂了,但是很多人不知道为什么只要联网,yum命令就能像老奶奶手中的魔法棒一样,随心所欲的下载到想到的包. 比如你想装个 ...

  4. python的闭包操作

    调用外函数的时候,只是存了闭包函数给flast列表,这个闭包函数并不是一个结果,个人理解而是存的是当前的环境,比如第一次循环当前的环境就是i=0,第二次循环,当前的环境就是i=1,等. 然后,真正的计 ...

  5. 如何在Java中创建数组列表

    为了在Java中存储动态大小的元素,我们使用了ArrayList.每当添加新元素时,它会自动增加它们的大小.ArrayList实现Java的List接口和Java的Collection的一部分. 由于 ...

  6. Java中ArrayList和LinkedList的性能分析

    ArrayList和LinkedList是Java集合框架中经常使用的类.如果你只知道从基本性能比较ArrayList和LinkedList,那么请仔细阅读这篇文章. ArrayList应该在需要更多 ...

  7. Redis的几个核心机制底层原理

    #### 1.S_DOWN和O_DOWN ######   S_DOWN和O_DOWN两种宕机状态  (1).S_DOWN是主观宕机,就一个哨兵如果自己觉得一个master宕机了,那么就是主观宕机 s ...

  8. X-扫描线算法

    多边形的扫描转换 多边形有两种重要的表示方法:顶点表示和点阵表示 顶点表示是用多边形的顶点序列来表示多边形.这种表示直观.几何意义强.占内存少,易于进行几何变换. 但由于它没有明确指出哪些象素在多边形 ...

  9. Python玩转人工智能最火框架 TensorFlow应用实践 ☝☝☝

    Python玩转人工智能最火框架 TensorFlow应用实践 (一个人学习或许会很枯燥,但是寻找更多志同道合的朋友一起,学习将会变得更加有意义✌✌) 全民人工智能时代,不甘心只做一个旁观者,那就现在 ...

  10. Kubernetes快速入门

    二.Kubernetes快速入门 (1)Kubernetes集群的部署方法及部署要点 (2)部署Kubernetes分布式集群 (3)kubectl使用基础 1.简介 kubectl就是API ser ...