生成订单写入分库1,白条扣款写入分库2

1、银行取钱事务说明ACID

原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)

如果同时完成了取出钱和账户的更改,那就是原子的。如果账户减少的钱等于取出的钱,那么就是一致的。如果这个过程不受其他程序并发读写你账户的程序的影响(比如你的女朋友们正在并发的刷你的银行卡),那么它是隔离的。一旦事务完成了,(无论完成之后机器宕机、断电、还是网络异常)账户的余额必然会反映取款后的情况,那么它是永久性的。

2、

Vitess告诉你两阶段提交到底长啥样

https://mp.weixin.qq.com/s/RQZ4f7FQqor61HyxQOa-Rg

 

Vitess告诉你两阶段提交到底长啥样

来这里找志同道合的小伙伴!

 

>>>>  背景

 

Vitess之前,先复习一下事务的四个基本特性:

 

●   原子性:一个事务对状态的改变是原子的,要么都发生,要么都不发生,这些改变包括数据库的改变、消息以及对转换器的操作。

●  一致性:一个事务是对状态的一个正确改变。作为一组操作没有违反任何与状态相关的完整性约束。这要求事务是一个正确的程序。

●  隔离性:尽管事务是并发执行的,但看起来是单个执行的,即对于一个事务T,任何其他事务要么在T之前执行,要么在T之后执行,但不会既在T之前执行,又在T之后执行。

●   永久性:一旦一个事务成功完成(提交),它对状态的改变不会受其他失败的影响。

 

举一个银行取钱事务的例子。如果同时完成了取出钱和账户的更改,那就是原子的。如果账户减少的钱等于取出的钱,那么就是一致的。如果这个过程不受其他程序并发读写你账户的程序的影响(比如你的女朋友们正在并发的刷你的银行卡),那么它是隔离的。一旦事务完成了,(无论完成之后机器宕机、断电、还是网络异常)账户的余额必然会反映取款后的情况,那么它是永久性的。

 

事务是数据库的核心特性,MySQL、ORACLE、PostgreSQL这些数据库都是支持事务的。国内互联网公司更多的使用MySQL。

 

但是互联网公司的数据一般比较大,单机数据库服务难以承担这么大的数据量。这时候一般会做分库,将原来一个数据库的数据拆分到多个库(这里简单认为一个MySQL实例上只有一个库),比用户表做拆分,根据用户id,id对64取余数,然后根据得到的余数定位特定的分库。比如id等于1的用户的信息就在第1个库,id等于2的用户数据就在第2库,这样就解决了单机数据容量问题。

 

现在用户1要下单了,需要白条扣款和生成订单两个步骤。两个步骤需要保证原子性的,要么扣款完成、订单生成,要么不扣款、不下单(all-or-nothing)。如果订单和白条表都是按照用户id做拆分的那最好,所有操作都会在同一个库上面进行,使用单机事务,MySQL就保证了原子性。如果白条表是按照订单id拆分的,那很可能不在第1分库了😢。这时候就涉及到分布式事务。

 

本来事务的执行流程应该是这样:

    BEGIN;

    生成订单; 白条扣款;

   COMMIT/ROLLBACK

 

问题就出在分布式场景中的提交事务。我们需要连接两个库开启两个分库的两个事务,TX1和TX2, TX1生成订单写入分库1,TX2执行白条扣款写入分库2。

 

在业务代码里执行时间线(t1到t5表示先后的时间点)会是这样的:

 (t1)SESSION1 BEGIN TX1;

 (t2)SESSION2 BEGIN TX2;

 (t3)SESSION1 生成订单,订单信息写入分库1;

 (t4)SESSION2白条扣款写入分库2;

 (t5)SESSION1 COMMIT TX1;

 (t6)SESSION2 COMMIT TX2.

 

如果t5执行正常,给客户下单了,分库2宕机了,提交TX2失败,TX2自动回滚。分布式事务只提交了一部分,订单生成了,但是白条没扣款(我做游戏的时候都是先扣钱、扣钱成功再发装备😏)。这就经典的是部分提交(PARTIAL COMMIT)问题。

 

