高并发系统设计:从理论到实践
作者:深圳某医疗SaaS公司Python后端开发,日常边听周杰伦写CRUD,最近在啃Rust红宝书试图逃离GIL。
上周五晚上十点半,我还在公司死磕一个线上告警——用户挂号接口在晚高峰期间超时率飙升到了15%。产品经理在钉钉上疯狂@我:“明天就是体检高峰期了,这要是崩了,咱俩一起滚蛋。” 我一边啃着冷掉的猪脚饭,一边盯着Grafana面板上那条刺眼的红色曲线,心里默念:“Python你可争点气啊!”
背景:我们不是淘宝,但也不能崩
我们做的是一套面向民营医院和体检中心的预约调度系统。听起来平平无奇,但去年双11那天(没错,医疗行业也有“大促”,叫“年度体检季”),单日请求量一度冲到 80万+,峰值QPS逼近 2000。更坑的是,我们的核心接口(比如 POST /api/v1/bookings)要同时做:
- 用户身份校验
- 号源库存扣减(涉及Redis + MySQL双写)
- 排班规则校验(医生是否上班、是否超限)
- 第三方医保平台回调(异步但强依赖)
早期版本全用Django ORM一把梭,结果一到高峰期,数据库连接池直接拉爆,运维大哥半夜打电话骂我:“你这代码是拿脚写的吧?”
拆解问题:高并发 ≠ 单纯加机器
很多人以为高并发就是堆服务器,但现实很骨感——资源瓶颈往往藏在最不起眼的地方。我们当时踩了几个经典坑:
- 数据库行锁竞争:多个用户抢同一个医生的号,MySQL InnoDB行锁排队,接口响应时间从200ms飙到3s+
- 同步阻塞调用第三方:医保平台响应慢(平均800ms),直接拖垮整个请求链路
- 缓存击穿:热门医生号源缓存过期瞬间,大量请求穿透到DB
被领导“亲切关怀”之后,我被迫翻开尘封已久的《Designing Data-Intensive Applications》,开始搞点“综合”性的优化。
实战:三层漏斗模型 + 异步化改造
我们的核心思路是:把同步流程拆成“预检 → 扣库存 → 异步处理”三阶段,像筛子一样层层过滤流量。
第一层:Nginx + Lua 限流 & 缓存预热
先在接入层拦掉垃圾流量。用OpenResty写了个简单的Lua脚本,对 /bookings 接口按 user_id 做令牌桶限流(每秒最多3次请求),避免恶意刷号:
location /api/v1/bookings {
access_by_lua_block {
local limiter = require "resty.limit.req"
local limit, err = limiter.new("booking_req", 3, 6) -- burst=6
if not limit then
ngx.log(ngx.ERR, "failed to instantiate a limit.req object: ", err)
return ngx.exit(500)
end
local delay, err = limit:incoming(ngx.var.binary_remote_addr, true)
if not delay then
if err == "rejected" then
return ngx.exit(429) -- Too Many Requests
end
return ngx.exit(500)
end
}
proxy_pass http://backend;
}
吐槽:运维说Lua脚本难维护,但比起半夜被call醒,我觉得香多了。
第二层:Redis原子操作 + 库存预占
最关键的是号源库存扣减。我们放弃“先查再减”的天真做法,改用 Redis Lua脚本保证原子性:
# booking_service.py
import redis
r = redis.StrictRedis()
def try_reserve_slot(doctor_id: str, count: int = 1) -> bool:
"""
Lua脚本确保:库存足够才扣减,且不会超卖
"""
lua_script = """
local stock = tonumber(redis.call('GET', KEYS[1]))
if not stock or stock < tonumber(ARGV[1]) then
return 0
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1
"""
key = f"slot_stock:{doctor_id}"
return bool(r.eval(lua_script, 1, key, count))
同时,每天凌晨用Airflow任务预热第二天的号源到Redis,TTL设为25小时,基本杜绝缓存击穿。
第三层:Celery异步解耦
把医保回调、短信通知这些非核心逻辑扔给Celery。用 Redis作为Broker(比RabbitMQ轻量,适合我们这种小团队),关键配置:
# celery_config.py
broker_url = "redis://localhost:6379/1"
result_backend = "redis://localhost:6379/2"
task_serializer = "json"
accept_content = ["json"]
task_acks_late = True # 任务失败重试
worker_prefetch_multiplier = 1 # 避免worker堆积任务
现在主接口只需150ms就能返回“预约成功”,后续动作全部异步。测试同学终于不用在提测时抱怨“回调没触发”了(虽然他们开始抱怨“异步任务监控看不到”……)。
效果对比:从崩坏到稳如老狗
上线两周后的压测数据(Locust模拟2000 QPS):
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 平均响应时间 | 1200ms | 180ms |
| 错误率 | 12.3% | 0.2% |
| DB CPU使用率 | 95% | 40% |
| 第三方调用阻塞 | 是 | 否 |
最爽的是上周体检高峰日——系统平稳扛住 2500 QPS,而我在工位上悠闲地听《七里香》,顺便给新来的实习生安利Rust:“你看,如果用Actix-web重写这个服务,可能连异步队列都省了……”
心得:高并发不是银弹,是权衡的艺术
- 别迷信微服务:我们尝试过拆Booking Service,结果分布式事务搞到头秃,最后还是用本地事务+补偿机制搞定。
- 监控比代码重要:接入Prometheus + Grafana后,才发现80%的延迟来自DNS解析(内网居然没配Hosts!)。
- Python依然能打:用好asyncio + uvicorn + redis-py,配合合理的架构,Python在IO密集型场景完全够用。当然,CPU密集型任务?我建议你看看隔壁Rust同学……
最后:致所有被高并发折磨的同行
写这篇文章时,耳机里正放着《稻香》。想起去年双11凌晨三点,我和运维蹲在机房吃泡面,看着负载曲线慢慢降下来,他说:“你这Python代码,勉强能看了。”
其实哪有什么银弹,不过是把每个环节抠到极致,再加一点运气。如果你也在深圳,欢迎来南山咖啡厅找我吹水——带上你的Rust学习笔记,我请你喝喜茶(报销额度还剩200块)。
技术分享的本质,不是炫耀多牛,而是告诉后来者:这条路,有人走过,坑我都替你踩了。
注:文中所有方案均已在生产环境稳定运行6个月以上。如有雷同,纯属我们都被产品经理逼过。

评论 0