Redis缓存

一、本地缓存与分布式缓存

1.1 缓存使用

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而db承担数据落盘工作,也就是承担持久化工作,它只需要将数据持久化保存到一个地方。

为了我们的系统能够加速访问,我们可以在第一次查出这个数据以后,就把它放入我们的缓存里面,以后我们需要数据,我们就直接从缓存中获取,没必要进行那些复杂的查询计算。这样就能极大的提升我们系统的性能1。

1.1.1 哪些数据适合放入缓存

1. 即时性、数据一致性要求不高的

什么是即时性

比如,购物的时候一些物流状态信息,比如10分钟看一次,20分钟开一次,你频率看的再高,他更新的速度非常慢,不是每走一米就更新一下,我们对它的即时性的要求也不高,所以,物流状态信息,我们就时候放在缓存里面。

什么是数据一致性

比如,我们数据库里面修改了商品的分类,如果我们使用了缓存,我们没有去改缓存,可能读取到的分类还是以前的分类,这个呢就叫不一致。但是,像商品分类这种,一致性要求不高,不高不是说不需要一致性,而是不需要很快的就到达一致性状态。比如,我们数据库里面商品的分类已经改了,可能3s,5s,甚至3分钟以后,缓存里面商品的分类数据才能更新起来,那我们三分钟以后,才能拿到最新的数据,看到一致的结果,这种我们还是可以接受的,因为这些数据并不是那么重要。

2.访问量大且更新频率不高的数据(读多,写少)

举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定),后台如果发布一个商品,买家需要5分钟才能看到新的商品,一般还是可以接受的。

比如,我们的商品,商品一旦录入以后,商品的介绍信息也好,基本属性也好,我们都不再做一些修改了,或者说很少做修改。这个时候呢,我们经常要查询商品数据,我们就可以把商品的信息放到缓存里面。因为商品的访问量特别大,每次都来查数据库,这是一个非常慢的操作。

比如,我们请求过来,要读取数据,我们就应该先从缓存中看一下有没有我们要的数据,如果缓存命中了,就直接返回结果结束了。如果,缓存里面没有这个数据,我们才需要查询数据库,然后,将查到的数据再放入缓存里面,把这个结果返回给我们当次请求。下次,我们再来请求,由于上一次已经从数据库里面查到的数据,给我们返回的同时,还把数据放到缓存里面了,所以,下次再来查询,缓存里面就有数据了,直接命中缓存,返回我们要的结果。

1.1.2 说起缓存,我们拿什么东西作为缓存呢

缓存的技术有很多,最简单的缓存方式就是Map,比如我们给Map里面放一个数据,我们以后要获取这个数据,先看Map里面有没有,如果有了直接返回 ,如果没有,查一下数据库,查到了再往Map中一放。

伪代码

data = cache.load(id); //从缓存加载数据
if (data == null) {
    data = db.load(id); //从数据库加载数据
    cache.put(id, data); //保存到cache中
}
return data;

 实际应用

@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    //这种方式我们成为本地缓存
    /**
     * 本地缓存跟我们当前的代码属于同一进程的,他们运行在同一个项目里面,在同一个JVM里面,只相当于在本地保存一个副本,我们称之为本地缓存。
     */
    private  Map<String, Object> cache = new HashMap<>();

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {

        //1. 如果缓存中有就用缓存的
        Map<String, List<Catelog2Vo>>  catalogJson = (Map<String, List<Catelog2Vo>>) cache.get("catalogJson");

        if (cache.get("catalogJson") == null) {
            System.out.println("查询了数据库");

            //将数据库的多次查询变为一次
            List<CategoryEntity> selectList = this.baseMapper.selectList(null);

            //1、查出所有分类
            //1、1)查出所有一级分类
            List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

            //封装数据
            Map<String, List<Catelog2Vo>> parentCid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                //1、每一个的一级分类,查到这个一级分类的二级分类
                List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

                //2、封装上面的结果
                List<Catelog2Vo> catelog2Vos = null;
                if (categoryEntities != null) {
                    catelog2Vos = categoryEntities.stream().map(l2 -> {
                        Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName().toString());

                        //1、找当前二级分类的三级分类封装成vo
                        List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());

                        if (level3Catelog != null) {
                            List<Catelog2Vo.Catelog3Vo> category3Vos = level3Catelog.stream().map(l3 -> {
                                //2、封装成指定格式
                                Catelog2Vo.Catelog3Vo category3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());

                                return category3Vo;
                            }).collect(Collectors.toList());
                            catelog2Vo.setCatalog3List(category3Vos);
                        }

                        return catelog2Vo;
                    }).collect(Collectors.toList());
                }
                return catelog2Vos;
            }));
            //从数据库获取到数据后,再放入本地缓存一份
            cache.put("catalogJson", parentCid);
            return parentCid;
        }
        return catalogJson;
    }
}

这里的Map就是本地缓存,本地缓存跟我们当前的代码属于同一进程的,他们运行在同一个项目里面,在同一个JVM里面,只相当于在本地保存一个副本,我们称之为本地缓存。

