

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

# 中的冲突检测和解决 AWS AppSync
<a name="conflict-detection-and-resolution"></a>

当发生并发写入时 AWS AppSync，您可以配置冲突检测和冲突解决策略以适当地处理更新。冲突检测确定变更是否与数据来源中的实际写入项目发生冲突。通过将`conflictDetection`字段中的值设置为， SyncConfig 即可启用冲突检测`VERSION`。

冲突解决是在检测到冲突时执行的操作。这是通过在中设置 “冲突处理程序” 字段来确定的 SyncConfig。有三种冲突解决策略：
+ OPTIMISTIC\$1CONCURRENCY
+ AUTOMERGE
+ LAMBDA

 AWS AppSync 在写入操作期间，版本会自动递增，不应由客户端或配置了支持版本的数据源的解析器外部进行修改。否则会改变系统的一致性行为，并可能导致数据丢失。

## 乐观并发
<a name="optimistic-concurrency"></a>

乐观并发是一种冲突解决策略， AWS AppSync 它提供了版本化数据源。当冲突解决程序设置为乐观并发时，如果检测到传入的变更具有与对象的实际版本不同的版本，则冲突处理程序将简单地拒绝传入请求。在 GraphQL 响应中，将提供服务器上具有最新版本的现有项目。然后，客户端需要在本地处理此冲突，并使用项目的更新版本重试变更。

## Automerge
<a name="automerge"></a>

Automerge 为开发人员提供了配置冲突解决策略的简单方法，而无需编写客户端逻辑来手动合并其他策略无法处理的冲突。Automerge 在合并数据以解决冲突时遵守严格的规则集。Automerge 的原则围绕 GraphQL 字段的底层数据类型。这些特性如下所示：
+ 标量字段上的冲突：GraphQL 标量或不是集合（即 List、Set、Map）的任何字段。拒绝标量字段的传入值并选择服务器中现有的值。
+ 列表上的冲突：GraphQL 类型和数据库类型是列表。将传入列表与服务器中的现有列表连接起来。传入变更中的列表值将附加到服务器中列表的末尾。将保留重复的值。
+ 集合上的冲突：GraphQL 类型是一个列表，数据库类型是一个集合。使用传入集合和服务器中的现有集合应用集合合并。这符合集合的属性，意味着没有重复的条目。
+ 当传入变更在项目中添加新字段或针对值为 `null` 的字段进行变更时，请将其合并到现有项目中。
+ 映射上的冲突：当数据库中的底层数据类型是映射（即键值文档）时，在解析和处理映射的每个属性时应用上述规则。

Automerge 旨在使用更新版本自动检测、合并和重试请求，从而使客户端无需手动合并任何冲突的数据。

为了显示 Automerge 如何处理标量类型上的冲突的示例，我们将使用以下记录作为起点。

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 5,
  "_version" : 4
}
```

现在，传入的变更可能正在尝试更新该项目，但使用的是较旧版本，因为客户端尚未与服务器同步。它类似于以下内容：

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 55,
  "_version" : 2
}
```

请注意传入请求中的过时版本 2。在此流程中，Automerge 将通过拒绝“球衣”字段更新为“55”来合并数据，并将其值保持在“5”，从而导致下面的项目图像保存在服务器中。

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 5,
  "_version" : 5 # version is incremented every time automerge performs a merge that is stored on the server.
}
```

鉴于上面显示的具有版本 5 的项目状态，现在假设一个传入的变更尝试使用以下图像改变项目：

```
{
  "id" : 1,
  "name" : "Shaggy",
  "jersey" : 5,
  "interests" : ["breakfast", "lunch", "dinner"] # underlying data type is a Set
  "points": [24, 30, 27] # underlying data type is a List
  "_version" : 3
}
```

传入的变更中有三个兴趣点。名称（一个标量）已被更改，但添加了两个新字段“兴趣”（集合）和“分数”（列表）。在这种情况下，将检测到由于版本不匹配而导致的冲突。Automerge 将遵循其属性并拒绝名称更改，因为它是一个标量并添加到非冲突字段。这将导致保存在服务器中的项目显示如下。

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 5,
  "interests" : ["breakfast", "lunch", "dinner"] # underlying data type is a Set
  "points": [24, 30, 27] # underlying data type is a List
  "_version" : 6
}
```

