分布式事务
什么是分布式事务
事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。
事务的四个属性
- Atomic,原子性。它意味着,事务可能有多个步骤,比如说写多个数据记录,尽管可能存在故障,但是要么所有的写数据都完成了,要么没有写数据能完成。不应该发生类似这种情况:在一个特定的时间发生了故障,导致事务中一半的写数据完成并可见,另一半的写数据没有完成,这里要么全有,要么全没有(All or Nothing)。
- Consistent,一致性。它通常是指数据库会强制某些应用程序定义的数据不变,保持正常。
- Isolated,隔离性。这一点还比较重要。这是一个属性,它表明两个同时运行的事务,在事务结束前,能不能看到彼此的更新,能不能看到另一个事务中间的临时的更新。目标是不能。隔离在技术上的具体体现是,事务需要串行执行。但是总结起来,事务不能看到彼此之间的中间状态,只能看到完成的事务结果。
- Durable,持久化的。这意味着,在事务提交之后,在客户端或者程序提交事务之后,在数据库中的修改是持久化的,它们不会因为一些错误而被擦除。在实际中,这意味着数据需要被写入到一些非易失的存储(Non-Volatile Storage),持久化的存储,例如磁盘。
而什么是分布式事务呢?
事务更多指的是单机版、单数据库的概念。
分布式事务 指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上 。换成比较容易理解的话,就是多个事务之间再保持事务的特性,也就是多个事务之间保证结果的一致性。
分布式事务方案
要实现分布式事务,主要考虑两个点,第一个是并发控制(Concurrency Control)第二个是原子提交(Atomic Commit)。并发控制实际上就是用来实现隔离性。
前置知识
对于事务的并发控制模型,一般有两个方向:悲观并发控制和乐观并发控制。前者适合于数据竞争严重且重试代价大的场景,后者适用于数据竞争不严重且重试代价不大的场景。
悲观并发控制:使用锁。获取数据前加锁。其他事务如果也要使用相同的数据,就必须等待锁释放。在悲观系统中,如果有锁冲突,比如其他事务持有了锁,就会造成延时等待。所以这里需要为正确性而牺牲性能。
乐观并发控制:你不用担心其他的事务是否正在读写你要使用的数据,你直接继续执行你的读写操作,通常来说这些执行会在一些临时区域,只有在事务最后的时候,你再检查是不是有一些其他的事务干扰了你。如果没有这样的其他事务,那么你的事务就完成了,并且你也不需要承受锁带来的性能损耗,因为操作锁的代价一般都比较高;但是如果有一些其他的事务在同一时间修改了你关心的数据,并造成了冲突,那么你必须要Abort当前事务,并重试。
两阶段锁(Two-Phase Locking):两阶段锁通常用来对单个事务进行并发控制,实现原子性。
- 扩展阶段:在执行任何数据的读写之前,先获取锁。持有并不断的累积所有的锁。
- 收缩阶段:持有锁直到事务结束。事务必须持有任何已经获得的锁,直到事务提交或者Abort,不允许在事务的中间过程释放锁。
可能出现双方事务互相持有对方需要的锁,导致死锁问题。死锁的解决办法,超时释放重试,逐个锁释放等等。
两阶段提交
两阶段提交(Two-Phase Commit,2PC)是实现分布式事务中原子性的常见方案。
两阶段提交中有协调者和参与者。协调者决定事务的开始、提交、回滚。参与者负责执行具体事务。
下面是两阶段提交的正常流程,协调者和参与者都没有出现问题。
参与者执行事务时通过两阶段锁进行并发控制。在回复协调者前,参与者将事务以日志形式写入磁盘,使得故障恢复时能够继续执行事务。
如果参与者集群中任意节点出现问题,协调者需要对事务进行回滚。
如果协调者出现问题,此时参与者事务已经执行但未提交,需要一直等待协调者的提交或者回滚消息。
两阶段提交的问题:
- 效率低下:协调者和参与者之间需要多轮消息交互,参与者需要将事务写入磁盘才能回复协调者,效率低。
- 阻塞:如果协调者奔溃,所有参与者需要持有锁无限期等待协调者恢复。
- 容错低:如果任何一个参与者奔溃,事务要回滚。如果协调者奔溃,参与者要阻塞并等待。
除开两阶段提交,还有三阶段提交(3PC),具体方案和2PC差距不大。
由于两阶段提交的容错性很低,可以使用Raft一致性协议来组成集群,提升参与者和协调者的容错性。
如下图,协调者TC由多台机器组成一个协调者集群,内部使用Raft协议达成一致,提升容错性。
参考文章:
WAL
WAL 全称是Write Ahead Log,预写日志。是数据库系统中常见的一种手段,用来提升性能,满足容错性。
具体做法:
考虑数据库事务场景,事务执行可能成功可能失败,如果失败需要回滚事务,撤销事务做的所有修改。
- 在事务提交前将预写日志写入磁盘持久化,日志中记录事务对数据的修改。
- 完成对内存中数据的修改。
- 最后提交事务。
- 在WAL累积到一定长度后,批量将数据持久化写入磁盘,清空WAL日志。
考虑容错性:
如果提交事务前数据库奔溃,客户端重试即可。因为事务没有提交,一切修改都是内存中修改,数据库将自动忽略掉未提交的WAL日志。
如果提交事务后数据库奔溃,数据库重启时读取WAL日志,重新修改数据(redo),恢复到奔溃前的状态。
WAL的优点:
- 如果不使用WAL,那么每次数据修改都需要写回磁盘,性能较差。
- 使用WAL,可以将多次磁盘写累积起来,将单次写入变成批量写入,将磁盘随机读写变更为顺序读写,从而提升性能。
checkpoint:
- WAL一般和checkpoint(snapshot)一起使用。
- WAL累积到一定长度后,批量将数据持久化写入磁盘,并清空WAL日志,这称为checkpoint。
- WAL不可能无限累积下去,因为这会导致奔溃重启时,要很长时间来重放(redo)WAL日志。
WAL的应用举例:
- Raft的日志和Snapshot机制就是典型的WAL。
- Mysql中的 redo、undo 日志。
- Zookeeper中的WAL日志。
参考文章: