长安链采用高效的并行调度方式执行交易,了解长安链交易调度、冲突检测和DAG构建流程有助于开发者更好地理解长安链并行调度的运行机制,帮助开发者编写高质量、低冲突的智能合约,更好地构建区块链应用。
我们将用两篇文章说明长安链交易调度、冲突检测流程和DAG的构建流程。
一、 概述
1. 基础概念
交易调度执行是区块链交易执行时的核心流程,对一轮共识性能影响巨大。一笔交易执行的本质是对某些key进行读、写操作,在区块链中,一个区块中往往包含了多笔交易,交易执行完后的最新数据状态叫做“世界状态”。区块中交易调度执行共有两种方式,一种是确定性调度,比如最简单的串行执行;另一种则是非确定性调度方式,在这种方式下,为了提升调度执行性能,往往会增加交易执行的并行度。在交易并行执行下,就需要确保所有交易执行的“正确性”和“有序性”。
● 正确性:一笔交易执行过程中,所依赖的前置充分条件不能被破坏;(本质上就是读写冲突交易不能同时执行)。
● 有序性:所有节点可以按照同一个执行顺序对区块中的交易进行执行,得到一致的世界状态;(主节点需要告诉从节点按照什么顺序执行交易)。
2. 场景构造
为了方便理解,我们构造一个简单的银行借贷合约,子公司向银行申请贷款时,需要由其上级公司或总公司的存款进行担保,如果上级公司存款金额不低于贷款额度,银行会向子公司发放贷款,并立即打入到子公司账户。
图1.1 世界状态
假设k1是总公司账户,k2是一级子公司账户,k3是二级子公司账户,当前的世界状态如上图所示。一个区块中共有如下4笔向银行申请贷款的交易:
图 1. 区块中4笔存在冲突的交易
● tx0表示的是一级子公司k2以总公司k1存款为担保向银行申请贷款;
● tx1表示的是二级子公司k3以一级子公司k2存款为担保向银行申请贷款;
● tx2表示的是二级子公司k3以总公司k1存款为担保向银行申请贷款;
● tx3表示的是一级子公司k2以总公司k1存款为担保向银行申请贷款。
其中,tx0和tx1之间存在读写冲突,tx1和t2之间存在写写冲突,tx3和tx1之间存在写读冲突。
二、 交易冲突检测
如果区块中所有交易按序串行执行,那么所有交易执行的正确性都不会受到影响,即使两笔交易对同一个key都进行读写操作,后面的一笔交易在前一笔交易执行后的“世界状态”上执行,交易的执行结果也是正确的。
图 2.1 交易串行执行
以上述的tx0和tx1为例,如果总公司k1账户余额为100元(v1),一级和二级子公司k2和k3账户余额为0元,分别以上一级公司存款额度为担保,申请贷款100元。虽然其中存在读写冲突,但是在串行执行时,后一笔交易基于前一笔交易最新的世界状态执行,两笔交易的执行结果是确定的。具体有共有上图中两种情况:
● 第一种情况,tx0先执行,贷款成功,一级子公司k2余额变为了100元,后面执行的tx1因为一级子公司k2账户余额变为100元,将会满足银行贷款条件,贷款成功。
● 第二种情况,tx1先执行,因为一级子公司k2余额开始为0,所以贷款失败,但是后面执行的tx0因为总公司k1余额还是100元,将会贷款成功。
在串行执行情况下,只会按照如上两种情况中的其中一种情况进行执行,但是无论按照哪种情况执行,交易的执行结果都是确定的,也都是符合业务逻辑的正确结果。
但是,为了提升调度执行交易的性能,采用并行调度时,情况就会复杂很多,就需要检测出来哪些并行执行的交易存在冲突(保障正确性),存在冲突的交易就需要按序执行,并且也要告知其他节点按照此顺序执行这些交易(保障有序性)。
接下来本部分将会介绍如何保障正确性,后面第二篇将介绍如何保障有序性。
1. 读写、写读冲突需要重新执行
在交易执行正确性判断时,读写冲突和写读冲突本质上是一种冲突类型,即一笔交易所依赖的读集中的内容,在执行过程中被其他交易修改了,使得这笔交易正确执行的前置充分条件被破坏了,那么该笔交易的执行结果的正确性也无法得到保障,那就需要对此笔交易重新执行。
图 2.2 交易读写冲突
如上图所示,tx0和tx1在并行执行时,如果按照左图中的情况执行,不存在读写冲突,不会存在正确性的问题,因此后一笔交易不需要重新执行。但是,如果按照右图中的情况执行,tx1和tx0存在读写冲突,tx1交易执行开始时所依赖的k2,在tx1执行过程中被tx0修改了,此种情况则需要对后执行完的tx1基于tx0执行完后的最新“世界状态”重新执行,才能得到正确的结果。
图 2.3 交易读写冲突(写读冲突演变的读写冲突)
如上图所示,tx1和tx3在并行执行时,如果按照图2.3左侧图中的情况执行,也不存在读写冲突,不存在正确性问题,因此后一笔交易也不用重新执行。但是,右图中的情况下,tx1和tx3之间存在读写冲突,后执行完的tx1所依赖的k2在tx1执行过程中被tx3修改了,那么后执行完的tx1就需要重新进行执行以得到正确的结果。这里着重强调一下,直观上的写读冲突,在并行执行时演变为读写冲突时,才会影响后执行完的交易的正确性。
2. 读读不冲突、写写直接覆盖
对于读读情况,我们不做过多讲解,因为在没有写的情况下,对同一个key进行并发读取,获取的值都是一致的正确结果,不存在并行执行的正确性问题。
对于写写情况,也比较简单,写写可以直接覆盖处理,直接以后一笔交易写入的值作为最终结果即可。
图 2.4 写写覆盖
如上图所示,tx1和tx2都对k3进行写操作,如果按照图2.4左侧图所示tx1先执行完,tx2后执行完,k3的value变化逻辑是v3->v3'->v3'',这个执行过程是正确的。如果按照右图所示tx2先执行完,tx1后执行完,k3的value变化逻辑是v3->v3''->v3',这个执行过程也是正确的。无论采用上述哪种执行顺序,两笔交易执行过程都是符合逻辑的,正确性没有任何问题(因为两笔交易都没有对k3进行读取,不存在读写冲突),只需要保障其他节点也按照同样的顺序执行即可,这个就是另外的有序性要求了。
3、 长安链中的交易冲突检测方案
经过上面的分析,我们能够知道:并发执行下的交易冲突检测,只需要检测出来读写冲突,并对后执行完的交易重新执行,直到所有交易执行过程中均不存在读写冲突即可。
再次强调,此处的交易冲突检测,只能够在调度执行过程中确保存在读写冲突的交易能够按照某一个顺序执行得到正确的结果,仅满足了正确性条件。怎么让其他节点知道这个节点执行交易的顺序,是由有序性条件进行保障,这是由后面第二篇文章中构造DAG负责的事情,请大家不要混淆。
图3.1 冲突检测流程图
在长安链中,Scheduler模块负责每个区块的调度执行,在每个区块调度执行时,Scheduler会给每个区块构造一个Snashot结构,在该结构中有四个变量需要着重关注:
● ExecutedTxs:已经执行完的交易队列,是一个Slice结构,每“正确的”执行完一笔交易,则把这笔交易append到队列中,队列索引从0开始。此外,在处理每笔交易构建simContext时,通过len(ExecutedTxs)来计算当前交易的执行编号txSeq;
● WriteTable:交易写集最新集合,是个map集合,键是交易写集中的key,值是包含写集key对应的value值和对应交易的txSeq;
● ReadTable:交易读集最新集合,也是个map集合,键是交易读集中的key,值是包含读集key对应的value值和对应交易的txSeq;
● Lock:Snapshot中的读写锁,保障并发操作下Snapshot中变量的正确性。
WriteTable和ReadTable可以理解成该区块执行的沙箱环境下,待修改和被读取的热点数据,与数据库中的数据一起构成最新的“世界状态”。
图3.2 Snapshot中三种数据结构
具体流程:
1. 首先,对区块中的交易进行分发到channel通道中,此处的交易分发有一些优化,可以根据gas账户或者sender进行分组,尽可能在不同分组间实现并发调度以减少读写冲突的情况,这里不详细描述;
2. 多个goroutine协程并发地从channel中取出交易,对交易并行执行,默认设置的并发程度是4倍的cpu核数,所以该区块中的4笔交易完全是并行执行的,这里实现并发的协程池子的容量也可以根据交易冲突率进行自适应调整,这里不详细描述;
2.1 每笔交易在实际调用vm执行前,需要通过Snapshot获取当前交易的执行编号txSeq,用于构造SimContext;
2.2 此时还没有交易执行完,所以4笔获得的txSeq都是0,具体是通过len(ExecutedTxs)计算而来;
3. 4个协程并行调用vm模块对4笔交易进行执行,为每笔交易生成读写集,其实VM执行合约时也是优先从WriteTable和ReadTable进行读取,不存在再从DB中读取相关的值;
4. vm模块将包含读写集的交易执行结果返回给调度模块;
5. 调度模块对执行完的交易,进行交易冲突检测,如果同时满足如下两个条件则说明此笔交易冲突:
● 此笔交易中读集的key(假设是k0)在WriteTable中;
● 并且此笔交易的txSeq小于等于WriteTable中k0对应的txSeq;
6. 若该交易与之前已经执行完的交易不存在读写冲突,则将交易放入ExecutedTxs队列,用该交易的读写集和txSeq更新WriteTable和ReadTable;
7. 若该交易与之前已经执行完的交易存在读写冲突,则将交易重新调度分发,放入channel通道中,后面重新再次执行,直到所有交易在执行时都通过交易冲突检测。
下面分别以tx0和tx1为例讲解读写冲突具体怎么检测出来的。
图3.3 冲突检测示例图
如上左图所示,此种情况不存在读写冲突,tx0和tx1并行交错执行,虽然txSeq相同,但是tx1先于tx0执行完,待tx1执行完后WriteTable中将会缓存k3对应的最新值和txSeq。等到tx0执行完时,tx0的读集k1不在WriteTable中,即tx0交易执行所依赖的前置充分条件没有被破坏,tx0执行正确,此时将tx0 添加进ExecutedTxs队列,用该交易的读写集和txSeq更新WriteTable和ReadTable即可。
如上右图,此种情况存在读写冲突,tx0先于tx1执行完,待tx0执行完后WriteTable中将会缓存k2对应的最新值和txSeq。等到tx1执行完时,tx1的读集k2在WriteTable中,并且tx1的txSeq为0小于等于WriteTable中k2当前的最新执行序0,tx1交易执行所依赖的前置充分条件被破坏,tx1执行失败,此时则需要对tx1重新调度分发,再重新执行。
图3.4 冲突后重新执行
当tx1重新执行时,假设只有这一笔交易执行,那么其txSeq的值将为len(ExecutedTxs)的值为1,随后VM在执行交易时,会优先从Snapshot的WriteTable和ReadTable中读取k2的值,此时读取的结果是最新的v2',待tx1执行完后,检测k2虽然在WriteTable中,但是其txSeq为1大于WriteTable中的txSeq 0,所以不冲突,将tx1添加进ExecutedTxs队列,并用该交易的读写集和txSeq更新WriteTable和ReadTable即可。
4. 结语
至此我们介绍了长安链中交易调度和交易冲突检测流程及机制,此时确保了交易执行过程的正确性,我们将在下一篇文章中介绍,长安链如何实现交易执行的有序性。