服务化与分布式事务冲突解析

Posted PersistentCoder

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了服务化与分布式事务冲突解析相关的知识,希望对你有一定的参考价值。

    互联网发展在加速,并且影响着每一个行业、每一个角落。在这么大的发展浪潮下,影响和变化最大的莫过于IT行业,IT行业又分为两种:传统软件行业和互联网行业,传统软件行业大多做ERP、OA居多,适用人群和并发量决定着单机垂直架构已经满足需求,反过来单机架构也决定了系统无法应对大访问量和并发量;互联网行业最典型的就是电商,其特点是访问量大、并发大,这就直接导致传统的软件架构和单机节点远远无法满足和支撑这么大的用户量和并发量。

    举一个例子,一个简单的系统使用tomcat+spring+springmvc+mysql搭建,分别部署在两台机器上:项目放到tomcat部署到机器1,mysql放到机器2上,这就是典型的单点垂直架构,我们都知道单机能够开启的线程数取决于硬件,假如机器1最多支持2000个线程,tomcat最多支持2000个并发量,那么机器1就最多可以配置2000的并发量,而机器2和mysql只支持1000并发,这种场景下tomcat接收2000的并发请求时访问DB就会出现大量的阻塞从而影响用户体验。

    对于互联网公司,这是不能接受的,肯定不能因为机器受限而损失用户,这个时候也就出现了水平横向扩展,分别在tomcat和DB层加机器,从而出现了最初的模型:


