[微服务感悟] 很好理解的分布式事务

事务是保证一系列操作是一个整体,要么都执行,要么都不执行。比如A给B转账,A扣钱了,B的账户的钱也要加上去,不能出现A扣钱B不加钱,或者B加钱A不扣钱的情况。在单体程序中,数据库和spring框架已经解决这个这个问题,我只要在需要事务的方法上加上@Translate,或者在Spring配置中某一层甚至全局事务。对于我这种CRUD程序员,最初的2年一直在写代码,居然还不知道事务是什么东西,这说明在单体程序开发中,事务已经被处理的很好了,和我们程序员关系不大,第二也说明不要一直写CRUD的代码,那是在浪费生命。

事务的四大特性是原子性,一致性,隔离型,持久性,简称ACID,上面只有原子性和一致性,其余的可以百度看看。

// springboot配置全局事务
@Bean(name = "txAdviceAdvisor")
public Advisor txAdviceAdvisor() {
  AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
  pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
  return new DefaultPointcutAdvisor(pointcut, txAdvice());
}

在单体服务拆分成多个微服务,一个数据库拆成多个数据库后,原有的事务可能会横跨拆分的多个微服务和其对应的多个数据库的情况。这种横跨多个服务的事务,就是分布式事务。

实际上无论使用什么样的分布式事务,它都会增加程序的复杂度,增加程序的性能消耗,增加程序的不稳定性。对于分布式事务,我们第一考虑是能不能把这个事务放在一个单体服务中,比如微服务拆分不合理导致的分布式事务,解决方案应该是合理的拆分/合并服务,而不是满脑子撞在分布式事务上。

对于分布式事务,最初大家想出的解决方案和单体程序中的事务一样,就是对代码无侵入,程序员感知不到,DB和框架它们自己配合解决,于是提出了两段提交法。

两段提交

两段提交法是事务管理器(Transaction Manager)协调各服务的本地事务来实现分布式事务的一种方式。两段提交存在一个事务管理器服务,业务方将事务提交到事务管理器,事务管理器向所有的资源管理器Resource Manager(数据库)发送预执行操作,并等待所有的资源管理发送执行结果;如果都执行成功,再想所有资源管理器发送commit操作;如果存在失败,则向所有资源管理器发送rollback操作。它依赖于TM和各服务的本地事务。

现在市面上有很多种数据库,mysql,oracle,sqlserver等,也有许多两段提交的事务框架和服务,怎么让不同的数据库都能理解各种分布式事务框架发出的指令呢?这需要大家一个都支持的公认协议,目前大家都支持的公认协议是XA。也就是支持XA的数据库和XA的分布式框架组合就能支持两段式分布式事务。mysql从5.7版本开始支持XA协议。

事务的提交,事务管理器对各个服务的本地事务的协调,本地事务的处理,这些操作对我们这些开发人员都是感知不到的。我们要想使用分布式事务,只要集成某个分布式事务的框架客户端,在程序中配置一下事务管理器地址,在方法上加个注解或者配置全局事务,分布式事务就可以生效并工作了,使用起来和单体程序开发时没有什么不同。

这么容易上手的,基本没有学习成本的分布式事务,大家一定都会选择使用吧?但实际情况并不是如此,基本互联网企业没有使用它的,为什么会这样呢?

先说说微服务的一个思想吧,就是随着服务越来越多,其中某些服务节点发生故障的概率越来越大,做个最坏的估计在服务集群中,每时每刻总会有服务节点出现故障。采取两段提交,最大的隐患是所有服务的事务都由事务管理器来执行,如果事务管理器服务出现问题,就会导致几乎所有服务的事务都会无法执行,整个服务群就崩了,这是它最大的一个隐患。

这里补充一个有趣的项目,针对集群服务器中一定会随机挂掉的想法Netflix公司开发出了一个叫疯狂猴子的项目,它的作用就是随机关掉某些生产环境上的一些服务,Netflix的开发者认为与其害怕服务挂掉,不如拥抱服务挂掉这种情况,把服务降级,熔断处理好一点。Netflix称,自从在生产上上线,他们的服务的抗风险能力强多了。

同时它还存在一个致命的性能问题,从事务管理器向资源管理器(DB)发送预执行到发送commit这个阶段,数据库会把数据给锁住,其他访问该数据的数据库请求会被阻塞排队,当事务管理器发送预执行后挂机了,没有发送commit/rollback指令,资源管理器(DB)会一直锁住那部分数据不让其他数据库连接使用。对于高并发,大量数据读写的情况下,一旦一部分数据被锁住,将会瞬间堆积很多的操作请求,服务一下就可能出现多个连锁超时反应,甚至导致雪崩熔断这种事情。一般数据库层面的锁,都使用version做乐观锁而不是用select from update这种排它锁,也是这个原因。

这个分布式事务的处理方式正是由于有这两大隐患,所以各大互联网公司基本不采用它,而宁愿麻烦一点通过业务代码的方式实现分布式事务。

下面是两段提交分布式事务的流程图
[微服务感悟] 很好理解的分布式事务_第1张图片

tcc分布式事务

tcc是一种通过重复补偿来实现分布式事务的做法,在介绍tcc前,先说一个之前实现的简单的,基于重复补偿思路的分布式事务的做法。

