redis的了解与使用 缓存的使用(整个redis&缓存穿透/雪崩/击穿&本地锁)

480 阅读9分钟

一、缓存

1,缓存的使用

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落 盘工作。 哪些数据适合放入缓存? (1)即时性、数据一致性要求不高的 (2)访问量大且更新频率不高的数据(读多,写少)

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

在这里插入图片描述

本地缓存与分布式缓存 本地缓存:和微服务同一个进程。缺点:分布式时本地缓存不能共享 分布式缓存:缓存中间件,例如:redis

2,整合redis作为缓存

1安装redis

官网下载

2pom.xml中引入redis依赖

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

版本统一交给父项目管理

3在application.yml添加redis配置

在这里插入图片描述

位于spring下

4使用StringRedisTemplate操作redis

 @Resource
    StringRedisTemplate stringRedisTemplate;
    @Test
    public void testRedisTeplate(){
        //hello  world
        ValueOperations<String, String> stringStringValueOperations = stringRedisTemplate.opsForValue();
        //保存
        stringStringValueOperations.set("hello","world_"+ UUID.randomUUID().toString());
        //查询
        String hello = stringStringValueOperations.get("hello");
        System.out.println(hello);
    }

测试结果如下

在这里插入图片描述

5切换使用jedis解决OutOfDirectMemoryError 堆外内存溢出

    //1)springboot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信
    //2)lettuce的bug导致堆外内存溢出 -Xmx300m; netty如果没有指定堆外内存 默认使用-Xmx300m;
    //  可以通过-Dio.netty.maxDirectMemory进行设置
    //解决方案:不能使用-Dio.netty.maxDirectMemory只去调大堆外内存
    //1)升级lettuce客户端   2)切换使用jedis
    //RedisTemplate
    //lettuce jedis操作redis的底层客户端   spring再次封装成RedisTemplate;

修改pom.xml 切换使用jedis

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

3,改造三级分类业务

修改product模块中CategoryServiceImpl方法

@Autowired
StringRedisTemplate redisTemplate;

//TODO 产生堆外内存溢出:OutOfDirectMemoryError
//(1)springboot2.0以后默认使用lettuce作为操作redis客户端。它使用netty进行网络通信
//(2)lettuce的bug导致netty堆外内存溢出  -Xmx300m;netty如果没有指定堆外内存,默认使用-Xmx300m
//     可以通过-Dio.netty.maxDirectMemory进行设置
// 解决方案:不能使用-Dio.netty.maxDirectMemory只去调大堆外内存。
//(1)升级lettuce客户端
//(2)切换使用jedis
@Override
public  Map<String, List<Catelog2Vo>> getCatalogJson() {
    //给缓存中放json字符串,拿出的json字符串,还要逆转为能用的对象类型:【序列化与反序列化】
    //1、加入缓存逻辑,缓存中存的数据是json字符串
    //JSO跨语言、跨平台兼容
    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;
    }
    //转为我们指定的对象
    Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
    return result;
}


/**
 * 从数据库查询并封装分类数据
 * @return
 */
public  Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
    /**
     * 1、将数据库的多次查询变为一次
     */
    List<CategoryEntity> selectList = baseMapper.selectList(null);

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

    //2、封装数据
    Map<String, List<Catelog2Vo>> parent_cid = level1Catagorys.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());
                //1、找当前二级分类的三级分类封装成vo
                List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId());
                if(level3Catalog!=null){
                    List<Catelog2Vo.Category3Vo> collect = level3Catalog.stream().map(l3 -> {
                        //2、封装成指定格式
                        Catelog2Vo.Category3Vo category3Vo = new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                        return category3Vo;
                    }).collect(Collectors.toList());
                    catelog2Vo.setCatalog3List(collect);
                }
                return catelog2Vo;
            }).collect(Collectors.toList());
        }
        return catelog2Vos;
    }));
    return parent_cid;
}

private List<CategoryEntity> getParent_cid( List<CategoryEntity> selectList, Long parent_cid) {
    List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid() == parent_cid).collect(Collectors.toList());
    return collect;
}

lettuce和jedis是操作redis的底层客户端,RedisTemplate是再次封装

二、缓存失效问题

解决大并发读情况下缓存失效问题

1,缓存穿透

(1)含义   指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不 存在的数据每次请求都要到存储层去查询,失去了缓存的意义 (2)风险  利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃 (3)解决方案  null结果缓存,并加入短暂过期时间

2, 缓存雪崩

(1)含义  缓存雪崩是指在我们设置缓存时key采用了相同的过期时间, 导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时 压力过重雪崩。(2)解决方案:  原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这 样每一个缓存的过期时间的重复率就会降低,就很难引发集体 失效的事件。   出现雪崩:降级 熔断   事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。   事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉   事后:利用 redis 持久化机制保存的数据尽快恢复缓存

3, 缓存击穿

(1)含义  对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。 如果这个key在大量请求同时进来前正好失效,那么所有对 这个key的数据查询都落到db,我们称为缓存击穿。 (2)解决方案  加锁   大量并发只让一个去查,其他人等待,查到以后释放锁,其他 人获取到锁,先查缓存,就会有数据,不用去db

4,加锁解决缓存击穿问题

在这里插入图片描述

在这里插入图片描述

1修改product模块中CategoryServiceImpl方法

  /**
     * 1,空结果缓存:解决缓存穿透
     * 2,设置过期时间(加随机值):解决缓存雪崩
     * 3,加锁:解决缓存击穿
     *
     * @return
     */


    //TODO 产生堆外内存溢出:OutOfDirectMemoryError
    //1)springboot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信
    //2)lettuce的bug导致堆外内存溢出 -Xmx300m; netty如果没有指定堆外内存 默认使用-Xmx300m;
    //  可以通过-Dio.netty.maxDirectMemory进行设置
    //解决方案:不能使用-Dio.netty.maxDirectMemory只去调大堆外内存
    //1)升级lettuce客户端   2)切换使用jedis
    //RedisTemplate
    //lettuce jedis操作redis的底层客户端   spring再次封装成RedisTemplate;
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        //给缓存中放json字符串 拿出json的字符串  还能逆转为能用的对象类型{序列化和反序列化}

        //1,加入缓存逻辑,缓存中存放的都是json数据
        //json数据的好处是跨平台跨语言兼容
        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)) {
            //2,缓存中没有,则去数据库中查
            System.out.println("缓存不命中。。。将要查询数据库。。。");
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();
        }
        //转化为指定对象
        System.out.println("缓存命中。。。。直接返回。。。。");
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        return result;

    }

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {

        String uuid = UUID.randomUUID().toString();
        //1,占分布式锁  去redis占锁
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        Map<String, List<Catelog2Vo>> dataFromDb = null;
//        System.out.println(lock+uuid+"=======================");
        if (lock) {
            //加锁成功  执行业务
            //2,设置过期时间   过期时间必须与占锁同步 原子性
//            stringRedisTemplate.expire("lock",30,TimeUnit.SECONDS);
            System.out.println("获取分布式锁成功.....");
            try {
                dataFromDb = getDataFromDb();

            } finally {
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                Long unlock = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }
            //获取值对比+对比成功删除+原子操作  lua脚本操作
//            String lockValue = stringRedisTemplate.opsForValue().get("lock");
//            if (lockValue.equals(uuid)){
//                //删除自己的锁
//                stringRedisTemplate.delete("lock");
//            }

            return dataFromDb;
        } else {
            //加锁失败 重试   synchronized ()
            //休眠100ms重试
            System.out.println("获取分布式锁失败.....等待重试");
            try{
                Thread.sleep(200);
            }catch (Exception e){

            }
            return getCatalogJsonFromDbWithRedisLock(); //自旋的方式
        }


    }

    private Map<String, List<Catelog2Vo>> getDataFromDb() {
        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
        if (!StringUtils.isEmpty(catalogJSON)) {
            //缓存不为null直接返回
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return result;
        }
        System.out.println("查询了数据库。。。。");

        List<CategoryEntity> categoryEntities1 = baseMapper.selectList(null);

        //1,查出所有1级分类
        List<CategoryEntity> level1Categorys = getParent_cid(categoryEntities1, 0L);
        //2,封装数据
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //1,每一个的一级分类,查到这个一级分类的二级分类
            List<CategoryEntity> categoryEntities = getParent_cid(categoryEntities1, 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());
                    //1 找当前二级分类的三级分类封装成vo
                    List<CategoryEntity> level3Catelog = getParent_cid(categoryEntities1, l2.getCatId());

                    if (level3Catelog != null) {
                        //2,封装成指定格式
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }

            return catelog2Vos;
        }));
        //3,查到数据再放入缓存中,将对象转化为json数据存放到缓存中
        String s = JSON.toJSONString(parent_cid);
        stringRedisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
        return parent_cid;
    }


    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithLocalLock() {

        //只要是同一把锁,就能锁住所有需要这个锁的所有线程
        //1,synchronized(this):springboot所有组件在容器中都是单例的
        //TODO 本地锁,synchronized,JUC(Lock),在分布式下必须使用分布锁

        synchronized (this) {
            //得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
            return getDataFromDb();

        }


    }
//        //1,如果缓存中有就用缓存的
//        Map<String,List<Catelog2Vo>> catalogJson = (Map<String,List<Catelog2Vo>>) cache.get("catalogJson");
//        if (cache.get("catalogJson")==null){
//
//            List<CategoryEntity> categoryEntities1 = baseMapper.selectList(null);
//
//            //1,查出所有1级分类
//            List<CategoryEntity> level1Categorys = getParent_cid(categoryEntities1,0L);
//            //2,封装数据
//            Map<String,List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k->k.getCatId().toString(),v->{
//                //1,每一个的一级分类,查到这个一级分类的二级分类
//                List<CategoryEntity> categoryEntities = getParent_cid(categoryEntities1,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());
//                        //1 找当前二级分类的三级分类封装成vo
//                        List<CategoryEntity> level3Catelog = getParent_cid(categoryEntities1,l2.getCatId());
//
//                        if (level3Catelog!=null){
//                            //2,封装成指定格式
//                            List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3->{
//                                Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
//                                return catelog3Vo;
//                            }).collect(Collectors.toList());
//                            catelog2Vo.setCatalog3List(collect);
//                        }
//                        return catelog2Vo;
//                    }).collect(Collectors.toList());
//                }
//
//                return catelog2Vos;
//            }));
//            cache.put("catalogJson",parent_cid);
//            return parent_cid;
//        }
//            return catalogJson;

    private List<CategoryEntity> getParent_cid(List<CategoryEntity> categoryEntities1, Long parent_cid) {
        List<CategoryEntity> collect = categoryEntities1.stream().filter(item -> item.getParentCid() == parent_cid).collect(Collectors.toList());
        //        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
        return collect;
    }

锁时序问题:之前的逻辑是查缓存没有,然后取竞争锁查数据库,这样就造成多 次查数据库。 解决方法:竞争到锁后,再次确认缓存中没有,再去查数据库。

2

jemeter 测试 100的并发 查看是否锁住,是否只查询了一次数据库

在这里插入图片描述

在这里插入图片描述

确实只查询了一遍数据库锁住了。

5,本地锁在分布式下问题

1,模拟多个product服务,分别copy三个端口号,更改端口号为10001 10002 10003

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2,修改jemeter

在这里插入图片描述

3清除之前测试的缓存数据,启动测试,查看结果

结果发现,每一个服务的控制台都显示查询了一次数据库,也就是说只能锁住本地服务,不能解决分布式下的所问题