Redis 主从复制及哨兵模式

1. Redis 主从复制

1.1 什么是Redis 主从复制

为了分担读压力,Redis支持主从复制,Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
大多数情况下,应用服务的读操作多于写操作。主从复制将业务的读和写分离开来,以主节点作为写服务器,配置多个从节点作为读服务器,主节点写入数据,并向从节点同步数据。当主节点宕机时,可以将主节点切换为从节点提供服务,从而实现故障的快速恢复。

1.2 全量同步和增量同步

1.2.1 全量同步

Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。
具体步骤如下:

  1. 从服务器连接主服务器,发送SYNC命令;
  2. 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
  3. 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
  4. 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
  5. 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
  6. 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
1.2.2 增量同步

Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

1.2.3 主从同步策略

主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

1.3 主从复制的特点

  • 采用异步复制;

  • 一个主redis可以含有多个从redis;

  • 每个从redis可以接收来自其他从redis服务器的连接;

  • 主从复制对于主redis服务器来说是非阻塞的,这意味着当从服务器在进行主从复制同步过程中,主redis仍然可以处理外界的访问请求;

  • 主从复制对于从redis服务器来说也是非阻塞的,这意味着,即使从redis在进行主从复制过程中也可以接受外界的查询请求,只不过这时候从redis返回的是以前老的数据,如果你不想这样,那么在启动redis时,可以在配置文件中进行设置,那么从redis在复制同步过程中来自外界的查询请求都会返回错误给客户端;(虽然说主从复制过程中对于从redis是非阻塞的,但是当从redis从主redis同步过来最新的数据后还需要将新数据加载到内存中,在加载到内存的过程中是阻塞的,在这段时间内的请求将会被阻,但是即使对于大数据集,加载到内存的时间也是比较多的);

  • 主从复制提高了redis服务的扩展性,避免单个redis服务器的读写访问压力过大的问题,同时也可以给为数据备份及冗余提供一种解决方案;

  • 为了解决主redis服务器写磁盘压力带来的开销,可以配置让主redis不在将数据持久化到磁盘,而是通过连接让一个配置的从redis服务器及时的将相关数据持久化到磁盘,不过这样会存在一个问题,就是主redis服务器一旦重启,因为主redis服务器数据为空,这时候通过主从同步可能导致从服务器上的数据也被清空;

1.4 主从同步实现介绍

1.4.1 全量同步

master服务器会开启一个后台进程用于将redis中的数据生成一个rdb文件,与此同时,服务器会缓存所有接收到的来自客户端的写命令(包含增、删、改),当后台保存进程处理完毕后,会将该rdb文件传递给slave服务器,而slave服务器会将rdb文件保存在磁盘并通过读取该文件将数据加载到内存,在此之后master服务器会将在此期间缓存的命令通过redis传输协议发送给slave服务器,然后slave服务器将这些命令依次作用于自己本地的数据集上最终达到数据的一致性。

1.4.2 增量同步

从redis 2.8版本以前,并不支持部分同步,当主从服务器之间的连接断掉之后,master服务器和slave服务器之间都是进行全量数据同步,但是从redis 2.8开始,即使主从连接中途断掉,也不需要进行全量同步,因为从这个版本开始融入了部分同步的概念。部分同步的实现依赖于在master服务器内存中给每个slave服务器维护了一份同步日志和同步标识,每个slave服务器在跟master服务器进行同步时都会携带自己的同步标识和上次同步的最后位置。当主从连接断掉之后,slave服务器隔断时间(默认1s)主动尝试和master服务器进行连接,如果从服务器携带的偏移量标识还在master服务器上的同步备份日志中,那么就从slave发送的偏移量开始继续上次的同步操作,如果slave发送的偏移量已经不再master的同步备份日志中(可能由于主从之间断掉的时间比较长或者在断掉的短暂时间内master服务器接收到大量的写操作),则必须进行一次全量更新。在部分同步过程中,master会将本地记录的同步备份日志中记录的指令依次发送给slave服务器从而达到数据一致。

1.4 配置简单的主从复制

使用单机多实例模拟,启动三个实例,一主两从。7001,7002,7003

1.4.1 准备工作

