Realm 核心數據庫引擎探秘
Realm 大部分代碼都是開源的,但是其強大功能取決于隱藏在平臺內部的一個核心數據庫引擎,這個引擎完全由 C++ 編寫而成。在這次講座當中,來自 Realm 的工程師 JP Simard 將帶領大家一探 Realm 的核心!JP 將闡述 Realm 設計背后的準則,包括 Realm 是如何保證快速高效運行的,以及為什么我們要自己撰寫數據庫引擎,而不是像包括 Core Data 在內的移動端數據庫解決方案那樣,采用 SQLite 作為內部核心。在這個講座中,你還能夠了解到如何創建一個高效的模型層,以及相比其他數據庫解決方案來說 Realm 的優勢何在。
See the discussion on Hacker News .
Transcription below provided by Realm: a replacement for SQLite & Core Data that makes building iOS apps a joy. Check out the docs!
Sign up to be notified of new videos — we won’t email you for any other reason, ever.
About the Speaker: JP Simard
JP 負責Realm 數據庫 Objective-C 與 Swift 版本的構建,是 jazzy (蘋果忘記發布的文檔工具) 的創始人,他十分喜歡開發關于 Swift 的工具。
簡介(0:00)
自從一年前我們發布 Realm 以來,我們得到了很多詢問關于 Realm 工作原理的問題。我們已經很詳細地闡述過使用 Realm 的方法以及 Realm 的優勢何在,但是我們仍未向大家分享我們核心數據庫引擎背后的復雜性(complexity)和強大能力。
我開始思考如何向大家分享為什么我們決定從頭自己搭建 Realm 引擎,而不是在諸如 SQLite 這樣的成熟、穩定的數據庫核心的基礎上構建 ORM。讓我們來詳細看一下是什么讓 Realm 工作的,也就是目前我們正使用的 C++ 核心引擎。
對象就是一切(1:33)
Realm 最核心的理念就是對象驅動,這是 Realm 的核心原則。這個原則是我們從頭自己搭建引擎,而不是采用現有的關系模型的原因之一。如果你看一下現有的解決方案,你會發現它們大多是 ORM 結構。通常情況下,人們下意識會使用面向對象模型,實際上也就是對底層發生的操作進行抽象。而 ORM 通常都是記錄、帶有外鍵的表以及主鍵的集合。一旦你開始建立關系,抽象步驟就會變得難上加難,因為你需要花費更多的操作來遍歷這些關系。
讓我們來直面這個問題,這也是大家構建應用的時候都會碰到的問題,這個問題從智能手機出現以來就困擾著大多數開發者。不幸的是,現有的數據庫并沒有考慮到這個功能。這帶來的結果就是數據庫存在著大量冗余復雜的映射層,如果要避免的話,就可以帶來極大的優化。不僅優化了性能,而且實現方式還足夠簡單。
Realm 是什么?(3:17)
Realm 本質上是一個嵌入式數據庫,但是它也是看待數據的另一種方式。它用另一種角度來重新看待你移動應用中的模型和業務邏輯。我們所做的就是嘗試減少數據庫讀寫的開銷。我們盡可能讓其運行得足夠快,因此我們一直在調整性能數據。
我們同樣盡可能添加了諸如零拷貝(zero-copy)之類的操作,這也是為什么我們有底氣保證 Realm 能夠替代你現在所使用的對象訪問器。我們減少了從數據庫讀出數據的步驟,你只需要讀出來,放到一個實例變量當中,然后就結束了。相反,你可以直接訪問數據庫。這樣一來,你就不會復制任何東西,并且如果沒有必要你也無需對數據進行反序列化(deserialize)。
Realm 目前正被很多人使用,就和其他優秀數據庫一樣,它兼容 ACID,并且是跨平臺的。
Realm 長什么樣?(4:17)
Swift
let company = Company() // Realm 對象單例 company.name = "Realm" // 諸如此類... let realm = Realm() // 默認的 Realm 數據庫 realm.write { // 寫操作事務 realm.add(company) // 持久化 Realm 對象 } // 檢索 let companies = realm.objects(Company) // 類型安全 companies[0].name // => Realm (泛型) // 檢索所有全職的名為"Jack"的人 (懶加載、可鏈) let ftJacks = realm.objects(Employee).filter("name = 'Jack'") .filter("fullTime = true")
Objective-C
// Realm 對象單例 Company *company = [[Company alloc] init]; company.name = @"Realm"; // 諸如此類... // 寫操作事務 RLMRealm *realm = [RLMRealm defaultRealm]; [realm transactionWithBlock:^{ [realm addObject:company]; }]; RLMResults *companies = [Company allObjects]; // 檢索所有全職的名為"Jack"的人 (懶加載、可鏈) RLMResults *ftJacks = [[Employee objectsWhere:@"name = 'Jack'"] objectsWhere:@"fullTime == YES"];
Java
Realm realm = Realm.getInstance(this.getContext()); // 默認的 Realm 數據庫 realm.beginTransaction(); // 事務 Company company = realm.createObject(Company.class); // 持久化 dog.setName("Realm"); // 諸如此類... realm.commitTransaction(); // 檢索 Company company = realm.where(Company.class).findFirst(); company.getName; // => Realm // 檢索所有全職的名為"Jack"的人 (懶加載、可鏈) RealmResults<Employee> ftJacks = realm.where(Employee.class) .equalTo("name", "Jack") .equalTo("fullTime", true) .findAll();
你可以通過 Realm 來創建對象,就和 Swift 或者 Objective-C 一樣,然后設置其屬性,但是 Realm 的對象帶有數據庫連接的概念。一旦你實例化 Realm 數據庫后,你就成功地連接到數據庫了。連接數據庫并不如你所想的那樣復雜。在 Realm 中,為了減少你可能會存儲在內存中的數據量,我們會將內存進行映射。只要你向 Realm 中添加了company對象,這個對象就會變成訪問器(accessor)。一旦你從中讀取屬性,你就不再是訪問內存變量了,而是直接訪問數據庫中的數據,這樣就可以免除一堆的內存拷貝以及至少四個到五個的讀取步驟。
接下來是檢索。在 Swift 中,我們使用泛型(和 Xcode 7 中的 Objective-C 很像),我們允許你像范圍屬性那樣訪問檢索列表和檢索結果。零拷貝和懶加載在你執行條件檢索的時候得到了充分的展現,比如說打算檢索一個公司中所有全職的名為”Jack”的人。即使我們在得到所有公司信息之前執行了同樣的檢索操作,我們仍不會從硬盤中讀取任何的固原信息。相反,我們會編譯這個檢索對象。即時我們在此之后添加了條件,我們仍不會重復執行檢索操作,我們簡單搭建了一個結果結構的屬性圖。即使在這個檢索外訪問第一條檢索結果,我們也不會從所有對象中讀取所有的屬性,因為這是懶加載的。這種行為允許我們能夠得到不錯的性能指標。
然而,這確實付出了許多代價。很多時候我們經常聽到這樣的話:“如果我能夠在 Realm 中使用 Swift 結構體就好了。”沒錯,這的確會非常贊,但是根據目前 Swift 語言的設計方式,你就必須將整個結構體全部拷貝到內存當中,這也正是我們希望避免的。所以盡管你可以自己分離所有的對象,將它們放到內存中,然后從數據庫中完全分離實際臃腫但是感覺輕巧的對象,這和我們致力于引導你如何構建應用的方向大相徑庭。我們希望能夠幫助你 使用 工具而不是 被 工具使用。
對于 Objective-C 來說,設計是非常相似的。對于 Java 來說,盡管有特定的檢索函數,但是整體上設計是也是非常相似的。對于 Cocoa 框架,我們使用NSPredicate來檢索。
為什么要從新構建 Realm 數據庫?(7:37)
我們為什么決定設計自己的數據庫引擎而不是使用已有的引擎呢?為什么要在已經有15年歷史、有良好穩定性、魯棒性以及久經考驗的引擎的基礎上獨辟蹊徑呢?
一部分原因我已經說過,就是避免 ORM 架構以及其帶來的抽象方式。通過盡可能細致的分割和組合,我們能夠減少復雜性。另一個原因就是市面上對商用數據庫已經有了大量的研究和開發。
在上面的視頻中有一幅圖表,描繪出了自20世紀90年代后期以來數據庫所進行的創新。查看視頻中的幻燈片。
注意圖表頂部的一系列活動,它代表著服務端數據庫。每一個熱點都意味著一個新的數據庫出現。這里出現了很多創新,尤其是2007年智能手機出現的時候。你可能會認為對于服務器端數據庫來說也一樣,但是事實并非如此。
這是圖標的下半部分,這就是移動端數據庫。現在我們有 SQLite,這是2000左右出現的……然后之后有記載的就沒有了。我們有許多基于 SQLite 構建的封裝數據庫:Core Data、ORMLite、greenDAO等等。許多優秀的產品都基于這個底層核心技術構建,但是沒有一個產品在核心層替換掉數據庫引擎。我們著眼于此,發現有大量能夠給移動端帶來的新技術,這些新技術都有十分強大的優點,但是限制是你只能在服務器端實現。
服務器端數據庫為了滿足服務器需求做出了許多妥協,比如說大規模分布(massively distribute)、多實例間共享,以及 Internet 低延遲訪問等等。在我們為移動端設計的時候,就可以將這些限制扔掉,專注于提供最好的本地體驗就可以了。
MVCC: 多版本并發控制(10:41)
我們的目標就是給移動端應用上這些新技術。其中一個就是 MVCC: 多版本并發控制, Multiversion Concurrency Control 。這和源代碼管理算法(例如 Git)的設計是一樣的。你可以將 Realm 內部的模型看作是 Git,因為它還包括了分支以及原子性提交(atomic commit)的概念。這意味著你無需完全復制所有的數據就可以在多個分支上工作。你可以執行一個語義化的寫時拷貝類型(copy-on-wirte type of semantic)而無需但系另一個線程上的寫操作會對其產生影響。事務當中將會完全摒除外界的影響,只需要很小的一點開銷就可以實現這一點。
當事務開始的時候,你可以將所有的事務都視為數據庫的一份快照。這就是為什么你能夠在上百個線程中做大量的操作并同時訪問數據庫,卻不會發生崩潰的原因。
另一個就是執行寫操作事務無需阻塞讀操作,并且也無需執行太多的簿記(bookkeeping)操作。你可以暫停寫事務,然后在讀事務中繼續讀取數據,因此即使有寫操作事務正在運行,你也可以有多個讀操作事務。在某種意義上,它是數據的一個常量,也就是一個快照。目前在構建級別上 Realm 的工作方式允許你在單獨事務中修改信息。然而,借助底層方案,我們可以阻止發生在事務中的任何修改操作,然后你就可以免費得到數據的不可變性。基本上,通過我們的核心你得到的好處十分多,無需考慮諸如大多數 ORM 數據庫還需要解決的底層數據庫存儲的問題。
本地鏈接(13:18)
鏈接無處不在,文件系統的核心就在于鏈接。在 Realm 中,在多鏈接中,也就是建立了關系的對象中執行檢索是依靠“B-樹”來進行的,因此檢索的速度十分快速。你無需在 ORM 之間建立關系的雙重抽象(dual abstraction),只需在文件系統層級的文件轉換中,建立直接的對象鏈接即可。對于檢索來說也是一樣的,比如說檢索整數列、關系、一對多關系甚至多對多關系都可以。
這正是對象圖遍歷的強大之處,因此當我們在以面向對象的方式設計移動應用的時候,我們通常就會采用這種方式。大家跟隨我們的做法就沒有任何問題。
我們在內核層級進行了一系列的優化,比如說在文件轉換層中對本地鏈接進行了優化。這也正是為什么我們不能給現有的數據庫引擎添加補丁以支持這種功能。有很多本質的修改是無法輕易地作為額外功能添加到現有的數據庫引擎當中的。
String & Int 優化(14:18)
另一點就是我們對這些數據進行了優化。我們可以執行諸如轉換之類的操作,比如說,你有一個供用戶選擇國家的下拉列表,然后你想要展示國家的名稱。在我們的這個例子中,我們有丹麥、美國、加拿大、澳大利亞等等。你可能會有一個國家名稱的巨型列表,里面包含了上百個國家,但是如果你的數據庫中只會有幾千個實體的話,那么你就會遇到字符串的大量重復。我們所能做的就是遍歷你得字符串,然后將其轉變為枚舉,因此它們現在就像 Objective-C 中的標記指針(tagged pointer)哪樣,提供快速查找的功能。
整數盡可能被包裝成 int 類型以減少空間占用,這也就是為什么在 Realm 中,在模型中指定不同長度的整數都是沒有任何問題的。Realm 會盡可能以最優化的方式在內部將整數存儲為 int 型,因為其實現方式因此這種做法是幾乎沒有任何性能開銷的。
崩潰保護(16:00)
崩潰保護(Crash Safety)是一個非常重要的內容。如果你的數據需求很小,比如說僅僅只是序列化一個二進制屬性列表、JSON 文件或者其他類似的數據,但是有一個很嚴重的問題就是,當你在執行寫操作的時候如果手機恰好中途沒電關機了,再次打開你就會發現文件被完全損壞了。
Realm 重新思考了數據存儲的方式,因此它可以很好的規避某些操作系統的 BUG 以及某些未預見的崩潰。在這種情況下能夠切實保護你的用戶數據。正如我所說,Realm 和大型“B-樹”的結構很類似,并且在任何時候你的提交操作都是第一優先級的(和 Git 的HEAD提交類似)。當你在執行修改的時候,寫時拷貝動作就會啟動,這意味著你建立了一個“B-樹”分支并且不會修改原有分支的數據,因此如果某些錯誤發生,原始數據仍不會被破壞。值得慶幸的是,由于良好的架構設計,頂層指針始終會指向未發生崩潰的樹結構,你的寫入操作是發生在別的地方。當最后你決定提交修改的時候,一旦我們確認數據能夠安全地同步到硬盤當中,我們就會移動指針到這個新的正式版本上來。這就意味著在最壞的情況下,你只會丟失當前正在進行的修改,而不會丟失所有數據。
零拷貝(18:06)
知道大多數 ORM 數據庫是怎么處理數據展示的么?你的數據大多數時間都靜靜地呆在硬盤當中。當你訪問NSManagedObject對象中的某個屬性的時候,Core Data 會將這個請求轉換為一組 SQL 語句,如果還未連接數據庫的話則創建一個數據庫連接,然后將這個 SQL 語句發送給硬盤,執行檢索,從匹配檢索的結果中讀取所有的數據,然后將它們放到內存當中(也就是內存分配)。然而,這時候你需要對其格式進行反序列化 (deserialize),因為硬盤上存儲的格式不能直接在內存中使用,這意味著你需要調整位,以便 CPU 能夠對其進行處理。因此,你就必須將其轉換為語言層級(比如說你要讀取一個字符串,即使你只需要其中一個屬性但是你仍必須加載實體的全部內容,加載完畢之后還需要將其轉換為字符串屬性類型)。最后,將這個對象返回給初始化請求器。這個時候仍有 很多 步驟需要做。
那么 Realm 是怎么做的呢?Realm 跳過了整個拷貝流程。首先,文件始終是內存映射的,無論文件是或否在內存當中,你都能夠訪問文件的任何內容。關于核心文件格式的重要一點就是,我們確保硬盤上的文件格式都是內存可讀的,這樣就無需執行任何反序列化操作了。看見沒有,我們跳過了一整個步驟。你所需做的就是計算在文件中的的偏移量,以便能夠在內存映射的內存中讀取數據,通過讀取偏移量以及數據長度之間的數據,接下來就可以通過屬性訪問獲取到原始值了。我們跳過了大部分步驟,事情更加高效。
真實的懶加載(20:33)
根據硬盤和固態硬盤的構建方式,只讀取一位數據是完全不可能實現的。因此如果你只打算讀取某個對象的布爾屬性,那么你就要加載一個硬盤頁大小的數據。你不能讀取比此更小的數據,因為硬盤訪問不會給你機會那么做的。大多數數據庫趨向于在水平層級存儲數據,這也就是為什么你從 SQLite 讀取一個屬性的時候,你就必須要加載整行的數據。它在文件中是連續存儲的。
不同的是,我們盡可能讓 Realm 在垂直層級連續存儲屬性,你也可以看作是按列存儲。這意味著如果你有一系列郵件對象并且打算把它們標記為“未讀”的話,我們不會為其執行一個特殊的操作,而是嘗試優化原始數據,對它們進行遍歷然后將它們的屬性設置為“不可讀”。基于懶加載的工作機制,這項操作將十分高效。當然,這個操作仍然還是會有性能浪費,因為我們必須創建一個語言層級的訪問,雖然有很多方法可以解決這個問題,但是我們仍試圖去優化原始數據。在大多數情況下,你真的無需去對數據進行國度優化,它避免了磁盤往返讀取以及讀取未使用過的屬性。
內部加密(22:10)
現有的數據庫解決方案很難做到的一點就是內部加密。雖然你可以使用 SQLCipher,但是它只是掛接到底層引擎,并且完全重做了很多引擎本身所做的事情。借助 Realm,我們可以輕松地進行加密,因為我們可以輕松地決定數據庫內核所應該做的事情。內部加密和通常在 Linux 當中做的加密哪樣很類似。因為我們對整個文件建立了內存映射,因此我們可以對這部分內存進行保護。如果任何人打算讀取這個加密的模塊,我們就會拋出一個文件系統警告“有人正視圖訪問加密數據。只有解密此模塊才能夠讓用戶讀取。”通過非常安全的技術我們有一個很高效的方式來實現加密。加密并不是在產品表面進行的一層封裝,而是在內部就構建好的一項功能。
支持多進程訪問(23:30)
隨著 iOS 應用擴展的發布,支持多個不同進程間進行并發訪問變得刻不容緩。試想你有一個鍵盤,這個鍵盤要訪問一個字典,你需要能夠執行檢索并且當隨著鍵盤一起出現的應用的同時向字典中寫入數據。
因此你需要多進程訪問。MVCC讓事情變得更簡單,因為它擁有結尾附加(append-only)、寫時拷貝技術。你的寫入事務會一直在新的、單獨的狀態中進行。通過數據庫底層的構建方式,我們就能夠輕易地執行并發操作,我們在頂層所需要做的就是在事件發生的時候通知其他進程即可。我們為此使用命名管道(named pipe)來執行,開銷大大降低。
空值(24:29)
有一個核心數據庫支持但是我們還沒有完全發布的一點就是“空值”。很久以前我們就有這樣一個 PR 了,我們一直在盡力讓所有的操作都能夠正常運行,以確保我們不會意外地破壞任何數據,不過我們的測試還沒有結束。另一個導致這個功能開發持續這么長的原因就是:我們必須確保不會完全毀滅你的工程,因為我們正試圖給大家帶來一個全新的功能。
class Conference: Object { dynamic var name: String? = nil }
這將是空值理想中的樣子。Swift 可選值就是一個很好的例子。在文件系統層級上支持可選值也是非常必要的。目前文件編碼和核心數據庫都支持空值了,隨著我們已有的全部檢索和類型的陸續支持,這項功能很快就可以推出。如果你對此感興趣的話可以查看我們的 PR。
妥協(25:24)
開發一個全新的數據庫引擎并不是沒有妥協。我一直在描述 Realm 的優勢,但是我們也為我們決定開發全新數據庫的決定付出了不少的代價。
-
漫長的開發周期(25:54)—— 如果我們決定在 SQLite 的基礎上開發的話,我們可能早就已經提供了大量的新特性,通過在編譯級別進行操作可以大量的減少我們的工作。但是我們就會止步于此。這就是區別所在。借助完全由我們控制的數據庫核心,我們可以為大家帶來更多的新特性,如果采用 SQLite 之類的成熟產品的話可能就會十分的困難。不過……這意味著大家必須要耐心等待!
-
還未到1.0(26:36)— 我們的 API 一直都在變化。如果你在使用 Swift 的話,你可能早已習慣,每次新的 Xcode 版本推出你都要重寫你的應用。但是這是一個缺點,這意味著你會有一些不向后兼容的更改。自一年前 Realm 推出以來我們已經棄用了不少的 API。不過大多數的通常都是 API 命名之類的級別,我盡量向大家保證,永遠不會出現在內部功能上的大規模變化——不過這也不能百分百保證。
文件格式也可能會發生改變。事實上,它 會 發生一些變化以能夠支持空值。我們必須確保你沒有運行某些在未來10年不會改變文件格式的軟件。這仍是一件有待考慮的事情。
-
功能還是有些少(27:26)— 我們已經在努力達成 Core Data 所擁有的功能,不過還有諸如替代“細粒度通知”的功能還沒有實現。不過這正是我們正在努力的方向。
還有更多(27:40)
最近我們推出了 KVO:現在你可以為每個對象建立更詳細的通知機制。這是Realm Cocoa 0.95 的一部分。當你的 Realm 文件發生修改或者你正在執行請求的時候,你都可以隨時查看相應的變化。空值目前也快要完美收工了。此外還有線程間的切換。這些都是我們正在積極努力改善的東西。
資源及鏈接(28:25)
問答時刻(28:45)
Q: 我知道數據庫實現了MVCC,那么過期的數據是否會消失,還是會一直遺留下來呢?
JP:這些數據可能會被垃圾收集清除掉,基于當前指針所指向的樹,我們可以知道哪些結點是未使用的,然后就可以將它們清除掉。有時你可以強制復制剛遍歷完可用結點的 Realm 文件,然后將數據寫入到一個新的文件當中。這時你可以釋放掉相當大的空間。
Q: 所以這個功能必須自己完成了?
JP:核心系統會自行完成,但是如果你想立刻強制執行,那么你隨時都可以調用此拷貝函數。
Q: 那么云同步功能呢?有沒有什么相關消息?
JP:敬請期待!
Q: 當 Realm 抵達 1.0 版本的時候,你如何平衡不同平臺和語言之間的功能的穩定性,以及新老版本的平穩過渡呢?我想肯定有一個平衡點。
JP:關于功能的廣度和深度總是有一個平衡點存在的。真的,我剛才說的為移動端優化這一點是我們功能設計的前提所在。這就是為什么我們首先專注于主要的移動平臺,并且在此基礎上得到很棒的功能。我們并不會止步于此。我們是如何平衡的呢?好吧,你首先應當關注最大的平臺,然后再慢慢往其他平臺發展。這就是我們的訣竅所在。
See the discussion on Hacker News .
Sign up to be notified of new videos — we won’t email you for any other reason, ever.