架构设计原则:高可用架构模式
聊到交易系统的稳定性,我第一个想说的就是架构设计原则。这些原则不是书本上飘着的理论,而是我在一次次线上故障中「摔」出来的经验。今天咱们就掰开揉碎,把高可用架构模式、无状态设计、异步解耦这些核心概念讲透。
核心观点:交易系统的架构设计,本质上是在「可用性」和「成本」之间找平衡。没有银弹,只有取舍。
一、高可用架构模式:主备、多活、单元化
这三种模式,我习惯把它们看作「高可用的三个台阶」。你想想看,从单机到分布式,每一步都是在跟「单点故障」较劲。
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 次,把下游打挂了。记住:重试是「最后的保险」,不是「万能药」。
知识体系总览
下面这张图,是我梳理的本章知识结构。你可以把它当作一张「地图」,随时回来对照。
总结一下:架构设计原则不是孤立的。高可用模式决定了系统的「骨架」,无状态设计让骨架「灵活」,异步解耦让骨架「松」,限流降级熔断和超时重试则是「保护层」。把这五层都做好了,交易系统才算真正「稳」了。
嗯,今天就聊到这儿。这些原则说起来简单,但真正落地的时候,每个点都能挖出不少坑。后面几章,我会结合具体的案例,把每个原则的「坑」和「填坑方法」都讲清楚。