Featured image of post 缓存体系:本地、集群、锁与持久化全链路实战

缓存体系:本地、集群、锁与持久化全链路实战

缓存体系:本地、集群、锁与持久化全链路实战

Java Web 微服务系列 · 第 9 篇 · 缓存体系 阅读时长:约 50 分钟 本文写于 2026 年 6 月 配套版本:Redis 7.2.x / Redisson 3.27.x / Caffeine 3.1.x / Spring Boot 3.2.x 前置阅读:《数据库演化:MySQL → PostgreSQL → 分布式》(系列第 8 篇) 后续衔接:《异地多活:Java Web 微服务的高可用终极形态》(系列第 1 篇)

引子:双 11 零点,一个被缓存雪崩打挂的支付链路

2020 年 11 月 11 日 00:00:03,我盯着监控大屏,心跳比系统心跳还快。

  • 流量曲线 3 秒内从 8 万 QPS 爬到 78 万 QPS,比去年峰值还高 30%
  • 订单服务的 Redis 集群 CPU 突然从 25% 跳到 92%,几秒钟后集群报警「慢查询超过阈值」
  • 0:00:08 库存服务的本地缓存(Caffeine)集体过期,所有请求穿透到 Redis
  • 0:00:11 Redis 连接池打满,订单服务开始大量 timeout
  • 0:00:15 网关层熔断器被触发,支付链路全链路降级

事后复盘,整条故障链清晰可见:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
双 11 零点流量尖峰
本地缓存(Caffeine) 集中过期(30 分钟 TTL 在 00:00 同时到期)
请求穿透到 Redis
Redis 单 Key 热点(库存 stock:{skuId})打中 master 节点
master 节点 CPU 100%,开始拒绝服务
连接池排队 → 客户端 timeout → 线程池打满
熔断器开启 → 全链路降级

整个故障持续 7 分钟,损失订单约 1200 万 GMV,CEO 在月度复盘会上点名批评。

复盘报告里我写下了八个字:"缓存是性能中枢,也是故障放大器。"

之后三年,我把这八个字拆成了 本地缓存、分布式缓存集群、主从同步锁、文件持久化、缓存三大问题、生产场景 六章,逐个补齐工程能力。这篇就是把六章的全部经验摊开——每章都给"原理 + 实战 + 坑 + 解决方案"。


一、本地缓存:JVM 进程内的速度加速器

本地缓存是缓存体系里最快、也最容易被低估的一层。它在 JVM 进程内部,没有网络、没有序列化、QPS 可以跑到百万级。但它的天花板也很明显:容量受限于堆内存、不能跨进程共享、GC 抖动会拖垮整个服务

1.1 本地缓存 vs 分布式缓存的边界

维度本地缓存(Caffeine/Guava)分布式缓存(Redis/Memcached)
访问延迟纳秒-微秒级0.1-1ms(同机房)
容量上限GB 级(受堆内存限制)TB 级(集群扩展)
一致性进程内一致即可需要主从同步、Redlock 保障
适用数据配置、白名单、热点字典、用户 Session跨服务共享、订单、库存
失效模式TTL + LRU/W-TinyLFUTTL + 主动失效 + 主从同步
失败影响进程崩溃 = 数据全丢单点故障可自动切换

💡 原理:本地缓存的 W-TinyLFU 算法为什么比 LRU 好

传统 LRU 淘汰策略有两个致命问题:

  1. 突发流量打爆缓存——突发访问的 Key 大量挤入,老的热点 Key 被踢出
  2. 缓存污染——一次全表扫把缓存塞满长期不用的 Key

Caffeine 用 W-TinyLFU(Window Tiny Least Frequently Used):

  • 1% 窗口区(Window Cache):LRU,吸纳新访问
  • 99% 主区(Main Cache):SLRU(分段 LRU),按访问频次分「受保护段 + 候选段」
  • Count-Min Sketch 频率统计:新 Key 进入主区前用 Sketch 估算历史访问频次,比 LR 高的才准入

效果:相同的容量下,Caffeine 的命中率比 Guava Cache 高 30%。这是 2018 年后 W-TinyLFU 论文被工程化后的红利。

1.2 Caffeine 实战

pom.xml

1
2
3
4
5
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>

三种缓存加载模式(根据业务场景选对,命中率天差地别):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 模式 1:手动填充 (适合数据从外部加载, 业务控制 put 时机)
Cache<String, UserProfile> manualCache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)        // 写后 10 分钟过期
    .maximumSize(100_000)                          // 最大 10w 条
    .recordStats()                                 // 开启统计
    .build();

// 模式 2:自动加载 (推荐, 业务只 get, miss 时自动调 loader)
LoadingCache<String, UserProfile> loadingCache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(100_000)
    .refreshAfterWrite(1, TimeUnit.MINUTES)        // 写后 1 分钟异步刷新
    .recordStats()
    .build(key -> userRepo.findById(key));         // loader, miss 时调用

// 模式 3:异步加载 (loader 慢时用, 不阻塞 get 线程)
AsyncCache<String, UserProfile> asyncCache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(100_000)
    .buildAsync();
CompletableFuture<UserProfile> future = asyncCache.get(key, k -> userRepo.findById(k));

1.3 Spring Cache 集成(一行注解切实现)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager mgr = new CaffeineCacheManager();
        mgr.setCaffeine(Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .maximumSize(10_000));
        return mgr;
    }
}

// 业务代码 - 用 Spring 注解切实现
@Service
@RequiredArgsConstructor
public class ProductService {
    private final ProductRepo productRepo;

    @Cacheable(value = "product", key = "#id", unless = "#result == null")
    public Product getProduct(Long id) {
        // 第一次 miss 时调用, 之后命中 Caffeine
        return productRepo.findById(id).orElse(null);
    }

    @CachePut(value = "product", key = "#product.id")
    public Product updateProduct(Product product) {
        return productRepo.save(product);
    }

