1.前言
本篇文章主要是完成springboot整合多数据源,一般都用来解决那些比较复杂需要连接不同的数据库来支持业务,可以做到不同的接口连接不同的数据库来请求数据,在配置方面不同于其他人的文章在application.yml文件里面配置两三个数据库连接,当然这种只适用于数据库数量不多且固定的情况。针对数据库动态增加的情况无能为力,当你有很多数据库需要连接时,这篇文章就为你提供了解决方法。
2.详细的代码实现
1.数据源配置管理类(DataSourceConfig.java)**
package com.unicom.microserv.cap.config.database;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
@MapperScan(basePackages = "xxxx.xxx.*mapper", value = "sqlSessionFactory")
public class DataSourceConfig {
static Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);
/**
* 根据配置参数创建数据源
*
* @return 数据源
*/
@Bean(name = "dataSource")
//prefix 这里写yml文件里面的spring.datasource.druid
@ConfigurationProperties(prefix = "spring.druid")
public DataSource getDataSource() {
DataSourceBuilder builder = DataSourceBuilder.create();
builder.type(DynamicDataSource.class);
return builder.build();
}
/**
* 创建会话工厂。
*
* @param dataSource 数据源
* @return 会话工厂
*/
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
try {
return bean.getObject();
} catch (Exception e) {
logger.info("异常信息:",e);
return null;
}
}
}
2.druid连接池的缺省配置类
@Configuration
public class DruidDefaultConfig {
/**
* druid连接池的缺省配置
* @return
*/
public static Properties getDefaultProperties(){
Properties properties = new Properties();
//初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
properties.setProperty("druid.initialSize", "1");
//最小连接池数量
properties.setProperty("druid.minIdle", "0");
//最大连接池数量
properties.setProperty("druid.maxActive", "8");
//是否缓存preparedStatement,也就是PSCache。
// PSCache对支持游标的数据库性能提升巨大,比如说oracle。
// 在mysql下建议关闭。
properties.setProperty("druid.poolPreparedStatements", "true");
//要启用PSCache,必须配置大于0,
// 当大于0时,poolPreparedStatements自动触发修改为true。
// 在Druid中,不会存在Oracle下PSCache占用内存过多的问题,
// 可以把这个数值配置大一些,比如说100
properties.setProperty("druid.maxPoolPreparedStatementPerConnectionSize", "10");
//用来检测连接是否有效的sql,要求是一个查询语句,
// 常用select 'x'。
// 如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
//properties.setProperty("druid.validationQuery", "select 12");
//申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
properties.setProperty("druid.testOnBorrow", "false");
//建议配置为true,不影响性能,并且保证安全性。
// 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,
// 执行validationQuery检测连接是否有效。
properties.setProperty("druid.testWhileIdle", "true");
// 连接池中的minIdle数量以内的连接,
// 空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。
properties.setProperty("druid.keepAlive", "false");
//有两个含义:
//1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
//2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
properties.setProperty("druid.timeBetweenEvictionRunsMillis", "60000");
// 连接保持空闲而不被驱逐的最小时间
properties.setProperty("druid.minEvictableIdleTimeMillis", "18000000");
properties.setProperty("druid.useGlobalDataSourceStat", "false");
properties.setProperty("druid.filters", "stat");
return properties;
}
}
3、定义动态数据源**
首先增加一个数据库标识类,用于区分不同的数据库(DBIdentifier.java)
/**
* Created by Administrator on 2019/8/5.
* 首先增加一个数据库标识类,用于区分不同的数据库访问。
*由于我们为不同的project创建了单独的数据库,所以使用项目编码作为数据库的索引。
* 而微服务支持多线程并发的,采用线程变量。
*/
public class DBIdentifier {
private static ThreadLocal<String> projectCode = new ThreadLocal<String>();
public static String getProjectCode() {
return projectCode.get();
}
public static void setProjectCode(String code) {
projectCode.set(code);
}
}
4.从DataSource派生了一个DynamicDataSource,在其中实现数据库连接的动态切换(DynamicDataSource.java)
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidPooledConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import java.sql.SQLException;
public class DynamicDataSource extends DruidDataSource {
private static Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
public DruidPooledConnection getConnection() {
int projectCode = DBIdentifier.getProjectCode();
//1、获取数据源
DruidDataSource dds = DDSHolder.instance().getDDS(projectCode);
//2、如果数据源不存在则创建
if (dds == null) {
logger.info("新增数据源");
DruidDataSource newDDS = initDDS(projectCode);
DDSHolder.instance().addDDS(projectCode, newDDS);
}
dds = DDSHolder.instance().getDDS(projectCode);
try {
return dds.getConnection();
} catch (SQLException e) {
logger.info("数据库连接失败:", e);
}
return null;
}
/**
* 以当前数据对象作为模板复制一份。
*
* @return dds
*/
private DruidDataSource initDDS(int projectCode) {
DruidDataSource dds = new DruidDataSource();
// 3、设置数据库名称和IP(一般来说,端口和用户名、密码都是统一固定的)
String urlFormat = this.getUrl();
logger.info("获取配置文件数据库连接url地址模板:" + urlFormat);
String url = String.format(urlFormat, ProjectDBMgr.instance().getDBType(projectCode), ProjectDBMgr.instance().getDBIP(projectCode),
ProjectDBMgr.instance().getDBName(projectCode));
String userName = ProjectDBMgr.instance().getUsername(projectCode);
String passWord = ProjectDBMgr.instance().getPassword(projectCode);
String driverName = ProjectDBMgr.instance().getDriverClassName(projectCode);
logger.info("数据库连接的url地址:" + url);
dds.setUrl(url);
dds.setUsername(userName);
dds.setPassword(passWord);
dds.setDriverClassName(driverName);
// 加载缺省配置
dds.configFromPropety(DruidDefaultConfig.getDefaultProperties());
// 如果不使用缺省配置,可以覆盖,如下:
dds.setMaxActive(300);
return dds;
}
}
5.通过DDSTimer控制数据连接释放(DDSTimer.java)
import com.alibaba.druid.pool.DruidDataSource;
import java.util.HashMap;
import java.util.Map;
public class DDSTimer {
/**
* 空闲时间周期。超过这个时长没有访问的数据库连接将被释放。默认为10分钟。
*/
private static long idlePeriodTime = 10 * 60 * 1000L;
private static Map<Integer, Long> ddsTimeMap = new HashMap<>();
Map<Integer, DDSTimer> ddsMap = DDSHolder.instance().getDdsMap();
/**
* 动态数据源
*/
private DruidDataSource dds;
/**
* 上一次访问的时间
*/
private long lastUseTime;
public DDSTimer(DruidDataSource dds, int projectCode) {
this.dds = dds;
this.lastUseTime = System.currentTimeMillis();
ddsTimeMap.put(projectCode, lastUseTime);
}
/**
* 更新最近访问时间
*/
public void refreshTime(int projectCode) {
lastUseTime = System.currentTimeMillis();
ddsTimeMap.put(projectCode, lastUseTime);
}
/**
* 检测数据连接是否超时关闭。
*
* @return true-已超时关闭; false-未超时
*/
public boolean checkAndClose(int projectCode) {
if (!ddsTimeMap.containsKey(projectCode)) {
return false;
}
if (System.currentTimeMillis() - ddsTimeMap.get(projectCode) > idlePeriodTime) {
dds = ddsMap.get(projectCode).getDds();
dds.close();
return true;
}
return false;
}
public DruidDataSource getDds() {
return dds;
}
}
6.通过DDSHolder来管理不同的数据源,提供数据源的添加、查询功能(DDSHolder.java)
import com.alibaba.druid.pool.DruidDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* 通过DDSHolder来管理不同的数据源,提供数据源的添加、查询功能
*/
public class DDSHolder {
static Logger logger = LoggerFactory.getLogger(DDSHolder.class);
/**
* 管理动态数据源列表。<工程编码,数据源>
*/
private static Map<Integer, DDSTimer> ddsMap = new HashMap<Integer, DDSTimer>();
private DDSHolder() {
}
/*
* 获取单例对象
*/
public static DDSHolder instance() {
return DDSHolderBuilder.instance;
}
/**
* 添加动态数据源。
*
* @param projectCode 项目编码
* @param dds dds
*/
public synchronized void addDDS(int projectCode, DruidDataSource dds) {
DDSTimer ddst = new DDSTimer(dds, projectCode);
ddsMap.put(projectCode, ddst);
}
/**
* 查询动态数据源
*
* @param projectCode 项目编码
* @return dds
*/
public synchronized DruidDataSource getDDS(int projectCode) {
if (ddsMap.containsKey(projectCode)) {
DDSTimer ddst = ddsMap.get(projectCode);
ddst.refreshTime(projectCode);
return ddst.getDds();
}
return null;
}
/**
* 清除超时无人使用的数据源。
*/
public synchronized void clearIdleDDS() {
Iterator<Map.Entry<Integer, DDSTimer>> iter = ddsMap.entrySet().iterator();
for (; iter.hasNext(); ) {
Map.Entry<Integer, DDSTimer> entry = iter.next();
if (entry.getValue().checkAndClose(entry.getKey())) {
logger.info("清除超时无人使用的数据源");
iter.remove();
}
}
}
public Map<Integer, DDSTimer> getDdsMap() {
return ddsMap;
}
/**
* 单例构件类
*
* @author elon
* @version 2020年8月14日
*/
private static class DDSHolderBuilder {
private static DDSHolder instance = new DDSHolder();
}
}
7.定时器任务TaskSchedule用于定时清除空闲的数据源(TaskSchedule.java)
@Component
@EnableScheduling // 通过@EnableScheduling注解开启对计划任务的支持
public class TaskSchedule {
static Logger logger = LoggerFactory.getLogger(TaskSchedule.class);
/**
* 定时处理过期信息
*/
@Scheduled(cron = "0 0/1 * * * ?")
public void clearIdleTimerTask() {
DDSHolder.instance().clearIdleDDS();
List<Integer> configs = totalProcessService.selectConfig();
if (!configs.isEmpty()) {
Map<Integer, HostEntity> hostMap = ProjectHostMgr.instance().getHostMap();
configs.forEach(config -> {
logger.info("清除失效主机信息:" + config);
hostMap.remove(config);
});
}
}
}
8.管理项目编码与数据库IP和名称的映射关系(ProjectDBMgr.java)
import java.util.HashMap;
import java.util.Map;
/**
* 项目数据库管理。提供根据项目编码查询数据库名称和IP的接口。
*/
public class ProjectDBMgr {
/**
* 保存项目编码与数据名称的映射关系。这里是硬编码,实际开发中这个关系数据可以保存到redis缓存中;
* 新增一个项目或者删除一个项目只需要更新缓存。到时这个类的接口只需要修改为从缓存拿数据。
*/
private static Map<Integer, DBData> depDBMap = new HashMap<>();
/**
* * 保存项目编码与数据库IP的映射关系。
*/
public ProjectDBMgr(int key, DBData dbData) {
depDBMap.put(key, dbData);
}
public ProjectDBMgr() {
}
public static ProjectDBMgr instance() {
return ProjectDBMgrBuilder.instance;
}
// 实际开发中改为从缓存获取
public String getDBType(int projectCode) {
if (depDBMap.containsKey(projectCode)) {
return depDBMap.get(projectCode).getDBType();
}
return "";
}
// 实际开发中改为从缓存获取
public String getDBName(int projectCode) {
if (depDBMap.containsKey(projectCode)) {
return depDBMap.get(projectCode).getSourcePath();
}
return "";
}
//实际开发中改为从缓存中获取
public String getDBIP(int projectCode) {
if (depDBMap.containsKey(projectCode)) {
return depDBMap.get(projectCode).getConnectionIp();
}
return "";
}
//动态获取密码
public String getPassword(int projectCode) {
if (depDBMap.containsKey(projectCode)) {
return depDBMap.get(projectCode).getConnectionPassword();
}
return "";
}
//动态获取username
public String getUsername(int projectCode) {
if (depDBMap.containsKey(projectCode)) {
return depDBMap.get(projectCode).getConnectionAccount();
}
return "";
}
//动态获取driverClassName
public String getDriverClassName(int projectCode) {
if (depDBMap.containsKey(projectCode)) {
return depDBMap.get(projectCode).getDriverClassName();
}
return "";
}
/**
* 获取容器里的数据源
*
* @return
*/
public static Map<Integer, DBData> getDepDBMap() {
return depDBMap;
}
private static class ProjectDBMgrBuilder {
private static ProjectDBMgr instance = new ProjectDBMgr();
}
}
9.编写数据库访问的mapper(PersonSnapshotHistoryMapper .java)
package com.example.demo.mapper;
import com.example.demo.bean.PersonSnapshotHistory;
import com.example.demo.bean.PersonSnapshotHistoryExample;
import java.util.List;
import org.apache.ibatis.annotations.*;
import org.springframework.data.jpa.repository.Query;
@Mapper
public interface PersonSnapshotHistoryMapper {
@Select(value = "SELECT uuid,cardid,cardno FROM PERSON_SNAPSHOT_HISTORY ORDER BY createdAt DESC LIMIT 0,3")
List<PersonSnapshotHistory> selectThreeSnapshot();
}
10.定义查询对象模型(PersonSnapshotHistory .java)
package com.example.demo.controller;
import com.example.demo.bean.PersonSnapshotHistory;
import com.example.demo.config.DBIdentifier;
import com.example.demo.mapper.PersonSnapshotHistoryMapper;
import com.example.demo.util.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* Created by Administrator on 2019/8/5.
*/
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*", maxAge = 3600)
public class PersonSnapshotHistoryController {
@Autowired
PersonSnapshotHistoryMapper personSnapshotHistoryMapper;
/**
* 显示前三条数据
* @return
*/
@RequestMapping("threeSnapshot")
public R selectThreeSnapshot(){
//不同的接口需要请求哪个数据库数据 直接填定义好的数据库编号
DBIdentifier.setProjectCode("5642");
List<PersonSnapshotHistory> list=personSnapshotHistoryMapper.selectThreeSnapshot();
return R.ok("").put("list",list);
}
@RequestMapping("threeSnapshot1")
public R selectThreeSnapshot1(){
DBIdentifier.setProjectCode("5455");
List<PersonSnapshotHistory> list=personSnapshotHistoryMapper.selectThreeSnapshot();
DBIdentifier.setProjectCode("5642");
List<PersonSnapshotHistory> list1=personSnapshotHistoryMapper.selectThreeSnapshot();
list.addAll(list1);
return R.ok("").put("list",list);
}
}
11.启动类配置重点
这样启动springboot项目的时候就不会去连接数据库 ,不然会启动报错
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class,HibernateJpaAutoConfiguration.class,JpaRepositoriesAutoConfiguration.class})
@MapperScan(basePackages = {"com.example.demo.*","com.example.demo.mapper"})
public class SmartCommunityBootApplication {
public static void main(String[] args) {
//DBIdentifier.setProjectCode("5642");
SpringApplication.run(SmartCommunityBootApplication.class, args);
}
}