文章目录
引言
哈喽,小伙伴们,一周不见了,在这段时间,我利用下班的闲暇时间更新了一版代码生成器,添加了之前呼声较高的多数据源模式,这样生成的代码可以实现动态切换数据源的功能,多数据源在项目当中还算比较常用的,例如主从读写分离,多库操作等都需要在同一个项目中操作多个数据库,本次更新正是解决了这个痛点,生成代码之后,可以通过注解的方式灵活切换数据源,并且支持多库事务一致性,下面就让我们一起看一下具体的实现效果,顺便讲一下动态多数据源的内部原理!
生成器界面调整
为了实现多数据源模式,代码生成器对界面进行了调整,如下:
主界面添加了选择数据源的功能,并且现在数据库信息需要点击数据源配置来进行配置,点击后会弹出如下窗口:
在这里我们可以配置数据库信息,配置完毕后点击保存,在主界面即可进行选择,在主界面被选择的数据源将在生成的代码中作为默认数据源使用。
勾选多数据源模式可以生成多数据源模式代码,不勾选则与之前一样,生成的是常规单数据源项目。
总体跟原来区别不大,使用多数据源模式生成代码基本步骤如下:
- 配置数据源保存
- 主界面依次选择数据源,配置数据项信息
- 勾选多数据源模式,点击生成代码即可
生成代码展示
多数据源模式下会在 config 包下生成多数据源相关的配置类及切面,如果大家有个性化需求可以通过修改 DynamicDataSourceAspect 切面来实现动态切换逻辑,现有切换逻辑基本足够。
多数据源其实还可以通过代码分包的方式实现,这种方式实现起来易于理解:配置多个数据源,扫描不同的包,创建属于自己的 sqlSessionFactory 和 txManager(事务管理器),在使用的时候可以通过调用不同包下的 mapper 来实现多数据源的效果,但是这种方式的弊端也较为明显,分包稍有不慎便会出错,并且如果想要实现不同数据源下的事务一致性也较为麻烦,在同一个 service 方法中操作多个数据库因此受限。
动态多数据源则不会有以上问题,因此代码生成器选择了动态多数据源的生成模式,利用 aop 实现数据源的动态切换,并且可以保证多库操作事务一致性,后面会详细讲解。
代码运行效果
在 idea 中运行生成的代码,启动完毕登录,点击左侧菜单查询:
查看后台日志,发现会切换不同的数据库执行sql:
下面以 springboot 为例,讲一下多数据源内部原理。
动态多数据源内部原理及核心代码
动态多数据源的内部原理其实就是 aop,只不过复杂的是 aop 的实现过程。
mybatis 为我们提供了一个抽象类 AbstractRoutingDataSource,通过继承此类,重写 determineCurrentLookupKey 方法可以根据返回值决定当前使用哪个数据源,因此我们创建类 DynamicDataSource 继承 AbstractRoutingDataSource 并重写 determineCurrentLookupKey 方法:
/**
* 重写数据源选择方法(获取当前线程设置的数据源)
* @author zrx
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
}
}
先不忙着实现,如果想要正确匹配数据源,我们还需要向 DynamicDataSource 类中注册数据源,所以需要先对数据源进行配置,这里注册两个数据源 db1(mysql) 和 db2(oracle),我们使用枚举值 DB1 和 DB2 作为数据源 db1 和 db2 的 key:
package mutitest.config.mutidatasource;
/**
* 数据源枚举
* @author zrx
*/
public enum DataSourceType {
/**
* DB1
*/
DB1,
/**
* DB2
*/
DB2,
}
package mutitest.config.mutidatasource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 数据源配置类
*
* @author zrx
*/
@Configuration
public class DynamicDataSourceConfig {
@Bean(name = "db1")
@ConfigurationProperties(prefix = "spring.datasource.db1")
public DataSource db1DataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "db2")
@ConfigurationProperties(prefix = "spring.datasource.db2")
public DataSource db2DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource dynamicDataSource(@Qualifier(value = "db1") DataSource db1,@Qualifier(value = "db2") DataSource db2) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(db1);
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(DataSourceType.DB1, db1);
dataSourceMap.put(DataSourceType.DB2, db2);
//向动态数据源中注册所有数据源信息
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
@Bean
public PlatformTransactionManager txManager(DataSource dataSource) {
//返回动态数据源的事务管理器
return new DataSourceTransactionManager(dataSource);
}
}
通过以上配置,我们成功向 DynamicDataSource 中注册了 db1 和 db2,如何才能获取当前程序运行中的数据源呢?这就需要我们用到 ThreadLocal,ThreadLocal 可以向当前线程中 set 和 get 值并且不受其他线程影响,而我们服务器的每一个请求都由一个工作线程来处理(nio 模式也是一个请求一个工作线程处理,只是在接收请求的时候使用了 io 多路复用),所以可以使用 ThreadLocal 存储当前工作线程的数据源,ThreadLocal 在很多开源框架中都有使用,主要用于线程隔离。
创建 DynamicDataSourceHolder 类,存储当前线程中的数据源:
package mutitest.config.mutidatasource;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* 数据源选择器
*
* @author zrx
*/
public class DynamicDataSourceHolder {
private static final ThreadLocal<DataSourceType> DATA_SOURCE_HOLDER = new ThreadLocal<>();
private static final Set<DataSourceType> DATA_SOURCE_TYPES = new HashSet<>();
static {
//添加全部枚举
DATA_SOURCE_TYPES.addAll(Arrays.asList(DataSourceType.values()));
}
public static void setType(DataSourceType dataSourceType) {
if (dataSourceType == null) {
throw new NullPointerException();
}
DATA_SOURCE_HOLDER.set(dataSourceType);
}
public static DataSourceType getType() {
return DATA_SOURCE_HOLDER.get();
}
static void clearType() {
DATA_SOURCE_HOLDER.remove();
}
static boolean containsType(DataSourceType dataSourceType) {
return DATA_SOURCE_TYPES.contains(dataSourceType);
}
}
然后,实现 determineCurrentLookupKey 方法,一行代码即可:
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getType();
}
最后一步,我们要实现数据源的动态切换,则需要自己实现一个数据源动态切面,改变当前线程中的数据源,我们可以使用注解来辅助实现,在切面中通过扫描方法上的注解来得知具体切换到哪个数据源。
创建 DBType 注解:
package mutitest.config.mutidatasource;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 多数据源注解
* @author zrx
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DBType {
DataSourceType value() default DataSourceType.DB1;
}
创建数据源动态切面 DynamicDataSourceAspect:
package mutitest.config.mutidatasource;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 动态数据源切面(order 必须要设置,否则事务的切面会优先执行,数据源已经设置完了,再设置就无效了)
* @author zrx
*/
@Aspect
@Component
@Order(1)
public class DynamicDataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
@Before("@annotation(dbType)")
public void changeDataSourceType(JoinPoint joinPoint, DBType dbType) {
DataSourceType curType = dbType.value();
//判断注解类型
if (!DynamicDataSourceHolder.containsType(curType)) {
logger.info("指定数据源[{}]不存在,使用默认数据源-> {}", dbType.value(), joinPoint.getSignature());
} else {
logger.info("use datasource {} -> {}", dbType.value(), joinPoint.getSignature());
// 切换当前线程的数据源
DynamicDataSourceHolder.setType(dbType.value());
}
}
@After("@annotation(dbType)")
public void restoreDataSource(JoinPoint joinPoint, DBType dbType) {
logger.info("use datasource {} -> {}", dbType.value(), joinPoint.getSignature());
//方法执行完,清空,防止内存泄漏
DynamicDataSourceHolder.clearType();
}
}
在数据源切面上需要添加 @Order 注解,值取1,这是因为之前我们配置了动态数据源事务,spring 会因此生成事务代理并且会优先于切面执行,事务代理一旦生成,数据源便被固定,这样我们在切面中切换数据源就会无效,所以切面逻辑需要在事务代理之前执行才可生效。
至此,动态多数据源基本实现完毕!
事务一致性问题
使用动态多数据源的同时,也要注意保证事务一致性,大家可能遇到这种情况,传统单数据源应用中,同一个 service ,在没有开启事务的方法里调用开启事务的方法会导致事务失效,这是因为 spring 只会对相同的 service 代理一次,否则如果在没有开启事务的方法中再次开启自身代理会导致循环依赖问题出现,类似 “无限套娃”:自己代理的方法调用自己代理的另一个方法,并且另一个方法还需要自己的代理。解决此类问题的方法很简单,让调用方开启事务即可,多数据源模式中同样适用。
除此之外,多数据源模式中还存在如下场景:serviceA 中的 A 和 B 方法都开启了事务,但操作的是不同的数据库(ip不同),这个时候 A 调用 B,使用的是 A 的代理,对 B 不适用,便会报错,对此我们可以把 B 方法移入另一个 serviceB 中,在 serviceA 中注入 serviceB ,在 A 方法中使用 serviceB 调用 B 方法,这样执行到 B 方法的时候使用的便是 serviceB 的代理,看起来没有问题,但还有一点遗漏,那就是事务的传播行为。
我们都知道,Spring 中默认的事务传播行为是 required:如果需要开启事务,则开启事务,如果已经开启事务,则加入当前事务。上文中,执行 B 方法的时候虽然使用的是 serviceB 的代理,但是由于其事务传播行为是 required,A 方法执行的时候已经开启了事务,所以导致 B 方法加入到了 A 方法的事务中,但 A 和 B 属于两个不同的数据库,使用相同的事务管理器必然会出现问题。为了解决此问题,我们可以把事务传播行为改为 required_new:如果需要开启事务,则开启事务,并且总是开启新的事务。这样执行 B 方法的时候会开启新的事务,使用的便是 B 所在数据库的事务管理器,B 方法也就可以正常执行了,并且如果 B 出现异常,如果 A 不主动捕获,则 A,B 都会回滚。
也许有人会问,单数据源模式下使用 required 为什么不会有上述问题呢,因为单数据源模式下使用的是同一个数据库,在事务执行过程中,当前事务是共享且通用的,所以没问题。除此之外,使用 required 不必频繁重开事务,也一定程度上提升了系统性能,多数据源模式下由于不同数据库之间事务是完全隔离的,所以才需要使用 required_new 重开事务,当然,也需要根据业务具体场景具体分析,这里讨论的只是较为通用的情况。
代码生成器多数据源模式下使用的事务传播行为正是 required_new,全局配置类如下:
package mutitest.config;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.TransactionInterceptor;
/**
* 全局事务支持
*
* @author zrx
*
*/
@Aspect
@Configuration
public class TransactionAdviceConfig {
private static final String AOP_POINTCUT_EXPRESSION = "execution(* mutitest.service.impl.*.*(..))";
@Autowired
private PlatformTransactionManager transactionManager;
@Bean
public TransactionInterceptor txAdvice() {
DefaultTransactionAttribute txAttr_REQUIRED = new DefaultTransactionAttribute();
txAttr_REQUIRED.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
DefaultTransactionAttribute txAttr_REQUIRED_READONLY = new DefaultTransactionAttribute();
txAttr_REQUIRED_READONLY.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
txAttr_REQUIRED_READONLY.setReadOnly(true);
NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
//可以根据业务需要自行添加需要被事务代理的方法
source.addTransactionalMethod("add*", txAttr_REQUIRED);
source.addTransactionalMethod("delete*", txAttr_REQUIRED);
source.addTransactionalMethod("update*", txAttr_REQUIRED);
source.addTransactionalMethod("select*", txAttr_REQUIRED_READONLY);
source.addTransactionalMethod("likeSelect*", 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());
}
}
到此为止,我们才算实现了一个完整的动态多数据源功能,可见是有许多技术细节潜藏在里面的,朋友们可以使用代码生成器生成多数据源模式下的代码自行运行体会。
结语
本文到这里就结束了,写这个多数据源生成功能其实也算花了一番心思,正着写代码容易,反过来生成是真不容易,并且由于最开始做的时候没有考虑到多数据源的情况,导致最开始的设计全都是针对单个数据库的,这次强行在外面包了一层,总归是实现了,在这个过程中,顺便也复习了一下 Spring 的循环依赖,Bean 加载周期等老生常谈的问题,也算有所收获。作为开发人员,我们要多关注一些功能底层的东西,而不是简单的 api 调用,这样才能不断突破瓶颈,取得成长。码字不易,各位看官可以点赞,在看,星标关注哦,我们下次再见!
关注公众号 螺旋编程极客
获取代码生成器最新动态,同时第一时间解锁更多精彩内容!