上一节已经对交易池的执行逻辑、数据结构等进行分析,本节将对
txQueue
和commonBatchPool
进行分析。
1. txQueue
txQueue
的数据结构为无锁并发队列,在文件annular_lockfree_queue.go
中,看名字也可以看出其为循环无锁队列。实现思路如下:
1)定义存储数组,以及写入游标、读取游标,假设capacity = 6
,读取游标readerIdx
、写入游标writerIdx
。
readerIdx
、writerIdx
会无限增加,每一个存储单元由val
、rIdx
、wIdx
构成,val表示该存储单元中的具体元素信息,rIdx表示当前存储单元上次被读取的index值、wIdx表示当前存储单元上次被写入的index值。
2)假设情况一:插入数据、立马读取。
- 插入数据
插入数据writeIdx 原子加1,wIdx表示当前存储单元最近一次被写入的index值,wIdx = writerIdx,该单元未发生任何读取,所以rIdx不变。
- 读取数据
当执行Poll方法时,readerIdx < writerIdx 表示还有元素在数组中,可以进行读取元素。读取后该elem中数据信息如下:
3)大量写入但并未读取
由于该设计为循环队列,当写完第6个元素,会取模重新从第1个元素写入。当写入到第2个元素时,系统会检测两个条件:1)如果第2个elem记录的rIdx 与 wIdx不相等,认为上次写入的元素还没有被消费。2)如果writerIdx != wIdx + capacity,表示写错单元数据。这两种情况都不允许写入成功,这里处理方式为等待。
4)大量读取但未写入
与步骤3同理,就不进行过多介绍。
思考:
无锁队列的存在,是希望交易的保存及读取是顺序且快速的。步骤3里提到写入不成功进行等待,等待的实现方式是调用Gosched
,Gosched
是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
}
关注作者,共同学习区块链技术。