缓存

使用Redis实现分布式缓存

  • Spring Boot 已经整合了Redis,使用时需要引入starter,并配置Redis的Server信息
  • 提供了RedisTemplate和StringRedisTemplate来操作Redis ```java // 通用的Redis模板 @Autowired private RedisTemplate redisTemplate

// 只能存储String的Redis模版 @Autowired private StringRedisTemplate stringRedisTemplate

public void test() { // 获取String数据类型的操作对象 ValueOperations ops = stringRedisTemplate.opsForValue(); // 存数据 ops.set(“hello”, “world”); // 取数据 String s = ops.get(“hello”); }

  1. - 统一使用JSON作为value进行存储
  2. - [Win系统Redis客户端Lettuce出现内存泄漏问题](https://www.bilibili.com/video/BV1np4y1C7Yf?p=154)
  3. <a name="VfETF"></a>
  4. # 缓存失效问题
  5. **缓存穿透**
  6. - 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
  7. - 在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
  8. - 解决: **缓存空结果**、并且设置短的过期时间。
  9. **缓存雪崩**
  10. - 缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DBDB 瞬时压力过重雪崩。
  11. - 解决:**原有的失效时间基础上增加一个随机值**,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
  12. **缓存击穿**
  13. - 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
  14. - 这个时候,需要考虑一个问题:如果这个key在大量请求同时进来前正好失效,那么所有对这个 key 的数据查询都落到 db,我们称为缓存击穿。
  15. - 解决:**加锁**
  16. <a name="mvJNB"></a>
  17. # Redis实现分布式锁
  18. ```java
  19. public void getDataWithReidsLock() {
  20. // 1. 占领分布式锁(加锁和设置过期时间要保证原子性)
  21. String UUID = UUID.randomUUID().toString();
  22. Boolen lock = redisTemplate.opsForValue().setIfAbsent("lock", UUID, 300, TimeUnit.SECONDS);
  23. // 相当于 set lock UUID EX 300 NX
  24. if(lock) {
  25. Map<String, List<Catelog2Vo>> dataFromDb;
  26. try {
  27. dataFromDb = getDataFromDb();
  28. System.out.println("获取数据");
  29. return;
  30. } finally {
  31. // 2. 解锁时也要保证原子性
  32. // 使用Lua脚本实现原子操作:获取值对比+对比成功删除=原子操作
  33. String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  34. Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
  35. }
  36. } else {
  37. // 获取锁失败,进行自旋
  38. Thread.sleep(100);
  39. getDataWithRedisLock();
  40. }
  41. }

Redisson

  • 配置Redisson客户端

    @Configuration
    public class MyRedissonConfig{
      @Bean
      public RedissonClient redisson() throws IOExcepton{
          // 默认连接地址 127.0.0.1:6379 RedissonClient redisson = Redisson.create();
          Config config = new Config();
          config.useSingleServer().setAddress("redis://192.168.56.10:6379"); 
          RedissonClient redisson = Redisson.create(config);
      }
    }
    
  • 使用分布式锁 ```java // 获取分布式锁,只要锁的名称一样,就是同一把锁 RLock lock = redisson.getLock(“anyLock”);

// 最常见的使用方法(上锁失败时进行阻塞等待),默认加锁30s lock.lock();

// 加锁以后 10 秒钟自动解锁,无需调用 unlock 方法手动解锁;但锁不会自动续期 lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待 100 秒,上锁以后 10 秒自动解锁 boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);

if (res) { try { // … } finally { lock.unlock(); } }


- Redisson提供的RLock实现了Lock接口

- 不指定过期时间时默认设置key的有效时间为30s
- 锁可以自动续期:业务执行时间超长时,运行期间会自动续期,每次续期30s

**读写锁**

- Redisson提供了读写锁`redisson.getReadWriteLock()`
- 保证一定可以读到最新数据,写锁释放前读锁必须等待
```java
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
// 加上读锁
RLock rlock = lock.readLock();
rlock.lock();
  • 写锁互斥,读锁共享
    • 写-写、写-读、读-写都是互斥操作
    • 只有写-写模式可以共享

信号量 Semaphore

  • 使用信号量可以进行限流
    // 获取信号量对象
    RSemaphore semaphore = redisson.getSemaphore("semaphore");
    // 获取信号,阻塞
    semaphore.aquire();
    // 尝试获取,不阻塞
    semaphore.tryAcquire();
    // 释放信号量
    semaphore.release();
    

闭锁 CountDownLatch

  • 等待所有任务都完成后再继续主线程 ```java RCountDownLatch latch = redisson.getCountDownLatch(“latch”);

// 设置等待任务数 latch.trySetCount(5); latch.await(); // 进行阻塞等待

latch.countDown(); // 计数-1 ```

缓存一致性

1、双写模式:写数据库 + 写缓存
image.png

  • 解决方案:

1、加锁,将写数据库和写缓存操作放在一起操作
2、如果对于数据的实效性不敏感,可以只设置好缓存中的数据过期时间,数据过期后自动查询最新数据

2、失效模式:写数据库 + 删缓存
image.png

  • 解决方案:

1、加锁,将写数据库和删缓存操作放在一起操作
2、如果对于数据的实效性不敏感,可以只设置好缓存中的数据过期时间,数据过期后自动查询最新数据
3、如果数据经常修改,则放弃缓存

一致性解决方案

image.png

1、过期时间
2、分布式锁
3、canal