一文詳解 OceanBase 2.0 的“全域性索引”功能

螞蟻金服OceanBase資料庫團隊發表於2019-03-01
OB君:本文是 “OceanBase 2.0 技術解析系列” 的第九篇文章。今天我們來聊聊2.0的全域性索引功能。本文將帶你簡單回顧全域性索引的概念,並詳細介紹OceanBase 2.0版本如何實現全域性索引的功能。更多精彩關注OceanBase公眾號持續訂閱本系列內容!
一文詳解 OceanBase 2.0 的“全域性索引”功能

前言

在資料庫領域中,“分割槽表”的概念大家並不陌生,但是分割槽表中“本地索引(Local Index)”和“全域性索引(Global Index)”的概念,未必每個朋友都關注過。和本地索引相比,全域性索引在使用上更加靈活方便,在很多場景下也能提供更好的查詢效能,對開發人員來說更加友好。但是全域性索引的實現也會有更大的難度,尤其是在分散式環境下,實現難度更大。

本文將幫助讀者簡單回顧一下全域性索引相關的概念,並介紹OceanBase資料庫如何在2.0版本中實現全域性索引的功能。

本地索引和全域性索引概述

為了方便大家閱讀,我們先約定一些固定的術語,後文的描述中會統一採用這些術語:

  • 主表

指使用CREATE TABLE語句建立的表物件。也是索引物件所依賴的表(即CREATE INDEX語句中ON子句所指定的表)。

  • 索引(索引表)

指使用CREATE INDEX語句建立的索引物件。有時為了便於大家理解,也會把索引物件類比為一個表物件,即索引表。

此外,為了方便大家理解文字描述,我們會以一個實際的資料庫表來演示各種情況,這個表的名字叫employee(員工資訊表),表的結構如下:

employee
{
emp_id, /* 員工ID */
emp_name /* 員工名字*/
dpet_id, /* 部門ID */
...
}

首先,我們來簡單回顧一下傳統的“非”分割槽表中,主表和索引的對應關係。主表的所有資料都儲存在一個完整的資料結構中,主表上的每一個索引也對應一個完整的資料結構(比如最常見的B+ Tree),主表的資料結構和索引的資料結構之間是一對一的關係,如下圖所示:

一文詳解 OceanBase 2.0 的“全域性索引”功能

上圖展示了employee表中,以emp_id建立的索引。

當分割槽表出現之後,情況發生了變化:主表的資料按照分割槽鍵(Partitioning Key)的值被分成了多個分割槽,每個分割槽都是獨立的資料結構,分割槽之間的資料沒有交集。這樣一來,索引所依賴的單一資料結構不復存在,那索引需要如何應對呢?這就引入了“本地索引”和“全域性索引”兩個概念:

1)本地索引(Local Index)

分割槽表的本地索引和非分割槽表的索引類似,索引的資料結構還是和主表的資料結構保持一對一的關係,但由於主表已經做了分割槽,主表的“每一個分割槽”都會有自己單獨的索引資料結構。對每一個索引資料結構來說,裡面的鍵(Key)只對映到自己分割槽中的主表資料,不會對映到其它分割槽中的主表,因此這種索引被稱為本地索引。

從另一個角度來看,這種模式下索引的資料結構也做了分割槽處理,因此有時也被稱為本地分割槽索引(Local Partitioned Index)。本地索引的結構如下圖所示:

一文詳解 OceanBase 2.0 的“全域性索引”功能

在上圖中,employee表按照emp_id做了範圍分割槽,同時也在emp_name上建立了本地索引。

2)全域性索引(Global Index)

和分割槽表的本地索引相比,分割槽表的全域性索引不再和主表的分割槽保持一對一的關係,而是將所有主表分割槽的資料合成一個整體來看,索引中的一個鍵可能會對映到多個主表分割槽中的資料(當索引鍵有重複值時)。更進一步,全域性索引可以定義自己獨立的資料分佈模式,既可以選擇非分割槽模式也可以選擇分割槽模式;在分割槽模式中,分割槽的方式既可以和主表相同也可以和主表不同。因此,全域性索引又分為以下兩種形式:

  • 全域性非分割槽索引(Global Non-Partitioned Index)

