文章目錄
分庫分表是什麼
比如小明是一家初創電商平臺的開發人員,他負責賣家模塊的功能開發,其中涉及了店鋪、商品的相關業務,設計如下數據庫
通過以下SQL能夠獲取到商品相關的店鋪信息、地理區域信息:
SELECT p.*,r.[地理區域名稱],s.[店鋪名稱],s.[信譽]
FROM [商品信息] p
LEFT JOIN [地理區域] r ON p.[產地] = r.[地理區域編碼]
LEFT JOIN [店鋪信息] s ON p.id = s.[所屬店鋪]
WHERE p.id = ?
形成類似以下列表展示:
隨着公司業務快速發展,數據庫中的數據量猛增,訪問性能也變慢了,優化迫在眉睫。分析一下問題出現在哪兒呢?
關係型數據庫本身比較容易成爲系統瓶頸,單機存儲容量、連接數、處理能力都有限。當單表的數據量達到1000W或100G以後,由於查詢維度較多,即使添加從庫、優化索引,做很多操作時性能仍下降嚴重。
- 方案1
通過提升服務器硬件能力來提高數據處理能力,比如增加存儲容量 、CPU等,這種方案成本很高,並且如果瓶頸在MySQL本身那麼提高硬件也是有很的。 - 方案2
把數據分散在不同的數據庫中,使得單一數據庫的數據量變小來緩解單一數據庫的性能問題,從而達到提升數據庫性能的目的。
如下圖:將電商數據庫拆分爲若干獨立的數據庫,並且對於大表也拆分爲若干小表,通過這種數據庫拆分的方法來解決數據庫的性能問題。
分庫分表就是爲了解決由於數據量過大而導致數據庫性能降低的問題,將原來獨立的數據庫拆分成若干數據庫組成,將數據大表拆分成若干數據表組成,使得單一數據庫、單一數據表的數據量變小,從而達到提升數據庫性能的目的。
分庫分表的方式
分庫分表包括分庫和分表兩個部分,在生產中通常包括:垂直分庫、水平分庫、垂直分表、水平分表四種方式
垂直分表
下邊通過一個商品查詢的案例講解垂直分表
通常在商品列表中是不顯示商品詳情信息的,如下圖:
用戶在瀏覽商品列表時,只有對某商品感興趣時纔會查看該商品的詳細描述。因此,商品信息中商品描述字段訪問頻次較低,且該字段存儲佔用空間較大,訪問單個數據IO時間較長;
商品信息中商品名稱、商品圖片、商品價格等其他字段數據訪問頻次較高。
由於這兩種數據的特性不一樣,因此他考慮將商品信息表拆分如下:
- 將訪問頻次低的商品描述信息單獨存放在一張表中
- 將訪問頻次較高的商品基本信息單獨放在一張表中
商品列表可採用以下sql
SELECT p.*,r.[地理區域名稱],s.[店鋪名稱],s.[信譽]
FROM [商品信息] p
LEFT JOIN [地理區域] r ON p.[產地] = r.[地理區域編碼]
LEFT JOIN [店鋪信息] s ON p.id = s.[所屬店鋪]
WHERE...ORDER BY...LIMIT...
需要獲取商品描述時,再通過以下sql獲取
SELECT *
FROM [商品描述]
WHERE [商品ID] = ?
這一步優化,就叫垂直分表。
垂直分表定義:將一個表按照字段分成多表,每個表存儲其中一部分字段。
它帶來的提升是:
- 爲了避免IO爭搶並減少鎖表的機率,查看詳情的用戶與商品信息瀏覽互不影響
- 充分發揮熱門數據的操作效率,商品信息的操作的高效率不會被商品描述的低效率所拖累。
一般來說,某業務實體中的各個數據項的訪問頻次是不一樣的,部分數據項可能是佔用存儲空間比較大的BLOB或是TEXT。
例如上例中的商品描述。所以,當表數據量很大時,可以將表按字段切開,將熱門字段、冷門字段分開放置在不同庫中,這些庫可以放在不同的存儲設備上,避免IO爭搶。垂直切分帶來的性能提升主要集中在熱門數據的操作效率上,而且磁盤爭用情況減少。
通常我們按以下原則進行垂直拆分
把不常用的字段單獨放在一張表;
把text,blob等大字段拆分出來放在附表中;
經常組合查詢的列放在一張表中;
垂直分庫
通過垂直分表性能得到了一定程度的提升,但是還沒有達到要求,並且磁盤空間也快不夠了,因爲數據還是始終限制在一臺服務器,庫內垂直分表只解決了單一表數據量過大的問題,但沒有將表分佈到不同的服務器上,因此每個表還是競爭同一個物理機的CPU、內存、網絡IO、磁盤。
把原有的SELLER_DB(賣家庫),分爲了PRODUCT_DB(商品庫)和STORE_DB(店鋪庫),並把這兩個庫分散到不同服務器,如下圖:
由於商品信息與商品描述業務耦合度較高,因此一起被存放在PRODUCT_DB(商品庫);而店鋪信息相對獨立,因此單獨被存放在STORE_DB(店鋪庫)。
這一步優化,就叫垂直分庫。
垂直分庫是指按照業務將表進行分類,分佈到不同的數據庫上面,每個庫可以放在不同的服務器上,它的核心理念是專庫專用。
它帶來的提升是:
- 解決業務層面的耦合,業務清晰
- 能對不同業務的數據進行分級管理、維護、監控、擴展等
- 高併發場景下,垂直分庫一定程度的提升IO、數據庫連接數、降低單機硬件資源的瓶頸
垂直分庫通過將表按業務分類,然後分佈在不同數據庫,並且可以將這些數據庫部署在不同服務器上,從而達到多個服務器共同分攤壓力的效果,但是依然沒有解決單表數據量過大的問題。
水平分庫
經過垂直分庫後,數據庫性能問題得到一定程度的解決,但是隨着業務量的增長,PRODUCT_DB(商品庫)單庫存儲數據已經超出預估。粗略估計,目前有8w店鋪,每個店鋪平均150個不同規格的商品,再算上增長,那商品數量得往1500w+上預估,並且PRODUCT_DB(商品庫)屬於訪問非常頻繁的資源,單臺服務器已經無法支撐。此時該如何
優化?
再次分庫?但是從業務角度分析,目前情況已經無法再次垂直分庫。
嘗試水平分庫,將店鋪ID爲單數的和店鋪ID爲雙數的商品信息分別放在兩個庫中。
也就是說,要操作某條數據,先分析這條數據所屬的店鋪ID。
- 如果店鋪ID爲單數,將此操作映射至RRODUCT_DB1(商品庫1);
- 如果店鋪ID爲雙數,將此操作映射至RRODUCT_DB2(商品庫2);
此操作要訪問數據庫名稱的表達式爲RRODUCT_DB[店鋪ID%2 + 1] 。
這一步優化,就叫水平分庫。
水平分庫是把同一個表的數據按一定規則拆到不同的數據庫中,每個庫可以放在不同的服務器上。
它帶來的提升是:
- 解決了單庫大數據,高併發的性能瓶頸。
- 提高了系統的穩定性及可用性。
當一個應用難以再細粒度的垂直切分,或切分後數據量行數巨大,存在單庫讀寫、存儲性能瓶頸,這時候就需要進行水平分庫了,經過水平切分的優化,往往能解決單庫存儲量及性能瓶頸。
但由於同一個表被分配在不同的數據庫,需要額外進行數據操作的路由工作,因此大大提升了系統複雜度。
水平分表
按照水平分庫的思路對他把PRODUCT_DB_X(商品庫)內的表也可以進行水平拆分,其目的也是爲解決單表數據量大的問題,如下圖:
與水平分庫的思路類似,不過這次操作的目標是表,商品信息及商品描述被分成了兩套表。
- 如果商品ID爲單數,將此操作映射至商品信息1表;
- 如果商品ID爲雙數,將此操作映射至商品信息2表;
此操作要訪問表名稱的表達式爲商品信息[商品ID%2 + 1] 。
這一步優化,就叫水平分表。
水平分表是在同一個數據庫內,把同一個表的數據按一定規則拆到多個表中。
它帶來的提升是:
- 優化單一表數據量過大而產生的性能問題
- 避免IO爭搶並減少鎖表的機率
庫內的水平分表,解決了單一表數據量過大的問題,分出來的小表中只包含一部分數據,從而使得單個表的數據量變小,提高檢索性能。
小結
-
垂直分表
可以把一個寬表的字段按訪問頻次、是否是大字段的原則拆分爲多個表,這樣既能使業務清晰,還能提升部分性能。拆分後,儘量從業務角度避免聯查,否則性能方面將得不償失
。 -
垂直分庫
可以把多個表按業務耦合鬆緊歸類,分別存放在不同的庫,這些庫可以分佈在不同服務器,從而使訪問壓力被多服務器負載,大大提升性能,同時能提高整體架構的業務清晰度,不同的業務庫可根據自身情況定製優化方案。但是它需要解決跨庫帶來的所有複雜問題。
-
水平分庫
可以把一個表的數據(按數據行)分到多個不同的庫,每個庫只有這個表的部分數據,這些庫可以分佈在不同服務器,從而使訪問壓力被多服務器負載,大大提升性能。它不僅需要解決跨庫帶來的所有複雜問題,還要解決數據路由的問題(數據路由問題後邊介紹)。
-
水平分表
可以把一個表的數據(按數據行)分到多個同一個數據庫的多張表中,每個表只有這個表的部分數據,這樣做能小幅提升性能,它僅僅作爲水平分庫的一個補充優化。
一般來說,在系統設計階段就應該根據業務耦合鬆緊來確定垂直分庫,垂直分表方案,在數據量及訪問壓力不是特別大的情況,首先考慮緩存、讀寫分離、索引技術等方案。若數據量極大,且持續增長,再考慮水平分庫水平分表方案。
分庫分錶帶來的問題
分庫分表能有效的緩解了單機和單庫帶來的性能瓶頸和壓力,突破網絡IO、硬件資源、連接數的瓶頸,同時也帶來了一些問題。
事務一致性問題
由於分庫分表把數據分佈在不同庫甚至不同服務器,不可避免會帶來分佈式事務問題。
跨節點關聯查詢
在沒有分庫前,我們檢索商品時可以通過以下SQL對店鋪信息進行關聯查詢
SELECT p.*,r.[地理區域名稱],s.[店鋪名稱],s.[信譽]
FROM [商品信息] p
LEFT JOIN [地理區域] r ON p.[產地] = r.[地理區域編碼]
LEFT JOIN [店鋪信息] s ON p.id = s.[所屬店鋪]
WHERE...ORDER BY...LIMIT...
但垂直分庫後 [商品信息] 和 [店鋪信息] 不在一個數據庫,甚至不在一臺服務器,無法進行關聯查詢。
可將原關聯查詢分爲兩次查詢,第一次查詢的結果集中找出關聯數據id,然後根據id發起第二次請求得到關聯數據,最後將獲得到的數據進行拼裝。
跨節點分頁、排序函數
跨節點多庫進行查詢時,limit分頁、order by排序等問題,就變得比較複雜了。需要先在不同的分片節點中將數據進行排序並返回,然後將不同分片返回的結果集進行彙總和再次排序。
如,進行水平分庫後的商品庫,按ID倒序排序分頁,取第一頁:
以上流程是取第一頁的數據,性能影響不大,但由於商品信息的分佈在各數據庫的數據可能是隨機的,如果是取第N頁,需要將所有節點前N頁數據都取出來合併,再進行整體的排序,操作效率可想而知。所以請求頁數越大,系統的性能也會越差。
在使用Max、Min、Sum、Count之類的函數進行計算的時候,與排序分頁同理,也需要先在每個分片上執行相應的函數,然後將各個分片的結果集進行彙總和再次計算,最終將結果返回。
主鍵避重
在分庫分表環境中,由於表中數據同時存在不同數據庫中,主鍵值平時使用的自增長將無用武之地,某個分區數據庫生成的ID無法保證全局唯一。因此需要單獨設計全局主鍵,以避免跨庫主鍵重複問題。
公共表
實際的應用場景中,參數表、數據字典表等都是數據量較小,變動少,而且屬於高頻聯合查詢的依賴表。例子中地理區域表也屬於此類型。
可以將這類表在每個數據庫都保存一份,所有對公共表的更新操作都同時發送到所有分庫執行。
由於分庫分表之後,數據被分散在不同的數據庫、服務器。因此,對數據的操作也就無法通過常規方式完成,並且它還帶來了一系列的問題。好在,這些問題不是所有都需要我們在應用層面上解決,市面上有很多中間件可供我們選擇,其中Sharding-JDBC使用流行度較高,我們來了解一下它。
Sharding-JDBC介紹
Sharding-JDBC
Sharding-JDBC是噹噹網研發的開源分佈式數據庫中間件,從 3.0 開始Sharding-JDBC被包含在 Sharding-Sphere中,之後該項目進入進入Apache孵化器,4.0版本之後的版本爲Apache版本。
ShardingSphere是一套開源的分佈式數據庫中間件解決方案組成的生態圈,它由Sharding-JDBC、ShardingProxy和Sharding-Sidecar(計劃中)這3款相互獨立的產品組成。他們均提供標準化的數據分片、分佈式事務和數據庫治理功能,可適用於如Java同構、異構語言、容器、雲原生等各種多樣化的應用場景。
官方地址:https://shardingsphere.apache.org/document/current/cn/overview/
目前只需關注Sharding-JDBC,它定位爲輕量級Java框架,在Java的JDBC層提供的額外服務。 它使用客戶端直連數據庫,以jar包形式提供服務,無需額外部署和依賴,可理解爲增強版的JDBC驅動,完全兼容JDBC和各種ORM框架。
Sharding-JDBC的核心功能爲 數據分片
和 讀寫分離
,通過Sharding-JDBC,應用可以 透明
的使用jdbc訪問已經分庫分表、讀寫分離的多個數據源,而不用關心數據源的數量以及數據如何分佈。
- 適用於任何基於Java的ORM框架,如: Hibernate, Mybatis, Spring JDBC
Template或直接使用JDBC。 - 基於任何第三方的數據庫連接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
- 支持任意實現JDBC規範的數據庫。目前支持MySQL,Oracle,SQLServer和PostgreSQL。
上圖展示了Sharding-Jdbc的工作方式,使用Sharding-Jdbc前需要人工對數據庫進行分庫分表,在應用程序中加入Sharding-Jdbc的Jar包,應用程序通過Sharding-Jdbc操作分庫分表後的數據庫和數據表,由於Sharding-Jdbc是對Jdbc驅動的增強,使用Sharding-Jdbc就像使用Jdbc驅動一樣,在應用程序中是無需指定具體要操作的分庫和分表的。
與JDBC性能對比
- 性能損耗測試:服務器資源充足、併發數相同,比較JDBC和Sharding-JDBC性能損耗,Sharding-JDBC相對JDBC損耗不超過7%。
- 性能對比測試:服務器資源使用到極限,相同的場景JDBC與Sharding-JDBC的吞吐量相當。
- 性能對比測試:服務器資源使用到極限,Sharding-JDBC採用分庫分表後,Sharding-JDBC吞吐量較JDBC不分表有接近2倍的提升。
Sharding-JDBC快速入門
需求說明
本章節使用Sharding-JDBC完成對訂單表的水平分表,通過快速入門程序的開發,快速體驗Sharding-JDBC的使用方法。
人工創建兩張表,t_order_1和t_order_2,這兩張表是訂單表拆分後的表,通過Sharding-Jdbc向訂單表插入數據,按照一定的分片規則,主鍵爲雙數的進入t_order_1,另一部分數據進入t_order_2,通過Sharding-Jdbc 查詢數據,根據 SQL語句的內容從t_order_1或t_order_2查詢數據。
環境說明
- 操作系統:Win10
- 數據庫:MySQL5
- JDK:64位 jdk1.8
- 應用框架:spring-boot-2.1.6.RELEASE,Mybatis3
- Sharding-JDBC:sharding-jdbc-spring-boot-starter-4.0.0-RC3
創建數據庫
- 創建訂單庫order_db
CREATE DATABASE `order_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
- 在order_db中創建t_order_1、t_order_2表
DROP TABLE IF EXISTS `t_order_1`;
CREATE TABLE `t_order_1` (
`order_id` bigint(20) NOT NULL COMMENT '訂單id',
`price` decimal(10, 2) NOT NULL COMMENT '訂單價格',
`user_id` bigint(20) NOT NULL COMMENT '下單用戶id',
`status` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '訂單狀態',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `t_order_2`;
CREATE TABLE `t_order_2` (
`order_id` bigint(20) NOT NULL COMMENT '訂單id',
`price` decimal(10, 2) NOT NULL COMMENT '訂單價格',
`user_id` bigint(20) NOT NULL COMMENT '下單用戶id',
`status` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '訂單狀態',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
引入maven依賴
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- sharding-jdbc和SpringBoot整合的Jar包 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC3</version>
</dependency>
</dependencies>
編寫程序
分片規則配置
分片規則配置是sharding-jdbc進行對分庫分表操作的重要依據,配置內容包括:數據源、主鍵生成策略、分片策略等。
在application.properties中配置
#sharding-jdbc分片規則配置
#數據源
spring.shardingsphere.datasource.names = m1
spring.shardingsphere.datasource.m1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.url = jdbc:mysql://localhost:3306/order_db?useUnicode=true
spring.shardingsphere.datasource.m1.username = root
spring.shardingsphere.datasource.m1.password = 123456
# 指定t_order表的數據分佈情況,配置數據節點 m1.t_order_1,m1.t_order_2
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes = m1.t_order_$->{1..2}
# 指定t_order表的主鍵生成策略爲SNOWFLAKE
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE
# 指定t_order表的分片策略,分片策略包括分片鍵和分片算法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column = order_id
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression = t_order_$->{order_id % 2 + 1}
# 打開sql輸出日誌
spring.shardingsphere.props.sql.show = true
logging.level.root = info
logging.level.org.springframework.web = info
logging.level.com.itheima.dbsharding = debug
logging.level.druid.sql = debug
- 首先定義數據源m1,並對m1進行實際的參數配置。
- 指定t_order表的數據分佈情況,他分佈在m1.t_order_1,m1.t_order_2
- 指定t_order表的主鍵生成策略爲SNOWFLAKE,SNOWFLAKE是一種分佈式自增算法,保證id全局唯一
- 定義t_order分片策略,order_id爲偶數的數據落在t_order_1,爲奇數的落在t_order_2,分表策略的表達式爲t_order_$->{order_id % 2 + 1}
數據操作