Redis集群学习笔记

Redis学习笔记

image-20220515170917237

前言

Redis是现在最受欢迎的NoSQL数据库之一,Redis是一个使用ANSI C编写的开源、包含多种数据结构、支持网络、基于内存、可选持久性的KV键值对存储数据库

具备如下特征

  • 基于内存运行,性能高效
  • 支持分布式、理论上可以无限扩展
  • key-value存储系统
  • 开源的使用ANSI C语言编写,科技与内存亦可持久化的日志型、可基于内存一颗持久化的日志型、Key-value数据库,并提供多种语言的API

基本类型:字符串(string),散列(hash), 列表(lists), 集合(sets) 有序集合(sorted sets)

redis官网: https://redis.io/

集群模式

模式一、主从(master-salver) 2.8之前

​ redis同mysql一样,虽然读写都很快,但是也会产生读写压力大的情况,为了分担读的压力,redis支持主从复制,master进行写操作slave进行读操作,Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否全量分为 全量同步 和 增量同步

image-20220515173958246

模式二、哨兵模式(2.8之后)

​ 由于redis单一的主从复制模式下,容灾性较差,当集群中master由于故障下线了,那么slaver因为没有master二同步终端,因此需要人工进行故障转移工作,Redis在2.8之后提供了一种该可用的方案-哨兵模式(Sentinel),由一个或多个哨兵节点组成Sentinel系统用于监听一个或多个redis集群,监听主节点和从节点的运行状态,并当redis的master下线后,从master的从头节点中选举出新的master并维护新的主从关系

注:哨兵监视redis集群的同时,哨兵节点也会相互监听

image-20220515175034579

哨兵模式过程

1、监听集群(master-slave)

​ sentinel在监听redis的集群过程中会周期性的对redis集群发送指令进行状态监控

​ Ⅰ:通过info指令定期更新当前节点最新的节点信息

​ Ⅱ:发现新加入的slave节点,确认其主从关系

2、master宕机过程

主管下线和客观下线

​ sentinnel集群在监听redis集群的过程中,每个哨兵会对master发送心跳PING来确认master的存活,如果master在一定时间范围内不回应PONG,或者回应了一个错误的信息,该sentinel会认为当前集群的master已经无法使用(主观下线),并同时向sentinel集群中的其他节点发送sentinel ismaster-down-by-addr命令询问其他节点对主机的状态判断,当超过一定的sentinel确认master已经无法使用,这时候master下线的判定就认为是客观的(客观下线)

3、领导者(sentinel)选举

Ⅰ、每个在线的sentinel节点都有资格成为领导者,当它确认主节点主管下线的时候,会向其他Sentinel节点发送 sentinel is-master-down-by-addr命令,要求将自己设置为领导者

Ⅱ、收到命令的Sentinel节点,如果没有同意锅其他Sentinel节点的Sentinel is-master-down-by-addr命令,将同意该请求,否则拒绝

Ⅲ、如果该Sentinel节点发现自己的票数大于等于max(quorum,num(sentinels)/2 + 1) (过半),那么它将成为领导者

Ⅳ、如果此过程中没有选举出领导者,将进入下一次选举

4、故障转移

​ 故障转移就是当master宕机,sentinel集群会在redis集群中,自动选择一个合适的slave节点来升级为master节点的操作,不需要人工故障转移

1、筛选slave成为master

​ (1) 过滤掉无法使用的slave(主观下线、断线)

​ (2) 选择slave-priority最大的从节点(可能存在多个相同大小),如果只存在一个,则完成选择,否则继续

slave-priority值在redis启动文件中配置,用于决定故障转移优先级,以及数据备份时的备份顺序

​ (3) 选择复制偏移量最大的从节点(复制最完整的) 如果存在,则完成选择,否则继续

最完整: master和slave同步进度通过参数master_repl_offset 参数来记录主从同步的程度,每完成一次同步此值会进行累加,从多个slave中选择偏移量最大的slave则能选出复制master最完整的主机

