架构设计原则:高可用架构模式

聊到交易系统的稳定性,我第一个想说的就是架构设计原则。这些原则不是书本上飘着的理论,而是我在一次次线上故障中「摔」出来的经验。今天咱们就掰开揉碎,把高可用架构模式、无状态设计、异步解耦这些核心概念讲透。

核心观点:交易系统的架构设计,本质上是在「可用性」和「成本」之间找平衡。没有银弹,只有取舍。

一、高可用架构模式:主备、多活、单元化

这三种模式,我习惯把它们看作「高可用的三个台阶」。你想想看,从单机到分布式,每一步都是在跟「单点故障」较劲。

1. 主备模式

这是最朴素的高可用方案。一台主节点干活,一台备节点 standby。主节点挂了,备节点顶上。

我在项目中遇到过一个问题:主备切换时,数据同步没做完,导致丢了几笔订单。后来我们加了「半同步复制」,才算踏实。

我的建议:主备模式适合业务量不大、对一致性要求高的场景。但别指望它扛住大流量,备机平时闲着也是浪费。

2. 多活模式

多活就不一样了。多个节点同时提供服务,流量分摊,一个挂了,其他的继续扛。说白了,就是「鸡蛋不放在一个篮子里」。

但多活有个坑——数据冲突。我记得有一次,两个机房同时修改同一笔订单的状态,结果乱套了。后来我们引入了「全局时钟」和「冲突检测机制」,才算搞定。

注意:多活不是简单的「多部署几台机器」。你得考虑数据一致性、流量调度、故障隔离。搞不好,多活比单机还容易出问题。

3. 单元化架构

单元化是我个人比较推崇的模式。它把用户分成一个个「单元」,每个单元独立部署、独立存储。一个单元挂了,只影响该单元的用户,其他单元照常运行。

我曾经参与过一个支付系统的单元化改造。我们把用户按 ID 哈希分到 16 个单元,每个单元有独立的数据库和缓存。结果呢?一次机房断电,只影响了 1/16 的用户,其他用户完全无感。

关键点:单元化的核心是「隔离」。隔离做得好,故障范围就小。但代价是运维复杂度上升,你得管理几十个甚至上百个单元。

二、无状态设计

无状态设计,说白了就是「别把状态留在服务器上」。每次请求都是独立的,服务器不记得你是谁。

为什么这么做?因为无状态的服务可以随便扩缩容。你想想看,如果服务器 A 存了用户的登录状态,那用户下次请求必须还得落到 A 上。这不就绑死了吗?

我的做法:把状态扔到外部存储里。比如用 Redis 存 Session,用数据库存业务数据。服务器只负责计算,不负责记忆。

但这里有个坑——外部存储本身也可能成为瓶颈。我曾经见过一个系统,所有请求都去 Redis 拿 Session,结果 Redis 扛不住,整个系统都慢了。后来我们加了本地缓存 + 异步同步,才算缓解。

避坑指南:无状态设计不是「完全不存状态」,而是「把状态集中管理」。你要考虑状态存储的高可用和性能。否则,无状态反而成了「有状态」的另一种形式。

三、异步解耦

异步解耦,是我在交易系统里最常用的手段。为什么?因为交易链路太长了。一笔订单从下单到支付,可能要经过十几个服务。如果每个服务都同步调用,那延迟和故障率都会爆炸。

异步解耦的核心是「消息队列」。生产者只管发消息,消费者只管处理消息。两者互不依赖。

// 伪代码示例:异步解耦
// 订单服务
public void createOrder(Order order) {
    // 1. 保存订单
    orderRepository.save(order);
    // 2. 发送消息到队列
    messageQueue.send("order.created", order);
}

// 支付服务
@MessageListener(topic = "order.created")
public void handleOrderCreated(Order order) {
    // 处理支付逻辑
    paymentService.process(order);
}

