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

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


核心目标

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


一、多级缓存架构

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

相关推荐
好多大米8 小时前
2.JVM-通俗易懂理解类加载过程
java·jvm·spring·spring cloud·tomcat·maven·intellij-idea
种豆走天下1 天前
Dubbo、SpringCloud框架学习
学习·spring cloud·dubbo
!!!5251 天前
Spring Cloud Gateway 笔记
笔记·spring cloud·gateway
B1nnnn丶1 天前
Spring Boot/Spring Cloud 整合 ELK(Elasticsearch、Logstash、Kibana)详细避坑指南
spring boot·elk·spring cloud
littleschemer2 天前
聊天服务器分布式改造
分布式·spring cloud·qq·聊天室
Justice link4 天前
Docker参数,以及仓库搭建
后端·spring·spring cloud
customer084 天前
【开源免费】基于SpringBoot+Vue.JS旅游管理系统(JAVA毕业设计)
java·vue.js·spring boot·spring cloud·开源
用户53866168248225 天前
SpringCloud实战 Gateway网关整合SpringSecurity 自定义过滤器执行多次原因及解决方案
分布式·spring cloud·微服务