CreatedAt: 20200820
SpringBoot Version: 2.3.1.RELEASE
springboot 可以自動裝配 redis 相關(guān)配置, 其入口被定義在 org.springframework.boot:spring-boot-autoconfigure:2.3.1.RELEASE 包中 /METE-INF/spring.factories 文件中, redis 配置
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
spring-data-redis 默認(rèn)支持的配置都在 org.springframework.boot.autoconfigure.data.redis.RedisProperties 中, 存在如下問題
- 只支持一套 redis 服務(wù), 同時配置 集群 和 哨兵 時, 默認(rèn)哨兵優(yōu)先, 然后是集群, 然后是單例(standalone)
- 很多細節(jié)并不支持, 如 lettuce 客戶端集群模式的拓?fù)浣Y(jié)構(gòu)自適應(yīng)刷新, 默認(rèn)是關(guān)閉的, 如果節(jié)點宕機或新增節(jié)點, 客戶端不會主動刷新, 而是一直嘗試連接宕機的節(jié)點
可以屏蔽 springboot 對 redis 的自動裝配, 完全手動配置
說明
配置案例中有一些我不是很理解, 肯定有些配置是不太合適的, 在生產(chǎn)上使用可能會出問題
關(guān)于 Lettuce 使用 Pool 的一些說法
Jedis 需要配置連接池是毫無疑問的, 但是 Lettuce 呢? 網(wǎng)上很多例子都是有池配置的, 但是 Lettuce 官網(wǎng)有一些描述如下
https://lettuce.io/core/release/reference/index.html#_connection_pooling
7.10. Connection Pooling
Lettuce connections are designed to be thread-safe so one connection can be shared amongst multiple threads and Lettuce connections auto-reconnection by default. While connection pooling is not necessary in most cases it can be helpful in certain use cases. Lettuce provides generic connection pooling support.
7.10.1. Is connection pooling necessary?
Lettuce is thread-safe by design which is sufficient for most cases. All Redis user operations are executed single-threaded. Using multiple connections does not impact the performance of an application in a positive way. The use of blocking operations usually goes hand in hand with worker threads that get their dedicated connection. The use of Redis Transactions is the typical use case for dynamic connection pooling as the number of threads requiring a dedicated connection tends to be dynamic. That said, the requirement for dynamic connection pooling is limited. Connection pooling always comes with a cost of complexity and maintenance.
Application
package com.mrathena;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author mrathena on 2019/7/27 16:00
*/
@EnableDubbo
@EnableCaching
@EnableScheduling
@SpringBootApplication(exclude = {
RedisAutoConfiguration.class,
RedisReactiveAutoConfiguration.class,
RedisRepositoriesAutoConfiguration.class
})
@MapperScan("com.mrathena.dao.mapper")
public class Application extends SpringBootServletInitializer {
/**
* war包部署的話,需要這個配置
*/
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
RedisConfig
package com.mrathena.web.configuration;
import com.mrathena.common.constant.Constant;
import io.lettuce.core.ClientOptions;
import io.lettuce.core.ReadFrom;
import io.lettuce.core.SocketOptions;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author mrathena on 2019-10-17 00:28
*/
@Slf4j
@Configuration
public class RedisConfig {
@Value("${spring.redis.cluster.nodes}")
private String clusterNodes;
@Value("${spring.redis.sentinel.master}")
private String sentinelMaster;
@Value("${spring.redis.sentinel.nodes}")
private String sentinelNodes;
@Bean
public RedisSerializer<String> keySerializer() {
return StringRedisSerializer.UTF_8;
}
@Bean
public RedisSerializer<Object> valueSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
@Bean
public LettuceConnectionFactory clusterLettuceConnectionFactory() {
// RedisClusterConfiguration
Set<String> clusterSet = Arrays.stream(clusterNodes.split(Constant.COMMA)).collect(Collectors.toSet());
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(clusterSet);
redisClusterConfiguration.setMaxRedirects(5);
// ClusterTopologyRefreshOptions - Options to control the Cluster topology refreshing of {@link RedisClusterClient}.
// 開啟自適應(yīng)刷新和定時刷新(定時刷新我感覺沒必要). 如自適應(yīng)刷新不開啟, Redis集群拓?fù)浣Y(jié)構(gòu)變更時將會導(dǎo)致連接異常
ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
// 開啟自適應(yīng)刷新
// .enableAdaptiveRefreshTrigger(ClusterTopologyRefreshOptions.RefreshTrigger.MOVED_REDIRECT, ClusterTopologyRefreshOptions.RefreshTrigger.PERSISTENT_RECONNECTS)
// 開啟所有自適應(yīng)刷新, MOVED_REDIRECT,ASK_REDIRECT,PERSISTENT_RECONNECTS,UNCOVERED_SLOT,UNKNOWN_NODE 都會觸發(fā)
// 本地提前緩存好了節(jié)點與插槽的印射關(guān)系,執(zhí)行命令時先計算出key對應(yīng)的插槽,即可知道存儲該key的節(jié)點,直接向?qū)?yīng)節(jié)點發(fā)送命令,避免了redis集群做moved操作,可提升效率
// 但是如果集群拓?fù)浣Y(jié)構(gòu)發(fā)生了變化(如新增了節(jié)點),本地緩存的節(jié)點與插槽的關(guān)系會不準(zhǔn)確,命令執(zhí)行時可能發(fā)生moved,這時候就會觸發(fā)拓?fù)浣Y(jié)構(gòu)刷新操作
.enableAllAdaptiveRefreshTriggers()
// 自適應(yīng)刷新超時時間(默認(rèn)30秒)
// .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30))
// 開啟周期刷新, 默認(rèn)60秒
// .enablePeriodicRefresh()
// .enablePeriodicRefresh(Duration.ofHours(1))
.build();
// SocketOptions - Options to configure low-level socket options for the connections kept to Redis servers.
SocketOptions socketOptions = SocketOptions.builder()
.keepAlive(true)
.tcpNoDelay(true)
.build();
// ClusterClientOptions - Client Options to control the behavior of {@link RedisClusterClient}.
ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
.topologyRefreshOptions(clusterTopologyRefreshOptions)
.socketOptions(socketOptions)
// 默認(rèn)就是重連的
// .autoReconnect()
// .maxRedirects(5)
// Accept commands when auto-reconnect is enabled, reject commands when auto-reconnect is disabled
.disconnectedBehavior(ClientOptions.DisconnectedBehavior.DEFAULT)
// 取消校驗集群節(jié)點的成員關(guān)系, 默認(rèn)是true, 需要校驗
.validateClusterNodeMembership(false)
.build();
// LettucePoolingClientConfiguration - Redis client configuration for lettuce using a driver level pooled connection by adding pooling specific configuration to {@link LettuceClientConfiguration}
LettucePoolingClientConfiguration lettucePoolingClientConfiguration = LettucePoolingClientConfiguration.builder()
.poolConfig(getGenericObjectPoolConfig())
.clientOptions(clusterClientOptions)
.readFrom(ReadFrom.REPLICA_PREFERRED)
.commandTimeout(Duration.ofMillis(100))
.build();
// LettuceConnectionFactory
return new LettuceConnectionFactory(redisClusterConfiguration, lettucePoolingClientConfiguration);
}
@Bean
public LettuceConnectionFactory sentinelLettuceConnectionFactory() {
// RedisSentinelConfiguration
Set<String> sentinelSet = Arrays.stream(sentinelNodes.split(Constant.COMMA)).collect(Collectors.toSet());
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(sentinelMaster, sentinelSet);
// LettucePoolingClientConfiguration
LettucePoolingClientConfiguration lettucePoolingClientConfiguration = LettucePoolingClientConfiguration.builder()
.poolConfig(getGenericObjectPoolConfig())
.commandTimeout(Duration.ofMillis(100))
.build();
// LettuceConnectionFactory
return new LettuceConnectionFactory(redisSentinelConfiguration, lettucePoolingClientConfiguration);
}
private GenericObjectPoolConfig<?> getGenericObjectPoolConfig() {
GenericObjectPoolConfig<?> genericObjectPoolConfig = new GenericObjectPoolConfig<>();
genericObjectPoolConfig.setMaxTotal(8);
genericObjectPoolConfig.setMaxIdle(8);
genericObjectPoolConfig.setMinIdle(0);
genericObjectPoolConfig.setMaxWaitMillis(1000);
genericObjectPoolConfig.setTestOnCreate(true);
genericObjectPoolConfig.setTestOnBorrow(false);
genericObjectPoolConfig.setTestOnReturn(false);
genericObjectPoolConfig.setTestWhileIdle(true);
genericObjectPoolConfig.setBlockWhenExhausted(false);
return genericObjectPoolConfig;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory clusterLettuceConnectionFactory,
RedisSerializer<String> keySerializer,
RedisSerializer<Object> valueSerializer) {
return generateRedisTemplate(clusterLettuceConnectionFactory, keySerializer, valueSerializer);
}
@Bean("sentinelRedisTemplate")
public RedisTemplate<String, Object> sentinelRedisTemplate(LettuceConnectionFactory sentinelLettuceConnectionFactory,
RedisSerializer<String> keySerializer,
RedisSerializer<Object> valueSerializer) {
return generateRedisTemplate(sentinelLettuceConnectionFactory, keySerializer, valueSerializer);
}
private RedisTemplate<String, Object> generateRedisTemplate(LettuceConnectionFactory lettuceConnectionFactory,
RedisSerializer<String> keySerializer,
RedisSerializer<Object> valueSerializer) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setValueSerializer(valueSerializer);
redisTemplate.setHashKeySerializer(keySerializer);
redisTemplate.setHashValueSerializer(valueSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public RedissonClient redissonClient() {
String[] redisClusterNodeArray = clusterNodes.split(Constant.COMMA);
for (int i = 0; i < redisClusterNodeArray.length; i++) {
redisClusterNodeArray[i] = "redis://".concat(redisClusterNodeArray[i]);
}
Config config = new Config();
config.useClusterServers().addNodeAddress(redisClusterNodeArray).setScanInterval(1000 * 60 * 60);
return Redisson.create(config);
}
}
CacheConfig
package com.mrathena.web.configuration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* @author mrathena on 2019/12/10 15:45
*/
@Slf4j
@Configuration
public class CacheConfig {
/**
* 注解 @Primary, 指定默認(rèn)使用的bean, 在配置多個相同類型bean的時候使用
*/
@Bean
@Primary
public CacheManager redisClusterCacheManager(RedisConnectionFactory clusterLettuceConnectionFactory,
RedisSerializer<String> keySerializer,
RedisSerializer<Object> valueSerializer) {
// RedisCacheConfiguration commonRedisCacheConfiguration
RedisCacheConfiguration commonRedisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.disableKeyPrefix()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer));
// 不緩存null值(有時候需要緩存,交給使用者來決定)
// .disableCachingNullValues();
// CacheConfigurationsMap
Map<String, RedisCacheConfiguration> cacheConfigurationMap = new HashMap<>(8);
cacheConfigurationMap.put(CacheNameEnum.ONE_MINUTE.name(), commonRedisCacheConfiguration.entryTtl(Duration.ofMinutes(1)));
cacheConfigurationMap.put(CacheNameEnum.FIVE_MINUTE.name(), commonRedisCacheConfiguration.entryTtl(Duration.ofMinutes(5)));
cacheConfigurationMap.put(CacheNameEnum.TEN_MINUTE.name(), commonRedisCacheConfiguration.entryTtl(Duration.ofMinutes(10)));
cacheConfigurationMap.put(CacheNameEnum.THIRTY_MINUTE.name(), commonRedisCacheConfiguration.entryTtl(Duration.ofMinutes(30)));
cacheConfigurationMap.put(CacheNameEnum.ONE_HOUR.name(), commonRedisCacheConfiguration.entryTtl(Duration.ofHours(1)));
cacheConfigurationMap.put(CacheNameEnum.SIX_HOUR.name(), commonRedisCacheConfiguration.entryTtl(Duration.ofHours(6)));
cacheConfigurationMap.put(CacheNameEnum.TWELVE_HOUR.name(), commonRedisCacheConfiguration.entryTtl(Duration.ofHours(12)));
cacheConfigurationMap.put(CacheNameEnum.ONE_DAY.name(), commonRedisCacheConfiguration.entryTtl(Duration.ofDays(1)));
cacheConfigurationMap.put(CacheNameEnum.SEVEN_DAY.name(), commonRedisCacheConfiguration.entryTtl(Duration.ofDays(7)));
cacheConfigurationMap.put(CacheNameEnum.FOURTEEN_DAY.name(), commonRedisCacheConfiguration.entryTtl(Duration.ofDays(14)));
cacheConfigurationMap.put(CacheNameEnum.ONE_MONTH.name(), commonRedisCacheConfiguration.entryTtl(Duration.ofDays(31)));
cacheConfigurationMap.put(CacheNameEnum.THREE_MONTH.name(), commonRedisCacheConfiguration.entryTtl(Duration.ofDays(93)));
cacheConfigurationMap.put(CacheNameEnum.SIX_MONTH.name(), commonRedisCacheConfiguration.entryTtl(Duration.ofDays(182)));
cacheConfigurationMap.put(CacheNameEnum.ONE_YEAR.name(), commonRedisCacheConfiguration.entryTtl(Duration.ofDays(366)));
cacheConfigurationMap.put(CacheNameEnum.FOREVER.name(), commonRedisCacheConfiguration.entryTtl(Duration.ZERO));
// RedisCacheManager
RedisCacheManager redisCacheManager = RedisCacheManager.builder(clusterLettuceConnectionFactory)
.withInitialCacheConfigurations(cacheConfigurationMap)
// 將緩存的操作納入到事務(wù)管理中,即回滾事務(wù)會同步回滾緩存(我猜的)
.transactionAware()
// 不允許添加除上述定義之外的緩存名稱
.disableCreateOnMissingCache()
.build();
log.info("Cache:redisClusterCacheManager:初始化完成");
return redisCacheManager;
}
public enum CacheNameEnum {
/**
* 緩存時間
*/
ONE_MINUTE, FIVE_MINUTE, TEN_MINUTE, THIRTY_MINUTE,
ONE_HOUR, SIX_HOUR, TWELVE_HOUR,
ONE_DAY, SEVEN_DAY, FOURTEEN_DAY,
ONE_MONTH, THREE_MONTH, SIX_MONTH,
ONE_YEAR,
FOREVER
}
}
參考
https://wenchao.ren/2020/06/Lettuce%E4%B8%80%E5%AE%9A%E8%A6%81%E6%89%93%E5%BC%80redis%E9%9B%86%E7%BE%A4%E6%8B%93%E6%89%91%E5%88%B7%E6%96%B0%E5%8A%9F%E8%83%BD/
https://www.cnblogs.com/gavincoder/p/12731833.html
https://juejin.im/post/6844904039096778759
https://blog.csdn.net/ankeway/article/details/100136675