土法炼钢兴趣小组的算法知识备份

【系统架构设计百科】微信架构:十亿用户的即时通信挑战

文章导航

分类入口
architecture
标签入口
#WeChat#instant-messaging#Mars#mobile-architecture#red-packet

目录

微信月活跃用户超过 13 亿。每天有超过 450 亿条消息在这个系统中流转,峰值时每秒的消息量达到数千万级别。这个数字背后,是一套从移动网络层到存储层、从消息同步协议到支付系统的完整技术体系。

本文从微信的实际技术选型和公开技术分享出发,拆解微信在长连接管理、消息同步、朋友圈扩散、红包洪峰等场景下的架构设计。重点关注三个问题:移动端弱网环境下如何保持连接稳定,十亿级消息如何高效同步,以及春节红包这样的极端流量场景如何应对。

一、微信技术架构演进概览

微信的技术架构经历了三个明显的阶段。

1.1 第一阶段:快速上线(2011-2012)

微信 1.0 版本的核心目标是快速验证产品。技术栈选择了最成熟的方案:

这个阶段的用户量级在千万级别,单机房架构能够支撑。

1.2 第二阶段:亿级扩展(2013-2016)

用户量突破 3 亿后,原有架构遇到了明显瓶颈:

微信团队做了几项关键改造:

  1. 自研分布式存储系统,替代 MySQL 承载消息数据
  2. 部署上海和深圳双机房,实现同城双活
  3. 开发 Mars 网络库,统一移动端网络层

1.3 第三阶段:十亿级精细化(2017-至今)

用户量突破 10 亿后,架构优化的重点从”能不能撑住”转向”如何更高效”:

下面这张图展示了微信当前架构的核心层次:

graph TB
    subgraph 客户端层
        iOS[iOS 客户端]
        Android[Android 客户端]
        Desktop[桌面客户端]
    end

    subgraph 接入层
        LB[全球负载均衡]
        LongConn[长连接网关]
        ShortConn[短连接网关]
    end

    subgraph 业务逻辑层
        MsgSvc[消息服务]
        MomentSvc[朋友圈服务]
        PaySvc[支付服务]
        ContactSvc[通讯录服务]
        GroupSvc[群聊服务]
        AuthSvc[认证服务]
    end

    subgraph 基础服务层
        Mars[Mars 网络库]
        MsgSync[消息同步服务]
        SeqSvr[序列号服务 SeqSvr]
        IDGen[ID 生成器]
    end

    subgraph 存储层
        PaxosStore[PaxosStore]
        KVStore[KV 存储]
        MySQL[(MySQL)]
        HDFS[HDFS 文件存储]
    end

    iOS --> LB
    Android --> LB
    Desktop --> LB
    LB --> LongConn
    LB --> ShortConn
    LongConn --> MsgSvc
    LongConn --> MomentSvc
    ShortConn --> PaySvc
    MsgSvc --> MsgSync
    MsgSync --> SeqSvr
    MsgSvc --> PaxosStore
    MomentSvc --> KVStore
    PaySvc --> MySQL
    ContactSvc --> PaxosStore
    GroupSvc --> PaxosStore
    SeqSvr --> KVStore

二、长连接架构与 Mars 网络库

移动端即时通信的核心挑战是:如何在复杂多变的移动网络环境下,保持一条稳定的长连接。

2.1 移动网络的现实困境

移动网络和固定宽带网络有本质区别:

传统的 HTTP 长轮询在这种环境下表现很差:每次请求都需要完整的 TCP 握手和 TLS 协商,延迟和功耗都不可接受。

2.2 Mars 网络库的设计

