1. 当缓存遇上多线程:一个快递柜引发的思考

想象一下这样的场景:小区快递柜被多个快递员同时使用,A快递员正在往3号柜存包裹时,B快递员刚好扫码打开了3号柜。这种"存取冲突"在Redis多线程读写时每天都在上演。我们的缓存就像这个快递柜系统,当多个线程同时读写时,如果没有合适的协调机制,就可能出现数据错乱、旧数据残留等问题。

典型的异常场景包括:

  • 线程A更新数据库后删除缓存时,线程B却读取到了旧数据
  • 高频更新导致缓存频繁失效,引发缓存雪崩
  • 写后立即读操作获取到更新前的脏数据

2. 问题根源剖析:多线程缓存操作的陷阱

2.1 数据覆盖危机

// Java示例使用Spring Data Redis
public void updateProduct(Product product) {
    // 线程A开始更新
    redisTemplate.delete("product:" + product.getId()); // 删除旧缓存
    db.update(product);  // 更新数据库(耗时操作)
    
    // 在线程A更新数据库期间...
    // 线程B查询相同商品
    Product cached = redisTemplate.opsForValue().get("product:" + product.getId());
    if(cached == null) {
        cached = db.get(product.getId());  // 此时获取到的是旧数据
        redisTemplate.opsForValue().set("product:" + product.getId(), cached);
    }
    // 最终缓存被旧数据覆盖
}

技术栈:Java + Spring Data Redis
注释说明

  1. 线程A先删缓存后更新数据库
  2. 在线程A更新数据库期间,线程B查询导致旧数据重新加载到缓存
  3. 最终数据库是新数据,缓存却是旧数据

3. 解决方案大比武

3.1 读写锁方案:给缓存操作上把"智能锁"

// 使用ReentrantReadWriteLock
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

public Product getProduct(Long id) {
    lock.readLock().lock();
    try {
        // 读锁保证并发读不受限
        Product product = redisTemplate.opsForValue().get("product:"+id);
        if(product == null) {
            // 释放读锁,获取写锁(防止多个线程同时查库)
            lock.readLock().unlock();
            lock.writeLock().lock();
            try {
                // 双重检查
                product = redisTemplate.opsForValue().get("product:"+id);
                if(product == null) {
                    product = db.get(id);
                    redisTemplate.opsForValue().set("product:"+id, product, 30, TimeUnit.MINUTES);
                }
            } finally {
                lock.writeLock().unlock();
                // 重新获取读锁保持方法对称
                lock.readLock().lock();
            }
        }
        return product;
    } finally {
        lock.readLock().unlock();
    }
}

技术栈:Java并发库 + Spring Data Redis
优势

  • 读操作并发性能高
  • 写操作互斥保证安全
    缺点
  • 锁粒度过粗会影响性能
  • 需要处理锁升级问题

3.2 版本号控制:给数据贴上"身份证"

> SET product:1001_version 1
> INCR product:1001_version  # 每次更新递增版本号

# 数据存储结构
{
  "id": 1001,
  "name": "智能手表",
  "version": 3,
  "...": "..."
}
public void updateProduct(Product newProduct) {
    // 获取当前版本
    Long currentVersion = redisTemplate.opsForValue().get("product:"+newProduct.getId()+"_version");
    
    // 乐观锁控制
    if(newProduct.getVersion() <= currentVersion) {
        throw new OptimisticLockException("版本过期,请刷新重试");
    }
    
    // 原子操作更新
    redisTemplate.execute(new SessionCallback<>() {
        public Object execute(RedisOperations operations) {
            operations.watch("product:"+newProduct.getId());
            operations.multi();
            operations.opsForValue().set("product:"+newProduct.getId(), newProduct);
            operations.opsForValue().increment("product:"+newProduct.getId()+"_version");
            return operations.exec();
        }
    });
}

技术栈:Redis事务 + 版本控制
适用场景

  • 高频更新业务(如库存秒杀)
  • 需要保证最终一致性的场景

4. 进阶方案:组合拳的威力

4.1 延时双删策略:给缓存更新加个"缓冲期"

public void updateProduct(Product product) {
    // 第一次删除
    redisTemplate.delete("product:"+product.getId());
    
    // 数据库更新
    db.update(product);
    
    // 延时二次删除(使用异步线程)
    CompletableFuture.runAsync(() -> {
        try {
            TimeUnit.MILLISECONDS.sleep(500); // 根据业务设置合理延时
            redisTemplate.delete("product:"+product.getId());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
}

注意事项

  • 延时时间需要根据业务负载动态调整
  • 必须配合重试机制防止删除失败

5. 技术选型指南:没有银弹,只有合适的鞋

5.1 方案对比矩阵

方案 适用QPS 数据一致性 实现复杂度 额外开销
读写锁 < 5k 强一致 中等
版本控制 > 10k 最终一致 中等
延时双删 1k-5k 最终一致

6. 避坑指南:血泪经验总结

  1. 锁粒度控制:某电商平台曾因全局锁导致性能下降80%,后改为按商品ID哈希分桶加锁
  2. 版本号存储:建议与业务数据分开存储,使用Redis Hash结构管理
  3. 监控告警:必须监控缓存命中率、锁等待时间等关键指标
  4. 压测验证:某社交APP在版本更新后未做压测,导致缓存击穿引发宕机

7. 总结:在一致性与性能的天平上

通过本文的多个实战方案可以看到,缓存一致性问题的解决需要根据具体业务场景选择合适策略。对于交易类系统,可能需要牺牲部分性能换取强一致性;而对于资讯类平台,可以接受短暂的不一致来换取更高的吞吐量。

关键决策点

  • 业务对数据时效性的容忍度
  • 系统的读写比例
  • 基础设施的监控能力
  • 团队的技术储备

未来随着Redis 6.0多线程模型的普及,还需要关注IO线程与worker线程的协同问题。缓存一致性这场攻防战,永远在性能与正确性之间寻找最佳平衡点。