一、背景:为什么需要幂等性
在微服务、分布式架构下,网络不可靠、请求重试机制(如前端超时重发、客户端重发、网关重试、消息消费失败重试等)会带来重复请求,如果接口没有幂等性,可能导致:
- 重复扣费
- 重复订单
- 重复数据写入
- 资源状态异常
因此接口的幂等性是保障系统一致性、稳定性的关键。
二、幂等性的演进历程
阶段 | 实现方式 | 适用场景 | 特点 |
---|---|---|---|
1. 前端防重复提交 | 禁用按钮、Token机制 | 简单场景(如表单) | 限制小,易绕过 |
2. 数据库唯一约束 | 唯一索引字段控制插入 | 插入类接口 | 利用数据库能力 |
3. 幂等标识(唯一业务ID) | 唯一业务号控制幂等 | 订单类接口 | 明确控制粒度 |
4. 分布式锁 | 请求上锁 | 任意接口 | 控制请求串行,牺牲吞吐 |
5. 状态机机制 | 状态+版本控制 | 状态转换操作 | 精细控制副作用 |
6. Redis 幂等性控制 | 幂等Key+过期时间 | 高并发下 | 快速、安全,结合令牌机制 |
三、最佳实践总结
🧠 原则
- 幂等性以"结果相同"为基准,副作用不可重复。
- 控制粒度应适当:写操作必须幂等,读操作天然幂等。
- 幂等 Token 最好由客户端生成或服务端下发后强制绑定。
✅ 最佳实践建议
- 业务唯一标识驱动幂等性,如订单号、支付流水号。
- 幂等性应放在业务代码逻辑前端处理,即"第一次判断是否重复提交"。
- 采用 Redis + 原子操作(如SETNX)+ Token + TTL 实现防重。
- 接口幂等性建议作为注解能力进行抽象封装,统一使用。
四、Spring Boot 实现示例(完善版)
下面我们以 Token机制 + Redis原子写入实现接口幂等性,构建完整示例。
1️⃣ 添加依赖
xml
<!-- Redis starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2️⃣ Redis配置类(Lettuce连接池)
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
3️⃣ 幂等Token生成接口(服务端)
java
@RestController
@RequestMapping("/token")
public class TokenController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("/generate")
public String generateToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("idem-token:" + token, "valid", 5, TimeUnit.MINUTES);
return token;
}
}
4️⃣ 幂等校验注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
String key() default ""; // 可自定义Key策略
}
5️⃣ 幂等校验切面
java
@Aspect
@Component
public class IdempotentAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Around("@annotation(idempotent)")
public Object checkIdempotency(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader("Idempotency-Token");
if (StringUtils.isEmpty(token)) {
throw new RuntimeException("幂等Token不能为空");
}
String redisKey = "idem-token:" + token;
Boolean result = redisTemplate.delete(redisKey); // 核心逻辑:删除即消费
if (Boolean.FALSE.equals(result)) {
throw new RuntimeException("重复请求,请勿重复提交");
}
return joinPoint.proceed();
}
}
6️⃣ 接口使用示例
java
@RestController
@RequestMapping("/order")
public class OrderController {
@PostMapping("/create")
@Idempotent
public String createOrder(@RequestBody OrderDTO orderDTO) {
// 模拟业务逻辑
return "订单创建成功,订单号:" + UUID.randomUUID().toString();
}
}
7️⃣ Postman 调用流程:
-
调用
GET /token/generate
获取 token -
调用
POST /order/create
,在请求头加上:Idempotency-Token: 你刚刚拿到的token
多次调用同一个 token,只会成功一次,后续将抛出"重复请求"异常。
五、可扩展建议
功能 | 实现方式 |
---|---|
支持注解中的自定义 key 逻辑 | SpEL 表达式,读取参数字段 |
幂等记录持久化 | Redis + 数据库双写 |
异常重试队列支持 | Kafka / RocketMQ 消息幂等处理 |
Redisson替换RedisTemplate | 使用 RLock.tryLock() 可实现锁粒度的幂等控制 |
六、架构图(逻辑)
客户端
|
[请求附带Token]
|
Spring Boot 接口(注解拦截)
|
切面逻辑校验 Token
|
Redis SETNX 删除 key -> 是第一次? -> 继续业务
|
业务逻辑执行(订单创建、支付、提交...)
后续实例回陆续添加
参数幂等、结果缓存、幂等日志记录