1. 项目概述
最近帮朋友开发了一套演唱会门票购票系统,用Node.js+Express+MongoDB技术栈实现。这个系统上线后成功支撑了多场万人演唱会的票务销售,峰值时每分钟处理超过5000笔订单。今天就来拆解下这个系统的设计思路和关键技术实现。
演唱会票务系统有几个典型痛点:高并发抢票、防黄牛刷票、座位锁定机制、支付超时处理等。传统PHP或Java方案要么性能不足,要么开发效率低。而Node.js的非阻塞I/O特性特别适合这种I/O密集型的场景,配合MongoDB的灵活文档结构,从技术选型上就占据了优势。
2. 技术栈选型解析
2.1 为什么选择Node.js
Node.js的异步非阻塞特性在处理高并发请求时有天然优势。实测表明,在4核8G的服务器上,单个Node进程可以轻松支撑3000+的并发连接。对于票务系统这种短连接高并发的场景特别合适。
另一个重要原因是前后端语言统一。我们的前端使用React,后端用Node.js,全栈JavaScript开发效率极高。比如共享DTO类型定义、复用工具函数等,这在多端协作时优势明显。
2.2 Express框架的轻量之道
相比Koa或NestJS,Express虽然"古老"但生态成熟。特别是以下特性对我们很有用:
- 中间件机制灵活:可以按需组合身份验证、请求日志等模块
- 路由系统简洁:RESTful API开发效率高
- 错误处理统一:通过错误中间件集中处理业务异常
实测中,Express在3000QPS压力下CPU占用率仍能保持在60%以下,完全满足需求。
2.3 MongoDB的灵活数据模型
票务数据有几个特点:
- 场次信息结构化程度高
- 订单数据变化频繁
- 需要频繁查询余票
MongoDB的文档模型可以很自然地映射这些业务实体。比如一个演出场次文档:
javascript复制{
"_id": "5f8d...",
"name": "周杰伦2023演唱会",
"date": ISODate("2023-11-20T19:30:00Z"),
"venue": {
"name": "北京鸟巢",
"sections": [
{
"name": "A区",
"price": 1680,
"total": 2000,
"available": 345 // 动态更新的余票数
}
]
}
}
这种嵌套文档结构比关系型数据库的多表关联查询效率更高,特别是在高并发余票查询场景。
3. 核心模块设计与实现
3.1 演出场次管理
场次管理采用经典的CRUD模式,但有几点特殊处理:
- 场次状态机设计:
javascript复制const statusMachine = {
draft: ['published', 'canceled'],
published: ['selling', 'canceled'],
selling: ['sold_out', 'canceled'],
sold_out: ['completed'],
completed: [],
canceled: []
}
- 场次修改的乐观锁控制:
javascript复制router.put('/:id', async (req, res) => {
const result = await Show.updateOne(
{ _id: req.params.id, version: req.body.version },
{ $set: req.body, $inc: { version: 1 } }
);
if (result.nModified === 0) {
throw new ConcurrencyConflictError();
}
});
3.2 购票业务流程
购票是系统最复杂的部分,核心流程如下:
- 查询余票(使用MongoDB的聚合管道):
javascript复制const pipeline = [
{ $match: { _id: showId } },
{ $unwind: '$venue.sections' },
{ $project: {
section: '$venue.sections.name',
available: '$venue.sections.available'
}
}
];
- 座位锁定机制(Redis实现):
javascript复制const lockKey = `seat_lock:${showId}:${section}`;
const locked = await redis.set(lockKey, userId, 'NX', 'EX', 300); // 锁定5分钟
if (!locked) {
throw new SeatLockedError();
}
- 订单创建(MongoDB事务):
javascript复制const session = await mongoose.startSession();
session.startTransaction();
try {
const order = new Order({...});
await order.save({ session });
await Show.updateOne(
{ _id: showId, 'venue.sections.name': section },
{ $inc: { 'venue.sections.$.available': -1 } },
{ session }
);
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
throw err;
}
3.3 支付处理
支付流程的几个关键点:
- 支付超时处理(使用Redis过期事件):
javascript复制// 订单创建时
await redis.set(`order:${orderId}`, 'pending', 'EX', 900); // 15分钟超时
// 订阅过期事件
redis.subscribe('__keyevent@0__:expired', (err) => {
// 处理超时未支付订单
});
- 支付结果异步通知:
javascript复制router.post('/payment/notify', verifyPaymentSignature, async (req, res) => {
const order = await Order.findById(req.body.orderId);
if (order.status !== 'paid') {
order.status = 'paid';
await order.save();
// 触发后续业务逻辑
}
res.send('success');
});
4. 高并发优化实践
4.1 缓存策略
- 多级缓存架构:
- CDN缓存静态资源
- Redis缓存热点数据(如余票信息)
- 本地内存缓存(使用node-cache)
- 余票缓存更新策略:
javascript复制// 查询余票时
async function getAvailableTickets(showId) {
const cacheKey = `available:${showId}`;
let available = await redis.get(cacheKey);
if (!available) {
available = await computeAvailableTickets(showId); // 从数据库计算
await redis.set(cacheKey, available, 'EX', 30); // 缓存30秒
}
return available;
}
4.2 限流与防刷
- 接口限流(使用express-rate-limit):
javascript复制const limiter = rateLimit({
windowMs: 60 * 1000, // 1分钟
max: 100, // 每个IP100次请求
handler: (req, res) => {
res.status(429).json({ code: 'TOO_MANY_REQUESTS' });
}
});
- 防机器人验证:
- 关键操作要求短信验证码
- 行为分析识别异常请求模式
- 图形验证码(使用svg-captcha)
4.3 负载均衡
- 水平扩展方案:
- 使用PM2集群模式启动多个Node进程
- Nginx做负载均衡
- Redis共享session
- 进程间通信:
javascript复制// 使用PM2的消息系统
process.on('message', (msg) => {
if (msg.type === 'invalidate_cache') {
// 处理缓存失效
}
});
5. 监控与运维
5.1 性能监控
- 关键指标采集:
- 使用Prometheus采集QPS、响应时间等指标
- 自定义业务指标(如购票成功率)
- 告警规则示例:
yaml复制groups:
- name: ticket-service
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[1m]) > 0.1
for: 5m
5.2 日志收集
- 结构化日志(使用winston):
javascript复制const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [new winston.transports.File({ filename: 'combined.log' })]
});
- 日志查询分析:
- ELK栈收集分析日志
- 关键业务日志单独存储(如支付日志)
5.3 持续部署
- 部署流程:
- GitHub Actions自动化测试
- Docker镜像构建
- Kubernetes滚动更新
- 回滚机制:
bash复制kubectl rollout undo deployment/ticket-service --to-revision=2
6. 踩坑与经验
6.1 MongoDB连接池优化
初期遇到连接泄漏问题,后来通过以下方式解决:
- 合理设置连接池大小(建议是CPU核数的3-5倍)
- 使用连接池监控中间件
- 为长时间操作单独创建连接
6.2 事务使用注意事项
MongoDB事务有几个坑要注意:
- 事务中的查询要使用相同的session
- 事务持续时间不宜过长(建议<1秒)
- 避免在事务中做网络IO
6.3 内存泄漏排查
Node.js应用常见内存泄漏场景:
- 未清理的定时器
- 全局变量积累
- 未关闭的数据库连接
排查工具:
- heapdump生成内存快照
- Chrome DevTools分析内存占用
7. 安全实践
7.1 接口安全
- 认证鉴权:
- JWT令牌认证
- RBAC权限控制
- 敏感操作二次验证
- 输入验证:
javascript复制router.post('/orders', [
body('showId').isMongoId(),
body('section').isString().notEmpty(),
body('count').isInt({ min: 1, max: 4 })
], createOrder);
7.2 数据安全
- 敏感数据加密:
- 用户密码加盐哈希
- 支付信息AES加密存储
- 数据库透明加密
- 防注入措施:
- Mongoose内置防查询注入
- 避免直接拼接查询字符串
7.3 运维安全
- 基础设施安全:
- 最小权限原则
- 网络隔离
- 定期漏洞扫描
- 应急响应:
- 安全事件预案
- 关键操作审计日志
- 数据备份策略
这个系统从设计到上线历时3个月,期间遇到了各种挑战,但最终成功支撑了多场大型演唱会的票务销售。Node.js+MongoDB的组合在开发效率和运行性能上都表现优异,特别适合此类高并发的互联网应用。
