告别缓存不一致!Redis三种经典读写策略深度解析

0 阅读6分钟

引言

缓存是提升系统性能的法宝,但缓存与数据库的双写问题一直是开发者面临的经典难题:是先更新数据库还是先删除缓存?要不要加锁?如何保证最终一致性?

在高并发系统中,Redis作为缓存几乎成了标配。然而,缓存与数据库的一致性问题却让许多开发者头疼不已。本文将带你深入剖析三种最常用的缓存读写策略——Cache Aside、Read/Write Through、Write Behind,结合实际场景分析其优劣与选型建议,帮你彻底告别缓存与数据库不一致的烦恼。


一、Cache Aside(旁路缓存)策略

1.1 核心流程

Cache Aside 是实际应用中最广泛的策略,它将缓存与数据库的交互完全交给应用层:

读流程

  1. 应用先查询缓存(Redis)
  2. 如果命中,直接返回数据
  3. 如果未命中,则查询数据库
  4. 将查询结果写入缓存,并返回

写流程

  1. 应用先更新数据库
  2. 删除对应的缓存(而不是更新缓存)

为什么是删除而不是更新?
因为更新操作可能涉及复杂逻辑,且并发更新可能导致缓存脏数据。删除后,下次读取时自然会重建缓存,简单可靠。

1.2 并发问题与解决方案

在写操作中,先更新数据库后删除缓存,可能遇到以下并发问题:

场景A:线程1更新数据库,但还未删除缓存;线程2读取缓存,命中了旧数据,返回了脏数据。

解决方案
此场景的概率极低(需要更新数据库和删除缓存之间恰好有读请求),通常可接受。若业务对一致性要求极高,可采用延迟双删

public void update(String key, Object data) {
    // 1. 更新数据库
    db.update(data);
    // 2. 删除缓存
    redis.del(key);
    // 3. 延迟后再次删除(可选)
    Thread.sleep(500);
    redis.del(key);
}

场景B:先删除缓存,后更新数据库。若删除后、更新前有读请求,会将旧数据写入缓存,导致缓存长期为脏数据。

结论:业界普遍采用先更新数据库再删除缓存的顺序。

1.3 适用场景

  • 绝大多数通用业务场景
  • 读多写少,且对一致性要求不是极端严苛
  • 缓存失效后可容忍短暂的数据不一致

1.4 代码示例(Java + Spring Data Redis)

@Service
public class UserService {
    @Autowired
    private UserDao userDao;
    
    @Autowired
    private RedisTemplate<String, User> redisTemplate;

    public User getUser(Long id) {
        String key = "user:" + id;
        User user = redisTemplate.opsForValue().get(key);
        if (user != null) {
            return user;
        }
        // 缓存未命中,查数据库
        user = userDao.getById(id);
        if (user != null) {
            redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
        }
        return user;
    }

    public void updateUser(User user) {
        // 更新数据库
        userDao.update(user);
        // 删除缓存
        redisTemplate.delete("user:" + user.getId());
    }
}

二、Read/Write Through(读写穿透)策略

2.1 核心流程

Read/Write Through 策略将缓存作为主要数据源,应用只与缓存交互,由缓存服务负责与数据库同步:

读流程

  1. 应用请求缓存
  2. 若缓存命中,直接返回
  3. 若缓存未命中,缓存服务从数据库加载数据,写入缓存后返回

写流程

  1. 应用将数据写入缓存
  2. 缓存服务同步将数据写入数据库,并返回结果

注意:Redis原生不支持此策略,需要应用层封装或使用代理层实现。

2.2 优点与缺点

优点缺点
应用层逻辑简单,只需与缓存交互缓存层实现复杂,需要处理数据加载、写入、事务等
缓存与数据库之间的一致性由缓存层保证如果缓存层宕机,整个系统不可用
减少了应用层的复杂度需要高可用部署

