Redis实战——商户查询缓存

目录

缓存简单介绍

Redis缓存的大致实现图 

缓存更新策略

三种方案 

三个问题 

先操作缓存 还是 先操作数据库?

先删除缓存再操作数据库

正常情况:

异常情况: 

 先操作数据库,再删除缓存

正常情况:

异常情况:

总结

缓存的三个问题

缓存穿透

缓存雪崩​

缓存击穿​


缓存简单介绍

Redis实战——商户查询缓存_第1张图片

Redis实战——商户查询缓存_第2张图片


Redis缓存的大致实现图 

Redis实战——商户查询缓存_第3张图片

缓存更新策略

解决:当数据库中的数据进行了更改,Redis缓存并不知晓,在缓存中查询到仍然是旧数据

Redis实战——商户查询缓存_第4张图片

三种方案 

Redis实战——商户查询缓存_第5张图片

三个问题 

Redis实战——商户查询缓存_第6张图片

先操作缓存 还是 先操作数据库?

示例:

缓存与数据库事先存入的初始值都为10

Redis实战——商户查询缓存_第7张图片

先删除缓存再操作数据库

正常情况:

       线程1删除缓存,然后更新数据库 v = 20;

       线程2查询缓存未命中,而后查询数据库 v,并写入缓存

 Redis实战——商户查询缓存_第8张图片

异常情况: 

        当线程1删除缓存后,由于更新数据库的耗时较长,线程2插入,执行查询缓存,未命中查询数据库v = 10,并写入缓存;线程1而后更新数据库 v = 20,这导致缓存中数据变成了旧数据,缓存中的数据和数据库中的数据不一致

 Redis实战——商户查询缓存_第9张图片

 先操作数据库,再删除缓存

正常情况:

        线程2更新数据库 v= 20,再删除缓存;

        线程1查询缓存未命中,查询数据库 v = 20,并写入缓存

Redis实战——商户查询缓存_第10张图片

异常情况:

        线程1查询的时候,缓存恰好失效,查询缓存未命中,而后查询数据库,被线程2插入,执行更新数据库,删除缓存操作,线程1再写入缓存,这就导致线程1写入缓存的数据为旧数据

Redis实战——商户查询缓存_第11张图片

总结

方案二更好,方案二发生的情况要求  有线程查询的时候,缓存恰好失效;发生的概率相较于方案一的异常状况更加小

Redis实战——商户查询缓存_第12张图片

缓存的三个问题

缓存穿透

解决方法:(1)缓存空对象:当缓存未命中,数据库也未命中时,缓存null,而后每次查询

                  (2)布隆过滤:

Redis实战——商户查询缓存_第13张图片

修改流程,缓存空对象

Redis实战——商户查询缓存_第14张图片

实现

    public Shop queryWithPassThrough(Long id) {
        String key=CACHE_SHOP_KEY+id;
        String JsonShop = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(JsonShop)){//null,"","\n"都属于false
            Shop shop = JSONUtil.toBean(JsonShop, Shop.class);
            return shop;
        }

        //判断命中的是否为空值
        if(JsonShop!=null){
            return null;
        }

        Shop shop = getById(id);
        if(shop==null){
            //缓存空对象
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;
    }


缓存雪崩Redis实战——商户查询缓存_第15张图片


缓存击穿Redis实战——商户查询缓存_第16张图片

利用互斥锁与逻辑过期(旧的key仍存在)解决缓存击穿

Redis实战——商户查询缓存_第17张图片

互斥锁与逻辑过期的优缺点

Redis实战——商户查询缓存_第18张图片

        简单实现互斥锁:利用Redis中的SETNX命令实现互斥锁的效果,释放锁就相当于删除key(一般设置有效期)

Redis实战——商户查询缓存_第19张图片

Redis实战——商户查询缓存_第20张图片

获取锁与释放锁

(不直接return flag的原因:防止在拆箱装箱过程中出现空指针,所以使用hutool中的工具类)

    private boolean trylock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

具体实现

