如果想要了解存儲,我比較推薦的方式還是從了解數據庫開始。從目前來看,數據庫發展了這么多年,各種理論相對的比較完善,面對各種應用場景,其核心處理模式也已經非常的成熟了,在新的海量數據的時代,人們只是對擴展性提出了更高的要求,而對數據存儲的其他方面卻仍然希望能保持之前的水平。
而從目前實際的發展來看,基本上目前發展的核心思路并沒有繞開人們在數據庫理論領域內所積累的那些關鍵的特性。因此,如果你希望能夠快速的在海量數據的在線處理領域內積累知識,從傳統數據庫領域入手是絕對不會錯的。
下面,就讓我們對數據庫做個簡單的解刨,看看數據庫里面有哪些核心的組件吧。
映射(Map):
首先就需要有能夠存儲數據并提供查詢的結構,這個結構,在java里面就是Map。 C里面也是Map.他的核心作用就是,建立一種key與value的映射關系,當給定某個key的時候,他能夠返回這個key所對應的value給用戶。這是用戶在進行查詢時的主要數據結構。
預寫式日志(write-ahead logging,WAL):
就是個隊列,記錄了你每一次寫的操作。自然而然的,因為你的每次寫操作都被記錄下來了,所以就算計算機斷電了,只要這個日志沒有損壞,計算機重啟后按照這個log ,重放在斷電時的那些寫操作,就可以保證你的數據不丟。
這里,一定會有人問:既然我數據都存儲在k-v表里了,明顯就不會丟失了。為什么還要有這個log呢?這其實就是一個計算機的本質性問題了,別看現代計算機運算速度這么快,他終歸也只是個“圖靈機”實現,或者更具象化一點,就是一臺打字機,一次只能打一個字母,那么可能會有人問了,如果我要用幾個字母來表示同一個意思,應該怎么做呢? 在英語中,最簡單的方式就是在詞組和詞組之間增加空格。 比如write ahead logging. 就是三個由字母組成的單詞。在計算機里,也有類似的問題,用戶的一次寫入操作,可能對應計算機內的多步操作,如何能夠保證這多次的操作要么全部成功,要么全部失敗呢? WAL就是個解決的方法,他利用的是操作系統里的一個原子操作fsync(). 該操作的作用是將一小段數據寫入到磁盤,從而保證數據不會丟失。
我們來看一下整體的操作思路: 記錄用戶的寫入操作(insert,update,delete)-> 進行內部多次key-value映射的構建,包括主數據,輔助索引數據等-> 標記該用戶操作完成。
觸發器(trigger)
一個不難理解的概念,當發生insert , update , delete等操作的時候,可能會有一些需求需要依托這些操作而被觸發執行其他的操作。比如每一行針對表A的更新,都會引發B表內的更新。那么 這個“引發”的過程,就是觸發器。在一些其他的語言里面,這也被叫做callback,IFTTT,Listener等。 但核心概念都一樣,被動的因為某個事件而觸發一段代碼邏輯的運行。
在一些數據庫的實現中,甚至二級索引的更新也是使用觸發器來完成的哦:)
在數據庫內,觸發器全部是同步實現的,也就是說,只有當數據寫入的操作,以及觸發器的操作全部都執行完成后,才會返回用戶執行成功。
鎖(lock)
鎖的主要目標是允許線程圈定一批資源,并規定該資源只允許發出圈定請求的那個線程進行訪問,而其他線程則必須等待。
這個概念產生的主要原因其實還是與計算機是圖靈機有關。。本來計算機就是臺圖靈機,一個時鐘周期內只能打一個字母,但這樣他就很難同時做好幾件事情,比如聽著歌寫代碼,這件事其實從計算機硬件來說是做不到的,他只能模擬,利用時分復用的方式,把cpu的運算分解成小片,每個線程都只占用一小段時間,從而能夠做到同一時間做好幾件事。但是,想一想,如果我們希望一個人A用打字機打i am god. 而希望另外一個人B用同一臺打字機打 pig is money. 開始,時間片分配給A,他打印了i am后,A被cpu換出,B被換入,打印了pig 后被其他人換出, 那么我們自然就發現。。數據就變成了。。。 那么鎖的作用就是保證一個邏輯的原子操作沒有完結的時候,這張打印紙只屬于A,其他人不能對其進行訪問或進行修改。
明白了原理,來簡單看看實現,鎖主要是由排他鎖(寫鎖)和共享鎖(讀鎖)構成,在數據庫的鎖實現中,有很多針對共享鎖和排他鎖相互組合的細節性描述,但其核心的問題卻永遠沒變:
1) 盡可能的減少同一時間內被阻塞的線程數,從而提升并行度。
2) 盡可能的避免死鎖
可以說數據庫實現的是好是壞,關鍵就看著鎖的優化好不好,這在分布式場景或者在單機內都是最重要的一個機制。
執行優化器
這是關系數據庫得名的原因,主要的作用是將關系查詢轉換成key-value 查詢,輸入是sql的抽象語法樹(ast),輸出則是執行計劃,就是各位在數據庫命令行打explain sql時候出來的那些東西。
理解上很簡單,但實際上實現起來卻是最為復雜的,在上個世紀,大部分的執行優化器使用rule based optimizer,也就是基于規則的優化,但在現代數據庫實現中,大部分的優化器都采取了cost based optimizer了,他們之間最大的不同,就是cbo更多的考慮了數據實際的區分度情況,從而能更簡單準確的從。多個可選的索引中選擇一個正確的索引。
sql解析器
作用很簡單,把用戶輸入的sql轉化為計算機可以理解的抽象語法樹(不懂就去看編譯原理:)
好了,基本組件兒介紹完畢,下面我們利用這些核心組件來嘗試拼裝一些外圍的概念。
第一個概念是:存儲過程。
我第一次接觸數據庫的時候,對存儲過程比較不理解。認為數據庫么,使用關系模型就足夠了啊,為什么還要支持一種類似編程語言的東西來額外的增加系統的復雜度呢?而且在當時,有大量的高級程序員在介紹他們的經驗的時候都會分享說:盡可能不使用存儲過程,那玩意兒非常不容易維護,也會增加非常多的使用成本,應該把所有業務邏輯放在客戶端。那么我自然就有個疑問,既然這些事情客戶端都能做,那么還要存儲過程干什么?可能第一次接觸數據庫的人也會有我之前的困惑吧。。。呵呵,所以既然我已經能解答這個問題,在這里自然而然的也要嘗試給有相同問題的人解惑。
存儲過程其實不是個復雜的概念,他的核心目標就是讓數據庫端能夠運行邏輯代碼(判斷,循環..etc),甚至在oracle,存儲過程可以做任何事。 我們排除oracle希望用戶只用數據庫來完成一切功能的陰謀論,來看看事情的本源是什么?或者說,有什么事情是存儲過程能做,而其他方式做不了的?
很簡單,也有很多人提到過,就是性能好。 那么,為什么會性能好呢?
這與我們目前的軟件結構有關系,在當前,大部分情況下,數據庫是一臺獨立的機器,而應用服務器則是另外一臺獨立的機器,那么,相互獨立的機器之間要進行交互操作,勢必需要使用網絡來進行通信。
網絡通信的代價比使用內存指針變更的代價大非常多,這就導致了一個直接的問題,如果使用網絡進行多次交互,那么延遲會遠遠地大于使用內存來進行消息交互。延遲變大,意味著鎖持有時間變長,也就意味著單位時間內針對同一個數據的操作頻率下降,TPS就會下降。
這才是存儲過程之所以能夠提升性能的關鍵。 它不是惡魔,但也不是天使,能不能發揮出特定的優勢,要看具體的業務場景需要。
我們做個簡單的總結:
存儲過程的好處,就是可以減少網絡交互開銷,可以用來封裝一些需要高性能的小的業務邏輯單元。
存儲過程的壞處,就是綁定到特定數據庫上,同時,因為大部分存儲過程是面向過程的代碼,所以運維難度相對較大,不適于處理復雜業務邏輯。
第二個概念是:視圖
視圖這個概念也是我開始看數據庫時候很暈的一個概念,在任何一個數據庫內,數據庫的說明文檔中都會給出特別多中視圖的實現,看起來就特別容易暈。經常有的困惑是: 為什么視圖不能寫數據? 以及,join本身也挺方便的的,我為什么還需要視圖?
這里,為了解答這個問題,我們就需要來看看一種最常見的計算機優化方法: 將不確定性變成確定性。
很多情況下,如果你能提前預知不確定性的范圍,往往就能大范圍的減少鎖的范圍,或者將計算量進行分解。
視圖,從一定程度上也是利用將不確定性變成確定性的方式,來實現join查詢速度的優化和聚焦。
如果計算機不知道你預先需要把哪些表進行join操作,他能做的就只有使用最悲觀的方式來對用戶的行為進行假定,也就是最壞情況下,所有表都可能產生關聯關系,并且關聯的次數和頻率都是均等的。那么針對這種場景,最安全的策略就是不緩存任何join的中間結果,而只使用通用的join算法進行join計算。
但是,如果用戶通過自己的實際業務場景,發現其實有兩個表是固定的被join在一起而進行查詢的。 這種情況就符合了”將不確定性變成確定性“ 這個優化的前提,因此就可以進行一些優化,view從某種程度上來說,就是告知數據庫這種確定性的一種手段。
數據庫在獲知這種hint后,就可以使用一些新的,空間換時間的方式,來預先進行一些操作,從而降低在join查詢計算發生時所消耗的計算量。從而提升查詢性能,降低系統開銷。
ok,本篇到這,本篇主要是介紹了數據庫的一些關鍵的概念,在下一篇,我將使用一些實際查詢的例子,來幫助大家更易于理解在實際數據庫中,上面的這些核心概念是如何被應用的。