基於spring 切面(AOP)實現動態多數據源切換,基於 MyBatis 插件方式實現動態分表查詢

基於spring 切面(AOP)實現動態多數據源切換;基於 MyBatis 插件方式實現動態分表查詢。 來源於多個已上線項目實踐,本項目有完整的測試示例。

mybatis-plugin-shard

  • 基於spring 切面(AOP)實現動態多數據源切換。
  • 基於 MyBatis 插件方式實現動態分表策略。
  • 來源於多個已上線項目實踐。
  • 本項目有完整的測試示例。

以後會出詳細的文檔,敬請期待。

todo

  • 將分庫分表配置與數據源配置統一放到文件 db-config.xml,並作爲配置的切面的參數,在整個分庫分表過程都可訪問。
  • 完善分表邏輯,比起之前將分庫分表配置在一個文件中更加優雅,也更加靈活,擴展性越好。
  • 完善文檔

項目地址

配套 MBG 增強插件

查看 MBG 增強插件請移步:mybatis-generator

  • 用該 MBG 增強插件生成的 {xxx}Mapper.xml,會把表名用[`](不包括中括號)引起來,這樣做的目的是分表時,動態給表名添加後綴後替換原始表名時不會“添亂”。
  • 注意 [`] 並非單引號,是在ESC 鍵下面、Q 鍵左上角的數字鍵 1 的左邊那個鍵對應的“單引號”。
  • 比如有兩張表:biz_trade、biz_trade_order,現在需要動態將 biz_trade 替換成 biz_trade_9,如果表名前後沒有[`],則 biz_trade_order 也會被替換,替換後爲:biz_trade_9_order,這顯然不是我們希望發生的。

功能概述

  • 分庫:簡單的分庫功能,更確切的講,是多數據源管理,可根據業務動態切換,基於切面(AOP)。
  • 分表:對於同一數據源或不同數據源下的相同表結構的表,通過簡單配置,實現分表查詢功能。
    • 適用數據量增加迅速的業務場景。
    • 底層實現:基於 MyBatis 插件,攔截最終執行的 SQL 語句並且根據分表配置對 SQL 語句中的表名進行修改之後再執行。
      • 要求表名必須用 [`](不包括中括號)引起來。請使用增強插件(mybatis-generator)生成 Mapper 和 entity model。

動態切換數據源的三種方式

  • 通過參數 ShardRequest.java 指定:優先級最高,也最靈活。
    • 可以根據具體業務場景決定要連接哪個數據源。
  • 註解:可用在類和方法上,方法註解優先於類註解。
  • biz service 配置
    • 以上兩種方式均沒有的情況下,會讀取 ShardConfig.shardSchemaInterfaceClassNameList 配置信息,在運行過程中,通過 AOP 攔截 biz.service,從而識別應該使用哪個數據源,達到分庫/多數據源動態切換的目的。
    • 這種方式的優點:可以由專人統一管理,同時生產環境與開發、測試環境可以用不同的配置信息,開發人員與測試人員不用關注分庫的細節。

如果以上三種方式都沒有找到數據源,則使用默認的數據源。

分庫分表思路

  • 分庫思路:
    • 每個庫有一個唯一的標誌,起名叫 shardKeySchema,每個數據庫的 shardKeySchema 與 db-source.xml 定義的數據源 dataSource -> targetDataSources -> map -> key 一一對應。
    • 用戶在初始化時根據業務規則分配到某一個庫,將該庫的 shardKeySchema 保存到用戶表。
  • 分表思路:
    • 每個用戶分配一個用於分表的數字編號 shardKeyTableNumber,同樣保存到用戶表。
  • 用戶表:
    • 集中在一個庫用於統一登錄驗證,登錄時獲取用戶 shardKeySchema 和 shardKeyTableNumber 並將用戶登錄信息緩存於 Session 或非關係型數據庫,業界常用的如 redis、memcached。
  • 業務操作請求:
    • 在請求數據時,就可以根據 shardKeySchema 動態切換數據源,根據 shardKeyTableNumber 決定查哪張表了(分表操作通過 MyBatis 插件實現)。

