12306 的本质是高并发、强一致性、分布式事务的票务系统。
一、核心认知:12306 的本质是什么?
▶ 1.业务本质
- 核心实体:
- 车次(Train)
- 座位(Seat)
- 订单(Order)
- 核心流程:
▶ 2.技术挑战
| 挑战 | 初学者简化方案 |
|---|---|
| 高并发 | 单机 MySQL + 乐观锁 |
| 分布式事务 | 本地事务 + 补偿机制 |
| 余票计算 | 预分配座位池 |
💡核心原则:
先实现单机版,再考虑分布式
二、阶段 1:单机 MVP(1 天)
▶ 1.数据库设计
-- 车次表CREATETABLEtrains(idINTPRIMARYKEYAUTO_INCREMENT,train_noVARCHAR(20)NOTNULL,-- G1234departureVARCHAR(50)NOTNULL,-- 北京南arrivalVARCHAR(50)NOTNULL,-- 上海虹桥depart_timeDATETIMENOTNULL);-- 座位表(预分配)CREATETABLEseats(idINTPRIMARYKEYAUTO_INCREMENT,train_idINTNOTNULL,seat_noVARCHAR(10)NOTNULL,-- 05车12AstatusTINYINTDEFAULT0,-- 0=可用, 1=已锁定, 2=已售FOREIGNKEY(train_id)REFERENCEStrains(id));-- 订单表CREATETABLEorders(idVARCHAR(32)PRIMARYKEY,-- UUIDuser_idINTNOTNULL,train_idINTNOTNULL,seat_idINTNOTNULL,statusTINYINTDEFAULT0,-- 0=待支付, 1=已支付created_atDATETIMEDEFAULTCURRENT_TIMESTAMP,FOREIGNKEY(seat_id)REFERENCESseats(id));▶ 2.PHP 基础代码(无框架)
db.php(数据库连接):
<?php$pdo=newPDO('mysql:host=localhost;dbname=12306','root','',[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);query.php(查询余票):
<?phprequire'db.php';$trainId=$_GET['train_id'];$stmt=$pdo->prepare(" SELECT COUNT(*) as available FROM seats WHERE train_id = ? AND status = 0 ");$stmt->execute([$trainId]);echojson_encode($stmt->fetch());lock.php(锁定座位):
<?phprequire'db.php';$trainId=$_POST['train_id'];// 乐观锁:仅锁定可用座位$pdo->beginTransaction();try{$stmt=$pdo->prepare(" SELECT id FROM seats WHERE train_id = ? AND status = 0 LIMIT 1 FOR UPDATE ");$stmt->execute([$trainId]);$seat=$stmt->fetch();if(!$seat){thrownewException('No seats available');}// 锁定座位$pdo->prepare("UPDATE seats SET status = 1 WHERE id = ?")->execute([$seat['id']]);// 创建订单$orderId=uniqid();$pdo->prepare(" INSERT INTO orders (id, user_id, train_id, seat_id) VALUES (?, 1, ?, ?) ")->execute([$orderId,$trainId,$seat['id']]);$pdo->commit();echojson_encode(['order_id'=>$orderId]);}catch(Exception$e){$pdo->rollback();http_response_code(400);echojson_encode(['error'=>$e->getMessage()]);}
三、阶段 2:核心功能增强(3 天)
▶ 1.余票缓存(防超卖)
- 问题:
- 高并发下
SELECT ... FOR UPDATE性能差
- 高并发下
- 解决方案:
-- 新增余票计数表CREATETABLEtrain_inventory(train_idINTPRIMARYKEY,available_seatsINTNOTNULL,versionINTDEFAULT0-- 乐观锁版本号); - 锁定逻辑:
// 先扣减库存$stmt=$pdo->prepare(" UPDATE train_inventory SET available_seats = available_seats - 1, version = version + 1 WHERE train_id = ? AND available_seats > 0 ");if($stmt->execute([$trainId])&&$stmt->rowCount()>0){// 再分配具体座位// ...}
▶ 2.订单超时释放
- 方案:
- 定时任务扫描 15 分钟未支付订单
- 释放座位并回滚库存
- crontab:
*/5 * * * * php /path/to/release_expired_orders.php
▶ 3.基础安全防护
- SQL 注入:
- 全程使用
PDO::prepare()
- 全程使用
- XSS:
- 输出时
htmlspecialchars()
- 输出时
- CSRF:
- 表单添加隐藏 Token 字段
四、阶段 3:性能与扩展(7 天+)
▶ 1.引入 Redis 缓存
- 缓存余票:
// 查询余票$available=$redis->get("train:{$trainId}:seats");if($available===false){// 从 DB 加载并缓存$available=getFromDB();$redis->setex("train:{$trainId}:seats",30,$available);}
▶ 2.队列解耦(Swoole)
- 架构:
- 优势:
- Web 层快速响应
- 异步处理高耗时操作
▶ 3.分库分表(未来扩展)
- 拆分策略:
- 按车次日期分表(
orders_20231001) - 按用户 ID 分库
- 按车次日期分表(
五、避坑指南
| 陷阱 | 破局方案 |
|---|---|
| 直接实现分布式事务 | 先用本地事务 + 补偿机制 |
| 过度设计缓存 | 余票缓存足够,避免复杂多级缓存 |
| 忽略数据一致性 | 用FOR UPDATE或 Redis Lua 脚本保证原子性 |
六、终极心法
**“12306 不是系统,
而是领域的显影——
- 当你建模实体,
你在校准边界;- 当你简化并发,
你在聚焦核心;- 当你渐进扩展,
你在铸造韧性。真正的工程能力,
始于对领域的敬畏,
成于对细节的精控。”
结语
从今天起:
- 先实现单机版余票查询 + 锁定
- 用
FOR UPDATE保证基础一致性 - 逐步引入 Redis/Swoole 优化
因为最好的高并发系统,
不是一蹴而就,
而是亲手活出每一行代码的密度。