实现接口幂等性的8种解决方案

古语云:“一而再,再而三,其效不二”
俗语讲:被虐千百遍,依然如初恋
数学符号:f(f(f(x))) = f(x)
即无论操作执行一次还是多次,其效果始终如一,不会有差异。这就是幂等性。

文章导读

图片

什么是幂等性?

  接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。比如:公交车刷卡,用户上车后刷码支付扣款成功,如果用户再次点击按钮刷卡并扣款成功,用户查询余额返发现多扣钱了,流水记录也变成了两条,这就没有保证接口的幂等性。因此,当你重复刷卡时,会提示:刷码重复。

注意:数据库可能产生幂等性问题,但是幂等性问题不只发生在数据库。

什么场景需要幂等设计?

一般对数据要求比较高的场景,如:金钱交易、数据一致性至关重要的业务场景:

  1. 在线支付:当用户发起支付请求时,避免重复扣款。
  2. 银行交易:确保同一笔交易不会因网络重试等原因被执行多次。
  3. 票务系统:在线购票平台在用户购票时,检查所选座位是否已被重复预订。
  4. 通信服务:如短信或通话服务,系统会检查是否已为相同内容的请求计费。
  5. 任务调度:在定时任务或批处理系统中,确保不会因为任务重启或重试而重复执行相同的操作。
  6. 用户注册:防止因重复提交表单而导致用户信息被创建多次。

如何产生幂等问题?

产生幂等性问题的原因主要有:

  1. 网络请求重试:网络波动或超时,客户端可能会重复发送相同的请求。
  2. 用户界面重复提交:用户在用户界面上可能会不小心重复点击按钮,导致相同的请求被发送多次。
  3. 消息队列重试机制:使用消息队列(如Kafka、RabbitMQ)时,消息可能会被重复消费。
  4. 数据库并发操作:数据库的插入、更新和删除操作多个事务同时修改同一条记录,而没有使用适当的锁机制或事务隔离级别。
  5. 外部系统API接口重试:对外提供的API接口可能由于调用方的重试逻辑,导致数据库操作被重复调用。
  6. 其它......

下面我们简单做些案例说明。

表设计(前期准备)

我们先来设计一张订单表并模拟一些数据:

1、表结构:

图片

2、字段说明:

  1. order_id:作为订单的唯一标识,通常是一个全局唯一的ID,如使用UUID或分布式ID生成器(如Snowflake算法)生成。

  2. user_id:标识下单的用户,用于关联用户信息。

  3. product_id:标识被购买的商品,用于关联商品信息。

  4. quantity:购买的商品数量。

  5. order_status:订单当前状态,用于控制订单的业务流程,确保幂等性。例如,只有当订单状态为“待支付”时,支付操作才会被执行。

  6. create_time:记录订单创建的时间戳。

  7. pay_time:记录订单支付的时间戳,如果订单被支付,这个字段会被更新。

  8. version:乐观锁的版本号,每次更新操作都会增加该字段的值,用于检测在业务处理期间订单是否被其他事务更新过。

3、业务规则:

  • 订单支付:在支付操作前,先检查order_status是否为“待支付”,若是,则执行支付逻辑,并更新order_status为“已支付”;如果不是,则拒绝支付,保持订单状态不变。

  • 订单取消:在取消操作前,同样检查order_status,只有订单在特定状态下才允许取消操作。

  • 插入订单:使用order_id作为唯一约束,防止重复插入相同订单。

  • 乐观锁:在更新订单状态时,使用version字段来确保在读取和更新之间没有其他事务更改了订单,如果读取的version和数据库中的version不一致,则拒绝更新。

4、数据状态

图片

幂等性解决方案

幂等性设计方案通常在分布式系统中,常见的幂等性设计方案如下:

1、唯一性约束

利用数据库的唯一性约束,如唯一索引或主键,来避免插入重复数据。

mysql> INSERT INTO `mydb`.`orders` (`order_id`, `user_id`, `product_id`, `quantity`, `order_status`, `create_time`, `pay_time`, `version`) VALUES ('ORD-20231023-0001', 'USR-A123456', 'PRD-X123', 2, 0, '2023-10-23 10:15:30', NULL, 1);
ERROR 1062 (23000): Duplicate entry 'ORD-20231023-0001' for key 'orders.PRIMARY'

