數據拆分的三種方案
當數據庫的數據量變得特別大,影響到查詢和更新效率的時候,我們就得考慮做數據拆分了。數據拆分一般包含三種方式:分區,分表和分庫,我們先分別來講一講:
分區:數據分區是數據庫提供的一種表結構設計方式,對一張數據表進行分區並不會創建新的表, 只是將原本存儲爲單個文件的表數據根據一定的規則拆分爲多個文件進行存儲(不同的文件還可以放到不同的硬盤上),這樣可以有效降低單個文件的IO壓力,提高讀寫效率,部分聚合語句也可以在不同的分區上並行的執行,縮短執行時間。目前主流數據庫都支持數據分區技術,這個活兒一般都是DBA直接做的,和應用層關係不大;
分表:分表就是將原本的一張數據表拆分成多張表,比如t_order拆成t_order_0,t_order_1,數據按照不同規則存儲到不同的表中。分表具備分區的所有好處(表都分了自然數據文件也分成多個了,IO性能也提高了),同時還能夠提高單表的併發訪問性能(請求會根據規則路由到不同的分表中併發執行);
分庫:分庫就更進一步了,將業務數據庫直接拆分成多個,數據庫請求會根據路由規則訪問不同的分庫。分庫可以部署到不同的服務器上,從而打破單機的硬件和網絡瓶頸,大大提高數據庫的整體併發能力,是數據庫水平擴展的重要方式。
由此看來分區,分庫,分表是層層遞進的關係,能夠帶來的好處也是越來越多。但便宜也不是白撿的,三種拆分方式實現起來也是越往後越困難。分區最簡單,剛纔說了,這東西數據庫一般原生支持,DBA幾個命令就能搞定;分表和分庫除了數據庫層面需要維護更多的表和庫以外,都還需要對應用層進行改造,改造的難度根據所選用的方式有所不同。另外相對於分表,分庫還需要面對事務支持的問題,分表只是在單個數據庫中進行拆分,不會對本地事務造成影響。但分庫以後,應用程序需要同時連接多個數據源進行操作,跨數據源的事務只能使用分佈式事務的解決方案,而分佈式事務本身又是一塊難啃的骨頭。
我們在設計數據拆分方案的時候也要有所取捨,必須想清楚拆分是爲了解決哪些具體問題。一般來說,如果只是因爲單表數據量比較大影響查詢更新的效率,但系統併發量訪問不高的情況,直接分區就可以了;如果數據和併發量都比較大,就需要考慮分表了;如果單節點的數據庫都已經撐不住了,比如存儲空間,網絡帶寬,併發數(單庫能承載的併發數一般也就1,2K)等,那就只有分庫了。能走到分庫這一步,說明你們公司業務發展的不錯呀,漲工資指日可待啦<( ̄︶ ̄)>
分庫分表的方案選擇
囉嗦了這麼多,我們進入這次的正題,怎麼使用sharding-jdbc去做分表分庫呢?前面說過,分庫分表數據庫是不管的,應用程序需要自行去處理業務數據與各個分表和分庫的映射關係,比如客戶A的訂單要保存到表order_a,而客戶B的訂單需要保存到表order_b,如果全部讓程序員手工去寫處理邏輯,那估計得原地爆炸吧...... 好在,計算機的問題幾乎都可以通過添加一箇中間抽象層來解決問題,JDBC不就是通過抽象和統一接口來解決了java程序與不同的數據庫進行通訊的問題嗎?那隻要再在JDBC之上再做一層抽象,由這個抽象層來處理業務數據到分庫分表的路由問題,程序員不就能解放了嗎。sharding-jdbc就是一個提供這層抽象的框架,sharding-jdbc最早是由噹噹開源出來的,現在已經劃入 ShardingSphere 項目成爲了Apache的頂級項目,ShardingSphere目前包括三個子項目:Sharding-JDBC,Sharding-Proxy,Sharding-Sidecar,都是用於處理分庫分表的解決方案,不過方式有所不同,Sharding-JDBC是以框架的形式直接集成到業務代碼中,而其它兩個都是以中間件的形式獨立部署的,具體的差異見下表:
Sharding-Sidecar看起來無疑是最好的選擇,其模仿了微服務領域的Service Mesh的概念,提出了所謂的Database Mesh,但目前該架構還不成熟,僅僅是Alpha狀態。Sharding-Proxy需要引入和維護額外的中間件,其類似的框架還有 Mycat,而且目前只支持Mysql數據庫的代理。所以,對於分庫和應用實例都比較少的Java項目,我個人覺得Sharding-JDBC應該是目前最好的解決方案了。
接入Sharding-JDBC
首先我們先來初始化數據庫的表結構,邏輯表有兩張:t_order(訂單表)和t_order_good(訂單產品表,用於存儲訂單中包含的產品明細),每張表劃分爲三張分表,一共六張表:t_order_0,t_order_1,t_order_2,t_order_good_1,t_order_good_2,t_order_good_3,建表語句如下:
CREATE DATABASE `sharding-db-1` /*!40100 DEFAULT CHARACTER SET utf8 */;
CREATE TABLE `t_order_0` (
`id` bigint(20) NOT NULL,
`order_code` varchar(32) DEFAULT NULL COMMENT '訂單編號',
`product_line` varchar(12) DEFAULT NULL,
`customer_code` varchar(32) DEFAULT NULL COMMENT '客戶ID',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `t_order_1` (
`id` bigint(20) NOT NULL,
`order_code` varchar(32) DEFAULT NULL COMMENT '訂單編號',
`product_line` varchar(12) DEFAULT NULL,
`customer_code` varchar(32) DEFAULT NULL COMMENT '客戶ID',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `t_order_2` (
`id` bigint(20) NOT NULL,
`order_code` varchar(32) DEFAULT NULL COMMENT '訂單編號',
`product_line` varchar(12) DEFAULT NULL,
`customer_code` varchar(32) DEFAULT NULL COMMENT '客戶ID',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `t_order_good_1` (
`id` bigint(20) NOT NULL,
`order_id` bigint(20) DEFAULT NULL,
`good_code` varchar(32) DEFAULT NULL,
`good_type` varchar(32) DEFAULT NULL,
`good_spec` varchar(32) DEFAULT NULL,
`good_quantity` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `t_order_good_2` (
`id` bigint(20) NOT NULL,
`order_id` bigint(20) DEFAULT NULL,
`good_code` varchar(32) DEFAULT NULL,
`good_type` varchar(32) DEFAULT NULL,
`good_spec` varchar(32) DEFAULT NULL,
`good_quantity` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `t_order_good_3` (
`id` bigint(20) NOT NULL,
`order_id` bigint(20) DEFAULT NULL,
`good_code` varchar(32) DEFAULT NULL,
`good_type` varchar(32) DEFAULT NULL,
`good_spec` varchar(32) DEFAULT NULL,
`good_quantity` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
然後新建一個Spring Boot項目,接入Sharding-JDBC非常簡單,按老規矩引入相應的Starter就行了,這裏同時引入Mybatis-Plus框架便於處理各種數據庫操作:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
其實Sharding-JDBC只是對datasource進行封裝,所以上層的持久層框架一般不會受到什麼影響,願意用什麼框架都可以。加入一個application-sharding.yml配置文件,專門用於分庫分表的相關配置(加到主配置文件也可以,但分庫分表的配置項較多,建議拆分出來獨立配置):
#sharding-jdbc配置
spring:
shardingsphere:
props:
sql:
show: true #是否顯示分片後實際的執行SQL
datasource:
names: ds0
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.211:3306/sharding-db-1?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password: *******
sharding:
# 配置綁定表,每一行爲一組
binding-tables:
- t_order,t_order_good
tables:
t_order:
actual-data-nodes: ds$->{0}.t_order_$->{0..2}
table-strategy:
inline:
sharding-column: id
algorithm-expression: t_order_$->{id % 3}
key-generator:
column: id
type: SNOWFLAKE
t_order_good:
actual-data-nodes: ds$->{0}.t_order_good_$->{0..2}
table-strategy:
inline:
sharding-column: order_id
algorithm-expression: t_order_good_$->{order_id % 3}
key-generator:
column: id
type: SNOWFLAKE
這些配置項應該算是整個接入過程中最複雜的地方,我們來逐項說一下:
- spring.shardingsphere.datasource下面用於配置所有需要接入的數據源,一般一個數據源就是一個獨立的數據庫,我們先只配置一個數據庫來測試一下單庫分表的實現;
- spring.shardingsphere.sharding下面就是分片規則的配置了,binding-tables是個很重要的概念,指分片規則一致的主表和子表,比如t_order和t_order_good表,都是按照order_id來進行分片的,則它們之間就是綁定關係,綁定表之間的多表關聯查詢會被內部優化,從而避免出現笛卡爾積式的關聯,使得查詢效率有極大的提升,詳細介紹可以參考 這裏。
- tables下面可以分別爲不同的表配置不同的分片規則,actualDataNodes表示該邏輯表對應的真實表的實際位置,比如t_order的真實表包括數據源ds0下的t_order_0,t_order_1,t_order_2;tableStrategy.inline表示使用InlineShardingStrategyConfiguration,可以在配置中使用行表達式,實際就是一段Groovy代碼,具體語法規則參考 這裏,需要注意的是InlineShardingStrategy只支持單分片鍵,且僅支持SQL語句中的=和IN的分片操作。如果需要將時間等支持範圍查詢的字段作爲分片鍵,那就需要使用其它的分片策略,比如StandardShardingStrategy。shardingColumn代表分片鍵的名稱,algorithmExpression是分片策略的表達式,這裏簡單的採用對mod(order_id)的方式進行分片;keyGenerator用於生成分佈式主鍵,這裏採用的是SNOWFLAKE算法,可以生成一個可排序的Long型ID值,最大隻有 19位,比UUID好多了。
配置完成以後,基本已經大功告成了,剩下的就是寫一些數據庫插入查詢的樣本代碼,這裏就不貼了。然後我們來測試一下分片的成果,首先是數據插入:
@Test
public void testCreateOrder() throws Exception {
List<String> productLines = ListUtil.toList("TC", "UB");
List<Order> newOrders = new ArrayList<>();
List<OrderGood> newOrderGoods = new ArrayList<>();
for (int i = 0; i < 30; i++) {
Order order = new Order();
order.setOrderCode(System.currentTimeMillis() + RandomUtil.randomString(6));
order.setCreateTime(new Date());
order.setProductLine(productLines.get(RandomUtil.randomInt(0, 2)));
newOrders.add(order);
}
//插入訂單數據
orderService.saveBatch(newOrders);
for (Order order : newOrders) {
OrderGood aGood = new OrderGood();
aGood.setOrderId(order.getId());
aGood.setGoodCode(RandomUtil.randomString(6));
aGood.setGoodQuantity(RandomUtil.randomInt(1, 200));
newOrderGoods.add(aGood);
}
//插入訂單產品數據
orderGoodService.saveBatch(newOrderGoods);
}
執行之後,查詢各表的數據,數據已經正確插入到各個分表當中了。需要注意的是,snowflake算法生成的ID並不是完全連續的,所有導致取模操作後並沒有完全平均的插入到各個分表中,但總體上相差不多。然後我們再來執行一下表的連接查詢,查詢SQL如下:
SELECT
t.*, t1.order_code
FROM
t_order_good t
inner join
t_order t1 ON t.order_id = t1.id
執行時,我們把spring.shardingsphere.props.sql.show設置爲true,便於觀察解析後實際執行的SQL情況,執行後實際打印的SQL語句如下:
SELECT t.*, t1.order_code FROM t_order_good_0 t inner join t_order_0 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_1 t inner join t_order_1 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_2 t inner join t_order_2 t1 ON t.order_id = t1.id;
這個結果是符合我們的預期的,因爲並沒有傳入分片key作爲where條件,所以連接時會嘗試按照主表與從表間的分片按順序進行一一對應進行join(和分表名稱無關,只和順序相關,1-A,2-B,3-C這樣連接也是可以的,所以主從表的分片策略最好是設置爲完全一致,包括分表的數量,否則如果沒有指定where條件就無法進行對應了)。如果我們此時將之前設置的bindingTables去掉,就會發現,執行的SQL由三個變成了9個,不再是按順序連接了,而是笛卡爾積:
SELECT t.*, t1.order_code FROM t_order_good_0 t inner join t_order_0 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_0 t inner join t_order_1 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_0 t inner join t_order_2 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_1 t inner join t_order_0 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_1 t inner join t_order_1 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_1 t inner join t_order_2 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_2 t inner join t_order_0 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_2 t inner join t_order_1 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_2 t inner join t_order_2 t1 ON t.order_id = t1.id;
分表看起來應該沒什麼問題了,至於分庫其實也非常簡單,只需要根據情況多配置幾個數據源,然後在actualDataNodes中指定對應的數據源範圍,再配置一下database的路由規則就行了,比如如果需要按照訂單的產品線進行分庫(TC,UB兩個產品線),那我們的配置文件可修改爲:
#sharding-jdbc配置
spring:
shardingsphere:
props:
sql:
show: true #是否顯示分片後實際的執行SQL
datasource:
names: ds_TC,ds_UB
ds_TC:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.211:3306/sharding-db-1?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password:
ds_UB:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.211:3306/sharding-db-1?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password:
sharding:
# 配置綁定表,每一行爲一組
binding-tables:
- t_order,t_order_good
tables:
t_order:
actual-data-nodes: ds_$->{[TC,UB]}.t_order_$->{0..2}
table-strategy:
inline:
sharding-column: id
algorithm-expression: t_order_$->{id % 3}
key-generator:
column: id
type: SNOWFLAKE
t_order_good:
actual-data-nodes: ds_$->{[TC,UB]}.t_order_good_$->{0..2}
table-strategy:
inline:
sharding-column: order_id
algorithm-expression: t_order_good_$->{order_id % 3}
key-generator:
column: id
type: SNOWFLAKE
# 默認的分庫規則,適用於所有表,按產品線進行分庫
default-database:
strategy:
inline:
sharding-column: product_line
algorithm-expression: ds_$->{product_line}
本文的所有代碼可以在這裏找到:sharding-jdbc-demo