手摸手系列之 - 什么是接口的幂等性以及 AOP+Redis 基于注解实现接口幂等性校验

news2024/12/24 8:34:56

接口的幂等性是指在分布式系统中,一个操作或者请求无论执行多少次,其结果都是相同的。换句话说,即使多次执行同一个操作,它也不会产生副作用,或者不会改变系统的状态。幂等性是设计 RESTful API 时的一个重要原则。

幂等性通常适用于以下两种情况:

  1. 安全操作: 例如,GET 请求用于获取资源,不论执行多少次,都不会改变资源的状态,因此是幂等的。
  2. 状态改变操作: 例如,PUT 请求用于更新资源,如果资源已经处于请求中描述的状态,再次执行相同的 PUT 请求不会对资源造成进一步的改变,因此也是幂等的。

幂等性对于确保分布式系统的一致性和可靠性非常重要,特别是在网络请求可能会因为各种原因被重复发送的情况下。例如,如果一个用户提交了一个表单,但由于网络问题,表单被提交了两次,幂等性可以保证系统不会因为重复的提交而产生错误的状态或数据。

如何实现幂等性?

前端控制,在前端做拦截,比如按钮点击一次之后就置灰或者隐藏。但是往往前端并不可靠,还是得后端处理才更放心。

现在我们在后端通过一个注解Idempotent来一步步实现接口的幂等性。

  1. 首先定义一个幂等注解Idempotent,用于标注方法为幂等操作。
package org.jeecg.common.idempotent.annotation;

import org.jeecg.common.idempotent.keyresolver.IdempotentKeyResolver;
import org.jeecg.common.idempotent.keyresolver.impl.DefaultIdempotentKeyResolver;

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;

/**
 * 幂等注解,用于标注方法为幂等操作。
 * 幂等性意味着无论调用多少次,结果都相同,不会产生副作用。
 * 通过此注解,可以实现对重复请求的拦截,提高系统稳定性和效率。
 *
 * @author ZHANGCHAO
 * @version 1.0.0
 * @date 2023/5/15 14:23
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {

    /**
     * 幂等的超时时间,默认为 1 秒
     * <p>
     * 注意,如果执行时间超过它,请求还是会进来
     */
    int timeout() default 1;

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

    /**
     * 提示信息,正在执行中的提示
     */
    String message() default "重复请求,请稍后重试";

    /**
     * 使用的 Key 解析器
     * 设置用于生成幂等键的解析器类。
     * 幂等键用于唯一标识一个幂等操作,通过解析器可以从方法参数等中提取出此键。
     * 默认解析器为DefaultIdempotentKeyResolver,它根据方法参数生成幂等键。
     */
    Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class;

    /**
     * 使用的 Key 参数
     * 设置用于生成幂等键的参数名。
     * 此参数名应对应方法的一个参数,解析器将根据此参数值生成幂等键。
     * 如果不设置,默认解析器将根据所有参数生成幂等键。
     * 注意,如果设置了keyResolver为自定义解析器,此参数可能被忽略。
     *
     * @return 用于生成幂等键的参数名。
     */
    String keyArg() default "";
}
  1. 定义幂等性切面类,用于对标注了{@link Idempotent}注解的方法进行幂等性校验。
package org.jeecg.common.idempotent.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.jeecg.common.idempotent.CollectionUtils;
import org.jeecg.common.idempotent.IdempotentRedisDAO;
import org.jeecg.common.idempotent.annotation.Idempotent;
import org.jeecg.common.idempotent.keyresolver.IdempotentKeyResolver;
import org.springframework.util.Assert;

import java.util.List;
import java.util.Map;

/**
 * 幂等性切面类,用于对标注了{@link Idempotent}注解的方法进行幂等性校验。
 * 通过在方法执行前检查是否已处理过相同的请求,来防止重复操作。
 * @author ZHANGCHAO
 * @version 1.0.0
 * @date 2023/5/15 14:28
 */
@Slf4j
@Aspect
public class IdempotentAspect {

    /**
     * IdempotentKeyResolver 集合
     * 幂等键解析器的映射,用于根据注解中指定的类名获取对应的解析器实例。
     */
    private final Map<Class<? extends IdempotentKeyResolver>, IdempotentKeyResolver> keyResolvers;

    /**
     * Redis操作DAO,用于在Redis中进行幂等键的设置和查询。
     */
    private final IdempotentRedisDAO idempotentRedisDAO;

