Java 项目中如何保证缓存一致性

在 Java 项目中保证缓存一致性是一个复杂但关键的问题,尤其是在高并发场景下。以下是常见的解决方案和最佳实践,结合策略分类和具体实现示例。

一、核心策略分类

1. 旁路缓存模式(Cache-Aside)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 读操作示例
public Data getData(String key) {
Data data = cache.get(key);
if (data == null) {
data = db.load(key);
cache.put(key, data); // 可能存在问题(见后文优化)
}
return data;
}

// 写操作示例
public void updateData(String key, Data newData) {
db.update(key, newData);
cache.delete(key); // 先更新DB再失效缓存
}

2. 写穿透模式(Write-Through)

1
2
3
public void writeThrough(String key, Data data) {
cache.update(key, data); // 缓存层负责同步写DB
}

3. 写回模式(Write-Behind)

1
2
3
4
5
// 使用队列异步处理
public void writeBehind(String key, Data data) {
cache.update(key, data);
mq.send(new UpdateEvent(key, data)); // 异步持久化到DB
}

二、进阶一致性方案

1. 双删策略 + 延迟队列

1
2
3
4
5
6
public void updateWithDoubleDelete(String key, Data newData) {
cache.delete(key); // 第一次删除
db.update(key, newData);
// 延迟后再次删除(使用线程池或MQ)
delayQueue.schedule(() -> cache.delete(key), 500, TimeUnit.MILLISECONDS);
}

2. 基于版本号/时间戳控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DataWrapper {
private long version; // 数据版本号
private Data data;
}

// 更新时校验版本
public void updateData(String key, Data newData, long clientVersion) {
DataWrapper cached = cache.get(key);
if (cached.getVersion() != clientVersion) {
throw new OptimisticLockException();
}
db.update(key, newData);
cache.put(key, new DataWrapper(newData, clientVersion + 1));
}

3. 订阅数据库变更日志(CDC)

1
2
3
4
5
6
7
8
// 使用Canal监听MySQL binlog
@CanalEventListener
public class CacheUpdateListener {
@ListenPoint(schema = "app_db", table = "user")
public void onUpdate(User user) {
cache.update(user.getId(), user);
}
}

三、分布式场景解决方案

1.分布式锁保证原子性

1
2
3
4
5
6
7
8
9
10
11
// Redisson实现示例
public void safeUpdate(String key, Data newData) {
RLock lock = redisson.getLock("lock:" + key);
try {
lock.lock();
db.update(key, newData);
cache.delete(key);
} finally {
lock.unlock();
}
}

2.多级缓存同步

1
2
3
4
5
6
7
8
9
10
11
// 使用Redis Pub/Sub同步节点
@Component
public class CacheSyncListener {
@Autowired
private LocalCache localCache;

@RedisListener(channel = "cache:invalid")
public void onInvalid(String key) {
localCache.remove(key);
}
}

四、框架级解决方案

1.Spring Cache + 事务管理

1
2
3
4
5
@Transactional
@CacheEvict(value="userCache", key="#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}

2.Tair(阿里)或Redis模块

1
2
3
# 使用Redis模块实现原子操作
Redis.call('SET', KEYS[1], ARGV[1])
Redis.call('PUBLISH', 'cache:update', KEYS[1])

五、设计原则总结

策略 一致性级别 性能影响 复杂度 适用场景
Cache-Aside 最终一致 读多写少
Write-Through 强一致 写多读少
Write-Behind 最终一致 允许数据丢失的写入场景
分布式锁 强一致 金融交易类系统
版本控制 乐观锁 高并发更新场景

最佳实践组合建议:

  • 常规场景:Cache-Aside + 双删策略 + 版本控制
  • 高并发写:Write-Through + 分布式锁
  • 最终一致:Write-Behind + CDC监听
  • 兜底方案:所有缓存设置合理TTL

通过合理选择策略组合,可以在一致性、性能和复杂度之间达到最佳平衡。实际项目中建议结合具体业务场景进行压力测试和方案验证。