Fari

《Redis开发与运维》笔记

scan命令用于增量遍历keys,类似于keys命令,但是scan命令可以指定分页以达到增量遍历的目的,时间复杂度为O1 但是如果在scan过程中key数量发生了变化,则获取的结果将和实际有偏差

除了五种数据结构的存储,redis还提供了其他的功能

慢查询分析、pipeline、事务于Lua、Bitmaps、HyperLoglog、发布订阅、GEO file

慢查询

每次执行命令分4步:

  1. 客户端向redis服务端发送命令
  2. 命令排队
  3. 执行命令
  4. 返回结果 其中,慢查询分析只到前3步
Pipeline

批处理命令(如mget、mset)有效地节约了多命令执行时间,但大部分命令不支持批量操作,pipeline就是将多条命令打包一起发送,然后一起返回,降低了命令执行时间

事务

redis提供了简单的事务功能,将多个命令放在 multi 和 exec 之间即可,事务过程中的命令并没有真正执行 file

注:

  1. redis中事务不支持回滚,如果事务中某个命令出错,其前面的命令仍会执行。
  2. 如果在事务中某个事务中用到的key被其他客户端修改了,则不会执行该事务,类似于乐观锁 但是这些问题可以使用Lua脚本解决,即使用一段脚本来控制redis,且是原子操作
Bitmaps

Bitmaps可以实现对位操作 其本质就是一个字符串,例如 a 的二进制为1001,虽然Bitmaps存储的是 a,但可以对其二进制进行操作

setbit key 20 1 // 设置key的第20位的值为1(只能是01) getbit key 20 // 获取key的第20位的值

可以使用 bitop 命令对多个bitmaps做交并非异或操作

HyperLogLog

一种技术算法,实际类型位字符串 通过HyperLogLog可以利用极小的内存空间完成独立总数的统计(粗略统计),例如统计IP、Email的独立个数(类似于Set.length())

发布订阅

redis不对订阅消息持久化 file

  1. 发布消息

    publish channel msg

  2. 订阅消息

    subscribe channel [ channel… ] // 会阻塞

GEO

客户端通信协议

redis客户端与服务端之间使用RESP协议进行通信(相当于明文传输),该协议建立在TCP之上 该协议较为简单,例如客户端发送一条 set name hunt 给服务端,则其传输的数据为

*3  // 有三个参数
$3  // 第一个参数长度为3
SET 
$5 
hello 
$5 
world

redis的返回结果分为5种(在client-cli种操作之所以看不到这些信息,是因为redis自己处理了,可以使用telnet发送请求即可查看)

  1. 状态回复:响应的第一个字节为“+”,例如 +OK
  2. 错误回复:第一个字节为“-”
  3. 整数回复:第一个字节为“:”
  4. 字符串回复:第一个字节为“$”
  5. 多条字符串回复:第一个字节为“*”,例如 mget k1 k2

file

客户端管理

使用 client list 命令查看所有与redis服务端相连的客户端信息,例如ip,名称,连接时间,缓冲区大小等

输入缓冲区

redis服务端为每个连接的客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存。同时redis会从输入缓冲区拉取命令并执行。一个输入缓冲区最大不能超过1G,超过则会关闭客户端

输出缓冲区

redis服务端为每个客户端分配了输出缓冲区,用于保存命令执行结果。出问题概率低。

异常处理案例

graph LR
subgraph string
String --8字节长整型--> int --小于等于39字节--> embstr --大于39字节--> row
end

subgraph hash
Hash --key小于512个同时value均小于64字节--> ziplist1 --否则--> hashtable1
ziplist内部使用数组方式更省内存
end

subgraph set
Set --元素个数小于512个且都是整型--> intset --否则--> hashtable2
end

subgraph list
List --元素个数小于512个同时值均小于64字节--> ziplist2 --否则--> linkedlist
end

subgraph zset
Zset --元素个数小于128个且值均小于64字节--> ziplist3 --否则--> skiplist
end

主从结构

复制

参与复制的redis实例划分为主节点(master)和从节点(slave),默认清空下redis都是主节点。每个从节点只能有一个主节点,而每个主节点可以有多个从节点 复制数据流是单向的,只能从主节点复制到从节点

配置从节点方式
  1. 在配置文件中加入 slaveof masterHost masterPort
  2. 在redis-server启动命令后加入 --slaveof masterHost masterPort
  3. 直接使用命令 slaveof masterHost masterPort,即可以在运行期动态配置。如果在一个从节点中使用该命令,则会使其切换到新的从节点
