什么是缓存

定义

  • 本质:数据交换的缓冲区,是存储数据的临时地方
  • 核心特性:读写性能较高,常用于解决数据访问速度不匹配问题
  • 计算机案例:CPU内部缓存解决CPU运算速度与内存/磁盘读写速度不匹配问题,缓存越大CPU性能通常越好
  • 典型应用场景:浏览器缓存静态资源、应用层缓存(如Redis)、数据库缓存索引数据等

作用

  • 降低后端负载:请求直接命中缓存可避免数据库查询,显著减轻数据库压力,特别是对复杂SQL查询效果更明显
  • 提高读写效率:Redis读写延迟在微秒级(磁盘读写通常在毫秒级),响应时间缩短1000倍左右
  • 应对高并发:通过内存快速响应请求,使系统能够支撑更高并发量,是大型互联网应用的必备方案

成本

  • 数据一致性:数据库更新时缓存可能仍保留旧数据,需要额外机制保证一致性(如双写、失效策略等)
  • 代码复杂度:需处理缓存穿透、击穿、雪崩等问题,业务逻辑复杂度显著增加
  • 运维成本:
    • 硬件成本:需要搭建Redis集群保证高可用
    • 人力成本:集群部署、监控、维护需要专业团队
  • 适用性评估:中小型企业用户量不大时可能得不偿失,需权衡收益与成本

添加Redis缓存

image-1

public Result queryById(Long id) {  
    String key = RedisConstants.CACHE_SHOP_KEY + id;  
    // 从Redis中查询  
    String shopJson = stringRedisTemplate.opsForValue().get(key);  
    if (StrUtil.isNotBlank(shopJson)) {  
        // 存在,直接返回  
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);  
        return Result.ok(shop);  
    }  
    // 不存在,根据id查询数据库  
    Shop shop = getById(id);  
    // 判断结果  
    if (shop == null) {  
        // 数据库不存在,返回错误  
        return Result.fail("店铺不存在");  
    }  
    // 存在,写入redis  
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));  
    return Result.ok(shop);  
}

店铺类型缓存

image-2

public Result queryTypeList() {  
  
    String key = "cache:shop:type";  
    // 从Redis中查询  
    String shopTypeJson = stringRedisTemplate.opsForValue().get(key);  
    if (StrUtil.isNotBlank(shopTypeJson)) {  
        // 存在,直接返回  
        List<ShopType> shopTypeList = JSONUtil.toList(JSONUtil.parseArray(shopTypeJson), ShopType.class);  
        return Result.ok(shopTypeList);  
    }  
    // 不存在,根据id查询数据库  
    List<ShopType> typeList = query().orderByAsc("sort").list();  
    // 判断结果  
    if (typeList == null || typeList.size() == 0) {  
        // 数据库不存在,返回错误  
        return Result.fail("店铺类型列表不存在");  
    }  
    // 存在,写入redis  
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(typeList));  
    return Result.ok(typeList);  
}

缓存更新策略

内存淘汰

  • 机制原理:Redis基于内存存储的特性,当内存不足时会自动触发淘汰机制,清除部分数据释放空间
  • 一致性特点:
    • 被动实现:当淘汰数据被查询时,会触发数据库查询重新加载,达到最终一致性
    • 不可控性:淘汰时机和数据选择由Redis决定,可能导致长期数据不一致
  • 适用场景:作为Redis默认机制存在,适合对一致性要求低的场景(如极少变更的店铺类型数据)

超时剔除

  • 实现方式:通过Redis的expire命令设置TTL过期时间
  • 一致性控制:
    • 最终一致性:依赖过期时间长短控制,时间越短一致性越好(如30分钟优于1天)
    • 非强一致:在TTL周期内仍可能出现数据不一致
  • 维护成本:仅需在设置缓存时添加过期时间参数,实现简单

主动更新