    @CacheEvict(value = "product", key = "#id")
    public void deleteProduct(Long id) {
        productRepo.deleteById(id);
    }
}

1.4 本地缓存的 6 个高频坑

🎯 避坑 1:maximumSize 设太小 → 缓存击穿

本地缓存 maximumSize=1000,热点字典有 5000 个 Key,前 1000 个热门常驻,后 4000 个永远 miss。建议:maximumSize = 业务总 Key 数 × 1.5,宁可浪费内存也别让热点被踢。

🎯 避坑 2:TTL 全部一样 → 雪崩

所有 Key 都设 30 分钟 TTL,00:00:00 写入的 Key 在 00:30:00 集体过期对策:TTL 加随机偏移 expireAfterWrite(Duration.ofMinutes(30).plusSeconds(ThreadLocalRandom.current().nextInt(60)))

🎯 避坑 3:没有上限 → OOM

用 Guava Cache 时忘记设 maximumSizeKey 无限增长直到 OOM。Caffeine 强制 maximumSizemaximumWeight 二选一,比 Guava 安全。

🎯 避坑 4:loader 抛异常 → 缓存击穿

loadingCache.get(key) 时 loader 抛异常,Caffeine 不会缓存异常结果,下一个请求继续调 loader。如果 loader 是数据库查询,一次慢查询会拖垮 N 个并发请求对策:loader 内部 try-catch,返回 null 或空集合,让缓存层能稳定返回。

🎯 避坑 5:value 是大对象 → GC 抖动

本地缓存里塞了完整的 List<Order>(单条 10KB),10w 条 = 1GB 堆内存,老年代 GC 一次 2 秒对策

  1. 大对象用 WeakReference.weakValues()
  2. 或者拆成 Map<orderId, OrderDto> 单条存
  3. 业务真的需要大集合,走 Redis 别用本地

🎯 避坑 6:进程间不一致 → 数据漂移

订单服务 A 把"商品 X 价格 99"缓存在本地,订单服务 B 的本地还是"商品 X 价格 100"。用户下单时 A 算的价是 99、B 算的是 100,同一笔订单价格不一致对策:价格类强一致数据禁止本地缓存,必须走 Redis。

1.5 命中率监控

Caffeine 提供了 recordStats() 之后的所有统计:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Cache<String, UserProfile> cache = Caffeine.newBuilder()
    .recordStats()
    .maximumSize(10_000)
    .build();

// 业务代码
cache.get(key, k -> loader.apply(k));

// 定时上报 (Spring @Scheduled)
@Scheduled(fixedRate = 60_000)
public void reportStats() {
    CacheStats stats = cache.stats();
    log.info("Caffeine stats: hitRate={}, missRate={}, evictionCount={}",
        stats.hitRate(), stats.missRate(), stats.evictionCount());
    // Micrometer 指标上报到 Prometheus
    Metrics.gauge("caffeine.hit_rate", stats.hitRate());
}
指标健康范围异常处理
hitRate> 0.8< 0.5 = 缓存容量不够或 TTL 太短
missRate< 0.2> 0.5 = 同上
evictionCount稳定暴增 = 触发淘汰, 检查 maximumSize
loadTimeP99 < 50msP99 > 200ms = loader 慢, 考虑异步

二、分布式缓存集群:水平扩展的容量与吞吐

本地缓存受限于单进程内存,业务规模上 10 万 QPS、单 Key 几 MB 后必须上分布式缓存。本节按"为什么需要集群 → Cluster 怎么分片 → Sentinel 怎么切换 → 容量怎么规划"展开。

2.1 集群方案对比

方案一致性扩展性运维复杂度适用场景
Redis Sentinel主从异步复制垂直扩展中小规模(< 50GB)
Redis Cluster主从异步 + 槽路由水平扩展中大规模(50GB-10TB)
Codis(豌豆荚)主从异步 + Proxy水平扩展历史方案,新项目不用
Twemproxy(Twitter)主从异步 + Proxy水平扩展历史方案,新项目不用
Memcached客户端哈希纯 KV、无持久化需求

🎯 决策:90% 场景选 Redis Cluster

Redis Cluster 解决了 Sentinel 的两个痛点:

  1. 数据分片——16384 个哈希槽分散到多 Master,容量不再是单节点瓶颈
  2. 自动故障转移——Cluster 内置 Raft 协议(其实是类 Paxos 的 gossip + 投票),不再需要 Sentinel 集群

唯一缺点:客户端需要 cluster-aware 客户端(Lettuce/Jedis 都已支持),不能在集群模式下用单节点命令(如 KEYS *)。

2.2 Redis Cluster 架构原理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
┌─────────────────────────────────────────────────────────────┐
│                  Redis Cluster (6 节点, 3 主 3 从)         │
│                                                             │
│     Master A          Master B          Master C            │
│    slots 0-5465     slots 5466-10923   slots 10924-16383   │
│        │                │                  │                │
│   Replica A1       Replica B1         Replica C1            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
           ▲              ▲                  ▲
           │              │                  │
        客户端 (Lettuce/Jedis) 走 CRC16(key) % 16384 路由

💡 原理:16384 个哈希槽的设计取舍

Redis Cluster 用 CRC16 算法对 key 计算 16 位 hash,然后取模 16384得到槽号。为什么是 16384 不是更多?

  • 够用:16384 槽意味着单集群最多 16384 个节点,实际生产 100 节点以内,完全够
  • 配置包小:每个节点的心跳包要带上自己负责的槽位 bitmap,16384 位 = 2KB,如果 65536 槽 = 8KB,每次心跳多 6KB,100 节点集群 = 600KB/s 流量浪费
  • 重新分片粒度细:迁移时按槽为单位,16384 槽比 1024 槽更平滑

2.3 Cluster 部署(最少 6 节点)

