《Java架構師的第一性原理》41存儲之MySQL第6篇鎖

1 鎖機制

1.1 併發控制

爲啥要進行併發控制?程序員

併發的任務對同一個臨界資源進行操做,若是不採起措施,可能致使不一致,故必須進行併發控制(Concurrency Control)。數據庫

技術上,一般如何進行併發控制?服務器

經過併發控制保證數據一致性的常見手段有:session

  • 鎖(Locking)架構

  • 數據多版本(Multi Versioning)併發

事務之間的隔離,是經過鎖機制實現的。當一個事務須要對數據庫中的某行數據進行修改時,須要先給數據加鎖;加了鎖的數據,其它事務是不運行操做的,只能等待當前事務提交或回滾將鎖釋放。鎖機制並非一個陌生的概念,在許多場景中都會利用到不一樣實現的鎖對數據進行保護和同步。而在MySQL中,根據不一樣的劃分標準,還可將鎖分爲不一樣的種類。高併發

  • 按照粒度劃分:行鎖、表鎖、頁鎖
  • 按照使用方式劃分:共享鎖、排它鎖
  • 按照程序員角度劃分:悲觀鎖、樂觀鎖
  • 按照事務隔離劃分:記錄鎖、間隙鎖、臨鍵鎖

InnoDB內核的第一種鎖:自增鎖(Auto-inc Locks)性能

InnoDB內核的三種鎖:共享/排他鎖(Shared and Exclusive Locks)、意向鎖(Intention Locks)、插入意向鎖(Insert Intention Locks)學習

InnoDB三種細粒度行鎖:記錄鎖(Record Locks)、間隙鎖(Gap Locks)、臨鍵鎖(Next-Key Locks)ui

1.2 按照粒度劃分(行鎖、表鎖、頁鎖)

粒度:指數據倉庫的數據單位中保存數據的細化或綜合程度的級別。細化程度越高,粒度級就越小;相反,細化程度越低,粒度級就越大。

MySQL按照鎖的粒度劃分能夠分爲行鎖、表鎖和頁鎖。

  • 行鎖:粒度最小的鎖,表示只針對當前操做的行進行加鎖;
  • 表鎖:粒度最大的鎖,表示當前的操做對整張表加鎖;
  • 頁鎖:粒度介於行級鎖和表級鎖中間的一種鎖,表示對頁進行加鎖。

 數據庫的粒度劃分

這三種鎖是在不一樣層次上對數據進行鎖定,因爲粒度的不一樣,其帶來的好處和劣勢也不一而同。

表鎖在操做數據時會鎖定整張表,於是併發性能較差;行鎖則只鎖定須要操做的數據,併發性能好。可是因爲加鎖自己須要消耗資源(得到鎖、檢查鎖、釋放鎖等都須要消耗資源),所以在鎖定數據較多狀況下使用表鎖能夠節省大量資源。

MySQL中不一樣的存儲引擎可以支持的鎖也是不同的。MyIsam只支持表鎖,而InnoDB同時支持表鎖和行鎖,且出於性能考慮,絕大多數狀況下使用的都是行鎖。

1.3 按照使用方式劃分(共享鎖、排他鎖)

如何使用普通鎖保證一致性?

普通鎖,被使用最多:

  • 操做數據前,鎖住,實施互斥,不容許其餘的併發任務操做;
  • 操做完成後,釋放鎖,讓其餘任務執行;

如此這般,來保證一致性。

普通鎖存在什麼問題?

簡單的鎖住太過粗暴,連「讀任務」也沒法並行,任務執行過程本質上是串行的。

因而出現了共享鎖排他鎖

  • 共享鎖(Share Locks,記爲S鎖),讀取數據時加S鎖

  • 排他鎖(eXclusive Locks,記爲X鎖),修改數據時加X鎖

共享鎖與排他鎖的玩法是:

  • 共享鎖之間不互斥,簡記爲:讀讀能夠並行

  • 排他鎖與任何鎖互斥,簡記爲:寫讀,寫寫不能夠並行

能夠看到,一旦寫數據的任務沒有完成,數據是不能被其餘任務讀取的,這對併發度有較大的影響。

畫外音:對應到數據庫,能夠理解爲,寫事務沒有提交,讀相關數據的select也會被阻塞。

有沒有可能,進一步提升併發呢?

即便寫任務沒有完成,其餘讀任務也可能併發,這就引出了數據多版本。

-- 表上共享鎖
LOCK TABLE product_comment READ;
UPDATE product_comment SET product_id = 10002 WHERE user_id = 912178;
-- ERROR 1099 (HY000): Table 'product_comment' was locked with a READ lock and can't be updated
UNLOCK TABLE;

-- 行上共享鎖
SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE user_id = 912178 LOCK IN SHARE MODE;

-- 表上排他鎖
LOCK TABLE product_comment WRITE;
UNLOCK TABLE;

-- 行上排他鎖
SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE user_id = 912178 FOR UPDATE;

1.4 按照事務隔離劃分(記錄鎖、間隙鎖、臨建鎖)

InnoDB,select爲啥會阻塞insert?

1)記錄鎖(Record Locks)

記錄鎖,它封鎖索引記錄,例如:

select * from t where id=1 for update;

2)間隙鎖(Gap Locks)

間隙鎖,它封鎖索引記錄中的間隔,或者第一條索引記錄以前的範圍,又或者最後一條索引記錄以後的範圍。

間隙鎖的主要目的,就是爲了防止其餘事務在間隔中插入數據,以致使「不可重複讀」。

若是把事務的隔離級別降級爲讀提交(Read Committed, RC),間隙鎖則會自動失效。

3)臨鍵鎖(Next-Key Locks)

臨鍵鎖,是記錄鎖與間隙鎖的組合,它的封鎖範圍,既包含索引記錄,又包含索引區間。

更具體的,臨鍵鎖會封鎖索引記錄自己,以及索引記錄以前的區間。

若是一個會話佔有了索引記錄R的共享/排他鎖,其餘會話不能馬上在R以前的區間插入新的索引記錄。

If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.

臨鍵鎖的主要目的,也是爲了不幻讀(Phantom Read)。若是把事務的隔離級別降級爲RC,臨鍵鎖則也會失效。

1.5 按照程序員的角度來進行劃分(樂觀鎖、悲觀鎖)

若是從程序員的視角來看鎖的話,能夠將鎖分紅樂觀鎖和悲觀鎖,從名字中也能夠看出這兩種鎖是兩種看待數據併發的思惟方式。

 

樂觀鎖(Optimistic Locking)認爲對同一數據的併發操做不會總髮生,屬於小几率事件,不用每次都對數據上鎖,也就是不採用數據庫自身的鎖機制,而是經過程序來實現。在程序上,咱們能夠採用版本號機制或者時間戳機制實現。

樂觀鎖的版本號機制

在表中設計一個版本字段 version,第一次讀的時候,會獲取 version 字段的取值。而後對數據進行更新或刪除操做時,會執行UPDATE ... SET version=version+1 WHERE version=version。此時若是已經有事務對這條數據進行了更改,修改就不會成功。

這種方式相似咱們熟悉的 SVN、CVS 版本管理系統,當咱們修改了代碼進行提交時,首先會檢查當前版本號與服務器上的版本號是否一致,若是一致就能夠直接提交,若是不一致就須要更新服務器上的最新代碼,而後再進行提交。

樂觀鎖的時間戳機制

時間戳和版本號機制同樣,也是在更新提交的時候,將當前數據的時間戳和更新以前取得的時間戳進行比較,若是二者一致則更新成功,不然就是版本衝突。

你能看到樂觀鎖就是程序員本身控制數據併發操做的權限,基本是經過給數據行增長一個戳(版本號或者時間戳),從而證實當前拿到的數據是否最新。

 

悲觀鎖(Pessimistic Locking)也是一種思想,對數據被其餘事務的修改持保守態度,會經過數據庫自身的鎖機制來實現,從而保證數據操做的排它性。

咱們都不但願出現死鎖的狀況,能夠採起一些方法避免死鎖的發生:

  1. 若是事務涉及多個表,操做比較複雜,那麼能夠儘可能一次鎖定全部的資源,而不是逐步來獲取,這樣能夠減小死鎖發生的機率;
  2. 若是事務須要更新數據表中的大部分數據,數據表又比較大,這時能夠採用鎖升級的方式,好比將行級鎖升級爲表級鎖,從而減小死鎖產生的機率;
  3. 不一樣事務併發讀寫多張數據表,能夠約定訪問表的順序,採用相同的順序下降死鎖發生的機率。

2 事務的隔離級別存在的問題

咱們知道事務有 4 個隔離級別,以及可能存在的三種異常問題,以下圖所示:

在 MySQL 中,默認的隔離級別是可重複讀,能夠解決髒讀和不可重複讀的問題,但不能解決幻讀問題。若是咱們想要解決幻讀問題,就須要採用串行化的方式,也就是將隔離級別提高到最高,但這樣一來就會大幅下降數據庫的事務併發能力。

有沒有一種方式,能夠不採用鎖機制,而是經過樂觀鎖的方式來解決不可重複讀和幻讀問題呢?實際上 MVCC 機制的設計,就是用來解決這個問題的,它能夠在大多數狀況下替代行級鎖,下降系統的開銷。

3 MVVC

InnoDB併發如此高,緣由居然在這?

3.1 數據多版本介紹

MVCC就是用來實現上面的第三個隔離級別,可重複讀RR。

MVCC:Multi-Version Concurrency Control,即多版本的併發控制協議。

從名字中也能看出來,MVCC 是經過數據行的多個版本管理來實現數據庫的併發控制,簡單來講它的思想就是保存數據的歷史版本。這樣咱們就能夠經過比較版本號決定數據是否顯示出來(具體的

規則後面會介紹到),讀取數據的時候不須要加鎖也能夠保證事務的隔離效果。

經過 MVCC 咱們能夠解決如下幾個問題:

  • 讀寫之間阻塞的問題,經過 MVCC 可讓讀寫互相不阻塞,即讀不阻塞寫,寫不阻塞讀,這樣就能夠提高事務併發處理能力。
  • 下降了死鎖的機率。這是由於 MVCC 採用了樂觀鎖的方式,讀取數據時並不須要加鎖,對於寫操做,也只鎖定必要的行。
  • 解決一致性讀的問題。一致性讀也被稱爲快照讀,當咱們查詢數據庫在某個時間點的快照時,只能看到這個時間點以前事務提交更新的結果,而不能看到這個時間點以後事務提交的更新結果。

MVCC的特色就是在同一時刻,不一樣事務能夠讀取到不一樣版本的數據,從而能夠解決髒讀和不可重複讀的問題。

MVCC實際上就是經過數據的隱藏列和回滾日誌(undo log),實現多個版本數據的共存。這樣的好處是,使用MVCC進行讀數據的時候,不用加鎖,從而避免了同時讀寫的衝突。

在實現MVCC時,每一行的數據中會額外保存幾個隱藏的列,好比當前行建立時的版本號和刪除時間和指向undo log的回滾指針。這裏的版本號並非實際的時間值,而是系統版本號。每開始新的事務,系統版本號都會自動遞增。事務開始時的系統版本號會做爲事務的版本號,用來和查詢每行記錄的版本號進行比較。每一個事務又有本身的版本號,這樣事務內執行數據操做時,就經過版本號的比較來達到數據版本控制的目的。

提升併發的演進思路,就在如此:

  • 普通鎖,本質是串行執行

  • 讀寫鎖,能夠實現讀讀併發

  • 數據多版本,能夠實現讀寫併發

畫外音:這個思路,比整篇文章的其餘技術細節更重要,但願你們牢記。

3.2 InnoDB實現MVCC

1)MVCC是怎樣實現隔離級別的

MVCC多版本併發控制是MySQL中基於樂觀鎖理論實現隔離級別的方式,用於讀已提交和可重複讀取隔離級別的實現。

在MySQL中,會在表中每一條數據後面添加兩個字段:最近修改該行數據的事務ID,指向該行(undolog表中)回滾段的指針。

Read View判斷行的可見性,建立一個新事務時,copy一份當前系統中的活躍事務列表。意思是,當前不該該被本事務看到的其餘事務id列表。

已提交讀隔離級別下的事務在每次查詢的開始都會生成一個獨立的ReadView,而可重複讀隔離級別則在第一次讀的時候生成一個ReadView,以後的讀都複用以前的ReadView。 

2)核心問題

舊版本數據存儲在哪裏?

舊版本數據存儲在回滾段裏;

存儲舊版本數據,對MySQL和InnoDB原有架構是否有巨大沖擊?