索引資料不做分割槽,保持單一的資料結構,和非分割槽表的索引類似。但由於主表已經做了分割槽,因此會出現索引中的某一個鍵對映到不同主表分割槽的情況,即“一對多”的對應關係。全域性非分割槽索引的結構如下圖所示:

一文詳解 OceanBase 2.0 的“全域性索引”功能

在上圖中,雖然employee表按照emp_id做了分割槽,但是建立在dept_id上的全域性索引並沒有分割槽,因此同一個鍵值可能會對映到多個分割槽中。

  • 全域性分割槽索引(Global Partitioned Index)

索引資料按照指定的方式做分割槽處理,比如做雜湊(Hash)分割槽或者範圍(Range)分割槽,將索引資料分散到不同的分割槽中。但索引的分割槽模式是完全獨立的,和主表的分割槽沒有任何關係,因此對於每個索引分割槽來說,裡面的某一個鍵都可能對映到不同的主表分割槽(當索引鍵有重複值時),索引分割槽和主表分割槽之間是“多對多”的對應關係。全域性分割槽索引的結構如下圖所示:

一文詳解 OceanBase 2.0 的“全域性索引”功能

在上圖中,employee表按照emp_id做了範圍分割槽,同時在emp_name上做了全域性分割槽索引。可以看到同一個索引分割槽裡的鍵,會指向不同的主表分割槽。

由於全域性索引的分割槽模式和主表的分割槽模式完全沒有關係,看上去全域性索引更像是另一張獨立的表,因此也會將全域性索引叫做索引表,理解起來會更容易一些(和主表相對應)。

這裡特別說一點:“非”分割槽表也可以建立全域性分割槽索引。但如果主表沒有分割槽的必要,通常來說索引也就沒有必要分割槽了。後文中會忽略這種特殊情況,主要討論分割槽表中的本地索引和全域性索引。

本地索引和全域性索引的比較

那麼,對於分割槽表來說,本地索引和全域性索引哪一種更好呢?其實兩種索引是各有優劣勢的,下面分別加以說明。

首先來說本地索引。它最明顯的好處就是實現簡單,但本地索引的問題也很明顯,尤其是“索引鍵沒有包含主表所有的分割槽鍵欄位”的情況,此時對某一個索引鍵值來說,對應的索引資料在所有分割槽的本地索引中都可能存在,這會引發兩個突出的問題:

1. 由於本地索引只處理一個主表分割槽的資料,因此只能在一個主表分割槽內保證索引鍵的“唯一性約束”,無法在全表範圍內保證索引鍵的唯一性約束,如下圖所示:

一文詳解 OceanBase 2.0 的“全域性索引”功能

上面的圖中,employee按照emp_id做了範圍分割槽,但同時想利用本地索引建立關於emp_name的唯一約束,這是根本無法實現的。

針對這個問題,有些資料庫在分割槽表中會增加限制,要求主鍵和唯一索引的定義中必須包含主表所有的分割槽鍵欄位。有了這個限制,索引中的某一個鍵所對應的索引資料只可能存在於一個分割槽中,因此只要在每一個分割槽內保證唯一性約束,即可在全表範圍內保證唯一性約束。這個限制雖然解決了資料庫的難題,卻大大增加了開發人員的煩惱,因為它會導致主鍵和唯一索引的可選範圍大大縮小(只能是主表分割槽鍵的“超集”),很多業務需求因此無法滿足,這也是本地索引最被詬病的地方。

2. 由於某一個索引鍵值在所有分割槽的本地索引上都可能存在,任何索引掃描必須在所有的分割槽上都做一遍,以免造成資料遺漏。

這會導致索引掃描效率低下,並且會在全域性範圍內造成CPU和IO資源的浪費。採用“分割槽間並行”的手段可以提高效率,但不能從根本上解決這個問題。

下面來看看全域性索引。相比本地索引來說,全域性索引具有諸多優勢,尤其是針對前面談到的,當“索引鍵沒有包含主表所有的分割槽鍵欄位”時本地索引所面臨的兩個突出問題:

1. 不能實現全域性唯一性約束的問題。

對於全域性索引來說,可以為索引資料指定自己的分割槽方式,並且索引的分割槽鍵一定是索引鍵的子集,因此可以很容易解決這個問題。下面分別以幾種情況做說明:

  • 全域性非分割槽索引

此時索引的結構和“非分割槽”表沒有區別,只有一個完整的索引樹,自然很容易保證唯一性。

  • 全域性分割槽索引

