前言
傳統應用項目設計通常都是采用單一數據庫作為存儲方案,但是隨着互聯網的迅猛發展以及應用數據量的增長,數據庫會隨着數據量的增長而漸漸成為整個應用框架的性能瓶頸。
首先是由於關系學數據庫大多是采用B+Tree類型的索引,當數據量超過一定的閾值之后,會導致索引的深度增長,而索引的深度又直接影響了磁盤IO操作的次數,直接影響了數據庫查詢性能的優劣。
其次由於用戶數量的提升,高並發的數據庫請求也會越來越多,單節點數據庫的連接數、TPS以及存儲容量都存在上限的限制,並發數達到一定量或者數據量超過了單節點存儲容量之后,數據庫性能會成為整個系統的瓶頸
最后當數據量很大的時候,數據庫的備份和遷移都會變得越來越難,時間成本和難度都會隨着數據數量的增長而增大。
所以在單節點關系型數據庫無法滿足互聯網的應用場景時,可以采用NoSQL數據庫來分擔一部分的壓力,但是NoSQL並不能完全替代關系型數據庫的特性,所以從本質上還是無法替代關系型數據庫,所以必須從關系型數據庫本身找到解決辦法。
既然單點數據庫無法解決,那么就可以考慮將單點改成多點,采用數據分片的方式將單節點數據按一定的規則拆分成多分數據分片存儲到多個節點,就可以解決單點數據庫的性能瓶頸,從而提升系統的可用性。
一、數據分片
數據分片的意思顧名思義就是將一塊比較大的數據集分割成多個較小的數據集,按照某個唯獨將存在單一數據庫中的數據分散地存放到多個數據庫、多個數據表、多個存儲區域中以達到提升性能瓶頸的效果。
1.1、數據分片的方式
數據分片的方式通常有垂直拆分和水平拆分兩種方式
垂直拆分是按照業務來進行拆分的方式,核心理念是專庫專用。通常一個數據庫中都會包含多個數據表,不同的數據表代表着不同的業務。根據業務將相同業務的表放到同一個數據庫,不同業務存放到不同數據庫,從業務上可以將數據庫進行拆分成多個數據庫,這樣就達到了不同業務的數據庫操作分散到了不同的數據庫中。另外如果某個數據表的字段比較多,同樣可以根據數據表的字段進行划分。如用戶信息表包含了用戶基本信息和詳細的信息,那么可以按重要程度進行垂直拆分,拆分成基礎用戶信息表和詳細用戶信息表。這樣可以根據數據的冷熱程度進行拆分處理。
通常情況下垂直拆分需要對系統的架構設計進行調整,而且雖然提高了數據庫的並發處理能力,但是並不能解決單表數據量大的問題。雖然將一個表拆分成了兩個表,兩個表中的字段雖然少了,但是數據量並沒有減少,並不能解決查詢效率低下的問題。
水平拆分是按照數據來進行橫向切分,根據表中某個或某幾個字段的某種規則,將數據分散到多個庫或表中。比如按年月進行拆分,不同年月的數據存放到不同年月的數據表中,又或者根據數據主鍵通過取模算法來選擇存儲的數據表等。水平拆分通過不同的數據表存儲不同的數據達到數據分散存儲的效果,從而可以解決單表數據量過大的問題。
水平拆分通常是業務層無感知的,也不需要對系統架構設計進行調整,所以水平拆分的方式一般優先於垂直拆分,而垂直拆分往往是系統架構設計的初期就應該做好規划。
1.2、數據分片的實現
數據分片通常的方式有分區、分庫、分表
1.2.1、分區
分區
mysql數據庫中的數據以文件存放在磁盤中,一張表會生成三個文件,.frm文件存放表結構,.myd文件存放表中數據,.myi文件存放表的索引。當表中存儲的數據很大的時候,.myd文件和.myi文件就會變大。
就會導致查詢數據時效率比較低。而mysql數據庫本身就提供了數據分區的功能,可以將磁盤中的文件拆分成多個子文件,這樣就可以在不修改表結構的情況下,將源數據分散存儲的效果。分區之后在邏輯上還是一個表,只是在物理存儲時已經分成了多個表。
分區可以垂直拆分將數據中的指定字段存放到指定區域,也可以水平拆分將不同數據存放到不同的區域。如下圖示:
1、將user表的id和name字段進行垂直分區,id和name字段分區存儲,單個存儲文件中數據量不變,但是少了字段之后文件大小變得很小
2、將user表中的數據進行水平分區成三個分區, 不同區域存儲不同的部分數據,單個數據文件變得很小
分區的特點:
1、可以動態增刪分區來快速增刪指定分區的數據
2、對於sum、count等聚合函數的查詢,不同分區可以並行處理,提高聚合函數的查詢效率
3、多個磁盤文件可以分散數據查詢,可以提高吞吐量
4、分區之后單個數據文件比較小,查詢效率提升比較大
5、單表分區上限為1024個區
6、分區字段中要么不包含主鍵或者索引列,要么就必須包含全部的主鍵和索引列
7、分區表無法使用外鍵約束
8、分區使用於表中的所有數據和索引,也就是全部數據和索引都必須分區,不能只對數據或只對索引或只對部分進行分區
1.2.2、分庫
將一個數據庫拆分成多個相互獨立的數據庫,各個數據庫直接互相獨立且各自獨享各自的存儲空間,垂直拆分可以將業務在數據庫層面進行解耦,水平拆分可以將數據分散存儲。都可以達到提升數據庫並發處理能力,以及解決單數據庫的性能問題。如下圖示:
1、將單庫中的多個表垂直拆分成不同庫存放不同的表
2、將單庫中數據量非常大的表垂直拆分到不同的庫中存放相同的表
1.2.3、分表
將一個數據庫表拆分成多個相互獨立表。垂直拆分實際就是將一個父表拆分成多個子表;水平拆分就是將一個數據量大的表拆分成多個數據量小的表。可以解決單表數據量大導致的查詢效率低的問題,同時也可以提高數據表的並發處理能力。如下圖示:
1、在業務層面將一個字段多的父表垂直拆分成兩個相互業務獨立的子表,兩個字表之間的讀寫操作相互獨立。
2、在數據層面將一個數據量很大的表水平拆分成多個數據量很小的表,多個小表之間數據結構完全一樣,和分區有點類似。分區是物理層面拆分邏輯層面不拆分;而分表是邏輯層面和物理層面都進行了拆分,只是在業務層面需要當作是一個表進行操作。
二、分庫分表的方案
分區功能是mysql數據庫高版本本身就自帶的功能,通過數據庫提高的API就可以很方便的對表中的數據進行動態分區。且數據分區對於業務層完全透明化,對於用戶而言是無感知的。
而分庫分表的實現是無法通過數據庫本身來實現,只能通過用戶自己思考解決方案。而解決方案無疑就是使用中間件,而中間件可以分成組件形式和代理服務兩個形式存在。組件形式需要集成到應用程序中,相當於增強了應用程序JDBC的功能。而代理服務的方式是在應用程序和數據庫之間添加一層代理服務,應用程序將SQL操作交給代理服務,代理服務再訪問多個數據節點進行操作,最后將結果返回給應用程序,但是需要獨立部署服務,兩種方案架構如下圖示
方案一:集成組件方式
方案二:代理服務方式
兩種方式各有優缺點:
1、組件方式需要集成到應用程序中,和應用程序耦合;代理方式和應用程序解耦
2、組件方式無需獨立部署服務,代理方式需要獨立部署服務,並且服務需要集群來實現高可用性,成本較大
3、組件方式無法實現緩存功能或僅能實現單節點應用的緩存功能,但是需要消耗應用程序的內存;代理方式可以在代理服務中實現緩存功能
三、ShardingJDBC的使用
ShardingJDBC采用的是組件的方式集成到應用程序中,定位是輕量級的組件工具,在Java應用程序的JDBC層提高了增強功能。可以使應用程序直連數據庫,不需要額外的部署和依賴,相當於增強版的JDBC。ShardingJDBC的架構圖和組件方式的架構如出一轍,架構圖如下:
3.1、ShardingJDBC的核心概念
邏輯表:通過水平拆分的庫/表擁有相同的的表結構和邏輯,如user表水平拆分成user_01和user_02等分表,但是在邏輯上都統稱為user表
真實表:真實存儲數據的物理表,如user表水平拆分成user_01和user_02表,那么user_01和user_02就是真實表
數據節點:數據分片的最小單元,由數據源和數據表組成,如db0.user_01表、db1.user_02表
綁定表:分片規則一致的主表和子表,如order表和order_detail表,都按照orderId進行分片,兩種表互為綁定表關系,配置綁定關系之后,主表和字表關聯查詢就不存在笛卡爾積關聯
廣播表:所有的分片中都存在表結構和數據都完全一致的表,一般用於存儲數據字典或每個庫都需要用到的公用數據,通常數據量都不大但是使用頻繁的
分片鍵:用於分片的數據庫字段,如水平拆分時將user表中的userId進行取模算法分配,根據userId%10的余數判斷數據在那個節點
分片算法:分片算法用於將分片鍵根據具體的算法進行分片,算法主要分如下幾種:
1.精確分片算法(PreciseShardingAlgorithm):用於處理使用單一鍵作為分片鍵的=和IN的場景,需要配合StandardShardingStrategy使用
2.范圍分片算法(RangeShardingAlgorithm):用於處理使用單一鍵作為分片鍵的between、and、>、<、>=、<=等分片場景,需要配合StandardShardingStrategy使用
3.復合分片算法(ComplexKeysShardingAlgorithm):用於處理使用多鍵作為分片鍵進行分片的場景,包含多個分片鍵的邏輯比較復雜,需要配合ComplexShardingStrategy使用
4.Hint分片算法(HintShardingAlgorithm):用於處理Hint行分片的場景,需要配合HintShardingStrategy使用
分片策略:分片鍵和分片算法配合起來就是一種分片策略,主要分片策略如下:
1.標准分片策略:對應StandardShardingStrategy,提高對SQL語句中的=,>,<,>=,<=,IN,AND,BETWEEN等分片操作的支持.StandardShardingStrategy只支持單鍵分片,提供PreciseShardingAlgorithm和RangeShardingAlgorithm兩個分片算法。PreciseShardingAlgorithm是必選的,用於處理=和IN的分片。RangeShardingAlgorithm是可選的,用於處理BETWEEN AND, >, <, >=, <=分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND將按照全庫路由處理
2.復合分片策略:對應ComplexShardingStrategy。復合分片策略。提供對SQL語句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片鍵,由於多分片鍵之間的關系復雜,因此並未進行過多的封裝,而是直接將分片鍵值組合以及分片操作符透傳至分片算法,完全由應用開發者實現,提供最大的靈活度
3.行表達式分片策略:對應InlineShardingStrategy。使用Groovy的表達式,提供對SQL語句中的=和IN的分片操作支持,只支持單分片鍵。對於簡單的分片算法,可以通過簡單的配置使用,從而避免繁瑣的Java代碼開發,如: t_user_$->{u_id % 8} 表示t_user表根據u_id模8,而分成8張表,表名稱為t_user_0到t_user_7。
4.Hint分片策略:對應HintShardingStrategy,通過Hint指定分片值而非從SQL中提取分片值的方式進行分片的策略
5.不分片策略:對應NoneShardingStrategy,不分片的策略
3.2、ShardingJDBC的配置
下面以Springboot集成ShardingJDBC的方式進行ShardingJDBC的配置說明
數據源配置:配置ShardingJDBC需要訪問的真實數據源列表,配置方式如下:
1 sharding.jdbc.datasource.names= #數據源名稱,多數據源以逗號分隔 2 3 sharding.jdbc.datasource.<data-source-name>.type= #數據庫連接池類名稱 4 sharding.jdbc.datasource.<data-source-name>.driver-class-name= #數據庫驅動類名 5 sharding.jdbc.datasource.<data-source-name>.url= #數據庫url連接 6 sharding.jdbc.datasource.<data-source-name>.username= #數據庫用戶名 7 sharding.jdbc.datasource.<data-source-name>.password= #數據庫密碼 8 sharding.jdbc.datasource.<data-source-name>.xxx= #數據庫連接池的其它屬性 9 10 sharding.jdbc.config.sharding.tables.<logic-table-name>.actual-data-nodes= #由數據源名 + 表名組成,以小數點分隔。多個表以逗號分隔,支持inline表達式。缺省表示使用已知數據源與邏輯表名稱生成數據節點。用於廣播表(即每個庫中都需要一個同樣的表用於關聯查詢,多為字典表)或只分庫不分表且所有庫的表結構完全一致的情況 11 12 #分庫策略,缺省表示使用默認分庫策略,以下的分片策略只能選其一 13 14 #用於單分片鍵的標准分片場景 15 sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.standard.sharding-column= #分片列名稱 16 sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.standard.precise-algorithm-class-name= #精確分片算法類名稱,用於=和IN。該類需實現PreciseShardingAlgorithm接口並提供無參數的構造器 17 sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.standard.range-algorithm-class-name= #范圍分片算法類名稱,用於BETWEEN,可選。該類需實現RangeShardingAlgorithm接口並提供無參數的構造器 18 19 #用於多分片鍵的復合分片場景 20 sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.complex.sharding-columns= #分片列名稱,多個列以逗號分隔 21 sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.complex.algorithm-class-name= #復合分片算法類名稱。該類需實現ComplexKeysShardingAlgorithm接口並提供無參數的構造器 22 23 #行表達式分片策略 24 sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.inline.sharding-column= #分片列名稱 25 sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.inline.algorithm-expression= #分片算法行表達式,需符合groovy語法 26 27 #Hint分片策略 28 sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.hint.algorithm-class-name= #Hint分片算法類名稱。該類需實現HintShardingAlgorithm接口並提供無參數的構造器 29 30 #分表策略,同分庫策略 31 sharding.jdbc.config.sharding.tables.<logic-table-name>.table-strategy.xxx= #省略 32 33 sharding.jdbc.config.sharding.tables.<logic-table-name>.key-generator-column-name= #自增列名稱,缺省表示不使用自增主鍵生成器 34 sharding.jdbc.config.sharding.tables.<logic-table-name>.key-generator-class-name= #自增列值生成器類名稱,缺省表示使用默認自增列值生成器。該類需提供無參數的構造器 35 36 sharding.jdbc.config.sharding.tables.<logic-table-name>.logic-index= #邏輯索引名稱,對於分表的Oracle/PostgreSQL數據庫中DROP INDEX XXX語句,需要通過配置邏輯索引名稱定位所執行SQL的真實分表 37 38 sharding.jdbc.config.sharding.binding-tables[0]= #綁定表規則列表 39 sharding.jdbc.config.sharding.binding-tables[1]= #綁定表規則列表 40 sharding.jdbc.config.sharding.binding-tables[x]= #綁定表規則列表 41 42 sharding.jdbc.config.sharding.broadcast-tables[0]= #廣播表規則列表 43 sharding.jdbc.config.sharding.broadcast-tables[1]= #廣播表規則列表 44 sharding.jdbc.config.sharding.broadcast-tables[x]= #廣播表規則列表 45 46 sharding.jdbc.config.sharding.default-data-source-name= #未配置分片規則的表將通過默認數據源定位 47 sharding.jdbc.config.sharding.default-database-strategy.xxx= #默認數據庫分片策略,同分庫策略 48 sharding.jdbc.config.sharding.default-table-strategy.xxx= #默認表分片策略,同分表策略 49 sharding.jdbc.config.sharding.default-key-generator-class-name= #默認自增列值生成器類名稱,缺省使用io.shardingsphere.core.keygen.DefaultKeyGenerator。該類需實現KeyGenerator接口並提供無參數的構造器 50 51 sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.master-data-source-name= #詳見讀寫分離部分 52 sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[0]= #詳見讀寫分離部分 53 sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[1]= #詳見讀寫分離部分 54 sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[x]= #詳見讀寫分離部分 55 sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.load-balance-algorithm-class-name= #詳見讀寫分離部分 56 sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.load-balance-algorithm-type= #詳見讀寫分離部分 57 sharding.jdbc.config.config.map.key1= #詳見讀寫分離部分 58 sharding.jdbc.config.config.map.key2= #詳見讀寫分離部分 59 sharding.jdbc.config.config.map.keyx= #詳見讀寫分離部分 60 61 sharding.jdbc.config.props.sql.show= #是否開啟SQL顯示,默認值: false 62 sharding.jdbc.config.props.executor.size= #工作線程數量,默認值: CPU核數 63 64 sharding.jdbc.config.config.map.key1= #用戶自定義配置 65 sharding.jdbc.config.config.map.key2= #用戶自定義配置 66 sharding.jdbc.config.config.map.keyx= #用戶自定義配置 67 讀寫分離 68 #省略數據源配置,與數據分片一致 69 70 sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.master-data-source-name= #主庫數據源名稱 71 sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[0]= #從庫數據源名稱列表 72 sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[1]= #從庫數據源名稱列表 73 sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[x]= #從庫數據源名稱列表 74 sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.load-balance-algorithm-class-name= #從庫負載均衡算法類名稱。該類需實現MasterSlaveLoadBalanceAlgorithm接口且提供無參數構造器 75 sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.load-balance-algorithm-type= #從庫負載均衡算法類型,可選值:ROUND_ROBIN,RANDOM。若`load-balance-algorithm-class-name`存在則忽略該配置 76 77 sharding.jdbc.config.config.map.key1= #用戶自定義配置 78 sharding.jdbc.config.config.map.key2= #用戶自定義配置 79 sharding.jdbc.config.config.map.keyx= #用戶自定義配置 80 81 sharding.jdbc.config.props.sql.show= #是否開啟SQL顯示,默認值: false 82 sharding.jdbc.config.props.executor.size= #工作線程數量,默認值: CPU核數 83 sharding.jdbc.config.props.check.table.metadata.enabled= #是否在啟動時檢查分表元數據一致性,默認值: false
3.3、ShardingJDBC使用案例
1、maven依賴配置
<dependency> <groupId>io.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>3.1.0</version> </dependency>
2、新建測試庫sharding_0和sharding_1,分別創建結構一模一樣的測試表dcs_log0、dcs_log1、dcs_log2,每個表中只有log_id和log_content兩個字段
3、Springboot的ShardingJDBC相關配置如下:
1 ## sharding-jdbc 2 sharding: 3 jdbc: 4 #數據源 5 datasource: 6 names: ds0,ds1 7 ds0: 8 type: com.alibaba.druid.pool.DruidDataSource 9 driver-class-name: com.mysql.jdbc.Driver 10 url: jdbc:mysql://127.0.0.1:3306/sharding_0 11 username: alpha 12 password: alpha 13 ds1: 14 type: com.alibaba.druid.pool.DruidDataSource 15 driver-class-name: com.mysql.jdbc.Driver 16 url: jdbc:mysql://127.0.0.1:3306/sharding_1 17 username: alpha 18 password: alpha 19 #分片 20 config: 21 sharding: 22 #庫分片策略 23 default-database-strategy: 24 inline: 25 sharding-column: log_id 26 algorithm-expression: ds${log_id % 2} 27 #表分片策略 28 tables: 29 dcs_log: 30 actual-data-nodes: ds$->{0..1}.dcs_log${0..2} 31 table-strategy: 32 inline: 33 sharding-column: log_id 34 algorithm-expression: dcs_log${log_id % 3}
這里配置了兩個數據源分別對應數據庫sharding_0和sharding_1,另外分庫的策略是根據log_id進行取模運算分片,分表同樣是根據log_id進行取模算法分配數據表.
4、批量插入數據測試代碼,從id為1開始到50,一共插入50條數據
1 @Resource 2 private DcsLogMapper dcsLogMapper; 3 4 public void addLogTest(){ 5 for(long i=1;i <= 50; i++){ 6 dcsLogMapper.addLog(i, ("logContent:" + i)); 7 } 8 }
由於是根據logId作為分片鍵進行分庫,所以可以得出結論為logId為奇數的會存入到數據庫sharding_1中,logId為偶數的會存入到數據庫sharding_0中,另外再根據logId進行分表操作,dcs_log0中存儲的是3的整倍數,dcs_log1中存儲3的整倍數+1;dcs_logs2中存儲的是logId為3的整倍數+2的數據。
5、校驗結果如下:
數據庫sharding_0中的三個dcs_log數據分別為:
1 SELECT * FROM sharding_0.dcs_log0; 2 SELECT * FROM sharding_0.dcs_log1; 3 SELECT * FROM sharding_0.dcs_log2;
數據庫sharding_1中的三個dcs_log數據分別為:
這樣就實現了將50條數據分散到兩個庫中的6個表中,當然還可以增加多個庫增加多個表來實現非常大的數據量分片的效果
3.4、分庫分表的高級用法
從上一節以及了解了shardingJDBC對於數據庫分庫分表的基本操作,但是實際的業務場景的復雜程度遠不止案例中的單表兩個字段那么簡單,還會存在以下諸多的問題需要解決。
3.4.1、分庫分表自增主鍵的實現
在單表的情況下,往往都是通過主鍵自增的方式來維護唯一的ID,但是在分庫分表的情況下,很顯然通過數據表的自增ID就會有問題,每個表都有自己的一套自增ID,很容易或者說必然會出現不同的表出現相同主鍵的情況,這顯然是首先需要解決的大問題。ShardingJDBC既然提高了分庫分表的方案,那么自然會考慮到這個問題,所以ShardingJDBC也提供了分布式主鍵生成的工具。
可以通過配置來自動生成唯一ID
1 sharding.jdbc.config.sharding.tables.<logic-table-name>.key-generator-column-name= #自增列名稱,缺省表示不使用自增主鍵生成器 2 34 sharding.jdbc.config.sharding.tables.<logic-table-name>.key-generator-class-name= #自增列值生成器類名稱,缺省表示使用默認自增列值生成器。該類需提供無參數的構造器
其中key-generator-column-name可以設置需要自動生成唯一ID的字段名稱,key-generator-class-name表示表示需要使用的生成器類型,默認提供了UUID和雪花算法兩種模式,分別為UUID/SNOWFLAKE,當然也可以自定義唯一ID生成器,自定義方式如下:
1 public class MyGeneratorKey implements KeyGenerator { 2 3 AtomicLong atomicLong = new AtomicLong(); 4 5 @Override 6 public Number generateKey() { 7 return atomicLong.incrementAndGet(); 8 } 9 }
自定義MyGenerator類實現KeyGenerator接口,重寫generatorKey方法即可,則每次插入數據時都會執行該方法給配置的字段設置主鍵值。
3.4.2、綁定表
當業務中涉及到主表和子表時就會涉及主表和子表的關聯查詢,采用分庫分表之后很容易就出現主表和子表分散到了不同的數據節點上,那么就會導致主表和子表關聯出現問題。比如電商系統中的訂單order表和訂單詳情order_detail表.
分別在各個數據節點上創建order表和order_detail表.
通過配置將兩個表綁定在一起,那么就可以order_detail表中的數據會和order表中相關聯的數據保存在同一個節點上,配置如下:
1 sharding.jdbc.config.sharding.tables.dcs_order.actual-data-nodes=ds$->{0..1}.dcs_order{0..2} 2 sharding.jdbc.config.sharding.tables.dcs_order.table-strategy.inline.sharding-column=log_id 3 sharding.jdbc.config.sharding.tables.dcs_order.table-strategy.inline.algorithm-expression=dcs_order${order_id % 3} 4 sharding.jdbc.config.sharding.tables.order_detail.actual-data-nodes=ds$->{0..1}.order_detail${0..2} 5 sharding.jdbc.config.sharding.tables.order_detail.table-strategy.inline.sharding-column=order_id 6 sharding.jdbc.config.sharding.tables.order_detail.table-strategy.inline.algorithm-expression=order_detail${order_id % 3} 7 #將dcs_order和order_detail表綁定 8 sharding.jdbc.config.sharding.binding-tables[0]=dcs_order,order_detail
3.4.3、廣播表
當業務系統中涉及到一些數據字典,比如省市區信息、用戶角色的公用的信息時,如果分庫分表之后就需要讓每個數據節點都擁有一份這樣的數據,此時就需要在每個數據節點上都創建同一個表,而當數據發生更新時,還需要將所有數據節點上的數據同步更新下。
如果通過代碼實現肯定比較麻煩,所以ShardingJDBC同樣提供了廣播表的配置方式來給用戶解決這個問題,只需要在配置中配置即可,配置如下:
#配置廣播表:用戶角色表
sharding.jdbc.config.sharding.broadcast-tables[0]=dcs_role
配置之后更新dcs_role表中的數據會同步更新所有節點的數據
3.4.4、分頁查詢
ShardingJDBC本身就支持MySQL、Oracle數據庫的分頁查詢
3.4.5、事務
ShardingJDBC支持分表和分庫的事務,但是前提是SQL路由到的數據庫必須是同一個,也就是說單庫中的SQL執行肯定是可以保證事務的。
四、ShardingJDBC的實現
ShardingJDBC的核心流程主要分成六個步驟,分別是:SQL解析->SQL優化->SQL路由->SQL改寫->SQL執行->結果歸並,流程圖如下:
4.1、SQL解析
分為詞法解析和語法解析。 先通過詞法解析器將SQL拆分為一個個不可再分的單詞。再使用語法解析器對SQL進行理解,並最終提煉出解析上下文。 解析上下文包括表、選擇項、排序項、分組項、聚合函數、分頁信息、查詢條件以及可能需要修改的占位符的標記。
SQL解析由ShardingJDBC的解析引擎負責處理
4.2、SQL優化
合並和優化分片條件,如OR等。
4.3、SQL路由
根據解析上下文匹配數據庫和表的分片策略,並生成路由路徑。 對於攜帶分片鍵的SQL,根據分片鍵的不同可以划分為單片路由(分片鍵的操作符是等號)、多片路由(分片鍵的操作符是IN)和范圍路由(分片鍵的操作符是BETWEEN)。 不攜帶分片鍵的SQL則采用廣播路由。
4.4、SQL改寫
將SQL改寫為在真實數據庫中可以正確執行的語句。SQL改寫分為正確性改寫和優化改寫。
SQL改寫的主要場景有:
1、真實表名的改寫:如查詢邏輯表的SQL為:select * from user where user_id = 1000,需要改寫成真實表的SQL:select * from user_0 where user_id = 1000
2、分頁參數的改寫:如兩個真實表中分表存儲數據為1、2、3、4;5、6、7、8;此時需要執行select * from user order by user_id limit 2,2 時,如果不改寫的話,那么分別在兩個真實表中取到的結果就是3、4和7、8,然后在合並結果之后得到的結果就是7和8,很顯然數據是不對的,因為實際情況下取第二頁的兩條數據應該是3和4才對。所以需要將SQL改寫成select * from user order by user_id limit 0,4。也就是說會取出前兩頁所有的數據,然后再內存中再進行排序取第二頁的數據。(limit偏移量值越大,效率越低)
不過如果SQL僅僅路由到一個節點,那么此時就不需要進行分頁參數的改寫,避免從偏移量0開始掃描數據。
3、批量拆分:當進行批量操作時,比如IN操作,假設兩個表根據user_id的奇偶數來分片,執行SQL為select * from user where user_id in (1,2,3,4).如果不進行拆分,就會讓這個SQL再兩個表中都執行,很顯然篩選的數據就多了,性能就差了,雖然對於結果沒有影響。
另外如果是批量插入操作時,就必須進行拆分,否則就會導致多個表中存在相同的數據了。
4.5、SQL執行
SQL執行通過執行引擎來處理,ShardingJDBC采用一套自動化的執行引擎,負責將路由和改寫完成之后的真實SQL安全且高效發送到底層數據源執行。 它不是簡單地將SQL通過JDBC直接發送至數據源執行;也並非直接將執行請求放入線程池去並發執行。它更關注平衡數據源連接創建以及內存占用所產生的消耗,以及最大限度地合理利用並發等問題。 執行引擎的目標是自動化的平衡資源控制與執行效率。
4.5.1、連接模式
首先從數據庫連接上來分析,當一個SQL條件中包含了多個庫多個表中的數據時,如果每個真實SQL都占用一個連接的話,很容易就導致數據庫連接數不夠用了,會嚴重影響其他SQL的執行。但是如果一個SQL解析了很多的真實SQL,如果都采用一個連接來處理的話,就無法達到並行的效果,比如一個SQL拆分成了10個真實SQL,一個連接串行處理的話就會導致一個線程串行執行10次數據庫查詢操作,很顯然效率又會大大的降低。
所以ShardingJDBC提供了兩種數據庫連接模式供用戶選擇,分別是內存限制模式和連接數限制模式
內存限制模式是每個真實SQL都分配一個連接去執行實現SQL的並行執行,達到效率的最大化
連接數限制模式是每個庫只會分配一個連接,分庫的情況下會分配多個連接,分表的情況下只會分配一個連接串行執行
對於這兩種模式ShardingJDBC還提供了自動化執行引擎來動態的選擇使用哪種模式,根據不同的SQL特點選擇最優的模式來執行。
不過用戶可以通過配置maxConnectionSizePerQuery來設置一次查詢操作最多可以分配多少個連接來限制連接數
4.6、結果歸並
將從各個數據節點獲取的多數據結果集,組合成為一個結果集並正確的返回至請求客戶端,稱為結果歸並。
ShardingSphere支持的結果歸並從功能上分為遍歷、排序、分組、分頁和聚合5種類型,它們是組合而非互斥的關系。 從結構划分,可分為流式歸並、內存歸並和裝飾者歸並。流式歸並和內存歸並是互斥的,裝飾者歸並可以在流式歸並和內存歸並之上做進一步的處理。
由於從數據庫中返回的結果集是逐條返回的,並不需要將所有的數據一次性加載至內存中,因此,在進行結果歸並時,沿用數據庫返回結果集的方式進行歸並,能夠極大減少內存的消耗,是歸並方式的優先選擇。
流式歸並是指每一次從結果集中獲取到的數據,都能夠通過逐條獲取的方式返回正確的單條數據,它與數據庫原生的返回結果集的方式最為契合。遍歷、排序以及流式分組都屬於流式歸並的一種。
內存歸並則是需要將結果集的所有數據都遍歷並存儲在內存中,再通過統一的分組、排序以及聚合等計算之后,再將其封裝成為逐條訪問的數據結果集返回。
裝飾者歸並是對所有的結果集歸並進行統一的功能增強,目前裝飾者歸並有分頁歸並和聚合歸並這2種類型。
ShardingSphere對於分頁查詢進行了2個方面的優化。
首先,采用流式處理 + 歸並排序的方式來避免內存的過量占用。由於SQL改寫不可避免的占用了額外的帶寬,但並不會導致內存暴漲。 與直覺不同,大多數人認為ShardingSphere會將1,000,010 * 2
記錄全部加載至內存,進而占用大量內存而導致內存溢出。 但由於每個結果集的記錄是有序的,因此ShardingSphere每次比較僅獲取各個分片的當前結果集記錄,駐留在內存中的記錄僅為當前路由到的分片的結果集的當前游標指向而已。 對於本身即有序的待排序對象,歸並排序的時間復雜度僅為O(n)
,性能損耗很小。
其次,ShardingSphere對僅落至單分片的查詢進行進一步優化。 落至單分片查詢的請求並不需要改寫SQL也可以保證記錄的正確性,因此在此種情況下,ShardingSphere並未進行SQL改寫,從而達到節省帶寬的目的。