惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
月光博客
月光博客
IT之家
IT之家
B
Blog RSS Feed
T
Tailwind CSS Blog
The GitHub Blog
The GitHub Blog
博客园 - 三生石上(FineUI控件)
MyScale Blog
MyScale Blog
J
Java Code Geeks
Stack Overflow Blog
Stack Overflow Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - Franky
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
U
Unit 42
博客园_首页
B
Blog
V
V2EX
腾讯CDC
Vercel News
Vercel News
量子位
Microsoft Security Blog
Microsoft Security Blog

极客兔兔

Go sync.Cond | Go 语言高性能编程 Go 死码消除与调试(debug)模式 | Go 语言高性能编程 Go sync.Once | Go 语言高性能编程 Go 逃逸分析 | Go 语言高性能编程 2020 年终总结 | 极客兔兔 Go struct 内存对齐 | Go 语言高性能编程 Go 空结构体 struct{} 的使用 | Go 语言高性能编程 控制协程(goroutine)的并发数量 | Go 语言高性能编程 | 极客兔兔 如何退出协程 goroutine (其他场景) | Go 语言高性能编程 如何退出协程 goroutine (超时场景) | Go 语言高性能编程 Go 语言陷阱 - 数组和切片 | Go 语言高性能编程 减小 Go 代码编译后的二进制体积 | Go 语言高性能编程 Go Reflect 提高反射性能 | Go 语言高性能编程 读写锁和互斥锁的性能比较 | Go 语言高性能编程 | 极客兔兔 for 和 range 的性能比较 | Go 语言高性能编程 切片(slice)性能及陷阱 | Go 语言高性能编程 | 极客兔兔 字符串拼接性能及原理 | Go 语言高性能编程 | 极客兔兔 pprof 性能分析 | Go 语言高性能编程 benchmark 基准测试 | Go 语言高性能编程 Go 语言高性能编程 | 极客兔兔 Go 接口型函数的使用场景 | 极客兔兔 Python 简明教程 | 快速入门 | 极客兔兔 Go 语言笔试面试题(代码输出) | 极客面试 | 极客兔兔 动手写RPC框架 - GeeRPC第七天 服务发现与注册中心(registry) | 极客兔兔 动手写RPC框架 - GeeRPC第六天 负载均衡(load balance) 动手写RPC框架 - GeeRPC第五天 支持HTTP协议 | 极客兔兔 动手写RPC框架 - GeeRPC第四天 超时处理(timeout) | 极客兔兔 动手写RPC框架 - GeeRPC第三天 服务注册(service register) 动手写RPC框架 - GeeRPC第二天 支持并发与异步的客户端 | 极客兔兔 动手写RPC框架 - GeeRPC第一天 服务端与消息编码 | 极客兔兔
动手写分布式缓存 - GeeCache第七天 使用 Protobuf 通信
2020-02-17 · via 极客兔兔

源代码/数据集已上传到 Github - 7days-golang

geecache protobuf

本文是7天用Go从零实现分布式缓存GeeCache的第七篇。

  • 为什么要使用 protobuf?
  • 使用 protobuf 进行节点间通信,编码报文,提高效率。代码约50行

1 为什么要使用 protobuf

protobuf 即 Protocol Buffers,Google 开发的一种数据描述语言,是一种轻便高效的结构化数据存储格式,与语言、平台无关,可扩展可序列化。protobuf 以二进制方式存储,占用空间小。

protobuf 的安装和使用教程请移步 Go Protobuf 简明教程,这篇文章就不再赘述了。protobuf 广泛地应用于远程过程调用(RPC) 的二进制传输,使用 protobuf 的目的非常简单,为了获得更高的性能。传输前使用 protobuf 编码,接收方再进行解码,可以显著地降低二进制传输的大小。另外一方面,protobuf 可非常适合传输结构化数据,便于通信字段的扩展。

