一点一滴成长

导航

Redis集群

 1、主从复制

  ①、概述

    Redis仅部署到一台机子上的话,如果该主机的硬盘出现硬件问题,那么就有可能丢失数据。可以将数据复制多个副本到多台主机上,单台主机出现故障的话也不影响Redis继续提供服务,这就是Redis主从复制。Redis主从复制中分为master主数据库和salve从数据库,不进行额外配置的话默认Redis服务是主数据库,当配置文件中加入"slaveof 主数据库地址 主数据库端口号"后,该数据库就变成了从数据库。从数据库一般是只读的,主数据库是可读可写的,当从数据库启动后主数据库会向其发送自己的快照文件,以后主数据库的写操作会同步给从数据库。因为有多个Redis实例提供服务,所以这种主从运行方式也被称为主从集群。虽然可以通过设置配置文件中slave-read-only为no来使从数据库可写,但除了主数据库宕机需要将从数据库切换为主数据库的情况,一般不会这么设置。

    除了通过配置文件和命令行参数设置从数据库外,还可以在运行时使用"SLAVEOF 127.0.0.1 6000"命令来设置数据库为从数据库,或者修改从数据库的主从关系。SLAVEOF NO ONE命令可以取消当前数据库为从数据库,从而不接受其它数据库的数据同步。也可以把一个从数据库当做主数据来使用,如下所示的B数据库,向B中写入数据只会同步到D和E中:  

   

 

  ②、原理

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

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

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

    配置文件中设置参数min-slaves-max-lag 10 表示,当从数据库最后与主数据库的联系(即发送REPLCONF ACK命令)时间大于10秒的话,就认为与这个从数据库已经失去连接。当与主数据库的失连个数达到min-slaves-to-write设置的值后就不能进行写操作了。

  ③、全量复制和增量复制

     当主从断开连接重连后,在Redis2.8之前的版本中会向主从刚开始第一次连接那样进行复制初始化,即主数据库重新保存快照并传送给从数据库,这称为“全量复制”。Redis2.8中对其进行了改进,其使用“增量复制”策略:当从数据库重连上主数据库后,主数据库只需将断线期间的命令传送给从数据库。

    具体做法是,主数据库会将每一个命令存放到一个积压队列中(又称命令缓冲区或复制积压缓冲区),然后记录命令偏移量,从数据库接收到主数据库传来的命令时,也会记录下该偏移量。当主从连接上以后,从数据库会通过PSYNC命令向主数据库发送最新的偏移量,主数据库收到PSYNC命令后会通过收到的偏移量来判断需要增量复制或全量复制。积压队列本质上是一个固定长度的循环队列,默认大小为1兆,可以通过配置文件的repl-backlog-size参数来设置其大小,其值越大的话允许的主从断线时间越长。另一个配置参数repl-backlog-ttl可以用来设置当所有从数据库与主数据库断开后,经过多长时间就释放积压队列内存,默认值是1小时。

  从数据库会存储主数据库的运行ID(每个Redis实例都拥有一个运行ID,实例重启后该ID会变化), PSYNC命令的格式是"PSYNC 主数据库的运行ID 断开前最新的命令偏移量",主数据库收到PSYNC命令后实际上会首先检查发来的运行ID是否与自己的相同,不同的话会使用全量复制,这样可以防止主数据库在断线期间重启过,从而造成偏移量失效。

  ④、INFO命令

  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的时间间隔。

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

  ⑥、发送限制 

    有可能一个命令会产生体积庞大的回复数据,或者多个命令产生大量的回复数据,这会导致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服务器输出缓冲区的堆积膨胀

  ⑦、从数据库持久化

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

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

  ⑧、MySql的主从复制

  类似Redis,MySql数据库也同样支持主从模式:master节点将写命令顺序写入到binlog中,并推送给slave节点,slave节点连接上master节点后会发送最近一次复制master的binlog的位置,master收到该位置后如果发现与当前位置不同的话会进行主从复制。

 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集群的过程,以创建一个3主3从的集群(Redis集群中最少需要3个主数据库)为例:

    ①、启动6个Redis实例,这些实例启动前需要在配置文件中配置参数cluster - enabled yes,表示开启集群。向开启集群后的Redis服务发送INFO cluster命令的话会收到cluster_enabled:1,其中的1表示集群正常启用。

    ②、向所有节点发送PING命令以确定节点正常(有任一节点无法连接的话创建集群失败),同时发送INFO命令判断节点是否开启了集群并且获取节点的运行ID。

    ③、向每个节点发送CLUSTER MEET ip port命令来使其加入到指定节点所在的集群,比如向第二个节点发送第一个节点的ip和port,向第三个节点发送第二个节点的ip和port...向第一个节点发送第六个节点的ip和port,这样使6个节点归入一个集群。

    ④、分别设置3个主数据库和其对应的3个从数据库(可以向每个从数据库发送CLUSTER REPLICATE 主数据库的运行ID来设置主从)。

    ⑤、为每个主数据库分配插槽,通过CLUSTER ADD SLOTS命令。一个Redis集群中共有16384个插槽,比如可以为主数据库1分配0-5460插槽,为主数据库2分配5461-10922插槽,为主数据库3分配10923-16383插槽。客户端通过插槽来判断使用哪个主数据库,比如对于要插入的键值对{"name", "value"},客户端先对键名"name"计算其CRC16值,然后取其对16384的余数来获得对应的插槽,这样键就可以分配到这个插槽所在的节点。插槽的分配又称为集群的分片,插槽默认位置是连续分配的,比如像前面连续分配插槽到3个节点,那么我们就可以在代码里直接判断使用哪个节点,比如"name"的CRC16值小于5461,那么就使用第一个节点。可以通过CLUSTER SLOTS命令来查看集群中插槽的分配情况,如下所示。

            

  经过上面的步骤后集群创建完成,可以向任一节点发送CLUSTER NODES来获得集群的信息,包括所有节点的运行ID、地址、端口、角色、负责的插槽等。也可以直接使用Redis源码中的redis-trib.rb工具来完成上面的②-⑤步骤,redis-trib.rb是用Ruby编写的,所以需要先安装Ruby,然后执行gem install redis来安装redis-trib.rb(gem是Ruby的包管理器,用来安装和管理Ruby库)。执行redis-trib.rb create --replicas 1 主数据库1ip:port 主数据库2ip:port 主数据库3ip:port 从数据库1ip:port 从数据库2ip:port 从数据库3ip:port,其中--replicas 1表示每个主数据库拥有的从数据库个数为1,然后根据提示输入yes来开始创建集群。

  向现有的集群添加新节点的话,可以向新的节点发送CLUSTER MEET ip port,其中ip和port为集群中任一节点比如A的ip和port,新节点会向其握手,握手成功后A节点使用Gossip协议将新节点的信息通知给集群中的其它成员,这样就使新节点加入到了集群中。新的节点加入集群后有两种选择,一个是通过向其发送CLUSTER REPLICATE命令来让其复制指定主数据库来成为从数据库,另一个是向集群申请分配插槽来以主数据库运行。

  分配插槽的话,有两种情况,一是插槽之前没有被分配过,一是插槽之前被分配过,第一种情况的话使用 CLUSTER ADD SLOTS命令来分配插槽,比如对节点执行 CLUSTER ADD SLOTS 100 101 的话就将100和101插槽分配给了该节点。对于第二种插槽之前被分配过现在想移动到新的节点的情况,可以使用CLUSTER SETSLOT命令,如CLUSTER SETSLOT 0 NODE d4f906940d6871(目标节点运行ID)表示移动0号插槽到指定运行ID的节点。使用CLUSTER SETSLOT命令仅仅是移动了插槽,插槽中的键还需要手动移动到目标节点(可以通过CLUSTER GETKEYSINSLOT命令获得插槽中存在哪些键),通过使用MIGRATE命令来移动键,如MIGRATE 127.0.0.1 6380 abc 0 15999 REPLACE 表示移动当前节点的abc键到主机1270.0.1:6380的目标节点,其中0表示数据库号(因为集群模式只能使用0号数据库,所以该值始终为0),15999表示超时时间,REPLACE表示如果目标节点存在相同的键的话则覆盖,其它的还有COPY表示移动键的时候不从原节点删除该键。

   综上所述,为新节点分配已分配过的插槽的话,不仅需要将该插槽移动到新的节点,还需要将该插槽下所有的键移动到新的节点,如果先移动插槽,这时候客户端获取该插槽下的某一个键的话会去新的节点取,但该键还没有来得及被移动到新的节点,出现读取不到数据的情况。如果先移动键再移动插槽的话,同样有可能存在键已经被移动到了新的节点,但因为插槽还没移动所以还是去原来的节点取该键,然后键不存在取不到数据。这里,我们选择先移动键再移动插槽,但是再次之前先执行两条命令,比如要把0号插槽从A移动到B,那么在B执行 CLUSTER SETSLOT 0 IMPORTING A,在A上执行CLUSTER SETSLOT 0 MIGRATING B,这样,当A上的键已经移动了但还未移动插槽的时候,有0下的键的请求到来A的话,A会向请求返回一个ASK跳转告诉客户端这个键已经在B上了,客户端收到ASK后会向B发送ASKING命令,然后向B请求该键,B收到该键的请求后,如果前面执行了该键的ASKING命令表示该键在B上,直接返回其值。

  节点的插槽很有可能不是连续的,比如后面又新增了节点,该节点负责的插槽会从其它节点移动过来,这样的话怎样判断插槽属于哪个节点呢?实际上我们可以不考虑键对应的插槽以及插槽所在的节点:客户端向集群中任一节点发送键的请求命令后,如果该键不在节点上的话,会返回一个MOVE重定向,告诉客户端这个键所属的插槽在哪个节点上,如MOVDED 12182 127.0.0.1:6382(因为每个节点都知晓当前集群中插槽的分布,如前面所说的CLUSTER SLOTS命令),而很多Redis客户端支持代理MOVED请求(Redis提供的命令行客户端使用-c参数来启用支持自动重定向,如redis-cli -c -p6380),其会自动去新的节点上去执行命令。客户端可以在发现重定向的时候缓存下当前插槽是由哪个节点负责的,下次发起请求的时候可以直接向其发送命令。客户端也可以提前缓存所有插槽所在节点的信息(通过CLUSTER SLOTS命令),然后发起请求的时候先计算键的插槽,然后通过缓存数据获得插槽所在的节点后向该节点发送命令。

  实际上,如果健名中包含大括号且大括号中有内容的话,那么会使用大括号中的文本(被称为键名的有效部分)而不是整个键名来计算键的CRC值,比如{user101}:last.name健名有效部分为user101。这样的话,就可以将一些键的有效部分设置成相同的从而使它们被保存到相同的节点,比如{user101}:last.name和{user101}:first.name虽然键名不同但因为其有效部分相同,所以其会被分配到同一节点。对于涉及多个键的命令,如MGET,只有所有键位于同一节点的时候,Redis集群才能正常支持,所以我们可以使用相同的有效部分来保证在集群中这些键在同一节点上。

  集群中每个节点会定期向其它节点发送PING命令(各节点通过相互发送消息来交换集群中各节点的状态,是在线、疑似下线或已下线),如果指定节点超时没有返回PONG的话会认为该节点疑似下线,并在集群广播该消息,如果某一个节点如X收到了超过半数以上节点发来疑似A下线的消息,X会认为A已经下线,并将该消息广播到集群中,这样A就在整个集群中下线了。集群中的主数据库出现“下线”的话,集群会将该主数据库的一个从数据库转变成主数据库,如果该主数据库没有从数据库的话则默认整个集群会进入下线状态从而停止工作(如果这种情况下想要集群仍能正常工作的话修改配置cluster-require-full-coverage为no)。如果下线的主数据库有多个从数据库的话,选择哪个从数据库作为主数据库同样基于Raft算法:1) 得知主数据库下线的从数据库A向集群中每个节点发送请求,要求它们选取自己为主数据库. 2) 收到请求的节点如果没有选过其它从数据库的话就同意将A设为主数据库. 3) 当A发现集群中有超过一半的节点同意自己成为主数据库的话,A成功成为主数据库,通过命令cluster-require-full-coverage将自己转换为主数据库,并将原来的插槽转给自己。4) 如果有多个从数据库同时参选主数据库的话,可能会出现没有任何节点当选的可能,这时候每个从数据库等待一个随机时间后重新发起参选请求,直到选举成功。

