接口的幂等性是指在分布式系统中,一个操作或者请求无论执行多少次,其结果都是相同的。换句话说,即使多次执行同一个操作,它也不会产生副作用,或者不会改变系统的状态。幂等性是设计 RESTful API 时的一个重要原则。
幂等性通常适用于以下两种情况:
- 安全操作: 例如,GET 请求用于获取资源,不论执行多少次,都不会改变资源的状态,因此是幂等的。
- 状态改变操作: 例如,PUT 请求用于更新资源,如果资源已经处于请求中描述的状态,再次执行相同的 PUT 请求不会对资源造成进一步的改变,因此也是幂等的。
幂等性对于确保分布式系统的一致性和可靠性非常重要,特别是在网络请求可能会因为各种原因被重复发送的情况下。例如,如果一个用户提交了一个表单,但由于网络问题,表单被提交了两次,幂等性可以保证系统不会因为重复的提交而产生错误的状态或数据。
如何实现幂等性?
前端控制,在前端做拦截,比如按钮点击一次之后就置灰或者隐藏。但是往往前端并不可靠,还是得后端处理才更放心。
现在我们在后端通过一个注解Idempotent
来一步步实现接口的幂等性。
- 首先定义一个幂等注解
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 "";
}
- 定义幂等性切面类,用于对标注了
{@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());
}
}
}
- 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);
}
- 定义两个 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);
}
}
- 幂等性配置类,用于初始化幂等性相关的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();
}
}
- 幂等性 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);
}
}
- 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;
}
}
- 依赖的其他的一些工具类
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
注解,并设置适当的超时时间,可以观察到重复请求被成功拦截,证明了实现的有效性。
通过在项目中应用这些类和注解,可以有效地防止因重复请求导致的系统状态错误或数据不一致问题,从而提高系统的稳定性和可靠性。