java-幂等性

幂等性

1.1幂等性定义:

在计算机领域中,幂等(Idempotence)是指任意一个操作的多次执行总是能获得相同的结果,不会对系统状态产生额外影响。在Java后端开发中,幂等性的实现通常通过确保方法或服务调用的结果具有确定性,无论调用次数如何,结果都是可预期的。

1.2注意:

在实际的互联网服务开发中,幂等性的理论定义与业务逻辑间的冲突是常见的。
例如,考虑查询操作,当A系统调用B系统的查询接口时,如果首次调用由于B系统中的程序错误而导致业务逻辑失败,即使在程序修复后系统A重新使用相同参数进行重试,B系统可能仍然返回相同的失败响应。尽管这符合幂等性的定义,却与实际业务逻辑不符。同样,以订单支付为例,首次调用由于账户余额不足而返回“余额不足”提示,用户充值后再次使用相同参数发起支付请求,服务仍然返回“余额不足”响应,也符合幂等性的定义,但同样不符合业务逻辑。
因此,在实现幂等性方案时,应该遵循幂等性方案的目标,而不仅仅是严格遵循幂等性的定义。尤其是涉及写操作的服务,应当更关注防止重复请求带来的不良副作用,例如重复扣款或退款。

1.3 什么情况下会出现幂等?

在微服务和分布式架构中,一个请求可能需要多个服务协作才能完成。在这个过程中,网络抖动、系统运行异常等不确定因素使得请求的成功率不可能达到100%,一旦发生失败或未知异常,最常见的处理方式就是重试,而重试必然会导致重复请求问题。

幂等设计主要是为了处理重复请求而生的,好的幂等方案可以保证重复请求获得预期结果,而不产生副作用。

用户不可靠: 用户通过客户端发起请求,由于手抖或有意重复点击,很容易造成导致极短时间内发起多次重复请求。
网络不可靠:网络抖动、网关内部抖动有可能触发重试机制,这个在使用消息队列投递消息时经常会遇到。MQ 消息中间件,消息重复消费;

服务不可靠: 在需要保证数据一致性的场景中,如果调用下游服务超时,在无法确认执行结果的情况下,常用的处理方法是重试。比如:前端调用后端接口发起支付超时,然后再次发起重试,可能会导致多次支付。

1.3 幂等与并发的关系

在具有并发写操作的场景下,通常需要考虑幂等问题。例如,当用户在极短时间内多次提交表单或者使用特殊手段同时提交多个表单时,这就是典型的并发场景,需要进行幂等性处理。为了防止重复请求被执行,服务端需要实施幂等性控制,以避免产生不符合预期的结果。

虽然并发场景大都存在幂等问题,但幂等问题却并非并发场景所特有。幂等设计是为了识别并处理重复请求,而并发仅仅是重复请求的一种特殊情况。 事实上,只要重复请求涉及写操作,无论是否并发,都需要做好幂等处理。举个例子,用户在pc端同时开了两个窗口,间隔10分钟分别提交表单,所有参数完全相同,这显然不属于并发,但仍需要进行幂等处理。


二、幂等性解决方案

这些方案的技术路线可以总结成三条:唯一索引、唯一数据、状态机约束。

  • 唯一索引是指数据库主键、唯一索引,唯一索引大部分是基于业务流水表建立,也可单独建表实现;
  • 唯一数据是指悲观锁、乐观锁、分布式锁等机制;
  • 状态机约束,对于存在状态流转的业务,通过状态机的流转约束,可以实现有限状态机的幂等。

在实际开发中,单独使用这些方法往往效果有限,需要根据具体的业务场景灵活选择、合理的运用上述实现方法。

  1. 数据库:乐观锁、悲观锁、唯一主键、唯一索引
  2. 业务层:分布式锁、下游传递唯一序列号
  3. Token令牌
  4. 状态机

2.1 方案一:数据库唯一主键实现幂等性

在这里插入图片描述
缺点:无法使用change buffer,InnoDB为了进行唯一性检查,必须有一次磁盘IO读页