Cache Aside Pattern

  • 核心思想:由业务代码显式维护缓存一致性
  • 实现方式:
    • 读操作:先查缓存,未命中则查库并回写缓存
    • 写操作:先更新数据库,再操作缓存(删除或更新)
  • 优势:实现可控性强,适合大多数业务场景

Read/Write ThroughPattern

  • 架构特点:将缓存与数据库封装为统一服务
  • 调用方式:
    • 对调用者透明,无需关心底层存储
    • 服务内部保证缓存与数据库的原子操作
  • 缺点:实现复杂度高,缺乏成熟开源方案

Write Behind CachingPattern

  • 异步机制:
    • 调用者仅操作缓存
    • 独立线程异步持久化到数据库
  • 性能优势:可合并多次写操作,提升IO效率
  • 风险点:
    • 缓存宕机可能导致数据丢失
    • 存在较长时间的数据不一致窗口

存在问题

删除缓存还是更新缓存?

  • 更新缓存:每次更新数据库都更新缓存,无效写操作较多
  • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存(更常用

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

  • 单体系统,将缓存与数据库操作放在一个事务
  • 分布式系统,利用TCC等分布式事务方案

数据库和缓存的先后操作问题(线程安全)

image-3

image-4

  • 先删除缓存再操作数据库
    • 问题场景:
      • 线程1删除缓存后,线程2查询未命中加载旧数据
      • 线程1更新数据库后产生不一致
    • 发生概率:较高(因数据库写操作耗时较长)
  • 先删除数据库再操作缓存
    • 异常情况:
      • 缓存失效时查询线程加载旧数据
      • 更新线程在极短时间内完成数据库更新和缓存删除
    • 发生概率:极低(需在微秒级时间窗口内完成数据库写操作)

总结

  • 低一致性需求:
    • 采用Redis内存淘汰机制
    • 可配合设置较长TTL时间
  • 高一致性需求:
    • 主方案:主动更新(Cache Aside Pattern)
    • 兜底方案:设置合理TTL超时剔除
  • 读写规范:
    • 读流程:命中返回→未命中查库→回写缓存(带TTL)
    • 写流程:先写数据库→再删缓存→保证操作原子性

实现商铺缓存与数据库的双写一致

ShopServiceImpl

增加过期时间

@Override  
public Result queryById(Long id) {  
	//... 
    // 存在,写入redis  
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);  
    return Result.ok(shop);  
}

更新店铺信息

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

缓存穿透的解决方案

概念

  • 缓存穿透定义:客户端请求的数据在缓存和数据库中都不存在的情况。
  • 缓存穿透危害:导致所有请求直接访问数据库,可能压垮数据库。
  • 缓存穿透特征:请求参数可能是伪造的,缓存无法建立。
  • 缓存穿透场景:恶意用户并发请求不存在的数据。
  • 缓存穿透原理:缓存未命中且数据库查询为空,无法回填缓存。
  • 缓存穿透确认条件:缓存和数据库连续查询均为空结果。

解决思路

image-6

缓存空对象

  • 缓存空对象定义:将数据库中不存在的查询结果以null值缓存到Redis中的解决方案。
  • 实现思路:当Redis和数据库均未命中时,将null值写入缓存以阻止后续数据库查询。
  • 核心优点:实现简单,代码容易编写,能有效减少数据库压力
  • 主要缺点:存在额外内存消耗和短期数据不一致性问题。
  • 内存优化方案:为null值设置较短TTL(如2-5分钟)自动清除垃圾数据。
  • 一致性解决方案:新增数据时主动更新缓存覆盖null值。
  • 适用场景:对短期不一致性容忍度较高的业务场景。

布隆过滤

  • 布隆过滤定义:一种基于概率的算法,用于判断数据是否存在。
  • 布隆过滤原理:使用bit数组存储数据的哈希值二进制形式。
  • 布隆过滤工作流程:客户端请求先经过布隆过滤判断,不存在则直接拒绝,存在才查询Redis。
  • 布隆过滤特点:内存占用少(仅存储二进制位),但存在误判可能(存在判断不100%准确)
  • 布隆过滤优点:减少空数据缓存,节省存储空间
  • 布隆过滤缺点:实现复杂,存在误判导致缓存穿透风险。
  • 布隆过滤应用:通常与Redis的bitmap结合使用简化开发。

