
一、认识缓存缓存是数据交换的缓冲区Cache是存储数据的临时地方一般读写性能较高降低后端负载提高读写效率降低响应时间需要实现数据一致性二、 缓存更新策略三种更新策略内存淘汰Redis的内存淘汰机制默认存在超时剔除给数据添加TTL时间到期后自动删除缓存主动更新编写业务逻辑在修改数据库的同时更新缓存Cache Aside模式低一致性需求使用内存淘汰机制高一致性需求主动更新并以超时剔除作为兜底方案最佳实践方案读操作缓存命中直接返回未命中则查询数据库并写入缓存设定超时时间写操作先写数据库 再删除缓存(Cache Aside 思路)确保数据库于缓存操作的原子性更新操作案例流程PUT请求更新数据库信息核心先update数据库再删除Redis缓存代码Override public Result update(Shop shop){ Long id shop.getId(); if(id null){ return Result.fail(id不能为空); } // 1. 先操作数据库 updateById(shop); // 2. 再删除缓存 stringRedisTemplate.delete(CACHE_SHOP_KEY id); return Result.ok(); }根据id查询案例流程优先查询 Redis缓存命中就直接返回缓存未命中查询数据库并把数据库查询结果写入 Redispublic Result queryById(Long id) { String key CACHE_SHOP_KEY id; // 1.从 redis中获取信息 String shopJson stringRedisTemplate.opsForValue().get(key); // 2.redis中存在获取成功返回信息 if(StrUtil.isNotBlank(shopJson)){ Shop shop JSONUtil.toBean(shopJson,Shop.class); return Result.ok(shop); } // 3.redis中不存在查询数据库 Shop shop getById(id); // 4.数据库中不存在返回false if(shop null){ return Result.fail(店铺不存在); } // 5.数据库中存在将信息添加到 redis中返回信息给前端 stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop)); // 6. 返回信息 return Result.ok(shop); }三、缓存穿透什么是缓存穿透客户端请求一个在缓存和数据库中都不存在的资源缓存永远不会命中这些请求都会打到数据库给数据库带来巨大的压力解决方案2.1 布隆过滤器内存占用少没有多余Key实现复杂存在误判可能2.2 缓存空对象不是null实现简单维护方便额外的内存消耗可能造成短期的不一致设置空对象的TTL缓存空对象实践案例在原有的根据id查询案例基础上,添加对缓存穿透的解决方案Redis命中判断是否为空对象是空对象则返回给前端信息不再查找数据库// 解决缓存穿透判断 shopJson 是否为为空对象 if(shopJson ! null){ return Result.fail(店铺信息不存在); }数据库未命中将空对象写入Redis中TTL比一般的数据设置的更短// 4.数据库中不存在返回false if(shop null){ // 解决缓存穿透将空对象写入Redis stringRedisTemplate.opsForValue().set(key,,CACHE_NULL_TTL,TimeUnit.MINUTES); return Result.fail(店铺不存在); }null 不是 null代表缓存没有数据需要去数据库中查代表为空对象四、缓存击穿什么是缓存击穿也叫热点Key问题就是一个被高并发并且缓存重建业务较复杂的key突然失效了无数请求会在瞬间给数据库带来巨大压力解决方案互斥锁没有额外的内存消耗保证一致性实现简单线程需要等待性能受影响可能有死锁风险逻辑过期线程无需等待性能较好不保证一致性有额外内存消耗实现复杂互斥锁解决缓存击穿案例3.1 过程请求查询缓存热点Key缓存未命中需要查询数据库在查询数据库之前需要获取互斥锁只有一个请求线程可以获取锁从而进入数据库进行查询和缓存重建其他请求线程获取锁失败就会线程等待然后重新获取查询缓存热点 Key 失效后只允许一个线程查询数据库并重建缓存其他线程等待后重试避免大量请求同时打到数据库3.2 通过 SETNX 实现互斥锁代码public boolean tryLock(String key){ Boolean flag stringRedisTemplate.opsForValue() .setIfAbsent(key, 1, 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); }锁必须设置 TTL避免线程异常退出后产生永久死锁flag是有可能为null可能出现空指针异常所以使用BooleanUtil.isTrue(flag)做拆箱public void unlock(String key){ stringRedisTemplate.delete(key); }核心 APIsetIfAbsent()对应 RedisSETNX含义只有 Key 不存在时才能写入成功3.3 核心代码查询Redis未命中尝试获取锁获取失败则进入休眠休眠结束重新查询Redis// 3.redis中不存在尝试获取锁 boolean isLock tryLock(lockKey); // 3.1 获取失败,则进入休眠,休眠结束回头重新执行 if (!isLock) { Thread.sleep(50); return queryWithMutex(id);注意休眠结束不是继续接着执行当前函数然后重新调用当前函数如果没有获得锁就需要线程休眠try { ... Thread.sleep(50); ... } catch (InterruptedException e) { throw new RuntimeException(e); } finally { unlock(lockKey); }Thread.sleep需要用try-catch包围查询数据库和写缓存应放在try中释放锁放在finally中finally确保拿到锁的线程保证无论业务成功还是异常都一定会释放锁避免死锁只有真正获得锁的线程才能执行unlock()获取锁失败的线程不能进入释放锁的 finally正常执行完/中途return/发生异常 都会 → finally执行3.4 完整代码实现// 缓存击穿(互斥锁) public Shop queryWithMutex(Long id){ String key CACHE_SHOP_KEY id; // 1.从 redis中获取信息 String shopJson stringRedisTemplate.opsForValue().get(key); // 2.redis中存在获取成功返回信息 if (StrUtil.isNotBlank(shopJson)) { Shop shop JSONUtil.toBean(shopJson, Shop.class); return shop; } // 解决缓存穿透判断 shopJson 是否为为空对象 if (shopJson ! null) { return null; } String lockKey lock:shop id; Shop shop null; try { // 3.redis中不存在尝试获取锁 boolean isLock tryLock(lockKey); // 3.1 获取失败,则进入休眠,休眠结束回头重新执行 if (!isLock) { Thread.sleep(50); return queryWithMutex(id); } } catch (InterruptedException e) { throw new RuntimeException(e); } try { // 3.2 获取成功查询数据库 shop getById(id); // 4.数据库中不存在返回false if (shop null) { // 解决缓存穿透将空对象写入Redis stringRedisTemplate.opsForValue() .set(key, , CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } // 5.数据库中存在将信息添加到 redis中返回信息给前端 stringRedisTemplate.opsForValue() .set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); } finally { // 6.释放锁 unlock(lockKey); } // 7.返回信息 return shop; }逻辑过期方式解决缓存击穿问题4.1 介绍对每一份数据都加上一个逻辑过期属性expireTime让过期数据仍然保留在 Redis 中请求发现数据逻辑过期后尝试获取锁获取成功则异步重建缓存获取失败说明已有线程正在重建此时直接返回旧数据从而避免大量线程等待和数据库瞬时压力不依赖 Redis 的物理 TTL 删除热点数据否则 Redis Key 真过期没了失去了“返回旧数据”的意义特点数据不物理删除Redis 中保留旧数据通过expireTime判断是否逻辑过期。互斥重建逻辑过期后通过 Redis 锁保证只有一个线程负责缓存重建避免缓存击穿。异步更新获取锁的线程把重建任务提交线程池当前请求和其他请求都直接返回旧数据实现低延迟和高可用。逻辑过期方案通常依赖热点数据提前缓存 / 预热到 Redis所以完全查不到时当前代码直接返回不去访问数据库。4.2 给每个数据对象添加到期时间Data public class RedisData { private LocalDateTime expireTime; //到期时间 private Object data; //数据信息 }在Redis中存储的是不data而是携带到期时间的RedisData4.3 查询Redis数据取出业务数据和逻辑时间// RedisData RedisData redisData JSONUtil.toBean(shopJson, RedisData.class); // 实际数据对象 Shop shop JSONUtil.toBean( (JSONObject) redisData.getData(), Shop.class ); // 到期时间 LocalDateTime expireTime redisData.getExpireTime();4.4 判断是否逻辑过期如果没有过期直接返回缓存if (expireTime.isAfter(LocalDateTime.now())) { return shop; }如果数据过期抢锁String lockKey LOCK_SHOP_KEY id; boolean isLock tryLock(lockKey);4.5 抢锁成功开启独立线程线程池实现private static final ExecutorService CACHE_REBUILD_EXECUTOR Executors.newFixedThreadPool(10);此时不是当前请求线程进行缓存重建而是开启独立线程进行缓存重建当前请求线程还是会返回旧缓存即缓存重建不阻塞当前用户请求if(isLock){ CACHE_REBUILD_EXECUTOR.submit(() - { //重建缓存 try { saveShop2Redis(id,20L); } catch (Exception e) { throw new RuntimeException(e); } finally { //释放锁 unlock(lockKey); } }); }saveShop2Redis主要实现查询数据库封装到期时间保存到Redis中public void saveShop2Redis(Long id,Long expireSeconds){ // 1.查询店铺数据 Shop shop getById(id); // 2.封装逻辑过期时间 RedisData redisData new RedisData(); redisData.setData(shop); redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds)); // 3.写入Redis不添加TTL stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY id,JSONUtil.toJsonStr(redisData)); }4.6 抢锁失败直接返回就缓存return shop;完整代码//创建线程池 private static final ExecutorService CACHE_REBUILD_EXECUTOR Executors.newFixedThreadPool(10); // 缓存击穿逻辑过期 public Shop queryWithLogicExpire(Long id){ String key CACHE_SHOP_KEY id; // 1.从 redis中获取信息 String shopJson stringRedisTemplate.opsForValue().get(key); // 2.redis未命中直接返回未找到 if(StrUtil.isBlank(shopJson)){ return null; } // 3.命中将 json 反序列化为对象 RedisData redisData JSONUtil.toBean(shopJson,RedisData.class); Shop shop JSONUtil.toBean((JSONObject) redisData.getData(),Shop.class); LocalDateTime expireTime redisData.getExpireTime(); // 4.判断是否逻辑过期 if(expireTime.isAfter(LocalDateTime.now())){ // 4.1 未过期直接返回缓存数据 return shop; } // 4.2 过期尝试获取锁 String lockKey LOCK_SHOP_KEY id; boolean isLock tryLock(lockKey); // 5.获取锁成功开启独立线程实现缓存重建 if(isLock){ CACHE_REBUILD_EXECUTOR.submit(() - { //重建缓存 try { saveShop2Redis(id,20L); } catch (Exception e) { throw new RuntimeException(e); } finally { //释放锁 unlock(lockKey); } }); } // 6.获取锁失败返回缓存数据 return shop; } // 缓存重建 public void saveShop2Redis(Long id,Long expireSeconds){ // 1.查询店铺数据 Shop shop getById(id); // 2.封装逻辑过期时间 RedisData redisData new RedisData(); redisData.setData(shop); redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds)); // 3.写入Redis不添加TTL stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY id,JSONUtil.toJsonStr(redisData)); }五、缓存雪崩同一时间大量的缓存Key同时失效TTL到期或者Redis服务宕机导致大量请求到达数据库带来巨大压力方案给不同的Key的TTL添加随机值利用Redis集群提高服务的可用性给缓存业务添加降级限流策略给业务添加多级缓存六、缓存工具工具封装在以上的案例中都是以商户信息为例子请求查询Shop对象返回Shop对象但是实际应用中数据类型是不确定的返回值类型id类型在数据类型不确定的情况下要利用泛型由调用者告诉我们真实的类型是什么牵扯到数据库查询时需要由调用者告诉我们数据库的查询函数要传入函数比如getById()缓存穿透方案泛型public R,ID R queryWithPassThrough( String keyPrefix, ID id, ClassR type,FunctionID,R dbFallback,Long time,TimeUnit unit){ String key keyPrefix id; // 1.从 redis中获取信息 String json stringRedisTemplate.opsForValue().get(key); // 2.redis中存在获取成功返回信息 if(StrUtil.isNotBlank(json)){ log.info(缓存中); return JSONUtil.toBean(json,type); } // 解决缓存穿透判断 json 是否为为空对象 if(json ! null){ return null; } // 3.redis中不存在查询数据库 R r dbFallback.apply(id); // 4.数据库中不存在返回false if(r null){ // 解决缓存穿透将空对象写入Redis this.set(key,,time,unit); return null; } // 5.数据库中存在将信息添加到 redis中返回信息给前端 this.set(key,r,time,unit); // 6. 返回信息 return r; }逻辑过期解决缓存击穿泛型//创建线程池 private static final ExecutorService CACHE_REBUILD_EXECUTOR Executors.newFixedThreadPool(10); // 缓存击穿逻辑过期 public R,ID R queryWithLogicExpire( String keyPrefix,ID id,ClassR type,FunctionID,R dbFallback,Long time,TimeUnit unit){ String key keyPrefix id; // 1.从 redis中获取信息 String json stringRedisTemplate.opsForValue().get(key); // 2.redis未命中直接返回未找到 if(StrUtil.isBlank(json)){ return null; } // 3.命中将 json 反序列化为对象 RedisData redisData JSONUtil.toBean(json,RedisData.class); R r JSONUtil.toBean((JSONObject) redisData.getData(),type); LocalDateTime expireTime redisData.getExpireTime(); // 4.判断是否逻辑过期 if(expireTime.isAfter(LocalDateTime.now())){ // 4.1 未过期直接返回缓存数据 return r; } // 4.2 过期尝试获取锁 String lockKey LOCK_SHOP_KEY id; boolean isLock tryLock(lockKey); // 5.获取锁成功开启独立线程实现缓存重建 if(isLock){ CACHE_REBUILD_EXECUTOR.submit(() - { //重建缓存 try { R r1 dbFallback.apply(id); this.setWithLogicExpire(key,r1,time,unit); } catch (Exception e) { throw new RuntimeException(e); } finally { //释放锁 unlock(lockKey); } }); } // 6.获取锁失败返回缓存数据 return r; }数据封装// Java对象存储到Redis中 public void set(String key, Object value, Long time, TimeUnit unit){ stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit); } // Java对象添加逻辑过期存储到Redis中 public void setWithLogicExpire(String key,Object value,Long time,TimeUnit unit){ // 设置逻辑过期 RedisData redisData new RedisData(); redisData.setData(value); redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time))); // 存入Redis stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData)); }七、常用API总结数据转换工具转换方向API当前使用场景Java → JSONJSONUtil.toJsonStr(obj)Java对象写RedisJSON → JavaJSONUtil.toBean(json, clazz)Redis读取业务对象Bean → BeanBeanUtil.copyProperties()User → UserDTOBean → MapBeanUtil.beanToMap()DTO → Redis HashMap → BeanBeanUtil.fillBeanWithMap()Redis Hash → DTO判断空字符串StrUtil.isBlank()判断Redis是否无有效数据判断非空StrUtil.isNotBlank()判断缓存是否命中Boolean安全判断BooleanUtil.isTrue()SETNX结果判断时间单位转换unit.toSeconds(time)逻辑过期执行传入函数dbFallback.apply(id)调用数据库查询Redis API数据类型Spring API常用方法作用StringopsForValue()get(key)获取值StringopsForValue()set(key,value)保存值StringopsForValue()set(key,value,time,unit)保存并设置TTLStringopsForValue()setIfAbsent()SETNX常用于锁StringopsForValue()increment()自增HashopsForHash()put()保存一个字段HashopsForHash()putAll()保存整个MapHashopsForHash()get()获取一个字段HashopsForHash()entries()获取整个HashHashopsForHash()delete()删除Hash字段ListopsForList()leftPush()左侧插入ListopsForList()rightPush()右侧插入ListopsForList()leftPop()左侧弹出ListopsForList()rightPop()右侧弹出ListopsForList()range()查询指定范围SetopsForSet()add()添加元素SetopsForSet()members()查询所有成员SetopsForSet()isMember()判断成员是否存在SetopsForSet()remove()删除元素SetopsForSet()intersect()求交集ZSetopsForZSet()add()添加元素和scoreZSetopsForZSet()incrementScore()修改分数ZSetopsForZSet()range()升序查询ZSetopsForZSet()reverseRange()降序查询