選擇開源核心組件的一個非常重要的考慮通常是社區活躍性,一旦項目團隊無法進行自己后續維護和擴展的情況下更是如此。
至於為什么選擇sharding-jdbc而不是Mycat,可以參考知乎討論帖子https://www.zhihu.com/question/64709787。
還可以參考https://blog.csdn.net/u013898617/article/details/79615427。
關於分庫分表和讀寫分離、主從
一般來說,需要分庫分表的系統是流量比較大的,而且比較容易出現峰值的比如說打折/活動的時候;其次,當單機扛不住業務流量的時候,分庫分表一定不是第一選擇,在分庫分表之前,應該先保證垂直拆分完成了,子系統內都是高內聚的,其次基於Master-Slave的讀寫分離或者模糊查詢很多的,可能NoSQL比如elastic就引流去很大一部分了。當讀寫分離也做完了,主庫只剩下關鍵業務邏輯之后,流量還是很高,這個時候才開始考慮分庫分表。因為相對於讀寫分離、垂直拆分,分庫分表對開發和運維的要求多得多,如果確定業務一兩年內不會劇增的,盲目引入只會導致成本高昂(尤其是各種SQL限制)。
其次,分庫分表會增加N倍的數據庫服務器,一般來說是4的倍數,如果某個應用說要做分庫分表,又只有兩台機器,那完全就是湊熱鬧。
讀寫分離和分庫分表應該來說是前后的兩件事比較合理,不少人將這兩個事情混到一起去講准確的說不合理的。分庫分表通常更多的是用於純粹的OLTP的事情,讓系統可以水平擴展。而讀寫分離更多的是為了把一部分可以容忍短時延遲/不保證100%(但應該在99%以上)准確的查詢路由到查詢庫,准確的說是對業務根據優先級做個歸類,這些查詢從資源消耗的角度來說相對邏輯上的PK查詢要高幾倍到數百倍,比如說查詢某個人過去3個月的交易情況,但他從業務角度並不算是DSS概念,比如說查詢已經T-N的訂單/針對這些訂單進行導出,並針對這個單子可能會進行一些操作,甚至人工修改狀態。通過把這些功能從核心的訂單/交易/資金OLTP中拆分出去,可以保證核心業務系統不會因為一個異常操作比如SQL不合理導致系統出現業務負載增加外的額外抖動因素。
從系統設計角度來說,讀寫分離雖然從邏輯表結構角度來說是相同的,都具有相同的字段定義,但是物理實現上是一定是不相同(要是完全相同,說明沒有領會讀寫分離的初衷)的,尤其是在索引上,寫庫可能除了PK、唯一索引(任何表都應該有唯一索引,分布式鎖只能作為緩沖器)外,最多還有一兩個簡單的字段索引以最大化性能,任何SQL符合條件的數據一般不會超過幾十條(極端客戶除外),但是讀庫根據業務不同,可能會有很多的索引以滿足各種查詢以及簡單的統計匯總報表的需求。
那寫庫是不是一定就比讀庫重要呢?是,又不是。是是絕對的,不是是相對的。因為讀庫不是DSS庫,是交易庫中相對來說不是特別重要的業務功能。所以,寫庫一旦掛了,就會導致業務下不去,讀庫掛了,可能會導致做業務的人決策錯誤。比如沒有沒有查到做過某交易,又重新交易一次。
考慮分庫分表一個很重要的決策就是是否允許跨多庫操作以及有沒有必要。TODO。。。。待補充。。。。。。
其次,分庫和分表是兩件事,是到底選擇分庫還是分表,如果量沒有那么大的話而且是虛擬機的話,估計分庫就夠了。
sharding-jdbc的版本及其架構差異
目前最新版本的sharding-jdbc是v3.0.0.M1,應該來說還不穩定,包名又改成了io.shardingsphere,jar包名是sharding-jdbc。
1.5.4.1是目前最新版,也可能是終版,1.x的,坐標是com.dangdang,jar包名是sharding-jdbc。
2.0.3是目前最新版,包名和坐標統一改成了io.shardingjdbc,jar包名是sharding-jdbc-core。
說明規划不是特別好,還是有些亂。
因為2.0之后基本上純粹分庫分表的核心特性的增強就不多了,主要往治理和代理方向去了,所以如果沒有特別的需求比如需要類似mycat的獨立服務代理模式,使用1.x(注:1.x版本官網文檔好像下線了)就可以了,不過如果是大規模的部署,同時已經使用了微服務架構中的注冊中心或者基於spring boot,可以考慮使用2.0,因為2.0增加了基於注冊中心的配置管理以及spring boot starter。所以2.0的架構是這樣的:
3.0之后,增加了類似mycat角色的sharding-proxy無狀態服務器(代理層可以有,但是不應該作為應用直接訪問的入口,如下圖所示),以及類似於Service Mesh的Database Mesh。不過核心沒有變化,對SQL語法增加了部分新的支持。所以3.0的架構如下:
就分庫分表核心來說,我們就只要關心sharding-jdbc就可以了,不需要關心sharding-sphere的其他部分。
sharding-jdbc/Sharding-Proxy/Sharding-Sidecar三者的對比如下:
核心特性
事務
對任何重要的系統來說,事務一致性都是關鍵的。對分布式系統來說更是如此,最重要的就是事務一致性,從這個層面來說,分庫分表本身並沒有引入另外一個層次的復雜性。因為它在jdbc驅動層包了一層,所以我們有必要理解它對事務的支持性以及相關的限制。事務
sharding jdbc 2.x不支持強一致性的分布式事務,一般現在的系統設計也不追求強一致性,而是最終一致性。所以sharding jdbc 2.x支持2中事務:弱XA(同mycat)和最大努力投遞事務(官方簡稱BED)(算是BASE的一種),具體選擇哪一種需要根據業務和開發進行權衡,如果架構規范和設計做得好,是可以做到不跨庫分布式事務的。
弱XA事務是默認的模式(即只要dml期間沒有拋出異常,commit期間有機器斷網或者宕機,是無法保證一致性的),沒有特別的要求。
BED則在一定上增加了短時容忍,將執行的語句另作中心化存儲,然后輪詢commit期間失敗的事務重試,所以BED的架構如下:
但是沒有免費的午餐,BED對開發和維護有着一定的額外要求,而且這些要求都涉及面很廣,絕對算得上傷筋動骨。開發層面包括:
- INSERT語句的主鍵不能自增
- UPDATE必須可重復執行,比如不支持UPDATE xxx SET x=x+1,對於更新余額類,這就相當於要求必須樂觀鎖了
運維層面包括:
- 需要存儲事務日志的數據庫
- 用於異步作業使用的zookeeper
- 解壓sharding-jdbc-transaction-async-job-$VERSION.tar,通過start.sh腳本啟動異步作業
我們選擇了從設計層面避免強一致性的分布式事務。
分片靈活性
對於分庫分表來說,很重要的一個特性是分片的靈活性,比如單個字段、多個字段的=、IN、>=、<=。為什么多個字段很重要的,這里涉及到一個特殊的考慮
sharding-jdbc目前提供4種分片算法。
由於分片算法和業務實現緊密相關,因此並未提供內置分片算法,而是通過分片策略將各種場景提煉出來,提供更高層級的抽象,並提供接口讓應用開發者自行實現分片算法。
- 精確分片算法
對應PreciseShardingAlgorithm,用於處理使用單一鍵作為分片鍵的=與IN進行分片的場景。需要配合StandardShardingStrategy使用。
- 范圍分片算法
對應RangeShardingAlgorithm,用於處理使用單一鍵作為分片鍵的BETWEEN AND進行分片的場景。需要配合StandardShardingStrategy使用。
- 復合分片算法
對應ComplexKeysShardingAlgorithm,用於處理使用多鍵作為分片鍵進行分片的場景,多分片鍵邏輯較復雜,需要應用開發者自行處理其中的復雜度。需要配合ComplexShardingStrategy使用。
- Hint分片算法(Hint分片指的是對於分片字段非SQL決定,而由其他外置條件決定的場景,可使用SQL Hint靈活的注入分片字段。例:內部系統,按照員工登錄ID分庫,而數據庫中並無此字段。SQL Hint支持通過Java API和SQL注釋(待實現)兩種方式使用。)
對應HintShardingAlgorithm,用於處理使用Hint行分片的場景。需要配合HintShardingStrategy使用。
因為算法的靈活性,標准的方式是通過實現具體的java接口是實現具體的分片算法比如SingleKeyDatabaseShardingAlgorithm,有不少的情況下,分片是比較簡單的,比如說純粹是客戶編號,此時提供了行內表達式分片策略,使用Groovy的表達式,提供對SQL語句中的=和IN的分片操作支持,不過這只支持單分片鍵。比如,t_user_${u_id % 8}
表示t_user表按照u_id按8取模分成8個表,表名稱為t_user_0
到t_user_7
。
分片鍵+分片算法=真正可用的分片策略。
算法和分片鍵的選擇是分庫分表的關鍵,其直接決定了各個分庫的負載是否均衡,以及擴展是否容易。在設計上的考慮一節筆者會詳細闡述,訂單和委托業務、用戶在使用分庫分表時設計上的考慮以及原因。
SQL語法限制
對於分庫分表來說,還需要知道有哪些SQL的限制,尤其是涉及到需要二次處理的,比如排序,去重,聚合等。
這里筆者就列下那些常用但沒有被支持的。比如:
- case when
- distinct
- union
不過好在這些在java/js中處理都比較方便。
如果有很復雜的SQL,那最大的可能就是設計上有問題,應該采用讀寫分離解決。
sharding-jdbc對SQL的限制完整可以參考http://shardingsphere.io/document/current/cn/features/sharding/usage-standard/sql/
設計上的考慮
哪些表要分庫分表
首先從設計上要區分清楚哪些是廣播表/哪些是分庫表/哪些是只在一個庫的全局表,因為是公用數據源的,所以不管是不是分庫的表,都需要配置,不配置分片規則Sharding-JDB即無法精確的斷定應該路由至哪個數據源。但是一般分庫分表組件包括Sharding-JDBC都會提供簡化配置的方法。對於不分片的表:
方法1:sharding-jdbc可以在<sharding:sharding-rule />配置default-data-source-name,這樣未配置分片規則的表將通過默認數據源定位。
方法2:將不參與分庫分表的數據源獨立於Sharding-JDBC之外,在應用中使用多個數據源分別處理分片和不分片的情況。
分庫還是分表
一般來說應該選擇分庫(准確的說是分schema),不應該使用分表, 因為oracle可以包含n個schema,mysql可以包含多個database,而且就算真的需要,schema之間也是可以關聯查詢的,所以感覺就算是為了擴展性問題,也沒有必要使用分表,分表反而在擴展的時候更加麻煩。就算數據量多了一點,感覺稍微偏慢,可以先采用分區擋一擋。
分片鍵的選擇
其中最重要的是分片鍵不能是自增字段,否則insert就不知道去哪里了。所以對於ID生成,需要一個ID生成中心。
分布式主鍵
分布式系統的主鍵生成可以通過設置增量或者也通過ID生成中心來生成,不過話說回來,既然使用ID生成中心了,就不要再使用數據庫機制的ID了,這不一定需要通過代碼全部重寫,可以在dao層通過aop判斷是否insert,是Insert的動態從ID中心獲取,這樣就避免了分庫分表和非分庫分表在開發商的差別。
Sharding-JDBC使用說明
對於只有一個分片鍵的使用=和IN進行分片的SQL,建議使用行表達式代替Java類的配置。假設我們不使用弱性事務(如果使用柔性事務,則還需要引入sharding-jdbc-transaction以及sharding-jdbc-transaction-async-job),這樣就只要引入sharding-jdbc-core這個jar包就可以了,(因為sharding-jdbc的配置支持java、yaml、spring boot以及spring命名空間(類似dubbo),所以建議使用spring 命名空間方式)如下:
<dependency> <groupId>io.shardingjdbc</groupId> <artifactId>sharding-jdbc-core</artifactId> <version>2.0.3</version> </dependency> <!-- for sharding-jdbc spring namespace --> <dependency> <groupId>io.shardingjdbc</groupId> <artifactId>sharding-jdbc-core-spring-namespace</artifactId> <version>2.0.3</version> </dependency>
因為sharding jdbc可以支持dbcp、druid,所以就不用改動其他的依賴項了。
接下去配置數據源、數據源分片策略和表分片策略。
我們假設分庫結構(此處2個庫僅做演示目的,實際應該至少4個分庫)如下:
order和order_item通過order_id關聯,且分片規則相同。
分庫1和分庫2通過user_id作為分片字段,采用取模(采用取模而不是區間的好處是負載從理論上最均衡,用區間其實是不均衡的,比如假設客戶比較穩定,就1000萬,0-100萬第一個分片,以此類推,實際上還是可能很不均衡,因為某段時間增加的客戶可能會特別活躍,尤其是在互聯網應用中)
配置三個數據源:
1、globalDataSource,指向全局庫,非分庫分表和廣播的表,比如系統參數;
2、dataSource_0、dataSource_1,指向分庫1和分庫2;
<bean name="dataSource_0" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${jdbc.url_0}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <!-- 配置初始化大小、最小、最大 --> <property name="initialSize" value="${jdbc.initialSize}"/> <property name="minIdle" value="${jdbc.minIdle}"/> <property name="maxActive" value="${jdbc.maxActive}"/> <!-- 配置獲取連接等待超時的時間 --> <property name="maxWait" value="${jdbc.maxWait}"/> <!-- 打開PSCache,並且指定每個連接上PSCache的大小 --> <property name="poolPreparedStatements" value="${jdbc.pps}"/> <property name="maxPoolPreparedStatementPerConnectionSize" value="${jdbc.mpps}"/> <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}"/> <!-- 配置一個連接在池中最小生存的時間,單位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}"/> <property name="removeAbandoned" value="${jdbc.removeAbandoned}"/> <property name="removeAbandonedTimeout" value="${jdbc.removeAbandonedTimeout}"/> <property name="logAbandoned" value="${jdbc.logAbandoned}"/> <!-- 配置監控統計攔截的filters --> <property name="filters" value="${jdbc.filters}"/> </bean> <bean name="dataSource_1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${jdbc.url_1}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <!-- 配置初始化大小、最小、最大 --> <property name="initialSize" value="${jdbc.initialSize}"/> <property name="minIdle" value="${jdbc.minIdle}"/> <property name="maxActive" value="${jdbc.maxActive}"/> <!-- 配置獲取連接等待超時的時間 --> <property name="maxWait" value="${jdbc.maxWait}"/> <!-- 打開PSCache,並且指定每個連接上PSCache的大小 --> <property name="poolPreparedStatements" value="${jdbc.pps}"/> <property name="maxPoolPreparedStatementPerConnectionSize" value="${jdbc.mpps}"/> <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}"/> <!-- 配置一個連接在池中最小生存的時間,單位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}"/> <property name="removeAbandoned" value="${jdbc.removeAbandoned}"/> <property name="removeAbandonedTimeout" value="${jdbc.removeAbandonedTimeout}"/> <property name="logAbandoned" value="${jdbc.logAbandoned}"/> <!-- 配置監控統計攔截的filters --> <property name="filters" value="${jdbc.filters}"/> </bean> <bean name="globalDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <!-- 配置初始化大小、最小、最大 --> <property name="initialSize" value="${jdbc.initialSize}"/> <property name="minIdle" value="${jdbc.minIdle}"/> <property name="maxActive" value="${jdbc.maxActive}"/> <!-- 配置獲取連接等待超時的時間 --> <property name="maxWait" value="${jdbc.maxWait}"/> <!-- 打開PSCache,並且指定每個連接上PSCache的大小 --> <property name="poolPreparedStatements" value="${jdbc.pps}"/> <property name="maxPoolPreparedStatementPerConnectionSize" value="${jdbc.mpps}"/> <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}"/> <!-- 配置一個連接在池中最小生存的時間,單位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}"/> <property name="removeAbandoned" value="${jdbc.removeAbandoned}"/> <property name="removeAbandonedTimeout" value="${jdbc.removeAbandonedTimeout}"/> <property name="logAbandoned" value="${jdbc.logAbandoned}"/> <!-- 配置監控統計攔截的filters --> <property name="filters" value="${jdbc.filters}"/> </bean>
全局表、分庫不分表、分庫分表、廣播表、主、子表綁定的各自規則配置(下面使用行內策略,注:https://blog.csdn.net/shijiemozujiejie/article/details/80786231提到行表達式的策略不利於數據庫和表的橫向擴展,這是有道理的,因為java代碼我們可以隨時重新加載分庫和分表的基數,做到不停機擴展,但是行內表達式不行)如下:
<!-- 必須加上ignore-unresolvable,否則很可能會出現java.lang.IllegalArgumentException: Could not resolve placeholder以及cannot invoke method mod() on null object-->
<context:property-placeholder location="classpath:jrescloud.properties" ignore-unresolvable="true" order="1"/>
<!-- 分庫策略,盡量使用sharding:standard-strategy而不是inline-stragegy --> <sharding:inline-strategy id="databaseStrategy" sharding-column="user_id" algorithm-expression="dataSource_${user_id % 2}" /> <!-- 分表策略 --> <sharding:inline-strategy id="orderTableStrategy" sharding-column="order_id" algorithm-expression="t_order_${order_id % 2}" /> <sharding:inline-strategy id="orderItemTableStrategy" sharding-column="order_id" algorithm-expression="t_order_item_${order_id % 2}" /> <sharding:data-source id="shardingDataSource"> <!-- configDataSource為不參數分庫分表的全局表的默認數據源,比如系統參數 --> <sharding:sharding-rule data-source-names="dataSource_0,dataSource_1,globalDataSource" default-data-source-name="globalDataSource"> <sharding:table-rules> <!-- 分庫+分表 --> <sharding:table-rule logic-table="t_order" actual-data-nodes="dataSource_${0..1}.t_order_${0..1}" database-strategy-ref="databaseStrategy" table-strategy-ref="orderTableStrategy" /> <sharding:table-rule logic-table="t_order_item" actual-data-nodes="dataSource_${0..1}.t_order_item_${0..1}" database-strategy-ref="databaseStrategy" table-strategy-ref="orderItemTableStrategy" />
<!-- 分庫不分表 --> <sharding:table-rule logic-table="t_order" database-strategy-ref="databaseStrategy"/> <!-- 廣播表 --> <sharding:table-rule logic-table="t_dict"/> </sharding:table-rules> <!-- 綁定表規則列表,表示分庫分表的規則相同,這樣萬一涉及到多個分片的查詢,sharding-jdbc就可以確定分庫之間不需要不必要的二次關聯,所有的查詢都應該如此 --> <sharding:binding-table-rules> <sharding:binding-table-rule logic-tables="t_order,t_order_item"/> </sharding:binding-table-rules> </sharding:sharding-rule> </sharding:data-source> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="shardingDataSource" /> </bean> <!-- 使用annotation 自動注冊bean, 並保證@Required、@Autowired的屬性被注入 --> <tx:annotation-driven transaction-manager="transactionManager"/> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="configLocation" value="classpath:mybatis.xml"/> <property name="dataSource" ref="shardingDataSource"/> <property name="mapperLocations"> <list> <value>classpath*:sqlmap/com/yidoo/k3c/**/*.xml</value> </list> </property> </bean>
HINT分片策略的使用
Hint分片指的是對於分片字段非SQL決定,而由其他外置條件決定的場景,可使用SQL Hint靈活的注入分片字段。舉個實例的例子,我們都知道mysql insert values相對於循環插入來說,性能差距基本上可以說是網絡延時的倍數,比如說插入1000條記錄,網絡來回1ms,1000條就得1m,如果是跨網段的,延時就更長了。而sharding-jdbc和Mycat都不支持多values(sharding-jdbc 3.x支持,但還沒有正式發布),有些SQL語句比較復雜比如說有(a = var or b = var) and member_id = N,這個時候druid sqlparser並不支持的時候,雖然可以通過多數據源解決,但是我們不希望架構搞得太復雜,這種情況下,可以通過sharding-jdbc的Hint分片策略來實現各種sharding-jdbc不支持的語法的限制。因為Hint分片策略是繞過SQL解析的,所以對於這些比較復雜的需要分片的非DSS查詢,采用Hint分片策略性能可能會更好。同樣,我們還是使用mybatis作為數據庫訪問層作為例子,對於Hint分片策略,帖子大都是基於1.x版本進行源碼分析路由,順帶提及,基本上都是瞎扯,同時在用法上sharding-jdbc-example沒有提及。因為我們實際要用,所以筆者基於2.0.3版本進行了完整的測試確保結果符合我們的預期。
首先定義一個hint策略,hint策略需要實現io.shardingjdbc.core.api.algorithm.sharding.hint.HintShardingAlgorithm接口。
<sharding:hint-strategy id="hintDatabaseStrategy" algorithm-class="com.yidoo.common.route.hint.HintShardingAlgorithm"/>
package com.yidoo.common.route.hint; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.alibaba.druid.util.StringUtils; import com.yidoo.utils.JacksonHelper; import io.shardingjdbc.core.api.algorithm.sharding.ListShardingValue; import io.shardingjdbc.core.api.algorithm.sharding.ShardingValue; public class HintShardingAlgorithm implements io.shardingjdbc.core.api.algorithm.sharding.hint.HintShardingAlgorithm { private static final Logger logger = LoggerFactory.getLogger(HintShardingAlgorithm.class); @Override public Collection<String> doSharding(Collection<String> availableTargetNames, ShardingValue shardingValue) { logger.debug("shardingValue=" + JacksonHelper.toJSON(shardingValue)); logger.debug("availableTargetNames=" + JacksonHelper.toJSON(availableTargetNames)); List<String> shardingResult = new ArrayList<>(); availableTargetNames.forEach(targetName -> { String suffix = targetName.substring(targetName.lastIndexOf("_")+1); if(StringUtils.isNumber(suffix)) {
// hint分片算法的ShardingValue有兩種具體類型: ListShardingValue和RangeShardingValue,取決於io.shardingjdbc.core.api.HintManager.addDatabaseShardingValue(String, String, ShardingOperator, Comparable<?>...)的時候,ShardingOperator的類型。見下文。 ListShardingValue<Integer> tmpSharding = (ListShardingValue<Integer>) shardingValue; tmpSharding.getValues().forEach(value -> { if (value % 2 == Integer.parseInt(suffix)) { shardingResult.add(targetName); } }); } }); return shardingResult; } }
@SuppressWarnings("unchecked") private ShardingValue getShardingValue(final String logicTable, final String shardingColumn, final ShardingOperator operator, final Comparable<?>[] values) { Preconditions.checkArgument(null != values && values.length > 0); switch (operator) { case EQUAL: case IN: return new ListShardingValue(logicTable, shardingColumn, Arrays.asList(values)); case BETWEEN: return new RangeShardingValue(logicTable, shardingColumn, Range.range(values[0], BoundType.CLOSED, values[1], BoundType.CLOSED)); default: throw new UnsupportedOperationException(operator.getExpression()); } }
sharding:sharding-rule節點上定義屬性default-database-strategy-ref為hintDatabaseStrategy,如下:
<sharding:sharding-rule data-source-names="dataSource_0,dataSource_1,globalDataSource" default-database-strategy-ref="hintDatabaseStrategy" default-data-source-name="globalDataSource">
不用定義<sharding:table-rules>。
接下去在執行sql語句前設置分片值即可,如下:
UnitQuery treq = new UnitQuery(); Unit record1 = new Unit(); record1.setUnitName("強制指定Hint-1"); record1.setRemark1(" "); record1.setInputMan("系統管理員"); Unit record2 = new Unit(); record2.setUnitName("強制指定Hint-3"); record2.setRemark1(" "); record2.setInputMan("系統管理員"); List<Unit> records = new ArrayList<>(); records.add(record1); records.add(record2); HintManager hintManager = HintManager.getInstance(); hintManager.setDatabaseShardingValue(1); // hintManager.addDatabaseShardingValue("t_Unit","user_id",1); unitMapper.insertValues(records); hintManager.close(); hintManager = HintManager.getInstance(); // 必須先clear,重新拿到instance hintManager.setDatabaseShardingValue(1); List<Unit> unitList = unitMapper.selectByHint(treq); logger.info("強制指定Hint-1提示" + JacksonHelper.toJSON(unitList)); hintManager.close(); Unit record3 = new Unit(); record3.setUnitName("強制指定Hint-2"); record3.setRemark1(" "); record3.setInputMan("系統管理員"); Unit record4 = new Unit(); record4.setUnitName("強制指定Hint-4"); record4.setRemark1(" "); record4.setInputMan("系統管理員"); records.clear(); records.add(record3); records.add(record4); hintManager = HintManager.getInstance(); hintManager.setDatabaseShardingValue(2); // hintManager.addDatabaseShardingValue("t_Unit","user_id",2); unitMapper.insertValues(records); hintManager.close(); hintManager = HintManager.getInstance(); unitList = unitMapper.selectByHint(treq); logger.info("強制指定Hint-2提示" + JacksonHelper.toJSON(unitList)); hintManager.close();
mapper中如下:
<insert id="insertValues"> insert into t_Unit( unit_name, remark1, input_man, input_date ) values <foreach collection="list" item="item" index="index" separator=","> ( #{item.unitName}, #{item.remark1}, #{item.inputMan}, now() ) </foreach> </insert> <select id="selectByHint" resultType="com.yidoo.k3c.global.model.pojo.Unit"> select * from t_Unit </select>
執行前:
執行后:
日志:
[] 2018-07-05 10:10:21 [61125] [Sharding-JDBC-SQL]-[INFO] localhost-startStop-1 io.shardingjdbc.core.util.SQLLogger.log(SQLLogger.java:59) Logic SQL: insert into t_Unit( unit_name, remark1, input_man, input_date ) values ( ?, ?, ?, now() ) , ( ?, ?, ?, now() ) [] 2018-07-05 10:10:21 [61125] [Sharding-JDBC-SQL]-[INFO] localhost-startStop-1 io.shardingjdbc.core.util.SQLLogger.log(SQLLogger.java:59) SQLStatement: DMLStatement(super=AbstractSQLStatement(type=DML, tables=Tables(tables=[]), conditions=Conditions(conditions={}), sqlTokens=[], parametersIndex=0)) [] 2018-07-05 10:10:21 [61125] [Sharding-JDBC-SQL]-[INFO] localhost-startStop-1 io.shardingjdbc.core.util.SQLLogger.log(SQLLogger.java:59) Actual SQL: dataSource_1 ::: insert into t_Unit( unit_name, remark1, input_man, input_date ) values ( ?, ?, ?, now() ) , ( ?, ?, ?, now() ) ::: [強制指定Hint-1, , 系統管理員, 強制指定Hint-3, , 系統管理員] [] 2018-07-05 10:10:21 [61514] [c.a.d.p.PreparedStatementPool]-[DEBUG] localhost-startStop-1 com.alibaba.druid.pool.PreparedStatementPool.put(PreparedStatementPool.java:129) {conn-110005, pstmt-120000} enter cache [] 2018-07-05 10:10:21 [61515] [o.m.s.SqlSessionUtils]-[DEBUG] localhost-startStop-1 org.mybatis.spring.SqlSessionUtils.closeSqlSession(SqlSessionUtils.java:168) Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2b3a6ec5] [] 2018-07-05 10:10:21 [61515] [o.s.j.d.DataSourceUtils]-[DEBUG] localhost-startStop-1 org.springframework.jdbc.datasource.DataSourceUtils.doReleaseConnection(DataSourceUtils.java:329) Returning JDBC Connection to DataSource [] 2018-07-05 10:10:21 [61519] [o.m.s.SqlSessionUtils]-[DEBUG] localhost-startStop-1 org.mybatis.spring.SqlSessionUtils.getSqlSession(SqlSessionUtils.java:104) Creating a new SqlSession [] 2018-07-05 10:10:21 [61519] [o.m.s.SqlSessionUtils]-[DEBUG] localhost-startStop-1 org.mybatis.spring.SqlSessionUtils.getSqlSession(SqlSessionUtils.java:140) SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41aefec2] was not registered for synchronization because synchronization is not active [] 2018-07-05 10:10:21 [61523] [o.s.j.d.DataSourceUtils]-[DEBUG] localhost-startStop-1 org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:110) Fetching JDBC Connection from DataSource [] 2018-07-05 10:10:21 [61523] [o.m.s.t.SpringManagedTransaction]-[DEBUG] localhost-startStop-1 org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:86) JDBC Connection [io.shardingjdbc.core.jdbc.core.connection.ShardingConnection@5e996b5d] will not be managed by Spring [] 2018-07-05 10:10:21 [61528] [i.s.c.r.t.h.DatabaseHintRoutingEngine]-[DEBUG] localhost-startStop-1 io.shardingjdbc.core.routing.type.hint.DatabaseHintRoutingEngine.route(DatabaseHintRoutingEngine.java:55) Before database sharding only db:[dataSource_0, dataSource_1, globalDataSource] sharding values: ListShardingValue(logicTableName=DB_TABLE_NAME, columnName=DB_COLUMN_NAME, values=[1]) [] 2018-07-05 10:10:21 [61529] [i.s.c.r.t.h.DatabaseHintRoutingEngine]-[DEBUG] localhost-startStop-1 io.shardingjdbc.core.routing.type.hint.DatabaseHintRoutingEngine.route(DatabaseHintRoutingEngine.java:59) After database sharding only result: [dataSource_1] [] 2018-07-05 10:10:21 [61530] [Sharding-JDBC-SQL]-[INFO] localhost-startStop-1 io.shardingjdbc.core.util.SQLLogger.log(SQLLogger.java:59) Logic SQL: select * from t_Unit [] 2018-07-05 10:10:21 [61530] [Sharding-JDBC-SQL]-[INFO] localhost-startStop-1 io.shardingjdbc.core.util.SQLLogger.log(SQLLogger.java:59) SQLStatement: SelectStatement(super=DQLStatement(super=AbstractSQLStatement(type=DQL, tables=Tables(tables=[]), conditions=Conditions(conditions={}), sqlTokens=[], parametersIndex=0)), containStar=false, selectListLastPosition=0, groupByLastPosition=0, items=[], groupByItems=[], orderByItems=[], limit=null, subQueryStatement=null) [] 2018-07-05 10:10:21 [61530] [Sharding-JDBC-SQL]-[INFO] localhost-startStop-1 io.shardingjdbc.core.util.SQLLogger.log(SQLLogger.java:59) Actual SQL: dataSource_1 ::: select * from t_Unit [] 2018-07-05 10:10:21 [61576] [c.a.d.p.PreparedStatementPool]-[DEBUG] localhost-startStop-1 com.alibaba.druid.pool.PreparedStatementPool.put(PreparedStatementPool.java:129) {conn-110005, pstmt-120001} enter cache [] 2018-07-05 10:10:21 [61577] [o.m.s.SqlSessionUtils]-[DEBUG] localhost-startStop-1 org.mybatis.spring.SqlSessionUtils.closeSqlSession(SqlSessionUtils.java:168) Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41aefec2] [] 2018-07-05 10:10:21 [61578] [o.s.j.d.DataSourceUtils]-[DEBUG] localhost-startStop-1 org.springframework.jdbc.datasource.DataSourceUtils.doReleaseConnection(DataSourceUtils.java:329) Returning JDBC Connection to DataSource [] 2018-07-05 10:10:21 [61582] [c.y.c.s.ParamexService]-[INFO] localhost-startStop-1 com.yidoo.common.service.ParamexService.afterPropertiesSet(ParamexService.java:79) 強制指定Hint-1提示[{"unitName":"體積","remark1":" ","inputMan":"系統管理員","inputDate":1528166814000,"updateMan":null,"updateDate":1528166814000},{"unitName":"容量","remark1":" ","inputMan":"系統管理員","inputDate":1528166814000,"updateMan":null,"updateDate":1528166814000},{"unitName":"廣播","remark1":" ","inputMan":"系統管理員","inputDate":1529984218000,"updateMan":null,"updateDate":1529984218000},{"unitName":"強制指定Hint-1","remark1":" ","inputMan":"系統管理員","inputDate":1530756621000,"updateMan":null,"updateDate":1530756621000},{"unitName":"強制指定Hint-3","remark1":" ","inputMan":"系統管理員","inputDate":1530756621000,"updateMan":null,"updateDate":1530756621000},{"unitName":"數量","remark1":" ","inputMan":"系統管理員","inputDate":1528166814000,"updateMan":null,"updateDate":1528166814000},{"unitName":"重量","remark1":" ","inputMan":"系統管理員","inputDate":1528166814000,"updateMan":null,"updateDate":1528166814000},{"unitName":"長度","remark1":" ","inputMan":"系統管理員","inputDate":1528166814000,"updateMan":null,"updateDate":1528166814000},{"unitName":"面積","remark1":" ","inputMan":"系統管理員","inputDate":1528166814000,"updateMan":null,"updateDate":1528166814000}]
[] 2018-07-05 10:10:21 [61585] [i.s.c.r.t.h.DatabaseHintRoutingEngine]-[DEBUG] localhost-startStop-1 io.shardingjdbc.core.routing.type.hint.DatabaseHintRoutingEngine.route(DatabaseHintRoutingEngine.java:55) Before database sharding only db:[dataSource_0, dataSource_1, globalDataSource] sharding values: ListShardingValue(logicTableName=DB_TABLE_NAME, columnName=DB_COLUMN_NAME, values=[2]) [] 2018-07-05 10:10:21 [61586] [i.s.c.r.t.h.DatabaseHintRoutingEngine]-[DEBUG] localhost-startStop-1 io.shardingjdbc.core.routing.type.hint.DatabaseHintRoutingEngine.route(DatabaseHintRoutingEngine.java:59) After database sharding only result: [dataSource_0] [] 2018-07-05 10:10:21 [61587] [Sharding-JDBC-SQL]-[INFO] localhost-startStop-1 io.shardingjdbc.core.util.SQLLogger.log(SQLLogger.java:59) Logic SQL: insert into t_Unit( unit_name, remark1, input_man, input_date ) values ( ?, ?, ?, now() ) , ( ?, ?, ?, now() ) [] 2018-07-05 10:10:21 [61587] [Sharding-JDBC-SQL]-[INFO] localhost-startStop-1 io.shardingjdbc.core.util.SQLLogger.log(SQLLogger.java:59) SQLStatement: DMLStatement(super=AbstractSQLStatement(type=DML, tables=Tables(tables=[]), conditions=Conditions(conditions={}), sqlTokens=[], parametersIndex=0)) [] 2018-07-05 10:10:21 [61587] [Sharding-JDBC-SQL]-[INFO] localhost-startStop-1 io.shardingjdbc.core.util.SQLLogger.log(SQLLogger.java:59) Actual SQL: dataSource_0 ::: insert into t_Unit( unit_name, remark1, input_man, input_date ) values ( ?, ?, ?, now() ) , ( ?, ?, ?, now() ) ::: [強制指定Hint-2, , 系統管理員, 強制指定Hint-4, , 系統管理員] [] 2018-07-05 10:10:21 [61624] [c.a.d.p.PreparedStatementPool]-[DEBUG] localhost-startStop-1 com.alibaba.druid.pool.PreparedStatementPool.put(PreparedStatementPool.java:129) {conn-10005, pstmt-20000} enter cache [] 2018-07-05 10:10:21 [61625] [o.m.s.SqlSessionUtils]-[DEBUG] localhost-startStop-1 org.mybatis.spring.SqlSessionUtils.closeSqlSession(SqlSessionUtils.java:168) Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@767f7324] [] 2018-07-05 10:10:21 [61625] [o.s.j.d.DataSourceUtils]-[DEBUG] localhost-startStop-1 org.springframework.jdbc.datasource.DataSourceUtils.doReleaseConnection(DataSourceUtils.java:329) Returning JDBC Connection to DataSource [] 2018-07-05 10:10:21 [61625] [o.m.s.SqlSessionUtils]-[DEBUG] localhost-startStop-1 org.mybatis.spring.SqlSessionUtils.getSqlSession(SqlSessionUtils.java:104) Creating a new SqlSession [] 2018-07-05 10:10:21 [61626] [o.m.s.SqlSessionUtils]-[DEBUG] localhost-startStop-1 org.mybatis.spring.SqlSessionUtils.getSqlSession(SqlSessionUtils.java:140) SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4c582b4c] was not registered for synchronization because synchronization is not active [] 2018-07-05 10:10:21 [61626] [o.s.j.d.DataSourceUtils]-[DEBUG] localhost-startStop-1 org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:110) Fetching JDBC Connection from DataSource [] 2018-07-05 10:10:21 [61627] [o.m.s.t.SpringManagedTransaction]-[DEBUG] localhost-startStop-1 org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:86) JDBC Connection [io.shardingjdbc.core.jdbc.core.connection.ShardingConnection@b551009] will not be managed by Spring [] 2018-07-05 10:10:21 [61769] [Sharding-JDBC-SQL]-[INFO] localhost-startStop-1 io.shardingjdbc.core.util.SQLLogger.log(SQLLogger.java:59) Logic SQL: select * from t_Unit [] 2018-07-05 10:10:21 [61770] [Sharding-JDBC-SQL]-[INFO] localhost-startStop-1 io.shardingjdbc.core.util.SQLLogger.log(SQLLogger.java:59) SQLStatement: SelectStatement(super=DQLStatement(super=AbstractSQLStatement(type=DQL, tables=Tables(tables=[Table(name=t_Unit, alias=Optional.absent())]), conditions=Conditions(conditions={}), sqlTokens=[TableToken(beginPosition=14, originalLiterals=t_Unit)], parametersIndex=0)), containStar=true, selectListLastPosition=9, groupByLastPosition=0, items=[StarSelectItem(owner=Optional.absent())], groupByItems=[], orderByItems=[], limit=null, subQueryStatement=null) [] 2018-07-05 10:10:21 [61771] [Sharding-JDBC-SQL]-[INFO] localhost-startStop-1 io.shardingjdbc.core.util.SQLLogger.log(SQLLogger.java:59) Actual SQL: globalDataSource ::: select * from t_Unit [] 2018-07-05 10:10:22 [61782] [c.a.d.p.PreparedStatementPool]-[DEBUG] localhost-startStop-1 com.alibaba.druid.pool.PreparedStatementPool.put(PreparedStatementPool.java:129) {conn-210005, pstmt-220000} enter cache [] 2018-07-05 10:10:22 [61782] [o.m.s.SqlSessionUtils]-[DEBUG] localhost-startStop-1 org.mybatis.spring.SqlSessionUtils.closeSqlSession(SqlSessionUtils.java:168) Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4c582b4c] [] 2018-07-05 10:10:22 [61783] [o.s.j.d.DataSourceUtils]-[DEBUG] localhost-startStop-1 org.springframework.jdbc.datasource.DataSourceUtils.doReleaseConnection(DataSourceUtils.java:329) Returning JDBC Connection to DataSource [] 2018-07-05 10:10:22 [61785] [c.y.c.s.ParamexService]-[INFO] localhost-startStop-1 com.yidoo.common.service.ParamexService.afterPropertiesSet(ParamexService.java:102) 強制指定Hint-2提示[{"unitName":"體積","remark1":" ","inputMan":"系統管理員","inputDate":1528166814000,"updateMan":null,"updateDate":1528166814000},{"unitName":"容量","remark1":" ","inputMan":"系統管理員","inputDate":1528166814000,"updateMan":null,"updateDate":1528166814000},{"unitName":"廣播","remark1":" ","inputMan":"系統管理員","inputDate":1529984218000,"updateMan":null,"updateDate":1529984218000},{"unitName":"強制指定Hint-1","remark1":" ","inputMan":"系統管理員","inputDate":1530701874000,"updateMan":null,"updateDate":1530701874000},{"unitName":"強制指定Hint-2","remark1":" ","inputMan":"系統管理員","inputDate":1530701874000,"updateMan":null,"updateDate":1530701874000},{"unitName":"強制指定Hint-3","remark1":" ","inputMan":"系統管理員","inputDate":1530701874000,"updateMan":null,"updateDate":1530701874000},{"unitName":"強制指定Hint-4","remark1":" ","inputMan":"系統管理員","inputDate":1530701874000,"updateMan":null,"updateDate":1530701874000},{"unitName":"數量","remark1":" ","inputMan":"系統管理員","inputDate":1528166814000,"updateMan":null,"updateDate":1528166814000},{"unitName":"重量","remark1":" ","inputMan":"系統管理員","inputDate":1528166814000,"updateMan":null,"updateDate":1528166814000},{"unitName":"長度","remark1":" ","inputMan":"系統管理員","inputDate":1528166814000,"updateMan":null,"updateDate":1528166814000},{"unitName":"面積","remark1":" ","inputMan":"系統管理員","inputDate":1528166814000,"updateMan":null,"updateDate":1528166814000}]
注意點
1、使用了sharding-jdbc之后,select from TableName會被轉換為select from tablename,也就是轉小寫,這是在代碼中處理的,如果不希望轉換(sql標准是大小寫不敏感的,主要是mysql linux下lower-case-table-names=1這個特例會區分大小寫,而且mysql 8.0不允許數據庫初始化和啟動的值不一致,5.7之前是可以的,https://bugs.mysql.com/bug.php?id=90695),則需要自己編譯sharding-jdbc-core(我們編譯了一版sharding-jdbc-core-2.0.3.zip),並更改getTableTokens中的轉小寫的代碼(這應該算是個bug),2.0.3版本代碼位置為:
// io.shardingjdbc.core.rewrite.SQLRewriteEngine.getTableTokens(TableUnit)中對sql語句中的表名做了toLowerCase()導致的,如下: private Map<String, String> getTableTokens(final TableUnit tableUnit) { String logicTableName = tableUnit.getLogicTableName().toLowerCase(); Map<String, String> tableTokens = new HashMap<>(); tableTokens.put(logicTableName, tableUnit.getActualTableName()); Optional<BindingTableRule> bindingTableRule = shardingRule.findBindingTableRule(logicTableName); if (bindingTableRule.isPresent()) { tableTokens.putAll(getBindingTableTokens(tableUnit, bindingTableRule.get())); } return tableTokens; }
2、一定要設置context:property-placeholder的ignore-unresolvable屬性為true,即<context:property-placeholder location="classpath:property/*.properties" ignore-unresolvable="true" />,否則會報無法解析占位符。
3、我們在調試的時候遇到個問題,如果eclipse中同時把sharding-jdbc的源代碼引入進來,war啟動的時候就會報sharding.xsd找不到,然后啟動出錯。
參考:
各非行內ShardingStrategy的用法可以參考https://www.cnblogs.com/mr-yang-localhost/p/8313360.html
其他:
https://wenku.baidu.com/view/ddeaed18910ef12d2bf9e74a.html
https://www.oschina.net/question/2918182_2280300
https://www.oschina.net/news/88860/sharding-jdbc-1-5-4-released
http://www.infoq.com/cn/news/2017/12/Sharding-JDBC-2
http://cmsblogs.com/?p=2542
http://shardingjdbc.io/document/legacy/2.x/en/00-overview/
https://www.oschina.net/question/2356021_2264290
https://blog.csdn.net/vkingnew/article/details/80613043
https://www.jianshu.com/p/dd47dd3b1f6b
https://www.zhihu.com/question/64709787
sharding-jdbc不支持SQL
https://blog.csdn.net/Farrell_zeng/article/details/52958189
mycat不支持SQL
https://blog.csdn.net/educast/article/details/50013355