高并发系统设计:一个架构师的实战经验分享

Grafana看图员
2025-06-28 08:24
阅读 392

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

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

作为一名后端开发出身的架构师,我经历过多个百万级用户、高并发访问量的产品项目。从最初面对突发流量手足无措,到如今能够冷静应对复杂场景,这中间踩过的坑、熬过的夜、掉过的发,只有同行才懂。

今天我想以一个真实的项目案例为主线,跟大家分享一下我在设计高并发系统时的一些经验和思路。我会从问题出发,讲清楚我们当时遇到的瓶颈、思考过程、技术选型和最终落地的效果。希望你读完之后不仅能了解一些“高大上”的架构技巧,还能感受到作为工程师在项目一线解决问题的真实状态。


项目背景:一次电商大促的挑战

项目背景:一次电商大促的挑战

故事要从2019年说起。那一年,我所在的公司准备做一场大型线上促销活动。目标是在短时间内吸引大量用户参与秒杀抢购。我们的系统原本是面向中等规模用户的电商平台,订单模块用的是MySQL单机部署,商品服务也没有任何缓存层支持。

产品经理信心满满地宣布:“这次我们要冲日活百万!”
我的内心却是拔凉拔凉的——就当时的架构水平,别说百万了,十万并发都得跪。

主要问题包括:

  • 商品详情页没有缓存,每次访问都要查数据库
  • 秒杀下单接口直接操作MySQL,容易出现锁争用
  • 订单创建过程中存在冗长的串行调用链,响应时间不稳定
  • 没有异步处理机制,所有任务都同步完成
  • 缺乏限流降级机制,一旦系统出问题整个平台都会瘫痪

最夸张的一次压测,QPS还不到1000,线程池就已经开始大面积超时,连接数占满的问题频繁出现。


我们是怎么解决这些问题的?

我们是怎么解决这些问题的?

第一步:拆分核心流程,找出性能瓶颈

首先,我们将整个下订单的流程拆解成了若干个子模块:

  • 用户身份校验
  • 库存扣减
  • 优惠券核销
  • 支付预创建
  • 订单写入DB
  • 消息推送通知

通过埋点采集日志 + SkyWalking 调用链分析,我们发现 库存扣减和支付预创建两个步骤耗时最长,且严重依赖DB事务。这两个模块成为了关键路径上的“卡脖子”环节。


第二步:引入缓存,减少对数据库的冲击

为了解决这个问题,我们做了以下几个层面的优化:

1. 接口级别缓存:使用Redis缓存热点商品信息

// 获取商品详情伪代码
public Product getProductDetail(Long productId) {
    String cacheKey = "product:detail:" + productId;
    Product product = redis.get(cacheKey);
    if (product == null) {
        product = db.getProductById(productId);
        redis.setex(cacheKey, 300, product); // 缓存5分钟
    }
    return product;
}

这个改动虽然简单,但效果非常明显。将原来80%以上的查询请求挡在了数据库之外,大幅减轻了MySQL的压力。

2. 写缓存队列:异步化库存扣减

对于库存这种需要严格控制的数据,我们采用了一个生产者/消费者模型来实现“软一致性”。

  • 用户下单请求进入后,先写入Kafka队列
  • 由独立的消费服务进行库存原子扣减,并落库
  • 前端展示库存数量的时候走Redis(延迟可接受)
// 发送库存变更消息示例
kafkaTemplate.send("inventory-decrease", inventoryDecreaseMessage);

// 消费端处理逻辑
@KafkaListener(topics = "inventory-decrease")
public void processInventoryDecrease(InventoryMessage message) {
    String lockKey = "lock:product:" + message.getProductId();
    Boolean locked = redis.setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
    if (!locked) {
        log.warn("库存更新失败,已被其他请求占用");
        return;
    }

    try {
        Integer currentStock = redis.opsForValue().get("stock:" + message.getProductId());
        if (currentStock != null && currentStock > 0) {
            Integer newStock = currentStock - message.getAmount();
            redis.opsForValue().set("stock:" + message.getProductId(), newStock);
            inventoryService.updateDbStock(message.getProductId(), newStock);
        } else {
            log.info("库存不足,拒绝下单");
        }
    } finally {
        redis.delete(lockKey);
    }
}

这里使用了Redis分布式锁来避免并发冲突,同时利用MQ实现了削峰填谷的作用。


第三步:重构订单核心逻辑,提升吞吐能力

