Resis分布式缓存

  • 本地缓存:单体应用时没有什么问题,但是当微服务集群的时候,就会出现数据不一致性以及每次还需要重复查询的问题。
  • 分布式缓存:可以很好的解决本地缓存的问题,使用缓存中间件。

SpringBoot使用Redis.

1、引入redis-starter

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

2、简单配置redis

  redis:
    port: 6379
    host: 121.43.234.157

3、使用springboot配置好的 StringRedisTemplate 模板.


项目中自测使用:

    //TODO 如果使用低版本的redis, 对redis进行压力测试时,会产生堆外内存泄漏异常: OutOfDirectMemoryError
    // 1)springboot2.0以后默认使用lettuce作为操作redis的客户端.它使用netty进行网络通信.
    // 2)lettuce的bug导致netty堆外内存溢出.netty如果没有指定堆外内存,默认使用-Xmlx300m
    //可以通过 -Dio.netty.maxDirectMemory 只去调大堆外内存.
    //解决方案:不能使用-Dio.netty.maxDirectMemory只去调大堆外内存.
    //1)升级 lettuce客户端. 2)切换使用jedis
    //redisTemplate:
        // lettuce、jedis操作redis的底层客户端,Spring再次封装了redisTemplate.
    /**
     *
     * 使用redis作为缓存
     *
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatelogJson() {
        String catelogJSON = redisTemplate.opsForValue().get("catelogJSON");
        //如果redis中没有就从数据库查询并将查询结果放入redis中.
        if (StringUtils.isEmpty(catelogJSON)) {
            Map<String, List<Catelog2Vo>> jsonFromDB = getCatelogJsonFromDB();
            //存入转为json存储的好处:json是跨言,跨平台兼容
            redisTemplate.opsForValue().set("catelogJSON", JSON.toJSONString(jsonFromDB));
            return jsonFromDB;
        }
        //如果redis中有,就转为我们指定对象
        Map<String, List<Catelog2Vo>> stringListMap = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
//        JSONObject jsonObject = JSON.parseObject(catelogJSON);
        return stringListMap;
    }

高并发下缓存失效问题

缓存穿透:

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

缓存雪崩:

  1. 缓存雪崩是指我们设置缓存时key采用了全部相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
  2. 解决:
  • 再原来失效时间的基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
  • 设置热点数据永远不过期。
  • 出现雪崩:降级 熔断
  • 事后:利用 redis 持久化机制保存的数据尽快恢复缓存

缓存击穿:

  1. 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发的访问,是一种非常"热点"的数据。但是这个key恰巧在大量请求到来之前正好失效,那么所有对这个key的数据查询全部落到了DB,我们称之为缓存击穿。
  2. 加锁:大量并发只让一个线程去查询,其它线程等待,查询到以后释放锁,其它人获得锁之后,先查询缓存中的数据,如果有就不用再去查询DB。

通过本地锁实现缓存

public Map<String, List<Catelog2Vo>> getCatelogJson() {
    String catelogJSON = redisTemplate.opsForValue().get("catelogJSON");
    //如果redis中没有就从数据库查询并将查询结果放入redis中.
    if (StringUtils.isEmpty(catelogJSON)) {
        System.out.println("缓存未命中....");
        /**
         * 解决缓存穿透、缓存雪崩、缓存击穿
         *
         */
        Map<String, List<Catelog2Vo>> jsonFromDB = getCatelogJsonLocalLock();
        return jsonFromDB;
    }
    System.out.println("缓存成功命中....");
    //如果redis中有,就转为我们指定对象
    Map<String, List<Catelog2Vo>> stringListMap = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
    });
    return stringListMap;
}

    /**
     * 本地锁(适用于单个实例,分布式环境下不再适用)
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatelogJsonLocalLock() {
        /**
         * 加锁:解决缓存击穿问题(适用单体项目)
         * 只要是同一把锁就可以锁住需要锁住这个锁的所有线程
         * synchronized (this):对于SpringBoot容器中所有组件都是单例的,所以使用this可以锁住
         */
        //TODO 本地锁:synchronized、JUC(Lock),本地锁只可以锁住当前服务的所有线程,但是分布式下是多个实例,所以要想锁住所有就必须使用分布式锁.
        synchronized (this) {
            return getCatelogJsonFromDB();
        }
    }

    /**
     * 从数据库查询数据
     * @return
     */
    private Map<String, List<Catelog2Vo>> getCatelogJsonFromDB() {
        //加锁之后还需要再次判断缓存中是否有缓存
        String catelogJSON = redisTemplate.opsForValue().get("catelogJSON");
        if (!StringUtils.isEmpty(catelogJSON)) {
            Map<String, List<Catelog2Vo>> stringListMap = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return stringListMap;
        }
        System.out.println("查询了数据库.."+Thread.currentThread().getName());
        //获取所有的节点信息
        List<CategoryEntity> levels = baseMapper.selectList(null);
        //获取所有的一级分类节点
        List<CategoryEntity> level1Categorys = getParent_cid(levels, 0L);
        Map<String, List<Catelog2Vo>> collect1 = null;
        if (level1Categorys != null) {
            collect1 = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                //还需要继续封装, 封装父节点下面的子节点
                List<CategoryEntity> category2List = getParent_cid(levels, v.getCatId());
                List<Catelog2Vo> c2List = null;
                if (category2List != null) {
                    c2List = category2List.stream().map(c2 -> {
                        Catelog2Vo c2Vo = new Catelog2Vo(c2.getCatId().toString(), c2.getName(), v.getCatId().toString(), null);
                        List<Catalog3Vo> collect = null;
                        //需要继续封装 孙子节点数据
                        List<CategoryEntity> c3List = getParent_cid(levels, c2.getCatId());
                        if (c3List != null) {
                            collect = c3List.stream().map(c3 -> {
                                Catalog3Vo catalog3Vo = new Catalog3Vo(c3.getCatId().toString(), c3.getName(), c2.getCatId().toString());
                                return catalog3Vo;
                            }).collect(Collectors.toList());
                        }
                        c2Vo.setCatalog3List(collect);
                        return c2Vo;
                    }).collect(Collectors.toList());
                }
                return c2List;
            }));
        }
        //存入转为json存储的好处:json是跨言,跨平台兼容
        redisTemplate.opsForValue().set("catelogJSON", JSON.toJSONString(collect1), 1, TimeUnit.DAYS);
        return collect1;
    }

