浅析Redis Sentinel 与 Redis Cluster


Posted in Redis onJune 24, 2021

一、前言

互联网高速发展的今天,对应用系统的抗压能力要求越来越高,传统的应用层+数据库已经不能满足当前的需要。所以一大批内存式数据库和Nosql数据库应运而生,其中redis,memcache,mongodb,hbase等被广泛的使用来提高系统的吞吐性,所以如何正确使用cache是作为开发的一项基技能。本文主要介绍Redis Sentinel 及 Redis Cluster的区别及用法,Redis的基本操作可以自行去参看其官方文档 。 其他几种cache有兴趣的可自行找资料去学习。

二、Redis Sentinel 及 Redis Cluster 简介

1、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的新地址。

       Redis master-slave 模式如下图:

 浅析Redis Sentinel 与 Redis Cluster

从上图片中可以看到,一个master 节点可以挂多个slave  ,Redis Sentinel 管理Redis 节点结构如下:

浅析Redis Sentinel 与 Redis Cluster    

上图中可以得出Sentinel其实就是Client和Redis之间的桥梁,所有的客户端都通过Sentinel程序获取Redis的Master服务。首先Sentinel是集群部署的,Client可以链接任何一个Sentinel服务所获的结果都是一致的。其次,所有的Sentinel服务都会对Redis的主从服务进行监控,当监控到Master服务无响应的时候,Sentinel内部进行仲裁,从所有的 Slave选举出一个做为新的Master。并且把其他的slave作为新的Master的Slave。最后通知所有的客户端新的Master服务地址。如果旧的Master服务地址重新启动,这个时候,它将被设置为Slave服务。

Sentinel 可以管理master-slave节点,看似Redis的稳定性得到一个比较好的保障。但是如果Sentinel是单节点的话,如果Sentinel宕机了,那master-slave这种模式就不能发挥其作用了。幸好Sentinel也支持集群模式,Sentinel的集群模式主要有以下几个好处:

  • 即使有一些sentinel进程宕掉了,依然可以进行redis集群的主备切换;
  • 如果只有一个sentinel进程,如果这个进程运行出错,或者是网络堵塞,那么将无法实现redis集群的主备切换(单点问题);
  • 如果有多个sentinel,redis的客户端可以随意地连接任意一个sentinel来获得关于redis集群中的信息。

     Redis Sentinel 集群模式可以增强整个Redis集群的稳定性与可靠性,但是当某个节点的master节点挂了要重新选取出新的master节点时,Redis Sentinel的集群模式选取的复杂度显然高于单点的Redis Sentinel 模式,此时需要一个比较靠谱的选取算法。下面就来介绍Redis Sentinel 集群模式的 “仲裁会”(多个Redis Sentinel共同商量谁是Redis 的 master节点)

1.1、Redis 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的授权。

2、Redis Cluster

使用Redis Sentinel 模式架构的缓存体系,在使用的过程中,随着业务的增加不可避免的要对Redis进行扩容,熟知的扩容方式有两种,一种是垂直扩容,一种是水平扩容。垂直扩容表示通过加内存方式来增加整个缓存体系的容量比如将缓存大小由2G调整到4G,这种扩容不需要应用程序支持;水平扩容表示表示通过增加节点的方式来增加整个缓存体系的容量比如本来有1个节点变成2个节点,这种扩容方式需要应用程序支持。垂直扩容看似最便捷的扩容,但是受到机器的限制,一个机器的内存是有限的,所以垂直扩容到一定阶段不可避免的要进行水平扩容,如果预留出很多节点感觉又是对资源的一种浪费因为对业务的发展趋势很快预测。Redis Sentinel 水平扩容一直都是程序猿心中的痛点,因为水平扩容牵涉到数据的迁移。迁移过程一方面要保证自己的业务是可用的,一方面要保证尽量不丢失数据所以数据能不迁移就尽量不迁移。针对这个问题,Redis Cluster就应运而生了,下面简单介绍一下RedisCluster。

Redis Cluster是Redis的分布式解决方案,在Redis 3.0版本正式推出的,有效解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构达到负载均衡的目的。分布式集群首要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整个数据的一个子集。Redis Cluster采用哈希分区规则中的虚拟槽分区。虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有的数据映射到一个固定范围内的整数集合,整数定义为槽(slot)。Redis Cluster槽的范围是0 ~ 16383。槽是集群内数据管理和迁移的基本单位。采用大范围的槽的主要目的是为了方便数据的拆分和集群的扩展,每个节点负责一定数量的槽。Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0 ~ 16383,计算公式:slot = CRC16(key)&16383。每一个实节点负责维护一部分槽以及槽所映射的键值数据。下图展现一个五个节点构成的集群,每个节点平均大约负责3276个槽,以及通过计算公式映射到对应节点的对应槽的过程。

浅析Redis Sentinel 与 Redis Cluster

Redis Cluster节点相互之前的关系如下图所示:

浅析Redis Sentinel 与 Redis Cluster

三、Redis Sentinel 及Redis Cluster 实践

Redis Sentinel 与Redis Cluster 使用需要引入如下jar包

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
 
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.5.0</version>
</dependency>

1、Redis Sentinel 使用

package com.knowledge.cache.redis;
 
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisSentinelPool;
import redis.clients.jedis.exceptions.JedisConnectionException;
 
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
 
/**
 * @author hzwangjunqiang1@corp.netease.com
 * @desc redis sentinel 使用
 */
public class RedisSentinelClient {
    private static JedisSentinelPool pool = null;
    private static String redisHosts = "127.0.0.1:26378;127.0.0.1:26379;127.0.0.1:26380";
    private static String redisMaster = "";//master name
    private static String password = "";//密码,可选
    private static final int MAX_IDLE = 200;//最大空闲数
    private static final int MAX_TOTAL = 400;//最大连接数
    private static final int MIN_IDLE = 200;//最小空闲数
 
    static {
        //redis 连接池配置
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        poolConfig.setMaxIdle(MAX_IDLE);
        poolConfig.setMaxTotal(MAX_TOTAL);
        poolConfig.setMinIdle(MIN_IDLE);
        poolConfig.setTestOnBorrow(true);
        poolConfig.setTestOnReturn(true);
        Set<String> hosts = new HashSet<String>(Arrays.asList(redisHosts.split(";")));
        if (StringUtils.isBlank(password)) {
            pool = new JedisSentinelPool(redisMaster, hosts, poolConfig);
        } else {
            pool = new JedisSentinelPool(redisMaster, hosts, poolConfig, password);
        }
    }
 
    public String get(String key) throws JedisConnectionException {
        Jedis jedis = pool.getResource();
        try {
            return jedis.get(key);
        } catch (JedisConnectionException e) {
            throw e;
        } finally {
            jedis.close();
        }
    }
}

2、Redis Cluster 使用

package com.knowledge.cache.redis;
 
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.exceptions.JedisConnectionException;
 
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
 
/**
 * @author hzwangjunqiang1@corp.netease.com
 * @desc Redis cluster 使用
 */
public class RedisClusterClient {
    private static JedisCluster jedisCluster = null;
    private static String redisHosts = "127.0.0.1:6378;127.0.0.1:6379;127.0.0.1:6380"; //如:127.0.0.1:26379;127.0.0.1:26378
    private static String password = "";//密码,可选
    private static final int CONNECT_TIMEOUT = 1000;//连接超时时间
    private static final int SO_TIMEOUT = 1000;//响应超时时间
    private static final int MAX_ATTEMPTS = 5;//最大尝试次数
    private static final int MAX_IDLE = 200;//最大空闲数
    private static final int MAX_TOTAL = 400;//最大连接数
    private static final int MIN_IDLE = 200;//最小空闲数
 
    static {
        //连接池配置
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        poolConfig.setMaxIdle(MAX_IDLE);
        poolConfig.setMaxTotal(MAX_TOTAL);
        poolConfig.setMinIdle(MIN_IDLE);
        poolConfig.setTestOnBorrow(true);
        poolConfig.setTestOnReturn(true);
        //Redis Cluster 初始化
        Set<String> hosts = new HashSet<String>(Arrays.asList(redisHosts.split(";")));
        Set<HostAndPort> nodes = new LinkedHashSet<HostAndPort>();
        for (String host : hosts) {
            HostAndPort hostAndPort = new HostAndPort(host.split(":")[0], Integer.parseInt(host.split(":")[1]));
            nodes.add(hostAndPort);
        }
 
        if (StringUtils.isBlank(password)) {
            jedisCluster = new JedisCluster(nodes, CONNECT_TIMEOUT, SO_TIMEOUT, MAX_ATTEMPTS, poolConfig);
        } else {
            jedisCluster = new JedisCluster(nodes, CONNECT_TIMEOUT, SO_TIMEOUT, MAX_ATTEMPTS, password, poolConfig);
        }
    }
 
    /**
     * @param key
     * @return
     * @throws JedisConnectionException
     */
    public String get(String key) throws JedisConnectionException {
        try {
            return jedisCluster.get(key);
        } catch (JedisConnectionException e) {
            throw e;
        }
    }
 
    /**
     * @param key
     * @param value
     * @return
     * @throws JedisConnectionException
     */
    public String set(String key, String value) throws JedisConnectionException {
 
        try {
            return jedisCluster.set(key, value);
        } catch (JedisConnectionException e) {
            throw e;
        }
    }
}

以上介绍了Redis Sentinel 及 Redis Cluster的初始化过程及简单的使用,其他比较复杂的应用可以参考Redis 的官方API

四、Redis的过期淘汰策略

1、定时删除

  • 含义:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除
  • 优点:保证内存被尽快释放
  • 缺点:1)若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key;2)定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重

2、懒汉式删除

  • 含义:key过期的时候不删除,每次通过key获取值的时候去检查是否过期,若过期,则删除,返回null。
  • 优点:删除操作只发生在通过key取值的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了
  • 缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)

3、定期删除

含义:每隔一段时间执行一次删除过期key操作

优点:
1)通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用--处理"定时删除"的缺点;
2)定期删除过期key--处理"懒汉式删除"的缺点

缺点:
1)在内存友好方面,不如"定时删除"(会造成一定的内存占用,但是没有懒汉式那么占用内存);
2)在CPU时间友好方面,不如"懒汉式删除"(会定期的去进行比较和删除操作,cpu方面不如懒汉式,但是比定时好)

难点:
1)合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了),每次执行时间太长,或者执行频率太高对cpu都是一种压力;
2) 每次进行定期删除操作执行之后,需要记录遍历循环到了哪个标志位,以便下一次定期时间来时,从上次位置开始进行循环遍历

memcached只是用了惰性删除,而redis同时使用了惰性删除与定期删除,这也是二者的一个不同点(可以看做是redis优于memcached的一点);

五、Redis 使用过程中踩过的坑

1、在生产环境中一定要配置GenericObjectPoolConfig中的 maxIdle、maxTotal、minIdle.因为里面默认值太低了,如果生产环境中流量比较大的话,就会出现等待redis的连接的情况。

2、使用Redis Sentinel 一定要在最后执行jedis.close方法来释放资源,这个close方法是表示将正常的连接放回去连接池中,将不正常的连接给关闭。之前jedis低版本中都是调用returnResource方法来释放资源,如果连接不正常了会被重复使用,这时会出现很诡异的异常。所以建议使用比较高版本的jedis

3、为了更好的使用redis 连接池,建议采用 JedisPoolConfig来替代GenericObjectPoolConfig。JedisPoolConfig里面有一些默认的参数

4、maxIdle,maxTotal 最佳实践为 maxIdle = maxTotal

到此这篇关于浅析Redis Sentinel 与 Redis Cluster的文章就介绍到这了,更多相关Redis Sentinel与Redis Cluster内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Redis 相关文章推荐
Redis IP地址的绑定的实现
May 08 Redis
使用Redis实现秒杀功能的简单方法
May 08 Redis
基于Redis延迟队列的实现代码
May 13 Redis
为Java项目添加Redis缓存的方法
May 18 Redis
使用Redis实现实时排行榜功能
Jul 02 Redis
Redis集群新增、删除节点以及动态增加内存的方法
Sep 04 Redis
Redis命令处理过程源码解析
Feb 12 Redis
redis数据结构之压缩列表
Mar 21 Redis
Redis分布式锁的7种实现
Apr 01 Redis
redis sentinel监控高可用集群实现的配置步骤
Apr 01 Redis
Redis基本数据类型Set常用操作命令
Jun 01 Redis
Redis Lua脚本实现ip限流示例
Jul 15 Redis
redis cluster支持pipeline的实现思路
了解Redis常见应用场景
Jun 23 #Redis
压缩Redis里的字符串大对象操作
你真的了解redis为什么要提供pipeline功能
Redis缓存-序列化对象存储乱码问题的解决
比较几种Redis集群方案
解析Redis Cluster原理
You might like
这东西价格,可以买几台TECSUN S-2000
2021/03/02 无线电
destoon实现会员商铺中指定会员或会员组投放广告的方法
2014/08/21 PHP
Android App中DrawerLayout抽屉效果的菜单编写实例
2016/03/21 PHP
PHP封装的XML简单操作类完整实例
2017/11/13 PHP
调试php程序的简单步骤
2019/10/04 PHP
JavaScript 闭包在封装函数时的简单分析
2009/11/28 Javascript
基于JQuery的访问WebService的代码(可访问Java[Xfire])
2010/11/19 Javascript
详解JavaScript语法对{}处理的坑爹之处
2014/06/05 Javascript
JavaScript bold方法入门实例(把指定文字显示为粗体)
2014/10/17 Javascript
JS实现图片平面旋转的方法
2016/03/01 Javascript
JS阻止事件冒泡行为和闭包的方法
2016/06/16 Javascript
jQuery实现立体式数字动态增加(animate方法)
2016/12/21 Javascript
jQuery实现字符串全部替换的方法【推荐】
2017/03/09 Javascript
jQuery使用正则验证15/18身份证的方法示例
2017/04/27 jQuery
简单实现js点击展开二级菜单功能
2017/05/16 Javascript
JavaScript你不知道的一些数组方法
2017/08/18 Javascript
回顾Javascript React基础
2019/06/15 Javascript
python基于phantomjs实现导入图片
2016/05/13 Python
利用python实现微信头像加红色数字功能
2018/03/26 Python
pandas创建新Dataframe并添加多行的实例
2018/04/08 Python
Python设计模式之模板方法模式实例详解
2019/01/17 Python
python networkx 包绘制复杂网络关系图的实现
2019/07/10 Python
Python 实现Serial 与STM32J进行串口通讯
2019/12/18 Python
pytorch程序异常后删除占用的显存操作
2020/01/13 Python
Django之富文本(获取内容,设置内容方式)
2020/05/21 Python
python 对象真假值的实例(哪些视为False)
2020/12/11 Python
一级方程式赛车官方网上商店:F1 Store(支持中文)
2018/01/12 全球购物
Wedgwood英国官方网站:英式精致骨瓷餐具、礼品与生活精品,源于1759年
2019/09/02 全球购物
Derek Rose官网:英国高档睡衣、家居服和内衣品牌
2020/01/18 全球购物
两年的个人工作自我评价
2014/01/10 职场文书
新学期决心书
2014/03/11 职场文书
正风肃纪剖析材料
2014/09/30 职场文书
计划生育证明格式及范本
2014/10/09 职场文书
群众路线专项整治工作情况报告
2014/10/28 职场文书
员工年终考核评语
2014/12/31 职场文书
如何解决.cuda()加载用时很长的问题
2021/05/24 Python