论坛首页 入门技术论坛

对于12306,我的完整技术方案

浏览 13317 次
该帖已经被评为新手帖
作者 正文
   发表时间:2012-01-13   最后修改:2012-01-14
12306主要就是卖票比较复杂,注册登录之类的功能就不说了。

有网友说,12306卖票系统比航空复杂,因为要分段卖,航空只有起点和终点,火车中间还有好多站。不过好消息是,这些站在售票时是连续的,不会出现1张票跳着站买的情况,这样就可以把一张票拆成N张只有起点和终点的票,和航空售票一样了。(我不了解航空售票,也不了解火车售票业务具体模型,下面都是基于推测和假设之类的。)

卖票分为两部分,查询和购买。12306目前提供了单独的查询,我觉得这个其实挺好的,至少有读写分离的思想;不过延迟10分钟的数据,肯定没什么人愿意用,大家还是要挤进购买系统查询。单独的查询相当于摆设了,没有发挥作用。要让查询系统有效,尤其是春运期间,延迟应该在30秒之内。

查询剩余票数设计:

查询的特点是按照车次信息或者时间查,车次和时间一般都不会变,因此在设计时,可以把页面分成两部分。一是匹配的车次列表信息,如北京到上海有哪些车,这个结果可以用CDN缓存。车次基本是固定的,缓存设置为10分钟应该就能满足需要。不占用负载。在浏览器拿到这个页面后,通过异步请求,根据每趟车的编号二次查询剩余票数等实时数据,合并显示。

车次查询时,把车次信息分库存储,并作冗余存储。好比这个库提供所有北京->xxx的查询,这个库提供上海到xxx的查询,这个库提供广州到xx的查询,剩下的在一个大库中等。车次变化很小,库可以分散存,而且可以冗余存储。用内存表也行,总数据量也不多,性能上没什么可说的。

对于实时票数,确实是比较困难的,网上很多方案都不对,没有考虑中间站的问题。剩余票数缓存并不好做。我的想法是,提前分好票,然后用单独的数据结构做缓存。

例如,对于G113高铁,共有8站:北京、德州、济南、徐州、南京、常州、苏州、上海。假设它有两千张票,座位啊卧铺啊啥的。在发票前,创建新表20120113_G113,然后把2000张票提前插入到表中,每个票都有一个本表内唯一的数字编号。表结构基本上就是:编号、起始站、终点站、座位类型、车厢、座位号、乘客姓名、乘客证件号码、车票状态等实际业务模型需要。初始化时,起点站就是北京(根据顺序存储为1),终点站就是上海(根据顺序存储为8),乘客信息空着表示尚未绑定乘客,车票状态置为“待出售”。

这样我们要查询2012年1月13号G113 济南->南京 的剩余票数时,就查询20120113_G113起始站编号大于等于3并且小于等于5,并且状态为“待出售”的记录数就行了。

每张表2000条数据,对于非春运时节,性能完全足够。对于春运时节,非繁忙路段,性能应该也足够了。对于春运繁忙期,继续看下面的。

车票出售基本流程:

用户选择车票并要求购买,系统锁定票并标记状态为“锁定中”,让用户付款等。完成后标记车票为“已经发售”,并且更新用户信息到车票的持有人信息字段中。此票不再出售。

对于中间站购票,假设用户购买了 济南->南京 ,前面流程不变。但完成出票后,将车票起始和终点站改为“济南”和“南京”,并且自动插入两张新票可用。一张是“北京->济南”,一张是“南京->上海”,通知队列更新相关缓存。相当于车票做了自动分裂。这样我们设计时只需要把一张票设计为“只有起点和终点”就行了。

更高效的剩余票数查询设计:

数据库的count操作并不快,因此对于繁忙季节的繁忙表,每次都count是铁定不行的。我想到的一个办法是:把上面提到的预售车票表加载到内存中。我们用一个64位long类型数字表示一张车票,每趟车的每种座位类型是一个long数组。

