缓存和分布式锁

缓存和分布式锁

  • 1. 缓存
    • 1.1. 缓存的使用
    • 1.2 整合redis作为缓存
  • 2. 缓存失效问题
    • 2.1 缓存穿透
    • 2缓存雪崩
    • 2.3缓存击穿
  • 3.分布式锁的原理与使用
    • 3.1分布式下如何加锁
    • 3.2 分布式锁演进-基本原理
      • 3.2.1 分布式锁演进-阶段一
      • 3.2.2分布式锁演进-阶段二
      • 3.3.3 分布式锁演进-阶段三
      • 3.3.4 分布式锁演进-阶段四
      • 3.3.5 分布式锁演进-阶段五-最终形态
  • 4.Redissn完成分布式锁
    • 4.1.简介
    • 4.2整合Redisson作为分布式锁等功能的框架
      • 4.2.1.引入依赖
      • 4.2.2 进行配置
      • 4.2.3进行测试
    • 4.3 锁
      • 4.3.1 可重入锁(Reentrant Lock)
      • 4.3.2 读写锁
      • 4.3.3 信号量
      • 4.3.4 闭锁
        • 4.3.4.1. 闭锁的原理
        • 4.3.4.2 应用场景
        • 4.3.4.3示例
    • 4.4 数据一致性问题
      • 4.4.1 双写模式
      • 4.4.2 失效模式
      • 4.4.3 解决方案
  • 5.Spring Cache
    • 5.1 简介
    • 5.2 基础概念
    • 5.3 SpringCache整合SpringBoot简化缓存开发
      • 5.3.1引入依赖
      • 5.3.2 写配置
      • 5.3.3测试使用缓存
        • 5.3.3.1. 开启缓存功能
        • 5.3.3.2. @Cacheable测试
          • 5.3.3.2.1 结论
          • 5.3.3.2.2 自定义参数
        • 5.3.3.3. @CacheEvict测试
    • 5.4 spring-cache的不足
      • 5.4.1 读模式
      • 5.4.2写模式
      • 5.4.3 总结

1. 缓存

1.1. 缓存的使用

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落 盘工作。
哪些数据适合放入缓存?

  • 即时性、数据一致性要求不高的
  • 访问量大且更新频率不高的数据(读多,写少)

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

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

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

1.2 整合redis作为缓存

  1. 添加依赖
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>

        <dependency>
            <groupId>redis.clientsgroupId>
            <artifactId>jedisartifactId>
        dependency>
  1. yml配置redis
spring:
  redis:
    host: 47.93.21.100
    port: 6379
  1. 使用 RedisTemplate 操作 redis
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void test(){
        ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
        valueOperations.set("hollow","word");
        String hollow = valueOperations.get("hollow");
        System.out.println(hollow);
    }


2. 缓存失效问题

2.1 缓存穿透

  1. 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数 据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
  2. 在流量大时,可能DB就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。
  3. 解决: 缓存空结果、并且设置短的过期时间。

2缓存雪崩

  1. 缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
  2. 解决: 原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

2.3缓存击穿

  1. 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问, 是一种非常“热点”的数据。
  2. 这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所 有对这个 key 的数据查询都落到 db,我们称为缓存击穿。
  3. 解决: 加锁

3.分布式锁的原理与使用

3.1分布式下如何加锁

缓存和分布式锁_第2张图片
本地锁,只能锁住当前进程,所以我们需要分布式锁

3.2 分布式锁演进-基本原理

缓存和分布式锁_第3张图片
我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,直到释放锁。 “占坑”可以去redis,可以去数据库,可以去任何大家都能访问的地方。 等待可以自旋的方式

3.2.1 分布式锁演进-阶段一

缓存和分布式锁_第4张图片

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

        //1. 分布式锁 去redis占坑
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
        if(lock){
            //加锁成功
            Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
            redisTemplate.delete("lock");
            return dataFromDb;
        }else {
            //加锁失败  重试  synchronize
            return getCatalogJsonFromDbWithRedisLock();//自旋的方式
        }
    }

3.2.2分布式锁演进-阶段二

缓存和分布式锁_第5张图片

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

        //1. 分布式锁 去redis占坑
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
        if(lock){
            //加锁成功
            //2. 设置过期时间
            redisTemplate.expire("lock",30,TimeUnit.SECONDS);
            Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
            redisTemplate.delete("lock");
            return dataFromDb;
        }else {
            //加锁失败  重试  synchronize
            return getCatalogJsonFromDbWithRedisLock();//自旋的方式
        }
    }

3.3.3 分布式锁演进-阶段三

缓存和分布式锁_第6张图片

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

        //1. 分布式锁 去redis占坑
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",300,TimeUnit.SECONDS);
        if(lock){
            //加锁成功
            //2. 设置过期时间
//            redisTemplate.expire("lock",30,TimeUnit.SECONDS);
            Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
            redisTemplate.delete("lock");
            return dataFromDb;
        }else {
            //加锁失败  重试  synchronize
            return getCatalogJsonFromDbWithRedisLock();//自旋的方式
        }
    }

3.3.4 分布式锁演进-阶段四

缓存和分布式锁_第7张图片

    public Map<String, List<Catelog2Vo>>  getCatalogJsonFromDbWithRedisLock() {
        String uuid = UuidUtils.generateUuid().toString();
        //1. 分布式锁 去redis占坑
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
        if(lock){
            //加锁成功
            //2. 设置过期时间
//            redisTemplate.expire("lock",30,TimeUnit.SECONDS);
            Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
            String lockValue = redisTemplate.opsForValue().get("lock");
            if(lockValue.equals(uuid)) {
                //删除自己的锁
                redisTemplate.delete("lock");
            }
            return dataFromDb;
        }else {
            //加锁失败  重试  synchronize
            return getCatalogJsonFromDbWithRedisLock();//自旋的方式
        }
    }

3.3.5 分布式锁演进-阶段五-最终形态

缓存和分布式锁_第8张图片


    public Map<String, List<Catelog2Vo>>  getCatalogJsonFromDbWithRedisLock() {
        String uuid = UuidUtils.generateUuid().toString();
        //1. 分布式锁 去redis占坑
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
        if(lock){
            //加锁成功
            Map<String, List<Catelog2Vo>> dataFromDb = null;
            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 lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

            }

//            String lockValue = redisTemplate.opsForValue().get("lock");
//            if(lockValue.equals(uuid)) {
//                //删除自己的锁
//                redisTemplate.delete("lock");
//            }

            return dataFromDb;
        }else {
            //加锁失败  重试  synchronize
            return getCatalogJsonFromDbWithRedisLock();//自旋的方式
        }
    }

4.Redissn完成分布式锁

4.1.简介

Redisson 是架设在 Redis 基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了 Redis 键值数据库提供的一系列优势,基于 Java 实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工 具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。 官方文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

4.2整合Redisson作为分布式锁等功能的框架

4.2.1.引入依赖

        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redissonartifactId>
            <version>3.12.0version>
        dependency>

4.2.2 进行配置

@Configuration
public class MyRedissonConfig {

    /**
     * 所有对Redisson的使用都是通过RedissonClient对象
     * @return
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() throws IOException {
        //1.创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://47.93.21.100:6379");
        //根据config创建出RedissonClient示例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

4.2.3进行测试

@SpringBootTest
class GulimallProductApplicationTests {
    @Autowired
    private RedissonClient redissonClient;

    @Test
    public void test1(){
        System.out.println(redissonClient);
    }    
}

4.3 锁

4.3.1 可重入锁(Reentrant Lock)

可重入锁是某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。再次获取锁的时候会判断当前线程是否是已经加锁的线程,如果是对锁的次数+1,释放锁的时候加了几次锁,就需要释放几次锁。
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

 @ResponseBody
    @GetMapping("/hello")
    public String hello(){
        //1.获取一把锁,只要锁的名字一样,就是同一把锁
        RLock lock = redisson.getLock("my-lock");
        //2.加锁
        lock.lock();//阻塞式等待 默认加的锁是30s
        //1. 锁的自动续期 如果业务超长,运行期间自动给锁续上新的30s,不用担心业务时间长,锁自动过期会删除
        //2.加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s后自动删除
        try {
            System.out.println("加锁成功,执行业务"+Thread.currentThread().getId());
            Thread.sleep(30000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //3.解锁
            System.out.println("释放锁"+Thread.currentThread().getId());
            lock.unlock();
        }
        return "hello";
    }

大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
特定

  1. 锁的自动续期 如果业务超长,运行期间自动给锁续上新的30s,不用担心业务时间长,锁自动过期会删除
  2. 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s后自动删除

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

 lock.lock(10, TimeUnit.SECONDS);//10秒自动解锁,自动解锁时间一定要大于业务的执行时间
  1. 问题:lock.lock(10, TimeUnit.SECONDS) 在锁时间到了以后不会自动续期
    1.如果我们传递了锁的过期时间,就发送给redis执行脚本,进行占锁,默认超时时间就是我们指定的时间
    2.如果我们指定了锁的超时时间,就使用30*1000[lockWatchdogTimeout看门狗的默认时间];只要占锁成功,就会启动一个定时任务[重新给锁设置过期时间,新的过期时间就是看门狗的默认时间]定时任务的周期的看门狗默认时间的三分之一进行自动续期,续期时间为满30s

4.3.2 读写锁

基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

 @GetMapping("/write")
    @ResponseBody
    public String writeValue(){
        String s="";
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        RLock lock = readWriteLock.writeLock();
        try {
            //改数据加写锁 读数据加读锁
            lock.lock();
            s = UUID.randomUUID().toString();
            Thread.sleep(30000);
            redisTemplate.opsForValue().set("writeValue",s);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
        return s;
    }

    @GetMapping("/read")
    @ResponseBody
    public String readValue(){
        String s="";
        RReadWriteLock writeLock = redisson.getReadWriteLock("rw-lock");
        //加读锁
        Lock rLock = writeLock.readLock();
        try {
            rLock.lock();
            s = redisTemplate.opsForValue().get("writeValue").toString();
            Thread.sleep(30000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            rLock.unlock();
        }

        return s;
    }

结论

  1. 保证一定可以读到最新的数据,修改期间,写锁是一个排他锁(互斥锁,独享锁).读锁是一个共享锁
  2. 写锁没有释放 读就必须等待
  3. 读 + 读:相当于无锁并发读,只会的redis中记录好,所有当前的读锁,他们都会同时加锁成功
  4. 写 + 读:等待写锁释放
  5. 写 + 写:阻塞方式
  6. 读 + 写:有读锁也需要等待
  7. 只要有写锁的存在,都必须等待

4.3.3 信号量

基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
这里以停车位为例,当停车时,获取一个信号量,获取到信号量之后进行停车,车开走之后可以在释放一个信号量

    /**
     * 车库停车
     * @return
     * @throws InterruptedException
     * 信号量 可以用作分布式限流
     */
    @GetMapping("/park")
    @ResponseBody
    public String park() throws InterruptedException {
        RSemaphore park = redisson.getSemaphore("park");
        park.acquire();//获取一个信号量,获取一个信号量占一个车位
        return "ok";
    }

    @GetMapping("/go")
    @ResponseBody
    public String go(){
        RSemaphore park = redisson.getSemaphore("park");
        park.release();//释放一个车位
        return "ok";
    }

4.3.4 闭锁

4.3.4.1. 闭锁的原理

闭锁相当于一扇门,在闭锁到达结束状态之前,这扇门一直是关闭着的,没有任何线程可以通过,当到达结束状态时,这扇门才会打开并容许所有线程通过。它可以使一个或多个线程等待一组事件发生。闭锁状态包括一个计数器,初始化为一个正式,正数表示需要等待的事件数量。countDown方法递减计数器,表示一个事件已经发生,而await方法等待计数器到达0,表示等待的事件已经发生。CountDownLatch强调的是一个线程(或多个)需要等待另外的n个线程干完某件事情之后才能继续执行。

4.3.4.2 应用场景

10个运动员准备赛跑,他们等待裁判一声令下就开始同时跑,当最后一个人通过终点的时候,比赛结束。10个运动相当于10个线程,这里关键是控制10个线程同时跑起来,还有怎么判断最后一个线程到达终点。可以用2个闭锁,第一个闭锁用来控制10个线程等待裁判的命令,第二个闭锁控制比赛结束。

4.3.4.3示例

5个班放学,当5个班的同学都走完之后,锁门

