50 [十gmicro重构项目] :库存服务\dtm分布式事务框架

目录


一、库存服务

1、代码

在这里插入图片描述

2、sort包使用

在这里插入图片描述
sort是一个接口,使用的话需要实现三个方法:Len(),Less(),Swap()
切片不能定义方法,可以用别名定义方法

type GoodsDetail struct {
	Goods int32
	Num   int32
}

type GoodsDetailList []GoodsDetail

func (a GoodsDetailList) Len() int           { return len(a) }
func (a GoodsDetailList) Less(i, j int) bool { return a[i].Goods < a[j].Goods }
func (a GoodsDetailList) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }



在这里插入图片描述

3、库存扣减

func (is *inventoryService) Sell(ctx context.Context, ordersn string, details []do.GoodsDetail) error {
	log.Infof("订单%s扣减库存", ordersn)
	//解决了空悬挂的问题
	//先查询刚才插入的记录是否存在,如果存在则说明已经cancel就不能执行了

	rs := redsync.New(is.pool)
	//实际上批量扣减库存的时候, 我们经常会先按照商品的id排序,然后从小大小逐个扣减库存,这样可以减少锁的竞争
	//如果无序的话 那么就有可能订单a 扣减 1,3,4 订单B 扣减 3,2,1
	var detail = do.GoodsDetailList(details)
	sort.Sort(detail)

	txn := is.data.Begin()
	defer func() {
		if err := recover(); err != nil {
			_ = txn.Rollback()
			log.Error("事务进行中出现异常,回滚")
			return
		}
	}()

	sellDetail := do.StockSellDetailDO{
		OrderSn: ordersn,
		Status:  1,
		Detail:  detail,
	}

	for _, goodsInfo := range detail {
		mutex := rs.NewMutex(inventoryLockPrefix + ordersn)
		if err := mutex.Lock(); err != nil {
			log.Errorf("订单%s获取锁失败", ordersn)
		}

		inv, err := is.data.Inventorys().Get(ctx, uint64(goodsInfo.Goods))
		if err != nil {
			log.Errorf("订单%s获取库存失败", ordersn)
			return err
		}

		//判断库存是否充足
		if inv.Stocks < goodsInfo.Num {
			txn.Rollback() //回滚
			log.Errorf("商品%d库存%d不足, 现有库存: %d", goodsInfo.Goods, goodsInfo.Num, inv.Stocks)
			return errors.WithCode(code.ErrInvNotEnough, "库存不足")
		}
		inv.Stocks -= goodsInfo.Num

		err = is.data.Inventorys().Reduce(ctx, txn, uint64(goodsInfo.Goods), int(goodsInfo.Num))
		if err != nil {
			txn.Rollback() //回滚
			log.Errorf("订单%s扣减库存失败", ordersn)
			return err
		}

		//释放锁
		if _, err := mutex.Unlock(); err != nil {
			txn.Rollback() //回滚
			log.Errorf("订单%s释放锁出现异常", ordersn)
		}
	}

	err := is.data.Inventorys().CreateStockSellDetail(ctx, txn, &sellDetail)
	if err != nil {
		txn.Rollback() //回滚
		log.Errorf("订单%s创建扣减库存记录失败", ordersn)
		return err
	}

	txn.Commit()
	return nil
}

4、库存归还


func (is *inventoryService) Reback(ctx context.Context, ordersn string, details []do.GoodsDetail) error {
	log.Infof("订单%s归还库存", ordersn)

	rs := redsync.New(is.pool)

	txn := is.data.Begin()
	defer func() {
		if err := recover(); err != nil {
			_ = txn.Rollback()
			log.Error("事务进行中出现异常,回滚")
			return
		}
	}()

	//库存归还的时候有不少细节
	//1. 主动取消 2. 网络问题引起的重试 3. 超时取消 4. 退款取消
	mutex := rs.NewMutex(orderLockPrefix + ordersn)
	if err := mutex.Lock(); err != nil {
		txn.Rollback() //回滚
		log.Errorf("订单%s获取锁失败", ordersn)
		return err
	}
	sellDetail, err := is.data.Inventorys().GetSellDetail(ctx, txn, ordersn)
	if err != nil {
		txn.Rollback()
		_, err := mutex.Unlock()
		if err != nil {
			log.Errorf("订单%s释放锁出现异常", ordersn)
			return err
		}
		if errors.IsCode(err, code.ErrInvSellDetailNotFound) {
			//空回滚
			log.Errorf("订单%s扣减库存记录不存在, 忽略", ordersn)
			//我应该记录一条数据去记录,说 ordersn已经被cancel了
			return nil
		}
		log.Errorf("订单%s获取扣减库存记录失败", ordersn)
		return err
	}

	if sellDetail.Status == 2 {
		log.Infof("订单%s扣减库存记录已经归还, 忽略", ordersn)
		return nil
	}

	var detail = do.GoodsDetailList(details)
	sort.Sort(detail)

	for _, goodsInfo := range detail {
		inv, err := is.data.Inventorys().Get(ctx, uint64(goodsInfo.Goods))
		if err != nil {
			txn.Rollback() //回滚
			log.Errorf("订单%s获取库存失败", ordersn)
			return err
		}
		inv.Stocks += goodsInfo.Num

		err = is.data.Inventorys().Increase(ctx, txn, uint64(goodsInfo.Goods), int(goodsInfo.Num))
		if err != nil {
			txn.Rollback() //回滚
			log.Errorf("订单%s归还库存失败", ordersn)
			return err
		}
	}

	err = is.data.Inventorys().UpdateStockSellDetailStatus(ctx, txn, ordersn, 2)
	if err != nil {
		txn.Rollback() //回滚
		log.Errorf("订单%s更新扣减库存记录失败", ordersn)
		return err
	}

	txn.Commit()
	return nil
}

二、dtm分布式事务框架之SAGA

1、github官网和文档

文档

官网
10分钟说透saga分布式事务
与tcc(try,commit,cancel)不同,saga取消了commit阶段.可以出现中间状态.例如saga分布式事务(saga是dtm框架一部分):
1.从前往后执行事务,执行出错,向前补偿(回滚)
2.没有configm阶段,B可以看见中间状态
在这里插入图片描述

2、各种分布式事务应用场景

在这里插入图片描述
秒杀、缓存等不用回滚的适合二阶段消息。订单服务需要回滚适合saga分布式事务。转账服务一致性高,不能让用户看到中间余额变动适合TCC。

3、dtm的安装

运行dtm-main就行

4、HTTP事务-SAGA转账

1.转入失败归还

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/lithammer/shortuuid/v3"

	"github.com/dtm-labs/client/dtmcli"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	glog "gorm.io/gorm/logger"
)

type UserAccount struct {
	ID             int     `gorm:"column:id;primary_key"`
	UserId         int     `gorm:"user_id"`
	Balance        float64 `gorm:"balance"`
	TradingBalance float64 `gorm:"trading_balance"`
}

func (UserAccount) TableName() string {
	return "user_account"
}

var lock sync.Mutex

// 转入和转出的时候,都要加锁,否则会出现并发问题
func SagaAdjustBalance(db *gorm.DB, uid int, amount float64) error {
	lock.Lock()
	defer lock.Unlock()

	if amount < 0 {
		var userAccount = UserAccount{}
		db.First(&userAccount, "user_id = ?", uid)
		if userAccount.Balance < -amount {
			return fmt.Errorf("余额不足")
		}
	}
	t := db.Exec("update dtm.user_account set balance = balance + ? where user_id = ?", amount, uid)
	if t.Error != nil {
		return t.Error
	}
	return nil
}

var db *gorm.DB

func initDB() error {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
		"root",
		"root",
		"192.168.2.13",
		"3306",
		"dtm")
	newLogger := glog.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
		glog.Config{
			SlowThreshold:             time.Second, // 慢 SQL 阈值
			LogLevel:                  glog.Info,   // 日志级别
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(记录未找到)错误
			Colorful:                  false,       // 禁用彩色打印
		},
	)

	var err error
	db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		return err
	}
	return nil
}

// MustBarrierFromGin 1
func MustBarrierFromGin(c *gin.Context) *dtmcli.BranchBarrier {
	ti, err := dtmcli.BarrierFromQuery(c.Request.URL.Query())
	fmt.Println(err)
	return ti
}

