共计 6874 个字符,预计需要花费 18 分钟才能阅读完成。
在分布式系统中,事务的解决散布在不同组件、服务中,因而分布式事务的 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 sqlite3
import time
# Connect to the database
conn = sqlite3.connect('example.db')
# Create a table for testing MVCC
conn.execute('''CREATE TABLE test (
id INTEGER PRIMARY KEY,
name TEXT,
value INTEGER,
version INTEGER
)''')
# Insert some initial data
conn.execute("INSERT INTO test (name, value, version) VALUES ('foo', 42, 1)")
# Start a transaction
tx = 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 data
new_version = version + 1
tx.execute("INSERT INTO test (name, value, version) VALUES ('foo', ?, ?)", (new_value, new_version))
# Update the version of the original data
tx.execute("UPDATE test SET version = ? WHERE name ='foo'AND version = ?", (new_version, version))
# Commit the transaction
tx.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 connection
conn.close()
示例代码
- 事务能够依据本人的工夫戳读取适当版本的数据,而不会影响其余事务。
-
波及如下步骤:
- 连贯数据库 SQLite3
- 创立测试 MVCC 的表
- 插入初始数据
- 启动事务
- 读取 ’foo’ 的以后值
- 将 ’foo’ 的值 +1
- 插入版本号更高的数据新版本
- 更新原始数据版本号,使其与新版本相匹配
- 提交事务
- 输入 ’foo’ 的最终值
- 敞开数据库连贯
长处
- 容许多个事务同时读写数据
- 防止应用锁
- 保护数据的多个版本
- 在事务之间提供高度隔离
毛病
- 减少了数据库设计的复杂性
- 因为须要存储多版本数据,减少了存储开销
- 因为须要扫描多版本数据,减少了查问执行工夫
实用场景
- 具备高读写比率,并且大多数是只读事务的利用
- 须要同时执行许多事务
- 须要高并发性和一致性的联机事务处理 (OLTP) 零碎
- 事务须要高度隔离的利用,例如金融应用程序
20. 分布式快照(Distributed Snapshots)
- 记录分布式系统在特定工夫点的状态
- 可用于多种利用,如分布式数据库、分布式文件系统和分布式音讯代理。
-
波及如下步骤:
- 抉择启动快照的过程。该过程向零碎中其余过程发送标记音讯,当过程接管到标记音讯时,获取其以后状态的快照,并将音讯发送给相邻过程。
- 当过程接管到标记音讯时,记录其本地状态,包含过程状态及其通信通道。
- 过程记录本地状态后,将标记音讯发送给相邻过程,相邻过程启动快照过程。
- 过程期待来自相邻过程的所有标记音讯实现快照。
- 过程接管到所有标记音讯后,记录所有用于与其余过程通信的通道状态。
- 一旦过程记录了所有通道的状态,就向发动快照的过程发送确认音讯。
- 发动快照的过程收到所有过程的确认音讯后,联合本地状态和通道状态信息,构建分布式系统的快照。
from multiprocessing import Process, Queue
class 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 threading
import time
class 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 多平台公布