大家好,我是老王。
聊到高并发,很多年轻同学第一反应往往是加机器、上微服务、甚至搞些花哨的中间件。但干过十几年订单和支付系统的老兵都知道:架构的本质不是为了炫技,而是用最小的代价解决系统的核心瓶颈,并在极端场景下保住系统的底线。
今天老王结合当年在大促期间踩过的几个大坑,从因果链的视角,梳理一套经过实战检验的高并发演进思路。
一、 缓存层:别让一条热点 Key 拖垮整个支付链路
很多团队做秒杀或高并发读,第一反应是:“加 Redis 完事”。但如果你没做好精细化防护,缓存不仅救不了命,还会变成压垮系统的第一块多米诺骨牌。
真实故障复盘:一次热点失效引发的级联雪崩
那年双十一前夕做预热,运营临时上线了一个爆款秒杀单品。由于配置失误,缓存过期时间被统一设成了 5 分钟后整点失效。 因果链如下:
瞬时 QPS 达到 8W -> 5 分钟到期,热点 Key 瞬间失效 -> 8W 流量全量穿透到 MySQL -> 数据库连接池瞬间打满 -> 活跃线程暴增触发排队 -> 依赖同一个 DB 的支付回调接口全部超时 -> 支付系统报警炸锅,订单状态同步停滞。
解法演进
- 过期时间加随机抖动:避免大规模集中失效。
- 互斥锁加载(Mutex Lock):查缓存 Miss 时,只允许一个线程去查 DB 并回填,其余请求重试等待。
- 多级缓存(LocalCache + Redis):在 Go 里可以用
sync.Map或bigcache,在 Java 里可以用Caffeine。先挡掉 90% 穿透到网络层的流量。
二、 核心动线:库存扣减的“锁”演进
订单系统最核心、也是并发冲突最激烈的操作就是“扣减库存”。
1. 传统 DB 悲观锁/乐观锁(瓶颈明显)
最早期大家用 SELECT ... FOR UPDATE(悲观锁),或者 UPDATE stock SET num = num - 1 WHERE id = 1 AND num > 0(乐观锁/行级锁)。
- 因果链:在高并发下,上千个线程同时争抢同一行的行级锁 -> MySQL 锁等待队列堆积 -> 事务执行变慢 -> 连接无法快速释放 -> 最终拖垮整个实例。
2. Redis + Lua 脚本(内存级原子扣减)
为了摆脱 DB 的行锁瓶颈,我们必须把扣减前置到 Redis。 利用 Lua 脚本的原子性,在单线程执行模型下搞定校验与扣除:
-- KEYS[1]: 商品库存 Key (例如: item:stock:1001)
-- ARGV[1]: 扣减数量 (例如: 1)
local stockKey = KEYS[1]
local decrementNum = tonumber(ARGV[1])
local currentStock = tonumber(redis.call('get', stockKey) or '-1')
if currentStock == -1 then
return -1 -- 库存 Key 不存在
end
if currentStock >= decrementNum then
redis.call('decrby', stockKey, decrementNum)
return 1 -- 扣减成功
else
return 0 -- 库存不足
end
在 Go 服务端调用示例如下:
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)
var ctx = context.Background()
func DeductStock(rdb *redis.Client, itemId int64, num int) (bool, error) {
luaScript := `
local stockKey = KEYS[1]
local decrementNum = tonumber(ARGV[1])
local currentStock = tonumber(redis.call('get', stockKey) or '-1')
if currentStock == -1 then
return -1
end
if currentStock >= decrementNum then
redis.call('decrby', stockKey, decrementNum)
return 1
else
return 0
end
`
key := fmt.Sprintf("item:stock:%d", itemId)
res, err := rdb.Eval(ctx, luaScript, []string{key}, num).Int()
if err != nil {
return false, err
}
return res == 1, nil
}
3. 异步落库:削峰填谷
Redis 扣减成功后,并不直接同步写 MySQL,而是发送一条带有唯一业务标识(如 OrderToken)的 MQ 消息,由消费者异步批量落库。
- 因果链:同步变异步 -> 用户请求在毫秒级返回 -> 流量峰值被 MQ 缓冲 -> 下游数据库以平滑恒定的速度(如每秒 2000 TPS)写入 -> DB 始终运行在健康水位。
三、 数据一致性:支付与订单的分布式事务陷阱
很多刚做支付系统的同学总想用两阶段提交(XA)或者死板的 TCC。但真实高并发场景下,分布式事务锁资源的时间太长,吞吐量极低。
在老王经历过的支付系统里,最靠谱的反而是 “本地消息表 + 幂等重试 + 对账”。
链路因果模型:
- 事务内写业务数据与消息:
在同一个本地数据库事务中,保存订单信息,同时往
outbox(本地消息表)写一条待发送消息。由于在同一个 DB 事务内,强一致性由数据库 ACID 保障。 - 异步轮询或监听 Binlog:
独立进程解析 MySQL Binlog(如通过 Canal),或者轮询
outbox表,把消息投递到 MQ。 - 下游消费与幂等防重:
支付服务消费消息时,必须依赖唯一业务单号(如
pay_order_id)做幂等校验:sqlINSERT INTO pay_record (order_id, amount, status) VALUES ('ORD_20231024_01', 9900, 'SUCCESS') ON DUPLICATE KEY UPDATE updated_at = NOW(); - 兜底手段:T+1 对账机制: 不要迷信 100% 的线上无故障。网络丢包、MQ 重复消费、Redis 偶发抖动都会发生。每天凌晨跑批对账脚本,拉取渠道方(微信、支付宝)的账单与本地流水比对,平账、补单、冲正。对账系统才是支付高可用的最终防线。
四、 防御性架构:限流、熔断与降级
在高并发体系中,你要把每一个下游依赖(RPC 服务、数据库、第三方接口)都预设为“随时可能挂掉”。
- 入口限流(Token Bucket / Leaky Bucket): 在网关层做令牌桶限流。宁可对超出承载能力的请求返回 “系统繁忙,请稍后再试”,也绝不能放任未知流量涌入把集群直接打挂。
- RPC 快速失败(Fail-Fast)与熔断:
当下游依赖(例如风控查询服务)出现超时率增加时,熔断器(如 Sentinel 或 Resilience4j)必须立刻打开,直接执行降级逻辑(例如默认放行低风险请求或走极简校验链路)。
- 因果链:下游卡顿 -> 上游线程无法释放 -> 上游 Tomcat/Go Routine 耗尽 -> 整个微服务集群被一个边缘服务拖垮。熔断就是防止这种坏死组织扩散的“截肢手术”。
老王总结
做高并发架构,往往是一个从“单点抗压”走向“分层卸力”的过程:
- 静态数据推向边缘(CDN、多级缓存)。
- 写请求快速校验与原子扣减(Redis + Lua 挡在最前面)。
- 数据落地拉长周期解耦(MQ 削峰、本地消息表驱动)。
- 异常情况保住主干(限流兜底、异步对账兜底)。
没有一劳永逸的银弹,每一处架构升级,都是上一场故障换来的代价。写代码时多想一步“如果这里挂了会引发什么连锁反应”,你的系统就离高可用更近了一步。
License: CC BY-NC 4.0
Updated 13 hours ago
Was this article helpful? Give it a like.
0 comments


