AlexiaChen.github.io
AlexiaChen.github.io copied to clipboard
Golang中decimal与Gorm MySQL的一个问题
最近在做一个金融相关账务的后台业务,主要是监听链上的合约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什么的。
后面简单验证了一下,确实就是MySQL版本的问题,MySQL 5.7.42 会出现此问题, MySQL 8就不会了。