Springboot 从数据库读取数据库配置信息,动态切换多数据源 最详细实战教程

 以前写过一篇教程,Springboot AOP方式切换多数据源(主从两库类似情况使用最佳):

https://blog.csdn.net/qq_35387940/article/details/100122788

网上大多流传的springboot系列的切换多数据源都是以上那种写死在配置文件里的方式,这样如果我需要切换的数据源有10个,那么这种方式会不会显得稍微有点繁琐了。

现在这篇介绍的流程是,我们把各个数据源的配置信息写在一张数据库表里,从数据库表去加载这些数据源信息,根据我们给每个数据源命名的id去切换数据源,操作对应的数据库。

OK,接下来我们开始(如果真的想弄懂,最好跟我一步步来)

首先准备多个数据库,test1 ,test2 ,test3  :

接下来,我们在test1中,创建表 databasesource ,相关的SQL语句:

1CREATE TABLE `databasesource` ( 2 `datasource_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '数据源的id', 3 `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '连接信息', 4 `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名', 5 `pass_word` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码', 6 `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '暂留字段', 7 `databasetype` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '数据库类型' 8) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; 9 10

然后往test1数据库中的表databasesource里填充test2 、test3 这两个数据库的相关配置信息(对应的数据库帐号密码改成自己的),相关的SQL语句:

ps:这里面的datasource_id的值,是我们后面手动切换数据源的是使用的数据源 id

1INSERT INTO `test1`.`databasesource`(`datasource_id`, `url`, `user_name`, `pass_word`, `code`, `databasetype`) VALUES ('dbtest2', 'jdbc:mysql://localhost:3306/test2?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull', 'root', 'root', NULL, 'mysql'); 2INSERT INTO `test1`.`databasesource`(`datasource_id`, `url`, `user_name`, `pass_word`, `code`, `databasetype`) VALUES ('dbtest3', 'jdbc:mysql://localhost:3306/test3?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull', 'root', 'root', NULL, 'mysql'); 3 4

 接下来,我们分别在test2数据库和test3数据库中都创建user表,相关的SQL语句:

1CREATE TABLE `user` ( 2 `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, 3 `age` int(3) NULL DEFAULT NULL 4) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; 5

然后往test2数据库的user表里面填充两条数据用于测试, 相关的SQL语句:

1INSERT INTO `test2`.`user`(`user_name`, `age`) VALUES ('数据库2-小明', 20); 2INSERT INTO `test2`.`user`(`user_name`, `age`) VALUES ('数据库2-小方', 17); 3 4

然后往test3数据库的user表里面填充两条数据用于测试, 相关的SQL语句:

1INSERT INTO `test3`.`user`(`user_name`, `age`) VALUES ('数据库3-啊强', 11); 2INSERT INTO `test3`.`user`(`user_name`, `age`) VALUES ('数据库3-啊木', 12); 3 4

OK,到这里我们的数据库模拟场景已经准备完毕了,接下来下面就是真正的核心环节了!

首先是pom.xml:

1<?xml version="1.0" encoding="UTF-8"?> 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4 <modelVersion>4.0.0</modelVersion> 5 <parent> 6 <groupId>org.springframework.boot</groupId> 7 <artifactId>spring-boot-starter-parent</artifactId> 8 <version>2.2.0.RELEASE</version> 9 <relativePath/> <!-- lookup parent from repository --> 10 </parent> 11 <groupId>com.testDb</groupId> 12 <artifactId>dbsource</artifactId> 13 <version>0.0.1-SNAPSHOT</version> 14 <name>dbsource</name> 15 <description>Demo project for Spring Boot</description> 16 17 <properties> 18 <java.version>1.8</java.version> 19 </properties> 20 21 <dependencies> 22 <dependency> 23 <groupId>org.springframework.boot</groupId> 24 <artifactId>spring-boot-starter-jdbc</artifactId> 25 </dependency> 26 <dependency> 27 <groupId>org.springframework.boot</groupId> 28 <artifactId>spring-boot-starter-web</artifactId> 29 </dependency> 30 <dependency> 31 <groupId>org.mybatis.spring.boot</groupId> 32 <artifactId>mybatis-spring-boot-starter</artifactId> 33 <version>2.0.0</version> 34 </dependency> 35 <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> 36 <dependency> 37 <groupId>org.projectlombok</groupId> 38 <artifactId>lombok</artifactId> 39 <version>1.16.10</version> 40 </dependency> 41 <dependency> 42 <groupId>mysql</groupId> 43 <artifactId>mysql-connector-java</artifactId> 44 <scope>runtime</scope> 45 </dependency> 46 <dependency> 47 <groupId>org.springframework.boot</groupId> 48 <artifactId>spring-boot-starter-test</artifactId> 49 <scope>test</scope> 50 </dependency> 51 <!-- druid数据源驱动 1.1.10解决springboot从1.0——2.0版本问题--> 52 <dependency> 53 <groupId>com.alibaba</groupId> 54 <artifactId>druid-spring-boot-starter</artifactId> 55 <version>1.1.10</version> 56 </dependency> 57 58 </dependencies> 59 60 <build> 61 <plugins> 62 <plugin> 63 <groupId>org.springframework.boot</groupId> 64 <artifactId>spring-boot-maven-plugin</artifactId> 65 </plugin> 66 </plugins> 67 </build> 68 69</project> 70 71

紧接着,application.yml(这里面的数据库配置信息将作为默认数据库):

1spring: 2 aop: 3 proxy-target-class: true #true为使用CGLIB代理 4 datasource: 5 6 #nullCatalogMeansCurrent=true& 7 url: jdbc:mysql://localhost:3306/test1?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull 8 username: root 9 password: root 10 #新版mysql驱动配置方法 11 driverClassName: com.mysql.cj.jdbc.Driver 12 ###################以下为druid增加的配置########################### 13 type: com.alibaba.druid.pool.DruidDataSource 14 # 下面为连接池的补充设置,应用到上面所有数据源中 15 # 初始化大小,最小,最大 16 initialSize: 5 17 minIdle: 5 18 maxActive: 20 19 # 配置获取连接等待超时的时间 20 maxWait: 60000 21 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 22 timeBetweenEvictionRunsMillis: 60000 23 # 配置一个连接在池中最小生存的时间,单位是毫秒 24 minEvictableIdleTimeMillis: 300000 25 validationQuery: SELECT 1 FROM DUAL 26 testWhileIdle: true 27 testOnBorrow: false 28 testOnReturn: false 29 # 打开PSCache,并且指定每个连接上PSCache的大小 30 poolPreparedStatements: true 31 maxPoolPreparedStatementPerConnectionSize: 20 32 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 33 filters: stat,wall,log4j 34 # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 35 connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 36 # 合并多个DruidDataSource的监控数据 37 useGlobalDataSourceStat: true 38 ###############以上为配置druid添加的配置######################################## 39 40mybatis: 41 type-aliases-package: com.testdb.dbsource.pojo #扫描包路径 42 configuration: 43 map-underscore-to-camel-case: true #打开驼峰命名 44 config-location: classpath:mybatis/mybatis-config.xml 45 46server: 47 port: 8097 48 49

 先创建DataSource.java实体类,数据源信息装配的时候用:

1import lombok.Data; 2import lombok.ToString; 3 4/** 5 * @Author : JCccc 6 * @CreateTime : 2019/10/22 7 * @Description : 8 **/ 9@Data 10@ToString 11public class DataSource { 12 String datasourceId; 13 String url; 14 String userName; 15 String passWord; 16 String code; 17 String databasetype; 18} 19 20

接下来,创建DruidDBConfig.java:

这里主要是配置默认的数据源,配置Druid数据库连接池,配置sql工厂加载mybatis的文件,扫描实体类等

1import com.alibaba.druid.pool.DruidDataSource; 2import com.alibaba.druid.support.http.StatViewServlet; 3import com.alibaba.druid.support.http.WebStatFilter; 4import org.apache.ibatis.session.SqlSessionFactory; 5import org.mybatis.spring.SqlSessionFactoryBean; 6import org.slf4j.Logger; 7import org.slf4j.LoggerFactory; 8import org.springframework.beans.factory.annotation.Qualifier; 9import org.springframework.beans.factory.annotation.Value; 10import org.springframework.boot.context.properties.ConfigurationProperties; 11import org.springframework.boot.web.servlet.FilterRegistrationBean; 12import org.springframework.boot.web.servlet.ServletRegistrationBean; 13import org.springframework.context.annotation.Bean; 14import org.springframework.context.annotation.Configuration; 15import org.springframework.context.annotation.Primary; 16import org.springframework.core.io.support.PathMatchingResourcePatternResolver; 17import org.springframework.core.io.support.ResourcePatternResolver; 18import org.springframework.transaction.annotation.EnableTransactionManagement; 19import javax.sql.DataSource; 20import java.sql.SQLException; 21import java.util.HashMap; 22import java.util.Map; 23 24/** 25 * @Author : JCccc 26 * @CreateTime : 2019/10/22 27 * @Description : 28 **/ 29@Configuration 30@EnableTransactionManagement 31public class DruidDBConfig { 32 private final Logger log = LoggerFactory.getLogger(getClass()); 33 34 // adi数据库连接信息 35 @Value("${spring.datasource.url}") 36 private String dbUrl; 37 @Value("${spring.datasource.username}") 38 private String username; 39 @Value("${spring.datasource.password}") 40 private String password; 41 @Value("${spring.datasource.driverClassName}") 42 private String driverClassName; 43 // 连接池连接信息 44 @Value("${spring.datasource.initialSize}") 45 private int initialSize; 46 @Value("${spring.datasource.minIdle}") 47 private int minIdle; 48 @Value("${spring.datasource.maxActive}") 49 private int maxActive; 50 @Value("${spring.datasource.maxWait}") 51 private int maxWait; 52 53 @Bean // 声明其为Bean实例 54 @Primary // 在同样的DataSource中,首先使用被标注的DataSource 55 @Qualifier("mainDataSource") 56 public DataSource dataSource() throws SQLException { 57 DruidDataSource datasource = new DruidDataSource(); 58 // 基础连接信息 59 datasource.setUrl(this.dbUrl); 60 datasource.setUsername(username); 61 datasource.setPassword(password); 62 datasource.setDriverClassName(driverClassName); 63 // 连接池连接信息 64 datasource.setInitialSize(initialSize); 65 datasource.setMinIdle(minIdle); 66 datasource.setMaxActive(maxActive); 67 datasource.setMaxWait(maxWait); 68 datasource.setPoolPreparedStatements(true); //是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 69 datasource.setMaxPoolPreparedStatementPerConnectionSize(50); 70 // datasource.setConnectionProperties("oracle.net.CONNECT_TIMEOUT=6000;oracle.jdbc.ReadTimeout=60000");//对于耗时长的查询sql,会受限于ReadTimeout的控制,单位毫秒 71 datasource.setConnectionProperties("druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000");//对于耗时长的查询sql,会受限于ReadTimeout的控制,单位毫秒 72 datasource.setTestOnBorrow(true); //申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用 73 datasource.setTestWhileIdle(true);//建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 74 String validationQuery = "select 1 from dual"; 75 datasource.setValidationQuery(validationQuery); //用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 76 datasource.setFilters("stat,wall");//属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall 77 datasource.setTimeBetweenEvictionRunsMillis(60000); //配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 78 datasource.setMinEvictableIdleTimeMillis(180000); //配置一个连接在池中最小生存的时间,单位是毫秒,这里配置为3分钟180000 79 datasource.setKeepAlive(true); //打开druid.keepAlive之后,当连接池空闲时,池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作,即执行druid.validationQuery指定的查询SQL,一般为select * from dual,只要minEvictableIdleTimeMillis设置的小于防火墙切断连接时间,就可以保证当连接空闲时自动做保活检测,不会被防火墙切断 80 datasource.setRemoveAbandoned(true); //是否移除泄露的连接/超过时间限制是否回收。 81 datasource.setRemoveAbandonedTimeout(3600); //泄露连接的定义时间(要超过最大事务的处理时间);单位为秒。这里配置为1小时 82 datasource.setLogAbandoned(true); ////移除泄露连接发生是是否记录日志 83 return datasource; 84 85 86 } 87 88 89 /** 90 * 注册一个StatViewServlet druid监控页面配置1-帐号密码配置 91 * 92 * @return servlet registration bean 93 */ 94 @Bean 95 public ServletRegistrationBean druidStatViewServlet() { 96 ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean( 97 new StatViewServlet(), "/druid/*"); 98 servletRegistrationBean.addInitParameter("loginUsername", "admin"); 99 servletRegistrationBean.addInitParameter("loginPassword", "123456"); 100 servletRegistrationBean.addInitParameter("resetEnable", "false"); 101 return servletRegistrationBean; 102 } 103 104 /** 105 * 注册一个:filterRegistrationBean druid监控页面配置2-允许页面正常浏览 106 * 107 * @return filter registration bean 108 */ 109 @Bean 110 public FilterRegistrationBean druidStatFilter() { 111 FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean( 112 new WebStatFilter()); 113 // 添加过滤规则. 114 filterRegistrationBean.addUrlPatterns("/*"); 115 // 添加不需要忽略的格式信息. 116 filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); 117 return filterRegistrationBean; 118 } 119 120 @Bean(name = "dynamicDataSource") 121 @Qualifier("dynamicDataSource") 122 public DynamicDataSource dynamicDataSource() throws SQLException { 123 DynamicDataSource dynamicDataSource = new DynamicDataSource(); 124 dynamicDataSource.setDebug(false); 125 //配置缺省的数据源 126 // 默认数据源配置 DefaultTargetDataSource 127 dynamicDataSource.setDefaultTargetDataSource(dataSource()); 128 Map<Object, Object> targetDataSources = new HashMap<Object, Object>(); 129 //额外数据源配置 TargetDataSources 130 targetDataSources.put("mainDataSource", dataSource()); 131 dynamicDataSource.setTargetDataSources(targetDataSources); 132 return dynamicDataSource; 133 } 134 135 @Bean 136 public SqlSessionFactory sqlSessionFactory() throws Exception { 137 SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); 138 sqlSessionFactoryBean.setDataSource(dynamicDataSource()); 139 //解决手动创建数据源后字段到bean属性名驼峰命名转换失效的问题 140 sqlSessionFactoryBean.setConfiguration(configuration()); 141 142 // 设置mybatis的主配置文件 143 ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); 144 // Resource mybatisConfigXml = resolver.getResource("classpath:mybatis/mybatis-config.xml"); 145 // sqlSessionFactoryBean.setConfigLocation(mybatisConfigXml); 146 // 设置别名包 147 // sqlSessionFactoryBean.setTypeAliasesPackage("com.testdb.dbsource.pojo"); 148 //就是这句代码,只能指定单个mapper.xml文件,加通配符的话找不到文件 149 sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mybatis/mapper/*.xml")); 150 return sqlSessionFactoryBean.getObject(); 151 } 152 153 154 /** 155 * 读取驼峰命名设置 156 * 157 * @return 158 */ 159 @Bean 160 @ConfigurationProperties(prefix = "mybatis.configuration") 161 public org.apache.ibatis.session.Configuration configuration() { 162 return new org.apache.ibatis.session.Configuration(); 163 } 164 165 166} 167 168 169

 然后是用于手动切换数据源的 DBContextHolder.java:

1import org.slf4j.Logger; 2import org.slf4j.LoggerFactory; 3 4/** 5 * @Author : JCccc 6 * @CreateTime : 2019/10/22 7 * @Description : 8 **/ 9public class DBContextHolder { 10 private final static Logger log = LoggerFactory.getLogger(DBContextHolder.class); 11 // 对当前线程的操作-线程安全的 12 private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); 13 14 // 调用此方法,切换数据源 15 public static void setDataSource(String dataSource) { 16 contextHolder.set(dataSource); 17 log.info("已切换到数据源:{}",dataSource); 18 } 19 20 // 获取数据源 21 public static String getDataSource() { 22 return contextHolder.get(); 23 } 24 25 // 删除数据源 26 public static void clearDataSource() { 27 contextHolder.remove(); 28 log.info("已切换到主数据源"); 29 } 30 31} 32 33

然后是核心,手动加载默认数据源、创建数据源连接、检查数据源连接、删除数据源连接等 ,DynamicDataSource.java:

1import com.alibaba.druid.pool.DruidDataSource; 2import com.alibaba.druid.stat.DruidDataSourceStatManager; 3import com.testdb.dbsource.pojo.DataSource; 4import org.slf4j.Logger; 5import org.slf4j.LoggerFactory; 6import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; 7import org.springframework.util.StringUtils; 8import java.sql.Connection; 9import java.sql.DriverManager; 10import java.util.Map; 11import java.util.Set; 12 13 14public class DynamicDataSource extends AbstractRoutingDataSource { 15 private boolean debug = true; 16 private final Logger log = LoggerFactory.getLogger(getClass()); 17 private Map<Object, Object> dynamicTargetDataSources; 18 private Object dynamicDefaultTargetDataSource; 19 20 21 @Override 22 protected Object determineCurrentLookupKey() { 23 String datasource = DBContextHolder.getDataSource(); 24 if (!StringUtils.isEmpty(datasource)) { 25 Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources; 26 if (dynamicTargetDataSources2.containsKey(datasource)) { 27 log.info("---当前数据源:" + datasource + "---"); 28 } else { 29 log.info("不存在的数据源:"); 30 return null; 31// throw new ADIException("不存在的数据源:"+datasource,500); 32 } 33 } else { 34 log.info("---当前数据源:默认数据源---"); 35 } 36 37 return datasource; 38 } 39 40 @Override 41 public void setTargetDataSources(Map<Object, Object> targetDataSources) { 42 43 super.setTargetDataSources(targetDataSources); 44 45 this.dynamicTargetDataSources = targetDataSources; 46 47 } 48 49 50 // 创建数据源 51 public boolean createDataSource(String key, String driveClass, String url, String username, String password, String databasetype) { 52 try { 53 try { // 排除连接不上的错误 54 Class.forName(driveClass); 55 DriverManager.getConnection(url, username, password);// 相当于连接数据库 56 57 } catch (Exception e) { 58 59 return false; 60 } 61 @SuppressWarnings("resource") 62// HikariDataSource druidDataSource = new HikariDataSource(); 63 DruidDataSource druidDataSource = new DruidDataSource(); 64 druidDataSource.setName(key); 65 druidDataSource.setDriverClassName(driveClass); 66 druidDataSource.setUrl(url); 67 druidDataSource.setUsername(username); 68 druidDataSource.setPassword(password); 69 druidDataSource.setInitialSize(50); //初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 70 druidDataSource.setMaxActive(200); //最大连接池数量 71 druidDataSource.setMaxWait(60000); //获取连接时最大等待时间,单位毫秒。当链接数已经达到了最大链接数的时候,应用如果还要获取链接就会出现等待的现象,等待链接释放并回到链接池,如果等待的时间过长就应该踢掉这个等待,不然应用很可能出现雪崩现象 72 druidDataSource.setMinIdle(40); //最小连接池数量 73 String validationQuery = "select 1 from dual"; 74// if("mysql".equalsIgnoreCase(databasetype)) { 75// driveClass = DBUtil.mysqldriver; 76// validationQuery = "select 1"; 77// } else if("oracle".equalsIgnoreCase(databasetype)){ 78// driveClass = DBUtil.oracledriver; 79// druidDataSource.setPoolPreparedStatements(true); //是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 80// druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(50); 81// int sqlQueryTimeout = ADIPropUtil.sqlQueryTimeOut(); 82// druidDataSource.setConnectionProperties("oracle.net.CONNECT_TIMEOUT=6000;oracle.jdbc.ReadTimeout="+sqlQueryTimeout);//对于耗时长的查询sql,会受限于ReadTimeout的控制,单位毫秒 83// } else if("sqlserver2000".equalsIgnoreCase(databasetype)){ 84// driveClass = DBUtil.sql2000driver; 85// validationQuery = "select 1"; 86// } else if("sqlserver".equalsIgnoreCase(databasetype)){ 87// driveClass = DBUtil.sql2005driver; 88// validationQuery = "select 1"; 89// } 90 druidDataSource.setTestOnBorrow(true); //申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用 91 druidDataSource.setTestWhileIdle(true);//建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 92 druidDataSource.setValidationQuery(validationQuery); //用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 93 druidDataSource.setFilters("stat");//属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall 94 druidDataSource.setTimeBetweenEvictionRunsMillis(60000); //配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 95 druidDataSource.setMinEvictableIdleTimeMillis(180000); //配置一个连接在池中最小生存的时间,单位是毫秒,这里配置为3分钟180000 96 druidDataSource.setKeepAlive(true); //打开druid.keepAlive之后,当连接池空闲时,池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作,即执行druid.validationQuery指定的查询SQL,一般为select * from dual,只要minEvictableIdleTimeMillis设置的小于防火墙切断连接时间,就可以保证当连接空闲时自动做保活检测,不会被防火墙切断 97 druidDataSource.setRemoveAbandoned(true); //是否移除泄露的连接/超过时间限制是否回收。 98 druidDataSource.setRemoveAbandonedTimeout(3600); //泄露连接的定义时间(要超过最大事务的处理时间);单位为秒。这里配置为1小时 99 druidDataSource.setLogAbandoned(true); ////移除泄露连接发生是是否记录日志 100 druidDataSource.init(); 101 this.dynamicTargetDataSources.put(key, druidDataSource); 102 setTargetDataSources(this.dynamicTargetDataSources);// 将map赋值给父类的TargetDataSources 103 super.afterPropertiesSet();// 将TargetDataSources中的连接信息放入resolvedDataSources管理 104 log.info(key+"数据源初始化成功"); 105 //log.info(key+"数据源的概况:"+druidDataSource.dump()); 106 return true; 107 } catch (Exception e) { 108 log.error(e + ""); 109 return false; 110 } 111 } 112 // 删除数据源 113 public boolean delDatasources(String datasourceid) { 114 Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources; 115 if (dynamicTargetDataSources2.containsKey(datasourceid)) { 116 Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances(); 117 for (DruidDataSource l : druidDataSourceInstances) { 118 if (datasourceid.equals(l.getName())) { 119 dynamicTargetDataSources2.remove(datasourceid); 120 DruidDataSourceStatManager.removeDataSource(l); 121 setTargetDataSources(dynamicTargetDataSources2);// 将map赋值给父类的TargetDataSources 122 super.afterPropertiesSet();// 将TargetDataSources中的连接信息放入resolvedDataSources管理 123 return true; 124 } 125 } 126 return false; 127 } else { 128 return false; 129 } 130 } 131 132 // 测试数据源连接是否有效 133 public boolean testDatasource(String key, String driveClass, String url, String username, String password) { 134 try { 135 Class.forName(driveClass); 136 DriverManager.getConnection(url, username, password); 137 return true; 138 } catch (Exception e) { 139 return false; 140 } 141 } 142 143 @Override 144 public void setDefaultTargetDataSource(Object defaultTargetDataSource) { 145 super.setDefaultTargetDataSource(defaultTargetDataSource); 146 this.dynamicDefaultTargetDataSource = defaultTargetDataSource; 147 } 148 149 /** 150 * @param debug 151 * the debug to set 152 */ 153 public void setDebug(boolean debug) { 154 this.debug = debug; 155 } 156 157 /** 158 * @return the debug 159 */ 160 public boolean isDebug() { 161 return debug; 162 } 163 164 /** 165 * @return the dynamicTargetDataSources 166 */ 167 public Map<Object, Object> getDynamicTargetDataSources() { 168 return dynamicTargetDataSources; 169 } 170 171 /** 172 * @param dynamicTargetDataSources 173 * the dynamicTargetDataSources to set 174 */ 175 public void setDynamicTargetDataSources(Map<Object, Object> dynamicTargetDataSources) { 176 this.dynamicTargetDataSources = dynamicTargetDataSources; 177 } 178 179 /** 180 * @return the dynamicDefaultTargetDataSource 181 */ 182 public Object getDynamicDefaultTargetDataSource() { 183 return dynamicDefaultTargetDataSource; 184 } 185 186 /** 187 * @param dynamicDefaultTargetDataSource 188 * the dynamicDefaultTargetDataSource to set 189 */ 190 public void setDynamicDefaultTargetDataSource(Object dynamicDefaultTargetDataSource) { 191 this.dynamicDefaultTargetDataSource = dynamicDefaultTargetDataSource; 192 } 193 194 public void createDataSourceWithCheck(DataSource dataSource) throws Exception { 195 String datasourceId = dataSource.getDatasourceId(); 196 log.info("正在检查数据源:"+datasourceId); 197 Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources; 198 if (dynamicTargetDataSources2.containsKey(datasourceId)) { 199 log.info("数据源"+datasourceId+"之前已经创建,准备测试数据源是否正常..."); 200 //DataSource druidDataSource = (DataSource) dynamicTargetDataSources2.get(datasourceId); 201 DruidDataSource druidDataSource = (DruidDataSource) dynamicTargetDataSources2.get(datasourceId); 202 boolean rightFlag = true; 203 Connection connection = null; 204 try { 205 log.info(datasourceId+"数据源的概况->当前闲置连接数:"+druidDataSource.getPoolingCount()); 206 long activeCount = druidDataSource.getActiveCount(); 207 log.info(datasourceId+"数据源的概况->当前活动连接数:"+activeCount); 208 if(activeCount > 0) { 209 log.info(datasourceId+"数据源的概况->活跃连接堆栈信息:"+druidDataSource.getActiveConnectionStackTrace()); 210 } 211 log.info("准备获取数据库连接..."); 212 connection = druidDataSource.getConnection(); 213 log.info("数据源"+datasourceId+"正常"); 214 } catch (Exception e) { 215 log.error(e.getMessage(),e); //把异常信息打印到日志文件 216 rightFlag = false; 217 log.info("缓存数据源"+datasourceId+"已失效,准备删除..."); 218 if(delDatasources(datasourceId)) { 219 log.info("缓存数据源删除成功"); 220 } else { 221 log.info("缓存数据源删除失败"); 222 } 223 } finally { 224 if(null != connection) { 225 connection.close(); 226 } 227 } 228 if(rightFlag) { 229 log.info("不需要重新创建数据源"); 230 return; 231 } else { 232 log.info("准备重新创建数据源..."); 233 createDataSource(dataSource); 234 log.info("重新创建数据源完成"); 235 } 236 } else { 237 createDataSource(dataSource); 238 } 239 240 } 241 242 private void createDataSource(DataSource dataSource) throws Exception { 243 String datasourceId = dataSource.getDatasourceId(); 244 log.info("准备创建数据源"+datasourceId); 245 String databasetype = dataSource.getDatabasetype(); 246 String username = dataSource.getUserName(); 247 String password = dataSource.getPassWord(); 248 String url = dataSource.getUrl(); 249 String driveClass = "com.mysql.cj.jdbc.Driver"; 250// if("mysql".equalsIgnoreCase(databasetype)) { 251// driveClass = DBUtil.mysqldriver; 252// } else if("oracle".equalsIgnoreCase(databasetype)){ 253// driveClass = DBUtil.oracledriver; 254// } else if("sqlserver2000".equalsIgnoreCase(databasetype)){ 255// driveClass = DBUtil.sql2000driver; 256// } else if("sqlserver".equalsIgnoreCase(databasetype)){ 257// driveClass = DBUtil.sql2005driver; 258// } 259 if(testDatasource(datasourceId,driveClass,url,username,password)) { 260 boolean result = this.createDataSource(datasourceId, driveClass, url, username, password, databasetype); 261 if(!result) { 262 log.error("数据源"+datasourceId+"配置正确,但是创建失败"); 263// throw new ADIException("数据源"+datasourceId+"配置正确,但是创建失败",500); 264 } 265 } else { 266 log.error("数据源配置有错误"); 267// throw new ADIException("数据源配置有错误",500); 268 } 269 } 270 271} 272 273 274

ok,然后是我们切换数据源使用的方法, 我们这里采用mybatis注解的方式获取test1数据库里的databasesource 表信息,然后根据我们传入对应的数据源id进行数据源切换:

DataSourceMapper.java :

1import com.testdb.dbsource.pojo.DataSource; 2import org.apache.ibatis.annotations.Mapper; 3import org.apache.ibatis.annotations.Select; 4import java.util.List; 5 6/** 7 * @Author : JCccc 8 * @CreateTime : 2019/10/23 9 * @Description : 10 **/ 11@Mapper 12public interface DataSourceMapper { 13 14 @Select("SELECT * FROM databasesource") 15 List<DataSource> get(); 16 17 18} 19

DBChangeService.java:

1import com.testdb.dbsource.pojo.DataSource; 2import java.util.List; 3 4/** 5 * @Author : JCccc 6 * @CreateTime : 2019/10/22 7 * @Description : 8 **/ 9 10public interface DBChangeService { 11 12 List<DataSource> get(); 13 14 boolean changeDb(String datasourceId) throws Exception; 15 16} 17 18

DBChangeServiceImpl.java:

1import com.testdb.dbsource.dbconfig.DBContextHolder; 2import com.testdb.dbsource.dbconfig.DynamicDataSource; 3import com.testdb.dbsource.mapper.DataSourceMapper; 4import com.testdb.dbsource.pojo.DataSource; 5import com.testdb.dbsource.service.DBChangeService; 6import org.springframework.beans.factory.annotation.Autowired; 7import org.springframework.stereotype.Service; 8import java.util.List; 9 10/** 11 * @Author : JCccc 12 * @CreateTime : 2019/10/22 13 * @Description : 14 **/ 15@Service 16public class DBChangeServiceImpl implements DBChangeService { 17 18 @Autowired 19 DataSourceMapper dataSourceMapper; 20 @Autowired 21 private DynamicDataSource dynamicDataSource; 22 @Override 23 public List<DataSource> get() { 24 return dataSourceMapper.get(); 25 } 26 27 @Override 28 public boolean changeDb(String datasourceId) throws Exception { 29 30 //默认切换到主数据源,进行整体资源的查找 31 DBContextHolder.clearDataSource(); 32 33 List<DataSource> dataSourcesList = dataSourceMapper.get(); 34 35 for (DataSource dataSource : dataSourcesList) { 36 if (dataSource.getDatasourceId().equals(datasourceId)) { 37 System.out.println("需要使用的的数据源已经找到,datasourceId是:" + dataSource.getDatasourceId()); 38 //创建数据源连接&检查 若存在则不需重新创建 39 dynamicDataSource.createDataSourceWithCheck(dataSource); 40 //切换到该数据源 41 DBContextHolder.setDataSource(dataSource.getDatasourceId()); 42 return true; 43 } 44 } 45 return false; 46 47 } 48 49} 50

注意认真看看上面的changeDb这个方法里面的代码,这就是后续手动切换调用的方法。

 

接下来,写相关操作user表的代码,因为user表分别在test2、test3数据库里,这样用于我们切换到test2或者test3数据库操作这些数据。

User.java:

1import lombok.Data; 2import lombok.ToString; 3 4/** 5 * @Author : JCccc 6 * @CreateTime : 2019/10/22 7 * @Description : 8 **/ 9 10@Data 11@ToString 12public class User { 13 14 String userName; 15 String age; 16} 17 18

UserMappper.java  (上面简单介绍了下使用注解的方式获取表数据,可能有些人不习惯,那么这里也使用传统的mapper.xml方式编写mysql语句):

1import com.testdb.dbsource.pojo.User; 2import org.apache.ibatis.annotations.Mapper; 3import java.util.List; 4 5/** 6 * @Author : JCccc 7 * @CreateTime : 2019/10/23 8 * @Description : 9 **/ 10@Mapper 11public interface UserMapper { 12 13 List<User> queryUserInfo(); 14 15 16 17} 18

userMapper.xml(注意namespace命名空间对应的路径以及user实体类对应的路径):

1<?xml version="1.0" encoding="UTF-8" ?> 2<!DOCTYPE mapper 3 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 5<mapper namespace="com.testdb.dbsource.mapper.UserMapper"> 6 7 <!--查询所有用户信息--> 8 <select id="queryUserInfo" resultType="com.testdb.dbsource.pojo.User"> 9 select * 10 from user 11 </select> 12 13 14</mapper> 15 16

顺便一提,我的mapper.xml放在了下面的这个目录结果里,这里的目录结构路径非常关键,因为我们是手动切换数据源,采取了手动配置SqlSessionFactory,需要我们自己去配置mapper.xml路径的,一开始的DruidDBConfig里面有相关的代码,可以去回顾下:

顺带,mybatis-config.xml里面,我做了简单的配置:

其实关于驼峰命名方式开启,我们在手动配置的时候也特意做了配置代码的。

1<?xml version="1.0" encoding="UTF-8" ?> 2<!DOCTYPE configuration 3 PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-config.dtd"> 5<configuration> 6 7 <settings> 8 <setting name="useGeneratedKeys" value="true"/> 9 <setting name="useColumnLabel" value="true"/> 10 <setting name="mapUnderscoreToCamelCase" value="true"/> 11 </settings> 12</configuration> 13 14 15 16

到这里,我们已经可以开始测试,

创建UserController.java,写个简单的测试接口:

1import com.testdb.dbsource.dbconfig.DBContextHolder; 2import com.testdb.dbsource.pojo.User; 3import com.testdb.dbsource.service.DBChangeService; 4import com.testdb.dbsource.service.UserService; 5import org.springframework.beans.factory.annotation.Autowired; 6import org.springframework.web.bind.annotation.GetMapping; 7import org.springframework.web.bind.annotation.RestController; 8import java.util.List; 9 10/** 11 * @Author : JCccc 12 * @CreateTime : 2019/10/23 13 * @Description : 14 **/ 15 16@RestController 17public class UserController { 18 19 20 @Autowired 21 private DBChangeService dbChangeServiceImpl; 22 @Autowired 23 UserService userService; 24 25 26 27 28 /** 29 * 查询所有 30 * @return 31 */ 32 @GetMapping("/test") 33 public String test() throws Exception { 34 35 //切换到数据库dbtest2 36 String datasourceId="dbtest2"; 37 dbChangeServiceImpl.changeDb(datasourceId); 38 List<User> userList= userService.queryUserInfo(); 39 System.out.println(userList.toString()); 40 41 //再切换到数据库dbtest3 42 dbChangeServiceImpl.changeDb("dbtest3"); 43 List<User> userList3= userService.queryUserInfo(); 44 System.out.println(userList3.toString()); 45 46 47 //切回主数据源 48 DBContextHolder.clearDataSource(); 49 return "ok"; 50 } 51 52} 53 54

 主要看代码注释,调用整合出来的changeDb方法,通过传送数据源id (dbtest2)

切换到对应的数据库,然后操作对应数据库。

 

项目运行起来,调用接口 /test :

 

然后我们来看看控制台输出内容:

OK,非常简单顺利,教程就到此。 

 

补充该实战教学的事务相关介绍:

在本篇文章里,使用事务,只可达: 单个数据源管理自己的事务
也就是,例如:

接口是这样的,

在不同数据源的添加方法里面,配置事务: 

 

以上这种情况,事务是有用的,不管切换多少个数据源进行数据库操作,每个数据源的方法开启事务注解,每个的事务是不会冲突的,自己的回滚不会影响到其他数据源。而且不用做任何的事务管理器配置。

 

那么事务失效的情形是哪种呢? (也就是有个伙伴在评论里提到事务失效,其实是指哪种情形呢?)
事务失效场景, 在controller开启事务,捕抓异常,想让不同数据源的对数据库的操作能统一进行事务回滚。
也就是下面这种场景:

这种场景在该篇实战(也包括网上种种多数据源/主从等切换) 都是做不到的,而且网上的很多老教程为了实现单个数据源管理自己的事务,还需要单独配置事务管理器,不同数据源配置不同事务管理器,起到单个数据源识别自己事务的作用。
在该篇实战里,是无需再单独为不同数据源设置不同的事务管理的。

做不到,是为什么?
是因为这种场景在一开始的时候使用注解@Transactional 的时候,已经从当期的数据源,也就是默认数据源内读取到了事务,被spring的事务同步影响,后面的切换数据源是不会影响这个事务的管理的。也就是说后面切换数据源dbtest2和dbtest3,尽管方法做了事务开启配置,但是这已经是嵌套事务的场景,回滚的也只有最早的事务,导致嵌套在内的事务失效。

 

最后简单说下:

如果只是用于主从数据库两个数据源的业务场景,那么该篇非常适合使用,因为只需保证主的事务,从库会同步数据。
而且如果仅仅是为了满足主从/读写的场景,大可不必从数据库读取数据源,使用AOP方式读取配置文件的数据源即可满足业务场景,也就是参考我这篇:https://blog.csdn.net/qq_35387940/article/details/100122788 

这种从数据库读取数据源,数据源的事务管理互不冲突使用场景是什么呢?
一.业务场景需要 很多个不同数据源,进行数据获取,不仅仅是两个,这样一来在配置文件配置是基本不可取的。

二.进行多从数据源数据获取,加上逻辑分析筛选后, 插入或更新 某个数据库,只需为该数据库事务进行负责。

例如我的使用场景:

公司各个小项目很多a,b,c,d,e,都用着自己项目的数据库。

而新项目接口需求,收到第三方的调用后,需要到a,b,c,d,e系统的数据库里读取出不同的核心数据,然后进行业务逻辑分析,然后生成新的订单,插入到新项目的数据库里,并返回结果给第三方。

 

 

那难道就没解决方案了吗?

并不然,使用JTA分布式事务即可解决。
请看我这篇:

Springboot 整合druid+mybatis+jta分布式事务+多数据源aop注解动态切换 (一篇到位)

https://blog.csdn.net/qq_35387940/article/details/103474353

 

 

 

 

代码交流 2021