SpringBoot使用Redis 数据访问(单点、集群、哨兵、连接池、Pipline、分布式框架Redisson、解决方案)

 

目录

Redis 文献资料

用Redis编程

Redis模块API

教程和常见问题解答

管理

嵌入式和物联网

故障排除

Redis集群

其他基于Redis的分布式系统

在SSD和永久性存储器上进行Redis

技术指标

资源资源

用例

Redis Standalone单点模式

原理简介

部署实例

Redis Cluster 集群模式

Redis集群101

Redis群集TCP端口

Redis集群和Docker

Redis集群数据分片

Redis Cluster主从模型

Redis群集一致性保证

Redis集群配置参数

创建和使用Redis集群

创建集群

使用create-cluster脚本创建Redis集群

玩集群

使用redis-rb-cluster编写示例应用

重新分簇

编写重新分片操作脚本

一个更有趣的示例应用程序

测试故障转移

手动故障转移

添加一个新节点

将新节点添加为副本

删除节点

副本迁移

升级Redis集群中的节点

迁移到Redis集群

Redis Sentinel哨兵模式

Sentinel支持集群

Sentinel版本

什么是Sentinel

redis sentine中的3个定时任务

主观下线和客观下线

故障转移过程

运行Sentinel

Sentinel的配置

Sentinel的“仲裁会”

配置版本号

配置传播

SDOWN和ODOWN的更多细节

Sentinel之间和Slaves之间的自动发现机制

网络隔离时的一致性

Sentinel状态持久化

无failover时的配置纠正

Slave选举与优先级

Sentinel和Redis身份验证

Sentinel API

Sentinel命令

动态修改Sentinel配置

增加或删除Sentinel

删除旧master或者不可达slave

发布/订阅

TILT 模式

-BUSY状态

Redis 连接池

Jedis

Lettuce

Others

Redis pipeline 

Pipeline原理分析

Pipeline 回调方法示例

Rredis 分布式框架Redisson

Redis代码解决方案

单点配置

集群配置

哨兵配置

参考文章


Redis 文献资料

https://redis.io/documentation

注意:Redis文档还可以在redis-doc github存储库中以原始(计算机友好)格式获得。Redis文档是根据Creative Commons Attribution-ShareAlike 4.0 International许可发布的

用Redis编程

  • Redis实现的命令的完整列表,以及每个命令的详尽文档。
  • 流水线:了解如何一次发送多个命令,以节省往返时间。
  • Redis发布/订阅:Redis是一个快速,稳定的发布/订阅消息传递系统!一探究竟。
  • Redis Lua脚本编制:Redis Lua脚本编制功能文档。
  • 调试Lua脚本:Redis 3.2引入了用于Redis脚本的本地Lua调试器。
  • 内存优化:了解Redis如何使用RAM并学习一些技巧以减少使用RAM。
  • 过期:Redis允许为每个密钥设置不同的生存时间,以便密钥过期后会自动将其从服务器中删除。
  • Redis作为LRU缓存:如何配置和使用Redis作为具有固定内存量和自动退出键的缓存。
  • Redis事务:可以将命令分组在一起,以便将它们作为单个事务执行。
  • 客户端缓存:从版本6开始,Redis支持服务器辅助的客户端缓存。本文档介绍了如何使用它。
  • 大量插入数据:如何在短时间内向Redis实例添加大量现有或生成的数据。
  • 分区:如何在多个Redis实例之间分配数据。
  • 分布式锁:使用Redis实现分布式锁管理器。
  • Redis键空间通知:通过Pub / Sub(Redis 2.8或更高版本)获取键空间事件的通知。
  • 使用Redis 创建二级索引:使用Redis数据结构创建二级索引,组合索引和遍历图。

Redis模块API

  • Redis模块简介。一个开始学习Redis 4.0模块编程的好地方。
  • 实现本机数据类型。扫描的模块实现了看起来像内置数据类型的新数据类型(数据结构等)。本文档介绍了这样做的API。
  • 使用模块阻止操作。这仍然是一种实验性的API,但是它是一种非常强大的API,用于编写可以阻止客户端(而不阻止Redis)并可以在其他线程中执行任务的命令。
  • Redis模块API参考。从顶部注释直接在源代码里面生成src/module.c。包含有关API使用情况的许多低级详细信息。

教程和常见问题解答

管理

  • Redis-cli:了解如何掌握Redis命令行界面,这是您经常使用的一些东西,用于管理,诊断Redis并对其进行故障排除和试验。
  • 配置:如何配置Redis。
  • 复制:设置主副本复制所需的知识。
  • 持久性:配置Redis的持久性时了解您的选择。
  • Redis管理:选定的管理主题。
  • 安全性:Redis安全性概述。
  • Redis访问控制列表:从版本6开始,Redis支持ACL。可以将用户配置为只能运行选定的命令,并且只能访问特定的键模式。
  • 加密:如何加密Redis客户端-服务器通信。
  • 信号处理:Redis如何处理信号。
  • 连接处理:Redis如何处理客户端连接。
  • 高可用性:Redis Sentinel是Redis的官方高可用性解决方案。
  • 延迟监视:Redis集成的延迟监视和报告功能有助于为低延迟工作负载调整Redis实例。
  • 基准测试:了解Redis在不同平台上的运行速度。
  • Redis发行版:Redis开发周期和版本编号。

嵌入式和物联网

故障排除

Redis集群

其他基于Redis的分布式系统

在SSD和永久性存储器上进行Redis

  • Redis Labs的Flash上的Redis通过SSD和永久内存扩展了DRAM容量。

技术指标

资源资源

用例

Redis Standalone单点模式

单点模式顾名思义就是一个redis节点,这样的方式在开发阶段没有任何问题。

原理简介

这是最简单的 redis 部署方案,所有数据存储和读写操作都在同一个 redis 服务上。

这种方式优点很明显:部署简单,无论是部署成本还是运维成本都很低,本地测试时最常见也是最方便的方式。

但同时缺点也很明显:不能实现高可用,也不能应对高并发场景,也无法轻易水平拓展,数据储存量很容易见顶。

部署实例

单节点模式的部署是最简单的,一下是 Linux 系统下部署单节点 redis 的方法:

 

# 下载 Redis 二进制安装包:
wget http://download.redis.io/releases/redis-5.0.4.tar.gz
# 解压二进制包
tar –zxvf redis-5.0.4.tar.gz
# 进入解压文件夹并编译二进制文件
cd redis-5.0.4
make
# 安装
cd src
make test
make install

在 make install 和 make test 的时候可能会遇到下面这个问题:

 

You need tcl 8.5 or newer in order to run the Redis test
make: *** [test] Error 1

这是因为系统中的 TCL 语言版本太低,TCL 语言是一种工具脚本语言,在安装 Redis 的过程中 make test 命令需要用到这个脚本语言,这个时候我们需要升级一下系统中的 TCL 版本:

 

# 下载一个高于 8.5 版本的 TCL 安装包,比如 8.6.8
wget http://downloads.sourceforge.net/tcl/tcl8.6.8-src.tar.gz
# 解压
tar -zxvf tcl8.6.8-src.tar.gz  -C /usr/local/  
# 切换到解压后的源码目录
cd  /usr/local/tcl8.6.8/unix/
# 编译和安装
sudo ./configure  
sudo make  
sudo make install

升级 TCL 到 8.5 版本以后,继续执行之前报错的语句,完成 Redis 的安装,安装完成后用 redis-server -v 验证安装是否成功,若成功输出如下版本信息则代表安装成功:

 

Redis server v=5.0.4

安装成功就可以直接运行了,但是默认配置下是不支持后台运行的,观点命令窗口就会结束 redis 进程,这显然是不行的。所以我们再简单改一下 redis 的配置,让其能够直接后台运行。

 

# 进入到 redis 的安装目录,编辑 redis.conf
vim /usr/redis/redis-5.0.4/redis.conf
# 将 daemonize no 修改成 daemonize yes (使 redis 服务可以在后台运行)

# 在指定配置下运行redis服务
/usr/local/bin/redis-server /usr/redis/redis-5.0.4/redis.conf 
# 查看redis运行情况
ps -ef | grep redis

# 输出
app   21794   1  0 Jan28 ?  03:31:25 ./redis-server *:6379

可以看到 redis 在默认的 6379 端口下运行,配置文件中还有一些可以调整的地方,这里就不一一列举了。那么单节点模式 redis 服务就部署完成了。

开启用户密码验证,日志和后台运行模式请自行研究。

Redis Cluster 集群模式

https://redis.io/topics/cluster-tutorial

本文档是对Redis Cluster的简要介绍,其中没有使用难以理解的分布式系统概念。它提供了有关如何设置集群,测试和操作集群的说明,而没有涉及Redis集群规范中涵盖的细节,而只是从用户的角度描述了系统的行为。

但是,本教程尝试从最终用户的角度提供有关Redis Cluster的可用性和一致性特征的信息,并以一种易于理解的方式进行陈述。

请注意,本教程需要Redis 3.0或更高版本。

如果您计划运行认真的Redis Cluster部署,则建议阅读更正式的规范,即使并非严格要求也是如此。但是,最好从本文档开始,与Redis Cluster一起玩一些时间,然后再阅读规范。

Redis集群101

Redis Cluster提供了一种运行Redis安装的方法,在该安装中,数据会 在多个Redis节点之间自动分片

Redis Cluster 在分区期间还提供了一定程度的可用性,这实际上是在某些节点出现故障或无法通信时继续操作的能力。但是,如果发生较大故障(例如,大多数主服务器不可用时),群集将停止运行。

因此,实际上,Redis Cluster会带来什么?

  • 的能力,自动分割在多个节点之间的数据集
  • 当一部分节点出现故障或无法与集群的其余部分通信继续运行的能力。

Redis群集TCP端口

每个Redis群集节点都需要打开两个TCP连接。用于服务客户端的常规Redis TCP端口,例如6379,再加上在数据端口上加上10000所获得的端口,因此在示例中为16379。

第二个端口用于群集总线,即使用二进制协议的节点到节点通信通道。节点将群集总线用于故障检测,配置更新,故障转移授权等。客户端永远不要尝试与群集总线端口进行通信,而应始终与普通的Redis命令端口进行通信,但是请确保您同时打开防火墙中的两个端口,否则Redis群集节点将无法进行通信。

命令端口和集群总线端口的偏移量是固定的,并且始终为10000。

请注意,对于每个节点,要使Redis群集正常工作,您需要:

  1. 普通客户端通信端口(通常为6379)用于与客户端通信,以向需要访问群集的所有客户端以及所有其他群集节点(使用客户端端口进行密钥迁移)开放。
  2. 群集总线端口(客户端端口+ 10000)必须可从所有其他群集节点访问。

如果您没有同时打开两个TCP端口,则您的群集将无法正常工作。

集群总线使用不同的二进制协议进行节点到节点的数据交换,它更适合于使用很少的带宽和处理时间在节点之间交换信息。

Redis集群和Docker

当前,Redis Cluster不支持NATted环境以及在重新映射IP地址或TCP端口的常规环境中。

Docker使用一种称为端口映射的技术:与该程序认为正在使用的端口相比,在Docker容器内运行的程序可能会使用不同的端口公开。为了在同一服务器上同时使用同一端口运行多个容器,这很有用。

为了使Docker与Redis Cluster兼容,您需要使用Docker 的主机联网模式。请检查Docker文档中--net=host选项以获取更多信息。

Redis集群数据分片

Redis Cluster不使用一致的哈希,而是使用不同形式的分片,其中每个键从概念上讲都是我们称为哈希槽的一部分

Redis集群中有16384个哈希槽,要计算给定密钥的哈希槽,我们只需对密钥的CRC16取模16384。

Redis群集中的每个节点都负责哈希槽的子集,因此,例如,您可能有一个包含3个节点的群集,其中:

  • 节点A包含从0到5500的哈希槽。
  • 节点B包含从5501到11000的哈希槽。
  • 节点C包含从11001到16383的哈希槽。

这样可以轻松添加和删除集群中的节点。例如,如果我想添加一个新节点D,则需要将一些哈希槽从节点A,B,C移到D。类似地,如果我想从集群中删除节点A,则只需移动A所服务的哈希槽到B和C。当节点A为空时,我可以将其从群集中完全删除。

