mirror of
https://github.com/unknwon/the-way-to-go_ZH_CN.git
synced 2025-08-12 05:33:04 +08:00
109
eBook/19.6.md
Normal file
109
eBook/19.6.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# 版本 3 - 添加协程
|
||||||
|
|
||||||
|
第 3 个版本的代码 *goto_v3* 见 [goto_v3](examples/chapter_19/goto_v3)。
|
||||||
|
|
||||||
|
# 19.6 用协程优化性能
|
||||||
|
|
||||||
|
如果有太多客户端同时尝试添加 URL,第 2 个版本依旧存在性能问题。得益于锁机制,我们的 map 可以在并发访问环境下安全地更新,但每条新产生的记录都要立即写入磁盘,这种机制成为了瓶颈。写入操作可能同时发生,根据不同操作系统的特性,可能会产生数据损坏。就算不产生写入冲突,每个客户端在 `Put` 函数返回前,必须等待数据写入磁盘。因此,在一个 I/O 负载很高的系统中,客户端为了完成 `Add` 请求,将等待更长的不必要的时间。
|
||||||
|
|
||||||
|
为缓解该问题,必须对 `Put` 和存储进程*解耦*:我们将使用 Go 的并发机制。我们不再将记录直接写入磁盘,而是发送到一个*通道*中,它是某种形式的缓冲区,因而发送函数不必等待它完成。
|
||||||
|
|
||||||
|
保存进程会从该通道读取数据并写入磁盘。它是以 `saveLoop` 协程启动的独立线程。现在 `main` 和 `saveLoop` 并行地执行,不会再发生阻塞。
|
||||||
|
|
||||||
|
将 `URLStore` 的 `file` 字段替换为 `record` 类型的通道:`save chan record`。
|
||||||
|
```go
|
||||||
|
type URLStore struct {
|
||||||
|
urls map[string]string
|
||||||
|
mu sync.RWMutex
|
||||||
|
save chan record
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
通道和 map 一样,必须用 `make` 创建。我们会以此修改 `NewURLStore` 工厂函数,并给定缓冲区大小为1000,例如:`save := make(chan record, saveQueueLength)`。为解决性能问题,`Put` 可以发送记录 record 到带缓冲的 `save` 通道:
|
||||||
|
```go
|
||||||
|
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` 通道的另一端必须有一个接收者:新的 `saveLoop` 方法在独立的协程中运行,它接收 record 值并将它们写入到文件。`saveLoop` 是在 `NewURLStore()` 函数中用 `go` 关键字启动的。现在,可以移除不必要的打开文件的代码。以下是修改后的 `NewURLStore()`:
|
||||||
|
```go
|
||||||
|
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` 方法的代码:
|
||||||
|
```go
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在无限循环中,记录从 `save` 通道读取,然后编码到文件中。
|
||||||
|
|
||||||
|
我们在 [14 章](14.0.md) 深入学习了协程和通道,但在这里我们见到了实用的案例,更好地管理程序的不同部分。注意现在 `Encoder` 对象只被创建一次,而不是每次保存时都创建,这也可以节省了一些内存和运算处理。
|
||||||
|
|
||||||
|
还有一个改进可以使 goto 更灵活:我们可以将文件名、监听地址和主机名定义为标志(flag),来代替在程序中硬编码或定义常量。这样当程序启动时,可以在命令行中指定它们的新值,如果没有指定,将采用 flag 的默认值。该功能来自另一个包,所以需要 `import "flag"`(这个包的更详细信息见 [12.4 节](12.4.md))。
|
||||||
|
|
||||||
|
先创建一些全局变量来保存 flag 的值:
|
||||||
|
```go
|
||||||
|
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` 函数中,在 flag 解析后才能实例化 `URLStore`,一旦得知了 `dataFile` 的值(在代码中使用了 `*dataFile`,因为 flag 是指针类型必须解除引用来获取值,见 [4.9 节](04.9.md)):
|
||||||
|
```go
|
||||||
|
var store *URLStore
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
store = NewURLStore(*dataFile)
|
||||||
|
http.HandleFunc("/", Redirect)
|
||||||
|
http.HandleFunc("/add", Add)
|
||||||
|
http.ListenAndServe(*listenAddr, nil)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
现在 `Add` 处理函数中须用 `*hostname` 替换 `localhost:8080`:
|
||||||
|
```go
|
||||||
|
fmt.Fprintf(w, "http://%s/%s", *hostname, key)
|
||||||
|
```
|
||||||
|
|
||||||
|
编译或直接使用现有的可执行程序测试第 3 个版本。
|
||||||
|
|
||||||
|
## 链接
|
||||||
|
|
||||||
|
- [目录](directory.md)
|
||||||
|
- 上一节:[持久化存储:gob](19.5.md)
|
||||||
|
- 下一节:[以 json 格式存储](19.7.md)
|
44
eBook/19.7.md
Normal file
44
eBook/19.7.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# 版本 4 - 用 JSON 持久化存储
|
||||||
|
|
||||||
|
第 4 个版本的代码 *goto_v4* 见 [goto_v4](examples/chapter_19/goto_v4)。
|
||||||
|
|
||||||
|
# 19.7 以 json 格式存储
|
||||||
|
|
||||||
|
如果你是个敏锐的测试者也许已经注意到了,当 goto 程序启动 2 次,第 2 次启动后能读取短 URL 且完美地工作。然而从第 3 次开始,会得到错误:
|
||||||
|
|
||||||
|
Error loading URLStore: extra data in buffer
|
||||||
|
|
||||||
|
这是由于 gob 是基于流的协议,它不支持重新开始。为补救该问题,这里我们使用 json 作为存储协议(见 [12.9 节](12.9.md)),它以纯文本形式存储数据,因此也可以被非 Go 语言编写的进程读取。同时也显示了更换一种不同的持久化协议是多么简单,因为与存储打交道的代码被清晰地隔离在 2 个方法中,即 `load` 和 `saveLoop`。
|
||||||
|
|
||||||
|
从创建新的空文件 store.json 开始,更改 main.go 中声明文件名变量的那一行:
|
||||||
|
```go
|
||||||
|
var dataFile = flag.String("file", "store.json", "data store file name")
|
||||||
|
```
|
||||||
|
|
||||||
|
在 store.go 中导入 `json` 取代 `gob`。然后在 `saveLoop` 中唯一需要被修改的行:
|
||||||
|
```go
|
||||||
|
e := gob.NewEncoder(f)
|
||||||
|
```
|
||||||
|
|
||||||
|
更改为:
|
||||||
|
```go
|
||||||
|
e := json.NewEncoder(f)
|
||||||
|
```
|
||||||
|
|
||||||
|
类似的,在 `load` 方法中:
|
||||||
|
```go
|
||||||
|
d := gob.NewDecoder(f)
|
||||||
|
```
|
||||||
|
|
||||||
|
修改为:
|
||||||
|
```go
|
||||||
|
d := json.NewDecoder(f)
|
||||||
|
```
|
||||||
|
|
||||||
|
这就是所有要改动的地方!编译,启动并测试,你会发现之前的错误不会再发生了。
|
||||||
|
|
||||||
|
## 链接
|
||||||
|
|
||||||
|
- [目录](directory.md)
|
||||||
|
- 上一节:[用协程优化性能](19.6.md)
|
||||||
|
- 下一节:[多服务器处理架构](19.8.md)
|
Reference in New Issue
Block a user