掘金 后端 ( ) • 2024-07-02 09:40

theme: vue-pro highlight: atom-one-dark

背景

开发自用工程,清理线上redis集群内指定工程缓存。遂搜索,发现工具 fastdep

使用方式

标准配置文件

fastdep:
  redis:
    redis1: #连接名称 product
      database: 0
      host: redis1
      port: 6379
      password: xxx
      lettuce: #下面为连接池的补充设置
        shutdown-timeout: 100 # 关闭超时时间
        pool:
          max-active: 10 # 连接池最大连接数(使用负值表示没有限制)
          max-idle: 3 # 连接池中的最大空闲连接
          max-wait: 30 # 连接池最大阻塞等待时间(使用负值表示没有限制)
          min-idle: 0 # 连接池中的最小空闲连接
    redis2: #连接名称
      host: reids2
      port: 6379
      database: 0
      password: xxxx
      lettuce: #下面为连接池的补充设置
        shutdown-timeout: 100 # 关闭超时时间
        pool:
          max-active: 10 # 连接池最大连接数(使用负值表示没有限制)
          max-idle: 3 # 连接池中的最大空闲连接
          max-wait: 30 # 连接池最大阻塞等待时间(使用负值表示没有限制)
          min-idle: 0 # 连接池中的最小空闲连接

工具类

@Component
public class RedisUtil {

    @Autowired
    private StringRedisTemplate redis1StringRedisTemplate;
    @Autowired
    private StringRedisTemplate redis2StringRedisTemplate;
    @Autowired
    private StringRedisTemplate redis3StringRedisTemplate;

    @Autowired
    private RedisTemplate redis2RedisTemplate;
    @Autowired
    private RedisTemplate redis1RedisTemplate;
    @Autowired
    private RedisTemplate redis3RedisTemplate;

    public RedisTemplate redisTemplate(String name) {
        RedisTemplate redisTemplate;

        switch (name) {
            case "redis2":
                redisTemplate = redis2RedisTemplate;
                break;
            case "redis3":
                redisTemplate = redis3RedisTemplate;
                break;
            default:
                redisTemplate = redis1RedisTemplate;
                break;
        }

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(stringRedisSerializer);
        return redisTemplate;

    }

    public StringRedisTemplate stringRedisTemplate(String name) {
        StringRedisTemplate stringRedisTemplate;
        switch (name) {
            case "redis2":
                stringRedisTemplate = redis2StringRedisTemplate;
                break;
            case "redis3":
                stringRedisTemplate = redis3StringRedisTemplate;
                break;
            default:
                stringRedisTemplate = redis1StringRedisTemplate;
                break;
        }
        stringRedisTemplate.setEnableTransactionSupport(true);
        return stringRedisTemplate;
    }

}

原理介绍

image.png

回顾下Spring Boot自动配置流程

  1. 创建一个 Spring Boot 应用,并在主类上使用 @SpringBootApplication 注解。
  2. @SpringBootApplication包含 @EnableAutoConfiguration,告诉 Spring Boot 启用自动配置。
  3. Spring Boot 在启动时查找 META-INF/spring.factories 文件,并加载所有列出的自动配置类。
  4. 自动配置类中的条件注解 (@Conditional ...)被评估,条件满足相关的 bean 被创建。
  5. 可以提供了自己的配置,覆盖自动配置类。
  6. 程序启动,所有配置好的 bean 可以被使用。

spring.factories 定义自动配置类

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.louislivi.fastdep.redis.FastDepRedisAutoConfiguration

FastDepRedisAutoConfiguration 自动配置类

// 表示这个类是一个配置类,包含了一个或多个 @Bean 方法,这些方法会被 Spring 容器处理,以生成 Spring 应用程序上下文中的 bean 定义
@Configuration
/**
 * 使用 @ConfigurationProperties 注解的类生效。
 * @EnableConfigurationProperties 相当于把使用 @ConfigurationProperties 的类进行了一次注入。
 * 可以将配置文件中的属性绑定到 FastDepRedis 类的实例上。
 */
