Redis项目实战--商户查询缓存

一、商户查询缓存要实现的内容

1、添加Redis缓存

2、缓存更新策略

3、缓存穿透

4、缓存雪崩

5、缓存击穿

6、缓存工具封装

二、根据id查询商品时添加Redis缓存

2.1 根据id查询商铺缓存流程

2.2 代码实现 

 @Override
    public Result queryById(Long id) {
        // 1、从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY+id);
        // 2、判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            // 3、存在  直接返回
            //将json字符串反序列化成java对象
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 4、不存在,根据id查询数据库
        Shop shop = getById(id);
        if(shop==null){
            // 5、不存在:返回错误
            return Result.fail("店铺不存在");
        }
        // 6、存在,写入redis缓存
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop));
        // 7、返回
        return Result.ok(shop);
    }

 2.3 知识点

        1、java对象与json对象的转换 ---- 利用 hutool 中的  JSONUtil 工具类

 //将json字符串反序列化成java对象
 Shop shop = JSONUtil.toBean(shopJson, Shop.class);
//将java对象序列化成json对象
JSONUtil.toJsonStr(shop)

三、缓存更新策略 

        缓存更新策略有三种:内存淘汰、超时剔除、主动更新。

        1、内存淘汰: 不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时更新缓存。 

        一致性 :差                      维护成本:无

        2、超时剔除:给缓存数据添加TTL时间,到期后自动删除缓存。下次查询时更新缓存。

        一致性 :一般                  维护成本:低

        3、主动更新:编写业务逻辑,在修改数据库的同时,更新缓存。

        一致性:好                      维护成本:高

        业务场景:

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

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

3.1 主动更新策略 

3.1.1 常用策略

        由缓存的调用者,在更新数据库的同时更新缓存。

3.1.2 操作缓存和数据库时有三个问题需要考虑

        1、删除缓存还是更新缓存?

        (1)更新缓存:每次更新数据库都更新缓存,无效写操作较多

        (2)删除缓存:更新数据库时让缓存失效,查询时再更新缓存(常用)

        2、如何保证缓存与数据库的操作的同时成功或失败?

        (1)单体系统,将缓存与数据库操作放在一个事务

        (2)分布式系统,利用TCC等分布式事务方案

        3、先操作缓存还是先操作数据库

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

        (2)先操作数据库,再删除缓存(常用)

3.2 缓存更新策略的最佳实践方案

1. 低一致性需求:使用 Redis 自带的内存淘汰机制
2. 高一致性需求:主动更新,并以超时剔除作为兜底方案
读操作:
缓存命中则直接返回
缓存未命中则查询数据库,并写入缓存,设定超时时间
写操作:
先写数据库,然后再删除缓存
要确保数据库与缓存操作的原子性

3.3 给查询的缓存添加超时剔除和主动更新的策略 

        1、需求:

        ① 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写到缓存,并设置超时时间。

 stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        ② 根据id修改店铺时,先修改数据库,再删除缓存。 

    @Override
    @Transactional
    public Result updateById1(Shop shop) {
        if(shop.getId() == null){
            return Result.fail("店铺id不能为空");
        }
        // 1、更新数据库
        updateById(shop);
        // 2、删除缓存
        stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY+shop.getId());
        
        return Result.ok();
    }

3.4 知识点 

        1、删除Redis中的缓存 

 stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY+shop.getId());

四、缓存穿透 

4.1 缓存穿透定义 和 解决方案

        1、解决缓存空对象的缺点的方式:每次新增数据时,主动的将数据插入到redis缓存中,覆盖之前的null。 

        2、布隆过滤器中存放的是一个个字节,并非真正的数据。

4.2 利用 缓存空对象 解决缓存穿透 

    @Override
    public Result queryById(Long id) {
        // 1、从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY+id);
        // 2、判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            // 3、存在 且 shopJson不为空(即redis中存入的不是空值)
            //返回店铺信息
            //将json字符串反序列化成java对象
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // redis中有key 但是对应的value为空
        if(shopJson == null){
            // 返回一个错误信息
            return Result.fail("店铺不存在");
        }
        
        // 4、不存在,根据id查询数据库
        Shop shop = getById(id);
        if(shop==null){
            // 5、不存在:将空值写入redis中
            stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return Result.fail("店铺不存在");
        }
        // 6、存在,写入redis缓存
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        // 7、返回
        return Result.ok(shop);
    }

4.3 总结 

