随着业务及客户的不断壮大,单数据库已经不足以支撑程序业务的完美运行(响应快、高吞吐),所以数据库往往都会进行分表分库/读写分离,那么问题来了,分库后程序如何从不同URL数据库中读取数据呢?
这篇文章只讲如何配置/使用多数据源,不讲分表分库/读写分离,也不讲主键生成策略及读取策略。
如何实现多数据源呢?原理很简单:Spring的AOP.只需要mybatis plus及spring boot的基础依赖,不需要引入其他依赖
说明:多数据源不仅指同类不同地址的数据源,也可以是异构关系型数据库
自定义注解
@Documented
@Inherited
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
DataSourceType value() default DataSourceType.MESH;
enum DataSourceType {
/**
* 数据源类型
**/
MASTER,
SLAVE,
}
}
yml文件
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
datasource:
driver-class-name: org.postgresql.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum-idle: 5
maximum-pool-size: 10
db:
conn:
str: useUnicode=true&characterEncoding=UTF-8
master:
jdbc-url: jdbc:postgresql://127.0.01:54320/master?${spring.datasource.db.conn.str}
username: postgres
password: 123456
salve:
jdbc-url: jdbc:postgresql://127.0.0.1:54321/salve?${spring.datasource.db.conn.str}
username: postgres
password: 123456
动态数据源上下文
@Slf4j
public class DynamicDataSourceContextHolder {
/**
* 数据源标识,保存在线程变量中,避免多线程操作数据源时互相干扰
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置数据源
*
* @param dataSource 数据源名称
*/
public static void setDataSource(String dataSource) {
log.info("切换到{}数据源", Assert.isEmpty(dataSource) ? DataSource.DataSourceType.MASTER.name() : dataSource);
CONTEXT_HOLDER.set(Assert.isEmpty(dataSource) ? DataSource.DataSourceType.MASTER.name() : dataSource);
}
/**
* 获取数据源
*
* @return java.lang.String
* @author Jackpot
* @date 2021/4/30 4:38 下午
*/
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
/**
* 清除数据源
*/
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
动态数据源
继承AbstractRoutingDataSource类,该类是能够实现数据源切换的关键所在。实现determineCurrentLookupKey(),返回数据源的key值。
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
String dataSource = DynamicDataSourceContextHolder.getDataSource();
log.info("当前数据源:{}", Assert.isEmpty(dataSource) ?
com.**.config.datasource.DataSource.DataSourceType.MASTE.name() : dataSource);
return dataSource;
}
}
多数据源配置
@Slf4j
@Configuration
public class DataSourceConfig {
final MybatisPlusInterceptor interceptor;
public DataSourceConfig(MybatisPlusInterceptor interceptor) {
this.interceptor = interceptor;
}
/**
* 主数据源配置
* Primary 表示当前数据源为主数据源
*
* @return javax.sql.DataSource
* @author Jackpot
* @date 2021/5/12 9:32 上午
*/
@Primary
@Bean("master")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 从数据源配置
*
* @return javax.sql.DataSource
* @author Jackpot
* @date 2021/5/12 9:32 上午
*/
@Bean("salve")
@ConfigurationProperties(prefix = "spring.datasource.salve")
public DataSource salveDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 多数据源定义
*
* @param masterDataSource 主数据源
* @param salveDataSource 从数据源
* @return com.**.config.datasource.DynamicDataSource
* @author Jackpot
* @date 2021/5/12 9:33 上午
*/
@Bean(name = "dynamicDataSource")
public DynamicDataSource dataSource(@Qualifier("master") DataSource meshDataSource, @Qualifier("salve") DataSource walleDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>(4);
targetDataSources.put(com.**.config.datasource.DataSource.DataSourceType.MASTER.name(), masterDataSource);
targetDataSources.put(com.**.config.datasource.DataSource.DataSourceType.SALVE.name(), salveDataSource);
return new DynamicDataSource(masterDataSource, targetDataSources);
}
/**
* 将动态数据源注入到SqlSessionFactory
* 同时解决分页失效
*
* @param dynamicDataSource 多数据源
* @return org.apache.ibatis.session.SqlSessionFactory
* @author Jackpot
* @date 2021/4/30 4:30 下午
*/
@Bean("sqlSessionFactory")
public SqlSessionFactory getSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
throws Exception {
final MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
//分页插件
bean.setPlugins(interceptor);
bean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/*Mapper.xml"));
return bean.getObject();
}
/**
* sqlSessionTemplate定义
*
* @param sessionFactory session工厂
* @return org.mybatis.spring.SqlSessionTemplate
* @author Jackpot
* @date 2021/5/12 9:34 上午
*/
@Bean("sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sessionFactory) {
return new SqlSessionTemplate(sessionFactory);
}
/**
* 多数据源事务管理
* 防止事务绑定主数据源无法对数据源进行切换及事务失效
*
* @param dataSource 多数据源
* @return org.springframework.jdbc.datasource.DataSourceTransactionManager
* @author Jackpot
* @date 2021/5/12 9:39 上午
*/
@Bean("dataSourceTransactionManager")
public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
AOP切面实现。
注:@annotation和@within怎么区分?通俗点说就是@annotation作用与方法(method)之上,@within作用于controller/service上,也就是对象
@Slf4j
@Order(0)
@Aspect
@Component
public class DataSourceAspect {
/**
* 通过自定义注解@DataSource定义切点
*/
@Pointcut("@annotation(com.**.config.datasource.DataSource)" + "|| @within(com.**.config.datasource.DataSource)")
public void dsPointCut() {
}
/**
* 切点环绕
*
* @param point 切入点
* @return java.lang.Object
* @author Jackpot
* @date 2021/4/30 4:34 下午
*/
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
DataSource dataSource = getDataSource(point);
if (dataSource != null) {
DynamicDataSourceContextHolder.setDataSource(dataSource.value().name());
}
try {
return point.proceed();
} finally {
DynamicDataSourceContextHolder.clearDataSource();
}
}
/**
* 获取需要切换的数据源
*
* @param point 切入点
* @return com.iot.mesh.common.config.datasource.DataSource
* @author Jackpot
* @date 2021/4/30 4:34 下午
*/
public DataSource getDataSource(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Class<?> targetClass = point.getTarget().getClass();
DataSource targetDataSource = targetClass.getAnnotation(DataSource.class);
if (targetDataSource != null) {
return targetDataSource;
} else {
Method method = signature.getMethod();
return method.getAnnotation(DataSource.class);
}
}
}
启动类,需要剔除spring boot的自动数据源配置
@EnableScheduling
@EnableTransactionManagement
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class ApiApplication {
public static void main(String[] args) {
SpringApplication.run(ApiApplication.class, args);
}
}
使用:粗粒度使用在service类对象之上,细粒度作用于方法之上,同时存在方法注解优先于类上注解。推荐使用第二种方式。
注:同一个方法里面如果涉及到多个数据源操作,事务会失效。


附:其实mybatis plus提供了多数据的依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>

想偷懒的同学可以直接使用,上面主要讲的是核心内容,dynamic-datasource-spring-boot-starter实现动态数据源的思想也是AOP,把上面的代码弄懂了,dynamic-datasource的源码看起来也就很简单了.
mybatis plus的多数据源的使用也很简单,对象或方法上使用@DS(**)即可。
注:使用mybatis plus动态数据源,yml文件格式和上面有点区别,我这里也把它贴出来。启动类上也不需要将DataSourceAutoConfiguration.class剔除
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
locale: zh_CN
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:postgresql://127.0.0.1:54320/master?useUnicode=true&characterEncoding=utf-8&reWriteBatchedInserts=true
username: postgres
password: 123456
salve:
url: jdbc:postgresql://127.0.0.1:54321/salve?useUnicode=true&characterEncoding=utf-8&reWriteBatchedInserts=true
username: postgres
password: 123456
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/281967.html