为了提高查询效率,会使用读写分离的方案。主库负责写操作,从库负责读取操作并且为只读属性。使用一主两从的拓扑关系讲述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;
}
}