目录
一、基本概述
二、添加Redis缓存
? 2.1 添加缓存概述
? 2.2 Controller层
? 2.3? Service层
? 2.4 效果图
三、缓存更新策略
? 3.1 主动更新策略
? 3.2 操作缓存和数据库时三个问题的考虑
? ? 3.2.1 删除缓存还是更新缓存?
??? 3.2.2 如何保证缓存与数据库的操作同时成功或失败?
??? 3.2.3先操作缓存还是先操作数据库?
?? 3.3? 缓存更新策略最佳实践方案
四、实现商铺缓存与数据库的双写一致
4.1 修改Service层查询代码
4.2? 更新业务的Controller层
4.3 更新业务的Service层
五、缓存穿透
5.1 缓存穿透的解决思路
5.2? 解决商铺查询缓存穿透问题
? 5.2.1? 修改查询商铺Service层
5.3 总结
?? 5.3.1? 缓存穿透产生的原因是什么?
?? 5.3.2 缓存穿透解决方案有哪些?
六、缓存雪崩
?6.1雪崩问题及解决思路
七、缓存击穿
?7.1 缓存击穿问题及解决思路
7.2 基于互斥锁方式解决缓存击穿问题
7.2.1? Controller层代码
7.2.2? 修改Service层代码
7.3 基于逻辑过期方式解决缓存击穿问题
?7.3.1? 新增RedisData封装数据
7.3.2 Service层代码
八、缓存工具封装
两个set方法(方法一与方法二)
方法三? 缓存穿透工具
方法四 缓存击穿工具
一、基本概述
? 数据交换的缓冲区(称为Cache),是存储数据的临时地方,一般读写性能较高

二、添加Redis缓存
? 2.1 添加缓存概述

? 2.2 Controller层
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}
? 2.3? Service层
参考流程图
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
// 1.从redis查询商铺缓存 这个地方可以用hash,我们这里用String演示一下
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
// 2.判断Redis中是否存在
if(StrUtil.isNotBlank(shopJson)){
// 3.不为空,存在,直接返回(得将JSON字符串转化成对应的对象)
Shop shop = JSONUtil.toBean(shopJson,Shop.class);
return Result.ok(shop);
}
// 4.不存在,根据id查询数据库
Shop shop = getById(id);
// 5. 判断数据库中是否存在
if(shop == null){
// 6. 不存在返回错误
return Result.fail("店铺不存在");
}
// 7.存在,写入Redis
stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(shop));
// 8.返回
return Result.ok(shop);
}
?? 2.4 效果图
当我们刷新页面之后,有关商铺的信息就不会走数据库,而是进入Redis查找


三、缓存更新策略

业务场景:
?? 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存(这个一般是不会更改的)
?? 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
? 3.1 主动更新策略

? 3.2 操作缓存和数据库时三个问题的考虑
??? 3.2.1 删除缓存还是更新缓存?
- 更新缓存:每次更新数据库时都需要更新缓存,无效写入操作较多
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓
???? 综上所述:选择删除缓存
?????? 假如我们对数据库操作了100次,然后此时并没有请求访问Redis的缓存,那岂不是我们还要更新100Redis? 这显然很不合理,所以选择删除!当第一次数据库操作的时候我们就把对应的缓存删除,所以接下来99次数据库操作都与Redis无关,只需要静静的等待有人发起对应请求访问Redis,然后发现Redis并没有对应缓存,然后访问数据库返回数据并且更新缓存
??? 3.2.2 如何保证缓存与数据库的操作同时成功或失败?
????? 假如我们在更新数据库的时候,对数据库的操作成功了,但是对缓存的操作失败了,这是很不合理的
????? 那怎么保证?
- 单体系统:将缓存与数据库操作放在一个事务
- 分布式系统:利用TCC等分布式事物方案
???
?? ?? 3.2.3先操作缓存还是先操作数据库?
??????? 还要考虑线程安全的问题,在多线程并发的时候这两个操作可能有多个线程来回穿插执行,那这样谁先操作谁后操作就会造成不一样的线程安全问题
- ?? 先删除缓存,再操作数据库
- ?? 先操作数据库,再删除缓存
??? 答案是这两种都可以
上述的两种方案都会存在问题,如下图所示,都有很大的可能造成缓存与数据库不一致的问题
左侧的可能性出现比较高,右侧的可能性出现比较低 ,但是都有问题

