专栏名称: 朱小厮的博客
著有畅销书:《深入理解Kafka》和《RabbitMQ实战指南》。公众号主要用来分享Java技术栈、Golang技术栈、消息中间件(如Kafka、RabbitMQ)、存储、大数据以及通用型技术架构等相关的技术。
目录
相关文章推荐
新闻株洲  ·  中车电动启动春季校招 ·  19 小时前  
湖南日报  ·  价格大跳水,还要继续降! ·  昨天  
株洲交通频道广播电台  ·  涉嫌危险驾驶罪!9座车挤17人,超员88.9%! ·  3 天前  
湖南建设投资集团有限责任公司  ·  满弓发力 全力以“复” | ... ·  3 天前  
51好读  ›  专栏  ›  朱小厮的博客

分布式事务可能是个伪概念

朱小厮的博客  · 公众号  ·  · 2019-10-18 17:41

正文

点击上方“ 朱小厮的博客 ”,选择“ 设为星标

后台回复” 加群 “加入公众号专属技术群


来源:http://suo.im/5e2Ath

分布式事务顾名思义,是分布式环境下的事务,而在分布式王国里有一个著名的CAP定理,那么事务这个概念是否需要服从CAP定理呢?

在回答这个问题之前,先将事务和CAP定理两个基本概念回顾一下.

一个事务是一个只包含所有读/写操作成功的集合,也就是一系列读写操作(主要针对写操作,读操作也是为了写)要么全部成功,要么全部失败,如同一个原子操作一样,由此可见,原子性是事务的一个重要特征。事务还有其他三个特征:一致性(Consistency)、隔离性(Isolation)和耐久性(Durability),这四个特征取其英文第一个字母简称ACID。

让我们看看ACID的一致性和隔离性含义,这两者本质上是一致的,因此一致性是针对数据状态的一致,而隔离性是针对导致数据状态变化的动作行为的隔离性。

如果同时有多个事务并发同时发生,事务中操作行为必须始终保持系统数
据处于一致的状态, 这句话含义其实有两个部分,操作行为的隔离性和数据状态变化的一致性。

操作行为的隔离性实际是通过并发控制实现的,让多个操作行为相互隔离,互相不影响,类似多线程编程需要使用同步锁一样。

何为数据状态一致性?因为数据库是数据状态的大本营,关系数据库需要有保证自己内部多个表之间数据变化的一致性,不能一个表数据改变,另外一个有关联的表不发生变化,除了约束一致性, 还有级联和trigger的一致性。最常见的一致性案例是银行转账,当A账户给B账户转账100元,那么必须保证A账户和B账户增减100元的一致性,不能A账户减去100元,B账户却没有新增100元,破坏了数据一致性和完整性。

事务这个概念表面上好像是针对操作行为的,但是实际上真正关注的是操作行为的结果:状态,行为是状态变化的因,行为与状态是因果关系,因为之所以需要事务,实际是关注事务中操作导致的数据状态的切换的一致性,因此,如果事务操作是并发多个,必须系统也必须如同一个事务一样操作。

操作的隔离并发性和数据状态的一致性实际是统一的,为了更形象表达这个问题,我们以Java对象为案例说明这个问题,以充血对象为案例:

class Book{
private String name;  //不变属性
private int state;     //可变属性 状态

public void order() throws StateException {
if (this,state != 0 )  //只有不处于支付或出货状态才能订购
throw new StateException();
this.state = 1;   //假设1代表被订购
}

    public void payment() throws StateException {
if (this,state != 1 ) //只有在订购以后才能支付
throw new StateException();
this.state = 2;   //假设2代表支付
}

    public void delivery() throws StateException {
if (this,state != 2 ) //只有在支付以后才能发货
throw new StateException();

       this.state = 3;   //假设3代表发货
}
}

书籍book的有三个状态:被订购、支付和发货,每个状态切换都是有规则约束的,比如发货时,首先要判断当前状态是已经支付了才能发货,这个规则判断其实是保障数据一致性,否则会出现有的书籍没有支付就出货情况,财务没有收到钱,但是货却发出了。因此,这个类的三个动作order()、payment()、delivery()都是保证了Book状态切换的一致性,但是只有逻辑代码的一致性还不够,还需要保证在多个线程同时执行这三个动作时不会发生状态切换混乱,在多线程并发运行环境下,通常看上去没有问题的代码实际存在数据正确性Bug。

