大家好,我是老王。
在交易和支付系统摸爬滚打这么多年,我见过太多“架构设计很丰满,大促上线直接瘫痪”的惨案。很多初学者聊高并发,开口就是 Redis、MQ、微服务分表分库三板斧。但实际上,高并发架构的本质不是组件的堆叠,而是对系统资源的精细化调度和对“失败”的极致防御。
今天老王结合以前踩过的几个分布式大坑,跟大家系统聊聊后端高并发架构的设计逻辑。中英文我都留了空格,方便大家阅读。
一、 核心底逻辑:高并发下的因果链
搞架构一定要懂因果链。系统在高并发下崩溃,从来不是瞬间发生的,它有一条极其清晰的传导路径:
因果链: 突发流量入流量超出设计上限 $\to$ 系统争抢 CPU / 内存 / IO 资源 $\to$ 数据库行锁竞争加剧或连接池耗尽 $\to$ 请求响应延迟(RT)飙升 $\to$ 上游调用方超时重试 $\to$ 流量呈指数级放大 $\to$ 线程池被打满,级联雪崩 $\to$ 整个集群不可用。
我们的架构设计,说白了就是在上面这条因果链的每一个环节“设卡下闸”,把灾难拦截在萌芽状态。
二、 流量入口层:削峰与无状态隔离
1. 真实故障案例:瞬时击穿网关
当年做秒杀系统,运营临时加了一场活动,前端没做答题验证和防刷,十几万 QPS 瞬间涌向后端。我们虽然加了 Nginx 集群,但由于后端 Gateway 做鉴权时强依赖了一个单点 Session 服务,导致鉴权服务 CPU 瞬间 100%,网关直接报 504 Gateway Timeout。
2. 解法:动静分离 + 令牌桶限流
- 网关无状态:鉴权采用轻量级 JWT 本地校验,严禁在网关层做同步 RPC 远程调用。
- 入口限流:单机限流采用 Token Bucket(令牌桶)算法,平滑突发流量。
这里给一个 Golang 常用的基于 x/time/rate 的局部中间件示例:
package middleware
import (
"net/http"
"golang.org/x/time/rate"
)
// 每秒生成 2000 个令牌,桶容量 5000
var limiter = rate.NewLimiter(2000, 5000)
func RateLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
// 快速失败,不要让请求挂起消耗 Goroutine
http.Error(w, `{"code": 429, "msg": "Too Many Requests"}`, http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
三、 核心业务层:缓存设计与一致性深坑
在订单和支付链路中,“直接读写 DB” 就是自杀。但用了 Redis,分布式一致性的坑才刚开始。
1. 真实故障案例:库存超卖与热点 Key 击穿
早期我们扣减库存直接走 MySQL:
UPDATE tb_inventory SET stock = stock - 1 WHERE sku_id = 1001 AND stock > 0;
在并发只有几百时一切正常。大促当天,单个热点 SKU 涌入上万并发,MySQL 针对这单行记录加排他锁,行锁竞争导致事务排队严重,DB 连接池 2 秒内被打满。
2. 解法:Redis + Lua 原子预扣减 + MQ 异步落库
将热点数据前置到 Redis 中,通过 Lua 脚本保证“查询 + 扣减”的原子性:
-- KEYS[1]: 缓存中库存的 key (如: item:stock:1001)
-- ARGV[1]: 扣减数量 (如: 1)
local stock = redis.call('get', KEYS[1])
if not stock then
return -1 -- 缓存未预热
end
local stockNum = tonumber(stock)
local deductNum = tonumber(ARGV[1])
if stockNum < deductNum then
return 0 -- 库存不足
end
redis.call('decrby', KEYS[1], deductNum)
return 1 -- 扣减成功
扣减成功后,立即向 RocketMQ 发送一条扣减流水消息,由消费端异步落库,实现数据库层面的物理削峰。
老王提醒: 这里的因果链是:
同步阻塞写 DB -> 锁竞争 + 连接池占满,改成内存原子扣减 -> 异步顺序落库,彻底斩断了数据库长事务的链条。
四、 数据持久层:分库分表与异步最终一致
对于交易和支付系统,高并发下的数据库设计必须遵守两个原则:分治与最终一致。
+--------------------+
| Client Request |
+---------+----------+
|
v
+--------------------+
| API / Order Svc |
+---------+----------+
|
+------------+------------+
| (1. 写本地事务) | (2. 投递事务消息)
v v
+------------------+ +-------------------+
| MySQL (分库分表) | | RocketMQ |
| 状态: PAYING | +---------+---------+
+------------------+ |
v (3. 异步订阅)
+-------------------+
| Accounting Svc |
| (记账/积分/通知) |
+-------------------+
- 分库分表:根据
user_id进行 Hash 取模分片,避免单表数据量突破千万带来的 B+ Tree 深度增加和 IO 压力。对于商家维度的查询,采用 Canal 监听 Binlog,将数据异构同步到 Elasticsearch。 - 拒绝分布式大事务:严禁在支付核心链路中使用 2PC/XA 等强一致性强锁方案。采用 本地消息表 或 RocketMQ 事务消息 实现 Base 理论的最终一致性。
五、 容灾与兜底:熔断、降级与线程池隔离
任何依赖网络和下游的调用,都必须做好“随时会死”的准备。
1. 线程池与信号量隔离(Bulkhead 舱壁模式)
在 Java 体系中,Spring Boot 默认使用的是全局 Tomcat 线程池。如果你调用第三方支付网关的接口卡死,整个 Tomcat 的 200 个线程会迅速被占满,导致哪怕是访问一个静态页面的请求也会被拒绝。
// Java 示例:对非核心下游使用独立线程池隔离
@Configuration
public class ThreadPoolConfig {
@Bean("orderNoticeExecutor")
public ThreadPoolExecutor orderNoticeExecutor() {
return new ThreadPoolExecutor(
10,
20,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500), // 必须是有界队列
new ThreadFactoryBuilder().setNameFormat("order-notice-%d").build(),
new ThreadPoolExecutor.DiscardOldestPolicy() // 明确拒绝策略
);
}
}
2. 自动化降级
结合 Sentinel / Resilience4j:
- 一旦下游服务 RT > 500ms 比例超过 30%,立刻触发熔断。
- 熔断后直接走 Local Fallback(返回 Mock 数据或空结果),绝不把压力传导给下游,也不让下游拖垮自己。
六、 老王总结
架构设计没有银弹,本质是取舍(Trade-off)。
你在做高并发架构时,不妨在白板上画出系统的因果链,问自己四个问题:
- 流量洪峰来了,哪个单点最先被打崩?
- 如果 Redis 突然挂掉,DB 会不会瞬间跟着死?
- 第三方支付通道 RT 变成 10 秒,我的订单系统会不会被拖死?
- 如果数据出现不一致,我有没有补偿和核对机制?
把这四个问题的漏洞补上,你的系统才算真正经得起生产环境的风吹雨打。
大家在并发设计上踩过哪些酸爽的坑?欢迎在评论区留言交流!
许可协议:CC BY-NC 4.0
更新于 1 小时前
觉得文章有帮助?点个赞吧!
0 条评论


