使用雪花算法生成独特的ID
#twitter #go #uuid

Original post

唯一的ID几乎在各处和每个应用程序中都使用。它们用于识别用户,产品,交易,会话等。这样,我们可以将交易分配给特定用户,而不是误认为另一个用户。在本文中,我将向您展示如何在应用程序中生成唯一的ID并给他们第二次使用。

如何获得独特的ID?

每个用户获取唯一ID的方法有几种方法。显然有些人可能会做
之类的事情

const min = 1000000
const max = 9999999999

const id = Math.floor(Math.random()*max) + min;

尽管我并不真正建议它,因为它显然不是真正唯一的。您可以并且将获得两次相同的ID生成的最终。当然,您可以在数据库中检查该ID是否已经存在并生成新的数据库,但是您可以获得我的观点。

自动增加整数

尽管这是一种非常基本的和广泛使用的方法,但我宁愿不使用它。关于数据库中这种外观的一个示例将与之接近:

id 用户名 密码 create_at
1 约翰 some_hash 1667329399
2 jane some_hash 1667329404
3 杰克 some_hash 1667329412

在这里,我们可以清楚地看到ID每次逐一增加。

UUID

UUID是获得唯一ID的非常普遍的方法。它们是唯一的128位数字。 UUID非常有用,因为它们是唯一的,是的,有很小的机会您可以获得两次同一ID ,并且它们不是顺序的。它们也很容易生成。例如,我们可以使用以下命令通过终端生成一个:

~
🕙 20:07:00 ❯ uuidgen
67DB31FB-1887-4372-A8B0-E87C092D7D11

正如您预期的那样,此UUID会在您的数据库中占用空间,但这并不重要 ,当您使用UUID时,您的数据库最终看起来像这样:

id 用户名 密码 create_at
45915234-74BE-499A-B7D5-D6D4882DA5B7 约翰 some_hash 1667329399
885A26B8-C7CA-4C84-A871-0E89AF4997BD jane some_hash 1667329404
689E682B-35C8-4594-A0D7-5893626B7B1D 杰克 some_hash 1667329412

现在让我向您介绍 SpaceFlake ,这是一种可轻松创建唯一ID的分布式发电机;受Twitter's Snowflake

的启发

什么是太空粉?

太空粉非常简单,它是基于Twitter的雪花算法的ID,它们正在积极使用,并且略微编辑以适合我的需求。该算法很简单:

“太空粉的结构”太空粉的结构

您可以看到,空间粉由4个主要部分组成:

  • timestamp :时间戳是自碱时期以来经过的毫秒。
  • 节点ID :节点ID代表生成空格flake的节点的ID。它是0到31之间的数字。
  • Worker ID :工人ID代表生成太空粉的工人的ID。它是0到31之间的数字。
  • 序列:序列是工人在节点上产生的空格数量。它是0到4095之间的数字。

有了这些算法的规格,我们可以从理论上 生成up to 4 million unique IDs每毫秒ð

工人和节点=太空粉网络

太空粉网络是一个非常基本的概念,您拥有多个独立节点本身由多个工人组成。这些工人是可以生成太空碎片的工人。

理想情况下,太空粉网络代表您的整个应用程序或公司。每个节点代表公司内部的一个服务/服务器或应用程序,每个工人代表一个可以为特定目的生成空格flake的过程。这样,您可以轻松地识别通过查看其节点ID和Worker ID来生成空间flake。

“太空粉网络

在上面的示例中,我们可以将节点1 视为应用程序的 API/后端 Worker(ID:1)将负责为用户ID 生成空间碎片。 工作者(ID:2)将负责为博客文章ID生成空间粉。这样,我们可以轻松地确定生成空间的位置以及出于什么目的。
然后,我们还拥有节点2 ,它可能是整个基础架构的记录系统,并且生成的ID将由 Worker(ID:1)。 p>

最后,您可以根据需要自由使用它们,只需确保使用这些节点和工人能够识别太空粉 - 这也是其背后的想法。

我为什么要创建空间碎片?

现在您可以问我

“为什么要创建这个?”

事实上,很多人已经问我这个...

要解释我的决定,让我们回到上面的数据库的示例。我故意在数据库中添加了一个created_at字段,因为知道何时创建用户或帖子非常有用。有了空间粉和雪花,您将拥有ID内置的创建时间((spaceflake >> 22) + BASE_EPOCH),因此您无需将其单独存储在数据库的新列中。这是非常有用的,因为它可以删除不必要的列,并且还使通过创建时间对数据进行排序变得更加容易。然后,您的数据库看起来像:

id 用户名 密码 create_at
1037425364332843009 约翰 some_hash 1667329399
103742545856270978 jane some_hash 1667329404
1037425546889924611 杰克 some_hash 1667329412

另一个原因是我使用 go (duh)用于我所有不同的后端。目的是使其尽可能容易地使用,并能够在我正在从事的任何项目中(例如Project Absence或其他未来项目)中使用它。

