redis的实战项目02_缓存、缓存更新策略、穿透、雪崩、击穿、缓存工具封装
一、什么是缓存?
缓存就是数据交换的缓冲区(称作Cache [ k?? ] ),是存贮数据的临时地方,一般读写性能较高。
例如:web应用 浏览器:【浏览器可以作为缓存:例如页面静态资源】 Tomcat:添加应用层缓存【map、redis】 数据库:数据库缓存【索引】
缓存的作用:
- 减低后端的负载
请求进入Tomcat后,之前是访问数据库,由于数据库要做磁盘读写,相对效率较低。数据压力较大。 加入缓存后,从缓存中拿到数据,返回前端。 - 提高读写效率,降低相应时间
数据库读写相应时间长。 缓存redis,读写延迟是微妙级别,可以应对更高的并发问题
缓存的成本:
- 数据一致性成本
数据库和缓存的数据一致性 - 代码维护成本
- 运维成本
搭建集群,需要增加人工成本
二、添加redis缓存:
最最普通添加缓存案例: 前端穿过来一个id,根据id查询商铺信息。
------controller----
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
Result shop = shopService.selectById(id);
return shop;
}
------------serviceImpl-------
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result selectById(Long id) {
String shopKey = CACHE_SHOP_KEY+id;
String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
if (StrUtil.isNotBlank(shopJson)){
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
Shop shop = getById(id);
if (shop == null){
return Result.fail("店铺不存在");
}
String shopStr = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(shopKey,shopStr);
return Result.ok(shop);
}
非常好用的工具包:例如下面格式转换就是用的该工具包
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
三、缓存一致性问题,更新策略【重点】
1、场景的缓存更新策略
1. 内存淘汰:
说明: redis是基于内存来存储的,内存数据有限。原本是redis解决内存不足的问题。redis自身的淘汰机制。当内存不足时,就会触发该机制,根据机制将一部分数据淘汰掉。
一致性:差 是因为,当数据库信息发生了改变,缓存的数据并没有变。当用户请求时,还是会查到旧的数据。
维护成本:无。配置即可
2.超时剔除
说明:添加在缓存中的时间。到期后自动删除。下次查询时,再次更新 一致性:一般 维护成本:低
3. 主动更新
自己编写业务逻辑,修改数据库的同时,修改缓存。
4. 业务场景:
低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存 高一致性需求:使用主动更新机制,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
2、主动更新的策略的实现:
1.主动更新需要考虑的3点问题:
主动更新:缓存调用者,在更新数据库的同时更新缓存。
1、删除缓存还是更新缓存?
更新缓存:每次更新数据库都更新缓存。无效的写操作较多,不推荐。 例如:更新了一百次数据库,从而也就更新了一百次缓存。如果一次都没有查询缓存。那么无效写缓存操作较多。
推荐:删除缓存:更新数据库时,让缓存删除,查询时在更新缓存。 例如:更新了一百次数据库,从而也就更新了一百次缓存。 当有用户查询该用户时,才会更新缓存。
2、如何保证缓存与数据库的操作的同时成功或失败?
单体系统,将缓存与数据库操作放在一个事务 分布式系统:利用TCC等分布式事务方案
3、先操作缓存还是先操作数据库? 先删除缓存,在操作数据库 推荐:先操作数据库,在删除缓存。
在先操作数据库,在删除缓存时,导致缓存和数据库不一致问题: 1.当线程1来查询数据库时,恰好缓存失效了。失效的同时,就会到数据库查询数据。比如id=10 2.在微妙级写入缓存时,这时: 3.线程2插入了进来,要更新数据库变为20,并且删除空缓存【是空的,在查询时就已经失效了】 4.这时将数据库查询到的旧数据放到了缓存当中。id=10
2. 具体实现-案例
给查询商铺的缓存添加超时剔除和主动更新的策略 修改ShopController中的业务逻辑,满足下面的需求: ① 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间 ② 根据id修改店铺时,先修改数据库,再删除缓存
3. 测试:
当执行了更新数据库后,发现缓存中以及没有了数据。 当再次查看商品信息时, 第一:数据库和缓存都有了全新数据 第二:缓存中增加了超时剔除策略。
四、缓存穿透
1、介绍:
缓存穿透:用户查询的数据,在数据库和缓存中都不存在。从而当用户请求过来,如果不加入措施的情况下,会直接访问数据库。 如果不断的发起请求,那么会给数据库带来巨大压力
6个解决方法:
- 缓存null值
- 布隆过滤
- 增加id复杂度,避免被猜测id的规律
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
2、解决方法一:缓存空对象
说明: 当用户请求数据时,redis中没有,就会访问数据库,数据库也没有,就会返回空值,先写入redis缓存中,然后返回客户端。 当客户再次访问该数据时,就直接从redis中拿到空值。从而避免访问数据库。
优点: 简单、维护方便
缺点: 1.额外的内存消耗。redis缓存了一些没有用的值。 【解决方法:设置有效期】
2.可能造成短期的不一致。 比如:请求了某一个id,请求时不存在,当把空值写入缓存时,这时将该id,添加到了数据库。从而缓存和数据库中的值不一致。 【解决办法:添加数据库时,覆盖redis缓存】
3、解决方法二:布隆过滤
说明: 在客户端与redis之间,加入一层布隆过滤器。 当用户请求过来以后,先到布隆过滤器,根据计算是否存在该key,如果存在则访问redis。如果不存在拒绝访问。
布隆过滤器原理:是一个bit数组,存放的是二进制位。判断数据库中的数据是否存在时,并不是把数据放到了布隆过滤器,而是通过hash算法,计算出hash值,再将hash值转换为二进制位,保存到布隆过滤器中。
优点: 空间占用小
缺点: 实现复杂、存在误判可能 不存在是真的不存在,存在不一定存在。
4、案例实现:缓存穿透-缓存空对象
需要改动的是: 1.当请求是id在缓存和数据库中都不存在的值时,数据库返回null,写入redis中。并返回null 2.用户从redis缓存中拿数据时,判断是否为空,是空则直接结束。不是空,则返回信息。
-------service代码------------
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result selectById(Long id) {
String shopKey = CACHE_SHOP_KEY+id;
String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
if (StrUtil.isNotBlank(shopJson)){
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
if (shopJson == null){
return Result.fail("店铺信息不存在!!");
}
Shop shop = getById(id);
if (shop == null){
stringRedisTemplate.opsForValue().set(shopKey,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
String shopStr = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(shopKey,shopStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
解释:isNotBlank方法:
ps:此消息【店铺信息不存在】是缓存到了浏览器页面。所以第二次访问该id也进行了提示。
五、缓存雪崩
缓存雪崩:是在指同一时间段大量的缓存key同时失效或者是redis服务宕机,导致大量请求到达数据库,从而给数据库带来巨大的压力。
解决雪崩的4个方法:
- 同时失效解决方法:给不同的key的TTL添加一个随机值。比如1-5分钟。
比如说可能做缓存的预热,那么批量将数据导入到缓存中。如果给所有数据设置的是同一个TTL值,那么所有的数据将会同一时间过期。造成雪崩。 从而:在缓存预热,批量导入数据时,可以给TTL后面添加一个随机数。这样这些数据,就可以在时间内过期,避免同一时间内失效。
- redis宕机解决方法:利用redis集群提高服务的可用性
reids哨兵机制:可以实现对服务的监控。当某个主节点宕机后,哨兵会从从节点中选出一个节点成为主节点。主从还可以实现数据同步,保证数据不会丢失。从而提高服务的可用性、数据安全性。
- 给缓存业务添加降级限流策略
当出现人们无法抗拒的事故,比如整个集群宕机了。那么可以给服务添加一种降级限流策略。比如sentinel快速失败、拒绝服务。从而避免对数据库的访问。 这样牺牲了部分服务,但是能保护整个数据库的服务。
- 给业务添加多级缓存
浏览器缓存【缓存静态数据】 反向代理Nginx【做本地缓存】 reids缓存 JVM内部建立缓存 数据库
六、缓存击穿
缓存雪崩是大量key同时过期,导致大量访问到数据库,从而给数据库带来巨大压力。 击穿是部分key过期,导致给数数据库带来巨大压力。
缓存击穿也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂(可以理解为查询语句比较耗时的SQL语句)的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大压力。
- 热点key:就是访问的非常多的某个key。比如秒杀商品。
- 缓存重建:某个key的缓存时间到期了。就会从新去数据库进行查询,写入redis中。
有些查询数据的时间比较长,比如设计多个表、做运算。从而达到毫秒级别。从而redis中一直都没有缓存。在查询数据库中的这段时间内,就会有无数个请求该key,这些都在redis中未命中,都会去访问数据库。
例如: 当一个热点key失效了,这时有100个请求访问该key。
- 在缓存中查询未命中,要进行缓存重建
- 查询数据库,重建缓存数据。【这段时间比较耗时】
- 查询数据库结束后,写入缓存中
- 那么:::当第一个请求在查询数据这段时间内,突然来了100个请求该key,那么请求缓存也是未命中,从而只要是在第一个请求写入缓存之前,来访问该key的请求,就都会去数据库查询。重复以上操作,并且重复查询数据库N次,重复更新缓存N次。
1、解决方法一:互斥锁
无数个请求都去做重建缓存,利用锁的机制。只允许一个请求去查询数据库。
- 当请求发现缓存未命中。
- 去获取互斥锁,
- 只有获取互斥锁成功的请求,才能去数据库进行查询数据。
- 当写入缓存后
- 释放锁
在第三步,没有获取互斥锁的请求, 1.会睡眠一会儿 2.然后在重试访问缓存,如果缓存失败 3.再去获取锁,锁也失败,在进行睡眠。 直到第一个请求写入缓存成功后,请求2才能在缓存中拿到数据。
缺点:互相等待,从而性能差 例如构建缓存时间比较久,比如200毫秒。在这一段时间内,涌入的所有线程只能做等待。
2、解决方法二:逻辑过期
缓存击穿的原因是:热点key缓存时间过期了。所以导致未命中,从而重建缓存。 既然如此,那么当在redis中存入数据时,不设置TTL过期时间。 而是:在插入数据时不加入真正的TTL过期时间,加一个字段为过期时间。并不是真正过期时间,而是某个字段内容是过期时间。
- 线程一:查询缓存发现逻辑时间过期,获取互斥锁成功后,
- 开启线程二,线程二进行缓存重建。释放互斥锁
- 线程一直接返回逻辑过期的数据。
- 当线程三来访问缓存时,发现缓存时间过期、并且互斥锁也获取失败,那么也会返回逻辑过期数据。
- 直到线程二,缓存重建成功,并且释放互斥锁后。就可以拿到正常缓存数据了。
3、互斥锁和逻辑过期对比
互斥锁可以保证数据一致性,当数据过期后,会一直等待锁释放比较耗时,但是拿到最新数据。 逻辑过期可以性能好,不需要等待释放锁。但是拿到的是过期后的数据。
要求一致性:互斥锁 要求性能:逻辑过期
4、代码实现互斥锁:【案例】
1.代码流程:
- 根据id查询数据库
- redis缓存查询数据
- 判断是否命中
- 如果命中,则返回数据
- 如果没有命中,尝试获取互斥锁
- 判断是否拿到锁,
- 如果没有拿到锁,进入休眠,在重新去redis缓存中查询数据。(可以做递归处理,调用本方法)
- 如果拿到锁,那么进行缓存重建。去数据库查询数据
- 查询到的数据写入redis中
- 释放锁
- 返回数据。
2.对于锁的介绍:
setnx命令:给某个key赋值,当这个key不存在的时候才能写入该key。 也就是说key存在,就不执行。 释放锁删除、获取锁就是赋值。 在利用setnx时,往往加入一个有效期TTL,10秒钟。往往一个SQL查询业务,在1秒内完成。从而避免某些原因锁得不到释放。
3.具体代码:
------service层代码:----------
@Override
public Result selectById(Long id) {
Shop shop = queryWithMutex(id);
if (shop ==null) {
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
public Shop queryWithMutex(Long id){
String shopKey = CACHE_SHOP_KEY+id;
String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
if (StrUtil.isNotBlank(shopJson)){
return JSONUtil.toBean(shopJson, Shop.class);
}
if (shopJson != null){
return null;
}
String lockKey= null;
Shop shop = null;
try {
lockKey = "lock:shop:"+id;
boolean isLock = tryLock(lockKey);
if (!isLock){
Thread.sleep(50);
return queryWithMutex(id);
}
shop = getById(id);
Thread.sleep(200);
if (shop == null){
stringRedisTemplate.opsForValue().set(shopKey,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return null ;
}
String shopStr = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(shopKey,shopStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
unlock(lockKey);
}
return shop;
}
4.测试缓存击穿、并发访问
热点key需要满足两个方面: 1.高并发。用Apache JMeter工具来模拟高并发。 2.缓存重建时间比较久。重建时间越久,发生线程并发安全问题就越高。也就是说缓存中并没有数据。 目前测试阶段:为了提高缓存重建时间,在查询数据时,添加睡眠时间: 测试要求:
测试结果:当reids缓存失效时,1000个请求来访问该接口时,只有一个请求线程拿到了锁,并访问了一次数据库。剩下的请求都在睡眠中。 当拿到锁的请求,进行了更新缓存,并且释放锁后。其他请求都在缓存中获取数据
5、代码实现逻辑过期【案例】
1.代码流程:
逻辑过期并不是真正的过期,要求在存储数据到redis的时候,不添加过期时间TTL,而是额外添加一个过期时间的字段。当调用该数据时,通过业务代码来判断该数据是否过期。
- 前端通过id查询数据
- 在redis查询缓存。
- 判断有没有命中。【其实理论上热点key是肯定能命中的,因为设置了永不过期】
- 如果没有命中,直接返回null
- 核心 :命中后,该key在逻辑上有没有过期
- 如果没有过期,可用状态。直接返回数据即可
- 如果过期数据:并发请求争抢互斥锁。
- 判断获取互斥锁是否成功
- 如果没有拿到互斥锁,直接返回旧数据。说明缓存数据已经开始做缓存重建了。
- 如果拿到了互斥锁,返回旧数据,开启新线程进行缓存重建。
- 新线程:查询数据库id,写到redis,并设置逻辑过期时间。
- 新线程:释放锁。
2. 逻辑过期时间问题
再把数据写入到redis中,如何将逻辑过期时间添加到数据中呢? 在写一个实体类:其中包括 过期时间字段、object类型的变量,用来作为所有数据的类型。 到时候存入逻辑时间过期的值时,将RedisData存入即可。
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
3.缓存预热,将热点key加入到redis中
------------test---
@SpringBootTest
class HmDianPingApplicationTests {
@Resource
private ShopServiceImpl shopService;
@Test
public void saveShop(){
shopService.save2ShopRedis(1L,10L);
}
}
-----------只写在了service层代码
private void unlock(String key){
stringRedisTemplate.delete(key);
}
public void save2ShopRedis(Long id,Long expireSeconds){
Shop shop = getById(id);
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
4. 所有代码:
以下代码包括:逻辑过期、加锁、释放锁、缓存预热 其实整体代码还包括:逻辑时间类【其中包括object数据、过期时间成员变量】
--------sevice层:
@Override
public Result selectById(Long id) {
Shop shop = queryWithLogicalExpire(id);
if (shop ==null) {
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
public Shop queryWithLogicalExpire(Long id){
String shopKey = CACHE_SHOP_KEY+id;
String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
if (StrUtil.isBlank(shopJson)){
return null;
}
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())){
return shop;
}
String lockKey =LOCK_SHOP_KEY+id;
boolean isLock = tryLock(lockKey);
if (isLock){
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
this.save2ShopRedis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unlock(lockKey);
}
});
}
return shop;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
private boolean tryLock(String key){
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(aBoolean);
}
private void unlock(String key){
stringRedisTemplate.delete(key);
}
public void save2ShopRedis(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));
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
5.测试:
缓存击穿的特点:
- 热点key逻辑过期。【提前将数据插入到缓存中,然后在进行测试】
- 高并发测试。1秒查询100次访问该热点key。
预先得知结论:当高并发访问该热点数据时,如果判断数据逻辑过期后,会有另外一个新线程进行更新数据,在更新数据前,所有的请求都是拿到旧数据。 为了能模拟上述情况:先将数据缓存到redis中,并等待逻辑过期。 然后只把数据库中的信息进行修改。 然后在看测试回馈结果,就会得出上述结论。从而达到测试目的。
可以看到前面部分数据查询出来的是旧数据: 后面部分是新数据。
七、缓存工具封装。看完直接实现击穿、穿透
基于StringRedisTemplate封装一个缓存工具类,满足下列需求: ? 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间 ? 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存 击穿问题 ? 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题 ? 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
1、工具类的代码:
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 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.LOCK_SHOP_KEY;
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
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)));
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 = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) {
return JSONUtil.toBean(json, type);
}
if (json != null) {
return null;
}
R r = dbFallback.apply(id);
if (r == null) {
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
this.set(key, r, time, unit);
return 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;
String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(json)) {
return null;
}
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
if(expireTime.isAfter(LocalDateTime.now())) {
return r;
}
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
if (isLock){
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
R newR = dbFallback.apply(id);
this.setWithLogicalExpire(key, newR, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
unlock(lockKey);
}
});
}
return r;
}
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)) {
return JSONUtil.toBean(shopJson, type);
}
if (shopJson != null) {
return null;
}
String lockKey = LOCK_SHOP_KEY + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
if (!isLock) {
Thread.sleep(50);
return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
}
r = dbFallback.apply(id);
if (r == null) {
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
this.set(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
unlock(lockKey);
}
return r;
}
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
}
2、如何使用工具类?使用案例:
缓存穿透:参数(key的前缀,key的id,实体的类型,查询数据库的函数,过期时间,和单位)
-----使用案例-----
------service层代码:-------
@Resource
private CacheClient cacheClient;
@Override
public Result queryById(Long id) {
参数(key的前缀,key的id,实体的类型,查询数据库的函数,过期时间,和单位)
Shop shop = cacheClient
.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
if (shop == null) {
return Result.fail("店铺不存在!");
}
return Result.ok(shop);
}
|