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

一、添加Redis缓存

 查询商铺

@Service
public class ShopServiceImpl
        extends ServiceImpl<ShopMapper, Shop> 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的读写速度相较于数据库的读写速度是更快的,所以第二种异常发生的概率比第一种更低。因此第二种方案是最佳选择:先操作数据库,再删缓存。

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

基于二中的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();
    }

四、缓存穿透

 

 【改造】

 基于缓存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)解决方案:

●互斥锁

●逻辑过期

 

 

解决方案

优点缺点
互斥锁

●没有额外内存消耗

●一致性

●实现简单

●线程需要等待,性能低

●死锁风险

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

●不保证一致性

●额外内存消耗

●实现复杂

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

 互斥锁的实现:可以利用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工具来模拟高并发场景,测试代码

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

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值