Spring Boot 接口防抖(防重复提交)实战:基于 @Lock4j + Hutool 实现

在实际开发中,接口重复提交是高频问题(如用户快速点击按钮、网络延迟导致的重试),尤其匿名接口(无需登录)更易出现。本文基于 @Lock4j 分布式锁 + Hutool 工具类,实现一套支持单机/集群环境的接口防抖方案,核心逻辑是通过「客户端IP + 接口标识」生成唯一锁键,拦截短时间内的重复请求。

👉 适用场景:匿名表单提交、游客评论、验证码发送等无需登录的接口;稍作修改也可适配登录接口(替换为用户ID+接口标识)。

一、方案核心思路

  1. IP 识别:通过 Hutool 的 ServletUtil 工具类,快速获取客户端真实 IP(支持反向代理/负载均衡场景);

  2. 分布式锁拦截:用 @Lock4j 注解为接口添加分布式锁,锁键由「IP + 接口标识」动态生成;

  3. 防抖控制:设置锁过期时间(防抖窗口,如3秒),同一 IP 在窗口期内重复请求会因获取锁失败被拦截;

  4. 友好提示:全局异常捕获锁失败异常,返回标准化响应(429 状态码 + 提示语)。

二、环境准备

2.1 核心依赖(Maven)

需引入 Spring Boot Web、Lock4j 分布式锁、Redis(锁存储介质)、Hutool 工具包,直接复制到 pom.xml 即可:

<!-- Spring Boot Web 核心 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Hutool 工具包(获取真实IP) -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.22</version> <!-- 推荐最新稳定版 -->
</dependency>

<!-- Lock4j 分布式锁核心(注解式开发) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>lock-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

<!-- Redis 依赖(Lock4j 底层存储,必须) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.2 配置文件(application.yml)

配置 Redis 连接信息和 Lock4j 全局参数(防抖窗口、锁前缀等):

spring:
  # Redis 配置(根据实际环境修改)
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456  # 无密码则省略
    database: 0
    timeout: 5000ms
  # Spring MVC 路径匹配策略(Spring Boot 3.x+ 需配置)
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher

# Lock4j 全局配置(防抖核心参数)
lock4j:
  lock-type: redis  # 锁类型:Redis(默认,支持Zookeeper)
  redis:
    key-prefix: API_DEBOUNCE_  # 锁键前缀,避免与其他业务锁冲突
    expire: 3000               # 锁过期时间 = 防抖窗口(3秒内重复请求拦截)
    acquire-timeout: 100       # 获取锁等待时间(100ms,重复请求直接失败)
    time-unit: MILLISECONDS    # 时间单位(默认毫秒)

三、核心实现代码

3.1 IP 工具类(基于 Hutool 封装)

统一封装 IP 获取逻辑,适配反向代理场景(如 Nginx),避免代码冗余:

import cn.hutool.extra.servlet.ServletUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;

/**
 * IP 工具类(基于 Hutool 封装,支持反向代理)
 */
@Component
public class IpHelper {

    // 可信代理 IP 段(根据实际代理服务器配置,如 Nginx、网关)
    // 内网网段参考:192.168.0.0/16、10.0.0.0/8、172.16.0.0/12
    private static final String[] TRUST_PROXY_IPS = {"192.168.1.0/24", "10.0.0.0/8", "172.16.0.0/12"};

    /**
     * 获取客户端真实 IP
     * @param request 请求对象
     * @return 真实 IP 字符串
     */
    public String getRealClientIp(HttpServletRequest request) {
        // Hutool 核心方法:自动解析 X-Forwarded-For、X-Real-IP 等请求头
        // 跳过可信代理 IP,获取真实客户端 IP
        return ServletUtil.getClientIP(request, TRUST_PROXY_IPS);
    }
}

3.2 通用封装类(DTO + 统一返回结果)

3.2.1 表单提交 DTO(请求参数)
import lombok.Data;

/**
 * 表单提交请求参数 DTO
 */
@Data
public class FormSubmitDTO {
    // 表单标题
    private String title;
    // 表单内容
    private String content;
    // 扩展字段:可根据业务添加(如联系方式、验证码等)
    private String contact;
}
3.2.2 统一返回结果类

标准化接口响应格式,方便前端统一处理:

import lombok.Data;

/**
 * 全局统一返回结果
 */
@Data
public class Result<T> {
    // 响应码(200成功,429请求频繁,500服务器错误等)
    private int code;
    // 响应消息
    private String msg;
    // 响应数据
    private T data;

    // 成功响应(带数据)
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "操作成功", data);
    }

    // 成功响应(无数据)
    public static <T> Result<T> success() {
        return new Result<>(200, "操作成功", null);
    }

    // 失败响应
    public static <T> Result<T> fail(int code, String msg) {
        return new Result<>(code, msg, null);
    }
}

3.3 防抖接口实现(核心 Controller)

@Lock4j 注解实现防抖,通过 SpEL 表达式动态生成锁键:

import com.baomidou.lock.annotation.Lock4j;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * 接口防抖 Demo Controller(匿名表单提交)
 */
@RestController
@RequestMapping("/api/debounce")
public class DebounceController {

    // 注入 IP 工具类
    @Resource
    private IpHelper ipHelper;

    /**
     * 匿名表单提交接口(防抖:同一IP 3秒内只能提交一次)
     * 核心逻辑:锁键 = ip_ + 真实IP + _form_submit(保证IP+接口维度唯一)
     */
    @PostMapping("/submit-form")
    @Lock4j(
            // SpEL 表达式:调用 IpHelper 获取真实IP,拼接接口标识
            key = "'ip_' + @ipHelper.getRealClientIp(#request) + '_form_submit'",
            // 锁过期时间(覆盖全局配置,可选)
            expire = 3000,
            // 获取锁失败提示语(前端展示)
            msg = "操作太频繁啦,请3秒后再试~"
    )
    public Result<String> submitForm(@RequestBody FormSubmitDTO formDTO, HttpServletRequest request) {
        // ========== 核心业务逻辑(根据实际需求修改) ==========
        String clientIp = ipHelper.getRealClientIp(request);
        System.out.printf("IP:%s 提交表单,标题:%s,内容:%s%n", 
                clientIp, formDTO.getTitle(), formDTO.getContent());

        // 模拟业务处理(如保存表单到数据库)
        // formService.save(formDTO);
        // ========== 业务逻辑结束 ==========

        return Result.success("表单提交成功,已收到您的反馈~");
    }

    /**
     * 简化写法:不封装 IpHelper,直接在 SpEL 中调用 Hutool 工具类
     * 适用场景:小型项目,无需复用 IP 获取逻辑
     */
    @PostMapping("/submit-form-simple")
    @Lock4j(
            // 直接调用 Hutool 的 ServletUtil,指定可信代理 IP
            key = "'ip_' + T(cn.hutool.extra.servlet.ServletUtil).getClientIP(#request, {'192.168.1.0/24'}) + '_form_simple'",
            msg = "操作太频繁,请稍后再试~"
    )
    public Result<String> submitFormSimple(@RequestBody FormSubmitDTO formDTO, HttpServletRequest request) {
        String clientIp = cn.hutool.extra.servlet.ServletUtil.getClientIP(request, new String[]{"192.168.1.0/24"});
        System.out.printf("简化写法 - IP:%s 提交表单%n", clientIp);
        return Result.success("简化版表单提交成功");
    }
}

3.4 全局异常处理器(捕获防抖异常)

当重复请求获取锁失败时,@Lock4j 会抛出 LockFailureException,通过全局异常处理器返回友好提示:

import com.baomidou.lock.exception.LockFailureException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理器
 */
@RestControllerAdvice  // 拦截所有 @RestController 接口
public class GlobalExceptionHandler {

    /**
     * 捕获 Lock4j 防抖异常(请求频繁)
     */
    @ExceptionHandler(LockFailureException.class)
    public Result<?> handleDebounceException(LockFailureException e) {
        // 返回 429 状态码(HTTP 标准:请求过于频繁)
        return Result.fail(429, e.getMessage());
    }

    /**
     * 捕获其他异常(可选,增强鲁棒性)
     */
    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e) {
        return Result.fail(500, "服务器内部错误:" + e.getMessage());
    }
}

四、反向代理配置(Nginx 示例)

如果服务部署在 Nginx 等反向代理之后,需在 Nginx 配置中添加以下参数,否则 ServletUtil 获取的是代理服务器 IP,而非客户端真实 IP:

server {
    listen 80;
    server_name your-domain.com;  # 你的域名

    location / {
        # 转发请求到后端 Spring Boot 服务
        proxy_pass http://127.0.0.1:8080;

        # 传递真实客户端 IP 的核心配置(必须)
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;  # 客户端真实 IP
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  # 代理链 IP 列表
    }
}

五、测试验证

5.1 测试步骤

  1. 启动 Redis 服务(确保配置文件中的 Redis 地址/密码正确);

  2. 启动 Spring Boot 项目;

  3. 用 Postman 或 curl 工具,连续 2 次请求接口:POST http://localhost:8080/api/debounce/submit-form

  4. 请求体(JSON 格式):
    { "title": "测试接口防抖", "content": "这是第一次提交的内容", "contact": "test@example.com" }

5.2 预期结果

  • 第一次请求:返回 {"code":200,"msg":"操作成功","data":"表单提交成功,已收到您的反馈~"}

  • 3秒内第二次请求:返回 {"code":429,"msg":"操作太频繁啦,请3秒后再试~","data":null}

  • 3秒后再次请求:恢复正常响应,返回成功信息。

六、关键注意事项(避坑指南)

  1. 锁键必须唯一:锁键公式为「固定前缀 + 请求主体标识 + 接口标识」,本文中是 ip_ + IP + _form_submit。如果锁键不唯一,会导致不同 IP/接口的请求互相拦截,或无法拦截重复请求;

  2. 可信代理 IP 必配置:如果服务有反向代理,必须在 IpHelper 中配置 TRUST_PROXY_IPS,否则恶意用户可伪造 X-Forwarded-For 头,导致 IP 获取错误;

  3. 防抖窗口合理设置expire 参数(防抖窗口)建议设置为 2~5 秒。太短无法有效拦截重复请求,太长会影响用户体验;

  4. 集群环境注意事项:所有服务实例必须连接同一个 Redis 集群,否则锁无法互通,防抖失效;

  5. 登录接口适配:如果是登录接口,只需将锁键中的「IP」替换为「用户ID」(如 user_ + #userId + _submit_order),即可实现同一用户的防抖。

七、方案优势对比

方案 优点 缺点
前端防抖(按钮置灰) 简单、无后端压力 无法防绕过前端的恶意请求(如 Postman 直接调用)
基于 Session 防抖 实现简单 不支持分布式集群,匿名接口无法使用
本文方案(@Lock4j + Hutool) 1. 注解式开发,无侵入;2. 支持分布式集群;3. 适配匿名/登录接口;4. 自动释放锁,防死锁;5. 集成简单,代码复用性高 依赖 Redis 服务

八、扩展场景:登录接口防抖适配

如果需要适配登录接口(基于用户ID防抖),只需修改 Controller 中的锁键,示例如下:

import com.baomidou.lock.annotation.Lock4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * 登录接口防抖示例
 */
@RestController
@RequestMapping("/api/user")
public class UserController {

    // 模拟获取当前登录用户ID(实际项目从 Token/Session 中获取)
    private Long getCurrentUserId() {
        // 示例:从 ThreadLocal 中获取用户ID(Spring Security/Shiro 可直接获取)
        return 1001L;
    }

    /**
     * 订单提交接口(防抖:同一用户 5 秒内只能提交一次)
     */
    @PostMapping("/submit-order")
    @Lock4j(
            // 锁键:user_ + 用户ID + _submit_order
            key = "'user_' + T(com.example.demo.util.UserContext).getUserId() + '_submit_order'",
            expire = 5000,
            msg = "订单提交中,请5秒后再试~"
    )
    public Result<String> submitOrder(@RequestBody OrderDTO orderDTO) {
        Long userId = getCurrentUserId();
        System.out.printf("用户 %d 提交订单:%s%n", userId, orderDTO.getOrderNo());
        // 业务逻辑:创建订单、扣减库存等
        return Result.success("订单提交成功");
    }
}

总结

本文基于 @Lock4j 和 Hutool 实现的接口防抖方案,兼顾了简洁性和实用性,支持单机/集群环境,适配匿名/登录多种场景。核心是通过「唯一锁键」和「过期时间」控制请求频率,配合 Hutool 快速获取真实 IP,无需手动编写加锁/解锁逻辑,大幅提升开发效率。

👉 完整代码可直接复制到项目中使用,只需根据实际业务修改「业务逻辑」和「锁键规则」即可。

(注:文档部分内容可能由 AI 生成)

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