分表分庫場景

  • 場景一:
    • SaaS 平臺,用戶量成千上萬,交易表 biz_trade 每天100萬級增長,如果只用一個庫的一張表,寫入和讀取壓力會非常大,會成爲瓶頸,所以需要分庫分表。
    • 請求數據時,需要通過 ShardRequest.java 傳 shardKeySchema 和 shardKeyTableNumber 參數。
    • 業務場景之:平均分配
      • 每個數據庫實例最多分配 10 萬用戶,超過 10 萬的用戶,再分配到新庫。
      • 交易記錄平均分到 10 張表,這就意味着用於分表的 shardKeyTableNumber,一個數字編號最多同時分配給一萬個用戶。
      • 用戶請求數據時,將用戶的 shardKeyTableNumber 除以 10,將餘數作爲分表後綴,比如用戶的 shardKeyTableNumber=8888,那麼,8888%10=8,則用戶的交易表是 biz_trade_8。
      • 同理,如果要平均分配到 100 張表,那麼就除以 100 再取餘作爲分表後綴,8888%100=88,則用戶的交易表是 biz_trade_88。
    • 業務場景之:區別對待
      • 在平均分配的基礎上,由於運營需要,現在有 vip 客戶,要保證 vip 客戶的用戶體驗,vip 客戶的數據庫讀寫速度要快,那怎麼辦呢?
      • 其實只要針對這部分用戶再製定一套規則就可以了,因爲 shardKeySchema 和 shardKeyTableNumber 都是可以指定的。
      • 如果用戶由一般用戶變爲了 vip 用戶,那麼在重新指定 shardKeySchema 和 shardKeyTableNumber 之後,用戶原來的數據做相應的遷移即可。
  • 場景二:
    • 不同於場景一,在某一些業務場景,需要與其它業務系統做對接,在其它系統不能提供 api 的情況下,直接操作數據庫無疑是最快也最直接的方式。
    • 這種情況,不同業務數據保存在不同的數據庫,請求數據的時候,對於從哪個數據庫請求數據是明確的,那麼最直接的方式就是使用註解,或者配置 ShardConfig.shardSchemaInterfaceClassNameList。
    • 在不需要分表的情況下,用註解和配置 ShardConfig.shardSchemaInterfaceClassNameList 就夠了,這種情況下請求數據時,不需要通過 ShardRequest(ShardRequest.java)傳 shardKeySchema 和 shardKeyTableNumber 參數。
    • 當然,也可以不用註解也不用配置 ShardConfig.shardSchemaInterfaceClassNameList,還是通過 ShardRequest 傳遞參數也行,怎麼靈活怎麼來。
  • 場景三:
    • 分表是確定的,不是動態分配的,那麼 ShardRequest.java 只傳 shardKeyTable 即可。

運行

  • git clone https://github.com/uncleAndyChen/mybatis-plugin-shard.git
  • 因爲依賴統一管理,添加了一個父模塊:dependencies,只有一個 pom.xml 文件,需要先把這個 model 安裝到本地倉庫,否則會去 maven 配置的倉庫下載。打開 cmd 窗口,在項目根目錄下操作:
cd dependencies
mvn clean
mvn compile
mvn install
  • 強烈建議:maven 遠程倉庫添加阿里雲鏡像。
    • 修改 maven 根目錄下 config/settings.xml,在 <mirrors> 下添加:
<mirror> 
    <id>alimaven</id> 
    <name>aliyun maven</name> 
    <url>https://maven.aliyun.com/repository/jcenter</url> 
    <mirrorOf>central</mirrorOf> 
</mirror>
  • 用你喜歡的 IDE 導入項目,如果你要我推薦一款 IDE,那麼我強烈推薦 IntelliJ IDEA,官網:http://www.jetbrains.com/
  • IDE 安裝 Lombok 插件。
  • MySQL 數據庫,導入 docs/schemas.sql
  • 修改 biz/biz-config/src/main/resources/jdbc.properties 中連接數據庫的參數
  • 啓動
  • 訪問:http://localhost:81,可以測試以三種不同方式切換數據源來查詢數據。具體細節請看源代碼,以後會出詳細的文檔,敬請期待。

數據源配置(部分)

