什么是从DynamoDB中删除大量项目的推荐方法?

154

我正在编写一个简单的DynamoDB日志记录服务。

我有一个以用户ID哈希和时间戳(Unix纪元整数)范围为键的日志表。

当服务的用户终止其帐户时,我需要删除表中的所有项目,而不考虑范围值。

在考虑可能有数百万个要删除的项目的情况下,如何推荐执行此类操作?

据我所见,我的选择是:

A:执行扫描操作,在每个返回的项目上调用delete,直到没有任何项目剩余

B:执行BatchGet操作,再次在每个项目上调用delete,直到没有任何项目剩余

对我来说,这两种方法都看起来很糟糕,因为它们需要很长时间。

我理想情况下希望做的是调用LogTable.DeleteItem(user_id) - 不提供范围,并让它为我删除所有内容。

9个回答

78
我理想情况下想要做的是调用LogTable.DeleteItem(user_id) - 不提供范围,让它替我删除所有内容。
这确实是一个可以理解的请求;我可以想象AWS团队可能会随着时间的推移添加类似于这样的高级操作(他们有一个从有限功能集开始,并根据客户反馈评估扩展的历史),但至少以下是您应该做的以避免全面扫描的成本:
  1. 使用Query而不是Scan来检索所有user_id的项目-这适用于使用组合哈希/范围主键的情况,因为HashKeyValueRangeKeyCondition是此API中的单独参数,前者仅针对复合主键的哈希组件的属性值。请注意,您需要像往常一样处理查询API分页,参见ExclusiveStartKey参数:

    • 如果之前的查询操作由于结果集大小或限制参数而被中断,则该项的主键可以作为LastEvaluatedKey提供给新的查询请求,以从那一点继续操作。
  2. 循环遍历所有返回的项目,并像往常一样使用DeleteItem删除

    • 更新:对于这种用例,最适合使用BatchWriteItem(有关详细信息,请参见下文)。

更新

正如 ivant 所强调的那样,BatchWriteItem 操作允许您在单个 API 调用中放置或删除多个表中的多个项目 [我强调]:

要上传一个项目,可以使用 PutItem API,要删除一个项目,可以使用 DeleteItem API。但是,当您想要上传或删除大量数据时,例如从 Amazon Elastic MapReduce (EMR) 上传大量数据或将数据从另一个数据库迁移到 Amazon DynamoDB 中,此 API 提供了一种高效的替代方法。

请注意,这仍然有一些相关限制,最值得注意的是:

  • 单次请求中的最大操作数 — 您可以指定最多25个put或delete操作;但是,总请求大小不能超过1 MB(HTTP有效负载)。

  • 不是原子操作 — 在BatchWriteItem中指定的各个操作是原子操作;但是BatchWriteItem作为一个整体是“尽力而为”的操作,而不是原子操作。也就是说,在BatchWriteItem请求中,有些操作可能成功,而有些操作可能失败。[...]

然而,这显然为像手头这个用例这样的用例提供了潜在的重大收益。


4
我认为在第二步中使用批量删除是有意义的(它被“伪装”成批量写操作)。 - ivant
1
@ivant - 非常感谢您的提示,我当时确实忽略了BatchWriteItem的这种“屏蔽”删除功能;我已经相应地更新了答案。 - Steffen Opel
使用 BatchWriteItem 删除项目时,需要通过 TableWriteItems 指定项目。 - Neil
1
BatchWriteItem的链接现在是http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html。 - Tony
5
我知道这篇文章可能已经有些过时了,而且原作者也没有提到具体的语言SDK,但是在Python中,boto3.resource.Table API 中有一个高级别的batch_writer()函数,可以“自动处理缓冲和分批发送条目。此外,批量写入器还会自动处理任何未处理的项,并根据需要重新发送它们”,即它是一个包装工具,用于管理烦人的部分,基于 BatchWriteItem 实现。更多信息请查看:https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Table.batch_writer - Davos
对于Python,Boto3的batch_writer是进行批量删除的最佳方式。我在GitHub上放了一个工作示例https://github.com/awsdocs/aws-doc-sdk-examples - Laren Crawford

54

根据DynamoDB文档,您可以直接删除整张表。

如下所示:

“删除整个表比逐个删除项目要高效得多,因为您进行了与放置操作一样多的删除操作,从而将写吞吐量翻倍。”

如果您只想删除部分数据,则可以为每个月、年或类似的内容制作单独的表。这样,您可以删除“上个月”并保留其余数据。

以下是使用AWS SDK在Java中删除表的方法:

DeleteTableRequest deleteTableRequest = new DeleteTableRequest()
  .withTableName(tableName);
DeleteTableResult result = client.deleteTable(deleteTableRequest);