在这里插入图片描述
通过上述代码及图分析可知,当服务为单体时,即不是分布式环境,本地锁可以解决缓存击穿问题。在分布式环境下,如果使用本地锁,有多少个服务还是要去查询多少次DB.所以并没有彻底的解决问题,还需要分布式锁来实现。


Redis 分布式锁
实现原理
SET key value [EX seconds] [PX milliseconds] [NX|XX]
在这里插入图片描述
代码实现:

  /**
     * Redis 分布锁  基本原理 SET key value [EX seconds] [PX milliseconds] [NX|XX]
     * 主要逻辑:加锁的原理性操作、删锁的原子性操作
     *
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatelogJsonRedisLock() {
        //分布式锁,占锁
        Boolean hasLock = redisTemplate.opsForValue().setIfAbsent("lock", "locak");
        //占锁成功
        if (hasLock) {
            //执行业务逻辑
            Map<String, List<Catelog2Vo>> jsonFromDB = getCatelogJsonFromDB();
            //删除锁
            redisTemplate.delete("lock");
            return jsonFromDB;
        } else {
            //采用自旋的方式
            return getCatelogJsonRedisLock();
        }
    }

其中占锁的方法 setIfAbsent()使用的是 SETNX.
在这里插入图片描述
代码此时还存在问题,当执行完业务逻辑业务代码异常或系统宕机此时解锁代码并没有执行,就会造成死锁。
解决方法:设置锁的有效时间即自动过期,即使没有删除,到期后也会自动删除。即
在这里插入图片描述


设置key自动过期:
   /**
     * Redis 分布锁  基本原理 SET key value [EX seconds] [PX milliseconds] [NX|XX]
     * 主要逻辑:加锁的原理性操作、删锁的原子性操作
     *
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatelogJsonRedisLock() {
        //分布式锁,占锁
        Boolean hasLock = redisTemplate.opsForValue().setIfAbsent("lock", "locak");
        //占锁成功
        if (hasLock) {
            //设置key值自动过期时间
            redisTemplate.expire("lock",300,TimeUnit.SECONDS);
            //执行业务逻辑
            Map<String, List<Catelog2Vo>> jsonFromDB = getCatelogJsonFromDB();
            //删除锁
            redisTemplate.delete("lock");
            return jsonFromDB;
        } else {
            //采用自旋的方式
            return getCatelogJsonRedisLock();
        }
    }

设置看key值自动过期时间之后还是存在问题:过期时间和占锁必须是原子的。如果不是原子的话,有可能在设置过期时间之前系统宕机,就又会有死锁问题发生。
解决方法:将占锁和设置过期时间为原子操作。redis语法支持 set key value EX seconds NX.即
在这里插入图片描述

    public Map<String, List<Catelog2Vo>> getCatelogJsonRedisLock() {
        //分布式锁,占锁
//        Boolean hasLock = redisTemplate.opsForValue().setIfAbsent("lock", "locak");
        Boolean hasLock = redisTemplate.opsForValue().setIfAbsent("lock", "locak", 300, TimeUnit.SECONDS);
        //占锁成功
        if (hasLock) {
            //设置key值自动过期时间
//            redisTemplate.expire("lock",300,TimeUnit.SECONDS);
            //执行业务逻辑
            Map<String, List<Catelog2Vo>> jsonFromDB = getCatelogJsonFromDB();
            //删除锁
            redisTemplate.delete("lock");
            return jsonFromDB;
        } else {
            //采用自旋的方式
            return getCatelogJsonRedisLock();
        }
    }

在这里插入图片描述


此时还存在问题: 如果我们直接删除锁的话,可能会由于业务执行时间过长,而此时锁又已经自动过期,我们直接执行删除操作,可能会把别人的锁给删除了。
解决:占锁的时候,指定一个UUID作为value值,删除锁之前进行判断操作。即:
    /**
     * Redis 分布锁  基本原理 SET key value [EX seconds] [PX milliseconds] [NX|XX]
     * 主要逻辑:加锁的原理性操作、删锁的原子性操作
     *
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatelogJsonRedisLock() {
        String s = UUID.randomUUID().toString();
        //分布式锁,占锁
//        Boolean hasLock = redisTemplate.opsForValue().setIfAbsent("lock", "locak");
        Boolean hasLock = redisTemplate.opsForValue().setIfAbsent("lock", s, 300, TimeUnit.SECONDS);
        //占锁成功
        if (hasLock) {
            //设置key值自动过期时间
//            redisTemplate.expire("lock",300,TimeUnit.SECONDS);
            //执行业务逻辑
            Map<String, List<Catelog2Vo>> jsonFromDB = getCatelogJsonFromDB();
            //删除锁
            String value = redisTemplate.opsForValue().get("lock");
            if (value.equals(s)){
                redisTemplate.delete("lock");
            }
            return jsonFromDB;
        } else {
            //采用自旋的方式
            return getCatelogJsonRedisLock();
        }
    }

虽然进行了value值的判断,但是删除key仍然不是原子操作,有可能我们在获取value值返回的路上此时key值自动到期,我们还是会删除错误。 解决:将删除操作也变为原子操作. 调用 execute()方法.
/*
 * (non-Javadoc)
 * @see org.springframework.data.redis.core.RedisOperations#execute(org.springframework.data.redis.core.script.RedisScript, java.util.List, java.lang.Object[])
 */
