幂等性和实现方法

幂等性和实现方法 – tommwq.tech/blog

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

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

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

将非幂等操作封装成幂等操作,需要将操作分成两部分:为操作执行实例分配编号;通过编号提交操作。服务器B在收到操作提交请求时,首先检查操作是否已经执行。如果操作已经执行,将结果发送给服务器A。如果操作尚未执行,执行操作,并发送结果。

// 非幂等操作

OperationResult result = operation.execute();

// 幂等操作
class Operation {
    public void commit(String id) {
        OperationState state = getOperationState(id);
        if (state.isExecuted()) {
            return;
        }
        Operation operation = getOperation(id);
        operation.execute();
        state.setExecuted();
    }
}

Operation.register(operation);
String id = Operation.getLastRegisteredId();
Operation.commit(id);
OperationResult result = Operation.getOperationResult(id);

我们来看拆分出来的两个步骤:

  1. 为操作执行实例分配编号。
  2. 通过编号提交操作,当且仅当操作尚未执行时,执行该操作。

第一步为操作分配了一个编号,但没有执行操作,没有修改业务状态。因此从业务状态的变化来看,可以认为是幂等的。第二步显然是幂等的,因为只要操作成功执行,后续的请求不会再次执行操作。

对于单体单线程服务,上面的代码已经够用了。对于多线程或分布式服务,还有一些细节需要考虑。首先是全局唯一ID生成,这已经有成熟的方案了。其次,在判断操作是否已经执行时,需要通过全局锁保护,以保证多线程或分布式程序中的个节点的观察顺序。或者在提交操作结果之前,再次检查操作状态。如果发现其他线程/节点已经完成操作,丢弃本地操作结果。

猜你喜欢

转载自blog.csdn.net/tq1086/article/details/110919471