学完 12306 分布式票务项目后,我对高并发系统设计的整体理解
前段时间我系统学习了一个 12306 分布式票务项目。刚开始看这个项目时,我更多关注的是“用了哪些技术”:Redis、RocketMQ、ShardingSphere、Canal、Redisson、Sentinel、Spring Boot Starter 等。
但真正把源码、业务链路和面试问题串起来之后,我发现这个项目最值得学习的地方并不是某一个单独技术点,而是它围绕“高并发购票”这个核心场景,把缓存、消息队列、分库分表、幂等、分布式锁、异常处理、敏感数据保护等能力组合到了一起。
这篇文章是我学完整个项目后的总结,不再单独展开某一个知识点,而是从整体角度复盘:这个项目解决了什么问题、核心链路怎么设计、每个技术点在里面承担什么角色,以及我学完之后对高并发系统设计有哪些新的理解。
一、我对这个项目整体定位的理解
这个项目模拟的是 12306 购票系统,核心业务包括:
- 用户注册、登录、乘车人管理
- 车次查询、余票查询
- 用户购票、选座、生成订单
- 支付回调、订单状态变更
- 超时未支付自动关单
- 订单查询、冷热数据区分
- 余票缓存同步、库存回滚
从服务划分来看,项目不是一个单体应用,而是拆成了多个微服务:
user-service 用户服务
ticket-service 车票服务
order-service 订单服务
pay-service 支付服务
aggregation-service 聚合服务
gateway-service 网关服务
我的理解是,这种拆分不是为了“微服务而微服务”,而是围绕业务边界拆的:
- 用户服务负责用户身份和乘车人信息。
- 车票服务负责车次、座位、余票和购票核心逻辑。
- 订单服务负责订单生命周期。
- 支付服务负责支付单、支付回调、退款。
- 聚合服务负责把多个服务的数据组装给前端。
- 网关服务负责统一入口。
这让我理解到,微服务拆分的关键不是服务数量,而是边界是否清晰。比如购票下单一定会涉及车票和订单,但车票服务不应该直接管理订单表,订单服务也不应该直接操作座位库存,所以两者之间通过远程调用和消息机制协作。
二、整个购票链路是我认为最核心的主线
学习这个项目时,我觉得最应该先抓住的是购票主链路。
大致流程可以理解为:
用户查询车次
↓
选择车次和乘车人
↓
提交购票请求
↓
令牌桶校验余票
↓
座位选择与锁定
↓
创建订单
↓
发送延迟关单消息
↓
用户支付
↓
支付回调更新订单状态
如果用户没有在规定时间内支付:
RocketMQ 延迟消息到达
↓
消费端查询订单状态
↓
如果仍是待支付,关闭订单
↓
释放座位
↓
回滚 Redis 余票和令牌桶
这个链路把很多技术点串在了一起:
| 余票校验 | Redis Hash + Lua 令牌桶 |
| 并发扣减 | Lua 原子操作 |
| 座位锁定 | 数据库座位状态 + 缓存预扣 |
| 订单生成 | 分库分表 + 雪花 ID |
| 超时关单 | RocketMQ 延迟消息 |
| 重复消费 | 幂等框架 |
| 并发状态修改 | Redisson 分布式锁 |
| 缓存一致性 | Canal + RocketMQ + 策略模式 |
学完后我最大的感受是:高并发系统不是靠某一个技术解决问题,而是靠一整套链路协作。
Redis 解决的是“别让所有请求都打数据库”。
Lua 解决的是“并发下校验和扣减必须原子”。
RocketMQ 解决的是“异步解耦和延迟补偿”。
幂等解决的是“消息重复、请求重复不能重复执行业务”。
分布式锁解决的是“多个节点同时改同一笔订单状态时不能乱”。
Canal 解决的是“数据库变化后缓存最终要同步”。
这些技术点单独看都不复杂,但放到购票链路里,才体现出价值。
三、我对“防超卖”的理解发生了变化
一开始我理解的防超卖比较简单:库存扣减时加锁,或者数据库 update 加条件。
但学习这个项目后,我发现真正的高并发防超卖不能只靠数据库。原因很简单:如果所有购票请求都直接打数据库,数据库很快就会扛不住。
所以项目在数据库之前做了多层削峰和预扣:
请求进入
↓
Redis 令牌桶判断余票是否足够
↓
Lua 原子扣减令牌
↓
进入座位锁定和订单创建
↓
数据库最终落库
项目里的令牌桶不是传统意义上“按固定速率生成令牌”的限流桶,而更像是“按车次、区间、座位类型维护的余票令牌容器”。
它的 Redis 结构大致是:
Key: ticket_availability_token_bucket:{trainId}
Field: {startStation}_{endStation}_{seatType}
Value: 当前区间当前座位类型剩余令牌数
购票时,Lua 脚本会先判断所有相关区间的令牌是否足够。如果足够,再一次性扣减。这样可以保证两个动作是原子的:
判断余票是否足够
+
扣减余票令牌
↓
一个 Lua 脚本内完成,不会被其他请求插入
这让我理解到:防超卖不是简单“加锁”,而是要根据并发量、数据库压力、业务一致性要求综合设计。
项目里的防超卖大致可以分三层:
第一层是 Redis 预扣减,挡住大部分无效请求。
第二层是 Lua 原子操作,保证缓存层不会并发扣乱。
第三层是数据库座位状态,作为最终兜底。
如果订单超时未支付,还会通过延迟消息释放座位并回滚令牌桶。这说明防超卖不只包含“扣减”,还必须包含“回滚”。只会扣不会回滚,系统最终也会不一致。
四、我对 RocketMQ 的理解不再停留在“发消息”
这个项目里 RocketMQ 不只是用来异步发送通知,而是参与了多个关键业务流程。
我印象最深的是延迟关单。
创建订单后,项目会发送一条延迟消息。消息到期后,消费者会检查订单状态:
- 如果订单仍然是待支付,就关闭订单、释放座位、回滚余票。
- 如果订单已经支付,就直接跳过。
这让我理解到,延迟消息本质上是在做一种“未来某个时间点的业务补偿”。
如果不用 MQ,也可以用定时任务扫表,比如每分钟扫描一次超时订单。但定时任务有几个问题:
- 扫表成本高,订单量大时压力明显。
- 时间精度不稳定,可能延迟较大。
- 多实例部署时还要解决重复扫描和并发处理。
延迟消息的优势是:每个订单创建时就绑定一条未来要执行的检查任务,到点后消费即可。
不过学习源码时我也注意到一个细节:RocketMQ 的延迟级别不是任意时间,而是固定等级。项目代码里用了 delayLevel(14),按 RocketMQ 默认配置对应的是 10 分钟,不是 30 分钟。如果要 30 分钟,默认应该用 level 16。
这个点让我意识到:学习项目不能只看注释,还要结合框架本身的机制核对。
RocketMQ 在项目里还有一个重要价值是解耦。
比如支付服务收到支付回调后,不应该直接强耦合订单服务内部逻辑,而是通过消息通知订单服务处理支付结果。这样支付和订单之间的耦合会更低,后续扩展也更方便。
五、幂等框架是我觉得最值得学习的工程化设计之一
高并发系统里,请求重复和消息重复是很常见的。
比如:
- 用户连续点击提交按钮。
- 网络抖动导致前端重试。
- MQ 消费失败后重新投递。
- 服务超时后调用方再次发起请求。
如果没有幂等保护,同一个业务可能被执行多次。对购票系统来说,这可能导致重复下单、重复释放座位、重复回滚库存等问题。
项目里封装了一个通用幂等框架,核心是:
@Idempotent 注解
↓
AOP 拦截方法
↓
根据注解类型选择处理器
↓
生成唯一幂等 key
↓
Redis 原子写入 key
↓
key 已存在则说明重复请求
它支持三类幂等方式:
| PARAM | HTTP 参数幂等 | 根据请求参数生成唯一标识 |
| TOKEN | 表单防重复提交 | 先申请 token,再提交时消费 token |
| SPEL | 灵活表达式 | MQ 或复杂对象场景下用 SpEL 取字段 |
我觉得这个设计最有价值的地方是:它不是在业务代码里到处写 Redis 判断,而是通过注解和 AOP 做成了框架能力。
业务方法只需要这样标记:
@Idempotent(
uniqueKeyPrefix = "index12306-ticket:delay_close_order:",
key = "#message.getKeys() + '_' + #message.hashCode()",
type = IdempotentTypeEnum.SPEL,
scene = IdempotentSceneEnum.MQ,
keyTimeout = 7200L
)
剩下的事情交给框架。
这让我理解到:工程化能力的一个重要体现,就是把横切逻辑从业务代码中抽出来。幂等、日志、异常、鉴权、限流都属于这类横切逻辑。
六、策略模式让我理解了“少写 if-else”的真正意义
以前我理解策略模式时,更多停留在设计模式书上的例子,比如不同支付方式、不同折扣策略。
这个项目里策略模式的落地更工程化。
项目封装了两个核心类:
AbstractExecuteStrategy
AbstractStrategyChoose
所有策略处理器实现 AbstractExecuteStrategy,并通过 mark() 返回自己的标识。项目启动时,AbstractStrategyChoose 会从 Spring 容器中拿到所有策略 Bean,放进一个 Map:
Map<mark, 策略实例>
运行时根据 mark 找到对应策略执行。
我觉得这个设计最典型的应用是 Canal binlog 同步缓存。
Canal 消费者拿到 binlog 消息后,并不直接写:
if (table.equals("t_seat")) {
// 更新余票缓存
} else if (table.equals("t_order")) {
// 订单关闭回滚
}
而是:
abstractStrategyChoose.chooseAndExecute(message.getTable(), message, predicateFlag);
然后不同表对应不同处理器:
- TicketAvailabilityCacheUpdateHandler 处理座位表变化。
- OrderCloseCacheAndTokenUpdateHandler 处理订单关闭后的缓存和令牌桶回滚。
这让我理解到:策略模式不是为了“看起来高级”,而是为了把变化点隔离出来。
如果后续新增一张表的 binlog 同步逻辑,不需要改消费者主流程,只需要新增一个策略处理器即可。
这就是开闭原则在项目里的真实体现。
七、Canal + MQ 让我理解了缓存一致性的另一种方案
在项目里,Redis 缓存不是简单地在业务代码里手动更新。项目使用 Canal 监听 MySQL binlog,再通过 RocketMQ 把变更事件发送出来,由消费者更新 Redis。
完整链路可以理解为:
业务更新 MySQL
↓
MySQL 写 binlog
↓
Canal 监听 binlog
↓
Canal 投递 MQ 消息
↓
项目消费者消费消息
↓
根据表名路由到不同策略处理器
↓
更新 Redis 缓存
这个设计的好处是:业务代码不用强依赖 Redis 更新逻辑。数据库发生变化后,通过 binlog 驱动缓存同步。
我觉得它适合几个场景:
- 多个服务都可能修改数据库,但缓存更新逻辑希望统一。
- 不想把缓存同步代码散落在业务逻辑里。
- 接受短暂延迟,追求最终一致性。
但这个方案也不是没有代价。
它是异步的,所以一定存在延迟。也就是说,数据库更新成功后,Redis 可能短时间内还是旧数据。因此这种方案更适合最终一致性场景,不适合强一致读写。
项目里通过策略模式处理不同表的 binlog,我觉得这是 Canal 这条链路里比较好的设计:消费者只负责接收事件,具体怎么更新缓存交给策略处理器。
八、分库分表让我理解了“分片键比框架更重要”
这个项目里订单服务和用户服务都使用了 ShardingSphere。
订单服务是 2 库 32 表,订单表按 user_id + order_sn 复合分片。
用户服务也是 2 库 32 表,但不同表分片键不同:
- t_user 按 username
- t_passenger 按 username
- t_user_mail 按 mail
- t_user_phone 按 phone
学习这部分后,我最大的收获是:分库分表最重要的不是配置语法,而是分片键选择。
如果用户订单列表查询最常带的是 user_id,那订单表就应该优先考虑 user_id。
如果支付回调只拿得到 order_sn,那算法也需要兼容 order_sn。
如果用户可以通过手机号登录,就需要让手机号查询能精准路由,所以项目单独设计了 t_user_phone。
这让我意识到:分片键一定要从业务查询路径出发,而不是从数据库角度拍脑袋决定。
分库分表也有代价:
- 跨库聚合查询变复杂。
- 分页、排序、统计会更麻烦。
- 分片键缺失时可能导致全路由。
- 后续扩容和数据迁移也需要考虑。
所以分库分表不是越早越好,也不是表越多越好。只有当数据规模、写入压力、单库瓶颈真的出现时,才值得引入。
九、订单冷热分离让我理解了“逻辑分离”和“物理分离”的区别
项目中订单数据并没有真正迁移到冷热两套物理表,而是通过订单状态做逻辑冷热分离。
订单状态大致包括:
0 待支付
10 已支付
11 部分退款
12 全部退款
20 已完成
30 已关闭
我的理解是:
- 待支付、已支付、退款中属于用户近期可能操作的数据,是热数据。
- 已完成、已关闭属于历史数据,是冷数据。
查询订单时,前端传 statusType,后端根据它转换成不同的订单状态集合。
比如:
statusType = 0 -> 待支付
statusType = 1 -> 已支付/退款中
statusType = 2 -> 已完成
这种方式的优点是简单,不需要额外的归档任务,也不需要维护冷热两套表。
但缺点也明显:数据还是在同一组分片表里,随着历史订单越来越多,主表压力仍然会上升。
所以我认为这个项目里的冷热分离更准确地说是“逻辑冷热分离”。如果未来数据量继续增长,可以演进成物理冷热分离:
- 主订单表只保留近期订单。
- 历史订单迁移到 t_order_history。
- 复杂历史查询走 ES 或归档库。
这让我理解到:很多项目里的设计是分阶段演进的。不是一开始就上最复杂方案,而是先用简单方案满足当前规模,再给未来演进留空间。
十、全局异常和统一返回让我理解了框架封装的价值
项目里封装了统一返回对象、全局异常处理器、自定义异常体系。
以前我觉得这些东西比较基础,但看完整个项目后,我发现它们非常重要。
如果没有统一异常处理,业务代码里会到处出现:
try {
// business
} catch (Exception e) {
return Result.fail("xxx");
}
这样会导致:
- 代码重复。
- 返回格式不统一。
- 错误码难管理。
- 未知异常可能暴露内部细节。
项目里通过 @RestControllerAdvice 统一拦截:
- 参数校验异常
- 业务异常
- 远程调用异常
- 未知系统异常
业务层只需要抛出 ServiceException、ClientException、RemoteException,框架层统一转换成 Result。
我对这块最大的感受是:成熟项目里,业务代码应该尽量只表达业务逻辑,通用能力应该沉到框架层。
这也是项目里多个 starter 的意义:
common
convention
web
database
cache
distributedid
designpattern
idempotent
log
bizs
user
这些 starter 不是为了拆目录,而是为了把通用能力标准化。
十一、敏感数据保护让我理解了“存储安全”和“展示安全”
项目里对敏感数据做了两层保护:
第一层是数据库加密。
用户服务和订单服务的 ShardingSphere 配置里,对身份证、手机号、邮箱、地址等字段配置了 AES 加密。这样即使数据库泄露,也不至于直接暴露明文。
第二层是接口返回脱敏。
项目通过 Jackson 自定义序列化器,对手机号和身份证号做脱敏:
- 手机号类似 138****5678
- 身份证保留前 4 位和后 4 位
这让我理解到:加密和脱敏不是一回事。
加密解决的是落库安全。
脱敏解决的是展示安全。
比如数据库加密后,业务代码查出来的对象可能已经是明文。如果接口直接返回,前端仍然能拿到完整身份证号。所以还需要在响应 DTO 上做脱敏。
这块我也看到一些可以改进的地方:
- AES 密钥不应该直接写在 YAML 里,生产环境应放到 KMS、配置中心或环境变量。
- 用户服务和订单服务里有重复的脱敏序列化器,可以抽到公共 starter。
- 邮箱、地址等字段如果会返回给前端,也应该有统一脱敏规则。
学习这部分后,我对“安全”有了更具体的理解:安全不是只靠一个点,而是要覆盖存储、传输、展示、日志等多个环节。
十二、我对这个项目不足之处的理解
学完之后,我觉得这个项目作为学习项目已经覆盖了很多核心问题,但如果从生产级系统角度看,还有一些可以继续优化的地方。
1. 部分配置更适合外置
比如 ShardingSphere Encrypt 的 AES 密钥直接写在配置文件中,生产环境会有风险。更合理的方式是放到配置中心、密钥管理系统或环境变量中。
2. 部分能力可以进一步统一封装
例如手机号、身份证脱敏序列化器在用户服务和订单服务里都有实现。这个可以抽成公共 starter,避免重复。
3. 逻辑冷热分离还不是物理冷热分离
目前订单冷热分离依赖 status 字段过滤,本质上还在同一组表里。数据量继续增长后,可以考虑历史订单归档、ES 查询、冷热库拆分。
4. Canal 同步存在最终一致性延迟
Canal + MQ 更新 Redis 是异步链路,数据库更新后 Redis 会有短暂延迟。对于强一致场景,还需要业务同步更新或读数据库兜底。
5. 分库分表后复杂查询成本会上升
分库分表可以解决单库单表压力,但也会带来跨库分页、聚合统计、分片键缺失全路由等问题。后续如果要做复杂检索,可以借助 ES 或宽表。
我觉得能看到这些不足也很重要。学习项目不是只背它“用了什么技术”,还要知道它为什么这样设计,以及它还有什么边界。
十三、如果面试让我讲这个项目,我会怎么组织
学完之后,我觉得面试讲项目不能一上来就堆技术名词,而是要围绕业务主线讲。
我会这样组织:
第一步,先讲项目背景。
这是一个模拟 12306 的分布式票务系统,核心场景是高并发购票。项目拆分为用户、车票、订单、支付、聚合、网关等服务。
第二步,讲核心业务链路。
用户提交购票请求后,系统先通过 Redis + Lua 令牌桶做余票预扣,再进行座位锁定和订单创建。订单创建成功后发送 RocketMQ 延迟消息,如果用户超时未支付,消费端会关闭订单、释放座位并回滚令牌桶。
第三步,讲高并发保障。
项目通过 Redis 预扣减降低数据库压力,通过 Lua 保证扣减原子性,通过分布式锁控制订单状态并发修改,通过幂等框架解决重复请求和 MQ 重复消费。
第四步,讲数据层设计。
用户和订单服务使用 ShardingSphere 分库分表。订单表按 user_id + order_sn 复合分片,兼顾用户订单查询和订单号查询。用户服务按 username、phone、mail 等业务键分片。
第五步,讲缓存一致性。
项目使用 Canal 监听 MySQL binlog,再通过 RocketMQ 发送变更事件,消费者根据表名通过策略模式路由到不同处理器,最终更新 Redis 缓存。
第六步,讲工程化。
项目封装了多个 starter,比如统一异常处理、幂等、日志、缓存、分布式 ID、策略模式等,把通用能力从业务代码中抽离出来。
最后主动补充不足。
这个项目里订单冷热分离是逻辑分离,还没有做物理归档;Canal 同步 Redis 是最终一致性;AES 密钥直接写在配置文件中,生产环境可以进一步优化。
我觉得这样的表达会比单纯说“我用了 Redis、RocketMQ、ShardingSphere”更清楚,因为它能体现我知道每个技术点解决了什么问题。
十四、学完这个项目后,我最大的收获
如果只用一句话总结我的收获:
高并发系统设计不是堆技术,而是围绕核心业务链路,把每个技术放在合适的位置解决具体问题。
这个项目让我对很多问题有了更具体的理解。
比如:
- Redis 不是简单做缓存,也可以做预扣减、令牌桶、幂等键。
- Lua 的价值不只是脚本,而是把多步 Redis 操作变成原子操作。
- MQ 不只是异步通知,也可以做延迟关单、削峰、最终一致性补偿。
- 幂等不是可有可无,而是分布式系统必须考虑的问题。
- 分库分表的关键不是配置,而是分片键是否贴合业务查询。
- 策略模式不是为了炫设计模式,而是为了隔离变化点。
- 全局异常处理、统一返回、脱敏这些基础能力,决定了项目是否规范。
学这个项目的过程,也让我意识到:看源码不能只看“这个类干了什么”,而要把它放回业务链路里思考。
比如看到令牌桶,就要问:它在购票链路的哪个位置?为什么不能直接扣数据库?失败后怎么回滚?
看到延迟消息,就要问:消息重复怎么办?订单已支付怎么办?释放座位失败怎么办?
看到分库分表,就要问:按什么字段分?哪些查询能精准路由?哪些查询会全路由?
只有带着这些问题看源码,才能真正把项目学透。
十五、后续我还想继续补的能力
虽然这个项目已经覆盖了很多内容,但我觉得后续还可以继续补几块能力:
1. 压测和性能分析
项目里有很多高并发设计,但如果能配合 JMeter、Arthas、Prometheus 这类工具做压测和指标观察,会更完整。
我希望后续能补充:
- 购票接口 QPS 压测
- Redis 和 MySQL 压力对比
- Lua 脚本执行耗时
- RocketMQ 消费堆积观察
- 分库分表前后查询性能对比
2. 更深入的事务一致性
项目中用了 MQ、Canal、幂等、补偿等方式保证最终一致性,但如果继续深入,可以学习:
- 本地消息表
- 事务消息
- TCC
- Saga
- 最大努力通知
这样对分布式事务的理解会更完整。
3. 更完整的生产级安全设计
敏感数据保护可以继续扩展:
- 密钥托管
- 日志脱敏
- 接口权限控制
- 数据访问审计
- 防止越权查询
4. 服务治理和可观测性
项目里可以继续补充:
- Sentinel 限流熔断
- SkyWalking 链路追踪
- Prometheus + Grafana 指标监控
- ELK 日志检索
这些能力能让项目更接近真实生产环境。
十六、我给自己整理的源码复习索引
学完整个项目后,我觉得后面复习不能再从头到尾翻所有代码,而是应该按能力模块复习。下面是我给自己整理的源码索引。
1. 高并发购票主链路
services/ticket-service/src/main/java/org/opengoofy/index12306/biz/ticketservice/service/handler/ticket/tokenbucket/TicketAvailabilityTokenBucket.java
services/ticket-service/src/main/resources/lua/ticket_availability_token_bucket.lua
services/ticket-service/src/main/resources/lua/ticket_availability_rollback_token_bucket.lua
services/ticket-service/src/main/java/org/opengoofy/index12306/biz/ticketservice/mq/consumer/DelayCloseOrderConsumer.java
services/order-service/src/main/java/org/opengoofy/index12306/biz/orderservice/service/impl/OrderServiceImpl.java
复习重点:购票请求为什么先经过令牌桶,Redis Hash 如何组织区间余票,Lua 如何保证判断和扣减原子性,订单关闭后座位、缓存、令牌桶如何回滚。
2. 幂等框架
frameworks/idempotent/src/main/java/org/opengoofy/index12306/framework/starter/idempotent/annotation/Idempotent.java
frameworks/idempotent/src/main/java/org/opengoofy/index12306/framework/starter/idempotent/aop/IdempotentAspect.java
frameworks/idempotent/src/main/java/org/opengoofy/index12306/framework/starter/idempotent/core/IdempotentExecuteHandler.java
frameworks/idempotent/src/main/resources/lua/set_if_absent_and_get.lua
复习重点:@Idempotent 注解标记了哪些信息,AOP 在什么时候拦截业务方法,PARAM、TOKEN、SPEL 三种模式分别解决什么问题,Redis 幂等 key 如何生成。
3. 策略模式与 Canal 路由
frameworks/designpattern/src/main/java/org/opengoofy/index12306/framework/starter/designpattern/strategy/AbstractExecuteStrategy.java
frameworks/designpattern/src/main/java/org/opengoofy/index12306/framework/starter/designpattern/strategy/AbstractStrategyChoose.java
services/ticket-service/src/main/java/org/opengoofy/index12306/biz/ticketservice/mq/consumer/CanalCommonSyncBinlogConsumer.java
services/ticket-service/src/main/java/org/opengoofy/index12306/biz/ticketservice/canal/TicketAvailabilityCacheUpdateHandler.java
services/ticket-service/src/main/java/org/opengoofy/index12306/biz/ticketservice/canal/OrderCloseCacheAndTokenUpdateHandler.java
复习重点:策略处理器如何通过 mark() 暴露自己的标识,Spring 容器启动后如何收集所有策略 Bean,Canal 消费者为什么不用大量 if-else。
4. 分库分表与工程化基础能力
services/order-service/src/main/resources/shardingsphere-config.yaml
services/user-service/src/main/resources/shardingsphere-config.yaml
services/order-service/src/main/java/org/opengoofy/index12306/biz/orderservice/dao/algorithm/OrderCommonDataBaseComplexAlgorithm.java
services/order-service/src/main/java/org/opengoofy/index12306/biz/orderservice/dao/algorithm/OrderCommonTableComplexAlgorithm.java
frameworks/web/src/main/java/org/opengoofy/index12306/framework/starter/web/GlobalExceptionHandler.java
frameworks/web/src/main/java/org/opengoofy/index12306/framework/starter/web/Results.java
services/user-service/src/main/java/org/opengoofy/index12306/biz/userservice/serialize/PhoneDesensitizationSerializer.java
services/user-service/src/main/java/org/opengoofy/index12306/biz/userservice/serialize/IdCardDesensitizationSerializer.java
复习重点:订单表为什么用 user_id + order_sn 复合分片,用户服务为什么按多个业务键分片,全局异常如何统一错误出口,存储加密和接口脱敏分别解决什么问题。
十七、我给自己整理的复习路线
如果后面重新复习这个项目,我不会再按模块从头看,而是按这条路线复习:
第一轮:先看业务链路
购票请求 -> 令牌桶 -> 座位锁定 -> 创建订单 -> 延迟关单 -> 回滚库存
第二轮:再看高并发保障
Redis Lua -> 幂等 -> 分布式锁 -> MQ 延迟补偿
第三轮:看数据层设计
分库分表 -> 冷热分离 -> 雪花 ID -> 敏感字段加密
第四轮:看工程化封装
全局异常 -> 统一返回 -> 策略模式 -> starter 抽象
第五轮:看一致性和补偿
Canal -> RocketMQ -> Redis 同步 -> 重复消费 -> 最终一致性
这条路线的好处是,它不是按文件夹学习,而是按系统问题学习。这样更容易把源码和业务场景对应起来。
十八、我最后形成的一张系统设计脑图
最后,我把这个项目按系统能力整理成一张文字版脑图:
12306 分布式票务系统
|
|– 业务主线
| |– 用户注册登录
| |– 车次和余票查询
| |– 购票下单
| |– 支付回调
| |– 订单关闭
| `– 订单查询
|
|– 高并发能力
| |– Redis 余票缓存
| |– Redis Hash 令牌桶
| |– Lua 原子扣减
| |– Redisson 分布式锁
| `– MQ 削峰和延迟补偿
|
|– 一致性能力
| |– MQ 延迟关单
| |– 幂等框架防重复消费
| |– Canal 监听 binlog
| |– Redis 缓存最终一致性
| `– 订单状态校验兜底
|
|– 数据层能力
| |– ShardingSphere 分库分表
| |– 用户表多业务键分片
| |– 订单表复合分片
| |– 雪花算法生成分布式 ID
| `– 逻辑冷热分离
|
|– 工程化能力
| |– starter 封装
| |– 全局异常处理
| |– 统一返回结果
| |– 策略模式路由
| |– 责任链模式扩展
| `– 日志与操作记录
|
`– 安全能力
|– ShardingSphere Encrypt 存储加密
|– Jackson 序列化脱敏
|– 手机号脱敏
|– 身份证号脱敏
`– 统一异常避免内部信息泄露
这张图也是我后面复习这个项目时最重要的总览。只要这张图能讲清楚,说明我对项目的整体理解就不会散。
结尾
这次学习 12306 项目,对我最大的帮助不是记住某个框架的 API,而是让我开始从系统角度思考问题。
以前看到一个技术点,我可能只会问“它怎么用”。
现在我更会问:
- 它解决了哪个业务问题?
- 它在链路的哪个位置?
- 它失败后有没有兜底?
- 它和其他组件怎么配合?
- 它有什么边界和不足?
我觉得这才是学习项目最重要的收获。
项目源码只是起点,真正有价值的是通过源码把业务、架构、工程化和面试表达串起来。
网硕互联帮助中心





评论前必须登录!
注册