12306票池架构探讨(一)

 最近论坛里已经慢慢有人在考虑票池的设计了,这是我关于票池架构的一些想法。具体的讨论请去论坛上讨论:http://12306ng.org/thread-1572-1-1.html


需求讨论
到目前为止,我了解到票池需求有: 
1、车票的预售期不定,有30天的,也有10天的,但应该是10天的居多。
2、票在事先有计划售票管理,制定票额计划和编制临时票额计划。这样一来票是动态分配的,在预售期的前几天,会根据热门乘车区间预先分配一些票,在预售期后面几天,会将没卖完的回收到票池里。
3、需要考虑退票和改签的问题。
4、需要考虑有一部分票可能是预留给一些特别的单位,这些预留票可能最后没有卖出去,回收到票池中。
5、需要考虑中途上下车的情形,为了保证客运的产出,最好是同一个座位,尽可能地多卖票。比如说,上海到北京的火车,如果有乘客甲买了上海到南京,乙买了南京到北京的车票,从座位的使用效率来讲,当然是甲和乙都坐一个位子就好了。

应该还有其他的需求,我建议一开始可以缩小需求范围,避免需求膨胀,我觉得开始可以只考虑下面的需求:
1、只支持10天预售。
2、不支持计划,也就是我们从上游计划系统获取已经计划好的票额,放进票池中。
3、支持退票和改签。
4、支持预留票。
5、支持中途上下车的情形,一个座位重复卖票的问题。

架构设计思路
票池的架构应该考虑下面几个问题:
1、票池应该可以方便分布式,从上面的需求来看,票池至少可以从两个维度考虑分布,首先根据售票的地点,即车票的始发站分布。这样一来,相应的票池服务器离乘客是最近的,对于异地购票的乘客,可以直接重定向到异地服务器,或者在本地缓存一些票都是可以的;再就是可以根据时间分布,即1天后开车的车票和9天后开车的车票是完全可以放在不同的服务器上的。
2、为了保证售票的速度,应该尽量将整个票池放在内存中,一些关键的数据尽量放在CPU缓存里。这是因为,硬盘的随机访问速度是内存访问速度的10000倍,硬盘的顺序访问速度要比随机访问速度快很多;而CPU二级缓存的访问速度又比内存快2到3倍,一级缓存要比内存快10倍左右。
3、CPU将数据读取到缓存的过程一般是批量读取的,而不是一个字节一个字节读取;为了能够尽可能地利用上CPU缓存,因此要尽量将相关数据放在连续的内存里。这样一来,最好是尽量使用数组结构,而不是链表
4、使用链表等非连续结构还有几个问题,第一在分配内存时,对于C/C++这样的程序,在分配内存时查找空闲内存比较耗时间,对于Java等基于垃圾回收语言,GC后更新链表的引用也是一个问题。第二就是内存碎片问题,对于长期在线的服务器,我觉得应该尽量避免使用链表结构。
5、尽可能的无锁操作,即使整个票池都在内存里,如果是需要锁来同步多线程的话,会有几个问题,第一是需要从用户态切换到内核态,这个过程可能需要执行几千个甚至更多的指令;第二是因为线程来回切换,原先CPU缓存的代码和数据都将无效,需要来回在缓存和内存倒腾数据。
6、在对票池并发处理时,我觉得应该只有一个线程负责写入信息,其他的线程都只负责读取。不使用多线程写入的好处是,第一可以实现无锁;第二可以避免伪共享问题。