9
我也喜欢这个答案,但是请注意:这可能会在您的系统中创建许多表格,并且我们按表格预配付费。因此,在月底结束后(如果您的表格是按月计费),您需要减少预配量,同时保留该表格未被删除。 - Sergio Marcelo C Figueiredo
3
同意这个答案,如果你需要删除表中的所有记录,可以使用这种方法。但是这里的问题是要删除用户基础条目,而不是整个表。 - Ihtsham Minhas
1
考虑到DynamoDB的定价,为每个用户单独创建一个表格将会非常昂贵。每月一个表格实际上会使情况变得更糟。这显然是针对不同、非常具体问题的答案。 - André Werlang
26
如果您使用自动配置工具(例如CloudFormation)来管理表格,将删除该表格可能也不是一种吸引人的选择。如果您手动删除了表格,我不知道有简单的方法让CloudFormation重新创建该表格。 - brabster
2
这种方法需要相当长的时间来删除和重新创建(在需要时)表格,使其在整个时间内不可用。问题明确说明了要删除用户数据,将其拆分为单独的每个用户表是不切实际的。 - André Werlang
显示剩余4条评论

24
如果您想在一段时间后删除项目,例如一个月后,只需使用Time To Live选项。它将不会计算写入单位。 在您的情况下,我会在日志过期时添加ttl并保留那些在用户被删除后的日志。TTL将确保最终删除日志。
当对表启用Time To Live时,后台任务将检查条目的TTL属性以查看其是否已过期。 DynamoDB通常会在48小时内删除已过期的项目。条目真正过期后被删除的确切持续时间取决于工作负载的性质和表的大小。已过期但未被删除的项目仍将出现在读取、查询和扫描中。这些项目仍然可以更新,成功更新更改或删除过期属性的操作将受到尊重。 https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/howitworks-ttl.html

1
添加TTL是一种“更新”(写操作)。我不确定与“删除”相比进行“更新”是否有任何收益。 - Tomer
你可以使用原始写入将数据插入,也可以使用任何其他更新操作进行更新。当然,如果您有大量数据,然后想要删除它,这不是一个选项。但是,对于您可以为插入或更新的数据设置ttl的情况,这是一个有效的选项。 - Lukas Liesis
2
我同意,如果已经配置了TTL并且清理可以等待48小时,那绝对是最佳选择。如果我表达不清,敬请谅解。 - Tomer

6
这个问题的答案取决于项目数量、大小和预算。基于这些,我们有以下三种情况:
1- 表中项目数量和大小不多。那么,就像Steffen Opel所说的那样,您可以使用查询而不是扫描来检索用户ID的所有项目,然后循环遍历所有返回的项目,并使用DeleteItemBatchWriteItem来删除它们。但请记住,这里可能会消耗大量吞吐量容量。例如,假设您需要从DynamoDB表中删除1000个项目。每个项目的大小为1 KB,导致大约1 MB的数据。此批量删除任务将需要2000个写入吞吐量单位进行查询和删除。为了在10秒钟内执行此数据加载(在某些应用程序中甚至不被认为是快速的),您需要将表的规定写入吞吐量设置为200个写入吞吐量单位。如您所见,如果项目数量较少或项目大小较小,则可以使用此方法。
2- 表中有很多项目或非常大的项目,我们可以根据时间将它们存储到不同的表中。那么,就像Jonathan所说的那样,您可以直接删除该表。这种方法更好,但我不认为它符合您的情况。因为您想要删除用户的所有数据,无论日志的创建时间如何,所以在这种情况下,您无法删除特定的表。如果您想为每个用户拥有单独的表,则我认为如果用户数量很多,则会非常昂贵,对于您的情况来说不切实际。
3- 如果您有大量数据,并且无法将热和冷数据分成不同的表,并且需要经常进行大规模删除,则不幸的是,DynamoDB根本不是一个好选择。它可能变得更加昂贵或者非常慢(取决于您的预算)。在这些情况下,我建议您为数据找到另一个数据库。

3

我们没有截断Dynamo表的选项。我们必须删除表并重新创建。 DynamoDB收费基于ReadCapacityUnits和WriteCapacityUnits。如果我们使用BatchWriteItem函数删除所有项目,它将使用WriteCapacityUnits。因此最好删除特定记录或删除表并重新开始。


1

这是一个递归函数,用于删除所有带有 batchWriteItems 的项目。定义表的键模式和表名,并调用 clearTable

var AWS = require("aws-sdk");
var docClient = new AWS.DynamoDB.DocumentClient();

const TABLE_NAME = ""
const TABLE_PRIMARY_KEY = ""

const clearTable = async () => {

    const batch = await getItemBatch();

    await recursiveDeleteTableItems(batch)

}

