springboot整合mybatis_plus配置多数据源aop方式的问题

刚转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 targetDataSources = new HashMap<>();
        targetDataSources.put("database1", firstDatasource);
        targetDataSources.put("database2", secondDatasource);
        bean.setTargetDataSources(targetDataSources);
        bean.setDefaultTargetDataSource(firstDatasource);
        return bean;
    }

    @Bean(name = "SqlSessionFactoryBean")
    public SqlSessionFactoryBean sqlSessionFactory(
            @Qualifier(value = "datasource") DynamicDataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // 扫描相关mapper文件
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        String packageSearchPath = PathMatchingResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "mapper/**/*.xml";
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources(packageSearchPath));
        // 调用dataSource
        sqlSessionFactoryBean.setDataSource(dataSource);
        // 映射实体类 typeAliasesPackage 默认只能扫描某一个路径下,或以逗号等分割的 几个路径下的内容,不支持通配符和正则
        sqlSessionFactoryBean.setTypeAliasesPackage("com.xlkh.boot");
        return sqlSessionFactoryBean;
    }

}

3、创建相关类:

创建DynamicDataSource实现AbstractRoutingDataSource冲写determineCurrentLookupKey方法用来选择使用哪个数据库。

public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * 取得当前使用哪个数据源
     *
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DbContextHolder.getDbType();
    }

}

在源码中可以看到是在datasources中根据key获取到datasources

springboot整合mybatis_plus配置多数据源aop方式的问题_第1张图片

创建一个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根本不执行了。下面断点不进入。

springboot整合mybatis_plus配置多数据源aop方式的问题_第2张图片

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方式实现多库的进展和问题。以后如果有机会再回头看看这个问题~~~

有大牛看到希望指定我一二。。。

 

你可能感兴趣的:(springboot,springboot多数据库,使用aop方式的问题)