写点什么

数据源的概念是什么?Springcloud+Mybatis 如何使用多数据源

用户头像
极客good
关注
发布于: 刚刚

} /***


* 获取当前线程数据源 key


*/


public static String getContextKey() {


String key = DATASOURCE_CONTEXT_KEY_HOLDER.get(); return key == null ? DataSourceConstants.DS_KEY_MASTER : key;


} /***


* 删除当前线程数据源 key


*/


public static void removeContextKey() {


DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean(DynamicDataSource.class); String currentKey = DATASOURCE_CONTEXT_KEY_HOLDER.get(); if (StringUtils.isNotBlank(currentKey) && !"master".equals(currentKey)) {


dynamicDataSource.getBackupTargetDataSources().remove(currentKey); } DATASOURCE_CONTEXT_KEY_HOLDER.remove(); }}


多数据源自动配置类




这里通过读取 yml 配置文件中所有数据源的配置,自动为每个数据源创建 datasource 对象并注册至 bean 工厂。同时将这些数据源对象,设置到 AbstractRoutingDataSource 中。


通过这种方式,后面如果需要添加或修改数据源,都无需新增或修改 java 配置类,只需去配置中心修改 yml 文件即可。


@Configuration


@MapperScan(basePackages = "com.hosjoy.xxx.xxx.modules.xxx.mapper")


public class DynamicDataSourceConfig {


@Autowired


private BeanFactory beanFactory;


@Autowired


private DynamicDataSourceProperty dynamicDataSourceProperty;


/**


* 功能描述: <br>


* 〈动态数据源 bean 自动配置注册所有数据源〉


*


* @param


* @return javax.sql.DataSource


* @Author li.he


* @Date 2020/6/4 16:47


* @Modifier


*/


@Bean


@Primary


public DataSource dynamicDataSource() {


DefaultListableBeanFactory listableBeanFactory = (DefaultListableBeanFactory) beanFactory; /*获取 yml 所有数据源配置*/


Map<String, Object> datasource = dynamicDataSourceProperty.getDatasource(); Map<Object, Object> dataSourceMap = new HashMap<>(5);


Optional.ofNullable(datasource).ifPresent(map -> { for (Map.Entry<String, Object> entry : map.entrySet()) {


//创建数据源对象


HikariDataSource dataSource = (HikariDataSource) DataSourceBuilder.create().build();


String dataSourceId = entry.getKey();


configeDataSource(entry, dataSource);


/*bean 工厂注册每个数据源 bean*/


listableBeanFactory.registerSingleton(dataSourceId, dataSource);


dataSourceMap.put(dataSourceId, dataSource);


}


});


//AbstractRoutingDataSource 设置主从数据源


return new DynamicDataSource(beanFactory.getBean("master", DataSource.class), dataSourceMap);


}


private void configeDataSource(Map.Entry<String, Object> entry, HikariDataSource dataSource) {


Map<String, Object> dataSourceConfig = (Map<String, Object>) entry.getValue();


dataSource.setJdbcUrl(MapUtils.getString(dataSourceConfig, "jdbcUrl"));


dataSource.setDriverClassName(MapUtils.getString(dataSourceConfig, "driverClassName"));


dataSource.setUsername(MapUtils.getString(dataSourceConfig, "username"));


dataSource.setPassword(MapUtils.getString(dataSourceConfig, "password"));


}


}


数据源切换工具类




切换逻辑:


(1)生成当前线程数据源 key


(2)根据业务条件,获取应切换的数据源 ID;


(3)根据 ID 从数据源缓存池中获取数据源对象,并再次添加到 backupTargetDataSources 缓存池中;


(4)threadLocal 设置当前线程对应的数据源 key;


(5)在执行数据库操作前,spring 会调用 determineCurrentLookupKey 方法获取 key,然后根据 key 去数据源缓存池取出数据源,然后 getConnection 获取该数据源连接;


(6)使用该数据源执行数据库操作;


(7)释放缓存:threadLocal 清理当前线程数据源信息、数据源缓存池清理当前线程数据源 key 和数据源对象,目的是防止内存泄漏。


@Slf4j


@Component


public class DataSourceUtil {


@Autowired


private DataSourceConfiger dataSourceConfiger; /*根据业务条件切换数据源*/


public void switchDataSource(String key, Predicate<? super Map<String, Object>> predicate) {


try {


//生成当前线程数据源 key


String newDsKey = System.currentTimeMillis() + "";


List<Map<String, Object>> configValues = dataSourceConfiger.getConfigValues(key);


Map<String, Object> db = configValues.stream().filter(predicate)


.findFirst().get();


String id = MapUtils.getString(db, "id");


//根据 ID 从数据源缓存池中获取数据源对象,并再次添加到 backupTargetDataSources


addDataSource(newDsKey, id);


//设置当前线程对应的数据源 key


DynamicDataSourceContextHolder.setContextKey(newDsKey);


log.info("当前线程数据源切换成功,当前数据源 ID:{}", id);


}


catch (Exception e) {


log.error("切换数据源失败,请检查数据源配置文件。key:{}, 条件:{}", key, predicate.toString());


throw new ClientException("切换数据源失败,请检查数据源配置", e);


}


}


/*将数据源添加至多数据源缓存池中*/


public static void addDataSource(String key, String dataSourceId) {


DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean(DynamicDataSource.class);


DataSource dataSource = (DataSource) dynamicDataSource.getBackupTargetDataSources().get(dataSourceId);


dynamicDataSource.addDataSource(key, dataSource);


}


}


使用


--


public void doExecute(ReqTestParams reqTestParams){


//构造条件


Predicate<? super Map<String, Object>> predicate =.........;


//切换数据源


dataSourceUtil.switchDataSource("testKey", predicate);


//数据库操作


mapper.testQuery(); //清理缓存,避免内存泄漏


DynamicDataSourceContextHolder.removeContextKey();}


每次数据源使用后,都要调用 removeContextKey 方法清理缓存,避免内存泄漏,这里可以考虑用 AOP 拦截特定方法,利用后置通知为执行方法代理执行缓存清理工作。


@Aspect


@Component


@Slf4j


public class RequestHandleMethodAspect {


@After("xxxxxxxxxxxxxxExecution 表达式 xxxxxxxxxxxxxxxxxx")


public void afterRunning(JoinPoint joinPoint){


String name = joinPoint.getSignature().toString(); long id = Thread.currentThread().getId();


log.info("方法执行完毕,开始清空当前线程数据源,线程 id:{},代理方法:{}",id,name);


DynamicDataSourceContextHolder.removeContextKey(); log.info("当前线程数据源清空完毕,已返回至默认数据源:{}",id);


}}


特点分析




(1)参数化切换数据源方式,出发点和分包方式不一样,适合于在运行时才能确定用哪个数据源。


(2)需要手动执行切换数据源操作;


(3)无需分包,mapper 和 xml 路径自由定义;


(4)增加数据源,无需修改 java 配置类,只需修改数据源配置文件即可。


注解方式




思想


--


该方式利用注解+AOP 思想,为需要切换数据源的方法标记自定义注解,注解属性指定数据源 ID,然后利用 AOP 切面拦截注解标记的方法,在方法执行前,切换至相应数据源;在方法执行结束后,切换至默认数据源。


需要注意的是,自定义切面的优先级需要高于 @Transactional 注解对应切面的优先级。


否则,在自定义注解和 @Transactional 同时使用时,@Transactional 切面会优先执行,切面在调用 getConnection 方法时,会去调用 AbstractRouti


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


ngDataSource.determineCurrentLookupKey 方法,此时获取到的是默认数据源 master。这时 @UsingDataSource 对应的切面即使再设置当前线程的数据源 key,后面也不会再去调用 determineCurrentLookupKey 方法来切换数据源了。


设计思路




数据源注册




同上。


数据源切换




利用切面,拦截所有 @UsingDataSource 注解标记的方法,根据 dataSourceId 属性,在方法执行前,切换至相应数据源;在方法执行结束后,清理缓存并切换至默认数据源。



代码实现




数据源配置文件




同上。


定义动态数据源




同上。


定义数据源 key 线程变量持有




同上。


多数据源自动配置类




同上。


数据源切换工具类




切换逻辑:


(1)生成当前线程数据源 key


(3)根据 ID 从数据源缓存池中获取数据源对象,并再次添加到 backupTargetDataSources 缓存池中;


(4)threadLocal 设置当前线程对应的数据源 key;


(5)在执行数据库操作前,spring 会调用 determineCurrentLookupKey 方法获取 key,然后根据 key 去数据源缓存池取出数据源,然后 getConnection 获取该数据源连接;


(6)使用该数据源执行数据库操作;


(7)释放缓存:threadLocal 清理当前线程数据源信息、数据源缓存池清理当前线程数据源 key 和数据源对象。


public static void switchDataSource(String dataSourceId) {


if (StringUtils.isBlank(dataSourceId)) {


throw new ClientException("切换数据源失败,数据源 ID 不能为空");


} try {


String threadDataSourceKey = UUID.randomUUID().toString(); DataSourceUtil.addDataSource(threadDataSourceKey, dataSourceId); DynamicDataSourceContextHolder.setContextKey(threadDataSourceKey); } catch (Exception e) {


log.error("切换数据源失败,未找到指定的数据源,请确保所指定的数据源 ID 已在配置文件中配置。dataSourceId:{}", dataSourceId);


throw new ClientException("切换数据源失败,未找到指定的数据源,请确保所指定的数据源 ID 已在配置文件中配置。dataSourceId:" + dataSourceId, e);


}}


自定义注解




自定义注解标记当前方法所使用的数据源,默认为主数据源。


@Target(ElementType.METHOD)


@Retention(RetentionPolicy.RUNTIME)


public @interface UsingDataSource {


String dataSourceId() default "master";


}


切面


--


主要是定义前置通知和后置通知,拦截 UsingDataSource 注解标记的方法,方法执行前切换数据源,方法执行后清理数据源缓存。


需要标记切面的优先级比 @Transaction 注解对应切面的优先级要高。否则,在自定义注解和 @Transactional 同时使用时,@Transactional 切面会优先执行,切面在调用 getConnection 方法时,会去调用 AbstractRoutingDataSource.determineCurrentLookupKey 方法,此时获取到的是默认数据源 master。这时 @UsingDataSource 对应的切面即使再设置当前线程的数据源 key,后面也不会再去调用 determineCurrentLookupKey 方法来切换数据源了。


@Aspect


@Component


@Slf4j


@Order(value = 1)


public class DynamicDataSourceAspect { //拦截 UsingDataSource 注解标记的方法,方法执行前切换数据源


@Before(value = "@annotation(usingDataSource)")


public void before(JoinPoint joinPoint, UsingDataSource usingDataSource) {


String dataSourceId = usingDataSource.dataSourceId();


log.info("执行目标方法前开始切换数据源,目标方法:{}, dataSourceId:{}", joinPoint.getSignature().toString(), dataSourceId);


try {


DataSourceUtil.switchDataSource(dataSourceId);


}


catch (Exception e) {


log.error("切换数据源失败!数据源可能未配置或不可用,数据源 ID:{}", dataSourceId, e);


throw new ClientException("切换数据源失败!数据源可能未配置或不可用,数据源 ID:" + dataSourceId, e);


}


log.info("目标方法:{} , 已切换至数据源:{}", joinPoint.getSignature().toString(), dataSourceId);


}


//拦截 UsingDataSource 注解标记的方法,方法执行后清理数据源,防止内存泄漏


@After(value = "@annotation(com.hosjoy.hbp.dts.common.annotation.UsingDataSource)")


public void after(JoinPoint joinPoint) {


log.info("目标方法执行完毕,执行清理,切换至默认数据源,目标方法:{}", joinPoint.getSignature().toString());


try {


DynamicDataSourceContextHolder.removeContextKey();


}


catch (Exception e) {


log.error("清理数据源失败", e);


throw new ClientException("清理数据源失败", e);


}


log.info("目标方法:{} , 数据源清理完毕,已返回默认数据源", joinPoint.getSignature().toString());


}


}


使用


--


@UsingDataSource(dataSourceId = "slave1")


@Transactional


public void test(){


AddressPo po = new AddressPo();


po.setMemberCode("asldgjlk");


po.setName("lihe");


po.setPhone("13544986666");


po.setProvince("asdgjwlkgj");


addressMapper.insert(po); int i = 1 / 0;


}


动态添加方式(非常用)




业务场景描述




这种业务场景不是很常见,但肯定是有人遇到过的。


项目里面只配置了 1 个默认的数据源,而具体运行时需要动态的添加新的数据源,非已配置好的静态的多数据源。例如需要去服务器实时读取数据源配置信息(非配置在本地),然后再执行数据库操作。


这种业务场景,以上 3 种方式就都不适用了,因为上述的数据源都是提前在 yml 文件配置好的。


实现思路




除了第 6 步外,利用之前写好的代码就可以实现。


思路是:


(1)创建新数据源;


(2)DynamicDataSource 注册新数据源;


(3)切换:设置当前线程数据源 key;添加临时数据源;


(4)数据库操作(必须在另一个 service 实现,否则无法控制事务);


(5)清理当前线程数据源 key、清理临时数据源;


(6)清理刚刚注册的数据源;


(7)此时已返回至默认数据源。


代码


--


代码写的比较粗陋,但是模板大概就是这样子,主要想表达实现的方式。


Service A:


public String testUsingNewDataSource(){


DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean("dynamicDataSource", DynamicDataSource.class);


try { //模拟从服务器读取数据源信息


//..........................


//....................


//创建新数据源


HikariDataSource dataSource = (HikariDataSource) DataSourceBuilder.create().build();


dataSource.setJdbcUrl("jdbc:mysql://192.168.xxx.xxx:xxxx/xxxxx?......");


dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");


dataSource.setUsername("xxx");


dataSource.setPassword("xxx");


//DynamicDataSource 注册新数据源


dynamicDataSource.addDataSource("test_ds_id", dataSource);


//设置当前线程数据源 key、添加临时数据源


DataSourceUtil.switchDataSource("test_ds_id");


//数据库操作(必须在另一个 service 实现,否则无法控制事务)


serviceB.testInsert();


}


finally {


//清理当前线程数据源 key


DynamicDataSourceContextHolder.removeContextKey();


//清理刚刚注册的数据源


dynamicDataSource.removeDataSource("test_ds_id");


}


return "aa";


}


Service B:


@Transactional(rollbackFor = Exception.class)


public void testInsert() {


AddressPo po = new AddressPo();


po.setMemberCode("555555555");


po.setName("李郃");


po.setPhone("16651694996");


po.setProvince("江苏省");


po.setCity("南京市");


po.setArea("浦口区");


po.setAddress("南京市浦口区宁六路 219 号");


po.setDef(false);


po.setCreateBy("23958");


addressMapper.insert(po); //测试事务回滚


int i = 1 / 0;


}


DynamicDataSource: 增加 removeDataSource 方法, 清理注册的新数据源。


public class DynamicDataSource extends AbstractRoutingDataSource {


................. ................. ................. public void removeDataSource(String key){


this.backupTargetDataSources.remove(key);


super.setTargetDataSources(this.backupTargetDataSources);


super.afterPropertiesSet();


} ................. ................. .................}


四种方式对比





事务问题




使用上述数据源配置方式,可实现单个数据源事务控制。

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
数据源的概念是什么?Springcloud+Mybatis如何使用多数据源