此外,如果交易中对一个key改写多次,则只保留最后的修改值。如果交易中读取一个key的值,即使交易在读取之前更新了该key的值,读取到的也会是之前提交过的而不是刚更新的。换句话说,不能读取到同一交易中修改的值。
如前所述,key的version只记录在read set
;write set
只包含key及对应新value。
对于read set
的version的实现有很多种方案,最基本要求就是为key生成一个非重复标识符。例如用递增的序号作为version。在目前的代码实现中我们使用了blockchain height作为version方案,就是用交易的height作为该交易所修改的key的version。交易height由一个结构表示(见下面Height struc),其中TxNum表示这个tx在block中的height(译注:也就是交易在区块中的顺序)。该方案相较于递增序号有很多优点—主要是这样的version可以很好地利用到诸如statedb、交易模拟和校验这些模块中。
此外,如果模拟交易中执行了批量查询(range query),批量查询结果会被放到read-write set中的query-info
。
下面是一个假设的交易模拟生成的read-write set示例,简单起见,示例中使用了递增序号作为version。
在验证阶段,如果read set
中每个key的version都与stateDB中对应worldState(假设所有之前的有效交易,包括同一个block中的交易,都已经提交完成,即已更新ledger)的version相匹配,则认为此交易有效。
如果read-write set中包含query-info
,则还要对此执行额外的校验。该校验确保在此批量查询的结果范围内没有key被增删改。换句话说,如果在验证阶段重新执行该批量查询(模拟期间执行的交易)应该产生与模拟交易期间相同的结果。此校验确保交易在提交时出现幻读会被认为无效。注意,这个幻读保护仅限于Chaincode的GetStateByRange
和GetStateByPartialCompositeKey
两个方法(译注:此处文档上提到的是和GetQueryResult
两个方法,但在代码里的注释却不是这样,此处以代码为准。详见fabric/examples/chaincode/go/marbles02/marbles_chaincode.go)。而其他批量查询方法(如:GetQueryResult
)会有幻读风险,因此这种查询应该只用于不会被提交到ordering的只读交易
,除非app能保证交易模拟和交易验证提交两阶段之间结果集稳定。
如果交易验证通过,committer就会用write set
更新worldState。在更新阶段,write set
中的每个key在worldState中对应的value都会被更新,然后worldState中这些key的version也会随着更新。
本节通过示例场景帮助理解read-write set。存在一个key设为k
,在worldState中由元组(k,var,val)
表示,其中ver
是k
的最新的version,val
是k
的value。
现在有五个交易,分别是T1,T2,T3,T4,T5
,这五个交易的模拟过程是针对相同的worldSate快照,下面的代码片段显示了模拟交易的worldState快照以及每个交易执行读写的顺序。
T1
验证成功,因为它没有read操作。之后在worldState中的和k2
会被更新成(k1,2,v1'), (k2,2,v2')
T3
验证成功,因为它没有read操作。之后在worldState中的k2
会被更新成(k2,3,v2'')
T4
验证失败,因为它读取的k2
在之前的交易T1
中被修改了T5
验证成功,因为它读取的k5
没有在之前的任何交易中修改
译注:
原文示例交易需要进一步阐述
值得注意的是,T1…T5在同一个区块和不同区块的处理方式不同。
- 如果读取的key在此区块前面的交易中已经有update,则直接置此交易为失效
- 如果读取的key在本区块前面的交易没有做update,则需要判断state中的版本和commit的版本是否一致,如果不一致,则置为失效交易
为了尽量避免失效交易,application和chaincode需要进行精心设计,避免同一个资产交易信息尝试在一个区块上进行update操作。如果避免不到,可以适度重试交易。