注意:在开发中,凡是放入缓存中的数据,我们都应该指定过期时间,使其可以在系统即使没有主动更新数据,也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致问题

1.2 本地缓存

假设,我们这有一个商品服务,我们获取分类,获取品牌,获取商品等等,这些数据我们经常要用,而且还不经常修改,一旦录入以后,基本就固化了。但是,如果我们每次查询都要走数据库,会很慢,我们可以选择,将这些数据查出以后,给缓存中放一份,这样以后,别人请求进来,只要是要这些数据,都可以先从缓存中看,缓存中有就给他返回,没有 呢,我们就在查一遍,查出数据了放到缓存里面,然后再返回,下次就再查出来了。

但是,如果我们使用本地缓存模式,那就是我们缓存的这个组件,这个Map跟我们都在同一个项目里面,会有什么问题呢?

如果我们这个应用是一个单体应用,永远只部署在一台机器,什么问题也没有,而且很快。

但是在分布式系统下,商品服务,可能会部署在十几个服务器,那这样每一个服务器里面,都自带了一个自己的本地缓存,这样就会出现这么一个问题。

问题一

比如,我第一次请求负载均衡来到第一台机器,第一台机器查数据的时候,缓存中没有,查了一次数据库,查到以后再放到缓存中了,那缓存中现在有数据了。如果我们下次请求,还能负载到第一台机器,那我们直接从缓存中拿就行了。

但如果没有负载到第一台机器,然后,我们来到了第二台机器,我们第二台机器的缓存,相当于还是没有缓存,还要查一遍数据库,依次类推,第三台还是没有,还要查数据库。由于他们是分开的,各顾各的,缓存中没有的话,都要各自再查一遍。

问题二

如果,我们对数据进行了修改,比如我们的三级分类数据改了一下,为了能够读取到正确的数据,我们还要改一下缓存中的数据。假设,第一次修改请求,来到第一台服务器,我们把这个分类数据修改了,把一号服务器的缓存改了,但是,2号3号服务器,之前缓存里面的数据,我们又没发改,以后,所有的请求,只要是负载均衡到了2号3号服务器,它拿到的数据,就跟1号服务器拿到的数据是完全不一样的,所以,这就产生了我们说的数据不一致的问题。所以,要解决这个问题,我们最终的方案应该是这样的,在我们分布式情况下,不应该再使用本地的这些缓存。

1.3 分布式缓存

我们的缓存使用方式,应该是这样的,我们可以将商品服务,缓存的所有数据,都可以放到一个中间件里面,大家都给集中的一个地方来缓存数据。这样,比如负载均衡来到第一台服务器,第一台服务器来看,缓存中没有,他呢就会给我们查出数据,查出以后返回给我们,并且,给缓存中放一份。那以后,负载均衡再过来,就算来到2号3号服务器,由于1号服务器之前已经给缓存中放过了,所以,2号服务器,就可以直接从缓存中拿到这个数据,就无需调用我们复杂的业务逻辑了。

同样的,如果我们的数据发生了修改,比如,我们的请求来到了3号服务器,这是一个修改请求,3号服务器,除了修改我们数据库外,修改完成以后呢,也将缓存中的数据,做了一个更新,这样,即使别请求,来到其他服务器,由于他们操作缓存,都是操作一个共同的指定地方,所以,就不会出现数据不一致的情况。

这是我们在分布式情况下的缓存,不应该把缓存放到它本地的进程里面,而是大家共享一个集中式的缓存中间件。

这个缓存中间件,就有很多中选择方案,比如,redis。而且,中间件的好处是,如果我们装了一台redis,我们容量不够,性能也不足,我们可以让redis做个集群工作,包括分片存储,比如,1号redis存1-10000,2号redis存10000-20000,这样呢,我们容量也可以做一个提升。所以,打破了我们本地缓存的容量限制,还可以做到高可用,高性能。

二、整合Redis测试

2.1 pom

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

2.2 yaml

2.3 测试Redis

三、Redis具体场景

3.1 Redis缓存

@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    //这种方式我们成为本地缓存
    /**
     * 本地缓存跟我们当前的代码属于同一进程的,他们运行在同一个项目里面,在同一个JVM里面,只相当于在本地保存一个副本,我们称之为本地缓存。
     */
    private  Map<String, Object> cache = new HashMap<>();

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        //给缓存中放json字符串,拿出的json字符串,还要逆转为能用的对象类型;【序列化与反序列化】

        //1. 加入缓存逻辑,缓存中存的数据是json字符串
        //JSON好处:跨语言,跨平台兼容
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)) {
            //2.缓存中没有,查询数据库
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
            //每次调用这个方法,都有查询数据库,得到返回值,效率非常低下,我们需要加入缓存
            //3. 查到的数据再放入缓存,将对象转为json放在缓存中
            String s = JSON.toJSONString(catalogJsonFromDb);
            redisTemplate.opsForValue().set("catalogJSON", s);
            return catalogJsonFromDb;
        }

        //从缓存中获取的JSON,再转为我们指定的对象返回
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        return result;
    }
}

 只要我们第一次进来,它先判断缓存中没有,然后,查数据库,查到结果以后,会把结果放到缓存中。

