当前位置: 首页 > news >正文

Redis 的三种高效缓存读写策略!

在这里插入图片描述

目录

      • 准备工作:环境与模型
      • 策略一:Cache-Aside (旁路缓存)
        • 1. 概念与工作流程
        • 2. 代码示例 (UserServiceImpl.java)
        • 3. 优缺点与适用场景
        • 4. 常见陷阱与注意事项
      • 策略二:Read/Write-Through (读穿/写穿)
        • 1. 概念与工作流程
        • 2. 代码示例 (使用 Spring Cache 注解)
        • 3. 优缺点与适用场景
      • 策略三:Write-Back (写回)
        • 1. 概念与工作流程
        • 2. 代码示例(概念性实现)
        • 3. 优缺点与适用场景
      • 总结与策略选择

在企业级应用中,缓存是应对高并发、提升系统性能的关键一环。而如何确保缓存与数据库之间数据的一致性、高效性与可用性,正是我们设计缓存策略的核心。下面,我将循序渐进地为您讲解 Cache-Aside、Read/Write-Through 和 Write-Back 这三种主流策略。


准备工作:环境与模型

为了让代码示例更贴近真实场景,我们先定义一个基础模型和环境。

技术栈:

  • Spring Boot 3.x
  • Spring Data Redis
  • MyBatis-Plus (或 JPA)
  • MySQL

数据模型 (User.java):

import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import java.io.Serializable;@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {private static final long serialVersionUID = 1L;private Long id;private String username;private String email;
}

数据访问层 (UserMapper.java) (MyBatis-Plus 接口):

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface UserMapper extends BaseMapper<User> {
}

策略一:Cache-Aside (旁路缓存)

这是最经典、最常用,也是最容易理解的缓存策略。它的核心思想是:应用程序代码直接负责维护缓存和数据库

1. 概念与工作流程

读操作流程:

  1. 应用程序先从缓存中读取数据。
  2. 如果缓存命中(Cache Hit),则直接返回数据。
  3. 如果缓存未命中(Cache Miss),则从数据库中读取数据。
  4. 将从数据库中读到的数据写入缓存
  5. 返回数据给调用方。

写操作流程 (关键点):

  1. 先更新数据库
  2. 再删除(失效)缓存

为什么是“删除缓存”而不是“更新缓存”?

  • 懒加载思想:只有在下次真实需要读取该数据时,才通过“读操作流程”将其加载到缓存。如果每次更新都去刷新缓存,而这个数据后续又很少被读取,就会造成不必要的缓存写操作。
  • 并发安全:考虑一个场景(写-写并发),如果线程A更新数据库后更新缓存,同时线程B也更新数据库并更新缓存。可能发生B先完成,A后完成,导致缓存中是A的旧数据,而数据库是B的新数据,造成不一致。而“删除缓存”能极大地降低这种不一致的概率。
2. 代码示例 (UserServiceImpl.java)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;import java.util.concurrent.TimeUnit;@Service
public class UserServiceImpl {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;private final ObjectMapper objectMapper = new ObjectMapper();private static final String CACHE_KEY_PREFIX = "user:";/*** 读取用户 - 实现Cache-Aside读策略*/public User getUserById(Long id) {String key = CACHE_KEY_PREFIX + id;// 1. 从缓存读取Object cachedUserObj = redisTemplate.opsForValue().get(key);if (cachedUserObj != null) {System.out.println("Cache Hit for user: " + id);return objectMapper.convertValue(cachedUserObj, User.class);}// 2. 缓存未命中,从数据库读取System.out.println("Cache Miss for user: " + id + ". Reading from DB.");User userFromDb = userMapper.selectById(id);// 3. 数据库存在数据,则写入缓存if (userFromDb != null) {redisTemplate.opsForValue().set(key, userFromDb, 60, TimeUnit.MINUTES); // 设置60分钟过期}return userFromDb;}/*** 更新用户 - 实现Cache-Aside写策略*/public void updateUser(User user) {if (user == null || user.getId() == null) {throw new IllegalArgumentException("User or user ID cannot be null.");}// 1. 先更新数据库userMapper.updateById(user);System.out.println("Updated user in DB: " + user.getId());// 2. 再删除缓存String key = CACHE_KEY_PREFIX + user.getId();redisTemplate.delete(key);System.out.println("Invalidated cache for user: " + user.getId());}
}
3. 优缺点与适用场景
  • 优点:

