前言
没有数据库前,数据如果是文件方式存储在磁盘中,那么如果写入的过程中,可能写入过程中出差;或者因为我们程序挂掉原因,导致数据可能写了一半,如果多个应用程序同时在写程序的话,那问题就更加复杂。
数据库发明,特别是事务概念,解决了这个问题,事务将多个读写操作看成一个整体,要么成功要么失败,对于应用程序来说,简化了我们的操作,失败的时候,我们知道数据是干净的,不存在乱的问题,可以放心地重试。说实话,事务涉及的东西挺多的,只能尽力去按照学习的内容梳理下。
一 事务
事务除了原子特性,还有一致性,隔离性,持久性,这四个特性。这就是耳熟能详的ACID。A代表原子性Atomicity,C代表一致性(Consistency), I 代表隔离性(Isolation)与D持久性(Durability)。
原子性,记得学习化学的时候, 学过“分子是保持物质化学性质的一种微粒,而原子是化学变化中最小的微粒”从这个角度来说,原子在化学变化中是不可分的。利用这个不可分的特性,来表示事务要么全部成功,要么全部失败,不存在中间状态,这样数据就很干净,非黑即白,不存在灰色地带;
一致性,一致性理解起来是有点别扭,数据满足特定的预定状态,经过事务之后仍然满足这些事务的状态约束。
隔离性,多个数据库的客户端在同时操作的时候,相互隔离,互不影响,完全不影响比较难,多个事务共同执行的时候,可能会出现脏读,不可重复读,幻读的问题。为解决这些问题,隔离性也是分级别的,读未提交,读已提交,可重复读,串行化。隔离性从前到后越来越严格,性能也越来越差。
读未提交,一个事务没提交时,它的变更就被其他的事务看到,那么从这个角度来说就有影响了,因为没提交的事务,如果最后事务回滚了,那就读到脏数据了;
读提交,是一个事务提交之后,别的事务才可以看到,这样就不会存在读到脏数据了;
可重复读,是指一个事务执行过程中看到的数据,总跟这个事务启动时看到数据一致,读提交虽然保证了事务提交之后才可以看到,但是在另外一个事务中随着时间的推移,我们看到的数据可能是不同的;而可重复读保障了在一个事务中看到的数据是相同的,可以解决幻读的其他问题。
串行化,串行化可以解决一切事务问题,因为所有的事务都是排序的了,所以不会有冲突。
持久性,一旦事务提交成功,即使存在硬件故障或数据库崩溃,事务对数据库的数据更改影响是持久的。
二 分布式事务
对于分布式系统来说,事务变的更加复杂,分布式事务经常用来解决的是多个系统操作的时候的事务问题。最常用举例的是电子商务上,我们购买东西,需要建立一个订单,并且需要扣减库存,我们需要保障两个系统都成功,才可以给用户返回成功。对订单系统来说是一个事务,对库存系统来说,又是另外一个事务。在这里分布式事务,要把订单系统的事务和库存系统事务看成一个事务,要么一起成功,要么一起失败。
实现分布式事务有三种基本方法:
- XA 二段式提交
- 三阶段提交
- 基于消息的最终一致性方法。
我们想下,如果我们要执行分布式事务,需要多个系统事务都执行成功,如果我们什么都不管,先发A系统,再发B系统,如果发送A系统后,执行事务失败的话直接返回失败,那么如果A系统事务执行成功;再发B系统的时候,如果B系统事务成功很容易,如果B系统执行事务失败,我们又如何对A系统进行回滚操作那?如果让程序来进行处理,也不是不可能,不过就会很复杂,我们需要针对不同的情况来执行不同操作。
2.1 XA 二段提交
我们如果把这两个执行事务的系统看成一个系统,这整个系统支持分布式事务,多个系统如果要整体对外一致的表现,需要一个协调者,这个系统者有点像用户代理,通过这个协调者来保障,那是不是还按照刚才的步骤来,刚才的步骤有很大的失败概率。比如订单系统很可能成功的,但是库存系统因为库存的原因有一定的几率会失败,如果我们在真正提交之前,我们先进行问询,在XA二阶段中被称为问询阶段。
XA是一个分布式事务协议,规定了事务管理器和资源管理器接口。其实事务管理器就是我们说的协调者,负责问询各节点事务,向各个节点发起事务提交; 资源管理接口,是分布式事务参与者,通常由数据库实现。
二阶段提交协议(The two-phase commit protocol 2PC)像我们所说的一样,先要进行问询(投票)和提交(commit)两个阶段。
协调者会向事务参与者发起执行操作的CanCommit请求,等待参与者的响应。参与者收到请求后,会执行请求中的事务操作,记录日志信息但是不提交,待参与者执行成功,则向协调者发送Yes,表示准许执行;如果不成功,则返回No消息,则表示终止操作。
当所有的参与者都返回结果后,协调者进入第二阶段,如果都返回yes,则向参与者发送DoCommit消息,参与者完成剩下的操作,并释放资源,向协调者返回“HaveCommited”消息; 如果有一个参与者返回No,则向所有参与者发送DoAbort消息,所有的参与者会做回滚操作,参与者向协调者发送“HaveCommited" 消息。
二阶段提交顺利的话,看起来很美好,但是实际上有不少问题:
同步阻塞:比如二阶段期间,本地资源管理器占用临界资源,处于阻塞状态,直到完成;
数据不一致:如果只是等待还好,在网络问题情况下,如果有的节点没有收到协调者的最后的执行消息,导致收到的节点处于提交状态,未收到的则处于未提交状态。
单点故障,协调者如果发生故障了,参与者锁定的资源会一直等待释放,一直阻塞下去,如果有两个协调者,虽然可以选举一个出来,但是刚才执行的事务处于什么阶段,又是个不好解决的问题。
2.2 三阶段提交
三阶段提交是为了解决二阶段的问题:
- 所有的参与者和协调者都引入超时机制,通过超时时候默认执行提交动作来保障数据一致性(当然如果是终止消息没收到,还是数据不一致)。
- 为保障网络问题,在二个阶段之间插入预提交阶段,来进一步确认事务,从而保障最终的提交成功。
2.2.1 CanCommit阶段
这个阶段和2PC的第一个阶段一样类似,判断是否可以执行事务,然后返回是否可以顺利执行事务。
2.2.2 preCommit阶段
如果都回复Yes,则协调者进行预提交:
- 协调者向参与者发送preCommit指令,表示要进行预提交。
- 参与者执行事务操作,记录日志,返回ACK确认。
如果回复中又一个No,则: - 协调器终止任务,向所有的参与者发送Abort请求。
-
参与者如果收到Abort请求或者在一定时间内没收到请求,则指向事务中断。
2.2.3 docommit阶段
如果协调者从参与者都收到ACK消息:
- 协调者接收到参与者发送的ACK响应,将从与提交状态进入提交状态,向所有参与者发送doCommit请求。
- 参与者收到doCommit请求后,执行真正的事务,执行后参与者发送haveCommited消息。
- 协调者收到所有参与者的反馈后,完成事务提交。
如果协调者没有收全所有参与者的成功返回,会执行中断事务操作。
2.3 利用分布式消息实现最终一致性
有些场合,数据一致性要求不一定要求这么严格,无论是两阶段提交还是三阶段提交都无法解决锁资源和数据最终不一致问题。在只要求最终一致性场合,可以通过分布式消息来实现最终一致性。
其实我理解因为通过了分布式消息,一般会引入分布式消息队列,这样只要第一个系统得事务消息被持久了之后,后续的系统就算失败了,还可以继续重试;这样异步方式实现最终一致性也有缺点,就是返回了不代表整个事务都完成了,多个子系统什么时候一定可以完成事务是不确定的,当然由于是异步处理的,所以系统的性能是最好的。
我们说的数据一致性要求不是这么严格,有个BASE理论专门来说这个事情。基本可用(Basically Avaiable) 即出现故障时候,可以断臂求生,其他功能还可以用。
(Soft state) 柔状态,准许系统处于一个中间状态,等待最终同步为最终状态。
(Eventual Consistency) 最终一致性事务在操作过程中数据存在同步延迟,但是最终状态是一致的。
三 诗词欣赏
《梦游天姥吟留别》
李白
海客谈瀛洲,烟涛微茫信难求。
越人语天姥,云霞明灭或可睹。
天姥连天向天横,势拔五岳掩赤城。
天台一万八千丈,对此欲倒东南倾。
我欲因之梦吴越,一夜飞度镜湖月。
湖月照我影,送我至剡溪。
谢公宿处今尚在,渌水荡漾清猿啼。
脚著谢公屐,身登青云梯。
半壁见海日,空中闻天鸡。
千岩万转路不定,迷花倚石忽已暝。
熊咆龙吟殷岩泉,栗深林兮惊层巅。
云青青兮欲雨,水澹澹兮生烟。
列缺霹雳,丘峦崩摧。洞天石扉,訇然中开。
青冥浩荡不见底,日月照耀金银台。
霓为衣兮风为马,云之君兮纷纷而来下。
虎鼓瑟兮鸾回车,仙之人兮列如麻。
忽魂悸以魄动,恍惊起而长嗟。
惟觉时之枕席,失向来之烟霞。
世间行乐亦如此,古来万事东流水。
别君去兮何时还,且放白鹿青崖间,
须行即骑访名山。安能摧眉折腰事权贵,
使我不得开心颜。