一、Mongo中的事务

1.Mongo新特性

首先,我们必须知道的是,Mongo的事务的在版本4.0时的新特性,也就是说,如果Mongo的版本是在4.0之前的,那么是不支持事务的,因此,我们的Mongo需要是4.0以上的版本。

2.基于会话的事务

其次,Mongo的事务是比较有意思的,Mongo要开启事务支持,需要搭建副本集(Replica Sets)。而且,Mongo的事务是和会话(Session)相关联的,一个会话同一时刻只能开启一个事务操作,当一个会话断开的时候,这个会话中的事务也会结束。

3.事务相关命令

Mongo中的事务相关命令如下:

//开启一个会话
sess = db.getMongo().startSession()
//开启事务
sess.startTransaction()
//回滚
sess.abortTransaction()
//提交
sess.commitTransaction()
//结束会话
sess.endSession()
二、搭建Mongo副本集

以下搭建副本集以Centos为例,并在同一个机子上进行搭建

1. 安装MongoDB

首先在MongoDB官网下载对应系统和版本的MongoDB包,如:

[root@vm1 ~]# wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-4.4.10.tgz

解压tgz包到/usr/local/目录下,并给目录重命名一下

[root@vm1 ~]# tar -zxvf mongodb-linux-x86_64-rhel70-4.4.10.tgz -C /usr/local/
[root@vm1 ~]# mv /usr/local/mongodb-linux-x86_64-rhel70-4.4.10/ /usr/local/mongodb-4.4.10/

2. 环境变量配置

# 修改环境变量
[root@vm1 ~]# vi /etc/profile
export PATH=$PATH:/usr/local/mongodb-4.4.10/bin
# 使环境变量生效
[root@vm1 ~]# source /etc/profile

3. 创建副本集目录

3.1 创建主节点相关目录
[root@vm1 ~]# cd /usr/local/mongodb-4.4.10/
[root@vm1 mongodb-4.4.10]# mkdir primary
[root@vm1 mongodb-4.4.10]# cd primary
[root@vm1 primary]# mkdir data logs config pid
3.2 创建副节点相关目录
[root@vm1 primary]# cd ..
[root@vm1 mongodb-4.4.10]# mkdir secondary
[root@vm1 mongodb-4.4.10]# cd secondary
[root@vm1 secondary]# mkdir data logs config pid
3.3 创建仲裁节点相关目录
[root@vm1 secondary]# cd ..
[root@vm1 mongodb-4.4.10]# mkdir arbiter
[root@vm1 mongodb-4.4.10]# cd arbiter
[root@vm1 arbiter]# mkdir data logs config pid

4. 创建副本集认证的key文件

[root@vm1 arbiter]# cd ..
[root@vm1 mongodb-4.4.10]# openssl rand -base64 90 -out mongo.keyfile
[root@vm1 mongodb-4.4.10]# chmod 400 mongo.keyfile

5 修改MongoDB配置文件

5.1 主节点配置文件
[root@vm1 mongodb-4.4.10]# cd primary/
[root@vm1 primary]# vi config/mongod.conf

添加内容如下:

systemLog:
    #MongoDB发送所有日志输出的目标指定为文件
    destination: file
    #mongod或mongos应向其发送所有诊断日志记录信息的日志文件的路径
    path: "/usr/local/mongodb-4.4.10/primary/logs/mongod.log"
    #当mongos或mongod实例重新启动时,mongos或mongod会将新条目附加到现有日志文件的末尾
    logAppend: true
storage:
    #mongod实例存储其数据的目录。storage.dbPath设置仅适用于mongod
    dbPath: "/usr/local/mongodb-4.4.10/primary/data"
    journal:
         #启用或禁用持久性日志以确保数据文件保持有效和可恢复。
        enabled: true
processManagement:
    #启用在后台运行mongos或mongod进程的守护进程模式。
    fork: true
    #指定用于保存mongos或mongod进程的进程ID的文件位置,其中mongos或mongod将写入其PID
    pidFilePath: "/usr/local/mongodb-4.4.10/primary/pid/mongod.pid"
net:
    #服务实例绑定所有IP,有副作用,副本集初始化的时候,节点名字会自动设置为本地域名,而不是ip
    #bindIpAll: true
    #服务实例绑定的IP
    bindIp: 0.0.0.0
    #bindIp
    #绑定的端口
    port: 27017
replication:
    #副本集的名称
    replSetName: "my-rs"
security:
    #副本集密钥文件
    keyFile: "/usr/local/mongodb-4.4.10/mongo.keyfile"
    #副本集认证方式
    clusterAuthMode: "keyFile"