我在开发过程中遇到的一些问题

走很快

“
Gotta Go快速(Ba Dum TSS)

是的,走很快;这也是我的第一期。背后的原因是,起初我制作了序列随机,这绝对不是最好的主意 - 尽管我一直保留了它,以防万一我想使用它。问题是,正如您所猜到的那样,序列是从0到4095,这意味着它只能每毫秒生成4096个唯一IDS。这并不多,而且绝对不足以确保为在毫秒内生成的每个太空粉的唯一ID。因此,我不得不将其更改为增量

增量不足以产生批量生产

因此,让我们假设某人想迁移其数据库以使用太空粉代替UUID。他们将不得不产生大量的空间流量,并且必须尽快做到这一点。这是问题所在的地方。如果您每毫秒每毫秒生成一个空间粉,则每毫秒只能生成4096个空间粉。这再一次没有很多。因此,我必须在使用BulkGenerate()函数时制作节点ID和Worker ID 增量
该代码有点疯狂,但是它起作用(至少我认为是)

// BulkGenerate will generate the amount of Spaceflakes specified and auto scale the node and worker IDs
func BulkGenerate(s BulkGeneratorSettings) ([]*Spaceflake, error) {
    node := NewNode(1)
    worker := node.NewWorker()
    worker.BaseEpoch = s.BaseEpoch
    spaceflakes := make([]*Spaceflake, s.Amount)
    for i := 1; i <= s.Amount; i++ {
        if i%(MAX12BITS*MAX5BITS*MAX5BITS) == 0 { // When the total amount of Spaceflakes generated is the maximum per millisecond, such as 3'935'295
            time.Sleep(1 * time.Millisecond) // We need to sleep to make sure the timestamp is different
            newNode := NewNode(1) // We set the node ID to 1
            newWorker := newNode.NewWorker() // And we create a new worker
            newWorker.BaseEpoch = s.BaseEpoch
            node = newNode
            worker = newWorker
        } else if len(node.workers)%MAX5BITS == 0 && i%(MAX5BITS*MAX12BITS) == 0 { // When the node ID is at its maximum value
            newNode := NewNode(node.ID + 1) // We need to create a new node
            newWorker := newNode.NewWorker() // And a new worker
            newWorker.BaseEpoch = s.BaseEpoch
            node = newNode
            worker = newWorker
        } else if i%MAX12BITS == 0 { // When the worker ID is at its maximum value
            newWorker := worker.Node.NewWorker() // We just create a new worker
            newWorker.BaseEpoch = s.BaseEpoch
            worker = newWorker
        }
        spaceflake, err := generateSpaceflakeOnNodeAndWorker(worker, worker.Node) // We generate the Spaceflake on the node and worker specified
        if err != nil {
            return nil, err
        }
        spaceflakes[i-1] = spaceflake // i starts at 1, so we need to subtract 1 to get the correct index
    }
    return spaceflakes, nil
}

Source

现在,我们可以每毫秒大量生成3'935'295太空粉,而无需自动缩放的情况大得多4096。一旦达到了最大数量,我们就需要睡1毫秒,以确保时间戳不同,并且我们需要创建一个新的节点和一个新的工人。

这是解决的另一个问题ð€

太空粉是大数字

最后但并非最不重要的一点是,我知道,如果不作为API中的字符串处理,大数字是痛苦的。
这正是在我的一个API中使用太空粉时发生的事情。我生成了一个太空粉-144328692659220481-并制作了一个端点以获取用户ID。当通过浏览器向端点提出请求时,我得到了以下JSON:

{
    "id": 144328692659220480
}

是的,有一个很小的区别 - 在这里:144432869265922048* 0 。现在,这可能是一个很小的差异,但是**这是一个差异*,我们不希望那样。唯一的解决方案是使API返回ID作为字符串。在客户端,如果我们愿意,我们可以将其转换为数字 - 例如,使用此信息:

spaceflakeID := "144328692659220481" // Will be the value returned by the API
id, _ := strconv.ParseUint(spaceflakeID, 10, 64)
sequence := spaceflake.ParseSequence(id)

这将返回空格的正确序列。

如何使用太空粉?

使用太空粉非常简单,如果您有一个GO项目,则可以使用
下载包裹

go get github.com/kkrypt0nn/spaceflake

然后,您可以通过导入并生成太空粉:
来使用它。

package main

import (
    "fmt"

    "github.com/kkrypt0nn/spaceflake"
)

func main() {
    node := spaceflake.NewNode(5)
    worker := node.NewWorker() // If BaseEpoch not changed, it will use the EPOCH constant
    sf, err := worker.GenerateSpaceflake()
    if err != nil {
        panic(err)
    }
    fmt.Println(sf.Decompose()) // map[id:<Spaceflake> nodeID:5 sequence:1 time:<timestamp> workerID:1]
}

Source

您可以在存储库的examples文件夹中查看其他示例。