    • 逻辑简单,易于实现和理解。
    • 强一致性(在大多数场景下),因为写操作直接操作数据库,读操作在缓存失效后会从数据库加载最新数据。
    • 灵活性高,缓存和数据库的交互完全由应用层控制。
  • 缺点:

    • 代码耦合,业务代码中混入了大量缓存操作逻辑,不够优雅。
    • 首次读取延迟,对于冷数据(首次被访问的数据),会经历一次“缓存未命中 -> 读数据库 -> 写缓存”的完整过程,延迟较高。
    • 可能存在一致性问题:在“更新DB”和“删除缓存”这两个非原子操作之间,如果发生异常或高并发读写,可能导致缓存中的数据是旧的,而数据库是新的。这被称为“缓存-数据库双写不一致”,但通过“先更新DB,再删除缓存”已将风险降到最低。
  • 适用场景:

    • 绝大多数的读多写少的业务场景。
    • 对数据一致性有较高要求,但能容忍极短暂不一致的场景。
    • 这是大部分互联网应用的首选和默认策略
4. 常见陷阱与注意事项
  • 缓存穿透:查询一个数据库和缓存中都不存在的数据。这会导致每次请求都直接打到数据库,缓存形同虚设。
    • 解决方案:对查询结果为null的数据也进行缓存(缓存空对象),但设置一个较短的过期时间。
  • 缓存击穿:某个热点Key在缓存中过期失效的瞬间,大量并发请求同时涌入,直接打到数据库上。
    • 解决方案:使用互斥锁(如分布式锁),只允许一个线程去查询数据库并回写缓存,其他线程等待。
  • 缓存雪崩:大量的Key在同一时间集体过期,导致所有请求瞬间全部打到数据库。
    • 解决方案:在Key的过期时间上增加一个随机值,避免集体失效。

策略二:Read/Write-Through (读穿/写穿)

这种策略将缓存作为主要的数据存储。应用程序只与缓存交互,由缓存服务自身来负责与底层数据库的同步。

1. 概念与工作流程

Read-Through (读穿):

  1. 应用程序向缓存请求数据。
  2. 如果缓存命中,直接返回。
  3. 如果缓存未命中,由缓存服务自己负责从数据库加载数据。
  4. 缓存服务将数据加载到缓存中,并返回给应用程序。
    • 这个过程对应用程序是透明的。

Write-Through (写穿):

  1. 应用程序向缓存写入数据。
  2. 缓存服务首先更新缓存
  3. 然后缓存服务同步地将数据写入数据库
  4. 操作完成后,缓存服务向应用程序返回成功。
    • 这个过程保证了缓存和数据库的强一致性

关键区别:Cache-Aside是应用层维护,Read/Write-Through是缓存服务(或一个封装层)维护。

2. 代码示例 (使用 Spring Cache 注解)

Spring Cache 的 @Cacheable, @CachePut, @CacheEvict 注解是 Read/Write-Through 和 Cache-Aside 写策略思想的完美体现。它将缓存逻辑从业务代码中解耦,使得代码更简洁。

配置 (CacheConfig.java):

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;@Configuration
@EnableCaching
public class CacheConfig {@Beanpublic RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(60)) // 默认缓存60分钟.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())).disableCachingNullValues(); // 不缓存null值return RedisCacheManager.builder(connectionFactory).cacheDefaults(config).build();}
}

