幂等设计的8种实现方式

news/2024/5/20 5:58:16

即无论操作执行一次还是多次,其效果始终如一,不会有差异。这就是幂等性。

什么是幂等性?

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

什么场景需要幂等设计?

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

  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.mrgr.cn/p/84868884

相关文章

Linux Shell 脚本专题

本文介绍了Linux Shell环境变量和脚本使用的常用知识点。V1.0 2024年5月8日 发布于博客园目录常用环境变量一、环境变量的概念1、环境变量的含义2、环境变量的分类3、Linux环境变量二、常用的环境变量1、查看环境变量2、常用的环境变量三、设置环境量1、系统环境变量2、用户环境…

Web实操(6),基础知识学习(24~)

1.[ZJCTF 2019]NiZhuanSiWei1 (1)进入环境后看到一篇php代码,开始我简单的以为是一题常规的php伪协议,多次试错后发现它并没有那么简单,它包含了基础的文件包含,伪协议还有反序列化 (2&#x…

使用docker-compose编排lnmp(dockerfile)完成wordpress

文章目录 使用docker-compose编排lnmp(dockerfile)完成wordpress1、服务器环境2、Docker、Docker-Compose环境安装2.1 安装Docker环境2.2 安装Docker-Compose 3、nginx3.1 新建目录,上传安装包3.2 编辑Dockerfile脚本3.3 准备nginx.conf配置文…

ue引擎游戏开发笔记(35)——为射击添加轨道,并显示落点

1.需求分析: 我们只添加了开枪特效,事实上并没有实际的效果产生例如弹痕,落点等等。所以逐步实现射击的完整化,先从实现落点开始。 2.操作实现: 1.思路:可以这样理解,每次射击的过程是一次由摄…

视频提取gif怎么制作?试试这个网站一键转换

通过把视频转换成gif动图的操作能够更加方便的在各种平台上分享和传播。相较于视频,gif图片具有较小的文件体积,gif动图能够快速的加载播放,不需要等待就能快速欣赏。很适合从事新媒体之类的小伙伴,可以用来做展示、宣传等。想要实…

公考学习|基于SprinBoot+vue的公考学习平台(源码+数据库+文档)

公考学习平台目录 目录 基于SprinBootvue的公考学习平台 一、前言 二、系统设计 三、系统功能设计 5.1用户信息管理 5.2 视频信息管理 5.3公告信息管理 5.4论坛信息管理 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&…

SOL链DApp智能合约代币质押挖矿分红系统开发

随着区块链技术的不断发展和普及,越来越多的项目开始探索基于区块链的去中心化应用(DApp)。Solana(SOL)作为一条高性能、低成本的区块链网络,吸引了众多开发者和项目,其中包括了各种类型的DApp&…

YOLOv5改进(二)BiFPN替换Neck网络

前言 针对红绿灯轻量化检测,上一节使用MobileNetv3替换了主干网络,本篇将在使用BiFPN替换Neck的方式优化算法~ 往期回顾 YOLOv5改进(一)MobileNetv3替换主干网络 目录 一、BiFPN简介二、改进方法一第一步:在common.…

链表的阶乘

int FactorialSum(List L) {int res 0; // 结果初始化struct Node* x L; // 从链表的头节点开始// 遍历链表中的每一个节点while (x ! NULL) {int data x->Data; // 当前节点的值int y 1; // 用于计算当前节点值的阶乘// 计算当前节点值的阶乘for (int j 1; j < dat…

SCI一区 | MFO-CNN-LSTM-Mutilhead-Attention多变量时间序列预测(Matlab)

SCI一区 | MFO-CNN-LSTM-Mutilhead-Attention多变量时间序列预测&#xff08;Matlab&#xff09; 目录 SCI一区 | MFO-CNN-LSTM-Mutilhead-Attention多变量时间序列预测&#xff08;Matlab&#xff09;预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现MFO-CNN…

OpenDiary 24.5

我去!五月了我去!五月了 一亿年没更日寄了pixiv 100277433四月后日谈 鉴于整个四月基本没记日记,有必要开展一次考古活动 因为考古是比较困难的事情,所以想到什么就写什么了打了一整月的 p5r,四月下旬全都在高强度 p5r,每天都情不自禁打很长很长时间 一个月打了 93h 之多…

Comate,一款基于文心大模型的智能编程助手

一、官网 Baidu Comate官网 二、安装VSCode 如何下载安装VSCode 三、VSCode安装Comate 安装方式1 访问Comate官网点击 立即安装Comate插件 按钮快速安装 安装方式2 访问VSCode市场中的BaiduComate 点击 Install 按钮访问扩展详情界面 2.打开VSCode 3.安装Comate 四、…

Linux进程——Linux进程间切换与命令行参数

前言&#xff1a;在上一篇了解完进程状态后&#xff0c;我们简单了解了进程优先级&#xff0c;然后遗留了一点内容&#xff0c;本篇我们就来研究进程间的切换&#xff0c;来理解上篇提到的并发。如果对进程优先级还有没理解的地方可以先阅读&#xff1a; Linux进程优先级 本篇…

利用STM32实现语音识别功能

引言 随着物联网和智能设备的普及&#xff0c;语音识别技术正逐渐成为用户交互的主流方式之一。 STM32微控制器具备处理高效率语音识别算法的能力&#xff0c;使其成为实现低成本、低功耗语音交互系统的理想选择。 本教程将介绍如何在STM32平台上开发和部署一个基础的语音识…

Initialize failed: invalid dom.

项目场景&#xff1a; 在vue中使用Echarts出现的错误 问题描述 提示&#xff1a;这里描述项目中遇到的问题&#xff1a; 例如&#xff1a;在vue中使用Echarts出现的错误 ERROR Initialize failed: invalid dom.at Module.init (webpack-internal:///./node_modules/echarts…

缓存雪崩、击穿、击穿

缓存雪崩&#xff1a; 就是大量数据在同一时间过期或者redis宕机时&#xff0c;这时候有大量的用户请求无法在redis中进行处理&#xff0c;而去直接访问数据库&#xff0c;从而导致数据库压力剧增&#xff0c;甚至有可能导致数据库宕机&#xff0c;从而引发的一些列连锁反应&a…

HFSS学习-day2-T形波导的优化设计

入门实例–T形波导的内场分析和优化设计 HFSS--此实例优化设计 优化设计要求1. 定义输出变量Power31、Power21、和Power11&#xff0c;表示Port3、Port2、Port1的输出功率2.参数扫描分析添加扫描变量和输出变量进行一个小设置添加输出变量进行扫描分析 3. 优化设计&#xff0c…

第八章——软件工程基础知识

软件工程概述,软件开发模型,软件开发方法,需求分析,系统设计,系统测试,软件开发项目管理,软件质量,软件度量第八章——软件工程基础知识 软件工程概述 软件开发模型 软件开发方法 需求分析 系统设计 系统测试 软件开发项目管理 软件质量 软件度量

libcity笔记:libcity/evaluator/traj_loc_pred_evaluator.py

1 构造函数 2 _check_config 检查配置是否符合评估器的要求&#xff0c;确保评估过程能够顺利执行 3 collect 4 evaluate 5 save_result & clear

BACnet转MQTT网关智联楼宇json格式自定义

智能建筑的BACnet协议作为楼宇自动化领域的通用语言&#xff0c;正逐步迈向更广阔的物联网世界。随着云计算和大数据技术的飞速发展&#xff0c;如何将BACnet设备无缝融入云端生态系统&#xff0c;成为众多楼宇管理者关注的焦点。本文将以一个实际案例&#xff0c;揭示BACnet网…