Redis学习笔记

文章目录

  • 1. 什么是 Redis?
  • 2. Redis 可以用来干什么?
  • 3. Redis 有哪些数据结构?
  • 4. Redis 为什么快呢?
  • 5. 能说一下 I/O 多路复用吗?
  • 6. Redis 6.0 之前为什么使用单线程?
  • 7. Redis 6.0 之后为何引入了多线程?
  • 8. Redis 持久化方式有哪些?有什么区别?
    • 8.1 RDB
      • 8.1.1 执行时机
      • 8.1.2 RDB 缺点
    • 8.2 AOF
      • 8.2.2 AOF 配置
      • 8.2.3 AOF 文件重写
  • 9. 如何选择 RDB 和 AOF?
  • 10. Redis 如何保证高并发和高可用?
    • 10.1 主从集群
      • 10.1.1 主从数据同步的流程
    • 10.2 哨兵机制
      • 10.2.1 哨兵机制的结构和作用
      • 10.2.2 集群监控原理
      • 10.2.3 集群故障恢复原理
    • 10.3 分片集群
  • 11. 什么是缓存穿透?
  • 12. 什么是缓存雪崩?
  • 13. 什么是缓存击穿?
  • 14. 如何保证缓存和数据库数据的一致性?
  • 15. Redis 的 key 过期之后,会立即删除吗?(Redis 数据过期策略)
  • 16. Redis 内存不足怎么办?(Redis 内存淘汰策略)
  • 17. 什么是大 Key?
  • 18. Redis 分布式锁
    • 18.1 Redis 分布式锁在项目中如何实现?
    • 18.2 如何控制 Redis 实现分布式锁有效时长呢?
    • 18.3 Redisson 实现的分布式锁是可重入的吗?
    • 18.4 Redisson 实现的分布式锁能解决主从数据一致性的问题吗?

1. 什么是 Redis?

  • Redis 是一种基于键值对(key-value)的数据库。为了满足不同的业务场景,Redis 中的 value 支持多种不同的数据结构,比如 String、Hash、List、Set、SortedSet、Bitmap、HyperLogLog、GEO 等数据结构。
  • Redis 是单线程执行命令,每个命令串行执行,一个命令在执行,其他命令不会中途插进来,线程是安全的。
  • Redis 是基于内存的,速度快。因为 Redis 会将数据都存在内存里,不像 MySQL 那样将数据都往磁盘里写。内存的读写速度相对于磁盘快很多。
  • 虽然 Redis 基于内存,查询性能高,但是存在数据不安全的情况,一旦断电,数据就消失了。为了解决这个问题,Redis 支持数据持久化,定期将数据从内存持久化到磁盘,从而确保数据的安全性。
  • Redis 支持主从集群(主节点负责写,从节点负责读,读写分离,提高查询效率)和 分片集群(将数据拆分,比如有 1TB 的数据拆成 n 份存到不同的节点上去,用很多台机器一起来存,存储的上限就提高了,实现水平的扩展。)
  • 总之,Redis 功能很强大。

2. Redis 可以用来干什么?

    1. 缓存:Redis 应用最广泛的就是用来作为缓存,降低数据库的压力,提高响应速度。
    1. 分布式锁:分布式系统或集群模式下,利用 Redis 实现分布式锁。
    1. 消息队列:Redis 的 Stream 数据结构适合用来做消息队列。
    1. 限流:一般是通过 Redis + Lua 脚本的方式来实现限流。
    1. 复杂业务场景:通过 Redis 提供的数据结构,我们可以很方便地完成很多复杂的业务场景。比如通过 Bitmap 统计活跃用户,通过 SortedSet 实现排行榜。

3. Redis 有哪些数据结构?

