原创

Spring Boot 2.缓存框架(三):自定义缓存管理器-Redis CacheManager

在Spring Boot中通过 @EnableCaching 注解自动化配置合适的缓存管理器(CacheManager).

Spring Boot根据下面的顺序去侦测缓存提供者:

  • Generic
  • JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
  • EhCache 2.x
  • Hazelcast
  • Infinispan
  • Couchbase
  • Redis
  • Caffeine
  • Simple (默认使用ConcurrentHashMap)

除了按顺序侦测外,我们也可以通过配置属性 spring.cache.type 来强制指定。
我们也可以通过debug调试查看cacheManager对象的实例来判断当前使用了什么缓存。

当我们不指定具体其他第三方实现的时候,Spring Boot的Cache模块会默认使用ConcurrentHashMap来存储

而实际生产使用的时候,因为我们可能需要更多其他特性,往往就会采用其他缓存框架:

  • EhCache: 直接在JVM虚拟机中缓存(换句话说就是,它与程序是绑在一起的,程序活着,它就活着),速度快,效率高;但是缓存共享麻烦,集群分布式应用不方便。
  • Redis : 通过socket访问到缓存服务,效率比ecache低,比数据库要快很多,处理集群和分布式缓存方便,有成熟的方案。属于独立的运行程序,需要单独安装后,使用JAVA中的Jedis来操纵。

由于EhCache是进程内的缓存框架,在集群模式下时,各应用服务器之间的缓存都是独立的,因此在不同服务器的进程间会存在缓存不一致的情况。即使EhCache提供了集群环境下的缓存同步策略,但是同步依然是需要一定的时间,短暂的缓存不一致依然存在。

本篇文章主要介绍 Redis 集中式缓存的实现方式。

程序要素

前文 中,数据实体需额外指定无参构造函数(注解 @NoArgsConstructor) ,以及实现序列化接口 Serializable。

@Data
@NoArgsConstructor
public class AgentAccountEntry  implements Serializable {
    ...
}

其他保持不变

改造步骤

下面开始改造。

第一步:pom.xml中增加相关依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

在Spring Boot 1.x 的早期版本中,该依赖的名称为spring-boot-starter-redis。

第二步:配置文件/配置中心中增加配置信息

此处分为两种方式,均可实现。

  • auto load 方式配置。 由springboot redis组件自动实现初始化。
  • Java Config 方式配置。此方式更为灵活,可以注解结合 API 的形式提供缓存服务。但此方式配置较为繁琐。个人推荐这种方式。

配置实现

auto load 方式配置

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.shutdown-timeout=100ms

关于连接池的配置,注意几点:
Redis的连接池配置在1.x版本中前缀为 spring.redis.pool , 与Spring Boot 2.x有所不同。
在1.x版本中采用jedis作为连接池,而在2.x版本中采用了lettuce作为连接池
以上配置值不作为生产配置依据。

Java Config 方式配置

定义配置实体类 RedisConfigData。

约定 redis的所有配置信息以 redis.client 开头。(按需自行更改)

@ConfigurationProperties(prefix = "redis.client")
@ToString
public class RedisConfigData {
    /**
     * 配置模式
     *
     * cluster : 集群模式
     * sentinels : 主从模式
     * standalone: 标准单机模式(阿里云集群模式也使用此方式)。 默认
     */
    @Getter
    @Setter
    @Maskble
    private String type = "standalone";

    /**
     * 服务器地址。
     *
     * 单机模式直接为 ip.
     * 集群模式则为 ip:port, ip:port
     */
    @Getter
    @Setter
    private String hostName;

    @Getter
    @Setter
    private Integer port;

    @Getter
    @Setter
    @Maskble
    private String password;

    @Getter
    @Setter
    private int connectionTimeout;

    @Getter
    @Setter
    private int connectionPoolSize;

    @Getter
    @Setter
    private boolean enableTransaction;

    @Getter
    @Setter
    private int commandTimeout;

    @Getter
    @Setter
    private int shutdownTimeout;

    @Getter
    @Setter
    private Integer cacheTtlSecond = 200;

    @Getter
    @Setter
    private Config config;

    @Data
    public static class Config {
        private int db;

        private String platform;

        private int maxIdle;

        private int maxTotal;

        private int minIdle;

        /**
         * 表示idle object evitor两次扫描之间要sleep的毫秒数
         */
        private int timeBetweenEvictionRunsMillis;

        /**
         * 表示一个对象至少停留在idle状态的最短时间,然后才能被idle object evitor扫描并驱逐
         * 这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义
         */
        private int minEvictableIdleTimeMillis;

        private boolean testOnBorrow;

        private boolean testOnReturn;
    }
}

Redis 组件初始化RedisConfiguration。

包括 JedisPoolConfig、RedisConnectionFactory(此处采用Jedis实现,可以按需变为Lettuce实现 )

@Configuration
@AutoConfigureAfter(value= ApolloConfigration.class)
@ConditionalOnClass(RedisOperations.class)
@Slf4j
public class RedisConfiguration implements InitializingBean {

    @Autowired
    private RedisConfigData redisConfigData;

    /**
     * jedis 的公共 poolConfig
     */
    @Bean(name="jedisPoolConfig")
    public JedisPoolConfig jedisPoolConfig() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(redisConfigData.getConfig().getMaxTotal());
        jedisPoolConfig.setMaxIdle(redisConfigData.getConfig().getMaxIdle());
        jedisPoolConfig.setMinIdle(redisConfigData.getConfig().getMinIdle());