這種情況下,對於某一個索引鍵來說,由於包含了所有的索引分割槽鍵,它的資料只可能落在一個固定的索引分割槽中,因此只要在每一個索引分割槽內保證唯一性約束,就可以在全表範圍內保證唯一性約束。而單個索引分割槽內只有一個索引資料結構,很容易保證唯一性約束,因此問題就得到了解決。

2. 必須掃描“所有”分割槽所帶來的效能和資源浪費問題。

這個問題也可以分幾種情況分別說明:

  • 全域性非分割槽索引

前面講述唯一性約束的問題時提到了,此時只有一個完整的索引樹,自然沒有多分割槽掃描的問題。

  • 全域性分割槽索引

前面講述唯一性約束的問題時提到了,全域性索引能保證某一個索引鍵的資料只落在一個固定的索引分割槽中。對於索引鍵值的範圍掃描,我們希望索引掃描按單向順序在每個分割槽內都只執行一次,而不必在索引分割槽之間來回穿梭,因此我們要求索引的分割槽鍵是索引鍵的“字首(Prefix)”:比如索引鍵是"a,b",那麼索引的分割槽鍵可以是"a"或者"a,b",但不能是"b",即要求分割槽的排序規則和索引樹的排序規則一致。這樣一來,無論是針對固定鍵值的索引掃描,還是針對一個鍵值範圍的索引掃描,都可以直接定位出需要掃描的一個或者幾個分割槽,而不是像本地索引一樣盲目地在所有分割槽上掃描,這樣就解決了本地索引面臨的問題。

解決了上述問題之後,從使用者的角度來看,分割槽表的全域性索引和非分割槽表的索引已經沒有太大區別,除了一點:全域性索引需要定義索引的分割槽模式。這裡雖然也可以使用全域性“非分割槽”索引,但是引進全域性索引的目的就是針對資料量較大的分割槽表,對應的索引資料量往往也是非常大的,因此還是推薦使用全域性分割槽索引,不但可以突破非分割槽索引面臨的單點容量限制,在併發較大的情況下也能獲得更好的效能。

當然,全域性索引也面臨一些問題,主要是架構複雜,實現難度大,以及由此引發的一些相關問題,比如當索引資料和對應的主表資料位於不同的機器時,在事務內會面臨資料一致性和效能方面的挑戰。但考慮到全域性索引給資料庫使用者帶來的巨大便利,付出一點代價也是值得的。

綜合二者的特點來看,本地索引和全域性索引的適用場景是不一樣的:

  • 本地索引比較適合“索引鍵包含主表所有的分割槽鍵欄位”這種特殊情況。
  • 除了上面的特殊情況之外的其它場景,全域性索引更加合適。

可以說,全域性索引具有更強的通用性。

OceanBase中的全域性索引

關於全域性索引的實現原理,以及全域性索引和本地索引的比較,前面都已經有過詳細的描述,這是業界實現全域性索引的基本思路,也是OceanBase中實現全域性索引的基本思路。但是,前面我們更多的是討論索引資料結構和主表資料結構的對映關係(一對一、一對多、多對多等),而忽略了另外一個很重要的因素要,那就是資料的物理分佈

對於傳統的單點資料庫來說,每個資料結構之下都是“本地可訪問”的儲存層,比如一個本地資料庫表空間(裡面包含若干本地檔案或者裝置),無論有多少個主表分割槽或者索引分割槽,資料庫總是可以通過本地機器訪問到,即Share-Everything的架構。

但是對於OceanBase這樣的分散式資料庫來說,每一個主表分割槽和索引分割槽都可能分佈在不同的機器上,比如“N個主表分割槽+M個全域性索引分割槽”這種多對多的複雜情況,可能是(N+M)臺物理機器上形成的(N*M)種索引鍵到主表分割槽的對映關係,這會帶來諸多挑戰:

  • 如果一個事務中要修改的N條記錄分別位於N臺不同的機器上,而它們對應的全域性索引資料又位於另外M臺不同的機器上,如何在這(N+M)臺機器間保證主表資料和索引資料的同步更新?
  • 還是上面所說的主表資料和索引資料分佈在不同機器的場景,在保證資料一致性的前提下,如何進一步縮短跨機器訪問的時間,進一步提高查詢效率?
  • 如何保證全域性索引資料在全域性範圍內的讀寫一致性?一臺機器上更新的索引資料,如何確保一定能被其它機器上後續的訪問者讀到?

