专业编程教程与实战项目分享平台

网站首页 > 技术文章 正文

Spring Boot3 中接口防抖操作全解析

ins518 2025-05-08 18:53:01 技术文章 2 ℃ 0 评论

嘿,各位后端开发的伙伴们!在日常开发工作里,大家是不是经常碰到这样的问题:用户在前端一顿疯狂操作,猛点按钮或者飞速提交表单,结果后端接口就收到了一连串重复请求。这不但浪费服务器资源,还可能引发数据不一致等诸多麻烦事。今天,咱们就来深入探讨在 Spring Boot3 中,如何巧妙实现接口防抖操作,一举解决这个令人头疼的问题。

问题引入

设想你正在开发一个电商系统的订单提交接口。用户在付款页面,由于网络卡顿,没看到提交按钮的响应,一着急就多点了好几次。此时,要是接口没有防抖机制,订单系统很可能会收到多条完全一样的订单提交请求,进而导致重复下单、库存混乱,给用户和商家都带来极大困扰。再比如在社交平台的点赞接口中,用户不小心手抖多点了几下,要是没有接口防抖,用户的点赞数就会瞬间不正常地飙升,这显然不是我们期望的结果。那么,怎样才能避免这种情况呢?这就是我们今天要全力攻克的接口防抖难题。

在当今高并发的互联网应用场景下,接口的稳定性和性能极为关键。接口防抖作为一种常见的优化方式,主要功能是防止在短时间内多次触发同一操作。用户端可能由于网络延迟、误操作等原因,在短时间内多次发送相同请求。如果后端接口不加以管控,这些请求一股脑涌入服务器,服务器就得重复处理相同业务逻辑,这无疑会加重服务器负担,严重时甚至可能致使系统崩溃。而且,多次处理重复请求还可能引发数据一致性问题,比如重复向数据库插入相同数据。所以,实现接口防抖对于提升系统性能、保障数据准确性和稳定性意义重大。

解决方案

基于注解和 AOP 实现防抖

定义防抖注解

首先,我们自定义一个防抖注解,比如@RequestLock。在这个注解里,我们可以设置一些参数,timeUnit用来指定锁时间的单位(默认可以设为秒),delimiter用于参数分隔,方便生成唯一的key。示例代码如下:

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 RequestLock {
    long value() default 1;
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    String delimiter() default "&";
}

生成唯key

为了准确判断两次请求是否重复,我们需要生成一个唯一key。这key可以由请求参数和注解中的配置共同组成。例如,对于一个添加用户的接口,请求参数中有用户userName和用户手机userPhone,我们可以选择这两个参数来生key。假userName是 “张三”,userPhone是 “123456”,按照注解中设置的分隔符 “&”,生成key就是 “张三 & 123456”,再加上注解中设置的锁前缀,就构成了一个唯一标识此次请求key。具体生key的代码逻辑如下(这里以反射获取方法参数为例):

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

public class RequestKeyGenerator {
    public static String getLockKey(ProceedingJoinPoint joinPoint, RequestLock requestLock) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Parameter[] parameters = method.getParameters();
        Object[] args = joinPoint.getArgs();
        StringBuilder keyBuilder = new StringBuilder(requestLock.prefix());
        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            Object arg = args[i];
            if (parameter.isAnnotationPresent(RequestKeyParam.class)) {
                if (arg != null) {
                    if (keyBuilder.length() > requestLock.prefix().length()) {
                        keyBuilder.append(requestLock.delimiter());
                    }
                    keyBuilder.append(arg.toString());
                }
            }
        }
        return keyBuilder.toString();
    }
}

RequestKeyParam是一个自定义注解,用于标记哪些参数参key的生成。

实现防抖逻辑(基于 Redis)

我们利用 Redis setnx命令来实现防抖逻辑。setnx(SET if Not eXists)命令可以判断指定key是否存在,如果不存在就设置这key并返回成功,否则直接返回失败。我们在获取锁成功后,给锁设置一个过期时间,防止死锁。当方法执行完成后,手动删除锁。示例代码如下(基于 Spring Data Redis):

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.concurrent.TimeUnit;

@Aspect
@Component
public class RequestLockAspect {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Around("@annotation(requestLock)")
    public Object around(ProceedingJoinPoint joinPoint, RequestLock requestLock) throws Throwable {
        String lockKey = RequestKeyGenerator.getLockKey(joinPoint, requestLock);
        boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "locked", requestLock.value(), requestLock.timeUnit());
        if (!success) {
            throw new RuntimeException("请求过于频繁,请稍后再试");
        }
        try {
            return joinPoint.proceed();
        } finally {
            stringRedisTemplate.delete(lockKey);
        }
    }
}

应用防抖注解

最后,在需要防抖的接口方法上加上@RequestLock注解就大功告成啦。比如一个保存用户信息的接口:

@PostMapping("/saveUser")
@RequestLock(value = 2, timeUnit = TimeUnit.SECONDS)
public ResponseEntity<String> saveUser(@RequestBody User user) {
    // 保存用户信息的业务逻辑
    return ResponseEntity.ok("用户信息保存成功");
}

这个接口设置了 2 秒的防抖时间,在这 2 秒内,如果收到相同参数的重复请求,就会提示 “请求过于频繁,请稍后再试”。

使用 Guava 的 RateLimiter 实现限流防抖

引入依赖

首先pom.xml文件中引入 Guava 的依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.2-jre</version>
</dependency>

配置 RateLimiter

在 Spring Boot 的配置类中,我们可以创建一RateLimiter的实例,并设置每秒允许通过的请求数。例如,我们设置每秒只允许通过 5 个请求:

import com.google.common.util.concurrent.RateLimiter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RateLimiterConfig {
    @Bean
    public RateLimiter rateLimiter() {
        return RateLimiter.create(5);
    }
}

在接口中使用 RateLimiter

在需要防抖的接口方法中,获RateLimiter实例并调tryAcquire方法尝试获取令牌。如果获取成功,说明请求在限流范围内,可以继续处理;如果获取失败,说明请求过于频繁,需要返回错误提示。示例代码如下:

import com.google.common.util.concurrent.RateLimiter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    @Autowired
    private RateLimiter rateLimiter;

    @PostMapping("/addUser")
    public ResponseEntity<String> addUser(@RequestBody User user) {
        if (!rateLimiter.tryAcquire()) {
            return ResponseEntity.status(429).body("请求过于频繁,请稍后再试");
        }
        // 添加用户的业务逻辑
        return ResponseEntity.ok("用户添加成功");
    }
}

这种方式通过限制请求的速率来实现防抖,适用于对请求频率有严格控制的场景。

总结

今天我们深入探讨了在 Spring Boot3 中实现接口防抖的两种常见且有效的方案。无论是基于注解和 AOP 利用 Redis 实现的防抖,还是借助 Guava 的 RateLimiter 实现的限流防抖,都能很好地应对高并发场景下的重复请求问题,提升我们系统的性能和稳定性。大家在实际项目中不妨试试这些方法,根据业务场景的特点选择最适合的方案。如果你在实现过程中有任何疑问或者遇到了有趣的问题,欢迎在评论区留言分享,咱们一起交流进步。同时,也别忘了点赞、收藏这篇文章,说不定以后在项目中就能派上用场哦!

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表