對MySQL和InnoDB原有架構體系衝擊不大;

3)InnoDB爲什麼可以作到這麼高的併發?

回滾段裏的數據,實際上是歷史數據的快照(snapshot),這些數據是不會被修改,select能夠肆無忌憚的併發讀取他們。

快照讀(Snapshot Read),這種一致性不加鎖的讀(Consistent Nonlocking Read),就是InnoDB併發如此之高的核心緣由之一。

這裏的一致性是指,事務讀取到的數據,要麼是事務開始前就已經存在的數據(固然,是其餘已提交事務產生的),要麼是事務自身插入或者修改的數據。

什麼樣的select是快照讀?

除非顯示加鎖,普通的select語句都是快照讀,例如:

select * from t where id>2;

這裏的顯示加鎖,非快照讀(當前讀,包括了加鎖的讀取和 DML 操做)是指:

select * from t where id>2 lock in share mode;

select * from t where id>2 for update;

INSERT INTO player values ...

DELETE FROM player WHERE ...

UPDATE player SET ...

MVCC 能夠解決讀寫互相阻塞的問題,這樣提高了效率,同時由於採用了樂觀鎖的思想,下降了死鎖的機率。

4)數據的隱藏列具體是指什麼?

InnoDB的內核,會對全部row數據增長三個內部屬性:

  • DB_TRX_ID,6字節,記錄每一行最近一次修改它的事務ID;
  • DB_ROLL_PTR,7字節,記錄指向回滾段undo日誌的指針;
  • DB_ROW_ID,6字節,單調遞增的行ID;

5)Read View 是如何工做的

在 MVCC 機制中,多個事務對同一個行記錄進行更新會產生多個歷史快照,這些歷史快照保存在 Undo Log 裏。若是一個事務想要查詢這個行記錄,須要讀取哪一個版本的行記錄呢?這時就須要用到 Read View 了,它幫咱們解決了行的可見性問題。Read View 保存了當前事務開啓時全部活躍(尚未提交)的事務列表,換個角度你能夠理解爲 Read View 保存了不該該讓這個事務看到的其餘的事務 ID 列表。

在 Read VIew 中有幾個重要的屬性:

  1. trx_ids,系統當前正在活躍的事務 ID 集合。
  2. low_limit_id,活躍的事務中最大的事務 ID。
  3. up_limit_id,活躍的事務中最小的事務 ID。
  4. creator_trx_id,建立這個 Read View 的事務 ID。

如圖所示,trx_ids 爲 trx二、trx三、trx5 和 trx8 的集合,活躍的最大事務 ID(low_limit_id)爲 trx8,活躍的最小事務 ID(up_limit_id)爲 trx2。

假設當前有事務 creator_trx_id 想要讀取某個行記錄,這個行記錄的事務 ID 爲 trx_id,那麼會出現如下幾種狀況。

若是 trx_id < 活躍的最小事務 ID(up_limit_id),也就是說這個行記錄在這些活躍的事務建立以前就已經提交了,那麼這個行記錄對該事務是可見的。

若是 trx_id > 活躍的最大事務 ID(low_limit_id),這說明該行記錄在這些活躍的事務建立以後才建立,那麼這個行記錄對當前事務不可見。

若是 up_limit_id < trx_id < low_limit_id,說明該行記錄所在的事務 trx_id 在目前 creator_trx_id 這個事務建立的時候,可能還處於活躍的狀態,所以咱們須要在 trx_ids 集合中進行遍歷,若是 trx_id 存在於 trx_ids 集合中,證實這個事務 trx_id 還處於活躍狀態,不可見。不然,若是 trx_id 不存在於 trx_ids 集合中,證實事務 trx_id 已經提交了,該行記錄可見。

瞭解了這些概念以後,咱們來看下當查詢一條記錄的時候,系統如何經過多版本併發控制技術找到它:

  1. 首先獲取事務本身的版本號,也就是事務 ID;
  2. 獲取 Read View;
  3. 查詢獲得的數據,而後與 Read View 中的事務版本號進行比較;
  4. 若是不符合 ReadView 規則,就須要從 Undo Log 中獲取歷史快照;
  5. 最後返回符合規則的數據。

你能看到 InnoDB 中,MVCC 是經過 Undo Log + Read View 進行數據讀取,Undo Log 保存了歷史快照,而 Read View 規則幫咱們判斷當前版本的數據是否可見。

須要說明的是,在隔離級別爲讀已提交(Read Commit)時,一個事務中的每一次 SELECT 查詢都會獲取一次 Read View。如表所示:

你能看到,在讀已提交的隔離級別下,一樣的查詢語句都會從新獲取一次 Read View,這時若是 Read View 不一樣,就可能產生不可重複讀或者幻讀的狀況。

當隔離級別爲可重複讀的時候,就避免了不可重複讀,這是由於一個事務只在第一次 SELECT 的時候會獲取一次 Read View,然後面全部的 SELECT 都會複用這個 Read View,以下表所示:

 

6)InnoDB 是如何解決幻讀的

不過這裏須要說明的是,在可重複讀的狀況下,InnoDB 能夠經過 Next-Key 鎖 +MVCC 來解決幻讀問題。

在讀已提交的狀況下,即便採用了 MVCC 方式也會出現幻讀。若是咱們同時開啓事務 A 和事務 B,先在事務 A 中進行某個條件範圍的查詢,讀取的時候採用排它鎖,在事務 B 中增長一條符合該條件範圍的數據,並進行提交,而後咱們在事務 A 中再次查詢該條件範圍的數據,就會發現結果集中多出一個符合條件的數據,這樣就出現了幻讀。

出現幻讀的緣由是在讀已提交的狀況下,InnoDB 只採用記錄鎖(Record Locking)。這裏要介紹下 InnoDB 三種行鎖的方式:

  1. 記錄鎖:針對單個行記錄添加鎖。
  2. 間隙鎖(Gap Locking):能夠幫咱們鎖住一個範圍(索引之間的空隙),但不包括記錄自己。採用間隙鎖的方式能夠防止幻讀狀況的產生。
  3. Next-Key 鎖:幫咱們鎖住一個範圍,同時鎖定記錄自己,至關於間隙鎖 + 記錄鎖,能夠解決幻讀的問題。

在隔離級別爲可重複讀時,InnoDB 會採用 Next-Key 鎖的機制,幫咱們解決幻讀問題。

仍是這個例子,咱們能看到當咱們想要插入球員艾利克斯·倫(身高 2.16 米)的時候,事務 B 會超時,沒法插入該數據。這是由於採用了 Next-Key 鎖,會將 height>2.08 的範圍都進行鎖定,就沒法插入符合這個範圍的數據了。而後事務 A 從新進行條件範圍的查詢,就不會出現幻讀的狀況。

總結

今天關於 MVCC 的內容有些多,經過學習你應該能對採用 MVCC 這種樂觀鎖的方式來保證事務的隔離效果更有體會。

咱們須要記住,MVCC 的核心就是 Undo Log+ Read View,「MV」就是經過 Undo Log 來保存數據的歷史版本,實現多版本的管理,「CC」是經過 Read View 來實現管理,經過 Read View 原則來決定數據是否顯示。同時針對不一樣的隔離級別,Read View 的生成策略不一樣,也就實現了不一樣的隔離級別。

MVCC 是一種機制,MySQL、Oracle、SQL Server 和 PostgreSQL 的實現方式均有不一樣,咱們在學習的時候,更主要的是要理解 MVCC 的設計思想。

7)臨鍵鎖(next-key lock)

InnoDB實現的隔離級別RR時能夠避免幻讀現象的,這是經過next-key lock機制實現的。

next-key lock實際上就是行鎖的一種,只不過它不僅是會鎖住當前行記錄的自己,還會鎖定一個範圍。好比上面幻讀的例子,開始查詢0<閱讀量<100的文章時,只查到了一個結果。next-key lock會將查詢出的這一行進行鎖定,同時還會對0<閱讀量<100這個範圍進行加鎖,這其實是一種間隙鎖。間隙鎖可以防止其餘事務在這個間隙修改或者插入記錄。這樣一來,就保證了在0<閱讀量<100這個間隙中,只存在原來的一行數據,從而避免了幻讀。

間隙鎖:封鎖索引記錄中的間隔

雖然InnoDB使用next-key lock可以避免幻讀問題,但卻並非真正的可串行化隔離。再來看一個例子吧。

