我已经把它摸的透透的了!!!Spring 动态数据源设计实践,全面解析

Java架构没有996
• 阅读 1748

Spring 动态数据源

动态数据源是什么?它能解决什么???

在实际的开发中,同一个项目中使用多个数据源是很常见的场景。比如,一个读写分离的项目存在主数据源与读数据源。

所谓动态数据源,就是通过Spring的一些配置来自动控制某段数据操作逻辑是走哪一个数据源。举个读写分离的例子,项目中引用了两个数据源,master、slave。通过Spring配置或扩展能力来使得一个接口中调用了查询方法会自动使用slave数据源。

一般实现这种效果可以通过:

  • 使用@MapperScan注解指定某个包下的所有方法走固定的数据源(这个比较死板些,会产生冗余代码,到也可以达到效果,可以作为临时方案使用);

  • 使用注解+AOP+AbstractRoutingDataSource的形式来指定某个方法下的数据库操作是走那个数据源。

  • 通过 Sharding-JDBC组件来实现(需要引入外部依赖,如果项目本身引用了该组件,建议用这种方式实现)

    <hr>

关键核心类【获取资料】

这里主要介绍通过注解+AOP+AbstractRoutingDataSource的联动来实现动态数据源的方式。

一切的起点是AbstractRoutingDataSource这个类,此类实现了 DataSource 接口

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

    // .... 省略 ... 

    @Nullable
    private Map<Object, Object> targetDataSources;

    @Nullable
    private Map<Object, DataSource> resolvedDataSources;


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

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

    @Override
    public void afterPropertiesSet() {

        // 初始化 targetDataSources、resolvedDataSources
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
        this.targetDataSources.forEach((key, value) -> {
            Object lookupKey = resolveSpecifiedLookupKey(key);
            DataSource dataSource = resolveSpecifiedDataSource(value);
            this.resolvedDataSources.put(lookupKey, dataSource);
        });//加入Java开发交流君样:756584822一起吹水聊天
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }


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

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


    /**
     * Retrieve the current target DataSource. Determines the
     * {@link #determineCurrentLookupKey() current lookup key}, performs
     * a lookup in the {@link #setTargetDataSources targetDataSources} map,
     * falls back to the specified
     * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
     * @see #determineCurrentLookupKey()
     */
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");

        // @1 start
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        // @1 end

        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }

    /**
     * 返回一个key,这个key用来从 resolvedDataSources 数据源中获取具体的数据源对象 见
     * //加入Java开发交流君样:756584822一起吹水聊天 @1
     */
    @Nullable
    protected abstract Object determineCurrentLookupKey();

}

可以看到AbstractRoutingDataSource中有个可扩展抽象方法determineCurrentLookupKey(),利用这个方法可以来实现动态数据源效果。

从零写一个简单动态数据源组件

从上一个part我们知道可以通过实现AbstractRoutingDataSource的 determineCurrentLookupKey() 方法动态设置一个key,然后 在配置类下通过setTargetDataSources()方法设置我们提前准备好的DataSource Map

注解,常量定义


/**
 * @author axin
 * @Summary 动态数据源注解定义
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyDS {
    String value() default "default";
}

/**
 * @author axin
 * @Summary 动态数据源常量
 */
public interface DSConst {

    String 默认 = "default";

    String 主库 = "master";

    String 从库 = "slave";

    String 统计 = "stat";
}
/**
 * @author axin
 * @Summary 动态数据源 ThreadLocal 工具
 */
public class DynamicDataSourceHolder {
     //加入Java开发交流君样:756584822一起吹水聊天
    //保存当前线程所指定的DataSource
    private static final ThreadLocal<String> THREAD_DATA_SOURCE = new ThreadLocal<>();

    public static String getDataSource() {
        return THREAD_DATA_SOURCE.get();
    }

    public static void setDataSource(String dataSource) {
        THREAD_DATA_SOURCE.set(dataSource);
    }

    public static void removeDataSource() {
        THREAD_DATA_SOURCE.remove();
    }
}

自定义一个AbstractRoutingDataSource类

/**
 * @author axin
 * @Summary 动态数据源
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * 从数据源中获取目标数据源的key
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        // 从ThreadLocal中获取key
        String dataSourceKey = DynamicDataSourceHolder.getDataSource();
        if (StringUtils.isEmpty(dataSourceKey)) {
            return DSConst.默认;
        }
        return dataSourceKey;
    }
}

AOP实现

/**
 * @author axin
 * @Summary 数据源切换AOP
 */
