How to do interface idempotence at work?

foreword

Hello everyone, I am a router with no way .

With the development of the Internet, Web API has become an important part of modern applications, which allows communication and data exchange between different applications.

So today I will talk about some content about the idempotence of interfaces in Web API, and I hope it will be helpful to everyone.

background

In Web API, interface idempotence is an important concept, or a mechanism that can ensure that the same request will not have any negative impact on the system when it is executed multiple times.

In web applications, due to factors such as network delays and request retries, the same request may be executed multiple times. If the interface is not idempotent, such repeated requests may lead to inconsistencies in the system state, repeated submission of data, and other issues.

Therefore, interface idempotence has become an important consideration in Web API design.

Usage Scenarios of Interface Idempotency

In normal business development, we often encounter scenarios where interfaces need to be idempotent, but idempotence is often one of the most overlooked points in development. Improper or lack of idempotent implementation can lead to serious system failures , such as: repeated orders, repeated shipments, repeated deduction of inventory , etc.

Interface idempotence is very important in many scenarios. Here are some common usage scenarios :

  • Repeated submission of the front-end form : After the user completes the form submission, many times the user will not respond to the user's successful submission in time due to network fluctuations, causing the user to think that the submission has not been successful, and then keep clicking the submit button. At this time, repeated form submission requests will occur.
  • Payment system : In the payment system, repeated payment requests may cause the user's account to be debited repeatedly. Therefore, the payment interface must be idempotent to ensure that when the same request is executed multiple times, it will not have any impact on the user's account.
  • 订单系统:在订单系统中,重复的订单请求可能会导致系统中出现重复的订单。因此,订单接口必须具备幂等性,以确保同一个请求多次执行时,不会对系统中的订单数据造成任何影响。
  • 数据库操作:在数据库操作中,重复的插入、更新、删除请求可能会导致数据的重复提交或丢失。因此,数据库接口必须具备幂等性,以确保同一个请求多次执行时,不会对数据库中的数据造成任何影响。
  • 用户恶意刷单:例如在实现用户投票这种功能时,如果用户针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,这样会使投票结果与事实严重不符。
  • 接口超时重复提交: 很多时候 HTTP 客户端工具(如 OpenFeign)都默认开启超时重试的机制,尤其是第三方调用接口时候,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。
  • 消息进行重复消费:当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。

Restful API 接口的幂等性是怎么样的

Restful API 中,接口幂等性是一种非常重要的概念。Restful API 的幂等性可以通过 HTTP 方法来实现。

根据 HTTP 规范,GETHEAD 方法是幂等的,因为它们只是读取资源,不会对资源进行修改。

POSTPUTDELETE 方法则不是幂等的,因为它们会对资源进行修改。

方法 是否幂等 描述
GET GET 方法用于获取资源,其一般不会对系统资源进行改变,所以是幂等的
HEAD Head 与 get 请求类似,返回的响应中没有具体内容,用于获取报头,所以也是幂等的
POST × POST 方法一般用于创建新的资源。其每次执行都会新增数据,所以不是幂等的
PUT × PUT 方法一般用于修改资源,所以也是非幂等的
DELETE × DELETE 方法一般用户删除资源,所以也是非幂等的

幂等的不足之处

虽然接口幂等性可以确保同一个请求多次执行时,不会对系统造成任何负面影响,但是它也存在一些不足之处。

以下是一些常见的问题:

  • 增加了额外的控制幂等业务逻辑,复杂了业务逻辑处理
  • 把本可以并行执行的操作变成了串行执行,降低了执行效率
  • 实现复杂:实现接口幂等性可能需要使用一些复杂的技术,如分布式锁、乐观锁等,这可能会增加系统的复杂度
  • 性能影响:实现接口幂等性可能会对系统的性能产生影响,如分布式锁可能会降低系统的吞吐量
  • 业务复杂度:有些业务场景下,可能无法实现接口幂等性,如在某些业务场景下,重复的请求可能会对系统产生影响,这时就需要在业务层面上进行处理

幂等实现的关键点

仔细分析幂等的定义,发现幂等在实现上关注的重点是辨别重复请求和重复请求对系统不会多次造成不良影响。

那如何判断相同请求呢?

  1. 请求方生成唯一请求 Id,服务提供方通过请求 Id 辨别请求是否重复,如果请求 Id 相同则判定为重复请求;
  2. 服务提供方根据请求参数经过一系列的 Hash 算法生成对应的 Hash 值,若 Hash 值相同则判定为重复请求;

实现接口幂等性需要注意以下几个关键点:

  • 确定请求的唯一性:需要确定每个请求的唯一性,可以使用请求头、请求参数等方式来标识每个请求的唯一性。
  • 避免重复执行:需要避免同一个请求多次执行的情况发生,可以使用分布式锁、乐观锁等方式来实现。
  • 处理异常情况:需要处理异常情况,如网络异常、系统故障等情况,可以使用重试机制来处理异常情况。
  • 记录请求日志:需要记录每个请求的日志,以便于进行排查和追踪。

幂等实现的方式

实现接口幂等性的方式有很多,以下是一些常见的方式。

去重表

利用数据库的特性来实现幂等。通常是在表上构建一个唯一索引,那么只要某一个数据构建完毕,后面再次操作也无法成功写入。

常见的业务就是博客系统点赞功能,一个用户对一个博文点赞后,就把用户 id 与 博文 id 绑定,后续该用户点赞同一个博文就无法插入了。

或是在金融系统中,给用户创建金融账户,一个用户肯定不能有多个账户,就在账户表中增加唯一索引来存储用户 id,这样即使重复操作用户也只能拥有一个账户。

状态标识

状态标识是很常见的幂等设计方式,主要思路就是通过状态标识的变更,保证业务中每个流程只会在对应的状态下执行,如果标识已经进入下一个状态,这时候来了上一个状态的操作就不允许变更状态,保证了业务的幂等性。

状态标识经常用在业务流程较长,修改数据较多的场景里。最经典的例子就是订单系统,假如一个订单要经历 创建订单 -> 订单支付/取消-> 发货-> 确认收货->关闭订单 这几个步骤。

那么就有可能一笔待支付的订单去支付,需要去账户里扣除对应的余额,消耗对应的优惠卷,但是由于支付完成后网络等原因返回了错误信息,这时候就会重试再次去进行账户余额扣减步骤造成数据错误。

所以为了保证整个订单流程的幂等性,可以在订单信息中增加一个状态标识,一旦完成了一个步骤就修改对应的状态标识。

比如订单支付成功后,就把订单标识为修改为支付完成待发货,现在再次调用订单支付或者取消接口,会先判断订单状态标识,如果是已经支付过或者取消订单,就不会再次支付了。

Token 机制(幂等标识)

Token 机制应该是适用范围最广泛的一种幂等设计方案了,具体实现方式也很多样化。但是核心思想就是每次操作都生成一个唯一 Token 凭证,服务器通过这个唯一凭证保证同样的操作不会被执行两次。

这个 Token 除了字面形式上的唯一字符串,也可以是多个标志的组合(比如上面提到的状态标志),甚至可以是时间段标识等等。

换句话说,幂等标识可以在请求头或响应头中添加一个唯一的标识符,来标识每个请求的唯一性。

举个例子,比如下单,这是一个典型的 Post 新增操作,要怎样防止用户多次点击提交导致产生多个同样的订单呢。可以让用户提交的时候带一个唯一 Token,服务器只要判断该 Token 存在了就不允许提交(提示用户已重复下单),便能保证幂等性。

上面这个例子比较容易理解,但是业务比较简单。由于 Token 机制适用较广,所以其设计中要注意的要求也会根据业务不同而不同。

那么 Token 在何时生成,怎么生成?

这是该机制的核心,就拿下单服务来说,如果在用户提交订单的时候才生成 Token,那用户每次点提交都会生成新的 Token 然后都能提交成功,就不是幂等的了。必须在用户提交内容之前,比如进入 checkout 页的时候生成 Token,用户在提交的时候内容带着 Token 一起提交,对于同一个页面无论用户提交多少次,就至多能成功一次。

所以 Token 生成的时机必须保证能够使该操作具多次执行都是相同的效果才行。使用 Token 机制就要求开发者对业务流程有较好的理解。

案例说明

下面以“数据库乐观锁 + 幂等性 + Go 伪代码”的案例来说明。

在数据库中,乐观锁是通过在数据表中增加一个版本号(或者时间戳)字段来实现的。

在更新数据时,先查询当前数据的版本号,然后将要更新的数据的版本号设置为当前版本号+1,然后执行更新操作。如果更新成功,则说明当前数据没有被其他线程修改,否则说明当前数据已经被其他线程修改过,更新失败。

乐观锁的实现可以很好地解决并发更新数据时的冲突问题,但是在某些情况下,可能会出现幂等性问题。例如,在某个接口中,多个请求同时对同一条数据进行更新,如果使用乐观锁来实现幂等,那么可能会导致多次更新操作,最终数据的结果可能并不是我们期望的结果。

为了解决这个问题,我们可以在接口层面增加一个幂等性校验,通过校验请求的唯一标识符(如请求 ID)来判断当前请求是否已经处理过。如果当前请求已经处理过,则直接返回结果,否则执行更新操作,并将请求的唯一标识符记录到数据库中,以便下次校验。这样就可以保证同一个请求只会被处理一次,从而实现幂等性。

下面是一个用 Golang 伪代码实现乐观锁和幂等性的示例:

// 定义数据结构
type User struct {
    ID      int
    Name    string
    Version int
}

// 更新用户信息
func updateUser(db *sql.DB, user *User, requestId string) error {
    // 查询当前版本号
    var currentVersion int
    err := db.QueryRow("SELECT version FROM user WHERE id = ?", user.ID).Scan(&currentVersion)
    if err != nil {
        return err
    }

    // 设置新版本号
    user.Version = currentVersion + 1

    // 执行更新操作
    result, err := db.Exec("UPDATE user SET name = ?, version = ? WHERE id = ? AND version = ?", user.Name, user.Version, user.ID, currentVersion)
    if err != nil {
        return err
    }

    // 判断更新是否成功
    rowsAffected, err := result.RowsAffected()
    if err != nil {
        return err
    }
    if rowsAffected == 0 {
        return errors.New("update failed")
    }

    // 记录请求ID,用于幂等性校验
    _, err = db.Exec("INSERT INTO request_log(request_id) VALUES(?)", requestId)
    if err != nil {
        return err
    }

    return nil
}

// 幂等性校验
func checkRequestId(db *sql.DB, requestId string) bool {
    var count int
    err := db.QueryRow("SELECT COUNT(*) FROM request_log WHERE request_id = ?", requestId).Scan(&count)
    if err != nil {
        return false
    }
    return count > 0
}

// 处理请求
func handleRequest(db *sql.DB, user *User, requestId string) error {
    // 幂等性校验
    if checkRequestId(db, requestId) {
        return nil
    }

    // 更新用户信息
    err := updateUser(db, user, requestId)
    if err != nil {
        return err
    }

    return nil
}

在这个示例中,我们定义了一个 User 结构体,其中包含了用户的 ID、姓名和版本号。

在更新用户信息时,我们首先查询当前版本号,然后将要更新的数据的版本号设置为当前版本号+1,然后执行更新操作。如果更新成功,则说明当前数据没有被其他线程修改,否则说明当前数据已经被其他线程修改过,更新失败。

为了保证幂等性,我们在 handleRequest()函数中增加了一个幂等性校验,通过查询请求日志表来判断当前请求是否已经处理过。

如果当前请求已经处理过,则直接返回结果,否则执行更新操作,并将请求的唯一标识符记录到数据库中,以便下次校验。这样就可以保证同一个请求只会被处理一次,从而实现幂等性。

方案比较

方案 优点 缺点
去重表 实现简单,易于理解和维护;可以避免重复提交和重复处理的问题 1)需要占用额外的存储空间;2) 只能用于插入和删除操作;3)只能存在于唯一键场景
状态标识 实现简单,查询效率高 1)只适用于更新操作;2)表中需要增加额外的状态标识
Token 机制 实现相对复杂,但安全性高,可以避免重放攻击和恶意请求 1)需要生成唯一 Token;2)获取 Token 可能需要与服务提供方交互;3)需要借助第三方存储(比如 Redis)

综上所述,选择哪种幂等实现方案取决于具体的业务需求和实现环境。

如果对存储空间和查询效率要求较高,可以选择状态标识;如果对安全性要求较高,可以选择 Token 机制;如果对实现简单和易于维护要求较高,可以选择去重表。

总结

总之,接口幂等性是 Web API 设计中的重要考虑因素,可以确保同一个请求多次执行时,不会对系统造成任何负面影响。

实现接口幂等性需要注意请求的唯一性、重复执行的处理、异常情况的处理以及请求日志的记录等关键点。

通常可以使用去重表、状态标识、token 机制等方式来实现接口幂等性。

Guess you like

Origin juejin.im/post/7245184987531608124