Redis单机模式主从模式哨兵模式集群模式搭建

文章目录

    • 一、Redis下载及安装
      • 1.1、下载
      • 1.2、环境安装
      • 1.3、编译安装
      • 1.4、修改配置
      • 1.5、启动Redis
      • 1.6、验证Redis是否启动
      • 1.7、进入到Redis客户端
      • 1.8、其它命令
    • 二、Redis持久化
      • 2.1、RDB(snapshot)
        • 2.1.1、bgsave的写时复制(COW)机制
        • 2.1.2、save与bgsave对比
        • 2.1.2、RDB配置文件
          • ①文件名称及其目录配置
          • ②多久操作会持久化
      • 2.2、AOF(append-only file)
        • 2.2.1、开启AOF
        • 2.2.2、AOF重写
      • 2.3、AOF和RDB的选择
        • 2.3.1、Redis 4.0之后的混合持久化
      • 2.4、 Redis的四种数据备份策略
    • 三、主从架构
      • 3.1、环境搭建
      • 3.2、Redis主从工作原理
        • 主从复制(全量复制)流程图
        • 数据部分复制
      • 3.3、使用Jedis测试
        • 管道(Pipeline)
    • 四、哨兵高可用架构
      • 4.1、环境搭建
      • 4.2、哨兵代码测试
      • 4.3、SpringBoot整合Redis哨兵架构
        • 存在问题
      • 4.4、补充说明
        • **Redis客户端命令对应的RedisTemplate中的方法列表**
        • 封装RedisTemplate工具类
    • 五、集群架构
      • 5.1、Redis集群搭建
        • 1、配置文件修改
        • 2、逐个启动命令
        • 3、查看某一个节点信息
        • 4、查看集群状态
        • 5、关闭集群命令
        • 6、遇到问题
          • 最终解决办法
        • 7、引出生产问题
        • 8、帮助文档
      • 5.2、给集群添加节点
        • 1、为什么考虑给Redis集群添加节点呢?
        • 2、如何添加Redis节点
        • 3、查看添加进去的节点数据
        • 4、为新添加进去的节点添加从节点
      • 5.3、删除节点
        • 1、删除8009从节点
        • 2、删除8007主节点
      • 5.4、Java代码操作Redis集群
        • 简单版本
        • Springboot版本
      • 5.5、原理分析
        • 槽位定位算法
        • 跳转重定位
        • Redis集群节点间的通信机制
          • 集中式
          • gossip
        • **网络抖动**
        • **Redis集群选举原理分析**
        • **集群脑裂数据丢失问题**
        • **集群是否完整才能对外提供服务**
        • **Redis集群为什么至少需要三个master节点,并且推荐节点数为奇数?**
        • **Redis集群对批量操作命令的支持**
        • **哨兵leader选举流程**

一、Redis下载及安装

1.1、下载

可以在Windows环境下进入Redis的下载网址:http://redis.io/download ,进行下载安装包

或者直接在Linux存在外网环境下直接下载:wget http://download.redis.io/releases/redis-5.0.3.tar.gz

当前使用版本是:redis-5.0.3.tar.gz

1.2、环境安装

# 安装gcc
yum install gcc

1.3、编译安装

把下载好的redis-5.0.3.tar.gz放在/usr/local文件夹下,并解压

tar -zxvf redis-5.0.3.tar.gz
cd redis-5.0.3

进入到解压好的redis-5.0.3目录下,进行编译与安装

make

1.4、修改配置

下面来修改部分配置,后续详细配置将会在当前系列文章中完整标出

# 后台启动
daemonize yes 
#关闭保护模式,开启的话,只有本机才可以访问redis    
protected-mode no  
# 需要注释掉bind
# bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,
# 代表允许客户端通过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可    
# bind 127.0.0.1

这里的bind我需要来额外说明一下:

在后期会配置主从模型、哨兵模式以及集群模式,如果不将这里的bind注释掉,那么默认使用的是127.0.0.1,就会导致外部无法访问,而只能够在当前机器上进行访问。

1.5、启动Redis

src/redis-server redis.conf

1.6、验证Redis是否启动

ps -ef | grep redis

1.7、进入到Redis客户端

src/redis-cli -h ip -p port

1.8、其它命令

1、退出redis客户端

quit/exit

2、退出Redis服务

(1)pkill redis-server
(2)kill 进程号
(3)src/redis-cli shutdown

二、Redis持久化

2.1、RDB(snapshot)

在默认情况下, Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。
你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。
比如说, 以下设置会让 Redis 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次数据集:

# save 60 1000    

:关闭RDB只需要将所有的save保存策略注释掉即可

除了上面在指定时间之内触发的操作之外,还可以手动执行命令生成RDB快照,进入redis客户端执行命令save或bgsave可以生成dump.rdb文件,每次命令执行都会将所有redis内存快照到一个新的rdb文件里,并覆盖原有rdb快照文件。

2.1.1、bgsave的写时复制(COW)机制

Redis 借助操作系统提供的写时复制技术(Copy-On-Write, COW),在生成快照的同时,依然可以正常处理写命令。简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些数据也都是读操作,那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据,那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。

2.1.2、save与bgsave对比

命令 save bgsave
IO类型 同步 异步
是否阻塞redis其它命令 否(在生成子进程执行调用fork函数时会有短暂阻塞)
复杂度 O(n) O(n)
优点 不会消耗额外内存 不阻塞客户端命令
缺点 阻塞客户端命令 需要fork子进程,消耗内存

注意:配置自动生成rdb文件后台使用的是bgsave方式

2.1.2、RDB配置文件

直接查看redis.conf配置文件中的配置信息

①文件名称及其目录配置

可以看下RDB中配置的文件目录以及文件名称

# The filename where to dump the DB
# rdb文件名称
dbfilename dump.rdb

# The working directory.
# dump.rdb和aop文件都在会这个目录下面
dir ./

这里我们可以自己来修改这里的持久化文件的目录

dbfilename dump.rdb
dir /usr/local/redis-5.0.3/dofile
②多久操作会持久化

可以看源文件中的注释

save 900 1
save 300 10
save 60 10000

:如果一旦将save都注释掉,那么就意味着关闭了RDB持久化

2.2、AOF(append-only file)

快照功能并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化,将修改的每一条指令记录进文件appendonly.aof中(先写入os cache,每隔一段时间fsync到磁盘)

比如执行命令“set lig 666”,aof文件里会记录如下数据

*3
$3
set
$5
lig
$3
666

这是一种resp协议格式数据,星号后面的数字代表命令有多少个参数,$号后面的数字代表这个参数有几个字符

注意,如果执行带过期时间的set命令,aof文件里记录的是并不是执行的原始命令,而是记录key过期的时间戳

比如执行“set tuling 888 ex 1000”,对应aof文件里记录如下

*3
$3
set
$6
tuling
$3
888
*3
$9
PEXPIREAT
$6
tuling
$13
1604249786301

2.2.1、开启AOF

你可以通过修改配置文件来打开 AOF 功能:

appendonly yes

从现在开始, 每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾。

这样的话, 当 Redis 重新启动时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。

你可以配置 Redis 多久才将数据 fsync 到磁盘一次。

有三个选项:

appendfsync always:每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
appendfsync everysec:每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。
appendfsync no:从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。

推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。

2.2.2、AOF重写

OF文件里可能有太多没用指令,所以AOF会定期根据内存的最新数据生成aof文件

例如,执行了如下几条命令:

127.0.0.1:6379> incr readcount
(integer) 1
127.0.0.1:6379> incr readcount
(integer) 2
127.0.0.1:6379> incr readcount
(integer) 3
127.0.0.1:6379> incr readcount
(integer) 4
127.0.0.1:6379> incr readcount
(integer) 5

重写后AOF文件里变成

*3
$3
SET
$2
readcount
$1
5

如下两个配置可以控制AOF自动重写频率

# auto-aof-rewrite-min-size 64mb   //aof文件至少要达到64M才会自动重写,文件太小恢复速度本来就很快,重写的意义不大
# auto-aof-rewrite-percentage 100  //aof文件自上一次重写后文件大小增长了100%则再次触发重写

当然AOF还可以手动重写,进入redis客户端执行命令bgrewriteaof重写AOF

注意,AOF重写redis会fork出一个子进程去做(与bgsave命令类似),不会对redis正常命令处理有太多影响

2.3、AOF和RDB的选择

命令 RDB AOF
启动优先级
体积
恢复速度
数据安全性 容易丢数据 根据策略决定

生产环境可以都启用,redis启动时如果既有rdb文件又有aof文件则优先选择aof文件恢复数据,因为aof一般来说数据更全一点。

2.3.1、Redis 4.0之后的混合持久化

重启 Redis 时,我们很少使用 RDB来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 RDB来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。 Redis 4.0版本及其之后为了解决这个问题,带来了一个新的持久化选项——混合持久化。

通过如下配置可以开启混合持久化(必须先开启aof):

aof-use-rdb-preamble yes   

如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。

于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,因此重启效率大幅得到提升。

混合持久化AOF文件结构如下

Redis单机模式主从模式哨兵模式集群模式搭建_第1张图片

2.4、 Redis的四种数据备份策略

  1. 写crontab定时调度脚本,每小时都copy一份rdb或aof的备份到一个目录中去,仅仅保留最近48小时的备份
  2. 每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份
  3. 每次copy备份的时候,都把太旧的备份给删了
  4. 每天晚上将当前机器上的备份复制一份到其他机器上,以防机器损坏

三、主从架构

主从架构如下所示:

Redis单机模式主从模式哨兵模式集群模式搭建_第2张图片

3.1、环境搭建

我在本地虚拟机中已经安装好了redis,那么首先按照单机配置,启动6379

我在做准备工作之前,首先在redis-5.0.3目录下面新建两个目录:

  • data目录:data/6380和data/6379用来存放持久化文件
  • conf目录:6379和6380用来存放redis-6379.conf和redis-6380.conf配置文件
1、复制一份redis.conf文件

2、将相关配置修改为如下值:
port 6380
pidfile /var/run/redis_6380.pid  # 把pid进程号写入pidfile配置的文件
logfile "6380.log"
dir /usr/local/redis-5.0.3/data/6380  # 指定数据存放目录
# 需要注释掉bind
# bind 127.0.0.1(bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可)

3、配置主从复制
replicaof 192.168.3.17 6379   # 从本机6379的redis实例复制数据,Redis 5.0之前使用slaveof
replica-read-only yes  # 配置从节点只读

4、启动从节点
redis-server redis.conf

5、连接从节点
redis-cli -p 6380

6、测试在6379实例上写数据,6380实例是否能及时同步新修改数据

7、可以自己再配置一个6381的从节点

在本机测试之后完美搭建成功,然后模拟复制搭建一个6381端口的redis。

3.2、Redis主从工作原理

如果你为master配置了一个slave,不管这个slave是否是第一次连接上Master,它都会发送一个PSYNC命令给master请求复制数据。

master收到PSYNC命令后,会在后台进行数据持久化通过bgsave生成最新的rdb快照文件,持久化期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。当持久化进行完毕以后,master会把这份rdb文件数据集发送给slave,slave会把接收到的数据进行持久化生成rdb,然后再加载到内存中。然后,master再将之前缓存在内存中的命令发送给slave。

当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多个slave并发连接请求,它只会进行一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送给多个并发连接的slave。

主从复制(全量复制)流程图

Redis单机模式主从模式哨兵模式集群模式搭建_第3张图片

数据部分复制

当master和slave断开重连后,一般都会对整份数据进行复制。但从redis2.8版本开始,redis改用可以支持部分数据复制的命令PSYNC去master同步数据,slave与master能够在网络连接断开重连后只进行部分数据复制(断点续传)。

master会在其内存中创建一个复制数据用的缓存队列,缓存最近一段时间的数据,master和它所有的slave都维护了复制的数据下标offset和master的进程id,因此,当网络连接断开后,slave会请求master继续进行未完成的复制,从所记录的数据下标开始。如果master进程id变化了,或者从节点数据下标offset太旧,已经不在master的缓存队列里了,那么将会进行一次全量数据的复制。

Redis单机模式主从模式哨兵模式集群模式搭建_第4张图片

注意点:如果有很多从节点,为了缓解主从复制风暴(多个从节点同时复制主节点导致主节点压力过大),可以做如下架构,让部分从节点与从节点(与主节点同步)同步数据

Redis单机模式主从模式哨兵模式集群模式搭建_第5张图片

3.3、使用Jedis测试

首先新建SpringBoot项目,引入坐标依赖:

<dependency>
    <groupId>redis.clientsgroupId>
    <artifactId>jedisartifactId>
    <version>2.9.0version>
dependency>

编写测试代码:

public class JedisSingleTest {
  public static void main(String[] args) throws IOException {

    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    jedisPoolConfig.setMaxTotal(20);
    jedisPoolConfig.setMaxIdle(10);
    jedisPoolConfig.setMinIdle(5);

    // timeout,这里既是连接超时又是读写超时,从Jedis 2.8开始有区分connectionTimeout和soTimeout的构造函数
    JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.3.17", 6379, 3000, null);

    Jedis jedis = null;
    try {
      //从redis连接池里拿出一个连接执行命令
      jedis = jedisPool.getResource();

      //******* jedis普通操作示例 ********
      System.out.println(jedis.set("single1", "liguang"));
      System.out.println(jedis.get("single1"));
      
      
          //******* 管道示例 ********
          //管道的命令执行方式:cat redis.txt | redis-cli -h 127.0.0.1 -a password - p 6379 --pipe
          Pipeline pl = jedis.pipelined();
            for (int i = 0; i < 10; i++) {
                pl.incr("pipelineKey");
                pl.set("name" + i, "name");
                //模拟管道报错
                pl.setbit("name", -1, true);
            }
            List<Object> results = pl.syncAndReturnAll();
            System.out.println(results);
      
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
      if (jedis != null)
        jedis.close();
    }
  }
}

管道(Pipeline)

客户端可以一次性发送多个请求而不用等待服务器的响应,待所有命令都发送完后再一次性读取服务的响应,这样可以极大的降低多条命令执行的网络传输开销,管道执行多条命令的网络开销实际上只相当于一次命令执行的网络开销。需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。

pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的响应中得到信息;也就是pipeline并不是表达“所有command都一起成功”的语义,管道中前面命令失败,后面命令不会有影响,继续执行。

详细代码示例见下面jedis连接示例:

Pipeline pl = jedis.pipelined();
for (int i = 0; i < 10; i++) {
  pl.incr("pipelineKey");
  pl.set("name" + i, "name");
  // 模拟管道报错
  // pl.setbit("name", -1, true);
}
List<Object> results = pl.syncAndReturnAll();
System.out.println(results);

四、哨兵高可用架构

Redis单机模式主从模式哨兵模式集群模式搭建_第6张图片

sentinel哨兵是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点

哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点,不会每次都通过sentinel代理访问redis的主节点,当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis主节点通知给client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)

4.1、环境搭建

1、复制一份sentinel.conf文件
cp sentinel.conf sentinel-26379.conf

2、将相关配置修改为如下值:
port 26379
daemonize yes
pidfile "/var/run/redis-sentinel-26379.pid"
logfile "26379.log"
dir "/usr/local/redis-5.0.3/data"
# sentinel monitor <master-redis-name> <master-redis-ip> <master-redis-port> <quorum>
# quorum是一个数字,指明当有多少个sentinel认为一个master失效时(值一般为:sentinel总数/2 + 1),master才算真正失效
sentinel monitor mymaster 192.168.0.60 6379 2   # mymaster这个名字随便取,客户端访问时会用到

