系统的正确性 - 事务及相关

事务这个词在开发世界里默认是和数据库绑定在一起,但其实其他领域里也有事务这个词,比如会计事务所,律师事务所,不同领域的事务所指的意思是不一样的。 这里主要聊聊

  • 什么是事务
  • 为什么是事务
  • 如何实现一个事务
  • 总结

什么是事务

在不加任何限定的情况下,这里的事务默认是指数据库领域的的事务,如果是其他领域的事务会特别说明。

数据库事务

一般来说,如果一个数据库支持以下四个特性,我们说这个数据库支持事务

  • Atomic
    • 一个事务由一系列操作组成,这些操作要么全部完成,要么不做。从系统的角度来看,这个事务只有完成和未完成两种状态,不存在完成一半这个说法。 换句话说,做事不要半吊子或者说做事有始有终.
  • Consistent
    • 在事务执行之前和事务执行之后,整个系统的状态是符合逻辑的。
    • 一致的概念往往是指A和B的状态行为一致,就比如军训的时候,我们要求大家的步伐一致。 但是数据库的一致性可不是这个意思,它的一致性更多的是从系统的角度来说逻辑上的正确性。 比如张三给李四转账20快,那么张三的余额就少了20块,李四的余额就多20块,这个从逻辑上来说是正确的。
    • 一致性就是指系统的正确性.
  • Isolation
    • 事务A和事务B在执行的过程中,是不会互相干扰对方的执行结果的正确性。
    • 事务隐含了如下事情
      • 事务的操作对象是存储单元(表)
      • 事务是并发的
      • 锁是在并发过程中保持正确性的一种方式
      • 事务的隔离级别是效率和正确性之间的权衡
  • Duraton
    • 事务完成之后数据变保存在存储单元里,并不能回滚。简单的说,就是覆水难收,落子无悔.
    • 事务如果完成了一半,是可以回滚.

ACID给人的第一感觉这四个特性是平等的,正如古人所信奉的仁,义,礼,智,行,这五者是并列的。但ACID并不符合MECE原则, 更像是为了凑ACID这个单词. ACID正确的描述:

  • 从原子性,并发,存储三个维度采取策略,完成正确性这个目标

这里的ACID这四个特性并没有涉及到具体的技术概念,换句话说,这是一种思想和策略,既然是思想和策略,那么意味着可以应用到宏观和微观层面.

分布式事务

将上述事务的四大特性应用到分布式领域就是分布式事务.
在设计分布式事务的时候,有两大默认约束条件

  • 网络故障
  • 通信延迟

Spring事务

Spring事务是为了支持数据库事务而提供的统一编程接口.

Redis事务

Redis事务是由一系列命令组成,它满足:

  • 原子性 - 所有命令要么全部执行,要么不执行。 但Redis的原子性是伪原子性,为什么呢?因为只要有一个命令执行报错,它并不会回滚,所以Redis遇到命令执行失败,会继续执行下去,简单的说, Redis在原子性这方面更像是一个二愣子,一顿操作猛如虎.
  • 隔离型 - 一个事务执行完毕,另外事务才会执行。这个更像是serializable的隔离级别.

Kafka事务

Kafka的事务机制是保证生产者和消费者的操作在逻辑上是一致的

软件事务内存

Http事务

HTTP事务在这里顺便提一下,它与上面所说的事务没有什么关系.
一个Http事务是由一个Http响应和一个Http请求组成.
Http事务是一家之言,有时候会让人和联想到数据库里的事务,但只要记住Http事务和别的事务没有任何关系.

为什么是事务

事务本质上是一种策略和机制,是为了保证系统的正确性,所以事务的出现是必然的.

如何实现一个事务

