Java通过Caffeine和自定义注解实现本地防抖接口限流

 更新时间:2025年05月20日 08:32:33   作者:不掉头发的阿水  
Caffeine 是目前Java领域最热门,性能最高的本地内存缓存库,本文为大家详细介绍了如何利用Caffeine和自定义注解实现本地防抖接口限流功能,需要的可以了解下

一、背景与需求

在实际项目开发中,经常遇到接口被前端高频触发、按钮被多次点击或者接口重复提交的问题,导致服务压力变大、数据冗余、甚至引发幂等性/安全风险。

常规做法是前端节流/防抖、后端用Redis全局限流、或者API网关限流。但在很多场景下:

  • 接口只要求单机(本地)防抖,不需要全局一致性;
  • 只想让同一个业务对象(同一手机号、同一业务ID、唯一标识)在自定义设置秒内只处理一次;
  • 想要注解式配置,让代码更优雅、好维护。

这个时候,Caffeine+自定义注解+AOP的本地限流(防抖)方案非常合适。

二、方案设计

1. Caffeine介绍

Caffeine 是目前Java领域最热门、性能最高的本地内存缓存库,QPS可达百万级,适用于低延迟、高并发、短TTL缓存场景。
在本地限流、防抖、接口去重等方面天然有优势。

2. 自定义注解+AOP

用自定义注解(如@DebounceLimit)标记要防抖的接口,AOP切面拦截后判断是否需要限流,核心思路是:

  • 以唯一标识作为key;
  • 每次访问接口,先查询本地Caffeine缓存;
  • 如果key在2秒内已被处理过,则直接拦截;
  • 否则执行业务逻辑,并记录处理时间。

这种方式无侵入、代码简洁、可扩展性强,适合绝大多数本地场景。

效果图如下:

三、完整实现步骤

1.Pom依赖如下

        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>2.9.3</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

2. 定义自定义注解

import java.lang.annotation.*;
 
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DebounceLimit {
    /**
     * 唯一key(支持SpEL表达式,如 #dto.id)
     */
    String key();
 
    /**
     * 防抖时间,单位秒
     */
    int ttl() default 2;
 
    /**
     * 是否返回上次缓存的返回值
     */
    boolean returnLastResult() default true;
}

3. 配置Caffeine缓存Bean

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import java.util.concurrent.TimeUnit;
 
@Configuration
public class DebounceCacheConfig {
    @Bean
    public Cache<String, Object> debounceCache() {
        return Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .maximumSize(100_000)
                .build();
    }
}

4. 编写AOP切面

import com.github.benmanes.caffeine.cache.Cache;
import com.lps.anno.DebounceLimit;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
 
@Slf4j
@Aspect
@Component
public class DebounceLimitAspect {
 
    @Autowired
    private Cache<String, Object> debounceCache;
 
    private final ExpressionParser parser = new SpelExpressionParser();
 
    @Around("@annotation(debounceLimit)")
    public Object around(ProceedingJoinPoint pjp, DebounceLimit debounceLimit) throws Throwable {
        // 1. 获取方法、参数
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        Object[] args = pjp.getArgs();
        String[] paramNames = methodSignature.getParameterNames();
        StandardEvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < paramNames.length; i++) {
            context.setVariable(paramNames[i], args[i]);
        }
 
        // 2. 解析SpEL表达式得到唯一key
        String key = parser.parseExpression(debounceLimit.key()).getValue(context, String.class);
        String cacheKey = method.getDeclaringClass().getName() + "." + method.getName() + ":" + key;
 
        long now = System.currentTimeMillis();
        DebounceResult<Object> debounceResult = (DebounceResult<Object>) debounceCache.getIfPresent(cacheKey);
 
        if (debounceResult != null && (now - debounceResult.getTimestamp() < debounceLimit.ttl() * 1000L)) {
            String methodName = pjp.getSignature().toShortString();
            log.error("接口[{}]被限流, key={}", methodName, cacheKey);
            // 是否返回上次结果
            if (debounceLimit.returnLastResult() && debounceResult.getResult() != null) {
                return debounceResult.getResult();
            }
            // 统一失败响应,可自定义异常或返回结构
            return new RuntimeException("操作过于频繁,请稍后再试!");
        }
 
        Object result = pjp.proceed();
        debounceCache.put(cacheKey, new DebounceResult<>(result, now));
        return result;
    }
 
    @Getter
    static class DebounceResult<T> {
        private final T result;
        private final long timestamp;
 
        public DebounceResult(T result, long timestamp) {
            this.result = result;
            this.timestamp = timestamp;
        }
    }
}

