基于spring 切面(AOP)實現(xiàn)動態(tài)多數(shù)據(jù)源切換,基于 MyBatis 插件方式實現(xiàn)動態(tài)分表查詢

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

mybatis-plugin-shard

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

以后會出詳細(xì)的文檔,敬請期待。

todo

  • 將分庫分表配置與數(shù)據(jù)源配置統(tǒng)一放到文件 db-config.xml,并作為配置的切面的參數(shù),在整個分庫分表過程都可訪問。
  • 完善分表邏輯,比起之前將分庫分表配置在一個文件中更加優(yōu)雅,也更加靈活,擴(kuò)展性越好。
  • 完善文檔

項目地址

配套 MBG 增強(qiáng)插件

查看 MBG 增強(qiáng)插件請移步:mybatis-generator

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

功能概述

  • 分庫:簡單的分庫功能,更確切的講,是多數(shù)據(jù)源管理,可根據(jù)業(yè)務(wù)動態(tài)切換,基于切面(AOP)。
  • 分表:對于同一數(shù)據(jù)源或不同數(shù)據(jù)源下的相同表結(jié)構(gòu)的表,通過簡單配置,實現(xiàn)分表查詢功能。
    • 適用數(shù)據(jù)量增加迅速的業(yè)務(wù)場景。
    • 底層實現(xiàn):基于 MyBatis 插件,攔截最終執(zhí)行的 SQL 語句并且根據(jù)分表配置對 SQL 語句中的表名進(jìn)行修改之后再執(zhí)行。
      • 要求表名必須用 [`](不包括中括號)引起來。請使用增強(qiáng)插件(mybatis-generator)生成 Mapper 和 entity model。

動態(tài)切換數(shù)據(jù)源的三種方式

  • 通過參數(shù) ShardRequest.java 指定:優(yōu)先級最高,也最靈活。
    • 可以根據(jù)具體業(yè)務(wù)場景決定要連接哪個數(shù)據(jù)源。
  • 注解:可用在類和方法上,方法注解優(yōu)先于類注解。
  • biz service 配置
    • 以上兩種方式均沒有的情況下,會讀取 ShardConfig.shardSchemaInterfaceClassNameList 配置信息,在運行過程中,通過 AOP 攔截 biz.service,從而識別應(yīng)該使用哪個數(shù)據(jù)源,達(dá)到分庫/多數(shù)據(jù)源動態(tài)切換的目的。
    • 這種方式的優(yōu)點:可以由專人統(tǒng)一管理,同時生產(chǎn)環(huán)境與開發(fā)、測試環(huán)境可以用不同的配置信息,開發(fā)人員與測試人員不用關(guān)注分庫的細(xì)節(jié)。

如果以上三種方式都沒有找到數(shù)據(jù)源,則使用默認(rèn)的數(shù)據(jù)源。

分庫分表思路

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

分表分庫場景

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

運行

  • git clone https://github.com/uncleAndyChen/mybatis-plugin-shard.git
  • 因為依賴統(tǒng)一管理,添加了一個父模塊:dependencies,只有一個 pom.xml 文件,需要先把這個 model 安裝到本地倉庫,否則會去 maven 配置的倉庫下載。打開 cmd 窗口,在項目根目錄下操作:
cd dependencies
mvn clean
mvn compile
mvn install
  • 強(qiáng)烈建議:maven 遠(yuǎn)程倉庫添加阿里云鏡像。
    • 修改 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 導(dǎo)入項目,如果你要我推薦一款 IDE,那么我強(qiáng)烈推薦 IntelliJ IDEA,官網(wǎng):http://www.jetbrains.com/
  • IDE 安裝 Lombok 插件。
  • MySQL 數(shù)據(jù)庫,導(dǎo)入 docs/schemas.sql
  • 修改 biz/biz-config/src/main/resources/jdbc.properties 中連接數(shù)據(jù)庫的參數(shù)
  • 啟動
  • 訪問:http://localhost:81,可以測試以三種不同方式切換數(shù)據(jù)源來查詢數(shù)據(jù)。具體細(xì)節(jié)請看源代碼,以后會出詳細(xì)的文檔,敬請期待。
    image

數(shù)據(jù)源配置(部分)

<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>
    <!-- 基于服務(wù)接口分庫策略,
        把針對某個 schema 的接口配置在該數(shù)據(jù)源 key 對應(yīng)的 list 下,沒有就不配置
    -->
    <property name="shardSchemaInterfaceClassNameList">
        <map>
            <entry key="student">
                <list>
                    <value>biz.service.facade.IEduStudentService</value>
                </list>
            </entry>
        </map>
    </property>
    <!-- 分表策略
        直接將 ShardRequest.shardKeyTable(優(yōu)先級高于后者) 或 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>
    <!-- 分表策略
        通過兩個數(shù)相除取余作為后綴的表,配合 ShardRequest.shardKeyTableNumber 使用
        ShardRequest 參見:https://github.com/uncleAndyChen/mybatis-plugin-shard/blob/master/common/common-shard/src/main/java/common/shard/ShardRequest.java
    -->
    <!-- key 將作為 shardKeyTableNumber 的除數(shù)(取余), 余數(shù)作為分表后綴-->
    <!-- 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 語句,默認(rèn)為 false 即不打印。-->
    <property name="printShardSqlInfo" value="true" />
    <!-- 不需要分表的 sql 語句列表,以下這句為 MyBatis 操作數(shù)據(jù)庫新增記錄時,查詢新增的主鍵值的語句 -->
    <property name="notNeedShardSqlList">
        <list>
            <value>SELECT LAST_INSERT_ID()</value>
        </list>
    </property>
</bean>

切面配置

<!-- 用于切面,實現(xiàn)攔截數(shù)據(jù)庫操作,實現(xiàn)分庫分表的類 -->
<bean id="dataSourceAspect" class="common.aspect.DataSourceAspect">
    <property name="shardTableConfigView" ref="shardConfig" />
</bean>

<!-- 定義切面,用于攔截數(shù)據(jù)庫操作,實現(xiàn)分庫分表 -->
<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>

請求參數(shù) ShardRequest.java 類

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

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

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

    // getter and setter
    // ...
}

重新生成 mapper 和 entity

請參考 生成 Mapper 操作

有關(guān) {xxx}Mapper.xml 文件

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

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

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

解決:需要在 pom.xml 里設(shè)置為需要將 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 就好。

技術(shù)清單

  • 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 維護(hù),如果要升級某一框架的版本,只需要修改這個文件就行,模塊 dependencies 被作為其它模塊的 parent,目的就是統(tǒng)一管理版本,同樣的依賴庫只定義一次版本號。
    • 以下依賴為當(dāng)前(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

支持

如果有疑問或建議,歡迎請?zhí)?Issue
可能不會立即回復(fù),尤其上班時間,不過我會盡量抽業(yè)余時間回復(fù)的。

如果幫到了你

請 Star 一下,讓我有動力繼續(xù)完善和優(yōu)化。

關(guān)于作者

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容