    /**
     * 构造函数,初始化幂等键解析器映射和Redis DAO。
     *
     * @param keyResolvers 幂等键解析器列表。
     * @param idempotentRedisDAO 幂等性Redis操作DAO。
     */
    public IdempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
        this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass);
        this.idempotentRedisDAO = idempotentRedisDAO;
    }

    /**
     * 在方法执行前的切面逻辑,用于实现幂等性校验。
     * 通过注解@annotation(idempotent)来标识需要进行幂等性校验的方法。
     *
     * @param joinPoint 切点,用于获取方法参数和签名等信息。
     * @param idempotent 幂等性注解实例,包含幂等键解析器的类名、锁的超时时间等信息。
     * @throws RuntimeException 如果key已存在,即重复请求,抛出运行时异常。
     */
    @Before("@annotation(idempotent)")
    public void beforePointCut(JoinPoint joinPoint, Idempotent idempotent) {
        // 根据注解中指定的幂等键解析器类名,获取对应的幂等键解析器
        // 获得 IdempotentKeyResolver
        IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver());
        // 确保幂等键解析器不为空,否则抛出异常
        Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver");
        // 使用幂等键解析器解析出请求的幂等键
        // 解析 Key
        String key = keyResolver.resolver(joinPoint, idempotent);
        // 日志记录解析出的幂等键
        log.info("key: {}", key);
        // 尝试在Redis中设置幂等键,如果不存在则设置成功,表示该请求是第一次到来
        // 锁定 Key。
        boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit());
        // 如果设置失败,表示幂等键已存在,即该请求是重复的,抛出运行时异常
        // 锁定失败,抛出异常
        if (!success) {
            log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs());
            throw new RuntimeException(idempotent.message());
        }
    }

}
  1. Key 解析器接口
package org.jeecg.common.idempotent.keyresolver;

import org.aspectj.lang.JoinPoint;
import org.jeecg.common.idempotent.annotation.Idempotent;

/**
 * 幂等性键解析器接口。
 * 该接口用于解析方法调用的幂等性键,以确保重复调用的处理符合幂等性原则。
 * @author ZHANGCHAO
 * @version 1.0.0
 * @date 2023/5/15 14:21
 */
public interface IdempotentKeyResolver {

    /**
     * 解析幂等性键 key
     *
     * @param joinPoint 切点,包含方法调用的相关信息。
     * @param idempotent 幂等性注解,用于配置幂等性处理的相关属性。
     * @return 解析得到的幂等性键。
     * @description 该方法通过分析方法参数和注解属性,生成一个唯一的幂等性键,用于标识一个幂等操作。
     */
    String resolver(JoinPoint joinPoint, Idempotent idempotent);
}
  1. 定义两个 Key 解析器接口的实现类,一个默认的根据方法名和参数生成幂等性 key,一个基于 Spring Expression Language (SpEL)
    首先是 DefaultIdempotentKeyResolver
/**
 * 默认的幂等性关键字解析器,实现了IdempotentKeyResolver接口。
 * 该解析器用于根据方法名和参数生成幂等性关键字。
 */
package org.jeecg.common.idempotent.keyresolver.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.jeecg.common.idempotent.annotation.Idempotent;
import org.jeecg.common.idempotent.keyresolver.IdempotentKeyResolver;

/**
 * 默认幂等性关键字解析器类。
 */
public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver {

    /**
     * 根据切面连接点和幂等注解,解析并返回幂等性关键字。
     *
     * @param joinPoint 切面连接点,包含目标方法和其参数信息。
     * @param idempotent 幂等注解,用于配置幂等性相关属性。
     * @return 生成的幂等性关键字。
     */
    /**
     * 解析一个 Key
     *
     * @param joinPoint  AOP 切面
     * @param idempotent 幂等注解
     * @return Key
     */
    @Override
    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
        // 获取目标方法名
        String methodName = joinPoint.getSignature().toString();

        // 创建一个数组,用于存储除ShiroHttpServletRequest外的所有参数
        Object[] objects = new Object[joinPoint.getArgs().length];
        for (int i = 0; i < joinPoint.getArgs().length; i++) {
            // 排除ShiroHttpServletRequest类型的参数,因为它们不参与幂等性关键字的生成
            if (!(joinPoint.getArgs()[i] instanceof ShiroHttpServletRequest)) {
                objects[i] = joinPoint.getArgs()[i];
            }
        }

        // 将参数数组转换为字符串,使用逗号分隔
        String argsStr = StrUtil.join(",", objects);

        // 使用methodName和argsStr拼接后的字符串进行MD5加密,生成幂等性关键字
        return SecureUtil.md5(methodName + argsStr);
    }
}

ExpressionIdempotentKeyResolver:

package org.jeecg.common.idempotent.keyresolver.impl;

import cn.hutool.core.util.ArrayUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.jeecg.common.idempotent.annotation.Idempotent;
import org.jeecg.common.idempotent.keyresolver.IdempotentKeyResolver;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.lang.reflect.Method;

/**
 * 基于Spring EL表达式
 *
 * 实现幂等性Key的解析,基于Spring Expression Language (SpEL)。
 * 该解析器通过评估给定的SpEL表达式来生成幂等性Key。
 *
 * @author ZHANGCHAO
 * @version 1.0.0
 * @date 2023/5/15 14:26
 */
public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver {

    /**
     * 用于发现方法参数名称的工具。
     */
    private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();

    /**
     * SpEL表达式解析器。
     */
    private final ExpressionParser expressionParser = new SpelExpressionParser();

    /**
     * 获取实际的方法对象,处理接口和实现类之间的映射。
     *
     * @param point 切点,包含方法调用的信息。
     * @return 方法对象。
     */
    private static Method getMethod(JoinPoint point) {
        // 处理,声明在类上的情况
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        if (!method.getDeclaringClass().isInterface()) {
            return method;
        }

        // 处理,声明在接口上的情况
        try {
            return point.getTarget().getClass().getDeclaredMethod(
                    point.getSignature().getName(), method.getParameterTypes());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 解析一个 Key
     * 根据SpEL表达式解析出幂等性Key。
     *
     * @param joinPoint 切面连接点,包含当前的Method调用信息。
     * @param idempotent 幂等注解实例,包含SpEL表达式。
     * @return 解析出的幂等性Key。
     */
    @Override
    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
        // 获取实际调用的方法
        Method method = getMethod(joinPoint);
        // 获取方法参数
        Object[] args = joinPoint.getArgs();
        // 获取方法参数名称
        String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method);
        // 创建SpEL表达式的评估上下文
        // 准备 Spring EL 表达式解析的上下文
        StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
        // 设置参数名称和值到评估上下文中
        if (ArrayUtil.isNotEmpty(parameterNames)) {
            for (int i = 0; i < parameterNames.length; i++) {
                evaluationContext.setVariable(parameterNames[i], args[i]);
            }
        }

        // 解析注解中定义的SpEL表达式,获取幂等性Key
        // 解析参数
        Expression expression = expressionParser.parseExpression(idempotent.keyArg());
        return expression.getValue(evaluationContext, String.class);
    }
}
  1. 幂等性配置类,用于初始化幂等性相关的Bean
package org.jeecg.common.idempotent;

import org.jeecg.common.idempotent.aop.IdempotentAspect;
import org.jeecg.common.idempotent.keyresolver.IdempotentKeyResolver;
import org.jeecg.common.idempotent.keyresolver.impl.DefaultIdempotentKeyResolver;
import org.jeecg.common.idempotent.keyresolver.impl.ExpressionIdempotentKeyResolver;
import org.jeecg.common.modules.redis.config.RedisConfig;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.List;

/**
 * 幂等性配置类,用于初始化幂等性相关的Bean。
 *
 * @author ZHANGCHAO
 * @version 1.0.0
 * @date 2023/5/15 14:40
 */
@Configuration
@AutoConfigureAfter(RedisConfig.class) // 依赖Redis配置,确保在Redis配置之后初始化
public class IdempotentConfiguration {

    /**
     * 初始化幂等性切面。
     *
     * @param keyResolvers 幂等性键解析器列表,用于生成唯一的幂等性键。
     * @param idempotentRedisDAO 幂等性Redis操作DAO,用于存储和查询幂等性键。
     * @return 初始化后的幂等性切面实例。
     */
    @Bean
    public IdempotentAspect idempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
        return new IdempotentAspect(keyResolvers, idempotentRedisDAO);
    }

    /**
     * 初始化幂等性Redis DAO。
     *
     * @param stringRedisTemplate 字符串Redis模板,用于操作Redis。
     * @return 初始化后的幂等性Redis DAO实例。
     */
    @Bean
    public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) {
        return new IdempotentRedisDAO(stringRedisTemplate);
    }

    // ========== 各种 IdempotentKeyResolver Bean ==========
    /**
     * 初始化默认幂等性键解析器。
     *
     * @return 默认幂等性键解析器实例。
     */
    @Bean
    public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() {
        return new DefaultIdempotentKeyResolver();
    }

    /**
     * 初始化基于表达式幂等性键解析器。
     *
     * @return 基于表达式幂等性键解析器实例。
     */
    @Bean
    public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() {
        return new ExpressionIdempotentKeyResolver();
    }
}
  1. 幂等性 Redis 数据访问对象
