基于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,可以測試以三種不同方式切換數據源來查詢數據。具體細節請看源代碼,以后會出詳細的文檔,敬請期待。
    image

數據源配置(部分)

<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 一下,讓我有動力繼續完善和優化。

關于作者

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容

  • 吳越永安之邑,有綠竹參天,蒼翠巍然之山者,出通幽小道,隱清泉于其中。秋露微寒,余以偷閑為樂,驅而往之,亦欲托山水之...
    白色烏托邦閱讀 305評論 0 3
  • 已經分手好多天了 昨晚是除夕之夜 凌晨看到你朋友圈 那是東京塔星光燦爛 你給它取名中國紅 點贊后睡去 竟夢到你發朋...
    你是我心里最難忘的記逸閱讀 322評論 0 1
  • 總要忙起來,才會過的充實, 不能虛度光陰,多做有意義的事!
    京心達查曉旭閱讀 29評論 0 0
  • 初一 看到年老的奶奶完全看不見東西,鞋子跟小孩一樣反著穿,忽然覺得心理一陣難受。 初二 媽媽從親戚口中聽說了春慶叔...
    吾不里閱讀 244評論 0 0
  • 生活中很少見到綁架案,但是當老板說你要不完成任務就要被解雇,對手說你不出高價我就賣給別人了,孩子說你不給我玩手機我...
    皓熹閱讀 380評論 0 1