您的位置:

java幂等,java幂等问题

本文目录一览:

JAVA中Get和Post请求的区别收集整理

一 原理区别

一般在浏览器中输入网址访问资源都是通过GET方式;在FORM提交中,可以通过Method指定提交方式为GET或者POST,默认为GET提交

Http定义了与服务器交互的不同方法,最基本的方法有4种,分别是GET,POST,PUT,DELETE

URL全称是资源描述符,我们可以这样认为:一个URL地址,它用于描述一个网络上的资源,而HTTP中的GET,POST,PUT,DELETE就对应着对这个资源的查 ,改 ,增 ,删 4个操作。到这里,大家应该有个大概的了解了,GET一般用于获取/查询 资源信息,而POST一般用于更新 资源信息(个人认为这是GET和POST的本质区别,也是协议设计者的本意,其它区别都是具体表现形式的差异 )。

根据HTTP规范,GET用于信息获取,而且应该是安全的和幂等的 。

1.所谓安全的意味着该操作用于获取信息而非修改信息。换句话说,GET请求一般不应产生副作用。就是说,它仅仅是获取资源信息,就像数据库查询一样,不会修改,增加数据,不会影响资源的状态。

* 注意:这里安全的含义仅仅是指是非修改信息。

2.幂等的意味着对同一URL的多个请求应该返回同样的结果。这里我再解释一下幂等 这个概念:

幂等 (idempotent、idempotence)是一个数学或计算机学概念,常见于抽象代数中。

幂等有以下几种定义:

对于单目运算,如果一个运算对于在范围内的所有的一个数多次进行该运算所得的结果和进行一次该运算所得的结果是一样的,那么我们就称该运算是幂等的。 比如绝对值运算就是一个例子,在实数集中,有abs(a) = abs(abs(a)) 。

对于双目运算,则要求当参与运算的两个值是等值的情况下,如果满足运算结果与参与运算的两个值相等,则称该运算幂等,如求两个数的最大值的函数,有在实数集中幂等,即max(x,x) = x 。

看完上述解释后,应该可以理解GET幂等的含义了。

但在实际应用中,以上2条规定并没有这么严格。引用别人文章的例子:比如,新闻站点的头版不断更新。虽然第二次请求会返回不同的一批新闻,该操作仍然被认为是安全的和幂等的,因为它总是返回当前的新闻。从根本上说,如果目标是当用户打开一个链接时,他可以确信从自身的角度来看没有改变资源即可。

根据HTTP规范,POST表示可能修改变服务器上的资源的请求 。继续引用上面的例子:还是新闻以网站为例,读者对新闻发表自己的评论应该通过POST实现,因为在评论提交后站点的资源已经不同了,或者说资源被修改了。

上面大概说了一下HTTP规范中,GET和POST的一些原理性的问题。但在实际的做的时候,很多人却没有按照HTTP规范去做,导致这个问题的原因有很多,比如说:

1.很多人贪方便,更新资源时用了GET,因为用POST必须要到FORM(表单),这样会麻烦一点。

2.对资源的增,删,改,查操作,其实都可以通过GET/POST完成,不需要用到PUT和DELETE。

3.另外一个是,早期的但是Web MVC框架设计者们并没有有意识地将URL当作抽象的资源来看待和设计 。还有一个较为严重的问题是传统的Web MVC框架基本上都只支持GET和POST两种HTTP方法,而不支持PUT和DELETE方法。

* 简单解释一下MVC:MVC本来是存在于Desktop程序中的,M是指数据模型,V是指用户界面,C则是控制器。使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。

以上3点典型地描述了老一套的风格(没有严格遵守HTTP规范),随着架构的发展,现在出现REST(Representational State Transfer),一套支持HTTP规范的新风格,这里不多说了,可以参考《RESTful Web Services》。

二 表现形式区别

搞清了两者的原理区别,我们再来看一下他们实际应用中的区别:

为了理解两者在传输过程中的不同,我们先看一下HTTP协议的格式:

HTTP请求:

request line          //http请求行

headers               //http请求消息报头

blank line             // 回 车 换行

[request-body]     //http请求正文

在HTTP请求中,第一行必须是一个请求行(request line),用来说明请求类型、要访问的资源以及使用的HTTP版本。紧接着是一个首部(header)小节,用来说明服务器要使用的附加信息。在首部之后是一个空行,再此之后可以添加任意的其他数据[称之为主体(body)]。

GET与POST方法实例:

GET /books/?sex=manname=Professional HTTP/1.1

Host: 

User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6)

Gecko/20050225 Firefox/1.0.1

Connection: Keep-Alive

POST / HTTP/1.1

Host: 

User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6)

Gecko/20050225 Firefox/1.0.1

Content-Type: application/x-www-form-urlencoded

Content-Length: 40

Connection: Keep-Alive

(----此处空一行----)

name=Professional%20Ajaxpublisher=Wiley

有了以上对HTTP请求的了解和示例,我们再来看两种提交方式的区别:

(1)GET提交,请求的数据会附在URL之后(就是把数据放置在HTTP协议头中),以?分割URL和传输数据,多个参数用连接;例 如:login.action?name=hydddpassword=idontknowverify=%E4%BD%A0 %E5%A5%BD。如果数据是英文字母/数字,原样发送,如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密,得出如: %E4%BD%A0%E5%A5%BD,其中%XX中的XX为该符号以16进制表示的ASCII。

POST提交:把提交的数据放置在是HTTP包的包体中。上文示例中红色字体标明的就是实际的传输数据

因此,GET提交的数据会在地址栏中显示出来,而POST提交,地址栏不会改变

(2)传输数据的大小:首先声明:HTTP协议没有对传输的数据大小进行限制,HTTP协议规范也没有对URL长度进行限制。

而在实际开发中存在的限制主要有:

GET:特定浏览器和服务器对URL长度有限制,例如 IE对URL长度的限制是2083字节(2K+35)。对于其他浏览器,如Netscape、FireFox等,理论上没有长度限制,其限制取决于操作系 统的支持。

因此对于GET提交时,传输数据就会受到URL长度的 限制。

POST:由于不是通过URL传值,理论上数据不受 限。但实际各个WEB服务器会规定对post提交数据大小进行限制,Apache、IIS6都有各自的配置。

(3)安全性:

POST的安全性要比GET的安全性高。注意:这里所说的安全性和上面GET提到的“安全”不是同个概念。上面 “安全”的含义仅仅是不作数据修 改,而这 里安全的含义是真正的Security的含义,比如:通过GET提交数据,用户名和密码将明文出现在URL上,因为(1)登录页面有可能被浏览器缓存, (2)其他人查看浏览器的历史纪录,那么别人就可以拿到你的账号和密码了,除此之外,使用GET提交数据还可能会造成Cross-site request forgery攻击

(4)Http get,post,soap协议都是在http上运行的

1)get:请求参数是作为一个key/value对的序列(查询字符串)附加到URL上的

查询字符串的长度受到web浏览器和web服务器的限制(如IE最多支持2048个字符),不适合传输大型数据集同时,它很不安全

2)post:请求参数是在http标题的一个不同部分(名为entity body)传输的,这一部分用来传输表单信息,因此必须将Content-type设置为:application/x-www-form- urlencoded。post设计用来支持web窗体上的用户字段,其参数也是作为key/value对传输。

但是:它不支持复杂数据类型,因为post没有定义传输数据结构的语义和规则。

3)soap:是http post的一个专用版本,遵循一种特殊的xml消息格式

Content-type设置为: text/xml   任何数据都可以xml化

三 HTTP响应 

1.HTTP响应格式:

status line          //http响应状态行

headers             //http响应消息报头

blank line            //回车换行

[response-body]  //http响应正文

在响应中唯一真正的区别在于第一行中用状态信息代替了请求信息。状态行(status line)通过提供一个状态码来说明所请求的资源情况。 

HTTP响应实例:

HTTP/1.1 200 OKDate: Sat, 31 Dec 2005 23:59:59 GMTContent-Type: text/html;charset=ISO-8859-1 Content-Length: 122

 

<html>

 <head>

  <title>Wrox Homepage</title>

 </head>

 <body><!-- body goes here --></body>

</html>

2.最常用的状态码有:

◆200 (OK): 找到了该资源,并且一切正常。

◆304 (NOT MODIFIED): 该资源在上次请求之后没有任何修改。这通常用于浏览器的缓存机制。

◆401 (UNAUTHORIZED): 客户端无权访问该资源。这通常会使得浏览器要求用户输入用户名和密码,以登录到服务器。

◆403 (FORBIDDEN): 客户端未能获得授权。这通常是在401之后输入了不正确的用户名或密码。

◆404 (NOT FOUND): 在指定的位置不存在所申请的资源.

四 完整示例

例子:

HTTP GET 发送

GET /DEMOWebServices2.8/Service.asmx/CancelOrder?UserID=stringPWD=stringOrderConfirmation=string HTTP/1.1

Host: api.efxnow.com

Response :

HTTP/1.1 200 OK

Content-Type: text/xml; charset=utf-8

Content-Length: length

?xml version="1.0" encoding="utf-8"?

objPlaceOrderResponse xmlns=""

Successboolean/Success

ErrorDescriptionstring/ErrorDescription

ErrorNumberint/ErrorNumber

CustomerOrderReferencelong/CustomerOrderReference

OrderConfirmationstring/OrderConfirmation

CustomerDealRefstring/CustomerDealRef

/objPlaceOrderResponse

Http Post 发送

POST /DEMOWebServices2.8/Service.asmx/CancelOrder HTTP/1.1

Host: api.efxnow.com

Content-Type: application/x-www-form-urlencoded

Content-Length: length

UserID=stringPWD=stringOrderConfirmation=string

Response : 

HTTP/1.1 200 OK

Content-Type: text/xml; charset=utf-8

Content-Length: length

?xml version="1.0" encoding="utf-8"?

objPlaceOrderResponse xmlns=""

Successboolean/Success

ErrorDescriptionstring/ErrorDescription

ErrorNumberint/ErrorNumber

CustomerOrderReferencelong/CustomerOrderReference

OrderConfirmationstring/OrderConfirmation

CustomerDealRefstring/CustomerDealRef

/objPlaceOrderResponse

SOAP 1.2 

发送

POST /DEMOWebServices2.8/Service.asmx HTTP/1.1

Host: api.efxnow.com

Content-Type: application/soap+xml; charset=utf-8

Content-Length: length

?xml version="1.0" encoding="utf-8"?

soap12:Envelope xmlns:xsi="" xmlns:xsd="" xmlns:soap12=""

soap12:Body

CancelOrder xmlns=""

UserIDstring/UserID

PWDstring/PWD

OrderConfirmationstring/OrderConfirmation

/CancelOrder

/soap12:Body

/soap12:Envelope

Response : 

HTTP/1.1 200 OK

Content-Type: application/soap+xml; charset=utf-8

Content-Length: length

?xml version="1.0" encoding="utf-8"?

soap12:Envelope xmlns:xsi="" xmlns:xsd="" xmlns:soap12=""

soap12:Body

CancelOrderResponse xmlns=""

CancelOrderResult

Successboolean/Success

ErrorDescriptionstring/ErrorDescription

ErrorNumberint/ErrorNumber

CustomerOrderReferencelong/CustomerOrderReference

OrderConfirmationstring/OrderConfirmation

CustomerDealRefstring/CustomerDealRef

/CancelOrderResult

/CancelOrderResponse

/soap12:Body

/soap12:Envelope

1. get是从服务器上获取数据,post是向服务器传送数据。 get 和 post只是一种传递数据的方式,get也可以把数据传到服务器,他们的本质都是发送请求和接收结果。只是组织格式和数据量上面有差别,http协议里面有介绍

2. get是把参数数据队列加到提交表单的ACTION属性所指的URL中,值和表单内各个字段一一对应,在URL中可以看到。post是通过HTTP post机制,将表单内各个字段与其内容放置在HTML HEADER内一起传送到ACTION属性所指的URL地址。用户看不到这个过程。 

因为get设计成传输小数据,而且最好是不修改服务器的数据,所以浏览器一般都在地址栏里面可以看到,但post一般都用来传递大数据,或比较隐私的数据,所以在地址栏看不到,能不能看到不是协议规定,是浏览器规定的。

3. 对于get方式,服务器端用Request.QueryString获取变量的值,对于post方式,服务器端用Request.Form获取提交的数据。 

没明白,怎么获得变量和你的服务器有关,和get或post无关,服务器都对这些请求做了封装

4. get传送的数据量较小,不能大于2KB。post传送的数据量较大,一般被默认为不受限制。但理论上,IIS4中最大量为80KB,IIS5中为100KB。 

post基本没有限制,我想大家都上传过文件,都是用post方式的。只不过要修改form里面的那个type参数

5. get安全性非常低,post安全性较高。 

如果没有加密,他们安全级别都是一样的,随便一个监听器都可以把所有的数据监听到,不信你自己下一个监听网络资源的软件,

一起讨论下,消息幂等(去重)通用解决方案

消息中间件是分布式系统常用的组件,无论是异步化、解耦、削峰等都有广泛的应用价值。我们通常会认为,消息中间件是一个可靠的组件——这里所谓的可靠是指,只要我把消息成功投递到了消息中间件,消息就不会丢失,即消息肯定会至少保证消息能被消费者成功消费一次,这是消息中间件最基本的特性之一,也就是我们常说的“AT LEAST ONCE”,即消息至少会被“成功消费一遍”。

举个例子,一个消息M发送到了消息中间件,消息投递到了消费程序A,A接受到了消息,然后进行消费,但在消费到一半的时候程序重启了,这时候这个消息并没有标记为消费成功,这个消息还会继续投递给这个消费者,直到其消费成功了,消息中间件才会停止投递。

然而这种可靠的特性导致,消息可能被多次地投递。举个例子,还是刚刚这个例子,程序A接受到这个消息M并完成消费逻辑之后,正想通知消息中间件“我已经消费成功了”的时候,程序就重启了,那么对于消息中间件来说,这个消息并没有成功消费过,所以他还会继续投递。这时候对于应用程序A来说,看起来就是这个消息明明消费成功了,但是消息中间件还在重复投递。

这在RockectMQ的场景来看,就是同一个messageId的消息重复投递下来了。

基于消息的投递可靠(消息不丢)是优先级更高的,所以消息不重的任务就会转移到应用程序自我实现,这也是为什么RocketMQ的文档里强调的,消费逻辑需要自我实现幂等。背后的逻辑其实就是:不丢和不重是矛盾的(在分布式场景下),但消息重复是有解决方案的,而消息丢失是很麻烦的。

例如:假设我们业务的消息消费逻辑是:插入某张订单表的数据,然后更新库存:

要实现消息的幂等,我们可能会采取这样的方案:

这对于很多情况下,的确能起到不错的效果,但是在并发场景下,还是会有问题。

假设这个消费的所有代码加起来需要1秒,有重复的消息在这1秒内(假设100毫秒)内到达(例如生产者快速重发,Broker重启等),那么很可能,上面去重代码里面会发现,数据依然是空的(因为上一条消息还没消费完,还没成功更新订单状态),

那么就会穿透掉检查的挡板,最后导致重复的消息消费逻辑进入到非幂等安全的业务代码中,从而引发重复消费的问题(如主键冲突抛出异常、库存被重复扣减而没释放等)

要解决上面并发场景下的消息幂等问题,一个可取的方案是开启事务把select 改成 select for update语句,把记录进行锁定。

但这样消费的逻辑会因为引入了事务包裹而导致整个消息消费可能变长,并发度下降。

当然还有其他更高级的解决方案,例如更新订单状态采取乐观锁,更新失败则消息重新消费之类的。但这需要针对具体业务场景做更复杂和细致的代码开发、库表设计,不在本文讨论的范围。

但无论是select for update, 还是乐观锁这种解决方案,实际上都是基于业务表本身做去重,这无疑增加了业务开发的复杂度, 一个业务系统里面很大部分的请求处理都是依赖MQ的,如果每个消费逻辑本身都需要基于业务本身而做去重/幂等的开发的话,这是繁琐的工作量。本文希望 探索 出一个通用的消息幂等处理的方法,从而抽象出一定的工具类用以适用各个业务场景。

在消息中间件里,有一个投递语义的概念,而这个语义里有一个叫”Exactly Once”,即消息肯定会被成功消费,并且只会被消费一次。以下是阿里云里对Exactly Once的解释:

在我们业务消息幂等处理的领域内,可以认为业务消息的代码肯定会被执行,并且只被执行一次,那么我们可以认为是Exactly Once。

但这在分布式的场景下想找一个通用的方案几乎是不可能的。不过如果是针对基于数据库事务的消费逻辑,实际上是可行的。

假设我们业务的消息消费逻辑是:更新MySQL数据库的某张订单表的状态:

要实现Exaclty Once即这个消息只被消费一次(并且肯定要保证能消费一次),我们可以这样做:在这个数据库中增加一个消息消费记录表,把消息插入到这个表,并且把原来的订单更新和这个插入的动作放到同一个事务中一起提交,就能保证消息只会被消费一遍了。

1、开启事务

2、插入消息表(处理好主键冲突的问题)

3、更新订单表(原消费逻辑)

4、提交事务

说明:

1、这时候如果消息消费成功并且事务提交了,那么消息表就插入成功了,这时候就算RocketMQ还没有收到消费位点的更新再次投递,也会插入消息失败而视为已经消费过,后续就直接更新消费位点了。这保证我们消费代码只会执行一次。2、如果事务提交之前服务挂了(例如重启),对于本地事务并没有执行所以订单没有更新,消息表也没插入成功;而对于RocketMQ服务端来说,消费位点也没更新,所以消息还会继续投递下来,投递下来发现这个消息插入消息表也是成功的,所以可以继续消费。这保证了消息不丢失。

事实上,阿里云ONS的EXACTLY-ONCE语义的实现上,就是类似这个方案基于数据库的事务特性实现的。更多详情可参考:

基于这种方式,的确这是有能力拓展到不同的应用场景,因为他的实现方案与具体业务本身无关——而是依赖一个消息表。

但是这里有它的局限性

1、消息的消费逻辑必须是依赖于关系型数据库事务。如果消费的消费过程中还涉及其他数据的修改,例如Redis这种不支持事务特性的数据源,则这些数据是不可回滚的。

2、数据库的数据必须是在一个库,跨库无法解决

注:业务上,消息表的设计不应该以消息ID作为标识,而应该以业务的业务主键作为标识更为合理,以应对生产者的重发。阿里云上的消息去重只是RocketMQ的messageId,在生产者因为某些原因手动重发(例如上游针对一个交易重复请求了)的场景下起不到去重/幂等的效果(因消息id不同)。

如上所述,这种方式Exactly Once语义的实现,实际上有很多局限性,这种局限性使得这个方案基本不具备广泛应用的价值。并且由于基于事务,可能导致锁表时间过长等性能问题。

例如我们以一个比较常见的一个订单申请的消息来举例,可能有以下几步(以下统称为步骤X):

1、 检查库存(RPC)

2、 锁库存(RPC)

3、 开启事务,插入订单表(MySQL)

4、 调用某些其他下游服务(RPC)

5、 更新订单状态

6、 commit 事务(MySQL)

这种情况下,我们如果采取消息表+本地事务的实现方式,消息消费过程中很多子过程是不支持回滚的,也就是说就算我们加了事务,实际上这背后的操作并不是原子性的。怎么说呢,就是说有可能第一条小在经历了第二步锁库存的时候,服务重启了,这时候实际上库存是已经在另外的服务里被锁定了,这并不能被回滚。当然消息还会再次投递下来,要保证消息能至少消费一遍,换句话说,锁库存的这个RPC接口本身依旧要支持“幂等”。

再者,如果在这个比较耗时的长链条场景下加入事务的包裹,将大大的降低系统的并发。所以通常情况下,我们处理这种场景的消息去重的方法还是会使用一开始说的业务自己实现去重逻辑的方式,如前面加select for update,或者使用乐观锁。

那我们有没有方法抽取出一个公共的解决方案,能兼顾去重、通用、高性能呢?

其中一个思路是把上面的几步,拆解成几个不同的子消息,例如:

1、库存系统消费A:检查库存并做锁库存,发送消息B给订单服务

2、订单系统消费消息B:插入订单表(MySQL),发送消息C给自己(下游系统)消费

3、下游系统消费消息C:处理部分逻辑,发送消息D给订单系统

4、订单系统消费消息D:更新订单状态

注:上述步骤需要保证本地事务和消息是一个事务的(至少是最终一致性的),这其中涉及到分布式事务消息相关的话题,不在本文论述。

可以看到这样的处理方法会使得每一步的操作都比较原子,而原子则意味着是小事务,小事务则意味着使用消息表+事务的方案显得可行。

然而,这太复杂了!这把一个本来连续的代码逻辑割裂成多个系统多次消息交互!那还不如业务代码层面上加锁实现呢。

上面消息表+本地事务的方案之所以有其局限性和并发的短板,究其根本是因为它依赖于关系型数据库的事务,且必须要把事务包裹于整个消息消费的环节。

如果我们能不依赖事务而实现消息的去重,那么方案就能推广到更复杂的场景例如:RPC、跨库等。

例如,我们依旧使用消息表,但是不依赖事务,而是针对消息表增加消费状态,是否可以解决问题呢?

67_1.png

以上是去事务化后的消息幂等方案的流程,可以看到,此方案是无事务的,而是针对消息表本身做了状态的区分:消费中、消费完成。只有消费完成的消息才会被幂等处理掉。而对于已有消费中的消息,后面重复的消息会触发延迟消费(在RocketMQ的场景下即发送到RETRY TOPIC),之所以触发延迟消费是为了控制并发场景下,第二条消息在第一条消息没完成的过程中,去控制消息不丢(如果直接幂等,那么会丢失消息(同一个消息id的话),因为上一条消息如果没有消费完成的时候,第二条消息你已经告诉broker成功了,那么第一条消息这时候失败broker也不会重新投递了)

上面的流程不再细说,后文有github源码的地址,读者可以参考源码的实现,这里我们回头看看我们一开始想解决的问题是否解决了:

1、 消息已经消费成功了,第二条消息将被直接幂等处理掉(消费成功)。

2、 并发场景下的消息,依旧能满足不会出现消息重复,即穿透幂等挡板的问题。

3、 支持上游业务生产者重发的业务重复的消息幂等问题。

关于第一个问题已经很明显已经解决了,在此就不讨论了。

关于第二个问题是如何解决的?主要是依靠插入消息表的这个动作做控制的,假设我们用MySQL作为消息表的存储媒介(设置消息的唯一ID为主键),那么插入的动作只有一条消息会成功,后面的消息插入会由于主键冲突而失败,走向延迟消费的分支,然后后面延迟消费的时候就会变成上面第一个场景的问题。

关于第三个问题,只要我们设计去重的消息键让其支持业务的主键(例如订单号、请求流水号等),而不仅仅是messageId即可。所以也不是问题。

如果细心的读者可能会发现这里实际上是有逻辑漏洞的,问题出在上面聊到的个三问题中的第2个问题(并发场景),在并发场景下我们依赖于消息状态是做并发控制使得第2条消息重复的消息会不断延迟消费(重试)。但如果这时候第1条消息也由于一些异常原因(例如机器重启了、外部异常导致消费失败)没有成功消费成功呢?也就是说这时候延迟消费实际上每次下来看到的都是 消费中 的状态,最后消费就会被视为消费失败而被投递到死信Topic中(RocketMQ默认可以重复消费16次)。

有这种顾虑是正确的!对于此,我们解决的方法是,插入的消息表必须要带一个最长消费过期时间,例如10分钟,意思是如果一个消息处于 消费中 超过10分钟,就需要从消息表中删除(需要程序自行实现)。所以最后这个消息的流程会是这样的:

67_2.png

我们这个方案实际上没有事务的,只需要一个存储的中心媒介,那么自然我们可以选择更灵活的存储媒介,例如Redis。使用Redis有两个好处:

1、性能上损耗更低

2、上面我们讲到的超时时间可以直接利用Redis本身的ttl实现

当然Redis存储的数据可靠性、一致性等方面是不如MySQL的,需要用户自己取舍。

以上方案针对RocketMQ的Java实现已经开源放到Github中,具体的使用文档可以参考 ,

以下仅贴一个Readme中利用Redis去重的使用样例,用以意业务中如果使用此工具加入消息去重幂等的是多么简单:

以上代码大部分是原始RocketMQ的必须代码,唯一需要修改的仅仅是创建一个 DedupConcurrentListener 示例,在这个示例中指明你的消费逻辑和去重的业务键(默认是messageId)。

更多使用详情请参考Github上的说明。

实现到这里,似乎方案挺完美的,所有的消息都能快速的接入去重,且与具体业务实现也完全解耦。那么这样是否就完美的完成去重的所有任务呢?

很可惜,其实不是的。原因很简单:因为要保证消息至少被成功消费一遍,那么消息就有机会消费到一半的时候失败触发消息重试的可能。还是以上面的订单流程X:

1、 检查库存(RPC)

2、 锁库存(RPC)

3、 开启事务,插入订单表(MySQL)

4、 调用某些其他下游服务(RPC)

5、 更新订单状态

6、 commit 事务(MySQL)

当消息消费到步骤3的时候,我们假设MySQL异常导致失败了,触发消息重试。因为在重试前我们会删除幂等表的记录,所以消息重试的时候就会重新进入消费代码,那么步骤1和步骤2就会重新再执行一遍。如果步骤2本身不是幂等的,那么这个业务消息消费依旧没有做好完整的幂等处理。

那么既然这个并不能完整的完成消息幂等,还有什么价值呢?价值可就大了!虽然这不是解决消息幂等的银弹(事实上,软件工程领域里基本没有银弹),但是他能以便捷的手段解决:

1、各种由于Broker、负载均衡等原因导致的消息重投递的重复问题

2、各种上游生产者导致的业务级别消息重复问题

3、重复消息并发消费的控制窗口问题,就算重复,重复也不可能同一时间进入消费逻辑

也就是说,使用这个方法能保证正常的消费逻辑场景下(无异常,无异常退出),消息的幂等工作全部都能解决,无论是业务重复,还是rocketmq特性带来的重复。

事实上,这已经能解决99%的消息重复问题了,毕竟异常的场景肯定是少数的。那么如果希望异常场景下也能处理好幂等的问题,可以做以下工作降低问题率:

1、消息消费失败做好回滚处理。如果消息消费失败本身是带回滚机制的,那么消息重试自然就没有副作用了。

2、消费者做好优雅退出处理。这是为了尽可能避免消息消费到一半程序退出导致的消息重试。

3、一些无法做到幂等的操作,至少要做到终止消费并告警。例如锁库存的操作,如果统一的业务流水锁成功了一次库存,再触发锁库存,如果做不到幂等的处理,至少要做到消息消费触发异常(例如主键冲突导致消费异常等)

4、在#3做好的前提下,做好消息的消费监控,发现消息重试不断失败的时候,手动做好#1的回滚,使得下次重试消费成功

如何判断请求的幂等性 java

x请求的报文头不一样,通过如下

Java代码

String requestType = request.getHeader("X-Requested-With");

如果requestType能拿到值,并且值为XMLHttpRequest,表示客户端的请求为异步请求,那自然是ajax请求了,反之如果为null,则是普通的请求

java程序领取优惠卷,高并发下怎么避免领取量超过预发量

基本思路是:锁+幂等性

具体实现:

使用redis的decr (对key对应的数字做减1操作。如果key不存在,那么在操作之前,这个key对应的值会被置为0。如果key有一个错误类型的value或者是一个不能表示成数字的字符串,就返回错误。这个操作最大支持在64位有符号的整型数字。)可以实现原子性的递增递减操作控制优惠码不超送,然后给每个用户维护一个userid+优惠码活动的key保证幂等性,只要redis存在这种key,那就代表已经领取了,具体的优惠码分发可以异步执行。为了避免竞争(同一个用户,多个设备同时领取),参考:网页链接