长安链源码学习-- 交易池(四)

上一节已经对交易池的执行逻辑、数据结构等进行分析,本节将对txQueuecommonBatchPool进行分析。

1. txQueue

   txQueue的数据结构为无锁并发队列,在文件annular_lockfree_queue.go中,看名字也可以看出其为循环无锁队列。实现思路如下:
1)定义存储数组,以及写入游标、读取游标,假设capacity = 6,读取游标readerIdx、写入游标writerIdx

queue初始化.png

readerIdxwriterIdx 会无限增加,每一个存储单元由valrIdxwIdx构成,val表示该存储单元中的具体元素信息,rIdx表示当前存储单元上次被读取的index值、wIdx表示当前存储单元上次被写入的index值。

2)假设情况一:插入数据、立马读取。

  • 插入数据
    插入数据writeIdx 原子加1,wIdx表示当前存储单元最近一次被写入的index值,wIdx = writerIdx,该单元未发生任何读取,所以rIdx不变。


    插入数据.png
  • 读取数据
    当执行Poll方法时,readerIdx < writerIdx 表示还有元素在数组中,可以进行读取元素。读取后该elem中数据信息如下:


    读取数据.png

3)大量写入但并未读取
   由于该设计为循环队列,当写完第6个元素,会取模重新从第1个元素写入。当写入到第2个元素时,系统会检测两个条件:1)如果第2个elem记录的rIdx 与 wIdx不相等,认为上次写入的元素还没有被消费。2)如果writerIdx != wIdx + capacity,表示写错单元数据。这两种情况都不允许写入成功,这里处理方式为等待。


写入队列满.png

4)大量读取但未写入
   与步骤3同理,就不进行过多介绍。

思考:
   无锁队列的存在,是希望交易的保存及读取是顺序且快速的。步骤3里提到写入不成功进行等待,等待的实现方式是调用GoschedGosched是go语言协程调度方式,将当前G放入可运行队列,让其他可运行的G放入M执行。可以简单理解为执行sleep,将CPU让出给其他协程调用。思考如果当前goroutine数量较多,下次调度到该协程的时间较长;如果当前goroutine数量较少,则在短时间内多次调度,但由于对应的queue数据未准备好,会重新执行Gosched
   可能很多同学会想,可否利用channel机制,当有数据Push后,通知到Pull可以获取数据;当执行Pull后,通知到Push可以继续写入数据?
   答:channel的实现机制为调用gopark方法暂停当前协程,并传入唤醒方法,当指定唤醒方法匹配后将暂停的G重新放入可运行队列,P再根据调度算法重新进行调度,也会导致处理事件不及时。
   目前长安链的做法好处:增加txQueue的读写速度,坏处:带来额外的协程调度工作量。另外,笔者确实也没有想好什么好方法,go1.14版本以前,可以使用无限for循环且没有函数调用来霸占goroutine,但go1.14版本及以后有抢占式调度,系统层面可以直接将运行的G替换。

2. commonBatchPool

   commonBatchPool的数据结构为有序map,在文件sorted_map.go中。这里以key为String类型构造有序map为例,实现思路如下:
1)通过sync.map保存原始信息。
2)通过keys将sync.map的所有key值进行保存,keys是切片类型,可有序存储。

这里的逻辑复杂度不高,思路也比较好理解。

type StringKeySortedMap struct {
    keysLock   sync.RWMutex
    keys       []string
    m          sync.Map
    needResort uint32
}

关注作者,共同学习区块链技术。

你可能感兴趣的:(长安链源码学习-- 交易池(四))