12306的西天取经路 - 春节抢票与PostgreSQL数据库设计思考

背景

立刻春节了,又到了火车票的销售旺季,一票难求的问题依旧存在吗?git

还记得10年前春节前买火车票得在放票前1天搬个小板凳去排队,对于热门路线,排一个晚上都有可能买不到票。github

随着互联网的发展,几年前建设了12306网上购票系统,能够从电脑上买票,可是不要觉得在电脑上就能买到票。算法

我记得12306刚推出时,常常发生12306网站打不开,没法付款的问题。数据库

为何呢?数组

缘由很简单,春节期间网上购票的人可能达到几亿的级别,并且放票日期是同一天同一个时间点,也就是说同一时刻12306要接受几亿用户的访问。架构

处理能力和实际的访问需求更不上,带来的结果就是网站打不开,系统不稳定的现象。并发

pic

后来12306想了分线路分时段开启的办法,想办法把不一样线路的用户错开时间来访问12306的网站,可是这个方法起初的效果不明显,并非全部用户都知道的(就好像你临时通知今天不上班,但仍是有用户会来单位的),因此大多数用户仍是集中在一个点去访问12306的网站。异步

随着硬件的发展,技术的演进,12306的系统愈来愈趋于成熟,稳定性和响应速度也愈来愈好。数据库设计

听说如今不少商家还开通了云抢票业务,本质上是让你不要冲击12306系统了,把需求提早收集,在放票时,这些系统会进行排队与合并购买,这种手段能够减小12306的访问并发。函数

抢火车票是颇有意思的一个课题,对IT人的智商以及IT系统的健壮性,尤为是数据库的功能和性能都是一种挑战。

接下来咱们一块儿来缕一缕有哪些难点,又有怎样的解决手段。

1、铁路售票系统 - 西天取经之路开始啦

铁路售票系统最基本的功能包括

查询余票、余票统计、购票、车次变化、退票、改签、中转乘车规划 等。

每一个需求都有各自的特色,例如

1. 查询余票,用户在购票前一般会查一下到达目的地有哪些余票,它属于一个高并发的操做,同时须要统计余票张数,须要很强的CPU来支撑实时的查询。

2. 购票,购票和查询不同,购票是会改变库存的,因此对数据库来讲是更新的操做。

并且购票极可能发生冲突,例如不少人要买同一趟车的票,那就出现冲突了,到底卖给谁呢?

须要考虑锁冲突,尽可能的让不一样的人购买时可并行,或者能够合并多人的购票请求,来减小数据库的更新操做。

3. 中转乘车,当用户须要购买的起点和到达站无票时,须要计算中转的搭乘方案。

好比从北京到上海,若是没有直达车,是否是该转车呢?转哪趟,在哪里转就成了问题,简单一点就是买票的人本身想。

高级一点的话,可让12306给你推荐路线,这个涉及的是数据库的路径规划功能。

咱们来逐一分析一下这些需求的特色。

1 查询余票

1. 普通的余票查询需求

你若是要买从北京到上海的火车票,一般会查一下哪些车次还有余票。

查询的过滤条件可能不少,好比

1.1. 上车站、下车站、中转站

1.2. 车次类型(高铁、动车、直达、快速、普客、...)

1.3. 出发日期、时段

1.4. 到达日期、时段

1.5. 席别(硬座、硬卧、...站票)

1.6. 过滤掉没有余票的车次

展现给用时还要考虑到怎么排序(是按始发时间排呢,仍是按票价,或者按余票数量排?),怎么分页。

pic1

眼见不必定为实

查询余票一般不是实时的、或者说不必定是准确的,有多是后台异步统计的结果。

即便是实时统计的结果,在高并发的抢票期间,你看到的信息对你来讲也许很快就会失效。

好比你看到某趟车还有100张票,极可能等你付款的时候,已经卖光了。

因此在高峰期,余票信息的参考价值并不大,不要被迷惑了。

2. 查询余票的另外一个更高级的需求是路径规划, 自动适配(根据用户输入的中转站点s)

这个功能之前可能没有,可是总有一天会暴露出来,特别是车票很紧张的状况下。

