为了应对越来越大的流量,缓存便成为系统服务必不可少的一部分,但使用缓存就会出现缓存击穿和缓存穿透的威胁。
一、背景介绍
二、缓存穿透常见的处理方式
2.1 空值缓存
这里需要强调注意:为了系统的最终一致性,这些key必须设置过期时间,或者必须存在更新方式,防止这个key的数据后期真实存在,但改key始终为空,导致数据不一致的情况出现。
public String getById(String key) {
String value = jimdbClient.get(key);
if (value == null) {
value = testMapper.get(key);
if (value == null) {
jimdbClient.set(key, null, 60, TimeUnit.SECONDS, false);
return null;
}
}
}
这种方式的缺点也十分的明显:如果key数量巨大且分散无任何规律,就会浪费大量缓存空间,并且不能抗住瞬时流量冲击(尤其是遇到恶意的攻击的时候,有可能将缓存空间打爆,影响范围更大),需要额外配置降级开关(查询数据库的开关或者限流),这时本方案就显得没想象的那么美好。针对不能抗住瞬时流量的情况,常见的处理方式是使用计数器,对不存在的key进行计数,当某个key在一定时间达到一定的量级,就查询一次数据库,按照数据库的返回值对key进行缓存。未达指定阈值数量之前,按照商定的空值返回。
2.2 布隆过滤器(BloomFilter)
public void put(String rediskey, String key) {
long[] indexs = getIndexs(key);
for (long index : indexs) {
jimdbClient.setBit(rediskey, index,true);
}
}
/**
* 根据key获取bitmap下标
*/
private long[] getIndexs(String key) {
long hash1 = hash(key);
long hash2 = hash1 >>> 16;
long[] result = new long[numHashFunctions];
for (int i = 0; i < numHashFunctions; i++) {
long combinedHash = hash1 + i * hash2;
if (combinedHash < 0) {
combinedHash = ~combinedHash;
}
result[i] = combinedHash % numBits;
}
return result;
}
/**
* 获取一个hash值
*/
private long hash(String key) {
Charset charset = Charset.forName("UTF-8");
return Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asLong();
}
这里需要强调注意的是必须使用高效的hash算法,否则这种方式会严重影响系统的性能,建议的算法包括MurmurHash、Fnv的稳定高效的算法。
三、缓存击穿常见的处理方式
3.1 互斥锁(mutex key)
public String getById(String key) {
String value = jimdbClient.get(key);
if (value == null) {
boolean nx = jimdbClient.set(lock_key, "test-lock", 2, TimeUnit.SECONDS, false);
if (nx) {
value = testMapper.get(key);
jimdbClient.set(key, value, 60 * 60 * 24, TimeUnit.SECONDS, false);
return value;
} else {
Thread.sleep(100);
return getById(key);
}
}
}
Redis还是善解人意的,从 2.6.12 起,我们可以使用SET命令完成SETNX和EXPIRE的操作,并且这种操作是原子操作,可以完全替代上述的代码了。
3.2 异步构建缓存
四、缓存雪崩的常见的处理方式
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
public String getById(String key) {
Random random = new Random();
int r = random.nextInt(100);
String value = jimdbClient.get(key);
if (value == null) {
boolean nx = jimdbClient.set(lock_key, "test-lock", 2, TimeUnit.SECONDS, false);
if (nx) {
value = testMapper.get(key);
jimdbClient.set(key, value, r, TimeUnit.SECONDS, false);
return value;
} else {
Thread.sleep(100);
return getById(key);
}
}
}
五、总结
综上所述,针对常见的缓存穿透和缓存击穿的问题,各自的优缺点如下: