题目
高并发场景下如何实现Spring MVC接口的防重提交与幂等性保障
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
幂等性设计,分布式锁实现,高并发优化,Spring拦截器应用,缓存策略
快速回答
实现核心要点:
- 使用唯一请求ID(如UUID)作为幂等键,通过HTTP头或参数传递
- 在拦截器中通过Redis分布式锁(Redisson或Lua脚本)实现请求拦截
- 采用两级校验:内存标记(ConcurrentHashMap)快速拦截 + Redis分布式锁精确控制
- 结合本地缓存(Caffeine)和Redis实现高效状态查询
- 异常处理需包含锁释放、重试机制和降级策略
1. 问题背景与核心挑战
在高并发场景下(如秒杀系统),需解决:
- 防重提交:短时间内的重复请求拦截(如用户连续点击)
- 幂等性保障:网络超时重试、消息重复消费等场景的业务数据一致性
- 性能要求:TPS 万级以上时,传统数据库唯一索引方案会导致性能瓶颈
2. 架构设计
// 伪代码:整体处理流程
1. 客户端生成 requestId(UUID)并附加到请求头
2. 拦截器校验:
if (内存标记存在requestId) → 直接拒绝
else → 尝试获取Redis分布式锁
3. 业务处理:
- 成功获取锁:执行业务 → 结果缓存 → 释放锁
- 锁获取失败:查询缓存结果或返回处理中状态
4. 响应:返回包含请求状态的标准JSON结构3. 关键实现代码示例
(1) 自定义拦截器
@Component
public class IdempotentInterceptor implements HandlerInterceptor {
@Autowired
private IdempotentService idempotentService; // 核心服务
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String requestId = request.getHeader("X-Request-ID");
if (StringUtils.isEmpty(requestId)) {
throw new BadRequestException("缺失请求ID");
}
// 两级校验
return idempotentService.checkAndLock(requestId);
}
}(2) 幂等服务核心逻辑
@Service
public class IdempotentService {
// 本地缓存(1秒过期)
private final Cache<String, Boolean> localCache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS).build();
@Autowired
private RedissonClient redisson; // 分布式锁客户端
public boolean checkAndLock(String requestId) {
// 第一级:本地缓存检查
if (localCache.getIfPresent(requestId) != null) {
throw new RepeatSubmitException("请求重复提交");
}
// 第二级:Redis分布式锁
RLock lock = redisson.getLock("IDEMPOTENT:" + requestId);
if (!lock.tryLock(0, 3, TimeUnit.SECONDS)) { // 非阻塞尝试
// 检查是否已处理完成
String result = redisTemplate.opsForValue().get("RESULT:" + requestId);
if (result != null) return false; // 返回历史结果
throw new ConcurrentAccessException("系统繁忙,请稍后重试");
}
try {
localCache.put(requestId, true); // 标记本地缓存
return true;
} finally {
// 注意:不在拦截器中释放锁!在业务完成后释放
}
}
}(3) 控制器层处理
@RestController
public class PaymentController {
@PostMapping("/pay")
public ResponseEntity<ApiResponse> createPayment(
@RequestHeader("X-Request-ID") String requestId,
@RequestBody PaymentRequest request) {
// 1. 检查是否已处理(防并发穿透)
ApiResponse cached = resultCache.get(requestId);
if (cached != null) return ResponseEntity.ok(cached);
// 2. 执行业务(数据库操作等)
PaymentResult result = paymentService.process(request);
// 3. 保存结果并释放锁
resultCache.put(requestId, result.toResponse());
lockManager.releaseLock(requestId); // 释放分布式锁
return ResponseEntity.ok(result.toResponse());
}
}4. 最佳实践与优化策略
- 性能优化:
- 本地缓存使用 caffeine,配置短过期时间(0.5-2秒)
- Redis 使用 Lua 脚本保证原子性:
SET lock_key 1 EX 3 NX
- 防死锁设计:
- 锁必须设置超时(建议3-5秒)
- 添加看门狗线程(Redisson 已内置)自动续期
- 降级方案:
- Redis 不可用时切换本地锁(影响分布式一致性)
- 开放短时重试窗口(如503响应中的Retry-After头)
5. 常见错误
- 锁超时设置不当:业务执行时间 > 锁超时导致并发穿透
- 未处理锁释放:异常场景未释放锁引发系统阻塞
- 本地缓存滥用:未设置过期导致内存泄漏
- 请求ID生成不安全:客户端生成可能被伪造,敏感操作需结合服务端生成
6. 扩展知识
- Token Bucket 限流:结合Guava RateLimiter实现请求限流
- 异步处理:复杂业务可返回202 Accepted,通过回调通知结果
- 数据库方案对比:
- 唯一索引:适合低频操作
- 乐观锁:
update table set version=version+1 where id=1 and version=current_version - 状态机:订单状态流转校验