在分布式场景中保证原子性避免部分提交,有一个办法,那就是两阶段提交(TWO PHASE COMMIT)。

 

上面的分布式事务涉及到了分库1和分库2。其实还有一个角色,就是执行业务逻辑的worker节点,worker在两阶段提交过程中也是一个重要的角色,我们称之为事务管理器(Transaction Manager),事务管理器在两阶段提交过程中是一个协调者的角色。事务管理器负责整个事务的开始、执行、回滚或者提交。事务管理器异常也可能导致整个事务的异常,比如worker节点在t5之后,t6之前宕机,也会导致整个事务的部分提交。我们也给分库1和分库2一个专业的名字,叫做资源管理器(Resource Manager),资源管理器在分布式事务中是参与者的角色,负责执行协调者下达的指令,此外资源管理器是能够支持本地事务的。

 

MySQL提供了XA分布式事务接口供外部协调者调用。

 

关于XA事务的两个概念:

 

●  资源管理器(Resource Manager):用来管理系统资源,是通向事务资源的途径。数据库就是一种资源管理器。资源管理还应该具有管理事务提交或回滚的能力。

●  事务管理器(Transaction Manager):事务管理器是分布式事务的核心管理者。事务管理器与每个资源管理器(Resource Manager)进行通信,协调并完成事务的处理。事务的各个分支由唯一命名进行标识。

 

上面例子中的生成订单白条扣款的分布式事务中,MySQL服务器相当于XA事务资源管理器,与MySQL链接的客户端,也就是执行业务逻辑的worker节点相当于事务管理器。

 

但是MySQL早期的版本XA事务存在BUG,Vitess自己实现了整套两阶段提交,所以Vitess是学习两阶段提交的一个很好的资源。Vitess地址: https://github.com/vitessio/vitess

 

>>>>  Vitess的几个概念

 

 


Vitess架构图

 

上面是Vitess的整体架构,两阶段提交过程中涉及到的模块主要是vtgate和 vttablet、MySQL。

 

vttablet和MySQL其实可以看做一个整体,我们称之为Shard。Shard在两阶段提交中是参与者,也就是资源管理器。

 

vtgate是事务管理器,也就是事务的协调者,业务的应用Application连接到vtgate上面,vtgate通过路由将业务的请求转发到后端的一个或者多个Shard,然后在将结果汇总发生Application。Application发起一个事务,比如插入两条数据,根据vtgate做路由后可能将两条数据插入到一个或者两个Shard。

 

上图中的三个Shard是三个资源管理器,也是两阶段提交的参与者。vtgate是事务管理器,是两阶段提交的的协调者。

 

vtgate到vttablet请求是通过grpc实现,vttablet维护了到MySQL的连接池。

 

对于非事务请求,vtgate请求发送到vttablet,vttablet随便从连接池里拿到一个连接,执行SQL,返回结果给vtgate,然后将连接放回到连接池,这样一个请求单独占用连接的时间很短。

 

对于事务请求,vtgate发生请求到vttablet,vttablet会从事务连接池里拿到一个连接,并生产一个本地事务id,执行SQL,将执行结果和本地事务id返回给vtgate,最后将事务连接放入一个单独的activePool,这样下次vtgate需要在该事务里继续执行SQL,只需要请求带着事务id,通过事务id从activePool拿到事务连接,执行SQL即可,直到vtgate提交或者回滚事务,vttablet才将连接从activePool放回到事务连接池。

 

下面的代码就是vttablet根据本地事务id从activePool拿连接:

// Get fetches the connection associated tothe transactionID.

// You must call Recycle on TxConnection oncedone.

func (axp *TxPool) Get(transactionID int64,reason string) (*TxConnection, error) {

    v,err := axp.activePool.Get(transactionID, reason)

    if err != nil {

       returnnil, vterrors.Errorf(vtrpcpb.Code_ABORTED, "transaction %d: %v", transactionID, err)

    }

    return v.(*TxConnection), nil

}

 