5.2 副节点配置文件
[root@vm1 primary]# cd ../secondary/
[root@vm1 secondary]# vi config/mongod.conf

添加内容如下:

systemLog:
    #MongoDB发送所有日志输出的目标指定为文件
    destination: file
    #mongod或mongos应向其发送所有诊断日志记录信息的日志文件的路径
    path: "/usr/local/mongodb-4.4.10/secondary/logs/mongod.log"
    #当mongos或mongod实例重新启动时,mongos或mongod会将新条目附加到现有日志文件的末尾
    logAppend: true
storage:
    #mongod实例存储其数据的目录。storage.dbPath设置仅适用于mongod
    dbPath: "/usr/local/mongodb-4.4.10/secondary/data"
    journal:
         #启用或禁用持久性日志以确保数据文件保持有效和可恢复。
        enabled: true
processManagement:
    #启用在后台运行mongos或mongod进程的守护进程模式。
    fork: true
    #指定用于保存mongos或mongod进程的进程ID的文件位置,其中mongos或mongod将写入其PID
    pidFilePath: "/usr/local/mongodb-4.4.10/secondary/pid/mongod.pid"
net:
    #服务实例绑定所有IP,有副作用,副本集初始化的时候,节点名字会自动设置为本地域名,而不是ip
    #bindIpAll: true
    #服务实例绑定的IP
    bindIp: 0.0.0.0
    #bindIp
    #绑定的端口
    port: 27018
replication:
    #副本集的名称
    replSetName: "my-rs"
security:
    #副本集密钥文件
    keyFile: "/usr/local/mongodb-4.4.10/mongo.keyfile"
    #副本集认证方式
    clusterAuthMode: "keyFile"
5.3 仲裁节点配置文件
[root@vm1 secondary]# cd ../arbiter/
[root@vm1 arbiter]# vi config/mongod.conf

添加内容如下:

systemLog:
    #MongoDB发送所有日志输出的目标指定为文件
    destination: file
    #mongod或mongos应向其发送所有诊断日志记录信息的日志文件的路径
    path: "/usr/local/mongodb-4.4.10/arbiter/logs/mongod.log"
    #当mongos或mongod实例重新启动时,mongos或mongod会将新条目附加到现有日志文件的末尾
    logAppend: true
storage:
    #mongod实例存储其数据的目录。storage.dbPath设置仅适用于mongod
    dbPath: "/usr/local/mongodb-4.4.10/arbiter/data"
    journal:
         #启用或禁用持久性日志以确保数据文件保持有效和可恢复。
        enabled: true
processManagement:
    #启用在后台运行mongos或mongod进程的守护进程模式。
    fork: true
    #指定用于保存mongos或mongod进程的进程ID的文件位置,其中mongos或mongod将写入其PID
    pidFilePath: "/usr/local/mongodb-4.4.10/arbiter/pid/mongod.pid"
net:
    #服务实例绑定所有IP,有副作用,副本集初始化的时候,节点名字会自动设置为本地域名,而不是ip
    #bindIpAll: true
    #服务实例绑定的IP
    bindIp: 0.0.0.0
    #bindIp
    #绑定的端口
    port: 27019
replication:
    #副本集的名称
    replSetName: "my-rs"
security:
    #副本集密钥文件
    keyFile: "/usr/local/mongodb-4.4.10/mongo.keyfile"
    #副本集认证方式
    clusterAuthMode: "keyFile"

6. 使用Systemd对Mongo服务进行管理

6.1 主节点配置
[root@vm1 arbiter]# vi /usr/lib/systemd/system/mongo-primary.service

添加内容如下

[Unit]
Description=mongodb primary

[Service]
ExecStart=/usr/local/mongodb-4.4.10/bin/mongod -f /usr/local/mongodb-4.4.10/primary/config/mongod.conf
ExecStop=/bin/kill -INT $MAINPID
PIDFile=/usr/local/mongodb-4.4.10/primary/pid/mongod.pid

[Install]
WantedBy=multi-user.target

添加为开机启动

[root@vm1 arbiter]# systemctl enable mongo-primary
6.2 副节点配置
[root@vm1 arbiter]# vi /usr/lib/systemd/system/mongo-secondary.service

添加内容如下

[Unit]
Description=mongodb secondary

[Service]
ExecStart=/usr/local/mongodb-4.4.10/bin/mongod -f /usr/local/mongodb-4.4.10/secondary/config/mongod.conf
ExecStop=/bin/kill -INT $MAINPID
PIDFile=/usr/local/mongodb-4.4.10/secondary/pid/mongod.pid

