← 返回首页

缓存一致性方案

📂 architecture ⏱ 1 min 196 words

缓存一致性方案

双写策略

同时更新数据库和缓存,先更新数据库再删除缓存,减少不一致窗口。

@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);
}