Redis(一) SpringBoot 整合 Redis-Cluster

一. Redis 架构模式

Redis 单机服务在高可用和高并发环境要求下提供的能力是十分有限的,因此 Redis 提供了多种架构部署模式,比如我们熟知的主从模式、哨兵模式与切片集群模式。

1. 主从模式

主从模式由主节点(master)和多个从节点(slave)组成,多个Redis服务实例之间通过replicaof命令构成主从关系并建立长连接 。其中,主节点可以用于处理客户端读写操作,而从节点一般仅处理客户端读操作,从而实现读写分离 ;除此之外,为了实现主从之间的数据一致性,主节点支持通过异步的方式将主节点数据变更单向同步到从节点,即主从复制过程,其核心同步命令为psync [runId] [offset]

  • 全量复制:在首次同步情况下,从节点会向主节点请求进行全量数据同步(开销大);
  • 增量复制:在断线重连情况下,从节点会根据保存的同步数据偏移量offset向主节点请求增量数据同步;

同时,在运行过程中,主从服务之间还会通过命令传播来保证数据一致性。但是,由于主从之间的数据同步是异步进行的,所以无法保证实现强一致性,在网络波动情况下,从节点读取到的数据可能会出现过时。

2. 哨兵模式

在主从复制模式中,一旦主节点宕机,就需要通过人工介入来手动切换从节点为新的主节点,从而继续对外提供服务,这在实际运维过程中十分被动和不便。因此,为了实现自动服务监控、主从切换和故障转移,可以在主从模式基础上,添加一组"哨兵"节点进行服务监控,这就构成了哨兵模式(Redis Sentinel)。其中,"哨兵节点"是一组特殊的Redis进程节点,可以通过sentinel monitor <master-name>配置哨兵节点信息(监听主节点信息),并使用redis-sentinel xxx.conf命令启动哨兵节点服务,启动后它们会自动读取网络拓扑并感知其他哨兵节点的存在;注意哨兵节点并不提供数据读写服务,仅与主从节点之间进行通信,实现监控、选主、切换和通知。

哨兵节点们会周期性的向所有主从节点发送心跳检测,并获取其状态信息。当主节点被判定为客观下线后,就会在哨兵集群中根据投票算法进行哨兵Leader节点的选举的过程,并由哨兵Leader节点按照规则和优先级从剩余从节点中选定新的主节点,该过程包括:

  • 对选定从节点发送slaveof no one命令将其转为主节点;
  • 向其他从节点发送slaveof <masterIp>命令以指向新的主节点;

最后在故障迁移完成后,向客户端或其他订阅者发送通知,以更新Redis服务拓扑结构。

3. 切片集群模式

主从模式和哨兵模式都保证了Redis服务架构的高可用性,但是目前为止不论是主节点还是从节点,所有数据仍然都被存储在该节点上,这并不满足分布式和数据水平拓展的需要,且随着数据量越来越大,数据的存储压力并没有被分担。在社区发展中,Redis的分布式集群架构涌现出很多优秀的项目,比如TwemProxyCodis等,目前主流的集群方案应该是Redis 3.x之后官方推出的Redis-Cluster切片集群模式,其采用的是去中心化分布式架构。

3.1 基本架构

Redis-Cluster集群模式中,Redis服务由多个集群节点共同提供,每个集群节点都是主从架构,即一主多从模式(主从之间数据同步),这保证了每个集群节点的高可用性;除此之外,Redis-Cluster 将数据分布在多个集群节点上存储,每个集群节点负责存储部分数据即可,这实现了数据的水平拓展。最后,所谓的去中心化是指Redis Cluster不存在中心节点或者像Zookeeper这样的统一管理中心,而是每个节点都记录集群的部分状态信息,并且周期性的通过Gossip协议(分布式一致性协议)与其他节点通信从而交换信息(节点地址、状态、维护的哈希槽等),使每个节点记录的信息实现最终一致性。

