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<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}.
        // 开启自适应刷新和定时刷新(定时刷新我感觉没必要). 如自适应刷新不开启, 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<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, 指定默认使用的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)
                // 将缓存的操作纳入到事务管理中,即回滚事务会同步回滚缓存(我猜的)
                .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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352