接口的幂等设计

接口幂等

什么是接口幂等?就是一个接口,被重复调用多次,却能够保证对系统内部产生的影响是一致的,也就是调用多次和调用一次,数据的变化是一样的,是相同的,不会因为调用多次而出现任何数据问题。分布式系统中,接口幂等性是系统可行性论证的第一个步骤。很多地方需要把接口设计成幂等。

思路基本上是3种:

1 当第N(N>1)次请求过来时,系统要能知道,这个业务我们已经处理过了,相同的请求我们忽略掉就好了
2 当第N(N>1)次请求过来时,不管三七二十一,执行执行之,底层的数据接口层面保证其幂等就好了
3 从源头上避免请求重复提交。当然,这个有一定的限制。对于用户重复点击,那么容易避免,代理端可以进行各种过滤,去重。但对于mq的情况等,可能无法避免。

虽然概念上很接近,我们也很容易混为一谈,但服务接口的幂等和数据接口的幂等 ,细分开来还是有所不同的。 服务接口(我们的service层)可能包括了 对数据的操作,对文件的操作,对网络的操作,对cpu、内存的计算,还有对其他服务的操作; 而数据接口(我们的dao层)常常限于对数据库表的CRUD(这里不讨论广义的“数据”的定义,而是内存、缓存、文件、数据库分开讨论),及其复合操作

数据接口层面的幂等设计

所有的数据接口都可以归结为增删改查四大类;当然,下面我们对这四大类接口进行分析;

查询和删除
查询和删除业务,天然的具有幂等的特性;

1. 查询
在数据不变的情况下,查询一次和查询多次,查询结果是一样的;

2. 删除
删除一次和多次删除的结果都是把数据删除;(如果第二次删除返回0 rows affected之类的,那么忽略即可)

3. 新增
我们可以把关键的 业务id 设置成唯一索引,这样,第二次会失败,唯一索引约束错误的话,调用方忽略即可。否则就出现了多条数据,一条正确的,其余的是脏数据。

主要就是通过业务的相关的字段组成的一个 唯一性的约束。执行消息处理之前可以先根据这个唯一约束是否存在,如果存在,说明已经执行过了,忽略即可,否则把这个唯一约束以某种方式 保存起来。大部分情况 都会存在 至少一个 业务的唯一性的约束, 比如用户的 邮箱不能重复吧。 

4. 更新
如果是幂等的更新操作,比如update table1 set f1 = v1,我们可以不用管。因为这些操作本身是幂等的。 否则我们可能需要对update 语句进行稍稍的改写,增加where 条件,也就是乐观锁的方式。比如 update table1 set f1 = v1 where f1 = v0 (v0 是初始值)。 这样第一次update会成功,后面的 就会失败。我们需要尽量的把那些非幂等的update sql改写。 但是这样,有一定限制就是我们需要 更多的参数,比如这个初始值。 而且,我们不能排除某些操作是 不能改写的, 比如给用户增加积分等等, 原始积分就是限制的参数, 如果不能提供,那么无法改写。

服务接口层面的幂等设计

查询业务

我们可以首先把数据加载到缓存,然后尽量保证一个高可用的缓存,同时在 新增、更新、删除的时候维护缓存。

新增业务
新增业务类接口,我们要解决如下两个问题
1. 同一个用户用同样的数据多次请求同一个接口(不管是什么原因多次提交,他应该只请求一次)
2. 不同用户的提交同样的数据请求同一个接口;
第一个问题可以通过防重复提交来解决;业务数据连同Token,一起提交给接口,同一个Token,只能被处理一次(这里要注意,只能被处理一次,应该改成只能被正确的处理一次,也就是说,我们应该缓存某次新增业务处理的结果,如果上一次请求时出现某些异常,比如数据库连接失败,用户再次提交的时候,我们应该放行用户的这次请求,当然有些异常就不需要放行了,比如提交的业务数据不对等);
第二个问题是无法解决的,一个开放的系统,不能杜绝两个不同的客户端(用户)同时请求;但是可以交给数据的最后防线,存储层;通过唯一索引或唯一组合索引可以防止新增数据存在脏数据 (当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可) ;

注意:
Token防重复提交,只需要网关这层控制即可;Token的处理机制,还需要缓存调用的处理结果,以判断是否需要放行后续的重试请求;

更新业务

系统中的大部分业务都可以归属到更新业务,比如禁用用户、电商秒杀等等,只要是有更新操作的,不管是不是还有其他的操作,都归结到更新业务;
更新业务接口,不仅需要有表单防重复提交的验证,还需要有下面这些更精细的控制,以防止高并发环境中出现脏读,幻读等引起错误的数据更新结果;
更新业务接口幂等性解决方案一般是通过各种层面的锁和CAS机制;

悲观锁
悲观锁,select for update,整个执行过程中锁定要操作的记录;

乐观锁
更新业务的接口,比如订单付款等,需要综合使用尽可能多的信息来逐步验证逐步减少直至杜绝重复消息重复处理的概率;基本思路是CAS(Compare And Set);
可以参考下面的两篇文章体会一下:
1. 《架构师之路-库存扣多了,到底怎么整》
2. 订单操作,利用订单编号和订单的状态机(序列号)

测试用例
通过下面的方法可以初步验证接口幂等性的健壮性:
1. 同一个请求,多次提交到同一台节点,多次提交到不同的节点
2. 同一个请求,同时到达同一个节点,同时到达到不同的节点
3. 有逻辑先后顺序的消息、请求乱序的处理,比如创建订单的请求和支付订单的请求,不能保证第一个请求先于第二个请求到达服务器;
--------------------- 摘抄至 https://blog.csdn.net/xichenguan/article/details/78085801-------------

消息消费流程的异常点

消息的消费确认流程中,任何一个环节都可能会出问题!

  • 方法:对于未确认的消息,采用按规则重新投递的方式进行处理。
  • 问题:消息的重复发送会导致业务处理接口出现重复调用的问题。

消息重复发送的原因

被动方应用接收到消息,业务处理完成后应用出问题,消息中间件不知道消息处理结果,会重新投递消息。
被动方应用接收到消息,业务处理完成后网络出问题,消息中间件收不到消息处理结果,会重新投递消息。
被动方应用接收到消息,业务处理时间过长,消息中间件因消息超时未确认,会再次投递消息。
被动方应用接收到消息,业务处理完成,消息中间件问题导致收不到消息处理结果,消息会重新投递。
被动方应用接收到消息,业务处理完成,消息中间件收到了消息处理结果,但由于消息存储故障导致消息没能成功确认,消息会再次投递。

怎么实现消费方的消息幂等

1 根据业务来实现幂等,前面已经说过。

2 增加消息表来实现幂等,主要就是说,如果无法通过业务信息判断是否已经消费过,那么我们通过mq的消息的id,存放到消息表,然后消费的时候先判断是否已经存在。通过消息ID,或生成一个唯一ID标记每一条消息,将消息处理成功和去重日志(也就是“消息表”)通过事物的形式写入去重表, 如果之前没有处理成功,那么去重日志肯定是没有记录的,那么就消费,否则就不消费。 如果mq 会删除 确定消费完了的数据,我们可以先通过消息ID 去mq peek一下是否存在,存在则表示还未消费,然后消费方进行处理,否则就忽略。

其实这两种方式道理都差不多,可以按照具体情况来做

有时候,我们还可以 通过mq本身的消息超时机制,比如根据业务需求给消息 ID 设置一个 TTL, 或者是直接用 Redis 等缓存机制来保证在合理的时间范围内不会重复消费

实现这些幂等是有成本的,我可以考虑业务情况,如果都每一个消费方的操作都做幂等设计, 有时候可能成本太高,得不偿失。我们需要权衡去重所花的代价决定是否需要实现幂等性,如:购物会员卡成功,向用户发送通知短信,发送一次或者多次影响不大。不做幂等性可以省掉写去重日志的操作。

参考:

https://blog.csdn.net/xichenguan/article/details/78085801 

https://blog.csdn.net/qq_27384769/article/details/79307340

原文地址:https://www.cnblogs.com/FlyAway2013/p/10126940.html