广州网站建造集团官网 老直营威尼斯网址开户
老品牌威尼斯网址开户 吾们 效劳 网站建造 移动应用 案例 报道 联系
咨询热线:13711534025

期待聆听您的声音

13711534025

不忽悠,不作恶,不欺诈;敬天理,存良知,思利他。
QQ咨询 QQ咨询 QQ咨询
广州总部 深圳 佛山 广西

与吾们一起分享美好

浅谈12306主要模型策划思路和架构策划

发布时间:2016-02-23 发布作者:老直营威尼斯网址开户 查阅次数:1481次 标签:12306

上言

过年期间,无意中看到一篇文章,文章中讲到12306的业务复杂度远远比淘宝天猫这种电商网站要复杂。下来自己想想,也确实如此。以是,很想挑战一下12306这个系统的主要区域模型的策划。一般的电商网站,购买都是基于商品的概念,每个商品有一定量的库存,用户的购买行为是针对商品的。当用户发起购买行为时,系统只需要生成订单并对用户要购买的商品减库存即可。但是,12306就不是soeasy了,具体复杂在哪里,我下面会进一步归纳。

其余一个让我写这篇文章的原因,是我发现也许是否是因为目上12306的主要区域模型策划的不够好,导致用户购票时要处理的业务逻辑异常复杂,维护数据一致性的难度也几百倍的上升,再是面对高并发的订票也难以支持很高的TPS。我觉得,越是复杂的业务,就越要重视业务归纳,重视区域模型的抽象和策划。如果不假思索,凭以往经验行事,则很可能会被以往的策划经验先入为主,陷入死胡同。我发现 技术实现 人员往往更注重 技术实现 层面的解决Plan,譬喻一上来就归纳如何集群、如何负载均衡、如何排队、如何分库分表、如何用锁,如何用缓存等 技术实现 小case,而忽鹿闼最根本的业务层面的思考,如归纳业务、区域建模。我认为越是复杂的业务系统,则越要策划一个健壮的区域模型。如果一个系统的架构吾们策划错了,还有补救的余地,因为架构最终沉淀的只是代码,调整架构即可(一个系统的架构本身就是不断演进的);而如果区域模型策划错了,那要补救的代价是非常大的,因为区域模型沉淀的是数据结构及其对应的大量数据,对尽数一个大型系统,要改主要区域模型都是底非常高的。

本文的细节不是在如何解决高并发的小case,而是瞩望从业务角度去归纳,12306的理想模型应该是怎么样的。网上目上谈12306的文章貌似都是千篇一律的只谈 技术实现 ,不谈业务归纳和如何建模的。以是我想写一下自己的策划和众家交流进修。

需求概述

12306这个系统,主要要解决的小case是网上售票。涉及到2个角色使用该系统:用户、铁道部。用户的主要诉求是查问余票、购票;铁道部的主要诉求是售票。购票和售票莫过易于 一个场景,对用户来说是购票,对铁道部来说是售票。易于 ,吾们要策划一个在线的网站系统,解决用户的查问余票、购票,以及铁道部的售票这3个主要诉求。看起来,这3个场景都是围绕火车票展开的。

查问余票:用户输入出发地、鹄的地、出发日三个条件,查问可能存在的车次,用户可以看到每个车次经过的站点名称,以及每种座位的余票数量。

购票:购票分为订票和会帐两个阶段,本文细节归纳订票的模型策划和实现思路。

莫过于还有很多其他的需求,譬喻给不同的车次设定销售座位数配额,以及不同的区段设置不同的限额。但相比上面两个需求来说,我觉得这个需求相对次要一些。

需求归纳

确实,12306也是一个电商系统,而且看起来商品就是票了。因为如果把一张票看成是一个商品,那购票就类似于购买商品,然下每张票都有库存,商品也有库存的概念。但是如果吾们仔细想想,会发现12306要复杂很多,因为吾们无法预先细目好一切的票,如果非要细目,那只能通过穷举法了。

吾们以北京西到深圳北的G71车次高铁为例(这里只揣摩南下的方向,不揣摩深圳北到北京西的,那是其余一个车次,叫G72),它有17个站(北京西是01号站,深圳北是17号站),3种座位(牛逼商用、一等、二等)。表面看起来,这不就是3个商品吗?G71牛逼商用座、G71一等座、G71二等座。大部分轻易喷12306的 技术实现 人员(包括某些中等规模集团官网的专家、CTO)就是在这里栽第一个跟头的。就现实来说,G71有136*3=408种商品(408个SKU),怎么算来的?如下:

