周末遇到一个服务卡死的问题,经过日志分析发现,可能锁死在两处数据库操作里面了,查看了rds数据库服务端慢日志,又无所获。当时,觉得可能存在的问题就是在一个数据库的事务操作流程里面,又执行了一个另外一个表的数据库写操作,因为,不知道确切原因,所以,先把这个数据库写操作移到了事务操作的外面,那么后面问题没有发生。
周一上班,再进一步和同事讨论分析这个问题,发现果然是mysql事务里面执行的这条数据库写操作造成的资源死锁,我又踩坑了。
问题代码如下:
//query and lock line 拿锁
if err := tx.Set("gorm:query_option", "FOR UPDATE").Take(&xxx, xxx).Error; err != nil {
return err
}
// 事务操作Update
tx.Model(&xxx).Update("game_duration", xxx)
// 另外一个数据库操作,需要拿连接,以前就有的函数,直接拿来复用
if e := yyy(); e != nil {
}
// 事务操作Commit
if err := tx.Commit().Error; err != nil {
return err
}
看出来了吗???
mysql事务本身会获取一个数据库可用连接来执行事务操作,这个连接在事务Commit以后释放;而Commit前面的一个数据库操作本身也要获取一个连接来执行里面的exec操作;当有大量类似流程的事务上来以后,那么就造成了,占着事务连接本身,在释放事务连接之前,如果拿不到yyy这个数据库操作所需要的连接,就会一直卡在yyy里面等待,本身占用的事务连接也不会释放,从而构造出了资源死锁的局面!!!拿着资源又去申请资源这种场景万万不能用于mysql数据库事务操作的场景,这个坑还是比较隐蔽~~
Commit后再去执行其他数据库操作,获取连接就没问题了,Commit过程中会释放当前事务连接。
func (tx *Tx) Commit() error {
// Check context first to avoid transaction leak.
// If put it behind tx.done CompareAndSwap statement, we can't ensure
// the consistency between tx.done and the real COMMIT operation.
select {
default:
case <-tx.ctx.Done():
if atomic.LoadInt32(&tx.done) == 1 {
return ErrTxDone
}
return tx.ctx.Err()
}
if !atomic.CompareAndSwapInt32(&tx.done, 0, 1) {
return ErrTxDone
}
var err error
withLock(tx.dc, func() {
err = tx.txi.Commit()
})
if err != driver.ErrBadConn {
tx.closePrepared()
}
tx.close(err)
return err
}
// close returns the connection to the pool and
// must only be called by Tx.rollback or Tx.Commit.
func (tx *Tx) close(err error) {
tx.cancel()
tx.closemu.Lock()
defer tx.closemu.Unlock()
tx.releaseConn(err)
tx.dc = nil
tx.txi = nil
}
后人观之,鉴之,而不犯之。