海量数据高并发读写方案设计

一、高并发系统核心分类(按读写特征)

核心判定维度:用户规模数量级、响应时间要求、读写频率,Java 面试中常结合电商、IM、支付等场景考察分类及对应技术选型。

1. 高并发读为主系统

核心特征 :读请求量远大于写,读响应要求毫秒级,写响应可容忍延迟。典型场景:搜索引擎、电商商品搜索 / 商品描述 & 图片 & 价格查询、静态资源访问。

2. 高并发写为主系统

核心特征 :写请求高频且实时性要求高,读请求相对较少。典型场景:广告计费 / 扣费系统、日志落盘系统。

3. 高并发读写混合系统

核心特征 :C 端用户同时发起高并发读和写,数据一致性要求高(部分场景强一致,部分最终一致)。典型场景:电商库存 / 秒杀系统、支付 / 微信红包系统、IM / 微博 / 朋友圈、12306 票务系统。

二、高并发读核心解决方案

Java 后端中高并发读是面试必考模块,核心策略围绕缓存 / 副本、并发优化、重写轻读 展开,最终落地为读写分离(CQRS) 架构。

策略 1:加缓存 / 读副本(最常用)

本质是空间换时间,通过数据冗余降低主库 / 源系统压力,Java 开发中需掌握不同缓存方案的选型、问题及解决。

方案 1:本地缓存 & 集中式缓存
  • 本地缓存:Caffeine、Guava Cache(Java 常用),优点是无网络开销,缺点是缓存不共享、内存占用高。

  • 集中式缓存:Redis、Memcached(Java 主流),<k,v>/<k,list>/<k,hash > 结构,适配关系型数据库单表数据缓存。

  • 缓存更新方式

    1. 主动更新:数据库数据变更时,主动删除 / 更新缓存(如 MySQL Binlog 监听 + Canal)。
    2. 被动更新:缓存过期 / 不存在时,查询数据库回源更新(懒加载)。
  • 面试高频问题:缓存四大问题及解决

    表格

    问题 现象描述 常见解决方法(Java)
    缓存穿透 大量不存在的 Key 查询,穿透缓存压垮数据库 布隆过滤器过滤无效 Key、缓存空值(设置短过期时间)
    缓存击穿 热点 Key 失效瞬间,大并发请求直接访问数据库 互斥锁(Redis SETNX)、热点 Key 永不过期、分布式锁(Redisson)
    缓存雪崩 大量 Key 同时过期 / 缓存宕机,所有请求压垮数据库 过期时间随机化、多级缓存(本地 + 集中式)、缓存集群高可用(Redis Cluster)
    缓存高可用 缓存服务宕机导致系统雪崩 缓存集群部署、主从复制、哨兵模式、熔断降级(Sentinel/Hystrix)
方案 2:MySQL Master/Slave(读副本)
  • 核心作用:主库负责写,从库负责读,分担主库读压力,Java 中通过 MyBatis 多数据源配置实现。
  • 适用场景:复杂业务数据查询,无法通过简单 K-V 缓存满足的场景。
  • 注意点:主从同步存在延迟,需结合业务容忍度设计(最终一致性)。
方案 3:CDN / 静态文件加速 / 动静分离
  • 静态内容:图片、HTML/JS/CSS、视频(Java 中通过 Nginx+CDN 部署)。
  • 动态内容:用户个性化数据、实时计算结果(由 Java 后端服务生成)。
  • 核心逻辑:静态内容缓存到全网 CDN 节点,用户就近访问,避免穿透到源服务器。
缓存 / 副本策略整体架构:

策略 2:并发读优化

将串行读改为并行读,降低请求总耗时,Java 中结合 RPC 框架、并发工具实现。

方案 1:异步 RPC
  • 核心原理:并行调用多个无耦合的 RPC 接口,总耗时 = 最大单接口耗时(同步为耗时之和)。
  • Java 实现:Dubbo 异步调用、Feign 异步调用、CompletableFuture 实现接口并行调用。
  • 适用场景:一个请求需要调用多个独立的微服务接口(如商品详情页查询商品、库存、评价)。
方案 2:冗余请求(对冲请求)
  • 核心原理:针对分布式系统长尾延迟问题,超时后向其他服务器发送重复请求,取最快返回结果。
  • 关键参数:等待时间 = 内部服务 95% 请求的响应时间。
  • 效果:Google 测试显示,仅 2% 额外请求可将 99.9% 请求响应时间从 1800ms 降至 74ms。
  • Java 注意点:需实现请求取消机制,避免无效请求占用服务器资源。