生产环境铁律

  • 至少 3 主 3 从——单 Master 没意义
  • Master 和 Replica 不要在同一台物理机——避免单机故障把一对主从一起带走
  • 跨机房时按单元化部署——参见系列第 1 篇异地多活
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 1. 准备 6 个节点配置 (redis-7000/7001/7002/7003/7004/7005)
# 配置文件 redis.conf 关键项
port 7000
cluster-enabled yes
cluster-config-file nodes-7000.conf
cluster-node-timeout 15000
appendonly yes
appendfsync everysec
# 持久化目录
dir /var/lib/redis/7000

# 2. 启动 6 个节点
for port in 7000 7001 7002 7003 7004 7005; do
    redis-server redis-${port}.conf &
done

# 3. 创建集群 (3 主 3 从, --cluster-replicas 1 表示每个 Master 配 1 个 Replica)
redis-cli --cluster create \
    10.1.0.10:7000 10.1.0.11:7001 10.1.0.12:7002 \
    10.1.0.13:7003 10.1.0.14:7004 10.1.0.15:7005 \
    --cluster-replicas 1

2.4 Spring Boot 接入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
spring:
  data:
    redis:
      cluster:
        nodes:
          - 10.1.0.10:7000
          - 10.1.0.11:7001
          - 10.1.0.12:7002
          - 10.1.0.13:7003
          - 10.1.0.14:7004
          - 10.1.0.15:7005
        max-redirects: 3
        timeout: 2s
      lettuce:
        pool:
          max-active: 200
          max-idle: 50
          min-idle: 10
          max-wait: 1000ms
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(factory);

    // Key 用 String 序列化
    StringRedisSerializer keySer = new StringRedisSerializer();
    template.setKeySerializer(keySer);
    template.setHashKeySerializer(keySer);

    // Value 用 JSON 序列化
    Jackson2JsonRedisSerializer<Object> valSer =
        new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
    template.setValueSerializer(valSer);
    template.setHashValueSerializer(valSer);

    template.afterPropertiesSet();
    return template;
}

2.5 哈希槽定位的 4 个常见问题

🎯 避坑 1:不同 Key 必须落在同一 slot 才能用 MGET/MSET

批量操作 MGET key1 key2 key3 要求所有 Key 在同一个哈希槽对策:用 hash tag——Key 中用 {} 包裹的部分会作为哈希依据。

1
2
3
4
5
6
// ❌ 三个 Key 在不同 slot, MGET 报错 CROSSSLOT
redisTemplate.opsForValue().multiGet(Arrays.asList("order:1", "order:2", "order:3"));

// ✅ 用 hash tag 强制同 slot
redisTemplate.opsForValue().multiGet(Arrays.asList(
    "{order}:1", "{order}:2", "{order}:3"));

实际工程里用 multiGet 不如用 Pipeline——Pipeline 不受 slot 限制。

🎯 避坑 2:事务 (MULTI/EXEC) 不支持 Cluster

Redis Cluster 的事务要求 Key 在同一 slot,生产几乎不用 MULTI/EXEC对策:用 Lua 脚本 替代事务——Lua 脚本在 Cluster 模式下也只允许同一 slot,但通过 hash tag 可以控制。

🎯 避坑 3:KEYS 命令禁用

KEYS * 在 Cluster 下报错(每个节点都得扫,Redis 干脆不支持)。对策:用 SCAN 命令 + 游标分批。

1
2
3
4
5
6
7
ScanOptions options = ScanOptions.scanOptions().match("order:*").count(1000).build();
try (Cursor<byte[]> cursor = redisTemplate.getConnectionFactory()
        .getConnection().keyCommands().scan(options)) {
    while (cursor.hasNext()) {
        // 处理 Key
    }
}

🎯 避坑 4:跨 slot 错误

客户端在 Slot 迁移过程中访问 Key,可能返回 MOVEDASK。Lettuce/Jedis 客户端会自动重试,但重定向超过 max-redirects 次会报错对策:把 max-redirects 设为 3-5,业务代码处理 RedisCommandExecutionException

2.6 容量规划公式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
总 Key 数 = DAU × 关键业务对象数
         = 100w × 10
         = 1000w = 1 千万条

单 Key 平均大小 = 2KB
总数据量 = 1 千万 × 2KB = 20GB

加 30% Buffer = 26GB
加主从副本 = 26GB × 2 = 52GB

3 Master 分摊:每 Master 17GB
每 Master 配 32GB 内存(预留 50% 给 fork / 复制 / 碎片)
3 Master × 3 Replica = 6 节点 × 32GB = 192GB 总内存

生产黄金法则

  • 单 Redis 实例内存不超过 64GB——fork 时间随内存线性增长,64GB 时 fork 已经 100ms+,主线程阻塞明显
  • Redis 节点数不超过 200——Cluster 内部 gossip 消息 O(N²),200 节点后心跳开销指数增长
  • 数据量超过 10TB——考虑业务拆分或上 Pika/SSDB 这种磁盘 Redis

三、主从同步与分布式锁:从原理到 4 大生产坑

主从同步是 Redis 高可用的基础,分布式锁是 Redis 最常被滥用的功能。这两件事看似独立,实际在生产里经常踩同一类坑——时钟漂移、网络分区、Master 切换时的锁丢失。

3.1 Redis 主从复制原理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
                      ┌──────────────────┐
                      │   Master         │
                      │   - 写请求处理    │
                      │   - 异步复制到 Replica │
                      │   - repl_backlog│
                      └────────┬─────────┘
              ┌────────────────┼────────────────┐
              │                │                │
        ┌─────▼─────┐    ┌─────▼─────┐    ┌─────▼─────┐
        │ Replica 1 │    │ Replica 2 │    │ Replica 3 │
        │  全量同步  │    │  全量同步  │    │  增量同步  │
        └───────────┘    └───────────┘    └───────────┘

💡 原理:全量同步 vs 增量同步的触发条件

Redis 用 repl_backlog(复制积压缓冲区)记录 Master 最近的写命令流:

  • 全量同步——Replica 第一次连上来 / 主从断开超过 repl-timeout / Replica 需要的 offset 已被 backlog 覆盖
  • 增量同步——Replica 需要的 offset 还在 backlog 里(默认 1MB)