@Override
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
	return scriptExecutor.execute(script, keys, args);
}
   public Map<String, List<Catelog2Vo>> getCatelogJsonRedisLock() {
        String s = UUID.randomUUID().toString();
        //分布式锁,占锁
//        Boolean hasLock = redisTemplate.opsForValue().setIfAbsent("lock", "locak");
        Boolean hasLock = redisTemplate.opsForValue().setIfAbsent("lock", s, 300, TimeUnit.SECONDS);
        //占锁成功
        if (hasLock) {
            //设置key值自动过期时间
//            redisTemplate.expire("lock",300,TimeUnit.SECONDS);
            //执行业务逻辑
            Map<String, List<Catelog2Vo>> jsonFromDB = getCatelogJsonFromDB();
            //删除锁
            String value = redisTemplate.opsForValue().get("lock");

//            if (value.equals(s)){
//                redisTemplate.delete("lock");
//            }
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return " +
                    "redis.call('del', KEYS[1]) else return 0 end";
            //方法参数 : 脚本和返回值类型, 参数,根据key获取的value值 ,原子操作脚本删除
            Long execute = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), value);
            return jsonFromDB;
        } else {
	       		 try {
	                Thread.sleep(100);
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
            //采用自旋的方式
            return getCatelogJsonRedisLock();
        }
    }

