高并发系统设计:从理论到实践,一个后端开发者的实战分享

Swagger抄写员
2025-06-24 19:04
阅读 341

引言:为什么我决定写这篇文章?

引言:为什么我决定写这篇文章?

作为一名在互联网公司工作的后端开发者,这几年来我参与过多个高并发系统的搭建和优化工作。从电商秒杀活动、社交平台的热点事件推送,到金融场景下的高频交易接口,每一次都让我对“高并发”这三个字有了更深的理解。

记得刚入行那会儿,我也听过不少关于高并发的设计方法论,比如缓存、队列、分布式、限流等等。但真正上手的时候才发现,纸上得来终觉浅。很多时候理论讲得很清楚,一到生产环境就发现这也不够用、那也出问题。

于是我就想着,不如结合自己的实际项目经验,写一篇真正贴地气的总结文章。希望通过这篇文字,不仅能帮大家更好地理解高并发系统的构建逻辑,也能少走一些我们踩过的坑。

项目背景:一场突如其来的“秒杀风波”

项目背景:一场突如其来的“秒杀风波”

事情要回溯到2021年,当时我在一家中型电商平台负责订单中心的研发工作。临近618大促,我们需要上线一款限时秒杀商品,预计会有大量用户同时涌入下单页面。

系统情况简述:

  • 主语言为Java(Spring Boot框架)
  • 数据库使用MySQL分库分表,主从架构
  • Redis用于缓存热点数据
  • 系统部署在Kubernetes集群上
  • 秒杀商品库存总量为5000个,售价极低,预计会在几秒钟内被抢空

为了提前预演风险,我们在测试环境中做了压测。初步结果还不错,TPS能达到2000左右,QPS也能扛住每秒万次访问量。当时的我想着,这不挺稳的吗?哪知道正式上线当天直接被打脸了……

遇到的问题与挑战:流量高峰下崩溃的系统

遇到的问题与挑战:流量高峰下崩溃的系统

正式开始前五分钟,我们就陆续收到监控告警:Redis连接超时、MySQL锁等待严重、服务响应延迟飙升。等到秒杀开始那一刹那,订单服务直接全线不可用,前端一片惨白。

事后复盘发现,问题主要集中在以下几个方面:

1. 缓存穿透 + 击穿 + 雪崩全中

虽然我们给商品信息加了Redis缓存,但在高并发请求下,很多没有命中缓存的数据导致数据库压力剧增。同时,缓存失效时间设置不合理,导致大量缓存同一时间过期,引发了雪崩效应。

2. 库存扣减逻辑出现线程安全问题

原本我们使用的是先查询再更新的方式进行库存扣除,但在多线程并发下出现了超卖现象。虽然MySQL本身有行级锁机制,但由于应用层频繁发起事务,导致死锁频发、性能急剧下降。

3. 没有限流降级机制

当时的服务没有任何限流机制。当大量请求蜂拥而至时,服务根本来不及处理,全部堆积在业务逻辑里,最终引发链式反应——所有服务都被拖垮。

4. 没有异步化处理机制

订单创建、支付回调等流程没有引入消息队列,而是全部通过同步方式处理。在瞬时峰值到来时,整个链路都处于满负荷状态,几乎无暇顾及其他正常请求。

那次事故给我们敲响了警钟,也让整个技术团队意识到一个问题:我们缺的不是理论知识,而是真正的高并发落地能力

解决方案:重构秒杀系统,实现“稳如老狗”的高并发设计

痛定思痛之后,我们决定对整个秒杀系统进行彻底重构。这个过程持续了一个半月,涉及代码重构、架构调整、压测验证等多个环节。

1. 缓存策略升级:三重防护

① 布隆过滤器防穿透

我们引入了一个本地布隆过滤器(使用Guava的Cache类实现),在请求进入DB之前进行拦截。这样能有效防止无效请求打到数据库。

LoadingCache<String, Boolean> bloomFilter = Caffeine.newBuilder()
    .maximumSize(1000)
    .build(key -> {
        // 实际查询数据库确认是否存在该key
        return existsInDatabase(key);
    });

② 缓存永不过期或随机过期时间

我们将热点商品的缓存设为永不过期,并由后台定时更新。非热点商品则采用随机过期时间(比如基础TTL + 一个随机值),避免集中失效。

③ 多级缓存结构

除了Redis之外,我们还在应用节点使用了本地缓存(Caffeine)作为第一道防线,避免所有请求都经过Redis,降低网络延迟。


2. 使用Redis Lua脚本解决库存原子性问题

为了避免超卖问题,我们改用Redis+Lua来控制库存的原子扣减。具体做法是:把库存预热到Redis中,每次扣减时通过Lua脚本来保证操作的原子性。

示例Lua脚本如下:

-- KEYS[1] 是商品ID,ARGV[1] 是要减少的数量
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) < tonumber(ARGV[1]) then
    return -1
else
    return redis.call('DECRBY', KEYS[1], ARGV[1])
end

Java代码调用:

public long deductStock(String productId, int quantity) {
    Long result = redisTemplate.execute(
        new DefaultRedisScript<>(deductLuaScript, Long.class),
        Arrays.asList(productId),
        String.valueOf(quantity)
    );
    return result;
}

这种方式大大减少了MySQL的压力,同时也解决了超卖问题。后续才异步持久化到MySQL中。


3. 接口限流 + 降级策略落地

我们采用了Guava的RateLimiter做客户端限流,以及Sentinel做服务端限流双保险。

客户端限流配置:

// 单个实例每秒最多处理1000个请求
RateLimiter rateLimiter = RateLimiter.create(1000);