​ (4) 选择runid(服务器运行的唯一ID)最小的从节点

2、对新master发送slaveof no one指令,停止其主从复制

3、修改程序段连接到新的master

4、向集群中其他slave发送指令修改为新的master的从机

5、原master重启后修改为新的master的slave

哨兵模式的缺点

1、如果是从节点下线了,sentinel是不会对其进行故障转移的,连接从节点的客户端也无法获取到新的可用从节点

2、较难支持在线扩容,在集群容量达到上限时会变得很复杂

3、集群中只有一个主节点,当写操作并发量特别大的时候,并无法缓解写的操作的压力

模式三、集群(clasuer) 3.0之后

​ 为了解决Redis高可用模式下集群动态扩容困难,写操作并发瓶颈问题,在3.0之后redis推出了Redis-Cluster集群模式

​ redis-cluster采用务中心结构,每个节点保存各自的数据和整个集群的状态,每个节点都和其他的所有节点连接,客户端连接任意主节点(master)可以对整个集群中数据进行读写,所有slave节点仅用于数据备份与故障转移

image-20220515190815922

分布式存储

​ Redis-Cluster 集群采用分布式存储的机制,每个master以及其slave只存储自己节点下的数据,客户端与任意master进行读写操作,会通过cluster的集群算法路由到对应的机器上【一致性hash算法】

一致性hash算法:redis-cluster把所有的redis节点映射到[0-16383]solt上(不一定是平均分配,图中的物理机的哈希值为5461,10822,16383),每一次读写操作集群会计算key的hash值,然后根据哈希值选择对应机器进行读写 

​ 将[0-16383]slot进行首位相连,形成哈希环,对于每个redis节点会分配到一个值,该节点就负责自己节点值到上一节点值的所有slot值数据的存储

​ 当集群要对一个key进行读写的时候,将key值计算出来的hash值向16384进行取模,将模值放入hash环,并向后寻找第一个redis的slot值,然后将key值存入redis上

image-20220515212531151

通过这种方式,能够保证集群中存在多个master同时写操作,极大的降低了单节点高并发读写的压力

动态扩容

​ redis-cluster 集群的一致性哈希算法支持动态扩容。动态扩容在一致性算法中涉及到两个问题。slot桶的重新分配数据转移

image-20220515212744920

​ 上图所示是一个三节点redis-cluster集群里,redis节点与key在哈希环上的映射关系,可以看出key1(16121),key2(2200)两个key存储在redis1上,key 3 4 5 6四个key会存储在redis2上,而redis3只存储了key7,当我们需要往集群中新增一台redis,如果改变了全部redis分配的slot,那么数据的转移会涉及到整个集群,那将是灾难性

​ 在一致性哈希算法下,会将新的redis节点计算出哈希值,放入哈希环中,这时,redis2中H(key) <= 7866 的所有key值会进行转移,转移到redis4中,而当我们需要在新集群中删除其中一台redis2,redis中的所有数据会根据算法迁移到redis3上存储

image-20220515214307353

​ 一致性哈希算法在保持了单调性的同时,还将数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减少了服务器的压力

故障发现与转移

​ Cluster集权在运行时所有的redis节点之间会通过ping/pong消息实现节点通信,消息不但传输节点槽信息,也能传播节点状态,主从状态,节点故障等。

​ 当集群中某一节点出现问题,集群会通过消息进行发现,与sentinel模式相同,节点故障在集群中也会经过主观下线,客观下线的过程,但是cluster集群中,并不需要sentinel来进行节点监控与故障转移,而是由集群中的master们来处理的

redis集群安装

1、在/uar/local/创建redis-cluster目录

mkdir redis-cluster

2、在redis-cluster目录下创建节点目录

mkdir 7001 7002 7003 7004 7005 7006

3、复制一份redis.conf文件到7001节点目录

cp redis.conf ../redis-cluster/7001

4、修改redis-cluster/7001/redis.conf

