1. 程式人生 > >MyBatis+Mysql分庫分表的案例分析

MyBatis+Mysql分庫分表的案例分析

**多資料來源動態切換 *分庫分表***

參考 [AbstractRoutingDataSource動態資料來源切換,AOP實現動態資料來源切換](https://blog.csdn.net/u012881904/article/details/77449710)

案例原理:
          主要是DynamicDataSource繼承AbstractRoutingDataSource重寫determineCurrentLookupKey進行了lookupkey的set,
    利用AOP在業務裡實現spring-jdbc的AbstractRoutingDataSource。

/**
 * 資料來源動態切換類
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
	@Override
	protected Object determineCurrentLookupKey() {
		 return DbContextHolder.getDbKey(); //獲取當前資料來源 
	}
}

      dataSource每次getConnection之前都要通過lookupkey獲取指定的DataSource(這裡的get(lookupkey)由別名獲取到資料來源,是因為在application-db.xml裡註冊了資料來源bean)

   /**
       * 以下為org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource.class原始碼
       *
	 * 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;
	}

資料來源配置:

<!-- 配置資料來源 開始-->
	<bean id="baseDataSource" class="com.alibaba.druid.pool.DruidDataSource" abstract="true" init-method="init" destroy-method="close">
		<!-- ......  -->
	</bean>
	<!-- 配置資料來源 結束-->
	
	<!-- 主db開始 -->
	<bean id="datasource"  parent="baseDataSource">
		<property name="url">
			<value>${mysql.jdbc.url}</value>
		</property>
		<property name="username">
			<value>${common.mysql.jdbc.user}</value>
		</property>
		<property name="password">
			<value>${common.mysql.jdbc.password}</value>
		</property>
	</bean>
	<!-- 主db結束 -->
	
	<!-- 主 DB-1 開始-->
	<bean id="masterDatasource1"  parent="baseDataSource">
	</bean>
	<!-- 主DB-1  結束-->
	    <!-- ..... -->
	<!-- 主DB-6  結束-->

	<!-- 從 DB-1 開始-->
	<bean id="slaveDatasource1"  parent="baseDataSource">
	</bean>
	<!-- 從DB-1  結束-->
	    <!-- ..... -->
	<!-- 從DB-6  結束-->
	
	<!-- 切換資料來源(繼承了AbstractRoutingDataSource) 開始 -->
	<bean id="dataSource" class="com.xxx.datasource.db.DynamicDataSource">
		<property name="targetDataSources">
			<map key-type="java.lang.String">
				<entry key="msd1" value-ref="masterDatasource1" />
				    <!-- ..... -->
				<entry key="msd6" value-ref="masterDatasource6" />
				<entry key="sld1" value-ref="slaveDatasource1" />
				    <!-- ..... -->
				<entry key="sld6" value-ref="slaveDatasource6" />
			</map>
		</property>
		<property name="defaultTargetDataSource" ref="datasource" />
	</bean>
	<!--切換資料來源 結束 -->
	
	<!-- 配置資料來源路由 -->
	<bean id="dbRuleSet" class="com.xxx.datasource.bean.RouterSet">
        <property name="dbNumber" value="6"></property><!-- db個數 -->
        <property name="tableNumber" value="6"></property><!-- 每個庫裡分表個數 -->
        <property name="masterDbKeyArray">
            <list>
                <value>msd1</value>
                    <!-- ..... -->
                <value>msd6</value>
            </list>
        </property>
        <property name="slaveDbKeyArray">
            <list>
                <value>sld1</value>
                    <!-- ..... -->
                <value>sld6</value>
            </list>
        </property>
        <property name="shardingTableArray">
            <list>
                <value>xxxxx</value>
                <value>xxxxx_express</value>
            </list>
        </property>
    </bean>


      AOP通過使用者id計算key值匹配配置檔案裡定義好的Datasource集合 得到具體資料來源。 從而繼續執行MyBatis後續分表和Sql操作。
      在service層利用@Router的切面織入業務: 計算分庫分表key,去setLookupkey的程式碼,每次檢查到@Router就去切換資料來源,執行資料操作。 

service層加@Router註解

/** 
	 * 
	 * @param userId
	 * @param orderIds
	 * @return
	 */
	@Router
	public Map<String, Object> getOrder(String userId, String[] orderIds) {
		//do something
		return null;
	}

切面前置增強:

/**
	 * 切換到分庫
	 * 
	 * @param jp
	 * @return
	 * @throws NoSuchMethodException
	 * @throws RouterException
	 * @throws Throwable
	 */
	@Before("aopPoint()")
	public Object doRoute(JoinPoint jp) throws NoSuchMethodException, RouterException {
		Method method = getMethod(jp);
		Router router = method.getAnnotation(Router.class);
		String routeField = router.routerField();
		Object[] args = jp.getArgs();
		Signature signature = jp.getSignature();
		MethodSignature methodSignature = (MethodSignature) signature;
		String[] argsName = methodSignature.getParameterNames();
        //根據快取的userId和請求提供的進行比較,計算routeFieldValue。“計算邏輯”見下文 
		String routeFieldValue = getRouteFieldValue(args, argsName, routeField);
		if (StringUtils.isNotEmpty(routeFieldValue)) {
			dBRouter.doRoute(routeFieldValue, router);
		} else {
			log.error("分庫分表字段為空,未切換資料來源");
		}
		return true;
	}


分庫分表原理:
    spring檢查到aop元件進入切面處理程式碼DBRouterInterceptor
    前置增強進行資料來源切換:
        根據當前使用者id進行 doRoute,若引數有userId,以入參為準,
        計算邏輯: 
            a.id轉換為utf-8下Base64位的string,獲取此string的雜湊值的絕對值
            b.再將此雜湊值除以10000去餘數得到整數值
            c.配合資料來源配置的路由陣列例如mode=6*6, b步驟得到的整數值除以mode取餘除以路由的庫數 為計算出的dbIndex
            d.b步驟得到的整數值除以路由的單庫的表數取餘為tbIndex
        將dbIndex tbIndex全放入路由物件RouterInfo,格式化表路由tableIndex之後存入DbContexHolder(此處格式化是因為表名字首一致,字尾_00遞增)
    再將庫路由dbIndex存入DbContexHolder
    
    *至此*路由全部計算出存入DbContexHolder。需要理解DbContexHolder裝載的執行緒常量ThreadLocal特性
    DbContexHolder的工作原理參見 “原理” 因為是繼承了dataSource的AbstractRoutingDataSource才可以進行lookupkey的set操作。
    
    完成庫的路由計算,切換了資料來源,然後就是MyBatis的分表。
    ShardingInterceptor攔截器攔截Executor實際的sql執行操作,路由到DbContexHolder裡指定的表。具體原理再看

    一個多庫多表的請求從路由到指定庫、指定表即以上步驟。目前設定的是:執行完之後AOP後置增強會切回主資料來源,異常也會切回主資料來源。

小結:專案是水平分表,將一個大表分了幾個欄位一模一樣的表。資料表裡的seq實際是以 庫數字字尾_表數字字尾_數字串 拼接進行唯一標識。資料這樣分散式否均勻,還是取決於使用者id的劃分是否合理(計算邏輯是否合理)。

如果有查全量的資料,這樣分好合資料嗎?一個使用者的資料會分到指定庫指定表,如果有跨庫業務怎麼辦。目前,日統計和週期統計都是單獨task服務去做的。

垂直分表目前還沒理解。待續

                                                                                                                                                                                        持續更新

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------