侧边栏壁纸
博主头像
colo

欲买桂花同载酒

  • 累计撰写 1823 篇文章
  • 累计收到 0 条评论

高并发场景下如何实现Spring MVC接口的防重提交与幂等性保障

2025-12-13 / 0 评论 / 4 阅读

题目

高并发场景下如何实现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
    • 状态机:订单状态流转校验