注意:业务上要求生成全局唯一的主键。且不是自增策略,否则在分库分表的场景下,不同的表之间主键互不关联。

2. 乐观锁

  通过记录数据的版本号时间戳,仅当数据未被其他事务修改时,才允许更新操作执行。每次更新数据时,版本号都会递增。

UPDATE orders
SETquantity = 1,order_status = 1,pay_time = '2024-04-30 10:20:00',version = version + 1
WHEREorder_id = 'ORD-20231023-0001' ANDversion = 1;

效果演示:

图片

如果 Session-01 已经提交了事务,Session-02 的更新操作将不会影响任何行,因为 version 已经从 1 增加到了 2。

3. 悲观锁

  使用悲观锁,事务在读取数据时会锁定相应的数据行,直到事务结束(提交或回滚)。这可以防止其他事务在锁定期间修改这些数据,从而确保数据的一致性。

在执行读取操作时,使用 SELECT ... FOR UPDATE 语句来锁定相关记录。

-- 锁定记录
SELECT * FROM orders WHERE order_id = 'ORD-20231023-0001' FOR UPDATE;-- 执行业务逻辑UPDATE orders SET quantity = 1, order_status = 1, pay_time = '2023-10-23 10:20:00' WHERE order_id = 'ORD-20231025-0003';

效果演示:

图片

  由此可见,悲观锁确保每个事务也能安全地执行,而不会导致数据不一致的问题。但是,悲观锁可能会因为锁定机制而导致 性能问题 ,尤其是在高并发的系统中,这可能会引起 锁争用和死锁 

4. 分布式锁

在分布式系统中,使用分布式锁来保证同一时间只有一个实例处理特定消息或请求。

图片

当前使用redis分布式锁案例实现,

public class MyService {private final RedisDistributedLock lock;public MyService(Jedis jedis, String lockKey, int lockTimeout) {this.lock = new RedisDistributedLock(jedis, lockKey, lockTimeout);}public void executeInLock() {if (lock.tryLock()) {try {// 执行业务逻辑} finally {lock.unlock();}} else {// 处理无法获取锁的情况,例如重试或记录日志}}
}

这里顺便提一句,建议采用Lua脚本实现删除锁的逻辑,保证原子性。

public void unlock() {// 释放锁,使用Lua脚本来确保原子性String unlockScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +"return redis.call('del', KEYS[1]) " +"else " +"return 0 " +"end";jedis.eval(unlockScript, 1, lockKey, "1");
}

5. Token令牌机制

  为每个请求生成一个唯一的Token,并在服务端进行校验,一旦处理了对应的请求,就丢弃该Token,避免重复处理。具体步骤:

图片

1、服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的, 就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。

2、然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。

3、服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业务。

4、如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行。

核心逻辑:

// 服务端接口,接收请求并处理token
void do(String token) {if (Redis.exists(token)) {// 删除token,确保不会重复处理Redis.del(token); // 执行具体的业务操作doSometing(); } else {log.info(token); }
}

注意:最好设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求。可以在 redis 使用 lua 脚本完成这个操作

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

6. 状态机

  使用状态机是判断业务流程,确保操作只执行一次。

状态机设计:

  1. 订单创建:订单初始化,状态为 PENDING(待支付)。

  2. 支付操作:当订单状态为 PENDING 时,允许执行支付操作,支付成功后状态变为 PAID(已支付)。

  3. 重复支付检查:如果再次尝试支付一个已经是 PAID 状态的订单,状态机将拒绝该操作,保持订单状态不变。

实现案例

public enum OrderStatus {PENDING, PAID, CANCELLED
}public class Order {private OrderStatus status; // 订单当前状态// 其他订单属性...public Order() {this.status = OrderStatus.PENDING; // 初始化状态为待支付}// 执行支付操作public synchronized void pay() {if (this.status == OrderStatus.PENDING) {// 执行支付逻辑,如减少库存、扣款等this.status = OrderStatus.PAID; // 状态转变为已支付} else {// 如果订单不是在待支付状态,抛出异常或记录日志throw new IllegalStateException("Order can only be paid when status is PENDING");}}// 其他业务逻辑...
}

幂等性保证:

  • 支付操作 pay 在订单状态不是 PENDING 时不会被执行,从而保证了幂等性。

  • 如果有重复的支付请求,由于状态机的保护,第二次及后续的支付请求将不会改变订单状态,因此不会执行重复的支付逻辑。

7. 去重表

  记录已经处理过的请求标识,对于重复的请求直接返回结果,而不再次执行业务逻辑。

1、去重表结构设计

表字段至少包括:

  • 请求标识符:唯一标识一次请求。

  • 创建时间:记录请求的时间戳。

  • 处理状态:标识请求是否已处理,以及处理的结果。

2、设置过期策略

  为了防止去重表无限增长,表中的记录可以设置过期时间。使用定时任务定期清理旧的请求记录。

实现案例:

1、检查去重表

在执行业务逻辑之前,检查去重表确定该请求是否已经被处理过。

boolean isDuplicate = checkDuplicateInDatabase(requestId);

2、处理请求

if (isDuplicate) {// 返回之前的结果或拒绝处理return previousResult;
} else {// 执行业务逻辑doSomthing();// 记录去重表saveRecord(requestId);// 返回新的结果return newResult;
}

注意事项:

  • 数据一致性:确保去重表的更新与业务逻辑的执行保持一致性,避免出现数据不一致的情况。

8. 全局请求唯一ID

调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。

可以使用 nginx 设置每一个请求的唯一 id;

proxy_set_header X-Request-Id $request_id;

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

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

相关文章

FPGA-PLL IP核的使用

1.前言 IP核是使用FPGA进行快速开发的一大法宝,FPGA有几个常用的IP核,如今天要写的PLL,即锁相环,还有FIFO,ROM等。熟练使用这些IP核,在一一些大型的项目中会省很多的精力,今天就通过一个实例来…

55 、mysql的存储引擎、备份恢复以及日志备份、恢复

一、数据库的存储引擎: 1.1、存储引擎的概念 概念:存储引擎,就是一种数据库存储数据的机制,索引的机制,索引的技巧,锁定水平。 存储的方式和存储的格式。 存储引擎也属于mysql当中的组件,实…

Linux挂载磁盘目录

一、背景 由于安装oracle数据库,磁盘空间不足,已经加了存储,但是没有挂载,需要将/dev/sdb全部挂载到/oracle目录下 [rootdatabase-001 ~]# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT sda 8:0 0 50G 0 disk …

HTTPServer改进思路2(mudou库核心思想融入)

mudou网络库思想理解 Reactor与多线程 服务器构建过程中,不仅仅使用一个Reactor,而是使用多个Reactor,每个Reactor执行自己专属的任务,从而提高响应效率。 首先Reactor是一种事件驱动处理模式,其主要通过IO多路复用…

Linux实用操作三

文章目录 Linux实用操作三网络传输ping命令介绍:示例: wget命令介绍:示例: curl命令介绍:示例: 端口介绍:端口的划分:查看端口占用: 进程管理进程介绍:查看进…

基于WebGoat平台的SQL注入攻击

目录 引言 一、安装好JAVA 二、下载并运行WebGoat 三、注册并登录WebGoat 四、模拟攻击 1. 第九题 2. 第十题 3. 第十一题 4. 第十二题 5. 第十三题 五、思考体会 1. 举例说明SQL 注入攻击发生的原因。 2. 从信息的CIA 三要素(机密性、完整性、可用性&…

【MySQL-17】存储过程-[变量篇]详解-(系统变量&用户定义变量&局部变量)

前言 大家好吖,欢迎来到 YY 滴MySQL系列 ,热烈欢迎! 本章主要内容面向接触过C的老铁 主要内容含: 欢迎订阅 YY滴C专栏!更多干货持续更新!以下是传送门! YY的《C》专栏YY的《C11》专栏YY的《Lin…

SpringBoot常用功能实现

1. 配置文件多环境配置 1.1 创建不同环境配置文件 文件名前缀和后缀为标准固定格式&#xff0c;不可以改变。 1.2 pom中加入文件配置 可以使用<activation>标签设置默认环境。 <profiles><profile><id>dev</id><activation><active…

内置华为视频终端API接口的中央控制系统

内置华为视频终端API接口的中控系统是一种高度集成化的智能控制系统&#xff0c;它通过将华为视频终端的控制功能集成到中控系统中&#xff0c;实现了对华为视频终端的远程控制和集中管理。以下是对该系统的详细介绍&#xff1a; 一、系统概述 该系统通过调用华为视频终端提供…

数据结构(队列及其实现)

概念与结构 概念&#xff1a;只允许在⼀端进⾏插⼊数据操作&#xff0c;在另⼀端进⾏删除数据操作的特殊线性表&#xff0c; 队列具有先进先出FIFO(First In First Out)原则。 ⼊队列&#xff1a;进⾏插⼊操作的⼀端称为队尾 出队列&#xff1a;进⾏删除操作的⼀端称为队头…

Python | Leetcode Python题解之第268题丢失的数字

题目&#xff1a; 题解&#xff1a; class Solution:def missingNumber(self, nums: List[int]) -> int:n len(nums)total n * (n 1) // 2arrSum sum(nums)return total - arrSum

texify - 识别数学/图像 PDF

文章目录 一、关于 texify例子训练 二、安装手动安装 三、使用1、使用技巧2、用于交互转换的应用程序3、转换图像4、Python 中导入并运行 四、限制五、基准测试运行自己的基准测试 六、其它商业用途感谢 一、关于 texify Texify是一种OCR模型&#xff0c;它将包含数学的图像或…

spring-retry详解

spring-retry详解 1.引入依赖2.Retryable基础使用3.Recover使用4.Retryable参数详解5.需要注意 重试机制对于大部分场景来说都是必要的&#xff0c;比如同步调用三方接口&#xff0c;三方接口、信息拉取等网络原因突然不通&#xff0c;有了重试就可以多一些容错机制&#xff0c…

HackTheBox--Knife

Knife 测试过程 1 信息收集 端口扫描 80端口测试 echo "10.129.63.56 knife.htb" | sudo tee -a /etc/hosts网站是纯静态的&#xff0c;无任何交互功能&#xff0c;检查网页源代码也未发现任何可利用的文件。 检查页面请求时&#xff0c;请求与响应内容&#xff0…

笔记小结:卷积神经网络之多输入多输出通道

本文为李沐老师《动手学深度学习》笔记小结&#xff0c;用于个人复习并记录学习历程&#xff0c;适用于初学者 彩色图像具有标准的RGB通道来代表红、绿和蓝&#xff0c;需要三个通道表示&#xff0c;故而只有单输入单输出是不够的。 对于单个输入和单个输出通道的简化例子&…

vscode配置latex环境制作【文档、简历、resume】

vscode配置latex环境制作【文档、简历、resume】 1. 安装Tex Live及vscode插件 可以参考&#xff1a;vscode配置latex环境制作beamer ppt 2. 添加vscode配置文件 打开vscode&#xff0c;按下Ctrl Shift P打开搜索框&#xff0c;搜索Preference: Open User Settings (JSON…

深入理解Linux网络(四):TCP接收阻塞

TCP socket 接收函数 recv 发出 recvfrom 系统调用。 进⼊系统调⽤后&#xff0c;⽤户进程就进⼊到了内核态&#xff0c;通过执⾏⼀系列的内核协议层函数&#xff0c;然后到 socket 对象的接收队列中查看是否有数据&#xff0c;没有的话就把⾃⼰添加到 socket 对应的等待队列⾥…

Conda和Pip有什么区别?

conda和pip是Python中两种常用的包管理工具&#xff0c;它们在用途、包来源以及环境管理等方面存在区别。以下是具体分析&#xff1a; 用途 conda&#xff1a;conda是Anaconda发行版中的包管理工具&#xff0c;可以管理包括非Python软件包在内的各种包。它是一个全面的环境管理…

为什么企业一定要多参加展会,参展有何好处?

在当今的商业环境中&#xff0c;展会已成为企业不可或缺的市场推广和交流平台。无论企业规模大小&#xff0c;参展都是一个极其有益的选择。以下&#xff0c;我们将深入探讨企业为何要多参加展会&#xff0c;以及参展所能带来的种种好处。 一、增强品牌影响力与知名度 展会是聚…

大数据量接口响应慢-传输优化

问题 接口一次性返回大量数据&#xff0c;导致JSON数据大小过大&#xff0c;带宽大小不足&#xff0c;导致接口响应时间过长 解决方案 通过数据传输压缩来降低传输数据的大小&#xff0c;从而提高传输效率 服务器端压缩 springboot项目配置application文件&#xff0c;通过…