基于AOP和Redis实现对接口调用情况的接口及IP限流

目录

  • 需求描述
  • 概要设计
  • 代码实现

需求描述

  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());
    }
}

参考资料

原文地址:https://www.cnblogs.com/csh24/p/11771647.html

时间: 2024-10-26 16:10:47

基于AOP和Redis实现对接口调用情况的接口及IP限流的相关文章

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

转自:https://blog.csdn.net/zzaric/article/details/80641786 应用场景如下: 公司内有多个业务系统,由于业务系统内有向用户发送消息的服务,所以通过统一消息系统对外暴露微服务接口供外部业务系统调用,所有公司内业务系统的消息(短信,APP,微信)推送都由统一消息系统去推送,短信推送需要走外部短信通道商去发送短信,APP和微信走内部系统的push服务器,但是不管是短信通道商还是内部push服务器都会有每秒上限的控制.在这假设n/s条. 以下是统一消息

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

分布式---基于Redis进行接口IP限流 场景 为了防止我们的接口被人恶意访问,比如有人通过JMeter工具频繁访问我们的接口,导致接口响应变慢甚至崩溃,所以我们需要对一些特定的接口进行IP限流,即一定时间内同一IP访问的次数是有限的. 实现原理 用Redis作为限流组件的核心的原理,将用户的IP地址当Key,一段时间内访问次数为value,同时设置该Key过期时间. 比如某接口设置相同IP10秒内请求5次,超过5次不让访问该接口. 1. 第一次该IP地址存入redis的时候,key值为IP地

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

原文地址:https://my.oschina.net/giegie/blog/1525931 摘要: 限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务. 限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务. 前几天在DD的公众号,看了一篇关于使用 瓜娃 实现单应用限流的方案 -->原文,参考<redis in action> 实现了一个jedis版本的,

Sentinel 发布0.2.0,异步调用支持、热点参数限流等成产品新亮点

Sentinel 是阿里中间件团队开源的,面向分布式服务架构的轻量级流量控制组件,主要以流量为切入点,从流量控制.熔断降级.系统负载保护等多个维度来帮助用户保护服务的稳定性. 近日,Sentinel 0.2.0 正式发布.作为一个重要的里程碑版本,Sentinel 0.2.0 释放了多项产品新特性,如 异步调用支持.热点参数限流 等,并包括了大量的体验优化与 bug 修复.下面我们来看一下 Sentinel 0.2.0 的重要新特性. 异步调用支持 未来各种 RPC 框架.Web 框架都朝着异步

从壹开始前后端分离 40 || 完美基于AOP的接口性能分析

旁白音:本文是不定时更新的.net core,当前主线任务的Nuxt+VueAdmin教程的 nuxt.js 之 tibug项目已上线,大家可以玩一玩:http://123.206.33.109:7090,具体的部署教程会在下周发表. 缘起 哈喽大家周五好呀,今天是一个不定时更新的文章,是很简单的一篇文章,大家应该都能看懂,主要包含了两个内容,一个是对AOP编程的进一步的理解(其中还有和过滤器比较),第二个就是一个简单的小插件——记录接口的调用时间调用情况,也就是很简单的性能记录,这个时候你肯定

分布式架构下的会话追踪实践【基于Cookie和Redis实现】

分布式架构下的会话追踪实践[基于Cookie和Redis实现] 博客分类: NoSQL/Redis/MongoDB session共享rediscookie分布式架构session 在单台Tomcat应用中,通常使用session保存用户的会话数据.面对高并发的场景,一台Tomcat难当大任,通常我们会使用Nginx在前端拦截用户请求,转发给后端的Tomcat服务器群组.在集群环境下,怎么才能做到session数据在多台Tomcat之间的共享呢? 当然我们可以在多台Tomcat之间进行sessi

Android基于AOP的非侵入式监控之——AspectJ实战

一引言 二什么是AspectJ 1 它只是一个代码编译器 2 它是用来做AOP编程的 3为什么要用AspectJ 三AspectJ原理与运用 1 基本原理 2 使用方式 21 纯注解方式 22 AspectJ语言 23 结合自定义注解使用 四AspectJ实战监听方法执行耗时打印并输出 五一些比较常见的问题 六推荐文章 一.引言 本博文的目的不是详细的介绍AspectJ的细节,而是最近项目用到了AspectJ,因此对其作了一些使用和重要概念上的总结. 相信很多做过Web的同学对AspectJ都不

asp.net mvc,基于aop实现的接口访问统计、接口缓存等

其实asp.net 上aop现有的框架应该蛮多的,比如静态注入式的PostSharp(新版本好像已经商业化了,旧版本又不支持.net4.0+),或者通过反射的(性能会降低). 本文则是通过mvc其中一种方法拦截器ActionFilter(参考网上已经有很多类似例子). 首先新建一个日志控制类,命名为ApiLogAttribute,继承于ActionFilterAttribute /// <summary> /// 记录访问日志以及站点安全检查 /// </summary> publ

基于Sentinel的Redis高可用方案

数据存储我们在应用设计过程中非常重要的一部分,无论是关系型数据库,还是Redis.MongoDB等非关系型数据库,都有很多的高可用方案,还有一些针对不同业务设计的中间件,使其性能更有特色,更能保证数据存储的稳定和安全. 目前主流的Redis的数据存储架构有Redis单点,Redis主从,基于Sentinel的Redis主备.基于keepalive的redis主备,以及Redis集群Cluster,还有豌豆荚开源的Codis等是目前业内比较流行的解决方案,不同的存储架构,是若干个技术工程师,根据自