缓存问题
缓存击穿(失效)
定义: 大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,造成数据库压力剧增甚至挂掉。
案例: 运营批量上架秒杀商品,这些商品存入缓存的时候过期时间设置相同,这些商品会同时过期,可能发生缓存击穿。
解决方案: 为缓存设置不同的超时时间,在原本设定的超时时间基础上加上一个随机数。
缓存穿透
定义: 查询一个根本不存在的数据,缓存和数据库中都不存在。通常如果在数据库查询不到数据,不会向缓存中存入数据,导致每次查询不存在数据时,都会将请求发送到数据库中。
案例:
- 黑客攻击,故意发送大量无效请求数据,造成空命中。
- 自身业务代码或者数据出现问题。
解决方案:
- 缓存空对象。即查询不到数据时也将这个 key 存入缓存,value 用统一标识 (例:“{ }”),表示这个 key 不在数据库中,设置 30s ~ 2min 的超时时间。
- 布隆过滤器。
布隆过滤器计算方式:
1、对 key 进行 hash 计算,计算结果和要存放数据的数组长度进行取模运算
2、计算后的结果为数组的 index,把这个位置上的值设置为 1。
3、重复多次 1,2 步,hash 计算使用不同的算法。
4、最终获得多个 index,把这些 index 的值都设置为 1。
注意: 不同的 key 计算的 Index 可能有重复。
结论: 查询布隆过滤器,如果能够查到,这个值可能不存在,因为可能是另外 key 赋值;但如果布隆过滤器查询不到,说明这个 key 肯定不存在。
布隆过滤器使用伪代码
package com.redisson;
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonBloomFilter {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
//构造Redisson
RedissonClient redisson = Redisson.create(config);
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("shopList");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
bloomFilter.tryInit(100000000L,0.03);
//将apple插入到布隆过滤器中
bloomFilter.add("apple");
//判断下面号码是否在布隆过滤器中
System.out.println(bloomFilter.contains("banana"));//false
System.out.println(bloomFilter.contains("orange"));//false
System.out.println(bloomFilter.contains("apple"));//true
}
}
缓存雪崩
定义: 缓存层承受不住超大并发请求或者宕机,这些请求都会被发送到后端存储;后端存储也承受不住因此挂掉,接连导致微服务等其他模块都挂掉。
案例: 微博明星热点事件,大量用户点击那条信息造成高并发量,可能导致缓存雪崩。
解决方案:
- 使用 Redis 哨兵或者集群模式。
- 使用Sentinel或Hystrix限流降级组件,实现为后端限流熔断并降级。
限流: 限制单位时间内访问后端服务的请求数量。
熔断: 在后端服务不稳定(如频繁超时、错误率高)时,主动阻断访问。
降级: 触发限流或熔断后,不让用户直接看到系统崩溃,返回一个友好提示或者直接返回到默认界面。
- 后端可以使用多级缓存,增加一层 JVM 级别的缓存来缓解 redis 的压力。
突发性热点缓存重建导致系统压力暴增
案例:
- 大 V 带冷门好物,这个物品信息不在 redis 缓存中,可能导致大量请求发送到存储层,导致存储层压力增大。
- 疫情期间板蓝根突然爆火,如果这个药品信息没有存储到 redis 中,高并发请求信息发送到存储层,导致存储层压力增大。
解决方案:
- Redisson 分布式锁:大量请求只有一条请求可以继续向下执行代码块。
- 双重检测 :加锁前和加锁后都查询一下 redis,加锁后的查询是为了有一条请求从数据库中查询到数据,并把数据存入 redis 之后,其余被阻塞的请求进来之后不用再次查询数据库,只查询 redis 就可获得结果。
public Product getProduct(Long productId) throws InterruptedException {
Product product = null;
String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
//从 redis 中获取物品信息
product = getProductFromCache(productCacheKey);
if (product != null) {
return product;
}
//Redisson 加锁
RLock rlock = redisson.getLock("lock:hot:product:"+ productId);
rlock.lock();
try {
// 双重检测
product = getProductFromCache(productCacheKey);
if (product != null) {
return product;
}
product = productDao.get(productId);
if (product != null) {
redisUtil.set(productCacheKey, JSON.toJSONString(product),
60 * 60 * 24 + new Random().nextInt(5) * 60 * 60(), TimeUnit.SECONDS);
productMap.put(productCacheKey, product);
} else {
redisUtil.set(productCacheKey, "{}", 30 + new Random().nextInt(30), TimeUnit.SECONDS);
}
} finally {
rlock.unlock();
}
return product;
}
private Product getProductFromCache(String productCacheKey) {
Product product = productMap.get(productCacheKey);
if (product != null) {
return product;
}
String productStr = redisUtil.get(productCacheKey);
if (!StringUtils.isEmpty(productStr)) {
if ("{}".equals(productStr)) {
redisUtil.expire(productCacheKey, 30 + new Random().nextInt(30), TimeUnit.SECONDS);
return new Product();
}
product = JSON.parseObject(productStr, Product.class);
redisUtil.expire(productCacheKey, 60 * 60 * 24 + new Random().nextInt(5) * 60 * 60, TimeUnit.SECONDS); //读延期
}
return product;
}
缓存与数据库双写不一致
如图所示,线程 1 更新数据库后没有更新缓存,线程 2 获得资源更新了数据库和缓存,在这之后线程1 获得资源并把它设置的 10 更新进缓存。此时数据库中数据是 6,缓存中数据是 10,导致数据库与缓存不一致。
- 线程 1 读取数据也会有可能发生这个问题。
- 线程 1 更新数据库后删除缓存,线程 2 执行流程不变。但是当线程 3 先查询到线程 1 的值 10,还未更新缓存,线程 2 流程走完,这时线程 3 更新缓存为 10。导致读写不一致。
解决方案: 在读多谢少的情况下,不能容忍缓存数据不一致,可以为线程 1 的两个操作加锁,为了提升系统性能,查询数据库 – 更新缓存使用读锁,修改数据库 – 更新缓存使用写锁。