数据一致性问题剖析与实践(四)——竞态条件竞争导致的一致性问题

一、前言

之前我们讨论了几种场景的一致性问题

  • 冗余数据存储中的一致性问题
  • 分布式共识中的一致性问题
  • 单机事务中的一致性问题
  • 分布式事务中的一致性问题

本文将围绕竞态条件竞争中的一致性问题展开讨论分析。

二、 问题定义

竞态条件(Race Condition)是多线程、多进程或分布式环境下常见的棘手问题。简单来说,当多个执行单元(线程、进程等)同时访问并试图修改共享资源,且最终结果依赖于这些执行单元的执行顺序时,竞态条件便会出现 。

例如,在一个多线程的电商库存系统中,多个线程可能同时处理商品的下单操作,都尝试读取当前库存数量,然后进行减 1 操作再写回库存数据。由于线程执行顺序的不确定性,如果没有恰当的同步机制,就可能导致某些下单操作丢失,库存数据不准确。

线程1 线程2 库存系统 读取库存数量 读取库存数量 将库存数量减1并写入 将库存数量减1并写入 线程1 线程2 库存系统

从本质上讲,竞态条件问题常出现在以下场景:

  • 对共享变量的读写操作:共享变量如同多线程环境中的 “公共财产”,当多个线程同时对其进行读取和修改时,就容易引发数据不一致。比如一个银行账户余额作为共享变量,多个取款线程同时读取余额后进行取款操作,最终可能导致账户余额计算错误。
  • 非原子操作:像 “读取 - 修改 - 写入” 这类非原子操作,如果没有同步控制,多个线程并发执行时可能会造成数据异常或丢失。以一个简单的计数器为例,若多个线程同时执行 “读取当前计数值,加 1,再写回新值” 的操作,可能会出现计数值增加不足的情况。
  • 访问共享资源的顺序问题:当多个线程按照不同顺序访问共享资源时,可能会产生不可预测的结果。例如,线程 A 和线程 B 同时访问一个文件系统,A 尝试创建一个新目录并在其中写入文件,B 尝试在该目录创建之前就写入文件,这就可能导致错误发生。

三、 常见场景

这里举一些业务开发过程中遇到的case,

3.1 case1 对公车型库mq消费没有考虑并发情况导致新增重复车款

对公车型库订阅了上游基础车型库的消息,在消费消息时,由于消息消费逻辑较为复杂,加上消息队列的重试机制,实际场景中出现了——新增重复品牌车型车款的情况。

这个就是没有考虑到并发情况带来的影响。

消费者线程1 消费者线程2 消息队列 车型库数据库 推送新车款消息 推送新车款消息 执行新增车款操作 执行新增车款操作 数据库中出现重复车款记录 消费者线程1 消费者线程2 消息队列 车型库数据库

3.2 case2 车辆证照并发更新Json字段导致写丢失

车辆证照的拓展字段的存储是通过Json字段进行存储的,对于Json字段的更新,是通过读-内存操作-写的方式进行。

在串行执行场景下,该流程可稳定运行;

但是在并发情况下,这就容易造成写丢失的问题。

线程A 线程B 数据库 读取证照Json数据(原始:{A:1,B:2}) 读取证照Json数据(原始:{A:1,B:2}) 在内存中将字段A修改为2 在内存中将字段B修改为3 写回修改后数据({A:2,B:2}) 写回修改后数据({A:1,B:3})此时A字段更新丢失 线程A 线程B 数据库

如上图所示,对于A字段的更新操作,由于没有考虑到并发情况,就会造成写丢失的问题。

四、 思路分析

竞态条件引发的数据一致性问题,其核心矛盾在于多个执行单元对共享资源的无序争夺。最直接的破局思路,便是通过某种机制确保同一时刻仅有一个任务单元能操作共享资源,将并发操作转化为串行执行,从根源上消除竞争冲突。

基于这一理念,悲观锁与乐观锁成为两大主流解决方案,二者虽策略迥异,但目标均指向数据一致性的保障。

