从架构设计到落地:我在一次高并发项目中的技术探索与优化实践
开篇:为什么写这篇文章?

说实话,写这篇文章之前,我纠结了很久。技术文章很多,讲高并发、性能优化的也不少,但真正能结合真实业务场景、带着实战视角的并不多。而今天要分享的这个项目,恰好是我最近一年参与的一个中型电商平台重构项目的核心部分——用户订单服务模块。它不是那种“纸上谈兵”的案例,而是经历了从设计之初的迷茫、上线前的压力测试踩坑、再到最终成功支撑住双十一活动的全过程。
我希望通过第一人称的视角,把这段经历完整地还原出来,既包括技术上的选型、实现和调优,也包括团队协作中的沟通成本、压力测试中暴露的问题,以及最后上线后的反思总结。
这是一段值得记录的经历,也希望对你们有所启发。
背景介绍:为什么要做这件事?


去年年初,我所在的公司决定对原有的电商平台进行重构。原有系统是典型的单体架构,随着用户量的增长,尤其是在大促期间(如618、双11),订单模块经常出现响应超时、数据库连接池打满、甚至服务不可用的情况。
我们接到的需求非常明确:
- 支持高并发访问
- 降低订单服务延迟
- 提高整体系统的可用性
- 为后续扩展留出空间
我的角色是项目的架构师之一,负责整个订单服务的技术选型、方案设计和关键路径的代码实现。而这次重构,正好给了我一个机会去验证一些平时只在书本上看到过的技术概念,比如:缓存穿透解决方案、分布式锁、异步队列削峰填谷、分库分表、事务管理等等。
下面我会围绕几个关键点来展开讲述:
- 问题描述
- 技术选型过程
- 核心实现思路与代码片段
- 遇到的坑与解决方法
- 最终效果与总结
一、遇到的问题:高并发下单场景下的痛点

最初我们接手的是老系统的订单创建流程。老系统的下单逻辑大致如下:
用户点击提交订单 -> 创建订单 -> 扣减库存 -> 记录用户账户变动 -> 返回订单ID
这些操作都是同步的、事务化的,并且全部走数据库写操作。看起来很常规,但在实际运行中,尤其是在大促期间,出现了以下几个典型问题:
1. 数据库连接池被打满
因为所有操作都需要写数据库,而且每个请求都开启一个事务,数据库连接数瞬间飙升,超过最大连接池容量后就报错:“Too many connections”。
2. 下单延迟高,TP99达到5秒以上
虽然我们部署了多个订单服务节点,但由于数据库瓶颈,请求排队导致延迟增加,用户体验很差。
3. 库存扣减不一致,出现“超卖”现象
原本使用的是数据库乐观锁扣减库存,但在高并发下依然会出现多个线程同时查到相同的库存值,导致数据不一致。
更糟的是,这些问题不是孤立存在的,它们会互相影响,形成雪崩效应。我们意识到必须做出改变,不能再继续依赖单一数据库和强一致性。
二、技术选型:权衡利弊后的选择

在做架构设计之前,我们团队开了多次评审会,讨论了几种主流做法:
| 方案 | 优势 | 劣势 |
|---|---|---|
| 纯数据库+锁机制 | 简单易维护 | 高并发下性能差,锁竞争严重 |
| 缓存+队列异步处理 | 响应快,削峰 | 复杂度高,需考虑一致性 |
| 分布式事务(如Seata) | 数据一致性好 | 对网络依赖高,性能损耗大 |
| 本地消息+最终一致 | 拓扑清晰,可扩展性强 | 实现复杂,需要补偿机制 |
最终我们选择了缓存 + 异步任务队列 + 最终一致性的方案,作为订单服务的核心架构模型:
- 使用 Redis 缓存库存信息
- 下单请求先写入 Kafka 队列
- 后台 Worker 消费消息,执行真正的写入逻辑
- 用户下单返回的是预订单号,稍后再异步确认是否成功
这样做的好处很明显:解耦了写操作,避免数据库直接承受压力,同时也提升了响应速度。
三、解决方案的设计与实现

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