Redis 八股文

什么是 Redis?

Redis 是一个高性能的 key-value 数据库,它是完全开源免费的,而且 Redis 是一个 NoSQL 类型的数据库,是为了解决高并发、高扩展和大数据存储等一系列问题而产生的数据库解决方案,是一个非关系型的数据库。

Redis 的特点?

Redis 本质上是一个 key-value 类型的内存数据库,很像 memcached,整个数据库加载在内存中进行操作,定期通过异步操作把数据库数据 flush 到磁盘上进行保存。因为是纯内存操作,所以 Redis 的性能非常出色,每秒可以处理超过 10 万次的读写操作,是已知性能最快的 key-value 型 db。

Redis 的出色之处不仅仅在于性能,它最大的魅力是支持保存多种数据结构,此外单个 value 的最大限制是 1GB,不像 memcached 那样,只能保存 1MB 的数据,因此 Redis 可以用来实现很多有用的功能,比如用 List 来做 FIFO 双向链表,实现一个轻量级的高性能消息队列服务;用 Set 来做高性能的 tag 系统等。另外 Redis 也可以对存入的 key-value 设置 expire 时间,因此也可以被当作一个功能加强版的 memcached 来用。

Redis 的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。

Redis 高并发和快速的原因?

  • Redis 是基于内存的,内存的读写速度非常快
  • Redis 是单线程的,省去了很多上下文切换线程的时间
  • Redis 使用了多路复用技术,可以处理并发的连接。非阻塞 I/O,内部实现上采用 epoll + 自己实现的简单的事件框架。epoll 中的读、写、关闭以及连接都转化成了事件,然后利用 epoll 的多路复用特性,绝不在 I/O 上浪费一点时间

为什么 Redis 是单线程的?

官方答案

因为 Redis 是基于内存的,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那么就顺理成章地采用单线程的方案了。

性能指标

关于 Redis 的性能,普通笔记本能轻松处理每秒几十万次的请求。

详细原因

  • 不需要各种锁的性能消耗。Redis 的数据结构并不全是简单的 key-value,还有 list、hash 等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在 hash 当中添加或者删除一个对象,这些操作可能就需要加非常多的锁,导致大大增加同步开销。总之,在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁、释放锁的操作,没有因为可能出现死锁而导致的性能消耗
  • 单线程多进程的集群方案。单线程的威力实际上非常强大,每核心的效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,在这些方案中,多线程的技术照样是用不上的。所以单线程、多进程的集群不失为是一个时髦的解决方案
  • CPU 消耗。采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程的切换而导致消耗 CPU。但是如果 CPU 成为 Redis 的瓶颈,或者不想让服务器其他的 CPU 核心闲置,这时我们可以考虑多起几个 Redis 进程,因为 Redis 是 key-value 数据库,而不是关系数据库,数据之间没有约束,所以只要客户端分得清哪些 key 放在哪个 Redis 进程上就可以了

Redis 单线程的优劣势?

单进程单线程的优势

  • 代码更清晰,处理逻辑更简单
  • 不用去考虑各种锁的问题,不存在加锁、释放锁的操作,没有因为可能出现死锁而导致的性能消耗
  • 不存在多进程或者多线程的切换而导致消耗 CPU

单进程单线程的弊端

无法发挥多核 CPU 的性能,不过可以通过在单机多开几个 Redis 实例来完善。

Redis 高并发的总结

  • Redis 是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在 I/O 上,所以读取速度快
  • Redis 使用的是非阻塞 I/O,I/O 多路复用,使用单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争
  • Redis 采用单线程的模型,保证每个操作的原子性,也减少了线程的上下文切换和竞争
  • Redis 全程使用 hash 结构,读取速度快,同时还有一些特殊的数据结构,对数据存储进行了优化,比如对短数据进行压缩存储的压缩表、使用有序数据结构加快读取速度的跳表
  • Redis 采用自己实现的事件分离器,效率较高,内部采用非阻塞的执行方式,吞吐能力比较大

什么是 I/O 多路复用技术?

Redis 采用网络 I/O 多路复用技术来保证在多连接的时候系统的高吞吐量。

“多路”指的是多个 Socket 连接(网络连接),“复用”指的是复用一个线程。

多路复用主要有三种技术:select、poll、epoll。其中 epoll 是最新的也是目前最好的多路复用技术。

采用 I/O 多路复用技术可以让单个线程高效地处理多个连接请求(尽量减少网络 I/O 的时间消耗),而且 Redis 在内存中操作数据的速度非常快(内存中的操作不会成为这里的性能瓶颈),以上两点造就了 Redis 具有很高的吞吐量。

