99.99%的操作,都可以这样顺利地完成,但生活吧,意外总是不期而遇:
这种情况下,如果我们不做任何设计,自然就会重复支付。要杜绝这种问题,最直接的思路就是:不要重试!不要重试!不要重试!(学一下三体)针对【意外1】:app可以设计成点击后将按钮失效。针对【意外2】和【意外3】:可以关闭相关的重试功能。这是采用了“逃避”的思路,也就是不要让问题发生。但这真不是你能控制的。况且,一旦整个架构体系变得复杂,你很难评估是不是某个点会有重试的逻辑。所以,解决幂等性问题,不能依赖别人“不重试”,而要以“肯定会重试”作为前提条件来设计。但这并不是说所有的逻辑可以在后端完成,app侧起码要做一个基本的改造,那就是每次用户的点击请求,会生成唯一一个ID,并且把这个ID一路带下来。
然后,后端可以这样来设计:注意:从这里开始,我们的后端设计不仅应对“不小心”的重复支付,更针对故意的调用方重试。你也可以理解为我们在做一个“支付服务”的设计。
(方案1)此时,如果原始请求超时异常,然后重试的话,会被拦截,如下图:
据我了解,大部分幂等的设计都是这种方式,你可以对比下你的系统。但这样设计会有个不容易想到的严重缺陷,看下图:
这种情况非常严重。你可以想象,如果调用方认为失败,但其实支付成功,会是什么结果?!这里的关键问题在于:需要控制在任何时刻,任何一个唯一键请求,只有一个线程在执行。所以,我们需要在业务检验之前,就做一个分布式锁,保证只有一个线程处理支付。这里我们有两个方案。第一个方案是:将落支付流水的动作提到业务检验之前。如下图:
(方案2)这个方案的问题在于,会有很多业务校验失败的流水在库中。这无论对检索的性能还是存储的成本来说,都是一个需要考虑的点。另外,所有的请求直接落库,对数据库压力很大。例如有黑产用高并发扫你的接口,你不先做一次黑名单检查直接落库,对db来说风险极高,可能会横向影响其他业务。如果你认为没有这种场景,并且有很多废流水没问题,这个方案是可以的。事实上,有些银行的接口就是这么设计的。如果你不想有那么多废流水,你可以采用第二个方案,那就是在业务检验前加一个分布式锁。同时,如果分布式锁获取失败,则查一下流水库,返回流水状态。如下图:
(方案3)上述方案采用的是redis分布式锁,也可以使用db的幂等表来实现。但是,这个方案是有问题的。如果原始请求在抢到分布式锁以后异常中断了(例如服务器重启)。重试的请求都只能获得“订单不存在”的状态。但是订单不存在有可能是因为中断,有可能是因为原始请求还没有走到落数据库这一步。对于调用方来说不敢直接认为失败。我们看下图:
这种情况下,我们往往会给到调用方一个约定。约定:如果原始请求后超过一段时间(例如1小时,以下都以1小时举例)重试,依然获取到订单不存在,则可以认定为失败!服务端要保证1小时内,原始请求一定执行完(无论是成功、失败、还是异常终止)。
到这里总该万事大吉了吧?没错,到这里确实就可以了。很多大厂都是这么设计的。但是,这里有一个问题。那就是,对于调用方来说,如果服务端发生异常中断(例如机器重启)的情况,他只能等到约定的1小时后换号重新支付。不要小看换号这个事情。调用方对一笔支付换号重试是高危操作,一旦换号,所有的幂等都失效。所以,如果调用方想要尽量保证支付成功,同时忌讳换号来做重试。该怎么办呢?上面的方案中,之所以需要换号,是因为我们的分布式锁不会释放。那么,我们如果1小时后删除幂等,就可以做原号重试了。如下图:
(方案4)不同于换号重试的是,原号重试依然在支付流水数据库层面有幂等控制,不会重复支付。这样,我们就实现了不换号重试的功能。我们来总结一下,我们一共有三种方案来实现幂等,我们汇总如下图:
这三个方案有自己的使用场景,我最后来说一下:【方案2】如果你确保没有恶意请求给数据库带来压力,并且接受大量废流水,可以直接使用这个方案。同时确保整个“从流水入库到支付完成”在一个事务中。如果不在一个事务中,会存在支付异常时支付流水悬挂的问题。需要通过补偿的方式推进。这个点我们此文不细讲了。如果有问题可以公众号给我留言。【方案3】如果你可以要求调用方接受一段时间后换号重试。你可以使用这个方案。【方案4】如果你的调用方无法接受换号重试,你可以选择这个方案。事实上,【方案3】和【方案4】是大厂的最佳实践。你可以在设计自己系统时酌情参考。当然,有一些变种的实现,但原理上和核心环节上的设计是一致的。你现在再回头看看方案1,是不是就深刻体会到,幂等性设计并没有那么容易吧。
转载本文请联系「 CodingBetterLife」公众号。 我的Rails4应用程序遇到一些异常行为。每次我单击View中的link_to时,我的Controller操作都会被调用两次。例如:在我的root_url中,我对users_profile有这个标准调用:"logout-button")%>当我单击此链接时,我的控制台显示以下输出:StartedGET"/users/profile"for127.0.0.1at2013-11-2520:45:53-0200ProcessingbyUsers::SessionsController#profileasHTMLUserLoad(0.7ms)SELECT"users".*FROM"users"
我在我的Rakefile中定义了以下RSpec(1.3.0)任务:require'spec/rake/spectask'Spec::Rake::SpecTask.new(:spec)do|spec|spec.libs我在spec/spec_helper.rb中有以下内容:require'rubygems'require'spec'require'spec/autorun'require'rack/test'require'webmock/rspec'includeRack::Test::MethodsincludeWebMockrequire'omniauth/core'我在spec/
我的表单提交了两次,经过仔细检查,这是由':remote=>true'引起的。我删除了它,我的项目运行良好。谁能告诉我为什么?以及如何使用':remote=>true'?我的ruby代码:true,:id=>'new_product_group_form')do%>[:product_scopes,:groups,group_name]),scopes.keys.mapdo|scope_name|[t(:name,:scope=>[:product_scopes,:scopes,scope_name]),scope_name]end]end)%>"/>浏览器中的最终html代码。Add
目录一种简单上手的暴力论文分析方法——以区块链为例【含项目源码】太长不看版本:最终成果:情况说明论文推荐方面论文投稿方面以下是具体的实现,有其他研究方向想自行确定的请仔细阅读,授人以鱼不如授人以渔第一章、确定对象——研究热点的中国计算机研究生第二章、思路——基于爬虫结合关键字过滤暴力获取所需论文信息第一步:从CCF推荐目录中获取网址01、背景介绍02、数据预处理03、数据写入表格第二步:从中科院分区中获取期刊对应分区第三步:从期刊/会议对应网址中爬取到子网页并进入,获取到其中的标题、年份等信息第四步:针对获取到的表格数据进行分析和整理实际爬取数据量【其实就论文的标题+对应年份】
我有一个像这样的Angular应用:angular.module('ngStyleApp',[]).controller('testCtrl',function($scope){$scope.list=[1,2,3];$scope.getStyles=function(index){console.log('gettingstylesforindex'+index);return{color:'red'};};});带有相应的标记:{{value}}正如预期的那样,可见输出是三个红色列表项。但是该语句总共被记录到控制台6次,这意味着View被渲染了两次:gettingstylesfor
我不知道为什么我的React组件会渲染两次。所以我从参数中提取一个电话号码并将其保存到状态,以便我可以搜索Firestore。一切似乎都工作正常,除了渲染两次......第一个渲染电话号码和零点。第二次渲染时所有数据都正确显示。有人可以指导我找到解决方案。classUpdateextendsComponent{constructor(props){super(props);const{match}=this.props;this.state={phoneNumber:match.params.phoneNumber,points:0,error:''}}getPoints=()=>{f
我正在使用ryanbatesnested_formgem将一些嵌套字段动态添加到表单。例如一切正常,除了每次单击链接时都会添加两个空字段。我在$('forma.add_nested_fields').live('click',function()上放置了一个断点并看到它被调用了两次...我在mac上使用chrome 最佳答案 查看标题。你会看到它在那里两次:只需删除第二个引用(可能在您的application.html.erb中)即可。 关于javascript-动态嵌套表单link_t
假设我们有很多事情要做。我们使用$('body').on('click','.todo',do_stuff)而不是$('.todo').click(do_stuff)所以我们只会将一个事件监听器附加到DOM。但是,我使用的是小型MVC。每个待办事项View都有此代码$('body').on('click','.todo',do_stuff)。所以如果我们有20件事情要做,这是否意味着body有20个听众或只有一个?他们都会开火吗? 最佳答案 你应该杀掉之前的事件处理器:$('body').off('click','.todo',do
这是一个按钮:和绑定(bind)事件:$("#addToCart").bind('click',function(){$.ajax({url:'/cartManager/add',data:{pictureId:currentImageId,printSize:$("#sizeoption:selected").val(),paperType:$("#paperTypeoption:selected").val(),quantity:1},success:function(){$("#modal").html("ОКClosinginasec").delay(1000);$("#mod
我正在研究chating模块。为此,我使用了private_pubgem.在这个模块中,我制作了三个channel,但我不能在这里一一列举,因为它会显示一个非常非常大的页面。因此,让我们坚持一个channel。我单击具有channel""的链接然后ajax工作并转到"conversations/send_invitation"(只有一次-没关系)在我的"/conversations/send_invitation"我有defsend_invitation@conversation=Conversation.new(conversation_params)respond_todo|for