?? 3.3? 缓存更新策略最佳实践方案

四、实现商铺缓存与数据库的双写一致
修改ShopController中的业务逻辑,满足下面的需求:
- 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存并设置超时时间
- 根据id修改店铺时,先修改数据库,再删除缓存
4.1 修改Service层查询代码
比之前多了一个设置超时时间
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
// 1.从redis查询商铺缓存 这个地方可以用hash,我们这里用String演示一下
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
// 2.判断Redis中是否存在
if(StrUtil.isNotBlank(shopJson)){
// 3.不为空,存在,直接返回(得将JSON字符串转化成对应的对象)
Shop shop = JSONUtil.toBean(shopJson,Shop.class);
return Result.ok(shop);
}
// 4.不存在,根据id查询数据库
Shop shop = getById(id);
// 5. 判断数据库中是否存在
if(shop == null){
// 6. 不存在返回错误
return Result.fail("店铺不存在");
}
// 7.存在,写入Redis
stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(shop),30, TimeUnit.MINUTES);
// 8.返回
return Result.ok(shop);
}
4.2? 更新业务的Controller层
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
// 写入数据库
return shopService.update(shop);
}
4.3 更新业务的Service层
@Override
@Transactional //如果报异常的话,整个记得回滚
public Result update(Shop shop) {
if(shop.getId() == null){
return Result.fail("店铺id不能为空");
}
// 1.更新数据库
updateById(shop);
// 2.删除缓存
stringRedisTemplate.delete("cache:shop:"+shop.getId());
return Result.ok();
}
五、缓存穿透
?? 5.1 缓存穿透的解决思路
?? 缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
?? 简洁说就是:当用户一直请求一个不存在的id时,从客户端向Redis请求,发现没有再向数据库找那个请求,发现也没有,然后将数据库中查询的null信息返回给客户端,如果被别有用心之人一直请求,会给我们的数据库造成很大的压力
? 解决方案
??????? 优点:实现简单,维护方便
??????? 缺点:额外的内存消耗,可能造成数据短期不一致的问题
额外的内存消耗是可以理解的,这样就在Redis中存储了很多没有用的垃圾null信息(解决就是设置消失的时间)
短期不一致时因为我们在往Redis中缓存对应的null时,设置了一个时间,在这个时间内只要客户端访问就会访问Redis,但是万一这段时间内我们数据库改变了,但是Redis缓存没有改变,这样就造成了短期数据不一致(解决方案是每次更新数据库时更新缓存)

其实是一个算法
?????? 优点:内存占用少,没有多余key
?????? 缺点:实现复杂,存在误判的可能(可能发生穿透)

?5.2? 解决商铺查询缓存穿透问题
? 与之前的区别就是当在数据库中查询到对应id不存在的时候不再返回404.而是将空值写入Redis中
? 还有就是缓存命中的时候也有区别,我们需要判断一下是否是空值,如果是空值直接结束

? 5.2.1? 修改查询商铺Service层
下面的空值值得都是空白字符串
?? 与之前的区别就是在shop==null的时候将空值写入到Redis
值得说明的是if(StrUtil.isNotBlank(shopJson))? 只有里面是真正字符串的时候才是true
另一个区别就是在if(StrUtil.isNotBlank(shopJson))? 语句之后又添加了一个判断是不是空值,如果是空值就查询数据库,不是空值就是空白字符串就查询Redis防止穿透

