zoukankan      html  css  js  c++  java
  • 抛砖引玉:使用二进制位操作,解决铁道部火车票的数据查询和存储问题,超轻量级的解决方案

        又到节假日,园子里面不少高人再次对12306网站的各种问题的各种分析和提出各种解决方案,我也看了这些讨论文章,出于也是一个买票难的“码农”,也来献计献言,把我跟其他人讨论的结果汇总发表一下,希望抛砖引玉,解决铁道部火车票的数据查询和存储问题。

        现在,12306网站给人的第一感受就是购票过程网页很卡,不少人分析是由于数据库非常庞大,有复杂的查询和数据传输,并着重在数据库的设计方面大作文章,却很少有人在数据存储“量”上下功夫。或许大家都说现在磁盘那么便宜,还要刻意关注数据存储量的大小么?我觉得做一个大型的系统必须关注这个问题,因为数据量决定了数据的存储方式、查询方式、处理方式等等,比如数据量太大了一般就要分表甚至分库,甚至分数据中心,数据量大了处理不过来就需要数据库做集群,还有灾备方案等等,甚至,数据量大了,还得在业务处理上做出改变,比如把过期的数据放到备用库去,当前系统只用最常用的数据。

        12306的数据肯定有很多种,这里我只讨论“车票数据”,

    它涉及的数据有:

    • 车票数量、
    • 余票数量、
    • 座位号、
    • 车厢号、
    • 车次、
    • 站次、
    • 发车时间等;

    (同样,为简化设计,我们这里假设车票都是从起点到终点的,没有考虑到中间的站到站的票。)

    涉及的业务操作有:

    • 查询是否有票、
    • 查询余票数量、
    • 查询座位信息(是否有空位、空位位置)、
    • 更新座位信息(买票、退票);

    车票数据该如何存储呢?我跟有的朋友交流,他们说至少要设计这样的数据表:

    ----------------------------------------

           车票表:

    ========================

    int           唯一标识  NOT NULL,

    ----------------------------------------

    int           车次号  NOT NULL,

    ----------------------------------------

    datetime  发车时间  NOT NULL,

    ----------------------------------------

    int           车厢号  NOT NULL,

    ----------------------------------------

    int            座位号  NOT NULL

    ----------------------------------------

        计算下,这个表存储一行数据需要 4+4+8+4+4=24个字节

        假设一列车中的每一节车厢,最多有128个座位(实际上没有这么多,卧铺车厢座位更少)

        那么1节车厢的座位占据的数据量是 128 * 24=3072字节

        假设每列火车有20节车厢,那么车票数据共需要占用 3072 * 20=61440 字节

        至于全国每年会发多少趟车,一共会卖出多少张车票,由于没有准确数字,我这里就不计算了,但相信肯定是一个很大的数字,13亿人每个人每年平均都坐过一次火车吧?那么这个火车票数量还是很庞大的。

        说了车票数量,我们来说说旅客买到一张票,他对一列火车中车票数据的查询情况:

        对1个人而言,最低需要查询1次,最多需要查询128 * 20=2560 次,每人平均查询量= 1280次;

        对整个列车的人而言,买完所有的票,需要查询 1280次 * (128座位 * 20节车厢) = 3276800 次(好熟悉的数字)

    -------------------------------

        使用数据库表的方式来存储车票数据,我们卖出所有的一趟车的车票,居然有高达300多万次查询!

        而且,在国庆、春运等客流量高峰时段,这些查询次数还会更大、更密集!

        分析到了这里,我们似乎已经发现了,12360的车票购买系统,最大的瓶颈,不仅仅在于数据量,还包括查询次数和查询频度!

        问题找到了,我们就来看看如何降低数据量,降低查询次数,来作为系统优化的关键点。

    颠覆传统的设计方案--“二进制位”车票数据查询与存储方案

    关键思想,就是利用二进制中的 0,1 来表示座位情况,即:

    如果当前位等于0,表示空位,可以售票,如果等于1,表示无空位,票已经售出。

    所以,一张车票,仅需一个二进制位!

    假设一列车有20节车厢,每节车厢128个座位,那么仅需要 20 * (128/8)=320 字节来存储整列火车的车票信息。

    各位看官,如果有板凳、西红柿等等的,请先别乱扔,且听下文分解:)

    注意:

    为讲述方便,下面会借用一些计算机语言中的某些概念,如变量申明、位操作等,但仅作为“伪代码”使用,不跟某个具体的语言相对应。)

     一、方案设计

    1,如何卖票?

    假设有一个字节表示1-8号位置,卖票的时候,只需要将字节中指定位置的“位”置为1即可。
    那么我们定义一个字节变量:

    byte P=0x00;


    卖出第一张票,那么现在的座位情况是“0000 0001”   ,变量变为 P=0x01;

    卖出第二张票,那么现在的座位情况是“0000 0011”   ,变量变为 P=0x03;

    卖出第三张票,那么现在的座位情况是“0000 0111”   ,变量变为 P=0x07;

    卖出第四张票,那么现在的座位情况是“0000 1111”   ,变量变为 P=0x0F;

    卖出第五张票,那么现在的座位情况是“0001 1111”   ,变量变为 P=0x1F;

    卖出第六张票,那么现在的座位情况是“0011 1111”   ,变量变为 P=0x3F;

    卖出第七张票,那么现在的座位情况是“0111 1111”   ,变量变为 P=0x7F;

    卖出第七张票,那么现在的座位情况是“0111 1111”   ,变量变为 P=0xFF;

    2,如何标记车票已经卖出?


    现在的问题是,卖出一张票的时候,怎么将变量指定位置的数置为1?
    其实,可以对位进行“OR”操作,
    例如当卖出第五张票的时候,可以这样执行:

    P5= 0x0F OR 0x10 = 0x1F

    其中,0x10 就是 “0001 0000”

    同理,
    例如当卖出第六张票的时候,可以这样执行:

    P6= 0x1F OR 0x20 = 0x3F

    ... ...

    3,如何查询有空位?


    假设我们这个变量P是严格保护的,没有外在因素作用于它,那么很简单,当 P<>0xFF ,那么肯定还有座位。

    4,如何查询特定位置是否还有空位(有票)?


    现在大部分车都需要买票的时候挑选座位,假设以后某列车很特别,需要挑选座位的功能,我们也可以简单实现:

    例如,查询2号位置是否有座:
      假设2号位置有座,那么我们可以这样表示(假设其它位置都有人在坐):P=“1111 1101”,那么进行下面的运算:
      P XOR 0x02 = "0000 0000" = 0

      假设2号位置无座,那么我们可以这样表示(假设其它位置都有人在坐):P=“1111 1111”,那么进行下面的运算:
      P XOR 0x02 = "0000 0000" = 1

      同理,要查询3号位置是否有座,那么只需要
      P XOR 0x4
      即可,如果结果=1,表示无座,可以卖出一张票,反之,则当前位置不可以卖出车票。

    5,如何计算座位号?


    我们规定,在一列车厢里面,有 8 * N 个座位,那么我们用一个数组来存储该列车厢中所有的座位:

    byte[] B=byte[N];

    显然,1-8号位置,是 B[0],9-16号,是B[1]... 以此类推,便可计算出所有的所有的座位号了。

    6,如何退票


      退票,就是将制定座位号的数的特定位置重新置回二进制位的“0”,具体过程,相信看了前面的说明,不用我再说了吧?

     二、方案比较

    1,查询某趟车是否还有空位,需要查询多少次?

         假设一列火车的座位数据为 byte[] T = byte[320], 那么只需要取 T[n]<>0xFF (n>=1,n<=320),最多总共只需要执行 320 次查询,平均算160次;

        对整个列车的人而言,买完所有的票,需要查询 160次 * (128座位 * 20节车厢) = 409600 次,

        当前方案的查询次数是传统查询方案的 1/8 。 

    2,查询某趟车是否还有空位,查询的数据量是多少?

        假设对某次列车的全部火车票的数据进行遍历,那么查询处理的数据量仅为 320字节!

        那么查询所需要的IO吞吐量,将是原有方案的 320 / 61440 = 1 /192 。

    3,查询某趟车是否还有空位,查询效率提升了多少?

        假设效率 E 跟查询次数 C 和数据量D成某个函数关系 E=C * D ,那么原有方案的效率比当前方案的效率相当于 1/ 1280

        看到了吗?现有方案,在查询效率上,有“数量级”的性能提升,是原有方案的1536倍!

    4,其它操作的效率?

        查询指定位置的车票是否已经买车、将指定座位号的车票卖出或者退票,这些都需要遍历整个车的车票数据,带来查询次数和查询的数据量问题,其实这个效率影响跟第3个问题应该差不多,也会有“数量级”的性能提升,具体的效率就由大家去计算了。

    三、方案的并发与效率问题

     本文发出后,下面回复的朋友一致指出了“锁”的问题,因为在传统的高并发程序中,“锁”似乎是必不可少的,必须用锁来保证数据的一致性。所以,这里补充一节内容进行说明。

    另外,我还需要特别说明的是,我的方案不是一个数据库里面的数据存储与查询方案,这是一个完全基于内存数据计算的方案,因为它的数据量足够小,全部的车票数据放到内存中也是没有问题的。

    1,不需要锁的“并发”方案

    当有一个线程准备进行对车票锁定的时候(在火车售票过程中,只要查询到有票就会锁定该票,以便顺利完成下面的售票过程),实质上是将当前查询到的字节中指定的位置置为“1”,出票的时候是确认该操作,而放弃购票的话将该位置重新置为“0”即可。

     那么如何标记当前位置(字节)有线程正在使用呢?

    假设当前位置的序号是S,数据是P,如果它正被前面一个线程使用,那么它必定某个位置已经被置为了“1”,假设P当前的情况是 "0000 0011" ,   那么肯定 P〉0 ,于是新的序号S=S+1,第二个线程使用的数据为P2。

    但这里还有一个问题,前面的这个数据P还有空位没有被填充使用完,所以,我们可以规定数据P一直被它原来使用的线程持有,直到填满,P=0xFF 为止。

    由此,我们推导出第二个重要的方案

    2,批量“写入”方案

    由于数据P一直被它原来使用的线程持有,所以这个线程完全可以等待下一个购票人来使用。注意这里的区别,是等待另外一个人,而不是另外一个线程来争抢。于是,我们便完成了批量写入购票信息的方案。写入之后,该线程再释放,获得另外一个新的数据序号S,进行新的处理。注意,这里的序号S其实是我们车厢车票数组的下标,它本身是不需要进行锁定然后加一的,它仅仅是一个存放在“寄存器”的循环变量。

    3,内存数据高效加载技术

    CPU取数据最快的地方是L1 Cache,通常计算机它的大小是 64字节,我们惊奇的发现,我们一节车厢的128个字节,仅需要加载2次就可以处理完,如果我们的CPU有2个核,那么我们的每个CPU可以同时分别加载64个字节,然后分别进行计算处理。

    如果采用1个CPU只运行1个线程的处理方案,128字节的车票数据分别在2个物理线程(CPU核)中处理,这2个线程根本无需考虑锁的问题。

    该理论可能令很多人难以理解,因为它涉及了真正的物理上的计算机知识,这里不做更多说明,详细内容,请参看著名的LMAX开源高性能架构Disruptor的 

    Disruptor 全解析(6):为什么它这么快 (二) - 神奇的 cacheline 补齐

    实际上,这里的第1点和第2点,原理也比较类似Disruptor的RingBuffer技术,详细内容可以参考

    Disruptor 全解析(6):为什么它这么快 (二) - 神奇的 cacheline 补齐

    四、方案总结与设想

      通过前面的分析,我们发现采用二进制位进行车票数据的查询和存储,能够极大地提升整个系统的操作效率,降低存储量,降低查询次数,而这正是现在12306最需要优化的关键点。

        由于该方案精巧的设计,使得整个12306网站不必依赖于庞大的存储设备和计算设备,以现在服务器的本身配置的超大内存和磁盘存储空间以及CPU计算能力,运行该方案应该很轻松。如果再使用 Disruptor 这样的架构(号称每秒处理600万订单),那么12306网站的问题,是有可能得到解决的。下面是它的架构图:

    附录:

    The LMAX Architecture

      

  • 相关阅读:
    HDU 1102 Constructing Roads
    HDU 1285 确定比赛名次。
    最小生成树 HDU 各种畅通工程的题,prim和kru的模板题
    HDU Jungle Roads 1301 最小生成树、
    并查集小结(转)
    HDU hdu 2094 产生冠军 拓扑排序 判定环
    模运算(转)
    拓扑排序(主要是确定环和加法) HDU 2647 Reward
    HDU 1372 Knight Moves 简单BFS
    用计算机模型浅析人与人之间沟通方式 (一)如何谈话
  • 原文地址:https://www.cnblogs.com/bluedoctor/p/2697206.html
Copyright © 2011-2022 走看看