什么是接口的幂等性以及如何实现接口幂等性

接口幂等性是指一个操作无论执行一次还是多次,其结果始终一致,避免了因重复请求导致的副作用。在支付、订单处理等场景中尤为重要,防止用户重复扣款或创建重复记录。实现幂等性的方法包括唯一索引、乐观锁、悲观锁、CAS思想、分布式锁以及基于token+redis的机制。在设计接口时,应考虑幂等性以确保系统的稳定性和数据一致性。
摘要由CSDN通过智能技术生成

目录

1、接口调用存在的问题

2、什么是接口的幂等性

3、不做接口的幂等性会产生什么影响

4、什么情况下需要保证接口的幂等性

4.1 select:查询操作

4.2 insert:新增操作

4.3 delete:删除操作

4.3.1 绝对删除 具有幂等性

4.3.2 相对删除 不具有幂等性

4.4 update:更新操作

4.4.1 绝对更新 具有幂等性

4.4.2 相对更新 不具有幂等性

5、使用幂等的业务场景

5.1 前端重复提交

5.2 接口超时重试

5.3 MQ消息重复消费

6、幂等性的解决方案

6.1 唯一索引

6.2 乐观锁

6.3 悲观锁

6.4 CAS思想保证接口幂等性

6.5 分布式锁

6.6 基于token+redis机制实现(通用性强)

6.7 基于redis命令setnx实现

6.8 通过业务代码逻辑判断实现

99、参考

1、接口调用存在的问题

在大多数情况下,一个大系统都会拆分为多个微服务组成。也就是说,一个大系统的完整功能往往是由多个子系统的小功能构建而成的,而一个子系统服务往往会调用另外一个子系统提供出来的服务,而服务调用无非就是使用RPC接口通信,既然是通信,那么就有可能在服务器处理数据完毕后返回结果的时候挂掉,这个时候客户端发现已经过了很久 但还是没能从服务器端拿到正确的响应,那么,客户端就有可能会多次点击按钮以触发多次接口请求,那么,处理数据的结果是否要统一呢?答案是肯定的,尤其是在支付场景。

2、什么是接口的幂等性

接口幂的等性,就是指 用户对于同一操作发起的一次请求或者多次请求,其操作的结果都是一致的,不会因为进行多次请求而产生副作用。这里的副作用,可以认为在多次请求操作时,每一次请求对数据的状态都会产生影响。注意,这里并没有要求接口返回的结果是一致的,而是要求被数据的状态是一致的。例如:update order set moeny = 100 where orderId = 2029282312;  该操作无论执行多少次,被操作数据的状态都是一致的。

3、不做接口的幂等性会产生什么影响

支付场景:用户购买商品后,发起支付操作,支付系统处理支付成功后,由于网络原因没有及时返回操作成功的信息给用户,其实这个时候订单已经扣过款,相应的支付流水也都已经生成。这个时候,用户又点击支付操作,此时会进行第二次扣款,扣款成功后吧操作成功的信息返回给了用户。用户去查看支付订单和流水时 会发现自己支付了两次,完蛋了,该系统要被用户投诉了。这就是没有保证接口的幂等性而造成的不良后果。

4、什么情况下需要保证接口的幂等性

在【增删改查】4个SQL操作中,尤为需要注意的就是增加和修改操作。

4.1 select:查询操作

查询操作不会对数据产生副作用。查询一次或者查询多次,在数据不变的情况下,查询结果都是一样的,所以,select 操作是天然的幂等操作

4.2 insert:新增操作

新增操作在重复提交的场景下会出现幂等性问题,比如以上的支付场景。

insert into product_info (id, price);

上述 insert SQL,ID是自增主键,执行多次就会新增多条记录,对结果集产生了副作用,所以,insert 操作天然不具有幂等性。

4.3 delete:删除操作

删除操作可以分为两种:绝对删除和相对删除。其中,绝对删除不会对数据产生副作用,具有幂等性;相对删除会对数据产生副作用,不具有幂等性。

4.3.1 绝对删除 具有幂等性

delete from order where id = 3;

无论该SQL执行多少次,对结果集产生的效果都是一样的,只删除了一条数据,不会对数据产生副作用,所以,它具有幂等性。

4.3.2 相对删除 不具有幂等性

delete from order where id > 23;

该SQL每执行一次,对结果集产生的结果可能都不一样,同一操作执行多次对数据产生了副作用,所以,它不具有幂等性。

4.4 update:更新操作

更新操作可以分为两种:绝对更新和相对更新。其中,绝对更新不会对数据产生副作用,具有幂等性;相对更新会对数据产生副作用,不具有幂等性。

4.4.1 绝对更新 具有幂等性

update Goods set stock = 586 where goodId = 10;

无论该SQL执行多少次,对结果集产生的效果都是一样的,只更新了一条数据,不会对数据产生副作用,所以,它具有幂等性。

4.4.2 相对更新 不具有幂等性

update Goods set stock = stock + 1 where goodid = 10;

该SQL每执行一次,对结果集产生的结果都不一样,库存数量都会增加10,同一操作执行多次对数据产生了副作用,所以,它不具有幂等性。

5、使用幂等的业务场景

5.1 前端重复提交

用户注册、用户创建商品订单等操作,前端都会提交一些数据给后台服务,后台需要根据用户提交的数据在数据库中创建记录。如果用户不小心多点了几次,后端就收到了好几次提交,这时,就会在数据库中重复创建了多条记录。这就是接口没有幂等性带来的 bug。

5.2 接口超时重试

对于给第三方调用的接口,有可能会因为网络原因而调用超时失败,这时,一般在设计的时候会对接口调用加上超时/失败重试机制。如果第一次调用已经执行了一半业务逻辑时,发生了网络异常,这时,再次调用时就会因为脏数据的存在而出现调用异常。

5.3 MQ消息重复消费

在使用消息中间件来处理消息队列,且手动 ACK 确认消息被正常消费时,如果消费者突然断开连接,那么已经执行了一半的消息就会被重新放回队列。当消息被其他消费者重新消费时,如果没有幂等性,就会导致消息重复消费时结果异常,如数据库重复数据、数据库数据冲突、资源重复等。

6、幂等性的解决方案

6.1 唯一索引

使用唯一索引可以避免脏数据的 insert,当插入重复数据时数据库会抛出异常,保证了数据的唯一性。

6.2 乐观锁

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

select version from tableName where Id = 1;

更新数据时,首先和最新的版本号作比较,如果不相等,则说明已经有其他的请求去更新数据了,则本次提示更新会失败,让用户重试即可。

update tableName set count = count + 1, version = version + 1 where version = #{version};

6.3 悲观锁

乐观锁可以实现的,往往使用悲观锁也能实现:即在获取被操作数据的时候进行加锁。当同时有多个重复请求过来时,其他请求都会因无法获得被操作数据的锁而阻塞住,因此,其他请求都无法对被操作数据进行操作。

6.4 CAS思想保证接口幂等性

状态机制来实现接口幂等性(一个事务的状态是不可逆的)。

针对更新操作,例如 电商订单的支付状态:0=待支付,1=支付中,2=支付成功,3=支付失败。

update Orders set status = 1 where status = 0 and orderId = “201251487987”;
update Orders set status = 2 where status = 1 and orderId = “201251487987”;
update Orders set status = 3 where status = 1 and orderId = “201251487987”;

该SQL语句利用【订单状态的CAS】来保证该操作的幂等性。比如,要进行订单支付,先用CAS思想做更新订单状态的操作,然后再去做实际支付的操作:

(1)返回影响行数=1,则代表订单状态修改成功,可以继续执行后面的支付业务代码。

(2)返回影响行数=0,则代表订单状态修改失败,该订单已经不是待支付订单了,不可以继续执行后面的支付业务代码。(其实这里的解释有待商榷)。

注释:实际这里是利用CAS原理。

6.5 分布式锁

幂等的本质是分布式锁的问题,分布式锁正常可以通过 redis 或 zookeeper 来实现。

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

6.6 基于token+redis机制实现(通用性强)

token 机制的核心思想:是为每一次操作都生成一个唯一性的凭证,也就是 token。一个token 在操作的每一个阶段只有一次执行权,一旦执行成功,则保存执行结果并且删除该 token。对重复的请求,因为没有了先前的那个 token 而返回指定的同一个结果给客户端。

通过【 token+redis 机制】实现接口的幂等性,这是一种比较通用性的实现方法。示意图如下:

具体流程步骤:

(1)客户端会先发送一个请求去获取 token,服务端会生成一个全局唯一的 ID 作为 token,并且保存在 redis 中,同时会把这个 ID 返回给客户端。

(2)客户端第二次调用业务请求的时候,必须携带这个 token。

(3)服务端会校验这个 token:如果校验成功,则执行业务,并删除 redis 中的 token。如果校验失败,说明 redis 中已经没有了对应的 token,则表示是重复操作,直接返回指定的结果给客户端。

注意:对 redis 中是否存在 token 以及删除的代码逻辑建议用 Lua 脚本实现,以此来保证多个操作的原子性。全局唯一 ID 可以用百度的 uid-generator、美团的 Leaf 去生成。

6.7 基于redis命令setnx实现

(1)这种实现方式是基于Redis的一个命令 setnx 实现的。

(2)setnx key value:当且仅当 key 不存在时,将 key 的值设为 value,并返回 1。若给定的 key 已经存在,则 setnx 不做任何动作,并返回 0。注意:该命令在设置成功时返回 1,设置失败时返回 0。

(3)通过【 redis的命令 setnx 】实现接口的幂等性,示意图如下:

具体流程步骤:

(1)客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段 。

(2)将该字段以 setnx 的方式存入 redis 中,并根据业务设置相应的超时时间 timeout。

(3)如果设置成功,则证明这是第一次请求,则执行后续的业务逻辑。

(4)如果设置失败,则证明这不是第一次请求,已经执行过当前请求,直接返回即可。

6.8 通过业务代码逻辑判断实现

通过【业务代码逻辑判断】实现接口幂等性,只能针对一些满足判断的业务逻辑实现,具有一定局限性。比如:用户购买商品的订单系统与支付系统

