一、什么是接口幂等性?

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条,这就没有保证接口的幂等性。

二、什么情况下需要保证接口的幂等性?

在增删改查4个操作中,尤为注意就是增加或者修改,

A: 查询操作,如:

select * from user where user_id = 1;

不管执行一次还是多次上面的查询语句,如果数据库记录没有变更的话,查询结果都是一样的。由此可见,select是天然的幂等操作。

B: 删除操作 ,如:

delte from user where user_id = 1;

不管是删除一次,还是删除多次,都是把数据删除,在不考虑返回结果的情况下,因此删除操作也是具有幂等性的。

C: 更新操作

如下例子:

把表中id为XXX的记录的A字段值设置为1,这种操作不管执行多少次都是幂等的。如:

update user set username = 'zhangsan' where user_id = 1

如上看见,将username字段改成一个固定的值,这种操作不管执行多少次,结果都是一样的,所以这种情况下更新操作时幂等的。

再看下面的例子。如:

update user set age = age + 1 where user_id = 1

这里将age字段进行递增操作,每执行一次,那么age就会加一,显然每次执行结果都不一样,所以这种情况下update操作就不是幂等操作。

D: 新增操作

增加在重复提交的场景下会出现幂等性问题,如用户下订单操作,如果网络卡顿,用户重复点击了多次购买按钮,那么就可能导致相同的订单被我们保存了多份。如:

insert into order(pkid, order_id, xx) values (1, '20210304020226953568', ...);

假设pkid是自增,如果order_id没有做唯一约束的话,那么就可能导致同一个订单保存多条数据,不具备幂等性;如果order_id做了唯一约束,那么这个新增就是幂等的。

三、哪些情况下需要防止接口?

对于业务中需要考虑幂等性的地方一般都是接口的重复请求,重复请求是指同一个请求因为某些原因被多次提交。导致这个情况会有几种场景:

  • 前端重复提交

用户在新增页面上快速点击多次,造成发了多次请求,后端重复保存了多条一模一样的数据。如用户提交订单,生成很多重复的订单;

  • 消息重复消费

消息重复消费,一般指的是消息中间件,如RabbitMQ,由于网络抖动,MQ Broker将消息发送给消费端消费,消费端进行了消费,在返回ack给MQ Broker时网络中断等原因,导致MQ Broker认为消费端没能正常消费消息,这时候MQ Broker会重复将这条消息发给消费端进行消费,如果没有做幂等,就会造成客户端重复消费同一条消息。

  • 页面回退再次提交

举个例子,用户购买商品的时候,如果第一次点击下单按钮后,提示下单成功,跳转到下单成功页面,这时候如果用户点击浏览器返回按钮,返回上一个下单页面,重新点击下单按钮,这时候如果没有做幂等的话,也会造成重复下单的问题。

  • 微服务互相调用

分布式系统中,服务之间的通信一般都通过RPC或者Feign进行调用,难免网络会出小问题,导致此次请求失败,这时候这些远程调用,如feign都会触发重试机制,所以我们也需要保证接口幂等。

对于一些业务场景影响比较大的,比如支付交易等场景,必须要实现接口的幂等,否则出现重复扣了客户的钱,可想而知后果。

  • 其他业务情况

四、如何实现接口幂等?

对于和web端交互的接口,我们可以在前端拦截一部分,例如防止表单重复提交,按钮置灰、隐藏、按钮不可点击等方式。

但是前端做控制实际效益不是很高,懂点技术的都会模拟请求调用你的服务,所以安全的策略还是需要从后端的接口层来做。

那么后端要实现分布式接口的幂等性有哪些策略方式呢?后端实现接口幂等的方式主要有下面几种,可以根据业务场景,结合起来一起使用,保证接口的幂等。

【a】数据库唯一约束

插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单号就不可能有两条记录插入,我们在数据库层面防止重复。

这个机制是利用了数据库的主键唯一索引的特性,解决了在插入场景时的幂等问题。

