如何理解编程接口的幂等性?

幂等性是编程中一个很重要的概念。特别是我们在设计编程接口时,往往需要考虑接口是否满足幂等性。那么,什么是幂等性呢?幂等性具体应用于哪些场景?实现幂等性有哪些可行的方案?本文一一为您揭秘。

什么是幂等性

幂等原本是一个数学概念,用数学公式表示为:

1
f(f(x)) = f(x)

引入到计算机编程领域,幂等性是指客户端对服务端接口调用一次和调用多次,对服务端状态的影响是相同的。对于HTTP请求,一次请求和多次请求,如果对被请求资源本身的影响完全相同,则可认为请求是幂等的。同样地,对于一个微服务接口,如果多次调用和一次调用的效果一样,则认为这个接口是幂等的。
我们用幂等性的上述定义来检查数据库的增删改查。显然,查询和删除是幂等的。新增和更新操作,则不是幂等的。举反例如下:

  • 新增客户记录时,每执行一次insert操作,都会在数据库表中添加一条记录。
  • 更新文章浏览量时,需要在原有浏览量数值上执行+1操作,每执行一次,浏览量数值都会随之改变。

幂等是服务端对外的一种承诺,承诺只要调用服务端成功,无论客户端调用多少次,都不用担心因重复调用打乱服务端的状态。

什么场景下需要幂等性

  1. 不允许重复提交时。 比如电商系统下订单时,用户重复点击下单按钮,后台应避免重复下单、重复扣款。
  2. 客户端引入失败重试机制时。在某些情况下,服务端执行操作成功而未来得及把操作结果返回给客户端,客户端误认为服务端操作失败,自动发起重试。

如何实现幂等性

实现幂等性,总体思路就是先判断再插入/更新。也就是check if exist then insertcheck if not updated then update类似的操作。这类操作往往不是原子的,需要加锁。具体有以下几种方案:

  1. 使用唯一索引
    适用于业务上要求只存在一条记录的数据表,比如用手机号来作为客户的唯一标识,则针对手机号建立唯一索引,这样在插入相同手机号的客户记录时,数据库就会报错并回滚插入操作,从而保证幂等性。

  2. token校验
    借助外部系统存储的token来判断是否重复提交。具体做法是,服务端先生成一个token保存下来,然后发给客户端使用。客户端在提交时,请求中必须带上该token。服务端收到客户端请求后,在执行具体操作前,先校验客户端传入的token,看是否和服务端已存储的token一致。若一致则表明是第一次请求,可以开绿灯放行,并在操作完成后删除或更新token。若不一致或未找到,则认为是重复提交,不予放行。我们常见的csrf_token就是为防止表单重复提交而使用的。

  3. 悲观锁
    悲观锁一般配合事务一起使用,用于记录更新频繁的场景。在更新某条记录前,需要先使用select ... where id=111 for update语法来对记录加锁,然后再执行更新操作。

  4. 乐观锁
    乐观锁只需要在更新数据的瞬间锁表,往往比悲观锁更高效。一般用增加记录版本号的方式来实现乐观锁。比如下面这个微博计数表:

weibo_idrepost_countversion
1001374

每次转发,都需要将转发数+1。这里我们用version字段来保存记录版本号。更新时,为保证幂等性,需执行如下SQL:

1
update t_weibo_count set repost_count=repost_count+1, version=version+1 where id=1001 and version=4

这样,如果是第一次请求,version匹配,更新成功,version+1。而后续的请求,无论执行多少次,因为version在第一次请求后已经+1,和SQL中的version对不上,因此更新不会被执行。

  1. 分布式锁
    分布式锁可以变相地认为是一种token校验机制。业务系统在插入或更新数据前,需要先获得锁。操作完成后,需要释放锁。在锁定期间,其他客户端只能等待。分布式锁一般用redis和zookeeper实现。

  2. 有限状态机
    有些领域对象比如订单,它的状态较为固定,转化途径也比较确定。我们在构造订单更新SQL时,可以带上订单状态字段,类似上面乐观锁的情况。若订单状态不匹配,则认为是重复操作,不予执行。

总结

幂等性规定多次操作和一次操作对系统状态的影响是想同的。幂等性同时也是接口对外部调用方的一种承诺。在接口和系统设计中,我们需要充分衡量某个接口是否需要实现成幂等性的。实现幂等性的主要思路就是先判断再插入/更新,可通过判断唯一索引、单次有效token、是否有锁、状态是否匹配等方式来识别并无视重复操作,达到幂等性的要求。