>>>>  Vitess的两阶段提交实现

 

回到上面的分布式事务,vtgate要在第一个Shard和第二个Shard写入数据,vtgate在两个SESSION里面开启两个事务,执行并记录SQL。

     (t1)SESSION1 BEGIN TX1 IN Shard1; 

     (t2)SESSION2 BEGIN TX2 IN Shard2

     (t3)SESSION1 生成订单写入Shard1未提交,将对应SQL语句作为redo log记录在内存中

     (t4)SESSION2白条扣款写入Shard2未提交,将对应SQL语句作为redo log记录在内存中

 

除了把事务中执行的SQL记录在内存中之外,前面四个步骤和之前没有任何太多区别,区别就在最后的提交阶段。下面我们结合代码看Vitess是如何做两阶段提交的。

 

下面是Vitess中vtgate作为协调者的代码模块,结合代码分析Vitess是如何做两阶段提交的。

1 func (txc *TxConn)commit2PC(ctx context.Context, session*SafeSession)error {

2  if len(session.ShardSessions) <=1 {

3    return txc.commitNormal(ctx, session)

4  }

5

6 participants :=make([]*querypb.Target, 0, len(session.ShardSessions)-1)

7for _, s :=range session.ShardSessions[1:] {

8   participants =append(participants, s.Target)

9 }

10

11 mmShard := session.ShardSessions[0]

12 dtid := dtids.New(mmShard)

13 err := txc.gateway.CreateTransaction(ctx,mmShard.Target, dtid, participants)

14if err != nil {

15// Normal rollback is safe because nothing was preparedyet.

16     txc.Rollback(ctx, session)

17   return err

18  }

19

20 err = txc.runSessions(session.ShardSessions[1:], func(s *vtgatepb.Session_ShardSession)error {

21  return txc.gateway.Prepare(ctx, s.Target,s.TransactionId, dtid)

22 })

23

24if err != nil {

25    if resumeErr := txc.Resolve(ctx, dtid); resumeErr != nil {

26      log.Warningf("Rollback failed after Prepare failure:%v",resumeErr)

27 }

28// Return the original error even if the previousoperation fails.

29      return err

30 }

31 err = txc.gateway.StartCommit(ctx, mmShard.Target,mmShard.TransactionId, dtid)

32if err != nil {

33   return err

34 }

35 err = txc.runSessions(session.ShardSessions[1:], func(s *vtgatepb.Session_ShardSession)error {

36   return txc.gateway.CommitPrepared(ctx, s.Target,dtid)

37 })

38if err != nil {

39  return err

40 }

41return txc.gateway.ConcludeTransaction(ctx, mmShard.Target,dtid)

42 }

 

从代码2~4(表示第2行到第4行,下同)可以看到,对于只涉及一个Shard的事务,不会涉及部分提交问题,MySQL的事务就可以保证原子性,不需要走两阶段提交,直接提交即可。

 

两阶段提交主要分为以下几个步骤:

 

1、生成分布式的事务id(11~12)

 

分布式的事务id为第一个参与者的Shard信息+该Shard的本地事务id,这里我们简单的认为是Shard1:TX1,我们将该Shard1称之Shard1:TX1这个分布式事务的MetadataManager Shard,因为Shard1上管理该分布式事务的元数据,缩写成mmShard。

 

注意Shard1是Shard1:TX1这个事务的mmShard,因为该分布式事务的第一个参与者是Shard1。

 

如果分布式事务的参与者是Shard3上面的TX3和Shard2上面的TX2,Shard3是第一个参与者,那么该分布式事务的id为Shard3:TX3。Shard3为该分布式事务的mmShard。这样mmShard能够比较均衡的分布式在所有Shard中,一方面可以避免对mmShard操作造成热点,另外也可以避免单个Shard故障影响整个Vitess集群。

 

