MongoDB 实现自增 ID 的最佳实践

1 篇文章 0 订阅

作者:陈明勇

个人网站:https://chenmingyong.cn

文章持续更新,如果本文能让您有所收获,欢迎点赞收藏加关注本号。 微信阅读可搜《程序员陈明勇》。 这篇文章已被收录于 GitHub https://github.com/chenmingyong0423/blog,欢迎大家 Star 催更并持续关注。

前言

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

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

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

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

程序员陈明勇.jpeg

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

创建自增序号的集合

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

{
    "_id": "posts",
    "seq_value": 1
}

该集合有两个字段:

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

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

实现自增序号的方法

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

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

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

在这里插入图片描述

  1. 开始:流程图从“开始”节点开始。

  2. 创建 posts 文章和 counters 计数器集合。

    db.createCollection("posts");
    db.createCollection("counters");
    
  3. 获取自增 序号:使用 findOneAndUpdatecounters 集合中获取并自增 seq_value。如果 counters 集合中 _idposts 的文档不存在,则通过 upsert: true 选项自动创建该文档,并初始化 seq_value1

    const seqValue = db.counters.findOneAndUpdate(
      { _id: 'posts' },
      { $inc: { seq_value: 1 }},
      { returnDocument: "after", upsert: true }
    ).seq_value;
    
  4. posts 集合中插入新文档:使用从 counters 集合中获取的自增 seq_value 作为新文档的一个字段,插入到 posts 集合中。

    db.posts.insertOne({ 
      title: "在 MongoDB 中实现自增 ID",
      author: "陈明勇",
      seq_value: seqValue
    });
    
  5. 结束:流程结束。

完整的脚本示例代码

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

// 创建 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 模块

    go get github.com/chenmingyong0423/go-mongox
    
  • 完整代码

    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 更易记、更直观,有助于提高用户体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员陈_明勇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值