spring+mybatis 多数据源切换,动态数据源增长,saas多租户模式方案

最近公司做一个saas模式的项目。在多数据源问题上卡了几天。有两个方案,这是1.0。

你们不知道我多辛苦,翻遍了中国网站,翻遍了外国网站。

前者不全面,不严谨,后者看不懂。




我们先从,多数据源切换开始。

[html]  view plain  copy
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans"  
  3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"  
  4.     xmlns:tx="http://www.springframework.org/schema/tx" xmlns:task="http://www.springframework.org/schema/task"  
  5.     xmlns:aop="http://www.springframework.org/schema/aop"  
  6.     xsi:schemaLocation="http://www.springframework.org/schema/beans    
  7.     http://www.springframework.org/schema/beans/spring-beans-4.1.xsd     
  8.     http://www.springframework.org/schema/context     
  9.     http://www.springframework.org/schema/context/spring-context-4.1.xsd    
  10.     http://www.springframework.org/schema/tx  
  11.     http://www.springframework.org/schema/tx/spring-tx-4.1.xsd  
  12.     http://www.springframework.org/schema/task   
  13.     http://www.springframework.org/schema/task/spring-task-4.1.xsd       
  14.     http://www.springframework.org/schema/aop   
  15.     http://www.springframework.org/schema/aop/spring-aop-4.1.xsd"  
  16.     default-lazy-init="false">  
  17.   
  18.     <!-- 定时器开关 开始 -->  
  19.     <task:annotation-driven />  
  20.     <tx:annotation-driven />  
  21.     <!-- 标注类型 的事务配置 如果使用注解事务。就放开 <tx:annotation-driven /> -->  
  22.     <!-- 统一异常处理方式 -->  
  23.     <!-- <bean id="exceptionHandler" class="com.framework.exception.MyExceptionHandler"/> -->  
  24.     <!-- 初始化数据 -->  
  25.     <!-- <bean id="SpringIocUtils" class="com.framework.util.SpringIocUtils"   
  26.         lazy-init="default" init-method="setBean"/> -->  
  27.   
  28.     <bean  
  29.         class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">  
  30.         <property name="locations">  
  31.             <list>  
  32.                 <value>classpath:jdbc.properties</value>  
  33.             </list>  
  34.         </property>  
  35.         <property name="ignoreUnresolvablePlaceholders" value="true" />  
  36.     </bean>  
  37.     <bean id="dynamicDataSource" class="com.framework.plugin.test.DynamicDataSource">  
  38.         <property name="targetDataSources">  
  39.             <map key-type="java.lang.String">  
  40.                 <entry value-ref="dataSource" key="dataSource"></entry>  
  41.                 <entry value-ref="dataSource2" key="dataSource2"></entry>  
  42.             </map>  
  43.         </property>  
  44.         <property name="defaultTargetDataSource" ref="dataSource">  
  45.         </property>  
  46.     </bean>  
  47.   
  48.     <bean id="dataSource" class="junit.test.JDBCTest">  
  49.         <property name="url" value="${jdbc.url}" />  
  50.         <property name="username" value="${jdbc.username}" />  
  51.         <property name="password" value="${jdbc.password}" />  
  52.         <property name="driverClassName" value="${jdbc.driverClass}" />  
  53.     </bean>  
  54.     <bean id="dataSource2" class="junit.test.JDBCTest">     
  55.         <property name="url" value="jdbc:mysql://127.0.0.1/db_shiro" />  
  56.         <property name="username" value="root" />  
  57.         <property name="password" value="123456" />  
  58.         <property name="driverClassName" value="${jdbc.driverClass}" />  
  59.     </bean>  
  60.     <!-- lpl 自定义注册 -->  
  61.     <bean id="springfactory" class="com.framework.util.SpringFactory"></bean>  
  62.   
  63.     <bean id="pagePlugin" class="com.framework.plugin.PagePlugin">  
  64.         <property name="properties">  
  65.             <props>  
  66.                 <prop key="dialect">mysql</prop>  
  67.                 <prop key="pageSqlId">.*query.*</prop>  
  68.             </props>  
  69.         </property>  
  70.     </bean>  
  71.     <bean id="sqlSessionFactoryBean" class="com.framework.plugin.test.MySqlFatoryBean">  
  72.         <property name="dataSource" ref="dynamicDataSource" />  
  73.         <!-- 自动匹配Mapper映射文件 -->  
  74.         <property name="mapperLocations" value="classpath:mappings/*-mapper.xml" />  
  75.         <!-- <property name="typeAliasesPackage" value="com.framework.entity"/> -->  
  76.         <property name="plugins">  
  77.             <array>  
  78.                 <ref bean="pagePlugin" />  
  79.             </array>  
  80.         </property>  
  81.     </bean>  
  82.     <!-- 通过扫描的模式,扫描目录在com.framework.mapper目录下,所有的mapper都继承SqlMapper接口的接口, 这样一个bean就可以了 -->  
  83.     <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
  84.         <property name="basePackage" value="com.framework.mapper" />  
  85.     </bean>  
  86.     <!-- 事务配置 -->  
  87.     <bean id="transactionManager"  
  88.         class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
  89.         <property name="dataSource" ref="dynamicDataSource" />  
  90.     </bean>  
  91.     <!-- <aop:config> <aop:pointcut expression="execution(public * com.framework.controller.*(..))"   
  92.         id="pointcut" /> <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"   
  93.         /> </aop:config> <tx:advice id="txAdvice" transaction-manager="transactionManager">   
  94.         <tx:attributes> <tx:method name="query*" propagation="REQUIRED" read-only="true"   
  95.         /> <tx:method name="find*" propagation="REQUIRED" read-only="true" /> <tx:method   
  96.         name="save*" propagation="REQUIRED" /> <tx:method name="delete*" propagation="REQUIRED"   
  97.         /> <tx:method name="add*" propagation="REQUIRED" /> <tx:method name="modify*"   
  98.         propagation="REQUIRED" /> <tx:method name="logicDelById" propagation="REQUIRED"   
  99.         /> </tx:attributes> </tx:advice> -->  
  100.     <!-- <aop:aspectj-autoproxy proxy-target-class="true"/> <bean id="log4jHandlerAOP"   
  101.         class="com.framework.logAop.LogAopAction"></bean> <aop:config proxy-target-class="true">   
  102.         <aop:aspect id="logAspect" ref="log4jHandlerAOP"> <aop:pointcut id="logPointCut"   
  103.         expression="execution(* org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(..))"   
  104.         /> <aop:around method="logAll" pointcut-ref="logPointCut" /> </aop:aspect>   
  105.         </aop:config> -->  
  106.     <!-- 使用Spring组件扫描的方式来实现自动注入bean -->  
  107.     <!-- <context:component-scan base-package="com.framework.task" /> -->  
  108.     <!-- 隐式地向 Spring 容器注册 -->  
  109.     <!-- <context:annotation-config /> -->  
  110. </beans>  