# redis安装目录
cd /usr/local/redis/redis-6.0.8

# 创建模拟目录
mkdir -pv replica/{7001,7002,7003}

复制redis.cnf到指定目录,并修改指定配置
这里是常用配置,主从配置在后面说明。

vi /usr/local/redis/redis-6.0.8/replica/7001/redis.conf

# 将绑定IP注释
bind 127.0.0.1

# 关闭保护模式
protected-mode no

# 修改服务端口号
port 7001

# 守护进程
daemonize yes

# 修改pid保存位置
pidfile /usr/local/redis/redis-6.0.8/replica/7001/redis.pid

# 设置快照文件的存放路径,是一个目录
dir /usr/local/redis/redis-6.0.8/replica/7001

# 修改内存策略
maxmemory-policy allkeys-lru

# 定义日志级别。
# 默认值为notice,有如下4种取值:
# debug(记录大量日志信息,适用于开发、测试阶段)
# verbose(较多日志信息)
# notice(适量日志信息,使用于生产环境)
# warning(仅有部分重要、关键信息才会被记录)
loglevel notice

# 日志文件
logfile /usr/local/redis/redis-6.0.8/replica/redis.log

启动三个实例

/usr/local/redis/redis-6.0.8/bin/redis-server /usr/local/redis/redis-6.0.8/replica/7001/redis.conf
/usr/local/redis/redis-6.0.8/bin/redis-server /usr/local/redis/redis-6.0.8/replica/7002/redis.conf
/usr/local/redis/redis-6.0.8/bin/redis-server /usr/local/redis/redis-6.0.8/replica/7003/redis.conf

1.4.2 查看服务角色

redis默认都是主节点,登录并查看节点角色

/usr/local/redis/redis-6.0.8/bin/redis-cli -p 7003

# 可通过 ROLE 命令来查看服务器当前担任的角色,在主节点和从节点上返回不同信息
127.0.0.1:7003> role
1) "master"
2) (integer) 0
3) (empty array)

# 也可通过info replication 查看到相关信息
127.0.0.1:7003> info replication
role:master
connected_slaves:0
master_replid:4dbea935b9175740a46cbc92399032b422e42907
master_replid2:c5c6a8359e15f9702da8f9801af276563acf7867
master_repl_offset:0
second_repl_offset:1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

或者使用以下命令,更加方便。但是在命令界面暴漏密码可能并不安全。

redis-cli -p 7001 -a 123456 info | grep role

1.4.3 配置主从

设置主从,可以通过修改配置文件,在从节点配置文件内设置主节点的信息来设置。
也可以在节点启动后,通过相关命令来设置。

(1) REPLICAOF:将服务器设置为从服务器
在很长的一段时间里,Redis 一直使用 SLAVEOF 作为复制命令,但是从 5.0.0 版本开始,Redis 正式将 SLAVEOF 命令改名成了 REPLICAOF 命令并逐渐废弃原来的 SLAVEOF 命令。

连接到7002,让7002成为7001的从节点.

127.0.0.1:7002> replicaof 127.0.0.1 7001
OK

# 查看设置后的服务器状态
127.0.0.1:7002> role
1) "slave"
2) "127.0.0.1"
3) (integer) 7001
4) "connect"
5) (integer) -1

7003执行同样命令即可。

配置完成后,查看主节点信息

127.0.0.1:7001> role
1) "master"
2) (integer) 140
3) 1) 1) "127.0.0.1"
      2) "7002"
      3) "140"
   2) 1) "127.0.0.1"
      2) "7003"
      3) "140" # 复制偏移量

取消复制

127.0.0.1:7002> REPLICAOF no one
OK

(2) 通过修改子节点配置文件来配置,在配置内增加下面配置

replicaof <masterip> <masterport>

# 主节点有设置密码的话,设置认证密码
masterauth <master-password>

(3) 在启动服务的时候来设置成为子节点

redis-cli --port 7002 --replicaof 127.0.0.1 7001

2. 哨兵模式

2.1 什么是Sentinel模式

反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库.