调优 repl-backlog-size:生产建议设为平均写入量 × 60 秒,10MB-100MB 都合理。

⚠️ 主从复制是异步的——Master 写入成功后立即返回客户端,复制到 Replica 有延迟(同机房 1-5ms,跨机房 30-100ms)。这就是为什么 Redis 主从切换时可能有少量数据丢失

3.2 Sentinel 主从切换流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
   Sentinel 1          Sentinel 2          Sentinel 3
       │                    │                    │
       │   PING 10s 一次     │                    │
       ├───────────────────►│                    │
       │                    ├───────────────────►│
       │                    │                    │
       │   主观下线 (PFAIL)  │                    │
       │   ↓ Master 失联    │                    │
       │   互相发 is-master-down-by-addr       │
       │                    │                    │
       │   ↓ 多数派确认     │                    │
       │   客观下线 (ODOWN) │                    │
       │                    │                    │
       │   ↓ 选举 Leader    │                    │
       │   (Raft 协议)      │                    │
       │                    │                    │
       │   ↓ Leader 执行切换│                    │
       │   - 选 Replica 做新 Master            │
       │   - 旧 Master 改 Replica             │
       │   - 通知客户端新 Master              │

📌 实践:Sentinel 至少 3 节点,跨机柜部署

  • 3 节点 = 容忍 1 节点故障,2 节点 = 1 节点故障就脑裂
  • 跨机柜部署避免单机柜断电时 Sentinel 也全军覆没
  • Sentinel 本身不存数据,用 1 核 1GB 虚拟机足够

3.3 分布式锁的 4 大方案对比

方案正确性性能复杂度适用场景
SETNX + EXPIRE差(两条命令非原子)极高❌ 不推荐
SET key value NX EX 30中(Master 切换丢锁)单 Redis 实例
Redlock 算法较高强一致场景
Redisson较高99% 场景推荐
Zookeeper强一致 + 低 QPS
etcdK8s 生态

3.4 Redlock 算法详解

核心思想不在一个 Redis 实例上加锁,而在 N 个独立 Redis 实例上(官方推荐 5 个),获得 ≥ N/2+1 个实例的锁才算加锁成功

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
                    ┌─────────┐
                    │ Client  │
                    └────┬────┘
                         │ SET key value NX PX 30000
        ┌────────────────┼────────────────┐
        ▼                ▼                ▼                ▼                ▼
   ┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐
   │Redis-1  │     │Redis-2  │     │Redis-3  │     │Redis-4  │     │Redis-5  │
   │ ✓ 成功   │     │ ✓ 成功   │     │ ✗ 失败   │     │ ✓ 成功   │     │ ✗ 失败   │
   └─────────┘     └─────────┘     └─────────┘     └─────────┘     └─────────┘
                                                3/5 = 成功 (过半)

4 个关键参数

  1. T1 = 获取锁前时间
  2. T2 = 获取锁后时间(获取耗时 = T2 - T1
  3. Lock TTL = 锁有效期(建议 30 秒)
  4. 有效时间 = Lock TTL - 获取耗时有效时间 < 0 = 加锁失败

💡 原理:为什么 Redlock 比单 Redis 更安全

单 Redis 加锁的最大风险:Master 还没把锁复制到 Replica 就挂了。Sentinel 把 Replica 提升为新 Master,但新 Master 上根本没有这个锁——其他客户端就能加同一把锁,同一资源被两个客户端同时操作

Redlock 用 5 个独立 Redis 实例,单个实例丢锁不影响整体——其他 4 个实例还有锁,第二个客户端拿不到过半

⚠️ 争议:Martin Kleppmann 2016 年发表的《How to do distributed locking》指出 Redlock 也有缺陷(GC 停顿、时钟漂移),业界仍有讨论。Antirez(Redis 作者)做了反驳。结论:在 99% 业务场景,Redlock + 业务幂等性已经够用,不要追求绝对正确

3.5 Redisson 实战(最推荐)

1
2
3
4
5
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.27.2</version>
</dependency>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redisson() {
        Config config = new Config();
        // 单 Redis 实例 (生产建议 Redisson 集群模式)
        config.useSingleServer()
            .setAddress("redis://10.1.0.10:7000")
            .setPassword("${REDIS_PASSWORD}")
            .setConnectionPoolSize(200)
            .setConnectionMinimumIdleSize(20);
        return Redisson.create(config);
    }
}

// 业务代码
@Service
@RequiredArgsConstructor
public class OrderService {
    private final RedissonClient redisson;
    private final OrderRepo orderRepo;

    public Order createOrder(OrderRequest req) {
        // 1. 加锁 (key = "lock:order:user:{userId}", value = 自动生成 UUID)
        RLock lock = redisson.getLock("lock:order:user:" + req.getUserId());

        // 2. 尝试加锁, 最多等 3 秒, 锁自动释放时间 30 秒
        //    看门狗 (watchdog) 默认 10 秒续期一次, 业务执行完才释放
        try {
            boolean acquired = lock.tryLock(3, 30, TimeUnit.SECONDS);
            if (!acquired) {
                throw new BizException("系统繁忙, 请稍后重试");
            }

            // 3. 业务逻辑
            return doCreateOrder(req);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BizException("加锁被中断");
        } finally {
            // 4. 必须释放! 看门狗会在 unlock 时停止续期
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

3.6 分布式锁的 4 大生产坑

🎯 坑 1:锁没释放 → 死锁

业务代码异常但 finally 里没 unlock锁永远不释放,其他线程永远拿不到。

对策

  1. finally严格 unlock
  2. tryLock(timeout) 限时等待,避免无限等
  3. 锁 TTL 一定要设(Redisson 默认 30 秒 + 看门狗续期)

🎯 坑 2:锁误删 → 别人干活你删锁

客户端 A 加锁后,业务执行慢超过 TTL,锁自动释放,客户端 B 拿到锁开始干活。这时 A 业务执行完调 unlock——A 删的是 B 的锁

对策每个锁有唯一 value(UUID/ThreadId),unlock 时用 Lua 脚本校验 value 是不是自己加的

1
2
3
4
5
6
// Redisson 内部已经处理, 你只要用 lock.unlock() 即可
// 自己写的话要这样:
String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
             "  return redis.call('del', KEYS[1]) " +
             "else return 0 end";
redis.eval(lua, Collections.singletonList(lockKey), uuidValue);

🎯 坑 3:锁粒度太粗 → 性能差

把整个订单服务都加一把大锁,QPS 100 时锁就成为瓶颈

对策按业务粒度加锁——

  • 用户维度的锁:lock:order:user:{userId}
  • 商品维度的锁:lock:stock:product:{skuId}
  • 业务维度的锁lock:order:create只锁关键操作,不锁整个流程

🎯 坑 4:Redlock 性能不够 → 排队

Redlock 要访问 5 个独立 Redis,延迟 ≈ 单 Redis × 5。高 QPS 场景下锁本身成为瓶颈。

对策

  1. 优先用单 Redis + 业务幂等——大多数业务能容忍「同一资源被处理 2 次」
  2. 把 Redlock 的 Redis 集群和业务 Redis 集群分开——避免业务流量抢占锁的连接池
  3. 锁续期用 watchdog 异步续,不阻塞业务线程

四、文件持久化:RDB / AOF / 混合,三种方案怎么选

缓存数据丢了怎么办?——答:看你是什么数据。电商购物车丢了用户重填,订单数据丢了老板提刀。持久化策略按"数据重要性 × 写入性能"权衡。

4.1 RDB 快照

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
时间 ─────────────────────────────────────►

   ▲                ▲                ▲
   │                │                │
   │ save 60 1000   │ save 300 10    │ bgsave
   │ 自动触发        │ 自动触发        │ 手动触发
fork 子进程 ──► 遍历内存数据 ──► 写 dump.rdb
                 (COW 内存复制)

💡 原理:RDB 的 fork + COW 机制

bgsave 触发时,Redis fork 一个子进程,父子进程共享内存页。子进程开始遍历内存写 RDB 文件父进程继续处理写请求——如果父进程要修改某内存页,内核触发 COW(Copy-On-Write),复制出新的内存页给子进程。

优点主进程不阻塞(除 fork 那瞬间)。 缺点

  1. fork 阻塞——64GB 实例 fork 大约 100ms,这 100ms 主进程不响应任何请求
  2. 数据丢失——两次快照之间的写操作丢失(默认 5 分钟窗口,最坏丢 5 分钟数据

4.2 AOF 追加日志

1
2
3
4
5
6
7
8
写入 ──► appendonly.aof(顺序追加)
         AOF 重写 (rewrite)
         合并为最小命令集
         (重写不阻塞主进程)

AOF 的 appendfsync 三种策略:

策略性能数据安全适用场景
always差(每条命令 fsync)零丢失金融、订单
everysec(默认)丢 ≤ 1 秒99% 业务
no最高OS 决定临时缓存

📌 实践:everysec 是 99% 业务的最优解

always 性能下降到 1/10,主线程要等磁盘 fsync——Redis 本身是为了快,always 让它退化成了 MySQL。

everysec 在主线程外异步刷盘,最多丢 1 秒大多数业务都能接受——购物车、用户 Session 这类非关键数据,丢 1 秒重算即可

4.3 混合持久化(Redis 4.0+ 默认推荐)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
                    混合持久化 AOF 文件
    ┌────────────────────────────────────────┐
    │  RDB 格式                              │
    │  ┌──────────────────────┐              │
    │  │ 当前内存全量数据      │              │
    │  └──────────────────────┘              │
    │                ↓                      │
    │  AOF 增量                                │
    │  ┌──────────────────────┐              │
    │  │ RDB 之后的写命令流     │              │
    │  │ (INCR / SET / DEL...)  │              │
    │  └──────────────────────┘              │
    └────────────────────────────────────────┘

配置

1
2
3
4
# redis.conf
aof-use-rdb-preamble yes    # 开启混合持久化 (Redis 4.0+ 默认 yes)
appendonly yes
appendfsync everysec

优势

  • 启动恢复时先全量加载 RDB(速度快),再重放 AOF 增量(补齐数据)
  • 比纯 AOF 恢复快 10 倍(实测 16GB AOF 恢复从 30 分钟降到 3 分钟)
  • 比纯 RDB 丢失数据少(只丢混合点之后的写命令)

4.4 持久化选型矩阵

业务场景数据丢失容忍推荐方案关键配置
购物车 / 用户 Session丢几秒可接受RDB + AOF 混合appendfsync everysec
订单 / 支付结果绝不能丢AOF alwaysappendfsync always
排行榜 / 计数器丢几十分钟可接受纯 RDBsave 900 1
配置 / 白名单启动加载即可纯 RDBsave 3600 1
临时验证码丢无所谓关闭持久化save "" + appendonly no

4.5 fork 阻塞的 5 个优化

🎯 避坑 1:单实例内存 > 32GB → 慎用 bgsave

64GB 实例 fork 100ms,这 100ms 主进程完全卡死。对策:用 多个小实例(如 4 个 16GB 实例)替代单大实例。

🎯 避坑 2:禁用透明大页 THP

Linux 默认开启 THP(透明大页),THP 让 fork 复制内存页时变慢 10 倍。必须关:

1
echo never > /sys/kernel/mm/transparent_hugepage/enabled

🎯 避坑 3:避免在高峰期 bgsave

Redis 自动 bgsave 触发条件 save 60 1000(60 秒内 1000 次写),高峰触发 bgsave 又叠 fork 阻塞 → 雪崩对策

  1. 业务低峰期(凌晨 4-5 点)手动 bgsave
  2. 调高 save 阈值:save 300 10000(5 分钟 1w 次才触发)

🎯 避坑 4:AOF 重写也 fork

bgrewriteaof 同样要 fork,避免与 bgsave 同时触发(不然 fork 内存页被复制两次)。

🎯 避坑 5:用 SSD 而不是 HDD

AOF 顺序写,SSD 比 HDD 性能高 10-100 倍。HDD 上 appendfsync always 写入 QPS 撑死 1000,SSD 上能撑到 5w+


五、缓存三大问题:雪崩 / 穿透 / 击穿

缓存是性能加速器,也是故障放大器。三大问题是每个 Java 后端开发必须背得滚瓜烂熟的"老八股"——但 90% 的人答得似是而非,本节给出原理 + 场景 + 解决方案完整链路。

5.1 雪崩:缓存集体失效,请求打爆数据库

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
时间 ─────────────────────────────────────────►

   00:00:00     00:30:00     00:30:01
       │            │            │
       ▼            ▼            ▼
   缓存写入     30 分钟 TTL   缓存集体过期
   (批量预热)    集体到期     ↓
                              请求全部穿透
                              DB 被打爆

🎯 解决方案 4 件套(按优先级)

1. TTL 加随机偏移——避免集体过期

1
2
3
int baseTtl = 1800;  // 30 分钟
int jitter = ThreadLocalRandom.current().nextInt(60);  // 0-60 秒
redisTemplate.opsForValue().set(key, value, baseTtl + jitter, TimeUnit.SECONDS);

2. 多级缓存——本地缓存兜底

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public UserProfile getUser(Long id) {
    // L1: 本地 Caffeine
    UserProfile p = caffeineCache.getIfPresent(id);
    if (p != null) return p;

    // L2: Redis
    p = redisTemplate.opsForValue().get("user:" + id);
    if (p != null) {
        caffeineCache.put(id, p);
        return p;
    }

    // L3: DB
    p = userRepo.findById(id).orElse(null);
    if (p != null) {
        redisTemplate.opsForValue().set("user:" + id, p, 30 + jitter, TimeUnit.MINUTES);
        caffeineCache.put(id, p);
    }
    return p;
}

3. 熔断降级——Redis 挂了直接走 DB 限流

1
2
3
4
5
6
7
8
9
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
public UserProfile getUser(Long id) {
    return redisTemplate.opsForValue().get("user:" + id);
}

public UserProfile getUserFallback(Long id, Throwable t) {
    // 走 DB 限流版本
    return rateLimitedDbQuery(id);
}

4. 缓存预热——提前加载热点数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Component
public class CacheWarmer implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) {
        // 启动时把热 Key 加载到 Redis
        List<Long> hotIds = hotKeyService.getHotIds();
        hotIds.forEach(id -> {
            UserProfile p = userRepo.findById(id).orElse(null);
            if (p != null) redisTemplate.opsForValue().set("user:" + id, p, 30, TimeUnit.MINUTES);
        });
    }
}

5.2 穿透:查询不存在的数据,缓存形同虚设

1
2
3
4
客户端 ──► Redis (miss) ──► DB (无此数据) ──► 缓存 null ──► 下次同样 miss
                                          但每次还是打 DB
                                          (空值没缓存, 或缓存时间太短)

🎯 解决方案 3 件套

1. 缓存空值——DB 查不到也缓存一个 null 标记

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public UserProfile getUser(Long id) {
    String key = "user:" + id;
    Object cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
        return cached == NULL_MARKER ? null : (UserProfile) cached;
    }

    UserProfile p = userRepo.findById(id).orElse(null);
    if (p != null) {
        redisTemplate.opsForValue().set(key, p, 30, TimeUnit.MINUTES);
    } else {
        // 缓存 null 标记, TTL 短一些 (5 分钟), 防止长期占空间
        redisTemplate.opsForValue().set(key, NULL_MARKER, 5, TimeUnit.MINUTES);
    }
    return p;
}

2. 布隆过滤器——前置判断 Key 是否可能存在

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class BloomFilterService {
    private final BloomFilter<Long> bloomFilter = BloomFilter.create(
        Funnels.longFunnel(), 10_000_000, 0.001);

    public void addUser(Long id) {
        bloomFilter.put(id);
    }

    public boolean mightExist(Long id) {
        return bloomFilter.mightContain(id);
    }
}

// 业务代码
public UserProfile getUser(Long id) {
    if (!bloomFilterService.mightExist(id)) {
        return null;  // 一定不存在, 直接返回
    }
    // 走正常查询
}

3. 接口校验——业务层拒绝明显非法的查询

1
2
3
4
5
6
public UserProfile getUser(Long id) {
    if (id == null || id <= 0 || id > MAX_USER_ID) {
        throw new IllegalArgumentException("非法的 userId");
    }
    // ...
}

5.3 击穿:单个热点 Key 过期瞬间被高并发打挂

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
                ┌─────────────────────┐
   请求 N 个    │   Redis              │
   ────────────►│   hotKey 已过期       │
                │   miss                │
                └──────────┬────────────┘
                  N 个请求同时查 DB
                  抢着回填缓存
                  DB 被打挂

🎯 解决方案 3 种

方案 1:互斥锁(最常用)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public UserProfile getUser(Long id) {
    String key = "user:" + id;
    UserProfile p = redisTemplate.opsForValue().get(key);
    if (p == null) {
        // 只让一个线程去查 DB
        if (tryLock("lock:user:" + id, 5, TimeUnit.SECONDS)) {
            try {
                p = userRepo.findById(id).orElse(null);
                if (p != null) {
                    redisTemplate.opsForValue().set(key, p, 30, TimeUnit.MINUTES);
                }
            } finally {
                unlock("lock:user:" + id);
            }
        } else {
            // 其他线程 sleep 50ms 后重试 (此时大概率缓存已回填)
            Thread.sleep(50);
            p = redisTemplate.opsForValue().get(key);
        }
    }
    return p;
}

方案 2:逻辑过期(推荐)

缓存不设 TTL,改在 value 里加一个 expire 字段,业务发现过期后异步回填

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
public class CacheItem<T> {
    private T data;
    private long expireAt;  // 逻辑过期时间
}

public UserProfile getUser(Long id) {
    String key = "user:" + id;
    CacheItem<UserProfile> item = redisTemplate.opsForValue().get(key);

    if (item == null) {
        return loadFromDbAndCache(id);
    }

    if (item.getExpireAt() < System.currentTimeMillis()) {
        // 已逻辑过期, 异步回填
        CompletableFuture.runAsync(() -> refreshCache(id));
    }

    return item.getData();  // 永远返回旧数据, 不阻塞
}

方案 3:热点永不过期(业务侧可控)

真正的热点 Key(如商品详情、明星排行榜),直接不设 TTL——业务更新时主动 SET 新值。不推荐:缓存与 DB 一致性管理变难。


六、生产场景 6 个真实案例

本节是 6 个真实生产事故 + 完整解决方案,每个都给出事故症状、根因、解决 SOP。

6.1 多级缓存架构设计

场景:电商商品详情页 QPS 50w,单 Redis 集群撑不住。

架构

1
2
3
4
用户 ──► Nginx (Lua 缓存) ──► 本地 Caffeine (10w 条) ──► Redis Cluster ──► MySQL
              │                       │                       │             │
              TTL 30s                TTL 5min                 TTL 30min      主库
              (URL 维度)            (商品 ID 维度)            (商品 ID)    (兜底)

关键设计