@EnableConfigurationProperties({FastDepRedis.class})
// FastDepRedisAutoConfiguration 应该在 RedisAutoConfiguration 之前被加载。
// RedisAutoConfiguration 是 Spring Boot 提供的默认 Redis 自动配置类,确保 FastDepRedisAutoConfiguration 优先于默认配置执行,从而覆盖或自定义默认配置。
@AutoConfigureBefore({RedisAutoConfiguration.class})
// 注册 FastDepRedisRegister 类到 Spring 容器
@Import(FastDepRedisRegister.class)
public class FastDepRedisAutoConfiguration {
}

配置类

// 只有在 application.properties 或 application.yml 配置文件中存在 fastdep.redis 属性时,这个配置类才会被加载和解析
@ConditionalOnProperty("fastdep.redis")
// 告诉 Spring Boot 这个类的属性应该从配置文件中 fastdep 前缀下的部分读取
@ConfigurationProperties("fastdep")
public class FastDepRedis {
    /**
     * key-字符串,value-RedisProperties 对象。用于存储多个 Redis 实例的配置信息,其中每个实例都有一个唯一的名称作为键。
     */
    private Map<String, RedisProperties> redis;

    public Map<String, RedisProperties> getRedis() {
        return redis;
    }

    public void setRedis(Map<String, RedisProperties> redis) {
        this.redis = redis;
    }
}

自动配置并注册 Redis 数据源

public class FastDepRedisRegister implements EnvironmentAware, ImportBeanDefinitionRegistrar {

    /**
     * 日志记录器,用于记录日志信息。
     */
    private static final Logger logger = LoggerFactory.getLogger(com.louislivi.fastdep.redis.FastDepRedisRegister.class);
    /**
     * 存储已注册的 Bean 实例
     */
    private static Map<String, Object> registerBean = new ConcurrentHashMap<>();
    /**
     * 访问应用程序的配置属性
     */
    private Environment env;
    /**
     * 通常与 @ConfigurationProperties 注解(标记一个类的属性应该被绑定到配置属性上)一起使用,Binder 可以直接在代码中使用,运行时动态地绑定和解析配置属性。
     */
    private Binder binder;

