在 Golang 中使用 MongoDB 事务支持

在一次业务实践中需要在 MongoDB 中使用自增 ID,而 MongoDB 本身并不支持自增 ID。我们需要通过一个单独的集合保存 ID,使用 FindOneAndUpdate$inc 操作符实现 ID 的自增.
然而此时需要操作两个集合,因 MongoDB 的原子性只是针对单文档的,故会出现 ID 增加而插入失败的情况。
好在 MongoDB 在 4.0 中,支持了副本集上的多文档事务,在版本 4.2 中,引入了分布式事务,这增加了对分片群集上的多文档事务的支持,并合并了对副本集上多文档事务的现有支持。

MongoDB事务

事务介绍

在 MongoDB 中,对单个文档的操作是原子的。由于您可以使用嵌入的文档和数组来捕获单个文档结构中的数据之间的关系,而不是跨多个文档和集合进行规范化,因此这种单一文档的原子性消除了对多文档的需求许多实际用例的事务。
对于需要对多个文档(在单个或多个集合中)进行读取和写入原子化的情况,MongoDB 支持多文档事务。对于分布式事务,事务可用于多个操作、集合、数据库、文档和分片。

事务和原子性

分布式事务和多单据事务
从 MongoDB 4.2 开始,这两个术语是同义词。分布式事务是指分片群集和副本集上的多文档交易记录。多文档事务(无论是在分片群集还是副本集上)也称为从 MongoDB 4.2 开始的分布式事务。
对于需要对多个文档(在单个或多个集合中)进行读取和写入原子化的情况,MongoDB 支持多文档事务:
在版本 4.0中,MongoDB 支持副本集上的多文档事务。
在版本 4.2中,MongoDB 引入了分布式事务,这增加了对分片群集上的多文档事务的支持,并合并了对副本集上多文档事务的现有支持。
要在 MongoDB 4.2 部署(副本集和分片群集)上使用事务,客户端必须使用为 MongoDB 4.2 更新的 MongoDB 驱动程序。
多文档事务是原子的(即提供”全无”命题):
当事务提交时,事务中所做的所有数据更改都将保存在事务外部并可见。也就是说,事务不会提交其某些更改,而回滚其他更改。
在事务提交之前,事务中所做的数据更改在事务外部不可见。
但是,当事务写入多个分片时,并非所有外部读取操作都需要等待提交的事务的结果在分片中可见。例如,如果提交事务,写入 1 在分片 A 上可见,但在分片 B 上尚未显示写入 2,则读取时的外部读取”local”可以读取写入 1 的结果,而看不到写入 2。
当事务中止时,事务中所做的所有数据更改将被丢弃,而不会变得可见。例如,如果事务中的任何操作失败,事务将中止,并且事务中所做的所有数据更改将被丢弃,而不会变得可见。

准备工作

MongoDB 使用事务的前提是 MongoDB 版本大于 4.0,需要配置 MongoDB 工作模式为副本集,单个 MongoDB 节点不足支持事务,因为 MongoDB 事务至少需要两个节点。其中一个是主节点,负责处理客户端请求,其余的都是从节点,负责复制主节点上的数据。mongodb各个节点常见的搭配方式为:一主一从、一主多从。主节点记录在其上的所有操作oplog,从节点定期轮询主节点获取这些操作,然后对自己的数据副本执行这些操作,从而保证从节点的数据与主节点一致。

  1. 创建数据库目录
    1
    mkdir -p /data/mongodb/rs0-0 /data/mongodb/rs0-1
  2. 创建日志文件
    1
    touch /data/mongodb/rs0-0.log /data/mongodb/rs0-1.log
  3. 创建两个 mongod 实例
    1
    2
    3
    mongod --bind_ip 0.0.0.0 --port 27017 --dbpath /data/mongodb/rs0-0 -fork --logpath /data/mongodb/rs0-0.log --replSet rs0

    mongod --bind_ip 0.0.0.0 --port 27018 --dbpath /data/mongodb/rs0-1 -fork --logpath /data/mongodb/rs0-1.log --replSet rs0
  4. 连接数据库
    1
    mongo --port 27017
  5. 初始化副本集
    使用rs.initiate()命令,MongoDB将初始化一个由当前节点构成、拥有默认配置的复制集。
    1
    rs.initiate()
  6. 副本集加入其他节点
    1
    rs.add("0.0.0.0:27018")

Golang 中使用 MongoDB 事务

本例使用 MongoDB 官方驱动 MongoDB事务

  1. 连接 MongoDB

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var err error
    clientOptions := options.Client().
    ApplyURI("mongodb://xxx.xxx.xxx.xxx:27017,xxx.xxx.xxx.xxx:27018").
    SetReplicaSet("rs0").
    SetAuth(options.Credential{Username: "xxx", Password: "xxx"})
    client, err = mongo.Connect(context.Background(), clientOptions)

    if err != nil {
    panic(err)
    }

    db = client.Database(Config.Mongo.Database)
  2. 使用事务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    func InsertXXX(document bson.M) error {
    opts := options.FindOneAndUpdate().SetProjection(bson.M{"_id": 0})
    update := bson.M{"$inc": bson.M{"id": 1}}
    ctx := context.TODO()

    err := db.Client().UseSession(ctx, func(sessionContext mongo.SessionContext) error {
    var err error
    err = sessionContext.StartTransaction()
    if err != nil {
    return err
    }
    if err = db.Collection("xxx_ids").
    FindOneAndUpdate(sessionContext, bson.M{}, update, opts).
    Decode(&document); err != nil {
    return err
    }
    _, err = db.Collection("xxx").InsertOne(sessionContext, document)

    if err != nil {
    sessionContext.AbortTransaction(sessionContext)
    return err
    } else {
    sessionContext.CommitTransaction(sessionContext)
    }
    return nil
    })
    return err
    }