记一次线上问题-Druid数据源配置失败

一、背景是这样的

        我们的服务是spring 服务。 数据库是mysql。     

        接到公司要求, mysql数据源配置(JDBCURL地址)

jdbc:mysql://IP:PORT/dbname?Unicode=true&characterEncoding=utf8&useSSL=false

        需要添加参数 allowMultiQueries=true。

        也就是变成

jdbc:mysql://IP:PORT/dbname?Unicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true

        配置是通过Apollo来动态实现的。

二、现象是这样的

        当修改了Apollo,点击发布以后。同时监控系统进行了报警。

这里说明:其实本身服务没有down ,甚至服务里面的数据库访问也都是正常的, 但是我们的网关配置了健康监测。 一旦出现服务里的某些中间件服务down  就会切掉这个endpoint。 所以出现了大量的 503 , out of service

        提示系统就出现了大量的 503错误。

        我们有5个服务进行了修改。 有两个服务出现了问题。 3个服务是没有问题的。

三、分析问题

        1、由于有3个服务是没有问题的, 所以第一反应不是因为修改Apollo导致的。

        2、怀疑是网关出了问题 。 检查发现网关是正常的。

        3、迅速查看了出问题的服务的日志。 出现了Error级的日志。如下

log: |2024-03-07 10:13:20.552|ERROR|||[Failed to invoke config change listener com.ctrip.framework.apollo.spring.annotation.ApolloAnnotationProcessor$1]-[c.c.f.a.internals.AbstractConfig.java:445]-[Apollo-Config-4]
org.springframework.boot.context.properties.ConfigurationPropertiesBindException: Error creating bean with name 'dataSource': Could not bind properties to 'DruidDataSourceWrapper' : prefix=spring.datasource.druid, ignoreInvalidFields=false, ignoreUnknownFields=true; nested exception is org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'spring.datasource.druid' to javax.sql.DataSource
	at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.bind(ConfigurationPropertiesBindingPostProcessor.java:110)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:93)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:416)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1686)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:407)
	at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.rebind(ConfigurationPropertiesRebinder.java:102)
	at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.rebind(ConfigurationPropertiesRebinder.java:84)
	at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.onApplicationEvent(ConfigurationPropertiesRebinder.java:128)
	at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.onApplicationEvent(ConfigurationPropertiesRebinder.java:50)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:400)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:354)Java

看到报错否定了第一个反应, 看来是由于这个数据源配置变化导致的。继续找到如下日志

        

Caused by: java.lang.IllegalStateException: Unable to set value for property url
	at org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanProperty.setValue(JavaBeanBinder.java:323)
	at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:79)
	at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:62)
	at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:54)
	at org.springframework.boot.context.properties.bind.Binder.lambda$null$5(Binder.java:341)
	at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
	at java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1359)
	at java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:126)
	at java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:499)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:486)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
	at java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:152)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:531)
	at org.springframework.boot.context.properties.bind.Binder.lambda$bindBean$6(Binder.java:342)
	at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:441)
	at org.springframework.boot.context.properties.bind.Binder$Context.withBean(Binder.java:427)
	at org.springframework.boot.context.properties.bind.Binder$Context.access$400(Binder.java:381)
	at org.springframework.boot.context.properties.bind.Binder.bindBean(Binder.java:339)
	at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:278)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:221)

正是提示我们刚修改的url set 失败。

4、下面检查了一下没有出问题的项目和出问题的项目配置之间的差距,发现

出问题的配置是:spring.datasource.druid.url = jdbc:mysql://***

没问题的配置是:spring.datasource.url = jdbc:mysql://***

那么锁定问题是druid在出现Apollo配置更新的时候出现了问题。

5、下一步就是赶紧重新启动服务。保证服务正常。 后续在测试环境复现问题,查找根本原因。

四、找到问题原因

1、测试环境进行了测试,没有发现问题。认真一看发现出问题的服务在测试并没有配置成 spring.datasource.druid.url 而是 spring.datasource.url。 所以, 在测试环境没有出问题。

2、在测试环境配置成druid的方式。进行测试。果然复现了问题。

过程是这样的

首先:Apollo的配置变化的Listener接收到变化事件。触发了更新Spring容器内Bean 的操作(公司自己的starter)。

代码如下:

  @ApolloConfigChangeListener
    public void onChange(ConfigChangeEvent changeEvent) {
        Set<String> changedKeys = changeEvent.changedKeys();
        if (null != changedKeys) {
            Iterator var3 = changedKeys.iterator();

            while(var3.hasNext()) {
                String key = (String)var3.next();
                ConfigChange change = changeEvent.getChange(key);
                log.info("apollo配置变更,[{}]发生[{}]变更:[{}]==>[{}]", new Object[]{key, change.getChangeType(), change.getOldValue(), change.getNewValue()});
            }
        }

        this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
        if (null != this.refreshScope) {
            this.refreshScope.refreshAll(); 
        }

    }

