谷粒商城-高级篇(分布式锁与缓存)

2022-03-20 13:53:00 浏览数 (1)

一、缓存

1、本地缓存

1.1 使用hashmap本地缓存
代码语言:javascript复制
//测试本地缓存,通过hashmap
private Map<String,Object> cache=new HashMap<>();

public Map<String, List<Catalog2Vo>> getCategoryMap() {
      Map<String, List<Catalog2Vo>> catalogMap = (Map<String, List<Catalog2Vo>>) cache.get("catalogMap");
    //如果没有缓存,则从数据库中查询并放入缓存中
    if (catalogMap == null) {
        catalogMap = getCategoriesDb();
        cache.put("catalogMap",catalogMap);
    }
    return catalogMap;
}
1.2 整合redis进行测试

导入依赖

代码语言:javascript复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置redis主机地址

代码语言:javascript复制
spring:
  redis:
    host: 192.168.56.10
    port: 6379

使用springboot自动配置的RedisTemplate优化菜单获取业务

代码语言:javascript复制
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
   String catalogJson = ops.get("catalogJson");
   if (catalogJson == null) {
       Map<String, List<Catalog2Vo>> categoriesDb = getCategoriesDb();
       String toJSONString = JSON.toJSONString(categoriesDb);
       ops.set("catalogJson",toJSONString);
       return categoriesDb;
   }
   Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
   return listMap;

内存泄漏及解决办法

当进行压力测试时后期后出现堆外内存溢出OutOfDirectMemoryError

产生原因:

1)、springboot2.0以后默认使用lettuce操作redis的客户端,它使用通信

2)、lettuce的bug导致netty堆外内存溢出

解决方案:由于是lettuce的bug造成,不能直接使用-Dio.netty.maxDirectMemory去调大虚拟机堆外内存

1)、升级lettuce客户端。 2)、切换使用jedis

代码语言:javascript复制
<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>
1.3 高并发下缓存失效问题

缓存击穿

只查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,将失去缓存的意义

风险:

​ 利用不存在的数据进行攻击,数据库瞬间压力增大,最终导致崩溃

解决:

​ null结果缓存,并加入短暂过期时间

缓存雪崩

​ 缓存雪崩是指我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻失效,请求全部转发到DB,DB瞬间压力过大雪崩。

解决:

​ 将原有的失效时间基础上增加一个随机值,比如1-5分钟的随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存击穿

  • 对于一些设置过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
  • 如果这个key在大量请求同时进行前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿

解决:

​ 加锁。大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db。

1.4 加锁解决缓存击穿问题

将查询db的方法加锁,这样在同一时间只有一个方法能查询数据库,就能解决缓存击穿的问题了

代码语言:javascript复制
public Map<String, List<Catalog2Vo>> getCategoryMap() {
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        String catalogJson = ops.get("catalogJson");
        if (StringUtils.isEmpty(catalogJson)) {
            System.out.println("缓存不命中,准备查询数据库。。。");
            Map<String, List<Catalog2Vo>> categoriesDb = getCategoriesDb();
            String toJSONString = JSON.toJSONString(categoriesDb);
            ops.set("catalogJson",toJSONString);
            return categoriesDb;
        }
        System.out.println("缓存命中。。。。");
        Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
        return listMap;
    }

 private synchronized Map<String, List<Catalog2Vo>> getCategoriesDb() {
        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
        if (StringUtils.isEmpty(catalogJson)) {
            System.out.println("查询了数据库");
      		。。。。。
            return listMap;
        }else {
            Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
            return listMap;
        }
    }
1.5 锁时序问题

在上述方法中,我们将业务逻辑中的确认缓存没有查数据库放到了锁里,但是最终控制台却打印了两次查询了数据库。这是因为在将结果放入缓存的这段时间里,有其他线程确认缓存没有,又再次查询了数据库,因此我们要将结果放入缓存也进行加锁

优化代码逻辑后

