Redis基础

Redis从入门到精通:初级篇

  • Redis简介
  • Redis安装、启动
  • Redis登录授权
  • Redis配置文件redis.conf中参数详细的一个解读
  • Redis性能测试

Redis从入门到精通:中级篇

  • Redis数据结构String、Hash、List、Set、ZSet及相关操作,提一下Redis在3.2.0之后有新增了一种GEO的数据类型表示地理位置,不过本文这种数据结构略过
  • Redis其他一些常用命令,分为Key操作与服务器操作
  • Redis事务机制

待补充

  • Redis线程模型
  • Redis的RDB
  • Redis的AOF
  • Redis的集群方式

Redis Stream

Redis5.0重量级特性Stream尝鲜

基于 Redis 的 Stream 类型的完美消息队列解决方案

Redis实现消息队列的方案

Redis持久化

RDB

fork子进程,子进程先把内存数据写到临时文件中,然后再用临时文件中的数据同步dump.rdb数据

写时复制技术:fork产生一个和父进程完全相同的子进程,但子进程在此后多会调用exec系统调用,出于系统效率考虑,Linux中引入了写时复制技术

缺点:

  • 最后一起之久化之后的数据有可能丢失
  • 备份时需要占用两倍的硬盘空间

AOF

append only file

优点:

  • 备份机制更稳健,丢失数据的概率比较低
  • 可读日志文件,可以处理误操作

缺点:

  • 比RDB占用更多的磁盘空间
  • 恢复备份速度慢
  • 每次读写都要写同步的话,有一定的性能压力
  • 存在个别bug,造成不能恢复

Redis集群

Redis有三种集群模式,分别是:

  • 主从模式

    主数据库可以进行读写操作,当读写操作导致数据变化时会自动将数据同步给从数据库
    从数据库一般都是只读的,并且接收主数据库同步过来的数据
    一个master可以拥有多个slave,但是一个slave只能对应一个master
    slave挂了不影响其他slave的读和master的读和写,重新启动后不会自动加入,需要手动加入,加入后会将数据从master同步过来
    master挂了以后,不影响slave的读,但redis不再提供写服务,master重启后redis将重新对外提供写服务
    master挂了以后,不会在slave节点中重新选一个master
    

    实现方式:在从服务器上执行slaveof ip port命令,设置从服务器,配置的ip port则为主服务器的信息。

    主从复制相关

    主从复制原理:

    1. 当从服务器连接到主服务器之后,从服务器向主服务器发送数据同步消息

    2. 主服务器接到从服务器的消息同步消息时,把主服务器数据进行持久化,生成rdb文件,并把rdb文件发送到从服务器

    3. 从服务器拿到rdb文件之后进行读取

    4. 每次主服务器进行写操作之后,会向从服务器同步数据

    • 全量同步:从服务器刚加入时的数据同步
    • 增量同步:主服务器执行写操作之后的数据同步

    薪火相传:

    逐级数据同步,在使用slaveof ip port设置从服务器时,第一级从服务器的设置为master,第二级设置为第一级从服务器,依此类推。

反客为主:

​ 主服务器故障后,在从服务器执行slaveof no one,对应的从服务器会转变为主服务器,该方法是手动的,哨兵模式能实现自动的反客为主过程。

  • Sentinel模式(哨兵模式)

    sentinel模式是建立在主从模式的基础上,如果只有一个Redis节点,sentinel就没有任何意义
    当master挂了以后,sentinel会在slave中选择一个做为master,并修改它们的配置文件,其他slave的配置文件也会被修改,比如slaveof属性会指向新的master
    当master重新启动后,它将不再是master而是做为slave接收新的master的同步数据
    sentinel因为也是一个进程有挂掉的可能,所以sentinel也会启动多个形成一个sentinel集群
    多sentinel配置的时候,sentinel之间也会自动监控
    当主从模式配置密码时,sentinel也会同步将配置信息修改到配置文件中,不需要担心
    一个sentinel或sentinel集群可以管理多个主从redis集群,多个sentinel也可以监控同一个redis集群
    sentinel最好不要和Redis部署在同一台机器,不然Redis的服务器挂了以后,sentinel也挂了
    
    每个sentinel以每秒钟一次的频率向它所知的master,slave以及其他sentinel实例发送一个PING命令 
    如果一个实例距离最后一次有效回复PING命令的时间超过down-after-milliseconds选项所指定的值,则这个实例会被sentinel标记为主观下线。 
    如果一个master被标记为主观下线,则正在监视这个master的所有sentinel要以每秒一次的频率确认master的确进入了主观下线状态
    当有足够数量的sentinel(大于等于配置文件指定的值)在指定的时间范围内确认master的确进入了主观下线状态,则master会被标记为客观下线 
    在一般情况下,每个sentinel会以每10秒一次的频率向它已知的所有master,slave发送INFO命令 
    当master被sentinel标记为客观下线时,sentinel向下线的master的所有slave发送INFO命令的频率会从10秒一次改为1秒一次 
    若没有足够数量的sentinel同意master已经下线,master的客观下线状态就会被移除;若master重新向sentinel的PING命令返回有效回复,master的主观下线状态就会被移除
    

    当使用sentinel模式的时候,客户端就不要直接连接Redis,而是连接sentinel的ip和port,由sentinel来提供具体的可提供服务的Redis实现,这样当master节点挂掉以后,sentinel就会感知并将新的master节点提供给使用者。

    实现方式:在主从模式的基础上进行配置,增加哨兵的配置文件sentinel.conf

    sentinel monitor mymaster ip port num # mymaster是为master起的名字,num代表至少有多少个哨兵同意才能进行master的切换
    

    配置项replica-priority,值越小表示优先级越高,在master故障时越可能成为新的master

    选择新master的条件:

    1. 优先选择replica-pripority最小的
    2. 再选择偏移量最大的(偏移量越大获取的master的数据越全)
    3. 选择runid最小的

    缺陷:复制延迟,所有的写操作都在master上执行,然后同步到slave,从master同步到slave的过程中存在一定的延迟,尤其系统繁忙的情况下,slave数量的增长也会加重该问题。

  • Cluster模式

    sentinel模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中。cluster模式的出现就是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。

    每个集群中至少需要三个主数据库才能正常运行,新增节点非常方便:

    • 防止由脑裂造成的集群不可用;
    • 在容错能力相同的情况下,奇数个更节省资源
    多个redis节点网络互联,数据共享
    所有的节点都是一主一从(也可以是一主多从),其中从不提供服务,仅作为备用
    不支持同时处理多个key(如MSET/MGET),因为redis需要把key均匀分布在各个节点上,并发量很高的情况下同时创建key-value会降低性能并导致不可预测的行为
    支持在线增加、删除节点
    客户端可以连接任何一个主节点进行读写
    

Redis集群详解

Cluster模式

集群最核心的功能是数据分区,因此首先介绍数据的分区规则;然后介绍集群实现的细节:通信机制和数据结构;最后以cluster meet(节点握手)、cluster addslots(槽分配)为例,说明节点是如何利用上述数据结构和通信机制实现集群命令的。

数据分区方案

数据分区有顺序分区、哈希分区等,其中哈希分区由于其天然的随机性,使用广泛;集群的分区方案便是哈希分区的一种。

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

衡量数据分区方法好坏的标准有很多,其中比较重要的两个因素是(1)数据分布是否均匀(2)增加或删减节点对数据分布的影响。由于哈希的随机性,哈希分区基本可以保证数据分布均匀;因此在比较哈希分区方案时,重点要看增减节点对数据分布的影响。

(1)哈希取余分区

哈希取余分区思路非常简单:计算key的hash值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要重新计算映射关系,引发大规模数据迁移。

(2)一致性哈希分区

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

img

与哈希取余分区相比,一致性哈希分区将增减节点的影响限制在相邻节点。以上图为例,如果在node1和node2之间增加node5,则只有node2中的一部分数据会迁移到node5;如果去掉node2,则原node2中的数据只会迁移到node4中,只有node4会受影响。

一致性哈希分区的主要问题在于,当节点数量较少时,增加或删减节点,对单个节点的影响可能很大,造成数据的严重不平衡。还是以上图为例,如果去掉node2,node4中的数据由总数据的1/4左右变为1/2左右,与其他节点相比负载过高。

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

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