主动方案

  • 增强ID复杂度(如使用10-20位复杂ID)
  • 加强参数格式校验(如过滤ID=0的请求)
  • 用户权限管理和限流措施

代码实现

利用缓存空对象来实现

image-7

修改查询店铺信息的接口业务代码

    public Result queryById(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        // 从Redis中查询
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shopJson)) {
            // 存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 判断命中的是否为空值
        if (shopJson != null) {
            // 此时为空字符串
            return Result.fail("店铺不存在");
        }
        // 不存在,根据id查询数据库
        Shop shop = getById(id);
        // 判断结果
        if (shop == null) {
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 数据库不存在,返回错误
            return Result.fail("店铺不存在");
        }
        // 存在,写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

缓存雪崩

概念

  • 核心定义:在同一时段内大量缓存key同时失效或Redis服务宕机,导致请求直接打到数据库造成巨大压力
  • 两种表现形式:
    • 批量key同时过期(如几万到几十万key瞬间过期)
    • Redis服务完全宕机(所有请求直接访问数据库)
  • 危害程度比较:服务宕机情况比key批量失效更严重,会导致所有请求”裸奔”访问数据库

解决方案

key失效导致雪崩解决思路

  • TTL随机化:
    • 在批量导入数据时,给基础TTL值(如30分钟)添加随机数(如1-5分钟)
    • 效果:使key过期时间分散在30-35分钟区间,避免同时失效
  • 应用场景:特别适用于缓存预热时的批量数据导入场景
  • 实现难度:技术简单,只需在设置TTL时添加随机数即可

redis宕机导致雪崩解决思路

  • 高可用架构:
    • 搭建Redis集群+哨兵机制
    • 主从数据同步保证数据不丢失
    • 哨兵监控实现故障自动转移(主节点宕机时自动选举新主)
  • 技术要点:需要掌握Redis集群搭建和哨兵机制配置
  • 学习路径:属于Redis高级课程内容,需后续专项学习

添加降级和限流策略

  • 核心思想:牺牲部分服务保护数据库整体健康
  • 具体措施:
    • 快速失败:检测到Redis故障时立即拒绝请求
    • 服务降级:返回兜底数据或错误提示
    • 请求限流:控制打到数据库的请求量
  • 技术实现:需要微服务架构支持(如Spring Cloud的Hystrix等组件)
  • 典型场景:应对机房级故障等极端情况

添加多级缓存

  • 防御层级:
    • 浏览器缓存(静态数据)
    • Nginx缓存(动态数据)
    • JVM本地缓存(如Caffeine)
    • Redis缓存
    • 数据库
  • 优势:类似”多层防弹衣”,单层失效仍有其他缓存保护
  • 应用案例:京东等电商平台的商品详情页架构
  • 学习资源:涉及Spring Cloud微服务和Redis高级课程内容

缓存击穿问题及解决方案

  • 核心特征:指被高并发访问且缓存重建业务复杂的key突然失效,导致瞬间大量请求直接冲击数据库
  • 与雪崩区别:不同于多个key同时失效的雪崩现象,击穿是单个热点key失效引发的问题
  • 必要条件:
    • 高并发访问:如活动商品同一时刻可能被数千请求访问
    • 复杂重建逻辑:涉及多表关联查询或复杂运算,耗时可达数百毫秒

  • 失效时间线:在缓存重建完成前的时间窗口内,所有请求都无法命中缓存
  • 连锁反应:重建耗时越长,涌入数据库的请求量呈指数级增长
  • 典型场景:活动商品缓存失效时,重建过程需要计算促销价格、库存等多维度数据 image-8|700x305

image-9|700x350

互斥锁

  • 核心机制:
    • 首个未命中线程获取分布式锁进行重建
    • 其他线程获取锁失败后进入休眠重试循环(非无限重试)
  • 关键节点:
    • 锁释放前:所有线程查询均为未命中状态
    • 锁释放后:后续请求可直接命中新建缓存
  • 性能影响:重建期间所有并发线程处于阻塞状态,如重建耗时500ms,数千线程将同步等待

  • 互斥锁优势:
    • 内存效率:无额外字段存储开销
    • 强一致性:保证返回数据绝对最新
    • 实现简单:仅需基础锁机制
  • 互斥锁缺陷:
    • 性能瓶颈:线程等待导致吞吐量下降
    • 死锁风险:多缓存访问可能产生循环依赖

image-10

public Result queryById(Long id) {  
    // 缓存穿透  
    // Shop shop = queryWithPassThrough(id);  
  
    // 互斥锁解决缓存击穿  
    Shop shop = queryWithMutex(id);  
    if (shop == null) {  
        return Result.fail("店铺不存在");  
    }  
    return Result.ok(shop);  
}
 
/**  
 * 缓存击穿解决方案  
 *  
 * @param id  
 * @return  
 */public Shop queryWithMutex(Long id) {  
    String key = RedisConstants.CACHE_SHOP_KEY + id;  
    // 从Redis中查询  
    String shopJson = stringRedisTemplate.opsForValue().get(key);  
    if (StrUtil.isNotBlank(shopJson)) {  
        // 存在,直接返回  
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);  
        return shop;  
    }  
    // 判断命中的是否为空值  
    if (shopJson != null) {  
        // 此时为空字符串  
        return null;  
    }  
  
    // 实现缓存重建  
    // 获取互斥锁  
    String lock = RedisConstants.LOCK_SHOP_KEY + id;  
    Shop shop = null;  
    try {  
        boolean isLock = tryLock(lock);  
        if (!isLock) {  
            // 获取锁失败,休眠并重试  
            Thread.sleep(50);  
            return queryWithMutex(id);  
        }  
  
        // 不存在,根据id查询数据库  
        shop = getById(id);  
        // 模拟重建的耗时  
        Thread.sleep(200);  
        // 判断结果  
        if (shop == null) {  
            // 将空值写入redis  
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);  
            // 数据库不存在,返回错误  
            return null;  
        }  
        // 存在,写入redis  
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);  
    } catch (InterruptedException e) {  
        throw new RuntimeException(e);  
    } finally {  
        // 释放互斥锁  
        unLock(lock);  
    }  
    return shop;  
}
 
/**  
 * 尝试获取锁  
 *  
 * @param key  
 * @return  
 */private boolean tryLock(String key) {  
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);  
    return BooleanUtil.isTrue(flag);  
}  
  
/**  
 * 释放锁  
 *  
 * @param key  
 */  
private void unLock(String key) {  
    stringRedisTemplate.delete(key);  
}

tryLock为什么不直接返回flag?

在Java中,stringRedisTemplate.opsForValue().setIfAbsent()方法返回的是一个Boolean包装类型,而不是原始的boolean类型。这个Boolean对象可能有三种状态:

  1. true - 表示键不存在,成功设置了值
  2. false - 表示键已经存在,没有设置值
  3. null - 在某些异常情况下可能返回null

如果直接返回flag,当flagnull时,在某些情况下可能会导致NullPointerException。例如,如果这个返回值后续被拆箱为boolean类型,就会出现空指针异常。

BooleanUtil.isTrue(flag)方法的作用是安全地检查一个Boolean对象是否为true,它会处理null值的情况:

  • flagBoolean.TRUE时,返回true
  • flagBoolean.FALSE时,返回false
  • flagnull时,也返回false

这等价于Boolean.TRUE.equals(flag)的写法,但更加简洁易读。

所以在这段代码中使用BooleanUtil.isTrue(flag)而不是直接返回flag是为了避免潜在的空指针异常,并明确表达开发者的意图:只有当返回值明确为true时才返回true,其他情况(包括nullfalse)都返回false。这是一种更加安全和清晰的编程实践。