@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
// 1.从redis查询商铺缓存 这个地方可以用hash,我们这里用String演示一下
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
// 2.判断Redis中是否存在
if(StrUtil.isNotBlank(shopJson)){
// 3.不为空,存在,直接返回(得将JSON字符串转化成对应的对象)
Shop shop = JSONUtil.toBean(shopJson,Shop.class);
return Result.ok(shop);
}
// 缓存穿透这里得再添加一个看看是否是空值
if( shopJson !=null){
// 不能等于null,就一定是一个空字符串
return Result.fail("店铺不存在");
}
// 4.不存在,根据id查询数据库
Shop shop = getById(id);
// 5. 判断数据库中是否存在
if(shop == null){
// 6. 不存在返回错误,并且将空值写入Redis 此时存放Redis就是空值
stringRedisTemplate.opsForValue().set("cache:shop:"+id,"",2, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
// 7.存在,写入Redis
stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(shop),30, TimeUnit.MINUTES);
// 8.返回
return Result.ok(shop);
}
?5.3 总结
?? 5.3.1? 缓存穿透产生的原因是什么?
???? 用户查询的数据在Redis与数据库中都没有
?? 5.3.2 缓存穿透解决方案有哪些?
- ?????? 缓存空白字符串
- ?????? 布隆过滤
- ?????? 增强id复杂度,避免被猜测id规律
- ?????? 做好数据的基础格式校验
- ?????? 加强数据权限校验
- ?????? 做好热点参数的限流
六、缓存雪崩
?6.1雪崩问题及解决思路
指同一时间段大量的缓存key同时失效或者Redis服务宕(dang)机,导致大量请求到达数据库带来巨大压力

?? 解决方案
???????? 在批量导入缓存的时候添加随机TTL,这样就避免了TTL同时到期(分散了)
???????? 这是针对宕机的解决方案
?????? ? ? 比如说拒绝部分服务访问数据库
????????? 在Ngix中添加缓存,Redis崩了之后,还有其他的缓存方式
?????????
七、缓存击穿
?7.1 缓存击穿问题及解决思路
? 也叫热点Key问题,就是一个高并发访问并且缓存重建业务较复杂的key突然失效,无数的请求访问会在瞬间给数据库带来巨大的冲击,如下图所示

常见的解决方案
??????? 此方案利用锁的方式,不让所有的请求都创建缓存数据,只让一个就可以了,但是性能较差
??????? 不设置TTL,那我们不设置TTL怎么说明过期? 在VALUE中重新加一个字段来当做过期时间
具体实现过程如下图所示

优缺点对比

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

7.2.1? Controller层代码
?? 与之前一模一样
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}
7.2.2? 修改Service层代码
@Override
public Result queryById(Long id) {
// 缓存穿透
// Shop shop = queryWithPassThrough(id);
// 利用互斥锁解决缓存击穿
Shop shop = queryWithMutex(id);
if(shop ==null){
return Result.fail("店铺不存在");
}
// 8.返回
return Result.ok(shop);
}
/**
* 缓存穿透
* @param id
* @return
*/
public Shop queryWithMutex(Long id){
// 1.从redis查询商铺缓存 这个地方可以用hash,我们这里用String演示一下
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
// 2.判断Redis中是否存在
if(StrUtil.isNotBlank(shopJson)){
// 3.不为空,存在,直接返回(得将JSON字符串转化成对应的对象)
Shop shop = JSONUtil.toBean(shopJson,Shop.class);
return shop;
}
// 缓存穿透这里得再添加一个看看是否是空值
if( shopJson !=null){
// 不能等于null,就一定是一个空字符串
return null;
}
Shop shop = null;
try {
// 4.开始实现缓存重建
// 4.1获取互斥锁 锁的key和缓存key不一样
boolean isLock = tryLock("lock:shop"+id);
// 4.2判断是否获取成功
if(!isLock){
// 4.3失败,则休眠并重试 重试就是重新执行查询动作,使用递归
Thread.sleep(50);
return queryWithMutex(id);
}
// 4.4.成功,根据id查询数据库
shop = getById(id);
// 5. 判断数据库中是否存在
if(shop == null){
// 6. 不存在返回错误,并且将空值写入Redis 此时存放Redis就是空值
stringRedisTemplate.opsForValue().set("cache:shop:"+id,"",2, TimeUnit.MINUTES);
return null;
}
// 7.存在,写入Redis
stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(shop),30, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 8.释放互斥锁
unlock("lock:shop"+id);
}
// 9.返回
return shop;
}
// 定义两个关于锁的方法
// 获取锁的代码
private boolean tryLock(String key){
// 这段代码执行完成之后返回的应该是0或者1,在这里帮我们封装成了Boolean
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);
// 因为封装成了Boolean,这里我们不能直接返回,直接返回会拆箱然后可能出现空指针,所以使用一个工具类
return BooleanUtil.isTrue(flag);
}
// 释放锁
private void unlock(String key){
stringRedisTemplate.delete(key);
}
7.3 基于逻辑过期方式解决缓存击穿问题
?需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题
?过期时间由程序员自己判断,这种方式Redis中是一直存在的,除非人工删除(一般活动结束的时候人工删除)

?7.3.1? 新增RedisData封装数据
?将来我们存储Redis中的数据就是下面这个样子的
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
7.3.2 Service层代码
@Override
public Result queryById(Long id) {
// 缓存穿透
// Shop shop = queryWithPassThrough(id);
// 利用互斥锁解决缓存击穿
// Shop shop = queryWithMutex(id);
// 利用逻辑过期解决缓存击穿
Shop shop = queryWithLogicalExpire(id);
if(shop ==null){
return Result.fail("店铺不存在");
}
// 8.返回
return Result.ok(shop);
}
// 创建一个线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 利用逻辑过期解决缓存击穿
* @param id
* @return
*/
public Shop queryWithLogicalExpire(Long id){
// 1.从redis查询商铺缓存 这个地方可以用hash,我们这里用String演示一下
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
// 2.判断Redis中是否存在
if(StrUtil.isBlank(shopJson)){
// 未命中 返回空
return null;
}
// 4.命中需要先把json反序列化 因为我们当时存储逻辑过期的时候就是存储的RedisData类型
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
// 因为我们在RedisData中设置data属性就是Object类型,所以当我们取的时候程序并不知道我们是什么类型,我们加一个强转就好了
JSONObject data = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
// 5.1未过期,直接返回商铺信息
return shop;
}
// 5.2 过期 缓存重建
// 6.缓存重建
// 6.1 获取互斥锁
String lockKey ="lock:shop:"+id;
boolean isLock = tryLock(lockKey);
// 6.2 判断是否获取锁成功
if(isLock){
// 6.3 成功 开启独立线程实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try{
// 重建缓存
this.saveShop2Redis(id,30L);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 7.返回
return shop;
}
// 将店铺信息保存到Redis中
private void saveShop2Redis(Long id,Long expireSeconds){
// 1.查询店铺数据
Shop shop = getById(id);
// 2.封装成逻辑过期
RedisData redisData = new RedisData();
redisData.setData(shop);
// plusSeconds(expireSeconds) 在当前时间的基础上增加多少秒
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 3.写入Redis
stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(redisData));
}
// 定义两个关于锁的方法
// 获取锁的代码
private boolean tryLock(String key){
// 这段代码执行完成之后返回的应该是0或者1,在这里帮我们封装成了Boolean
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);
// 因为封装成了Boolean,这里我们不能直接返回,直接返回会拆箱然后可能出现空指针,所以使用一个工具类
return BooleanUtil.isTrue(flag);
}
// 释放锁
private void unlock(String key){
stringRedisTemplate.delete(key);
}
八、缓存工具封装
封装的时候降低我们以后使用难度
前两个是存,后两个是取
方法一和方法三是针对普通的缓存,解决穿透
方法二和方法四是针对热点key的,解决击穿

