缓存一致性解决

根据上一章的学习,我们现在就可以用Redisson框架来解决分布式锁的问题,进行修改获取三级二级分类代码如下

    //todo 产生堆外内存溢出异常:OutOfDirectMemoryEror
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)){
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedissonLock();
            return catalogJsonFromDb;
        }
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        return result;
    }
 //使用redisson框架,进行加锁,解决缓存击穿问题,提高性能
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
        //1:占分布式锁。去redis占坑
        //锁的名字。锁的粒度,越细越快
        //锁的粒度:具体缓存的是某个数据,11-号商品:product-11-lock  12-号商品 product-12-lock
        RLock lock = redissonClient.getLock("CatalogJson-lock");
        //获取锁,加锁
        lock.lock();
            System.out.println("获取分布式锁成功。。。。。");
            Map<String, List<Catelog2Vo>> dataFromDB;
            try {
                dataFromDB = getDataFromDB();

            }finally {

                lock.unlock();
            }
            return dataFromDB;
    }

带来问题:我们读所有的数据都是先来看缓存,缓存中没有在执行上面的代码读取数据库 dataFromDB = getDataFromDB();,这是我们第一次添加进来,缓存中肯定没有,我们就要读取数据库;如果我们的数据修改了,三级分类的信息变了,我们再从缓存中拿到的肯定就是一个旧数据,这就是缓存里面的数据如何和数据库保持一致(缓存数据一致性),我们对于这个场景用到的非常多的两种模式

  1. 双写模式
  • 我们对数据做了修改,redis缓存里面的数据想要跟着数据库里面改变,我们可以这样,改完菜单以后,我们把缓存里面的数据也改一下,代码如下
    @Override
    public void updateCascade(CategoryEntity category) {

        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
        
        //同时修改缓存里面的数据

    }

在这里插入图片描述

  • 大并发下带来的问题:我们的想法是写完数据库,把缓存里面的东西修改一下;假设如图,我们模拟两个并发同时在修改一条id为3的数据,第一个请求先来执行,他把数据改成了1,第二个请求进来执行,他把数据改成了2,相当于我们最后一次对数据库的修改变成了2,第一个请求进来改完数据库,我们要改缓存,但是由于各种原因,改完数据库以后,cpu没有马上执行写缓存-1(或者卡顿,慢,或者没有2号机器跑的快),2号机器发生的第二个请求改完数据库,马上修改完缓存就ok了,但此时一号机器才写完数据库,慢慢吞吞的修改完缓存;此时我们的缓存中保存着id为3的数据是1,但是我们缓存中保存的数据应该是2才对(最后一次更新是2,相当于出现了脏数据)。。。总结:双写模式会产生脏数据。那么我们该怎么解决??
    第一个设计方案就是加锁,我们第一个请求加锁,只有写数据库-1和写缓存-1执行完了,第二个请求才可以进来得到锁,这就不会产生数据不一致问题;第二个设计方案就是看看我们的业务是否会允许有暂时的业务不一致问题,举一个例子,京东在后台帮我们的商品数据修改了可呢五分钟,十分钟,二十分钟以后,我们的网站才是商品展示的数据,如果我们允许,就可以不管双写模式带来的脏数据问题,不管这个事情咋办???我们以前将数据放入缓存的时候,给每一个数据都加上了过期时间,比如一天,这个数据过期了就会自动删除,删除了以后,下一次再来查就会查出一份新的数据放入缓存,我们也可以把这个称为暂时性的脏数据问题(前提是我们得为缓存的数据加上过期时间);
  • 我们最终也发现无论我们怎么做,我们写数据库完以后,在写入缓存,缓存里面更新,我们再来读数据,我们读取到的数据肯定跟数据库的数据的那一刻有一段延迟时间,那就是看,我们这段延迟时间,看大家容忍有多大(容忍10s内看到新数据,还是容忍1ms内我们就要看到新数据,还是我们可以容忍一天,无论我们怎么容忍我们都可以把这个叫做最终的一致性
  • 最终的一致性:数据库改完以后的值,和我们最后看到的值,有一个比较大的延迟时间,但无论我们怎么延迟,我们最终都会看到数据库的最新数据,所以缓冲数据的一致性就是完全满足最终一致性的(我们缓存里面读取到的数据,最终就是数据库里面修改的新数据)
  • 如上就是我们双写模式容易出现的问题
  1. 失效模式
  • 我们修改完数据,直接删除完缓存,等待下次主动查询进行更新
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);    categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
        //删除缓存里面的数据
        //redis.del("catalogJSON")//等待下次主动查询进行更新
    }

