We have a series that covers the full practice of microservices from requirements to live, from code to K8S deployment, from logging to monitoring, and more.
The whole project uses the micro-services developed by GO-Zero, and basically includes some middleware developed by Go-Zero and related GO-Zero authors. The technology stack used is basically self-developed components of go-Zero project team, which is basically the whole Family of Go-Zero.
Actual combat project address: github.com/Mikaelemmmm…
About Distributed Transactions
Because the service division of this project is relatively independent, distributed transactions are not used at present. However, I have prepared a demo of the best practices of distributed transactions by GO-Zero combined with DTM. Here I will introduce the use of Go-Zero combined with DTM. Github.com/Mikaelemmmm…
[Note] This is not the Go-Zero-Looklook project, it is the project github.com/Mikaelemmmm…
First, matters needing attention
-
Go Zero 1.2.4 and above, this must be noted
-
DTM you just need the latest one
Second, the clone DTM
git clone https://github.com/yedf/dtm.git
Copy the code
Configuration file
1. Go to conf.sample.yml in the project folder
2. Cp conf.sample.yml conf.yml
3, using etCD, open the following comment in the configuration (if you do not use ETCD, it is easier, this is saved, directly link to the DTM server address is ok)
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'
Copy the code
Explain:
Leave MicroService alone. This means to register THE DTM in the MicroService cluster so that the internal services of the MicroService cluster can interact directly with the DTM through GRPC
Driver: ‘DTM-driver-gozero’, which uses go-Zero’s registration service to discover drivers and supports Go-Zero
Target: ‘etcd: / / localhost: 2379 / dtmservice’ current of DTM micro server registered directly to the service’s etcd cluster, if go to zero as the service, you can directly through the etcd got DTM server GRPC links, 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
4. Start the DTM Server
In the DTM project root directory
go run app/main.go dev
Copy the code
5. Use GO-Zero GRPC to connect to DTM
This is an example of a quick inventory order
1, the order – API
Order-api is an HTTP service entry to create an order
service order {
@doc "Create order"
@handler create
post /order/quickCreate (QuickCreateReq) returns (QuickCreateResp)
}
Copy the code
Next, logic
func (l *CreateLogic) Create(req types.QuickCreateReq,r *http.Request) (*types.QuickCreateResp, error) {
orderRpcBusiServer, err := l.svcCtx.Config.OrderRpcConf.BuildTarget()
iferr ! =nil{
return nil,fmt.Errorf("Abnormal order timeout")
}
stockRpcBusiServer, err := l.svcCtx.Config.StockRpcConf.BuildTarget()
iferr ! =nil{
return nil,fmt.Errorf("Abnormal order timeout")
}
createOrderReq:= &order.CreateReq{UserId: req.UserId,GoodsId: req.GoodsId,Num: req.Num}
deductReq:= &stock.DecuctReq{GoodsId: req.GoodsId,Num: req.Num}
// Here is only the saga example, TCC and other examples are basically the same, please refer to DTM official website for details
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)
iferr ! =nil{
return nil,fmt.Errorf("submit data to dtm-server err : %+v \n",err)
}
return &types.QuickCreateResp{}, nil
}
Copy the code
When entering the order logic, get the ETCD address of the ORDER order and the RPC of the stock inventory service, using the BuildTarget() method
Then create the request parameters for order and stock
Request DTM to obtain the global transaction ID, based on this global transaction ID, start the saga distributed transaction of GRPC, and put the request for order creation and inventory reduction into the transaction. GRPC form request is used here, and each business should have one forward request, one rollback request and request parameters. When executing any of these forward requests fails, all business rollback requests in the transaction are automatically invoked to achieve the rollback effect.
2, the order – the SRV
Order-srv is an RPC service for orders that interacts with the ORDER table in the DTM-GOzero-Order database
// service
service order {
rpc create(CreateReq)returns(CreateResp);
rpc createRollback(CreateReq)returns(CreateResp);
}
Copy the code
2.1 the Create
The create method is requested by default when order-API commits a transaction, so let’s look at logic
func (l *CreateLogic) Create(in *pb.CreateReq) (*pb.CreateResp, error) {
fmt.Printf("Create order in: % v \n", in)
// Add a barrier table to the existing SQL database to prevent void compensation and void suspension
barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
db, err := sqlx.NewMysql(l.svcCtx.Config.DB.DataSource).RawDB()
iferr ! =nil {
/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
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)
iferr ! =nil {
return fmt.Errorf("Create order failed err :% v, order:%+v \n", err, order)
}
return nil}); err ! =nil {
/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return nil, status.Error(codes.Internal, err.Error())
}
return &pb.CreateResp{}, nil
}
Copy the code
As you can see, DTM’s sub-transaction barrier technology is used as soon as we enter the method. The reason why we use the sub-transaction barrier technology is because there may be repeated requests or dirty data caused by empty requests. Here DTM automatically does idempotent processing for us, so we don’t need to do it ourselves. At the same time to ensure that its internal idempotent processing and our own execution of the transaction in the same transaction, so to use a session db link, this time we need to obtain first
db, err := sqlx.NewMysql(l.svcCtx.Config.DB.DataSource).RawDB()
Copy the code
DTM then does idempotent processing internally through SQL execution based on this DB connection, and we start transactions based on this DB connection so that the sub-transaction barrier within DTM executes 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 determine whether to perform the rollback operation or retry all the time according to the GRPC error code we return to it:
- Codes. Internal: DTM server will not call rollback, and will always retry, each retry DTM database will add a retry number, you can monitor the retry number alarm, manual processing
- Codes. Aborted: DTM Server invokes all rollback requests to perform the rollback operation
If DTM returns nil error when calling GRPC, the call is considered successful
2.2 CreateRollback
When we call codes.Aborted which is returned to DTM server when the order creation order or inventory deduction is made, DTM Server will call all rollback operations, CreateRollback is the rollback operation corresponding to the order 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)
iferr ! =nil&& err ! = model.ErrNotFound {/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return nil, status.Error(codes.Internal, err.Error())
}
iforder ! =nil {
barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
db, err := l.svcCtx.OrderModel.SqlDB()
iferr ! =nil {
/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return nil, status.Error(codes.Internal, err.Error())
}
if err := barrier.CallWithDB(db, func(tx *sql.Tx) error {
order.RowState = - 1
iferr := 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)
/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return nil, status.Error(codes.Internal, err.Error())
}
}
return &pb.CreateResp{}, nil
}
Copy the code
In fact, if the previous order is successful, the previous successful order is cancelled, which is the rollback operation of the corresponding order
3, stock – the SRV
3.1 Deduct
Subtract inventory, which is a forward operation within an order transaction, as shown in the code below
func (l *DeductLogic) Deduct(in *pb.DecuctReq) (*pb.DeductResp, error) {
fmt.Printf("Start....")
stock, err := l.svcCtx.StockModel.FindOneByGoodsId(in.GoodsId)
iferr ! =nil&& err ! = model.ErrNotFound {/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return nil, status.Error(codes.Internal, err.Error())
}
if stock == nil || stock.Num < in.Num {
Aborted, dtmcli.ResultFailure can be rolled back
return nil, status.Error(codes.Aborted, dtmcli.ResultFailure)
}
// Add a barrier table to the existing SQL database to prevent void compensation and void suspension
barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
db, err := l.svcCtx.StockModel.SqlDB()
iferr ! =nil {
/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
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)
iferr ! =nil{
/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return status.Error(codes.Internal, err.Error())
}
affected, err := sqlResult.RowsAffected()
iferr ! =nil{
/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return status.Error(codes.Internal, err.Error())
}
// If the number of affected rows is 0, DTM is told to fail without retrying
if affected <= 0 {
return status.Error(codes.Aborted, dtmcli.ResultFailure)
}
/ /!!!!! Open the test!! The rollback status of the test order changes to invalid and does not need to be rolled back for the current repository failure
//return FMT.Errorf(" Err :% v,in :%+v \n",err,in)
return nil}); err ! =nil {
/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return nil,err
}
return &pb.DeductResp{}, nil
}
Copy the code
It is worth noting here that DTM server needs to be told to roll back only when the inventory is insufficient or the number of rows affected by the held inventory is 0 (failed). In other cases, network jitter or hardware abnormality is the cause, so DTM server should always try again. Of course, it should add a monitoring alarm of the maximum number of retries. If the maximum number of times has not been successful to achieve automatic SMS, call manual intervention.
3.2 DeductRollback
Here is the rollback operation corresponding to the withholding inventory
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()
iferr ! =nil {
/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return nil, status.Error(codes.Internal, err.Error())
}
if err := barrier.CallWithDB(db, func(tx *sql.Tx) error {
iferr := 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)
/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return nil, status.Error(codes.Internal, err.Error())
}
return &pb.DeductResp{}, nil
}
Copy the code
Sixth, sub-transaction barriers
This term is defined by the DTM authors, and there is not much code for sub-transaction barriers.
// CallWithDB the same as Call, but with *sql.DB
func (bb *BranchBarrier) CallWithDB(db *sql.DB, busiCall BarrierBusiFunc) error {
tx, err := db.Begin()
iferr ! =nil {
return err
}
return bb.Call(tx, busiCall)
}
Copy the code
Since this method starts a local transaction internally, it performs SQL operations in this transaction, so when we perform our own business, we must use the same transaction with it, which is based on the same DB connection to open the transaction, so~ you know why we need to obtain the DB connection in advance. The goal is to make the SQL operations that it executes inside the same transaction as our SQL operations. As to why it performs its own SQL operations internally, let’s examine.
Let’s look at the bb.Call method
/ / Call the child transaction barriers, detail see https://zhuanlan.zhihu.com/p/388444465
// tx: a transaction object for the local database that allows the sub-transaction barrier to perform transactions
// busiCall: business function, called only 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(a) {
// Logf("barrier call error is %v", rerr)
if x := recover(a); x ! =nil {
tx.Rollback()
panic(x)
} else ifrerr ! =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
}
Copy the code
The core is just a few 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)
Copy the code
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)
}
Copy the code
OriginType = originType = originType = originType = originType = originType = originType = originType = originType = action
originAffected, _ := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, originType, bid, ti.Op)
Copy the code
Then the above SQL will not execute because ti.Op == “” returns in insertBarrier
currentAffected, rerr := insertBarrier(tx, ti.TransType, ti.Gid, ti.BranchID, ti.Op, bid, ti.Op)
Copy the code
The second SQL ti.Op is an action, so the child transaction barrier inserts a single entry
Similarly, an entry is inserted in the execution inventory
Subtransaction barriers where the entire transaction is successful
In the case of a normal successful order request, ti.Op is all action, so the originType is “”. Therefore, no matter the ordered barrier or the inventory barrier, when executing their two barrier inserts, The originAffected will be ignored, because the originType== “” will be directly inserted by return without inserting data, so it seems that the second insert data of the barrier will take effect whether the order is placed or the inventory is deducted, so there will be two order data in the barrier data table. One is for the order and one is for the inventory
Gid: DTM global transaction ID
Branch_id: indicates the service ID of each global transaction ID
Op: action, if a normal successful request is an action
Barrier_id: indicates that multiple meetings are incremented for the same service
These four fields are joint unique indexes in the table. When inserting tbarrier, DTM judges that they exist and ignores them
2. If the order is successful, the inventory is insufficient to roll back the transaction barrier
We only have 10 in stock. We’ll 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 sufficient query and insufficient deduction),
Therefore, a successful order in the barrier table will generate a correct data execution data in the barrier table according to the previous logic
2) Then execute the inventory withholding operation
func (l *DeductLogic) Deduct(in *pb.DecuctReq) (*pb.DeductResp, error) {
fmt.Printf("Start....")
stock, err := l.svcCtx.StockModel.FindOneByGoodsId(in.GoodsId)
iferr ! =nil&& err ! = model.ErrNotFound {/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return nil, status.Error(codes.Internal, err.Error())
}
if stock == nil || stock.Num < in.Num {
Aborted, dtmcli.ResultFailure can be rolled back
return nil, status.Error(codes.Aborted, dtmcli.ResultFailure)
}
.......
}
Copy the code
Before executing the storage logic, we directly return codes.Aborted because we query the inventory and find that the inventory is insufficient. Therefore, we do not go to the sub-transaction barrier barrier, so the barrier table does not insert data, but tells DTM to roll back
3) Call the order rollback operation
When the order is rolled back, the Barrier code (see below) will be executed. The compensation code for the rollback is ti.Op, 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)
Copy the code
Since the order was successfully placed, the Barrier table contains an action record for the successful order, so originAffected==0, only the current rollback record will be inserted and busiCall(tx) will be called to perform the subsequent rollback that we wrote ourselves
At this point, we should have only two data, one for the order successfully created record and one for the order rollback record
4) DeductRollback
DeductRollback will be called again after the order rollback is successful. The inventory rollback code is shown below
That’s what the subtransaction barrier automatically does for us, that’s what those two core insert statements do for us, so that our business doesn’t have dirty data
There are two types of inventory rollback
-
Rollback failed. Procedure
-
Rollback succeeded. Procedure
Rollback failed (this is our current example scenario)
Op is compensate and orginType is Action, which performs the following 2 inserts
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)
}
Copy the code
If the rollback or cancel operation is performed, originAffected > 0 The current operation was successfully inserted and the previous forward deduction inventory operation was not successfully inserted, indicating that the previous deduction inventory was not successfully inserted. Direct return does not need to perform subsequent compensation. Therefore, a return will be inserted into the barrier table, and subsequent compensation operations will not be performed
At this point we have four entries in the barrier table
Successful rollback (try to simulate this scenario yourself)
If we succeeded in the previous inventory drawdown, ti.Op is compensate and orginType is action when this compensation is executed. Continue with 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 || // This is null compensation
currentAffected == 0 { // This is a repeat request or suspension
return}rerr = busiCall(tx)
}
Copy the code
If the rollback or cancel operation is performed, originAffected == 0 was not inserted, indicating that the current insert was successfully inserted into the forward deduction inventory. Only the second SQL statement record can be inserted, and the subsequent business operations can be compensated.
Therefore, the core statement is 2 INSERTS, which helps us to solve the repeated rollback data and data idempotent situation. It can only be said that the author of DTM has a really good idea, and helped us solve a very troublesome problem with the least amount of code
7. Precautions in GO-Zero docking
1. DTM rollback compensation
When using DTM GRPC, when using SAGA, TCC, etc., if the first step fails, it is expected that it can perform the following rollback. If there is an error in the GRPC service, it must return: Status.error (codes.aborted, dtmcli.resultFailure), returns other errors, will not execute your rollback operation, DTM will always retry, as follows:
stock, err := l.svcCtx.StockModel.FindOneByGoodsId(in.GoodsId)
iferr ! =nil&& err ! = model.ErrNotFound {/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return nil, status.Error(codes.Internal, err.Error())
}
if stock == nil || stock.Num < in.Num {
Aborted, dtmcli.ResultFailure can be rolled back
return nil, status.Error(codes.Aborted, dtmcli.ResultFailure)
}
Copy the code
2. Empty compensation and suspension of barrier, etc
In preparation, we created the dtm_barrier library and executed the barrier. Mysql.sql. This is a check for our business services to prevent null compensation.
If you use it online, every service you use to interact with db will need to be granted access to the Barrier library if it uses a mysql account
Barrier is a local transaction in RPC
In the case of RPC, if barriers are used, transactions must be used to interact with the DB in the Model, and the same transaction must be used with barrier
logic
barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
db, err := sqlx.NewMysql(l.svcCtx.Config.DB.DataSource).RawDB()
iferr ! =nil {
/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
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)
iferr ! =nil{
/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return status.Error(codes.Internal, err.Error())
}
affected, err := sqlResult.RowsAffected()
iferr ! =nil{
/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return status.Error(codes.Internal, err.Error())
}
// If the number of affected rows is 0, DTM is told to fail without retrying
if affected <= 0 {
return status.Error(codes.Aborted, dtmcli.ResultFailure)
}
/ /!!!!! Open the test!! : The rollback status of the test order changes to invalid and does not need to be rolled back if the current repository fails
// return FMT.Errorf(" Err :% v,in :%+v \n",err,in)
return nil}); err ! =nil {
/ /!!!!!! Aborted, dTMCLI. ResultFailure is not a failure to return codes.aborted.
return nil, err
}
Copy the code
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
}
Copy the code
7. Use GO-Zero HTTP docking
Given that go is not used in many HTTP scenarios in microservices, I will not go into details here. I wrote a simple barrier in a previous version, but it is not as complete as this. If you are interested, you can check it out. The official version of DTM has been modified and is no longer needed.
Project address: github.com/Mikaelemmmm…
The project address
Github.com/zeromicro/g…
Welcome to Go-Zero and star support us!
Wechat communication group
Pay attention to the public account of “micro-service Practice” and click on the exchange group to obtain the QR code of the community group.