

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

# Neptune 中的交易語意
<a name="transactions"></a>

Amazon Neptune 旨在透過資料圖形支援高度並行線上交易處理 (OLTP) 工作負載。[適用於 RDF 的 W3C SPARQL 查詢語言](https://www.w3.org/TR/rdf-sparql-query/)和 [Apache TinkerPop Gremlin 圖形周遊語言](http://tinkerpop.apache.org/gremlin.html)文件不會為並行查詢處理定義交易語意。由於 ACID 支援和定義良好的交易保證非常重要，因此我們會強制執行嚴格的語義，以協助避免資料異常。

本節會定義這些語義，並說明它們如何套用到 Neptune 中的一些常用案例。

**Topics**
+ [隔離層級的定義](transactions-isolation-levels.md)
+ [Neptune 中的交易隔離層級](transactions-neptune.md)
+ [Neptune 交易語義範例](transactions-examples.md)

# 隔離層級的定義
<a name="transactions-isolation-levels"></a>

`ACID` 中的 "I" 代表*隔離*。交易的隔離程度決定了其他並行交易可能影響其操作的資料有多少。

[SQL:1992 標準](http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt) 建立了描述隔離層級的詞彙。它定義了兩個並行交易 (`Tx1` 和 `Tx2`) 之間可能發生的三種互動 (它稱為「現象」**)：
+ `Dirty read` – 當 `Tx1` 修改項目，然後 `Tx2` 在 `Tx1` 遞交了變更之前讀取該項目，就會發生此情況。然後，如果 `Tx1` 從未成功遞交變更或將其回復，則 `Tx2` 已讀取從未進入資料庫的值。
+ `Non-repeatable read` – 當 `Tx1` 讀取項目，接著 `Tx2` 修改或刪除該項目並遞交變更，然後 `Tx1` 嘗試重新讀取該項目時，便會發生此情況。`Tx1` 現在會讀取與之前不同的值，或發現該項目不再存在。
+ `Phantom read` – 當 `Tx1` 讀取一組滿足搜尋條件的項目，接著 `Tx2` 新增符合搜尋條件的新項目，然後 `Tx1` 重複搜尋時，便會發生此情況。`Tx1` 現在取得與之前不同的一組項目。

這三種類型的互動，每一種都可能導致資料庫中產生的資料不一致。

SQL:1992 標準定義了四種隔離層級，在三種互動類型以及它們可能產生的不一致性方面具有不同的保證。在所有四個層級，可以保證交易完全執行或完全不執行：
+ `READ UNCOMMITTED` – 允許所有三種互動 (亦即，已變更讀取、不可重複讀取及幽靈讀取)。
+ `READ COMMITTED` – 已變更讀取是不可行，但不可重複讀取和幽靈讀取卻可行。
+ `REPEATABLE READ` – 已變更讀取或不可重複讀取全都不可行，但幽靈讀取仍然可行。
+ `SERIALIZABLE` – 三種類型的互動現象都不能發生。

多版本並行控制 (MVCC) 允許另一種隔離，即 *SNAPSHOT* 隔離。這保證在交易開始時，交易可在存在之資料的快照上操作，而且其他交易都不能變更該快照。

# 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)來處理。

# Neptune 交易語義範例
<a name="transactions-examples"></a>

以下範例說明 Amazon Neptune 中交易語義的不同使用案例。

**Topics**
+ [屬性的條件式插入](#transactions-examples-conditional-insertion)
+ [屬性值唯一性](#transactions-examples-unique-property)
+ [條件式屬性變更](#transactions-examples-conditional-edit)
+ [取代屬性](#transactions-examples-replace)
+ [避免懸置元素](#transactions-examples-dangling)

## 範例 1 - 僅在屬性不存在時插入屬性
<a name="transactions-examples-conditional-insertion"></a>

假設您想要確保屬性只設定一次。例如，假設多個查詢正在嘗試同時將信用分數指派給人員。您只想要插入一個屬性執行個體，而其他查詢會失敗，因為已設定屬性。

```
# GREMLIN:
g.V('person1').hasLabel('Person').coalesce(has('creditScore'), property('creditScore', 'AAA+'))

# SPARQL:
INSERT { :person1 :creditScore "AAA+" .}
WHERE  { :person1 rdf:type :Person .
         FILTER NOT EXISTS { :person1 :creditScore ?o .} }
```

Gremlin `property()` 步驟會插入具有所指定索引鍵和值的屬性。`coalesce()` 步驟會在第一個步驟中執行第一個引數，如果失敗，則會執行第二個步驟：

在插入所指定 `person1` 頂點的 `creditScore` 屬性值之前，交易必須嘗試為 `person1` 讀取可能不存在的 `creditScore` 值。此嘗試讀取會鎖定 `SPOG` 索引中 `S=person1` 和 `P=creditScore` 的 `SP` 範圍，其中 `creditScore` 值存在或將被寫入。

採取此範圍鎖定可防止任何並行交易同時插入 `creditScore` 值。有多個平行交易時，一次最多一個平行交易可以更新該值。這會排除建立多個 `creditScore` 屬性的異常。

## 範例 2 - 宣告屬性值在全域上是唯一的
<a name="transactions-examples-unique-property"></a>

假設您想要插入社會安全號碼做為主索引鍵的人員。您想要變動查詢保證，在全域層級中，資料庫中沒有任何其他人具有相同的社會安全號碼：

```
# GREMLIN:
g.V().has('ssn', 123456789).fold()
  .coalesce(__.unfold(),
            __.addV('Person').property('name', 'John Doe').property('ssn', 123456789'))

# SPARQL:
INSERT { :person1 rdf:type :Person .
         :person1 :name "John Doe" .
         :person1 :ssn 123456789 .}
WHERE  { FILTER NOT EXISTS { ?person :ssn 123456789 } }
```

此範例與上述範例類似。主要差別在於範圍鎖定是針對 `POGS` 索引而非 `SPOG` 索引採取的。

執行查詢的交易必須讀取模式 (`?person :ssn 123456789`)，其中繫結 `P` 和 `O` 位置。範圍鎖定是針對 `P=ssn` 和 `O=123456789` 的 `POGS` 索引採取的。
+ 如果模式的確存在，則不會採取任何動作。
+ 如果不存在，鎖定會防止任何並行交易也插入該社會安全號碼

## 範例 3 - 如果另一個屬性具有指定值，則變更屬性
<a name="transactions-examples-conditional-edit"></a>

假設遊戲中的各種事件將人物從第一層移到第二層，並將設為零的新 `level2Score` 屬性指派給他們。您需要確保這類交易的多個並行執行個體無法建立層級二分數屬性的多個執行個體。Gremlin 和 SPARQL 中的查詢可能看起來如下所示。

```
# GREMLIN:
g.V('person1').hasLabel('Person').has('level', 1)
 .property('level2Score', 0)
 .property(Cardinality.single, 'level', 2)

# SPARQL:
DELETE { :person1 :level 1 .}
INSERT { :person1 :level2Score 0 .
         :person1 :level 2 .}
WHERE  { :person1 rdf:type :Person .
         :person1 :level 1 .}
```

在 Gremlin 中，指定 `Cardinality.single` 時，`property()` 步驟會新增屬性，或將現有的屬性值取代為指定的新值。

屬性值的任何更新 (例如將 `level` 從 1 增加到 2) 會實作為刪除目前記錄，並使用新的屬性值插入新記錄。在此情況下，會刪除層級編號 1 的記錄，並重新插入層級編號 2 的記錄。

若要讓交易能夠新增 `level`，並將 `level2Score` 從 1 更新到 2，必須先驗證 `level` 值目前是否等於 1。執行此動作時，它會對 `SPOG` 索引中 `S=person1`、`P=level` 和 `O=1` 的 `SPO` 字首採取範圍鎖定。此鎖定可防止並行交易刪除版本 1 三元組，因此不可能發生起衝突的並行更新。

## 範例 4 - 取代現有屬性
<a name="transactions-examples-replace"></a>

特定事件可能會將人員的信用分數更新為新值 (這裡指的是 `BBB`)。但您想要確保該類型的並行事件無法為人員建立多個信用分數屬性。

```
# GREMLIN:
g.V('person1').hasLabel('Person')
 .sideEffect(properties('creditScore').drop())
 .property('creditScore', 'BBB')

# SPARQL:
DELETE { :person1 :creditScore ?o .}
INSERT { :person1 :creditScore "BBB" .}
WHERE  { :person1 rdf:type :Person .
         :person1 :creditScore ?o .}
```

此案例與範例 3 類似，差別在於不是鎖定 `SPO` 字首，而是 Neptune 只會鎖定具有 `S=person1` 和 `P=creditScore` 的 `SP` 字首。這可防止並行交易插入或刪除任何具有 `person1` 主旨之 `creditScore` 屬性的三元組。

## 範例 5 - 避免懸置屬性或邊緣
<a name="transactions-examples-dangling"></a>

實體上的更新不應留下懸置元素，也就是與未輸入之實體相關聯的屬性或邊緣。這只是 SPARQL 中的問題，因為 Gremlin 具有內建限制條件來防止懸置元素。

```
# SPARQL:
tx1: INSERT { :person1 :age 23 } WHERE { :person1 rdf:type :Person }
tx2: DELETE { :person1 ?p ?o }
```

`INSERT` 查詢必須讀取和鎖定 `SPOG` 索引 `O=Person` 中具有 `S=person1` 和 `P=rdf:type`,的 `SPO` 字首。鎖定可防止 `DELETE` 查詢平行成功。

在這兩個查詢 (嘗試刪除 `:person1 rdf:type :Person` 記錄的 `DELETE` 查詢，以及讀取記錄並在 `SPOG` 索引中的 `SPO` 上建立範圍的 `INSERT` 查詢) 之間的競爭中，可能會產生以下結果：
+ 如果 `INSERT` 查詢在 `DELETE` 查詢讀取並刪除 `:person1` 的所有記錄之前遞交，則 `:person1` 會完全從資料庫中移除，包括新插入的記錄。
+ 如果 `DELETE` 查詢在 `INSERT` 查詢嘗試讀取 `:person1 rdf:type :Person` 記錄之前遞交，則讀取會觀察遞交的變更。也就是說，它找不到任何 `:person1 rdf:type :Person` 記錄，因此變成無操作。
+ 如果 `INSERT` 查詢在 `DELETE` 查詢讀取之前讀取，則 `:person1 rdf:type :Person` 三元組會遭到鎖定，而且 `DELETE` 查詢會遭到封鎖，直到 INSERT 查詢遞交為止，如先前的第一個案例一樣。
+ 如果 `DELETE` 在 `INSERT` 查詢之前讀取，而且 `INSERT` 查詢嘗試讀取，並對記錄的 `SPO` 字首採取鎖定，則會偵測到衝突。這是因為三元組已標記為移除，然後 `INSERT` 失敗。

在所有這些不同的可能事件序列中，不會建立懸置邊緣。