2.3 适用场景

  • 业务逻辑简单,希望将数据访问统一收敛到缓存层
  • 对一致性要求较高,且愿意接受额外的开发成本
  • 适用于构建在缓存之上的数据服务

2.4 代码示例(模拟封装层)

public class RedisThroughCache {
    private RedisTemplate redisTemplate;
    private UserDao userDao;

    public User get(Long id) {
        String key = "user:" + id;
        User user = redisTemplate.opsForValue().get(key);
        if (user != null) {
            return user;
        }
        // 缓存未命中,从数据库加载并写入缓存
        user = userDao.getById(id);
        if (user != null) {
            redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
        }
        return user;
    }

    public void put(User user) {
        String key = "user:" + user.getId();
        // 先写缓存
        redisTemplate.opsForValue().set(key, user);
        // 再同步写数据库
        userDao.update(user);
    }
}

三、Write Behind(写回)策略

3.1 核心流程

Write Behind 策略也称 Write Back,是异步写入的典型代表:

读流程:与 Read/Write Through 类似,若未命中则从数据库加载

写流程

  1. 应用直接更新缓存
  2. 缓存返回成功
  3. 后台有一个异步任务(或消息队列)将缓存中的变更批量、异步地同步到数据库

3.2 优点与缺点

优点缺点
写性能极高,因为只操作内存缓存数据一致性窗口较大
可以合并多次写操作,减少数据库压力如果缓存宕机,未同步的数据可能丢失
适合高并发写入场景实现复杂,需要设计可靠的同步机制

3.3 适用场景

  • 对数据一致性要求不高,但要求极高写入吞吐量的场景(如日志收集、点赞数、访问计数)
  • 可接受短时间不一致的业务
  • 结合消息队列实现最终一致性

3.4 代码示例(基于消息队列)

@Service
public class WriteBehindService {
    @Autowired
    private RedisTemplate redisTemplate;
    
    @Autowired
    private KafkaTemplate kafkaTemplate;

    // 写操作:直接更新缓存,并发送异步消息
    public void updateUser(User user) {
        String key = "user:" + user.getId();
        redisTemplate.opsForValue().set(key, user);
        // 发送消息到Kafka,消费者异步更新数据库
        kafkaTemplate.send("user-update-topic", user);
    }
}

四、三种策略对比与选型建议

策略优点缺点适用场景
Cache Aside简单,灵活,主流并发时可能短暂不一致绝大多数业务系统
Read/Write Through应用层无感知,缓存与数据库统一管理实现复杂,缓存层成为瓶颈对一致性要求较高,可接受封装
Write Behind写入性能极高,可合并操作数据可能丢失,一致性差高吞吐写入,可容忍最终一致

选型建议

  • 如果你正在使用Redis作为缓存,Cache Aside 是最自然、最易于维护的选择
  • 如果业务需要更高的一致性保障,且愿意在缓存层投入开发,可以考虑 Read/Write Through
  • 对于日志、计数、监控等写密集型且允许丢数据的场景,Write Behind 可以极大提升吞吐量

五、总结

缓存读写策略的选择,本质上是性能、一致性、复杂度三者之间的权衡。

  • Cache Aside 以其简洁和普适性成为大多数系统的首选
  • Read/Write Through 将复杂性封装在缓存层,使应用更加简洁
  • Write Behind 则牺牲强一致性换取极致写入性能

在实际项目中,你还可以结合多种策略:例如热点数据使用 Cache Aside,写密集型数据使用 Write Behind。

最重要的是,根据业务特性选择最合适的方案,并在设计时充分考虑并发场景下的数据一致性问题。

希望本文能帮助你理清缓存读写策略的脉络,在实际架构设计中做出更明智的决策。

本文为原创内容,转载请注明出处。如果你在Redis应用中遇到问题,欢迎在评论区留言讨论!

关注「卷毛的技术笔记」微信公众号,获取Redis深度解析与实战技巧,告别缓存陷阱,让系统性能飙升!