12306 一度成为技术圈的评论热点,期间被鄙视过, 也被理解过。在我 2.5 年的技术生涯中, 至少 读到过 8 篇关于 12306 的文章,当然,最早的文章是骂 12306 的。今年的春节也不出意外读到了关于 12306 的文章,当然是对 12306 的理解与包容,甚至是赞美。前面两个春节读到 12306 的文章并没有 引发我的思考,而这个春节,可能由于我将近 1 个月未上班的原因,我倒是认真思考起 12306 的业务 模型,进而在头脑中有了这个初步设计。
本文的目的不是对 12306 作评价,而是基于本人对 12306 业务的理解,自己重新实现一套 12306 的系统设计。 本人对自己的这个设计方案比较满意,认为该方案可以很好的解决 12306 的性能瓶颈。
免喷声明
由于本人不是 12306 的工作人员,对铁道部的运行方案不是很了解,难免有些分析不当的地方,请专业人士批判指出,请圈外人士勿喷。
本人对自己的文字表达能力相当有怀疑, 如有疑问, 欢迎交流。
业务的抽象与提取
数据结构,算法的设计
数据库设计
设计容易犯的错误
我们容易忽略 1 步骤,交换 2,3 步骤, 也是设计思路是变成了:
数据库设计
算法设计(数据结构被数据库决定了)
以后详细描述, 现在简单提取
Nothing but Motto: 交流的时候统一语言,计算的时候统一量纲。
业务流程
用户输入起点站,终点站,选择日期
根据输入找到相应的班次
查询每个班次的剩余票数
用户点击购买,填写乘客人信息
根据起点站,终点站和日期分配票
准备出票:
提取
系统的核心功能是查票,购票。票作为我们的商品, 我们需要根据具体情况来定义票的概念,合理化票的量纲。
火车票的最小单位是:1(座位)* 1(个站区间), 我们约定这个最小单位是ticket。后续提到票都是指用户购买的票(n 个ticket构造而成)。
对于有座位的票,要统一票 id(反正我坐火车从来没有中途被要求换个位置)。对于这一点, 解决还是比较方便的, 对于同一座位, 放入一个票池子, 同时限制一张车票,只能从同一个池子里面去。
对于无座位的票, 统一放入一个票池子,或者分车厢放入几个票池子。
站票的最小单位是:1(个站区间),由于站票的无固定位置特性,站票的最小单位和座位票有区别,因此站票的数据应该跟着列车的站点走。
抽象
根据上述定义,我们可以构造一张tickets表,座位号为行,站区间为列。假设列车经过 ABCDEFG 7 个站点,上面有 5 个座位号,那么tickets表为(先忽略站票):
实例
火车经过 ABCDEFG 7 个站点, 小王买了一张 B 到 D 的票。
-
小王买了几张ticket?
答:2 张,分别为BC和CD区间的ticket。
-
假设小王买的票是 2 号座位, 请标记相关ticket。
答: 如下图:
-
下图出票方式是否合理?
答: 不合理,其中红色的票使得tickets池碎片化。 后续只能出 2 张全程票(AG)。 如果红色的票分配到 1 号座位,后续还可以出 3 张全程票,可使收益最大化,12306 也是要赚钱的了。
那这样是不是会使得位置特别不均匀呢?确实是的,坐过火车的人会注意到这点的。不过这 并不是什么问题,大家总是会自动向座位空空的地方移。但实际上,这根本不是问题, 这图上的位置不过是逻辑位置而已,把它跟实际物理位置做个映射就行。 学过计算机的人都知道内存有逻辑地址和物理地址的概念,他们有个映射关系。
综上所述:出票分配位置时应该从上向下遍历,找到第一个可用的。
这里不讨论我对有关设计的演进了,直接指出最终设计。
tickets 数据结构
我们观察上述图片的时候,有没有想过 白色区域代表 1, 着色区域代表 0, 就可以得到一个内存图。每个座位对应的行用一个int64就可以代替了(我们假设列车最多经过 65 个站点,这个假设应该是合理的,假设不正确也没关系, 不影响我们的算法)。
查询
假设小王要查询 BD 的票,占据第 2 和第 3 个区间。 我们只需要构建位置值 (后续称ticket_value) 6, 分别和每一个位置的值做位与运算,如果结果为 6, 说明位置可用, 遍历一遍, 就可以得到剩余位置数。
购买
假设小王要购买 BD 的票,从可分配的位置里面选取第一个减去
数据库系统设计
-
trains
id
code
type
-
sites
id
name
-
site_train_xRef
site_id
train_id
site_nth
n_sro
site_nth 为 0 代表始发站。
n_sro 为站票剩余数目,跟着列车的站点走。
tickets
id
pos_id
n_remain
|
|
|
1 |
1A |
int_max |
1 |
3F |
int_max |
查票步骤
根据site_train_xRef找到符合要求的的列车班次。
分别各列车计算出票值ticket_value。
分别在找出的班次中检索余票, 判断该座位可用:(n_remain