如果卖北京西始发的,有16种卖法(因为下面有16个站),北京西到:保定、石家庄、郑州、武汉、长沙、广州、虎门、深圳。。。。都是一个独立的商品,更容易,石家庄上车的,有15种下车的可能,以此类推,单如该下车的站来计算,有136种票:16+15+14....+2+1=136。每种票都有3种座位,一共是408个商品。

为了方便下面的聊下,吾们先明确一下票是什么?

一张票的主要信息包括:出发时间、出发地、鹄的地、车次、座位号。持有票的人就拥有了一个凭证,该凭证表示持有它的人可以坐某个车次的某个座位号,从某地到某地。以是,一张票,对用户来说是一个凭证,对铁道部来说是一个承诺;那对系统来说是什么呢?不知道。这就是吾们要归纳业务,区域建模的原因,吾们再继续思考吧。

明白了票的主要信息下,吾们再看看G71这个车次的高铁,可以卖多少张票?

聊下上先说明一下,一辆火车的物理座位数(站票也可以看成是一种座位,因为站票也有数量配额)不等于可用的较小配合。一切的物理座位非能够都通过12306网站来销售,而是只会销售一部分,譬喻40百分比。其余的灰子 腔嵬ü呦碌姆绞较邸3艘酝 ,可能有些站点上车的人会比较多,有些比较少,以是吾们还会给不同的区间配置不同的限额。譬喻D31北京南至上海共有765张,北京南有260张,杨柳青有80张,泰安有76张。如果杨柳青的80张票售完就会显示无票,就当它如此其他站有票也会显示无票的。每个车次肯定会有各种座位的配额和限额的配置的,这种配置我目上无法预料,但我已经把这些规则都封装近车次聚合根里了,一切的配置策略都是基于座位类型、站点、区间配置的。About票的配置抽象出来,我觉得主要有两种:1)某个区段最多允许出多少张;2)某个区段最少允许出多少张。当用户订票时,把用户指定的区段和这两种配置条件进行比较,两个条件都满足,则可以出票。不满足,则认为无票了。下面举个例子:

ABCDEFG,这是一切站点。座位总配额是100,假设B站点上车,E站下车的人比较少,那吾们就可以设定BE这个区段最多只能出10张票。以是,只要是用户的订票是在这个区段内的,就最多出10张。再譬喻,一列车次,总共100个座位配额,瞩望全程票最少满足80张,那吾们只要给AG这个区段设定最少80张。那尽数订票请求,如果是子区间的,就不能超过100-80,即20张。这两种条件必须再是满足,才允许出票。

但是,不管如何做配额和限额,吾们总是针对某个车次进行配置,这些配置只是车次内部售票时的一些额外的判断条件(业务规则),不影响车次模型的主要地位和对外暴露的功能。以是,为了本文聊下的清楚起见,我下续的聊下都不涉及配额和限额的小case,而是认为尽数区段都可以享受火车较小的物理座位数。

并且,为了聊下小case方便,吾们减少一些站点来聊下。假设某个车次有A,B,C,D四个站点。那001这个人购买了A,B这个区间,系统会分配给001一个座位x;但是因为001坐到B站点下会下车,以是相当于x这个座位又空出来了,也就是说,从B站点开始,系统又可以认为x这个座位是可用的。以是,吾们得出结论:同一个座位,莫过于可以再是出售AB,BC这两张票。通过这个easy的归纳,吾们知道,一列火车虽然只有有限的座位数,譬喻1000个座位。但可以卖出的票远远不止1000个。灰子 且訟,B,C,D四个站点为例,假如火车总共有1000个座位,那AB可以卖1000张,BC也可以卖1000张,同样,CD也可以卖1000张。也就是说,理论上最多可以卖出3000张票。但是如果换一种卖法,一切人都是买ABCD的票,也就是说一切的票都是经过一切站点的,那就是最多只能卖出1000张票了。而实际的场景,一定是介于1000到3000之间。然下实际的G71这个车次,有17个站,那到底可以卖出多少个票,众家应该可以算了吧。理论上这17个站中的任意两个站点之间所形成的线段,都可以出售为一张票。我数学不好,算不太清楚,麻烦有数学好的人帮我算算,呵呵。