原来的下单接口是一个长达十几个方法调用的大函数,每个步骤都是同步阻塞执行的。我们做了如下改进:

  1. 业务逻辑拆解:把非强一致性的步骤抽离出来,如支付创建、消息通知等改为异步处理。
  2. 数据库分表分库:按照用户ID做了垂直拆分,缓解单表压力。
  3. 写索引分离:将写操作和读取操作分别走不同DB节点,降低主库负担。

其中,订单号的设计也做了调整:

// 使用Snowflake生成唯一订单号
String orderId = SnowflakeIdGenerator.nextId();

// 订单结构体字段简略示意
class Order {
    Long userId;       // 用于分库依据
    String orderId;     // 分布式ID,全局唯一
    List<OrderItem> items;
    BigDecimal totalAmount;
    LocalDateTime createTime;
    String payStatus;   // 状态机管理
}

通过把 userIdorderId 结合起来使用,既保证了数据分布均匀,又便于后续扩展。


第四步:接入网关+限流熔断体系

为了防止突发流量冲垮系统,我们在前端加了一道 Nginx + Spring Cloud Gateway 的组合拳,并接入 Sentinel 实现自动限流。

在GateWay中配置了如下规则:

spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                rate-limiter: "${spring.cloud.gateway.filter.requests-per-second}"
                key-resolver: "#{@userResolver}" # 基于用户维度限流

Sentinel 则通过 dashboard 配置资源保护策略,例如某个库存接口每秒最多只允许 1000 次访问,超出则降级返回提示语。


开发过程中遇到的坑和教训

开发过程中遇到的坑和教训

坑一:缓存击穿导致雪崩效应

初期我们设置的商品缓存过期时间为统一的300秒,在高峰时段恰好多个热门商品缓存同时失效,造成DB瞬时压力剧增。

解决方案:

  • 在原有TTL基础上增加随机偏移值,使缓存不过度集中过期
  • 设置空值缓存防止穿透攻击
Integer expiration = 300 + new Random().nextInt(60); // 随机加上0~60秒
redis.setex(key, expiration, result);

坑二:Kafka分区太少导致消费堆积

由于前期分区数设置太少,当高峰期消息涌入时,消费速度远远跟不上。

对策:

  • 提前评估数据量,合理设置topic分区数
  • 同时扩容消费者实例,确保并行消费能力

坑三:分布式锁误删导致超卖

有一版代码在使用Redis分布式锁时,忘记添加锁释放判断,导致释放了不属于自己的锁,进而引发多线程重复扣库存的问题。

正确做法:

  • 锁加标识,例如使用UUID作为value
  • 使用Lua脚本保证原子性释放锁
  • 加入看门狗机制,延长自动续约时间

最终效果:扛住了预期并发压力

上线当天,整体峰值达到了 13万QPS,CPU平均负载控制在40%以内,GC频率稳定在毫秒级,订单服务响应时间保持在200ms以下。

  • MySQL慢查询基本消失
  • Redis命中率达到97%
  • Kafka积压控制在100条以内
  • Sentinel成功拦截异常流量达30W+

最让我欣慰的是,整个活动期间,系统没有出现一起重大故障,监控面板上的曲线也非常平稳。


给读者的几点建议

系统架构设计图-1

  1. 不要迷信“高并发方案”本身。很多文章动不动说“上缓存、上MQ”,但是不考虑实际场景和技术栈成熟度,很容易适得其反。

  2. 重视日志埋点和性能监控工具的建设。SkyWalking、Prometheus、ELK这些不是花瓶,而是你定位问题的眼睛。

  3. 提前演练比事后补救更有价值。在真正压测之前,我们做了三次模拟演练,每次都暴露出新的问题。有些问题在线下根本跑不出来。

  4. 架构演进应该循序渐进。不要一口吃成胖子,先把瓶颈找出来,再一个一个优化,否则你会陷入“过度设计”的泥潭。

  5. 高并发系统的本质在于“削峰填谷”。通过缓存、队列、异步等方式,把瞬间的高压变成平稳的低负载流动。


尾声:技术没有银弹,但我们一直在路上

回顾那次大促的经历,我深刻体会到高并发系统不是靠几个组件堆出来的,而是一个个决策不断打磨的结果。每一个参数背后都有无数次推倒重来的选择;每一行代码的背后,也都是一次次深夜调试的坚持。

如果你现在正在面临类似的问题,不妨从你系统的某一个核心接口入手,慢慢理清它的调用链,找到最关键的那根“稻草”。我相信,只要方向对了,哪怕走得慢一点,总有一天也能构建起属于你的高性能系统。

路虽远,行则将至。加油,兄弟们!

评论 0

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