Skip to content

支付环境下复杂的嵌套事务

Tuuz edited this page Jun 20, 2022 · 5 revisions

对应Model:

1.查询某个用户的余额(model)

BalanceModel.go

type Interface struct {
	Db gorose.IOrm
}

func (self *Interface) Api_find(uid, fid, cid interface{}) gorose.Data {
	db := self.Db.Table(table)
	where := map[string]interface{}{
		"uid": uid,
		"fid": fid,
		"cid": cid,
	}
	db.Where(where)
	db.LockForUpdate()
	ret, err := db.Find()
	if err != nil {
		Log.Dbrr(err, tuuz.FUNCTION_ALL())
		return nil
	} else {
		return ret
	}
}

2.为用户增加余额:

BalanceModel.go

func (self *Interface) Api_incr(uid, fid, cid, incr_balance interface{}) bool {
	db := self.Db.Table(table)
	where := map[string]interface{}{
		"uid": uid,
		"fid": fid,
		"cid": cid,
	}
	db.Where(where)
	db.LockForUpdate()
	_, err := db.Increment("balance", incr_balance)
	if err != nil {
		Log.Dbrr(err, tuuz.FUNCTION_ALL())
		return false
	} else {
		return true
	}
}

3.查询用户最后一条记录的model(一般是用于查询用户记录表中最后一条的余额是否可以和用户的显示余额对上)

BalanceRecordModel.go

type Interface struct {
	Db gorose.IOrm
}

func (self *Interface) Api_find_last(uid, fid, cid interface{}) gorose.Data {
	db := self.Db.Table(table)
	where := map[string]interface{}{
		"uid": uid,
		"fid": fid,
		"cid": cid,
	}
	db.Where(where)
	db.OrderBy("id desc")
	db.LockForUpdate()
	ret, err := db.Find()
	if err != nil {
		Log.Dbrr(err, tuuz.FUNCTION_ALL())
		return nil
	} else {
		return ret
	}
}

BalanceRecordModel.go

func (self *Interface) Api_insert(uid, fid, cid, Type, order_id, before, amount, after, extra, remark1, remark2 interface{}) bool {
	db := self.Db.Table(table)
	data := map[string]interface{}{
		"uid":      uid,
		"fid":      fid,
		"cid":      cid,
		"type":     Type,
		"order_id": order_id,
		"before":   before,
		"amount":   amount,
		"after":    after,
		"extra":    extra,
		"remark1":  remark1,
		"remark2":  remark2,
	}
	db.Data(data)
	db.LockForUpdate()
	_, err := db.Insert()
	if err != nil {
		Log.Dbrr(err, tuuz.FUNCTION_ALL())
		return false
	} else {
		return true
	}
}

对应Action

ActionModel逻辑层-BalanceAction.go

查询余额所用的BalanceAction,后面查询余额的复用功能都需要走这个action

func (self *Interface) App_check_balance(uid, fid, cid interface{}) decimal.Decimal {
	var balmodel BalanceModel.Interface
	balmodel.Db = self.Db
	userbalance := balmodel.Api_find(uid, fid, cid)
	if len(userbalance) > 0 {
		return Calc.ToDecimal(userbalance["balance"])
	} else {
		balmodel.Api_insert(uid, fid, cid, 0)
		return decimal.Zero
	}
}
func (self *Interface) App_single_balance(uid, fid, cid, Type, order_id interface{}, amount decimal.Decimal, extra, remark1, remark2 interface{}) error {
	db := self.Db
	if order_id == nil {
		order_id = Calc.GenerateOrderId()
	}
//Begin注解:如果外部已经执行过Begin开启事务后,当你在此处再次执行Begin指令后,相当于这条*db流就开启了子事务,在这条*db流被rollback或者commit之后,该条事务会被当做savepoint进行提交,但是在父事务最终提交前,任一子事务都不会写入数据库
//请放心,这里和Thinkphp的think\Db::startTrans()是一个用法,如果你不理解,按照PHP里面那么写就行
	db.Begin()
	balance_left := self.App_check_balance(uid, fid, cid)
	if balance_left.Add(amount).LessThan(decimal.Zero) {
                //请关注最后Rollback注解
		db.Rollback()
		return errors.New("余额不足")
	}
	var balance BalanceModel.Interface
	balance.Db = db
	balance.Api_incr(uid, fid, cid, amount)

	//插入变动数据
	var balancerecord BalanceRecordModel.Interface
	balancerecord.Db = db
	last_record := balancerecord.Api_find_last(uid, fid, cid)
	after := decimal.Zero
	before := "0"
	if len(last_record) > 0 {
		after = Calc.Bc_add(last_record["after"], amount)
		before = last_record["after"].(string)
	} else {
		after = amount
	}
	if after.LessThan(decimal.Zero) {
                //请关注Rollback注解
		db.Rollback()
		return errors.New("余额记录不足")
	}
	if !balancerecord.Api_insert(uid, fid, cid, Type, order_id, before, amount, after, extra, remark1, remark2) {
//Rollback注解:请注意这里执行Rollback后,仅仅会将当前的Begin指令进行恢复,不会影响到之前的事务,该功能仅在GorosePro中支持,请勿在原版框架中使用!!!
		db.Rollback()
		return errors.New("balance_record添加失败")
	}
//Commit注解:请注意,这里Commit只会提交上面从Begin开始的子事务,不会影响到父级事务,该功能仅在GorosePro中支持,请勿在原版框架中使用!!!
	db.Commit()
	return nil
}