Java Redis客户端

    jedis:Redis客户端,提供了比较全面的Redis命令的支持,Jedis中的方法基本和Redis的API保持着一致,所以是比较纯粹的 Redis 命令客户端。Jedis使用阻塞的I/O,所以其方法调用都是同步的。一般配合连接池使用Jedis来提高效率。

    Lettuce:底层使用非阻塞IO和基于Netty框架的事件驱动通信,所以其方法支持异步调用,提高了服务的效率。Lettuce的API是线程安全的,支持集群。Lettuce是SpringBoot默认的Redis客户端。

    Redisson:与Lettuce一样,Redisson方法同样支持异步调用,API方法也是线程安全的,支持集群。 Redisson提供了多种分布式锁方案,如可重入锁、读写锁、信号量等,同时也提供了很多与Java标准类型十分相似的对象和集合类型,如AtomicLong、AtomicDouble、RBucket(通用对象类型),以及Map、Multimap、Set、SortedSet、List、Queue、Deque等。Redisson对于Redis客户端功能的支持不如Jedis全面,不支持字符串、排序、事务、管道、分区等 Redis 特性。如果项目中除了对基本的数据缓存操作需求以外,还需要用到分布式锁或者想使用更丰富的数据类型(普通客户端只提供Redis支持的字符串、散列、列表、集合、有序集合这五种数据类型),推荐采用Redisson + Lettuce组合方式使用(使用Lettuce弥补Redisson对于基础功能支持的不足)。

    RedisTemplate:Spring中默认使用Spring Data Redis来访问Redis,RedisTemplate是Spring Data Redis框架提供的对Jedis和Lettuce的封装客户端,其本质上还是使用Jedis或Lettuce,spring boot1.x的版本默认采用Jedis实现,spring boot2.x的版本默认采用Lettuce实现。比如我们为了方便切换Jedis和Lettuce,就可以通过RedisTemplate来使用Jedis或Lettuce。使用RedisTemplate时项目中至少需要有Jedis或Lettuce客户端之一的依赖包,否则会报错,RedisTemplate会自动根据项目中依赖的客户端选择底层使用Jedis还是Lettuce。

