什么是接口幂等性
接口幂等性指的是一个接口在多次调用中的结果与调用一次的结果相同。
换句话说,无论对一个幂等性借口进行多少次重复调用,系统的状态都保持一致,不会因为多次调用而导致不同的结果
在Web开发中,由于重试机制或者网络不稳定等对接口的重复调用,如果接口是幂等的,系统可以更容易的处理重复请求
为什么需要接口幂等性
举个常见的例子在没有幂等的时候,网上支付场景,比如用户购买商品下单成功,跳转到支付页面,点击支付按钮进行付款,
假设系统在返回支付结果的时候出现了网络异常,但此时后台已经扣款成功,但用户没看到支付成功的结果
再次点击支付按钮,此时会进行第二次付款请求,返回结果成功了,用户查询余额就会发现扣了两次钱,流水记录也变成了两条
常见的重复请求的场景
-
前端表单的重复提交
类似于上面提到的支付场景,前端表单在提交的时候遇到网络波动,没有及时的对用户做出提交成功的响应
致使用户认为没有提交成功,然后一直点提交按钮,就会发生重复提交表单的请求
-
黑客恶意攻击
比如网上投票,黑客会针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息
这样会导致投票结果与事实严重不符
-
接口超时重试
现在http请求或者是rpc请求在实现的时候,都会添加超时重试的机制,为了防止网络波动超时造成的请求失败
就会进行重试,这样就会出现多次重复请求
-
消息重复消费
当使用MQ消息中间件的时候,如果Consumer消费超时或者Producer发送了消息,但由于网络原因未收到ACK
导致消息进行再次消费或者再次生产,都会出现重复请求,MQ是不处理幂等性问题的
哪些接口需要幂等
接口幂等性的验证和实施需要消耗一定的资源,因此并非每个接口都应该被赋予幂等性验证的功能
接口类型 | 描述 | 是否天然幂等 |
---|---|---|
新增操作 | 新增操作每次执行都会往DB新增数据,需要做接口幂等 | ❌ |
更新操作 | 修改在大多数场景下结果一样,但是如果是增量修改的话是需要保证幂等性的 例子1:把表中id为xxx的记录的A字段值设置为1,这种操作不管执行多少次都是幂等的 例子2:把表中id为xxx的记录的A字段值增加1,这种操作就不是幂等的,是需要做接口幂等的 | 分情况 |
查询操作 | 查询用于根据条件过去资源,不会对系统资源进行改变,本身就是幂等的 | 是 |
删除操作 | 删除一次和删除多次都是把数据删除,效果是一样的,本身就是幂等的 | 是 |
幂等性常见的解决方案
数据库唯一键
通过数据库中的Unique Key唯一约束的特性,通常来说是将业务表中的某个字段设置为唯一键,即加上唯一索引
这种方式比较适用于插入数据的场景,唯一键能够确保一张表只存在一条唯一的记录
在使用数据库唯一键确保幂等性的时候,需要注意通常情况下并不使用数据库的自增键,而是采用分布式ID生成一个唯一键
防重表
利用防重表来解决幂等的原理其实和数据库唯一键差不多,都是利用唯一键的约束来进行,这种方式比较适合用于插入数据的场景
但是防重表和数据库唯一键有一个区别,那就是把这个唯一键抽离出去了,而不是耦合在原来的业务表
这样做有一个好处,比如这个比业务表可能涉及到多个业务场景,比如A,B两个场景,
其中A场景不允许存在重复,B场景是允许重复的,这样如果是在业务表中直接加唯一键的话,就会影响到B场景
而防重表通过将唯一键与业务表分开,将判重逻辑与业务逻辑剥离,就可以避免这种情况
通常防重表只包含两个字段:id和唯一索引(比如支付场景中的订单ID),这个唯一索引标识一次业务请求
这个唯一的请求ID通常也是采用分布式唯一ID生成器来处理的,比如雪花算法
缺点就是需要额外新增一张防重表,还要考虑大数据量下的分库分表或者是过期数据的清理问题
防重Token令牌
为了应对客户端连续点击或调用方的超时重试等情况,例如在提交订单时,可以通过Token机制来防止重复提交。
简而言之,调用方在调用接口之前会首先向后端请求一个全局ID(Token),并在请求时将该全局ID与其他数据一同发送
最好是将Token放置在Headers中,后端会将该Token作为键,用户信息作为值存储在Redis中进行键值内容的校验。
如果该键存在且值匹配,就会执行删除命令,然后正常执行后续的业务逻辑
如果找不到对应的键或值不匹配,则表明是重复请求,不会执行业务逻辑,直接返回重复请求的信息
操作流程:
-
客户端会先发送一个请求去获取Token,服务端会生成一个Token保存在Redis中(这个Token可以是一个分布式ID或者UUID串),
并且设置过期时间,在Redis中保存完之后,把这个ID返回给客户端
-
客户端调用业务请求的时候必须要携带这个Token(通常将这个Token放在Headers中,请求携带这个Header)
-
服务端收到请求从header中获取该Token,然后校验这个Token(去Redis查是否存在该Key)
-
如果校验成功,则执行业务,并且删除redis中的Token
-
如果校验失败,说明Redis已经删除了这个Key或者没有这个Key,则表示重复操作,直接返回指定结果给客户端
问题分析:
通过Redis + Token的方式虽然绕开了DB层面来进行幂等性的校验,总的效率会高很多,但是仍然存在不够精准的场景,做不到完全幂等
假设某个客户端第一次发起请求,然后服务端收到后将Token从Redis中删除,接着去执行业务逻辑,但业务逻辑执行失败,有两种情况
- 此时服务端可能会向客户端返回执行失败,客户端收到该返回后,自动的重新请求了一个Token,然后再次发起请求重试,这种场景下如果是正常请求,也就不存在幂等性问题
- 如果此时服务端向客户端返回执行失败的过程中,由于网络或者其他什么原因导致的,客户端无法接收到服务端返回的执行失败的响应。那么此时客户端会再次使用第一次申请的Token,再次向服务端发送请求,但是此时服务端返回的却是重复请求或者执行成功
但综合效率来说,这种方案实用性还是比较强的,没有什么明显的缺陷
扩展 - 为什么要把Token放在Headers中:
简单说一下我的理解,如果和业务数据放在请求体中,可能会导致Token被恶意篡改,如果放在URL中,会增加Token的泄漏机率
因为浏览器这些都会记录URL,放在请求头中也有利于分离关注点
思考:
Token防重令牌的方式跟分布式锁的方式很像,都是维护一个全局资源,类似于一个全局锁,获取到了才有资格进行处理
那用分布式锁来处理幂等性可以吗?如果加锁成功就执行请求处理,加锁失败说明请求已经在处理了,直接返回就行?
肯定是不合适的,我们探讨一下为什么:
-
客户端连续发起两次请求(比如用户快速点击按钮的情况),第一次请求先到达服务端,然后第二次请求由于某些原因
第二次请求过了一会才到达服务端,当第二次请求到达服务端的时候,第一次请求已经执行完毕,并且释放了锁,此时第二次
请求仍然能够加锁成功,并且执行业务逻辑,这种情况下,幂等性失效
-
客户端发起第一次请求,服务端正常执行完毕后释放了分布式锁。但由于网络问题,客户端没有正常收到服务端的响应,
此时客户端再次发起请求。由于第一次请求所加的分布式锁已经过期了,所以第二次仍然能加锁,幂等性失效
-
客户端连续发起多次请求,多次请求同时到达服务端,然后开始争抢锁,谁抢到锁就谁执行,其他没有抢到锁的不执行,这种情况可以保证幂等性,上述两种无法保证幂等性