Redis学习笔记_第1张图片
Redis 有五种基本数据结构。

  • String (字符串类型,是 Redis 中最简单的存储类型,可以用 String 存登录时的验证码)
  • Hash (Hash 类型的 value 类似于 HashMap 结构,可以用 Hash 存储用户信息)
  • List (保存一些对顺序有要求的数据,可以用 List 实现文章列表、消息队列)
  • Set(Set 集合的特点是无序不可重复,可以用 Set 实现共同关注)
  • SortedSet (SortedSet 是可排序的集合,可以用 SortedSet 来实现排行榜)

4. Redis 为什么快呢?

Redis 速度快的原因主要有几点:

    1. Redis 是基于内存的,内存的读写速度相对于磁盘快很多
    1. Redis 执行命令是单线程的,避免了多线程的上下文切换带来的时间消耗
    1. Redis 使用基于 IO 多路复用的事件处理模型,可以同时处理多个 IO 请求。
    1. Redis 对数据结构进行了优化,性能非常高。

5. 能说一下 I/O 多路复用吗?

I/O 多路复用是指一个进程或者线程可以同时处理多个 IO 请求,是一种非阻塞的 IO 模型。

  • 阻塞 IO 模型是指:按顺序挨个处理每个 Socket

举个例子

假设我是奶茶店的店员,顾客们排队买奶茶。

阻塞 IO 模型:按顺序逐个点餐,先给 A 点,然后是 B、C、D … 这中间如果有一个人卡住,后面排队的人都会被耽误。

非阻塞的 IO 模型:谁先想好要点什么谁先说。这时 C、D 先说,表示他们想好要点什么了,然后我依次给 C、D 点单,然后继续等别的顾客。此时 E、A 又说要点什么,然后我去给 E、A 点单。

6. Redis 6.0 之前为什么使用单线程?

我觉得主要原因有 3 点:

    1. 单线程编程容易并且更容易维护;
    1. Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
    1. 多线程就会存在死锁、线程上下文切换等问题,会影响性能。

7. Redis 6.0 之后为何引入了多线程?

  • Redis 6.0 引入多线程主要是为了用提高 IO 读写性能,但是 Redis 执行命令还是单线程的。
  • Redis 6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 的配置文件 redis.conf,设置 IO 线程数 > 1。
    Redis学习笔记_第2张图片

8. Redis 持久化方式有哪些?有什么区别?

Redis 持久化分为 RDB 和 AOF。

8.1 RDB

  • RDB 是 Redis 数据快照文件,将数据记录在磁盘中。当 Redis 重启后,从磁盘读取快照文件,恢复数据。

8.1.1 执行时机

RDB 持久化在四种情况下会执行:

    1. 执行 save 命令,主进程执行 RDB,由于 Redis 是单线程的,这时候其他命令被阻塞。
    1. 执行 bgsave 命令,fork 一个子进程执行 RDB,不会阻塞 Redis 主进程。
    1. Redis 停机时 会执行一次 save 命令。
    1. 触发 RDB 条件时
    • Redis 内部有触发 RDB 的机制,可以在 redis.conf 文件中找到,格式如下:

900秒内,如果至少有1个 key 被修改,则执行 bgsave , 如果是 save “” 则表示禁用RDB
save 900 1
save 300 10
save 60 10000

8.1.2 RDB 缺点

RDB 是每隔一段时间进行持久化,没法做到实时持久化。在这相隔的时间内如果 Redis 宕机,数据就会丢失。

8.2 AOF

  • AOF 是追加文件,会将每一个写命令追加到 AOF 文件中。当 Redis 重启后,从磁盘读取 AOF 文件,重新执行里面的命令,恢复数据。

8.2.2 AOF 配置

  • AOF 默认是关闭的,需要修改 redis.conf 配置文件来开启 AOF,可以通过 appendonly 参数开启。
# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"
  • AOF 命令记录的频率也可以通过 redis.conf 文件来配置,默认每秒刷盘一次。
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always 
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec 
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no

8.2.3 AOF 文件重写

