用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