策略 3:重写轻读(将计算从读端移到写端)

面试常考微博 Feeds 流实现 ,核心是写扩散 / 推拉结合,Java 中结合 Redis+MySQL 实现。

方案 1:微博 Feeds 流的实现(推拉结合)
1. 基础设计:发件箱 + 收件箱(写扩散)
  • 发件箱:存储用户自己发布的内容(MySQL+Redis),全量持久化。
  • 收件箱:存储关注用户的内容(Redis List),限制长度(如 2000 条),满足高并发读。
  • 写逻辑:用户发内容,先写入自己发件箱,后台异步推送到粉丝收件箱(Java 异步线程 / 消息队列)。
  • 读逻辑:直接读取 Redis 收件箱,无需实时聚合,毫秒级返回。
2. 优化:推拉结合(解决大 V 粉丝过多问题)
  • 推:粉丝数 < 阈值(如 5000),发布内容直接推送到所有粉丝收件箱。
  • 拉:粉丝数 > 阈值(如大 V),仅推送给在线粉丝,普通用户读 Feeds 流时主动拉取大 V 内容。
  • 聚合:读端将 "推的内容 + 拉的内容" 按时间排序,分页返回(Java 中通过 Comparator/Stream 实现)。
3. 数据持久化与分片
  • Redis:存储最近的热点 Feeds 流(满足高并发读)。
  • MySQL :全量存储,按user_id + 时间范围 分片,配合二级索引表(user_id, 月份,count) 实现分页查询。
Feeds 流推拉结合架构:
方案 2:多表关联查询优化(宽表 + 搜索引擎)
  • 问题:分库分表后,数据库原生 Join 失效,程序聚合排序分页性能差。
  • 解决方案 1:宽表提前将多表关联数据计算好,存入宽表(Java 中通过定时任务 / Canal 监听 Binlog 更新宽表),读端直接查询宽表。
  • 解决方案 2:搜索引擎将多表 Join 结果构造成文档,存入 Elasticsearch(ES),利用 ES 的高效排序和分页能力满足高并发读。
  • Java 实现:Canal 监听 MySQL Binlog,同步数据到 ES / 宽表,通过 MyBatis/ES Client 查询。

总结:读写分离(CQRS 架构)

Command Query Responsibility Separation,命令(写)与查询(读)职责分离,是高并发读的终极解决方案,Java 微服务中主流落地架构。

CQRS 核心特征
  1. 读写设计不同的数据结构:写库用关系型数据库(MySQL),读库用缓存 / 宽表 / ES。
  2. 写端:分库分表抵抗写压力;读端:缓存 / ES/CDN 抵抗读压力。
  3. 读写数据同步:定时任务 /消息队列 /Binlog 监听(Canal)。
  4. 数据一致性:最终一致性 (读比写有延迟),核心数据可做强一致性(如用户自身数据)。
  5. 数据可靠性:同步通道需保证不丢失、不乱序(如 RocketMQ/Kafka 持久化)。
CQRS 架构整体流程:

三、高并发写核心解决方案

高并发写的核心思路是拆分、异步、批量 ,Java 中结合分库分表、消息队列、流式计算实现,需重点掌握落地细节。

策略 1:数据分片(拆分)

将数据 / 请求拆分为多份,并行处理,分散单节点写压力,Java 后端中主流基于Sharding-JDBC实现。

1. 数据库分库分表
  • 分表:单表数据量过大,拆分为多张表(如用户表按 user_id 取模),同库,利用 CPU / 内存资源。
  • 分库:单库写压力过大,拆分为多个库,多台机器,水平扩展写能力。
  • 分片维度 :按主键(user_id/order_id) 、时间范围 、业务维度(如按地区)。
  • Java 实现:Sharding-JDBC(轻量级,无中间件)、MyCat(中间件)。
2. 中间件分片
  • Redis:Redis Cluster(按槽位分片),Java 中通过 JedisCluster/Spring Data Redis 实现。
  • ES:分布式索引(按文档 ID 分片),并行写入 / 查询。
3. 核心注意点
  • 分片规则需提前规划,避免后期数据迁移。
  • 避免跨分片查询 / 写 (如联表、范围查询),如需跨分片,Java 中通过归并排序处理。
  • 分片后需考虑数据扩容(如弹性分片、预分片)。

策略 2:异步化(核心)

