接口的幂等性
概念
接口幂等性是指,描述了一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。也就是说对一个接口,同一个请求无论对其请求了多少次,最后产生的结果是一样的。
常见场景
一般正常情况下,在接口调用时都能正常返回信息,不会重复提交,但是下面的情况会出现问题
网络波动:网络波动可能会导致重复请求,如果网络连接在请求发送后中断,客户端可能会尝试重新发送请求
用户重复操作:用户在操作时可能会无意触发多次操作
使用了失效或者超时重试机制:例如nginx重试或者业务层重试,如果一个请求在超时或者失败后被动自动重试,那么可能会导致重复请求
支付接口:在支付场景中,重复支付会导致多次扣款,例如一个支付请求被重复发送,那么可能会导致用户的账号被多次扣款
订单接口:在订单处理场景中,同一个订单肯能会被多次创建
MQ消费者读取重复消息:在消息队列的使用场景中,消费者可能会读取到重复的消息进行消费
前端重复提交表单:在填写一些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。
用户恶意进行刷单:例如在实现用户投票这种功能时,如果用户针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,这样会使投票结果与事实严重不符
解决方案
- 数据库唯一主键:利用数据库中主键唯一约束的特性,一般来说唯一主键比较适用于“插入”时的幂等性,其能保证一张表中只能存在一条带该唯一主键的记录。
- 数据库乐观锁:一般只能适用于执行“更新操作”的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。
- 防重Token令牌:针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用Token的机制实现防止重复提交。
- 使用GET请求:GET请求是幂等的,即多次相同的GET请求对服务器没有任何副作用。如果接口的操作满足幂等性要求,并且不涉及修改数据状态,可以考虑将接口设计为GET请求。
- 限制并发:在并发场景下,通过原子操作(如Redis的SETNX命令)确保在验证Token有效的同时,将其删除或更新状态,避免多个请求同时通过验证。
- 服务端控制:在服务端接口处理逻辑时,可以通过通过一些特定的标识符或请求参数来校验请求的幂等性,以确保同样的请求不会被重复处理
- 乐观锁:如果更新已有数据,可以进行加锁更新,也可以设计表结构时使用乐观锁,通过version来做乐观锁,这样既能保证执行效率,又能保证幂等, 乐观锁的version版本在更新业务数据要自增
Restful API 接口的幂等性
| 方法类型 | 是否幂等 | 描述 |
|---|---|---|
| Get | 是 | Get 方法用于获取资源。其一般不会也不应当对系统资源进行改变,所以是幂等的。 |
| Post | 否 | Post 方法一般用于创建新的资源。其每次执行都会新增数据,所以不是幂等的。 |
| Put | 看具体情况 | Put 方法一般用于修改资源。该操作则分情况来判断是不是满足幂等,更新操作中直接根据某个值进行更新,也能保持幂等。不过执行累加操作的更新是非幂等。 |
| Delete | 看具体情况 | Delete 方法一般用于删除资源。该操作则分情况来判断是不是满足幂等,当根据唯一值进行删除时,删除同一个数据多次执行效果一样。不过需要注意,带查询条件的删除则就不一定满足幂等了。例如在根据条件删除一批数据后,这时候新增加了一条数据也满足条件,然后又执行了一次删除,那么将会导致新增加的这条满足条件数据也被删除。 |
- 在增量跟新时,更新操作是不具备幂等的
数据库唯一主键
数据库唯一主键就是利用数据库中主键唯一的约束性,在插入操作时,保证这个唯一主键对应的数据只能存在一条无法重复插入
使用数据库唯一主键来实现接口幂等性时一般使用的是分布式ID充当主键,而不是数据库自增的id
- 客户端执行创建请求,调用服务端接口。
- 服务端执行业务逻辑,生成一个分布式 ID,将该 ID 充当待插入数据的主键,然后执数据插入操作,运行对应的 SQL 语句。
- 服务端将该条数据插入数据库中,如果插入成功则表示没有重复调用接口。如果抛出主键重复异常,则表示数据库中已经存在该条记录,返回错误信息到客户端
防重token令牌
在设计接口幂等性时,你可以采用以下的策略来确保只有在第一次请求时会存入token,然后在第一次请求完成之后删除token,再次请求则不会存入token:
- 生成Token:当客户端发起一个需要保证幂等性的操作时,首先在客户端生成一个唯一的token。这个token可以是UUID或者其他任何能够保证全局唯一的标识符。
- 存储Token:将这个token作为参数发送到服务器。在服务器端,检查Redis中是否已经存在这个token:
- 如果token不存在,那么将token存入Redis,并继续执行后续的业务逻辑。
- 如果token已经存在,那么直接返回一个错误响应,比如”重复的请求”
在删除token时有两种策略
先执行业务再删除token:在高并发下,很有可能出现第一次访问时token存在,完成具体业务操作。但在还没有删除token时,客户端又携带token发起请求,此时,因为token还存在,第二次请求也会验证通过,执行具体业务操作- 解决方案就是串行变并行:对于业务代码执行和删除token整体加线程锁。当后续线程再来访问时,则阻塞排队
先删除token再执行业务:假设业务代码执行超时或者失败,没有向客户端返回明确的结果,那么客户端就会进行重试,都是token已经被删除掉了,则会被认为是重复请求,不再进行业务的处理无需额外处理,让用户重新发起请求再次创建新的token即可
此外,在高并发场景下还要考虑Redis中token的判空和删除的原子操作问题,对Redis删除token的结果做一个判断,删除成功才能进行业务操作,否则直接返回
1
2
3
4Boolean delete = valueOperations.getOperations().delete(token);
if(!delete){
return "token不合法";
}
参考文章: