redis介绍

why 快

  • 完全基于内存,绝大部分请求是纯粹的内存操作
  • 数据结构简单,对数据操作也简单
  • 单线程 避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
  • 多路I/O复用模型,非阻塞IO

多路 I/O 复用模型

利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。

“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。

  • 注意1:只有当上一个套接字产生的事件被所关联的事件处理器执行完毕,I/O多路复用程序才会继续向文件事件分派器传送下一个套接字,所以对每个命令的执行时间是有要求的,如果某个命令执行过长,会造成其他命令的阻塞。所以慎用O(n)命令,Redis是面向快速执行场景的数据库。
  • 注意2:命令的并发性。Redis是单线程处理命令,命令会被逐个被执行,假如有3个客户端命令同时执行,执行顺序是不确定的,但能确定不会有两条命令被同时执行,所以两条incr命令无论怎么执行最终结果都是2。

新版6.0对比

Redis 在处理客户端的请求时,包括获取(Socket 读)、解析、执行、内容返回(Socket 写)等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。

但如果严格来讲从 Redis 4.0 之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 Key 的删除等等。

使用 Redis 时,几乎不存在 CPU 成为瓶颈的情况, Redis 主要受限于内存和网络。Redis 通过使用 Pipelining 每秒可以处理 100 万个请求,所以如果应用程序主要使用 O(N) 或 O(log(N)) 的命令,它几乎不会占用太多 CPU

使用了单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。

单线程机制使得 Redis 内部实现的复杂度大大降低,Hash 的惰性 Rehash、Lpush 等等 “线程不安全” 的命令都可以无锁进行。

why 多线程

为了应付更大的QPS(现在的要管理的 Redis 服务器太多,维护代价大。)

因为读写网络的 Read/Write 系统调用占用了 Redis 执行期间大部分 CPU 时间,瓶颈主要在于网络的 IO 消耗。

优化主要有两个方向:

  • 提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方式。
  • 使用多线程充分利用多核,典型的实现比如 Memcached。

所以总结起来,Redis 支持多线程主要就是两个原因:

  • 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核。
  • 多线程任务可以分摊 Redis 同步 IO 读写负荷。

Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。

底层结构

redis有5种数据类型,包括字符串、列表、集合、有序集合和字典

redis底层的数据结构有6种,包括动态字符串、双向链表、压缩列表(ziplist)、hash表、跳表(skip list)和整数数组

字符串类型

底层数据结构是动态字符串。

列表

如果同时满足下面条件,就使用压缩列表,否则使用双向链表。

  • 列表中单个元素小于64字节
  • 列表中元素个数少于 512

「压缩列表」在内存中是一块儿连续的内存空间,结构如下:图片压缩列表查找时间复杂度是o(n)

集合

如果同时满足下面条件,就使用有序整数数组,否则使用hash表。

  • 集合中元素都是整数类型
  • 集合中元素个数不超过512

有序集合

如果同时满足下面2个条件,就使用压缩列表,否则使用跳表。

  • 集合中元素都小于64字节
  • 集合中元素个数小于128

「注意:有序集合还有一个HASH表用于保存集合中元素的分数,做ZSCORE操作时,查询的就是这个HASH表,所以效率很高。」

「跳表」的结构如下:图片如果不加索引,查找10这个数字需要查询10次,使用了二级索引,查找10这个数字需要5次,而使用一级索引,需要查询3次。

跳表的每一层都是一个有序链表,最下面一层保存了全部数据。跳表插入、删除、查询的时间复杂度是o(logN)。跳表需要存储额外的索引节点,会增加额外的空间开销。

字典

如果同时满足下面2个条件,就使用压缩列表,否则使用hash表。

  • 字典中每个entrykey/value都小于64字节
  • 字典中元素个数小于512

缓存淘汰

  • lru是按照数据的最近最少访问原则来淘汰数据,可能存在的问题是如果大批量冷数据最近被访问了一次,就会占用大量内存空间,如果缓存满了,部分热数据就会被淘汰掉。
  • lfu是按照数据的最小访问频率访问次数原则来淘汰数据,如果两个数据的访问次数相同,则把访问时间较早的数据淘汰。

删除策略

下面介绍三种删除策略:

  • 定时删除:在这是键的过期时间的同时,创建一个定时器 Timer,让定时器在键过期时间来临时立即执行对过期键的删除。
  • 惰性删除:键过期后不管,每次读取该键时,判断该键是否过期,如果过期删除该键返回空。
  • 定期删除:每隔一段时间对数据库中的过期键进行一次检查。

定时删除:对内存友好,对 CPU 不友好。如果过期删除的键比较多的时候,删除键这一行为会占用相当一部分 CPU 性能,会对 Redis 的吞吐量造成一定影响。