故障检测与迁移与哨兵模式的核心思想基本一致,集群中的每个节点都会周期性地向集群中的其它节点发送PING消息,以此来检测节点是否在线。当判定某主节点下线之后,就会由具有投票权的主节点们基于Raft算法对其从节点进行投票选举,新当选的主节点会接管后续的迁移(接管管理的哈希槽)、重指向(其他从节点指向新的主节点)和通知工作(广播,更新其他节点所保存的集群配置),以此来完成之前哨兵的工作。

3.2 数据分配

Redis Cluster 使用哈希槽(Hash Slot)机制来分配数据。在
Redis Cluster 方案中,切片集群共有 16384 (0~16383) 个哈希槽(固定),每个集群主节点被指派并负责管理一部分哈希槽,在对16384个槽指派完成后,集群才会进入上线状态。其中,哈希槽类似于数据分区,用来处理数据和节点之间的映射关系,从而实现数据在不同集群节点上的分配与读写;当客户端向集群任意节点 发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键key属于哪个槽:

  • 通过CRC16算法(Hash算法)计算对应哈希槽的映射,即 slot = CRC16(key) % 16384
    • 若计算slot哈希槽刚好属于当前节点管理的区间,那么由当前节点直接执行命令即可;
    • 若计算slot哈希槽不属于当前节点管理区间,则节点会基于保存的集群信息向客户端返回MOVED指令,对客户端请求进行重定向转至正确节点;

比如,当前集群有三个分片节点,⼀种可能的分配⽅式如下:

0号分⽚:[0,5461],共5462个槽位;

1号分⽚:[5462,10923],共5462个槽位;

2号分⽚:[10924,16383],共5460个槽位;

3.3 集群部署

Redis Cluster集群的部署非常简单,首先需要单独启动每个Redis实例,并在配置文件conf中修改开启集群模式cluster-enabled yes,然后可以通过官方脚本 redis-cli --cluster create --cluster-replicas <cnt> <master list> <slaves list>来在任意Redis服务节点 机器上启动集群服务,并指定分片的主从关系(cnt表示每个主节点master的从节点数量),以及自动均匀分配哈希槽slots

最后,在集群搭建完成后,此时使⽤客户端连上集群中的任何⼀个节点都相当于连上了整个集群;客户端不需要连接集群中所有的节点,只需要任意连接集群中的一个可用节点即可自动读取拓扑。

二. Redis Java 客户端

Redis 服务搭建完成后,访问 Redis 服务需要通过 Redis 的 Java 客户端,常见的客户端类型包括:

  • Jedis:最经典也是Redis官方提供的客户端,其特点是简单、易上手、学习成本低,但Jedis实例是线程不安全的,在多线程环境下频繁的创建和销毁又会存在性能损耗,因此通常需要基于连接池使用;

  • Lettuce:目前主流的Java客户端之一,其是基于Netty实现的,支持同步、异步和响应式编程,功能和性能优化都不错,并且是线程安全的;

  • Redission:基于Redis实现的分布式客户端,功能非常强大,提供很多Redis场景解决方案框架,比如WatchDog、分布式锁等;

1. Jedis 直连方式

1.1 引入依赖

xml 复制代码
<!-- jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

1.2 代码使用

java 复制代码
public void testRedis(){

    // 建立连接对象
    Jedis jedis = new Jedis("localhost", 6379);
    // 设置密码(如果 Redis 服务设置了密码的话)
    jedis.auth("password");
    // 选择操作库(可选操作,默认为0)
    jedis.select(0);
    // redis 操作
    // keys
    Set<String> keys = jedis.keys("*");
    System.out.println(keys);
    // string
    String result = jedis.set("name","user_name");
    String name = jedis.get("name");
    System.out.println(name);
    // hash
    jedis.hset("user:1","name","user_1_name");
    String hname = jedis.hget("user:1","name");
    System.out.println(hname);
    // 释放连接
    if(jedis != null){
        jedis.close();
    }
}

2. Spring Data Redis