bind  0.0.0.0
daemonize yes (设置后台运行redis)
cluster-enabled yes (开启集群)
cluster-node-timeout 15000 (设置请求超时时间,默认为15秒,可以自行修改)
appendonly yes (aop日志开启,会每次进行写操作都记录一条日志)
port 7001(和目录端口一致)
pidfile /var/run/redis_7001.pid
dbfilename dump_7001.rdb
appendfilename "appendonly_7001.aof"
cluster-config-file nodes_7001.conf
# 设置密码
masterauth 123456
requirepass 123456

5、修改完成后,把redis.conf 文件复制到其他节点中,并修改不同端口部分:

# 当前目录:redis-cluster/7001/
cp redis.conf ../7002/
cp redis.conf ../7003/
cp redis.conf ../7004/
cp redis.conf ../7005/
cp redis.conf ../7006/

6、编辑群起脚本 start-all.sh

#! /bin/bash
REDIS_HOME=/usr/local/redis-cluster
cd $REDIS_HOME/7001 && ./redis-server redis.conf
cd $REDIS_HOME/7002 && ./redis-server redis.conf
cd $REDIS_HOME/7003 && ./redis-server redis.conf
cd $REDIS_HOME/7004 && ./redis-server redis.conf
cd $REDIS_HOME/7005 && ./redis-server redis.conf
cd $REDIS_HOME/7006 && ./redis-server redis.conf

7、赋予权限

chmod 755 start-all.sh
注:查看进程是否启动,并确定端口:
ps -ef|grep redis|grep cluster

8、创建集群

./redis-cli -a 123456 --cluster create 192.168.136.152:7001 192.168.136.152:7002 192.168.136.152:7003 192.168.136.152:7004 192.168.136.152:7005 192.168.136.152:7006 --cluster-replicas 1

--cluster-replicas 1 :表示一个master挂载几个slave,至少需要3个master,此处设置为1 至少需要 6节点,如果设置为2 至少需要9个节点。

9、关闭集群

#! /bin/bash
REDIS_HOME=/usr/local/redis-cluster
cd $REDIS_HOME/7001 && ./redis-cli -c -h 192.168.136.152 -p 7001 shutdown
cd $REDIS_HOME/7002 && ./redis-cli -c -h 192.168.136.152 -p 7002 shutdown
cd $REDIS_HOME/7003 && ./redis-cli -c -h 192.168.136.152 -p 7003 shutdown
cd $REDIS_HOME/7004 && ./redis-cli -c -h 192.168.136.152 -p 7004 shutdown
cd $REDIS_HOME/7005 && ./redis-cli -c -h 192.168.136.152 -p 7005 shutdown
cd $REDIS_HOME/7006 && ./redis-cli -c -h 192.168.136.152 -p 7006 shutdown

cd /usr/local/redis-cluster/7001 && rm -f `ls |grep _7001`
cd /usr/local/redis-cluster/7002 && rm -f `ls |grep _7002`
cd /usr/local/redis-cluster/7003 && rm -f `ls |grep _7003`
cd /usr/local/redis-cluster/7004 && rm -f `ls |grep _7004`
cd /usr/local/redis-cluster/7005 && rm -f `ls |grep _7005`
cd /usr/local/redis-cluster/7006 && rm -f `ls |grep _7006`

操作代码

JedisCluster

说明:在redis中一共有16384个Slot,每个节点负责一部分Slot,当对Key进行操作时,redis会通过CRC16计算出key对应的Slot,将Key映射到Slot所在节点上执行操作,相当于一个Map<Integer,Jedis>,根据slot值去找个Map中获取Jedis,然后使用传统Jedis方式查询数据 

弊端:大量数据查询是会大大增加tcp连接中交互往返的开销,每次查询会创建一次jedis连接

