什么是缓存穿透
场景如下图所示:
黑客每次故意查询一个在缓存内必然不存在的数据,导致每次请求都要去数据库中去查询,这样缓存就失去了意义。如果几十万的大请求越过缓存,直接怼到数据库,数据库很可能挂掉,造成整体服务荡掉,这就是缓存穿透。
解决方案
在这里我们给出三套解决方案,大家根据项目中的实际情况,选择使用。
1、使用互斥锁
该方法是比较普遍的做法,即,在根据key获得的value值为空时,先锁上,再从数据库加载,加载完毕,释放锁。若其他线程发现获取锁失败,则睡眠50ms后重试。
至于锁的类型,单机环境用并发包的Lock类型就行,集群环境则使用分布式锁( redis的setnx)
集群环境的redis的代码如下所示:
#伪代码
String get(String key) {
String value = redis.get(key);
if (value == null) {
if (redis.setnx(key_mutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(key_mutex, 3 * 60)
value = db.get(key);
redis.set(key, value);
redis.delete(key_mutex);
} else {
//其他线程休息50毫秒后重试
Thread.sleep(50);
get(key);
}
}
}
优点
-
思路简单
-
保证一致性
缺点
-
代码复杂度增大
-
存在死锁的风险
-
小型项目可用
2、异步构建缓存
在这种方案下,构建缓存采取异步策略,会从线程池中取线程来异步构建缓存,从而不会让所有的请求直接怼到数据库上。该方案redis自己维护一个timeout,当timeout小于System.currentTimeMillis()时,则进行缓存更新,否则直接返回value值。
集群环境的redis代码如下所示:
#伪代码
String get(final String key) {
V v = redis.get(key);
String value = v.getValue();
long timeout = v.getTimeout();
if (v.timeout <= System.currentTimeMillis()) {
// 异步更新缓存
threadPool.execute(new Runnable() {
public void run() {
String keyMutex = "mutex:" + key;
if (redis.setnx(keyMutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(keyMutex, 3 * 60);
String dbValue = db.get(key);
redis.set(key, dbValue);
redis.delete(keyMutex);
}
}
});
}
return value;
}
优点
-
性价最佳,用户无需等待
缺点
-
无法保证缓存一致性
-
需要进行缓存预热操作,将数据提前放入缓存
3、布隆过滤器(推荐)
布隆过滤器的巨大用处就是,能够迅速判断一个元素是否在一个集合中。因此非常适合解决缓存穿透问题,将已存在的数据放到布隆过滤器中,当黑客访问不存在的缓存时,迅速返回null,避免缓存及DB挂掉。
guava包已经帮我们实现布隆过滤器的java版本了,可以直接拿来用。
springboot2.x以上版本,已经帮我们导入guava包的依赖了。
guava包的依赖是:
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>22.0</version>
</dependency>
</dependencies>
实际使用步骤
-
将需要缓存的key,先存入布隆过滤器中,代码如下所示:
import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import com.orange.shop.bo.Order;
import com.orange.shop.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import java.util.List;
/**
* @ClassName: LegalIdsBloomFilter
* @Auther: chw
* @Description: 布隆过滤器
* @Vsersion: 1.0
*/
@Configuration
@EnableScheduling //开启定时器
@Slf4j
public class LegalIdsBloomFilter {
@Autowired
private OrderMapper orderMapper;
//容量
private static int capacity=1000000
//创建一个能够容纳100万个元素且容错率默认为0.03布隆过滤器
private static final BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), capacity);
/**
* 开启定时任务,将合法id存入过滤器
* 每5分钟执行一次
*/
@Scheduled(cron = "0 0/5 * * * ?")
public void initLegalIdsBloomFilter() {
log.info("................定时调度任务-配置布隆过滤器...............");
List<Order> orders = orderMapper.selectAll();
orders.forEach(order -> bloomFilter.put(order.getId()));
}
/**
* 布隆过滤器匹配
* @param id
* @return
*/
public boolean checkLegalId(String id) {
return bloomFilter.mightContain(id);
}
}
2. 布隆过滤器先匹配传来的key,不存在直接返回null;如果存在,先读取缓存,缓存没有,再读取db,这样可大幅度阻挡不合法key侵入,从而保护缓存和db,代码如下所示:
/**
* 测试缓存穿透-布隆过滤器
* @param id
* @param threadName
* @return
*/
@Override
public List<Order> getObjectByBloom(String id, String threadName) {
//返回结果
List<Order> orderList = new ArrayList<>();
//判断是否为合法id
if (!legalIdsBloomFilter.checkLegalId(id)) {
//不合法id,直接返回为null
log.info("当前线程:[{}],return by bloomFilter", threadName);
return null;
} else {
//从缓存中获取
String redisKey = id;
String redisValue = redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotBlank(redisValue)) {
log.info("当前线程:[{}],from Redis", threadName);
orderList = JSON.parseArray(redisValue, Order.class);
} else {
//查询数据库
log.info("当前线程:[{}],from DB", threadName);
Order order = orderMapper.selectByPrimaryKey(id);
orderList.add(order);
//存入redis
redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(orderList), 3L, TimeUnit.MINUTES);
}
}
return orderList;
}
优点
-
思路简单
-
保证一致性
-
性能强
-
适合大型项目使用
缺点
-
代码复杂度增大
-
需要维护一个布隆过滤器,来存放缓存的key
-
布隆过滤器不支持删值操作