高并发系统设计:从理论到实践(一个Android转Flutter选手的后端血泪史)

RAG小工匠
2025-12-15 01:08
阅读 226

去年双11前两周,我正吭哧吭哧给公司新App做Flutter性能优化——是的,你没看错,我这个三年Android老兵,现在主力写Flutter了。每天早上8点准时打开MacBook,一边喝着速溶咖啡,一边想着“这波跳槽简历怎么写”,结果领导突然把我拉进一个叫“大促支撑系统”的钉钉群。

群里一句话:“这个抢购活动后端接口你来搞一下,QPS预估5万。”
我当时差点把咖啡喷屏幕上:“我一个前端(划掉)跨端开发,你让我写高并发后端?!”

但没办法,小公司人手紧,谁让你会Java又懂点Spring Boot呢?于是,在产品经理第8次改需求、测试同学凌晨三点还在提Bug、运维大哥翻白眼说“服务器预算只有这么多”的背景下,我被迫踏上了高并发系统的“炼狱之旅”。


从爬虫说起:流量是怎么来的?

你以为高并发都是用户自然增长?天真!我们这次活动上线第一天,监控就报警:瞬时QPS飙到8万+,数据库CPU直接干到95%

查日志发现,一半流量来自几个固定的User-Agent,IP段也高度集中。一问安全团队,好家伙,全是爬虫和脚本党!有些甚至用Selenium模拟点击,伪装得跟真用户一模一样。

“你们App不是刚上架吗?哪来这么多机器人?”我一脸懵。
安全同事冷笑:“你家优惠券面值200,门槛50,不抢才怪。”

于是第一课:高并发系统必须考虑恶意流量。我们紧急加了三层防护:

  1. Nginx层限流limit_req_zone 按IP限速
  2. 网关层验证:集成阿里云验证码(虽然用户体验炸了,但保命要紧)
  3. 业务层风控:记录设备指纹 + 行为分析(比如1秒内点10次“立即抢购”?直接拉黑)
# nginx.conf 片段
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

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

后端架构:别再让数据库当炮灰

早期版本,我图省事,直接在Spring Boot里写了个接口:

@Transactional
public Result buy(Long userId, Long itemId) {
    Item item = itemMapper.selectById(itemId);
    if (item.getStock() <= 0) return Result.fail("售罄");
    
    // 扣库存
    itemMapper.decreaseStock(itemId);
    
    // 创建订单
    orderService.createOrder(userId, itemId);
    
    return Result.ok();
}

上线10分钟,MySQL主库直接挂了。死锁、慢查询、连接池耗尽,三件套齐活。运维在群里@我:“兄弟,你这是要送我进ICU?”

痛定思痛,开始重构。核心思路就一条:让数据库尽量少干活,能缓存就缓存,能异步就异步

工具链升级

组件 用途 踩坑点
Redis 库存预热 + 分布式锁 初期用INCR扣库存,超卖了!后来改用Lua脚本保证原子性
RabbitMQ 订单创建异步化 消息堆积时消费者处理不过来,加了死信队列
Sentinel 熔断降级 配置不当导致整个服务雪崩,调参调到凌晨3点

关键代码:Redis Lua脚本扣库存

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

Java调用:

String script = loadLuaScript("check_and_decrease_stock.lua");
Boolean success = redisTemplate.execute(
    (RedisCallback<Boolean>) conn -> {
        Object result = conn.eval(script.getBytes(), 1, 
            ("stock:" + itemId).getBytes());
        return (Long) result == 1;
    }
);

自嘲一下:以前写Android只知道Handler.post(),现在写Lua脚本比写Dart还溜……


数据库设计:别再用自增ID了!

初期订单表主键是BIGINT AUTO_INCREMENT。结果压测时发现,MySQL的InnoDB在高并发插入下,自增锁会成为瓶颈

解决方案:

  • 改用雪花算法(Snowflake) 生成分布式ID
  • 库存字段加版本号乐观锁
  • 热点商品分库存储(比如iPhone单独一张表)
CREATE TABLE seckill_order (
    id BIGINT NOT NULL COMMENT '雪花ID',
    user_id BIGINT NOT NULL,
    item_id BIGINT NOT NULL,
    version INT DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE KEY uk_user_item (user_id, item_id)
) ENGINE=InnoDB;

顺便吐槽:产品经理非要加“用户可重复购买”,结果唯一索引被我删了,后来又被风控团队骂——高并发场景下,业务规则和系统设计必须对齐,否则就是给自己挖坑。


面试题 vs 真实生产环境

面试时被问“如何设计秒杀系统”,答得头头是道:缓存、队列、限流、降级……
但真实上线才发现,最坑的往往不是技术,而是协作和边界

  • 测试同学:“你这接口返回500,是bug还是限流?” —— 得明确区分错误码
  • 运维大哥:“Redis内存快爆了,能不能清点缓存?” —— 得提前规划TTL和淘汰策略
  • 产品总监:“为什么用户点完没反应?” —— 因为异步下单,前端得轮询结果!

所以现在我面试别人,必问一句:“你线上出过P0事故吗?怎么复盘的?”
纸上谈兵不如一次真实故障来得深刻。


最终效果 & 心得

经过三轮压测 + 两次灰度发布,系统最终扛住了峰值6.2万QPS,数据库CPU稳定在40%以下。虽然过程像坐过山车,但收获巨大:

  1. 高并发不是堆中间件,而是做减法:减少数据库交互、减少同步操作、减少不必要的计算
  2. 工具要趁手:Arthas在线诊断、SkyWalking链路追踪、Prometheus监控,缺一不可
  3. 跨端经验反而有帮助:写Flutter习惯了状态管理,设计后端状态机(如订单状态流转)更清晰

最重要的是——我现在简历上可以写“主导高并发系统设计”了(手动狗头)。

下周就要去新公司面试了,岗位是“Flutter + 后端全栈”。希望别再让我一个人扛5万QPS……
但如果真遇到,至少我知道该先关掉爬虫,再祭出Redis Lua脚本。


最后送大家一句血泪总结

高并发系统不是设计出来的,是被流量打出来的。
别怕踩坑,但记得——每次事故后,一定要写复盘文档,不然下次还会栽同一个坑里。

(完)

作者:一个早起写Flutter、深夜debug后端的前Android工程师。目前在职求变,欢迎内推~

评论 0

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