preface
One of the problems with microservices architecture, distributed transactions, was described in the previous SEATA solution introduction, along with common solutions in the industry. This article, in view of the various problems encountered by me and my colleagues in the process of learning Seata, combined with the current latest version of Seata 1.4.2 code implementation, with everyone in-depth understanding of Seata. The main problems are:
- What are Seata’s core competencies?
- What modules are available within Seata?
- What is the difference between AT and XA schemes?
- What is the Commit/Rollback process?
- How is the AT mode initialized?
- Why doesn’t RM need @GlobalTransactional?
- What does @GlobalLock do?
- Why does ExceptionHandler cause a global transaction to fail?
What are Seata’s main strengths?
In the last article we introduced 2PC/XA, TCC, and SAGA schemes. 2 PC/XA has the advantage of the business code without intrusion, but its drawback is obvious: must request database support for the XA protocol, and the characteristics of the XA protocol itself, it will cause the transaction resource for a long time can not get release, lock cycle is long, and unable to intervene on the application layer, so the performance is very poor, belong to kill one thousand since the eight hundred loss. TCC and SAGA schemes are business intrusive, and the implementation of commit logic is accompanied by rollback logic (or compensation logic), which makes the code very bloated and costly to maintain.
According to Ali engineers, AT mode, as the default mode of Seata, although it is also a two-stage submission scheme similar to XA scheme, is aimed AT non-invasive business and high performance direction from the beginning, which is exactly the urgent need for us to solve the problem of distributed transaction.
What modules are available within Seata?
Seata-at is designed to treat a distributed transaction as a global transaction, with several branch transactions hanging from it. A branch transaction is a local transaction that meets ACID, so we can operate on a distributed transaction as if it were a local transaction.
Seata internally defines three modules to handle global and branch transactions:
- Transaction Coordinator (TC) : A Transaction Coordinator that maintains the running status of global transactions and is deployed independently to coordinate and drive the submission or rollback of global transactions.
- Transaction Manager (TM) : Controls the boundaries of a global Transaction, is responsible for starting a global Transaction, and ultimately initiating a global commit or rollback resolution.
- Resource Manager (RM) : Controls branch transactions, is responsible for branch registration, status reporting, receives transaction coordinator instructions, and drives the commit and rollback of branch (local) transactions.
Global transaction execution steps:
- TM applies to TC for starting a global transaction. TC creates a global transaction and returns a globally unique XID, which will be propagated in the context of the global transaction
- RM registers branch transactions with TC. The branch transactions belong to global transactions with the same XID.
- TM initiates global commit or rollback to TC;
- TC schedules branch transactions under XID to commit or roll back.
What is the difference between AT and XA schemes?
Seata’s transaction commit is basically the same as XA’s two-part commit in general. What’s the difference?
As we all know, XA protocol relies on the database level to guarantee the consistency of transactions, that is, each branch transaction of XA is driven at the database level. Since each branch transaction of XA requires XA drivers, on the one hand, database and XA driver will be coupled. On the other hand, it will lead to long transaction resource lock cycle of each branch, which is also an important factor that it is not popular in Internet companies.
Based on the XA protocol above, Seata takes a different approach. Since there are so many problems caused by relying on the database layer, I will start with the application layer. This also starts with the RM module of Seata. For example, DataSource, Connection, and Statement have a layer of proxies.
Seata proxies data sources, so we actually use Seata’s own DataSourceProxy, DataSourceProxy. Seata adds a lot of logic to this layer of proxy, mainly parsing SQL. The data mirrors of service data before and after data update are organized into rollback logs, and the UndoLog logs are inserted into the undo_log table to ensure that each service SQL of updated data has a corresponding rollback log.
In this way, the resources locked by the local transaction can be released immediately after the local transaction is executed, and the branch status can be reported to the TC. When TM decides global submission, there is no need for synchronous coordination processing, TC will asynchronously schedule each RM branch transaction to delete the corresponding UndoLog log, this step can be completed very quickly; When TM decides to roll back globally, RM receives the rollback request from TC. RM uses XID to find the corresponding UndoLog rollback log and executes the rollback log to complete the rollback operation.
As shown in the figure above, the RM of the XA schema is placed in the database layer and relies on the XA driver of the database.
However, RM of Seata is actually placed in the application layer in the form of middleware, which does not rely on the support of the database for the protocol, completely stripping the requirements of the distributed transaction scheme on the support of the database for the protocol.
What is the Commit/Rollback process?
In summary, the WORKFLOW of the AT pattern is divided into two phases. In the first stage, business SQL is executed, and snapshots (images) before and after data modification are generated through SQL interception and REWRITING, and submitted in the same local transaction as UndoLog and business modification.
If the first phase succeeds, then the second phase will only asynchronously delete the newly inserted UndoLog; If phase 2 fails, UndoLog generates a reverse SQL statement to roll back the data modification in phase 1. The key SQL parsing and concatenation work is done using code from Druid Parser, which is not covered in this article. If you are interested, check out the source code.
Let’s use order_TBL from the previous article as an example to illustrate how the entire AT branch works.
Business table: order_tbL
Field | Type | Key |
---|---|---|
id | int | PRI |
user_id | varchar(255) | |
commodity_code | varchar(255) | |
count | int | |
money | int |
The business logic of an AT branch transaction:
insert into order_tbl values (12, '1002', '2001', 1, 5);
Copy the code
A phase
Process:
- Parse SQL: get the type of SQL (INSERT), table (order_TBL), condition, etc. Image before query: Generates a query statement to locate data based on the condition information obtained through parsing. If data is to be updated, the following image is generated based on the conditions of the update statement.
Select id, user_id, commodity_code, count, money from product where id = 12;Copy the code
- Execute business SQL: Insert this record.
- Mirror after query: Locate data by primary key based on the result of the former mirror.
Select id, user_id, commodity_code, count, money from product where id = 12;Copy the code
After obtaining the mirror image:
id | user_id | commodity_code | count | money |
---|---|---|---|---|
1 | 1002 | 2001 | 1 | 5 |
- Insert rollback log: The system inserts the mirror data and service SQL information into a rollback log record
UNDO_LOG
In the table.
{@ "class" : "IO. Seata. Rm. The datasource. Undo. BranchUndoLog", "xid" : "172.27.0.2:8091-18207193960247322", "branchId" : 18207193960247324, "sqlUndoLogs": [ "java.util.ArrayList", [ { "@class": "io.seata.rm.datasource.undo.SQLUndoLog", "sqlType": "INSERT", "tableName": "order_tbl", "beforeImage": { "@class": "io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords", "tableName": "order_tbl", "rows": [ "java.util.ArrayList", [] ] }, "afterImage": { "@class": "io.seata.rm.datasource.sql.struct.TableRecords", "tableName": "order_tbl", "rows": [ "java.util.ArrayList", [ { "@class": "io.seata.rm.datasource.sql.struct.Row", "fields": [ "java.util.ArrayList", [ { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "id", "keyType": "PRIMARY_KEY", "type": 4, "value": 12 }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "user_id", "keyType": "NULL", "type": 12, "value": "1002" }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "commodity_code", "keyType": "NULL", "type": 12, "value": "2001" }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "count", "keyType": "NULL", "type": 4, "value": 1 }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "money", "keyType": "NULL", "type": 4, "value": 5 } ] ] } ] ] } } ] ] }Copy the code
- Before submitting, apply to TC Registration Branch:
order_tbl
Table where the primary key is equal to 12Global lock 。 - Local transaction commit: Updates to the business data are committed along with the UndoLog generated in the previous steps.
- The local transaction submission result is reported to the TC.
Phase 2 – Rollback
- After receiving the Branch rollback request from the TC, a local transaction is started and the corresponding UndoLog record is found based on the XID and Branch ID.
- Data check: Compare the mirror in UndoLog with the current data. If there is a difference, the data has been modified by an action other than the current global transaction. In this case, you need to handle it according to the configuration policy.
- Generate and execute a rollback statement based on the information about the front image and business SQL in UndoLog:
delete from order_tbl where id = 12;
Copy the code
- Delete UndoLog.
- Commit a local transaction.
- Report the execution results of local transactions (that is, the rollback results of branch transactions) to the TC.
Phase two – Commit
- After receiving the branch commit request from TC, it puts the request into an asynchronous task queue and immediately returns the result of successful commit to TC. Find UndoLog.
- Delete corresponding UndoLog records in batches.
- Start committing the local transaction.
- Report local results to TC.
How is the AT mode initialized?
TM is responsible for starting a GlobalTransactional transaction. In the last article, we started a GlobalTransactional transaction in BusinessService. We noticed that there was a @globaltransactional annotation. Detailed IO. Seata. Spring. The annotation. GlobalTransactional code can refer to here.
//BusinessService
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
LOGGER.info("purchase begin ... xid: " + RootContext.getXID());
storageClient.deduct(commodityCode, orderCount);
orderClient.create(userId, commodityCode, orderCount);
}
Copy the code
Under the same package, there is an IO seata. Spring. The annotation. GlobalTransactionScanner, . It inherits the org. Springframework. Aop framework. Autoproxy. AbstractAutoProxyCreator, in its initClient () method, the TM and RM is initialized.
private void initClient(a) {
// ...
//init TM
TMClient.init(applicationId, txServiceGroup, accessKey, secretKey);
// ...
//init RM
RMClient.init(applicationId, txServiceGroup);
// ...
registerSpringShutdownHook();
}
Copy the code
Initialization of TM
In the init method TMClient obtained ty. Io.seata.core.rpc.net TmNettyRemotingClient instance, used for processing various interact with the message from the server.
// TMClient
public static void init(String applicationId, String transactionServiceGroup, String accessKey, String secretKey) {
TmNettyRemotingClient tmNettyRemotingClient = TmNettyRemotingClient.getInstance(applicationId, transactionServiceGroup, accessKey, secretKey);
tmNettyRemotingClient.init();
}
Copy the code
TmNettyRemotingClient inherited io.seata.core.rpc.net ty AbstractNettyRemotingClient, in AbstractNettyRemotingClient init method, RegisterTMRequest (RM client sends RegisterRMRequest) request to the server. The logic is as follows: NettyClientChannelManager channels in the cache the client channel, if the channels does not exist or has expired, Then it tries to connect to the server to retrieve the channel and cache it in channels.
@Override
public void init(a) {
timerExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run(a) {
clientChannelManager.reconnect(getTransactionServiceGroup());
}
}, SCHEDULE_DELAY_MILLS, SCHEDULE_INTERVAL_MILLS, TimeUnit.MILLISECONDS);
if (NettyClientConfig.isEnableClientBatchSendRequest()) {
mergeSendExecutorService = new ThreadPoolExecutor(MAX_MERGE_SEND_THREAD,
MAX_MERGE_SEND_THREAD,
KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
new NamedThreadFactory(getThreadPrefix(), MAX_MERGE_SEND_THREAD));
mergeSendExecutorService.submit(new MergedSendRunnable());
}
super.init();
clientBootstrap.start();
}
Copy the code
Initialization of RM
In the init method RMClient RmNettyRemotingClient. GetInstance processing logic and TM are roughly the same; ResourceManager is an RM ResourceManager. ResourceManager registers, submits, reports, rolls back branch transactions, and queries global locks. DefaultResourceManager holds all RM resource managers and invokes them in a unified manner.
TransactionMessageHandler is RM message handler to handle from TC send instructions, and shall submit to branch to branch, branch rollback, and UndoLog delete operation; Finally, the init method is generally consistent with TM logic; DefaultRMHandler encapsulates some of the operational logic for RM branch transactions.
public static void init(String applicationId, String transactionServiceGroup) {
RmNettyRemotingClient rmNettyRemotingClient = RmNettyRemotingClient.getInstance(applicationId, transactionServiceGroup);
rmNettyRemotingClient.setResourceManager(DefaultResourceManager.get());
rmNettyRemotingClient.setTransactionMessageHandler(DefaultRMHandler.get());
rmNettyRemotingClient.init();
}
Copy the code
Adding interceptors
GlobalTransactionScanner’s wrapIfNecessary method scans methods annotated with @GlobalTransactional, @GlobalLock, etc., and adds interceptors to them.
- Determine if a corresponding annotation exists
- Creating interceptors
- Adds an interceptor to the target object
/** * The following will be scanned, and added corresponding interceptor */
@Override
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
try {
synchronized (PROXYED_SET) {
if (PROXYED_SET.contains(beanName)) {
return bean;
}
interceptor = null;
//check TCC proxy
if (TCCBeanParserUtils.isTccAutoProxy(bean, beanName, applicationContext)) {
//TCC interceptor, proxy bean of sofa:reference/dubbo:reference, and LocalTCC
interceptor = new TccActionInterceptor(TCCBeanParserUtils.getRemotingDesc(beanName));
ConfigurationCache.addConfigListener(ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION,
(ConfigurationChangeListener)interceptor);
} else{ Class<? > serviceInterface = SpringProxyUtils.findTargetClass(bean); Class<? >[] interfacesIfJdk = SpringProxyUtils.findInterfaces(bean);/ / # # # # < 1 > # # # #
if(! existsAnnotation(newClass[]{serviceInterface}) && ! existsAnnotation(interfacesIfJdk)) {return bean;
}
if (globalTransactionalInterceptor == null) {
/ / # # # # < 2 > # # # #
globalTransactionalInterceptor = new GlobalTransactionalInterceptor(failureHandlerHook);
ConfigurationCache.addConfigListener(
ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION,
(ConfigurationChangeListener)globalTransactionalInterceptor);
}
interceptor = globalTransactionalInterceptor;
}
LOGGER.info("Bean[{}] with name [{}] would use interceptor [{}]", bean.getClass().getName(), beanName, interceptor.getClass().getName());
if(! AopUtils.isAopProxy(bean)) { bean =super.wrapIfNecessary(bean, beanName, cacheKey);
} else {
/ / # # # # < 3 > # # # #
AdvisedSupport advised = SpringProxyUtils.getAdvisedSupport(bean);
Advisor[] advisor = buildAdvisors(beanName, getAdvicesAndAdvisorsForBean(null.null.null));
for (Advisor avr : advisor) {
advised.addAdvisor(0, avr);
}
}
PROXYED_SET.add(beanName);
returnbean; }}catch (Exception exx) {
throw newRuntimeException(exx); }}Copy the code
Transaction processing
In a global transaction @ GlobalTransactional, for example, in IO. Seata. Spring. The annotation. GlobalTransactionalInterceptor invoke method, HandleGlobalTransaction method, in this method, call the IO seata. Tm. API. TransactionalTemplate the execute method.
@Override
public Object invoke(final MethodInvocation methodInvocation) throws Throwable { Class<? > targetClass = methodInvocation.getThis() ! =null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null;
Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass);
if(specificMethod ! =null && !specificMethod.getDeclaringClass().equals(Object.class)) {
final Method method = BridgeMethodResolver.findBridgedMethod(specificMethod);
final GlobalTransactional globalTransactionalAnnotation =
getAnnotation(method, targetClass, GlobalTransactional.class);
final GlobalLock globalLockAnnotation = getAnnotation(method, targetClass, GlobalLock.class);
boolean localDisable = disable || (degradeCheck && degradeNum >= degradeCheckAllowTimes);
if(! localDisable) {if(globalTransactionalAnnotation ! =null) {
return handleGlobalTransaction(methodInvocation, globalTransactionalAnnotation);
} else if(globalLockAnnotation ! =null) {
returnhandleGlobalLock(methodInvocation, globalLockAnnotation); }}}return methodInvocation.proceed();
}
Object handleGlobalTransaction(final MethodInvocation methodInvocation,
final GlobalTransactional globalTrxAnno) throws Throwable {
boolean succeed = true;
try {
return transactionalTemplate.execute(new TransactionalExecutor() {
@Override
public Object execute(a) throws Throwable {
return methodInvocation.proceed();
}
// ...
});
}
// ...
}
Copy the code
The execute method of TransactionalTemplate performs specific transactions, such as starting a transaction, committing, rolling back, and so on.
public Object execute(TransactionalExecutor business) throws Throwable {
// 1. Get transactionInfo
TransactionInfo txInfo = business.getTransactionInfo();
if (txInfo == null) {
throw new ShouldNeverHappenException("transactionInfo does not exist");
}
/ / 1.1 Get the current transaction, if not null, the tx role is' GlobalTransactionRole. The Participant.
GlobalTransaction tx = GlobalTransactionContext.getCurrent();
// 1.2 Handle the transaction propagation.
Propagation propagation = txInfo.getPropagation();
SuspendedResourcesHolder suspendedResourcesHolder = null;
try {
// ...
/ / 1.3 If null, the create new transaction with roles' GlobalTransactionRole. The Launcher '.
if (tx == null) {
tx = GlobalTransactionContext.createNew();
}
// set current tx config to holder
GlobalLockConfig previousConfig = replaceGlobalLockConfig(txInfo);
try {
// 2. If the tx role is 'GlobalTransactionRole.Launcher', send the request of beginTransaction to TC,
// else do nothing. Of course, the hooks will still be triggered.
beginTransaction(txInfo, tx);
Object rs;
try {
// Do Your Business
rs = business.execute();
} catch (Throwable ex) {
// 3. The needed business exception to rollback.
completeTransactionAfterThrowing(txInfo, tx, ex);
throw ex;
}
// 4. everything is fine, commit.
commitTransaction(tx);
return rs;
} finally {
//5. clearresumeGlobalLockConfig(previousConfig); triggerAfterCompletion(); cleanUp(); }}finally {
// If the transaction is suspended, resume it.
if(suspendedResourcesHolder ! =null) { tx.resume(suspendedResourcesHolder); }}}Copy the code
Why doesn’t RM need @GlobalTransactional?
In the previous code example, we only annotated the @GlobalTransactional annotation on the BusinessService method on the TM side, but not on the downstream microservice. Why should it be treated as a global transaction?
Transaction context
Let’s start by looking at Seata’s transaction context, which is managed by RootContext.
When an application starts a global transaction, RootContext automatically binds the XID of the transaction. When the transaction ends (commit or rollback is complete), RootContext automatically unbinds the XID.
// bind XID rootContext.bind (XID); // Unbind XID String XID = rootContext.unbind ();Copy the code
The application can obtain the global transaction XID of the current runtime through the RootContext API.
// getXID String XID = rootcontext.getxid ();Copy the code
Whether the application is running in the context of a global transaction is determined by whether the RootContext is bound to XID.
public static boolean inGlobalTransaction() { return CONTEXT_HOLDER.get(KEY_XID) ! = null; }Copy the code
Transaction propagation
The propagation mechanism of Seata global transactions refers to the propagation of transaction context and, ultimately, the propagation of XID application runtime.
1. Transaction propagation within the service
By default, the implementation of RootContext is ThreadLocal based, meaning that the XID is bound to the current thread context.
public class ThreadLocalContextCore implements ContextCore {
private ThreadLocal<Map<String, String>> threadLocal = new ThreadLocal<Map<String, String>>() {
@Override
protected Map<String, String> initialValue(a) {
return newHashMap<String, String>(); }};@Override
public String put(String key, String value) {
return threadLocal.get().put(key, value);
}
@Override
public String get(String key) {
return threadLocal.get().get(key);
}
@Override
public String remove(String key) {
returnthreadLocal.get().remove(key); }}Copy the code
So XID propagation within a service is usually naturally linked by invocation links from the same thread. By default nothing is done and the context of the transaction is propagated.
If you want to suspend the transaction context, you need to do so through the API provided by RootContext:
// Suspend (pause) String xid = rootContext.unbind (); // TODO: business logic running outside the global transaction // Restore the global transaction context rootContext.bind (xid);Copy the code
2. Transaction propagation across service invocations
Through the above basic principles, we can easily understand:
Transaction propagation across service invocation scenarios is essentially passing the XID to the service provider through the service invocation and binding it to the RootContext.
As long as this is done, Seata can theoretically support any microservice framework.
We noticed that in SeataFilter of the Common module, the global transaction ID XID was retrieved from the Http request Header and set to the transaction context RootContext.
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
String xid = req.getHeader(RootContext.KEY_XID.toLowerCase());
boolean isBind = false;
if (StringUtils.isNotBlank(xid)) {
RootContext.bind(xid);
isBind = true;
}
try {
filterChain.doFilter(servletRequest, servletResponse);
} finally {
if(isBind) { RootContext.unbind(); }}}Copy the code
In SeataRestTemplateInterceptor, obtain the XID from RootContext first, and then set the Http request header, that can be obtained through SeataFilter downstream of the RM to the XID.
@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
HttpRequestWrapper requestWrapper = new HttpRequestWrapper(httpRequest);
String xid = RootContext.getXID();
if (StringUtils.isNotEmpty(xid)) {
requestWrapper.getHeaders().add(RootContext.KEY_XID, xid);
}
return clientHttpRequestExecution.execute(requestWrapper, bytes);
}
Copy the code
What does @GlobalLock do?
How does @GlobalLock differ from @GlobalTransactional when used in the initialization section?
In the case of a business method decorated with @globallock, even though the method is not a branch transaction under a global transaction, its operations on data resources also require the GlobalLock to be queried first, and wait if other Seata global transactions are being modified. So, if you want the database not to be modified by other transactions during the execution of a Seata global transaction, this method forces the addition of GlobalLock annotations to bring it under the management of a Seata distributed transaction.
The Transactional annotation is similar to Spring’s @Transactional annotation. If you want to start transactions, you must add this annotation. As with Seata, if you want an SQL operation that is not part of a global transaction to not affect an AT distributed transaction, you must add GlobalLock annotations.
Composition and function of global lock
The global lock in Seata AT mode consists of two parts: the table name and the primary key of the operation row. The global lock can be saved on the server to ensure:
- Write isolation before a global transaction
- Global transaction and by
GlobalLock
Modifies write isolation between methods
Registration of global locks
When a client on a stage before the local transaction commit, will first registered branch, with the server at this time will modify the table name, primary key information encapsulated into global lock shall be sent to the server to save, if found that when the server save other existing global transactions lock the line of the primary key, it throws the global lock conflict, client cycle wait and try again.
Global lock query
Methods decorated with @globallock are not part of a global transaction, but they also perform a GlobalLock query before committing the transaction, and if they find that the GlobalLock is being held by another global transaction, they also wait in a loop.
Global lock release
Because the two-phase commit is asynchronous, when the server sends a Branch commit request to the client, the client simply inserts the branch commit information into the memory queue and returns. The server releases the global lock as long as it determines that the process is normal. Therefore, it can be said that if phase one succeeds, the global lock will be released at the beginning of phase two and not locked until the end of the phase two commit process.
However, if phase-1 fails and phase-2 rollback is performed, the global lock will not be released until phase-2 rollback is complete because the rollback is performed synchronously.
Why does ExceptionHandler cause a global transaction to fail?
In the SpringBoot project, we often use @ControllerAdvice to construct ExceptionHandler for handling various exceptions. Sometimes global transactions cannot be rolled back because of this. Why?
Taking the ApiExceptionHandler in this article as an example,
First let’s look at the execution of TM. We mentioned above that the actual global transaction processing, such as enabling global transactions, committing, and rolling back, is implemented in the Execute method of TransactionalTemplate, so to be able to roll back, You must ensure that business processing business.execute() throws an exception.
try {
// Do Your Business
rs = business.execute();
} catch (Throwable ex) {
// 3. The needed business exception to rollback.
completeTransactionAfterThrowing(txInfo, tx, ex);
throw ex;
}
Copy the code
The sample code in this article is an API that uses RestTemplate to call other services. Specific error handling is org. Springframework. Web. Client. Complete DefaultResponseErrorHandler.
public void debit(String userId, BigDecimal orderMoney) {
String url = "Http://127.0.0.1:8083? userId=" + userId + "&orderMoney=" + orderMoney;
try {
restTemplate.getForEntity(url, Void.class);
} catch (Exception e) {
log.error("debit url {} ,error:", url, e);
throw newRuntimeException(); }}Copy the code
In DefaultResponseErrorHandler hasError method whether the returned Response is wrong, if you have the wrong will call handleError method. If the STATUS of the HTTP response is 4xx or 5XX, it is judged to have an error and an exception is thrown in the handleError method.
public boolean isError(a) {
return (is4xxClientError() || is5xxServerError());
}
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
String statusText = response.getStatusText();
HttpHeaders headers = response.getHeaders();
byte[] body = getResponseBody(response);
Charset charset = getCharset(response);
String message = getErrorMessage(statusCode.value(), statusText, body, charset);
switch (statusCode.series()) {
case CLIENT_ERROR:
throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
case SERVER_ERROR:
throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
default:
throw newUnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset); }}Copy the code
Therefore, if the RestTemplate is used to call the downstream API when the downstream service needs Rollback, make sure that the HTTP status returned is 4xx or 5XX. If you call the API in other ways, you also need to ensure that the error message is fed back to the TM side and an exception is thrown on the TM side.
reference
Seata. IO/useful – cn/docs /…
Seata. IO/useful – cn/blog /…
Mp.weixin.qq.com/s/Pypkm5C9a…
Chenjiayang. Me / 2019/06/29 /…