清除redis缓存后,使用apifox的自动化测试,可以看到控制台只有一条sql语句

image-12

image-11

逻辑过期

  • 逻辑过期定义
    • 存储结构:在value中嵌入过期时间字段(非Redis TTL)
    • 永不过期设计:通过内存淘汰策略管理,理论上key永久有效
    • 典型应用:活动期间预先加载热点数据,活动结束手动清除
  • 过期时间字段作用
    • 异步更新:发现逻辑过期后,由独立线程负责重建
    • 旧数据兜底:主线程直接返回旧数据保证可用性
    • 时间控制:如设置30分钟逻辑过期,实际数据更新延迟可控
  • 逻辑过期优势:
    • 高并发性:线程无需等待立即响应
    • 系统可用:始终有数据返回(可能为旧值)
  • 逻辑过期缺陷:
    • 弱一致性:存在短暂数据不一致窗口
    • 内存消耗:需存储额外过期字段
    • 实现复杂度:需维护独立更新线程

image-1

封装RedisData存储需要缓存的数据,设置逻辑过期时间

package com.hmdp.utils;  
  
import lombok.Data;  
  
import java.time.LocalDateTime;  
  
@Data  
public class RedisData {  
    private LocalDateTime expireTime;  
    private Object data;  
}
public Result queryById(Long id) {  
    // 缓存穿透  
    // Shop shop = queryWithPassThrough(id);  
  
    // 互斥锁解决缓存击穿  
    // Shop shop = queryWithMutex(id);  
  
    // 逻辑过期解决缓存击穿  
    Shop shop = queryWithLogincalExpire(id);  
  
    if (shop == null) {  
        return Result.fail("店铺不存在");  
    }  
    return Result.ok(shop);  
}
// 线程池用于缓存重建  
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);  
  
/**  
 * 逻辑过期解决缓存击穿  
 *  
 * @param id  
 * @return  
 */public Shop queryWithLogincalExpire(Long id) {  
    String key = RedisConstants.CACHE_SHOP_KEY + id;  
    // 从Redis中查询  
    String shopJson = stringRedisTemplate.opsForValue().get(key);  
    if (StrUtil.isBlank(shopJson)) {  
        // 不存在  
        return null;  
    }  
  
    // 命中,需要先将json反序列化为对象  
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);  
    // 获取到JSONObject对象,因为RedisData对象中data字段为Object类型  
    JSONObject jsonObject = (JSONObject) redisData.getData();  
    Shop shop = jsonObject.toBean(Shop.class);  
    LocalDateTime expireTime = redisData.getExpireTime();  
    // 判断是否过期  
    if (expireTime.isAfter(LocalDateTime.now())) {  
        // 未过期,直接返回店铺信息  
        return shop;  
    }  
    // 已过期,需要缓存重建  
    // 缓存重建,获取互斥锁  
    String lockKey = RedisConstants.LOCK_SHOP_KEY + id;  
    if (tryLock(lockKey)) {  
        // 获取锁成功,开启独立线程,实现缓存重建  
        CACHE_REBUILD_EXECUTOR.submit(() -> {  
            try {  
                this.saveShop2Redis(id, 20L);  
            } catch (Exception e) {  
                throw new RuntimeException(e);  
            } finally {  
                // 释放锁  
                unLock(lockKey);  
            }  
  
        });  
    }  
    return shop;  
}
 
public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {  
    // 查询店铺数据  
    Shop shop = getById(id);  
    // 模拟耗时  
    Thread.sleep(200);  
    // 封装逻辑过期时间  
    RedisData redisData = new RedisData();  
    redisData.setData(shop);  
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));  
    // 写入Redis  
    stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));  
}

