专栏首页大话swift用gorm谈谈mysql中的事务操作

用gorm谈谈mysql中的事务操作

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

本文分享自微信公众号 - 大话swift(gh_ca2266b7cab0),作者:寒云

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-03-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • vapor Web Authentication

    最近写东西一直没有加Authorware,也一直知道这个功能很重要,但是一直没有下定决心去看这一块,其实是有原因的:1 一开始粗略的看了一下,似是而非,于是感觉...

    大话swift
  • gorm之Has Many与Has One

    我们在日常工作当中经常遇到一对一和一对多的关系,那么在gorm中我们是怎么使用的呢?听我细细道来。 首先我们定义email、user和mobile三张表,他们对...

    大话swift
  • SwiftUI之常见UI

    Picker(selection: $selection, label: Text("Picker")) {

    大话swift
  • Windows Phone 7实战 第一天 设计启动页面和应用程序图标

    每一个 Windows Phone 7 应用程序在启动时多少会花上一些时间,在这个等待的时刻经常都会摆放一些启动画面 (Splash screen) 先来充充场...

    张善友
  • 写在2015 项目回看 -- 敏捷在思想不在形式

    面试的时候了解到的情况: 软件组主管刚刚离职,需要一个人接手 公司有一个成熟的框架,国外开发的,很多功能可以复用 公司的程序员都在公司干了2~3年左右 团队项目...

    麦克-堂
  • 深入了解Mysql索引数据结构

    提到数据库索引大家肯定不陌生,那到底什么是索引呢,索引是怎么工作的呢,今天就一起来聊聊这个话题 索引的出现就是为了解决数据库查询的效率问题,就像平时我们看书一样...

    JAVA葵花宝典
  • Netflix选择AVIF作为下一代图片压缩技术

    原文https://netflixtechblog.com/avif-for-next-generation-image-coding-b1d75675fe4

    LiveVideoStack
  • 大神 | EfficientNet模型的完整细节

    本文介绍了一种高效的网络模型EfficientNet,并分析了 EfficientNet B0 至B7的网络结构之间的差异。

    计算机视觉联盟
  • Confluence 6 附件存储提取文本文件

    当基于文本的文件上传到 Confluence(例如,Word,PowerPoint 等),这些文件中的文本是可以提取并且添加到索引中的,用户可以通过索引来搜索这...

    HoneyMoose
  • 【解决方案】requests.exceptions.SSLError: HTTPSConnectionPool

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

    云雀叫了一整天

扫码关注云+社区

领取腾讯云代金券