<bean id="dataSource" class="common.aspect.ChooseDataSource" primary="true">
    <property name="defaultTargetDataSource" ref="dataSourceSystem"/>
    <!-- 下面的各個 0key 需要配置到 shardTableConfigView 的 schemaKeyList -->
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <entry key="system" value-ref="dataSourceSystem"/>
            <entry key="student" value-ref="dataSourceStudent"/>
            <entry key="finance" value-ref="dataSourceFinance"/>
            <entry key="biz" value-ref="dataSourceBiz"/>
        </map>
    </property>
</bean>

配置分表分庫配置類

<!-- 以下配置,部分表名只是用於配置示例,僅爲了更好的展示如何配置。
    本項目沒有用到的表名有:edu_class、biz_trade_order、biz_item、biz_item_sku
-->
<bean id="shardConfig" class="common.shard.ShardConfig" >
    <!-- 列表值爲 dataSource.targetDataSources 的 keys  -->
    <property name="schemaKeyList">
        <list>
            <value>system</value>
            <value>student</value>
            <value>finance</value>
            <value>biz</value>
        </list>
    </property>
    <!-- 基於服務接口分庫策略,
        把針對某個 schema 的接口配置在該數據源 key 對應的 list 下,沒有就不配置
    -->
    <property name="shardSchemaInterfaceClassNameList">
        <map>
            <entry key="student">
                <list>
                    <value>biz.service.facade.IEduStudentService</value>
                </list>
            </entry>
        </map>
    </property>
    <!-- 分表策略
        直接將 ShardRequest.shardKeyTable(優先級高於後者) 或 ShardRequest.shardKeyTableNumber 作爲分表後綴的表。
         ShardRequest 參見:https://github.com/uncleAndyChen/mybatis-plugin-shard/blob/master/common/common-shard/src/main/java/common/shard/ShardRequest.java
     -->
    <property name="shardTableDirectlyList">
        <list>
            <value>edu_student</value>
            <value>edu_class</value>
        </list>
    </property>
    <!-- 分表策略
        通過兩個數相除取餘作爲後綴的表,配合 ShardRequest.shardKeyTableNumber 使用
        ShardRequest 參見:https://github.com/uncleAndyChen/mybatis-plugin-shard/blob/master/common/common-shard/src/main/java/common/shard/ShardRequest.java
    -->
    <!-- key 將作爲 shardKeyTableNumber 的除數(取餘), 餘數作爲分表後綴-->
    <!-- shardKeyTableNumber 通過 ShardRequest 傳遞,在請求 api 時傳遞 -->
    <property name="shardTableDivideList">
        <map>
            <entry key="10">
                <list>
                    <value>biz_trade</value>
                    <value>biz_trade_order</value>
                </list>
            </entry>
            <entry key="5">
                <list>
                    <value>biz_item</value>
                    <value>biz_item_sku</value>
                </list>
            </entry>
        </map>
    </property>
    <!-- 打印分表的 sql 語句,默認爲 false 即不打印。-->
    <property name="printShardSqlInfo" value="true" />
    <!-- 不需要分表的 sql 語句列表,以下這句爲 MyBatis 操作數據庫新增記錄時,查詢新增的主鍵值的語句 -->
    <property name="notNeedShardSqlList">
        <list>
            <value>SELECT LAST_INSERT_ID()</value>
        </list>
    </property>
</bean>

切面配置

<!-- 用於切面,實現攔截數據庫操作,實現分庫分表的類 -->
<bean id="dataSourceAspect" class="common.aspect.DataSourceAspect">
    <property name="shardTableConfigView" ref="shardConfig" />
</bean>

<!-- 定義切面,用於攔截數據庫操作,實現分庫分表 -->
<aop:config proxy-target-class="true">
    <aop:aspect id="dataSourceAspect" ref="dataSourceAspect" order="1">
        <aop:pointcut id="point" expression="(execution(* biz.service.impl.*.*(..)))"/>
        <aop:before pointcut-ref="point" method="before"/>
        <aop:after pointcut-ref="point" method="afterHandler"/>
    </aop:aspect>
</aop:config>

請求參數 ShardRequest.java 類

