设计Airbnb式房屋预订系统:架构、功能与实现详解
在设计房屋预订系统时,我们需要考虑诸多方面,包括设计决策、非功能需求、数据复制、数据模型、高并发处理、系统架构、功能分区、列表创建与更新以及审批服务等。以下将详细阐述这些内容。
1. 设计决策与非功能需求
在设计系统时,我们需要先明确一些设计决策,同时也要满足相应的非功能需求。
-
设计决策排除项
:一些功能如保险、各方之间的聊天或其他通信、注册和登录、主机和客人的停电补偿以及用户评价等,不在本次预订服务的设计范围内。
-
API 端点
:为了实现房间的列出和预订功能,我们设计了以下 API 端点:
-
findRooms(cityId, checkInDate, checkOutDate)
-
bookRoom(userId, roomId, checkInDate, checkOutDate)
-
cancelBooking(bookingId)
-
viewBookings(hostId)
-
viewBookings(guestId)
-
非功能需求
:
-
可扩展性
:系统要能扩展到 10 亿个房间或 1 亿个每日预订,过去的预订数据可以删除,且不允许有编程生成的用户数据。
-
强一致性
:对于预订,尤其是房间可用性的列出,需要强一致性,以避免双重预订或在不可用日期进行预订。而对于其他列表信息,如描述或照片,最终一致性是可以接受的。
-
高可用性
:由于丢失预订会带来经济后果,所以系统需要高可用性。但如果要防止双重预订,就无法完全避免丢失预订。
-
性能要求
:高性能不是必需的,P99 在几秒内是可以接受的。
-
安全和隐私
:需要典型的安全和隐私要求,如身份验证,用户数据是私有的,在本次面试范围内的功能不需要授权。
2. 数据复制与缓存决策
在系统设计中,数据复制和缓存的使用是重要的决策点。
-
数据复制
:我们的系统类似于 Craigslist,产品是本地化的,一次只能搜索一个城市。因此,我们可以利用这一点,将数据中心主机分配给有大量列表的城市或多个列表较少的城市。由于写入性能不是关键,我们可以使用单领导者复制。为了最小化读取延迟,二级领导者和追随者可以在地理上分布在各个数据中心。我们使用元数据服务来包含城市到领导者和追随者主机 IP 地址的映射,以便服务查找地理上最近的追随者主机来获取任何特定城市的房间,或写入该城市对应的领导者主机。这个映射的大小很小,且管理员很少修改,所以我们可以在所有数据中心复制它,管理员在更新映射时可以手动确保数据一致性。
-
缓存决策
:与通常的做法相反,我们可能选择不使用内存缓存。在搜索结果中,我们只显示可用的房间。如果一个房间很受欢迎,它很快就会被预订,不再显示在搜索中。如果一个房间一直在搜索中显示,它可能不受欢迎,我们可能不想承担提供缓存的成本和额外复杂性。另一种说法是,缓存新鲜度很难维护,缓存的数据很快就会过时。
3. 房间可用性的数据模型
在设计数据模型时,我们需要考虑如何表示房间的可用性,以下是两种常见的方式:
-
(room_id, date, guest_id) 表
:概念上很简单,但会包含多个仅日期不同的行。例如,如果房间 1 在整个 1 月被客人 1 预订,将会有 31 行。
-
(room_id, guest_id, check_in, check_out) 表
:更紧凑。当客人提交带有入住和退房日期的搜索时,我们需要一个算法来确定是否有重叠日期。我们可以选择将这个算法编码在数据库查询中或后端中。前者更难维护和测试,但如果后端主机必须从数据库中获取房间可用性数据,会产生 I/O 成本。这两种方法的代码都可能在编码面试中被问到。
4. 处理重叠预订
当多个用户尝试预订同一房间且日期重叠时,我们需要合理处理,以避免双重预订。
-
基本处理原则
:第一个用户的预订应该被批准,我们的 UI 应该通知其他用户该房间在他们选择的日期不再可用,并引导他们找到另一个可用的房间。
-
替代方法
:
-
随机化搜索结果
:我们可以随机化搜索结果的顺序,以减少这种情况的发生,但这可能会干扰个性化(如推荐系统)。
-
锁定房间
:当用户点击搜索结果查看房间详情并可能提交预订请求时,我们可以在几分钟内锁定该房间的这些日期。在此期间,其他用户具有重叠日期的搜索将不会在结果列表中返回该房间。如果该房间在其他用户已经收到搜索结果后被锁定,点击房间详情应该显示锁定通知,并可能显示剩余的锁定时间,以防该用户未预订该房间。这意味着我们会失去一些预订,但我们认为防止双重预订比失去预订更重要,这是 Airbnb 与酒店的区别。
5. 系统的高等级架构
基于前面的需求讨论,我们设计了系统的高等级架构,各个服务负责一组相关的功能需求,这样可以分别开发和扩展这些服务。
| 服务名称 | 功能描述 | 重要性及特点 |
| ---- | ---- | ---- |
| 预订服务 | 供客人进行预订,是直接的收入来源,对可用性和延迟有最严格的非功能要求。高延迟会直接导致收入降低,该服务的停机对收入和声誉影响最大。但强一致性可能不太重要,可以为了可用性和延迟而牺牲一致性。 | 核心服务,对业务影响大 |
| 列表服务 | 供主机创建和管理列表,重要性低于预订服务。它是一个独立的服务,因为它与预订和可用性服务有不同的功能和非功能要求,不应与它们共享资源。 | 重要但相对次要 |
| 可用性服务 | 跟踪列表的可用性,供预订和列表服务使用。可用性和延迟要求与预订服务一样严格。读取必须可扩展,但写入不太频繁,可能不需要可扩展性。 | 支持预订和列表服务 |
| 审批服务 | 某些操作,如添加新列表或更新某些列表信息,可能需要运维人员批准才能发布。我们为此设计了审批服务。 | 确保列表合规性 |
| 推荐服务 | 为客人提供个性化的列表推荐,可以看作是内部广告服务。详细讨论不在面试范围内,但可以在图中包含并简要讨论。 | 提升用户体验 |
| 法规服务 | 列表服务和预订服务需要考虑当地法规,法规服务可以为列表服务提供 API,以便后者为主机提供符合当地法规的创建列表的用户体验。列表服务和法规服务可以由不同的团队开发,每个团队成员可以专注于获取与各自服务相关的领域专业知识。 | 确保系统合规性 |
| 其他服务 | 如分析等内部使用的服务,大多不在本次面试范围内。 | 辅助系统运营 |
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(API Gateway):::process --> B(Booking Service):::process
A --> C(Listing Service):::process
D(Client (web and mobile apps)):::process --> A
E(CDN):::process --> D
F(Recommender Service):::process --> D
G(Elasticsearch):::process --> B
H(Logging/Kafka):::process --> B
I(Availability Service):::process --> B
I --> C
J(Payment Service):::process --> B
K(Metadata Service):::process --> B
K --> C
6. 功能分区与列表创建更新
为了更好地管理系统,我们采用功能分区,并详细设计了列表的创建和更新流程。
-
功能分区
:我们可以采用按地理区域进行功能分区,类似于 Craigslist 的做法。列表可以放在数据中心,我们将应用程序部署到多个数据中心,并将每个用户路由到为他们所在城市服务的数据中心。
-
创建或更新列表
:创建列表可以分为两个任务。首先,主机需要获取适当的列表法规;然后,主机提交列表请求。
-
获取法规流程
:
1. 主机在客户端(网页或移动应用组件)上点击创建新列表的按钮,应用程序向列表服务发送包含用户位置的请求(位置可以通过手动提供或请求权限获取)。
2. 列表服务将位置转发给法规服务,法规服务返回适当的法规。
3. 列表服务将法规返回给客户端,客户端可以调整用户体验以适应法规。
-
提交列表请求流程
:主机输入列表信息并提交,这是一个 POST 请求到列表服务。列表服务会进行以下操作:
1. 验证请求体。
2. 将数据写入 SQL 列表表(我们可以命名为 Listing 表)。新列表和某些更新需要运维人员手动批准,该表可以包含一个布尔列 “Approved” 来指示列表是否已被批准。
3. 如果需要运维人员批准,列表服务会向审批服务发送 POST 请求,通知运维人员审核列表。
4. 向客户端发送 200 响应。
sequenceDiagram
participant Client
participant ListingService
participant RegulationService
Client->>ListingService: 新列表请求,包含用户位置
ListingService->>RegulationService: 转发位置
RegulationService->>ListingService: 返回法规
ListingService->>Client: 返回法规
sequenceDiagram
participant Client
participant ListingService
participant SQLService
participant ApprovalService
Client->>ListingService: 提交列表信息(POST 请求)
ListingService->>ListingService: 验证请求体
ListingService->>SQLService: 写入新列表
SQLService->>ListingService: 响应 OK
alt 需要审批
ListingService->>ApprovalService: 通知审核列表
ApprovalService->>ListingService: 响应 OK
end
ListingService->>Client: 发送 200 响应
在实际实现中,列表过程可能包含多个对列表服务的请求,创建列表的表单可能分为多个部分,主机可以分别填写和提交每个部分。同时,要允许主机在列表请求审核期间进行额外的更新,每次更新应更新相应的列表表行。对于通知,由于其业务逻辑可能复杂且经常变化,我们可以将其实现为批量 ETL 作业,该作业向列表服务发出请求,然后请求共享的通知服务发送通知,以提醒主机完成列表流程或通知运维人员未完成的列表。
设计Airbnb式房屋预订系统:架构、功能与实现详解
7. 审批服务的架构与实现
审批服务在系统中起着确保列表合规性的重要作用,下面详细介绍其架构和具体实现。
审批服务是一个内部应用程序,流量较低,架构相对简单,由客户端 Web 应用程序和后端服务组成,后端服务会向列表服务和共享 SQL 服务发出请求。
审批服务为列表服务提供了一个 POST 端点,用于提交需要审核的列表请求,这些请求会被写入名为 “listing_request” 的 SQL 表,该表包含以下列:
| 列名 | 描述 |
| ---- | ---- |
| id | 主键,用于唯一标识列表请求 |
| listing_id | 列表服务中 Listing 表的列表 ID,如果两个表在同一服务中,这将是一个外键 |
| created_at | 列表请求创建或更新的时间戳 |
| listing_hash | 用于确保运维人员不会对在审核过程中发生变化的列表请求进行审批或拒绝的额外机制 |
| status | 列表请求的枚举状态,值可以是 “none”、“assigned” 和 “reviewed” |
| last_accessed | 列表请求最后被获取并返回给运维人员的时间戳 |
| review_code | 枚举值,如 “APPROVED” 表示批准的列表请求,还有多种表示拒绝原因的枚举值,如 “VIOLATE_LOCAL_REGULATIONS”、“BANNED_HOST” 等 |
| reviewer_id | 被分配该列表请求的运维人员的 ID |
| review_submitted_at | 运维人员提交审批或拒绝的时间戳 |
| review_notes | 运维人员对列表请求批准或拒绝的备注 |
假设我们有 10,000 名运维人员,每人每周最多审核 5000 个新的或更新的列表,那么运维人员每周将向 SQL 表写入 5000 万行。如果每行占用 1KB,审批表每月将增长 1KB * 50M * 30 天 = 1.5TB。我们可以在 SQL 表中保留 1 - 2 个月的数据,并运行定期批量作业将旧数据存档到对象存储中。
我们还为每个运维人员设计了端点和 SQL 表,以便他们获取和执行分配的工作/审核。运维人员首先发送包含其 ID 的 GET 请求,从 listing_request 表中获取列表请求。为了防止多个人员被分配到同一个列表请求,后端会执行以下 SQL 事务步骤:
1. 如果运维人员已经被分配了一个列表请求,返回该分配的请求。即选择状态为 “assigned” 且 reviewer_id 为该运维人员 ID 的行。
2. 如果没有分配的列表请求,选择状态为 “none” 且 created_at 时间戳最小的行,这将是分配的列表请求。
3. 将状态更新为 “assigned”,并将 reviewer_id 更新为运维人员的 ID。
后端将这个列表请求返回给运维人员进行审核和批准或拒绝。以下是同步审批过程的步骤:
1.
更新列表请求表
:更新 listing_request 表中的行,包括状态、review_code、review_submitted_at 和 review_notes 列。为了避免主机在运维人员审核期间更新列表请求导致的竞态条件,POST 请求应包含审批服务先前返回给运维人员的列表哈希,后端应确保该哈希与当前哈希相同。如果哈希不同,将更新后的列表请求返回给运维人员重新审核。
2.
更新列表服务
:向列表服务发送 PUT 请求,更新 listing_request.status 和 listing_request.reviewed_at 列。首先选择哈希并验证其与提交的哈希相同,将两个 SQL 查询包装在一个事务中。
3.
通知预订服务
:向预订服务发送 POST 请求,以便预订服务开始向客人展示该列表。
4.
通知主机
:后端请求共享的通知服务通知主机审批或拒绝的结果。
5.
返回响应
:最后,后端向客户端发送 200 响应。这些步骤应采用幂等方式编写,以便在任何步骤失败时可以重复执行。
sequenceDiagram
participant Client
participant ApprovalService
participant ListingService
participant NotificationService
participant BookingService
Client->>ApprovalService: 提交审批请求
ApprovalService->>ApprovalService: 更新 listing_request 表
ApprovalService->>ListingService: 发送 PUT 请求更新列表状态
ApprovalService->>BookingService: 发送 POST 请求展示列表
ApprovalService->>NotificationService: 请求通知主机
ApprovalService->>Client: 发送 200 响应
8. 异步审批与相关问题处理
为了避免同步请求可能带来的长延迟和不一致问题,我们可以采用异步审批的方式,使用变更数据捕获(CDC)。
在异步审批请求中,审批服务将请求发送到 Kafka 队列并返回 200。消费者从 Kafka 队列消费请求,并向其他服务发出请求。由于审批速率较低,消费者可以采用指数退避和重试机制,避免在队列为空时频繁轮询,在队列为空时每分钟轮询一次。
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(Approval Service):::process --> B(Kafka queue):::process
B --> C(Consumer):::process
C --> D(Listing Service):::process
C --> E(Booking Service):::process
C --> F(Notification Service):::process
通知服务应在列表服务和预订服务更新后再通知主机,因此它会从两个 Kafka 主题消费事件,对应每个服务。当通知服务从一个主题消费到与特定列表审批事件对应的事件时,它必须等待另一个服务对应的事件,然后才能发送通知,所以通知服务需要一个数据库来记录这些事件。
为了防止服务之间可能出现的无声错误导致的不一致,我们可以实现批量 ETL 作业来审计这三个服务。如果发现不一致,该作业将触发警报通知开发人员。
我们选择使用 CDC 而不是 Saga 进行这个过程,因为我们预计任何服务都不会拒绝请求,所以不需要补偿事务。
9. 其他相关问题讨论
在设计审批服务时,还会遇到一些其他相关问题,以下是对这些问题的讨论。
-
多运维人员审核
:如果一个列表审核可能涉及多个运维人员,我们可以根据具体情况进行讨论。例如,如果运维人员可以审核特定数据中心的列表请求,我们的设计不需要额外的处理。否则,我们可以考虑以下方法:
-
JOIN 查询
:在 listing_request 表和 listing 表之间进行 JOIN 查询,以获取特定国家或城市的列表请求。但由于这两个表在不同的服务中,我们需要不同的解决方案,如重新设计系统,将列表和审批服务合并;在应用层处理 JOIN 逻辑,但会有服务间数据传输的 I/O 成本;对列表数据进行反规范化或复制,如在 listing_request 表中添加位置列或在审批服务中复制 listing 表。
-
利用列表 ID
:列表 ID 可以包含城市 ID,公司可以维护一个 (ID, 城市) 的列表,任何服务都可以访问。这个列表应该是追加式的,以避免昂贵且容易出错的数据迁移。
-
高流量问题处理
:由于预订服务可能有高流量,将批准的列表复制到预订服务这一步可能有最高的失败率。我们可以采用指数退避和重试或死信队列等常见方法。审批服务到预订服务的流量与客人的流量相比可以忽略不计,所以我们不会通过减少审批服务的流量来降低预订服务停机的概率。
-
自动化审批
:我们可以讨论一些审批或拒绝的自动化方法。例如,在 SQL 表 “Rules” 中定义规则,一个函数可以获取这些规则并应用于列表内容。我们还可以使用机器学习,在机器学习服务中训练模型,将选定的模型 ID 放入 Rules 表,函数将列表内容和模型 ID 发送到机器学习服务,该服务将返回批准、拒绝或不确定(即需要手动审核)。listing_request.reviewer_id 可以是 “AUTOMATED”,而不确定审核的 listing_request.review_code 值可以是 “INCONCLUSIVE”。
综上所述,设计一个像 Airbnb 这样的房屋预订系统需要综合考虑多个方面,包括系统架构、数据模型、并发处理、审批流程等。通过合理的设计和优化,可以确保系统的高可用性、一致性和用户体验,同时满足业务的需求和法规要求。在实际设计过程中,我们需要根据具体情况进行权衡和选择,不断优化和改进系统。
超级会员免费看

被折叠的 条评论
为什么被折叠?