对于每个long数字,前面的32位用来顺序存储32个车站(假设一趟车最多有32个站,没查过,不行可以放长点),每一位标记“车票是否包含此站”。如G113 济南到南京的票(车站顺序为第3到第5站),long数字的第2到第4位设置为1,其他前32位设置为0。后面的32位用来表示车票在车票出售表中的唯一编号。这样根据一个long类型数字,我们就能表述一张票的发售信息了。

比如2012年1月13号G113,有软卧500张和硬卧1500张。那就需要两个数组。20120113_G113_软卧 对应一个长度500的long数组;20120113_G113_硬卧 对应一个长度1500的long数组。存储所有售票信息。

有点类似BitSet的感觉,对空间要求不高。我们可以做个系统把所有车票信息按照这种结构加载到内存中。对于实时查票,如查询2012年1月13号G113车次 济南->南京 的余票,就是遍历两个数组,检查位数为2和4的long数字有多少个,就直接获得了软卧和硬卧的剩余票数。对于现代计算机,这点遍历,时间是纳秒级的。

当车票被出售后,从long数组中删除票信息,比如先置为-1表示已经无效。再用后台线程实际删除(避免写冲突,将删除延迟到重建一个数组所消耗的多少纳秒内刚好没有写请求的时间段中)。如果long数组长度为0,那就是没有车票了,直接返回0;用户再怎么刷,也不会干扰数据库。

更高效的剩余票数查询方案的扩展性和容错性:

本身车票是按照车次划分的,同时也有时间维度,横向扩展不存在任何问题。

long数组可以根据数据库票务信息重新构造,而且成本不高(一条“select * from xxx where 状态=待发售” SQL语句)。在硬件故障,扩容机器,或是发现数据不一致时,重新构造数组就行。而且可以从后台异步做。扩展性和容错性都不是问题。

售票交易与锁票:

用户查询到有票后,填写要购买的票数,提交。好比购买两张硬卧,流程如下:

1. 在20120113_G113_硬卧long数组中随机获取符合要求的顺序的两个long数字,并将其从数组中删除;这个速度很快,纳秒级;

2. 根据long数字后32位,从数据库中锁票。如果全部锁定成功,设置票为预定状态,更新用户预定信息,锁票时间等。然后记录日志等其他相关操作。如果锁票失败,说明在纳秒级的时间内,票还是有冲突,购票失败,直接返回报错。锁票就是数据库行锁,sql中的 select for update nowait。

3. 页面提示错误,或者进入下一步交易流程,如网银支付等。

在整个过程中,我们看到,用户请求就算集中爆发,事务的冲突性也能降低到“随机获取的long数组值刚好一样 + 在cpu执行2000个for循环的可能百万分之一秒内刚好同时提交”。我觉得冲突概率应该很低很低。一趟车2000个车票,1:100比例也就20万人同时抢,20万人同1秒提交cpu也算的过来。而实际上,怎么可能一秒钟有20万人同时抢一趟车的票哪……

在整个过程中,大部分请求都被long数组消耗完后,直接检查long数组长度为0,提示无票拦截。进入数据库阶段的,也就是比实际的票数多一点点的有效订单而已。

回票:

中间站买票的,在预定成功后,车票自动分裂,分裂的票可以通过队列调度实时的回到long数组中,继续服务。

预定后不买的,可以通过预定时间的超时检查,后台做个线程,让票回归。

欢迎讨论。

微博:http://weibo.com/guzzframework



   发表时间:2012-01-13  
真正的理性谈方案的还真是没人看。除了诅咒贴能火,其他都是浮云啊。
0 请登录后投票
   发表时间:2012-01-13  
嗯,分析的不错
0 请登录后投票
   发表时间:2012-01-13  
myreligion 写道
真正的理性谈方案的还真是没人看。除了诅咒贴能火,其他都是浮云啊。