4.4 知识点 

        1、判断String类型是否为空:StrUtil.isNotBlank(shopJson)。当shopJson为null、""、" "是均返回false。

五、缓存雪崩

5.1 缓存雪崩定义 和 解决方案 

六、缓存击穿

6.1 缓存击穿定义 和 解决方案  

6.2 互斥锁 和 逻辑过期 比较

 6.3 互斥锁 和 逻辑过期 优缺点

6.4 基于互斥锁方式解决缓存击穿问题 

        1、需求:修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题

        2、注意:这里的互斥锁不是我们平常用的synolize锁或者lock锁:这种锁,我们拿到了可以执行代码,没有拿到则继续等待。但我们这个,我们拿到锁和拿不到锁的逻辑是需要我们自己定义的。

        3、自定义互斥锁:利用redis的setnx属性。

        获取锁

  // 获取锁
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
        //注意:不要直接将 Boolean 的值返回,因为这样返回它会进行拆箱,在这个拆箱的过程中,可能会出现空指针
        return BooleanUtil.isTrue(flag);
    }

        释放锁

 // 释放锁
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

        利用互斥锁解决缓存击穿

 // 利用 互斥锁解决 缓存击穿问题
    public Shop queryWithMutex(Long id){
        // 1、从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY+id);
        // 2、判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            // 3、存在 且 shopJson不为空(即redis中存入的不是空值)
            //返回店铺信息
            //将json字符串反序列化成java对象
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        // redis中有key 但是对应的value为空
        if(shopJson == null){
            // 返回一个错误信息
            return null;
        }

        // 4、实现缓存重建
        // 4.1、获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2、判断是否获取成功
            if(!isLock){
                // 4.3、失败,则休眠并重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            // 4.4、成功,根据id查询数据库
            shop = getById(id);
            
            // 模拟重建的延时
            Thread.sleep(200);
            
            if(shop==null){
                // 5、不存在:将空值写入redis中
                stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            // 6、存在,写入redis缓存
            stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        } catch (InterruptedException e) {
           throw new RuntimeException(e);
        } finally {
            // 释放锁
            unLock(lockKey);
        }
        // 7、返回
        return shop;
    }

6.5 基于逻辑过期方式解决缓存击穿问题

需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题

        1、将数据和逻辑过期时间保存到redis中

    private void saveShop2Redis(Long id,Long expireSeconds){
        // 1、查询店铺数据
        Shop shop = getById(id);
        // 2、封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 3、写入Redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
    }

        2、编写测试类测试saveShop2Redis()方法是否可行

@SpringBootTest
class HmDianPingApplicationTests {
    @Resource
    private ShopServiceImpl shopService;
    
    @Test
    void testSaveShop(){
        shopService.saveShop2Redis(1L,10L);
    }
}

        3、编写逻辑过期代码

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

    // 利用 逻辑过期 解决 缓存击穿问题
    public Shop queryWithLogicalExpire(Long id){
        // 1、从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
        // 2、判断是否存在 -- 缓存是否命中
        if(StrUtil.isBlank(shopJson)){
           // 未命中,直接返回null
            return null;
        }

        // 3、命中 -- 需要判断逻辑时间是否过期
        //  现将json反序列化为对象,获得里面的过期时间
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        //获取商铺信息
        JSONObject data = (JSONObject)redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();

        //判断逻辑时间是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            // 3.1、未过期,直接返回商品信息
            return shop;
        }
        // 3.2、 过期、需要缓存重建
        // 4、缓存重建
        // 4.1、获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        // 4.2、判断是否获取锁成功
        boolean isLock = tryLock(lockKey);
        // 4.2、判断是否获取成功
        if(isLock){
            //TODO 4.3、成功 开启独立线程,实现缓存重建  --- 利用线程池
            CACHE_REBUILD_EXECUTOR.submit(()->{
                //缓存重建
                try {
                    this.saveShop2Redis(id,20L);
                } catch (Exception e) {
                   throw new RuntimeException();
                } finally {
                    //释放锁
                    unLock(lockKey);
                }

            });
        }

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

6.6 知识点 

        1、 redis的setnx属性。对于相同的key,value只能保存一次,之后不能修改。可以用它来设计自定义锁

        2、对于返回值boolean类型的数据返回时,不要直接返回Boolean(其封装类),因为这样返回它会进行拆箱,在这个拆箱的过程中,可能会出现空指针。如tryLock()方法。

        3、idea生成try-catch快捷键:alt+ctrl+l

        4、lamdom表达式

        5、 创建线程池,并在线程池中开启独立线程,实现缓存重建。

            CACHE_REBUILD_EXECUTOR.submit(()->{
                //缓存重建
                try {
                    this.saveShop2Redis(id,20L);
                } catch (Exception e) {
                   throw new RuntimeException();
                } finally {
                    //释放锁
                    unLock(lockKey);
                }

            });

七、缓存工具封装 

7.1 说明 

        目前我们暂时只封装4种方法:

        1、将任意对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间。

        2、将任意对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题。

        3、根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题。

        4、根据指定的key查询缓存,并反序列化为指定类型,利用逻辑过期的方式解决缓存击穿问题。

package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.CACHE_NULL_TTL;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;

@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 将任意对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间。
     *
     * **/
    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }

    /**
     * 将任意对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题。
     *
     * **/
    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)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题。
     * @param keyPrefix redis中每个key的前缀
     * @param type
     * */
    public <R,ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){
        String key = keyPrefix + id;
        // 1、从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2、判断是否存在
        if(StrUtil.isNotBlank(json)){
            // 3、存在 且 json不为空(即redis中存入的不是空值)
            //将json字符串反序列化成java对象
            return JSONUtil.toBean(json, type);
        }
        // redis中有key 但是对应的value为空
        if(json == null){
            // 返回一个错误信息
            return null;
        }

        // 4、不存在,根据id查询数据库
        R r = dbFallback.apply(id);
        if(r==null){
            // 5、不存在:将空值写入redis中
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
        // 6、存在,写入redis缓存
        this.set(key,r,time,unit);
        // 7、返回
        return r;
    }


    /**
     * 根据指定的key查询缓存,并反序列化为指定类型,利用逻辑过期的方式解决缓存击穿问题
     * @param keyPrefix redis中每个key的前缀
     * @param type
     * 
     * @param Function<ID,R> 中的ID表示参数、R表示函数的返回类型
     * */

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

    // 利用 逻辑过期 解决 缓存击穿问题
    public  <R,ID> R  queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallback,Long time, TimeUnit unit){
        String key = keyPrefix + id;
        // 1、从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
        // 2、判断是否存在 -- 缓存是否命中
        if(StrUtil.isBlank(json)){
            // 未命中,直接返回null
            return null;
        }

        // 3、命中 -- 需要判断逻辑时间是否过期
        //  现将json反序列化为对象,获得里面的过期时间
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        //获取商铺信息
        JSONObject data = (JSONObject)redisData.getData();
        R r = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();

        //判断逻辑时间是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            // 3.1、未过期,直接返回商品信息
            return r;
        }
        // 3.2、 过期、需要缓存重建
        // 4、缓存重建
        // 4.1、获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        // 4.2、判断是否获取锁成功
        boolean isLock = tryLock(lockKey);
        // 4.2、判断是否获取成功
        if(isLock){
            //TODO 4.3、成功 开启独立线程,实现缓存重建  --- 利用线程池
            CACHE_REBUILD_EXECUTOR.submit(()->{
                //缓存重建
                try {
                    // 先查数据库
                    R r1 = dbFallback.apply(id);
                    // 写入redis
                    this.setWithLogicalExpire(key,r1,time,unit);
                } catch (Exception e) {
                    throw new RuntimeException();
                } finally {
                    //释放锁
                    unLock(lockKey);
                }

            });
        }

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

    // 获取锁
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
        //注意:不要直接将 Boolean 的值返回,因为这样返回它会进行拆箱,在这个拆箱的过程中,可能会出现空指针
        return BooleanUtil.isTrue(flag);
    }

    // 释放锁
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }
}

        调用工具类

    @Resource
    private CacheClient cacheClient;

    @Override
    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,id2->getById(id2),CACHE_SHOP_TTL,TimeUnit.MINUTES);

        // 7、返回
        return Result.ok(shop);
    }

7.2 知识类

        1、泛型 :查询时由于返回值的类型和ID的类型是不确定的,因此返回值的类型需要使用泛型。【<R,ID> R】【ID id,Class<R> type】

        2、函数式编程 : 我们封装工具类的时候,涉及到了数据库查询(不同的业务,查询自己的数据库),故在工具类中我们并不知道数据库如何进行查询的,因此利用参数由调用者告诉我们如何查。查数据库是一个函数,因此传入一个函数。【Function<ID,R> dbFallback】

        3、lamden表达式 :

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值