4.1 悲观锁:以 “阻塞” 换 “安全” 的保守策略

悲观锁遵循 “先锁后用,独占资源” 的原则,如同给共享资源加上一把 “物理锁”。它假定任何时刻都存在其他线程修改资源的风险,因此在访问共享资源前,线程会先尝试获取锁。一旦成功获取,该线程便拥有资源的独占访问权,其他线程只能处于阻塞等待状态,直至锁被释放。这种策略通过强制串行化操作,能 100% 杜绝竞态条件,但也可能因线程频繁阻塞降低系统并发性能。

常见做法:

  • 单机环境
    • Java中Synchronized关键字
    • Go中的Mutex、RWMutex
    • 数据库层面的行锁(比如select for update)
  • 分布式环境
    • Redis 分布式锁:基于 Redis 的原子操作(如SETNX命令)实现锁机制,通过设置唯一标识符和过期时间,避免死锁;**红锁(Redlock)**则是在多个 Redis 实例上获取锁,增强高可用性。
    • Zookeeper 分布式锁:利用 Zookeeper 的临时有序节点特性,通过监听节点变化实现公平锁,确保分布式环境下资源访问的一致性。
    • 分布式数据OceanBase中的锁

回到刚才提到的case1,我们其中一种解法就是在mq消费入口加上该车款id的分布式锁,只有占有该锁,该车款id才能进行处理。此方法相当于针对单一车款id进行串行化操作,避免并发情况。

消费者线程1 消费者线程2 消息队列 车型库数据库 Redis服务器 推送新车款消息 推送新车款消息 SET lock_{fashion_id} value NX PX 10000 执行新增车款操作 DEL lock_{fashion_id} 消费失败 alt [锁获取成功] [锁获取失败] 消费者线程1 消费者线程2 消息队列 车型库数据库 Redis服务器

悲观锁的本质?

从底层逻辑上看,“锁” 并非物理意义上的限制,而是一种标识符状态标记

悲观锁底层逻辑
单机
分布式
获取锁
释放锁
内存
锁标识符
Redis键值对/Zookeeper节点
线程

无论是单机还是分布式场景,悲观锁的本质都是通过状态标记实现资源独占

  • 单机场景:依赖内存中的布尔变量或计数器(如 Java 对象头中的锁标记位),借助 CAS(Compare And Swap) 原子操作完成锁的获取与释放。例如,当线程尝试获取锁时,会通过 CAS 将锁标记从0(未锁定)改为1(已锁定),若修改失败则表示锁已被占用。
  • 分布式场景:以 Redis 为例,通过SETNX(Set If Not Exists)命令在 Redis 服务器上创建键值对作为锁标识,若键已存在则获取失败;Zookeeper 则通过创建临时有序节点,节点的创建顺序即锁的获取顺序,通过监听节点变化实现锁的公平分配。

4.2 乐观锁:以 “试探” 求 “高效” 的激进策略

与悲观锁不同,乐观锁秉持 “先操作,后验证” 的理念,假设多数情况下不会发生并发冲突。线程在更新数据时,会先读取数据及其版本号(或时间戳),在执行修改操作前,再次检查数据是否被其他线程修改。若版本号未变,则提交更新并更新版本号;若版本号已更新,则按预设策略(如重试、回滚)处理。

典型实现-Synchronized自旋锁:
线程A 线程B 请求获取锁 授予锁 请求获取锁 检查锁是否释放 锁未释放 loop [自旋次数] 自旋结束,进入阻塞状态 alt [锁被占用] 释放锁 唤醒线程B,授予锁 执行临界区代码 释放锁 线程A 线程B
典型实现 - 数据库版本号机制
线程 数据库 读取数据及版本号 进行业务处理 尝试更新数据,提交版本号 更新数据,版本号+1 返回更新成功 返回更新失败 alt [版本号匹配] [版本号不匹配] 线程 数据库

