浅谈分布式事务从理论到实践 从一次小哥哥与小姐姐的转账开始 (浅谈分布式事件的理解)

浅谈分布式事务从理论到实践 从一次小哥哥与小姐姐的转账开始 (浅谈分布式事件的理解)

什么是事务

事务(Transaction)是数据库系统中一系列操作的一个逻辑单元,所有操作要么全部成功要么全部失败。

可以看一个经典的转账事务示例,小哥哥转账 100 元给小姐姐:

操作:查询小哥哥账号余额,确保余额充足操作:从小哥哥账号扣除元操作:往小姐姐账号增加元
复制代码

转账的一系列操作就是一个事务,事务会确保这一系列操作要么全部成功,要么全部失败。

谈起事务就不得不谈事务的四大特性 ACID

还拿之前的转账案例来理解,原子性就是要求转账的一系列操作(操作 1、操作 2、操作 3)要么全部完成,要么全部失败。不能出现钱转了一半的情况,比如小哥哥的账号钱扣除成功了,但是小姐姐账号加钱的操作失败了,这种属于不满足原子性。

我们看看关键点,事务确保数据从一个 valid 状态转换到另外一个 valid 状态。什么样才算是 valid 呢,符合 all defined rules。all defined rules 包括了 constraints、 cascades、triggers 等。所以这里的一致性强调的是事务操作使得数据一直处于符合预定规则(约束、触发器等)。This prevents>

看看转账案例,怎么样才算符合一致性呢,账户余额不能为负数可以算,而两者账户余额相加=200 则属于应用语义层面的一致性,由原子性来保证。

在转账案例中,如果在转账事务执行过程中,能读取到事务中间状态,比如转了一半然后出错事务进行了回滚,读到了“转一半”的不一致的数据状态,属于脏读。为了提高并发度,在最低的读未提交隔离级别是允许这种脏读,其他几种不会出现此种脏读。

从某种意义来说,ACID 都是为了保障数据的一致性,不满足 ACID 则会有数据的不一致。

分布式事务

互联网时代,业务发展迅猛,数据往往超出单机数据库所能处理的极限,遇到性能的瓶颈。应用层面微服务架构越来越流行,从原来的单体应用拆分成一个个独立的微服务,当应用通过一组微服务来协助完成时,对数据的一致性就需要分布式事务来保证。

对数据库通常采用垂直拆分和水平数据分片,将数据拆分到多个不同的数据节点上。如果一个事务里的操作涉及了多个不同分片节点则产生了分布式事务。

我们来看看业界常见的几种分布式事务实现:

两阶段提交将提交过程分为两个阶段,在第一阶段,协调者询问所有的参与者是否可以提交事务(请参与者投票),所有参与者向协调者投票。在第二阶段,协调者根据所有参与者的投票结果做出是否事务可以全局提交的决定,并通知所有的参与者执行该决定。

2PC 的缺点

2PC 虽然保证了提交的原子性,但缺点也很明显,先从协议本身来看看两阶段提交的缺点:

1、同步阻塞。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有资源时,其他第三方节点访问资源不得不处于阻塞状态。

2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。

从性能来看,2PC 中协调者与每个参与者至少有 2 轮消息交互、多次写日志,过程又是同步阻塞,性能十分低下。

还以之前转账为例,转入分支事务已经成功,转出分支事务还未提交成功,这个时候就看到不一致,两个账号总额度是 300,属于脏读,能看到不一致还能算强一致么。

从隔离性角度来说,2PC 的分布式事务只能算最终一致,算不得强一致。一般人说的强一致只是说的原子性,事务要么全成功要么全失败。所以一致性这个词已经被玩坏了。

TCC 事务机制相对于 XA 的 2PC 相比,其特征在于它不依赖资源管理器(RM)对 XA 的支持,而是通过对(由业务系统提供的)业务逻辑的调度来实现分布式事务。

TCC 型事务(Trying/Confirming/Canceling)。

2PC 的一个完整的事务生命周期是:begin -> 业务逻辑 -> prepare -> commit。再看 TCC 的一个完整的事务生命周期是:begin -> 业务逻辑(try 业务) -> commit(comfirm 业务)。

虽然 TCC 的 confirm 阶段也会包含部分业务逻辑,当然从事务执行角度可以简化来看将 commit 与 confirm 类比,所以 TCC 并不是两阶段提交。

TCC 的 Trying/Confirming/Canceling 三个接口针对每个事务都需要用户自己来实现,其实对用户不太友好,增加用户开发工作量,另外不能保证所有人实现的接口一定能符合一致性要求,如果接口实现的有漏洞很可能会造成不一致。

SAGA 事务模型,是牺牲了一定的隔离性的,但是提高了 long-running 事务的可用性。

除了隔离性的问题,SAGA 跟 TCC 一样对于补偿的动作也是需要用户自己实现,这点其实对用户不太友好。

上述解决方案看似完美,实际上还没有解决分布式问题。为了使第一个事务不涉及分布式操作,消息队列必须与主事务使用同一套存储资源,但为了使第二个事务是本地的,消息队列存储又必须与第二事务的存储在一起。这两者是不可能同时满足的。本质上并没有规避分布式事务。

如果消息具有操作幂等性,也就是一个消息被应用多次与应用一次产生的效果是一样的话,上述问题是很好解决的。但实际情况下,有些消息很难具有幂等性,比如转账中的扣款操作,执行一次和执行多次的结束显然是不一样的,因此需要做很多额外处理,一般通过状态表或者事务消息来解决。

最大努力提交最早在 spring 中事务管理中广泛流传,感兴趣的可以参考。

在分布式数据库中间件的场景也广泛应用,我们来看看 MyCAT 的事务模型,有时也被称为弱 XA。

最大努力提交优点是性能非常好且对用户透明,缺点是可能存在部分提交成功部分失败的场景(Partial commits),而对于已经 commit 成功的场景无法 rollback。但是由于将容易出错的 sql 执行阶段先执行,commit 推迟到最后一起执行,相当于可能出错的危险窗口期缩短到只有最后的 commit 阶段,实际出错概率很低。而 commit 开始之前出错时可以正常回滚,不会有不一致。

如果是在应用层采用该事务模型可以将分支事务设计成幂等性,这样在 commit 出错时可以对出错分支进行重试。在分布式数据库中间件的场景,则很难具备幂等性。

DDM 分布式事务解决方案

各种分布式事务的方案都各有优缺点,而业务场景又是复杂多样的,对一致性的要求也各不一样,很难有一种方案包打天下。所以我们 DDM 在设计分布式事务方案时,充分考虑和权衡了各种方案的优缺点,提供了四种分布式事务模型,可以由用户自由选择。TCC 等模型使用起来需要用户自己实现相应的接口,对用户非常不友好。因此 DDM 提供了全透明模型的分布式事务,使用接口与原来单机一致。

当出现 Partial commits 异常情况是,是允许应用支持读取,所以可能会有脏读,如果业务场景对脏读比较敏感,比如之前转账事务中的查询余额,可以通过对该 select 加 for update 或者 lock in share mode 来解决,相当于针对该语句了保证了读已提交。

从 Partial commits 到补偿成功时间窗内,业界有选择不加锁的则会出现回滚覆盖,造成数据错误回补不成功,而 DDM 采用了高效的加锁避免了该问题。

原文链接:

声明:本文来自用户分享和网络收集,仅供学习与参考,测试请备份。