导航:首页 > 数据处理 > 如何解决重复数据幂等

如何解决重复数据幂等

发布时间:2022-11-19 14:24:39

1. 理解幂等性

一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。

也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。换种说法,就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

举个最简单的例子,那就是支付,用户购买商品使用支付,此时多次触发支付,只会支付一次,而不会多扣钱。

1. 幂等需要关注的几个重点:

(1)幂等不仅仅只是一次(或多次)请求对资源没有副作用。
(2)幂等还包括第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。
(3)幂等关注的是以后的多次请求是否对资源产生的副作用,而不关注结果。
(4)网络超时等问题,不是幂等的讨论范围。

幂等性是系统服务对外一种承诺(而不是实现),承诺只要调用接口成功,外部多次调用对系统的影响是一致的。声明为幂等的服务会认为外部调用失败是常态,并且失败之后必然会有重试。

2. 幂等与防重的区别:

(1)重复提交是在 第一次请求已经成功的情况下 ,人为的进行多次操作,导致不满足幂等要求的服务多次改变状态。
(2)幂等更多使用的情况是 第一次请求不知道结果(比如超时)或者失败的异常情况下 ,发起多次请求,目的是多次确认第一次请求成功,却不会因多次请求而出现多次的状态变化。( 重点重点重点!!!

业务开发中,经常会遇到重复提交的情况,无论是由于网络问题无法收到请求结果而重新发起请求,或是前端的操作抖动而造成重复提交情况。

交易系统,支付系统这种重复提交造成的问题有尤其明显,比如:

(1)用户在APP上连续点击了多次提交订单,后台应该只产生一个订单

(2)向支付宝发起支付请求,由于网络问题或系统BUG重发,支付宝应该只扣一次钱。 很显然,声明幂等的服务认为,外部调用者会存在多次调用的情况,为了防止外部多次调用对系统数据状态的发生多次改变,将服务设计成幂等。

以SQL为例,有下面三种场景,只有第三种场景需要开发人员使用其他策略保证幂等性:

幂等可以使得客户端逻辑处理变得简单,但是却以服务逻辑变得复杂为代价。 满足幂等服务的需要在逻辑中至少包含两点:

(1)首先去查询上一次的执行状态,如果没有则认为是第一次请求。
(2)在服务改变状态的业务逻辑前,保证防重复提交的逻辑。

幂等是为了简化客户端逻辑处理,却增加了服务提供者的逻辑和成本,是否有必要,需要根据具体场景具体分析,因此除了业务上的特殊要求外,尽量不提供幂等的接口。

(1)增加了额外控制幂等的业务逻辑,复杂化了业务功能;
(2)把并行执行的功能改为串行执行,降低了执行效率。

幂等需要通过唯一的业务单号来保证。也就是说相同的业务单号,认为是同一笔业务。使用这个唯一的业务单号来确保,后面多次的相同的业务单号的处理逻辑和执行效果是一致的。

下面以支付为例, 在不考虑并发的情况下,实现幂等很简单:

① 先查询一下订单是否已经支付过;
② 如果已经支付过,则返回支付成功;如果没有支付,进行支付流程,修改订单状态为‘已支付’。

上述的保证幂等方案是分成两步的,第②步依赖第①步的查询结果,无法保证原子性的。在高并发下就会出现下面的情况:第二次请求在第一次请求第②步订单状态还没有修改为‘已支付状态’的情况下到来。

既然得出了这个结论,余下的问题也就变得简单: 把查询和变更状态操作加锁,将并行操作改为串行操作。

(1)乐观锁

如果只是更新已有的数据,没有必要对业务进行加锁,设计表结构时使用乐观锁,一般通过version来做乐观锁,这样既能保证执行效率,又能保证幂等。

例如: UPDATE tab1 SET col1=1,version=version+1 WHERE version=#version# 不过,乐观锁存在失效的情况,就是常说的ABA问题,不过如果version版本一直是自增的就不会出现ABA的情况。

(2)悲观锁

select * from xx for update;

悲观锁和乐观锁的区别:

使用订单号orderNo做为去重表的唯一索引,每次请求都根据订单号向去重表中插入一条数据。

第一次请求查询订单支付状态,当然订单没有支付,进行支付操作,无论成功与否,执行完后更新订单状态为成功或失败,删除去重表中的数据。

后续的订单因为表中唯一索引而插入失败,则返回操作失败,直到第一次的请求完成(成功或失败)。可以看出防重表作用是加锁的功能。

订单发起支付请求,支付系统会去Redis缓存中查询是否存在该订单号的Key,如果不存在,则向Redis增加Key为订单号。

查询订单支付已经支付,如果没有则进行支付,支付完成后删除该订单号的Key。通过Redis做到了分布式锁,只有这次订单订单支付请求完成,下次请求才能进来。

相比去重表,将放并发做到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。

这种方式分成两个阶段:申请token阶段和支付阶段。

第一阶段,在进入到提交订单页面之前,需要订单系统根据用户信息向支付系统发起一次申请token的请求,支付系统将token保存到Redis缓存中,为第二阶段支付使用。

第二阶段,订单系统拿着申请到的token发起支付请求,支付系统会检查Redis中是否存在该token,如果存在,表示第一次发起支付请求,删除缓存中token后开始支付逻辑处理;如果缓存中不存在,表示非法请求。

实际上这里的token是一个信物,支付系统根据token确认是否是非法请求。不足是需要系统间交互两次,流程较上述方法复杂。

把订单的支付请求都快速地接下来,一个快速接单的缓冲管道。后续使用异步任务处理管道中的数据,过滤掉重复的待支付订单。优点是同步转异步,高吞吐。不足是不能及时地返回支付结果,需要后续监听支付结果的异步返回。

2. Excel 2007中对重复数据如何处理

1、数据输入时避免重复
在输入学籍号、身份证号码等数据时,我们可以通过如下方法解决:
(1)首先选中要输入数据的固定区域,如A2:A10单元格区域,然后打开“开始”标签页,在“条件格式”下拉菜单中选择“突出显示单元格规则”→“重复值”(如图一)。
(2)时就会打开“重复值”对话框,在这里你可以根据需要自行定义重复值单元格内数据的颜色,如设置成醒目的红色(如图二),确认后即可生效。
这样,当你不慎输入了重复的数据,当前单元格就会立即变成红色,给你提示。
2、轻松删除重复数据
如果大量的数据已经输入完毕,这时我们需要将相同的数据找出来并将其删除,方法是:选中需要删除重复项的单元格区域,然后切换到“数据”标签页,直接单击工具栏中的“删除重复项”,此时会打开“删除重复项”对话框(如图三),确认后就会将指定区域中的所有重复内容一次性清除。

3. 分布式系统中实现幂等性的几种方式

在微服务架构下,我们在完成一个订单流程时经常遇到下面的场景:

为了解决以上问题,就需要保证接口的幂等性 ,接口的幂等性实际上就是接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。 有些接口可以天然的实现幂等性 ,比如查询接口,对于查询来说,你查询一次和两次,对于系统来说,没有任何影响,查出的结果也是一样。

除了查询功能具有天然的幂等性之外,增加、更新、删除都要保证幂等性。 那么如何来保证幂等性呢?

多版本并发控制,该策略主要使用update with condition(更新带条件来防止)来保证多次外部请求调用对系统的影响是一致的。在系统设计的过程中,合理的使用乐观锁,通过version或者updateTime(timestamp)等其他条件,来做乐观锁的判断条件,这样保证更新操作即使在并发的情况下,也不会有太大的问题。例如

在更新的过程中利用version来防止,其他操作对对象的并发更新,导致更新丢失。为了避免失败,通常需要一定的重试机制。

在插入数据的时候,插入去重表,利用数据库的唯一索引特性,保证唯一的逻辑。

这种方法适用于在业务中有唯一标的插入场景中,比如在以上的支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据和写入去去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。

select for update,整个执行过程中锁定该订单对应的记录。注意:这种在DB读大于写的情况下尽量少用。

并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了。注意:核心高并发流程不要用这种方法。

在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机,就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。

这种方法适合在有状态机流转的情况下,比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为100。付款失败为99

在做状态机更新时,我们就这可以这样控制

业务要求:页面的数据只能被点击提交一次

发生原因:由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交

解决办法:

处理流程:

token特点:要申请,一次有效性,可以限流

如银联提供的付款接口:需要接入商户提交付款请求时附带:source来源,seq序列号。source+seq在数据库里面做唯一索引,防止多次付款,(并发时,只能处理一个请求)

总结: 幂等性应该是合格程序员的一个基因,在设计系统时,是首要考虑的问题,尤其是在像支付宝,银行,互联网金融公司等涉及的都是钱的系统,既要高效,数据也要准确,所以不能出现多扣款,多打款等问题,这样会很难处理,用户体验也不好 。

如果使用全局唯一ID,就是根据业务的操作和内容生成一个全局ID,在执行操作前先根据这个全局唯一ID是否存在,来判断这个操作是否已经执行。如果不存在则把全局ID,存储到存储系统中,比如数据库、redis等。如果存在则表示该方法已经执行。

从工程的角度来说,使用全局ID做幂等可以作为一个业务的基础的微服务存在,在很多的微服务中都会用到这样的服务,在每个微服务中都完成这样的功能,会存在工作量重复。另外打造一个高可靠的幂等服务还需要考虑很多问题,比如一台机器虽然把全局ID先写入了存储,但是在写入之后挂了,这就需要引入全局ID的超时机制。

使用全局唯一ID是一个通用方案,可以支持插入、更新、删除业务操作。但是这个方案看起来很美但是实现起来比较麻烦,下面的方案适用于特定的场景,但是实现起来比较简单。

4. excel表怎么去掉重复数据

你好
平时用Excel表格处理大量数据的时候,总会遇到大量的重复无效数据。

如果只有几十条数据,也就罢了,一条一条比对,很快也能清理完,可是如果数据多达上百条,甚至成千上万条,还是去一条一条筛选,那得排查到过年了。

其实,Excel自带很多简单的重复数据处理功能,无论是删除还是查找,只需点击几下,就能迅速处理表格中的重复数据,简单又轻松。

今天,我们就来学习几个Excel中处理重复数据的小功能,保证各位学会后,一定有相见恨晚的感觉。

查找重复数据

步骤:

1、用【WPS】打开需要处理的数据

2、单机上方工具栏中的【数据】

3、选择【高亮重复项】→【设置高亮重复项】→确定

4、选择【清除高亮重复项】,可去除高亮

删除重复数据

步骤:

1、用【WPS】打开需要处理的数据

2、单机上方工具栏中的【数据】

3、选择【数据工具】→【删除重复项】

使用函数查找重复数据

使用函数将重复数据标记为重复,并且可以隐藏,需要的时候还可以找回,不用删除哦,此功能非常人性化!

步骤:

1、用【WPS】打开需要处理的数据

2、在第一行数据右方的【单元格】中输入函数【=IF(COUNTIF(A$2:A$16,A2)>1,"重复","")】,在这里一定要注意的一点是【函数括号中的(单元格)】,需要根据各位在实际处理数据表格中【单元格的选择范围】来填写。

3、最后讲鼠标移置单元格右下角,变为十字后,下拉即可

条 件 格 式

使用条件格式查找重复数据这个功能跟上面的第一个【设置高亮重复项】把重复数据标出的功能有异曲同工之妙。

步骤:

1、用【WPS】打开要处理的表格数据

2、选择需要处理的数据区域

3、在上述【开始】功能栏中选择右方的【条件格式】

4、选择【突出显示单元格规则】→【重复值】,后边可根据自己需要进行选择

办公技能延伸

在我们日常工作中,PDF是受到大家热烈欢迎的一种文件格式,简洁美观,并且不易修改,如果我们想将手中的PDF文件进行PPT幻灯片展示的时候,就会很麻烦。

今天教各位一个方法,将PDF文档转换为PPT文件,就可以进行PPT幻灯片展示了。

5. 如何解决Oracle数据库中重复数据的方法步骤

在平时的开发中,我们经常遇到数据表中出现重复的数据,那么该如何解决呢?这里介绍两种情况下的数据去重方法,一、完全重复数据去重;二、部分字段数据重复去重。

一、完全重复数据去重方法

对于表中完全重复数据去重,可以采用以下SQL语句。

Code

CREATETABLE"#temp"AS (SELECTDISTINCT * FROM 表名);--创建临时表,并把DISTINCT 去重后的数据插入到临时表中

truncateTABLE 表名;--清空原表数据

INSERTINTO 表名(SELECT * FROM"#temp");--将临时表数据插入到原表中

DROPTABLE"#temp";--删除临时表

具体思路是,首先创建一个临时表,然后将DISTINCT之后的表数据插入到这个临时表中;然后清空原表数据;再讲临时表中的数据插入到原表中;最后删除临时表。

二、部分数据去重方法

首先查找重复数据

select 字段1,字段2,count(*) from 表名 groupby 字段1,字段2 havingcount(*) > 1

将上面的>号改为=号就可以查询出没有重复的数据了。

想要删除这些重复的数据,可以使用下面语句进行删除:

deletefrom 表名 a where 字段1,字段2 in

(select 字段1,字段2,count(*) from 表名 groupby 字段1,字段2 havingcount(*) > 1)

上面的语句非常简单,就是将查询到的数据删除掉。不过这种删除执行的效率非常低,对于大数据量来说,可能会将数据库卡死。

基于上述情况,可以先将查询到的重复的数据插入到一个临时表中,然后对进行删除,这样,执行删除的时候就不用再进行一次查询了。如下:

CREATETABLE 临时表 AS

(select 字段1,字段2,count(*) from 表名 groupby 字段1,字段2 havingcount(*) > 1)

下面就可以进行这样的删除操作了:

deletefrom 表名 a where 字段1,字段2 in (select 字段1,字段2 from 临时表);

先建临时表再进行删除的操作要比直接用一条语句进行删除要高效得多。

上面的语句会把所有重复的全都删除,在oracle中,有个隐藏了自动rowid,里面给每条记录一个唯一的rowid,我们如果想保留最新的一条记录,我们就可以利用这个字段,保留重复数据中rowid最大的一条记录就可以了。

下面是查询重复数据的一个例子:

select a.rowid,a.* from 表名 a

where a.rowid !=

(

selectmax(b.rowid) from 表名 b

where a.字段1 = b.字段1 and

a.字段2 = b.字段2

)

上面括号中的语句是查询出重复数据中rowid最大的一条记录。而外面就是查询出除了rowid最大之外的其他重复的数据了。

由此,我们要删除重复数据,只保留最新的一条数据,就可以这样写了:

deletefrom 表名 a

where a.rowid !=

(

selectmax(b.rowid) from 表名 b

where a.字段1 = b.字段1 and

a.字段2 = b.字段2

)

同理,上述代码的执行效率毕竟低,所以我们可以考虑建立临时表,将需要判断重复的字段、rowid插入临时表中,然后删除的时候在进行比较。

createtable 临时表 as

select a.字段1,a.字段2,MAX(a.ROWID) dataid from 正式表 a GROUPBY a.字段1,a.字段2;

deletefrom 表名 a

where a.rowid !=

(

select b.dataid from 临时表 b

where a.字段1 = b.字段1 and

a.字段2 = b.字段2

);

commit;

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

消息中间件是分布式系统常用的组件,无论是异步化、解耦、削峰等都有广泛的应用价值。我们通常会认为,消息中间件是一个可靠的组件——这里所谓的可靠是指,只要我把消息成功投递到了消息中间件,消息就不会丢失,即消息肯定会至少保证消息能被消费者成功消费一次,这是消息中间件最基本的特性之一,也就是我们常说的“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语义的实现上,就是类似这个方案基于数据库的事务特性实现的。更多详情可参考:https://help.aliyun.com/document_detail/102777.html

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

但是这里有它的局限性

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中,具体的使用文档可以参考https://github.com/Jaskey/RocketMQDepListener ,

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

以上代码大部分是原始RocketMQ的必须代码,唯一需要修改的仅仅是创建一个 DepConcurrentListener 示例,在这个示例中指明你的消费逻辑和去重的业务键(默认是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的回滚,使得下次重试消费成功

7. 如何删除数据库中的重复数据

有两个意义上的重复记录

①完全重复的记录,也即所有字段均重复的记录。

②部分关键字段重复的记录,比如Name字段重复,而其他字段不一定重复或都重复可以忽略。


1、对于第一种重复,比较容易解决,使用

selectdistinct*fromtableName


这样就可以得到无重复记录的结果集。然后通过临时表实现对数据的维护。

selectdistinct*into#TmpfromtableName
droptabletableName
select*intotableNamefrom#Tmp
droptable#Tmp

#Tmp为什么系统参数,tableName为要操作的表名。


2、第二类重复问题通常要求保留重复记录中的第一条记录,操作方法如下:

假设有重复的字段为Name,Address,要求得到这两个字段唯一的结果集

selectidentity(int,1,1)asautoID,*into#TmpfromtableName
selectmin(autoID)asautoIDinto#Tmp2from#TmpgroupbyName,autoID
select*from#TmpwhereautoIDin(selectautoIDfrom#tmp2)

最后一个select即得到了Name,Address不重复的结果集(但多了一个autoID字段,实际写时可以写在select子句中省去此列)

8. 接口幂等问题探究

最近遇到一些问题,表单重复提交,导致插入重复数据到数据库,这里查询一些通用的方案,自己都实践一下,以后好回顾。

实践代码项目 Github: https://github.com/h-dj/Spring-Learning/tree/master/repeat-submit

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

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。--- 网络

简单理解:就是针对一个操作,不管做多少次,产生的效果都是一样的。

举例:

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

前端方面:

后台方面:

注意:

以下准备使用加入购物车为例,实现各个方案

分布式锁可以使用 Redis 和 Zookeeper ,更多关于 Redis 和 Zookeeper 的使用 请自行查阅资料。以下使用 Redis 来实现分布式锁

以上是较为常见通用的幂等方案,但实际业务可能比较个性化,需要跟业务结合进行考虑,采用合适的方法或结合使用,例如:

本文作者:JiaJianHuang

本文链接:https://www.cnblogs.com/JianJianHuang/p/15702912.html

9. 如何保证幂等性

数据的对象和范围
你要考虑你的幂等的全局性:空间全局性和时间全局性。
空间全局性:比如是交易流水幂等还是用户ID幂等。是某种类型交易流水幂等,还是某个人|机构|渠道的交易流水幂等
时间全局性:是幂等几秒,还是几分钟,还是永远。
不同的要求,可以有不一样的解决方案、难度和成本。
幂等方案
对时间全局性要求高的,可能就必须选择DB这种持久化方案比较可靠,但是性能不够好啊(然后就要考虑loadmemory,以及数据同步的问题,就一步还要考虑实时性要求了)
在空间的要求中,根据不同的幂等范围,可以考虑分布式数据库(分布式集群全局流水号幂等)。还是某种少量数据幂等(可能只需要单台,做好主备)。

10. Excel中如何去掉重复数据

启动Excel2013,例如有如下表格,发现表格中“学生3”和“学生4”有重复的记录。下面解决如何删除重复的记录。
选中表格中的需要查找重复值的一列或区域的数据。这里选择整个表格为例,选中的效果如下图所示。单击“开始”选项卡,然后单击“样式”组中的“条件格式”中的下三角号,在弹出菜单中依次选择“突出显示单元格规则”—“重复值”。打开“重复值”对话框,
在“为包含以下类型的单元格设置格式”下的,可以选择默认的“重复值”设置为“浅红填充色深红色文本”,然后单击“确定”。这时候可以对数据进行颜色排序和筛选,单击“数据”选项卡,然后单击“排序和筛选”组中的“筛选”。这时可以选择数据标题行的某一列的下三角,然后选择“按颜色排序”—“按单元格颜色排序”中的颜色单元格(上述设置的重复数据的单元格的颜色)。这时发现第4行到第7行都是重复的数据(如下图所示),然后删除掉重复的数据就可以了哦。

阅读全文

与如何解决重复数据幂等相关的资料

热点内容
期货交易机会是什么 浏览:669
通过命令行调用的程序怎么调试 浏览:157
养鸽子的技术教学鸽子如何分公母 浏览:422
如何在火山直播上推广产品 浏览:526
生鱼片市场在哪里 浏览:126
老的程序怎么学 浏览:949
贡小美如何选择私护产品 浏览:600
委托代理什么概念 浏览:672
有了美食和技术还需要什么 浏览:518
丰田有什么技术优点 浏览:361
如何选k40数据线 浏览:930
男装推送优惠券怎么给顾客发信息 浏览:553
欧姆龙有哪些大的代理商 浏览:740
一个保险代理点一年挣多少钱 浏览:263
手游代理为什么会热门 浏览:526
郑州市哪个市场仓库最多聚集 浏览:857
货到了邮政所为什么还没有信息 浏览:861
为什么网店的产品都那么立体 浏览:626
禁止风险代理的案子有哪些 浏览:73
古董交易平台哪里有卖 浏览:298