关于Redis的击穿穿透与雪崩
知识点助记
就比如你开了一个商店,Redis就像放在门外面的小货架,那上面摆放着最畅销的商品。
但是有这么三种情况会造成商店崩溃:
- 缓存击穿:有一个很热门的商品正好过期了,这个时候涌入了一大批购买这个商品的顾客,所有人都涌向了你的小商店去买这一个物品,造成系统瘫痪。
- 缓存穿透:有一个调皮的小孩子,他一直管你要你商店里没有的东西,这导致店员每次都要去找这个不存在的商品。大量这样的请求会让店员累坏。
- 缓存雪崩:货架上很多畅销的产品在同一时间过期,这个时候来了很多顾客来买这些商品,导致大量请求瞬间砸向商店,造成系统瘫痪。
解决办法
缓存击穿
核心思路就是防止多个请求同时去数据库查询同一个失效的热点key
- 对于极热点key设置永不过期。通过后台任务或者消息队列在数据更新时主动刷新缓存。
- 互斥锁:当第一个请求发现缓存失效时,他并不直接去查数据库,而是先获取一个分布式锁。获取锁成功的请求才有资格去查询数据表并
重建缓存。其他请求则等待,缓存重建完后直接从Redis中获取数据。
public String getData(String key){ String data = redis.get(key); if (data == null){ if (tryLock(key)){ try{ data = redis.get(key); if (data == null) { data = dataMapper.getData(key); redis.set(key, 36000, data); } } finally { unlock(key); } } else { Thread.sleep(50); return getData(key); } } return data; }
|
缓存穿透
核心思路是即使数据不存在,也要在缓存中留下一个标记,为数据库挡住恶意无效请求。
- 缓存空对象:如果从数据库中查询数据不存在,我们仍然要向Redis中写入一个空值(如null,””),并设置一个较短的过期时间(3-5min)。
这样缓存空对象,就可以防止缓存穿透。较短的过期也可以及时在Redis中释放无效key
- 布隆过滤器:在访问缓存和数据库之前先访问布隆过滤器。
布隆过滤器的原理就是告诉你某个key一定不存在或者可能存在
在系统启动的时候,将所有可能存在的key预先加载到布隆过滤器,当请求过来的时候先用布隆过滤器判断。
如果返回不存在,则直接返回空。如果可能存在,那就继续查询缓存和数据库。
缓存雪崩
核心思路是避免大量热点Key在同一时间过期。
- 设置随机过期时间:我们可以在基础值上添加一个随机值。
关于布隆过滤器的拓展
布隆过滤器告诉你这个key存在,那只是可能存在。但是他说这个key不存在那就一定没有。
其实布隆过滤器的底层实现就是一个超大的位数组(Bit Array) + 多个独立的哈希函数。
位数组所有位置默认是0.哈希函数将任意输入的key映射到位数组的某个位置。
它可以用极小的空间,极快的查询效率(时间复杂度O(1))简单判断key是否存在。
具体实现
布隆过滤器的操作只有
- 添加(Add)
将要添加的元素item分别通过k个哈希函数计算,得到k个哈希值
将这k个哈希值对位数组长度m取模,得到k个位置索引。
将位数组这k个位置的值设置为1
public void add(String item) { for(int i = 0; i < k; i++) { int index = hashFunction[i].hash(item) % m; bitArray.set(index, true); } }
|
- 查询(Contains)
将要查询的元素item同样通过k个哈希函数计算并取模,得到k个位置索引。
如果所有位置都是1,则返回true(可能存在),否则返回false(一定不存在)。
public boolean contains(String item){ for(int i = 0; i < k; i++) { int index = hashFunction[i].hash(item) % m; if(!bitArray.get(index)) { return false; } } return true; }
|
主要参数
- m 位数组长度。m越大误判率越低,但空间消耗越大
- k 哈希函数数量。k越大误判率越低,但计算开销大,且位数组更快被填满(增加误判)
- n 预期元素数量。就是你预计要存多少个元素
- p 期望误判率。可以容忍误判率多少
公式:
最优哈希函数数量:k = ln(2) * (m / n)
误判率:p = (1 - e^(-k * n / m))^k
Java集成Redission实现布隆过滤器
- 引入pom依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.16.6</version> </dependency>
|
- 配置Redission客户端
@Configuration public class RedissonConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private String port;
@Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer() .setAddress("redis://" + host + ":" + port); return Redisson.create(config); } }
|
- 初始化布隆过滤器
@Service public class BloomFilterService { @Resource private RedissonClient redissonClient; @Resource private DataService dataService;
@PostConstruct public void initBloomFilter() { RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("userFilter"); bloomFilter.tryInit(1000000L, 0.01); List<String> ids = dataService.getAllIds(); ids.forEach(bloomFilter::add); } }
|
- 业务逻辑中使用布隆过滤器
public User getUserById(String id) { RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("userFilter"); if (!bloomFilter.contains(id)) { return null; } User user = redisTemplate.opsForValue().get("user:" + id); if (user != null) { return user; } user = userRepository.findById(id); if (user != null) { redisTemplate.opsForValue().set("user:" + id, user, 30, TimeUnit.MINUTES); } else { redisTemplate.opsForValue().set("user:" + id, NullValue.DEFAULT, 5, TimeUnit.MINUTES); } return user; }
|