一、基本概念
1、缓存穿透
缓存穿透:
指缓存和数据库中都没有用户需要查询的数据(即根本不存在的数据),而用户不断发起请求,那么缓存层和存储层都不会命中,从而导致数据库压力过大。
缓存穿透将导致每次请求根本不存在的数据都要到存储层去查询,失去了缓存层保护后端存储层的意义。可能会使后端存储负载加大,由于很多后端存储不具备高并发性,甚至可能造成后端存储宕掉。
1.1 造成原因
- 自身业务代码或者数据出现问题
比如,我们数据库的 id 都是 1 开始自增上去的,请求id=-1,如果不做校验。 - 一些恶意攻击、爬虫等造成大量空命中。
1.2 解决方案
- 接口层增加校验
比如:用户鉴权校验,id做基础校验,id<=0的直接拦截; 缓存空对象
当存储层不命中后,仍然将空对象保留到缓存层中(一般设置一个较短的过期时间,让其自动剔除),之后再访问这个数据将会从缓存中获取,这样就保护了后端数据源。
2、缓存击穿
缓存击穿:
指缓存中没有但是数据库中有的数据(一般是缓存时间到期),这时持续的大并发请求同时来读缓存没读到数据,所以又直接同时去数据库查数据,从而引起数据库压力瞬间增大。
缓存击穿可能会使后端存储负载加大,甚至可能造成后端存储宕掉。
2.1 解决方案
比较常用的方案就是 使用互斥锁
。
3、缓存雪崩
缓存雪崩的英文原意是 stampeding herd(奔逃的野牛)
,指的是缓存层宕掉后,流量会像奔逃的野牛一样,打向后端存储。
缓存雪崩:
指同一时间缓存中数据大批量到了过期时间,而查询请求量巨大,于是所有的请求都会达到存储层,存储层调用量会暴增,从而引起数据库压力过大甚至级联宕机的情况。
3.1 解决方案
- 保证缓存层服务高可用性。比如:搭建 Redis集群架构
- 依赖隔离组件为后端限流并降级。
将缓存失效时间分散开
(在原有的失效时间基础上增加一 个随机值)。- 提前演练。
4、三者的区别
缓存穿透、缓存击穿和缓存雪崩的区别:
- 缓存穿透针对的是根本不存在的数据,穿透缓冲层和存储层,访问存储层压力加大。
- 缓存击穿针对的是某一个 key 的缓存失效,击穿缓冲层,访问存储层压力加大。
- 缓存雪崩针对的是同一时刻大批量 key 的缓存失效,击穿缓冲层,访问存储层压力压力过大甚至级联宕机的情况。
二、实战代码
针对上面问题,会采取不同的解决方案,这里提供一种,在开发过程中可作参考。
以商品为例,RedisUtil是简单封装的 Redis 操作方法工具类。
@Service
public class ProductService {
@Autowired
private ProductDao productDao;
@Autowired
private RedisUtil redisUtil;
@Autowired
private RedissonClient redissonClient;
/**
* 商品缓存生存时间
*/
public static final Integer PRODUCT_CACHE_TIMEOUT = 60 * 60 * 24;
/**
* 空对象:解决缓存穿透问题
*/
public static final String EMPTY_CACHE = "{}";
/**
* 商品查询分布式锁Key:解决热点缓存并发重建问题
*/
public static final String LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX = "lock:product:hot_cache_create:";
/**
* 商品修改分布式读写锁Key:解决缓存双写不一致问题
*/
public static final String LOCK_PRODUCT_UPDATE_PREFIX = "lock:product:update:";
@Transactional
public Product create(Product product) {
//1.入库
Product productResult = productDao.create(product);
//2.更新缓存
redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
return productResult;
}
@Transactional
public Product update(Product product) {
Product productResult = null;
RReadWriteLock productUpdateLock = redissonClient.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
RLock writeLock = productUpdateLock.writeLock();
//1.加分布式读写锁(写锁):解决缓存双写不一致问题
writeLock.lock();
try {
//2.更新数据库
productResult = productDao.update(product);
//3.更新缓存
redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
genProductCacheTimeout(), TimeUnit.SECONDS);
} finally {
//1.解分布式读写锁(写锁)
writeLock.unlock();
}
return productResult;
}
public Product get(Long productId) {
Product product = null;
String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
//1.从缓存里查数据
product = getProductFromCache(productCacheKey);
if (product != null) {
return product;
}
//2.加分布式锁:解决热点缓存并发重建问题
RLock hotCreateCacheLock = redissonClient.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX + productId);
hotCreateCacheLock.lock();
// 这个优化谨慎使用
// hotCreateCacheLock.tryLock(1, TimeUnit.SECONDS);
try {
//再次从缓存里查数据
product = getProductFromCache(productCacheKey);
if (product != null) {
return product;
}
//3.加分布式读写锁(读锁):解决缓存双写不一致问题
RReadWriteLock productUpdateLock = redissonClient.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
RLock rLock = productUpdateLock.readLock();
rLock.lock();
try {
//4.查询数据库
product = productDao.get(productId);
if (product != null) {//如果存在,记录放到缓存
redisUtil.set(productCacheKey, JSON.toJSONString(product),
genProductCacheTimeout(), TimeUnit.SECONDS);
} else {//如果不存在
//设置空缓存:解决缓存穿透问题
redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
}
} finally {
//解分布式读写锁(读锁)
rLock.unlock();
}
} finally {
//解分布式锁
hotCreateCacheLock.unlock();
}
return product;
}
/**
* 将缓存失效时间分散开(在原有的失效时间基础上增加一 个随机值)
*/
private Integer genProductCacheTimeout() {
return PRODUCT_CACHE_TIMEOUT + new Random().nextInt(5) * 60 * 60;
}
private Integer genEmptyCacheTimeout() {
//空对象,时间短一些
return 60 + new Random().nextInt(30);
}
private Product getProductFromCache(String productCacheKey) {
Product product = null;
//1.查询缓存
String productStr = redisUtil.get(productCacheKey);
if (!StringUtils.isEmpty(productStr)) {
// 2.判断是不是空对象
if (EMPTY_CACHE.equals(productStr)) {
// 如果是空对象,key续期
redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
return new Product(); //返回空实例(注意和null的区别),和前端约定好,id=null为空对象。
}
//3. 如果不是空对象,返回缓存数据,并将 key续期
product = JSON.parseObject(productStr, Product.class);
//缓存读延期
redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS);
}
return product;
}
}
– 求知若饥,虚心若愚。