JedisCluster jedisCluster = null; // Redis 集群模式下
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//设置过大,当客户端或服务端出现抖动,可能造成服务端连接过多
jedisPoolConfig.setMaxIdle(100);
jedisPoolConfig.setMaxTotal(100);
jedisPoolConfig.setMaxWaitMillis(100);
//Redis Cluster模式下,设置为True,每次操作多进行一次PING,影响性能
jedisPoolConfig.setTestOnReturn(false);
//同上
jedisPoolConfig.setTestOnBorrow(false);
Set<HostAndPort> jedisClusterNodes = new HashSet<>();
jedisClusterNodes.add(192.168.23.114,6379);
jedisClusterNodes.add(192.168.23.115,6379);
jedisClusterNodes.add(192.168.23.116,6379);
// timeout,password,maxAttempts 都是整数,根据需要配置
jedisCluster = new JedisCluster(jedisClusterNodes, timeout, timeout, maxAttempts, password, jedisPoolConfig);

//string 
String value = jedisCluster.get(s);
//hash
Map<String, String> stringStringMap = jedisCluster.hgetAll(s);
//set
Set<String> sm = jedisCluster.smembers(s);
//zset
Set<Tuple> zr = jedisCluster.zrangeWithScores(s, 0, -1);
//list
List<String> lrange = jedisCluster.lrange(((String) v), 0, -1);

JedisCluster PipeLime

说明:pipeline对比redisCluster操作,先将要查询的数据进行收集,类似一个Map<String,List>,将同一个槽内的查询进行合并,相同槽位的key,使用同一个jedis.pipeline去执行命令,同时维护一个Map<String,JedisClient>的连接池,数据查询结束后,将client释放返回到池中 

弊端:pipeline不是一个事务,不能保证里面的一组命令一起成功或者失败。

// 1、获取Jedis集群连接
JedisCluster jedisCluster = null; // Redis 集群模式下
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//设置过大,当客户端或服务端出现抖动,可能造成服务端连接过多
jedisPoolConfig.setMaxIdle(100);
jedisPoolConfig.setMaxTotal(100);
jedisPoolConfig.setMaxWaitMillis(100);
//Redis Cluster模式下,设置为True,每次操作多进行一次PING,影响性能
jedisPoolConfig.setTestOnReturn(false);
//同上
jedisPoolConfig.setTestOnBorrow(false);
Set<HostAndPort> jedisClusterNodes = new HashSet<>();
jedisClusterNodes.add(192.168.23.114,6379);
jedisClusterNodes.add(192.168.23.115,6379);
jedisClusterNodes.add(192.168.23.116,6379);
// timeout,password,maxAttempts 都是整数,根据需要配置
jedisCluster = new JedisCluster(jedisClusterNodes, timeout, timeout, maxAttempts, password, jedisPoolConfig);

// 2、
JedisClusterPipeLineUtil2 pipelined = JedisClusterPipeLineUtil2.pipelined(jedisCluster);
// 刷新集群信息
pipelined.refreshNodesInfo();

//string 
pipelined.get(s);
//hash
pipelined.hgetAll(s);
//set
pipelined.smembers(s);
//zset
pipelined.zrangeWithScores(s, 0, -1);
//list
pipelined.lrange(s, 0, -1);
// 执行pipelined代码
List<Map<String, Object>> result = pipelined.syncAndReturnAll();

