一、为什么需要缓存同步?
在分布式系统或高并发场景中,若后端数据源(如数据库)发生变更而未及时更新缓存,会导致读取到旧数据,产生不一致问题。例如:用户下单后库存余量未实时刷新,可能允许超卖。因此,必须通过同步机制确保缓存与数据源始终一致。
二、本地缓存同步方案
✅ 方案1:读写锁(ReadWriteLock)
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LockBasedCache<K, V> {
private final Map<K, V> cache = new HashMap<>(); // 存储实际数据的哈希表
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); // 可重入读写锁对象
// 读操作加共享锁(多个线程可同时读)
public V get(K key) {
rwLock.readLock().lock(); // 获取读锁阻止其他线程写操作
try {
return cache.get(key); // 安全地从map中读取值
} finally {
rwLock.readLock().unlock(); // 确保释放读锁避免死锁
}
}
// 写操作加排他锁(独占访问权)
public void put(K key, V value) {
rwLock.writeLock().lock(); // 获取写锁禁止所有读写操作
try {
cache.put(key, value); // 安全更新缓存内容
} finally {
rwLock.writeLock().unlock(); // 释放写锁恢复其他线程访问权限
}
}
}
适用场景:适合读多写少的场景,利用读锁并发提升性能。但写操作会阻塞所有线程,不适合高频写入场景。
✅ 方案2:synchronized关键字
public class SynchCache<K, V> {
private final Map<K, V> cache = new HashMap<>(); // 底层使用普通HashMap存储数据
private final Object lock = new Object(); // 自定义锁对象控制同步块范围
public V get(K key) {
synchronized (lock) { // 同步整个代码块保证原子性
return cache.get(key); // 从缓存中安全取值
}
}
public void put(K key, V value) {
synchronized (lock) { // 互斥访问确保线程安全
cache.put(key, value); // 更新缓存条目
}
}
}
缺点:同步范围过大时导致性能瓶颈,仅适用于简单场景。
✅ 方案3:ConcurrentHashMap(推荐)
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentCache<K, V> {
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>(); // 线程安全的并发哈希表
public V get(K key) {
return cache.get(key); // 内部已实现分段锁机制,细粒度控制并发冲突
}
public void put(K key, V value) {
cache.put(key, value); // 自动处理并发修改异常情况
}
}
优势:基于CAS无锁算法实现高效并发,比Hashtable性能更优。适合大部分非复杂逻辑的缓存需求。
三、分布式缓存同步方案
🔧 方案1:发布/订阅模型(以Redis为例)
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.beans.factory.annotation.Autowired;
@Service
public class RedisSyncService {
@Autowired private RedisTemplate<String, Object> redisTemplate; // Spring管理的Redis客户端组件
// 当数据库更新时调用此方法广播事件
public void publishUpdateEvent(String channel, String key) {
redisTemplate.convertAndSend(channel, key); // 向指定频道发送消息通知所有订阅者
}
// 监听指定频道的变化并刷新缓存
@PostConstruct
public void subscribeEvents() {
redisTemplate.getConnectionFactory().addMessageListener(new MessageListenerAdapter() {
@Override
public void onMessage(Message message, byte[] pattern) {
String updatedKey = new String(message.getBody()); // 解析收到的消息内容
refreshCache(updatedKey); // 根据变化键刷新本地缓存副本
}
}, "__keyspace@0__:updated"); // 订阅键空间通知事件(通配符匹配所有库表变动)
}
private void refreshCache(String key) {
// TODO: 从数据库加载最新数据到缓存
Object newValue = loadFromDatabase(key); // 重新查询主库获取最新值
redisTemplate.opsForValue().set(key, newValue); // 更新Redis中的缓存项
}
}
工作原理:利用Redis的Pub/Sub功能,当某个Key被修改时,向特定通道发送消息,其他服务监听该通道并主动刷新自己的缓存副本。适用于集群环境下的跨节点同步。
📡 方案2:Spring框架集成缓存管理器
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching // 开启Spring缓存抽象层支持注解驱动开发
public class SpringCacheConfig {
@Bean
public CacheManager cacheManager() { // 创建默认的ConcurrentMapCacheManager实例
return new ConcurrentMapCacheManager("default"); // 可配置多个命名缓存区域相互隔离作用域
}
}
// 在业务类中使用@Cacheable自动填充缓存
@Service
public class UserService {
@Cacheable(value = "userCache", key = "#id") // 首次调用时从数据库加载并存入缓存;后续直接命中缓存返回结果
public User getUserById(Long id) {
return userRepository.findById(id); // 实际的数据访问实现逻辑封装在此方法内
}
@CachePut(value = "userCache", key = "#result.id") // 显式更新指定缓存项的内容(常用于对象属性修改后的持久化操作)
public User updateUser(User user) {
return userRepository.save(user); // 保存变更到数据库后再同步到缓存层保持数据一致性
}
@CacheEvict(value = "userCache", key = "#id") // 删除对应的缓存条目以便下次查询能重新加载最新数据
public void deleteUser(Long id) {
userRepository.deleteById(id); // 执行物理删除操作前先使相关缓存失效防止脏读现象发生
}
}
特点:通过AOP切面自动管理缓存生命周期,支持主动/被动同步策略切换。配合@CacheConfig
可定制化更多高级功能(如条件化刷新策略)。
四、高级模式:LoadingCache动态加载机制
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
public class AutoRefreshingCache {
private final LoadingCache<String, User> cache; // Guava提供的带自动加载能力的缓存实现类
private final UserRepository userRepo; // 依赖注入的用户数据访问接口组件
public AutoRefreshingCache(UserRepository repo) { // 构造函数初始化缓存实例并设置参数调优选项
this.userRepo = repo; // 保存仓库引用供后续查询使用
this.cache = CacheBuilder.newBuilder() // 创建构建者对象开始配置各项属性值限制规则等...
.maximumSize(1000) // 设置最大条目数量超过此限制将触发淘汰策略执行回收老数据腾出空间给新条目加入进来维持整体规模稳定在一个合理范围内波动不会无限增长消耗过多内存资源造成OOM错误发生的可能性降低很多倍...
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后的过期时间定义了多久之后被视为陈旧可能需要重新验证有效性或者直接丢弃释放所占用的内存空间给其他更需要的对象存放提供机会...
.build(new CacheLoader<String, User>() { // 构建匿名内部类作为数据源适配器负责在缺失时自动填充空白区域的内容来源...
@Nullable
@Override
public User load(String userId) throws Exception { // 当调用get方法且对应键不存在时会被调用该方法去加载真实对象实例化过程...
return userRepo.findById(userId); // 从持久层获取最新的用户详细信息记录返回给上层调用方使用...
}
});
}
public User getUser(String userId) { // 对外暴露的统一入口点方法封装底层复杂逻辑细节对外部透明化处理...
return cache.getUnchecked(userId); // 如果存在就直接返回不存在的话会自动调用load方法加载新值然后再返回结果给请求方...
}
}
核心思想:采用“懒加载”模式,只有在访问不存在的Key时才触发数据加载流程。结合Guava Cache的异步刷新特性,可实现最小化数据库交互次数的同时保证数据新鲜度。适用于读远多于写的应用场景。
五、选型建议与最佳实践
方案类型 | 优势 | 适用场景 | 注意事项 |
---|---|---|---|
ReadWriteLock | 细粒度控制读写权限 | 读多写少且需严格一致性保障的场景 | 避免写风暴导致读阻塞 |
synchronized | 实现简单无需额外依赖 | 小规模应用或原型快速开发 | 慎用于高并发场景 |
ConcurrentHashMap | JVM原生支持高性能并发操作 | 中等规模缓存池(<1万条目) | 无自动过期策略需手动管理生命周期 |
Redis Pub/Sub | 天然支持分布式环境 | 跨节点缓存同步、实时性要求高的系统 | 消息堆积可能影响吞吐量 |
Spring Cache | 与框架深度集成配置灵活 | Spring生态项目首选方案 | 注意缓存穿透雪崩击穿等问题防范 |
Guava LoadingCache | 自动异步加载减少数据库压力 | 数据量大但访问分散的业务模型 | 合理设置大小限制防止内存溢出 |
总之,Java缓存同步的本质是在性能、一致性和复杂度之间寻找平衡点。建议优先使用ConcurrentHashMap+Spring Cache解决本地缓存需求,对于分布式系统则采用Redis Pub/Sub或JGroups实现跨节点同步。