【巨杉數據庫SequoiaDB】巨杉 Tech

01

概述


數據庫是一個多用戶使用的共享資源。當多個用戶併發地存取數據時,在數據庫中就會產生多個事務同時存取同一數據的情況。若對併發操作不加控制就可能會讀取和存儲不正確的數據,破壞數據庫的一致性。加鎖是實現數據庫併發控制的一個非常重要的技術。當事務在對某個數據對象進行操作前,先向系統發出請求,對其加鎖。加鎖後事務就對該數據對象有了一定的控制,在該事務釋放鎖之前,其他的事務不能對此數據對象進行更新操作。


OLTP 場景下通常要求具有很高的併發性。併發事務實際上取決於資源的使用狀況,原則上應儘量減少對資源的鎖定時間,減少對資源的鎖定範圍,從而能夠儘量增加併發事務的數量,那麼影響併發的因素有哪些呢?本文將從巨杉分佈式數據庫本身的機制以及隔離級別、數據庫鎖、參數、及實際例子進行詳解,讀完本文將對巨杉數據庫併發性與鎖機制有一個初步的瞭解。


02

隔離級別與併發性


在單用戶環境中,每個事務都是順序執行的,而不會遇到與其他事務的衝突。但是,在多用戶環境下,多個事務併發執行。因此每個事務都有可能與其他正在運行的事務發生衝突。有可能與其他事務發生衝突的事務稱為交錯的或並行的事務,而相互隔離的事務稱為串行化事務,這意味著同時運行它們的結果與一個接一個連續地運行它們的結果沒有區別。在多用戶環境下,在使用並行事務時,會發生四種現象:

  • 丟失更新:這種情況發生在兩個事務讀取並嘗試更新同一數據時,其中一個更新會丟失。例如:事務 1 和事務 2 讀取同一行數據,並都根據所讀取的數據計算出該行的新值。如果事務 1 用它的新值更新該行以後,事務 2 又更新了同一行,則事務 1 所執行的更新操作就丟失了。
  • 髒讀:當事務讀取尚未提交的數據時,就會發生這種情況。例如:事務 1 更改了一行數據,而事務 2 在事務1 提交更改之前讀取了已更改的行。如果事務 1 回滾該更改,則事務 2 就會讀取被認為是不曾存在的數據。
  • 不可重複的讀:當一個事務兩次讀取同一行數據,但每次獲得不同的數據值時,就會發生這種情況。例如:事務 1 讀取了一行數據,而事務 2 在更改或刪除該行後提交了更改。當事務 1 嘗試再次讀取該行時,它會檢索到不同的數據值(如果該行已經被更新的話),或發現該行不復存在了(如果該行被刪除的話)。
  • 幻像:當最初沒有看到某個與搜索條件匹配的數據行,而在稍後的讀操作中又看到該行時,就會發生這種情況。例如:事務 1 讀取滿足某個搜索條件的一組數據行,而事務 2 插入了與事務 1 的搜索條件匹配的新行。如果事務 1 再次執行產生原先行集的查詢,就會檢索到不同的行集。


維護數據庫的一致性和數據完整性,同時又允許多個應用程序同時訪問同一數據,這樣的特性稱為併發性。巨杉數據庫目前通過事務、隔離級別、鎖等機制來對併發性進行控制,它決定在第一個事務訪問數據時,如何對其他事務鎖定或隔離該事務所使用的數據。目前巨杉數據庫支持以下隔離級別來實現併發性:

  • 讀未提交(ReadUncommitted):該隔離級別指即使一個事務的更新語句沒有提交,但是別的事務可以讀到這個改變,幾種異常情況都可能出現。會出現讀取的數據是不對的。
  • 讀已提交(Read Committed):該隔離級別指一個事務只能看到其他事務的已經提交的更新,看不到未提交的更新,消除了髒讀和第一類丟失更新,這是大多數數據庫的默認隔離級別。保證了一個事務不會讀到另一個並行事務已修改但未提交的數據,避免了“髒讀取”,但不能避免“幻讀”和“不可重複讀取”。該級別適用於大多數系統。
  • 讀穩定性(RepeatableStability):該隔離級別指一個事務中進行兩次或多次同樣的對於數據內容的查詢,得到的結果是一樣的。假設SQL語句中包括查詢條件, 則會對全部符合條件的紀錄加對應的鎖。假設沒有條件語句。也就是對錶中的全部記錄進行處理。則會對全部的紀錄加鎖。
  • 可重複讀(Repeatable Read):REPEATABLE READ隔離級解決了READUNCOMMITTED隔離級導致的問題。它確保同一事務的多個實例在併發讀取數據時,會“看到同樣的”數據行。不過理論上,這會導致另一個棘手問題:幻讀(Phantom Read)。簡單來說,幻讀指當用戶讀取某一範圍的數據行時,另一個事務又在該範圍內插入了新行,當用戶再讀取該範圍的數據行時,會發現有新的“幻影”行。數據庫存儲引擎可以通過多版本併發控制 (Multiversion Concurrency Control)機制解決了幻讀問題,如MySQL的InnoDB和Falcon。巨杉數據庫對於多版本控制(MVCC)技術是通過採用事務鎖、內存老版本以及磁盤迴滾段重建老版本的設計來實現。此架構設計的理論基礎是通過對內存結構的合理利用,存儲數據和索引的老版本信息,從而實現數據的快速的併發訪問。


03

數據庫鎖參數與併發性實踐


1. SequoiaDB的事務配置

事務作為一個完整的工作單元執行,事務中的操作要麼全部執行成功要麼全部執行失敗。SequoiaDB事務中的操作只能是插入數據、修改數據以及刪除數據,在事務過程中執行的其它操作不會納入事務範疇,也就是說事務回滾時非事務操作不會被執行回滾。如果一個表或表空間中有數據涉及事務操作,則該表或表空間不允許被刪除。


  • 事務開啟、提交與回滾

在SDB中,關於事務啟停的配置項如下:

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

默認情況下,SequoiaDB 所有節點的事務功能都是開啟的。若用戶不需要使用事務功能,可參考以下方法,關閉事務功能。


步驟1:通過sdb shell設置集群所有節點都關閉事務。


<code>db.updateConf( { transactionon: false }, { Global: true } )/<code>
【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

步驟2:在集群每臺服務器上都重啟 SequoiaDB 的所有節點。​​​​​​​

<code>[sdbadmin@ubuntu-dev1 ~]$ /opt/sequoiadb/bin/sdbstop -t all[sdbadmin@ubuntu-dev1 ~]$ /opt/sequoiadb/bin/sdbstart -t all/<code>


注意:

1. 開啟及關閉節點的事務功能都要求重啟該節點。

2. 在開啟節點事務功能的情況下,節點的配置項logfilenum(該配置項默認值為20)的值不能小於 5。

SequoiaDB 事務支持的操作如下:

  • 寫事務操作:INSERT、UPDATE、DELETE。
  • 讀事務操作:QUERY。


SequoiaDB的其它操作(如:創建表、創建索引、創建並讀寫LOB等其它非 CRUD 操作)不在事務功能的考慮範圍。

支持隔離級別配置參數及取值如下:

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

可以通過以下方式修改:


<code>db.updateConf( { transisolation: 1 }, { Global: true } )/<code>
【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

注意:該參數在線生效,會在下一次事務中生效


通過 "transBegin"、"transCommit" 及"transRollback" 方法,用戶可以在一個事務中,對若干個操作進行事務控制。其使用方式如下:

<code>> db.transBegin()> 操作1> 操作2> 操作3> ...> db.transCommit() or db.transRollback()/<code>


在上述使用模式中,用戶必須顯式調用"transCommit" 及 "transRollback" 方法來結束當前事務。然而,對於寫事務操作,若在操作過程發生錯誤,數據庫配置中的 transautorollback 配置項可以決定當前會話所有未提交的寫操作是否自動回滾。transautorollback 的描述如下:


注意:該配置項只有在事務功能開啟(即 transactionon 為 true )的情況下才生效。


默認情況下,transautorollback 配置項的值為 true。所以,當寫事務操作過程出現失敗時,當前事務所有未提交的寫操作都將被自動回滾。


  • 事務自動提交

數據庫配置中,關於事務自動提交的配置項如下:

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

​​

事務自動提交功能默認情況下是關閉的。當transautocommit 設置為 true 時,事務自動提交功能將開啟。此時,使用事務存在以下兩點不同:


  • 用戶不需要顯式調用 "transBegin" 和"transCommit" 或者 "transRollback" 方法來控制事務的開啟、提交或者回滾。
  • 事務提交或者回滾的範圍僅僅侷限於單個操作。當單個操作成功時,該操作將被自動提交;當單個操作失敗時,該操作將被自動回滾。


例如,如下操作中:


<code>> /* transautocommit 設置為 true */> db.foo.bar.update({$inc:{"salary": 1000}}, {"department": "A"}) // 更新 1> db.foo.bar.update({$inc:{"salary": 2000}}, {"department": "B"}) // 更新 2> db.foo.bar.update({$inc:{"salary": 3000}}, {"department": "C"}) // 更新 3> .../<code>


更新 1、更新 2、更新 3 分別為獨立的操作。假設更新 1 和 更新 2 操作成功,而更新 3失敗。那麼更新 1 和 更新 2 修改的記錄將全部被自動提交。而更新 3 修改的記錄將全部被自動回滾。


  • 其它配置

數據庫配置中,關於事務的其它主要配置項如下:

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

  • 調整設置

當用戶希望調整事務的設置時(如:是否開啟事務、調整事務配置項等),有如下 3 種方式供用戶選擇使用:


  1. 用戶可以將數據庫配置描述的事務配置項,配置到集群所有(或者部分)節點的配置文件中。若修改的配置項要求重啟節點才能生效,用戶需重啟相應的節點。
  2. 使用 updateConf()命令在 sdb shell 中修改集群的事務配置項。若修改的配置項要求重啟節點才能生效,用戶需重啟相應的節點。
  3. 使用 setSessionAttr()命令在會話中修改當前會話的事務配置項。該設置只在當前會話生效,並不影響其它會話的設置情況。

2. SequoiaDB併發與鎖操作實踐

示例:

建立數據庫以及表

<code>mysql> use company;Database changedmysql>  create table t1 (a int,b int);Query OK, 0 rows affected (0.03 sec)/<code>


1)事務提交與回滾


例子1:

使用事務回滾插入操作。事務回滾後,插入的記錄將被回滾,集合中無記錄:

<code>mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> insert into t1 values(1,1);Query OK, 1 row affected (0.08 sec)mysql> select * from t1;+------+------+| a    | b    |+------+------+|    1 |    1 |+------+------+1 row in set (0.00 sec)mysql> rollback;Query OK, 0 rows affected (0.01 sec)mysql> select * from t1;Empty set (0.00 sec)/<code>


例子2:

使用事務提交插入操作。提交事務後,插入的記錄將被持久化到數據庫:

<code>mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> insert into t1 values(1,1);Query OK, 1 row affected (0.00 sec)mysql> commit;Query OK, 0 rows affected (0.01 sec)mysql> select * from t1;+------+------+| a    | b    |+------+------+|    1 |    1 |+------+------+1 row in set (0.01 sec)/<code>


2)隔離級別為RU併發與鎖

例子3:

在隔離級別為RU(transisolation 設置為0)的情況下,設置當前會話級(會話1及會話2同時設置)隔離級別為read uncommitted :

​​​​​​​

<code>mysql> SELECT @@tx_isolation;+-----------------+| @@tx_isolation  |+-----------------+| REPEATABLE-READ |+-----------------+1 row in set, 1 warning (0.00 sec)mysql> set session transaction isolation level read uncommitted;Query OK, 0 rows affected (0.00 sec)mysql> SELECT @@tx_isolation;+------------------+| @@tx_isolation   |+------------------+| READ-UNCOMMITTED |+------------------+1 row in set, 1 warning (0.00 sec)/<code>


在窗口1:會話1對該表寫入數據,並不提交。

​​​​​​​

<code>mysql> create table t3(a int,b int);Query OK, 0 rows affected (0.02 sec)mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> insert into t3 values(1,1);Query OK, 1 row affected (0.03 sec)/<code>


在窗口2:會話2對該表進行查詢,查到的是未提交的數據。

<code>mysql>  select * from t3;+------+------+| a    | b    |+------+------+|   1 |    1 |+------+------+1 row in set (0.01 sec)/<code>


小結:由於採用了隔離級別是RU,允許髒讀,在第二個會話中不會產生鎖等待,而直接會讀到未提交的數據。該隔離級別建議設置在以讀為主的歷史數據平臺應用中,在真實的OLTP環境中,不能滿足業務需求。這樣業務查詢會讀取到未提交事務的修改,如果事務發生回滾,那麼讀取的數據是錯誤的。不能滿足一致性的要求。


3)隔離級別為RC併發與鎖

RR隔離級別的實現概述:

巨杉數據庫在RC隔離級別上除了支持傳統關係型數據庫的讀已提交以外,通過MVCC多版本訪問的方式支持讀取最後一次提交的版本而不會產生鎖等待,從而提高業務的並行處理能力。