重构后的 Service (UserServiceWithCacheAnnotations.java):

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;@Service
public class UserServiceImplWithAnnotations {@Autowiredprivate UserMapper userMapper;/*** @Cacheable 实现了 Read-Through 思想* - `value` 或 `cacheNames`: 指定缓存的名称(命名空间)* - `key`: 缓存的key,这里使用SpEL表达式取方法参数id* - `unless`: 结果为null时不缓存,防止缓存穿透*/@Cacheable(cacheNames = "user", key = "#id", unless = "#result == null")public User getUserById(Long id) {System.out.println("Reading from DB for user: " + id);return userMapper.selectById(id);}/*** @CacheEvict 实现了 Cache-Aside 的写策略(删除缓存)* - `key`: 指定要删除的缓存key*/@CacheEvict(cacheNames = "user", key = "#user.id")public void updateUser(User user) {System.out.println("Updating user in DB: " + user.getId());userMapper.updateById(user);System.out.println("Cache evicted for user: " + user.getId());}// 如果需要Write-Through(每次都更新缓存),可以使用@CachePut// @CachePut(cacheNames = "user", key = "#user.id")// public User updateUserAndCache(User user) {//     userMapper.updateById(user);//     return user; // @CachePut 要求方法必须有返回值,返回值会被放入缓存// }
}
3. 优缺点与适用场景
  • 优点:

    • 代码简洁,业务逻辑与缓存逻辑分离,可维护性高。
    • 强一致性(对于Write-Through),因为写操作是原子的(从应用角度看)。
    • 对应用透明,开发者无需关心底层细节。
  • 缺点:

    • 灵活性较低,缓存的读写行为由框架或缓存服务固定,不易定制。
    • 写操作延迟增加(对于Write-Through),因为需要同步写入数据库。
  • 适用场景:

    • 对代码整洁度要求高的项目。
    • 需要强一致性且能接受写操作延迟的场景。
    • 在Java生态中,使用Spring Cache进行常规业务对象缓存是此模式的最佳实践。

策略三:Write-Back (写回)

这是一种以性能为先的策略,追求极致的写性能,但牺牲了一定的数据一致性和可靠性。

1. 概念与工作流程

写操作流程:

  1. 应用程序将数据只写入缓存,并立即返回。
  2. 缓存服务将此数据标记为“脏数据”(Dirty)。
  3. 一个独立的异步任务会批量地、或延迟地将这些“脏数据”刷回(flush)到数据库中。

读操作流程:

  • 与 Read-Through 类似。如果缓存命中(无论是干净数据还是脏数据),直接返回。如果未命中,从数据库加载。
2. 代码示例(概念性实现)