5. 控制器里直接用注解实现防抖

@RestController
@RequiredArgsConstructor
@Slf4j
public class DebounceControl {
    private final UserService userService;
    @PostMapping("/getUsernameById")
    @DebounceLimit(key = "#dto.id", ttl = 10)
    public String test(@RequestBody User dto) {
        log.info("在{}收到了请求,参数为:{}", DateUtil.now(), dto);
        return userService.getById(dto.getId()).getUsername();
    }
}

只要加了这个注解,同一个id的请求在自定义设置的秒内只处理一次,其他直接被拦截并打印日志。

四、扩展与注意事项

1.SpEL表达式灵活

可以用 #dto.id、#dto.mobile、#paramName等,非常适合多参数、复杂唯一性业务场景。

2.returnLastResult适合有“缓存返回结果”的场景

比如查询接口、表单重复提交直接复用上次的返回值。

3.本地限流仅适用于单机环境

多节点部署建议用Redis分布式限流,原理一样。

4.缓存key建议加上方法签名

避免不同接口之间key冲突。

5.Caffeine最大缓存、过期时间应根据业务并发和内存合理设置

绝大多数接口几千到几万key都没压力。

五、适用与不适用场景

适用:

  • 单机接口防抖/限流
  • 短时间重复提交防控
  • 按业务唯一标识维度防刷
  • 秒杀、报名、投票等接口本地保护

不适用:

  • 分布式场景(建议用Redis或API网关限流)
  • 需要全局一致性的业务
  • 内存非常敏感/极端高并发下,需结合Redis做混合限流

六、总结

Caffeine + 注解 + AOP的本地限流防抖方案,实现简单、代码优雅、性能极高、扩展灵活

到此这篇关于Java通过Caffeine和自定义注解实现本地防抖接口限流的文章就介绍到这了,更多相关Java本地防抖接口限流内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring中的@Transactional事务失效场景解读

    Spring中的@Transactional事务失效场景解读

    这篇文章主要介绍了Spring中的@Transactional事务失效场景解读,如果Transactional注解应用在非public 修饰的方法上,Transactional将会失效此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息,需要的朋友可以参考下
    2023-12-12
  • 关于Spring @Bean 相同加载顺序不同结果不同的问题记录

    关于Spring @Bean 相同加载顺序不同结果不同的问题记录

    本文主要探讨了在Spring 5.1.3.RELEASE版本下,当有两个全注解类定义相同类型的Bean时,由于加载顺序不同,最终生成的Bean实例也会不同,文章通过分析ConfigurationClassPostProcessor的执行过程,解释了BeanDefinition的加载和覆盖机制,感兴趣的朋友一起看看吧
    2025-02-02
  • Springboot详解RocketMQ实现广播消息流程

    Springboot详解RocketMQ实现广播消息流程

    RocketMQ作为一款纯java、分布式、队列模型的开源消息中间件,支持事务消息、顺序消息、批量消息、定时消息、消息回溯等,本篇我们了解如何实现广播消息
    2022-06-06
  • 最好的Java 反编译工具的使用对比分析

    最好的Java 反编译工具的使用对比分析

    恰好最近工作中也需要用到 Java 反编译,所以这篇文章介绍目前常见的的几种 Java 反编译工具的使用,在文章的最后也会通过编译速度、语法支持以及代码可读性三个维度,对它们进行测试,分析几款工具的优缺点,感兴趣的朋友一起看看吧
    2021-05-05
  • Spring中的父子容器原理解析

    Spring中的父子容器原理解析

    这篇文章主要为大家介绍了Spring中的父子容器原理解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-07-07
  • java利用注解实现简单的excel数据读取

    java利用注解实现简单的excel数据读取

    这篇文章主要为大家详细介绍了java利用注解实现简单的excel数据读取,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-06-06
  • MyBatis的动态SQL语句实现

    MyBatis的动态SQL语句实现

    这篇文章主要介绍了MyBatis的动态SQL语句实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-01-01
  • SpringBoot操作MaxComputer方式(保姆级教程)

    SpringBoot操作MaxComputer方式(保姆级教程)

    这篇文章主要介绍了SpringBoot操作MaxComputer方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-03-03
  • zookeeper概述图文详解

    zookeeper概述图文详解

    今天小编就为大家分享一篇关于Zookeeper概述图文详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-01-01
  • SpringBoot如何使用@Value取配置文件中的map配置

    SpringBoot如何使用@Value取配置文件中的map配置

    这篇文章主要介绍了SpringBoot如何使用@Value取配置文件中的map配置方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-05-05

最新评论