Redis 缓存常见问题
本文为博主原创,未经允许不得转载:
目录:
1. 缓存穿透
1.1 出现原因
1.2 解决方案
1.3 布隆过滤器
2. 缓存雪崩
3. 缓存失效(缓存击穿,热点缓存)
1. 缓存穿透:
缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中,如果从存储层查不到数据则不写入缓存层。将会导致缓存失去作用,也就失去了缓存提高性能和保护后端服务的作用了。如果大量的类似请求过来,将会占用数据库连接资源,使得服务性能下降。
1.1 出现原因:
1. 大量恶意请求
2. 自身业务代码或者数据出现问题
1.2 解决方案:
1. 对缓存不存在的key,存储一个空对象,并设置过期时间
String get(String key) { // 从缓存中获取数据 String cacheValue = cache.get(key); // 缓存为空 if (StringUtils.isBlank(cacheValue)) { // 从存储中获取 String storageValue = storage.get(key); cache.set(key, storageValue); // 如果存储数据为空, 需要设置一个过期时间(300秒) if (storageValue == null) { cache.expire(key, 60 * 5); } return storageValue; } else { // 缓存非空 return cacheValue; } }
2. 使用布隆过滤器:
布隆过滤器就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。
向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。如果这个位数组比较稀疏,这个概率就会很大,如果这个位数组比较拥挤,这个概率就会降低。
这种方法适用于数据命中不高、 数据相对固定、 实时性低(通常是数据集较大) 的应用场景, 代码维护较为复杂, 但是缓存空间占用很少。
使用 redission 的布隆过滤器:
引入依赖:
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.6.5</version> </dependency>
示例代码:
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("nameList"); //初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小 bloomFilter.tryInit(100000000L,0.03); //将zhuge插入到布隆过滤器中 bloomFilter.add("test2"); //判断下面号码是否在布隆过滤器中 System.out.println(bloomFilter.contains("test1"));//false System.out.println(bloomFilter.contains("test2"));//false System.out.println(bloomFilter.contains("test3"));//true } }
2. 缓存雪崩
缓存雪崩指的是缓存层支撑不住或宕掉后,请求会到后端数据库层。由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访bigkey,导致缓存能支撑的并发急剧下降), 于是大量请求都会到存储层, 存储层的调用量会暴增, 造成数据库层也会级联宕机的情况。
解决方案:
- 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
- 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
- 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
3.缓存击穿
缓存击穿,就是说某个 key 非常热点,突然之间访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
不同场景下的解决方式可如下:
- 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期。
- 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。
- 若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。
第二种场景伪代码:
String get(String key) { // 从Redis中获取数据 String value = redis.get(key); // 如果value为空, 则开始重构缓存 if (value == null) { // 只允许一个线程重建缓存, 使用nx, 并设置过期时间ex String mutexKey = "mutext:key:" + key; if (redis.set(mutexKey, "1", "ex 180", "nx")) { // 从数据源获取数据 value = db.get(key); // 回写Redis, 并设置过期时间 redis.setex(key, timeout, value); // 删除key_mutex redis.delete(mutexKey); }// 其他线程休息50毫秒后重试 else { Thread.sleep(50); get(key); } } return value; }