緩存穿透
什麼是緩存穿透?緩存裏面不存在數據,數據庫裏面也不存在的數據。新的請求(例如黑客惡意攻擊: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;
}
.......
}