换个思路看12306,其核心模型设计思路到底复杂在哪里?

黑帽SEO 2019-07-15 14:09

按:本文作者汤雪华,网名,年毕业于浙江大学,目前住在杭州对,架构比较感兴趣目前致力于开发和完善节结束,年就真的过完了自己想想,也确实如此,很想挑战一下这个系统的核心领域模型的设计一般的网站,购买都是基于商品的概念,每个商品有一定量的库存,用户的购买行为是针对商品的当用户购买行为时,系统只需要生成订单并对用户要购买的商品减库存即可,就不是那么简单了,具体复杂在哪里,我下面会进一步分析一个让我写这篇文章的原因,是我发现也许是否是因为目前的核心领域模型设计的不够好,导致用户购票时要处理的业务逻辑异常复杂,维护数据一致性的难度也几百倍的上升,同时面对高并发的订票也难以支持很高的我觉得,越是复杂的,就越要重视业务分析,重视领域模型的抽象和设计如果不思索,凭以往经验行事,则很可能会被以往的设计经验先入为主,陷入死胡同技术人员更注重技术层面的解决方案,比如一上来就分析如何集群如何负载均衡如何排队如何分库分表如何用锁,如何用缓存等技术问题,而忽略了最根本的业务层面的思考,如分析业务领域建模我认为越是复杂的系统,则越要设计一个健壮的领域模型的重点不是在如何解决高并发的问题,而是希望从业务角度去分析,的理想模型应该是怎么样的网上谈的文章貌似都是千篇一律的只谈技术,不谈业务分析和如何建模的我想写一下自己的设计和大家交流学习需求概述这个系统,核心要解决的问题是网上售票到个角色使用该系统:用户铁道部的核心诉求是查询余票购票;铁道部的核心诉求是售票和售票其实是一个场景,对用户来说是购票,对铁道部来说是售票,我们要设计一个在线的网站系统,解决用户的查询余票购票,以及铁道部的售票这个核心诉求看起来这个场景都是围绕火车票展开的查询票:用户输入出发地目的地出发日三个条件,查询可能存在的车次,用户可以看到每个车次经过的站点名称,以及每种座位的余票数量:购票分为订票和付款两个阶段,本文重点分析订票的模型设计和实现思路其实还有很多其他的,比如给不同的车次设定销售座位数配额,以及不同的区段设置不同的限额相比前面两个需求来说,我觉得这个需求相对次要一些需求分析确实,也是一个电商系统,而且看起来商品就是票了因为如果把一张票看成是一个商品那购票就类似于购买商品,然后每张票都有库存,商品也有库存的概念如果我们仔细想想,会发现要复杂很多,因为我们无法预先确定好所有的票,如果非要确定,那只能通过穷举法了我们以北京西到深圳北的车次为例(这里只考虑南下的方向,不考虑深圳北到北京西的,那是另外一个车次,叫),它有个站(北京西是号站,深圳北是号站),种座位(商务一等二等)看起来,这不就是个商品吗?商务座一等座二等座大部分喷的技术人员(包括某些中等规模公司的专家)就是在这里栽第一个跟头的上,有种商品(个),怎么算来的?如下:如果卖北京西始发的,有种卖法(因为后面有个站),北京西到:保定石家庄郑州武汉长沙广州虎门深圳都是一个独立的,同理,石家庄上车的,有种下车的可能,以此类推,单以上下车的站来计算,有种票:种票都有种座位,一共是个商品为了方便的讨论,我们先明确一下票是什么?一张票的核心信息包括:出发时间出发地目的地车次座位号票的人就拥有了一个凭证,该凭证表示持有它的人可以坐某个车次的某个座位号,从某地到某地,一张票,对用户来说是一个凭证,对铁道部来说是一个承诺;那对系统来说是什么呢?不知道这就是我们要业务,领域建模的原因,我们再继续思考吧了票的核心信息后,我们再看看这个车次的高铁,可以卖多少张票?讨论前先说明一下,一辆火车的物理座位数(站票也可以看成是一种座位,因为站票也有数量配额)不等于可用的最大配合的物理座位不可能都通过网站来销售,而是只会销售一部分,比如的还是会通过线下的方式销售如此,可能有些站点上车的人会比较多,有些比较少,所以我们还会给不同的区间配置不同的限额北京南至上海共有张,北京南有张,杨柳青有张,泰安有张如果青的张票售完就会显示无票,就算其他站有票也会显示无票的车次肯定会有各种座位的配额和限额的配置的,这种配置我目前无法预料,但我已经把这些规则都封装近车次聚合根里了,所有的配置策略都是基于座位类型站点区间配置的关于的配置抽象出来,我觉得主要有种:某个区段最多允许出多少张;某个区段最少允许出多少张;某个站点上车的最多多少张当用户时,把用户指定的区段和这种配置条件进行比较,个条件都满足,则可以出票不满足,认为无票了下面个例子:,这是所有站点总配额是,假设站点上车,站下车的人比较少,那我们就可以设定这个区段最多只能出张票,只要是用户的订票是在这个区段内的,就最多出张再,一列车次,总共个座位配额,希望全程票最少满足张,那我们只要给这个区段设定最少张那订票请求,如果是子区间的,就不能超过,即张这两种条件必须同时满足才允许出票,不管如何做配额和限额,我们总是针对某个车次进行配置,这些配置只是车次内部售票时的一些额外的判断条件(业务规则),不影响车次模型的核心地位和对外暴露的功能,为了本文讨论的清楚起见,我后续的讨论都不涉及配额和限额的问题,而是认为任何区段都可以享受火车最大的物理座位数,为了讨论问题方便,我们减少一些站点来讨论某个车次有四个站点那这个人了这个区间,系统会分配给一个座位;但是因为坐到站点后会下车,所以相当于这个座位又空出来了,也就是说,从站点开始,系统又可以认为这个座位是可用的,我们得出结论:同一个座位,其实可以同时出售这两张票这个简单的分析,我们知道,一列火车虽然只有有限的座位数,比如个座位可以卖出的票远远不止个以四个站点为例,假如火车总共有个座位,那可以卖张,也可以卖张,同样,也可以卖张就是说,理论上最多可以卖出张票如果换一种卖法,所有人都是买的票,也就是说所有的票都是经过所有站点的,那就是最多只能卖出张票了实际的场景,一定是介于到之间实际的这个车次,有个站,那到底可以卖出多少个票,大家应该可以算了吧上这个站中的任意两个站点之间所形成的线段,都可以出售为一张票我不好,算不太清楚,麻烦有数学好的人帮我算算,呵呵上面的分析,我们知道一张票的本质是某个车次的某一段区间(一条线段),这个区间包含了若干个站点我们还发现,只要区间不重叠,那座位就不会发生竞争,可以被回收利用,也就是说,可以同时预先出售,经过更深入的分析,我们还发现区间有种关系:不重叠;部分重叠;完全重叠;覆盖不的情况我们已经讨论过了,而覆盖也是重叠的一种我们发现如果重叠,比如有两个区间发生重叠,那重叠部分的区间(可能夸一个或多个站点)是在争抢座位的因为一列火车有个座位,那每个原子区间(两个相邻站点的连线),最多允许重叠次,经过上面的分析,我们知道了一个车次能够出售一张车票的核心业务规则是什么?就是:这张车票所包含的每个原子区间的重叠次数加都不能超过车次的总座位数,实际上重叠次数也可以理解为线段的厚度模型设计上面我分析了一下票的本质是什么那接下来我们看看怎么设计模型,来快速实现购票的需求,重点是怎么设计商品聚合以及减库存的逻辑传统电商的如果按照普通电商的思路,把票(站点区间)设计为商品(聚合根),然后为票设计库存数量因为这种聚合根非常多(上面的就有个);另一方面,即便枚举出来了,一次购票也一定会影响非常多其他聚合根的库存数量(只要被部分或全部重叠的区间都受影响)这样的一次处理的复杂度是难以评估的这么多聚合根的更新要在一个事务里,这不是为难数据库吗?而且,这种设计必然带来大量的事务的并发冲突,很可能导致数据库死锁,我认为这种是典型的由于领域模型的设计错误,导致并发冲突高数据持久化落地困难如果要解决并发问题,只能排队单线程处理,但是仍然解决不了要在一个事务里修改大量聚合根的尴尬局面听说是了这种高大上的内存数据库,我对这个不太了解我不可想象要是不内存数据库,他们要怎么实现车次内的票之间的数据强一致性(就是保证所有出售的票都是符合上面讨论的业务规则的)?所以,这种设计,我个人认为是思维定势了,把火车票看成是普通电商的商品来看待,我们有时做设计又要依赖于经验,又要不能被以往经验所束缚,真的不容易,关键还是要根据具体的业务场景多多深入分析,尽量分析抽象出问题的本质出来,这样才能对症下药那是否有其他的设计思路呢我的思路聚合设计通过上面的分析我们知道,其实任何一次购票都是针对某个车次的,我认为车次是负责处理订票的聚合根我们看看一个包含了哪些信息?一个车次包括了:车次名称,如;座位数,实际座位数会分类型,比如商务座个,一等座个;二等座个;我们这里为了简化问题,可以暂时忽略类型,我认为这个类型不影响核心的模型的设计决策格外注意的是:这里的座位数不要理解为真实的物理座位数,很有可能比真实的座位数要少因为我们不可能把一个的所有座位都在网上通过来出售,而是只出售一部分,具体出售多少,要由工作人员人工指定的站点信息(包括站点的站点名称等),注意:车次还会记录这些站点之间的顺序关系;出发时间;看过九大模式中的信息专家模式的同学应该知道,将职责分配给拥有执行该职责所需信息的类我们这个场景车次具有一次出票的所有信息,所以我们应该把出票的职责交给车次学过的同学应该知道,聚合设计有一个原则,就是:聚合内强一致性,聚合之间最终一致性上面的分析,我们知道要产生一张票,其实要影响很多和这个票对应的线段相交的其他票的可用数量因为的站点信息都在车次聚合内部,所以车次聚合内部自然可以维护所有的原子区间,以及每个原子区间的可用票数(相当于是库存数)当一个区间的可用票数为的时候,意味着火车针对这个区间的票已经卖完了,我们完全可以让车次这个聚合根来保证出票时对所有原子区间的可用票数的更新的强一致性对于聚合根来说,这很简单,因为只是几次简单的内存操作而已,耗时可以忽略一列火车有四个站点,那原子区间就是个对于,是个怎么判断是否能出票?基于上面的聚合设计,出票时扣减库存的逻辑是:根据订单信息,拿到出发地和目的地,然后获取这段区间里的所有的原子区间尝试将每个原子区间的可用票数减,如果所有的原子区间都够减,则购票成功;否则购票失败,提示用户该票已经卖完了是不是很简单呢知道了出票的逻辑,那退票的逻辑也就很简单了,就是把这个票的所有原子区间的可用票数加就了如果我们从的厚度的角度去考虑,那出票时,每个原子区间的厚度就是,退票时就是减一就是的操作,但本质是一样的,通过这样的思路,我们将一次订票的处理控制在了一个聚合根里,用聚合根内的强一致性的特性保证了订票处理的强一致性,同时也保证了性能,免去了并发冲突的可能性传统电商把票单做类似商品的核心聚合根的设计,我当时第一眼看到就觉得不妥因为这了强调的强一致性应该由聚合根来保证聚合根之间的最终一致性通过来保证的原则还有一个很重要的概念我想说一下我的看法就是座位和区间的关系因为有些朋友和我讲考虑座位号的问题,虽然都能减,座位号也必须是同一个我觉得是全局共享的,和区段无关(也许我的理解完全有误,请大家指正)是一个物理概念,一个用户成功购买了一张票后,座位就会少一个,一张票唯一对应一个座位,但是一个座位有可能会对应多张票;而区间是一个逻辑上的概念,区间的作用有两个:)表示票的出发地和目的地;)记录票的可用数额如果能连通(即该区间内的每个原子区间的可用数额都大于),则表示允许拥有一个座位,我觉得座位和票(区间)是两个维度的概念如何为票分配座位?我觉得车次聚合根内部应该维护所有该车次已经售出的票,已经出售的票的的本质是区间和座位的对应关系处理订票时,用户提交过来的是一段区间,系统应该做两个事情:先根据区间去判断是否有可用的座位;如果有可用座位,则再通过算法去选择一个可用的座位;当得到一个可用座位后,就可以生成一张票了,然后保存这个票到车次聚合根内部即可下面个例子:假设现在的情况是座位有个,站点有个:座位:站点:票的卖法:票:票:票:票:票:这种选座位的方式应该比较高效,因为总是优先从座位池里去拿座位,只有在万不得已的时候才会去回收可重复利用的票的,两个票,就是考虑回收利用的结果的卖法:票:票:票:票:票:这种选座位的方式应该相对低效,因为总是优先会去扫描是否有可回收的座位,而扫描相对直接从座位池里去拿票总是成本相对要高的的,两个票,就是考虑回收利用的结果,优先从座位池里拿票的算法有缺陷,就是会出现虽然第一步判断认为有可用的座位,但是这个座位可能不是全程都是同一个座位:假设现在的情况是座位有个,站点有个:座位:站点:票的卖法:票:票:票:现在如果有人要买的票,那可用的座位有,或者无论是还是,都要这个乘客中途换车位卖给他座位,那他是坐的座位,但是的时候要坐座位的拿票的那个人上车时,发现座位已经有人了通过优先回收利用的算法,是没这个问题的,从上面的分析我们也知道选座位的算法该怎么写了,就是采用优先回收利用座位的算法我认为我们这里怎么设计算法,都不影响大局,因为这一切都只发生在车次聚合根内部,这就是预先设计好聚合根,明确出票职责在哪个对象上的好处模型分析总结我认为票不是核心聚合根,票只是一次出票的结果,一个凭证而已的核心聚合根应该是车次,车次具有出票的职责,一次出票具体做的事情有:判断是否可出票;选择可用的座位;更新一次出票时所有原子区间的可用票数,用于判断下次是否能出票;维护所有已售出的票,用于为选择可用座位提供依据这样的模型设计,我们可以确保一次出票处理只会在一个车次聚合根内进行这样的好处是不需要依赖数据库事务就能实现数据修改的强一致性,因为所有修改只在一个聚合根内发生;在保证数据强一致性的同时还能提供很高的并发处理能力,具体设计见下面的架构设计架构设计我觉得这样的业务场景,非常适合使用架构;因为首先它是一个查多写少但是写的业务逻辑非常复杂的系统,非常适合做架构层面的读写分离,即采用架构应该使用数据存储也分离的这样才可以完全不需要顾及对方的问题,各自优化自己的问题即可我们可以在使用领域模型的思路,用良好设计的领域模型实现复杂的业务规则和业务逻辑端则使用分布式缓存方案,实现可伸缩的查询能力订票的实现思路同时借助像这样的框架,我们可以实现的架构我们只要保存了根每次变化的事件(事件的结构怎么设计,本文不做多的介绍了,大家可以思考下),就相当于保存了聚合根的最新状态正是由于技术的引入,让我们的模型可以一直存活在内存中,即可以使用技术不要小看,技术在某些方面对提高命令的处理性能非常有帮助就以我们车次聚合根处理出票的逻辑,假设某个车次有大量的命令发送到分布式消息队列,然后有一台机器订阅了这个队列的消息,然后这台机器处理这个车次的订票命令时,由于这个车次聚合根一直在内存,所以就省去了每次要去数据库取出聚合根的步骤,相当于少了一次数据库这样的好处是因为一个车次能够真正出售的票是有限的,因为座位就那么几个,比如就个座位,估计一般正常情况也就出个个左右的票吧(具体能出多少张票要取决于区间的相交程度,上面分析过)就是说,这个聚合根只会产生个事件,也就是说只会有个订票命令的处理是会产生事件,并持久化事件;而其余的大量命令,因为车次在内存计算后发现没有余票了,就不会做任何修改,也不会产生领域事件,这样就可以直接处理下一个订票命令了这样就可以大大提高处理命令的性能一个问题我觉得还需要提一下,因为用户订票成功后,还需要付款用户有可能不去付款或者没有在规定的时间内完成付款那这种情况下系统会自动释放该用户之前订购的票基于这样的需求,我们在业务上需要支持业务级别的先预扣库存,也就是先占住这张票一定时间(比如分钟),然后付款成功后再真实给你这张票,系统做真正的库存修改这样的预扣处理,可以保证不会出现超卖的情况这个其实和传统电商比如淘宝这样的系统类似,我就不多展开了,我之前写的案例也是这样的思路,大家有兴趣的可以去看一下我之前录制的视频查询余票的实现思路我觉得余票的查询的实现相对简单对于来说,查询的请求占了,提交订单的请求只占查询由于对数据没有修改,所以我们完全可以使用分布式缓存来实现我们只需要设计好缓存的即可;缓存的多少要看成本,如果所有可能的查询都设计对应的,那时间复杂度为,查询性能自然高;但代价也大,因为多了如果想少一点那查询的复杂度自然要上去一点缓存设计无非就是空间换时间的思路,缓存的更新无非就是:自动失效定时更新主动通知种架构,由于两端是事件驱动的,当端有任何状态变化,都会产生对应的事件去通知端,所以我们几乎可以做到端的准实时更新由于两端的完全解耦,端我们可以设计多种存储,如数据库和缓存(等);数据库用于线下维护关系型数据,缓存用户实时查询数据库和缓存的更新速度不受影响,因为是并行的对同一个事件可以台机器负责更新缓存,台机器负责更新数据库数据库的更新很慢,也不会影响缓存的更新进度这就是的好处,的架构完全不同,且我们随时可以重建一种新的端存储不知道大家到了没有?关于缓存的设计,我觉得主要从查询余票时传递的信息来考虑关键查询是:出发地目的地出发日期三个信息我觉得有两种的设计思路直接设计了该查询条件的,然后快速拿到车次信息,直接;这种方式就是要求我们系统已经枚举了所有车次的所有可能出现的票(区间)的缓存,相信你一定知道这样的是非常多的不是所有区间,而是把每个车次的每个原子区间(相邻的两个站点所连成的直线)的可用票数作为这样就非常少了,因为车次假如有个,然后每个车次平均个区间,那也就W个而已当我们要时,只需要把用户输入的出发地和目的地之间的所有原子区间的可用票数都查出来,然后比较出最小可用票数的那个原子区间这个原子区间的可用票数就是用户输入的区间的可用票数了,到这里我提到考虑出发日期我认为日期是用来决定具体是哪个车次聚合根的同一个,不同的日期,对应的聚合根实例是不同的,即便是同一天,也可能有多个车次聚合根,因为有些车次一天有几班的,比如上午点发车的一班,下午点发车的一般,我们也只要把日期也作为缓存的一部分即可本文完全是凭自己对这个网站的核心业务的简单思考而得到的一些设计结果如果真正的建模,的是要和业务一线的工作人员领域专家进行深入沟通,才能更深入的了解该领域内的业务知识,从而才能设计出更靠谱的领域模型和架构设计惭愧,我没有上买过火车票,家离的比较近,就算要买也是家人给我买:)所以,本文所分享的内容难免是纸上谈兵我觉得这个系统的业务确实比传统的电商系统要复杂,且并发又这么高,我觉得这个系统真的很值得大家重视模型的设计,而不只是只关注技术层面的实现


上一篇:站长最关心的网站优化问题汇总-基础版
下一篇:【刷百度】无线监控设备网站诊断分析书