在 WhatsApp 中,媒体文件(如图片、视频、语音消息)的上传和其在消息中的引用,通常不发生在一个单一的、跨服务的数据库事务中。这是一个经典的分布式系统挑战:如何原子性地协调两个独立系统(媒体存储服务和消息数据库)的操作。
为了保证大规模、高可用和高性能,WhatsApp 采用的是一个两阶段的、最终一致性的处理模式,并辅以强大的可靠性机制。
媒体文件上传和引用的两阶段处理
阶段 1:媒体文件上传 (Media File Upload - 媒体存储事务)
用户发起上传: 用户在 WhatsApp 客户端中选择并发送媒体文件。
客户端直传媒体: 客户端应用程序会直接将媒体文件(通常经过加密处理)上传到 WhatsApp 的媒体存储服务。这个服务是独立于核心消息数据库的,通常基于分布式文件系统或对象存储(例如,WhatsApp 内部可能类似于 Amazon S3 或 Google Cloud Storage 的大规模分布式存储)。
媒体存储服务处理:
媒体存储服务接收并持久化存储文件。这涉及到其内部的存储事务,确保文件被可靠地写入到分布式存储系统(通常会在多个副本上)。
成功存储后,媒体存储服务会生成一个唯一的媒体文件 ID(或 URL),以及文件哈希值(用于完整性校验),并将其返回给发送方客户端。
客户端确认: 发送方客户端收到媒体文件 ID 后,确认媒体上传成功。
此阶段的“原子性”: 媒体文件要么被成功上传并获得唯一 ID,要么上传失败(客户端会提示重试)。这个过程是原子性地发生在媒体存储系统内部的。
阶段 2:消息元数据发送 (Message Metadata Sending - 消息系统事务)
客户端构建消息: 一旦客户端收到媒体文件 ID,它就会构建一条消息。这条消息不包含媒体文件本身,而是包含:
消息文本(如果有)。
媒体文件 ID(作为引用)。
消息的元数据(发送方 ID、接收方 ID、时间戳、消息类型、消息 ID 等)。
所有这些数据(包括媒体 ID)都会进行端到端加密。
发送消息到核心消息系统: 这条包 埃及 whatsapp 数据库 含媒体 ID 的消息(元数据)会像普通文本消息一样,被发送到 WhatsApp 的核心消息系统:
消息首先被发送到 WhatsApp 的前端服务器。
服务器将消息(元数据)写入持久化消息队列(如 Kafka)。这是消息系统中的关键事务点,确保消息被服务器可靠地接受并进入处理流程。
消息投递服务从队列中消费消息,并将其写入核心消息数据库(如 Cassandra)。
最终,消息被投递到接收方设备。
此阶段的“原子性”: 消息元数据(包括媒体引用)要么被服务器可靠地接受并最终投递给接收方,要么整个消息发送失败。这得益于消息队列的可靠性、幂等性和状态机机制。
如何确保一致性(最终一致性)
松耦合: 媒体文件存储在一个系统,消息元数据存储在另一个系统。它们之间通过媒体文件 ID进行逻辑关联,而不是通过强耦合的分布式事务。
客户端协调: 发送方客户端是这两个阶段的协调者。它会先等待媒体上传成功,才继续发送包含媒体引用的消息。如果媒体上传失败,客户端不会发送消息。
异常处理与重试:
上传成功,消息发送失败: 如果媒体文件成功上传,但发送消息失败(例如,网络中断),客户端会重试发送消息。媒体文件在服务器上会保留一段时间,如果最终没有消息引用它,可能会在某个保留期后被垃圾回收。
接收方下载: 当接收方收到包含媒体 ID 的消息时,其客户端会识别出这是一个媒体消息,然后通过媒体 ID 从 WhatsApp 的媒体存储服务下载媒体文件。如果媒体文件因某些极端原因(例如,在极短的窗口期内被清理,或者上传未完全成功就已超时)无法下载,接收方可能会看到一个占位符或下载失败提示。
幂等性: 上传操作和消息发送操作都是幂等的。即使因网络重试导致操作被执行多次,结果也是一致的。
为什么不使用一个事务?
性能和可用性: 强制媒体上传和消息发送在一个单一的分布式 ACID 事务中,会引入巨大的延迟和复杂性。例如,需要等待媒体文件上传完成并被所有存储副本确认,才能提交整个消息事务,这会显著降低消息发送的速度。
系统解耦: 将媒体存储从核心消息流中分离出来,使得两个系统可以独立扩展、独立维护,提高了整体架构的韧性。
数据类型差异: 媒体文件是二进制大对象(BLOB),适合对象存储;消息元数据是结构化数据,适合 NoSQL 数据库。在一个事务中处理两者会非常不便。
结论: WhatsApp 通过一个基于媒体 ID 引用的两阶段提交模式来处理媒体上传和消息发送。这个过程不是一个传统的单一 ACID 事务,而是依赖客户端协调、持久化队列、幂等操作以及最终一致性的分布式系统设计,以确保高可用性和可靠性,同时有效管理海量媒体数据。