Redis缓存穿透,缓存击穿,缓存雪崩分析
1、简介:
Redis 作为一个典型的非关系型Nosql数据库,数据存储在内存中,读写速度非常快。使用Redis主要是防止大量的请求直接访问到数据库(Mysql等),产生大量的磁盘 I/O, 最终导致服务不可用。
特点: key-value键值类型存储系统、支持数据可靠存储(RDB、AOF持久化)、单进程单线程高性能服务器、单机qps(秒并发)可以达到10W
使用场景: 存储一些对数据的一致性要求不是特别高的数据、不经常改变的数据、但是经常性访问的数据。
当然使用Redis也不是能解决所有的问题,依然也存在弊端:
2、缓存穿透
每当请求从缓存中获取不到值时,请求就会到最底层的数据库(Mysql)中查找,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,如果在高并发下大量的请求进入,从而可能压垮数据库(Mysql)。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
缓存穿透解决方案
最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
方式二:缓存空值
//缓存这个空的值【伪代码】
public object GetProductList() {
int cacheTime = 30;
String cacheKey = "product_list";
cacheValue = CacheHelper.Get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
//数据库查询不到,为空
cacheValue = ProductDao.queryProducts;
if (cacheValue == null) {
//如果发现为空,设置个默认值,也缓存起来
cacheValue = string.Empty;
}
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
return cacheValue;
}
}
3、缓存击穿
当我们使用Redis时,对于一些经常访问的数据,我们会放在Redis中,但为了保证数据尽量的一致性,我们会设置Key的过期时间,在高并发场景下,如果Key有效的情况下,Redis会帮我们抵挡很多的请求,防止请求直接打入底层数据库(Mysql), 但是如果刚好在高并发场景下,这个热点Key突然失效了,那么这时所有的请求就会进入数据库,可能我们还没来得及查询数据库并放入缓存,瞬间就把后面的DB压垮了。
缓存击穿解决方案:
要解决这个问题主要就是要避免大量线程同时重建缓存。 我们可以利用互斥锁来解决,此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从 缓存获取数据即可。
比如Redis的SETNX去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
最佳示例:
public static final Integer PRODUCT_CACHE_TIMEOUT = 60 * 60 * 24;
public static final String EMPTY_CACHE = "{}";
public static final String LOCK_PRODUCT_HOT_CACHE_PREFIX = "lock:product:hot_cache:";
public static final String LOCK_PRODUCT_UPDATE_PREFIX = "lock:product:update:";
public Product get(Long productId) throws InterruptedException {
Product product = null;
String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
// 缓存更新双重检查1
product = getProductFromCache(productCacheKey);
if (product != null) {
return product;
}
// 加锁防止多个线程同时查询mysql并更新缓存
RLock hotCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
hotCacheLock.lock();
//boolean result = hotCacheLock.tryLock(3, TimeUnit.SECONDS);
try {
// 缓存更新双重检查2
product = getProductFromCache(productCacheKey);
if (product != null) {
return product;
}
// 加读锁,解决双写不一致(mysql与redis数据不一致),其他更新缓存得地方也要加读锁(修改产品信息等)
RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
RLock rLock = readWriteLock.readLock();
rLock.lock();
try {
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 {
hotCacheLock.unlock();
}
return product;
}
private Product getProductFromCache(String productCacheKey) {
String productStr = redisUtil.get(productCacheKey);
if (!StringUtils.isEmpty(productStr)) {
// 解决缓存穿透(缓存与数据库都没有查到数据,导致每次请求都会打到底层数据库)
if (EMPTY_CACHE.equals(productStr)) {
redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
return new Product();
}
product = JSON.parseObject(productStr, Product.class);
redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS); //读延期
}
return product;
}
// 缓存过期时间加随机数防止大批量缓存同时失效
private Integer genProductCacheTimeout() {
return PRODUCT_CACHE_TIMEOUT + new Random().nextInt(5) * 60 * 60;
}
private Integer genEmptyCacheTimeout() {
return 60 + new Random().nextInt(30);
}
关于缓存优化双重检查。
4、缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
缓存雪崩 解决方案
大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
5、总结
- 缓存穿透:程序中没有缓存null值;当大量请求获取一个不存在的数据时,由于缓存中没有缓存到null值,大量请求直接访问数据库,数据库压力陡增,从而出现穿透问题!
- 解决方案:将查询结果为null的值缓存到redis中
- 缓存雪崩:大量缓存同一个时间内失效;
- 解决方案:在设置数据有效时间时,增加一个随机数
- 缓存击穿:大量请求同时访问同一个正好过期的缓存数据
- 解决方案:添加分布式锁

浙公网安备 33010602011771号