查看复制相关状态

可以在主/从节点查看命令

info replication

断开复制

在从节点执行

slaveof no one

复制过程

file

  1. 执行slaveof命令后,从节点保存主节点地址信息便返回,此时建立复制流程还没开始
  2. 从节点内部通过每秒的定时任务扫描配置的主节点信息,当发现新的主节点后便会创世与该节点建立网络连接
  3. 从节点会建立一个socket套接字,专门用于接收主节点发送的复制命令。 建立连接成功后,从节点发送ping到主节点,如果发送之后没有收到主节点的回复(正常应该返回一个pong),如果无法建立连接,则会从2开始每秒重试,或执行slaveof no one。
  4. 主从复制连接正常后,首次建立复制时,主节点会把持有的数据通过 全量复制部分同步的方式发送给从节点
  5. 对于后续对主节点的数据操作,会持续同步给从节点

从节点内部使用redis的 psync 命令进行全量/部分复制

psync {主节点运行id} {当前从节点已复制的数偏移宜量}

该命令需要如下三个组件: 复制偏移量:参与复制的主从节点自身都会维护一个复制偏移量,主/从节点在处理完写入命令后,会把对偏移量做累加。从节点每秒上报自身复制偏移量给主节点,主节点也会保存从节点的复制偏移量。使用 info replication 命令可以查看该偏移量 复制积压缓冲区:是保存在主节点上的一个固定长度队列,用于部分复制和复制命令丢失的数据补救。默认为1MB,当主节点有连接的从节点时被创建,此时向主节点写入数据,其不但会把命令发送给从节点,还会写入复制积压缓冲区。注:当从节点断开连接,主节点会根据记录的该从节点的偏移量,将后续的写操作记录在该复制积压缓冲区,如果后续从节点重新连接,且该缓冲区未满,则从该缓冲区部分复制,否则执行全量复制 主节点运行ID:每个redis节点每次启动后都会动态分配一个40位的十六进制的字符串作为运行ID。如果只使用ip+port的方式识别主节点,那么当主节点重启后变更了数据集(如替换了RDB/AOF文件),主节点再依据偏移量做主从复制是不安全的。因此,从节点一旦发现主节点ID有变化,就会做全量复制. 可以使用 debug reload命令重新加载RDB并保持ID不变,避免不必要的全量复制

主节点创建RDB和发送RDB快照期间,仍需要响应读写命令,此时主节点会把这期间的命令数据保存在复制客户端缓冲区内,当从节点加载完RDB后,主节点再把缓冲区内的数据发送给从节点

安全性

只读:默认情况下,要求从节点使用 slave-read-only=yes 配置为只读模式 传输延迟:主从复制时,会出现网络延迟干扰,使用 repl-disable-tcp-nodelay=TCP_NODELAY参数控制复制策略

拓扑集群

根据拓扑复杂性,可以分为三种:一主一从,一主多从,树状主从结构

  1. 一主一从 file

用于主节点出现宕机提供故障转移支持。当应用写命令并发量较高且需持久化时,可以只在从节点开启AOF。当主节点需要关闭或重启时,应先使用 slaveof no one 断开主从节点复制关系,避免因主节点重启后数据被清空而导致从节点数据也被清空

  1. 一主多从 file

对于读占比比较大的场景,可以将读命令发送到从节点来分担主节点的压力 从节点过多会导致过度消耗网络资源和主节点的负载

  1. 树状主从结构 file

可以有效降低主节点的负载和要传给从节点的数据量,同时也能支持较多的读操作

心跳机制

主从节点建立后,彼此通过心跳机制保持连接

  1. 主从节点彼此都有心跳检测机制,各自模拟成对方的客户端进行通信,可以通过client list命令查看复制相关的客户端信息,主节点连接状态为flags=M,从节点为S。
  2. 主节点默认每隔10秒对从节点发送ping命令
  3. 从节点每隔1秒发送 replconf ack {复制偏移量} 命令,用于检测数据是否丢失。主节点会记录每个从节点最后发送该命令的时间,记录在 info replication 中的lag字段中,如果超过60秒,则判定下线,断开复制连接。如果后续从节点重新恢复连接,则心跳检测继续

异步复制

主节点将读写命令同步给从节点的过程是异步的,不需要等待复制完成

