跳到正文
Joeplover
学习笔记·2026-01-18·约 3 分钟阅读

MySQL + Redis 缓存整合入门

Spring Boot 整合 MySQL 和 Redis 的入门 Demo,演示 Redis 基本操作和数据缓存模式。

数据库缓存

MySQL + Redis 缓存整合:Cache-Aside 模式实战

缓存策略选型

缓存 + 数据库的组合,常见三种策略:

Cache-Aside(旁路缓存)

读:检查缓存 → 命中 → 返回
              → 未命中 → 查 DB → 写入缓存 → 返回
写:更新 DB → 删除缓存

这是最常用、最推荐的策略。关键在写操作:先更新 DB,再删缓存,而不是更新缓存。

为什么删除缓存比更新缓存好?

// 不推荐:更新缓存
public void updateProduct(Product product) {
    db.update(product);
    redis.set("product:" + product.getId(), product);  // 问题:并发写时数据不一致
}

// 推荐:删除缓存
public void updateProduct(Product product) {
    db.update(product);
    redis.delete("product:" + product.getId());  // 下次读时自动写入新值
}

原因:删除操作是幂等的,而更新操作在并发场景下可能写入旧数据。

Read-Through / Write-Through

缓存层同步写入数据库,应用不直接操作数据库。实现复杂,适合有成熟缓存中间件的场景。

Write-Behind

先写缓存,异步刷回数据库。性能最高但存在数据丢失风险。

三大缓存问题

1. 缓存穿透(Cache Penetration)

问题:查询一个不存在的数据。每次请求都穿透缓存打到数据库。

// 错误做法
@Cacheable(key = "#id")
public Product findById(Long id) {
    return productRepository.findById(id).orElse(null);
    // 返回 null 也不缓存,下次同样穿透
}

解决:缓存空值

@Cacheable(key = "#id", unless = "#result == null")
public Product findById(Long id) {
    return productRepository.findById(id).orElse(null);
}

不对——这样 null 不缓存,还是会穿透。正确做法:

public Product findById(Long id) {
    String key = "product:" + id;
    Object cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
        // 如果是空值标记,返回 null
        if (cached instanceof NullObject) return null;
        return (Product) cached;
    }
    Product product = productRepository.findById(id).orElse(null);
    // 无论是否存在都缓存(空值缓存 5 分钟)
    redisTemplate.opsForValue().set(key, 
        product != null ? product : NullObject.INSTANCE,
        product != null ? 30 : 5, TimeUnit.MINUTES);
    return product;
}

2. 缓存击穿(Hotspot Invalid)

问题:某个热点 key 刚好过期的瞬间,大量并发请求同时打到数据库。

解决:互斥锁

public Product findById(Long id) {
    String key = "product:" + id;
    Product cached = (Product) redisTemplate.opsForValue().get(key);
    if (cached != null) return cached;
    
    // 互斥锁,只允许一个线程查数据库
    String lockKey = "lock:product:" + id;
    Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    if (Boolean.TRUE.equals(locked)) {
        try {
            Product product = productRepository.findById(id).orElse(null);
            redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
            return product;
        } finally {
            redisTemplate.delete(lockKey);
        }
    } else {
        Thread.sleep(50);
        return findById(id); // 递归重试
    }
}

3. 缓存雪崩(Cache Avalanche)

问题:大量 key 同时过期,导致大量请求打到数据库。

解决:TTL 加随机偏移

// 不是固定 30 分钟,而是 30 ± 5 分钟随机
long ttl = 30 * 60 + ThreadLocalRandom.current().nextInt(-300, 300);
redisTemplate.opsForValue().set(key, product, ttl, TimeUnit.SECONDS);

总结

缓存是性能优化的首选手段,但不是银弹。引入缓存意味着要处理一致性问题、缓存穿透/击穿/雪崩问题、缓存过期策略等。没有想清楚这些问题之前,不要轻易加缓存。

一个实用原则:先不加缓存,等真的遇到性能瓶颈时,再针对热点数据加缓存。过早优化是万恶之源。