  1. 每层命中率目标 ≥ 70%——L1 70% + L2 25% + L3 5%(数据库实际只扛 5%)
  2. 每层失效时间递增——L1 短、L2 中、L3 长,任意一层有数据就不穿透
  3. 数据变更推送到所有层——商品改价时 mq send 消息 → L1 主动失效 + L2 主动失效 + L3 写新值

6.2 大 Key 与热 Key 处理

场景 1:大 Key 拖慢集群

某运营配置单个 Key 存了 5MB 的 JSON(商品标签字典),DEL 这个 Key 直接阻塞 800ms——Redis 单线程执行 DEL,5MB 数据要遍历。

解决方案

1
2
3
4
5
6
7
8
# ❌ 直接 DEL 阻塞 800ms
redis-cli DEL big:config

# ✅ 用 UNLINK 异步删除 (Redis 4.0+)
redis-cli UNLINK big:config

# ✅ 或拆成多个小 Key
# big:config:1, big:config:2, ... 每个 1MB

场景 2:热 Key 打挂单节点

秒杀商品 stock:{skuId} 在 Master A 节点,5w QPS 集中打 A,A CPU 100%。

解决方案

  1. Key 哈希分散——stock:{skuId}:{shard} 分到 10 个 Key,每个 Key 1w QPS
  2. 读写分离——读路由到 Replica,写走 Master
  3. 客户端本地缓存——秒杀商品库存在客户端本地缓存 1 秒,本地没拿到再走 Redis
1
2
3
// 热 Key 哈希分散 (10 分片)
int shard = (skuId.hashCode() & Integer.MAX_VALUE) % 10;
String key = "stock:" + skuId + ":" + shard;

6.3 缓存与 DB 一致性(Cache-Aside 模式的陷阱)

经典 Cache-Aside 模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 读
public Product getProduct(Long id) {
    Product p = cache.get(id);
    if (p == null) {
        p = db.findById(id);
        cache.set(id, p);  // 回填
    }
    return p;
}

// 写
public void updateProduct(Product p) {
    db.update(p);
    cache.del(p.getId());  // 删除缓存 (不是更新缓存)
}

为什么"先更新 DB 再删缓存"比"先删缓存再更新 DB"安全?

时序先删缓存再更新 DB先更新 DB 再删缓存
并发问题A 删缓存 → B 读 miss → 读 DB 旧值 → 回填旧缓存 → A 更新 DB → 缓存是旧值A 更新 DB → B 读缓存命中旧值 → A 删缓存 → 下次读 miss 走 DB 拿新值
不一致窗口长(缓存被回填后才发现 DB 已更新)短(只在下一次读前有旧值)

3 个进阶模式

模式一致性性能适用
Cache-Aside最终一致99% 业务
Read-Through最终一致缓存层统一管理
Write-Through强一致金融、订单
Write-Behind弱一致极高计数、点赞
延迟双删较强写少读多

🎯 延迟双删(生产常用)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void updateProduct(Product p) {
    // 1. 先删缓存
    cache.del(p.getId());

    // 2. 更新 DB
    db.update(p);

    // 3. 延迟 500ms 再删一次 (异步)
    CompletableFuture.delayedExecutor(500, TimeUnit.MILLISECONDS)
        .execute(() -> cache.del(p.getId()));
}

第二次删除是处理并发场景——第一次删完到 DB 更新完之间,其他线程可能读了旧 DB 值并回填了缓存。第二次删除把这个旧值清掉

6.4 慢查询引发的缓存污染

事故:某天 Redis 集群 CPU 突然飙到 90%,查 SLOWLOG GET 10 发现大量 HGETALL order:detail:12345 耗时 500ms+。

根因:单 order 详情 Hash 里有 5000 个 field(订单的多商品 + 多优惠 + 多日志),HGETALL 要遍历全部 field。

解决方案

