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

devtools/2024/9/22 17:51:08/

接口的幂等性是指在分布式系统中,一个操作或者请求无论执行多少次,其结果都是相同的。换句话说,即使多次执行同一个操作,它也不会产生副作用,或者不会改变系统的状态。幂等性是设计 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) {// 根据注解中指定的幂等键解析器类名,获取对应的幂等键解析器// 获得 IdempotentKeyResolverIdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver());// 确保幂等键解析器不为空,否则抛出异常Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver");// 使用幂等键解析器解析出请求的幂等// 解析 KeyString 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*/@Overridepublic 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。*/@Overridepublic 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 初始化后的幂等性切面实例。*/@Beanpublic IdempotentAspect idempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {return new IdempotentAspect(keyResolvers, idempotentRedisDAO);}/*** 初始化幂等性Redis DAO。** @param stringRedisTemplate 字符串Redis模板,用于操作Redis。* @return 初始化后的幂等性Redis DAO实例。*/@Beanpublic IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) {return new IdempotentRedisDAO(stringRedisTemplate);}// ========== 各种 IdempotentKeyResolver Bean ==========/*** 初始化默认幂等性键解析器。** @return 默认幂等性键解析器实例。*/@Beanpublic DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() {return new DefaultIdempotentKeyResolver();}/*** 初始化基于表达式幂等性键解析器。** @return 基于表达式幂等性键解析器实例。*/@Beanpublic 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", // 参数为 uuidSTRING, 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@AllArgsConstructorpublic enum KeyTypeEnum {STRING("String"),LIST("List"),HASH("Hash"),SET("Set"),ZSET("Sorted Set"),STREAM("Stream"),PUBSUB("Pub/Sub");/*** 类型*/@JsonValueprivate final String type;}@Getter@AllArgsConstructorpublic enum TimeoutTypeEnum {FOREVER(1), // 永不超时DYNAMIC(2), // 动态超时FIXED(3); // 固定超时/*** 类型*/@JsonValueprivate 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";@Resourceprivate LettuceConnectionFactory lettuceConnectionFactory;/*** RedisTemplate配置** @param lettuceConnectionFactory* @return*/@Beanpublic 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*/@Beanpublic 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*/@Beanpublic 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;}@BeanMessageListenerAdapter 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.ppmy.cn/devtools/53739.html

相关文章

Zookeeper 集群节点故障剔除、切换、恢复原理

Zookeeper 集群节点故障剔除、切换、恢复原理 zookeeper 集群节点故障时,如何剔除节点,如果为领导节点如何处理,如何进行故障恢 复的,实现原理? 在 Zookeeper 集群中,当节点故障时,集群需要自动剔除故障节点并进行故障恢复,确保集群的高 可用性和一致性。具体来说,…

112、路径总和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;这条路径上所有节点值相加等于目标和 targetSum 。如果存在&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 叶子节点 是指没有子节点…

【退役之重学 AI】Ubuntu 安装 Anaconda

一. 下载 安装文件 https://www.anaconda.com/download/success 二. 安装 bash anaconda****.bash 一路 enter&#xff0c;yes 最后一个问你 要不要 conda init&#xff0c;这里得输入yes&#xff08;默认是no&#xff0c;所以不要直接 enter&#xff09;&#xff0c;否则你…

html和css创建一个简单的网页

html代码及解析 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>CSS Example</title><lin…

【数学建模】——【新手小白到国奖选手】——【学习路线】

专栏&#xff1a;数学建模学习笔记 目录 ​编辑 第一阶段&#xff1a;基础知识和工具 1.Python基础 1.学习内容 1.基本语法 2.函数和模块 3.面向对象编程 4.文件操作 2.推荐资源 书籍&#xff1a; 在线课程&#xff1a; 在线教程&#xff1a; 2.数学基础 1.学习内…

BGP简介

BGP 的概念定义 BGP 的概念&#xff1a; 边界网关协议&#xff08;Border Gateway Protocol&#xff0c;BGP&#xff09; 一种用于在不同自治系统&#xff08;AS&#xff09;之间交换路由信息的动态路由协议。互联网的核心路由协议之一&#xff0c;确保了全球网络的互联性和…

模拟原神圣遗物系统-小森设计项目,设计圣遗物(生之花,死之羽,时之沙,空之杯,理之冠)抽象类

分析圣遗物 在圣遗物系统&#xff0c;玩家操控的是圣遗物的部分 因此我们应该 物以类聚 人与群分把每个圣遗物的部分&#xff0c;抽象出来 拿 生之花&#xff0c;死之羽为例 若是抽象 类很好的扩展 添加冒险家的生之花 时候继承生之花 并且名称冒险者- 生之花 当然圣遗物包含…

【机器学习】机器的登神长阶——AIGC

目录 什么是AIGC 普通用户接触AIGC网站推荐 通义千问 白马 普通用户如何用好AIGC 关键提示词的作用 AIGC的影响 就业市场&#xff1a; 教育领域&#xff1a; 创意产业&#xff1a; 经济活动&#xff1a; 社交媒体与信息传播&#xff1a; AIGC面临的挑战 什么是AIGC…