本文我会尝试聊一下去年我主R做月租酒店项目时的一些核心设计要点。

酒店项目是我来贝壳后主R的第一个比较完整的从零到一的项目,涵盖了对外交互、房源、签约、交易、结算等诸多领域,项目中有比较多有意思且有挑战的设计,对于当时的我来说,能够全链路承接整个项目也是一大挑战。

本文聚焦的重点是其中的签约、支付、外部下单模块,我统称这些模块为交易模块。

背景

先简单给读者介绍一下项目背景。

贝壳租赁有多种业务模式,其中公寓是B2B2C的三方商家模式,比如自如、自营公寓(海盐)均是公寓模式下的大KA商家。商家通过签约入驻与贝壳达成合作协议,之后便可以使用公寓完善的系统进行管房、管钱、流量商业化等能力。平台对城市、商家也有完善的运营管理能力。

而月租酒店是对公寓模式的一种房源供给以及交易模式的补充。

前期运营同学使用公寓系统已经跑通了业务模式:录房、上架、签约去化,验证了模式的有效性。但是使用原系统录房只有两种途径:

  1. 通过贝壳管房系统手动录
  2. 通过贝壳的OpenApi调接口录

对于酒店方来说,并没有动力针对贝壳这类外部渠道开发对接OpenApi因此想要提升录房效率,需要贝壳侧进行适配对接

像抖音、快手这类渠道,与贝壳类似,都是针对酒店侧提供的接口进行适配对接。

让管家联系酒店下单,效率也很低。因此除了对接房源,订单对接也很有必要。

除此之外,我们也建设了结算对账线上化能力,帮助平台与酒店商家高效分账。

由于我们是直接对接了外部的房源接口,同时订单也是直接调外部接口创建的,从系统交互上来看,我们跟外部做了直连,因此月租酒店项目,也叫做直连酒店。

房源直连,提升录房、发房效率

订单直连,提升下单效率

月租直连酒店整体的运作过程:

系统设计

了解了大致背景,下面看下我们是怎么设计这块系统的。

整体设计

首先简单看下整体的设计。

还是看一下全景架构图:

相信读者看完背景介绍后,能够理解我们要做什么。简单来说我们做了三块建设:

  1. 针对酒店供应商的接口,我们做了适配对接,其中包括房源、订单的对接;
  2. 针对租客侧的签约、下单、支付等签中流程,我们建设了完善的交易能力;
  3. 针对平台与商家的结算,我们也做了系统功能;

其中有些链路沿用了公寓、C端已有的能力,其中商家与贝壳的签约,则沿用了公寓的CRM能力。

在系统的层面看,我们新增了三个服务:

  1. hotel-adator:负责适配外部接口,屏蔽不同供应商的差异;
  2. hotel-house:负责维护酒店房源模型,对外接入adaptor的酒店、房源,对内同步数据到公寓的房源链路;
  3. hotel-zuwu:负责维护酒店交易模型,流转租约、账单等核心状态机,对接内部支付、对账中台; 经过评估,我们的结算业务并没有太复杂,因此我们将结算跟签约交易统一放到了租务项目中,租务是租赁业务中负责租中事务的领域。

到这里,读者能对这块业务、系统有一个整体感知即可。

交易模块设计

到这部分,其实才是我想写这篇文章的重点。

接下来我们来看看,交易整个大的模块内,到底有哪些点是需要我们仔细考虑的。

数据模型设计

设计模型的前提是,要对业务流程、业务动作、业务角色有掌控力。经过前期调研、充分沟通、流程梳理,我们明确了月租酒店的业务流程与迭代的规划。

对于外部的订单交互来说,我们只需要做这几件事:

在内部的订单模型中,我们需要关联外部订单。内部的订单主模型,我们叫做租约。

对于内部的签约、交易、结算以及营销,我们需要实现以下核心能力:

根据以上对业务流程、业务模式、核心动作的讨论、推断,我们最终的模型设计如下:

