当前位置: 首页 > article >正文

分布式项目保证消息幂等性的常见策略

Hello,大家好,我是灰小猿! 在分布式系统中,由于各个服务之间独立部署,各个服务之间依靠远程调用完成通信,再加上面对用户重复点击时的重复请求等情况,所以如何保证消息消费的幂等性是在分布式或微服务项目中必须要考虑的问题。

常见的保证消息幂等性的策略有以下几种,根据具体的使用场景选用不同的幂等策略。

1、数据库唯一索引

这种策略主要通过数据库的唯一索引约束来保证消息的幂等性主要是用于数据插入的场景,防止数据重复插入,

适用场景:数据强一致性的场景,订单创建防重,用户注册时手机号、邮箱号的唯一性校验等,如防止用户重复提交订单,在订单表中设置一个唯一标识的字段,如order表的Business_Key(业务ID)字段,当用户提交重复的订单时,这些重复的订单所对应的Business_Key是相同的,此时插入数据数据库会报索引重复DataIntegrityViolationException 异常,从而避免数据的重复插入。

对于这个Business_Key,可以使用用户ID+商品ID+下单时间来生成,这个下单时间可能由于用户点击的先后顺序有所不同,所以可以对时间进行处理,如五分钟之内使用同一个时间标识,则可以使用下单时间戳除以300000(即5分钟=300000毫秒),这样可以有效保证同一业务订单在一段时间内只会下单一次。

我们以一个商品订单表为例,举例数据表结构如下:

CREATE TABLE payment_order (order_id VARCHAR(32) PRIMARY KEY,business_key VARCHAR(64) UNIQUE NOT NULL,user_id BIGINT NOT NULL,amount DECIMAL(10,2),status VARCHAR(20),create_time DATETIME
);

给订单表的business_key建立唯一索引

ALTER TABLE payment_order ADD UNIQUE INDEX uniq_business_key (business_key);

之后具体的业务实现流程大概如下:

1、用户发起支付请求

2、生成订单业务ID(business_key)

3、判断存在业务ID相同的订单

  • 返回已有订单

  • 生成新的订单

4、拉取第三方支付接口

生成唯一业务标识的方法如下:

public class BusinessKeyGenerator {// 时间窗口:5分钟(300000毫秒)private static final long TIME_WINDOW = 300000;public static String generateKey(Long userId, String packageId) {long timeSlot = System.currentTimeMillis() / TIME_WINDOW;String rawKey = userId + ":" + packageId + ":" + timeSlot;return DigestUtils.md5Hex(rawKey);}
}

防止订单重复创建的幂等性设计

1、通过先查询订单是否存在的方式插入

@Service
public class OrderService {@Autowiredprivate OrderRepository orderRepository;@Transactionalpublic Order createOrder(Long userId, String packageId) {String businessKey = BusinessKeyGenerator.generateKey(userId, packageId);// 检查是否存在未支付的相同业务订单Order existingOrder = orderRepository.findPendingByBusinessKey(businessKey);if (existingOrder != null) {return existingOrder;}try {// 创建新订单Order newOrder = new Order();newOrder.setOrderId(generateOrderId()); // 生成唯一订单号newOrder.setBusinessKey(businessKey);newOrder.setUserId(userId);newOrder.setPackageId(packageId);newOrder.setStatus(OrderStatus.PENDING);return orderRepository.save(newOrder);} catch (DataIntegrityViolationException ex) {// 处理唯一键冲突(高并发场景)return orderRepository.findPendingByBusinessKey(businessKey);}}
}

2、通过捕获唯一索引异常的方式插入

上面这种策略在创建新订单之前是先通过业务ID的方式去查询了数据库中是否已经存在了这个业务ID对应的订单,还有一种方式是直接生成订单信息并且执行insert插入,之后通过捕获唯一索引异常(DuplicateKeyException)的方式来返回已经创建的订单信息。

具体的实现代码如下:

@Transactional
public void createOrder(Order order) {try {orderDao.insert(order); // 触发唯一约束} catch (DataIntegrityViolationException ex) {// 抓取重复提交异常Order existingOrder = orderRepository.findPendingByBusinessKey(businessKey);throw new DuplicateOrderException(existingOrder.getOrderId());}
}

2、乐观锁

通过数据库乐观锁的方式保证幂等性,同样也是基于数据库的一种实现方式,

首先介绍一下乐观锁的概念:

乐观锁:即认为死锁的发生是极小概率的事件,所以在修改数据之前不会对数据进行加锁,只有在修改数据时通过判断本次修改的版本和上一次的版本是否相同,相同则表示数据未被修改,不相同则表示数据已经被修改,此时的数据修改失败。

适用场景:乐观锁机制适用于存在版本属性的更新,这种方式的使用通常需要在数据库表中增加int类型的versionId字段,每次修改数据时versionId=versionId+1,以此来保证每次更新的版本都是新的。

我们同样以商品订单表为例,其中加入version_id字段,用来记录当前的数据版本。

CREATE TABLE payment_order (order_id VARCHAR(32) PRIMARY KEY,version_id int NOT NULL,user_id BIGINT NOT NULL,amount DECIMAL(10,2),status VARCHAR(20),create_time DATETIME
);

当执行更新时,需要判断当前查询到的version和将要更新的version是否相同

#查询数据
SELECT version_id FROM payment_order WHERE order_id = #{order_id}#更新数据,要求数据当前版本号和已知版本号相同,并且每次更新版本号递增
UPDATE payment_order SET status=PAID, version_id = version_id+1 
WHERE order_id = #{order_id} AND version_id = #{version_id}

3、悲观锁

介绍一下悲观锁的概念

悲观锁:即认为死锁总是会发生的,所以在每次更新数据时都会对数据进行加锁,当其他线程想要修改数据时会处于一个阻塞的状态

这种处理方式一般需要我们在更新数据库时使用行级锁的更新方法,即开启事务并先查询出数据,同时对数据进行加锁,更新完成数据之后,再提交事务,从而释放锁。

以获取商品信息并生成订单,之后进行库存扣减为例,具体的sql操作如下:

//0.开始事务
begin//1.查询出商品信息
select number from payment where id=#{payment_id} for update;//2.根据商品信息生成订单
insert into payment_order (id,其他字段...) values (?,?,?,...);//3.修改商品库存
update payment set number=#{number} where id=#{payment_id}//4.提交事务
commit

4、状态机

状态机的原理是通过状态机的流转控制,确保操作只会被执行一次

适用场景:这种机制适用于订单或工单流程类系统,如订单状态变更,状态机来保证消息幂等性的策略可以说是依据严格的业务执行流程来的,换句话来说就是一条数据的状态只能由一个状态变为指定的另外一种或多种状态,

以订单数据为例,状态可以分为:待支付、已支付、已超时、已取消这几种状态,那么订单的状态流向就是固定的一个状态机制,

以下是一个订单状态的状态机

public enum OrderStateTypeEnum {PENDING,    // 待支付PAID,       // 已支付EXPIRED,    // 已超时CANCELED;   // 已取消/*** 状态机*/private static final Map<OrderStateTypeEnum, Set<OrderStateTypeEnum>> transitions = new HashMap<>();static {//待支付状态可以转换为其他三种transitions.put(PENDING, EnumSet.of(PAID, EXPIRED, CANCELED));//已支付状态不能转换为其他状态transitions.put(PAID, EnumSet.noneOf(OrderStateTypeEnum.class));}public boolean canTransitionTo(OrderStateTypeEnum orderStateType) {return transitions.get(this).contains(orderStateType);}
}

通过状态机的方式去更新数据时,会先查询订单当前的状态,并且判断当前状态是否可以转化为将要更新后的状态,如果可以再执行数据的更新,否则则认为当前的状态转变是不合理的。

5、Redis

基于Redis的原子操作来实现分布式锁,通过SETNX设置key来标识是否处理过,并且设置过期时间,如果成功则处理,否则则忽略。

适用场景:高并发情况下的快速检查,比如秒杀活动等,这种方式具有高性能低延迟的特点,但是在使用过程中要注意Redis的高可用问题。

以下是一个通过Redis实现分布式锁,来避免订单重复处理的代码逻辑,

// Redis SETNX 实现分布式锁
public class RedisLockService {public boolean tryLock(String key, String value, long expireSeconds) {return redis.opsForValue().setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS);}
}public class OrderService {public void addOrder(String orderId) {String lockKey = "lock:order:" + orderId;String requestId = UUID.randomUUID().toString();try {if (tryLock(lockKey, requestId, 30)) {// 业务处理processOrder(orderId);}} finally {// Lua脚本保证原子删除String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";redis.execute(script, Collections.singletonList(lockKey), requestId);}}
}

6、token机制

token机制的实现是:客户端先从服务器上获取一个token,提交请求时携带上这个token,服务器端会验证这个token是否已经存在,如果已经存在则删除(因为在客户端获取这个Token时,服务器端已经存起来了)并且继续后面的操作,这样可以防止重复提交的发生,

适用场景:防止请求重复提交,API接口的短时效防重等,如在用户下单时生成token,提交时服务器进行验证,在代码实现中可以用Redis存储token,以此可以防止用户重复提交多个订单,

基于Token的实现的关键代码处理如下:

// Redis + Token 机制
public class TokenService {@Autowiredprivate RedisTemplate<String, String> redis;/*** 生成Token*/public String generateToken(String userId) {String token = UUID.randomUUID().toString();//存入Redisredis.opsForValue().set(userId + ":" + token, "1", 5, TimeUnit.MINUTES);return token;}/*** 校验Token的有效性*/public boolean validateToken(String userId, String token) {String key = userId + ":" + token;Long deleted = redis.delete(key); // 原子性删除return deleted != null && deleted > 0;}
}

具体选用哪种幂等策略,还需要根据具体的业务功能来确定,在一个项目中,可能同时使用了多种幂等策略,这些都需要结合他们不同的特点和业务需求来分析,原则就是以实现功能的前提下以最小代码成本实现功能。

相关文章:

分布式项目保证消息幂等性的常见策略

Hello&#xff0c;大家好&#xff0c;我是灰小猿&#xff01; 在分布式系统中&#xff0c;由于各个服务之间独立部署&#xff0c;各个服务之间依靠远程调用完成通信&#xff0c;再加上面对用户重复点击时的重复请求等情况&#xff0c;所以如何保证消息消费的幂等性是在分布式或…...

山东大学软件学院创新项目实训开发日志——第十三周

目录 1.开展prompt工程&#xff0c;创建个性化AI助理&#xff0c;能够基于身份实现不同角度和语言风格的回答。 2.对输出进行格式化&#xff0c;生成特定格式的会议计划文档。 3.学习到的新知识 本阶段我所做的工作 1.开展prompt工程&#xff0c;创建个性化AI助理&#xff…...

如何在sublime text中批量为每一行开头或者结尾添加删除指定内容

打开你的文件&#xff1a;首先&#xff0c;在 Sublime Text 中打开你想要编辑的文件&#xff0c;然后全选 行首插入&#xff1a; 选择所有行的开头&#xff1a; 使用快捷键 Ctrl Shift L&#xff08;Windows/Linux&#xff09;或 Cmd Shift L&#xff08;Mac&#xff09;&…...

Cesium 透明渐变墙 解决方案

闭合路径修复 通过增加额外点确保路径首尾相接 透明渐变效果 使用RGBA颜色模式实现从完全不透明到完全透明的平滑渐变 参数可调性 提供多个可调参数&#xff0c;轻松自定义颜色、高度和圆环尺寸 完整代码实现 <!DOCTYPE html> <html> <head><meta …...

网络原理与 TCP/IP 协议详解

一、网络通信的本质与基础概念 1.1 什么是网络通信&#xff1f; 网络通信的本质是跨设备的数据交换&#xff0c;其核心目标是让不同物理位置的设备能够共享信息。这种交换需要解决三个核心问题&#xff1a; 如何定位设备&#xff1f; → IP地址如何找到具体服务&#xff1f;…...

day022-定时任务-故障案例与发送邮件

文章目录 1. cron定时任务无法识别命令1.1 故障原因1.2 解决方法1.2.1 对命令使用绝对路径1.2.2 在脚本开头定义PATH 2. 发送邮件2.1 安装软件2.2 配置邮件信息2.3 巡检脚本与邮件发送2.3.1 巡检脚本内容2.3.2 制作时任务发送邮件 3. 调取API发送邮件3.1 编写文案脚本3.2 制作定…...

新增 git submodule 子模块

文章目录 1、基本语法2、添加子模块后的操作3、拉取带有submodule的仓库 git submodule add 是 Git 中用于将另一个 Git 仓库作为子模块添加到当前项目中的命令。 子模块允许你将一个 Git 仓库作为另一个 Git 仓库的子目录&#xff0c;同时保持它们各自的提交历史独立。 1、基…...

List优雅分组

一、前言 最近小永哥发现&#xff0c;在开发过程中&#xff0c;经常会遇到需要对list进行分组&#xff0c;就是假如有一个RecordTest对象集合&#xff0c;RecordTest对象都有一个type的属性&#xff0c;需要将这个集合按type属性进行分组&#xff0c;转换为一个以type为key&…...

Linux 使用 Docker 安装 Milvus的两种方式

一、使用 Docker Compose 运行 Milvus (Linux) 安装并启动 Milvus Milvus 在 Milvus 资源库中提供了 Docker Compose 配置文件。要使用 Docker Compose 安装 Milvus&#xff0c;只需运行 wget https://github.com/milvus-io/milvus/releases/download/v2.5.10/milvus-standa…...

AR眼镜+AI视频盒子+视频监控联网平台:消防救援的智能革命

在火灾现场&#xff0c;每一秒都关乎生死。传统消防救援方式面临信息滞后、指挥盲区、环境复杂等挑战。今天&#xff0c;一套融合AR智能眼镜AI视频分析盒子智能监控管理平台的"三位一体"解决方案&#xff0c;正在彻底改变消防救援的作业模式&#xff0c;为消防员装上…...

编程技能:字符串函数10,strchr

专栏导航 本节文章分别属于《Win32 学习笔记》和《MFC 学习笔记》两个专栏&#xff0c;故划分为两个专栏导航。读者可以自行选择前往哪个专栏。 &#xff08;一&#xff09;WIn32 专栏导航 上一篇&#xff1a;编程技能&#xff1a;字符串函数09&#xff0c;strncmp 回到目录…...

使用tunasync部署企业内部开源软件镜像站-Centos Stream 9

使用tunasync部署企业内部开源软件镜像站 tunasync 是清华大学 TUNA 镜像源目前使用的镜像方案&#xff0c;本文将介绍如何使用 tunasync 部署企业内部开源软件镜像站。 基于tunasync mirror-web nginx进行镜像站点搭建。 1. tunasync设计 tunasync架构如下&#xff1a; …...

c/c++的opencv像素级操作二值化

图像级操作&#xff1a;使用 C/C 进行二值化 在数字图像处理中&#xff0c;图像级操作 (Image-Level Operations) 是指直接在图像的像素级别上进行处理&#xff0c;以改变图像的视觉特性或提取有用信息。这些操作通常不依赖于图像的全局结构&#xff0c;而是关注每个像素及其邻…...

C++----Vector的模拟实现

上一节讲了string的模拟实现&#xff0c;string的出现时间比vector靠前&#xff0c;所以一些函数给的也比较冗余&#xff0c;而后来的vector、list等在此基础上做了优化。这节讲一讲vector的模拟实现&#xff0c;vector与模板具有联系&#xff0c;而string的底层就是vector的一…...

Mac redis下载和安装

目录 1、官网&#xff1a;https://redis.io/ 2、滑到最底下 3、下载资源 4、安装&#xff1a; 5、输入 sudo make test 进行编译测试 会提示 ​编辑 6、sudo make install 继续 7、输入 src/redis-server 启动服务器 8、输入 src/redis-cli 启动测试端 1、官网&#xff…...

[25-cv-05718]BSF律所代理潮流品牌KAWS公仔(商标+版权)

潮流品牌KAWS公仔 案件号&#xff1a;25-cv-05718 立案时间&#xff1a;2025年5月21日 原告&#xff1a;KAWS, INC. 代理律所&#xff1a;Boies Schiller Flexner LLP 原告介绍 原告是一家由美国街头艺术家Brian Donnelly创立的公司&#xff0c;成立于2002年2月25日&…...

【PhysUnits】9 取负重载(negation.rs)

一、源码 这段代码是类型级二进制数&#xff08;包括正数和负数&#xff09;的取反和取负操作。它使用了类型系统来表示二进制数&#xff0c;并通过特质&#xff08;trait&#xff09;和泛型来实现递归操作。 use super::basic::{B0, B1, Z0, N1}; use core::ops::Neg;// 反…...

深度思考、弹性实施,业务流程自动化的实践指南

随着市场环境愈发复杂化&#xff0c;各类型企业的业务步伐为了跟得上市场节奏也逐步变得紧张&#xff0c;似乎只有保持极强的竞争力、削减成本、提升抗压能力才能在市场洪流中博得一席之位。此刻企业需要制定更明智的解决方案&#xff0c;以更快、更准确地优化决策流程。与简单…...

UWB:litepoint获取txquality里面的NRMSE

在使用litepoint测试UWB,获取txquality里面的NRMSE时,网页端可以正常获取NRMSE。但是通过SCPI 命令来获取NRMSE一直出错。 NRMSE数据类型和pyvisa问题: 参考了user guide,发现NRMSE的数值是ARBITRARY_BLOCK FLOAT,非string。 pyvisa无法解析会返回错误。 查询了各种办法…...

VUE npm ERR! code ERESOLVE, npm ERR! ERESOLVE could not resolve, 错误有效解决

VUE &#xff1a; npm ERR! code ERESOLVE npm ERR! ERESOLVE could not resolve 错误有效解决 npm install 安装组件的时候出现以上问题&#xff0c;npm版本问题报错解决方法&#xff1a;用上述方法安装完成之后又出现其他的问题 npm install 安装组件的时候出现以上问题&…...

IoT/HCIP实验-1/物联网开发平台实验Part1(快速入门,MQTT.fx对接IoTDA)

文章目录 实验介绍设备接入IoTDA进入IoTDA平台什么是IoTDA 开通服务创建产品和设备定义产品模型&#xff08;Profile&#xff09;设备注册简思(实例-产品-设备) 模拟.与平台通信虚拟设备/MQTT.fx应用 Web 控制台QA用户或密码错误QA证书导致的连接失败设备与平台连接成功 上报数…...

DMA STM32H7 Domains and space distrubution

DMA这个数据搬运工&#xff0c;对谁都好&#xff0c;任劳任怨&#xff0c;接受雇主设备的数据搬运业务。每天都忙碌着&#xff01;哈哈哈。 1. DMA 不可能单独工作&#xff0c;必须接收其他雇主的业务&#xff0c;所以数据搬运业务的参与者是DMA本身和业务需求发起者。 2. 一…...

洪水危险性评价与风险防控全攻略:从HEC-RAS数值模拟到ArcGIS水文分析,一键式自动化工具实战,助力防洪减灾与应急管理

&#x1f50d; 洪水淹没危险性是洪水损失评估、风险评估及洪水应急和管理规划等工作的重要基础。当前&#xff0c;我国正在开展的自然灾害风险普查工作&#xff0c;对洪水灾害给予了重点关注&#xff0c;提出了对洪水灾害危险性及风险评估的明确要求。洪水危险性及风险评估通常…...

Gemini Pro 2.5 输出

好的&#xff0c;我已经按照您的要求&#xff0c;将顶部横幅提示消息修改为右下角的 Toast 样式通知。 以下是涉及更改的文件及其内容&#xff1a; 1. my/src/html-ui.js 移除了旧的 #message-area div。在 <body> 底部添加了新的 #toast-container div 用于存放 Toas…...

SQL Server 和 MySQL 对比

下面是 SQL Server 和 MySQL 的详细对比&#xff0c;从功能、性能、成本、生态等多个维度展开&#xff0c;帮助你判断在什么情况下该选择哪一个。 ✅ 总览对比表 维度SQL ServerMySQL开发公司微软&#xff08;Microsoft&#xff09;Oracle&#xff08;2008年起&#xff09;是否…...

Leetcode 3269. 构建两个递增数组

1.题目基本信息 1.1.题目描述 给定两个只包含 0 和 1 的整数数组 nums1 和 nums2&#xff0c;你的任务是执行下面操作后使数组 nums1 和 nums2 中 最大 可达数字 尽可能小。 将每个 0 替换为正偶数&#xff0c;将每个 1 替换为正奇数。在替换后&#xff0c;两个数组都应该 递…...

三轴云台之积分分离PID控制算法篇

一、核心原理 积分分离PID控制的核心在于动态调整积分项的作用&#xff0c;以解决传统PID在三轴云台应用中的超调、振荡问题&#xff1a; 大误差阶段&#xff08;如云台启动或快速调整时&#xff09;&#xff1a; 关闭积分项&#xff0c;仅使用比例&#xff08;P&#xff09;…...

【Elasticsearch】scripted_upsert

在 Elasticsearch 中&#xff0c;scripted_upsert 是一个用于更新操作的参数&#xff0c;它允许在文档不存在时通过脚本初始化文档内容&#xff0c;而不是直接使用 upsert 部分的内容。这种方式提供了更灵活的文档创建和更新逻辑。 scripted_upsert 的工作原理 当设置 scripte…...

uv - 一个现代化的项目+环境管理工具

参考&#xff1a; 【uv】Python迄今最好的项目管理环境管理工具&#xff08;吧&#xff1f;&#xff09;_哔哩哔哩_bilibili 项目需求 想象&#xff0c;每次创建一个项目的时候&#xff0c;我们需要去写 README. md, .git 仓库, .gitignore&#xff0c;你会感觉很头大 对于 …...

经典密码学和现代密码学的结构及其主要区别(2)维吉尼亚密码—附py代码

Vigenre cipher 维吉尼亚密码 维吉尼亚密码由布莱斯德维吉尼亚在 16 世纪发明&#xff0c;是凯撒密码的一个更复杂的扩展。它是一种多字母替换密码&#xff0c;使用一个关键字来确定明文中不同字母的多个移位值。 与凯撒密码不同&#xff0c;凯撒密码对所有字母都有固定的偏移…...