所以,通过这样的思路,我们将一次订票的处理控制在了一个聚合根里,用聚合根内的强一致性的特性保证了订票处理的强一致性,同时也保证了性能,免去了并发冲突的可能性。传统电商那种把票单做类似商品的核心聚合根的设计,我当时第一眼看到就觉得不妥。因为这违背了 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 真正的核心聚合根应该是车次,车次具有出票的职责,一次出票具体做的事情有: 判断是否可出票; 选择可用的座位; 更新一次出票时所有原子区间的可用票数,用于判断下次是否能出票; 维护所有已售出的票,用于为选择可用座位提供依据。 通过这样的模型设计,我们可以确保一次出票处理只会在一个车次聚合根内进行。这样的好处是: 不需要依赖数据库事务就能实现数据修改的强一致性,因为所有修改只在一个聚合根内发生; 在保证数据强一致性的同时还能提供很高的并发处理能力,具体设计见下面的架构设计。 5、架构设计 (责任编辑:admin) |