就好比从北京到上海,直达的没有了,系统能够帮你看看转一趟车的,转2趟车的,转N趟车的。(固然,转的越多越复杂)。

从中转这个角度来说,实际上已经扯上路径规划的技术了。

怎么中转是时间最短的、价格最低的、中转次数最少的等等。(里面还涉及转车的输入要求(好比用户要求在一线城市转车,或者必需要转高铁))。

关于路径规划,能够参考一下PostgreSQL pgrouting,已支持多种路径规划算法,支持算法的自定义扩展。

简直是居家旅行,杀人灭口的必备良药。

《聊一聊双十一背后的技术 - 物流, 动态路径规划》

师父当心,有妖怪。。。

1. 大多数用户是有选择综合症的,一般来讲,用户可能会查询不少次,才选到合适日期的合适车次的票。

查询量比较大,春节期间更甚。

2. 为了展现余票数量,须要统计,会耗费较多的CPU, IO资源。

3. 路径规划,帮用户选择最佳的转车路线,很考验数据库的功能,大多数数据库没有这个功能。

2 余票统计

对于售票系统来讲,查询余票其实是一个统计操做。

统计操做相比简单查询,不但消耗更多的IO还消耗更多的CPU资源。

想像一下几亿人(其实不用这么多,可能几十万就够了)来查询余票,即便机器没挂掉,也会把全部机器的资源跑满,CPU产生的热量,可能几分钟就能把鸡蛋煮熟咯。

为了减小实时查询余票的开销,一般会分时进行统计,更新最新的统计信息。

用户查询余票信息时,查到的是统计后的结果,前面我已经分析过了,余票是不可信的,因此存在必定的延迟其实也是容许的。

这下不能煮鸡蛋了,由于把几亿个统计请求,变成了1个统计请求,是否是一会儿世界就冷静了呢?

咱们能够看到12306主页的余票大盘数据

pic

师父当心,有妖怪。。。

1. 余票信息须要统计,查询会耗费较多的CPU,IO。

因为余票是不可信的,因此存在必定的延迟其实也是容许的,优化手段是异步统计,用户查询统计后的结果。

3 购票

购票相对于查询余票来讲,从请求量来分析,比查询请求更少,由于一般来讲,用户可能会查询不少次,才选到合适日期的合适车次的票。

可是因为购票是一次交易,每次交易都会产生写操做,并且这种交易并非无限库存的交易,由于库存是有限的,因此设计的关键是下降粒度,减小锁冲突,减小数据扫描量。

另外还须要考虑的因素包括

1. 同一趟车次的同一个座位,在不一样的维度可能会被屡次售卖

1.1 时间维度,如发车日期

1.2 空间维度,不一样的起始站点

2. 票价

票价通常和席别绑定,按区间计费。

另外一个需求是尽可能的将票卖出去,减小空洞座位。

打个比方,从北京到上海的车,中间通过(天津、徐州、南京、无锡、苏州),若是天津到南京段有人买了,剩下的没有被购买的段应该还能够继续被购买。

若是一趟从北京到上海的车,全部的票都被苏州到上海的用户买了,其余的位置没有卖出,铁大哥是否是要哭晕在厕所。

又或者某趟车大量的座位被中途上车的用户买了,是否是能够买到全程的票数就少了。

之前就存在这种状况,对铁大哥的成本是个不小的考验。

师父当心,有妖怪。。。

1. 为了减小购票系统的写锁冲突,例如同一个座位,尽可能不出现由于一个会话在更新它,其余会话须要等待的状况。

(好比A用户买了北京到天津的,B用户买了天津到上海的同一趟车的同一个座位,那么应该设计合理的合并操做(如数据库内核改进)或者从设计上避免锁等待)

其实就是把座位的空间维度(从哪里到哪里)、自己的属性(座位号)、时间维度(发车日期)进行解耦,放到多条记录中,从而在购买时,能够同时进行。

由于数据库中最小的锁目前是行锁(单行记录同一时刻只容许一个会话进行更新,其余的被堵塞,等待释放锁),也许随着技术的发展,会演变成列锁,或者列里面的元素锁(好比数组,JSON)。

4 车次新增、删除、变动