C++ Redis客户端

    C++中可以使用redis-plus-plus这个客户端库,它支持同步/异步访问Redis、连接池、哨兵模式(主从模式)、集群模式、分布式锁等功能,如下所示。

    连接池初始化后并不会立即创建,Redis++会在向Redis发送命令的时候才创建连接池。

    集群模式下的Redis++会缓存所有插槽所在节点的信息(通过CLUSTER SLOTS命令),所以写入或读取的时候Redis++内部会自动选取合适的节点。当新的master节点加入集群或者现有节点之间的数据迁移的话,会自动更新插槽缓存信息(比如新节点加入集群后,向节点发送命令可能会收到MOVE重定向,Redis++在收到MOVE重定向更新插槽缓存)。使用连接池的话,Redis++会为每个主节点创建一个线程池,主节点对应的从节点(多个从节点的话随机选择一个)创建一个线程池。

    使用哨兵模式的话,redis++通过哨兵节点获得主从节点的地址和端口,主节点和从节点各自有一个连接池(读取的时候随机选择一个从节点建立连接池,以后读取都从这个从节点操作)。连接池中连接断开的话redis++会自动重连,redis发生故障切换的话会自动将连接切换到新的master节点上,slave节点故障的话也会将连接切换到另一个slave节点上。因为一个SentinelRedis中的读取操作都是在同一个从节点上操作,所以可以创建多个Role::SLAVE角色的SentinelRedis对象,读取的时候随机选择一个来使用。

