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); }
|
2. 写穿透模式(Write-Through)
1 2 3
| public void writeThrough(String key, Data data) { cache.update(key, data); }
|
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)); }
|
二、进阶一致性方案
1. 双删策略 + 延迟队列
1 2 3 4 5 6
| public void updateWithDoubleDelete(String key, Data newData) { cache.delete(key); db.update(key, newData); 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
| @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
| 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
| @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
通过合理选择策略组合,可以在一致性、性能和复杂度之间达到最佳平衡。实际项目中建议结合具体业务场景进行压力测试和方案验证。