随着命令越来越多,AOF 文件也会越来越大。为了解决这个问题,Redis 提供了 bgrewriteaof 命令,用最少的命令完成对 AOF 文件的重写。
在这里插入图片描述
如图,AOF 原本有三个命令,但是 set num 123 和 set num 666都是对 num 的操作,第二次会覆盖第一次的值,因此第一个命令记录下来没有意义。

所以重写命令后,AOF 文件内容就是:mset name jack num 666

9. 如何选择 RDB 和 AOF?

  • 如果可以接受一些数据的丢失,那么可以 只使用 RDB 持久化。
  • 不建议只使用 AOF,虽然 AOF 数据更完整,但是这时候 RDB 的作用就是用来进行数据备份,把这个 RDB 可以拷贝一份放到别的机房,这样相当于一个异地容灾
  • 如果想要保证数据的安全性,往往会同时结合 RDB 和 AOF。在这种情况下,Redis 重启时会先执行 AOF 文件恢复数据,因为 AOF 文件保存的数据会比 RDB 更完整。

10. Redis 如何保证高并发和高可用?

Redis 保证高并发和高可用主要有三种方式:搭建主从集群、哨兵机制、分片集群。

10.1 主从集群

单节点 Redis 的并发能力是有限的,为了提高 Redis 的并发能力,就需要搭建主从集群,实现读写分离。主节点负责写,并将数据同步给从节点,从节点负责读,一主多从,多个从节点一起分担读的压力,这样读并发能力就得到提升了。
Redis学习笔记_第3张图片

10.1.1 主从数据同步的流程

主从数据同步分为全量同步增量同步

  • 主从第一次建立连接时,会执行全量同步,将主节点的所有数据生成 RDB,拷贝给从节点。
  • 如果从节点重启后同步,会执行增量同步,只更新从节点与主节点存在差异的部分数据。

10.2 哨兵机制

Redis 提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。

10.2.1 哨兵机制的结构和作用

哨兵的结构:
Redis学习笔记_第4张图片

作用:

  • 搭建完主从集群后,Redis 提供了哨兵机制来 监控 整个集群,对集群进行 故障恢复 。如果主节点挂了,在从节点中重新选出一个作为主节点来保证集群可以正常运行。并且主从发生切换会 通知 Java 客户端,这样就知道新的主节点和新的从节点是谁了,这样就可以去修改节点访问的地址了。

10.2.2 集群监控原理

哨兵 通过 心跳机制 监测服务状态,每隔 1 秒 向集群的每个实例发送 ping 命令:

  • 如果超过一定时间没有响应则认为该实例是 主观下线
  • 如果超过一半数量的哨兵都认为该实例主观下线,就认为这个节点确实有问题,就认为是 客观下线
    Redis学习笔记_第5张图片

10.2.3 集群故障恢复原理

一旦发现 master 故障,哨兵 需要在 slave 中选择一个作为新的 master,选择依据是这样的:

  • 首先会判断从节点与主节点断开时间的长短,如果时间太长就排除
  • 然后判断从节点的优先级
  • 如果优先级一样,则选择偏移量最大的从节点,偏移量越大说明数据越新
  • 最后选择 runid 最小的从节点

当选出一个新的 master 后,该如何实现切换呢?

流程如下:

  • 首先选定一个 slave 作为新的 master,执行 slaveof no one
  • 然后让所有节点都执行 slaveof 新的 master
  • 修改故障节点的配置文件,添加 slaveof 新的 master

10.3 分片集群

  • 分片集群:主从和哨兵可以解决高可用、高并发读的问题。但是面对 海量数据存储问题高并发写 的问题,就需要 使用分片集群,多主多从。
    Redis学习笔记_第6张图片

11. 什么是缓存穿透?

缓存穿透 是指请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,这些请求都会直接打到数据库。

解决缓存穿透的目的是为了防止有人恶意攻击,如果知道请求的路径,不断发送这样的请求,就会造成缓存穿透。

