12306究竟难在哪里?

12306 自发布之日,就伴随者巨大的关注度,再加上12306刚出来的时候体验糟糕,骂声不断。但骂声中又逐渐分化成了不同的声音,一方面就是讽刺,觉得举国之力做出来的东西居然如此粗糙。另一方面一些略懂技术的人又出来给它正名,把它说成是"世界第一,而且极其牛逼",秒杀淘宝。那么,12306究竟难在哪里呢?是不是真的拳打腾讯脚踩淘宝呢?我们对网上的问题逐一分析。

所销售商品极其复杂(SKU极多)

在电商平台上,每个商品对应一个SKU,有人购买1件,库存就减1。而12306的特殊性在于,火车票是一种动态的SKU,计算起来的数据量可能是普通电商产品的数百倍。以北京西到深圳福田的G71次高铁为例,共有17个站、3种座位。表面看起来是3个SKU,即G71商务座、一等座、二等座,但实际上,G71次高铁有408个SKU。原因很简单:从北京西站始发的车票,后面有16个车站,即16种不同的车票;涿州东站是第二站,有15种不同的车票,以此类推,单以上下车的站来计算,G71次高铁就会有16+15……+2+1=136个SKU,而每种票对应3种座位,一共是408个商品。也就是说,如果旅客购买了一张北京西站到涿州东站的车票,G71次高铁的SKU要减去16;而如果购买的是北京西站到深圳福田站的车票,则要减去136个SKU。以上只是SKU的减值。若旅客购买的是短途票,如北京西站到涿州东站,则在SKU减去16的同时,还要增加涿州东站到之后各站、之后各站相互间的SKU,即增加120个SKU。若再叠加当前的选座功能(A、B、C、D、F),计算数量可能还要再翻倍。而这些计算数据,需要在大量购票者抢票的数秒,甚至数毫秒内完成。

这个说法在技术圈流传甚广,按这说法,中国有1805个客运火车站,且每条线路上一天有几十到数百班次的火车。如此排列组合算下来,SKU数是天文数字。为此,还有人专门设计出了利用位运算来表示站点售卖情况,即把每个站点区间当做一个bit位,用0和1来表示可售和不可售,那么可售的区间就是一个连续的1。可以根据目标区间所有的1组成的数字,跟线路进行位运算,判断是否可售。

咋一看,这种方案确实合情合理。如果在面试中,还能卖弄一番,唬一唬面试官。然而真的如此吗?

(1)12306只是售票系统,并不负责线路调度。而且这种动态规划法最大的漏洞在于短板效应。一个区间卖多了,整条线受影响。比如北京到上海的高铁线要经过郑州。郑州有富士康的工厂,人数众多且多为周边省市的旅客,他们也有春节回家需求。如果郑州到周末短途的票卖多了,那岂不是没人能从北京直达上海?况且中国的所有政策都强调“计划”和“有序”,怎么可能放任这种无序订票。实际上,火车票就那么几种,就以杭州–齐齐哈尔为例,第一种叫预留,每个席位预留给哪个站是分配好的,一般从始发站开始算,比如在杭州–南京区间的站点分配;第二种叫限售,比如限售天津以远,想买杭州–济南车票是不出票的;第三种叫共享,比如在嘉兴–南京站点共享,先到先得,共享一般在售票的后期,因为销售不佳,打破各种限制,在高峰期,拿出来共享的车票非常少,越是淡季共享越多,而且一般在开车之前,所谓无限种售票可能,其实会出现在淡季;第四种叫复用,这个也就是常说的计算量大,售出一张票,比如南京–天津,生成两张服用票额,杭州–南京,天津–齐齐哈尔。但是,复用票不是即时生成的,所以也不需要马上计算,完全可以利用23点以后休息时间计算。

