Redis实战—黑马点评项目—商户查询缓存

Redis实战—黑马点评项目—商户查询缓存_第1张图片一、添加Redis缓存Redis实战—黑马点评项目—商户查询缓存_第2张图片

 查询商铺

@Service
public class ShopServiceImpl
        extends ServiceImpl implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result queryById(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;//查商铺用的key
        //1、从redis中查询商铺
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shopJson)) {
            //2、 存在 返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //3、不存在 从mysql查询商铺
        Shop shop = getById(id);
        //4、不存在 报错
        if(shop==null){
            return Result.fail("店铺不存在");
        }
        //5、 mysql中存在 在redis中写数据并返回
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));

        return Result.ok(shop);
    }
}

二、缓存更新策略

 (1)主要有以下三种

内存淘汰 超时剔除 主动更新
说明 利用Redis的内存淘汰机制,内存不足自动淘汰部分数据,下次查询时更新缓存 给缓存数据添加TTL时间,到期自动删除缓存,下次查询更新缓存 编写业务逻辑,在修改数据库的同时,更新缓存
一致性 一般
维护成本

(2)业务场景 :

低一致性需求:内存淘汰。如店铺类型的查询缓存。

高一致性需求:主动更新,并以超时剔除作为兜底。如店铺详情查询缓存

(3)主动更新的三种模式

Cache Aside Pattern:缓存调用者在更新数据库的同时更新缓存(较多使用)

Read/Write Through Pattern:缓存和数据库整合成一个服务,由服务来维护一致性。调用者无需关心一致性问题。

Write Behind Caching Pattern:调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致。

(4)Cache Aside的一些策略

●删除旧缓存而不是更新缓存

●保证数据库和缓存同时成功或失败的方法:对于单体架构,将二者置于同一事物下;对于分布式架构,利用TCC等分布式事务方案

●操作数据库和删除缓存的先后

Redis实战—黑马点评项目—商户查询缓存_第3张图片Redis实战—黑马点评项目—商户查询缓存_第4张图片

两种策略的对比。由于redis的读写速度相较于数据库的读写速度是更快的,所以第二种异常发生的概率比第一种更低。因此第二种方案是最佳选择:先操作数据库,再删缓存。

 三、修改店铺——缓存更新策略实现

基于二中的CacheAside策略,对ShopService进行改造同时编写修改店铺业务:

(1)读:根据id查店铺时,如果缓存未命中,则查询数据库,查到结果写入缓存,并设置超时剔除

(2)写:根据id修改店铺时,先修改数据库,再删除缓存

查询店铺

//5、 若mysql中存在 在redis中写数据(同时设置超时剔除)并返回
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

 修改店铺

    //修改店铺
    @Override
    @Transactional
    public Result update(Shop shop) {
        //根据id修改数据库

        updateById(shop);
        //根据token删除缓存
        Long id = shop.getId();
        if(id==null){
            return Result.fail("店铺id不能为空");
        }
        stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);
        return Result.ok();
    }

四、缓存穿透

 Redis实战—黑马点评项目—商户查询缓存_第5张图片

 【改造】Redis实战—黑马点评项目—商户查询缓存_第6张图片

 基于缓存null值的策略对店铺查询的代码进行改造

    //查店铺
    @Override
    public Result queryById(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;//查商铺用的key
        //1、从redis中查询商铺
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shopJson)) {
            //2、 存在 返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }//仅当不为空字符串 不为null时返回true
        //判断是否命中空值
        if(shopJson!=null){
            return Result.fail("店铺信息不存在");
        }
        //3、不存在 从mysql查询商铺
        Shop shop = getById(id);
        //4、不存在 报错
        if(shop==null){
            stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
        }
        //5、 mysql中存在 在redis中写数据并返回
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        return Result.ok(shop);
    }

除此以外,解决缓存穿透的方法还有:

增强id的复杂度、做好数据的基础格式校验、加强用户权限校验、做好热点参数限流等。

五、缓存雪崩 

 (1)缓存雪崩指的是在同一时段大量的缓存key同时失效或redis服务宕机,导致大量请求到达数据库,产生巨大压力。

(2)解决方案:

●给不同key添加随机的TTL

●利用redis集群提高服务的可用性

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

●给业务添加多级缓存(nginx、redis、jvm、本地等)

六、缓存击穿

(1)也称作热点key问题,指一个被高并发访问的并且缓存重建业务较复杂的key突然失效,无数的请求访问会在瞬间给数据库带来冲击。

(2)解决方案:

●互斥锁

●逻辑过期

Redis实战—黑马点评项目—商户查询缓存_第7张图片 

 

解决方案

优点 缺点
互斥锁

●没有额外内存消耗

●一致性

●实现简单

●线程需要等待,性能低

●死锁风险

逻辑过期 ●线程无需等待,性能较好

●不保证一致性

●额外内存消耗

●实现复杂

(3)基于互斥锁解决缓存击穿的查询商铺业务

Redis实战—黑马点评项目—商户查询缓存_第8张图片

 互斥锁的实现:可以利用redis 的SETNX创建锁。它的特点是,只有当一个key不存在时才去创建。那么SETNX操作就是获取锁的操作,当第一个线程set了一个锁之后,其他线程将无法继续set,直到这个线程del(delete)这个锁,即释放锁。

    //查店铺
    @Override
    public Result queryById(Long id) {

        //解决缓存穿透
        //queryWithPassThrough(id);

        //解决缓存击穿
        Shop shop = queryWithMutex(id);
        if(shop!=null){
            return Result.ok(shop);
        }
        return Result.fail("店铺信息不存在");

    }


    //解决缓存击穿的代码(互斥锁)
    private Shop queryWithMutex(Long id){
        String key = RedisConstants.CACHE_SHOP_KEY + id;//查商铺用的key
        //1、从redis中查询商铺
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shopJson)) {
            //2、 存在 返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }//仅当不为空字符串 不为null时返回true
        //判断是否命中空值
        if(shopJson!=null){
            /*return Result.fail("店铺信息不存在");*/
            return null;
        }
        //3、不存在 获取互斥锁 拿到再到数据库查询
        String lockKey="lock:shop:"+id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);//true 拿到锁

            //判断是否获取锁
            if(!isLock){
                //否 休眠 从redis查缓存(递归)
                Thread.sleep(50);
                queryWithMutex(id);
            }

            //再次查redis  存在则无需重建缓存
            String shopJson1 = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(shopJson1)) {

                return JSONUtil.toBean(shopJson1, Shop.class);
            }

            //是 根据id查数据库 重建缓存 模拟重建延时 释放锁
            shop = getById(id);
            Thread.sleep(200);
            //4、不存在 报错
            if(shop==null){
                stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
            }
            //5、 mysql中存在 在redis中写数据并返回
            stringRedisTemplate.opsForValue()
                    .set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            unlock(lockKey);
        }
        return shop;
    }

重启服务,删除redis中已存在的商铺缓存,使用jmeter工具来模拟高并发场景,测试代码

Redis实战—黑马点评项目—商户查询缓存_第9张图片

 模拟5秒发出1000次请求,查看后台,只有一次请求到达了数据库、在redis中亦有商铺缓存,代码改造成功。

Redis实战—黑马点评项目—商户查询缓存_第10张图片 Redis实战—黑马点评项目—商户查询缓存_第11张图片

 

 

你可能感兴趣的:(缓存)