一、什么是幂等性?
幂等一般指的是方法被多次重复执行的时候,所产生的影响和第一次执行的影响是相同的。
二、技术场景中的幂等性
操作类型 | 示例 | 实现方式 |
---|---|---|
创建资源 | 订单提交 | Token机制、唯一ID |
更新资源 | 设置用户状态为"已激活" | 乐观锁、条件更新 |
删除资源 | 取消订单 | 先查状态再删除 |
金融操作 | 账户扣款 | 事务+唯一流水号 |
三、HTTP方法的幂等性
方法 | 幂等性 | 说明 |
---|---|---|
GET | ✅ | 读取操作不影响资源状态 |
PUT | ✅ | 全量更新(如更新用户信息) |
DELETE | ✅ | 删除一次后再删除返回404 |
POST | ❌ | 每次调用创建新资源(如订单) |
PATCH | ⚠️ | 部分更新,需自行实现幂等 |
⚠️ 实际业务中需通过设计将POST/PATCH转为幂等操作
四、接口会被重复请求的情况
场景1:用户的重复提交或者用户的恶意攻击,导致请求被多次重复执行。
✅ 核心思路
- 生成唯一Token:在用户进入订单提交页时,后端生成唯一Token并存储到Redis
- 前端携带Token:提交订单时,前端将Token附加到请求中(Header/Param)
- 验证并删除Token:后端通过Redis原子操作验证Token存在性,并立即删除
- 拒绝重复请求:若Token不存在/重复,则判定为重复提交
✅ 执行流程
✅ 实现步骤
- 生成Token接口(供前端获取)
@RestController
public class TokenController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/generateToken")
public Response<String> generateToken() {
String token = UUID.randomUUID().toString();
// 存储到Redis,有效期5分钟(根据业务调整)
redisTemplate.opsForValue().set(token, "1", 5, TimeUnit.MINUTES);
return Response.success(token);
}
}
- 订单提交接口(幂等校验)
@RestController
public class OrderController {
@Autowired
private StringRedisTemplate redisTemplate;
@PostMapping("/submitOrder")
public Response<String> submitOrder(@RequestHeader("X-Idempotent-Token") String token,
@RequestBody Order order) {
// 1. 校验Token有效性(原子操作:查询+删除)
Long result = redisTemplate.execute(
new DefaultRedisScript<>(
"if redis.call('get', KEYS[1]) then return redis.call('del', KEYS[1]) else return 0 end",
Long.class
),
Collections.singletonList(token)
);
// 2. 处理验证结果
if (result == 0) {
// Token不存在或已使用(重复请求)
return Response.fail("重复提交或无效请求");
}
// 3. 正常处理业务(创建订单等)
orderService.createOrder(order);
return Response.success("订单创建成功");
}
}
✅ 关键点解析
- 原子操作:使用Lua脚本保证GET+DEL操作的原子性,避免并发问题:
if redis.call('get', KEYS[1]) then
return redis.call('del', KEYS[1])
else
return 0
end
-
Token传递方式
-
Token绑定用户ID:order_{userId}_{uuid} 防止Token被其他用户盗用
-
推荐使用请求头:@RequestHeader(“X-Idempotent-Token”)
-
可选请求参数:@RequestParam(“token”)
-
-
Redis配置
- Key:Token字符串(如 “c6a9d6c7-3d4f-4eaa-bb7e-123456789abc”)
- Value:任意值(比如"1")
- TTL:根据业务设置(通常5-30分钟)
-
前端配合
- 进入订单页时调用/generateToken获取Token
- 提交订单时在Header中添加X-Idempotent-Token:
✅ 增强方案
- 添加业务标识(防不同订单复用Token)
// 生成Token时加入用户ID
String token = "ORDER_" + userId + "_" + UUID.randomUUID();
- 自定义注解简化流程
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
String header() default "X-Idempotent-Token"; // 指定Token位置
}
// 使用AOP统一处理
@Around("@annotation(idempotent)")
public Object checkIdempotent(ProceedingJoinPoint joinPoint, Idempotent idempotent) {
// 从Header获取Token并验证
// ...
}
- 异常处理
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IdempotentException.class)
public ResponseEntity<String> handleDuplicate(IdempotentException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(ex.getMessage());
}
}
场景2:在分布式架构中,RPC接口远程调用,上游服务调用下游服务接口超时通常会重试,此时有可能会重复调用。
✅ 核心思路
- 上游服务生成唯一ID,下游服务通过状态机保证幂等
✅ 执行流程
✅ 实现步骤
- 设计幂等处理器
// 幂等处理器
@Component
public class IdempotentProcessor {
@Autowired
private IdempotentRecordRepository recordRepository;
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String LOCK_PREFIX = "IDEMPOTENT_LOCK:";
public <T> T process(String requestId, Supplier<T> businessLogic) {
// 1. 检查是否已处理过
IdempotentRecord record = recordRepository.findByRequestId(requestId);
if (record != null) {
return (T) JSON.parseObject(record.getResponse(), businessLogic.get().getClass());
}
// 2. 获取分布式锁(防止并发请求)
String lockKey = LOCK_PREFIX + requestId;
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (!locked) {
throw new IdempotentException("系统处理中,请稍后");
}
try {
// 3. 双重检查(获取锁后再次查询)
record = recordRepository.findByRequestId(requestId);
if (record != null) {
return (T) JSON.parseObject(record.getResponse(), businessLogic.get().getClass());
}
// 4. 执行业务逻辑
T result = businessLogic.get();
// 5. 保存处理结果(事务中)
saveIdempotentRecord(requestId, result);
return result;
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
}
@Transactional
private <T> void saveIdempotentRecord(String requestId, T result) {
IdempotentRecord record = new IdempotentRecord();
record.setRequestId(requestId);
record.setStatus(IdempotentStatus.COMPLETED);
record.setResponse(JSON.toJSONString(result));
record.setCreatedAt(LocalDateTime.now());
recordRepository.save(record);
}
}
// 业务服务使用示例
@Service
public class OrderService {
@Autowired
private IdempotentProcessor idempotentProcessor;
public Order createOrder(CreateOrderCommand command) {
// 从上游获取的请求ID(由调用方生成)
String requestId = command.getRequestId();
return idempotentProcessor.process(requestId, () -> {
// 真正的业务逻辑
Order order = new Order();
// ...订单创建逻辑
return orderRepository.save(order);
});
}
}
- 数据库设计,新建幂等记录表
CREATE TABLE idempotent_record (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
request_id VARCHAR(64) NOT NULL COMMENT '唯一请求ID',
status TINYINT NOT NULL COMMENT '状态:1-处理中,2-成功,3-失败',
response TEXT COMMENT '响应结果(JSON)',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE KEY uniq_request_id (request_id)
) COMMENT='幂等记录表';
双场景统一方案
对于同时需要处理两种场景的系统,可结合两种方案:
@PostMapping("/orders")
public ResponseEntity<?> createOrder(
@RequestHeader(value = "X-Idempotent-Token", required = false) String token,
@RequestBody OrderRequest request) {
// 场景1:来自前端的请求(带Token)
if (token != null) {
if (!validateToken(token)) {
return ResponseEntity.status(409).body("重复请求");
}
return processOrder(request);
}
// 场景2:来自服务的请求(带RequestID)
if (request.getRequestId() != null) {
return idempotentProcessor.process(request.getRequestId(),
() -> processOrder(request));
}
// 无防护的请求
return ResponseEntity.badRequest().body("缺少幂等标识");
}
方案对比总结:
场景 | 方案 | 核心组件 | 优点 | 适用接口 |
---|---|---|---|---|
用户重复提交 | Token+Redis | 一次性令牌 | 前端友好,简单易实现 | 用户触发的写操作 |
分布式系统重试 | 请求ID+状态机 | 唯一ID+状态记录 | 服务间通用,支持结果缓存 | RPC接口/消息队列消费 |
混合场景 | 双方案结合 | 网关路由识别 | 全场景覆盖 | 统一入口的API |
最佳实践建议
- 前端场景:
- Token有效期不宜过长(建议5-30分钟)
- 结合验证码防止机器人攻击
- 敏感操作添加二次确认
- 服务间场景:
- 请求ID生成规则:服务标识+业务ID+UUID
- 状态机设计明确状态流转
- 设置合理的记录保留时间(建议72小时)
- 通用原则:
- 监控告警:
- 记录重复请求率
- 监控幂等表数据量增长
- 设置异常重复告警阈值
通过以上方案,可有效解决用户重复提交和分布式系统重试导致的幂等问题,同时保证系统的高可用性和数据一致性。