本文目录导读:

我来详细讲解接口幂等性防重复提交的令牌机制(Token Pattern),这是一种最常用的解决方案。
什么是令牌机制
核心思想:客户端在提交请求前,先获取一个唯一令牌(Token),提交时必须携带此令牌,服务端处理请求时,检查这个令牌是否存在且有效。
实现原理
sequenceDiagram
participant C as 客户端
participant S as 服务端
participant DB as Redis
C->>S: 1. 请求获取令牌
S->>DB: 2. 生成唯一令牌并存储
DB-->>S: 3. 返回令牌
S-->>C: 4. 返回令牌
C->>S: 5. 请求处理(携带令牌)
S->>DB: 6. 检查并删除令牌
alt 令牌存在
DB-->>S: 7. 删除成功
S->>S: 8. 执行业务逻辑
S-->>C: 9. 返回成功
else 令牌不存在
DB-->>S: 7. 删除失败(已用/过期)
S-->>C: 8. 返回重复请求
end
完整代码实现
令牌生成与存储
@Service
public class TokenService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 生成令牌
public String createToken(String key) {
// 1. 生成唯一令牌
String token = UUID.randomUUID().toString();
// 2. 存储到Redis,设置有效期(比如30分钟)
String redisKey = buildRedisKey(key, token);
redisTemplate.opsForValue()
.set(redisKey, "1", 30, TimeUnit.MINUTES);
return token;
}
// 校验并删除令牌
public boolean checkAndDeleteToken(String key, String token) {
String redisKey = buildRedisKey(key, token);
// 使用Lua脚本保证原子性
String script = "if redis.call('get', KEYS[1]) == '1' then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(redisKey)
);
return result != null && result > 0;
}
private String buildRedisKey(String key, String token) {
return "token:prevent:" + key + ":" + token;
}
}
自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicateSubmit {
// 令牌获取的key,支持SpEL表达式
String key();
// 令牌来源,默认从请求头获取
String tokenSource() default "header";
// 自定义验证失败的消息
String message() default "请勿重复提交";
}
AOP拦截器
@Aspect
@Component
public class PreventDuplicateAspect {
@Autowired
private TokenService tokenService;
@Around("@annotation(preventDuplicate)")
public Object around(ProceedingJoinPoint point,
PreventDuplicateSubmit preventDuplicate) throws Throwable {
// 1. 解析SpEL表达式获取key
String key = parseKey(preventDuplicate.key(), point);
// 2. 从请求中获取令牌
String token = getTokenFromRequest(preventDuplicate.tokenSource());
// 3. 校验令牌
if (!tokenService.checkAndDeleteToken(key, token)) {
throw new RuntimeException(preventDuplicate.message());
}
// 4. 执行业务逻辑
return point.proceed();
}
private String getTokenFromRequest(String source) {
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
if ("header".equals(source)) {
return request.getHeader("idempotent-token");
} else {
return request.getParameter("idempotent-token");
}
}
}
控制器实现
@RestController
public class OrderController {
@Autowired
private TokenService tokenService;
// 获取令牌接口
@GetMapping("/token")
public Response<String> getToken() {
String token = tokenService.createToken("order");
return Response.success(token);
}
// 提交订单(防止重复提交)
@PostMapping("/order/create")
@PreventDuplicateSubmit(
key = "'order:' + #order.userId",
tokenSource = "header",
message = "订单正在处理中,请勿重复提交"
)
public Response<String> createOrder(@RequestBody OrderDTO order) {
// 业务逻辑
return Response.success("订单创建成功");
}
}
前端使用示例
// Vue.js 示例
export default {
data() {
return {
idempotentToken: '',
submitting: false
}
},
methods: {
async submitOrder(orderData) {
this.submitting = true;
try {
// 第一步:获取令牌
const tokenResponse = await axios.get('/api/token');
this.idempotentToken = tokenResponse.data.data;
// 第二步:携带令牌提交请求
const result = await axios.post('/api/order/create', orderData, {
headers: {
'idempotent-token': this.idempotentToken
},
timeout: 30000
});
console.log('提交成功', result);
} catch (error) {
if (error.response?.status === 400) {
alert(error.response.data.message); // "请勿重复提交"
} else {
alert('提交失败,请重试');
}
} finally {
this.submitting = false;
}
}
}
}
Redis Lua脚本(保证原子性)
-- check_and_delete.lua
-- KEYS[1]: token key
-- ARGV[1]: expected value
local token = redis.call('GET', KEYS[1])
if token == false or token ~= ARGV[1] then
return 0 -- 令牌不存在或不匹配
end
redis.call('DEL', KEYS[1])
return 1 -- 校验成功并删除
高级优化方案
批量请求优化
@Component
public class BatchTokenService {
// 批量生成令牌
public Map<String, String> createBatchTokens(int count) {
Map<String, String> tokens = new HashMap<>();
// 使用pipeline批量写入
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (int i = 0; i < count; i++) {
String token = UUID.randomUUID().toString();
connection.setEx(
buildRedisKey(token).getBytes(),
1800, // 30分钟
"1".getBytes()
);
tokens.put("token_" + i, token);
}
return null;
});
return tokens;
}
}
分布式环境支持
public interface DistributedTokenService {
// 使用分布式锁保证token获取的互斥性
default boolean acquireTokenWithLock(String key, String token) {
// 1. 先尝试获取分布式锁
String lockKey = "lock:" + key;
String lockValue = UUID.randomUUID().toString();
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 2. 执行业务逻辑
return processToken(key, token);
} finally {
// 3. 释放锁(使用Lua脚本保证原子性)
releaseLock(lockKey, lockValue);
}
}
return false;
}
}
注意事项
令牌有效期
# application.yml
token:
prevent:
# 令牌有效期(分钟)
expire-time: 30
# Redis key前缀
key-prefix: "idempotent:token:"
# 清理过期令牌的间隔(分钟)
clean-interval: 60
异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(DuplicateSubmitException.class)
public Response<String> handleDuplicateSubmit(DuplicateSubmitException e) {
return Response.error(400, e.getMessage());
}
@ExceptionHandler(TokenExpiredException.class)
public Response<String> handleTokenExpired(TokenExpiredException e) {
return Response.error(401, "令牌已过期,请重新获取");
}
}
性能优化
- 使用Redis pipeline批量操作
- 令牌预生成(提前生成一批令牌)
- 使用本地缓存结合Redis(Caffeine + Redis)
这个令牌机制是实现接口幂等性最可靠的方式之一,适用于金融转账、订单创建、支付等关键业务场景。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播