在这里插入图片描述
在这里插入图片描述
此时,还有锁得自动续期。即当业务执行过程中,锁得有限期时间到了,怎么保证程序可以继续执行下去。最简单的办法,将业务逻辑 try cayth.


自己手写分布式缓存逻辑 代码过于啰嗦且还没有更好的解决锁的自动续期问题,因此又引出了 Redisson,它提供了分布式锁等其它有关分布式的内容.
概述: 官网
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

  1. 环境搭建
	<dependency>
	    <groupId>org.redisson</groupId>
	    <artifactId>redisson-spring-boot-starter</artifactId>
	    <version>3.13.6</version>
	</dependency>

使用配置
Redisson 通过RedissonClient 调用.

@Configuration
public class MyredissonConfig {

    @Value("${spring.redis.host}")
    private String ipAddr;

    @Bean(destroyMethod="shutdown")
    RedissonClient redisson() throws IOException {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + ipAddr + ":6379");
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}
  1. 可重入锁(Reentrant Lock)
    基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。
    Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟。另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了,此时不会自动续时,因此要注意指定加锁的时间与业务处理的时间。因此可以通过可重入锁的看门狗机制解决redis锁的自动过期时间。
    @GetMapping("/test")
    @ResponseBody
    public String test() throws InterruptedException {
        //获取一把锁,只要锁得名字一样就是同一把锁
        RLock lock = redisson.getLock("my-lock");
        //加锁
        //lock.lock(); //阻塞式等待  RLock 内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。
        //锁的自动续期,会自动给锁续上新的时间30s,不用担心业务时间太长,锁自动过期。
        //加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁会在默认时间到期后自动删除.
        //*******************************
        //如果我们传递了锁的超时时间就给redis发送超时脚本 默认超时时间就是我们指定的
        //如果我们未指定,就使用 30 * 1000 [LockWatchdogTimeout] Rlock看门狗的默认时间 只要占锁成功 就会启动一个定时任务 任务就是重新给锁设置过期时间
        // 这个时间还是 [LockWatchdogTimeout] 的时间 1/3 看门狗的时间续期一次 续成满时间
        lock.lock(10, TimeUnit.SECONDS); //十秒之后自动解锁,且不会续期,所以要求 lessTime一定要大于业务执行时间,否则会发生死锁.
        try {
            System.out.println("加锁成功..执行业务逻辑方法..." + Thread.currentThread().getId());
            Thread.sleep(20000);
        } finally {
            //释放锁 假设没有手动释放锁,redisson也不会出现死锁现象.
            System.out.println("释放锁.." + Thread.currentThread().getId());
            lock.unlock();
        }
        return "test";
    }

实战代码:
 /**
 1. 使用 redisson 配置redis分布式锁
 2.  */
public Map<String, List<Catelog2Vo>> getCatelogJsonRedissonLock() {

    RLock lock = redisson.getLock("catelog-lock");
    lock.lock(30000, TimeUnit.SECONDS);
    Map<String, List<Catelog2Vo>> catelogJsonFromDB = null;
    try {
        catelogJsonFromDB = getCatelogJsonFromDB();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
    }
    return catelogJsonFromDB;
}
如果传递了锁的超时时间,就执行脚本,进行占锁;
如果没传递锁时间,使用看门狗的时间,占锁。如果返回占锁成功future,调用future.onComplete();
没异常的话调用scheduleExpirationRenewal(threadId);
重新设置过期时间,定时任务;
看门狗的原理是定时任务:重新给锁设置过期时间,新的过期时间就是看门狗的默认时间;
锁时间1/3是定时任务周期,当时间过了默认时间的三分之一 即十秒,就会自动续时间为30s;
  1. 读写锁(ReadWriteLock)
    基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。
    分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
/**
     * 读写锁
     * 保证一定可以读取到最新数据,修改期间,写锁是一个排他锁(互斥锁、独享锁)。读锁是一个共享锁
     * 写锁没有释放之前,读就必须等待
     * 读+读: 相当于无锁,并发读,只会在redis中记录好
     * 写+读: 需等待写锁释放
     * 写+写: 阻塞方式
     * 读+写:有读锁,写也必须要等待
     * 只要有写锁存在,都必须要等待.
     */
    @GetMapping("/writeLock")
    @ResponseBody
    public String writeLock() {
        RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
        //获取写锁
        RLock wLock = lock.writeLock();
        wLock.lock();
        String s = "";
        try {
            System.out.println("进行写加锁.." + Thread.currentThread().getId());
            Thread.sleep(10000);
            s = UUID.randomUUID().toString();
            redisTemplate.opsForValue().set("write-Value", s);
        } catch (Exception e) {

        } finally {
            System.out.println("写解锁.." + Thread.currentThread().getId());
            wLock.unlock();
        }
        return s;
    }

    @GetMapping("/readLock")
    @ResponseBody
    public String readLock() {
        RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
        //获取写锁
        RLock rLock = lock.readLock();
        rLock.lock();
        String s = "";
        try {
            System.out.println("进行读加锁.." + Thread.currentThread().getId());
//            Thread.sleep(10000);
            s = (String) redisTemplate.opsForValue().get("write-Value");
        } catch (Exception e) {

        } finally {
            System.out.println("读解锁.." + Thread.currentThread().getId());
            rLock.unlock();
        }
        return s;
    }
  1. 信号量(Semaphore)
    /**
     * 信号量
     *  信号量:也可以用作限流
     */
    @GetMapping("/park")
    @ResponseBody
    public String park() throws InterruptedException {
        //获得信号量锁
        RSemaphore semaphore = redisson.getSemaphore("park");
//        semaphore.acquire(); //获取一个信号值,获取一个值,占一个车位
        boolean b = semaphore.tryAcquire();
        return "获取车位 =>" + b;
    }
    @GetMapping("/go/park")
    @ResponseBody
    public String gopark() throws InterruptedException {
        //获得信号量锁
        RSemaphore semaphore = redisson.getSemaphore("park");
        semaphore.release();
        return "park车位加一";
    }
  1. 闭锁(CountDownLatch)
 /**
     * 闭锁 CountDownLatch
     */
    @GetMapping("/lockDoor")
    @ResponseBody
    public String CountDownLatch() throws InterruptedException {
        RCountDownLatch countDownLatch = redisson.getCountDownLatch("door");
        boolean b = countDownLatch.trySetCount(5);
        countDownLatch.await();
        return "所有年级全部放假了.可以关闭校门了..";
    }
    @GetMapping("/go/{id}")
    @ResponseBody
    public String gogogo(@PathVariable("id") Long id) throws InterruptedException {
        RCountDownLatch countDownLatch = redisson.getCountDownLatch("door");
        //每次访问一次相当一 放假了一个年级.
        countDownLatch.countDown();
        return id+"年级放假了..";
    }