将同步写改为异步写,快速返回请求,后台处理业务 ,Java 中通过消息队列实现,是解决高并发写的最核心手段。

核心原理

客户端发送请求后,服务端将请求放入消息队列(如 RocketMQ/Kafka),立即返回 "请求接收成功",后台消费者从队列取消息异步处理。

典型案例
案例 1:短信验证码注册 / 登录
  • 同步问题:调用第三方短信平台(HTTP 调用,1-2s),Tomcat 线程被阻塞,并发量低。
  • 异步实现:Java 后端将短信请求放入 RocketMQ,返回 "发送中",消费者线程调用短信平台,完成发送。
  • 优势:内网通信无阻塞,Tomcat 可处理海量请求,消息队列削峰填谷。
案例 2:电商订单拆单
  • 业务场景:用户下单多个商品(来自不同卖家),需拆分为多个子订单,卖家分别发货。
  • 异步实现:支付成功后,Java 后端将拆单请求放入消息队列,立即返回 "支付成功",消费者线程异步完成拆单、创建子订单、通知卖家。
  • 核心原则 :不阻碍主流程的业务,全部异步化。
案例 3:广告计费系统
  • 异步实现 :用户点击 / 浏览广告,请求先落盘(日志 / 消息队列),立即返回,后续通过流式计算(Flink/Spark) 异步处理反作弊、扣费。
  • Java 配合:日志采集(Logback/Log4j2)、消息队列(Kafka)、流式计算(Flink Java API)。
案例 4:写内存 + Write-Ahead Log(WAL)
  • 核心思路:借鉴 MySQL Redo Log,先写内存(Redis)保证高性能,再落日志保证数据可靠性。
  • Java 实现:扣库存 / 扣余额时,先通过 Redis INCR/DECR 完成写操作,同时将操作日志写入消息队列 / MySQL,若 Redis 宕机,重放日志恢复数据。
  • 适用场景:超高并发写(如秒杀扣库存),要求毫秒级响应,同时保证数据不丢失。
异步化核心架构:

策略 3:批量写(合并)

将多个小的写请求合并为一个大的写请求,减少数据库 IO 次数,提升写性能,Java 中结合定时任务、消息队列实现。

典型案例
案例 1:广告计费系统合并扣费
  • 业务场景:同一个广告主的多个扣费请求(如 10 次点击,每次扣 1 元)。
  • 批量实现:Java 消费者从消息队列批量取消息,按广告主 ID 分组,累加扣费金额(10 次合并为扣 10 元),一次更新数据库。
  • 优势:将 10 次数据库更新变为 1 次,大幅降低 IO 压力。
案例 2:MySQL 小事务合并 & Java 业务借鉴
  • MySQL 原理:对同一个 SKU 的多次扣库存(10 次扣 1 个),内核合并为 1 次扣 10 个,减少事务开销。

  • Java 业务实现 :如缓存更新,一个表多次修改,仅需删除一次 Redis 缓存(通过 Set 去重,批量删除)。

    java

    运行

    复制代码
    // Java代码示例:批量删除缓存(借鉴小事务合并)
    Set<String> factKeys = new HashSet<>();
    for (CanalEntry.Entry entry : message.getEntries()) {
        String tableName = entry.getHeader().getTableName();
        factKeys.add(tableMapKey.get(tableName)); // 去重,避免重复删除
    }
    for (String key : factKeys) {
        redisTemplate.delete(key); // 批量删除
    }
  • 多机房场景:事务合并是加速数据库跨机房复制的重要手段,减少网络传输次数。

批量写核心流程(Mermaid)
相关推荐
卓怡学长31 分钟前
w214基于jsp知道特产网
java·intellij-idea
步行cgn34 分钟前
Spring 注解使用详解
java·spring
一条小小yu1 小时前
Spring IoC的理解
java·后端·spring
落魄实习生1 小时前
Agent Scope Java 2.x 系列【7】工具使用
java·开发语言·ai
旺仔学长 哈哈1 小时前
springboot钓鱼爱好者交流平台APP设计与实现
java·spring boot·mysql·充电桩管理系统
自强的小白3 小时前
核心功能(Service接口)
java·mybatis
乌暮4 小时前
深入理解 Java 泛型:把「万能盒子」用对、用稳
java·开发语言·后端·学习
大猫和小黄4 小时前
深入理解 Java Optional:从设计哲学到工程实战
java
数据库百宝箱4 小时前
rum&gin索引对比
java·数据库·gin