1. 程式人生 > >資料庫的原理,一篇文章搞定(三)

資料庫的原理,一篇文章搞定(三)

合併聯接

合併聯接是唯一產生排序的聯接演算法。

注:這個簡化的合併聯接不區分內表或外表;兩個表扮演同樣的角色。但是真實的實現方式是不同的,比如當處理重複值時。

1.(可選)排序聯接運算:兩個輸入源都按照聯接關鍵字排序。

2.合併聯接運算:排序後的輸入源合併到一起。

排序

我們已經談到過合併排序,在這裡合併排序是個很好的演算法(但是並非最好的,如果記憶體足夠用的話,還是雜湊聯接更好)。

然而有時資料集已經排序了,比如:

如果表內部就是有序的,比如聯接條件裡一個索引組織表 【譯者注: index-organized table 】
如果關係是聯接條件裡的一個索引
如果聯接應用在一個查詢中已經排序的中間結果

合併聯接

screenshot

這部分與我們研究過的合併排序中的合併運算非常相似。不過這一次呢,我們不是從兩個關係裡挑選所有元素,而是隻挑選相同的元素。道理如下:

1) 在兩個關係中,比較當前元素(當前=頭一次出現的第一個)
2) 如果相同,就把兩個元素都放入結果,再比較兩個關係裡的下一個元素
3) 如果不同,就去帶有最小元素的關係裡找下一個元素(因為下一個元素可能會匹配)
4) 重複 1、2、3步驟直到其中一個關係的最後一個元素。
因為兩個關係都是已排序的,你不需要『回頭去找』,所以這個方法是有效的。

該演算法是個簡化版,因為它沒有處理兩個序列中相同資料出現多次的情況(即多重匹配)。真實版本『僅僅』針對本例就更加複雜,所以我才選擇簡化版。

如果兩個關係都已經排序,時間複雜度是 O(N+M)

如果兩個關係需要排序,時間複雜度是對兩個關係排序的成本:O(N*Log(N) + M*Log(M))

對於計算機極客,我給出下面這個可能的演算法來處理多重匹配(注:對於這個演算法我不保證100%正確):

mergeJoin(relation a, relation b)
  relation output
  integer a_key:=0;
  integer  b_key:=0;

  while (a[a_key]!=null and b[b_key]!=null)
     if (a[a_key] < b[b_key])
      a_key++;
else if (a[a_key] > b[b_key]) b_key++; else //Join predicate satisfied write_result_in_output(a[a_key],b[b_key]) //We need to be careful when we increase the pointers if (a[a_key+1] != b[b_key]) b_key++; end if if (b[b_key+1] != a[a_key]) a_key++; end if if (b[b_key+1] == a[a_key] && b[b_key] == a[a_key+1]) b_key++; a_key++; end if end if end while

哪個演算法最好?

如果有最好的,就沒必要弄那麼多種型別了。這個問題很難,因為很多因素都要考慮,比如:

  • 空閒記憶體:沒有足夠的記憶體的話就跟強大的雜湊聯接拜拜吧(至少是完全記憶體中雜湊聯接)。
  • 兩個資料集的大小。比如,如果一個大表聯接一個很小的表,那麼巢狀迴圈聯接就比雜湊聯接快,因為後者有建立雜湊的高昂成本;如果兩個表都非常大,那麼巢狀迴圈聯接CPU成本就很高昂。
  • 是否有索引:有兩個 B+樹索引的話,聰明的選擇似乎是合併聯接。
  • 結果是否需要排序:即使你用到的是未排序的資料集,你也可能想用成本較高的合併聯接(帶排序的),因為最終得到排序的結果後,你可以把它和另一個合併聯接串起來(或者也許因為查詢用 ORDER BY/GROUP BY/DISTINCT 等操作符隱式或顯式地要求一個排序結果)。
  • 關係是否已經排序:這時候合併聯接是最好的候選項。
  • 聯接的型別:是等值聯接(比如 tableA.col1 = tableB.col2 )? 還是內聯接?外聯接?笛卡爾乘積?或者自聯接?有些聯接在特定環境下是無法工作的。
  • 資料的分佈:如果聯接條件的資料是傾斜的(比如根據姓氏來聯接人,但是很多人同姓),用雜湊聯接將是個災難,原因是雜湊函式將產生分佈極不均勻的雜湊桶。
  • 如果你希望聯接操作使用多執行緒或多程序。

想要更詳細的資訊,可以閱讀DB2, ORACLE 或 SQL Server)的文件。

簡化的例子

我們已經研究了 3 種類型的聯接操作。

現在,比如說我們要聯接 5 個表,來獲得一個人的全部資訊。一個人可以有:

多個手機號(MOBILES)
多個郵箱(MAILS)
多個地址(ADRESSES)
多個銀行賬號(BANK_ACCOUNTS)
換句話說,我們需要用下面的查詢快速得到答案:


SELECT * from PERSON, MOBILES, MAILS,ADRESSES, BANK_ACCOUNTS
WHERE
PERSON.PERSON_ID = MOBILES.PERSON_ID
AND PERSON.PERSON_ID = MAILS.PERSON_ID
AND PERSON.PERSON_ID = ADRESSES.PERSON_ID
AND PERSON.PERSON_ID = BANK_ACCOUNTS.PERSON_ID

作為一個查詢優化器,我必須找到處理資料最好的方法。但有 2 個問題:

每個聯接使用那種型別?
我有 3 種可選(雜湊、合併、巢狀),同時可能用到 0, 1 或 2 個索引(不必說還有多種型別的索引)。
按什麼順序執行聯接?
比如,下圖顯示了針對 4 個表僅僅 3 次聯接,可能採用的執行計劃:
screenshot

那麼下面就是我可能採取的方法:

1) 採取粗暴的方式
用資料庫統計,計算每種可能的執行計劃的成本,保留最佳方案。但是,會有很多可能性。對於一個給定順序的聯接操作,每個聯接有三種可能性:雜湊、合併、巢狀,那麼總共就有 3^4 種可能性。確定聯接的順序是個二叉樹的排列問題,會有 (2*4)!/(4+1)! 種可能的順序。對本例這個相當簡化了的問題,我最後會得到 3^4*(2*4)!/(4+1)! 種可能。
拋開專業術語,那相當於 27,216 種可能性。如果給合併聯接加上使用 0,1 或 2 個 B+樹索引,可能性就變成了 210,000種。我是不是告訴過你這個查詢其實非常簡單嗎?

2) 我大叫一聲辭了這份工作
很有誘惑力,但是這樣一來,你不會的到查詢結果,而我需要錢來付賬單。

3) 我只嘗試幾種執行計劃,挑一個成本最低的。
由於不是超人,我不能算出所有計劃的成本。相反,我可以武斷地從全部可能的計劃中選擇一個子集,計算它們的成本,把最佳的計劃給你。

4) 我用聰明的規則來降低可能性的數量
有兩種規則:
我可以用『邏輯』規則,它能去除無用的可能性,但是無法過濾大量的可能性。比如: 『巢狀聯接的內關係必須是最小的資料集』。
我接受現實,不去找最佳方案,用更激進的規則來大大降低可能性的數量。比如:『如果一個關係很小,使用巢狀迴圈聯接,絕不使用合併或雜湊聯接。』
在這個簡單的例子中,我最後得到很多可能性。但現實世界的查詢還會有其他關係運算符,像 OUTER JOIN, CROSS JOIN, GROUP BY, ORDER BY, PROJECTION, UNION, INTERSECT, DISTINCT … 這意味著更多的可能性。

那麼,資料庫是如何處理的呢?

動態程式設計,貪婪演算法和啟發式演算法

關係型資料庫會嘗試我剛剛提到的多種方法,優化器真正的工作是在有限時間裡找到一個好的解決方案。

多數時候,優化器找到的不是最佳的方案,而是一個『不錯』的

對於小規模的查詢,採取粗暴的方式是有可能的。但是為了讓中等規模的查詢也能採取粗暴的方式,我們有辦法避免不必要的計算,這就是動態程式設計。

動態程式設計

這幾個字背後的理念是,很多執行計劃是非常相似的。看看下圖這幾種計劃:
screenshot

它們都有相同的子樹(A JOIN B),所以,不必在每個計劃中計算這個子樹的成本,計算一次,儲存結果,當再遇到這個子樹時重用。用更正規的說法,我們面對的是個重疊問題。為了避免對部分結果的重複計算,我們使用記憶法。

對於計算機極客,下面是我在先前給你的教程裡找到的一個演算法。我不提供解釋,所以僅在你已經瞭解動態程式設計或者精通演算法的情況下閱讀(我提醒過你哦):

procedure findbestplan(S)
if (bestplan[S].cost infinite)
   return bestplan[S]
// else bestplan[S] has not been computed earlier, compute it now
if (S contains only 1 relation)
         set bestplan[S].plan and bestplan[S].cost based on the best way
         of accessing S  /* Using selections on S and indices on S */
     else for each non-empty subset S1 of S such that S1 != S
   P1= findbestplan(S1)
   P2= findbestplan(S - S1)
   A = best algorithm for joining results of P1 and P2
   cost = P1.cost + P2.cost + cost of A
   if cost < bestplan[S].cost
       bestplan[S].cost = cost
      bestplan[S].plan = 『execute P1.plan; execute P2.plan;
                 join results of P1 and P2 using A』
return bestplan[S]

針對大規模查詢,你也可以用動態程式設計方法,但是要附加額外的規則(或者稱為啟發式演算法)來減少可能性。

如果我們僅分析一個特定型別的計劃(例如左深樹 left-deep tree,參考),我們得到 n*2^n 而不是 3^n。
screenshot

如果我們加上邏輯規則來避免一些模式的計劃(像『如果一個表有針對指定謂詞的索引,就不要對錶嘗試合併聯接,要對索引』),就會在不給最佳方案造成過多傷害的前提下,減少可能性的數量。【譯者注:原文應該是有兩處筆誤: as=has, to=too】
如果我們在流程裡增加規則(像『聯接運算先於其他所有的關係運算』),也能減少大量的可能性。
……

貪婪演算法

但是,優化器面對一個非常大的查詢,或者為了儘快找到答案(然而查詢速度就快不起來了),會應用另一種演算法,叫貪婪演算法。

原理是按照一個規則(或啟發)以漸進的方式制定查詢計劃。在這個規則下,貪婪演算法逐步尋找最佳演算法,先處理一條JOIN,接著每一步按照同樣規則加一條新的JOIN。

我們來看個簡單的例子。比如一個針對5張表(A,B,C,D,E)4次JOIN 的查詢,為了簡化我們把巢狀JOIN作為可能的聯接方式,按照『使用最低成本的聯接』規則。

直接從 5 個表裡選一個開始(比如 A)
計算每一個與 A 的聯接(A 作為內關係或外關係)
發現 “A JOIN B” 成本最低
計算每一個與 “A JOIN B” 的結果聯接的成本(“A JOIN B” 作為內關係或外關係)
發現 “(A JOIN B) JOIN C” 成本最低
計算每一個與 “(A JOIN B) JOIN C” 的結果聯接的成本 ……
最後確定執行計劃 “( ( (A JOIN B) JOIN C) JOIN D ) JOIN E )”
因為我們是武斷地從表 A 開始,我們可以把同樣的演算法用在 B,然後 C,然後 D, 然後 E。最後保留成本最低的執行計劃。

順便說一句,這個演算法有個名字,叫『最近鄰居演算法』。

拋開細節不談,只需一個良好的模型和一個 N*log(N) 複雜度的排序,問題就輕鬆解決了。這個演算法的複雜度是 O(N*log(N)) ,對比一下完全動態程式設計的 O(3^N)。如果你有個20個聯接的大型查詢,這意味著 26 vs 3,486,784,401 ,天壤之別!

這個演算法的問題是,我們做的假設是:找到 2 個表的最佳聯接方法,保留這個聯接結果,再聯接下一個表,就能得到最低的成本。但是:

即使在 A, B, C 之間,A JOIN B 可得最低成本
(A JOIN C) JOIN B 也許比 (A JOIN B) JOIN C 更好。
為了改善這一狀況,你可以多次使用基於不同規則的貪婪演算法,並保留最佳的執行計劃。

其他演算法

[ 如果你已經受夠了演算法話題,就直接跳到下一部分。這部分對文章餘下的內容不重要。] 【譯者注:我也很想把這段跳過去 -_- 】

很多電腦科學研究者熱衷於尋找最佳的執行計劃,他們經常為特定問題或模式探尋更好的解決方案,比如:

如果查詢是星型聯接(一種多聯接查詢),某些資料庫使用一種特定的演算法。
如果查詢是並行的,某些資料庫使用一種特定的演算法。 ……
其他演算法也在研究之中,就是為了替換在大型查詢中的動態程式設計演算法。貪婪演算法屬於一個叫做啟發式演算法的大家族,它根據一條規則(或啟發),儲存上一步找到的方法,『附加』到當前步驟來進一步搜尋解決方法。有些演算法根據特定規則,一步步的應用規則但不總是保留上一步找到的最佳方法。它們統稱啟發式演算法。

比如,基因演算法就是一種:

一個方法代表一種可能的完整查詢計劃
每一步保留了 P 個方法(即計劃),而不是一個。
0) P 個計劃隨機建立
1) 成本最低的計劃才會保留
2) 這些最佳計劃混合在一起產生 P 個新的計劃
3) 一些新的計劃被隨機改寫
4) 1,2,3步重複 T 次
5) 然後在最後一次迴圈,從 P 個計劃裡得到最佳計劃。
迴圈次數越多,計劃就越好。

這是魔術?不,這是自然法則:適者生存!

PostgreSQL 實現了基因演算法,但我並沒有發現它是不是預設使用這種演算法的。