package org.jeecg.common.idempotent;

import lombok.AllArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

import static org.jeecg.common.idempotent.RedisKeyDefine.KeyTypeEnum.STRING;

/**
 * 幂等性Redis数据访问对象,用于实现操作的幂等性。
 * 通过在Redis中设置和检查键值对,确保相同操作在重复请求时不会被多次执行。
 *
 * @author ZHANGCHAO
 * @version 1.0.0
 * @date 2023/5/15 14:36
 */
@AllArgsConstructor
public class IdempotentRedisDAO {

    /**
     * RedisKeyDefine对象,预定义了幂等键的模板和类型。
     */
    private static final RedisKeyDefine IDEMPOTENT = new RedisKeyDefine("幂等操作",
            "idempotent:%s", // 参数为 uuid
            STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);

    /**
     * Redis模板,用于操作Redis数据库。
     */
    private final StringRedisTemplate redisTemplate;

    /**
     * 格式化Redis键。
     *
     * @param key 原始键。
     * @return 格式化后的Redis键。
     */
    private static String formatKey(String key) {
        return String.format(IDEMPOTENT.getKeyTemplate(), key);
    }

    /**
     * 如果键不存在,则设置键的值并返回true;如果键已存在,则返回false。
     *
     * @param key 键的标识。
     * @param timeout 键的过期时间。
     * @param timeUnit 时间单位。
     * @return 如果键被设置,则返回true;否则返回false。
     */
    public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) {
        String redisKey = formatKey(key);
        return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit);
    }
}
  1. Redis Key 定义类,用于定义和管理 Redis 键的相关属性
package org.jeecg.common.idempotent;

import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

/**
 * Redis Key定义类
 *
 * 用于定义和管理Redis键的相关属性,如键模板、键类型、值类型、超时类型和超时时间等。
 *
 * @author ZHANGCHAO
 * @version 1.0.0
 * @date 2023/5/15 14:30
 */
@Data
public class RedisKeyDefine {

    /**
     * Redis RedisKeyDefine 数组
     */
    private static final List<RedisKeyDefine> DEFINES = new ArrayList<>();
    /**
     * Key 模板
     */
    private final String keyTemplate;
    /**
     * Key 类型的枚举
     */
    private final KeyTypeEnum keyType;
    /**
     * Value 类型
     * <p>
     * 如果是使用分布式锁,设置为 {@link java.util.concurrent.locks.Lock} 类型
     */
    private final Class<?> valueType;
    /**
     * 超时类型
     */
    private final TimeoutTypeEnum timeoutType;
    /**
     * 过期时间
     */
    private final Duration timeout;
    /**
     * 备注
     */
    private final String memo;
    private RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType,
                           TimeoutTypeEnum timeoutType, Duration timeout) {
        this.memo = memo;
        this.keyTemplate = keyTemplate;
        this.keyType = keyType;
        this.valueType = valueType;
        this.timeout = timeout;
        this.timeoutType = timeoutType;
        // 添加注册表
        add(this);
    }

    public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, Duration timeout) {
        this(memo, keyTemplate, keyType, valueType, TimeoutTypeEnum.FIXED, timeout);
    }

    public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, TimeoutTypeEnum timeoutType) {
        this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO);
    }

    public static void add(RedisKeyDefine define) {
        DEFINES.add(define);
    }

    /**
     * 格式化 Key
     * <p>
     * 注意,内部采用 {@link String#format(String, Object...)} 实现
     *
     * @param args 格式化的参数
     * @return Key
     */
    public String formatKey(Object... args) {
        return String.format(keyTemplate, args);
    }

    @Getter
    @AllArgsConstructor
    public enum KeyTypeEnum {

        STRING("String"),
        LIST("List"),
        HASH("Hash"),
        SET("Set"),
        ZSET("Sorted Set"),
        STREAM("Stream"),
        PUBSUB("Pub/Sub");

        /**
         * 类型
         */
        @JsonValue
        private final String type;

    }

    @Getter
    @AllArgsConstructor
    public enum TimeoutTypeEnum {

        FOREVER(1), // 永不超时
        DYNAMIC(2), // 动态超时
        FIXED(3); // 固定超时

        /**
         * 类型
         */
        @JsonValue
        private final Integer type;

    }
}
  1. 依赖的其他的一些工具类

