高并发系统设计:我是怎么从“撑不住”到“扛得住”的

程序员小陈
2025-06-27 01:29
阅读 334

背景介绍

背景介绍

去年年底,我们团队接手了一个电商平台的秒杀系统重构项目。说是重构,其实更像是重建——老系统已经不堪重负,每逢大促,服务器就疯狂报警,数据库主从延迟严重,接口超时率飙升,用户抱怨不断。

我作为技术负责人,第一次看到压测报告的时候,整个人都不好了。QPS 两三百?连双十一的零头都不到!更糟的是,每次促销活动一开,订单系统就开始排队,甚至出现数据不一致的问题。

这事儿不能再拖了。我们决定全面重构秒杀模块的核心架构,目标只有一个:能抗住上万 QPS,保证数据一致性,并且在极端压力下还能稳得住

今天就来分享一下,我们在设计和落地这个高并发系统的整个过程中所踩过的坑、学到的经验,以及最终交出的成绩单。


真实挑战:我们到底遇到了什么问题?

服务器部署方案-1

真实挑战:我们到底遇到了什么问题?

刚开始分析老系统的问题时,发现并不是单一瓶颈,而是多个方面都在拖后腿:

1. 接口无限制,随便抢

老系统没有做任何限流措施,用户只要疯狂刷新页面或用脚本,就能瞬间打爆服务端。一个简单的请求,直接穿透到了数据库层,导致 DB 压力暴增。

2. 数据库扛不住写操作

所有下单逻辑都走 MySQL 写入流程,而且是同步的。一旦库存扣减失败,还会触发事务回滚,进一步影响性能。尤其是在并发高的时候,经常出现死锁。

3. Redis 缓存穿透频繁发生

缓存设计不合理,大量无效请求穿透到 DB。虽然用了 Redis,但没设置合理的失效策略和空值处理机制。

4. 异步处理能力薄弱

订单生成、短信发送等非核心功能都是同步执行的,结果导致主线程被阻塞,进一步加剧了响应延迟。

5. 没有降级机制

当系统快崩溃的时候没有任何自动降级手段,只能靠人工关掉接口或者重启服务。

面对这些问题,我们深知不能只修修补补,必须从架构层面重新设计一套稳定高效的系统。


架构设计方案与选型对比

我们做了几轮内部技术评审,最终确定采用如下架构方案:

整体结构:

  • 接入层:Nginx + Lua
  • 服务层:Spring Boot + Netty(部分异步处理)
  • 缓存层:Redis Cluster + Guava 本地缓存
  • 消息队列:RocketMQ 解耦关键链路
  • 数据库:MySQL 分库分表 + TDDL 中间件
  • 限流熔断:Sentinel + Nginx 限流
  • 监控告警:Prometheus + Grafana

我们为什么这么选?

1. 为什么要用 Nginx+Lua 做前置控制?

一开始我们考虑用 Java 做限流,比如 RateLimiter 或者 Sentine,但这些毕竟还是运行在 JVM 层。而 Nginx 天然具备高性能的流量控制能力,再配合 OpenResty 的 Lua 脚本,可以快速完成请求拦截、黑白名单、频次控制等功能。

举个例子,在 Lua 里我们做了这样的限流逻辑:

location /seckill {
    access_by_lua_block {
        local limit = require "limit"
        local key = ngx.var.remote_addr -- 或者 token
        if not limit.check(key) then
            return ngx.exit(503)
        end
    }
}

这段代码在 Nginx 层面就把很多恶意请求挡住了,对 DB 的冲击小了很多。

2. Redis 和 MySQL 如何协同作战?

我们采取了经典的“双缓存 + 预减库存”策略:

  • 商品信息预加载到 Redis
  • 抢购开始前,通过 Lua 脚本原子性地扣减 Redis 中库存
  • 成功扣减后再将订单写入数据库(异步)

这样设计的好处是尽可能减少 MySQL 的写入次数,同时又保障了数据准确性。

3. RocketMQ 为什么必不可少?

把订单生成、物流通知、积分变更等非核心链路解耦出来,使用消息队列异步处理,让主流程尽可能快地返回。比如:

// 下单成功后发消息到 MQ
rocketMQTemplate.convertAndSend("ORDER_CREATED_TOPIC", orderDTO);

消费方根据消息逐步执行后续动作,即使某一步失败也不会影响用户下单体验。


核心代码实现和关键配置

1. Redis 扣减库存的 Lua 脚本

-- stock.lua
local key = KEYS[1]
local count = tonumber(ARGV[1])

if redis.call('exists', key) == 1 then
    local current_stock = tonumber(redis.call('get', key))
    if current_stock >= count then
        redis.call('decrby', key, count)
        return true
    else
        return false
    end
