基于注解+AOP的多数据源配置学习和应用

前言:在分布式微服务系统中,我们存在这么一种情况,在服务设计之初,进行了分库的设计。例如我们接下来要讲的UPMS用户权限管理服务,在我的设计中,将用户操作日志表和各个业务库分离开了,采用异步消息的方式实现操作日志的记录,这样的设计可以减轻业务库的压力,让操作日志业务(登录、修改密码等日志)的情况无论怎么样都不会影响到主干流程。


Step1:关闭Spring自动装配数据源的操作。

首先,我们还是明确我们的目标:

要操作两个数据库,所以要得到两个数据源。

那么在单个数据源的情况下,SpringBoot会为我们的数据源和连接池进行自动装配,但是此时我们需要建立两个数据源和连接池,Spring就不能为我们自动装配了,所以此时,我们需要关闭Spring的数据源自动装配。

上代码:

上面这段代码,我们在启动SpringBoot时,将DataSourceAutoConfiguration.class自动装配类排除掉,这样Spring就不会再执行自动装配的逻辑了。


Step2:yml配置文件中添加多数据源配置。

我使用的是Nacos配置中心,单数据源配置的代码如下:

单数据源情况下,这里只需要在spring.datasource.druid下直接写配置即可。

接下来我们看一下多数据源该怎么改:

多数据源配置的代码如下:

可以看到,在druid节点下,我们这里配置了两个数据源的子节点配置。

这里我们就完成了我们的配置环节


Step3:代码中配置多数据源并通过注解+AOP的方式实现。(先给出结论,后面有源码解析)

目标:完成共用数据源的抽取,并且在不同的模块中建立多数据源。


Step3-1:创建公共数据源标识。

我们可以在公用的Core模块中的常量类中去定义公用的数据源标识以用于全局数据源的管理。

上代码:

我们在这里启用了一个公共数据源,改数据源标识将会被所有需要使用这个公共数据源的模块所使用。


 Step3-2:通过注册表模式的形式存储每个数据源标识。

我们通过一个静态Map去保存各个数据源标识,这可以看作为一个注册表模式(类似策略模式),后面我们会将该注册表中的数据源标识注册进多数据源路由表中去。

 

注意:这个DataSourceType是跟着业务模块走的,即应写在业务模块中并非Core公用模块中,需要多少个数据源,就往里面加。


Step3-3:创建多数据源路由对象。

数据源标识设置好了,那怎么让Spring能通过我们的标识找到对应的数据库呢?此时我们就需要一个多数据源路由对象,实现Spring提供的AbstractRoutingDataSource抽象类就可以完成我们的数据源选择。

上代码:

我们这里实现了Spring提供的AbstractRoutingDataSource抽象类,并重写了其中的determineCurrentLookupKey方法,而这个方法就是为了筛选我们的数据源的。

我们翻译一下这个方法名:“确定当前查询键”,大概可以猜出这个方法的意思:要把我们的路由查询键返回,而这个路由查询键,就是我们设定的数据库标识。(看到这里可能会有点疑惑,到后面会讲解为什么我们通过路由查询键就能找到对应的数据源对象) 


Step3-4:基于ThreadLocal线程变量在线程中传递动态路由键。

问:我们怎么将动态路由键传入到多数据源路由对象的determineCurrentLookupKey方法中并返回呢?

答:我们有三种方案:

1、使用全局变量传递路由键,这种方式虽然简单,但是会有并发问题,多个线程间的路由键会互相影响。

2、通过在方法的参数中传输路由键, 这需要改变方法签名和调用方式,可能涉及大范围的代码修改。

3、通过显式的上下文对象来传递路由键,也就是创建一个单独的上下文对象,通过这个上下文对象在函数调用过程中传递路由键。

那么1和2的方法都会产生一些问题,那么我们直接选用第3种通过上下文对象传递,ThreadLocal则是一个非常好的选择,它会创建一个线程独有的副本来存储其中的值。

上代码:

在这里,我们创建了一个上下文传递用的ThreadLocal,里面存储的就是我们指定的数据源路由键,这时,我们就可以在determineCurrentLookupKey方法中通过该ThreadLocal获取到当前线程需要执行的数据源的路由键了,代码如下图。

 


Step3-5:编写用于指定数据源的注解。

上代码:

这里我们在Core模块中定义一个MyDataSource注解,其中的value属性就是我们需要查找的路由键,而这个路由键的来源就是我们在ApplicationConstant全局常量或DataSourceType中的定义的路由键。


Step3-6:编写切面类,并通过自定义注解完成数据源路由键的传递。

 上代码:

这一步,我们定义了数据源动态切面类,在这个切面中,我们的切点是所有被DataSource注解标记的Service方法,拦截后,获取切点上的@MyDataSource注解并读取其中的value(路由键),最后放入ThreadLocal中,并且如果当前线程前面已经设置了另一个数据源,则返回前面的数据源的路由键,当前方法结束时,在finally中将原来的路由键再设置回去,以让上一层代码能够走回正常的数据源。


Step4:配置多数据源并注册到Spring中。

在之前,我们关闭了DataSource的自动配置,所以在多数据源的情况下,我们要对每一个数据源都注册一个数据源Bean,然后注册到Spring的多数据源路由表中去。

上代码:

那么这个配置类里面,就配置了我们系统中的两个数据源,一个是upms数据源(当前模块业务库),另一个是公共模块数据源。

在这个类中,我们不仅注册了两个数据源的Bean,还需要注册一个多数据源路由对象,然后将里面的两个数据源加入到路由表里面去,其中targetDataSources就是我们的数据源Map<Integer, DataSource>,而DefaultTargetDataSource就是我们的默认数据源,当我们没有指定数据源时,Service会使用默认数据源。


Step5:测试代码。

跟着上面的步骤,我们就已经完成了我们的高可用的灵活的多数据源配置。接下来写两个Service分别测试不同的数据源。两个数据源对应的库如下图:

然后创建一个Controller,在里面分别调用两个不同的Service,其中mapCodeService是加了@MyDataSource注解的。如下图所示:

 接下来我们使用工具分别调用这两个接口。如下两张图所示:

 

到这里,我们发现,两个Service都可以获取到数据,那么意味着我们当前模块成功对两个库进行了查询,多数据源配置成功了!

至此,我们已经完成了基于注解+AOP的动态多数据源的配置,其中还涉及到了ThreadLocal等知识,如果有什么错误或不懂的地方,欢迎各位一起讨论。

接下来是源码解析部分,各位有兴趣的也可以看一看。


思考:在多数据源的情况下,Spring是怎么管理我们的数据源的呢?又是怎么做到调用的时候可以动态的选择数据源的呢?

答:通过实现Spring提供的AbstractRoutingDataSource抽象类来完成我们的数据源配置。

上源码:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    @Nullable
    private Map<Object, Object> targetDataSources;
    @Nullable
    private Object defaultTargetDataSource;
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;
    @Nullable
    private DataSource resolvedDefaultDataSource;

    public AbstractRoutingDataSource() {
    }

    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        this.defaultTargetDataSource = defaultTargetDataSource;
    }

    public void setLenientFallback(boolean lenientFallback) {
        this.lenientFallback = lenientFallback;
    }

    public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) {
        this.dataSourceLookup = (DataSourceLookup)(dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
    }

    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        } else {
            this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
            this.targetDataSources.forEach((key, value) -> {
                Object lookupKey = this.resolveSpecifiedLookupKey(key);
                DataSource dataSource = this.resolveSpecifiedDataSource(value);
                this.resolvedDataSources.put(lookupKey, dataSource);
            });
            if (this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }

        }
    }

    protected Object resolveSpecifiedLookupKey(Object lookupKey) {
        return lookupKey;
    }

    protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
        if (dataSource instanceof DataSource) {
            return (DataSource)dataSource;
        } else if (dataSource instanceof String) {
            return this.dataSourceLookup.getDataSource((String)dataSource);
        } else {
            throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
        }
    }

    public Map<Object, DataSource> getResolvedDataSources() {
        Assert.state(this.resolvedDataSources != null, "DataSources not resolved yet - call afterPropertiesSet");
        return Collections.unmodifiableMap(this.resolvedDataSources);
    }

    @Nullable
    public DataSource getResolvedDefaultDataSource() {
        return this.resolvedDefaultDataSource;
    }

    public Connection getConnection() throws SQLException {
        return this.determineTargetDataSource().getConnection();
    }

    public Connection getConnection(String username, String password) throws SQLException {
        return this.determineTargetDataSource().getConnection(username, password);
    }

    public <T> T unwrap(Class<T> iface) throws SQLException {
        return iface.isInstance(this) ? this : this.determineTargetDataSource().unwrap(iface);
    }

    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return iface.isInstance(this) || this.determineTargetDataSource().isWrapperFor(iface);
    }

    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }

        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        } else {
            return dataSource;
        }
    }

    @Nullable
    protected abstract Object determineCurrentLookupKey();
}

暂未完成...

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值