什么是缓存
缓存就是数据交换的缓冲区(称作cache [ kze]]),是存贮数据的临时地方,一般读写性能较高。
缓存的作用:
- 降低后端负载
- 提高读写效率,降低响应时间
缓存的成本:
- 数据一致性成本
- 代码维护成本
- 运维成本
缓存更新策略
解决缓存与数据库不一致
- 低一致性需求:使用内存淘汰机制。
- 高一致性需求:主动更新,并以超时剔除作为兜底方案。
操作缓存和数据库时有三个问题需要考虑:
1.删除缓存还是更新缓存?
- 更新缓存:每次更新数据库都更新缓存,无效写操作较多
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存 √
2.如何保证缓存与数据库的操作的同时成功或失败?
- 单体系统,将缓存与数据库操作放在一个事务
- 分布式系统,利用TCC等分布式事务方案
3.先操作缓存还是先操作数据库?
- 先删除缓存,再操作数据库
- 先操作数据库,再删除缓存 √
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。如果不断发起这样的请求,便会给数据库带来巨大压力。
常见的解决方案:
- 缓存空对象
- 布隆过滤
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
缓存空对象
缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存
- 优点:简单
- 缺点:消耗内存,可能会发生不一致的问题
代码示例:
public Shop queryWithPassThrough(Long id) {
//获取缓存
String shopjson = redisTemplate.opsForValue().get("cache:shop:" + id);
//判断店铺是否存在 isNotBlank的判断情况:1.null=false 2.""=false 3."\n\t"=false 4."abc"=ture
if (StrUtil.isNotBlank(shopjson)) {
return JSONUtil.toBean(shopjson,Shop.class);
}
//判断是否是空值 不等于空说明是值为空字符串的垃圾缓存(用于解决缓存穿透),如果等于空说明还没有此id的缓存需要查询数据库
if (shopjson != null) {
return null;
}
//没有缓存查询数据库
Shop shopone = getById(id);
//不存在,添加缓存并返回空值
if (shopone == null) {
//防止缓存击穿,存入短效空值
redisTemplate.opsForValue().set("cache:shop:" + id, "",3l,TimeUnit.MINUTES);
return null;
}
//设置随机值 防止缓存雪崩
Long randomNumbers = Long.valueOf(RandomUtil.randomNumbers(2));
//将数据添加到缓存
redisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shopone),30 + randomNumbers, TimeUnit.MINUTES);
return shopone;
}
布隆过滤
- 优点:内存占用较少,没有多余key
- 缺点:实现复杂,存在误判

布隆过滤器原理
存储数据时将数据id通过多个hash函数获取值,并将其对应的数组位置改为1。当下次查询数据时首先进过布隆过滤器。将数据id使用相同的hash函数计算出对应的位置是否都为1(都为1:说明数据存在。反之只要有一个不为1则数据不存在)
bitmap(位图)︰相当于是一个以(bit)位为单位的数组,数组中每个单元只能存储二进制数0或1
布隆过滤器作用:布隆过滤器可以用于检索一个元素是否在一个集合中。

但布隆过滤器有可能存在误判,当其它数据进过hash计算将其改为1后,此时恰好查询一个不存在的数据经过hash计算的值都被其它数据改为1,这时就会出现误判情况。

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

常见的两种解决方案:

互斥锁
为了避免无数的线程都来重建缓存,可以给它加锁,当线程进来的时候首先要获取锁,只有获取锁成功的人才可以进行缓存重建,这时其它线程进来只有等第一个线程释放锁后才能重建。
思维导图

代码示例:
public boolean tryLock(String key) {
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(aBoolean);
}
public boolean unlock(String key) {
Boolean aBoolean = redisTemplate.delete(key);
return BooleanUtil.isTrue(aBoolean);
}
/**
* 缓存击穿lock
*/
public Shop queryWithMutex(Long id) {
//获取商铺缓存
String shopjson = redisTemplate.opsForValue().get("cache:shop:" + id);
if (StrUtil.isNotBlank(shopjson)) {
return JSONUtil.toBean(shopjson,Shop.class);
}
//判断是否是空值
if (shopjson != null) {
return null;
}
//实现缓存重建
//获取互斥锁
String lock = "lock:shop:" + id;
boolean tryLock = tryLock(lock);
Shop shopone = null;
try {
//判断是否获取锁
if (!tryLock) {
//失败,回调
return queryWithMutex(id);
}
//没有缓存查询数据库
shopone = getById(id);
if (shopone == null) {
//防止缓存穿透,存入短效空值
redisTemplate.opsForValue().set("cache:shop:" + id, "",3,TimeUnit.MINUTES);
return null;
}
//设置随机值
Long randomNumbers = Long.valueOf(RandomUtil.randomNumbers(2));
//将数据添加到缓存
redisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shopone),30 + randomNumbers, TimeUnit.MINUTES);
} finally {
//释放锁
unlock(lock);
}
return shopone;
}
逻辑过期
用户查询某个热门产品信息,如果缓存未命中(即信息为空),则直接返回空,不去查询数据库。如果缓存信息命中,则判断是否逻辑过期,未过期返回缓存信息,过期则重建缓存,尝试获得互斥锁,获取失败则直接返回已过期缓存数据,获取成功则开启独立线程去重构缓存然后直接返回旧的缓存信息,重构完成之后就释放互斥锁。
思维导图:
代码示例:
逻辑过期实体类:
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
逻辑过期业务处理类:
public boolean tryLock(String key) {
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(aBoolean);
}
public boolean unlock(String key) {
Boolean aBoolean = redisTemplate.delete(key);
return BooleanUtil.isTrue(aBoolean);
}
//线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 缓存击穿 逻辑过期
*/
public Shop queryWithLogicalExpire(Long id) {
//获取商铺缓存
String shopjson = redisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
if (StrUtil.isBlank(shopjson)) {
return null;
}
//命中,把json反序列化为对象
RedisData redisData = JSON.parseObject(shopjson, RedisData.class);
String datajson = redisData.getData().toString();
Shop shop = JSON.parseObject(datajson, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//判断是否逻辑过期
if (expireTime.isAfter(LocalDateTime.now())) {
//未过期
return shop;
}
//已过期,尝试获取锁
String lock = "lock:shop:" + id;
boolean tryLock = tryLock(lock);
//判断是否获取锁成功
if (tryLock) {
//成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//重建缓存
//查询店铺
Shop shopById = getById(id);
//封装为逻辑过期
RedisData redisData1 = new RedisData();
redisData1.setData(shopById);
redisData1.setExpireTime(LocalDateTime.now().plusSeconds(30l));
//添加到缓存
String jsonStr = JSONUtil.toJsonStr(redisData1);
redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,jsonStr);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//释放锁
unlock(lock);
}
});
}
return shop;
}
缓存工具封装
@Component
public class CacheClient {
@Autowired
private StringRedisTemplate redisTemplate;
//将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
public void set(String key, Object value, Long ttl, TimeUnit timeUnit) {
String jsonStr = JSONUtil.toJsonStr(value);
redisTemplate.opsForValue().set(key,jsonStr,ttl,timeUnit);
}
//将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间
public void setWithLogicalExpire(String key, Object value, Long ttl, TimeUnit timeUnit) {
RedisData redisData = new RedisData();
redisData.setData(value);
//将时间添加到ExpireTime,timeUnit.toSeconds()将数据转为秒
redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(ttl)));
redisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
}
//根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
public <Result, ID> Result queryWithPassThrough(String key, ID id, Class<Result> type, Function<ID,Result> dbFallback, Long ttl, TimeUnit timeUnit) {
//获取商铺缓存
String json = redisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) {
return JSONUtil.toBean(json, type);
}
//判断是否是空值
if (json != null) {
return null;
}
//没有缓存查询数据库
Result result = dbFallback.apply(id);
//不存在返回错误
if (result == null) {
//将空值写入redis,预防缓存穿透
redisTemplate.opsForValue().set(key,"",2l,TimeUnit.MINUTES);
//返回错误信息
return null;
}
String jsonStr = JSONUtil.toJsonStr(result);
redisTemplate.opsForValue().set(key,jsonStr,ttl,timeUnit);
return result;
}
//根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public <Result, ID> Result queryWithLogicalExpire(String keyprefix,ID id, Class<Result> type,Function<ID,Result> dbFallback, Long ttl, TimeUnit timeUnit) {
//获取商铺缓存
String json = redisTemplate.opsForValue().get(keyprefix + id);
if (StrUtil.isBlank(json)) {
return null;
}
//命中,把json反序列化为对象
RedisData redisData = JSON.parseObject(json, RedisData.class);
String datajson = redisData.getData().toString();
Result result = JSON.parseObject(datajson, type);
LocalDateTime expireTime = redisData.getExpireTime();
//判断是否逻辑过期
if (expireTime.isAfter(LocalDateTime.now())) {
//未过期
return result;
}
//已过期,尝试获取锁
String lock = keyprefix + id;
boolean tryLock = tryLock(lock);
//判断是否获取锁成功
if (tryLock) {
//成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//重建缓存,查数据库
Result apply = dbFallback.apply(id);
this.set(keyprefix + id,apply,ttl,timeUnit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//释放锁
unlock(lock);
}
});
}
return result;
}
public boolean tryLock(String key) {
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(aBoolean);
}
public boolean unlock(String key) {
Boolean aBoolean = redisTemplate.delete(key);
return BooleanUtil.isTrue(aBoolean);
}
}


被折叠的 条评论
为什么被折叠?



