1. 程式人生 > >【資料庫設計】分庫,分表,主從,讀寫分離

【資料庫設計】分庫,分表,主從,讀寫分離

Mysql效能優化

一 資料庫設計合理性

1.1正規化

         為了建立冗餘較小、結構合理的資料庫,設計資料庫時必須遵循一定的規則。在關係型資料庫中這種規則就稱為正規化。正規化是符合某一種設計要求的總結。要想設計一個結構合理的關係型資料庫,必須滿足一定的正規化。

 

1.2 遵循三正規化的反三正規化設計

第一正規化:1NF是對屬性的原子性約束,要求屬性(列)具有原子性,不可再分解;(只要是關係型資料庫都滿足1NF)

第二正規化:2NF是對記錄的惟一性約束,表中的記錄是唯一的, 就滿足2NF, 通常我們設計一個主鍵來實現,主鍵不能包含業務邏輯。

第三正規化:3NF是對欄位冗餘性的約束,它要求欄位沒有冗餘。 沒有冗餘的資料庫設計可以做到。但是,沒有冗餘的資料庫未必是最好的資料庫,有時為了提高執行效率,就必須降低正規化標準,適當保留冗餘資料。具體做法是: 在概念資料模型設計時遵守第三正規化,降低正規化標準的工作放到物理資料模型設計時考慮。降低正規化就是增加欄位,允許冗餘。

二 分表分庫技術(取模分表,水平分割,垂直分割)

2.1 垂直拆分

2.1.1 垂直分庫

         垂直拆分就是要把表按模組劃分到不同資料庫中。比如大型的電商專案平臺是由不同的子專案模組構成的如訂單系統,賬戶系統,商品管理系統等等,這些個獨立的模組都可以拆分成獨立的服務與獨立的資料庫。做法與大系統拆分為多個小系統類似,按業務分類進行獨立劃分。與"微服務治理"的做法相似,每個微服務使用單獨的一個數據庫。如下圖所示一個大型的電商應用拆分成一個個小應用於單獨的庫。

 

2.1.2 垂直分表

如上圖示所示原本是一個大表的表結構,被拆垂直拆分成了兩個很小的子表。當然我們的原則是能不分表就不分表,除非這張表中欄位比較多,我們可以將不經常用,不經常查詢的欄位或者大的文字欄位拆分到垂直擴充套件的表中。把欄位多的大表拆分成小表。不僅便於開發與維護,當然也可以避免跨頁的問題。Mysql底層是按頁的方式來儲存的,欄位相對較少就可以避免跨頁訪問,減少系統的開銷。欄位相對比較少,訪問頻率高,記憶體載入更多的資料,提高查詢的命中率,減少磁碟的IO,提高系統的效能。垂直分表最好是根據業務來進行劃分,如果單表字段過大,且大部分欄位不參與業務維度的統計,就沒必要把這一部分資料查詢出來了。垂直分表是1v1的關係。所以主表與擴充套件表是一對一的對映關係。

2.1.3 垂直拆分優缺點

(1)優點:

解決業務系統層面的耦合,業務清晰

與微服務的治理類似,也能對不同業務的資料進行分級管理、維護、監控、擴充套件等

併發場景下,垂直切分一定程度的提升IO、資料庫連線數、單機硬體資源的瓶頸

(2)缺點:

部分表無法join,只能通過介面聚合方式解決,提升了開發的複雜度

分散式事務處理複雜

依然存在單表資料量過大的問題(需要水平切分)

 

2.2 水平拆分

當一個應用難以再細粒度的垂直切分,或切分後資料量行數巨大,存在單庫讀寫、儲存效能瓶頸,這時候就需要進行水平切分了。水平切分分為庫內分表和分庫分表,是根據表內資料內在的邏輯關係,將同一個表按不同的條件分散到多個數據庫或多個表中,每個表中只包含一部分資料,從而使得單個表的資料量變小,達到分散式的效果。

 

2.2.1 庫內分表

對於在一個數據庫裡面儲存的表,如果資料量過大的情況下,對大表的查詢或者按照不同的業務維度對單表進行水平的分表可以對業務的查詢有一定的幫助,可以提高查詢效能。當然也不是隨意對錶進行拆分的,前提是確實有這樣的業務並且有這樣的需求。

 

 

如上圖所示是庫內水平分表的示意圖,這樣進行資料庫的分表,有助於根據指定的維護進行查詢,提高查詢的效能。具體怎麼進行水平分表,當然也是根據業務規則來進行拆分的。比如本人最近參與的一個專案10000個經銷商,每個經銷商可以投放n多的廣告。那麼按照3-5年的廣告投放量來算的話。一個經銷商投放1000條廣告也就是1千萬的資料量了。一個表容量.這麼來說放到一個表裡就不太合適了,這個時候可以按經銷商 ID根據具體的業務規則放到不同的表中。減輕根據單個經銷商查詢的壓力。