2.1.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通常会组合成一个联合索引,这样做能有效避免在并发情况下的数据重复插入问题,从而保障了业务操作的幂等性。

2.2 方案二:数据库乐观锁实现幂等性

乐观锁主要依靠带条件更新 来确保多次外部请求的一致性。在系统设计中,可以在数据表中添加版本号字段,用于标识当前数据的版本。每次对该数据表的记录进行更新时,都需要提供上一次更新的版本号,示例操作如下:

//1. 取出要更新的对象,带有版本versoin
select * from tablename where id = xxx//2. 更新数据
update tableName set sq = sq-#{quantity},version = #{version}+1 where id = xxx and version=#{version}

在这里插入图片描述
特点:乐观锁主要适用于更新场景,确保多次更新不会影响结果的一致性。
缺点:操作业务前,需要先查询出当前的version版本。会增加操作

2.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;

特点:
select for update,整个执行过程中锁定该条记录
缺点:
在DB读大于写的情况下尽量少用。悲观锁主要适用于更新场景,通过串行化请求处理来确保幂等性,但需要小心使用,因为在并发场景下,重复请求可能会导致线程长时间处于等待状态,浪费资源且降低性能。

2.4 方案四:业务层采用分布式锁机制

分布式锁与悲观锁本质上相似,都通过串行化请求处理来实现幂等性。与悲观锁不同的是,分布式锁更轻量。在系统接收请求后,首先尝试获取分布式锁。如果成功获取锁,则执行业务逻辑;如果获取失败,则立即拒绝请求。
在这里插入图片描述
分布式锁的核心是识别重复请求,实现串行化处理。但要注意,获取锁成功后,业务逻辑的执行并没有可靠保证。因此,在实际应用中,分布式锁需要结合事务机制和重试机制,以形成完整的幂等性解决方案。

2.5方案五:防重 Token 令牌实现幂等性

2.5.1 流程:

1)当用户访问表单页面时,客户端请求服务端接口以获取唯一的Token(可以是UUID或全局ID),服务端生成的Token会被存储在Redis或数据库中。
2)用户首次提交表单时,将Token与表单一起发送至服务端,服务端会验证Token的存在性,如果Token存在,则执行业务逻辑,并在完成后销毁Token。
3)用户再次提交表单时,同样携带Token一起发送至服务端。但由于Token已被销毁,服务端无法找到对应的Token,从而拒绝重复提交请求。
在这里插入图片描述
在这里插入图片描述

2.5.2实现:

(1)集群环境:token+redis
(2)单jvm环境:token+redis 或者token+jvm内存

2.5.3 Token特点

== 要申请,一次有效性,可以限流 ==

2.5.4缺点:

(1)产生过多额外请求
(2)先删除token,如果业务处理出现异常但token已经删除掉了,再来请求会被认定为重复请求
后删除token,如果删除redis中的token失败了,再来请求不会拦截,发生了重复请求
无论是先删除token还是后删除token,都会导致每次业务请求都产生一个额外的请求去获取token。然而,在生产环境中,业务失败或超时的情况并不多见,大多数请求都能成功完成。因此,为了处理这少数失败的请求,让绝大多数请求都产生额外的请求也算是一种资源的浪费。

2.5.5 存在问题:删除token时,是先完成业务操作后删除token,还是先删除token后执行业务操作呢?

答案:要先删除 token ,再执行业务代码 。『后删除 token』的缺陷太致命
(1)先执行业务操作再删除token
情况:在高并发下,可能出现第一次访问时token存在,完成具体业务操作,但在还没有删除token时,客户端又携带token发起请求。此时,因为token还存在,第二次请求也会验证通过,执行具体业务操作。
对于这个问题有如下两种解决方案:
第一种方案: 对于业务代码执行和删除token整体加线程锁,使得后续线程阻塞排队,但可能造成一定性能损耗与吞吐量降低。
第二种方案: 借助Redis单线程和INCR原子性特性,在获取token时对其进行自增操作。当客户端携带token访问执行业务代码时,继续对其进行自增,如果自增后的返回值为2,则是一个合法请求允许执行,否则认为是非法请求,直接返回。
在这里插入图片描述

(2)先删除token再执行业务
如果业务执行超时或失败,没有向客户端返回明确结果,客户端就会进行重试,但此时之前的token已经被删除,导致被认为是重复请求,不再进行业务处理。
在这里插入图片描述

这种方案无需额外处理,一个token只能代表一次请求。一旦业务执行出现异常,则让客户端重新获取令牌,重新发起一次访问即可。
先删除token,再执行业务逻辑,中间如果出现宕机,可能会导致业务调用失败,对于这种情况,大不了就重新获取token再次请求

2.6 方案六:状态机机制

在许多业务单据中,存在有限数量的状态,并且这些状态之间的流转顺序是固定的。如果状态已经处于下一个状态,那么再次应用上一个状态的变更逻辑是不会产生任何效果的,这就确保了有限状态机的幂等性。
例如,库存状态通常包括"预扣中"、“扣减中”、“占用中"和"已释放"等状态。如果系统重复调用扣减接口,而库存状态已经是"扣减中”,则可以直接返回结果。
状态机可以与乐观锁机制结合使用,示例操作如下:

update tableName set sq=sq-#{quantity},status=#{udpate_status} where id =#{id} and status=#{status}

特点:和任务、状态相关的业务,肯定会涉及状态机,业务的一个属性状态,可以作为幂等的一个根据

2.7方案七:下游传递唯一序列号实现幂等性

在这里插入图片描述
缺点:无法控制下游唯一序列号的生成规则,如果序列号由时间戳生成,那么无法拦截类似重复点击这种情况下的重复请求

3.1 补充 redis幂等性

3.1.1 redis幂等性

在Redis中,幂等性是指相同的操作可以被多次执行而不会产生额外的影响或副作用。简而言之,就是无论执行多少次相同的操作,结果都是一样的。 在Redis中,可以通过以下几种方式来实现redis的幂等性:
(1)使用Redis的原子性操作:Redis提供了一些原子性操作,如SETNX、INCR、SADD等。 这些操作在执行时是原子性的,即是一个操作的结果要么成功执行,要么没有执行。通过使用这些原子性操作,可以保证相同的操作在执行时只会生效一次。
(2)使用Redis的事务:Redis的事务可以将一系列的操作包装在一个事务中,然后一起执行。在事务执行期间,其他客户端的请求不会干扰到事务的执行。通过将幂等操作放在一个事务中执行,可以保证这些操作只会被执行一次。
(3)使用Redis的分布式锁:通过使用Redis的分布式锁,可以保证同一时间只有一个客户端可以执行特定的操作。当一个客户端获取到锁后,其他客户端尝试获取锁的操作会被阻塞,直到锁被释放。通过使用分布式锁,可以保证相同的操作只会被执行一次。

总结起来,Redis中可以通过原子性操作、事务和分布式锁等方式来实现redis的幂等性。 这样可以保证相同的操作在执行时不会产生额外的影响或副作用。

3.1.2 redis SETNX分布锁详解

1.SETNX:向Redis中添加一个key,只用当key不存在的时候才添加并返回1,存在则不添加返回0。并且这个命令是原子性的。
2. 使用SETNX作为分布式锁时,添加成功表示获取到锁,添加失败表示未获取到锁。至于添加的value值无所谓可以是任意值(根据业务需求),只要保证多个线程使用的是同一个key,所以多个线程添加时只会有一个线程添加成功,就只会有一个线程能够获取到锁。而释放锁锁只需要将锁删除即可。
3.设置过期时间防止死锁
在添加时存在则添加,不存在则不添加。同时设置过期时间,单位秒
示例:

/*** 计算结果写入到kafka幂等性的实现** @param json* @return*/public boolean idempotent(String json) {try {RedisPool redisPool = RedisPool.instance(properties);Jedis jedis = redisPool.getResource();String value = "1";if (jedis.setnx(json, value) > 0) {jedis.expire(json, 24 * 3600);redisPool.returnResource(jedis);return true;}redisPool.returnResource(jedis);} catch (Exception e) {System.err.println("redis pool get redis failed: " + json);}return false;
}  

2.防止死锁

SET key value NX EX time//通过java代码实现SETNX同时设置过期时间 //key--键 value--值 time--过期时间 TimeUnit--时间单位枚举
stringRedisTemplate.opsForValue().setIfAbsent(key, value , time, TimeUnit); 

优点:
1.程序可以分组,可以分布式,亦可以用于数据恢复程序
2.减少了对redis操作频度,提高了程序的并发性.

3.2 参考文章:

https://mp.weixin.qq.com/s/7YDtl8EfYvre49Al9yVZIw
https://blog.csdn.net/sinat_32023305/article/details/119610885
https://blog.csdn.net/q7w8e9r4/article/details/132533849

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/718118.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

设计模式(十四)中介者模式

请直接看原文: 原文链接:设计模式(十四)中介者模式_设计模式之中介模式-CSDN博客 -------------------------------------------------------------------------------------------------------------------------------- 前言 写了很多篇设计模式的…

Ribbon实现Cloud负载均衡

安装Zookeeper要先安装JDK环境 解压 tar -zxvf /usr/local/develop/jdk-8u191-linux-x64.tar.gz -C /usr/local/develop 配置JAVA_HOME vim /etc/profile export JAVA_HOME/usr/local/develop/jdk1.8.0_191 export PATH$JAVA_HOME/bin:$PATH export CLASSPATH.:$JAVA_HOM…

Windows Server 各版本搭建文件服务器实现共享文件(03~19)

一、Windows Server 2003 打开服务器,点击左下角开始➡管理工具➡管理您的服务器➡添加或删除角色 点击下一步等待测试 勾选自定义配置,点击下一步 选择文件服务器,点击下一步 勾选设置默认磁盘空间,数据自己更改,最…

【JavaEE】_Spring MVC 项目传参问题

目录 1. 传递单个参数 1.1 关于参数名的问题 2. 传递多个参数 2.1 关于参数顺序的问题 2.2 关于基本类型与包装类的问题 3. 使用对象传参 4. 后端参数重命名问题 4.1 关于RequestPara注解 1. 传递单个参数 现创建Spring MVC项目,.java文件内容如下&#xff…

Apache Flink连载(三十七):Flink基于Kubernetes部署(7)-Kubernetes 集群搭建-3

🏡 个人主页:IT贫道-CSDN博客 🚩 私聊博主:私聊博主加WX好友,获取更多资料哦~ 🔔 博主个人B栈地址:豹哥教你学编程的个人空间-豹哥教你学编程个人主页-哔哩哔哩视频 目录

AI-数学-高中-32-概率-样本空间与随机事件

原作者视频:【概率】【一数辞典】1样本空间与随机事件_哔哩哔哩_bilibili 1.随机试验: 2.样本点、样本空间、有限样本空间: 示例1 示例2 3.事件: 示例:

自己本地模拟内存数据库增删改查

目录 学习初衷准备代码实现结果感谢阅读 学习初衷 用于满足自己的测试要求,不连接数据库,也不在意数据丢失 准备 maven依赖 org.springframework.boot spring-boot-starter-test test 代码实现 内存数据库(InMemoryDatabase&#xff0…

[AutoSar]BSW_Com08 CAN driver 模块介绍及参数配置说明 (二)

目录 关键词平台说明一、CanControllers二、CanTxProcessing三、CanFilterMask四、CanHardwareObjects五、CanGeneral 关键词 嵌入式、C语言、autosar、OS、BSW 平台说明 项目ValueOSautosar OSautosar厂商vector ,芯片厂商TI 英飞凌编程语言C,C编译器…

游戏引擎分层简介

游戏引擎分层架构(自上而下) 工具层(Tool Layer) 在一个现代游戏引擎中,我们最先看到的可能不是复杂的代码,而是各种各样的编辑器,利用这些编辑器,我们可以制作设计关卡、角色、动画…

数据类型和变量

1.数据类型 在Java中数据类型主要分为两类:基本数据类型和引用数据类型。 基本数据类型有四类八种: 1. 四类:整型、浮点型、字符型以及布尔型 2.八种: 整形是分为如上四种 byte short int long 浮点型分为 float 和double …

【大厂AI课学习笔记NO.64】机器学习开发框架

机器学习开发框架本质上是一种编程库或工具,目的是能够让开发人员更容易、更快速地构建机器学习模型。 机器学习开发框架封装了大量的可重用代码,可以直接调用,目的是避免“重复造轮子’大幅降低开发人员的开发难度,提高开发效率…

Spring框架精髓:带你手写IoC

个人名片: 🐼作者简介:一名大三在校生,喜欢AI编程🎋 🐻‍❄️个人主页🥇:落798. 🐼个人WeChat:hmmwx53 🕊️系列专栏:🖼️…

足球青训俱乐部|基于Springboot的足球青训俱乐部管理系统设计与实现(源码+数据库+文档)

足球青训俱乐部管理系统目录 目录 基于Springboot的足球青训俱乐部管理系统设计与实现 一、前言 二、系统设计 1、系统架构设计 三、系统功能设计 1、管理员登录界面 2、公告信息管理界面 3、学员管理界面 4、商品信息管理界面 5、课程安排管理界面 四、数据库设计…

ArcGIS Runtime For Android开发之符号化和图层渲染

一、用Symbol对要素进行符号化 首先我们看一下Symbol 接口关系: 1、SimpleFillSymbol 他是用来进行简单的Graphic面要素填充符号化的,它可以设置要素的填充颜色,边线颜色、线宽,其用法如下: Polygon polygonnew Po…

常用的电阻、电容的种类和应用场合?

电阻的 a.按阻值特性:固定电阻、可调电阻、特种电阻(敏感电阻),不能调节的,我们称之为固定电阻,而可以调节的,我们称之为可调电阻.常见的例如收音机音量调节的,主要应用于电压分配的,我们称之为电位器. b.按制造材料:碳膜电阻、金属膜电阻、线绕电阻,捷…

二叉树叶节点个数,根节点个数,树的深度,查找数据为x的节点

文章目录 一、计算二叉树叶节点个数二、叶节点的个数 引言:补充树的概念 节点的度:一个节点含有的子树的个数称为节点的度 叶节点或终端节点:度为0的节点称为叶节点 节点的层次:从根开始为第一层,以此类推 树的度&…

【Godot 4.2】Tree控件与TreeItem完全解析

概述 本篇是控件完全解析系列之一,主要总结一下Tree控件与TreeItem的使用。 Tree控件是一个非常强大的控件,尤其是在编写一些相关的程序或编辑器插件时,非常适合展示树形组织的节点型数据。 本篇将从简单的添加根节点,根节点子…

uniapp和vue项目配置多语言,实现前端切换语言

在uniapp中配置多语言功能,实现前端切换语言,可以按照以下步骤进行: 1. 创建语言包 首先,创建一个名为 lang 的目录,并在该目录下为每种支持的语言创建对应的JSON或JS文件。例如: lang/en.js&#xff08…

Microsoft PyRIT能自动化完成AI红队的任务

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领…

《异常检测——从经典算法到深度学习》26 Time-LLM:基于大语言模型的时间序列预测

《异常检测——从经典算法到深度学习》 0 概论1 基于隔离森林的异常检测算法 2 基于LOF的异常检测算法3 基于One-Class SVM的异常检测算法4 基于高斯概率密度异常检测算法5 Opprentice——异常检测经典算法最终篇6 基于重构概率的 VAE 异常检测7 基于条件VAE异常检测8 Donut: …