java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java接口防抖幂等性

Java接口防抖/幂等性解决方案(redis)

作者:JavaRookies13

在Java项目开发过程中并发处理与幂等性问题紧密相关,这也导致了一些人认为解决幂等性就是解决高并发的问题,这篇文章主要介绍了Java接口防抖/幂等性(redis)的相关资料,需要的朋友可以参考下

一、核心区别

特性接口防抖(Debouncing)接口幂等性(Idempotency)
目的减少资源浪费:防止短时间内多次触发同一操作(如用户频繁点击、网络抖动导致重复请求)。保证结果一致性:确保同一请求无论调用一次还是多次,最终结果相同,避免重复操作导致的数据异常。
作用层面前端/后端均可实现:前端优化用户体验,后端过滤重复请求。后端核心逻辑:依赖业务逻辑和数据层设计,确保操作的唯一性。
关注点时间窗口内的重复请求:只处理最后一次或首次请求。请求的唯一性标识:通过唯一标识符(如请求ID、业务参数)判断是否重复。
典型场景用户搜索输入、按钮多次点击、无限滚动加载。支付接口、订单创建、数据修改等需避免重复操作的场景。

二、实现方式

接口防抖:

核心思想:在指定时间窗口内,仅允许最后一次(或首次)请求生效。

1.前端画面每次请求添加loading遮罩层(接口响应时间过长就会导致用户体验不好)

2.使用redis每次将请求主要参数和请求人绑定起来,放入指定的缓存时间,第二次再请求看到是同一个接口和同一个人操作则提示:操作频繁,稍后重试!

(推荐,做成自定义注解的方式,实现简单)

3.前端发送请求时,在指定时间窗口内,延迟发送请求

(不推荐,毕竟会延迟发送请求,影响接口速度)

let timeout;
function handleSearchInput(event) {
  clearTimeout(timeout);
  timeout = setTimeout(() => {
    // 发送请求
    fetch('/search', { query: event.target.value });
  }, 300); // 300ms防抖间隔
}

接下来聊聊第二种方式,自定义注解:

 1.AOP (拦截请求,并获取请求具体信息,将url,接口主要参数,用户id存入Redis中)

package com.qeoten.sms.edu.config;

import com.qeoten.sms.util.api.R;
import com.qeoten.sms.util.auth.AuthUtil;
import com.qeoten.sms.util.util.DigestUtil;
import com.qeoten.sms.util.util.RedisUtil;
import io.lettuce.core.dynamic.support.ReflectionUtils;
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.stereotype.Component;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.concurrent.TimeUnit;

/**
 * 接口防抖aop
 */
@Aspect
@Component
@Slf4j
public class AntiShakeAOP {

    @Autowired
    private RedisUtil redisUtil;

    private static final String prefix = "RepeatSubmit";

    @Around(value = "@annotation(com.qeoten.sms.edu.config.RepeatClick)")
    public Object antiShake(ProceedingJoinPoint pjp) throws Throwable {

        // 获取调用方法的信息和签名信息
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        // 获取方法
        Method method = signature.getMethod();
        // 获取注解中的参数
        RepeatClick annotation = method.getAnnotation(RepeatClick.class);

        String key = getLockKey(pjp);
        // 查询redis中是否存在对应关系
        if (!redisUtil.hasKey(key)) {

            redisUtil.setKeyAndExpire(key, null, annotation.value(), TimeUnit.MILLISECONDS);
            return pjp.proceed();
        } else {
            log.error(annotation.message());
            return R.fail(annotation.message());
        }
    }