2.2.2 分庫分表

         庫內分表只解決了單一表資料量過大的問題,但沒有將表分佈到不同機器的庫上,因此對於減輕MySQL資料庫的壓力來說,幫助不是很大,大家還是競爭同一個物理機的CPU、記憶體、網路IO,最好通過分庫分表來解決。

 

如上圖所示是分庫分表的示意圖,如圖我們把單個庫中某個表,分別分到下面3個庫中的表中,按照指定的資料庫分庫分表規則與欄位分配策略進行拆分。如上圖所示通過分庫分表的規則就可以解決資料庫IO瓶頸,資料庫連線壓力,CPU,網路,記憶體等等的瓶頸問題。這個是資料庫瓶頸問題的解決方案。

 

2.2.3 分表策略

2.2.3.1 按範圍拆分

按照時間區間或ID區間來切分。例如:按日期將不同月甚至是日的資料分散到不同的庫中;將userId1~9999的記錄分到第一個庫,10000~20000的分到第二個庫,以此類推。某種意義上,某些系統中使用的"冷熱資料分離",將一些使用較少的歷史資料遷移到其他庫中,業務功能上只提供熱點資料的查詢,也是類似的實踐。

優點:

單表大小可控

天然便於水平擴充套件,後期如果想對整個分片叢集擴容時,只需要新增節點即可,無需對其他分片的資料進行遷移

使用分片欄位進行範圍查詢時,連續分片可快速定位分片進行快速查詢,有效避免跨分片查詢的問題。

缺點:

熱點資料成為效能瓶頸。連續分片可能存在資料熱點,例如按時間欄位分片,有些分片儲存最近時間段內的資料,可能會被頻繁的讀寫,而有些分片儲存的歷史資料,則很少被查詢

2.2.3.2 根據欄位值取模

 

 

如上圖所示是取key%n的運算進行分表的,要分多少張表就進行模多少的運算。實際上我們可以把這個取模的運算看做是一個hash函式,只是這個函式是一個取模的函式罷了總的一句話這種方式就是 hash分表。當然對於水平拆分如何建立一個唯一的自增Id呢,這個時候我們可以採用一張輔助的表,來形成一個唯一的Id值

一般採用hash取模mod的切分方式,例如:將 Customer 表根據 key 欄位切分到3個庫中,餘數為0的放到第一個庫,餘數為1的放到第二個庫,以此類推。這樣同一個使用者的資料會分散到同一個庫中,如果查詢條件帶有key欄位,則可明確定位到相應庫去查詢。

 

優點:

資料分片相對比較均勻,不容易出現熱點和併發訪問的瓶頸

缺點:

後期分片叢集擴容時,需要遷移舊的資料(使用一致性hash演算法能較好的避免這個問題)容易面臨跨分片查詢的複雜問題。

2.2.3.2 根據時間拆分

    

如上圖所示我們可以按時間去拆分,只是這個拆分週期就會作為表的字尾名,比如當前時間是18年的資料往18表存,19的往19表存,根據不同的時間粒度可以把表拆分成為年表,月表,天表,小時表,分鐘表這樣對於大量的按時間做維度的資料統計來說就能夠快速的定位到表並得出報表。比如移動或者聯動的實時的流量,攻擊的次數這些按時間統計的資料就可以按日期來做分表操作。

 

優點:

單表大小可控

天然便於水平擴充套件,後期如果想對整個分片叢集擴容時,只需要新增節點即可,無需對其他分片的資料進行遷移

使用分片欄位進行範圍查詢時,連續分片可快速定位分片進行快速查詢,有效避免跨分片查詢的問題。

 

缺點:

熱點資料成為效能瓶頸。連續分片可能存在資料熱點,例如按時間欄位分片,有些分片儲存最近時間段內的資料,可能會被頻繁的讀寫,而有些分片儲存的歷史資料,則很少被查詢,聯表查詢困難。

注意!  如上所示說明的分表的方式不僅僅只侷限於資料Id的範圍或者時間,實際上分表的規則是來自於業務場景的,它可以按照你的任何欄位來分表。可以對不同的欄位做數字的hash() 再通過數值做hash()這樣也可以分表。具體還是看對於真正的使用者來說,它怎麼方便查詢,要怎麼操作查詢才合適。還有就是並不是分表就是最佳的效能實踐。分表以後也會造成一些分表查詢的問題。所以最好站在業務的高度上去分表,分庫。所有的技術都是為業務服務的,要不然就是耍流氓。

 

 

2.2.4 分表實踐  

2.2.4.1 建立表

create table user0(

id int unsigned primary key ,

name varchar(32) not null default '',

pwd  varchar(32) not null default '')

engine=myisam charset utf8;