因为将哈希槽从一个节点移动到另一个节点不需要停止操作,所以添加和删除节点或更改节点持有的哈希槽的百分比不需要任何停机时间。

只要单个命令执行(或整个事务或Lua脚本执行)中涉及的所有键都属于同一个哈希槽,Redis Cluster就支持多种键操作。用户可以通过使用称为哈希标签的概念来强制多个键成为同一哈希槽的一部分。

哈希标签记录在Redis群集规范中,但要点是,如果密钥中{}括号之间有一个子字符串,则仅对该字符串内的内容进行哈希处理,因此,例如this{foo}keyanother{foo}key 并保证它们在同一哈希槽中,并且可以在带有多个键作为参数的命令中一起使用。

Redis Cluster主从模型

为了在主节点子集出现故障或无法与大多数节点通信时保持可用,Redis Cluster使用主从模型,其中每个哈希槽具有从1(主节点本身)到N个副本(N个) -1个其他从属节点)。

在具有节点A,B,C的示例集群中,如果节点B失败,则集群将无法继续,因为我们不再有办法为5501-11000范围内的哈希槽提供服务。

但是,在创建集群(或稍后)时,我们向每个主节点添加一个从属节点,以便最终集群由作为主节点的A,B,C和作为从属节点的A1,B1,C1组成,如果节点B发生故障,系统将能够继续。

节点B1复制B,并且B失败,群集将把节点B1提升为新的主节点,并将继续正常运行。

但是请注意,如果节点B和B1同时失败,则Redis Cluster无法继续运行。

Redis群集一致性保证

Redis Cluster无法保证强一致性。实际上,这意味着在某些情况下,Redis Cluster可能会丢失系统认可给客户端的写入。

Redis Cluster可能丢失写入的第一个原因是因为它使用异步复制。这意味着在写入期间发生以下情况:

  • 您的客户写信给主B。
  • 主B向您的客户回复OK。
  • 主机B将写操作传播到其从机B1,B2和B3。

如您所见,B在回复客户端之前不会等待B1,B2,B3的确认,因为这会对Redis造成极高的延迟,因此,如果您的客户端写了一些东西,B会确认写,但是在崩溃之前崩溃由于能够将写操作发送到其从属设备,一个从属设备(未接收到写操作)可以被提升为主设备,从而永远丢失写操作。

这与配置为每秒将数据刷新到磁盘的大多数数据库所发生的情况非常相似,因此由于过去使用不涉及分布式系统的传统数据库系统的经验,您已经可以对此进行推理。同样,您可以通过强制数据库在答复客户端之前刷新磁盘上的数据来提高一致性,但这通常会导致性能过低。在Redis Cluster的情况下,这相当于同步复制。

基本上,需要在性能和一致性之间进行权衡。

Redis Cluster在绝对需要时支持通过WAIT命令实现的同步写入,这使得丢失写入的可能性大大降低,但是请注意,即使使用同步复制,Redis Cluster也不实现强一致性:在更复杂的情况下,总是有可能的无法将写操作的从属设备选为主设备的失败情况。

还有另一种值得注意的情况,Redis Cluster将丢失写操作,这种情况发生在网络分区期间,在该分区中,客户端与少数实例(至少包括主实例)隔离。

以我们的6个节点集群为例,该集群由A,B,C,A1,B1,C1组成,具有3个主节点和3个从节点。还有一个客户,我们将其称为Z1。

发生分区后,可能在分区的一侧有A,C,A1,B1,C1,而在另一侧有B和Z1。

Z1仍然能够写入B,它将接受其写入。如果分区在很短的时间内恢复正常,则群集将继续正常运行。但是,如果分区持续的时间足以使B1升级为该分区的多数端的主服务器,则Z1向B发送的写操作将丢失。

请注意,Z1能够发送到B的写入量有一个最大的窗口:如果已经有足够的时间让分区的多数派选举出一个从属服务器为主节点,那么少数派的每个主节点都将停止接受写入操作。

此时间量是Redis Cluster的非常重要的配置指令,称为节点超时

节点超时结束后,将主节点视为故障节点,并且可以用其副本之一替换该主节点。类似地,在没有主节点能够感知大多数其他主节点的节点超时之后,它进入错误状态并停止接受写操作。

Redis集群配置参数

我们将创建一个示例集群部署。在继续之前,让我们介绍Redis Cluster在redis.conf文件中引入的配置参数。当您继续阅读时,有些会很明显,有些会更清晰。

  • cluster-enabled<yes/no>:如果是,则在特定Redis实例中启用Redis Cluster支持。否则,实例将像往常一样作为独立实例启动。
  • cluster-config-file<filename>:请注意,尽管有此选项的名称,但它不是用户可编辑的配置文件,而是Redis Cluster节点每次发生更改时都会自动持久保存集群配置的文件(状态,基本上是),为了能够在启动时重新阅读它。该文件列出了诸如集群中其他节点之类的东西,它们的状态,持久变量等等。通常,由于收到某些消息,此文件将被重写并刷新到磁盘上。
  • cluster-node-timeout<milliseconds>:Redis群集节点不可用的最长时间(不将其视为失败)。如果主节点无法访问的时间超过指定的时间量,则其主节点将对其进行故障转移。此参数控制Redis Cluster中的其他重要事项。值得注意的是,在指定的时间内无法到达大多数主节点的每个节点都将停止接受查询。
  • cluster-slave-validity-factor<factor>:如果设置为零,则从服务器将始终尝试对主服务器进行故障转移,而不管主服务器和从服务器之间的链接保持断开状态的时间长短。如果该值为正,则将最大断开时间计算为节点超时值乘以此选项提供的因子,如果节点是从节点,则如果断开主链接的时间超过指定的时间,它将不会尝试启动故障转移。例如,如果节点超时设置为5秒,而有效性因子设置为10,则从服务器与主服务器断开连接超过50秒将不会尝试对其主服务器进行故障转移。请注意,如果没有从属可以对其进行故障转移,则任何非零的值都可能导致Redis群集在主服务器发生故障后不可用。在这种情况下,只有当原始主服务器重新加入集群后,集群才会恢复可用。
  • cluster-migration-barrier<count>:主机将保持连接的最小数量的从机,以便另一个从机迁移到不再被任何从机覆盖的主机。有关更多信息,请参见本教程中有关副本迁移的相应部分。
  • cluster-require-full-coverage<yes/no>:如果设置为yes,默认情况下,如果某个节点没有覆盖一定比例的密钥空间,集群将停止接受写入。如果该选项设置为no,即使仅可以处理有关密钥子集的请求,群集仍将提供查询。
  • cluster-allow-reads-when-down<yes/no>:如果将其设置为no(默认情况下为默认值),则当Redis群集被标记为失败或节点无法到达时,该节点将停止为所有流量提供服务达不到法定人数或完全覆盖。这样可以防止从不知道群集更改的节点读取可能不一致的数据。可以将此选项设置为yes,以允许在失败状态期间从节点进行读取,这对于希望优先考虑读取可用性但仍希望防止写入不一致的应用程序很有用。当仅使用一个或两个分片的Redis Cluster时,也可以使用它,因为它允许节点在主服务器发生故障但无法进行自动故障转移时继续为写入提供服务。

创建和使用Redis集群

注意:手动部署Redis群集,了解其某些操作方面非常重要。但是,如果要尽快建立集群并运行(尽快),请跳过本节和下一节,直接转到使用create-cluster脚本创建Redis集群

要创建集群,我们需要做的第一件事就是让一些空的Redis实例在集群模式下运行。基本上,这意味着不使用常规Redis实例创建集群,因为需要配置特殊模式,以便Redis实例将启用集群特定的功能和命令。

以下是最小的Redis群集配置文件:

port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes

如您所见,启用集群模式的只是cluster-enabled 指令。每个实例还包含该节点的配置存储位置的文件路径,默认情况下为nodes.conf。该文件永远不会被人类触摸;它只是由Redis Cluster实例在启动时生成,并在需要时进行更新。

请注意,按预期工作的最小群集要求至少包含三个主节点。对于您的第一个测试,强烈建议启动一个包含三个主节点和三个从节点的六个节点群集。

为此,输入一个新目录,并创建以下目录,该目录以我们将在任何给定目录中运行的实例的端口号命名。

就像是:

mkdir cluster-test
cd cluster-test
mkdir 7000 7001 7002 7003 7004 7005

redis.conf在每个目录(从7000到7005)中创建一个文件。作为配置文件的模板,只需使用上面的小示例,但请确保7000根据目录名称将端口号替换为正确的端口号。

现在,将您的redis-server可执行文件(从GitHub不稳定分支中的最新源编译)复制到cluster-test目录中,最后在您喜欢的终端应用程序中打开6个终端选项卡。

像这样启动每个实例,每个选项卡一个:

cd 7000
../redis-server ./redis.conf

从每个实例的日志中可以看到,由于不nodes.conf存在文件,因此每个节点都会为其分配一个新的ID。

