Redis学习
写在前
8月份看了《Redis开发与运维》、《Redis设计与实现》这两本书,看完之后,除了在书上画一画知识点、做一做笔记,没有其他输出。于是我想着在网上找一些Redis面试题,通过回答这些面试题来回顾、巩固书中的内容,并记录下来。
什么是Redis
Redis ,全称 Remote Dictionary Server ,是一个基于内存的高性能 Key-Value 数据库。
Redis的优缺点
Redis的优点
- 速度快 (官方给出的数字是读写性能可以达到10万/秒,当然这也取决于机器的性能。Redis如此快,可以大致归纳为以下四点:1) 数据存放在内存中,这是Redis快的最主要原因。 2) Redis用C语言实现,一般来说C语言实现的程序"距离"操作系统更近,执行速度相对会更快。 3) 使用了单线程架构,预防了多线程可能产生的竞争问题。 4) 作者对于Redis源代码可谓是精打细磨,曾经有人评论Redis是少有的集性能和优雅于一身的开源代码。)
- 支持丰富的数据结构 (常规的有字符串、哈希、列表、集合、有序集合,进阶的在后面会提到)
- 丰富的特性 (订阅发布 Pub / Sub 功能、Key 过期策略等等)
- 支持持久化存储 (RDB、AOF)
- 高可用 (Redis Sentinel,提供高可用方案,实现主从故障自动转移;Redis Cluster,提供集群方案,实现基于槽的分片方案,从而支持更大的 Redis 规模。)
Redis的缺点
- Redis是内存数据库,因此,单台机器存储的数据量受限于机器本身的内存大小。虽然Redis本身有Key过期策略,但还是需要提前预估和节约内存。如果内存增长过快,需要定期删除数据。
- 若进行完整重同步,由于需要生成RDB文件并进行传输,会占用主机的CPU,并会消耗线网的带宽。不过Redis从2.8版本开始,有了部分重同步 (使用PSYNC命令代替SYNC命令,PSYNC命令具有完整重同步和部分重同步两种模式) 的功能。但还是有可能进行完整重同步的,比如,新上线的备机。
- 修改配置文件后进行重启,将硬盘中的数据加载进内存所花的时间比较久。这个过程中,Redis不能提供服务。
Redis线程模型
ps:两本书好像都没怎么提及线程模型,这里参考一下芋道源码知识星球里面的相关内容。
Redis内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以Redis才叫做单线程的模型。它采用IO多路复用机制同时监听多个Socket,根据Socket上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包括4个部分:
- 多个Socket
- IO多路复用程序
- 文件事件分派器
- 事件处理器 (连接应答处理器、命令请求处理器、命令回复处理器)
多个Socket可能会并发产生不同的操作,每个操作对应不同的文件事件(file event),但是IO多路复用程序会监听多个socket,会将socket产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
客户端与redis的一次通信过程如下:
- 客户端Socket01向Redis的Server Socket请求建立连接,此时Server Socket会产生一个 AE_READABLE 事件,IO多路复用程序监听到server socket产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的Socket01,并将该Socket01的AE_READABLE事件与命令请求处理器关联。
- 假设此时客户端发送了一个 set key value 请求,此时Redis中的Socket01 会产生 AE_READABLE 事件,IO多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面的Socket01的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派处理器交给命令请求处理器来处理。命令请求处理器读取Socket01的 set key value 并在自己内存中完成 set key value 的设置。操作完成后,它会将Socket01的 AE_READABLE 事件与命令回复处理器关联。
- 如果此时客户端准备好接收返回结果了,那么Redis中的Socket01会产生一个AE_READABLE 事件,同样压入队列中,事件分派处理器找到相关联的命令回复处理器,由命令回复处理器对socket01输入本次操作的一个结果,比如ok,之后解除Socket01的 AE_READABLE 事件与命令回复处理器的关联。
Redis是单线程模型,为什么能高效
- C语言实现 (C语言的执行速度快)
- 纯内存操作 (如果不将数据放在内存中,磁盘I/O速度会严重影响Redis的性能。)
- 基于非阻塞的IO多路复用机制
- 单线程,避免了多线程的频繁上下文切换问题 (Redis利用队列技术,将并发访问变为串行访问,消除了传统数据库串行控制的开销)
- 丰富的数据结构
Redis的持久化方式
两种方式
- 【全量】RDB持久化,是指在指定的时间间隔内将内存中的数据集快照写入磁盘。实际操作过程是,fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。("RDB文件是一个经过压缩的二进制文件,由多个部分组成。" --- 《Redis设计与实现》p137 )
- 【增量】AOF持久化,以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,打开文件可以看到详细的操作记录。(被写入AOF文件的所有命令都是以Redis的命令请求协议格式保存的,因为Redis的命令请求协议是纯文本格式,所以我们可以直接打开一个AOF文件,观察里面的内容。)
RDB
RDB优缺点摘自《Redis开发与运维》一书的p156、p157
优点
- RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适用于备份,全量复制等场景。比如每6小时执行bgsave备份,并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复。
- Redis加载RDB恢复数据远远快于AOF的方式。
缺点
- RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。
- RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。
针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决。
AOF
关于AOF的优缺点,两本书上没有特意提到,此处还是参考芋道源码知识星球的内容。
优点
- 该机制可以带来更高的数据安全性,即数据持久型。Redis中提供了3种同步策略,即每秒同步(everysec,该同步操作由一个线程专门负责执行)、每执行一个命令同步(always)和不同步(no,何时同步由操作系统来定)。
- 事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。
- 而每执行一个命令同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。
- 不同步,无需多言。
- 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。
- 因为以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高。
- 另外,如果我们本次操作知识写入了一半数据就出现了系统崩溃问题,不必担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性问题。
- 如果AOF文件过大,Redis可以自动启用rewrite机制。即使出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件照常写入,当新的merge后的日志文件ready的时候,再交换新老日志文件即可。(AOF rewrite机制,和RDB一样,也需要fork出一次子进程,如果Redis内存比较大,可能会因为fork阻塞下主进程。)
- AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
缺点
- 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB在恢复大数据集时的速度比AOF的恢复速度要快。
- 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的。
- 以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复出一摸一样的数据出来。所以说,类似AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有bug。不过AOF就是为了避免rewrite过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。
如何选择
- 不要仅仅使用RDB,因为那样可能会导致你丢失很多数据。
- 也不要仅仅使用AOF,因为那样有两个问题。1.通过AOF做冷备,没有RDB做冷备的恢复速度快;2.RDB每次简单粗暴生成数据快照,更健壮,可以避免AOF这种复杂的备份和恢复机制的bug。
- Redis支持同时开启两种持久化方式。我们可以综合使用AOF和RDB两种持久化方式,用AOF来保证数据不丢失,作为数据恢复的第一选择;用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以用RDB来进行快速的数据恢复。
- 如果同时使用RDB和AOF两种持久化机制,那么在Redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整(更新频率更高)。==> 关于优先使用AOF这点,可参考《Redis设计与实现》p120
Redis的序列化协议(RESP)
这部分内容摘自《Redis开发与运维》p113 - 114
几乎所有的主流编程语言都有Redis的客户端,不考虑Redis非常流行的原因,如果站在技术的角度看原因还有两个:第一,客户端与服务端之间的通信协议是在TCP协议之上构建的。第二,Redis制定了RESP(REdis Serialization Protocol,Redis序列化协议)实现客户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易被人类识别。例如客户端发送一条set hello world命令给服务端,按照RESP的标准,客户端需要将其封装为如下格式(每行用\r\n分隔):
*3 ==> 参数数量为3
$3 ===> 参数字节数为3,下面的5 5 同理
SET
$5
hello
$5
world
上面只是格式化显示的结果,实际传输格式为:*3\r\n$3\r\nSET\r\n$5\r\hello\r\n$5\r\nworld\r\n
Redis过期键删除策略
摘自《Redis设计与实现》p107
数据库键的过期时间都保存在过期字典中,又知道了如何根据过期时间去判断一个键是否过期,现在剩下的问题是:如果一个键过期了,那么它什么时候会被删除呢?
这个问题有三种可能的答案,它们分别代表了三种不同的删除策略:
- 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
- 惰性删除:放任键过期不管,但是每次从键空间获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
在这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。
Redis内存溢出控制策略
摘自《Redis开发与运维》p206、207
当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制,Redis支持6种策略,如下所示:
- noevection:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息 (error) OOM command not allowed when used memory,此时Redis只响应读操作。
- volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。
- allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性。如果没有,回退到noeviction策略。
- allkeys-random:随机删除所有键,直到腾出足够空间为止。
- volatile-random:随机删除过期键,知道腾出足够空间为止。
- volatile-ttl:根据键值对象的ttl属性,删除最近将要过期的数据。如果没有,回退到noeviction策略。
Redis数据结构
基础的5种:
- 字符串 String
- 哈希 Hash
- 列表 List
- 集合 Set
- 有序集合 Sorted Set
进阶:
-
HyperLogLog
- Geo
- Bitmap
再进一步:(这一部分除了布隆过滤器,其他的我还真没怎么听到)
-
BloomFilter
-
RedisSearch
-
Redis-ML
-
JSON
Redis的使用场景
- 缓存
- 排行榜系统
- 计数器应用 (播放数、浏览数等)
- 社交网络 (赞、踩、粉丝、共同好友/喜好等)
- 消息队列系统 (和专业的消息队列比还不够强大,仅满足一些基本功能)
Redis事务
和其他众多数据库一样,Redis作为NoSQL数据库也同样提供了事务机制。在Redis中,MULTI / EXEC / DIACARD / WATCH这四个命令是实现事务的基石。
Redis中事务的实现特征:
- 在事务中的所有命令都会被串行化地顺序执行 (多个命令会被入队到事务队列中,按先进先出FIFO的顺序执行),事务执行期间,Redis不会再为其他客户端的请求提供任何服务,从而保证了事务中的所有命令都被原子地执行。
- 和关系型数据库的事务相比,在Redis事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行。
- 通过MULTI命令开启一个事务,可以理解为关系型数据库的 "BEGIN TRANSACTION" 语句。在该语句之后执行的命令,都被视为事务之内的操作,最后我们可以通过执行 EXEC / DISCARD命令来提交 / 回滚该事务内的所有操作。这两个Redis命令,可被视为等同于关系型数据库中的 COMMIT / ROLLBACK命令。
ACID
在Redis中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),但Redis运行在某种特定的持久化模式(AOF且appendfsync选项的指为always)下时,事务也具有耐久性(Durability)。
Redis Sentinel
Sentinel是Redis的高可用性解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
摘自《Redis设计与实现》p243、244
- Sentinel只是一个运行在特殊模式下的Redis服务器,它使用了和普通模式不同的命令表。
- Sentinel以每秒一次的频率向实例 (包括主服务器、从服务器、其他Sentinel) 发送PING命令,并根据实例对PING命令的回复来判断实例是否在线,当一个实例在指定的时长中连续向Sentinel发送无效回复时,Sentinel会将这个实例判断为主观下线。
- 当Sentinel将一个主服务器判断为主观下线时,它会向同样监视这个主服务器的其他Sentinel进行询问,看它们是否同意将这个主服务器已经进入主观下线状态。
- 当Sentinel收集到足够多的主观下线投票之后,它会将主服务器判断为客观下线,并发起一次针对主服务器的故障转移操作。
Redis Cluster
Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。
哈希槽 16384
Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点都可以处理0个或最多16384个槽。
当数据库中的16384个槽都有节点在处理时,集群处于上限状态 (ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态 (fail) 。