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

Redis学习笔记 ----- 缓存

一、什么是缓存

缓存(Cache)是数据交换的缓冲区,是存储数据的临时地方,一般读写性能较高。

(一)缓存的作用

  • 降低后端负载:减少对数据库等后端存储的直接访问压力。
  • 提高读写效率,降低响应时间:利用缓存的高性能,快速响应数据请求。

(二)缓存的成本

  • 数据一致性成本:缓存与后端数据可能存在不一致,维护一致性需要额外处理。
  • 代码维护成本:引入缓存后,代码逻辑会更复杂,增加维护难度。
  • 运维成本:缓存系统的部署、监控、扩容等都需要投入运维资源。

二、缓存更新策略

策略说明一致性维护成本
内存淘汰利用Redis的内存淘汰机制,内存不足时自动淘汰部分数据,下次查询时更新缓存
超时剔除给缓存数据添加TTL时间,到期后自动删除缓存,下次查询时更新缓存一般
主动更新编写业务逻辑,在修改数据库的同时更新缓存

业务场景

  • 低一致性需求:使用内存淘汰机制,例如店铺类型的查询缓存。
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案,例如店铺详情查询的缓存。

代码实现:主动更新策略

@Override
@Transactional
public Result updateShop(Shop shop) {Long id = shop.getId();if (id == null) {return Result.fail("商铺ID不能为空");}// 1.先更新数据库updateById(shop);// 2.再删除缓存(保证数据一致性)stringRedisTemplate.delete(CACHE_SHOP_KEY + shop.getId());return Result.ok();
}

三、缓存更新策略的最佳实践方案

(一)低一致性需求

使用Redis自带的内存淘汰机制。

(二)高一致性需求

主动更新,并以超时剔除作为兜底方案。

  • 读操作
    • 缓存命中则直接返回。
    • 缓存未命中则查询数据库,并写入缓存,设定超时时间。
  • 写操作
    • 先写数据库,然后再删除缓存。
    • 要确保数据库与缓存操作的原子性。

代码实现:基础缓存读写逻辑

private Result queryShopById1(Long id) {// 1.查询RedisString cacheShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);// 2.缓存命中直接返回if (StrUtil.isNotBlank(cacheShop)) {Shop shop = JSONUtil.toBean(cacheShop, Shop.class);return Result.ok(shop);}// 3.缓存未命中查询数据库Shop shop = getById(id);if (shop == null) {return Result.fail("商铺不存在!");}// 4.数据库查询结果写入Redis,设置超时时间stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);return Result.ok(shop);
}

四、缓存问题及解决方案

(一)缓存穿透

问题定义

客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

问题场景
  1. 恶意攻击:黑客故意构造大量不存在的ID(如负数、超范围的随机数)发起请求,试图耗尽数据库资源
  2. 业务误操作:前端表单未做校验,用户输入了无效ID导致大量无效查询
  3. 数据已删除:商品已下架或用户已注销,但仍有请求访问这些已不存在的数据
  4. 爬虫抓取:爬虫程序遍历不存在的URL路径,导致大量无效查询
解决方案
1. 缓存空对象

原理:当数据库查询结果为空时,仍然将这个空结果缓存起来(可以是空字符串、null或特定标识),并设置较短的过期时间,避免同一无效请求反复穿透到数据库。
在这里插入图片描述

实现代码

private Result queryShopById2(Long id) {// 1.查询RedisString cacheShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);// 2.缓存命中(包括空值)if (StrUtil.isNotBlank(cacheShop)) {// 2.1 空值判断if (cacheShop.isEmpty()) {return Result.fail("商铺不存在!");}// 2.2 正常数据返回Shop shop = JSONUtil.toBean(cacheShop, Shop.class);return Result.ok(shop);}// 3.数据库查询Shop shop = getById(id);if (shop == null) {// 3.1 数据库不存在则缓存空值(短期有效)stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return Result.fail("商铺不存在!");}// 4.正常数据写入缓存stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);return Result.ok(shop);
}