转账方法需要调用两次如上的资金变动action

本action干的事情就是调用1次资金减少,再调用1次资金增加从而完成转账,如果有必要,可以向一个专门用于记录转账信息的表中再写入一条转账记录数据(有别于资金记录),这里就是一个完整的带有手续费功能的转账方法

func (self *Interface) App_single_transfer(uid, fid, cid, to_uid, order_id interface{}, amount decimal.Decimal, extra, remark1, remark2 interface{}) (err error) {
	if len(FacilityModel.Api_find(fid)) < 1 {
		return errors.New("机构不存在")
	}
	db := self.Db
	if order_id == nil {
		order_id = Calc.GenerateOrderId()
	}
	user := UserModel.Api_find(to_uid)
	if len(user) < 1 {
		err = errors.New("没有找到接收人")
		return
	}
//这里开始了一个“父级”的事务,一般来说转账方法的上层就已经是Controller了,所以一般本action不会作为子action被执行
	coin := CoinModel.Api_find(cid)
	if len(coin) < 1 {
		db.Rollback()
		err = errors.New("未找到币种")
		return
	}
	transfer_fee := coin["transfer_fee"].(float64)
	transfer_time_limit := coin["transfer_time_limit"].(int64)
	transfer_limit := coin["transfer_limit"].(float64)
	transfer_per_limit := coin["transfer_per_limit"].(float64)
	var trm TransferRecordModel.Interface
	trm.Db = db
	todays_time := trm.Api_count_today(uid, Date.Today())
	if todays_time > transfer_time_limit {
		db.Rollback()
		err = errors.New("超过当日最大转出数量")
		return
	}
	todays_amount := trm.Api_sum_today(uid, Date.Today())
	if todays_amount.GreaterThan(Calc.ToDecimal(transfer_limit)) {
		db.Rollback()
		err = errors.New("超过当日最大转出数量")
		return
	}
	if decimal.NewFromFloat(transfer_per_limit).LessThan(amount) {
		db.Rollback()
		err = errors.New("超过单笔最大限制")
		return
	}
	fee := Calc.Bc_mul(transfer_fee, amount)
	after_amount := Calc.Bc_min(amount, fee)
//这个方法是扣款方法,在这func下就是转出的意思
	err = self.App_single_balance(uid, fid, cid, 22, order_id, amount.Abs().Neg(), extra, "用户转账转出", remark2)
	if err != nil {
//如果这个执行了Rollback,就会把当前的父级的事务给结束掉
		db.Rollback()
//如果上面执行了Rollback,这里通常要return把程序停掉
		return
	}
//这个方法就是加钱的方法,在这个func下就是转入的意思
	err = self.App_single_balance(to_uid, fid, cid, 21, order_id, after_amount.Abs(), extra, "用户收款转入", remark2)
	if err != nil {
//这里同上
		db.Rollback()
		return
	}
//最后在完成后,插入一条转账记录
	if !trm.Api_insert(uid, cid, 0, order_id, amount, fee, after_amount.Abs(), 1, 1, "") {
//这里如果插入记录失败了,虽然这是一个model但是依然可以执行rollback,只要这个父级的rollback被执行并return,之前所有的子级事务全部都会被回档,不会写入数据库
		db.Rollback()
		err = errors.New("插入转账记录失败")
		return
	}
//如果一切顺利,这里执行Commit之后,之前两个资金的Action+最后一条转账记录将会被确认写入到数据库
	db.Commit()
	return
}

到此一个复杂的解耦嵌套事务就结束了

死锁注意事项

避免同源增改查

  • 尽量避免相同数据库下同时调用修改和查询接口,例如:
func (self *Interface)Api_insert()

func Api_Find()

非常容易造成死锁!

避免同源查增改查

同时存在在一个事务内,不要再继承db之后再调用非db类型(需要重新init)的方法,例如:

id:=UserMode.Api_find()
var usr UserModel.Interface
usr=tuuz.Db()
if usr.Api_insert(){
//....执行代码
}
id:=UserMode.Api_find()

这样执行有极大的概率会在生产环境中出错,造成不可预料的死锁,所以千万要避免掉!

Clone this wiki locally