1、什么是幂等?
根据百度百科解释:
“幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。”
例如,数学上,乘法下唯一两个幂等实数为0和1。在计算机中,http的get方法是幂等。
2、为什么需要幂等?
声明幂等的服务认为,外部调用者会存在多次调用的情况,为了防止外部多次调用对系统的数据状态发生多次改变,需要将服务设计为幂等。在接口调用时一般情况下都能正常返回信息不会重复提交,不过在遇见以下情况时可以就会出现问题,如:
1、重复提交表单: 在填写一些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。有时在实现用户投票这种功能时,如果用户针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,这样会使投票结果与事实严重不符。
2、接口超时重复提交:很多时候 HTTP 客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时候,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。例如,第一次请求接口超时了,请求方没能及时获取返回结果(此时有可能已经成功了),为了避免返回错误的结果,于是会对该请求重试几次,这样也会产生重复的数据。
3、消息进行重复消费: 当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。
在交易系统,支付系统这种重复提交造成的问题有尤其明显,比如:
1、用户在APP上连续点击了多次提交订单,后台应该只产生一个订单;
2、向支付系统发起支付请求,由于网络问题或系统BUG重发,支付系统应该只扣一次钱。
2.1幂等的不足
幂等是为了简化客户端逻辑处理,却增加了服务提供者的逻辑和成本,是否有必要,需要根据具体场景具体分析,因为幂等会带来另外两个问题:
1. 增加了额外控制幂等的业务逻辑,复杂化了业务功能;
2. 把并行执行的功能改为串行执行,降低了执行效率。
2.2幂等和防重
重复提交的情况和服务幂等的初衷是不同的。
重复提交是在第一次请求已经成功的情况下 ,人为地进行多次操作, 导致不满足幂等要求的服务多次改变状态。
幂等更多使用的情况是第一次请求因为某些情况,不如超时,而导致不知道结果或者请求失败的异常情况下,发起多次请求。
幂等的目的是请求多次确认第一次请求成功,不会因为多次请求而出现多次的状态变化
3、怎么做幂等?
幂等需要通过唯一的业务单号来保证,如果是相同的业务单号,认为是同一次业务。
使用唯一的业务单号确保,后面多次相同业务单号的处理逻辑和执行效果是一致的。
例如幂等实现示例-支付:
1、先查询订单是否支付过;
2、如果已经支付过,返回支付成功;
3、如果没有支付,则进行支付流程,修改订单的状态为已支付
在保证幂等的策略中,执行是分两步执行的,后面一步依赖上面一步的查询结果,这样就无法保证原子性。无法保证原子性在高并发的情况下会存在问题:第二次请求在第一次请求的下一步订单状态没有修改为"已支付状态"时进行。
为了解决这个问题,需要进行防重复提交策略 。
3.1、防重复提交策略
防重复提交策略是将查询和变更状态操作加锁,并将并行操作改为串行执行。具体方法有如下几种:
3.1.1、唯一索引
数据库唯一索引的实现主要是利用数据库中唯一索引约束的特性,一般来说唯一约束比较适用于“插入”时的幂等性,其能保证一张表中只能存在一条带该唯一索引的记录。
使用数据库唯一索引完成幂等性时需要注意的是,该索引一般来说并不是使用数据库中自增主键,而是使用业务单号(分布式ID)充当,这样才能能保证在分布式环境下 ID 的全局唯一性。
3.1.2、防重表
有时候表中并非所有的场景都不允许产生重复的数据,只有某些特定场景才不允许。这时候,直接在表中加唯一索引,显然是不太合适的。针对这种情况,我们可以通过建防重表来解决问题。
该表可以只包含两个字段:id 和 唯一索引,唯一索引可以是多个字段比如:name、code等组合起来的唯一标识,
需要特别注意的是:防重表和业务表必须在同一个数据库中,并且操作要在同一个事务中。
3.1.3、乐观锁(悲观锁)
数据库乐观锁方案一般只能适用于执行更新操作的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。
这样每次对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值。
如果竞争比较厉害,可以直接用悲观锁。
3.1.4、使用状态机
业务上的数据处理大多都是有状态的,比如交易订单。这个时候首选的方法是通过状态机的序列来判断某个更新行为是否已经做过了。不过这并不简单,我们需要非常小心的去看业务上的各种规则和限制,同时,除了更新状态之外,大多数情况下还会伴随着更新一些别的附加信息,我们还需要去检查这些附加信息是否如请求要求的那样更新过了。
需要特别注意的是:状态表和附加操作表必须在同一个数据库中,并且操作要在同一个事务中。
3.1.5、分布式锁
加唯一索引或者加防重表,本质是使用了数据库的分布式锁,也属于分布式锁的一种。但由于数据库分布式锁的性能不太好,我们可以改用:redis或zookeeper。
需要特别注意的是:分布式锁一定要设置一个合理的过期时间,如果设置过短,无法有效的防止重复请求。如果设置过长,可能会浪费redis的存储空间,需要根据实际业务情况而定。
3.1.6、token令牌
该方案跟之前的所有方案都有点不一样,需要两次请求才能完成一次业务操作。
简单的说就是调用方在调用接口的时候先向后端请求一个全局 ID(Token),请求的时候携带这个全局 ID 一起请求(Token 最好将其放到 Headers 中),后端需要对这个 Token 作为 Key,用户信息作为 Value 到 Redis 中进行键值内容校验,如果 Key 存在且 Value 匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的 Key 或 Value 不匹配就返回重复执行的错误信息,这样来保证幂等操作。
3.1.7、支付缓冲区
将订单的支付请求都快速地接收下来,是一个快速接收请求的缓冲管道。使用异步任务处理管道中的数据,过滤调掉重复的待支付的数据。
4、例子参考
4.1在底层接口无法保证幂等的情况下,也没有做防重复提交。
4.2使用redis分布式锁来做防重复策略,但是设置的过期时间不合理导致,用的是固定时间过期方法。redisson客户端的lock方法本身就支持看门狗功能,在客户端挂的情况下,默认30s也会释放锁。
见3.1.5
4.3状态表和操作表没有放在一个事务里面,此操作为update,insert,当update失败,但是insert成功导致。
见3.1.4
参考资料:
滑动验证页面
一口气说出四种幂等性解决方案,面试官露出了姨母笑~ - 掘金
接口的幂等性设计和防重保证,详细分析幂等性的几种实现方法-阿里云开发者社区