春节来临时、一般须要对某些热门线路增长车次。

及车次的新增、删除和变动需求。

在设计数据库时,应该考虑到这一点。

师父当心,有妖怪。。。

车次的变动简直是牵一发而动全身,好比余票统计会跟着变化,查询系统也要跟着变化。

还有初始化信息的准备,例如为了加快购票的速度,可能会将车次的数据提早准备好(也许是每一个座位一条记录),参考第3个需求的解说。

5 对帐需求

票多是通过不少渠道卖出去的,例如支付宝、去哪儿、携程、铁老大的售票窗口、银行的代理窗口、客运机构 等等。

涉及到实际的销售信息与资金往来的对帐需求。

一般这个操做是隔天延迟对帐的。

6 退票、改签需求

退票和改签也是比较常见的需求,特别是如今APP流行起来,退改签都很方便。

这就致使了用户可能会先买好一些,特别是春节期间,用户没法预先知道何时请假回家,因此先买几张不一样日期的,到时候提早退票或者改签。

改签和退票就涉及到位置回收(对数据库来讲也许是更新数据),改签还涉及购票一样的流程。

7 取票

这个就很简单了,就是按照用户ID,查询已购买,未打印的车票。

8 其余需求

票的种类

学生票、团体票、卧铺、站票

这里特别是站票,站票是有上限的,须要控制一趟车的站票人数

站票一样有起点和终点,可是有些用户可能买不到终点的票,会先买一段的,而后补票或者就一直在车上不下车,下车后再补票。

先上车后补票

这个手段极其恶劣,不过不少人都是这么干的,未婚先孕,如今的年轻人啊。。。。

一般会考虑容积率,避免站票太多。

若是无节制的销售站票,可能坐不下的。

猴哥,师父被妖怪抓走啦

1. 大多数用户是有选择综合症的,一般来讲,用户可能会查询不少次,才选到合适日期的合适车次的票。

查询量比较大,春节期间更甚。

2. 为了展现余票数量,须要统计,会耗费较多的CPU, IO资源。

3. 路径规划的需求,帮用户找出(时间最短、行程最短、指定中转站、最廉价、或者站票最少)等条件的中转搭乘路线。

妈妈不再用担忧买不到票啦。

4. 余票信息须要统计,查询会耗费较多的CPU,IO。

因为余票是不可信的,因此存在必定的延迟其实也是容许的,优化手段是异步统计,用户查询统计后的结果。

5. 为了减小购票系统的写锁冲突,例如同一个座位,尽可能不出现由于一个会话在更新它,其余会话须要等待的状况。

(好比A用户买了北京到天津的,B用户买了天津到上海的同一趟车的同一个座位,那么应该设计合理的合并操做(如数据库内核改进)或者从设计上避免锁等待)

其实就是把座位的空间维度(从哪里到哪里)、自己的属性(座位号)、时间维度(发车日期)进行解耦,放到多条记录中,从而在购买时,能够同时进行。

由于数据库中最小的锁目前是行锁(单行记录同一时刻只容许一个会话进行更新,其余的被堵塞,等待释放锁),也许随着技术的发展,会演变成列锁,或者列里面的元素锁(好比数组,JSON)。

6. 车次的变动简直是牵一发而动全身,好比余票统计会跟着变化,查询系统也要跟着变化。

还有初始化信息的准备,例如为了加快购票的速度,可能会将车次的数据提早准备好(也许是每一个座位一条记录),参考第3个需求的解说。

综合以上痛点和需求分析,咱们在设计时应尽可能避免锁等待,避免实时余票查询,同时还要避免席位空洞。

2、谁是猴子请来的救兵?

通过前面的分析,已经把铁路售票系统最关键的几个业务场景进行了描述,而且阐述了其中的设计痛点,那么咱们如何设计合理的系统来知足几亿人民抢票的需求呢?

西游记里每一集孙悟空师父被妖怪抓走,总能找到救兵来解救。

咱们也须要救兵,救兵快来啊。。。。

PostgreSQL是全世界最高级的开源数据库,几乎适用于任何场景。

有不少特性是能够用来加快开发效率,知足架构需求的。

针对铁路售票系统,能够用到哪些救命法宝呢?