原生 Redis 和 Spring Boot 不直接提供 Write-Back 机制,需要自己实现或借助第三方框架。下面是一个简化的概念性实现,用 BlockingQueueExecutorService 模拟异步写回。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;@Service
public class UserWriteBackService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;private static final String CACHE_KEY_PREFIX = "user:";// 使用阻塞队列作为缓冲区private final BlockingQueue<User> dirtyQueue = new LinkedBlockingQueue<>(10000);// 使用单线程的Executor来顺序处理写回任务private final ExecutorService writerExecutor = Executors.newSingleThreadExecutor();// 初始化时启动异步写回任务@PostConstructpublic void init() {writerExecutor.submit(() -> {while (!Thread.currentThread().isInterrupted()) {try {// 每隔5秒或缓冲区达到100条时,批量写回数据库List<User> userBatch = new ArrayList<>();// 从队列中取出最多100个元素,最多等待5秒Queues.drain(dirtyQueue, userBatch, 100, 5, TimeUnit.SECONDS);if (!userBatch.isEmpty()) {System.out.println("Writing back batch of size: " + userBatch.size());// 在实际应用中,这里应该是批量更新操作for (User user : userBatch) {userMapper.updateById(user);}}} catch (InterruptedException e) {Thread.currentThread().interrupt(); // 恢复中断状态System.err.println("Write-back thread interrupted.");} catch (Exception e) {// 必须处理异常,否则线程可能终止System.err.println("Error during write-back: " + e.getMessage());}}});}// 更新操作:只写缓存,并放入脏数据队列public void updateUser(User user) {// 1. 更新缓存redisTemplate.opsForValue().set(CACHE_KEY_PREFIX + user.getId(), user);// 2. 放入异步写回队列// 注意:为避免重复放入,可以先从队列中移除旧的相同ID的项dirtyQueue.removeIf(u -> u.getId().equals(user.getId()));boolean offered = dirtyQueue.offer(user);    if(!offered){System.err.println("Write-back queue is full. Data for user " + user.getId() + " might be lost!");// 可以在此添加降级策略,例如同步写入}}public User getUserById(Long id) {// 读操作逻辑与Cache-Aside或Read-Through类似Object user = redisTemplate.opsForValue().get(CACHE_KEY_PREFIX + id);if (user != null) {return (User) user;}return userMapper.selectById(id); // 此处简化,未回写缓存}// 关闭服务时,确保缓冲区数据被处理@PreDestroypublic void shutdown() {writerExecutor.shutdown();try {if (!writerExecutor.awaitTermination(60, TimeUnit.SECONDS)) {writerExecutor.shutdownNow();}} catch (InterruptedException e) {writerExecutor.shutdownNow();}// 处理队列中剩余的数据...}
}
3. 优缺点与适用场景
  • 优点:

    • 极高的写性能,因为应用“写”操作的耗时仅仅是写入内存(Redis)的时间,响应极快。
    • 降低数据库压力,通过批量异步写入,大大减少了对数据库的写请求次数。
  • 缺点:

    • 数据丢失风险:如果 Redis 服务宕机,且缓冲区中的“脏数据”还未写回数据库,这部分数据将永久丢失。
    • 数据一致性差:是“最终一致性”,在数据写回数据库之前,缓存和数据库的数据是不同的。
    • 实现复杂度高:需要自己实现异步队列、批量写入、失败重试、服务关闭时的数据处理等机制,非常复杂。
  • 适用场景:

    • 写密集型应用,例如:高频次的用户行为记录、点赞数、文章浏览量计数等。
    • 对数据丢失有一定容忍度的业务。比如,丢失几秒内的点赞数或浏览量通常是可以接受的。
    • 绝对不能用于金融、交易等对数据可靠性和一致性要求极高的场景。

总结与策略选择

特性Cache-Aside (旁路缓存)Read/Write-Through (读写穿)Write-Back (写回)
实现复杂度中等 (业务代码侵入) (框架支持,如Spring Cache) (需自行实现异步逻辑)
数据一致性准实时一致性强一致性 (Write-Through)最终一致性
数据可靠性最高低 (有数据丢失风险)
读性能高 (命中时)高 (命中时)高 (命中时)
写性能中等 (DB + Cache)慢 (同步写DB+Cache)极高 (只写内存)
适用场景通用,读多写少,互联网首选代码简洁性要求高,通用业务写密集型,对性能要求极致,能容忍数据丢失

进阶建议与最佳实践:

  1. 从 Cache-Aside 开始:对于绝大多数项目,Cache-Aside 是最稳妥、最灵活的起点。
  2. 拥抱 Spring Cache:在 Spring 生态中,优先使用 @Cacheable@CacheEvict 等注解来实践 Read-Through 和 Cache-Aside 的思想,能极大简化代码,提高开发效率。
  3. 谨慎使用 Write-Back:只有在写性能成为明确瓶颈,且业务能容忍其数据丢失风险时,才考虑自行实现或引入支持 Write-Back 的缓存组件。
  4. 一致性是关键挑战:深入理解“先更新DB,再删除缓存”策略,并了解其在极端并发下的风险。对于要求更强一致性的场景,可以研究基于消息队列(如Canal+RocketMQ/Kafka)的**订阅数据库变更日志(Binlog)**来异步更新缓存的方案,这是目前业界解决该问题的主流高级方案。
  5. 监控不可或缺:无论使用哪种策略,都必须对缓存的命中率、内存使用率、响应时间等关键指标进行全面监控,这是优化和排查问题的基础。