服务化与分布式事务冲突解析

    但是上述架构存在一些问题:

  1. 用户请求如何路由到tomcat1或者tomcat2?

  2. 对于用户的状态如何控制?例如用户1第一次访问到tomcat1登陆成功,

    session放在tomcat1上,但是下一次请求路由到tomcat2还要重新登录?

    并且同一用户在不同机器上状态不一致带俩很大的困扰。

  3. 用户新增了一条数据,持久化到了mysql1上,那么下次查询的时候在mysql2上发现没有之前的数据,不同数据库节点之间的同步问题如果解决?

  4. tomcat层太过臃肿,所有的请求接收和处理都在这一层完成,如果是一个很大型的项目,里边包含很多的业务和代码,会导致tomcat启动或重启特别慢,并且项目对机器的性能要求特别高。

  5. tomcat层访问DB层如何路由切换来营地并发问题?



     

    当然上述问题都有解决方案: 

  1. 对应问题1,至少有两种解决方案;①使用DNS轮询,同一个域名配置多个IP。每次请求过来,DNS轮询分配到指定IP对应的tomcat上。②使用nginx代理层,用户请求到nginx,然后nginx根据指定规则转发给对应的tomcat。

    第②种方式比①要好,因为如果其中一个IP对应tomcat挂了之后,仍然会有一部分请求路由到该IP,导致请求失败(因为没有动态将挂掉的IP从DNS轮询列表中剔除),而nginx可以轻松实现。

  2. 对于问题3,其实是多借点数据同步问题,解决方案是主从同步,具体实现分为一主一从、一主多从、互为主从以及多级同步,同步方式又分为同步复制、异步复制和半同步复制,基于性能的考虑,一般使用异步复制,但是异步复制存在数据延迟问题,举例:一主一从场景,写操作走主库,读操作走从库,但是一些时效性比较强的业务需要写完立刻看到结果,这种情况可以把读操作强制走主库。

  3. 第四个问题,可以归结为服务化的问题,也就是将tomcat层打薄,将接收请求和处理业务逻辑拆开,tomcat层只存放接受请求、简单参数校验以及处理结果封装的控制器层的代码,将业务逻辑处理下沉到单独提供服务的服务层,解决方案是使用rpc框架,将业务服务层单独封装独立提供服务,供tomcat中控制器层调用。

  4. 如果不做上一步中的拆分,可以使用mycat,shardingJdbc,TDDL(阿里未开源)或者自己实现,但是d步骤中做了分层之后,tomcat不再直接和DB打交道,该问题也就转变成了d中面临的问题,假使所有业务逗孩子啊一个库,我们可以使用开源的mycat解决。


     

        经过以上改进之后,架构大致变成了下图:

    经过分析得知,上述架构已经解决了一些问题,但是还有些问题如下:

  1. 业务问题;虽然上一环节将tomcat层打薄,业务也下沉了,但是过于简单粗暴地将所有业务逻辑沉到一个模块,所有的业务开发人员开发和维护同一个模块,不太合理,也容易发代码相互影响。

  2. 单点故障问题;nginx层、redis和mycat都是单点,nginx挂了之后所有请求进不来,redis挂了之后每次请求都要重新登录,mycat挂了之后所有DB访问失败。

     

    针对以上问题,可以考虑如下解决方案:

  1. 针对1,将业务逻辑拆分成不同的单元并梳理清楚依赖关系,比方说拆成支付、卡券和会员三个业务单元(正向依赖关系),然后三个模块独立启动并且由不同开发人员维护。


    经过再次改进后的架构大致如下:

    这种应该是比较成熟的高可用分布式架构,但是有一个问题来了,举一个场景:支付成功后需要向用户派发优惠券,如何保证数据一致性问题?

    我们先回顾一下,如果没有做所有上述的架构和业务数据库的拆分,那所有操作都由同一个jvm进程中的同一个事务管理器控制,那么事物提交和回滚是比较容易控制的,但是在分布式环境下,所有的操作都是以服务为业务单元,所以上述的操作已经跨了两个进程,简单的事务回滚机制无法保证数据一致性。

    不放分析一下上述操作的不同结果(支付为A,派发为B):

  1. A失败自己回滚,B不执行=>数据一致

  2. A成功事务提交,B失败自己回滚=>数据不一致

  3. A成功事务提交,B成功事务提交=>数据一致


    很明显,对于跨两个业务单元的场景,只需要解决第2种情况带来的不一致即可。如果在控制层(tomcat中)执行上述操作是无法保证一致性的,可以考虑业务下沉,不是在控制器中调用A和B,而是基于以上环节中pay依赖card前提下,将业务下沉到A中,也就是控制器层调用A,A中调用B,那么如果在A中先执行A的本地事务(支付),再执行B呢(A和B都加事务控制)?会有以下几种结果:

  1. A执行失败-回滚,B不执行 => 一致

  2. A成功,B失败,B在自己进程内回滚,A捕获B失败的异常自己回滚 => 一致

  3. A成功,B成功 => 一致

    可以看出,能够保证数据最终一致性,方案可行。

    那么如果一个业务场景比较复杂,垮了3个rpc服务的?比方说,用户支付成功后派发券,然后扣除会员账户总余额(支付A,派发B,扣款C),假如A中调用B和C,可能出现以下结果:

  1. A失败-rollback,B和C不执行 =>一致 

  2. A成功,B失败-rollback,A捕获异常也回滚,C不执行 =>一致

  3. A成功,B成功,C失败-rollback,A捕获异常也回滚 => 不一致

  4. A成功,B成功,C成功 =>一致

    同样,将A、C放到B中执行或者将A、B放到C中执行都无法保证一致。我们可以换一个思路,参考跨两个服务的操作,假如我们将业务继续合并下沉,也就是B、C操作放到B事务中执行能够保证一致性,然后将A、B合并到A中执行,同样也能保证一致性,这样在分布式中跨3个进程的服务调用我们也能够保证数据一致性。

    那如果一个业务操作中包含了n个模块的调用ABCDE···呢?是不是要从最后边一层一层合并到A中执行?先抛开性能不谈,却是能够保证数据一致性,但是回到文章开始的地方,我们为什么做分布式?为什么做业务拆分?如果按照上述做法,我们从最初的大项目拆成了多个业务单元,最后又全部合并了,变成了名义上分布式,实质上是代码上高内聚松耦合业务上低内聚紧耦合的系统,这一番周折意义何在?

    那么分布式系统中的分布式事务如何保证数据一致性呢?简单给出以下几个比较抽象的方案:

  1. 产品层面;将强一致性需求转变成若一致性需求,或者说从设计角度规避分布式场景强一致性

  2. 强一致性但相对简单的业务场景;比方说只跨两个服务单元,可以考虑业务下沉与合并

  3. 强一致性并且比较复杂的场景,考虑使用分布式事务中间件,例如TXC或者自己实现

  4. 业务场景复杂但是可以接受最终一致性(ACID中牺牲CI),可以考虑本地消息表,TCC模式,消息事务等



谢谢参读,如有不周可以直接联系本人或者留言!



以上是关于服务化与分布式事务冲突解析的主要内容,如果未能解决你的问题,请参考以下文章

微服务39分布式事务Seata源码解析七:图解Seata事务执行流程之开启全局事务

微服务中台技术解析之分布式事务方案和实践

最终一致分布式事务方案解析

MQ关于实现最终一致性分布式事务原理解析

《分布式技术原理与算法解析》学习笔记Day06

蚂蚁金服一面:十道经典面试题解析