缓存和数据库一致性

分布式保持缓存和数据库一致性:

  1. 双写模式:写数据库后,写缓存
    问题:在这里插入图片描述
  2. 失效模式:写完数据库后,删缓存,等下次查询更新缓存
    在这里插入图片描述
    解决方案:

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


SpringCache 官网

spring从3.1开始定义了Cache、CacheManager接口来统一不同的缓存技术。并支持使用JCache(JSR-107)注解简化我们的开发。

每次调用需要缓存功能的方法时,spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
常用注解:

  • @Cacheable: Triggers cache population. 触发将数据保存到缓存的操作.
  • @CacheEvict: Triggers cache eviction.触发将数据从缓存删除的操作. (失效模式)
  • @CachePut: Updates the cache without interfering with the method
    execution.不影响方法执行更新缓存.(双写模式)
  • @Caching: Regroups multiple cache operations to be applied on a
    method. 组合以上操作
  • @CacheConfig: Shares some common cache-related settings at
    class-level. 在类级别共享缓存的相同配置.
  • 环境
<!--SpringCache 缓存依赖,简化缓存开发-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

指定缓存类型并在主配置类上加上注解@EnableCaching

spring:
  cache:
    type: redis
    redis:
      time-to-live: 3600000  #指定缓存的存货时间 ms
      #key-prefix: CACHE_  #key的前缀
      use-key-prefix: true #是否启用前缀
      cache-null-values: true  #是否缓存Null值防止缓存 穿透

默认使用jdk进行序列化(可读性差),默认ttl为-1永不过期,自定义序列化方式需要编写配置类

@EnableConfigurationProperties(CacheProperties.class)  //将配置类添加到容器中
@Configuration
@EnableCaching  //开启缓存
public class MyCacheConfig {
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {

        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
                .defaultCacheConfig();
        //指定缓存序列化方式为json
        config = config.serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        //设置配置文件中的各项配置,如过期时间
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }

        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;
    }
}

自动配置原理:
CacheAutoConfiguration自动配置了RedisCacheConfiguration.

原理:CacheAutoConfiguration->RedisCacheConfiguration自动的配置了RedisCacheManager,初始化所有缓存->每个缓存决定使用什么配置,redisCacheConfiguration有就用已有的,没有就用默认配置->想要修改默认的配置,只需要自定义
RedisCacheConfiguration即可->就会应用到当前RedisCacheManager管理的所有缓存分区中.

  • @Cacheable 缓存中有就从缓存中获取, 缓存中无就调用方法,并将结果放到缓存中.
    sync = true 开启本地同步锁