优缺点

  • 优点:实现简单,维护方便,能有效拦截重复的无效请求
  • 缺点:
    • 占用额外内存空间存储空值
    • 可能造成短期数据不一致(如刚删除的数据又被创建)
    • 对于海量不同的无效ID,仍可能消耗大量缓存空间
2. 布隆过滤器

原理:在缓存之前增加一层布隆过滤器,预先将数据库中所有存在的Key存入布隆过滤器。当请求进来时,先通过布隆过滤器判断Key是否可能存在:

  • 若不存在,则直接返回,无需访问缓存和数据库
  • 若可能存在,再走正常的缓存+数据库查询流程

实现思路

  1. 系统初始化时,将数据库中所有有效ID加载到布隆过滤器
  2. 接收请求时,先通过布隆过滤器验证ID有效性
  3. 对布隆过滤器判断不存在的ID,直接返回错误

优缺点

  • 优点:内存占用少(相比缓存空对象),处理海量无效ID效率高
  • 缺点:
    • 实现复杂,需要维护布隆过滤器
    • 存在误判可能(不能准确判断元素是否存在)
    • 需要定期更新布隆过滤器数据,以反映数据库变化

(二)缓存雪崩

问题定义

在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力,甚至引起数据库宕机。

问题场景
  1. 集中过期:系统上线时为一批热点数据设置了相同的过期时间(如24小时),导致24小时后这批数据同时失效
  2. Redis宕机:Redis服务器因硬件故障、网络问题或内存溢出等原因突然宕机,整个缓存层失效
  3. 批量更新:电商平台在大促前批量更新商品信息,导致大量缓存被删除
  4. 缓存穿透引发:大量穿透请求导致数据库压力过大,进而影响缓存服务的正常运行
解决方案
1. 过期时间随机化

原理:为不同的缓存Key设置基础过期时间的同时,增加一个随机偏移量(如±10%),避免大量Key在同一时间点同时过期。

实现代码

// 设置过期时间时增加随机值
int baseTtl = 30; // 基础30分钟
int random = new Random().nextInt(10); // 0-9分钟随机
stringRedisTemplate.opsForValue().set(key, value, baseTtl + random, TimeUnit.MINUTES
);
2. Redis集群

原理:通过部署Redis集群(主从+哨兵或Redis Cluster)提高缓存服务的可用性,避免单点故障导致整个缓存层失效。

关键措施

  • 主从复制:实现数据备份和读写分离
  • 哨兵机制:自动监控和故障转移
  • 集群分片:分散数据存储,提高整体容量和性能
3. 降级限流

原理:当缓存服务出现异常时,通过降级策略限制对数据库的请求流量,保护数据库不被压垮。

实现方式

  • 使用熔断器(如Sentinel、Hystrix)监控缓存服务状态
  • 当缓存服务异常时,返回默认数据或提示信息,而非直接查询数据库
  • 对查询数据库的请求设置限流阈值,超出阈值的请求直接拒绝
4. 多级缓存

原理:构建多级缓存架构(如本地缓存+分布式缓存),即使分布式缓存失效,本地缓存仍能提供部分缓冲能力。

常见架构

  1. 本地缓存(Caffeine、Guava):存在于应用进程内,速度最快
  2. 分布式缓存(Redis):供多个应用实例共享
  3. 数据库缓存:数据库自身的缓存机制

(三)缓存击穿

问题定义

也叫热点Key问题,指一个被高并发访问的热点数据的缓存突然失效,无数请求会在瞬间同时到达数据库,给数据库带来巨大冲击。

问题场景
  1. 热门商品:电商平台的爆款商品缓存过期,恰逢促销活动期间,大量用户同时访问
  2. 热点事件:突发新闻事件的相关数据缓存过期,引发大量用户同时查询
  3. 排行榜:热门游戏排行榜数据缓存过期,大量玩家同时刷新页面
  4. 秒杀活动:秒杀商品的缓存过期,大量用户在同一时间点抢购
