golang操作mongodb的驱动mongo-go-driver的事务支持和访问控制

mongodb要支持事务,需要满足以下条件:

  • 4.0以上版本;
  • 安装后时以replication set(复本集)模式启动;
  • storageEngine存储引擎须是wiredTiger (支持文档级别的锁),4.0以上版本已经默认是这个,参考

安装mongodb server 4.0以上版本

下载地址 目前最新的release版本是4.0.5,package 类型是server:

  • 可根据自己的系统平台选择相应的安装包进行安装
  • 可下载源码包进行编译安装

我用的是ubuntu 16.04系统 x64,所以我直接下载该该平台的server deb包进行安装

ubuntu@VM-0-3-ubuntu:~$ wget https://repo.mongodb.org/apt/ubuntu/dists/xenial/mongodb-org/4.0/multiverse/binary-amd64/mongodb-org-server_4.0.5_amd64.deb
ubuntu@VM-0-3-ubuntu::~$ dkpg -i mongodb-org-server_4.0.5_amd64.deb
ubuntu@VM-0-3-ubuntu:~$ whereis mongod  #可以看到安装后的程序和配置文件路径
mongod: /usr/bin/mongod /etc/mongod.conf /usr/share/man/man1/mongod.1.gz

replication set模式

  • 就是多个server进程组合成一个集群,会自动推选出一个Primary server来对外,提供数据库服务,其它的server就是Secondary角色,还有一种不存储数据只选举充数的arbiter
  • Primary接收所有数据的写操作并记录到操作日志。
  • 所有Secondary server复制Primary server的操作日志并更新到自己的数据集,异步的,所以有数据一致性问题。
  • server之间相互有心跳。当Primary一段时间(可配置,默认10秒)内没有心跳,Secondary们就再选出一个Primary,一个合适的secondary server会发起选举自荐为Primary,让其它server投票。如果本来数据库进程是偶数,就需要增加一个arbiter角色的server集成奇数个,以在选举时确定“大多数”。选举过程中不再接收写操作。

而要在replication set模式下同时支持访问控制,那么多节点之间也需要进行内部认证,有几种方式,即设置配置文件中的security.clusterAuthMode,这里我使用其默认的keyFile方式,所以需要事先准备一个保存有密钥的keyFile文件,要求:

  • 密钥长度必须是在6到1024个字符间且必须使用base64字符集;
  • 在类unix系统上,keyFile文件不能设置组权限和其它权限,即只能有文件所有者权限,建议是-rw-------,即600;而windows系统不检查文件权限,没有这个限制。
#用openssl命令随机生成一个64位(可自己定)长度的密码串,写到mongod-keyfile中,并修改权限
ubuntu@VM-0-3-ubuntu:~$ openssl rand -base64 64 > ./mongod-keyfile
ubuntu@VM-0-3-ubuntu:~$ sudo mv ./mongod-keyfile /etc
ubuntu@VM-0-3-ubuntu:~$ sudo chmod 600 /etc/mongod-keyfile

所有mongod进程都要使用这个相同的密钥,所以在同一台主机上可以共享这个文件,如果是多台主机,最好将这个文件拷贝到每台机器上给mongod进程使用,保证相同密钥。

如果有条件,最好准备多台主机。我测试,只有一台主机,所以运行3个端口的进程。

打开mongod.conf配置文件,做点修改:

ubuntu@VM-0-3-ubuntu:~$ sudo vi /etc/mongod.conf  #看到文件内容如下

# mongod.conf

# for documentation of all options, see:
#   http://docs.mongodb.org/manual/reference/configuration-options/

# Where and how to store data.
storage:
  dbPath: /var/lib/mongodb
  journal:
    enabled: true
#  engine:
#  mmapv1:
#  wiredTiger:

# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongodb/mongod.log

# network interfaces
net:
  port: 27017      #端口
  bindIp: 0.0.0.0  #地址默认是127.0.0.1 ,如果想要网络上能访问到,最好改成外网地址或全0


# how the process runs
processManagement:
  timeZoneInfo: /usr/share/zoneinfo

security:
  authorization: enabled     #开启访问控制
  #clusterAuthMode: keyFile  #默认的
  keyFile: /etc/mongod-keyfile  #指定keyFile文件
  
#operationProfiling:

replication:     #此行默认是加#注释掉的,需要去掉#,开启复本集模式,再加上下面一行
  replSetName: rs1   #rs1是复本集的名字,可自定义,但集群中所有server的配置必须一样
#sharding:

## Enterprise-Only Options:

#auditLog:

#snmp:

修改配置文件后,复制两份:

ubuntu@VM-0-3-ubuntu:~$ sudo cp /etc/mongod.conf /etc/mongod1.conf
ubuntu@VM-0-3-ubuntu:~$ sudo cp /etc/mongod.conf /etc/mongod2.conf

打开/etc/mongod1.conf,修改三行:

ubuntu@VM-0-3-ubuntu:~$ sudo vi /etc/mongod1.conf

# mongod.conf

# for documentation of all options, see:
#   http://docs.mongodb.org/manual/reference/configuration-options/

# Where and how to store data.
storage:
  dbPath: /var/lib/mongodb1   #此处的数据存储路径目录改一下,以区分
  journal:
    enabled: true
#  engine:
#  mmapv1:
#  wiredTiger:

# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongodb/mongod1.log #此处的日志文件路径改一下,以区分

# network interfaces
net:
  port: 27018				#因在同一主机上,需要一个不同的端口,改为27018
  bindIp: 0.0.0.0


# how the process runs
processManagement:
  timeZoneInfo: /usr/share/zoneinfo

security:
  authorization: enabled
  #clusterAuthMode: keyFile
  keyFile: /etc/mongod-keyfile

#operationProfiling:

replication:
  replSetName: rs1
#sharding:

## Enterprise-Only Options:

#auditLog:

#snmp:

同样修改/etc/mongod2.conf,对应修改即可,端口27019。

然后启动三个mongod服务进程:

ubuntu@VM-0-3-ubuntu:~$ sudo mongod --fork -f /etc/mongod.conf  #--fork表示后台进程运行
ubuntu@VM-0-3-ubuntu:~$ sudo mongod --fork -f /etc/mongod1.conf
ubuntu@VM-0-3-ubuntu:~$ sudo mongod --fork -f /etc/mongod2.conf
ubuntu@VM-0-3-ubuntu:~$ ps -ef | grep mongod #可查看一下三个进程

安装mongodb shell客户端

下载地址 package 类型选 shell

ubuntu@VM-0-3-ubuntu:~$ wget https://repo.mongodb.org/apt/ubuntu/dists/xenial/mongodb-org/4.0/multiverse/binary-amd64/mongodb-org-shell_4.0.5_amd64.deb
ubuntu@VM-0-3-ubuntu::~$ dkpg -i mongodb-org-shell_4.0.5_amd64.deb
ubuntu@VM-0-3-ubuntu:~$ whereis mongo  #可以看到安装后的程序和配置文件路径
mongo: /usr/bin/mongo /usr/share/man/man1/mongo.1.gz

然后需要用客户端连接一个server,并初始化复本集:

ubuntu@VM-0-3-ubuntu:~$ mongo          #运行mongo客户端,默认是连接127.0.0.1:27107/test,也可以指定服务器地址,比如 mongodb://127.0.0.1:27107/test,test表示数据库名。

MongoDB shell version v4.0.5
connecting to: mongodb://127.0.0.1:27017/?gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("76f42f17-4012-4ad0-b386-7e66459af51d") }
MongoDB server version: 4.0.5
Server has startup warnings: 
2018-12-24T22:37:51.871+0800 I STORAGE  [initandlisten] 
2018-12-24T22:37:51.871+0800 I STORAGE  [initandlisten] ** WARNING: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine
2018-12-24T22:37:51.871+0800 I STORAGE  [initandlisten] **          See http://dochub.mongodb.org/core/prodnotes-filesystem
2018-12-24T22:37:52.628+0800 I CONTROL  [initandlisten] 
2018-12-24T22:37:52.628+0800 I CONTROL  [initandlisten] ** WARNING: Access control is not enabled for the database.
2018-12-24T22:37:52.628+0800 I CONTROL  [initandlisten] **          Read and write access to data and configuration is unrestricted.
2018-12-24T22:37:52.628+0800 I CONTROL  [initandlisten] ** WARNING: You are running this process as the root user, which is not recommended.
2018-12-24T22:37:52.628+0800 I CONTROL  [initandlisten] 
---
Enable MongoDB's free cloud-based monitoring service, which will then receive and display
metrics about your deployment (disk utilization, CPU, operation statistics, etc).

The monitoring data will be available on a MongoDB website with a unique URL accessible to you
and anyone you share the URL with. MongoDB may use this information to make product
improvements and to suggest MongoDB products and deployment options to you.

To enable free monitoring, run the following command: db.enableFreeMonitoring()
To permanently disable this reminder, run the following command: db.disableFreeMonitoring()
---

# [创建用户](https://docs.mongodb.com/manual/tutorial/enable-authentication/)
# 最开始是没有用户的,必须先去admin数据库中创建一个权限较高的管理员用户,然后用它来创建其它用户以及初始化replication set.否则很多操作都没有权限做了。
> use admin
switched to db admin

#如下创建admin用户,创建的用户信息也会自动同步到集群中所有server;
#userAdminAnyDatabase角色有权限管理其它数据库的用户
#readWriteAnyDabase角色有权限读写其它数据库
#clusterAdmin角色有权限操作replication set集群,比如执行rs.initiate
> db.createUser({user:'admin',pwd:'admin',roles:['userAdminAnyDatabase','readWriteAnyDatabase','clusterAdmin']})
successfully added user:{
    user:'admin',
    roles:['userAdminAnyDatabase','readWriteAnyDabase','clusterAdmin']
}

#先认证
>db.auth('admin','admin')
1

# 认证后可以使用show roles查看当前数据库有哪些角色可分配,还可以用show users显示当前数据库所有用户
# 如果没有开启访问控制,则执行show roles和show users等命令是不需要先认证的。

#切换到test测试数据库,创建一个dbOwner角色的用户,对当前的test数据库具有最高权限,是readWrite、dbAdmin和userAdmin三种角色的组合。
>use test
>db.createUser({user:'test',pwd:'123456',roles:['dbOwner']})
successfully added user:{
    user:'test',
    roles:['dbOwner']
}

#执行如下命令初始化集群的三个成员,rs1是集群名;members里的_id是序号,可自定义,不同就行;host是ip:端口,我用的是本地127.0.0.1,如果要外网访问,请设置相应的外网ip。
> rs.initiate({_id:"rs1",members:[{_id:0,host:"127.0.0.1:27017"},{_id:1,host:"127.0.0.1:27018"},{_id:2,host:"127.0.0.1:27019"}]})

#成功会就会开始推选primary,一般是客户端连的这个server.

rs1:PRIMARY> rs.status()   #查看一下复本集的状态,显示如下