类似于 Spring Data JPA,为了屏蔽 Redis Java 客户端之间的差异,SpringBoot 提供了 Spring Data Redis 项目,对 Jedis 和 Letture 客户端进行了封装和整合,实现了 RedisTemplate 并提供了统一的 Redis 服务交互操作,支持连接池、序列化、集群等;接下来本节将以 Jedis 为例进行介绍。

2.1 引入依赖

xml 复制代码
<!--在springboot 2.0版本后,spring-boot-starter-data-redis 默认使用Lettuce客户端-->
<!--如果要使用Jedis, 就要在pom.xml中去掉Lettuce 并且添加 Jedis 依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- jedis 内置了 commons-pool2 连接池-->
<!-- 注意:若是 Lettuce 使用连接池的话,需要单独引入 commons-pool2 依赖 -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

2.2 SpringBoot 配置

2.2.1 YML配置
xml 复制代码
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    password: wangxin88
    # 连接超时时间(ms)
    timeout: 5000
    jedis:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 30
        # #连接池最大阻塞等待时间 ms(使用负值表示没有限制)
        max-wait: -1
2.2.2 自定义配置类

SpringBoot YML 配置中以 spring.redis 为前缀的配置项默认绑定到SpringBoot提供的 RedisProperties 配置类中,因此可以默认使用 RedisProperties接收配置项,并在Config中初始化;除此之外,使用定义配置类,绑定 YML 自定义配置项,接收 YML 配置参数也是可以的。

java 复制代码
@Configuration
public class RedisConfig {

    @Resource
    RedisProperties redisProperties;

    // 配置 RedisTemplate,并设置序列化
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(String.class);
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // key 序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // value 序列化器(jackson2Json)
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 初始化配置
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    // 配置 StringRedisTemplate
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate redisTemplate = new StringRedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }

    // 配置连接工厂方式一: 构造函数配置(推荐)
    // SpringBoot 提供默认的连接工厂,如果自己提供则会覆盖原有工厂对象
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        // 方式一: 构造函数配置
        // 将连接池配置转化到客户端配置 JedisClientConfiguration
        JedisClientConfiguration jedisClientConfiguration = JedisClientConfiguration.builder()
                .usePooling()
                .poolConfig(jedisPoolConfig())
                .build();
        // 单机配置 + 客户端配置 = jedis连接工厂
        return new JedisConnectionFactory(jedisStandaloneConfg(), jedisClientConfiguration);

    }

    // 配置连接工厂方式二: set配置
    // 注意:该方式从 Jedis 2.0 之后弃用,改为RedisStandaloneConfiguration, RedisSentinelConfiguration,RedisClusterConfiguration的方式
    //@Bean
    //public RedisConnectionFactory redisConnectionFactory() {
    //    JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
    //    // set 方法均废弃
    //    jedisConnectionFactory.setHostName(redisProperties.getHost());
    //    jedisConnectionFactory.setPort(redisProperties.getPort());
    //    jedisConnectionFactory.setPassword(redisProperties.getPassword());
    //    jedisConnectionFactory.setPoolConfig(jedisPoolConfig());
    //    // 初始化配置
    //    jedisConnectionFactory.afterPropertiesSet();
    //    return jedisConnectionFactory;
    //}

    // 配置 Redis Standalone 模式(单机模式)
    public RedisStandaloneConfiguration jedisStandaloneConfg() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(redisProperties.getHost());
        redisStandaloneConfiguration.setDatabase(redisProperties.getDatabase());
        redisStandaloneConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
        redisStandaloneConfiguration.setPort(redisProperties.getPort());
        return redisStandaloneConfiguration;
    }

    // 配置连接池
    public JedisPoolConfig jedisPoolConfig() {
        // 继承自 GenericObjectPoolConfig(commons.pool2),但 JedisPoolConfig 是 redis.clients.jedis 中的
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        // 最小空闲连接数
        poolConfig.setMinIdle(redisProperties.getJedis().getPool().getMinIdle());
        // 最大空闲连接数
        poolConfig.setMaxIdle(redisProperties.getJedis().getPool().getMaxIdle());
        // 最大阻塞等待时间 :setMaxWaitMillis 已被废弃
        poolConfig.setMaxWait(redisProperties.getJedis().getPool().getMaxWait());
        // 最大连接数
        poolConfig.setMaxTotal(redisProperties.getJedis().getPool().getMaxActive());
        // 连接在池中最小空闲时间(以毫秒为单位)
        // poolConfig.setMinEvictableIdleTimeMillis();
        return poolConfig;
    }

}