代码语言:javascript复制
public Map<String, List<Catalog2Vo>> getCategoryMap() {
    ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        String catalogJson = ops.get("catalogJson");
        if (StringUtils.isEmpty(catalogJson)) {
            System.out.println("缓存不命中,准备查询数据库。。。");
            synchronized (this) {
                String synCatalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
                if (StringUtils.isEmpty(synCatalogJson)) {
                    Map<String, List<Catalog2Vo>> categoriesDb= getCategoriesDb();
                    String toJSONString = JSON.toJSONString(categoriesDb);
                    ops.set("catalogJson", toJSONString);
                    return categoriesDb;
                }else {
                    Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(synCatalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
                    return listMap;
                }
            }
        }
        System.out.println("缓存命中。。。。");
        Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
        return listMap;
}

优化后多线程访问时仅查询一次数据库

2、分布式缓存

2.1 本地缓存面临问题

当有多个服务存在时,每个服务的缓存仅能够为本服务使用,这样每个服务都要查询一次数据库,并且当数据更新时只会更新单个服务的缓存数据,就会造成数据不一致的问题

所有的服务都到同一个redis进行获取数据,就可以避免这个问题

2.2 分布式锁

当分布式项目在高并发下也需要加锁,但本地锁只能锁住当前服务,这个时候就需要分布式锁

2.3 分布式锁的演进

基本原理

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

阶段一

代码语言:javascript复制
	public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
        //阶段一
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
        //获取到锁,执行业务
        if (lock) {
            Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
            //删除锁,如果在此之前报错或宕机会造成死锁
            stringRedisTemplate.delete("lock");
            return categoriesDb;
        }else {
            //没获取到锁,等待100ms重试
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getCatalogJsonDbWithRedisLock();
        }
    }

public Map<String, List<Catalog2Vo>> getCategoryMap() {
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        String catalogJson = ops.get("catalogJson");
        if (StringUtils.isEmpty(catalogJson)) {
            System.out.println("缓存不命中,准备查询数据库。。。");
            Map<String, List<Catalog2Vo>> categoriesDb= getCategoriesDb();
            String toJSONString = JSON.toJSONString(categoriesDb);
            ops.set("catalogJson", toJSONString);
            return categoriesDb;
        }
        System.out.println("缓存命中。。。。");
        Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
        return listMap;
    }

问题:

1、setnx占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁

解决:设置锁的自动过期,即使没有删除,会自动删除

阶段二

代码语言:javascript复制
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
     Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
     if (lock) {
         //设置过期时间
         stringRedisTemplate.expire("lock", 30, TimeUnit.SECONDS);
         Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
         stringRedisTemplate.delete("lock");
         return categoriesDb;
     }else {
         try {
             Thread.sleep(100);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         return getCatalogJsonDbWithRedisLock();
     }
 }

问题:

1、setnx设置好,正要去设置过期时间,宕机。又死锁了。

解决:

设置过期时间和占位必须是原子的。redis支持使用setnx ex命令

阶段三

代码语言:javascript复制
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
    //加锁的同时设置过期时间,二者是原子性操作
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111",5, TimeUnit.SECONDS);
    if (lock) {
        Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
        //模拟超长的业务执行时间
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        stringRedisTemplate.delete("lock");
        return categoriesDb;
    }else {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return getCatalogJsonDbWithRedisLock();
    }
}

问题:

1、删除锁直接删除???

如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。

解决:

占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除。

阶段四

代码语言:javascript复制
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
       String uuid = UUID.randomUUID().toString();
       ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
    	//为当前锁设置唯一的uuid,只有当uuid相同时才会进行删除锁的操作
       Boolean lock = ops.setIfAbsent("lock", uuid,5, TimeUnit.SECONDS);
       if (lock) {
           Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
           String lockValue = ops.get("lock");
           if (lockValue.equals(uuid)) {
               try {
                   Thread.sleep(6000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               stringRedisTemplate.delete("lock");
           }
           return categoriesDb;
       }else {
           try {
               Thread.sleep(100);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           return getCatalogJsonDbWithRedisLock();
       }
   }

问题:

1、如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值。那么我们删除的是别人的锁

解决:

删除锁必须保证原子性。使用redis Lua脚本完成

阶段五-最终形态

代码语言:javascript复制
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
       String uuid = UUID.randomUUID().toString();
       ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
       Boolean lock = ops.setIfAbsent("lock", uuid,5, TimeUnit.SECONDS);
       if (lock) {
           Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
           String lockValue = ops.get("lock");
           String script = "if redis.call("get",KEYS[1]) == ARGV[1] thenn"  
                   "    return redis.call("del",KEYS[1])n"  
                   "elsen"  
                   "    return 0n"  
                   "end";
           stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), lockValue);
           return categoriesDb;
       }else {
           try {
               Thread.sleep(100);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           return getCatalogJsonDbWithRedisLock();
       }
   }

保证加锁【占位 过期时间】和删除锁【判断 删除】的原子性。更难的事情,锁的自动续期

2.4 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),从而让使用者能够将精力更集中地放在处理业务逻辑上。

本文我们仅关注分布式锁的实现,更多请参考官方文档

2.4.1 环境搭建

导入依赖

代码语言:javascript复制
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>

开启配置

代码语言:javascript复制
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.56.102:6379");
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}
2.4.2 可重入锁(Reentrant Lock)
代码语言:javascript复制
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisson() {
       Map<String, List<Catalog2Vo>> categoryMap=null;
       RLock lock = redissonClient.getLock("CatalogJson-Lock");
       lock.lock();
       try {
           Thread.sleep(30000);
           categoryMap = getCategoryMap();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }finally {
           lock.unlock();
           return categoryMap;
       }
   }

如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,所以就设置了过期时间,但是如果业务执行时间过长,业务还未执行完锁就已经过期,那么就会出现解锁时解了其他线程的锁的情况。

所以Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

在本次测试中CatalogJson-Lock的初始过期时间TTL为30s,但是每到20s就会自动续借成30s

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

代码语言:javascript复制
// 加锁以后10秒钟自动解锁,看门狗不续命
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}
如果传递了锁的超时时间,就执行脚本,进行占锁;
如果没传递锁时间,使用看门狗的时间,占锁。如果返回占锁成功future,调用future.onComplete();
没异常的话调用scheduleExpirationRenewal(threadId);
重新设置过期时间,定时任务;
看门狗的原理是定时任务:重新给锁设置过期时间,新的过期时间就是看门狗的默认时间;
锁时间/3是定时任务周期

edisson同时还为分布式锁提供了异步执行的相关方法:

代码语言:javascript复制
RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);

RLock对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore 对象.

代码语言:javascript复制
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisson() {
    Map<String, List<Catalog2Vo>> categoryMap=null;
    RLock lock = redissonClient.getLock("CatalogJson-Lock");
    lock.lock();
    try {
        Thread.sleep(30000);
        categoryMap = getCategoryMap();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }finally {
        lock.unlock();
        return categoryMap;
    }
}

最佳实战:自己指定锁时间,时间长点即可

2.4.3 读写锁(ReadWriteLock)
代码语言:javascript复制
@GetMapping("/read")
@ResponseBody
public String read() {
    RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
    RLock rLock = lock.readLock();
    String s = "";
    try {
        rLock.lock();
        System.out.println("读锁加锁" Thread.currentThread().getId());
        Thread.sleep(5000);
        s= redisTemplate.opsForValue().get("lock-value");
    }finally {
        rLock.unlock();
        return "读取完成:" s;
    }
}

@GetMapping("/write")
@ResponseBody
public String write() {
    RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
    RLock wLock = lock.writeLock();
    String s = UUID.randomUUID().toString();
    try {
        wLock.lock();
        System.out.println("写锁加锁" Thread.currentThread().getId());
        Thread.sleep(10000);
        redisTemplate.opsForValue().set("lock-value",s);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }finally {
        wLock.unlock();
        return "写入完成:" s;
    }
}

写锁会阻塞读锁,但是读锁不会阻塞读锁,但读锁会阻塞写锁

总之含有写的过程都会被阻塞,只有读读不会被阻塞

上锁时在redis的状态

2.4.4 信号量(Semaphore)