首先提一個問題:

在T6時間,事務A提交事務以後,猜一猜文章A和文章B的閱讀量爲多少?

答案是,文章AB的閱讀量都被修改爲了10000。這表明着事務B的提交實際上對事務A的執行產生了影響,代表兩個事務之間並非徹底隔離的。雖然可以避免幻讀現象,可是卻沒有達到可串行化的級別。這還說明,避免髒讀、不可重複讀和幻讀,是達到可串行化的隔離級別的必要不充分條件。可串行化是都可以避免髒讀、不可重複讀和幻讀,可是避免髒讀、不可重複讀和幻讀卻不必定達到了可串行化。

3 別廢話,各類SQL到底加了什麼鎖?

這個月花了一些功夫寫InnoDB:併發控制,MVCC,索引,鎖... 

有朋友留言:你TM講了這麼多,分了這麼多類型,又和事務隔離級別相關,又和索引相關,究竟能不能直接告訴我,一個SQL到底加了什麼鎖!?

我竟無言以對。

好吧,作過簡單梳理以後,今天嘗試着直接回答,儘可能作到不重不漏,各類SQL語句究竟加了什麼鎖。

1、普通select

(1)在讀未提交(Read Uncommitted),讀提交(Read Committed, RC),可重複讀(Repeated Read, RR)這三種事務隔離級別下,普通select使用快照讀(snpashot read),不加鎖,併發很是高;

(2)在串行化(Serializable)這種事務的隔離級別下,普通select會升級爲select ... in share mode;

【快照讀】輔助閱讀:

InnoDB,併發如此之高的緣由

【事務隔離級別】輔助閱讀:

InnoDB,巧妙的實現四種事務的隔離級別

2、加鎖select

加鎖select主要是指:

  • select ... for update

  • select ... in share mode

(1)若是,在惟一索引(unique index)上使用惟一的查詢條件(unique search condition),會使用記錄鎖(record lock),而不會封鎖記錄之間的間隔,即不會使用間隙鎖(gap lock)與臨鍵鎖(next-key lock);

【記錄鎖,間隙鎖,臨鍵鎖】輔助閱讀:

InnoDB,索引記錄上的三種鎖

舉個栗子,假設有InnoDB表:

t(id PK, name);

表中有三條記錄:

1, shenjian

2, zhangsan

3, lisi

SQL語句:

select * from t where id=1 for update;

只會封鎖記錄,而不會封鎖區間。

(2)其餘的查詢條件和索引條件,InnoDB會封鎖被掃描的索引範圍,並使用間隙鎖與臨鍵鎖,避免索引範圍區間插入記錄;

3、update與delete

(1)和加鎖select相似,若是在惟一索引上使用惟一的查詢條件來update/delete,例如:

update t set name=xxx where id=1;

也只加記錄鎖;

(2)不然,符合查詢條件的索引記錄以前,都會加排他臨鍵鎖(exclusive next-key lock),來封鎖索引記錄與以前的區間;

(3)尤爲須要特殊說明的是,若是update的是彙集索引(clustered index)記錄,則對應的普通索引(secondary index)記錄也會被隱式加鎖,這是由InnoDB索引的實現機制決定的:普通索引存儲PK的值,檢索普通索引本質上要二次掃描彙集索引。

【索引底層實現】輔助閱讀:

索引,底層是如何實現的?

【彙集索引與普通索引的實現差別】輔助閱讀:

InnoDB,彙集索引與普通索引有什麼不一樣?

4、insert

一樣是寫操做,insert和update與delete不一樣,它會用排它鎖封鎖被插入的索引記錄,而不會封鎖記錄以前的範圍。

同時,會在插入區間加插入意向鎖(insert intention lock),但這個並不會真正封鎖區間,也不會阻止相同區間的不一樣KEY插入。

【插入意向鎖】輔助閱讀:

InnoDB,插入意向鎖

瞭解不一樣SQL語句的加鎖,對於分析多個事務之間的併發與互斥,以及事務死鎖,很是有幫助。

99 直接讀這些牛人的原文

MySQL事務,MVCC,undo log,redo log——最全總結!

關於MVCC,我以前寫錯了,此次我改好了!

架構師之路:InnoDB,快照讀,在RR和RC下有何差別?

相關文章
相關標籤/搜索