看上去话题扯得有点远,实际这些都是关于隔离并发控制和数据正确一致性的问题,上述Book类如果使用Fork/Join这样并行库进行并行运行测试,肯定是无法满足正确一致性的,那么只有给这个类的三个动作order()、payment()、delivery()的方法加上同步锁,才能实现并发控制,如下:

public synchronized void payment() throws StateException {
if (this,state != 1 ) //只有在订购以后才能支付
throw new StateException();
this.state = 2;   //假设2代表支付
}

数据库内部并发控制机制也是类似这样通过加入同步锁才能保证同一时刻只有一个线程操作,当然加同步锁是性能最差的方式,数据库内部也不会像简单使用粗粒度锁,这里只是进行并发控制的原理解释时引入简单的案例,在以后篇幅中我将介绍如何通过Actor模型实现Book这样充血模型的操作方法的一致性,也就是事务性。

回到事务ACID的隔离性与一致性属性话题上,让我们深入思考:这两个特性意味着如果一系列操作没有引起任何状态切换,那么,是不是就是可重复执行,像之前操作没发生一样呢?也就是是幂等操作?如果是幂等操作我们就可能无需再关注其是否需要事务了,反正失败了重新再来一次呗,但是如果有动作引起状态变化,那么我们就需要事务机制,以确保状态的一致性,因此,这里也给出我们一个判断何时需要事务机制的条件。

平时我们的状态切换都是使用数据库表实现,而不是像上面使用内存中的一个Book对象,因此事务ACID的隔离性需要依托数据库悲观锁或乐观锁提供的串行化机制才能实现ACID的一致性,如同上面Book对象中三个方法需要同步锁保证每个方法同时只能一个线程执行一样,比如多线程同时访问payment()方法,因为遭遇同步锁,变成一个个依次执行,如同一个个单线程串起来一样。

因此,是否可以产生这样一个经验?如果数据库的写操作基本都是需要事务的,因为数据库的增删改查四个动作中,增删改三个动作肯定是会引起数据库状态变化,它们都需要事务ACID机制,数据库乐观锁或悲观锁等策略提供不同粒度的串行化,如果对数据一致性要求越高,性能必然会所牺牲。这种串行化是有成本的, Amdahl法则描述如下:它是描述序列串行执行和并发之间的关系。

“一个程序在并行计算情况下使用多个处理器所能提升的速度是由这个程序中串行执行部分的时间决定的。”

大多数数据库管理系统选择(默认情况下)是放宽一致性高标准,以达到更好的并发性。
事务ACID的最后一个属性比较容易理解,事务一旦被提交,其引起的状态必须永久保存,这样即使在断电情况下也不会影响这些状态结果。

CAP是分布式系统中进行平衡的理论,它是由 Eric Brewer发布在2000年。

  • Consistent一致性: 同样数据在分布式系统中所有地方都是被复制成相同。

  • Available可用性: 所有在分布式系统活跃的节点都能够处理操作且能响应查询。

  • Partition Tolerant分区容忍性: 在两个复制系统之间,如果发生了计划之外的网络连接问题,对于这种情况,有一套容忍性设计来保证。
    事务

一般情况下CAP理论认为你不能同时拥有上述三种,只能同时选择其中两种,这是一个实践总结,当有网络分区P的情况下,也就是分布式系统中,你不能又要有完美的高一致性C和100%的可用性A,只能在这两者中选择一个。因此在实际操作中,我们总是降低一些一致性高标准,以及降低100%的可用性要求,一致性C和可用性A都各自妥协一点,不需要那么完美,不需要那么刚性,那么最终才能带来真正好的解决方案。

但是说到容易做到难,因为我们很多人都是靠关系数据库吃饭了N多年,已经对关系数据库有很深的依赖性了,特别是Java中的JTA事务机制能够让我们跨数据库安全操作数据,几乎在平时开发中根本不需要考虑数据正确性和一致性问题,集中使用SQL语句解决业务逻辑就可以了,现在要告诉他们JTA是一种2PC(两段事务提交two-phase commit),属于CAP中选择了完美的高一致性C和100%可用性,放弃了分区容忍性,估计比较难以接受。我们看看2PC两段事务的原理:







请到「今天看啥」查看全文