接口幂等性,源自数学中的幂运算性质,意指一个操作无论执行一次还是多次,对系统状态的影响均等同于执行一次的效果。在编程和API设计语境中,幂等性意味着对同一接口发起的多次相同请求,不论请求次数如何,其最终结果都是确定的且对系统资源状态的改变是一致的。
接口幂等性的价值主要体现在以下几个方面:
容错与重试机制
: 在网络环境不稳定或系统短暂故障时,客户端的请求可能丢失、超时或重复发送。幂等接口允许客户端在未收到明确响应或响应不可信时,安全地重新发起请求,而不必担心导致数据不一致或资源的重复消耗。例如,支付系统中的扣款操作,即使因网络问题重复发送扣款请求,幂等设计确保只扣除一次费用,避免用户资金被重复扣减。
系统一致性维护
: 在分布式环境中,系统间的通信延迟、节点故障等情况可能导致操作的重复执行。幂等接口确保即使在这些异常情况下,系统状态也能保持一致,不会因为重复请求而出现逻辑混乱或数据冲突。
安全性提升
: 防止恶意攻击或用户误操作导致的资源滥用。例如,用户无意中连续点击“提交订单”按钮,幂等设计能确保仅创建一份有效的订单,而不是生成多个重复订单。
实现接口幂等性通常采用以下几种策略:
服务端提供一个获取token的接口。在执行操作之前,先去访问token接口来获取一个token,该token会被存储到Redis中。在发起操作请求的时候带上token,在执行操作逻辑之前,先判断token存在,如果存在,先删除token则执行操作逻辑;否则说明同样的请求已经被处理过,不再执行处理逻辑
个人看法
:不好保证删除token和执行业务的原子性,本人还没有找到可行的方案
在更新资源时,使用乐观锁机制,通过比较在更新资源的时候比较版本号来判断操作是否为重复执行。若版本号不匹配(即资源已被其他操作更新),则拒绝此次更新请求。
个人看法
:需要对数据库进行改造,比较麻烦,除非一开始在设计数据库的时候就考虑
通过获取方法参数或使用SpEL表达式来生成分布式锁的键,在执行业务之前获取分布式锁,获取锁成功才可以执行业务,业务执行完毕删除锁,保证同一时间同一个请求只能处理一次
类似上面的唯一约束,只是单独用一个表来存储响应标识,并对该表示设置唯一约束。能插入标识说明是第一次执行
import com.dam.enums.IdempotentSceneEnum;
import com.dam.enums.IdempotentTypeEnum;
import java.lang.annotation.*;
/**
* 幂等注解
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
/**
* 幂等Key,只有在 {@link Idempotent#type()} 为 {@link IdempotentTypeEnum#SPEL} 时生效
*/
String key() default "";
/**
* 触发幂等失败逻辑时,返回的错误提示信息
*/
String message() default "您操作太快,请稍后再试";
/**
* 验证幂等类型,支持多种幂等方式
* RestAPI 建议使用 {@link IdempotentTypeEnum#TOKEN} 或 {@link IdempotentTypeEnum#PARAM}
* 其它类型幂等验证,使用 {@link IdempotentTypeEnum#SPEL}
*/
IdempotentTypeEnum type() default IdempotentTypeEnum.PARAM;
/**
* 验证幂等场景,支持多种 {@link IdempotentSceneEnum}
*/
IdempotentSceneEnum scene() default IdempotentSceneEnum.RESTAPI;
/**
* 设置防重令牌 Key 前缀,MQ 幂等去重可选设置
* {@link IdempotentSceneEnum#MQ} and {@link IdempotentTypeEnum#SPEL} 时生效
*/
String uniqueKeyPrefix() default "";
/**
* 设置防重令牌 Key 过期时间,单位秒,默认 1 小时,MQ 幂等去重可选设置
* {@link IdempotentSceneEnum#MQ} and {@link IdempotentTypeEnum#SPEL} 时生效
*/
long keyTimeout() default 3600L;
}
注意,接口定义了两个空方法,因为不需要所有处理器都需要处理那两个方法。该接口默认实现了execute方法,即每个处理器的共同之处都是先调用buildParam构建包装参数,再调用handler方法来进行幂等逻辑判断,
import com.dam.annotation.Idempotent;
import com.dam.core.aop.IdempotentParam;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 幂等执行处理器
*
*/
public interface IdempotentExecuteHandler {
IdempotentParam buildParam(ProceedingJoinPoint joinPoint);
/**
* 幂等处理逻辑
*
* @param wrapper 幂等参数包装器
*/
void handler(IdempotentParam wrapper);
/**
* 执行幂等处理逻辑
*
* @param joinPoint AOP 方法处理
* @param idempotent 幂等注解
*/
default void execute(ProceedingJoinPoint joinPoint, Idempotent idempotent){
// 模板方法模式:构建幂等参数包装器
IdempotentParam idempotentParam = buildParam(joinPoint).setIdempotent(idempotent);
// 如果不满足幂等,handler方法会通过抛出异常来使下面的程序被中断
handler(idempotentParam);
};
/**
* 幂等异常流程处理
*/
default void exceptionProcessing() {
}
/**
* 执行目标方法成功的后置处理
*/
default void postProcessing() {
}
}
import com.dam.core.handler.IdempotentExecuteHandler;
/**
* Token 实现幂等接口
*
*/
public interface IdempotentTokenService extends IdempotentExecuteHandler {
/**
* 创建幂等验证Token
*/
String createToken();
}
使用token机制,在发送具体请求之前,需要先发送一个前置请求来向服务端获取token,因此需要开发一个Controller供前端使用
import com.dam.constant.KeyConstants;
import com.dam.model.result.R;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 基于 Token 验证请求幂等性控制器
*/
@RestController
@RequiredArgsConstructor
public class IdempotentTokenController {
private final IdempotentTokenService idempotentTokenService;
/**
* 请求申请Token
*/
@GetMapping("/token")
public R createToken() {
return R.ok().addData(KeyConstants.TOKEN_KEY, idempotentTokenService.createToken());
}
}
import cn.hutool.core.util.StrUtil;
import com.dam.constant.KeyConstants;
import com.dam.core.aop.IdempotentParam;
import com.dam.exception.SSSException;
import com.dam.model.enums.ResultCodeEnum;
import com.dam.properties.IdempotentTokenProperties;
import com.google.common.base.Strings;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Optional;
import java.util.UUID;
/**
* 基于 Token 验证请求幂等性, 通常应用于 RestAPI 方法
*/
@RequiredArgsConstructor
public final class IdempotentTokenExecuteHandler implements IdempotentTokenService {
/**
* Redis 操作模板,用于存储和验证幂等Token
*/
private final StringRedisTemplate redisTemplate;
/**
* 幂等相关配置属性
*/
private final IdempotentTokenProperties idempotentTokenProperties;
/**
* 如果配置文件里面没有写明,则使用该默认值
*/
private static final String TOKEN_PREFIX_KEY = KeyConstants.IDEMPOTENT_PREFIX + "token:";
/**
* 幂等Token在Redis中的默认过期时间(毫秒),若配置文件未指定,则使用此值
*/
private static final long TOKEN_EXPIRED_TIME = 6000;
@Override
public IdempotentParam buildParam(ProceedingJoinPoint joinPoint) {
return new IdempotentParam();
}
/**
* 创建一个新的幂等Token。生成一个全局唯一的UUID,并添加前缀(从配置或默认值获取),
* 将此Token作为键存入Redis,并设置一个过期时间(从配置或默认值获取)。
* 值为空字符串,因为仅用于标识Token的存在,不存储额外数据。
*
* @return 新创建的幂等Token字符串
*/
@Override
public String createToken() {
String token = Optional.ofNullable(Strings.emptyToNull(idempotentTokenProperties.getPrefix())).orElse(TOKEN_PREFIX_KEY) + UUID.randomUUID();
redisTemplate.opsForValue().set(token, "", Optional.ofNullable(idempotentTokenProperties.getTimeout()).orElse(TOKEN_EXPIRED_TIME));
return token;
}
/**
* 处理幂等Token验证逻辑。在接收到业务请求时,从请求头或请求参数中提取幂等Token,
* 然后验证该Token在Redis中的存在性。如果存在,则删除该Token并允许业务逻辑执行;
* 否则,抛出异常表示重复请求或无效Token。
*
* @param wrapper 幂等参数包装器
*/
@Override
public void handler(IdempotentParam wrapper) {
// 获取当前请求上下文中的HttpServletRequest对象
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
// 从请求头或者请求参数中获取token对应的值
String token = request.getHeader(KeyConstants.TOKEN_KEY);
if (StrUtil.isBlank(token)) {
token = request.getParameter(KeyConstants.TOKEN_KEY);
if (StrUtil.isBlank(token)) {
// 如果请求中未找到Token,抛出异常
throw new SSSException(ResultCodeEnum.IDEMPOTENT_TOKEN_NULL_ERROR);
}
}
// 验证并删除Token
Boolean tokenDelFlag = redisTemplate.delete(token);
if (!tokenDelFlag) {
// 删除失败,说明token对应的请求已经被执行过了,不能再执行了
String errMsg = StrUtil.isNotBlank(wrapper.getIdempotent().message())
? wrapper.getIdempotent().message()
: ResultCodeEnum.IDEMPOTENT_TOKEN_DELETE_ERROR.getMessage();
throw new SSSException(ResultCodeEnum.IDEMPOTENT_TOKEN_DELETE_ERROR.getCode(), errMsg);
}
}
}
注意,该Service继承接口IdempotentExecuteHandler
,多写一层接口是为了定义一些属于自己的方法
import com.dam.core.handler.IdempotentExecuteHandler;
/**
* 参数方式幂等实现接口
*
*/
public interface IdempotentParamService extends IdempotentExecuteHandler {
}
幂等性判断逻辑:将包含请求路径
、当前用户ID
和参数MD5值
组合为key来做分布式锁,如果请求能加锁成功,说明是第一次执行,执行结束之后解锁。使用该方式可以保证同一时刻,同样的请求只有一个在执行。
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.alibaba.fastjson2.JSON;
import com.dam.constant.KeyConstants;
import com.dam.context.UserContext;
import com.dam.core.IdempotentContext;
import com.dam.core.aop.IdempotentParam;
import com.dam.exception.SSSException;
import com.dam.model.enums.ResultCodeEnum;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 基于方法参数验证请求幂等性
* 基于分布式锁实现
*/
@RequiredArgsConstructor//使用构造器注入
public final class IdempotentParamExecuteHandler implements IdempotentParamService {
/**
* 注入Redisson客户端,用于操作分布式锁
*/
private final RedissonClient redissonClient;
/**
* 获取到的分布式锁对应的key,用来给上下文使用
*/
private final static String CONTEXT_LOCK_KEY = KeyConstants.IDEMPOTENT_PREFIX + "lock:param:restAPI";
@Override
public IdempotentParam buildParam(ProceedingJoinPoint joinPoint) {
// 构建分布式锁的唯一key,包含请求路径、当前用户ID和参数MD5值
String lockKey = String.format(KeyConstants.IDEMPOTENT_PREFIX + "lock:param:path:%s:currentUserId:%s:md5:%s", getServletPath(), getCurrentUserId(), calcArgsMD5(joinPoint));
return IdempotentParam.builder().lockKey(lockKey).joinPoint(joinPoint).build();
}
/**
* 幂等逻辑判断,如果获取分布式锁失败,说明同样的请求和参数已经在执行,返回异常
*
* @param wrapper 幂等参数包装器
*/
@Override
public void handler(IdempotentParam wrapper) {
// 从包装器中获取分布式锁对应的key
String lockKey = wrapper.getLockKey();
// 通过RedissonClient获取分布式锁实例
RLock lock = redissonClient.getLock(lockKey);
// 尝试获取锁,如果无法立即获取(即锁已被其他请求持有),则抛出异常,表示请求重复
if (!lock.tryLock()) {
throw new SSSException(ResultCodeEnum.FAIL.getCode(), wrapper.getIdempotent().message());
}
// 将获取到的锁存储在IdempotentContext中,以便后续解锁操作
IdempotentContext.put(CONTEXT_LOCK_KEY, lock);
}
/**
* 将分布式锁进行解锁
*/
@Override
public void postProcessing() {
RLock lock = null;
try {
// 从IdempotentContext中获取之前存储的锁实例
lock = (RLock) IdempotentContext.getKey(CONTEXT_LOCK_KEY);
} finally {
if (lock != null) {
lock.unlock();
}
}
}
/**
* 获取当前线程上下文中的ServletPath(请求路径)
*
* @return 当前线程上下文 ServletPath
*/
private String getServletPath() {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
// 从ServletAttributes中获取请求的ServletPath
return sra.getRequest().getServletPath();
}
/**
* 获取当前操作用户的ID
*
* @return 当前操作用户 ID
*/
private String getCurrentUserId() {
// 从UserContext中获取当前用户的ID
String userId = UserContext.getUserId();
if (StrUtil.isBlank(userId)) {
throw new SSSException(ResultCodeEnum.FAIL.getCode(), "用户ID获取失败,请登录");
}
return userId;
}
/**
* 计算参数的MD5码
*
* @param joinPoint 包含方法参数的ProceedingJoinPoint对象
* @return 参数的MD5值,用于标识请求参数的唯一性
*/
private String calcArgsMD5(ProceedingJoinPoint joinPoint) {
String md5 = DigestUtil.md5Hex(JSON.toJSONBytes(joinPoint.getArgs()));
return md5;
}
}
import com.dam.core.handler.IdempotentExecuteHandler;
/**
* SpEL 方式幂等实现接口
*
*/
public interface IdempotentSpELService extends IdempotentExecuteHandler {
}
import com.dam.annotation.Idempotent;
import com.dam.constant.KeyConstants;
import com.dam.core.IdempotentContext;
import com.dam.core.aop.IdempotentAspect;
import com.dam.core.aop.IdempotentParam;
import com.dam.core.handler.spel.IdempotentSpELService;
import com.dam.exception.SSSException;
import com.dam.model.enums.ResultCodeEnum;
import com.dam.toolkit.SpELUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
/**
* 基于 SpEL 方法验证请求幂等性,适用于 RestAPI(Restful) 场景
* 基于分布式锁实现
*/
@RequiredArgsConstructor
public final class IdempotentSpELByRestAPIExecuteHandler implements IdempotentSpELService {
/**
* Redisson 客户端,用于操作分布式锁
*/
private final RedissonClient redissonClient;
/**
* 分布式锁的基础键名,用于存储全局唯一标识的锁
*/
private final static String LOCK = KeyConstants.IDEMPOTENT_PREFIX + "lock:spEL:restAPI";
/**
* 构建幂等参数包装器,通过解析 SpEL 表达式生成请求的唯一标识(锁键)
*
* @param joinPoint 切点对象,包含目标方法信息及参数
* @return 构建好的幂等参数包装器
*/
@SneakyThrows
@Override
public IdempotentParam buildParam(ProceedingJoinPoint joinPoint) {
// 从切点对象中获取方法上的 @Idempotent 注解
Idempotent idempotent = IdempotentAspect.getIdempotent(joinPoint);
// 使用 SpEL 工具解析注解中的 key 属性表达式,生成请求的唯一标识(锁键)
String lockKey = (String) SpELUtil.parseKey(idempotent.key(), ((MethodSignature) joinPoint.getSignature()).getMethod(), joinPoint.getArgs());
return IdempotentParam.builder().lockKey(lockKey).joinPoint(joinPoint).build();
}
/**
* 幂等性逻辑处理。尝试获取分布式锁,如果获取失败(即锁已被其他请求持有),说明有相同方法和参数的请求正在执行,
* 此时抛出异常,拒绝当前请求。
*
* @param wrapper 幂等参数包装器
*/
@Override
public void handler(IdempotentParam wrapper) {
// System.out.println("wrapper.getLockKey():" + wrapper.getLockKey());
String uniqueKey = KeyConstants.IDEMPOTENT_PREFIX + wrapper.getIdempotent().uniqueKeyPrefix() + wrapper.getLockKey();
RLock lock = redissonClient.getLock(uniqueKey);
// 尝试获取锁,如果无法立即获取(即锁已被其他请求持有),抛出异常,表示请求重复
if (!lock.tryLock()) {
// System.out.println("wrapper.getIdempotent().message():" + wrapper.getIdempotent().message());
throw new SSSException(ResultCodeEnum.FAIL.getCode(), wrapper.getIdempotent().message());
}
// 上下文用来传递分布式锁,便于请求处理完成之后进行解锁
IdempotentContext.put(LOCK, lock);
}
/**
* 后处理,对分布式锁进行解锁
*/
@Override
public void postProcessing() {
// 从 IdempotentContext 中获取之前存储的锁实例
RLock lock = null;
try {
lock = (RLock) IdempotentContext.getKey(LOCK);
} finally {
// 如果锁实例不为空,进行解锁操作
if (lock != null) {
lock.unlock();
}
}
}
/**
* 请求的方法执行过程中发生了异常,也对分布式锁进行解锁
*/
@Override
public void exceptionProcessing() {
// 从 IdempotentContext 中获取之前存储的锁实例
RLock lock = null;
try {
lock = (RLock) IdempotentContext.getKey(LOCK);
} finally {
// 如果锁实例不为空,进行解锁操作
if (lock != null) {
lock.unlock();
}
}
}
}
import cn.hutool.core.util.ArrayUtil;
import com.google.common.collect.Lists;
import org.springframework.core.DefaultParameterNameDiscoverer;
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;
import java.util.ArrayList;
import java.util.Optional;
/**
* SpEL 表达式解析工具
*/
public class SpELUtil {
/**
* 校验并返回实际使用的 SpEL 表达式
*
* @param spEl SpEL 表达式字符串
* @param method 目标方法对象
* @param contextObj 目标方法的参数数组
* @return 如果传入的 SpEL 表达式包含特定符号(如 "#" 或 "T("),则解析并返回其实际值;否则直接返回传入的 SpEL 表达式字符串
*/
public static Object parseKey(String spEl, Method method, Object[] contextObj) {
// 定义一个列表,存储 SpEL 表达式可能包含的特殊标志符
ArrayList<String> spELFlag = Lists.newArrayList("#", "T(");
// 查找传入 SpEL 表达式是否包含这些特殊标志符中的任意一个
Optional<String> optional = spELFlag.stream().filter(spEl::contains).findFirst();
// 如果找到,则需要解析 SpEL 表达式
if (optional.isPresent()) {
// 调用 parse 方法解析 SpEL 表达式,并返回解析后的值
Object parse = parse(spEl, method, contextObj);
return parse;
}
// 如果未找到特殊标志符,直接返回传入的 SpEL 表达式字符串
return spEl;
}
/**
* 转换参数为字符串
*
* @param spEl spEl 表达式
* @param contextObj 上下文对象
* @return 解析的字符串值
*/
public static Object parse(String spEl, Method method, Object[] contextObj) {
DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(spEl);
String[] params = discoverer.getParameterNames(method);
// System.out.println("contextObj:" + JSON.toJSONString(contextObj));
StandardEvaluationContext context = new StandardEvaluationContext();
if (ArrayUtil.isNotEmpty(params)) {
for (int len = 0; len < params.length; len++) {
context.setVariable(params[len], contextObj[len]);
}
}
Object value = exp.getValue(context);
return value;
// String md5Hex = DigestUtil.md5Hex(JSON.toJSONString(contextObj));
// System.out.println("md5Hex:" + md5Hex);
// return md5Hex;
}
}
简单工厂模式提供专门的工厂类用于创建对象,实现了对象创建和使用的职责分离,客户端不需知道所创建的具体产品类的类名以及创建过程(在这里不需要创建,只需要从容器里面获取相应的Bean即可),只需知道具体产品类所对应的参数即可,通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
import com.dam.ApplicationContextHolder;
import com.dam.core.handler.param.IdempotentParamService;
import com.dam.core.handler.spel.mq.IdempotentSpELByMQExecuteHandler;
import com.dam.core.handler.spel.restful.IdempotentSpELByRestAPIExecuteHandler;
import com.dam.core.handler.token.IdempotentTokenService;
import com.dam.enums.IdempotentSceneEnum;
import com.dam.enums.IdempotentTypeEnum;
/**
* 幂等执行处理器工厂
* 简单工厂模式
*/
public final class IdempotentExecuteHandlerFactory {
/**
* 根据枚举参数获取对应的幂等执行处理器handler
*
* @param scene 指定幂等验证场景类型
* @param type 指定幂等处理类型
* @return 幂等执行处理器
*/
public static IdempotentExecuteHandler getInstance(IdempotentSceneEnum scene, IdempotentTypeEnum type) {
IdempotentExecuteHandler result = null;
switch (scene) {
case RESTAPI: {
switch (type) {
case PARAM:
result = ApplicationContextHolder.getBean(IdempotentParamService.class);
break;
case TOKEN:
result = ApplicationContextHolder.getBean(IdempotentTokenService.class);
break;
case SPEL:
result = ApplicationContextHolder.getBean(IdempotentSpELByRestAPIExecuteHandler.class);
break;
default: {
}
}
break;
}
case MQ:
result = ApplicationContextHolder.getBean(IdempotentSpELByMQExecuteHandler.class);
break;
default: {
}
}
return result;
}
}
import com.dam.annotation.Idempotent;
import com.dam.enums.IdempotentTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 幂等参数包装
*
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public final class IdempotentParam {
/**
* 幂等注解
*/
private Idempotent idempotent;
/**
* AOP 处理连接点
*/
private ProceedingJoinPoint joinPoint;
/**
* 锁标识,{@link IdempotentTypeEnum#PARAM}
*/
private String lockKey;
}
import com.dam.annotation.Idempotent;
import com.dam.core.IdempotentContext;
import com.dam.core.exception.RepeatConsumptionException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;
/**
* 幂等注解 AOP 拦截器
*/
@Aspect
public final class IdempotentAspect {
/**
* 使用Around来对Idempotent注解标记的方法进行环绕增强
* @param joinPoint 使用 @Around ,自定义的切入点
* @return
* @throws Throwable
*/
@Around("@annotation(com.dam.annotation.Idempotent)")
public Object idempotentHandler(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取注解信息
Idempotent idempotent = getIdempotent(joinPoint);
// 根据注解来获取相应的处理器
IdempotentExecuteHandler instance = IdempotentExecuteHandlerFactory.getInstance(idempotent.scene(), idempotent.type());
// 存储实际方法的执行结果
Object resultObj;
try {
// 当不满足幂等时,execute会报错,后面的代码不会再执行
instance.execute(joinPoint, idempotent);
// 请求的具体方法执行
resultObj = joinPoint.proceed();
// 处理器进行后处理,如解除分布式锁
instance.postProcessing();
} finally {
IdempotentContext.clean();
}
return resultObj;
}
/**
* 从给定的ProceedingJoinPoint中获取目标方法上的Idempotent注解实例
*
* @param joinPoint 切点对象,包含方法签名等信息
* @return 目标方法上的Idempotent注解实例
* @throws NoSuchMethodException 如果找不到对应方法时抛出此异常
*/
public static Idempotent getIdempotent(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
// 获取方法签名对象,包含方法名、参数类型等信息
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 通过反射获取目标类(切面所拦截的对象)上与当前方法签名匹配的Method对象
Method targetMethod = joinPoint.getTarget().getClass().getDeclaredMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes());
// 从Method对象上获取Idempotent注解实例
return targetMethod.getAnnotation(Idempotent.class);
}
}
将需要的Bean注入到容器中
import com.dam.core.aop.IdempotentAspect;
import com.dam.core.handler.param.IdempotentParamExecuteHandler;
import com.dam.core.handler.param.IdempotentParamService;
import com.dam.core.handler.spel.IdempotentSpELService;
import com.dam.core.handler.spel.mq.IdempotentSpELByMQExecuteHandler;
import com.dam.core.handler.spel.restful.IdempotentSpELByRestAPIExecuteHandler;
import com.dam.core.handler.token.IdempotentTokenExecuteHandler;
import com.dam.core.handler.token.IdempotentTokenService;
import com.dam.properties.IdempotentTokenProperties;
import org.redisson.api.RedissonClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* 幂等自动装配
*/
@EnableConfigurationProperties(IdempotentTokenProperties.class)
@Configuration
public class IdempotentAutoConfiguration {
/**
* 幂等切面
*/
@Bean
public IdempotentAspect idempotentAspect() {
return new IdempotentAspect();
}
/**
* 参数方式幂等实现,基于 RestAPI 场景
*/
@Bean
@ConditionalOnMissingBean
public IdempotentParamService idempotentParamExecuteHandler(RedissonClient redissonClient) {
return new IdempotentParamExecuteHandler(redissonClient);
}
/**
* Token 方式幂等实现,基于 RestAPI 场景
*/
@Bean
@ConditionalOnMissingBean
public IdempotentTokenService idempotentTokenExecuteHandler(StringRedisTemplate distributedCache,
IdempotentTokenProperties idempotentTokenProperties) {
return new IdempotentTokenExecuteHandler(distributedCache, idempotentTokenProperties);
}
/**
* 申请幂等 Token 控制器,基于 RestAPI 场景
*/
// @Bean
// public IdempotentTokenController idempotentTokenController(IdempotentTokenService idempotentTokenService) {
// return new IdempotentTokenController(idempotentTokenService);
// }
/**
* SpEL 方式幂等实现,基于 RestAPI 场景
*/
@Bean
@ConditionalOnMissingBean
public IdempotentSpELService idempotentSpELByRestAPIExecuteHandler(RedissonClient redissonClient) {
return new IdempotentSpELByRestAPIExecuteHandler(redissonClient);
}
/**
* SpEL 方式幂等实现,基于 MQ 场景
*/
@Bean
@ConditionalOnMissingBean
public IdempotentSpELByMQExecuteHandler idempotentSpELByMQExecuteHandler(StringRedisTemplate distributedCache) {
return new IdempotentSpELByMQExecuteHandler(distributedCache);
}
}
在方法上面添加@Idempotent注解,案例如下:
想要自定义分布式锁的键,可以给key设置不同的SpEL表达式
/**
* 修改
*/
@PostMapping("/update")
@PreAuthorize("hasAnyAuthority('bnt.user.update','bnt.storeUser.update')")
@Idempotent(
uniqueKeyPrefix = "sss-system-server:lock_userInfo_update:",
key = "T(com.dam.context.UserContext).getUsername()",
message = "正在执行用户信息修改流程,请稍后...",
scene = IdempotentSceneEnum.RESTAPI,
type = IdempotentTypeEnum.SPEL
)
public R update(@RequestBody UserEntity user) {
boolean b = userService.updateById(user);
return R.ok();
}
@Idempotent(
message = "正在执行用户信息修改流程,请稍后...",
scene = IdempotentSceneEnum.RESTAPI,
type = IdempotentTypeEnum.PARAM
)
本文代码来源于马哥 12306 的代码,本人只是根据自己的理解进行少量修改并应用到智能排班系统中。代码仓库为12306,该项目含金量较高,有兴趣的朋友们建议去学习一下。
更多【php-【智能排班系统】基于AOP和自定义注解实现接口幂等性】相关视频教程:www.yxfzedu.com