  1. 拆 Hash——order:detail:12345:basic + order:detail:12345:items + order:detail:12345:logs
  2. 业务上避免 HGETALL——用 HSCAN 游标分批
  3. 大 Hash 监控——redis-cli --bigkeys 定期扫描,单 Key > 10KB 报警

6.5 Redis 内存淘汰策略选型

Redis 内存满时,根据 maxmemory-policy 决定淘汰哪些 Key:

策略淘汰规则适用场景
noeviction不淘汰,写报错严禁丢数据
allkeys-lru所有 Key 中 LRU 淘汰通用推荐
volatile-lru仅淘汰带 TTL 的 Key兜底 + 永久缓存混合
allkeys-lfu(Redis 4.0+)所有 Key 中 LFU 淘汰热点数据保护更强
volatile-ttl淘汰快过期的 KeyTTL 多样时
allkeys-random随机淘汰不推荐

生产推荐配置

1
2
3
4
# redis.conf
maxmemory 16gb
maxmemory-policy allkeys-lru
maxmemory-samples 10    # LRU 采样数, 越大越精确

6.6 监控告警体系

关键指标(Micrometer + Prometheus + Grafana):

指标告警阈值排查方向
redis_used_memory_bytes> 80% maxmemoryKey 增长 / 大 Key
redis_cpu_usage> 70%慢查询 / 持久化
redis_connected_clients> 80% maxclients连接泄漏
redis_instantaneous_ops_per_sec突增 > 3x业务异常
redis_slowlog_length> 10慢查询堆积
redis_keyspace_hit_rate< 0.7命中率下降
redis_blocked_clients> 0BLPOP 阻塞
redis_master_link_down_since_seconds> 10主从断开

生产必须的 3 条告警

