平常我们工作有时会有读写分离,或者业务数据在不同的数据库的情况,如果在一个项目里操作,就需要配置多个数据源,并进行动态的切换了。
服务框架:spring + mybatis + druid
多数据源的配置有两种方式:
第一种是基于配置来实现:将数据源的配置一摸一样的配置多个,这种情况下,就需要将不同的业务拆分到不同的包下了,数据源配置中指定扫描不同的包。
<!-- ========= A数据源配置 ======== -->
<bean id="dataSourceA" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- MySQL数据库配置 -->
<property name="url" value="${db.a.url}" />
<property name="username" value="${db.a.username}" />
<property name="password" value="${db.a.password}" />
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${db.a.initial-size}" />
<property name="minIdle" value="${db.a.min-idle}" />
<property name="maxActive" value="${db.a.max-active}" />
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="${db.a.max-wait}" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${db.a.time-between-eviction-runs-millis}" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${db.a.min-evictable-idle-time-millis}" />
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<property name="poolPreparedStatements" value="${db.a.pool-prepared-statements}" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="${db.a.max-pool-prepared-statement-per-connection-size}" />
</bean>
<bean id="sqlSessionFactoryA" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSourceA"/>
<!-- 自动扫描entity目录, 省掉Configuration.xml里的手工配置 -->
<property name="typeAliasesPackage" value="dbsource.manager.a.entity"/>
<!-- 显式指定Mapper文件位置 -->
<property name="mapperLocations" value="classpath:/asqlmap/*Mapper.xml"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryA"/>
<property name="basePackage" value="dbsource.manager.a.dao"/>
</bean>
<bean id="transactionManagerA" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSourceA"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManagerA" proxy-target-class="true"/>
<!-- ========= B数据源配置 ======== -->
<bean id="datasourceB" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- MySQL数据库配置 -->
<property name="url" value="${db.b.url}" />
<property name="username" value="${db.b.username}" />
<property name="password" value="${db.b.password}" />
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${db.b.initial-size}" />
<property name="minIdle" value="${db.b.min-idle}" />
<property name="maxActive" value="${db.b.max-active}" />
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="${db.b.max-wait}" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${db.b.time-between-eviction-runs-millis}" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${db.b.min-evictable-idle-time-millis}" />
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<property name="poolPreparedStatements" value="${db.b.pool-prepared-statements}" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="${db.b.max-pool-prepared-statement-per-connection-size}" />
</bean>
<bean id="sqlSessionFactoryB" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="datasourceB"/>
<!-- 自动扫描entity目录, 省掉Configuration.xml里的手工配置 -->
<property name="typeAliasesPackage" value="dbsource.manager.b.entity"/>
<!-- 显式指定Mapper文件位置 -->
<property name="mapperLocations" value="classpath:/bsqlmap/*Mapper.xml"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryB" />
<property name="basePackage" value="dbsource.manager.b.dao"/>
</bean>
<bean id="tmsgTransactionManagerB" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="datasourceB"/>
</bean>
<tx:annotation-driven transaction-manager="tmsgTransactionManagerB" proxy-target-class="true"/>
划重点:po、dao、mapper三处的文件需要放在不同的包里,不同的数据源配置扫描不同的包
这种多数据源配置改动起来很简单,缺点就是不够灵活,无法对具体的场景进行数据源的切换。
第二种是基于切面来实现:自定义注解,在需要切换数据源的地方加上注解,切换到指定的数据源。
定义数据源类型枚举类
public enum DBTypeEnum {
/**
* A数据库
*/
ADB,
/**
* B数据库
*/
DBD;
}
定义数据源类型管理类
public class DatasourceTypeManager {
private final static ThreadLocal<DBTypeEnum> DB_HOLDER = new ThreadLocal<>();
public static DBTypeEnum getDatasource() {
return DB_HOLDER.get();
}
public static void setDatasource(DBTypeEnum dbType) {
DB_HOLDER.set(dbType);
}
public static void resetDatasource() {
DB_HOLDER.set(DBTypeEnum.MASTER);
}
}
Threadlocal顾名思义保存的是每个线程自己的变量,线程间是彼此隔离的,所以不会有线程安全问题。Threadlocal会为每个线程创建一个副本,存储变量,线程内任何地方都可以直接使用,所以对性能不会有严重影响。
实现抽象类AbstractRoutingDataSource,实现数据源的动态路由
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DatasourceTypeManager.getDatasource();
}
}
自定义注解,这里定义注解可加在类上和方法上
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Datasource {
DBTypeEnum type() default DBTypeEnum.ADB;
}
切面实现数据源动态切换
@Aspect
@Order(0)
@Component
public class DatasourceAspect {
@Pointcut("@annotation(dbsource.manager.multipledb.Datasource) " +
"|| @within(dbsource.manager.multipledb.Datasource)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 数据源切换
this.changeDbType(joinPoint);
try {
return joinPoint.proceed();
} finally {
DatasourceTypeManager.resetDatasource();
}
}
/**
* 数据源切换,优先方法上的配置,方法上没有配置,则取类上的配置
*
* @param joinPoint
*/
private void changeDbType(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Datasource datasource = method.getAnnotation(Datasource.class);
if (datasource != null) {
DatasourceTypeManager.setDatasource(datasource.type());
return;
}
// 获取类上的注解
datasource = joinPoint.getTarget().getClass().getAnnotation(Datasource.class);
if (datasource == null) {
DatasourceTypeManager.setDatasource(DBTypeEnum.ADB);
} else {
DatasourceTypeManager.setDatasource(datasource.type());
}
}
}
说明:切点中,@annotation匹配方法上的注解,@within匹配类上的注解
数据源的XML配置,多个数据源的基本信息配置是省不掉的,还有就是在配置中向DynamicDataSource注入多个数据源
<!-- ========= A数据源配置 ======== -->
<bean id="dataSourceA" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- MySQL数据库配置 -->
<property name="url" value="${db.a.url}" />
<property name="username" value="${db.a.username}" />
<property name="password" value="${db.a.password}" />
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${db.a.initial-size}" />
<property name="minIdle" value="${db.a.min-idle}" />
<property name="maxActive" value="${db.a.max-active}" />
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="${db.a.max-wait}" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${db.a.time-between-eviction-runs-millis}" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${db.a.min-evictable-idle-time-millis}" />
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<property name="poolPreparedStatements" value="${db.a.pool-prepared-statements}" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="${db.a.max-pool-prepared-statement-per-connection-size}" />
</bean>
<!-- ========= B数据源配置 ======== -->
<bean id="datasourceB" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- MySQL数据库配置 -->
<property name="url" value="${db.b.url}" />
<property name="username" value="${db.b.username}" />
<property name="password" value="${db.b.password}" />
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${db.b.initial-size}" />
<property name="minIdle" value="${db.b.min-idle}" />
<property name="maxActive" value="${db.b.max-active}" />
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="${db.b.max-wait}" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${db.b.time-between-eviction-runs-millis}" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${db.b.min-evictable-idle-time-millis}" />
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<property name="poolPreparedStatements" value="${db.b.pool-prepared-statements}" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="${db.b.max-pool-prepared-statement-per-connection-size}" />
</bean>
<!-- 多个数据源注入 -->
<bean id="dataSource" class="dbsource.manager.multipledb.DynamicDataSource">
<!-- 设置默认数据源 -->
<property name="defaultTargetDataSource" ref="datasourceA"/>
<property name="targetDataSources">
<map key-type="dbsource.manager.multipledb.DBTypeEnum">
<entry key="ADB" value-ref="datasourceA"/>
<entry key="BDB" value-ref="datasourceB"/>
</map>
</property>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 自动扫描entity目录, 省掉Configuration.xml里的手工配置 -->
<property name="typeAliasesPackage" value="dbsource.manager.entity"/>
<!-- 显式指定Mapper文件位置 -->
<property name="mapperLocations" value="classpath:/sqlmap/*Mapper.xml"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<property name="basePackage" value="dbsource.manager.dao"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="datasource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
然后我们就可以在需要切换数据源的方法和类上加上注解了 @Datasource(type = DBTypeEnum.ADB)