springboot 使用阿里数据连接池 切换不同数据源

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);    
	}    
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值