1. 程式人生 > 實用技巧 >【分庫分表】學習+整理

【分庫分表】學習+整理

分庫分表解決的好處:


舉例:

頁面(只有商品基本資訊,沒有商品詳情資訊):

資料庫:

通過以下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爭搶。垂直切分帶來的效能提升主要集中在熱門資料的操作效率上,而且磁碟爭用情況減少。

通常我們按以下原則進行垂直拆分:

  1. 把不常用的欄位單獨放在一張表;
  2. 把text,blob等大欄位拆分出來放在附表中;
  3. 經常組合查詢的列放在一張表中;


垂直分庫

通過垂直分表效能得到了一定程度的提升,但是還沒有達到要求,並且磁碟空間也快不夠了,因為資料還是始終限制在一臺伺服器,庫內垂直分表只解決了單一表資料量過大的問題,但沒有將表分佈到不同的伺服器上,因此每個表還是競爭同一個物理機的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&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;allowMultiQueries=true&amp;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&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;allowMultiQueries=true&amp;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&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;allowMultiQueries=true&amp;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

未完待續》》》》》》》》》》》》》》》》》》》》》》》