Microservices have everything from code to k8s deployment series (IX. transaction elaboration)

Posted by Balu on Thu, 17 Feb 2022 03:50:40 +0100

We will use a series to explain the complete practice of microservices from requirements to online, from code to k8s deployment, from logging to monitoring.

The whole project uses the micro services developed by go zero, which basically includes go zero and some middleware developed by relevant go zero authors. The technology stack used is basically the self-developed component of the go zero project team, which is basically the go zero software.

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. However, the best practice of using distributed transactions in combination with dtm is go zero. I have a finishing demo. Here I will introduce the use of go zero combined with dtm. The project address is the best practice warehouse address of go zero combined with dtm: https://github.com/Mikaelemmmm/gozerodtm

[note] the following is not the go zero look project, but this project https://github.com/Mikaelemmmm/gozerodtm

1, Precautions

  • Go zero version 1.2.4 or above, this must be noted

  • dtm, just use the latest one

2, clone dtm

git clone https://github.com/yedf/dtm.git

3, Configuration file

1. Find conf.sample.under the project and folder yml

2,cp conf.sample.yml conf.yml

3. Use etcd to open the following comment in the configuration (if etcd is not used, it will be easier. This is saved. Just link directly to the dtm server address)

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 means that dtm should be registered in the MicroService cluster, so that the internal services of the MicroService cluster can interact with dtm directly through grpc

Driver: 'DTM driver gozero', use the registration service of go zero to discover the driver, and support go zero

Target: 'etcd://localhost:2379/dtmservice 'register the current dtm server directly in the etcd cluster where the micro service is located. If go zero is used as a micro service, you can directly get the dtm server grpc link through etcd and interact with the dtm server directly

EndPoint: 'localhost:36790', which represents the connection address + port of dtm server. Micro services in the cluster can obtain this address directly through etcd and interact with dtm,

If you change the grpc port of dtm source code yourself, remember to change the port here

4, Start dtm server

Under the root directory of dtm project

go run app/main.go dev

5, Connect dtm with grpc of go zero

This is an example of quick single deduction of commodity inventory

1,order-api

Order API is an http service entry to create orders

service order {
   @doc "Create order"
   @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("Order abnormal timeout")
	}
	stockRpcBusiServer, err := l.svcCtx.Config.StockRpcConf.BuildTarget()
	if err != nil{
		return nil,fmt.Errorf("Order abnormal timeout")
	}

	createOrderReq:= &order.CreateReq{UserId: req.UserId,GoodsId: req.GoodsId,Num: req.Num}
	deductReq:= &stock.DecuctReq{GoodsId: req.GoodsId,Num: req.Num}

	// Here are only saga examples. tcc and other examples are basically the same. For details, please refer to the official dtm website

	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 order logic, obtain the addresses of the rpc of the order and stock inventory services in etcd respectively, and use the method of BuildTarget()

Then create the 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 to create an order and reduce inventory into the transaction. Here, the request is in the form of grpc. Each business should have a forward request, a rollback request and request parameters, When an error occurs in executing any one of the business forward requests, it will automatically call all business rollback requests in the transaction to achieve the rollback effect.

2,order-srv

