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
相关产品推荐
相关产品推荐

