如果有太多的客户端试图同时添加 URLs ,第 2 版仍然存在一个性能问题: 由于锁定机制,我们的 map 可以安全的更新并发访问,但是立即将每一个新记录写入到磁盘,是一个瓶颈。磁盘写入可能会同时发生,并且根据你的操作系统的特性,可能会导致崩溃。即使写入不会冲突,每个客户端在 Put 函数返回之前,必须等待他们的数据写入到磁盘。因此,在 I/O 负载大的系统上,客户端需要等待的时间将超过完成添加请求所必须的时间。
为了解决这个问题,我们必须将 Put 与 Save 的过程解耦合:可以通过 Go 的并发机制做到这点。我们不再将记录直接保存到磁盘,而是发送它们到一个通道,这是一种缓冲,所以发送函数不需要再等待它。
写入磁盘的保存过程从这个通道读取,并且是启动在一个叫 saveloop 的协程单独启动的线程上。主程序与 saveloop 是同时执行的,所以没有那么多的阻塞。
我们通过一个 record 类型的通道替换 URLStore 中的 file 字段: save chan record
。
type URLStore struct {
urls map[string]string
mu sync.RWMutex
save chan record
}
一个 channel,就像一个 map 一样必须使用 make 创建;我们将修改我们的工厂 NewURLStore ,在它里面使用 make 去创建 channel ,并给他一个 1000 长度的缓冲区,如: save := make(chan record, saveQueueLength)
。为了弥补我们的性能瓶颈, Put 可以将一个 record 发送到我们的 channel 缓冲区保存,而不是进行函数调用保存每一条记录到磁盘。
func (s *URLStore) Put(url string) string {
for {
key := genKey(s.Count())
if s.Set(key, url) {
s.save <- record{key, url}
return key
}
}
panic("shouldn't get here")
}
在 save channel
的另一端,我们必须有一个接收器: 我们的新方法 saveLoop 将运行在一个单独的 goroutine
中; 它接收 record
的值并将他们写入到一个文件。 saveLoop 也是在 NewURLStore () 函数中通过 go 关键字启动,我们现在可以删除不再需要的文件打开的代码。这里修改后的 NewURLStore () :
const saveQueueLength = 1000
func NewURLStore(filename string) *URLStore {
s := &URLStore{
urls: make(map[string]string),
save: make(chan record, saveQueueLength),
}
if err := s.load(filename); err != nil {
log.Println("Error loading URLStore:", err)
}
go s.saveLoop(filename)
return s
}
这里是 saveLoop 方法的代码:
func (s *URLStore) saveLoop(filename string) {
f, err := os.Open(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatal("URLStore:", err)
}
defer f.Close()
e := gob.NewEncoder(f)
for {
// taking a record from the channel and encoding it
r := <-s.save
if err := e.Encode(r); err != nil {
log.Println("URLStore:", err)
}
}
}
Records 从一个无限循环中的 save channel
读取并编码到文件。
在 14 章 我们深入的学习了协程与通道,但是在这里我们看到了一个有用的示例,它可以更好的管理一个程序的不同部分。还要注意,现在我们仅创建了一次 Encoder 对象, 而不是每次保存,这样也节省了一些内存和处理。
另外一个改善可以使 goto 变得更加灵活:替换代码中的 filename 、硬编码的或者作为程序中常量的监听地址和主机名,我们可以将它们定义为 flags 。
这样,当启动程序的时候,如果在命令行中输入这些值,它们将被替换成新的值,如果没有输入,将从 flag 中获取默认值。这个功能来自一个不同的包,所以我们必须: import “flag” (关于此包的详细信息,参见 章节 12.4 )。
我们首先创建一些全局变量去保存 flag 的值:
var (
listenAddr = flag.String("http", ":8080", "http listen address")
dataFile = flag.String("file", "store.gob", "data store file name")
hostname = flag.String("host", "localhost:8080", "host name and port")
)
为了处理命令行参数,我们必须添加 flag.Parse()
到 main 函数中,并且在 flags 被解析后实例化 URLStore ,因为参数解析后我们才能知道 dataFile
的值( 在代码中使用的是 *dataFile
,这时因为 flag 是一个指针,必须取消引用去获取值,参见 章节 4.9 ):
var store *URLStore
func main() {
flag.Parse()
store = NewURLStore(*dataFile)
http.HandleFunc("/", Redirect)
http.HandleFunc("/add", Add)
http.ListenAndServe(*listenAddr, nil)
}
在 Add 处理器中,我们现在必须将 localhost:8080
替换成 *hostname
:
fmt.Fprintf(w, "http://%s/%s", *hostname, key)
编译并测试第 3 版本或使用目录中的可执行文件(译者注:一样不用去纠结为什么没有这个文件,直接编译去执行就好了)。
版本 4 —— 使用 json 做持久存储
第 4 版 goto_4 的代码(在 章节 19.7 中讨论 )能在 code_examples\chapter_19\goto_v4. 中找到。