问题概述
在使用Redis作为缓存层时,当数据库中的数据发生变化,如果不及时更新或删除缓存,就会导致缓存数据与数据库数据不一致的问题。
解决模式
Cache-Aside模式(旁路缓存模式)
应用程序负责维护缓存和数据库之间的一致性。读取数据时先查缓存,缓存未命中时从数据库读取并更新缓存;写入数据时先更新数据库,然后使缓存失效。
// 读取数据
public Data get(String key) {
// 1. 从缓存读取
Data data = redisCache.get(key);
// 2. 缓存未命中,从数据库读取
if (data == null) {
data = database.get(key);
// 3. 将数据库数据写入缓存
if (data != null) {
redisCache.set(key, data);
}
}
return data;
}
// 更新数据
public void update(String key, Data newData) {
// 1. 更新数据库
database.update(key, newData);
// 2. 删除缓存
redisCache.delete(key);
}
Write-Through模式(直写模式)
写入数据时,同时更新缓存和数据库。读取数据时直接从缓存获取。
// 读取数据
public Data get(String key) {
// 直接从缓存读取
return redisCache.get(key);
}
// 更新数据
public void update(String key, Data newData) {
// 同时更新数据库和缓存
database.update(key, newData);
redisCache.set(key, newData);
}
Write-Behind模式(异步写入模式)
写入数据时,先更新缓存,然后异步批量更新数据库,提高写性能。
// 读取数据
public Data get(String key) {
// 直接从缓存读取
return redisCache.get(key);
}
// 更新数据
public void update(String key, Data newData) {
// 1. 更新缓存
redisCache.set(key, newData);
// 2. 将更新操作放入队列,异步更新数据库
updateQueue.add(new UpdateOperation(key, newData));
}
// 异步更新线程
public void asyncDatabaseUpdater() {
while (true) {
List batch = updateQueue.getBatch(MAX_BATCH_SIZE);
if (!batch.isEmpty()) {
database.batchUpdate(batch);
}
Thread.sleep(BATCH_INTERVAL);
}
}
Refresh-Ahead模式(预刷新模式)
系统智能地预测哪些数据可能会被访问,在数据过期之前主动刷新缓存。
// 定时刷新任务
@Scheduled(fixedRate = 60000) // 每分钟执行一次
public void refreshHotData() {
List hotKeys = accessStatistics.getTopAccessedKeys(100);
for (String key : hotKeys) {
CacheEntry entry = redisCache.getWithMeta(key);
// 如果数据即将过期(例如 TTL < 5分钟),提前刷新
if (entry != null && entry.getTtl() < 300) {
Data freshData = database.get(key);
redisCache.set(key, freshData);
}
}
}
具体策略
双删策略
为了解决更新数据库后,可能其他线程把旧数据读入缓存的问题,可以采用双删策略。
// 更新数据
public void update(String key, Data newData) {
// 1. 先删除缓存
redisCache.delete(key);
// 2. 更新数据库
database.update(key, newData);
// 3. 延时再次删除缓存,避免其他线程将旧数据写入缓存
executor.schedule(() -> {
redisCache.delete(key);
}, 500, TimeUnit.MILLISECONDS);
}
消息队列保证最终一致性
使用消息队列在缓存和数据库操作之间进行解耦,确保最终一致性。
// 更新数据
public void update(String key, Data newData) {
// 1. 更新数据库
database.update(key, newData);
// 2. 发送消息到消息队列
messageQueue.send(new CacheInvalidateMessage(key));
}
// 消息消费者
@KafkaListener(topics = "cache-invalidate")
public void processCacheInvalidation(CacheInvalidateMessage message) {
// 收到消息后删除对应的缓存
redisCache.delete(message.getKey());
}
TTL策略
为缓存设置合理的过期时间,过期后自动从数据库加载新数据。
// 读取数据
public Data get(String key) {
// 从缓存读取
Data data = redisCache.get(key);
// 缓存未命中,从数据库读取
if (data == null) {
data = database.get(key);
// 设置缓存并添加过期时间
if (data != null) {
redisCache.set(key, data, 5, TimeUnit.MINUTES);
}
}
return data;
}
// 更新数据
public void update(String key, Data newData) {
// 只需要更新数据库,缓存会自动过期
database.update(key, newData);
}
动态演示
方案对比
策略 |
优点 |
缺点 |
适用场景 |
Cache-Aside |
|
|
读多写少的通用场景 |
Write-Through |
|
|
需要强一致性的场景 |
Write-Behind |
|
|
高并发写入、对一致性要求不高的场景 |
双删策略 |
|
|
读写并发较高的场景 |
消息队列 |
|
|
高可用分布式系统 |
TTL策略 |
|
|
对一致性要求不高、数据变化较少的场景 |