[Install]
WantedBy=multi-user.target

添加为开机启动

[root@vm1 arbiter]# systemctl enable mongo-secondary
6.3 仲裁节点配置
[root@vm1 arbiter]# vi /usr/lib/systemd/system/mongo-arbiter.service

添加内容如下

[Unit]
Description=mongodb arbiter

[Service]
ExecStart=/usr/local/mongodb-4.4.10/bin/mongod -f /usr/local/mongodb-4.4.10/arbiter/config/mongod.conf
ExecStop=/bin/kill -INT $MAINPID
PIDFile=/usr/local/mongodb-4.4.10/arbiter/pid/mongod.pid

[Install]
WantedBy=multi-user.target

添加为开机启动

[root@vm1 arbiter]# systemctl enable mongo-arbiter

7. 启动MongoDB服务

7.1 启动节点
[root@vm1 arbiter]# systemctl start mongo-primary mongo-secondary mongo-arbiter
7.2 查看节点状态
[root@vm1 arbiter]# systemctl status mongo-primary mongo-secondary mongo-arbiter

8. 创建用户,并初始化副本集

[root@vm1 mongodb-4.4.10]# mongo --port 27017
> use admin
> db.createUser({user:"root", pwd:"root", roles:["root"]})
> config={
    "_id": "my-rs",
    "members": [
        {
            "_id": 0,
            "host": "127.0.0.1:27017",
            "priority": 2
        },
        {
            "_id": 1,
            "host": "127.0.0.1:27018",
            "priority": 1
        },
        {
            "_id": 2,
            "host": "127.0.0.1:27019",
            "arbiterOnly": true
        }
    ]
}
> rs.initiate(config)
> rs.status()
三、在Go代码中实现Mongo事务

1. 下载驱动包

直接执行下面的命令获取Golang的官方Mongo驱动包

go get -u go.mongodb.org/mongo-driver/mongo

2. 代码例子

使用事务时,特别要注意该用哪个context,在进行任何的db操作,不管是插入还是更新,都必须使用函数参数的那个sessionCtx,否则事务是无法生效的。另外在进行提交和回滚的时候,需要使用context.Background(),这是因为提交和回滚不应被context超时等因素影响。

package main

import (
	"context"
	"fmt"
	"github.com/beego/beego/v2/core/logs"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"time"
)

//mongo连接参数
const (
	user     = "root"
	pwd      = "root"
	hosts    = "127.0.0.1:27017,127.0.0.1:27018,127.0.0.1:27019"
	mongoOpt = "replicaSet=my-rs"
	auth     = "admin"
	timeout  = time.Duration(3000) * time.Millisecond
)

//mongo文档结构体
type student struct {
	Name   string `bson:"name"`
	Gender string `bson:"gender"`
	Age    int    `bson:"age"`
}

func main() {
	//设置连接参数
	uri := fmt.Sprintf("mongodb://%s:%s@%s/%s?%s",
		user, pwd, hosts, mongoOpt, auth)
	opt := options.Client().ApplyURI(uri).SetSocketTimeout(timeout)

	//创建一个context上下文
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	//获得一个mongo client
	client, err := mongo.Connect(ctx, opt)
	if err != nil {
		logs.Error("connect mongo failed, err:%s", err.Error())
		return
	}

	//ping一下mongo
	err = client.Ping(ctx, nil)
	if err != nil {
		logs.Error("ping mongo failed, err:%s", err.Error())
		return
	}

	//设定连接的数据库和集合
	database := "school"
	collection := "student"
	//构造插入的数据
	students := []interface{}{
		student{
			Name:   "Michael",
			Gender: "Male",
			Age:    21,
		},
		student{
			Name:   "Alice",
			Gender: "Female",
			Age:    19,
		},
	}

	//在会话中使用mongo
	if err = client.UseSession(ctx, func(sessionContext mongo.SessionContext) error {
		//开启事务
		if err := sessionContext.StartTransaction(); err != nil {
			return err
		}

		//插入数据
		if _, err := client.Database(database).Collection(collection).InsertMany(sessionContext, students); err != nil {
			if err := sessionContext.AbortTransaction(context.Background()); err != nil {
				//回滚事务
				logs.Error("mongo transaction rollback failed, %s", err.Error())
				return err
			}
			return err
		}
		//提交事务
		return sessionContext.CommitTransaction(context.Background())
	}); err != nil {
		logs.Error("insert failed, err:%s", err.Error())
	}
}