我是靠谱客的博主 糊涂绿草,最近开发中收集的这篇文章主要介绍Redis 基础 - 用Redis查询商户信息请参考本文摘要Redis缓存简单应用例子1:用Redis查询商户信息缓存更新策略缓存更新策略的业务场景操作缓存和数据库时有三个问题需要考虑例子2:读、写时数据库与缓存的同步编辑例子1中的代码用Redis查询商户业务时其他的相关问题例子3:用“缓存空对象”的方式给查询商户时处理缓存穿透的风险例子4:基于互斥锁方式解决缓存击穿问题例子5:基于逻辑过期方式解决缓存击穿问题,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

请参考

Redis基础 - 基本类型及常用命令
Redis基础 - Java客户端
Redis 基础 - 短信验证码登录

本文摘要

本文是Redis缓存的简单应用,以根据id查询商户信息为例,先简单介绍基本使用。接着根据缓存更新策略对代码实例进行简单修改。最后再简单介绍缓存穿透缓存雪崩缓存击穿的问题以及代码修改。

Redis缓存简单应用

缓存是存储数据的临时地方,一般读写性能较好
前端请求时,先到达Redis。查看有无数据,有则直接返回。没有时才会去数据库查。能大大减轻数据库的压力。当然,查数据库后,把结果也要存入Redis里,以供下一次的请求命中。

例子1:用Redis查询商户信息

业务流程

在查询商户信息例子中,前端向服务端提供商户id,服务端用id去Redis查,若命中,就把信息返回给前端。若没命中,就去查数据库。若数据库中有商户,就把查出来的商户信息存入Redis。
使用到的工具类:StrUtil,JSONUtil(hutool)

代码示例

ShopController.java

@RestController
@RequestMapping("/shop")
public class ShopController {
@Resource
public IShopService shopService;
// 查询商户信息
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}
}

IShopService.java

public interface IShopService extends IService<Shop> {
Result queryById(Long id);
}

ShopServiceImpl.java

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
// 1,根据商户id,从Redis查询商户缓存
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
// 2,判断缓存中商户信息是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3,存在时,把JSON字符串转成对象后返回
//(网友1:直接传JSON给前端不行吗?)
// JSONUtil cn.hutool.json
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 4,缓存中该商户信息不存在时,根据id查询数据库
Shop shop = getById(id);
// 5,若数据库里也不存在,则返回错误
if (shop == null) return Result.fail("无此商户');
// 6,若数据库里存在时,转成JSON字符串后写入Redis缓存,以备下一个线程来执行时成功命中
stringRedisTemplate.opsForValue().set("cache:shop:" + id , JSONUtil.toJsonStr(shop));
// 7,返回数据给客户端
return Result.ok(shop);
}
}

当然,上面的代码仅仅是简单的使用例子。但上面的代码过于单调,需要加强一下。

缓存更新策略

内存淘汰

  • 说明:不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时更新缓存。
  • 一致性:差 因为数据库更新了,Redis不一定把相关数据给及时干掉
  • 维护成本:无 不用管,Redis自己看着办

超时剔除

  • 说明:给缓存数据添加TTL时间,到期后自动删除缓存。下次查询时更新缓存。
  • 一致性:一般 毕竟数据库更新了,若还没超时,可能就暂时不一致了
  • 维护成本:低 添加过期时间即可

主动更新【推荐】

  • 说明:编写业务逻辑,在修改数据库的同时,更新缓存。
  • 一致性:好 好并不等于完全保证一致,因为程序总会有一些意外,所以只能说好
  • 维护成本:高 需要自己编码

缓存更新策略的业务场景

低一致性需求

使用内存淘汰机制。比如分类。

高一致性需求

使用主动更新策略,(由于主动更新并不能完全保证100%没毛病,所以最好是)并以超时剔除作为兜底方案(万一主动更新失败了,也能用超时剔除来干掉)。

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

删除缓存还是更新缓存?

  • 更新缓存:每次更新数据库都更新缓存,如果长期没人来访问的话,无效的写操作可能会较多。
  • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存。【推荐】

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

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

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

  • 先删除缓存,再操作数据库 线程安全问题发生的概率较高

《正常的情况》
比如缓存和数据库里的值都是10,在多线程情况下,比如有线程1和线程2,此时线程1根据“先删缓存,再操作数据库”,线程1会先去操作缓存,删掉后缓存里的10没了,然后更新数据库,数据库变成了20。此时线程2来查询缓存,由于未命中,所以直接查数据库,查到20,然后把20写到缓存,这时候缓存也变成了20,数据库数据和缓存数据保持了一致。

《线程安全问题发生的情况》
比如,假设目前缓存和数据库都是10,线程1来执行删除缓存,假设由于没有加锁,所以线程1在执行缓存删除时,缓存变成了0,但是在更新数据库之前的业务比较复杂,所以花点时间时,此时线程2趁虚而入,她干的事是查询了缓存,由于线程1刚刚干掉了缓存,所以线程2未命中,就去查数据库,因为线程2是趁虚而入的,此时线程1还没有更新到数据库,所以线程2查到的是旧的值即10,紧接着写入到缓存,所以缓存变成了10,这个时候,线程1终于开始执行更新数据库的操作了,执行后数据库值变成了20,于是,数据库数据和缓存数据产生了不一致的问题。

  • 先操作数据库,再删除缓存 线程安全问题发生的概率较低【推荐】

《正常的情况》
假设还有有线程1和2,缓存和数据库数据都是10。比如线程2先来执行,所以他操作了数据库,数据库值变成了20,然后删除缓存,即缓存变成没有了。之后线程1过来查询缓存时由于未命中,所以去数据库查并把结果20加到缓存,即数据保持了一致。

《线程安全问题发生的情况》
比如缓存和数据库目前都10,比如,线程1过来查缓存数据,但遇到了特殊情况,恰好缓存失效了,所以线程1查缓存未命中,就去查数据库,然后要把得到的10写入缓存,但是正在此时,线程2进来了,线程2来了也会第一步更新数据库,数据库因此被改成了20,然后线程2会去删除缓存,但缓存已经被删掉,所以等于没删,线程2的任务结束,紧接着,线程1又开始继续执行了,她做的是她的第二步即写缓存,由于线程1取得的数据是10,所以写缓存后,缓存变成了10,导致缓存和数据库数据不一致。

她的发生几率高不高呢?第一个条件是两个线程并行执行,第二个条件是线程1来查询的时候刚好缓存失效,然后恰好失效的同时,她查完了数据库后,要去写缓存,写缓存的操作是微秒级别,就在这个微秒的范围内,突然来了个线程2,她先去更新数据库,更新数据库的速度相对来说比写缓存速度慢,更新完了还要去删缓存,然后才轮到线程1去执行,这个非常短的时间内,线程2要完成那些并不容易,所以这种发生几率是相对较低的,但不能说不会发生。

所以这两种方式都有可能发生线程安全问题,但第二个方案相对来说出现的可能性比方案1低,所以一般采用第二个方案,即先操作数据库再删缓存

网友1:为什么不能枷锁?
网友2:枷锁就降低查询效率了。
网友3:枷锁就相当于做HS只有一条队。

例子2:读、写时数据库与缓存的同步编辑例子1中的代码

代码示例1

根据id查询商户时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间(如上所述,以超时剔除作为兜底方案)。

// 6,存在时,转成JSON字符串后写入Redis,并设置有效期为30分钟
stringRedisTemplate.opsForValue().set("cache:shop:" + id , JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);

代码示例2

根据id修改商户时,先修改数据库,再删除缓存。

ShopController.java

...
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
return shopService.update(shop);
}
...

IShopService.java

...
Result update(Shop shop);
...

ShopServiceImpl.java

...
@Override
@Transactional // 单体项目时,让操作数据库和操作缓存在同一个事务里。
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null) return Result.fail("商户id不能为空");
// 1,更新数据库
updateById(shop);
// 2,删除缓存
stringRedisTemplate.delete("cache:shop:" + id);
return Result.ok();
}
...
网友1:Redis的事务和数据库的事务能够被一个注解同时管理吗?
网友2:spring的事务是基于aop实现了,控制的是方法,不要和数据库的事务搞混了。

用Redis查询商户业务时其他的相关问题

缓存穿透

指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,所以这些请求一定都会到达数据库。用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力。

