从零到稳:一次高并发场景下的技术探索与实践

React炼金术士
2025-06-18 22:38
阅读 268

背景介绍:一个“看起来很简单”的需求

背景介绍:一个“看起来很简单”的需求

2023年初,我所在的团队承接了一个新项目 —— 为一家全国连锁零售品牌构建一套会员积分系统,核心需求是支持门店扫码核销、在线积分兑换等功能。听起来不算复杂,但其中有一个关键点:在节假日高峰时段,系统需要应对每秒上万次的扫码请求。

起初我们都觉得“不就是个积分系统嘛”,直到第一次压力测试时,服务直接挂了。那个时候我才意识到,我们面对的是一个典型的高并发场景。

这篇文章想和大家分享一下,我们在那个项目中是如何一步步摸索、踩坑、调整架构,最终把这个系统稳定下来的全过程。如果你也正在处理类似的问题,或者对高并发架构感兴趣,希望这篇文章能给你一些启发。


问题描述:当“看起来正常”变成“完全失控”

系统架构设计-1

问题描述:当“看起来正常”变成“完全失控”

我们的初始设计其实挺标准的:

  • Spring Boot + MyBatis
  • 单表设计:用户积分信息存在一张表里
  • 前端通过Nginx负载均衡访问多个微服务节点

第一次压测时,使用JMeter模拟5000个并发请求进行扫码核销操作,结果惨不忍睹:

  • 接口平均响应时间超过5秒
  • 数据库出现大量锁等待
  • 服务频繁触发OOM(Out of Memory)

最离谱的是,有几次甚至出现了同一个用户被扣除了多次积分的情况 —— 这不是性能问题,这是严重的产品错误风险。

更头疼的是,我们还没上线,就接到了客户运营部门的通知:“双十二活动期间必须上线,而且要支撑住峰值1w+ QPS。”


解决方案:架构上的“三板斧”与细节打磨

第一板斧:缓存先行,读写分离

我们首先想到的是缓存。把高频读取的积分余额信息放到Redis里。这样每次扫码核销时,先读Redis,更新的时候再落库。这样一来,数据库的压力瞬间减轻了不少。

但我们很快发现了另一个问题:Redis中的数据和数据库不一致怎么办?

于是引入了TTL机制,以及定时任务兜底补偿:

// 缓存设置逻辑片段
redisTemplate.opsForValue().set("user:points:" + userId, points, 5, TimeUnit.MINUTES);

同时将积分更新的操作从“单纯SQL UPDATE”改成了Lua脚本控制原子性更新,确保Redis和DB之间的最终一致性。

第二板斧:异步化处理,削峰填谷

扫码核销是一个强实时操作,但积分变更后的日志记录、消息通知等后续动作是可以异步处理的。

于是我们引入了Kafka作为中间队列。扫码接口只负责处理核心业务逻辑,其他操作通过消息队列解耦:

// 伪代码示例
public void handleScan(String userId) {
    // 1. 核销主流程
    deductPoints(userId);

    // 2. 异步写日志 & 发通知
    kafkaProducer.send(new PointsLogMessage(userId, amount));
}

这一步之后,接口响应时间从原来的2秒降到200ms以内。

第三板斧:分库分表 + 分布式ID

随着用户量增长到百万级别,单表开始出现瓶颈。我们采用了一种较为稳妥的分表策略:按userId做哈希分片,每个用户落在固定的分区。

为了保障唯一性和递增性,我们选择了Snowflake算法来生成订单ID和积分变动记录ID。

不过在实际落地过程中,我们也遇到了一些小插曲:比如部署在不同机房的服务因为时间同步问题导致生成重复ID。最后是通过引入了带机房标识的自定义版本解决。


踩坑经验:那些深夜调试的记忆

技术应用场景-2

1. Redis穿透问题差点酿成大祸

初期我们没有为热点Key加空值缓存,导致某些无效userId频繁查询打穿Redis直达数据库,险些让DB挂掉。后来增加了如下逻辑:

if (userNotExist) {
    redisTemplate.set(key, "null", 60, TimeUnit.SECONDS); // 设置空值缓存
}

并且配合Bloom Filter做了预检。

2. Kafka消费延迟导致业务滞后

有一段时间我们发现积分变动的消息会延迟几秒钟才到达下游系统。排查后发现是因为消费者线程池配置太小,并且没有开启批量拉取。

调整Consumer配置后,效果明显提升:

# 消费者相关配置优化
enable.auto.commit=false
max.poll.records=500
session.timeout.ms=30000

并增加手动提交offset机制,确保消费无误。

3. 分布式事务引发的数据不一致

最头疼的一次问题是两个服务之间转账失败导致积分丢失。我们尝试过用Seata做分布式事务管理,但由于对锁竞争过于激烈,导致系统性能下降严重。

最终退而求其次,采用本地事务表+补偿任务的方式来实现最终一致性。虽然无法做到强一致,但在我们业务场景下是可接受的。


实践成果:从崩溃边缘到稳定运行

经过上述一系列调整后,我们在压测环境中成功扛住了每秒12000次的扫码核销请求,平均响应时间控制在250ms以内,TP99达到400ms左右。

系统上线后经历了多个促销高峰,包括春节、“双11”等活动,均未出现重大故障。更重要的是,我们建立了一整套可观测性体系,包括:

  • Prometheus监控各模块指标
  • Grafana看板展示系统整体健康状况
  • ELK日志收集分析异常情况

这些基础设施为我们后续运维提供了极大的便利。


我的一些感悟和建议

1. “看似简单的需求,背后可能隐藏着巨大的挑战”

很多人觉得积分系统很基础,但实际上当你面对真正的流量冲击时,每一个小细节都可能成为瓶颈。一定要提前做压力测试,不要等到上线前一天才发现问题。

2. 技术选型要有取舍,不能盲目追求新技术

我们曾经想过用Pulsar替换Kafka,也研究过Flink来处理流式计算。但最终还是坚持用老朋友Kafka和Spring Batch组合,因为它们足够稳定、社区支持好、团队成员熟悉。

有时候,“保守”也是一种智慧。

3. 架构不是一开始就完美的,而是持续演进的结果

我们的系统架构从最初的单体服务,逐步演进到现在基于Redis+Kafka+MySQL分片+监控平台的结构,整个过程花了将近半年。每一次迭代都是在不断试错和改进。

4. 团队协作是技术落地的关键

在整个项目过程中,研发、运维、产品、测试都深度参与了进来。特别是测试同学,在模拟真实场景方面给到了极大帮助。技术从来不是一个人的事,尤其在高并发这种复杂场景下,团队配合尤为重要。


写在最后:技术的本质是解决问题

回顾整个项目,最大的收获并不是用了多少牛逼的技术,而是我们学会了如何在一个真实复杂的业务场景中,找到平衡点:稳定性 vs 性能,开发效率 vs 长期维护成本,一致性 vs 可用性。

我也慢慢理解了那句话:“好的架构,是能适应变化的”。

如果你现在正面临类似的挑战,不妨从最简单的做起 —— 把第一个接口写出来,然后一步步压测、调优、重构。你不需要一开始就把所有问题都想清楚,只要方向是对的,剩下的交给时间和实践。

愿我们在技术的路上,都能越走越远。共勉。

评论 0

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