使用 protobuf 一般分为以下 2 步:

  • 按照 protobuf 的语法,在 .proto 文件中定义数据结构,并使用 protoc 生成 Go 代码(.proto 文件是跨平台的,还可以生成 C、Java 等其他源码文件)。
  • 在项目代码中引用生成的 Go 代码。

2 使用 protobuf 通信

新建 package geecachepb,定义 geecachepb.proto

day7-proto-buf/geecache/geecachepb/geecachepb.proto - github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
syntax = "proto3";

package geecachepb;

message Request {
string group = 1;
string key = 2;
}

message Response {
bytes value = 1;
}

service GroupCache {
rpc Get(Request) returns (Response);
}
  • Request 包含 2 个字段, group 和 cache,这与我们之前定义的接口 /_geecache/<group>/<name> 所需的参数吻合。
  • Response 包含 1 个字段,bytes,类型为 byte 数组,与之前吻合。

生成 geecache.pb.go

1
2
3
$ protoc --go_out=. *.proto
$ ls
geecachepb.pb.go geecachepb.proto

可以看到 geecachepb.pb.go 中有如下数据类型:

1
2
3
4
5
6
7
8
type Request struct {
Group string `protobuf:"bytes,1,opt,name=group,proto3" json:"group,omitempty"`
Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"`
...
}
type Response struct {
Value []byte `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
}

接下来,修改 peers.go 中的 PeerGetter 接口,参数使用 geecachepb.pb.go 中的数据类型。

day7-proto-buf/geecache/peers.go - github

1
2
3
4
5
import pb "geecache/geecachepb"

type PeerGetter interface {
Get(in *pb.Request, out *pb.Response) error
}

最后,修改 geecache.gohttp.go 中使用了 PeerGetter 接口的地方。

day7-proto-buf/geecache/geecache.go - github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import (

pb "geecache/geecachepb"
)

func (g *Group) getFromPeer(peer PeerGetter, key string) (ByteView, error) {
req := &pb.Request{
Group: g.name,
Key: key,
}
res := &pb.Response{}
err := peer.Get(req, res)
if err != nil {
return ByteView{}, err
}
return ByteView{b: res.Value}, nil
}

day7-proto-buf/geecache/http.go - github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import (

pb "geecache/geecachepb"
"github.com/golang/protobuf/proto"
)

func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) {


body, err := proto.Marshal(&pb.Response{Value: view.ByteSlice()})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Write(body)
}

func (h *httpGetter) Get(in *pb.Request, out *pb.Response) error {
u := fmt.Sprintf(
"%v%v/%v",
h.baseURL,
url.QueryEscape(in.GetGroup()),
url.QueryEscape(in.GetKey()),
)
res, err := http.Get(u)

if err = proto.Unmarshal(bytes, out); err != nil {
return fmt.Errorf("decoding response body: %v", err)
}

return nil
}
  • ServeHTTP() 中使用 proto.Marshal() 编码 HTTP 响应。
  • Get() 中使用 proto.Unmarshal() 解码 HTTP 响应。

至此,我们已经将 HTTP 通信的中间载体替换成了 protobuf。运行 run.sh 即可以测试 GeeCache 能否正常工作。

总结

到这一篇为止,7 天用 Go 动手写/从零实现分布式缓存 GeeCache 这个系列就完成了。简单回顾下。第一天,为了解决资源限制的问题,实现了 LRU 缓存淘汰算法;第二天实现了单机并发,并给用户提供了自定义数据源的回调函数;第三天实现了 HTTP 服务端;第四天实现了一致性哈希算法,解决远程节点的挑选问题;第五天创建 HTTP 客户端,实现了多节点间的通信;第六天实现了 singleflight 解决缓存击穿的问题;第七天,使用 protobuf 库,优化了节点间通信的性能。如果看到这里,还没有动手写的话呢,赶紧动手写起来吧。一天差不多只需要实现 100 行代码呢。

附 推荐


last updated at 2026-02-23