/*普通用法*/
ConnectionOptions connection_options;
connection_options.host = "127.0.0.1";  // Required.
connection_options.port = 6666; // Optional. The default port is 6379.
connection_options.password = "auth";   // Optional. No password by default.
connection_options.db = 1;  // Optional. Use the 0th database by default.
connection_options.connect_timeout = std::chrono::milliseconds(100); //Optional. connect timeout
connection_options.socket_timeout = std::chrono::milliseconds(100); //Optional. If no response is received after a timeout, the operation is return? By default, the timeout is 0ms,That means The operation is non blocking?

Redis redis1(connection_options); //Connect to Redis server with a single connection.
redis1.set("key", "val"); //use Redis::method


/*使用连接池*/
ConnectionPoolOptions pool_options;
pool_options.size = 3; // Pool size
pool_options.wait_timeout = std::chrono::milliseconds(100); //Optional. Max time to wait for a connection,0ms by default, which means wait forever.
pool_options.connection_lifetime = std::chrono::minutes(10);// Optional. Max lifetime of a connection. 

Redis redis2(connection_options, pool_options);
std::vector<OptionalString> result;
redis2.command("mget", "k1", "k2", "k3", std::back_inserter(result)); //use generic command interface,you can parse reply into a STL container


/*连接池+集群*/
onnectionOptions connection_options; //Only need set a master node's host & port,RedisCluster will get other nodes' info automatically(with the 'CLUSTER SLOTS' command)
connection_options.host = "127.0.0.1";
connection_options.port = 7000;
connection_options.password = "auth";