总结

  • CAP权衡:
    • 互斥锁选择CP(一致性优先)
    • 逻辑过期选择AP(可用性优先)
  • 选型建议:
    • 金融交易等强一致性场景适用互斥锁
    • 商品展示等高并发场景适用逻辑过期

封装Redis工具类

this::getById等价于id2->getById(id2)

    public Result queryById(Long id) {  
        // 缓存穿透  
        // Shop shop = queryWithPassThrough(id);  
        Shop shop = cacheClient.queryWithPassThrough(RedisConstants.CACHE_SHOP_KEY,id,Shop.class,id2->getById(id2) ,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);  
  
        // 互斥锁解决缓存击穿  
        // Shop shop = queryWithMutex(id);  
  
        // 逻辑过期解决缓存击穿  
//        Shop shop = queryWithLogincalExpire(id);  
  
        if (shop == null) {  
            return Result.fail("店铺不存在");  
        }  
        return Result.ok(shop);  
    }
@Component  
@Slf4j  
public class CacheClient {  
  
    private final StringRedisTemplate stringRedisTemplate;  
    // 线程池用于缓存重建  
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);  
  
    public CacheClient(StringRedisTemplate stringRedisTemplate) {  
        this.stringRedisTemplate = stringRedisTemplate;  
    }  
  
    /**  
     * 尝试获取锁  
     *  
     * @param key  
     * @return  
     */    private boolean tryLock(String key) {  
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);  
        return BooleanUtil.isTrue(flag);  
    }  
  
    /**  
     * 释放锁  
     *  
     * @param key  
     */  
    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, ID> R queryWithPassThrough(  
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {  
        String key = RedisConstants.CACHE_SHOP_KEY + id;  
        // 从Redis中查询  
        String json = stringRedisTemplate.opsForValue().get(key);  
        if (StrUtil.isNotBlank(json)) {  
            // 存在,直接返回  
            return JSONUtil.toBean(json, type);  
        }  
        // 判断命中的是否为空值  
        if (json != null) {  
            // 此时为空字符串  
            return null;  
        }  
        // 不存在,根据id查询数据库  
        R r = dbFallback.apply(id);  
        // 判断结果  
        if (r == null) {  
            // 将空值写入redis  
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);  
            // 数据库不存在,返回错误  
            return null;  
        }  
        // 存在,写入redis  
        this.set(key, r, time, unit);  
        return r;  
    }  
  
    public <R, ID> R queryWithLogincalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {  
        String key = RedisConstants.CACHE_SHOP_KEY + id;  
        // 从Redis中查询  
        String json = stringRedisTemplate.opsForValue().get(key);  
        if (StrUtil.isBlank(json)) {  
            // 不存在  
            return null;  
        }  
  
        // 命中,需要先将json反序列化为对象  
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);  
        // 获取到JSONObject对象,因为RedisData对象中data字段为Object类型  
        JSONObject jsonObject = (JSONObject) redisData.getData();  
        R r = jsonObject.toBean(type);  
        LocalDateTime expireTime = redisData.getExpireTime();  
        // 判断是否过期  
        if (expireTime.isAfter(LocalDateTime.now())) {  
            // 未过期,直接返回店铺信息  
            return r;  
        }  
        // 已过期,需要缓存重建  
        // 缓存重建,获取互斥锁  
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;  
        if (tryLock(lockKey)) {  
            // 获取锁成功,开启独立线程,实现缓存重建  
            CACHE_REBUILD_EXECUTOR.submit(() -> {  
                try {  
                    // 查询数据库  
                    R r1 = dbFallback.apply(id);  
                    // 写入redis  
                    this.setWithLogicalExpire(key, r1, time, unit);  
  
                } catch (Exception e) {  
                    throw new RuntimeException(e);  
                } finally {  
                    // 释放锁  
                    unLock(lockKey);  
                }  
  
            });  
        }  
        return r;  
    }  
}