在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点之间的关系,增加或删除节点对系统的影响很小。仍以上图为例,系统中有4个实际节点,假设为其分配16个槽(0-15); 槽0-3位于node1,4-7位于node2,以此类推。如果此时删除node2,只需要将槽4-7重新分配即可,例如槽4-5分配给node1,槽6分配给node3,槽7分配给node4;可以看出删除node2后,数据在其他节点的分布仍然较为均衡。

槽的数量一般远小于2^32,远大于实际节点的数量;在Redis集群中,槽的数量为16384。

下面这张图很好的总结了Redis集群将数据映射到实际节点的过程:

img

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

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

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

节点通信机制

集群要作为一个整体工作,离不开节点之间的通信。

两个端口

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

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

Gossip协议

节点间通信,按照通信协议可以分为几种类型:单对单、广播、Gossip协议等。重点是广播和Gossip的对比。

  • 广播是指向集群内所有节点发送消息;优点是集群的收敛速度快(集群收敛是指集群内所有节点获得的集群信息是一致的),缺点是每条消息都要发送给所有节点,CPU、带宽等消耗较大。
  • Gossip协议的特点是:在节点数量有限的网络中,每个节点都“随机”的与部分节点通信(并不是真正的随机,而是根据特定的规则选择通信的节点),经过一番杂乱无章的通信,每个节点的状态很快会达到一致。Gossip协议的优点有负载(比广播)低、去中心化、容错性高(因为通信有冗余)等;缺点主要是集群的收敛速度慢。

消息类型

集群中的节点采用固定频率(每秒10次)的定时任务进行通信相关的工作:判断是否需要发送消息及消息类型、确定接收节点、发送消息等。如果集群状态发生了变化,如增减节点、槽状态变更,通过节点间的通信,所有节点会很快得知整个集群的状态,使集群收敛。

节点间发送的消息主要分为5种:meet消息、ping消息、pong消息、fail消息、publish消息。不同的消息类型,通信协议、发送的频率和时机、接收节点的选择等是不同的。

  • MEET消息:在节点握手阶段,当节点收到客户端的CLUSTER MEET命令时,会向新加入的节点发送MEET消息,请求新节点加入到当前集群;新节点收到MEET消息后会回复一个PONG消息。
  • PING消息:集群里每个节点每秒钟会选择部分节点发送PING消息,接收者收到消息后会回复一个PONG消息。PING消息的内容是自身节点和部分其他节点的状态信息;作用是彼此交换信息,以及检测节点是否在线。PING消息使用Gossip协议发送,接收节点的选择兼顾了收敛速度和带宽成本,具体规则如下:
    • 随机找5个节点,在其中选择最久没有通信的1个节点
    • 扫描节点列表,选择最近一次收到PONG消息时间大于cluster_node_timeout/2的所有节点,防止这些节点长时间未更新
  • PONG消息:PONG消息封装了自身状态数据。可以分为两种:
    • 在接到MEET/PING消息后回复的PONG消息
    • 节点向集群广播PONG消息,这样其他节点可以获知该节点的最新信息,例如故障恢复后新的主节点会广播PONG消息。
  • FAIL消息:当一个主节点判断另一个主节点进入FAIL状态时,会向集群广播这一FAIL消息;接收节点会将这一FAIL消息保存起来,便于后续的判断。
  • PUBLISH消息:节点收到PUBLISH命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该PUBLISH命令。

数据结构

节点需要专门的数据结构来存储集群的状态。所谓集群的状态,是一个比较大的概念,包括:集群是否处于上线状态、集群中有哪些节点、节点是否可达、节点的主从状态、槽的分布……

节点为了存储集群状态而提供的数据结构中,最关键的是clusterNode和clusterState结构:前者记录了一个节点的状态,后者记录了集群作为一个整体的状态。

clusterNode

clusterNode结构保存了一个节点的当前状态,包括创建时间、节点id、ip和端口号等。每个节点都会用一个clusterNode结构记录自己的状态,并为集群内所有其他节点都创建一个clusterNode结构来记录节点状态。

下面列举了clusterNode的部分字段,并说明了字段的含义和作用:

