基于Vert.x、Node.js的异步机票预订系统同步问题咨询
为什么不适合用同步阻塞机制
Vert.x 和 Node.js 都是基于异步非阻塞事件循环设计的,核心优势就是用少量线程处理大量并发请求。如果在这类应用里强行引入同步锁或阻塞式操作,会直接卡住事件循环——Node.js 是单事件循环,一旦阻塞整个服务都会停摆;Vert.x 的多事件循环也会因为线程被占用导致吞吐量暴跌,最终引发性能问题和极差的用户体验。所以绝对不适合直接用同步阻塞的方式解决这个问题。
可行的解决方案
1. MongoDB 原子更新操作(最简单高效)
利用 MongoDB 的原子性操作,直接在更新语句里判断剩余票数并扣减,一步完成并发控制。因为updateOne是原子执行的,多个请求同时操作同一张票时,只有一个能满足条件并成功修改,其他请求会收到modifiedCount = 0的结果。
Node.js 示例代码:
async function bookTicket(ticketId, userId) { // 原子扣减票数,仅当剩余票数>0时执行 const updateResult = await db.collection('tickets').updateOne( { _id: ticketId, remaining: { $gt: 0 } }, { $inc: { remaining: -1 } } ); if (updateResult.modifiedCount === 0) { return { success: false, message: '票已售罄或被他人抢先预订' }; } // 保存订单记录 await db.collection('orders').insertOne({ ticketId, userId, createdAt: new Date() }); return { success: true, message: '订票成功' }; }
Vert.x 示例(异步回调风格):
mongoClient.updateOne("tickets", new JsonObject().put("_id", ticketId).put("remaining", new JsonObject().put("$gt", 0)), new JsonObject().put("$inc", new JsonObject().put("remaining", -1)), res -> { if (res.succeeded()) { UpdateResult result = res.result(); if (result.getModifiedCount() == 0) { ctx.response().end(new JsonObject().put("success", false).put("message", "票已售罄").encode()); return; } // 保存订单 mongoClient.insert("orders", new JsonObject() .put("ticketId", ticketId) .put("userId", userId) .put("createdAt", new Date()), orderRes -> { if (orderRes.succeeded()) { ctx.response().end(new JsonObject().put("success", true).put("message", "订票成功").encode()); } else { // 处理订单保存失败的回滚逻辑(可选) } }); } else { // 处理数据库错误 } });
2. 乐观锁机制(适合复杂业务场景)
如果你的订票逻辑需要先查询机票详细信息(比如价格、舱位规则)再执行扣减,可以用乐观锁:给机票文档加一个version字段,更新时带上当前查询到的版本号,确保更新的是最新的文档。
核心逻辑:
- 查询机票时获取当前
version值 - 更新时添加条件
{ version: 当前版本号 },同时将version自增1 - 如果更新返回的
modifiedCount为0,说明有其他请求先修改了文档,此时重试或告知用户抢票失败
3. 分布式锁(跨服务/多实例场景)
如果你的两个项目是多实例部署,或者跨 Vert.x/Node.js 服务共享机票数据,需要用分布式锁来保证全局唯一操作。可以用 MongoDB 的findAndModify实现:
- 抢锁:插入/更新锁文档,条件是锁不存在或已过期
- 拿到锁后执行订票逻辑,完成后释放锁
- 拿不到锁则返回用户重试
注意设置合理的锁过期时间,避免服务异常导致锁无法释放。
4. 消息队列削峰(高并发秒杀场景)
如果遇到秒杀级别的高并发请求,把订票请求先放到消息队列(比如 Vert.x EventBus、RabbitMQ),用单线程或有限线程串行消费队列中的请求,自然避免并发冲突。用户提交请求后会收到“排队处理中”的提示,后台按顺序处理,既保证一致性,又不会阻塞事件循环。
总结
- 禁止使用同步阻塞机制,会彻底破坏异步应用的性能优势
- 优先选择 MongoDB 原子更新,简单高效,完全符合异步非阻塞设计
- 复杂业务用乐观锁,跨实例用分布式锁,高并发场景用消息队列削峰
内容的提问来源于stack exchange,提问作者Võ Khắc Bảo

