背景:一个项目中可能存在多数据源的状况,虽然微服务中,通常是单数据源,可是例如后台管理这些管理接口则不适合使用微服务来 提供接口,因此业务库也须要共存于后台管理项目,然后台管理项目中则有本身自己的一个权限数据库,则就会存在多数据源的状况。 思路:Spring自己已经有实现数据源切换的功能类,能够实如今项目运行时根据相应key值切换到对应的数据源DataSource上。 咱们只需扩展实现便可。 并结合数据源动态切换为须要切换数据源的方法增长注解,从而实现对带有注解的拦截切换。 问题:事务控制,缺省数据源生效,而切换为第二数据源时,事务的数据源默认采用了缺省的。 网上有说更改切面和事务的执行顺序,可是试验后并未成功。
如下是为动态数据源切换,及缺省事务第二数据源的事务控制的实现方案,以springboot做为基础框架。java
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.jdbc.Driver druid: first: #数据源1 url: jdbc:mysql://127.0.0.01:63885/demo?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8 username: demo password: demo rongyuan: #数据源2 url: jdbc:mysql://127.0.0.01:63885/demo?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8 username: demo password: demo initial-size: 10 max-active: 100 min-idle: 10 max-wait: 60000 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 FROM DUAL test-while-idle: true test-on-borrow: false test-on-return: false stat-view-servlet: enabled: true url-pattern: /druid/* #login-username: admin #login-password: admin filter: stat: log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true
package io.y.common.datasources; import java.util.HashMap; import java.util.Map; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; /** * @title * @author zengzp * @time 2018年7月25日 上午11:22:46 * @Description */ @Configuration // 加上此注解禁用数据源自动配置 @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) public class DynamicDataSourceConfig { @Bean(name="first") @ConfigurationProperties("spring.datasource.druid.first") public DataSource firstDataSource(){ return DruidDataSourceBuilder.create().build(); } @Bean(name="rongyuan") @ConfigurationProperties("spring.datasource.druid.rongyuan") public DataSource secondDataSource(){ return DruidDataSourceBuilder.create().build(); } @Bean @Primary public DynamicDataSource dataSource(@Qualifier("first")DataSource firstDataSource, @Qualifier("rongyuan")DataSource secondDataSource) { Map<String, DataSource> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceNames.FIRST, firstDataSource); targetDataSources.put(DataSourceNames.SECOND, secondDataSource); return new DynamicDataSource(firstDataSource, targetDataSources); } }
package io.y.common.datasources; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * @title * @author zengzp * @time 2018年7月25日 上午 10:20:31 * @Description */ public class DynamicDataSource extends AbstractRoutingDataSource { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); public DynamicDataSource(DataSource defaultTargetDataSource, Map<String, DataSource> targetDataSources) { super.setDefaultTargetDataSource(defaultTargetDataSource); super.setTargetDataSources(new HashMap<>(targetDataSources)); super.afterPropertiesSet(); } @Override protected Object determineCurrentLookupKey() { return getDataSource(); } public static void setDataSource(String dataSource) { contextHolder.set(dataSource); } public static String getDataSource() { return contextHolder.get(); } public static void clearDataSource() { contextHolder.remove(); } }
package io.y.common.datasources.annotation; import java.lang.annotation.*; /** * @title 多数据源注解 * @author zengzp * @time 2018年7月25日 下午14:50:53 * @Description */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface TargetDataSource { String name() default ""; }
package io.y.common.datasources.aspect; import java.lang.reflect.Method; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import io.y.common.datasources.DataSourceNames; import io.y.common.datasources.DynamicDataSource; import io.y.common.datasources.annotation.TargetDataSource; /** * @title 多数据源切面处理类 * @author zengzp * @time 2018年7月25日 下午11:56:43 * @Description */ @Aspect @Component @Order(0) public class DataSourceAspect { protected Logger logger = LoggerFactory.getLogger(getClass()); @Pointcut("@annotation(io.y.common.datasources.annotation.TargetDataSource)") public void dataSourcePointCut() { } @Before("dataSourcePointCut()") public void around(JoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); TargetDataSource ds = method.getAnnotation(TargetDataSource.class); if(ds == null){ DynamicDataSource.setDataSource(DataSourceNames.FIRST); logger.debug("set datasource is " + DataSourceNames.FIRST); }else { DynamicDataSource.setDataSource(ds.name()); logger.debug("set datasource is " + ds.name()); } } @AfterReturning("dataSourcePointCut()") public void after(){ DynamicDataSource.clearDataSource(); logger.debug("clean datasource"); } }
package io.y.common.datasources; /** * @title 增长多数据源,在此配置 * @author zengzp * @time 2018年7月25日 下午4:55:20 * @Description */ public interface DataSourceNames { String FIRST = "first"; String SECOND = "rongyuan"; }
- 以上已经完成了动态数据源的切换,只需在Service方法上加上@TargetDataScoure注解而且指定须要切换的数据源名称,first数据源为缺省数据源。
- 若是使用@Transactional,缺省数据源的事务正常执行,若是使@TargetDataScoure切换为第二数据源并执行事务时,则数据源切换失败。
问题分析:mysql
大多数项目只须要一个事务管理器。若是存在多数据源的状况,事务管理器是否会生效,因为spingboot约定大于配置的理念, 默认事务管理器无需咱们再声明定义,而是默认加载时已经指定了其数据源,其数据源则为缺省数据源,若是执行事务时是第二数据源,则 还会以第一数据源作处理,这时则会异常。
- 定义事务管理器 并指定其对应管理的数据源和声明name
package io.y.common.datasources; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.DataSourceTransactionManager; /** * @title 多事物管理器配置 * @author zengzp * @time 2018年7月25日 下午4:55:33 * @Description */ @Configuration public class TransactionConfig { public final static String DEFAULT_TX = "defaultTx"; public final static String RONGYUAN_TX = "rongyuanTx"; @Bean(name=TransactionConfig.DEFAULT_TX) public DataSourceTransactionManager transaction(@Qualifier(DataSourceNames.FIRST)DataSource firstDataSource){ DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(firstDataSource); return dataSourceTransactionManager; } @Bean(name=TransactionConfig.RONGYUAN_TX) public DataSourceTransactionManager rongyuanTransaction(@Qualifier(DataSourceNames.SECOND) DataSource rongyuanDataScoure){ DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(rongyuanDataScoure); return dataSourceTransactionManager; } }
2.事务管理器使用
@Override @Transactional(value=TransactionConfig.RONGYUAN_TX, rollbackFor=Exception.class) @TargetDataSource(name = "rongyuan") public void deleteBatch(Integer[] advertIds) { if (advertIds == null || advertIds.length <= 0) { throw new IllegalArgumentException("参数异常"); } advertDao.deleteBatch(advertIds); }