读写分离

主从结构下,可以做到写入在主节点,读取在从节点

可能出现的情况

  1. 数据延迟 但可能会出现刚写入主节点就立刻在从节点读取,但读不到的情况 可以编写额外的程序用于监控主从偏移量,一旦发现延迟过高则触发熔断或降级机制

  2. 读到过期数据 当主节点存储大量设置了超时数据时,redis内部需要维护过期数据删除策略,删除策略分两种:惰性删除和定时删除 惰性删除:主节点每次读取命令时都会检测key是否超时。注意:为了保证复制的一致性,从节点永远不会主动删除超时数据 定时删除:主节点内部使用定时任务循环采样一定数量的key。默认每秒运行10次,当发现获取时执行del命令并同步给从节点。但是如果此时采样速度跟不上过期速度,那么从节点就暂时收不到del命令,就可能读取到过期数据。解决方法是:从节点读取数据之前会检查键过期时间决定是否返回数据

    1. 定时任务在每个数据库空间随机检查20个键,当发现过期时删除对应的键
    2. 如果超过25%的键过期,循环执行回收逻辑直到不足25%或者运行超时为止(慢模式默认25毫秒超时)
    3. 如果慢模式回收超时,则在报错之前会再次以快模式运行回收任务,快模式下超时时间为1毫秒,且2秒内只能运行一次,快慢模式的内部删除逻辑相同,只是超时时间不同

复制风暴

大量从节点对同一主节点或者对同一台机器的多个主节点短时间内发起全量复制的过程。 解决方案:减少从节点数量或采用树状主从结构,且尽量将主节点分散到多个物理机上,避免资源竞争

内存相关

内存溢出控制策略

当所用内存达到maxmemory设置的上限时会触发溢出控制策略,使用maxmemory-policy动态控制

  1. noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端OOM错误信息,此时Redis只响应读操作
  2. volatile-lru:根据LRU(Least Recent Used)算法删除设置了超时属性的键,直到腾出足够空间为止。如果没有可以删除的键,回退到noeviction策略。
  3. allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
  4. allkeys-random:随机删除所有键,直到腾出足够空间为止。
  5. volatile-random:随机删除过期键,直到腾出足够空间为止。
  6. volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据,如果没有,回退到noeviction策略。

内存优化

redisObject

redis所有键值对象都使用redisObject类型来封装,包括string、hash、list、set、zset在内的所有数据类型

typedef struct redisObject {
    int type;  // 对象类型
    int encoding;  // 内部编码类型
    string lru;  // LRU计时时钟,记录对象最后一次被访问的时间,用于辅助LRU算法删除键数据
    int refcount;  // 当前对象被引用的次数,用于通过引用次数回收内存,当该值为0时,可以安全被回收
    void *ptr;  // 数据,如果数据类型为整数,则直接存储数据,否则表示指向数据的指针
}

优化建议1:可以使用 scan+object idletime 命令查询哪些键长时间未被访问,然后进行清理 优化建议2:建议将字符串长度控制在39字节以内,因为string在39字节以内时使用embstr编码,字符串数据SDS(下面具体数据类型有详细介绍)将和redisObject一起分配内存,故只需要分配一次内存即可

缩减键值对象

在设计key时,其长度越短越好 对value来说,常见的需求是把对象序列化为二进制数组放入redis,所以应在业务上尽量精简对象,去掉不必要的属性。并选择高效的序列化方式。或者存储压缩后的json、xml字符串

共享对象池

Redis会将 0-9999 的整数redisObject放在一个共享对象池中,当创建的value为这些整数时,直接引用共享对象池中的对象。所以应尽量使用整数对象以节省内存

字符串优化
编码优化

redis为每一种数据类型提供了多种编码方式,调整各个编码方式转换配置规则,选择最合适自己业务的编码

注:set类型在一定阈值下使用该编码方式,超过阈值则升级为hashtable,且不再回退。升级操作会导致重新申请内存,并把原有数据转换后拷贝到新数组

控制键的数量

可以考虑将多个键合并到一个hash类型中,因为hash会使用ziplist进行优化,但是如果hash对象过大升级成为了hashtable反而会增加内存消耗,应避免

哨兵Sentinel

主从模式中,一旦主节点发生故障,就需要手动将一个从节点晋升为主节点,同时需要修改应用方主节点的地址,还需要命令其他从节点去复制新的主节点,整个过程都需要人工干预,易出错。且在此过程中可能造成数据丢失 Sentinel节点本身就是独立的redis节点,不过他们不存储数据,只支持部分命令

使用Sentinel搭建高可用架构

当主节点出现故障时,sentinel自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可用

原理

redis通过三个定时监控任务完成对各个节点的发现和监控

  1. 每隔10秒,每个Sentinel节点会向主节点和从节点发送info命令获取集群的最新拓扑结构 其作用有:1)通过向主节点执行 info 命令,从而获取从节点信息。这也就是为什么只用对sentinel配置主节点的原因。2)当有新的节点加入时可以立即感知。3)节点不可达或故障转移后实时更新节点拓扑信息

  2. 每隔2秒,每隔Sentinel节点会向redis数据节点的__sentinel__:hello频道上发送该sentinel节点对主节点的判断以及当前sentinel节点的信息。同时每隔sentinel节点也会订阅该频道,来了解其他sentinel节点以及他们对主节点的判断

  3. 每隔1秒,每隔sentinel节点会向主节点、从节点、其他sentinel节点发送一条ping命令做心跳检测

主客观下线
故障转移过程
  1. Sentinel领导者从slave节点中选举master节点,选择过程: file
  2. Sentinel领导者从选举出来的master节点执行 slaveof no one 命令让其成为主节点
  3. Sentinel领导者会向剩余的从节点发送命令,让它们成为新主节点的从节点,使用parallel-syncs参数控制同时向新主节点同步数据的请求数量。
  4. Sentinel节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后让其成为从节点。
领导者sentinel节点选举过程

客观下线后,还需要选举出一个领导者sentinel来完成主观下线和故障转移 redis使用了Raft算法实现领导者的选举,其基本过程如下:

  1. 每个在线的Sentinel节点都有资格成为领导者,当它确认主节点主观下线时,会向其他sentinel节点发送 sentinel is-master-down-by-addr命令,要求将自己设置为领导者(注:该命令同时会向其他sentinel节点确认主节点是否真的下线)
  2. 收到命令的Sentinel节点,如果还没同意过其他sentinel节点的领导者请求,就会同意该请求,否则拒绝。
  3. 一旦该Sentinel节点发现自己的票数已经大于等于 quorum或者一半以上的sentinel节点数,那么它将成为领导者
  4. 如果此过程没有选举出领导者,将会进入下一次选举

高可用的读写分离

集群与分布式

当遇到单机内存、并发、流量等瓶颈时,可以采用Redis Cluster架构达到负载均衡的目的 注:集群自身已经依靠Gossip协议实现了高可用,不需要sentinel也行

数据分布理论

分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上。 重点需要关注的是数据分区规则,常见的分区汇则有哈希分区顺序分区两种: file

哈希分区

创建的哈希分区分为如下几种

  1. 节点取余分区 使用redis的key或者请求用户ID与节点总数取余,得到数据应储存在节点的角标 优点:足够简单,常用于数据库的分库分表 缺点:当节点数量发生变化时,数据节点映射关系需要重新计算

  2. 一致性哈希分区 一致性哈希分区实现思路是为系统中每个节点分配一个token,范围一般在 0-2^32,这些token构成一个哈希环。 数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。 优点:加入和删除节点时只影响哈希环中相邻的节点 缺点:

    1. 加减节点会造成哈希环中部分数据无法命中,需手动处理或忽略这部分数据,因此一致性哈希常用于缓存场景。例如:原本有两个节点A、B,其token分别是10、30.向其中加入一个hash值为15的key,此时它应该保存在B节点。现增加一个新的节点C,token为20,则再次查找这个hash为15的key时就会去C中找,自然找不到该数据
    2. 当使用少量节点时,节点数量的变化将大范围影响哈希环中数据的映射,故这种方式不适合节点少的分布式方案
    3. 普通的一致性哈希在分区的增减节点时需要增加一倍或者减去一半节点才能保证数据的负载均衡。(即在每两个节点中插入一个节点或删除一个节点)
  3. 虚拟槽分区(Redis Cluster采用的方法) 使用分散度良好的哈希函数把所有数据映射到一个固定的整数集合中,这些整数就定义为槽(slot)。这个整数集合范围一半远远大于节点数。比如Redis Cluster槽的范围就是0-16383.槽是集群内数据管理和迁移的基本单位。采用大范围的槽主要目的是为了方便数据拆分和集群扩展。每个节点负责一定数量的槽 例如:有5个节点,则每个节点平均负责3276个槽。由于采用高质量的哈希算法,每个槽所映射的数据通常比较均匀,将数据平均划分到5个节点进行数据分区

