springboot对于数据源的动态管理实践(代码片段)

trey trey     2022-12-03     244

关键词:

springboot对于数据源的动态管理实践

需求

用户通过界面配置数据源,可能包括oracle、mysql、mongodb等等;配置完数据源后,支持对于具体数据源进行实时sql查询操作。

一般来说,如果不考虑性能,最简单的实现就是每次进行sql connecntion操作,实时连接查询。很显然,这样的操作,没有利用到数据库连接池。性能不好。所以 本篇博客的具体实现就是利用spring框架提供的AbstractRoutingDataSource类来实现动态的添加数据源。最终实现不同数据源的请求,都通过数据库连接池来实现。

本文通过原理剖析、实践操作,最后附上demo代码的github项目地址。

原理剖析

前面提到了spring框架提供的AbstractRoutingDataSource类,能够帮助我门实现动态的添加数据源。所以这边我们就先一起来看下这个接口的内部实现。

首先我们会发现这是一个抽象类,然后我们看一下它的依赖关系。可以看到两个关键,一个是它实现了DataSource接口,这个接口我们很熟悉,就是进行sql连接的核心类。另外一个是它实现了InitializingBean接口,这个接口的作用是进行初始化。
技术图片

首先我们看下这个类的注释,好的注释基本能够说明很多事情。
技术图片

我来蹩脚翻一下,首先这个类是DataSource的一个抽象实现,它基于一个lookup key来实现目标数据源的调用。然后这个key一般(但是也不一定)通过一些线程绑定的事务上下文来确定。ok,我们能够掌握一个关键点,就是这个类是基于lookup key来进行目标数据源调用的。

前面提到AbstractRoutingDataSource类实现了InitializingBean接口,我们来看下这个接口的具体实现:
技术图片

方法不复杂,简单介绍下:

  1. targetDataSources是一个map,存储所有的目标数据源,如果为空,则抛出异常。
  2. resolvedDataSources表示一个经过解析后的所有数据源,看命名应该也能看出来。
  3. 最后把默认数据源也解析好存储起来。

这里为什么需要这么处理?我们可以看下resolveSpecifiedDataSource()这个解析方法。它这里做了一个判断,如果targetDataSources的value是string则会从Jndi数据源查找。
技术图片

通过上述分析,我们知道一点,就是这个AbstractRoutingDataSource类必须有默认的数据源。否则初始化阶段就会报错。然后我们也发现它内部的targetDataSources其实是一个map,存储的就是lookup key 以及对应的DataSource对象。很自然的,我们可以想到,其实 所谓的动态数据源切换,其实就是 其内部 缓存了所有目标数据源,并且通过对应的key能够找到对应的数据源。

继续查看代码,我们需要找寻下,哪边去触发动态数据源的切换动作。我们知道这个AbstractRoutingDataSource是个抽象类,它是需要子类为其实现具体的方法的,所以我们就去看下它的抽象方法。可以看到,这个类就一个抽象方法:determineCurrentLookupKey()
技术图片

我们看下 调用这个抽象方法的地方,继续找线索。发现只有这个determineTargetDataSource()进行了调用。我们一起过下这个方法。方法的实现依然不复杂,就是获取对应的lookupkey,然后根据key去resolvedDataSource里获取解析的数据源对象,返回。
技术图片

很明显,这就是我们想要的答案。动态数据源的切换,就是子类实现具体lookupKey的实现,然后就可以切换到对应的数据源对象了。这个AbstractRoutingDataSource,其实就是做的这个事情。

实践

现在我们了解了AbstractRoutingDataSource类的作用,可以结合我们的原始需求来看下具体应该如何进行。

原始需求

首先我们再回顾下原始需求,就是现在任何一个数据源的配置信息和sql过来,我们服务能够实现动态添加数据源到数据库连接池,进行数据库的查询。等到下次再次访问时,可以动态切换来查询。另外,如果注册的数据源,长时间不使用,我们也需要支持过期失效的操作。

上面的原始需求,简单概括下来,就是以下几个步骤:

  1. 对于一个新的数据源,需要实现动态添加
  2. 对于一个已经添加过的数据源,需要实现动态切换
  3. 对于一个长时间不使用的数据源,需要动态删除

根据上面我们对AbstractRoutingDataSource类的原理剖析,我们会发现,其实对于上述需求1和2,都是在对AbstractRoutingDataSource类里管理的数据源进行添加或者切换。所以我们需要基于这个关键类来实现我们的需求。

数据源初始化Bean

前面我们在分析AbstractRoutingDataSource类的时候,有提到,必须设置一个默认数据源。这里我们利用spring boot的配置文件进行默认数据源的配置。
技术图片

