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);
总结
缓存是性能优化的首选手段,但不是银弹。引入缓存意味着要处理一致性问题、缓存穿透/击穿/雪崩问题、缓存过期策略等。没有想清楚这些问题之前,不要轻易加缓存。
一个实用原则:先不加缓存,等真的遇到性能瓶颈时,再针对热点数据加缓存。过早优化是万恶之源。