06. Java 缓存策略与 Redis 集成

掌握本地缓存 Caffeine、Spring Cache 抽象与 Redis 分布式缓存的多级架构设计,解决缓存一致性、穿透、雪崩与击穿问题

缓存是性能优化的第一现场。从线程级 ThreadLocal 到进程级 Caffeine,再到分布式 Redis,每一层缓存都是空间换时间的权衡。本文逐层拆解缓存设计,聚焦一致性、并发、过期三大挑战。

1. 缓存层级金字塔

访问速度 ▲
        │  L1 CPU Cache      < 1ns
        │  L2/L3 CPU Cache   < 10ns
        │  ThreadLocal       ~10ns
        │  Caffeine (本地)    ~100ns
        │  Redis (网络)       ~1ms
        │  数据库             ~10ms
        │  外部接口           ~100ms+
        └────────────────────────
               容量 →

企业常用的三级缓存

  • L1:Caffeine(JVM 本地,读 QPS 10 万+)
  • L2:Redis(分布式共享,读 QPS 5-10 万)
  • L3:数据库(最终持久化)

2. Caffeine:高性能本地缓存

2.1 核心特性

  • W-TinyLFU 驱逐算法:频率 + 时效双维度,命中率优于 LRU/LFU
  • Window Cache:热点数据保护窗口
  • 异步加载 / 刷新:不阻塞读线程
  • 弱/软引用支持:防止内存泄漏

2.2 使用示例

LoadingCache<String, User> userCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(Duration.ofMinutes(10))
    .refreshAfterWrite(Duration.ofMinutes(5))   // 异步刷新,不阻塞读
    .recordStats()                                // 开启统计
    .build(key -> userRepository.findById(key));  // CacheLoader

// 使用
User user = userCache.get(userId);  // 缓存未命中 → 自动加载
userCache.invalidate(userId);       // 主动失效

2.3 W-TinyLFU 原理

访问请求
  ↓
Window Cache (1% 空间,纯粹 FIFO)
  ├─ 命中 → 移到 TinyLFU 过滤器评估
  │
  └─ Window淘汰 → 进入 TinyLFU 淘汰区
       ↓
  TinyLFU (99% 空间)
  ├─ Doorkeeper Bloom Filter → 严格频率统计
  ├─ Count-Min Sketch → 近似频率计数(节省内存)
  └─ 淘汰低频 → 保留高频

对比其他算法

算法特点劣势
LRU淘汰最近最少使用对突发流量敏感,历史热点被覆盖
LFU淘汰使用频率最低累积频率,新热点无法进入
W-TinyLFU窗口 + 频率略复杂,但命中率高

3. Spring Cache 抽象

3.1 注解驱动缓存

@Service
public class ProductService {
    
    @Cacheable(value = "products", key = "#id", unless = "#result == null")
    public Product getProduct(Long id) {
        return productDao.findById(id);
    }
    
    @CachePut(value = "products", key = "#product.id")
    public Product updateProduct(Product product) {
        return productDao.save(product);
    }
    
    @CacheEvict(value = "products", key = "#id")
    public void deleteProduct(Long id) {
        productDao.deleteById(id);
    }
    
    @Caching(evict = {
        @CacheEvict(value = "products", key = "#product.id"),
        @CacheEvict(value = "products", key = "'list'")
    })
    public Product saveProduct(Product product) {
        return productDao.save(product);
    }
}

3.2 自定义 CacheManager

@Configuration
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));
        
        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .withCacheConfiguration("products", config.entryTtl(Duration.ofMinutes(5)))
            .withCacheConfiguration("users", config.entryTtl(Duration.ofHours(1)))
            .transactionAware()
            .build();
    }
}

4. Redis 缓存实战

4.1 序列化选型

序列化器优点缺点
JdkSerializationRedisSerializer原生兼容二进制不可读、体积大
StringRedisSerializer可读仅支持 String
Jackson2JsonRedisSerializerJSON 可读、跨语言日期/类型信息需处理
GenericJackson2JsonRedisSerializer自动类型信息类型安全需开启白名单
Kryo / FST(自定义)体积小、速度快额外依赖、兼容性
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(factory);
    
    // Key: String
    template.setKeySerializer(new StringRedisSerializer());
    template.setHashKeySerializer(new StringRedisSerializer());
    
    // Value: JSON with type info
    ObjectMapper mapper = new ObjectMapper()
        .activateDefaultTyping(
            BasicPolymorphicTypeValidator.builder()
                .allowIfBaseType(Object.class).build(),
            ObjectMapper.DefaultTyping.NON_FINAL
        );
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer(mapper));
    template.afterPropertiesSet();
    return template;
}

4.2 多级缓存架构 (Caffeine + Redis)

@Component
public class MultiLevelCache {
    private final LoadingCache<String, Object> localCache;
    private final RedisTemplate<String, Object> redisTemplate;
    
    public Object get(String key) {
        // L1: 本地缓存
        Object value = localCache.getIfPresent(key);
        if (value != null) return value;
        
        // L2: Redis
        value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            localCache.put(key, value);  // 回填本地
            return value;
        }
        