JedisClusterPipeLineUtil2.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.*;
import redis.clients.jedis.exceptions.JedisMovedDataException;
import redis.clients.jedis.exceptions.JedisRedirectionException;
import redis.clients.util.JedisClusterCRC16;
import redis.clients.util.SafeEncoder;
public class JedisClusterPipeLineUtil2 extends PipelineBase implements Closeable {
    private static Logger log = LoggerFactory.getLogger(JedisClusterPipeLineUtil2.class);
    private final Queue<Client> orderedClients = new LinkedList<Client>();
    // 顺序调用的所有key
    private List<String> keys = new ArrayList<>();
    /**
     * 一次pipeline过程中使用到的jedis缓存
     */
    private final Map<JedisPool, Jedis> poolToJedisMap = new HashMap<>();
    private boolean hashDataInBuff = false; // 查看数据是否再缓存区
    private final JedisSlotBasedConnectionHandler connectionHandler;
    private final JedisClusterInfoCache clusterInfoCache;
    private static final Field SLOT_BASED_CONNECTION_HANDLER_FIELD;
    private static final Field CLUSTER_INFO_CACHE_FIELD;
    // 是否因为集群信息更改导致重启
    private boolean errorCountReset = false;
    static {
        SLOT_BASED_CONNECTION_HANDLER_FIELD = getField(BinaryJedisCluster.class, "connectionHandler");
        CLUSTER_INFO_CACHE_FIELD = getField(JedisClusterConnectionHandler.class, "cache");
    }
    public JedisClusterPipeLineUtil2(JedisCluster jedisCluster) {

        this.connectionHandler = getValue(jedisCluster, SLOT_BASED_CONNECTION_HANDLER_FIELD);
        this.clusterInfoCache = getValue(connectionHandler, CLUSTER_INFO_CACHE_FIELD);
    }
    private static Field getField(Class<?> cls, String fieldName) {
        log.info("调用 getField 方法");
        try {
            Field field = cls.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field;
        } catch (NoSuchFieldException | SecurityException e) {
            throw new CommonException(BaseEnum.QUERY_SQL_ERROR);
        }
    }
    private static <T> T getValue(Object obj, Field field) {
        log.info("调用 getValue 方法");
        try {
            return (T) field.get(obj);
        } catch (IllegalArgumentException | IllegalAccessException e) {
            // throw new RuntimeException(e);
            throw new CommonException(BaseEnum.QUERY_SQL_ERROR);
        }
    }
    @Override
    protected Client getClient(String key) {
        keys.add(key);
        log.info("调用 getClient方法");
        Client client = getClient(SafeEncoder.encode(key));
        orderedClients.add(client);
        return client;
    }
    @Override
    protected Client getClient(byte[] key) {
        Client client;int slot = JedisClusterCRC16.getSlot(key);
        JedisPool pool = clusterInfoCache.getSlotPool(slot);
        Jedis borrowedJedis = poolToJedisMap.get(pool);
        if (null == borrowedJedis) {
            // 获取客户端连接 存储到容器中去
            borrowedJedis = pool.getResource();
            poolToJedisMap.put(pool, borrowedJedis);
        }
        client = borrowedJedis.getClient();
        // 如果已经存在直接返回
        hashDataInBuff = true;
        return client;
    }
    @Override
    public void close() {
        clean();
        // 清除客户端队列
        orderedClients.clear();
        // 关闭缓存的jedis连接
        for (Jedis jedis : poolToJedisMap.values()) {
            if (hashDataInBuff){
                flushCacheData(jedis);
            }
            jedis.close();
        }
        // 清除jedis连接缓存数据
        poolToJedisMap.clear();
        // 还原初始状态
        hashDataInBuff = false;
    }
    private void flushCacheData(Jedis jedis){
        try{
            jedis.getClient().getAll();
        }catch (RuntimeException ex){
            // 保证代码高可用
            ex.printStackTrace();
            log.error("刷新jedis连接信息超时");
        }
    }
    public List<Map<String,Object>> syncAndReturnAll() {
        log.info("调用 syncAndReturnAll 方法");
        List<Object> formatted = new ArrayList<>();

        innerSync(formatted);

        List<Map<String,Object>> objects = new ArrayList<>();

        if (formatted.size() == keys.size()){
            for (int i = 0; i < keys.size(); i++) {
                HashMap<String, Object> objectObjectHashMap = new HashMap<>();
                objectObjectHashMap.put(keys.get(i),formatted.get(i));
                objects.add(objectObjectHashMap);
            }
        }
        return objects;
    }
    private void innerSync(List<Object> formatted) {
        HashSet<Client> clientSet = new HashSet<>();
        try{
            for (Client client : orderedClients) {
                Object response = generateResponse(client.getOne()).get();
                formatted.add(response);
                // size相同说明所有的client都已经添加了,不用再调用add方法了 注:此方法在刷新集群信息之后
                if (clientSet.size() != poolToJedisMap.size()){
                    clientSet.add(client);
                }

            }
        }catch (JedisRedirectionException ex){
            // 捕获因为集群信息更改导致查询数据失败的问题
            if (ex instanceof JedisMovedDataException){
                refreshNodesInfo();
                if (!errorCountReset){
                    innerSync(formatted);
                    errorCountReset = true;
                }
            }
        } finally {
            if (clientSet.size()!= poolToJedisMap.size()){
                // 所有还没有执行过的client要保证执行刷新,防止放回线程池后面被污染
                for (Jedis jedis : poolToJedisMap.values()) {
                    if (clientSet.contains(jedis.getClient())) {
                        continue;
                    }
                    flushCacheData(jedis);
                }
            }
            hashDataInBuff = false;
            close();
        }
    }
    /**
     * 根据jedisCluster实例生产对于的JedisClusterPipeline
     */
    public static JedisClusterPipeLineUtil2 pipelined(JedisCluster jedisCluster) {
        log.info("创建 JedisClusterPipLineUtils对象");
        // 清除请求队列
        JedisClusterPipeLineUtil2 pipeline = new JedisClusterPipeLineUtil2(jedisCluster);
        return pipeline;
    }
    /**
     * 由于集群存在节点的动态添加删除 client不能实时感知 这个命令就是手动刷新solt槽的集群信息
     */
    public void refreshNodesInfo() {
        connectionHandler.renewSlotCache();
    }
}