惰性删除:对 CPU 友好,内存不友好。如果很多键过期了,但在将来很长一段时间内没有很多客户端访问该键导致过期键不会被删除,占用大量内存空间。

定期删除:是定时删除和惰性删除的一种折中。每隔一段时间执行一次删除过期键的操作,并且限制删除操作执行的时长和频率。

  • Redis 会将每一个设置了 expire 的键存储在一个独立的字典中,以后会定时遍历这个字典来删除过期的 key。除了定时遍历外,它还会使用惰性删除策略来删除过期的 key。
  • Redis 默认每秒进行十次过期扫描,过期扫描不会扫描所有过期字典中的 key,而是采用了一种简单的贪心策略。 从过期字典中随机选择 20 个 key;删除这 20 个 key 中已过期的 key;如果过期 key 比例超过 1/4,那就重复步骤 1。
  • 同时,为了保证在过期扫描期间不会出现过度循环,导致线程卡死,算法还增加了扫描时间上限,默认不会超过 25ms。

主从同步

https://www.cnblogs.com/kismetv/p/9236731.html

同步:将从服务器的数据库状态更新成主服务器当前的数据库状态。

命令传播:当主服务器数据库状态被修改后,导致主从服务器数据库状态不一致,此时需要让主从数据同步到一致的过程。

从服务器

1
slaveof 192.168.1.9 6379  #添加属于某台主机的从 服务  从服务只读,不可在命令行写入数据

主服务器

1
info replication

同步

  • 从服务器向主服务器发送 sync 命令
  • 收到 sync 命令后,主服务器执行 bgsave 命令,用来生成 rdb 文件,并在一个缓冲区中记录从现在开始执行的写命令。
  • bgsave 执行完成后,将生成的 rdb 文件发送给从服务器,用来给从服务器更新数据
  • 主服务器再将缓冲区记录的写命令发送给从服务器,从服务器执行完这些写命令后,此时的数据库状态便和主服务器一致了。

命令传播

为了再次让主从数据库状态一致,主服务器就需要向从服务器执行命令传播操作 ,即把刚才造成不一致的写命令,发送给从服务器去执行。从服务器执行完成之后,主从数据库状态就又恢复一致了。

psync

2.8 版本开始,进行主从同步可能只需要执行命令传播即可。这个也是因为 sync 比较耗资源,从而采取的优化

主从同步实际分 2 种情况:

  • 初次复制:从服务器第一次复制当前主服务器(PS:主服务器是有可能更换的)
  • 断线后重复制:处于命令传播阶段的主从服务器,因为网络问题而中断复制,从服务器通过自动重连,重新连接上主服务器并继续复制。

psync 具有完整重同步和部分重同步两种模式

  • 完整重同步:用于初次复制情况,执行过程同 sync,在这不赘述了。
  • 部分重同步:用于断线后重复制情况,如果满足一定条件(偏移量的比较),主服务器只需要将断线期间执行的写命令发送给从服务器即可。

部分重同步功能由以下 3 部分组成:

  • 主从服务器的复制偏移量
  • 主服务器的复制积压缓冲区
  • 服务器的运行 id(run id)

偏移量

执行复制的主从服务器都会分别维护各自的复制偏移量:

  • 主服务器每次向从服务器传播 n 个字节数据时,都会将自己的复制偏移量加 n。
  • 从服务器接受主服务器传来的数据时,也会将自己的复制偏移量加 n

缓冲区

首先,复制积压缓冲区是一个固定长度,先进先出的队列,默认 1MB。

当主服务器进行命令传播时,不仅会将命令发送给从服务器,还会发送给这个缓冲区。

因此复制积压缓冲区的构造是这样的:

img

当从服务器向主服务器发送 psync 命令时,还需要将自己的复制偏移量带上,主服务器就可以通过这个复制偏移量和复制积压缓冲区的偏移量进行对比。

若复制积压缓冲区存在从服务器的复制偏移量 + 1 后的数据,则进行部分重同步,否则进行完整重同步

run id

运行 id 是在进行初次复制时,主服务器将会将自己的运行 id 发送给从服务器,让其保存起来。

当从服务器断线重连后,从服务器会将这个运行 id 发送给刚连接上的主服务器。

若当前服务器的运行 id 与之相同,说明从服务器断线前复制的服务器就是当前服务器,主服务器可以尝试执行部分同步;若不同则说明从服务器断线前复制的服务器不是当前服务器,主服务器直接执行完整重同步。

心跳

当完成了同步之后,主从服务器就会进入命令传播阶段,此时从服务器会以每秒 1 次的频率,向主服务器发送命令:REPLCONF ACK <replication_offset> 其中 replication_offset 是从服务器当前的复制偏移量

发送这个命令主要有三个作用

  • 检测主从服务器的网络状态
  • 辅助实现 min-slaves 选项
  • 检测命令丢失(若丢失,主服务器会将丢失的写命令重新发给从服务器)

哨兵

Redis主从复制的作用有数据热备、负载均衡、故障恢复等;但主从复制存在的一个问题是故障恢复无法自动化。

哨兵的核心功能是主节点的自动故障转移

  • 监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
  • 自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
  • 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
  • 通知(Notification):哨兵可以将故障转移的结果发送给客户端。

它由两部分组成,哨兵节点和数据节点:

  • 哨兵节点:哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的redis节点,不存储数据。
  • 数据节点:主节点和从节点都是数据节点。

哨兵配置

其中,sentinel monitor mymaster 192.168.92.128 6379 2 配置的含义是:该哨兵节点监控192.168.92.128:6379这个主节点,该主节点的名称是mymaster,最后的2的含义与主节点的故障判定有关:至少需要2个哨兵节点同意,才能判定主节点故障并进行故障转移。

才能判定主节点故障并进行故障转移info sentinel,哨兵发现主节点故障并转移,需要一段时间。从从节点选举主节点,同时原主节点会变成从节点。

虽然原主节点已经挂掉,(但是从节点不变),由于哨兵并不会对从节点进行客观下线

客户端可以通过哨兵节点+masterName获取主节点信息,在这里哨兵起到的作用就是配置提供者。

需要注意的是,哨兵只是配置提供者,而不是代理

关于哨兵的原理,关键是了解以下几个概念。

  • 定时任务:每个哨兵节点维护了3个定时任务。
    • 通过向主从节点发送info命令获取最新的主从结构;
    • 通过发布订阅功能获取其他哨兵节点的信息;
    • 通过向其他节点发送ping命令进行心跳检测,判断是否下线。
  • 主观下线:在心跳检测的定时任务中,如果其他节点超过一定时间没有回复,哨兵节点就会将其进行主观下线。顾名思义,主观下线的意思是一个哨兵节点“主观地”判断下线;与主观下线相对应的是客观下线。

  • 客观下线:哨兵节点在对主节点进行主观下线后,会通过sentinel is-master-down-by-addr命令询问其他哨兵节点该主节点的状态;如果判断主节点下线的哨兵数量达到一定数值,则对该主节点进行客观下线。

    需要特别注意的是,客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作。

  • 选举领导者哨兵节点:当主节点被判断客观下线以后,各个哨兵节点会进行协商,选举出一个领导者哨兵节点,并由该领导者节点对其进行故障转移操作。一般来说,哨兵选择的过程很快,谁先完成客观下线,一般就能成为领导者。

  • 故障转移:

    • 在从节点中选择新的主节点:选择的原则是,首先过滤掉不健康的从节点;然后选择优先级最高的从节点(由slave-priority指定);如果优先级无法区分,则选择复制偏移量最大的从节点;如果仍无法区分,则选择runid最小的从节点。
    • 更新主从状态:通过slaveof no one命令,让选出来的从节点成为主节点;并通过slaveof命令让其他节点成为其从节点。
    • 将已经下线的主节点(即6379)设置为新的主节点的从节点,当6379重新上线后,它会成为新的主节点的从节点。

集群

集群中的节点分为主节点和从节点:只有主节点负责读写请求和集群信息的维护;从节点只进行主节点数据和状态信息的复制。

1、数据分区:数据分区(或称数据分片)是集群最核心的功能。

集群将数据分散到多个节点,一方面突破了Redis单机内存大小的限制,存储容量大大增加;另一方面每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。

Redis单机内存大小受限问题,在介绍持久化和主从复制时都有提及;例如,如果单机内存太大,bgsave和bgrewriteaof的fork操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出……。

2、高可用:集群支持主从复制和主节点的自动故障转移(与哨兵类似);当任一节点发生故障时,集群仍然可以对外提供服务

集群配置

原理

集群的搭建可以分为四步:

(1)启动节点:将节点以集群模式启动,此时节点是独立的,并没有建立联系;

(2)节点握手:让独立的节点连成一个网络;

(3)分配槽:将16384个槽分配给主节点;

(4)指定主从关系:为从节点指定主节点。

实际上,前三步完成后集群便可以对外提供服务;但指定从节点后,集群才能够提供真正高可用的服务。

在Redis集群中,借助槽实现数据分区,具体原理后文会介绍。集群有16384个槽,槽是数据管理和迁移的基本单位。当数据库中的16384个槽都分配了节点时,集群处于上线状态(ok);如果有任意一个槽没有分配节点,则集群处于下线状态(fail)。

