并行系统Parallelism System(三)
并发控制 (Concurrency Control) 和分布式事务 (Distributed Transactions)
单机系统中,学习了锁(Locks)、信号量(Semaphores)等机制处理并发,在分布式环境中,这些机制不够用,甚至无法适用
q1:为什么并发控制在分布式系统中会变得更加复杂
- No Shared Memory(缺乏共享内存):
- 单机: 所有线程/进程都运行在同一台机器上,共享同一份物理内存。可以直接通过内存地址访问共享数据,并通过内存级的原子指令来实现锁
- 分布式: 不同的节点(机器)拥有自己独立的内存空间。它们之间不能直接访问对方的内存。所有通信都必须通过网络(如 RPC)进行消息传递。这意味着你无法直接用一个简单的 lock() 语句来保护一个跨多台机器的数据
- 独立故障(Independent Failures)的常态化
- 单机: 如果某个线程在持有锁时崩溃,整个进程可能随之崩溃,操作系统通常会清理资源,释放锁
- 分布式: 一个节点可能在持有“分布式锁”时突然崩溃(电源故障、网络中断、软件崩溃)。如果其他节点不知道这个节点已经崩溃或者无法有效释放这个锁,那么被锁定的资源可能永远无法被其他节点访问,导致死锁 (deadlock) 或活锁 (livelock),从而影响系统的可用性
- 网络的不确定性 (Network Unreliability/Latency/Partitions):
- 单机: 函数调用几乎没有延迟
- 分布式: 网络通信存在不可预测的延迟。一个锁请求可能因为网络拥堵而延迟到达;一个锁释放消息可能在传输过程中丢失
- 网络分区 (Network Partitions): 这是最糟糕的情况。整个网络可能会断裂成多个互不相连的子网。在分区发生时,两个子网的节点可能都认为自己拥有对共享资源的独占访问权,导致**“脑裂 (Split-Brain)”**问题,从而产生严重的数据不一致或数据损坏
- 没有全局时钟 (No Global Clock):
- 单机: 很容易确定事件的发生顺序(例如,线程 A 在线程 B 之前访问了数据)
- 分布式: 不同的机器有自己独立的本地时钟,这些时钟可能不同步。由于网络延迟的存在,很难精确判断一个事件在另一个事件之前还是之后发生。这使得在分布式环境中协调操作顺序变得极其困难
事物 Transactions
事务是一个非常重要的概念,它源于数据库领域,但其原则同样适用于分布式系统中的数据管理
What is a Transaction?
- 一个逻辑上的操作序列,这个序列中的所有操作要么全部成功完成,要么全部失败回滚,就好像它们是一个不可分割的原子操作一样
- 数据库系统(以及分布式系统中的事务系统)通常需要满足以下四个核心属性,被称为 ACID 特性:
- 原子性 (Atomicity):
- 事务是最小的执行单位,要么所有操作都成功提交,要么所有操作都失败回滚(Rollback)。不存在部分完成的状态
- 确保数据的完整性
- 一致性 (Consistency):
- 含义: 事务执行前后,数据库或系统的数据从一个有效状态转换到另一个有效状态。它遵循所有预定义的规则、完整性约束(如非空约束、唯一性约束、外键约束)和业务逻辑
- 目标: 确保数据在逻辑上是正确的
- 隔离性 (Isolation):
- 含义: 多个并发事务的执行互不干扰,就好像它们是串行执行的一样。一个事务在执行过程中,不会看到其他并发事务的中间状态
- 目标: 避免并发操作导致的数据读写冲突,例如脏读 (Dirty Read)、不可重复读 (Non-Repeatable Read)、幻读 (Phantom Read) 等
- 持久性 (Durability):
- 含义: 一旦事务成功提交,它对数据的修改就是永久性的,即使系统发生故障(如电源故障、系统崩溃),这些修改也不会丢失
- 目标: 确保数据不会丢失
- 原子性 (Atomicity):
存根 (Stub)
RPC 中,存根 (Stub) 是一个特殊的代码模块,它扮演着代理 (Proxy) 或适配器 (Adapter) 的角色,目的是隐藏网络通信的复杂性,让远程调用看起来像本地调用;分为客户端存根 (Client Stub) 和服务端存根 (Server Stub)
-
客户端存根 (Client Stub)
-
运行在客户端机器上
-
客户端应用程序直接调用的那个“远程函数”
-
职责:
- 封装调用: 客户端程序调用它时,它会截获这个调用
- 参数打包 (Marshaling / Serialization): 将客户端传入的函数参数打包(即序列化)成适合通过网络传输的格式(字节流)
- 寻找服务器: 确定远程服务的地址(IP 地址和端口)
- 网络发送: 将打包好的请求通过网络发送到服务器
- 等待响应: 阻塞等待服务器的响应(或者在异步 RPC 中处理回调)
- 结果解包 (Unmarshaling / Deserialization): 收到服务器的响应后,将其解包(即反序列化)成客户端程序能理解的数据结构
- 返回结果: 将结果返回给客户端调用者
eg:把它想象成一个本地的“电话机”,对着它说话(调用函数),它负责把声音(参数)转换成信号(字节流)通过电话线(网络)发送出去
-
-
服务端存根 (Server Stub) 或 骨架 (Skeleton)
-
运行在服务器端机器上
-
远程服务和 RPC 运行时系统之间的桥梁
- 接收请求: 从网络中接收到客户端发送过来的请求字节流
- 参数解包 (Unmarshaling / Deserialization): 将接收到的字节流解包(反序列化)成服务器程序能理解的函数参数
- 调用本地函数: 使用这些参数,调用服务器上真正的业务逻辑函数
- 结果打包 (Marshaling / Serialization): 将业务逻辑函数的返回值打包(序列化)成字节流
- 网络发送: 将打包好的结果通过网络发送回客户端
eg:想象成一个服务器上的“电话机”,它接收到信号(字节流)后,转换成听得懂的话(参数),然后交给实际的“人”(业务逻辑函数)去处理,再把“人”的回复(结果)转换成信号(字节流)发回去
-
-
存根就是 RPC 框架自动生成的一段代码,作为客户端和服务端之间通信的“中间人”,隐藏了底层的网络通信和数据转换细节,使得远程调用看起来就像本地调用一样