原创:谈谈12306铁路客运售票系统的架构问题(四、最终篇)
原创:谈谈12306铁路客运售票系统的架构问题(四、最终篇)
作者:刘常军(2014-01-20) 经过前面三篇文章的铺垫和说明,如今终于可以具体分析要如何对12306.cn的架构进行优化了。
在这里我要强调,我不是该系统的架构师,没看过12306.cn的设计说明书,跟该系统的开发单位也无业务往来,对12306.cn铁路客运售票系统的架构问题,仅是本人出于对IT技术进步的追求而自发的进行思考和分析,进而形成的一些技术上的分析意见,如有雷同实属巧合。
首先我们来简要分析一下12306.cn网站架构为什么是现在这个样子,而不是淘宝、京东那样的架构,也不是道路客运售票系统那样“天然的”分布式系统架构?这不是巧合,而是一系列的因素形成的合力所导致。(1)受铁路系统行政管理体制的影响,12306.cn必须是一种集中式系统架构。(2)系统的承建单位是铁道部信息技术中心、中国铁道科学研究院,不是淘宝、京东这类的互联网企业。(3)12306.cn要满足其核心业务需求。即12306.cn出售的商品(即火车票)是动态的,要随时根据乘客已购车票的起点站和终点站进行拆分与合并操作,这也是12306.cn与淘宝、京东这类互联网购物平台的最大区别。
现在我们来谈谈系统的优化问题,其实可以将12306.cn的优化分两大块来谈。
(一)关于旅客、订单及常用 联系人。
这部分数据架构比较简单,以下是未拆分的数据ER图:
从图上我们可以看出,主表数据是“旅客”,而“订单”和“常用联系人”是“旅客”的字表数据,一对多的关系,即一名旅客可以有多份订单和多个常用联系人。从主表数据来看,由于任意两个旅客之间不存在关联关系,对一个旅客数据进行任何操作,不影响另一个旅客数据,因此这部分数据结构完全可以采取“拆分+缓存”的策略进行优化。
“拆分”技术主要是进行水平拆分。水平拆分的规则不复杂,方法也很多,例如可以很简单的对“旅客ID”进行哈希运算,进而均匀分布到若干个水平分区表中,然后根据每台x86服务器的硬件资源配置部署一定数量的水平分区表,最终达到通过若干台x86服务器实现这部分数据拆分的目标。
“缓存”技术主要是应对高并发的。数据水平拆分后,减少了每台服务器上的数据量,使得数据不那么集中。但是面对春运期间汹涌而来的高并发,有必要进一步采取缓存机制加以应对。从数据架构上我们看到,这部分数据的主要操作其实是读(SELECT)操作,而插入(INSERT)和更新(UPDATE)操作相对来说比较少,所以可以采取缓存技术,将数据库中的信息进行缓存,当客户端传来读操作请求时,将其重定向到缓存中去执行读操作。当数据库进行了插入(INSERT)和更新(UPDATE)操作后,由于后台去主动刷新缓存,以此提高系统的读操作性能。
(二)关于车次、站点、席位及车票。
这部分数据架构不甚复杂,但数据间的操作相当复杂,可以说整个12306.cn的核心运算都在这里,也是12306.cn与其他电子商务平台的最大区别。正是因为这里运算复杂,所以才会导致其数据拆分很难实现,进而在高并发时几近瘫痪。我们先看看它的ER图:
从这里我们可以看到,车次、站点、席位类别三个表的数据是可以预先生成的。其中,“车次”是用来存储“G72广州西-北京南”、“D5北京-沈阳北”这样的完整线路信息的。“席位类别”用于存储该车次上各种类别的席位,例如“一等座 5车厢08号”、“二等座10车厢15号”等此类信息。“站点”用于存储该车次从始发站开始,至终点站结束,期间所经停的所有途经站点,包括站序、站点、到达时间、出发时间等一系列信息。这三个信息相对来说是静态的,只要车次确定,那么这三个信息就是确定的,因此可以在开车前全部预先生成好。
但图里用黄颜色标记的“车票”表,里面的数据则是动态生成的,核心算法全在这里。为什么这里要动态生成?这个问题在前面的《谈谈12306铁路客运售票系统的架构问题(二)》文章里已经做过说明,这里再复述一遍:
假设有A旅客买了一张“广州-长沙”的票(一端为起点),那么票务库应当立刻取一张“广州-北京”的票将其一拆为二,一张是“广州-长沙”,卖给A旅客,另一张是“长沙-北京”,保存到车票库里。假设有B旅客买了一张“武汉-郑州”的票(中间段),那么票务库应当立刻取一张“广州-北京”的票将其一拆为三,一张是“武汉-郑州”,卖给B旅客,其余两张“广州-武汉”及“郑州-北京”的票则保存到车票库里。实际在拆分时还会涉及到很多优先级算法,例如旅客要买短途票,那么系统是优先拆长途车票、还是优先拆短途车票?旅客要买长途票,但车票库里已经没有符合条件的长途票,只有一些很零碎的车票了,那么能不能把几张很零碎的车票拼起来卖给旅客?……诸如此类,要考虑很多细致的算法,这样才能保证将尽可能多的车票卖出去,降低列车座位的空座率,减少浪费。
从上面这段需求描述中,我们可以看到,在12306.cn里,“车票”数据是核心数据,跟淘宝、京东这样的电子商务平台上的“在售商品”数据所处地位类似。但不同的是,“车票”表内的数据,其中某一条数据的产生严重依赖表中另外一条数据,新数据的插入(INSERT)通常是伴随着老数据的更新(UPDATE),并且还涉及到要对多条老数据进行优先级判断,正是因为这种复杂的算法导致了车票表的数据很难被拆分。之所以出现了这么复杂的算法,应该是12306.cn系统必须要满足几个基本需求——即先卖有座的票,有座票卖完了再卖无座票;购买短途票需要把长途票拆开,剩下的旅程的座位票要接着卖,尽量让座位在12306上卖出去,不能让中途上车补票的人有座位。基于这样的需求,我估计,12306的系统架构师在设计的时候,自然而然的将“票源”和“席位”混合在一起进行存储和计算了。于是导致原本是两个独立的简单对象,但如果把它们当作一个对象来分析,那计算逻辑就复杂多了,牵一发动全身!所以12306.cn前段时间的性能调优工作才没有去改动架构和算法,原来怎么算的、现在还是怎么算,只不过现在是把这种计算全部搬到内存里去,借助内存的高速计算能力达到性能提升的效果。
综上所述,关于12306铁路售票系统的架构问题,首先我建议首先在系统架构层面将“票源”和“席位”两个对象独立开、分别进行考虑。改进后的系统架构ER图如下:
在改进后的架构里,票源就是单纯的票源,不跟席位挂钩,票源就是标记本次旅程的起点站和终到站。这样,既然某条线路的始发站、所有途经站和终点站全部已知,那么把这些站点两两组合,通过穷举法就可以预先生成全部的票源信息。席位就是单纯的席位,通过车次、车厢及席位号三个指标就可以唯一确定一个席位,一趟列车有多少个席位是确定的,因此席位信息也可以全部预先生成。这里注意席位信息中的“席位占用”字段,要根据本车次全程站点数生成一个对应位数的字符串(或者二进制数值也可以),例如某次列车从始发站到终点站有20站,那么就先生成一个20位的字符串(或者二进制数字)。这个字符串从左到右每一位字符分别用1和0标识该席位是否已被占用,最初生成的时候当然全都是0。
其次,乘客在购票时,后台系统改为这样的计算方法:(1)根据旅客选择的车次、起点站、终到站,查询出票源。由于票源不跟席位关联,并且已经全部预先静态化,因此这一步肯定会查到唯一一条票源信息。(2)系统根据旅客选定的起点站和终到站,生成一个席位申请标识,例如某次列出从始发站到终点站有20站,旅客本次购买的车票是从第3站上车到第8站下车,那么这个标识就是一个20位的字符串(或者二进制数),其中第1-2位是0,第3-8位是1,第9-20位是0。(3)用前一步生成好的席位申请标识到“席位”表中去跟“席位占用”字段进行匹配,匹配出来之后,立即更新该席位记录的“席位占用”字段,同时取得席位ID。(4)用前面获取到的票源信息和席位信息,再加上旅客信息,最终生成“订单”信息。
上面这个算法的关键之处有两点,一是通过增加票源对象,把票源和席位独立开来,通过“空间换时间”,提前把一个车次可能会用到的数据全部静态化。二是把原来的车票数据的动态增删改操作转换为一个字段的简单更新,降低了操作维度,减少了数据更新的复杂性。通过这种方式进行改进后,12306.cn的核心数据不再是互相关联的数据,不会出现售票时的动态生成操作。至此,经过此番系统架构的调整,核心数据也可以采取“拆分+缓存”技术来应对大数据量和高并发了。
后记(一):在写作本文接近尾声之际,我看到了这篇文章《对话12306技术负责人:春运能否不瘫痪》(http://tech.163.com/14/0120/09/9J18KPPH000915BF.html),里面的12306技术负责人朱建生说了如下一番话:
淘宝是很优秀的网站,也是我们学习的榜样。不过,网售火车票与淘宝这种综合购物确实有很大不同。举个简单的例子,在淘宝出售一件商品,同样的库存只需减数量就可以了。但是车票不一样,它具有不可替代性。
前一段网上有个帖子,例子举得挺对。北京西到深圳北的高铁,它有17个站,3种座位。表面看起来,就是3个产品,即商务座、一等座、二等座。但实际上,它有408种商品,因为旅客的上车站和下车站都可以是不同的选择,每一张票都是一个相对独立的商品,而且这种产品是动态的。如果售出了一张北京到武汉的二等座,客票核心系统会立即自动生成一张武汉到深圳北之间的同席位二等座,同时取消北京至石家庄直至武汉之间车站的车票,此外,还需将这些车票数量的变化实时同步到网站上。因此售票系统是在不停地计算生成新的“商品”。
这段话完全证实了我对于12306.cn架构的猜想。因此,希望本文能对12306.cn的改进有所帮助,希望明年我们买票能是买车票,而不是买彩票。
后记(二):有朋友问我,为什么能想到这个算法?原因很简单,因为我想起了我小时候在家乡坐“小火车”的情形。我小时候在东北林区长大,那里有一种所谓的森林铁路,其轨道比通常的火车道要窄一些,火车头和车厢也都小一号。这种铁路是专门用于从山上运送木材到镇上的,客运只是其一小块业务。每次坐这种“小火车”时,买票完全无压力,开车前随时都可以买到。但是买到票只是允许你上车,上车以后要想有座,必须有“座号”!”座号“就是大概一平方厘米那么大的一张薄薄的小纸片,上面印着车次、车厢和座位号。有它就有座,没它就站着。“票号”不收费,但它是很紧缺的资源,除非你早点去买票或者认识车站的人,否则就站一路吧。后来到90年代前后,国家保护森林资源的力度逐渐加大,树木都不让砍了,“小火车”也无声无息的消失在历史的长河中。如今镇上和各个村子之间的运输任务,全部由客车、货车来完成。
后记(三):坚守了几个晚上,作者我终于抢到一张半夜零点左右到家的动车票,时间早晚问题咱就不挑了,这年头能有张票就不错了,还要啥自行车!