1. 看招,法宝1,varbit类型

使用varbit存储每趟车的每一个座位途径站点是否已销售。

例如 G1921车次,从北京到上海,途径天津、徐州、南京、苏州。包括起始站,总共6个站点。 那么使用6个比特位来表示。

'000000'

若是我要买从天津到徐州的,这个值变动为(下车站的BIT不须要设置)

'010000'

这个位置还能够卖从北京到天津,从徐州到终点的任意站点。

余票统计也很方便,对整个车次根据BIT作聚合计算便可。

统计任意组合站点的余票( 北京-天津, 北京-徐州, 北京-南京, 北京-苏州, 北京-上海, 天津-徐州, 天津-南京, ......, 苏州-上海 )

udf_count(varbit) returns record

统计指定起始站点的余票(start: 北京, end: 南京; 则返回的是 北京-南京 的余票)

udf_count(varbit, start, end) returns record

以上两个需求,开发对应的聚合函数便可,其实就是一些指定范围的bitand的count操做。

经过法宝1,解决了统计余票的需求、售票无空洞的需求。

2. 看招,法宝2,数组类型

使用数组存储每趟车的起始站点,途经站点。

使用数组来存储,好处是可使用到数组的GIN索引,快速的检索哪些车次是能够搭乘的。

例如查询从北京到南京的车次。

select 车次 from 全国列车时刻表 where column_arr @> array['北京','南京'];

这条SQL是能够走索引的,效率很是高,每秒请求几十万不是问题。

法宝2解决了高并发请求查询符合条件的列车信息的需求。

3. 看招,法宝3,skip locked

这个特性是跳过已被锁定的行,好比用户在购买某一趟从北京到南京的车票时,实际上是一次UPDATE ... SET BIT的操做。

可是极可能其余用户也在购买,可能就会出现锁冲突,为了不这个状况发生,能够skip locked,跳过锁冲突,直接找另外一个座位。

select * from table   
  where column1='车次号'   -- 指定车次  
  and column2='车第二天期'   -- 指定发车日期  
  -- and mod(pg_backend_pid(),100) = mod(pk,100)   -- 提升并发,若是有多个链接并发的在更新,能够直接分开落到不一样的行,可是可能某些pID卖完了,可能会找不到票,建议不要开启这个条件  
  and column4='席别'  -- 指定席别  
  and getbit(column3, 开始站点位置, 结束站点位置-1) = '0...0'  -- 获取起始位置的BIT位,要求所有为0  
  order by column3 desc   -- 这个目的是先把已经卖了散票的的座位拿来卖,也符合铁大哥的思想,尽可能把起点和重点的票卖出去,减小空洞  
  for update  
  skip locked  -- 跳过被锁的行,老牛逼了,不须要锁等待  
  limit ?;     -- 要买几张票

法宝3解决了一伙人来抢票时,在同一趟车的座位发生冲突的问题。

4. 看招,法宝4,cursor

若是要查询大量记录,可使用cursor,减小重复扫描。

5. 看招,法宝5,路径规划

若是用户选择直达车已经无票了,能够自动计算如何转乘,根据用户的乘车站点和目的地选择最佳搭乘路线。

参考一下pgrouting,与物流的动态路径规划需求一致。

《聊一聊双十一背后的技术 - 物流, 动态路径规划》

6. 看招,法宝6,多核并行计算

开源也支持多核并行计算的,在生成余票统计时,为了提升生成速度,能够将更多的CPU加入进来并行计算,快速获得余票统计。

就好比你策划了一本书,已经列好了大纲,同时你找了100个做者,这100个做者能够根据你分配的工做,同时开始写做,很快就能把一本书写完。

而传统的状况,一本书,只能一个做者帮你写,即便你找了100个做者,另外的99位也只能空闲,或者他们只能写其余的99本书。

7. 看招,法宝7,资源隔离

PostgreSQL为进程模型,因此能够控制每一个进程的资源开销,包括(CPU,IOPS,MEMORY,network),在铁路售票系统中,查询和售票是最关键的需求,使用这种方法,能够在关键时刻保证关键业务有足够的资源,流畅运行。

