【分庫分表】學習+整理
分庫分表解決的好處:
舉例:
頁面(只有商品基本資訊,沒有商品詳情資訊):
資料庫:
通過以下SQL能夠獲取到商品相關的店鋪資訊、地理區域資訊:
1 SELECT p.*,r.[地理區域名稱],s.[店鋪名稱],s.[信譽] 2 FROM [商品資訊] p 3 LEFT JOIN [地理區域] r ON p.[產地] = r.[地理區域編碼] 4 LEFT JOIN [店鋪資訊] s ON p.id = s.[所屬店鋪] 5 WHERE p.id = ?
拆分方式:
垂直分表 | 以欄位為維度,根據冷熱資料拆分(如:商品基本資訊欄位和商品詳細資訊欄位) |
垂直分庫 | |
水平分庫 |
以業務為維度,根據不同業務拆分,專庫專用(如:商品庫,店鋪庫) 水平分表較水平分庫可減少資料庫例項,降低運維的成本 |
水平分表 |
垂直分表
將訪問頻次低的商品描述資訊單獨存放在一張表中,訪問頻次較高的商品基本資訊單獨放在一張表中。
使用者在瀏覽商品列表時,只有對某商品感興趣時才會檢視該商品的詳細描述。因此,商品資訊中商品描述欄位訪問頻次較低,且該欄位儲存佔用空間較大,訪問單個數據IO時間較長;商品資訊中商品名稱、商品圖片、商品價格等其他欄位資料訪問頻次較高。
由於這兩種資料的特性不一樣,因此他考慮將商品資訊表拆分如下:
將訪問頻次低的商品描述資訊單獨存放在一張表中,訪問頻次較高的商品基本資訊單獨放在一張表中。
商品列表可採用以下sql:
1 SELECT p.*,r.[地理區域名稱],s.[店鋪名稱],s.[信譽] 2 FROM [商品資訊] p 3 LEFT JOIN [地理區域] r ON p.[產地] = r.[地理區域編碼] 4 LEFT JOIN [店鋪資訊] s ON p.id = s.[所屬店鋪] 5 WHERE...ORDER BY...LIMIT...
需要獲取商品描述時,再通過以下sql獲取:
1 SELECT * 2 FROM [商品描述] 3 WHERE [商品ID] = ?
垂直分表定義:將一個表按照欄位分成多表,每個表儲存其中一部分欄位。
它帶來的提升是:
1.為了避免IO爭搶並減少鎖表的機率,檢視詳情的使用者與商品資訊瀏覽互不影響
2.充分發揮熱門資料的操作效率,商品資訊的操作的高效率不會被商品描述的低效率所拖累。
為什麼大欄位IO效率低:第一是由於資料量本身大,需要更長的讀取時間;第二是跨頁,頁是資料庫儲存單位,很多查詢及定位操作都是以頁為單位,單頁內的資料行越多資料庫整體效能越好,而大欄位佔用空間大,單頁記憶體儲行數少,因此IO效率較低。第三,資料庫以行為單位將資料載入到記憶體中,這樣表中欄位長度較短且訪問頻率較高,記憶體能載入更多的資料,命中率更高,減少了磁碟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] 。
水平分庫是把同一個表的資料按一定規則拆到不同的資料庫中,每個庫可以放在不同的伺服器上。
垂直分庫是把不同表拆到不同資料庫中,它是對資料行的拆分,不影響表結構
它帶來的提升是:
- 解決了單庫大資料,高併發的效能瓶頸。
- 提高了系統的穩定性及可用性。
穩定性體現在IO衝突減少,鎖定減少,可用性指某個庫出問題,部分可用`
當一個應用難以再細粒度的垂直切分,或切分後資料量行數巨大,存在單庫讀寫、儲存效能瓶頸,這時候就需要進行水平分庫了,經過水平切分的優化,往往能解決單庫儲存量及效能瓶頸。但由於同一個表被分配在不同的資料庫,需要額外進行資料操作的路由工作,因此大大提升了系統複雜度。
水平分表
按照水平分庫的思路對他把PRODUCT_DB_X(商品庫)內的表也可以進行水平拆分,其目的也是為解決單表資料量大的問題,如下圖:
與水平分庫的思路類似,不過這次操作的目標是表,商品資訊及商品描述被分成了兩套表。如果商品ID為雙數,將此操作對映至商品資訊1表;如果商品ID為單數,將操作對映至商品資訊2表。此操作要訪問表名稱的表示式為商品資訊[商品ID%2 + 1] 。
水平分表是在同一個資料庫內,把同一個表的資料按一定規則拆到多個表中。
它帶來的提升是:
-
優化單一表資料量過大而產生的效能問題
-
避免IO爭搶並減少鎖表的機率
庫內的水平分表,解決了單一表資料量過大的問題,分出來的小表中只包含一部分資料,從而使得單個表的資料量變小,提高檢索效能。
總結
垂直分表:可以把一個寬表的欄位按訪問頻次、是否是大欄位的原則拆分為多個表,這樣既能使業務清晰,還能提升部分效能。拆分後,儘量從業務角度避免聯查,否則效能方面將得不償失。
垂直分庫:可以把多個表按業務耦合鬆緊歸類,分別存放在不同的庫,這些庫可以分佈在不同伺服器,從而使訪問壓力被多伺服器負載,大大提升效能,同時能提高整體架構的業務清晰度,不同的業務庫可根據自身情況定製優化方案。但是它需要解決跨庫帶來的所有複雜問題。
水平分庫:可以把一個表的資料(按資料行)分到多個不同的庫,每個庫只有這個表的部分資料,這些庫可以分佈在不同伺服器,從而使訪問壓力被多伺服器負載,大大提升效能。它不僅需要解決跨庫帶來的所有複雜問題,還要解決資料路由的問題(資料路由問題後邊介紹)。
水平分表:可以把一個表的資料(按資料行)分到多個同一個資料庫的多張表中,每個表只有這個表的部分資料,這樣做能小幅提升效能,它僅僅作為水平分庫的一個補充優化。
一般來說,在系統設計階段就應該根據業務耦合鬆緊來確定垂直分庫,垂直分表方案,在資料量及訪問壓力不是特別大的情況,首先考慮快取、讀寫分離、索引技術等方案。若資料量極大,且持續增長,再考慮水平分庫水平分表方案。
轉:https://blog.csdn.net/weixin_44062339/article/details/100491744
分庫分表引發的問題:
1、分散式事務問題:?下面要提的ShardingJDBC無法解決
2、跨節點關聯查詢問題:連線查詢
3、跨節點分頁、排序問題:合併多個查詢結果之後再分頁或者排序
4、主鍵避重問題:全域性主鍵
5、公共表問題(如城市表):每個庫都有一個公共表,分別去操作
分庫分表框架-Sharding-JDBC:
Sharding-JDBC主要是操作分庫和分的表。核心功能是資料分頁和讀寫分離。透明的使用JDBC訪問已經分庫分表、讀寫分離的多個數據源,而不用關心資料來源的數量以及其資料如何分佈。
架構圖:
分片規則配置:
1、資料來源
2、配置資料節點(庫,表)
3、主鍵生成策略(全域性主鍵-SNOWFLAKE雪花演算法)
4、分片策略(分片鍵+分片演算法)
基於Spring boot的規則配置
sharding.jdbc.datasource.names=test0,test1
sharding.jdbc.datasource.test0.type=org.apache.commons.dbcp2.BasicDataSource
sharding.jdbc.datasource.test0.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.test0.url=jdbc:mysql://localhost:3306/test0
sharding.jdbc.datasource.test0.username=root
sharding.jdbc.datasource.test0.password=123456
sharding.jdbc.datasource.test1.type=org.apache.commons.dbcp2.BasicDataSource
sharding.jdbc.datasource.test1.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.test1.url=jdbc:mysql://localhost:3306/test1
sharding.jdbc.datasource.test1.username=root
sharding.jdbc.datasource.test1.password=123456
sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=user_id
sharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=ds$->{user_id % 2}
sharding.jdbc.config.sharding.tables.t_order.actual-data-nodes=test$->{0..1}.t_order$->{0..1} <!--Groovy表示式,代表$的取值為0到1-->
sharding.jdbc.config.sharding.tables.t_order.table-strategy.inline.sharding-column=order_id
sharding.jdbc.config.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order$->{order_id % 2}
sharding.jdbc.config.sharding.tables.t_order.key-generator.column=order_id
基於Spring名稱空間的規則配置
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sharding="http://shardingsphere.apache.org/schema/shardingsphere/sharding" xmlns:bean="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://shardingsphere.apache.org/schema/shardingsphere/sharding http://shardingsphere.apache.org/schema/shardingsphere/sharding/sharding.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd" default-lazy-init="false"> <!-- 配置多資料來源 --> <bean id="db_test_0" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close" primary="false"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://${jdbc.sharding.addr}/db_test_0?useSSL=false&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull" /> <property name="username" value="${jdbc.sharding.username}" /> <property name="password" value="${jdbc.sharding.password}" /> <property name="initialSize" value="${druid.initialSize}"/> <property name="maxActive" value="${druid.maxActive}"/> <property name="maxWait" value="${druid.maxWait}"/> <property name="validationQuery" value="SELECT 'x'" /> <property name="testWhileIdle" value="true" /> <property name="timeBetweenEvictionRunsMillis" value="60000 " /> <property name="testOnBorrow" value="false" /> <property name="testOnReturn" value="false" /> </bean> <bean id="db_test_1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close" primary="false"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://${jdbc.sharding.addr}/db_test_1?useSSL=false&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull" /> <property name="username" value="${jdbc.sharding.username}" /> <property name="password" value="${jdbc.sharding.password}" /> <property name="initialSize" value="${druid.initialSize}"/> <property name="maxActive" value="${druid.maxActive}"/> <property name="maxWait" value="${druid.maxWait}"/> <property name="validationQuery" value="SELECT 'x'" /> <property name="testWhileIdle" value="true" /> <property name="timeBetweenEvictionRunsMillis" value="60000 " /> <property name="testOnBorrow" value="false" /> <property name="testOnReturn" value="false" /> </bean> <bean id="db_test_2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close" primary="false"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://${jdbc.sharding.addr}/db_test_2?useSSL=false&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull" /> <property name="username" value="${jdbc.sharding.username}" /> <property name="password" value="${jdbc.sharding.password}" /> <property name="initialSize" value="${druid.initialSize}"/> <property name="maxActive" value="${druid.maxActive}"/> <property name="maxWait" value="${druid.maxWait}"/> <property name="validationQuery" value="SELECT 'x'" /> <property name="testWhileIdle" value="true" /> <property name="timeBetweenEvictionRunsMillis" value="60000 " /> <property name="testOnBorrow" value="false" /> <property name="testOnReturn" value="false" /> </bean> <!-- 配置分庫分表策略,庫表各有一個分片策略檔案 --> <bean id="dataSourceStrategy" class="com.fangyan.test.dal.strategy.DataSourceStrategy"/> <bean id="dataTableStrategy" class="com.fangyan.test.dal.strategy.DataTableStrategy"/> <sharding:standard-strategy id="databaseStrategy" sharding-column="f_user_id" precise-algorithm-ref="dataSourceStrategy"/> <sharding:standard-strategy id="orderTableStrategy" sharding-column="f_user_id" precise-algorithm-ref="dataTableStrategy"/> <!-- sharding資料來源,分三個庫,每個庫中又分15個表 --> <sharding:data-source id="shardingDataSource"> <sharding:sharding-rule data-source-names="db_test_0,db_test_1,db_test_2"> <sharding:table-rules> <sharding:table-rule logic-table="t_order_info" key-generator-ref="keyGenerator" actual-data-nodes="db_test_$->{0..2}.t_order_$->{0..15}" database-strategy-ref="databaseStrategy" table-strategy-ref="orderTableStrategy" /> </sharding:table-rules> </sharding:sharding-rule> <sharding:props> <prop key="sql.show">false</prop> </sharding:props> </sharding:data-source> <!-- 雪花演算法ID生成器,自定義的主鍵生成器 --> <sharding:key-generator id="keyGenerator" column="order_id" type="SIMPLE" /> </beans>
基於yml的規則配置
spring:
shardingsphere:
datasource: ## 略......
sharding:
tables:
t_order:
## 指定 t_order表的 資料分佈情況,配置資料節點
actual-data-nodes: m1.t_order_$->{1..2}
## 指定t_order表的主鍵列,以及主鍵生成策略為SNOWFLAKE
key-generator:
column: order_id
## 指定分片策略型別
type: SIMPLE
## 指定t_order的分片策略: 設定分片鍵和分片演算法
table-strategy:
inline:
sharding-column: order_id
## 根據order_id % 2 + 1 的結果,指定t_order的實際表
algorithm-expression: t_order_$->{order_id % 2 +1}
sharding-jdbc提供了兩種主鍵生成策略UUID、SNOWFLAKE ,預設使用SNOWFLAKE,還抽離出分散式主鍵生成器的介面org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator,方便使用者自行實現自定義的自增主鍵生成器。
自定義主鍵生成器
public class SimpleShardingKeyGenerator implements ShardingKeyGenerator { private AtomicLong atomic = new AtomicLong(0); @Getter @Setter private Properties properties = new Properties(); @Override public Comparable<?> generateKey() { return atomic.incrementAndGet(); } @Override public String getType() { //宣告型別 return "SIMPLE"; } }
新增:新增的時候,主鍵不用賦值了,由雪花演算法自動生成
查詢:當查詢的資料涉及多個庫表,真實的查詢sql會在多個庫表中查詢,執行了多次查詢。
執行流程分析:
1、解析SQL,獲取分片鍵對應的欄位的值
2、通過分片策略計算分片值確定庫表(邏輯表解析成真實表,並且解析到對應的資料來源和表上。)
3、根據分片值改寫SQL,改寫後的SQL是要執行的真實SQL
4、執行改寫後的真實SQL
5、將SQL的執行結果合併返回
結果歸併:遍歷、排序、分組、分頁、聚合
1、記憶體歸併:所有結果集在記憶體中進行合併
2、流水式歸併:一邊處理,一邊歸併
3、裝飾者歸併:對歸併結果集增強
基本概念:
繫結表:分片鍵相同的兩個表互為繫結表關係(如:商品基本資訊表及詳細資訊表)。繫結表查詢不會笛卡爾積,查詢效率高。
廣播表:公共表
廣播路由:沒有分片鍵,所有的庫表都執行SQL
未完待續》》》》》》》》》》》》》》》》》》》》》》》