云计算百科
云计算领域专业知识百科平台

12306项目学习day6

学完 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,而是让我开始从系统角度思考问题。

以前看到一个技术点,我可能只会问“它怎么用”。

现在我更会问:

  • 它解决了哪个业务问题?
  • 它在链路的哪个位置?
  • 它失败后有没有兜底?
  • 它和其他组件怎么配合?
  • 它有什么边界和不足?

我觉得这才是学习项目最重要的收获。

项目源码只是起点,真正有价值的是通过源码把业务、架构、工程化和面试表达串起来。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 12306项目学习day6
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!