文章目录
- 前言
- 什么是接口幂等?
- 为什么接口需要幂等性设计
- 前端重复提交表单
- 黑客恶意攻击
- 接口超时重复提交
- 消息重复消费
- 哪些接口需要幂等?
- 如何实现幂等
- 前端拦截
- 数据库唯一索引实现
- 数据库乐观锁实现
- 数据库悲观锁实现
- JVM锁实现
- 分布式锁实现
- Token实现
- 总结
前言
接口幂等-幂等性-接口的幂等性-分布式幂等性-如何保证幂等-幂等性实现方案-去重表-下单幂等-支付幂等-扣还库存幂等
什么是接口幂等?
在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与第一次执行的影响相同。
接口的幂等性实际上就是接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。有些接口可以天然的实现幂等性,比如查询接口,对于查询来说,你查询一次和两次,对于系统来说,没有任何影响,查出的结果也是一样
为什么接口需要幂等性设计
该问题等同于 为什么会重复调用?
前端重复提交表单
在填写一些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。
黑客恶意攻击
例如在实现用户投票这种功能时,如果黑客针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,这样会使投票结果与事实严重不符。
接口超时重复提交
大部分RPC框架[比如Dubbo],为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。
消息重复消费
当使用 MQ 消息中间件时候,如果Consumer消费超时或者producer发送了消息但由于网络原因未收到ACK导致消息重发,都会导致重复消费。
哪些接口需要幂等?
幂等性的实现与判断需要消耗一定的资源,因此不应该给每个接口都增加幂等性判断,要根据实际的业务情况和操作类型来进行区分。例如,我们在进行查询操作和删除操作时就无须进行幂等性判断。
查询操作查一次和查多次的结果都是一致的,因此我们无须进行幂等性判断。删除操作也是一样,删除一次和删除多次都是把相关的数据进行删除(这里的删除指的是条件删除而不是删除所有数据),因此也无须进行幂等性判断。
所以到底哪些接口需要幂等?关于这个问题需要从具体业务出发,但是也有规律可循如下表:
如何实现幂等
前端拦截
前端拦截是指通过 Web 站点的页面进行请求拦截,比如在用户点击完“提交”按钮后,我们可以把按钮设置为不可用或者隐藏状态,避免用户重复点击。
该方法可以解决用户误操作提交两次表单所产生的重复提交问题。但前端拦截有一个致命的问题,如果是懂行的程序员或者黑客可以直接绕过页面的 JS 执行,直接模拟请求后端的接口,这样的话,我们前端的这些拦截就不能生效了。因此除了前端拦截一部分正常的误操作之外,后端的验证必不可少。
数据库唯一索引实现
数据库唯一索引实现方案一般只能适用于执行插入操作的过程。
具体流程步骤:
- 建立一张去重表,其中某个字段需要建立唯一索引
- 客户端去请求服务端,服务端会将这次请求的一些信息插入这张去重表中
- 因为表中某个字段带有唯一索引,如果插入成功,证明表中没有这次请求的信息,则执行后续的业务逻辑
- 如果插入失败,则代表已经执行过当前请求,直接返回
数据库乐观锁实现
数据库乐观锁方案一般只能适用于执行更新操作的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。
这样每次对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值。
具体流程步骤:
- 客户端带着version字段请求服务端
- 服务端执行update的时候需要给version+1,并且需要加version的更新条件如下SQL
update t set stock = stock - 1 , version = version + 1 where id = #{id} and version = #{version}
数据库悲观锁实现
具体流程步骤:
- 客户端通过业务id,访问服务端
- 先查数据库是否存在该业务id,查库的时候需要加X锁
- 如果存在则说明是重复请求,不存在则进行业务逻辑处理
JVM锁实现
JVM 锁实现是指通过 JVM 提供的内置锁如 Lock 或者是 synchronized 来实现幂等性。使用 JVM 锁来实现幂等性的一般流程为:首先通过 Lock 对代码段进行加锁操作,然后再判断此订单是否已经被处理过,如果未处理则开启事务执行订单处理,处理完成之后提交事务并释放锁,执行流程如下图所示:
JVM 锁存在的最大问题在于,它只能应用于单机环境,因为 Lock 本身为单机锁,所以它就不适应于分布式多机环境。
分布式锁实现
分布式锁实现解决JVM锁实现单机锁局限问题。
具体流程步骤:
- 客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段
- 将该字段以 SETNX 的方式存入 redis 中,并根据业务设置相应的超时时间
- 如果设置成功,证明这是第一次请求,则执行后续的业务逻辑
- 如果设置失败,则代表已经执行过当前请求,直接返回
Token实现
具体流程步骤:
- 客户端会先发送一个请求去获取 token,服务端会生成一个全局唯一的 ID 作为 token 保存在 redis 中,同时把这个 ID 返回给客户端
- 客户端第二次调用业务请求的时候必须携带这个 token
- 服务端会校验这个 token,如果校验成功,则执行业务,并删除 redis 中的 token
- 如果校验失败,说明 redis 中已经没有对应的 token,则表示重复操作,直接返回指定的结果给客户端
注意:
对 redis 中是否存在 token 以及删除的代码逻辑建议用 Lua 脚本实现,保证原子性
全局唯一 ID 可以用百度的 uid-generator、美团的 Leaf 去生成
总结
幂等性不但可以保证程序正常执行,还可以杜绝一些垃圾数据以及无效请求对系统资源的消耗。推荐使用分布式锁来实现,这样的解决方案更加通用。