如何防止多客户端并发调用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
相关产品推荐
相关产品推荐

