用gorm谈谈mysql中的事务操作

  • 2020 年 3 月 27 日
  • 筆記

后端的小伙伴经常面对并发的情况,特别是电商网站,经常会被刷单,那么我们改怎么防止被刷单呢?这个时候有的小伙伴会跳出来说用事务。是的,因为事务具有一下特性:
  • 原子性(Atomicity)
  • 一致性(Consistency)
  • 隔离性(Isolation)
  • 持久性(Durability)

但是开启了事务就可以了么?

下面我们以gorm为例讲解一下,为什么,同时让大家熟悉一下gorm的是如何开启事务的。

GORM 默认会将单个的 create, update, delete操作封装在事务内进行处理,以确保数据的完整性。如果你想把多个 create, update, delete 操作作为一个原子操作,Transaction 就是用来完成这个的。

我们以订单的支付流程为例:

订单表

package models    import "github.com/jinzhu/gorm"    type Order struct {    gorm.Model    Price  float64 `gorm:"type:decimal(20,2)"` //0表示未支付 1表示已经支付    UserId uint    Status uint8 `gorm:"default:0"` //0表示未支付 1表示已经支付  }  

用户表

package models    import "github.com/jinzhu/gorm"    type User struct {    gorm.Model    Balance float64 `gorm:"type:decimal(20,2)"`  }  
主要的业务逻辑  
package main    import (    "errors"    "fmt"    "ginLearn.com/models"    "github.com/jinzhu/gorm"    "time"  )    func payOrder() {    db := models.DB()    user := models.User{}    user.ID = 1    order := models.Order{}    db.First(&user)    db.Order("RAND()").Where("status=0").First(&order)    if user.Balance >= order.Price {      if order.ID > 0 && order.Status == 0 {        //如果个人资金大于订单价格就支付        //这里有坑,在并发的情况下就会出问题,当两个请求同时走到了这里,就出现了刷单的情况        user.Balance = user.Balance - order.Price        db.Save(&user)        order.Status = 1        db.Save(&order)      }    } else {      //抛出错误    }  }  func payOrderTransactionAuto() error {    return models.DB().Transaction(func(db *gorm.DB) error {      user := models.User{}      user.ID = 1      order := models.Order{}      db.Set("gorm:query_option", "FOR UPDATE").First(&user)      db.Where("status=0").Order("RAND()").First(&order)      if user.Balance >= order.Price {        if order.ID > 0 && order.Status == 0 {          //如果个人资金大于订单价格就支付          //这里有坑,在并发的情况下就会出问题,当两个请求同时走到了这里,就出现了刷单的情况          user.Balance = user.Balance - order.Price          db.Save(&user)          order.Status = 1          db.Save(&order)          return nil        } else {          return errors.New("重复支付订单")        }      } else {        //抛出错误        return errors.New("个人账户金额小于订单金额")      }    })  }  func payOrderTransaction() error {    tx := models.DB().Begin()      user := models.User{}    user.ID = 1    order := models.Order{}    tx.Set("gorm:query_option", "FOR UPDATE").First(&user)    tx.Where("status=0").Order("RAND()").First(&order)    if user.Balance >= order.Price {      if order.ID > 0 && order.Status == 0 {        //如果个人资金大于订单价格就支付        //这里有坑,在并发的情况下就会出问题,当两个请求同时走到了这里,就出现了刷单的情况        user.Balance = user.Balance - order.Price        tx.Save(&user)        order.Status = 1        tx.Save(&order)        tx.Commit()        return nil      } else {        tx.Rollback()        return errors.New("重复支付订单")      }    } else {      //抛出错误      tx.Rollback()      return errors.New("个人账户金额小于订单金额")    }  }  func payOrderTransactionUnlock() error {    tx := models.DB().Begin()      user := models.User{}    user.ID = 1    order := models.Order{}    tx.First(&user)    tx.Where("status=0").Order("RAND()").First(&order)    if user.Balance >= order.Price {      if order.ID > 0 && order.Status == 0 {        //如果个人资金大于订单价格就支付        //这里有坑,在并发的情况下就会出问题,当两个请求同时走到了这里,就出现了刷单的情况        user.Balance = user.Balance - order.Price        tx.Save(&user)        order.Status = 1        tx.Save(&order)        tx.Commit()        return nil      } else {        tx.Rollback()        return errors.New("重复支付订单")      }    } else {      //抛出错误      tx.Rollback()      return errors.New("个人账户金额小于订单金额")    }  }  func payOrderTest() {    for i := 0; i < 50; i++ {      go payOrder()    }    time.Sleep(2 * time.Second)    result()  }  func result() {    user := models.User{}    count := 0    models.DB().First(&user)    models.DB().Where("status=1").Model(&models.Order{}).Count(&count)    fmt.Println("账户剩余金额:")    fmt.Println(user.Balance)    fmt.Println("支付成功的订单数:")    fmt.Println(count)  }  func payOrderTransactionAutoTest() {    for i := 0; i < 50; i++ {      go payOrderTransactionAuto()    }    time.Sleep(2 * time.Second)    result()  }  func payOrderTransactionTest() {    for i := 0; i < 50; i++ {      go payOrderTransaction()    }    time.Sleep(2 * time.Second)    result()  }  func payOrderTransactionUnlockTest() {    for i := 0; i < 50; i++ {      go payOrderTransactionUnlock()    }    time.Sleep(2 * time.Second)    result()  }  func reset() {    models.DB().Model(&models.Order{}).Update("status", 0)    models.DB().Model(&models.Order{}).Update("price", 100)    models.DB().Model(&models.User{}).Update("balance", 1000)  }  func main() {    reset()    //休眠两秒等数据库重置    time.Sleep(1 * time.Second)      //我们假设了账户金额为1000元,每笔订单金额100元      //没有开启事务    //payOrderTest()      //开启事务 lock表    //payOrderTransactionAutoTest()      //开启事务 lock表    //payOrderTransactionTest()      //开启事务 没有lock表    payOrderTransactionUnlockTest()  }  

我们定义了几个函数去分别进行测试

payOrderTest() 没有开启事务 失败

账户剩余金额:  800  支付成功的订单数:  16    复制代码

payOrderTransactionAutoTest() 自动开启事务 lock表 成功

账户剩余金额:  0  支付成功的订单数:  10  复制代码

payOrderTransactionTest() 手动开启事务 lock表 成功

账户剩余金额:  0  支付成功的订单数:  10  复制代码

payOrderTransactionUnlockTest() 手动开启事务没有lock表 失败

账户剩余金额:  0  支付成功的订单数:  10  综上所述,mysql在开启事务的情况下也不能防止刷单,还要加上for update  

在gorm中,我们可以这样为SQL加上for update

Set("gorm:query_option", "FOR UPDATE")

记住要想通过事务防止刷单,需要以下两点

  • 开启事务
  • 加上for update
  • 正确的业务逻辑

链接:https://pan.baidu.com/s/17oIaB1xqMW441oZyOtZ-sQ

提取码:7zxa