typedef struct clusterNode {
    //节点创建时间
    mstime_t ctime;
    //节点id
    char name[REDIS_CLUSTER_NAMELEN];
    //节点的ip和端口号
    char ip[REDIS_IP_STR_LEN];
    int port;
    //节点标识:整型,每个bit都代表了不同状态,如节点的主从状态、是否在线、是否在握手等
    int flags;
    //配置纪元:故障转移时起作用,类似于哨兵的配置纪元
    uint64_t configEpoch;
    //槽在该节点中的分布:占用16384/8个字节,16384个比特;每个比特对应一个槽:比特值为1,则该比特对应的槽在节点中;比特值为0,则该比特对应的槽不在节点中
    unsigned char slots[16384/8];
    //节点中槽的数量
    int numslots;
    
    ...

} clusterNode;

除了上述字段,clusterNode还包含节点连接、主从复制、故障发现和转移需要的信息等。

clusterState

clusterState结构保存了在当前节点视角下,集群所处的状态。主要字段包括:

typedef struct clusterState {
    //自身节点
    clusterNode *myself;
    //配置纪元
    uint64_t currentEpoch;
    //集群状态:在线还是下线
    int state;
    //集群中至少包含一个槽的节点数量
    int size;
    //哈希表,节点名称->clusterNode节点指针
    dict *nodes;
    //槽分布信息:数组的每个元素都是一个指向clusterNode结构的指针;如果槽还没有分配给任何节点,则为NULL
    clusterNode *slots[16384];
    
    ...
        
} clusterState;

除此之外,clusterState还包括故障转移、槽迁移等需要的信息。

集群命令的实现

这一部分将以cluster meet(节点握手)、cluster addslots(槽分配)为例,说明节点是如何利用上述数据结构和通信机制实现集群命令的。

cluster meet

假设要向A节点发送cluster meet命令,将B节点加入到A所在的集群,则A节点收到命令后,执行的操作如下:

  1. A为B创建一个clusterNode结构,并将其添加到clusterState的nodes字典中
  2. A向B发送MEET消息
  3. B收到MEET消息后,会为A创建一个clusterNode结构,并将其添加到clusterState的nodes字典中
  4. B回复A一个PONG消息
  5. A收到B的PONG消息后,便知道B已经成功接收自己的MEET消息
  6. 然后,A向B返回一个PING消息
  7. B收到A的PING消息后,便知道A已经成功接收自己的PONG消息,握手完成
  8. 之后,A通过Gossip协议将B的信息广播给集群内其他节点,其他节点也会与B握手;一段时间后,集群收敛,B成为集群内的一个普通节点

通过上述过程可以发现,集群中两个节点的握手过程与TCP类似,都是三次握手:A向B发送MEET;B向A发送PONG;A向B发送PING。

cluster addslots

集群中槽的分配信息,存储在clusterNode的slots数组和clusterState的slots数组中,两个数组的结构前面已做介绍;二者的区别在于:前者存储的是该节点中分配了哪些槽,后者存储的是集群中所有槽分别分布在哪个节点。

cluster addslots命令接收一个槽或多个槽作为参数,例如在A节点上执行cluster addslots {0..10}命令,是将编号为0-10的槽分配给A节点,具体执行过程如下:

  1. 遍历输入槽,检查它们是否都没有分配,如果有一个槽已分配,命令执行失败;方法是检查输入槽在clusterState.slots[]中对应的值是否为NULL。
  2. 遍历输入槽,将其分配给节点A;方法是修改clusterNode.slots[]中对应的比特为1,以及clusterState.slots[]中对应的指针指向A节点
  3. A节点执行完成后,通过节点通信机制通知其他节点,所有节点都会知道0-10的槽分配给了A节点

客户端访问集群

在集群中,数据分布在不同的节点中,客户端通过某节点访问数据时,数据可能不在该节点中;下面介绍集群是如何处理这个问题的。

redis-cli

当节点收到redis-cli发来的命令(如set/get)时,过程如下:

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

    集群提供的cluster keyslot命令也是使用上述公式实现,如:

img

  1. 判断key所在的槽是否在当前节点:假设key位于第i个槽,clusterState.slots[i]则指向了槽所在的节点,如果clusterState.slots[i]==clusterState.myself,说明槽在当前节点,可以直接在当前节点执行命令;否则,说明槽不在当前节点,则查询槽所在节点的地址(clusterState.slots[i].ip/port),并将其包装到MOVED错误中返回给redis-cli。

  2. redis-cli收到MOVED错误后,根据返回的ip和port重新发送请求。

    下面的例子展示了redis-cli和集群的互动过程:在7000节点中操作key1,但key1所在的槽9189在节点7001中,因此节点返回MOVED错误(包含7001节点的ip和port)给redis-cli,redis-cli重新向7001发起请求。

img

上例中,redis-cli通过-c指定了集群模式,如果没有指定,redis-cli无法处理MOVED错误:

img

Smart客户端

redis-cli这一类客户端称为Dummy客户端,因为它们在执行命令前不知道数据在哪个节点,需要借助MOVED错误重新定向。与Dummy客户端相对应的是Smart客户端。

Smart客户端(以Java的JedisCluster为例)的基本原理:

  1. JedisCluster初始化时,在内部维护slot->node的缓存,方法是连接任一节点,执行cluster slots命令,该命令返回如下所示:

img

  1. 此外,JedisCluster为每个节点创建连接池(即JedisPool)。

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

下面代码演示了如何使用JedisCluster访问集群(未考虑资源释放、异常处理等):

public static void test() {
   Set<HostAndPort> nodes = new HashSet<>();
   nodes.add(new HostAndPort("192.168.72.128", 7000));
   nodes.add(new HostAndPort("192.168.72.128", 7001));
   nodes.add(new HostAndPort("192.168.72.128", 7002));
   nodes.add(new HostAndPort("192.168.72.128", 8000));
   nodes.add(new HostAndPort("192.168.72.128", 8001));
   nodes.add(new HostAndPort("192.168.72.128", 8002));
   JedisCluster cluster = new JedisCluster(nodes);
   System.out.println(cluster.get("key1"));
   cluster.close();
}

注意事项如下:

  1. JedisCluster中已经包含所有节点的连接池,因此JedisCluster要使用单例。
  2. 客户端维护了slot->node映射关系以及为每个节点创建了连接池,当节点数量较多时,应注意客户端内存资源和连接资源的消耗。
  3. Jedis较新版本针对JedisCluster做了一些性能方面的优化,如cluster slots缓存更新和锁阻塞等方面的优化,应尽量使用2.8.2及以上版本的Jedis。

Redis底层实现

单线程模式

底层数据结构

Redis常见问题

缓存穿透

一般是因为黑客攻击,导致redis缓存的命中率下降,从而导致数据库的发昂问量激增。

常见解决方案:

  • 对空值缓存:如果一个查询返回的数据为空(不管数据是否存在),仍然对这个空值进行缓存,设置的过期时间相对较短,最长不超过五分钟
  • 设置可访问的白名单:使用bitmaps类型定义一个可以访问的白名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里面的id进行比较,如果id不在bitmaps里面,进行拦截,不允许访问
  • 采用布隆过滤器:布隆过滤器1970年由布隆提出,实际上是一个很长的二进制向量(位图)和一系列的随机映射函数(哈希函数)。底层原来与bitmaps类似,做了优化。
  • 进行实施监控

缓存击穿

redis服务正常,只不过某个热点key过期,导致与该热点key相关的访问压力都转移到数据库,数据库的瞬时访问量激增。

常见解决方案:

  • 预先设置热门数据
  • 实时调整
  • 使用锁

缓存雪崩

在短时间内,出现大量查询key集中过期的情况。数据库压力变大,导致服务响应时间变慢,导致redis的访问等待,最终导致数据库,服务和redis都失效

常见解决方案:

  • 构建多级缓存架构
  • 使用锁或者队列
  • 设置国旗标志更新缓存
  • 将缓存失效时间分散

Redis使用

Java Redis

基于Redis消息队列实现异步操作

Java Lettus

Redis分布式锁

实现分布式锁的的几种方式

  • 基于数据库实现分布式锁
  • 基于缓存实现(redis等),基于redis的性能最高
  • 基于zookeeper,基于zookeeper实现的可靠性最高

分布式锁的作用及实现

分布式锁之Redis实现

怎样实现redis分布式锁?

Redisson项目介绍

Redis6新特性

acl

IO多线程

redis6开始支持IO多线程,IO多线程值得是苦护短交互部分的网络IO交互处理模块支持多线程,而非执行命令多线程,redis6执行命令依然是单线程的。且IO多线程默认是关闭的,需要用以下配置开启

io-threads-do-read yes
io-threads 4
posted on 2022-02-17 11:36  yaohl0911  阅读(48)  评论(0编辑  收藏  举报