SpringBoot 2 Redis Lettuce

CreatedAt: 20200820
SpringBoot Version: 2.3.1.RELEASE

springboot 可以自动装配 redis 相关配置, 其入口被定义在 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 默认支持的配置都在 org.springframework.boot.autoconfigure.data.redis.RedisProperties 中, 存在如下问题

  1. 只支持一套 redis 服务, 同时配置 集群 和 哨兵 时, 默认哨兵优先, 然后是集群, 然后是单例(standalone)
  2. 很多细节并不支持, 如 lettuce 客户端集群模式的拓扑结构自适应刷新, 默认是关闭的, 如果节点宕机或新增节点, 客户端不会主动刷新, 而是一直尝试连接宕机的节点

可以屏蔽 springboot 对 redis 的自动装配, 完全手动配置

说明

配置案例中有一些我不是很理解, 肯定有些配置是不太合适的, 在生产上使用可能会出问题

关于 Lettuce 使用 Pool 的一些说法

Jedis 需要配置连接池是毫无疑问的, 但是 Lettuce 呢? 网上很多例子都是有池配置的, 但是 Lettuce 官网有一些描述如下

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 keySerializer() {
        return StringRedisSerializer.UTF_8;
    }

    @Bean
    public RedisSerializer valueSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

    @Bean
    public LettuceConnectionFactory clusterLettuceConnectionFactory() {
        // RedisClusterConfiguration
        Set 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}.
        // 开启自适应刷新和定时刷新(定时刷新我感觉没必要). 如自适应刷新不开启, Redis集群拓扑结构变更时将会导致连接异常
        ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                // 开启自适应刷新
                // .enableAdaptiveRefreshTrigger(ClusterTopologyRefreshOptions.RefreshTrigger.MOVED_REDIRECT, ClusterTopologyRefreshOptions.RefreshTrigger.PERSISTENT_RECONNECTS)
                // 开启所有自适应刷新, MOVED_REDIRECT,ASK_REDIRECT,PERSISTENT_RECONNECTS,UNCOVERED_SLOT,UNKNOWN_NODE 都会触发
                // 本地提前缓存好了节点与插槽的印射关系,执行命令时先计算出key对应的插槽,即可知道存储该key的节点,直接向对应节点发送命令,避免了redis集群做moved操作,可提升效率
                // 但是如果集群拓扑结构发生了变化(如新增了节点),本地缓存的节点与插槽的关系会不准确,命令执行时可能发生moved,这时候就会触发拓扑结构刷新操作
                .enableAllAdaptiveRefreshTriggers()
                // 自适应刷新超时时间(默认30秒)
                // .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30))
                // 开启周期刷新, 默认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)
                // 默认就是重连的
                // .autoReconnect()
                // .maxRedirects(5)
                // Accept commands when auto-reconnect is enabled, reject commands when auto-reconnect is disabled
                .disconnectedBehavior(ClientOptions.DisconnectedBehavior.DEFAULT)
                // 取消校验集群节点的成员关系, 默认是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 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 redisTemplate(LettuceConnectionFactory clusterLettuceConnectionFactory,
                                                       RedisSerializer keySerializer,
                                                       RedisSerializer valueSerializer) {
        return generateRedisTemplate(clusterLettuceConnectionFactory, keySerializer, valueSerializer);
    }

    @Bean("sentinelRedisTemplate")
    public RedisTemplate sentinelRedisTemplate(LettuceConnectionFactory sentinelLettuceConnectionFactory,
                                                               RedisSerializer keySerializer,
                                                               RedisSerializer valueSerializer) {
        return generateRedisTemplate(sentinelLettuceConnectionFactory, keySerializer, valueSerializer);
    }

    private RedisTemplate generateRedisTemplate(LettuceConnectionFactory lettuceConnectionFactory,
                                                                RedisSerializer keySerializer,
                                                                RedisSerializer valueSerializer) {
        RedisTemplate 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, 指定默认使用的bean, 在配置多个相同类型bean的时候使用
     */
    @Bean
    @Primary
    public CacheManager redisClusterCacheManager(RedisConnectionFactory clusterLettuceConnectionFactory,
                                                 RedisSerializer keySerializer,
                                                 RedisSerializer valueSerializer) {
        // RedisCacheConfiguration commonRedisCacheConfiguration
        RedisCacheConfiguration commonRedisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .disableKeyPrefix()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer));
                // 不缓存null值(有时候需要缓存,交给使用者来决定)
                // .disableCachingNullValues();
        // CacheConfigurationsMap
        Map 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)
                // 将缓存的操作纳入到事务管理中,即回滚事务会同步回滚缓存(我猜的)
                .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

你可能感兴趣的:(SpringBoot 2 Redis Lettuce)