什么是接⼝的幂等性以及如何实现接⼝幂等性
⽬录
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 接⼝超时重试
对于给第三⽅调⽤的接⼝,有可能会因为⽹络原因⽽调⽤超时失败,这时,⼀般在设计的时候会对接⼝调⽤加上超时/失败重试机制。如果第⼀次调⽤已经执⾏了⼀半业务逻辑时,发⽣了⽹络异常,这时,再次调⽤时就会因为脏数据的存在⽽出现调⽤异常。update是什么
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';
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论