[java]  view plain  copy
  1. package junit.test;  
  2.   
  3.   
  4. import java.sql.SQLException;  
  5. import java.util.Properties;  
  6.   
  7. import org.springframework.jdbc.datasource.DriverManagerDataSource;  
  8.   
  9. import com.framework.util.JDBCConnectionUtil;  
  10.   
  11. public class JDBCTest extends DriverManagerDataSource{  
  12.       
  13.       
  14.     public void changeJDBC(String url,String username,String password) {  
  15.         setUrl(url);  
  16.         setUsername(username);  
  17.         setPassword(password);  
  18.     }  
  19.       
  20.     public void changeJDBC(Properties properties) throws Exception {  
  21.         setConnectionProperties(properties);  
  22.         setUrl(properties.getProperty("jdbc.url"));  
  23.         setUsername(properties.getProperty("jdbc.username"));  
  24.         setPassword(properties.getProperty("jdbc.password"));  
  25.     }  
  26. }  
上面这个类,我只是贪图方便然后就继承了,来使用。


[java]  view plain  copy
  1. package com.framework.plugin.test;  
  2.   
  3. import org.mybatis.spring.SqlSessionFactoryBean;  
  4. import org.mybatis.spring.SqlSessionTemplate;  
  5.   
  6. public class MySqlFatoryBean extends SqlSessionFactoryBean {  
  7.       
  8.     public void test() {  
  9.     }  
  10. }  

