Redis 查询接口加缓存 缓存雪崩 缓存穿透 缓存击穿 精彩!精彩!
项目地址 Gitee
https://gitee.com/yangjunbo-jetli/hmdp-redis.git
一、Redis 的作用
1.1、基于 session 保存用户登录信息,集群环境下没有共享





1.2、基于 Redis 实现集群共享登录信息














1.3、查询接口加 Redis 缓存提高性能




1.4、解决缓存穿透问题


1.5、解决缓存雪崩问题

1.6、利用互斥锁解决缓存击穿问题








1.7、利用逻辑过期解决缓存击穿问题






1.8、封装一个 Redis 操作工具类

java
package com.hmdp.utils;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.RedisData;
import com.hmdp.entity.Shop;
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.*;
@Slf4j
@Component
public class CacheClient {
//使用构造方法注入 StringRedisTemplate
private final StringRedisTemplate stringRedisTemplate;
/**
* 构造方法注入 StringRedisTemplate
* @param stringRedisTemplate
*/
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
//封装一个给 Redis 中保存数据并设置过期时间方法
/**
* 给 Redis 中保存数据并设置过期时间方法
* @param key
* @param value
* @param time
* @param timeUnit
*/
public void set(String key, Object value, Long time, TimeUnit timeUnit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,timeUnit);
}
//封装一个给 Redis 中保存数据并设置逻辑过期时间方法
/**
* 给 Redis 中保存数据并设置逻辑过期时间方法
* @param key
* @param value
* @param time
* @param timeUnit
*/
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit timeUnit){
//设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
//存入 Redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
}
//封装一个处理过缓存穿透问题的查询方法
/**
* 处理过缓存穿透问题的查询方法
* @param keyPrefix
* @param id
* @param type
* @param dbFallback
* @param time
* @param timeUnit
* @return
* @param <R>
* @param <ID>
*/
public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit timeUnit){
//1、从 Redis 查询商铺缓存
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
//2、判断是否存在
if(StrUtil.isNotBlank(json)){
//3、存在直接返回
return JSONUtil.toBean(json,type);
}
//4、判断是否是 "" 空值
//走到这里 json 是 null 或 "",如果不是 null 那就是 ""。
if(json != null){
return null;
}
//5、不存在,根据 id 查询数据库。
R r = dbFallback.apply(id);
//6、数据库不存在把空值 "" 写入 Redis 并返回错误
if(r == null){
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
//7、数据库存在,写入 Redis。
this.set(key,r,time,timeUnit);
return r;
}
//封装一个处理过缓存击穿的方法
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(5);
/**
* 处理过缓存击穿的方法
* @param keyPrefix
* @param id
* @param type
* @param dbFallback
* @param time
* @param timeUnit
* @return
* @param <R>
* @param <ID>
*/
public <R,ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit timeUnit){
//1、从 Redis 查询商铺信息缓存
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、判断是否存在
if(StrUtil.isBlank(shopJson)){
//3、不存在,直接返回
return null;
}
//4、命中,需要先把 JSON 序列化为对象。
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
//5、判断是否过期
LocalDateTime expireTime = redisData.getExpireTime();
if(expireTime.isAfter(LocalDateTime.now())){
//5.1、未过期,直接返回商铺信息。
return r;
}
//5.2、已过期,需要重建缓存。
//6、缓存重建
//6.1、获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
//6.2、判断是否获取互斥锁成功
if(isLock){
//6.3、抢到锁以后,可能别的线程已经重建好缓存了,再确认下缓存是否过期。
shopJson = stringRedisTemplate.opsForValue().get(key);
redisData = JSONUtil.toBean(shopJson, RedisData.class);
r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
expireTime = redisData.getExpireTime();
if(expireTime.isAfter(LocalDateTime.now())){
return r;
}
//6.4、成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
//重建缓存
try {
//查询数据库
R r1 = dbFallback.apply(id);
//写入 Redis
this.setWithLogicalExpire(key,r1,time,timeUnit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//释放锁
unLock(lockKey);
}
});
}
//6.5、返回过期的商铺信息
return r;
}
//尝试获取 Redis 互斥锁方法
//哪个线程在 Redis 保存数据成功等于哪个线程拿到互斥锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return flag;
}
//释放锁,删除 Redis 中的数据,别的线程又可以保存成功数据,抢到互斥锁了。
private void unLock(String key){
stringRedisTemplate.delete(key);
}
}
java
package com.hmdp.entity;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用来给 Redis 传输数据的实体类,封装了逻辑过期时间。
*/
@Data
public class RedisData {
//过期时间
private LocalDateTime expireTime;
//数据
private Object data;
}