用gorm谈谈mysql中的事务操作

7,521 阅读4分钟

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

  • 原子性(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
  • 正确的业务逻辑

链接:pan.baidu.com/s/17oIaB1xq… 提取码:7zxa 复制这段内容后打开百度网盘手机App,操作更方便哦