資料庫中還使用了其它啟發式演算法,像『模擬退火演算法(Simulated Annealing)』、『互動式改良演算法(Iterative Improvement)』、『雙階段優化演算法(Two-Phase Optimization)』…..不過,我不知道這些演算法當前是否在企業級資料庫應用了,還是僅僅用在研究型資料庫。

如果想進一步瞭解,這篇研究文章介紹兩個更多可能的演算法《資料庫查詢優化中聯接排序問題的演算法綜述》,你可以去閱讀一下。

真實的優化器

[ 這段不重要,可以跳過 ]

然而,所有上述羅裡羅嗦的都非常理論化,我是個開發者而不是研究者,我喜歡具體的例子。

我們來看看 SQLite 優化器 是怎麼工作的。這是個輕量化資料庫,它使用一種簡單優化器,基於帶有附加規則的貪婪演算法,來限制可能性的數量。

SQLite 在有 CROSS JOIN 操作符時從不給表重新排序
使用巢狀聯接
外聯接始終按順序評估
……
3.8.0之前的版本使用『最近鄰居』貪婪演算法來搜尋最佳查詢計劃
等等……我們見過這個演算法!真是巧哈!
從3.8.0版本(釋出於2015年)開始,SQLite使用『N最近鄰居』貪婪演算法來搜尋最佳查詢計劃
我們再看看另一個優化器是怎麼工作的。IBM DB2 跟所有企業級資料庫都類似,我討論它是因為在切換到大資料之前,它是我最後真正使用的資料庫。

看過官方文件後,我們瞭解到 DB2 優化器可以讓你使用 7 種級別的優化:

對聯接使用貪婪演算法
0 – 最小優化,使用索引掃描和巢狀迴圈聯接,避免一些查詢重寫
1 – 低階優化
2 – 完全優化
對聯接使用動態程式設計演算法
3 – 中等優化和粗略的近似法
5 – 完全優化,使用帶有啟發式的所有技術
7 – 完全優化,類似級別5,但不用啟發式
9 – 最大優化,完全不顧開銷,考慮所有可能的聯接順序,包括笛卡爾乘積
可以看到 DB2 使用貪婪演算法和動態程式設計演算法。當然,他們不會把自己的啟發演算法分享出來的,因為查詢優化器是資料庫的看家本領。

DB2 的預設級別是 5,優化器使用下列特性: 【譯者注:以下出現的一些概念我沒有做考證,因為[ 這段不重要,可以跳過 ]】

使用所有可用的統計,包括線段樹(frequent-value)和分位數統計(quantile statistics)。
使用所有查詢重寫規則(含物化查詢表路由,materialized query table routing),除了在極少情況下適用的計算密集型規則。
使用動態程式設計模擬聯接
有限使用組合內關係(composite inner relation)
對於涉及查詢表的星型模式,有限使用笛卡爾乘積
考慮寬泛的訪問方式,含列表預取(list prefetch,注:我們將討論什麼是列表預取),index ANDing(注:一種對索引的特殊操作),和物化查詢表路由。
預設的,DB2 對聯接排列使用受啟發式限制的動態程式設計演算法。

其它情況 (GROUP BY, DISTINCT…) 由簡單規則處理。

查詢計劃快取

由於建立查詢計劃是耗時的,大多資料庫把計劃儲存在查詢計劃快取,來避免重複計算。這個話題比較大,因為資料庫需要知道什麼時候更新過時的計劃。辦法是設定一個上限,如果一個表的統計變化超過了上限,關於該表的查詢計劃就從快取中清除。

查詢執行器

在這個階段,我們有了一個優化的執行計劃,再編譯為可執行程式碼。然後,如果有足夠資源(記憶體,CPU),查詢執行器就會執行它。計劃中的操作符 (JOIN, SORT BY …) 可以順序或並行執行,這取決於執行器。為了獲得和寫入資料,查詢執行器與資料管理器互動,本文下一部分來討論資料管理器。

資料管理器

screenshot

在這一步,查詢管理器執行了查詢,需要從表和索引獲取資料,於是向資料管理器提出請求。但是有 2 個問題:

關係型資料庫使用事務模型,所以,當其他人在同一時刻使用或修改資料時,你無法得到這部分資料。
資料提取是資料庫中速度最慢的操作,所以資料管理器需要足夠聰明地獲得資料並儲存在記憶體緩衝區內。
在這一部分,我沒看看關係型資料庫是如何處理這兩個問題的。我不會講資料管理器是怎麼獲得資料的,因為這不是最重要的(而且本文已經夠長的了!)。

快取管理器

screenshot
我已經說過,資料庫的主要瓶頸是磁碟 I/O。為了提高效能,現代資料庫使用快取管理器。