if (!rateLimiter.tryAcquire()) {
    throw new TooManyRequestsException("请求过于频繁,请稍后再试");
}

Sentinel规则配置(基于Nacos动态配置):

flow:
  rules:
    - resource: /api/secKill
      limitApp: default
      grade: 1
      count: 1000
      strategy: 0
      controlBehavior: 0
      clusterMode: false

同时我们还实现了简单的服务降级逻辑:当Redis异常或库存耗尽时,直接返回静态页面引导用户到其他商品页面,而不是一直抛异常。


4. 异步化改造 + 消息队列削峰填谷

我们将订单创建这一核心流程进行了异步化改造,借助Kafka将订单写入解耦出来:

@PostMapping("/secKill")
public ResponseEntity<?> handleSecKillRequest(@RequestBody SecKillRequest request) {
    if (redisService.deductStock(request.getProductId(), request.getUserId())) {
        // 扣减成功,发送消息到Kafka
        orderProducer.sendOrderMessage(request.getUserId(), request.getProductId());
        return ResponseEntity.ok("排队中,请勿重复提交");
    } else {
        return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("库存不足");
    }
}

然后订单消费者异步消费这条消息,完成真实落单动作:

@KafkaListener(topics = "order-topic")
public void processOrder(OrderMessage message) {
    try {
        orderService.createOrder(message.getUserId(), message.getProductId());
    } catch (Exception e) {
        log.error("订单处理失败", e);
    }
}

服务器部署方案-1

这样一来,既避免了阻塞主线程,又实现了“削峰填谷”,让系统更稳定。


5. 数据库层面优化

虽然Redis已经挡掉了大部分请求,但我们还是对MySQL进行了一些针对性优化:

  • 对订单表增加二级索引,加快用户订单查询
  • 使用分库分表策略,每个用户的数据根据user_id取模分配到不同实例
  • 写操作尽量使用Batch Insert,减少事务次数
  • 加大连接池大小(HikariCP)

6. 整体架构图一览

Client ---(限流)--> API Gateway 
               |
             Load Balancer
               |
          Nginx/Redis(前置缓存)
               |
     Spring Boot Application Cluster
               |
         Kafka(订单异步处理)
               |
   MySQL 分库分表 + 主从架构

整体架构以“缓存前置 + 异步解耦 + 限流保护”为核心思想,尽可能把压力分散到各个环节。


效果总结:系统终于稳住了!

这套新系统上线后,我们进行了多次压测,效果非常显著:

指标 原系统 新系统
TPS 2000+ 2.5w+
平均响应时间 500ms 120ms
CPU占用率 95%+ 稳定在60%以下
成功订单数 存在超卖 全部正确
用户体验 页面卡顿频繁 流畅无卡顿

最让人安心的是,即使在模拟5倍流量冲击的情况下,系统也能自动限流并平稳过渡,不再出现服务宕机的情况。

那次618之后,老板还专门开了一个小会表扬我们:“你们这次做得不错,至少首页没挂。”

经验分享:高并发系统设计的关键点总结

1. 不要轻视任何一次压测

很多人认为压测只是形式主义,但我亲身经历过才知道:压测暴露的问题往往是你平时意识不到的盲区。建议一定要按真实场景模拟压测,不要只关注平均值。

2. “缓存 + 异步 + 限流”三板斧必不可少

这三个点几乎是所有高并发系统的基础标配。特别是异步化设计,它不仅提升性能,还能极大增强系统的健壮性。

3. 重视幂等性设计

在高并发环境下,网络抖动、重试机制很容易造成请求重复。建议在关键接口中加入幂等Token校验逻辑,避免重复处理。

例如,在订单创建接口中,可以传一个全局唯一ID作为去重标识:

String uniqueId = UUID.randomUUID().toString();
if (idempotentChecker.exists(uniqueId)) {
    return "请勿重复提交";
}
idempotentChecker.mark(uniqueId);

4. 系统要有“优雅降级”的准备

一旦系统真的撑不住,我们要有预案,比如:

  • 自动切换静态页展示
  • 关闭非核心功能(如推荐算法)
  • 告诉用户“系统繁忙,请稍后再试”

这些看似“失败”的措施,其实是保障用户体验的关键。

5. 技术选型要贴近业务实际

别盲目追求最新技术。如果你的团队对某种组件不熟悉,硬上反而可能适得其反。比如我们可以选择RabbitMQ而不是Kafka,只要能满足当前业务需求即可。


写在最后:高并发不是终点,而是起点

回顾这几年的经历,我发现所谓的“高并发”其实并不是目的,而是手段。它的本质是为了让用户在面对海量请求时,依然能够获得流畅、稳定的服务体验。

也许你会问:“我们公司现在业务量不大,是不是不需要搞这么复杂?”我以前也有这样的想法,直到那次秒杀活动彻底改变了我的认知。技术债越早还,代价越小;系统容量越早规划,未来越轻松

希望这篇文章能带给你一点启发。如果你正在经历高并发系统的设计或重构阶段,欢迎留言交流。我们一起在这条路上继续前行,打造更稳定、更高效的后端系统。


📌 结语

正如《高性能网站建设指南》中说的那样:“性能就是用户体验”。愿我们都能写出既快又稳的系统,给用户带来更好的体验,也让自己更有成就感。

如果你觉得这篇文章对你有帮助,不妨点个赞或收藏,让更多人看到它。技术的成长从来都不是一个人的事,而是一群人的共同进步。

Stay hungry,stay geek ✨

评论 0

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