然后通过Configuration类来进行配置,具体代码如下。利用@Bean进行动态数据源类的注册。在配置的核心实现里,需要将默认的配置数据源注册上去。其中lookupKey这里随便取了个默认名,只要保证后面动态添加的数据源与其不重复就ok了。

@Configuration
public class DataSourceConfigurer
    
    private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceConfigurer.class);

    @Value("$spring.datasource.url")
    private String url;
    @Value("$spring.datasource.username")
    private String username;
    @Value("$spring.datasource.password")
    private String password;
    @Value("$spring.datasource.driverClassName")
    private String driverClassName;

    public Map<String, Object> getProperties() 
        Map<String, Object> map = new HashMap<>();
        map.put("driverClassName", driverClassName);
        map.put("url", url);
        map.put("username", username);
        map.put("password", password);
        return map;
    

    public DataSource dataSource() 
        DataSource dataSource = null;
        try 
            dataSource = DruidDataSourceFactory.createDataSource(getProperties());
         catch (Exception e) 
            LOGGER.error("Create DataSource Error : ", e);
            throw new RuntimeException();
        
        return dataSource;
    

    /**
     * 注册动态数据源
     * 
     * @return
     */
    @Bean("dynamicDataSource")
    public DynamicRoutingDataSource dynamicDataSource() 
        DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>(1);
        dataSourceMap.put("default_db", dataSource());
        // 设置默认数据源
        dynamicRoutingDataSource.setDefaultTargetDataSource(dataSource());
        dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
        return dynamicRoutingDataSource;
    

数据源的动态添加&动态切换

这里的主要做法就是继承AbstractRoutingDataSource类。
首先我们需要需要实现determineCurrentLookupKey()方法。前面的分析,我们已经知道该方法是获取目标数据源的lookupKey。所以这里涉及到lookupKey在上下文的存储。

这里我们使用ThreadLocal来实现lookupKey的管理。ThreadLocal适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。所以很适合我们这里的场景。下面是具体的实现代码。
技术图片

回到最原始的需求,结合目前获取的信息,我们可以整理下动态数据源查询的具体步骤:

  1. 根据数据源配置信息,判断目标数据源是否已经存在,存在执行2,不存在执行3.
  2. 根据配置信息,生成lookupKey,存储在上下文环境。执行4
  3. 执行数据源添加操作。通过生成lookupKey,存储在上下文环境。执行4
  4. 进入AbstractRoutingDataSource的具体实现类,获取lookupKey,根据lookupKey获取目标数据源。

以上步骤的核心,我们通过代码一步步演示。首先通过如下的代码,实现目标数据源的判断操作。这个判断也可以通过切面来实现,这边只是简单的集成在业务实现内部。

DynamicDataSourceContextHolder.setDataSourceKey(sqlInfoVO.getProjectId());
        if(!DynamicRoutingDataSource.isExistDataSource(sqlInfoVO.getProjectId())) 
            dynamicDataSource.addDataSource(sqlInfoVO);
        

添加数据源的方法,主要就是先构建Connection连接进行测试,然后添加进AbstractRoutingDataSource的目标数据源map里。切记,添加完毕后,要进行afterPropertiesSet()操作,相当于刷新操作。

    public synchronized boolean addDataSource(SqlInfoVO sqlInfoVO) 
        try 
            Connection connection = null;
            // 排除连接不上的错误
            try  
                Class.forName(sqlInfoVO.getDriverClassName());
                connection = DriverManager.getConnection(
                        sqlInfoVO.getUrl(),
                        sqlInfoVO.getUsername(),
                        sqlInfoVO.getPassword());
             catch (Exception e) 
                e.printStackTrace();
                return false;
             finally 
                if (connection != null && !connection.isClosed()) 
                    connection.close();
                
            
            //获取要添加的数据库名
            String projectId = sqlInfoVO.getProjectId();
            if (StringUtils.isBlank(projectId)) 
                return false;
            
            if (DynamicRoutingDataSource.isExistDataSource(projectId)) 
                return true;
            
            DruidDataSource druidDataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(beanToMap(sqlInfoVO));
            druidDataSource.init();
            Map<Object, Object> targetMap = DynamicRoutingDataSource.targetTargetDataSources;
            targetMap.put(projectId, druidDataSource);
            this.setTargetDataSources(targetMap);
            this.afterPropertiesSet();
            logger.info("dataSource [] has been added" + projectId);
         catch (Exception e) 
            logger.error(e.getMessage());
            return false;
        
        return true;
    

关于获取lookupKey的实现,应该很容易了,就是通过ThreadLocal去上下文获取。这里看到进行了updateTimer()操作,这个操作是为了数据源的连接过期断开而设定的。

      // 每次设置当前数据源key时,更新timeMap中的时间
        String lookupKey = DynamicDataSourceContextHolder.getDataSourceKey();
        updateTimer(lookupKey);
        return lookupKey;

