在元宵节结束时,这一年真的结束了。在对勤劳的城市说再见之后,理性思考是否与工作状态一起回归?每年的春节都是对12306的重大考验,抛出盲目的服从和偏见,让我们重新审视工程师的思维,从业务分析的角度来看,12306的核心模型设计理念和架构设计在哪里复杂?/p>
为什么我要研究这个问题?
在春节期间,我偶然看到一篇文章,其中12306的业务复杂性远比淘宝天猫复杂得多。我想到以后,这是真的。因此,我想挑战12306系统核心域模型的设计。一般的电子商务网站,购买是基于商品的概念,每个产品都有一定的库存量,用户的购买行为是针对商品的。当用户发起购买时,系统仅需要生成订单并减少用户想要购买的商品的库存。但是,12306并不是那么简单,具体的复杂性是我将在下面进一步分析的地方。
我写这篇文章的另一个原因是我发现可能因为当前12306核心域模型设计不够好,用户购票时要处理的业务逻辑非常复杂,维护数据一致性的难度是也很难。增加了一百倍,并且在高并发预订面前很难支持非常高的TPS。我认为业务越复杂,关注业务分析和领域模型的抽象和设计就越重要。如果你没有考虑它并根据过去的经验采取行动,你可能会专注于以前的设计经验而陷入死胡同。
技术人员倾向于更多地关注技术级解决方案,例如分析如何集群,如何进行负载平衡,如何排队,如何使用子表,如何使用锁,如何使用缓存和其他技术问题,同时无视最基本的业务。在这个层面思考,例如分析业务和领域建模。我认为越复杂的业务系统,我就越需要设计一个健壮的域模型。如果系统的体系结构设计错误,仍然存在修复的空间,因为体系结构最终只能沉淀代码,体系结构可以调整(系统本身的体系结构在不断发展);如果域模型设计错误,则修复成本非常大,因为域模型会导致数据结构及其相应的大量数据。对于任何大型系统,更改核心域模型的成本非常高。
本文的重点不是如何解决高并发性问题,而是如何从业务角度分析12306的理想模型应该是什么。目前正在谈论12306的互联网上的文章似乎是关于技术的一刀切,而不是关于业务分析以及如何对其进行建模。所以我想写一下我自己的设计,并与大家交流学习。
1、需求概述
12306该系统的核心问题是在线门票销售。该系统涉及两个角色:用户,铁道部。用户的核心诉求是检查剩余的选票和购买门票;铁道部的核心诉求是出售门票。买票和卖票实际上就是一个场景。对于用户来说,这是购票,而对于铁道部则是一张票。因此,我们需要设计一个在线网站系统来解决用户查询,购票和铁道部门票销售的三个核心需求。似乎这三个场景都在火车票周围。
查询剩余的投票:用户输入出发地点,目的地和出发日期的三个条件,以查询可能的列车数量。用户可以看到每次旅行经过的车站的名称以及每个座位的剩余车票数量。
购票:购票分为两个阶段:预订和付款。本文重点介绍预订的模型设计和实施思路。
事实上,还有许多其他需求,例如为不同的列车设置座位配额以及为不同的部分设置不同的限制。但与前两个要求相比,我认为这种需求相对较小。
2、需求分析
事实上,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%。其余的仍将离线销售。不仅如此,网站上可能有更多人,有些人更少,因此我们还将为不同区域配置不同的限制。
例如,D31北京有765件从南到上海,260件在北京,80件在杨柳青,76件在泰安。如果杨柳青的80张门票售罄,就没有门票,即使有其他车站的门票,也没有票。每次旅行肯定会有各种座位的配额和配额。我目前无法预测此配置,但我已将这些规则封装在近车聚合中。所有配置策略均基于座位类型。站点,间隔配置。关于票证配置的抽象,我认为有三种主要类型:
一节中允许多少张纸;
某个部分允许多少张纸;
网站上的最大汽车数量。
当用户下票时,将用户指定的部分与三个配置条件进行比较,如果满足所有三个条件,则可以发出票证。如果你不满意,你会认为没有票。这是一个例子:
ABCDEFG,这是所有网站。总座位额度为100.如果B站上车并且在E站下车的人数较少,那么我们可以将BE设置为最多10张票。因此,只要用户的预订在此部分,最多可以有10张门票。例如,对于火车,总共100个座位配额,并希望总票数至少为80,那么我们只需要为AG部分设置最低80。任何预订请求,如果是一个子间隔,不能超过100-80,即20张。必须同时满足这两个条件以允许票务。
但是,无论配额和配额如何制定,我们总是配置一定数量的列车。当列车在内部销售时,这些配置只是一些额外的判断条件(业务规则),并且不影响列车模型的核心位置和暴露于外部的功能。因此,为了清楚本文中的讨论,我的后续讨论没有涉及配额和配额问题,而是任何部分都可以享受列车的最大实际座位数。
并且,为了便于讨论该问题,我们减少了一些网站进行讨论。假设一列火车有四个站A,B,C和D.那001这个人买了A,B部分,系统将分配001个座位x;但是因为001坐在B站点后会下车,相当于x座位是空的,也就是说,从B站点开始,系统可以假设x座位可用。因此,我们得出结论,同一座位实际上可以出售AB和BC票。通过这个简单的分析,我们知道列车的座位数量有限,例如1000个座位。但是可以卖出的门票超过1000张。
例如,使用四个站点A,B,C和D.如果火车总共有1000个座位,那么AB可以卖出1000个,而BC可以卖出1000个。同样,CD也可以卖出1,000。换句话说,理论上可以出售多达3000张门票。但是如果你改变销售方式,每个人都会购买ABCD的门票,这意味着所有门票都会通过所有网站,也就是说,他们只能卖出1000张门票。实际场景必须在1000到3000之间。然后实际的G71这列火车有17个站。到底能售出多少张票?每个人都应该能够数数。理论上,17个站中任意两个站之间形成的线段可以作为一张票出售。我的数学不好,不是很清楚,有些人有很好的数学帮我计算,呵呵。
通过上述分析,我们知道票证的本质是某个列车的某个间隔(一个线段),其中包含几个车站。然后我们还发现,只要间隔不重叠,座椅就不会竞争并且可以回收,也就是说,它们可以同时预售。
此外,经过更深入的分析,我们还发现区间内有4种关系:
不要重叠;
部分重叠;
完全重叠;
覆盖。
我们已经讨论了非重叠情况,覆盖范围也是重叠的。因此我们发现,如果存在重叠,例如,有两个重叠的区间,则重叠(可能拥有一个或多个站点)正在争夺席位。由于假设列车有100个座位,每个原子间隔(两个相邻站的连接)允许重叠多达99次。
那么,经过上述分析,我们知道火车的核心业务规则可以卖票吗?即:此票证中包含的每个原子间隔的重叠数加1不能超过列车中的总座位数。实际上,重叠数+1也可以理解为线段的厚度。
3、模型设计
上面我分析了门票的性质。然后让我们来看看如何设计模型以快速实现购票需求。关键是如何设计产品聚合和库存减少的逻辑。
传统电商的思路
如果根据一般电子商务理念将票证(站点间隔)设计为商品(聚合根),则为票证设计库存数量。我个人认为这很糟糕。因为一方面,有很多聚合(上面有408个G71);另一方面,即使枚举,购买肯定会影响许多其他聚合的库存(只要它们是部分或完全重叠的间隔受到影响)。这种订单处理的复杂性很难评估。聚合根的许多更新都在一个事务中,这不是一个困难的数据库吗?而且,这种设计必然会导致大量并发的事务冲突,这可能导致数据库死锁。
简而言之,我认为这是典型的,因为域模型的设计错误,导致高并发冲突和困难的数据持久性。或者,如果要解决并发问题,则只能排队进行单线程处理,但仍无法解决在一个事务中修改大量聚合根的尴尬局面。
我听说12306正在使用Pivotal Gemfire的高内存数据库。我对此并不了解。如果你不使用内存数据库(也就是说,确保所有出售的门票都符合上面讨论的业务规则),我无法想象如何在列车车票之间实现强大的数据一致性。所以,这个设计,我个人认为这是一种心态,我认为火车票是普通电子商务的商品。因此,有时我们必须依靠经验来设计,我们不能受过去经验的束缚。这真的不容易。关键是要根据具体的业务场景进行更详细的分析,并尽可能地分析问题的本质。 。还有其他设计理念吗?
我的思路
1、聚合设计
通过以上分析,我们知道实际上,任何购买都是为了一定数量的旅行,我认为火车负责处理预订的总根。让我们看看旅行中包含哪些信息。一趟包括:
火车的名称,如G71;
座位数量,实际座位数量将分为20个商务座位,200个头等舱座位; 500个二等座位;我们可以暂时忽略该类型以简化问题,我认为这种类型不会影响核心模型决策的设计。重要的是要注意,这里的座位数量不应被理解为实际座位的实际数量,并且可能小于实际的座位数量。因为我们不能通过12306在互联网上出售火车的所有座位,而是只出售一部分,以及多少出售,由工作人员手动指定。
通过网站信息(包括网站ID,网站名称等),注意:旅行还会记录这些网站之间的订单关系;
出发时间;在GRASP Nine Models中看过信息专家模型的学生应该知道对具有履行职责所需信息的班级的责任分配。
在我们的场景中,火车上有一张机票的所有信息,所以我们应该把车票的责任交给火车。同时学习DDD的学生应该知道聚合设计中存在一个原则,即聚合内部的一致性和聚合之间的最终一致性。经过上述分析,我们知道要生成票证,实际上它会影响与该票证对应的线段相交的其他票证的数量。因为所有站点信息都在汽车聚合内部,所以保持每个原子区域中所有原子间隔和可用票数(相当于库存数量)是很自然的。当原子间隔的可用票数为0时,表示列车票已售罄。因此,我们可以对列车进行聚合,以确保在票务时所有原子间隔的可用票数的一致性。对于汽车聚合根,这很简单,因为它只是一些简单的内存操作,而且时间可以忽略不计。如果列车中有四个ABCD站,则原子间隔为三。对于G71,有16个。
2、怎么判断是否能出票?
根据上述汇总设计,在票务时扣除库存的逻辑是:
根据订单信息,获取出发地和目的地,然后获取该间隔中的所有原子间隔。然后尝试将每个原子间隔的可用投票数减少1.如果所有原子间隔都足以减少,则购买成功;否则,购买失败,提示用户该票已售罄。这很简单吗?知道了票证的逻辑,退款的逻辑非常简单,就是在票证的所有原子间隔中可用的票数加1即可。如果我们考虑线段的厚度,则在发出票证时每个原子间隔的厚度为+1,并且在票证被退还时为-1。这是相反的操作,但实质是相同的。
因此,通过这种思考,我们在聚合根中控制一个预订的处理,并且聚合根中的强一致性确保了预订过程的强一致性,同时还确保了性能,消除了并发冲突的可能性。传统的电子商务设计使用票证来制作类似产品的核心,我觉得一见钟情是错误的。因为这违反了DDD强调聚合根应保证强一致性的原则,并且Saga保证了聚合根之间的最终一致性。
还有另一个非常重要的概念。我想谈谈我的意见,即座位和间隔之间的关系。因为有些朋友告诉我,考虑到座位号,虽然它们都可以减少,但座位号也必须相同。我认为座位是全球共享的,与该部分无关(也许我的理解完全错误,请纠正我)。座椅是一种物理概念。用户成功购买机票后,将减少一个座位。一张票有一个座位,但一个座位可能对应多张票。间隔是一个逻辑概念。间隔有两个角色:1)票证的来源和目的地; 2)机票的可用金额。如果可以连接间隔(即,间隔中每个原子间隔的可用量大于0),则允许其具有座位。所以,我认为座位和票证(间隔)是两个维度。
3、如何为票分配座位?
我认为聚合根目录内部应该保留此行程中已售出的所有票证。已经售出的门票的本质是间隔和座位之间的对应关系。当系统处理预订时,用户提交间隔。因此,系统应该做两件事:
首先根据间隔确定是否有座位;
如果有座位,则使用算法选择可用座位;
有座位后,您可以生成票证,然后将票证保存到汽车聚合根目录的内部。这是一个例子:
假设目前的情况是3个席位和4个站点:
座位:1,2,3
网站:abcd
售票方法1:
门票1:ab,1
票2:bc,2
票3:cd,3
票4:交流,3
门票5:bd,1
这种选择座位的方式应该更有效率,因为座位总是从座位池中取出,并且只有在绝对必要时才能恢复可重复使用的票。
以上4,5两票是考虑回收的结果。
售票方式2:
门票1:ab,1
票2:bc,1
票3:cd,1
票4:ac,2
门票5:bd,3
这种选择座位的方法应该是相对低效的,因为总是优选扫描可回收座椅,并且相对于直接从座位池取票而言扫描相对昂贵。
以上2,3投票是考虑回收的结果。
然而,优先从座位池取票的算法是有缺陷的,即,尽管第一步判断有可用座位,但座位可能不是一直是同一座位。例如:
假设目前的情况是3个席位和4个站点:
座位:1,2,3
网站:abcd
售票方式3:
门票1:ab,1
票2:bc,2
票3:cd,3
现在,如果有人想购买广告票,可用的座位是2或3.但无论是2还是3,乘客都必须在中途改变停车位。例如,卖给他一个座位2,然后他就是座位2,但是bc是要坐1。否则,乘坐机票2的人上了车,发现座位2已经在那里。并且通过优先回收的算法,没有这样的问题。
因此,从上面的分析,我们也知道如何编写用于选择座位的算法,即,优先回收座位的算法。我认为无论我们如何在这里设计算法,它都不会影响大局,因为它只发生在列车的聚合根中。这是预先设计的聚合根,以及票务责任对象的好处。
4、模型分析总结
我认为票证不是汇总的核心,票证只是票证,凭证的结果。
12306真正的核心聚合根应该是出行次数。火车有责任发票。一张票的具体内容是:
确定是否可以发票;
选择可用座位;
更新票务时所有原子间隔的可用票数,以确定下次是否可以发票;
维护所有售出的门票,为选择可用座位提供依据。
在这个模型的设计,我们可以确保单个售票过程只需要一个聚集基地内进行。好处是:
而不依赖于数据库事务,因为所有的修改仅在一个聚合根发生数据修改的强一致性;
它可以提供高并发处理能力,同时确保强劲的数据一致性。具体设计显示在以下架构设计中。
5、架构设计
我认为像12306这样的业务场景非常适合使用CQRS架构;首先,它是一个有大量写入的系统,但业务逻辑非常复杂。因此,它非常适合于架构级读写分离,即使用CQRS架构。您应该使用也与数据存储分离的CQRS。通过这种方式,CQ的两端可以完全忽略对方的问题,并且每个都可以优化自己的问题。我们可以在C端使用DDD域模型方法,通过精心设计的域模型实现复杂的业务规则和业务逻辑。 Q端使用分布式缓存方案来实现可伸缩查询功能。
1、订票的实现思路
同时,通过像ENode这样的框架,我们可以实现内存+事件源的架构。事件采购技术,可以统一域模型中所有状态变化的持久性。最初,您需要在ORM中保存聚合根的最新状态。现在,您只需要以简单而通用的方式保存事件。修改了列车的聚合根,修改只生成一个事件,只需要保留一个事件(一个JSON字符串),保证高性能,不需要依赖事务,并且可以通过ENode解决并发问题)。
我们只需要保存聚合根的每次更改事件(如何设计事件的结构,本文不多介绍,我们可以考虑一下),它相当于保存聚合根的最新状态。由于Event Sourcing技术的引入,我们的模型可以在内存中存活,也就是说,您可以使用内存技术。不要低估内存技术,内存技术在某些方面对提高命令的处理性能非常有帮助。
例如,我们的汽车聚合根处理票务的逻辑假设将一定数量的命令发送到分布式消息队列,然后一台机器订阅队列的消息,然后机器处理预订订单火车。此时,由于该列的聚合根总是在内存中,因此省略了每次去数据库取出聚合根的步骤,这相当于少了一个数据库IO。
这样做的好处是,因为可以在火车上出售的门票数量有限,因为只有几个座位,例如1000个座位,估计在正常情况下将有大约2,000张门票(如何如上所述,门票取决于交叉的程度。换句话说,这个聚合根只会生成2000个事件,这意味着只有2000个排序命令会生成事件并保持事件;其余的命令将在内存计算后找到。如果没有更多投票,则不会进行任何更改,也不会生成域事件,因此可以直接处理下一个订单。这可以大大提高处理订单的性能。
我认为需要提及的另一个问题是,因为在用户成功预订了机票后,他仍然需要付费。但是,用户可能无法在指定时间内付款或完成付款。在这种情况下,系统将自动释放用户先前订购的票证。因此,基于这种需求,我们需要支持业务中2pc的业务水平。也就是说,首先扣除库存,即,票证被占用一段时间(例如,15分钟),然后票证被成功地提供给票证,并且系统进行真正的库存修改。
通过这种预扣过程,可以保证不会出现超卖情况。这个想法实际上类似于传统电子商务系统,如淘宝。我不会开始太多。我之前写过的会议案例也是这个想法。如果您有兴趣,可以查看我之前录制的视频。
2、查询余票的实现思路
我认为对剩余投票的查询的实现相对简单。虽然对于12306,查询请求占80%,但提交订单的请求仅占20%。但由于查询没有对数据进行修改,我们可以使用分布式缓存来实现它。我们只需要仔细设计缓存的密钥;缓存密钥取决于成本。如果所有可能的查询都使用相应的密钥设计,则时间复杂度为1,查询性能自然高;但成本也很大,因为关键太多了。如果你想要一点关键,那么查询的复杂性自然会有所提升。因此,缓存设计只不过是时间空间的概念。然后,缓存更新只不过是自动故障,计划更新和活动通知。通过CQRS架构,由于CQ的两端都是事件驱动的,当C端有任何状态变化时,会产生相应的事件来通知Q端,因此我们几乎可以实现准实现时间更新Q端。
同时,由于CQ的完全解耦,我们可以在Q侧设计各种存储,如数据库和缓存(Redis等);数据库用于离线维护关系数据,并缓存用户实时查询。数据库和缓存更新速度不受彼此影响,因为它们是并行的。对于同一事件,10台机器可以负责更新缓存,100台机器负责更新数据库。即使数据库更新很慢,也不会影响缓存更新进度。这是CQRS架构的优势,CQ的架构完全不同,我们可以随时重建新的Q侧存储。我不知道是否每个人都意识到了这一点?
关于缓存密钥的设计,我认为主要是从查询剩余投票时传递的信息来考虑。 12306的关键查询是:出发地点,目的地和出发日期。我认为有两个关键的设计理念:
直接设计查询条件的关键,快速获取并直接返回列车信息;此方法要求我们的系统枚举所有列车的所有可能票证(间隔)的缓存键。我相信你必须知道这一点。关键是非常重要的。
不是枚举所有间隔,而是将每个列车的每个原子间隔(连接两个相邻站点的直线)的可用投票数作为关键。这样,钥匙很小,因为有1万列火车,然后每列火车平均有15个间隔,即15W键。当我们想要查询时,我们只需要查找用户输入的起点和目的地之间所有原子间隔的可用投票,然后比较最小可用投票数的原子间隔。然后,该原子间隔可用的投票数是用户输入的间隔的可用投票数。当然,我在这里提到考虑出发日期。我认为出发日期用于决定哪个公交车号码是特定的公交车号码。相同的列车,不同的日期,相应的聚合根实例是不同的,即使在同一天,也可能有多个列车聚集根,因为有些列车每天有几班,例如上午9点班,下午3点'时钟一般。因此,我们只需要将日期作为缓存键的一部分。
总结
本文完全基于我从12306网站核心业务的简单思考中获得的一些设计发现。如果真正的DDD域被建模,它将与前线员工和领域专家进行更深入的沟通,以更深入地了解该领域的业务知识,从而设计出更可靠的域模型和架构。设计。
非常尴尬,我没有买12306的火车票,家里比较近,即使我想买家给我买:)因此,本文分享的内容不可避免地在纸面上。但我认为12306系统的业务确实比传统的电子商务系统更复杂,而且并发性如此之高。因此,我认为这个系统确实值得每个人关注模型的设计,而不仅仅是技术层面的实施。