文章目录
- 什么是缓存
- 添加Redis缓存
- 缓存更新策略
- 缓存穿透
- 缓存空对象
- 布隆过滤器
- 缓存雪崩
- 给不同的key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
- 缓存击穿
- 互斥锁
- 逻辑过期
- 缓存工具封装
- 方法1 写入redis
- 方法2 设置逻辑过期
- 方法3 解决缓存穿透
- 方法4 解决缓存击穿
什么是缓存
缓存就是数据交换的缓冲区( Cache ), 是存储数据的临时地方, 一般读写性能较高
缓存的作用:
- 降低后端负载
- 提高读写效率, 降低响应时间
缓存的成本:
- 数据一致性成本
- 代码维护成本
- 运维成本
添加Redis缓存
一图胜过千言万语~
/*** 根据id查询商铺信息* @param id 商铺id* @return 商铺详情数据*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {return Result.ok(shopService.queryById(id));
}@Override
public Object queryById(Long id) {String shopKey = CACHE_SHOP_KEY + id;// 从redis查询商店缓存String shopJson = stringRedisTemplate.opsForValue().get(shopKey);// 判断缓存是否存在if(StrUtil.isNotBlank(shopJson)){// 缓存存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}// 缓存不存在,查询数据库Shop shop = getById(id);// 店铺不存在, 返回错误if(shop == null){return Result.fail("商铺不存在");}// 店铺存在, 写入redusstringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop));// 返回return Result.ok(shop);
}
- 从redis中查询是否存在商铺缓存
stringRedisTemplate.opsForValue().get(shopKey)
- 缓存存在, 将数据转换成Java对象并返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class)
- 缓存不存在, 先查库看商铺是否存在
Shop shop = getById(id)
- 商铺不存在, 直接返回错误提示
return Result.fail("商铺不存在")
- 商铺存在, 写入redis再返回商铺信息
stringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop))
return Result.ok(shop)
缓存更新策略
业务场景:
- 低一致性要求: 使用内存淘汰机制. 例如店铺类型的查询缓存
- 高一致性要求: 主动更新, 并以超时剔除作为兜底方案. 例如店铺详情查询的缓存
- 读操作:
- 缓存命中则直接返回
- 缓存未命中则查数据库, 并写入缓存, 设定超时时间
- 写操作:
- 先写数据库, 然后再删除缓存
- 要确保数据库与缓存操作的原子性
更新商铺信息
/*** 更新商铺信息* @param shop 商铺数据* @return 无*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {// 写入数据库return shopService.updateShop(shop);
}@Override
@Transactional
public Result updateShop(Shop shop) {Long id = shop.getId();if(id == null){return Result.fail("商铺ID不能为空");}// 更新数据库updateById(shop);// 删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY + id);return Result.ok();
}
- 判断商铺ID是否为空, 防止NPE
- 更新数据库, 直接调用MP的方法, 最后在删除缓存
- 添加
@Transactional
注解, 保持事务一致性, 防止缓存与数据库不一致
缓存穿透
缓存穿透:
指客户端请求的数据在缓存中和数据库都不存在,
这样缓存永远不会生效, 这些请求都会发送到数据库
解决方案:
缓存空对象
- 优点: 实现简单 维护方便
- 缺点: 额外内存消耗 可能造成短期数据不一致
布隆过滤器
- 优点: 内存占用较少 没有多余key
- 缺点: 实现复杂 存在误判可能
@Override
public Object queryById(Long id) {String shopKey = CACHE_SHOP_KEY + id;// 从redis查询商店缓存String shopJson = stringRedisTemplate.opsForValue().get(shopKey);// 判断缓存是否存在if(StrUtil.isNotBlank(shopJson)){// 缓存存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}// 判断命中的是否是空值
*** if(shopJson != null){ <***为多加的代码,防止缓存穿透>
*** // 命中空值,直接返回
*** return Result.fail("商铺不存在");
*** }// 缓存不存在,查询数据库Shop shop = getById(id);// 店铺不存在, 返回错误if(shop == null){// 将空值写入redis
*** stringRedisTemplate.opsForValue().set(shopKey, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return Result.fail("商铺不存在");}// 店铺存在, 写入redusstringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);// 返回return Result.ok(shop);
}
缓存雪崩
缓存雪崩:
指同一时段大量缓存的key同时失效或者Redis服务器宕机
导致大量请求到达数据库, 带来巨大请求压力
解决方案:
给不同的key的TTL添加随机值
利用Redis集群提高服务的可用性
给缓存业务添加降级限流策略
给业务添加多级缓存
缓存击穿
缓存击穿:
缓存击穿问题也叫热点Key问题, 一个被高并发访问并且缓存重建业务较复杂的key突然失效
无数的请求访问会在瞬间给数据库带来巨大的冲击
解决方案:
互斥锁
- 优点:
- 没有额外的内存消耗
- 保证一致性
- 实现简单
- 缺点:
- 线程需要等待 性能受到影响
- 可能有死锁的风险
逻辑过期
- 优点:
- 现成无需等待
- 性能较好
- 缺点:
- 不保证一致性
- 有额外的内存消耗
- 实现复杂
互斥锁解决缓存击穿
互斥锁解决缓存击穿
// 缓存击穿
public Shop queryWithMutex(Long id){String shopKey = CACHE_SHOP_KEY + id;// 从redis查询商店缓存String shopJson = stringRedisTemplate.opsForValue().get(shopKey);// 判断缓存是否存在if(StrUtil.isNotBlank(shopJson)){// 缓存存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}// 判断命中的是否是空值if(shopJson != null){// 命中空值,直接返回return null;}// 实现缓存重建// 1. 获取互斥锁String lockKey = "lock:shop:" + id;Shop shop = null;try{boolean isLock = tryLock(lockKey);// 2. 判断是否获取成功if(!isLock){// 3. 失败 则休眠并重试Thread.sleep(50);return queryWithMutex(id);}// 4. 成功 根据id查询数据库shop = getById(id);// 模拟重建延时Thread.sleep(1000);// 店铺不存在, 返回错误if(shop == null){// 将空值写入redisstringRedisTemplate.opsForValue().set(shopKey, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 店铺存在, 写入redusstringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);}catch(InterruptedException e){throw new RuntimeException(e);}finally{// 释放互斥锁unLock(lockKey);}// 返回return shop;
}// 获取锁
private boolean tryLock(String key){Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);
}
// 释放锁
private void unLock(String key){stringRedisTemplate.delete(key);
}
- 判断缓存是否存在, 存在直接返回, 不存在先判断是否为空值, 空值返回null, 不是空值则继续
- 实现缓存重建, 尝试拿到锁, 失败则休眠一段时间之后再次尝试拿锁~
- 成功拿到锁之后查询数据库, 判断数据是否存在, 不存在则写入空字符串到redis, 防止缓存穿透
- 数据存在则将数据写入redis, 下次查询直接命中缓存, 无需抢锁再查库, 最后缓存重建成功, 释放锁
使用逻辑过期解决缓存击穿问题
使用逻辑过期解决缓存击穿问题
// 数据预热 / 缓存重建
public void saveShopRedis(Long id, Long expireSeconds){// 1. 查询店铺数据Shop shop = getById(id);// 2. 封装逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));// 3. 写入RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}// 独立线程重建缓存(线程池)
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
// 逻辑过期解决缓存击穿
public Shop queryWithLogicalExpire(Long id){String shopKey = CACHE_SHOP_KEY + id;// 1. 从redis查询商店缓存String shopJson = stringRedisTemplate.opsForValue().get(shopKey);// 2. 判断缓存是否存在if(StrUtil.isBlank(shopJson)){// 3. 缓存不存在,直接返回return null;}// 4. 命中, 需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();// 5. 判断是否过期if(expireTime.isAfter(LocalDateTime.now())){// 5.1 未过期, 直接返回return shop;}// 5.2 过期, 重建缓存// 6. 缓存重建// 6.1 获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2 判断是否获取成功if(isLock){// 6.3 成功, 开启独立线程, 实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 重建缓存this.saveShopRedis(id, 20L);} catch (Exception e) {throw new RuntimeException(e);} finally {// 释放互斥锁unLock(lockKey);}});}// 6.4 返回过期的商铺信息return shop;
}
- 判断redis是否存在缓存数据
stringRedisTemplate.opsForValue().get(shopKey)
- 判断缓存是否存在
if(StrUtil.isBlank(shopJson))
- 缓存不存在, 返回
null
- 缓存存在, 将缓存反序列化为Java对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class)
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class)
LocalDateTime expireTime = redisData.getExpireTime()
-> 逻辑过期时间
- 判断逻辑时间是否过期
if(expireTime.isAfter(LocalDateTime.now()))
- 未过期直接返回缓存信息
return shop
- 过期则进行缓存重建, 要获取锁,
boolean isLock = tryLock(lockKey)
- 抢锁成功则进行缓存重建
Shop shop = getById(id)
RedisData redisData = new RedisData()
String cacheKey = CACHE_SHOP_KEY + id
redisData.setData(shop)
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds))
stringRedisTemplate.opsForValue().set(cacheKey, JSONUtil.toJsonStr(redisData))
- 缓存重建成功之后释放锁,
unLock()
, 最后返回信息return shop
缓存工具封装
缓存工具封装
方法1 写入redis
// 写入缓存
public void set(String key, Object value, Long time, TimeUnit unit){stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
方法2 设置逻辑过期
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){// 设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));// 写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
- 拿到缓存的key, 缓存的数据value, 缓存时间time, 缓存时间单位unit
- 分别设置数据和过期时间, 过期时间是以当前时间加上缓存时间time
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)))
- 最后写入Redis, 缓存数据转换成String类型
方法3 解决缓存穿透
// 解决缓存穿透
public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){String key = keyPrefix + id;// 从redis查询商店缓存String json = stringRedisTemplate.opsForValue().get(key);// 判断缓存是否存在if(StrUtil.isNotBlank(json)){// 缓存存在,直接返回return JSONUtil.toBean(json, type);}// 判断命中的是否是空值if(json != null){// 命中空值,直接返回return null;}// 缓存不存在,查询数据库R r = dbFallback.apply(id);// 店铺不存在, 返回错误if(r == null){// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 店铺存在, 写入redusthis.set(key, r, time, unit);// 返回return r;
}
方法4 解决缓存击穿
// 解决缓存击穿
// 独立线程重建缓存(线程池)
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
// 逻辑过期解决缓存击穿
public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){String key = keyPrefix + id;// 1. 从redis查询商店缓存String json = stringRedisTemplate.opsForValue().get(key);// 2. 判断缓存是否存在if(StrUtil.isBlank(json)){// 3. 缓存存在,直接返回return null;}// 4. 命中, 需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();// 5. 判断是否过期if(expireTime.isAfter(LocalDateTime.now())){// 5.1 未过期, 直接返回return r;}// 5.2 过期, 重建缓存// 6. 缓存重建// 6.1 获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2 判断是否获取成功if(isLock){// 6.3 成功, 开启独立线程, 实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 查询数据库R r1 = dbFallback.apply(id);// 写入Redisthis.setWithLogicalExpire(key, r1, time, unit);} catch (Exception e) {throw new RuntimeException(e);} finally {// 释放互斥锁unLock(lockKey);}});}// 6.4 返回过期的商铺信息return r;
}
// 获取锁
private boolean tryLock(String key){Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);
}
// 释放锁
private void unLock(String key){stringRedisTemplate.delete(key);
}