本文已参与「新人创作礼」活动,一起开启掘金创作之路。
缓存简介
缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。
Redis是运行于内存中,一般作为数据库缓存来使用。
缓存使用场景:
当数据发生变更(增删改),我们要删除缓存还是更新缓存?
- 如果每次更新数据库都更新缓存,无效写操作较多
- 所以要使用删除缓存,更新数据库时让缓存失效,查询时再更新缓存
如何保证缓存与数据库的操作的同时成功或失败?
- 单体系统,将缓存与数据库操作放在一个事务
- 分布式系统,利用TCC等分布式事务方案
数据发生变更时,先操作缓存还是先操作数据库?
- 这两种操作在并发状态下(如果没加锁),都会导致共享资源问题,但是先操作数据库这方案,发生共享问题的可能性会更小,所以:先操作数据库,再删除缓存
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。如果有人恶意请求数据库不存在的数据,则会导致数据库负载过重。
解决方法一:设置缓存空对象(容易实现,但是会造成一定的内存消耗)
解决方法二:布隆过滤器(存在误判的可能,不一定全部拦截成功)
上面两种是防止缓存穿透的两种方法,下面这些是主动避免缓存穿透的方法:
- 增强id的复杂度,避免被猜测id规律
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
@Override
public Result queryById(Long id) {
// 根据id查询商品缓存
String json = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 判断是否命中
if (StrUtil.isNotBlank(json)){
// 命中:直接返回
Shop shop = JSONUtil.toBean(json, Shop.class);
return Result.ok(shop);
}
// 判断命中的是否是空值(缓存穿透)
if (json != null){
return Result.fail("店铺信息不存在");
}
// 未命中:查询数据库、写到内存
Shop shop = getById(id);
if (shop == null) {
// 缓存穿透预防 设置空值
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在!");
}
// 设置缓存
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 返回
return Result.ok(shop);
}
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值(防止Key同时失效)
- 利用Redis集群提高服务的可用性(哨兵机制)
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案:
- 互斥锁
- 逻辑过期
互斥锁实现:
@Override
public Result queryById(Long id) {
// -----------缓存穿透----------
String json = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
if (StrUtil.isNotBlank(json)){
Shop shop = JSONUtil.toBean(json, Shop.class);
return Result.ok(shop);
}
if (json != null){
return null;
}
// 未命中:查询数据库、写到内存, 实现**缓存重建**
// 1.获取互斥锁、判断是否成功: 成功:查数据库, 失败:休眠重试
Shop shop = null;
String lockKey = "lock:shop:" + id;
try {
boolean isLock = tryLock(lockKey);
if (!isLock){
Thread.sleep(50);
return queryById(id);
}
shop = getById(id);
if (shop == null) {
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在!");
}
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 2. 释放锁
unLock(lockKey);
}
return Result.ok(shop);
}