Java双重检测锁解决MySQL和Redis数据一致性问题
双重检测锁(Double-Checked Locking)是一种在多线程环境下优化性能的设计模式,可以用于解决MySQL和Redis之间的数据一致性问题。下面我将介绍如何实现这一方案。
问题背景
在MySQL和Redis双存储系统中,常见的一致性问题包括:
- 缓存穿透:查询不存在的数据,导致每次请求都打到数据库
- 缓存击穿:热点key失效瞬间,大量请求直接访问数据库
- 缓存雪崩:大量key同时失效,导致数据库压力骤增
- 数据不一致:数据库更新后缓存未同步
双重检测锁实现方案
public class CacheService {
private final RedisTemplate<String, Object> redisTemplate;
private final DataMapper dataMapper;
private final ReentrantLock lock = new ReentrantLock();
public CacheService(RedisTemplate<String, Object> redisTemplate, DataMapper dataMapper) {
this.redisTemplate = redisTemplate;
this.dataMapper = dataMapper;
}
public Object getData(String key) {
// 第一次检测:查询缓存
Object value = redisTemplate.opsForValue().get(key);
if (value != null) {
return value;
}
// 加锁防止缓存击穿
lock.lock();
try {
// 第二次检测:再次查询缓存(其他线程可能已经更新了缓存)
value = redisTemplate.opsForValue().get(key);
if (value != null) {
return value;
}
// 查询数据库
value = dataMapper.selectByKey(key);
if (value != null) {
// 写入缓存并设置过期时间
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
} else {
// 防止缓存穿透:缓存空值但设置较短过期时间
redisTemplate.opsForValue().set(key, "", 1, TimeUnit.MINUTES);
}
return value;
} finally {
lock.unlock();
}
}
@Transactional
public void updateData(String key, Object newValue) {
// 更新数据库
dataMapper.updateByKey(key, newValue);
// 删除缓存(采用延迟双删策略)
redisTemplate.delete(key);
// 异步延迟再次删除(可选,解决可能的脏数据)
new Thread(() -> {
try {
Thread.sleep(500); // 延迟500ms
redisTemplate.delete(key);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
关键点解析
-
双重检测:
- 第一次检测:无锁状态下快速检查缓存
- 第二次检测:加锁后再次检查,防止其他线程已经更新了缓存
-
锁机制:
- 使用ReentrantLock确保只有一个线程能执行数据库查询
- 防止缓存击穿(热点key失效时大量请求直接访问数据库)
-
缓存策略:
- 缓存空值防止缓存穿透
- 设置合理过期时间防止数据长期不一致
- 延迟双删策略确保数据一致性
-
事务处理:
- 数据库更新使用@Transactional确保原子性
- 先更新数据库再删除缓存(Cache Aside Pattern)
优化建议
- 可以使用分布式锁(如Redis的SETNX)替代本地锁,适用于分布式环境
- 考虑引入消息队列异步更新缓存,降低数据库压力
- 对于热点数据,可以设置永不过期,通过后台任务定期刷新
- 使用布隆过滤器(Bloom Filter)进一步防止缓存穿透
注意事项
- 锁的粒度要尽可能小,避免性能瓶颈
- 要考虑锁超时情况,避免死锁
- 双重检测锁在Java中需要配合volatile关键字使用(但在Spring管理的Bean中通常不需要)
- 高并发场景下需要充分测试性能表现
这种方案在大多数场景下能较好地平衡性能与一致性需求,但需要根据具体业务场景进行调整。