本文是思维导图笔记转的markdown,有需要原文件的小伙伴私
基础数据结构
-
string:动态字符串,内存结构类似于ArrayList,采用预分配冗余空间来减少内存的频繁分配。小于1M时成倍扩容,大于1M只会扩容1M,最大512M
-
list :链表结构,相当于LinkedList。列表元素较少时使用zipList(一块连续内存),当数据量比较多的时候才会改用quickList
-
hash:相当于HashMap,区别在于value只能是字符串、扩容时采用渐进式rehash。保存新老两个hash,查询时同时查询两个hash结构,后续定时任务和hash命令将旧的hash迁移到新的hash结构中。hash结构的存储消耗要高于单个字符串,到底使用hash还是字符串需权衡。
-
set:相当于HashSet,内部相当于一个字典,所有value都是NULL
-
zset:有序唯一集合,相当于SortedSet和HashMap的结合体。内部依靠跳跃列表实现
高级应用
位图
- 位图,也就是byte数组,可以用来计数大大节约了内存空间
HyperLogLog
- HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是 0.81%,只提供了pfadd和pfcount方法
布隆过滤器
- 可以理解为一个不怎么精确的set结构,判断是否不存在精确,判断是否存在它可能会误判。但是布隆过滤器也不是特别不精确,只要参数设置的合理,它的精确度可以控制的相对足够精确,只会有小小的误判概率。bf.reserve {key} {error_rate} {initial_size};initial_size过大会浪费内存,过小会影响准确率;error_rate越小需要的存储空间就越大。
限流
- 可以使用redis-cell模块(使用的是漏斗算法),或者是用令牌桶算法(保证原子性可以使用lua脚本)
Scan
- Redis的所有key存储在一个很大的字典中,Redis提供一个简单暴力的指令keys用来列出所有满足正则字符串规则的key,但是它存在两个缺点:1、没有offset、limit参数,一次性返回所有满足条件的key。2、keys算法是遍历算法O(n),执行时间过长会影响其他的指令。
- 1、时间复杂度也是O(n)但是它是通过游标分布进行的,不会阻塞线程。 2、提供limit参数,可以控制每次返回结果的最大条数,limit只是一个提示,返回结果可多可少(因为limit表示的是遍历的槽位数) 3、同keys一样,它也提供模式匹配功能 4、服务器不需要为游标保存状态,游标的唯一状态就是scan返回给客户端的游标整数 5、返回的结果可能会有重复,需要客户端去重 6、遍历过程中如果有数据修改,改动后的数据能不能遍历到是不确定的 7、单次返回的结果是空的并不意味遍历结束,而要看返回的游标值是否为零
原理
线程IO模型
-
为什么Redis单线程还那么快:所有数据都是在内存中;所有的数据结构都是精心设计的;采用IO多路复用模型。
-
指令队列
- Redis会将每个客户端socket都关联一个指令队列,客户端的指令通过队列来排队进行顺序处理
-
响应队列
- Redis同样也会为每个客户端socket关联一个响应队列,如果队列为空,那么意味着连接暂时处于空闲状态,不需要去获取写事件,也就是可以将客户端描述符从write_fds里面移出来。等到队列有数据了再将描述符放进去,避免select系统调用立即返回写事件结果发现没什么数据可以写。这种情况会飙高CPU。
-
定时任务
- Redis的定时任务会记录在一个最小堆的数据结构中,最快要执行的任务排在堆的最上方。在每个循环周期Redis都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,作为select系统调用的timeout参数,因为未来timeout时间内没有其他定时任务需要处理。
通信协议
- RESP(redis serialization protocol)是一种直观的文本协议,优势在于实现异常简单解析性能极好
- 将传输的数据结构分为5种最小单元,单元结束时统一加上回车换行符号\r\n。NULL用多行字符串表示长度为-1,空字符串用多行字符串表示长度为0。
1、单行字符串以+符号开头
2、多行字符串以$符号开头,后跟字符串长度
3、整数值以:符号开头,后跟整数的字符串形式
4、错误消息以-符号开头
5、数组以*号开头,后跟数组的长度
持久化
-
RDB快照
-
是一次全量备份,是内存数据的二进制序列化形式,在存储上非常紧凑
-
原理
- redis使用操作系统的多进程copy on write机制来实现快照持久化。redis在持久化时会fork出一个子进程,快照持久化完全交给子进程处理。子进程不会修改现有的内存数据,它只是对数据遍历读取然后序列化到磁盘中。父进程必须保持服务对内存数据修改,这个时候就会使用操作系统的COW机制来进行数据段页面的分离,数据段是由很多操作系统页面组合而成,当父进程对其中一个页面的数据修改时,会将被共享的页面复制一份分离出来,然后对复制的页面修改,这时子进程相应的页面是没有变化的,还是进程产生那一瞬间的快照
-
-
AOF日志
-
是连续的增量备份,记录的是内存数据修改的指令记录文本。
-
原理
- Redis会在收到客户端修改指令后先进行参数校验,如果没问题立即将该指令文本存储到AOF日志中,然后再执行指令,这样即使突发宕机已经存储到AOF的指令重放一下就可以恢复之前的状态。在长期运行过程中AOF日志会变得越来越大,重启时AOF指令重放时非常耗时,所以需要对AOF日志瘦身
-
AOF重写
- redis提供bgrewriteaof指令用于对AOF日志进行重写瘦身,其原理是开辟一个子进程对内存进行遍历转换成一系列redis操作指令,序列化到一个AOF日志文件中,序列化完毕之后再将操作期间发生的增量AOF追加到这个新的文件中,最后替换旧的AOF日志文件
-
fsync
- 当程序对AOF日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓冲中,然后内核会异步将脏数据刷回磁盘。存在AOF日志还没刷盘机器突然宕机,fsync函数可以将指定文件从内核刷盘,Redis提供三种策略:每个1S(可以配置)执行一次fsync操作;永不fsync让操作系统来决定合适刷盘;每个指令后执行一次fsync。
-
-
Redis4.0混合持久化
- 重启时使用RDB来恢复内存状态会丢失大量数据,使用AOF性能相对较差。4.0提供混合持久化,将RDB文件的内容和增量的AOF日志文件放在一起,这里的AOF不是全量的而是持久化开始到结束这段时间发生的增量AOF日志
管道
- 对管道来说连续的write操作根本没有耗时,之后的第一个read操作会等待一个网络请求的开销,然后所有的响应消息就都已经返回内核的读缓冲了,后续的read操作直接就可以从缓冲拿到结果。它并不是什么服务器的特性而是客户端通过改变读写的顺序带来的性能提升
事务
- redis的事务模型与关系型数据库不一样。multi/exec/discard分别表示事务开始/执行/丢弃,所有指令在收到exec之前不执行而是缓存在一个事务队列中,一旦服务器收到exec则开始执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。因为redis是单线程所以不用担心自己在执行队列的时候被其他指令打断,可以保证它们一次性执行,但是不能保证同时成功失败。为了减少网络传输开销可以配合pipeline使用。
PubSub
- 缺点:如果一个消费者都没有那么消息直接丢弃,消费者宕机之后重新连接断开连接期间的消息会丢失。PubSub的消息是不会持久化的。
内存回收
- redis并不总是可以立即将空闲内存归还给操作系统,因为操作系统回收内存是以页为单位,只要这个页上还有一个key那么它就不能被回收。虽然无法保证立即回收已经删除key的内存,但是它会重用那些尚未回收的空闲内存
主从同步
-
Redis的主从数据是异步同步的,保证最终一致性
-
增量同步
- 同步的是指令流,主节点会将对自己状态修改的指令记录在本地内存的buffer中,然后异步将buffer的指令同步到从节点,从节点一边执行同步的指令一边向主节点反馈同步位置。buffer是一个环形数组,如果数组内存满了就会从头开始覆盖前面的内容。
-
快照同步
- 快照同步是一个非常耗资源的操作,它首先需要在主库上进行一次bgsave将当前内存的数据全部快照到磁盘文件,然后将文件传输到从节点。从节点执行一次全量加载,加载之前先要将当前内存的数据清空,加载完成后通知主节点继续进行增量同步。整个快照同步的过程中主节点的buffer还在不停的往前移动,如果快照同步的时间过长或者复制buffer太小都会导致同步期间的增量指令在复制buffer中被覆盖,这样会导致快照同步完成后无法进行增量复制,然后会再次发起快照同步,极有可能陷入死循环。
-
无盘复制
- 主节点在进行快照同步时,会进行很重的文件IO操作,当系统正在进行AOF的fsync操作的时候如果发生快照同步,fsync将会被推迟,会严重影响主节点的服务效率。redis2.8.18开始支持主服务器直接通过socket将快照内容发送到从节点,主节点会一边遍历内存一边发送到从节点,从节点将收到的内容存储到磁盘文件中再进行一次性加载
过期策略
- redis会将每个设置了过期时间的key放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的key。除此之外它还会使用惰性策略来删除过期的key,所谓惰性策略就是在客户端访问这个key的时候,redis对key的过期时间进行检查,如果过期了就立即删除。定时删除是集中处理,惰性删除是零散处理。
- Redis默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的key,而是采用了一种简单的贪心策略。扫描时间的上限默认不会超过25ms。 1、从过期字典中随机20个key 2、删除这20个key中已经过期的key 3、如果过期的key比率超过1/4,那就重复步骤1
- 从库不会进行过期扫描,从库对过期的处理是被动的。主库在key到期时会在AOF文件里增加一条del指令,同步到所有的从库,从库通过执行这条del指令来删除过期的key
LRU
-
实际上不使用LRU算法,是因为需要消耗大量的额外的内存,需要对现有的数据结构进行较大的改造。近似LRU算法则很简单,在现有数据结构的基础上使用随机采样法来淘汰元素,能达到和LRU算法非常近似的效果。Redis为实现近似LRU算法,它给每个key增加了一个额外的小字段,这个字段的长度是24个bit,也就是最后一次被访问的时戳。
-
当Redis内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的swap。交换会让Redis的性能急剧下降。在生产环境中我们是不允许Redis出现交换行为的,为了限制最大使用内存,Redis提供了配置参数maxmemory来限制内存超出期望大小。
-
当实际内存超出maxmemory时,Redis提供了几种可选策略(maxmemory-policy)来让用户自己决定该如何腾出新的空间以继续提供读写服务
- noeviction:不会继续服务写请求(DEL请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。
- volatile-lru:尝试淘汰设置了过期时间的 key,最少使用的key优先被淘汰。没有设置过期时间的 key不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。
- volatile-ttl:跟上面一样,除了淘汰的策略不是 LRU,而是key的剩余寿命ttl的值,ttl越小越优先被淘汰。
- volatile-random:跟上面一样,不过淘汰的key 是过期key集合中随机的key。
- allkeys-lru:区别于volatile-lru,这个策略要淘汰的key对象是全体的key集合,而不只是过期的key集合。这意味着没有设置过期时间的key也会被淘汰。
- allkeys-random:跟上面一样,不过淘汰的策略是随机的key。
volatile-xxx:策略只会针对带过期时间的key进行淘汰,allkeys-xxx策略会对所有的key进行淘汰。如果你只是拿Redis做缓存,那应该使用allkeys-xxx,客户端写缓存时不必携带过期时间。如果你还想同时使用Redis的持久化功能,那就使用volatile-xxx策略,这样可以保留没有设置过期时间的key,它们是永久的key不会被LRU算法淘汰。
懒惰删除
- Redis内部实际上并不是只有一个主线程,它还有几个异步线程专门用来处理一些耗时的操作。
- 删除指令del会直接释放对象的内存,大部分情况下这个指令非常快,没有明显延迟。不过如果删除的key是一个非常大的对象,比如一个包含了千万元素的hash,那么删除操作就会导致单线程卡顿。Redis为了解决这个卡顿问题,在4.0版本引入了unlink指令,它能对删除操作进行懒处理,丢给后台线程来异步回收内存(如果key所占内存很小就会和del一样)。Redis 提供了flushdb和flushall指令,用来清空数据库,这也是极其缓慢的操作。Redis4.0同样给这两个指令也带来了异步化,在指令后面增加async参数。
集群
Sentinel
-
高可用方案,sentinel本身也是一个集群,它负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为主节点。客户端来连接集群时,会首先连接 sentinel,通过 sentinel 来查询主节点的地址,然后再去连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 sentinel 要地址,sentinel 会将最新的主节点地址告诉客户端。如此应用程序将无需重启即可自动完成节点切换。已经挂掉的主节点在恢复之后会成为新的主节点下的从节点
-
消息丢失问题
- 如果主从延迟特别大,那么丢失的数据就可能会特别 多。Sentinel 无法保证消息完全不丢失,但是也尽可能保证消息少丢失。它有两个选项可以 限制主从延迟过大。min-slaves-to-write n, min-slaves-max-lag m表示至少有n个节点进行正常复制(如果m s没有收到从节点的反馈意味着从节点同步不正常),否则停止对外服务
Cluster
-
它是去中心化的,集群有N个Redis节点组成, 每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。这N个节点相互连接组成一个对等的集群,它们之间通过一种特殊的二进制协议相互交互集群信息。Redis Cluster将所有数据划分为16384的 slots,每个节点负责其中一部分槽位。槽位的信息存储于每个节点中,它不需要另外的分布式存储来存储节点槽位信息。当Redis Cluster的客户端来连接集群时,它也会得到一份集群的槽位配置信息。这样当客户端要查找某个key时,可以直接定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息的校验调整。
-
槽位定位算法
- Cluster默认会对key值使用crc32算法进行hash得到一个整数值,然后用这个整数值对16384进行取模来得到具体槽位
-
跳转
- 当客户端向一个错误的节点发出来指令,该节点会发现指令的key所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址告诉客户端去连接这个节点获取
-
扩容
- 迁移工具 redis-trib首先会在源和目标节点设置好中间过渡状态,然后一次性获取源节点槽位的所有key列表(keysinslot指令,可以部分获取),再挨个key进行迁移。每个key的迁移过程是以原节点作为目标节点的「客户端」,原节点对当前的key执行dump指令得到序列化内容,然后通过「客户端」向目标节点发送指令restore携带序列化的内容作为参数,目标节点再进行反序列化就可以将内容恢复到目标节点的内存中,然后返回「客户端」OK,原节点「客户端」收到后再把当前节点的key删除掉就完成了单个key迁移的整个过 程。
- 注意这里的迁移过程是同步的,在目标节点执行 restore指令到原节点删除key之间,原节点的主线程会处于阻塞状态,直到key被成功删除。首先新旧两个节点对应的槽位都存在部分key数据。客户端先尝试访问旧节点,如果对应的数据还在旧节点里面,那么旧节点正常处理。如果对应的数据不在旧节点里面,那么有两种可能,要么该数据在新节点里,要么根本就不存在。旧节点不知道是哪种情况,所以它会向客户端返回一个-ASK targetNodeAddr的重定向指令。客户端收到这个重定向指令后,先去目标节点执行一个不带任何参数的asking指令,然后在目标节点再重新执行原先的操作指令。如果这个时候向目标节点发送该槽位的指令,节点是不认的,它会向客户端返回一个-MOVED重定向指令告诉它去源节点去执行。如此就会形成重定向循环
-
容错
- Redis Cluster可以为每个主节点设置若干个从节点,单主节点故障时集群会自动将其中某个从节点提升为主节点。如果某个主节点没有从节点,那么当它发生故障时,集群将完全处于不可用状态。不过Redis也提供了一个参数cluster-require-full-coverage可以允许部分节点故障,其它节点还可以继续提供对外访问。cluster-node-timeout表示当某个节点持续timeout的时间失联时,才可以认定该节点出现故障需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换 (数据的重新复制)。还有另外一个选项cluster-slave-validity-factor作为倍乘系数来放大这个超时时间来宽松容 错的紧急程度。如果这个系数为零,那么主从切换是不会抗拒网络抖动的。如果这个系数大于1,它就成了主从切换的松弛系数。
-
网络抖动
- Redis Cluster是去中心化的,一个节点认为某个节点失联了并不代表所有的节点都认为它失联了。所以集群还得经过一次协商的过程,只有当大多数节点都认定了某个节点失联了,集群才认为该节点需要进行主从切换来容错。采用Gossip协议来广播自己的状态以及自己对整个集群认知的改变。比如一个节点发现某个节点失联了PFail,它会将这条信息向整个集群广播,其它节点也就可以收到这点失联信息。如果一个节点收到了某个节点失联的数量PFail Count已经达到了集群的大多数,就可以标记该节点为确定下线状态 (Fail),然后向整个集群广播,强迫其它节点也接收该节点已经下线的事实,并立即对该失联节点进行主从切换。