CREATE TABLE `tbs_idempotent` (`pkid` bigint(20) NOT NULL COMMENT '主键ID',`serial_no` bigint(20) NOT NULL COMMENT '唯一序列号',PRIMARY KEY (`pkid`),UNIQUE KEY `serial_no` (`serial_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='幂等校验表'

如上,建立了一个唯一序列号是唯一索引,我们在进行业务操作的时候,往这张表插入一条数据,如果后面第二次提交【序列号还是一样,比如订单ID】,发现这张表的序列号已经在第一次插入进去了,那么第二次操作就什么都不进行,直接返回,保证幂等。

【b】redis set防重

很多数据需要处理,只能被处理一次,比如我们可以计算数据的MD5将其放入redis的set数据结构,每次处理数据,先看这个MD5是否已经存在,如果已经存在就不处理。

【c】防重数据表

使用订单号orderNo作为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一事务操作中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一个数据库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据的一致性。

【d】数据库锁

数据库常见的锁也就是乐观锁和悲观锁。

  • 数据库悲观锁

数据库悲观锁指的就是每次操作的时候,先把记录锁定起来,其他人无法操作这条记录。如下面的查询的时候添加for update锁定这行:

select * from user where user_id = 1 for update;

注意,数据库悲观锁使用时,一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。

  • 2、数据库乐观锁

数据库乐观锁就是利用版本号的概念,在操作前先获取到操作记录的当前version版本号,然后操作的时候带上此版本号。

这种方法适合在更新的场景中:

update user set age = age + 1, version = version + 1 where user_id = 2 and version = 1

注意,乐观锁主要使用于处理读多写少的问题。

【e】业务层分布式锁

如果多个线程可能在同一时间处理相同的数据,比如多个线程在同一时刻都拿到了相同的数据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断这个数据是否被处理过。

【f】Token机制

这种token令牌机制应该是市面上用的比较多的一种保证幂等方式,简单理解,就是每次请求都拿着一张门票,这个门票是一次性的,用过一次就被毁掉了,不能重复利用。这个token令牌就相当于门票的概念,每次接口请求的时候带上token令牌,服务器第一次处理的时候去校验token,并且这个token只能用一次,如果用户使用相同的令牌请求二次,那么第二次就不处理,直接返回。

大体的流程如下:

  • 1、服务端提供了发送Token的接口,在执行业务前,先去获取Token,服务器会把Token保存到redis中;
  • 2、然后调用业务接口请求时,把token携带过去,一般作为请求参数或者请求头中传递;
  • 3、服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业务;
  • 4、服务器如果短时间内重复提交这个接口,因为两次请求token是一样的,所以第二次请求的时候,服务器校验token时,redis中已经没有了刚刚被第一次删掉的token,就表示是重复操作,所以第二次请求会校验失败,不作处理,这样就保证了业务代码,不被重复执行;

流程图大体如下:

不过token这种方案有一定的危险性,其实就在于服务端我们到底该如何去验证令牌。

  • 1、先删除token【先删除token令牌,再执行业务】还是后删除token【先执行业务,再删除token令牌】;

(a)、先删除可能导致,业务确实没有执行,重试还带上了之前的token,由于防重设计导致,请求还是不能执行;

(b)、后删除token问题很大,可能导致,业务处理成功,但是服务闪断,出现超时,没有删除token,别人继续重试,导致业务被执行两次;

(c)、我们最好设计为先删除token,如果业务调用失败,就重新获取token再次请求。

  • 2、token获取,比较和删除必须保证原子性

(a)、redis.get(token)【获取】、token.equals()【比较】、redis.del(token)【删除】、如果这三个操作不是原子的,可能导致高并发下,多个线程都获取到同样的数据,判断都成功,继续业务并发执行;

(b)、可以在redis中使用lua脚本完成这个操作,保证上述操作原子性。

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

五、幂等校验案例 - 防止订单重复提交

针对前端重复连续多次点击的情况,例如用户购物提交订单,提交订单的接口就可以通过 Token 的机制实现防止重复提交。

【a】服务端生成token令牌,并保存在redis中

@Autowired
private StringRedisTemplate redisTemplate;public static final String USER_ORDER_TOKEN_PREFIX = "order:token:";//服务端发放token令牌,发送令牌可以在查询订单确认页的接口中进行生成,然后返回给前端,使用隐藏域保存在
//form表单中,作为提交订单的一个参数提交到服务器进行token校验
//防重令牌
String orderToken = UUID.randomUUID().toString().replace("-", "");
//order:token:{会员ID} 如: order:token:1
//token过期时间设置为15分钟
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberId, orderToken,15, TimeUnit.MINUTES);
orderConfirmVo.setOrderToken(orderToken);
return orderConfirmVo;提交到服务

【b】前端页面

提交订单的form中使用隐藏域把服务器生成的token作为form表单项,作为用户提交订单的一个参数,提交给服务器进行令牌校验。

<form action="xxxxx" method="post">//.....<!--orderToken就是服务器返回给前端的令牌--><input name="orderToken" th:value="${confirmOrderData.orderToken}" type="hidden"/><button class="tijiao" type="submit">提交订单</button>
</form>

【c】后端原子校验令牌

//1.原子验证令牌
//获取用户提交订单页面传递过滤的token令牌
String orderToken = orderSubmitVo.getOrderToken();
Long memberId = memberResponseVo.getId();
//获取redis中的令牌【令牌的对比和删除必须保证原子性】
//LUA脚本  返回0表示校验令牌失败  1表示删除成功,校验令牌成功
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberId),orderToken);
if (result == 1) {//令牌验证成功//去创建、下订单、验令牌、验价格、锁定库存...
} else {//令牌校验失败,返回失败信息
}

说明:在处理提交上来的订单时,一定要通过lua脚本原子验证令牌和删除令牌机制,否则还会有漏洞。如果快速点击了两下提交按钮,这时两个请求几乎同时进来,第一个先去查询redis,然后再准备删除token的时候,第二个也查了redis,存在Token,这样还是会造成重复提交。如果使用原子验证令牌,就可以完美解决该问题。

六、总结

通过以上的了解我们可以知道,针对不同的业务场景我们需要灵活的选择幂等性的实现方式。例如防止类似于前端重复提交、重复下单的场景就可以通过 Token 的机制实现,对于那些重复消费和接口重试的场景则使用数据库唯一索引的方式实现更合理。

接口幂等性常见的解决方案相关推荐

  1. 小工匠聊架构-分布式场景下的并发幂等性常见的解决方案

    文章目录 Pre 分布式锁 能否解决幂等性问题? 常见的四种解决方案 DB唯一索引 先select 后 insert (存在并发安全的问题,需要分布式锁) 分布式锁 状态机 小结 思考 Pre 我们来 ...

  2. Redis 处理接口幂等性的两种方案

    前言:接口幂等性问题,对于开发人员来说,是一个跟语言无关的公共问题.对于一些用户请求,在某些情况下是可能重复发送的,如果是查询类操作并无大碍,但其中有些是涉及写入操作的,一旦重复了,可能会导致很严重的 ...

  3. 处理接口幂等性的两种常见方案

    在上周发布的 TienChin 项目视频中,我和大家一共梳理了六种幂等性解决方案,接口幂等性处理算是一个非常常见的需求了,我们在很多项目中其实都会遇到.今天我们来看看两种比较简单的实现思路. 1. 接 ...

  4. java中接口幂等性解决方案总结

    一.概念 一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同.幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数.这些函数不会影响系统状态,也不用担心重复执行 ...

  5. oracle 锁表如何解决_「技术分享」高并发下的接口幂等性解决方案

    高并发下的接口幂等性解决方案! 一.背景我们实际系统中有很多操作,是不管做多少次,都应该产生一样的效果或返回一样的结果.例如:前端重复提交选中的数据,应该后台只产生对应这个数据的一个反应结果.我们发起 ...

  6. 接口幂等性的几种简单解决方案

    1.接口幂等性是什么 接口幂等性并不是数学中的幂等,而是对于用户来说不论网络波动或者刷新页面后重新操作某个动作等,最终导致对同一接口产生多次请求, 但是对于用户而言并没有产生影响.也就是说重复多次操作 ...

  7. Java接口幂等性多种解决方案

    Java接口幂等性的解决方案: java 语音中,同一个接口相同的参数多次和一次请求产生的效果是一样,这样的过程即被称为满足幂等性 //这中情况无论执行多少次,结果都不受影响,是幂等的. update ...

  8. Springboot + redis + 注解 + 拦截器来实现接口幂等性校验

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:wangzaiplus www.jianshu.com/p/ ...

  9. springboot + redis + 注解 + 拦截器 实现接口幂等性校验

    点击上方"方志朋",选择"设为星标" 做积极的人,而不是积极废人 来源:https://www.jianshu.com/p/6189275403ed 一.概念 ...

  10. Spring Boot + Redis 实现接口幂等性 | 分布式开发必知!

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 来源:http://tinyurl.com/y5k2sx5t >>阿里云8月最新 ...

最新文章

  1. 总点第一个视频产生选择偏差?Youtube用“浅塔”来纠正
  2. getDeclaredField(x);与set方法的用法
  3. 汇编语言 实验10.1 显示字符串
  4. 常见虚拟主机目录对照及星外提权目录
  5. 有了 for (;;) 为什么还需要 while (true) ? 到底哪个更快?
  6. 二进制编译安装mysql_MariaDB通用二进制编译安装详解
  7. 详解RPC远程调用和消息队列MQ的区别
  8. 左侧栏下拉框HTML代码,html5下拉菜单代码
  9. 什么是Intel LBR(上次分支记录),BTS(分支跟踪存储)和AET(体系结构事件跟踪)?
  10. 提醒!赶快弃掉这个区块链平台!
  11. 个人作业六:单元测试
  12. 封装好的C++ md5类
  13. C语言动态链表实现KTV点歌系统
  14. 国内外云服务器运维面板有哪些?运维面板全面汇总
  15. Unity 父子约束 手持弓箭
  16. 继承的表现形式与特点
  17. 电池的寿命(c语言)
  18. 押对信息流,百度站上全新起点
  19. 视频教程-2021软考系统集成项目管理工程师视频教程精讲 基础知识(下)-软考
  20. OceanBase Developer Center

热门文章

  1. C/C++[PAT B1009]说反话
  2. 开启Accessibility的快捷方式-3次home键或者侧边键
  3. c语言山东科技大学答案oj,山东科技大学oj部分题目记答案.doc
  4. java jar包 资源_一个小坑:java如何访问依赖jar包中的资源文件
  5. 相同的树 深度优先搜索
  6. 445.两数相加II
  7. 432.全O(1)的数据结构
  8. lottie动画_设计师和程序员的动画还原神器Lottie+Bodymovin
  9. 凸优化第六章逼近与拟合 6.4鲁棒逼近
  10. 【ML小结12】隐马尔科夫模型HMM