CollectionUtils:

package org.jeecg.common.idempotent;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.google.common.collect.ImmutableMap;

import java.util.*;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * Collection 工具类
 */
public class CollectionUtils {

    public static boolean containsAny(Object source, Object... targets) {
        return Arrays.asList(targets).contains(source);
    }

    public static boolean isAnyEmpty(Collection<?>... collections) {
        return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty);
    }

    public static <T> List<T> filterList(Collection<T> from, Predicate<T> predicate) {
        if (CollUtil.isEmpty(from)) {
            return new ArrayList<>();
        }
        return from.stream().filter(predicate).collect(Collectors.toList());
    }

    public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper) {
        if (CollUtil.isEmpty(from)) {
            return new ArrayList<>();
        }
        return distinct(from, keyMapper, (t1, t2) -> t1);
    }

    public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper, BinaryOperator<T> cover) {
        if (CollUtil.isEmpty(from)) {
            return new ArrayList<>();
        }
        return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values());
    }

    public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func) {
        if (CollUtil.isEmpty(from)) {
            return new ArrayList<>();
        }
        return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList());
    }

    public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
        if (CollUtil.isEmpty(from)) {
            return new ArrayList<>();
        }
        return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList());
    }

    public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
        if (CollUtil.isEmpty(from)) {
            return new HashSet<>();
        }
        return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet());
    }

    public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
        if (CollUtil.isEmpty(from)) {
            return new HashSet<>();
        }
        return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
    }

    public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return convertMap(from, keyFunc, Function.identity());
    }

    public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc, Supplier<? extends Map<K, T>> supplier) {
        if (CollUtil.isEmpty(from)) {
            return supplier.get();
        }
        return convertMap(from, keyFunc, Function.identity(), supplier);
    }

    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1);
    }

    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new);
    }

    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, Supplier<? extends Map<K, V>> supplier) {
        if (CollUtil.isEmpty(from)) {
            return supplier.get();
        }
        return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier);
    }

    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction, Supplier<? extends Map<K, V>> supplier) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier));
    }

    public static <T, K> Map<K, List<T>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList())));
    }

    public static <T, K, V> Map<K, List<V>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return from.stream()
                .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList())));
    }

    // 暂时没想好名字,先以 2 结尾噶
    public static <T, K, V> Map<K, Set<V>> convertMultiMap2(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet())));
    }

    public static <T, K> Map<K, T> convertImmutableMap(Collection<T> from, Function<T, K> keyFunc) {
        if (CollUtil.isEmpty(from)) {
            return Collections.emptyMap();
        }
        ImmutableMap.Builder<K, T> builder = ImmutableMap.builder();
        from.forEach(item -> builder.put(keyFunc.apply(item), item));
        return builder.build();
    }

    public static boolean containsAny(Collection<?> source, Collection<?> candidates) {
        return org.springframework.util.CollectionUtils.containsAny(source, candidates);
    }

    public static <T> T getFirst(List<T> from) {
        return !CollectionUtil.isEmpty(from) ? from.get(0) : null;
    }

    public static <T> T findFirst(List<T> from, Predicate<T> predicate) {
        if (CollUtil.isEmpty(from)) {
            return null;
        }
        return from.stream().filter(predicate).findFirst().orElse(null);
    }

    public static <T, V extends Comparable<? super V>> V getMaxValue(List<T> from, Function<T, V> valueFunc) {
        if (CollUtil.isEmpty(from)) {
            return null;
        }
        assert from.size() > 0; // 断言,避免告警
        T t = from.stream().max(Comparator.comparing(valueFunc)).get();
        return valueFunc.apply(t);
    }

    public static <T, V extends Comparable<? super V>> V getMinValue(List<T> from, Function<T, V> valueFunc) {
        if (CollUtil.isEmpty(from)) {
            return null;
        }
        assert from.size() > 0; // 断言,避免告警
        T t = from.stream().min(Comparator.comparing(valueFunc)).get();
        return valueFunc.apply(t);
    }

    public static <T, V extends Comparable<? super V>> V getSumValue(List<T> from, Function<T, V> valueFunc, BinaryOperator<V> accumulator) {
        if (CollUtil.isEmpty(from)) {
            return null;
        }
        assert from.size() > 0; // 断言,避免告警
        return from.stream().map(valueFunc).reduce(accumulator).get();
    }

    public static <T> void addIfNotNull(Collection<T> coll, T item) {
        if (item == null) {
            return;
        }
        coll.add(item);
    }

    public static <T> Collection<T> singleton(T deptId) {
        return deptId == null ? Collections.emptyList() : Collections.singleton(deptId);
    }

}

