幂等性和实现方法

如果一个操作重复执行多次,其效果(不考虑操作时间)和只执行一次是一样的,那么这个操作就叫做是幂等(idempotent)的。乍看起来,幂等操作似乎没什么用处,毕竟只有第一次执行有效。但如果在系统设计中考虑到“失败”场景的话,幂等操作是非常重要的。因为失败发生和感知失败发生是两件不同的事情。想象两个服务器进行通过网络进行通信。服务器A发送请求到服务器B,服务器B执行操作并将结果返回A。在理想的情况下,一切执行顺利。我们从服务器A的角度来看看发生了什么。首先服务器A发出一个请求,等待了一会后,服务A收到一个应答,里面的消息说明操作成功。现在我们引入失败场景。可能的失败有这么几种:

  1. 服务器A发送请求失败。
  2. 服务器B接收请求失败。
  3. 服务器B处理请求失败。
  4. 服务器B发送应答失败。
  5. 服务器A接收应答失败。

从服务器A的角度来看,除了第1种失败可以立刻感知外,其他的失败都存在无法被感知的可能。在一个更加实际的场景中,服务器A发送了请求,等待了一会,没有收到应答。服务器A无法判断操作是成功了还是失败了。它应该继续等待吗?还是要重新发送请求呢?选择继续等待,如果服务器B发生故障,服务器A将陷入永久停滞。选择重新发送请求,如果服务器B已经成功执行了操作(失败情形4、5),再次请求会引发重复操作。假设请求的操作是“支付100元”,重复操作的效果将是“支付200元”,这不是服务器A的真实意图。可如果这是一个幂等操作,无论重复几次效果都和执行一次相同,那么服务器A就可以放心的重发请求。这就是幂等操作的好处。幂等操作可以大大简化客户代码的失败处理逻辑,提高系统整体的稳定性。

要避免重复操作,必须能唯一识别操作,因此需要为操作分配唯一编号。服务器记录下唯一编号和操作执行结果。当收到具有相同编号的请求时,不再执行操作,将第一次操作的结果返回。

ca88d6e11f17459d919777db125c529b.png

 

idempotent1.png

Listing 1: 非幂等操作:客户代码

Operation operation = new Operation("do some thing");
OperationResult result = server.remoteExecute(operation);

Listing 2: 非幂等操作:服务代码

public OperationResult onRequest(Operation operation) {
  OperationResult result = doOperate(operation);
  return result;
}

7c3f4b95f08c4340a86075cce659570d.png

 

idempotent2.png

Listing 3: 幂等操作:客户代码

String operationNumber = server.allocateOperationNumber();
Operation operation = new Operation(operationNumber, "do some thing");
OperationResult result = server.remoteExecute(operation);

Listing 4: 幂等操作:服务代码

public OperationResult onRequest(Operation operation) {
  Optional<OperationResult> resultOpt = findOperationResult(operation.getOperationNumber());
  if (resultOpt.isPresent()) {
    return resultOpt.get();
  }

  OperationResult result = doOperate(operation);
  storeOperationResult(operation.getOperationNumber(), result);
  return result;
}

可以看到幂等操作分两步:分配编号、执行操作。需要说明一下,这里提到的幂等性是从业务状态变更的角度讲的。业务状态是类似用户名、账户余额这类具有业务意义的数据。操作编号本身没有业务意义。无论分配了多少个编号,业务状态都没有改变,因此分配编号行为被认为是幂等的。在执行操作这一步,有了唯一编号就可以确认操作的执行状态,保证只有首次请求时操作才会得到执行。后续请求只是在读取首次执行的结果,也没有改变业务状态,因此也是幂等的。

幂等操作多了一次请求,这会带来性能开销。一个改进方法是批量分配编号。每次客户端申请时,服务器返回多个编号,后续操作只需要一次网络请求。

500e35a555cb4bc29764f85947a72c18.png

 

idempotent3.png

另一个方法是客户端分配编号。常见的方法有两种,一是为每个客户端分配唯一的客户端编号,由客户端自行分配本地唯一编号。这样采用一个两层结构(客户端编号,客户端本地唯一编号)作为操作编号。二是采用全局唯一编号,如SnowFlakeID。

819ef769a4f94f10b60e2b045c6f1604.png

 

idempotent4.png

对于单独部署的单线程服务,上面的设计已经够用了。对于多线程或分布式服务,还有一些细节需要考虑。首先是全局唯一ID生成,这已经有成熟的方案了。其次是要协调操作的执行,避免多个节点并行执行。常用的方法有:

  • 锁。通过全局锁保证操作不会并行执行。
  • 分片。某种类型的请求或某些客户端的请求始终向某个固定的服务器提交申请。
  • 向首领提交。集群选举出首领,客户端只向首领提交请求。首领可以选择自己执行操作或者分派给其他节点执行。

修改记录

  • 2020年12月08日 新建文档。
  • 2023年08月21日 增加服务代码示例;增加时序图;修改部分文字。

Date: 2020年12月8日

Created: 2023-08-21 周一 22:46

 

幂等性指的是对同一个接口多次调用,结果是一样的,不会产生副作用。在实际场景中,比如网络不稳定、请求超时等情况下,可能会导致接口被重复调用,如果接口不具备幂等性,就有可能造成数据重复提交等问题。 为了保证接口幂等性,我们可以在接口中添加某些操作,比如在数据库中添加唯一约束、使用分布式锁等等。下面是一个使用 Spring Boot 和 Redis 实现接口幂等性的示例代码: ```java @RestController public class DemoController { @Autowired private RedisTemplate<String, String> redisTemplate; @PostMapping("/demo") public String demo(@RequestParam("id") String id) { String key = "demo:" + id; ValueOperations<String, String> opsForValue = redisTemplate.opsForValue(); Boolean absent = opsForValue.setIfAbsent(key, "true"); if (absent != null && absent) { // 执行业务逻辑 // ... redisTemplate.delete(key); return "success"; } return "fail"; } } ``` 在这个示例中,我们使用 Redis 来实现接口幂等性。当第一次请求接口时,我们使用 Redis 的 setIfAbsent 方法来设置一个键值对,如果设置成功,说明这个接口还没有被调用过,可以执行业务逻辑。执行完业务逻辑之后,我们再删除这个键值对,这样下次再请求同一个接口时,就不会重复执行业务逻辑了。 需要注意的是,接口幂等性实现不是一成不变的,具体实现方式需要根据业务场景进行调整和优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值