如何设置有效时间?

可以对一个已经带有有效时间的 key 执行 expire 命令,新指定的生存时间会取代旧的生存时间。过期时间的精度控制在 1ms 以内,主键失效的时间复杂度是 O(1)。

expire 和 ttl 命令搭配使用,ttl 可以查看 key 的当前生存时间。expire 设置成功时返回 1,当 key 不存在或者不能为 key 设置生存时间时返回 0。

在 Redis 中,允许用户设置最大使用的内存大小(server.maxmemory),默认值为 0,即没有指定最大缓存。如果有新的数据添加,超过最大内存时会使 Redis 崩溃,所以一定要设置。

Redis 的内存数据集的大小上升到一定程度后,就会实行数据淘汰策略。

Redis 提供了 6 种数据淘汰策略(这里还应该补充基于 lfu 算法的两种策略):

  • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中挑选任意数据淘汰
  • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
  • allkeys-random:从数据集(server.db[i].dict)中挑选任意数据淘汰
  • no-enviction:禁止淘汰数据

volatile 和 allkeys 规定了是对“已设置过期时间的数据集”淘汰数据还是从“全部数据集”淘汰数据,后面的 lru、ttl 以及 random 是三种不同的淘汰策略,再加上一种 no-enviction 永不回收的策略。

使用策略的规则

如果数据呈现幂律分布,也就是一部分数据的访问频率高,一部分数据的访问频率低,则使用 allkeys-lru;如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用 allkeys-random。

三种数据淘汰策略

ttl 和 random 比较容易理解,实现也会比较简单。lru 最近最少使用淘汰策略,设计上会对 key 按有效时间排序,然后取最先失效的 key 进行淘汰。

为什么 Redis 需要把所有的数据都放到内存中?

Redis 为了达到最快的读写速度,将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以 Redis 具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘 I/O 速度就会严重影响 Redis 的性能。

此外,如果设置了最大使用的内存,则数据已有记录数在达到内存限值后将不能再继续插入新值。

假如 Redis 里有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知前缀开头,如何将它们全部找出来?

使用 keys 指令可以扫出指定模式的 key 列表。

如果 Redis 正在给线上的业务提供服务,那使用 keys 指令会带来什么问题?

因为 Redis 是单线程的,keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞地提取出指定模式的 key 列表,但是会有一定的重复概率,需要在客户端做一次去重,整体所花费的时间会比直接用 keys 指令更长。

如果有大量的 key 需要设置同一时间过期,一般需要注意什么?

如果大量的 key 的过期时间设置得过于集中,等到过期的那个时间节点,Redis 可能会出现短暂的卡顿现象。一般需要在过期时间上加一个随机值,使得过期时间分散一些。

Redis 如何做持久化?

BGSAVE 做镜像全量持久化,AOF 做增量持久化。

因为 BGSAVE 会耗费较长的时间,不够实时,在停机的时候会导致丢失大量的数据,所以需要 AOF 来配合使用。在 Redis 实例重启时,会使用 BGSAVE 持久化文件重新构建内存,再使用 AOF 重放近期的操作指令来实现完整恢复至重启之前的状态。

机器突然断电时取决于 AOF 日志的 sync 属性配置,如果不要求性能,每条写指令时都 sync 一下磁盘,就不会丢失数据。但是在高性能的要求下每次都 sync 是不现实的,一般都使用定时 sync,比如 1s/1次,这个时候最多就会丢失 1s 的数据。

BGSAVE 的原理是通过创建子进程来进行 BGSAVE 操作,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

pipeline 有什么好处?为什么要用 pipeline?

pipeline 可以将多次 I/O 往返的时间缩减为一次,前提是 pipeline 执行的指令之间没有因果相关性。

使用 redis-benchmark 进行压测的时候,可以发现影响 Redis QPS 峰值的一个重要因素是 pipeline 批次指令的数目。

redis-benchmark -t set, get -n 100000 -q

管道(pipeline)可以一次性发送多条命令并且在执行完后一次性将结果返回,pipeline 通过减少客户端与 Redis 的通信次数来实现降低往返延时时间,而且 pipeline 的实现原理是队列,而队列是先进先出的,这样就保证了数据的顺序性。

  • pipelined.sync(),表示一次性异步发送到 Redis,不关注执行结果
  • pipeline.syncAndReturnAll(),返回执行过的命令所返回的 List 列表结果

Redis 的同步机制?