    /**
     * 实现了 ImportBeanDefinitionRegistrar 接口中的 registerBeanDefinitions 方法,用于向 Spring 容器中注册与 Redis 相关的 Bean 定义
     *
     * @param annotationMetadata     annotationMetadata
     * @param beanDefinitionRegistry beanDefinitionRegistry
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        // get all redis config(从配置文件中读取所有的 Redis 配置信息)
        Map<String, Map> multipleRedis;
        try {
            multipleRedis = binder.bind("fastdep.redis", Map.class).get();
        } catch (NoSuchElementException e) {
            logger.error("Failed to configure fastDep redis: 'fastdep.redis' attribute is not specified and no embedded redis could be configured.");
            return;
        }
        boolean onPrimary = true;
        // 遍历这些配置信息,为每个 Redis 实例创建和配置 RedisStandaloneConfiguration、GenericObjectPoolConfig 和 LettuceConnectionFactory 对象。
        for (String key : multipleRedis.keySet()) {
            Map map = binder.bind("fastdep.redis." + key, Map.class).get();
            // RedisStandaloneConfiguration(Redis客户端配置信息)
            RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
            configuration.setHostName((String) map.get("host"));
            configuration.setPort((Integer) map.get("port"));
            configuration.setDatabase((Integer) map.get("database"));
            String password = (String) map.get("password");
            if (!StringUtils.isEmpty(password)) {
                RedisPassword redisPassword = RedisPassword.of(password);
                configuration.setPassword(redisPassword);
            }
            // GenericObjectPoolConfig(配置Lettuce或Jedis连接池信息)
            GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
            try {
                RedisProperties.Pool pool = binder.bind("fastdep.redis." + key + ".lettuce.pool", RedisProperties.Pool.class).get();
                genericObjectPoolConfig.setMaxIdle(pool.getMaxIdle());
                genericObjectPoolConfig.setMaxTotal(pool.getMaxActive());
                genericObjectPoolConfig.setMinIdle(pool.getMinIdle());
                if (pool.getMaxWait() != null) {
                    genericObjectPoolConfig.setMaxWaitMillis(pool.getMaxWait().toMillis());
                }
            } catch (NoSuchElementException ignore) {
            }
            // LettuceConnectionFactory(Lettuce 连接池的实现,提供了对 Redis 客户端的统一管理)
            Supplier<LettuceConnectionFactory> lettuceConnectionFactorySupplier = () -> {
                LettuceConnectionFactory factory = (LettuceConnectionFactory) registerBean.get(key + "LettuceConnectionFactory");
                if (factory != null) {
                    return factory;
                }
                LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder();
                try {
                    Duration shutdownTimeout = binder.bind("fastdep.redis." + key + ".shutdown-timeout", Duration.class).get();
                    if (shutdownTimeout != null) {
                        builder.shutdownTimeout(shutdownTimeout);
                    }
                } catch (NoSuchElementException ignore) {
                }
                LettuceClientConfiguration clientConfiguration = builder.poolConfig(genericObjectPoolConfig).build();
                factory = new LettuceConnectionFactory(configuration, clientConfiguration);
                registerBean.put(key + "LettuceConnectionFactory", factory);
                return factory;
            };
            LettuceConnectionFactory lettuceConnectionFactory = lettuceConnectionFactorySupplier.get();
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(LettuceConnectionFactory.class, lettuceConnectionFactorySupplier);
            
            // 使用 BeanDefinitionBuilder 创建 LettuceConnectionFactory、StringRedisTemplate 和 RedisTemplate 的 bean 定义,并将它们注册到 Spring 容器中。
            AbstractBeanDefinition factoryBean = builder.getRawBeanDefinition();
            factoryBean.setPrimary(onPrimary);
            beanDefinitionRegistry.registerBeanDefinition(key + "LettuceConnectionFactory", factoryBean);

            // 注册 StringRedisTemplate 和 RedisTemplate Bean:
            // StringRedisTemplate
            GenericBeanDefinition stringRedisTemplate = new GenericBeanDefinition();
            stringRedisTemplate.setBeanClass(StringRedisTemplate.class);
            ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
            constructorArgumentValues.addIndexedArgumentValue(0, lettuceConnectionFactory);
            stringRedisTemplate.setConstructorArgumentValues(constructorArgumentValues);
            stringRedisTemplate.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME);
            beanDefinitionRegistry.registerBeanDefinition(key + "StringRedisTemplate", stringRedisTemplate);
            
            // RedisTemplate
            GenericBeanDefinition redisTemplate = new GenericBeanDefinition();
            redisTemplate.setBeanClass(RedisTemplate.class);
            redisTemplate.getPropertyValues().add("connectionFactory", lettuceConnectionFactory);
            redisTemplate.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME);
            beanDefinitionRegistry.registerBeanDefinition(key + "RedisTemplate", redisTemplate);
            
            logger.info("Registration redis ({}) !", key);
            if (onPrimary) {
                onPrimary = false;
            }
        }
        logger.info("Registration redis completed !");
    }


    /**
     * 实现 EnvironmentAware 接口中的 setEnvironment 方法。
     * 接收 Environment 对象将其赋值给类的成员变量 env 并创建了一个 Binder 对象,用于后续的配置属性绑定
     */
    @Override
    public void setEnvironment(Environment environment) {
        this.env = environment;
        // bing binder
        binder = Binder.get(this.env);
    }
}

总结

主要原理包含:

  • 自动配置:利用 Spring Boot 自动配置特性,在类路径上检测到特定的依赖,自动配置相关的 beans。
  • 配置属性解析:读取 application.yaml 或 application.properties 文件中的配置属性,并根据这些属性创建配置 Redis 客户端实例。
  • Bean 的创建和注入:根据配置文件中定义的数据源,FastDepRedis 创建多个 RedisTemplate 或 StringRedisTemplate 实例,将它们注册为 Spring 应用程序上下文中的 beans。
  • 条件注解:使用 Spring 的条件注解(如 @ConditionalOnClass 和 @ConditionalOnProperty)确保只有在满足特定条件时才创建和配置 beans。