        // L3: 数据库(CacheLoader 自动处理)
        value = loadFromDatabase(key);
        if (value != null) {
            redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(10));
            localCache.put(key, value);
        }
        return value;
    }
}

5. 缓存三大经典问题

5.1 缓存穿透(Cache Penetration)

场景:查询不存在 key(如 id=-1),绕过缓存直达数据库。

方案

// 1. 布隆过滤器(Bloom Filter)预判定
@Component
public class BloomFilterCache {
    private final BloomFilter<String> bloomFilter;
    
    public Object get(String key) {
        if (!bloomFilter.mightContain(key)) {
            return null;  // 肯定不存在,直接返回
        }
        // 继续查缓存/数据库
    }
}

// 2. 缓存空值(短期)
if (value == null) {
    redisTemplate.opsForValue().set(key, EMPTY_VALUE, Duration.ofMinutes(5));
}

布隆过滤器参数

  • 预计元素 n,期望误判率 p → 位图大小 m = -n * ln(p) / (ln2)^2,哈希函数 k = m/n * ln2
  • Guava: BloomFilter.create(Funnels.stringFunnel(), 1000000, 0.01)

5.2 缓存击穿(Cache Breakdown)

场景:热点 key 过期瞬间,大量请求同时打到数据库。

方案:互斥锁 + 异步重建

public Object getWithMutex(String key) {
    Object value = redisTemplate.opsForValue().get(key);
    if (value != null) return value;
    
    // 分布式锁
    String lockKey = "lock:" + key;
    Boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, "1", Duration.ofSeconds(10));
    
    if (Boolean.TRUE.equals(locked)) {
        try {
            value = loadFromDatabase(key);
            redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(10));
        } finally {
            redisTemplate.delete(lockKey);
        }
    } else {
        // 其他线程等待后重试
        Thread.sleep(100);
        return getWithMutex(key);
    }
    return value;
}

Redisson 实现

RLock lock = redissonClient.getLock("cache:lock:" + key);
try {
    lock.lock(10, TimeUnit.SECONDS);
    // 双重检查
    value = redisTemplate.opsForValue().get(key);
    if (value == null) {
        value = loadFromDatabase(key);
        redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(10));
    }
} finally {
    lock.unlock();
}

5.3 缓存雪崩(Cache Avalanche)

场景:大量 key 同时过期 / Redis 宕机,数据库瞬时压力暴增。

方案

// 1. 过期时间加随机偏移
Duration baseTtl = Duration.ofMinutes(30);
Duration jitter = Duration.ofSeconds(ThreadLocalRandom.current().nextInt(300));
redisTemplate.opsForValue().set(key, value, baseTtl.plus(jitter));

// 2. 多级缓存 + 熔断
// L1 Caffeine 即使 Redis 宕机,仍有 10 分钟缓冲

// 3. Redis 高可用
// 主从 + Sentinel / Cluster;K8s 环境下持久化 PVC + StatefulSet

6. 缓存一致性策略

6.1 Cache-Aside(旁路缓存)

读:Cache → Miss → DB → 写入 Cache
写:更新 DB → 删除 Cache(非更新 Cache,避免并发脏写)

延迟双删(最终一致性增强)

public void update(Product product) {
    cache.delete("product:" + product.getId());   // 先删
    db.update(product);                            // 再写
    
    // 异步延迟再删(覆盖读写并发窗口)
    executor.schedule(() -> cache.delete("product:" + product.getId()), 
                      500, TimeUnit.MILLISECONDS);
}

6.2 Read-Through / Write-Through

缓存层封装了 DB 操作,代码只与缓存交互。需使用支持此模式的框架(如 Redis with CacheStore)。

6.3 Write-Behind(异步写回)

写请求 → 写入缓存 → 立即返回
         └── 异步队列 → 批量写入 DB

适用:写多读少、允许短暂不一致(如计数器、日志)。

6.4 Canal + 缓存订阅

MySQL Binlog → Canal Server → Kafka/消息队列 → 缓存更新/删除

优势:业务代码零侵入,最终一致性。

7. Redisson 高级功能

// 分布式对象
RMap<String, User> map = redisson.getMap("users");
RLocalCachedMap<String, User> localMap = redisson.getLocalCachedMap("users", 
    LocalCachedMapOptions.<String, User>defaults()
        .evictionPolicy(LocalCachedMapOptions.EvictionPolicy.LRU)
        .timeToLive(10, TimeUnit.MINUTES));

// 分布式锁(可重入、看门狗自动续期)
RLock lock = redisson.getLock("order:" + orderId);
lock.lock();
try { processOrder(); } finally { lock.unlock(); }

// 分布式计数器
RAtomicLong counter = redisson.getAtomicLong("counter");

counter.incrementAndGet();

8. 性能基准参考

操作Caffeine (本地)Redis (单节点)Redis (集群)
~10M ops/s~100K ops/s~80K ops/s
~5M ops/s~50K ops/s~40K ops/s
内存/M条目~200MB~400MB~400MB
序列化损耗~010-30%10-30%

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 限流算法深度解析:令牌桶、漏桶与滑动窗口计数
  2. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  3. Spring IoC 容器与依赖注入原理深度剖析