加入收藏 | 设为首页 | 会员中心 | 我要投稿 应用网_丽江站长网 (http://www.0888zz.com/)- 科技、建站、数据工具、云上网络、机器学习!
当前位置: 首页 > 运营中心 > 产品 > 正文

12306 的核心模型设计思路究竟复杂在哪里

发布时间:2016-03-06 12:25:12 所属栏目:产品 来源:产品100
导读:春节期间,无意中看到一篇文章,文章中讲到 12306 的业务复杂度远远比淘宝天猫这种电商网站要复杂。

根据订单信息,拿到出发地和目的地,然后获取这段区间里的所有的原子区间。然后尝试将每个原子区间的可用票数减 1,如果所有的原子区间都够减,则购票成功;否则购票失败,提示用户该票已经卖完了。是不是很简单呢?知道了出票的逻辑,那退票的逻辑也就很简单了,就是把这个票的所有原子区间的可用票数加 1 就 OK 了。如果我们从线段的厚度的角度去考虑,那出票时,每个原子区间的厚度就是 +1,退票时就是减一。就是相反的操作,但本质是一样的。

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

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

3、如何为票分配座位?

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

先根据区间去判断是否有可用的座位;

如果有可用座位,则再通过算法去选择一个可用的座位;

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

假设现在的情况是座位有 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 两个票,就是考虑回收利用的结果。

但是,优先从座位池里拿票的算法有缺陷,就是会出现虽然第一步判断认为有可用的座位,但是这个座位可能不是全程都是同一个座位。举例:

假设现在的情况是座位有 3 个,站点有 4 个:

座位:1,2,3 站点:abcd 票的卖法 3: 票 1:ab,1 票 2:bc,2 票 3:cd,3

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

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

4、模型分析总结

我认为票不是核心聚合根,票只是一次出票的结果,一个凭证而已。

12306 真正的核心聚合根应该是车次,车次具有出票的职责,一次出票具体做的事情有:

判断是否可出票;

选择可用的座位;

更新一次出票时所有原子区间的可用票数,用于判断下次是否能出票;

维护所有已售出的票,用于为选择可用座位提供依据。

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

不需要依赖数据库事务就能实现数据修改的强一致性,因为所有修改只在一个聚合根内发生;

在保证数据强一致性的同时还能提供很高的并发处理能力,具体设计见下面的架构设计。

4、架构设计

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

1、订票的实现思路

同时借助像 ENode 这样的框架,我们可以实现 in-memory + Event Sourcing 的架构。Event Sourcing 技术,可以让领域模型的所有状态修改的持久化统一起来,本来要用 ORM 的方式保存聚合根最新状态的,现在只需要简单的通用的方式保存一个事件即可(一次订票只涉及一个车次聚合根的修改,修改只产生一个事件,只需要持久化一个事件(一个 JSON 串)即可,保证了高性能,无须依赖事务,而且通过 ENode 可以解决并发问题)。

我们只要保存了聚合根每次变化的事件(事件的结构怎么设计,本文不做多的介绍了,大家可以思考下),就相当于保存了聚合根的最新状态。而正是由于 Event Sourcing 技术的引入,让我们的模型可以一直存活在内存中,即可以使用 in-memory 技术。不要小看 in-memory 技术,in-memory 技术在某些方面对提高命令的处理性能非常有帮助。

比如就以我们车次聚合根处理出票的逻辑,假设某个车次有大量的命令发送到分布式消息队列,然后有一台机器订阅了这个队列的消息,然后这台机器处理这个车次的订票命令时,由于这个车次聚合根一直在内存,所以就省去了每次要去数据库取出聚合根的步骤,相当于少了一次数据库 IO。

(编辑:应用网_丽江站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

热点阅读