《浅谈12306核心模型设计思路和架构设计》阅读笔记
本文的重点不是在如何解决高并发的问题,而是希望从业务角度去分析,12306的理想模型应该是怎么样的。网上目前谈12306的文章貌似都是千篇一律的只谈技术,不谈业务分析和如何建模的。下面是我对于笔者的设计的阅读笔记供大家交流学习。
需求简述
12306这个系统,核心要解决的问题是网上售票。涉及到2个角色使用该系统:用户、铁道部。用户的核心诉求是查询余票、购票;铁道部的核心诉求是售票。购票和售票其实是一个场景,对用户来说是购票,对铁道部来说是售票。因此,我们要设计一个在线的网站系统,解决用户的查询余票、购票,以及铁道部的售票这3个核心诉求。看起来,这3个场景都是围绕火车票展开的。
查询余票:用户输入出发地、目的地、出发日三个条件,查询可能存在的车次,用户可以看到每个车次经过的站点名称,以及每种座位的余票数量。
购票:购票分为订票和付款两个阶段,本文重点分析订票的模型设计和实现思路。
其实还有很多其他的需求,比如给不同的车次设定销售座位数配额,以及不同的区段设置不同的限额。我觉得这个需求不是核心最重要的诉求,所以,本文针对这个需求不做具体讨论,也不是本文分析设计的重点。
需求分析
确实,12306也是一个电商系统,而且看起来商品就是票了。因为如果把一张票看成是一个商品,那购票就类似于购买商品,然后每张票都有库存,商品也有库存的概念。但是如果我们仔细想想,会发现12306要复杂很多,因为我们无法预先确定好所有的票,如果非要确定,那只能通过穷举法了。
我们以北京西到深圳北的G71车次高铁为例(这里只考虑南下的方向,不考虑深圳北到北京西的,那是另外一个车次,叫G72),它有17个站(北京西是01号站,深圳北是17号站),3种座位(商务、一等、二等)。表面看起来,这不就是3个商品吗?G71商务座、G71一等座、G71二等座。大部分轻易喷12306的技术人员(包括某些中等规模公司的专家、CTO)就是在这里栽第一个跟头的。
实际上,这个车次可以卖的票是非常多的。为了方便后面的讨论,我们先明确一下票是什么?
一张票的核心信息包括:出发时间、出发地、目的地、车次、座位号。持有票的人就拥有了一个凭证,该凭证表示持有它的人可以坐某个车次的某个座位号,从某地到某地。所以,一张票,对用户来说是一个凭证,对铁道部来说是一个承诺;那对系统来说是什么呢?不知道。这就是我们要分析业务,领域建模的原因,我们再继续思考吧。
明白了票的核心信息后,我们再看看G71这个车次的高铁,可以卖多少张票?
通过上面的分析,我们知道一张票的本质是某个车次的某一段区间(一条线段),这个区间包含了若干个站点。然后我们还发现,只要区间不重叠,那座位就不会发生竞争,可以被回收利用,也就是说,可以同时预先出售。
另外,经过更深入的分析,我们还发现区间有4种关系:1)不重叠;2)部分重叠;3)完全重叠;4)覆盖;不重叠的情况我们已经讨论过了,而覆盖也是重叠的一种。所以我们发现如果重叠,比如有两个区间发生重叠,那重叠部分的区间(可能夸一个或多个站点)是在争抢座位的。因为假设一列火车有100个座位,那每个原子区间(两个相邻站点的连线),最多允许重叠99次。
所以,经过上面的分析,我们知道了一个车次能够出售一张车票的核心业务规则是什么?就是:这张车票所包含的每个原子区间的重叠次数加1都不能超过车次的总座位数,实际上重叠次数+1也可以理解为线段的厚度。
模型设计
上面我分析了一下票的本质是什么。那接下来我们再来看看怎么设计模型,来快速实现购票的需求,重点是怎么设计商品聚合以及减库存的逻辑。
传统电商的思路
如果按照普通电商的思路,把票(站点区间)设计为商品(聚合根),然后为票设计库存数量。我个人觉得是很糟糕的。因为一方面这种聚合根非常多,另一方面,即便枚举出来了,一次购票也一定会影响非常多其他聚合根的库存数量(只要被部分或全部重叠的区间都受影响)。这样的一次订单处理的复杂度是难以评估的。而且这么多聚合根的更新要在一个事务里,这不是为难数据库吗?而且,这种设计必然带来大量的事务的并发冲突,很可能导致数据库死锁。总之,我认为这种是典型的由于领域模型的设计错误,导致并发冲突高、数据持久化落地困难。或者如果要解决并发问题,只能排队单线程处理,但是仍然解决不了要在一个事务里修改大量聚合根的尴尬局面。听说12306是采用了Pivotal Gemfire这种高大上的内存数据库,我对这个不太了解。我不可想象要是不使用内存数据库,他们要怎么实现车次内的票之间的数据强一致性(就是保证所有出售的票都是符合上面讨论的业务规则的)?
架构设计
订票的实现思路
同时借助像ENode这样的框架,我们可以实现in-memory + Event Sourcing的架构。Event Sourcing技术,可以让领域模型的所有状态修改的持久化统一起来,本来要用ORM的方式保存聚合根最新状态的,现在只需要简单的通用的方式保存一个事件即可(一次订票只涉及一个车次聚合根的修改,修改只产生一个事件,只需要持久化一个事件(一个JSON串)即可,保证了高性能,无须依赖事务,而且通过ENode可以解决并发问题)。我们只要保存了聚合根每次变化的事件(事件的结构怎么设计,本文不做多的介绍了,大家可以思考下),就相当于保存了聚合根的最新状态。而正是由于Event Sourcing技术的引入,让我们的模型可以一直存活在内存中,即可以使用in-memory技术。不要小看in-memory技术,in-memory技术在某些方面对提高命令的处理性能非常有帮助。比如就以我们车次聚合根处理出票的逻辑,假设某个车次有大量的命令发送到分布式消息队列,然后有一台机器订阅了这个队列的消息,然后这台机器处理这个车次的订票命令时,由于这个车次聚合根一直在内存,所以就省去了每次要去数据库取出聚合根的步骤,相当于少了一次数据库IO。这样的好处是,因为一个车次能够真正出售的票是有限的,因为座位就那么几个,比如就1000个座位,估计一般正常情况也就出个2000个左右的座位吧(具体能出多少张票要取决于区间的相交程度,上面分析过)。也就是说,这个聚合根只会产生2000个事件,也就是说只会有2000个订票命令的处理是会产生事件,并持久化事件;而其余的大量命令,因为车次在内存计算后发现没有余票了,就不会做任何修改,也不会产生领域事件,这样就可以直接处理下一个订票命令了。这样就可以大大提高处理订票命令的性能。
另外一个问题我觉得还需要提一下,因为用户订票成功后,还需要付款。但用户有可能不去付款或者没有在规定的时间内完成付款。那这种情况下,系统会自动释放该用户之前订购的票。所以基于这样的需求,我们在业务上需要支持业务级别的2pc。即先预扣库存,也就是先占住这张票一定时间(比如15分钟),然后付款成功后再真实给你这张票,系统做真正的库存修改。通过这样的预扣处理,可以保证不会出现超卖的情况。这个思路其实和传统电商比如淘宝这样的系统类似,我就不多展开了,我之前写的Conference案例也是这样的思路,大家有兴趣的可以去看一下我之前录制的视频。
查询余票的实现思路
我觉得余票的查询的实现相对简单。虽然对于12306来说,查询的请求占了80%,提交订单的请求只占20%。但查询由于对数据没有修改,所以我们完全可以使用分布式缓存来实现。我们只需要精心设计好缓存的key即可;缓存key的多少要看成本,如果所有可能的查询都设计对应的key,那时间复杂度为1,查询性能自然高;但代价也大,因为key多了。如果想key少一点,那查询的复杂度自然要上去一点。所以缓存设计无非就是空间换时间的思路。然后,缓存的更新无非就是:自动失效、定时更新、主动通知3种。通过CQRS架构,由于CQ两端是事件驱动的,当C端有任何状态变化,都会产生对应的事件去通知Q端,所以我们几乎可以做到Q端的准实时更新。
同时由于CQ两端的完全解耦,Q端我们可以设计多种存储,如数据库和缓存(Redis等);数据库用于线下维护关系型数据,缓存用户实时查询。数据库和缓存的更新速度相互不受影响,因为是并行的。对同一个事件,可以10台机器负责更新缓存,100台机器负责更新数据库。即便数据库的更新很慢,也不会影响缓存的更新进度。这就是CQRS架构的好处,CQ的架构完全不同,且我们随时可以重建一种新的Q端存储。不知道大家体会到了没有?
关于缓存key的设计,我觉得主要从查询余票时传递的信息来考虑。12306的关键查询是:出发地、目的地、出发日期三个信息。我觉得有两种key的设计思路:1)直接设计了该查询条件的key,然后快速拿到车次信息,直接返回;这种方式就是要求我们系统已经枚举了所有车次的所有可能出现的票(区间)的缓存key,相信你一定知道这样的key是非常多的。2)不是枚举所有区间,而是把每个车次的每个原子区间(相邻的两个站点所连成的直线)的可用票数作为key。这样,key就非常少了,因为车次假如有10000个,然后每个车次平均15个区间,那也就15W个key而已。当我们要查询时,只需要把用户输入的出发地和目的地之间的所有原子区间的可用票数都查出来,然后比较出最小可用票数的那个原子区间。则这个原子区间的可用票数就是用户输入的区间的可用票数了。当然,到这里我提到考虑出发日期。我认为出发日期是用来决定具体是哪个车次聚合根的。同一个车次,不同的日期,对应的聚合根实例是不同的,即便是同一天,也可能有多个车次聚合根,因为有些车次一天有几班的,比如上午9点发车的一班,下午3点发车的一般。所以,我们也只要把日期也作为缓存key的一部分即可。
总结
本文完全是凭自己对12306这个网站的核心业务的简单思考而得到的一些设计结果。如果真正的DDD领域建模,更多的是要和业务一线的工作人员、领域专家进行深入沟通,才能更深入的了解该领域内的业务知识,从而才能设计出更靠谱的领域模型和架构设计。我们大学生也经常会购票,此文有助于我们了解整个过程。