{
        "set" : "rs1",
        "date" : ISODate("2018-12-25T06:02:09.320Z"),
        "myState" : 1,
        "term" : NumberLong(1),
        "syncingTo" : "",
        "syncSourceHost" : "",
        "syncSourceId" : -1,
        "heartbeatIntervalMillis" : NumberLong(2000),
        "optimes" : {
                "lastCommittedOpTime" : {
                        "ts" : Timestamp(1545717724, 1),
                        "t" : NumberLong(1)
                },
                "readConcernMajorityOpTime" : {
                        "ts" : Timestamp(1545717724, 1),
                        "t" : NumberLong(1)
                },
                "appliedOpTime" : {
                        "ts" : Timestamp(1545717724, 1),
                        "t" : NumberLong(1)
                },
                "durableOpTime" : {
                        "ts" : Timestamp(1545717724, 1),
                        "t" : NumberLong(1)
                }
        },
        "lastStableCheckpointTimestamp" : Timestamp(1545717714, 1),
        "members" : [
                {
                        "_id" : 0,
                        "name" : "127.0.0.1:27017",
                        "health" : 1,
                        "state" : 1,
                        "stateStr" : "PRIMARY",
                        "uptime" : 55458,
                        "optime" : {
                                "ts" : Timestamp(1545717724, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2018-12-25T06:02:04Z"),
                        "syncingTo" : "",
                        "syncSourceHost" : "",
                        "syncSourceId" : -1,
                        "infoMessage" : "",
                        "electionTime" : Timestamp(1545662551, 1),
                        "electionDate" : ISODate("2018-12-24T14:42:31Z"),
                        "configVersion" : 1,
                        "self" : true,
                        "lastHeartbeatMessage" : ""
                },
                {
                        "_id" : 1,
                        "name" : "127.0.0.1:27018",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 55189,
                        "optime" : {
                                "ts" : Timestamp(1545717724, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDurable" : {
                                "ts" : Timestamp(1545717724, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2018-12-25T06:02:04Z"),
                        "optimeDurableDate" : ISODate("2018-12-25T06:02:04Z"),
                        "lastHeartbeat" : ISODate("2018-12-25T06:02:07.796Z"),
                        "lastHeartbeatRecv" : ISODate("2018-12-25T06:02:07.795Z"),
                        "pingMs" : NumberLong(0),
                        "lastHeartbeatMessage" : "",
                        "syncingTo" : "127.0.0.1:27017",
                        "syncSourceHost" : "127.0.0.1:27017",
                        "syncSourceId" : 0,
                        "infoMessage" : "",
                        "configVersion" : 1
                },
                {
                        "_id" : 2,
                        "name" : "127.0.0.1:27019",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 55189,
                        "optime" : {
                                "ts" : Timestamp(1545717724, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDurable" : {
                                "ts" : Timestamp(1545717724, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2018-12-25T06:02:04Z"),
                        "optimeDurableDate" : ISODate("2018-12-25T06:02:04Z"),
                        "lastHeartbeat" : ISODate("2018-12-25T06:02:07.796Z"),
                        "lastHeartbeatRecv" : ISODate("2018-12-25T06:02:07.795Z"),
                        "pingMs" : NumberLong(0),
                        "lastHeartbeatMessage" : "",
                        "syncingTo" : "127.0.0.1:27017",
                        "syncSourceHost" : "127.0.0.1:27017",
                        "syncSourceId" : 0,
                        "infoMessage" : "",
                        "configVersion" : 1
                }
        ],
        "ok" : 1,
        "operationTime" : Timestamp(1545717724, 1),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1545717724, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}


可以确定一下server的存储引擎是不是wiredTiger:

ubuntu@VM-0-3-ubuntu:~$ echo "db.serverStatus()" | mongo | grep wiredTiger
			"name" : "wiredTiger",
        "wiredTiger" : {

如果没有输出,则不是wiredTiger,再看看具体是什么。因为输出内容较多,在终端上很难找,可以输出到文件去后再找storageEngine:

ubuntu@VM-0-3-ubuntu:~$ echo "db.serverStatus()"| mongo > a.log
ubuntu@VM-0-3-ubuntu:~$ vi a.log

本来mongod默认是wiredTiger,我就遇到一个问题:之前装过旧版本的mongod,其存储引擎是mmapv1 ,在dbpath路径下留下了数据文件,换成4.0的mongod server后,依然设置了该数据目录,导致它去读了旧的数据文件,就初始化成了mmapv1 引擎,最后测试事务的时候总报错不支持“transaction numbers”,需要支持文档级别锁的存储引擎。

Mongo-go-driver测试事务

在gopath项目里使用go get或者go vendor下载github.com\mongodb\mongo-go-driver开源包,具体过程略。

mongo-go-driver是mongo官方的golang驱动库,目前还有频繁修改。

驱动源码里,连接server过程内会先生成连接池,然后返回有一个client对象,通过client对象可以对server里的数据库集合进行读写。但是任何读写操作本身是不带session对象的,所以在操作前会先生成一个默认的session对象,然后再从连接池中取一个连接来进行通信。而事务相关的接口是在session接口内,包括Transaction的Start、Abort、Commit,但session接口里并没有其它CRUD相关方法。

研究时走了些弯路,先看一下如下代码:

import (
	"context"
	"github.com/mongodb/mongo-go-driver/mongo"
	"net/url"
    "fmt"
)

func main(){
    connectString := "mongodb://127.0.0.1/test"
	dbUrl, err := url.Parse(connectString)
	if err != nil {
		fmt.Println(err)
         return
	}

    //认证参数设置,否则连不上
	opts := &options.ClientOptions{}
	opts.SetAuth(options.Credential{
			AuthMechanism:"SCRAM-SHA-1",
			AuthSource:"test",
			Username:"test",
			Password:"123456"})
			
	client, err = mongo.Connect(context.Background(), connectString,opts)
	if err != nil {
		fmt.Println(err)
         return
	}

    db := client.Database(dbUrl.Path[1:])
    
    ctx := context.Background()
	defer db.Client().Disconnect(ctx)
	
	col := db.Collection("test")
	//先在事务外写一条id为“111”的记录
    _,err = col.InsertOne(ctx, bson.M{"_id": "111", "name": "ddd", "age": 50})
    if(err != nil){
        fmt.Println(err)
        return
    }
	
	session,err := db.Client().StartSession()
	if(err != nil){
        fmt.Println(err)
        return
    }
    defer db.Client().EndSession()
    
    //开始事务
    err := session.StartTransaction()
    if(err != nil){
    	fmt.Println(err)
    	return
    }

    //在事务内写一条id为“222”的记录
    _, err = col.InsertOne(ctx, bson.M{"_id": "222", "name": "ddd", "age": 50})
    if(err != nil){
    	fmt.Println(err)
    	return
    }

    //写重复id
    _, err = col.InsertOne(ctx, bson.M{"_id": "111", "name": "ddd", "age": 50})
    if err != nil {
    	session.AbortTransaction(ctx)
    }else {
    	session.CommitTransaction(ctx)
    }
} 

//最终成功写入的数据有"111","222"两条,显然,理所当然认为的后两条数据并没有在事务内。

为什么读写操作没有在事务内?找任何一个操作源码看看,比如InsertOne:

//cellection.go

// InsertOne inserts a single document into the collection.
func (coll *Collection) InsertOne(ctx context.Context, document interface{},
	opts ...*options.InsertOneOptions) (*InsertOneResult, error) {

	if ctx == nil {
		ctx = context.Background()
	}

	doc, err := transformDocument(coll.registry, document)
	if err != nil {
		return nil, err
	}

	doc, insertedID := ensureID(doc)

    //下句很重要,从Context中获取的session
	sess := sessionFromContext(ctx) 

	err = coll.client.ValidSession(sess)
	if err != nil {
		return nil, err
	}

	wc := coll.writeConcern
	if sess != nil && sess.TransactionRunning() {
		wc = nil
	}
	oldns := coll.namespace()
	cmd := command.Insert{
		NS:           command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
		Docs:         []bsonx.Doc{doc},
		WriteConcern: wc,
		Session:      sess,
		Clock:        coll.client.clock,
	}

	// convert to InsertManyOptions so these can be argued to dispatch.Insert
	insertOpts := make([]*options.InsertManyOptions, len(opts))
	for i, opt := range opts {
		insertOpts[i] = options.InsertMany()
		insertOpts[i].BypassDocumentValidation = opt.BypassDocumentValidation
	}

    
	res, err := driver.Insert(
		ctx, cmd,
		coll.client.topology,
		coll.writeSelector,
		coll.client.id,
		coll.client.topology.SessionPool,
		coll.client.retryWrites,
		insertOpts...,
	)

	rr, err := processWriteError(res.WriteConcernError, res.WriteErrors, err)
	if rr&rrOne == 0 {
		return nil, err
	}

	return &InsertOneResult{InsertedID: insertedID}, err
}
//session.go

// sessionFromContext checks for a sessionImpl in the argued context and returns the session if it
// exists
func sessionFromContext(ctx context.Context) *session.Client {
    //从带key-Value的Context里取出session
	s := ctx.Value(sessionKey{})
	if ses, ok := s.(*sessionImpl); ses != nil && ok {
		return ses.Client
	}

	return nil
}

// driver/insert.go

func Insert(
	ctx context.Context,
	cmd command.Insert,
	topo *topology.Topology,
	selector description.ServerSelector,
	clientID uuid.UUID,
	pool *session.Pool,
	retryWrite bool,
	opts ...*options.InsertManyOptions,
) (result.Insert, error) {

	ss, err := topo.SelectServer(ctx, selector)
	if err != nil {
		return result.Insert{}, err
	}

     //Session为nil,则新建一个默认的session
	// If no explicit session and deployment supports sessions, start implicit session.
	if cmd.Session == nil && topo.SupportsSessions() {
		cmd.Session, err = session.NewClientSession(pool, clientID, session.Implicit)
		if err != nil {
			return result.Insert{}, err
		}
		defer cmd.Session.EndSession()
	}

	insertOpts := options.MergeInsertManyOptions(opts...)

	if insertOpts.BypassDocumentValidation != nil && ss.Description().WireVersion.Includes(4) {
		cmd.Opts = append(cmd.Opts, bsonx.Elem{"bypassDocumentValidation", bsonx.Boolean(*insertOpts.BypassDocumentValidation)})
	}
	if insertOpts.Ordered != nil {
		cmd.Opts = append(cmd.Opts, bsonx.Elem{"ordered", bsonx.Boolean(*insertOpts.Ordered)})
	}

	// Execute in a single trip if retry writes not supported, or retry not enabled
	if !retrySupported(topo, ss.Description(), cmd.Session, cmd.WriteConcern) || !retryWrite {
		if cmd.Session != nil {
			cmd.Session.RetryWrite = false // explicitly set to false to prevent encoding transaction number
		}
		return insert(ctx, cmd, ss, nil)
	}

	// TODO figure out best place to put retry write.  Command shouldn't have to know about this field.
	cmd.Session.RetryWrite = retryWrite
	cmd.Session.IncrementTxnNumber()

	res, originalErr := insert(ctx, cmd, ss, nil)

	// Retry if appropriate
	if cerr, ok := originalErr.(command.Error); ok && cerr.Retryable() ||
		res.WriteConcernError != nil && command.IsWriteConcernErrorRetryable(res.WriteConcernError) {
		ss, err := topo.SelectServer(ctx, selector)

		// Return original error if server selection fails or new server does not support retryable writes
		if err != nil || !retrySupported(topo, ss.Description(), cmd.Session, cmd.WriteConcern) {
			return res, originalErr
		}

		return insert(ctx, cmd, ss, cerr)
	}

	return res, originalErr
}

它这样设计,说明它希望在调用InsertOne里传入一个带session-value的context对象给它。问题关键是:sessionFromContext()方法还只认sessionKey{}这个key,而sessionKey struct是没有导出的,我们项目包里是访问不到,所以我们不能通过context.WithValue()方法建建一个session-value-context对象给它。

那么它包内肯定有构建session-value-context对象的,那就找sessionKey{}的出现位置,发现:

// UseSession creates a default session, that is only valid for the
// lifetime of the closure. No cleanup outside of closing the session
// is done upon exiting the closure. This means that an outstanding
// transaction will be aborted, even if the closure returns an error.
//
// If ctx already contains a mongo.Session, that mongo.Session will be
// replaced with the newly created mongo.Session.
//
// Errors returned from the closure are transparently returned from
// this method.
func (c *Client) UseSession(ctx context.Context, fn func(SessionContext) error) error {
	return c.UseSessionWithOptions(ctx, options.Session(), fn)
}

// UseSessionWithOptions works like UseSession but allows the caller
// to specify the options used to create the session.
func (c *Client) UseSessionWithOptions(ctx context.Context, opts *options.SessionOptions, fn func(SessionContext) error) error {
	defaultSess, err := c.StartSession(opts)
	if err != nil {
		return err
	}

	defer defaultSess.EndSession(ctx) //如果事务没有显示的Commit,内部会Abort。
    
    //在这里了
	sessCtx := sessionContext{
		Context: context.WithValue(ctx, sessionKey{}, defaultSess),
		Session: defaultSess,
	}

	return fn(sessCtx)
}

显然,只有如下closure闭包方式才能使用事务接口,修改golang测试代码如下:

import (
    "context"
    "github.com/mongodb/mongo-go-driver/mongo"
    "net/url"
    "fmt"
)

func main(){
    connectString := "mongodb://127.0.0.1/test"
    dbUrl, err := url.Parse(connectString)
    if err != nil {
        panic(err)
	}

    //认证参数设置,否则连不上
	opts := &options.ClientOptions{}
	opts.SetAuth(options.Credential{
			AuthMechanism:"SCRAM-SHA-1",
			AuthSource:"test",
			Username:"test",
			Password:"123456"})
			
    client, err = mongo.Connect(context.Background(), connectString,opts)
    if err != nil {
        panic(err)
	}

    db := client.Database(dbUrl.Path[1:])
    
    ctx := context.Background()
    defer db.Client().Disconnect(ctx)
    
    col := db.Collection("test")
    
    //先在事务外写一条id为“111”的记录
    _,err = col.InsertOne(ctx, bson.M{"_id": "111", "name": "ddd", "age": 50})
    if(err != nil){
        fmt.Println(err)
        return
    }
    
    //第一个事务:成功执行
    db.Client().UseSession(ctx, func(sessionContext mongo.SessionContext) error {
        err = sessionContext.StartTransaction()
        if(err != nil){
            fmt.Println(err)
            return err
        }

        //在事务内写一条id为“222”的记录
        _, err = col.InsertOne(sessionContext, bson.M{"_id": "222", "name": "ddd", "age": 50})
        if(err != nil){
            fmt.Println(err)
            return err
        }

        //在事务内写一条id为“333”的记录
        _, err = col.InsertOne(sessionContext, bson.M{"_id": "333", "name": "ddd", "age": 50})
        if err != nil {
            sessionContext.AbortTransaction(sessionContext)
            return err
        }else {
            sessionContext.CommitTransaction(sessionContext)
        }
        return nil
    })
	
    //第二个事务:执行失败,事务没提交,因最后插入了一条重复id "111",
    err = db.Client().UseSession(ctx, func(sessionContext mongo.SessionContext) error {
        err := sessionContext.StartTransaction()
        if(err != nil){
            fmt.Println(err)
            return err
        }

        //在事务内写一条id为“222”的记录
        _, err = col.InsertOne(sessionContext, bson.M{"_id": "444", "name": "ddd", "age": 50})
        if(err != nil){
            fmt.Println(err)
            return err
        }

		//写重复id
        _, err = col.InsertOne(sessionContext, bson.M{"_id": "111", "name": "ddd", "age": 50})
        if err != nil {
            sessionContext.AbortTransaction(sessionContext)
            return err
        }else {
            sessionContext.CommitTransaction(sessionContext)
        }
        return nil
    })
} 

//最终数据只有 "111","222","333" 三条,事务测试成功。
    
  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值