高并发下的缓存一致性,并发,穿透问题

缓存在高并发场景下的常见问题

缓存一致性问题

当数据时效性要求很高的时候,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也要保持一致,不能出现差异现象。这样就比较依赖缓存的过期和更新策略。一般会在数据库发生更改的时候,主动更新缓存中的数据或者移除对应的缓存。

  1. 更新数据库成功—>更新缓存失败—数据不一致
  2. 更新缓存成功—>更新数据库失败—数据不一致
  3. 更新数据库成功—>淘汰缓存失败—数据不一致
  4. 淘汰缓存成功—>更新数据库失败—查询缓存丢失

缓存并发问题

缓存过期后将尝试从数据库获取数据,在单线程情况下是合理而又稳固的流程,但是在高并发情况下,有可能多个请求并发的从数据库中获取数据,对后端数据库造成极大的压力,甚至导致数据库崩溃。另外,当某个缓存的key被更新时,同时也有可能在被大量的请求获取,也同样导致数据一致性的问题。如何解决?一般我们会想到类似“锁”的机制,在缓存更新或者过期的情况下,先获取锁,在进行更新或者从数据库中获取数据后,再释放锁,需要一定的时间等待,就可以从缓存中继续获取数据

  1. 使用互斥锁(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);
        }
    }
}
  1. 提前使用互斥锁
    在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. 永远不过期
    这里的“永远不过期”包含两层意思:
    (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;  
    }  
  1. 资源保护(尚未了解)

缓存穿透问题

场景:在高并发场景下,如果一个key被高并发访问,没有被命中,处于对容错性的考虑,会尝试去从后端数据库中获取,从而导致了大量请求到达数据库,而当该key对应的数据本身就是空的情况下,就导致数据库中并发地去执行很多不必要的查询操作,从而导致巨大冲击和压力
可以通过下面的几种常用方式来避免缓存问题

  1. 缓存空对象
    对查询结果为空的对象也进行缓存,如果是集合,可以缓存一个空的集合(非null),如果是缓存单个对象,可以通过字段标识来区分。这样避免请求穿透到后端数据库,同时,也需要保证缓存数据的时效性。适合命中不高,但可能被频繁更新的数据
    这里写图片描述
  2. 单独过滤处理
    对所有可能对应数据为空的key进行统一的存放,并在请求前做拦截,这样避免请求穿透到后端数据库。这种方式实现起来相对复杂,比较适合命中不高,但是更新不频繁的数据

总结:作为一个并发量较大的互联网应用,我们的目标有3个:

  1. 加快用户访问速度,提高用户体验。

  2. 降低后端负载,保证系统平稳。

  3. 保证数据“尽可能”及时更新(要不要完全一致,取决于业务,而不是技术。)

---接下来一篇将对 缓存雪崩 做个简单的总结
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
保证高并发时数据一致性是一个复杂的问题,以下是一些常用的策略和技术: 1. 事务管理:使用数据库事务来保证一组操作的原子性,要么全部成功,要么全部回滚。通过在关键操作上使用事务,可以确保在并发场景下数据的一致性。 2. 锁机制:使用锁来实现对共享资源的互斥访问,防止多个线程同时修改同一份数据。可以使用悲观锁或乐观锁来保证数据的一致性。 3. 并发控制:使用并发控制算法来解决并发访问数据时可能出现的冲突问题,如读写锁、信号量、版本控制等。这些机制可以确保在高并发情况下数据的一致性和正确性。 4. 分布式事务:在分布式系统中,可以使用分布式事务协调器(如XA协议)来管理多个参与者之间的事务,保证数据在不同节点之间的一致性。 5. 缓存策略:合理使用缓存来减轻数据库负载,但需要注意及时更新缓存,以避免缓存与数据库数据不一致的情况发生。 6. 数据复制与同步:通过数据复制和同步机制将数据在多个节点之间进行同步,确保数据的一致性。常见的方法有主从复制、集群复制等。 7. 一致性哈希算法:在分布式环境中,使用一致性哈希算法可以有效地解决节点的动态加入和删除带来的数据迁移问题,保证数据的一致性。 需要根据具体场景和需求选择合适的策略和技术来保证高并发时数据的一致性

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值