常见的解决方案有两种:

  • 缓存空对象:请求的数据不存在,就把空值存到缓存里

    • 优点:实现起来比较简单,维护方便
    • 缺点:可能会发生数据不一致的问题,比如刚开始数据库里是没有这一条数据的,后面数据库里有这个数据了,但是缓存里还是 null,就造成了缓存与数据库数据不一致的问题。
  • 布隆过滤器:在客户端和 Redis 之间加了一层布隆过滤器,如果发送请求的数据在数据库里有,就放行去访问 Redis,不存在,就拦截,拒绝访问。

    • 优点:节约内存
    • 缺点:有可能会产生误判。误判原因在于:布隆过滤器走的是哈希思想,可能存在哈希冲突。

布隆过滤器原理

  • 布隆过滤器的底层是 BitMap(位图),用于判断一个元素是否在数组中。用 0 和 1 标识业务状态,没有就记为 0,有就记为 1。
  • 它的底层是先去初始化一个数组,里面存放的 0 或 1。在一开始都是 0,当一个 key 来了之后经过多次 hash 计算,对数组长度取模,找到数据的下标然后把数组中原来的 0 改为 1。这样的话,数组的位置对应的 0 和 1 就能表示一个 key 是否存在。

如何实现布隆过滤器?

Redission 提供了对布隆过滤器的实现,可以设置一个误判率,一般是 0.05,也就是 5% 的误判率。

12. 什么是缓存雪崩?

缓存雪崩 是指由于设置缓存时 不同的 key 采用了相同的过期时间,在同一时段大量的 key 同时失效或者 Redis 服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
1. 给不同的 key 的 TTL 添加随机值
2. 搭建 Redis 集群保证高可用

13. 什么是缓存击穿?

缓存击穿 问题也叫热点 Key 问题,就是一个被高并发访问并且缓存重建耗时长的 key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:

    1. 互斥锁:查询缓存未命中,获取互斥锁,获取到互斥锁的线程才能查询数据库重建缓存,将数据写入缓存中后,释放锁。
    1. 逻辑过期:

①:在设置 key 的时候,将逻辑时间存入缓存中,不给当前 key 设置过期时间

②:当查询的时候,从 Redis 取出数据后判断逻辑时间是否过期

③:如果过期则开启新线程进行数据同步,当前线程正常返回数据,会将过期的数据返回
Redis学习笔记_第7张图片
当然两种方案各有利弊:

如果需要数据的强一致性,建议使用锁的方案,但是性能没那么高,可能会产生死锁

如果需要性能比较高,则使用逻辑过期的方案,但是数据同步这块做不到强一致。

14. 如何保证缓存和数据库数据的一致性?

  • 选择合适的 缓存更新策略
内存淘汰 超时剔除 主动更新
不用自己维护,当内存不足时自动淘汰部分数据。下次查询时更新缓存。 给缓存数据添加 TTL 时间,到期后自动删除缓存。下次查询时更新缓存。 编写业务逻辑,先更新数据库,再删缓存

业务场景

  • 低一致性需求:使用 Redis 自带的内存淘汰机制。
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案。

具体例子
项目中 ShopController 中给查询商铺的缓存添加超时剔除主动更新的策略

  • 查询数据时:根据 id 查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
  • 修改数据时:根据 id 修改店铺时,先更新数据库,再删缓存,通过事务保证原子性。

更新商铺时,保证数据库和缓存的一致性

@Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if(id == null){
            return Result.fail("店铺id不能为空");
        }
        // 1.更新数据库
        updateById(shop);
        // 2.删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
        return Result.ok();
    }

15. Redis 的 key 过期之后,会立即删除吗?(Redis 数据过期策略)

可以通过 expire 命令给 Redis 的 key 设置 TTL(存活时间):
Redis学习笔记_第8张图片
可以发现,当 key 的 TTL 到期以后,再次访问 name 返回的是 nil,说明这个 key 已经不存在了,对应的内存也得到释放。从而起到内存回收的目的。

这里有两个问题需要我们思考:

    1. Redis 如何知道一个 key 是否过期呢?

Redis 数据库中有两个字典分别记录 键值对 和 过期时间

    1. 是不是过期,key 就立即删除了呢?

不会立即删除。Redis 采用的过期数据的删除策略是 惰性删除定期删除

  • 惰性删除指的是每次访问(增删改查) key 时判断是否过期,如果过期就删除。

    • 缺点:如果这些过期的 key 没有被访问,那么就⼀直无法被删除,⼀直占用内存。
  • 定期删除指的是每隔一段时间,就对一些 key 进行检查,删除里面过期的 key。

  • Redis 的过期删除策略:惰性删除 + 定期删除 两种策略进行配合使用。

定期删除的两种模式: SLOW 模式 和 FAST模式

  • SLOW 模式 是定时任务,执行频率默认为 10hz,每次不超过 25ms
  • FAST 模式执行频率不固定,但两次间隔不低于 2ms,每次耗时不超过 1ms

16. Redis 内存不足怎么办?(Redis 内存淘汰策略)

Redis 提供了8种 内存淘汰策略 来选择要删除的 key,默认是 noeviction,不删除任何 key,内存不足时直接报错。

可以在 Redis 的配置文件中选择内存淘汰策略。最常使用的是 allkeys-lru,当内存不足时,删除最近最少使用的 key (用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高)。

17. 什么是大 Key?

如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。

如何找到大 Key?

使用 Redis 的 --bigkeys 命令来查找

如何处理 大 Key?

使用 UNLINK 命令删除大 Key

18. Redis 分布式锁

18.1 Redis 分布式锁在项目中如何实现?

  • 抢券业务用到了分布式锁,用 Redission 作为分布式锁,底层是 setnx 和 lua 脚本。
  • 分布式锁是满足分布式系统或集群模式下多进程可见并且互斥的锁。
  • 分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程。
    Redis学习笔记_第9张图片
    实现分布式锁时需要实现两个方法:获取锁和释放锁
  • 获取锁:利用 setnx 命令获取锁
  • 释放锁:
    • 手动释放:利用 del 命令 直接删除
    • 超时释放:获取锁时通过 expire 命令添加超时时间,避免服务宕机

18.2 如何控制 Redis 实现分布式锁有效时长呢?

Redis学习笔记_第10张图片
在 Redisson 中,提供了 WatchDog 看门狗机制,一个线程获取锁成功以后,WatchDog 会给持有锁的线程 续期(默认是每隔10秒续期一次),就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要释放锁就可以了。

18.3 Redisson 实现的分布式锁是可重入的吗?

  • 是可重入的。是不是可以重入就是判断是不是同一个线程。每个线程都有一个线程 id 作为唯一标识。
  • 这样做是为了避免死锁的产生。在存储数据的时候采用的是 Redis 的 hash 结构记录线程 id 重入次数,其中 key 是当前线程的唯一标识,value 是当前线程重入的次数
  • 这个重入其实就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数 +1,如果释放锁就会在计数上减一。
    Redis学习笔记_第11张图片

18.4 Redisson 实现的分布式锁能解决主从数据一致性的问题吗?

不能。企业中一般会搭建 Redis 主从集群架构,为了分担读的压力,Redis 通过主从集群架构,实现读写分离,主节点负责写,并将数据同步给其他从节点,从节点负责读,从而实现高并发。假如主节点还没来得及写,主节点挂了,Redis 提供的哨兵模式,会在从节点中选出新的主节点。新的线程也会尝试获取锁,因为之前数据没有同步过来,新的线程也会加锁成功。这时候就出现了 2 个线程同时持有一把锁的问题,如果业务还在执行,可能就会出现脏数据的现象。

如果业务非要保证数据的强一致性,该怎么解决呢?

如果有强一致性要求高的业务,建议使用 zookeeper 实现的分布式锁

你可能感兴趣的:(redis,学习)