亿级分布式系统架构演进实战(一)- 总体概要
亿级分布式系统架构演进实战(二)- 横向扩展(服务无状态化)
亿级分布式系统架构演进实战(三)- 横向扩展(数据库读写分离)
亿级分布式系统架构演进实战(四)- 横向扩展(负载均衡与弹性伸缩)
核心目标
降低数据库读压力,提升响应速度
一、多级缓存架构
客户端 CDN/浏览器缓存 本地应用缓存 分布式缓存 数据库缓冲池
1.1 客户端缓存
缓存数据类型 :
• 静态资源(JS/CSS/图片)
• 用户个性化配置
• 低频更新的业务数据(如城市列表)
数据失效机制:
nginx
# Nginx配置带哈希版本号的资源路径
location /static/v1.0.3/ {
add_header Cache-Control "public, max-age=31536000";
}
实现方案:
html
<!-- HTML页面声明版本号 -->
<script src="/static/js/main.v1.2.3.js"></script>
1.2 本地缓存(Caffeine)
缓存数据类型 :
• 高频访问的动态数据(如商品基础信息)
• 短期有效的中间计算结果
多节点一致性方案:
java
// 使用Redis PubSub同步失效消息
redisTemplate.listen("cache_invalidate", (message) -> {
caffeineCache.invalidate(message);
});
监控配置:
java
CaffeineCache cache = Caffeine.newBuilder()
.recordStats()
.build();
// 获取命中率指标
cache.stats().hitRate(); // 命中率
cache.stats().loadFailureRate(); // 加载失败率
1.3 分布式缓存
缓存数据类型 :
• 全局共享数据(如库存信息)
• 会话级数据(如购物车信息)
读写方案:
java
public Product getProduct(String skuId) {
Product product = redis.get(skuKey);
if (product == null) {
product = loadFromDB(skuId);
redis.setex(skuKey, 300, product); // 5分钟过期
}
return product;
}
与本地缓存配合:
java
public Product getProductWithLocalCache(String skuId) {
Product product = caffeineCache.getIfPresent(skuId);
if (product == null) {
product = redis.get(skuKey);
caffeineCache.put(skuId, product);
}
return product;
}
1.4 数据库缓存
底层原理 :
• InnoDB缓冲池缓存磁盘数据页
• 禁用Query Cache缓存完整查询结果(MySQL 8.0已弃用)
配置建议:
sql
-- 设置缓冲池大小为物理内存的70%
SET GLOBAL innodb_buffer_pool_size=16G;
# 设置 Buffer Pool 实例数量为 8
innodb_buffer_pool_instances = 8
-- 预热常用表数据
SELECT * FROM products WHERE id <= 1000;
--在高并发场景下建议禁用Query Cache,各位可以测试结果决定是否禁用
query_cache_type = 0
query_cache_size = 0
监控方法:
sql
SHOW STATUS LIKE 'innodb_buffer_pool_read%';
-- 命中率计算公式:
-- (1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) * 100%
二、多级缓存策略设计
2.1 淘汰策略设计
不合理淘汰策略的典型问题与解决方案
问题类型 | 根本原因 | 具体解决方案 | 实施细节 |
---|---|---|---|
缓存雪崩 | 大量缓存同时失效 | 错峰过期时间 + 互斥锁 + 熔断机制 | 基础过期时间增加随机偏移(±10%),结合分布式锁控制重建并发度 |
资源浪费 | 无效数据长期驻留 | LRU/LFU淘汰算法 + 定期扫描清理 | 本地缓存配置权重淘汰,分布式缓存设置最大内存限制并启用主动清理任务 |
数据不一致 | 多级缓存失效不同步 | 版本号控制 + 失效广播机制 | 每次数据变更递增版本号,通过Redis PubSub广播失效事件 |
缓存雪崩解决方案实现细节
是 否 缓存失效 是否热点数据? 获取分布式锁 重建缓存 设置随机过期时间 正常获取数据 释放锁
代码示例:
java
// 使用Redisson实现分布式锁
public Product getProductWithLock(String skuId) {
String cacheKey = "product:" + skuId;
Product product = redis.get(cacheKey);
if (product == null) {
RLock lock = redisson.getLock("lock:" + cacheKey);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 二次检查
product = redis.get(cacheKey);
if (product == null) {
product = db.query(skuId);
// 设置基础300秒+随机60秒偏移
redis.setex(cacheKey, 300 + new Random().nextInt(60), product);
}
}
} finally {
lock.unlock();
}
}
return product;
}
资源浪费优化策略
分层淘汰算法配置:
缓存层级 | 推荐算法 | 配置示例 |
---|---|---|
客户端缓存 | FIFO | localStorage 自动淘汰 |
本地缓存 | 权重LFU | Caffeine配置.weigher((k,v) -> ((Product)v).size()).maximumWeight(100000) |
分布式缓存 | 内存淘汰策略 | Redis配置maxmemory 16gb + allkeys-lru |
多级缓存时间约束优化策略
设计目标:
分层控制:建立多级缓存的协同淘汰机制
资源优化:最大化内存利用率,减少无效数据占用
稳定性保障:避免大规模失效引发的系统雪崩
多级缓存淘汰架构
过期时间最短 主动失效广播 版本号同步 LRU淘汰 客户端缓存 本地缓存 分布式缓存 数据库缓冲池 磁盘存储
分层淘汰策略配置
缓存层级 | 时间策略 | 监控指标 | 调优方法 |
---|---|---|---|
客户端缓存 | 动态TTL(服务端返回max-age) | 浏览器Memory Cache统计 | 根据用户活跃度调整:max-age = 基础值 × 用户活跃系数 |
本地缓存 | 短周期 + 滑动过期 | Caffeine命中率 | 命中率<70%时增加10%容量,>95%时缩短过期时间 |
分布式缓存 | 分级TTL(热数据长,冷数据短) | Redis内存碎片率 | CONFIG SET activedefrag yes + 定期执行MEMORY PURGE |
分级TTL配置示例:
java
// 热点数据延长缓存时间
String key = "product:" + skuId;
if (isHotProduct(skuId)) {
redis.expire(key, 7200); // 2小时
} else {
redis.expire(key, 1800); // 30分钟
}
2.2 更新策略设计
更新策略对比:
策略类型 | 适用场景 | 实现复杂度 | 一致性风险 |
---|---|---|---|
旁路缓存 | 读多写少 | 低 | 中 |
写穿透 | 写密集型 | 高 | 低 |
写回 | 允许数据丢失 | 中 | 高 |
旁路缓存实现 :
对数据实时性要求高可以选择旁路缓存这方案,配合延迟双删方案使用也行。
java
public void updateProduct(Product product) {
// 1. 更新数据库
db.update(product);
// 2. 删除缓存
redis.del("product:"+product.getId());
// 3. 广播失效消息
messageQueue.send("cache_invalidate", product.getId());
}
写穿透模式配置:
yaml
# Spring Cache配置示例
spring:
cache:
type: redis
redis:
cache-null-values: false
time-to-live: 30m
enable-statistics: true
2.3 多级缓存一致性保障
2.3.1 主流方案全景图
一致性层级 强一致性 最终一致性 同步双删+锁 异步更新 事件驱动 版本控制 延迟双删 Binlog驱动 全局版本服务
2.3.2 核心方案重构
方案一:延迟双删+异步回填
多年的验证我觉得这个方案是最靠谱的,我司核心流程选用这个方案
流程图:
App Redis LocalCache DB 1.删除缓存(含本地) 2.数据更新 3.异步回填最新数据(Binlog驱动) 4.延迟二次删除(覆盖主从延迟) 5.实时失效广播 App Redis LocalCache DB
关键点 :
异步回填是主动推送最新数据的优化手段,旨在提升一致性和性能。
延迟双删是防御性清除脏数据的兜底机制,覆盖并发和异常场景。
(各位可以深入思考一下为什么需要异步回填、延迟双删2步配合)
生产配置示例:
java
// 使用Redisson分布式锁保证原子性
public void updateWithLock(String key, Data data) {
RLock lock = redisson.getLock(key + "_LOCK");
try {
lock.lock();
// 1.同步删除多级缓存
redis.delete(key);
localCache.broadcastInvalidate(key);
// 2.更新数据库
db.update(data);
// 3.延迟双删(通过分布式延时队列)
DelayQueue.push(new CacheTask(key, Operation.DELETE), 500);
} finally {
lock.unlock();
}
}
// Binlog监听回填(最终兜底)
@KafkaListener("binlog_updates")
public void onBinlogEvent(ChangeEvent event) {
String key = buildCacheKey(event);
redis.set(key, event.getData());
localCache.broadcastUpdate(key, event.getData());
}
适用场景 :
• 电商库存、秒杀系统
• 可接受亚秒级不一致窗口的业务
方案二:Binlog驱动更新(金融级方案)
流程:
Row格式Binlog MQ消息 立即删除+回填 实时广播 MySQL Canal Server RocketMQ 缓存更新Worker Redis LocalCache
关键流程:
-
数据更新触发 :
sqlUPDATE products SET stock=stock-1 WHERE id=100
-
Binlog捕获:Canal解析DDL/DML事件
-
消息分发:通过RocketMQ分区保证顺序性
-
缓存处理 :
java// 带版本号的缓存更新 public void processCacheUpdate(ChangeEvent event) { String key = "product:" + event.getId(); long newVersion = versionService.increment(key); // 对比版本决定是否更新 if (newVersion > redis.getVersion(key)) { redis.set(key, event.getData(), newVersion); localCache.broadcastUpdate(key, event.getData(), newVersion); } }
优势与约束:
维度 | 说明 |
---|---|
数据一致性 | 最终一致性(500ms内) |
系统影响 | 数据库压力降低70% |
实现复杂度 | 需部署Canal+Kafka集群 |
特殊场景处理 | 分库分表需配置正则过滤 |
三、热点数据防护
3.1 热点数据问题分析
典型特征 :
• 单个Key的QPS > 5000
• 占用超过30%的缓存内存
• 引发Redis CPU使用率 > 80%
影响范围 :
• 数据库连接池耗尽
• 缓存节点负载不均衡
• 服务响应延迟飙升
3.2 热点发现机制
多维度检测方法:
-
Redis监控:
bash# 实时热点Key检测 redis-cli --hotkeys
-
Prometheus指标:
promqlsum(rate(redis_cmd_calls_total{command="get"}[1m])) by (key) > 5000
-
链路追踪采样:
java// 在关键方法添加埋点 @Around("execution(* com.service.*(..))") public Object monitorHotspot(ProceedingJoinPoint pjp) { long start = System.currentTimeMillis(); Object result = pjp.proceed(); stats.record(pjp.getSignature().getName(), System.currentTimeMillis()-start); return result; }
3.3 热点数据解决方案
三级防护体系:
客户端限流 本地缓存 分布式锁 数据库熔断
实施步骤:
-
请求合并:
java// 使用Guava的LoadingCache合并请求 LoadingCache<String, Product> cache = CacheBuilder.newBuilder() .expireAfterWrite(100, TimeUnit.MILLISECONDS) .build(new CacheLoader<String, Product>() { public Product load(String key) { return fetchFromDB(key); } });
-
本地缓存预热:
java// 定时任务预加载热点数据 @Scheduled(fixedRate = 5000) public void preloadHotData() { hotKeys.forEach(key -> { if (!localCache.hasKey(key)) { localCache.put(key, redis.get(key)); } }); }
-
动态限流:
yaml# Sentinel规则配置 - resource: product:query controlBehavior: WarmUp thresholdCount: 10000 grade: QPS warmUpPeriodSec: 10 maxQueueingTimeMs: 1000
四、多级缓存带来的常见问题解决方案
4.1 缓存雪崩
问题现象 :
• 大量缓存同时失效
• 数据库QPS瞬时增长10倍以上
解决思路:
错峰过期 互斥锁 熔断降级
实现细节:
java
public Product getProduct(String skuId) {
Product product = redis.get(skuKey);
if (product == null) {
// 获取分布式锁
if (lock.tryLock()) {
try {
// 二次检查
product = redis.get(skuKey);
if (product == null) {
product = db.query(skuId);
redis.setex(skuKey, 300 + random(0,60), product); // 随机过期时间
}
} finally {
lock.unlock();
}
} else {
// 返回默认值
return defaultProduct;
}
}
return product;
}
4.2 缓存穿透
问题现象 :
• 频繁查询不存在的数据
• 缓存命中率低于50%
解决思路:
布隆过滤器 空值缓存 参数校验
实现细节:
java
// 布隆过滤器初始化
BloomFilter<String> filter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000,
0.01);
// 查询逻辑
public Product getProduct(String skuId) {
if (!filter.mightContain(skuId)) {
return null;
}
// ...正常查询逻辑
}
// 空值缓存设置
redis.setex("null:"+skuId, 300, "NULL");
4.3 缓存击穿
问题现象 :
• 单个热点Key失效后大量请求涌入
• Redis CPU使用率瞬时达到100%
解决思路:
永不过期策略 互斥锁 后台更新
实现细节:
java
// 逻辑过期方案
public Product getProduct(String skuId) {
Product product = redis.get(skuKey);
if (product.isExpired()) {
// 异步更新
executor.submit(() -> {
Product newProduct = db.query(skuId);
redis.set(skuKey, newProduct.setExpireTime(now+300));
});
}
return product;
}
五、升级效果
总结:
1、使用客户端缓存静态文件、低频更新的业务数据等。
2、使用本地缓存缓存高频访问的动态数据,减少请求分布式缓存带来的网络开销。
3、使用分布式缓存查询数据,减少数据库访问流量。
4、使用数据库缓存缓存热点数据,减少磁盘IO访问次数。
使用多级缓存能极大的减少磁盘IO访问次数(营销中台优化后大概只有1.5%的读请求没有命中缓存),极大的提升系统读性能。
