亿级分布式系统架构演进实战(五)- 横向扩展(缓存策略设计)

亿级分布式系统架构演进实战(一)- 总体概要
亿级分布式系统架构演进实战(二)- 横向扩展(服务无状态化)
亿级分布式系统架构演进实战(三)- 横向扩展(数据库读写分离)
亿级分布式系统架构演进实战(四)- 横向扩展(负载均衡与弹性伸缩)


核心目标

降低数据库读压力,提升响应速度


一、多级缓存架构

客户端 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

关键流程

  1. 数据更新触发

    sql 复制代码
    UPDATE products SET stock=stock-1 WHERE id=100
  2. Binlog捕获:Canal解析DDL/DML事件

  3. 消息分发:通过RocketMQ分区保证顺序性

  4. 缓存处理

    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 热点发现机制

多维度检测方法

  1. Redis监控

    bash 复制代码
    # 实时热点Key检测
    redis-cli --hotkeys
  2. Prometheus指标

    promql 复制代码
    sum(rate(redis_cmd_calls_total{command="get"}[1m])) by (key)
    > 5000
  3. 链路追踪采样

    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 热点数据解决方案

三级防护体系
客户端限流 本地缓存 分布式锁 数据库熔断

实施步骤

  1. 请求合并

    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);
            }
        });
  2. 本地缓存预热

    java 复制代码
    // 定时任务预加载热点数据
    @Scheduled(fixedRate = 5000)
    public void preloadHotData() {
        hotKeys.forEach(key -> {
            if (!localCache.hasKey(key)) {
                localCache.put(key, redis.get(key));
            }
        });
    }
  3. 动态限流

    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%的读请求没有命中缓存),极大的提升系统读性能。

相关推荐
wno7041 小时前
Spring Cloud Hystrix Dashboard仪表盘
spring·spring cloud·hystrix
BUG指挥官4 小时前
Redis已接入AI
数据库·人工智能·spring boot·redis·spring cloud
wno70416 小时前
Spring Cloud Hystrix服务容错
spring·spring cloud·hystrix
苏渡苇1 天前
Spring Insight 里上报 Span 为什么用 JDK HttpClient 而不是 RestTemplate
java·后端·spring·spring cloud·springboot·性能监控
YOU OU2 天前
Spring Cloud Consul
spring cloud·consul·java-consul
重生之我是Java开发战士2 天前
【Spring Cloud】Eureka服务注册发现与LoadBalancer负载均衡
spring cloud·eureka·负载均衡
谢亮_vipxieliang2 天前
ValidX vs Google Guava Preconditions:验证 vs 断言
java·spring boot·后端·spring cloud·hibernate·guava
夏天拐跑了西瓜3 天前
Spring Cloud 微服务实战(八):Hystrix熔断降级——从服务雪崩到断路器模式
spring cloud·hystrix·微服务
重生之我是Java开发战士4 天前
【Spring Cloud】认识微服务与工程搭建
spring·spring cloud·微服务
SelectDB技术团队4 天前
Apache Doris 多表 Join 比 Starrocks 更快?ClickBench 与 RTABench 实测对比
数据分析·高并发·技术选型·多表join·查询加速·复杂分析·高频查询