网上大多流传的springboot系列的切换多数据源都是以上那种写死在配置文件里的方式,这样如果我需要切换的数据源有10个,那么这种方式会不会显得稍微有点繁琐了。
现在这篇介绍的流程是,我们把各个数据源的配置信息写在一张数据库表里,从数据库表去加载这些数据源信息,根据我们给每个数据源命名的id去切换数据源,操作对应的数据库。
OK,接下来我们开始(如果真的想弄懂,最好跟我一步步来)
首先准备多个数据库,test1 ,test2 ,test3 :
接下来,我们在test1中,创建表 databasesource ,相关的SQL语句:
CREATE TABLE `databasesource` ( `datasource_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '数据源的id', `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '连接信息', `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名', `pass_word` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码', `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '暂留字段', `databasetype` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '数据库类型' ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
然后往test1数据库中的表databasesource里填充test2 、test3 这两个数据库的相关配置信息(对应的数据库帐号密码改成自己的),相关的SQL语句:
ps:这里面的datasource_id的值,是我们后面手动切换数据源的是使用的数据源 id
INSERT 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'); INSERT 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');
接下来,我们分别在test2数据库和test3数据库中都创建user表,相关的SQL语句:
CREATE TABLE `user` ( `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `age` int(3) NULL DEFAULT NULL ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
然后往test2数据库的user表里面填充两条数据用于测试, 相关的SQL语句:
INSERT INTO `test2`.`user`(`user_name`, `age`) VALUES ('数据库2-小明', 20); INSERT INTO `test2`.`user`(`user_name`, `age`) VALUES ('数据库2-小方', 17);
然后往test3数据库的user表里面填充两条数据用于测试, 相关的SQL语句:
INSERT INTO `test3`.`user`(`user_name`, `age`) VALUES ('数据库3-啊强', 11); INSERT INTO `test3`.`user`(`user_name`, `age`) VALUES ('数据库3-啊木', 12);
OK,到这里我们的数据库模拟场景已经准备完毕了,接下来下面就是真正的核心环节了!
首先是pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.testDb</groupId> <artifactId>dbsource</artifactId> <version>0.0.1-SNAPSHOT</version> <name>dbsource</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.10</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- druid数据源驱动 1.1.10解决springboot从1.0——2.0版本问题--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
紧接着,application.yml(这里面的数据库配置信息将作为默认数据库):
spring: aop: proxy-target-class: true #true为使用CGLIB代理 datasource: #nullCatalogMeansCurrent=true& url: jdbc:mysql://localhost:3306/test1?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull username: root password: root #新版mysql驱动配置方法 driverClassName: com.mysql.cj.jdbc.Driver ###################以下为druid增加的配置########################### type: com.alibaba.druid.pool.DruidDataSource # 下面为连接池的补充设置,应用到上面所有数据源中 # 初始化大小,最小,最大 initialSize: 5 minIdle: 5 maxActive: 20 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false # 打开PSCache,并且指定每个连接上PSCache的大小 poolPreparedStatements: true maxPoolPreparedStatementPerConnectionSize: 20 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 filters: stat,wall,log4j # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 合并多个DruidDataSource的监控数据 useGlobalDataSourceStat: true ###############以上为配置druid添加的配置######################################## mybatis: type-aliases-package: com.testdb.dbsource.pojo #扫描包路径 configuration: map-underscore-to-camel-case: true #打开驼峰命名 config-location: classpath:mybatis/mybatis-config.xml server: port: 8097
先创建DataSource.java实体类,数据源信息装配的时候用:
import lombok.Data; import lombok.ToString; /** * @Author : JCccc * @CreateTime : 2019/10/22 * @Description : **/ @Data @ToString public class DataSource { String datasourceId; String url; String userName; String passWord; String code; String databasetype; }
接下来,创建DruidDBConfig.java:
这里主要是配置默认的数据源,配置Druid数据库连接池,配置sql工厂加载mybatis的文件,扫描实体类等
import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; import java.sql.SQLException; import java.util.HashMap; import java.util.Map; /** * @Author : JCccc * @CreateTime : 2019/10/22 * @Description : **/ @Configuration @EnableTransactionManagement public class DruidDBConfig { private final Logger log = LoggerFactory.getLogger(getClass()); // adi数据库连接信息 @Value("${spring.datasource.url}") private String dbUrl; @Value("${spring.datasource.username}") private String username; @Value("${spring.datasource.password}") private String password; @Value("${spring.datasource.driverClassName}") private String driverClassName; // 连接池连接信息 @Value("${spring.datasource.initialSize}") private int initialSize; @Value("${spring.datasource.minIdle}") private int minIdle; @Value("${spring.datasource.maxActive}") private int maxActive; @Value("${spring.datasource.maxWait}") private int maxWait; @Bean // 声明其为Bean实例 @Primary // 在同样的DataSource中,首先使用被标注的DataSource @Qualifier("mainDataSource") public DataSource dataSource() throws SQLException { DruidDataSource datasource = new DruidDataSource(); // 基础连接信息 datasource.setUrl(this.dbUrl); datasource.setUsername(username); datasource.setPassword(password); datasource.setDriverClassName(driverClassName); // 连接池连接信息 datasource.setInitialSize(initialSize); datasource.setMinIdle(minIdle); datasource.setMaxActive(maxActive); datasource.setMaxWait(maxWait); datasource.setPoolPreparedStatements(true); //是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 datasource.setMaxPoolPreparedStatementPerConnectionSize(20); // datasource.setConnectionProperties("oracle.net.CONNECT_TIMEOUT=6000;oracle.jdbc.ReadTimeout=60000");//对于耗时长的查询sql,会受限于ReadTimeout的控制,单位毫秒 datasource.setConnectionProperties("druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000");//对于耗时长的查询sql,会受限于ReadTimeout的控制,单位毫秒 datasource.setTestOnBorrow(true); //申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用 datasource.setTestWhileIdle(true);//建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 String validationQuery = "select 1 from dual"; datasource.setValidationQuery(validationQuery); //用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 datasource.setFilters("stat,wall");//属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall datasource.setTimeBetweenEvictionRunsMillis(60000); //配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 datasource.setMinEvictableIdleTimeMillis(180000); //配置一个连接在池中最小生存的时间,单位是毫秒,这里配置为3分钟180000 datasource.setKeepAlive(true); //打开druid.keepAlive之后,当连接池空闲时,池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作,即执行druid.validationQuery指定的查询SQL,一般为select * from dual,只要minEvictableIdleTimeMillis设置的小于防火墙切断连接时间,就可以保证当连接空闲时自动做保活检测,不会被防火墙切断 datasource.setRemoveAbandoned(true); //是否移除泄露的连接/超过时间限制是否回收。 datasource.setRemoveAbandonedTimeout(3600); //泄露连接的定义时间(要超过最大事务的处理时间);单位为秒。这里配置为1小时 datasource.setLogAbandoned(true); 移除泄露连接发生是是否记录日志 return datasource; } /** * 注册一个StatViewServlet druid监控页面配置1-帐号密码配置 * * @return servlet registration bean */ @Bean public ServletRegistrationBean druidStatViewServlet() { ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean( new StatViewServlet(), "/druid/*"); servletRegistrationBean.addInitParameter("loginUsername", "admin"); servletRegistrationBean.addInitParameter("loginPassword", "123456"); servletRegistrationBean.addInitParameter("resetEnable", "false"); return servletRegistrationBean; } /** * 注册一个:filterRegistrationBean druid监控页面配置2-允许页面正常浏览 * * @return filter registration bean */ @Bean public FilterRegistrationBean druidStatFilter() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean( new WebStatFilter()); // 添加过滤规则. filterRegistrationBean.addUrlPatterns("/*"); // 添加不需要忽略的格式信息. filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; } @Bean(name = "dynamicDataSource") @Qualifier("dynamicDataSource") public DynamicDataSource dynamicDataSource() throws SQLException { DynamicDataSource dynamicDataSource = new DynamicDataSource(); dynamicDataSource.setDebug(false); //配置缺省的数据源 // 默认数据源配置 DefaultTargetDataSource dynamicDataSource.setDefaultTargetDataSource(dataSource()); Map<Object, Object> targetDataSources = new HashMap<Object, Object>(); //额外数据源配置 TargetDataSources targetDataSources.put("mainDataSource", dataSource()); dynamicDataSource.setTargetDataSources(targetDataSources); return dynamicDataSource; } @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dynamicDataSource()); //解决手动创建数据源后字段到bean属性名驼峰命名转换失效的问题 sqlSessionFactoryBean.setConfiguration(configuration()); // 设置mybatis的主配置文件 ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); // Resource mybatisConfigXml = resolver.getResource("classpath:mybatis/mybatis-config.xml"); // sqlSessionFactoryBean.setConfigLocation(mybatisConfigXml); // 设置别名包 // sqlSessionFactoryBean.setTypeAliasesPackage("com.testdb.dbsource.pojo"); //手动配置mybatis的mapper.xml资源路径,如果单纯使用注解方式,不需要配置该行 // sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mybatis/mapper/*.xml")); return sqlSessionFactoryBean.getObject(); } /** * 读取驼峰命名设置 * * @return */ @Bean @ConfigurationProperties(prefix = "mybatis.configuration") public org.apache.ibatis.session.Configuration configuration() { return new org.apache.ibatis.session.Configuration(); } }
然后是用于手动切换数据源的 DBContextHolder.java:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @Author : JCccc * @CreateTime : 2019/10/22 * @Description : **/ public class DBContextHolder { private final static Logger log = LoggerFactory.getLogger(DBContextHolder.class); // 对当前线程的操作-线程安全的 private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); // 调用此方法,切换数据源 public static void setDataSource(String dataSource) { contextHolder.set(dataSource); log.info("已切换到数据源:{}",dataSource); } // 获取数据源 public static String getDataSource() { return contextHolder.get(); } // 删除数据源 public static void clearDataSource() { contextHolder.remove(); log.info("已切换到主数据源"); } }
然后是核心,手动加载默认数据源、创建数据源连接、检查数据源连接、删除数据源连接等 ,DynamicDataSource.java:
import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.stat.DruidDataSourceStatManager; import com.testdb.dbsource.pojo.DataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.util.StringUtils; import java.sql.Connection; import java.sql.DriverManager; import java.util.Map; import java.util.Set; public class DynamicDataSource extends AbstractRoutingDataSource { private boolean debug = true; private final Logger log = LoggerFactory.getLogger(getClass()); private Map<Object, Object> dynamicTargetDataSources; private Object dynamicDefaultTargetDataSource; @Override protected Object determineCurrentLookupKey() { String datasource = DBContextHolder.getDataSource(); if (!StringUtils.isEmpty(datasource)) { Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources; if (dynamicTargetDataSources2.containsKey(datasource)) { log.info("---当前数据源:" + datasource + "---"); } else { log.info("不存在的数据源:"); return null; // throw new ADIException("不存在的数据源:"+datasource,500); } } else { log.info("---当前数据源:默认数据源---"); } return datasource; } @Override public void setTargetDataSources(Map<Object, Object> targetDataSources) { super.setTargetDataSources(targetDataSources); this.dynamicTargetDataSources = targetDataSources; } // 创建数据源 public boolean createDataSource(String key, String driveClass, String url, String username, String password, String databasetype) { try { try { // 排除连接不上的错误 Class.forName(driveClass); DriverManager.getConnection(url, username, password);// 相当于连接数据库 } catch (Exception e) { return false; } @SuppressWarnings("resource") // HikariDataSource druidDataSource = new HikariDataSource(); DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setName(key); druidDataSource.setDriverClassName(driveClass); druidDataSource.setUrl(url); druidDataSource.setUsername(username); druidDataSource.setPassword(password); druidDataSource.setInitialSize(1); //初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 druidDataSource.setMaxActive(20); //最大连接池数量 druidDataSource.setMaxWait(60000); //获取连接时最大等待时间,单位毫秒。当链接数已经达到了最大链接数的时候,应用如果还要获取链接就会出现等待的现象,等待链接释放并回到链接池,如果等待的时间过长就应该踢掉这个等待,不然应用很可能出现雪崩现象 druidDataSource.setMinIdle(5); //最小连接池数量 String validationQuery = "select 1 from dual"; // if("mysql".equalsIgnoreCase(databasetype)) { // driveClass = DBUtil.mysqldriver; // validationQuery = "select 1"; // } else if("oracle".equalsIgnoreCase(databasetype)){ // driveClass = DBUtil.oracledriver; // druidDataSource.setPoolPreparedStatements(true); //是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 // druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(50); // int sqlQueryTimeout = ADIPropUtil.sqlQueryTimeOut(); // druidDataSource.setConnectionProperties("oracle.net.CONNECT_TIMEOUT=6000;oracle.jdbc.ReadTimeout="+sqlQueryTimeout);//对于耗时长的查询sql,会受限于ReadTimeout的控制,单位毫秒 // } else if("sqlserver2000".equalsIgnoreCase(databasetype)){ // driveClass = DBUtil.sql2000driver; // validationQuery = "select 1"; // } else if("sqlserver".equalsIgnoreCase(databasetype)){ // driveClass = DBUtil.sql2005driver; // validationQuery = "select 1"; // } druidDataSource.setTestOnBorrow(true); //申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用 druidDataSource.setTestWhileIdle(true);//建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 druidDataSource.setValidationQuery(validationQuery); //用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 druidDataSource.setFilters("stat");//属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall druidDataSource.setTimeBetweenEvictionRunsMillis(60000); //配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 druidDataSource.setMinEvictableIdleTimeMillis(180000); //配置一个连接在池中最小生存的时间,单位是毫秒,这里配置为3分钟180000 druidDataSource.setKeepAlive(true); //打开druid.keepAlive之后,当连接池空闲时,池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作,即执行druid.validationQuery指定的查询SQL,一般为select * from dual,只要minEvictableIdleTimeMillis设置的小于防火墙切断连接时间,就可以保证当连接空闲时自动做保活检测,不会被防火墙切断 druidDataSource.setRemoveAbandoned(true); //是否移除泄露的连接/超过时间限制是否回收。 druidDataSource.setRemoveAbandonedTimeout(3600); //泄露连接的定义时间(要超过最大事务的处理时间);单位为秒。这里配置为1小时 druidDataSource.setLogAbandoned(true); 移除泄露连接发生是是否记录日志 druidDataSource.init(); this.dynamicTargetDataSources.put(key, druidDataSource); setTargetDataSources(this.dynamicTargetDataSources);// 将map赋值给父类的TargetDataSources super.afterPropertiesSet();// 将TargetDataSources中的连接信息放入resolvedDataSources管理 log.info(key+"数据源初始化成功"); //log.info(key+"数据源的概况:"+druidDataSource.dump()); return true; } catch (Exception e) { log.error(e + ""); return false; } } // 删除数据源 public boolean delDatasources(String datasourceid) { Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources; if (dynamicTargetDataSources2.containsKey(datasourceid)) { Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances(); for (DruidDataSource l : druidDataSourceInstances) { if (datasourceid.equals(l.getName())) { dynamicTargetDataSources2.remove(datasourceid); DruidDataSourceStatManager.removeDataSource(l); setTargetDataSources(dynamicTargetDataSources2);// 将map赋值给父类的TargetDataSources super.afterPropertiesSet();// 将TargetDataSources中的连接信息放入resolvedDataSources管理 return true; } } return false; } else { return false; } } // 测试数据源连接是否有效 public boolean testDatasource(String key, String driveClass, String url, String username, String password) { try { Class.forName(driveClass); DriverManager.getConnection(url, username, password); return true; } catch (Exception e) { return false; } } @Override public void setDefaultTargetDataSource(Object defaultTargetDataSource) { super.setDefaultTargetDataSource(defaultTargetDataSource); this.dynamicDefaultTargetDataSource = defaultTargetDataSource; } /** * @param debug * the debug to set */ public void setDebug(boolean debug) { this.debug = debug; } /** * @return the debug */ public boolean isDebug() { return debug; } /** * @return the dynamicTargetDataSources */ public Map<Object, Object> getDynamicTargetDataSources() { return dynamicTargetDataSources; } /** * @param dynamicTargetDataSources * the dynamicTargetDataSources to set */ public void setDynamicTargetDataSources(Map<Object, Object> dynamicTargetDataSources) { this.dynamicTargetDataSources = dynamicTargetDataSources; } /** * @return the dynamicDefaultTargetDataSource */ public Object getDynamicDefaultTargetDataSource() { return dynamicDefaultTargetDataSource; } /** * @param dynamicDefaultTargetDataSource * the dynamicDefaultTargetDataSource to set */ public void setDynamicDefaultTargetDataSource(Object dynamicDefaultTargetDataSource) { this.dynamicDefaultTargetDataSource = dynamicDefaultTargetDataSource; } public void createDataSourceWithCheck(DataSource dataSource) throws Exception { String datasourceId = dataSource.getDatasourceId(); log.info("正在检查数据源:"+datasourceId); Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources; if (dynamicTargetDataSources2.containsKey(datasourceId)) { log.info("数据源"+datasourceId+"之前已经创建,准备测试数据源是否正常..."); //DataSource druidDataSource = (DataSource) dynamicTargetDataSources2.get(datasourceId); DruidDataSource druidDataSource = (DruidDataSource) dynamicTargetDataSources2.get(datasourceId); boolean rightFlag = true; Connection connection = null; try { log.info(datasourceId+"数据源的概况->当前闲置连接数:"+druidDataSource.getPoolingCount()); long activeCount = druidDataSource.getActiveCount(); log.info(datasourceId+"数据源的概况->当前活动连接数:"+activeCount); if(activeCount > 0) { log.info(datasourceId+"数据源的概况->活跃连接堆栈信息:"+druidDataSource.getActiveConnectionStackTrace()); } log.info("准备获取数据库连接..."); connection = druidDataSource.getConnection(); log.info("数据源"+datasourceId+"正常"); } catch (Exception e) { log.error(e.getMessage(),e); //把异常信息打印到日志文件 rightFlag = false; log.info("缓存数据源"+datasourceId+"已失效,准备删除..."); if(delDatasources(datasourceId)) { log.info("缓存数据源删除成功"); } else { log.info("缓存数据源删除失败"); } } finally { if(null != connection) { connection.close(); } } if(rightFlag) { log.info("不需要重新创建数据源"); return; } else { log.info("准备重新创建数据源..."); createDataSource(dataSource); log.info("重新创建数据源完成"); } } else { createDataSource(dataSource); } } private void createDataSource(DataSource dataSource) throws Exception { String datasourceId = dataSource.getDatasourceId(); log.info("准备创建数据源"+datasourceId); String databasetype = dataSource.getDatabasetype(); String username = dataSource.getUserName(); String password = dataSource.getPassWord(); String url = dataSource.getUrl(); String driveClass = "com.mysql.cj.jdbc.Driver"; // if("mysql".equalsIgnoreCase(databasetype)) { // driveClass = DBUtil.mysqldriver; // } else if("oracle".equalsIgnoreCase(databasetype)){ // driveClass = DBUtil.oracledriver; // } else if("sqlserver2000".equalsIgnoreCase(databasetype)){ // driveClass = DBUtil.sql2000driver; // } else if("sqlserver".equalsIgnoreCase(databasetype)){ // driveClass = DBUtil.sql2005driver; // } if(testDatasource(datasourceId,driveClass,url,username,password)) { boolean result = this.createDataSource(datasourceId, driveClass, url, username, password, databasetype); if(!result) { log.error("数据源"+datasourceId+"配置正确,但是创建失败"); // throw new ADIException("数据源"+datasourceId+"配置正确,但是创建失败",500); } } else { log.error("数据源配置有错误"); // throw new ADIException("数据源配置有错误",500); } } }
ok,然后是我们切换数据源使用的方法, 我们这里采用mybatis注解的方式获取test1数据库里的databasesource 表信息,然后根据我们传入对应的数据源id进行数据源切换:
DataSourceMapper.java :
import com.testdb.dbsource.pojo.DataSource; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.util.List; /** * @Author : JCccc * @CreateTime : 2019/10/23 * @Description : **/ @Mapper public interface DataSourceMapper { @Select("SELECT * FROM databasesource") List<DataSource> get(); }
DBChangeService.java:
import com.testdb.dbsource.pojo.DataSource; import java.util.List; /** * @Author : JCccc * @CreateTime : 2019/10/22 * @Description : **/ public interface DBChangeService { List<DataSource> get(); boolean changeDb(String datasourceId) throws Exception; }
DBChangeServiceImpl.java:
import com.testdb.dbsource.dbconfig.DBContextHolder; import com.testdb.dbsource.dbconfig.DynamicDataSource; import com.testdb.dbsource.mapper.DataSourceMapper; import com.testdb.dbsource.pojo.DataSource; import com.testdb.dbsource.service.DBChangeService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * @Author : JCccc * @CreateTime : 2019/10/22 * @Description : **/ @Service public class DBChangeServiceImpl implements DBChangeService { @Autowired DataSourceMapper dataSourceMapper; @Autowired private DynamicDataSource dynamicDataSource; @Override public List<DataSource> get() { return dataSourceMapper.get(); } @Override public boolean changeDb(String datasourceId) throws Exception { //默认切换到主数据源,进行整体资源的查找 DBContextHolder.clearDataSource(); List<DataSource> dataSourcesList = dataSourceMapper.get(); for (DataSource dataSource : dataSourcesList) { if (dataSource.getDatasourceId().equals(datasourceId)) { System.out.println("需要使用的的数据源已经找到,datasourceId是:" + dataSource.getDatasourceId()); //创建数据源连接&检查 若存在则不需重新创建 dynamicDataSource.createDataSourceWithCheck(dataSource); //切换到该数据源 DBContextHolder.setDataSource(dataSource.getDatasourceId()); return true; } } return false; } }
注意认真看看上面的changeDb这个方法里面的代码,这就是后续手动切换调用的方法。
接下来,写相关操作user表的代码,因为user表分别在test2、test3数据库里,这样用于我们切换到test2或者test3数据库操作这些数据。
User.java:
import lombok.Data; import lombok.ToString; /** * @Author : JCccc * @CreateTime : 2019/10/22 * @Description : **/ @Data @ToString public class User { String userName; String age; }
UserMappper.java (上面简单介绍了下使用注解的方式获取表数据,可能有些人不习惯,那么这里也使用传统的mapper.xml方式编写mysql语句):
import com.testdb.dbsource.pojo.User; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * @Author : JCccc * @CreateTime : 2019/10/23 * @Description : **/ @Mapper public interface UserMapper { List<User> queryUserInfo(); }
userMapper.xml(注意namespace命名空间对应的路径以及user实体类对应的路径):
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.testdb.dbsource.mapper.UserMapper"> <!--查询所有用户信息--> <select id="queryUserInfo" resultType="com.testdb.dbsource.pojo.User"> select * from user </select> </mapper>
顺便一提,我的mapper.xml放在了下面的这个目录结果里,这里的目录结构路径非常关键,因为我们是手动切换数据源,采取了手动配置SqlSessionFactory,需要我们自己去配置mapper.xml路径的,一开始的DruidDBConfig里面有相关的代码,可以去回顾下:
ps:
DruidDBConfig.java
顺带,mybatis-config.xml里面,我做了简单的配置:
其实关于驼峰命名方式开启,我们在手动配置的时候也特意做了配置代码的。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="useGeneratedKeys" value="true"/> <setting name="useColumnLabel" value="true"/> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> </configuration>
到这里,我们已经可以开始测试,
创建UserController.java,写个简单的测试接口:
import com.testdb.dbsource.dbconfig.DBContextHolder; import com.testdb.dbsource.pojo.User; import com.testdb.dbsource.service.DBChangeService; import com.testdb.dbsource.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * @Author : JCccc * @CreateTime : 2019/10/23 * @Description : **/ @RestController public class UserController { @Autowired private DBChangeService dbChangeServiceImpl; @Autowired UserService userService; /** * 查询所有 * @return */ @GetMapping("/test") public String test() throws Exception { //切换到数据库dbtest2 String datasourceId="dbtest2"; dbChangeServiceImpl.changeDb(datasourceId); List<User> userList= userService.queryUserInfo(); System.out.println(userList.toString()); //再切换到数据库dbtest3 dbChangeServiceImpl.changeDb("dbtest3"); List<User> userList3= userService.queryUserInfo(); System.out.println(userList3.toString()); //切回主数据源 DBContextHolder.clearDataSource(); return "ok"; } }
主要看代码注释,调用整合出来的changeDb方法,通过传送数据源id (dbtest2)
切换到对应的数据库,然后操作对应数据库。
项目运行起来,调用接口 /test :
然后我们来看看控制台输出内容:
OK,非常简单顺利,教程就到此。
补充该实战教学的事务相关介绍配置和介绍:
实现动态数据源事务,找到该篇项目实例中的DruidDBConfig.java ,
将 我们实现的动态数据源加载类DynamicDataSource 添加到数据事务管理器里:
@Bean public DataSourceTransactionManager transactionManager(DynamicDataSource dynamicDataSource) { return new DataSourceTransactionManager(dynamicDataSource); }
事务测试 (单个数据源的事务)
接着,给我们的新增User方法里面,故意搞个异常出来,测试下回滚是否生效:
在不指定任何异常时,使用注解事务,默认只对RuntimeException异常生效,所以咱们简单点,在啥时候想回滚,咱们就手动丢个RuntimeException异常出来。
写个测试接口,给db2 插入一条user信息,看看事务回滚情况:
/** * 添加 * @return */ @GetMapping("/addTest") public String addTest() throws Exception { //切换到数据库dbtest2 String datasourceId="dbtest2"; dbChangeServiceImpl.changeDb(datasourceId); User user2=new User(); user2.setUserName("db2用户"); user2.setAge("11"); userService.insertUser(user2); //切回主数据源 DBContextHolder.clearDataSource(); return "ok"; }
调用该接口,可以看到在未抛出异常时,可以看到插入根据影响行数,提示是成功了,但后面咱们抛异常触发了事务回滚:
看下数据库没出现新增的数据,事务回滚成功:
事务测试 (多个数据源的事务)
可以上图这个场景,就是测试切换不同数据源的时候,目前这两个数据源的插入方法都开启了事务,我们把test2的插入User方法加了故意抛出异常触发事务的代码,看看对于不同数据源事务触发情况:
看看调用该接口,控制台的输出:
再看看数据库情况,
test2数据库,
test3数据库,
可以看到在多个数据源的时候,还是只有单独的数据自己的事务起作用了(怎么解决这种情况? 文章末尾有介绍)。
如果只是用于主从数据库两个数据源的业务场景,那么该篇非常适合使用,因为只需保证主的事务,从库会同步数据。
而且如果仅仅是为了满足主从/读写的场景,大可不必从数据库读取数据源,使用AOP方式读取配置文件的数据源即可满足业务场景
那该篇的动态数据源实战适合哪些场景呢?
一.业务场景需要 很多个不同数据源,进行数据获取,不仅仅是两个,这样一来在配置文件配置是基本不可取的。
二.进行多从数据源数据获取,加上逻辑分析筛选后, 插入或更新 某个数据库,只需为该数据库事务进行负责。
例如我的使用场景:
公司各个小项目很多a,b,c,d,e,都用着自己项目的数据库。
而新项目接口需求,收到第三方的调用后,需要到a,b,c,d,e系统的数据库里读取出不同的核心数据,然后进行业务逻辑分析,然后生成新的订单,插入到新项目的主数据库里,并返回结果给第三方,我只需要对一个数据进行事务管理。
那如果就是想多个数据源事务可以一块回滚呢? 难道就没解决方案了吗?
很可以,就是需要有这种精神,不管你目前的业务场景有没有触及。值得敬佩的你,请看:
并不然,使用JTA分布式事务即可解决。
请看我这篇:
Springboot 整合druid+mybatis+jta分布式事务+多数据源aop注解动态切换
PS: 这篇文章我很久前写的,日常搬砖也比较忙,但是从评论里包括我其他的动态切换数据源文章里面得知,大家使用mybatis-plus来开发还是比较多的。
回归这个问题,那么在该篇文章里,如果使用 mybatis-plus 的话,为了保证使用,需要做什么调整呢?
1. 找到配置文件
span,[object Object],2.把本文原先的 SqlSessionFactoryBean 调整为使用 MybatisSqlSessionFactoryBean ,其实也就是说换一下sqlSessionFactory()这个方法的代码即可:,span,[object Object]配置文件 代码:
@Bean public SqlSessionFactory sqlSessionFactory() throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dynamicDataSource()); sqlSessionFactoryBean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/*.xml")); // 设置mybatis的主配置文件 ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); Resource mybatisConfigXml = resolver.getResource("classpath:mybatis/mybatis-config.xml"); sqlSessionFactoryBean.setConfigLocation(mybatisConfigXml); return sqlSessionFactoryBean.getObject(); }
ok,我们来简单写个接口测试一下,
这里新建一个Mapper继承mybatis-plus里面的BaseMapper,service层我就省略了。
然后写个测试接口,从主数据源分别切换到db2和db3 ,分别插入一下数据:
import com.testdb.dbsource.dbconfig.DBContextHolder; import com.testdb.dbsource.pojo.User; import com.testdb.dbsource.service.DBChangeService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @Author : JCccc * @CreateTime : 2020/10/10 * @Description : **/ @RestController public class UserPlusController { @Autowired private DBChangeService dbChangeServiceImpl; @Autowired UserPlusMapper userMapper; @GetMapping("/testPlus") public void testPlus() throws Exception { User User2 = new User(); User2.setUserName("TEST insert db 2"); User2.setAge("2"); //切换到数据库dbtest2 String datasourceId2="dbtest2"; dbChangeServiceImpl.changeDb(datasourceId2); int effectNum2 = userMapper.insert(User2); System.out.println("db2添加后的影响行数:"+effectNum2); //切换到数据库dbtest3 String datasourceId3="dbtest3"; dbChangeServiceImpl.changeDb(datasourceId3); User User3 = new User(); User3.setUserName("TEST insert db 3"); User3.setAge("3"); int effectNum3 = userMapper.insert(User3); System.out.println("db3添加后的影响行数:"+effectNum3); //切回主数据源 DBContextHolder.clearDataSource(); } }
调用接口,控制台打印:
然后是数据库分别都有了数据:
ok,那这个对于mybatis-plus的使用补充就到这。