教训:使用gorm框架因主从同步延迟导致全表更新

1,383 阅读3分钟

背景

涉及目标表visitors结构如下:

CREATE TABLE `visitors` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `openid` varchar(100) DEFAULT '' COMMENT 'openid',
  `unionid` varchar(100) DEFAULT '' COMMENT 'unionid',
  `member_id` int(11) DEFAULT '0' COMMENT 'member_id',
  `account_id` varchar(50) DEFAULT '' COMMENT 'account_id',
  ... -- 省略了一些字段
  `created_at` datetime DEFAULT NULL COMMENT '创建时间',
  `updated_at` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
)

去年10月国庆期间,出现了visitors表的所有记录被更新为同一个member_id,当时没有查到事故的原因,搁置了问题,当时想着,基于visitors的功能已经上线两个月了,应该很稳定才对,小概率事件,后面肯定(应该)不会出现。

于是,三个月过去了。

元旦,请了长假,回家办事,1月3号,visitors表中50多万的用户数据的member_id又被更新为了同一个用户了,这个时候,这个问题引起了我足够的重视。1月5号,花了大半天的时间使用了前日备份的数据有损的修复了数据(最近的10几个小时的数据变更没有了)。从代码层面强制避免了该问题的发生,但是当时为什么出现这种更新全局表的情况并没有找到根本的原因,于是又搁置了(再也不会更新全表了,但是数据的完整性肯定是有影响的,只是是小概率事件,没放在心上)。

于是,两个月过去了。

来到了三月,在功能逻辑简化之后,出现了很多visitors表数据记录某些字段没有更新。仔细分析之后,发现是主从同步问题导致,联系到之前元旦出现的问题,总算是明白一系列事故出现的逻辑。

即主从同步导致无法查询到刚创建的数据,导致更新数据库的行记录出现问题。

演示

直接原因:访问数据库的连接使用的是代理的模式,写操作会操作主库,读操作会操作从库。

创建记录

insert into visitors(openid, unionid) values("openid", "unionid");

更新记录

select * from visitors where openid="openid";
update visitors set member_id = 100, account_id='gog' where openid="openid"; 

代码分析:


type Visitors struct {
	ID           int64     `json:"id"`
	Openid       string    `json:"openid"`
	UnionId      string    `json:"unionid"`
	MemberId     int64     `json:"member_id"`
	CreatedAt    time.Time `json:"created_at"`
	UpdatedAt    time.Time `json:"updated_at"`
        RegisteredAt time.Time `json:"registered_at"`
	AccountId    string    `json:"account_id"`
}
func GetVisitorByOpenId(openId string) (*Visitors, error) {
	visitor := new(Visitors)
	err := MysqlDB.Where("openid=?", openId).Last(&visitor).Error
	return visitor, err
}
visitor, err := GetVisitorByOpenId(userInfo.OpenID)
// err处理逻辑 
// 更新
err = yidui_mini_mysql.UpdateMemberAttrs(visitor, map[string]interface{}{
        "member_id":     member.ID,
        "registered_at": time.Now(),
        "account_id":    authService.AccountId,
})

func UpdateMemberAttrs(visitor *Visitors, elements map[string]interface{}) error {
	if visitor == nil || visitor.ID == 0 {
		return errors.New("UpdateMemberAttrs visitor is not correct")
	}
	return MysqlDB.Model(visitor).Updates(elements).Error
}

1、如果刚创建对象,使用GetVisitorByOpenId很可能查不到数据,visitor变量就指向了一块空内存,在UpdateMemberAttrs中如果没有visitor == nil || visitor.ID == 0判断,就演变成了更新全表的情况,元旦的时候增加了这个判断,但是后续查询日志的时候出现了极少的数据因为空判断而缺失数据完整性的现象。

2、在三月,由于业务需求的变更,精简了insertupdate之间的语句,减少了两个操着之间的执行时间间隔,调用UpdateMemberAttrs方法出现空visitor的情况一下子变多了,当时的采用了最直接的处理方法,让读和写都使用主库连接,避免出现读取从库的现象。

不过,这些数据延迟问题应该通过在创建数据或者更新数据之后保存完整的当前数据库的数据对象,之后的操作应该是直接使用该对象操作,而不是再次从mysql中获取来解决。

在golang里面更新部分数据使用orm是比较麻烦,即使是gorm,用起来也不是怎么方便,现在看来还是写原生的sql语句来拼接比较合适。