支付核心系统设计:Airbnb的分布式事务方案简介( 三 )

}, // 2. RPC (isRetry, paymentRequest) -> { return executePayment(paymentRequest, isRetry); }, // 3. Post RPC - record response information to database (isRetry, paymentResponse) -> { return recordPaymentResponse(paymentResponse); });}这是源代码的简化版本:public <R extends Object, S extends Object, A extends IdempotencyRequest> Response process( String idempotencyKey, UriInfo uriInfo, SetupExecutable<A> preRpcExecutable, // Pre-RPC lambda ProcessExecutable<R, A> rpcExecutable, // RPC lambda PostProcessExecutable<R, S> postRpcExecutable) // Post-RPC lambda throws YourCustomException { try { // Find previous request (for retries), otherwise create IdempotencyRequest idempotencyRequest = createOrFindRequest(idempotencyKey, apiUri); Optional<Response> responseoptional = findIdempotencyResponse(idempotencyRequest); // Return the response for any deterministic end-states, such as // non-retryable errors and previously successful responses if (responseOptional.isPresent) { return responseOptional.get; } boolean isRetry = idempotencyRequest.isRetry; A requestObject = ; // STEP 1: Pre-RPC phase: // Typically used to create transaction and related sub-entities // Skipped if request is a retry if(!isRetry) { // Before a request is made to the external service, we record // the request and idempotency commit in a single DB transaction requestObject = dbTransactionManager.execute( tc -> { final A preRpcResource = preRpcExecutable.execute; updateIdempotencyResource(idempotencyKey, preRpcResource); return preRpcResource; }); } else { requestObject = findRequestObject(idempotencyRequest); } // STEP 2: RPC phase: // One or more network calls to the service. May include // additional idempotency logic in the case of a retry // Note: NO database transactions should exist in this executable R rpcResponse = rpcExecutable.execute(isRetry, requestObject); // STEP 3: Post-RPC phase: // Response is recorded and idempotency information is updated, // such as releasing the lease on the idempotency key. Again, // all in one single DB transaction S response = dbTransactionManager.execute( tc -> { final S postRpcResponse = postRpcExecutable.execute(isRetry, rpcResponse); updateIdempotencyResource(idempotencyKey, postRpcResponse); return postRpcResponse; }); return serializeResponse(response); } catch (Throwable exception) { // If CustomException, return error code and response based on // ‘retryable’ or ‘non-retryable’. Otherwise, classify as ‘retryable’ // and return a 500. }}我们没有实现嵌套数据库事务 , 而是将Orpheus和应用程序中的数据库指令组合成单个数据库事务 , 传递Java 闭包 。
开发人员必须预先考虑好 , 才能保代码的可读性和可维护性 。他们还需要始终如一地评估适当的依赖关系和数据传递 。现在需要将API调用重构为三个部分 , 这可能会限制开发人员编写代码的方式 。实际上 , 某些复杂的API调用实际上很难有效地分解为三步 。我们的服务实现了一个有限状态机 , 每次转换都是使用StatefulJ的幂等步骤 , 可以在API调用中安全地复用幂等调用 。
 
处理异常 - 重试还是不重试?使用像Orpheus这样的框架 , 服务器应该知道何时可以重试请求 , 而何时不行 。要做到这一点 , 应该以细致的处理异常 , 异常被分类为“可重试”或“不可重试”两大类 。这无疑为开发人员增加了一层复杂性 , 如果他们错误使用 , 就会产生副作用 。
例如 , 假设下游服务暂时宕机 , 经常被错误地标记为“不可重试” 。这样请求将无限期地“失败” , 并且后续重试请求将永远返回不可重试错误 。相反 , 如果异常被标记为“可重试”(而实际应该是“不可重试”且需要人工干预) , 则可能会发生双重付款 。


推荐阅读