现有方案对比
我在之前的帖子里提到了使用有向图的设计方案,我现在依然坚持这个方案 - 不过改成用有向图做索引,我先对比一下论坛上其他几个方案(详细情况参看:http://12306ng.org/forum.php?mod ... 01&fromuid=5805):
1、二进制的方案,虽然在我的设计里会有类似二进制的方案,但是原始二进制方案的一个很大的问题是,好像没有考虑数据库自身的实现,例如在帖子里说是编写类似的查询:
where (station>0011111100) and (not (station&0011111100)^0011111100) limit 10

上面的条件子句,从数据库实现的角度来说,需要考虑怎么建立索引,B树应该是不能建立这样支持按位操作的索引的(如果可以的话请纠正我),不过不知道位图索引是否可以支持 – 但mysql好像不支持位图索引。如果没有一个很有效的索引解决方案的话,在数据库中使用二进制方案恐怕会变成逐行扫描 - 也就是有大量的磁盘访问,效率就很低了 。

2、两个整数表示始发站和结束站,这个方案会经常维护二叉树结构,而且树的节点个数和高度都不是确定的 – 这是因为一个座位如果拆分成多个短途订单,这个座位会在二叉树里有多个节点。 

 

 


在上图里面,可以看到,每个站点(就是图里面的节点)用一个列表保存了经过它的所有的车次(边),通过有向边的方式指明车次的方向,一个车次其实是由多条边组成的。

可以把站点(例如北京)和车次(例如G017)本身看成获取数据的索引,例如在server-core/cpp/sites.h里,将所有的站点定义成一个枚举型;server-core/cpp/trains.h里,将所有的车次定义成一个枚举型(以数字开头的,在前面加上下划线就可以了)。由于站点和车次不是经常更换,因此可以固定起来,以后有更新的话,只需要提供站点和车次的配置文件,直接生成上面两个代码就可以了,如果买票订单保存的是起始和终点站的索引的话,在重新生成的时候就需要考虑保证相同站点名的索引值不变,但如果订单直接保存站点名称,就没必要保证索引值不变了。

索引如下图的二维表所示,其中上面两个数组分别是车次G108和G107的余票信息,“-”表示这个位置车次经过该站点,它的值实际是一个指针,指向对应车次的余票数组:




又因为需要考虑中间上车的情况,二进制的方案如果是放在数据库里,会有很大的性能的问题,那么我在考虑是否可以将二进制的方案整个放在内存呢?我觉得是可能的,主要是出于下面几个发现:
1. 首先在上图里,车次的余票信息的确是一个大数组,这个数组可以是一个位数组,每一位代表这个座位的售票情况,只要这个座位有过售票 - 不管是从始发站坐到终点站的,还是中间上车的,那么就将这个位设成1。而一个车次的车厢配置、车厢的座位、铺位配置在一个固定的时间段,至少是一天内是固定的,可以认为是不经常改变的。
2. 还没有卖出去票,是不需要保存在内存里,只要在上面的数组里将对应位设为0就好了。
3. 所有从始发站坐到终点站的车票也不需要保留在内存里,只要在上面的数组里将对应位设为1就好了.
4. 在内存里我们只要找到一个数据结构,用来保存中间会上下车的座位信息就可以了,这个信息就可以用二进制的方案来表述,第一是占用的内存量小,第二是对比和修改都很快。
5. 至于退票,我还在考虑是放回票池,还是用一个单独的链表结构来保存,我现在倾向于放回票池。
6. 那保存每个车次的中间上下车的余票信息,我们可以借鉴Windows系统管理内存分配的数据结构,这个结构可以做成一个包含数组的数组,数组的下标代表这个位置的元素的空闲位数,如下图所示:


 

每个车次都有类似上图的二维数组,在上图里,数组的第一个元素里,包含的是该车次所有最大连续空闲站点数为1的座位,也就是说只有1站没有人坐的位置;第二个元素,是该车次有连续2站没有人坐的位置,虽然第二个元素我们看到实际是有三站空余,但我们仍然放在第二个元素里。

这个时候,如果有人买票,例如是坐一站的,那我们就首先去第一个数组里找,找到第一个匹配,将位补齐,这个时候发现位置已满,因此将其从上图的数组中移除,移除它剩下的空就放在那里,如下图所示:

 


如果有人买两站的票,跟上面一样,找到第二个数组的第一个座位匹配,买了票之后,它的值变成:“11111101”,因为它只有一站是空闲的,因此我们将其放到第一个数组中去,如下图所示:

 


这个二维数组,每一个车次的列数是固定的 – 因为每个车次经过的站点数目是固定的,而每列对应的数组,如果空间不够了,可以动态分配(这里是一个风险,我还没有仔细计算过极端情形)。

为了节省内存,每个座位的车次是一个长整形,即8个字节组成,这8个字节里,前14位用来表示座位在车次的索引(14位里,可以有12位表示索引,可以表示4096个座位,应该可以满足一趟车上的坐票、卧铺和站票信息了,另外两位可以用来做一些标志位,具体干什么我还没有想好),后50位就是座位的站点占用信息。如下图所示:


对于运行区间超过50个站点的车次,作为特殊车次特殊处理 - 这样的车次应该不是很多,可以先枚举下。

还有对分布式的支持、负载均衡等方面的想法,还没有写完,这两周慢慢写,先把现在想到的发出来,抛砖引玉。
posted @ 2012-10-17 19:54  donjuan  阅读(4530)  评论(9编辑  收藏  举报