Order SRV is the rpc service of the order, which interacts 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 the 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("Create order in : %+v \n", in)

   // The barrier can prevent empty compensation and empty suspension. Please refer to the dtm official website for details. Don't forget to add the barrier table in the current library, because the judgment compensation is the local transaction together with the sql to be executed
   barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
   db, err := sqlx.NewMysql(l.svcCtx.Config.DB.DataSource).RawDB()
   if err != nil {
      // !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
      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("Failed to create order err : %v , order:%+v \n", err, order)
      }

      return nil
   }); err != nil {
      // !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
      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 dtm's sub transaction barrier technology. As for why we use the sub transaction barrier, there may be dirty data caused by repeated requests or empty requests. Here, dtm automatically performs idempotent processing for us, which does not need to be done by ourselves, At the same time, ensure that its internal idempotent processing and the transactions executed by ourselves are in the same transaction, so we need to use the db link of a session. At this time, we need to obtain it first

db, err := sqlx.NewMysql(l.svcCtx.Config.DB.DataSource).RawDB()

Then, based on this db connection, dtm performs idempotent processing internally through sql execution. At the same time, we start transactions based on this db connection, so as to ensure that the sub transaction barrier in dtm performs sql operations in the same transaction as the sql operations performed by our own business.

When dtm uses grpc to call our business, when our grpc service returns an error to dtm server, dtm will judge whether to roll back or retry all the time according to the grpc error code we return to it:

  • codes.Internal: the dtm server will not call rollback, but will try again all the time. Each time it tries, the dtm database will add one retry number. You can monitor the retry number, alarm and handle it manually
  • codes. Aborted: DTM server will call all rollback requests and execute rollback operation

If dtm returns nil when calling grpc, the call is considered successful

2.2 CreateRollback

When we call order creation or inventory deduction, the codes returned to dtm server When aborted, 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("Order rollback  , in: %+v \n", in)

	order, err := l.svcCtx.OrderModel.FindLastOneByUserIdGoodsId(in.UserId, in.GoodsId)
	if err != nil && err != model.ErrNotFound {
		// !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
		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 {
			// !!! Generally, there is no error in the database. You don't need dtm rollback to keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
			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("Rollback order failed  err : %v , userId:%d , goodsId:%d", err, in.UserId, in.GoodsId)
			}

			return nil
		}); err != nil {
			logx.Errorf("err : %v \n", err)

			// !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
			return nil, status.Error(codes.Internal, err.Error())
		}

	}
	return &pb.CreateResp{}, nil
}

In fact, if the previous order is successful, canceling the previous order is the rollback operation of the corresponding order

3,stock-srv

3.1 Deduct

Inventory deduction is the same as the Create of order. It is a forward operation within the order transaction. The code is as follows

func (l *DeductLogic) Deduct(in *pb.DecuctReq) (*pb.DeductResp, error) {

	fmt.Printf("Deduct inventory start....")

	stock, err := l.svcCtx.StockModel.FindOneByGoodsId(in.GoodsId)
	if err != nil && err != model.ErrNotFound {
		// !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
		return nil, status.Error(codes.Internal, err.Error())
	}
	if stock == nil || stock.Num < in.Num {
		// [rollback] if the inventory is insufficient, dtm needs to roll back directly, and codes will be returned directly Aborted, dtmcli. Only resultfailure can be rolled back
		return nil, status.Error(codes.Aborted, dtmcli.ResultFailure)
	}

	// The barrier can prevent empty compensation and empty suspension. Please refer to the dtm official website for details. Don't forget to add the barrier table in the current library, because the judgment compensation is the local transaction together with the sql to be executed
	barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
	db, err := l.svcCtx.StockModel.SqlDB()
	if err != nil {
		// !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
		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{
			// !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
			return status.Error(codes.Internal, err.Error())
		}
		affected, err := sqlResult.RowsAffected()
		if err != nil{
			// !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
			return status.Error(codes.Internal, err.Error())
		}

		// If the number of affected rows is 0, directly tell dtm that it fails and there is no need to retry
		if affected <= 0 {
			return  status.Error(codes.Aborted,  dtmcli.ResultFailure)
		}

		// !! Open test!! The rollback status of the test order is changed to invalid, and the current stock deduction fails. There is no need to rollback
		//return fmt.Errorf("inventory deduction failed, err:% v, in:% + V \ n", err, in)

		return nil
	}); err != nil {
		// !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
		return nil,err
	}

	return &pb.DeductResp{}, nil
}

It is worth noting here that only when the inventory is insufficient or the number of lines affected by inventory deduction is 0 (unsuccessful), you need to tell the dtm server to roll back. In other cases, it is actually caused by network jitter and hardware exceptions. You should let the dtm server retry all the time. Of course, you should add a monitoring alarm for the maximum number of retries. If the maximum number of retries is not successful, you can automatically send text messages Called and intervened manually.

3.2 DeductRollback

Here is the rollback operation corresponding to inventory deduction

func (l *DeductRollbackLogic) DeductRollback(in *pb.DecuctReq) (*pb.DeductResp, error) {
	fmt.Printf("Inventory rollback in : %+v \n", in)

	barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
	db, err := l.svcCtx.StockModel.SqlDB()
	if err != nil {
		// !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
		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("Rolling back inventory failed err : %v ,goodsId:%d , num :%d", err, in.GoodsId, in.Num)
		}
		return nil
	}); err != nil {
		logx.Errorf("err : %v \n", err)
		// !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &pb.DeductResp{}, nil
}

