github
scenario
The client invokes service A’s interface, which in turn invokes service B. If both service A and service B execute successfully, they succeed, and both transactions should commit; If either service A or service B fails, it fails and neither transaction is executed or rolled back. Due to the unreliability of network requests, if user A fails to invoke user B, user B may: 1. 2. B fails to execute the message after receiving it. 3. B An exception occurs when the request is returned. 4. The call to USER B times out. The call to user B may succeed or fail. So when A calls B, there may be inconsistencies.
Interface calls in several ways
Method 1: Directly invoke feign
A Service interface:
try{business code A feign invokes B service interface transaction commit}catch(Exception e) {transaction rollback}Copy the code
B Service interface:
try{business code B transaction commit// The interface status code is 2XX
} catch(Exception e) {transaction rollback// The interface status code is not 2XX
}
Copy the code
Consistency analysis:
- Feign successfully invoked the service interface B. The status code is 2XX. B commits the service transaction and A commits the transaction. If transaction A commits successfully, it is consistent; Inconsistent if transaction A fails to commit but transaction B has committed at this time.
- B does not receive the network request. B is not executed, the feign call throws an exception, A transaction does not commit, goes into rollback, data consistent.
- B An exception occurs when the request is returned. The transaction B has been committed, but feign fails to invoke the service interface of B. The transaction A is rolled back, and the data is inconsistent.
- B The call timed out. User B does not receive the network request, executes the request successfully, returns the request abnormally, or receives the request slowly. The consistency state is uncertain. It’s all possible.
Method two: Based on reliable information
Service A sends A message to A message queue (for example, RabbitMQ) after the service code is executed, and uses ack to ensure that the message is sent successfully. After the service B receives a successful consumption, it manually acknowledges the message, and Kafka manually submits the shift.
A Service Producer:
tryA ack = {business code rabbitmqProducer sendAndGetAckif! ack {// If sending fails, you can set automatic retry. If you do not retry, an exception will be thrown
throws newRabbitmqSendException} Transaction commit}catch (Exception e) {
// Transaction rollback
}
Copy the code
B Service message queue consumer 1:
//
try{business code B channel.basicAck Manual confirmation// Kafka submits the shift manuallyTransaction commit// The interface status code is 2XX
} catch(Exception e) {transaction rollback// The interface status code is not 2XX
}
Copy the code
B Service message queue consumer 2:
//
try{business code B transaction commit// The interface status code is 2XX
} catch(Exception e) {transaction rollback// The interface status code is not 2XX} channel.basicAck manual confirmation// Kafka submits the shift manually
Copy the code
Consistency analysis:
- Service A successfully sends A message to the message queue, but fails to submit the transaction, causing data inconsistency.
- B Consumer 1: After manual confirmation, the message is deleted from the message queue, and the transaction of B fails to be submitted, causing data inconsistency.
- Consumer B: The transaction of user B is successfully submitted but manual confirmation fails. The message may be received repeatedly, causing inconsistency. In this case, the UUID can be added to the message. After receiving the message, service B performs a deprocessing based on the UUID to achieve idempotency.
Method 3: Pre-execute + confirm + Check, similar to TCC
Service A needs to insert A table, transaction_record, to record the call status for service B’s callback.
A Service business interface:
String uuid = generateUUID()// Generate a UUID
try{
feign.preCreate(uuid,...)// Feign invokes B preexecution, for example, the B service is the create order service, which precreates an order, but the state is pending confirmationBusiness code A inserts uUID into transaction_record table transaction commit}catch(Exception e) {transaction rollback feign.cancel(uuid)// Feign calls B to cancel, for example, B is the order creation service, and sets the order status to cancel
}
feign.confirm(uuid)// Feign calls B to confirm, for example, B is the order creation service, and sets the status of the order to confirm, then the order is available
Copy the code
Service A provides service B with the service check status:
Get /v1/transaction/{uuid} query from transaction_record, if yes, confirm; if no, cancelCopy the code
B service needs to provide pre-execute, confirm, and cancel interfaces. If there is no confirmation or cancellation after the pre-execution, USER B checks back with USER A and confirms or cancels the confirmation based on the result.
Consistency analysis:
- Description preCreate execution in A is abnormal. An exception should be thrown, the business code is no longer executed, and the event table inserts the UUID to perform cancel. If B executes, the status is also unconfirmed, which does not affect consistency; If cancel also fails to be performed, for example, B hangs, B should invoke the lookup interface of service A after the restart to determine the status. The status is consistent.
- Business code A fails, the transaction is rolled back, and feign calls B to cancel. If the cancellation succeeds, the status is the same. If the cancellation fails, throw an exception and confirm will not be executed. The status is consistent.
- Business code A executes successfully, transaction commit fails, transaction rollback is performed. If the rollback fails, an exception is thrown and cancel and confirm are not executed. In this case, USER B checks the confirm status based on timeout. If the rollback succeeds, cancel the rollback. The status is consistent.
- Transaction A commits successfully, but confirmation fails (for example, service A or B hangs while A performs confirmation). Service B times out and checks the UUID, and changes its status to confirm. The status is consistent.
- After the pre-processing is completed, the business code is executed. If the business code is executed slowly and B service thinks it has timed out, service B will check back due to timeout. If the transaction of A has not been submitted, the lookup interface of A will return cancellation, then B will be cancelled, but A has submitted the transaction, and the transaction status is inconsistent at this time. This can be adjusted by setting A slightly larger timeout for B, allowing service A to pass in the expected timeout during A pre-processed feign call.
conclusion
The above is several ways of interface invocation, here only provides a general idea, application can be optimized, synchronous send can also be changed to asynchronous + retry and other ways. If consistency requirements can be adopted in method 3, uUID + insert table can also be implemented in other ways. For consistency, interface calls should also be idempotent, either through idempotent business logic or Uuid.