黑马点评项目02——商户查询缓存(缓存穿透、缓存雪崩、缓存击穿)以及细节

在这里插入图片描述

1.添加redis缓存

在这里插入图片描述
StringRedisTemplate 使用的是这个哈,有人可能有疑问,存放的是字符串吗,商铺值应该是个对象才对啊,在细节中解析
代码:

@Override
public Result queryById(Long id) {
    //查询redis,若存在则转换成对象后返回
    String key = CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    if (StringUtils.isNotBlank(shopJson)) {
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.ok(shop);
     }
    //不存在则查询数据库,然后转成以json串存⼊redis后,返回
    Shop shop = shopMapper.selectById(id);
        if(shop==null){
    		return Result.fail("店铺不存在");
     }

    stringRedisTemplate.opsForValue()
    		.set(key,JSONUtil.toJsonStr(shop));
    return Result.ok(shop);
}

2.API 细节解析 json串与对象相互转换

stringRedisTemplate.opsForValue().get(key)的返回值是一个String,如果查不到,返回null,为了以防万一,HuTool工具判断 StringUtils.isNotBlank(shopJson),可以确保是确实是一个商铺。在这里插入图片描述
命中缓存,转换为将String json串转换为对象, Shop shop = JSONUtil.toBean(shopJson, Shop.class); 注意这个API,字符串转化为Shop;
不命中缓存,查数据库返回商铺Shop shop = shopMapper.selectById(id),
此时注意了,不能直接把对象放进去,要放进去一个json,也注意这个API。
stringRedisTemplate.opsForValue() .set(key,JSONUtil.toJsonStr(shop))

3.Redis缓存和数据库一致性策略

Cache Aside(旁路缓存)策略(适合读多写少)
在这里插入图片描述
注意:写的时候先更新数据库,这样也可能发生不一致问题,只是几率相对较小,一个解决策略就是加上延迟双删
在这里插入图片描述
在这里插入图片描述
另外,Cache Aside 策略适合读多写少的场景,不适合写多的场景,因为当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。

 //先更新数据库,再删除缓存
    shopMapper.updateById(shop);
    stringRedisTemplate.delete(CACHE_SHOP_KEY+ id);

4. 缓存穿透

缓存穿透 :是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:

4.1 缓存空对象

在原来的逻辑中,我们如果发现这个数据在mysql中不存在,直接就返回404了,这样是会存在缓存穿透问题的。现在,我如果查询到数据库没有这个对象时,我就往Redis存放(id:‘’)空字符串,下次你再访问,给你空字符串,根本过不了isBank()
在这里插入图片描述
缺点:
可能存在短时间不一致问题;占用内存
注意:缓存空值要设置较短的过期时间(如 5~10 分钟)

4.2 布隆过滤

直接拦截了,只要数据库中没有,当然可能会存在误判,不过概率较小!!!
4.3 其他方案
在这里插入图片描述

5. 缓存雪崩

缓存雪崩:是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。就是缓存Redis废了
在这里插入图片描述

6. 缓存击穿(例如:优惠劵信息id)

缓存击穿是指:某个热点 key访问频率极高)突然失效,大并发请求在同一时间全部打到数据库,短时间内数据库可能被压垮。

在这里插入图片描述

6.1 互斥锁

在这里插入图片描述

    /*模拟加锁*/
    private boolean tryLock(String key){
        Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(key, "", 30, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(b);
    }
 
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

stringRedisTemplate.opsForValue().setIfAbsent(key, "", 30, TimeUnit.SECONDS) 拿到了锁就返回true在这里插入图片描述
Boolean.TRUE.equals(success)或者 BooleanUtil.isTrue(success)来判断
互斥锁逻辑

 public Result queryById(Long id) {
        // 1. 从 Redis 查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

        // 2. 如果缓存命中,直接返回  必须有实际东西才可以
        if(StrUtil.isNotBlank(shopJson)){
            log.info("shopJson缓存中有:{}",shopJson);
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }

        // 3. 如果缓存中是空字符串或者占位符(说明数据库中查过确实不存在),返回错误
        if (shopJson != null) {
            return Result.fail("店铺信息不存在(缓存空值)");
        }

        // 4. 缓存未命中,准备查询数据库前先尝试加锁,防止缓存击穿
        String lockKey = "shop:lock" + id;
        boolean lock = tryLock(lockKey); // 尝试加锁 true 该线程拿到了锁
        Shop shop;

        try 
        {
            if (lock) 
            {
                // 5. 获取锁成功,查询数据库
                shop = getById(id);
                // 6. 数据库中也不存在,返回错误(此处未缓存空值,依赖布隆拦截)
                if (shop == null) {
                    // return 之前会进入finally
                    return Result.fail("店铺不存在");
                }
                // 7. 查询成功,写入缓存
                String jsonStr = JSONUtil.toJsonStr(shop);
                stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonStr, 3L, TimeUnit.MINUTES);
   
            } 
            else 
            {
                // 8. 获取锁失败,稍等后递归重试(等待其他线程完成缓存填充)
                Thread.sleep(50);
                // return 之前会进入finally
                return queryById(id);
            }
        } 
        catch (InterruptedException e) {
            throw new RuntimeException(e);
        } 
        finally
        {// 9. 只有拿到锁的线程才释放锁,避免误删其他线程的锁
            if (lock) {
                unlock(lockKey);
            }
        }

        // 10. 返回结果
        return Result.ok(shop);
    }

6.2 逻辑过期

既然是高并发访问那干脆就直接redis里面一直都不要删除了,再加个逻辑过期时间,过期的话就开个独立线程去更新数据写入redis,在没更新完之前访问到的都是redis里面的旧数据。
在这里插入图片描述
具体实现见:逻辑过期解决缓存击穿

我只是讲一下难以实现的技术点:
1、需要封装一个实体类+过期时间一起构成RedisData对象,有两种实现方式。
第一种:泛型

@Data
public class RedisData<T> {
    private LocalDateTime expireTime;
    private T data;
}

第二种:Object

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

第一种就是序列化麻烦一些,不过更规范,api记住

     // 1. 查询 Redis 缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(json)) {
            // 缓存未命中
            return null;
        }

        // 2. 反序列化为带逻辑过期的数据结构
        RedisData<?> redisData = JSONUtil.toBean(json, RedisData.class);

      /*  JSONObject dataJson = (JSONObject) redisData.getData(); // 先转 JSONObject
        T data = dataJson.toBean(type);*/
        T data = JSONUtil.toBean((JSONObject) redisData.getData(), type);


        LocalDateTime expireTime = redisData.getExpireTime();

接下来就是判断是否过期了,如果没有过期,直接返回data;如果过期了,尝试获取锁,注意有个细节,如果获取锁后一定要再判断一下是否从缓存中得到是否为空,为空,说明被删掉了,返回之前找的旧data,再判断这时候是不是不过期了,这样就少一次IO,不过期,说明有其他线程刚刚更新过了。
在这里插入图片描述
如果确实是过期,交给其他线程重建,

// 缓存重建线程池(用于异步更新缓存)
private static final ExecutorService CACHE_REBUILD_EXECUTOR =
        Executors.newFixedThreadPool(10);
  // 6. 异步线程池重建缓存
                CACHE_REBUILD_EXECUTOR.submit(() -> {
                    try {
                        // 查询数据库
                        T fresh = dbFallback.apply(id);
                        // 模拟重建缓存
                        Thread.sleep(200);
                        // 重新写入缓存(逻辑过期)
                        this.setWithLogicalExpire(key, fresh, time, unit);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        // 释放锁
                        unlock(lockKey);
                    }
                });
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值