    public static String getLockKey(ProceedingJoinPoint joinPoint) {
        //获取连接点的方法签名对象
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        //Method对象
        Method method = methodSignature.getMethod();
        String className = method.getDeclaringClass().getName();

        //获取Method对象上的注解对象
        //获取方法参数
        final Object[] args = joinPoint.getArgs();
        //获取Method对象上所有的注解
        final Parameter[] parameters = method.getParameters();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < parameters.length; i++) {
            final RepeatClick keyParam = parameters[i].getAnnotation(RepeatClick.class);
            if (keyParam == null) {
                //如果属性不是RepeatSubmit注解,则获取方法的参数名
                sb.append(args[i]).append("&");
            } else {
                final Object object = args[i];
                //获取注解类中所有的属性字段
                final Field[] fields = object.getClass().getDeclaredFields();
                for (Field field : fields) {
                    //判断字段上是否有RepeatSubmit注解
                    final RepeatClick annotation = field.getAnnotation(RepeatClick.class);
                    //如果没有,跳过
                    if (annotation == null) {
                        continue;
                    }
                    //如果有,设置Accessible为true(为true时可以使用反射访问私有变量,否则不能访问私有变量)
                    field.setAccessible(true);
                    //如果属性是RepeatSubmit注解,则拼接 连接符" & + RepeatSubmit"
                    sb.append(ReflectionUtils.getField(field, object)).append("&");
                }
            }
        }
        //返回指定前缀的key
        return prefix + ":" + className + ":" + method.getName() + ":" + AuthUtil.getUserId() + ":" + DigestUtil.md5Hex((sb.toString()));
    }
}

2.自定义注解模板(配置缓存时间,和指定提示消息)

package com.qeoten.sms.edu.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author QT-PC-0021
 */
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatClick {

    /**
     * 默认的防抖时间ms
     *
     * @return
     */
    long value() default 1000;

    String message() default "操作太频繁,请稍后再试!";

}

3.在需要进行操作表的接口上,添加自定义注解,实现功能

    @GetMapping("/advancePaper")
    @ApiOperationSupport(order = 2)
    @ApiOperation(value = "交卷", notes = "传入考试id")
    @RepeatClick
    public R<MyExamVo> advancePaper(@RequestParam Long examId){

      // 接口逻辑,可能频繁操作表

    }

接口幂等性:

核心思想:通过唯一标识符(如请求ID、业务参数)确保同一请求只处理一次。

1.数据库唯一索引:

    数据库设置唯一索引重复提交时,插表就会直接报错重复

     (不推荐,毕竟压力直接进入数据库了)

2.数据库乐观锁:(数据修改时间 / 版本号) => 比对

    查询列表画面时,将数据的修改时间(毫秒级)记录一下,下次请求增删改接口时,将数据原本的修改时间传入接口,接口第一步判断当前数据的修改时间是否和画面上传入的修改时间一致,一致就代表没有人修改做此数据,否则就提示此数据已被他人修改,请稍后再试!

最后更新记录时,带入版本号或者修改时间进去,
update xxx  set name = xxx   where id = xxx  and updateTime = xxx 

   (并发量小的时候可以,并发大的时候存在重复修改问题)

3.唯一值+缓存:

  其实也就是接口防抖中的第二个实现方案的变化版本

  上面提到将接口的主要参数+用户id作为唯一标识存入redis并记录指定的缓存时间,那么这次存入redis不记录时间,并且在接口结束时清除掉此缓存。

(推荐,但是当服务异常挂掉时,或者某些原因接口没有正常执行完成时,redis缓存一直都会在,不好维护,浪费资源)

4.分布式锁(redisson)

 业务开始时候去tryLock,尝试获取锁(锁的参数可以是本次操作的对象id,假如说本次要给某个商品增加扣减库存,那么参数可以是商品id),保障在接口的最后一步,释放锁即可。

    RLock lock = redissonClient.getLock("my-distributed-lock");

    // 尝试获取锁:等待最多 10 秒,锁自动续期 30 秒
    boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);

 这样每次拿到锁的线程才会继续进行接口逻辑操作。

5.手动实现锁

 其实原理和第4点一样,就是需要考虑手动实现锁的复杂性

1. 加锁时setnx命令,设置其lock资源名称 + value(一般为threadId / 时间戳) + 过期时间

2. 进行后续业务操作

3. 最后需要用lua脚本来释放锁(先获取锁的value确保是当前的lock,使用脚本释放锁)

总结

根据业务需求选择合适的方案,例如:

到此这篇关于Java接口防抖/幂等性解决(redis)的文章就介绍到这了,更多相关Java接口防抖幂等性内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文