从架构设计到落地:我在一次高并发项目中的技术探索与优化实践

云端小木屋
2025-06-19 18:49
阅读 299

开篇:为什么写这篇文章?

开篇:为什么写这篇文章?

说实话,写这篇文章之前,我纠结了很久。技术文章很多,讲高并发、性能优化的也不少,但真正能结合真实业务场景、带着实战视角的并不多。而今天要分享的这个项目,恰好是我最近一年参与的一个中型电商平台重构项目的核心部分——用户订单服务模块。它不是那种“纸上谈兵”的案例,而是经历了从设计之初的迷茫、上线前的压力测试踩坑、再到最终成功支撑住双十一活动的全过程。

我希望通过第一人称的视角,把这段经历完整地还原出来,既包括技术上的选型、实现和调优,也包括团队协作中的沟通成本、压力测试中暴露的问题,以及最后上线后的反思总结。

这是一段值得记录的经历,也希望对你们有所启发。


背景介绍:为什么要做这件事?

背景介绍:为什么要做这件事?

实现方案图-2

去年年初,我所在的公司决定对原有的电商平台进行重构。原有系统是典型的单体架构,随着用户量的增长,尤其是在大促期间(如618、双11),订单模块经常出现响应超时、数据库连接池打满、甚至服务不可用的情况。

我们接到的需求非常明确:

  • 支持高并发访问
  • 降低订单服务延迟
  • 提高整体系统的可用性
  • 为后续扩展留出空间

我的角色是项目的架构师之一,负责整个订单服务的技术选型、方案设计和关键路径的代码实现。而这次重构,正好给了我一个机会去验证一些平时只在书本上看到过的技术概念,比如:缓存穿透解决方案、分布式锁、异步队列削峰填谷、分库分表、事务管理等等

下面我会围绕几个关键点来展开讲述:

  1. 问题描述
  2. 技术选型过程
  3. 核心实现思路与代码片段
  4. 遇到的坑与解决方法
  5. 最终效果与总结

一、遇到的问题:高并发下单场景下的痛点

一、遇到的问题:高并发下单场景下的痛点

最初我们接手的是老系统的订单创建流程。老系统的下单逻辑大致如下:

用户点击提交订单 -> 创建订单 -> 扣减库存 -> 记录用户账户变动 -> 返回订单ID

这些操作都是同步的、事务化的,并且全部走数据库写操作。看起来很常规,但在实际运行中,尤其是在大促期间,出现了以下几个典型问题:

1. 数据库连接池被打满

因为所有操作都需要写数据库,而且每个请求都开启一个事务,数据库连接数瞬间飙升,超过最大连接池容量后就报错:“Too many connections”。

2. 下单延迟高,TP99达到5秒以上

虽然我们部署了多个订单服务节点,但由于数据库瓶颈,请求排队导致延迟增加,用户体验很差。

3. 库存扣减不一致,出现“超卖”现象

原本使用的是数据库乐观锁扣减库存,但在高并发下依然会出现多个线程同时查到相同的库存值,导致数据不一致。

更糟的是,这些问题不是孤立存在的,它们会互相影响,形成雪崩效应。我们意识到必须做出改变,不能再继续依赖单一数据库和强一致性。


二、技术选型:权衡利弊后的选择

二、技术选型:权衡利弊后的选择

在做架构设计之前,我们团队开了多次评审会,讨论了几种主流做法:

方案 优势 劣势
纯数据库+锁机制 简单易维护 高并发下性能差,锁竞争严重
缓存+队列异步处理 响应快,削峰 复杂度高,需考虑一致性
分布式事务(如Seata) 数据一致性好 对网络依赖高,性能损耗大
本地消息+最终一致 拓扑清晰,可扩展性强 实现复杂,需要补偿机制

最终我们选择了缓存 + 异步任务队列 + 最终一致性的方案,作为订单服务的核心架构模型:

  • 使用 Redis 缓存库存信息
  • 下单请求先写入 Kafka 队列
  • 后台 Worker 消费消息,执行真正的写入逻辑
  • 用户下单返回的是预订单号,稍后再异步确认是否成功

这样做的好处很明显:解耦了写操作,避免数据库直接承受压力,同时也提升了响应速度。


三、解决方案的设计与实现

开发流程示意-1

1. 核心流程图

[Web层] -> [Kafka Producer] -> [Kafka Topic]
                             ↓
                         [Worker Consumer] -> [DB写入]
                                        ↓
                                   [发送结果通知]

2. 技术栈选用

  • Spring Boot 2.7 + JDK 11
  • MySQL 主从读写分离 + MyCat 分库分表
  • Redis 6.x 作为热点库存缓存
  • Kafka 3.0 实现任务队列
  • RabbitMQ 用于异步通知(可选)

四、关键代码实践

下面是一些核心模块的代码示例,展示如何实现这一架构。

下单接口示例(伪代码)

public String createOrder(Long userId, Long productId, Integer quantity) {
    // 1. 查询Redis中的当前库存
    Integer stock = redisTemplate.opsForValue().get("product_stock_" + productId);
    
    if (stock == null || stock < quantity) {
        throw new OrderException("库存不足");
    }

    // 2. 扣减Redis库存(设置TTL)
    Boolean success = redisTemplate.opsForValue().setIfAbsent(
        "lock_product_stock_" + productId,
        "locked", 5, TimeUnit.SECONDS);

    if (Boolean.FALSE.equals(success)) {
        throw new OrderException("系统繁忙,请稍后再试");
    }

    try {
        Integer currentStock = redisTemplate.opsForValue().get("product_stock_" + productId);
        if (currentStock < quantity) {
            throw new OrderException("库存不足");
        }

        // 3. 写入Kafka队列
        OrderMessage msg = new OrderMessage(userId, productId, quantity);
        kafkaTemplate.send("order_topic", JSON.toJSONString(msg));

        // 4. 返回预订单ID
        return generatePreOrderId(userId, productId);
    } finally {
        // 释放锁
        redisTemplate.delete("lock_product_stock_" + productId);
    }
}

Kafka 消费者处理订单入库逻辑

@Component
public class OrderConsumer {

    @Autowired
    private OrderService orderService;

    @KafkaListener(topics = "order_topic")
    public void process(String message) {
        OrderMessage orderMsg = JSON.parseObject(message, OrderMessage.class);
        
        try {
            orderService.processOrder(orderMsg);
        } catch (Exception e) {
            // 重试机制 or 日志记录
            log.error("处理订单失败: {}", message, e);
        }
    }
}

最终一致性保证

为了防止 Kafka 中的消息丢失,我们在消费端引入了:

  • 消息重试机制(基于 Kafka 的 offset 机制)
  • 死信队列兜底
  • 定时校验 Redis 与 DB 中的库存一致性

五、踩坑经验分享

1. Redis 分布式锁失效:误删锁

我们最开始使用的锁结构比较简单:

SET key random_value NX EX 10s

但在实践中发现,在高并发场景下某个请求处理时间过长,可能导致锁被其他线程误删。

解决方案

引入 Lua 脚本来保证原子性,并加上唯一标识(UUID):

String lockKey = "lock:" + key;
String requestId = UUID.randomUUID().toString();
// 加锁
Boolean success = redisTemplate.execute((RedisCallback<Boolean>) connection -> 
    connection.set(lockKey.getBytes(), requestId.getBytes(), Expiration.seconds(10), true));

if (success) {
    try {
        // 执行业务逻辑
    } finally {
        // 使用Lua脚本确保删除锁的原子性
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockKey), requestId);
    }
}

2. Kafka 消费积压:批量拉取配置不对

刚开始我们消费者拉取 Kafka 是默认每次拉取500条,结果因为某些消息处理慢,导致大量消息堆积。

后来调整为动态拉取,结合 backpressure 和限流控制:

spring:
  kafka:
    consumer:
      max-poll-records: 50  # 每次最多消费50条
      fetch-min-size: 1KB   # 最小拉取大小
      fetch-max-wait: 500ms # 最大等待时间

3. 异常日志打印导致线程阻塞

有一个错误的日志输出方式:

log.error("error", e); // 不要这么写!

应该改为:

log.error("处理订单失败: {}", message, e);

前者会导致堆栈信息输出为字符串拼接,严重影响性能。


六、上线后的效果评估

经过一系列的压测优化和灰度发布,最终上线后的表现如下:

指标 上线前 上线后 提升幅度
下单QPS ~200 ~2500 +1150%
平均延迟 1.5s 300ms -80%
DB连接数峰值 800+ 150~200 显著下降
超卖事件 有发生 0次 改进明显

特别是在双十一大促当天,我们撑住了每秒近3000个订单的写入压力,整个服务基本保持稳定无故障。


七、给读者的经验建议

如果你也在做类似的架构优化,以下几点建议希望能帮到你:

✅ 1. 性能优化不能贪多,优先核心路径

不要一开始就想着搞“全链路压测”、“服务网格”这些东西,先把最重要的那条链路跑通,比如“下单 → 创建订单 → 扣库存 → 付款完成”,其余可以慢慢迭代。

✅ 2. 缓存并不是万金油

很多人一上来就用 Redis,但如果数据更新频繁,缓存容易变成脏数据。要考虑 TTL、缓存更新策略(主动刷新 vs 过期淘汰)、以及是否真的适合缓存。

✅ 3. 重视压测工具的选择和搭建

我们自己写了一个简单的 JMeter 测试套件,模拟不同用户行为,提前暴露了很多问题,比如慢SQL、死锁、CPU飙高等。一定要尽早介入性能测试,而不是等上线后才想起来。

✅ 4. 技术选型没有银弹,权衡比选择更重要

比如我们一开始也考虑过用 RocketMQ,但考虑到团队熟悉程度,最终还是选择了 Kafka。技术选型不是越新越好,而是适合当下团队的能力和发展节奏。

✅ 5. 写日志要有节制,别让它拖累你的服务性能

日志过多不仅影响性能,还会让排查问题变得困难。合理使用 traceId 和日志级别(debug/info/warn/error),能让你在出问题的时候快速定位。


结语:技术探索的价值在于落地

写到这里,我想起有一次凌晨上线,我和同事守着监控看 QPS 慢慢爬升,那一刻,所有的加班、熬夜、争论都变得值得了。

技术探索从来不只是“用什么框架”,更重要的是“怎么用得好”,以及“出了问题怎么处理”。每一次的尝试和优化,其实都在锻炼我们作为工程师的综合能力。

希望这篇来自实战经验的文章,能够帮助你少走弯路,也能给你带来一些启发。

如果喜欢这样的内容,欢迎留言或者关注我,后续还会分享更多关于架构设计、性能调优等方面的真实案例。


作者:张工 | 某电商公司平台架构师 | 从业十年,专注高并发系统设计

评论 0

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