如何在DynamoDB中生成UUID?

31

在我的数据库方案中,我需要一个自动增量主键。我如何实现这个功能?

提示:为了访问DynamoDB, 我使用了Node.js的模块dynode


请查看Twitter的雪花算法 - Tobias P.
11个回答

28

免责声明:我是Dynamodb-mapper项目的维护者。

自增键的直观工作流程:

  1. 获取最后一个计数器位置
  2. 加1
  3. 使用新数字作为对象的索引
  4. 保存新的计数器值
  5. 保存对象

这只是为了解释基本思路。永远不要这样做,因为这不是原子操作。在某些工作负载下,由于它不是原子操作,您可能会将相同的ID分配给2个或更多不同的对象,从而导致数据丢失。

解决方案是使用UpdateItem原子ADD操作和ALL_NEW

  1. 原子生成ID
  2. 使用新数字作为对象的索引
  3. 保存对象

在最坏的情况下,应用程序在对象保存之前崩溃,但永远不会有重复分配ID的风险。

还有一个问题:要存储上次的ID值在哪里?我们选择:

{
    "hash_key"=-1, #0 was judged too risky as it is the default value for integers.
    "__max_hash_key__y"=N
}

当然,为了能够可靠地工作,所有插入数据的应用程序都必须知道该系统,否则您可能会(再次)覆盖数据。

最后一步是自动化该过程。例如:

When hash_key is 0:
    atomically_allocate_ID()
actual_save()

有关实现细节(使用Python,抱歉)请参见https://bitbucket.org/Ludia/dynamodb-mapper/src/8173d0e8b55d/dynamodb_mapper/model.py#cl-67

说实话,我的公司没有在生产中使用它,因为大多数情况下最好找到另一个关键词,例如对于用户,ID,对于交易,日期时间...

我在dynamodb-mapper的文档中编写了一些示例,并且可以轻松地推广到Node.JS

如果您有任何问题,请随时提出。


1
很好,但现在我正在使用时间戳和随机数。PS非常感谢这个出色的答案,并感谢改进DynamoDB。 - NiLL
4
另一个解决方法是使用Redis计数器,这样可以大大减少对DynamoDB的压力和操作次数。当您执行插入时,请求Redis当前计数器并将其增加。如果Redis没有该计数器,则会请求DynamoDB获取最新的ID,然后将其存储。 - Daniel Aranda
1
“uuid” npm包的使用是一种更好、更快、更便宜的解决方案。即使在分布式负载下使用“ALL_NEW”标志来计数,也会导致更多的RCU消耗。 - fdaugan

15

另一种方法是使用UUID生成器作为主键,因为这些几乎不可能发生冲突。

在我看来,跨高可用性的DynamoDB表合并主键计数器时出现错误的可能性比生成的UUID冲突要大。

例如,在Node中:

npm install uuid

var uuid = require('uuid');

// Generate a v1 (time-based) id
uuid.v1(); // -> '6c84fb90-12c4-11e1-840d-7b25c5ee775a'

// Generate a v4 (random) id
uuid.v4(); // -> '110ec58a-a0f2-4ac4-8393-c866d813b8d1'

来自SO的答案


1
UUID是分布式系统更为强大的方法,现在已经内置于Node v15.6中,因此您可以简单地使用crypto.randomUUID()文档)。 - tenni
uuid不是唯一的。uuid是一个随机生成的字符串,它很可能是唯一的,但不一定是唯一的,因此您仍然需要手动检查生成的id是否已经存在,如果是,则生成一个新的id。 - aprilmintacpineda

6
如果您可以接受增量ID中出现的间隙,并且只需要大致对应添加行的顺序,那么您可以自己创建:创建一个名为NextIdTable的单独表,其中包含一个主键(数字),称之为Counter。
每次想要生成新ID时,您需要执行以下操作:
- 在NextIdTable上执行GetItem以读取Counter的当前值->curValue。 - 在NextIdTable上执行PutItem以将Counter的值设置为curValue + 1。使其成为有条件的PutItem,以便在Counter的值更改时失败。 - 如果有条件的PutItem失败,则意味着其他人与您同时进行此操作。重新开始。 - 如果成功,则curValue是您的新唯一ID。
当然,如果在实际应用该ID之前,您的进程崩溃了,那么您将会“泄漏”它,并且您的ID序列中将会有一个间隙。如果您正在与其他进程同时进行此操作,则其中一个进程将获得值39,另一个进程将获得值40,并且无法保证它们将实际应用于数据表的顺序;获得40的人可能会在获得39的人之前写入它。但是,这确实给您提供了一个粗略的顺序。
node.js中条件PutItem的参数详见此处http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/frames.html#!AWS/DynamoDB.html。如果您之前从Counter中读取了值38,则您的条件PutItem请求可能如下所示。
var conditionalPutParams = {
    TableName: 'NextIdTable',
    Item: {
        Counter: {
            N: '39'
        }
    },
    Expected: {
        Counter: {
            AttributeValueList: [
                {
                    N: '38'
                }
            ],
            ComparisonOperator: 'EQ'
        }
    }
};