@Slf4j
@Aspect
@Service
public class DynamicDataSourceAOP {

    public DynamicDataSourceAOP() {
        log.info("/*---------------------------------------*/");
        log.info("/*----------                   ----------*/");
        log.info("/*---------- 动态数据源初始化... ----------*/");
        log.info("/*----------                   ----------*/");
        log.info("/*---------------------------------------*/");
    }

    /**
     * 切点
     */
    @Pointcut(value = "@annotation(xxx.xxx.MyDS)")
    private void method(){}

    /**
     * 方法执行前,切换到指定的数据源
     * @param point
     */
    @Before("method()")
    public void before(JoinPoint point) {
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        //获取被代理的方法对象
        Method targetMethod = methodSignature.getMethod();
        //获取被代理方法的注解信息
        CultureDS cultureDS = AnnotationUtils.findAnnotation(targetMethod, CultureDS.class);

        // 方法链条最外层的动态数据源注解优先级最高
        //加入Java开发交流君样:756584822一起吹水聊天
        String key = DynamicDataSourceHolder.getDataSource();

        if (!StringUtils.isEmpty(key)) {
            log.warn("提醒:动态数据源注解调用链上出现覆盖场景,请确认是否无问题");
            return;
        }

        if (cultureDS != null ) {
            //设置数据库标志
            DynamicDataSourceHolder.setDataSource(MyDS.value());
        }
    }

    /**
     * 释放数据源
     */
    @AfterReturning("method()")
    public void doAfter() {
        DynamicDataSourceHolder.removeDataSource();
    }
}

DataSourceConfig配置

通过以下代码来将动态数据源配置到 SqlSession 中去

/**
 * 数据源的一些配置,主要是配置读写分离的sqlsession,这里没有使用mybatis annotation
 *
@Configuration
@EnableTransactionManagement
@EnableAspectJAutoProxy
class DataSourceConfig {

    /** 可读写的SQL Session */
    public static final String BEANNAME_SQLSESSION_COMMON = "sqlsessionCommon";
    /** 事务管理器的名称,如果有多个事务管理器时,需要指定beanName */
    public static final String BEANNAME_TRANSACTION_MANAGER = "transactionManager";

    /** 主数据源,必须配置,spring启动时会执行初始化数据操作(无论是否真的需要),选择查找DataSource class类型的数据源 配置通用数据源,可读写,连接的是主库 */
    @Bean
    @Primary
    @ConfigurationProperties(prefix = "datasource.common")
    public DataSource datasourceCommon() {
        // 数据源配置 可更换为其他实现方式
        return DataSourceBuilder.create().build();
    }

    /**
     * 动态数据源
     * @returnr
     */
    @Bean
    public DynamicDataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        LinkedHashMap<Object, Object> hashMap = Maps.newLinkedHashMap();
        hashMap.put(DSConst.默认, datasourceCommon());
        hashMap.put(DSConst.主库, datasourceCommon());
        hashMap.put(DSConst.从库, datasourceReadOnly());
        hashMap.put(DSConst.统计, datasourceStat());

        // 初始化数据源 Map
        dynamicDataSource.setTargetDataSources(hashMap);
        dynamicDataSource.setDefaultTargetDataSource(datasourceCommon());
        return dynamicDataSource;
    }

    /**
     * 配置事务管理器
     */
    @Primary
    @Bean(name = BEANNAME_TRANSACTION_MANAGER)
    public DataSourceTransactionManager createDataSourceTransactionManager2() {
        DataSource dataSource = this.dynamicDataSource();
        DataSourceTransactionManager manager = new DataSourceTransactionManager(dataSource);
        return manager;
    }

    /**
     * 配置读写sqlsession
     */
    @Primary
    @Bean(name = BEANNAME_SQLSESSION_COMMON)
    public SqlSession readWriteSqlSession() throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
          //加入Java开发交流君样:756584822一起吹水聊天
        // 设置动态数据源
        factory.setDataSource(this.dynamicDataSource());
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        factory.setConfigLocation(resolver.getResource("mybatis/mybatis-config.xml"));
        factory.setMapperLocations(resolver.getResources("mybatis/mappers/**/*.xml"));
        return new SqlSessionTemplate(factory.getObject());
    }
}

总结

综上,利用AOP+注解实现了一个简单的Spring动态数据源功能,使用的时候,仅需要在目标方法上加上 @MyDS 注解即可。许多开源组件,会在现有的基础上增加一个扩展功能,比如路由策略等等。【获取资料】

顺便聊一下 sharding-jdbc 的实现方式,更新写入类sql自动走主库,查询类自动走读库,如果是新项目无历史债务的话,是可以使用该方案的。如果你是在原有旧的项目上进行读写分离改造,那如果你使用了 sharding-jdbc 读写分离方案,你就必须梳理已有代码逻辑中的sql调用情况,来避免主从延迟造成数据不一致对业务的影响。

主从延迟造成读取数据不一致的情况是指:主从在同步的时候是有一定的延迟时间的,不管是什么网络的情况,这个延迟的值都是存在的,一般在毫秒级左右。这个时候如果使用sharding-jdbc进行读写分离处理,进行实时数据插入并查询判断的时候,就会出现判断异常的情况。【参考文献】

最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。

可以的话请给我一个三连支持一下我哟🧐🧐🧐【获取资料】我已经把它摸的透透的了!!!Spring 动态数据源设计实践,全面解析

点赞
收藏
评论区
推荐文章
技术小男生 技术小男生
1个月前
linux环境jdk环境变量配置
1:编辑系统配置文件vi /etc/profile2:按字母键i进入编辑模式,在最底部添加内容: JAVAHOME/opt/jdk1.8.0152 CLASSPATH.:$JAVAHOME/lib/dt.jar:$JAVAHOME/lib/tools.jar PATH$JAVAHOME/bin:$PATH3:生效配置
光头强的博客 光头强的博客
1个月前
Java面向对象试题
1、 请创建一个Animal动物类,要求有方法eat()方法,方法输出一条语句“吃东西”。 创建一个接口A,接口里有一个抽象方法fly()。创建一个Bird类继承Animal类并实现 接口A里的方法输出一条有语句“鸟儿飞翔”,重写eat()方法输出一条语句“鸟儿 吃虫”。在Test类中向上转型创建b对象,调用eat方法。然后向下转型调用eat()方
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录 问题 用navicat导入数据时,报错: 原因这是因为当前的MySQL不支持datetime为0的情况。 解决修改sql\mode: sql\mode:SQL Mode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。 全局s
浩浩 浩浩
1年前
【Flutter实战】图片和Icon
3.5 图片及ICON 3.5.1 图片Flutter中,我们可以通过Image组件来加载并显示图片,Image的数据源可以是asset、文件、内存以及网络。 ImageProviderImageProvider 是一个抽象类,主要定义了图片数据获取的接口load(),从不同的数据源获取图片需要实现不同的ImageProvi
小森森 小森森
1个月前
校园表白墙微信小程序V1.0 SayLove -基于微信云开发-一键快速搭建,开箱即用
后续会继续更新,敬请期待2.0全新版本 欢迎添加左边的微信一起探讨!项目地址:](https://www.aliyun.com/activity/daily/bestoffer?userCodesskuuw5n) \2. Bug修复更新日历 2. 情侣脸功能大家不要使用了,现在阿里云的接口已经要收费了(土豪请随意), \ \ 和 注意
Stella981 Stella981
1年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置 1、virsh edit centos7 找到“memory”和“vcpu”标签,将 <name>centos7</name> <uuid>2220a6d1-a36a-4fbb-8523-e078b3dfe795</uuid>
Easter79 Easter79
1年前
Twitter的分布式自增ID算法snowflake (Java版)
概述 == 分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。 有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。 而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Stella981 Stella981
1年前
Spring boot 多数据源
网上多是基于XML文件,本文使用基于配置类的方式使用动态数据源。 多数据源原理 ------ Spring作为项目的应用容器,也对多数据源提供了很好的支持,当我们的持久化框架需要数据库连接时,我们需要做到动态的切换数据源,这些Spring的`AbstractRoutingDataSource`都给我们留了拓展的空间,可以先来看看抽象类`AbstractR
Wesley13 Wesley13
1年前
3分钟搞定SpringBoot+Mybatis+druid多数据源和分布式事务
       在一些复杂的应用开发中,一个应用可能会涉及到连接多个数据源,所谓多数据源这里就定义为至少连接两个及以上的数据库了。        下面列举两种常用的场景:         一种是读写分离的数据源,例如一个读库和一个写库,读库负责各种查询操作,写库负责各种添加、修改、删除。        另一种是多个数据源之间并没有特别明显的操作,只是程序
helloworld_34035044 helloworld_34035044
4个月前
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。 uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid() 或 uuid(sep)参数说明:sep 布尔值,生成的uuid中是否包含分隔符'',缺省为