我在项目中遇到过一个问题:消息队列挂了,订单创建成功了,但支付没触发。后来我们加了「本地消息表」——先把消息存到数据库,再异步发送到队列。这样即使队列挂了,也能重试。

核心原则:异步解耦不是「不管了」,而是「把耦合点转移到消息队列上」。你要保证消息不丢、不重复、不乱序。这三点,每一点都是坑。

四、限流降级熔断

这三个词经常一起出现,但它们的侧重点不同。我习惯这么理解:

  • 限流:控制流量入口,别让系统被冲垮。
  • 降级:牺牲非核心功能,保住核心功能。
  • 熔断:发现下游挂了,主动切断调用,避免雪崩。

1. 限流

限流的算法有很多:令牌桶、漏桶、滑动窗口。我个人比较喜欢令牌桶,因为它能应对突发流量。

// 令牌桶限流示例
RateLimiter limiter = RateLimiter.create(1000); // 每秒1000个令牌
if (limiter.tryAcquire()) {
    // 处理请求
} else {
    // 返回限流提示
    return "系统繁忙,请稍后再试";
}

但限流有个问题——阈值设多少?设小了,正常流量被误杀;设大了,又起不到保护作用。我建议先压测,摸清系统的真实容量,再留 20% 的余量。

2. 降级

降级是「有舍有得」。比如双十一大促,可以把「历史订单查询」降级掉,只保留「当前订单处理」。用户虽然查不到历史订单,但至少能正常下单。

我曾经犯过一个错:降级策略没做灰度,一上线就把所有非核心功能全关了。结果用户投诉说「怎么连订单详情都看不到了?」——嗯,后来我们加了「按用户等级降级」的逻辑。

3. 熔断

熔断是「主动止损」。比如调用支付服务,连续失败 10 次,就熔断 30 秒。这 30 秒内,所有请求直接返回失败,不再调用下游。

我的经验:熔断的阈值和恢复时间要动态调整。我见过一个系统,熔断阈值设得太低,稍微抖动一下就熔断了,反而加剧了故障。后来我们用了「自适应熔断」——根据失败率动态调整阈值。

五、超时与重试策略

超时和重试,看起来简单,但坑特别多。我一个个说。

1. 超时策略

超时时间设多少?设短了,正常请求被误杀;设长了,线程被拖死。我一般建议:

  • 内部调用:50ms - 200ms
  • 外部依赖:500ms - 2s
  • 数据库查询:1s - 5s

但这不是绝对的。你得根据实际响应时间动态调整。我习惯用「P99 响应时间」作为参考,超时时间设为 P99 的 2 倍。

2. 重试策略

重试不是「失败了就再来一次」。你得考虑:

  • 幂等性:重试的请求,必须保证结果一致。否则重复扣款就麻烦了。
  • 退避策略:别一失败就立刻重试。用指数退避,比如 1s、2s、4s、8s。
  • 重试次数:一般 3 次就够了。再多就是浪费资源。

避坑指南:我曾经见过一个系统,重试策略没做限制,结果一个请求重试了 100 次,把下游打挂了。记住:重试是「最后的保险」,不是「万能药」。

知识体系总览

下面这张图,是我梳理的本章知识结构。你可以把它当作一张「地图」,随时回来对照。

交易系统稳定性建设 - 架构设计原则 架构设计原则 高可用架构模式 主备模式 多活模式 单元化架构 无状态设计 状态外置(Redis/DB) 水平扩缩容 异步解耦 限流降级熔断、超时重试策略为横向支撑能力

总结一下:架构设计原则不是孤立的。高可用模式决定了系统的「骨架」,无状态设计让骨架「灵活」,异步解耦让骨架「松」,限流降级熔断和超时重试则是「保护层」。把这五层都做好了,交易系统才算真正「稳」了。

嗯,今天就聊到这儿。这些原则说起来简单,但真正落地的时候,每个点都能挖出不少坑。后面几章,我会结合具体的案例,把每个原则的「坑」和「填坑方法」都讲清楚。


专注资料整理