例子4:在隔離級別為RC(transisolation 設置為1,translockwait為true)的情況下,看看併發情況:

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

首先,修改隔離級別為1,translockwait為true該修改將在下一次連接的時候生效。


<code>> db.updateConf({transisolation:1},{Global:true});Takes 0.051656s.> db.updateConf({translockwait:true},{Global:true});Takes 0.041699s.> db.updateConf({transactiontimeout:30},{Global:true});Takes 0.099934s.> db.updateConf({transautocommit:true},{Global:true});Takes 0.040183s./<code>


備註:也可以通過mysql端進行當前session隔離級別參數的修改。

設置當前會話級(會話1及會話2同時設置)隔離級別為read committed :

​​​​​​​

<code>mysql> set session transaction isolation level read committed;  Query OK, 0 rows affected (0.00 sec)/<code>


在窗口1:事務1對該表寫入數據,並不提交。

​​​​​​​

<code>mysql> create table t4 (a int,b int);Query OK, 0 rows affected (0.09 sec)mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> insert into t4 values(1,1);Query OK, 1 row affected (0.03 sec)/<code>


在窗口2:事務2對該表進行查詢,可以看到一直會處於等待鎖的狀態,直到鎖超時(transactiontimeout設置為30秒)退出。

​​​​​​​

<code>mysql> select * from t4;ERROR 1030 (HY000): Got eror 40013 from storage engine/<code>


通過捉取鎖的快照,可以看到第一個事務持有鎖的信息,持有該表上的IX,IS鎖以及記錄上的X鎖。如下圖所示:

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

而第二個事務等待鎖的情況,在等鎖該表記錄上的S鎖。如下圖所示:

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

小結:由於採用了隔離級別是RC 並且translockwait設置為true的情況下,在第二個事務中會產生鎖等待,直到第一個事務釋放該表上的行鎖,第二個事務才能執行,否則會一直等待到鎖超時退出為止。這也是大多數傳統關係型數據庫的默認隔離級別。


例子5:

在隔離級別為RC(transisolation 設置為1,translockwait為false)的情況下,看看併發情況:


我們先來看看translockwait設置為false的說明: 不等待記錄鎖,直接從系統讀取最後一次提交的版本。


設置SDB參數配置:

​​​​​​​

<code>> db.updateConf({translockwait:false},{Global:true});Takes 0.041699s./<code>


備註:可以通過mysql端進行當前session隔離級別參數的修改。

設置當前會話級(會話1及會話2同時設置)隔離級別為read committed :

​​​​​​​

<code>mysql> set session transaction isolation level read committed;  Query OK, 0 rows affected (0.00 sec)/<code>


在窗口1:事務1對該表寫入數據,並不提交。

​​​​​​​

<code>mysql> create table t5 (a int,b int);Query OK, 0 rows affected (0.09 sec)mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> insert into t5 values(1,1);Query OK, 1 row affected (0.03 sec)/<code>


在窗口2:事務2對該表進行查詢,可以看到馬上返回,並沒有發生鎖等待的情況。這時候查到的數據是最後一次提交的版本

<code>mysql> select * from t5;Empty set (0.01 sec)/<code>


小結:由於採用了隔離級別是RC 並且translockwait設置為false的情況下,在第二個事務不會產生鎖等待,而是會讀到最後一次版本已提交的數據。通過鎖快照也可以看到沒有任何鎖等待的情況出現。該隔離級別設置適用於絕大多數的OLTP場景。


4)隔離級別為RS併發與鎖


例子6:

在隔離級別為RS(transisolation 設置為2)的情況下,看看併發情況:

<code>> db.updateConf({transisolation:1},{Global:true});Takes 0.051656s./<code>


在窗口1:事務1對該表進行查詢數據,不提交。

​​​​​​​

<code>mysql> create table t6 (a int,b int,primary key(a));Query OK, 0 rows affected (0.09 sec)mysql> insert into t6 values(1,1);Query OK, 1 row affected (0.03 sec)mysql> insert into t6 values(2,1);Query OK, 1 row affected (0.03 sec)mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> select * from t6;+------+------+| a    | b    |+------+------+|    1 |    1 ||    2 |    1 |+------+------+1 row in set (0.01 sec)/<code>


在窗口2:事務2對該表進行更新,可以看到處於鎖等待的狀態。最終鎖超時事務進行回滾:

​​​​​​​

<code>mysql> egin;Query OK, 0 rows affected (0.00 sec)mysql> update t6 set b=11 where b=1;ERROR 1030 (HY000): Got error 40013 from storage engine/<code>


通過捉取鎖的快照,可以看到第一個事務持有鎖的情況,查詢拿到了該表上的S鎖。如下圖所示:

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

而事務2需要取到該表上的X鎖而產生了等待,如下圖所示:

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

小結:由於採用了隔離級別是RS ,在第二個事務更新的事務會產生鎖等待,任何事務查找的記錄都不允許更新,直到該讀取的表的鎖被釋放。通過鎖快照也可以看到有鎖等待的情況出現。RS場景併發性較差,一般適應於總帳計算系統系統。查到的數據該事務不提交,側不允許被修改。


5)隔離級別為RR併發與鎖

RR隔離級別的實現概述:

在多版本控制技術的事務鎖實現中,RR(可重複讀)配置下的讀操作可以在使用完記錄之後立即釋放鎖,不需要一直持有,直到事務提交或者回滾。但是寫事務操作則需要一直持有插入、更改和刪除的鎖,直到事務完成提交或者回滾。巨杉數據庫鎖的實現是採用悲觀鎖機制,與傳統關係型數據庫的採用的主流鎖機制類似。

在多版本控制技術的實現中,除了引入悲觀鎖的機制以外,巨杉數據庫還採用了內存老版本機制提升數據庫併發訪問及操作的能力。內存老版本是通過在記錄鎖上附加有一個存儲原版本數據和索引相關的結構,於內存中存儲了老版本的數據。

以下通過實例操作進行詳解:


例子7:

在隔離級別為RR(transisolation 設置為3)的情況下,看看併發情況:

<code>> db.updateConf({transisolation:3},{Global:true});Takes 0.051656s.> db.updateConf({mvccon:true},{Global:true})Takes 0.156197s.> db.updateConf({globtranson:true},{Global:true})Takes 0.051241s./<code>


備註:打開RR隔離,除了transisolation 設置為3以外,需要修改以上多二個參數。mvccon及globtranson這二個參數為true。通過mysql端也可以直接進行設置。


通過mysql直接設置當前會話級(會話1及會話2同時設置)隔離級別為REPEATABLEREAD;

​​​​​​​

<code>mysql> set session transaction isolation level REPEATABLE READ;  Query OK, 0 rows affected (0.00 sec)/<code>


在窗口1:事務1對該表rr進行查詢數據,不提交

<code>mysql> create table rr (a int);Query OK, 0 rows affected (0.06 sec)mysql> insert into rr values(1);Query OK, 1 row affected (0.19 sec)mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> select * from rr;+------+| a    |+------+|    1 |+------+1 row in set (0.00 sec)/<code>


在窗口2:事務2對該表rr(事務1第一次查詢後)進行數據更新後提交。

<code>mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> update rr set a=2 where a=1;Query OK, 1 row affected (0.01 sec)Rows matched: 1  Changed: 1  Warnings: 0mysql> commit;Query OK, 0 rows affected (0.01 sec)/<code>

在窗口1:事務1對該表rr進行再次查詢數據,查詢到的數據可以看到不會由於事務2的更新提交而改變,而是讀到事務開始前的版本數據。

<code>mysql> select * from rr;+------+| a    |+------+|    1 |+------+1 row in set (0.00 sec)/<code>


通過捉取鎖的快照,可以看到第一個查詢的事務1在整個事務的查詢中沒有持任何鎖,而事務2更新的操作持用該表的行鎖。如下所示:

事務1持有鎖的情況(未持有鎖):

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

事務2持有鎖的情況,持有該表的X鎖,如下圖所示:

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

【巨杉數據庫SequoiaDB】巨杉 Tech | 併發性與鎖機制解析與實踐

小結:由於採用了隔離級別是RR ,任何查詢都不會持用鎖,也不會等鎖,可以看到在第二個事務更新的操作不會影響事務1,任何更新的操作不會影響查詢,由於事務1是在事務2之前執行查詢,當前事務1始終查到的是rr表的事務2的更新前版本。該隔離級別適應於大併發查詢的交易場景,能有效提高整個應用的併發性。


05

總結

巨杉數據庫完整支持傳統關係型數據庫的幾種常用隔離級別,可滿足所有核心生產場景(OLTP及OLAP等場景)需求。創新性採用事務鎖、內存老版本以及磁盤迴滾段重建老版本的設計來實現了多版本併發控制技術。通過對內存結構的合理利用,存儲數據和索引的老版本信息,從而實現多版本數據的快速的併發訪問。


分享到:


相關文章: