Nemo

Nemo 关注TA

路漫漫其修远兮,吾将上下而求索。

Nemo

Nemo

关注TA

路漫漫其修远兮,吾将上下而求索。

  •  普罗旺斯
  • 负责帅就完事了
  • 写了1,496,113字

该文章投稿至Nemo社区   Java  板块 复制链接


关于分布式系统中事务相关简单探索

发布于 2018/01/05 15:45 2,399浏览 0回复 4,269

数据的原子性对于一个系统而言,重要性不言而喻。


这里拿普遍的购物付款举个栗子:

        你在淘宝上买了一件商品,结账的时候,其实淘宝会帮你处理两件事情:

        1、从你的余额扣款。

        2、添加你的购物订单。

如果这时候,系统在扣除了你的余额后,忽然挂掉了,这时候该怎么办?这时候你的账户被扣钱了,但是你的订单并没有添加,淘宝的系统就会出现数据不一致的情况了。


类似的场景在各种类型的系统中都会有。

本质其实归纳起来并不复杂:当一张表的数据更新后,如何才能保证另一张表的数据也必须更新成功呢?


1、普通系统做法:

假设余额表为:t_balance,订单表为:t_order,假设商品的价格为100块钱,商品的ID为1,你在淘宝的ID为1001;

购物的系统处理可以用SQL这么表示:

 update t_balance set amt = amt - 100;    #从你的余额扣除100
 insert into t_order (user_id,goods_id) values(1001,1);    #添加你的订单  

正常来说,为了保证数据的原子性,我们一般都会使用事务来解决:

Begin transaction 
update t_balance set amt = amt - 100;    #从你的余额扣除100
insert into t_order (user_id,goods_id) values(1001,1);    #添加你的订单  
End transation
commit;

可以看到,我们在更新数据之前,开启了事务,把要执行的更新内容都包含在同一个事务之中,在最后才提交执行。

这是目前系统规模比较小的系统比较普遍的做法。

规模较小的系统的数据库表都在同一个机器上,所以使用这种本地事务的方式是可以很好的保证数据的原子性了。但是,如果系统规模比较大的情况下,数据表与表之间,并不会都放到一起,它们可能每张表都会单独分布在不同的物理机器上。

例如咱们的余额表和订单表。他们可能分属于不同的业务模块,可能作为整个系统里头的单独程序,所以他们可能会不在同一个数据库,也有可能不在同一个物理机器上。

如果它们并不在相同的数据库/物理机器上,那么也就意味着上述中可以运行得很好的本地事务在这时候就完全排不上用场了。

所以我们需要探索的分布式事务也就孕育而生。


2、分布式系统的做法:

在网络上检索分布式事务,可以很轻易的找到很多分布式系统事务相关的处理方式。比如这个“分布式事务 - 两阶段提交协议”。

2.1、两阶段提交协议(Two-phase Commit,2PC)

它的基本原则大概可以概括为:

阶段1:请求阶段(commit-request phase,或称表决阶段,voting phase)
在请求阶段,协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。在表决过程中,参与者将告知协调者自己的决策:同意(事务参与者本地作业执行成功)或取消(本地作业执行故障)。
阶段2:提交阶段(commit phase)
在该阶段,协调者将基于第一个阶段的投票结果进行决策:提交或取消。当且仅当所有的参与者同意提交事务协调者才通知所有的参与者提交事务,否则协调者将通知所有的参与者取消事务。参与者在接收到协调者发来的消息后将执行响应的操作。

它其实分为三个层:

第一层便是咱们的运用层。就是咱们的运用程序。
第二层是事务协调器层。用来处理和记录运用程序和数据中间的事务逻辑。
第三层是数据库运用层层。用来执行事务协调器传达的一些指令,进行数据库操作、数据库事务相关的逻辑等。

举个例子来说明下它是怎么工作的好了:

        在咱们之前提到的余额和订单的处理中,运用程序需要扣除用户的账户余额,并且为该用户添加订单。

        如果这个动作使用2PC处理,那么可以分为几个阶段:

1、运用程序发送请求到事务协调器,企图修改余额和订单数据。
2、事务协调器把运用程序的请求记录下来,顺便给数据库层t_balance和t_order节点分别发送请求,企图让它们开始修改数据。
3、数据库层节点收到来自事务协调器的请求后,先把请求记录下来,然后在本机开启事务,执行事务协调器请求中需要执行的sql,但是并不会马上提交事务,它根据sql的执行结果给事务协调器返回yes或者no,通知事务协调器当前sql的执行状态。
4、事务协调器在收到所有数据库节点返回的结果后,判断是否全为yes,如果全为yes,那么则再给数据库发送commit请求,让数据库层提交之前相应的事务;如果不全为yes,那么则给数据库层发送abort请求,让数据库层abort相应的未提交的事务。

这里需要注意的是,这里事务协调器和数据库层在收到请求后,都会把相应的请求记录下来。这主要是为了方便故障恢复用。

这个例子中,需要咱们额外编程的部分有两个:事务协调器和数据库层。

不过目前市面上已经有了一些已经完成了事务协调器层  和 数据库层的一些组件了,所以相对来说,这并不是困难所在。

但是通过上述过程,其实不难发现,这整个事务过程中,事务协调器与数据库处理层的通信太多,导致所有节点的事务时间会大大增加。

我们都知道,在某个事务中,其实也就是三部分过程:

1、开启事务;
2、执行过程;
3、提交事务。

这个过程相对没有事务来说,相对需要的时间会多一些,但是总的还在可以接受的范围内。

但是如果使用2PC后,数据库的节点可能会有很多,每个节点跟事务协调器层都会有网络通信。并且需要所有的过程都完成后,最终数据库才会将事务提交。

这也导致了一个问题:事务周期的延长。这意味这事务锁定的资源的变长了。最终导致总体资源等待的时间也不断增长。

简单来说,2PC存在严重的性能问题。一般我们在使用中,都不会这么使用。
现在网络上在谈分布式系统的事务时,不少人都在说的“避免分布式事务”大概也是因为这个。
所以我们需要一些别的手段来处理分布式系统中的事务问题。


2.2、避免分布式事务

因为分布式事务有着严重的性能问题,所以我们应该在分布式系统中避免分布式事务。

那么,我们该如何避免分布式事务,进而也能保证分布式系统中的数据的原子性呢?这是我们需要探索的重点。

其实现实生活中,人与人之间的一些交流活动中,也存在需要保持原子性的地方。

举个栗子:

还是说订单和余额好了。
你跟你的朋友去餐厅吃饭。
服务员会先给你拿菜单,然后你们开始点菜,服务员在一旁记录下你们需要点的菜。
过一会后你们吃完饭后,服务员拿来刚刚你们点菜票据,然后你们根据票据的记录,最后才跟餐厅结账。

这里的订单就是你们点的菜,余额扣除是最后的结账。

餐厅来来往往那么多人,为什么服务员能精确的给你们上你们点的菜?为什么在最后你们结账的时候,你们跟餐厅之间没有异议?(当然,这里的描述也不一定准确,服务员可能并不会根据每一个菜然后找单子给你们上菜。。。)

很明显,关键在服务员一开始给你们写的点菜票据。

所以在咱们的系统中,咱们也需要一个“点菜票据”。

放到一开始咱们的订单和余额上,我们应该扣除了余额后,生成一个“点菜票据”,这个票据上写着“给xx用户添加订单”。只要这个“点菜票据”保存妥当,我们最终还是能根据这个票据,“给xx用户添加订单”的。


那么,如何实现这个“点菜票据”,来达到我们维持数据一致性的目的呢?

2.2.1、消息记录方式避免分布式事务:

建立一张简单的消息表,这张表含有需要执行的sql内容,sql的执行状态等。message表:

 sql status
  
  

所以我们在扣除余额的时候:

Begin transaction 
update t_balance set amt = amt - 100;    #从你的余额扣除100
insert into message (sql,status) values('insert into t_order (user_id,goods_id) values(1001,1);','0')  #添加消息
End transation
commit;

只要余额扣除,那么我们就一定能拿到需要添加订单的记录消息。(这里直接保存sql的形式可能会有问题,这里举例子用)然后我们就能根据消息,给这个客户添加订单。添加完订单后,系统需要标记这个消息内容已执行。

不过从架构上说,这里的处理不够优雅,也存在一些问题。

1、首先,这里业务数据处理和消息处理耦合了。

2、最后的订单添加与消息标记不在同一个事务中,可能出现订单添加完成而消息未标记导致重复下单的情况。


解决方案也很简单:

1、message表不应该参杂在业务表中,它不应该设计为业务表,而应该是一个单独的服务。

2、消息处理应该在业务的事务周期之中。
3、系统应添加“对账”机制,确认每个消息的有效性。


具体的操作是:

   1、在扣除余额事务提交前,向消息服务发送消息,让消息服务记录下消息。消息服务器这时候只是标记新消息。

   2、只有当消息发布成功,扣除余额的事务才提交。

  3、扣除余额事务成功,向消息服务确认消息发送。如果扣除余额事务失败回滚,则向消息服务发送取消消息请求。而消息服务只有在得到确认后,才会标记消息为可用。

   4、业务系统定时扫描消息服务,用来执行下订单操作。订单操作跟余额扣除操作类似,需要确定订单操作完毕后,才请求消息服务,让消息服务标记消息已发送。

   5、业务系统定时扫描消息服务的未发送消息,根据消息来判定业务系统中的订单数据是否已经新添,进而标记一些因为订单标记成功但是标记消息已发送请求失败导致的脏数据为。

   6、业务系统添加消息确认表。每次在执行消息中的下订单指令之前,先确认该消息是否还未处理。避免出现重复下单的情况。


嗯,就写到这吧==


-END-

本文标签
 {{tag}}
点了个评