黑马redis实战篇-商铺缓存

news2024/9/20 8:09:36

目录

五、实战篇-商户查询缓存

5.1 什么是缓存

5.2 添加Redis缓存

1、不添加redis时,数据查询的作用模型:

2、添加redis时,数据查询的作用模型:

3、业务流程图:​编辑

4、代码实现

5、练习题

5.3 缓存更新策略

1、主动更新

2.Cache Aside Pattern(旁路缓存模式)

3、总结

4、给查询商铺的缓存添加超时剔除和主动更新的策略

5.4 缓存穿透

1、解决方案

2、解决商铺查询时,缓存穿透问题

3、总结

5.5 缓存雪崩

5.6 缓存击穿

1、解决方案

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

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

4、JMeter下载和安装

5.7 缓存工具封装


五、实战篇-商户查询缓存

5.1 什么是缓存

缓存就是数据交换的缓冲区(称作Cache ),是存储数据的临时地方,一般读写性能较高

 

缓存的作用:

  • 降低后端负载 ---直接访问缓存,返回数据

  • 提高读写效率,降低响应时间---基于内存存储

缓存的成本:

  • 数据一致性成本

  • 代码维护成本----解决一致性问题代码复杂

  • 运维成本-- 要保证高可用搭建集群

5.2 添加Redis缓存

1、不添加redis时,数据查询的作用模型:

 

2、添加redis时,数据查询的作用模型:

redis命中直接返回数据,未命中数据库查询返回数据,并且将数据缓存到redis中

 

3、业务流程图:

 

4、代码实现

 public Result selectShopInfoById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1.判断redis中是否存在该id的数据
        String str = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(str)) {
            //2.存在 直接返回数据
            return Result.ok(JSONUtil.toBean(str,Shop.class));
        }
        //3.不存在 查询数据库是否存在
        Shop shop = baseMapper.selectById(id);
        if (StringUtils.isEmpty(shop)) {
            //4.不存在直接返回 404
            return Result.fail("店铺不存在");
        }
        //5.存在将数据存储在redis,然后返回
        String shopJsonStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(key,shopJsonStr);
        return Result.ok(shop);
    }

5、练习题

给店铺类型业务添加缓存

 

public Result queryOrderByAscList() {
        //1.判断redis是否存在 商铺类型的缓存
        Long size = stringRedisTemplate.opsForList().size(CACHE_SHOP_TYPE_KEY);
        if (size > 0){
            //2.存在 直接取出返回
            List<String> range = stringRedisTemplate.opsForList().range(CACHE_SHOP_TYPE_KEY, 0, size);
            List<ShopType> shopTypes = range.stream().map(item -> {
                return  JSONUtil.toBean(item,ShopType.class);
            }).collect(Collectors.toList());
            //.sorted(Comparator.comparing(ShopType::getSort).reversed())
            return Result.ok(shopTypes);
        }
        //3.不存在,查询数据库
        QueryWrapper<ShopType> queryWrapper = new QueryWrapper<>();
        queryWrapper.orderByAsc("sort");
        List<ShopType> shopTypes = baseMapper.selectList(queryWrapper);
        if (shopTypes.size() <= 0){
            //4.数据库不存在 直接返回错误
            return Result.fail("数据不存在");
        }
        //5.数据库存在,将数据存储在缓存中然后返回
        List<String> collect = shopTypes.stream().map(item -> {
            return JSONUtil.toJsonStr(item);
        }).collect(Collectors.toList());
        stringRedisTemplate.opsForList().rightPushAll(CACHE_SHOP_TYPE_KEY,collect);
        //设置过期时间是1天
        stringRedisTemplate.expire(CACHE_SHOP_TYPE_KEY,CACHE_SHOP_TYPE_TTL, TimeUnit.MINUTES);

        return Result.ok(shopTypes);
    }

5.3 缓存更新策略

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

业务场景:

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

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

1、主动更新

  • Cache Aside Pattern 有缓存的调用缓存,在更新数据库的同时更新缓存

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

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