create table user1(

id int unsigned primary key ,

name varchar(32) not null default '',

pwd  varchar(32) not null default '')

engine=myisam charset utf8;


create table user2(

id int unsigned primary key ,

name varchar(32) not null default '',

pwd  varchar(32) not null default '')

engine=myisam charset utf8;



create table uuid(

id int unsigned primary key auto_increment)engine=myisam charset utf8;

如上所示的sql語句建立三張user表,其中一張輔助生成表,輔助生成Id用。

2.2.4.2 業務邏輯程式碼

@Service

public class UserService {


     @Autowired

     private JdbcTemplate jdbcTemplate;


     public String regit(String name, String pwd) {

         // 1.先獲取到 自定增長ID

         String idInsertSQL = "INSERT INTO uuid VALUES (NULL);";

         jdbcTemplate.update(idInsertSQL);

         Long insertId = jdbcTemplate.queryForObject("select last_insert_id()", Long.class);

         // 2.判斷儲存表名稱

         String tableName = "user" + insertId % 3;

         // 3.註冊資料

         String insertUserSql = "INSERT INTO " + tableName + " VALUES ('" + insertId + "','" + name + "','" + pwd

                   + "');";

         System.out.println("insertUserSql:" + insertUserSql);

         jdbcTemplate.update(insertUserSql);

         return "success";

     }


     public String get(Long id) {

         String tableName = "user" + id % 3;

         String sql = "select name from " + tableName + "  where id="+id;

         System.out.println("SQL:" + sql);

         String name = jdbcTemplate.queryForObject(sql, String.class);

         return name;

     }

}

        

如上所示是一個業務層程式碼的實踐案例,如上程式碼所示,它就是通過一個空表進行主鍵生成,又按主鍵來進行分表的一種方式。

2.3 主從複製

        

 

2.3.1 主從複製示意

    影響MySQL-A資料庫的操作,在資料庫執行後,都會寫入本地的日誌系統A中。

 假設,實時的將變化了的日誌系統中的資料庫事件操作,在MYSQL-A的3306埠,通過網路發給MYSQL-B。

 MYSQL-B收到後,寫入本地日誌系統B,然後一條條的將資料庫事件在資料庫中完成。

 那麼,MYSQL-A的變化,MYSQL-B也會變化,這樣就是所謂的MYSQL的複製,即MYSQL replication。

 在上面的模型中,MYSQL-A就是主伺服器,即master,MYSQL-B就是從伺服器,即slave。

 日誌系統A,其實它是MYSQL的日誌型別中的二進位制日誌,也就是專門用來儲存修改資料庫表的所有動作,即bin log。【注意MYSQL會在執行語句之後,釋放鎖之前,寫入二進位制日誌,確保事務安全】

 日誌系統B,並不是二進位制日誌,由於它是從MYSQL-A的二進位制日誌複製過來的,並不是自己的資料庫變化產生的,有點接力的感覺,稱為中繼日誌,即relay log。

 可以發現,通過上面的機制,可以保證MYSQL-A和MYSQL-B的資料庫資料一致,但是時間上肯定有延遲,即MYSQL-B的資料是滯後的。

【即便不考慮什麼網路的因素,MYSQL-A的資料庫操作是可以併發的執行的,但是MYSQL-B只能從relay log中讀一條,執行下。因此MYSQL-A的寫操作很頻繁,MYSQL-B很可能跟不上。】

 

2.3.2 配置主從

2.3.2.1 配置主伺服器

(1) 首先準備兩個安裝好mysqlserver的伺服器

(2)  修改主伺服器(master)

vim /etc/my.cnf  新增以下內容

server_id=177  ###伺服器id

log-bin=mysql-bin   ###開啟日誌檔案

 (3) 重新啟動一下(master)

Service mysqld stop

Service mysqld start

 (4) 查詢一下檢測一下server_id是否配置正確

SHOW VARIABLES LIKE 'server_id'

(5)通過如下所示的指令碼,查詢一下日誌檔案

SHOW MASTER STATUS;

 (6) 查詢視窗中給從伺服器授權

  

GRANT REPLICATION SLAVE ON *.* to 'mysync'@'%' identified by 'q123456';

2.3.2.2 配置從伺服器

(1)/etc/my.cnf 下配置伺服器id

server_id=178

log-bin=mysql-bin

binlog_do_db=test

   (2)重啟mysql伺服器

     service mysqld restart

(3)同上檢查id等

4查詢視窗執行同步指令碼

STOP SLAVE

CHANGE MASTER TO MASTER_HOST='192.168.232.128',MASTER_USER='admin',MASTER_PASSWORD='admin',

         MASTER_LOG_FILE='mysql-bin.000001',MASTER_LOG_POS=316;

START SLAVE

中間的master_host 主庫

 Master_user=admin

 Master_password=admin