三. 整合 Redis-Cluster 集群配置

在整合Redis-Cluster时,虽然RedisTemplate在二者之上封装了一层,屏蔽了底层差异,但Jedis客户端和Letture客户端的区别是:

  • JedisCluster 可以自动感知集群节点变化,并自动刷新节点拓扑;Letture Cluster 在 SpringBoot 2.3 之后是默认支持属性配置开启集群拓扑刷新点;
  • JedisCluster 不支持集群读写分离,默认主库读写,从库仅同步数据;Letture 支持读写分离配置,但是从库数据可能过时,不保证时效性;
  • JedisCluster不支持批量mset,mget,pipline等批量方法,因为RedisCluster集群本身就不支持;而Letture支持这些操作,因为客户端层面进行了分组处理;

本节以 Jedis Cluster 的配置为例,集群拓扑实例配置介绍如下:

1. YML 配置项

xml 复制代码
spring:
  redis:
    # 密码(若有)
    password: wangxin88
    # 连接超时时间(ms)
    timeout: 5000
    jedis:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大连接数
        max-active: 30
        # #连接池最大阻塞等待时间 ms(使用负值表示没有限制)
        max-wait: -1
    cluster:
      # 集群节点初始列表 List<String> nodes
      # 连接集群任意可用节点地址即可,会自动读取全部拓扑
      nodes:
        - 192.18.216.249:31754
      # 集群执行命令时的最大重定向数
      max-redirects: 5

2. 自定义配置类

java 复制代码
@Configuration
public class ClusterConfig {

    @Resource
    private RedisProperties redisProperties;

    // 配置 RedisTemplate,并设置序列化
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(String.class);
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // string 序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash 序列化方式
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 初始化配置
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    // 配置 StringRedisTemplate
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate redisTemplate = new StringRedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }

    // 配置连接工厂
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {

        // 将连接池配置转化到客户端配置 JedisClientConfiguration
        JedisClientConfiguration jedisClientConfiguration = JedisClientConfiguration.builder()
                .usePooling()
                .poolConfig(jedisPoolConfig())
                .build();
        // 集群配置 + 客户端配置 = jedis连接工厂
        return new JedisConnectionFactory(jedisClusterConfg(), jedisClientConfiguration);
    }

    // 配置 Redis Cluster 模式(集群模式)
    public RedisClusterConfiguration jedisClusterConfg() {
        RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
        redisClusterConfiguration.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
        redisClusterConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
        return redisClusterConfiguration;
    }

    // 配置连接池
    public JedisPoolConfig jedisPoolConfig() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        // 最小空闲连接数
        poolConfig.setMinIdle(redisProperties.getJedis().getPool().getMinIdle());
        // 最大空闲连接数
        poolConfig.setMaxIdle(redisProperties.getJedis().getPool().getMaxIdle());
        // 最大阻塞等待时间 :setMaxWaitMillis 已被废弃
        poolConfig.setMaxWait(redisProperties.getJedis().getPool().getMaxWait());
        // 最大连接数
        poolConfig.setMaxTotal(redisProperties.getJedis().getPool().getMaxActive());
        // 连接在池中最小空闲时间(以毫秒为单位)
        // poolConfig.setMinEvictableIdleTimeMillis();
        return poolConfig;
    }

}

3. JedisCluster 原理分析

基于Jedis配置的ClusterJedisConnectionFactory 初始化完成后会自动调用afterPropertiesSet()方法初始化配置,若RedisConfigurationClusterConfiguration类型,则表示其在集群模式下,此时会通过createrCluster()方法创建一个 JedisCluster 实例(Jedis提供的集群操作对象)并交给 RedisTemplate使用,所以本质上 RedisTemplate 还是调用的 JedisCluster