Redis的主动更新有三种常见的方案,包括:
​
Cache Aside Pattern(旁路缓存模式):应用程序先从缓存中获取数据,如果缓存中不存在要访问的数据,则从数据库获取,再将数据写入缓存中。
优点:高效性能,减少数据库访问次数和负载,适合于对数据实时性要求不高的应用。
​
缺点:存在缓存和数据库数据不一致的问题,当读写并发量大时,可能会出现脏数据。
​
Read/Write Through Pattern(读写穿透模式):数据缓存和数据库相连,应用程序从缓存中获取数据,如缓存中没有相应数据,会通过缓存访问层查找数据。该层在未命中数据后,查询数据库。若命中则返回数据,并同步写入缓存中;否则返回空值或默认值。
优点:保证缓存、数据库数据一致性,并且在缓存失效的情况下,也可以避免因读操作而引起的数据库压力过大,同时也可以防止缓存数据与数据库之间的数据不一致。
​
缺点:每次访问数据都必须通过缓存去访问数据库,增加了结构的复杂性并降低了系统的效率。
​
Write Behind Caching Pattern(写回缓存模式):在进行写操作时,不直接将数据写入到数据库中,而是先将数据写入缓存中,待缓存达到一定条件后再批量同步到数据库中。
优点:提高了写操作的性能,并且降低了数据库负载,可以适用于写入比较频繁但读取全量较少的应用场景,同时也减少了与数据库的交互次数和延迟。
​
缺点:由于只在达到缓存阈值之后才进行同步,因此可能会存在缓存中未及时更新的数据,从而引起数据不一致性问题,同时当缓存重新启动时还需要从磁盘上读取数据进行恢复,增加了复杂度。
​
需要针对具体应用场景选择合适的主动更新方案,并结合Redis中提供的其他功能一起使用。

这里比较常用的:Cache Aside Pattern(旁路缓存模式)

2.Cache Aside Pattern(旁路缓存模式)

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

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

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

    • 删除缓存:更新数据库时让缓存失败,查询时再更新缓存(比较符合)

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

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

    • 分布式系统,利用TCC等分布式事务方案

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

    • 先删除缓存,再操作数据库(不推荐,更新数据库时间长,出现概率很大)

      第一个线程删除缓存后,在更新数据库的时候,还没更新成功的时候,
      ​
      第二个线程访问了,发现缓存没有,查询数据库的数据,这是数据库的数据的旧的,将旧的数据更新到缓存中出现了不一致性

       

      可以使用延时双删的策略,即先删除缓存,在更新数据库,然后休眠500毫秒在删除缓存,但是因为第二次延时时间,不确定性很大,一般不推荐使用

    • 先操作数据库,再删除缓存(推荐,相较于上一种出现概率很低)

      因为某种原因,缓存找中数据没了,线程1访问的时候发现没有缓存,查询数据库得到旧数据,要进行写入缓存操作时
      线程2进行了更新数据库,删除缓存,然后线程1更新了缓存为旧数据

       

3、总结

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

  1. 低一致性需求:使用Redis自带的内存淘汰机制

  2. 高一致性需求:主动更新,并以超时剔除作为兜底方案

    • 读操作 Cache Aside Pattern(旁路缓存模式):

      • 缓存未命则直接返回

      • 缓存未命中则查询数据库,并写入缓存,设定超时时间

    • 写操作:

      • 先写数据库,然后再删除缓存

      • 要确保数据库与缓存操作的原子性

4、给查询商铺的缓存添加超时剔除和主动更新的策略

