题目
设计高并发场景下的Spring MVC接口,实现防重提交与分布式幂等性
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
Spring MVC请求处理,分布式锁实现,幂等性设计,防重提交机制,高并发优化
快速回答
在高并发场景下实现防重提交和接口幂等性需要综合运用多种技术:
- 防重提交:采用Token机制,前端提交携带唯一Token,后端验证后立即失效
- 幂等性保障:基于业务唯一标识(如订单ID)配合分布式锁实现
- 分布式锁:使用Redis实现原子锁操作,设置合理的锁超时时间
- 高并发优化:结合异步处理、数据库乐观锁和限流机制
- 错误处理:定义清晰的错误码体系,区分重复提交和业务异常
1. 核心原理说明
防重提交:防止用户短时间内重复提交相同数据。核心是通过Token机制实现:
- 页面加载时生成唯一Token存储到Redis并返回前端
- 提交时携带Token,后端验证存在后立即删除
- Token缺失或重复视为非法请求
幂等性设计:确保多次请求产生相同结果:
- 基于业务唯一键(如订单ID+操作类型)创建防重表
- 使用数据库唯一索引或Redis原子操作保证唯一性
- 结合状态机验证业务状态流转有效性
2. 完整代码实现
防重Token生成接口:
@RestController
public class TokenController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("/token")
public String generateToken(@RequestParam String bizType) {
String token = UUID.randomUUID().toString();
String key = "ANTI_REPEAT:" + bizType + ":" + token;
// 设置30秒有效期
redisTemplate.opsForValue().set(key, "1", 30, TimeUnit.SECONDS);
return token;
}
}幂等接口实现(使用分布式锁):
@RestController
public class OrderController {
@Autowired
private RedissonClient redissonClient;
@PostMapping("/pay")
public ResponseEntity<?> payOrder(
@RequestHeader("X-Request-Token") String token,
@RequestBody PaymentRequest request) {
// 1. 防重提交验证
String tokenKey = "ANTI_REPEAT:PAYMENT:" + token;
if (!redisTemplate.delete(tokenKey)) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ErrorResponse("REPEAT_SUBMIT", "重复提交"));
}
// 2. 获取分布式锁
String lockKey = "IDEMPOTENT_LOCK:" + request.getOrderId();
RLock lock = redissonClient.getLock(lockKey);
try {
// 等待锁时间0,持有锁5秒
if (!lock.tryLock(0, 5, TimeUnit.SECONDS)) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.body(new ErrorResponse("OPERATION_IN_PROGRESS", "处理中"));
}
// 3. 幂等性检查
if (orderService.isPaid(request.getOrderId())) {
return ResponseEntity.ok(new PaymentResponse("ALREADY_PAID", 0));
}
// 4. 业务处理(带乐观锁)
boolean success = orderService.processPayment(
request.getOrderId(),
request.getAmount()
);
return success ?
ResponseEntity.ok(new PaymentResponse("SUCCESS", request.getAmount())) :
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("PAYMENT_FAILED", "支付失败"));
} finally {
lock.unlock();
}
}
}3. 最佳实践
- 分层验证:前端按钮防重 → 网络层Token验证 → 业务层幂等检查
- 锁优化:
- 锁粒度:基于业务ID而非全局锁
- 锁时长:根据业务复杂度设置,添加看门狗自动续期
- 锁释放:finally块确保释放,避免死锁
- 数据库设计:
- 防重表:创建(order_id, operation_type)唯一索引
- 乐观锁:使用version字段控制并发更新
- 降级方案:Redis不可用时降级到数据库唯一约束
4. 常见错误与规避
- Token未失效:验证后必须立即删除Token(使用Redis DEL原子操作)
- 锁超时设置不当:
- 过短:业务未完成锁释放导致并发问题
- 过长:系统故障时长时间阻塞
- 方案:设置自动续期机制(如Redisson看门狗)
- ABA问题:订单状态从已支付→退款→待支付时,需通过版本号或时间戳校验
- 网络超时重试:客户端应使用相同Token重试,而非生成新Token
5. 高并发优化策略
- 异步化:支付核心逻辑放入MQ,快速释放Tomcat线程
- 限流:在API网关层配置令牌桶限流(如Redis+Lua)
- 缓存优化:
- 幂等结果缓存:将已处理结果缓存5-10秒
- 布隆过滤器:快速过滤无效订单ID
- 数据库优化:
- 热点账户:使用账户分段减少锁竞争
- 批量提交:合并短时间内的相同操作
6. 扩展知识
- 分布式锁选型:Redis(AP)/ZooKeeper(CP)/ETCD根据场景选择
- Token安全:JWT签名防止客户端篡改Token
- 状态机引擎:使用Spring StateMachine规范状态流转
- 监控指标:埋点统计重复请求率、锁等待时间等关键指标