但是开启了事务就可以了么?
下面我们以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")
记住要想通过事务防止刷单,需要以下两点
链接:https://pan.baidu.com/s/17oIaB1xqMW441oZyOtZ-sQ
提取码:7zxa