(2)不符合“料敌于先,压力前置”的原则。12306和淘宝“双11”一样,都是时间明确,压力可估的项目。这种项目完全可以提前做好准备,尽可能的需求前置,减少运行时的负担和压力。12306历年的购票数据都是可知的,可以实现按比例划分好线路,跟淘宝单个商品一样。比如对于a-b-c-d-e, 统计历年数据发现,a-e的票占50%, b-d占20%, c-e占10%。 那么就可以事先拿出部分票来,按a-e,b-d这样单独卖,只把剩余的部分做为单站商品单卖。这样可以进一步减少单票总数量级。

商品间、渠道间相互干涉

12306相比淘宝,“商品”间还会互相干涉。比如如果有人 买了济南到南京的高铁票,那么其他涉及同路段的高铁票也需要校验是否还能售卖。比如北京到天津的就不受影响(不重叠),但徐州到上海的可能就售空了。

前面说过,动态分配方案推翻后,这个问题也不存在了。每张票是提前分配好的,是独立的,不会影响其他票,比如北京-郑州-上海,你买了北京-郑州的票,不影响北京-上海的票。

访问量极大

根据2018年官方发布的数据https://www.ndrc.gov.cn/xwdt/ztzl/cyzl/2018cy/201803/t20180329_1188549.html

3月12日,为期40天的铁路春运圆满结束,全国铁路累计发送旅客38153.9万人次,同比增加2435.1万人次,增长6.8%。春运40天,全国铁路有11天旅客发送量超1000万人次,其中超1200万人次4天。特别是从2月20日(正月初五)开始,春节返程客流高度叠加,全国铁路旅客发送量连续8天在超1000万人次以上持续高位运行。节前15天,全国铁路日均发送旅客879.9万人次,节后25天日均发送旅客998.2万人次。3月4日,全国铁路发送旅客1283.8万人次,较去年春运最高峰日多发送187.2万人次,创铁路春运单日旅客发送量新高。

这个数据说少肯定是不少的,但是跟头部互联网产品的量级比起来,也并有多多少。春运的时候会有几亿人要刷票,有很高的QPS。然而不管有多少人刷票,最后的交易量都是有限且基本固定的,真正交易、出票、分配座位等根本没有多少压力。所以难点在于高并发的余票查询而不是交易。

余票查询是个典型的读操作,可以完全命中cache。现在就算有1亿人同时查询余票,对于系统来说,都是直接把cache里面的结果输出,完全没有任何计算。对余票的一致性也没有很强的要求,很容易做到每秒1亿的QPS,而实际12306的峰值QPS只有几十万。卖票退票时更新余票cache,也是非常常见的方案,就是几毫秒的事情,没有压力。

铁路车次是极少变化的,可以离线建好索引。比如深圳到北京,一共有几个车次,直达、一次换乘、二次换乘,这是可以提前算好的。把它们当做离线资源通过CDN分发到前端或客户端,由客户端来根据起始点和终点查出车次。前端查询余票的时候,只需要拿着车次列表,起点,终点即可。后端再用车次-起点站编号-终点编号为Term,以车票ID为DocumentID进行倒排索引。每张票是唯一的,它有座位号,座位类型,是否靠窗,上下铺等属性。

后端需要先选定一个车次,然后再进去订票。所以到订票的时候,其实只需要查询这个车次的剩余库存,我国的火车线路分为铁路局-线路-站点,由于各车次之间互不干扰,可以分布式存储,也不存在太大的性能压力。

到了支付环节,流量就大大减少了,考虑一个订单多张票,退票,订而不付,实际上到支付环节的数量跟实际的票数是一致的,大约一天千万级别,算不上是特别高的量了。

至于所谓的刷票软件,更无足轻重。我国的火车票预定和查询是实名制的,这大大限制了爬虫随意生成账号的数量。如果真要限制每个账号的查询次数,轻而易举。

总结

总结一下,12306是个流量较大,功能单一的秒杀系统,并没有比淘宝双11难度更高。整个漏斗模型中,流量最大的就是余票(库存)查询,因为有许多人会不停地刷新抢票。早些年12306慢,就是慢在这里,登录进不去,查询出不来。在阿里帮助下,把余票查询解决之后,12306已经不存在什么大的系统问题了。12306早期的最大问题,是资源投入不够,钱和人力都不够,架构师水平不够。