6, Sub transaction barrier

This term is defined by the author of dtm. In fact, there are not many sub transaction barrier codes. See barrier The callwithdb method is sufficient.

// 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 the local transaction internally, it executes sql operations in this transaction internally, so we must use the same transaction with it when we execute our own business, so we need to open the 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 its internal sql operations and our sql operations under the same transaction. As for why it executes its own sql operations internally, let's analyze it next.

Let's watch BB Call this method

// For details of Call sub transaction barrier, see https://zhuanlan.zhihu.com/p/388444465
// tx: transaction object of local database, which allows sub transaction barrier to conduct transaction operation
// busiCall: business function, which can only be called when necessary
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 || // This is null compensation
  currentAffected == 0 { // This is a repeat request or suspension
  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 || // This is null compensation		
  currentAffected == 0 { // This is a repeat request or suspension		
  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 requests normally and successfully, Ti By default, the operation normally executed by OP is action, so the first normal request is ti OP values are all actions, 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 = = "" return ed directly in insertBarrier

	currentAffected, rerr := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, ti.Op, bid, ti.Op)

The second sql is ti OP is action, so a piece of data will be inserted into the sub transaction barrier table barrier

Similarly, an item will also be inserted when executing inventory

1. Sub transaction barrier for the success of the whole transaction

In a normal and successful request to place an order, due to Ti Ops are all action s, so the originType is "", so originAffected will be ignored when executing the barrier insert twice, regardless of whether the order is placed or the inventory is deducted. Because originType = = "" will be return ed directly without inserting data, it seems that the second inserted data of the barrier will take effect regardless of whether the order is placed or the inventory is deducted, Therefore, there will be two pieces of order data in the barrier data table, one is the order and the other is the deduction of inventory

GID: DTM global transaction id

branch_id: each business id under each global transaction id

op: operation. If it is normal and successful, the request is action

barrier_id: multiple meetings are held under the same business

These four fields are joint unique indexes in the table. During insertBarrier, dtm judges that if they exist, they will be ignored and not inserted

2. If the order is successful and the inventory is insufficient, the transaction barrier is returned

We only have 10 in stock and we place an order for 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 time for query and insufficient time for deduction),

Therefore, if the order is placed successfully, a correct data execution data will be generated in the barrier table according to the logic combed before

2) Then perform inventory deduction

func (l *DeductLogic) Deduct(in *pb.DecuctReq) (*pb.DeductResp, error) {

	fmt.Printf("Deduct inventory start....")

	stock, err := l.svcCtx.StockModel.FindOneByGoodsId(in.GoodsId)
	if err != nil && err != model.ErrNotFound {
		// !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
		return nil, status.Error(codes.Internal, err.Error())
	}
	if stock == nil || stock.Num < in.Num {
		//[rollback] if the inventory is insufficient, dtm needs to roll back directly, and codes will be returned directly Aborted, dtmcli. Only resultfailure can be rolled back
		return nil, status.Error(codes.Aborted, dtmcli.ResultFailure)
	}
  
  .......
}

Before implementing the inventory deduction business logic, we directly return codes because we found that the inventory was insufficient Once aborted, you will not go to the sub transaction barrier, so data will not be inserted into the barrier table, but dtm will be told to roll back

3) Call order rollback operation

When the order is rolled back, the barrier will be opened, and the barrier code will be executed at this time (as shown below), due to the Ti of the rollback code OP is compensate and 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 || // This is null compensation		
  currentAffected == 0 { // This is a repeat request or suspension		
  return	
}
rerr = busiCall(tx)

Because we have successfully placed the order before, there is a record action when the order was successfully placed in the barrier table, so originAffected==0, so only one current rollback record will be inserted, and continue to call busiCall(tx) to perform the subsequent rollback operations written by ourselves

So far, we should only have two pieces of data, one record of successful order creation and one record of order rollback

4) Inventory rollback

After the order is rolled back successfully, 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 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 of inventory rollback

  • Failed to roll back

  • Buckle rolled back successfully

Failed to roll back successfully (this is our current example scenario)