Redis 可以使用主从同步、从从同步。

  1. 第一次同步时,主节点做一次 BGSAVE 操作,同时将后续的修改操作记录到内存 buffer 中
  2. 待完成后,将 rdb 文件全量同步到复制节点,复制节点接收完成后将 rdb 镜像加载到内存
  3. 加载完成后,再通知主节点将期间内修改的操作记录同步到复制节点,复制节点进行重放就完成了同步过程

Lua 脚本

Redis 从 2.6 版本开始引入使用 Lua 编程语言进行服务端脚本编程功能,这个功能可以让用户直接在 Redis 内部执行各种操作,从而达到简化代码并提高性能的作用。

通过使用 Lua 对 Redis 进行脚本编程,可以避免一些减慢开发速度或者导致性能下降的常见陷阱。

  • SCRIPT LOAD:可以将脚本载入 Redis,这个命令接收一个字符串格式的 Lua 脚本为参数,它会把脚本存储起来等待之后使用,然后返回被存储脚本的 SHA1 校验和
  • EVALSHA:可以调用之前存储的脚本,这个命令接收脚本的 SHA1 校验和以及脚本所需的全部参数
  • EVAL:可以直接执行指定的脚本,这个命令接收脚本字符串以及脚本所需的全部参数。这个命令除了会执行脚本之外,还会将被执行的脚本缓存到 Redis 服务器里

Lua 脚本和单个 Redis 命令以及 multi/exec 事务一样,都是原子性操作。

此外,已经对结构进行了修改的 Lua 脚本将无法被中断。

  • 不执行任何写命令的只读脚本:可以在脚本的运行时间超过 lua-time-limit 选项指定的时间之后,执行 script kill 命令杀死正在运行的脚本
  • 有写命令的脚本:杀死脚本将导致 Redis 存储的数据进入不一致的状态

Redis 持久化选项

Redis 提供了两种不同的持久化方法来将数据存储到磁盘里。

  • RDB(redis database):快照,可以将某一时刻的所有数据都写入磁盘(保存的是数据本身)
  • AOF(append only file):只追加文件,会在执行命令时,将被执行的写命令复制到磁盘(保存的是数据的变更记录)

两种持久化方法既可以同时使用,也可以单独使用,甚至在某些情况下可以两种方法都不使用,具体选择哪种持久化方法需要根据数据以及应用来决定。

将内存中的数据存储到磁盘的一个主要原因是为了在之后能够重用数据,或者是为了防止系统故障而将数据备份到一个远程位置。另外,存储在 Redis 里面的数据有可能是经过长时间计算得出的,或者是有程序正在使用 Redis 存储的数据进行计算。

快照持久化

Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间节点上的副本。

在创建快照之后,用户可以对快照进行备份,也可以将快照复制到其他服务器从而创建具有相同数据的服务器副本,还可以将快照留在原地以便重启服务器时使用。

如果在新的快照文件创建完成之前,Redis 、系统或者硬件任一崩溃,那么 Redis 将丢失最近一次成功创建快照之后写入的所有数据。所以快照持久化只适用于那些即使丢失了一部分数据也不会造成问题的程序,而不接受数据损失的程序可以考虑使用 AOF 持久化。

创建快照

  • 客户端可以通过向 Redis 发送 BGSAVE 命令来主动创建快照。对于支持 BGSAVE 命令的平台(除了 Windows 平台)来说,Redis 会调用 fork 来创建一个子进程,然后子进程负责将快照写入硬盘,而父进程则继续处理命令请求
  • 客户端可以通过向 Redis 发送 SAVE 命令来主动创建快照。接到 SAVE 命令的 Redis 服务器在快照创建完毕之前不会响应其他任何命令,所以通常只会在没有足够的内存去执行 BGSAVE 命令的情况下,又或者即使等待持久化操作执行完毕也无所谓的情况下,才会使用这个命令
  • 如果用户设置了 save 配置选项,比如 save 60 10000,那么从 Redis 最近一次创建快照之后开始算起,当“60 秒内有 10000 次写入”这个条件被满足时,Redis 就会自动触发 BGSAVE 命令。如果用户设置了多个 save 配置选项,那么当任意一个 save 配置选项所设置的条件被满足时,Redis 就会触发一次 BGSAVE 命令
  • 当 Redis 通过 SHUTDOWN 命令接收到关闭服务器的请求时,或者接收到标准 TERM 信号时,会执行 SAVE 命令,阻塞所有的客户端,不再执行客户端发送的任何命令,并且在 SAVE 命令执行完毕后关闭服务器
  • 当一个 Redis 服务器连接另一个 Redis 服务器,并向对方发送 SYNC 命令来开始一次复制操作的时候,如果主服务器目前没有在执行 BGSAVE 操作,或者并非刚刚执行完 BGSAVE 操作,那么主服务器就会执行 BGSAVE 命令