        jedisPoolConfig.setTimeBetweenEvictionRunsMillis(redisConfigData.getConfig().getTimeBetweenEvictionRunsMillis());
        jedisPoolConfig.setMinEvictableIdleTimeMillis(redisConfigData.getConfig().getMinEvictableIdleTimeMillis());

        jedisPoolConfig.setTestOnBorrow(redisConfigData.getConfig().isTestOnBorrow());
        jedisPoolConfig.setTestOnReturn(redisConfigData.getConfig().isTestOnReturn());

        return jedisPoolConfig;
    }

    @Bean
    @ConditionalOnMissingBean(RedisConnectionFactory.class)
    public RedisConnectionFactory redisConnectionFactory(@Qualifier("jedisPoolConfig") JedisPoolConfig jedisPoolConfig,
                                                         ClientResources clientResources) {
        Duration commandTimeout = Duration.ofMillis(redisConfigData.getCommandTimeout());
        Duration shutdownTimeout = Duration.ofMillis(redisConfigData.getShutdownTimeout());

        /*
         * JedisClientConfiguration
         */
        JedisClientConfiguration clientConfiguration = JedisClientConfiguration.builder()
                .readTimeout(Duration.ofMillis(redisConfigData.getConnectionTimeout()))
                .connectTimeout(Duration.ofMillis(redisConfigData.getConnectionTimeout()))
                .usePooling()
                .poolConfig(jedisPoolConfig)
                .build();
        /*
         * LettuceClientConfiguration
         */
//        LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder()
//                .poolConfig(jedisPoolConfig)
//                .commandTimeout(commandTimeout)
//                .shutdownTimeout(shutdownTimeout)
//                .clientResources(clientResources);
//        LettuceClientConfiguration clientConfiguration = builder.build();

        RedisPassword password = RedisPassword.of(redisConfigData.getPassword());

        RedisConnectionFactory connectionFactory = null;
        // 集群模式
        if (StringUtils.equals(redisConfigData.getType(), "cluster")) {
            /* 集群模式的 JedisConnectionFactory 配置  */
            Collection<String> clusterNodes = Lists.newArrayList(Splitter.on(",").split(redisConfigData.getHostName()));
            RedisClusterConfiguration configuration = new RedisClusterConfiguration(clusterNodes);
            configuration.setMaxRedirects(3);
            configuration.setPassword(password);
            connectionFactory = new JedisConnectionFactory(configuration, clientConfiguration);
//            connectionFactory = new LettuceConnectionFactory(configuration, clientConfiguration);

        // 主从模式
        } else if (StringUtils.equals(redisConfigData.getType(), "sentinels")) {
            Set<String> set = Sets.newHashSet(sentinels.split(","));
            RedisSentinelConfiguration configuration = new RedisSentinelConfiguration(master, set);
            configuration.setPassword(password);
            connectionFactory = new LettuceConnectionFactory(configuration, lettucePoolingClientConfiguration);

        } else {
            /* 单机模式 */
            RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration(redisConfigData.getHostName(), redisConfigData.getPort());
            standaloneConfiguration.setPassword(password);
            standaloneConfiguration.setDatabase(redisConfigData.getConfig().getDb());

            connectionFactory = new JedisConnectionFactory(standaloneConfiguration, clientConfiguration);
//            connectionFactory = new LettuceConnectionFactory(standaloneConfiguration, clientConfiguration);
        }

        return connectionFactory;
    }
}

缓存初始化CacheConfiguration。

包括自定义的 基于Redis 的CacheManager, 自定义序列化方式、自定义缓存key等。

@Configuration
@ConditionalOnClass({CacheProperties.Redis.class, RedisCacheConfiguration.class})
public class CacheConfiguration {
    @Autowired
    private RedisConfigData redisConfigData;

    @Bean(destroyMethod = "shutdown")
    @ConditionalOnMissingBean(ClientResources.class)
    public ClientResources clientResources() {
        return DefaultClientResources.create();
    }

    @Bean
    @ConditionalOnMissingBean(CacheManager.class)
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = getJackson2JsonRedisSerializer();

        RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .entryTtl(Duration.ofSeconds(redisConfigData.getCacheTtlSecond()))
                // key前缀
                .computePrefixWith(cacheKeyPrefix())
                ;

        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager
                .builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
//                .builder(redisConnectionFactory)
                .cacheDefaults(configuration);

        if (new Boolean(redisConfigData.isEnableTransaction())) {
            builder.transactionAware();
        }

        RedisCacheManager cacheManager = builder.build();

        return cacheManager;
    }

    @Bean
    public CacheKeyPrefix cacheKeyPrefix() {
        return new CacheKeyPrefix() {
            @Override
            public String compute(String cacheName) {

                StringBuilder sb = new StringBuilder(100);
                sb.append(CacheKeyGeneratorConfiguration.prefix);
                sb.append(cacheName);
                sb.append(":");

                return sb.toString();
            }
        };
    }

    /**
     * 自定义序列化
     *
     * @return
     */
    private Jackson2JsonRedisSerializer getJackson2JsonRedisSerializer() {
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(
                Object.class);
        ObjectMapper mapper = new ObjectMapper();

        mapper.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        mapper.setSerializationInclusion(Include.NON_NULL);
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        jackson2JsonRedisSerializer.setObjectMapper(mapper);

        return jackson2JsonRedisSerializer;
    }
}

使用各注解,开始缓存之旅

Spring Boot 2.缓存框架(二):Cache注解详解

~ end

正文到此结束
广告是为了更好的提供数据服务
本文目录