通过上面的归纳,吾们知道一张票的本质是某个车次的某一段区间(一条线段),这个区间包含了若干个站点。然下吾们还发现,只要区间不重叠,那座位就不会发生竞争,可以被回收利用,也就是说,可以再是预先出售。

其余,经过更深入的归纳,吾们还发现区间有4种关系:1)不重叠;2)部分重叠;3)完全重叠;4)覆盖;不重叠的环境吾们已经聊下过了,而覆盖也是重叠的一种。以是吾们发现如果重叠,譬喻有两个区间发生重叠,那重叠部分的区间(可能夸一个或多个站点)是在争抢座位的。因为假设一列火车有100个座位,那每个原子区间(两个相邻站点的连线),最多允许重叠99次。

以是,经过上面的归纳,吾们知道了一个车次能够出售一张车票的主要业务规则是什么?就是:这张车票所包含的每个原子区间的重叠次数加1都不能超过车次的总座位数,就现实来说重叠次数+1也可以理解为线段的厚度。

模型策划

上面我归纳了一下票的本质是什么。那接下来吾们再来看看怎么策划模型,来快速实现购票的需求,细节是怎么策划商品聚合以及减库存的逻辑。

上卫电商的思路

如果按照普通电商的思路,把票(站点区间)策划为商品(聚合根),然下为票策划库存数量。我个人觉得是很糟糕的。因为一关键这种聚合根非常多(上面的G71就有408个);另一关键,即便枚举出来了,一次购票也一定会影响非常多其他聚合根的库存数量(只要被部分或全部重叠的区间都受影响)。这样的一次订单处理的复杂度是难以评估的。而且这么多聚合根的更新要在一个事务里,这不是为难数据库吗?而且,这种策划必然带来大量的事务的并发冲突,很可能导致数据库死锁。总之,我认为这种是典型的由于区域模型的策划错误,导致并发冲突高、数据持久化落地困难。或者如果要解决并发小case,只能排队单线程处理,但是仍然解决不了要在一个事务里修改大量聚合根的尴尬局面。听说12306是采取应用了Pivotal Gemfire这种高大上的内存数据库,我对这个不太了解。我不可想象要是不使用内存数据库,他们要怎么实现车次内的票之间的数据强一致性(就是保证一切出售的票都是符合上面聊下的业务规则的)?以是,这种策划,我个人认为是思维定势了,把火车票看成是普通电商的商品来看待。以是,吾们有时做策划又要依赖于经验,又要不能被以往经验所束缚,真的不匆子 侄茫丶易于 且凑站咛宓囊滴癯【岸喽嗌钊牍槟桑×抗槟沙橄蟪鲂ase的本质出来,这样才能对症下药。那是否有其他的策划思路呢?

我的思路

聚合策划

通过上面的归纳吾们知道,莫过于尽数一次购票都是针对某个车次的,我认为车次是负责处理订票的聚合根。吾们看看一个车次包含了哪些信息?一个车次包括了:1)车次名称,如G71;2)座位数,实际座位数会分类型,譬喻牛逼商用座20个,一等座200个;二等座500个;吾们这里为了简化小case,可以暂时忽略类型,我认为这个类型不影响主要的模型的策划决策。需要格外care的是:这里的座位数不要理解为真实的物理座位数,很有可能比真实的座位数要少。因为吾们非能够把一个车次的一切座位都在网上通过12306来出售,而是只出售一部分,具体出售多少,要由work人员人员指定。3)经过的站点信息(包括站点的ID、站点名称等),care:车次还会记录这些站点之间的顺序关系;4)出发时间;看过GRASP九大模式中的信息专家模式的同学应该知道,将职责分配给拥有执行该职责所需信息的类。吾们这个场景,车次具有一次出票的一切信息,以是吾们应该把出票的职责交给车次。其余学过DDD的同学应该知道,聚合策划有一个原则,就是:聚合内强一致性,聚合之间最终一致性。经过上面的归纳,吾们知道要产生一张票,莫过于要影响很多和这个票对应的线段相交的其他票的可用数量。因为一切的站点信息都在车次聚合内部,以是车次聚合内部自然可以维护一切的原子区间,以及每个原子区间的可用票数(相当易于 库存数)。当一个原子区间的可用票数为0的时候,意味着火车针对这个区间的票已经卖完了。以是,吾们完全可以让车次这个聚合根来保证出票时对一切原子区间的可用票数的更新的强一致性。对于车次聚合根来说,这很easy,因为只是几次easy的内存操作而已,耗时可以忽略。一列火车假如有ABCD四个站点,那原子区间就是3个。对于G71,则是16个。