❤️ 爱这个答案! - Resist Design

5
如果您使用Java编码,DynamoDBMapper现在可以代表您生成唯一的UUID。

DynamoDBAutoGeneratedKey

将分区键或排序键属性标记为自动生成。 DynamoDBMapper将在保存这些属性时生成随机UUID。只有字符串属性才能标记为自动生成的键。

像这样使用DynamoDBAutoGeneratedKey注释。
@DynamoDBTable(tableName="AutoGeneratedKeysExample")
public class AutoGeneratedKeys { 
    private String id;

    @DynamoDBHashKey(attributeName = "Id")
    @DynamoDBAutoGeneratedKey
    public String getId() { return id; }
    public void setId(String id) { this.id = id; } 

如上例所示,您可以在同一属性上应用DynamoDBAutoGeneratedKey和DynamoDBHashKey注释,以生成唯一的哈希键。


5
@yadutaf的回答中还有一个补充

AWS支持原子计数器

创建一个单独的表格(order_id),其中包含一行,保存最新的订单号:

+----+--------------+
| id | order_number |
+----+--------------+
|  0 |         5000 |
+----+--------------+

这将允许通过AWS DynamoDB在回调中递增order_number并获得递增后的结果:

1

config={
  region: 'us-east-1',
  endpoint: "http://localhost:8000"
};
const docClient = new AWS.DynamoDB.DocumentClient(config); 

let param = {
            TableName: 'order_id',
            Key: {
                "id": 0
            },
            UpdateExpression: "set order_number = order_number + :val",
            ExpressionAttributeValues:{
                ":val": 1
            },
            ReturnValues: "UPDATED_NEW"
        };
        
       
docClient.update(params, function(err, data) {
   if (err) {
                console.log("Unable to update the table. Error JSON:", JSON.stringify(err, null, 2));
   } else {
                console.log(data);
                console.log(data.Attributes.order_number); // <= here is our incremented result
    }
  });

请注意,在一些罕见情况下,您的呼叫点与 AWS API 之间的连接可能存在问题。这将导致 dynamodb 行被递增,同时您会收到连接错误。因此,可能会出现一些未使用的递增值。
您可以在表格中使用递增的 data.Attributes.order_number,例如将 {id: data.Attributes.order_number, otherfields:{}} 插入 order 表格。

4

我认为在分布式系统中,像SQL中的自动递增方式是不可能的。因此,我使用PHP生成我的UUID,这样可以完成相同的工作。你也可以尝试使用JavaScript实现类似的功能


2

从性能角度来看,自动递增不是很好,因为它会使某些分片过载而保持其他分片空闲,如果您要存储数据到DynamoDB中,它并不能实现均匀分布。

awsRequestId 看起来实际上是V.4 UUID(随机生成),以下是代码片段:

import uuid print(str(uuid.uuid4()))

请注意:本文仅供参考,具体实现可能会有所不同。

exports.handler = function(event, context, callback) {
    console.log('remaining time =', context.getRemainingTimeInMillis());
    console.log('functionName =', context.functionName);
    console.log('AWSrequestID =', context.awsRequestId);
    callback(null, context.functionName);
};

如果您想自己生成UUID,可以使用https://www.npmjs.com/package/uuidUlide,根据RFC-4122生成不同版本的UUID:

  • V1(基于时间戳)
  • V3(命名空间)
  • V4(随机)

对于Go开发人员,您可以使用这些来自Google's UUIDPbormanSatori的包。Pborman在性能方面更好,查看这些文章和基准测试以获取更多详细信息。

有关通用唯一标识符规范的更多信息,请单击此处


2

这个在生产中使用有多稳定?它的正常运行统计数据是什么? - Mirage
@Mirage在statuscake.com上的最近1000天统计数据:https://www.statuscake.com/App/button/index.php?Track=vAZJhwOtwc&Days=1000&Design=1 - yegor256

0

0
如果您正在使用NoSQL DynamoDB,则可以使用Dynamoose ORM轻松设置默认唯一标识符。以下是一个简单的用户创建示例:
// User.modal.js
const dynamoose = require("dynamoose");

const userSchema = new dynamoose.Schema(
  {
    id: {
      type: String,
      hashKey: true,
    },
    displayName: String,
    firstName: String,
    lastName: String,
  },
  { timestamps: true },
);

const User = dynamoose.model("User", userSchema);

module.exports = User;

// 用户控制器.js

const { v4: uuidv4 } = require("uuid");    
const User = require("./user.model");

exports.create = async (req, res) => {
  const user = new User({ id: uuidv4(), ...req.body }); // set unique id
  const [err, response] = await to(user.save());
  if (err) {
    return badRes(res, err);
  }
  return goodRes(res, reponse);
};

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