缓存穿透
什么是缓存穿透?缓存里面不存在数据,数据库里面也不存在的数据。新的请求(例如黑客恶意攻击:https://item.jd.com/6729892714444444.html查询一个不存在商品)进来会不断查询数据库,严重可能会导致数据库服务停止。
Null值返回解决方案:
如果数据库查询不到数据,缓存Null值的对象返回。
布隆过滤器
显然返回Null值方案存在问题,如果查询编码不存在数据,之后又新增了此编号的数据,将导致此数据永远查不到。
布隆过滤器(性能问题不用担心,可以自行查阅资料),例如将商品所有Id加入布隆过滤器,后续访问必须先经过bloom布隆过滤器判断是否存在,如果不存在就直接返回,否则放行。以下是bloom过滤器的redis实现。
bloom.filter.expectedInsertions=10000000
bloom.filter.fpp=0.001F
// 基于Java 配置
@ConfigurationProperties("bloom.filter")
@Component
public class RedisBloomFilter {
private static final String BLOOM_NAME = "bf.name";
//预计插入量
@Getter @Setter
private long expectedInsertions;
//可接收错误率
@Getter @Setter
private double fpp;
//bit数组长度
@Getter @Setter
private long numBits;
//hash函数数量
@Getter @Setter
private int numHashFunctions;
@Autowired
private RedisTemplate redisTemplate;
@PostConstruct
public void init(){
this.numBits = optimalNumOfBits(expectedInsertions, fpp);
this.numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
}
/**
* 计算bit数组长度
* @return
*/
private long optimalNumOfBits(long n, double p){
if (p == 0){
p = Double.MIN_VALUE;
}
return (long)(-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
/**
* 计算hash函数个数
* @return
*/
private int optimalNumOfHashFunctions(long n, long m){
return Math.max(1, (int)Math.round((double)m / n * Math.log(2)));
}
/**
* 判断keys是否存在于集合中
* @param key
* @return
*/
public boolean isExist(String key){
long[] indexs = getIndexs(key);
List list = redisTemplate.executePipelined(new RedisCallback<Object>() {
@Nullable
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException{
connection.openPipeline();
for (long index: indexs){
//加入布隆过滤器
connection.getBit(BLOOM_NAME.getBytes(), index);
}
connection.close();
return null;
}
});
return !list.contains(false);
}
/**
* 将key存入redis bitmap
* @param key
*/
public void put(String key){
long[] indexs = getIndexs(key);
redisTemplate.executePipelined(new RedisCallback<Object>() {
@Nullable
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException{
connection.openPipeline();
for (long index: indexs){
//加入布隆过滤器
connection.setBit(BLOOM_NAME.getBytes(), index, true);
}
connection.close();
return null;
}
});
}
/**
* 根据key获取bitmap的下标
* @param key
* @return
*/
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 combineHash = hash1 + i * hash2;
if (combineHash < 0){
combineHash = ~combineHash;
}
result[i] = combineHash % numBits;
}
return result;
}
}
实战应用
/*
* 系统初始化,将所有商品ID加入布隆过滤器,后续增加商品ID也加入布隆过利器
* 注意:如果是在分布式情况下,使用分布式锁限定一次创建即可
*/
@PostConstruct
public void init(){
List<Product> products = productService.findAll();
products.forEach(p->{
redisBloomFilter.put(String.valueOf(p.getProductId()));
});
}
//布隆过滤器判断商品是否存在,不存在直接返回
public Product getProductById(Long productId){
log.debug("查询商品信息id:{}", productId);
//先走布隆过滤器【缓存穿透】
if (!redisBloomFilter.isExist(String.valueOf(productId))){
return null;
}
.......
}