Redis 哨兵主要功能

  • (1)集群监控:负责监控 Redis master 和 slave 进程是否正常工作
  • (2)消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员
  • (3)故障转移:如果 master node 挂掉了,会自动转移到 slave node 上
  • (4)配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址

Redis 哨兵的高可用
原理:当主节点出现故障时,由 Redis Sentinel 自动完成故障发现和转移,并通知应用方,实现高可用性。

哨兵机制建立了多个哨兵节点 (进程),共同监控数据节点的运行状况。
同时哨兵节点之间也互相通信,交换对主从节点的监控状况。
每隔 1 秒每个哨兵会向整个集群:Master 主服务器 + Slave 从服务器 + 其他 Sentinel(哨兵)进程,发送一次 ping 命令做一次心跳检测。
这个就是哨兵用来判断节点是否正常的重要依据,涉及两个新的概念:主观下线和客观下线。

  • 主观下线:一个哨兵节点判定主节点 down 掉是主观下线。
  • 客观下线:只有半数哨兵节点都主观判定主节点 down 掉,此时多个哨兵节点交换主观判定结果,才会判定主节点客观下线。
  • 原理:基本上哪个哨兵节点最先判断出这个主节点客观下线,就会在各个哨兵节点中发起投票机制 Raft 算法(选举算法),最终被投为领导者的哨兵节点完成主从自动化切换的过程。

对sentinel模式的理解:

  • sentinel模式是建立在主从模式的基础上,如果只有一个Redis节点,sentinel就没有任何意义
  • 当master节点挂了以后,sentinel会在slave中选择一个做为master,并修改它们的配置文件,其他slave的配置文件也会被修改,比如replicaof属性会指向新的master
  • 当master节点重新启动后,它将不再是master而是做为slave接收新的master节点的同步数据
  • sentinel因为也是一个进程有挂掉的可能,所以sentinel也会启动多个形成一个sentinel集群
  • 当主从模式配置密码时,sentinel也会同步将配置信息修改到配置文件中,不许要担心。
  • 一个sentinel或sentinel集群可以管理多个主从Redis。
  • sentinel最好不要和Redis部署在同一台机器,不然Redis的服务器挂了以后,sentinel也挂了
  • sentinel监控的Redis集群都会定义一个master名字,这个名字代表Redis集群的master Redis。

当使用sentinel模式的时候,客户端就不要直接连接Redis,而是连接sentinel的ip和port,由sentinel来提供具体的可提供服务的Redis实现,这样当master节点挂掉以后,sentinel就会感知并将新的master节点提供给使用者。
sentinel模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中,就是cluster模式。

2.2 配置哨兵

在原来的基础上,配置三个Sentinel,实现一个简单的一主二从三哨兵的配置。

2.2.1 准备工作

# redis安装目录
cd /usr/local/redis/redis-6.0.8

# 创建sentinel目录及其哨兵工作目录
mkdir -pv sentinel/{27001,27002,27003}

# 在其对应目录下,创建sentinel配置文件监听master 7001
vi sentinel/27001/sentinel.conf

2.2.2 sentinel配置文件

# 端口
port 27001

# 守护进程
daemonize yes

# 守护进程文件
pidfile /usr/local/redis/redis-6.0.8/sentinel/27001/sentinel.pid

# 日志文件
logfile /usr/local/redis/redis-6.0.8/sentinel/27001/sentinel.log

# 工作目录
dir /usr/local/redis/redis-6.0.8/sentinel/27001

#我们可以通过配置sentinel announce-ip和sentinel announce-port,让sentinel运行在  指定的ip(gossip协议握手地址)和port上
# sentinel announce-ip 1.2.3.4


# sentinel monitor <master-name> <ip> <redis-port> <quorum>
# 告诉sentinel去监听地址为ip:port的一个master,这里的master-name可以自定义,quorum是一个数字,指明当有多少个sentinel认为一个master失效时,master才算真正失效
# 根据情况,配置成公网ip
sentinel monitor mymaster 127.0.0.1 7001 2

# sentinel auth-pass <master-name> <password>
# 设置连接master和slave时的密码,注意的是sentinel不能分别为master和slave设置不同的密码,因此master和slave的密码应该设置相同。
sentinel auth-pass mymaster 123456


# sentinel down-after-milliseconds <master-name> <milliseconds>
# 这个配置项指定了需要多少失效时间,一个master才会被这个sentinel主观地认为是不可用的。 单位是毫秒,默认为30秒
sentinel down-after-milliseconds mymaster 30000

# Sentinels本身设置密码,配置所有的Sentinels具有相同的密码
# for more info: https://redis.io/topics/sentinel
# requirepass <password>


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

# sentinel failover-timeout <master-name> <milliseconds>
# failover-timeout 可以用在以下这些方面:     
# 	1. 同一个sentinel对同一个master两次failover之间的间隔时间。   
#	2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。    
#	3.当想要取消一个正在进行的failover所需要的时间。    
#	4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了。
# Default is 3 minutes.
sentinel failover-timeout mymaster 180000


# sentinel 无法更新通知脚本 默认yes
sentinel deny-scripts-reconfig yes

# 重命名命令
# SENTINEL rename-command mymaster CONFIG CONFIG

同理,复制27001的配置文件到27002,27003,修改其端口和工作目录。

2.2.3 启动sentinel

在redis主从配置好后,分别启动sentinel。

/usr/local/redis/redis-6.0.8/bin/redis-sentinel /usr/local/redis/redis-6.0.8/sentinel/27003/sentinel.conf

启动后,发现配置文件被重写了,从内容可以看出有哪些slave和sentinel

2.2.4 故障转移

首先停止主节点redis_27001的服务,查看sentinel的日志信息。

14956:X 13 Oct 2020 19:58:45.080 # +sdown master mymaster 127.0.0.1 7001
14956:X 13 Oct 2020 20:02:29.160 * +reboot master mymaster 127.0.0.1 7001
14956:X 13 Oct 2020 20:02:29.259 # -sdown master mymaster 127.0.0.1 7001
14956:X 13 Oct 2020 20:04:09.513 * +sentinel sentinel 6a296a02a8d968be2e12d08d8164673d017c7a18 127.0.0.1 27002 @ mymaster 127.0.0.1 7001
14956:X 13 Oct 2020 20:04:13.205 * +sentinel sentinel f32415333f451c0e85240f8dfc5d33b49ba49d94 127.0.0.1 27003 @ mymaster 127.0.0.1 7001
14956:X 13 Oct 2020 20:05:54.612 # +sdown master mymaster 127.0.0.1 7001
14956:X 13 Oct 2020 20:05:54.689 # +new-epoch 1
14956:X 13 Oct 2020 20:05:54.696 # +vote-for-leader f32415333f451c0e85240f8dfc5d33b49ba49d94 1
14956:X 13 Oct 2020 20:05:55.693 # +odown master mymaster 127.0.0.1 7001 #quorum 3/2
14956:X 13 Oct 2020 20:05:55.693 # Next failover delay: I will not start a failover before Tue Oct 13 20:11:55 2020
14956:X 13 Oct 2020 20:05:55.747 # +config-update-from sentinel f32415333f451c0e85240f8dfc5d33b49ba49d94 127.0.0.1 27003 @ mymaster 127.0.0.1 7001
14956:X 13 Oct 2020 20:05:55.747 # +switch-master mymaster 127.0.0.1 7001 127.0.0.1 7002
14956:X 13 Oct 2020 20:05:55.747 * +slave slave 127.0.0.1:7003 127.0.0.1 7003 @ mymaster 127.0.0.1 7002
14956:X 13 Oct 2020 20:05:55.747 * +slave slave 127.0.0.1:7001 127.0.0.1 7001 @ mymaster 127.0.0.1 7002
14956:X 13 Oct 2020 20:06:25.765 # +sdown slave 127.0.0.1:7001 127.0.0.1 7001 @ mymaster 127.0.0.1 7002

可以看到master由7001更换到7002,查看7002状态

127.0.0.1:7002> role
1) "master"
2) (integer) 31196
3) 1) 1) "127.0.0.1"
      2) "7003"
      3) "31063"

重新启动7001,会发现7001变成了slave.
同时sentinel配置文件内监听的7001也会变成新的master.

3. Redis Api

3.1 Jedis操作

<!--jedis-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.0.1</version>
</dependency>

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

/**
 * 哨兵模式
 * 主节点故障后,主动选举一个新的主节点
 */
@Test
public void testReplica() {

    //主动发现主节点
    //名称 与sentinel配置中的<master-name>一致
    String clusterName = "mymaster";
    String password = "123456";

    //sentinel
    Set<String> sentinels = new HashSet<String>();
    sentinels.add("xxx.xxx.xx.xx:27001");
    sentinels.add("xxx.xxx.xx.xx:27002");
    sentinels.add("xxx.xxx.xx.xx:27003");

    //连接池配置
    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    jedisPoolConfig.setMaxIdle(10);
    jedisPoolConfig.setMaxTotal(10);
    jedisPoolConfig.setMaxWaitMillis(10);
    jedisPoolConfig.setJmxEnabled(true);

    //获取的是主节点
    JedisSentinelPool jsentinelPool = new JedisSentinelPool(clusterName, sentinels, jedisPoolConfig, 3000, password);
    Jedis jedis = null;
    String msg, result;

    //每秒获取一个连接,手动关闭主节点,测试故障转移
    for (int i = 0; i < 1000; i++) {
        try {
            //获取连接
            jedis = jsentinelPool.getResource();

            //设置值
            result = jedis.set("msg", String.valueOf(i));

            //获取值
            msg = jedis.get("msg");

            //当前主节点
            String master = jsentinelPool.getCurrentHostMaster().toString();
            System.out.printf("%s Current master:%s set:%s get:%s%n", i, master, result, msg);
        } catch (Exception ex) {
            System.out.println("resource error:" + ex.getMessage());
        } finally {
            try {
                if (null != jedis) {
                    jedis.close();
                }
            } catch (Exception ex) {
                System.out.println("resource close error:" + ex.getMessage());
            }
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //jsentinelPool.close();
}
}

输出结果

0 Current master:xxx.xxx.xx.xx:7002 set:OK get:0
1 Current master:xxx.xxx.xx.xx:7002 set:OK get:1
2 Current master:xxx.xxx.xx.xx:7002 set:OK get:2
resource error:Unexpected end of stream.
resource error:Could not get a resource from the pool
resource error:Could not get a resource from the pool
resource error:Could not get a resource from the pool
resource error:Could not get a resource from the pool
resource error:Could not get a resource from the pool
resource error:Could not get a resource from the pool
resource error:Could not get a resource from the pool
[DEBUG] 2020-10-14 02:43:44.088 [MasterListener-mymaster-[xxx.xxx.xx.xx:27002]] redis.clients.jedis.JedisSentinelPool 298 - Sentinel xxx.xxx.xx.xx:27002 published: mymaster xxx.xxx.xx.xx 7002 xxx.xxx.xx.xx 7003.
[INFO] 2020-10-14 02:43:44.089 [MasterListener-mymaster-[xxx.xxx.xx.xx:27002]] redis.clients.jedis.JedisSentinelPool 130 - Created JedisPool to master at xxx.xxx.xx.xx:7003
[DEBUG] 2020-10-14 02:43:44.095 [MasterListener-mymaster-[xxx.xxx.xx.xx:27003]] redis.clients.jedis.JedisSentinelPool 298 - Sentinel xxx.xxx.xx.xx:27003 published: mymaster xxx.xxx.xx.xx 7002 xxx.xxx.xx.xx 7003.
[DEBUG] 2020-10-14 02:43:44.098 [MasterListener-mymaster-[xxx.xxx.xx.xx:27001]] redis.clients.jedis.JedisSentinelPool 298 - Sentinel xxx.xxx.xx.xx:27001 published: mymaster xxx.xxx.xx.xx 7002 xxx.xxx.xx.xx 7003.
11 Current master:xxx.xxx.xx.xx:7003 set:OK get:11
12 Current master:xxx.xxx.xx.xx:7003 set:OK get:12
13 Current master:xxx.xxx.xx.xx:7003 set:OK get:13

3.2 spring配置哨兵模式

<!--哨兵模式配置方式-->
<bean id="sentinelPool" class="redis.clients.jedis.JedisSentinelPool">
    <constructor-arg name="poolConfig" ref="jedisPoolConfig"/>
    <constructor-arg name="masterName" value="mymaster"/>
    <constructor-arg name="sentinels">
        <set>
            <value>xxx.xxx.xx.xx:27001</value>
            <value>xxx.xxx.xx.xx:27002</value>
            <value>xxx.xxx.xx.xx:27003</value>
        </set>
    </constructor-arg>
    <constructor-arg name="password" value="123456"/>
    <constructor-arg name="timeout" value="3000"/>
</bean>

3.3 spring-data配置哨兵模式

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd         http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!--ignore-unresolvable="true"
        property-placeholder 在加载第一个配置时,会扫描所有的bean,如果有bean里面使用了其他配置文件的属性,这时候就会找不到属性报错
        配置该属性可以忽略错误,将其配置为${}
    -->
    <context:property-placeholder location="classpath:redis.properties" ignore-unresolvable="true"/>

    <!--连接池配置-->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxTotal" value="${redis.pool.max}"/>
        <property name="maxIdle" value="${redis.pool.maxIdle}"/>
        <property name="minIdle" value="${redis.pool.minIdle}"/>
        <property name="maxWaitMillis" value="${redis.pool.maxWait}"/>
        <property name="testOnBorrow" value="${redis.pool.testOnBorrow}"/>
    </bean>

    <!--spring-data-redis 哨兵模式 信息-->
    <bean id="redisSentinelConfiguration" class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
        <property name="master">
            <bean class="org.springframework.data.redis.connection.RedisNode">
                <property name="name" value="mymaster">
                </property>
            </bean>
        </property>
        <property name="sentinels">
            <set>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="xxx.xxx.xx.xx"/>
                    <constructor-arg name="port" value="27001"/>
                </bean>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="xxx.xxx.xx.xx"/>
                    <constructor-arg name="port" value="27002"/>
                </bean>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="xxx.xxx.xx.xx"/>
                    <constructor-arg name="port" value="27003"/>
                </bean>
            </set>
        </property>
    </bean>

    <!-- 自定义redis中key序列化方式 -->
    <bean id="keySerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
    <!-- 自定义redis中value序列化方式 -->
    <bean id="valueSerializer"
          class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>
    
    <!--redid 连接工厂-->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="password" value="123456"/>
        <property name="timeout" value="3000"/>
        <constructor-arg name="poolConfig" ref="jedisPoolConfig"/>
        <constructor-arg name="sentinelConfig" ref="redisSentinelConfiguration"/>
    </bean>

    <!--spring-data-redis template-->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
        <property name="keySerializer" ref="keySerializer"/>
        <property name="valueSerializer" ref="valueSerializer"/>
        <property name="hashKeySerializer" ref="keySerializer"/>
        <property name="hashValueSerializer" ref="valueSerializer"/>
    </bean>
</beans>

使用方式

import org.junit.Test;
import org.junit.runner.RunWith;
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 org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.*;
import java.util.concurrent.TimeUnit;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:application-redis.xml")
public class SpringDataRedis {

    //redis 存储数据出现乱码 需配置序列化方式(需要jackson-databind)

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 获取Redis-key序列化方式
     * @return
     */
    public RedisSerializer<String> getKeySerializer() {
        return (RedisSerializer<String>) redisTemplate.getKeySerializer();
    }

    /**
     * 获取Redis-value序列化方式
     * @return
     */
    public RedisSerializer<String> getValueSerializer() {
        return (RedisSerializer<String>) redisTemplate.getValueSerializer();
    }

    /**
     * 获取测试数据
     * @return Map<String, String>
     */
    public Map<String, String> getDataMap() {
        Map<String, String> map = new HashMap<String, String>();

        for (int i = 0; i < 10; i++) {
            map.put("name" + i, "value" + i);
        }
        return map;
    }

    /**
     * RedisTemplate 常用操作测试
     */
    @Test
    public void test1() {

        final ValueOperations<String, String> valueOps = redisTemplate.opsForValue();

        //先清空数据库
        Set<String> keys = redisTemplate.keys("*");
        redisTemplate.delete(keys);

        //先存一个值,然后获取
        //不通过java程序存,取的时候序列化失败,暂不知道解决方案
        valueOps.set("valueTest", "this is string");
        String valueTest = valueOps.get("valueTest");
        System.out.println(valueTest);

        //判断一个key是否存在
        redisTemplate.hasKey("valueTest");

        //设置key的过期时间
        redisTemplate.expire("valueTest", 60, TimeUnit.SECONDS);

        //获取key的过期时间
        redisTemplate.getExpire("valueTest");

        //删除一个key
        redisTemplate.delete("valueTest");

        //获取不存在的值,java这里为null
        String nullString = valueOps.get("valueTest");
        System.out.println("获取不存在的值:" + nullString);

        //通过map一次更新多个键值
        Map<String, String> map = new HashMap<String, String>();
        map.put("name", "ningchuan");
        map.put("password", "123456");
        valueOps.multiSet(map);

        //获取多个键值
        List<String> lsKey = new ArrayList<String>();
        lsKey.add("name");
        lsKey.add("password");

        //遍历获取到的键值
        List<String> lsValue = valueOps.multiGet(lsKey);
        for (String str : lsValue) {
            System.out.println(str);
        }

        //通过spring-data 批量设置数据 同一个连接
        redisTemplate.executePipelined(new RedisCallback<Object>() {
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                getDataMap().forEach((k, v) -> {
                    redisConnection.set(getKeySerializer().serialize(k), getValueSerializer().serialize(v));
                });
                return null;
            }
        });

        //通过spring-data 批量获取数据 同一个连接
        List<Object> lsObject = redisTemplate.executePipelined(new RedisCallback<List<String>>() {
            public List<String> doInRedis(RedisConnection redisConnection) throws DataAccessException {

                getDataMap().forEach((k, v) -> {
                    redisConnection.get(getKeySerializer().serialize(k));
                });

                //这里必须返回null,自定义的返回值会被覆盖掉,会出现运行时错误
                return null;
            }
        });

        System.out.println("----------获取PipeLine批量设置的键值----------");
        lsObject.forEach(k -> {
            System.out.println(k);
        });
    }

    /**
     * Redis集群
     * jedisConnectionFactory 更改为cluster
     */
    @Test
    public void test2() {

        redisTemplate.opsForValue().set("msg", "redis-data-cluster");
        String address = redisTemplate.opsForValue().get("msg");

        System.out.println(address);
    }

    /**
     * 哨兵模式
     * jedisConnectionFactory 更改为sentinel
     */
    @Test
    public void testSentinel() {
        redisTemplate.opsForValue().set("msg", "spring-data");
        String msg = redisTemplate.opsForValue().get("msg");
        System.out.println(msg);
    }
}

3.4 spring-boot配置哨兵模式

application.yml

# 哨兵模式配置
spring:
  redis:
    password: 123456
    sentinel:
      master: mymaster
      nodes: xxx.xxx.xx.xx:27001,xxx.xxx.xx.xx:27002,xxx.xxx.xx.xx:27003
    timeout: 3000

4. 总结

Redis 主从复制、哨兵和集群这三个有什么区别?

主从复制是为了数据备份,哨兵是为了高可用,Redis 主服务器挂了哨兵可以切换,集群则是因为单实例能力有限,搞多个分散压力,简短总结如下:

主从模式:备份数据、负载均衡,一个 Master 可以有多个 Slaves。

sentinel 发现 master 挂了后,就会从 slave 中重新选举一个 master。

cluster 是为了解决单机 Redis 容量有限的问题,将数据按一定的规则分配到多台机器。

sentinel 着眼于高可用,Cluster 提高并发量。

  1. 主从模式:读写分离,备份,一个 Master 可以有多个 Slaves。
  2. 哨兵 sentinel:监控,自动转移,哨兵发现主服务器挂了后,就会从 slave 中重新选举一个主服务器。
  3. 集群:为了解决单机 Redis 容量有限的问题,将数据按一定的规则分配到多台机器,内存 / QPS 不受限于单机,可受益于分布式集群高扩展性。
posted @ 2020-10-14 04:54  宁川  阅读(233)  评论(0)    收藏  举报