MongoDB 实现自增 ID 的最佳实践

2024-08-14 02:15:47 浏览数 (1)

最近有幸观看了 腾讯云开发者社区 发布的 《中国数据库前世今生》 纪录片,该纪录片深入探讨一个时代的数据库演变历程,以及这些大趋势下鲜为人知的小故事。看完以后,我对中国数据库的发展历程有了更深入的认识。感兴趣的小伙伴可以去观看一下。本文介绍的内容也和数据库有关,请看下文!

前言

熟悉 MongoDB 的用户应该都知道,它并不像一些关系型数据库那样提供内置的自增 ID 功能,而是默认使用 ObjectId 作为主键的类型。但有时使用自增 ID 可能更符合某些应用场景的需求,例如:

  • 兼容现有系统 某些系统需要将数据迁移到 MongoDB 时,如果原来使用的是自增 ID 作为主键,在迁移过去之后需要保持自增主键的特点。
  • 对外展示的 ID 在一些应用场景中,一个更直观、更易记的标识符,对用户更友好,例如展示给用户的 用户编号文章编号 等。这在需要手动输入或与用户交流时特别有用,因为自增 IDObjectId 更短、更易读。

虽然 MongoDB 不支持自增 ID 的功能,但我们仍然可以使用其他方式来实现此功能。本文将会介绍如何在 MongoDB 中实现自增 ID 序号。

准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。

基于计数器集合实现自增序号

创建自增序号的集合

我们可以使用计数器集合 counters 来实现实现自增序号,这也是官方推荐的一种实现方式。counters 集合的文档结构如下:

代码语言:json复制
{
    "_id": "posts",
    "seq_value": 1
}

该集合有两个字段:

  • _id:代表某个集合的名称。
  • seq_value:为自增序号。

由于 counters 集合中的 _id 字段值代表某个集合的名称,因此我们可以利用 counters 集合为多个集合实现自增 序号,而不仅限于单个集合。

实现自增序号的方法

那么 counters 集合要怎么实现 seq_value 字段的自增呢?这就需要用到 findOneAndUpdate 方法了。

findOneAndUpdate 方法用于查找并更新集合中的单个文档。该方法还支持选择性地返回更新前或更新后的文档。

下面是一个简单案例的具体流程:

  • 1、开始:流程图从“开始”节点开始。
  • 2、创建 posts 文章和 counters 计数器集合。
代码语言:js复制
db.createCollection("posts");
db.createCollection("counters");
  • 3、获取自增 序号:使用 findOneAndUpdatecounters 集合中获取并自增 seq_value。如果 counters 集合中 _idposts 的文档不存在,则通过 upsert: true 选项自动创建该文档,并初始化 seq_value1
代码语言:js复制
const seqValue = db.counters.findOneAndUpdate(
    { _id: 'posts' },
    { $inc: { seq_value: 1 }},
    { returnDocument: "after", upsert: true }
).seq_value;
  • 4、向 posts 集合中插入新文档:使用从 counters 集合中获取的自增 seq_value 作为新文档的一个字段,插入到 posts 集合中。
代码语言:js复制
db.posts.insertOne({ 
    title: "在 MongoDB 中实现自增 ID",
    author: "陈明勇",
    seq_value: seqValue
});
  • 5、结束:流程结束。

完整的脚本示例代码

下面是完整的 MongoDB 脚本示例代码,展示了如何创建集合、获取自增序号并插入新文档。

代码语言:js复制
// 创建 posts 和 counters 集合
db.createCollection("posts");
db.createCollection("counters");

// 获取自增的 seq_value
const seqValue = db.counters.findOneAndUpdate(
  { _id: 'posts' },
  { $inc: { seq_value: 1 }},
  { returnDocument: "after", upsert: true }
).seq_value;

// 向 posts 集合中插入新文档
db.posts.insertOne({ 
  title: "在 MongoDB 中实现自增 ID",
  author: "陈明勇",
  seq_value: seqValue
});

Go 语言代码示例

  • Go 项目里安装 go mongox 模块
代码语言:bash复制
go get github.com/chenmingyong0423/go-mongox
  • 完整代码
代码语言:go复制
package main

import (
    "context"
    "fmt"

    "github.com/chenmingyong0423/go-mongox"
    "github.com/chenmingyong0423/go-mongox/builder/query"
    "github.com/chenmingyong0423/go-mongox/builder/update"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "go.mongodb.org/mongo-driver/mongo/readpref"
)

