题目
设计高并发场景下的分布式幂等接口
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
Spring MVC请求处理流程,分布式锁实现,接口幂等性设计,高并发优化,防重机制
快速回答
实现高并发幂等接口的核心要点:
- 使用分布式锁(如Redis RedLock)保证原子性操作
- 通过唯一请求ID实现幂等性校验(客户端生成或服务端颁发)
- 采用三级防重策略:请求ID内存缓存 → Redis标记 → 数据库唯一索引
- 使用异步处理+状态查询分离读写压力
- 数据库操作使用乐观锁或唯一约束
1. 问题背景与挑战
在分布式系统中,当用户重复提交、网络重试或服务重发时,需要保证接口的幂等性。典型场景如:支付回调、订单创建等。高并发下需解决:
- 重复请求导致数据不一致
- 数据库写入冲突
- 系统资源竞争
2. 核心实现方案
2.1 幂等性设计原理
幂等性指同一请求多次执行结果一致。关键要素:
- 唯一请求ID:客户端生成(如前端UUID)或服务端颁发(Token机制)
- 状态记录:记录请求处理状态(pending/success/fail)
2.2 Spring MVC接口实现
@RestController
public class PaymentController {
@PostMapping("/pay")
public ResponseEntity<String> payment(
@RequestHeader("X-Request-ID") String requestId,
@RequestBody PaymentRequest request) {
// 1. 幂等校验
IdempotentService.verifyRequestId(requestId);
// 2. 业务处理(含分布式锁)
return paymentService.process(requestId, request);
}
}2.3 三级防重策略
// 伪代码实现
public class IdempotentService {
// 一级:本地缓存(Guava Cache)
private static final Cache<String, Boolean> localCache =
CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).build();
// 二级:Redis标记
public static void verifyRequestId(String requestId) {
// 本地缓存检查
if (localCache.getIfPresent(requestId) != null) {
throw new DuplicateRequestException();
}
// Redis原子操作(SETNX+EXPIRE)
boolean locked = redisTemplate.execute((RedisCallback<Boolean>) conn ->
conn.set(requestId.getBytes(), "1".getBytes(),
Expiration.seconds(30),
RedisStringCommands.SetOption.SET_IF_ABSENT)
);
if (!locked) throw new DuplicateRequestException();
localCache.put(requestId, true);
}
}2.4 数据库层防护
-- 创建唯一索引
ALTER TABLE payments ADD UNIQUE INDEX idx_request_id (request_id);3. 高并发优化策略
- 读写分离:
- 写操作:异步处理(@Async + 线程池)
- 读操作:返回202 Accepted + 结果查询接口
- 锁优化:
- 使用Redisson红锁(RedLock)替代简单Redis锁
- 锁粒度控制到业务ID级别(如用户ID+业务类型)
- 流量控制:
- Guava RateLimiter限流
- Nginx层限速
4. 常见错误与规避
- 错误1:仅用本地缓存 → 分布式系统失效
规避:必须结合分布式存储 - 错误2:未设置锁超时 → 死锁风险
规避:Redis锁必须设置过期时间 - 错误3:先查DB再操作 → 并发间隙
规避:用唯一索引兜底
5. 扩展知识
- Token方案:预生成令牌(防止客户端重复生成ID)
- 客户端获取token(GET /token)
- 携带token发起请求(POST /pay?token=xxx)
- 状态机设计:业务状态流转校验(如订单状态:created → paid → fulfilled)
- 消息队列:Kafka/RocketMQ自带消息去重机制