本周在medium阅读一篇关于如何避免在分布式支付系统中重复支付。
文件主要介绍如何在分布式系统中,系统幂等性、数据一致且确定的方法。
阅读本文章让我了解到一些新概念和一些需要再次深入了解知识点。
Avoiding Double Payments in a Distributed Payments System
我选出文章重点的知识点说明
微服务保持数据一致性方法
读修复、写修复、异步修复
(本文没有详细介绍这三个概念)
什么是幂等
如果一个接口请求是幂等,则客户端发送相同请求得到返回结果也是一样的。换一句话说,就是发送数个相同请求只会造成相同效果
以下图片说明幂等性

流程说明:
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阶段没有数据库交互。

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.
}
}
如何区分重发或不重发

重发:
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。
每个接口请求都有一个到期契约
由于多个用户或同个客户多次点击请求,可能导致相同请求。为了避免这个问题,需要在获取幂等关键字加入行级锁。给每个请求申请一个契约和许可。
请求如果没有响应且已经超过到期契约时间,才可以进行重发请求。此外该逻辑还可以避免恶性重试
记录响应信息
记录成功响应信息用于用户发送相同请求时,直接从数据库返回处理结果。
主从复制数据库
主从复制延迟导致重复支取

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