这个思想和双十一护航也是同样的,在双十一期间,会关掉一些没必要要的业务,保证主要业务的资源,以及它们的流畅运行。

8. 看招,法宝8,分库分表

铁路数据达到了海量数据的级别,很显然一台机器没法存下全部的铁路数据。

那么怎么办呢? 能够将铁路的数据进行分区存储,存到不一样的主机。

PostgreSQL的分库分表方案不少,例如plproxy, pgpool-II, pg-xl, pg-xc, citus等等.

9. 看招,法宝9,递归查询

铁路有很是典型的上下文相关特性,例如一趟车途径N个站点,全国铁路组成了一个很大的铁路网。

递归查询能够根据某一个节点,向上或者向下递归搜索相关的站点。

好比在有哪些车能够直达北京,有哪些车能够转车到达北京,又或者查询从北京到拉萨,有哪些线路以及途经线路能够走。

pic

10. 看招,法宝10,MPP,打完收工

为了持续的提升12306的体验,铁大哥还有数据挖掘的需求,好比今年春节应该对哪些线路增长车次,天天的车次增长的规划,哪些线路能够减小车次也能在春节前将用户送回家。

这些问题能够基于以往的运输数据进行挖掘计算,进行回答。

基于PostgreSQL的MPP产品不少,例如Postgres-XL, Greenplum, Hawq, REDSHIFT, paraccl, 等等。

使用PG能够和这些产品很好的融合,保持语法一致。

下降数据分析的开发成本。

10道法宝一出,师父又回来啦。

猴子请来的救兵厉害吧,别急,还有更厉害的,阿里云在PostgreSQL基础上作了不少的改进,好比对12306的系统,就有特别的定制特性。

3、阿里云PostgreSQL varbit, array加强介绍

在铁路购票系统中,有几个需求须要用到bit和array的特殊功能,这些特殊的功能目前社区版本没有,阿里云RDS PostgreSQL对此作了加强,以下。

1. 余票统计

统计指定bit范围=全0的计数

不指定范围,查询任意组合的bit范围全=0的计数

2. 购票

指定bit位置过滤、取出、设置对应的bit值

根据数组值取其位置下标。

回顾一下我以前写的两篇文章,也是使用varbit的应用场景,有殊途同归之妙

《基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统》

《门禁广告销售系统需求剖析 与 PostgreSQL数据库实现》

PostgreSQL的bit, array功能已经很强大,阿里云RDS PostgreSQL的bitpack也是用户实际应用中的需求提炼的新功能,大伙一块儿来给阿里云提需求。

打造属于国人的PostgreSQL。

小结

本文从铁路购票系统的需求出发,分析了购票系统的部分痛点,以及数据库设计时须要注意的事项。

PostgreSQL的10个特性,以及阿里云对PostgreSQL的改进,能够很好的知足铁路购票系统的需求。

1. 使用varbit存储每趟车的每一个座位途径站点是否已销售。解决了统计余票的需求、售票无空洞的需求。

2. 使用数组存储每趟车的起始站点,途经站点。数组类型支持索引,解决了高并发请求查询符合条件的列车信息的需求。

3. 使用skip locked特性,解决了一伙人来抢票时,在同一趟车的座位发生冲突的问题。

4. 使用pgrouting路径规划特性,解决了智能推荐乘车的需求。

同时还能够用在不少场景,好比金融风险控制,刑侦,社会关系分析,人脉分析等。

若是你感兴趣,网上有不少分析PostgreSQL, pgrouting, Neo4j的文章,PostgreSQL甚至比Neo4j更适合graph场景.

5. 多核并行计算,让更多的CPU同时帮你干活,例如快速的异步余票统计。

6. 减小坐席空洞的产生,保证更多的人能够购买到全程票。(购票时,若是是中途票,尽可能选择已售的中途票)

7. 根据每一个进程进行资源隔离,能够提升稳定性。

8. 对接HybridDB (基于GP\HAWQ) MPP系统,语法一致,能够支持铁路系统的数据挖掘需求,节约了开发成本。

阿里云长期提供PostgreSQL, HybridDB ( 基于Greenplum, HAWQ ) 服务和支持。

相关文章
相关标签/搜索