浅谈12306设计思路和算法
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈12306设计思路和算法相关的知识,希望对你有一定的参考价值。
前言
春节期间,在汤哥的ENode QQ群(185916873)里,大家对12306的模型设计的讨论已经炸开了锅,很多大神都参与了讨论。由于我的DDD知识比较弱,和汤哥讨论了3-4个晚上,最后我跪了,在模型设计方面汤哥是相当专业的,汤哥的模型设计是相当正确的。在订票的算法上,和汤哥有点不同,我这篇文章旨在介绍我的设计思路和算法。在模型设计和架构设计上,汤哥的文章已经讲的很详细了。阅读本文之前,先要拜读汤哥的这篇文章。
我是技术性宅男,平时不写文章,我认为有写文章的时间,代码都能实现出来了。汤哥鼓励我们多去写文章,因为在文章当中能把思路的变化阐述出来,还能落地下去,于是有了这篇处女作。我平时就不善于表达和沟通,可能文章会有很多地方欠妥,大家凑活着看吧。
分析与思路
大体上和汤哥是一样的。下面是我的分析:
分析票
票是一个你上车的凭证,商品。
一张票的核心信息包括:出发时间、出发地、目的地、车次、座位号。
同一车次,出发地、目的地、座位号不同,票就不同。
由此可见,这三个元素,是相当重要的。
再分析,出发地、目的地是我们订票的需求条件,已知的。
那这个座位呢,不知道,是铁道部给我们的。
但是票的组成要有这3个,一想可知,想要生成票,就必须确定这个座位。
那么订票的重点就出来了,要确定这个座位。
那无座怎么算呢,无座也是一种座位,没有座位号的座位,正常的座位都是有座位号的。
在这里,我把出发地到目的地合并为一个线段,叫区间。下面文中所有提到的区间,都是某一站到另一站的这个线段。如:12区间,就是第1站到第2站的线段。
订票区间:就是用户要订票的出发地到目的地的线段。
分析座位
继续分析,那座位呢。
这个座位,在没订票前,全程都可以坐,一但有人订了AB区间,那么AB这个区间就不能坐了,只能坐其他的区间,比如:BC区间,CF区间等等。
这样看来,区间也是座位的重要信息,是座位的组成部分。
在订票中,对我们有意义的是可订票的区间。已经被订过的区间,我们并不感兴趣。
12306复杂度在哪里呢?
这个座位某个区间被预定出去后,它的可订票区间就发生了变化。经过上面分析,这个区间又是我们确定座位的条件,那可确定的座位也会发生变化。
也就是说,根据区间确定座位,座位确定后又影响区间,区间又影响确定座位,座位确定后,又影响区间,循环了。这个问题,就有点像蛋破生鸡,鸡大生蛋的感觉了。于是复杂度就出来了。
再加上铁道部还会有一些战略层面的考虑,比如:长途车的某些大站会有预留票的,要不然长途车都被短途的人坐了,失去了长途车的意义了。这个角度我这里不做考虑,因为和思路算法没关系。
简单化问题
我们要分开看。
1.找到座位,就是根据区间,确定座位,那么只要有区间能找到座位就完成了。
2.确定座位后,更新座位的可订票区间,这个确定业务就完成了。
但是,2步骤会影响1步骤的后续进行,那么我们在完成2步骤后,再刷新一下1步骤中的区间不就可以了吗。
于是就有了第3步。
3.刷新确定座位的可订票区间。
订票当然要有票的生成,这个步骤放在第3或第4步骤都可以,我放在了第3步。
于是就变成了:
1.找到座位。
2.确定座位,更新座位的可订票区间。
3.生成票。
4.刷新确定座位的可订票区间。
为了保证订票的合理性,这4个步骤必须是原子的,否则就会出现问题了。
思路与算法
经过上面的分析,找到了座位,票就算是订到了。找不到,就是无票可订了。那么我们具体来分析一下订票和退票,我的思路如下:
订票
经过上面的分析,订票就是根据区间确定座位,并生成票。确定座位是很重要的一环,输入就是区间,输出就是座位。
1.找到座位
那么最优的算法,就是直接简单得从输入得到输出。那么按照这个原则,于是我们就想到了区间到座位的映射。
于是有了Map<区间,座位>,根据区间得到座位,直接,用Map直接get简单。满足上面的最优算法的原则。
找到了座位,这个订票就完成一大半了,没找到这个座位,那就说明没票了。
那么这个Map怎么设计呢:
假如:
这个车次,共有9个站点,分别为1,2,3...到9。
一共有1000个座位,座位号为1,2,3...到1000.(暂时不考虑什么一等座,二等座什么的)
区间15,就是表第1站到第5站。
那这个Map<区间,座位>的数据就像这样:{15,1-500},{19,200-500},{57,150-550}等等。
2.确定座位
经过上面的分析,这个座位是有可订票区间的,其实这个确定步骤,就是更新这些可订票区间。
那么怎么更新这个区间呢,算法有什么样的呢,下面我说下:
订票时,会把原来的可订票区间分裂,减去订票区间。
退票时,会把原来的可订票区间合并,加上订票区间。
分离还好,这个合并会有点小复杂,牵扯到前合并,后合并,同时前后合并问题。
下面举例说下,假如:
这个车次,共有9个站点,分别为1,2,3...到9。
一共有1000个座位,座位号为1,2,3...到1000.(暂时不考虑什么一等座,二等座什么的)
这个座位A的可订票区间变化如下:
未出票前:{19}
出57票后:{15,79} 分裂19变为{15,57,79},去掉57,就剩下{15,79}去掉原19
出35票后:{13,79} 分裂15变为{13,35,79},去掉35,就剩下{13,79},去掉原15
出78票后:{13,89} 分裂79变为{13,78,89},去掉78,就剩下{13,89},去掉原79
退57票后:{13,57,89} 不能合并,增加57变成{15,57,89}
退35票后:{17,89} 合并13,35,57,变为{17,89},去掉原13,57
退78票后:{19} 合并17,78,89,变为{19},去掉原17,89
出19票后:{} 直接去掉原{19}
座位区间的变化算法,就是分裂与合并,经过上面的演变,可以看出分裂和合并的具体细节。
分裂:减少原来的区间,可能会增加分裂后的新区间。
合并:可能减少原来合并前的区间,增加合并后的新区间
3.生成票
这个没什么可说的,就是生成一张凭证。包含出发时间、出发地、目的地、车次、座位号这些信息。
4.刷新确定座位的可订票区间
经过2步骤,根据座位的区间变化,来更新Map<区间,座位>的座位内容。
怎么更新呢,这个就比较简单了。
添加的区间,在Map<区间,座位>中,找到对应的区间元素,添加这个座位,如果没有这个区间配备的元素就新建。
减少的区间,在Map<区间,座位>中,根据区间找到对应的座位列表,移除这个座位,如果座位数为0了,可以移除这个区间元素。
举例说明一下,假如:
这个车次,共有9个站点,分别为1,2,3...到9。
一共有1000个座位,座位号为1,2,3...到1000.(暂时不考虑什么一等座,二等座什么的)
Map<区间,座位>的数据变化如下:
1.未出票前:{19,1-1000}
2.订57区间票后:{19,2-1000},{15,1},{79,1}
3.订35区间票后:{19,2-1000},{13,1},{79,1}
4.退35区间票后:{19,2-1000},{15,1},{79,1}
5.退57区间票后:{19,1-1000}
退票
大体上和订票差不多,为了保证能退票,你的车次聚合根要有一个所有票的实体,如:List<Ticket>
1.退票
这个没什么可说的,就是消除这张凭证。从List<Ticket>中移除这张票。
2.释放座位
退票时,会把原来的可订票区间合并,加上订票区间。处理同上面的订票部分。
合并会有点小复杂,牵扯到前合并,后合并,同时前后合并问题。
下面举例说下,假如:
这个车次,共有9个站点,分别为1,2,3...到9。
一共有1000个座位,座位号为1,2,3...到1000.(暂时不考虑什么一等座,二等座什么的)
这个座位A的可订票区间变化如下:
未出票前:{19}
出57票后:{15,79} 分裂19变为{15,57,79},去掉57,就剩下{15,79}去掉原19
出35票后:{13,79} 分裂15变为{13,35,79},去掉35,就剩下{13,79},去掉原15
出78票后:{13,89} 分裂79变为{13,78,89},去掉78,就剩下{13,89},去掉原79
退57票后:{13,57,89} 不能合并,增加57变成{15,57,89}
退35票后:{17,89} 合并13,35,57,变为{17,89},去掉原13,57
退78票后:{19} 合并17,78,89,变为{19},去掉原17,89
出19票后:{} 直接去掉原{19}
座位区间的变化算法,就是分裂与合并,经过上面的演变,可以看出分裂和合并的具体细节。
分裂:减少原来的区间,可能会增加分裂后的新区间。
合并:可能减少原来合并前的区间,增加合并后的新区间
3.刷新确定座位的可订票区间
处理同上面的订票部分。
同样,整个退票过程也是原子的。
大体上我的思路和算法就说完了,对应的代码我也落地了,数据存储、持久化部分和查询端我都没有做。这部分内容接近架构层面的问题了,和思路算法关系不大。
刨除这3部分,在本机也测试了一下,性能还可以,因为都是内存操作。10W次订票120左右毫秒,查询就更快了,100W次查询20毫秒左右。
要想实现更高的QPS,就要牵扯架构方面,这部分可以参考汤哥的文章,这块不是本文的重点。
以上是关于浅谈12306设计思路和算法的主要内容,如果未能解决你的问题,请参考以下文章