引言
在分布式系统中,幂等性是一种十分重要的设计原则。它确保了系统在面对重复请求时能够产生相同的结果,而不会引发意外的行为或者数据不一致的问题。在本文中,我们将深入探讨幂等性设计的重要性,并结合 Java 代码以及不同场景下的实现方式进行说明。
1. 什么是幂等性?
所谓幂等性设计,就是说,一次和多次请求某一个资源应该具有同样的副作用。用数学的语言来表达就是:f(x) = f(f(x))。
比如,求绝对值的函数,abs(x) = abs(abs(x))。
为什么我们需要这样的操作?说白了,就是在我们把系统解耦隔离后,服务间的调用可能会有三个状态,一个是成功(Success),一个是失败(Failed),一个是超时(Timeout)。前两者都是明确的状态,而超时则是完全不知道是什么状态。
比如,超时原因是网络传输丢包的问题,可能是请求时就没有请求到,也有可能是请求到了,返回结果时没有正常返回等等情况。于是我们完全不知道下游系统是否收到了请求,而收到了请求是否处理了,成功 / 失败的状态在返回时是否遇到了网络问题。总之,请求方完全不知道是怎么回事。
举几个例子:
- 订单创建接口,第一次调用超时了,然后调用方重试了一次。是否会多创建一笔订单?
- 订单创建时,我们需要去扣减库存,这时接口发生了超时,调用方重试了一次。是否会多扣一次库存?
- 当这笔订单开始支付,在支付请求发出之后,在服务端发生了扣钱操作,接口响应超时了,调用方重试了一次。是否会多扣一次钱?
因为系统超时,而调用方重试一下,会给我们的系统带来不一致的副作用。
在这种情况下,一般有两种处理方式。
- 一种是需要下游系统提供相应的查询接口。上游系统在 timeout 后去查询一下。如果查到了,就表明已经做了,成功了就不用做了,失败了就走失败流程。
- 另一种是通过幂等性的方式。也就是说,把这个查询操作交给下游系统,我上游系统只管重试,下游系统保证一次和多次的请求结果是一样的。
对于第一种方式,需要对方提供一个查询接口来做配合。而第二种方式则需要下游的系统提供支持幂等性的交易接口。
2. 幂等性的重要性
- 数据一致性: 幂等性设计能够保证系统的数据一致性,即使同样的请求被多次发送,系统也能够保持相同的状态。
- 避免副作用: 有些操作可能会对系统产生副作用,如扣款、发送消息等,如果没有幂等性保证,重复请求可能会导致意外的结果,比如多次扣款或重复发送消息。
- 提高可用性: 在分布式系统中,网络通信可能会因为各种原因失败,而幂等性设计能够保证即使请求被重复发送,系统也能够正确处理,提高了系统的可用性。
对于第一种方式,需要对方提供一个查询接口来做配合。而第二种方式则需要下游的系统提供支持幂等性的交易接口。 下面就说一下幂等性的集中实现方式。
3. 幂等性的实现方式
1. 唯一标识符(IDempotent Key)
在每次请求中添加一个唯一标识符,服务器端根据该标识符判断是否已经处理过该请求。一般使用 UUID 或者类似的全局唯一标识符来实现。
public class IdempotentKeyGenerator {
public String generateKey() {
// 生成唯一标识符,例如 UUID
return UUID.randomUUID().toString();
}
}
2. Token 检查
客户端在发送请求时,带上一个 Token,服务器端根据 Token 判断是否已经处理过该请求。
public class IdempotentTokenManager {
private Set<String> processedTokens = new HashSet<>();
public synchronized boolean isTokenProcessed(String token) {
if (processedTokens.contains(token)) {
return true;
} else {
processedTokens.add(token);
return false;
}
}
}
3. 幂等性操作
在具体的业务逻辑中,设计幂等性操作,确保即使同样的请求被多次执行,结果也是一致的。
public class IdempotentOperationService {
private IdempotentTokenManager tokenManager;
public IdempotentOperationService(IdempotentTokenManager tokenManager) {
this.tokenManager = tokenManager;
}
public void processRequest(String token, Request request) {
if (tokenManager.isTokenProcessed(token)) {
// 请求已处理,直接返回结果
return;
}
// 执行业务逻辑
// ...
// 标记 Token 为已处理
tokenManager.markTokenAsProcessed(token);
}
}
4. 不同场景下的实现方式
- 支付场景: 对于支付请求,可以使用唯一订单号作为幂等性的标识符,确保同一订单只能被处理一次。
- 消息发送场景: 对于消息发送,可以使用消息 ID 作为幂等性的标识符,确保同一消息只能被发送一次。
- Web 表单提交场景: 通过表单 Token 或者防重放 Token 来实现。每次用户打开表单页面时,服务器端生成一个唯一的 Token 并返回给客户端,客户端在提交表单时携带该 Token,服务器端校验 Token 的有效性,确保表单只能被提交一次。
- 缓存更新场景: 在缓存更新场景下,当多个请求同时修改同一个缓存数据时,需要确保缓存数据的一致性。一种常见的实现方式是使用版本号(Versioning)来实现幂等性。每次修改缓存数据时,都将版本号一并更新,并在更新时比较当前版本号与请求中携带的版本号是否一致,从而确保同一请求只能更新一次缓存数据。
- 文件上传场景: 在文件上传场景下,用户可能会重复上传同一个文件,因此需要确保文件上传的幂等性。一种常见的实现方式是通过文件的哈希值来实现幂等性。服务器端在接收到文件上传请求后,首先计算文件的哈希值,并检查是否已经存在相同哈希值的文件,如果存在则直接返回已存在的文件信息,避免重复存储相同的文件。
- 并发调用接口场景: 在并发调用接口场景下,多个客户端同时调用同一个接口,可能会导致接口被重复调用。一种常见的实现方式是通过接口调用日志或者消息队列来实现幂等性。每次接口被调用时,记录下接口调用的唯一标识符,并在处理接口请求时检查该标识符是否已经存在于系统中,如果存在则直接返回已处理的结果,避免重复处理同一请求。
结语
幂等性设计在分布式系统中具有重要的意义,能够保证系统的数据一致性、避免副作用以及提高系统的可用性。通过合理的设计和实现,可以有效地确保系统在面对重复请求时能够产生相同的结果,从而提升系统的稳定性和可靠性。