在这里插入图片描述

  • 大并发下带来的问题:我们来举一个例子,我们数据更新以后,就来写数据库,然后在删除缓存,那么我们删除完缓存以后,缓存中就没有数据了,我们就会主动查询数据库进行更新缓存,最终得到最新数据;我们有第一个请求进来修改id为3的商品数据为1,修改完了,我们进行删除缓存;第二个请求进来,我们修改id为3的商品数据为2,但是可呢由于第二个机器负载比较重,做的比较慢,它想把id为3的数据改成2;第三个请求又进来了;现在我们会出现这么一个问题,当第一个请求进来的时候删除完缓存,我们第三个请求进来读缓存,里面没有数据,那么我们就去读数据库,可是这个时候,二号请求它比较慢它还没有更新完数据库,所以查询的数据库的值就是1,然后我们要更新缓存(如果更新的比较快,那么我们更新到了一个错误的数据,然后就没2号机器发送删除缓存删除了,还好)(更新的慢,如图,二号机器把缓存删除了,相当于没有更新缓存,就是把db-1里面的数据读到缓存里面)所以,我们发现,最新的数据库想把id为3的数据改成2还是放不到缓存里面只可以更新修改id为3的商品数据为1放入到缓存里面,这是一个写和读的并发问题,都是可以加锁来解决(加完锁系统就比较笨重一点);我们接下来就讨论另外一种场景,如果我们的数据经常修改的话,我们还要不要放入缓存????数据经常修改,就意味着我们要在项目里不断加锁,意味着项目运行会非常慢(不如不加锁,直接查询数据库);总结经常要修改的数据,我们就要经常读取数据库(实时性要求高)
  • 所以无论是双写模式还是失效模式,好的一点就是,我们都加了过期时间,哪怕暂时的脏数据,只要我们的过期时间一到,缓存一清,我们又可以得到新的数据,如果我们真的要解决缓存一致性问题,那我们我们看如下解决方案
    在这里插入图片描述
  • 如果我们数据库的最后一次更新没有映射到缓存上(线程乱了),我们就可以使用Canal(阿里开源的一个中间件)(可以伪装成mysql的从服务器),从服务器的特点就是mysql里面有什么变化,从服务器都是会获取到的,利用Canal的这个特性;只要我们的业务代码更新了这个数据库,我们的mysql数据库肯定要开启binlog的二进制日志(记录着mysql的更新日志),Canal就假装成Canal的从服务器,把每次mysql的更新都获取到,更新进入到redis里面;这个有一个好处就是编码期间,我们只要改数据库就行了(Canal在后台帮我们自动更新缓存,屏蔽了我们对缓存的操作),它的一个缺点:增加了一个额外的中间件,我们还要写自己定义的功能;
  • 在大数据情况下Caanal还可以来解决数据异构的问题:举一个例子,大家如果去京东每一个人的首页都不一样(都是基于你的个人爱好,特点),这些东西我们也可以使用canal来做,每一个浏览的商品数据库里面都有这些人记录的商品记录表,商品里面也有对应的记录表,购物车也是;我们现在进京东首页,如果想推荐一些与我相关的该怎么做呢??我可以使用canal,它可以实时的更新访问记录表里面的内容,包括商品订阅的一些更新,canal就知道有哪些记录,哪些更新进行分析计算,生成另外一种异构系统的用户推荐表,然后返回给web端
    在这里插入图片描述
  • 我们最终使用电商项目的解方案就是:失效模式(我们写完数据库直接把缓存一删除就可以了,自己触发主动更新,害怕更新出现乱序问题,我们就要加上读写锁来控制乱序)
    在这里插入图片描述
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值