Spring Boot + Redis 防止接口重复提交解决方案

Spring Boot + Redis 防止接口重复提交解决方案

在Web应用中,接口重复提交是一个常见问题,可能导致数据重复、业务逻辑错误等严重后果。本文将介绍如何使用Spring Boot结合Redis实现高效可靠的接口防重提交机制。

解决方案概述

核心思路

  1. 唯一标识生成:为每个请求生成唯一标识(用户+接口+参数)
  2. Redis存储:利用Redis的原子性操作存储请求标识
  3. 过期策略:设置合理的过期时间自动清理
  4. 校验机制:在业务执行前校验请求是否已处理

技术栈

  • Spring Boot 2.7+
  • Spring Data Redis
  • Redis 5.0+

完整实现代码

1. 添加依赖 (pom.xml)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.12.0</version>
    </dependency>
</dependencies>

2. 防重提交注解 (PreventDuplicateSubmit.java)

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicateSubmit {
    // 锁定时间(默认5秒)
    long lockTime() default 5;

    // 时间单位(默认秒)
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    // 自定义错误消息
    String message() default "请勿重复提交";

    // 是否包含请求参数
    boolean includeParams() default true;

    // 是否包含用户信息
    boolean includeUser() default true;
}

3. 切面处理类 (DuplicateSubmitAspect.java)

import org.apache.commons.lang3.StringUtils;
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.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
public class DuplicateSubmitAspect {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Around("@annotation(preventDuplicateSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, PreventDuplicateSubmit preventDuplicateSubmit) 
        throws Throwable {

        // 获取请求信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) 
            RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 生成唯一请求标识
        String requestKey = generateRequestKey(joinPoint, request, preventDuplicateSubmit);

        // 尝试设置Redis锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(
            requestKey, 
            "1", 
            preventDuplicateSubmit.lockTime(), 
            preventDuplicateSubmit.timeUnit()
        );

        if (success == null || !success) {
            // 重复提交处理
            throw new DuplicateSubmitException(preventDuplicateSubmit.message());
        }

        try {
            // 执行目标方法
            return joinPoint.proceed();
        } finally {
            // 根据业务需求决定是否立即删除key
            // 如果不删除,等待自动过期即可
            // stringRedisTemplate.delete(requestKey);
        }
    }

    private String generateRequestKey(ProceedingJoinPoint joinPoint, 
                                     HttpServletRequest request,
                                     PreventDuplicateSubmit annotation) {

        // 获取方法信息
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        String className = method.getDeclaringClass().getName();
        String methodName = method.getName();

        StringBuilder keyBuilder = new StringBuilder("duplicate:submit:");

        // 包含用户信息
        if (annotation.includeUser()) {
            String userId = getCurrentUserId(request);
            keyBuilder.append("user:").append(userId).append(":");
        }

        // 包含方法信息
        keyBuilder.append("method:").append(className).append(".").append(methodName).append(":");

        // 包含请求参数
        if (annotation.includeParams()) {
            String params = getRequestParams(request, joinPoint.getArgs());
            keyBuilder.append("params:").append(params);
        }

        return keyBuilder.toString();
    }

    private String getCurrentUserId(HttpServletRequest request) {
        // 实际项目中从token或session中获取用户ID
        // 这里使用简单示例
        return request.getSession().getId();
    }

    private String getRequestParams(HttpServletRequest request, Object[] args) {
        // 简单实现:组合请求参数
        StringBuilder sb = new StringBuilder();
        request.getParameterMap().forEach((key, values) -> {
            sb.append(key).append("=").append(StringUtils.join(values, ",")).append("&");
        });

        // 添加方法参数
        if (args != null && args.length > 0) {
            for (Object arg : args) {
                sb.append(arg.toString()).append(",");
            }
        }

        return sb.toString();
    }
}

4. 自定义异常类 (DuplicateSubmitException.java)

public class DuplicateSubmitException extends RuntimeException {
    public DuplicateSubmitException(String message) {
        super(message);
    }
}

5. 全局异常处理 (GlobalExceptionHandler.java)

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(DuplicateSubmitException.class)
    @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
    public ApiResponse handleDuplicateSubmit(DuplicateSubmitException e) {
        return ApiResponse.error(e.getMessage());
    }
}

// 简单的API响应对象
class ApiResponse {
    private int code;
    private String message;
    private Object data;

    public static ApiResponse error(String message) {
        ApiResponse response = new ApiResponse();
        response.code = 429;
        response.message = message;
        return response;
    }

    // getters and setters
}

6. 控制器示例 (OrderController.java)

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @PreventDuplicateSubmit(
        lockTime = 10, 
        message = "订单提交过于频繁,请稍后再试",
        includeUser = true
    )
    @PostMapping("/order/submit")
    public ApiResponse submitOrder(@RequestBody OrderRequest orderRequest) {
        // 模拟业务处理
        try {
            Thread.sleep(2000); // 模拟耗时操作
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        return ApiResponse.success("订单提交成功");
    }
}

