

本文属于机器翻译版本。若本译文内容与英语原文存在差异，则一律以英文原文为准。

# Neptune 中的事务隔离级别
<a name="transactions-neptune"></a>

Amazon Neptune 为只读查询和突变查询实现了不同的事务隔离级别。基于以下标准，SPARQL 和 Gremlin 查询被划分为只读查询或更改查询：
+ 在 SPARQL 中，读取查询（`SELECT`、`ASK`、`CONSTRUCT` 和 `DESCRIBE`，如 [SPARQL 1.1 Query Language](https://www.w3.org/TR/sparql11-query/) 规范中所定义）和突变查询（`INSERT` 和 `DELETE`，如 [SPARQL 1.1 Update](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` 的更改，就像 `Tx2` 在 `Tx1` 开始之前已经完成一样。由于这仅适用于已提交的更改，因此绝不会发生 `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


| 计数器 | 说明 | 
| --- | --- | 
| `sharedLocksWaitTimeMillis` | 等待获取共享 (S) 锁所花费的时间，共享 (S) 锁允许多个读取器但会屏蔽写入器。 | 
| `exclusiveLocksWaitTimeMillis` | 等待获得专用 (X) 锁所花费的时间，这些锁会阻止所有其他访问。 | 

只有在`debug`模式 () `neptune_enable_slow_query_log=debug` 中启用慢查询日志记录时，这两个字段才会出现在`storageCounters`对象中。

**提示**  
如果`sharedLocksWaitTimeMillis + exclusiveLocksWaitTimeMillis`接近查询`overallRunTimeMs`，则查询会受到锁争用（而不是 CPU、网络或 I/O）的瓶颈。

减少争夺的实用技巧：
+ **错开相互冲突的作业** — 在用户活动较少的时期进行大量批量突变。
+ 将较@@ **大的突变分成较小的区块** ——较小的交易锁定时间更短，从而减少超时的几率。

## 范围锁定和虚假冲突
<a name="transactions-neptune-false-conflicts"></a>

Neptune 使用间隙锁定来进行范围锁定。间隙锁定是对索引记录之间间隙的锁定，或者是对第一条索引记录之前或最后一条索引记录之后间隙的锁定。

Neptune 使用所谓的字典表将数字 ID 值与特定的字符串文本关联起来。以下是此类 Neptune 字典的示例状态：表：


| 字符串 | ID | 
| --- | --- | 
| 类型 | 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 | 类型 | 人员 | default\$1graph | 
| 5 | 1 | 12 | 2 |  | person\$11 | 类型 | 人员 | 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 | 类型 | 人员 | default\$1graph | 
| 11 | 1 | 13 | 2 |  | New York | 类型 | 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` 的 gap5）。

范围锁定只放在读取发生的索引中。在上述情况下，记录仅锁定在 SPOG 索引中，而不锁定在 POGS 或 GPSO 中。可以对所有索引执行查询读取，具体取决于访问模式，可以使用 `explain` APIs （对于 [Sparql 和 Gre](sparql-explain-examples.md) [m](gremlin-explain.md) lin）列出访问模式。

**注意**  
也可以使用间隙锁定来安全地并发更新底层索引，这也可能导致虚假冲突。这些间隙锁定的放置与隔离级别或事务执行的读取操作无关。

虚假冲突不仅发生在*并发*事务由于间隙锁定而发生冲突时，而且在某些情况下，当事务在任何类型的失败后重试时，也会发生。如果失败所触发的回滚仍在进行中，并且之前为该事务采取的锁定尚未完全释放，则重试将遇到虚假冲突并失败。

在高负载下，您通常会发现 3-4% 的写入查询由于虚假冲突而失败。对于外部客户端，此类虚假冲突很难预测，应使用[重试](transactions-exceptions.md)来处理。