前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MongoDB 实现自增 ID 的最佳实践

MongoDB 实现自增 ID 的最佳实践

原创
作者头像
陈明勇
修改2024-08-14 02:15:47
550
修改2024-08-14 02:15:47
举报
文章被收录于专栏:用户7707913的专栏(2)Go 实战

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

前言

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 基于计数器集合实现自增序号
    • 创建自增序号的集合
      • 实现自增序号的方法
        • 完整的脚本示例代码
          • Go 语言代码示例
          • 并发安全与数据一致性问题
            • 并发更新时自增序号的安全性
              • 使用事务保证数据的一致性
              • 小结
              相关产品与服务
              数据库
              云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档