// 订单请求对象
class OrderRequest {
    private String productId;
    private int quantity;
    private String address;

    // getters and setters
}

// 扩展ApiResponse
class ApiResponse {
    // ...
    public static ApiResponse success(String message) {
        ApiResponse response = new ApiResponse();
        response.code = 200;
        response.message = message;
        return response;
    }
}

技术原理详解

1. 请求标识生成策略

  • 用户标识:通过session或token获取用户ID
  • 方法标识:类名+方法名唯一确定接口
  • 参数标识:请求参数组合的哈希值
  • 组合策略duplicate:submit:user:123:method:com.example.OrderController.submit:params:productId=1001

2. Redis原子操作

Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(
    key, 
    "1", 
    expireTime, 
    timeUnit
);

使用setIfAbsent实现原子操作,避免并发问题:

  1. 检查key是否存在
  2. 如果不存在则设置值并过期时间
  3. 返回操作结果

3. 过期时间策略

  • 短时效:5-30秒,适用于高频操作
  • 长时效:1-5分钟,适用于关键业务
  • 永久保留:不推荐,需手动清理

4. 防重提交流程

图片[1]-Spring Boot + Redis 防止接口重复提交解决方案-QQ沐编程

高级优化策略

1. 分布式锁升级

// 使用Redisson实现分布式锁
@Autowired
private RedissonClient redissonClient;

public Object around(ProceedingJoinPoint joinPoint) {
    RLock lock = redissonClient.getLock(requestKey);
    try {
        if (lock.tryLock(0, lockTime, timeUnit)) {
            return joinPoint.proceed();
        } else {
            throw new DuplicateSubmitException("请勿重复提交");
        }
    } finally {
        lock.unlock();
    }
}

2. 参数哈希优化

private String getParamsHash(Object[] args) {
    // 使用SHA-256生成参数哈希
    String paramsString = Arrays.deepToString(args);
    return DigestUtils.sha256Hex(paramsString);
}

3. 滑动窗口限流

// 使用Redis实现滑动窗口限流
public boolean allowRequest(String key, int maxRequests, int seconds) {
    long now = System.currentTimeMillis();
    long windowStart = now - seconds * 1000L;

    // 删除过期记录
    stringRedisTemplate.opsForZSet().removeRangeByScore(key, 0, windowStart);

    // 获取当前请求数
    long count = stringRedisTemplate.opsForZSet().count(key, windowStart, now);

    if (count < maxRequests) {
        // 添加当前请求
        stringRedisTemplate.opsForZSet().add(key, String.valueOf(now), now);
        return true;
    }
    return false;
}

4. 前端配合策略

// 前端防重提交
let submitting = false;

function submitForm() {
    if (submitting) {
        alert('请勿重复提交');
        return;
    }

    submitting = true;
    // 显示加载状态
    showLoading();

    fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify(formData)
    })
    .then(response => {
        if (response.status === 429) {
            return response.json().then(data => {
                alert(data.message);
            });
        }
        return response.json();
    })
    .finally(() => {
        submitting = false;
        hideLoading();
    });
}

使用场景建议

适用场景

  1. 订单提交、支付接口
  2. 表单提交(注册、反馈)
  3. 数据导入导出
  4. 关键业务状态变更

不适用场景

  1. 数据查询接口
  2. 实时性要求极高的接口
  3. 需要幂等性设计的接口(应使用其他方案)

性能与可靠性保障

性能优化

  1. Redis集群:部署Redis集群提高吞吐量
  2. 本地缓存:结合Caffeine实现二级缓存
  3. Key压缩:使用更短的key前缀
  4. 连接池:合理配置Redis连接池参数

可靠性保障

  1. 异常处理:Redis不可用时降级处理
  2. 监控报警:监控Redis状态和防重命中率
  3. 自动清理:设置合理的过期时间
  4. 压力测试:模拟高并发场景测试防重效果

总结

通过Spring Boot和Redis实现的接口防重提交方案具有以下优势:

  • 高效性:Redis内存操作,响应时间在毫秒级
  • 可靠性:原子操作保证并发安全
  • 灵活性:注解配置,可定制参数
  • 可扩展性:支持分布式部署
  • 易集成:与Spring生态无缝集成

在实际应用中,建议结合业务场景选择合适的防重策略:

  • 对普通表单使用5-10秒的短时效策略
  • 对关键业务使用30-60秒的长时效策略
  • 对特别重要的操作结合数据库唯一索引

这种方案能够有效防止重复提交导致的业务问题,提升系统稳定性和用户体验。

© 版权声明
THE END
喜欢就支持一下吧
点赞10赞赏 分享