题目
高并发场景下如何设计Spring MVC接口防止重复提交并保证数据一致性
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
并发控制,事务管理,分布式锁,幂等性设计,Spring MVC拦截器
快速回答
在高并发场景下防止重复提交和保证数据一致性,需要综合运用以下技术:
- 幂等性设计:通过唯一标识(如token)确保同一操作只执行一次
- 并发控制:使用分布式锁(如Redis/Redisson)控制并发访问
- 事务管理:利用Spring事务保证数据操作的原子性
- 前端拦截:通过按钮置灰等方式减少重复请求
- 后端拦截:使用拦截器或AOP进行重复请求过滤
1. 问题背景与挑战
在高并发场景(如秒杀、支付)中,重复提交会导致:
1) 数据不一致(如库存超卖)
2) 重复创建资源
3) 业务逻辑错误。需要从前端、网络层和服务端多层防御。
2. 核心解决方案
2.1 幂等性设计(关键)
实现方案:
- Token机制:
@RestController public class TokenController { @GetMapping("/token") public String generateToken() { String token = UUID.randomUUID().toString(); // 存入Redis并设置过期时间 redisTemplate.opsForValue().set(token, "1", 5, TimeUnit.MINUTES); return token; } } - 业务唯一键:如订单ID+业务类型组合唯一索引
2.2 分布式锁实现
使用Redisson实现分布式锁:
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
@Transactional
public void createOrder(Order order) {
String lockKey = "order_lock_" + order.getOrderId();
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待100ms,锁持有10秒
if (lock.tryLock(100, 10000, TimeUnit.MILLISECONDS)) {
// 1. 检查幂等token是否有效
// 2. 执行业务逻辑
// 3. 删除token
}
} finally {
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}2.3 Spring MVC拦截器实现
自定义重复提交拦截器:
public class RepeatSubmitInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
RepeatSubmit annotation = method.getMethodAnnotation(RepeatSubmit.class);
if (annotation != null) {
String token = request.getHeader("X-Submit-Token");
// 检查Redis中token是否存在
if (!redisTemplate.hasKey(token)) {
throw new RepeatSubmitException("重复提交");
}
// 删除token
redisTemplate.delete(token);
}
}
return true;
}
}3. 事务管理关键点
- 事务范围:锁必须在事务外部获取(避免锁释放后事务未提交)
- 隔离级别:使用
@Transactional(isolation = Isolation.SERIALIZABLE)处理幻读 - 传播行为:
PROPAGATION_REQUIRES_NEW确保关键操作独立事务
4. 最佳实践
- 分层防御:前端按钮置灰 + 网络层Nginx限流 + 服务端幂等
- 锁超时设置:根据业务合理设置(太短导致业务中断,太长降低并发)
- 降级策略:锁竞争激烈时返回"系统繁忙"提示
- 监控:监控Redis锁竞争情况和事务回滚率
5. 常见错误
- 错误1:在事务内部加锁(导致锁提前释放)
- 错误2:忽略网络超时导致的重复请求
- 错误3:使用本地锁代替分布式锁
- 错误4:未处理锁释放异常(可能死锁)
6. 扩展知识
- 数据库层面:乐观锁(version字段)、唯一约束、SELECT FOR UPDATE
- 消息队列:通过Kafka等消息队列保证最终一致性
- Token管理优化:使用JWT包含时间戳和业务ID
- 限流组件:结合Sentinel实现QPS控制