缓存更新策略
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库.
- 常见的解决方案:
- 缓存空对象
- 优点: 实现简单, 维护方便
- 缺点: 额外的内存消耗, 可能造成短期的不一致
- 布隆过滤
- 优点: 内存占用较少(保存的是数据的二进制位)
- 缺点: 实现复杂, 存在误判可能
- 缓存空对象
- 缓存空对象业务逻辑
- 写入空值
redisTemplate.opsForValue().set("item:1001", "", 30L, TimeUnit.SECONDS);
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机, 导致大量请求到达数据库, 带来巨大压力.
解决方案:
- 给不同的key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存击穿
缓存击穿问题也叫热点Key问题, 就是一个高并发访问并且缓存重建业务较复杂的key突然失效了, 无数的请求访问会瞬间给数据库带来巨大的冲击.
常见的解决方案:
- 互斥锁
- 逻辑过期
互斥锁
- 定义获取锁和释放锁的方法
private boolean tryLock(String key){
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
return BooleanUtils.isTrue(flag);
}
private void unlock(String key){
redisTemplate.delete(key);
}
逻辑过期
- 逻辑过期业务逻辑
- 缓存预热
- RedisData.java
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
- 判断是否逻辑过期
- 开启独立线程重建缓存
封装Redis工具类
缓存工具类
import cn.hutool.json.JSONUtil;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
private static final long NULL_TTL = 1L;
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 String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
public void delete(String key) {
stringRedisTemplate.delete(key);
}
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
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
/**
* 查询--缓存穿透处理
*/
public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Long time, TimeUnit unit, Function<ID, R> dbFallBack) {
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(json)) {
// 存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否为空值,缓存穿透处理会存""
if (json != null) {
// 返回一个错误信息
return null;
}
R r = dbFallBack.apply(id);
if (r == null) {
// 将空值写入redis, 缓存穿透处理
stringRedisTemplate.opsForValue().set(key, "", NULL_TTL, TimeUnit.SECONDS);
return null;
}
// 存在, 写入redis
this.set(key, JSONUtil.toJsonStr(r), time, unit);
return r;
}
}
@Data
class RedisData {
// 逻辑过期时间
private LocalDateTime expireTime;
private Object data;
}
分布式锁类
- ILock.java
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec
* @return
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
- SimpleRedisLock.java
import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock {
private StringRedisTemplate stringRedisTemplate;
private String name;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long timeoutSec) {
String threadId = ID_PREFIX + Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
String threadId = ID_PREFIX + Thread.currentThread().getId();
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if (threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
- 使用分布式锁