RedisSentinel(哨兵模式)实现redis的读写分离

为了提高查询效率,会使用读写分离的方案。主库负责写操作,从库负责读取操作并且为只读属性。使用一主两从的拓扑关系讲述redis的读写分离方案,如图:

redis复制

redis的读写分离基于redis的复制机制实现,环境搭建的过程可以参考这位网友的介绍Redis集群主从复制(一主两从)搭建配置教程【Windows环境】。该机制存在读写分离的使用场景下有如下隐患:

  • 复制数据有延迟

master采用异步复制的方式把数据复制到slave从库。master生成RDB压缩镜像文件,然后发送给slave节点;slave节点成功接收RDB文件后,清理当前节点的数据,然后执行RDB文件录入数据;slave节点如果开启了AOF持久化方式,会生成AOF文件,如果文件比较大的话甚至会对AOF文件执行重写操作。所以,从库数据与主库数据不一致的情景。

  • 节点故障

master节点故障,写操作无法执行,这时需要人工介入手动切换master节点,从从节点中选择一个节点作为新的master节点。slave节点故障,连接到该节点的查询操作无法执行。当出现这两种节点故障时,客户端应用程序没有得到通知,就无法继续正常工作。

redis sentinel哨兵模式

redis使用Redis Sentinel哨兵模式实现了redis的高可用方案,环境搭建过程可以参考这位网友的介绍Redis哨兵(Sentinel)模式。一主二从的拓扑关系图如下:

该模式的主要功能如下:

  • 监控

每个Sentinel节点,都会监控所有redis数据节点,特别是master节点的监控,如果redis数据节点故障,就会sentinel集合就会马上进行故障转移。客户端应用程序是通过sentinel集合中的单个sentinel节点获取redis master主节点的配置配置信息。所以,sentinel节点也会监控Sentinel集合中其余的Sentiel节点,即使其中某个sentinel节点故障,也不会影响主从的高可用,提高了Sentinel节点的容错性。

  • 故障转移

Sentienl集合检测到master节点故障,会通过主观下线和客观下线机制,master选举,等一些列措施从slave节点中选举新的master节点,并完成新的主从模式的配置自动切换。

  • 消息通知

当redis数据节点出现故障时,Sentinel节点会通知客户端应用程序,master节点的地址切换。客户端应用程序通过master-name主节点名称(标识redis master主节点)和sentinel节点列表实现与redis数据节点的配置信息的获取,类似于服务注册于发现的方式。例如,客户端应用程序通过master-name主节点名称,获取主节点的配置信息。调用sentinel节点的sentinel get-master-addr-by-name master-name API就可获取对应主节点的相关信息。

spring data集成Redis Sentinel

spring只有Jedis和 lettuce 支持 Redis Sentinel。如果要实现sentinel哨兵模式,必须配置master 属性(主节点名称);nodes 属性(sentinel节点集合),对应的RedisSentinelConfiguration配置文件类主要属性master(redis主节点的名称),Set<RedisNode> sentinels(sentinel节点),代码如下:

public class RedisSentinelConfiguration implements RedisConfiguration, SentinelConfiguration {

    /**
     * redis master主节点
     */
	private @Nullable NamedNode master;
    /**
     * redis sentinel节点集合
     */
	private Set<RedisNode> sentinels;
	private int database;
	private RedisPassword password = RedisPassword.none();

    ......
}

可以通过properties或者yml文件设置RedisProperties属性,配置redis sentinel。代码如下:

spring:
  redis:
    sentinel:
      master: mymaster
      nodes: 192.168.11.128:26379,192.168.11.129:26379,192.168.11.130:26379
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

下面以LettuceConnectionConfiguration为例,介绍spring data的集成。根据LettuceConnectionConfiguration源码分析,如果配置中包含sential的配置,就会创建基于哨兵模式的LettuceConnectionFactory。源码如下:

class LettuceConnectionConfiguration extends RedisConnectionConfiguration {

	......