分布式事务id定义成Shard:TX是有意义的,这样后续的流程中可以根据分布式事务id直接知道该分布式事务的元数据信息在哪个Shard上面,方便读取。

 

2、在mmShard记录元数据信息(13~18

 

记录分布式事务的元数据信息到mmShard的MySQL数据库中(也就是记录到Shard1的MySQL中),记录的信息包括分布式事务id、事务当前状态、开始时间,其他参与者(participants)

 分布式事务id     事务当前状态           事务开始时间            事务的其他所有参与者信息 

  Shard1:TX1     初始Prepare状态    比如1989-09-20 00:00:00    Shard2:TX2

 

此处所有的记录操作不是在Shard1的事务TX1,而是单独使用了一个连接,因为记录的元数据信息需要直接提交写入磁盘,如果放入TX1中就连同TX1一起提交了。

 

3、Prepare阶段(20-22

 

给所有参与者发送Prepare指令,参与者接收到带有本身事务id的Prepare指令之后:

  1. 通过本地事务id(TX1、TX2)将事务连接从普通的事务连接池拿出来放到一个叫做preparedPool的特有连接池,preparedPool通过 分布式事务id作为key,也就是Shard1和Shard2都是通过Shard1:TX1作为key,SESSION1和SESSION2 使用的事务连接作为value存储到preparedPool中。

    注意普通事务连接池和preparedPool不一样的地方是,普通事务连接池以本地事务id作为key存储、查找的,preparedPool中的连接是分布式事务id作为key来存储查找的。问题1,为什么需要一个特殊的preparedPool呢?

  2. 将之前事务TX1、TX2里记录的redo log,也就是在事务里执行的SQL对于的全局事务id号 Shard1:TX1,记录到本地数据库中,注意是需要使用单独的新的连接,写入数据库并提交(此时不能使用TX1、TX2使用的连接提交redolog,这样就把TX1、TX2一起提交了)。redo log写入到数据库之后,即便TX1、TX2没有提交、即便vttablet发升重启、如果需要我们也可以重新执行redo log。

 

4、StartCommit阶段(30~34

 

在步骤2,我们将事务的元数据信息记录到了mmShard(也就是Shard1)的MySQL数据库中,记录的状态是Prepare状态。现在将Prepare状态修改成为Committed状态。这是一个关键步骤,修改该状态之前出任何异常我们都认为事务尚未提交,修改成为Committed状态之后,出任何异常我们都认为事务已经提交。出现异常之后,我们需要做的就是根据记录下的该状态做相应的补偿措施。

 分布式事务id     事务当前状态           事务开始时间            事务的其他所有参与者信息

  Shard1:TX1     Committed状态   比如1989-09-20 00:00:00    Shard2:TX2

 

在Shard1上记录事务状态标记为Committed之后,我们就认为两阶段提交成功了,所以这时候我们可以顺便把Shard1上面的TX1也提交了。使用单独的连接先修改事务状态再提交TX1,那不如直接在TX1里修改当前事务状态,直接提交TX1,效果一样一样的。

 

在是否在TX1里直接修改状态,也是一个有意思的问题,如果直接在TX1里修改状态,之后马上提交,那么修改状态和提交操作在一个事务里,两个行为必然是原子操作,如果没有在TX1里修改事务状态,而是使用了单独的连接修改,那么我们在做异常处理的时候就需要多考虑一种情况,状态修改成功但是TX1未提交成功。

 

5、CommitPrepare阶段(35~37)

 

vtgate给出mmShard之外的其他参与者发送CommitPrepare指令,分布式事务id Shard1:TX1作为参数。

 

因为Shard1是mmShard,mmShard在StartCommit阶段已经把事务提交了,这里以Shard2为例子说明,Shard2上面的vttablet接受到CommitPrepare指令之后,根据分布式事务id Shard1:TX1去preparedPool里面取出之前的事务连接,TX2执行了白条扣款但是尚未提交。之前提到,在Prepare阶段,我们还在本地数据库里记录了redo log和对应的全局事务id,这时候拿到了事务连接,先根据全局事务id删除redo log,然后执行Commit。

和StartCommit步骤一样,删除redo log直接放到TX2事务中,这样能保证两个操作的原子性,避免部分成功,否则我们还需要多处理一种异常。

 

6、ConcludeTransaction结束分布式事务(41~41)

 

只需要根据分布式事务id Shard1:TX1 删除Shard1上面的元数据信息。

 

>>>>  异常分析

 

上面是正常的两阶段提交流程,但是两阶段最核心的任务是处理异常,下面我们看看Vitess是如何处理各种异常的。

 

1、创建分布式事务异常(14~17

 

对所有Shard发送Rollback指令,这样Shard1回滚TX1,Shard2回滚TX2。如果是Shard1节点异常导致的写入分布式事务异常,那么Shard1回滚也会报错,不过没关系,反正事务TX1没有提交。Shard1回滚异常也不影响其他Shard执行回滚操作。

  // Rollback rolls back the currenttransaction. There are no retries on this operation.

func (txc *TxConn) Rollback(ctxcontext.Context, session *SafeSession) error {

    if !session.InTransaction() {

       returnnil

    }

   defer session.Reset()

    return txc.runSessions(session.ShardSessions,func(s *vtgatepb.Session_ShardSession) error {

       return txc.gateway.Rollback(ctx, s.Target,s.TransactionId)

    })

}

 

对万一在mmShard已经将分布式事务的元数据写入了呢?这个问题和后面的ConcludeTransaction类似。

 

2、Prepare异常(24~27)

 

创建事务,写事务元数据信息到Shard1(mmShard)成功,Prepare阶段异常,因为当前事务状态仍然是Prepared状态(未提交状态),按照我们理解如果Prepare异常直接rollback就好了,但是在commit2PC函数中并未直接rollback,而是调用了Resolve函数,为什么呢不直接rollback而是Resolve呢?我们先看Resolve函数的实现。

 // Resolve resolves the specified 2PCtransaction.

1func (txc *TxConn) Resolve(ctx context.Context, dtidstring) error {

2   mmShard, err :=dtids.ShardSession(dtid)

3   if err != nil {

4       return err

5   }

6

7   transaction, err:= txc.gateway.ReadTransaction(ctx, mmShard.Target, dtid)

8   if err != nil {

9       return err

10  }

11  if transaction == nil || transaction.Dtid == "" {

12      // It was already resolved.

13      returnnil

14  }

15  switch transaction.State {

16  case querypb.TransactionState_PREPARE:

17      // If state is PREPARE, make a decision torollback and

18      // fallthrough to the rollback workflow.

19      if err := txc.gateway.SetRollback(ctx, mmShard.Target,transaction.Dtid, mmShard.TransactionId); err != nil {

20          return err

21      }

22      fallthrough

23  case querypb.TransactionState_ROLLBACK:

24      if err := txc.resumeRollback(ctx, mmShard.Target,transaction); err != nil {

25          return err

26      }

27  case querypb.TransactionState_COMMIT:

28      if err := txc.resumeCommit(ctx, mmShard.Target,transaction); err != nil {

29          return err

30      }

31  default:

32      // Should never happen.

33      return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "invalid state: %v", transaction.State)

34  }

35  returnnil

36}

 

创建首先根据事务idShard1:TX1拿到mmShard,也就是Shard1,然后去Shard1读取该事务的元数据信息,发现事务是Prepare状态,尚未提交:

 分布式事务id     事务当前状态           事务开始时间            事务的其他所有参与者信息

  Shard1:TX1     Prepared状态   比如1989-09-20 00:00:00    Shard2:TX2

 

创建首先修改mmShard事务状态为回滚状态SetRollback

分布式事务id     事务当前状态           事务开始时间            事务的其他所有参与者信息

 Shard1:TX1     Rollbck状态       比如1989-09-20 00:00:00    Shard2:TX2

 

创建Resolve函数中22行fallthrough表示,如果SetRollback成功无异常,那么继续执行下一个case,即resumeRollback。

 

创建通过下面的resumeRollback函数我们明白了,不能简单的执行Rollback操作是因为可能一些分片已经Prepare成功并记录了redo log,我们需要删除redo log。

此外,上面的Prepare阶段提到过,Prepare会把事务连接以分布式事务idShard1:TX1作为key,存储到 preparedPool中,现在回滚Prepare,还需要将preparedPool中的以Shard1:TX1为key的连接放回到普通的事务连接池,这样后面的事务请求可以继续使用,避免连接泄露。

 

所以我们是需要做一些对之前Prepare操作的清理工作,对每个Shard执行RollbackPrepared操作,删除redo log,将连接从preparedPool放回事务连接池避免连接泄露。

1 func (txc *TxConn) resumeRollback(ctx context.Context,target *querypb.Target, transaction *querypb.TransactionMetadata) error {

2   err :=txc.runTargets(transaction.Participants, func(t *querypb.Target) error {

3       return txc.gateway.RollbackPrepared(ctx, t,transaction.Dtid, 0)

4   })

5   if err != nil {

6       return err

7   }

8   return txc.gateway.ConcludeTransaction(ctx, target,transaction.Dtid)

9}

最后,删除mmShard上面记录的事务元数据信息,resumeRollback函数第9行删除mmShard的元数据信息,结束分布式事务。

 

3、StartCommit异常(32~34

 

创建前面提到过,StartCommit就是修改分布式事务状态,这是一个原子性的操作,修改事务状态只要修改成为Committed状态,就认为事务是提交状态。如果状态还是prepared状态,那么就认为事务没有提交。

当然StartCommit也可以发生异常,返回错误,甚至数据库已经修改成功,但是网络或者其他原因,vtgate没有收到,仍然需要进行错误处理。此处的错误处理逻辑和CommitPrepared是一样的。此时给客户端返回一个错误码。

 

对于这种异常的两阶段提交,Vitess采取了补偿策略。

 

创建Vitess的策略是在每个Shard的vttablet上开启一个watchDog。watchDog定时去去读存储在MySQL上面的分布式事务元数据信息,只有事务执行完成才会将元数据信息删除。

前面我们提到,mmShard记录了事务的开始时间,那vttablet又定义了一个事务超时时间,比如10秒。对于超过10秒未删除的分布式事务,vttablet自动给vtgate集群(注意这里是集群,可以是任何一个vtgate)发送ResolveTransaction指令,告诉vtgate可能有事务异常了,需要vtgate看看咋回事,ResolveTransaction传递一个分布式事务id作参数,比如Shard1:TX1。vtgate收到ResolveTransaction指令后,对分布式事务进行Resolve,Resolve函数前面已经介绍过一次,现在又派上用场了。

 

回到上面的StartCommit异常,可能StartCommit已经修改事务状态未成功,事务状态仍然为prepare。这时候和处理prepare异常一致,RollbackPrepare。

如果修改事务状态成功,事务状态已经成为Committed状态。从Resolve函数中(27~30)可以看到,执行resumeCommit。

1 func (txc *TxConn) resumeCommit(ctx context.Context,target *querypb.Target, transaction *querypb.TransactionMetadata) error {

2   err :=txc.runTargets(transaction.Participants, func(t *querypb.Target) error {

3       return txc.gateway.CommitPrepared(ctx, t,transaction.Dtid)

4   })

5   if err != nil {

6       return err

7   }

8   return txc.gateway.ConcludeTransaction(ctx, target,transaction.Dtid)

9 }

 

从代码中可以看出,resumeCommit其实就是再StartCommit的基础上继续往下执行,继续对每个Shard执行CommitPrepared,然后执行ConcludeTransaction删除mmShard上的事务员数据。这时候某些Shard可能已经成功执行过CommitPrepared,这样可能会重复执行。嗯,没错,CommitPrepared函数是幂等的,调用一次和调用多次效果一致。

 

4、CommitPrepared异常(38~40

 

最后一步骤,提交事务,Shard1提交TX1,Shard2提交TX2。

 

此处CommitPrepared函数的参数是分布式事务id,即Shard1:TX1。Shard1和Shard2的vttablet接受到vtgate发送的rpc请求后根据参数分布式事务id去preparedPool里拿到TX1和TX2所使用的事务连接,然后使用该连接,也就是在TX1和TX2两个事务中,分别删除对于的redo log,然后提交事务,同样在TX1和TX2中删除redo log保证了,删除redo log和提交事务是一个原子操作。

 

该流程可能是commit本地事务异常,也可能是commit都成功返回成功信息时候网络异常等。

 

无论何种异常gate收到的是一个错误,这时候,gate只能给客户端返回一个错误码。

 

对于CommitPrepared异常处理策略和上面一样,vttablet上面的watchDog发现本地MySQL有记录分布式事务超时,根据发送分布式事务id到vtgate,vtgate对分布式事务进行Resolve。

 

5、ConcludeTransaction异常

 

ConcludeTransaction只是将mmShard的元数据删除,删除操作可能成功可能失败,最终返回给客户端一个错误码。

 

对于ConcludeTransaction和上面3、4异常处理策略一样。都会执行resumeCommit。

 

resumeCommit会调用幂等的CommitPrepared,然后调用ConcludeTransaction删除mmShard上的分布式事务元数据信息。

 

同样ConcludeTransaction操作也是幂等的。

 

6、其他异常

 

●  事务长时间未完成

vttablet配置了分布式事务超时时间,mmShard记录了事务开始时间,长时间未提交自动发起Resolve,避免长时间未提交的事务。

 

●  vtgate宕机

在commit2PC函数中,执行到任何一个步骤vtgate都可能宕机,但是通过上面的错误分析得知,对于任何异常,要么回滚要么补偿,Vitess都可以避免部分提交。

此外,vttablet需要知道协调者vtgate地址,对于长时间未提交事务发起Resolve。vtgate是一个集群,避免单点故障。

Resolve的参数是分布式事务id。根据分布式事务id,我们能拿到mmShard。从mmShard上能读到分布式事务的元数据信息(不管mmShard和vtgate是否发生过宕机、重启、网络异常等)。有了事务的元数据信息,就可以发起Resolve操作了。

 

●  vttablet或者MySQL宕机

vttablet或者MySQL宕机、重启、或者发生主从切换之后,需要从代码逻辑上保证各个接口能够正常服务,比如下面的问题1。

 

>>>>  几个小问题

 

1、为什么我们需要一个preparedPool而不是直接复用之前的activePool?

因为当vttablet或者MySQL重启后,我们需要能够从之前记录的redo log中恢复回来。preparedPool是以分布式事务id作为key的。当vttablet重启,初始化过程就从MySQL中去取redo log(prepare步骤记录下来的SQL就是redo log已经对应的分布式事务id),有redo log就说明有分布式事务未完成。然后对每个分布式事务分配一个连接,重新执行redo log,然后将该连接放到preparedPool。这样即便vttablet发生重启,我们可以重新生成一个preparedPool,vttablet仍然可以执行vtgate在Commit或者Resolve时候发送过来的CommitPrepared或者RollbackPrepared指令,从而保证分布式事务的正常执行。

 

2、vttablet发起Resolve对接口的调用和vtgate正常事务流程对接口的调用是否有可能并发?

有可能,vtgate第一步记录分布式事务的元数据信息,但是事务超过10秒未完成。那么vttablet自动发起Resolve,发现事务未提交调用RollbackPrepared,同时有可能vtgate因为某些异常也发起了RollbackPrepared。CommitPrepared、ConcludeTransaction也有一样的问题。所以Resolve过程中调用的接口需要考虑到并发、幂等性等。

 

3、应用端执行commit返回错误就是事务提交失败么?

不一定,通过前面的例子我们可以看到,可能两阶段提交前面都成功了,只是最后删除mmShard上面的元数据失败了,还是会返回给应用端一个error。

甚至所有步骤都成功了,要正常返回给应用端OK包了,断网了,应用端也是会收到网络异常错误,这就需要应用端有合适的补偿机制处理这种异常。

 

4、Vitess中的两阶段提交事务满足文章开头提到的事务四个特性么?

不满足,Vitess的两阶段提交只是保证了分布式事务的原子性,即便使用两阶段提交,在Vitess中是有可能读取到部分提交结果的。 但是两阶段提交是分布式中经典问题,也是最基础的算法,几乎所有的复杂的分布式算法都会使用到两阶段提交。

 

5、Vitess中只有两阶段提交一种事务模型么?

不是,除了两阶段提交外,Vitess还支持单节点事务,单节点事务强制要求事务只能在一个Shard执行;还有多节点事务,多节点事务对部分提交问题不做处理。

 

6、Vitess中两阶段提交性能如何?

Vitess两阶段提交比一般的多节点事务会多四次vtgate到vttablet的交互,我的测试结果两阶段提交带来的延迟在5毫秒以内。

 

7、线上的交易订单系统真是例子中这样的么?

复杂的多,首先订单和白条可能都不在同一个大部门,比如订单业务属于商城交易部门,白条业务属于金融部门。各个服务间可能通过WebService或者mq交互。其次业务也要复杂的多,一个下单操作可能涉及到几十个甚至上百个接口的调用,各个接口能够稳定的提供服务最主要的原因是依赖于基础平台的各种中间件,好了编不下去了,其实我不知道😁。

 

8、不是说好的两阶段提交么,Vitess为啥分了好几个阶段啊,这还是正经两阶段提交么?

两阶段指的是协调者和参与者之间的交互主要分两个阶段。

两阶段提交第一步,在提交之前协调者vtgate向所有参与者vttablet发出是否可提交的询问,CreateTransaction相当于先记录事务状态,然后向mmShard发起了是否可提交的询问,Prepare请求相当于向除mmShard之外的其他参与者发起了询问。

两阶段提交第二步,是如果所有参与都答复可以正常提交,那么,协调者给所有参与者发出提交指令。Vitess中询问之后得到了可以提交的答复,首先通过StartCommit将状态记录下了,这样可以避免协调组vtgate异常导致分布式事务异常。记录可以提交状态之后,协调者vtgate向所有参与者发出提交指令CommitPrepared。Vitess做的一点优化是元数据记录在了mmShard,所以在记录事务已经提交的步骤中,直接把mmShard上的事务提交了。

两阶段提交上面已经完成了,ConcludeTransaction是做了一些清理元数据的工作。

翻了一下事务处理,在讲两阶段提交过程有明确指出[P14]:
对于参与者的答复,事务管理器将在日志中记录下这个事实。

Vitess做法是将这个状态记录到了mmShard。对于资源管理器重启,事务处理[P14]中也提到:
作为重启逻辑的一部分,资源管理器向事务管理器发出询问,这时候事务管理器告诉他们 发生故障时每一个活动的事务的执行结果。一些可能提交了,一些可能终止了,一些可能仍在提交过程中。资源管理器可以独立的恢复其已提交的状态,也可以参与事务管理器对日志重做或者撤销的检测。

Vitess也是资源管理器vttablet向事务管理器发出询问,只不过事务管理器维护的事务日志信息还是放到了资源管理器,因为资源管理器有MySQL啊,正好存储事务信息不会丢。

 

所以Vitess的两阶段提交是正经的两阶段提交。而没有实现上面两点的两阶段提交是不能从错误正常恢复,也就不能保证原子性。

 
 
posted @ 2019-03-01 21:46  papering  阅读(292)  评论(0编辑  收藏  举报