public Shop queryWithMuteX(Long id) {
        String key=CACHE_SHOP_KEY+id;

        // redis查看缓存
        String JsonShop = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(JsonShop)){//null,"","\n"都属于false
            Shop shop = JSONUtil.toBean(JsonShop, Shop.class);
            return shop;
        }

        //判断命中的是否为空值
        if(JsonShop!=null){
            return null;
        }
        //实现缓存重建
        //1.尝试获取互斥锁

        String lockKey="lock:shop"+id;
        Shop shop = null;
        try {
            boolean isLock = trylock(lockKey);
            //2.判断是否获取成功
            if(!isLock){
                //失败,休眠并重试
                Thread.sleep(50);
                return queryWithMuteX(id);
            }
            //成功,根据id查询数据库
            shop = getById(id);

            //模拟重建的延时
            Thread.sleep(200);

            if(shop==null){
                //缓存空对象
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }

            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //3.释放互斥锁
            unlock(lockKey);
        }

        return shop;
    }

 


Redis实战——商户查询缓存_第21张图片

不需对原来的shop进行字段的修改,重新写一个类,使其进行继承或设置

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

声明线程池 

链接:java并发编程:Executor、Executors、ExecutorService

//线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

实现:

@Override
    public Shop queryWithLogicalExpire(Long id) {
        String key=CACHE_SHOP_KEY+id;

        // redis查看缓存
        String JsonShop = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isBlank(JsonShop)){
            //不存在直接返回null
            return null;
        }
        //命中,需要json反序列化为对象
        RedisData redisData = JSONUtil.toBean(JsonShop, RedisData.class);
//        Object data = redisData.getData(); //本质是JSONObject

        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();

        //判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //未过期,直接返回店铺信息
            return shop;
        }

        //过期,需要缓存重建

        //1.尝试获取互斥锁
        String lockKey=LOCK_SHOP_KEY+id;
        boolean islock = trylock(lockKey);
        //2.判断是否成功
        if(islock){
            //成功开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //重建缓存
                    this.saveShop2Redis(id,20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }

        //返回过期的商铺信息
        return shop;
    }


优化:对上述查询缓存进行封装

Redis实战——商户查询缓存_第22张图片

 实现:(采用泛型与函数式编程)

@Slf4j
@Component
public class CacheClient {
    //线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private boolean trylock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }

    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)));

        //写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    public  R queryWithPassThrough(
            String keyPrefix, ID id, Class type, Function dbFallback, Long time, TimeUnit unit) {
        String key=keyPrefix+id;
        String Json = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(Json)){//null,"","\n"都属于false
            return JSONUtil.toBean(Json, type);
        }

        //判断命中的是否为空值
        if(Json!=null){
            return null;
        }

        R r = dbFallback.apply(id);
        if(r==null){
            //缓存空对象
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        this.set(key,r,time,unit);
        return r;
    }

    public  R queryWithLogicalExpire(String keyPrefix,ID id,Class type,Function dbFallback,Long time, TimeUnit unit) {
        String key=keyPrefix+id;

        // redis查看缓存
        String JsonShop = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isBlank(JsonShop)){
            //不存在直接返回null
            return null;
        }
        //命中,需要json反序列化为对象
        RedisData redisData = JSONUtil.toBean(JsonShop, RedisData.class);
//        Object data = redisData.getData(); //本质是JSONObject

        JSONObject data = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();

        //判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //未过期,直接返回店铺信息
            return r;
        }

        //过期,需要缓存重建

        //1.尝试获取互斥锁
        String lockKey=LOCK_SHOP_KEY+id;
        boolean islock = trylock(lockKey);
        //2.判断是否成功
        if(islock){
            //成功开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //查询数据库
                    R r1 = dbFallback.apply(id);

                    //重建缓存
                    this.setWithLogicalExpire(key,r1,time,unit);

                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }

        //返回过期的商铺信息
        return r;
    }
}

与之前的差异

    @Override
    @Transactional
    public Result queryById(Long id) {
        //缓存穿透
//        Shop shop = queryWithPassThrough(id);
//        Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, id2 -> getById(id2), CACHE_SHOP_TTL, TimeUnit.MINUTES);

        //互斥锁解决缓存击穿
//        Shop shop=queryWithMuteX(id);
        
        //利用逻辑过期解决缓存击穿
//        Shop shop=queryWithLogicalExpire(id);
        Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        if(shop==null){
            return Result.fail("店铺不存在!");
        }

        return Result.ok(shop);
    }

你可能感兴趣的:(redis)