你自己项目中的RedisConfig

package org.jeecg.common.modules.redis.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CacheConstant;
import org.jeecg.common.constant.GlobalConstants;
import org.jeecg.common.modules.redis.receiver.RedisReceiver;
import org.jeecg.common.modules.redis.writer.JeecgRedisCacheWriter;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.annotation.Resource;
import java.time.Duration;

import static java.util.Collections.singletonMap;

/**
 * 开启缓存支持
 *
 * @author zyf
 * @Return:
 */
@Slf4j
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    //不同的频道名
    //业务消息
    private static final String channel = "BusinessNews";

    @Resource
    private LettuceConnectionFactory lettuceConnectionFactory;

    /**
     * RedisTemplate配置
     *
     * @param lettuceConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        log.info(" --- redis config init --- ");
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = jacksonSerializer();
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        RedisSerializer<String> stringSerializer = new StringRedisSerializer();

        // key序列化
        redisTemplate.setKeySerializer(stringSerializer);
        // value序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // Hash key序列化
        redisTemplate.setHashKeySerializer(stringSerializer);
        // Hash value序列化
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * 缓存配置管理器
     *
     * @param factory
     * @return
     */
    @Bean
    public CacheManager cacheManager(LettuceConnectionFactory factory) {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = jacksonSerializer();
        // 配置序列化(解决乱码的问题),并且配置缓存默认有效期 6小时
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(6));
        RedisCacheConfiguration redisCacheConfiguration = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
        //.disableCachingNullValues();

        // 以锁写入的方式创建RedisCacheWriter对象
        //update-begin-author:taoyan date:20210316 for:注解CacheEvict根据key删除redis支持通配符*
        RedisCacheWriter writer = new JeecgRedisCacheWriter(factory, Duration.ofMillis(50L));
        //RedisCacheWriter.lockingRedisCacheWriter(factory);
        // 创建默认缓存配置对象
        /* 默认配置,设置缓存有效期 1小时*/
        //RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1));
        // 自定义配置test:demo 的超时时间为 5分钟
        RedisCacheManager cacheManager = RedisCacheManager.builder(writer).cacheDefaults(redisCacheConfiguration)
                .withInitialCacheConfigurations(singletonMap(CacheConstant.SYS_DICT_TABLE_CACHE,
                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10)).disableCachingNullValues()
                                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))))
                .withInitialCacheConfigurations(singletonMap(CacheConstant.TEST_DEMO_CACHE, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(5)).disableCachingNullValues()))
                .withInitialCacheConfigurations(singletonMap(CacheConstant.PLUGIN_MALL_RANKING, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(24)).disableCachingNullValues()))
                .withInitialCacheConfigurations(singletonMap(CacheConstant.PLUGIN_MALL_PAGE_LIST, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(24)).disableCachingNullValues()))
                .transactionAware().build();
        //update-end-author:taoyan date:20210316 for:注解CacheEvict根据key删除redis支持通配符*
        return cacheManager;
    }

    /**
     * redis 监听配置
     *
     * @param redisConnectionFactory redis 配置
     * @return
     */
    @Bean
    public RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory,
                                                        MessageListenerAdapter commonListenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(redisConnectionFactory);
        container.addMessageListener(commonListenerAdapter, new ChannelTopic(GlobalConstants.REDIS_TOPIC_NAME));
        //listenerAdapter的通道
//     container.addMessageListener(businessListenerAdapter, new PatternTopic(RedisConfig.channel));
        return container;
    }


    @Bean
    MessageListenerAdapter commonListenerAdapter(RedisReceiver redisReceiver) {
        MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(redisReceiver, "onMessage");
        messageListenerAdapter.setSerializer(jacksonSerializer());
        return messageListenerAdapter;
    }

//  @Bean
//  MessageListenerAdapter businessListenerAdapter(RedisReceiver redisReceiver) {
//     MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(redisReceiver, "receiveMessage");
//     messageListenerAdapter.setSerializer(jacksonSerializer());
//     return messageListenerAdapter;
//  }

    private Jackson2JsonRedisSerializer jacksonSerializer() {
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        return jackson2JsonRedisSerializer;
    }
}

