无限缓存channel的设计与实现
一.引言
Go语言的Channel有两种类型,一种是无缓存的channle,一个种是有缓存的channel,但是对于有缓存的channle来说,其缓存长度在创建时就已经固定了,中间也不能扩缩容,这导致对某些特定的业务场景来说不太方便
业务场景如下 :
- 爬虫场景,想爬取某个URL页面上可达的所有URL
- 一个channle中存在待处理的URL
- 一堆worker groutine从channle中读取URL,下载解析网页并且提取URL,再把URL放入channle
这种场景下,使用消息队列或sync包可以解决这个问题,但是比较复杂,如果有一个可以无限缓存的Channle或许是比较好的解决方案
二.设计
基于以上特定的业务场景,我们的无限缓存Channle应该满足以下要求:
- 缓存无限,最核心的基本要求。
- 不能阻塞写,普通channle的写操作之所以阻塞,是因为缓存满了,无限缓存的channle不应该存在这个问题。
- 无数据时阻塞读,此特性保持和普通channle一样。
- 读写都应通过channle操作 :通过channle的 <- 和 ->,第一个是方便,仍遵循普通channle的语法,第二是不能暴露内部缓存
- channle被关闭后,未读取的数据应该仍然可读,此特性和普通channle保持一致
- 可基于数据量自动扩缩容,在数据量很大的时候要求可以自适应的扩容,在数据量变小后,为了避免内存浪费,要求可以自适应的缩容
针对以上要求,设计思想如下:
- 内部含有两个普通channle,分别用于读写,我们将其称作In和Out,往In中写入数据,然后从Out中读取数据
- 内部有一个可以自适应扩缩容的buf,当写channle满了写不了之后,写入到此buf中
- 内部含有一个工作goroutine,总是In中数据放入到Out或者buf中
内部的自适应扩缩容buf可以采用双向环形链表
和采用数组实现相比,优点如下:
- 数组大小是有限制的,语言层面就做不到真正的无限缓存
- 数组扩容代价大,而采用双向环形链表则只用增加节点即可,缩容同样
type T interface{}
type UnlimitSizeChan struct {
bufCount int64 // 统计元素数量,原子操作
In chan<- T // 写入channle
Out <-chan T // 读取channle
buffer *RingBuffer // 自适应扩缩容Buf
}
双向环形链表 如何写入和读取数据&#x