• Redis集群


       Redis服务是一条一条的处理命令的,如果是高并发的客户端,单一的服务有时效率就不高,可以使用Redis集群服务。

       Redis所在主机内存不足的时候会导致内存swap,即操作系统里将内存数据在内存和磁盘间来回换入和换出的机制,Redis触发swap后会影响Redis的主IO线程,大大增加Redis的响应时间,对应方法就是增大物理内存或使用Redis集群。

    1、主从复制

      ①、概述

        Redis集群中分为master主数据库和salve从数据库,默认Redis服务是主数据库,当配置文件中加入"slaveof 主数据库地址 主数据库端口号"后,该数据库就变成了从数据库。从数据库一般是只读的,主数据库是可读写的,当从数据库启动后主数据库会向其发送自己的快照文件,然后主数据库会将所有写操作数据同步给从数据库。虽然可以通过设置配置文件中slave-read-only为no来使从数据库可写,但除了主数据库宕机需要将从数据库切换为主数据库的情况,一般不会这么设置。

        除了通过配置文件和命令行参数设置从数据库外,还可以在运行时使用"SLAVEOF 127.0.0.1 6000"命令来设置主数据库为从数据库,或者修改从数据库的主从关系。SLAVEOF NO ONE命令可以取消当前数据库为从数据库,从而不接受其它数据库的数据同步。  

      ②、原理

        从数据库启动后会连接主数据库(有一个定时每秒检查是否有主数据库可连接的任务,连接失败的话会自动重连),然后向主数据库发送PING命令,收到主数据库的+PONG回复后(超时未收到回复的话断开连接,自动重连)再发送AUTH命令进行密码验证,然后再发送REPLCONF命令来说明自己的端口号,收到主数据库OK回复后,再向其发送PSYNC命令,主数据库收到PSYNC命令后会在后台保存快照,然后将快照数据和在快照期间收到的客户端命令一起传给从数据库,从数据库会将该快照文件替换自己的快照文件,然后加载它。主数据库在数据同步过程中依然可以接受客户端的请求,主从同步结束会将这些命令发送给从数据库。从数据库在数据同步过程中也不会阻塞,默认会用原来的数据处理客户的请求,可以设置slave-serve-stale-data参数为no来使从数据库在同步完成前对客户的的请求回复错误 "SYNC with master in progress"。

      ③、全量复制和增量复制

         主数据库会将收到的客户端的写请求转发给从数据库并备份到命令缓冲区(实际是个环形缓冲区,又称积压队列或复制积压缓冲区),主从复制快照结束后,主数据库会将复制积压缓冲区中命令全部发给从数据库,主数据库会维护一个偏移量来保存已经写入到复制积压缓冲区的数据大小。具体表现为,每当主数据库收到一条写命令,将其发送给从数据库后,也会将其写入到复制积压缓冲区中,然后更新主数据库的偏移量。从数据库也会维护一个偏移量来记录已经收到的命令数据的大小。

         1)、全量复制

         首次从数据库连接主数据库时,主数据库和从数据的偏移量值都为0,进行全量复制:主数据库进行快照,将快照数据全部发送给从数据库,然后将快照同步期间收到的所有写命令发送给从数据库。

         PSYNC命令的格式为 PSYNC 主数据库运行ID 当前偏移量,每个Redis在启动后都会生成一个唯一的run_id (运行ID),Redis重启后, run_id会改变。从数据库第一次连接主数据的时候,主数据库向从数据库发送自己的run_id,当主从数据库断线重连时,主数据库会判断收到的PSYNC命令中的run_id是否和自己的相同,比如断线期间主数据库重启过(run_id就不一样),那么为了防止数据不一致(比如主数据库是因为崩溃重启的),就需要进行全量复制。

         2)、增量复制

         如果断线重连后,run_id一致,那么就判断从数据库的偏移量是否与主数据相等,不相等的话就进行增量复制:将主数据库偏移量减去从数据库偏移量大小的数据发送给从数据库。这里有一个问题,就是当主数据库的偏移量超过复制积压缓冲区的大小的情况,出现这种情况后,我想到的一个方法是下次断线重连的时候,主从数据库的偏移量都设为0,然后进行全量复制,不清楚Redis内部是否采用了这种方法。

      ④、INFO replication命令

      INFO replication命令可以查看主从复制的相关信息,比如role为角色:master/slave,connected_slaves为连接的从数据库个数,master_repl_offset表示主数据库中维护的偏移量,slave_repl_offset为从数据库中维护的偏移量,slave0/1/2...表示连接的从数据库,offset为从数据库每秒向主数据库上报的自己的偏移量,lag为与上次收到REPLCONF ACK的时间间隔,如下为主数据库中的相关信息:

    
    

    127.0.0.1:6379 > INFO replication
    # Replication
    role : master
    ...
    master_repl_offset : 308
    connected_slaves:1
    slave0 : ip = 127.0.0.1, port = 6380, state = online, offset = 308, lag = 1

      如下为从数据库中的相关信息:

    127.0.0.1:6380 > INFO replication
    # Replication
    role : slave
    master_host:127.0.0.1
    master_port:6379
    ......

      ⑤、心跳

        主从复制建立后,会维护两个心跳:一个是主数据库默认每隔10秒向从数据库发送PING命令,另一个是从数据库默认每秒向主数据库发送REPLCONF ACK命令来上报同步数据(已收到命令数据)的偏移量。

        配置参数repl-ping-slave-period 可以控制主数据库向从数据库发送PING的时间间隔。

        配置参数 min-slaves-max-lag 10 表示,当从数据库最后与主数据库的联系(即发送REPLCONF ACK命令)时间大于10秒的话,就认为与这个从数据库已经失去连接。

        配置参数min-slaves-to-write 3 表示,当从数据库的连接小于3个的话,对主数据库进行的写操作会返回错误Not enough good slaves to write。

        配置参数repl-timeout 100 表示,心跳数据(主数据库向从数据库发送PING / 从数据库向主数据库发送偏移量)超时100秒的话就关闭连接,默认值是60秒。比如从数据库配置这个参数为100,那么主数据库超过100秒没有向其发送PING的话就关闭与主数据库的连接?而且从数据库会自动重新连接?

      ⑥、缓冲区大小设置  

        可以通过配置repl-backlog-size参数来设置复制积压缓冲区的大小(默认为1M,可以适当增大,以防止主从数据库断线时间过长或者主从复制的时间过长(主从复制这段时间里又有很多写入命令))。

        配置repl-backlog-ttl参数:所有的从实例与主实例的连接全部断开,主实例需要等待多久才释放复制积压缓冲区,配置该参数以防止从实例意外断开,当再次重连的时候命令缓冲区已经被清空,需要全量复制。默认为1小时?设置该参数为0表示永远不清空?。

        有可能一个命令会产生体积庞大的回复数据,或者多个命令产生大量的回复数据,这会导致Redis服务堆积大量消息到发送缓冲区中,致使大量占用内存,默认Redis配置中会有以下的限制:

    client - output - buffer - limit slave 256mb 64mb 60 //对于发送给从数据库的数据,发送缓冲区中数据大于256M,或者发送缓冲区中数据持续60秒大于64兆的话,关闭从数据的连接(一般在全量复制的时候,比如从数据库启动的时候需要发送整个快照数据的情况下回产生大于设置值的情况)
    client - output - buffer - limit pubsub 8mb 2mb 60  //对于发布/订阅模式,发送缓冲区中数据大于8M,或者发送缓冲区中数据持续60秒大于2兆的话,关闭连接
    client - output - buffer - limit normal 0 0 0 //对于普通客户端来说,不进行限制,因为普通客户端通常采用阻塞式的消息应答模式,通常不会导致Redis服务器输出缓冲区的堆积膨胀

      ⑦、无硬盘复制

          即使我们没有设置配置文件中sava参数以开启RDB持久化方式,主从复制的时候主数据库也会产生快照文件,所以当Redis重启的时候会加载该快照文件,这其实不是我们想要的。另一方面,如果硬盘的性能很差的话,快照文件的读写也会影响Redis性能。可以在配置文件中设置repl-diskless-sync yes来开启无硬盘复制,这样主从复制的时候不会将快照保存到硬盘上,而是直接将其发送给从数据库,避免了硬盘性能瓶颈。

      ⑧、从数据库持久化

        上一篇文章我们说过,Redis持久化会影响性能,但为了防止Redis崩溃后数据清空,必须做持久化。使用Redis集群后,因为有了重启后主从复制机制,所以我们可以不必所有的Redis都开启持久化。比较好的方法是在从数据库中开启持久化,主数据库则不打开持久化,如果从数据库崩溃重启后,会加载持久化的数据,然后主数据库会利用主从复制自动将崩溃期间的数据同步过来,而如果是主数据库崩溃后,因为其没有开始持久化,所以必须严格遵守下面的两步进行:a、使用SLAVEOF NO ONE将从数据库提升为主数据库。b、启动崩溃的主数据库,并设置为从数据库来使用,这样原来主数据库的数据会通过主从复制的全量复制把所有数据同步过来。

        从上面可以看到,开启从数据库持久化后,不能使用Supervisor等进程管理工具来自动重启崩溃后的Redis,而是要手动按照相应的步骤来启动。可以看到,手动维护主数据库或从数据库的重启和数据恢复有些麻烦,可以使用Redis的哨兵模式来实现自动化方案。

     2、哨兵机制

      哨兵是一个单独的进程,它可以监控主、从数据库,当主数据库出现故障时自动将从数据库转换为主数据库,当原来出现故障的主数据库恢复后自动将其变为从数据库。还可以使用多个哨兵,哨兵之间互相监控:

             

      配置哨兵:建立一个配置文件,如sentinel.conf,内容如下,其中mymaster为自定义的要监控的主数据库的名称(该名称只能由小写字母、数字和".-_"这三个字符组成,因为主数据库可能会因为崩溃重启后地址和端口号发生变化,所以哨兵提供了通过主数据库名称获取其当前地址和端口号的命令),后面分别为主数据库的地址、端口号、最低通过票数。port为哨兵程序绑定的端口号,可以不进行单独设置。daemonize设置是否要用守护线程的方式启动redis。当配置了监控主数据库后,哨兵会自动监控其从数据库。启动哨兵进程需要传入前面建立的配置文件,如redis-sentinel  /path/sentinel.conf(Windows中为redis-server.exe sentinel.conf --sentinel)。“最低通过票数”表示最少需要几个哨兵同意才进行redis故障处理,这个值可以设置成 " 哨兵数量/2 + 1"。

    port 26379
    sentinel monitor mymaster 127.0.0.1 6379 2
    daemonize yes

      一个哨兵也可以监控多个Redis主从系统,如下所示的配置文件就表示监视两个主从系统,而且可以为不同的主从系统配置不同的参数。一个Redis主从系统也可以使用多个哨兵,最为稳妥的方案是为每个数据库配置一个哨兵,但这样也会影响Reis的性能。

    sentinel monitor mymaster 127.0.0.1 6379 1
    sentinel monitor othermaster 192.168.1.3 6379 2
    ......
    sentinel down-after-milliseconds mymaster 500 //哨兵每隔500毫秒向Reids节点发送一次PING
    sentinel down - after - milliseconds mymaster 10000 //哨兵每隔1秒向Reids节点发送一次PING,这里虽然设置了10秒但超过1秒的话不起作用,也会是1秒

      哨兵进程启动后,输出的+monitor master......表示监视主数据库成功,+slave slave......表示监视从数据库成功。主数据库崩溃后的指定时间后(默认为30秒,可以配置),哨兵会输出相关的信息,如+sdown......表示主观认为主数据库停止了,+odown......表示客观认为主数据库停止了,然后哨兵会挑选一个从数据库来提升为主数据库以处理故障,输出+try-failover......表示开始进行故障恢复,输出+failover-end......表示完成故障恢复,+switch-master......表示原来的从数据库变成了主数据库的相关信息,+slave slave表示新的主数据库的从数据库的相关信息(这里的从数据库中会包含原来崩溃的主数据库的地址等信息,因为它被设置成了从数据库,即使它还处于故障中未恢复)。当我们重启原来崩溃的主数据库后,哨兵会监控到,输出-sdown表示原来的主数据库已经恢复,输出+convert-to-slave表示该数据库被设置成从数据库的相关信息。

      哨兵启动后,会与主数据库建立两条连接,一条用来订阅该主数据库的频道以获取其它哨兵的信息,另一条用来向主数据库发送INFO等命令。具体为三个动作:

         ①、哨兵启动后订阅主数据库的__sentinel__::hello频道,并向主数据库发送INFO命令来获取主数据库的当前从数据库等相关信息,哨兵对所有的从数据库建立一个连接,然后每隔10秒哨兵向主从数据库发送INFO命令来获得节点的信息,如果主数据库有新的从数据库连接的话就建立与其的连接已监控该新的节点。

         ②、哨兵每2秒会向主数据库和从数据库的__sentinel__::hello频道发送自己的信息(地址、端口号等),因为每个哨兵启动后都会订阅该频道,所以他们都会收到这些信息,当哨兵发现收到的是新的哨兵发来的信息时会与其创建一个连接。

         ③、哨兵默认每1秒会向主数据库、从数据库、其它哨兵发送PING命令(可以通过上面提到的down-after-milliseconds来配置),这用来实现哨兵的监控作用。

         哨兵具体的监控方法:如果对方超过down-after-milliseconds指定的时间没有对PING进行回复的话,哨兵主观认为其已出现故障,然后向其他哨兵发送SENTINEL is-master-down-by-addr命令来询问他们是否也主观认为该节点已下线,如果达到指定的数量(前面说过的“最低通过票数”)的话,选举领头哨兵来对故障进行处理。选举领头哨兵使用Raft算法:发现主数据库客观下线的A哨兵向每个哨兵发送命令来要求对方选自己为领头哨兵(如果有多个哨兵同时发现主数据库下线的话,则会出现没有任何哨兵当选的可能此时每个哨兵会随机等待一个时间再次发起参选),收到要求的哨兵如果没有投票过其他人的话就会投票给A哨兵,当有超过半数并且超过"最低通过票数"数量的哨兵选择了A,则A成为领头哨兵。

         一个哨兵成为领头哨兵后,开始对主数据库下线故障进行处理:挑选一个从数据库为主数据库(先选取优先级最高的(参数slave-priority设置优先级),优先级相同的话选取复制偏移最大的,偏移量相同的话选择ID最小的),具体为领头哨兵向这个从数据库发送SLAVEOFNO ONE命令使其提升为主数据库,而后哨兵向其他的数据库发送SLAVEOF命令来使其成为新主数据库的从数据库,然后就是更新内部信息,将已停止的旧的主数据库更新为新主数据库的从数据库,从而使停止的数据库恢复时自动成为新主数据库的从数据库。

    3、客户端分片

      Redis集群可以分为主从集群(sentinel哨兵集群)和数据分片集群(客户端分片集群)。哨兵集群实际上并不算是真正的集群,因为它只有一个节点可写,而且因为每台主机上的数据都相同,所以单个节点主机的性能会成为整个集群系统的瓶颈,形成木桶效应。一般所谓的Redis集群实际上指的是客户端分片集群,使用它可以包含多个主数据库以缓解写的压力。

      客户端分片一开始为Redis Sharding集群,它将数据分成N个部分来部署到N个主节点上,部署的规则由我们自定义。比如制定一个路由规则,对于不同的 key,使用这个路由规则来保存到对应的节点上,通过这个路由规则可以将数据分布到多个主节点上,每个节点各自存储一部分数据,所有节点数据之和才是全量数据。关于路由规则,业界普遍使用的思想是使用哈希算法将key进行散列,通过hash函数,特定key映射到特定的节点上。 Sharding集群示例如下:

        

       后来出现了Redis Cluster集群,Cluster集群使用Redis规定的路由规则:通过插槽来获得key对应的节点。Cluster集群中也是使用客户端分片技术来使每个节点只保存一部分的数据,比如内存大的主机存储比较多的数据,具体做法为:一开始我们就创建足够多的Redis节点,比如128个,因为初期数据很小,所以可以将这128个节点先放到一两个主机上,等日后数据扩大后,再增加主机,将节点实例迁移到新的主机上,可以将内存高的主机放置多一些的节点。

      Cluster集群中也应该使用主从集群,而且集群中至少需要有三个主数据库。分片集群中的从数据库不能进行读写,只用来作为主数据库宕机后的切换?使用数据分片集群的话,需要将每个节点的cluster-enabled配置选项打开yes,而且只能使用默认的0号数据库,不能使用SELECT命令切换数据库,并且对于涉及多键的命令(如MGET),如果多个键不在一个节点上的话,会提示错误。默认的节点集群信息文件都会存在工作目录的nodes.conf文件下,所以如果多个节点在一台主机上的话需要通过cluster-config-file选项修改该文件名称,或者为每个节点使用不同的工作目录。Cluster集群的示例如下(由于Cluster集群自带哨兵机制,所以少了哨兵节点):

       

      将数据分散保存到多个Redis节点上通过插槽来实现,每个键通过健名有效值的CRC16值与16384求余来得到插槽号,所以插槽号的范围为0-16383,而每个Redis节点负责一部分的插槽,分片集群中的所有主数据库负责一共的16384个插槽,比如一个键健名有效值的CRC16值为100,与16384求余得到100,而0-5000的插槽由A节点负责,所以该键就会保存到A节点。如果一个键名中包含大括号的话,那么大括号内的字符串为健名的有效值,否则整个健名都是有效值,所以我们如果想要两个键保存到同一个节点的话(比如需要使用MGET来获得两个键),可以给这两个健名中设置一个大括号内相同的字符串。通过CLUSTER ADDSLOTS命令来向节点分配指定的插槽,如CLUSTER ADDSLOTS 100 101为为节点分配100和101号的插槽。可以通过CLUSTER SLOTS命令来查看集群中插槽的分配情况,如下所示。插槽被分配后也可以再移动到其它节点,具体方法可以查看《Redis入门指南》中的 "插槽的分配"。

      

       Redis节点的cluster-enabled配置选项打开后,启动Redis后会输出"No cluster configuration found, I'm c219eec9293f32......",其中后面的一串数字为节点在集群中的运行ID,这时候可以向节点发送命令INFO cluster,收到cluster_enalbed:1表示该节点准备集群完毕,下面就需要将指定的节点加入到集群中去,可以使用辅助工具redis-trib.rb的一个命令来实现(具体可以查看《Redis入门指南》),这个命令的大体做法为:连接各指定的节点并发送PING命令以确定节点服务正常,然后发送INFO来获得各节点是否开启了cluster_enabled,然后向各节点发送CLUSTER MEET命令来将这些节点放到一个集群中,然后为集群分配主从数据库,比如有6个节点的话,就会设置3个节点为主数据库,另外3个节点为对应的从数据库,主从分配完成后再为每个主数据库节点分配插槽。集群创建完成后,可以向任一节点发送CLUSTER NODES来获得集群中所有节点的信息,如节点运行ID、地址和端口、插槽范围等。

      前面通过redis-trib.rb创建集群的过程中,主从节点分配的原则是尽量保证每个主数据库在不同的主机上,每个主数据库和其从数据库不在同一主机上,以保证系统的容灾能力。向各节点发送CLUSTER MEET命令来将这些节点放到一个集群中的具体为:向A节点发送CLUSTER MEET ip port命令,其中的ip和port是B节点的地址和端口号,A节点收到后会主动与B进行握手,A与B握手成功后,B会将A节点的信息通过Gossip协议通知给现在集群中的每个节点成员,这样A就加入到了这个集群中。以后想加入新的节点的话,也是通过向该节点发送CLUSTER MEET命令来实现。

      Redis客户端虽然能够计算出某个键所属的插槽(通过健名的CRC16算法),但该插槽所属哪个节点还需要通过CLUSTER SLOTS命令来获得,客户端可以在一开始就通过CLUSTER SLOTS命令来获得各个节点的插槽信息,然后保存起来以供后面的使用,当然这种情况适合插槽顺序分配的情况,比如A节点负责0-4000插槽,B节点负责4001-10000插槽,C节点负责10001-16383插槽。我们也可以直接向集群中的某一个主节点发送对某个键的操作命令,因为如果这个键对应的插槽不在这个节点上的话,节点会返回一个MOVE重定向到正确节点的提示,如下所示,如果客户端开启了支持自动重定向的话,会自动向重定向的节点再次发送原来的命令, 启动客户端的时候通过-c参数来支持自动重定向,如redis-cli -c -p 6000。因为重定向会返回该键所在实际节点的信息,如下所示,所以我们可以将该节点信息保存起来,下次请求命令的时候,先判断该键所属的节点信息是否已经缓存了下来,缓存下来的话就直接向该节点发送命令,从而避免了重定向导致的一条命令要执行两次的问题。

            

       Cluster集群中也有哨兵机制:每个节点定时向其它节点发送PING(默认每隔1秒随机选择5个节点发送?),如果目标节点B超时没有回复的话,则A节点主观认为B节点已下线,然后A会在集群中广播该下线消息,当某个节点C收到半数以上节点发送B主观下线消息的情况,则C认为B已客观下线,并向集群中广播该消息,从而使B在集群中正式下线。如果下线的节点不存在从节点的话,该节点负责的插槽就会失效,默认情况下这会导致整个集群不能使用,可以配置cluster-require-full-coverage 为no来使集群中其它节点正常工作。当某个主节点下线后,如果存在对应的从节点的话,集群就会将从数据库转变成主数据库(并且接管主数据库负责的插槽)来恢复故障,如果一个主节点存在多个从数据库的话,选择哪个从数据库进行转换会使用Raft算法(类似选择领头哨兵):发现主数据库下线的从数据库A向集群中每个节点发送消息来让它们选择自己,收到请求的节点如果没有收到它人的请求的话就会同意A,A发现集群中有超过半数的节点同意的话,则A就会通过SLAVEOF ON ONE命令将自己转换为主数据库,当有多个从数据库同时请求的话,会出现没有任何节点当选的可能,这时候每个节点随机等待一个时间后再次发起请求。

      在Jedis中可以通过JedisSentinelPool来处理Sentinel集群,使用ShardedJedis以及结合缓存池ShardedJedisPool可以处理Sharding集群,JedisCluster用来处理Cluster集群,使用这些类的话,我猜测只需要传入一个主节点地址然后其就可以自动获得集群中的主从地址来使用,在集群中主从切换后也会自动切换节点地址。不过我在网上看到的是ShardedJedisPool和JedisCluster不支持哨兵,Sharding集群的话可以使用一个集成了哨兵的类ShardedJedisSentinelPool,没验证过,不知道真假。

      数据分片集群中对于新节点加入后的数据迁移方案,具体可以参考该文章:Redis集群管理

      相对于客户端分片,现在又有了服务端分片的集群方案,这种方案指的是,路由规则不放在客户端来做,而是在客户端和服务端之间增加一个「中间代理层」,这个代理就是我们经常听到的 Proxy。而数据的路由规则,就放在这个 Proxy 层来维护。这样客户端就无需关心服务端有多少个 Redis 节点了,只需要和这个 Proxy 交互即可。由于增加了代理层,所以服务端分片会降低性能。现在用的最多的还是Sharding集群和Cluster集群。

    Redis客户端

        jedis:Redis客户端,提供了比较全面的Redis命令的支持,Jedis中的方法基本和Redis的API保持着一致。Jedis使用阻塞的I/O,所以其方法调用都是同步的,程序流程要等到sockets处理完I/O才能执行,可以通过连接池来使用Jedis。

        Redisson:Redisson实现了分布式的java数据结构,如Set、List、Map等,这些数据结构基于redis实现。Redisson使用非阻塞的I/O和基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,提供了分布式锁。Redisson对于Redis客户端功能的支持不如Jedis全面。

        Lettuce:高级Redis客户端,基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,支持集群,所以可以操作单个Lettuce连接来完成各种操作。

        Spring中可以添加spring-data-redis来使用Redis。

  • 相关阅读:
    JS eval()小结
    纯JS的ajax实例
    js特效代码-鼠标样式
    JS typeof与instanceof的区别
    linux下网卡绑定
    KVM+VNC 虚拟机远程管理
    smokeping安装
    Python:字符串中引用外部变量的3种方法
    Python:模块学习——os模块
    Python:模块学习——sys模块
  • 原文地址:https://www.cnblogs.com/milanleon/p/15776064.html
Copyright © 2020-2023  润新知