// 服务发现, 库存服务有5个
func main() {
	err := initDB()
	if err != nil {
		panic(err)
	}

	r := gin.Default()
	r.POST("/SagaBTransIn", func(c *gin.Context) {
		fmt.Println("开始转入")
		userID := 1
		err := SagaAdjustBalance(db, userID, 100)
		if err != nil {
			fmt.Printf("转入失败:%s\r\n", err.Error())
			return
		}
		fmt.Println("转入失败")
		c.JSON(http.StatusConflict, gin.H{})
		return

	})

	r.POST("/SagaBTransInCom", func(c *gin.Context) {
		fmt.Println("转入失败, 开始补偿")
		userID := 1
		err := SagaAdjustBalance(db, userID, -100)
		if err != nil {
			fmt.Printf("转入补偿失败:%s\r\n", err.Error())
			return
		}
		fmt.Println("转入补偿成功")
	})

	r.POST("/SagaBTransOut", func(c *gin.Context) {
		fmt.Println("开始转出")
		userID := 3
		err := SagaAdjustBalance(db, userID, -100)
		if err != nil {
			if err.Error() == "余额不足" {
				c.JSON(http.StatusConflict, gin.H{})
				return
			}
			fmt.Printf("转出失败:%s\r\n", err.Error())
			c.JSON(500, gin.H{"msg": err.Error()})
			return
		}
		fmt.Println("转出成功")
	})

	r.POST("/SagaBTransOutCom", func(c *gin.Context) {
		fmt.Println("转出失败, 开始补偿")
		userID := 3
		err := SagaAdjustBalance(db, userID, 100)
		if err != nil {
			fmt.Printf("转出补偿失败:%s\r\n", err.Error())
			return
		}
		fmt.Println("转出补偿成功")
	})

	r.GET("start", func(c *gin.Context) {
		req := gin.H{}
		dmtServer := "http://127.0.0.1:36789/api/dtmsvr"
		qsBusi := "http://127.0.0.1:8089"
		saga := dtmcli.NewSaga(dmtServer, shortuuid.New()).
			// 添加一个TransOut的子事务,正向操作为url: qsBusi+"/TransOut", 逆向操作为url: qsBusi+"/TransOutCom"
			Add(qsBusi+"/SagaBTransOut", qsBusi+"/SagaBTransOutCom", req).
			// 添加一个TransIn的子事务,正向操作为url: qsBusi+"/TransOut", 逆向操作为url: qsBusi+"/TransInCom"
			Add(qsBusi+"/SagaBTransIn", qsBusi+"/SagaBTransInCom", req)
		// 提交saga事务,dtm会完成所有的子事务/回滚所有的子事务
		saga.WaitResult = true
		err := saga.Submit()
		if err != nil {
			c.JSON(500, gin.H{"message": err.Error()})
		}
		c.JSON(200, gin.H{"message": "ok"})
	})

	r.Run(":8089")
}

开始转出
2023/04/19 22:53:36 D:/xuexi/resources/resources/GoStart/dtm/main.go:40
[11.015ms] [rows:1] SELECT * FROM user_account WHERE user_id = 3 ORDER BY user_account.id LIMMIT 1

2023/04/19 22:53:36 D:/xuexi/resources/resources/GoStart/dtm/main.go:45
[5.692ms] [rows:1] update dtm.user_account set balance = balance + -100.000000 where user_id = 3
转出成功
[GIN] 2023/04/19 - 22:53:36 | 200 | 17.8382ms | 127.0.0.1 | POST “/SagaBTransOut?branch_id=01&gid=vms3zDQmnwo2Lj5MBMbtMn&op=action&trans_type=saga”
开始转入

2023/04/19 22:53:36 D:/xuexi/resources/resources/GoStart/dtm/main.go:45
[3.855ms] [rows:1] update dtm.user_account set balance = balance + 100.000000 where user_id = 1
转入失败
[GIN] 2023/04/19 - 22:53:36 | 409 | 3.855ms | 127.0.0.1 | POST “/SagaBTransIn?branch_id=02&gid=vms3zDQmnwo2Lj5MBMbtMn&op=action&trans_type=saga”
转入失败, 开始补偿

2023/04/19 22:53:36 D:/xuexi/resources/resources/GoStart/dtm/main.go:40
[1.950ms] [rows:1] SELECT * FROM user_account WHERE user_id = 1 ORDER BY user_account.id LIMIT 1

2023/04/19 22:53:36 D:/xuexi/resources/resources/GoStart/dtm/main.go:45
[3.109ms] [rows:1] update dtm.user_account set balance = balance + -100.000000 where user_id = 1
转入补偿成功
[GIN] 2023/04/19 - 22:53:36 | 200 | 5.0594ms | 127.0.0.1 | POST “/SagaBTransInCom?branch_id=02&gid=vms3zDQmnwo2Lj5MBMbtMn&op=compensate&trans_type=saga”
转出失败, 开始补偿

2023/04/19 22:53:36 D:/xuexi/resources/resources/GoStart/dtm/main.go:45
[3.262ms] [rows:1] update dtm.user_account set balance = balance + 100.000000 where user_id = 3
转出补偿成功
[GIN] 2023/04/19 - 22:53:36 | 200 | 3.2617ms | 127.0.0.1 | POST “/SagaBTransOutCom?branch_id=01&gid=vms3zDQmnwo2Lj5MBMbtMn&op=compensate&trans_type=saga”
[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 500 with 200
[GIN] 2023/04/19 - 22:53:36 | 200 | 43.1154ms | 127.0.0.1 | GET “/start”

2.转入成功情况

取消故意失败return

	r.POST("/SagaBTransIn", func(c *gin.Context) {
		fmt.Println("开始转入")
		userID := 1
		err := SagaAdjustBalance(db, userID, 100)
		if err != nil {
			fmt.Printf("转入失败:%s\r\n", err.Error())
			return
		}
		fmt.Println("转入成功")
	})

[GIN-debug] Listening and serving HTTP on :8089
开始转出

2023/04/19 22:56:43 D:/xuexi/resources/resources/GoStart/dtm/main.go:40
[12.626ms] [rows:1] SELECT * FROM user_account WHERE user_id = 3 ORDER BY user_account.id LIMIT 1

2023/04/19 22:56:43 D:/xuexi/resources/resources/GoStart/dtm/main.go:45
[2.035ms] [rows:1] update dtm.user_account set balance = balance + -100.000000 where user_id = 3
转出成功
[GIN] 2023/04/19 - 22:56:43 | 200 | 14.6617ms | 127.0.0.1 | POST “/SagaBTransOut?branch_id=01&gid=3L8mGzu4iyAi6L3Rczqk3j&op=action&trans_type=saga”
开始转入

2023/04/19 22:56:43 D:/xuexi/resources/resources/GoStart/dtm/main.go:45
[2.727ms] [rows:1] update dtm.user_account set balance = balance + 100.000000 where user_id = 1
转入成功

5、GRPC事务-SAGA转账

1.接入consul

在这里插入图片描述

consul接入类似kratos
1.复制一份conf.sample.yml 改名为conf.yaml
内容如下:

在这里插入图片描述

MicroService: # gRPC/HTTP based microservice config
 Driver: 'dtm-driver-kratos' # name of the driver to handle register/discover
 Target: 'consul://127.0.0.1:8500/dtmservice' # register dtm server to this url
 EndPoint: 'grpc://127.0.0.1:36790'

2.运行dtm的main.go启动dtm,将dtm也作为微服务注册进consul

3.rpc\main.go 通过gin添加访问grpc服务的start接口

在这里插入图片描述
具体grpc方法如下:
在这里插入图片描述

6、事务屏障达到通过gin集成转入转出功能

1.建表

在这里插入图片描述
在这里插入图片描述
先建立dtm_barrier库和表

create database if not exists dtm_barrier
/*!40100 DEFAULT CHARACTER SET utf8mb4 */
;
drop table if exists dtm_barrier.barrier;
create table if not exists dtm_barrier.barrier(
  id bigint(22) PRIMARY KEY AUTO_INCREMENT,
  trans_type varchar(45) default '',
  gid varchar(128) default '',
  branch_id varchar(128) default '',
  op varchar(45) default '',
  barrier_id varchar(45) default '',
  reason varchar(45) default '' comment 'the branch type who insert this record',
  create_time datetime DEFAULT now(),
  update_time datetime DEFAULT now(),
  key(create_time),
  key(update_time),
  UNIQUE key(gid, branch_id, op, barrier_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

2.下载dmt.example做参考

example

3.gin集成转入转出功能

package main

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"os"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/lithammer/shortuuid/v3"

	"github.com/dtm-labs/client/dtmcli"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	glog "gorm.io/gorm/logger"
)

type UserAccount struct {
	ID             int     `gorm:"column:id;primary_key"`
	UserId         int     `gorm:"user_id"`
	Balance        float64 `gorm:"balance"`
	TradingBalance float64 `gorm:"trading_balance"`
}

func (UserAccount) TableName() string {
	return "user_account"
}

var lock sync.Mutex

// 转入和转出的时候,都要加锁,否则会出现并发问题
func SagaAdjustBalance(db *sql.Tx, uid int, amount float64) error {
	lock.Lock()
	defer lock.Unlock()

	if amount < 0 {
		var balance float64
		db.QueryRow("select balance from dtm.user_account where user_id = ?", uid).Scan(&balance)
		if balance < -amount {
			return fmt.Errorf("余额不足")
		}
	}
	_, err := db.Exec("update dtm.user_account set balance = balance + ? where user_id = ?", amount, uid)
	if err != nil {
		return err
	}
	return nil
}

var db *gorm.DB

func initDB() error {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
		"root",
		"root",
		"192.168.2.13",
		"3306",
		"dtm")
	newLogger := glog.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
		glog.Config{
			SlowThreshold:             time.Second, // 慢 SQL 阈值
			LogLevel:                  glog.Info,   // 日志级别
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(记录未找到)错误
			Colorful:                  false,       // 禁用彩色打印
		},
	)

	var err error
	db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		return err
	}
	return nil
}

// 获取屏障
// MustBarrierFromGin 1
func MustBarrierFromGin(c *gin.Context) *dtmcli.BranchBarrier {
	ti, err := dtmcli.BarrierFromQuery(c.Request.URL.Query())
	fmt.Println(err)
	return ti
}

// 服务发现, 库存服务有5个
func main() {
	err := initDB()
	if err != nil {
		panic(err)
	}

	r := gin.Default()
	r.POST("/SagaBTransIn", func(c *gin.Context) {
		barrier := MustBarrierFromGin(c) //1.生成一个屏障
		tx := db.Begin()                 //2.开启事务
		sourceTx := tx.Statement.ConnPool.(*sql.Tx)
		err := barrier.Call(sourceTx, func(tx1 *sql.Tx) error { //3.将业务逻辑翻到Call方法执行
			fmt.Println("开始转入")
			userID := 1
			err := SagaAdjustBalance(sourceTx, userID, 100) //4.修改gorm为 sql.Tx并使用原生sql查询(gorm支持不全)
			if err != nil {
				fmt.Printf("转入失败:%s\r\n", err.Error())
				return err
			}
			return nil
		})
		if err != nil {
			c.JSON(http.StatusOK, gin.H{"code": 1, "msg": err.Error()})
			return
		}

		return
	})

	r.POST("/SagaBTransInCom", func(c *gin.Context) {
		fmt.Println("转入失败, 开始补偿")
		//userID := 1
		//err := SagaAdjustBalance(db, userID, -100)
		//if err != nil {
		//	fmt.Printf("转入补偿失败:%s\r\n", err.Error())
		//	return
		//}
		fmt.Println("转入补偿成功")
	})

	r.POST("/SagaBTransOut", func(c *gin.Context) {
		barrier := MustBarrierFromGin(c)
		tx := db.Begin()
		sourceTx := tx.Statement.ConnPool.(*sql.Tx)

		err := barrier.Call(sourceTx, func(tx1 *sql.Tx) error {
			fmt.Println("开始转出")
			userID := 3
			err := SagaAdjustBalance(sourceTx, userID, -100)
			if err != nil {
				if err.Error() == "余额不足" {
					c.JSON(http.StatusConflict, gin.H{})
				}
				fmt.Printf("转出失败:%s\r\n", err.Error())
				c.JSON(500, gin.H{"msg": err.Error()})
			}
			fmt.Println("转出成功")
			return nil
		})
		if err != nil {
			c.JSON(http.StatusOK, gin.H{"code": 1, "msg": err.Error()})
			return
		}
		return
	})

	r.POST("/SagaBTransOutCom", func(c *gin.Context) {
		fmt.Println("转出失败, 开始补偿")
		//userID := 3
		//err := SagaAdjustBalance(db, userID, 100)
		//if err != nil {
		//	fmt.Printf("转出补偿失败:%s\r\n", err.Error())
		//	return
		//}
		fmt.Println("转出补偿成功")
	})

	r.GET("start", func(c *gin.Context) {
		req := gin.H{}
		dmtServer := "http://127.0.0.1:36789/api/dtmsvr"
		qsBusi := "http://127.0.0.1:8089"
		saga := dtmcli.NewSaga(dmtServer, shortuuid.New()).
			// 添加一个TransOut的子事务,正向操作为url: qsBusi+"/TransOut", 逆向操作为url: qsBusi+"/TransOutCom"
			Add(qsBusi+"/SagaBTransOut", qsBusi+"/SagaBTransOutCom", req).
			// 添加一个TransIn的子事务,正向操作为url: qsBusi+"/TransOut", 逆向操作为url: qsBusi+"/TransInCom"
			Add(qsBusi+"/SagaBTransIn", qsBusi+"/SagaBTransInCom", req)
		// 提交saga事务,dtm会完成所有的子事务/回滚所有的子事务
		saga.WaitResult = true
		err := saga.Submit()
		if err != nil {
			c.JSON(500, gin.H{"message": err.Error()})
		}
		c.JSON(200, gin.H{"message": "ok"})
	})

	r.Run(":8089")
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值