在分布式系统中,事务的解决散布在不同组件、服务中,因而分布式事务的ACID保障面临着一些非凡难点。本系列文章介绍了21种分布式事务设计模式,并剖析其实现原理和优缺点,在面对具体分布式事务问题时,能够抉择适合的模式进行解决。原文: Exploring Solutions for Distributed Transactions (7)
在不同业务场景下,能够有不同的解决方案,常见办法有:
- 阻塞重试(Blocking Retry)
- 二阶段和三阶段提交(Two-Phase Commit (2PC) and Three-Phase Commit (3PC))
- 基于后盾队列的异步解决(Using Queues to Process Asynchronously in the Background)
- TCC弥补(TCC Compensation Matters)
- 本地音讯表(异步保障)/发件箱模式(Local Message Table (Asynchronously Ensured)/Outbox Pattern)
- MQ事务(MQ Transaction)
- Saga模式(Saga Pattern)
- 事件驱动(Event Sourcing)
- 命令查问职责拆散(Command Query Responsibility Segregation, CQRS)
- 原子提交(Atomic Commitment)
- 并行提交(Parallel Commits)
- 事务复制(Transactional Replication)
- 一致性算法(Consensus Algorithms)
- 工夫戳排序(Timestamp Ordering)
- 乐观并发管制(Optimistic Concurrency Control)
- 拜占庭容错(Byzantine Fault Tolerance, BFT)
- 分布式锁(Distributed Locking)
- 分片(Sharding)
- 多版本并发管制(Multi-Version Concurrency Control, MVCC)
- 分布式快照(Distributed Snapshots)
- 主从复制(Leader-Follower Replication)
本文将介绍MVCC、分布式快照以及主从复制三种模式。
19. 多版本并发管制(Multi-Version Concurrency Control, MVCC)
- 容许多个事务并发拜访雷同的数据,而不会互相烦扰。
- 创立同一数据对象的多个版本,并容许事务同时拜访不同的版本。通过这种形式,事务能够在非阻塞的状况下读取数据,并且能够在不产生抵触或不统一的状况下执行写操作。
波及如下步骤:
- 当事务想要读写数据时,首先查看零碎事务表,以确定是否能够进行。如果事务能够持续,则为其调配惟一的事务ID。
- 当事务写入数据对象时,创立该对象的新版本,并且事务ID与该版本相关联,新版本被增加到零碎版本表中。
- 当事务读取数据对象时,会在版本表中搜寻该对象在事务开始之前创立的最新版本,并从对象的那个版本读取数据。
- 数据对象的每个版本都与事务ID、开始工夫和完结工夫相关联。开始工夫为版本创立的工夫,完结工夫为版本被更新版本取代的工夫。
- 当事务提交时,完结工夫被记录在事务表中。与该事务关联的所有版本的数据对象都被标记为已提交,并且完结工夫设置为事务的完结工夫。
- 事务终止时,其完结工夫记录在事务表中。与该事务关联的所有版本的数据对象都被标记为终止,并且完结工夫设置为事务的完结工夫。
- 当新的事务开始时,只能拜访在它开始工夫之前曾经提交的数据对象,确保事务只读取统一的数据。
import sqlite3import time# Connect to the databaseconn = sqlite3.connect('example.db')# Create a table for testing MVCCconn.execute('''CREATE TABLE test ( id INTEGER PRIMARY KEY, name TEXT, value INTEGER, version INTEGER )''')# Insert some initial dataconn.execute("INSERT INTO test (name, value, version) VALUES ('foo', 42, 1)")# Start a transactiontx = conn.begin()# Read the current value of 'foo'cursor = tx.execute("SELECT value, version FROM test WHERE name = 'foo'")value, version = cursor.fetchone()# Increment the value of 'foo'new_value = value + 1# Insert a new version of the datanew_version = version + 1tx.execute("INSERT INTO test (name, value, version) VALUES ('foo', ?, ?)", (new_value, new_version))# Update the version of the original datatx.execute("UPDATE test SET version = ? WHERE name = 'foo' AND version = ?", (new_version, version))# Commit the transactiontx.commit()# Print the final value of 'foo'cursor = conn.execute("SELECT value FROM test WHERE name = 'foo'")value = cursor.fetchone()[0]print(f"Final value of 'foo': {value}")# Close the database connectionconn.close()
示例代码
- 事务能够依据本人的工夫戳读取适当版本的数据,而不会影响其余事务。
波及如下步骤:
- 连贯数据库SQLite3
- 创立测试MVCC的表
- 插入初始数据
- 启动事务
- 读取'foo'的以后值
- 将'foo'的值+1
- 插入版本号更高的数据新版本
- 更新原始数据版本号,使其与新版本相匹配
- 提交事务
- 输入'foo'的最终值
- 敞开数据库连贯
长处
- 容许多个事务同时读写数据
- 防止应用锁
- 保护数据的多个版本
- 在事务之间提供高度隔离
毛病
- 减少了数据库设计的复杂性
- 因为须要存储多版本数据,减少了存储开销
- 因为须要扫描多版本数据,减少了查问执行工夫
实用场景
- 具备高读写比率,并且大多数是只读事务的利用
- 须要同时执行许多事务
- 须要高并发性和一致性的联机事务处理(OLTP)零碎
- 事务须要高度隔离的利用,例如金融应用程序
20. 分布式快照(Distributed Snapshots)
- 记录分布式系统在特定工夫点的状态
- 可用于多种利用,如分布式数据库、分布式文件系统和分布式音讯代理。
波及如下步骤:
- 抉择启动快照的过程。该过程向零碎中其余过程发送标记音讯,当过程接管到标记音讯时,获取其以后状态的快照,并将音讯发送给相邻过程。
- 当过程接管到标记音讯时,记录其本地状态,包含过程状态及其通信通道。
- 过程记录本地状态后,将标记音讯发送给相邻过程,相邻过程启动快照过程。
- 过程期待来自相邻过程的所有标记音讯实现快照。
- 过程接管到所有标记音讯后,记录所有用于与其余过程通信的通道状态。
- 一旦过程记录了所有通道的状态,就向发动快照的过程发送确认音讯。
- 发动快照的过程收到所有过程的确认音讯后,联合本地状态和通道状态信息,构建分布式系统的快照。
from multiprocessing import Process, Queueclass ProcessNode(Process): def __init__(self, pid, processes, markers): super().__init__() self.pid = pid self.processes = processes self.markers = markers self.state = 0 def send_message(self, dest): self.processes[dest].queue.put(self.pid) def run(self): while True: if not self.queue.empty(): message = self.queue.get() if message == 'marker': self.markers[self.pid] = True for i in range(len(self.processes)): if i != self.pid: self.send_message(i) else: self.state += message for i in range(len(self.markers)): if self.markers[i]: self.send_message(i) self.markers[i] = False else: # do some work self.state += 1
示例代码
- Chandy-Lamport算法是一种罕用的分布式快照算法。
ProcessNode
类 —— 扩大multiprocessing.Process
类- 每个
ProcessNode
实例代表分布式系统中的一个过程。 send_message
办法 —— 将音讯发送到另一个过程run
办法 —— 定义过程的主逻辑- 如果接管到
marker
音讯,则过程将标记设置为True
,并向所有其余过程发送marker
音讯,开始记录其状态。 - 如果接管到非
marker
音讯,则该过程将该音讯增加到其状态,并向所有设置了标记的其余过程发送marker
音讯。 - 如果队列中没有音讯,则过程执行本人的工作并扭转其状态。
- 每个过程获取其本地状态的快照,并向其余过程发送音讯以获取统一的全局状态。
- 每个
长处
- 实现跨多个节点的数据一致性
- 实现容错机制,并能从故障中复原
毛病
- 实现简单
- 引入额定的网络流量和开销
- 难以调试和诊断问题
- 要求批改现有零碎
实用场景
- 要求跨多个节点实现数据一致性的金融零碎
- 须要跨多个节点实现数据一致性的库存治理和订单跟踪零碎
- 须要容错性和一致性的牢靠消息传递
21. 主从复制(Leader-Follower Replication)
- 在分布式系统中复制数据
- 一个节点充当领导者,其余节点充当追随者
- 领导者节点 —— 更新数据
- 跟随者节点 —— 复制领导者节点所做的更改
波及如下步骤:
- 领导者节点收到客户端的写申请
- 领导者节点更新其本地数据正本,并将更新发送给所有追随者节点
- 跟随者节点将更新利用到本人的本地数据正本
- 领导者节点向客户端发送写操作胜利的确认信息
- 如果跟随者节点呈现故障,领导者节点将更新信息发送给代替节点,以确保代替节点领有最新的数据正本
- 客户端想要读取数据时,能够向领导者节点或任何跟随者节点申请数据。如果从跟随者节点申请数据,则跟随者节点须要查看是否领有最新的数据正本。如果没有,则从领导者节点申请数据。
import threadingimport timeclass LeaderFollowerReplication: def __init__(self, data): self.data = data self.leader_lock = threading.Lock() self.follower_lock = threading.Lock() self.leader = None self.follower = None def start_leader(self): while True: self.leader_lock.acquire() self.follower_lock.acquire() self.follower = self.data time.sleep(0.5) self.leader = self.data self.follower_lock.release() self.leader_lock.release() def start_follower(self): while True: self.follower_lock.acquire() if self.follower != self.data: self.data = self.follower self.follower_lock.release() time.sleep(1)if __name__ == "__main__": lfr = LeaderFollowerReplication("original_data") leader_thread = threading.Thread(target=lfr.start_leader) follower_thread = threading.Thread(target=lfr.start_follower) leader_thread.start() follower_thread.start() leader_thread.join() follower_thread.join()
示例代码
LeaderFollowerReplication
类 —— 存储要复制的数据并治理锁机制- 这个类有两个锁,
leader_lock
和follower_lock
,以确保两个线程中一次只有一个能够拜访数据。 start_leader
办法 —— 运行领导者线程,首先获取leader_lock
和follower_lock
,以确保跟随者线程没有批改数据。而后将数据设置为与跟随者的数据相等,并在将数据设置为与本人的数据相等之前休眠0.5秒,最初开释锁。start_follower
办法 —— 运行跟随者线程,获取follower_lock
并查看跟随者的数据是否与以后数据不同。如果是,将数据设置为等于跟随者的数据,而后开释follower_lock
并休眠1秒。
- 这个类有两个锁,
- 创立
LeaderFollowerReplication
实例,初始数据值为"original_data"。而后创立两个线程,一个用于领导者,一个用于跟随者。
长处
- 即便跟随者节点呈现故障,领导者节点也能够持续解决写申请并保持数据的一致性
- 随着跟随者节点数量的减少,零碎能够在不影响领导者节点性能的状况下解决大量读申请
- 确保所有节点都有统一的数据正本
毛病
- 如果领导者节点故障,零碎将无奈解决写申请,直到选出新的领导者节点
- 因为每次更新都必须播送到所有跟随者节点,因而会产生更多网络流量
实用场景
- 须要在多个地点保持一致库存的电子商务网站
- 须要在多个分支机构之间放弃账户余额统一的金融机构
- 须要在多个服务器上放弃用户材料统一的社交媒体平台
挑战
- 很难确保所有节点在任何时候都有雷同的数据正本
- 如果领导者节点必须解决大量写申请,那么有可能成为瓶颈
- 零碎必须可能检测并从节点故障中复原,以保持数据一致性
参考文献
How does MVCC (Multi-Version Concurrency Control) work
What is MVCC? How multi-version concurrency control works
Chapter 13. Concurrency Control
Multiversion Concurrency Control (MVCC)
Setting Multi-Version Concurrency Control (MVCC)
Open Source Database (RDBMS) for the Enterprise
Distributed Snapshots
Distributed Snapshots
Distributed Snapshot Problem - Distributed Systems for Practitioners
Distributed Snapshots
Reading Group. Distributed Snapshots: Determining Global States of Distributed Systems
Distributed snapshots for YSQL
An example run of the Chandy-Lamport snapshot algorithm
DB Replication (I): Introduction to database replication
Chapter 5. Replication - Shichao's Notes
Leader and Followers
Understanding Database Replication
Leaders and Followers
Multi-master vs leader-follower
Data Replication - Grokking Modern System Design Interview for Engineers & Managers
Build software better, together
你好,我是俞凡,在Motorola做过研发,当初在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓重的趣味,平时喜爱浏览、思考,置信继续学习、一生成长,欢送一起交流学习。微信公众号:DeepNoMind
本文由mdnice多平台公布