最近有幸观看了 腾讯云开发者社区 发布的 《中国数据库前世今生》 纪录片,该纪录片深入探讨一个时代的数据库演变历程,以及这些大趋势下鲜为人知的小故事。看完以后,我对中国数据库的发展历程有了更深入的认识。感兴趣的小伙伴可以去观看一下。本文介绍的内容也和数据库有关,请看下文!
前言
熟悉 MongoDB
的用户应该都知道,它并不像一些关系型数据库那样提供内置的自增 ID
功能,而是默认使用 ObjectId
作为主键的类型。但有时使用自增 ID
可能更符合某些应用场景的需求,例如:
- 兼容现有系统
某些系统需要将数据迁移到
MongoDB
时,如果原来使用的是自增ID
作为主键,在迁移过去之后需要保持自增主键的特点。 - 对外展示的
ID
在一些应用场景中,一个更直观、更易记的标识符,对用户更友好,例如展示给用户的 用户编号、 文章编号 等。这在需要手动输入或与用户交流时特别有用,因为自增ID
比ObjectId
更短、更易读。
虽然 MongoDB
不支持自增 ID
的功能,但我们仍然可以使用其他方式来实现此功能。本文将会介绍如何在 MongoDB
中实现自增 ID
序号。
准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。
基于计数器集合实现自增序号
创建自增序号的集合
我们可以使用计数器集合 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、获取自增 序号:使用
findOneAndUpdate
从counters
集合中获取并自增seq_value
。如果counters
集合中_id
为posts
的文档不存在,则通过upsert: true
选项自动创建该文档,并初始化seq_value
为1
。
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
的情况。
使用事务保证数据的一致性
在涉及更新多个集合(如 counters
和 posts
)的操作时,确保数据的一致性尤为重要。假设在 seq_value
自增后,由于某种意外(例如向 posts
集合插入文档时出错)导致插入失败,那么此次自增的 seq_value
就不会成功保存到 posts
集合中,从而使序列号出现空洞。这种情况下,下一次操作会跳过这个序列号,导致保存到 posts
集合中的序列号不连续。
如果你的业务逻辑要求序列号必须是连续的,那么使用事务是必要的。通过使用事务,我们可以确保整个操作的原子性:要么所有相关操作(包括 seq_value
的自增和文档的插入)都成功执行,要么在发生任何问题时回滚所有更改。这种方式能够有效避免 posts
集合中的序列号的不连续性,并确保数据的一致性。
小结
本文详细探讨了在 MongoDB
中实现自增 ID
序号的方法。其核心思路是通过创建 counters
集合,并使用 $inc
操作符来维护自增的 ID
序号 seq_value
,从而满足特定应用场景下的需求。
这种自增序号的实现方式特别适用于需要为用户可见的实体(如文章编号、用户编号)生成更短、更直观标识符的场景。相比 ObjectId
,自增 ID
更易记、更直观,有助于提高用户体验。