文章阅读-7周-Avoiding Double Payments in a Distributed Payments System

本周在medium阅读一篇关于如何避免在分布式支付系统中重复支付。
文件主要介绍如何在分布式系统中,系统幂等性、数据一致且确定的方法。
阅读本文章让我了解到一些新概念和一些需要再次深入了解知识点。
Avoiding Double Payments in a Distributed Payments System

我选出文章重点的知识点说明

微服务保持数据一致性方法

读修复、写修复、异步修复
(本文没有详细介绍这三个概念)

什么是幂等

如果一个接口请求是幂等,则客户端发送相同请求得到返回结果也是一样的。换一句话说,就是发送数个相同请求只会造成相同效果

以下图片说明幂等性


image.png

流程说明:
1、客户支付10美元进行预订(ID:123)
2、支付失败重新发送(下游服务网络超时)
3、再次客户支付10美元进行预订(ID:123)
4、返回成功且只进行一次扣费
5、客户支付10美元进行预订(ID:123)。客户同时多次点击,重复请求。
6、返回成功但不扣费

问题描述

1、我们需要幂等可以配置灵活方案,不是一个针对特性产品。(可以应用于支付SOA服务)
2、SOA架构产品不断迭代,数据一致性影响范围广。
3、需要非常低延迟,所有一个分离且独立幂等服务不满足要求。更大问题是该服务也会遇到相同问题。
4、在SOA架构下,不需要程序员解决数据准确或一致性问题

方案解决

1、每个请求都必须携带幂等关键字,该关键字能够唯一表示这个交易
2、幂等信息表存在一个共享主数据中,被所有服务读写。(为了一致性)
3、可以通过使用Java lamdbas表达式,来使不同代码的数据库事务的原子性
(我有疑问,为什么lamdbas表达式能够确保原子性)
4、错误响应分为“可以重复”或“不可以重复”

让数据库提交数次尽量少

文章中将请求分为以下三个阶段(本文主要以RPC架构,可以深入了解RPC与REST区别)
Pre-RPC:支付请求信息记录到数据库
RPC:该请求被实时处理且接收到响应。这阶段需要进行一个或多个幂等运算。(如果要尝试重复,请先查询服务的状态)
Post-RPC:响应信息记录到数据库,包含成功信息以及错误请求是否重发。

为了保证数据准确,还需要两个规则:
1、Pre-RPC和Post-RPC没有网络交互。
2、RPC阶段没有数据库交互。

image.png

Java Lambdas 实现

public Response processPayment(InitiatePaymentRequest request, UriInfo uriInfo)
   throws YourCustomException {

 return orpheusManager.process(
     request.getIdempotencyKey(),
     uriInfo,
     // 1. Pre-RPC
     () -> {
       // Record payment request information from the request object
       PaymentRequestResource paymentRequestResource = recordPaymentRequest(request);
       return Optional.of(paymentRequestResource);
     },
     // 2. RPC
     (isRetry, paymentRequest) -> {
       return executePayment(paymentRequest, isRetry);
     },
     // 3. Post RPC - record response information to database
     (isRetry, processorPaymentResponse) -> {
       return recordPaymentResponse(processorPaymentResponse);
     });
}
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 = null;

   // 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.
 }
}

如何区分重发或不重发

image.png

重发:
1、在暂时时间内,期望后续重发生产不同返回结果
2、服务内部错误
3、数据库或网络连接问题
4、5XX Http 情况
不重复:
1、期望后续重发以相同问题失败
2、无效输入和请求
3、记录不见
4、请求不支持
5、4XX Http 情况

客户端

1、每个新请求携带一个唯一幂等关键字。用相同幂等关键字来重发请求。
2、在调用服务之前,将幂等关键字记录到数据库
3、处理成功的响应之后让关键字失效。
4、不允许重复请求过程中,请求元数据出现变化。
5、根据需求仔细设计和配置重发策略(使用指数退化或随机等待时间来避免雪崩问题)

如何选择一个幂等关键字

请求级:随机且唯一KEY值。(可以采用UUID,采用雪花算法保证唯一)
实体级:需要KEY值加入具体业务类型。例如支付请求(payment-1234),后续针对这个该笔请求退款(只允许一次),则为payment-1234-refund。

每个接口请求都有一个到期契约

由于多个用户或同个客户多次点击请求,可能导致相同请求。为了避免这个问题,需要在获取幂等关键字加入行级锁。给每个请求申请一个契约和许可。
请求如果没有响应且已经超过到期契约时间,才可以进行重发请求。此外该逻辑还可以避免恶性重试

记录响应信息

记录成功响应信息用于用户发送相同请求时,直接从数据库返回处理结果。

主从复制数据库

主从复制延迟导致重复支取


image.png

通过只在主库查询关键字避免重复支取


image.png
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容