1. 问题背景:我们为什么需要关心数据一致性?

想象这样一个场景:你刚在电商平台抢到限量球鞋,付款成功后页面却显示库存未减少。这种"显示有货实际无货"的尴尬,就是典型的缓存与数据库数据不一致问题。在互联网高并发场景中,当Redis作为缓存层和MySQL等持久化数据库配合使用时,如何保证两者数据同步,成为系统设计的核心挑战。

2. 经典解决方案全景图

2.1 Cache Aside Pattern(旁路缓存模式)
// 技术栈:Spring Boot + Redis + MySQL
// 查询操作示例
public Product getProductById(Long id) {
    // 1. 先查缓存
    String cacheKey = "product:" + id;
    Product product = redisTemplate.opsForValue().get(cacheKey);
    if (product != null) {
        return product; // 缓存命中直接返回
    }
    
    // 2. 缓存未命中则查数据库
    product = productMapper.selectById(id);
    if (product != null) {
        // 3. 数据库查询结果写入缓存(设置合理过期时间)
        redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
    }
    return product;
}

// 更新操作示例
@Transactional
public void updateProduct(Product product) {
    // 1. 先更新数据库
    productMapper.updateById(product);
    
    // 2. 再删除缓存
    String cacheKey = "product:" + product.getId();
    redisTemplate.delete(cacheKey);
}

实现要点

  • 读流程遵循"缓存存在直接取,不存在查库回填"原则
  • 写操作严格遵循"先更新数据库,再删除缓存"顺序
  • 设置合理的缓存过期时间作为兜底策略
2.2 Read/Write Through Pattern(穿透读写模式)
class CacheThrough:
    def __init__(self, cache, db):
        self.cache = cache  # Redis客户端实例
        self.db = db        # 数据库连接
        
    def read_through(self, key):
        # 自动处理缓存未命中情况
        value = self.cache.get(key)
        if not value:
            value = self.db.query("SELECT * FROM data WHERE key=%s", (key,))
            if value:
                self.cache.setex(key, 3600, value)  # 设置1小时过期
        return value
    
    def write_through(self, key, value):
        # 原子化更新操作
        with self.db.transaction():
            self.db.execute("UPDATE data SET value=%s WHERE key=%s", (value, key))
            self.cache.setex(key, 3600, value)

模式特点

  • 抽象出统一的缓存访问层
  • 对业务代码透明化处理缓存逻辑
  • 需要维护复杂的状态管理
2.3 延迟双删策略
// 技术栈:Spring Boot + Redisson
@Autowired
private RedissonClient redissonClient;

public void updateWithDoubleDelete(Long productId, Product newData) {
    // 第一次删除缓存
    String cacheKey = "product:" + productId;
    redisTemplate.delete(cacheKey);
    
    // 更新数据库
    productMapper.updateById(newData);
    
    // 提交事务后异步二次删除
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                // 延迟500毫秒二次删除
                redissonClient.getLock(cacheKey + "_lock").lock();
                try {
                    Thread.sleep(500);
                    redisTemplate.delete(cacheKey);
                } finally {
                    redissonClient.getLock(cacheKey + "_lock").unlock();
                }
            }
        });
}

适用场景

  • 高并发写操作环境
  • 需要处理极端情况下的脏读问题
  • 配合分布式锁防止并发修改

3. 进阶解决方案

3.1 基于消息队列的最终一致性
// 技术栈:Spring Cloud Stream + RabbitMQ
@Autowired
private CacheUpdateSender cacheUpdateSender;

public void updateOrderStatus(Long orderId, String status) {
    // 1. 数据库更新
    orderMapper.updateStatus(orderId, status);
    
    // 2. 发送缓存更新事件
    CacheUpdateEvent event = new CacheUpdateEvent(
        "order", 
        orderId.toString(), 
        "status"
    );
    cacheUpdateSender.send(event);
}

// 消息消费者
@StreamListener("cacheUpdateInput")
public void handleCacheUpdate(CacheUpdateEvent event) {
    String cacheKey = event.getEntityType() + ":" + event.getEntityId();
    if ("delete".equals(event.getOperationType())) {
        redisTemplate.delete(cacheKey);
    } else {
        Object data = fetchFromDatabase(event);
        redisTemplate.opsForValue().set(cacheKey, data);
    }
}

架构优势

  • 解耦数据库操作与缓存更新
  • 通过消息持久化保证操作可达
  • 支持批量处理提升效率
3.2 版本号控制策略
# Redis命令示例
> SET product:123 "{'name':'球鞋','version':5}" EX 3600
> WATCH product:123
> MULTI
> SET product:123 "{'name':'跑鞋','version':6}"
> EXEC

实现原理

  • 在缓存数据中嵌入版本标识
  • 更新时校验版本号连续性
  • 结合WATCH命令实现乐观锁控制
3.3 异步补偿机制
// 技术栈:Quartz调度框架
@Scheduled(fixedDelay = 60000)  // 每分钟执行
public void cacheConsistencyCheck() {
    List<Long> hotProductIds = getHotProductsFromDB();
    hotProductIds.parallelStream().forEach(id -> {
        String cacheKey = "product:" + id;
        Product cacheProduct = redisTemplate.opsForValue().get(cacheKey);
        Product dbProduct = productMapper.selectById(id);
        
        if (cacheProduct != null && !cacheProduct.equals(dbProduct)) {
            log.warn("发现数据不一致,产品ID:{}", id);
            redisTemplate.delete(cacheKey);
        }
    });
}

注意事项

  • 需要识别业务关键数据
  • 控制扫描频率避免性能损耗
  • 记录差异日志用于后续分析

4. 技术选型指南

应用场景矩阵

方案名称 适用场景 QPS支持 数据敏感性
Cache Aside 读多写少 10万+ 最终一致
Write Through 写密集型业务 5万+ 强一致
延迟双删 秒杀类场景 1万+ 最终一致
消息队列方案 分布式系统 横向扩展 最终一致

技术对比表

维度 实现复杂度 一致性强度 性能影响 适用阶段
手动维护策略 ★★☆☆☆ ★★☆☆☆ 快速验证期
中间件方案 ★★★★☆ ★★★★☆ 成熟系统
全自动解决方案 ★★★★★ ★★★★★ 金融级系统

5. 实战注意事项

  1. 缓存穿透防护:对空值设置短TTL,避免频繁查询不存在的数据
  2. 雪崩预防:采用随机过期时间,避免大量缓存同时失效
  3. 热点Key处理:使用本地缓存+Redis的多级缓存架构
  4. 失败重试机制:对缓存删除操作增加重试队列
  5. 监控告警:实时监控缓存命中率、延迟等核心指标

6. 总结与展望

通过六大核心策略的灵活组合,我们能够构建出适应不同业务场景的缓存一致性解决方案。随着Redis6.0推出的Client-side caching功能,以及新一代分布式数据库的HTAP特性,未来缓存与数据库的边界将越来越模糊。但无论技术如何演进,理解数据流动的本质、选择合适的同步策略,仍然是保证系统可靠性的关键。