AlexiaChen / AlexiaChen.github.io

My Blog https://github.com/AlexiaChen/AlexiaChen.github.io/issues
87 stars 11 forks source link

Golang中decimal与Gorm MySQL的一个问题 #177

Open AlexiaChen opened 11 months ago

AlexiaChen commented 11 months ago

最近在做一个金融相关账务的后台业务,主要是监听链上的合约emit出的Event,把Event存储库的表中,golang用的是Gorm来访问MySQL。但是发现用以下Gorm Update一个字段的时候,在及少数情况下,小数点会计算错误, 其中 Gorm的结构对象的两个字段是用decimal.Decimal来存储的金额,当然decimal。Decimal就是可以用来做账的。

// 可用余额
BalanceAvailable decimal.Decimal `gorm:"type:decimal(36,18);not null;default:'0';check:balance_available >= '0'" json:"balance_available"`
// 冻结余额
BalanceFrozen decimal.Decimal `gorm:"type:decimal(36,18);not null;default:'0';check:balance_frozen >= '0'" json:"balance_frozen"`

如果是一下更新Balance Available Balance Frozen的金额,在极端情况下会有错误,比如计算1 - 0.7的时候,会计算成0.30000000000000004, 我不知道在gorm.Expr里面发生了什么,或者是MySQL对decimal.Decimal支持的问题,版本原因,反正是错误了

func UpdateAccountBalanceAvailable(db *gorm.DB, accountId uint64, tokenID TokenId, changeValue decimal.Decimal) error {
    return db.Model(&Account{}).Where("account_id = ? AND token_id = ?", accountId, tokenID).
        Update("balance_available", gorm.Expr("balance_available + ?",  changeValue )).Error
}

func UpdateAccountBalanceFrozen(db *gorm.DB, accountId uint64, tokenID TokenId, changeValue decimal.Decimal) error {
    return db.Model(&Account{}).Where("account_id = ? AND token_id = ?", accountId, tokenID).
        Update("balance_frozen", gorm.Expr("balance_frozen+ ?",  changeValue )).Error
}

然后修改成以下代码,就不会错误了,就是把decimal.Decimal的计算,放到gorm.Expr外面来,计算完再更新进去,我写的单元测试就过了。也就是正确了,懒得深究就为什么了,不知道是gorm还是MySQL的原因,因为我本地的MySQL是5.x。第一次做金融账务相关的业务,也算是第一次真正用golang写后端代码。坑还是不少的。我以下面的写法应该就绕过这个坑了,也不需要管到底是Gorm的问题还是MySQL的问题了。

func UpdateAccountBalanceAvailable(db *gorm.DB, accountId uint64, tokenID TokenId, changeValue decimal.Decimal) error {
    account, err := GetAndLockAccount(db, accountId, tokenID)
    if err != nil {
        return fmt.Errorf("GetAndLockAccount When UpdatesAccountBalanceAvailable error: %v", err)
    }
    updatedValue := account.BalanceAvailable.Add(changeValue)
    return db.Model(&Account{}).Where("account_id = ? AND token_id = ?", accountId, tokenID).
        Update("balance_available", updatedValue).Error
}

func UpdateAccountBalanceFrozen(db *gorm.DB, accountId uint64, tokenID TokenId, changeValue decimal.Decimal) error {
    account, err := GetAndLockAccount(db, accountId, tokenID)
    if err != nil {
        return fmt.Errorf("GetAndLockUniGasAccount When UpdateUniGasAccountBalanceFrozen error: %v", err)
    }
    updatedValue := account.BalanceFrozen.Add(changeValue)
    return db.Model(&Account{}).Where("account_id = ? AND token_id = ?", accountId, tokenID).
        Update("balance_frozen", updatedValue).Error
}

以上代码,在单元测试中,就把1 - 0.7,计算成0.3

不过我可以猜测一下,原因就在于gorm.Expr中,decimal调用的+ 的实现,绝对就不是decimal.Decimal自带的那个Add方法实现。可能跟SQL的实现有关,MySQL的概率更大些,Gorm本身有问题的概率小一些。因为我记得SQL Update语句就是可以 Update var = var + XX 这样的表达。看来以后要有个最佳实践,就是凡是涉及数据库中字段的一些计算,最好放在业务侧处理,不要用任何SQL提供的这些操作。当然哈,这里不包括SQL 提供的 COUNT SUM什么的。

AlexiaChen commented 10 months ago

后面简单验证了一下,确实就是MySQL版本的问题,MySQL 5.7.42 会出现此问题, MySQL 8就不会了。