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

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;}
}

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

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

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

相关文章

微信小程序(uniapp)对接腾讯云IM

UniApp 对接腾讯云 IM&#xff08;即时通讯&#xff09;完整指南 一、项目背景与需求分析 随着社交场景的普及&#xff0c;即时通讯功能已成为移动应用的标配。腾讯云 IM&#xff08;Tencent IM&#xff0c;即 TIM&#xff09;提供稳定可靠的即时通讯服务&#xff0c;支持单聊…

Portainer安装指南:多节点监控的docker管理面板-家庭云计算专家

背景 Portainer 是一个轻量级且功能强大的容器管理面板&#xff0c;专为 Docker 和 Kubernetes 环境设计。它通过直观的 Web 界面简化了容器的部署、管理和监控&#xff0c;即使是非技术用户也能轻松上手。Portainer 支持多节点管理&#xff0c;允许用户从一个中央控制台管理多…

[Redis] Redis命令在Pycharm中的使用

初次学习&#xff0c;如有错误还请指正 目录 String命令 Hash命令 List命令 set命令 SortedSet命令 连接pycharm的过程见&#xff1a;[Redis] 在Linux中安装Redis并连接桌面客户端或Pycharm-CSDN博客 redis命令的使用见&#xff1a;[Redis] Redis命令&#xff08;1&#xf…

计算机网络:物理层

目录 一、物理层的基本概念 二、物理层下面的传输媒体 2.1 导引型传输媒体 2.1.1 同轴电缆 2.1.2 双绞线 2.1.3 光纤 2.1.4 电力线 2.2 非导引型传输媒体 2.2.1 无线电波 2.2.2 微波 2.2.3 红外线 2.2.4 可见光 三、传输方式 3.1 串行与并行 3.2 同步与异步 3.…

构建系统maven

1 前言 说真的&#xff0c;我是真的不想看构建了&#xff0c;因为真的太多了。又多又乱。Maven、Gradle、Make、CMake、Meson、Ninja&#xff0c;Android BP。。。感觉学不完&#xff0c;根本学不完。。。 但是没办法最近又要用一下Maven&#xff0c;所以咬着牙再简单整理一下…

UE5蓝图暴露变量,在游戏运行时修改变量实时变化、看向目标跟随目标Find Look at Rotation、修改玩家自身弹簧臂

UE5蓝图中暴露变量&#xff0c;类似Unity中public一个变量&#xff0c;在游戏运行时修改变量实时变化 1&#xff0c;添加变量 2&#xff0c;设置变量的值 3&#xff0c;点开小眼睛&#xff0c;此变量显示在编辑器中&#xff0c;可以运行时修改 看向目标跟随目标Find Look at R…

proteus美观与偏好设置

本文主要讲&#xff1a; 1 快捷键修改&#xff08;复制&#xff0c;粘贴&#xff0c;原件旋转&#xff09; 2 背景颜色替换 3 模块分区 一 快捷键的设置 设置复制粘贴和旋转三个 这里只是强调一下要分配 二 背景颜色 原来的背景颜色&#xff1a; 之后的背景颜色&#xff1a;…

Arm处理器调试采用jlink硬件调试器的命令使用大全

arm处理器分为cortex-a&#xff0c;cortex-r&#xff0c;cortex-m等3个内核系列&#xff0c;其中m系列一般是单片机&#xff0c;例如stm32等&#xff0c;工控用得挺多。a系列一般是消费娱乐产品等使用较多&#xff0c;例如手机处理器。r系列是高端实时类型处理器&#xff0c;价…

如何将图像插入 PDF:最佳工具比较

无论您是编辑营销材料、写报告还是改写原来的PDF文件&#xff0c;将图像插入 PDF 都至关重要。幸运的是&#xff0c;有多种在线和离线工具可以简化此任务。在本文中&#xff0c;我们将比较一些常用的 PDF 添加图像工具&#xff0c;并根据您的使用场景推荐最佳解决方案&#xff…

4、获取树莓派温度

打开终端&#xff0c;使用指令查看CPU温度&#xff0c;依次输入以下指令&#xff1a; 1.进入操作目录 cd /sys/class/thermal/thermal_zone0 2.查看温度 cat temp 树莓派的返回值 51540 返回值除以1000为当前CPU温度值。即当前温度为51摄氏度。