[82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1

此ID将由该特定实例永久使用,以使该实例在群集的上下文中具有唯一的名称。每个节点都使用此ID而不是IP或端口记住其他每个节点。IP地址和端口可能会更改,但是唯一的节点标识符在节点的整个生命周期中都不会改变。我们将此标识符简称为Node ID

创建集群

现在我们有许多实例正在运行,我们需要通过将一些有意义的配置写入节点来创建集群。

如果您使用的是Redis 5,这很容易完成,这是因为嵌入到中的Redis Cluster命令行实用程序为我们提供了帮助,该实用程序redis-cli可用于创建新集群,检查或重新分片现有集群等。

对于Redis版本3或4,有一个称为的旧工具redis-trib.rb,它非常相似。您可以src在Redis源代码分发的目录中找到它。您需要安装redisgem才能运行redis-trib

gem install redis

第一个示例,即集群创建,将redis-cli在Redis 5和redis-tribRedis 3和4中同时使用。但是,所有下一个示例将仅使用redis-cli,因为如您所见,语法非常相似,因此您可以轻松地通过redis-trib.rb help获取有关旧语法的信息,将一个命令行更改为另一命令行。重要:请注意,如果需要,可以redis-cli对Redis 4群集使用Redis 5 ,而不会出现问题。

redis-cli只需键入以下内容即可为Redis 5创建集群:

redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1

使用redis-trib.rb用于Redis的4或3型:

./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005

此处使用的命令是create,因为我们要创建一个新集群。该选项--cluster-replicas 1意味着我们希望为每个创建的主机都提供一个从机。其他参数是我要用于创建新集群的实例的地址列表。

显然,满足我们要求的唯一设置是创建具有3个主设备和3个从设备的集群。

Redis-cli将为您提出配置。输入yes接受建议的配置。集群将被配置并加入,这意味着实例将被引导成彼此对话。最后,如果一切顺利,您将看到如下消息:

[OK] All 16384 slots covered

这意味着至少有一个主实例为16384个可用插槽中的每一个提供服务。

使用create-cluster脚本创建Redis集群

如果您不想如上所述通过手动配置和执行各个实例来创建Redis集群,则可以使用一个简单得多的系统(但是您将不会学到相同数量的操作细节)。

只需检查utils/create-clusterRedis发行版中的目录即可。create-cluster内部有一个脚本(名称与包含在其中的目录相同),它是一个简单的bash脚本。为了启动具有3个主节点和3个从节点的6节点群集,只需键入以下命令:

  1. create-cluster start
  2. create-cluster create

yesredis-cli实用程序希望您接受群集布局时,请在步骤2中进行答复。

现在,您可以与群集进行交互,默认情况下,第一个节点将从端口30001开始。完成后,使用以下命令停止集群:

  1. create-cluster stop

请阅读README此目录内的内容,以获取有关如何运行脚本的更多信息。

玩集群

在此阶段,Redis Cluster的问题之一是缺少客户端库的实现。

我知道以下实现:

  • redis-rb-cluster是我(@antirez)编写的Ruby实现,可作为其他语言的参考。它是原始redis-rb的简单包装,实现了最小语义以有效地与集群通信。
  • redis-py-cluster redis-rb-cluster到Python的端口。支持大多数redis-py功能。正在积极发展中。
  • 流行的Predis支持Redis Cluster,该支持最近已更新并且正在积极开发中。
  • 使用最频繁的 Java客户端Jedis最近添加了对Redis Cluster的支持,请参阅项目README中的Jedis Cluster部分。
  • StackExchange.Redis提供对C#的支持(并且应该可以与大多数.NET语言,VB,F#等配合使用)
  • thunk-redis提供对Node.js和io.js的支持,它是基于thunk / promise的redis客户端,具有管道和集群。
  • redis-go-cluster是使用Redigo库客户端作为基本客户端的Go语言Redis集群的实现。通过结果聚合实现MGET / MSET。
  • ioredis是流行的Node.js客户端,为Redis Cluster提供了强大的支持。
  • redis-cli-c交换机启动时,该实用程序将实现基本的群集支持。

测试Redis Cluster的一种简单方法是尝试上述任何客户端,或者只是redis-cli命令行实用程序。以下是使用后者进行交互的示例:

$ redis-cli -c -p 7000
redis 127.0.0.1:7000> set foo bar
-> Redirected to slot [12182] located at 127.0.0.1:7002
OK
redis 127.0.0.1:7002> set hello world
-> Redirected to slot [866] located at 127.0.0.1:7000
OK
redis 127.0.0.1:7000> get foo
-> Redirected to slot [12182] located at 127.0.0.1:7002
"bar"
redis 127.0.0.1:7000> get hello
-> Redirected to slot [866] located at 127.0.0.1:7000
"world"

注意:如果使用脚本创建集群,则节点可能会侦听不同的端口,默认情况下从30001开始。

redis-cli集群支持非常基础,因此它始终使用Redis Cluster节点能够将客户端重定向到正确节点的事实。认真的客户可以做得更好,并且可以在哈希槽和节点地址之间缓存映射,以直接使用与正确节点的正确连接。仅在集群配置中发生某些更改时(例如,在故障转移之后或系统管理员通过添加或删除节点来更改集群布局之后),才会刷新映射。

使用redis-rb-cluster编写示例应用

在继续展示如何操作Redis Cluster,执行故障转移或重新分片之类的操作之前,我们需要创建一些示例应用程序,或者至少要能够理解简单的Redis Cluster客户端交互的语义。

通过这种方式,我们可以运行一个示例,同时尝试使节点发生故障或开始重新分片,以了解Redis Cluster在现实条件下的行为。观察没有人向集群写入数据时发生的情况不是很有帮助。

本节介绍了redis-rb-cluster的一些基本用法,其中 显示了两个示例。首先是以下内容,它是 example.rb redis-rb-cluster发行版中的文件:

   1  require './cluster'
   2
   3  if ARGV.length != 2
   4      startup_nodes = [
   5          {:host => "127.0.0.1", :port => 7000},
   6          {:host => "127.0.0.1", :port => 7001}
   7      ]
   8  else
   9      startup_nodes = [
  10          {:host => ARGV[0], :port => ARGV[1].to_i}
  11      ]
  12  end
  13
  14  rc = RedisCluster.new(startup_nodes,32,:timeout => 0.1)
  15
  16  last = false
  17
  18  while not last
  19      begin
  20          last = rc.get("__last__")
  21          last = 0 if !last
  22      rescue => e
  23          puts "error #{e.to_s}"
  24          sleep 1
  25      end
  26  end
  27
  28  ((last.to_i+1)..1000000000).each{|x|
  29      begin
  30          rc.set("foo#{x}",x)
  31          puts rc.get("foo#{x}")
  32          rc.set("__last__",x)
  33      rescue => e
  34          puts "error #{e.to_s}"
  35      end
  36      sleep 0.1
  37  }

该应用程序做了一件非常简单的事情,它将一个键的形式设置foo<number>number,一个接一个地设置。因此,如果您运行该程序,结果将是以下命令流:

  • SET foo0 0
  • 设置foo1 1
  • SET foo2 2
  • 依此类推...

该程序看起来比通常应该复杂,因为它被设计为在屏幕上显示错误而不是异常退出,因此对集群执行的每个操作都由begin rescue块包装。

14行是程序中的第一个有趣的行。它创建Redis Cluster对象,使用启动节点列表作为参数,允许该对象与不同节点建立的最大连接数,最后将给定操作后的超时视为失败。

启动节点不必是集群的所有节点。重要的是至少一个节点是可到达的。还要注意,只要能够与第一个节点连接,redis-rb-cluster就会更新此启动节点列表。您应该期望任何其他认真的客户都采取这种行为。

现在我们已经将Redis Cluster对象实例存储在rc变量中,我们可以像使用普通的Redis对象实例一样使用该对象了。

这恰好发生在第18至26行中:重新启动示例时,我们不想再以开头foo0,因此我们将计数器存储在Redis本身中。上面的代码旨在读取此计数器,或者如果不存在该计数器,则将其分配为零。

但是请注意这是一个while循环,因为即使集群关闭并返回错误,我们也要一次又一次尝试。正常的应用程序不需要那么小心。

28和37之间的行开始主循环,在该循环中设置了键或显示了错误。

注意sleep循环结束时的调用。在测试中,如果您想尽可能快地写入集群,则可以消除睡眠(相对于事实上这是一个没有真正的并行性的繁忙循环,因此,您通常会在每秒获得10k ops的事实)最好的条件)。

通常,为了使示例应用程序更容易被人遵循,写入速度会减慢。

启动应用程序将产生以下输出:

ruby ./example.rb
1
2
3
4
5
6
7
8
9
^C (I stopped the program here)

这不是一个非常有趣的程序,我们稍后会使用更好的程序,但是我们已经可以看到程序运行时在重新分片期间发生了什么。

重新分簇

现在,我们准备尝试集群重新分片。为此,请保持example.rb程序运行,以便您查看对程序运行是否有影响。另外,您可能需要注释该sleep 调用,以在重新分片期间增加一些更严重的写负载。

重新分片基本上意味着将哈希槽从一组节点移动到另一组节点,并且像群集创建一样,它是使用redis-cli实用程序完成的。

要开始重新分片,只需键入:

redis-cli --cluster reshard 127.0.0.1:7000

您只需要指定一个节点,redis-cli将自动找到其他节点。

当前redis-cli仅能在管理员支持下重新分片,您不能仅仅说将5%的插槽从该节点移到另一个节点(但这实现起来很简单)。因此,它始于问题。首先是您想做多少重分片:

How many slots do you want to move (from 1 to 16384)?

我们可以尝试重新分派1000个哈希槽,如果该示例仍在运行时没有睡眠调用,则该哈希槽应已包含少量的密钥。

然后redis-cli需要知道重新分片的目标是什么,即将接收哈希槽的节点。我将使用第一个主节点,即127.0.0.1:7000,但是我需要指定实例的节点ID。它已由redis-cli打印在列表中,但是如果需要,我总是可以使用以下命令找到节点的ID:

$ redis-cli -p 7000 cluster nodes | grep myself
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460

好的,所以我的目标节点是97a3a64667477371c4479320d683e4c8db5858b1。

现在,您将被询问要从哪些节点获取这些密钥。我只是键入all以从所有其他主节点获取一些哈希槽。

最终确认后,您会看到一条消息,表明redis-cli将要从一个节点移动到另一个节点,并且将从一侧移动到另一侧的每个实际密钥都会打印一个点。

在重新分片过程中,您应该能够看到示例程序运行不受影响。如果需要,您可以在重新分片期间停止并重新启动它多次。

重新分片结束时,可以使用以下命令测试群集的运行状况:

redis-cli --cluster check 127.0.0.1:7000

所有插槽都将照常被覆盖,但是这次127.0.0.1:7000的主机将具有更多的哈希插槽,大约为6461。

编写重新分片操作脚本

重新分片可以自动执行,而无需以交互方式手动输入参数。可以使用如下命令行来实现:

redis-cli reshard <host>:<port> --cluster-from <node-id> --cluster-to <node-id> --cluster-slots <number of slots> --cluster-yes

如果您可能经常重新分片,这可以建立一些自动机制,但是目前还没有办法redis-cli自动检查集群中各个节点的密钥分布并根据需要智能地移动插槽,从而自动重新平衡集群。将来会添加此功能。

一个更有趣的示例应用程序

我们早期编写的示例应用程序不是很好。它以一种简单的方式写入集群,甚至无需检查写入的内容是否正确。

从我们的观点来看,集群接收写入可能只是总是写的关键foo,以42每一个动作,我们不会在所有通知。

因此,在redis-rb-cluster存储库中,有一个更有趣的应用程序,称为consistency-test.rb。它使用一组计数器,默认为1000,并发送INCR命令以增加计数器。

但是,该应用程序不仅可以编写,还可以做两件事:

  • 当使用INCR更新计数器时,应用程序会记住该写入。
  • 它还在每次写入之前读取一个随机计数器,并检查该值是否符合我们的预期,并将其与内存中的值进行比较。

这意味着该应用程序是一个简单的一致性检查程序,它可以告诉您集群是否丢失了一些写操作,或者是否接受了我们未收到确认的写操作。在第一种情况下,我们将看到一个计数器的值小于我们记住的值,而在第二种情况下,该值将更大。

运行一致性测试应用程序每秒产生一行输出:

$ ruby consistency-test.rb
925 R (0 err) | 925 W (0 err) |
5030 R (0 err) | 5030 W (0 err) |
9261 R (0 err) | 9261 W (0 err) |
13517 R (0 err) | 13517 W (0 err) |
17780 R (0 err) | 17780 W (0 err) |
22025 R (0 err) | 22025 W (0 err) |
25818 R (0 err) | 25818 W (0 err) |

该行显示执行的R eads和W rite 的数目,以及错误的数目(由于系统不可用,因此由于错误而无法接受查询)。

如果发现不一致,则将新行添加到输出中。例如,如果我在程序运行时手动重置计数器,则会发生这种情况:

$ redis-cli -h 127.0.0.1 -p 7000 set key_217 0
OK

(in the other tab I see...)

94774 R (0 err) | 94774 W (0 err) |
98821 R (0 err) | 98821 W (0 err) |
102886 R (0 err) | 102886 W (0 err) | 114 lost |
107046 R (0 err) | 107046 W (0 err) | 114 lost |

当我将计数器设置为0时,实际值为114,因此程序报告114丢失写操作(群集无法记住的INCR命令)。

该程序作为测试用例更加有趣,因此我们将使用它来测试Redis Cluster故障转移。

测试故障转移

注意:在此测试期间,应打开一个标签,并运行一致性测试应用程序。

为了触发故障转移,我们可以做的最简单的事情(也就是在分布式系统中可能发生的语义上最简单的失败)是使单个进程崩溃,在我们的例子中是单个主机崩溃。

我们可以使用以下命令来识别母版并将其崩溃:

$ redis-cli -p 7000 cluster nodes | grep master
3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385482984082 0 connected 5960-10921
2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 master - 0 1385482983582 0 connected 11423-16383
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5959 10922-11422

好的,所以7000、7001和7002是大师。让我们使用DEBUG SEGFAULT命令使节点7002崩溃 :

$ redis-cli -p 7002 debug segfault
Error: Server closed the connection

现在,我们可以查看一致性测试的输出以查看其报告的内容。

18849 R (0 err) | 18849 W (0 err) |
23151 R (0 err) | 23151 W (0 err) |
27302 R (0 err) | 27302 W (0 err) |

... many error warnings here ...

29659 R (578 err) | 29660 W (577 err) |
33749 R (578 err) | 33750 W (577 err) |
37918 R (578 err) | 37919 W (577 err) |
42077 R (578 err) | 42078 W (577 err) |

如您所见,在故障转移期间,系统无法接受578次读取和577次写入,但是在数据库中未创建任何不一致的地方。这听起来可能是意外的,因为在本教程的第一部分中,我们说过Redis Cluster在故障转移期间会丢失写入,因为它使用异步复制。我们没有说的是,这不太可能发生,因为Redis大约同时将回复发送到客户端,并将命令复制到从属服务器,因此丢失数据的窗口很小。但是,很难触发这一事实并不意味着它不可能,因此这不会改变Redis集群提供的一致性保证。