然后会走到Apollo代码里面,使用反射调用对应Bean的set 方法,对Apollo 配置变化的key进行重新设置。

  ReflectionUtils.makeAccessible(method);
    String[] namespaces = annotation.value();
    String[] annotatedInterestedKeys = annotation.interestedKeys();
    Set<String> interestedKeys = annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null;
    ConfigChangeListener configChangeListener = new ConfigChangeListener() {
      @Override
      public void onChange(ConfigChangeEvent changeEvent) {
        ReflectionUtils.invokeMethod(method, bean, changeEvent); //这一行就在日志异常中能看到了,也是从这里找到的原因
      }
    };

这里的 invokeMethod 会调用对应的set 方法。 这里对应的就是 DruidAbstractDataSource类里面的setUrl

 public void setUrl(String jdbcUrl) {
        if (StringUtils.equals(this.jdbcUrl, jdbcUrl)) {
            return;
        }

        if (inited) {
            throw new UnsupportedOperationException();
        }

        if (jdbcUrl != null) {
            jdbcUrl = jdbcUrl.trim();
        }

        this.jdbcUrl = jdbcUrl;

        // if (jdbcUrl.startsWith(ConfigFilter.URL_PREFIX)) {
        // this.filters.add(new ConfigFilter());
        // }
    }

在这里我们发现了         if (inited) 这个条件, 一旦出现true , 那么就会跑出异常。 这也就是根本原因。原来 Druid 在进行数据源配置的时候会判断当前是否已经进行了初始化 。 如果已经inited,就不允许在进行重新设置。 

查看了    if (inited) 这个条件 同时出现在很多其他的地方。比如设置数据库用户名、设置密码。都存在这个限制。 

3、进一步看一下, inited 在什么时候会发生改变。

在init 的时候,设置为true 。在restart 的时候会设置为false:

public void restart() throws SQLException {
        lock.lock();
        try {
            if (activeCount > 0) {
                throw new SQLException("can not restart, activeCount not zero. " + activeCount);
            }
            if (LOG.isInfoEnabled()) {
                LOG.info("{dataSource-" + this.getID() + "} restart");
            }

            this.close();
            this.resetStat();
            this.inited = false;
            this.enable = true;
            this.closed = false;
        } finally {
            lock.unlock();
        }
    }

也就是说, 修改Apollo配置的时候,并不会restart  druid数据源。 导致设置失败。

致辞问题发现并解决,后续使用过程中需要注意。当然修改Apollo其他配置不会导致这个问题。

  • 13
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
首先,需要在pom.xml中添加依赖,如下: ```xml <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.6</version> </dependency> ``` 然后,需要在application.yml中配置Druid数据和Mybatis-plus的配置,如下: ```yaml spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8 username: root password: root druid: max-active: 20 initial-size: 1 max-wait: 60000 min-idle: 1 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 FROM DUAL test-while-idle: true test-on-borrow: false test-on-return: false mybatis-plus: mapper-locations: classpath:/mapper/*.xml global-config: db-config: id-type: auto field-strategy: not_empty table-prefix: t_ logic-delete-value: 1 logic-not-delete-value: 0 configuration: map-underscore-to-camel-case: true ``` 接着,我们需要创建一个数据切换的工具类,如下: ```java public class DynamicDataSourceContextHolder { private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); public static void setDataSourceType(String dataSourceType) { CONTEXT_HOLDER.set(dataSourceType); } public static String getDataSourceType() { return CONTEXT_HOLDER.get(); } public static void clearDataSourceType() { CONTEXT_HOLDER.remove(); } } ``` 然后,我们需要创建一个切面,用来在方法执行前切换数据,如下: ```java @Component @Aspect public class DynamicDataSourceAspect { @Pointcut("@annotation(com.example.demo.annotation.DataSource)") public void dataSourcePointCut() {} @Around("dataSourcePointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); DataSource dataSource = signature.getMethod().getAnnotation(DataSource.class); if (dataSource == null) { DynamicDataSourceContextHolder.setDataSourceType("db1"); } else { DynamicDataSourceContextHolder.setDataSourceType(dataSource.value()); } try { return point.proceed(); } finally { DynamicDataSourceContextHolder.clearDataSourceType(); } } } ``` 在需要使用不同数据的方法上,我们可以使用@DataSource注解来指定数据,如下: ```java @DataSource("db2") public List<User> selectUserList() { return userMapper.selectList(null); } ``` 最后,我们需要在配置类中配置多个数据,如下: ```java @Configuration public class DataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.druid.db1") public DataSource db1() { return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.druid.db2") public DataSource db2() { return DruidDataSourceBuilder.create().build(); } @Bean @Primary public DataSource dataSource(DataSource db1, DataSource db2) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("db1", db1); targetDataSources.put("db2", db2); DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(); dataSource.setTargetDataSources(targetDataSources); dataSource.setDefaultTargetDataSource(db1); return dataSource; } } ``` 以上就是Mybatis-plus切换Druid数据的完整代码和配置文件配置。其中,DynamicRoutingDataSource是动态数据的实现类,需要自行实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值