一致性哈希和虚拟槽的区别
  1. 虚拟槽不是闭合的,每个节点维护一定数量的槽,并且每个节点记录所有的槽和节点的对应关系
  2. 一致性hash使用虚拟节点来应对数据转移和hash分配不均的问题来保证数据的安全性和集群的可用性,而虚拟槽使用主从节点保证安全和可用
  3. 扩容和收缩时,一致性哈希会按照顺时针重新分布节点,而虚拟槽需要手动增删和分配槽位
Redis数据分区

redis cluster采用虚拟槽分区,所有的键根据哈希函数(CRC16)映射到0-16383整数槽内。每个节点负责维护一部分槽以及槽所映射的键值数据 注:一个槽对应多个key file

集群功能限制

redis集群相对于单机在功能上存在一些限制:

  1. key批量操作支持有限,如mset、mget等目前只支持具有相同slot值的key进行批量操作
  2. key数据操作支持有限,也只能在统一节点上执行事务
  3. 不能将键值对象拆分成多个部分存储在不同的节点,例如将list拆分为多个list
  4. 集群模式只能使用0号数据库
  5. 主从结构只支持一层,即不能创建树状主从结构
集群的搭建

一共分为三步(可以使用redis-trib.rb 工具自动完成)

  1. 准备节点 集群一般由多个节点组成,节点数量至少为6个才能保证高可用。每个节点需要开启配置 cluster-enabled yes 使得redis运行在集群模式下

    • 第一次启动如果没有集群配置文件,每个节点就会自动创建一份
    • 当集群内节点信息发生变化,例如添加节点、节点下线等,节点会自动保存集群状态到配置文件中,所以该文件不要手动修改。该文件同时记录了集群中所有节点的节点id,注意,不同于redis节点的runid,该节点id创建之后不会发生变化,节点重启后会读取该配置文件并获取之前的节点id
  2. 节点握手 上一步只是启动了6个节点,但这些节点还并不知道其他节点的存在。节点握手指一批运行在集群模式下的节点通过 Gossip 协议彼此通信,达到感知对方的过程。节点握手是集群彼此通信的第一步,由客户端发起命令: cluster meet {要握手的ip} {要握手的端口},注:例如集群有两台机器,端口分别是 6379和6380,现需要他们握手,则使用redis-cli连接上6379,然后执行上述命令,ip和端口为6380的地址。6380收到消息后,保存6379的信息并回复pong,之后这两个节点定期ping/pong

    • 向集群中新添加一个节点时,只需要让其和集群中任意一个节点握手,则握手信息就会自动在集群中传播,其他节点就会发现该新节点并发起握手流程
    • 由于此时还没有分配槽,所以此时集群还不能正常工作,集群处于下线状态,所有的数据读写都被禁止
  3. 分配槽 通过对应节点上执行 cluster addslots {0...5461} 命令将指定范围的槽分配给指定的节点,之后节点会进入在线状态 例如,启动了6个节点,为其中3个节点分配了槽,剩下三个使用 cluster replicate {nodeId} 作这三个的从节点,其复制过程同主从结构复制模型

节点通信

通信流程

redis集群采用p2p的Gossip协议维护集群节点的元数据(即节点负责哪些数据,是否出现故障等状态信息) 事实上,在一定时间内,每个节点可能只直到集群中部分节点的信息,但只要这些节点彼此正常通信,最终他们会达到一致的状态,类似于流言(Gossip)的传播,从而达到集群状态同步。

基本过程

  1. 集群中每个节点都会单独开辟一个tcp通道用于节点间彼此通信
  2. 每个节点在固定周期内通过特定规则选择部分节点发送ping消息
  3. 接收到ping消息的节点用pong消息作为响应
Gossip协议

该协议主要职责就是信息交换 常用的Gossip消息可分为ping消息、pong、消息、meet消息、fail消息等

节点选择

由于每个节点都保存了整个集群的所有节点信息,每次信息交换自然不可能给所有节点都进行信息交换(redis集群中的节点默认每秒执行10次信息交换)。由此,选择合适的节点进行通信就尤为重要 file

  1. 每秒随机选取5个最久没有通信的节点发送ping消息
  2. 每100毫秒都会扫描本地节点列表,如果发现节点最近一次接收pong消息的时间大于 cluster_node_timeout/2 则会立刻发送ping消息,防止该节点信息太长时间没更新 注:由此可知,redis每秒会发送11个ping消息

集群伸缩/扩容

可以对集群中的节点进行动态扩容和收缩,其原理可理解为槽和对应数据在不同节点之间的灵活移动 如果希望加入一个新的节点实现扩容,执行完添加节点操作后,还需要通过相关命令把一部分槽和数据迁移给新节点

请求路由

redis集群中任何节点都保存了所有节点信息和其维护的槽信息。客户端可以向集群中任何一台主节点发送数据请求,该节点会计算key的hash,如果对应自身的槽就直接进行处理, 否则

  1. 如果目标节点没有发生槽的迁移,则返回一条 MOVED 重定向信息给客户端,该信息中包含key应该对应的槽以及该槽对应的主机信息。客户端收到该重定向信息后,应重新请求返回的redis节点,该过程是手动的,可以通过加入 -c 参数使其自动处理 客户端可以通过缓存这些重定向信息以便下次访问
  2. 如果目标节点正在发生槽的迁移,则返回一条 ASK 重定向信息给客户端,该信息内容同MOVED信息。当客户端重新访问目标节点时,其会先在本地找目标key,如果没找到则会去一个迁移缓存中找,所有以及迁移完毕的槽会保存在这里。如果在缓存中找到了,则会继续返回 ASK 信息给客户端

主客观下线

故障恢复

故障节点客观下线后,如果下线节点是持有槽的主节点,则需要在它的从节点中选举一个晋升,从而保证集群高可用。当从节点发现自己的主节点出现客观下线后,会触发故障恢复流程: file

故障恢复流程
  1. 资格检查 每个从节点都要检查最后与主节点断线的时间,超过一定时间则不具备故障转移资格
  2. 准备选举时间 多个从节点符合故障转移资格后,会根据复制偏移量进行延迟选举,复制偏移量越小,延迟事件越短。
  3. 发起选举 在集群内广播选举消息,并记录已发送过消息的状态,保证该从节点在一个配置纪元内只能发生一次选举 每个主节点自身维护一个“配置纪元”标示当前主节点的版本,所有主节点的配置妓院都不相等,从节点会复制主节点的配置纪元。整个集群又维护一个全局的配置纪元,用于记录集群内所有主节点配置纪元的最大版本。 配置纪元会随着ping/pong信息在集群内传播,当发送方与接收方都是主节点,且配置纪元相等时,则代表出现了冲突,nodeId更大的一方将会递增全局配置纪元并赋值给当前节点来区分冲突 配置纪元的作用:
    • 标识集群内每个主节点的不同版本和当前集群最大的版本
    • 每次集群发生重要的事件时(例如加入新的主节点、从节点竞争选举),都会递增全局纪元并赋值给相关主节点,用于记录这一关键事件
    • 主节点具有更大的配置纪元代表了更新的集群状态,因此当主节点间进行信息交换出现了slots等关键信息不一致时,以配置纪元更大的一方为准
  4. 选举投票 著有持有槽的主节点才会处理故障选举消息,因为每个持有槽的节点在一个配置纪元内都有唯一一张选票,当接收到第一个请求投票的从节点消息时回复ACK消息作为投票,之后相同纪元内其他从节点的选举消息将被忽略 选举过程其实就是一个领导者选举过程,当从节点收集到一半以上主节点的选票时,就可以执行替换主节点的操作 如果在开始选举一段时间后,仍没有获得足够多的选票,该次选举作废,从节点自增节点的配置纪元并发起下一轮投票。
  5. 替换主节点 完成主节点晋升后,向所有主节点发送一个pong消息,通知主节点晋升完毕

String

内部编码

Hash

内部编码
字典的实现

Redis的字典使用哈希表作为底层实现,一个哈希表里面可以由多个哈希表节点,每个哈希表节点就保存了字典中的一个键值对

定义

file


// 哈希表
typedef struct dictht{
    dictEntry **table; // 哈希表数组,数组中的每个元素都是一个指向 `dictEntry` 结构的指针,每个dictEnry结构保存着一个键值对
    unsigned long size;  //哈希表大小
    unsigned long sizemask;  // 哈希表大小掩码,用于计算索引值,总是等于 size-1
    unsigned long used;  // 该哈希表已有节点数量
}