那是快递柜的开门取快递并后关门的业务处理:用户关门后,柜机基础服务的将格口状态改为空闲,包裹服务将改包裹状态改为已取出,这里的修改格口状态和修改包裹状态就是一个分布式事务。但是我这里的业务,快递都已经被拿走了,结果已经注定,我需要的就只是数据的最终一致,只需要重复执行直到最终执行成功就可以了,有可能存在数据某一时刻的不一致(格口状态改了,包裹单状态没改),但对整体业务没有影响,保证最终一致性就可以了。

实现是这样的,业务服务异步调用柜机服务的修改格口状态和包裹服务的修改包裹状态,如果返回执行失败,就把任务放入到队列(RabbitMQ)中,业务服务有个线程不断拿mq的任务并执行,修改格口状态和修改包裹状态这两个接口是幂等接口,即如果接口已经执行成功,遇到再次重发的请求就不再执行,直接返回执行成功的结果。

[微服务感悟] 很好理解的分布式事务_第2张图片

tcc是Try - Confirm - Cancel的简写,它没有事务管理器这个角色,事务的控制需要程序员自己写在每个发起事务的服务中。这样,每个服务都可以发起,控制事务,而不用都经过一个事务管理器。同时,它也不依赖数据库的本地事务,它每个服务的事务都需要程序员编码实现,在接口层实现try, confirm, cancel的逻辑。这样他就能解决两段式提交中事务都要由事务管理器控制的风险,也能解决本地事务在事务期间内一直使用排它锁锁住数据的问题。但是它极大的增加了程序员的实现业务的难度,因为这一套都需要程序员自己设计并实现。

它的代码大致是这样的

if (try()) {
  commit();
} else {
  cancel();
}

使用这种分布式事务,需要在事务的各个服务都实现try, confirm, cancel三套逻辑接口,业务发起方在事务开始时统一调用各微服务的try接口,这是一个预执行方法,一般这个方法并不会直接修改各业务数据,只是尝试是否能够执行,它是为confirm执行做准备的;如果所有服务的try都执行成功,再调用各服务confirm方法,这时才做业务数据的真正修改;如果try执行过程中发生错误,调用各服务的cancel方法,将数据回滚。try, confirm, cancel的逻辑怎么实现,需要开发者自己去思考怎么实现。

我这里有个tcc事务实现的demo,它模拟的业务是智能仓储柜,OA系统下发入库订单,员工看到订单后将对应的货物放入柜机某一个格口,关门在柜机屏幕上确认放入。后台将订单状态改为完成,柜机库存进行修改,柜机格口状态进行更新。

说说实现,有4个服务,除了eureka是服务中心外,业务服务为订单服务,柜机服务,库存服务。业务由订单服务发起,订单服务会调用库存服务增加库存,柜机服务更新格口状态。订单服务先会调用库存服务和柜机的try接口,如果try执行成功则调用他们commit接口;如果失败调用它们的cancel接口。在try的环节时,会检查订单的状态和该格口的可用库存的数量是否支持这次入柜,并修改订单表的执行状态,库存表的锁定库存和执行状态,通过执行状态字段把数据锁住;在confirm的环节,修改order状态,库存数据,格口状态;在cancel环节,还原数据。

confirmcancel执行失败后,会重复执行直到成功或失败次数到达阀值后记录并转人工处理。为什么这么做,因为try执行成功,说明执行confirm一般一定会成功的;而cancel是数据回滚,也一般是可以成功的,所以这两个方法执行失败了,重复执行有大几率最终执行成功。

github项目地址:https://github.com/programluo/tcc-demo,采用springCloud+h2db技术实现。

这里分享一篇介绍TCC事务具体实现的博客,写的很好 - 终于有人把“TCC分布式事务”实现原理讲明白了!。但博客中例子存在一个小问题,try阶段如果库存服务压根没执行锁定库存+2,就报错了,来到cancel阶段时,执行锁定库存-2,这会导致锁定库存平白的减了2,而实际上这个数应该保持不变。我认为cancel不能通过++--的方式去还原,还要通过不变的数据计算还原。

总结

两段提交的优点是代码无侵入,缺点是存在一个事务管理器,会有事务管理器不可用导致整个服务集群不可用的风险;它的事务依赖db,而db会把数据锁住,直到事务结束,这个也是很大的风险项,会导致一段时间对改数据的请求全部堆积。

tcc更适用于分布式场景,因为在没有一个中心的事务管理器存在,每个事务的管理器都在事务发起业务本身,这样它会避免因事务管理器故障导致整个集群所有服务的事务都无法执行的严重隐患;同时锁定资源采用程序员自己的实现方式,不依赖数据实现,可以避开数据库使用排它锁锁住数据导致堵塞的情况。

tcc分布式事务应该是最流行的分布式事务的处理方式了,目前TCC分布式事务的框架有很多,比如tcc-transaction。不过,现在最火的还是分布式事务的框架还是阿里的Seata,它支持TCC 、XA、 AT、SAGA 事务模式,有阿里背书,这个框架很快就流行了,恐怕以后就是行业标准吧。

你可能感兴趣的:(编程心得)