Mars 是微信团队开源的跨平台网络库(https://github.com/Tencent/mars),专门针对移动场景设计。它包含四个核心模块:

Mars 架构
├── STN(Signal Transport Network):信令传输网络
│   ├── 长连接管理
│   ├── 短连接管理
│   └── 智能心跳
├── SDT(Signal Detection Tool):网络诊断
│   ├── 连通性检测
│   └── 网络质量评估
├── Comm:公共库
│   ├── 日志组件 xlog
│   ├── 线程模型
│   └── 平台适配层
└── Xlog:高性能日志
    ├── mmap 写入
    └── 压缩加密

2.3 STN 的连接策略

STN(Signal Transport Network,信令传输网络)是 Mars 的核心模块,负责管理客户端与服务器之间的网络连接。

2.3.1 复合连接策略

STN 不依赖单一的连接方式,而是同时尝试多种策略:

// Mars STN 连接策略伪代码
class ConnectionManager {
public:
    void startConnection() {
        // 第一优先级:尝试长连接
        // 长连接使用自定义二进制协议,端口 8080 或 443
        tryLongConnection();

        // 如果长连接在 5 秒内未建立,并行尝试短连接
        scheduleTimeout(5000, [this]() {
            if (!longConnEstablished_) {
                tryShortConnection();
            }
        });
    }

private:
    void tryLongConnection() {
        // 获取服务器 IP 列表(优先使用本地缓存)
        auto ipList = getServerIPs();

        // 并行发起多个 TCP 连接(竞速模式)
        for (const auto& ip : ipList) {
            asyncConnect(ip, 8080, [this](Socket* sock) {
                if (!longConnEstablished_) {
                    longConnEstablished_ = true;
                    activateConnection(sock);
                    // 关闭其他并行连接
                    cancelOtherConnections();
                }
            });
        }
    }

    void tryShortConnection() {
        // 短连接走 HTTPS,兼容性最好
        // 用于发送即时消息,不保持长连接
        httpPost(shortConnUrl_, payload_);
    }

    std::vector<std::string> getServerIPs() {
        // 三级 IP 获取策略
        // 1. 本地缓存(上次成功连接的 IP)
        auto cached = loadCachedIPs();
        if (!cached.empty()) return cached;

        // 2. HTTP DNS(绕过运营商 DNS 劫持)
        auto httpDns = queryHTTPDNS();
        if (!httpDns.empty()) return httpDns;

        // 3. 系统 DNS(最后兜底)
        return querySystemDNS();
    }

    bool longConnEstablished_ = false;
    std::string shortConnUrl_;
    std::string payload_;
};

这个策略的核心思想是竞速:不等任何一种方式超时,而是让多种方式同时竞争,用最先成功的那条连接。

2.3.2 智能心跳机制

长连接建立后,需要定期发送心跳包来防止 NAT 超时。但心跳间隔的选择是一个权衡问题:

Mars 使用了一个自适应心跳算法:

class SmartHeartbeat {
public:
    // 心跳间隔范围:最短 60 秒,最长 300 秒
    static constexpr int MIN_INTERVAL = 60;   // 秒
    static constexpr int MAX_INTERVAL = 300;   // 秒
    static constexpr int STEP = 30;            // 步进值

    int computeInterval() {
        if (lastHeartbeatFailed_) {
            // 上次心跳失败,回退到上一个成功的间隔
            currentInterval_ = lastSuccessInterval_;
            failureCount_++;

            if (failureCount_ >= 3) {
                // 连续失败 3 次,使用最小间隔
                currentInterval_ = MIN_INTERVAL;
                failureCount_ = 0;
            }
        } else {
            // 心跳成功,记录当前间隔并尝试增大
            lastSuccessInterval_ = currentInterval_;
            failureCount_ = 0;

            if (currentInterval_ + STEP <= MAX_INTERVAL) {
                currentInterval_ += STEP;
            }
        }
        return currentInterval_;
    }

    void onNetworkChange() {
        // 网络环境变化时(Wi-Fi/4G 切换),重置心跳探测
        currentInterval_ = MIN_INTERVAL;
        lastSuccessInterval_ = MIN_INTERVAL;
        failureCount_ = 0;
    }

private:
    int currentInterval_ = MIN_INTERVAL;
    int lastSuccessInterval_ = MIN_INTERVAL;
    int failureCount_ = 0;
    bool lastHeartbeatFailed_ = false;
};

算法的本质是二分探测:逐步增大心跳间隔,直到发现某个间隔会导致心跳失败,然后回退到上一个成功的值。不同运营商、不同网络类型的 NAT 超时时间差异很大,这种自适应方式比写死一个固定值要可靠得多。

2.4 Xlog 高性能日志

Mars 的日志模块 Xlog 解决了移动端日志的一个经典问题:如何在保证不丢日志的前提下,尽量减少对性能和电池的影响。

Xlog 的核心设计:

  1. 使用 mmap 将日志文件映射到内存,写日志等价于写内存,极低延迟
  2. 即使进程崩溃,mmap 映射的数据也会被操作系统刷到磁盘,不会丢失
  3. 日志在写入前使用 Zlib 压缩 + 自定义加密,减少存储空间和保护用户隐私
// Xlog mmap 写入的简化实现
class MmapLogWriter {
public:
    bool init(const std::string& logPath, size_t bufferSize) {
        fd_ = open(logPath.c_str(), O_RDWR | O_CREAT, 0644);
        if (fd_ < 0) return false;

        // 预分配文件空间
        ftruncate(fd_, bufferSize);

        // mmap 映射
        buffer_ = (char*)mmap(nullptr, bufferSize,
                              PROT_READ | PROT_WRITE,
                              MAP_SHARED, fd_, 0);
        if (buffer_ == MAP_FAILED) return false;

        bufferSize_ = bufferSize;
        writePos_ = 0;
        return true;
    }

    void write(const char* data, size_t len) {
        // 压缩数据
        auto compressed = zlibCompress(data, len);

        // 加密
        auto encrypted = encrypt(compressed);

        // 写入 mmap 缓冲区(等价于写内存)
        if (writePos_ + encrypted.size() <= bufferSize_) {
            memcpy(buffer_ + writePos_,
                   encrypted.data(), encrypted.size());
            writePos_ += encrypted.size();
        } else {
            // 缓冲区满,切换到新文件
            rotateLogFile();
        }
    }

private:
    int fd_;
    char* buffer_;
    size_t bufferSize_;
    size_t writePos_;

    void rotateLogFile();
    std::vector<char> zlibCompress(const char* data, size_t len);
    std::vector<char> encrypt(const std::vector<char>& data);
};

三、消息收发与存储模型

3.1 消息的基本流转路径

一条微信消息从发送到接收,经历以下步骤:

sequenceDiagram
    participant A as 发送方客户端
    participant GW as 长连接网关
    participant MS as 消息服务
    participant Seq as SeqSvr
    participant Store as PaxosStore
    participant NS as 通知服务
    participant B as 接收方客户端

    A->>GW: 发送消息(加密)
    GW->>MS: 转发消息
    MS->>Seq: 申请消息序列号
    Seq-->>MS: 返回 sequence
    MS->>Store: 写入消息(发送方信箱 + 接收方信箱)
    Store-->>MS: 写入确认
    MS-->>GW: 发送成功 ACK
    GW-->>A: 确认发送成功
    MS->>NS: 通知接收方有新消息
    NS->>B: 推送通知(携带最新 sequence)
    B->>MS: 拉取新消息(携带本地最大 sequence)
    MS->>Store: 读取消息
    Store-->>MS: 返回消息列表
    MS-->>B: 返回消息数据
    B-->>MS: 确认收到(ACK)

3.2 消息存储的信箱模型

微信的消息存储采用信箱模型(Mailbox Model),而不是传统即时通信系统常用的会话模型

两种模型的区别:

维度 会话模型 信箱模型
存储结构 按会话存储,一个会话的所有消息在一起 按用户存储,每个用户有独立信箱
写入操作 一条消息写一份,存在会话中 一条消息写多份,分别写入发送方和接收方信箱
读取操作 查看会话消息需要按会话 ID 查询 查看自己的消息只需要扫描自己的信箱
多端同步 需要额外的同步机制来标记各端的读取位置 每个信箱独立维护 sequence,天然支持多端同步
群聊扩展 群消息只存一份,读取效率高 群消息写 N 份(N 为群成员数),写入放大
离线消息 需要单独的离线消息队列 信箱天然支持离线消息,上线后拉取信箱增量
消息删除 一方删除需要标记而非真删 各自信箱独立,删除操作互不影响

信箱模型的写入放大是明显的代价。一条群消息如果群里有 500 人,就需要写 500 份。微信通过以下方式缓解:

3.3 SeqSvr:序列号生成服务

消息的有序性依赖于 SeqSvr(Sequence Server,序列号服务器)。微信的 SeqSvr 有几个设计要点:

  1. 每个用户拥有独立的递增序列号,不同用户的序列号互不影响
  2. 序列号按段分配(Section),减少对存储的访问频率
  3. 使用两级存储:内存中缓存当前段,持久化存储段的最大值
/**
 * SeqSvr 段分配策略的简化实现
 * 每次从存储层分配一个步长为 10000 的段
 * 段内的序列号分配在内存中完成,无需访问存储
 */
public class SeqAllocator {

    private static final long SECTION_SIZE = 10000L;

    private final ConcurrentHashMap<String, SeqSection> sections
        = new ConcurrentHashMap<>();

    private final PaxosStoreClient storeClient;

    public SeqAllocator(PaxosStoreClient storeClient) {
        this.storeClient = storeClient;
    }

    public long allocSeq(String userId) {
        SeqSection section = sections.computeIfAbsent(
            userId, k -> loadNewSection(k)
        );

        synchronized (section) {
            if (section.current < section.max) {
                return ++section.current;
            }
            // 当前段用完,加载新段
            SeqSection newSection = loadNewSection(userId);
            sections.put(userId, newSection);
            return ++newSection.current;
        }
    }

    private SeqSection loadNewSection(String userId) {
        // 从存储层原子性地分配一个新段
        // CAS 操作保证并发安全
        long currentMax = storeClient.getAndAdd(
            "seq:" + userId, SECTION_SIZE
        );
        SeqSection section = new SeqSection();
        section.current = currentMax;
        section.max = currentMax + SECTION_SIZE;
        return section;
    }

    private static class SeqSection {
        long current;
        long max;
    }
}

段分配的好处是:只要段没用完,序列号的分配就是纯内存操作,延迟极低。一个用户一天发送的消息量通常不会超过 10000 条,所以大多数情况下一天只需要访问一次存储层。

四、消息同步协议(Diff-Sync)

4.1 同步问题的本质

微信需要解决的消息同步问题是:用户可能在手机、平板、PC 等多个设备上登录,每个设备的消息状态可能不一致。例如:

4.2 基于 Sequence 的增量同步

微信的消息同步协议可以概括为一句话:每个设备维护自己的最大 sequence,拉取时只请求比本地 sequence 大的消息

这就是差异同步(Diff-Sync)的核心思想。具体流程如下:

sequenceDiagram
    participant Device as 客户端设备
    participant Server as 消息服务器
    participant Store as 存储层

    Note over Device: 本地最大 sequence = 1000

    Server->>Device: 推送通知(最新 sequence = 1050)

    Device->>Server: 同步请求(本地 sequence = 1000)
    Server->>Store: 查询 sequence > 1000 的消息
    Store-->>Server: 返回 50 条消息
    Server-->>Device: 返回消息列表 + 最新 sequence = 1050

    Note over Device: 更新本地 sequence = 1050

    Note over Device: 一段时间后...

    Server->>Device: 推送通知(最新 sequence = 1052)
    Device->>Server: 同步请求(本地 sequence = 1050)
    Server->>Store: 查询 sequence > 1050 的消息
    Store-->>Server: 返回 2 条消息
    Server-->>Device: 返回消息列表 + 最新 sequence = 1052

    Note over Device: 更新本地 sequence = 1052

4.3 同步协议的关键细节

4.3.1 推拉结合

早期微信使用纯拉模式:客户端定期轮询服务器检查是否有新消息。这种方式延迟高、资源浪费大。改进后采用推拉结合的模式:

推送的通知非常小(只包含最新 sequence),真正的消息数据通过拉取获得。这种设计的好处是:

  1. 推送通道只需要传输极少的数据,即使推送偶尔丢失也不影响正确性
  2. 拉取操作是幂等的,可以安全重试
  3. 客户端可以控制拉取的时机和频率,避免被推送淹没

4.3.2 分页拉取

当用户长时间离线后重新上线时,可能有大量未读消息需要同步。一次性拉取所有消息会导致:

微信对消息拉取做了分页处理:

class MessageSyncClient:
    """客户端消息同步逻辑"""

    PAGE_SIZE = 20  # 每次拉取 20 条消息

    def __init__(self):
        self.local_seq = self.load_local_seq()

    def on_new_message_notify(self, server_seq: int):
        """收到服务器推送的新消息通知"""
        if server_seq <= self.local_seq:
            return  # 没有新消息

        self.sync_messages(server_seq)

    def sync_messages(self, target_seq: int):
        """增量同步消息,分页拉取"""
        current_seq = self.local_seq

        while current_seq < target_seq:
            # 分页拉取
            response = self.fetch_messages(
                from_seq=current_seq,
                limit=self.PAGE_SIZE
            )

            if not response.messages:
                break

            # 处理并存储消息
            for msg in response.messages:
                self.save_message_to_local_db(msg)
                current_seq = max(current_seq, msg.sequence)

            # 更新本地 sequence
            self.local_seq = current_seq
            self.save_local_seq(current_seq)

            # 通知 UI 层刷新
            self.notify_ui_update(response.messages)

    def fetch_messages(self, from_seq: int, limit: int):
        """从服务器拉取消息"""
        request = SyncRequest(
            user_id=self.user_id,
            last_seq=from_seq,
            count=limit
        )
        return self.network_client.sync(request)

    def load_local_seq(self) -> int:
        """从本地数据库加载最大 sequence"""
        pass

    def save_local_seq(self, seq: int):
        """持久化最新 sequence"""
        pass

    def save_message_to_local_db(self, msg):
        """存储消息到本地 SQLite 数据库"""
        pass

    def notify_ui_update(self, messages):
        """通知界面刷新"""
        pass

4.3.3 冲突处理

多端同步不可避免地会遇到冲突场景。微信的处理策略相对简单——最后写入胜出(Last-Writer-Wins,LWW)。由于消息是追加写入的,真正的冲突只发生在状态类操作上(比如消息撤回、已读标记)。对于这类操作,服务器端的 sequence 保证了全局顺序,各端按照 sequence 顺序重放操作即可。

五、朋友圈的读扩散架构

5.1 写扩散 vs 读扩散

社交信息流的架构有两种经典模式:

维度 写扩散(Fan-out on Write) 读扩散(Fan-out on Read)
发布时 将内容写入每个粉丝的收件箱 只写入发布者的发件箱
读取时 直接读取自己的收件箱,无需聚合 遍历所有关注者的发件箱,实时聚合
延迟 读取延迟低(预计算) 发布延迟低,读取延迟高
存储 存储量大(写入放大) 存储量小
适用场景 粉丝数少、读多写少 粉丝数多、写多读少
代表系统 Twitter 早期 微信朋友圈

微信朋友圈选择了读扩散模式。原因是:

  1. 微信是双向好友关系,一个用户的好友数上限是 5000,不会出现百万粉丝的场景
  2. 朋友圈的浏览频率远低于消息收发,大部分用户不会频繁刷新朋友圈
  3. 写扩散在发布时需要写入所有好友的收件箱,5000 个好友就是 5000 次写入,对发布延迟影响大

5.2 朋友圈的存储结构

朋友圈的存储分为三部分:

  1. 内容表(Content Table):存储朋友圈的原始内容(文字、图片 ID、视频 ID 等),按发布者 ID 索引
  2. 评论表(Comment Table):存储评论和点赞,按内容 ID 索引
  3. 时间线索引(Timeline Index):存储每个用户的朋友圈时间线,按时间倒序排列
/**
 * 朋友圈时间线的读扩散实现
 */
public class MomentTimelineService {

    private final ContentStore contentStore;
    private final ContactService contactService;
    private final CommentStore commentStore;
    private final CacheService cache;

    /**
     * 获取用户的朋友圈时间线
     * 读扩散:实时聚合所有好友的朋友圈内容
     */
    public List<MomentItem> getTimeline(
            String userId, long beforeTime, int count) {

        // 获取好友列表
        List<String> friendIds = contactService.getFriends(userId);
        friendIds.add(userId);  // 包含自己

        // 从缓存或存储中获取每个好友的最新内容
        PriorityQueue<MomentItem> mergeHeap = new PriorityQueue<>(
            (a, b) -> Long.compare(b.getCreateTime(), a.getCreateTime())
        );

        for (String friendId : friendIds) {
            // 每个好友最多取 count 条(减少不必要的扫描)
            List<MomentItem> items = contentStore.getByUser(
                friendId, beforeTime, count
            );
            mergeHeap.addAll(items);
        }

        // 归并排序取 Top-N
        List<MomentItem> result = new ArrayList<>();
        while (!mergeHeap.isEmpty() && result.size() < count) {
            MomentItem item = mergeHeap.poll();
            // 权限过滤(检查是否对当前用户可见)
            if (isVisible(item, userId)) {
                // 加载评论和点赞(只加载共同好友的)
                item.setComments(
                    commentStore.getVisibleComments(
                        item.getId(), userId
                    )
                );
                result.add(item);
            }
        }
        return result;
    }

    private boolean isVisible(MomentItem item, String viewerId) {
        // 检查朋友圈可见性设置
        // 部分可见、不给谁看等逻辑
        return true; // 简化
    }
}

5.3 朋友圈的缓存策略

读扩散的性能瓶颈在于每次刷新都需要聚合多个好友的数据。微信通过多级缓存来缓解:

六、微信支付架构

6.1 金融级系统的核心要求

微信支付从社交工具的附属功能,发展成为日均交易笔数超过 10 亿的支付平台。金融级系统与普通互联网系统有本质区别:

6.2 支付核心链路

微信支付的核心链路包括以下步骤:

graph LR
    subgraph 用户端
        User[用户确认支付]
    end

    subgraph 支付网关
        Gateway[支付网关]
        Risk[风控引擎]
    end

    subgraph 支付核心
        Order[订单服务]
        Account[账户服务]
        Channel[渠道路由]
    end

    subgraph 外部系统
        Bank[银行系统]
        ClearHouse[清算中心]
    end

    User --> Gateway
    Gateway --> Risk
    Risk -->|通过| Order
    Risk -->|拒绝| User
    Order --> Account
    Account --> Channel
    Channel --> Bank
    Bank --> ClearHouse

6.3 幂等性设计

支付系统中最常见的事故是重复扣款。网络超时、客户端重试、服务端重启都可能导致同一笔支付请求被执行多次。微信支付通过幂等性设计来解决:

/**
 * 支付幂等性处理
 * 基于全局唯一的交易号保证同一笔支付只执行一次
 */
public class PaymentService {

    private final TransactionStore txStore;
    private final AccountService accountService;
    private final DistributedLock lock;

    public PayResult pay(PayRequest request) {
        String txId = request.getTransactionId();

        // 第一步:查询是否已处理过
        Transaction existingTx = txStore.findByTxId(txId);
        if (existingTx != null) {
            // 幂等返回:直接返回上次的处理结果
            return buildResultFromTx(existingTx);
        }

        // 第二步:获取分布式锁,防止并发重复执行
        String lockKey = "pay_lock:" + txId;
        if (!lock.tryLock(lockKey, 10, TimeUnit.SECONDS)) {
            throw new PayException("PROCESSING",
                "支付正在处理中,请勿重复提交");
        }

        try {
            // 双重检查
            existingTx = txStore.findByTxId(txId);
            if (existingTx != null) {
                return buildResultFromTx(existingTx);
            }

            // 第三步:执行扣款
            Transaction tx = new Transaction();
            tx.setTxId(txId);
            tx.setStatus("PROCESSING");
            txStore.insert(tx);  // 先写入事务记录

            try {
                accountService.debit(
                    request.getUserId(), request.getAmount()
                );
                tx.setStatus("SUCCESS");
            } catch (Exception e) {
                tx.setStatus("FAILED");
                tx.setErrorMsg(e.getMessage());
            }

            txStore.update(tx);
            return buildResultFromTx(tx);

        } finally {
            lock.unlock(lockKey);
        }
    }

    private PayResult buildResultFromTx(Transaction tx) {
        PayResult result = new PayResult();
        result.setTransactionId(tx.getTxId());
        result.setStatus(tx.getStatus());
        return result;
    }
}

6.4 对账系统

微信支付每天需要与银行、第三方支付渠道进行对账,确保每一笔交易的金额和状态一致。对账的基本流程:

  1. 每天凌晨从各渠道下载交易流水文件
  2. 将渠道流水与微信内部流水逐笔比对
  3. 差异部分生成差错记录,人工或自动处理
  4. 长款(渠道有、微信没有)和短款(微信有、渠道没有)分别处理

七、春节红包的流量洪峰设计

7.1 问题规模

春节红包是微信系统面临的最大流量挑战。2024 年除夕夜的峰值数据:

这种极端的脉冲式流量具有以下特征:

7.2 红包系统的核心架构

红包系统的技术难点在于:抢红包是一个高并发的扣减操作。一个 100 人群里发了一个 10 人份的红包,可能有 100 个人同时点击”抢”,但只有 10 个人能成功。

graph TB
    subgraph 客户端
        UI[红包 UI]
    end

    subgraph 接入层
        CDN[CDN 静态资源]
        APIGateway[API 网关]
    end

    subgraph 红包服务
        OrderSvc[红包订单服务]
        GrabSvc[抢红包服务]
        AmountCalc[金额拆分服务]
    end

    subgraph 排队层
        Queue[请求排队队列]
    end

    subgraph 存储层
        RedisCluster[Redis 集群]
        DBCluster[数据库集群]
    end

    subgraph 异步处理
        AsyncTransfer[异步转账服务]
        Reconcile[对账服务]
    end

    UI --> CDN
    UI --> APIGateway
    APIGateway --> OrderSvc
    APIGateway --> Queue
    Queue --> GrabSvc
    GrabSvc --> RedisCluster
    GrabSvc --> AmountCalc
    OrderSvc --> DBCluster
    GrabSvc --> AsyncTransfer
    AsyncTransfer --> DBCluster
    AsyncTransfer --> Reconcile

7.3 红包金额的拆分算法

微信红包采用的是二倍均值法:每次抢红包时,从剩余金额中随机生成一个值,范围是 [0.01, 剩余金额 / 剩余人数 * 2]

/**
 * 红包金额拆分算法——二倍均值法
 * 保证每个人抢到的金额期望值相等
 */
public class RedPacketSplitter {

    private static final int MIN_AMOUNT = 1; // 最小 1 分钱(单位:分)

    /**
     * 预拆分红包金额
     * @param totalAmount 总金额(单位:分)
     * @param count 红包个数
     * @return 每个红包的金额列表
     */
    public static List<Integer> split(int totalAmount, int count) {
        List<Integer> amounts = new ArrayList<>();
        int remainAmount = totalAmount;
        int remainCount = count;

        Random random = new Random();

        for (int i = 0; i < count - 1; i++) {
            // 二倍均值法
            // 最大值 = 剩余金额 / 剩余人数 * 2
            int maxAmount = (remainAmount / remainCount) * 2;

            // 随机金额范围:[1, maxAmount]
            int amount = random.nextInt(maxAmount - MIN_AMOUNT)
                         + MIN_AMOUNT;

            // 确保剩余金额够分给剩下的人
            if (remainAmount - amount < (remainCount - 1) * MIN_AMOUNT) {
                amount = remainAmount - (remainCount - 1) * MIN_AMOUNT;
            }

            amounts.add(amount);
            remainAmount -= amount;
            remainCount--;
        }

        // 最后一个人拿剩余所有金额
        amounts.add(remainAmount);
        return amounts;
    }
}

7.4 抢红包的并发控制

抢红包的核心是一个库存扣减问题。微信的解决方案分为几个层次:

7.4.1 请求排队

面对瞬间的海量请求,直接打到数据库会导致数据库崩溃。微信在抢红包链路中加入了请求排队层:

// 抢红包请求排队处理
package redpacket

import (
    "context"
    "fmt"
    "sync"
    "time"
)

// GrabRequest 抢红包请求
type GrabRequest struct {
    PacketID string
    UserID   string
    Result   chan GrabResult
}

// GrabResult 抢红包结果
type GrabResult struct {
    Success bool
    Amount  int64
    ErrMsg  string
}

// QueueProcessor 排队处理器
// 将并发请求转换为串行处理,消除数据库并发冲突
type QueueProcessor struct {
    queues map[string]chan *GrabRequest
    mu     sync.RWMutex
}

func NewQueueProcessor() *QueueProcessor {
    return &QueueProcessor{
        queues: make(map[string]chan *GrabRequest),
    }
}

// Submit 提交抢红包请求
func (p *QueueProcessor) Submit(
    ctx context.Context, req *GrabRequest,
) GrabResult {
    queue := p.getOrCreateQueue(req.PacketID)

    req.Result = make(chan GrabResult, 1)

    select {
    case queue <- req:
        // 成功入队,等待结果
    case <-ctx.Done():
        return GrabResult{
            Success: false,
            ErrMsg:  "请求超时",
        }
    }

    select {
    case result := <-req.Result:
        return result
    case <-ctx.Done():
        return GrabResult{
            Success: false,
            ErrMsg:  "处理超时",
        }
    }
}

// getOrCreateQueue 为每个红包创建独立的处理队列
func (p *QueueProcessor) getOrCreateQueue(
    packetID string,
) chan *GrabRequest {
    p.mu.RLock()
    q, exists := p.queues[packetID]
    p.mu.RUnlock()

    if exists {
        return q
    }

    p.mu.Lock()
    defer p.mu.Unlock()

    // 双重检查
    if q, exists = p.queues[packetID]; exists {
        return q
    }

    q = make(chan *GrabRequest, 1000)
    p.queues[packetID] = q

    // 启动消费者协程
    go p.consume(packetID, q)

    return q
}

// consume 串行消费队列中的请求
func (p *QueueProcessor) consume(
    packetID string, queue chan *GrabRequest,
) {
    timeout := time.NewTimer(30 * time.Minute)
    defer timeout.Stop()

    for {
        select {
        case req := <-queue:
            result := processGrab(packetID, req.UserID)
            req.Result <- result
        case <-timeout.C:
            // 红包超时未抢完,清理队列
            p.mu.Lock()
            delete(p.queues, packetID)
            p.mu.Unlock()
            return
        }
    }
}

func processGrab(packetID, userID string) GrabResult {
    // 实际的抢红包逻辑
    fmt.Printf("处理抢红包: packet=%s, user=%s\n",
        packetID, userID)
    return GrabResult{Success: true, Amount: 100}
}

通过为每个红包创建独立的队列,将并发请求转换为串行处理。同一个红包的所有抢夺请求在同一个协程中串行执行,消除了数据库层面的锁竞争。

7.4.2 预拆分与 Redis 缓存

红包金额在发红包时就预先拆分好,存储在 Redis 中:

import redis
import json

class RedPacketCache:
    """红包缓存层,使用 Redis 管理红包状态"""

    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client

    def create_packet(
        self, packet_id: str, amounts: list, expire_seconds: int = 86400
    ):
        """创建红包:将预拆分的金额存入 Redis"""
        key = f"rp:{packet_id}"

        # 使用 List 存储预拆分的金额
        pipe = self.redis.pipeline()
        for amount in amounts:
            pipe.rpush(key, amount)
        pipe.expire(key, expire_seconds)

        # 记录已抢用户集合
        grab_key = f"rp_grabbed:{packet_id}"
        pipe.expire(grab_key, expire_seconds)
        pipe.execute()

    def try_grab(self, packet_id: str, user_id: str):
        """
        尝试抢红包
        使用 Redis 原子操作保证并发安全
        返回 (success, amount)
        """
        grab_key = f"rp_grabbed:{packet_id}"

        # 检查是否已抢过(幂等性)
        if self.redis.sismember(grab_key, user_id):
            return False, 0

        # 原子操作:弹出一个金额
        key = f"rp:{packet_id}"
        amount = self.redis.lpop(key)

        if amount is None:
            # 红包已抢完
            return False, 0

        # 记录已抢用户
        self.redis.sadd(grab_key, user_id)

        return True, int(amount)

7.5 异步转账

抢红包成功后,资金的实际转账是异步完成的。这是春节红包能扛住峰值的关键设计:

这种设计将用户体验关键路径(抢到红包的即时反馈)与资金处理路径(实际到账)解耦。用户看到的”已到账”信息可能比实际资金到账早几秒到几分钟,但这个延迟在用户可接受的范围内。

7.6 春节红包的容量规划

维度 日常 春节峰值 扩容策略
接入层 QPS 百万级 千万级 弹性扩容,提前 2 周部署
Redis 集群 数百节点 数千节点 提前扩容,峰值后缩容
数据库 常规分片 额外只读副本 读写分离,异步写入
带宽 常规 10 倍以上 运营商专线预留
人员 常规值班 全员战备 除夕夜全公司待命

八、微信的容灾与多活架构

8.1 SET 化架构

微信采用 SET 化(单元化)架构来实现容灾和扩展。SET 化的核心思想是:将用户按照某种规则划分到不同的 SET(单元)中,每个 SET 包含完整的服务和数据,可以独立处理该 SET 内用户的所有请求

graph TB
    subgraph 路由层
        Router[全局路由服务]
    end

    subgraph SET-A 深圳
        GW_A[网关 A]
        Svc_A[业务服务 A]
        DB_A[(数据 A)]
    end

    subgraph SET-B 上海
        GW_B[网关 B]
        Svc_B[业务服务 B]
        DB_B[(数据 B)]
    end

    subgraph SET-C 深圳
        GW_C[网关 C]
        Svc_C[业务服务 C]
        DB_C[(数据 C)]
    end

    Router --> GW_A
    Router --> GW_B
    Router --> GW_C
    GW_A --> Svc_A
    Svc_A --> DB_A
    GW_B --> Svc_B
    Svc_B --> DB_B
    GW_C --> Svc_C
    Svc_C --> DB_C

用户的 SET 归属由用户 ID 的哈希值决定。路由规则在全局路由服务中维护,所有接入层节点缓存这份路由表。

8.2 SET 化的跨 SET 通信

单聊场景下,发送方和接收方可能属于不同的 SET。微信的处理方式是:

  1. 消息先在发送方所属的 SET 中处理(写入发送方信箱)
  2. 通过跨 SET 的消息转发通道,将消息投递到接收方所属的 SET
  3. 接收方的 SET 负责写入接收方信箱,并通过长连接推送通知

跨 SET 通信使用异步消息队列,保证最终一致性。

8.3 PaxosStore:基于 Paxos 的分布式存储

微信自研的 PaxosStore 是底层数据一致性的基石。它使用 Paxos 共识协议来保证多副本之间的强一致性。

PaxosStore 的关键设计:

/**
 * PaxosStore 写入流程的简化示意
 */
public class PaxosWriter {

    private final List<ReplicaNode> replicas;
    private final int quorum;

    public PaxosWriter(List<ReplicaNode> replicas) {
        this.replicas = replicas;
        // 多数派:3 副本需要 2 个确认
        this.quorum = replicas.size() / 2 + 1;
    }

    public boolean write(String key, byte[] value) {
        // Phase 1: Prepare
        long proposalId = generateProposalId();
        int prepareOk = 0;

        for (ReplicaNode replica : replicas) {
            try {
                boolean accepted = replica.prepare(proposalId);
                if (accepted) prepareOk++;
            } catch (Exception e) {
                // 副本不可用,跳过
            }
        }

        if (prepareOk < quorum) {
            return false;  // Prepare 未达到多数派
        }

        // Phase 2: Accept
        int acceptOk = 0;
        for (ReplicaNode replica : replicas) {
            try {
                boolean accepted = replica.accept(
                    proposalId, key, value
                );
                if (accepted) acceptOk++;
            } catch (Exception e) {
                // 副本不可用,跳过
            }
        }

        return acceptOk >= quorum;
    }

    private long generateProposalId() {
        // 生成全局递增的提案 ID
        // 通常使用时间戳 + 节点 ID 的组合
        return System.nanoTime();
    }
}

8.4 容灾演练

微信定期进行故障演练,验证容灾能力:

九、工程案例:春节红包峰值处理

9.1 背景

2017 年春节是微信红包面临的一次重大考验。根据微信团队在公开技术会议上的分享,这一年的目标是:在除夕零点峰值时刻,系统不降级、不限流,所有用户都能正常收发红包

9.2 面临的挑战

  1. 峰值流量预估困难:用户行为受电视节目(春晚)影响,峰值时间和峰值大小难以精确预测
  2. 资金安全要求:红包涉及真金白银,不允许出现资金差错
  3. 全链路压力:红包涉及消息推送、支付、存储等多个系统的联动
  4. 准备时间有限:核心优化工作集中在春节前 2-3 个月

9.3 解决方案

微信团队从以下几个方面入手:

9.3.1 资源预备

提前 2 个月开始扩容:

9.3.2 系统优化——请求合并

针对群红包的场景,同一个群里几十甚至上百人同时抢一个红包,会产生大量并发请求。优化方案是请求合并

/**
 * 请求合并器:将短时间内对同一红包的多次请求合并处理
 * 减少对下游存储的并发访问
 */
public class RequestMerger {

    // 合并窗口:5 毫秒
    private static final long MERGE_WINDOW_MS = 5;

    // 每个红包的请求缓冲区
    private final ConcurrentHashMap<String, RequestBuffer> buffers
        = new ConcurrentHashMap<>();

    public CompletableFuture<GrabResult> submit(GrabRequest request) {
        String packetId = request.getPacketId();
        CompletableFuture<GrabResult> future = new CompletableFuture<>();

        RequestBuffer buffer = buffers.computeIfAbsent(
            packetId,
            k -> new RequestBuffer(k, this::processBatch)
        );

        buffer.add(request, future);
        return future;
    }

    /**
     * 批量处理同一个红包的多个抢夺请求
     */
    private void processBatch(
            String packetId,
            List<Pair<GrabRequest, CompletableFuture<GrabResult>>> batch) {

        // 一次性从 Redis 获取红包状态
        RedPacketState state = redisClient.getState(packetId);

        // 已抢完的请求直接返回失败
        if (state.getRemainCount() <= 0) {
            for (var pair : batch) {
                pair.getSecond().complete(
                    new GrabResult(false, 0, "红包已抢完")
                );
            }
            return;
        }

        // 过滤已抢过的用户
        Set<String> grabbedUsers = redisClient.getGrabbedUsers(packetId);
        int successCount = 0;

        for (var pair : batch) {
            GrabRequest req = pair.getFirst();
            CompletableFuture<GrabResult> future = pair.getSecond();

            if (grabbedUsers.contains(req.getUserId())) {
                future.complete(
                    new GrabResult(false, 0, "已抢过")
                );
                continue;
            }

            if (successCount >= state.getRemainCount()) {
                future.complete(
                    new GrabResult(false, 0, "红包已抢完")
                );
                continue;
            }

            // 执行抢红包逻辑
            long amount = redisClient.popAmount(packetId);
            if (amount > 0) {
                redisClient.recordGrab(packetId, req.getUserId());
                future.complete(new GrabResult(true, amount, ""));
                successCount++;
            } else {
                future.complete(
                    new GrabResult(false, 0, "红包已抢完")
                );
            }
        }
    }

    /**
     * 请求缓冲区:在合并窗口内收集请求
     */
    static class RequestBuffer {
        private final String packetId;
        private final BiConsumer<String,
            List<Pair<GrabRequest, CompletableFuture<GrabResult>>>>
            processor;
        private final List<Pair<GrabRequest,
            CompletableFuture<GrabResult>>> pending
            = new ArrayList<>();
        private ScheduledFuture<?> flushTask;

        RequestBuffer(String packetId,
                BiConsumer<String,
                    List<Pair<GrabRequest,
                        CompletableFuture<GrabResult>>>> processor) {
            this.packetId = packetId;
            this.processor = processor;
        }

        synchronized void add(GrabRequest request,
                CompletableFuture<GrabResult> future) {
            pending.add(Pair.of(request, future));
            if (flushTask == null) {
                flushTask = scheduler.schedule(
                    this::flush,
                    MERGE_WINDOW_MS,
                    TimeUnit.MILLISECONDS
                );
            }
        }

        synchronized void flush() {
            if (pending.isEmpty()) return;
            List<Pair<GrabRequest,
                CompletableFuture<GrabResult>>> batch
                = new ArrayList<>(pending);
            pending.clear();
            flushTask = null;
            processor.accept(packetId, batch);
        }
    }
}

9.3.3 降级预案

虽然目标是”不降级”,但必须准备降级预案作为最后的保障:

  1. 柔性降级:红包封面动画简化、减少不必要的附加信息查询
  2. 非核心功能关闭:暂停朋友圈红包分享等非核心功能的写入
  3. 限流兜底:如果 QPS 超过系统极限的 120%,对新发红包进行限流(保证已发红包的正常抢夺)
  4. 熔断保护:对下游依赖(银行接口等)设置熔断,防止级联故障

9.3.4 全链路压测

春节前 1 个月,微信进行了多次全链路压测:

9.4 结果

2017 年除夕夜,微信红包系统实现了零故障目标:

9.5 经验总结

这次实践验证了几个关键原则:

  1. 异步化是扛峰值的核心手段:将资金转账从用户交互路径中分离,极大降低了峰值压力
  2. 请求合并显著减少下游压力:同一红包的并发请求合并后,Redis 操作次数减少 60% 以上
  3. 全链路压测不可替代:单服务压测无法发现跨服务的瓶颈和级联问题
  4. 降级预案必须提前准备并验证:即使最终没有用到,准备过程本身也会暴露系统中的问题

十、十亿级即时通信的设计哲学

10.1 移动优先的技术选型

微信的技术决策始终围绕一个核心约束:移动端是第一优先级。这影响了几乎所有的架构设计:

10.2 简单优于复杂

微信在技术选型上倾向于选择更简单的方案:

10.3 可预测性优于极致性能

微信更看重系统行为的可预测性:

10.4 渐进演进而非推倒重来

微信的架构是在 13 年的时间里逐步演进的,没有任何一次”大重写”:

这种渐进式演进的好处是:每一步的风险都可控,不会因为一次大规模重构而引入系统性风险。

10.5 不同架构方案的取舍对比

设计决策 微信的选择 替代方案 选择原因
消息同步 基于 Sequence 增量拉取 CRDT / OT 实现简单,sequence 天然有序,满足需求
朋友圈信息流 读扩散 写扩散 好友数上限 5000,避免写入放大
数据一致性 Paxos(强一致) Raft / Gossip 支付等场景需要强一致保证
网络协议 自研二进制协议 HTTP/2 / gRPC 移动端省流量,控制粒度更细
红包金额分配 预拆分 + 队列弹出 实时计算 避免高并发下的计算竞争
消息存储 信箱模型 会话模型 天然支持多端同步和离线消息
日志方案 mmap + 压缩 直接文件写入 崩溃安全 + 低延迟

10.6 面向未来的思考

微信的架构仍在持续演进。几个值得关注的方向:

  1. 端到端加密:更多消息类型支持端到端加密,对服务端架构透明
  2. 边缘计算:将部分计算(如消息解密、图片缩略图生成)下沉到边缘节点
  3. AI 集成:智能回复、内容审核等 AI 能力的架构集成
  4. 跨境合规:不同国家和地区的数据驻留和隐私合规要求

这些方向的共同特点是:不会改变核心架构的基本范式,而是在现有架构上叠加新的能力。这也是微信架构”渐进演进”哲学的延续。

上一篇:Uber 架构演进 下一篇:阿里巴巴架构


参考资料

论文

技术分享 / 演讲

开源项目 / 文档

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计百科】架构质量属性:不只是"高可用高性能"

需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。

2026-04-13 · architecture

【系统架构设计百科】告警策略:如何避免"狼来了"

大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。

2026-04-13 · architecture

【系统架构设计百科】复杂性管理:架构的核心战场

系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略


By .