NestJS中如何避免用户并发调用端点超量创建文档?
在NestJS中处理用户文档创建并发限制的最佳方案
要解决同一用户并发请求创建文档时的竞态问题,核心是在检查文档数量和创建文档的操作之间加锁,确保同一时间只有一个请求能执行完整的校验+创建流程。以下是几种可靠的实现方案,按优先级和适用场景排序:
1. 数据库层面悲观锁(最可靠,优先推荐)
利用数据库的事务和行级锁,直接在数据层面阻塞并发请求,从根源避免竞态条件。以TypeORM为例:
@Injectable() export class DocumentService { constructor( @InjectRepository(Document) private docRepo: Repository<Document>, @InjectRepository(User) private userRepo: Repository<User>, private connection: Connection, ) {} async createDocument(userId: string, docData: DocumentDTO) { return this.connection.transaction(async (manager) => { // 悲观写锁:锁住当前用户的行,其他请求必须等待当前事务完成才能操作 const user = await manager.findOne(User, userId, { lock: { mode: 'pessimistic_write' } }); if (!user) { throw new NotFoundException('用户不存在'); } // 在锁内查询文档数量,确保结果准确 const docCount = await manager.count(Document, { where: { userId } }); if (docCount >= 2) { throw new BadRequestException('每个用户最多只能创建2个文档'); } // 创建新文档 const newDoc = manager.create(Document, { ...docData, userId }); await manager.save(newDoc); return newDoc; }); } }
适用场景
- 单数据库实例部署
- 对数据一致性要求极高,允许短暂的请求阻塞
2. 分布式锁(多实例部署必选)
如果你的NestJS是多实例集群部署,内存锁无法跨实例生效,此时需要用Redis等分布式锁来控制同一用户的并发请求:
@Injectable() export class DocumentService { constructor( @InjectRepository(Document) private docRepo: Repository<Document>, private redisService: RedisService, ) {} async createDocument(userId: string, docData: DocumentDTO) { const lockKey = `doc_create_lock:${userId}`; const lockTTL = 5000; // 锁过期时间5秒,防止死锁 const redis = this.redisService.getClient(); // 尝试获取锁:NX表示只有锁不存在时才设置,PX指定过期时间 const lockAcquired = await redis.set(lockKey, 'locked', 'NX', 'PX', lockTTL); if (!lockAcquired) { throw new TooManyRequestsException('请求过于频繁,请稍后重试'); } try { // 锁内执行校验和创建逻辑 const docCount = await this.docRepo.count({ where: { userId } }); if (docCount >= 2) { throw new BadRequestException('每个用户最多只能创建2个文档'); } const newDoc = this.docRepo.create({ ...docData, userId }); return await this.docRepo.save(newDoc); } finally { // 无论成功失败都释放锁 await redis.del(lockKey); } } }
注意事项
- 必须设置合理的锁过期时间,避免因服务崩溃导致锁无法释放
- 可以结合Redlock算法实现高可用的分布式锁(适用于Redis集群)
3. 乐观锁(低冲突场景备选)
如果用户并发创建文档的概率极低,可以用乐观锁减少锁阻塞,通过版本号冲突来检测并发修改:
// 先给User实体增加version字段 @Entity() export class User { @PrimaryGeneratedColumn() id: string; @Column({ default: 0 }) version: number; } @Injectable() export class DocumentService { constructor( @InjectRepository(Document) private docRepo: Repository<Document>, @InjectRepository(User) private userRepo: Repository<User>, private connection: Connection, ) {} async createDocument(userId: string, docData: DocumentDTO) { let success = false; const maxRetries = 3; let retries = 0; while (!success && retries < maxRetries) { retries++; try { await this.connection.transaction(async (manager) => { const user = await manager.findOne(User, userId); if (!user) throw new NotFoundException('用户不存在'); const docCount = await manager.count(Document, { where: { userId } }); if (docCount >= 2) throw new BadRequestException('文档数量已达上限'); // 创建文档 const newDoc = manager.create(Document, { ...docData, userId }); await manager.save(newDoc); // 更新用户版本号,触发乐观锁校验 user.version++; await manager.save(user); success = true; }); } catch (err) { // 捕获版本冲突异常,重试请求 if (!(err instanceof QueryFailedError && err.message.includes('version'))) { throw err; } } } if (!success) { throw new TooManyRequestsException('请求过于频繁,请稍后重试'); } } }
适用场景
- 并发冲突概率低的业务场景
- 希望避免请求阻塞,提升系统吞吐量
关键注意事项
- 绝对不要只在应用层做
count()检查再创建,这种无锁逻辑在并发场景下必然失效 - 多实例部署时,内存级别的锁(如用Map存储用户锁)完全无效,必须用分布式锁
- 所有锁相关逻辑必须加
finally块确保锁释放,避免死锁
内容的提问来源于stack exchange,提问作者Jimmy Tudesky
相关产品推荐
相关产品推荐