可以說,如果不能解決上述問題,全域性索引在分散式資料庫中就失去了存在的基礎。因此,雖然業內有很多分散式資料庫,但全域性索引功能卻並不是一個標配,很多大家所熟悉的分散式資料庫產品(如MongoDB,Cassandra,Hbase等)並不提供全域性索引功能,只有少數產品才能提供,比如Google F1/Spanner和CouchBase,這也從一個側面印證了在分散式資料庫中實現全域性索引的難度。下面我就簡單給大家介紹一下,OceanBase資料庫是如何跨越上述的一個個難關,在2.0版本中實現了全域性索引的功能。

首先來看第一個問題,如何保證主表資料和索引資料的跨機器同步更新。這個問題的本質,其實是分散式資料庫中“分散式事務一致性”問題,主表資料和索引資料是同一個事務中的兩部分資料,當它們分佈在不同的機器上時,分散式事務需要保證事務的原子性(Atomicity):兩部分資料要麼全部更新成功,要麼全部失敗,不會因事務異常而導致主表資料和索引資料的不同步。

OceanBase資料庫在幾年前的1.0版本中就提供了分散式事務功能,利用Paxos協議和經過改良的“兩階段提交(Two-phase Commit)”方法,能在跨機器的分散式事務內保證ACID,即使事務的參與者發生異常(如機器當機),也能確保分散式事務的完整性,避免了傳統兩階段提交方法會導致的“事務部分未決(In-doubt Transaction)”問題,因此OceanBase完全可以保證跨機器事務中主表資料和索引資料的同步。關於OceanBase資料庫分散式事務的詳細內容,讀者可以查閱本文末尾參考文件中的相關文章。

解決了分散式事務的一致性問題,我們來關注一下第二個問題,就是索引資料和主表資料分跨機器訪問時的效率問題。這裡面臨的最大挑戰就是分散式事務的網路延遲和多次日誌落盤,而這種物理開銷是沒有辦法完全消除的,為此Google在F1的論文中明確說明不推薦太多的全域性索引,並且應儘量避免對有全域性索引的表做大事務訪問。那OceanBase是如何應對這個問題的呢?

前面提到過,OceanBase使用了經過改良的兩階段提交方法,這種改良不僅體現在保證資料的一致性上,也體現在效能上:和傳統的兩階段提交相比,OceanBase的兩階段提交過程中會有更少的網路互動次數以及更少的寫日誌次數,而這些恰恰都是分散式事務中最耗時的操作。有了這樣的優化,在很大程度上縮短了分散式事務的處理時間,提高了全域性索引的處理效率。

最後一個問題,則是分散式資料庫中全域性(跨多臺機器)的讀寫一致性問題。對OceanBase這樣實現了快照隔離級別(Snapshot Isolation)和多版本併發控制(MVCC)的分散式資料庫來說,如何在機器間有時鐘差異的情況下,仍能維持時間戳(即版本號)在全域性範圍內的前後一致性,這是一個很重要的問題。

在OceanBase資料庫的2.0版本中,我們提供了“全域性一致性快照”功能,並在此基礎上實現了全域性範圍內的快照隔離級別和多版本併發控制,這樣就能保證全域性範圍內前後事務的讀寫一致性,滿足了全域性索引的要求。關於OceanBase資料庫全域性一致性快照的詳細內容,讀者可以查閱本文末尾參考文件中的相關文章。

因此,OceanBase資料庫的2.0版本解決了上述所有的技術難題,我們也正式推出了全域性索引功能。對於資料庫使用者來說,可以不再擔心本地索引在分割槽表使用中的諸多不便,利用全域性索引實現任意模式的全域性一致性約束,並且為更多的複雜查詢場景提供更優的效能,盡情體驗“全域性索引+分散式架構”帶來的完美體驗!

參考文件

OceanBase 2.0技術群

— 想了解更多OceanBase 2.0新特性?

— 想與螞蟻金服OceanBase的一線技術專家深入交流?

掃描下方二維碼聯絡小編,快速加入OceanBase技術交流群!

一文詳解 OceanBase 2.0 的“全域性索引”功能

2.0解析系列文章


相關文章