mysql MVCC和分庫分表
參考文件:
https://www.jianshu.com/p/8845ddca3b23
https://www.jianshu.com/p/7aec260ca1a2
https://www.cnblogs.com/monkeyblog/p/10449363.html
談談你對Mysql的MVCC的理解?
MVCC(Mutil-Version Concurrency Control),就是多版本併發控制。MVCC 是一種併發控制的方法,一般在資料庫管理系統中,實現對資料庫的併發訪問。
結論:MVCC在MySQL InnoDB中的實現主要是為了提高資料庫併發效能,用更好的方式去處理讀-寫衝突,做到即使有讀寫衝突時,也能做到不加鎖,非阻塞併發讀。
MySQL InnoDB下的當前讀和快照讀:
-
當前讀
像select lock in share mode(共享鎖), select for update ; update, insert ,delete(排他鎖)這些操作都是一種當前讀,為什麼叫當前讀?就是它讀取的是記錄的最新版本,讀取時還要保證其他併發事務不能修改當前記錄,會對讀取的記錄進行加鎖。 -
快照讀
像不加鎖的select操作就是快照讀,即不加鎖的非阻塞讀;快照讀的前提是隔離級別不是序列級別,序列級別下的快照讀會退化成當前讀;之所以出現快照讀的情況,是基於提高併發效能的考慮,快照讀的實現是基於多版本併發控制,即MVCC,可以認為MVCC是行鎖的一個變種,但它在很多情況下,避免了加鎖操作,降低了開銷;既然是基於多版本,即快照讀可能讀到的並不一定是資料的最新版本,而有可能是之前的歷史版本
- 準確的說,MVCC多版本併發控制指的是 “維持一個數據的多個版本,使得讀寫操作沒有衝突” 這麼一個概念。僅僅是一個理想概念。
MVCC模型在MySQL中的具體實現則是由 3個隱式欄位,undo日誌 ,Read View 等去完成的。
MVCC能解決什麼問題,好處是?
資料庫併發場景有三種,分別為:
- 讀-讀:不存在任何問題,也不需要併發控制
- 讀-寫:有執行緒安全問題,可能會造成事務隔離性問題,可能遇到髒讀,幻讀,不可重複讀
- 寫-寫:有執行緒安全問題,可能會存在更新丟失問題,比如第一類更新丟失,第二類更新丟失
MVCC帶來的好處是?
多版本併發控制(MVCC)是一種用來解決讀-寫衝突的無鎖併發控制,也就是為事務分配單向增長的時間戳,為每個修改儲存一個版本,版本與事務時間戳關聯,讀操作只讀該事務開始前的資料庫的快照。 所以MVCC可以為資料庫解決以下問題
- 在併發讀寫資料庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了資料庫併發讀寫的效能
- 同時還可以解決髒讀,幻讀,不可重複讀等事務隔離問題,但不能解決更新丟失問題
- MVCC + 悲觀鎖
MVCC解決讀寫衝突,悲觀鎖解決寫寫衝突 - MVCC + 樂觀鎖
MVCC解決讀寫衝突,樂觀鎖解決寫寫衝突
這種組合的方式就可以最大程度的提高資料庫併發效能,並解決讀寫衝突,和寫寫衝突導致的問題
在InnoDB引擎表中,它的聚簇索引記錄中有兩個必要的隱藏列:
t
每行記錄除了我們自定義的欄位外,還有資料庫隱式定義的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等欄位
- DB_TRX_ID
6byte,最近修改(修改/插入)事務ID:記錄建立這條記錄/最後一次修改該記錄的事務ID - DB_ROLL_PTR
7byte,回滾指標,指向這條記錄的上一個版本(儲存於rollback segment裡) - DB_ROW_ID
6byte,隱含的自增ID(隱藏主鍵),如果資料表沒有主鍵,InnoDB會自動以DB_ROW_ID產生一個聚簇索引 -
實際還有一個刪除flag隱藏欄位, 既記錄被更新或刪除並不代表真的刪除,而是刪除flag變了
----------------------------------------------------------------------------------------------------
說明:
purge
- 從前面的分析可以看出,為了實現InnoDB的MVCC機制,更新或者刪除操作都只是設定一下老記錄的deleted_bit,並不真正將過時的記錄刪除。
- 為了節省磁碟空間,InnoDB有專門的purge執行緒來清理deleted_bit為true的記錄。為了不影響MVCC的正常工作,purge執行緒自己也維護了一個read view(這個read view相當於系統中最老活躍事務的read view);如果某個記錄的deleted_bit為true,並且DB_TRX_ID相對於purge執行緒的read view可見,那麼這條記錄一定是可以被安全清除的。
-
-
-
Undo Log是為了實現事務的原子性,在MySQL資料庫InnoDB儲存引擎中, 還用Undo Log來實現多版本併發控制(簡稱:MVCC)。
-
在操作任何資料之前,首先將資料備份到一個地方(這個儲存資料備份的地方 稱為Undo Log)。然後進行資料的修改。如果出現了錯誤或者使用者執行了 ROLLBACK語句,系統可以利用Undo Log中的備份將資料恢復到事務開始之 前的狀態。
-
注意:undo log是邏輯日誌,可以理解為:
– 當delete一條記錄時,undo log中會記錄一條對應的insert記錄
– 當insert一條記錄時,undo log中會記錄一條對應的delete記錄
– 當update一條記錄時,它記錄一條對應相反的update記錄
-
-
-
Redo日誌—innodb儲存引擎的日誌檔案:
當發生資料修改的時候,innodb引擎會先將記錄寫到redo log中, 並更新記憶體,此時更新就算是完成了,同時innodb引擎會在合適 的時機將記錄操作到磁碟中。
▪ Redolog是固定大小的,是迴圈寫的過程。
▪ 有了redolog之後,innodb就可以保證即使資料庫發生異常重啟,之前的記錄也不會丟失,叫做crash-safe。
binlog—服務端的日誌檔案:Binlog是server層的日誌,主要做mysql功能層面的事情
▪ 與redo日誌的區別:
– 1、redo是innodb獨有的,binlog是所有引擎都可以使用的。
– 2、redo是物理日誌,記錄的是在某個資料頁上做了什麼修改,binlog是邏輯日誌,記錄的是這個語句的原始邏輯。
– 3、redo是迴圈寫的,空間會用完,binlog是可以追加寫的,不會覆蓋之前的日誌資訊。
資料更新的流程:
執行流程:
1、執行器先從引擎中找到資料,如果在記憶體中直接返回,如果不在記憶體中,查 詢後返回 。
2、執行器拿到資料之後會先修改資料, 然後呼叫引擎介面重新寫入資料 。
3、引擎將資料更新到記憶體,同時寫資料 到redo中,此時處於prepare階段,並通 知執行器執行完成,隨時可以操作 。
4、執行器生成這個操作的binlog。
5、執行器呼叫引擎的事務提交介面,引 擎把剛剛寫完的redo改成commit狀態, 更新完成。
聚集索引(聚簇索引):葉節點包含了完整的資料記錄,即:索引和表的資料及樹節點都在一個檔案中。
----------------------------------------------------------------------------------------------------
Mysql分庫分表:
影響資料庫效能:
- 資料量
MySQL單庫資料量在5000萬以內效能比較好,超過閾值後效能會隨著資料量的增大而變弱。MySQL單表的資料量是500w-1000w之間效能比較好,超過1000w效能也會下降。 - 磁碟
因為單個服務的磁碟空間是有限制的,如果併發壓力下,所有的請求都訪問同一個節點,肯定會對磁碟IO造成非常大的影響。 - 資料庫連線
資料庫連線是非常稀少的資源,如果一個庫裡既有使用者、商品、訂單相關的資料,當海量使用者同時操作時,資料庫連線就很可能成為瓶頸。
為了提升效能,就有必要引進分庫分表。
垂直拆分 or 水平拆分?
單個庫太大時,確定是因為表太多還是資料量太大?如果是表太多,則應該將部分表進行遷移(可以按業務區分),這就是所謂的垂直切分。如果是資料量太大,則需要將表拆成更多的小表,來減少單表的資料量,這就是所謂的水平拆分。
垂直拆分
垂直分庫:垂直分庫針對的是一個系統中的不同業務進行拆分,比如使用者一個庫,商品一個庫,訂單一個庫。
垂直分表:即“大表拆小表”,基於列欄位進行的。一般是表中的欄位較多,將不常用的, 資料較大,長度較長(比如text型別欄位)的拆分到“擴充套件表“。一般是針對那種幾百列的大表,也避免查詢時,資料量太大造成的“跨頁”問題。水平拆分
- 水平分表
和垂直分表有一點類似,不過垂直分表是基於列的,而水平分表是基於全表的。水平拆分可以大大減少單表資料量,提升查詢效率。 - 水平分庫分表
將單張表的資料切分到多個伺服器上去,每個伺服器具有相應的庫與表,只是表中資料集合不同。 水平分庫分表能夠有效的緩解單機和單庫的效能瓶頸和壓力,突破IO、連線數、硬體資源等的瓶頸。
幾種常用的分庫分表的策略
-
HASH取模假設有使用者表user,將其分成3個表user0,user1,user2.路由規則是對3取模,當uid=1時,對應到的是user1,uid=2時,對應的是user2.
-
範圍分片從1-10000一個表,10001-20000一個表。
-
地理位置分片華南區一個表,華北一個表。
-
時間分片按月分片,按季度分片等等,可以做到冷熱資料。
三、分庫分表工具
分庫分表步驟
根據容量(當前容量和增長量)評估分庫或分表個數 -> 選key(均勻)-> 分表規則(hash或range等)-> 執行(一般雙寫)-> 擴容問題(儘量減少資料的移動)。
分庫分表問題
1、事務一致性問題:
當更新內容同時分佈在不同庫中,不可避免會帶來跨庫事務問題。跨分片事務也是分散式事務,一般可使用"XA協議"和"兩階段提交"處理。
分散式事務能最大限度保證了資料庫操作的原子性。但在提交事務時需要協調多個節點,推後了提交事務的時間點,延長了事務的執行時間。導致事務在訪問共享資源時發生衝突或死鎖的概率增高。隨著資料庫節點的增多,這種趨勢會越來越嚴重,從而成為系統在資料庫層面上水平擴充套件的枷鎖。
最終一致性
對於那些效能要求很高,但對一致性要求不高的系統,往往不苛求系統的實時一致性,只要在允許的時間段內達到最終一致性即可,可採用事務補償的方式。與事務在執行中發生錯誤後立即回滾的方式不同,事務補償是一種事後檢查補救的措施,一些常見的實現方法有:對資料進行對賬檢查,基於日誌進行對比,定期同標準資料來源進行同步等等。事務補償還要結合業務系統來考慮。
2、跨節點關聯查詢 join 問題
切分之前,系統中很多列表和詳情頁所需的資料可以通過sql join來完成。而切分之後,資料可能分佈在不同的節點上,此時join帶來的問題就比較麻煩了,考慮到效能,儘量避免使用join查詢。
1)全域性表
全域性表,也可看做是"資料字典表",就是系統中所有模組都可能依賴的一些表,為了避免跨庫join查詢,可以將這類表在每個資料庫中都儲存一份。這些資料通常很少會進行修改,所以也不擔心一致性的問題。
2)欄位冗餘
一種典型的反正規化設計,利用空間換時間,為了效能而避免join查詢。例如:訂單表儲存userId時候,也將userName冗餘儲存一份,這樣查詢訂單詳情時就不需要再去查詢"買家user表"了。
但這種方法適用場景也有限,比較適用於依賴欄位比較少的情況。而冗餘欄位的資料一致性也較難保證,就像上面訂單表的例子,買家修改了userName後,是否需要在歷史訂單中同步更新呢?這也要結合實際業務場景進行考慮。
3)資料組裝
在系統層面,分兩次查詢,第一次查詢的結果集中找出關聯資料id,然後根據id發起第二次請求得到關聯資料。最後將獲得到的資料進行欄位拼裝。
4)ER分片
關係型資料庫中,如果可以先確定表之間的關聯關係,並將那些存在關聯關係的表記錄存放在同一個分片上,那麼就能較好的避免跨分片join問題。在1:1或1:n的情況下,通常按照主表的ID主鍵切分。如下圖所示:
這樣一來,Data Node1上面的order訂單表與orderdetail訂單詳情表就可以通過orderId進行區域性的關聯查詢了,Data Node2上也一樣。
3、跨節點分頁、排序、函式問題
跨節點多庫進行查詢時,會出現limit分頁、order by排序等問題。分頁需要按照指定欄位進行排序,當排序欄位就是分片欄位時,通過分片規則就比較容易定位到指定的分片;當排序欄位非分片欄位時,就變得比較複雜了。需要先在不同的分片節點中將資料進行排序並返回,然後將不同分片返回的結果集進行彙總和再次排序,最終返回給使用者。
只是取第一頁的資料,對效能影響還不是很大。但是如果取得頁數很大,情況則變得複雜很多,因為各分片節點中的資料可能是隨機的,為了排序的準確性,需要將所有節點的前N頁資料都排序好做合併,最後再進行整體的排序,這樣的操作時很耗費CPU和記憶體資源的,所以頁數越大,系統的效能也會越差。
在使用Max、Min、Sum、Count之類的函式進行計算的時候,也需要先在每個分片上執行相應的函式,然後將各個分片的結果集進行彙總和再次計算,最終將結果返回。
4、全域性主鍵避重問題
在分庫分表環境中,由於表中資料同時存在不同資料庫中,主鍵值平時使用的自增長將無用武之地,某個分割槽資料庫自生成的ID無法保證全域性唯一。因此需要單獨設計全域性主鍵,以避免跨庫主鍵重複問題。有一些常見的主鍵生成策略:
1)UUID
UUID標準形式包含32個16進位制數字,分為5段,形式為8-4-4-4-12的36個字元,例如:550e8400-e29b-41d4-a716-446655440000
UUID是主鍵是最簡單的方案,本地生成,效能高,沒有網路耗時。但缺點也很明顯,由於UUID非常長,會佔用大量的儲存空間;另外,作為主鍵建立索引和基於索引進行查詢時都會存在效能問題,在InnoDB下,UUID的無序性會引起資料位置頻繁變動,導致分頁。
2)結合資料庫維護主鍵ID表
在資料庫中建立 sequence 表:
CREATE TABLE `sequence` ( `id` bigint(20) unsigned NOT NULL auto_increment, `stub` char(1) NOT NULL default '', PRIMARY KEY (`id`), UNIQUE KEY `stub` (`stub`) ) ENGINE=MyISAM;
stub欄位設定為唯一索引,同一stub值在sequence表中只有一條記錄,可以同時為多張表生成全域性ID。sequence表的內容,如下所示:
+-------------------+------+ | id | stub | +-------------------+------+ | 72157623227190423 | a | +-------------------+------+
使用 MyISAM 儲存引擎而不是 InnoDB,以獲取更高的效能。MyISAM使用的是表級別的鎖,對錶的讀寫是序列的,所以不用擔心在併發時兩次讀取同一個ID值。
當需要全域性唯一的64位ID時,執行:
REPLACE INTO sequence (stub) VALUES ('a'); SELECT LAST_INSERT_ID();
這兩條語句是Connection級別的,select last_insert_id() 必須與 replace into 在同一資料庫連線下才能得到剛剛插入的新ID。
使用replace into代替insert into好處是避免了錶行數過大,不需要另外定期清理。
此方案較為簡單,但缺點也明顯:存在單點問題,強依賴DB,當DB異常時,整個系統都不可用。配置主從可以增加可用性,但當主庫掛了,主從切換時,資料一致性在特殊情況下難以保證。另外效能瓶頸限制在單臺MySQL的讀寫效能。
升級版:建立2個以上的全域性ID生成的伺服器,每個伺服器上只部署一個數據庫,每個庫有一張sequence表用於記錄當前全域性ID。表中ID增長的步長是庫的數量,起始值依次錯開,這樣能將ID的生成雜湊到各個資料庫上。
由兩個資料庫伺服器生成ID,設定不同的auto_increment值。第一臺sequence的起始值為1,每次步長增長2,另一臺的sequence起始值為2,每次步長增長也是2。結果第一臺生成的ID都是奇數(1, 3, 5, 7 ...),第二臺生成的ID都是偶數(2, 4, 6, 8 ...)。
這種方案將生成ID的壓力均勻分佈在兩臺機器上。同時提供了系統容錯,第一臺出現了錯誤,可以自動切換到第二臺機器上獲取ID。但有以下幾個缺點:系統新增機器,水平擴充套件時較複雜;每次獲取ID都要讀寫一次DB,DB的壓力還是很大,只能靠堆機器來提升效能。
5、資料遷移、擴容問題
當業務高速發展,面臨效能和儲存的瓶頸時,才會考慮分片設計,此時就不可避免的需要考慮歷史資料遷移的問題。一般做法是先讀出歷史資料,然後按指定的分片規則再將資料寫入到各個分片節點中。此外還需要根據當前的資料量和QPS,以及業務發展的速度,進行容量規劃,推算出大概需要多少分片(一般建議單個分片上的單表資料量不超過1000W)
如果採用數值範圍分片,只需要新增節點就可以進行擴容了,不需要對分片資料遷移。如果採用的是數值取模分片,則考慮後期的擴容問題就相對比較麻煩。
三. 什麼時候考慮切分
下面講述一下什麼時候需要考慮做資料切分。
1、能不切分儘量不要切分
並不是所有表都需要進行切分,主要還是看資料的增長速度。切分後會在某種程度上提升業務的複雜度,資料庫除了承載資料的儲存和查詢外,協助業務更好的實現需求也是其重要工作之一。
不到萬不得已不用輕易使用分庫分表這個大招,避免"過度設計"和"過早優化"。分庫分表之前,不要為分而分,先盡力去做力所能及的事情,例如:升級硬體、升級網路、讀寫分離、索引優化等等。當資料量達到單表的瓶頸時候,再考慮分庫分表。
2、資料量過大,正常運維影響業務訪問
這裡說的運維,指:
1)對資料庫備份,如果單表太大,備份時需要大量的磁碟IO和網路IO。例如1T的資料,網路傳輸佔50MB時候,需要20000秒才能傳輸完畢,整個過程的風險都是比較高的
2)對一個很大的表進行DDL修改時,MySQL會鎖住全表,這個時間會很長,這段時間業務不能訪問此表,影響很大。如果使用pt-online-schema-change,使用過程中會建立觸發器和影子表,也需要很長的時間。在此操作過程中,都算為風險時間。將資料表拆分,總量減少,有助於降低這個風險。
3)大表會經常訪問與更新,就更有可能出現鎖等待。將資料切分,用空間換時間,變相降低訪問壓力
3、隨著業務發展,需要對某些欄位垂直拆分
舉個例子,假如專案一開始設計的使用者表如下:
id bigint #使用者的ID name varchar #使用者的名字 last_login_time datetime #最近登入時間 personal_info text #私人資訊 ..... #其他資訊欄位
在專案初始階段,這種設計是滿足簡單的業務需求的,也方便快速迭代開發。而當業務快速發展時,使用者量從10w激增到10億,使用者非常的活躍,每次登入會更新 last_login_name 欄位,使得 user 表被不斷update,壓力很大。而其他欄位:id, name, personal_info 是不變的或很少更新的,此時在業務角度,就要將 last_login_time 拆分出去,新建一個 user_time 表。
personal_info 屬性是更新和查詢頻率較低的,並且text欄位佔據了太多的空間。這時候,就要對此垂直拆分出 user_ext 表了。
4、資料量快速增長
隨著業務的快速發展,單表中的資料量會持續增長,當效能接近瓶頸時,就需要考慮水平切分,做分庫分表了。此時一定要選擇合適的切分規則,提前預估好資料容量。
--------------------------------------------------------------------------------------------------------
說明:
XA分散式事務協議,包含二階段提交(2PC),三階段提交(3PC)兩種實現。
1、二階段提交方案:強一致性
事務的發起者稱為協調者,事務的執行者稱為參與者。
處理流程:
1、準備階段
事務協調者,向所有事務參與者傳送事務內容,詢問是否可以提交事務,並等待參與者回覆。
事務參與者收到事務內容,開始執行事務操作,記錄 undo 和 redo 資訊的事務日誌(但此時並不提交事務)。
如果參與者執行成功,給協調者回復yes,表示可以進行事務提交。如果執行失敗,給協調者回復no,表示不可提交。
2、提交階段
如果協調者收到了參與者的失敗資訊或超時資訊,直接給所有參與者傳送回滾(rollback)資訊進行事務回滾,否則,傳送提交(commit)資訊。
方案總結
2PC 方案實現起來簡單,實際專案中使用比較少,主要因為以下問題:
- 效能問題:所有參與者在事務提交階段處於同步阻塞狀態,佔用系統資源,容易導致效能瓶頸。
- 可靠性問題:如果協調者存在單點故障問題,如果協調者出現故障,參與者將一直處於鎖定狀態。
- 資料一致性問題:在階段 2 中,如果發生區域性網路問題,一部分事務參與者收到了提交訊息,另一部分事務參與者沒收到提交訊息,那麼就導致了節點之間資料的不一致。
2、3階段提交
三階段提交是在二階段提交上的改進版本,主要是加入了超時機制。同時在協調者和參與者中都引入超時機制。
三階段將二階段的準備階段拆分為2個階段,插入了一個preCommit階段,以此來處理原先二階段,參與者準備後,參與者發生崩潰或錯誤,導致參與者無法知曉是否提交或回滾的不確定狀態所引起的延時問題。
處理流程
階段 1:canCommit
協調者向參與者傳送 commit 請求,參與者如果可以提交就返回 yes 響應(參與者不執行事務操作),否則返回 no 響應:
- 協調者向所有參與者發出包含事務內容的 canCommit 請求,詢問是否可以提交事務,並等待所有參與者答覆。
- 參與者收到 canCommit 請求後,如果認為可以執行事務操作,則反饋 yes 並進入預備狀態,否則反饋 no。
階段 2:preCommit
協調者根據階段 1 canCommit 參與者的反應情況來決定是否可以進行基於事務的 preCommit 操作。根據響應情況,有以下兩種可能。
情況 1:階段 1 所有參與者均反饋 yes,參與者預執行事務:
-
- 協調者向所有參與者發出 preCommit 請求,進入準備階段。
- 參與者收到 preCommit 請求後,執行事務操作,將 undo 和 redo 資訊記入事務日誌中(但不提交事務)。
- 各參與者向協調者反饋 ack 響應或 no 響應,並等待最終指令。
情況 2:階段 1 任何一個參與者反饋 no,或者等待超時後協調者尚無法收到所有參與者的反饋,即中斷事務:
-
- 協調者向所有參與者發出 abort 請求。
- 無論收到協調者發出的 abort 請求,或者在等待協調者請求過程中出現超時,參與者均會中斷事務。
階段 3:do Commit :該階段進行真正的事務提交,也可以分為以下兩種情況。
情況 1:階段 2 所有參與者均反饋 ack 響應,執行真正的事務提交,如上圖:
-
- 如果協調者處於工作狀態,則向所有參與者發出 do Commit 請求。
- 參與者收到 do Commit 請求後,會正式執行事務提交,並釋放整個事務期間佔用的資源。
- 各參與者向協調者反饋 ack 完成的訊息。
- 協調者收到所有參與者反饋的 ack 訊息後,即完成事務提交。
情況 2:階段 2 任何一個參與者反饋 no,或者等待超時後協調者尚無法收到所有參與者的反饋,即中斷事務,如上圖:
-
- 如果協調者處於工作狀態,向所有參與者發出 abort 請求。
- 參與者使用階段 1 中的 undo 資訊執行回滾操作,並釋放整個事務期間佔用的資源。
- 各參與者向協調者反饋 ack 完成的訊息。
- 協調者收到所有參與者反饋的 ack 訊息後,即完成事務中斷。
方案總結
優點:相比二階段提交,三階段提交降低了阻塞範圍,在等待超時後協調者或參與者會中斷事務。避免了協調者單點問題,階段 3 中協調者出現問題時,參與者會繼續提交事務。
缺點:資料不一致問題依然存在,當在參與者收到 preCommit 請求後等待 do commite 指令時,此時如果協調者請求中斷事務,而協調者無法與參與者正常通訊,會導致參與者繼續提交事務,造成資料不一致。