从概念开始
我们先从事务的定义开始。事务即一系列读存动作被当作一个执行单元,这些动作要么全成功,要么全失败,执行动作的过程中保证数据的隔离性和一致性。
我们抛离数据库这个特定场景,先假设一个数据存储设备,我们定义两个标准操作,一个读一个写。当写操作依赖于读到的数据时,执行的顺序决定了得到的结果。
当单线程时,任意读或写操作在这个数据容器上,他必然是符合上述所有的要求的。
当多线程的时候,任意读写,实际上就是导致标准的 race condition,大部分情况下我们是不知道执行结果的。对于单核cpu来说,多线程实际上是cpu模拟,可以理解成所有的操作是合并到一起顺序执行的。在我们不加以控制的前提下,合并的操作队列必然是相对之间无序的。要想多线程情况下,达到单线程一样的事务性。最简单粗暴的办法,就是保证所有请求串行。
保证所有请求串行执行的最简单粗暴的办法就是锁。任意线程操作的适合,上锁,只有这个线程的所有动作做完之后,才能开始下一线程提供的动作。这样一来不管多少事务并行过来,保证了组内的动作一定是串行的。多线程下的动作组的事务性也就保证了。实际上工程后来的进化也是这样的,把执行顺序会影响结果的操作锁住,强制线性执行。
对于数据库来说也是一样,要完成事务的特性,本质还是锁。数据库实际上把读锁和写锁是分开的,颗粒度更细。mvcc的本质也是锁,可以理解成利用 copy-on-write 让写锁独立出来,不影响其他的操作,数据库事务进化的本质就是对锁的优化,从表锁到行锁,不停的降低锁的颗粒度,针对不同的场景使用颗粒度更小的锁。
不一致性的由来
单数据库实例读写必然是高度一致性的。问题是,单实例,更确切来说是单实例MySQL,是很难扛住所有流量的。绝大部分web应用必然是读多写少的,针对这一系统,大部分业务都做了读写分离,主写从读。这样的情况下,主从实际上是有一个不一致窗口的。不去管这个一致性窗口有多么的小,只要经过网络这样的一个慢速设备,不一致窗口,实际上必然存在。所幸的事情是,大部分应用对这个不一致性是可以容忍的。
再随着业务的发展,单机甚至都可能扛不住所有流量。我们需要去分拆数据库。这时问题就更大了,这不仅仅是外部的问题了,核心的问题可能是,在在没有单机事务的庇护下,我们如何去实现一致性。
大部分人的第一个想法是,数据库 XA ,对于两个库,引入外部协调者,然后做2pc。这样真的可行么。我们仔细想一下,如果数据库扛不住流量分拆,那么必然是分拆到两个机器,那么原本在内存中进行的协调操作必须经过网络这个慢速设备,并且XA为了做2pc,必然加长锁, 系统正常性能一下子干掉9/10,还自带随机抽风。这样搞必然走不通。
另外一部分的高端解法是,CockroachDB, TiDB,各种基于F1的高精尖分布式数据库,彻底替换掉MySQL。公司最重要的部分,可能就是数据部分,完全替换数据库的做法,实际上容易踩坑,并且踩进去还得爬出来,最气的这坑是还是自己挖的。在笔者个人的看法里,一个文件系统的彻底成熟大概要经过5到10年的时间,ex4是差不多经过5年才成熟起来的。对于数据库系统想来也不会差太多。底层数据储存,可以尝试,不建议勇猛。不是这个方法不可行,只是可能需要再等上一段时间。但是从另一角度来说,长期来看,这种方案很可能是最优解法。
重新定义需要解决的问题
分布式环境的著名问题是强CAP不可兼得。我们可以这样想一下这句话,分布式环境下,要不等数据同步一致再提供服务,要么我保证服务,不去管数据同步的问题。因为数据同步必然有时间窗口,所以强CAP 不可兼得。又因为服务可用对互联网公司应该说是最基本的要求。大部分场景下,互联网公司的选择都是强A弱C,追求最终一致性,保证高可用。(这里无意去争辩CAP是否过时,只希望能够表达清楚场景,如果对这里有疑问的话,需要想一下,心中所列反例到底是A还是HA)
任意一个系统,想要达成最终一致性,场景无非是两种。
第一个场景,某动作发生后,后续动作保证完成。
第二个场景,某动作发生后,中间动作硬性无法完成,那么需要回滚操作,并清除之前操作的副作用。
第一个场景非常适合消息队列,消息队列在处理这个场景的时候非常合适,消息队列天然具有编排服务的能力,单事件触发后续的多个服务,后续操作异步完成,不降低系统的吞吐的下限。消息做 at least once 的投递,消息消费对消息幂等,消息的发送机制利用单机事务结合事务消息,是对于这个场景非常优雅的解法。
第二个场景,更接近标准的事务场景,只不过因为跨实例,跨机器,单机事务是不可依靠的。
更确切的说,第二个场景就是分布式事务组件需要解决的问题。
设计与实现
我们基于 SAGAS 来模拟长事务,从而解决来解决上述的第二个场景。
SAGA是上个世纪80年代诞生的思想,当时想解决的问题,更多的是长事务的问题,因为事务过长,实际资源锁的时限也过长,资源损耗严重 。在现有微服务演化的前提下,我们需求的不就是可模拟的类似长事务的行为么,于是SAGAS就非常的适用于我们的场景。
SAGAS简单来说,用短的单机事务拼接长跨机事务,这一组单机事务我们称为事务组。当事务正常时正常处理,事务组执行中间有异常case时,反向补偿整个事务组。我们把事务组想象成一个状态机,那么最终要么在完成状态,要么最终在补偿成功状态。补偿善后完成后,可以认为系统到达最终一致性的状态。
我们依据上述抽象,设计开发落地了我们的分布式事务组件。
系统架构如下图:
分布式事务组件流程如下,当用户请求进入上层服务的时候,在当前线程上下文生成一个唯一的事务ID做标示事务组,rpc组件在上层服务中调用原子层时,会把事务组内所有对远程的原子层的调用,记录为一个事务组,持久化到远端储存层。
事务补偿器运行状态机,找到需要补偿的事务组,多次尝试补偿调用,直至成功为止,事务组补偿组间并行,组内串行,补偿调用经过限流器,防止后端雪崩。
这样,原子层通过事务保证一致性,逻辑层通过记录和补偿服务保证每一次逻辑层作业都会到达最终一致的状态。事务组件请求的耗损仅为持久化远程调用的时间加上初始化事务组的时间。对于业务有即时强一致性的要求的场景,实际上是业务需要一个分布式锁来保证某一属性是不存在一致性窗口的。由于补偿服务本身不含有状态,水平扩展是非常容易的。另一方面来说,因为事务组实际上是通过事务ID标示,我们可以通过事务ID去把整个事务的补偿流程串起来,可视化分析也非常的容易。
总结
1.事务本质还是并发和锁的问题。
2.分布式环境下,一般情况一致性需要为可用性牺牲,保障最终一致性,两种场景,两种手段。
3.基于 sagapattern 模拟长事务来解决分布式事务问题。
跟多免费技术资料及视频