不积跬步,无以至千里

数据库事务导致的更新失败


今天遇到一个获取数据库连接失败的问题,简化后的示例代码如下:

package basic

import (
	"fmt"
	"github.com/jinzhu/gorm"
	_ "github.com/go-sql-driver/mysql"
)

type Product struct {
	gorm.Model
	Code  string
	Price uint
}

var db *gorm.DB

func initDb() {
	dbGorm, err := gorm.Open("mysql", "appclouddev:password@tcp(10.8.121.175:3306)/appcloud_test?charset=utf8&parseTime=True&loc=Local")
	if err != nil {
		fmt.Printf("init db error:%v", err)
		panic(fmt.Sprintf("Init db: connect db error, %v", err))
	}
	db = dbGorm
}
func TestGormConnect() {
	initDb()
	defer db.Close()
	tx := db.Begin()
	//err := Db.Table("products").CreateTable(&Product{Code: "L1212", Price: 1000}).Error  //第一次执行创建测试数据表
	err := tx.Table("products").Where("code='1'").Update("code","abc1").Error
	fmt.Println("tx update  err,err=", err)
	err = db.Table("products").Create(&Product{Code: "L1211", Price: 1000}).Error
	fmt.Println("normal create data err,err=", err)
	tx.Commit()
}

在执行的数据库操作中,首先开启一个事务,然后在事务中再次获取数据库连接更新/创建数据库记录,最后提交事务.执行测试代码会发现在执行第二个创建语句的时候获取数据库连接超时.

由于对gorm不熟悉,一开始怀疑是不是由于连接获取不到的问题.但是看了下源码,写了下测试代码验证,发现并不是.

func Open(driverName, dataSourceName string) (*DB, error) {
	driversMu.RLock()
	driveri, ok := drivers[driverName]
	driversMu.RUnlock()
	if !ok {
		return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
	}

	if driverCtx, ok := driveri.(driver.DriverContext); ok {
		connector, err := driverCtx.OpenConnector(dataSourceName)
		if err != nil {
			return nil, err
		}
		return OpenDB(connector), nil
	}

	return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}

可以看到gorm把线程池维护在了*DB

func (s *DB) Table(name string) *DB {
	clone := s.clone()
	clone.search.Table(name)
	clone.Value = nil
	return clone
}

每次使用之前的s.clone()的操作,也只是拷贝的跟单次数据库操作相关的内容,对数据库连接通过指针拷贝的方式保证了同时只有一个DB对象.所以不是数据库连接的问题.

排除掉gorm使用的问题之后,就需要考虑mysql事务的问题了,现在问题就简单了,首先考虑事务导致数据被锁定,gap锁导致无法对临近的数据做操作.针对当时操作的具体数据,在操作的插入数据之前,通过事务做了数据更新操作,而检查数据表发现,更新时使用的查询条件并没有添加索引.在使用事务的时候,会对涉及到的数据做锁定,而没有索引的事务带来的灾难性问题就是整张表被锁.

定位到问题之后,解决方案就好办了,既然是因为整张表被锁导致无法对数据表做更新操作,最好的解决方案就是添加索引,减少被锁定的数据记录,由此,问题解决.

再次复习下mysql innodb引擎的锁机制. Innodb存储引擎

在ReadCommited和RepeatableRead下,InnoDB存储引擎使用非锁定的一致性读.但在ReadCommitted事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据.在Repeatable事务隔离级别下和RepeatableRead事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本. InnoDB存储引擎中,通过NextKeyLock算法来避免不可重复读的问题,在这个算法下,对于索引的扫描,不仅仅锁住扫描到的索引,而且还锁住这些索引覆盖的范围,避免了另外的事务在这个范围内插入数据导致不可重复读的问题.因此InnoDB存储引擎默认的事务隔离级别是ReadRepeatable