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

如何在高并发下,保证共享数据的一致性

在高并发环境下,要保证共享数据的一致性,核心在于通过一系列严谨的、从数据库到应用架构层面的“并发控制”机制,来确保多个并行的操作,在逻辑上,能够像“串行”执行一样,产生一个确定的、符合业务规则的正确结果。一套全面、健壮的数据一致性保障体系,其构建必须系统性地涵盖五大关键策略:运用“锁”机制保障操作的“互斥性”、通过“事务”确保一系列操作的“原子性”、在分布式系统中选择合适的“一致性模型”、利用“消息队列”等工具实现“最终一致性”、以及在架构设计层面采用“无锁”或“数据分片”等策略。其中,通过“事务”确保一系列操作的“原子性”,是保障数据一致性的“基石”。

一个事务,就如同一个“保险箱”,它将一系列独立的数据库读写操作,都包裹在了一个“要么全部成功、要么全部失败”的、不可分割的单元之中。如果在事务执行的过程中,发生了任何错误,整个事务,都会被“回滚”到操作开始前的原始状态,从而,从根本上,杜绝了数据,因为“操作执行了一半”而被遗留在一种“中间的、不一致的”残缺状态的风险。

一、问题的根源、并发下的“数据混沌”

在探讨“如何保证”之前,我们必须首先,深刻地,理解“为何会不一致”。在单用户、低并发的环境中,数据的一致性,通常是自然而然的。然而,一旦进入高并发的世界,多个用户或进程,在几乎同一微秒,对同一个共享数据,进行“读取-修改-写回”的操作时,一场“数据的混沌”风暴,就将不可避免地降临

1. 经典的“商品库存”场景

这是一个最能直观地,揭示并发问题所在的经典案例:

场景:一件热门商品,其在数据库中的库存,只剩下最后1件。

并发事件:在几乎完全相同的时间点,用户A和用户B,都点击了“购买”按钮。这两个请求,被分配到了服务器的两个不同线程(线程A和线程B)上,并行地开始处理。

一个未经保护的、灾难性的执行时序

时刻1:线程A,从数据库中,读取商品库存,得到值为1

时刻2:线程A,在内存中,进行逻辑判断:1 > 0,库存充足,可以购买。

时刻3此时,发生了一次线程切换! 操作系统,暂停了线程A的执行,转而去执行线程B。

时刻4:线程B,也从数据库中,读取商品库存。因为线程A的修改,尚未被写回,所以,它读取到的值,依然1

时刻5:线程B,在内存中,也进行了逻辑判断:1 > 0,库存充足,可以购买。

时刻6:线程B,执行“写回”操作,将库存修改为1 - 1 = 0,并更新到数据库中。然后,为用户B,创建了订单。

时刻7线程再次切换,回到线程A。

时刻8:线程A,从上次被暂停的地方,继续执行。它基于自己早已“过时”的、在时刻1读取到的旧数据,执行“写回”操作,将库存,再次,修改为1 - 1 = 0,并为用户A,也创建了订单。

最终结果:一件商品,被成功地,卖给了两个人。数据库中的库存,变为了-1(如果字段允许负数)。数据,进入了严重的“不一致”状态。

2. 一致性的“定义”

数据一致性,是指数据,在其生命周期中的任何时间点,都必须,处于一种符合预设业务规则的、有效的、逻辑自洽的状态。例如,“商品库存,永远不能为负数”,就是一条业务规则。

二、方案一、“单体”世界中的“强一致性”

对于那些运行在“单个”数据库服务器上的、传统的“单体”应用而言,我们通常,会追求一种最高级别的、可立即验证的“强一致性”。

1. “锁”机制:从“悲观”到“乐观”

“锁”,是实现并发控制的、最基础、也最直接的工具。

悲观锁:它秉持一种“悲观”的哲学,即“假设冲突,总是会发生”。因此,它在对数据,进行任何操作之前,会先** preemptively**地,将这条数据“锁定”。

实现方式:例如,在数据库中,使用SELECT ... FOR UPDATE语句。当一个事务,执行了这条语句后,数据库,就会将该行数据锁定。任何其他的事务,如果也想修改这一行,就必须“排队等待”,直到前一个事务,完成并“释放”锁为止。

优缺点数据一致性保障极高,但因为“串行化”了并行操作,所以,会牺牲掉一部分的“并发性能”

乐观锁:它秉持一种“乐观”的哲学,即“假设冲突,是小概率事件”。它不会在操作前加锁,而是在**最终“提交更新”**的那一刻,去检查,数据,是否已经被其他线程所修改。

实现方式:通常,会在数据表中,增加一个“版本号”字段。

线程A,读取数据时,会同时读取其“版本号”(例如,v=1)。

当线程A,准备写回数据时,它的更新语句,会变成:UPDATE products SET stock = 0, version = 2 WHERE id = ? AND version = 1

如果,在此期间,线程B,已经抢先一步,修改了库存,那么,该行数据的版本号,就已经变成了2。此时,线程A的UPDATE语句,会因为version = 1这个条件不满足,而更新失败(影响行数为0)。

优缺点并发性能,远高于悲观锁。但如果“冲突”频繁发生,那么,大量的“重试”操作,反而会降低整体性能。

2. 数据库“事务”:原子性的保障

事务,是将一系列独立的数据库读写操作,打包成一个“不可分割”的、“要么全部成功,要么全部失败”的“原子”单元的、最核心的机制。 一个符合ACID标准(即原子性、一致性、隔离性、持久性)的事务,能够从根本上,保障在一个操作序列中的数据一致性。 在开篇的“商品库存”案例中,只要我们将“读取库存 -> 判断库存 -> 修改库存”这三个步骤,完整地,包裹在一个“事务”中,并配合“悲观锁”,那么,当线程A,在事务中,执行SELECT ... FOR UPDATE时,线程B的SELECT ... FOR UPDATE请求,就会被阻塞,直到线程A的整个事务,提交回滚为止。

三、挑战的“升维”:分布式系统的一致性

当我们的业务,发展到需要将数据,分散存储在多个独立的服务器(即“分布式系统”)上时,一致性的保障,其难度,会呈指数级上升。

1. CAP定理的“三难困境”

CAP定理,是分布式系统领域的“基石”理论。它指出,任何一个分布式系统,在以下三个核心指标中,最多,只能同时,满足其中两个

一致性:任何一次读操作,都能读取到“最新的”写操作的结果。

可用性:任何一次请求,都能收到一个“非错误”的响应,但不保证数据是最新的。

分区容错性:系统在遇到“网络分区”(即节点间的网络连接中断)时,依然能够继续对外提供服务。

在现实世界的、一个必然会遇到网络故障的分布式系统中,“分区容错性”,是一个必须被保障的基础。因此,我们,常常被迫地,在“一致性”和“可用性”之间,做出一个艰难的“权衡取舍”。

2. BASE理论与“最终一致性”

为了应对这种“三难困境”,许多大型的互联网系统,都选择,放弃“强一致性”,而遵循“BASE理论”,追求一种更具弹性和扩展性的“最终一致性”。

BASE理论,即基本可用软状态最终一致

最终一致性,意味着,系统,并不保证,在数据写入后的“任何时刻”,所有节点上的数据,都是完全一致的。但它“承诺”,在经过了一个短暂的“不一致窗口期”之后,所有节点上的数据,最终,都将会,达到一个一致的状态。

四、方案二、“分布式”世界中的“最终一致性”

1. 分布式事务的“两阶段提交”与“补偿事务”

为了在分布式环境下,实现多个独立服务之间的“事务性”操作,业界发展出了“两阶段提交”、“补偿事务”等复杂的分布式事务协议。它们,通过引入一个“协调者”的角色,来尽力地,模拟传统单体数据库的事务行为。但因为其实现的复杂性和对性能的巨大影响,在许多互联网场景下,团队更倾向于,采用基于“消息”的、更松耦合的最终一致性方案。

2. 核心模式:基于“消息队列”的异步通信

消息队列(如Kafka, RabbitMQ),是实现最终一致性的、最常用、也最可靠的“核心基础设施”。

场景:在一个电商系统中,“订单服务”,在创建了一个新订单后,需要通知“库存服务”,去扣减库存,并通知“积分服务”,去为用户增加积分。

最终一致性的实现

“订单服务”,在自己的本地数据库事务中,成功地,创建了订单。

然后,它并不直接地,去远程调用“库存服务”和“积分服务”的接口(因为,这些调用,可能会失败或超时)。

取而代之,它,向一个高可用的“消息队列”中,发送一条包含了订单信息的、可靠的“订单已创建”的消息。

“库存服务”和“积分服务”,则作为“消费者”,分别地,“订阅”了这个消息。

当它们,收到这条消息后,再各自地,在自己的服务内部,执行“扣减库存”和“增加积分”的操作。

优点:这种模式,通过“消息中间件”,将原本紧耦合的、同步的调用,进行了解耦,极大地,提升了系统的“可用性”和“弹性”。即便“库存服务”暂时宕机,也不会影响到“订单服务”的正常运行。

五、在流程与实践中“抉择”

一致性,并非一个“越高越好”的单一指标。在不同的业务场景下,我们需要,有策略地,选择不同“级别”的一致性模型

需要“强一致性”的场景:所有与“金钱”和“交易”直接相关的核心流程,例如,支付、下单、核心库存等。在这些场景下,我们必须,不计代价地,采用数据库的“事务”和“悲观锁”等机制,来保障数据的绝对一致。

可接受“最终一致性”的场景:大量的、非交易核心的场景,例如,用户动态的更新、文章点赞数的显示、非核心信息的跨系统同步等。在这些场景下,短暂的数据不一致,对用户体验的影响很小,我们可以,放心地,采用基于“消息队列”的最终一致性方案,来换取系统更高的可用性可扩展性

常见问答 (FAQ)

Q1: “强一致性”和“最终一致性”哪个更好?

A1: 两者并无绝对的“好坏”之分,只有“适用场景”的不同。“强一致性”,提供了最可靠的数据保证,但牺牲了部分的“性能”和“可用性”。而“最终一致性”,则通过接受一个短暂的“不一致”窗口,来换取系统极高的“可用性”和“可扩展性”。

Q2: 什么是“ACID”?

A2: “ACID”,是数据库事务,必须具备的四个核心特性:原子性(一个事务,是不可分割的工作单元,要么全做,要么全不做)、一致性(事务,必须使数据库,从一个一致性状态,转变到另一个一致性状态)、隔离性(并发执行的事务之间,不应相互干扰)和持久性(一旦事务提交,其结果,就是永久性的)。

Q3: 为什么说在分布式系统中,我们必须在“一致性”和“可用性”之间做出选择?

A3: 这是由“CAP定理”所决定的。因为,在分布式系统中,“网络分区”(即网络故障)是不可避免的。当网络分区发生时,一个节点,为了保证“一致性”,就必须拒绝那些可能“过时”的读写请求,从而,牺牲了“可用性”。反之,如果它为了保证“可用性”,而继续处理请求,那么,它所处理的数据,就可能是“过时”的,从而,牺牲了“一致性”。

Q4: “乐观锁”和“悲观锁”有什么区别?

A4: 两者的核心区别,在于对“并发冲突”的“假设”不同。“悲观锁”,假设冲突“很可能会发生”,所以,在操作数据“之前”,就先将其“锁定”。而“乐观锁”,则假设冲突“很少会发生”,所以,它在操作前“不加锁”,而只是在最终“提交更新”时,才去“检查

http://www.lryc.cn/news/626773.html

相关文章:

  • 如何制作免费的比特币冷钱包
  • 自我探索之旅:哲学人格测试H5案例赏析
  • YT8512C拓展寄存器配置方式
  • 机器学习数学基础与商业实践指南:从统计显著性到预测能力的认知升级
  • 设计模式的一些笔记
  • 对抗式域适应 (Adversarial Domain Adaptation)
  • 零基础学Java第二十一讲---异常(1)
  • 卸载win10/win11系统里导致磁盘故障的补丁
  • CorrectNav——基于VLM构建带“自我纠正飞轮”的VLN:通过视觉输入和语言指令预测导航动作,且从动作和感知层面生成自我修正数据
  • 有关SWD 仿真和PA.15, PB3, PB4的冲突问题
  • 基于STM32单片机的温湿度采集循迹避障APP小车
  • 关于uniappx注意点1 - 鸿蒙app
  • vue:vue中的ref和reactive
  • win10安装最新docker 4.44.2版图文教程(2025版)
  • [TryHackMe](知识学习)Hacking with PowerShell
  • 【React】评论案例列表渲染和删除功能
  • SpringAop源码详解
  • 【AI应用】部署AI向量数据库Milvus
  • 机器学习——数据清洗
  • Java基础语法three
  • 【LeetCode题解】LeetCode 209. 长度最小的子数组
  • 机器学习-数据预处理全指南:从缺失值到特征编码
  • 如何选择汽车ECU的加密方法
  • ROS2核心模块
  • Nik Collection 6.2全新版Nik降噪锐化调色PS/LR插件
  • CreateRef和useRef
  • Java内功修炼(2)——线程安全三剑客:synchronized、volatile与wait/notify
  • Web前端调试与性能优化,Charles抓包工具的高效应用
  • YOLOv11 到 C++ 落地全流程:ONNX 导出、NMS 判别与推理实战
  • Vue透传 Attributes(详细解析)2