  1. 命中率突降——hit_rate 5 分钟内下降 > 10% → 大概率有热点 Key 过期
  2. 主从断开——master_link_down > 30 秒 → 自动切换可能触发
  3. 内存接近上限——used_memory / maxmemory > 0.9 → 立刻扩容或清理

七、与系列其他篇的联动

缓存体系不是孤岛,它和数据库演化、熔断限流、异地多活都强相关。本节把这条联动链路拉通。

7.1 与系列第 8 篇《数据库演化》的联动

缓存与 DB 一致性是两者最核心的联动点:

  • 强一致数据(订单状态、支付结果)→ 不走缓存,直接 DB
  • 最终一致数据(商品详情、用户昵称)→ Cache-Aside 模式
  • 统计类数据(阅读数、点赞数)→ Write-Behind 模式(先写缓存,异步刷 DB)

📌 原则:缓存是 DB 的"加速副本",不是"替代品"

任何缓存数据都必须能通过 DB 重建——这是分布式系统的不变式。缓存丢了 = 重建一次。如果缓存数据没法重建,就根本不该放缓存

7.2 与系列第 7 篇《熔断限流 Sentinel》的联动

缓存击穿是熔断限流的典型触发场景

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
缓存击穿
大量请求穿透到 DB
DB 响应慢 (RT 涨到 2s)
Sentinel 检测到慢调用比例 > 50%
熔断器打开
后续请求直接返回降级结果
缓存回填后, 熔断器半开探测
探测成功 → 熔断器关闭, 恢复正常

📌 实践:缓存三大问题必须配合熔断限流

单独的缓存击穿治理只能降低风险,配合熔断器才能在事故发生时防止雪崩蔓延

7.3 与系列第 1 篇《异地多活》的联动

异地多活下缓存同步是个两难问题——

方案 A:每机房独立 Redis 集群

1
2
3
4
5
杭州机房 Redis 集群            上海机房 Redis 集群
        │                              │
   只服务杭州用户                  只服务上海用户
        │                              │
   └────── 跨机房调用极少 ──────────────┘

✅ 优点:调用零延迟 ❌ 缺点:跨机房数据不一致(如商品价格)

方案 B:跨机房 Redis 主从

1
2
3
杭州机房 Redis Master ──► 上海机房 Redis Replica (异步复制)
        ▲                              ▲
   本机房写                        跨机房读

✅ 优点:数据最终一致 ❌ 缺点:跨机房写延迟 30-100ms,主写从读模式下从可能延迟 1-5 秒

方案 C:CRDT + 多主(终极方案)

异地多活多写场景下用 CRDT(Conflict-free Replicated Data Types),每个机房独立写、独立读、后台异步合并冲突——实现难度高,但阿里这类大厂在用。

🎯 决策:业务用方案 A,少数汇总用方案 B

99% 业务走单元化 + 本机房缓存——本机房调本机房,跨机房调用很少。少数需要"全局数据"的场景(排行榜、汇总报表)用方案 B。不推荐方案 C——CRDT 复杂度极高,业务收益不匹配


收尾:本篇要点 + 系列承上启下

✍️ 本篇核心结论

  1. 本地缓存——Caffeine 的 W-TinyLFU 比 Guava 命中率提升 30%,TTL 必须加随机偏移防雪崩
  2. 分布式集群——Redis Cluster 用 16384 哈希槽分片,单实例内存不超过 64GB
  3. 主从同步锁——Redisson + watchdog 是 99% 场景的最优解,Redlock 在 5 个独立实例上过半才生效
  4. 文件持久化——混合持久化是 4.0+ 默认推荐,everysec 丢 ≤ 1 秒 是大多数业务的最优解
  5. 三大问题——雪崩(TTL 偏移+多级缓存)/ 穿透(空值+布隆)/ 击穿(互斥锁+逻辑过期)各有一套
  6. 生产场景——多级缓存 + 延迟双删 + 慢查询监控 + LRU 淘汰 + 8 条告警

📚 Java 微服务系列地图

#主题关系
1异地多活跨机房缓存同步
2流量调度(Nginx/LVS)缓存前哨
3K8s 容器编排Redis 部署
4技术选型(SCA + Dubbo3)选型总结
5Nacos配置中心缓存
6Spring Cloud Gateway网关层缓存
7熔断限流 Sentinel缓存击穿保护
8数据库演化缓存与 DB 一致性
9本篇 缓存体系本地 + 集群 + 锁 + 持久化

📖 参考资料


下一篇预告:系列第 10 篇《可观测性体系:Metrics + Tracing + Logging 三件套》——缓存只是性能加速器,怎么监控它的健康、定位它的瓶颈、追溯它的链路?下一篇把 Java 微服务的可观测性讲透。

本系列共 16 篇,本文为第 12 篇 · 查看全部
使用 Hugo 构建
主题 StackJimmy 设计