这个个我经历的真实的项目需求,估计很多人都经历过了类似的情况,事情过程:项目中要接入短信,短息提供方提供了两种方案.
这个就是我们项目数据量上来了,基本都会经历的 读写分离 下面的 实现方式二 很适合
这个实现方法在我看来不管是实现还是原理都是比较通俗易懂的( 假如你看过mybatis源码 ,没看过可以翻翻我以前的文章),只是使用了 @MapperScan
直接看下实现代码
配置
spring.datasource.kiss.jdbc-url=jdbc:mysql://127.0.0.1:3306/kiss spring.datasource.kiss.username=root spring.datasource.kiss.password=qwer1234 spring.datasource.kiss.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.crm.jdbc-url=jdbc:mysql://127.0.0.1:3306/crm spring.datasource.crm.username=root spring.datasource.crm.password=qwer1234 spring.datasource.crm.driver-class-name=com.mysql.cj.jdbc.Driver 复制代码
两个mybatis配置类
@Configuration
//关键就是 这个 @MapperScan注解
//basePackages这个参数我们经常使用就是指定扫面的包
//sqlSessionTemplateRef 就是指定你扫面的包里面的 mapper接口 做代理的时候 是使用的那个 sqlSessionTemplateRef
//这个地方说下使用sqlSessionFactoryRef这个参数和sqlSessionTemplateRef是一样的,假如你看了mybatis'源码你会发现 sqlSessionTemplate是sqlSessionFactory创建的
@MapperScan(basePackages="com.kiss.mxb.mapper001",sqlSessionTemplateRef="test1SqlSessionTemplate")
public class MybatisConfig001 {
//创建数据源注册到 spring-ioc容器 设置为主类
@Bean(name = "test1DataSource")
//这个自动装配属性值的,但是必须是 spring.dataSource 开头
@ConfigurationProperties(prefix = "spring.datasource.kiss")
@Primary
public DataSource testDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "test1SqlSessionFactory")
@Primary
public SqlSessionFactory testSqlSessionFactory(@Qualifier("test1DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper001/*.xml"));
return bean.getObject();
}
//事物创建
@Bean(name = "test1TransactionManager")
@Primary
public DataSourceTransactionManager testTransactionManager(@Qualifier("test1DataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
//SqlSessionTemplate注册 就是我们使用的 bean
@Bean(name = "test1SqlSessionTemplate")
@Primary
public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("test1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
复制代码
//这个和上面的类似不介绍了
@Configuration
@MapperScan(basePackages="com.kiss.mxb.mapper002",sqlSessionTemplateRef="test2SqlSessionTemplate")
public class MybatisConfig002 {
@Bean(name = "test2DataSource")
@ConfigurationProperties(prefix = "spring.datasource.crm")
public DataSource testDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "test2SqlSessionFactory")
public SqlSessionFactory testSqlSessionFactory(@Qualifier("test2DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper002/*.xml"));
return bean.getObject();
}
@Bean(name = "test2TransactionManager")
public DataSourceTransactionManager testTransactionManager(@Qualifier("test2DataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "test2SqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("test2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
复制代码
这种方式是使用aop实现的,先看代码,随后会解释原理 复制代码
public class DataSourceHolder {
//保存当前线程的需要使用那个数据源
private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
public static void set(DBTypeEnum dbType) {
contextHolder.set(dbType);
}
public static DBTypeEnum get() {
return contextHolder.get();
}
public static void master() {
set(DBTypeEnum.MASTER);
System.out.println("切换到master");
}
public static void slave() {
set(DBTypeEnum.SLAVE);
System.out.println("切换到slave");
}
}
复制代码
//配置两个数据源
@Configuration
public class DataSourceConfig {
@Primary
@Bean
@ConfigurationProperties("spring.datasource.kiss")
public DataSource kissDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.crm")
public DataSource crmDataSource() {
return DataSourceBuilder.create().build();
}
}
复制代码
//AbstractRoutingDataSource 的父类为AbstractDataSource 最顶层接口为 DataSource
//你要想搞懂多数据源的原理,首先我相信你是知道 DataSource 是个顶层接口,不同的数据源有不同的实现,例如:HikariDataSource,BasicDataSource之类
//AbstractDataSource也是其中一种,有啥不同呢,我们稍后会讲
public class CustomRoutingDataSource extends AbstractRoutingDataSource {
//这个是必须实现的,也是关键代码,判断当前该使用那个数据源的依据
@Override
protected Object determineCurrentLookupKey() {
return DataSourceHolder.get();
}
}
复制代码
重写MybatisAutoConfiguration类的sqlSessionFactoryBean方法
//方式一你可能会放心我们自己创建了2个sqlSessionFactory
//正常的项目中sqlSessionFactory是MybatisAutoConfiguration创建的,不懂的话可以看下我以前写的mybatis源码的文章
@Configuration
@AutoConfigureAfter({DataSourceConfig.class})
public class MyMybatisAutoConfiguration extends MybatisAutoConfiguration {
public MyMybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
ObjectProvider<TypeHandler[]> typeHandlersProvider,
ObjectProvider<LanguageDriver[]> languageDriversProvider, ResourceLoader resourceLoader,
ObjectProvider<DatabaseIdProvider> databaseIdProvider,
ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
super(properties, interceptorsProvider, typeHandlersProvider, languageDriversProvider, resourceLoader,
databaseIdProvider, configurationCustomizersProvider);
// TODO Auto-generated constructor stub
}
@Resource(name = "kissDataSource")
private DataSource kissDataSource;
@Resource(name = "crmDataSource")
private DataSource crmDataSource;
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
//重写了sqlSessionFactoryBean 使用我们自己实现的dataSource
//请看customRoutingDataSource()
return super.sqlSessionFactory(customRoutingDataSource());
}
//关键就是这个方法
@Bean(name = "customRoutingDataSource")
public CustomRoutingDataSource customRoutingDataSource(){
//创建map存放你配置的数据源 key值为determineCurrentLookupKey() 方法可能返回的东西
Map<Object, Object> targetDataSources = new HashMap<Object, Object>(2);
targetDataSources.put(DBTypeEnum.MASTER,kissDataSource);
targetDataSources.put(DBTypeEnum.SLAVE,crmDataSource);
//创建我们实现AbstractRoutingDataSource的数据源
CustomRoutingDataSource customRoutingDataSource = new CustomRoutingDataSource();
//设置目标数据源
customRoutingDataSource.setTargetDataSources(targetDataSources);
//设置默认数据源
customRoutingDataSource.setDefaultTargetDataSource(customRoutingDataSource);
return customRoutingDataSource;
}
}
复制代码
//aop切面
@Aspect
@Component
public class DataSourceAop {
//com.kiss.mxb.annotation.Kiss com.kiss.mxb.annotation.Crm 都是我自定义的注解,也可以指定什么方法开头的,这是aop的东西,不多说了,表达式还是很多的
@Pointcut("@annotation(com.kiss.mxb.annotation.Kiss)")
public void KissPointcut() {
}
@Pointcut("@annotation(com.kiss.mxb.annotation.Crm)")
public void crmPointcut() {
}
//当被@kiss注释的方法执行
@Before("KissPointcut()")
public void kiss() {
DataSourceHolder.master();
}
//当被@Ciss注释的方法执行
@Before("crmPointcut()")
public void crm() {
DataSourceHolder.slave();
}
}
复制代码
实现已经写了完了,测试类不写了 复制代码
其实原理也没啥说的
sqlSessionFactory 创建 sqlSession 的过程中使用configurtion 中的environment.dataSource 创建了transaction , 但是 sqlSessionFactory.configurtion 是在 ioc容器启动的时候就创建了,所以说dataSource 是固定的
说实话我解释这个之前你最好要知道mybatis是如何被spring整合的,以及我们的mapper层接口是如何实例化的,假如不懂的话,我建议你去看看我前面讲的mybatis源码的那几篇文章
上面说到mapper接口实例化的的时候dataSource的固定的,那么如何做到数据源的切换呢
这个很容易理解,我们在使用 @MapperScan 指定了 不同的扫面包下面的 mapper层接口 使用不同的数据源.就是那个sqlSessionTemplateRef参数,所以说 方式一 是在mapper层接口被代理之前就确定了数据源,不同的包的mapper接口使用不同的数据源,非常符合我上面提到的 场景一 使用
这个就有点意思了,问题就在于我们现在创建的 sqlSessionFactory 是使用了我们实现的DataSource 就是AbstractRoutingDataSource
AbstractRoutingDataSource 这个其实从名字也能看出点不一样的地方 路由
先说先我当时看代码时候的结题思路
//org.apache.ibatis.transaction.managed.ManagedTransaction.openConnection()
//看下这个方法
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
//根据数据源 获取 连接
//我们现在的数据源为 AbstractRoutingDataSource
//那就去看看AbstractRoutingDataSource.getConnection();
this.connection = this.dataSource.getConnection();
if (this.level != null) {
this.connection.setTransactionIsolation(this.level.getLevel());
}
}
复制代码
@Override
public Connection getConnection() throws SQLException {
//关键代码 determineTargetDataSource()
//啥意思呢 就是这个方法返回一个具体可执行的数据源 然后再去调用数据源的 getConnection()
//由此可得知 determineTargetDataSource() 就是我们动态获取数据源的额关键
return determineTargetDataSource().getConnection();
}
复制代码
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
//记得这个方法吧 我们重写的方法,能获取当当前执行的方法 要使用的数据源的 标志
Object lookupKey = determineCurrentLookupKey();
//这里获取数据源 你可能会问 resolvedDataSources 是个啥 好吧看下下面的afterPropertiesSet()
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 + "]");
}
//返回切换的DataSource
return dataSource;
}
//初始化方法
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
//把我们设置的 targetDataSources 放入 resolvedDataSources中
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
复制代码