解决方案
1. 互斥锁

原理:当缓存失效时,不是让所有请求都去查询数据库,而是通过锁机制保证只有一个请求能去查询数据库并重建缓存,其他请求则等待缓存重建完成后再从缓存获取数据。
在这里插入图片描述

实现代码

private Result queryShopById3(Long id) {// 1.查询RedisString cacheShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);if (StrUtil.isNotBlank(cacheShop)) {if (cacheShop.isEmpty()) {return Result.fail("商铺不存在!");}return Result.ok(JSONUtil.toBean(cacheShop, Shop.class));}Shop shop = null;try {// 2.获取互斥锁boolean isLock = tryLock(LOCK_SHOP_KEY + id);if (!isLock) {// 2.1 获取锁失败则重试(短暂等待后再次查询)Thread.sleep(50);return queryShopById3(id);}// 3.获取锁成功,查询数据库shop = getById(id);// 模拟缓存重建耗时操作Thread.sleep(200);if (shop == null) {stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return Result.fail("商铺不存在!");}// 4.写入缓存stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {// 5.释放锁unlock(LOCK_SHOP_KEY + id);}return Result.ok(shop);
}// 获取锁(使用Redis的setIfAbsent实现)
private boolean tryLock(String key) {return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 100, TimeUnit.SECONDS));
}// 释放锁
private void unlock(String key) {stringRedisTemplate.delete(key);
}

优缺点

  • 优点:
    • 保证数据一致性(每次都是最新数据)
    • 无额外内存消耗
    • 实现相对简单
  • 缺点:
    • 锁竞争会导致请求等待,影响接口响应性能
    • 可能存在死锁风险(需设置合理的锁过期时间)
    • 高并发下,第一个获取锁的请求可能成为性能瓶颈
2. 逻辑过期

原理:给缓存数据设置一个逻辑过期时间(而非Redis的实际过期时间),缓存永不过期。当查询时发现数据已过逻辑过期时间,不直接删除缓存,而是返回旧数据并异步更新缓存,保证后续请求能尽快获取到新数据。
在这里插入图片描述

实现代码