使用具有版本 6 的项目的更新图像，现在假设传入的变更（版本不匹配）尝试将项目转换为以下内容：

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 5,
  "interests" : ["breakfast", "lunch", "brunch"] # underlying data type is a Set
  "points": [30, 35] # underlying data type is a List
  "_version" : 5
}
```

在这里，我们观察到“兴趣”的传入字段有一个存在于服务器中的重复值和两个新值。在这种情况下，由于底层数据类型是集合，Automerge 会将服务器中现有的值与传入请求中的值合并，并剔除任何重复项。同样，“分数”字段上也存在冲突，其中有一个重复值和一个新值。但是，由于这里的底层数据类型是列表，Automerge 会简单地将传入请求中的所有值附加到服务器中已存在的值的末尾。存储在服务器上的结果合并图像如下所示：

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 5,
  "interests" : ["breakfast", "lunch", "dinner", "brunch"] # underlying data type is a Set
  "points": [24, 30, 27, 30, 35] # underlying data type is a List
  "_version" : 7
}
```

现在，让我们假设存储在服务器中的项目（版本 8）显示如下。

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 5,
  "interests" : ["breakfast", "lunch", "dinner", "brunch"] # underlying data type is a Set
  "points": [24, 30, 27, 30, 35] # underlying data type is a List
  "stats": {
      "ppg": "35.4",
      "apg": "6.3"
  }
  "_version" : 8
}
```

但是传入的请求尝试使用以下图像更新该项目，从而再次出现版本不匹配的情况：

```
{
  "id" : 1,
  "name" : "Nadia",
  "stats": {
      "ppg": "25.7",
      "rpg": "6.9"
  }
  "_version" : 3
}
```

现在，在这种情况下，我们可以看到服务器中已经存在的字段丢失（兴趣、分数、球衣）。此外，正在编辑映射“统计”中的“ppg”值，添加了一个新值“rpg”，并省略了“apg”。Automerge 保留已省略的字段（注意：如果打算删除字段，则必须使用匹配版本再次尝试请求），以便它们不会丢失。它也会将相同的规则应用于映射中的字段，因此对“ppg”的更改将被拒绝，而“apg”被保留，并添加一个新字段“rpg”。存储在服务器中的结果项目现在将显示为：

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 5,
  "interests" : ["breakfast", "lunch", "dinner", "brunch"] # underlying data type is a Set
  "points": [24, 30, 27, 30, 35] # underlying data type is a List
  "stats": {
      "ppg": "35.4",
      "apg": "6.3",
      "rpg": "6.9"
  }
  "_version" : 9
}
```

## Lambda
<a name="lambda"></a>

有几种 Lambda 解析策略可供选择：
+  `RESOLVE`：将现有项目替换为响应有效载荷中提供的新项目。您一次只能对单个项目重试同一操作。当前受 DynamoDB `PutItem` 和 `UpdateItem` 支持。
+  `REJECT`：拒绝变更并返回错误以及 GraphQL 响应中的现有项目。当前受 DynamoDB `PutItem`、`UpdateItem` 和 `DeleteItem` 支持。
+  `REMOVE`：删除现有项目。当前受 DynamoDB `DeleteItem` 支持。

 **Lambda 调用请求** 

 AWS AppSync DynamoDB 解析器调用中指定的 Lambda 函数。`LambdaConflictHandlerArn`它将使用在数据来源上配置的相同 `service-role-arn`。调用的负载具有以下结构：

```
{
    "newItem": { ... },
    "existingItem": {... },
    "arguments": { ... },
    "resolver": { ... },
    "identity": { ... }
}
```

字段定义如下：

** `newItem` **  
预览项目（如果变更成功）。

** `existingItem` **  
该项目当前位于 DynamoDB 表中。

** `arguments` **  
来自 GraphQL 变更的参数。

** `resolver` **  
有关 AWS AppSync 解析器的信息。

** `identity` **  
有关调用方的信息。如果使用 API 密钥进行访问，则该字段设置为 null。

有效负载示例：

```
{
    "newItem": {
        "id": "1",
        "author": "Jeff",
        "title": "Foo Bar",
        "rating": 5,
        "comments": ["hello world"],
    },
    "existingItem": {
        "id": "1",
        "author": "Foo",
        "rating": 5,
        "comments": ["old comment"]
    },
    "arguments": {
        "id": "1",
        "author": "Jeff",
        "title": "Foo Bar",
        "comments": ["hello world"]
    },
    "resolver": {
        "tableName": "post-table",
        "awsRegion": "us-west-2",
        "parentType": "Mutation",
        "field": "updatePost"
    },
    "identity": {
         "accountId": "123456789012",
         "sourceIp": "x.x.x.x",
         "username": "AIDAAAAAAAAAAAAAAAAAA",
         "userArn": "arn:aws:iam::123456789012:user/appsync"
    }
}
```

 **Lambda 调用响应** 

对于 `PutItem` 和 `UpdateItem` 解决冲突

 `RESOLVE` 变更。响应必须采用以下格式。

```
{
    "action": "RESOLVE",
    "item": { ... }
}
```

`item` 字段表示将用于替换底层数据来源中的现有项目的对象。如果在 `item` 中包含主键和同步元数据，则将被忽略。

 `REJECT` 变更。响应必须采用以下格式。

```
{
    "action": "REJECT"
}
```

对于 `DeleteItem` 冲突解决

 `REMOVE` 项目。响应必须采用以下格式。

```
{
    "action": "REMOVE"
}
```

 `REJECT` 变更。响应必须采用以下格式。

```
{
    "action": "REJECT"
}
```

下面的示例 Lambda 函数检查谁进行调用以及解析器名称。如果是由 resolver `jeffTheAdmin` 创建`REMOVE`的，则是 DeletePost解析者的对象`RESOLVE`，或者是与 Update/Put 解析者的新项目冲突。如果不是，则变更是 `REJECT`。

```
exports.handler = async (event, context, callback) => {
    console.log("Event: "+ JSON.stringify(event));

    // Business logic goes here.
    var response;
    if ( event.identity.user == "jeffTheAdmin" ) {
        let resolver = event.resolver.field;

        switch(resolver) {
            case "deletePost":
                response = {
                    "action" : "REMOVE"
                }
                break;

            case "updatePost":
            case "createPost":
                response = {
                    "action" : "RESOLVE",
                    "item": event.newItem
                }
                break;
            default:
                response = { "action" : "REJECT" };
        }
    } else {
        response = { "action" : "REJECT" };
    }

    console.log("Response: "+ JSON.stringify(response));
    return response;
}
```

## 错误
<a name="errors"></a>

以下是在冲突解决过程中可能出现的错误列表：

** `ConflictUnhandled` **  
冲突检测发现版本不匹配情况，并且冲突处理程序拒绝变更。  
示例：使用乐观并发冲突处理程序解决冲突。或者，Lambda 冲突处理程序返回 `REJECT`。

** `ConflictError` **  
尝试解决冲突时发生内部错误。  
示例：Lambda 冲突处理程序返回格式错误的响应。或者，无法调用 Lambda 冲突处理程序，因为找不到提供的资源 `LambdaConflictHandlerArn`。

** `MaxConflicts` **  
已达到解决冲突的最大重试次数。  
示例：同一对象上的并发请求太多。在解决冲突之前，另一个客户端将对象更新为新版本。

** `BadRequest` **  
客户端尝试更新元数据字段（`_version`、`_ttl`、`_lastChangedAt`、`_deleted`）。  
示例：客户端尝试使用更新变更来更新对象的 `_version`。

** `DeltaSyncWriteError` **  
写入增量同步记录失败。  
示例：变更成功，但尝试写入增量同步表时出现内部错误。

** `InternalFailure` **  
出现内部错误。

**`UnsupportedOperation`**  
不支持的操作 “*X*”。数据源版本控制仅支持以下操作（TransactGetItems、、、扫描PutItem、查询BatchGetItem、、、、GetItemDeleteItemUpdateItem、同步）。  
示例：在 detection/resolution 启用冲突的情况下使用某些事务处理和批量操作。目前不支持这些操作。

## CloudWatch 日志
<a name="cloudwatch-logs"></a>

如果 AWS AppSync API 启用了 CloudWatch 日志，其日志记录设置为字段级日志`enabled`，字段级日志的日志级别设置为`ALL`，则 AWS AppSync 会向日志组发送冲突检测和解决方案信息。有关日志消息格式的信息，请参阅[冲突检测和同步日志记录的文档](monitoring.md#aws-appsync-monitoring-conflict-detection-and-sync-logging)。