

本文為英文版的機器翻譯版本，如內容有任何歧義或不一致之處，概以英文版為準。

# Neptune 中的交易隔離層級
<a name="transactions-neptune"></a>

Amazon Neptune 針對唯讀查詢和變動查詢實作不同的交易隔離層級。SPARQL 和 Gremlin 查詢會根據下列條件分類為唯讀或變動：
+ 在 SPARQL 中，讀取查詢 (`SELECT`、`ASK`、`CONSTRUCT` 和 `DESCRIBE`，如 [SPARQL 1.1 查詢語言](https://www.w3.org/TR/sparql11-query/)規格中所定義) 與變動查詢 (`INSERT` 和 `DELETE`，如 [SPARQL 1.1 更新](https://www.w3.org/TR/sparql11-update/)規格中所定義) 之間有明確的區別。

  請注意，Neptune 將多個一起提交的變動查詢 (例如，在 `POST` 訊息中，以分號分隔) 視為單一交易。其為保證作用的原子單位，不是成功就是失敗，若是失敗則會轉返部分變更。
+ 不過，在 Gremlin 中，Neptune 會根據是否包含任何查詢路徑步驟 (例如操縱資料的 `addE()`、`addV()`、`property()` 或 `drop()`) 將查詢分類為唯讀查詢或變動查詢。如果查詢包含任何這類路徑步驟，則會將其分類並做為變動查詢執行。

也可以在 Gremlin 中使用常設工作階段。如需詳細資訊，請參閱[Gremlin 指令碼型工作階段](access-graph-gremlin-sessions.md)。在這些工作階段中，所有查詢 (包括唯讀查詢) 都會在與寫入器端點上變動查詢相同的隔離下執行。

在 openCypher 中使用 Bolt 讀寫工作階段，所有查詢 (包括唯讀查詢) 都會在與寫入器端點上變動查詢相同的隔離下執行。

**Topics**
+ [Neptune 中的唯讀查詢隔離](#transactions-neptune-read-only)
+ [Neptune 中的變動查詢隔離](#transactions-neptune-mutation)
+ [使用鎖定等待逾時的衝突解決機制](#transactions-neptune-conflicts)
+ [範圍鎖定和錯誤衝突](#transactions-neptune-false-conflicts)

## Neptune 中的唯讀查詢隔離
<a name="transactions-neptune-read-only"></a>

Neptune 會依據快照隔離語義來評估唯讀查詢。這表示唯讀查詢會在邏輯上對查詢評估開始時所取得之資料庫的一致快照進行操作。然後，Neptune 可以保證不會發生以下任何現象：
+ `Dirty reads` – Neptune 中的唯讀查詢絕不會看到來自並行交易的未遞交資料。
+ `Non-repeatable reads` – 多次讀取相同資料的唯讀交易一律會取回相同的值。
+ `Phantom reads` – 唯讀交易絕不會讀取交易開始後新增的資料。

由於快照隔離是使用多版本並行控制 (MVCC) 來實現，因此唯讀查詢不需要鎖定資料，因此不會封鎖變動查詢。

僅供讀取複本僅接受唯讀查詢，因此針對僅供讀取複本的所有查詢都會依據 `SNAPSHOT` 隔離語意執行。

查詢僅供讀取複本時，唯一額外的考量是寫入器和僅供讀取複本之間可能會有較短的複寫延遲。這表示對寫入器所做的更新可能需要很短的時間傳播到您正在從中讀取的僅供讀取複本。實際複寫時間取決於針對主要執行個體的寫入負載。Neptune 架構支援低延遲複寫，而且複寫延遲是以 Amazon CloudWatch 指標進行檢測。

儘管如此，由於 `SNAPSHOT` 隔離層級，讀取查詢永遠都會看到資料庫的一致狀態，即使不是最新的查詢也一樣。

如果您需要強力保證查詢會觀察先前更新的結果，請將查詢傳送到寫入器端點本身，而不是傳送到僅供讀取複本。

## Neptune 中的變動查詢隔離
<a name="transactions-neptune-mutation"></a>

做為變動查詢一部分進行的讀取會在 `READ COMMITTED` 交易隔離下執行，以排除已變更讀取的可能性。超越對 `READ COMMITTED` 交易隔離提供的一般保證，Neptune 強力保證，既不會發生 `NON-REPEATABLE` 讀取，也不會發生 `PHANTOM` 讀取。

這些強力保證是透過讀取資料時鎖定記錄和記錄範圍來實現。這可防止並行交易在讀取後對索引範圍進行插入或刪除，因而保證可重複讀取。

**注意**  
不過，並行變動交易 `Tx2` 可能會在變動交易 `Tx1` 啟動之後開始，並可能在 `Tx1` 鎖定資料以讀取變更之前遞交變更。在此情況下，`Tx1` 會看到 `Tx2` 的變更，宛如在 `Tx1` 啟動之前，`Tx2` 就已完成。因為這只適用於遞交的變更，所以 `dirty read` 永遠不會發生。

若要了解 Neptune 用於變動查詢的鎖定機制，首先了解 Neptune [圖形資料模型](feature-overview-data-model.md) 和 [索引策略](feature-overview-storage-indexing.md) 的詳細資訊很有幫助。Neptune 會使用三個索引 (即 `SPOG`、`POGS` 和 `GPSO`) 管理資料。

為了實現 `READ COMMITTED` 交易層級的可重複讀取，Neptune 會在使用中的索引中採取範圍鎖定。例如，如果變動查詢讀取名為 `person1` 之頂點的所有屬性和傳出邊緣，則節點會在讀取資料之前，鎖定 `SPOG` 索引中字首 `S=person1` 所定義的整個範圍。

使用其他索引時，也適用相同的機制。例如，當變動交易使用 `POGS` 索引，查詢所指定邊緣標籤的所有來源目標頂點時，`P` 位置中的邊緣標籤範圍將被鎖定。任何並行交易，無論它是唯讀或變動查詢，仍可在鎖定的範圍內執行讀取。不過，任何涉及在鎖定字首範圍內插入或刪除新記錄的變動都需要獨佔鎖定，而且將予以防止。

換言之，當變動交易讀取某個索引範圍時，強力保證在讀取交易結束之前，任何並行交易都不會修改此範圍。這可保證 `non-repeatable reads` 不會發生。

## 使用鎖定等待逾時的衝突解決機制
<a name="transactions-neptune-conflicts"></a>

如果第二個交易嘗試修改第一個交易已鎖定之範圍中的記錄，Neptune 會立即偵測到衝突並封鎖第二個交易。

如果未偵測到相依性死結，Neptune 會自動套用鎖定等待逾時機制。其中封鎖的交易會等待最多 60 秒，讓保留鎖定的交易完成並釋出鎖定。
+ 如果鎖定等待逾時在釋出鎖定之前過期，則會復原封鎖的交易。
+ 如果鎖定在鎖定等待逾時內釋出，則第二個交易會解除封鎖並可以成功完成，而不需要重試。

不過，如果 Neptune 在兩個交易之間偵測到相依性死結，則無法自動調解衝突。在此情況下，Neptune 會立即取消並復原兩個交易之一，而不會啟動鎖定等待逾時。Neptune 會盡最大努力復原已插入或刪除最少記錄的交易。

### 測量鎖定等待時間 （引擎 ≥ 1.4.5.0)
<a name="transactions-neptune-lock-wait-metrics"></a>

從引擎 1.4.5.0 版開始，您可以使用兩個 slow-query-log 計數器來觀察變動查詢的封鎖時間：


| 計數器 | Description | 
| --- | --- | 
| `sharedLocksWaitTimeMillis` | 等待取得共用 (S) 鎖定所花費的時間，允許多個讀取器，但封鎖寫入器。 | 
| `exclusiveLocksWaitTimeMillis` | 等待取得專屬 (X) 鎖定所花費的時間，這會封鎖所有其他存取。 | 

只有當您啟用慢速查詢登入`debug`模式 () 時，這兩個欄位才會出現在 `storageCounters` 物件中`neptune_enable_slow_query_log=debug`。

**提示**  
如果 `sharedLocksWaitTimeMillis + exclusiveLocksWaitTimeMillis`接近查詢的 `overallRunTimeMs`，查詢會受到鎖定爭用，而不是 CPU、網路或 I/O 的瓶頸。

減少爭用的實際秘訣：
+ **交錯衝突任務** – 在較低的使用者活動期間執行繁重的批次變動。
+ 將**大型變動分解為較小的區塊** – 較小的交易會鎖定更短的時間，減少逾時的機會。

## 範圍鎖定和錯誤衝突
<a name="transactions-neptune-false-conflicts"></a>

Neptune 會使用間隙鎖定採取範圍鎖定。間隙鎖定是鎖定索引記錄之間的間隙，或鎖定第一個索引記錄之前或最後一個索引記錄之後的間隙。

Neptune 使用所謂的字典資料表，將數值 ID 值與特定字串常值建立關聯。以下是這類 Neptune 字典的範例狀態：資料表：


| String | ID | 
| --- | --- | 
| type | 1 | 
| default\$1graph | 2 | 
| person\$13 | 3 | 
| person\$11 | 5 | 
| knows | 6 | 
| person\$12 | 7 | 
| age | 8 | 
| edge\$11 | 9 | 
| lives\$1in | 10 | 
| New York | 11 | 
| 個人 | 12 | 
| Place | 13 | 
| edge\$12 | 14 | 

上面的字串屬於一個屬性圖模型，但這些概念也同樣適用於所有 RDF 圖形模型。

SPOG (Subject-Predicate-Object\$1Graph) 索引的對應狀態會顯示在下面的左側。右側會顯示對應字串，以協助了解索引資料的含義。


| S (ID) | P (ID) | O (ID) | G (ID) |  | S (字串) | P (字串) | O (字串) | G (字串) | 
| --- | --- | --- | --- | --- | --- | --- | --- | --- | 
| 3 | 1 | 12 | 2 |  | person\$13 | type | 個人 | default\$1graph | 
| 5 | 1 | 12 | 2 |  | person\$11 | type | 個人 | default\$1graph | 
| 5 | 6 | 3 | 9 |  | person\$11 | knows | person\$13 | edge\$11 | 
| 5 | 8 | 40 | 2 |  | person\$11 | age | 40 | default\$1graph | 
| 5 | 10 | 11 | 14 |  | person\$11 | lives\$1in | New York | edge\$12 | 
| 7 | 1 | 12 | 2 |  | person\$12 | type | 個人 | default\$1graph | 
| 11 | 1 | 13 | 2 |  | New York | type | Place | default\$1graph | 

現在，如果變動查詢讀取名為 `person_1` 之頂點的所有屬性和傳出邊緣，則節點會在讀取資料之前，鎖定 SPOG 索引中字首 `S=person_1` 所定義的整個範圍。範圍鎖定會在所有相符的記錄和第一筆不相符的記錄上放置間隙鎖定。相符記錄將遭到鎖定，而不相符的記錄將不會遭到鎖定。Neptune 將放置間隙鎖定，如下所示：
+ ` 5 1 12 2 ` *(間隙 1)*
+ ` 5 6 3 9 ` *(間隙 2)*
+ ` 5 8 40 2 ` *(間隙 3)*
+ ` 5 10 11 14 ` *(間隙 4)*
+ ` 7 1 12 2 ` *(間隙 5)*

這會鎖定下列記錄：
+ ` 5 1 12 2`
+ ` 5 6 3 9`
+ ` 5 8 40 2`
+ ` 5 10 11 14`

在此狀態下，下列操作會遭合法封鎖：
+ 插入 `S=person_1` 的新屬性或邊緣。不同於 `type` 的新屬性或新邊緣必須進入間隙 2、間隙 3、間隙 4 或間隙 5，它們全都遭到鎖定。
+ 刪除任何現有記錄。

同時，一些並行作將遭錯誤地封鎖 (產生錯誤衝突）：
+ `S=person_3` 的任何屬性或邊緣插入都會遭到封鎖，因為它們必須進入間隙 1。
+ 在 3 與 5 之間獲指派 ID 的任何新頂點插入將遭封鎖，因為它必須進入間隙 1。
+ 在 5 與 7 之間獲指派 ID 的任何新頂點插入將遭封鎖，因為它必須進入間隙 5。

間隙鎖定不夠精確，無法鎖定一個特定述詞的間隙 (例如，鎖定述詞 `S=5` 的間隙 5)。

範圍鎖定只會放置在發生讀取的索引中。在上述情況下，記錄僅鎖定在 SPOG 索引中，而不是在 POGS 或 GPSO 中。查詢的讀取可能跨所有索引上執行，取決於存取模式，這些模式可以使用 `explain` API 來列出 (適用於 [Sparql](sparql-explain-examples.md) 和 [Gremlin](gremlin-explain.md))。

**注意**  
也可以採取間隙鎖定，對基礎索引進行安全的並行更新，這也可能導致錯誤的衝突。放置這些間隙鎖定與交易所執行的隔離層級或讀取操作無關。

不僅在「並行」**交易由於間隙鎖定而發生衝突時，可能發生錯誤衝突，還可能在某些情況下，於任何類型的失敗之後重試交易時也會發生。如果失敗觸發的復原仍在進行中，且先前針對交易採取的鎖定尚未完全釋放，則重試將會遇到錯誤的衝突並失敗。

在高負載下，您通常會發現 3-4% 的寫入查詢會由於錯誤的衝突而失敗。對於外部用戶端，這類錯誤的衝突很難預測，應該使用[重試](transactions-exceptions.md)來處理。