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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 13:52:07