3、启动sentinel哨兵实例
src/redis-sentinel sentinel-26379.conf

4、查看sentinel的info信息
src/redis-cli -p 26379
127.0.0.1:26379>info
可以看到Sentinel的info里已经识别出了redis的主从

5、可以自己再配置两个sentinel,端口2638026381,注意上述配置文件里的对应数字都要修改

sentinel集群都启动完毕后,会将哨兵集群的元数据信息写入所有sentinel的配置文件里去(追加在文件的最下面),我们查看下如下配置文件sentinel-26379.conf,如下所示:

# 代表redis主节点的从节点信息
sentinel known-replica mymaster 192.168.0.60 6380 
# 代表redis主节点的从节点信息
sentinel known-replica mymaster 192.168.0.60 6381 
# 代表感知到的其它哨兵节点    
sentinel known-sentinel mymaster 192.168.0.60 26380 52d0a5d70c1f90475b4fc03b6ce7c3c56935760f  
# 代表感知到的其它哨兵节点    
sentinel known-sentinel mymaster 192.168.0.60 26381 e9f530d3882f8043f76ebb8e1686438ba8bd5ca6  

当redis主节点如果挂了,哨兵集群会重新选举出新的redis主节点,同时会修改所有sentinel节点配置文件的集群元数据信息,比如6379的redis如果挂了,假设选举出的新主节点是6380,则sentinel文件里的集群元数据信息会变成如下所示:

#代表主节点的从节点信息
sentinel known-replica mymaster 192.168.0.60 6379 
#代表主节点的从节点信息    
sentinel known-replica mymaster 192.168.0.60 6381 
#代表感知到的其它哨兵节点    
sentinel known-sentinel mymaster 192.168.0.60 26380 52d0a5d70c1f90475b4fc03b6ce7c3c56935760f
#代表感知到的其它哨兵节点    
sentinel known-sentinel mymaster 192.168.0.60 26381 e9f530d3882f8043f76ebb8e1686438ba8bd5ca6  

同时还会修改sentinel文件里之前配置的mymaster对应的6379端口,改为6380

sentinel monitor mymaster 192.168.0.60 6380 2

当6379的redis实例再次启动时,哨兵集群根据集群元数据信息就可以将6379端口的redis节点作为从节点加入集群

4.2、哨兵代码测试

public class JedisSentinelTest {
    public static void main(String[] args) throws IOException {

        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(20);
        config.setMaxIdle(10);
        config.setMinIdle(5);

        String masterName = "mymaster";
        Set<String> sentinels = new HashSet<String>();
        sentinels.add(new HostAndPort("192.168.0.60",26379).toString());
        sentinels.add(new HostAndPort("192.168.0.60",26380).toString());
        sentinels.add(new HostAndPort("192.168.0.60",26381).toString());
        //JedisSentinelPool其实本质跟JedisPool类似,都是与redis主节点建立的连接池
        //JedisSentinelPool并不是说与sentinel建立的连接池,而是通过sentinel发现redis主节点并与其建立连接
        JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinels, config, 3000, null);
        Jedis jedis = null;
        try {
            jedis = jedisSentinelPool.getResource();
            System.out.println(jedis.set("sentinel", "name"));
            System.out.println(jedis.get("sentinel"));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
            if (jedis != null)
                jedis.close();
        }
    }
}

4.3、SpringBoot整合Redis哨兵架构

1、引入相关依赖

<dependency>
   <groupId>org.springframework.bootgroupId>
   <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

<dependency>
   <groupId>org.apache.commonsgroupId>
   <artifactId>commons-pool2artifactId>
dependency>

2、springboot项目核心配置

server:
  port: 8080

spring:
  redis:
    database: 0
    timeout: 3000
    sentinel:    #哨兵模式
      master: mymaster #主服务器所在集群名称
     nodes: 192.168.0.60:26379,192.168.0.60:26380,192.168.0.60:26381
   lettuce:
      pool:
        max-idle: 50
        min-idle: 10
        max-active: 100
        max-wait: 1000

3、代码如下

@RestController
public class IndexController {

    private static final Logger logger = LoggerFactory.getLogger(IndexController.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 测试节点挂了哨兵重新选举新的master节点,客户端是否能动态感知到
     * 新的master选举出来后,哨兵会把消息发布出去,客户端实际上是实现了一个消息监听机制,
     * 当哨兵把新master的消息发布出去,客户端会立马感知到新master的信息,从而动态切换访问的masterip
     *
     * @throws InterruptedException
     */
    @RequestMapping("/test_sentinel")
    public void testSentinel() throws InterruptedException {
        int i = 1;
        while (true){
            try {
                stringRedisTemplate.opsForValue().set("name"+i, i+"");
                System.out.println("设置key:"+ "name" + i);
                i++;
                Thread.sleep(1000);
            }catch (Exception e){
                logger.error("错误:", e);
            }
        }
    }
}

存在问题

但是测试证明,在sentinel选择节点期间,如果还有请求进来,会丢失部分请求数据。

4.4、补充说明

StringRedisTemplate与RedisTemplate详解

spring 封装了 RedisTemplate 对象来进行对redis的各种操作,它支持所有的 redis 原生的 api。在RedisTemplate中提供了几个常用的接口方法的使用,分别是:

private ValueOperations<K, V> valueOps;
private HashOperations<K, V> hashOps;
private ListOperations<K, V> listOps;
private SetOperations<K, V> setOps;
private ZSetOperations<K, V> zSetOps;

RedisTemplate中定义了对5种数据结构操作

redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set

StringRedisTemplate继承自RedisTemplate,也一样拥有上面这些操作。

StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。

RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。

所谓的JDK序列化就是将对象序列化成/xxx/yyy/ddd,通常来说,公司都会采用自定义操作。

Redis客户端命令对应的RedisTemplate中的方法列表

String类型结构
Redis RedisTemplate rt
set key value rt.opsForValue().set(“key”,“value”)
get key rt.opsForValue().get(“key”)
del key rt.delete(“key”)
strlen key rt.opsForValue().size(“key”)
getset key value rt.opsForValue().getAndSet(“key”,“value”)
getrange key start end rt.opsForValue().get(“key”,start,end)
append key value rt.opsForValue().append(“key”,“value”)
Hash结构
hmset key field1 value1 field2 value2… rt.opsForHash().putAll(“key”,map) //map是一个集合对象
hset key field value rt.opsForHash().put(“key”,“field”,“value”)
hexists key field rt.opsForHash().hasKey(“key”,“field”)
hgetall key rt.opsForHash().entries(“key”) //返回Map对象
hvals key rt.opsForHash().values(“key”) //返回List对象
hkeys key rt.opsForHash().keys(“key”) //返回List对象
hmget key field1 field2… rt.opsForHash().multiGet(“key”,keyList)
hsetnx key field value rt.opsForHash().putIfAbsent(“key”,“field”,“value”
hdel key field1 field2 rt.opsForHash().delete(“key”,“field1”,“field2”)
hget key field rt.opsForHash().get(“key”,“field”)
List结构
lpush list node1 node2 node3… rt.opsForList().leftPush(“list”,“node”)
rt.opsForList().leftPushAll(“list”,list) //list是集合对象
rpush list node1 node2 node3… rt.opsForList().rightPush(“list”,“node”)
rt.opsForList().rightPushAll(“list”,list) //list是集合对象
lindex key index rt.opsForList().index(“list”, index)
llen key rt.opsForList().size(“key”)
lpop key rt.opsForList().leftPop(“key”)
rpop key rt.opsForList().rightPop(“key”)
lpushx list node rt.opsForList().leftPushIfPresent(“list”,“node”)
rpushx list node rt.opsForList().rightPushIfPresent(“list”,“node”)
lrange list start end rt.opsForList().range(“list”,start,end)
lrem list count value rt.opsForList().remove(“list”,count,“value”)
lset key index value rt.opsForList().set(“list”,index,“value”)
Set结构
sadd key member1 member2… rt.boundSetOps(“key”).add(“member1”,“member2”,…)
rt.opsForSet().add(“key”, set) //set是一个集合对象
scard key rt.opsForSet().size(“key”)
sidff key1 key2 rt.opsForSet().difference(“key1”,“key2”) //返回一个集合对象
sinter key1 key2 rt.opsForSet().intersect(“key1”,“key2”)//同上
sunion key1 key2 rt.opsForSet().union(“key1”,“key2”)//同上
sdiffstore des key1 key2 rt.opsForSet().differenceAndStore(“key1”,“key2”,“des”)
sinter des key1 key2 rt.opsForSet().intersectAndStore(“key1”,“key2”,“des”)
sunionstore des key1 key2 rt.opsForSet().unionAndStore(“key1”,“key2”,“des”)
sismember key member rt.opsForSet().isMember(“key”,“member”)
smembers key rt.opsForSet().members(“key”)
spop key rt.opsForSet().pop(“key”)
srandmember key count rt.opsForSet().randomMember(“key”,count)
srem key member1 member2… rt.opsForSet().remove(“key”,“member1”,“member2”,…)

封装RedisTemplate工具类

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object,Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        serializer.setObjectMapper(serializingObjectMapper());
        // value值的序列化采用jacksonRedisSerializer
        template.setValueSerializer(serializer);
        template.setHashValueSerializer(serializer);
        // key的序列化采用StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());

        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    private ObjectMapper serializingObjectMapper(){
        //创建jsr310包下的javaTimeModule对象,其中包含了jdk8以后的时间类型序列化和反序列化配置
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        //LocalDateTime序列化设置为yyyy-MM-dd HH:mm:ss
        LocalDateTimeSerializer localDateTimeSerializer =
        new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        //增加序列化到配置当中
        javaTimeModule.addSerializer(LocalDateTime.class,localDateTimeSerializer);
        //LocalDateTime反序列化设置为yyyy-MM-dd HH:mm:ss
        LocalDateTimeDeserializer localDateTimeDeserializer =
        new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        //增加反序列化到配置当中
        javaTimeModule.addDeserializer(LocalDateTime.class,localDateTimeDeserializer);
        ObjectMapper objectMapper = new ObjectMapper();
        		       objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL,JsonTypeInfo.As.PROPERTY);
        objectMapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        //注册配置
        objectMapper.registerModule(javaTimeModule);
        return objectMapper;
    }

}

然后将RedisTemplate分装成工具类

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import lombok.AllArgsConstructor;

/**
 * @Description redis工具类
 * @Author liguang
 * @Date 2021/4/12 18:07
 */
@Component
@AllArgsConstructor
public class RedisService<K,V> {

    private final RedisTemplate<K,V> redisTemplate;

    /**
     * 添加、修改缓存
     * @param key   key
     * @param value value
     * @return boolean
     * @author liguang
     * @date 2019/11/1 19:50
     **/
    public boolean set(K key,V value){
        redisTemplate.opsForValue().set(key,value);
        return true;
    }

    /**
     * 添加、修改缓存并且设置过期时间--原子性操作,但是只有setEX操作,不包含setNX
     * @param key      key
     * @param value    value
     * @param expire   过期时间
     * @param timeUnit 时间单位
     * @return boolean
     * @author liguang
     * @date 2019/11/1 19:50
     **/
    public boolean set(K key,V value,long expire,TimeUnit timeUnit){
        redisTemplate.opsForValue().set(key,value,expire,timeUnit);
        return true;
    }

    /**
     * 添加、修改缓存并且设置过期时间--时间单位为:秒
     * @param key    key
     * @param value  value
     * @param expire 过期时间
     * @return boolean
     * @author liguang
     * @date 2019/11/1 19:50
     **/
    public boolean set(K key,V value,long expire){
        return set(key,value,expire,TimeUnit.SECONDS);
    }

    /**
     * 添加、修改缓存并且设置过期时间--原子性操作用于分布式加锁操作等。这里包含了sexNX和setEX两个操作
     * @param key      key
     * @param value    value
     * @param expire   过期时间
     * @param timeUnit 时间单位
     * @return boolean
     * @author liguang
     * @date 2019/11/1 19:50
     **/
    public boolean setIfAbsent(K key,V value,long expire,TimeUnit timeUnit){
        return Optional.ofNullable(redisTemplate.opsForValue().setIfAbsent(key,value,expire,timeUnit)).orElse(false);
    }

    /**
     * 获取缓存信息
     * @param key key
     * @return V
     * @author liguang
     * @date 2019/11/1 19:50
     **/
    public V get(K key){
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 对key添加过期时间
     * @param key      key
     * @param expire   过期时间
     * @param timeUnit 时间单位
     * @return boolean
     * @author liguang
     * @date 2019/11/1 19:50
     **/
    public boolean expire(K key,long expire,TimeUnit timeUnit){
        return Optional.ofNullable(redisTemplate.expire(key,expire,timeUnit)).orElse(false);
    }

    /**
     * 获取key的过期时间 默认单位:秒。-2表示不存在,-1表示永久,正数为正常时间。如果获取失败默认返回-2
     * @param key key
     * @return Long
     * @author liguang
     * @date 2019/11/1 19:50
     **/
    public Long getExpire(K key){
        return redisTemplate.getExpire(key);
    }

    /**
     * 根据时间单位,获取key的过期时间 获取key的过期时间。 -2表示不存在,-1表示永久,正数为正常时间。如果获取失败默认返回-2
     * @param key key
     * @return Long
     * @author liguang
     * @date 2019/11/1 19:50
     **/
    public Long getExpire(K key,TimeUnit timeUnit){
        return redisTemplate.getExpire(key,timeUnit);
    }

    /**
     * 删除缓存信息
     * @param key key
     * @return boolean
     * @author liguang
     * @date 2019/11/1 19:50
     **/
    public boolean delete(K key){
        return Optional.ofNullable(redisTemplate.delete(key)).orElse(false);
    }

    /**
     * 将 key 中储存的数字值增一。 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
     * 

* 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 * @param key key * @return Long * @author liguang * @date 2019/11/1 19:50 **/ public Long increment(K key){ return redisTemplate.opsForValue().increment(key); } /** * 将 key 中储存的数字值加上增量 * @param key key * @param increment 增量 * @return Long * @author liguang * @date 2019/11/1 19:50 **/ public Long increment(K key,long increment){ return redisTemplate.opsForValue().increment(key,increment); } /** * 将 key 中储存的数字值加上增量 * @param key key * @param increment 增量 * @return Double * @author liguang * @date 2019/11/1 19:50 **/ public Double increment(K key,double increment){ return redisTemplate.opsForValue().increment(key,increment); } /** * 将 key 中储存的数字值减一。 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。 *

* 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 * @param key key * @return Long * @author liguang * @date 2019/11/1 19:50 **/ public Long decrement(K key){ return redisTemplate.opsForValue().decrement(key); } /** * 将 key 中储存的数字值减去指定数值 * @param key key * @param increment 指定数值-减量 * @return Long * @author liguang * @date 2019/11/1 19:50 **/ public Long decrement(K key,long increment){ return redisTemplate.opsForValue().decrement(key,increment); } /** * 设置hash存储,包含redis key对应一个hash桶。默认value为key * @param key redis key * @param hashKey hashKey * @return boolean * @author liguang * @date 2021/4/20 12:35 **/ public boolean setForHash(K key,Object hashKey){ redisTemplate.opsForHash().put(key,hashKey,hashKey); return true; } /** * 设置hash存储,包含redis key对应一个hash桶 * @param key redis key * @param hashKey hashKey * @param value value * @return boolean * @author liguang * @date 2021/4/20 12:35 **/ public boolean setForHash(K key,Object hashKey,Object value){ redisTemplate.opsForHash().put(key,hashKey,value); return true; } /** * 根据redis key和hash key获取值 * @param key redis key * @param hashKey hashKey * @return java.lang.Object * @author liguang * @date 2021/4/20 12:34 **/ public Object getForHash(K key,Object hashKey){ return redisTemplate.opsForHash().get(key,hashKey); } /** * 根据redis key删除指定hashkey的值,可以为多个 * @param key redis key * @param hashKey hashKey * @return java.lang.long * @author liguang * @date 2021/4/20 12:34 **/ public long deleteForHash(K key,Object... hashKey){ return redisTemplate.opsForHash().delete(key,hashKey); } /** * 根据redis key获取hash所有值 * @param key key * @return java.util.Map * @author liguang * @date 2021/4/20 12:33 **/ public Map<Object,Object> getForHashEntries(K key){ return redisTemplate.opsForHash().entries(key); } /** * 设置set存储,支持设置多个value的传入 * @param key key * @param values value * @return long * @author liguang * @date 2021/4/20 12:35 **/ public long setForSet(K key,V... values){ return Optional.ofNullable(redisTemplate.opsForSet().add(key,values)).orElse(0L); } /** * 根据redis key和判断value是否存在set集合中 * @param key redis key * @param value value * @return java.lang.boolean * @author liguang * @date 2021/4/20 12:34 **/ public boolean checkInSet(K key,Object value){ return Optional.ofNullable(redisTemplate.opsForSet().isMember(key,value)).orElse(false); } /** * 根据key获取set集合全部内容 * @param key key * @return java.util.Set * @author liguang * @date 2021/5/17 11:55 **/ public Set<V> getForSet(K key){ return redisTemplate.opsForSet().members(key); } /** * 根据key获取集合数据条数 * @param key key * @return long * @author liguang * @date 2021/5/17 11:56 **/ public long getSizeForSet(K key){ return Optional.ofNullable(redisTemplate.opsForSet().size(key)).orElse(0L); } /** * 根据redis key删除set集合指定元素,value可以为多个 * @param key key * @param values values * @return long * @author liguang * @date 2021/4/20 12:33 **/ public long removeFromSet(K key,Object... values){ return Optional.ofNullable(redisTemplate.opsForSet().remove(key,values)).orElse(0L); } /** * 根据key获取list集合里的大小 * @param key key * @return java.lang.Long * @author liguang * @date 2021/4/21 10:54 **/ public Long getListSize(K key){ return redisTemplate.opsForList().size(key); } /** * 在集合的最左边(前面)插入数据,并且返回当前集合的size,插入失败返回-1 * @param key key * @param value value * @return java.lang.Long * @author liguang * @date 2021/4/21 11:13 **/ public Long leftPush(K key,V value){ return redisTemplate.opsForList().leftPush(key,value); } /** * 在某个值的左边(前面)插入数据,把value值放到集合中指定的pivot的前面,如果pivot存在的话。如果不存在则插入失败,并且返回-1 * @param key key * @param pivot 参考值 * @param value value * @return Long 当前集合size * @author liguang * @date 2021/4/21 10:59 **/ public Long leftPush(K key,V pivot,V value){ return redisTemplate.opsForList().leftPush(key,pivot,value); } /** * 在集合的最左边(前面)插入数据,注意集合的顺序,index在前面的先插入,也就是说如果集合是[1,2,3],插入以后变成[3,2,1] *

* 返回当前集合的size,-1表示插入失败 * @param key key * @param values values * @return java.lang.Long 当前集合的size,-1表示插入失败 * @author liguang * @date 2021/4/21 11:13 **/ public Long leftPushAll(K key,V... values){ return redisTemplate.opsForList().leftPushAll(key,values); } /** * 在集合的最左边(前面)插入数据,注意集合的顺序,index在前面的先插入,也就是说如果集合是[1,2,3],插入以后变成[3,2,1] *

* 返回当前集合的size,-1表示插入失败 * @param key key * @param values values--集合数据 * @return java.lang.Long * @author liguang * @date 2021/4/21 11:13 **/ public Long leftPushAll(K key,Collection<V> values){ return redisTemplate.opsForList().leftPushAll(key,values); } /** * 在集合的最右边(后面)插入数据,并且返回当前集合的size,插入失败返回-1 * @param key key * @param value value * @return java.lang.Long * @author liguang * @date 2021/4/21 11:13 **/ public Long rightPush(K key,V value){ return redisTemplate.opsForList().rightPush(key,value); } /** * 在某个值的最右边(后面)插入数据,把value值放到集合中指定的pivot的后面,如果pivot存在的话。如果不存在则插入失败,并且返回-1 * @param key key * @param pivot 参考值 * @param value value * @return Long 当前集合size * @author liguang * @date 2021/4/21 10:59 **/ public Long rightPush(K key,V pivot,V value){ return redisTemplate.opsForList().leftPush(key,pivot,value); } /** * 在集合的最右边(后面)插入数据,按顺序插入 *

* 返回当前集合的size,-1表示插入失败 * @param key key * @param values values * @return java.lang.Long 当前集合的size,-1表示插入失败 * @author liguang * @date 2021/4/21 11:13 **/ public Long rightPushAll(K key,V... values){ return redisTemplate.opsForList().rightPushAll(key,values); } /** * 在集合的最右边(后面)插入数据,按顺序插入 *

* 返回当前集合的size,-1表示插入失败 * @param key key * @param values values--集合数据 * @return java.lang.Long * @author liguang * @date 2021/4/21 11:13 **/ public Long rightPushAll(K key,Collection<V> values){ return redisTemplate.opsForList().rightPushAll(key,values); } /** * 从集合的最左边(前面)当中取出元素 * @param key key * @return V * @author liguang * @date 2021/4/21 12:29 **/ public V leftPop(K key){ return redisTemplate.opsForList().leftPop(key); } /** * 从集合的最右边(后面)当中取出元素 * @param key key * @return V * @author liguang * @date 2021/4/21 12:29 **/ public V rightPop(K key){ return redisTemplate.opsForList().rightPop(key); } /** * 根据key获取集合数据--因为先查找范围才返回数据,是两个操作,不能保证数据完全准确性,如需要保证数据完整需要通过lua脚本 * @param key key * @return java.util.List * @author liguang * @date 2021/4/21 11:28 **/ public List<V> getForList(K key){ Long size = getListSize(key); return size == null || size == 0 ? new ArrayList<>() : getForList(key,0,size); } /** * 根据key,获取list下标范围集合的数据 * @param key key * @param start 开始下标 * @param end 结束下标 * @return java.util.List * @author liguang * @date 2021/4/21 11:29 **/ public List<V> getForList(K key,long start,long end){ return redisTemplate.opsForList().range(key,start,end); } /** * 根据key获取指定集合下标数据,注意:如果不存在会返回null * @param key key * @param index 下标 * @return V * @author liguang * @date 2021/4/21 11:30 **/ public V index(K key,long index){ return redisTemplate.opsForList().index(key,index); } }

五、集群架构

哨兵模式:

Redis单机模式主从模式哨兵模式集群模式搭建_第7张图片

在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态,如果master节点异常,则会做主从切换,将某一台slave作为master,哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般,特别是在主从切换的瞬间存在访问瞬断的情况,而且哨兵模式只有一个主节点对外提供服务,没法支持很高的并发,且单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率

高可用集群模式

Redis单机模式主从模式哨兵模式集群模式搭建_第8张图片

redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。Redis集群不需要sentinel哨兵·也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。redis集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单

5.1、Redis集群搭建

redis集群需要至少三个master节点,我们这里搭建三个master节点,并且给每个master再搭建一个slave节点,总共6个redis节点,这里用三台机器部署6个redis实例,每台机器一主一从,搭建集群的步骤如下:

1、配置文件修改

第一步:在第一台机器的/usr/local下创建文件夹redis-cluster,然后在其下面分别创建2个文件夾如下
(1)mkdir -p /usr/local/redis-cluster
(2)mkdir 8001 8004

第一步:把之前的redis.conf配置文件copy到8001下,修改如下内容:
(1)daemonize yes
(2)port 8001(分别对每个机器的端口号进行设置)
(3)pidfile /var/run/redis_8001.pid  # 把pid进程号写入pidfile配置的文件
(4)dir /usr/local/redis-cluster/8001/(指定数据文件存放位置,必须要指定不同的目录位置,不然会丢失数据)
(5)cluster-enabled yes(启动集群模式)
(6)cluster-config-file nodes-8001.conf(集群节点信息文件,这里800x最好和port对应上)
(7)cluster-node-timeout 10000
 (8)# bind 127.0.0.1(bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可)
 (9)protected-mode  no   (关闭保护模式)
 (10)appendonly yes
如果要设置密码需要增加如下配置:
 (11)requirepass zhuge     (设置redis访问密码)
 (12)masterauth zhuge      (设置集群节点间访问密码,跟上面一致)

第三步:把修改后的配置文件,copy到8004,修改第2346项里的端口号,可以用批量替换:
:%s/源字符串/目的字符串/g 

第四步:另外两台机器也需要做上面几步操作,第二台机器用80028005,第三台机器用80038006

第五步:分别启动6个redis实例,然后检查是否启动成功
(1/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/800*/redis.conf
(2)ps -ef | grep redis 查看是否启动成功
    
第六步:用redis-cli创建整个redis集群(redis5以前的版本集群是依靠ruby脚本redis-trib.rb实现)
# 下面命令里的1代表为每个创建的主服务器节点创建一个从服务器节点
# 执行这条命令需要确认三台机器之间的redis实例要能相互访问,可以先简单把所有机器防火墙关掉,如果不关闭防火墙则需要打开redis服务端口和集群节点gossip通信端口16379(默认是在redis端口号上加1W)
# 关闭防火墙
# systemctl stop firewalld # 临时关闭防火墙
# systemctl disable firewalld # 禁止开机启动
# 注意:下面这条创建集群的命令大家不要直接复制,里面的空格编码可能有问题导致创建集群不成功
(1/usr/local/redis-5.0.3/src/redis-cli -a zhuge --cluster create --cluster-replicas 1 192.168.0.61:8001 192.168.0.62:8002 192.168.0.63:8003 192.168.0.61:8004 192.168.0.62:8005 192.168.0.63:8006 

第七步:验证集群:
(1)连接任意一个客户端即可:./redis-cli -c -h -p (-a访问服务端密码,-c表示集群模式,指定ip地址和端口号)
    如:/usr/local/redis-5.0.3/src/redis-cli -a zhuge -c -h 192.168.0.61 -p 800*2)进行验证: cluster info(查看集群信息)、cluster nodes(查看节点列表)
(3)进行数据操作验证
(4)关闭集群则需要逐个进行关闭,使用命令:
/usr/local/redis-5.0.3/src/redis-cli -a zhuge -c -h 192.168.0.60 -p 800* shutdown

技巧:首先在三台机器下分别创建对应的目录,修改完其中一个之后,同一台机器上直接使用cp,然后直接搜索对应的端口号进行修改即可。同理,如果在其他机器上直接使用:scp redis.conf [email protected] /usr/local/redis-cluster/880X目录下即可。

注意:

[root@master bin]# ./redis-cli --cluster create 192.168.xxx.21:7001 192.168.xxx.21:7002 192.168.xxx.21:7003 192.168.xxx.21:8001 192.168.xxx.21:8002 192.168.xxx.21:8003 --cluster-replicas 1 -a 123456

上面的命令只能在新创健集群的时候执行一次,目的是为了建立内部各个节点的对应关系,比如主从关系,这些关系仅且只能在一个集群中初始化时对应一次;

如果再此执行,则会出现如下错误:

[root@master bin]# ./redis-cli --cluster create 192.168.xxx.21:7001 192.168.xxx.21:7002 192.168.xxx.21:7003 192.168.xxx.21:8001 192.168.xxx.21:8002 192.168.xxx.21:8003 --cluster-replicas 1 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
[ERR] Node 192.168.xxx.21:7001 is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0.

在创建集群完成之后,只需要再次重启每个服务即可。不需要再次执行创建集群的命令。

2、逐个启动命令

如下所示:

/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/8001/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/8002/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/8003/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/8004/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/8005/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/8006/redis.conf

3、查看某一个节点信息

连接集群中的某一个节点命令如下所示:

/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.0.61 -p 8001

然后执行命令创建集群:

/usr/local/redis-5.0.3/src/redis-cli -a liguang --cluster create --cluster-replicas 1 192.168.1.2:8001 192.168.1.4:8002 192.168.1.5:8003 192.168.1.2:8004 192.168.1.4:8005 192.168.1.5:8006 

创建好的集群如下所示:

Redis单机模式主从模式哨兵模式集群模式搭建_第9张图片

然后选择yes之后,会出现以下界面

Redis单机模式主从模式哨兵模式集群模式搭建_第10张图片

4、查看集群状态

192.168.0.61:8001> cluster  nodes

Redis单机模式主从模式哨兵模式集群模式搭建_第11张图片

从上图可以看出,整个集群运行正常,三个master节点和三个slave节点,8001端口的实例节点存储0-5460这些hash槽,8002端口的实例节点存储5461-10922这些hash槽,8003端口的实例节点存储10923-16383这些hash槽,这三个master节点存储的所有hash槽组成redis集群的存储槽位,slave点是每个主节点的备份从节点,不显示存储槽位

5、关闭集群命令

关闭集群则需要逐个进行关闭,使用命令

/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.0.60 -p 800* shutdown

逐一进行关闭

/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.1.2 -p 8001 shutdown
/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.1.2 -p 8004 shutdown

/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.1.4 -p 8002 shutdown
/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.1.4 -p 8005 shutdown

/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.1.5 -p 8003 shutdown
/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.1.5 -p 8006 shutdown

6、遇到问题

我已经在本地虚拟机上搭建好了集群,但是将笔记本带回家之后,发现虚拟机中的ip地址发生修改了,大无语事件。

然后又从网上找对应的解决方案,果然找到了。

下面来还原一下我遇到了的问题:

在我连接上了Redis集群之后,发现显示如下所示

在这里插入图片描述

然后从博客:https://blog.csdn.net/zzhongcy/article/details/119914677中了解到各个节点的状态是什么意思

下面摘抄重要内容:fail?表示的是当前节点无法和其联系,但其它节点可以

还需要注意一个flags字段为noaddr(这个是为了方便后续来进行增加节点进行的配置)

然后就去定位原因,为什么和其他节点无法联系。

首先查到的一篇博客:分析槽位是否是16384

参考博客:https://blog.csdn.net/liuyinfei_java/article/details/88557860?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-88557860-blog-106217811.235%5Ev35%5Epc_relevant_increate_t0_download_v2_base&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-88557860-blog-106217811.235%5Ev35%5Epc_relevant_increate_t0_download_v2_base&utm_relevant_index=1

但是当前我的机器查到的是16384,是准确的。

Redis单机模式主从模式哨兵模式集群模式搭建_第12张图片

继续去排查,然后去查看日志,发现报错如下所示:

Redis单机模式主从模式哨兵模式集群模式搭建_第13张图片

节点一直在尝试连接192.168.2.79:8002节点,但是我本地根本不存在192.168.2.79这个IP。

然后利用看到博客:https://blog.csdn.net/qq_44895681/article/details/125181531中查看属性cluster-announce-ip对应的ip是否和当前是相同的,去排查的时候,发现在我的redis.conf文件中属性cluster-announce-ip没有配置。

Redis单机模式主从模式哨兵模式集群模式搭建_第14张图片

既然已经排除了两个,现在网上已经没有了现成的答案了。

最终解决办法

1、先停止所有redis节点。
2、删除每个节点的缓存文件,包括node-6380.conf dump.rdp等文件。
3、重启每个redis节点。
4、重新创建redis集群。

先停掉所有节点:

/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.1.2 -p 8001 shutdown
/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.1.2 -p 8004 shutdown

/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.1.4 -p 8002 shutdown
/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.1.4 -p 8005 shutdown

/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.1.5 -p 8003 shutdown
/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.1.5 -p 8006 shutdown

删除缓存文件,然后重启每个节点

/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/8001/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/8002/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/8003/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/8004/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/8005/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/8006/redis.conf

然后重新创建集群

/usr/local/redis-5.0.3/src/redis-cli -a liguang --cluster create --cluster-replicas 1 192.168.1.2:8001 192.168.1.4:8002 192.168.1.5:8003 192.168.1.2:8004 192.168.1.4:8005 192.168.1.5:8006 

至此解决。

7、引出生产问题

参考博客:

https://huaweicloud.csdn.net/637ef5b6df016f70ae4ca759.html?spm=1001.2101.3001.6650.7&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Eactivity-7-114614978-blog-125181531.235%5Ev35%5Epc_relevant_increate_t0_download_v2_base&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Eactivity-7-114614978-blog-125181531.235%5Ev35%5Epc_relevant_increate_t0_download_v2_base&utm_relevant_index=11

8、帮助文档

cd /usr/local/redis-5.0.3
src/redis-cli --cluster help

Redis单机模式主从模式哨兵模式集群模式搭建_第15张图片

5.2、给集群添加节点

1、为什么考虑给Redis集群添加节点呢?

因为对于部分Redis节点来说,可能存在着热点数据,对于单台服务器的访问压力比较大。添加节点是为了分摊单台机器的压力

2、如何添加Redis节点

添加节点的时候,可以查看一下Redis中的提示文档中的命令

cd /usr/local/redis-5.0.3
src/redis-cli --cluster help

1.create:创建一个集群环境host1:port1 … hostN:portN

2.call:可以执行redis命令

3.add-node:将一个节点添加到集群里,第一个参数为新节点的ip:port,第二个参数为集群中任意一个已经存在的节点的ip:port

4.del-node:移除一个节点

5.reshard:重新分片

6.check:检查集群状态

注意:新添加的节点是没有哈希槽的,所以并不能正常存储数据,需要给新添加的节点分配哈希槽:分配哈希槽。

当添加节点成功以后,新增的节点不会有任何数据,因为它还没有分配任何的slot(hash槽),我们需要为新节点手工分配hash槽

当将存放热点数据Redis上的哈希槽迁移到新增节点上的哈希槽上时,数据也会跟着迁移过去。

下面再来配置一个主节点8007和从节点8009,然后拷贝并修改配置文件。

/usr/local/redis-5.0.3/src/redis-cli -a liguang  --cluster add-node 192.168.1.5:8007 192.168.1.2:8001

使用add-node命令新增一个主节点8007(master),前面的ip:port为新增节点,后面的ip:port为已知存在节点,看到日志最后有"[OK] New node added correctly"提示代表新节点加入成功

Redis单机模式主从模式哨兵模式集群模式搭建_第16张图片

3、查看添加进去的节点数据

/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.1.2 -p 8001
cluster nodes

Redis单机模式主从模式哨兵模式集群模式搭建_第17张图片

下面开始为其来分配哈希槽位

使用redis-cli命令为8007分配hash槽,找到集群中的任意一个主节点,对其进行重新分片工作

可以查看某个节点是否存在着热点数据,参考文档链接:https://blog.51cto.com/keep11/5098350?b=totalstatistic

我用8001给8007分配节点:

/usr/local/redis-5.0.3/src/redis-cli -a liguang --cluster reshard 192.168.1.2:8001

输出如下:

… …

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

(ps:需要多少个槽移动到新的节点上,自己设置,比如600个hash槽)

What is the receiving node ID? 2728a594a0498e98e4b83a537e19f9a0a3790f38

(ps:把这600个hash槽移动到哪个节点上去,需要指定节点id)

Please enter all the source node IDs.

Type ‘all’ to use all the nodes as source nodes for the hash slots.

Type ‘done’ once you entered all the source nodes IDs.

Source node 1:all

(ps:输入all为从所有主节点(8001,8002,8003)中分别抽取相应的槽数指定到新节点中,抽取的总槽数为600个)

… …

Do you want to proceed with the proposed reshard plan (yes/no)? yes

(ps:输入yes确认开始执行分片任务)

… …

查看下最新的集群状态

/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.0.61 -p 8001
192.168.0.61:8001> cluster nodes

Redis单机模式主从模式哨兵模式集群模式搭建_第18张图片

如上图所示,现在我们的8007已经有hash槽了,也就是说可以在8007上进行读写数据啦!到此为止我们的8007已经加入到集群中,并且是主节点(Master)

4、为新添加进去的节点添加从节点

添加从节点8009到集群中去并查看集群状态

首先将节点启动

/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/8009/redis.conf
/usr/local/redis-5.0.3/src/redis-cli -a liguang  --cluster add-node 192.168.1.5:8009 192.168.1.2:8001

Redis单机模式主从模式哨兵模式集群模式搭建_第19张图片

如图所示,还是一个master节点,没有被分配任何的hash槽

我们需要执行replicate命令来指定当前节点(从节点)的主节点id为哪个,首先需要连接新加的8009节点的客户端,然后使用集群命令进行操作,把当前的8009(slave)节点指定到一个主节点下(这里使用之前创建的8007主节点)

/usr/local/redis-5.0.3/src/redis-cli -a liguang -c -h 192.168.1.5 -p 8009
192.168.1.5:8009> cluster replicate b6f666c376081f0419dcd30e6c587b11fb39608b  #后面这串id为8007的节点id

Redis单机模式主从模式哨兵模式集群模式搭建_第20张图片

查看集群状态,8009节点已成功添加为8007节点的从节点

Redis单机模式主从模式哨兵模式集群模式搭建_第21张图片

5.3、删除节点

1、删除8009从节点

用del-node删除从节点8009,指定删除节点ip和端口,以及节点id(红色为8008节点id)

/usr/local/redis-5.0.3/src/redis-cli -a liguang --cluster del-node 192.168.1.5:8009 aa9060a05e011a99b7de4fbfe2bad84773a56a2f

提示信息如下所示:

在这里插入图片描述

再次查看集群状态,如下图所示,8009这个slave节点已经移除,并且该节点的redis服务也已被停止

Redis单机模式主从模式哨兵模式集群模式搭建_第22张图片

2、删除8007主节点

最后,我们尝试删除之前加入的主节点8007,这个步骤相对比较麻烦一些,因为主节点的里面是有分配了hash槽的,所以我们这里必须先把8007里的hash槽放入到其他的可用主节点中去,然后再进行移除节点操作,不然会出现数据丢失问题(目前只能把master的数据迁移到一个节点上,暂时做不了平均分配功能),执行命令如下:

/usr/local/redis-5.0.3/src/redis-cli -a liguang --cluster reshard 192.168.1.5:8007

输出如下:

… …

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

What is the receiving node ID? dfca1388f124dec92f394a7cc85cf98cfa02f86f

(ps:这里是需要把数据移动到哪?8001的主节点id)

Please enter all the source node IDs.

Type ‘all’ to use all the nodes as source nodes for the hash slots.

Type ‘done’ once you entered all the source nodes IDs.

Source node 1:2728a594a0498e98e4b83a537e19f9a0a3790f38

(ps:这里是需要数据源,也就是我们的8007节点id)

Source node 2:done

(ps:这里直接输入done 开始生成迁移计划)

… …

Do you want to proceed with the proposed reshard plan (yes/no)? Yes

(ps:这里输入yes开始迁移)

至此,我们已经成功的把8007主节点的数据迁移到8001上去了,我们可以看一下现在的集群状态如下图,你会发现8007下面已经没有任何hash槽了,证明迁移成功!

在这里插入图片描述

最后我们直接使用del-node命令删除8007主节点即可

/usr/local/redis-5.0.3/src/redis-cli -a liguang --cluster del-node 192.168.1.5:8007 b6f666c376081f0419dcd30e6c587b11fb39608b

查看集群状态,一切还原为最初始状态啦!大功告成!

在这里插入图片描述

5.4、Java代码操作Redis集群

简单版本

借助redis的java客户端jedis可以操作以上集群,引用jedis版本的maven坐标如下:

<dependency>
    <groupId>redis.clientsgroupId>
    <artifactId>jedisartifactId>
    <version>2.9.0version>
dependency>

Java编写访问redis集群的代码非常简单,如下所示:

public class JedisClusterTest {
    public static void main(String[] args) throws IOException {

        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(20);
        config.setMaxIdle(10);
        config.setMinIdle(5);

        Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
        jedisClusterNode.add(new HostAndPort("192.168.1.2", 8001));
        jedisClusterNode.add(new HostAndPort("192.168.1.4", 8002));
        jedisClusterNode.add(new HostAndPort("192.168.1.5", 8003));
        jedisClusterNode.add(new HostAndPort("192.168.1.2", 8004));
        jedisClusterNode.add(new HostAndPort("192.168.1.4", 8005));
        jedisClusterNode.add(new HostAndPort("192.168.1.5", 8006));

        JedisCluster jedisCluster = null;
        String password = "liguang";
        try {
            //connectionTimeout:指的是连接一个url的连接等待时间
            //soTimeout:指的是连接上一个url,获取response的返回等待时间
            jedisCluster = new JedisCluster(jedisClusterNode, 6000, 5000, 10, password, config);
            System.out.println(jedisCluster.set("cluster", "liguang"));
            System.out.println(jedisCluster.get("cluster"));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedisCluster != null)
                jedisCluster.close();
        }
    }
}

运行效果如下所示:

OK
liguang

Springboot版本

1、引入相关依赖:

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

<dependency>
    <groupId>org.apache.commonsgroupId>
    <artifactId>commons-pool2artifactId>
dependency>

2、配置文件如下所示

server:
  port: 8080

spring:
  redis:
    database: 0
    timeout: 3000
    password: liguang
    cluster:
      nodes: 192.168.1.2:8001,192.168.1.4:8002,192.168.1.5:8003,192.168.1.2:8004,192.168.1.4:8005,192.168.1.5:8006
    lettuce:
      pool:
        max-idle: 50
        min-idle: 10
        max-active: 100
        max-wait: 1000

3、测试代码

@RestController
public class IndexController {

    private static final Logger logger = LoggerFactory.getLogger(IndexController.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/test_cluster")
    public void testCluster() throws InterruptedException {
       stringRedisTemplate.opsForValue().set("zhuge", "666");
       System.out.println(stringRedisTemplate.opsForValue().get("zhuge"));
    }
}

5.5、原理分析

Redis Cluster 将所有数据划分为 16384 个 slots(槽位),每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。

当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地。这样当客户端要查找某个 key 时,可以直接定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息的校验调整。

槽位定位算法

Cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。
HASH_SLOT = CRC16(key) mod 16384

跳转重定位

当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据。客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有 key 将使用新的槽位映射表。

在这里插入图片描述

Redis集群节点间的通信机制

redis cluster节点间采取gossip协议进行通信

维护集群的元数据(集群节点信息,主从角色,节点数量,各节点共享的数据等)有两种方式:集中式和gossip

集中式

优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以立即感知到;不足在于所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。 很多中间件都会借助zookeeper集中式存储元数据

gossip

gossip协议包含多种消息,包括ping,pong,meet,fail等等。

meet:某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信;

ping:每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据(类似自己感知到的集群节点增加和移除,hash slot信息等);

pong: 对ping和meet消息的返回,包含自己的状态和其他信息,也可以用于信息广播和更新;

fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了。

gossip协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;缺点在于元数据更新有延时可能导致集群的一些操作会有一些滞后。

gossip通信的10000端口

每个节点都有一个专门用于节点间gossip通信的端口,就是自己提供服务的端口号+10000,比如7001,那么用于节点间通信的就是17001端口。 每个节点每隔一段时间都会往另外几个节点发送ping消息,同时其他几点接收到ping消息之后返回pong消息。

网络抖动

真实世界的机房网络往往并不是风平浪静的,它们经常会发生各种各样的小问题。比如网络抖动就是非常常见的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正常。

为解决这种问题,Redis Cluster 提供了一种选项cluster-node-timeout,表示当某个节点持续 timeout 的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换 (数据的重新复制)。

Redis集群选举原理分析

当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程, 其过程如下:

1.slave发现自己的master变为FAIL

2.将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST 信息

3.其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack

4.尝试failover的slave收集master返回的FAILOVER_AUTH_ACK

5.slave收到超过半数master的ack后变成新Master(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的)

6.slave广播Pong消息通知其他集群节点。

注意:如果收到的ack都是一致的,那么将会进行重新选举。

从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待FAIL状态在集群中传播,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票

延迟计算公式

 DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms
  • SLAVE_RANK表示此slave已经从master复制数据的总量的rank。Rank越小代表已复制的数据越新。这种方式下,持有最新数据的slave将会首先发起选举(理论上)。

集群脑裂数据丢失问题

redis集群没有过半机制会有脑裂问题,网络分区导致脑裂后多个主节点对外提供写服务,一旦网络分区恢复,会将其中一个主节点变为从节点,这时会有大量数据丢失。

规避方法可以在redis配置里加上参数(这种方法不可能百分百避免数据丢失,参考集群leader选举机制):

//写数据成功最少同步的slave数量,这个数量可以模仿大于半数机制配置,比如集群总共三个节点可以配置1,加上leader就是2,超过了半数
min-replicas-to-write 1  

注意:这个配置在一定程度上会影响集群的可用性,比如slave要是少于1个,这个集群就算leader正常也不能提供服务了,需要具体场景权衡选择。

集群是否完整才能对外提供服务

当redis.conf的配置cluster-require-full-coverage为no时,表示当负责一个插槽的主库下线且没有相应的从库进行故障恢复时,集群仍然可用,如果为yes则集群不可用。

Redis集群为什么至少需要三个master节点,并且推荐节点数为奇数?

因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。

奇数个master节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源角度出发说的。

Redis集群对批量操作命令的支持

对于类似mset,mget这样的多个key的原生批量操作命令,redis集群只支持所有key落在同一slot的情况,如果有多个key一定要用mset命令在redis集群上操作,则可以在key的前面加上{XX},这样参数数据分片hash计算的只会是大括号里的值,这样能确保不同的key能落到同一slot里去,示例如下:

mset {user1}:1:name zhuge {user1}:1:age 18

假设name和age计算的hash slot值不一样,但是这条命令在集群下执行,redis只会用大括号里的 user1 做hash slot计算,所以算出来的slot值肯定相同,最后都能落在同一slot。

哨兵leader选举流程

当一个master服务器被某sentinel视为下线状态后,该sentinel会与其他sentinel协商选出sentinel的leader进行故障转移工作。每个发现master服务器进入下线的sentinel都可以要求其他sentinel选自己为sentinel的leader,选举是先到先得。同时每个sentinel每次选举都会自增配置纪元(选举周期),每个纪元中只会选择一个sentinel的leader。如果所有超过一半的sentinel选举某sentinel作为leader。之后该sentinel进行故障转移操作,从存活的slave中选举出新的master,这个选举过程跟集群的master选举很类似。

哨兵集群只有一个哨兵节点,redis的主从也能正常运行以及选举master,如果master挂了,那唯一的那个哨兵节点就是哨兵leader了,可以正常选举新master。

不过为了高可用一般都推荐至少部署三个哨兵节点。为什么推荐奇数个哨兵节点原理跟集群奇数个master节点类似。

你可能感兴趣的:(redis,java,数据库)