利用定时任务管理数据源的过期

根据以上的实现,其实我们已经实现了数据源的动态添加和切换操作。然而我们还需要考虑下,对于部分很少甚至只连接一次的数据源,其实是没必要一直缓存的。所以我们需要实现一个过期检测的操作。
具体的功能,这里实现的思路是:

  1. 在设置目标数据源的Map时,通过copy存储一份带时间戳的数据源对象。
  2. 每次数据源查询时,都会去更新下对应数据源的时间戳。
  3. 设置一个过期时间,然后利用一个定时任务,来判断数据源是否过期。如果过期,则进行移除。

这里面核心的操作涉及到三块,首先是需要构建一个带有时间戳的数据源扩展类:

public class DynamicDataSourceTimer 

    /**
     * 空闲时间周期。超过这个时长没有访问的数据库连接将被释放。默认为10分钟。
     */
    private static long idlePeriodTime = 10 * 60 * 1000;

    /**
     * 动态数据源
     */
    private DataSource dds;

    /**
     * 上一次访问的时间
     */
    private long lastUseTime;

    public DynamicDataSourceTimer(DataSource dds) 
        this.dds = dds;
        this.lastUseTime = System.currentTimeMillis();
    

    /**
     * 更新最近访问时间
     */
    public void refreshTime() 
        lastUseTime = System.currentTimeMillis();
    

    /**
     * 检测数据连接是否超时关闭。
     *
     * @return true-已超时关闭; false-未超时
     */
    public boolean checkAndClose() 

        if (System.currentTimeMillis() - lastUseTime > idlePeriodTime)
        
            return true;
        

        return false;
    

构建一个定时任务,这里我们利用spring的@Schedula注解实现:

/**
     * 通过定时任务周期性清除不使用的数据源
     */
    @Scheduled(initialDelay= 10 * 60 * 1000, fixedRate= 10 * 60 * 1000)
    public void clearTask() 
        // 遍历timetMap,判断
        clearIdleDDS();
    

    private void clearIdleDDS() 
        timerMap.forEach((k,v) -> 
            if(v.checkAndClose()) 
                delDatasources(k.toString());
            
        );

实现一个移除数据源的方法,其核心依然在于更新AbstractRoutingDataSource的AbstractRoutingDataSource:

    // 删除数据源
    public synchronized boolean delDatasources(String datasourceid) 
        Map<Object, Object> dynamicTargetDataSources2 = DynamicRoutingDataSource.targetTargetDataSources;
        if (dynamicTargetDataSources2.containsKey(datasourceid)) 
            Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();
            for (DruidDataSource l : druidDataSourceInstances) 
                if (datasourceid.equals(l.getName())) 
                    System.out.println(l);
                    dynamicTargetDataSources2.remove(datasourceid);
                    DruidDataSourceStatManager.removeDataSource(l);
                    // 将map赋值给父类的TargetDataSources
                    setTargetDataSources(dynamicTargetDataSources2);
                    // 将TargetDataSources中的连接信息放入resolvedDataSources管理
                    super.afterPropertiesSet();
                    return true;
                
            
            return false;
         else 
            return false;
        
    

完整代码实现

基于上述的实现和分析,我整理了一个完整的demo项目:
https://github.com/trey-tao/spring-dynamic-datasource-demo

springboot+springsecurity数据库动态管理用户角色权限(代码片段)

序: 本文使用springboot+mybatis+SpringSecurity实现数据库动态的管理用户、角色、权限管理本文细分角色和权限,并将用户、角色、权限和资源均采用数据库存储,并且自定义滤器,代替原有的FilterSecurityInterceptor过滤器, 并... 查看详情

springboot定时任务动态管理通用解决方案(代码片段)

一、功能说明SpringBoot的定时任务的加强工具,实现对SpringBoot原生的定时任务进行动态管理,完全兼容原生@Scheduled注解,无需对原本的定时任务进行修改二、快速使用具体的功能已经封装成SpringBoot-starter即插即用<dependency&g... 查看详情

springboot定时任务动态管理通用解决方案(代码片段)

一、功能说明SpringBoot的定时任务的加强工具,实现对SpringBoot原生的定时任务进行动态管理,完全兼容原生@Scheduled注解,无需对原本的定时任务进行修改二、快速使用具体的功能已经封装成SpringBoot-starter即插即用<dependency&g... 查看详情

springboot定时任务动态管理通用解决方案(代码片段)

一、功能说明SpringBoot的定时任务的加强工具,实现对SpringBoot原生的定时任务进行动态管理,完全兼容原生@Scheduled注解,无需对原本的定时任务进行修改二、快速使用具体的功能已经封装成SpringBoot-starter即插即用<dependency&g... 查看详情

