You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java Rest API限制单用户并发请求的最优实现方案咨询

单用户高消耗接口并发限制优化方案

你当前的实现方案存在原子性缺陷:「查询标记位」和「设置标记位为true」是两个独立操作,高并发下同一用户的两个请求可能同时查到标记位为false,同时进入处理流程,限制完全失效。另外如果请求处理过程中服务异常宕机、重启,标记位会永久停在true状态,后续该用户的所有请求都会被拒绝,出现死锁问题。

根据你的技术栈和业务场景,推荐以下两种优化方案:

方案1:MongoDB原子操作优化(无额外组件依赖,改动最小)

如果不想引入新的中间件,直接用MongoDB的原子更新能力替换原有先查后改的逻辑即可:

  • 用findOneAndUpdate原子操作,查询条件为「用户ID + 标记位为false」,更新操作为「设置标记位为true,同时新增过期时间字段(设置超过PDF生成最长耗时的过期时间,比如10分钟)」
  • 如果原子操作返回匹配到的记录,说明抢锁成功,允许处理请求;如果返回空,说明已有正在处理的请求,直接拒绝
  • 请求处理完成(包括正常结束、异常报错)后,必须在finally块中原子更新标记位为false
  • 给MongoDB的expireAt字段建TTL索引,就算服务宕机没执行finally逻辑,锁也会自动过期释放,不会出现死锁

伪代码示例:

// 原子抢锁,同时设置10分钟自动过期兜底
Document updatedDoc = mongoTemplate.findAndModify(
  Query.query(Criteria.where("userId").is(currentUserId).and("processingFlag").is(false)),
  Update.update("processingFlag", true).set("expireAt", LocalDateTime.now().plusMinutes(10)),
  FindAndModifyOptions.options().returnNew(true),
  UserProcessFlag.class
);
if (updatedDoc == null) {
  // 抢锁失败,直接返回拒绝提示
  return Response.status(429).entity("当前已有PDF生成任务在处理中,请稍后再试").build();
}
try {
  // 原有PDF生成、下载逻辑
  return generatePdfResponse();
} finally {
  // 无论处理成功失败都释放锁
  mongoTemplate.updateFirst(
    Query.query(Criteria.where("userId").is(currentUserId)),
    Update.update("processingFlag", false).unset("expireAt"),
    UserProcessFlag.class
  );
}

方案2:Redis分布式锁(性能更高,适合接口调用量较大的场景)

如果项目本身已经接入Redis,用Redis锁是更优选择,性能比MongoDB操作高很多:

  • 锁的key用pdf:process:lock:${userId},value存随机串,过期时间设置为比PDF最长生成时间多1~2分钟(比如10分钟)
  • 用SET key value NX EX 过期时间原子命令抢锁,抢锁成功才允许处理请求
  • 释放锁的时候要判断value是否和自己存的一致,避免误删其他请求的锁,同样放到finally块执行

伪代码示例:

String lockKey = "pdf:process:lock:" + currentUserId;
String lockValue = UUID.randomUUID().toString();
// 原子抢锁,NX=不存在才设置,EX=过期时间单位秒
Boolean lockSuccess = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 600, TimeUnit.SECONDS);
if (!Boolean.TRUE.equals(lockSuccess)) {
  return Response.status(429).entity("当前已有PDF生成任务在处理中,请稍后再试").build();
}
try {
  return generatePdfResponse();
} finally {
  //  lua脚本原子释放锁,判断value一致才删除
  String unlockLua = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
  stringRedisTemplate.execute(new DefaultRedisScript<>(unlockLua, Long.class), Collections.singletonList(lockKey), lockValue);
}

额外优化建议

  • 接口返回拒绝的时候带上建议的重试时间,前端可以配合做倒计时提示,用户体验更好
  • 如果PDF生成耗时超过3分钟,建议改成异步任务模式:用户请求后直接返回任务ID,前端轮询任务状态,生成完成后再提供下载链接,这样用户不需要一直等待页面加载,也能避免浏览器超时问题,同时也自然避免了并发请求的问题。

内容的提问来源于stack exchange,提问作者Stunner

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 08:09:03