Redis

一、使用场景

1、缓存穿透

查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,导致每次请求都会查询数据库

1.1 解决方法

  1. 缓存空数据,查询返回的数据为空,仍把这个数据缓存

    1.1 缺点:消耗内存,可能会发生不一致的问题

  2. 布隆过滤器

    image

    主要是通过数组来判断的,但是会出现误判

    2.1 缺点: 实现复杂,存在误判

2、缓存击穿

当给某个key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把数据库压垮

image

2.1 解决方法

  1. 互斥锁

    image

    强一致,性能差

  2. 逻辑过期

    image

    高可用,性能优,不能保证数据的绝对一致

3 缓存雪崩

指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量的请求到达数据库,带来巨大的压力

image

3.1 解决方法

  1. 给不同的key的TTL添加随机值
  2. 利用Redis集群提高服务的可用性 如:哨兵模式,集群模式
  3. 给缓存业务添加降级限流策略 如:nigix或者springcloud gateway
  4. 给业务添加多级缓存

4 redis作为缓存,mysql的数据如何与redis进行同步?(双写一致性)

4.1 双写一致性

当修改了数据库中的数据也要同时更新缓存中的数据,缓存中的数据要和数据库中的数据保存一致

image

  1. 读操作:缓存命中,直接返回;缓存未命中查询数据库,然后写入缓存,设置超时时间

  2. 写操作:延迟双删

    image

    应该是先删除缓存还是先删除数据库?

    1. 先删除缓存,在操作数据库

      ​​image

      会出现脏数据的现象

    2. 先操作数据库,在删除缓存

      image

      也会出现问题

    3. 可以使用分布式锁实现

      image

    4. image

    5. 最好的方式

      使用mq实现

      image

5 redis 作为缓存,数据的持久化是怎么做的?

5.1 RDB

RDB 全称 Redis Database Backup file ( Redis 数据备份文件),也被叫做 Redis 数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当 Redis 实例故障重启后,从磁盘读取快照文件,恢复数据

5.1.1 RDB的执行原理

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RGB文件。

image

5.2 AOF

image

image

6 假如redis的key过期后,会立即删除吗?

6.1 惰性删除

置该key过期时间后,我们不需要去管他,当需要该key时,我们再检查其是否过期,如果过期,我们就删除,反之返回该key

6.2 定期删除

每隔一段时间,我们就对key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)

两种模式:

  1. Slow模式是定时任务,执行频率默认为 10hz ,每次不超过 25ms ,以通过修改配置文件 redis.conf 的 hz 选项来调整这个次数
  2. FAST 模式执行频率不固定,但两次间隔不低于 2ms ,每次耗时不超过 1ms

7 假如缓存过多,内存是有限的,内存被占满了怎么办?(数据淘汰策略)

数据的淘汰策略:当 Redis 中的内存不够用时,此时在向 Redis 中添加新的 key ,那么 Redis 就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。

Redis支持8种不同的策略来选择要删除的key:

  1. noeviction : 不淘汰任何 key ,但是内存满时不允许写入新数据,默认就是这种策略
  2. volatile-ttl : 对设置了 TTL 的 key ,比较 key 的剩余 TTL 值, TTL 越小越先被淘汰
  3. allkeys-random :对全体 key ,随机进行淘汰。
  4. volatile-random :对设置了 TTL 的 key ,随机进行淘汰。
  5. allkeys-lru : 对全体 key ,基于 LRU 算法进行淘汰。LRU 最近最少使用,LFU:最少频率使用
  6. volatile-lru : 对设置了 TTL 的 key ,基于 LRU 算法进行淘汰
  7. allkeys-lfu : 对全体 key ,基于 LFU 算法进行淘汰
  8. volatile-lfu : 对设置了 TTL 的 key ,基于 LFU 算法进行淘汰

使用策略

  1. 优先使用 allkeys-lru 策略。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。
  2. 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用allkeys-random ,随机选择淘汰。
  3. 如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。
  4. 如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略。

image

8 redis 分布式锁,是如何实现的?(场景:集群情况下的定时任务,抢单,幂等性)

image

redisson实现的分布式锁可重入

主从一致性

使用红锁:不能只在一个redis实例上创建锁,应该在多个redis实例上进行创建锁

9 Redis集群有哪些方案?

在redis中提供的集群方案总共有三种

  1. 主从复制
  2. 哨兵模式
  3. 分片集群

9.1 主从复制

​![image](images/Redis/image-20230809153853-mqeh8xp.png)​

image

9.2 哨兵模式

redis提供了哨兵机制来实现主从集群的自动故障恢复,主要作用:

  1. 监控:哨兵会不断检查master和slave是否按预期工作

  2. 自动故障恢复:如果master故障,哨兵会将一个slave提升为master。当故障实例恢复后也以新的master为主

  3. 通知:哨兵充当redis客户端的服务发现来源,当集群发生故障转移时,会将最新的信息推送给redis客户端

哨兵选主的规则

  1. 首先判断主与从节点断开时间长短,如超过指定值就排该从节点
  2. 然后判断从节点的 slave-priority 值,越小优先级越高
  3. 如果 slave-prority 一样,则判断 slave 节点的 offset 值,越大优先级越高
  4. 最后是判断 slave 节点的运行 id 大小,越小优先级越高。

redis集群(哨兵模式)脑裂

9.3 分片集群结构

image

10 其他问题

image

10.1 用户空间和内核空间

image

10.2 阻塞IO

阶段一:

  1. 用户进程尝试读取数据(比如网卡数据)
  2. 此时数据尚未到达,内核需要等待数据
  3. 此时用户进程也处于阻塞状态

阶段二:

  1. 数据到达并拷贝到内核缓冲去,代表已就绪
  2. 将内核数据拷贝到用户缓冲区
  3. 拷贝过程中,用户进程依然阻塞等待
  4. 拷贝完成,用户进程接触阻塞,处理数据

image

10.3 非阻塞IO

阶段一:

  1. 用户进程尝试读取数据(比如网卡数据)
  2. 此时数据尚未到达,内核需要等待数据
  3. 返回异常给用户
  4. 用户进程拿到error后,再次尝试读取
  5. 循环往复,知道数据就绪

阶段二:

  1. 将内核数据拷贝到用户缓冲区
  2. 拷贝过程中,用户进程依然阻塞等待
  3. 拷贝完成,用户进程接触阻塞,处理数据

image

10.4 IO多路复用

IO多路复用:是利用单个线程来同时监听多个 Socket ,并在某个 Socket 可读、可写时得到通知,从而避免无效的等待,充分利用 CPU 资源。

阶段一:

  1. 用户进程调用 select ,指定要监听的 Socket 集合
  2. 内核监听对应的多个 socket
  3. 任意一个或多个 socket 数据就绪则返回 readable
  4. 此过程中用户进程阻塞

阶段二:

  1. 用户进程找到就绪的 socket
  2. 依次调用 recvfrom 读取数据
  3. 内核将数据拷贝到用户空间
  4. 用户进程处理数据

image

End SpringBoot整合Redis

1、 配置类

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

2、springboot 缓存注解说明

2.1 缓存@Cacheable

根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不 存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。
属性/方法名 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key

2.2 缓存@CachePut

使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存 中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。
属性/方法名 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key

2.3 缓存@CacheEvict

使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上
属性/方法名 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key
allEntries 是否清空所有缓存,默认为 false。如果指定为 true,则方法调用 后将立即清空所有的缓存
beforeInvo cation 是否在方法执行前就清空,默认为 false。如果指定为 true,则在 方法执行前就会清空缓存

3 使用过程

在需要存储数据的方法上面添加@Cacheable注解,注解里面写入需要添加的key和value

image