We use a series to explain the complete practice of microservices from requirements to online, from code to k8s deployment, from logging to monitoring, etc.
The whole project uses microservices developed by go-zero, which basically includes go-zero and some middleware developed by related go-zero authors. The technology stack used is basically the self-developed components of the go-zero project team, basically go -zero the whole family bucket.
Actual project address: https://github.com/Mikaelemmmm/go-zero-looklook
About Distributed Transactions
Because the service division of this project is relatively independent, distributed transactions are not used at present, but go-zero is the best practice of using distributed transactions in combination with dtm. I have sorted out the demo. Here I will introduce the use of go-zero in combination with dtm. Project address go-zero combined with dtm best practice warehouse address: https://github.com/Mikaelemmmm/gozerodtm
[Note] The following is not the go-zero-looklook project, but this project https://github.com/Mikaelemmmm/gozerodtm
1. Matters needing attention
- Go-zero version 1.2.4 or above, this must be noted
- dtm you can use the latest
2. clone dtm
git clone https://github.com/yedf/dtm.git
3. Configuration file
1. Find conf.sample.yml under the project and folder
2. cp conf.sample.yml conf.yml
3. Using etcd, open the following comment in the configuration (it would be simpler if etcd is not used, this is all saved, just link to the dtm server address directly)
MicroService:
Driver: 'dtm-driver-gozero' # name of the driver to handle register/discover
Target: 'etcd://localhost:2379/dtmservice' # register dtm server to this url
EndPoint: 'localhost:36790'
explain:
Don't move MicroService, this representative needs to register dtm in the microservice service cluster, so that the internal services of the microservice cluster can directly interact with dtm through grpc
Driver: 'dtm-driver-gozero', use go-zero's registered service to discover drivers, support go-zero
Target: 'etcd://localhost:2379/dtmservice' Register the current dtm server directly to the etcd cluster where the microservice is located. If go-zero is used as a microservice, you can directly get the dtm server grpc through etcd Link, you can directly interact with the dtm server
EndPoint: 'localhost:36790' , which represents the connection address + port of the dtm server. Microservices in the cluster can directly obtain this address through etcd to interact with dtm.
If you change the dtm source grpc port yourself, remember to change the port here
Fourth, start the dtm server
In the dtm project root directory
go run app/main.go dev
5. Use go-zero's grpc to connect to dtm
This is an example of quickly placing an order and deducting inventory
1. order-api
order-api is the http service entry to create an order
service order {
@doc "创建订单"
@handler create
post /order/quickCreate (QuickCreateReq) returns (QuickCreateResp)
}
Next look at logic
func (l *CreateLogic) Create(req types.QuickCreateReq,r *http.Request) (*types.QuickCreateResp, error) {
orderRpcBusiServer, err := l.svcCtx.Config.OrderRpcConf.BuildTarget()
if err != nil{
return nil,fmt.Errorf("下单异常超时")
}
stockRpcBusiServer, err := l.svcCtx.Config.StockRpcConf.BuildTarget()
if err != nil{
return nil,fmt.Errorf("下单异常超时")
}
createOrderReq:= &order.CreateReq{UserId: req.UserId,GoodsId: req.GoodsId,Num: req.Num}
deductReq:= &stock.DecuctReq{GoodsId: req.GoodsId,Num: req.Num}
// 这里只举了saga例子,tcc等其他例子基本没啥区别具体可以看dtm官网
gid := dtmgrpc.MustGenGid(dtmServer)
saga := dtmgrpc.NewSagaGrpc(dtmServer, gid).
Add(orderRpcBusiServer+"/pb.order/create", orderRpcBusiServer+"/pb.order/createRollback", createOrderReq).
Add(stockRpcBusiServer+"/pb.stock/deduct", stockRpcBusiServer+"/pb.stock/deductRollback", deductReq)
err = saga.Submit()
dtmimp.FatalIfError(err)
if err != nil{
return nil,fmt.Errorf("submit data to dtm-server err : %+v \n",err)
}
return &types.QuickCreateResp{}, nil
}
When entering the ordering logic, obtain the addresses of the order order and the rpc of the stock inventory service in etcd respectively, and use the BuildTarget() method
Then create request parameters corresponding to order and stock
Request dtm to obtain the global transaction id, start the saga distributed transaction of grpc based on this global transaction id, and put the request for creating an order and deducting inventory into the transaction. Here, the request in the form of grpc is used. The rollback request and the request parameters, when an error occurs in the execution of any of the business forward requests, all business rollback requests in the transaction will be automatically called to achieve the rollback effect.
2. order-srv
order-srv is an RPC service for orders, interacting with the order table in the dtm-gozero-order database
// service
service order {
rpc create(CreateReq)returns(CreateResp);
rpc createRollback(CreateReq)returns(CreateResp);
}
2.1 Create
When order-api submits a transaction, the default request is the create method, let's look at logic
func (l *CreateLogic) Create(in *pb.CreateReq) (*pb.CreateResp, error) {
fmt.Printf("创建订单 in : %+v \n", in)
// barrier防止空补偿、空悬挂等具体看dtm官网即可,别忘记加barrier表在当前库中,因为判断补偿与要执行的sql一起本地事务
barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
db, err := sqlx.NewMysql(l.svcCtx.Config.DB.DataSource).RawDB()
if err != nil {
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return nil, status.Error(codes.Internal, err.Error())
}
if err := barrier.CallWithDB(db, func(tx *sql.Tx) error {
order := new(model.Order)
order.GoodsId = in.GoodsId
order.Num = in.Num
order.UserId = in.UserId
_, err = l.svcCtx.OrderModel.Insert(tx, order)
if err != nil {
return fmt.Errorf("创建订单失败 err : %v , order:%+v \n", err, order)
}
return nil
}); err != nil {
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return nil, status.Error(codes.Internal, err.Error())
}
return &pb.CreateResp{}, nil
}
It can be seen that as soon as we enter the method, we use the sub-transaction barrier technology of dtm. As for why we use the sub-transaction barrier, it is because there may be dirty data caused by repeated requests or empty requests. Here, dtm automatically gives us power. Waiting for the processing does not need to be done by ourselves, and at the same time to ensure that its internal idempotent processing and the transaction we execute ourselves must be in the same transaction, so we need to use a session db link, then we must first get
db, err := sqlx.NewMysql(l.svcCtx.Config.DB.DataSource).RawDB()
Then based on this db connection dtm performs idempotent processing internally through sql, and we start transactions based on this db connection, so as to ensure that the sub-transaction barrier inside dtm performs sql operations and our own business execution sql operations in a transaction middle.
When dtm uses grpc to call our business, when our grpc service returns an error to the dtm server, dtm will judge whether to perform a rollback operation or keep retrying according to the grpc error code we return to it:
- codes.Internal : The dtm server will not call rollback and will keep retrying. Each time the dtm database is retried, the number of retries will be added.
- codes.Aborted : dtm server will call all rollback requests and perform rollback operations
If dtm calls grpc and returns an error of nil, the call is considered successful
2.2 CreateRollback
When we call the codes.Aborted returned to the dtm server when the order is created or the inventory is deducted, the dtm server will call all rollback operations. CreateRollback is the rollback operation of the corresponding order. The code is as follows
func (l *CreateRollbackLogic) CreateRollback(in *pb.CreateReq) (*pb.CreateResp, error) {
fmt.Printf("订单回滚 , in: %+v \n", in)
order, err := l.svcCtx.OrderModel.FindLastOneByUserIdGoodsId(in.UserId, in.GoodsId)
if err != nil && err != model.ErrNotFound {
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return nil, status.Error(codes.Internal, err.Error())
}
if order != nil {
barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
db, err := l.svcCtx.OrderModel.SqlDB()
if err != nil {
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return nil, status.Error(codes.Internal, err.Error())
}
if err := barrier.CallWithDB(db, func(tx *sql.Tx) error {
order.RowState = -1
if err := l.svcCtx.OrderModel.Update(tx, order); err != nil {
return fmt.Errorf("回滚订单失败 err : %v , userId:%d , goodsId:%d", err, in.UserId, in.GoodsId)
}
return nil
}); err != nil {
logx.Errorf("err : %v \n", err)
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return nil, status.Error(codes.Internal, err.Error())
}
}
return &pb.CreateResp{}, nil
}
In fact, if the previous order was successfully placed, canceling the previously placed order is the rollback operation of the corresponding order.
3. stock-srv
3.1 Deduct
Deducting inventory, which is the same as Create in order, is a positive operation in the order transaction, deducting inventory, the code is as follows
func (l *DeductLogic) Deduct(in *pb.DecuctReq) (*pb.DeductResp, error) {
fmt.Printf("扣库存start....")
stock, err := l.svcCtx.StockModel.FindOneByGoodsId(in.GoodsId)
if err != nil && err != model.ErrNotFound {
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return nil, status.Error(codes.Internal, err.Error())
}
if stock == nil || stock.Num < in.Num {
// 【回滚】库存不足确定需要dtm直接回滚,直接返回 codes.Aborted, dtmcli.ResultFailure 才可以回滚
return nil, status.Error(codes.Aborted, dtmcli.ResultFailure)
}
// barrier防止空补偿、空悬挂等具体看dtm官网即可,别忘记加barrier表在当前库中,因为判断补偿与要执行的sql一起本地事务
barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
db, err := l.svcCtx.StockModel.SqlDB()
if err != nil {
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return nil, status.Error(codes.Internal, err.Error())
}
if err := barrier.CallWithDB(db, func(tx *sql.Tx) error {
sqlResult,err := l.svcCtx.StockModel.DecuctStock(tx, in.GoodsId, in.Num)
if err != nil{
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return status.Error(codes.Internal, err.Error())
}
affected, err := sqlResult.RowsAffected()
if err != nil{
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return status.Error(codes.Internal, err.Error())
}
// 如果是影响行数为0,直接就告诉dtm失败不需要重试了
if affected <= 0 {
return status.Error(codes.Aborted, dtmcli.ResultFailure)
}
// !!开启测试!! 测试订单回滚更改状态为失效,并且当前库扣失败不需要回滚
//return fmt.Errorf("扣库存失败 err : %v , in:%+v \n",err,in)
return nil
}); err != nil {
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return nil,err
}
return &pb.DeductResp{}, nil
}
It is worth noting here that only when the inventory is insufficient, or the number of rows affected by the deduction of inventory is 0 (unsuccessful), it is necessary to tell the dtm server to roll back. In other cases, it is actually caused by network jitter and hardware abnormalities. The dtm server should be kept Retry, of course, you need to add a monitoring alarm for the maximum number of retries. If the maximum number of retries is not successful, the function will automatically send text messages and make calls to manually intervene.
3.2 DeductRollback
Here is the rollback operation corresponding to the deduction of inventory
func (l *DeductRollbackLogic) DeductRollback(in *pb.DecuctReq) (*pb.DeductResp, error) {
fmt.Printf("库存回滚 in : %+v \n", in)
barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
db, err := l.svcCtx.StockModel.SqlDB()
if err != nil {
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return nil, status.Error(codes.Internal, err.Error())
}
if err := barrier.CallWithDB(db, func(tx *sql.Tx) error {
if err := l.svcCtx.StockModel.AddStock(tx, in.GoodsId, in.Num); err != nil {
return fmt.Errorf("回滚库存失败 err : %v ,goodsId:%d , num :%d", err, in.GoodsId, in.Num)
}
return nil
}); err != nil {
logx.Errorf("err : %v \n", err)
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return nil, status.Error(codes.Internal, err.Error())
}
return &pb.DeductResp{}, nil
}
6. Sub-transaction barrier
This word is defined by the author of dtm. In fact, there is not much sub-transaction barrier code. Just look at the method barrier.CallWithDB.
// CallWithDB the same as Call, but with *sql.DB
func (bb *BranchBarrier) CallWithDB(db *sql.DB, busiCall BarrierBusiFunc) error {
tx, err := db.Begin()
if err != nil {
return err
}
return bb.Call(tx, busiCall)
}
Because this method starts a local transaction internally, it performs the sql operation in this transaction internally, so when we execute our own business, we must use the same transaction with it, then we must open a transaction based on the same db connection, so ~ You know why we need to obtain the db connection in advance, the purpose is to make the sql operation performed internally and our sql operation under the same transaction. As for why it performs its own sql operations internally, let's analyze it next.
Let's look at the bb.Call method
// Call 子事务屏障,详细介绍见 https://zhuanlan.zhihu.com/p/388444465
// tx: 本地数据库的事务对象,允许子事务屏障进行事务操作
// busiCall: 业务函数,仅在必要时被调用
func (bb *BranchBarrier) Call(tx *sql.Tx, busiCall BarrierBusiFunc) (rerr error) {
bb.BarrierID = bb.BarrierID + 1
bid := fmt.Sprintf("%02d", bb.BarrierID)
defer func() {
// Logf("barrier call error is %v", rerr)
if x := recover(); x != nil {
tx.Rollback()
panic(x)
} else if rerr != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
ti := bb
originType := map[string]string{
BranchCancel: BranchTry,
BranchCompensate: BranchAction,
}[ti.Op]
originAffected, _ := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, originType, bid, ti.Op)
currentAffected, rerr := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, ti.Op, bid, ti.Op)
dtmimp.Logf("originAffected: %d currentAffected: %d", originAffected, currentAffected)
if (ti.Op == BranchCancel || ti.Op == BranchCompensate) && originAffected > 0 || // 这个是空补偿
currentAffected == 0 { // 这个是重复请求或者悬挂
return
}
rerr = busiCall(tx)
return
}
The core is actually the following lines of code
originAffected, _ := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, originType, bid, ti.Op)
currentAffected, rerr := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, ti.Op, bid, ti.Op)
dtmimp.Logf("originAffected: %d currentAffected: %d", originAffected, currentAffected)
if (ti.Op == BranchCancel || ti.Op == BranchCompensate) && originAffected > 0 || // 这个是空补偿
currentAffected == 0 { // 这个是重复请求或者悬挂
return
}
rerr = busiCall(tx)
func insertBarrier(tx DB, transType string, gid string, branchID string, op string, barrierID string, reason string) (int64, error) {
if op == "" {
return 0, nil
}
sql := dtmimp.GetDBSpecial().GetInsertIgnoreTemplate("dtm_barrier.barrier(trans_type, gid, branch_id, op, barrier_id, reason) values(?,?,?,?,?,?)", "uniq_barrier")
return dtmimp.DBExec(tx, sql, transType, gid, branchID, op, barrierID, reason)
}
For each business logic, when the dtm server makes a normal and successful request, the default normal operation of ti.Op is action, so the normal first request is that the value of ti.Op is action, and the originType is ""
originAffected, _ := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, originType, bid, ti.Op)
Then the above sql will not be executed because ti.Op == "" returns directly in insertBarrier
currentAffected, rerr := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, ti.Op, bid, ti.Op)
The ti.Op of the second sql is action, so the sub-transaction barrier table barrier will insert a piece of data
Similarly, a line will be inserted in the execution of the inventory
1. Subtransaction barriers where the entire transaction succeeds
Under a normal and successful request to place an order, since ti.Op is all action, the originType is "" , so whether it is the barrier of the order or the barrier of the deduction of inventory, when executing their two barrier inserts, originAffected will be ignored. , because originType=="" will be returned directly without inserting data, so it seems that whether it is placing an order or deducting inventory, the second insertion data of the barrier takes effect, so there will be 2 order data in the barrier data table, one One of the orders is deducted from stock
gid : dtm global transaction id
branch_id : each business id under each global transaction id
op : operation, if it is a normal successful request, it is action
barrier_id : multiple openings under the same service will increase
These four fields are joint unique indexes in the table. When insertBarrier, dtm judges that if it exists, it will be ignored and not inserted.
2. If the order is successful and the inventory is insufficient, the sub-transaction barrier is rolled back
We only have 10 in stock, we order 20
1) When the order is placed successfully, because the subsequent inventory situation is not known when the order is placed (even if the inventory is checked first when the order is placed, there will be enough query time and insufficient deduction time),
Therefore, if the order is successfully placed in the barrier table, a piece of correct data will be generated in the barrier table according to the logic previously sorted out.
2) Then perform the deduction operation
func (l *DeductLogic) Deduct(in *pb.DecuctReq) (*pb.DeductResp, error) {
fmt.Printf("扣库存start....")
stock, err := l.svcCtx.StockModel.FindOneByGoodsId(in.GoodsId)
if err != nil && err != model.ErrNotFound {
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return nil, status.Error(codes.Internal, err.Error())
}
if stock == nil || stock.Num < in.Num {
//【回滚】库存不足确定需要dtm直接回滚,直接返回 codes.Aborted, dtmcli.ResultFailure 才可以回滚
return nil, status.Error(codes.Aborted, dtmcli.ResultFailure)
}
.......
}
Before executing the inventory deduction business logic, since we query the inventory and find that the inventory is insufficient, we directly return codes.Aborted and will not go to the sub-transaction barrier barrier, so the barrier table will not insert data, but tell dtm to roll back
3) Call the order rollback operation
When the order is rolled back, the barrier will be turned on, and the barrier code (as follows) will be executed at this time. Since the ti.Op of the rollback code is compensate, the orginType is action
originAffected, _ := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, originType, bid, ti.Op)
currentAffected, err := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, ti.Op, bid, ti.Op)
dtmimp.Logf("originAffected: %d currentAffected: %d", originAffected, currentAffected)
if (ti.Op == BranchCancel || ti.Op == BranchCompensate) && originAffected > 0 || // 这个是空补偿
currentAffected == 0 { // 这个是重复请求或者悬挂
return
}
rerr = busiCall(tx)
Since we successfully placed the order before, there is a record action in the barrier table when the order is successfully placed, so originAffected==0, so only one current rollback record will be inserted and continue to call busiCall(tx) to execute the subsequent rollback written by ourselves operate
At this point, we should only have two pieces of data, one record for order creation and one record for order rollback
4) Inventory rollback DeductRollback
After the order rollback is successful, it will continue to call the inventory rollback DeductRollback. The inventory rollback code is as follows
This is what the sub-transaction barrier automatically helps us to judge, that is, the two core insert statements help us judge, so that there will be no dirty data in our business.
There are two cases for inventory rollback here
- Failed to roll back successfully
- Deduction successfully rolled back
Successful rollback without deduction (our current example scenario is this)
First, when the inventory rollback is called, ti.Op is compensate, orginType is action, and the following two inserts will be executed.
originAffected, _ := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, originType, bid, ti.Op)
currentAffected, err := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, ti.Op, bid, ti.Op)
dtmimp.Logf("originAffected: %d currentAffected: %d", originAffected, currentAffected)
if (ti.Op == BranchCancel || ti.Op == BranchCompensate) && originAffected > 0 || // 这个是空补偿
currentAffected == 0 { // 这个是重复请求或者悬挂
return}rerr = busiCall(tx)
}
Combining the judgment here, if it is a rollback or cancel operation, originAffected > 0 The current insertion is successful, and the previous corresponding forward deduction operation was not successfully inserted, indicating that the previous inventory was not deducted successfully, and the direct return does not need to perform subsequent compensation. Therefore, at this time, 2 pieces of data will be inserted into the barrier table and returned directly, and our subsequent compensation operations will not be performed.
At this point, we have 4 pieces of data in our barrier table
The deduction is successfully rolled back (in this case, you can try to simulate this scenario by yourself)
If we successfully deducted the inventory in the previous step, when executing this compensation, ti.Op is compensate, orginType is action, and continue to execute 2 insert statements
originAffected, _ := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, originType, bid, ti.Op)
currentAffected, err := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, ti.Op, bid, ti.Op)
dtmimp.Logf("originAffected: %d currentAffected: %d", originAffected, currentAffected)
if (ti.Op == BranchCancel || ti.Op == BranchCompensate) && originAffected > 0 || // 这个是空补偿
currentAffected == 0 { // 这个是重复请求或者悬挂
return}rerr = busiCall(tx)
}
If it is a rollback or cancellation operation, originAffected == 0 The current insertion is ignored and not inserted, indicating that the previous forward deduction was successfully inserted, and only the second sql statement record can be inserted here, and then we will execute the follow-up. Compensation for business operations.
Therefore, after the overall analysis, the core statement is 2 inserts, which help us solve the situation of repeated rollback of data and data idempotence. It can only be said that the author of dtm has a really good idea, and used the least code to help us solve a very troublesome problem. The problem
7. Matters needing attention in go-zero docking
1. DTM rollback compensation
When using dtm's grpc, when we use saga, tcc, etc., if the first attempt or execution fails, we hope that it can execute the subsequent rollback. If an error occurs in the service in grpc, it must return: status.Error (codes.Aborted, dtmcli.ResultFailure) , other errors are returned, your rollback operation will not be executed, dtm will always retry, as follows:
stock, err := l.svcCtx.StockModel.FindOneByGoodsId(in.GoodsId)
if err != nil && err != model.ErrNotFound {
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return nil, status.Error(codes.Internal, err.Error())
}
if stock == nil || stock.Num < in.Num {
//【回滚】库存不足确定需要dtm直接回滚,直接返回 codes.Aborted, dtmcli.ResultFailure 才可以回滚
return nil, status.Error(codes.Aborted, dtmcli.ResultFailure)
}
2. Barrier's air compensation, suspension, etc.
In the previous preparations, we created the dtm_barrier library and executed barrier.mysql.sql. This is actually a check for our business services to prevent null compensation. For details, see the source code in barrier.Call. There are only a few lines of code. understand.
If we use it online, each of your services that interact with db only needs to use the barrier. The MySQL account used by this service must be assigned the permission of the barrier library. Don't forget this.
3, barrier local transaction in rpc
In the rpc business, if a barrier is used, then a transaction must be used when interacting with the db in the model, and the same transaction must be used with the barrier
logic
barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
db, err := sqlx.NewMysql(l.svcCtx.Config.DB.DataSource).RawDB()
if err != nil {
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return nil, status.Error(codes.Internal, err.Error())
}
if err := barrier.CallWithDB(db, func(tx *sql.Tx) error {
sqlResult,err := l.svcCtx.StockModel.DecuctStock(tx, in.GoodsId, in.Num)
if err != nil{
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return status.Error(codes.Internal, err.Error())
}
affected, err := sqlResult.RowsAffected()
if err != nil{
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return status.Error(codes.Internal, err.Error())
}
// 如果是影响行数为0,直接就告诉dtm失败不需要重试了
if affected <= 0 {
return status.Error(codes.Aborted, dtmcli.ResultFailure)
}
// !!开启测试!! : 测试订单回滚更改状态为失效,并且当前库扣失败不需要回滚
// return fmt.Errorf("扣库存失败 err : %v , in:%+v \n",err,in)
return nil
}); err != nil {
// !!!一般数据库不会错误不需要dtm回滚,就让他一直重试,这时候就不要返回codes.Aborted, dtmcli.ResultFailure 就可以了,具体自己把控!!!
return nil, err
}
model
func (m *defaultStockModel) DecuctStock(tx *sql.Tx,goodsId , num int64) (sql.Result,error) {
query := fmt.Sprintf("update %s set `num` = `num` - ? where `goods_id` = ? and num >= ?", m.table)
return tx.Exec(query,num, goodsId,num)
}
func (m *defaultStockModel) AddStock(tx *sql.Tx,goodsId , num int64) error {
query := fmt.Sprintf("update %s set `num` = `num` + ? where `goods_id` = ?", m.table)
_, err :=tx.Exec(query, num, goodsId)
return err
}
7. Use go-zero's http docking
This is basically not difficult. It is very simple for grpc to know this. Since there are not many HTTP scenarios used by go in microservices, I will not do it in detail here. I wrote a simple one in a previous version, but it is not perfect. If you are interested, you can take a look, but the barrier is based on go-zero's sqlx, and the official dtm is modified, which is no longer needed.
Project address: https://github.com/Mikaelemmmm/dtmbarrier-go-zero
project address
https://github.com/zeromicro/go-zero
Welcome go-zero
and star support us!
WeChat exchange group
Follow the official account of " Microservice Practice " and click on the exchange group to get the QR code of the community group.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。