springboot动态数据源(代码片段)

由于项目中要用到springboot结合mybatis做一个动态的数据源,所以自己做了一个,也踩了很多坑,这里把成果分享出来。如果是1.x的springboot版本可以看前面的,如果是2.x版本的可以看后面的,2.x版本的更简单方... 查看详情

玩转springboot集成篇(任务动态管理代码篇)(代码片段)

*定时任务管理**/@privateTaskInfoServicetaskInfoService;@publicResultlist(@RequestBodyTaskInfoReqreqVo)@publicResultedit(@RequestBodyTaskInfoReqreqVo)@publicResultpause(IntegertaskId)@publicResultadd(@Reques 查看详情

springboot+easyui+jpa实现动态权限角色的后台管理系统

最近因为多次需要使用easyui的后台管理系统,所以自己写了一个easyui后台管理系统的模版,可修改权限增加角色(这里先放创建数据库和加载菜单,配置拦截器的方法和遇到的问题)1.先创建数据库(我是在本地创建的数据库)资源表:存... 查看详情

springboot集成flyway实现数据库版本控制?

...家介绍一款比较好用的数据库版本控制工具Flyway。在通过SpringBoot构建微服务的过程中,一般情况下在拆分微服务的同时,也会按照系统功能的边界对其依存的数据库进行拆分。在这种情况下,微服务的数据库版本管理对于研发... 查看详情

分布式系统中数据存储方案实践(代码片段)

一、背景简介在项目研发的过程中,对于数据存储能力的依赖无处不在,项目初期,相比系统层面的组件选型与框架设计,由于数据体量不大,在存储管理方面通常容易被轻视,当项目发展进入到中后期阶段,系统的复杂性很大... 查看详情

springboot2.0基础案例(08):集成redis数据库,实现缓存管理(代码片段)

一、Redis简介SpringBoot中除了对常用的关系型数据库提供了优秀的自动化支持之外,对于很多NoSQL数据库一样提供了自动化配置的支持,包括:Redis,MongoDB,Elasticsearch。这些案例整理好后,陆续都会上传Git。SpringBoot2版本,支持的组... 查看详情

第四周《c语言及程序设计》实践项目39动态存储管理与动态数组的实现(代码片段)

【项目1-学生人数没个准】/**Copyright(c)2016,CSDN学院*Allrightsreserved.*文件名称:【项目1-折腾二维数组】.cpp*作者:张易安*完成日期:2016年9月13日*版本号:v1.0**问题描述:输入学生成绩,输出高于平均成绩的... 查看详情

springboot与动态多数据源切换(代码片段)

本文简单的介绍一下基于SpringBoot框架动态多数据源切换的实现,采用主从配置的方式,配置master、slave两个数据库。一、配置主从数据库spring:datasource:type:com.alibaba.druid.pool.DruidDataSourcedriverClassName:com.mysql.cj.jdbc.Driverdruid:#主库数据... 查看详情

springboot动态数据源(多数据源自动切换)

...的方法使用,本文基于注解和AOP的方法实现,在springboot框架的项目中,添加本文实现的代码类后,只需要配置好数据源就 查看详情

springboot配置文件隐私数据脱敏的最佳实践(原理+源码)(代码片段)

大家好!我是小富~这几天公司在排查内部数据账号泄漏,原因是发现某些实习生小可爱居然连带着账号、密码将源码私传到GitHub上,导致核心数据外漏,孩子还是没挨过社会毒打,这种事的后果可大可小。说起这个我是比较有... 查看详情

springboot使用动态数据源(代码片段)

...清一下,这里说的动态数据源并不是多数据源,SpringBoot是支持多数据源的,多数据源是指在SpringBoot上下文中可以配置多个DataSource,有一个是 查看详情

springboot+mybatis数据源动态切换与加载

...ataSourceUtil类保存projectId与dataSourceId的对应关系  springboot启动时的配置类配置默认datasource  可以看到,已经实现了数据源的动态切换 查看详情

springboot:事物管理

SpringBoot整合事物管理  Springboot默认集成事物,只主要在方法上加上@Transactional即可。多数据源情况下事物怎么管理事物  对于这种传统的分布式事物管理,采用jta+atomikos 分布式事物管理。Atomikos是一个为Java平台提供增值... 查看详情

springboot配置文件隐私数据脱敏的最佳实践(原理+源码)(代码片段)

这几天公司在排查内部数据账号泄漏,原因是发现某些实习生小可爱居然连带着账号、密码将源码私传到GitHub上,导致核心数据外漏,孩子还是没挨过社会毒打,这种事的后果可大可小。说起这个我是比较有感触... 查看详情