分布式事务的21种武器 - 7

在分布式系统中,事务的处理分布在不同组件、服务中,因此分布式事务的ACID保障面临着一些特殊难点。本系列文章介绍了21种分布式事务设计模式,并分析其实现原理和优缺点,在面对具体分布式事务问题时,可以选择合适的模式进行处理。原文: Exploring Solutions for Distributed Transactions (7)

在不同业务场景下,可以有不同的解决方案,常见方法有:

  1. 阻塞重试(Blocking Retry)
  2. 二阶段和三阶段提交(Two-Phase Commit (2PC) and Three-Phase Commit (3PC))
  3. 基于后台队列的异步处理(Using Queues to Process Asynchronously in the Background)
  4. TCC补偿(TCC Compensation Matters)
  5. 本地消息表(异步保证)/发件箱模式(Local Message Table (Asynchronously Ensured)/Outbox Pattern)
  6. MQ事务(MQ Transaction)
  7. Saga模式(Saga Pattern)
  8. 事件驱动(Event Sourcing)
  9. 命令查询职责分离(Command Query Responsibility Segregation, CQRS)
  10. 原子提交(Atomic Commitment)
  11. 并行提交(Parallel Commits)
  12. 事务复制(Transactional Replication)
  13. 一致性算法(Consensus Algorithms)
  14. 时间戳排序(Timestamp Ordering)
  15. 乐观并发控制(Optimistic Concurrency Control)
  16. 拜占庭容错(Byzantine Fault Tolerance, BFT)
  17. 分布式锁(Distributed Locking)
  18. 分片(Sharding)
  19. 多版本并发控制(Multi-Version Concurrency Control, MVCC)
  20. 分布式快照(Distributed Snapshots)
  21. 主从复制(Leader-Follower Replication)

本文将介绍MVCC、分布式快照以及主从复制三种模式。

19. 多版本并发控制(Multi-Version Concurrency Control, MVCC)
  • 允许多个事务并发访问相同的数据,而不会相互干扰。
  • 创建同一数据对象的多个版本,并允许事务同时访问不同的版本。通过这种方式,事务可以在非阻塞的情况下读取数据,并且可以在不产生冲突或不一致的情况下执行写操作。
  • 涉及如下步骤:
    1. 当事务想要读写数据时,首先检查系统事务表,以确定是否可以进行。如果事务可以继续,则为其分配唯一的事务ID。
    2. 当事务写入数据对象时,创建该对象的新版本,并且事务ID与该版本相关联,新版本被添加到系统版本表中。
    3. 当事务读取数据对象时,会在版本表中搜索该对象在事务开始之前创建的最新版本,并从对象的那个版本读取数据。
    4. 数据对象的每个版本都与事务ID、开始时间和结束时间相关联。开始时间为版本创建的时间,结束时间为版本被更新版本取代的时间。
    5. 当事务提交时,结束时间被记录在事务表中。与该事务关联的所有版本的数据对象都被标记为已提交,并且结束时间设置为事务的结束时间。
    6. 事务终止时,其结束时间记录在事务表中。与该事务关联的所有版本的数据对象都被标记为终止,并且结束时间设置为事务的结束时间。
    7. 当新的事务开始时,只能访问在它开始时间之前已经提交的数据对象,确保事务只读取一致的数据。
python 复制代码
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()

示例代码

  • 事务可以根据自己的时间戳读取适当版本的数据,而不会影响其他事务。
  • 涉及如下步骤:
    1. 连接数据库SQLite3
    2. 创建测试MVCC的表
    3. 插入初始数据
    4. 启动事务
    5. 读取'foo'的当前值
    6. 将'foo'的值+1
    7. 插入版本号更高的数据新版本
    8. 更新原始数据版本号,使其与新版本相匹配
    9. 提交事务
    10. 输出'foo'的最终值
    11. 关闭数据库连接

优点

  • 允许多个事务同时读写数据
  • 避免使用锁
  • 维护数据的多个版本
  • 在事务之间提供高度隔离

缺点

  • 增加了数据库设计的复杂性
  • 由于需要存储多版本数据,增加了存储开销
  • 由于需要扫描多版本数据,增加了查询执行时间

适用场景

  • 具有高读写比率,并且大多数是只读事务的应用
  • 需要同时执行许多事务
  • 需要高并发性和一致性的联机事务处理(OLTP)系统
  • 事务需要高度隔离的应用,例如金融应用程序

20. 分布式快照(Distributed Snapshots)
  • 记录分布式系统在特定时间点的状态
  • 可用于多种应用,如分布式数据库、分布式文件系统和分布式消息代理。
  • 涉及如下步骤:
    1. 选择启动快照的进程。该进程向系统中其他进程发送标记消息,当进程接收到标记消息时,获取其当前状态的快照,并将消息发送给相邻进程。
    2. 当进程接收到标记消息时,记录其本地状态,包括进程状态及其通信通道。
    3. 进程记录本地状态后,将标记消息发送给相邻进程,相邻进程启动快照进程。
    4. 进程等待来自相邻进程的所有标记消息完成快照。
    5. 进程接收到所有标记消息后,记录所有用于与其他进程通信的通道状态。
    6. 一旦进程记录了所有通道的状态,就向发起快照的进程发送确认消息。
    7. 发起快照的进程收到所有进程的确认消息后,结合本地状态和通道状态信息,构建分布式系统的快照。
python 复制代码
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)
  • 在分布式系统中复制数据
  • 一个节点充当领导者,其他节点充当追随者
  • 领导者节点 ------ 更新数据
  • 跟随者节点 ------ 复制领导者节点所做的更改
  • 涉及如下步骤:
    1. 领导者节点收到客户端的写请求
    2. 领导者节点更新其本地数据副本,并将更新发送给所有追随者节点
    3. 跟随者节点将更新应用到自己的本地数据副本
    4. 领导者节点向客户端发送写操作成功的确认信息
    5. 如果跟随者节点出现故障,领导者节点将更新信息发送给替代节点,以确保替代节点拥有最新的数据副本
    6. 客户端想要读取数据时,可以向领导者节点或任何跟随者节点请求数据。如果从跟随者节点请求数据,则跟随者节点需要检查是否拥有最新的数据副本。如果没有,则从领导者节点请求数据。
python 复制代码
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_lockfollower_lock,以确保两个线程中一次只有一个可以访问数据。
    • start_leader方法 ------ 运行领导者线程,首先获取leader_lockfollower_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多平台发布

相关推荐
qq_17448285754 小时前
springboot基于微信小程序的旧衣回收系统的设计与实现
spring boot·后端·微信小程序
锅包肉的九珍5 小时前
Scala的Array数组
开发语言·后端·scala
心仪悦悦5 小时前
Scala的Array(2)
开发语言·后端·scala
2401_882727576 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架
心仪悦悦6 小时前
Scala中的集合复习(1)
开发语言·后端·scala
代码小鑫6 小时前
A043-基于Spring Boot的秒杀系统设计与实现
java·开发语言·数据库·spring boot·后端·spring·毕业设计
真心喜欢你吖7 小时前
SpringBoot与MongoDB深度整合及应用案例
java·spring boot·后端·mongodb·spring
激流丶7 小时前
【Kafka 实战】Kafka 如何保证消息的顺序性?
java·后端·kafka
uzong8 小时前
一个 IDEA 老鸟的 DEBUG 私货之多线程调试
java·后端
飞升不如收破烂~8 小时前
Spring boot常用注解和作用
java·spring boot·后端