高并发系统设计:从理论到实践

半杯咖啡写代码
2025-12-23 07:58
阅读 471

上周五晚上十点半,我还在公司死磕一个接口超时的线上问题。产品明天就要给客户演示新功能,结果压力一上来,后端直接502。运维大哥在群里@我:“兄弟,你这Python服务撑不住啊,再崩一次我就要报警了。”

我叹了口气,默默泡了杯速溶咖啡——这已经是我入职三个月来第三次被高并发“教育”了。作为一个普通一本CS专业的大四学生,靠着秋招侥幸拿了个中厂offer,本以为能安稳摸鱼到毕业,没想到刚进组就被安排重构核心下单链路。更惨的是,现在一边上班,一边还得偷偷刷LeetCode准备跳槽(别问,问就是大环境不好),每天8点准时起床,凌晨1点才睡,感觉自己快成永动机了。

今天这篇博客,就是想和大家聊聊我在实战中踩过的那些高并发坑。不讲教科书式的八股文,就说说前端请求怎么打爆后端Python服务如何扛住流量洪峰,以及为什么你以为的“小优化”其实根本没用


一开始,我以为加个缓存就完事了

刚接手项目时,老同事甩给我一句:“这个接口QPS峰值能到3k,你看着办。” 我心想,不就是个查询接口嘛,Redis缓存+连接池,分分钟搞定。

结果上线第一天,监控告警炸了:CPU飙到95%,响应时间从50ms暴涨到2s+。我一脸懵逼,打开日志一看,全是 Too many connectionsRedis timeout。原来我光顾着缓存数据,却忘了前端发请求的方式有多“野”。

我们的前端是Vue3 + Axios,产品经理为了“用户体验流畅”,在商品详情页疯狂轮询库存接口,每2秒一次。用户一多,瞬间几千个长轮询压过来。更要命的是,前端没做防抖,页面切换时旧请求还在发,导致大量无效请求堆积。

教训一:高并发不是后端一个人的事,前端也是关键一环。

后来我和前端同学开了个紧急对齐会(其实就是一起骂产品经理),做了三件事:

  1. 前端加防抖 + 取消重复请求:用 Axios 的 CancelTokenAbortController 干掉未完成的旧请求。
  2. 降低轮询频率:从2秒改成5秒,并加上“页面可见性检测”(document.visibilityState),用户切走标签页就暂停轮询。
  3. 引入WebSocket替代轮询:对于实时性要求高的场景(比如秒杀倒计时),直接推,不拉。

改完之后,无效请求减少70%,后端压力肉眼可见地降下来了。


Python真的扛不住高并发吗?

很多人一提高并发就摇头:“Python?GIL锁死,别想了,换Go吧。” 刚开始我也信了,差点跪求领导换语言。但现实是——我们没时间重写,只能在现有Python栈上优化。

我们的服务用的是 Flask + Gunicorn + Gevent,数据库是 MySQL + Redis。经过几次压测和线上复盘,我发现瓶颈根本不在GIL,而在I/O等待数据库连接管理

1. 异步非阻塞是救命稻草

Flask 默认是同步的,每个请求占一个线程。当遇到数据库慢查询或外部API调用,线程就卡住了。虽然Gunicorn可以开几十个worker,但内存和上下文切换成本太高。

解决方案:用 Gevent 打猴子补丁(monkey patch),把标准库的阻塞 I/O 替换成协程友好的版本。

# gunicorn.conf.py
from gevent import monkey
monkey.patch_all()  # 必须放在最前面!

bind = "0.0.0.0:8000"
workers = 4
worker_class = "gevent"
worker_connections = 1000  # 每个worker支持1000个协程

注意:monkey.patch_all() 必须在导入任何其他模块前执行,否则可能出诡异bug。我第一次部署时忘放第一行,结果Redis连接池死锁,线上服务挂了半小时……运维看我的眼神像在看罪犯。

2. 数据库连接池别乱配

以前图省事,每次请求都新建数据库连接:

# 错误示范!
def get_user(user_id):
    conn = pymysql.connect(...)  # 每次新建
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users WHERE id=%s", (user_id,))
    return cursor.fetchone()

高峰期直接把MySQL max_connections 耗尽。后来改用 SQLAlchemy 的连接池:

from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool

engine = create_engine(
    "mysql+pymysql://...",
    poolclass=QueuePool,
    pool_size=20,        # 常驻连接数
    max_overflow=30,     # 最多可超出30个
    pool_pre_ping=True,  # 每次取连接前ping一下,避免断连
)

