// 幂等性Token服务
@Service
public class IdempotentTokenService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String IDEMPOTENT_PREFIX = "idempotent:token:";
private static final long TOKEN_EXPIRE_SECONDS = 300; // Token有效期5分钟
/**
* 生成幂等性Token
*/
public String generateToken(String userId) {
String token = UUID.randomUUID().toString();
String redisKey = IDEMPOTENT_PREFIX + userId + ":" + token;
// 存储Token,设置过期时间
redisTemplate.opsForValue().set(
redisKey,
"1",
TOKEN_EXPIRE_SECONDS,
TimeUnit.SECONDS
);
return token;
}
/**
* 检查并消费Token
* @return true: Token有效且消费成功; false: Token无效或已消费
*/
public boolean checkAndConsumeToken(String userId, String token) {
String redisKey = IDEMPOTENT_PREFIX + userId + ":" + token;
// 使用Lua脚本保证原子性
String luaScript = """
if redis.call('get', KEYS[1]) == '1' then
redis.call('del', KEYS[1])
return 1
else
return 0
end
""";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(luaScript);
redisScript.setResultType(Long.class);
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList(redisKey)
);
return result != null && result == 1L;
}
}
// 使用AOP实现幂等性校验
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
String key() default ""; // 幂等键,支持SpEL表达式
long expireTime() default 300; // 过期时间,秒
}
@Aspect
@Component
public class IdempotentAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
// 1. 获取方法参数
Object[] args = joinPoint.getArgs();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 2. 解析幂等键(支持SpEL)
String keyExpression = idempotent.key();
String redisKey = parseKey(keyExpression, method, args);
// 3. 尝试获取分布式锁(防止并发请求同时通过检查)
String lockKey = redisKey + ":lock";
boolean lockAcquired = false;
try {
// 尝试加锁
lockAcquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!lockAcquired) {
throw new BusinessException("系统繁忙,请稍后重试");
}
// 4. 检查Token是否已使用
Boolean exists = redisTemplate.hasKey(redisKey);
if (Boolean.TRUE.equals(exists)) {
// Token已使用,直接返回之前的处理结果(这里需要根据实际业务调整)
throw new BusinessException("请勿重复提交订单");
}
// 5. 执行业务逻辑
Object result = joinPoint.proceed();
// 6. 标记Token已使用
redisTemplate.opsForValue().set(
redisKey,
"processed",
idempotent.expireTime(),
TimeUnit.SECONDS
);
return result;
} finally {
// 释放锁
if (lockAcquired) {
redisTemplate.delete(lockKey);
}
}
}
private String parseKey(String expression, Method method, Object[] args) {
// 这里实现SpEL表达式解析,获取实际的幂等键
// 例如可以从参数中提取userId+orderToken
return "parsed:key:from:expression";
}
}
// 在订单提交接口上使用
@RestController
@RequestMapping("/order")
public class OrderController {
@PostMapping("/submit")
@Idempotent(key = "#request.userId + ':' + #request.submitToken", expireTime = 300)
public ApiResponse<OrderSubmitResult> submitOrder(@RequestBody OrderSubmitRequest request) {
// 这里是真正的订单创建逻辑
OrderSubmitResult result = orderService.createOrder(request);
return ApiResponse.success(result);
}
}