RDB 的优点

  • 非常紧凑
  • 适用于灾难恢复
  • 可以最大化 Redis 的性能
  • 在恢复大数据集时,速度比 AOF 要快

RDB 的缺点

  • 宕机时丢失的数据可能较多
  • 每次持久化时都要 fork 一个子进程,当数据集比较庞大时,可能非常耗时
# 持久化触发条件: seconds秒内至少有changes个键被更改
# save <seconds> <changes> 
# 默认配置了以下三个触发持久化的条件
# 【注】注释掉所有save的配置, 就不会开启快照持久化
# 900秒(15分钟)内至少有1个键被更改
save 900 1
# 300秒(5分钟)内至少有10个键被更改
save 300 10
# 60秒(1分钟)内至少有10000个键被更改
save 60 10000

# 快照持久化开启时
# yes: 后台持久化操作失败时, Redis就会停止接受更新操作(默认yes)
# no: 后台持久化操作失败时, Redis仍然可以继续正常工作
stop-writes-on-bgsave-error yes

# 在进行镜像备份时, 是否进行压缩
# yes: 压缩, 会有更多cpu消耗, 时间会更长(默认yes)
# no: 不压缩, 需要更多磁盘空间
rdbcompression yes

# 数据库文件的文件名
dbfilename dump.rdb

# 工作目录, 数据库文件的位置
# AOF(append only file)也会在此目录创建 
dir ./

AOF 持久化

简单来说,AOF 持久化会将被执行的写命令写到 AOF 文件的末尾,以此来记录数据发生的变化。因此只要从头到尾重新执行一次 AOF 文件里包含的所有写命令,就可以恢复 AOF 文件所记录的数据集。

重写 AOF 文件

由于 Redis 会不断地将被执行的写命令记录到 AOF 文件里,所以 AOF 文件会不断增大,甚至可能会用完磁盘的所有可用空间,重启之后的还原操作也可能会执行很长时间。

为了解决 AOF 文件不断增大的问题,可以向 Redis 发送 BGREWRITEAOF 命令,通过移除 AOF 文件中的冗余命令来重写 AOF 文件,使 AOF 文件变得尽可能小。

BGREWRITEAOF 的工作原理和 BGSAVE 创建快照时的工作原理非常相似,Redis 会创建一个子进程, 然后由子进程负责对 AOF 文件进行重写。

重写 AOF 文件是从数据集中读取键当前的值,然后用一条命令去记录键值对,代替之前记录的多个命令,最终产生一个新的 AOF 文件,这个文件里包含重建当前数据集所需的最少命令。

AOF 的优点

  • 文件只追加,所以写入时不需要进行 seek
  • 文件过大时,后台会自动进行重写
  • 有序地保存了数据库执行的所有写入性操作,易读懂,也能轻松分析文件

AOF 的缺点

  • 相同数据集时,AOF 文件的体积通常大于 RDB 文件
  • 根据所使用的 fsync 策略, AOF 的速度可能会慢于 RDB
  • 个别命令(例如 BRPOPLPUSH source destination timeout)会导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样
# AOF 持久化是否开启
# yes: 开启AOF持久化, Redis在启动时会载入AOF, 而忽略快照持久化文件
# no: 不开启AOF持久化(默认不开启)
appendonly no

# AOF文件名(默认为appendonly.aof)
appendfilename "appendonly.aof"

# Redis支持三种不同的回写模式
# always: 每次写操作都调用fsync(), 立刻写入AOF文件, 非常慢但是很安全
# appendfsync always
# everysec: 每秒调用一次fsync(), 折中方案(默认为everysec)
appendfsync everysec
# no: 不调用fsync(), 等待操作系统刷数据, 很快
# appendfsync no

# 如果有延迟问题就将该选项设置为yes, 否则就保持no, 这是最安全的方式
no-appendfsync-on-rewrite no

# 自动重写AOF文件, 若百分比设置为0, 则表示禁用自动重写
# 如果当前AOF文件大小比上次AOF文件的大小大了指定的百分比, 则会重写AOF文件
auto-aof-rewrite-percentage 100
# 同时需要指定AOF文件重写的最小大小, 以避免因百分比过小而频繁重写
auto-aof-rewrite-min-size 64mb
  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值