赞一个,LZ精神可嘉。不知道怎么了,没看完,最近12306帖子太多了,疲劳了。
0 请登录后投票
   发表时间:2012-01-13   最后修改:2012-01-13
对于中间站购票,假设用户购买了 济南->南京 ,前面流程不变。但完成出票后,将车票起始和终点站改为“济南”和“南京”,并且自动插入两张新票可用。一张是“北京->济南”,一张是“南京->上海”,通知队列更新相关缓存。相当于车票做了自动分裂。这样我们设计时只需要把一张票设计为“只有起点和终点”就行了。
有个疑问:
假如一个用户购买了常州-南京 是不是这张票又得分裂出另外一张票常州-上海,这样子的话,哪是不是得写个JOB自动分裂程序去分析。
0 请登录后投票
   发表时间:2012-01-13   最后修改:2012-01-13
想法很好啊,车票分拆,这个可以有的。

为了提高检索速度,使用按日期分表存储的方式,倒是觉得不是很有必要,起码像Oracle这种类型的数据库,可以使用按日期分区来达到等同的优化目的,还可以便于宏观方面的统计(当然,由于数据量的关系,如果考虑到你后面提及的余票统计内存级缓存优化,代表每一张票的KEY值范围可能不够用)。

如果加上对“退票”情况的考虑,要想最大程度的重用此票,涉及的“合票”处理貌似不省事。
0 请登录后投票
   发表时间:2012-01-14  
楼主精神可嘉,思路也是非常正确,而有调理的,赞一个
0 请登录后投票
   发表时间:2012-01-14  
rainyear 写道
对于中间站购票,假设用户购买了 济南->南京 ,前面流程不变。但完成出票后,将车票起始和终点站改为“济南”和“南京”,并且自动插入两张新票可用。一张是“北京->济南”,一张是“南京->上海”,通知队列更新相关缓存。相当于车票做了自动分裂。这样我们设计时只需要把一张票设计为“只有起点和终点”就行了。
有个疑问:
假如一个用户购买了常州-南京 是不是这张票又得分裂出另外一张票常州-上海,这样子的话,哪是不是得写个JOB自动分裂程序去分析。


不是job,出一张票的同时,就知道需要分裂出的票,或者是0张,或者1张,或者2张。分裂车票在出票时就能做立刻做。

对于退票以后的票合并,确实麻烦点。不过这个可以不做成实时的,而且也已经避开了抢票高峰期,慢慢算然后合成最可能整的票也就行了,应该不会影响性能,毕竟可以做成后台异步的。

0 请登录后投票
   发表时间:2012-01-14  
myreligion 写道
rainyear 写道
对于中间站购票,假设用户购买了 济南->南京 ,前面流程不变。但完成出票后,将车票起始和终点站改为“济南”和“南京”,并且自动插入两张新票可用。一张是“北京->济南”,一张是“南京->上海”,通知队列更新相关缓存。相当于车票做了自动分裂。这样我们设计时只需要把一张票设计为“只有起点和终点”就行了。
有个疑问:
假如一个用户购买了常州-南京 是不是这张票又得分裂出另外一张票常州-上海,这样子的话,哪是不是得写个JOB自动分裂程序去分析。


不是job,出一张票的同时,就知道需要分裂出的票,或者是0张,或者1张,或者2张。分裂车票在出票时就能做立刻做。

对于退票以后的票合并,确实麻烦点。不过这个可以不做成实时的,而且也已经避开了抢票高峰期,慢慢算然后合成最可能整的票也就行了,应该不会影响性能,毕竟可以做成后台异步的。



这个就是一个算法,要说麻烦也不是很麻烦,类似webgis的公交换乘算法的。
0 请登录后投票
   发表时间:2012-01-14   最后修改:2012-01-14
看的出楼主用了心,我想提醒各位的是,你真的确信12306需要干这些事?
0 请登录后投票
论坛首页 入门技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics