Redis八股
Redis
为什么用 Redis 作为 MySQL 的缓存
Redis 具备「高性能」和「高并发」两种特性。
Redis 数据类型
String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)
String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
Hash 类型:缓存对象、购物车等。
Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
Zset 类型:排序场景,比如排行榜、电话和姓名排序等。
String 类型内部实现
String 类型的底层的数据结构实现主要是 SDS(简单动态字符串
SDS 不仅可以保存文本数据,还可以保存二进制数据
SDS 获取字符串长度的时间复杂度是 O(1)
Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出
List 类型内部实现
由双向链表或压缩列表实现
元素个数小于 512 个,Redis 会使用压缩列表
Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现
Hash 类型内部实现
Hash 类型的底层数据结构是由压缩列表或哈希表实现的
元素个数小于 512 使用压缩列表
Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了
Set 类型内部实现
哈希表或整数集合实现的:
小于512用整数集合
ZSet 类型内部实现
由压缩列表或跳表实现的
Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了
Redis 是单线程
Redis 是单线程的,
但是,Redis 程序并不是单线程的
Redis 在启动的时候,是会启动后台线程(BIO)
Redis 为「关闭文件、AOF 刷盘、释放内存」
Redis 单线程模式
首先,调用 epoll_create() 创建一个 epoll 对象和调用 socket() 创建一个服务端 socket
然后,调用 bind() 绑定端口和调用 listen() 监听该 socket;
然后,将调用 epoll_ctl() 将 listen socket 加入到 epoll,同时注册「连接事件」处理函数如果是连接事件到来,则会调用连接事件处理函数,该函数会做这些事情:调用 accpet 获取已连接的 socket -> 调用 epoll_ctl 将已连接的 socket 加入到 epoll -> 注册「读事件」处理函数;
如果是读事件到来,则会调用读事件处理函数,该函数会做这些事情:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送;
如果是写事件到来,则会调用写事件处理函数,该函数会做这些事情:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理
Redis 采用单线程为什么还这么快
Redis 的大部分操作都在内存中完成
Redis 采用单线程模型可以避免了多线程之间的竞争
I/O 多路复用机制+事件分派
Redis 6.0 之前为什么使用单线程
CPU 并不是制约 Redis 性能表现的瓶颈所在
Redis 持久化
AOF 日志,RDB 快照,混合持久化方式
为什么先执行命令,再把数据写入日志呢
避免额外的检查开销,不会阻塞当前写操作命令的执行
风险:
数据可能会丢失,可能阻塞其他操作
写操作->命令写入aof缓冲区->写入aof文件(还在内核缓冲区)->内核决定实机写入硬盘
AOF 写回策略有几种
Always,同步写
Everysec,每秒写入硬盘
No,交给系统决定
AOF 日志过大
触发AOF 重写机制,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件
重写 AOF 日志的过程
重写 AOF 过程是由后台子进程bgrewriteaof 来完成的
因为写时复制导致数据不一致,所以使用AOF 重写缓冲区,期间执行的命令会也写入该缓冲区
RDB 快照
save 和 bgsave
通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令
写时复制技术
执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个,此时如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响
如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据
一个切片集群共有 16384 个哈希槽
根据键值对的 key,按照 CRC16 算法 (opens new window)计算一个 16 bit 的值。
再用 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽
集群脑裂导致数据丢失
当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端,可以设置:
min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。
min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据
Redis 使用的过期删除策略
Redis 会把该 key 带上过期时间存储到一个过期字典
Redis 使用的过期删除策略是「惰性删除+定期删除」
定期,惰性删除策略的优缺点
定期删除:在规定时间内不断随机取20个key,看过期比例是否超过约定来删除或结束
Redis 持久化时,对过期键会如何处理的
RDB 文件生成阶段,过期的键「不会」被保存到新的 RDB 文件中
RDB 加载阶段,过期键「不会」被载入到主数据库中,会载入从数据库,但同步后就被删除了
AOF 文件写入阶段,会写入,删除时会加一条del
AOF 重写阶段,已过期的键不会被保存到重写后的 AOF 文件中
Redis 主从模式中,对过期键会如何处理
主库负责删过期,加aof,从库等同步
Redis 内存满了,会发生什么
内存淘汰机制
Redis 内存淘汰策略有哪些
不进行数据淘汰的策略,返回错误
在过期键中淘汰或全键中淘汰
随机淘汰,淘汰最早过期的,最少最近使用,最少使用
如何避免缓存雪崩、缓存击穿、缓存穿透
雪崩:打散过期,设置不过期
击穿:锁或不过期
穿透:设null,布隆过滤器,非法请求限制
大 key 会带来以下四种影响
客户端超时阻塞,引发网络阻塞,阻塞工作线程,集群内存分布不均
找到大 key ?
redis-cli --bigkeys 查找大key,只能返回每种类型中最大的那个 bigkey,无法得到大小排在前 N 位的 bigkey;对于集合类只统计个数,不统计大小
使用 SCAN 命令对数据库扫描,然后用 TYPE 命令获取返回的每一个 key 的类型。
删除大 key
分批次删除
异步删除
Redis 管道有什么用
使用管道技术可以解决多个命令执行时的网络等待
Redis 事务支持回滚吗
Redis 中并没有提供回滚机制
Redis 实现分布式锁
setnx
SET lock_key unique_value NX PX 10000
Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性
Redis 如何解决集群情况下分布式锁的可靠性
Redis 官方已经设计了一个分布式锁算法 Redlock(红锁)
Redlock 算法的基本思路,是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。