我想在Go中构建一个支持多个并发读者和一个编写器的缓冲区。所有读者都应该阅读写入缓冲区的内容。允许新读者随时进入,这意味着已经写好的数据必须能够为后期读者播放。
缓冲区应满足以下接口:
type MyBuffer interface {
Write(p []byte) (n int, err error)
NextReader() io.Reader
}
您是否对此类实施有任何建议,最好使用内置类型?
答案 0 :(得分:4)
根据作者的性质以及您如何使用它,将所有内容保存在内存中(以便能够为以后加入的读者重新播放所有内容)是非常危险的,可能需要大量内存,或导致您的应用程序因内存不足而崩溃。
将它用于“低流量”记录器,将所有内容保存在内存中可能没问题,但是例如流式传输某些音频或视频很可能不会。
如果下面的读者实现读取了写入缓冲区的所有数据,则他们的Read()
方法将正确报告io.EOF
。必须小心,因为一些构造(例如bufio.Scanner
)在遇到io.EOF
时可能无法读取更多数据(但这不是我们实现的缺陷)。
如果你希望缓冲区的读者等待缓冲区中没有更多数据可用,要等到写入新数据而不是返回io.EOF
,你可以将返回的读者包装在“尾读者”中“在这里提出:Go: "tail -f"-like generator。
这是一个非常简单和优雅的解决方案。它使用文件写入,并使用文件进行读取。同步基本上由操作系统提供。这不存在内存不足错误的风险,因为数据仅存储在磁盘上。根据作者的性质,这可能也可能不够。
我宁愿使用以下界面,因为Close()
在文件的情况下很重要。
type MyBuf interface {
io.WriteCloser
NewReader() (io.ReadCloser, error)
}
实施非常简单:
type mybuf struct {
*os.File
}
func (mb *mybuf) NewReader() (io.ReadCloser, error) {
f, err := os.Open(mb.Name())
if err != nil {
return nil, err
}
return f, nil
}
func NewMyBuf(name string) (MyBuf, error) {
f, err := os.Create(name)
if err != nil {
return nil, err
}
return &mybuf{File: f}, nil
}
我们的mybuf
类型嵌入了*os.File
,因此我们获得了“免费”的Write()
和Close()
方法。
NewReader()
只是打开现有的备份文件进行读取(以只读模式)并返回它,再次利用它实现io.ReadCloser
。
创建新的MyBuf
值正在NewMyBuf()
函数中实现,如果创建文件失败,该函数也可能返回error
。
备注:强>
请注意,由于mybuf
嵌入了*os.File
,因此type assertion可以“覆盖”os.File
的其他导出方法,即使它们不属于MyBuf
{1}}界面。我不认为这是一个缺陷,但是如果你想禁止这个,你必须将mybuf
的实现更改为不嵌入os.File
,而是将其作为命名字段(但是你必须自己添加Write()
和Close()
方法,正确转发到os.File
字段。
如果文件实现不充分,那么就会出现内存实现。
由于我们现在只在内存中,我们将使用以下界面:
type MyBuf interface {
io.Writer
NewReader() io.Reader
}
我们的想法是存储所有传递给缓冲区的字节片。调用Read()
时,读者将提供存储的切片,每个读者将跟踪其Read()
方法提供的存储切片的数量。必须处理同步,我们将使用简单的sync.RWMutex
。
不用多说,这是实施:
type mybuf struct {
data [][]byte
sync.RWMutex
}
func (mb *mybuf) Write(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
// Cannot retain p, so we must copy it:
p2 := make([]byte, len(p))
copy(p2, p)
mb.Lock()
mb.data = append(mb.data, p2)
mb.Unlock()
return len(p), nil
}
type mybufReader struct {
mb *mybuf // buffer we read from
i int // next slice index
data []byte // current data slice to serve
}
func (mbr *mybufReader) Read(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
// Do we have data to send?
if len(mbr.data) == 0 {
mb := mbr.mb
mb.RLock()
if mbr.i < len(mb.data) {
mbr.data = mb.data[mbr.i]
mbr.i++
}
mb.RUnlock()
}
if len(mbr.data) == 0 {
return 0, io.EOF
}
n = copy(p, mbr.data)
mbr.data = mbr.data[n:]
return n, nil
}
func (mb *mybuf) NewReader() io.Reader {
return &mybufReader{mb: mb}
}
func NewMyBuf() MyBuf {
return &mybuf{}
}
请注意,Writer.Write()
的一般合同包括实现不能保留传递的切片,因此我们必须在“存储”它之前复制它。
另请注意,Read()
的读者会尝试锁定最少的时间。也就是说,它仅在我们需要来自缓冲区的新数据切片时锁定,并且仅进行读锁定,这意味着如果读取器具有部分数据切片,则将在Read()
中发送该数据切片而不锁定并触摸缓冲区。
答案 1 :(得分:1)
我链接到仅附加提交日志,因为它看起来非常类似于您的要求。我对分布式系统和提交日志都很陌生,所以我可能会对几个概念进行屠宰,但是kafka的介绍清楚地解释了所有的图表。
对我来说,Go也是一个新手,所以我确信有更好的方法:但也许您可以将缓冲区建模为切片,我认为有几种情况:
缓冲区有一个/多个阅读器:
这解决了pubsub实时消费者流,其中消息被扇出,但没有解决回填问题。
Kafka启用了回填功能及其intro illustrates如何完成:)
此偏移量由消费者控制:通常是消费者 在读取记录时线性地推进其偏移,但事实上,从那以后 该职位由消费者控制,可以消费记录 任何它喜欢的订单。例如,消费者可以重置为较旧的 偏移以重新处理过去的数据或向前跳过最多 最近的记录并从“现在”开始消费。
这种功能组合意味着Kafka的消费者非常喜欢 便宜 - 他们可以来来去去,对群集或其他方面没有太大影响 其他消费者。例如,您可以使用我们的命令行工具 “尾随”任何主题的内容而不改变所消耗的内容 任何现有的消费者。
答案 2 :(得分:0)
作为实验的一部分,我必须做类似的事情,所以分享:
type MultiReaderBuffer struct {
mu sync.RWMutex
buf []byte
}
func (b *MultiReaderBuffer) Write(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
b.mu.Lock()
b.buf = append(b.buf, p...)
b.mu.Unlock()
return len(p), nil
}
func (b *MultiReaderBuffer) NewReader() io.Reader {
return &mrbReader{mrb: b}
}
type mrbReader struct {
mrb *MultiReaderBuffer
off int
}
func (r *mrbReader) Read(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
r.mrb.mu.RLock()
n = copy(p, r.mrb.buf[r.off:])
r.mrb.mu.RUnlock()
if n == 0 {
return 0, io.EOF
}
r.off += n
return n, nil
}