First call inventory rollback ti OP is compensate and orginType is action. The following two insert s 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 || // This is null compensation
  currentAffected == 0 { // This is a repeat request or suspension
  return}rerr = busiCall(tx)
}

It is judged that if the operation is rollback or cancellation, originaffected > 0 is inserted successfully, and the previous corresponding forward inventory deduction operation is not inserted successfully, which indicates that the previous inventory deduction is not successful, and direct return does not need to perform subsequent compensation. Therefore, at this time, two pieces of data will be inserted into the barrier table and returned directly, so our subsequent compensation operations will not be performed

At this point, we have four pieces of data in the barrier table

The deduction is rolled back successfully (in this case, you can try to simulate this scenario)

If we succeeded in deducting the inventory in the previous step, when we implement this compensation ti OP is compensate and orginType is action. Continue to execute two 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 || // This is null compensation
  currentAffected == 0 { // This is a repeat request or suspension
  return}rerr = busiCall(tx)
}

If it is a rollback or cancellation operation, originAffected == 0. The current insertion ignores the fact that it is not inserted, which indicates that the previous forward inventory deduction insertion is successful. Here, just insert the second sql statement record, and then perform the subsequent business operations of our compensation.

Therefore, according to the overall analysis, the core statement is two insert s, which helps us solve the repeated rollback of data and data idempotence. It can only be said that the author of dtm has a really good idea and uses the least code to help us solve a very troublesome problem

7, Precautions in go zero docking

1. Rollback compensation of dtm

When using grpc with dtm, when we use saga, tcc, etc., if the first attempt or execution fails, we hope it can execute the subsequent rollback. If there is an error in the service in grpc, we must return: status Error (codes. Aborted, dtmcli. Resultfailure). Other errors are returned. Your rollback operation will not be executed. dtm will try again all the time, as follows:

stock, err := l.svcCtx.StockModel.FindOneByGoodsId(in.GoodsId)
if err != nil && err != model.ErrNotFound {
  // !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
  return nil, status.Error(codes.Internal, err.Error())
}
if stock == nil || stock.Num < in.Num {
  //[rollback] if the inventory is insufficient, dtm needs to roll back directly, and codes will be returned directly Aborted, dtmcli. Only resultfailure can be rolled back
  return nil, status.Error(codes.Aborted, dtmcli.ResultFailure)
}

2. Empty compensation and suspension of barrier

In the previous preparatory work, we created dtm_barrier library and the implementation of barrier mysql. SQL, this is actually a check for our business services to prevent null compensation. For details, see barrier Call source code, few lines of code can understand.

If we use it online, every service you interact with db only needs to use barrier. For the mysql account used by this service, you should assign him the permission of barrier library. Don't forget this

3. barrier local transaction in rpc

In rpc business, if a barrier is used, transactions must be used when interacting with 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 {
		// !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
		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{
			// !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
			return status.Error(codes.Internal, err.Error())
		}
		affected, err := sqlResult.RowsAffected()
		if err != nil{
			// !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
			return status.Error(codes.Internal, err.Error())
		}

		// If the number of affected rows is 0, directly tell dtm that it fails and there is no need to retry
		if affected <= 0 {
			return  status.Error(codes.Aborted,  dtmcli.ResultFailure)
		}

		// !! Open test!!: The rollback status of the test order is changed to invalid, and the current stock deduction fails. There is no need to rollback
		// return fmt.Errorf("inventory deduction failed, err:% v, in:% + V \ n", err, in)

		return nil
	}); err != nil {
		// !!! Generally, the database will not make errors. You don't need dtm rollback, so you can keep trying again. At this time, don't return codes Aborted, dtmcli. Resultfailure is OK. You can control it yourself!!!
		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, http docking using go zero

It's basically easy for grpc to do this. Since there are few scenarios for go to use http in microservices, I won't do it in detail here. I wrote a simple version before, but it's not perfect. If you're interested, you can have a look. However, the barrier is its own sqlx based on go zero, which has modified the official dtm. Now it's not necessary.

Project address: https://github.com/Mikaelemmmm/dtmbarrier-go-zero

Project address

https://github.com/zeromicro/go-zero

https://gitee.com/kevwan/go-zero

Welcome to go zero and star support us!

Wechat communication group

Focus on the "micro service practice" official account and click on the exchange group to get the community community's two-dimensional code.

Topics: Go grpc