目录
文章目录
一、库存服务
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做参考
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")
}