/**
     * 查询所有一级分类
     *
     * @Cacheable 缓存中有就从缓存中获取, 缓存中无就调用方法,并将结果放到缓存中.
     * 缓存的数据值 默认使用jdk序列化(可以通过配置 RedisCacheConfiguration 来设置为Json数据)
     * 默认ttl时间 -1 (可以通过配置 RedisCacheConfiguration 来设置)
     * key: 里面默认会解析表达式 字符串用 '' SpEl
     * value:【可以当做缓存的分区(按照业务类型去分 )】
     */
    @Cacheable(value = {"category"}, key = "#root.method.name",sync = true)
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        long l = System.currentTimeMillis();
        List<CategoryEntity> list = categoryDao.getLevel1Categorys();

        System.out.println("总耗时:" + (System.currentTimeMillis() - l));
        return list;
    }

	改造后的查询三级缓存菜单
    /**
     * 使用SpringCache 改造后的数据缓存, 开始无需判断缓存中是否包含,因为 SpirngCache 使用了 本地锁的方式解决缓存击穿问题
     *  sync = true: --- 开启本地同步锁
     *  都是先去缓存中读取数据,如果没有在 查询,最后在将数据放到缓存里面去。
     */
    @Cacheable(value = "category",key = "#root.methodName",sync = true)
    public Map<String, List<Catelog2Vo>> getCatelogJson() {
        System.out.println("查询了数据库.." + Thread.currentThread().getName());
        //获取所有的节点信息
        List<CategoryEntity> levels = baseMapper.selectList(null);
        //获取所有的一级分类节点
        List<CategoryEntity> level1Categorys = getParent_cid(levels, 0L);
        Map<String, List<Catelog2Vo>> collect1 = null;
        if (level1Categorys != null) {
            collect1 = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                //还需要继续封装, 封装父节点下面的子节点
                List<CategoryEntity> category2List = getParent_cid(levels, v.getCatId());
                List<Catelog2Vo> c2List = null;
                if (category2List != null) {
                    c2List = category2List.stream().map(c2 -> {
                        Catelog2Vo c2Vo = new Catelog2Vo(c2.getCatId().toString(), c2.getName(), v.getCatId().toString(), null);
                        List<Catalog3Vo> collect = null;
                        //需要继续封装 孙子节点数据
                        List<CategoryEntity> c3List = getParent_cid(levels, c2.getCatId());
                        if (c3List != null) {
                            collect = c3List.stream().map(c3 -> {
                                Catalog3Vo catalog3Vo = new Catalog3Vo(c3.getCatId().toString(), c3.getName(), c2.getCatId().toString());
                                return catalog3Vo;
                            }).collect(Collectors.toList());
                        }
                        c2Vo.setCatalog3List(collect);
                        return c2Vo;
                    }).collect(Collectors.toList());
                }
                return c2List;
            }));
        }
        return collect1;
    }
  • @CacheEvict 缓存失效模式, 触发就从执行redis中删除操作
  • @Caching(evict = {
    @CacheEvict(value = “category”, key = “‘getLevel1Categorys’”),
    @CacheEvict(value = “category”, key = “‘getCatelogJson’”)
    })
    /**
     * @CacheEvict 缓存失效模式, 触发就从执行redis中删除操作
     *  @CacheEvict(value = "category",allEntries = true)  删除整个分区下面的数据缓存
     */
    //@CacheEvict(value = "category", key = "'getLevel1Categorys'")
//    @Caching(evict = {
//            @CacheEvict(value = "category", key = "'getLevel1Categorys'"),
//            @CacheEvict(value = "category", key = "'getCatelogJson'")
//    })
    @CacheEvict(value = "category",allEntries = true)
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
    }

通过Debug分析RedisCache类中的方法,可以知道@Cacheable 使用本地锁的方式来解决缓存击穿问题.(先查询缓存中是否有数据,有就从缓存中拿,没有就查询DB,之后将查询结果放入到缓存.)


SpringCache原理与不足

  1. 读模式
  • 缓存穿透:查询一个null的数据。解决方案:将查询出来的null数据进行缓存,通过 spring.cache.redis.cache-null-values=true设置.
  • 缓存击穿:大量并发同时访问一个刚好过期的缓存数据。解决方案:加锁,默认sync=false是不加锁的,谁用sync=true,加本地锁.
  • 缓存雪崩:大量的key同时过期.解决:加随机过期时间。

2.写模式(缓存与数据库实时保持一致):

  • 读写加锁.
  • 引入Canal,感知mysql的更新去更新Redis缓存.
  • 读多写多的数据,直接去查询数据库.

3.总结:

常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache.

写模式(只要缓存的数据有过期时间就足够了)

特殊数据:特殊设计:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值