前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >用gorm谈谈mysql中的事务操作

用gorm谈谈mysql中的事务操作

作者头像
大话swift
发布2020-03-27 16:18:56
6.9K0
发布2020-03-27 16:18:56
举报
文章被收录于专栏:大话swift大话swift
后端的小伙伴经常面对并发的情况,特别是电商网站,经常会被刷单,那么我们改怎么防止被刷单呢?这个时候有的小伙伴会跳出来说用事务。是的,因为事务具有一下特性:
  • 原子性(Atomicity)
  • 一致性(Consistency)
  • 隔离性(Isolation)
  • 持久性(Durability)

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

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

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

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

订单表

代码语言:javascript
复制
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表示已经支付
}

用户表

代码语言:javascript
复制
package models

import "github.com/jinzhu/gorm"

type User struct {
  gorm.Model
  Balance float64 `gorm:"type:decimal(20,2)"`
}
代码语言:javascript
复制
主要的业务逻辑
代码语言:javascript
复制
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() 没有开启事务 失败

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

复制代码

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

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

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

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

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

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

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

代码语言:javascript
复制
Set("gorm:query_option", "FOR UPDATE")

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

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

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

提取码:7zxa

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 大话swift 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档