2024→2026 版本升级:10 万 QPS 会员系统架构,两年后我们改了什么
核心观点:真正的"架构演进"不是画一张全新的架构图,而是在生产环境的炮火中,一边救火一边修修补补,把一个个坑填上,让系统在压力下慢慢变得更健壮。
说明:系统从 2024 年的 10 万 QPS 增长到 2026 年的 15 万 QPS,会员数从 2000 万增长到 3500 万。本文标题沿用了两年前的命名,重在回顾这两年的架构演进历程。
引言
两年前,我写了一篇《10 万 QPS 会员系统架构设计》,在圈内引起了不小的反响。两年后的今天,系统依然在运行,但架构已经面目全非——不是因为我们推翻了重来,而是因为生产环境教会了我们什么才是真正需要的。
这篇文章,我想聊聊这两年我们到底改了什么,为什么改,以及改完之后的真实效果。
一、原架构回顾
先回顾一下两年前的架构:
┌─────────────────────────────────────────────────────────────────────────┐
│ 2024 年架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌───────┐ ┌───────────┐ ┌───────────┐ │
│ │ CDN │───>│ Nginx │───>│ Spring │ │ MySQL │ │
│ │(静态资源)│ │(负载均衡)│ │ Boot │───>│(分库分表) │ │
│ └─────────┘ └───────┘ │ 应用层 │ │ 8库64表 │ │
│ └─────┬─────┘ └───────────┘ │
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌───────────┐ │
│ │ Redis │ │ RabbitMQ│ │ ELK │ │
│ │(主从哨兵)│ │(消息队列)│ │(日志系统) │ │
│ └─────────┘ └─────────┘ └───────────┘ │
│ │
│ 核心数据:10 万 QPS,2000 万会员,日均交易 500 万笔 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
技术栈:
- 语言:Java 17
- 框架:Spring Boot 3.0
- 数据库:MySQL 8.0(分库分表)
- 缓存:Redis 6.0(主从哨兵)
- 消息队列:RabbitMQ 3.9
- 日志:ELK Stack
- 容器:Docker + Kubernetes
二、架构演进之路
2.1 Redis Cluster 替换主从哨兵:热 Key 问题的血泪史
问题发生
2024 年双 11,系统遇到了第一次大规模故障:
现象:Redis 主节点 CPU 飙到 100%,响应时间从 2ms 飙升到 500ms
影响:会员查询接口超时,下游服务雪崩
持续时间:2 小时
根因分析
事后分析发现,问题出在几个"超级热点 Key"上:
Key: "member:vip:count" → 每秒访问 5000+ 次(统计 VIP 会员数量)
Key: "member:online:list" → 每秒访问 3000+ 次(在线会员列表)
Key: "promotion:active" → 每秒访问 2000+ 次(当前活动状态)
主从哨兵架构的问题:
- 所有读请求都打到主节点(哨兵模式下,从节点只读)
- 热 Key 导致主节点 CPU 满,无法处理其他请求
- 从节点资源闲置,但无法分担热 Key 压力
解决方案:Redis Cluster
我们花了三个月,逐步迁移到 Redis Cluster:
// 迁移前:主从哨兵配置
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName("redis-master");
config.setPort(6379);
return new LettuceConnectionFactory(config);
}
// 迁移后:Cluster 配置
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisClusterConfiguration config = new RedisClusterConfiguration();
config.addClusterNode(new RedisNode("redis-1", 6379));
config.addClusterNode(new RedisNode("redis-2", 6379));
config.addClusterNode(new RedisNode("redis-3", 6379));
config.addClusterNode(new RedisNode("redis-4", 6379));
config.addClusterNode(new RedisNode("redis-5", 6379));
config.addClusterNode(new RedisNode("redis-6", 6379));
return new LettuceConnectionFactory(config);
}
热 Key 优化策略:
// 本地缓存 + Redis 二级缓存
@Service
public class HotKeyService {
private final Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();
@Autowired
private StringRedisTemplate redisTemplate;
public Object getHotKey(String key) {
Object value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value);
}
return value;
}
}
效果对比
| 指标 | 迁移前(主从哨兵) | 迁移后(Cluster) |
|---|---|---|
| P99 延迟 | 200ms | 25ms |
| CPU 峰值 | 100% | 30% |
| 单节点 QPS | 15,000 | 80,000 |
| 故障恢复时间 | 5 分钟 | 30 秒 |
2.2 Virtual Threads 替换线程池:连接数提升的魔法
问题发生
随着业务增长,会员系统的并发连接数从 5 万增长到 15 万,传统线程池遇到了瓶颈:
现象:线程池队列满,拒绝策略触发,大量请求被丢弃
影响:会员登录成功率从 99.9% 下降到 95%
持续时间:持续 3 天(逐步恶化)
根因分析
传统线程池配置:
- corePoolSize: 200
- maxPoolSize: 500
- queueCapacity: 1000
问题:
- 每个线程占用约 1MB 栈空间 → 500 线程 = 500MB
- 线程切换开销大 → CPU 大部分时间在切换线程
- 连接池耗尽 → 数据库连接不够用
解决方案:Virtual Threads
Java 21 发布后,我们第一时间开始试点 Virtual Threads:
// 迁移前:传统线程池
@Bean
public ExecutorService taskExecutor() {
return new ThreadPoolExecutor(
200,
500,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "task-" + counter.incrementAndGet());
t.setDaemon(true);
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
// 迁移后:Virtual Threads
@Bean
public ExecutorService taskExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
数据库连接池优化:
// 连接池配置
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/member");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(200);
config.setMinimumIdle(50);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
return new HikariDataSource(config);
}
⚠️ 关键警告:连接池成为新瓶颈
迁移到 Virtual Threads 后,我们发现了一个新问题:虚拟线程可以创建大量并发请求,但数据库连接池只有 200 个连接。这导致虚拟线程在等待数据库连接时大量阻塞,浪费了虚拟线程的优势。
解决方案:使用 Semaphore 限制并发
@Service
public class DatabaseService {
private static final int MAX_CONCURRENT_QUERIES = 200;
private final Semaphore semaphore = new Semaphore(MAX_CONCURRENT_QUERIES);
@Autowired
private JdbcTemplate jdbcTemplate;
public <T> T executeWithLimit(Supplier<T> task) throws InterruptedException {
semaphore.acquire();
try {
return task.get();
} finally {
semaphore.release();
}
}
public List<Member> queryMembers(String condition) throws InterruptedException {
return executeWithLimit(() ->
jdbcTemplate.query("SELECT * FROM members WHERE " + condition,
new MemberRowMapper()));
}
}
这样既保留了 Virtual Threads 的优势,又避免了连接池耗尽的问题。
效果对比
| 指标 | 迁移前(传统线程池) | 迁移后(Virtual Threads) |
|---|---|---|
| 最大并发连接 | 5,000 | 50,000+ |
| P99 延迟 | 300ms | 80ms |
| 内存占用 | 1.5GB | 500MB |
| 线程切换开销 | 高 | 极低 |
| 请求拒绝率 | 5% | 0.1% |
2.3 OpenTelemetry 替换 ELK:可观测性升级
问题发生
随着微服务数量从 10 个增长到 30 个,ELK 的问题越来越明显:
现象:
1. 日志查询慢,一个 Trace 需要在多个 Kibana 索引中搜索
2. 链路追踪不完整,TraceID 在异步线程中丢失
3. 监控指标分散在不同系统(Prometheus + ELK + Zipkin)
4. 告警规则维护困难,每个服务一套规则
根因分析
ELK 本质上是一个日志系统,不是完整的可观测性平台:
- 日志:ELK 擅长,但查询慢、成本高
- 指标:需要额外部署 Prometheus
- 追踪:需要额外部署 Zipkin/Jaeger
- 关联:三者之间无法自动关联
解决方案:OpenTelemetry
我们用 OpenTelemetry 替换了 ELK,实现了日志、指标、追踪的统一:
// OpenTelemetry 配置
@Configuration
public class OtelConfig {
@Bean
public OpenTelemetry openTelemetry() {
return OpenTelemetrySdk.builder()
.setTracerProvider(SdkTracerProvider.builder()
.addSpanProcessor(SimpleSpanProcessor.create(
OtlpGrpcSpanExporter.builder().build()))
.build())
.setMeterProvider(SdkMeterProvider.builder()
.registerMetricReader(PeriodicMetricReader.builder(
OtlpGrpcMetricExporter.builder().build()).build())
.build())
.setLoggerProvider(SdkLoggerProvider.builder()
.addLogRecordProcessor(SimpleLogRecordProcessor.create(
OtlpGrpcLogRecordExporter.builder().build()))
.build())
.buildAndRegisterGlobal();
}
}
自动注入 TraceID:
// 过滤器:自动注入 TraceID 到 MDC
@Component
public class TraceIdFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
SpanContext spanContext = Span.current().getSpanContext();
if (spanContext.isValid()) {
MDC.put("traceId", spanContext.getTraceId());
}
try {
chain.doFilter(request, response);
} finally {
MDC.remove("traceId");
}
}
}
效果对比
| 指标 | 迁移前(ELK) | 迁移后(OpenTelemetry) |
|---|---|---|
| Trace 查询时间 | 10-30 秒 | < 1 秒 |
| 指标覆盖度 | 60% | 100% |
| 告警规则数 | 50+ | 15(统一规则) |
| 问题定位时间 | 30 分钟 | 5 分钟 |
| 存储成本 | 高 | 降低 40% |
2.4 新增 AI 会员推荐模块:从规则引擎到机器学习
业务背景
会员系统需要根据用户行为推荐个性化内容,但传统的规则引擎越来越难以满足需求:
传统规则:
- 用户购买过 A 商品 → 推荐同类商品
- 用户浏览过 B 页面 → 推荐相关商品
- VIP 用户 → 推荐高端商品
问题:
- 规则太多,维护困难
- 推荐效果差,点击率只有 5%
- 无法发现潜在的用户兴趣
解决方案:AI 推荐系统
我们搭建了基于机器学习的推荐系统:
// 推荐服务
@Service
public class RecommendationService {
@Autowired
private RecommendationClient recommendationClient;
public List<RecommendItem> getRecommendations(Long userId, int count) {
RecommendRequest request = RecommendRequest.builder()
.userId(userId)
.count(count)
.context(getUserContext(userId))
.build();
return recommendationClient.recommend(request);
}
private UserContext getUserContext(Long userId) {
// 获取用户画像、行为历史等
return UserContext.builder()
.age(getUserAge(userId))
.gender(getUserGender(userId))
.interests(getUserInterests(userId))
.purchaseHistory(getPurchaseHistory(userId))
.build();
}
}
推荐系统架构:
┌─────────────────────────────────────────────────────────────┐
│ AI 推荐模块 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 特征工程 │───>│ 模型训练 │───>│ 模型服务 │ │
│ │ 用户行为分析│ │ XGBoost │ │ TensorFlow │ │
│ │ 商品画像 │ │ 协同过滤 │ │ Serving │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 推荐缓存层 │ │
│ │ Redis + Caffeine 本地缓存,热点推荐结果加速 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
效果对比
| 指标 | 迁移前(规则引擎) | 迁移后(AI 推荐) |
|---|---|---|
| 推荐点击率 | 5% | 18% |
| 推荐转化率 | 2% | 8% |
| 规则维护成本 | 高(每月更新) | 低(自动训练) |
| 用户满意度 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
三、2026 年架构全景图
┌─────────────────────────────────────────────────────────────────────────┐
│ 2026 年架构(高亮部分为新增/变更) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌───────┐ ┌───────────┐ ┌───────────┐ │
│ │ CDN │───>│ Nginx │───>│ Spring │ │ MySQL │ │
│ │(静态资源)│ │(负载均衡)│ │ Boot │───>│(分库分表) │ │
│ └─────────┘ └───────┘ │ 应用层 │ │ 8库64表 │ │
│ │ Java 21 │ └───────────┘ │
│ │Virtual Threads│ │
│ └─────┬─────┘ │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────┐ ┌─────────────────────┐ │
│ │ Redis │ │RabbitMQ │ │ OpenTelemetry │ │
│ │ Cluster │ │(消息队列)│ │(日志/指标/追踪一体) │ │
│ │(6节点集群) │ │ │ │ │ │
│ └─────────────┘ └─────────┘ └─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ AI 推荐模块 │ ⬅ 新增 │
│ │ 特征/训练/服务 │ │
│ └─────────────────┘ │
│ │
│ 核心数据:15 万 QPS,3500 万会员,日均交易 1000 万笔 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
变更汇总:
| 组件 | 2024 年 | 2026 年 | 变更原因 |
|---|---|---|---|
| 语言 | Java 17 | Java 21 | Virtual Threads |
| Redis | 主从哨兵 | Cluster | 热 Key 问题 |
| 线程池 | 传统线程池 | Virtual Threads | 并发连接数瓶颈 |
| 可观测性 | ELK | OpenTelemetry | 统一日志/指标/追踪 |
| 推荐系统 | 规则引擎 | AI 推荐 | 推荐效果差 |
四、关键指标仪表盘
┌────────────────────────────────────────────────────────────────────┐
│ 会员系统关键指标对比 │
├────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┬────────────────┬────────────────┬─────────────┐ │
│ │ 指标 │ 2024 年 │ 2026 年 │ 变化率 │ │
│ ├──────────────┼────────────────┼────────────────┼─────────────┤ │
│ │ QPS │ 100,000 │ 150,000 │ +50% │ │
│ │ 会员数 │ 2000 万 │ 3500 万 │ +75% │ │
│ │ 日均交易 │ 500 万笔 │ 1000 万笔 │ +100% │ │
│ │ P99 延迟 │ 200ms │ 50ms │ -75% │ │
│ │ 可用性 │ 99.9% │ 99.99% │ +0.09% │ │
│ │ Redis CPU │ 100% │ 30% │ -70% │ │
│ │ 内存占用 │ 4GB │ 3GB │ -25% │ │
│ │ 推荐点击率 │ 5% │ 18% │ +260% │ │
│ │ 问题定位时间 │ 30 分钟 │ 5 分钟 │ -83% │ │
│ └──────────────┴────────────────┴────────────────┴─────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────┘
五、仍未解决的问题
真实的架构演进不是完美的,我们还有很多问题没解决:
5.1 MySQL 分库分表的瓶颈
问题:
- 分库分表后,跨库查询困难
- 数据迁移复杂,扩容成本高
- 事务一致性难以保证
现状:
- 暂时没有更好的方案,只能继续优化 SQL
- 正在评估 TiDB 作为替代方案
5.2 RabbitMQ 的消息堆积
问题:
- 高峰期消息堆积严重,需要人工干预
- 死信队列处理不完善
- 消息顺序性无法保证
现状:
- 增加了消费者数量
- 优化了消息处理逻辑
- 正在评估 Kafka 作为替代方案
5.3 AI 推荐系统的冷启动
问题:
- 新用户没有历史行为数据,推荐效果差
- 新商品没有曝光数据,难以被推荐
现状:
- 使用热门商品兜底
- 逐步收集用户行为数据
- 探索冷启动算法
六、架构演进的思考
6.1 架构演进的本质
很多人以为架构演进是:
┌─────────┐ ┌─────────┐
│ 旧架构 │───>│ 新架构 │
└─────────┘ └─────────┘
但真实的架构演进是:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 旧架构 │───>│ 修修补补 │───>│ 再修补 │───>│ 新架构 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
每一步都是在生产环境的压力下,发现问题、解决问题、再发现新问题的循环。
6.2 什么时候该重构
| 条件 | 是否重构 |
|---|---|
| 当前架构能满足业务需求 | ❌ 不需要 |
| 当前架构无法满足业务需求,但有临时方案 | ❌ 先临时方案 |
| 当前架构无法满足业务需求,且没有临时方案 | ✅ 需要重构 |
| 重构成本 < 维护成本 | ✅ 需要重构 |
| 重构成本 > 维护成本 | ❌ 不值得 |
6.3 架构师的职责
架构师不是画架构图的人,而是:
1. 解决问题的人:在生产环境出现问题时,能快速定位并解决
2. 权衡利弊的人:在多个方案中选择最适合当前业务的
3. 持续演进的人:不追求完美,追求不断改进
4. 背锅的人:出问题时第一个站出来承担责任
七、总结
这两年的架构演进,让我明白了一个道理:
真正的架构不是设计出来的,是演化出来的。
我们没有画过一张全新的架构图,而是在原有的基础上,根据业务的变化和生产环境的反馈,一点点地修改、优化、替换。每一次变更都不是心血来潮,而是被逼无奈——要么是遇到了性能瓶颈,要么是遇到了故障,要么是业务需求发生了变化。
这种"修修补补"的演进方式,看似不完美,但却是最真实、最有效的。因为它保证了系统的稳定性,让业务能够持续发展,同时也让我们的技术团队在实战中不断成长。
互动话题:你的系统经历过哪些架构演进?最大的挑战是什么?欢迎留言分享!
附录
技术栈对比
| 分类 | 2024 年 | 2026 年 |
|---|---|---|
| 语言 | Java 17 | Java 21 |
| 框架 | Spring Boot 3.0 | Spring Boot 3.2 |
| 数据库 | MySQL 8.0 | MySQL 8.0 |
| 缓存 | Redis 6.0(主从哨兵) | Redis 7.0(Cluster) |
| 消息队列 | RabbitMQ 3.9 | RabbitMQ 3.11 |
| 可观测性 | ELK Stack | OpenTelemetry |
| 推荐系统 | 规则引擎 | AI 推荐(XGBoost + TensorFlow) |
| 容器 | Docker + K8s 1.25 | Docker + K8s 1.29 |
参考资料
订阅提醒:关注我获取更多架构实战经验分享!
标题:2024→2026 版本升级:10 万 QPS 会员系统架构,两年后我们改了什么
作者:jiangyi
地址:http://jiangyi.space/articles/2026/07/29/1785082535642.html
公众号:服务端技术精选
- 引言
- 一、原架构回顾
- 二、架构演进之路
- 2.1 Redis Cluster 替换主从哨兵:热 Key 问题的血泪史
- 问题发生
- 根因分析
- 解决方案:Redis Cluster
- 效果对比
- 2.2 Virtual Threads 替换线程池:连接数提升的魔法
- 问题发生
- 根因分析
- 解决方案:Virtual Threads
- 效果对比
- 2.3 OpenTelemetry 替换 ELK:可观测性升级
- 问题发生
- 根因分析
- 解决方案:OpenTelemetry
- 效果对比
- 2.4 新增 AI 会员推荐模块:从规则引擎到机器学习
- 业务背景
- 解决方案:AI 推荐系统
- 效果对比
- 三、2026 年架构全景图
- 四、关键指标仪表盘
- 五、仍未解决的问题
- 5.1 MySQL 分库分表的瓶颈
- 5.2 RabbitMQ 的消息堆积
- 5.3 AI 推荐系统的冷启动
- 六、架构演进的思考
- 6.1 架构演进的本质
- 6.2 什么时候该重构
- 6.3 架构师的职责
- 七、总结
- 附录
- 技术栈对比
- 参考资料
评论