常见的解决方案有三种

1)缓存空对象【推荐】

比如,有人用id请求数据,缓存和数据库都没有,为了防止再用这个id调用,就直接把null给缓存到Redis,以后这个id就达不到数据库了。
优点:

  • 实现简单,维护方便

缺点:

  • 额外的内存消耗其实加比较短的TTL在一定程度上能解决这个问题

  • 可能造成短期的不一致比如,用户请求的id都不存在,所以给Redis存了null,就在此时,真的给这个id插入了一条数据,这样在TTL范围内,数据库与Redis数据不一致。当然,也可以在生成数据时再把这个插入到缓存中覆盖null也多少能解决

2)布隆过滤

她是在客户端和Redis之间放的,当请求来时,布隆过滤器就会检查这个数据存在与否,如果不存在,就直接拒绝继续放行,如果布隆过滤器发现这个数据存在时,才会放行,让她继续去Redis或数据库查,即放行后剩下的逻辑与之前一样。但布隆过滤器并不是百分百准确的,比如她说不存在,那肯定是不存在,但她说存在,那么可能是真存在或可能是不存在。即,使用布隆过滤器还是有一定的穿透风险的。

优点:

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

缺点:

  • 实现复杂
  • 存在误判可能

3)主动防止

上面的两种方案是被动的方案,即产生穿透之后的方案。也可以主动的去提前防止穿透,比如增强id的复杂度,避免被猜测id规律。也可以做好基础的格式校验看她是否符合id的规范。还可以加强用户权限校验,比如一些业务不是任何人都能访问,也能多多少少防止。也可以做好热点参数的限流,以此来避免给数据库带来的压力。

例子3:用“缓存空对象”的方式给查询商户时处理缓存穿透的风险

在查询商户信息的方法中,要改两处地方,一个是查redis时判断值是不是“” 因为为了防止缓存穿透会设置成空。还有一个是数据库里也没有时,要往Redis干入空值“”。

代码示例

ShopServiceImpl.java

@Override
public Result queryById(Long id) {
// 1,根据商户ID,从Redis查询商户缓存
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
// 2,判断缓存中的商户信息是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3,存在时,把JSON字符串转成对象后返回(网友1:直接传JSON给前端不行吗?)
// JSONUtil cn.hutool.json
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// ----------------处理 缓存穿透 start ------------------
/** 判断命中的是不是空值“”,由于StrUtil.isNotBlank()这个函数null ""都返回false,所以要额外再判断**/
// 因为前面已经判断了是不是不是空,所以这里只有两种情况,null和"",所以不等于null,就是等于""。
if (shopJson != null) {
// 返回一个错误信息
return Result.fail("商户信息不存在。");
}
// ----------------处理 缓存穿透 end ------------------
// 4,商户信息不存在时,根据id查询数据库
Shop shop = getById(id);
// 5,数据库中商户信息不存在时,返回错误
if (shop == null) {
// ----------------处理 缓存穿透 start ------------
/** 往Redis干入空值“”,有效期设置为2分钟 **/
stringRedisTemplate.opsForValue().set("cache:shop:" + id , "", 2L, TimeUnit.MINUTES);
// ----------------处理 缓存穿透 end ------------
return Result.fail("无此商户');
}
// 6,存在时,转成JSON字符串后写入Redis
stringRedisTemplate.opsForValue().set("cache:shop:" + id , JSONUtil.toJsonStr(shop));
// 7,返回
return Result.ok(shop);
}

缓存雪崩

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

解决方案:

  • 给不同的key的TTL添加随机值(比如设置30分钟时,30的后面加上随机数)
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存击穿

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

常见的解决方案有两种:

1)互斥锁
比如线程1来了,未命中,所以要重建(从数据库查了后要往Redis存值),比如这个重建过程较复杂,高并发下其他一堆线程也过来干同样的事,导致数据库压力巨大。所以线程1查询缓冲时,若未命中,就去获取互斥锁,线程1干完了整个业务,把业务数据干入缓冲之后,再释放锁。高并发下,其他线程就算来了,就算他们也在Redis里未命中,也获取不到互斥锁,那么他会重试去获取,为了不让他不断获取,一般会让她休眠一会再重试,等线程1的任务干完了,其他线程就能在Redis查询命中了。由于线程1玩的时候,其他的大量的线程都得等着,所以这个方法性能会差一点。所以有了第二种方案。

2)逻辑过期
逻辑过期就是不是真过期,即不设置TTL,而是逻辑上的过期。

比如,存了下面的内容:

KEYVALUE
user:1{name:“Jack”, age:21, expire:152141223}

这个key没有TTL,没有过期时间,意味着这个key一旦存储到Redis,他就会永不过期。加上若配置合适的内存淘汰策略的话,理论上讲,只要这个key写入到Redis,以后永远都能查到,所以不会出现未命中的情况。往往这种key是活动时添加进去的,所以添加上逻辑过期时间后,活动结束时移除就可以了。所以任何线程来访问时,理论上是都会命中的,所以他唯一要判断的是,逻辑时间是否已过期,如果已经过期了,说明这个数据是老数据,需要更新,接下来肯定还要去重建缓存,为了避免其他线程也来干同样的事,她也要获取互斥锁,但前面的互斥锁就是因为都在等待导致性能下降,这里却又来了互斥锁,所以在这里为了避免获取锁以后等待时间过长,她拿到锁后,她不是自己去构建,而是会开启一个独立的新线程,让新线程去做查询数据缓存重建、重置逻辑过期时间等,然后新线程会释放锁,也就是说这个耗时比较大的业务不再是线程1自己做,而是交给另外一个线程做了,那此时线程1干啥呢,线程1直接返回旧数据,而后线程3来的时候,假设此时新线程还没执行完,所以线程3查询缓存后,发现逻辑过期时间也是过期的,那么线程3也要获取互斥锁,由于新线程还没释放锁,所以线程3获取锁一定失败,失败后不像之前的“互斥锁方式”那样等待并不断重试,而是线程3会认为原来是有人已经在帮她去更新着呢,所以就不管了,直接返回旧数据,即不争不抢,直到刚才的新线程执行完,如果线程4是这个时候才来读取的话,那么拿到的就是最新的数据了。

所以这两种方案都在解决缓存重建这一期间的并发问题。所以互斥锁的实现方案是在缓存重建的这一段时间内让一些并发的线程串行执行或者互相等待,从而确保安全,虽然实现了数据的一致性,但导致了性能的下降,甚至阻塞过程中不可用。

而逻辑过期这种方案是在缓存重建的这段时间内,保证了可用性,大家来了都可以访问,只不过我访问得到的是旧的数据,与数据库不一致,所以失去了一致性。

也就是说这两个一个是选择了一致性一个是选择了可用性。根据自己更看重一致性还是更看重可用性,从而去选择不同的方案。

例子4:基于互斥锁方式解决缓存击穿问题

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

互斥锁方式业务流程

前端提交商户id,服务端先在Redis查,并判断是否命中,若命中,直接返回数据,若没命中,先尝试获取互斥锁,并判断是否获取了锁,若没能拿到,即其他线程正在执行中时,此时休眠一会儿,休眠完了之后,重新查Redis,看看别人是否更新完。若刚才拿到了锁,说明你是第一个来的,就可以去查数据库,并去做缓存重建,最后释放互斥锁,然后返回结果。

这里需要注意的是,这里的锁不是我们平常用的那个锁,如synchronizedlock之类的,这两种锁是当你拿到锁了之后,可以执行,没拿到呢,就会一直等待。而我们这里的锁是拿到锁和不拿到锁的执行逻辑是我们自己去自定义的,所以不能使用synchronized或lock这种方式。这里要采用自定义的锁。即只有一个人能成功,其他人都失败。这里暂时可以参考Redis的setnx命令,即没有key的时候可以设置,一旦有key了,就无法再设置值。其特点类似于互斥锁。释放锁的类似方法是把这个key给del了就行,那么其他人就可以给他赋值了。在使用setnx的时候,往往都会加有效期,因为如果因为程序问题而没能及时执行del key的话,这个就永远基本锁住了。

代码示例

ShopServiceImpl.java

// 修改业务逻辑
@Override
public Result queryById(Long id) {
// 互斥锁解决缓存的击穿
// 1,根据商户ID从Redis查询商户缓存
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
// 2,判断缓存中商户信息是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3,存在时,把JSON字符串转成对象后返回(网友1:直接传JSON给前端不行吗?)
// JSONUtil cn.hutool.json
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
/** 判断命中的是不是空值“”,由于StrUtil.isNotBlank()这个函数null ""都返回false,所以要额外判断**/
if (shopJson != null) {// 因为前面已经判断了是不是不是空,所以这里只有两种情况,null和"",所以不等于null,就是等于""。
// 返回一个错误信息
return Result.fail("商户信息不存在。");
}
// 4,实现缓存重建
// 4.1 获取互斥锁
// 用于互斥锁的key
String lockkey = "lock:shop:" + id;
try {
boolean isLock = tryLock(lockkey);
// 4.2 判断是否获取互斥锁成功
if (!isLock) {
// 4.3 失败时,休眠并重试
Thread.sleep(50);// 比如休眠50毫秒
return queryById(id);// 重试,即递归
}
// 4.4,成功时,根据id查询数据库
/* [注意:获取锁成功应该再次检测redis缓存是否存在,做DoubleCheck。
如果存在则无需重建缓存。因为成功的可能是第N个线程,即比如其他线程之前已完成重建。]
这里没有做DoubleCheck。*/
Shop shop = getById(id);
// 5,不存在时,返回错误
if (shop == null) {
/** 往Redis干入空值“”,有效期设置为2分钟 **/
stringRedisTemplate.opsForValue().set("cache:shop:" + id , "", 2L, TimeUnit.MINUTES);
return Result.fail("无此商户');
}
// 6,存在时,转成JSON字符串后写入Redis
stringRedisTemplate.opsForValue().set("cache:shop:" + id , JSONUtil.toJsonStr(shop));
}catch (InterruptedException e) {
throw new RuntimeException();
}finally {
// 7,释放互斥锁
unlock(lockKey);
}
// 8,返回
return Result.ok(shop);
}
// 定义两个方法,分别是获取锁和释放锁
private boolean tryLock(String key) {
// 值可以随便设置,这里就设置为“1”
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// 不要直接返回Boolean,使用工具类返回
return BooleanUtil.isTure(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}

例子5:基于逻辑过期方式解决缓存击穿问题

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

逻辑过期方式业务流程

前端提交商户id,后端从Redis查询缓存,理论上,这个缓存不会出现未命中的情况,因为key是不会过期的,所以可以认为一旦这个key添加到了缓存里面,他应该是会永久存在的,除非活动结束人工删除,而这种热点Key往往是一些参加活动的商品或其他东西,我们会提前给他加入缓存,在那个时候设置一下逻辑过期时间,所以理论上讲,所有的这些热点key都会提前添加好,都会一直存在直到活动结束。因此去Redis查询的时候,不用去判断有没有命中,如果说查的真的没有的话,只能说明一个问题,这个商品不在活动中了,不属于一个热点key了。所以,为了健壮性考虑,还是要判断有没有命中。如果真没命中,就不用去做击穿啊或穿透啊这种解决方案,直接返回空就行。核心逻辑是,她命中了之后(默认),要判断她有没有过期(即判断逻辑过期时间),如果未过期,那么这个数据说明是可用的,直接返回即可。若这个数据过期了,需要去做缓存重建,但不是任何人来了都可以重建,即先尝试去获取互斥锁,然后判断获取互斥锁成功与否,如果获取失败,意味着在你之前已经有人尝试着去更新缓存了,那就直接把旧的数据返回,这样的话性能会比较好。若获取锁成功,开启独立线程,让独立线程去执行重建,然后她自己就返回旧的数据。独立线程会去查数据库,并把最新数据写入Redis,并设置逻辑过期时间,然后释放互斥锁。

代码示例

逻辑过期时间怎么设置,可以在Shop.java中添加,但这样做的话得修改Shop的源代码,所以也可以单独建立个RedisData.java,里面放逻辑过期时间。

如何让Shop.java具备这个属性呢?可以让她继承RedisData,但这样还是会去修改Shop的源代码。

还有一种方法是在RedisData里添加一个属性,即RedisData自己带有过期时间,并且带有data即你想存进Redis的数据,比如这里是Shop。这种方案完全不需要去修改实体类的源代码。

RedisData.java

@Data
public class RedisData {
// LocalDateTime : 同时含有年月日时分秒的日期对象
private LocalDateTime expireTime;
private Object data;
}

另外热点key的缓存是提前要导入进去的,实际开发中,可能会在后台管理系统直接人工操作。由于这个例子中,没有后台系统,所以暂时用单元测试方式把热点key的数据干入到缓存中。

ShopServiceImpl.java

// 添加代码 expireSeconds是秒数,比如10秒
public void saveShopToRedis(Long id, Long expireSeconds) {
// 1,查询店铺数据
Shop shop = getById(id);
// 2,封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDataTime.now().plusSeconds(expireSeconds));// 当前时间+expireSeconds秒
// 3,写入Redis
stringRedisTemplate.opsForValue().set("cache:shop:"+id, JSONUtil.toJsonStr(redisData));// 没添加TTL
}

然后再用单元测试执行这段代码,模拟预先给Redis存了热点key。这里就算了。

// 修改业务逻辑
// 线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);// 10个线程
@Override
public Result queryById(Long id) {
// 互斥锁解决缓存的击穿
// 1,从Redis查询商户缓存
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
// 2,判断是否存在(未命中就返回)
if (StrUtil.isBlank(shopJson)) {
//3,不存在就返回null
return null;
}
// 4,若命中,为了看过期时间,把JSON反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
// 取得商户
/* 由于data是object类型,所以返回的也是JSONobject,可以使用cn.hutool.json工具包的JsonObject
来接收。然后用JSONUtil工具类(也是hutool的)得到店铺,因为她的第一个参数是JsonObject。*/
JsonObject jsonObject = (JsonObject)redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
// 取得过期时间
LocalDataTime expireTime = redisData.getExpireTime();
// 5,判断是否过期(过期时间是不是当前时间之后)
if (expireTime.isAfter(LocalDataTime.now())){
// 5.1 未过期时,直接返回店铺信息
return shop;
}
// 5.2 已过期时,需要缓存重建
// 6,缓存重建
// 6.1 获取互斥锁
String lockKey = "lock:shop:" + id;
boolean isLock = tryLock(lockKey);
// 6.2 判断是否获取锁成功
if (isLock) {
// 注意:获取锁成功应该再次检测Redis缓存是否过期,做DoubleCheck。如果存在则无需重建缓存。
// 6.3 成功时,开启独立线程,实现缓存重建。建议使用线程池。
CACHE_REBUILD_EXECUTOR.submit(() -> {
// 重建任务
this.saveShopToRedis(id, 10L);// 设置短一点 10秒,方便观察效果。实际中,建议30分钟。
// 释放锁
unlock(lockKey);
});// 参数是任务,可以用lambda表达式
}
// 6.4 失败时(也包括成功时)要返回过期的商户信息
return shop;
}

最后

以上就是糊涂绿草为你收集整理的Redis 基础 - 用Redis查询商户信息请参考本文摘要Redis缓存简单应用例子1:用Redis查询商户信息缓存更新策略缓存更新策略的业务场景操作缓存和数据库时有三个问题需要考虑例子2:读、写时数据库与缓存的同步编辑例子1中的代码用Redis查询商户业务时其他的相关问题例子3:用“缓存空对象”的方式给查询商户时处理缓存穿透的风险例子4:基于互斥锁方式解决缓存击穿问题例子5:基于逻辑过期方式解决缓存击穿问题的全部内容,希望文章能够帮你解决Redis 基础 - 用Redis查询商户信息请参考本文摘要Redis缓存简单应用例子1:用Redis查询商户信息缓存更新策略缓存更新策略的业务场景操作缓存和数据库时有三个问题需要考虑例子2:读、写时数据库与缓存的同步编辑例子1中的代码用Redis查询商户业务时其他的相关问题例子3:用“缓存空对象”的方式给查询商户时处理缓存穿透的风险例子4:基于互斥锁方式解决缓存击穿问题例子5:基于逻辑过期方式解决缓存击穿问题所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(45)

评论列表共有 0 条评论

立即
投稿
返回
顶部