同理。

这些都不重要。

重要的是对于 ThreadLocal<String> contextHolder,这就是一个线程,对于当前变量的保存,克隆。各个线程之间互不影响。

[java]  view plain  copy
  1. package com.framework.plugin.test;  
  2.   
  3. public class ContextHolder {    
  4.     public static final String DATA_SOURCE_A = "dataSource";    
  5.     public static final String DATA_SOURCE_B = "dataSource2";    
  6.     private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();    
  7.     public static void setCustomerType(String customerType) {    
  8.         contextHolder.set(customerType);    
  9.         System.out.println(" contextHolder.set(customerType);  ==="+customerType);  
  10.     }    
  11.     public static String getCustomerType() {    
  12.         return contextHolder.get();    
  13.     }    
  14.     public static void clearCustomerType() {    
  15.         contextHolder.remove();    
  16.     }    
  17. }    

只要每次调用setCustomerType方法,而且前提是,你的那个数据源的集合里面有,才行。

那么切换数据源就成功了。


如何动态增长数据源呢,map,就是突破口。

利用spring,得到对象

[java]  view plain  copy
  1. JDBCTest jdbcTest = new JDBCTest();  
  2.         Properties properties = MapToProperties.map2properties(datacenterFormMap);  
  3.         String username = datacenterFormMap.getStr("username");  
  4.         jdbcTest.changeJDBC(properties);  
  5.         DynamicDataSource dataSource = (DynamicDataSource) SpringFactory.getObject("dynamicDataSource");  
  6.         HashMap<String, JDBCTest> hashMap = (HashMap<String, JDBCTest>) ReflectHelper.getValueByFieldName(dataSource,  
  7.                 "resolvedDataSources");  
  8.         hashMap.put(username, jdbcTest);  
  9.         System.out.println(ReflectHelper.getFieldByFieldName(dataSource, "targetDataSources"));  
  10.         ContextHolder.setCustomerType(username);  

详细就不给出了,抛砖引玉。


3.如何满足saas多租户模式的需求。

通过拦截器interceptor。SaasInterceptor


不断拦截用户的请求,不断更换。

这个方案很低级,不高效。

sqlsession,也还是单例的,没有saas的思想。就是一个伪saas。


/** 
     * 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;  
    }

真正实现自动切换的地方。。


现在,记录相关的dynamicdatasource与spring 的相关特性,

每一次,进入spring,都会自动执行一边这个方法。


还发现,就算又是明明切换,但是我的框架,切实数据源切换失败的,着实有点蛋疼。。

既然这样,我就改写底层框架,使他变成,适应多数据源的spring。

 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection);
    handler.parameterize(stmt);
    return stmt;
  }
都是在这里执行connection的

进去看看,怎么得到的

 protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }

我的乖乖,越来越复杂,,

/*
   * Creates a logging version of a connection
   *
   * @param conn - the original connection
   * @return - the connection with logging
   */
  public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
    InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
    ClassLoader cl = Connection.class.getClassLoader();
    return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
  }

最后总结,connection,跟statementlog没有半毛钱关系,只跟transaction有关系。

protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }

翻了整个类,transaction,只是初始的时候,完成了初始化。。

transaction又是一个接口来的,必然有实现类,不翻了,猜想应该就是xml里配置的transactionManager,有关。

其实就是线索断了。。

那么实现就是,要么硬来,可以实现,我测试了。代码,很简单。

就是改变原有的connection,变成自定义。

这样的解决办法,相当暴力,那么耦合性就会很强。


其实还有下一篇的,等我BY了,我就写出来了。

 QQ交流群315309006 

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值