基于spring 切面(AOP)實現動態多數據源切換;基于 MyBatis 插件方式實現動態分表查詢。 來源于多個已上線項目實踐,本項目有完整的測試示例。
mybatis-plugin-shard
- 基于spring 切面(AOP)實現動態多數據源切換。
- 基于 MyBatis 插件方式實現動態分表策略。
- 來源于多個已上線項目實踐。
- 本項目有完整的測試示例。
以后會出詳細的文檔,敬請期待。
todo
- 將分庫分表配置與數據源配置統一放到文件 db-config.xml,并作為配置的切面的參數,在整個分庫分表過程都可訪問。
- 完善分表邏輯,比起之前將分庫分表配置在一個文件中更加優雅,也更加靈活,擴展性越好。
- 完善文檔
項目地址
- github: https://github.com/uncleAndyChen/mybatis-plugin-shard
- gitee: ??https://gitee.com/uncleAndyChen/mybatis-plugin-shard
配套 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>
下添加:
- 修改 maven 根目錄下
<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.original
和 biz.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
- maven 依賴版本在
支持
如果有疑問或建議,歡迎請提 Issue。
可能不會立即回復,尤其上班時間,不過我會盡量抽業余時間回復的。
如果幫到了你
請 Star 一下,讓我有動力繼續完善和優化。