Redis-06 Redis面试高频问题、Redis日常开发规避问题
众所周知,使用Redis可极大提升查询效率,也可提供分布式锁解决方案,本章节将根据日常开发中可能出现的实际场景重点讲解使用Redis的架构设计相关问题,以及如何设计高效的分布式锁。
1,缓存穿透
查询缓存和数据库中都没有的数据,导致请求都下沉至数据库,造成数据库压力过大。
常用场景:
1,程序判断机制有误,并发场景下大量查询下沉至数据库;
2,黑客攻击,通过缓存穿透方式打垮数据库,造成系统崩溃。
解决方案:
1,前端程序做限频率
1,后端程序做基本判断。ID<0直接返回;
2,缓存空对象并设置过期时间。可使相同数据第二次请求时快速返回;
3,使用布隆过滤器。通过bitmap将所有对象ID缓存至布隆过滤器,请求先通过布隆过滤器判断对象是否存在,不存在则直接返回,存在再去Redis或数据库拿取数据。
2,缓存击穿
查询缓存中有但数据库没有的数据,导致请求都下沉至数据库,造成数据库压力过大。
常用场景:
1,大批量缓存数据在同一时间或临近时间失效,导致查询下沉;
2,少量缓存数据失效,大并发请求该数据,同样导致查询下沉。
解决方案:
1,查询时为缓存续命,每次查询添加或重置过期时间,引导冷热数据分离;
2,设置缓存的过期时间时,在时间基数上添加随机时间,造成缓存过期时间不同。
3,缓存雪崩
解释一:Redis服务宕机,所有查询直接流向数据库,导致数据压力变大,查询变慢甚至宕机;
解释二:大批量数据在同一时间或临近时间段内失效,后续大量查询流向数据库,导致数据压力变大,查询变慢甚至宕机。
解决方案:同缓存击穿
4,热点key重建优化
常用场景:
某个热点消息或热点商品的缓存过期后,大并发请求查询,可能造成多个线程重建缓存,导致程序和Redis服务压力过大
解决方案:
重建缓存时加分布式锁,并使用双重验证法。
加分布式锁可使得只能有一个线程重建缓存;双重验证可使得其余线程执行时直接从缓存获取。伪代码如下:
String get(String key) {
// 从Redis中获取数据
String value = redis.get(key);
if (value != null){
return value;
}
// 如果value为空, 则开始重构缓存
// 只允许一个线程重建缓存, 使用nx, 并设置过期时间ex
String lockKey = "lock:key:" + key;
if (redis.set(lockKey , "1", "ex 180", "nx")) {
// 双重验证。从Redis中获取
value = redis.get(key);
if (value ==null){
// 从数据源获取数据
value = db.get(key);
// 回写Redis, 并设置过期时间
redis.setex(key, timeout, value);
} else {
// 重置过期时间
redis.expire(key, timeout);
}
// 删除锁
redis.delete(mutexKey);
}
// 其他线程休息50毫秒后重试
else {
Thread.sleep(50);
get(key);
}
return value;
}
5,缓存与数据库双写不一致
如下图:线程1在写完数据库后,写Redis前因程序或网络导致延迟,线程2在延迟期间通过数据库查询到该值,更新后写至数据库和Redis。线程1再写Redis时,写入的就是错误的旧值。
解决方案:
使用redisson的读写锁,读写锁对Redis的读读不互斥,读写/写写互斥,保证在同一时刻,只能有一个线程写Redis。
同上例,若使用了读写锁,线程1在写Redis时卡顿,线程2对Redis的写操作将会阻塞,等待线程1写完Redis并释放锁后,线程2才能获取锁再写Redis,保证了Redis数据为最新值。
注意:
1,双写不一致现象为小概率事件。并发量不高的系统,几乎不用考虑;
2,Redis数据一般都会设置过期时间,如果可以容忍短期数据不一致,也不用考虑。