@Slf4j
@Component
public class CacheClient {
// @Resource 也可以用注解注入
private final StringRedisTemplate stringRedisTemplate;
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
}
?两个set方法(方法一与方法二)
//
public void set(String key, Object value, Long time, TimeUnit unit){
// 我们往Redis存的时候不能是Object类型,我们需要把Object序列化为JSON字符串
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(value),time,unit);
}
// 逻辑过期
public void setWithLogincalExpire(String key, Object value,Long time,TimeUnit unit){
// 设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
// 我们往Redis存的时候不能是Object类型,我们需要把Object序列化为JSON字符串
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
}
方法三? 缓存穿透工具
// R 代表Return 返回值 因为返回值不确定
// Class<R> type 泛型的推断 告诉类型便会返回什么类型
public <R, ID> R queryWithPassThrough(String keyPrefis, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefis + id;
// 1.从redis查询商铺缓存 这个地方可以用hash,我们这里用String演示一下
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断Redis中是否存在
if (StrUtil.isNotBlank(json)) {
// 3.不为空,存在,直接返回(得将JSON字符串转化成对应的对象)
return JSONUtil.toBean(json, type);
}
// 缓存穿透这里得再添加一个看看是否是空值
if (json != null) {
// 不能等于null,就一定是一个空字符串
return null;
}
// 4.不存在,根据id查询数据库
R r = dbFallback.apply(id);
// 5. 判断数据库中是否存在
if (r == null) {
// 6. 不存在返回错误,并且将空值写入Redis 此时存放Redis就是空值
stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
return null;
}
// 7.存在,写入Redis
this.set(key,r,time , unit);
// stringRedisTemplate.opsForValue().set(key + id, JSONUtil.toJsonStr(r),time , unit);
// 8.返回
return r;
}
修改之前的Service调用
@Resource
private CacheClient cacheClient;
@Override
public Result queryById(Long id) {
// 缓存穿透
Shop shop = cacheClient.queryWithPassThrough("cache:shop:",id,Shop.class,id2->getById(id2),30L,TimeUnit.MINUTES);
if(shop ==null){
return Result.fail("店铺不存在");
}
// 8.返回
return Result.ok(shop);
}
方法四 缓存击穿工具
使用这个的前提是把缓存已经缓存到Redis中(提前存入数据),否则会出现空指针异常,如下图所示,下图只存入了一个

?
// 创建一个线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 利用逻辑过期解决缓存击穿
* @param id
* @return Function<ID,R> 参数是id,返回值是R对应的类型
*/
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查询商铺缓存 这个地方可以用hash,我们这里用String演示一下
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断Redis中是否存在
if(StrUtil.isBlank(json)){
// 未命中 返回空
return null;
}
// 4.命中需要先把json反序列化 因为我们当时存储逻辑过期的时候就是存储的RedisData类型
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
// 因为我们在RedisData中设置data属性就是Object类型,所以当我们取的时候程序并不知道我们是什么类型,我们加一个强转就好了
JSONObject data = (JSONObject) redisData.getData();
R r = JSONUtil.toBean(data, type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
// 5.1未过期,直接返回商铺信息
return r;
}
// 5.2 过期 缓存重建
// 6.缓存重建
// 6.1 获取互斥锁
String lockKey ="lock:shop:"+id;
boolean isLock = tryLock(lockKey);
// 6.2 判断是否获取锁成功
if(isLock){
// 6.3 成功 开启独立线程实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try{
// 重建缓存分为两个步骤
// 查询数据库
R r1 = dbFallback.apply(id);
// 写入Redis
this.setWithLogincalExpire(key,r1,time,unit);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 7.返回
return r;
}
// 定义两个关于锁的方法
// 获取锁的代码
private boolean tryLock(String key){
// 这段代码执行完成之后返回的应该是0或者1,在这里帮我们封装成了Boolean
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);
// 因为封装成了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 = cacheClient.queryWithPassThrough("cache:shop:",id,Shop.class,id2->getById(id2),30L,TimeUnit.MINUTES);
// 利用互斥锁解决缓存击穿
// Shop shop = queryWithMutex(id);
// 利用逻辑过期解决缓存击穿
Shop shop = cacheClient.queryWithLogicalExpire("cache:shop:",id,Shop.class,id2->getById(id2),30L,TimeUnit.MINUTES);
if(shop ==null){
return Result.fail("店铺不存在");
}
// 8.返回
return Result.ok(shop);
}
|