public class ShardRequest {
    /**
     * 分庫標誌 key,是定義數據源時指定的 key,在執行數據庫操作之前,通過該 key 動態切換數據源。
     * 如果只是分庫,除了用到個屬性,還可利用 ShardTableConfig.shardSchemaInterfaceNameList 實現。
     *      有關這兩項配置的詳細信息,請參見:https://github.com/uncleAndyChen/mybatis-plugin-shard/blob/master/biz/biz-config/src/main/resources/db-source.xml
     */
    private String shardKeySchema;

    /**
     * 分表標誌 key,直接用作分表後綴的 key 值,針對直接添加後綴的表
     *      舉例:應用該規則的原始表名爲 table_name,則對應的分表爲:table_name_key
     * 需要配合 ShardTableConfig 使用,與該類位於同一個目錄,在 db-source.xml 中配置各屬性值
     *     應用該規則的原始表名:ShardTableConfig.shardTableDirectlyList
     *          詳細描述,請參見:https://github.com/uncleAndyChen/mybatis-plugin-shard/blob/master/biz/biz-config/src/main/resources/db-source.xml
     */
    private String shardKeyTable;

    /**
     * 動態分表參數編號,整形,一般與用戶綁定,針對需要除一個數得到後綴的表
     * 需要配合 ShardTableConfig 使用,與該類位於同一個目錄,在 db-source.xml 中配置各屬性值
     *     應用該規則的原始表名:ShardTableConfig.shardTableDivideList
     *          詳細描述,請參見:https://github.com/uncleAndyChen/mybatis-plugin-shard/blob/master/biz/biz-config/src/main/resources/db-source.xml
     *
     * 場景:SaaS 平臺,每個用戶分配一個編碼值,可以按一定規則平均分配,比如現有有10萬個用戶,我們打算分10張表,那麼,平均分配的話,就意味着每一萬個用戶有一個分表編號。
     * 極端地,對於 SasS 的超級 VIP 用戶,可以分配一個唯一的分表編號,這就意味着這個 VIP 用戶獨享一套表。
     * 多個用戶的數據可能存在於同一個數據庫實例,也可能存在於多個數據庫實例,可根據業務靈活分配。
     */
    private int shardKeyTableNumber;

    // getter and setter
    // ...
}

重新生成 mapper 和 entity

請參考 生成 Mapper 操作

有關 {xxx}Mapper.xml 文件

我是直接把 MBG 生成的 {xxx}Mapper.xml 文件放到了 biz-service-dal 模塊下與 {xxx}Mapper.java 平級的目錄下了,包名爲:biz.mapper.xml.originalbiz.mapper.xml.extend

默認情況下,xml 文件不會被打包,所以,運行的時候會出現類似這樣的錯誤:

Invalid bound statement (not found): biz.service.dal.mapper.original.EduStudentMapper.selectByExample

解決:需要在 pom.xml 裏設置爲需要將 xml 一起打包,如下:

<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
</build>

directory 配置到 xml 的父目錄 src/main/java/biz/mapper/xml 不會生效,配置成 src/main/java 就好。

技術清單

  • JDK 1.8,理論上支持 1.8 以上的版本,如需升級,比如要改爲 JDK 11,將文件 ./dependencies/pom.xml<java.version>1.8</java.version> 改爲 <java.version>11</java.version>
  • MySQL 5.6.46、MySQL 5.7,用這兩個版本作的測試,理論上支持 5.6 及以上版本。
  • maven 依賴庫
    • maven 依賴版本在 ./dependencies/pom.xml 維護,如果要升級某一框架的版本,只需要修改這個文件就行,模塊 dependencies 被作爲其它模塊的 parent,目的就是統一管理版本,同樣的依賴庫只定義一次版本號。
    • 以下依賴爲當前(2020-01-06)最新版本
      • Spring Boot 2.2.2.RELEASE
      • Spring Framework 5.2.2.RELEASE (common-shard 模塊直接依賴了 spring framework 下的 spring-aspects)
      • MyBatis 3.5.3
      • druid 1.1.21
      • lombok 1.18.10
      • jackson 2.10.1

支持

如果有疑問或建議,歡迎請提 Issue
可能不會立即回覆,尤其上班時間,不過我會盡量抽業餘時間回覆的。

如果幫到了你

請 Star 一下,讓我有動力繼續完善和優化。

關於作者

發佈了57 篇原創文章 · 獲贊 10 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章