订单系统负责记录用户的购买记录以及订单的流转状态(orderStatus)。支付系统用于付款,提供如下接口。订单系统与支付系统通过分布式交互。

boolean pay ( int accountid, BigDecimal amount );  // 用于付款,扣除用户余额

这种情况下,支付系统已经扣款,但是,因为网络原因,订单系统没有获取到支付系统返回的确切结果,因此,订单系统需要重试。

由上图可见,支付系统并没有做到接口的幂等性,订单系统第一次调用和第二次调用,用户分别被扣了两次钱,不符合幂等性原则(同一个订单,无论是调用了多少次,用户都只会扣款一次)。

如果需要支持幂等性,付款接口需要修改为以下接口:

boolean pay ( int orderId, int accountId, BigDecimal amount);

通过 orderId 来标定订单的唯一性,支付系统只要检测到该订单已经支付过,则第二次调用就不会扣款,而是会直接返回结果:

在不同的业务中,不同接口需要有不同的幂等性,特别是在分布式系统中,因为网络原因而未能得到确定的结果,往往需要支持接口做幂等性校验。

随着分布式系统及微服务的普及,因为网络原因而导致调用系统未能获取到确切结果而导致重试,这就需要被调用系统具有幂等性。

例如上文所阐述的支付系统,针对同一个订单保证支付的幂等性,一旦订单的支付状态确定之后,以后的操作都会返回相同的结果,对用户的扣款也只会有一次。

这种接口的幂等性,简化到数据层面的操作就是:

update userAmount set amount = amount - 'value', paystatus = 'paid' where orderId= 'orderid' and paystatus = 'unpay';

其中:value 是用户要减少的金额,paystatus 代表支付状态( paid 代表已经支付,unpay 代表未支付 ),orderid 是唯一的订单号。

在上文中提到的订单系统,订单具有自己的状态(orderStatus),订单状态存在一定的单向流转。

订单首先有:订单提交(0),订单付款中(1),订单付款成功(2),订单付款失败(3),简化之后:

(1)当 orderStatus = 1 时,其前置状态只能是 0,也就是说:将 orderStatus 由【 0 -> 1】是需要幂等性的,SQL如下:

update Order set orderStatus = 1 where OrderId = 'orderid' and orderStatus = 0;

(2)当 orderStatus = 0、1 两种状态时,对订单执行【 0 -> 1】的状态流转操作应该是具有幂等性的。这时候,需要在执行 update 操作之前检测 orderStatus 是否已经 = 1,如果已经 = 1,则直接返回 true 即可。

(3)当 orderStatus = 2 时,再进行订单状态【 0 -> 1】操作就无法成功。但是,幂等性是针对同一个请求的,也就是针对同一个 requestId 保持幂等,这时候再执行:

update Order set orderStatus = 1 where OrderId = 'orderid' and orderStatus = 0;

接口就会返回失败,系统没有产生修改,如果再发一次,requestId 还是相同的,对系统同样没有产生修改。

 

这几种实现幂等的方式其实都是大同小异的,类似的还有使用【状态机、悲观锁、乐观锁】的方式来实现,都是比较简单的。总之,当你去设计一个接口的时候,接口的幂等性都是首要考虑的问题,特别是当你负责设计转账、支付这种涉及到 money 的接口,要格外注意。

99、参考

(1)https://javayz.blog.csdn.net/article/details/109684180

(2)https://blog.csdn.net/qq_29978863/article/details/107739744

(3)https://www.cnblogs.com/huaixiaonian/p/9577567.html

 

分布式接口幂等性问题是指在分布式系统中,由于网络延迟、重试机制等原因,可能导致同一个请求被重复处理,从而产生重复的业务逻辑。为了解决这个问题,需要保证接口幂等性。 保证接口幂等性的方法有多种。一种常见的方法是使用唯一标识来标识每一次请求,比如订单id、支付流水号或者前端生成的唯一随机串。在每次请求之前,需要将唯一标识存放到数据库或者缓存中。后端服务在处理请求之前,需要先检查这个唯一标识是否存在,如果存在,则判定此次请求已经处理过,不需要进行重复处理。这样可以避免重复的业务逻辑。 在分布式场景中,由于负载均衡算法的原因,可能会导致同一个请求被多台机器处理。为了解决这个问题,可以使用分布式锁来保证只有一个机器能够处理该请求。另外,使用分布式事务也可以保证接口幂等性。 此外,还可以通过拦截器(AOP)和注解的方式实现一个通用的解决方案,避免每次请求都写重复的代码。在设计系统时,幂等性是一个需要首要考虑的问题,特别是在涉及到金融交易等关键业务的系统中。 综上所述,保证分布式接口幂等性可以通过使用唯一标识、分布式锁、分布式事务等方法来实现。这样可以避免重复的业务逻辑和数据不一致的问题。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *2* [分布式环境下接口幂等性浅析](https://blog.csdn.net/ice24for/article/details/86084613)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [分布式开发(二)---接口幂等性(防止重复提交)](https://blog.csdn.net/icanlove/article/details/117652662)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值