    @GetMapping("/lockDoor")
    @ResponseBody
    public String lockDoor() throws InterruptedException {
        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.trySetCount(5);
        door.await();
        return "放假了";
    }

    @GetMapping("/gogogo/{id}")
    @ResponseBody
    public String gogogo(@PathVariable("id") Long id){
        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.countDown();//计算减一
        return id+"班的人走完了";
    }

4.4 数据一致性问题

4.4.1 双写模式

缓存和分布式锁_第9张图片

4.4.2 失效模式

缓存和分布式锁_第10张图片

4.4.3 解决方案

• 无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?

  1. 如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加 上过期时间,每隔一段时间触发读的主动更新即可
  2. 如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
  3. 缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
  4. 通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心 脏数据,允许临时脏数据可忽略);

• 总结:

  1. 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保 证每天拿到当前最新数据即可。
  2. 我们不应该过度设计,增加系统的复杂性 • 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。

5.Spring Cache

5.1 简介

  1. Spring 从 3.1 开始定义了 org.springframework.cache.Cache 和org.springframework.cache.CacheManager 接口来统一不同的缓存技术; 并支持使用 JCache(JSR-107)注解简化我们开发;
  2. Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合; Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 ; 如 RedisCache , EhCacheCache , ConcurrentMapCache 等;
  3. 每次调用需要缓存功能的方法时,Spring 会检查检查指定参数的指定的目标方法是否已 经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓 存结果后返回给用户。下次调用直接从缓存中获取。
  4. 使用 Spring 缓存抽象时我们需要关注以下两点;
    1. 确定方法需要被缓存以及他们的缓存策略
    2. 从缓存中读取之前缓存存储的数据

5.2 基础概念

缓存和分布式锁_第11张图片

5.3 SpringCache整合SpringBoot简化缓存开发

5.3.1引入依赖

<dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
     <groupId>org.springframework.bootgroupId>
     <artifactId>spring-boot-starter-data-redisartifactId>
 dependency>

5.3.2 写配置

  1. 自动配置:CacheAutoConfiguration会导入RedisCacheConfiguration自动配置好缓存管理器RedisCacheManager
  2. 使用redis作为缓存
spring.cache.type=redis

5.3.3测试使用缓存

@Cacheable 触发将数据保存到缓存的操作
@CacheEvict 触发数据从缓存删除的操作
@CachePut 不影响方法执行更新缓存
@Caching 组合以上多个操作
@CacheConfig 在类级别共享缓存的相同配置

5.3.3.1. 开启缓存功能

@EnableCaching

@MapperScan("com.atguigu.gulimall.product.dao")
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.atguigu.gulimall.product.feign")
@EnableCaching
public class GulimallProductApplication {
    public static void main(String[] args) {
        SpringApplication.run(GulimallProductApplication.class, args);
    }
}

5.3.3.2. @Cacheable测试

    //每一个需要缓存的数据我们都来指定要放到那个名字的缓存【缓存的分区(按照业务类型分)】
    @Cacheable({"category"}) //代表当前方法的结果需要进行缓存,如果缓存中有方法不用调用,缓存中没有会调用方法,最后将方法的结果存入缓存
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        QueryWrapper<CategoryEntity> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("parent_cid",0);
        List<CategoryEntity> list = baseMapper.selectList(queryWrapper);
        return list;
    }

效果
缓存和分布式锁_第12张图片

5.3.3.2.1 结论
  • 每一个需要缓存的数据我们都来指定要放到那个名字的缓存【缓存的分区(按照业务类型分)】
  • .@Cacheable({“category”})
    1. 代表当前方法的结果需要进行缓存,如果缓存中有方法不用调用
    2. 缓存中没有会调用方法,最后将方法的结果存入缓存
  • 默认行为
    1. 如果缓存中有方法不调用
    2. key默认自动生成:缓存的名字::SimpleKey[] (自主生成的key值)
    3. 缓存的value值:默认使用jdk的序列化机制,序列化的数据存到redis
    4. 默认ttl -1秒
5.3.3.2.2 自定义参数
  1. 指定生成缓存使用的key:key属性指定一个,接受一个SpEL
	@Cacheable(value = {"category"},key = "'level'")
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        QueryWrapper<CategoryEntity> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("parent_cid",0);
        List<CategoryEntity> list = baseMapper.selectList(queryWrapper);
        return list;
    }
  1. 指定缓存数据的缓存时间:配置文件里面改ttlspring.cache.redis.time-to-live=60000
  2. 将数据存为JSON格式
    配置类
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {

    
//    @Autowired
//    CacheProperties cacheProperties;
    
    /**
     * 配置文件中的东西没有用上
     *      @ConfigurationProperties(prefix = "spring.cache")
     * public class CacheProperties {
     * @return
     */

    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));


        CacheProperties.Redis redisProperties = cacheProperties.getRedis();

        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;
    }
}

配置参数

spring.cache.type=redis
spring.cache.redis.time-to-live=60000
# 如果指定了前缀就用指定的前缀,如果没有就默认使用缓存的名字作为前缀
spring.cache.redis.key-prefix=CACHE
# 是否使用缓存前缀 默认为TRUE
spring.cache.redis.use-key-prefix=true

# 是否缓空值 防止缓存穿透
spring.cache.redis.cache-null-values=true

5.3.3.3. @CacheEvict测试

    /**
     * 级联更新所有关联的数据
     * 缓存失效模式
     *  1.同时进行多种操作 @caching
     *  2.指定删除某个分区下的所有数据 @CacheEvict(value = {"category"},allEntries = true)
     *  3.存储同一类型的数据,都可以指定成同一分区,分区名默认就是缓存的前缀
     * @param category
     */
    @Override
//    @Caching(
//            evict = {
//                    @CacheEvict(value = {"category"},key = "'getLevel1Categorys'"),
//                    @CacheEvict(value = {"category"},key = "'getCatalogJson'")
//            }
//    )

    @CacheEvict(value = {"category"},allEntries = true)
    @Transactional
    public void updateDetail(CategoryEntity category) {
        //修改分类
        updateById(category);
        //修改其他的分类信息
        categoryBrandRelationService.updateCategpry(category.getCatId(),category.getName());
       // TODO


    }

缓存失效模式

  1. 同时进行多种操作 @caching
  2. 指定删除某个分区下的所有数据 @CacheEvict(value = {"category"},allEntries = true)
  3. 存储同一类型的数据,都可以指定成同一分区,分区名默认就是缓存的前缀

5.4 spring-cache的不足

5.4.1 读模式

  1. 缓存穿透:查一个null值的数据,解决:清空缓存数据 spring.cache.redis.cache-null-values=true
  2. 缓存击穿:大量并发进来同时查询一个正好过期的数据,默认不加锁,可以加锁@Cacheable(value = {"category"},key = "#root.method.name",sync = true) 只有@Cacheable可以
  3. 缓存雪崩:大量的key同时过期 解决:加随机时间

5.4.2写模式

  1. 读写加锁
  2. 引入Canal,感知mysql的更新去更新数据库
  3. 读多写多,直接去数据库查询

5.4.3 总结

  1. 常规数据(读多写少,即实时性一致性要求不高的数据)完全可以使用Spring-cache,写模式(只有缓存的数据的过期时间足够就可以了)
  2. 特殊数据:特殊设计

你可能感兴趣的:(java,缓存,分布式,redis)