早期的区块链系统,其执行引擎是一个串行执行模型,这种模型虽能保证执行的正确性,但却是区块链性能的一个核心瓶颈之一。天玄链中,通过识别交易中的状态依赖,构建交易依赖图来对执行引擎进行并行化,从而提升交易执行速度,解决该瓶颈。
通用 DAG 分析器
一个无环的有向图称做有向无环图(Directed Acyclic Graph),简称 DAG 图。在一批交易中,可以通过一定方法识别出每笔交易需要占用的互斥资源,再根据交易在 Event 中的顺序及互斥资源的占用关系构造出一个交易依赖 DAG 图,如下图所示,凡是同一 Level (无被依赖的前序任务) 的交易均可以并行执行。如下图所示,基于左图的原始交易列表的顺序进行拓扑排序后,可以得到下图的交易 DAG 。
图1. 交易DAG
通用 DAG 分析器具体案例
上面两个表格中,我们假设有四笔交易,交易发起的先后顺序如图中序号所示
交易1:用户B转账给用户A 20元钱
交易2:用户C转账给用户D 20元钱
交易3:用户A转账给用户C 15元钱
交易4:用户D转账给用户A 10元钱
各个用户账户的初始余额分别是 10 ,20 , 30 , 40
假设,这四笔交易串行执行,也就是按照交易发起的顺序按序执行,得到的最终余额是25 , 0 ,25, 50。
这个时候,如果我们在不构建任何依赖的前提下,去并行执行这四笔交易。会发生什么呢。并行执行意味着这四笔交易实际执行的物理时间是完全不定的。很可能,交易3是在交易1之前执行完成。
那么我们看看在这种情况下,会发生什么
1. 很明显,如果在交易1都没有完成的情况下,交易3发生了,会导致A用户的账户里不存在足够的余额用于支撑这笔交易,也就是会触发交易1执行失败。
然后,我们会看到,四笔交易都完成后的最终状态和串行执行时的最终状态完全不一致了。
在系统代码层面,要如何去构建这个 DAG 依赖图呢?我们可以看到,tx 1 和 tx 3 之间之所以存在并行冲突,是因为他们俩都涉及到同一个资源的修改,也就是用户A的账户余额,tx1会用户A的账户余额进行+20的操作,而tx3会对用户A的余额进行-15的操作。所以,如果我们在构建交易的时候将涉及到的资源做好标识,就可以有效的按照资源冲突,去构建DAG图谱了。
DAG核心结构
- LevelDAG:会根据输入的可执行交易列表 Txs ,最大可并行执行层级(maxParallelLevel)以及最大层级深度(maxLevelDeep),生成可并行执行 ExecuteRoot(Level-0)。
- ExecuteRoot:可并行执行根节点,该数据结构主要包含可并行执行的节点 (List\) 列表。换言之,并行执行器会并行执行 ExecuteRoot 所对应的可执行节点 (List\) 集合。然后在依据当前 Level 的执行状况,主动触发下一 Level 层级所对应的可执行节点(List\)列表。
- Level:当前层级数据结构抽象,主要用于控制最大可并行执行层级(maxParallelLevel),该参数可以这样理解,如当 maxParallelLevel 为 1L 时,即表明 当前执行 Level 必须要等待 Level 1 的所有执行可执行节点执行完成,才可以继续执行。
- ExecuteState:执行状态抽象,对应 DAG 中的状态。
通用 DAG 执行引擎
该通用执行引擎的设计目的就是根据 2.1 中通用 DAG 分析器所生成的 ExecuteRoot ,以固定的线程任务,最大并行度地执行交易,根据 LevelDAG 生成的 ExecuteRoot ,并获取 ExecuteRoot 可并行执行节点列表 parallelExecuteNodes,然后遍历该列表节点(ExecuteNode),将执行任务提交给线程池。
线程池会对 ExecuteNode 执行逻辑,具体流程如下:
-
先判断节点所在的 Level 的前一个 Level 是否执行完成,如果没有执行完成则加入待重试队列,交由待重试 reactor 线程后续轮循执行。否则,说明当前节点可以执行,则执行相应的交易方法(即可以是以太坊的智能合约交易,也可以是 Java 的智能合约)。
-
前置依赖节点执行完成,执行当前 Level 节点。
-
当前 Level 每个节点执行完毕。将当前 ExecuteNode 所对应 Level 的待完成执行数减 1(该标志为步骤 1)中判定前置节点是否执行完成).
-
遍历当前 ExecuteNode 所对应的被依赖节点,并对被依赖节点执行移除前置依赖。若此时被依赖节点依赖关系节点数量为 0 ,则对该节点执行步骤 1)。如此递归,直到所有的节点被执行完成。