最终的项目结构:

在这里插入图片描述

测试接口幂等性

在相应的需要幂等性的接口上加Idempotent注解,如:
在这里插入图片描述

这里设置的默认超时时间是 5 秒,即 5 秒内只允许相同参数的请求进来一次,前端重复点击审核按钮测试:

在这里插入图片描述

可以看到,请求已被拦截:

在这里插入图片描述

总结

我们定义了Idempotent注解,它允许我们标记方法为幂等操作,并提供了超时时间、提示信息和Key解析器等配置。然后,通过创建幂等性切面类IdempotentAspect,利用AOP在方法执行前进行幂等性校验。在实际测试中,通过在需要幂等性的接口上添加Idempotent注解,并设置适当的超时时间,可以观察到重复请求被成功拦截,证明了实现的有效性。
通过在项目中应用这些类和注解,可以有效地防止因重复请求导致的系统状态错误或数据不一致问题,从而提高系统的稳定性和可靠性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1815018.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RabbitMQ高频面试题整理

文章目录 1、RabbitMQ如何保证消息不丢失1&#xff09;confirm 消息确认机制 (生产者)2&#xff09;消息持久化机制 (RabbitMQ 服务)3&#xff09;ACK 事务机制(消费者) 2、RabbitMQ 中有哪几种交换机类型?1&#xff09; Direct Exchange2&#xff09;Fanout Exchange3&#x…

meilisearch的分页

Elasticsearch 做为老牌搜索引擎&#xff0c;功能基本满足&#xff0c;但复杂&#xff0c;重量级&#xff0c;适合大数据量。 MeiliSearch 设计目标针对数据在 500GB 左右的搜索需求&#xff0c;极快&#xff0c;单文件&#xff0c;超轻量。 所以&#xff0c;对于中小型项目来说…

细说MCU串口函数及使用printf函数实现串口发送数据的方法

目录 1、硬件及工程 2、串口相关的库函数 &#xff08;1&#xff09;串口中断服务函数&#xff1a; &#xff08;2&#xff09;串口接收回调函数&#xff1a; &#xff08;3&#xff09;串口接收中断配置函数&#xff1a; &#xff08;4&#xff09;非中断发送&#xff…

使用API有效率地管理Dynadot域名,列表形式查询已存在的文件夹信息

关于Dynadot Dynadot是通过ICANN认证的域名注册商&#xff0c;自2002年成立以来&#xff0c;服务于全球108个国家和地区的客户&#xff0c;为数以万计的客户提供简洁&#xff0c;优惠&#xff0c;安全的域名注册以及管理服务。 Dynadot平台操作教程索引&#xff08;包括域名邮…

2024年学习AI绘画是还有来得及吗?事实上看这篇就足够了aigc绘画入门基础篇

想要学好stable diffusion&#xff0c;学习资料很重要&#xff0c;本文就将常用的模型下载、提示词工具、学习资料网站进行&#xff0c;以及AI可以做的那些副业&#xff0c;汇总&#xff0c;以提升各位彦祖、亦非们的学习体验~ 一、简介 今天给大家分享Stable Diffusion模型存…

力扣42 接雨水

听说字节每人都会接雨水&#xff0c;我也要会哈哈哈 数据结构&#xff1a;数组 算法&#xff1a;核心是计算这一列接到多少雨水&#xff0c;它取决于它左边的最大值和右边的最大值&#xff0c;如下图第三根柱子能接到的雨水应该是第一根柱子高度和第五根柱子高度的最小值减去第…

DNS响应时间分析

目录 什么是DNS响应时间&#xff1f; 为什么DNS响应时间很重要&#xff1f; AnaTraf流量分析仪DNS分析 在当今数字化时代&#xff0c;网络的稳定性和性能对企业的运营至关重要。作为IT运维人员&#xff0c;我们的职责是确保网络顺畅运行&#xff0c;而DNS&#xff08;域名系…

我国喷砂机产量逐渐增长 金属加工为最大应用领域

我国喷砂机产量逐渐增长 金属加工为最大应用领域 喷砂是通过压缩空气作为动力形成高速喷射束&#xff0c;将粉状喷料高速喷射到需处理工件表面&#xff0c;使得工件外表面的外表发生变化&#xff0c;起到清理和粗化基体表面的作用。喷砂机是喷砂设备的核心组成部分&#xff0c;…

网站选择定制化的优缺点

