这个个我经历的真实的项目需求,估计很多人都经历过了类似的情况,事情过程:项目中要接入短信,短息提供方提供了两种方案.
这个就是我们项目数据量上来了,基本都会经历的 读写分离 下面的 实现方式二 很适合
这个实现方法在我看来不管是实现还是原理都是比较通俗易懂的( 假如你看过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); } } 复制代码