查詢執行器不會直接從檔案系統拿資料,而是向快取管理器要。快取管理器有一個記憶體快取區,叫做緩衝池,從記憶體讀取資料顯著地提升資料庫效能。對此很難給出一個數量級,因為這取決於你需要的是哪種操作:

  • 順序訪問(比如:全掃描) vs 隨機訪問(比如:按照row id訪問)
  • 讀還是寫

以及資料庫使用的磁碟型別:

  • 7.2k/10k/15k rpm的硬碟
  • SSD
  • RAID 1/5/…

要我說,記憶體比磁碟要快100到10萬倍。

然而,這導致了另一個問題(資料庫總是這樣…),快取管理器需要在查詢執行器使用資料之前得到資料,否則查詢管理器不得不等待資料從緩慢的磁碟中讀出來。

預讀

這個問題叫預讀。查詢執行器知道它將需要什麼資料,因為它瞭解整個查詢流,而且通過統計也瞭解磁碟上的資料。道理是這樣的:

當查詢執行器處理它的第一批資料時
會告訴快取管理器預先裝載第二批資料
當開始處理第二批資料時
告訴快取管理器預先裝載第三批資料,並且告訴快取管理器第一批可以從快取裡清掉了。
……
快取管理器在緩衝池裡儲存所有的這些資料。為了確定一條資料是否有用,快取管理器給快取的資料添加了額外的資訊(叫閂鎖)。

有時查詢執行器不知道它需要什麼資料,有的資料庫也不提供這個功能。相反,它們使用一種推測預讀法(比如:如果查詢執行器想要資料1、3、5,它不久後很可能會要 7、9、11),或者順序預讀法(這時候快取管理器只是讀取一批資料後簡單地從磁碟載入下一批連續資料)。

為了監控預讀的工作狀況,現代資料庫引入了一個度量叫緩衝/快取命中率,用來顯示請求的資料在快取中找到而不是從磁碟讀取的頻率。

注:糟糕的快取命中率不總是意味著快取工作狀態不佳。更多資訊請閱讀Oracle文件。

緩衝只是容量有限的記憶體空間,因此,為了載入新的資料,它需要移除一些資料。載入和清除快取需要一些磁碟和網路I/O的成本。如果你有個經常執行的查詢,那麼每次都把查詢結果載入然後清除,效率就太低了。現代資料庫用緩衝區置換策略來解決這個問題。

緩衝區置換策略

多數現代資料庫(至少 SQL Server, MySQL, Oracle 和 DB2)使用 LRU 演算法。

LRU

LRU代表最近最少使用(Least Recently Used)演算法,背後的原理是:在快取裡保留的資料是最近使用的,所以更有可能再次使用。

圖解:
screenshot

為了更好的理解,我假設緩衝區裡的資料沒有被閂鎖鎖住(就是說是可以被移除的)。在這個簡單的例子裡,緩衝區可以儲存 3 個元素:

1:快取管理器(簡稱CM)使用資料1,把它放入空的緩衝區
2:CM使用資料4,把它放入半載的緩衝區
3:CM使用資料3,把它放入半載的緩衝區
4:CM使用資料9,緩衝區滿了,所以資料1被清除,因為它是最後一個最近使用的,資料9加入到緩衝區
5:CM使用資料4,資料4已經在緩衝區了,所以它再次成為第一個最近使用的。
6:CM使用資料1,緩衝區滿了,所以資料9被清除,因為它是最後一個最近使用的,資料1加入到緩衝區
……
這個演算法效果很好,但是有些限制。如果對一個大表執行全表掃描怎麼辦?換句話說,當表/索引的大小超出緩衝區會發生什麼?使用這個演算法會清除之前快取內所有的資料,而且全掃描的資料很可能只使用一次。

改進

為了防止這個現象,有些資料庫增加了特殊的規則,比如Oracle文件中的描述:

『對非常大的表來說,資料庫通常使用直接路徑來讀取,即直接載入區塊[……],來避免填滿緩衝區。對於中等大小的表,資料庫可以使用直接讀取或快取讀取。如果選擇快取讀取,資料庫把區塊置於LRU的尾部,防止清空當前緩衝區。』
還有一些可能,比如使用高階版本的LRU,叫做 LRU-K。例如,SQL Server 使用 LRU-2。

這個演算法的原理是把更多的歷史記錄考慮進來。簡單LRU(也就是 LRU-1),只考慮最後一次使用的資料。LRU-K呢:

考慮資料最後第K次使用的情況
資料使用的次數加進了權重
一批新資料載入進入快取,舊的但是經常使用的資料不會被清除(因為權重更高)
但是這個演算法不會保留快取中不再使用的資料
所以資料如果不再使用,權重值隨著時間推移而降低
計算權重是需要成本的,所以SQL Server只是使用 K=2,這個值效能不錯而且額外開銷可以接受。

關於LRU-K更深入的知識,可以閱讀早期的研究論文(1993):資料庫磁碟緩衝的LRU-K頁面置換演算法

其他演算法

當然還有其他管理快取的演算法,比如:

2Q(類LRU-K演算法)
CLOCK(類LRU-K演算法)
MRU(最新使用的演算法,用LRU同樣的邏輯但不同的規則)
LRFU(Least Recently and Frequently Used,最近最少使用最近最不常用)
……

寫緩衝區

我只探討了讀快取 —— 在使用之前預先載入資料。用來儲存資料、成批刷入磁碟,而不是逐條寫入資料從而造成很多單次磁碟訪問。

要記住,緩衝區儲存的是頁(最小的資料單位)而不是行(邏輯上/人類習慣的觀察資料的方式)。緩衝池內的頁如果被修改了但還沒有寫入磁碟,就是髒頁。有很多演算法來決定寫入髒頁的最佳時機,但這個問題與事務的概念高度關聯,下面我們就談談事務。

事務管理器

最後但同樣重要的,是事務管理器,我們將看到這個程序是如何保證每個查詢在自己的事務內執行的。但開始之前,我們需要理解ACID事務的概念。

“I’m on acid”

一個ACID事務是一個工作單元,它要保證4個屬性:

  • 原子性(Atomicity): 事務『要麼全部完成,要麼全部取消』,即使它持續執行10個小時。如果事務崩潰,狀態回到事務之前(事務回滾)。
  • 隔離性(Isolation): 如果2個事務 A 和 B 同時執行,事務 A 和 B 最終的結果是相同的,不管 A 是結束於 B 之前/之後/執行期間。
  • 永續性(Durability): 一旦事務提交(也就是成功執行),不管發生什麼(崩潰或者出錯),資料要儲存在資料庫中。
  • 一致性(Consistency): 只有合法的資料(依照關係約束和函式約束)能寫入資料庫,一致性與原子性和隔離性有關。

在同一個事務內,你可以執行多個SQL查詢來讀取、建立、更新和刪除資料。當兩個事務使用相同的資料,麻煩就來了。經典的例子是從賬戶A到賬戶B的匯款。假設有2個事務:

  • 事務1(T1)從賬戶A取出100美元給賬戶B
  • 事務2(T2)從賬戶A取出50美元給賬戶B

我們回來看看ACID屬性:

原子性確保不管 T1 期間發生什麼(伺服器崩潰、網路中斷…),你不能出現賬戶A 取走了100美元但沒有給賬戶B 的現象(這就是資料不一致狀態)。
隔離性確保如果 T1 和 T2 同時發生,最終A將減少150美元,B將得到150美元,而不是其他結果,比如因為 T2 部分抹除了 T1 的行為,A減少150美元而B只得到50美元(這也是不一致狀態)。
永續性確保如果 T1 剛剛提交,資料庫就發生崩潰,T1 不會消失得無影無蹤。
一致性確保錢不會在系統內生成或滅失。

[以下部分不重要,可以跳過]
現代資料庫不會使用純粹的隔離作為預設模式,因為它會帶來巨大的效能消耗。SQL一般定義4個隔離級別:

  • 序列化(Serializable,SQLite預設模式):最高級別的隔離。兩個同時發生的事務100%隔離,每個事務有自己的『世界』。 -可重複讀(Repeatable read,MySQL預設模式):每個事務有自己的『世界』,除了一種情況。如果一個事務成功執行並且添加了新資料,這些資料對其他正在執行的事務是可見的。但是如果事務成功修改了一條資料,修改結果對正在執行的事務不可見。所以,事務之間只是在新資料方面突破了隔離,對已存在的資料仍舊隔離。 舉個例子,如果事務A執行”SELECT count(1) from TABLE_X” ,然後事務B在 TABLE_X 加入一條新資料並提交,當事務A再執行一次 count(1)結果不會是一樣的。 這叫幻讀(phantom read)。
  • 讀取已提交(Read committed,Oracle、PostgreSQL、SQL Server預設模式):可重複讀+新的隔離突破。如果事務A讀取了資料D,然後資料D被事務B修改(或刪除)並提交,事務A再次讀取資料D時資料的變化(或刪除)是可見的。 這叫不可重複讀(non-repeatable read)。
  • 讀取未提交(Read uncommitted):最低級別的隔離,是讀取已提交+新的隔離突破。如果事務A讀取了資料D,然後資料D被事務B修改(但並未提交,事務B仍在執行中),事務A再次讀取資料D時,資料修改是可見的。如果事務B回滾,那麼事務A第二次讀取的資料D是無意義的,因為那是事務B所做的從未發生的修改(已經回滾了嘛)。 這叫髒讀(dirty read)。

多數資料庫添加了自定義的隔離級別(比如 PostgreSQL、Oracle、SQL Server的快照隔離),而且並沒有實現SQL規範裡的所有級別(尤其是讀取未提交級別)。

預設的隔離級別可以由使用者/開發者在建立連線時覆蓋(只需要增加很簡單的一行程式碼)。

併發控制

確保隔離性、一致性和原子性的真正問題是對相同資料的寫操作(增、更、刪):

如果所有事務只是讀取資料,它們可以同時工作,不會更改另一個事務的行為。
如果(至少)有一個事務在修改其他事務讀取的資料,資料庫需要找個辦法對其它事務隱藏這種修改。而且,它還需要確保這個修改操作不會被另一個看不到這些資料修改的事務擦除。
這個問題叫併發控制。

最簡單的解決辦法是依次執行每個事務(即順序執行),但這樣就完全沒有伸縮性了,在一個多處理器/多核伺服器上只有一個核心在工作,效率很低。

理想的辦法是,每次一個事務建立或取消時:

監控所有事務的所有操作
檢查是否2個(或更多)事務的部分操作因為讀取/修改相同的資料而存在衝突
重新編排衝突事務中的操作來減少衝突的部分
按照一定的順序執行衝突的部分(同時非衝突事務仍然在併發執行)
考慮事務有可能被取消
用更正規的說法,這是對衝突的排程問題。更具體點兒說,這是個非常困難而且CPU開銷很大的優化問題。企業級資料庫無法承擔等待幾個小時,來尋找每個新事務活動最好的排程,因此就使用不那麼理想的方式以避免更多的時間浪費在解決衝突上。

鎖管理器

為了解決這個問題,多數資料庫使用鎖和/或資料版本控制。這是個很大的話題,我會集中探討鎖,和一點點資料版本控制。

悲觀鎖

原理是:

如果一個事務需要一條資料
它就把資料鎖住
如果另一個事務也需要這條資料
它就必須要等第一個事務釋放這條資料
這個鎖叫排他鎖。
但是對一個僅僅讀取資料的事務使用排他鎖非常昂貴,因為這會迫使其它只需要讀取相同資料的事務等待。因此就有了另一種鎖,共享鎖。
screenshot
共享鎖是這樣的:

如果一個事務只需要讀取資料A
它會給資料A加上『共享鎖』並讀取
如果第二個事務也需要僅僅讀取資料A
它會給資料A加上『共享鎖』並讀取
如果第三個事務需要修改資料A
它會給資料A加上『排他鎖』,但是必須等待另外兩個事務釋放它們的共享鎖。
同樣的,如果一塊資料被加上排他鎖,一個只需要讀取該資料的事務必須等待排他鎖釋放才能給該資料加上共享鎖。

鎖管理器是新增和釋放鎖的程序,在內部用一個雜湊表儲存鎖資訊(關鍵字是被鎖的資料),並且瞭解每一塊資料是:

相關推薦

資料庫原理文章

合併聯接 合併聯接是唯一產生排序的聯接演算法。 注:這個簡化的合併聯接不區分內表或外表;兩個表扮演同樣的角色。但是真實的實現方式是不同的,比如當處理重複值時。 1.(可選)排序聯接運算:兩個輸入源都按照聯接關鍵字排序。 2.合併聯接運算:排序後的輸入源合併到一起。

資料庫原理文章

https://blog.csdn.net/zhangcanyan/article/details/51439012 一提到關係型資料庫,我禁不住想:有些東西被忽視了。關係型資料庫無處不在,而且種類繁多,從小巧實用的 SQLite 到強大的 Teradata 。但很少有文章講解資料庫是如何工作的。你可以自己

Python正則表達式很難?文章不是我吹!

編譯 返回 特殊字符 但是 參數 查找字符串 cas 行處理 產生 1. 正則表達式語法 1.1 字符與字符類 1 特殊字符:.^$?+*{}| 以上特殊字符要想使用字面值,必須使用進行轉義 2 字符類 1. 包含在[]中的一個或者多個字符被稱為字符類,字符類在匹

文章Markdown

markdown一篇文章搞定Markdown一篇文章搞定Markdown

文章前端面試

ron miss 就是 節點數 網頁 那是 png html 性能優化 本文旨在用最通俗的語言講述最枯燥的基本知識 面試過前端的老鐵都知道,對於前端,面試官喜歡一開始先問些HTML5新增元素啊特性啊,或者是js閉包啊原型啊,或者是css垂直水平居中怎麽實現啊之類的基礎問題

文章你的spring定時器

Cron表示式是一個字串,字串以5或6個空格隔開,分開工6或7個域,每一個域代表一個含義,Cron有如下兩種語法 格式: Seconds Minutes Hours DayofMonth Month DayofWeek Year 或 Seconds Minutes Hours DayofMo

面試專欄|文章ArrayList和LinkedList所有面試問題

在面試中經常碰到:ArrayList和LinkedList的特點和區別? 個人認為這個問題的回答應該分成這幾部分: 介紹ArrayList底層實現 介紹LinkedList底層實現 兩者個適用於哪些場合 本文也是按照上面這幾部分組織的。 ArrayList的原始碼解析

文章C語言所有的基本語法.

C 作為一門工程實用性極強的語言,提供了對作業系統和記憶體的精準控制,高效能的執行時環境,原始碼級的跨平臺編譯等優點,才是我們必須學習和使用 C 的理由。 C語言標記/令牌 C語言程式包括各種令牌和令牌可以是一個關鍵字,識別符號,常量,字串文字或符號。 例如,下面的C語句包括五個

文章Maven安裝到建立maven版Spring MVC專案及配置

配置maven 本地安裝 新建變數名為MAVEN_HOME,值為maven安裝目錄的系統變數 在系統變數名為Path的值中新增“%MAVEN_HOME%\bin;” cmd命令列輸入mvn -v 檢視是否安裝成功 修改maven配置檔案 apac

文章2017最新標題優化方法

很多人都說小2哥你為什麼一直都在寫直通車的文章?為什麼不寫寫自然搜尋方面的內容?你不知道我們新手更需要的就是這些免費流量的內容嗎? 我從13年開始寫文章,13年和14年我是寫自然搜尋寫的最多的,但是從15年開始的時候,我基本上寫的文章都是圍繞直通車。 其實這也是和我的

文章矩陣相關概念及意義--通俗解釋彙總

一篇文章理解矩陣在講什麼。 最近在學習矩陣相關知識,但是其抽象的解釋讓人摸不著頭腦,通過瀏覽一些部落格的內容和自己的理解,本文通過通俗的語言將矩陣的內涵做了總結。其中除了書本和個人觀點,部分引用部落格:。本文主要幫助大家理解矩陣,但不一定都是正確的,但願能起

文章springMVC中的請求對映

實驗的專案是採用預設配置的spring boot專案,使用的工具為IDEA和POSTMAN。 希望這些案例能夠幫助你理解和思考。 talk is cheap,show me the code! 1、從最簡單的hello world開始 @

文章面試中的連結串列題目(java實現)

連結串列的資料結構 class ListNode { ListNode next; int val; ListNode(int x){ val = x; next = null;

Git | 文章Git、GitHub的理解和使用學習筆記

Git learning note 本筆記整理自廖雪峰老師的Git教程,加上了自己的實踐結果和一些理解,旨在使科研工作者(基本上是獨立開發的那種)看完就能理解和使用Git。廖老師的教程生動活潑,條理清晰,推薦閱讀。還可以贊助哦。 目錄 Git 簡

文章SpringMVC參數綁

進行 pwd dad 技術 默認 int acra servlet key SpringMVC參數綁定,簡單來說就是將客戶端請求的key/value數據綁定到controller方法的形參上,然後就可以在controller中使用該參數了 下面通過5個常用的註解演

文章面試中的二叉樹題目(java實現)

結構 cer dea mat lastcomm ++ mir let balanced 最近總結了一些數據結構和算法相關的題目,這是第一篇文章,關於二叉樹的。 先上二叉樹的數據結構: class TreeNode{ int val; //左孩子 Tr

文章——JDK8中新增的StampedLock

一、StampedLock類簡介 StampedLock類,在JDK1.8時引入,是對讀寫鎖ReentrantReadWrit

文章Python多程序(全)

1.Python多程序模組 Python中的多程序是通過multiprocessing包來實現的,和多執行緒的threading.Thread差不多,它可以利用multiprocessing.Process物件來建立一個程序物件。這個程序物件的方法和執行緒物件的方法差不多也有start(), run(), j

文章 Nginx 反向代理與負載均衡

## 代理      要想弄明白反向代理,首先要知道啥是正向代理,要搞懂正向代理只需要知道啥是代理即可。代理其實就是一箇中介,在不同事物或同一事物內部起到居間聯絡作用的環節。比如買票黃牛,房屋中介等等。   在網際網路中代理更多指的是代理伺服器,代理伺服器位於客戶端和伺服器之間,它充當兩者之間的中介。這

MySQL命令文章替你全部

MySQL的基本操作可以包括兩個方面:MySQL常用語句如高頻率使用的增刪改查(CRUD)語句和MySQL高階功能,如儲存過程,觸發器,事務處理等。而這兩個方面又可以細分如下:MySQL常用語句表(或者資料庫)的CRUD表資料的CRUD,其中表資料查詢使用最多,也更復雜。查詢可以按照單表還是多表可以分為:單表