在生产 Java SDK 中向 Table 添加和删除属性的 DynamoDB 解决方案

DynamoDB solution to adding and removing attributes to Table in Production Java SDK

问题:我们在生产中使用了多个 lambda 和 dynamodb table,在发布新版本代码时,我们有时会删除一个属性或向 table [添加属性=33=]es(Java 代码使用 com.amazonaws.services。dynamodbv2.datamodeling)高级 api。当我们部署新版本的代码并查询 table 时,如果现有项目不存在新属性,或者我们从代码中删除属性。它破坏了代码,因为我们的 Item 对象与生产数据不一致。

我们希望通过添加具有默认值的额外属性或删除现有项目的属性来避免处理产品中的数据。在我们出于与竞争条件和一致性有关的各种原因部署新版本之前。如果我们在代码级别处理它会更可取,如果属性不存在会自动添加一个默认值。或者让代码忽略 item/table class 中未定义的属性。这可能使用高级 java sdk api?

我们想出的另一个解决方案是创建一个提供增量(代码项对象和产品中数据之间的变化)的服务,该服务由在部署新服务时处理数据的流量前 lambda 执行lambda 的版本。但是,我们希望避免这种情况。

package com.ourcompany.module.dynamodb.items;

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBVersionAttribute;
import lombok.Data;

@Data
@DynamoDBTable(tableName = "Boxes")
public class BoxItem {

@DynamoDBHashKey(attributeName = "boxID")
private String channelID;

#This is the field we added, the previous version did not have this field, in prod we have many items without this attribute
@DynamoDBAttribute(attributeName = "lastTimeAccess")
private String lastTimeAccess;

@DynamoDBAttribute(attributeName = "initTime")
private String initTime;

@DynamoDBAttribute(attributeName = "boxIDhash")
private String streamBoxIDHash;

@DynamoDBAttribute(attributeName = "CFD")
private String cfd;

@DynamoDBAttribute(attributeName = "originDomain")
private String originDomain;

@DynamoDBAttribute(attributeName = "lIP")
private String lIP;

@DynamoDBAttribute(attributeName = "pDomain")
private String pDomain;

以上是我们更改后的项目,添加了属性。

package com.ourcompany.shared.module.repository.dynamob;

import ...

public class DynamoDbRepository<Item, Key> {

private final DynamoDBMapper mapper;
private static final Logger logger = LogManager.getLogger(DynamoDbRepository.class);

@Inject
public DynamoDbRepository() {
    val client = AmazonDynamoDBClientBuilder
            .standard()
            .withRegion(Regions.US_EAST_1) // TODO: hardcoded now
            .withRequestHandlers(new TracingHandler(AWSXRay.getGlobalRecorder()))
            .build();


    DynamoDBMapperConfig dynamoDBMapperConfig = new DynamoDBMapperConfig.Builder()
                                                   .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES)
                                                   .withTableNameResolver(new DynamoDBTableNameResolver())
                                                   .build();

    mapper = new DynamoDBMapper(client, dynamoDBMapperConfig);

}
/*
* Many accessor methods are listed here below is the one where we have issue,
*/
public List<Item> findBy(Map<String, Condition> filter, final Class<Item> clazz) throws Exception {
    try {
        logger.trace("DynamoDbRepository findBy(filter, class)");
        val scanExpression = new DynamoDBScanExpression().withScanFilter(filter).withConsistentRead(true);
        PaginatedScanList<Item> ls = mapper.scan(clazz, scanExpression);
        ls.loadAllResults();
        return ls;
    } catch (Exception ex) {
        logger.trace(ex.getMessage());
        throw handleException(ex);
    }
}

以上是我们的 Dynamob DB 映射器 class,但只有所讨论的方法。我们能够通过日志记录追踪到 logger.trace("DynamoDbRepository findBy(filter, class)"); 行,我们知道问题出现在映射器中。然而它并没有吐出异常,所以我们无法看到实际的错误。我们必须通过清除 prod 中 table 中的所有数据来解决这个问题,然后让新版本的代码用属性重新填充条目并且代码有效。

对于小型 window 或者如果您 运行 长期使用拆分测试,您将遇到此问题。

我们通过以下方式解决:

  1. 无论哪个 lambda 使用属性,请确保他们检查 属性 是否存在并对其进行处理。如果所需的 属性 不存在,则抛出错误并假定失败。如果您在事务路径中使用它,这可能是个问题,但会让您知道失败的原因以及如何修复它。这是用于拆分测试。
  2. 为至少落后一个版本的向后兼容性设计您的代码。确保在所需版本到位后删除代码。
  3. 如果window较小且负载不重,您可以让服务无法捕获较新的版本。

希望对您有所帮助。

只是关于这个问题的更新。在听取了@zapl 关于尝试打印堆栈跟踪的建议后,我发现 AWS DynamoDB Mapper 或 SDK 的工作方式完全没有问题。我期待从 SDK 捕获一些堆栈跟踪,但没有,经过更仔细的跟踪我发现 Java 开发人员误诊了这个问题,真正的问题是他们有逻辑来过滤依赖于新领域。因此,故事的教训,架构代码以实现向后兼容至少落后一个版本!