目录
🍊 缓存查询策略
🍩 缓存更新策略
🍭 缓存穿透
🍣 缓存雪崩
🍕 缓存击穿
👾 项目源码下载
🍊 缓存查询策略
我们要查询的业务数据并不是经常改变的, 这里我们可以放到Redis缓存中, 降低对数据库的请求
下面我们以查询店铺为例, 因为店铺列表是不经常改变的数据, 所以我们可以请求redis缓存来降低MySQL的查询压力
@Override
public Result queryShopById(Long id) {
//1.从Redis中查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop" + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在, 直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//4.不存在, 根据id查询数据库
Shop shop = getById(id);
//不存在, 返回错误提示信息
if (shop == null){
return Result.fail("店铺不存在!");
}
//存在, 写入redis中
stringRedisTemplate.opsForValue().set("cache:shop" + id, JSONUtil.toJsonStr(shop));
return Result.ok(shop);
}
🍩 缓存更新策略
在常规的企业开发中,我们优先选择的缓存策略是 更新数据库的同时也会去更新缓存
在此情况下我们也要考虑三点 :
1. 更新数据库后再删除缓存, 再查询的时候重新添加缓存 (这样可以保证数据查询的是最新的)
2.在单体项目中, 将缓存与数据库操作放在同一个事务中, 这样方便回滚. 分布式项目中需要使用分布式事务
3. 在并发场景下, 应当先操作数据库,再删除缓存
@Override
@Transactional
public void updateShop(Shop shop) {
if (shop.getId() == null) {
throw new RuntimeException("ID不能为null");
}
//1. 先更新数据库
updateById(shop);
//2. 后删除缓存
stringRedisTemplate.delete("cache:shop" + shop.getId());
}
🍭 缓存穿透
缓存穿透场景 : 假设用户恶意请求的数据在Redis和MySQL中均不存在, 导致Redis中的缓存不生效从而一直去请求MySQL
解决方案 : 因为用户传来的恶意数据在缓存和数据库中都不存在, 在从数据库中查询不到后将恶意数据缓存在Redis中
代码实现如下, 当在数据库没有查询到后, 将空信("")息存入到Redis中,并设置过期时间为2分钟, 当用户再次查询时, 校验如果为("") 直接返回 店铺信息不存在!
@Override
public Result queryShopById(Long id) {
//1.从Redis中查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop" + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在, 直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//判断命中的是否是空值
if (Objects.equals(shopJson, "")) {
//返回错误信息
return Result.fail("店铺信息不存在!");
}
//4.不存在, 根据id查询数据库
Shop shop = getById(id);
//不存在, 返回错误提示信息
if (shop == null){
//将空值写入Redis中 并将有效期时间改为2分钟
stringRedisTemplate.opsForValue().set("cache:shop" + id, "", 2L, TimeUnit.MINUTES);
//返回错误信息
return Result.fail("店铺不存在!");
}
//存在, 写入redis中 设置过期时间为30分钟
stringRedisTemplate.opsForValue().set("cache:shop" + id, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
return Result.ok(shop);
}
🍣 缓存雪崩
Redis的缓存雪崩意思是指: 在统一同一时间内Redis中的大量的Key失效, 导致请求压力到达数据库
解决办法 : 缓存数据的过期时间设置随机,将不同的Key的TTL设置随机值
🍕 缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的ky突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
🍥 解决方案1 : 互斥锁
锁代码实现(获取锁, 释放锁)
/**
* 获取锁
*
* @param key
* @return
*/
private boolean tryLock(String key) {
//相当于 SETNX:添加一个String类型的键值对,当key不存在的时候执行
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// BooleanUtil可以帮你自动拆装箱解决可能空指针问题
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
*
* @param key
*/
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
业务代码实现
这里的互斥锁如果获取不到锁就会进入休眠状态, 然后再去重新获取锁, 这样做能保持数据的一致性
/**
* 缓存击穿
*/
public Shop queryWithmutex(Long id){
//1.从Redis中查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop" + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在, 直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
//判断命中的是否是空值
if (Objects.equals(shopJson, "")) {
//返回错误信息
log.error("店铺信息不存在");
return null;
}
//4实现缓存重建
//4.1 获取互斥锁
String lockKey = "lock:shop" + id;
Shop shop;
try {
//这块功能的业务是, 当A线程操作该方法是, B线程进来判断锁是否释放, 如果没有释放则休眠重试, 为了解决数据一致性的问题
boolean isLock = tryLock(lockKey);
//4.2判断锁是否获取成功
if (!isLock){
//4.3 失败,则休眠重试
Thread.sleep(50);
//递归重试(这块地方有异议 不建议递归, 后期用到类似业务可以寻找其他解决办法)
return queryWithmutex(id);
}
//根据id查询数据库
shop = getById(id);
if (shop == null) {
//不存在 将空值写入Redis中 并将有效期时间改为2分钟 (这里是为了解决缓存穿透问题)
stringRedisTemplate.opsForValue().set("cache:shop" + id, "", 2L, TimeUnit.MINUTES);
//返回错误信息
log.error("店铺信息不存在");
return null;
}
//存在, 写入redis中 设置过期时间为30分钟
stringRedisTemplate.opsForValue().set("cache:shop" + id, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
}catch (InterruptedException e){
throw new RuntimeException(e);
}finally {
//7. 释放互斥锁
unlock(lockKey);
}
return (shop);
}
解决方案2 : 逻辑过期
设置逻辑过期数据和过期时间
/**
* 向Redis中写入店铺信息并设置逻辑过期时间
* @param id
* @param expireSeconds
*/
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
stringRedisTemplate.opsForValue().set("cache:shop" + id, JSONUtil.toJsonStr(redisData));
}
手动加入逻辑过期数据
@Test
void test(){
//模拟后台管理手动设置热点数据
shopService.saveShop2Redis(1L, 10L);
}
逻辑过期业务代码
/**
* 逻辑过期
* @param id
* @return
*/
public Shop queryWithLogicalExpire(Long id) {
//1.从Redis中查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop" + id);
//2.判断是否存在
if (StrUtil.isBlank(shopJson)) {
//3.不存在, 直接返回
return null;
}
//4. 命中, 需要把JSON反序列化为对象
log.info("打桩数据 : {}", shopJson);
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" + id;
boolean isLock = tryLock(lockKey);
//6.2判断锁是否获取成功
if (isLock) {
//TODO 6.3成功, 开启独立线程, 实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//重建缓存
log.info("开始缓存重建");
this.saveShop2Redis(id, 20L);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
log.info("释放锁");
unlock(lockKey);
}
});
}
//6.4 返回过期的商铺信息
return shop;
}
这里实现的互斥锁, 如果没有拿到锁就会直接return返回历史数据, 在并发环境下短期内会造成数据的不一致性
比如我修改了name属性字段
👾 项目源码下载
扫描下方公众号二维码 回复: Redis缓存实战 即可领取项目源码 👇👇👇