数据一致性问题、缓存雪崩(缓存击穿)及缓存穿透
在我们实际项目中,如果其性能达到了瓶颈,特别是对于数据库的访问压力来说,那么我们首先想到的肯定是使缓存来解决,但是一旦使用到了缓存,就可能会涉及到有数据一致性的问题。
数据一致性问题
对于其数据一致性问题,一般主要有从下图中的四个方案来进行分析处理,如下:
方案名称 | 技术特点 | 优点 | 缺点 | 使用场景 |
---|---|---|---|---|
缓存失效机制 | 弱一致性,基于缓存本身的失效机制 | 实现简单 | 有一定延迟,不保证强一致性,存在缓存雪崩问题 | 适合读多写少的场景,能接受一定数据延时 |
任务调度更新 | 最终一致性,采用任务调度框架,按照一定频率更新 | 不影响正常业务 | 不保证一致性,依赖定时任务,容易堆积垃圾数据 | 适合复杂统计类数据缓存更新,对数据一致实时性要求低的场景;如:统计类数据,BI分析等 |
数据实时同步更新 | 强一致性,更新数据库同时更新缓存,使用缓存工具类和或编码实现 | 数据一致性强 | 代码耦合,运行期耦合,影响正常业务 | 数据一致实时性要求比较高的场景,如:银行业务、证券交易 |
数据准实时更新 | 准一致性,更新数据库后,异步更新缓存,使用观察者模式/发布订阅/MQ实现 | 数据同步有较短延迟,与业务解耦,不影响正常业务 | 有较短延迟,需要补偿机制 | 不适合写操作频繁并且数据一致实时性要求严格的场景 |
缓存雪崩
在实际项目中,可能会使用上述中的缓存失效机制,但是如果当缓存服务器重启或者大量缓存集中在某一个时间段失效,而这时又有大量的查询请求,这时所有的请求都会到达数据库,就有可能引起数据库压力过大甚至宕机,这就是缓存雪崩。
那么我们应该如何避免了,我们可以从一下几点来
- 在缓存服务器重启后进行缓存预热,比如对于常用值可以使用http接口预热错峰加载
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
- 设置热点数据永远不过期。
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
这里我们来看看最后一点,通过加锁或者队列来控制读数据库写缓存的线程数量,应该如何实现,假设我们采用了Spring Cache对数据进行缓存(有关Spring Cache相关可见Spring Cache缓存),处理如下:
@Service
public class UserCacheServiceImpl extends UserServiceImpl {
private static final String CACHE_NAME = "userService";
private final ConcurrentHashMap<Object, ReentrantLock> locks = new ConcurrentHashMap<>();
@Resource
private CacheManager cacheManager;
@Override
public User findById(Long id) {
// 1.从缓存中取数据
Cache.ValueWrapper valueWrapper = cacheManager.getCache(CACHE_NAME).get(id);
if (valueWrapper != null) {
return (User) (valueWrapper.get());
}
//2.加锁排队,阻塞式锁
acquireLock(id);
try {
//双重校验,为了避免缓存击穿
valueWrapper = cacheManager.getCache(CACHE_NAME).get(id);
if (valueWrapper != null) {
return (User) (valueWrapper.get());
}
//3.从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询
User user = super.findById(id);
if (null != user) {
cacheManager.getCache(CACHE_NAME).put(id, user);
}
return user;
} catch (Exception e) {
return null;
} finally {
//4.解锁
releaseLock(id);
}
}
private void acquireLock(Object key) {
Lock lock = getLockForKey(key);
lock.lock();
}
private ReentrantLock getLockForKey(Object key) {
ReentrantLock lock = new ReentrantLock();
ReentrantLock previous = locks.putIfAbsent(key, lock);
return previous == null ? lock : previous;
}
private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
对于上述代码我们先来看一下我们对其加锁的处理,这里我们没有使用synchronized,而是使用了Lock锁,这里我们使用Lock锁来减少锁的粒度,即上述我们使用了多把锁,对于不同的userId使用了不同的锁,这样对其性能会有较大的提高。
其实上述对资源的加锁解锁的操作,在MyBatis的源码也有所体现,可见 MyBatis缓存模块分析
缓存击穿
另外上述在代码中,还体现了避免缓存击穿的问题,就是我们在加锁代码块的第一步操作,会再次查询缓存,如下:
我们只需明白缓存击穿的概念,就明白了上述代码的作用了,缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,所以大量请求就同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
缓存穿透
还有一个和上述介绍的缓存击穿类似的概念,叫缓存穿透。缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为"-1"的数据或id为特别大不存在的数据。 这时的用户很可能是攻击者,攻击会导致数据库压力过大,甚至宕机。
常见解决方案:
- 接口层增加校验,如用户鉴权校验,id做基础校验,比如id<=0的直接拦截
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
但是如果攻击者采用大量不存在key来访问的话,上述的解决方案就无法解决了,这里就需要采用布隆过滤器了,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
这里我们使用了Guava实现的布隆过滤器,首先需要引入其依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
@Service
@CacheConfig(cacheNames = "userService")
public class UserCacheServiceImpl extends UserServiceImpl {
private BloomFilter<Long> bloomFilter = null;
@PostConstruct
public void init() {
List<User> userList = super.searchAll();
bloomFilter = BloomFilter.create(Funnels.longFunnel(), userList.size(), 0.05);
for (User user : userList) {
bloomFilter.put(user.getId());
}
}
@Override
public User findById(Long id) {
//先判断布隆过滤器中是否存在该值,值存在才允许访问缓存和数据库
if (!bloomFilter.mightContain(id)) {
return null;
}
User user = super.findById(id);
return user;
}
}
推荐阅读
-
vue项目动态设置页面title及是否缓存页面的问题
-
Redis之缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级
-
IE8下Ajax缓存问题及解决办法
-
[redis]关于 缓存穿透/缓存击穿/缓存雪崩 看这篇文章就够了
-
关于缓存穿透,缓存击穿,缓存雪崩,热点数据失效问题的解决方案(转)
-
Redis中几个简单的概念:缓存穿透/击穿/雪崩,别再被吓唬了
-
缓存穿透、缓存击穿、缓存雪崩概念及解决方案
-
SpringCache @Cacheable 在同一个类中调用方法,导致缓存不生效的问题及解决办法
-
带上问题来学redis,看到不吃亏(什么是redis?缓存问题、数据一致性、redis配置文件汉化版)
-
redis的缓存雪崩、缓存穿透和缓存击穿