redis实现查询缓存的业务逻辑
service层实现
@Override
public Result queryById(Long id) {
String key = CACHE_SHOP_KEY + id;
// 现查询redis内有没有数据
String shopJson = (String) redisTemplate.opsForValue().get(key);
if(StrUtil.isNotBlank(shopJson)){ // 如果redis的数据为存在,那么解析为对象
// 将json转为对象
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 如果不存在,就先查数据库,再存入redis
Shop shop = getById(id);
if(shop == null){
return Result.fail("店铺不存在");
}
// 存在就写入redis,包括将对象转为json
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
return Result.ok(shop);
}
缓存更新策略
缓存更新策略的最佳实践方案:
- 低一致性需求:使用Redis自带的内存淘汰机制
- 高一致性需求:主动更新,并以超时删除作为处理方案
读操作:(查询)
- 缓存命中则直接返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间
写操作:(增删改)
- 先写数据库,然后再删除缓存
- 要确保数据库与缓存操作的原子性
缓存穿透
缓存穿透产生的原因是什么?
- 用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力。
缓存穿透的解决方案有哪些?
- 缓存null值
- 布隆过滤器
- 增强id的复杂度,避免被猜测id规律
- 做好数据的基本格式校验
- 加强用户权限校验
- 做好热点参数的限流
缓存空对象的方法解决缓存穿透
@Override
public Result queryById(Long id) {
// 使用店铺ID构建缓存键
String key = CACHE_SHOP_KEY + id;
// 检查店铺信息是否已经缓存到Redis中
String shopJson = (String) redisTemplate.opsForValue().get(key);
// 如果缓存中存在数据,则将JSON字符串解析为Shop对象
if (StrUtil.isNotBlank(shopJson)) {
// 将JSON转换为Shop对象
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop); // 返回店铺对象作为成功结果
}
// 如果缓存中包含表示无数据的占位符,则返回错误信息
if ("#".equals(shopJson)) {
return Result.fail("没有店铺相关信息"); // 没有店铺信息可用
}
// 如果缓存中未找到店铺数据,则查询数据库
Shop shop = getById(id);
// 如果数据库中不存在该店铺
if (shop == null) {
// 在缓存中存储一个占位符,以表示该店铺不存在
// 这可以防止对同一ID的进一步查询再次访问数据库
redisTemplate.opsForValue().set(key, "#", CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在"); // 返回错误,指示店铺不存在
}
// 如果找到店铺,则将店铺对象以JSON字符串的形式存储到缓存中
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop); // 返回店铺对象作为成功结果
}
缓存雪崩
缓存击穿
缓存击穿是指在高并发环境下,某个热点数据的缓存同时失效,导致大量请求直接访问数据库,从而造成数据库压力骤增的现象。
缓存击穿解决方案
解决方案 | 优点 | 缺点 |
---|---|---|
互斥锁 | - 没有额外的内存消耗 - 保证一致性 - 实现简单 | - 线程需要等待,性能受到影响 - 可能有死锁风险 |
逻辑锁 | - 线程无需等待,性能较好 | - 不保证一致性 - 有额外的内存消耗 - 实现复杂 |
基于互斥锁解决缓存击穿
基于互斥锁解决缓存击穿 + 缓存空对象的方法解决缓存穿透
// 查询店铺信息,使用互斥锁解决缓存击穿问题
public Shop queryWithMutex(Long id) {
// 构造缓存的key
String key = CACHE_SHOP_KEY + id;
// 1. 先从Redis中查询店铺信息
String shopJson = (String) redisTemplate.opsForValue().get(key);
// 如果Redis中存在缓存数据,直接解析JSON并返回对象
if (StrUtil.isNotBlank(shopJson)) {
// 将JSON字符串转换为Shop对象
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
// 如果Redis中缓存的值为"#", 表示数据库中没有该店铺信息
if ("#".equals(shopJson)) {
return null;
}
// 2. 构造互斥锁的key
String lockKey = "lock:shop:" + id;
// 定义店铺对象
Shop shop = null;
try {
// 尝试获取互斥锁
boolean isLock = tryLock(lockKey);
// 如果获取锁失败,线程休眠50ms后重试
if (!isLock) {
Thread.sleep(50); // 等待50ms
return queryWithMutex(id); // 递归调用,再次尝试获取锁
}
// 3. 如果没有获取到缓存数据,查询数据库
shop = getById(id);
// 模拟数据库查询的延时,生产环境应该去掉这段代码
// Thread.sleep(200);
// 4. 如果数据库中没有该店铺信息
if (shop == null) {
// 在Redis中存储一个特殊的标记值"#", 表示该店铺不存在
redisTemplate.opsForValue().set(key, "#", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 5. 如果数据库中有数据,将店铺信息存入Redis
// 将Shop对象转换为JSON字符串
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
// 捕获线程中断异常
throw new RuntimeException(e);
} finally {
// 释放互斥锁
unlock(lockKey);
}
// 返回查询到的店铺信息
return shop;
}
// 封装获取锁,释放锁
// 尝试获取互斥锁
private boolean tryLock(String key) {
// 使用Redis的setIfAbsent方法尝试设置锁
// 如果key不存在,则设置成功并返回true;否则返回false
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// 并设置一个过期时间(这里是 10 秒)
return BooleanUtil.isTrue(flag);
}
// 释放互斥锁
private void unlock(String key) {
// 删除锁对应的key
redisTemplate.delete(key);
}
setIfAbsent
方法是 Redis 中的一种操作,用于设置一个键的值,仅在该键不存在的情况下进行设置。具体来说,它的功能如下:
-
键不存在时:如果指定的键(
key
)在 Redis 中不存在,则将其设置为指定的值(在这个例子中是"1"
),并可以指定该键的过期时间(这里是 10 秒)。此时,方法返回true
。 -
键已存在时:如果指定的键已经存在于 Redis 中,则不会进行任何操作,保持原有的值不变,方法返回
false
。
基于逻辑锁解决缓存击穿
// 创建一个固定大小的线程池,用于缓存重建任务,避免频繁创建线程带来的开销
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 查询商铺信息,考虑逻辑过期
* @param id 商铺ID
* @return 商铺信息,如果不存在或已过期则返回null
*/
public Shop queryWithLogicalExpire(Long id){
// 构建缓存的key,用于从Redis中查询对应的商铺信息
String key = CACHE_SHOP_KEY + id;
// 1. 从Redis查询商铺缓存,获取商铺信息的JSON字符串
String shopJson = (String) redisTemplate.opsForValue().get(key);
// 2. 判断缓存是否存在
if (StrUtil.isBlank(shopJson)) {
// 3. 缓存不存在,直接返回null
return null;
}
// 4. 缓存命中,需要先将JSON字符串反序列化为RedisData对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
// 从RedisData对象中提取商铺信息,并将其反序列化为Shop对象
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
// 获取缓存的过期时间
LocalDateTime expireTime = redisData.getExpireTime();
// 5. 判断缓存是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 5.1. 缓存未过期,直接返回商铺信息
return shop;
}
// 构建锁的key,用于控制缓存重建的并发访问
String lockKey = LOCK_SHOP_KEY + id;
// 尝试获取锁,确保缓存重建操作的线程安全
boolean isLock = tryLock(lockKey);
// 6.2. 判断是否成功获取锁
if (isLock) {
// 6.3. 成功获取锁,开启独立线程进行缓存重建,避免阻塞主线程
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 重建缓存,将最新的商铺信息保存到Redis中,并设置过期时间
this.saveShop2Redis(id, 20L); // 假设20L是过期时间,单位为秒
} catch (Exception e) {
// 如果在缓存重建过程中发生异常,抛出运行时异常,并记录日志
throw new RuntimeException(e);
} finally {
// 无论缓存重建成功与否,都需要释放锁,避免死锁
unlock(lockKey);
}
});
}
// 返回当前查询到的商铺信息(可能已过期)
return shop;
}
public void saveShop2Redis(Long id, Long expireSeconds) {
// 1. 查询店铺数据
Shop shop = getById(id);
// 2. 封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 3. 写入Redis
redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
RedisData类
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
封装redis工具类
@Resource
private CacheClient cacheClient;
public static final String CACHE_SHOP_KEY = "cache:shop:";
public static final Long CACHE_SHOP_TTL = 30L;
@Override
public Result queryById(Long id) {
// 解决缓存穿透
// Shop shop = cacheClient // 传入一个从数据库内获取Shop对象的函数:this::getById
// .queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 互斥锁解决缓存击穿
Shop shop = cacheClient
.queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 逻辑过期解决缓存击穿
// Shop shop = cacheClient
// .queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
if(shop == null){
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
1. queryWithPassThrough
这个方法用于处理缓存穿透问题。缓存穿透是指查询一个数据库中不存在的数据,由于缓存中也没有这个数据,所以每次查询都会直接打到数据库上,增加数据库的压力。
-
参数:
-
keyPrefix
:缓存的前缀。 -
id
:缓存的ID。 -
type
:返回对象的类型。 -
dbFallback
:数据库查询的回调函数。 -
time
:缓存时间。 -
unit
:时间单位。
-
2. queryWithLogicalExpire
这个方法用于处理缓存击穿问题。缓存击穿是指一个缓存中非常热门的数据突然过期,导致大量请求同时打到数据库上,增加数据库的压力。
-
参数:与
queryWithPassThrough
相同。
最大的缺点是运行前要把所有缓存加到redis内,不然怎么查都是null
3. queryWithMutex(互斥锁)
这个方法结合了 queryWithPassThrough
和 queryWithLogicalExpire
的功能,用于处理缓存穿透和缓存击穿问题。
-
参数:与
queryWithPassThrough
相同。
@Slf4j
@Component
public class CacheClient {
public static final Long CACHE_NULL_TTL = 2L;
public static final String LOCK_SHOP_KEY = "lock:shop:";
private final RedisTemplate redisTemplate;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void set(String key, Object value, Long time, TimeUnit unit) {
redisTemplate.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
redisTemplate.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;
// 1.从redis查询商铺缓存
String json = (String) redisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值
if (json != null) {
// 返回一个错误信息
return null;
}
// 4.不存在,根据id查询数据库
R r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
redisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
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;
// 1.从redis查询商铺缓存
String json = (String) redisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
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.成功,开启独立线程,实现缓存重建
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);
}
});
}
// 6.4.返回过期的商铺信息
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;
// 1.从redis查询商铺缓存
String shopJson = (String) redisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
return JSONUtil.toBean(shopJson, type);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return null;
}
// 4.实现缓存重建
// 4.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2.判断是否获取成功
if (!isLock) {
// 4.3.获取锁失败,休眠并重试
Thread.sleep(50);
return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
}
// 4.4.获取锁成功,根据id查询数据库
r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
redisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7.释放锁
unlock(lockKey);
}
// 8.返回
return r;
}
private boolean tryLock(String key) {
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
redisTemplate.delete(key);
}
}