Leetcode 269. 火星词典

1.题目基本信息 1.1.题目描述 现有一种使用英语字母的外星文语言&#xff0c;这门语言的字母顺序与英语顺序不同。 给定一个字符串列表 words &#xff0c;作为这门语言的词典&#xff0c;words 中的字符串已经 按这门新语言的字母顺序进行了排序 。 请你根据该词典还原出此…

使用vscode进行c/c++开发的时候,输出报错乱码、cpp文件本身乱码的问题解决

使用vscode进行c/c开发的时候&#xff0c;输出报错乱码、cpp文件本身乱码的问题解决 问题描述解决方案问题1的解决方案问题2解决方案 问题描述 本篇文章解决两个问题&#xff1a; 1.当cpp文件出现错误的时候&#xff0c;编译时报错&#xff0c;但是报错内容缺是乱码&#xff0…

现代数据湖架构全景解析:存储、表格式、计算引擎与元数据服务的协同生态

本文全面剖析现代数据湖架构的核心组件,深入探讨对象存储(OSS/S3)、表格式(Iceberg/Hudi/Delta Lake)、计算引擎(Spark/Flink/Presto)及元数据服务(HMS/Amoro)的协作关系,并提供企业级选型指南。 一、数据湖架构演进与核心价值 数据湖架构演进历程 现代数据湖核心价…

主数据编码体系全景解析:从基础到高级的编码策略全指南

在数字化转型的浪潮中&#xff0c;主数据管理&#xff08;MDM&#xff09;已成为企业数字化转型的基石。而主数据编码作为MDM的核心环节&#xff0c;其设计质量直接关系到数据管理的效率、系统的可扩展性以及业务决策的准确性。本文将系统性地探讨主数据编码的七大核心策略&…

Mac电脑上本地安装 MySQL并配置开启自启完整流程

文章目录 一、mysql安装1.1 使用 Homebrew 安装&#xff08;推荐&#xff09;1.2 手动下载 MySQL 社区版1.3 常见问题1.4 图形化管理工具&#xff08;可选&#xff09; 二、Mac 上配置 MySQL 开机自动启动2.1 使用 launchd 系统服务&#xff08;原生支持&#xff09;2.2 通过 H…

SQL Server 事务详解:概念、特性、隔离级别与实践

一、事务的基本概念 事务&#xff08;Transaction&#xff09;是数据库操作的基本单位&#xff0c;它是由一组SQL语句组成的逻辑工作单元。事务具有以下关键特性&#xff0c;通常被称为ACID特性&#xff1a; ​​原子性&#xff08;Atomicity&#xff09;​​&#xff1a;事务…

【C语言极简自学笔记】项目开发——扫雷游戏

一、项目概述 1.项目背景 扫雷是一款经典的益智游戏&#xff0c;由于它简单而富有挑战性的玩法深受人们喜爱。在 C 语言学习过程中&#xff0c;开发扫雷游戏是一个非常合适的实践项目&#xff0c;它能够综合运用 C 语言的多种基础知识&#xff0c;如数组、函数、循环、条件判…

unix/linux source 命令,其发展历程详细时间线、由来、历史背景

追本溯源,探究技术的历史背景和发展脉络,能够帮助我们更深刻地理解其设计哲学和存在的意义。source 命令(或者说它的前身和等效形式)的历史,与 Unix Shell 本身的发展紧密相连。 让我们一起踏上这段追溯之旅,探索 source 命令的由来和发展历程。 早期 Unix Shell 与命令…

720全景展示:VR全景的技术原理及应用

VR720全景展示&#xff1a;技术原理及应用探索 720全景技术&#xff0c;作为当前全球范围内迅速崛起流行的视觉新技术&#xff0c;为用户带来了全新的真实现场感和交互式的体验。凭借全方位、无死角的视觉展示特性&#xff0c;在VR&#xff08;虚拟现实&#xff09;领域中得到…

Python爬虫实战:研究Requests-HTML库相关技术

1. 引言 1.1 研究背景与意义 随着互联网数据量的爆炸式增长,网络爬虫已成为数据获取的重要工具,广泛应用于市场调研、舆情分析、学术研究等领域。传统爬虫技术在面对现代 JavaScript 动态渲染网页时面临挑战,而 Requests-HTML 库通过集成浏览器渲染引擎,为解决这一问题提…