深入理解接口幂等性

一. 什么是接口幂等性

幂等性原本是数学上的概念,用在接口上就可以理解为:同一个接口,多次发出同一个请求,必须保证操作只执行一次。它是系统服务对外的一种承诺(注意不是一种实现),接口服务提供方承诺只要调用接口成功了,外部多次调用对系统的影响是一致的。

举一个最常见的例子,用户购买商品后支付扣款成功,但是此时网络发生了异常,导致返回结果失败。因为没收到返回结果,用户就会再次点击付款按钮,就会多付了一笔钱,一旦用户发现余额少了,开发人员就等着被祭天吧,这就是没有保证接口的幂等性。

需要强调一点是,声明为幂等的服务会认为调用方调用失败是常态,并且调用失败后必然会有重试。

现在我们已经知道了什么是接口幂等性,那又有哪些场景需要保证幂等性呢?

二. 需要保证幂等性的场景分析

以 SQL 为例,有下面三种场景,只有第三种场景需要开发人员使用策略来保证幂等性:

1. 场景一:查询

SELECT column1 FROM table1 WHERE column2 = 2

无论执行多少次都不会改变状态,是天然的幂等。

2. 场景二:常量赋值更新

UPDATE table1 SET column1 = 1 WHERE column2 = 2

无论执行成功多少次状态都是一致的,因此也是幂等操作。

3. 场景三:变量赋值更新

UPDATE table1 SET column1 = column1 + 1 WHERE column2 = 2

每次执行的结果都会发生变化,这种场景就不是幂等的。

三. 为什么需要实现幂等性

如果接口没有实现幂等性,遇到以下情况就会出现问题:

  • 前端重复提交表单: 在填写表格时,用户填写完成提交,很多时候会因网络波动没有及时返回成功响应,致使用户认为没有提交成功,然后一直点提交按钮,这时就会导致重复提交表单请求。
  • 用户恶意刷单: 在实现用户投票这种功能时,如果用户针对同一个人进行重复投票,会导致接口接收到用户重复提交的投票信息,会使投票结果与事实严重不符。
  • 接口超时重复提交: 很多时候 HTTP 客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时,为了处理网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。
  • 消息重复消费: 当使用 MQ 消息中间件时,如果消息中间件出现错误未及时提交消费信息,就会导致消息重复消费。

四. 引入幂等性后对系统的影响

幂等性是为了简化客户端逻辑处理,能防止重复提交,但却增加了服务端的逻辑复杂性和成本:

  • 把并行执行的功能改为串行执行,降低了执行效率;
  • 增加了额外控制幂等的业务逻辑,业务功能变得更加复杂;

所以在使用时,需要考虑引入幂等性的必要性,根据实际业务场景分析,除了业务上的特殊要求外,一般情况下不建议引入接口幂等性。

五. 如何实现接口幂等性

乐观锁

这里的乐观锁指的是用乐观锁的原理去实现,为数据字段增加一个 version 字段,当数据需要更新时,先去数据库里获取此时的 version 版本号:

SELECT version FROM tablename WHERE ...

更新数据时首先和版本号作对比,如果不相等说明已经有其他的请求去更新数据了,就会提示更新失败。

UPDATE tablename
SET count = count + 1,
 version = version + 1
WHERE
	version = #{version}

不过,乐观锁存在失效的情况,就是常说的 ABA 问题,不过如果 version 版本一直是自增的就不会出现这种情况,乐观锁主要用于读多写少的场景。

悲观锁

乐观锁可以实现的往往用悲观锁也能实现,是指在获取数据时进行加锁,当同时有多个重复请求时其他请求都无法进行操作。

SELECT
	*
FROM
	tablename
WHERE
	id = 1 FOR UPDATE

注意:id 字段一定是主键或者唯一索引,不然会锁表。

悲观锁一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用; 

数据库唯一性索引

利用数据库表单的特性来实现幂等,常用的一个思路是在表上构建唯一性索引,保证某一类数据一旦执行完毕,后续同样的请求再也无法成功写入。

以博客点赞为例,要想防止一个人重复点赞,可以设计一张去重表,将博客 id 与用户 id 绑定建立唯一索引,每当用户点赞时就往表中写入一条数据,这样重复点赞的数据就无法写入了。

分布式锁

去重表也可以使用分布式锁来代替,比如 Redis。

在分布式环境下,锁定全局唯一资源,使请求串行化,实际表现为互斥锁,防止重复,解决幂等。

Token 令牌

这种机制适用范围较广,有多种不同的实现方式。其核心思想是为每一次操作生成一个唯一性的凭证,也就是 Token。一个 Token 在操作的每一个阶段只有一次执行权,一旦执行成功就直接保存执行结果。

它主要针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用 Token 的机制实现防止重复提交。

简单的说就是调用方在调用接口的时候先向后端请求一个全局 ID(Token),请求的时候携带这个全局 ID 一起请求(最好将其放到 Headers 中),后端需要对这个 Token 作为 Key,用户信息作为 Value 到 Redis 中进行键值内容校验,如果 Key 存在且 Value 匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的 Key 或 Value 不匹配就返回重复执行的错误信息,以此来保证幂等操作。


 本篇博客参考了以下文章,感谢:

https://www.cnblogs.com/javalyy/p/8882144.html

https://zhuanlan.zhihu.com/p/359289667 

おすすめ

転載: blog.csdn.net/j1231230/article/details/119843895