const recursiveDeleteTableItems = async (batch) => {

    if(batch && batch.length > 0) {
      await deleteItemBatch(batch)
    } else {
      return
    }

    const newItemBatch = await getItemBatch()

    await recursiveDeleteTableItems(newItemBatch)

}

const deleteItemBatch = async (batch) => {

   const deleteOperations = batch.map( i => ({ 
     "DeleteRequest": { 
       "Key": { 
        [TABLE_PRIMARY_KEY] : i.KEY_VALUE
       }
     }
   }))

   return new Promise(async (resolve, reject) => {

     const params = {
       "RequestItems": {
         [TABLE_NAME]: deleteOperations
       }
     }

     docClient.batchWrite(params, (err, data) => {

       if (err) {
         reject(`Unable to query. Error: ${err} ${JSON.stringify(err, null, 2)}`);
         return
       }

       resolve(data)

      })

    })

}

const getItemBatch = async () => {

  var params = {
    TableName: TABLE_NAME,
    Limit: 25 // match batchWriteItem
  };

  return new Promise(async (resolve, reject) => {

    docClient.scan(params, async function (err, data) {

        if (err) {
            reject(`Unable to query. Error: ${err} ${JSON.stringify(err, null, 2)}`);
            return
        }

        resolve(data.Items)

      });
  });

}

1

您是否考虑使用测试来传递变量?例如:

测试输入可能如下所示:

{
  "TABLE_NAME": "MyDevTable",
  "PARTITION_KEY": "REGION",
  "SORT_KEY": "COUNTRY"
}

调整了您的代码以接受以下输入:
const AWS = require('aws-sdk');
const docClient = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012-08-10' });

exports.handler = async (event) => {
    const TABLE_NAME = event.TABLE_NAME;
    const PARTITION_KEY = event.PARTITION_KEY;
    const SORT_KEY = event.SORT_KEY;
    let params = {
        TableName: TABLE_NAME,
    };
    console.log(`keys: ${PARTITION_KEY} ${SORT_KEY}`);

    let items = [];
    let data = await docClient.scan(params).promise();
    items = [...items, ...data.Items];
    
    while (typeof data.LastEvaluatedKey != 'undefined') {
        params.ExclusiveStartKey = data.LastEvaluatedKey;

        data = await docClient.scan(params).promise();
        items = [...items, ...data.Items];
    }

    let leftItems = items.length;
    let group = [];
    let groupNumber = 0;

    console.log('Total items to be deleted', leftItems);

    for (const i of items) {
        // console.log(`item: ${i[PARTITION_KEY] } ${i[SORT_KEY]}`);
        const deleteReq = {DeleteRequest: {Key: {},},};
        deleteReq.DeleteRequest.Key[PARTITION_KEY] = i[PARTITION_KEY];
        deleteReq.DeleteRequest.Key[SORT_KEY] = i[SORT_KEY];

        // console.log(`DeleteRequest: ${JSON.stringify(deleteReq)}`);
        group.push(deleteReq);
        leftItems--;

        if (group.length === 25 || leftItems < 1) {
            groupNumber++;

            console.log(`Batch ${groupNumber} to be deleted.`);

            const params = {
                RequestItems: {
                    [TABLE_NAME]: group,
                },
            };

            await docClient.batchWrite(params).promise();

            console.log(
                `Batch ${groupNumber} processed. Left items: ${leftItems}`
            );

            // reset
            group = [];
        }
    }

    const response = {
        statusCode: 200,
        //  Uncomment below to enable CORS requests
        headers: {
            "Access-Control-Allow-Origin": "*"
        },
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};

1

我在DynamoDb中删除表中所有行的方法是使用DynamoDb的ScanAsync将所有行都取出,然后将结果列表提供给DynamoDb的AddDeleteItems。以下C#代码对我很有效。

        public async Task DeleteAllReadModelEntitiesInTable()
    {
        List<ReadModelEntity> readModels;

        var conditions = new List<ScanCondition>();
        readModels = await _context.ScanAsync<ReadModelEntity>(conditions).GetRemainingAsync();

        var batchWork = _context.CreateBatchWrite<ReadModelEntity>();
        batchWork.AddDeleteItems(readModels);
        await batchWork.ExecuteAsync();
    }

注意:如果使用YAML/CloudFormation创建表格,则从Web控制台删除表格并再次创建可能会导致问题。

1

更新一下,DynamoDB控制台发布了一个新版本,其中包含一个名为PartiQL编辑器的新功能。它是用于DynamoDB操作的类似SQL的编辑器。

删除特定记录

DELETE FROM <Table-Name> WHERE id=some-Id;

缺点:一次只能删除一个项目


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接