修改ShopController中的业务逻辑,满足下面的需求:

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

    @Override
        public Result selectShopInfoById(Long id) {
            String key = CACHE_SHOP_KEY + id;
            //1.判断redis中是否存在该id的数据
            String str = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(str)) {
                //2.存在 直接返回数据
                return Result.ok(JSONUtil.toBean(str,Shop.class));
            }
            //3.不存在 查询数据库是否存在
            Shop shop = baseMapper.selectById(id);
            if (StringUtils.isEmpty(shop)) {
                //4.不存在直接返回 404
                return Result.fail("店铺不存在");
            }
            //5.存在将数据存储在redis,然后返回
            String shopJsonStr = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
            return Result.ok(shop);
        }

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

     @Override
        @Transactional
        public Result updateShopById(Shop shop) {
            Long id = shop.getId();
            if (id == null) {
                return Result.fail("店铺id不能为空");
            }
            //修改数据库
            baseMapper.updateById(shop);
            //删除缓存
            stringRedisTemplate.delete(CACHE_SHOP_KEY+shop.getId());
            return Result.ok();
        }

5.4 缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

1、解决方案

常见的解决方案有两种:

  • 缓存空对象

    • 优点:实现简单,维护方便

    • 缺点:

      • 额外的内存消耗 ---设置ttl过期时间

      • 可能造成短期的不一致 ----插入数据的时候,更新缓存将null的覆盖

         

  • 布隆过滤

    • 优点:内存占用较少,没有多余key

    • 缺点:

      • 实现复杂

      • 存在误判可能---布隆过滤器是居于hash算法,存在哈希碰撞问题

        判断不存在的肯定不存在,判断存在的时候,可能不存在

         

2、解决商铺查询时,缓存穿透问题

 

public Result selectShopInfoById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1.判断redis中是否存在该id的数据
        String str = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(str)) {
            //2.存在 直接返回数据
            return Result.ok(JSONUtil.toBean(str,Shop.class));
        }
        //上面判断后 执行到这句的时候,只能是null或者空字符串
        if (str != null) {
            return Result.fail("店铺不存在");
        }
​
        //3.不存在 查询数据库是否存在
        Shop shop = baseMapper.selectById(id);
        if (StringUtils.isEmpty(shop)) {
            //将null存入到redis中
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            //4.不存在直接返回 404
             return Result.fail("店铺不存在");
        }
        //5.存在将数据存储在redis,然后返回
        String shopJsonStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

3、总结

缓存穿透产生的原因是什么?

  • 用户请求的数据在缓存和数据库汇总都不存在,不断发起这样的请求给数据库带来巨大压力

缓存穿透的解决方案有那些?

  • 缓存null值

  • 布隆过滤器

  • 增强id的复杂度,避免被猜测id规律,然后做好数据的基础格式校验

  • 加强用户权限校验

  • 做好热点参数的限流

5.5 缓存雪崩

缓存雪崩是指同一时段大量的缓存key同时失效或者Redis服务五宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的key的TTL添加随机值

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

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

  • 给业务添加多级缓存

     

5.6 缓存击穿

缓存击穿问题也叫作热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大冲击。

 

1、解决方案

互斥锁

 

逻辑过期

 

比较

解决方案优点缺点
互斥锁没有额外的内存消耗 保证了一致性 实现简单线程需要等待,性能受影响 可能有死锁的情况
逻辑过期线程无序等待,性能好不保证一致性 存在内存消耗 实现复杂

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

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

 

 private  Result cacheShopWithMutex(Long id) {
            String key = CACHE_SHOP_KEY + id;
            Shop shop = null;
            //1.判断redis中是否存在该id的数据
            String str = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(str)) {
                //2.存在 直接返回数据
                return Result.ok(JSONUtil.toBean(str,Shop.class));
            }
            //上面判断后 执行到这句的时候,只能是null或者空字符串
            if (str != null) {
                return Result.fail("店铺不存在");
            }
        String lockKey = LOCK_SHOP_KEY + id;
        try {
​
            //3.不存在 先尝试获取互斥锁  利用redis中string字符串中set
            Boolean flagBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
            boolean flag = BooleanUtil.isTrue(flagBoolean);
            //4.获取锁失败
            if (!flag) {
                //获取锁失败休眠一会
                Thread.sleep(100);
                //然后进行重试 ---递归
                return selectShopInfoById(id);
            }
            //5.如果获取锁成功 查询数据库
            shop = baseMapper.selectById(id);
            //模拟重建延迟
            Thread.sleep(200);
            //如果数据库中没有数据
            if (StringUtils.isEmpty(shop)) {
                //将null存入到redis中
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                //不存在直接返回 404
                return Result.fail("店铺不存在");
            }
            //.存在将数据存储在redis,然后返回
            String shopJsonStr = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //6.释放锁
            stringRedisTemplate.delete(lockKey);
        }
        return Result.ok(shop);
    }

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

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

 

//弄一个线程池
    private static  final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    //逻辑过期
    private Result cacheShopWithLogicTTL(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1.判断redis中是否存在该id的数据
        String str = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(str)) {
            //2.不存在 直接返回空
            return Result.fail("商铺信息为空");
        }
​
​
        //3.存在 判断缓存是否过期  逻辑时间
        RedisData redisData = JSONUtil.toBean(str, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        JSONObject data = (JSONObject)redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        if (expireTime.isAfter(LocalDateTime.now())) {
            //4.未过期 直接返回商铺信息
            return Result.ok(shop);
        }
        //5.过期了尝试获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        boolean flag = BooleanUtil.isTrue(aBoolean);
        if (!flag) {
            //6.如果未获取到锁 直接返回旧数据
            return Result.ok(shop);
        }
        //7.成功获取到锁 开启一个独立线程
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            //重构缓存
            try {
                saveShopToRedis(id,30L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                stringRedisTemplate.delete(lockKey);
            }
            //释放锁
        });
        //8.返回旧的数据
        return Result.ok(shop);
    }

4、JMeter下载和安装

参考

JMeter下载和安装_仰望_1的博客-CSDN博客

1.下载

2.解压

 

 

3.设置环境变量

 

4.path中设置

 

5.启动

双击打开bin中的jemter.bat

 

就自动启动了

 

6.设置中文

 

7.进行配置

 

 

 

8、输入参数,测试

 

 

 

5.7 缓存工具封装

基于StingRedisTemplate封装一个缓存工具类,满足下列需求:

方法1:将任意Java对象序列化为json并存储在String类型的key中,并且可设置TTL过期时间

方法2:将任意Java对象序列化为json并存储在String类型的key中,并在可以设计逻辑过期时间,用于处理缓存击穿问题

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

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

封装类:CacheClient

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 lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
​
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.*;
​
/**
 * @packageName: com.hmdp.utils
 * @author: winter
 * @date: 2023/4/25 8:55
 * @version: 1.0
 * @email 1660420659@qq.com
 * @description: 封装Redis工具类
 */
@Slf4j
@Component
public class CacheClient {
​
    @Resource
    private StringRedisTemplate stringRedisTemplate;
​
    /**
     * 将任意Java对象序列化为json并存储在String类型的key中,
     * 并且可设置TTL过期时间
     * @param key  key
     * @param obj  存储对象
     * @param timeTTL  过期时间
     * @param timeUnit  单位
     */
    public void set(String key, Object obj, Long timeTTL, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(obj),timeTTL,timeUnit);
    }
​
    /**
     * 将任意Java对象序列化为json并存储在String类型的key中,
     * 并在可以设计逻辑过期时间,用于处理缓存击穿问题
     * @param key  key
     * @param obj  存储对象
     * @param timeTTL  过期时间
     * @param timeUnit  单位
     */
    public  void setWithLogicalExpire(String key,Object obj,Long timeTTL, TimeUnit timeUnit) {
        RedisData redisData = new RedisData();
        redisData.setData(obj);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(timeTTL)));
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }
​
    /**
     * 通过key获取字符串
     * @param key
     * @return
     */
    public String get(String key) {
        String str = stringRedisTemplate.opsForValue().get(key);
        return str;
    }
​
    /**
     * 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
     * @param key  key值
     * @param id  id值
     * @param tClass  类型
     * @param function 手写方法
     * @param timeTTL  过期时间
     * @param timeUnit  时间单位
     * @param <T>  对象类型
     * @param <ID> id类型
     * @return  对象
     */
    public <T,ID> T getWithPassThrough(String key,ID id, Class<T> tClass, Function<ID,T> function,Long timeTTL, TimeUnit timeUnit) {
        //1.判断redis中是否存在该id的数据
        String str = get(key);
        if (StrUtil.isNotBlank(str)) {
            //2.存在 直接返回数据
            return JSONUtil.toBean(str,tClass);
        }
        //上面判断后 执行到这句的时候,只能是null或者空字符串
        if (str != null) {
            return null;
        }
​
        //3.不存在 查询数据库是否存在
        T shop = function.apply(id);
        if (StringUtils.isEmpty(shop)) {
            //将null存入到redis中
            set(key,"",CACHE_NULL_TTL,timeUnit);
            //4.不存在直接返回 404
            return null;
        }
        //5.存在将数据存储在redis,然后返回
        set(key,shop,timeTTL,timeUnit);
        return shop;
    }
​
​
    //弄一个线程池
    private static  final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
​
    /**
     * 根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
     * @param key key
     * @param id id
     * @param tClass  RedisDate中存储对象类型
     * @param function 方法
     * @param timeTTL 过期时间
     * @param timeUnit 过期类型
     * @param <T>  对象类型
     * @param <ID>  id类型
     * @return 对象
     */
    public <T,ID> T getWithLogicalExpire(String key,ID id, Class<T> tClass, Function<ID,T> function,Long timeTTL, TimeUnit timeUnit) {
        //1.判断redis中是否存在该id的数据
        String str = get(key);
        if (StrUtil.isBlank(str)) {
            //2.不存在 直接返回空
            return null;
        }
​
​
        //3.存在 判断缓存是否过期  逻辑时间
        RedisData redisData = JSONUtil.toBean(str, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        JSONObject data = (JSONObject)redisData.getData();
        T shop = JSONUtil.toBean(data, tClass);
        if (expireTime.isAfter(LocalDateTime.now())) {
            //4.未过期 直接返回商铺信息
            return shop;
        }
        //5.过期了尝试获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        boolean flag = BooleanUtil.isTrue(aBoolean);
        if (!flag) {
            //6.如果未获取到锁 直接返回旧数据
            return shop;
        }
        //7.成功获取到锁 开启一个独立线程
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            //重构缓存
            try {
                //查询店铺数据
                T tshop = function.apply(id);
                //模拟
                Thread.sleep(200);
                //封装逻辑过期时间
                setWithLogicalExpire(key,tshop,timeTTL,timeUnit);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                stringRedisTemplate.delete(lockKey);
            }
            //释放锁
        });
        //8.返回旧的数据
        return shop;
    }
}
​

测试:ShopServiceImpl

 @Override
    public Result selectShopInfoById(Long id) throws InterruptedException {
        //1.缓存穿透 存储null值解决方案
//     return  cacheShopWithPassThrough(id);
        //2.缓存击穿  --互斥锁解决方案
//       return cacheShopWithMutex(id);
        //3.缓存击穿 ---逻辑过期解决方案
//        return cacheShopWithLogicTTL(id);
​
        //4.使用封装类中解决缓存穿透  存储null值办法
//        Shop shop = cacheClient.getWithPassThrough(CACHE_SHOP_KEY + id, id, Shop.class
//                            ,this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
//        return Result.ok(shop);
​
        //5.使用封装类中解决缓存击穿  逻辑过期方式
        //为了测试 将逻辑过期时间设置短一点
        Shop shop = cacheClient.getWithLogicalExpire(CACHE_SHOP_KEY + id, id, Shop.class
                , this::getById, 10L, TimeUnit.SECONDS);
        return Result.ok(shop);
    }

具体代码

redis实战篇-hmdp-短信登录-商铺缓存: 存放黑马点评中redis进行短信登录、商铺查询的代码 ,包括前端后后端

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/458774.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Android FrameWork (三)】- SystemServer

文章目录 知识回顾启动第一个流程initZygote的流程 前言源码分析1.system_server2.SystemServer.main3,startBootstrapServices4,startService 拓展知识LoadApkcontext 对于Android context 大家是怎么理解的&#xff1f;LocalServices.java: addServece方法中 ArrayMap和HashM…

