同事过年要早回家,失去单身手速好多年的他,只能依赖抢票软件定时来抢。
结果,两个抢票软件居然都成功了,一下抢了两个车次。。。真是几家欢喜几家愁!
12306在很久以前,对购票和乘车规则是有限制的,当同一乘车人的两张车票涉及的行程出现冲突时,会拒绝购票请求。
仔细想来,购票业务的真实场景非常复杂,单是 任何区间段
购票这一个场景,就涉及到了好多的问题,要兼顾不同区间段的旅客数量,分配不同的席位库存。还要考虑到铁路的收入,长行程旅客优先等等(这就是为什么有时候本站出发无票,但往前多买几站就会有票的原因吧)。
那么,12036是怎么做到席位按区间售卖,而又怎么做到行程冲突校验的呢?而现在,被购买了两张行程冲突的车票,又是哪里出了问题呢?
客票系统架构猜想
猜想1:全境所有的车票以及电子车票,都由总部统一分配调控吗?
如果能统一收口应该是最理想的状态。但是,我理解现有的系统架构不是被统一收口的。只要是坐过火车的都应该听过“某某铁路局温馨提示,xxxx”。而且,对于每个客运段,其运载能力和承载的旅客情况都是不一样的,所以,每个客运局来管理自己负责客运段的票,才更符合实际情况和运营需要。
猜想2:那么,乘客是怎么通过统一的购票平台来订购不同局段的票的呢?
理论上,是由平台向各个涉及的客运段查询余票以及下发订票请求。由下游客运局操作自己的数据库。并同步数据到购票平台。
这种席位库存分别维护的架构下,虽然对独立运营的实际需求是友好的,但是对系统交互提出了更高的要求。涉及到系统间信息同步,特别是涉及到跨系统的库存扣减和信息同步时,就要求客票系统要有合理的大并发下的最终一致性方案。
分布式事务是这类场景的一个较成熟的解决方案,就订票场景来看,XA性能较差,用事务消息,时效性上会打折扣, TCC
则是一个相对折中且成熟的方案:
•幂等,由事务发起者生成全局唯一事务ID,参与者根据事务ID幂等。•网络中断导致一阶段丢包,二阶段允许空回滚;即锁定资源为空的回滚请求,返回回滚成功。•网络拥堵导致请求错序,二阶段需要防资源悬挂;即,已经回滚过的事务ID,不允许锁定资源。
我们用TCC解决了分布式下跨系统分配资源的最终一致性问题,那么,对于文章开头提到的购票限制,又该怎么做呢?
订票业务模型猜想
一次购票行为包含哪些属性
一张车票就是一个订单,那订单的主要属性包括:
购票时间
、 车次
、 席位号
、 乘车人
、 乘客类型
、 出发地
、 目的地
、 出发时间
、 到达时间
、 行程区间集合
。
其中, 行程区间集合
中的 行程区间
,需要包含 区间ID
和 经过该区间的 开始结束时间
两个部分。
这样,配合出发时间,就可以唯一的刻画出该用户的一次旅途的路径细节。
听起来比较复杂,好在我们的列车行驶区间和对应的运行时刻表基本都是固定的,很少调整,即使有也会提前公布。
因此,可以采用两个 BitMap ,分别对各个列车的区间 和 区间时间进行提前刻画.
用户购票区间存储设计
如下图所示,在列车的整个行程bitmap中,将用户购买的区间置为1 ,这样,只要getBit==1 的区间就是用户的行程区间:
用户行程时间区间存储设计
如下图所以,将列车的整个行驶时长分段,比如按5分钟分段,将用户购买的车次对应的区间耗时段置为1 :
当我们希望获取用户的行程真实时间段时,只需要用 出发时间
加上对应的占用时间段耗时时长,即可。
余票库存存储设计
和用户的bitmap初始值为0相反,库存管理使用的bitmap初始值位总的席位数,当有用户订票成功后,对席位数进行扣减:
余票和席位绑定设计
上面的库存管理方案,只能笼统的来统计当前的可用余票库存,但是,购票时,每个订单都需要和席位绑定才行:
每个区间,除了总的席位数,还将席位编号的链表挂在当前区间上。 1-1-A
则表 1车厢-1号-A座
。
当用户购买了一个中间区间的席位后,将对应区间下挂靠的席位摘除:
而其他区间的相同席位,也摘除,重新挂在本区间的最后。这样,可以保证所有区间所挂席位集合,每次获取的第一个席位都是同一个,可以最大努力的保证最长行程可以被优先分配。
我们每次订区间票的时候应该都有体会,从这站订没有票,但是靠近始发地多定几站就有票了。
区间冲突幂等控制
回到文章开头的问题,如果要求同一乘车人,在 同一时间段车次不冲突 、不同车次行程不冲突,从幂等的角度来看,就需要根据 乘车人
、 行程区间
两个属性做幂等控制。
我们一般的幂等策略包括前端拦截 + 服务端校验,这里我们只考虑服务端的幂等方案。
这个场景下,如果要做唯一索引,则需要用UID + 行程区间 。
但是如果采用上述的行程区间的组织方式,唯一索引的方式则不太好用了。
不管是分布式锁,或数据库锁,核心思路是一样的:先加锁,防止对共享资源同时操作,来避免并发引起的资源冲突。
数据库锁一般的模式:一锁 、二判 、三更新,其实更适合于对已有记录进行业务更新。
分布式锁则更灵活一些:当我们创建订单时,要先以用户为key , 查询缓存中存储的区间bitmap和时间bitmap。用当前订单的bitmap 来比对 缓存中的bitmap,如果存在区间冲突或者时间冲突,则不再执行后续操作。
总结
作为客票业务的行外人,没法拿到详细准确系统设计资料,但这不妨碍我们就既有现象,对遇到的问题做个最基本的分析判断。
难道真的是系统bug吗?不至于吧!于是细查了下新闻,原来是12306把行程冲突的限制去掉了。。。白瞎了我这么多的分析~
不过也不能算白瞎,习惯性的从专业的角度去看待日常现象,是我们系统研发工作者应该刻意培养和具备的素质之一。
很多人不想多关注业务场景,觉得和技术关系不大,但是,Coder的技术之路却认为:任何技术的落地和实现,都需要依托于真实的业务场景,只有更深入的理解了业务逻辑,才能更好的构建更加合理、稳定、可持续的技术架构。