配合 Gevent,单个worker能轻松处理上千并发请求。


架构层面:分层拆解,别让一个接口背负全世界

我们最初的设计很“朴素”:一个 /order/create 接口,干了所有事——校验库存、扣减库存、生成订单、发MQ、记录日志、调风控……结果一压测,稍微有点流量,整个链路就雪崩。

后来学乖了,搞异步化 + 服务拆分

  • 核心路径只做最关键的事:校验 + 扣库存 + 写DB(保证原子性)
  • 非核心操作扔到消息队列:比如发短信、更新推荐算法、同步ERP,全部通过 RabbitMQ/Kafka 异步处理
  • 读写分离:查询类接口走从库,写操作走主库

这里有个细节:扣库存必须和创建订单在一个事务里,否则可能出现“库存扣了但订单没建”的资损问题。我们用的是 Redis Lua 脚本保证原子性:

-- check_and_decr_stock.lua
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) >= tonumber(ARGV[1]) then
    redis.call('DECRBY', KEYS[1], ARGV[1])
    return 1
else
    return 0
end

Python 调用:

result = redis.eval(lua_script, 1, f"stock:{product_id}", quantity)
if not result:
    raise InsufficientStockError()

Lua 脚本在Redis内部执行,天然原子,比先GET再DECR安全多了。


安全意识:高并发下的隐藏雷区

很多人只关注性能,却忽略了高并发场景下的安全风险。举两个真实案例:

1. 缓存击穿打垮DB

某个热门商品上架,缓存过期瞬间,几千个请求同时穿透到DB查库存。DB CPU飙升,连带其他服务一起慢。

防御方案

  • 缓存永不过期(靠后台任务主动刷新)
  • 或者用互斥锁(mutex key):第一个请求发现缓存失效,加锁去查DB并回填,其他请求等它完成
def get_product_with_mutex(product_id):
    cache_key = f"product:{product_id}"
    product = redis.get(cache_key)
    if product:
        return json.loads(product)
    
    # 尝试获取锁
    lock_key = f"lock:{product_id}"
    if redis.set(lock_key, "1", nx=True, ex=5):  # 5秒自动释放
        try:
            product = db.query(...)  # 查DB
            redis.setex(cache_key, 300, json.dumps(product))
            return product
        finally:
            redis.delete(lock_key)
    else:
        # 没拿到锁,短暂等待后重试(或返回旧数据/默认值)
        time.sleep(0.1)
        return get_product_with_mutex(product_id)

2. 接口被恶意刷

有一次,竞争对手用脚本疯狂调我们的优惠券领取接口,每秒几千次。虽然业务上做了“每人限领1张”,但验证逻辑在应用层,请求还是打到了后端。

解决方案

  • Nginx 层限流:按IP或用户ID限制QPS
  • 验证码兜底:异常流量触发图形验证码
# nginx.conf
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

server {
    location /coupon/grab {
        limit_req zone=api burst=20 nodelay;
        proxy_pass http://backend;
    }
}

这样,即使应用层有漏洞,Nginx也能挡掉大部分洪水。


效果对比:优化前后数据说话

指标 优化前 优化后 提升幅度
P99 响应时间 2100 ms 85 ms 96% ↓
错误率 (5xx) 8.3% 0.05% 99% ↓
单机 QPS 300 2800 833% ↑
DB 连接数峰值 480 65 86% ↓

最爽的是上周双11大促,我们系统稳如老狗。运维大哥在群里发了个红包:“Python小哥牛逼!” ——那一刻,我觉得早起刷题、熬夜调优都值了。


最后一点真心话

高并发系统设计,从来不是堆技术名词,而是在约束条件下做权衡。我们没有无限服务器,没有完美架构,只有不断迭代的代码和一次次线上救火的经验。

作为即将跳槽的应届生,我越来越意识到:能写出高性能代码的人很多,但能设计出高可用、易维护、安全可靠的系统的,才是真正的工程师

如果你也在为高并发头疼,不妨从这几点入手:

  • 先看前端请求是否合理
  • 再查数据库和缓存有没有成为瓶颈
  • 然后考虑异步化和服务拆分
  • 最后,别忘了安全兜底

共勉。我要去刷今天的LeetCode了——毕竟,下一份offer,还得靠硬实力。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