如何实现分布式事务?

  • 策略
    • 两阶段提交协议
      • 第一阶段:事务管理器发生CanCommit消息给资源管理器,每个资源管理器在本地执行操作但不提交(有点类似于正式打仗之前实战演练一遍),然后发送Yes/No给事务管理器, 代码本地操作成功或者失败。
      • 第二阶段:事务处理器根据Yes/NO有如下操作
        Yes - 发送DoCommit给各个资源处理器,执行剩下的操作,执行成功发送 HaveCommited给资源处理器
        No - 发送DoAbort给各个资源处理器,本地执行成功的操作根据事务日志做回滚,回滚到一开始的状态,就好像什么都没发生一样, 然后发送HaveCommited给资源处理器.
      • 事务管理器接受到资源管理器的HaveCommited, 整个事务执行完毕。
      • MysqlSql的InnoDB事务就是基于两阶段提交的
      • 两阶段提交协议有点类似准备打仗的时候,司令问部下,你们都战术都演练好了吗? 部下回答都演练好了,那么司令的回复是开打,如果有部下的回答是还没,那么司令的答复是不打.
      • 两阶段提交协议的缺点
      • 事务管理器必须等所有资源管理器等操作完毕才能继续下一步的操作,所以两阶段提交协议不太适合高并发场景
      • 如果事务管理器挂了,那么整个系统就处于停滞状态
      • 网络发生故障的时候,有些结点接受到DoCommit命令,就会执行操作,有些结点没有接受到就不会执行, 这样就导致数据不一直. 这个问题属于一个通用问题.
      • 注意事项
        • 两阶段提交协议是数据库层面的协议,换句话说,相应的数据库必须支持XA协议,但一些数据库,比如MongoDB或者Cassandra并不支持XA协议, 同样Kafka也不支持.
    • 三阶段提交协议
      • 三阶段提交协议是为了解决两阶段提交协议中的同步阻塞和网络故障导致的数据不一致问题
      • 超时机制 - 在发送CanCommit之后,等待返回结果的这段时间内,有个时间限制,如果超过了这个限制,就放弃本次事务。 如果失败了再重试,而且失败了这个成本和代价是可以接受的,因为没有带来额外的数据损失. 超时时间和重试次数取决于业务本身.
      • 将第一个Voting阶段拆分为CanCommit和PreCommit两个阶段.
      • 不论是两阶段提交还是三阶段提交,它们
        • 是集中式事务管理,存在单点故障的风险
        • 是同步调用
        • 存在数据不一致的风险
    • TCC(Try-Confirm-Cancel)
      • 最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出
      • TCC类似于两阶段提交协议,主要应用在业务层面,所以需要TCC框架.
      • 由主业务方发起
      • 不存在集中式事务管理单点故障的问题
    • 本地消息表
      • 本地消息表这个方案是由eBay提出
      • 系统A不直接给消息中间件发送消息,而是将一个事务操作保存到消息表里。
      • 系统A有个后台程序读取这个消息表,将读到的消息发送给消息中间件. 但也又可能发送失败,如果发送失败,接着发。所以系统A有如下特征
        • 消息不会丢失,但可能有重复
        • 消息之间的顺序有保证
      • 系统B作为消费方,会遇到两个问题
        • 丢失消费 - 拿到消息之后,处理到一半机器挂了。 解决方案:使用ACK机制。 如果处理到一半,机器挂了,那么重启之后,还会继续收到同样的消息.
        • 重复消费 - 在本地增加一个已经处理过的消息表,然后判断消息是不是已经处理过.
    • Saga事务
      • Saga 事务源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文
      • 一个Saga事务是由一系列子事务组成
      • Saga事务由两种执行顺序
        • T1, T2, T3, Tn
        • T1, T2, T3, Tn, Cn, C3, C2, C1
      • Saga的两种恢复策略
        • 向前恢复: 如果T3执行失败,那么会一直重试T3直到成功为止.
        • 向后恢复: 如果T3执行失败, 那么会执行C3, C2, C1做相应的补偿. 无论是补偿也好,还是rollback也巴,不纠结名词。 这里的补偿大体分为两类
          • 操作上真正的逆向, 比如 T3代表余额增加了10块钱,那么C3代表给余额减去10块钱.
          • 操作上无法逆向,也就是覆水难收. 比如 T3代表火箭发射, 那么在这个时候C3是无法逆向的,T3失败的话,C3意味着召开一个新闻发布会。
      • Saga的子事务是如何组织的?
        • 协同式 - 每个子事务都知道整个事务的执行序列。事务和事务之间通过消息进行通信. 相当于一个团队里,每个人都有大局观, 积极主动性比较强.
        • 编排式 - 有一个统一的编排器来统一管理子事务,和协同式是完全相反的思路,类似于一个团队里老大说了算的方式.
      • Saga的缺点
        • Saga的事务不是隔离的,需要在应用层面采取某种策略和机制来保证.
      • Saga的一些补充
        • T和C是幂等的
        • 无论执行 T, C还是执行C, T,这种两个操作逻辑上必须是等价的.
  • 基本定理
    • CAP - CAP定理是在描述发生网络故障(Partition Tolerance)的时候, 一致性(Consistency)和可用性(Availability)不能两全.
      • P - 分区容错性,白话一点,就是网络发生故障了,每个结点会形成一个隔离的区域. 网络一定会发生故障的,这是客观事实。 所以在讨论CAP的时候,我们往往是讨论当P发生时,应该怎么选C和A.
      • C - 在发生网络故障的时候,系统想要获得数据,必须等网络故障恢复,具体一点,用户看到是一个等待页面.
      • A - 在发生网络故障的时候,系统为了让用户不等待,可以给用户相对的可以接受的数据.
    • BASE - Basically Available, Soft State eventually consistency, CAP是一个比较不接地气的定理, BASIC定理是CAP的一个补充。
      • BA - 基本可用, 这个是主观性比较强的指标。 比如之前一个请求的响应时间是0.5秒, 现在一个请求的响应时间是2秒,那么这个两秒在业务层面是可以接受的。或者牺牲非核心业务和流量的可用性,保证核心业务和流量的可用性.
      • S - Soft Sate, 在不影响可用性的前提下, 允许同一时刻系统的状态存在中间状态. 比如将张三地址从A修改为B,突然发生了网络故障, 一部分机器上已经修改完,另一部分机器上没修改完,这时候不同机器查询张三的地址会得到不同的结果。 我们能不能容忍这种状态?取决于业务本身.
      • E - Eventually Consistency, 继续上面的例子,等网络故障恢复了,所有结点上的张三的地址都为B

总结

在上面说了这么多,无论是单机数据库事务,还是分布式数据库事务,还是应用层面的事务,都是为了解决一个问题:执行一系列操作,如何保证一系列操作完成之后,整个系统的状态是一致的?