数据源的概念是什么?Springcloud+Mybatis 如何使用多数据源
} /***
* 获取当前线程数据源 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
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();
} ................. ................. .................}
四种方式对比
事务问题
使用上述数据源配置方式,可实现单个数据源事务控制。
评论