高并发下的缓存一致性,并发,穿透问题
共 8645字,需浏览 18分钟
·
2021-04-06 09:23
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
缓存在高并发场景下的常见问题
缓存一致性问题
当数据时效性要求很高的时候,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也要保持一致,不能出现差异现象。这样就比较依赖缓存的过期和更新策略。一般会在数据库发生更改的时候,主动更新缓存中的数据或者移除对应的缓存。
1、更新数据库成功—>更新缓存失败—数据不一致
2、更新缓存成功—>更新数据库失败—数据不一致
3、更新数据库成功—>淘汰缓存失败—数据不一致
4、淘汰缓存成功—>更新数据库失败—查询缓存丢失
缓存并发问题
缓存过期后将尝试从数据库获取数据,在单线程情况下是合理而又稳固的流程,但是在高并发情况下,有可能多个请求并发的从数据库中获取数据,对后端数据库造成极大的压力,甚至导致数据库崩溃。另外,当某个缓存的key被更新时,同时也有可能在被大量的请求获取,也同样导致数据一致性的问题。如何解决?一般我们会想到类似“锁”的机制,在缓存更新或者过期的情况下,先获取锁,在进行更新或者从数据库中获取数据后,再释放锁,需要一定的时间等待,就可以从缓存中继续获取数据
使用互斥锁(mutex key)
只让一个线程构建缓存,其他线程构建缓存的线程执行完,重新从缓存获取数据就ojbk了
单机直接用synchronized或者lock,分布式就用分布式锁(可以用memcache的add,redis的setnx,zookeeper的节点添加监听等等等…..)
memcache伪代码如下
if (memcache.get(key) == null) {
// 3 min timeout to avoid mutex holder crash
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
}
redis伪代码
String get(String key){
String value = redis.get(key);
if(value == null){
if(redis.setnx(key_Mutex),"1"){
redis.expire(key_mutex,3*60);//防止死锁
value = db.get(key);
redis.set(key,value);
resdis.delete(key_Mutex);
}else{
Thread.sleep(50);
get(key);
}
}
}
提前使用互斥锁
在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。伪代码如下
v = memcache.get(key);
if (v == null) {
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
} else {
if (v.timeout <= now()) {
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
// extend the timeout for other threads
v.timeout += 3 * 60 * 1000;
memcache.set(key, v, KEY_TIMEOUT * 2);
// load the latest value from db
v = db.get(key);
v.timeout = KEY_TIMEOUT;
memcache.set(key, value, KEY_TIMEOUT * 2);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
}
}
上面两种方案
优点:避免cache失效时刻大量请求获取不到mutex并进行sleep
缺点:代码复杂性增大,会出现死锁和线程池阻塞等问题,因此一般场合用方案一也已经足够
永远不过期
这里的“永远不过期”包含两层意思:
(1) 从redis上看,没有设置过期时间,就不会出现热点key过期问题,也就是“物理”不过期。
(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期,有一个问题是在异步构建缓存完成之前其他线程访问的是旧的数据
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;
}
资源保护(尚未了解)
缓存穿透问题
场景:在高并发场景下,如果一个key被高并发访问,没有被命中,处于对容错性的考虑,会尝试去从后端数据库中获取,从而导致了大量请求到达数据库,而当该key对应的数据本身就是空的情况下,就导致数据库中并发地去执行很多不必要的查询操作,从而导致巨大冲击和压力
可以通过下面的几种常用方式来避免缓存问题
缓存空对象
对查询结果为空的对象也进行缓存,如果是集合,可以缓存一个空的集合(非null),如果是缓存单个对象,可以通过字段标识来区分。这样避免请求穿透到后端数据库,同时,也需要保证缓存数据的时效性。适合命中不高,但可能被频繁更新的数据
单独过滤处理
对所有可能对应数据为空的key进行统一的存放,并在请求前做拦截,这样避免请求穿透到后端数据库。这种方式实现起来相对复杂,比较适合命中不高,但是更新不频繁的数据
总结:作为一个并发量较大的互联网应用,我们的目标有3个:
1. 加快用户访问速度,提高用户体验。
2. 降低后端负载,保证系统平稳。
3. 保证数据“尽可能”及时更新(要不要完全一致,取决于业务,而不是技术。)
---接下来一篇将对 缓存雪崩 做个简单的总结
————————————————
版权声明:本文为CSDN博主「Kevins Danish」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:
https://blog.csdn.net/weixin_36708538/article/details/80338643
粉丝福利:Java从入门到入土学习路线图
👇👇👇
👆长按上方微信二维码 2 秒
感谢点赞支持下哈