分布式事务

什么是事务

将多个不同的命令组装到一起的过程。

核心:锁与并发

称为事务为了更易理解。

性能较低。

容易理解的模型性能都不好,性能好的模型(锁的粒度小,增加了编程难度)都不易理解。

追求平衡。

事务要保证的问题就是一致性。

ACID保证事务完整性、原子性。什么是ACID?

建索引、读一行数据、插入一行并建索引、删除整张表….每个操作都是事务。

  • two phase lock(2PL):两阶段锁,读(加锁) - 操作 - 提交(解锁)

事务单元

多个事务单元关联时,每个事务单元都不会看到数据的中间状态。

事务单元之间的 happen-before 关系:

  • 读写
  • 写读
  • 读读
  • 写写

《事务处理》

如何满足事务关联的同时更快完成(速度)?如何保证上面四种操作的逻辑顺序(正确性)?

  • 排队各种操作:串行序列化、不需要冲突控制,但是慢,
  • 针对同一个单元的访问进行访问控制,排成多个队,没有冲突的地方进行并行:读写均加锁,
  • 针对读场景做优化,读与写锁分离开,读进行并行操作:提升读多写少性能,即读写锁,可重复读
  • MVCC,多版本并发控制,写不阻塞读,copy on write,写读场景优化,写的时候可以读,只有写写时冲突,系统实现复杂性变高,日志变多。

ACID 中的 I,隔离级别,但为了并发而破坏了一致性:

  • 可序列化
  • 可重复读
  • 读已提交

事务处理的常见问题

事务顺序

MVCC中,每次数据写入都放入不同版本的数据log,如何保证写写读的顺序。

内存中维持数据自增号,写的时候加1。读的时候根据ID找对应的数据。逻辑时间戳(事务单元先后)、物理时间戳(时钟)。

故障恢复

错误类型:

  • 业务属性不匹配,记录操作的反向操作,进行回退
  • 系统崩溃,在数据恢复没有完全完成时不对外不服务,防止崩溃时没有完成的事务而造成的中间数据暴露

死锁怎么办

原因:

  • 两个线程
  • 不同方向
  • 相同资源

方法:

  • 尽可能不死锁,降低隔离级别
  • 碰撞检测,记录各单元持有的锁进行检查,终止一边
  • 等锁超时,但是超长事务导致每次死锁释放需要太久时间,2是主流,这个辅助

深入单机事务

ACID

原子性操作:要么同时成功要么同时失败,回滚到事务最初状态。执行每个操作都记录一个回滚段。

一致性:核心为“看”,happen before,一个事务单元保证全部成功以后才可见。对多个事务操作的同一数据加锁,将事务顺序化,排队,同时将锁下推到数据上,将锁分离,而不是一个超级大锁。

隔离性:因为一致性中加锁而带来的性能问题,对强一致性进行破坏。

  • 序列化读写,用排他锁,将所有读写操作排队:性能差==不可用
  • 读写锁,读锁不能被写锁升级(读的时候不能写):
    • 可重复读,读读操作并行
  • 读写锁,读已提交,读锁能被写锁升级(读的时候能写):
    • 读读并行
    • 读写并行
  • 读未提交,只加写锁,不加读锁
    • 读读并行
    • 读写并行
    • 写读并行,所有的写是串行的,读都是并行的,可以读到写过程中未提交的数据(因此不建议用)
  • MVCC,打脸上面的 SQL92标准。快照隔离级别,核心:copy on write+无锁编程,针对读多写少优化
    • 每个事务都有一个版本号
    • 新的事务同时进来后生成一个新的版本号,读取上一个版本号事务开始之前的数据
    • 在读一致性数据的同时实现读未提交

持久性:事务完成后,所有的提交都要物理存储。延迟与持久性平衡。

  • (操作 -> 内存 -> 磁盘(块越大越块),如果在内存中攒成一块再写磁盘)

  • 等待多次 commit 之后(group commit)再刷磁盘,攒成一大块,但吞吐变小,需要权衡

  • RAID 保证持久性:RAID controller 同时写到多个磁盘,同时成功同时失败,这又成为一个新的事务。

典型异常应对策略

业务属性不匹配:

  • 原子性
  • 一致性
  • 回滚:事务单元中的每个操作都记录一个回滚段

系统宕机:重启后进入 recovery 模式,执行回滚段

调优原则

在不影响业务应用的前提下:

  • 减少锁的覆盖范围(减小锁粒度)
    • myisam 表锁 -> innodb 行锁
    • 原位锁 -> mvcc 多版本(将一个大锁拆分到不同版本的数据上)
  • 增加锁上可并行的线程数
    • 读锁写锁分离、允许并行读取数据
  • 选择正确的锁类型,与读写锁是不同层次的概念(共两种锁:排他锁,共享锁)
    • 悲观锁,适合并发争抢比较严重的场景
    • 乐观锁,适合并发争抢不太严重的场景

数据库中的 U 锁(update 锁):如果事务单元中有写操作,则在进行读操作时直接申请为写锁,而不是读锁。

分布式事务与单机事务

分布式事务的目标:

  • 完整的事务支持,和单机一样
  • 无限扩展

事务

(对于共享数据,)让多步操作顺序发生,让多线程看上去就像一步操作。

事务优化:尽可能的快,数据又不错乱。

网络

优点:去中心化,网络提供了理论上无限的扩展能力,理论上无线的数据安全性(不丢失),理论上无线的服务可用性。

缺点:共享数据困难(通过消息复制),更多的延迟,确定性丧失,并发编程难度。

基于锁的事务遇到的问题

从 2PL 到 2PC

所有的数据库都会抽象为两阶段锁的操作。在分布式中抽象为两阶段提交。

由第三者-协调器负责跨机提交。

分布式事务异常处理

任何步骤出现状况时全部回滚。但是当一阶段已提交并暴露后,二阶段提交失败则无法再回滚,只能等待直到处理成功。

分布式日志记录

协调者高可用:

  • 必须是多机,任意协调者必须知道这个事务运行的状态
  • 记录日志,准备阶段需要记录一次日志
  • 每个节点的commit 都必须记录日志
分布式事务延迟变大

随着数据、节点增长,延迟越来越大,即分布式事务的最大问题。

基于 MVCC 的事务视线中遇到的问题

分布式顺序问题

逻辑时间戳,所有操作都需要一个时间戳,然后才能知道该操作需要操作的数据版本,以保证操作顺序。

但是分布式中无法再进行时间戳的分配了,单台机器分配的话将成为单点,同时,单位时间内能够分配的时间戳是有限的,比如一台机器为100台机器分配时间戳。多台又无法保证递增性。

分布式事务的主要难题

传统数据库的分布式事务

Google Spanner 赏析

阿里分布式事务模型

DRDS/TDDL 实战