12306 出票的一种算法设计

引子

12306 一度成为技术圈的评论热点,期间被鄙视过, 也被理解过。在我 2.5 年的技术生涯中, 至少 读到过 8 篇关于 12306 的文章,当然,最早的文章是骂 12306 的。今年的春节也不出意外读到了关于 12306 的文章,当然是对 12306 的理解与包容,甚至是赞美。前面两个春节读到 12306 的文章并没有 引发我的思考,而这个春节,可能由于我将近 1 个月未上班的原因,我倒是认真思考起 12306 的业务 模型,进而在头脑中有了这个初步设计。

本文的目的不是对 12306 作评价,而是基于本人对 12306 业务的理解,自己重新实现一套 12306 的系统设计。 本人对自己的这个设计方案比较满意,认为该方案可以很好的解决 12306 的性能瓶颈。


免喷声明

  1. 由于本人不是 12306 的工作人员,对铁道部的运行方案不是很了解,难免有些分析不当的地方,请专业人士批判指出,请圈外人士勿喷。

  2. 本人对自己的文字表达能力相当有怀疑, 如有疑问, 欢迎交流。

设计步骤
  1. 业务的抽象与提取

  2. 数据结构,算法的设计

  3. 数据库设计


设计容易犯的错误

我们容易忽略 1 步骤,交换 2,3 步骤, 也是设计思路是变成了:

  1. 数据库设计

  2. 算法设计(数据结构被数据库决定了)

业务抽象和提取

以后详细描述, 现在简单提取

Nothing but Motto: 交流的时候统一语言,计算的时候统一量纲。

业务流程

  1. 用户输入起点站,终点站,选择日期

  2. 根据输入找到相应的班次

  3. 查询每个班次的剩余票数

  4. 用户点击购买,填写乘客人信息

  5. 根据起点站,终点站和日期分配票

  6. 准备出票:

  • 用户支付成功,出票

  • 用户超时,票回收(回滚操作)


提取

  1. 系统的核心功能是查票,购票。票作为我们的商品, 我们需要根据具体情况来定义票的概念,合理化票的量纲。

  2. 火车票的最小单位是:1(座位)* 1(个站区间), 我们约定这个最小单位是 ticket 。后续提到  都是指用户购买的票(n 个 ticket 构造而成)。

  3. 对于有座位的票,要统一票 id(反正我坐火车从来没有中途被要求换个位置)。对于这一点, 解决还是比较方便的, 对于同一座位, 放入一个票池子, 同时限制一张车票,只能从同一个池子里面去。

  4. 对于无座位的票, 统一放入一个票池子,或者分车厢放入几个票池子。

  5. 站票的最小单位是:1(个站区间),由于站票的无固定位置特性,站票的最小单位和座位票有区别,因此站票的数据应该跟着列车的站点走。


抽象

根据上述定义,我们可以构造一张 tickets 表,座位号为行,站区间为列。假设列车经过 ABCDEFG 7 个站点,上面有 5 个座位号,那么 tickets 表为(先忽略站票):

0?wx_fmt=png


实例

火车经过 ABCDEFG 7 个站点, 小王买了一张 B 到 D 的票。

  1. 小王买了几张 ticket ?

    答:2 张,分别为 BC 和 CD 区间的 ticket 。

  2. 假设小王买的票是 2 号座位, 请标记相关 ticket 。

    答: 如下图:

    0?wx_fmt=jpeg

  3. 下图出票方式是否合理?0?wx_fmt=png

    答: 不合理,其中红色的票使得 tickets 池碎片化。 后续只能出 2 张全程票(A➔G)。 如果红色的票分配到 1 号座位,后续还可以出 3 张全程票,可使收益最大化,12306 也是要赚钱的了。

    那这样是不是会使得位置特别不均匀呢?确实是的,坐过火车的人会注意到这点的。不过这 并不是什么问题,大家总是会自动向座位空空的地方移。但实际上,这根本不是问题, 这图上的位置不过是 逻辑位置 而已,把它跟实际 物理位置 做个映射就行。 学过计算机的人都知道内存有 逻辑地址 和 物理地址 的概念,他们有个映射关系。

    综上所述:出票分配位置时应该从上向下遍历,找到第一个可用的。

数据结构和算法的设计

这里不讨论我对有关设计的演进了,直接指出最终设计。


tickets 数据结构

我们观察上述图片的时候,有没有想过 白色区域代表 1, 着色区域代表 0, 就可以得到一个内存图。每个座位对应的行用一个 int64 就可以代替了(我们假设列车最多经过 65 个站点,这个假设应该是合理的,假设不正确也没关系, 不影响我们的算法)。


查询

假设小王要查询 B➔D 的票,占据第 2 和第 3 个区间。 我们只需要构建位置值 (后续称ticket_value) 6, 分别和每一个位置的值做 位与 运算,如果结果为 6, 说明位置可用, 遍历一遍, 就可以得到剩余位置数。


购买

假设小王要购买 B➔D 的票,从可分配的位置里面选取第一个减去 

程序系统设计


数据库系统设计

  1. trains

    id

    code

    type




    7

    G77

    高铁

    8

    D63

    动车

  2. sites

    id

    name



    1

    上海

    2

    济南

    3

    北京

  3. site_train_xRef

    site_id

    train_id

    site_nth

    n_sro





    1

    7

    0

    3

    2

    7

    7

    3

    3

    7

    9

    3

  • site_nth 为 0 代表始发站。

  • n_sro 为站票剩余数目,跟着列车的站点走。

tickets

id

pos_id

n_remain




1

1A

int_max

1

3F

int_max

  • 建议为每一辆列车建立一个 tickets 表, 这样也很容易分配到不同的机器上去查询。也可以在 tickets 中加入一个 train_id 字段。


查票步骤

  1. 根据 site_train_xRef 找到符合要求的的列车班次。

  2. 分别各列车计算出票值 ticket_value 。

  3. 分别在找出的班次中检索余票, 判断该座位可用: (n_remain & ticket_value) = ticket_value 。


锁定票

用户购买,生成订单后,锁定票。

  • 解释步骤不如直接上代码:+

  update tickets set n_remain = n_remain - ticket_value  where id in
  (select id from tickets     where n_remain & ticket_value = ticket_value
     order by id
     limit 1);

和公司 CTO 讨论一下后发现上述 sql 不是个好方法:

  1. 上述语句的原子性不清楚,如果不是原子性, 那就是 bug 了。

  2. 如果上述 sql 是原子性的, 上述原子操作锁的粒度比较大, 性能有问题。

我们得出方案, 改成两步操作, 第二步如果失败, 就重试:

  select id as oid from tickets       where n_remain & ticket_value = ticket_value
       order by id
       limit 1;

  update tickets set n_remain = n_remain - ticket_value    where id = oid and n_remain & ticket_value = ticket_value;

其他业务场景暂时交给读者们了

结语
  • 本人仅仅给出了出票算法一种设计而已, 离实用性还差很远,铁道部的具体卖票措施也不知道。比如说会有站点限制什么的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值