java 复制代码
// 创建 JedisCluster 实例
protected JedisCluster createCluster(RedisClusterConfiguration clusterConfig,

        GenericObjectPoolConfig<Jedis> poolConfig) {
    Assert.notNull(clusterConfig, "Cluster configuration must not be null!");
    Set<HostAndPort> hostAndPort = new HashSet<>();
    for (RedisNode node : clusterConfig.getClusterNodes()) {
        hostAndPort.add(new HostAndPort(node.getHost(), node.getPort()));
    }
    int redirects = clusterConfig.getMaxRedirects() != null ? clusterConfig.getMaxRedirects() : 5;
    return new JedisCluster(hostAndPort, this.clientConfig, redirects, poolConfig);
}

// 获取 JedisClusterConnection 连接
@Override
public RedisClusterConnection getClusterConnection() {
    assertInitialized();
    if (!isRedisClusterAware()) {
        throw new InvalidDataAccessApiUsageException("Cluster is not configured!");
    }
    return new JedisClusterConnection(this.cluster, this.clusterCommandExecutor, this.topologyProvider);
}

我们知道在 Redis 中一共有 16384 个Slot槽位,每个集群节点各映射并负责部分Slot,当对 Key 进行操作时,Redis会通过CRC16算法计算出key对应的Slot,然后将Key映射到Slot所在集群节点上执行操作。我们先看一下JedisCluster是如何执行命令的:

(1)JedisCluster是通过JedisSlotBasedConnectionHandler获取连接的,在JedisCluster的方法中,会创建一个JedisSlotBasedConnectionHandler,它有一个字段cache,类型为JedisClusterInfoCacheJedisClusterInfoCache缓存了每个主节点 对应的连接池nodes,以及每个槽位对应的连接池。

(2)该JedisClusterInfoCache类型的成员变量cache有两个HashMap类型的成员变量nodesslotsnodes保存节点和JedisPool的映射关系,slots保存16384个slot和JedisPool的映射关系,这里slot和节点实现了映射关系 。接着,cache会调用getSlotPool(),从成员变量slots中通过slot取到了相应节点的JedisPool

JedisCluster在发送命令前会根据CRC16(key) % 16384 计算出key所在的槽位,根据槽位获取对应的节点连接池,再从连接池中获取一个Jedis连接。

(3)注意: 上图中109、105、198三个从节点对应的 JedisPool 是没有在 slots 中映射的(slots中映射的JedisPool都是主redis节点),因此在创建连接时也是从主库拿的连接Connection,没有经过从库中访问(已验证,包括读也没有),从库仅作为数据同步的高可用。但是 Redis-Cluster 从库是支持读取的,JedisCluster可能考虑到从库数据的滞后性风险,没有提供从库读操作。

相关推荐
Resean022317 分钟前
架构设计系列(六):缓存
java·redis·缓存·策略模式
楚钧艾克24 分钟前
利用go-migrate实现MySQL和ClickHouse的数据库迁移
数据库·redis·后端·mysql·clickhouse·migrate
我码玄黄4 小时前
Redis多线程技术助力向量数据库性能飞跃
数据库·redis·缓存
嘵奇4 小时前
SpringBoot五:Web开发
java·前端·spring boot
赤橙红的黄4 小时前
SpringBoot两种方式接入DeepSeek
java·spring boot·spring
呆萌很4 小时前
SpringBoot项目打包为jar包
spring boot
呆萌很4 小时前
SpringBoot 热部署
spring boot
ErizJ6 小时前
Redis|持久化
数据库·redis·持久化·aof·rdb
天上掉下来个程小白8 小时前
登录-10.Filter-登录校验过滤器
spring boot·后端·spring·filter·登录校验
找了一圈尾巴9 小时前
自定义Spring Boot Starter(官网文档解读)
java·spring boot