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

Spring 动态数据源

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

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

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

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

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

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

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

    <hr>
    

关键核心类【获取资料】

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

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

1public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { 2 3 // .... 省略 ... 4 5 @Nullable 6 private Map<Object, Object> targetDataSources; 7 8 @Nullable 9 private Map<Object, DataSource> resolvedDataSources; 10 11 12 public void setTargetDataSources(Map<Object, Object> targetDataSources) { 13 this.targetDataSources = targetDataSources; 14 } 15 16 public void setDefaultTargetDataSource(Object defaultTargetDataSource) { 17 this.defaultTargetDataSource = defaultTargetDataSource; 18 } 19 20 @Override 21 public void afterPropertiesSet() { 22 23 // 初始化 targetDataSources、resolvedDataSources 24 if (this.targetDataSources == null) { 25 throw new IllegalArgumentException("Property 'targetDataSources' is required"); 26 } 27 this.resolvedDataSources = new HashMap<>(this.targetDataSources.size()); 28 this.targetDataSources.forEach((key, value) -> { 29 Object lookupKey = resolveSpecifiedLookupKey(key); 30 DataSource dataSource = resolveSpecifiedDataSource(value); 31 this.resolvedDataSources.put(lookupKey, dataSource); 32 });//加入Java开发交流君样:756584822一起吹水聊天 33 if (this.defaultTargetDataSource != null) { 34 this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource); 35 } 36 } 37 38 39 @Override 40 public Connection getConnection() throws SQLException { 41 return determineTargetDataSource().getConnection(); 42 } 43 44 @Override 45 public Connection getConnection(String username, String password) throws SQLException { 46 return determineTargetDataSource().getConnection(username, password); 47 } 48 49 50 /** 51 * Retrieve the current target DataSource. Determines the 52 * {@link #determineCurrentLookupKey() current lookup key}, performs 53 * a lookup in the {@link #setTargetDataSources targetDataSources} map, 54 * falls back to the specified 55 * {@link #setDefaultTargetDataSource default target DataSource} if necessary. 56 * @see #determineCurrentLookupKey() 57 */ 58 protected DataSource determineTargetDataSource() { 59 Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); 60 61 // @1 start 62 Object lookupKey = determineCurrentLookupKey(); 63 DataSource dataSource = this.resolvedDataSources.get(lookupKey); 64 // @1 end 65 66 if (dataSource == null && (this.lenientFallback || lookupKey == null)) { 67 dataSource = this.resolvedDefaultDataSource; 68 } 69 if (dataSource == null) { 70 throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); 71 } 72 return dataSource; 73 } 74 75 /** 76 * 返回一个key,这个key用来从 resolvedDataSources 数据源中获取具体的数据源对象 见 77 * //加入Java开发交流君样:756584822一起吹水聊天 @1 78 */ 79 @Nullable 80 protected abstract Object determineCurrentLookupKey(); 81 82}

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

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

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

注解,常量定义

1 2/** 3 * @author axin 4 * @Summary 动态数据源注解定义 5 */ 6@Target(ElementType.METHOD) 7@Retention(RetentionPolicy.RUNTIME) 8public @interface MyDS { 9 String value() default "default"; 10} 11 12/** 13 * @author axin 14 * @Summary 动态数据源常量 15 */ 16public interface DSConst { 17 18 String 默认 = "default"; 19 20 String 主库 = "master"; 21 22 String 从库 = "slave"; 23 24 String 统计 = "stat"; 25} 26
1/** 2 * @author axin 3 * @Summary 动态数据源 ThreadLocal 工具 4 */ 5public class DynamicDataSourceHolder { 6 //加入Java开发交流君样:756584822一起吹水聊天 7 //保存当前线程所指定的DataSource 8 private static final ThreadLocal<String> THREAD_DATA_SOURCE = new ThreadLocal<>(); 9 10 public static String getDataSource() { 11 return THREAD_DATA_SOURCE.get(); 12 } 13 14 public static void setDataSource(String dataSource) { 15 THREAD_DATA_SOURCE.set(dataSource); 16 } 17 18 public static void removeDataSource() { 19 THREAD_DATA_SOURCE.remove(); 20 } 21}

自定义一个AbstractRoutingDataSource类

1/** 2 * @author axin 3 * @Summary 动态数据源 4 */ 5public class DynamicDataSource extends AbstractRoutingDataSource { 6 7 /** 8 * 从数据源中获取目标数据源的key 9 * @return 10 */ 11 @Override 12 protected Object determineCurrentLookupKey() { 13 // 从ThreadLocal中获取key 14 String dataSourceKey = DynamicDataSourceHolder.getDataSource(); 15 if (StringUtils.isEmpty(dataSourceKey)) { 16 return DSConst.默认; 17 } 18 return dataSourceKey; 19 } 20}

AOP实现

1/** 2 * @author axin 3 * @Summary 数据源切换AOP 4 */ 5@Slf4j 6@Aspect 7@Service 8public class DynamicDataSourceAOP { 9 10 public DynamicDataSourceAOP() { 11 log.info("/*---------------------------------------*/"); 12 log.info("/*---------- ----------*/"); 13 log.info("/*---------- 动态数据源初始化... ----------*/"); 14 log.info("/*---------- ----------*/"); 15 log.info("/*---------------------------------------*/"); 16 } 17 18 /** 19 * 切点 20 */ 21 @Pointcut(value = "@annotation(xxx.xxx.MyDS)") 22 private void method(){} 23 24 /** 25 * 方法执行前,切换到指定的数据源 26 * @param point 27 */ 28 @Before("method()") 29 public void before(JoinPoint point) { 30 MethodSignature methodSignature = (MethodSignature) point.getSignature(); 31 //获取被代理的方法对象 32 Method targetMethod = methodSignature.getMethod(); 33 //获取被代理方法的注解信息 34 CultureDS cultureDS = AnnotationUtils.findAnnotation(targetMethod, CultureDS.class); 35 36 // 方法链条最外层的动态数据源注解优先级最高 37 //加入Java开发交流君样:756584822一起吹水聊天 38 String key = DynamicDataSourceHolder.getDataSource(); 39 40 if (!StringUtils.isEmpty(key)) { 41 log.warn("提醒:动态数据源注解调用链上出现覆盖场景,请确认是否无问题"); 42 return; 43 } 44 45 if (cultureDS != null ) { 46 //设置数据库标志 47 DynamicDataSourceHolder.setDataSource(MyDS.value()); 48 } 49 } 50 51 /** 52 * 释放数据源 53 */ 54 @AfterReturning("method()") 55 public void doAfter() { 56 DynamicDataSourceHolder.removeDataSource(); 57 } 58}

DataSourceConfig配置

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

1/** 2 * 数据源的一些配置,主要是配置读写分离的sqlsession,这里没有使用mybatis annotation 3 * 4@Configuration 5@EnableTransactionManagement 6@EnableAspectJAutoProxy 7class DataSourceConfig { 8 9 /** 可读写的SQL Session */ 10 public static final String BEANNAME_SQLSESSION_COMMON = "sqlsessionCommon"; 11 /** 事务管理器的名称,如果有多个事务管理器时,需要指定beanName */ 12 public static final String BEANNAME_TRANSACTION_MANAGER = "transactionManager"; 13 14 /** 主数据源,必须配置,spring启动时会执行初始化数据操作(无论是否真的需要),选择查找DataSource class类型的数据源 配置通用数据源,可读写,连接的是主库 */ 15 @Bean 16 @Primary 17 @ConfigurationProperties(prefix = "datasource.common") 18 public DataSource datasourceCommon() { 19 // 数据源配置 可更换为其他实现方式 20 return DataSourceBuilder.create().build(); 21 } 22 23 /** 24 * 动态数据源 25 * @returnr 26 */ 27 @Bean 28 public DynamicDataSource dynamicDataSource() { 29 DynamicDataSource dynamicDataSource = new DynamicDataSource(); 30 LinkedHashMap<Object, Object> hashMap = Maps.newLinkedHashMap(); 31 hashMap.put(DSConst.默认, datasourceCommon()); 32 hashMap.put(DSConst.主库, datasourceCommon()); 33 hashMap.put(DSConst.从库, datasourceReadOnly()); 34 hashMap.put(DSConst.统计, datasourceStat()); 35 36 // 初始化数据源 Map 37 dynamicDataSource.setTargetDataSources(hashMap); 38 dynamicDataSource.setDefaultTargetDataSource(datasourceCommon()); 39 return dynamicDataSource; 40 } 41 42 /** 43 * 配置事务管理器 44 */ 45 @Primary 46 @Bean(name = BEANNAME_TRANSACTION_MANAGER) 47 public DataSourceTransactionManager createDataSourceTransactionManager2() { 48 DataSource dataSource = this.dynamicDataSource(); 49 DataSourceTransactionManager manager = new DataSourceTransactionManager(dataSource); 50 return manager; 51 } 52 53 /** 54 * 配置读写sqlsession 55 */ 56 @Primary 57 @Bean(name = BEANNAME_SQLSESSION_COMMON) 58 public SqlSession readWriteSqlSession() throws Exception { 59 SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); 60 //加入Java开发交流君样:756584822一起吹水聊天 61 // 设置动态数据源 62 factory.setDataSource(this.dynamicDataSource()); 63 PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); 64 factory.setConfigLocation(resolver.getResource("mybatis/mybatis-config.xml")); 65 factory.setMapperLocations(resolver.getResources("mybatis/mappers/**/*.xml")); 66 return new SqlSessionTemplate(factory.getObject()); 67 } 68} 69

总结

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

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

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

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

可以的话请给我一个三连支持一下我哟🧐🧐🧐【获取资料】在这里插入图片描述

点赞
收藏

评论区

加载中...

相关推荐

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:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

【Flutter实战】图片和Icon

3.5图片及ICON3.5.1图片Flutter中,我们可以通过Image组件来加载并显示图片,Image的数据源可以是asset、文件、内存以及网络。ImageProviderImageProvider是一个抽象类,主要定义了图片数据获取的接口load(),从不同的数据源获取图片需要实现不同的ImageProvi

Spring Boot 集成 Mybatis 实现双数据源

这里用到了SpringBootMybatisDynamicDataSource配置动态双数据源,可以动态切换数据源实现数据库的读写分离。添加依赖加入Mybatis启动器,这里添加了Druid连接池、Oracle数据库驱动为例。<dependency<groupIdorg.mybatis.spring