怎么判断是否能出票

基于上面的聚合策划,出票时扣减库存的逻辑是:

按照订单信息,拿到出发地和鹄的地,然下获取这段区间里的一切的原子区间。然下尝试将每个原子区间的可用票数减1,如果一切的原子区间都够减,则购票成功;否则购票失败,提示用户该票已经卖完了。是不是很easy呢?知道了出票的逻辑,那退票的逻辑也就很easy了,就噬涎这个票的一切原子区间的可用票数加1就OK了。如果吾们从线段的厚度的角度去揣摩,那出票时,每个原子区间的厚度就是+1,退票时就是减一。就是相悖的操作,但本质是一样的。

以是,通过这样的思路,吾们将一次订票的处理把握在了一个聚合根里,用聚合根内的强一致性的特性保证了订票处理的强一致性,再是也保证了性能,免去了并发冲突的可能性。上卫电商那种把票单做类似商品的主要聚合根的策划,我当时第一眼看到就觉得不妥。因为这违背了DDD强调的强一致性应该由聚合根来保证、聚合根之间的最终一致性通过Saga来保证的原则。

还有一个很要紧的概念我想说一下我的看法,就是座位和区间的关系。因为有些朋友和我讲,揣摩座位号的小case,虽然都能减1,座位号也必须是同一个。我觉得座位是全局共享的,和区段无关(也许我的理解完全有误,请众家指正)。座位是一个物理概念,一个用户成功购买了一张票下,座位就会少一个,一张票唯一对应一个座位,但是一个座位有可能会对应多张票;而区间是一个逻辑上的概念,区间的感化有两个:1)表示票的出发地和鹄的地;2)记录票的可用数额。如果区间能连通(即该区间内的每个原子区间的可用数额都大于0),则表示允许拥有一个座位。以是,我觉得座位和票(区间)是两个维度的概念。

如何为票分配座位

我觉得车次聚合根内部应该维护一切该车次已经售出的票,已经出售的票的的本质是区间和座位的对应关系。系统处理订票时,用户提交过来的是一段区间。以是,系统应该做两个事情:

  1. 先按照区间去判断是否有可用的座位;
  2. 如果有可用座位,则再通过算法去决定一个可用的座位;

当得到一个可用座位下,就可以生成一张票了,然下保存这个票到车次聚合根内部即可。下面举个例子:

假设Now的环境是座位有3个,站点有4个
座位:1,2,3
站点:abcd

票的卖法1:
票1:ab,1
票2:bc,2
票3:cd,3
票4:ac,3
票5:bd,1
这种选座位的方式应该比较高效,因为总是优先从座位池里去拿座位,只有在万不得已的时候才会去回收可重复利用的票。
上面的4,5两个票,就是揣摩回收利用的结果。

票的卖法2:
票1:ab,1
票2:bc,1
票3:cd,1
票4:ac,2
票5:bd,3
这种选座位的方式应该相对低效,因为总是优先会去扫描是否有可回收的座位,而扫描相对直接从座位池里去拿票总是底相对要高的。
上面的2,3两个票,就是揣摩回收利用的结果。

但是,优先从座位池里拿票的算法有缺陷,就是会出现虽然第一步判断认为有可用的座位,但是这个座位可能不是全程都是同一个座位。举例:
假设Now的环境是座位有3个,站点有4个
座位:1,2,3
站点:abcd

票的卖法3:
票1:ab,1
票2:bc,2
票3:cd,3

Now如果有人要买ad的票,那可用的座位有2,或者3。但是无论是2灰子 3,都要这个乘客中途换车位。譬喻卖给他座位2,那他ab是坐的座位2,但是bc的时候要坐座位1的。否则拿票2的那个人上车时,发现座位2已经有人了。而通过优先回收利用的算法,是没这个小case的。

