我如何修复实时座位预订系统中的竞争条件(为此失眠)

发布: (2026年3月29日 GMT+8 08:28)
3 分钟阅读
原文: Dev.to

Source: Dev.to

问题

两个用户,一个座位——一片混乱。
用户 A 和用户 B 在几乎同一毫秒查询座位,看到它是可用的,然后继续预订。数据库被更新了两次,导致同一个座位出现了两个拥有者。

失败的尝试

简单的读后写检查

我首先尝试了一个直接的 if 检查:读取座位状态,然后写入新的预订。由于读后写的序列 不是原子操作,在任何真实的并发环境下都会立即失效。两次操作之间的微小窗口正是竞争条件产生的地方。

客户端标志

随后我添加了一个客户端标志来表示锁定。这同样失败——Socket.io 事件可能会乱序到达,UI 状态并不是可靠的真相来源。

解决方案

将读取和写入合并为单个原子 MongoDB 操作,并配合 Socket.io 广播,使每个已连接的客户端能够即时看到锁定。

// reserveSeat.js
async function reserveSeat(tripId, seatNo, userId) {
  // Atomically find the seat if it is still free and mark it as taken
  const seat = await Seats.findOneAndUpdate(
    { tripId, seatNo, taken: false },          // query condition (the guard)
    { $set: { taken: true, userId } },        // update (atomic at document level)
    { returnDocument: 'after' }              // return the updated document
  );

  if (!seat) {
    throw new Error("Seat already taken!");
  }

  // Broadcast the lock to all travelers
  io.emit('seatLocked', {
    seatNo,
    userId,
    message: `Seat ${seatNo} secured for User ${userId}`
  });
}

关键步骤

  1. 查询条件即为守卫——仅匹配仍然空闲的座位。
  2. findOneAndUpdate 在文档层面是原子的——读取和写入在一次操作中完成。
  3. 空值检查是你的错误边界——如果没有返回文档,说明座位已经被占用。
  4. Socket.io 立即广播锁定——所有客户端实时收到更新。

最大的教训

这个修复不仅仅是关于 MongoDB;更是一种思维方式的转变:在并发系统中,永远不要在写入之前依赖读取。把你的守卫逻辑推入数据库操作本身,使其能够保持原子性。

轮到你了

你在自己的项目中遇到过竞争条件吗?欢迎分享你是如何处理的。

0 浏览
Back to Blog

相关文章

阅读更多 »

为什么学习 Node.js?

为什么要学习 Node.js?如果你正踏入开发世界或想要提升为程序员,学习 Node.js 可以成为最具战略性的决定之一……