1. 幂等概述
1.1 什么是幂等性
在计算机领域中,幂等(Idempotence)是指任意一个操作的多次执行总是能获得相同的结果,不会对系统状态产生额外影响。在Java后端开发中,幂等性的实现通常通过确保方法或服务调用的结果具有确定性,无论调用次数如何,结果都是可预期的。
上面的定义是目前大多数文章和书籍对幂等的描述,然而,在实际的Java 项目开发中,幂等性的理论定义与业务逻辑间的冲突是常见的。
例如,考虑查询操作,当A系统调用B系统的查询接口时,如果首次调用由于B系统中的程序错误而导致业务逻辑失败,即使在程序修复后系统A重新使用相同参数进行重试,B系统可能仍然返回相同的失败响应。尽管这符合幂等性的定义,却与实际业务逻辑不符。同样,以订单支付为例,首次调用由于账户余额不足而返回“余额不足”提示,用户充值后再次使用相同参数发起支付请求,服务仍然返回“余额不足”响应,也符合幂等性的定义,但同样不符合业务逻辑。
因此,在实现幂等性方案时,应该遵循幂等性方案的目标,而不仅仅是严格遵循幂等性的定义。尤其是涉及写操作的服务,应当更关注防止重复请求带来的不良副作用,例如重复扣款或退款。
1.2 幂等性的必要性
在微服务和分布式架构中,一个请求可能需要多个服务协作才能完成。在这个过程中,网络抖动、系统运行异常等不确定因素使得请求的成功率不可能达到100%,一旦发生失败或未知异常,最常见的处理方式就是重试,而重试必然会导致重复请求问题。
幂等设计主要是为了处理重复请求而生的,好的幂等方案可以保证重复请求获得预期结果,而不产生副作用。 在实际开发中,以下场景会产生重复请求:
- 用户不可靠:用户通过客户端发起请求,由于手抖或有意重复点击,很容易造成导致极短时间内发起多次重复请求。
- 网络不可靠:网络抖动、网关内部抖动有可能触发重试机制,这个在使用消息队列投递消息时经常会遇到。
- 服务不可靠:在需要保证数据一致性的场景中,如果调用下游服务超时,在无法确认执行结果的情况下,常用的处理方法是重试。
1.3 幂等与并发的关系
在具有并发写操作的场景下,通常需要考虑幂等问题。例如,当用户在极短时间内多次提交表单或者使用特殊手段同时提交多个表单时,这就是典型的并发场景,需要进行幂等性处理。为了防止重复请求被执行,服务端需要实施幂等性控制,以避免产生不符合预期的结果。
虽然并发场景大都存在幂等问题,但幂等问题却并非并发场景所特有。幂等设计是为了识别并处理重复请求,而并发仅仅是重复请求的一种特殊情况。 事实上,只要重复请求涉及写操作,无论是否并发,都需要做好幂等处理。举个例子,用户在pc端同时开了两个窗口,间隔10分钟分别提交表单,所有参数完全相同,这显然不属于并发,但仍需要进行幂等处理。
在Java项目开发过程中,并发处理与幂等性问题紧密相关,这也导致了一些人认为解决幂等性就是解决高并发的问题。
2. 幂等号的设计
幂等性设计的目的是确保即使在多次接收相同请求的情况下,也只执行一次操作,防止重复处理。要实现这一点,通常需要事先约定一个具有唯一性的标识符,如Token或业务流水号,我们称之为幂等号(Idempotency Key)。
幂等号有三个关键特性:唯一性、不变性和传递性。
唯一性确保每个请求都能被准确识别,不变性保证在请求处理期间幂等号保持不变,传递性则确保在多系统处理同一请求时,幂等号能够被传递和保持。
幂等号通常有两种设计方式:
-
非业务幂等号:通过唯一标识符(如UUID、时间戳或业务流水号)在调用方和被调用方之间明确实现幂等性。由于非业务幂等号难以通过业务上下文追溯,因此调用双方都必须将其持久化,从而保证请求与幂等号的关系有迹可循。
-
业务幂等号:由业务元素组合构成的幂等号,如“用户ID+活动ID”。使用此方法时,调用方无需单独持久化幂等号,被调用方可以根据请求参数和业务上下文直接获取并组合这些参数。例如,通过设置“用户ID”和“活动ID”的联合唯一索引来实现幂等性。
3. 幂等的实现方案
幂等性的实现关键在于确保相同的请求仅被处理一次,这通常可以通过设置唯一性约束和检查来实现。实践中有六种常见的方案:唯一索引、Token机制、悲观锁、乐观锁、分布式锁和状态机。
3.1 唯一索引方案机制
唯一索引方案依赖于数据库表中不允许存在具有相同索引值的重复行。这种策略在关系型数据库中广泛支持,并且能有效利用唯一性约束来确保幂等性。在高并发场景中,唯一索引能保证当多个线程尝试同时插入相同记录时,只有一个线程能成功执行,而其他线程将会因违反唯一性约束而抛出异常。
通常,业务流水表的建立是基于以下核心字段:
id
(bigint 类型):作为主键,唯一标识每条记录。gmt_create
(datetime 类型):记录的创建时间。gmt_modified
(datetime 类型):记录的最后修改时间。user_id
(varchar(32) 类型):用户ID,这个字段也可以作为分表的依据。out_biz_no
(varchar(64) 类型):外部业务流水号,即调用方的幂等号。biz_no
(varchar(64) 类型):内部业务流水号,用于系统内部追踪。status
(char(1) 类型):记录执行状态。
在这种设计中,user_id
和out_biz_no
通常会组合成一个联合索引,这样做能有效避免在并发情况下的数据重复插入问题,从而保障了业务操作的幂等性。
3.2 Token机制
Token机制是用于防止客户端重复提交的一种特殊机制,特别适用于客户端创建订单等提交表单场景。
其执行流程如下:
- 当用户访问表单页面时,客户端请求服务端接口以获取唯一的Token(可以是UUID或全局ID),服务端生成的Token会被存储在Redis或数据库中。
- 用户首次提交表单时,将Token与表单一起发送至服务端,服务端会验证Token的存在性,如果Token存在,则执行业务逻辑,并在完成后销毁Token。
- 用户再次提交表单时,同样携带Token一起发送至服务端。但由于Token已被销毁,服务端无法找到对应的Token,从而拒绝重复提交请求。
3.3 悲观锁机制
悲观锁依赖数据库提供的锁机制来实现,整个数据处理过程中,数据处于锁定状态,并与事务机制配合,能够有效实现业务幂等性。操作示例如下:
// 1. 开启事务
begin;
// 2. 基于幂等号查询
record = select * from tbl_xxx where out_biz_no = 'xxx' for update;
// 3. 根据状态进行决策
if(record.getStatus() != 预期状态){
return;
}
// 4. 更新记录
update tbl_xxx set status = '目标状态' where out_biz_no = 'xxx';
// 5. 提交事务
commit;
悲观锁主要适用于更新场景,通过串行化请求处理来确保幂等性,但需要小心使用,因为在并发场景下,重复请求可能会导致线程长时间处于等待状态,浪费资源且降低性能。
3.4 乐观锁机制
乐观锁主要依靠"带条件更新"(update with condition)来确保多次外部请求的一致性。在系统设计中,可以在数据表中添加版本号字段,用于标识当前数据的版本。每次对该数据表的记录进行更新时,都需要提供上一次更新的版本号,示例操作如下:
//1. 取出要更新的对象,带有版本versoin
select * from tablename where id = xxx
//2. 更新数据
update tableName set sq = sq-#{quantity},version = #{version}+1 where id = xxx and version=#{version}
乐观锁主要适用于更新场景,确保多次更新不会影响结果的一致性。
3.5 分布式锁机制
分布式锁与悲观锁本质上相似,都通过串行化请求处理来实现幂等性。与悲观锁不同的是,分布式锁更轻量。在系统接收请求后,首先尝试获取分布式锁。如果成功获取锁,则执行业务逻辑;如果获取失败,则立即拒绝请求。
分布式锁的核心是识别重复请求,实现串行化处理。但要注意,获取锁成功后,业务逻辑的执行并没有可靠保证。因此,在实际应用中,分布式锁需要结合事务机制和重试机制,以形成完整的幂等性解决方案。
3.6 状态机机制
在许多业务单据中,存在有限数量的状态,并且这些状态之间的流转顺序是固定的。如果状态已经处于下一个状态,那么再次应用上一个状态的变更逻辑是不会产生任何效果的,这就确保了有限状态机的幂等性。
例如,库存状态通常包括"预扣中"、"扣减中"、"占用中"和"已释放"等状态。如果系统重复调用扣减接口,而库存状态已经是"扣减中",则可以直接返回结果。
状态机可以与乐观锁机制结合使用,示例操作如下:
update tableName set sq=sq-#{quantity},status=#{udpate_status} where id =#{id} and status=#{status}
总结
以上介绍了六种实现幂等性的方式,并简要介绍了每种方式适用的场景和关键信息。这些方式可以总结为三个技术路线:唯一索引、唯一数据和状态机约束。
- 唯一索引:指的是数据库的唯一索引,通常基于业务流水表创建,也可以单独创建表来实现。
- 唯一数据:包括悲观锁、乐观锁、分布式锁等机制。
- 状态机约束:适用于具有状态流转的业务,通过状态机的流转约束,可以实现有限状态机的幂等性。
然而,需要注意的是,在实际开发中,单独使用这些方法往往效果有限。 例如,悲观锁和分布式锁只是将请求串行处理,对于异常情况的重试并没有足够的防御能力,因此需要结合唯一索引来实现完整的幂等性解决方案。同样,唯一索引方案也需要与事务机制结合使用。因此,在实际应用中,需要根据具体的业务场景灵活选择、合理的运用上述实现方法。
标签:请求,索引,重复,业务,接口,并发,Token,开发,保证 From: https://www.cnblogs.com/binbingg/p/18027445