// 线程池(用于异步重建缓存)
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);// 逻辑过期数据封装类
@Data
public class RedisData {// 逻辑过期时间private LocalDateTime expireTime;// 实际存储的数据(如Shop对象)private Object data;
}private Result queryShopById4(Long id) {// 1.查询RedisString cacheShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);if (StrUtil.isBlank(cacheShop)) {return Result.fail("商铺不存在!");}if (cacheShop.isEmpty()) {return Result.fail("商铺不存在!");}// 2.解析缓存数据(带逻辑过期时间)RedisData redisData = JSONUtil.toBean(cacheShop, RedisData.class);JSONObject data = (JSONObject) redisData.getData();Shop shop = JSONUtil.toBean(data, Shop.class);LocalDateTime expireTime = redisData.getExpireTime();// 3.判断是否逻辑过期if (expireTime.isAfter(LocalDateTime.now())) {// 3.1 未过期直接返回return Result.ok(shop);}// 3.2 已过期,尝试获取锁重建缓存boolean isLock = tryLock(LOCK_SHOP_KEY + id);if (isLock) {// 3.2.1 获取锁成功,异步重建缓存(不阻塞当前请求)CACHE_REBUILD_EXECUTOR.submit(() -> {try {saveShop2Redis(id, CACHE_SHOP_TTL);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {unlock(LOCK_SHOP_KEY + id);}});}// 3.2.2 无论是否获取锁,都返回旧数据(保证用户体验)return Result.ok(shop);
}// 封装逻辑过期数据并写入Redis
public void saveShop2Redis(long id, Long seconds) throws InterruptedException {Shop shop = getById(id);// 模拟缓存重建延时Thread.sleep(200);RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(seconds));stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

优缺点

  • 优点:
    • 无请求阻塞,性能好(始终返回数据,不等待)
    • 避免大量请求同时冲击数据库
  • 缺点:
    • 数据可能短期不一致(返回旧数据)
    • 需要额外存储过期时间,占用更多内存
    • 实现相对复杂,需要处理异步更新逻辑

五、缓存常量定义

public class RedisConstants {// 商铺缓存键前缀public static final String CACHE_SHOP_KEY = "cache:shop:";// 商铺缓存默认过期时间(30分钟)public static final Long CACHE_SHOP_TTL = 30L;// 空值缓存过期时间(2分钟,用于解决缓存穿透)public static final Long CACHE_NULL_TTL = 2L;// 商铺缓存锁键前缀(用于解决缓存击穿)public static final String LOCK_SHOP_KEY = "lock:shop:";// 锁过期时间(10秒)public static final Long LOCK_SHOP_TTL = 10L;
}

六、缓存方案对比

方案解决问题优点缺点适用场景
基础缓存实现简单存在缓存穿透、击穿问题低并发、非核心业务
缓存空值缓存穿透防止数据库压力过大占用额外缓存空间无效请求相对集中的场景
布隆过滤缓存穿透内存占用少有误判、实现复杂海量无效ID场景
互斥锁缓存击穿数据一致性好可能阻塞请求一致性要求高的热点数据
逻辑过期缓存击穿无阻塞,性能好可能返回旧数据高并发、一致性要求不高
随机TTL缓存雪崩实现简单只能解决集中过期问题所有需要设置过期的场景
多级缓存缓存雪崩提高可用性和性能实现复杂、维护成本高核心业务、高可用要求

参考来源

本文内容基于黑马程序员《Redis入门到实战教程》相关章节学习整理,部分代码示例与知识点解析参考了该课程的讲解。

http://www.dtcms.com/a/347513.html

相关文章:

  • 寻鲜之旅“咖”约深圳,容声冰箱引领“养鲜”新体验
  • 解决coze api使用coze.workflows.runs.create运行workflow返回400,但text为空
  • ⚡ Ranger 基础命令与功能详解
  • Talkie AI
  • 硬件笔记(27)---- 恒流源电路原理
  • 环境 (shell) 变量
  • QT-Mysql-查询语句-查询是否有表-表列名-查询记录
  • 力扣hot100:搜索二维矩阵与在排序数组中查找元素的第一个和最后一个位置(74,34)
  • ros 消息类型与查阅相关内容
  • XCVM1802-2MSEVSVA2197 XilinxAMD Versal Premium FPGA
  • 同步和异步、阻塞和非阻塞的再理解
  • JAVA核心基础篇-集合
  • 力扣(组合)
  • 如何解决 pyqt5 程序“长时间运行失效” 问题?
  • React学习(十一)
  • Windows 平台查看端口占用情况并终止进程
  • flink常见问题之非法配置异常
  • leetcode 852 山脉数组的顶峰索引
  • 讲点芯片验证中的统计覆盖率
  • 【URP】[平面阴影]原理与实现
  • 如何使用和优化SQL Server存储过程:全面指南
  • 论文阅读:arxiv 2025 Can You Trick the Grader? Adversarial Persuasion of LLM Judges
  • 【数据分享】地级市对外开放程度(2002-2021)-有缺失值
  • SpringBoot自动装配原理深度解析
  • 【LeetCode 热题 100】300. 最长递增子序列——(解法一)记忆化搜索
  • mmap映射物理内存之四内核cache同步
  • 后台管理系统-14-vue3之tag标签页的实现
  • JEI(Journal of Electronic lmaging)SCI四区期刊
  • TypeScript的接口 (Interfaces)讲解
  • Python 版本与 package 版本兼容性检查方法