关于我在一个优惠券系统中rocketMQ消息幂等性自定义注解的处理
幂等性注解的定义
我定义一个注解 @MQIdempotent
,用于加在需要幂等控制的方法上:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MQIdempotent {/*** key 前缀*/String keyPrefix();/*** 唯一 key 的 SpEL 表达式*/String key();/*** 过期时间(秒)*/long expireTime() default 3600;
}
解释:
keyPrefix
:幂等性校验的 Redis Key 前缀key
:通过 SpEL 表达式 从方法参数中动态解析唯一 KeyexpireTime
:幂等 Key 的过期时间,默认 1 小时
定义aop切面
@Aspect
@Component
@RequiredArgsConstructor
public class IdempotentAspect {private final StringRedisTemplate redisTemplate;@Around("@annotation(MQidempotent)")public Object around(ProceedingJoinPoint joinPoint, MQIdempotent idempotent) throws Throwable {Method method = getMethod(joinPoint);// 解析 SpEL 表达式,得到唯一 keyString key = SpELUtil.parseKey(idempotent.keyPrefix(),idempotent.key(),method,joinPoint.getArgs());// 执行 Lua 脚本,确保原子性 setnx + expireString script = """if redis.call('setnx', KEYS[1], 1) == 1 thenredis.call('expire', KEYS[1], ARGV[1])return 1elsereturn 0end""";DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);Long result = redisTemplate.execute(redisScript, Collections.singletonList(key),String.valueOf(idempotent.expireSeconds()));if (result == null || result == 0) {throw new RuntimeException("请勿重复提交");}return joinPoint.proceed();}private Method getMethod(ProceedingJoinPoint joinPoint) {MethodSignature signature = (MethodSignature) joinPoint.getSignature();return signature.getMethod();}
}
其中的apEL的解析工具类是这样的
public class SpELUtil {private static final SpelExpressionParser PARSER = new SpelExpressionParser();private static final DefaultParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer();/*** 解析注解中的 key*/public static String parseKey(String keyPrefix, String keySpEL, Method method, Object[] args) {// 获取方法参数名String[] paramNames = NAME_DISCOVERER.getParameterNames(method);if (paramNames == null) {throw new IllegalArgumentException("无法获取方法参数名");}// 构造 SpEL 上下文EvaluationContext context = new StandardEvaluationContext();for (int i = 0; i < paramNames.length; i++) {context.setVariable(paramNames[i], args[i]);}// 解析表达式Expression expression = PARSER.parseExpression(keySpEL);Object value = expression.getValue(context);return keyPrefix + value;}
}
示例
@Idempotent(keyPrefix = "order:", key = "#request.userId")
public void createOrder(OrderRequest request) {// 下单逻辑
}
调用时,如果同一个 userId
短时间内多次请求,就会被拦截,避免重复下单。