以是,从上面的归纳吾们也知道选座位的算法该怎么写了,就是采取应用优先回收利用座位的算法。我认为不管吾们这里怎么策划算法,都不影响大局,因为这一切都只发生在车次聚合根内部,这就是预先策划好聚合根,明确出票职责在哪个对象上的好处。

模型归纳总结

  1. 我认为票不是主要聚合根,票只是一次出票的结果,一个凭证而已。
  2. 12306真正的主要聚合根应该是车次,车次具有出票的职责,一次出票具体做的事情有:
    • 判断是否可出票;
    • 决定可用的座位;
    • 更新一次出票时一切原子区间的可用票数,用于判断下次是否能出票;
    • 维护一切已售出的票,用于为决定可用座位供给按照;

通过这样的模型策划,吾们可以确保一次出票处理只会在一个车次聚合根内进行。这样的好处是:

  1. 不需要依赖数据库事务就能实现数据修改的强一致性,因为一切修改只在一个聚合根内发生;
  2. 在保证数据强一致性的再是还能供给很高的并发处理能力,具体策划见下面的架构策划;

架构策划(非本文细节,没兴趣的朋友可以略过)

我觉得12306这样的业务场景,非常适合使用CQRS架构;因为起首它是一个查多写少、但是写的业务逻辑非常复杂的系统。以是,非常适合做架构层面的读写分离,即采取应用CQRS架构。而且应该使用数据存储也分离的CQRS。这样CQ两端才可以完全不需要顾及对方的小case,各自优化自己的小case即可。吾们可以在C端使用DDD区域模型的思路,用良好策划的区域模型实现复杂的业务规则和业务逻辑。而Q端则使用分布式缓存Plan,实现可伸缩的查问能力。

订票的实现思路

再是借助像ENode这样的框架,吾们可以实现in-memory + Event Sourcing的架构。Event Sourcing 技术实现 ,可以让区域模型的一切状态修改的持久化统一起来,本来要用ORM的方式保存聚合根最新状态的,Now只需要easy的通用的方式保存一个事件即可(一次订票只涉及一个车次聚合根的修改,修改只产生一个事件,只需要持久化一个事件(一个JSON串)即可,保证了高性能,无须依赖事务,而且通过ENode可以解决并发小case)。吾们只要保存了聚合根每次改动的事件(事件的结构怎么策划,本文不做多的介绍了,众家可以思考下),就相当于保存了聚合根的最新状态。而正是由于Event Sourcing 技术实现 的引入,让吾们的模型可以一直存活在内存中,即可以使用in-memory 技术实现 。不要小看in-memory 技术实现 ,in-memory 技术实现 在某些关键对提高命令的处理性能非常有扶掖。譬喻就以吾们车次聚合根处理出票的逻辑,假设某个车次有大量的命令发送到分布式消息队列,然下有一台机器订阅了这个队列的消息,然下这台机器处理这个车次的订票命令时,由于这个车次聚合根一直在内存,易于 这般省去了每次要去数据库取出聚合根的步骤,相当于少了一次数据库IO。这样的好处是,因为一个车次能够真正出售的票是有限的,因为座位就so几个,譬喻就1000个座位,估计一般正常环境也就出个2000个左右的票吧(具体能出多少张票要取决于区间的相交程度,上面归纳过)。也就是说,这个聚合根只会产生2000个事件,也就是说只会有2000个订票命令的处理是会产生事件,并持久化事件;而其余的大量命令,因为车次在内存计算下发现没有余票了,就不会做尽数修改,也不会产生区域事件,这样就可以直接处理下一个订票命令了。这样就可以大大提高处理订票命令的性能。

其余一个小case我觉得还需要提一下,因为用户订票成功下,还需要会帐。但用户有可能不去会帐或者没有在规定的时间内完成会帐。那这种环境下,系统会自动释放该用户之上订购的票。以是基于这样的需求,吾们在业务上需要支持业务级其余2pc。即先预扣库存,也就是先占住这张票一定时间(譬喻15分钟),然下会帐成功下再真实给您这张票,系统做真正的库存修改。通过这样的预扣处理,可以保证不会出现超卖的环境。这个思路莫过于和上卫电商譬喻淘宝这样的系统类似,我就不多展开了,我之上写的Conference案例也是这样的思路,众家有兴趣的可以去看一下我之上录制的视频。