else
    return false
end

Java 调用示例:

public boolean deductStock(String productId, int quantity) {
    DefaultRedisScript<Boolean> script = new DefaultRedisScript<>(luaScript, Boolean.class);
    Boolean result = redisTemplate.execute(script, Arrays.asList(productId), quantity);
    return Boolean.TRUE.equals(result);
}

API接口文档-2


2. 使用 Sentinel 实现限流

配置一个全局的限流规则:

rules:
  - resource: "/seckill/submit"
    grade: 1
    count: 100
    strategy: 0

并在 Controller 上添加注解即可:

@GetMapping("/submit")
@SentinelResource(value = "/seckill/submit", blockHandlerClass = BlockHandler.class, blockHandler = "handleBlock")
public ResponseEntity<?> submitOrder() {
    // 扣库存 + 下单逻辑
}

踩过的坑和解决思路

坑1:Redis 减库存之后 MySQL 插入失败怎么办?

这个问题我们初期也没想到。假设 Redis 库存减了 1,但 MySQL 因为唯一索引冲突(比如重复提交同一订单)插入失败,这时候库存就没法恢复。

解决方案: 我们引入了一个“待定订单池”,Redis 中记录当前用户是否有待定订单,如果 MySQL 插入失败,则释放 Redis 锁并触发补偿机制。

坑2:MQ 消费顺序被打乱

某个促销活动中,由于 RocketMQ 的默认并发消费机制,导致用户的订单状态更新和短信通知先后顺序错乱。

解决方法

  • 对同一个用户的消息按照 user_id hash 到同一个队列中
  • 设置消费者线程数为 1,确保顺序消费
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
    for (MessageExt msg : msgs) {
        String userId = new String(msg.getBody()).split(":")[0];
        processInOrder(userId, msg);
    }
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});

坑3:压测时 CPU 100%,接口无响应

后来才发现是因为我们用了一个同步日志组件,每个请求都刷盘一次,导致 I/O 线程阻塞。

对策

  • 改成异步日志打印(Logback AsyncAppender)
  • 控制日志级别,关闭不必要的 debug 日志

最终效果和收益

经过三个月的开发和多轮压测,最终上线后的表现超出预期:

指标 上线前 上线后
QPS 300 11,000
平均响应时间 800ms 60ms
数据一致性错误 0.5% <0.01%
大促期间故障率 时常宕机 全程平稳

最让我欣慰的是,今年双十一大促当天,整个秒杀系统顶住了峰值流量,没有出现一起用户投诉。


经验总结:给你的几点建议

如果你也在做类似的高并发系统开发,这里是我踩过坑之后总结的一些实用经验:

✅ 1. 永远不要相信前端会帮你限流

很多研发前期会想:“反正前端会限制点击频率,不会有问题。”但实际生产环境里,总有聪明人拿着脚本模拟请求。所以,限流必须从网关做起

✅ 2. Redis 是好帮手,但不是万能钥匙

缓存能提升性能,但也要考虑缓存击穿、穿透等问题。记得结合本地缓存(如 Guava)、布隆过滤器、以及后台定时刷新策略。

✅ 3. 异步不是万能,但几乎总是需要的

把非核心链路抽离出来,通过 MQ 或线程池处理,主流程才能更快响应,用户体验也更好。

✅ 4. 数据库必须提前规划扩容方案

分库分表、读写分离、连接池大小设置、慢 SQL 监控,这些数据库相关的优化工作必须在项目早期做好架构设计。

✅ 5. 监控比编码更重要

上线之前一定要集成 Prometheus + Grafana + ELK,监控每一个服务节点的状态、请求成功率、JVM 内存变化等指标,出了问题可以第一时间定位。

✅ 6. 测试要真实,压力测试更要贴近现实

别只测 HTTP 请求,要模拟真实业务场景,包括异常情况、网络抖动、MQ 延迟等情况下的系统反应。


写在最后

回头看,这套高并发系统的设计和实现过程并不轻松,中间也遇到不少挫折,但我们始终坚持两个原则:

  • 以用户为中心:任何技术决策都不能牺牲用户体验。
  • 工程化思维先行:先设计架构、再写代码,避免盲目堆代码导致不可维护。

现在每当用户反馈“这次秒杀体验很棒”的时候,我就特别感慨。那些深夜调试、线上查日志、反复修改限流配置的日子,终于有了回报。

希望这篇文章,不只是讲了一套技术方案,也能带给你一些真实的启发。愿我们一起写出更多“扛得住”的系统。

如有问题欢迎留言交流,共同成长 💪

评论 0

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