本篇文章9278字,读完约23分钟
编者按:作者唐,网名为网络焦点,2006年毕业于浙江大学,现居杭州。对ddd和cqrs架构感兴趣。目前,我们一直致力于开发和完善enode和equeue。文章首先在微信公众账户infoq (infoq(id:infoqchina)上发布,并经氪36授权转载。
元宵节结束了,一年真的结束了。告别家乡,回到勤劳的城市。作为回报,理性思维也跟随工作状态吗?每年的春节对12306来说都是一个巨大的考验,抛弃盲目服从和偏见。让我们用工程师的思维重新审视它,并从业务分析的角度来讨论它。12306的核心模型设计和架构设计有多复杂?
我为什么要研究这个问题?
春节期间,我偶然看到一篇文章,说12306的业务复杂性远比淘宝天猫复杂。后来,当我想起它时,它是真的。因此,我很想挑战12306系统核心域模型的设计。在一般的电子商务网站中,购买是基于商品的概念,每种商品都有一定的库存量,用户的购买行为是针对商品的。当用户发起购买行为时,系统只需要生成一个订单,并减少用户要购买的商品的库存。然而,12306并不是那么简单。我将在下面进一步分析它。
我写这篇文章的另一个原因是,我发现这是否是因为目前12306的核心域模型设计得不好,导致用户在购票时必须处理复杂的业务逻辑,维护数据一致性的难度上升了数百倍。同时,面对高并发预订,很难支持高tps。我认为业务越复杂,就越应该重视业务分析和领域模型的抽象与设计。如果你不假思索地根据过去的经验行事,你可能会被以前的设计经验先入为主,陷入死胡同。
技术人员往往更注重技术解决方案,例如分析技术问题,如如何群集、如何平衡负载、如何排队、如何划分数据库和表、如何使用锁、如何使用缓存等。,而忽略了最基本的业务思维,如业务分析和领域建模。我认为业务系统越复杂,域模型应该设计得越健壮。如果一个系统的架构是错误的,仍然有补救的空间,因为架构只会沉淀代码,架构是可以调整的(系统本身的架构是不断发展的);然而,如果领域模型是错误的,它将花费很多来补救,因为领域模型沉淀了数据结构和大量的对应数据。对于任何大型系统来说,改变核心领域模型将花费很多。
本文的重点不是如何解决高并发的问题,而是如何从业务角度分析12306的理想模型。目前,互联网上关于12306的文章似乎都是一样的,只是谈论技术,而不是商业分析和如何建模。所以我想写我自己的设计和你交流。
1.需求概述
12306系统的核心问题是在线售票。使用该系统涉及两个角色:用户和铁道部。用户的核心诉求是查询剩余门票并购买门票;铁道部的核心需求是售票。买票和卖票实际上是一个场景,这意味着为用户买票和为铁道部卖票。因此,我们需要设计一个在线网站系统来解决用户的三个核心需求,如查询剩余车票、购买车票和铁道部售票。这三个场景似乎都围绕着火车票。
查询剩余的车票:用户输入三个条件:出发地点、目的地和出发日期,并查询可能的列车。用户可以看到每列火车经过的车站名称和每个座位的剩余票数。
购票:购票分为两个阶段:订票和付款。本文主要研究预约的模型设计和实现。
事实上,还有很多其他的要求,比如为不同的列车设置销售座位配额,为不同的区段设置不同的配额。但与前两个要求相比,我认为这个要求相对次要。
2.需求分析
事实上,12306也是一个电子商务系统,似乎商品就是门票。因为如果一张票被视为一种商品,那么购买票就类似于购买商品,那么每张票都有库存,而商品也有库存的概念。但是如果我们仔细想想,我们会发现12306要复杂得多,因为我们不能提前确认所有的票。如果我们必须证实它,我们只能通过穷举方法。
让我们以从北京西到深圳北的g71列车为例(在这里,我们只考虑向南的方向,而不考虑从深圳北到北京西的列车,这是另一列称为g72的列车)。它有17个车站(北京西站是01站,深圳北站是17站)和3个座位(商务,一等,二等)。从表面上看,这不是三种商品吗?g71商务区、g71一级区和G71二级区。大多数容易喷12306的技术人员(包括一些中型公司的专家和首席技术官)是第一个在这里碰壁的。实际上,g71有136*3=408种商品(408个Sku)。它是如何计算的?如下所示:
如果你从北京西站销售,有16种销售方式(因为后面有16个站),北京西站去保定、石家庄、郑州、武汉、长沙、广州、虎门和深圳。。。。它们都是独立的商品。同理,石家庄有15种下车方式,类推,有136种车票:16+15+14...+2+1 = 136。每张票有三种座位,总共有408个项目。
为了方便下面的讨论,让我们弄清楚什么是票。
车票的核心信息包括:出发时间、出发地点、目的地、车次和座位号。车票的持有者有一个证书,表明持有者可以乘坐某一列火车的某个座位号,从某一个地方旅行到某一个地方。因此,车票是用户的凭证,是铁道部的承诺;这对于系统来说是什么?我不知道。这就是为什么我们需要分析业务和建模领域。让我们继续思考。
在了解了车票的核心信息后,让我们来看看g71高铁能卖出多少张车票。
在讨论之前,应该说明一列火车的实际座位数(站票也可以看作是一种座位,因为站票也有配额)不等于最大可用协调。不能通过12306网站出售所有实体座椅,但只能出售一部分,如40%。其余的仍将在网下出售。不仅如此,在一些车站可能会有更多的人上车,而人会更少,所以我们会给不同的区域分配不同的限制。
例如,从北京南部到上海有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张。
以甲、乙、丙、丁四站为例。如果火车总共有1000个座位,ab可以卖1000张票,bc可以卖1000张票,cd也可以卖1000张票。换句话说,理论上,最多可以卖出3000张票。然而,如果我们改变销售方式,每个人都会购买abcd门票,这意味着所有的门票将通过所有的网站,这意味着我们最多只能销售1000张门票。实际场景必须在1000到3000之间。那么,实际的g71列车有17站,那么能卖出多少票?你应该忘记它。理论上,在17个车站中的任意两个之间形成的线段可以作为一张票出售。我不擅长数学,所以我对它了解不多。请找一个擅长数学的人帮我做,呵呵。
通过以上分析,我们知道一张票的本质是某一车次的某一段(一个线段),它包含几个车站。然后我们还发现,只要间隔不重叠,座位将不会竞争,并可以回收,即他们可以提前出售在同一时间。
此外,经过更深入的分析,我们还发现区间之间有四种关系:
不要重叠;
部分重叠;
完全重叠;
覆盖。
我们已经讨论了非重叠的情况,而覆盖也是一种重叠。因此,我们发现,如果有重叠,例如,两个部分重叠,那么重叠的部分(可能拥有一个或多个站)竞争席位。因为假设一列火车有100个座位,每个原子间隔(两个相邻车站之间的连接)最多允许重叠99次。
因此,经过以上分析,我们知道火车可以售票的核心商业规则是什么?也就是说,包含在该票中的每个原子间隔加上1的重叠时间不能超过列车中的座位总数。事实上,重叠时间加1也可以理解为线段的厚度。
3.模型设计
我分析了上面的票的本质。然后,让我们看看如何设计模型来快速实现购票需求,重点是如何设计商品聚合和库存减少的逻辑。
传统电子商务的理念
如果根据普通电子商务的思想,票(站点间隔)被设计为商品(集合根),那么库存数量是为票设计的。就我个人而言,我认为这很糟糕。因为一方面,有许多这样的聚合根(上面有408个g71另一方面,即使被枚举,一次购票肯定会影响许多其他聚集根的库存数量(只要部分或完全重叠的间隔被影响)。这种订单处理的复杂性很难评估。那么多聚合根更新需要在一个事务中完成,这对数据库来说不是很困难吗?此外,这种设计将不可避免地导致大量并发的事务冲突,这可能导致数据库死锁。
总之,我认为这是典型的,因为域模型的设计错误,导致高并发冲突和数据持久性的困难。或者,如果您想解决并发问题,您只能排队等待单线程处理,但您仍然不能解决在一个事务中修改大量聚合根的尴尬情况。
我听说12306使用的是pivotal gemfire,这是一个很高的内存数据库,但我对此知之甚少。我无法想象在不使用内存数据库的情况下,他们如何在火车票之间实现强大的数据一致性(也就是说,确保所有售出的票都符合上面讨论的业务规则)。因此,这种设计,我个人认为是一种心态,把火车票当成普通电子商务的商品。因此,对我们来说,依靠经验进行设计真的不容易,但不要被过去的经验所束缚。关键是要根据具体的业务场景进行更深入的分析,试图分析和提炼问题的实质,从而对症下药。还有其他设计想法吗?
我的思路
1.聚合设计
从上面的分析中,我们知道实际上每一次购票都是针对某一个车次的,我认为车次是负责订票的聚合根。让我们看看火车包含什么信息。列车编号包括:
列车名称,如g71;
座位数,实际座位数将分为多种类型,如20个商务座位和200个头等舱座位;500个二等座位;为了简化问题,我们可以暂时忽略类型。我认为这种类型不会影响核心模型的设计决策。值得注意的是,这里的座位数不应理解为实际座位数,实际座位数可能比实际座位数少。因为我们不能通过12306在网上出售所有的火车座位,只能出售一部分。要出售的座位的具体数量必须由工作人员手动指定。
传递车站信息(包括车站id、车站名称等)),注:列车编号还将记录这些车站之间的顺序关系;
出发时间;在掌握的九种模式中看过信息专家模式的学生应该知道,责任被分配给了拥有履行责任所需信息的班级。
在我们的场景中,火车一次拥有所有的出票信息,所以我们应该把出票的责任交给火车。此外,学习过ddd的学生应该知道聚合设计有一个原则,即聚合内部的强一致性和聚合之间的最终一致性。通过以上分析,我们知道要生成一张票,它实际上会影响与该票对应的线段相交的许多其他票的可用数量。因为所有站点信息都在列车聚合中,所以维护列车聚合中所有原子间隔和每个原子间隔的可用票数(相当于库存数量)是很自然的。当原子区间内的可用票数为0时,表示该区间的火车票已经售完。因此,我们可以使用列车号码的聚合根来确保在出票时在所有原子间隔中可用投票的更新的强一致性。对于列车聚合根,这是非常简单的,因为它只是一些简单的内存操作,时间消耗可以忽略不计。如果一列火车有四个abcd站,原子间隔是三个。对于71国集团,它是16。
2.如何判断是否可以出票?
基于以上聚合设计,出票时扣除存货的逻辑是:
根据订单信息,得到出发地点和目的地,然后得到该区间内的所有原子区间。然后尝试将每个原子间隔中可用的票数减少1。如果所有的原子间隔都减少到足够的程度,则购票成功;否则,购票失败,提示用户票已售完。很简单吗?知道了发行票的逻辑,退票的逻辑就很简单了,也就是说,在这张票的所有原子间隔中,把可用票加1就可以了。如果我们考虑线段的厚度,当出票时每个原子间隔的厚度为+1,退票时为-1。是相反的操作,但本质是一样的。
因此,通过这种思想,我们在一个聚合根中控制一次性预订的处理,并利用聚合根中的强一致性来保证预订处理的强一致性,同时保证性能,避免并发冲突的可能性。传统电子商务的设计,把票作为类似商品的核心聚合根,乍看起来是不合适的。因为这违反了ddd强调强一致性应该由聚合根保证的原则,聚合根之间的最终一致性由saga保证。
我还想谈另一个非常重要的概念,那就是座位和间隔的关系。因为一些朋友告诉我,考虑到座位数,虽然他们都可以减少1,座位数必须是相同的。我认为座位是全球共享的,与区域无关(也许我的理解是完全错误的,请纠正我)。座位是一个物理概念。用户成功购票后,将少一个座位。一张票只对应一个座位,但一个座位可能对应多张票;区间是一个逻辑概念,它有两个功能:1)指示车票的出发地点和目的地;2)记录可用的门票数量。如果间隔可以连接(即间隔中每个原子间隔的可用数量大于0),则意味着允许一个座位。因此,我认为座位和票(间隔)是二维的概念。
3.如何为票分配座位?
我认为列车聚集根应该维护该列车已售出的所有车票。已经售出的票的本质是间隔和座位之间的对应关系。当系统处理预订时,用户提交一个间隔。因此,系统应该做两件事:
首先,根据间隔判断是否有空位;
如果有空座,则通过算法选择一个空座;
当获得可用座位时,可以生成票,然后将票保存在列车聚集根中。这里有一个例子:
假设目前的情况是有三个座位和四个车站:
座位:1,2,3
网站:abcd
如何售票1:
投票1: AB,1
投票2: BC,2
表决3:裁谈会,3
投票4:空调,3
投票5: BD,1
这种选择座位的方式应该更有效率,因为总是优先考虑从座位池中获得座位,并且只有在绝对必要时才回收可重复使用的票。
以上四五票是考虑回收的结果。
如何售票2:
投票1: AB,1
投票2: BC,1
表决3:裁谈会,1
投票4:空调,2
第5票:BD,3票
这种选择座位的方式应该是相对低效的,因为总是优先考虑扫描是否有可回收的座位,并且直接从座位池中扫描票总是相对昂贵的。
以上2、3票是考虑回收的结果。
然而,首先从座位池中获取票的算法有一个缺陷,即,尽管在第一步中有一个座位可用,但是这个座位可能不是一直都是同一个座位。示例:
假设目前的情况是有三个座位和四个车站:
座位:1,2,3
网站:abcd
如何售票3:
投票1: AB,1
投票2: BC,2
表决3:裁谈会,3
现在,如果有人想买广告票,有两三个座位。但是不管是2号还是3号,乘客都应该换中间的停车位。例如,如果你卖给他座位2,他将坐在ab的座位2,但他将坐在bc的座位1。否则,当拿着票2的人上车时,他发现座位2已经被占用了。但是通过优先回收算法,就没有这样的问题了。
因此,从以上分析中,我们也知道如何编写座位选择算法,即采用优先座位回收算法。我认为无论我们在这里如何设计算法,它都不会影响全局,因为所有这一切都只发生在行程的聚合根,这是提前设计聚合根和明确售票责任对象的好处。
4.模型分析和总结
我不认为票是核心聚合根。这张票只不过是一次出票的结果,只是一张凭证。
12306真正的核心聚合根应该是列车编号。车号负责出票。一次要做的具体事情有:
判断是否可以出票;
选择可用座位;
一次出票时更新所有原子区间的可用票,用于判断下一次是否可以出票;
维护所有售出的门票,为选择可用座位提供依据。
通过这种模型设计,我们可以保证一个出票过程只在一个列车集合根中进行。这样做的好处是:
数据修改的强一致性可以在不依赖数据库事务的情况下实现,因为所有的修改只发生在一个聚合根中;
在确保强大的数据一致性的同时,它还可以提供高并发处理能力。具体设计见以下架构设计。
4.建筑设计
我认为像12306这样的业务场景非常适合使用cqrs架构。因为首先,它是一个查找更多、写更少的系统,但是写的业务逻辑非常复杂。因此,在体系结构层次上进行读写分离是非常合适的,即采用cqrs体系结构。应该使用具有独立数据存储的cqr。这样,cq的两端可以优化他们自己的问题,而不用考虑彼此的问题。我们可以在C端使用ddd域模型的思想,用设计良好的域模型实现复杂的业务规则和业务逻辑。另一方面,Q端使用分布式缓存机制来实现可伸缩的查询能力。
1、预约理念的实现
同时,借助enode这样的框架,我们可以实现内存+事件源的架构。事件源技术可以统一领域模型所有状态修改的持久性。最初,orm用于保存聚合根的最新状态,但是现在只需要以简单和通用的方式保存一个事件(一个预订只涉及一个列车聚合根的修改,并且修改只生成一个事件,只需要保存一个事件(json string ),这确保了高性能,不需要依赖事务,并且可以通过enode解决并发问题)。
只要我们保存聚合根的每个变化的事件(如何设计事件的结构,我们在本文中不会介绍它,所以我们可以考虑一下),这就相当于保存聚合根的最新状态。正是因为事件源技术的引入,我们的模型可以一直保存在内存中,也就是说,可以使用内存技术。不要低估内存技术,它在某些方面对提高命令处理性能非常有帮助。
例如,我们使用列车聚合根的逻辑来处理车票发行,假设某一列车有大量的命令发送到分布式消息队列,然后一台机器订阅该队列的消息。当该机器处理该列车的预订命令时,因为该列车聚合根始终在内存中,所以它省略了每次去数据库取出聚合根的步骤,这相当于少了一个数据库io。
这样做的好处是,一列火车真正能卖出的车票数量是有限的,因为只有几个座位,例如,只有1000个座位,估计在正常情况下大约会有2000张车票(具体能卖出的车票数量取决于路段的交叉程度,这已经在上面分析过了)。换句话说,这个聚合根将仅生成2000个事件,这意味着仅2000个预订订单将生成事件并持续事件;而剩下的大量订单,因为经过记忆计算后发现火车没有剩余的票,不会做任何改动,也不会产生域事件,这样下一个预订订单就可以直接处理了。这可以大大提高处理预订订单的性能。
我认为还有一个问题需要提及,因为用户在成功订票后仍然需要付费。但是,用户可能不付款或未能在指定时间内完成付款。在这种情况下,系统将自动释放用户先前订购的票。因此,基于这一需求,我们需要在业务中支持业务级2pc。也就是说,先暂扣存货,也就是票据被占用一段时间(例如15分钟),付款成功后再给你票据,系统进行真正的存货修改。
通过这种扣压处理,可以保证不会出现超卖。事实上,这个想法类似于传统的电子商务系统,比如淘宝,所以我不会对它做太多的扩展。我之前写的会议案例也是同样的想法。如果你感兴趣,你可以看我之前录制的视频。
2.查询剩余选票的实现思路
我认为剩余票数的查询相对简单。虽然对于12306,查询请求占80%,提交订单请求仅占20%。但是,由于没有对数据的修改,我们可以使用分布式缓存来实现查询。我们只需要仔细设计缓存的密钥;缓存键的数量取决于成本。如果所有可能的查询都设计相应的键,时间复杂度为1,查询性能自然高;但是价格也很高,因为有很多钥匙。如果您想要较少的关键字,查询的复杂性自然会增加。因此,缓存设计无非是改变空.之间的时间然后,缓存更新只不过是自动失效、定期更新和主动通知。通过cqrs架构,由于cq端是事件驱动的,当C端有任何状态变化时,会产生相应的事件通知Q端,所以我们几乎可以准实时更新Q端。
同时,由于cq端的完全解耦,我们可以在Q端设计多种存储,如数据库和缓存(redis)等。);该数据库用于离线维护关系数据,并缓存用户的实时查询。数据库和缓存的更新速度不受彼此影响,因为它们是并行的。对于同一事件,10台机器可以更新缓存,100台机器可以更新数据库。即使数据库更新很慢,也不会影响缓存更新进度。这就是cqrs架构的优势。cq架构完全不同,我们可以随时重建新的Q端存储。我不知道你是否意识到了。
至于缓存键的设计,我认为主要是从查询剩余票数时传递的信息来考虑的。12306的关键查询是:出发地点、目的地和出发日期。我认为有两个关键的设计理念:
直接设计查询条件的关键字,然后快速获取列车信息并直接返回;这种方式要求我们的系统已经列举了所有列车的所有可能的票(区间)缓存键。我相信你一定知道有很多这样的钥匙。
每列火车的每个原子区间(由两个相邻车站组成的直线)的可用票数被视为关键,而不是枚举所有区间。这样,键就很少了,因为如果有10,000列火车,并且每列火车平均有15个间隔,那么它只有15w个键。当我们想要查询时,我们只需要找出用户输入的出发地和目的地之间的所有原子间隔的可用投票,然后将原子间隔与最小的可用投票进行比较。此原子间隔的可用投票是用户输入的间隔的可用投票。当然,这里我提到考虑出发日期。我想出发日期是用来决定哪个车次是聚合的根。对于相同的列车号和不同的日期,相应的聚合根示例是不同的。即使在同一天,火车也可能有多个聚合根,因为有些火车一天有几个班次,比如上午9: 00开始,下午3: 00开始..因此,我们只需要将日期作为缓存密钥的一部分。
摘要
这篇文章是基于我自己对12306网站核心业务的简单思考。如果ddd领域建模是真的,那么有必要与一线员工和领域专家进行沟通,以便更深入地了解该领域的业务知识,并设计出更可靠的领域模型和架构设计。
我很惭愧我没有买12306的火车票,即使我想买,我的家人也给我买了。)因此,本文共享的内容不可避免地是纸上谈兵。但是我觉得12306系统的业务确实比传统的电子商务系统复杂,并发性也很高。因此,我认为这个系统确实值得每个人关注模型的设计,而不仅仅是关注技术实现。
这篇文章由读者提交,并不代表36英寸的立场。如有转载,请注明出处
“读完这篇文章还不够吗?如果你也开始创业,希望你的项目被报道,请点击这里告诉我们!”
标题:不就是一个订票网站吗 12306 的核心模型设计思路究竟复杂在哪里?
地址:http://www.j4f2.com/ydbxw/8298.html