刚转springboot还不熟悉,想配置双数据源,看来下网上大家写的一些,很多都是利用AOP的方式去切换数据源。实现思路如下:(最终结果是实现了主从但不支持从库事务,如需完美的下文就帮不到你了,不过里面有好多问题一定是你遇到过的,也可以排排坑)
注:如果想要正常的请看下一篇博客springboot实现多数据源方法二。
1、在yum中配置自定义的多数据源的url、username、password等
spring:
datasource:
druid:
first:
url: jdbc:mysql://10.0.0.0:3306/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
username: root
password: root
initial-size: 5
max-active: 60
max-wait: 60000
min-idle: 5
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
filters: mergeStat,wall
second:
url: jdbc:mysql://10.0.0.0:3306/xxx2?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
username: root
password: root
initial-size: 5
max-active: 60
max-wait: 60000
min-idle: 5
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
filters: mergeStat,wall
2、创建多数据配置文件(配置多个datasource、配置DynamicDataSource、配置SqlSessionFactoryBean等)
@Configuration
public class DataSourceConfig {
@Bean(name = "database1")
@ConfigurationProperties("spring.datasource.druid.first")
public DataSource initDataSource() {
DruidDataSource source = new DruidDataSource();
return source;
}
@Bean(name = "database2")
@ConfigurationProperties("spring.datasource.druid.second")
public DataSource initDataSource2() {
DruidDataSource source = new DruidDataSource();
return source;
}
@Bean(name = "datasource")
@Primary//这个一定要加
public DynamicDataSource dynamicDataSource(@Qualifier(value = "database1") DataSource firstDatasource,
@Qualifier(value = "database2") DataSource secondDatasource) {
DynamicDataSource bean = new DynamicDataSource();
Map
3、创建相关类:
创建DynamicDataSource实现AbstractRoutingDataSource冲写determineCurrentLookupKey方法用来选择使用哪个数据库。
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 取得当前使用哪个数据源
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
}
在源码中可以看到是在datasources中根据key获取到datasources
创建一个DbContextHolder,用来获取和设置数据源
public class DbContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal<>();
/**
* 设置数据源
*
* @param dbTypeEnum
*/
public static void setDbType(DBTypeEnum dbTypeEnum) {
contextHolder.set(dbTypeEnum.getValue());
System.out.println("内部set:"+contextHolder.get());
}
/**
* 取得当前数据源
*
* @return
*/
public static String getDbType() {
System.out.println("内部get:"+contextHolder.get());
return contextHolder.get();
}
/**
* 清除上下文数据
*/
public static void clearDbType() {
contextHolder.remove();
}
}
创建一个枚举存放数据源
public enum DBTypeEnum {
FIRSTDATASOURCE("database1"),
SECONDDATASOURCE("database2");
private String value;
DBTypeEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
创建一个AOP来负责选择数据库
@Component
@Aspect
@Order(-1000)
public class DataSourceInterceptor {
@Pointcut(value = "execution(public * com.boot.service..*.*(..))")
private void firstServicePointcut() {
}
@Pointcut(value = "execution(public * com.boot.service2..*.*(..))")
private void secondServicePointcut() {
}
/**
* 切换数据源1
*/
@Before("firstServicePointcut()")
public void firstDataSourceInterceptor() {
System.out.println("切换到数据源{}..............................firstDataSource");
DbContextHolder.setDbType(DBTypeEnum.FIRSTDATASOURCE);
System.out.println(DbContextHolder.getDbType());
}
/**
* 切换数据源2
*/
@Before("secondServicePointcut()")
public void secondDataSourceInterceptor() {
System.out.println("切换到数据源{}..............................secondDataSource");
DbContextHolder.setDbType(DBTypeEnum.SECONDDATASOURCE);
System.out.println(DbContextHolder.getDbType());
}
}
4、我的问题
这样基本就完成了数据库的切换,看起来是没有问题的。但是跑起来会发现第二个库的事务是失效的。dubug走下发现使用事务注解@Transactional在执行的时候会先选择一次数据库。而且优先级是高于我们自定义的aop的。导致事务注解执行了而数据库没有被切换会使用默认的第一个库。determineCurrentLookupKey部分为null。
/**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
1) @Transactional对databases选择的操作结束后进入我们的自定义aop中,更改了datasource。但是数据库的选择在之前已经结束了。aop更改后,sql执行并不会选择我们想要的数据库而是默认的数据库。
所以我们考虑将自定义的aop优先于事务执行。
2) 使用@Order数据,order中的值越低优先级越高(支持负数)。
在自定义aop中@Order(-1000),在springboot的启动类上@EnableTransactionManagement(order = 0)配置完之后发现根本没有生效.
3) 再接着看事务的默认模式是proxy@EnableTransactionManagement(order = 0,mode = AdviceMode.PROXY),而我们自定义的aop属于aspect。接着我们将事务的模式更改为aspect@EnableTransactionManagement(order = 0,mode = AdviceMode.ASPECTJ)。之后直接报错了。
4)发现少包,接着引入spring-aspects与aspectjweaver
org.springframework
spring-aspects
org.aspectj
aspectjweaver
5)引入包之后发现order数据的确是可以用了。自定义aop与事务的优先级更改了。但是接着发现事务的注解根本不起作用了,发现源码中TransactionAspectSupport类中的invokeWithinTransaction根本不执行了。下面断点不进入。
6)发现变更mode之后需要配置Advisor类,接着又写bean配置文件如下
@Aspect
@Configuration
public class GlobalTransactionAdviceConfig {
private static final String AOP_POINTCUT_EXPRESSION = "execution (* com.boot.service2..*.*(..))";
@Autowired
private PlatformTransactionManager transactionManager;
@Bean
public TransactionInterceptor txAdvice() {
DefaultTransactionAttribute txAttr_REQUIRED = new DefaultTransactionAttribute();
txAttr_REQUIRED.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
DefaultTransactionAttribute txAttr_REQUIRED_READONLY = new DefaultTransactionAttribute();
txAttr_REQUIRED_READONLY.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
txAttr_REQUIRED_READONLY.setReadOnly(true);
NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
source.addTransactionalMethod("add*", txAttr_REQUIRED);
source.addTransactionalMethod("insert*", txAttr_REQUIRED);
source.addTransactionalMethod("save*", txAttr_REQUIRED);
source.addTransactionalMethod("create*", txAttr_REQUIRED);
source.addTransactionalMethod("delete*", txAttr_REQUIRED);
source.addTransactionalMethod("update*", txAttr_REQUIRED);
source.addTransactionalMethod("exec*", txAttr_REQUIRED);
source.addTransactionalMethod("set*", txAttr_REQUIRED);
source.addTransactionalMethod("test*", txAttr_REQUIRED);
source.addTransactionalMethod("get*", txAttr_REQUIRED_READONLY);
source.addTransactionalMethod("query*", txAttr_REQUIRED_READONLY);
source.addTransactionalMethod("find*", txAttr_REQUIRED_READONLY);
source.addTransactionalMethod("list*", txAttr_REQUIRED_READONLY);
source.addTransactionalMethod("count*", txAttr_REQUIRED_READONLY);
source.addTransactionalMethod("is*", txAttr_REQUIRED_READONLY);
source.addTransactionalMethod("select*", txAttr_REQUIRED_READONLY);
return new TransactionInterceptor(transactionManager, source);
}
@Bean
public Advisor txAdviceAdvisor() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
return new DefaultPointcutAdvisor(pointcut, txAdvice());
}
}
7) 这回事务一切都可以按照我们想要的顺序执行了。但是事务还是不能回滚。这回是真心看不下去了。
最终成果是数据库可以切换,从库事务不生效。将aop改成正对dao层去切入更改数据库。现在这个版本只针对主从库,主库增删,从库查询这种模式剩下。要想主从事务都可用,我用这种AOP的方式还真没搞定,还是自己才疏学浅。java路上还要继续学习。以上就是利用aop方式实现多库的进展和问题。以后如果有机会再回头看看这个问题~~~
有大牛看到希望指定我一二。。。