3.2 测试

 

 

四、缓存击穿、穿透、雪崩

4.1 缓存穿透

所谓的缓存失效指的就是,我们缓存没有命中,没有查到数据,我们缓存没有使用到,就可以简称为缓存失效。

我们只要查询一个一定不存在的数据,就会出现缓存穿透,穿透的原因就是因为我们没有将这次查询的空结果null写入缓存,假设这个1000号商品,数据查来是空的,我们给缓存里面也放一个标志位,比如说0,下次来查,就能够从缓存中拿到这个数据。这样只要是拿到了,就不会来到我们数据库,就不会产生缓存穿透的问题,也不用担心这些恶意攻击。

所以,要解决这个问题的做法,就是把我们的空结果null也进行缓存,但是,如果我们一直把这个空结果进行缓存,那未来即使有这个数据,我们缓存里面存的也都是没有。所以,我们就可以在缓存数据的时候,给我们的空结果加上一个短暂的过期时间,比如三分钟,五分钟过期,过期以后,下次查缓存中没有,就会真正的再去查一次数据库,但是,不管怎样,数据库此次查询有还是没有,我们都应当,将空结果继续缓存,这就是缓存穿透,我们一直来查询一个不存在的结果,导致缓存一直不命中,全部查数据库,就会导致数据库的瞬时压力过大,导致数据库崩溃。

4.2 缓存雪崩

缓存穿透,指的是查询一个不存在的数据,而缓存雪崩指的是什么呢

假设我们给缓存里面放了非常多的数据,比如,菜单数据、商品数据、品牌数据等等,但是,我们在放数据的时候,给每一个key都设置了相同的过期时间。比如,我们放的时候,这1w个数据都是同时放进去的,它们拥有相同的过期时间,等到某一时刻,这1w个数据全部在缓存中过期没有了,redis给它自动删掉了,这样,我们100w个并发进来,巧了这100w都是来查这1w个数据的,那这1w个数据在缓存中都没有,它们又进来去查数据库,数据库瞬时压力过重,导致雪崩。

雪崩指的是,我们所有存来的数据,大面积失效。雪崩指的是大面积key同时失效

所以,要解决这个问题,我们最好的办法,就是我们在存每一个数据的时候,它们的过期时间,最好来加一个随机的值,这样保证,我们缓存里面的所有数据,不会在某一时刻集体失效,导致缓存不命中,又把请求放给数据库,这就是我们说的缓存雪崩问题。

4.3 缓存击穿 

区别于缓存雪崩,雪崩呢是大面积key同时失效,什么是击穿呢

比如,我们枪法很准,我们瞄着某一个点,一直进行射击,最终这个点就会被打穿。

缓存击穿也类似于这样,我们会访问一个热点的key

什么叫热点的key

比如iphone刚发布新品,iphone手机上架了以后,每天大家光查iphone手机,比如这是1号商品,要面对百万的查询,但是,我们来存iphone手机的时候,我们给缓存里面设置iphone的过期时间,比如是一天,正好呢,到晚上没人的时候,iphone手机的信息缓存失效了,然后,到第二天一大早起来,突然高峰期100w流量请求,全进来,来查询iphone,但此时呢,iphone已经失效了,缓存中没有,相当于100w请求全部又进来,放给我们数据库了,这样又会导致缓存被击穿,我们全部压力被数据库承担,数据库最终崩溃,服务无响应。

所以,我们缓存击穿指的是某一个key失效,但是这个key是一个高频热点数据,我们每天都有很大的流量来访问。而且,失效的这一刻,正好是大量请求同时进来,要说是十来个请求,查询十次数据库倒也无所谓,但是恰好是百万并发进来,那就会导致缓存击穿了。

我们要解决这个问题,想到的方式就是加锁,既然大家都是去查一号商品,我们1百万的并发进来,都等一等,我们加上一把锁,只让一个人去查数据库查到以后呢,把这个结果放到缓存,然后释放锁,别人拿到锁以后,继续进行,但是不继续查数据库,而是,先来看我们缓存中,还有没有数据,如果有了就没有必要再查数据库了,这样最终,即使百万并发进来,只有一个人去查数据库,这就很合适了。

4.4 高并发下缓存失效问题总结

缓存穿透,指的是查询一个永不存在的数据

解决方案,是将空结果,也缓存进来,即使数据查询为空,下次来到缓存,缓存中能拿到这个数据,这个数据不是null,可能是一个标志位0也好1也好,或者true也好,false也好,我们没有拿到null数据,这样就不用去数据库查询,我们请求就全部放不进来了。

缓存雪崩,指的是我们大面积key同时失效,然后,高并发进来都是来查询这些请求,缓存里面没有,同时放进来,再次来查询数据库,数据库压力过大,导致我们服务无响应

解决的方案,是我们缓存每一个数据的时候,给它过期时间加上一个随机值。

缓存击穿,指的是某一个单点key,被高频访问,高频访问的前一刻,正好它失效了,百万并发全放进来,给数据库,导致数据库崩溃

解决方案是,加锁,

视频教程

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值