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

如何防止多客户端并发调用NestJS预约接口导致重复预约?

解决NestJS + Google Calendar API并发预约重复问题

你的问题核心是竞态条件:多个请求同时执行「查询时段是否空闲→创建预约」的非原子操作,导致都通过校验并创建重复事件。以下是几种实用的解决思路:

1. 后端加分布式锁(推荐多实例部署场景)

用Redis实现分布式锁,确保同一时段同一时间只有一个请求能执行预约逻辑,从根源上阻止并发冲突。

实现步骤:

  • 安装依赖:@nestjs/redis 和 redlock
  • 生成唯一锁键:根据预约的日期、时段生成,比如 lock:appointment:2024-05-20-14-00-14-30(精准到分钟级,和你的预约粒度匹配)
  • 获取锁:设置合理的超时时间(比如10秒,覆盖Google API请求的最大耗时),尝试获取锁;获取失败直接返回“预约繁忙,请重试”
  • 执行业务逻辑:拿到锁后再查询Google Calendar该时段的事件,确认空闲后创建预约
  • 释放锁:用try/finally保证无论成功失败,锁都会被释放

代码示例(NestJS):

import { Injectable } from '@nestjs/common';
import { RedisService } from '@nestjs/redis';
import Redlock from 'redlock';

@Injectable()
export class AppointmentService {
  private redlock: Redlock;

  constructor(private redisService: RedisService) {
    const client = this.redisService.getClient();
    this.redlock = new Redlock([client], {
      driftFactor: 0.01,
      retryCount: 3,
      retryDelay: 200,
    });
  }

  async createAppointment(startTime: Date, endTime: Date, userId: string) {
    // 生成与时段绑定的唯一锁键
    const lockKey = `lock:appointment:${startTime.toISOString().slice(0, 16)}:${endTime.toISOString().slice(0, 16)}`;
    let lock;

    try {
      // 10秒内只有一个请求能拿到锁
      lock = await this.redlock.lock(lockKey, 10000);
      
      // 校验Google Calendar时段是否空闲
      const existingEvents = await this.googleCalendarService.listEvents(startTime, endTime);
      if (existingEvents.length > 0) {
        return { success: false, message: '该时段已被预约' };
      }

      // 创建Google Calendar预约事件
      await this.googleCalendarService.createEvent(startTime, endTime, userId);

      return { success: true };
    } catch (err) {
      if (err instanceof Redlock.LockError) {
        return { success: false, message: 当前预约人数较多,请稍后重试' };
      }
      throw err;
    } finally {
      // 无论结果如何都释放锁
      if (lock) {
        await lock.unlock().catch(() => {});
      }
    }
  }
}

2. 数据库层加唯一约束(单/多实例都适用)

把预约记录存在自己的数据库中,给「预约时段」添加联合唯一索引,利用数据库的原子性约束直接阻止重复插入,同时作为预约状态的唯一可信源。

实现步骤:

  • 在预约表中添加start_time、end_time字段
  • 创建联合唯一索引:比如针对PostgreSQL,执行 CREATE UNIQUE INDEX idx_appointment_time ON appointments (start_time, end_time)
  • 预约流程:先尝试往数据库插入预约记录→插入成功(说明时段空闲)后,再调用Google Calendar API创建事件→如果API调用失败,回滚数据库记录

代码示例(TypeORM):

// 预约实体定义
@Entity()
export class Appointment {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'timestamp' })
  startTime: Date;

  @Column({ type: 'timestamp' })
  endTime: Date;

  @Column()
  userId: string;

  // 给时段添加联合唯一约束
  @Unique(['startTime', 'endTime'])
  timeConstraint: string;
}

// 服务层逻辑
async createAppointment(startTime: Date, endTime: Date, userId: string) {
  const queryRunner = this.connection.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();

  try {
    // 插入数据库,唯一约束会自动阻止重复时段的请求
    const appointment = queryRunner.manager.create(Appointment, {
      startTime,
      endTime,
      userId,
    });
    await queryRunner.manager.save(appointment);

    // 调用Google API创建日历事件
    await this.googleCalendarService.createEvent(startTime, endTime, userId);

    await queryRunner.commitTransaction();
    return { success: true };
  } catch (err) {
    await queryRunner.rollbackTransaction();
    // 捕获PostgreSQL唯一约束冲突错误码
    if (err.code === '23505') {
      return { success: false, message: '该时段已被预约' };
    }
    throw err;
  } finally {
    await queryRunner.release();
  }
}

3. 结合Google Calendar API的幂等机制

Google Calendar API支持通过requestId参数实现幂等:给每个预约请求生成唯一的requestId(比如UUID),24小时内相同的requestId请求会被Google忽略,能有效防止同一用户的重复提交,但无法完全解决跨用户的并发冲突,建议和前两种方案配合使用。

代码片段(Google API请求):

async createEvent(startTime: Date, endTime: Date, userId: string) {
  const requestId = uuidv4(); // 生成唯一requestId
  const event = {
    summary: `用户${userId}的预约`,
    start: { dateTime: startTime.toISOString() },
    end: { dateTime: endTime.toISOString() },
  };

  await this.googleCalendar.events.insert({
    calendarId: 'primary',
    requestBody: event,
    requestId: requestId, // 加入幂等参数
  });
}

优先级建议

  • 单实例部署:数据库唯一约束足够解决问题,实现简单
  • 多实例部署:分布式锁+数据库约束双重保障,避免锁失效导致的漏判
  • 依赖Google Calendar作为唯一数据源:优先用分布式锁,同时搭配requestId做幂等兜底

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 01:24:58