现在,我们可以检查故障转移后的集群设置是什么(请注意,在此期间,我重新启动了崩溃的实例,以便它作为从属重新加入集群):

$ redis-cli -p 7000 cluster nodes
3fc783611028b1707fd65345e763befb36454d73 127.0.0.1:7004 slave 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 0 1385503418521 0 connected
a211e242fc6b22a9427fed61285e85892fa04e08 127.0.0.1:7003 slave 97a3a64667477371c4479320d683e4c8db5858b1 0 1385503419023 0 connected
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5959 10922-11422
3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7005 master - 0 1385503419023 3 connected 11423-16383
3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385503417005 0 connected 5960-10921
2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385503418016 3 connected

现在,主机在端口7000、7001和7005上运行。以前是主机(即在端口7002上运行的Redis实例)现在是7005的从属。

CLUSTER NODES命令的输出可能看起来令人生畏,但实际上非常简单,并且由以下标记组成:

  • 节点编号
  • ip:端口
  • 标志:主人,奴隶,我自己,失败,...
  • 如果是从属节点,则为主节点的节点ID
  • 最后一个未决PING的时间仍在等待答复。
  • 最后收到的PONG时间。
  • 此节点的配置时期(请参阅集群规范)。
  • 到该节点的链接状态。
  • 插槽已投放...

手动故障转移

有时,强制故障转移而实际上不会在主服务器上引起任何问题很有用。例如,为了升级主节点之一的Redis进程,最好对其进行故障转移,以将其转变为对可用性的影响最小的从节点。

Redis Cluster支持使用CLUSTER FAILOVER 命令进行手动故障转移,该命令必须在要进行故障转移的主服务器的从服务器之一中执行。

手动故障转移是特殊的,与实际的主服务器故障导致的故障转移相比,它更安全,因为它们的发生方式避免了过程中的数据丢失,只有在系统确定新的主服务器将客户端从原始主服务器切换到新的主服务器后,才能进行此操作主服务器处理了旧的所有复制流。

这是您在执行手动故障转移时在从属日志中看到的内容:

# Manual failover user request accepted.
# Received replication offset for paused master manual failover: 347540
# All master replication stream processed, manual failover can start.
# Start of election delayed for 0 milliseconds (rank #0, offset 347540).
# Starting a failover election for epoch 7545.
# Failover election won: I'm the new master.

基本上,连接到我们要进行故障转移的主服务器的客户端都已停止。同时,主服务器将其复制偏移发送到从服务器,从服务器等待从其一侧到达偏移。当达到复制偏移量时,故障转移开始,并且将向旧的主服务器通知配置切换。当客户端在旧的主服务器上解除阻止时,它们将被重定向到新的主服务器。

添加一个新节点

添加新节点基本上是以下过程:添加一个空节点,然后将一些数据移入该节点(如果它是新的主节点),或者告诉它设置为已知节点的副本(如果它是从节点的话)。

从添加新的主实例开始,我们将展示两者。

在这两种情况下,要执行的第一步都是添加一个空节点

这就像在端口7006中启动一个新节点(现有6个节点已经从7000到7005使用新节点)一样简单,除了端口号以外,其他节点都使用相同的配置,因此您应该按顺序进行操作符合我们先前节点使用的设置:

  • 在终端应用程序中创建一个新选项卡。
  • 输入cluster-test目录。
  • 创建一个名为的目录7006
  • 在内部创建一个redis.conf文件,类似于用于其他节点的文件,但使用7006作为端口号。
  • 最后用以下命令启动服务器 ../redis-server ./redis.conf

此时服务器应该正在运行。

现在,我们可以像往常一样使用redis-cli来将节点添加到现有集群中。

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000

如您所见,我使用add-node命令将新节点的地址指定为第一个参数,并将集群中随机存在的节点的地址指定为第二个参数。

实际上,redis-cli在这里对我们没有什么帮助,它只是向节点发送了CLUSTER MEET消息,这也可以手动完成。但是redis-cli也会在运行之前检查集群的状态,因此,即使您知道内部结构如何运行,也始终通过redis-cli执行集群操作是一个好主意。

现在,我们可以连接到新节点,以查看它是否确实加入了集群:

redis 127.0.0.1:7006> cluster nodes
3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385543178575 0 connected 5960-10921
3fc783611028b1707fd65345e763befb36454d73 127.0.0.1:7004 slave 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 0 1385543179583 0 connected
f093c80dde814da99c5cf72a7dd01590792b783b :0 myself,master - 0 0 0 connected
2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543178072 3 connected
a211e242fc6b22a9427fed61285e85892fa04e08 127.0.0.1:7003 slave 97a3a64667477371c4479320d683e4c8db5858b1 0 1385543178575 0 connected
97a3a64667477371c4479320d683e4c8db5858b1 127.0.0.1:7000 master - 0 1385543179080 0 connected 0-5959 10922-11422
3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7005 master - 0 1385543177568 3 connected 11423-16383

请注意,由于此节点已经连接到群集,因此它已经能够正确重定向客户端查询,并且通常来说是群集的一部分。但是,与其他大师相比,它有两个特点:

  • 由于没有分配的哈希槽,因此不保存任何数据。
  • 因为它是没有分配插槽的主机,所以当从机要成为主机时,它不会参与选举过程。

现在可以使用的重新分片功能为此节点分配哈希槽redis-cli。基本上没有必要像我们在上一节中所做的那样显示这一点,没有区别,只是将空节点作为目标进行重新分片。

将新节点添加为副本

可以通过两种方式添加新副本。显而易见的是再次使用redis-cli,但是使用--cluster-slave选项,如下所示:

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave

请注意,此处的命令行与我们用于添加新主服务器的命令行完全相同,因此我们并未指定要向其添加副本的主服务器。在这种情况下,发生的事情是redis-cli将新节点添加为副本较少的主节点中的随机主节点的副本。

但是,您可以使用以下命令行确切指定要与新副本一起使用的主数据库:

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

这样,我们将新副本分配给特定的主数据库。

将副本添加到特定主副本的一种更手动的方法是将新节点添加为空的主副本,然后使用CLUSTER REPLICATE命令将其转换为副本 。如果该节点被添加为从属节点,但您想将其作为另一个主节点的副本进行移动,则这也适用。

例如,要为节点127.0.0.1:7005添加一个副本,该副本当前正在11423-16383范围内的哈希槽中,节点ID为3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e,我要做的就是与新节点连接(已经添加为空的主服务器)并发送命令:

redis 127.0.0.1:7006> cluster replicate 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

而已。现在,我们为这组哈希槽有了一个新的副本,并且集群中的所有其他节点都已经知道(几秒钟后需要更新其配置)。我们可以使用以下命令进行验证:

$ redis-cli -p 7000 cluster nodes | grep slave | grep 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e
f093c80dde814da99c5cf72a7dd01590792b783b 127.0.0.1:7006 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617702 3 connected
2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617198 3 connected

节点3c3a0c ...现在具有两个从属,分别在端口7002(现有的一个)和7006(新的)上运行。

删除节点

要删除一个从节点,只需使用del-noderedis-cli命令:

redis-cli --cluster del-node 127.0.0.1:7000 `<node-id>`

第一个参数只是集群中的一个随机节点,第二个参数是您要删除的节点的ID。

您也可以用相同的方法删除主节点,但是要删除主节点,它必须为空。如果主节点不为空,则需要先将数据从其重新分片到所有其他主节点。

删除主节点的另一种方法是在其从节点之一上对其执行手动故障转移,并在该节点成为新主节点的从节点之后将其删除。显然,这在您要减少群集中的主节点的实际数量时无济于事,在这种情况下,需要重新分片。

副本迁移

在Redis Cluster中,可以使用以下命令随时重新配置从属服务器以与其他主服务器进行复制:

CLUSTER REPLICATE <master-node-id>

但是,在一种特殊情况下,您希望副本在没有系统管理员帮助的情况下自动从一个主服务器移动到另一个主服务器。副本的自动重新配置称为副本迁移,它能够提高Redis群集的可靠性。

注意:您可以在Redis集群规范中阅读副本迁移的详细信息,这里我们仅提供一些有关一般想法以及您应该从中受益的信息。

在某些情况下,您可能希望让群集副本从一个主副本移动到另一个主副本的原因是,Redis群集通常具有与附加到给定主副本的副本数量相同的耐故障性。

例如,如果一个主节点及其副本同时失败,则每个主节点都有一个副本的集群将无法继续操作,这仅仅是因为没有其他实例拥有该主节点服务的哈希槽的副本。但是,尽管网络拆分可能同时隔离多个节点,但许多其他类型的故障(例如单个节点本地的硬件或软件故障)是非常显着的故障类别,不太可能在同一时间发生时间,因此在每个主节点都有一个从节点的集群中,有可能在凌晨4点杀死该从节点,而在凌晨6点杀死该主节点。这仍然会导致群集无法运行。

为了提高系统的可靠性,我们可以选择向每个主数据库添加其他副本,但这很昂贵。副本迁移允许将更多从服务器添加到仅几个主服务器中。因此,您有10个主机,每个主机1个从机,总共有20个实例。但是,例如,您增加了3个实例作为您的某些主服务器的从服务器,因此某些主服务器将具有多个从服务器。

使用副本迁移时,发生的情况是,如果一个主服务器不包含从服务器,则具有多个从服务器的主服务器的副本将迁移到孤立的主服务器。因此,当您的从属服务器在上述示例中的凌晨4点关闭之后,将替换另一个从属服务器,当主服务器在凌晨5点也发生故障时,仍然可以选择一个从属服务器,以便群集可以继续操作。

简而言之,您应该了解有关副本迁移的知识吗?

  • 群集将尝试从给定时刻具有最多副本数的主副本迁移副本。
  • 要从副本迁移中受益,您只需向群集中的单个主数据库中添加更多副本即可,无论哪个主数据库都没有关系。
  • 有一个配置参数可控制称为“副本迁移”的功能cluster-migration-barrier:您可以在redis.confRedis Cluster随附的示例文件中了解有关此功能的更多信息。

升级Redis集群中的节点

升级从节点很容易,因为您只需要停止节点并使用更新版本的Redis重新启动它即可。如果存在使用从属节点扩展读取的客户端,则在给定的从属节点不可用时,它们应该能够重新连接到其他从属节点。

升级母版要复杂一些,建议的过程是:

  1. 使用CLUSTER FAILOVER触发主服务器到其从服务器之一的手动故障转移(请参阅本文档的“手动故障转移”部分)。
  2. 等待主机变成从机。
  3. 最后,像对从属服务器一样升级节点。
  4. 如果要将主节点作为刚刚升级的节点,请触发新的手动故障转移,以将升级后的节点转换回主节点。

按照此过程,您应该先升级一个节点,然后再升级所有节点。

迁移到Redis集群

愿意迁移到Redis群集的用户可能只有一个主节点,或者可能已经在使用预先存在的分片设置,其中使用一些内部算法或由其客户端库或Redis代理实现的分片算法在N个节点之间分配密钥。

在这两种情况下,都可以轻松迁移到Redis Cluster,但是最重要的细节是应用程序是否使用了多键操作以及如何使用。有三种不同的情况:

  1. 不使用多键操作或事务或涉及多个键的Lua脚本。键是独立访问的(即使通过事务或将多个命令(大约是同一键)组合在一起的Lua脚本访问)。
  2. 使用了涉及多个键的多个键操作,事务或Lua脚本,但仅对具有相同哈希标签的键使用,这意味着一起使用的键都具有{...}恰好相同的子字符串。例如,以下多个键操作是在同一哈希标签的上下文中定义的:SUNION {user:1000}.foo {user:1000}.bar
  3. 涉及多个键的多个键操作,事务或Lua脚本与不具有显式或相同哈希标签的键名称一起使用。

第三种情况不是Redis Cluster可以处理的:需要修改应用程序,以便不使用多键操作或仅在同一哈希标签的上下文中使用它们。

讨论了案例1和2,因此我们将集中讨论以相同方式处理的这两种案例,因此在文档中将没有区别。

假设您已将现有数据集划分为N个主数据,如果没有现有分片,则N = 1,如果要将数据集迁移到Redis集群,则需要执行以下步骤:

  1. 停止您的客户。当前无法自动实时迁移到Redis Cluster。您可能能够在应用程序/环境的上下文中编排实时迁移。
  2. 使用BGREWRITEAOF命令为所有N个母版生成仅附加文件,然后等待AOF文件完全生成。
  3. 将AOF文件从aof-1保存到aof-N。此时,您可以根据需要停止旧实例(这很有用,因为在非虚拟化部署中,您经常需要重用同一台计算机)。
  4. 创建由N个主节点和零个从节点组成的Redis集群。稍后将添加奴隶。确保所有节点都使用仅附加文件来保持持久性。
  5. 停止所有群集节点,用您现有的仅附加文件替换它们的仅附加文件,第一个节点为aof-1,第二个节点为aof-2,最大为aof-N。
  6. 使用新的AOF文件重新启动Redis Cluster节点。他们会抱怨有些钥匙根据其配置不应该放在那儿。
  7. 使用redis-cli --cluster fix命令来修复群集,以便将根据每个节点是否具有权威性的哈希槽迁移密钥。
  8. 最后使用redis-cli --cluster check以确保您的群集正常。
  9. 重新启动您的客户端,使其修改为使用支持Redis Cluster的客户端库。

还有一种将数据从外部实例导入Redis群集的替代方法,即使用redis-cli --cluster import命令。

该命令将正在运行的实例的所有键(从源实例中删除键)移动到指定的预先存在的Redis群集。但是请注意,如果您将Redis 2.8实例用作源实例,则由于2.8无法实现迁移连接缓存,因此操作可能会很慢,因此您可能需要在执行此操作之前使用Redis 3.x版本重新启动源实例。

Redis Sentinel哨兵模式

Redis-Sentinel是Redis官方推荐的高可用性(HA)解决方案,当用Redis做Master-slave的高可用方案时,假如master宕机了,Redis本身(包括它的很多客户端)都没有实现自动进行主备切换,而Redis-sentinel本身也是一个独立运行的进程,它能监控多个master-slave集群,发现master宕机后能进行自动切换。

它的主要功能有以下几点

  • 不时地监控redis是否按照预期良好地运行;

  • 如果发现某个redis节点运行出现状况,能够通知另外一个进程(例如它的客户端);

  • 能够进行自动切换。当一个master节点不可用时,能够选举出master的多个slave(如果有超过一个slave的话)中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。

Sentinel支持集群

很显然,只使用单个sentinel进程来监控redis集群是不可靠的,当sentinel进程宕掉后(sentinel本身也有单点问题,single-point-of-failure)整个集群系统将无法按照预期的方式运行。所以有必要将sentinel集群,这样有几个好处:

  • 即使有一些sentinel进程宕掉了,依然可以进行redis集群的主备切换;

  • 如果只有一个sentinel进程,如果这个进程运行出错,或者是网络堵塞,那么将无法实现redis集群的主备切换(单点问题);

  • 如果有多个sentinel,redis的客户端可以随意地连接任意一个sentinel来获得关于redis集群中的信息。

Sentinel版本

Sentinel当前最新的稳定版本称为Sentinel 2(与之前的Sentinel 1区分开来)。随着redis2.8的安装包一起发行。安装完Redis2.8后,可以在redis2.8/src/里面找到Redis-sentinel的启动程序。

强烈建议
如果你使用的是redis2.6(sentinel版本为sentinel 1),你最好应该使用redis2.8版本的sentinel 2,因为sentinel 1有很多的Bug,已经被官方弃用,所以强烈建议使用redis2.8以及sentinel 2。

什么是Sentinel

Redis Sentinel是一个用来监控redis集群中节点的状态,不用来存储数据。当集群中的某个节点有故障时,可以自动的进行故障转移的操作。通常为了保证sentinel的高可用,sentinel也会部署多个。sentinel的结构图如下所示:

 

 

redis sentine中的3个定时任务

在redis sentinel中,一共有3个定时任务,通过这些任务,来发现新增节点和节点的状态。

1、每10秒每个sentinel节点对master节点和slave节点执行info操作:

 

2、每2秒每个sentinel节点通过master节点的channel(sentinel:hello)交换信息

 

 

3、 每1秒每个sentintel节点对master节点和slave节点以及其余的sentinel节点执行ping操作

 

 

 

主观下线和客观下线

  • 主观下线:当前sentintel节点认为某个redis节点不可用。

  • 客观下线:所有sentinel节点认为某个redis节点不可用。

故障转移过程

当多个sentinel认为master节点不可用,会进行故障转移操作,如下图所示:

 

 

1. 领导者选举

作用:选举出一个sentenel节点作为领导者去进行故障转移操作。

过程:

1). 每个做主观下线的sentinel节点向其他sentinel节点发送上面那条命令,要求将它设置为领导者。

2). 收到命令的sentinel节点如果还没有同意过其他的sentinel发送的命令(还未投过票),那么就会同意,否则拒绝。

3). 如果该sentinel节点发现自己的票数已经过半且达到了quorum的值,就会成为领导者。

4). 如果这个过程出现多个sentinel成为领导者,则会等待一段时间重新选举。

2. 选出新的master节点

redis sentinel会选一个合适的slave来升级为master,那么,如何选择一个合适的slave呢?顺序如下:

1). 选择slave-priority最高的slave节点(默认是相同)。

2). 选择复制偏移量最大的节点。

3). 如果以上两个条件都不满足,选runId最小的(启动最早的)。

3. 更改slave节点的master节点

当选举出新的master节点后,会将其余的节点变更为新的master节点的slave节点,如果原有的master节点重新上线,成为新的master节点的slave节点。

4. 通知客户端

当所有节点配置结束后,sentinel会通知客户端节点变更信息。

5. 客户端连接新的master节点

客户端收到节点信息后,会连接新的master节点。
 

 

运行Sentinel

运行sentinel有两种方式:

  • 第一种

    redis-sentinel /path/to/sentinel.conf
  • 第二种

    redis-server /path/to/sentinel.conf --sentinel

    以上两种方式,都必须指定一个sentinel的配置文件sentinel.conf,如果不指定,将无法启动sentinel。sentinel默认监听26379端口,所以运行前必须确定该端口没有被别的进程占用。

Sentinel的配置

Redis源码包中包含了一个sentinel.conf文件作为sentinel的配置文件,配置文件自带了关于各个配置项的解释。典型的配置项如下所示:

sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

sentinel monitor resque 192.168.1.3 6380 4
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
sentinel parallel-syncs resque 5

上面的配置项配置了两个名字分别为mymaster和resque的master,配置文件只需要配置master的信息就好啦,不用配置slave的信息,因为slave能够被自动检测到(master节点会有关于slave的消息)。需要注意的是,配置文件在sentinel运行期间是会被动态修改的,例如当发生主备切换时候,配置文件中的master会被修改为另外一个slave。这样,之后sentinel如果重启时,就可以根据这个配置来恢复其之前所监控的redis集群的状态。

接下来我们将一行一行地解释上面的配置项:

sentinel monitor mymaster 127.0.0.1 6379 2

这一行代表sentinel监控的master的名字叫做mymaster,地址为127.0.0.1:6379,行尾最后的一个2代表什么意思呢?我们知道,网络是不可靠的,有时候一个sentinel会因为网络堵塞而误以为一个master redis已经死掉了,当sentinel集群式,解决这个问题的方法就变得很简单,只需要多个sentinel互相沟通来确认某个master是否真的死了,这个2代表,当集群中有2个sentinel认为master死了时,才能真正认为该master已经不可用了。(sentinel集群中各个sentinel也有互相通信,通过gossip协议)。

除了第一行配置,我们发现剩下的配置都有一个统一的格式:

sentinel <option_name> <master_name> <option_value>

接下来我们根据上面格式中的option_name一个一个来解释这些配置项:

  • down-after-milliseconds
    sentinel会向master发送心跳PING来确认master是否存活,如果master在“一定时间范围”内不回应PONG 或者是回复了一个错误消息,那么这个sentinel会主观地(单方面地)认为这个master已经不可用了(subjectively down, 也简称为SDOWN)。而这个down-after-milliseconds就是用来指定这个“一定时间范围”的,单位是毫秒。

不过需要注意的是,这个时候sentinel并不会马上进行failover主备切换,这个sentinel还需要参考sentinel集群中其他sentinel的意见,如果超过某个数量的sentinel也主观地认为该master死了,那么这个master就会被客观地(注意哦,这次不是主观,是客观,与刚才的subjectively down相对,这次是objectively down,简称为ODOWN)认为已经死了。需要一起做出决定的sentinel数量在上一条配置中进行配置。

  • parallel-syncs
    在发生failover主备切换时,这个选项指定了最多可以有多少个slave同时对新的master进行同步,这个数字越小,完成failover所需的时间就越长,但是如果这个数字越大,就意味着越多的slave因为replication而不可用。可以通过将这个值设为 1 来保证每次只有一个slave处于不能处理命令请求的状态。

其他配置项在sentinel.conf中都有很详细的解释。
所有的配置都可以在运行时用命令SENTINEL SET command动态修改。

Sentinel的“仲裁会”

前面我们谈到,当一个master被sentinel集群监控时,需要为它指定一个参数,这个参数指定了当需要判决master为不可用,并且进行failover时,所需要的sentinel数量,本文中我们暂时称这个参数为票数

不过,当failover主备切换真正被触发后,failover并不会马上进行,还需要sentinel中的大多数sentinel授权后才可以进行failover。
当ODOWN时,failover被触发。failover一旦被触发,尝试去进行failover的sentinel会去获得“大多数”sentinel的授权(如果票数比大多数还要大的时候,则询问更多的sentinel)
这个区别看起来很微妙,但是很容易理解和使用。例如,集群中有5个sentinel,票数被设置为2,当2个sentinel认为一个master已经不可用了以后,将会触发failover,但是,进行failover的那个sentinel必须先获得至少3个sentinel的授权才可以实行failover。
如果票数被设置为5,要达到ODOWN状态,必须所有5个sentinel都主观认为master为不可用,要进行failover,那么得获得所有5个sentinel的授权。

配置版本号

为什么要先获得大多数sentinel的认可时才能真正去执行failover呢?

当一个sentinel被授权后,它将会获得宕掉的master的一份最新配置版本号,当failover执行结束以后,这个版本号将会被用于最新的配置。因为大多数sentinel都已经知道该版本号已经被要执行failover的sentinel拿走了,所以其他的sentinel都不能再去使用这个版本号。这意味着,每次failover都会附带有一个独一无二的版本号。我们将会看到这样做的重要性。

而且,sentinel集群都遵守一个规则:如果sentinel A推荐sentinel B去执行failover,B会等待一段时间后,自行再次去对同一个master执行failover,这个等待的时间是通过failover-timeout配置项去配置的。从这个规则可以看出,sentinel集群中的sentinel不会再同一时刻并发去failover同一个master,第一个进行failover的sentinel如果失败了,另外一个将会在一定时间内进行重新进行failover,以此类推。

redis sentinel保证了活跃性:如果大多数sentinel能够互相通信,最终将会有一个被授权去进行failover.
redis sentinel也保证了安全性:每个试图去failover同一个master的sentinel都会得到一个独一无二的版本号。

配置传播

一旦一个sentinel成功地对一个master进行了failover,它将会把关于master的最新配置通过广播形式通知其它sentinel,其它的sentinel则更新对应master的配置。

一个faiover要想被成功实行,sentinel必须能够向选为master的slave发送SLAVE OF NO ONE命令,然后能够通过INFO命令看到新master的配置信息。

当将一个slave选举为master并发送SLAVE OF NO ONE`后,即使其它的slave还没针对新master重新配置自己,failover也被认为是成功了的,然后所有sentinels将会发布新的配置信息。

新配在集群中相互传播的方式,就是为什么我们需要当一个sentinel进行failover时必须被授权一个版本号的原因。

每个sentinel使用##发布/订阅##的方式持续地传播master的配置版本信息,配置传播的##发布/订阅##管道是:__sentinel__:hello

因为每一个配置都有一个版本号,所以以版本号最大的那个为标准。

举个栗子:假设有一个名为mymaster的地址为192.168.1.50:6379。一开始,集群中所有的sentinel都知道这个地址,于是为mymaster的配置打上版本号1。一段时候后mymaster死了,有一个sentinel被授权用版本号2对其进行failover。如果failover成功了,假设地址改为了192.168.1.50:9000,此时配置的版本号为2,进行failover的sentinel会将新配置广播给其他的sentinel,由于其他sentinel维护的版本号为1,发现新配置的版本号为2时,版本号变大了,说明配置更新了,于是就会采用最新的版本号为2的配置。

这意味着sentinel集群保证了第二种活跃性:一个能够互相通信的sentinel集群最终会采用版本号最高且相同的配置。

SDOWN和ODOWN的更多细节

sentinel对于不可用有两种不同的看法,一个叫主观不可用(SDOWN),另外一个叫客观不可用(ODOWN)。SDOWN是sentinel自己主观上检测到的关于master的状态,ODOWN需要一定数量的sentinel达成一致意见才能认为一个master客观上已经宕掉,各个sentinel之间通过命令SENTINEL is_master_down_by_addr来获得其它sentinel对master的检测结果。

从sentinel的角度来看,如果发送了PING心跳后,在一定时间内没有收到合法的回复,就达到了SDOWN的条件。这个时间在配置中通过is-master-down-after-milliseconds参数配置。

当sentinel发送PING后,以下回复之一都被认为是合法的:

PING replied with +PONG.
PING replied with -LOADING error.
PING replied with -MASTERDOWN error.

其它任何回复(或者根本没有回复)都是不合法的。

从SDOWN切换到ODOWN不需要任何一致性算法,只需要一个gossip协议:如果一个sentinel收到了足够多的sentinel发来消息告诉它某个master已经down掉了,SDOWN状态就会变成ODOWN状态。如果之后master可用了,这个状态就会相应地被清理掉。

正如之前已经解释过了,真正进行failover需要一个授权的过程,但是所有的failover都开始于一个ODOWN状态。

ODOWN状态只适用于master,对于不是master的redis节点sentinel之间不需要任何协商,slaves和sentinel不会有ODOWN状态。

Sentinel之间和Slaves之间的自动发现机制

虽然sentinel集群中各个sentinel都互相连接彼此来检查对方的可用性以及互相发送消息。但是你不用在任何一个sentinel配置任何其它的sentinel的节点。因为sentinel利用了master的发布/订阅机制去自动发现其它也监控了统一master的sentinel节点。

通过向名为__sentinel__:hello的管道中发送消息来实现。

同样,你也不需要在sentinel中配置某个master的所有slave的地址,sentinel会通过询问master来得到这些slave的地址的。

每个sentinel通过向每个master和slave的发布/订阅频道__sentinel__:hello每秒发送一次消息,来宣布它的存在。
每个sentinel也订阅了每个master和slave的频道__sentinel__:hello的内容,来发现未知的sentinel,当检测到了新的sentinel,则将其加入到自身维护的master监控列表中。
每个sentinel发送的消息中也包含了其当前维护的最新的master配置。如果某个sentinel发现
自己的配置版本低于接收到的配置版本,则会用新的配置更新自己的master配置。

在为一个master添加一个新的sentinel前,sentinel总是检查是否已经有sentinel与新的sentinel的进程号或者是地址是一样的。如果是那样,这个sentinel将会被删除,而把新的sentinel添加上去。

网络隔离时的一致性

redis sentinel集群的配置的一致性模型为最终一致性,集群中每个sentinel最终都会采用最高版本的配置。然而,在实际的应用环境中,有三个不同的角色会与sentinel打交道:

  • Redis实例.

  • Sentinel实例.

  • 客户端.

为了考察整个系统的行为我们必须同时考虑到这三个角色。

下面有个简单的例子,有三个主机,每个主机分别运行一个redis和一个sentinel:

             +-------------+
             | Sentinel 1  | <--- Client A
             | Redis 1 (M) |
             +-------------+
                     |
                     |
 +-------------+     |                     +------------+
 | Sentinel 2  |-----+-- / partition / ----| Sentinel 3 | <--- Client B
 | Redis 2 (S) |                           | Redis 3 (M)|
 +-------------+                           +------------+

在这个系统中,初始状态下redis3是master, redis1和redis2是slave。之后redis3所在的主机网络不可用了,sentinel1和sentinel2启动了failover并把redis1选举为master。

Sentinel集群的特性保证了sentinel1和sentinel2得到了关于master的最新配置。但是sentinel3依然持着的是就的配置,因为它与外界隔离了。

当网络恢复以后,我们知道sentinel3将会更新它的配置。但是,如果客户端所连接的master被网络隔离,会发生什么呢?

客户端将依然可以向redis3写数据,但是当网络恢复后,redis3就会变成redis的一个slave,那么,在网络隔离期间,客户端向redis3写的数据将会丢失。

也许你不会希望这个场景发生:

  • 如果你把redis当做缓存来使用,那么你也许能容忍这部分数据的丢失。

  • 但如果你把redis当做一个存储系统来使用,你也许就无法容忍这部分数据的丢失了。

因为redis采用的是异步复制,在这样的场景下,没有办法避免数据的丢失。然而,你可以通过以下配置来配置redis3和redis1,使得数据不会丢失。

min-slaves-to-write 1
min-slaves-max-lag 10

通过上面的配置,当一个redis是master时,如果它不能向至少一个slave写数据(上面的min-slaves-to-write指定了slave的数量),它将会拒绝接受客户端的写请求。由于复制是异步的,master无法向slave写数据意味着slave要么断开连接了,要么不在指定时间内向master发送同步数据的请求了(上面的min-slaves-max-lag指定了这个时间)。

Sentinel状态持久化

snetinel的状态会被持久化地写入sentinel的配置文件中。每次当收到一个新的配置时,或者新创建一个配置时,配置会被持久化到硬盘中,并带上配置的版本戳。这意味着,可以安全的停止和重启sentinel进程。

无failover时的配置纠正

即使当前没有failover正在进行,sentinel依然会使用当前配置去设置监控的master。特别是:

  • 根据最新配置确认为slaves的节点却声称自己是master(参考上文例子中被网络隔离后的的redis3),这时它们会被重新配置为当前master的slave。

  • 如果slaves连接了一个错误的master,将会被改正过来,连接到正确的master。

Slave选举与优先级

当一个sentinel准备好了要进行failover,并且收到了其他sentinel的授权,那么就需要选举出一个合适的slave来做为新的master。

slave的选举主要会评估slave的以下几个方面:

  • 与master断开连接的次数

  • Slave的优先级

  • 数据复制的下标(用来评估slave当前拥有多少master的数据)

  • 进程ID

如果一个slave与master失去联系超过10次,并且每次都超过了配置的最大失联时间(down-after-milliseconds option),并且,如果sentinel在进行failover时发现slave失联,那么这个slave就会被sentinel认为不适合用来做新master的。

更严格的定义是,如果一个slave持续断开连接的时间超过

(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state

就会被认为失去选举资格。
符合上述条件的slave才会被列入master候选人列表,并根据以下顺序来进行排序:

  1. sentinel首先会根据slaves的优先级来进行排序,优先级越小排名越靠前(?)。

  2. 如果优先级相同,则查看复制的下标,哪个从master接收的复制数据多,哪个就靠前。

  3. 如果优先级和下标都相同,就选择进程ID较小的那个。

一个redis无论是master还是slave,都必须在配置中指定一个slave优先级。要注意到master也是有可能通过failover变成slave的。

如果一个redis的slave优先级配置为0,那么它将永远不会被选为master。但是它依然会从master哪里复制数据。

Sentinel和Redis身份验证

当一个master配置为需要密码才能连接时,客户端和slave在连接时都需要提供密码。

master通过requirepass设置自身的密码,不提供密码无法连接到这个master。
slave通过masterauth来设置访问master时的密码。

但是当使用了sentinel时,由于一个master可能会变成一个slave,一个slave也可能会变成master,所以需要同时设置上述两个配置项。

Sentinel API

Sentinel默认运行在26379端口上,sentinel支持redis协议,所以可以使用redis-cli客户端或者其他可用的客户端来与sentinel通信。

有两种方式能够与sentinel通信:

  • 一种是直接使用客户端向它发消息

  • 另外一种是使用发布/订阅模式来订阅sentinel事件,比如说failover,或者某个redis实例运行出错,等等。

Sentinel命令

sentinel支持的合法命令如下:

  • PING sentinel回复PONG.

  • SENTINEL masters 显示被监控的所有master以及它们的状态.

  • SENTINEL master <master name> 显示指定master的信息和状态;

  • SENTINEL slaves <master name> 显示指定master的所有slave以及它们的状态;

  • SENTINEL get-master-addr-by-name <master name> 返回指定master的ip和端口,如果正在进行failover或者failover已经完成,将会显示被提升为master的slave的ip和端口。

  • SENTINEL reset <pattern> 重置名字匹配该正则表达式的所有的master的状态信息,清楚其之前的状态信息,以及slaves信息。

  • SENTINEL failover <master name> 强制sentinel执行failover,并且不需要得到其他sentinel的同意。但是failover后会将最新的配置发送给其他sentinel。

动态修改Sentinel配置

从redis2.8.4开始,sentinel提供了一组API用来添加,删除,修改master的配置。

需要注意的是,如果你通过API修改了一个sentinel的配置,sentinel不会把修改的配置告诉其他sentinel。你需要自己手动地对多个sentinel发送修改配置的命令。

以下是一些修改sentinel配置的命令:

  • SENTINEL MONITOR <name> <ip> <port> <quorum> 这个命令告诉sentinel去监听一个新的master

  • SENTINEL REMOVE <name> 命令sentinel放弃对某个master的监听

  • SENTINEL SET <name> <option> <value> 这个命令很像Redis的CONFIG SET命令,用来改变指定master的配置。支持多个<option><value>。例如以下实例:

  • SENTINEL SET objects-cache-master down-after-milliseconds 1000

只要是配置文件中存在的配置项,都可以用SENTINEL SET命令来设置。这个还可以用来设置master的属性,比如说quorum(票数),而不需要先删除master,再重新添加master。例如:

SENTINEL SET objects-cache-master quorum 5

增加或删除Sentinel

由于有sentinel自动发现机制,所以添加一个sentinel到你的集群中非常容易,你所需要做的只是监控到某个Master上,然后新添加的sentinel就能获得其他sentinel的信息以及masterd所有的slave。

如果你需要添加多个sentinel,建议你一个接着一个添加,这样可以预防网络隔离带来的问题。你可以每个30秒添加一个sentinel。最后你可以用SENTINEL MASTER mastername来检查一下是否所有的sentinel都已经监控到了master。

删除一个sentinel显得有点复杂:因为sentinel永远不会删除一个已经存在过的sentinel,即使它已经与组织失去联系很久了。
要想删除一个sentinel,应该遵循如下步骤:

  1. 停止所要删除的sentinel

  2. 发送一个SENTINEL RESET * 命令给所有其它的sentinel实例,如果你想要重置指定master上面的sentinel,只需要把*号改为特定的名字,注意,需要一个接一个发,每次发送的间隔不低于30秒。

  3. 检查一下所有的sentinels是否都有一致的当前sentinel数。使用SENTINEL MASTER mastername 来查询。

删除旧master或者不可达slave

sentinel永远会记录好一个Master的slaves,即使slave已经与组织失联好久了。这是很有用的,因为sentinel集群必须有能力把一个恢复可用的slave进行重新配置。

并且,failover后,失效的master将会被标记为新master的一个slave,这样的话,当它变得可用时,就会从新master上复制数据。

然后,有时候你想要永久地删除掉一个slave(有可能它曾经是个master),你只需要发送一个SENTINEL RESET master命令给所有的sentinels,它们将会更新列表里能够正确地复制master数据的slave。

发布/订阅

客户端可以向一个sentinel发送订阅某个频道的事件的命令,当有特定的事件发生时,sentinel会通知所有订阅的客户端。需要注意的是客户端只能订阅,不能发布。

订阅频道的名字与事件的名字一致。例如,频道名为sdown 将会发布所有与SDOWN相关的消息给订阅者。

如果想要订阅所有消息,只需简单地使用PSUBSCRIBE *

以下是所有你可以收到的消息的消息格式,如果你订阅了所有消息的话。第一个单词是频道的名字,其它是数据的格式。

注意:以下的instance details的格式是:

<instance-type> <name> <ip> <port> @ <master-name> <master-ip> <master-port>

如果这个redis实例是一个master,那么@之后的消息就不会显示。

    +reset-master <instance details> -- 当master被重置时.
    +slave <instance details> -- 当检测到一个slave并添加进slave列表时.
    +failover-state-reconf-slaves <instance details> -- Failover状态变为reconf-slaves状态时
    +failover-detected <instance details> -- 当failover发生时
    +slave-reconf-sent <instance details> -- sentinel发送SLAVEOF命令把它重新配置时
    +slave-reconf-inprog <instance details> -- slave被重新配置为另外一个master的slave,但数据复制还未发生时。
    +slave-reconf-done <instance details> -- slave被重新配置为另外一个master的slave并且数据复制已经与master同步时。
    -dup-sentinel <instance details> -- 删除指定master上的冗余sentinel时 (当一个sentinel重新启动时,可能会发生这个事件).
    +sentinel <instance details> -- 当master增加了一个sentinel时。
    +sdown <instance details> -- 进入SDOWN状态时;
    -sdown <instance details> -- 离开SDOWN状态时。
    +odown <instance details> -- 进入ODOWN状态时。
    -odown <instance details> -- 离开ODOWN状态时。
    +new-epoch <instance details> -- 当前配置版本被更新时。
    +try-failover <instance details> -- 达到failover条件,正等待其他sentinel的选举。
    +elected-leader <instance details> -- 被选举为去执行failover的时候。
    +failover-state-select-slave <instance details> -- 开始要选择一个slave当选新master时。
    no-good-slave <instance details> -- 没有合适的slave来担当新master
    selected-slave <instance details> -- 找到了一个适合的slave来担当新master
    failover-state-send-slaveof-noone <instance details> -- 当把选择为新master的slave的身份进行切换的时候。
    failover-end-for-timeout <instance details> -- failover由于超时而失败时。
    failover-end <instance details> -- failover成功完成时。
    switch-master <master name> <oldip> <oldport> <newip> <newport> -- 当master的地址发生变化时。通常这是客户端最感兴趣的消息了。
    +tilt -- 进入Tilt模式。
    -tilt -- 退出Tilt模式。

TILT 模式

redis sentinel非常依赖系统时间,例如它会使用系统时间来判断一个PING回复用了多久的时间。
然而,假如系统时间被修改了,或者是系统十分繁忙,或者是进程堵塞了,sentinel可能会出现运行不正常的情况。
当系统的稳定性下降时,TILT模式是sentinel可以进入的一种的保护模式。当进入TILT模式时,sentinel会继续监控工作,但是它不会有任何其他动作,它也不会去回应is-master-down-by-addr这样的命令了,因为它在TILT模式下,检测失效节点的能力已经变得让人不可信任了。
如果系统恢复正常,持续30秒钟,sentinel就会退出TITL模式。

-BUSY状态

注意:该功能还未实现。

当一个脚本的运行时间超过配置的运行时间时,sentinel会返回一个-BUSY 错误信号。如果这件事发生在触发一个failover之前,sentinel将会发送一个SCRIPT KILL命令,如果script是只读的话,就能成功执行。

 

Redis 连接池

Redis操作是单线程的,使用连接池可以减少连接的创建,redis连接池有两种方式:Jedis(JedisPool) 和 Lettuce(LettucePool)。 Lettuce 和 Jedis 的定位都是Redis的client,所以他们当然可以直接连接redis server。在Lettuce和Jedis之外还有Redission ,Redisson:实现了分布式和可扩展的Java数据结构。

Jedis

Jedis:是Redis的Java实现客户端,提供了比较全面的Redis命令的支持。Jedis在实现上是直接连接的Redis Server,如果在多线程环境下是非线程安全的。每个线程都去拿自己的 Jedis 实例,当连接数量增多时,资源消耗阶梯式增大,连接成本就较高了。这个时候只有使用连接池,为每个Jedis实例增加物理连接。

Lettuce

Lettuce的连接是基于Netty的,Netty 是一个多线程、事件驱动的 I/O 框架。连接实例可以在多个线程间共享,当多线程使用同一连接实例时,是线程安全的。Lettuce连接实例(StatefulRedisConnection)可以在多个线程间并发访问,应为StatefulRedisConnection是线程安全的,所以一个连接实例(StatefulRedisConnection)就可以满足多线程环境下的并发访问,当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。

Others

Redis pipeline 

Pipeline原理分析

参考地址:https://www.cnblogs.com/pc-boke/articles/9045576.html

1. 基本原理

1.1 为什么会出现Pipeline
  Redis本身是基于Request/Response协议的,正常情况下,客户端发送一个命令,等待Redis应答,Redis在接收到命令,处理后应答。在这种情况下,如果同时需要执行大量的命令,那就是等待上一条命令应答后再执行,这中间不仅仅多了RTT(Round Time Trip),而且还频繁的调用系统IO,发送网络请求。如下图。
为了提升效率,这时候Pipeline出现了,它允许客户端可以一次发送多条命令,而不等待上一条命令执行的结果,这和网络的Nagel算法有点像(TCP_NODELAY选项)。不仅减少了RTT,同时也减少了IO调用次数(IO调用涉及到用户态到内核态之间的切换)。如下图:
客户端这边首先将执行的命令写入到缓冲中,最后再一次性发送Redis。但是有一种情况就是,缓冲区的大小是有限制的,比如Jedis,限制为8192,超过了,则刷缓存,发送到Redis,但是不去处理Redis的应答,如上图所示那样。

1.2 实现原理
  要支持Pipeline,其实既要服务端的支持,也要客户端支持。对于服务端来说,所需要的是能够处理一个客户端通过同一个TCP连接发来的多个命令,可以理解为,这里将多个命令切分,和处理单个命令一样(之前老生常谈的黏包现象),
Redis就是这样处理的。而客户端,则是要将多个命令缓存起来,缓冲区满了就发送,然后再写缓冲,最后才处理Redis的应答,如Jedis。

1.3 从哪个方面提升性能
正如上面所说的,一个是RTT,节省往返时间,但是另一个原因也很重要,就是IO系统调用。一个read系统调用,需要从用户态,切换到内核态。

1.4 注意点
  Redis的Pipeline和Transaction不同,Transaction会存储客户端的命令,最后一次性执行,而Pipeline则是处理一条,响应一条,但是这里却有一点,就是客户端会并不会调用read去读取socket里面的缓冲数据,这也就造就了,
如果Redis应答的数据填满了该接收缓冲(SO_RECVBUF),那么客户端会通过ACK,WIN=0(接收窗口)来控制服务端不能再发送数据,那样子,数据就会缓冲在Redis的客户端应答列表里面。所以需要注意控制Pipeline的大小。如下图:

2. Codis Pipeline
  在一般情况下,都会在Redis前面使用一个代理,来作负载以及高可用。这里在公司里面使用的是Codis,以Codis 3.2版本为例(3.2版本是支持Pipeline的)。
Codis在接收到客户端请求后,首先根据Key来计算出一个hash,映射到对应slots,然后转发请求到slots对应的Redis。在这过程中,一个客户端的多个请求,有可能会对应多个Redis,这个时候就需要保证请求的有序性(不能乱序),
Codis采用了一个Tasks队列,将请求依次放入队列,然后loopWriter从里面取,如果Task请求没有应答,则等待(这里和Java的Future是类似的)。内部BackenRedis是通过channel来进行通信的,dispatcher将Request通过channel发送到BackenRedis,然后BackenRedis处理完该请求,则将值填充到该Request里面。最后loopWriter等待到了值,则返回给客户端。如下图所示:

3. 总结
  1、Pipeline减少了RTT,也减少了IO调用次数(IO调用涉及到用户态到内核态之间的切换)
  2、需要控制Pipeline的大小,否则会消耗Redis的内存
  3、Codis 3.2 Pipeline默认10K,3.1则是1024Jedis客户端缓存是8192,超过该大小则刷新缓存,或者直接发送

4. 参考资料
Redis官方文档:https://redis.io/topics/pipelining

Pipeline 回调方法示例

redis回调实现:

package com.xxx.position.redis.pipeline.position;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.xxx.position.bean.XhyPosition;
import com.xxx.position.service.MobileWebService;
import com.xxx.position.util.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import java.util.List;

/**
 * @Copyright: 2019-2021
 * @FileName: OnlinePositionRedisCallback.java
 * @Author: PJL
 * @Date: 2020/4/22 18:02
 * @Description: 实时位置相关pipeline批量处理
 */
@Slf4j
public class OnlinePositionRedisCallback implements RedisCallback<Object> {

    MobileWebService mobileService;

    XhyPosition position;

    List<XhyPosition> positionList;

    long mobilePositionTimeout;

    int pointLimit;

    /**
     * 实时位置批量执行指令构造
     *
     * @param position
     * @param positionList
     */
    public OnlinePositionRedisCallback(MobileWebService mobileService, XhyPosition position, List<XhyPosition> positionList, long mobilePositionTimeout,int pointLimit){
        this.mobileService = mobileService;
        this.position = position;
        this.positionList = positionList;
        this.mobilePositionTimeout = mobilePositionTimeout;
        this.pointLimit = pointLimit;
    }

    @Override
    public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
        // 验证指令执行方式
        if(null != positionList){
            for (XhyPosition position : positionList){
                doOnlineCommand(redisConnection,position);
            }
        }else{
            doOnlineCommand(redisConnection,position);
        }
        return null;
    }

    /**
     * 执行单个指令
     *
     * @param redisConnection
     */
    private void doOnlineCommand(RedisConnection redisConnection,XhyPosition position) {
        String userId = position.getId();
        String json = JSON.toJSONString(position);
        Point point = new Point(position.getX(),position.getY());
        String timeoutKey = new StringBuffer(Constants.MOBILE_POSITION_TIMEOUT_KEY ).append(userId).toString();
        try {
            /********** 用户单位位置存储***********/
            // 命令1:保存用户当前位置及今日巡护公里和时间
            redisConnection.hSet(Constants.MOBILE_TRACK_AGGREGATION_HLY_KEY.getBytes(), userId.getBytes(), json.getBytes());
            // 命令2:保存用户实时轨迹
            String trackKey = new StringBuffer(Constants.MOBILE_TRACK_LIST_KEY ).append(userId).toString();
            redisConnection.lPush(trackKey.getBytes(), JSONObject.toJSONString(point).getBytes());
            // 命令3:保存用户实时轨迹限制点个数
            redisConnection.lRange(trackKey.getBytes(),0,pointLimit);
            // 命令4:保存全国级别位置
            redisConnection.geoAdd(Constants.MOBILE_POSITION_QG_KEY.getBytes(),point,userId.getBytes());
            // 命令5:获取用户父级单位数据
            Object[] parentIds = mobileService.getParentIdsWithoutType(userId);
            for (Object o : parentIds) {
                String dwCode = o.toString();
                /***********用户单位数量原子增长(说明:多线程有并发问题,用户上线是非原子操作(有状态),同步操作会降低效率弃用原子操作)****/
                // 命令6-n:获取用户是否在线的标志
               /* Object obj = mobileService.getUserWithoutType(userOnlineKey);
                if (null == obj) {
                    String incrementKey = new StringBuffer(Constants.MOBILE_POSITION_DW_CAS_SUM_KEY ).append(dwCode).toString();
                    // 命令7-n:保存全国级别位置
                    redisConnection.incr(incrementKey.getBytes());
                }*/
                String  dwCodeNew =new StringBuffer(Constants.MOBILE_POSITION_DW_KEY ).append(dwCode).toString();
                // 命令8-n:保存全国级别位置
                redisConnection.geoAdd(dwCodeNew.getBytes(),point,userId.getBytes());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            /*************位置数据过期标记*********/
            // 命令9-n:保存全国级别位置
            redisConnection.setEx(timeoutKey.getBytes(), mobilePositionTimeout,json.getBytes());
            log.debug("用户userId="+userId+"已上线!");
        }
    }
}

调用方法:

 /**
     * 上线单个位置
     *
     * @param position
     */
    public void online(XhyPosition position){
        redisTemplate.executePipelined(new OnlinePositionRedisCallback(mobileService,position,null,MOBILE_POSITION_OUT_TIME,trackListRange));
    }

    /**
     * 上线多个位置
     *
     * @param positionList
     */
    public void onlineList(List<XhyPosition> positionList){
        redisTemplate.executePipelined(new OnlinePositionRedisCallback(mobileService,null,positionList,MOBILE_POSITION_OUT_TIME,trackListRange));
    }

封装类:

package com.boonya.springcloud.cache.redis.service;

import com.boonya.springcloud.cache.redis.bean.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.List;

/**
 * @ClassName: RedisPipelineService
 * @Description: TODO(功能说明:Redis Pipeline 高性能处理命令操作【每个回调少于1000个指令效率最佳】)
 * @author: pengjunlin
 * @motto: 学习需要毅力,那就秀毅力
 * @date 2020/4/21 0:27
 */
public class RedisPipelineService {

    @Autowired
    RedisTemplate redisTemplate;

    /**
     * SET执行插入命令
     *
     * @param list
     */
    public  List<Object>  setAdd(List<KVParam> list){
        return redisTemplate.executePipelined(new RedisCallback<Object>() {

            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                for (KVParam kvParam : list){
                    if(kvParam.getTimeout()>-1){
                        redisConnection.setEx(kvParam.getKey().getBytes(),kvParam.getTimeout(),kvParam.getValue().getBytes());
                    }else{
                        redisConnection.set(kvParam.getKey().getBytes(),kvParam.getValue().getBytes());
                    }
                }
                return null;
            }
        });
    }

    /**
     * HASH执行插入命令
     *
     * @param list
     */
    public  List<Object>  hashAdd(List<HashParam> list){
        return redisTemplate.executePipelined(new RedisCallback<Object>() {

            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                for (HashParam hashParam : list){
                    redisConnection.hSet(hashParam.getKey().getBytes(),hashParam.getIdentify().getBytes(),hashParam.getValue().getBytes());
                }
                return null;
            }
        });
    }

    /**
     * GEOHASH执行插入命令
     *
     * @param list
     */
    public  List<Object>  geohashAdd(List<GeoHashParam> list){
        return redisTemplate.executePipelined(new RedisCallback<Object>() {

            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                for (GeoHashParam geoHashParam : list){
                    redisConnection.geoAdd(geoHashParam.getKey().getBytes(),geoHashParam.getPoint(),geoHashParam.getIdentify().getBytes());
                }
                return null;
            }
        });
    }

    /**
     * ZSET执行插入命令
     *
     * @param list
     */
    public  List<Object>  zsetAdd(List<ZsetParam> list){
        return redisTemplate.executePipelined(new RedisCallback<Object>() {

            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                for (ZsetParam zsetParam : list){
                    redisConnection.zAdd(zsetParam.getKey().getBytes(),zsetParam.getScore(),zsetParam.getIdentify().getBytes());
                }
                return null;
            }
        });
    }

    /**
     * LIST执行插入命令
     *
     * @param list
     */
    public List<Object>  listAdd(List<ListParam> list){
        return redisTemplate.executePipelined(new RedisCallback<Object>() {

            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                for (ListParam listParam:list){
                    if(listParam.isLeft()){
                        redisConnection.lPush(listParam.getKey().getBytes(),listParam.getValue().getBytes());
                        if(listParam.getRange()>-1){
                            redisConnection.lRange(listParam.getKey().getBytes(),0,listParam.getRange());
                        }
                    }else{
                        redisConnection.rPush(listParam.getKey().getBytes(),listParam.getValue().getBytes());
                       /* if(listParam.getRange()>-1){
                            // 此处必须大于限制个数的点方能取暂不实现
                            int total = listParam.getKey().length();//实际的点数
                            if(total>listParam.getRange()){
                                long start=-listParam.getRange();
                                redisConnection.lRange(listParam.getKey().getBytes(),start,total);
                            }
                        }*/
                    }

                }

                return null;
            }
        });
    }
}

Rredis 分布式框架Redisson

 

Redis代码解决方案

以下配置来自本人自己编写的多模式支持代码,实例项目中对连接池和redis模式做了区分

Github地址:https://github.com/open-micro-services/springcloud/tree/master/sc-caches/cache-redis

单点配置

application.yml配置

Java配置:


/**
 * @ClassName: RedisConfigForSingle
 * @Description: TODO(功能说明 : Redis单机配置【注意:单点模式需要把cluster和sentinel配置注释掉)
 * @author: pengjunlin
 * @motto: 学习需要毅力,那就秀毅力
 * @date 2018-12-28 17:26
 */
@Configuration
@Conditional(value = {RedisEnableCondition.class,RedisSingleCondition.class})
@Primary
public class RedisConfigForSingle {


    /**
     * REDIS 序列化器设置[平台RedisTemplate注入方式有一定问题]
     *
     * @param lettuceConnectionFactory lettuce基于Netty高性能连接池可选连接工厂
     * @return
     */
    @Bean
    @Conditional(RedisLettuceCondition.class)
    public RedisTemplate<String, Object> redisTemplateByLettuce(LettuceConnectionFactory lettuceConnectionFactory) {
        return RedisUtil.getRedisTemplate(lettuceConnectionFactory);
    }

    /**
     * REDIS 序列化器设置[平台RedisTemplate注入方式有一定问题]
     *
     * @return
     */
    @Bean
    @Conditional(RedisJedisCondition.class)
    public RedisTemplate<String, Object> redisTemplateByJedis(JedisConnectionFactory jedisConnectionFactory) {
        return RedisUtil.getRedisTemplate(jedisConnectionFactory);
    }

}

集群配置

application.yml配置:

Java配置:

/**
 * @ClassName: RedisConfigForCluster
 * @Description: TODO(功能说明 : Redis集群配置)
 * @author: pengjunlin
 * @motto: 学习需要毅力,那就秀毅力
 * @date 2018-12-28 17:28
 */
@Configuration
@Conditional(value = {RedisEnableCondition.class,RedisClusterCondition.class})
@Primary
public class RedisConfigForCluster {

    @Value("${spring.redis.password}")
    private String password;


    @Value("${spring.redis.cluster.nodes}")
    private String clusterNodes;

    @Value("${spring.redis.cluster.max-redirects}")
    private Integer maxRedirects;

    /**
     * Redis集群客户端配置【 此种方式仅使用在没有密码的情况】
     *
     * @return JedisCluster
     */
    @Bean
    public JedisCluster jedisCluster() {
        String[] serverArray = clusterNodes.split(",");
        Set<HostAndPort> nodes = new HashSet<>();

        for (String ipPort : serverArray) {
            String[] ipPortPair = ipPort.split(":");
            nodes.add(new HostAndPort(ipPortPair[0].trim(), Integer.valueOf(ipPortPair[1].trim())));

        }
        // 此种方式仅使用在没有密码的情况
        JedisCluster jedisCluster = new JedisCluster(nodes, 10000);
        return jedisCluster;
    }

    /**
     * Redis集群的配置
     *
     * @return RedisClusterConfiguration
     */
    @Bean
    public RedisClusterConfiguration redisClusterConfiguration() {
        RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
        //Set<RedisNode> clusterNodes
        String[] serverArray = clusterNodes.split(",");

        Set<RedisNode> nodes = new HashSet<RedisNode>();

        for (String ipPort : serverArray) {
            String[] ipAndPort = ipPort.split(":");
            nodes.add(new RedisNode(ipAndPort[0].trim(), Integer.valueOf(ipAndPort[1])));
        }
        redisClusterConfiguration.setClusterNodes(nodes);
        redisClusterConfiguration.setMaxRedirects(maxRedirects);
        redisClusterConfiguration.setPassword(password);
        return redisClusterConfiguration;
    }

    /**
     * 【JedisConnectionFactory】实例化 RedisTemplate 对象
     *
     * @param redisClusterConfiguration
     * @param jedisPoolConfig 继承自JedisPoolConfig
     * @return
     */
    @Bean
    @Conditional(RedisJedisCondition.class)
    public RedisTemplate<String, Object> redisTemplateByJedis(RedisClusterConfiguration redisClusterConfiguration,RedisJedisPoolConfig jedisPoolConfig) {
        return RedisUtil.getRedisTemplate(redisClusterConfiguration,jedisPoolConfig);
    }

    /**
     * 【LettuceConnectionFactory】实例化 RedisTemplate 对象
     *
     * @param redisClusterConfiguration
     * @param lettuceClientConfiguration LettuceClientConfiguration接口实现
     * @return
     */
    @Bean
    @Conditional(RedisLettuceCondition.class)
    public RedisTemplate<String, Object> redisTemplateByLettuce(RedisClusterConfiguration redisClusterConfiguration,RedisLettuceClientConfiguration lettuceClientConfiguration) {
        return RedisUtil.getRedisTemplate(redisClusterConfiguration,lettuceClientConfiguration);
    }
}

 

哨兵配置

application.yml配置:

Java配置:

/**
 * @ClassName: RedisConfigForSentinel
 * @Description: TODO(功能说明:Redis哨兵配置)
 * @author: pengjunlin
 * @motto: 学习需要毅力,那就秀毅力
 * @date 2018-12-28 17:39
 */
@Configuration
@Conditional(value = {RedisEnableCondition.class,RedisSentinelCondition.class})
@Primary
public class RedisConfigForSentinel {

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.sentinel.master}")
    private String master;

    @Value("${spring.redis.sentinel.nodes}")
    private String clusterNodes;

    /**
     * Redis集群的配置
     *
     * @return RedisClusterConfiguration
     */
    @Bean
    public RedisSentinelConfiguration redisSentinelConfiguration() {
        RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();
        //Set<RedisNode> sentinelNodes
        String[] serverArray = clusterNodes.split(",");

        Set<RedisNode> nodes = new HashSet<RedisNode>();

        for (String ipPort : serverArray) {
            String[] ipAndPort = ipPort.split(":");
            nodes.add(new RedisNode(ipAndPort[0].trim(), Integer.valueOf(ipAndPort[1])));
        }
        redisSentinelConfiguration.setSentinels(nodes);
        redisSentinelConfiguration.setMaster(master);
        redisSentinelConfiguration.setPassword(password);
        return redisSentinelConfiguration;
    }

    /**
     * 【JedisConnectionFactory】实例化 RedisTemplate 对象
     *
     * @param redisSentinelConfiguration
     * @param jedisPoolConfig 继承自JedisPoolConfig
     * @return
     */
    @Bean
    @Conditional(RedisJedisCondition.class)
    public RedisTemplate<String, Object> redisTemplateByJedis(RedisSentinelConfiguration redisSentinelConfiguration, RedisJedisPoolConfig jedisPoolConfig) {
       return RedisUtil.getRedisTemplate(redisSentinelConfiguration,jedisPoolConfig);
    }

    /**
     * 【LettuceConnectionFactory】实例化 RedisTemplate 对象
     *
     * @param redisSentinelConfiguration
     * @param redisLettuceClientConfiguration LettuceClientConfiguration接口实现
     * @return
     */
    @Bean("redisTemplateByLettuce")
    @Conditional(RedisLettuceCondition.class)
    public RedisTemplate<String, Object> redisTemplateByLettuce(RedisSentinelConfiguration redisSentinelConfiguration, RedisLettuceClientConfiguration redisLettuceClientConfiguration) {
        return RedisUtil.getRedisTemplate(redisSentinelConfiguration,redisLettuceClientConfiguration);
    }


}

参考文章

https://blog.csdn.net/qq2430/article/details/80679439

https://www.cnblogs.com/liyan492/p/9858548.html

https://www.cnblogs.com/huangxincheng/p/5615037.html

 

©️2020 CSDN 皮肤主题: 护眼 设计师: 闪电赇 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值