幂等性实现 -接口幂等性

接口幂等性

1.什么是幂等性

对于同一笔业务操作,不管调用多少次,得到的结果都是一样的。
也就是方法调用一次和调用多次产生的额外效果是相同的,他就具有幂等性

2.为什么需要幂等性

在系统高并发的环境下,很有可能因为网络,阻塞等等问题导致客户端或者调用方并不能及时的收到服务端的反馈甚至是调用超时的问题。总之,就是请求方调用了你的服务,但是没有收到任何的信息,完全懵逼的状态。比如订单的问题,可能会遇到如下的几个问题:
1.创建订单时,第一次调用服务超时,再次调用是否产生两笔订单?
2.订单创建成功去减库存时,第一次减库存超时,是否会多扣一次?
3.订单支付时,服务端扣钱成功,但是接口反馈超时,此时再次调用支付,是否会多扣一笔呢?
作为消费者,前两种能接受,第三种情况就MMP了,哈哈哈!!!

这种情况一般有如下两种解决方式:
1.服务方提供一个查询操作是否成功的api,第一次超时之后,调用方调用查询接口,如果查到了就走成功的流程,失败了就走失败的流程。
2.服务方需要使用幂等的方式保证一次和多次的请求结果一致。

3.产生幂等性的场景

幂等性问题在我们的开发中,分布式、微服务架构中随处可见;

  1. 因网络波动,可能会引起重复请求;
  2. 用户重复操作,用户在使用产品时可能会无意的触发多次下单多次交易,甚至没有响应而有意触发多笔交易;
  3. 应用使用了失败或超时重试机制(如nginx重试、RPC重试或业务层重试等)
  4. 第三方平台的接口:(如:支付成功回调接口),因为异常导致多次异步回调;
  5. 中间件/应用服务根据自身的特性,也有可能进行重试
  6. 用户双击提交按钮;
  7. 页面重复刷新;
  8. 使用浏览器后退按钮重复之前的操作,导致重复提交表单;
  9. 使用浏览器历史记录重复提交表单;
  10. 浏览器重复的HTTP请求;
  11. 定时任务重复执行;

3.RESTFUL HTTP的幂等性

  • GET:只是获取资源,对资源本身没有任何副作用,天然的幂等性。
  • HEAD:本质上和GET一样,获取头信息,主要是探活的作用,具有幂等性。
  • OPTIONS:获取当前URL所支持的方法,因此也是具有幂等性的。
  • DELETE:用于删除资源,有副作用,但是它应该满足幂等性,比如根据id删除某一个资源,调 用方可以调用N次而不用担心引起的错误(根据业务需求而变)。
  • Put和Post:都可以用于新增/修改,使用区别就说Put请求是幂等的,Post不是幂等性的,使用时不应该简单的却别与做新增还是修改,根据业务是否需要幂等来进行选择。

在这里插入图片描述

4.幂等性的实现方式

  1. 前端实现:第5节
  2. 后端实现:第6节

在这里插入图片描述

5.前端幂等性

5.1 按钮只可点击一次

对于客户端交互的接口,可以在前端拦截一部分,例如防止表单重复提交,按钮置灰,隐藏,不可点击等方式。但是前端进行拦截器显然是针对普通用户,懂点技术的都可以模拟请求调用接口,所以后端幂等性很重要。

5.2 Token机制(前端+后端)

  • 针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用Token的机制实现防止重复提交。

  • TOKEN机制的实现:简单的说就是调用方在调用接口的时候先向后端请求一个全局ID(TOKEN),请求的时候携带这个全局ID一起请求,后端需要对这个全局ID校验来保证幂等操作
    在这里插入图片描述

  • 主要的流程步骤如下:

    • 客户端进入业务操作之前(例如进入提交页面时),先发送获取token的请求,服务端会生成一个全局唯一的ID保存在redis中,同时把这个ID返回给客户端;
    • 客户端调用业务请求的时候必须携带这个token,一般放在请求头上;
    • 服务端操作redis做del删除token,如果删除成功证明是第一次请求,执行后续操作;
    • 如果删除失败,则表示重复操作,直接返回指定的结果给客户端。
  • 通过以上的流程分析,唯一的重点就是这个全局唯一ID如何生成,在分布式服务中往往都会有一个生成全局ID的服务来保证ID的唯一性,但是工程量和实现难度比较大,UUID的数据量相对有些大,此处可以选择雪花算法生成全局唯一ID。

  • token机制缺点:
    业务请求每次请求,都会有额外的请求(一次获取token请求、判断token是否存在的业务)。其实真实的生产环境中,1万请求也许只会存在10个左右的请求会发生重试,为了这10个请求,我们让9990个请求都发生了额外的请求。(当然redis性能很好,耗时不会太明显)

6.后端幂等性实现

6.1 普通方式

例子过程如下:

  1. 接收到支付宝支付成功请求
  2. 根据trade_no查询当前订单是否处理过
  3. 如果订单已处理直接返回,若未处理,继续向下执行
  4. 开启本地事务
  5. 本地系统给用户加钱
  6. 将订单状态置为成功
  7. 提交本地事务上面的过程,对于同一笔订单,如果支付宝同时通知多次,会出现什么问题?当多次通知同时到达第2步时候,查询订单都是未处理的,会继续向下执行,最终本地会给用户加两次钱。此方式适用于单机其,通知按顺序执行的情况,只能用于自己写着玩玩。

6.2 JVM加锁的方式

方式1中由于并发出现了问题,此时我们使用java中的Lock加锁,来防止并发操作,过程如下:1. 接收到支付宝支付成功请求

  1. 调用java中的Lock加锁
  2. 根据trade_no查询当前订单是否处理过
  3. 如果订单已处理直接返回,若未处理,继续向下执行
  4. 开启本地事务
  5. 本地系统给用户加钱
  6. 将订单状态置为成功
  7. 提交本地事务
  8. 释放Lock锁分析问题:
    Lock只能在一个jvm中起效,如果多个请求都被同一套系统处理,上面这种使用Lock的方式是没有问题的,不过互联网系统中,多数是采用集群方式部署系统,同一套代码后面会部署多套,如果支付宝同时发来多个通知经过负载均衡转发到不同的机器,上面的锁就不起效了。此时对于多个请求相当于无锁处理了,又会出现方式1中的结果。此时我们需要分布式锁来做处理。

6.3 Token机制(同5.2)

6.4 唯一索引

可以限制重复插入数据,当数据重复时,插入数据库会抛出异常,保证不会出现脏数据。
对于insert操作,当我们插入数据的时候会出现两种情况?
1.自增主键
如果是自增主键,多次插入一定会出现重复插入问题
2.业务主键(唯一索引)
如果是业务主键,假设我们对订单id做唯一索引,前提是我们可以保证这一笔订单的id是唯一的,即便多次提交也是唯一的。

6.5 悲观锁的实现(查询语句加锁for update)

  1. 接收到支付宝支付成功请求
  2. 打开本地事物
  3. 查询订单信息并加悲观锁select * from t_order where order_id = trade_no for update;
  4. 判断订单是已处理
  5. 如果订单已处理直接返回,若未处理,继续向下执行
  6. 给本地系统给用户加钱
  7. 将订单状态置为成功
  8. 提交本地事物

重点在于for update,对for update,做一下说明:
1.当线程A执行for update,数据会对当前记录加锁,其他线程执行到此行代码的时候(也要有for update如果没有会出现问题),会等待线程A释放锁之后,才可以获取锁,继续后续操作。
2.事物提交时,for update获取的锁会自动释放。

问题:for update可以正常实现我们需要的效果,能保证接口的幂等性,不过存在一些缺点:
如果业务处理比较耗时,并发情况下,后面线程会长期处于等待状态,占用了很多线程,让这些线程处于无效等待状态,我们的web服务中的线程数量一般都是有限的,如果大量线程由于获取for update锁处于等待状态,不利于系统并发操作。

6.6 有限状态机实现

在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候如果状态机已经处于下一个状态,却来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。
针对更新操作,如果业务上需要修改订单状态,订单有待支付、支付中、支付成功、支付失败、订单超时等,在设计时最好只支持单项改变(不可逆),这样在更新的时候where条件里可以加上status=我期望的上一个status,多次调用的话实际上也指挥执行一次。
update xx set status = “支付中” where status = ‘待支付’ and id=xx (注:update是当前读,多次update会等待)

6.7 乐观锁实现

如果更新已有数据,可以进行加锁更新,也可以设计表结构时使用乐观锁,比如通过加version字段来做乐观锁,这样既能保证执行效率,又能保证幂等性。乐观锁的version版本在更新业务数据要自增。
也可以采用更新带条件,实现乐观锁,通过version或者其他条件来实现乐观锁

  • 例子主要流程如下:
    1. 接收到支付宝支付成功请求
    2. 查询订单信息
      select * from t_order where order_id = trade_no;
    3. 判断订单是否已经处理
    4. 如果处理直接返回,未处理,继续执行
    5. 打开本地事务
    6. 给本地系统用户加钱
    7. 将订单状态置为成功(这里要判断update返回的结果)
      update t_order set status = 1 where order_id = trade_no where status = 0;
      • 返回为1表示更新成功,提交事务
      • 返回为其他表示更新失败,回滚事务
  • 因为update是当前读,也就是多个事务去更新同一行数据,update会锁定数据上了排他锁,直到事务提交。
    • 例子:
    比如,表名A,字段名为 number,如下的SQL语句:语句1:update A set number=number+ 5 where id=1;语句2:update A set number=number+ 7 where id=1;假设这两条SQL语句同时被mysql执行,id=1的记录中number字段的原始值为 10,那么是否有可能出现这种情况:语句1和2因为同时执行,他们得到的number的值都是10,都是在10的基础上分别加5和7,导致最终number被更新为15或17,而不是22?这个其实就是 关系型数据库本身就需要解决的问题。首先,他们同时被MySQL执行,你的意思其实就是他们是并发执行的,而并发执行的事务在关系型数据库中是有专门的理论支持的- ACID,事务并行等理论,所有关系型数据库实现,包括Oracle, MySQL都需要遵循这个原理。简单一点理解就是锁的原理。这个时候第一个update会持有id=1这行记录的 排它锁,第二个update需要持有这个记录的排它锁的才能对他进行修改,正常的话, 第二个update会阻塞,直到第一个update提交成功,他才会获得这个锁,从而对数据进行修改。也就是说,按照关系型数据库的理论,这两个update都成功的话,id=1的number一定会被修改成22。

在这里插入图片描述

6.8 防重表+唯一约束实现

需要增加一个表,防止数据重复的表

CREATE TABLE `t_uq_dipose` (`id` bigint(20) NOT NULL AUTO_INCREMENT, `ref_type` varchar(32) NOT NULL DEFAULT '' COMMENT '关联对象类型', `ref_id` varchar(64) NOT NULL DEFAULT '' COMMENT '关联对象id', PRIMARY KEY (`id`), UNIQUE KEY `uq_1` (`ref_type`,`ref_id`) COMMENT '保证业务唯一性' ) ENGINE=InnoDB;

对于任何一个业务,有一个业务类型(ref_type),业务有一个全局唯一的订单号,业务来的时候,先查询t_uq_dipose表中是否存在相关记录,若不存在,继续放行。
过程如下:

  1. 接收到支付宝支付成功请求
  2. 查询t_uq_dipose(条件ref_id,ref-type),可以判断订单是否已经处理
    select * from t_uq_dipose where ref_type = '充值订单' and ref_id = trade_no;
  3. 判断订单是否已经处理
  4. 如果订单已经处理直接返回,若未处理,继续放行
  5. 打开本地事务
  6. 给本地系统给用户加钱
  7. 将订单状态置为成功
  8. 向t_uq_dipose插入数据,插入成功,提交本地事务,插入失败,回滚本地事务:
try{ insert into t_uq_dipose (ref_type,ref_id) values ('充值订单',trade_no); 提交本地事务:
}catch(Exception e){ 回滚本地事务; 
}

在这里插入图片描述

说明:
对于同一个业务,ref_type是一样的,当并发时,插入数据只会有一条成功,其他的会违法唯一约束,进入catch逻辑,当前事务会被回滚,最终最有一个操作会成功,从而保证了幂等性操作。
关于这种方式可以写成通用的方式,不过业务量大的情况下,t_uq_dipose插入数据会成为系统的瓶颈,需要考虑分表操作,解决性能问题。
上面的过程中向t_uq_dipose插入记录,最好放在最后执行,原因:插入操作会锁表,放在最后能让锁表的时间降到最低,提升系统的并发性。关于消息服务中,消费者如何保证消息处理的幂等性?
每条消息都有一个唯一的消息id,类似于上面业务中的trade_no,使用上面的方式即可实现消息消费的幂等性。

参考

幂等性

  1. https://www.cnblogs.com/Leo_wl/p/12640651.html
  2. https://www.cnblogs.com/itsoku123/p/10860527.html
  3. https://zhuanlan.zhihu.com/p/151438657
  4. https://www.bilibili.com/video/BV1YJ411V7aj?p=15
    数据库:
  5. https://blog.csdn.net/zmemorys/article/details/104814110
  6. https://blog.csdn.net/silyvin/article/details/79294508?tdsourcetag=s_pctim_aiomsg
  7. https://blog.csdn.net/winy_lm/article/details/49718193
  8. https://blog.csdn.net/sinat_27143551/article/details/89968902

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/437377.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C 怎么读取Cpp文件_python之调用C加速计算(一)

一、前言python语言是目前比较火的语言,很容易上手,对数据处理也比较友好,可以用几行代码就能进行一些简单的数据处理工作。但是对于稍微大型的数值计算,或者一些涉及到大量循环的数值计算python的计算速度有点让人失望。即使是使…

【转】刨根究底字符编码【2.0版】(3):字符编码的由来、演变与ASCII码

为什么需要字符编码 1. 计算机一开始发明出来时是用来解决数字计算问题的,后来人们发现,计算机还可以做更多的事,例如文本处理。 但计算机其实挺“笨”的,它只“认识”010110111000…这样由0和1两个数字组成的二进制数字&#…

JS创建对象的模式介绍

转自http://www.cnblogs.com/asqq/archive/2013/02/01/3194993.html

matplotlib的优点_超详细matplotlib基础介绍!!!

(给Python开发者加星标,提升Python技能)来源:逐梦erhttps://zhumenger.blog.csdn.net/article/details/106530281【导语】:出色的数据可视化,会让你的数据分析等工作锦上添花,让人印(升)象(职)深(加)刻(薪)。matplotli…

【转】WPF PRISM开发入门一( 初始化PRISM WPF程序)

这篇博客将介绍在WPF项目中引入PRISM框架进行开发的一些基础知识。目前最新的PRISM的版本是Prism 6.1.0,可以在Github上获取PRISM的源码。这个系列的博客将选择PRISM 4.1版本来讲解。可以从微软官网上下载到PRISM 4.1相关内容。将下载下来的文件解压开: …

截屏悬浮软件_功能强大,却小巧的录屏软件,不在错过你的王者时刻

看看录屏是一款操作简单。功能强大的录屏软件。他可以设置你录制视频的一个分辨率,帧率以及录制屏幕方向,非常方便,用户将手机摇一摇就可以控制开启和停止录屏,高效录制精彩瞬间,在录制游戏视频的时候也可以做到不掉帧…

公司用的非标普通自动化用单片机还是plc_PLC的介绍

PLC又叫可编程控制器,一开始是替代传统接触器的一个东西。随着人工价格不断的上涨,自动化的设备会越来越普及。自动化不再是大企业才用的起的东西 ,各种多元化小型自动化设备进入了普通小企业甚至家庭作坊。PLC其实是单片机开发出来的一种工业…

比较文本差异的工具_Linux 开发的五大必备工具 | Linux 中国

Linux 已经成为工作、娱乐和个人生活等多个领域的支柱,人们已经越来越离不开它。在 Linux 的帮助下,技术的变革速度超出了人们的想象,Linux 开发的速度也以指数规模增长。因此,越来越多的开发者也不断地加入开源和学习 Linux 开发…

【转】C# 动态对象(dynamic)的用法

说到正确用法,那么首先应该指出一个错误用法: 常有人会拿var这个关键字来和dynamic做比较。实际上,var和dynamic完全是两个概念,根本不应该放在一起做比较。var实际上是编译期抛给我们的“语法糖”,一旦被编译&#x…

关于prototype使用位置问题的讨论

问题贴:http://bbs.csdn.net/topics/390446362 new四部曲: (1)创建一个新的对象,并让函数的 this 指针指向它; (2)将函数的 prototype 对象的所有成员都赋给这个新对象&#xff0c…

@query传参_vue-router中params传参和query传参的区别及处理方法

在 Vue 实例内部,你可以通过 $router 访问路由实例。因此你可以调用 this.$router.push想要导航到不同的 URL,则使用 router.push 方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则…

JS成员函数声明位置优化

上代码 function A() {this.a function(){}; } a1 new A(); a2 new A(); alert( a1.aa2.a);输出 说明了a1.a,a2.a指向的内存不是同一个,也就是每个对象都有一份自己的函数,只不过一个类的所有实例之间的函数长得是一样的! 所以…

mybatis plus 事务管理器_SpringBoot第七篇:springboot开启声明式事务

springboot开启事务很简单,只需要一个注解Transactional 就可以了。因为在springboot中已经默认对jpa、jdbc、mybatis开启了事事务,引入它们依赖的时候,事物就默认开启。当然,如果你需要用其他的orm,比如beatlsql&…

JS静态变量和静态函数

function A(){this.id "我是AA"} // 在构造函数外定义的都是所有对象共享的 A.id "我是A"; A.sayId function(){alert(A.id);} A.sayId(); 如上,在构造函数外用函数名定义的属性或者方法,可以也只可以通过函数名来访问&…

Spark读取HDFS上的Snappy压缩文件所导致的内存溢出问题 java.lang.OutOfMemoryError: GC overhead limit exceeded

报错java.lang.OutOfMemoryError: GC overhead limit exceeded HDFS上有一些每天增长的文件,使用Snappy压缩,突然某天OOM了 1.原因: 因为snappy不能split切片,也就会导致一个文件将会由一个task来读取,读取后解压,数…

【转】VS编程,快速折叠或者展开代码到 #region 级别的设置方法。

在代码比较多的文档中,使用#region进行分功能的区分折叠是一个方便的方法。 如果文档中含有很多个#region标签,想一次全部折叠或者展开,有时是必要的。 这里给出一种设置方法,适用于VS2019,其它VS版本请自己验证。 1、…

.net一个函数要用另一个函数的值_Mysql:条件判断函数-CASE WHEN、IF、IFNULL详解

前言在众多SQL中,统计型SQL绝对是让人头疼的一类,之所以如此,是因为这种SQL中必然有大量的判读对比。而条件判断函数就是应对这类需求的利器。本文重点总结CASE WHEN、IF、IFNULL三种函数。1 CASE WHENCase when语句能在SQL语句中织入判断逻辑…

Spark2内存调优总结 - 内存划分 与 内存计算 与 调参方式

使用的Spark2以上版本所以只考虑UnifiedMemoryManager动态内存管理,如图: 1. 内存划分 与 内存计算 与 调参方式 1.1 三部分:Spark内存、用户内存、预留内存 预留内存:300MB 固定Spark内存和用户内存比例由参数spark.memory.fra…

java 通过id获取html代码_Maven私服安装配置,java通过私服下载代码,并打包后上传到私服(Nexus)...

Maven私服一般安装Nexus。首先,Nexus下载,访问Nexus官方网址https://www.sonatype.com/download-nexus-repo-oss下载完成后是个压缩包第二步 配置:1)将上一步下载的nexus解压2)端口和监听配置application-port:监听端口applicatio…