性能文章>抢了个票,还以为发现了12306的系统BUG>

抢了个票,还以为发现了12306的系统BUG原创

4578130
同事过年要早回家,失去单身手速好多年的他,只能依赖抢票软件定时来抢。
结果,两个抢票软件居然都成功了,一下抢了两个车次。。。真是几家欢喜几家愁!
同一乘车人,同一行程,同一时间段
12306在很久以前,对购票和乘车规则是有限制的,当同一乘车人的两张车票涉及的行程出现冲突时,会拒绝购票请求。
仔细想来,购票业务的真实场景非常复杂,单是 任何区间段购票这一个场景,就涉及到了好多的问题,要兼顾不同区间段的旅客数量,分配不同的席位库存。还要考虑到铁路的收入,长行程旅客优先等等(这就是为什么有时候本站出发无票,但往前多买几站就会有票的原因吧)。
那么,12036是怎么做到席位按区间售卖,而又怎么做到行程冲突校验的呢?而现在,被购买了两张行程冲突的车票,又是哪里出了问题呢?

客票系统架构猜想

猜想1:全境所有的车票以及电子车票,都由总部统一分配调控吗?
如果能统一收口应该是最理想的状态。但是,我理解现有的系统架构不是被统一收口的。只要是坐过火车的都应该听过“某某铁路局温馨提示,xxxx”。而且,对于每个客运段,其运载能力和承载的旅客情况都是不一样的,所以,每个客运局来管理自己负责客运段的票,才更符合实际情况和运营需要。
猜想2:那么,乘客是怎么通过统一的购票平台来订购不同局段的票的呢?
理论上,是由平台向各个涉及的客运段查询余票以及下发订票请求。由下游客运局操作自己的数据库。并同步数据到购票平台。
那么,整体的系统架构可以来大致描画一下了:
这种席位库存分别维护的架构下,虽然对独立运营的实际需求是友好的,但是对系统交互提出了更高的要求。涉及到系统间信息同步,特别是涉及到跨系统的库存扣减和信息同步时,就要求客票系统要有合理的大并发下的最终一致性方案。
怎样保障横跨多客运段席位库的最终一致性呢?
分布式事务是这类场景的一个较成熟的解决方案,就订票场景来看,XA性能较差,用事务消息,时效性上会打折扣, TCC则是一个相对折中且成熟的方案:
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的技术之路却认为:任何技术的落地和实现,都需要依托于真实的业务场景,只有更深入的理解了业务逻辑,才能更好的构建更加合理、稳定、可持续的技术架构。
点赞收藏
分类:标签:
Coder的技术之路

欢迎关注同名微信公众号

请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

API性能调优
30
1