type Post struct {
    Title    string `bson:"title"`
    Author   string `bson:"author"`
    SeqValue int64  `bson:"seq_value"`
}

type Counter struct {
    Id       string `bson:"_id"`
    SeqValue int64  `bson:"seq_value"`
}

var db *mongo.Database

func init() {
    client, err := mongo.Connect(context.Background(), options.Client().ApplyURI("mongodb://localhost:27017").SetAuth(options.Credential{
        Username:   "test",
        Password:   "test",
        AuthSource: "db-test",
    }))
    if err != nil {
        panic(err)
    }
    err = client.Ping(context.Background(), readpref.Primary())
    if err != nil {
        panic(err)
    }
    db = client.Database("db-test")
}

func main() {
    postColl := mongox.NewCollection[Post](db.Collection("posts"))

    seqValue, err := getNextSeqValue("posts")
    if err != nil {
        panic(err)
    }
    fmt.Println(seqValue) // 如果是第一次执行 FindOneAndUpdate,值为 1

    // 插入一个 Post 文档,seq_value 字段为 Counter 文档的 seq_value 字段值
    insertOneResult, err := postColl.Creator().InsertOne(context.Background(), &Post{
        Title:    "在 MongoDB 中实现自增 ID",
        Author:   "陈明勇",
        SeqValue: seqValue,
    })
    if err != nil {
        panic(err)
    }

    // 验证插入的 Post 文档的 seq_value 字段值是否为 Counter 文档的 seq_value 字段值
    post, err := postColl.Finder().Filter(query.Id(insertOneResult.InsertedID)).FindOne(context.Background())
    if err != nil {
        panic(err)
    }
    fmt.Println(post.SeqValue == seqValue) // true
}

func getNextSeqValue(collectionName string) (int64, error) {
    // 创建 Counter 泛型集合
    counterColl := mongox.NewCollection[Counter](db.Collection("counters"))
    // 执行 FindOneAndUpdate 操作,如果不存在,则插入一个新的 Counter 文档,否则更新 seq_value 字段自增 1,并返回新增或更新后的 Counter 文档
    counter, err := counterColl.Finder().Filter(query.Id(collectionName)).Updates(update.Inc("seq_value", 1)).FindOneAndUpdate(context.Background(), options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After))
    if err != nil {
        panic(err)
    }
    // 返回自增序号
    return counter.SeqValue, nil
}

并发安全与数据一致性问题

并发更新时自增序号的安全性

使用计数器集合实现自增序号的方案在并发更新时,seq_value 是否是并发安全的?答案是肯定的。这是因为 MongoDB$inc 操作符能原子性地对文档中指定字段的值进行递增或递减操作。

当多个操作同时对同一文档执行 $inc 时,MongoDB 会确保这些操作按顺序依次执行。每个操作都会基于前一个操作的结果进行累加。例如,如果两个并发操作分别对某个字段执行 $inc: 1,最终结果是该字段的值增加了 2,而不会出现仅增加 1 的情况。

使用事务保证数据的一致性

在涉及更新多个集合(如 countersposts)的操作时,确保数据的一致性尤为重要。假设在 seq_value 自增后,由于某种意外(例如向 posts 集合插入文档时出错)导致插入失败,那么此次自增的 seq_value 就不会成功保存到 posts 集合中,从而使序列号出现空洞。这种情况下,下一次操作会跳过这个序列号,导致保存到 posts 集合中的序列号不连续。

如果你的业务逻辑要求序列号必须是连续的,那么使用事务是必要的。通过使用事务,我们可以确保整个操作的原子性:要么所有相关操作(包括 seq_value 的自增和文档的插入)都成功执行,要么在发生任何问题时回滚所有更改。这种方式能够有效避免 posts 集合中的序列号的不连续性,并确保数据的一致性。

小结

本文详细探讨了在 MongoDB 中实现自增 ID 序号的方法。其核心思路是通过创建 counters 集合,并使用 $inc 操作符来维护自增的 ID 序号 seq_value,从而满足特定应用场景下的需求。

这种自增序号的实现方式特别适用于需要为用户可见的实体(如文章编号、用户编号)生成更短、更直观标识符的场景。相比 ObjectId,自增 ID 更易记、更直观,有助于提高用户体验。

0 人点赞