// 哈希表节点
typedef struct dictEntry {
    void *key; // 键

    // 值
    union{
        void *val;
        uint64_tu64;
        int64_ts64;
    }

    struct dictEntry *next; //指向下个哈希表节点,形成链表
}

// 字典
typedef struct dict {
    dictType *type; // 类型特定函数,该结构体中仅包含了计算哈希值的函数、复制键的函数、复制值的函数、对比键的函数、销毁键/值的函数
    void *privdate; // 私有数据
    dictht ht[2]; //哈希表,一般情况下只使用ht[0],ht[1]只会在对ht[0]进行rehash时使用
    in trehashidx; // rehash索引,当rehash不在进行时,值为-1
}

List

内部编码

Set

内部编码

Zset

内部编码

持久化

file

RDB

把当前进程数据生成快照保存再硬盘的过程

手动触发
  1. save 命令:阻塞,一般不用
  2. bgsave 命令:持久化过程由子进程负责,阻塞只发生在fork子进程阶段,一般时间很短。
自动触发
  1. 使用 save m n 命令,表示在m秒内数据集存在n次修改时自动触发
  2. 如果从节点执行全量复制操作,主节点自动生成RDB文件发送给从节点
  3. 执行debug reload命令重新加载Redis时会触发save
  4. 默认情况下执行shutdown命令时,若没有开启AOF则自动执行bgsave
RDB文件处理

RDB文件保存在配置文件指定的目录下 Redis默认采用LZF算法对生成的RDB文件进行压缩处理,默认打开! file

优缺点

file

AOF (append only file)

理解为每次执行命令都会记录一个日志(aof buf),重启redis时读取该日志文件 所有的写入命令都会追加到aof_buf缓冲区当中,AOF缓冲区根据对应的同步策略向硬盘同步数据 aof_buf缓冲区写入的数据同RESP协议内容(即客户端与服务端通信的协议)

同步策略
  1. always:命令写入aof_buf 后立即调用系统fsync操作同步到AOF文件,fsync完成后线程返回 注:系统调用write和fsync说明
    • write操作会触发延迟写机制,Linux在内核提供页缓冲区来提高硬盘IO性能,write操作在写入系统缓冲区后直接返回,同步硬盘操作依赖于系统调度机制
    • fsync针对单个文件(比如AOF文件)做强制硬盘同步,fsync将阻塞直到写入硬盘后返回
  2. everysec:建议、默认。命令写入aof_buf后调用系统write操作,write完成后线程返回。fsync同步文件操作由专门的线程每秒调用一次
  3. no:命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步,同步硬盘操作由操作系统负责,通常同步周期最长30秒
重写机制

随着命令不断写入AOF,文件会越来越大。AOF文件重写是把Redis进程内的数据转化为命令同步到新的AOF文件的过程。这样AOF文件只保留最终数据的写入命令,去除多余无效命令(如del key、srem key)

手动触发

调用 bgrewriteaof 命令

自动触发

根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参 数确定自动触发时机。

自动触发时机=aof_current_size>auto-aof-rewrite-minsize&&(aof_current_size-aof_base_size)/aof_base_size>=auto-aof-rewritepercentag

问题定位与优化
fork操作

当Redis做RDB或AOF重写时,必须执行fork创建子进程,虽然fork创建的子进程不需要拷贝父进程的物理内存空间,但是会复制父进程的空间内存页表,非常耗时 可以使用 info stats 命令查找 latest_fork_usec指标获取最近依次fork操作耗时 应当尽量避免使用虚拟化技术例如docker,KVM等。控制redis最大可用内存,因为fork耗时和内存使用量成正比。降低fork频率,例如减少AOF自动触发时机

AOF追加阻塞

当开启AOF并使用everysec为同步策略时,redis使用另一条线程每秒执行fsync同步硬盘,当硬盘资源繁忙时,如果上一次执行fsync命令时间超过2秒,则主线程会阻塞等待fsync执行结束。 由此可见everysec配置最多可能丢失2秒数据

单机多实例部署

单redis实例只使用一个cpu,则可以在一个多核机器上部署多个redis实例。 但是多实例会造成fork线程压力倍增,redis提供了一系列指标获取当前redis实例状态。可以单独开启一个外部程序,轮询所有的redis实例,判断其是否满足AOF条件,如果满足,则执行bgsave命令手动持久化

Tags: