探讨一下实现幂等性的几种方式

什么是幂等性?

对于同一笔业务操作,不管调用多少次,得到的结果都是一样的。

幂等性设计

我们以对接支付宝充值为例,来分析支付回调接口如何设计?

如果我们系统中对接过支付宝充值功能的,我们需要给支付宝提供一个回调接口,支付宝回调信息中会携带(out_trade_no【商户订单号】,trade_no【支付宝交易号】),trade_no在支付宝中是唯一的,out_trade_no在商户系统中是唯一的。

回调接口实现有以下实现方式。

方式1(普通方式)

过程如下:

1.接收到支付宝支付成功请求

2.根据trade_no查询当前订单是否处理过

3.如果订单已处理直接返回,若未处理,继续向下执行

4.开启本地事务

5.本地系统给用户加钱

6.将订单状态置为成功

7.提交本地事务

上面的过程,对于同一笔订单,如果支付宝同时通知多次,会出现什么问题?当多次通知同时到达第2步时候,查询订单都是未处理的,会继续向下执行,最终本地会给用户加两次钱。

此方式适用于单机其,通知按顺序执行的情况,只能用于自己写着玩玩。

方式2(jvm加锁方式)

方式1中由于并发出现了问题,此时我们使用java中的Lock加锁,来防止并发操作,过程如下:

1.接收到支付宝支付成功请求

2.调用java中的Lock加锁

3.根据trade_no查询当前订单是否处理过

4.如果订单已处理直接返回,若未处理,继续向下执行

5.开启本地事务

6.本地系统给用户加钱

7.将订单状态置为成功

8.提交本地事务

9.释放Lock锁

分析问题:

Lock只能在一个jvm中起效,如果多个请求都被同一套系统处理,上面这种使用Lock的方式是没有问题的,不过互联网系统中,多数是采用集群方式部署系统,同一套代码后面会部署多套,如果支付宝同时发来多个通知经过负载均衡转发到不同的机器,上面的锁就不起效了。此时对于多个请求相当于无锁处理了,又会出现方式1中的结果。此时我们需要分布式锁来做处理。

方式3(悲观锁方式)

使用数据库中悲观锁实现。悲观锁类似于方式二中的Lock,只不过是依靠数据库来实现的。数据中悲观锁使用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,数据会对当前记录加锁,其他线程执行到此行代码的时候,会等待线程A释放锁之后,才可以获取锁,继续后续操作。

2.事物提交时,for update获取的锁会自动释放。

方式3可以正常实现我们需要的效果,能保证接口的幂等性,不过存在一些缺点:

1.如果业务处理比较耗时,并发情况下,后面线程会长期处于等待状态,占用了很多线程,让这些线程处于无效等待状态,我们的web服务中的线程数量一般都是有限的,如果大量线程由于获取for update锁处于等待状态,不利于系统并发操作。

方式4(乐观锁方式)

依靠数据库中的乐观锁来实现。

1.接收到支付宝支付成功请求

2.查询订单信息

select * from t_order where order_id = trade_no;

3.判断订单是已处理

4.如果订单已处理直接返回,若未处理,继续向下执行

5.打开本地事物

6.给本地系统给用户加钱

7.将订单状态置为成功,注意这块是重点,伪代码:

update t_order set status = 1 where order_id = trade_no where status = 0;
//上面的update操作会返回影响的行数num
if(num==1){
 //表示更新成功
 提交事务;
}else{
 //表示更新失败
 回滚事务;
}

注意:

update t_order set status = 1 where order_id = trade_no where status = 0; 是依靠乐观锁来实现的,status=0作为条件去更新,类似于java中的cas操作;关于什么是cas操作,可以移步:什么是 CAS 机制?

执行这条sql的时候,如果有多个线程同时到达这条代码,数据内部会保证update同一条记录会排队执行,最终最有一条update会执行成功,其他未成功的,他们的num为0,然后根据num来进行提交或者回滚操作。

方式4(唯一约束方式)

依赖数据库中唯一约束来实现。

我们可以创建一个表:

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.实现幂等性常见的方式有:悲观锁(for update)、乐观锁、唯一约束

2.几种方式,按照最优排序:乐观锁 > 唯一约束 > 悲观锁

欢迎工作一到五年的Java工程师朋友们加入Java程序员开发: 721575865

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

<think>嗯,用户问的是接口幂等性的定义及实现方法。首先,我需要先理解什么是幂等性。记得之前学过,幂等性是指无论操作执行多少次,结果都保持一致。比如在接口调用中,多次请求和一次请求的效果相同。那用户可能是在开发中遇到了重复提交的问题,比如支付场景,用户点击多次导致多次扣款,这时候就需要接口幂等性保证只处理一次。 接下来,根据提供的引用内容,引用[1]解释了幂等性的定义,特别是在微服务架构中的重要性。引用[2]详细列举了几种实现方法,比如唯一ID、Token机制、去重表、版本控制和状态控制。我需要把这些方法整理清楚,用简单的中文解释给用户。 用户可能还想知道为什么需要幂等性,尤其是在分布式系统和网络不稳定的情况下。比如网络超时导致客户端重试,如果没有幂等性处理,可能会重复处理请求,造成数据不一致或者资金损失。 另外,要注意回答的结构,先定义,再作用,然后实现方法。每个方法要简明扼要,可能还需要举例说明,比如支付订单的状态变化,使用唯一ID来防止重复处理。 需要确保使用正确的LaTeX格式,比如变量用$...$,但看起来用户的问题里没有数学公式,可能不需要。不过按照系统指令,行内表达式需要正确格式,但这里可能用不上,暂时注意一下。 最后,生成相关问题时要围绕幂等性展开,比如应用场景、具体实现技术、与事务的区别、在RESTful API中的实践等。这样用户如果有进一步的问题,可以引导他们深入探讨。 检查一下引用标记是否正确,每个引用要对应到正确的来源,比如引用[^1]和[^2]分别对应提供的引用内容。确保回答中的引用位置自然,比如在解释定义时引用[^1],在方法时引用[^2]。 总结一下,回答的结构应该是: 1. 定义幂等性 2. 作用(为什么重要) 3. 实现方法(列举几种,并简要说明) 4. 相关问题 确保语言口语化,避免技术术语堆砌,让用户容易理解。同时,保持段落清晰,分点说明,方便阅读。</think>### 接口幂等性的定义及作用 **接口幂等性**指在分布式系统中,同一请求被多次调用时,其对系统状态的影响与一次调用相同[^1]。例如:支付接口重复提交时,仅会成功扣款一次,而非多次。 #### 核心作用 1. **防止重复操作**:网络超时、客户端重试等场景下避免数据错误 2. **保证数据一致性**:确保多次调用不会破坏业务逻辑(如订单状态、库存扣减) 3. **提升系统容错性**:降低因重试导致的副作用 --- ### 实现接口幂等性的常用方法 #### 1. 唯一标识(Unique ID) - **原理**:为每个请求生成唯一ID(如UUID、业务字段组合),服务端校验ID是否已处理 - **实现**: ```java if (redis.exists(requestId)) return "重复请求"; else { processRequest(); redis.set(requestId, TTL); } ``` - **适用场景**:订单创建、支付请求 #### 2. Token机制 - **流程**: 1. 客户端先申请临时Token 2. 携带Token发起业务请求 3. 服务端验证Token存在后执行业务,并删除Token - **关键点**:Token需一次性且有时效性 #### 3. 去重表 - **实现**:在数据库建立唯一索引字段(如订单号),插入重复数据时报错 - **示例**: ```sql CREATE TABLE deduplicate ( id VARCHAR(64) PRIMARY KEY, created_at TIMESTAMP ); ``` #### 4. 版本控制(乐观锁) - **方法**:在数据更新时校验版本号 ```sql UPDATE account SET balance=100, version=version+1 WHERE id=123 AND version=5; ``` #### 5. 状态机约束 - **规则**:仅允许特定状态转换(如订单状态从"未支付"到"已支付") ```sql UPDATE orders SET status='paid' WHERE id=456 AND status='unpaid'; ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值