1. 项目概述
最近在做一个Node.js的机票预订系统,这个项目让我对航空售票领域的业务逻辑和技术实现有了更深入的理解。机票预订系统看似简单,但背后涉及复杂的业务规则和实时性要求,比如航班查询、座位锁定、支付超时处理等。这个系统需要处理高并发的查询请求,同时保证数据一致性和系统可靠性。
航空售票系统与传统电商系统最大的区别在于其实时性和库存的特殊性。机票库存不像普通商品可以随意增减,每个航班的座位数是固定的,而且需要处理复杂的舱位等级、价格浮动、超售规则等业务场景。这些特性决定了系统架构需要特别关注并发控制和事务管理。
2. 系统架构设计
2.1 技术栈选择
选择Node.js作为后端主要基于以下几个考虑:
- 高并发I/O密集型场景下Node.js的性能优势
- 全栈JavaScript的开发效率
- 丰富的npm生态支持
核心组件包括:
- Express.js作为Web框架
- MongoDB存储航班和订单数据
- Redis用于缓存和分布式锁
- RabbitMQ处理异步任务
- Jest进行单元测试
2.2 微服务划分
系统拆分为以下服务:
- 航班服务:管理航班信息和座位库存
- 预订服务:处理订单创建和支付流程
- 用户服务:处理用户认证和权限
- 通知服务:发送短信和邮件通知
- 报表服务:生成销售和运营报表
这种划分遵循了单一职责原则,每个服务可以独立开发、部署和扩展。
3. 核心功能实现
3.1 航班查询与缓存
航班查询是最频繁的操作,需要特别优化:
javascript复制// 使用Redis缓存热门航线数据
async function getFlights(query) {
const cacheKey = `flights:${query.origin}-${query.destination}-${query.date}`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const flights = await Flight.find({
origin: query.origin,
destination: query.destination,
date: query.date,
seatsAvailable: { $gt: 0 }
}).lean();
await redis.setex(cacheKey, 300, JSON.stringify(flights)); // 缓存5分钟
return flights;
}
3.2 座位锁定机制
为了防止超售,采用分布式锁实现座位预留:
javascript复制async function reserveSeat(flightId, seatId, userId) {
const lockKey = `lock:${flightId}:${seatId}`;
const lock = await redis.set(lockKey, userId, 'EX', 30, 'NX'); // 30秒锁
if (!lock) {
throw new Error('座位已被锁定');
}
try {
const flight = await Flight.findById(flightId);
if (!flight.seats.includes(seatId)) {
throw new Error('无效座位');
}
await Flight.updateOne(
{ _id: flightId },
{ $pull: { seats: seatId }, $inc: { seatsAvailable: -1 } }
);
return true;
} finally {
await redis.del(lockKey);
}
}
3.3 订单状态机
订单状态流转是核心业务逻辑:
javascript复制class Order {
constructor() {
this.state = 'PENDING';
this.transitions = {
PENDING: ['CONFIRMED', 'CANCELLED'],
CONFIRMED: ['COMPLETED', 'REFUNDING'],
REFUNDING: ['REFUNDED']
};
}
transitionTo(newState) {
if (!this.transitions[this.state].includes(newState)) {
throw new Error(`无效状态转换: ${this.state} -> ${newState}`);
}
this.state = newState;
}
}
4. 高并发处理
4.1 库存扣减策略
采用乐观锁解决库存竞争问题:
javascript复制async function bookFlight(flightId, seats) {
const session = await mongoose.startSession();
session.startTransaction();
try {
const flight = await Flight.findById(flightId).session(session);
if (flight.seatsAvailable < seats.length) {
throw new Error('座位不足');
}
await Flight.updateOne(
{ _id: flightId, version: flight.version },
{
$pull: { seats: { $in: seats } },
$inc: { seatsAvailable: -seats.length, version: 1 }
}
).session(session);
await session.commitTransaction();
return true;
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
session.endSession();
}
}
4.2 限流与熔断
使用令牌桶算法保护系统:
javascript复制const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 每个IP限制100次请求
message: '请求过于频繁,请稍后再试'
});
app.use('/api/search', limiter);
5. 支付流程设计
5.1 支付超时处理
使用延时队列处理未支付订单:
javascript复制async function createOrder(orderData) {
const order = await Order.create(orderData);
// 15分钟后检查支付状态
await channel.sendToQueue('payment_timeout',
Buffer.from(JSON.stringify({ orderId: order._id })),
{ expiration: 900000 } // 15分钟
);
return order;
}
// 消费者处理超时订单
channel.consume('payment_timeout', async (msg) => {
const { orderId } = JSON.parse(msg.content.toString());
const order = await Order.findById(orderId);
if (order.status === 'PENDING') {
await cancelOrder(orderId);
await releaseSeats(order.flightId, order.seats);
}
channel.ack(msg);
});
5.2 支付网关集成
封装统一的支付接口:
javascript复制class PaymentGateway {
constructor(provider) {
this.provider = provider;
}
async pay(order) {
switch (this.provider) {
case 'alipay':
return this._alipayPay(order);
case 'wechat':
return this._wechatPay(order);
default:
throw new Error('不支持的支付方式');
}
}
async _alipayPay(order) {
// 支付宝支付实现
}
async _wechatPay(order) {
// 微信支付实现
}
}
6. 系统监控与运维
6.1 性能监控
使用Prometheus收集指标:
javascript复制const client = require('prom-client');
const collectDefaultMetrics = client.collectDefaultMetrics;
collectDefaultMetrics({ timeout: 5000 });
app.get('/metrics', async (req, res) => {
res.set('Content-Type', client.register.contentType);
res.end(await client.register.metrics());
});
// 自定义业务指标
const orderCounter = new client.Counter({
name: 'orders_total',
help: 'Total number of orders',
labelNames: ['status']
});
6.2 日志收集
结构化日志便于分析:
javascript复制const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// 记录业务日志
logger.info('Order created', {
orderId: '123',
userId: '456',
amount: 999
});
7. 测试策略
7.1 单元测试
使用Jest测试核心业务逻辑:
javascript复制describe('Flight Service', () => {
it('should reserve seats correctly', async () => {
const flight = await createTestFlight();
const result = await reserveSeats(flight._id, ['A1', 'A2']);
expect(result).toBeTruthy();
const updated = await Flight.findById(flight._id);
expect(updated.seatsAvailable).toBe(flight.seatsAvailable - 2);
});
});
7.2 压力测试
使用Artillery模拟高并发:
yaml复制config:
target: "http://localhost:3000"
phases:
- duration: 60
arrivalRate: 50
scenarios:
- flow:
- get:
url: "/api/flights?origin=PEK&destination=SHA&date=2023-12-01"
- post:
url: "/api/orders"
json:
flightId: "123"
seats: ["A1"]
8. 安全考虑
8.1 数据加密
敏感信息加密存储:
javascript复制const crypto = require('crypto');
function encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(
'aes-256-cbc',
Buffer.from(process.env.ENCRYPTION_KEY),
iv
);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return iv.toString('hex') + ':' + encrypted.toString('hex');
}
8.2 API安全
JWT认证实现:
javascript复制const jwt = require('jsonwebtoken');
function generateToken(user) {
return jwt.sign(
{ userId: user._id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
}
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).send('未提供认证令牌');
}
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
res.status(403).send('无效令牌');
}
}
9. 部署方案
9.1 Docker化部署
使用Docker Compose编排服务:
dockerfile复制# Node.js服务Dockerfile
FROM node:16
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
yaml复制# docker-compose.yml
version: '3'
services:
app:
build: .
ports:
- "3000:3000"
depends_on:
- mongo
- redis
mongo:
image: mongo
volumes:
- mongo_data:/data/db
redis:
image: redis
volumes:
mongo_data:
9.2 CI/CD流程
GitHub Actions自动化部署:
yaml复制name: Node.js CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm install
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: docker-compose up -d --build
10. 经验总结
在开发这个机票预订系统的过程中,有几个关键点值得特别注意:
-
库存一致性是最难解决的问题,我们最终采用了Redis分布式锁+数据库乐观锁的组合方案,在性能和一致性之间取得了平衡。
-
支付超时处理是机票系统的特殊需求,通过消息队列的延时消息功能可以优雅地实现,避免了轮询检查的开销。
-
微服务划分要合理,初期我们把用户服务和订单服务合并导致了后期扩展困难,后来不得不重新拆分。
-
监控系统要尽早建立,特别是在生产环境出现性能问题时,完善的监控指标能快速定位瓶颈。
-
压力测试要模拟真实场景,我们最初只测试了查询接口,导致上线后订单创建接口在高并发下出现问题。