精华内容
下载资源
问答
  • 缓存一致性问题
    千次阅读
    2021-01-17 21:32:53

    java缓存一致性问题及解决方案:使用缓存,肯定会存在一致性问题;

    读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容 易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。
     
    一、讨论一致性问题之前,先来看一个更新的操作顺序问题:
    先删除缓存,再更新数据库
    问题:同时有一个请求 A 进行更新操作,一个请求 B 进行查询操作。可能出现:
    (1)请求 A 进行写操作(key = 1 value = 2),先删除缓存 key = 1 value = 1
    (2)请求 B 查询发现缓存不存在
    (3)请求 B 去数据库查询得到旧值 key = 1 value = 1
    (4)请求 B 将旧值写入缓存 key = 1 value = 1
    (5)请求 A 将新值写入数据库 key = 1 value = 2
    缓存中数据永远都是脏数据
     
    我们比较推荐操作顺序:
    先删除缓存,再更新数据库,再删缓存(双删,第二次删可异步延时)
     
    public void write(String key,Object data){
        redis.delKey(key);
        db.updateData(data);
        Thread.sleep(500);
        redis.delKey(key);
    }
    
    接下来,看一看缓存同步的一些方案,见下图:
     
     
    1、 数据实时同步更新
    更新数据库同时更新缓存,使用缓存工具类和或编码实现。
    优点:数据实时同步更新,保持强一致性
    缺点:代码耦合,对业务代码有侵入性
     
    2、 数据准实时更新
    准一致性,更新数据库后,异步更新缓存,使用观察者模式/发布订阅/MQ 实现;
    优点:数据同步有较短延迟 ,与业务解耦
     
    缺点:实现复杂,架构较重
    3 、缓存失效机制
    弱一致性,基于缓存本身的失效机制
    优点:实现简单,无须引入额外逻辑
    缺点:有一定延迟,存在缓存击穿/雪崩问题
     
    4、 定时任务更新
    最终一致性,采用任务调度框架,按照一定频率更新
    优点:不影响正常业务
    优点:不保证一致性,依赖定时任务
    二、 缓存击穿、缓存雪崩及解决方案
    1 、缓存击穿
    缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于 并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力
    瞬间增大,造成过大压力
     
    2 、缓存雪崩
    缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压 力过大甚至 down 机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩
    是不同数据都过期了,很多数据都查不到从而查数据库。
     
    解决方案:
    1)单体服务:此时需要对数据库的查询操作,加锁 ---- lock (因考虑到是对同一个参数数值上 一把锁,此处 synchronized 机制无法使用) 加锁的标准流程代码如下:
     
    /**
     * 解决缓存雪崩和击穿方案
     */
    @Service("provincesService")
    public class ProvincesServiceImpl3 extends ProvincesServiceImpl implements ProvincesService{
        private static final Logger logger = LoggerFactory.getLogger(ProvincesServiceImpl3.class);
        @Resource
        private CacheManager cm;//使用注解缓存
        private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();//线程安全的
    
        private static final String CACHE_NAME = "province";
    
        public Provinces detail(String provinceid) {
            // 1.从缓存中取数据
            Cache.ValueWrapper valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);
            if (valueWrapper != null) {
                logger.info("缓存中得到数据");
                return (Provinces) (valueWrapper.get());
            }
    
            //2.加锁排队,阻塞式锁---100个线程走到这里---同一个sql的取同一把锁
            doLock(provinceid);//32个省,最多只有32把锁,1000个线程
            try{//第二个线程进来了
                // 一次只有一个线程
                 //双重校验,不加也没关系,无非是多刷几次库
                valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);//第二个线程,能从缓存里拿到值?
                if (valueWrapper != null) {
                    logger.info("缓存中得到数据");
                    return (Provinces) (valueWrapper.get());//第二个线程,这里返回
                }
    
                Provinces provinces = super.detail(provinceid);
                // 3.从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询
                if (null != provinces){
                    cm.getCache(CACHE_NAME).put(provinceid, provinces);
                }
                return provinces;
            }catch(Exception e){
                return null;
            }finally{
                //4.解锁
                releaseLock(provinceid);
            }
        }
    
        private void releaseLock(String userCode) {
            ReentrantLock oldLock = (ReentrantLock) locks.get(userCode);
            //查询锁是否存在和查询当前线程是否保持此锁
            if(oldLock !=null && oldLock.isHeldByCurrentThread()){
                oldLock.unlock();
            }
        }
    
        private void doLock(String lockcode) {//给一个搜索条件,对应一个锁
            //provinceid有不同的值,参数多样化
            //provinceid相同的,加一个锁,---- 不是同一个key,不能用同一个锁
            ReentrantLock newLock = new ReentrantLock();//创建一个锁
            Lock oldLock = locks.putIfAbsent(lockcode, newLock);//若已存在,则newLock直接丢弃
            if(oldLock == null){
                newLock.lock();//首次加锁,成功取锁,执行
            }else{
                oldLock.lock();//阻塞式等待取锁
            }
        }
    }

    2}  集群或微服务场景下:

    此场景下的锁换成分布式锁(redis或zk等);同时设置多次取锁功能;
    /**
     * 解决缓存雪崩和击穿方案
     */
    @Service("provincesService")
    public class ProvincesServiceImpl5 extends ProvincesServiceImpl implements ProvincesService{
        private static final Logger logger = LoggerFactory.getLogger(ProvincesServiceImpl3.class);
        @Resource
        private CacheManager cm;//使用注解缓存
    
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();//线程安全的
    
        private static final String CACHE_NAME = "province";
    
        public Provinces detail(String provinceid) throws Exception{
            // 1.从缓存中取数据
            Cache.ValueWrapper valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);
            if (valueWrapper != null) {
                logger.info("缓存中得到数据");
                return (Provinces) (valueWrapper.get());
            }
    
            //2.加锁排队,阻塞式锁---100个线程走到这里---同一个sql的取同一把锁
           //32个省,最多只有32把锁,1000个线程
            boolean flag=false;
            flag = RedisUtil.setNX(provinceid, 3000);
            //如果首次没有取到锁,可以取10次
            if(!flag){
                for(int i=0;i<10;i++){
                    Thread.sleep(200);
                    flag = RedisUtil.setNX(provinceid, 3000);//分布式锁
                    if(flag){
                        break;
                    }
                }
            }
            //如果首次没有取到锁,一直取直到取到为止
         /*   if(!flag){
                for (;;){
                    Thread.sleep(200);
                    flag = RedisUtil.setNX(provinceid, 3000);//分布式锁
                    if(flag){
                        break;
                    }
                }
            }*/
            try{//第二个线程进来了
                // 一次只有一个线程
                 //双重校验,不加也没关系,无非是多刷几次库
                valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);//第二个线程,能从缓存里拿到值?
                if (valueWrapper != null) {
                    logger.info("缓存中得到数据");
                    return (Provinces) (valueWrapper.get());//第二个线程,这里返回
                }
                Provinces provinces = super.detail(provinceid);
                // 3.从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询
                if (null != provinces){
                    cm.getCache(CACHE_NAME).put(provinceid, provinces);
                }
                return provinces;
            }catch(Exception e){
                return null;
            }finally{
                //4.解锁
                RedisUtil.releaseLock(provinceid);
            }
        }
    }

    这里加分布式锁解决缓存一致性问题,也解决缓存击穿的问题;分布式锁参考:分布式锁使用及原理

    今天缓存一致性问题到此结束,下篇我们使用布隆过滤器解决缓存穿透问题,敬请期待。

     

     
     
     
     
     
     
     

     

    更多相关内容
  • 关于缓存一致性问题的思考

    千次阅读 2022-04-08 17:36:17
    缓存一致性问题是实际工作中很少很少遇见但面试过程中经常出现的一个问题。本文主要谈一下自己对缓存一致性问题的一些思考,而不是面试科普文。

    一、引言

    缓存一致性问题是实际工作中很少很少遇见但面试过程中经常出现的一个问题。本文主要谈一下自己对缓存一致性问题的一些思考,而不是面试科普文。

    二、双写一致性问题

    根据 CAP 原理,分布式系统在可用性、一致性和分区容错性上无法兼得,通常由于分区容错无法避免,所以一致性和可用性难以同时成立。

    常见的redis+mysql的场景也是一个典型的分布式场景。
    如果需要保证两者数据的强一致性,那么就需要加分布式锁和全局事务,导致读写并发降低;
    如果需要保证两者的数据可用性,那么在redis和mysql的数据同步过程中,就必然存在数据的不一致问题。
    数据可用性简单来说就是在数据不一致的情况下还可以被查询到,对外提供服务

    持久化层和缓存层的一致性问题也通常被称为双写一致性问题,“双写”意为数据既在数据库中保存一份,也在缓存中保存一份。对于一致性来说,包含强一致性和弱一致性,强一致性保证写入后立即可以读取,弱一致性则不保证立即可以读取写入后的值,而是尽可能的保证在经过一定时间后可以读取到,在弱一致性中应用最为广泛的模型则是最终一致性模型,即保证在一定时间之后写入和读取达到一致的状态。对于应用缓存的大部分场景来说,追求的则是最终一致性,少部分对数据一致性要求极高的场景则会追求强一致性。

    这里我个人认为,过分追求缓存和数据库的强一致性是非常不明智的,首先是技术实现复杂,难度大,给项目增加了额外的风险,而且会导致并发性能降低。
    本来引入缓存是为了提高并发,现在由于要保证强一致性,增加分布式事务和分布式锁,反而会导致并发降低,那么就失去了引入缓存的意义。

    三、缓存的典型使用方式

    Cache-Aside 是应用最为广泛的一种缓存策略。下面的图示展示了它的读写流程,来看看它是如何保证最终一致性的。

    1. 在读请求中,首先请求缓存,若缓存命中( cache hit ),则直接返回缓存中的数据;若缓存未命中( cache miss ),则查询数据库并将查询结果更新至缓存,然后返回查询出的数据( demand-filled look-aside )。
    2. 在写请求中,先更新数据库,再删除缓存(write-invalidate)。
      在这里插入图片描述

    四、常见缓存使用的问题

    1、为什么是删除,而不是更新缓存?
    我们以先更新数据库,再删除缓存来举例。
    如果是更新的话,那就是先更新数据库,再更新缓存。

    举个例子:如果数据库1小时内更新了1000次,那么缓存也要更新1000次,但是这个缓存可能在1小时内只被读取了1次,那么这1000次的更新有必要吗?

    反过来,如果是删除的话,就算数据库更新了1000次,那么也只是做了1次缓存删除,只有当缓存真正被读取的时候才去数据库加载。

    2、先删除缓存,再更新数据库的问题?
    如果先删除缓存再更新数据库,那么在执行删除缓存到更新数据库的中间时间,如果出现查询请求,就会将数据库中的旧值保存到缓存中,从而导致缓存和数据库的数据不一致。
    实际项目中,往往查询请求的并发会高于更新请求。
    (删除缓存 + 更新数据库)的过程中出现(查询数据库+更新旧值到缓存)的概率极大
    所以一般不会采用先删除缓存,再更新数据库的方式。

    在这里插入图片描述

    3、先更新数据库,再删除缓存的问题?
    如果采用先更新数据库再删除缓存,比如更新请求A,那么除非这时出现像查询请求B的情况,刚好在更新之前查询到数据库的旧值,还没来的急更新到缓存,请求A就执行完成了更新数据库和删除缓存的操作。这样请求B中完成缓存更新后,就导致缓存和数据库的数据不一致了。
    但是实际过程中,数据库的查询请求一般都比更新请求快。
    (查询数据库+更新缓存的时间间隔)往往都是小于(更新数据库+删除缓存的耗时),
    并且由于更新请求A没有先删除缓存,查询请求B大概率会命中缓存(缓存过期丢失除外),而不会去查库更新缓存。
    所以采用先更新数据库再删除缓存的处理方式出现缓存不一致情况的概率极低

    在这里插入图片描述

    四、如何保证缓存的最终一致性

    1、设置缓存的过期时间

    每次放入缓存的时候,设置一个过期时间,比如5分钟,以后的操作只修改数据库,不操作缓存,等待缓存超时后从数据库重新读取。

    如果对于一致性要求不是很高的情况,可以采用这种方案。

    这个方案还会有另外一个问题,就是如果数据更新的特别频繁,不一致性的问题就很大了。

    在实际生产中,我们有一些活动的缓存数据是使用这种方式处理的。

    因为活动并不频繁发生改变,而且对于活动来说,短暂的不一致性并不会有什么大的问题。

    2、延迟双删

    延时双删的方案的思路是,为了避免更新数据库的时候,其他线程从数据库中读取到旧的数据更新到缓存中,需要在更新完数据库之后,再sleep一段时间,然后再次删除缓存。

    sleep的时间要对业务读写缓存的时间做出评估,sleep时间大于读写缓存的时间即可。

    流程如下:

    1. 线程1删除缓存,然后去更新数据库
    2. 线程2来读缓存,发现缓存已经被删除,所以直接从数据库中读取,这时候由于线程1还没有更新完成,所以读到的是旧值,然后把旧值写入缓存
    3. 线程1根据估算的时间sleep,由于sleep的时间大于线程2读数据+写缓存的时间,所以缓存被再次删除。
    4. 如果还有其他线程来读取缓存的话,就会再次从数据库中读取到最新值。

    在这里插入图片描述
    在这里插入图片描述

    延时双删常用步骤有 4 个,参考下面伪代码:

    @Transactional(rollbackFor = Exception.class)
    void  update_data(String key,Order obj){
        del_cache(key)     # 删除 redis 缓存数据。
        update_db(obj)     # 更新数据库数据。
        logic_sleep(_time) # 当前逻辑延时执行。
        del_cache(key)     # 删除 redis 缓存数据。
    }
    

    大家认为上面的这个伪代码正确吗?
    1、由于有更新数据的操作,这里添加了事务控制,使用的是方法级别的事务注解@Transactional,这就导致更新数据库的方法update_db(obj)只有在方法执行完成才会执行commit操作,提交数据库的更新。虽然代码看上去是在更新数据库后延迟执行的第二次删除缓存的操作,但是实际上两次缓存都是在更新操作前执行的。

    2、那如果将事务控制在update_db方法中呢?
    这就会导致删除缓存操作如果执行失败,数据库的更新操作不能回滚,缓存中仍然是旧的数据。这里就涉及到一个分布式事务的问题,暂时不展开讨论,默认删除缓存操作一定成功。

    3、延迟双删中,在更新数据库操作前,执行的第一次删除缓存操作的意义在哪?
    只要第一次删除缓存的操作和更新数据库操作之间,有其他查询请求未命中缓存,就会查询到数据库的旧值,并更新到缓存中。
    而删除缓存的速度是非常快的,所以在延迟双删过程中,第一次缓存删除操作,基本没什么作用。

    缓存的意义就是为了提高查询速度,如果数据库值的更新操作都没有完成,这时候的提前执行缓存删除操作毫无意义。
    在这里插入图片描述

    4、为什么要进行延迟删除?
    进行延时删除的核心原因是为了让更新数据库操作之前进行的查询请求先完成缓存更新操作。
    也就是等缓存的更新操作都执行完毕后,再执行缓存删除操作,从而保证数据的最终一致性。
    在这里插入图片描述
    5、延时删除会不会降低更新操作的并发能力?
    这里的一个优化方法是更新数据库完成后,调用一个异步方法,再该方法内部进行延时等待+删除缓存的操作,这样就可以保障更新操作的并发响应速度。

    说明
    延迟双删应该是网上解决缓存一致性问题看到的最多的解决方案。
    但有很多细节痛点都没有仔细考虑。
    比如最基本的一条,如何保证先完成数据库的更新操作(进行了commit),在执行删除缓存的操作?是不是有很多人犯了伪代码中同样的错误。

    3、采用消息队列

    先更新数据库,成功后往消息队列发消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现,达到最终一致性的效果。
    在这里插入图片描述
    这个解决方案其实问题更多。
    1、引入消息中间件之后,问题更复杂了,怎么保证消息不丢失更麻烦
    2、就算更新数据库和删除缓存都没有发生问题,消息的延迟也会带来短暂的不一致性,不过这个延迟相对来说还是可以接受的

    4、基于数据库日志( MySQL binlog )增量解析、订阅和消费

    鉴于上述方案对业务代码具有一定入侵性,所以需要一种更加优雅的解决方案,让缓存删除失败的补偿机制运行在背后,尽量少的耦合于业务代码。一个简单的思路是通过后台任务使用更新时间戳或者版本作为对比获取数据库的增量数据更新至缓存中,这种方式在小规模数据的场景可以起到一定作用,但其扩展性、稳定性都有所欠缺。

    一个相对成熟的方案是基于 MySQL 数据库增量日志进行解析和消费,这里较为流行的是阿里巴巴开源的作为 MySQL binlog 增量获取和解析的组件 canal (类似的开源组件还有 Maxwell、Databus 等)。canal sever 模拟 MySQL slave 的交互协议,伪装为 MySQL slave ,向 MySQL master 发送 dump 协议,MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal sever ),canal sever 解析 binary log 对象(原始为 byte 流),可由 canal client 拉取进行消费,同时 canal server 也默认支持将变更记录投递到 MQ 系统中,主动推送给其他系统进行消费。在 ack 机制的加持下,不管是推送还是拉取,都可以有效的保证数据按照预期被消费。当前版本的 canal 支持的 MQ 有 kafka 或者 RocketMQ 。另外, canal 依赖 zookeeper 作为分布式协调组件来实现 HA ,canal 的 HA 分为两个部分:

    为了减少对 MySQL dump 的请求压力,不同 canal server 上的 instance 要求同一时间只能有一个处于运行状态,其他的 instance 处于 standby 状态;
    为了保证有序性,对于一个 instance 在同一时间只能由一个 canal client 进行 get/ack 等动作;

    在这里插入图片描述

    五、不用过分放大缓存不一致问题

    真的不用过分放大缓存不一致的问题。

    遇到技术问题,首先应该评估他会带来什么负面影响,是否有必要解决,解决方案的投入和收益

    在我看来缓存一致性问题更多的情况下只是一个面试问题,完全算不上是技术问题,更谈不上是什么业务问题。

    核心关键点:

    • 业务操作尽量不使用缓存数据
    • 查询操作中才考虑使用缓存数据

    把握住这两个关键点,这样就能保证业务操作不受缓存一致性问题的影响,保证所有业务操作正常进行。
    而大部分系统对查询操作的数据的实时性要求和感知其实并不是明显。
    再简单给缓存数据设置一下过期时间,就能满足大多数场景下数据查询的实时性要求。
    完全没有必要把问题搞的过分复杂化。针对那些实时性要求高且变更频率快的数据,完全没有必要添加缓存,直接查数据库反而是更好的方式。

    在这里插入图片描述


    六、总结

    1、介绍了缓存一致性问题是怎么产生的。
    2、缓存一致性问题本质上是数据库和缓存之间数据的最终一致性问题。通常可以采用以下办法实现缓存的最终一致性:

    • 添加缓存过期时间
    • 延迟删除
    • 消息队列
    • 监听数据库binlog更新缓存

    3、不用过分放大缓存不一致的问题,把握关键点:业务操作中避免使用缓存,保证业务功能不受缓存不一致的影响;查询操作添加缓存提高响应速度。大部分系统对查询数据的实时性要求一般都能接受一定的容忍度。
    针对更新频繁又要保证较高的数据一致性的场景,最好重新考虑使用缓存的必要性。

    4、最推荐的组合方式是:添加缓存过期时间 + 延迟删除。实现简单能满足大部分项目需求。

    展开全文
  • 缓存一致性问题解决方案

    千次阅读 多人点赞 2022-04-07 11:26:30
    四种方案: 先写缓存,再写数据库(差) 先写数据库,再写缓存(一般) 先删缓存,再写数据库(能接受) 先写数据库,再删缓存(比较优秀)

    通常情况下,我们使用缓存的主要目的是为了提升查询的性能。大多数情况下,是这样使用缓存的:

    当数据库有数据更新时,在很长的一段时间内(决定于缓存的过期时间),用户请求从缓存中获取到的都可能是旧值,而非数据库的最新值。那么,该如何更新缓存呢?目前有以下四种解决方案:

    1. 先写缓存,再写数据库(差)
    2. 先写数据库,再写缓存(一般)
    3. 先删缓存,再写数据库(能接受)
    4. 先写数据库,再删缓存(比较优秀)

    讨论四种方案前先统一两个认知,以便更好理解四种方案:

    1. 缓存一致性问题没有绝对可靠的方案,我们只能让两者尽量接近,但无论如何也不能百分百达到一致性效果。
    2. 缓存和数据库,无论先处理谁,只要后者有延迟/失败,都会导致不一致的情况,这也正是缓存不一致的根本原因所在。所有解决方案和讨论都是围绕这一点来进行的。

    方案一:先写缓存,再写数据库

    缺点:如果刚写完缓存,突然网络出现了异常,导致写数据库失败了。这样缓存中的数据就变成脏数据,这个问题非常严重,也是最差的一种解决方案。

    方案二:先写数据库,再写缓存

    缺点一:问题又来了,写数据库成功,但写缓存失败了,依然会造成缓存脏数据的问题。但写缓存失败比写数据库失败的概率要小很多了(因为数据库可能有加锁、外键约束、超时等机制限制),所以此方案要比第一种方案好一点。

    如果对接口性能要求不高,还可以把写数据库和写缓存放到一个事务中,写缓存失败就回滚数据库。

    缺点二:然而高并发场景下,还会有个棘手问题:

    1. 请求a先过来,刚写完了数据库。但由于网络原因,卡顿了一下,还没来得及写缓存。
    2. 这时候请求b过来了,先写了数据库。
    3. 接下来,请求b顺利写了缓存。
    4. 此时,请求a卡顿结束,也写了缓存。

    很显然,在这个过程当中,请求b在缓存中的新数据,被请求a的旧数据覆盖了。

    也就是说:在高并发场景中,如果多个线程同时执行先写数据库,再写缓存的操作,可能会出现数据库是新值,而缓存中是旧值,两边数据不一致的情况。

     缺点三:浪费系统资源

    写的缓存的内容,并不是简单的数据,而是要经过非常复杂的计算或者查询筛选得出的结果,这样每写一次缓存都要计算一次,这是非常浪费系统资源的,尤其对那些写多读少的业务场景,更是雪上加霜。

    方案三:先删缓存,再写数据库

    既然更新缓存会有浪费系统资源等问题,那就直接删除缓存来代替更新缓存呢?

    方案一:

    嗯,看起来还不错。即使写数据库失败了,下个请求也会重新触发写缓存操作,基本上避免更新缓存的所有弊端,然而也不是十全十美。

    缺点:

    1. 请求d先过来,把缓存删除了。但由于网络原因,卡顿了一下,还没来得及写数据库。
    2. 这时请求c过来了,先查缓存发现没数据,再查数据库,有数据,但是旧值。
    3. 请求c将数据库中的旧值,更新到缓存中。
    4. 此时,请求d卡顿结束,把新值写入数据库。

    这种极端情况下依然会导致写入的缓存为旧值。

    方案二:延迟双删

    为了避免方案1的避免,写完数据库后,再删除一次。

    该方案有个非常关键的地方是:第二次删除缓存,并非立马就删,而是要在一定的时间间隔之后。

    sleep的时间要对业务读写缓存的时间做出评估,sleep时间大于读写缓存的时间即可。

    那么,为什么一定要间隔一段时间之后,才能删除缓存呢?

    请求d卡顿结束,把新值写入数据库后,请求c将数据库中的旧值,更新到缓存中。此时,如果请求d删除太快,在请求c将数据库中的旧值更新到缓存之前,就已经把缓存删除了,这次删除就没任何意义。必须要在请求c更新缓存之后,再删除缓存,才能把旧值及时删除了。

    方案四:先写数据库,再删缓存

     

    1. 请求e先写数据库,由于网络原因卡顿了一下,没有来得及删除缓存。
    2. 请求f查询缓存,发现缓存中有数据,直接返回该数据。
    3. 请求e删除缓存。

    这种情况下,只会影响f或类似f的少数请求读了一次脏数据,看起来好多了。

    但如果是读数据请求先过来呢?

    1. 请求f查询缓存,发现缓存中有数据,直接返回该数据。
    2. 请求e先写数据库。
    3. 请求e删除缓存。

    这种情况看起来也没问题。

    但就怕一种情况:缓存失效。

    1. 缓存自动失效。
    2. 请求f查询缓存,发缓存中没有数据,查询数据库的旧值,但由于网络原因卡顿了,没有来得及更新缓存。
    3. 请求e先写数据库,接着删除了缓存。
    4. 请求f更新旧值到缓存中。

    这时,缓存和数据库的数据同样出现不一致的情况了。但这种情况还是比较少的,需要同时满足以下条件:

    1. 缓存刚好自动失效。
    2. 请求f从数据库查出旧值,更新缓存的耗时,比请求e写数据库,并且删除缓存的耗时还长。

    出现这种情况的概率已经极低了,除非是查询比写入还慢。说实话如果对于这种极低概率的脏数据都不能容忍,建议不需要使用缓存了。毕竟现在大部分都是读写分离,主从还存在延时呢。这种要强一致性的建议走mysql。对msql进行扩容比如分库分表,读写分离等等。

    删除缓存失败怎么办?

    其实先写数据库,再删缓存的方案,跟缓存双删的方案一样,有一个共同的风险点,即:如果缓存删除失败了怎么办?

    方案一:设置过期时间

    缓存设置一个过期时间,比如5分钟。当然这种方案只适合数据更新不是太频繁的业务。

    方案二:同步重试

    在接口中判断是否删除成功,如果失败就重试,直到成功或超过最大重试次数为止,返回数据。当然,这种方案的缺点就是可能影响接口性能。

    方案三:消息队列

    将删除缓存任务写入mq等消息中间件中,在mq的consumer中处理。但问题也很多:

    1. 引入消息中间件之后,问题更复杂了,对业务代码有一定侵入性、消息丢失怎么办
    2. 消息本身的延迟也会带来短暂的不一致性,不过这个延迟相对来说还是可以接受的

    方案四:订阅mysql的binlog

    我们可以借助监听binlog的消息队列来做删除缓存的操作。这样做的好处是,删除动作无需侵入到业务代码,消息中间件帮你做了解耦,同时,中间件的这个东西本身就保证了高可用。

    总结

    首先,要明确一点,缓存删除比更新效果更好。为什么呢?

    举个例子:如果数据库1小时内更新了1000次,那么缓存也要更新1000次,但是这个缓存可能只在最后一次更新后被读取了1次,那么前999次的更新有必要吗?

    反过来,如果是删除的话,就算数据库更新了1000次,那么也只是做了1次缓存删除(删除前判断key是否存在),只有当缓存真正被读取的时候才去数据库加载

    删除缓存有两种方式:

    1. 先删除缓存,再更新数据库。解决方案是使用延迟双删。
    2. 先更新数据库,再删除缓存。解决方案是消息队列或者监听binlog同步,引入消息队列会带来更多的问题,对业务代码有一定侵入性,并不推荐直接使用。

    针对缓存一致性要求不是很高的场景,那么只通过设置超时时间就可以了。

    展开全文
  • 关于 Redis 的其他的一些面试问题已经写过了,比如常见的缓存穿透、雪崩、击穿、热点的问题,但是还有一个比较麻烦的问题就是如何保证缓存一致性。 对于缓存和数据库的操作,主要有以下两种方式。 先删缓存,...

    关于 Redis 的其他的一些面试问题已经写过了,比如常见的缓存穿透、雪崩、击穿、热点的问题,但是还有一个比较麻烦的问题就是如何保证缓存一致性。

     

    对于缓存和数据库的操作,主要有以下两种方式。

     

    先删缓存,再更新数据库

     

    先删除缓存,数据库还没有更新成功,此时如果读取缓存,缓存不存在,去数据库中读取到的是旧值,缓存不一致发生。

    图片

    解决方案

     

    延时双删

     

    延时双删的方案的思路是,为了避免更新数据库的时候,其他线程从缓存中读取不到数据,就在更新完数据库之后,再 Sleep 一段时间,然后再次删除缓存。

     

    Sleep 的时间要对业务读写缓存的时间做出评估,Sleep 时间大于读写缓存的时间即可。

     

    流程如下:

     

    1. 线程1删除缓存,然后去更新数据库。

    2. 线程2来读缓存,发现缓存已经被删除,所以直接从数据库中读取,这时候由于线程1还没有更新完成,所以读到的是旧值,然后把旧值写入缓存。

    3. 线程1,根据估算的时间,Sleep,由于 Sleep 的时间大于线程2读数据+写缓存的时间,所以缓存被再次删除。

    4. 如果还有其他线程来读取缓存的话,就会再次从数据库中读取到最新值。

    图片

     

    先更新数据库,再删除缓存

     

    如果反过来操作,先更新数据库,再删除缓存呢?

     

    这个就更明显的问题了,更新数据库成功,如果删除缓存失败或者还没有来得及删除,那么,其他线程从缓存中读取到的就是旧值,还是会发生不一致。

    图片

    解决方案

     

    消息队列

     

    这是网上很多文章里都有写过的方案。但是这个方案的缺陷会更明显一点。

     

    先更新数据库,成功后往消息队列发消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现,达到最终一致性的效果。

     

    图片

    这个解决方案其实问题更多。

     

    1. 引入消息中间件之后,问题更复杂了,怎么保证消息不丢失更麻烦。

    2. 就算更新数据库和删除缓存都没有发生问题,消息的延迟也会带来短暂的不一致性,不过这个延迟相对来说还是可以接受的。

     

    进阶版消息队列

     

    为了解决缓存一致性的问题单独引入一个消息队列,太复杂了。

     

    其实,一般大公司本身都会有监听 binlog 消息的消息队列存在,主要是为了做一些核对的工作。

     

    这样,我们可以借助监听 binlog 的消息队列来做删除缓存的操作。这样做的好处是,不用你自己引入,侵入到你的业务代码中,中间件帮你做了解耦,同时,中间件的这个东西本身就保证了高可用。

     

    当然,这样消息延迟的问题依然存在,但是相比单纯引入消息队列的做法更好一点。

    而且,如果并发不是特别高的话,这种做法的实时性和一致性都还算可以接受的。

     

    图片

     

    其他解决方案

     

    设置缓存过期时间

     

    每次放入缓存的时候,设置一个过期时间,比如5分钟,以后的操作只修改数据库,不操作缓存,等待缓存超时后从数据库重新读取。

     

    如果对于一致性要求不是很高的情况,可以采用这种方案。

     

    这个方案还会有另外一个问题,就是如果数据更新的特别频繁,不一致性的问题就很大了。

     

    在实际生产中,我们有一些活动的缓存数据是使用这种方式处理的。

     

    因为活动并不频繁发生改变,而且对于活动来说,短暂的不一致性并不会有什么大的问题。

     

    为什么是删除,而不是更新缓存?

     

    我们以先更新数据库,再删除缓存来举例。

     

    如果是更新的话,那就是先更新数据库,再更新缓存

     

    举个例子:如果数据库 1 小时内更新了 1000 次,那么缓存也要更新 1000 次,但是这个缓存可能在1小时内只被读取了 1 次,那么这 1000 次的更新有必要吗?

     

    反过来,如果是删除的话,就算数据库更新了 1000 次,那么也只是做了 1 次缓存删除,只有当缓存真正被读取的时候才去数据库加载。

     

    总结

     

    首先,我们要明确一点,缓存不是更新,而应该是删除。

     

    删除缓存有两种方式:

     

    1. 先删除缓存,再更新数据库。解决方案是使用延迟双删。

    2. 先更新数据库,再删除缓存。解决方案是消息队列或者其他 binlog 同步,引入消息队列会带来更多的问题,并不推荐直接使用。

     

    针对缓存一致性要求不是很高的场景,那么只通过设置超时时间就可以了。

     

    其实,如果不是很高的并发,无论你选择先删缓存还是后删缓存的方式,都几乎很少能产生这种问题,但是在高并发下,你应该知道怎么解决问题。

    展开全文
  • 缓存一致性问题也是使用缓存中比较经典的问题之一。使用缓存,涉及数据库和缓存两部分数据的维护,既然是两个组件的数据,那么必然有数据一致性问题。常用的解决方案有三种,分别是设置过期时间,先更新数据库,再删...
  • Canal解决Redis与mysql缓存一致性问题

    千次阅读 2022-04-24 09:19:44
    目录1 缓存一致性2 缓存一致性解决方案3 Canal介绍3.1 Canal应用场景3.2 MySQL主从复制原理3.3 Canal工作原理3.4 Canal配置5 同步更新Redis缓存 1 缓存一致性 用户每次抢完红包,要查看自己抢红包记录,此时需要...
  • Redis缓存一致性问题解决方案

    万次阅读 多人点赞 2020-04-27 12:05:20
    每次设置缓存时都有一个过期时间,根据不同业务,过期时间也不一致,设置过期时间能保证缓存数据最终一致性问题。这样能保证在更新数据库成功,更新缓存失败,或者缓存了脏数据时,缓存过期后,能正确的读取到最新的...
  • 为什么产生有线程安全问题? CPU的高速缓存 线程是CPU最小的调度单元,当CPU去调度线程执行一些操作的时候,计算机的处理器需要与内存做交互的,比如...这就产生了缓存一致性问题。 以我们熟悉的i++举个例子,i...
  • redis缓存一致性问题解决方案

    万次阅读 2019-03-18 22:39:19
    但是引入缓存之后,随之而来的问题就是当DB数据更新时,缓存中的数据就会与db数据不一致,这时候就需要对缓存的数据进行更新或者淘汰缓存,这篇文章就是分析各种处理缓存一致性问题的解决方案。 先更新DB还是操作...
  • 当程序在运行过程中,会将运算需要的数据从主存复制一份到cup的高速缓存当中,那么cpu进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束后,再将高速缓存中的数据刷新到主存当中。...
  • 缓存和数据库一致性问题

    千次阅读 2022-03-30 10:37:10
    如何保证缓存与数据库双写一致性问题
  • 这次就来介绍一下Redis的缓存一致性问题。 对于缓存和数据库的更新操作,主要分为以下两种 先删除缓存,再更新数据库 先更新数据库,再删除缓存 首先可能会带来疑惑的点是,为什么这里是删除缓存而不是更新缓存...
  • 分布式缓存一致性问题解决方案

    千次阅读 2020-08-22 21:27:03
    该文章主要是来自于通用配置系统使用了文件缓存作为二级缓存,他的一致性如果保证的问题,目前了解到的有三种方案: ... 2.采用队列方式,将更新作为消息放入mq中进行...3.缓存同步、如何保证缓存一致性、缓存误用 ...
  • Redis缓存一致性问题

    千次阅读 2018-11-28 17:27:27
    从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存...
  • Redis缓存数据一致性问题

    千次阅读 2021-12-01 22:11:37
    2、Redis缓存数据一致性问题 2.1、如何保证缓存和数据库数据一致性 3、缓存问题 3.1、缓存穿透-查不到 3.2、缓存击穿-量太大,缓存过期 3.3、缓存雪崩 1、高客户端连接服务端处理 当客户端比较多,高并发...
  • 数据库缓存一致性问题

    千次阅读 2022-03-16 14:32:12
    数据库缓存一致性问题 要保证缓存和数据库的强一致性,最好的方法就是加分布式锁,但是引入缓存的目的就是提高性能,而加分布式锁付出的的代价很可能超过引入缓存带来的性能提升 现在 业内常用的是 Cache Aside ...
  • 解决缓存一致性问题的几种方式

    千次阅读 2020-09-09 08:05:27
    缓存一致性问题 缓存的实时同步,这种数据同步是增量、主动、强一致性 1、对数据库数据进行更新的时候淘汰缓存 2、读取数据的时候更新缓存,为了避免缓存击穿带来的雪崩问题我们需要做同步处理,控制只有一个线程去...
  • 怎样保证缓存一致性问题

    千次阅读 2020-02-08 10:59:22
    什么是缓存 百度百科定义 > **缓存(cache)**,原始意义是指访问速度比一般[随机存取存储器](https://baike.baidu.com/item/随机存取存储器)(RAM)快的一种高速存储器,通常它不像系统主存那样使用[DRAM]...
  • 总结:解决DB与缓存一致性问题

    千次阅读 2022-04-01 18:01:27
    方案: 1、读取:读的时候,先读缓存缓存没有的话,就读数据库,然后取出数据后放...面试:高频面试题:如何保证缓存与数据库的双写一致性? 高频面试题:如何保证缓存与数据库的双写一致性 - 半分、 - 博客园 ...
  • Redis(六) 数据库和缓存一致性问题

    千次阅读 2022-03-17 18:13:51
    缓存更新策略、失败重试策略、旁路缓存模式
  • 缓存是我们经常用到的,而如何解决缓存和数据库的一致性也是一个挺让人头疼的问题
  • 关于Redis的其他的一些面试问题已经写过了,比如常见的缓存穿透、雪崩、击穿、热点的问题,但是还有一个比较麻烦的问题就是如何保证缓存一致性。对于缓存和数据库的操作,主要有以下两种方式。先删...
  • 如何解决数据更新导致缓存一致性问题? 一.为什么使用分布式缓存 我们初步了解了什么是缓存以及一般的缓存使用逻辑,那么为什么要使用分布式缓存呢? 在服务架构初期我们基本上都是单体架构,一个tomcat服务器撑起...
  • 内存模型是怎么解决缓存一致性问题的?

    万次阅读 多人点赞 2018-10-10 09:30:42
    在再有人问你Java内存模型是什么,就把这篇文章发给他这篇...每个CPU会有L1、L2甚至L3缓存,在多核计算机中会有多个CPU,那么就会存在多套缓存,那么这多套缓存之间的数据就可能出现不一致的现象。为了解决这个问...
  • 缓存数据一致性 - 双写模式 这种模式下 ,当我们更改某个数据的时候,同时修改数据库的数据,和缓存的数据,但是在并发情况下会出现缓存的不一致问题, 解决方案: 加分布式读写锁(Redisson) 如果不需要数据的强...
  • 本文由以下三个部分组成1、讲解缓存更新策略2、对每种策略进行缺点分析3、针对缺点给出改进方案先做一个说明,从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的...
  • 也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。  Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。...
  • 内存模型如何解决缓存一致性问题

    千次阅读 2018-08-20 22:18:35
    每个CPU会有L1、L2甚至L3缓存,在多核计算机中会有多个CPU,那么就会存在多套缓存,那么这多套缓存之间的数据就可能出现不一致的现象。为了解决这个问题,有了内存模型。内存模型定义了共享内存系统中多线程程序读写...
  • 数据库缓存一致性解决方案

    千次阅读 2022-04-24 12:30:58
    那么在修改数据库的时候缓存的更新肯定会有延迟,如何保存缓存和数据库数据的一致性是一个问题。 方案1:同步删除 顾名思义在修改数据库数据的时候,同步删除缓存 缺点: 1、不能保证每个入口都能同步删除 2、如果...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 453,178
精华内容 181,271
关键字:

缓存一致性问题

友情链接: 新建文件夹.zip