golang 操作 mongodb 使用的包是 “gopkg.in/mgo.v2”,coding 过程中需要并发读写 mongodb 数据库,简单观摩了下源码,记录下自己的一些理解,如有错误,敬请斧正。
一般来说,我们直接这样创建一个 Session:
Session, err = mgo.Dial(URL)
if err != nil {
log.Println(err)
}
来看看Dial这个函数做了什么:
func Dial(url string) (*Session, error) {
session, err := DialWithTimeout(url, 10*time.Second)
if err == nil {
session.SetSyncTimeout(1 * time.Minute)
session.SetSocketTimeout(1 * time.Minute)
}
return session, err
}
调用 DialWithTimeout 函数设置默认的超时时间是 10 秒。该函数中调用了 DialWithInfo 这个函数,而 DialWithInfo 函数中比较重要是是调用了newSession,看看这个函数做了什么操作:
func newSession(consistency Mode, cluster *mongoCluster, timeout time.Duration) (session *Session) {
cluster.Acquire()
session = &Session{
cluster_: cluster,
syncTimeout: timeout,
sockTimeout: timeout,
poolLimit: 4096,
}
debugf("New session %p on cluster %p", session, cluster)
session.SetMode(consistency, true)
session.SetSafe(&Safe{})
session.queryConfig.prefetch = defaultPrefetch
return session
}
返回的session设置了一些默认的参数,暂时先忽略,直接看看Session的数据结构:
type Session struct {
m sync.RWMutex
cluster_ *mongoCluster
slaveSocket *mongoSocket
masterSocket *mongoSocket
slaveOk bool
consistency Mode
queryConfig query
safeOp *queryOp
syncTimeout time.Duration
sockTimeout time.Duration
defaultdb string
sourcedb string
dialCred *Credential
creds []Credential
poolLimit int
bypassValidation bool
}
m 是 mgo.Session 的并发锁,因此所有的 Session 实例都是线程安全的。slaveSocket,masterSocket 代表了该 Session 到 mongodb 主节点和从节点的一个物理连接的缓存。而 Session 的策略总是优先使用缓存的连接。是否缓存连接,由 consistency 也就是该 Session 的模式决定。假设在并发程序中,使用同一个 Session 实例,不使用 Copy,而该 Session 实例的模式又恰好会缓存连接,那么,所有的通过该 Session 实例的操作,都会通过同一条连接到达 mongodb。虽然 mongodb 本身的网络模型是非阻塞通信,请求可以通过一条链路,非阻塞地处理,但是会影响效率。
其次 mgo.Session 缓存的一主一从连接,实例本身不负责维护。也就是说,当 slaveSocket,masterSocket 任意其一,连接断开,Session 自己不会重置缓存,该 Session 的使用者如果不主动重置缓存,调用者得到的将永远是 EOF。这种情况在主从切换时就会发生,在网络抖动时也会发生。
mgo 的 DB 句柄需要你做一个 copy 操作:
// Copy works just like New, but preserves the exact authentication
// information from the original session.
func (s *Session) Copy() *Session {
s.m.Lock()
scopy := copySession(s, true)
s.m.Unlock()
scopy.Refresh()
return scopy
}
copySession 将源 Session 浅拷贝到临时 Session 中,这样源 Session 的配置就拷贝到了临时 Session 中。关键的 Refresh,将源 Session 浅拷贝到临时 Session 的连接缓存指针,也就是 slaveSocket,masterSocket 置为空,这样临时 Session 就不存在缓存连接,而转为去尝试获取一个空闲的连接。
mgo 自身维护了一套到 mongodb 集群的连接池。这套连接池机制以 mongodb 数据库服务器为最小单位,每个 mongodb 都会在 mgo 内部,对应一个 mongoServer 结构体的实例,一个实例代表着 mgo 持有的到该数据库的连接。看看这个连接池的定义:
type mongoServer struct {
sync.RWMutex
Addr string
ResolvedAddr string
tcpaddr *net.TCPAddr
unusedSockets []*mongoSocket
liveSockets []*mongoSocket
closed bool
abended bool
sync chan bool
dial dialer
pingValue time.Duration
pingIndex int
pingCount uint32
pingWindow [6]time.Duration
info *mongoServerInfo
}
info 代表了该实例对应的数据库服务器在集群中的信息——是否 master,ReplicaSetName 等。而两个 Slice,就是传说中的连接池。unusedSockets 存储当前空闲的连接,liveSockets 存储当前活跃中的连接,Session 缓存的连接就同时存放在 liveSockets 切片中,而临时 Session 获取到的连接就位于 unusedSockets 切片中。
每个 mongoServer 都会隶属于一个 mongoCluster 结构,相当于 mgo 在内部,模拟出了 mongodb 数据库集群的模型。
type mongoCluster struct {
sync.RWMutex
serverSynced sync.Cond
userSeeds []string
dynaSeeds []string
servers mongoServers
masters mongoServers
references int
syncing bool
direct bool
failFast bool
syncCount uint
setName string
cachedIndex map[string]bool
sync chan bool
dial dialer
}
mongoCluster 持有一系列 mongoServer 的实例,以主从结构分散到两个数组中。 每个 Session 都会存储自己对应的,要操作的 mongoCluster 的引用。
前面的描述可以总结成下面这张图:
那么我们在使用的时候就可以创建一个 Session,然后 clone 操作,用 clone 得到的 copysession 完成操作,结束后关闭这个 copysession 就可以了。