缓存一致性方案
缓存一致性方案
双写策略
同时更新数据库和缓存,先更新数据库再删除缓存,减少不一致窗口。
@Service
public class CacheAsideService {
@Transactional
public void updateProduct(String id, ProductDTO dto) {
// 1. 先更新数据库
productRepository.save(dto);
// 2. 再删除缓存
redisTemplate.delete("product:" + id);
// 3. 发送异步消息保证删除
kafkaTemplate.send("cache-evict", "product:" + id);
}
public ProductDTO getProduct(String id) {
String key = "product:" + id;
ProductDTO dto = (ProductDTO) redisTemplate.opsForValue().get(key);
if (dto != null) return dto;
dto = productRepository.findById(id).orElse(null);
if (dto != null) {
redisTemplate.opsForValue().set(key, dto, 30, TimeUnit.MINUTES);
}
return dto;
}
}
延迟双删
第一次删除在更新数据库前,第二次删除在写入数据库后延迟执行,解决并发读写不一致。
@Service
public class DelayDoubleDeleteService {
@Transactional
public void update(String id, Object data) {
String key = "cache:" + id;
// 1. 先删缓存
redisTemplate.delete(key);
// 2. 更新数据库
repository.update(id, data);
// 3. 延迟500ms再删一次
CompletableFuture.runAsync(() -> {
Thread.sleep(500);
redisTemplate.delete(key);
}, executor);
}
}
Canal监听Binlog
通过Canal监听MySQL binlog变更,异步更新缓存,实现最终一致性。
@Component
public class CanalCacheSyncListener {
@CanalListener(destination = "product")
public void onProductChange(CanalMessage message) {
for (CanalEntry.RowData row : message.getRowDataList()) {
String id = getPrimaryKey(row);
String key = "product:" + id;
if (isDelete(row)) {
redisTemplate.delete(key);
} else {
ProductDTO dto = productRepository.findById(id);
redisTemplate.opsForValue().set(key, dto, 30, TimeUnit.MINUTES);
}
}
}
}
TTL兜底策略
所有方案都应设置合理的TTL作为最终兜底,防止不一致长期存在。
public void setWithTTL(String key, Object value) {
int ttl = 30 * 60; // 30分钟
int jitter = ThreadLocalRandom.current().nextInt(300);
redisTemplate.opsForValue().set(key, value, ttl + jitter, TimeUnit.SECONDS);
}