ConnectionPoolOptions pool_options; //For each master node, it maintains a connection pool
pool_options.size = 3;

RedisCluster cluster3(connection_options, pool_options);
cluster3.set("key", "val");
RedisCluster cluster4(connection_options, pool_options, Role::SLAVE); //If you want to read from slave nodes, need to set `Role::SLAVE` option, redis++ will randomly pick a slave node for each master node of the cluster, and create a connection pool for the slave node.
auto val = cluster4.get("key");


/*连接池+哨兵*/
SentinelOptions sentinel_opts;
sentinel_opts.nodes = { {"127.0.0.1", 9000},
                                    {"127.0.0.1", 9001},
                                    {"127.0.0.1", 9002} };  // Required. List of Redis Sentinel nodes.
auto sentinel = std::make_shared<Sentinel>(sentinel_opts);

ConnectionOptions connection_opts;
connection_opts.password = "auth";
connection_opts.connect_timeout = std::chrono::milliseconds(100);   // Required. CANNOT be 0ms
connection_opts.socket_timeout = std::chrono::milliseconds(100); // Required. CANNOT be 0ms

ConnectionPoolOptions pool_opts; //The master node and the slave node each have a connection pool
pool_opts.size = 3;

//redis++ can automatic get Redis master or slave's IP and port,In order to use this feature, you only need to initialize `Redis` object with 3 parts: Sentinel nodes info、master name、role (master or slave).
auto master = Redis(sentinel, "master_name", Role::MASTER, connection_opts, pool_opts);//If a failover occurs, redis++ can automatically get the address of the new master, and refresh all connections in the underlying connection pool. auto slave = Redis(sentinel, "master_name", Role::SLAVE, connection_opts, pool_opts); //If the connection is broken, redis++ will reconnect to this slave automatic. if this slave instance is down,redis++ will randomly connect to another slave. master.set("key", "value"); // Write to master. slave.get("key"); // Read from slave.

    以下为redis++异步操作,回调通知方法的类型为template <typename ReplyType> void(sw::redis::Future<ReplyType>&& fut),因为回调是在redis++中的线程调用的,所以在回调中不应该进行耗时的操作。可以将Redis操作关联的对象作为捕获对象传入,在回调中通知该对象Redis操作已完成。

ConnectionOptions opts;
opts.host = "127.0.0.1";
opts.port = 6379;

ConnectionPoolOptions pool_opts;
pool_opts.size = 3;

auto async_redis = AsyncRedis(opts, pool_opts);

//异步读取
async_redis.get("key", [](Future<OptionalString>&& fut) {
    try {
        auto val = fut.get(); //获取读取到的内容
        if (val)
            cout << *val << endl;
        else
            cout << "not exist" << endl;
    }
    catch (const Error& err) {
        // handle error
    }
    });
//异步写入
async_redis.set("key", "val",
    [](Future<bool>&& fut) {
        try {
            auto set_res = fut.get(); //获取写入结果
        }
        catch (const Error& err) {
            // handle error
        }
    });

SpringBoot中使用Redis集群 

  如果Redis服务是主从模式的话,通过SpringBott的配置文件设置主从的相关信息来使用Redis:

  如果Redis服务是集群模式的话,同样通过配置文件设置集群的相关信息以使用Redis:

 

posted on 2022-03-17 15:57  整鬼专家  阅读(277)  评论(0编辑  收藏  举报