	@Bean
	@ConditionalOnMissingBean(RedisConnectionFactory.class)
	LettuceConnectionFactory redisConnectionFactory(
            // 用户自定义LettuceClient配置,例如从从库查询的配置 ReadFrom.REPLICA
			ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
			ClientResources clientResources) throws UnknownHostException {
		LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources,
				getProperties().getLettuce().getPool());
		return createLettuceConnectionFactory(clientConfig);
	}

	private LettuceConnectionFactory createLettuceConnectionFactory(LettuceClientConfiguration clientConfiguration) {

        // 包含sentinel配置则根据哨兵模式生成LettuceConnectionFactory 
		if (getSentinelConfig() != null) {
			return new LettuceConnectionFactory(getSentinelConfig(), clientConfiguration);
		}
		if (getClusterConfiguration() != null) {
			return new LettuceConnectionFactory(getClusterConfiguration(), clientConfiguration);
		}
		return new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration);
	}

	private LettuceClientConfiguration getLettuceClientConfiguration(
			ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
			ClientResources clientResources, Pool pool) {
		LettuceClientConfigurationBuilder builder = createBuilder(pool);
		applyProperties(builder);
		if (StringUtils.hasText(getProperties().getUrl())) {
			customizeConfigurationFromUrl(builder);
		}
		builder.clientResources(clientResources);
        
        // 加载用户自定义的LettuceClient配置
		builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
		return builder.build();
	}

	......


    // 设置对象缓冲池的配置
	private GenericObjectPoolConfig<?> getPoolConfig(Pool properties) {
			GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
			config.setMaxTotal(properties.getMaxActive());
			config.setMaxIdle(properties.getMaxIdle());
			config.setMinIdle(properties.getMinIdle());
			if (properties.getTimeBetweenEvictionRuns() != null) {
				config.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRuns().toMillis());
			}
			if (properties.getMaxWait() != null) {
				config.setMaxWaitMillis(properties.getMaxWait().toMillis());
			}
			return config;
		}

	}

}

LettuceClientConfigurationBuilderCustomizer接口用于用户自定义LettuceClient的配置,例如从从库查询的属性ReadFrom.REPLICA 配置,接口定义如下:

public interface LettuceClientConfigurationBuilderCustomizer {

	/**
	 * Customize the {@link LettuceClientConfigurationBuilder}.
	 * @param clientConfigurationBuilder the builder to customize
	 */
	void customize(LettuceClientConfigurationBuilder clientConfigurationBuilder);

}

在项目实践中,集成redis的配置,在pom中引入如下依赖:

        <!-- spring data redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 对象缓存池 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>${commons-pool2.version}</version>
        </dependency>

自定义redis配置从从库获取查询的基于哨兵模式的redis配置,key序列化为StringRedisSerializer,value的序列化为GenericFastJsonRedisSerializer,代码如下:

@Configuration
@Slf4j
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class CustomerRedisAutoConfiguration {

    /**
     * redis FastJson序列化
     */
    @Bean
    @ConditionalOnMissingBean(value = RedisSerializer.class)
    public RedisSerializer<Object> redisSerializer() {
        return new GenericFastJsonRedisSerializer();
    }

    /**
     * 配置从库读取策略
     */
    @Bean
    public LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer() {
        return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA);
    }

    /**
     * 设置自定义序列化的RedisTemplate
     *
     * @param redisConnectionFactory
     * @param redisSerializer        序列化
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(RedisTemplate.class)
    public RedisTemplate<String, Object> redisTemplate(@Autowired RedisConnectionFactory redisConnectionFactory,
                                                       @Autowired RedisSerializer<Object> redisSerializer) {

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        // value,hash value设置FastJson序列化
        template.setHashValueSerializer(redisSerializer);
        template.setValueSerializer(redisSerializer);
        // key,hash key使用String序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(Charset.forName("UTF-8"));
        template.setHashKeySerializer(stringRedisSerializer);
        template.setKeySerializer(stringRedisSerializer);
        return template;
    }
}
发布了37 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/new_com/article/details/104284256