读到这里,读者对数据模型有大概的认知即可。

分布式一致性设计

上面的模型只是为了方便读者理解业务跟系统,这部分的分布式一致性设计,其实才是真正的重难点

在月租直连酒店项目中,交易与结算模块都需要考虑接口的事务一致性、数据的完整性,由于交易涉及场景更多,本文我们就以交易为例,结算模块中的实践与考量实际大同小异。

简单来说,我们为了保证一致性,引入了分布式事务的方案。

行业内针对这类问题,通用实践的解法一般是可靠消息、最大努力通知、TCC、Seata框架。

在我们的项目中,根据不同场景的诉求,我们主要引入了三类方案,以下是按照一致性保障强度从高到低的排序:

  1. SeataAT 模式
  2. 可靠消息最终一致性
    1. RocketMQ事务消息
    2. 本地消息表
  3. 最大努力通知
    1. MQ普通消息(可能会丢消息)+接口补偿

我们从方案+场景的维度逐个分析。

创建租约(Seata AT模式)

商机管家接到租客的咨询并且拿到租客的下单意向后,会在我们的B端系统创建租约,之后租客便可以签约、支付。

创建租约的时序图如下:

可以看到这一步逻辑很多,同时交互的服务也很多。图中红框标出的是分布式事务的范围。

设计1:

首先我们在做协议的校验之前就调用了外部酒店的验单接口,如果这一步没有库存或者被其他规则拦住,则流程终止,过程中没有数据损失。

设计2:

其次协议的创建、盖章等动作并没有放入事务中,有这些考虑:

  • 首先协议中台的这些接口除了数据写入,同时还有文件操作,Seata的AT模式只支持MySQL的数据写入;
  • 其次协议中台的这一系列接口,支持重入+数据无需回滚,如果前序某个接口调用失败了,我们会提示用户明确的文案,提示用户可以重试,这样数据、流程整体也是完整自洽的;
  • 最后一点是因为,协议中台提供的SLA并不保障多个接口的连续调用性能,如果接入到分布式事务中,可能会导致事务耗时过长;

结合以上几点,我们评估在B端的场景下,协议交互的这几个调用可以不放到事务内。

设计3:

对于酒店B端租约、账单等数据的创建以及锁券,则需要使用分布式事务保障调用的一致性。

我们看下主流分布式事务协议的对比:

结合我们这里的业务场景,要求尽可能保障事务的一致性,要求具备隔离性,同时事务参与的几方均为关系型数据库操作。Seata AT模式对业务代码基本无侵入性,同时公司内有基建团队维护,因此这里我们倾向于使用AT模式来保障一致性。

在创建租约的场景里,Seata服务端就是TC(协调者),我们酒店的B端租务服务就是TM(管理者),而营销的服务就是RM(资源管理器)。一次分布式事务调用的过程大致如下:

  1. TM(酒店B端租务服务)调用TC创建分布式事务的记录,获得生成的XID;
  2. TM(酒店B端租务服务)通过RPC框架调用其他服务接口,同时会携带上一步生成的XID;
  3. RM(营销服务)通过其接收到的XID,将其所管理的资源且被该调用所使用到的资源注册为一个事务分支(Branch Transaction);
  4. 当该请求的调用链全部结束时,TM(酒店B端租务服务)根据本次调用是否有失败的情况,如果所有调用都成功,则决议Commit,如果有超时或者失败,则决议Rollback;
  5. TM将结果同步给TC,TC根据结果协调各RM,进行事务二阶段的提交或回滚;

设计4:

创建租约这一步,由于调用多耗时较长,我们针对前端的交互做了体验上的优化,点击提交按钮后,系统会弹窗提示用户目前进行到了哪一步来缓解用户体验上的焦虑(类似携程上买票的过程)。

对比C端电商更通用的下单场景,其中的设计要点跟倾向性会跟B端很不一样,比如C端需要更多性能、可用性方面的设计。