LettucePipeLine

介绍:Lettuce是一个高性能基于Java编写的Redis驱动框架,底层集成了Project Reactor提供天然的反应式编程,通信框架集成了Netty使用了非阻塞IO5.x版本之后融合了JDK1.8的异步编程特性,在保证高性能的同时提供了十分丰富易用的API



Set<RedisURI> uris = new HashSet<>();
RedisURI.Builder builder = RedisURI.builder()
    .withHost(192.168.136.131)
    .withPassword("xxxx01")
    .withPort(7890);
uris.add(builder.build());

// 设置redis连接池
ClientResources resources = DefaultClientResources.builder()
    .ioThreadPoolSize(4) //IO线程数
    .computationThreadPoolSize(4) // 任务线程数
    .build();

// 创建连接
RedisClusterClient client = RedisClusterClient.create(resources,uris);

// 开启 自适应集群拓扑刷新和周期拓扑刷新
ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
    .enableAdaptiveRefreshTrigger(
    ClusterTopologyRefreshOptions.RefreshTrigger.MOVED_REDIRECT,
    ClusterTopologyRefreshOptions.RefreshTrigger.PERSISTENT_RECONNECTS,
    ClusterTopologyRefreshOptions.RefreshTrigger.UNKNOWN_NODE,
    ClusterTopologyRefreshOptions.RefreshTrigger.ASK_REDIRECT
 )
 .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(3))
 .build();
// 配置集群选项,自动重连,最多重连次数
ClusterClientOptions build = ClusterClientOptions.builder()
    .topologyRefreshOptions(clusterTopologyRefreshOptions)
    .maxRedirects(maxAttempts)
    .build();

client.setOptions(build);
client.setDefaultTimeout(Duration.ofSeconds(timeout));
StatefulRedisClusterConnection<String, String> connection = client.connect();

RedisAdvancedClusterAsyncCommands<String, String> async = connection.async();
List<Map<String, RedisFuture>> list = new ArrayList<>();
// 关闭自动提交刷新
async.setAutoFlushCommands(false);
//string 
list.add(async.get(s));
//hash
list.add(async.hgetAll(s);
//set
list.add(async.smembers(s));
//zset
list.add(async.zrangeWithScores(s, 0, -1));
//list
list.add(async.lrange(s, 0, -1));


// 执行pipelined代码
async.flushCommands();
         
for(li:list){
    System.out.println(li.getValue().get());
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值