一、引言:高并发读写问题分类与挑战
在大型互联网系统中,所有的业务操作最终都归结为读和写两种操作。不同业务场景对读写的要求各不相同,我们需要根据具体业务特点采取不同的优化策略。本章将从读写两个维度深入分析高并发问题的解决方案。
1.1 业务场景分类
根据读写特点的不同,大型系统可以分为三类:
1.1.1 侧重于"高并发读"的系统
场景1:搜索引擎
读写对比:
用户规模:读端亿级用户,写端百万级发布者
响应时间:读要求毫秒级,写允许分钟级
频率:读频率远高于写频率
场景2:电商商品搜索
类似搜索引擎,卖家发布商品,买家搜索商品
读写特征与搜索引擎类似
场景3:商品描述、图片和价格
C端买家只读,B端卖家可修改
读频率远高于写频率
1.1.2 侧重于"高并发写"的系统
场景:广告扣费系统
每次浏览或点击都需扣减广告主余额
要求实时扣费,避免平台流量损失
C端用户行为触发高频写操作
1.1.3 同时有"高并发读"和"高并发写"的系统
场景1:电商库存和秒杀系统
用户同时进行高并发读(查看库存)和写(扣减库存)
数据需实时更新,保证一致性
12306火车票系统是更复杂版本
场景2:支付系统和微信红包
用户实时查看余额(高并发读)
转账扣款(高并发写)
数据一致性要求极高
红包系统涉及一人发多人抢,更复杂
场景3:IM、微博和朋友圈
用户发送和接收消息
发微博/朋友圈和查看内容
读写两端都面临高并发压力
二、高并发读策略:多层次优化方案
2.1 策略1:加缓存/读副本
缓存是以空间换时间的经典策略,通过数据冗余提升读性能。
方案1:本地缓存与集中式缓存
缓存数据结构:
<k, v>结构:对应数据库单行记录<k, list>结构:列表数据<k, hash>结构:哈希表数据
缓存更新策略:
主动更新:数据库变更时主动删除/更新缓存
被动更新:查询时缓存过期则重新加载
缓存问题与解决方案:
| 问题 | 描述 | 解决方案 |
|---|---|---|
| 缓存高可用 | 缓存宕机导致数据库压力 | 缓存集群、多级缓存 |
| 缓存穿透 | 查询不存在的数据 | 布隆过滤器、空值缓存 |
| 缓存击穿 | 热点Key失效瞬间 | 永不过期、互斥锁 |
| 缓存雪崩 | 大量Key同时过期 | 随机过期时间、预热 |
回源策略:
不回源:缓存没有直接返回空,需主动更新缓存
回源:缓存没有则查询数据库,需处理缓存问题
方案2:MySQL主从复制
对于复杂业务查询,通过MySQL主从架构分担读压力:
Master处理写操作
多个Slave处理读操作
简单有效的读写分离方案
方案3:CDN/静态文件加速/动静分离
静态内容处理:
图片、HTML、JS、CSS等静态文件
视频直播内容(内容相同)
使用CDN全网缓存,就近访问
动态内容处理:
根据用户信息实时生成
需应用服务器实时计算
技术本质:Redis、MySQL Slave、CDN都是"加副本"策略的变体。
2.2 策略2:并发读
将串行操作改为并行,提升读性能。
方案1:异步RPC
同步调用:T = T1 + T2 + T3
异步调用:T = Max(T1, T2, T3)
前提条件:多个调用间无依赖关系,可并行执行。
方案2:冗余请求(对冲请求)
问题背景:
分布式系统中单节点延迟率低(如1%)
但请求涉及节点多时,整体延迟率高
100个节点,每节点99%成功率,整体成功率:99%¹⁰⁰ ≈ 36.6%
整体延迟率:1 - 36.6% = 63.4%
解决方案:
同时发送多个相同请求,使用最快返回的结果
对冲请求:
先发一个请求
等待内部服务95%请求的响应时间
未收到响应则发送第二个请求
使用最先返回的结果
Google测试数据:
仅用2%额外请求
将99.9%请求响应时间从1800ms降低到74ms
2.3 策略3:重写轻读
将计算逻辑从读端转移到写端,读时直接获取预处理结果。
方案1:微博Feeds流实现
原始方案问题:
关注关系表和微博发布表分开
查询Feeds流需要两条SQL和聚合操作
无法满足高并发查询
重写轻读方案:
发件箱:用户发布的微博
收件箱:粉丝接收的微博列表
写扩散:发布微博后,异步推送到所有粉丝收件箱
读优化:用户直接读取自己的收件箱,无需实时聚合
关键技术挑战:
收件箱实现:使用Redis的
<key, list>结构容量限制:最多保存2000条(Twitter限制800条)
历史数据存储:MySQL分片存储
分页查询:需要二级索引定位数据位置
推拉结合策略:
推:粉丝数<5000的用户,发布时推送给所有粉丝
拉:粉丝数>5000的用户,只推送给在线粉丝
聚合:读时聚合推送和拉取的数据
方案2:宽表与搜索引擎
多表关联查询问题:
分库后无法使用原生join
程序聚合无法支持排序和分页
数据量大时内存计算不可行
解决方案:
宽表:提前计算关联结果,定时或触发更新
搜索引擎:将join结果做成文档存入ES,支持灵活查询
2.4 总结:读写分离(CQRS架构)
核心特征:
数据结构分离:为读和写设计不同的数据模型
压力分担:写端通过分库分表应对压力,读端通过缓存/宽表/ES应对压力
数据同步:定时任务、消息中间件、Binlog监听
一致性模型:最终一致性,读比写有延迟
业务容忍度:
微博:粉丝延迟几秒看到可以接受
库存:读有延迟,但下单时实时扣减库存
账户余额:自己写自己读需要强一致性
三、高并发写方案:提升写入性能的策略
3.1 策略1:数据分片
通过数据拆分实现并行处理。
应用场景:
数据库分库分表:应对高并发写压力
Redis Cluster:分布式存储
ES分布式索引:将大索引拆分为多个小索引并行查询
3.2 策略2:异步化
将同步操作改为异步,提升系统吞吐量。
案例1:短信验证码注册/登录
同步问题:
调用第三方短信平台需1-2秒
Tomcat线程被阻塞,并发能力有限
异步方案:
请求放入消息队列,立即返回
后台消费者调用短信平台
内网通信,不受外网延迟影响
案例2:电商订单拆单
处理流程:
创建订单并支付
支付成功立即返回
后台异步拆单(1个订单拆为多个子订单)
卖家分别发货
案例3:广告计费系统
异步处理流程:
点击请求以日志形式落盘
立即返回客户端
流式计算处理后续逻辑
异步扣费,避免数据库压力
案例4:写内存+Write-Ahead日志
应用场景:高并发扣减库存或余额
实现方案:
在Redis中扣减
同时写入WAL日志(消息队列或数据库)
Redis宕机后重放日志恢复
数据库初始化Redis数据
3.3 策略3:批量写
将多次写操作合并为一次,减少I/O次数。
案例1:广告计费系统合并扣费
原始方案:10次点击扣10次钱
批量方案:10次点击合并为1次扣10元
实现方式:
从消息队列批量获取消息
按广告主ID分组
累加扣费金额
一次数据库操作完成扣费
案例2:MySQL小事务合并
数据库优化:
10次扣1个库存 → 1次扣10个库存
10个事务合并为1个事务
应用实例:Canal同步代码中的缓存更新
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) { if(StringUtils.isNotEmpty(key)) redisOpsExtUtil.delete(key); }多机房数据同步:事务合并加速数据库复制。
四、实战总结与架构演进
4.1 读写分离架构演进路径
| 阶段 | 读写策略 | 适用场景 |
|---|---|---|
| 初级阶段 | 缓存(读写同一数据结构) | 简单查询加速 |
| 中级阶段 | 读写分离(不同数据结构) | 复杂业务场景 |
| 高级阶段 | CQRS架构(完全分离) | 高并发读写场景 |
4.2 技术选型建议
缓存选型:
本地缓存:Caffeine、Guava Cache
分布式缓存:Redis、Memcached
CDN:静态资源加速
数据库选型:
OLTP:MySQL(分库分表)、PostgreSQL
OLAP:ClickHouse、HBase
搜索:Elasticsearch、Solr
消息队列:
高吞吐:Kafka、RocketMQ
低延迟:RabbitMQ、Pulsar
4.3 监控与调优
关键监控指标:
缓存:命中率、响应时间、内存使用率
数据库:QPS、连接数、慢查询
消息队列:积压量、消费延迟
系统整体:吞吐量、错误率、响应时间
调优策略:
容量规划:根据业务峰值预留30%余量
弹性伸缩:基于监控指标自动扩缩容
故障演练:定期测试降级和熔断策略
性能测试:定期压测,发现瓶颈点
五、未来发展趋势
5.1 智能化调优
基于机器学习的自动参数调优
智能缓存预热和淘汰策略
自适应流量调度
5.2 云原生架构
容器化部署和弹性伸缩
Serverless无服务器架构
多云和混合云部署
5.3 数据湖与实时数仓
批流一体数据处理
实时数据分析和决策
数据湖与业务系统深度融合