文章目录
- 接口幂等性介绍
- 为何重视接口幂等性
- 幂等性实现策略
- token机制
- 存在问题: 先删除token还是执行完业务再删除
- 乐观锁与版本号
- 分布式锁
- 唯一约束
- 防重表
- 实现
- 定义注解
- handler处理器
- handler接口定义
- 接口实现:基于token机制
- Service
- Controller
- 实现
- 接口实现:参数param作为分布式锁的键
- Service
- 实现
- 接口实现:SPEL表达式的值作为分布式锁的键
- Service
- 实现
- 工具类
- handler工厂
- AOP
- 幂等相关参数包装
- 切面
- 配置类
- 使用
- SpEL方式
- param方式
- 说明
接口幂等性介绍
接口幂等性,源自数学中的幂运算性质,意指一个操作无论执行一次还是多次,对系统状态的影响均等同于执行一次的效果。在编程和API设计语境中,幂等性意味着对同一接口发起的多次相同请求,不论请求次数如何,其最终结果都是确定的且对系统资源状态的改变是一致的。
为何重视接口幂等性
接口幂等性的价值主要体现在以下几个方面:
-
容错与重试机制
: 在网络环境不稳定或系统短暂故障时,客户端的请求可能丢失、超时或重复发送。幂等接口允许客户端在未收到明确响应或响应不可信时,安全地重新发起请求,而不必担心导致数据不一致或资源的重复消耗。例如,支付系统中的扣款操作,即使因网络问题重复发送扣款请求,幂等设计确保只扣除一次费用,避免用户资金被重复扣减。 -
系统一致性维护
: 在分布式环境中,系统间的通信延迟、节点故障等情况可能导致操作的重复执行。幂等接口确保即使在这些异常情况下,系统状态也能保持一致,不会因为重复请求而出现逻辑混乱或数据冲突。 -
安全性提升
: 防止恶意攻击或用户误操作导致的资源滥用。例如,用户无意中连续点击“提交订单”按钮,幂等设计能确保仅创建一份有效的订单,而不是生成多个重复订单。
幂等性实现策略
实现接口幂等性通常采用以下几种策略:
token机制
服务端提供一个获取token的接口。在执行操作之前,先去访问token接口来获取一个token,该token会被存储到Redis中。在发起操作请求的时候带上token,在执行操作逻辑之前,先判断token存在,如果存在,先删除token则执行操作逻辑;否则说明同样的请求已经被处理过,不再执行处理逻辑
存在问题: 先删除token还是执行完业务再删除
- 先删除:可能导致业务由于服务器宕机没有执行,重试还带上之前token,由于防重设计,请求不能再执行
- 后删除:业务处理成功,但是服务闪断,没有成功删除token。发起同样的请求,导致业务被执行两遍
个人看法
:不好保证删除token和执行业务的原子性,本人还没有找到可行的方案
乐观锁与版本号
在更新资源时,使用乐观锁机制,通过比较在更新资源的时候比较版本号来判断操作是否为重复执行。若版本号不匹配(即资源已被其他操作更新),则拒绝此次更新请求。
个人看法
:需要对数据库进行改造,比较麻烦,除非一开始在设计数据库的时候就考虑
分布式锁
通过获取方法参数或使用SpEL表达式来生成分布式锁的键,在执行业务之前获取分布式锁,获取锁成功才可以执行业务,业务执行完毕删除锁,保证同一时间同一个请求只能处理一次
唯一约束
- 数据库唯一约束:给MySQL表的字段添加唯一约束,例如给username添加唯一约束之后,就算用户同时发起了多次注册请求,也只有一个请求可以注册成功
- Redis Set 防重:将方法参数的MD5码添加到set中,每次执行请求时,判断md5是否在set中,如果在,说明已经执行过,直接返回失败(不建议,set会越来越多)
防重表
类似上面的唯一约束,只是单独用一个表来存储响应标识,并对该表示设置唯一约束。能插入标识说明是第一次执行
实现
定义注解
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;
}
handler处理器
handler接口定义
注意,接口定义了两个空方法,因为不需要所有处理器都需要处理那两个方法。该接口默认实现了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() {}
}
接口实现:基于token机制
Service
import com.dam.core.handler.IdempotentExecuteHandler;/*** Token 实现幂等接口**/
public interface IdempotentTokenService extends IdempotentExecuteHandler {/*** 创建幂等验证Token*/String createToken();
}
Controller
使用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;@Overridepublic IdempotentParam buildParam(ProceedingJoinPoint joinPoint) {return new IdempotentParam();}/*** 创建一个新的幂等Token。生成一个全局唯一的UUID,并添加前缀(从配置或默认值获取),* 将此Token作为键存入Redis,并设置一个过期时间(从配置或默认值获取)。* 值为空字符串,因为仅用于标识Token的存在,不存储额外数据。** @return 新创建的幂等Token字符串*/@Overridepublic 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 幂等参数包装器*/@Overridepublic 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);}}// 验证并删除TokenBoolean 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);}}
}
接口实现:参数param作为分布式锁的键
Service
注意,该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";@Overridepublic 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 幂等参数包装器*/@Overridepublic void handler(IdempotentParam wrapper) {// 从包装器中获取分布式锁对应的keyString 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);}/*** 将分布式锁进行解锁*/@Overridepublic 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中获取请求的ServletPathreturn sra.getRequest().getServletPath();}/*** 获取当前操作用户的ID** @return 当前操作用户 ID*/private String getCurrentUserId() {// 从UserContext中获取当前用户的IDString 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;}
}
接口实现:SPEL表达式的值作为分布式锁的键
Service
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@Overridepublic 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 幂等参数包装器*/@Overridepublic 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);}/*** 后处理,对分布式锁进行解锁*/@Overridepublic void postProcessing() {// 从 IdempotentContext 中获取之前存储的锁实例RLock lock = null;try {lock = (RLock) IdempotentContext.getKey(LOCK);} finally {// 如果锁实例不为空,进行解锁操作if (lock != null) {lock.unlock();}}}/*** 请求的方法执行过程中发生了异常,也对分布式锁进行解锁*/@Overridepublic 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;}
}
handler工厂
简单工厂模式提供专门的工厂类用于创建对象,实现了对象创建和使用的职责分离,客户端不需知道所创建的具体产品类的类名以及创建过程(在这里不需要创建,只需要从容器里面获取相应的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;}
}
AOP
幂等相关参数包装
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 {/*** 幂等切面*/@Beanpublic IdempotentAspect idempotentAspect() {return new IdempotentAspect();}/*** 参数方式幂等实现,基于 RestAPI 场景*/@Bean@ConditionalOnMissingBeanpublic IdempotentParamService idempotentParamExecuteHandler(RedissonClient redissonClient) {return new IdempotentParamExecuteHandler(redissonClient);}/*** Token 方式幂等实现,基于 RestAPI 场景*/@Bean@ConditionalOnMissingBeanpublic 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@ConditionalOnMissingBeanpublic IdempotentSpELService idempotentSpELByRestAPIExecuteHandler(RedissonClient redissonClient) {return new IdempotentSpELByRestAPIExecuteHandler(redissonClient);}/*** SpEL 方式幂等实现,基于 MQ 场景*/@Bean@ConditionalOnMissingBeanpublic IdempotentSpELByMQExecuteHandler idempotentSpELByMQExecuteHandler(StringRedisTemplate distributedCache) {return new IdempotentSpELByMQExecuteHandler(distributedCache);}
}
使用
SpEL方式
在方法上面添加@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();
}
param方式
@Idempotent(message = "正在执行用户信息修改流程,请稍后...",scene = IdempotentSceneEnum.RESTAPI,type = IdempotentTypeEnum.PARAM
)
说明
本文代码来源于马哥 12306 的代码,本人只是根据自己的理解进行少量修改并应用到智能排班系统中。代码仓库为12306,该项目含金量较高,有兴趣的朋友们建议去学习一下。