回到我们刚才提到的case1,其还有一种解法就是增加唯一键索引,当遇到唯一键冲突时,执行对应的逻辑(消费失败)。纠其本质,其实是一种“先试探后验证”的思路。

消费者线程1 消费者线程2 消息队列 车型库数据库 推送新车款消息 推送新车款消息 执行新增车款操作 执行新增车款操作 唯一键冲突,新增失败 消费失败 消费者线程1 消费者线程2 消息队列 车型库数据库

针对于3.2提到的case2,因为业务场景更新较少,并发冲突可能性较低,所以可以使用乐观锁的思路去解决。

线程A 线程B 数据库 读取证照Json数据(原始:{A:1,B:2},version=1) 读取证照Json数据(原始:{A:1,B:2},version=1) 在内存中将字段A修改为2 在内存中将字段B修改为3 写回修改后数据({A:2,B:2},version=2) 写回修改后数据({A:1,B:3},version=1)此时version不符合要求,写入失败,避免写丢失 线程A 线程B 数据库

乐观锁因为缺少加锁的过程,所以在并发量少的情况性能表现较好,但是如果并发量较高,未抢到锁的执行单元一般会进行自旋重试,导致CPU空转,浪费CPU资源。

4.3 悲观锁与乐观锁的思考

悲观锁与乐观锁并非具体的技术工具,而是两种截然不同的并发控制思维范式,它们如同硬币的两面,虽策略迥异,却共同指向 “单一操作串行化” 这一保障数据一致性的核心目标。这种思想层面的抽象,为开发者提供了灵活解决竞态问题的方法论,在不同业务场景下衍生出多样的技术实现。

殊途同归:两种策略的本质共性

无论是悲观锁的 “先锁后用”,还是乐观锁的 “先试后验”,其核心都在于将并行操作转换为串行执行。

悲观锁通过 “阻塞”强制串行化 —— 当线程获取锁后,其他线程只能排队等待,确保同一时刻仅有一个线程访问共享资源;

而乐观锁则采用**“逻辑重试” **机制,允许线程先执行操作,再通过版本号或时间戳校验判断是否存在并发冲突,若冲突则重新执行,间接实现串行化效果。

场景适配:策略选择的关键考量

选择何种策略需深度结合业务场景特性:

  • 悲观锁适用场景:适用于写操作频繁、冲突概率高的场景。例如金融系统的账户扣款、电商系统的库存扣减等场景,数据准确性要求极高,即便牺牲部分并发性能,也需通过悲观锁确保操作的原子性,避免出现资金或库存错误。
  • 乐观锁适用场景:更适合读多写少、对响应速度敏感的场景。如社交平台的用户动态展示,大量用户并发读取数据,偶尔有少量更新操作。此时采用乐观锁可减少线程阻塞,提升系统吞吐量,即便出现少量冲突,通过重试机制也能快速解决。

五、 开发建议

  1. 评估并发需求:在设计阶段明确系统是否需要支持高并发,避免过度设计。如果并发量较低,可优先采用简单的同步机制。
  2. 选择合适的锁机制
    • 对于并发冲突频繁的场景,优先使用悲观锁
    • 对于读多写少的场景,考虑使用乐观锁或读写锁(如 Go 中的RWMutex
    • 分布式环境下,根据系统可用性要求选择 Redis 或 Zookeeper 实现分布式锁
  3. 最小化锁的粒度:因为锁的本质是串行化操作,尽量缩小锁的作用范围,只对必要的共享资源加锁,减少线程阻塞时间,可以提高系统并发性能。
  4. 异常处理:在使用锁机制时,要做好异常处理,确保锁能够正确释放,避免死锁。如redis分布式锁实现上,利用超时机制,对锁的释放进行兜底,从而避免死锁。

六、小结

竞态条件引发的数据一致性问题,其本质为多执行单元对共享资源的无序并发访问。核心解决思路是将并发操作串行化,主要通过悲观锁乐观锁两种策略实现。

悲观锁采用 “先锁后用” 的保守策略,通过获取锁来独占资源访问权,强制其他线程阻塞等待,常见实现包括单机环境下的 JavaSynchronized关键字、Go 的Mutex,以及分布式环境中的 Redis 和 Zookeeper 分布式锁。其本质是利用状态标记实现资源的独占控制。

乐观锁秉持 “先操作,后验证” 的理念,允许线程先执行操作,通过版本号或时间戳校验判断是否存在并发冲突,若冲突则按策略重试或回滚,典型实现有Synchronized自旋锁、数据库版本号机制。

实际开发中,需根据业务场景选择合适的锁机制:写操作频繁、冲突概率高的场景优先使用悲观锁;

读多写少、对响应速度要求高的场景更适合乐观锁。

同时,应评估并发需求,最小化锁粒度,做好异常处理,以保障系统的数据一致性和性能。

七、系列总结

至此,本系列文章已经全部结束,回顾梳理一下我们提到的一致性问题:

第一篇文章中,我们提到最典型的一致性问题——冗余数据存储,多份数据存储势必会造成数据一致性问题。首先该不该冗余存储是我们在业务需求开发中必须要评估的问题。其次针对冗余存储的一致性方案,一方面是同步,不论是增量同步(包括多写、异步mq),还是定时任务全量同步,都是基于存储数据的角度去分析;另一方面来说,在冗余存储环境下,我们很难在兼顾性能的前提下去保证冗余存储的一致性,所以另一种设计就是读屏障,我可以设计一些机制(比如读失效、读快照)等实现对外的数据一致性。

和冗余存储常常混在一起的便是分布式共识算法中的一致性问题,与冗余存储不同的点在于,分布式共识强调的是分布式环境中的决策一致性,即在一个范围内的所有节点,如何在一个确定的提案上达成一个统一的结论,可以理解为认知上的一致性。典型算法如paxos、raft,典型实践如Zookeeper、ES集群模块等等。

第四篇文章提到竞态条件下的一致性问题,核心解法就是 执行串行化,主要通过悲观锁乐观锁两种策略实现。悲观锁采用 “先锁后用” 的保守策略,通过获取锁来独占资源访问权,强制其他线程阻塞等待;乐观锁秉持 “先操作,后验证” 的理念,允许线程先执行操作,通过版本号或时间戳校验判断是否存在并发冲突,若冲突则按策略重试或回滚。在实际开发中,我们应评估并发需求,最小化锁粒度,做好异常处理,以保障系统的数据一致性和性能。

第二、三篇提到一个综合型问题——事务,是一种基于业务场景的综合性问题,在设计方案时其实也会涉及到冗余数据存储和竞态数据竞争的问题。

第二篇提到单机事务,数据库事务的本质是一组数据库操作的集合,这些操作要么全部成功执行,要么全部不执行,以保证数据库从一个一致性状态转换到另一个一致性状态。接着以Mysql为例切入事务的ACID特性,解释Mysql是如何通过保证AID特性最终实现C(Consistent)的。

第三篇围绕分布式事务展开,分布式环境中最大的挑战就是不可靠的网络和时钟,市面上很多关于分布式的解决方案,都是基于原子性——“要么成功,要么失败”去设计实现的,其本质都是通过确认应答模式拉齐认知,通过重试机制来进行可靠性兜底,两种常规的实现思路就是回滚尽最大努力交付。但是要实现分布式事务,仅仅只有原子性还不够,还需要保证分布式环境下的隔离性 和 持久性。

所以,回到我们实际的开发场景,我们在提到一致性问题的时候,需要明确到底是什么场景下的一致性问题,基于不同的场景,结合实际的业务场景,我们才能去设计不同的一致性方案。

此上,如有不正确的地方,欢迎指正。

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

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

相关文章

PCL点云处理之基于FPFH特征的SAC-IA全局配准算法 (二百四十六)

提示: 有相关点云需求的可以私信 PCL 点云处理之基于 FPFH 特征的 SAC - IA 全局配准算法 一、前言二、相关概念介绍2.1 点云2.2 FPFH 特征2.3 SAC - IA 算法三、SAC - IA 全局配准算法原理3.1 FPFH 特征提取3.2 SAC - IA 配准过程四、代码实现与分析4.1 完整代码4.2 代码分析…

JavaScript性能优化实战:从瓶颈定位到极致提速

JavaScript作为现代Web应用的核心,其性能直接影响用户体验。本文将从性能瓶颈定位、高频优化场景、现代API利用三个维度,结合代码示例和Chrome DevTools实战,为你构建完整的性能优化体系。 一、性能分析:精准定位瓶颈 1.1 Chrome…

JavaScript 页面刷新:从传统到现代的全面解析

在 Web 开发中,"刷新"是一个基础但极其重要的功能。本文将全面探讨页面刷新的实现方式,从传统方法到现代最佳实践,深入解析每一种方案的原理和适用场景,并给出实用代码示例。 一、理解页面刷新的本质 在 Web 开发中&am…

NLP高频面试题(五十五)——DeepSeek系列概览与发展背景

大型模型浪潮背景 近年来,大型语言模型(Large Language Model, LLM)领域发展迅猛,从GPT-3等超大规模模型的崛起到ChatGPT的横空出世,再到GPT-4的问世,模型参数规模和训练数据量呈指数级增长。以GPT-3为例,参数高达1750亿,在570GB文本数据上训练,显示出模型规模、数据…

鸿蒙系统应用开发全栈指南

一、开发环境搭建与工具链配置 1. DevEco Studio深度解析 作为鸿蒙生态的官方IDE,DevEco Studio 4.2版本已集成ArkTS 3.0编译器与AI代码助手功能。安装过程需注意: 系统要求:Windows 10 21H2或macOS Monterey以上环境依赖:Node…

iOS18 MSSBrowse闪退

iOS18 MSSBrowse闪退 问题方案结果 问题 最近升级了电脑系统(15.4.1),并且也升级了xcode(16.3)开发工具。之后打包公司很早之前开发的项目。 上线之后发现在苹果手机系统18以上,出现了闪退问题。 涉及到的是第三方MSSBrowse,在选择图片放大的…

鸿蒙-使用Charles抓包

目录 前言鸿蒙应用中的网络请求rcp 抓包http 抓包 以下是排查过程,没啥参考价值发送文件http 抓包报错 前言 抓包,对于各位开发者应该不陌生,各种抓包工具应该的都听说过,像 charles、fiddler、Wireshark‌等。在 Android 和 iOS…

回顾|Apache Cloudberry™ (Incubating) Meetup·2025 杭州站

2025 年 4 月 19 日,由酷克数据与中启乘数联合举办的 Apache Cloudberry™ (Incubating) Meetup 杭州站在浙江省杭州市滨江区滨江会展中心成功举办。本次活动邀请了 Cloudberry PPMC 团队成员、活跃内核贡献者以及中兴 EBASE-A、阿里云 ADB-PG、网易、中启乘数等多…

Linux网络编程 深入Linux网络栈:原始套接字链路层实战解析

之前我们编程都是在应用层,只需在地址结构体中传 地址与端口号。然后协议栈在传输层,与网络层帮我们进行数据的封装。但这里我们要学的是在链路层进行编程 这里我想说一下,当数据到达链路层,有三个分支:ARP&#xff0c…

用python写一个相机选型的简易程序

最近有点忙,上来写的时间不多。 今天就把之前写的一个选型的简易程序,供大家参考。 代码: import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QLabel, QLineEdit, QPushButton, QGro…

【实战篇】数字化打印——打印格式设计器的功能说明

前言 myBuilder内置了覆盖丰富场景的打印格式设计器,效果统一,功能完善。 设计器一:小票 用于设计小票、水单等滚筒纸张的场景,例如:超市购物小票 主要功能 打印格式的保存、下载、上传设计时功能:撤销…

Qt 中 QSQLITE 和 QODBC 数据库连接的区别

Qt 中 QSQLITE 和 QODBC 数据库连接的区别 这两行代码都是创建 Qt 数据库连接,但使用了不同的数据库驱动和连接方式: 1. QSqlDatabase::addDatabase("QSQLITE") 特点: 使用 SQLite 数据库的 原生驱动直接与 SQLite 数据库文件(…

Eigen核心矩阵/向量类 (Matrix, Vector, Array)

1. Matrix 类&#xff08;稠密矩阵&#xff09; 模板参数 cpp Matrix<Scalar, Rows, Cols, Options, MaxRows, MaxCols> Scalar: 元素类型&#xff08;如 float, double, int&#xff09;。 Rows/Cols: 行数和列数&#xff08;Dynamic 表示动态大小&#xff09;。 O…

汽车免拆诊断案例 | 2016款奔驰C200L车组合仪表上多个故障灯偶尔点亮

故障现象 一辆2016款奔驰C200L车&#xff0c;搭载274 920发动机&#xff0c;累计行驶里程约为13万km。该车组合仪表上的防侧滑故障灯、转向助力故障灯、安全气囊故障灯等偶尔异常点亮&#xff0c;且此时将挡位置于R挡&#xff0c;中控显示屏提示“后视摄像头不可用”&#xff…

实现 Babylon.js 鼠标输入管理单例 (MouseController) 的最佳实践

在现代 Web3D 开发中&#xff0c;高效的输入管理是创建流畅交互体验的关键。本文将详细介绍如何在 Babylon.js 中实现一个强大的鼠标输入管理单例&#xff0c;帮助你优雅地处理所有指针事件。 为什么需要鼠标输入管理单例&#xff1f; 在复杂的 3D 场景中&#xff0c;鼠标/指…

【LLM+Code】Cursor Agent 46.11 版本PromptTools最细致解读

一、cursor Agent cursor的agent模式, 多说一句&#xff0c;cursor目前我付费使用&#xff0c;是我目前为止使用过AI coding工具里最喜欢的一个&#xff0c;cursor nb&#xff01; https://gist.github.com/sshh12/25ad2e40529b269a88b80e7cf1c38084version&#xff1a;46.11 …

Flask + ajax上传文件(二)--多文件上传

Flask多文件上传完整教程 本教程将详细介绍如何使用Flask实现多文件上传功能,并使用时间戳为上传文件自动命名,避免文件名冲突。 一、环境准备 确保已安装Python和Flask pip install flask项目结构 flask_upload/ ├── app.py ├── upload/ # 上传文…

多级缓存入门:Caffeine、Lua、OpenResty、Canal

之前写过——Google Guava Cache简介 本文系统学习一下多级缓存 目录 0.什么是多级缓存商品查询业务案例导入1.JVM进程缓存初识Caffeine实现JVM进程缓存2.Lua语法入门HelloWorld数据类型、变量和循环函数、条件控制3.Nginx业务编码实现多级缓存安装OpenRestyOpenResty快速入门…

Python + Playwright:如何在Docker 容器运行测试?

Python + Playwright:如何在Docker 容器运行测试? 前言一、简介二、环境准备1. 安装 DockerWindows 用户macOS 用户Linux 用户(以 Ubuntu 为例)2. 启动 browserless 服务拉取 browserless 镜像启动 browserless 容器验证 browserless 是否启动成功三、创建自动化测试项目1.…

语音合成之四大语言模型(LLM)与TTS的深度融合

基于LLM的语音合成 1.技术架构1.1 LlaSA1.2 CosyVoice (和 CosyVoice2)1.3 SparkTTS 2 特性对比2.1 零样本语音克隆2.2 多语种支持2.3 可控语音生成2.4 计算效率和模型大小 总结 当前&#xff0c;在大型语言模型&#xff08;Large Language Models&#xff0c;LLMs&#xff09;…