哈希分区的基本思路是:对数据的特征值(如key)进行哈希,然后根据哈希值决定数据落在哪个节点。常见的哈希分区包括:哈希取余分区、一致性哈希分区、带虚拟节点的一致性哈希分区等。

衡量数据分区方法好坏的标准有很多,其中比较重要的两个因素是

(1)数据分布是否均匀

(2)增加或删减节点对数据分布的影响。

由于哈希的随机性,哈希分区基本可以保证数据分布均匀;因此在比较哈希分区方案时,重点要看增减节点对数据分布的影响。

一致性哈希算法将整个哈希值空间组织成一个虚拟的圆环,如下图所示,范围为0-2^32-1;对于每个数据,根据key计算hash值,确定数据在环上的位置,然后从此位置沿环顺时针行走,找到的第一台服务器就是其应该映射到的服务器。

带虚拟节点的一致性哈希分区

该方案在一致性哈希分区的基础上,引入了虚拟节点的概念。Redis集群使用的便是该方案,其中的虚拟节点称为槽(slot)。槽是介于数据和实际节点之间的虚拟概念;每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。引入槽以后,数据的映射关系由数据hash->实际节点,变成了数据hash->槽->实际节点

在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点之间的关系,增加或删除节点对系统的影响很小。 节点的增删就是槽的重新分配。槽的数量一般远小于2^32,远大于实际节点的数量

在哨兵系统中,节点分为数据节点和哨兵节点:前者存储数据,后者实现额外的控制功能。在集群中,没有数据节点与非数据节点之分:所有的节点都存储数据,也都参与集群状态的维护。为此,集群中的每个节点,都提供了两个TCP端口:

  • 普通端口:即我们在前面指定的端口(7000等)。普通端口主要用于为客户端提供服务(与单机节点类似);但在节点间数据迁移时也会使用。
  • 集群端口:端口号是普通端口+10000(10000是固定值,无法改变),如7000节点的集群端口为17000。集群端口只用于节点之间的通信,如搭建集群、增减节点、故障转移等操作时节点间的通信;不要使用客户端连接集群接口。为了保证集群可以正常工作,在配置防火墙时,要同时开启普通端口和集群端口。

客户端访问集群

  • 计算key属于哪个槽:CRC16(key) & 16383

槽(编号0-16383)

(1)Redis对数据的特征值(一般是key)计算哈希值,使用的算法是CRC16。

(2)根据哈希值,计算数据属于哪个槽。

(3)根据槽与节点的映射关系,计算数据属于哪个节点。

  • 判断key所在的槽是否在当前节点
    • 在当前节点,可以直接在当前节点执行命令
    • 不在当前节点, 查询槽所在节点的地址,包装到MOVED错误中返回给redis-cli
  • redis-cli收到MOVED错误后,根据返回的ip和port重新发送请求。

client端可以内部维护slot->node的缓存

当执行命令时,client根据key->slot->node选择需要连接的节点,发送命令。如果成功,则命令执行完毕。如果执行失败,则会随机选择其他节点进行重试,并在出现MOVED错误时,使用cluster slots重新同步slot->node的映射关系。

槽迁移 增删节点

客户端收到ASK错误后,从中读取目标节点的地址信息,并向目标节点重新发送请求,就像收到MOVED错误时一样。但是二者有很大区别:ASK错误说明数据正在迁移,不知道何时迁移完成,因此重定向是临时的,SMART客户端不会刷新slots缓存;MOVED错误重定向则是(相对)永久的,SMART客户端会刷新slots缓存。

局限

由于集群中的数据分布在不同节点中,导致一些功能受限,包括:

(1)key批量操作受限:例如mget、mset操作,只有当操作的key都位于一个槽时,才能进行。针对该问题,一种思路是在客户端记录槽与key的信息,每次针对特定槽执行mget/mset;另外一种思路是使用Hash Tag,将在下一小节介绍。

(2)keys/flushall等操作:keys/flushall等操作可以在任一节点执行,但是结果只针对当前节点,例如keys操作只返回当前节点的所有键。针对该问题,可以在客户端使用cluster nodes获取所有节点信息,并对其中的所有主节点执行keys/flushall等操作。

(3)事务/Lua脚本:集群支持事务及Lua脚本,但前提条件是所涉及的key必须在同一个节点。Hash Tag可以解决该问题。

(4)数据库:单机Redis节点可以支持16个数据库,集群模式下只支持一个,即db0。

(5)复制结构:只支持一层复制结构,不支持嵌套。

Hash Tag原理是:当一个key包含 {}的时候,不对整个key做hash,而仅对 {}包括的字符串做hash