最近论坛里已经慢慢有人在考虑票池的设计了,这是我关于票池架构的一些想法。具体的讨论请去论坛上讨论: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系统管理内存分配的数据结构,这个结构可以做成一个包含数组的数组,数组的下标代表这个位置的元素的空闲位数,如下图所示: 这个时候,如果有人买票,例如是坐一站的,那我们就首先去第一个数组里找,找到第一个匹配,将位补齐,这个时候发现位置已满,因此将其从上图的数组中移除,移除它剩下的空就放在那里,如下图所示:
如果有人买两站的票,跟上面一样,找到第二个数组的第一个座位匹配,买了票之后,它的值变成:“11111101”,因为它只有一站是空闲的,因此我们将其放到第一个数组中去,如下图所示:
这个二维数组,每一个车次的列数是固定的 – 因为每个车次经过的站点数目是固定的,而每列对应的数组,如果空间不够了,可以动态分配(这里是一个风险,我还没有仔细计算过极端情形)。 为了节省内存,每个座位的车次是一个长整形,即8个字节组成,这8个字节里,前14位用来表示座位在车次的索引(14位里,可以有12位表示索引,可以表示4096个座位,应该可以满足一趟车上的坐票、卧铺和站票信息了,另外两位可以用来做一些标志位,具体干什么我还没有想好),后50位就是座位的站点占用信息。如下图所示: 对于运行区间超过50个站点的车次,作为特殊车次特殊处理 - 这样的车次应该不是很多,可以先枚举下。 还有对分布式的支持、负载均衡等方面的想法,还没有写完,这两周慢慢写,先把现在想到的发出来,抛砖引玉。 |