Matlab 绘制双纵轴三纵轴图

三纵轴图 三坐标的图在前文中有所介绍&#xff1b;这次主要讲绘制双轴。 matlab 绘制三坐标&#xff08;轴&#xff09;图 绘制双纵轴图: yyaxis 简单用法 在MATLAB中&#xff0c;yyaxis可以用于绘制具有两个不同y轴的图形。以下是yyaxis的简单用法&#xff1a; 1.首先&am…

UG NX二次开发(C#)-UIStyler-找不到指定的Dlx文件的错误解决方法

1、项目场景: 在UG NX二次开发过程中,我们为了更好的操作,采用UI Styler设计了软件界面,然后按照UI Styler的编程流程成功的生成了dll,但是在采用Ctrl+U或者用“文件“->“执行”->"NX Open"执行dll时,遇到如下图所示的错误页面,提示内容为:找不到指定…

成就更强大的自己

每一次低谷&#xff0c;都会酝酿向上的力量。 每一次痛苦过后&#xff0c;都会洗涤掉心理深处的灰尘。 人生的路上&#xff0c;坎坷前行&#xff0c;只有保持积极向上的态度&#xff0c;才能把坎坷化为坦途。 走过一段路后&#xff0c;才发现&#xff0c;当内心强大、修养、爱…

Android之 颜色选择器

一&#xff0c;简介 1.1 计算机的颜色通常有两种表示方式&#xff1a; 光源模式RGB(Red红, Green绿, Blue蓝)&#xff0c;数值0-255 印刷模式CMYK(Cyan青, Magenta品红, Yellow黄, Black黑)&#xff0c;数值1-100 任何颜色都是由RGB或CMYK混合出来的&#xff0c;再加上透明度…

2023年产业基金研究报告

第一章 行业概况 1.1 概述 产业基金&#xff0c;又称为产业投资基金&#xff0c;是一种由政府、企业、金融机构等出资设立的&#xff0c;专门用于支持和促进特定产业发展的投资基金。产业基金通常以股权投资和长期投资为主&#xff0c;旨在推动产业结构升级、促进科技创新、提…

算法刷题|139.单词拆分、多重背包

单词拆分 题目&#xff1a;给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。 注意&#xff1a;不要求字典中出现的单词全部都使用&#xff0c;并且字典中的单词可以重复使用。 思路&#xff1a;字符串s就是我们的背包…

【移动端网页布局】流式布局案例 ① ( 视口标签设置 | CSS 样式文件设置 | 布局宽度设置 | 设置最大宽度 | 设置最小宽度 )

文章目录 一、视口标签设置二、CSS 样式文件设置三、布局宽度设置1、设置布局宽度2、设置布局最大宽度3、设置布局最小宽度4、查看网页最大最小宽度5、布局宽度设置 四、代码示例1、主界面标签2、CSS 布局设置 一、视口标签设置 参考 【移动端网页布局】移动端网页布局基础概念…

打造卓越游戏 | 2023 Google 游戏开发者峰会

一款游戏从初始构想的开发到辉煌赛季的策划&#xff0c;开发者们每时每刻都在倾注心血潜心钻研&#xff0c;Google 也致力于在整个开发和发布生命周期中为您提供帮助。我们很高兴能在今年如约而至的 Google 游戏开发者峰会中与您分享诸多更新&#xff0c;展示我们为助力您打造精…

JavaScript常用方法整理

文章目录 前言1.栈方法&#xff1a;push()、pop()2.队列方法&#xff1a;unshift()、shift()3.indexof()、lastIndexOf()、includes()4.操作方法&#xff1a;concat()、slice()、splice()5.Array.isArray()6.排序方法:sort()、reverse()7.转换方法&#xff1a;toString()、join…

input 元素 change 事件失效,失去焦点的时候才执行?什么原因导致?如何正确使用?

具体问题如标题所示&#xff0c;不再过多水字数&#xff0c;请看下面的代码&#xff1a; <input :class"[custom-form-item-input, (isFocusUserName && !userName) ? custom-form-item-input-err-active : ]"autoCompletenew-password type"text&…

带着疑问学C语言-C语言常用变量

目录 目录 一、声明变量和定义变量的区别是什么&#xff1f; 二、什么常量&#xff0c;如何区分常量和变量&#xff1f; 三、各数据类型所占的内存是多少&#xff1f; 四、字符与字符串的差别有哪些&#xff1f; 五、为什么会发生数据溢出&#xff0c;如何避免数据溢出&am…

springboot整合juit和springboot整合mybatis和springboot整合ssm

springboot整合juit 先看一眼包路径&#xff0c;发现main程序的路径和测试类的路径是一样的 启用新注解&#xff1a;SpringBootTest代替了之前sm整合juit时的 RunWith(SpringJUnit4ClassRunner.class) //spring配置类 ContextConfiguration(classes config.class)新的如此…

数字北京城,航行在联通2000M的“大运河”

前故宫博物院院长单霁翔&#xff0c;在《大运河漂来紫禁城》一书中提到过&#xff0c;紫禁城里的石材、木材&#xff0c;甚至每一块砖&#xff0c;都是通过大运河&#xff0c;跋山涉水来到北京的。某种程度上说&#xff0c;北京城的繁荣与这条纵跨南北的“中华大动脉”密不可分…

OpenGL入门教程之 变换

引言 这是一个闪耀的时刻&#xff0c;因为我们即将能生产出令人惊叹的3D效果&#xff01; 变换 向量和矩阵变换包括太多内容&#xff0c;但由于学过线性代数和GAMES101&#xff0c;因此不在此做过多阐述。仅阐述包括代码的GLM内容。 GLM的使用 &#xff08;1&#xff09;GLM…

拓扑排序例题 P4017 最大食物链计数

拓扑排序例题 P4017 最大食物链计数 题目链接&#xff1a;P4017 最大食物链计数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 最大食物链计数 题目背景 你知道食物链吗&#xff1f;Delia 生物考试的时候&#xff0c;数食物链条数的题目全都错了&#xff0c;因为她总是重…

陆奇博士4月23日深圳奇绩创坛分享会PPT及核心观点:新范式、新时代、新机遇(附PPT下载链接)...

省时查报告-专业、及时、全面的行研报告库 省时查方案-专业、及时、全面的营销策划方案库 【免费下载】2023年3月份热门报告合集 【限时免费】ChatGPT4体验&#xff0c;无需翻墙直接用 ChatGPT调研报告&#xff08;仅供内部参考&#xff09; ChatGPT的发展历程、原理、技术架构…

记录-使用双token实现无感刷新,前后端详细代码

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 前言 近期写的一个项目使用双token实现无感刷新。最后做了一些总结&#xff0c;本文详细介绍了实现流程&#xff0c;前后端详细代码。前端使用了Vue3Vite&#xff0c;主要是axios封装&#xff0c;服务…

Unity之OpenXR+XR Interaction Toolkit接入Pico VR一体机

一.前言 Pico VR 一体机是目前国内比较流行的VR设备之一&#xff0c;PICO成立于2015年3月&#xff0c;于2021年9月并入字节跳动。最新推出的Pico4一体机售价只有2400左右&#xff0c;这让很多家庭都入手了Pico设备&#xff0c;VR一体机的功能包括&#xff1a;VR全景视频&#…

学成在线笔记+踩坑(8)——课程预览、提交审核,Freemarker模板引擎

导航&#xff1a; 【黑马Java笔记踩坑汇总】JavaSEJavaWebSSMSpringBoot瑞吉外卖SpringCloud黑马旅游谷粒商城学成在线牛客面试题_java黑马笔记 目录 1 模块需求分析 1.1 模块介绍 1.2 业务流程 1.2.1 课程预览 1.2.2 课程审核 1.2.3 课程发布 2 课程预览 2.1 需求分析…