文章转载自:

http://izXjnneI.bfsqz.cn
http://NCraDhvl.bfsqz.cn
http://E6oHLNdW.bfsqz.cn
http://uzHy4wKA.bfsqz.cn
http://Lr5oT3f9.bfsqz.cn
http://nKjn5k5R.bfsqz.cn
http://JH4rXEX0.bfsqz.cn
http://jvmlkCAl.bfsqz.cn
http://uKEWvf2j.bfsqz.cn
http://Gqzbk9cH.bfsqz.cn
http://T1xY1LlE.bfsqz.cn
http://6CjIRWwv.bfsqz.cn
http://rHediT85.bfsqz.cn
http://BexJI257.bfsqz.cn
http://quKlsw1m.bfsqz.cn
http://iVbGWN5y.bfsqz.cn
http://RzzZbxLP.bfsqz.cn
http://biVjqXWc.bfsqz.cn
http://oGP8aLqv.bfsqz.cn
http://XjFEsaQ3.bfsqz.cn
http://LE0sJLYP.bfsqz.cn
http://VlxyQ4RN.bfsqz.cn
http://VKAEjRnM.bfsqz.cn
http://DLtwMCBQ.bfsqz.cn
http://NPIuJPzE.bfsqz.cn
http://WO9a0j7T.bfsqz.cn
http://L45voOHy.bfsqz.cn
http://axbbVNbo.bfsqz.cn
http://EwOyNbZI.bfsqz.cn
http://MlgmWFe8.bfsqz.cn
http://www.dtcms.com/a/370025.html

相关文章:

  • 安装MATLAB205软件记录
  • Day28 打卡
  • 【FastDDS】XTypes Extensions
  • 软考 系统架构设计师系列知识点之杂项集萃(142)
  • 【音视频】H264编码参数优化和cbr、vbr、crf模式设置
  • 软考 系统架构设计师系列知识点之杂项集萃(141)
  • 竞价代运营:百度竞价账户托管优化
  • Python实战:爬取百度热搜榜,制作动态可视化报告
  • Windows 设备音频录制 | WASAPI 音频数据采集 / 环回录制
  • uniapp新增页面及跳转配置方法
  • 西门子S7-200 SMART PLC:编写最基础的“起保停”程序
  • UDP-Server(2)词典功能
  • 最大似然估计:损失函数的底层数学原理
  • 今日分享:C++ -- list 容器
  • 报错:OverflowError: Python integer 4294967296 out of bounds for uint32
  • 贪心算法应用:蛋白质折叠问题详解
  • AI-调查研究-71-具身智能 案例分析:从ROS到Tesla Optimus的开源与商业化实践
  • 【嵌入式C语言】七
  • [数据结构] LinkedList
  • 【C++】引用的本质与高效应用
  • Date、BigDecimal类型值转换
  • 基于Node.js和Three.js的3D模型网页预览器
  • Scikit-learn Python机器学习 - 特征降维 压缩数据 - 特征提取 - 主成分分析 (PCA)
  • CSP-J/S IS COMING
  • GraphQL API 性能优化实战:在线编程作业平台指南
  • 【基础-判断】Background状态在UIAbility实例销毁时触发,可以在onDestroy()回调中进行系统资源的释放、数据的保存等操作。
  • PageHelper的使用及底层原理
  • 探寻卓越:高级RAG技术、架构与实践深度解析
  • 【51单片机】【protues仿真】基于51单片机PM2.5空气质量检测系统
  • AI工具深度测评与选型指南 - 图像生成与编辑类