Redis缓存查询

Redis结构:String

缓存更新策略

内存淘汰、超时剔除、主动更新

缓存与数据库双写一致

1、如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间

2、修改信息时,先修改数据库,再删除缓存

一个原因两个优点:

原因:若先删缓存再写数据库,在这两个操作之间可能:又来了一个读请求触发 “缓存未命中(此时缓存被删除) → 查数据库(此时数据库更新操作还未完成) → 写入旧数据到缓存” 的流程,导致脏数据。 优点:而先写数据库后删缓存,即使中间有读请求,也会读取到旧缓存数据,但后续缓存被删除,下次查询会重新获取最新数据; 其次,数据库的操作时间远比缓存要慢,先删掉缓存在去处理数据库的话,这个时间间隔更大,更有可能导致脏数据。

解决缓存穿透

用户访问了一个不存在的资源,且进行大量访问。此时数据库中没有,redis中自然也没有缓存相应的资源,因此大量请求打到数据库中,给数据库带来巨大压力。

// 解决方式一:缓存空对象(即用户访问没有的资源时,将空对象也缓存到redis,下一次请求时就可以直接返回redis中的空结果)
public Shop queryWithPassThrough(Long id) {
        //从redis中查询商铺缓存
        String key = "cache:shop:" + id;
        String s = stringRedisTemplate.opsForValue().get(key);
​
        //判断是否存在
        //命中,直接返回
        if (!StrUtil.isBlank(s)) {
            Shop shop = JSONUtil.toBean(s, Shop.class);
            return shop;
        }
​
        //如果redis存入则上面已经返回,
        //此时只存在两种情况:null(没有查询过,数据库中可能有); ‘’(已经查询过了,数据库中没有)
        // 如果已经查询过数据库
        if (s != null) {
            return null;
        }
​
        //否则没有查询过数据库, redis中不存在为null, 则根据id查询数据库
        Shop shop = getById(id);
        //数据库不存在,返回错误
        if (shop == null) {
            // 解决缓存穿透 写入空值到redis
            stringRedisTemplate.opsForValue().set("cache:shop:" + id, "", 5, TimeUnit.MINUTES);
            return null;
        }
        //存在,写入redis
        stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES);
        //返回
        return shop;
    }
​
​
//改进:如进行广泛使用,可对上述方法进行封装
 public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit) {
       // 从redis中取出
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
       // 存在则直接返回数据
        if (StrUtil.isNotBlank(json)) {
            return JSONUtil.toBean(json, type);
        }
​
        //如果redis存入则上面已经返回,
        //此时只存在两种情况:null(没有查询过,数据库中可能有); ‘’(已经查询过了,数据库中没有)
        // 如果已经查询过数据库
        if (json != null) {
            return null;
        }
​
        //否则没有查询过数据库, redis中不存在为null, 则根据id查询数据库
        R r = dbFallback.apply(id);
     
        //数据库不存在
        if (r == null) {
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", 10L, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 存在,写入redis
        this.set(key, r, time, unit);
        return r;
    }
​
// 将缓存对象到redis
public void set(String key, Object value, Long time, TimeUnit unit) {
    stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(value));
}
​
// 封装后可直接调用
        Shop shop = cacheClient.queryWithPassThrough("cache:shop:", id, Shop.class, this::getById, 10L, TimeUnit.MINUTES);
​
//解决方法二:布隆算法进行过滤

 

缓存雪崩

同一时间段大量缓存的key同时失效,或者Redis服务宕机,导致大量请求直接到达数据库给数据库带来巨大压力

解决:

1、给不同的Key的TTL添加随机值

2、利用Redis集群提高服务的可用性

3、给缓存业务添加降级限流策略

4、给业务添加多级缓存

 

缓存击穿

热点key问题,就是一个被高并发访问并且缓存重建业务比较复杂的key突然失效,无数的请求访问会给数据库带来巨大压力

解决方法一:互斥锁(在缓存未命中查询数据库时,只有拿到唯一锁的线程才能查询数据库对缓存进行重建,其它线程休眠等待一段时间后重试查询能否命中缓存)。此方法数据一致性高,性能慢,有死锁风险

解决方法二:逻辑过期(存入Redis缓存时数据过期时间为永不过期,在缓存的数据类型上增加一个逻辑过期时间,每次将数据返回前,需要判断逻辑过期时间是否已到。到了则开一个新线程重新查询数据库进行缓存重建,并将旧数据直接返回)。此方法数据一致性低,性能高,无死锁风险

// 逻辑过期解决缓存击穿
public Shop queryWithLogicalExpire(Long id) {
        //从redis中查询商铺缓存
        String key = "cache:shop:" + id;
        String s = stringRedisTemplate.opsForValue().get(key);
​
       //判断是否存在 Redis缓存中为null,直接返回null,因为热点key是会先做数据预热的,如果redis中没有则表名该key不是热点key
        if (StrUtil.isBlank(s)) {
            return null;
        }
​
        // 存在,则判断数据是否过期
        // 命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(s, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
    
        if (expireTime.isAfter(LocalDateTime.now())) {
            //未过期,直接返回店铺信息
            return shop;
        }
​
        //已过期则把旧数据直接返回,交给另一个线程完成最新数据的更新
        //缓存重建
        String lockKey = "lock:shop:" + id;
        //获取互斥锁
        boolean islock = tryLock(lockKey);
        //是否获取锁成功
        //获取锁成功
        if (islock) {
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                //开启独立线程,实现缓存重建
                try {
                    this.saveShopToRedis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    unLock(lockKey);
                }
            });
        }
        //返回过期的商品信息
        return shop;
    }
​
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
​
private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(flag);
    }
​
private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }
​
​

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值