查问余票的实现思路

我觉得余票的查问的实现相对easy。虽然对于12306来说,查问的请求占了80百分比,提交订单的请求只占20百分比。但查问由于对数据没有修改,以是吾们完全可以使用分布式缓存来实现。吾们只需要精心策划好缓存的key即可;缓存key的多少要看底,如果一切可能的查问都策划对应的key,彼时间复杂度为1,查问性能自然高;但代价也大,因为key多了。如果想key少一点,那查问的复杂度自然要上去一点。以是缓存策划无非就是容量换时间的思路。然下,缓存的更新无非就是:自动失效、定时更新、主动通知3种。通过CQRS架构,由于CQ两端是事件驱动的,当C端有尽数状态改动,都会产生对应的事件去通知Q端,以是吾们险些可以做到Q端的准实时更新。

再是由于CQ两端的完全解耦,Q端吾们可以策划多种存储,如数据库和缓存(Redis等);数据库用于线下维护关系型数据,缓存用户实时查问。数据库和缓存的更新速度竞相不受影响,因为是并行的。对同一个事件,可以10台机器负责更新缓存,100台机器负责更新数据库。即便数据库的更新很慢,也不会影响缓存的更新实行工过程。这就是CQRS架构的好处,CQ的架构完全不同,且吾们随时可以重建一种新的Q端存储。不知道众家体会到了没有?

About缓存key的策划,我觉得主要从查问余票时传递的信息来揣摩。12306的关键查问是:出发地、鹄的地、出发日期三个信息。我觉得有两种key的策划思路:1)直接策划了该查问条件的key,然下快速拿到车次信息,直接返回;这种方式就是要旨吾们系统已经枚举了一切车次的一切可能出现的票(区间)的缓存key,相信您一定知道这样的key是非常多的。2)不是枚举一切区间,而噬涎每个车次的每个原子区间(相邻的两个站点所连成的直线)的可用票数作为key。这样,key就非常少了,因为车次假如有10000个,然下每个车次平均15个区间,那也就15W个key而已。当吾们要查问时,只需要把用户输入的出发地和鹄的地之间的一切原子区间的可用票数都查出来,然下比较出较大可用票数的那个原子区间。则这个原子区间的可用票数就是用户输入的区间的可用票数了。易于 这般,到这里我提到揣摩出发日期。我认为出发日期是用来决定具体是哪个车次聚合根的。同一个车次,不同的日期,对应的聚合根实例是不同的,即便是同一天,也可能有多个车次聚合根,因为有些车次一天有几班的,譬喻上午9点发车的一班,下午3点发车的一般。以是,吾们也只要把日期也作为缓存key的一部分即可。

总结

本文完全是凭自己对12306这个网站的主要业务的easy思考而得到的一些策划结果。如果真正的DDD区域建模,更好优质的是要和业务一线的work人员、区域专家进行深入沟通,才能更深入的了解该区域内的业务知识,易于 才能策划出更靠谱的区域模型和架构策划。我本人非常惭愧因为没有上12306买过火车票,家离的比较近,就当它如此要买也是家人给我买:)以是,本文所分享的始末难免是纸上谈兵。但我觉得12306这个系统的业务确实比上卫的电商系统要复杂,且并发又这么高。以是,我觉得这个系统真的很值得众家重视模型的策划,而不只是只关注 技术实现 层面的实现。有兴趣的朋友们可夜悛系我。


做网站上的四点倡议

2016年国内SEM区域现状调研报告

吾们的地位

广州 广州市天河区岗顶百脑汇高技术大厦B塔27楼 020-6235 2949

深圳 深圳市南山区汉京万国大厦18A 159 8916 9178

广西 茂名市茂南区油城三路广西创业创新孵化基地B110 159 8916 9178

吾们的效劳

网站及移动应用 牛逼直营网站 APP开发 小程序开发 WeChat运营

系统应用开发 OA/ERP/CRM/HR系统开发 教学管理系统 电商系统 应用型软件系统定制开发

了解吾们

集团官网简介 联系吾们 吾们的案例 讯息报道

使用条款 隐私声明 Cookies

© 2009-2020 老直营威尼斯网址开户 版权一切 广ICP备16051058号

XML 地图 | Sitemap 地图