网站定制化要明白的是&#xff0c;先有需求&#xff0c;然后在按照每一个需求去进行任务开发。 一.优点&#xff1a; 1.能够落实到每一个需求细节里面&#xff0c;可以很好的掌握需求的实现。 2.网站的所有使用权都在自己的手里&#xff0c;不需要第三方托管&#xff0…

Linux 防火墙 Firewall 和 Iptables 的使用

如果我们在Linux服务器的某个端口上运行了个服务&#xff0c;需要外网能访问到&#xff0c;就必须通过防火墙将服务运行端口给开启。Linux中有两种防火墙软件&#xff0c;CentOS7.0以上使用的是firewall&#xff0c;CentOS7.0以下使用的是iptables&#xff08;使用较少且不建议…

代码签名证书一年的价格是多少?如何申请

代码签名证书的价格因品牌、类型及所提供的服务等因素而有所不同&#xff0c;价格通常在数千元至万余元人民币之间不等。 不同类型代码签名证书价格差异 个人代码签名证书&#xff1a;个人代码签名证书是最基础的类型&#xff0c;适用于个体开发者&#xff0c;其价格较为经济…

通信原理眼图硬件实验

一、实验目的 1. 了解眼图与系统抗噪性能、码间干扰之间的关系及实际意义&#xff1b; 2. 掌握眼图观测的方法并记录研究&#xff1b; 二、实验内容 1. 观测ASK调制系统眼图并记录分析&#xff1b; 2. 观测FSK调制系统眼图并记录分析&#xff1b; 三、实验器材 1. 双踪示…

训练大模型自动在RAG和记忆间选择

现如今&#xff0c;检索增强生成(Retrieval-augmented generation&#xff0c;RAG)管道已经能够使得大语言模型(Large Language Models&#xff0c;LLM)在其响应环节中&#xff0c;充分利用外部的信息源了。不过&#xff0c;由于RAG应用会针对发送给LLM的每个请求&#xff0c;都…

RabbitMQ-Stream(高级详解)

文章目录 什么是流何时使用 RabbitMQ Stream&#xff1f;在 RabbitMQ 中使用流的其他方式基本使用Offset参数chunk Stream 插件服务端消息偏移量追踪示例 示例应用程序RabbitMQ 流 Java API概述环境创建具有所有默认值的环境使用 URI 创建环境创建具有多个 URI 的环境 启用 TLS…

C# WinForm ——31 32 Menustrip菜单栏

1. 介绍 菜单控件&#xff0c;包含多个菜单项的菜单容器 主菜单下面可以有子菜单&#xff0c;子菜单下面可以有下一级子菜单 2. 常用属性 属性解释(Name)控件ID&#xff0c;在代码里引用的时候会用到Enabled控件是否启用Dock定义要绑定到容器的控件边框&#xff0c;默认是t…

最短路:Bellman-Ford

最短路&#xff1a;Bellman-Ford 题目描述参考代码 题目描述 输入样例 3 3 1 1 2 1 2 3 1 1 3 3输出样例 3参考代码 #include <iostream> #include <cstring> #include <algorithm>using namespace std;const int N 510, M 10010;int n, m, k; int dist…

Master-Worker 架构的灰度发布难题

作者&#xff1a;石超 一、前言 Master-Worker 架构是成熟的分布式系统设计模式&#xff0c;具有集中控制、资源利用率高、容错简单等优点。我们数据中心内的几乎所有分布式系统都采用了这样的架构。 &#xfeff; 我们曾经发生过级联故障&#xff0c;造成了整个集群范围的服…

使用 C# 进行面向对象编程:第 9 部分

使用 OOP 的用户活动日志 应用程序背后的关键概念 在这一部分中&#xff0c;我们将使用之前学到的一些 OOP 概念。我们将创建一个小型应用程序。在继续之前&#xff0c;请阅读我的文章user-activity-log-using-C-Sharp-with-sql-server/。在本课程中&#xff0c;我们将再次使…

Python实现base64加密/解密

实现原理&#xff1a;导入base64库 一、加密 import base64# 加密 username "admin" base64_username base64.b64encode(username.encode(utf-8)).decode() print(base64_username) password "123" base64_password base64.b64encode(password.encod…

Vue23-过滤器

一、效果图 二、好用的时间戳三方工具 该三方工具比较大 推荐使用 dayjs的用法&#xff1a; 三、过滤器的使用 3-1、计算属性实现 3-2、methods函数实现 3-3、过滤器filters属性实现 过滤器的本质就是函数&#xff01;&#xff01;&#xff01; 1、过滤器-未传参 默认将管道…