月租酒店项目中,除了这个场景,在以下场景也使用了AT模式保障了事务的实时一致性:

  • 签约完成:创建支付流水+支付单,其中创建流水、更新账单为本地数据库操作,创建支付单则需要调用支付中台;
  • 外部下单成功:更新租约状态、同步核销券;

签约、支付超时关单(最大努力通知)

租约在创建后以及租客在支付后,服务端均需要通过RocketMQ的延迟消息来完成超时关闭的功能。我们的实现方案中,在对应的节点,会发送一条RocketMQ延迟消息,在消费逻辑中判断是否满足超时的条件,从而触发关闭逻辑。

这里发送消息的时候,我们一般不会将发消息与本地事务放在一起,因此,消息发送无法保证事务的原子性,也就是说,消息可能会丢

这里我们评估了场景对消息的丢失容忍度,是可以接受丢消息的,这就是最大努力通知的一种应用场景。

当然,为了提高业务的可用性、数据完整性,我们会在用户查询租约、账单的时候,做一个被动超时关单的操作,算是对最大努力通知的一种补偿


支付成功(本地消息表)

租客在支付完成之后,就进入了外部下单的流程,这一步我们主要是更新本地的账单状态,更新流水实付信息,同时触发外部下单的调用。

支付成功的时序图如下:

这里我们主要有两个设计:

设计1:

首先我们设计了一个重试任务的表,放在酒店租务服务中,能保证只要本地事务成功,重试的任务就可以持久化下来。这解决了使用普通消息发送失败然后丢消息的问题。

重试任务目前我们设计每分钟都会触发。

这里为什么要考虑做这种设计呢?原因是比如如家提供的接口,有可能响应非常慢。在外部提供的SLA不够高的情况下,使用定时任务的设计能够提高流程整体的可靠性。同时与支付成功的节点做了一定的解耦。

设计2:

其次重试的任务中会记录状态与重试次数,重试次数消耗完之前,只要没有进入子流程,会一直重试。

至于重试次数的配置,会根据不同的供应商提供的SLA进行调整。

下单成功或者失败的子流程一致性则在子流程内部收敛。


关闭租约(RocketMQ事务消息)

签约超时、支付超时、外部下单失败修需要关闭租约。

而在关闭租约时除了作废租约、账单,还需要同步解锁券,券的操作就涉及到了营销中台的接口调用。

设计考虑:

这里我们同样可以考虑Seata的AT模式,但是在这个场景下,其实不需要强一致以及隔离性。

由于券已经被锁定,并且传入了我们的业务标识(租约的唯一编码),因此资源其实已经在业务数据层面做了隔离。

这个场景下,使用RocketMQ事务消息是一种更轻的方式,同时也能保障最终一致性。作为柔性事务的实现方式之一,事务消息满足BASE理论,即基本可用+最终一致。

总结

本文主要给读者介绍了我去年主R月租直连酒店项目的整体设计以及交易核心涉及的一些分布式一致性设计要点。

在B端交易的场景下,我们主要用到了三类方案来保障分布式环境下的数据一致性,即:

  1. SeataAT 模式
  2. 可靠消息最终一致性
    1. RocketMQ事务消息
    2. 本地消息表
  3. 最大努力通知
    1. MQ普通消息(可能会丢消息)+接口补偿

在针对不同场景考虑同样的分布式一致性问题的时候,最后的方案可能不同,但是设计的思路其实是一致的,即:

  1. 评估场景的侧重点、倾向性;
  2. 评估场景对一致性、隔离性、数据实时性等指标的容忍度;
  3. 评估方案与场景诉求的契合程度;
  4. 评估团队能力、基建现状;

系统设计出了需要梳理业务流程、明确业务规则,还需要做出取舍。除了一致性,在做系统设计的时候,也需要兼顾性能、可用性。同时技术方案需要做好兜底设计。

最后,如果读者有疑问或者建议,欢迎评论区指出。

以上。