Master_log_file=通過這個指令碼SHOW MASTER STATUS;master查詢相關的日誌檔案資訊與位置埠號,這樣主從就建好了

   5 SHOW SLAVE STATUS 檢視是否已經做了同步

  

 如上圖所示 我們已經看到已經做了主從了。這樣主資料庫的資料變更就會同步到從資料庫了。

2.4 讀寫分離

        

     在資料庫叢集架構中,讓主庫負責處理事務性查詢,而從庫只負責處理select查詢,讓兩者分工明確達到提高資料庫整體讀寫效能。當然,主資料庫另外一個功能就是負責將事務性查詢導致的資料變更同步到從庫中,也就是寫操作。這就是資料庫的讀寫分離。

2.4.1 優點

 1)分攤伺服器壓力,提高機器的系統處理效率

  2)增加冗餘,提高服務可用性,當一臺資料庫伺服器宕機後可以調整另外一臺從庫以最快速度恢復服務

 

2.5 mycat中介軟體

是一個開源的分散式資料庫系統,但是因為資料庫一般都有自己的資料庫引擎,而Mycat並沒有屬於自己的獨有資料庫引擎,所有嚴格意義上說並不能算是一個完整的資料庫系統,只能說是一個在應用和資料庫之間起橋樑作用的中介軟體。

在Mycat中介軟體出現之前,MySQL主從複製叢集,如果要實現讀寫分離,一般是在程式段實現,這樣就帶來了一個問題,即資料段和程式的耦合度太高,如果資料庫的地址發生了改變,那麼我的程式也要進行相應的修改,如果資料庫不小心掛掉了,則同時也意味著程式的不可用,而對於很多應用來說,並不能接受;

    引入Mycat中介軟體能很好地對程式和資料庫進行解耦,這樣,程式只需關注資料庫中介軟體的地址,而無需知曉底層資料庫是如何提供服務的,大量的通用資料聚合、事務、資料來源切換等工作都由中介軟體來處理;

    Mycat中介軟體的原理是對資料進行分片處理,從原有的一個庫,被切分為多個分片資料庫,所有的分片資料庫叢集構成完成的資料庫儲存,有點類似磁碟陣列中的RAID0.

 2.5.1配置mycat

(1)配置server.xml檔案

         <!-- 新增user -->    <

user name="mycat">

                   <property name="password">mycat</property>

                   <property name="schemas">mycat</property>

    </user>

        

         <!-- 新增user -->

   <user name="mycat_red">

                   <property name="password">mycat_red</property>

                   <property name="schemas">mycat</property>

                   <property name="readOnly">true</property>

</user>

mycat的安裝目錄下面配置可讀可寫的訪問方式。

(2) 配置schema檔案

         <?xml version="1.0"?>

<!DOCTYPE mycat:schema SYSTEM "schema.dtd">

<mycat:schema xmlns:mycat="http://org.opencloudb/">

    <!-- 與server.xml中user的schemas名一致 -->

    <schema name="mycat" checkSQLschema="true" sqlMaxLimit="100">

        <table name="t_users" primaryKey="user_id" dataNode="dn1" rule="rule1"/>

        <table name="t_message" type="global" primaryKey="messages_id" dataNode="dn1" />

    </schema>

<dataNode name="dn1" dataHost="jdbchost" database="weibo_simple

" />

  

    <dataHost name="jdbchost" maxCon="1000" minCon="10" balance="1"

                writeType="0" dbType="mysql" dbDriver="native" switchType="1"

                slaveThreshold="100">

         <heartbeat>select user()</heartbeat> 

        <writeHost host="hostMaster" url="192.168.232.128:3306" user="root" password="root">

        </writeHost>

        <writeHost host="hostSlave" url="192.168.232.129:3306" user="root" password="root"/>

    </dataHost>

</mycat:schema>

如上所示指定了主庫與從褲

(3)配置rule.xml檔案

<?xml version="1.0" encoding="UTF-8"?>

<!-- - - Licensed under the Apache License, Version 2.0 (the "License");

         - you may not use this file except in compliance with the License. - You

         may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0

         - - Unless required by applicable law or agreed to in writing, software -

         distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT

         WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the

         License for the specific language governing permissions and - limitations

         under the License. -->

<!DOCTYPE mycat:rule SYSTEM "rule.dtd">

<mycat:rule xmlns:mycat="http://org.opencloudb/">

          <tableRule name="rule1">

        <rule>

            <columns>user_id</columns>

            <algorithm>func1</algorithm>

        </rule>

    </tableRule>

    <function name="func1" class="org.opencloudb.route.function.AutoPartitionByLong">

       <property name="mapFile">autopartition-long.txt</property>

    </function>

</mycat:rule>

(4) log4j.xml配置

<level value=”debug” />

(5)啟動mycat

(6)通過不同的server.xml配置的賬戶進行連線操作檢測讀寫分離。