信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用acquire()方法增加数量,也可以调用release()方法减少数量,但是当调用release()之后小于0的话方法就会阻塞,直到数字大于0

代码语言:javascript复制
@GetMapping("/park")
@ResponseBody
public String park() {
    RSemaphore park = redissonClient.getSemaphore("park");
    try {
        park.acquire(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "停进2";
}

@GetMapping("/go")
@ResponseBody
public String go() {
    RSemaphore park = redissonClient.getSemaphore("park");
    park.release(2);
    return "开走2";
}
2.4.5 闭锁(CountDownLatch)

可以理解为门栓,使用若干个门栓将当前方法阻塞,只有当全部门栓都被放开时,当前方法才能继续执行。

以下代码只有offLatch()被调用5次后 setLatch()才能继续执行

代码语言:javascript复制
@GetMapping("/setLatch")
  @ResponseBody
  public String setLatch() {
      RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
      try {
          latch.trySetCount(5);
          latch.await();
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      return "门栓被放开";
  }

  @GetMapping("/offLatch")
  @ResponseBody
  public String offLatch() {
      RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
      latch.countDown();
      return "门栓被放开1";
  }

闭锁在redis的存储状态

3、缓存数据的一致性

3.1 双写模式

当数据更新时,更新数据库时同时更新缓存

存在问题

由于卡顿等原因,导致写缓存2在最前,写缓存1在后面就出现了不一致

这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据

3.2 失效模式

数据库更新时将缓存删除

存在问题

当两个请求同时修改数据库,一个请求已经更新成功并删除缓存时又有读数据的请求进来,这时候发现缓存中无数据就去数据库中查询并放入缓存,在放入缓存前第二个更新数据库的请求成功,这时候留在缓存中的数据依然是第一次数据更新的数

解决方法

1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新

2、读写数据的时候(并且写的不频繁),加上分布式的读写锁。

3.3 解决方案

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

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

总结:

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

4、 SpringCache

这部分可以参考我之前的学习笔记:https://cloud.tencent.com/developer/article/1955387

4.1 导入依赖
代码语言:javascript复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
4.2 自定义配置

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

代码语言:javascript复制
spring:
  cache:
    #指定缓存类型为redis
    type: redis
    redis:
      #指定redis中的过期时间为1h
      time-to-live: 3600000
      #如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
      #key-prefix: CACHE_
      use-key-prefix: true
      #是否缓存空值,防止缓存穿透
      cache-null-values: true

默认使用jdk进行序列化,自定义序列化方式需要编写配置类

代码语言:javascript复制
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {

    /**
     * 配置文件的配置没有用上
     * 1. 原来和配置文件绑定的配置类为:@ConfigurationProperties(prefix = "spring.cache")
     *                                public class CacheProperties
     * <p>
     * 2. 要让他生效,要加上 @EnableConfigurationProperties(CacheProperties.class)
     */
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // config = config.entryTtl();
        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;
    }
}
4.3 自定义序列化原理

缓存使用

代码语言:javascript复制
 	//调用该方法时会将结果缓存,缓存名为category,key为方法名
//表示该方法的缓存被读取时会加锁
@Cacheable(value = {"category"},key = "#root.methodName",sync = true)
   public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithSpringCache() {
       return getCategoriesDb();
   }

//调用该方法会删除缓存category下的所有cache
   @Override
   @CacheEvict(value = {"category"},allEntries = true)
   public void updateCascade(CategoryEntity category) {
       this.updateById(category);
       if (!StringUtils.isEmpty(category.getName())) {
           categoryBrandRelationService.updateCategory(category);
       }
   }

第一个方法缓存结果后

第二个方法调用清除缓存后

4.4 Spring-Cache的不足之处
  1. 读模式
  2. 缓存穿透:查询一个null数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true
  3. 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题

  • 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
  1. 写模式:(缓存与数据库一致)
  2. 读写加锁。
  3. 引入Canal,感知到MySQL的更新去更新Redis
  4. 读多写多,直接去数据库查询就行
  5. 总结: 常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache): 写模式(只要缓存的数据有过期时间就足够了) 特殊数据:特殊设计

0 人点赞