golang实现分布式缓存笔记(一)基于http的缓存服务
前言
这个月我想学一下go语言,于是决定学习一个go实现的缓存服务。
首先本文基于golang的http包实现一个简单http的缓存服务,因为用golang自带的http包实现一个处理请求的服务端十分便利,我们只需要写一个简单的map保存数据,写一个http的handler处理请求即可,你不需要考虑任何复杂的并发问题,因为golang的http服务框架会帮你处理好底层的一切。
cache
缓存服务接口
本文实现的简单缓存具备三种基本接口 : set
get
del
分别通过http协议的put
、get
、delete
、操作进行。
put
put /cache/<key> content <value>
get
get /cache/<key> content <value>
delete
delete /cache/<key>
cache包实现
本缓存服务里面通过一个cache包实现缓存功能。cache包接口定义
:
package cache type cache interface { set(string, []byte) error get(string) ([]byte, error) del(string) error getstat() stat }
cache 接口实现
cache 结构很简单,一张map,另加一把锁保护即可.
package cache import "sync" type simplecache struct { c map[string][]byte mutex sync.rwmutex stat } func (c *simplecache) set(k string, v []byte) error { c.mutex.lock() defer c.mutex.unlock() tmp, exist := c.c[k] if exist { c.del(k, tmp) } c.c[k] = v c.add(k, v) return nil } func (c *simplecache) get(k string) ([]byte, error) { c.mutex.rlock() defer c.mutex.runlock() return c.c[k], nil } func (c *simplecache) del(k string) error { c.mutex.lock() defer c.mutex.unlock() v, exist := c.c[k] if exist { delete(c.c, k) c.del(k, v) } return nil } func (c *simplecache) getstat() stat { return c.stat } func newinmemorycache() *simplecache { return &simplecache{make(map[string][]byte), sync.rwmutex{}, stat{}} }
cache包测试:
package main import ( "./cache" "fmt" ) func main() { c := cache.new("inmemory") k, v := "sola", []byte{'a','i','l','u','m','i','y','a'} c.set(k, v) tmp, _ := c.get(k) fmt.println("key: ", k, " value: ", tmp) c.del(k) tmp, _ = c.get(k) fmt.println("key: ", k, " value: ", tmp) }
sola@sola:~/coder/github/go-cache/http-cache/server$ go run main.go 2019/02/10 00:07:15 inmemory ready to serve key: sola value: [97 105 108 117 109 105 121 97] sola@sola:~/coder/github/go-cache/http-cache/server$ go run main.go 2019/02/10 00:07:28 inmemory ready to serve key: sola value: [97 105 108 117 109 105 121 97] key: sola value: []
golang http包使用介绍
golang自带的http包已经实现了htpp客户端和服务端,我们可以利用它更为快速的开发http服务。本章仅介绍一下http包服务端的使用。
golang中处理 http 请求主要跟两个东西相关:servemux 和 handler。
servrmux 本质上是一个 http 请求路由器(或者叫多路复用器,multiplexor)。它把收到的请求与一组预先定义的 url 路径列表做对比,然后在匹配到路径的时候调用关联的处理器(handler)。
处理器(handler)负责输出http响应的头和正文。任何满足了http.handler接口的对象都可作为一个处理器。通俗的说,对象只要有个如下签名的servehttp方法即可:
servehttp(http.responsewriter, *http.request)
golang的 http 包自带了几个函数用作常用处理器,比如notfoundhandler 和 redirecthandler。notfoundhandler
返回一个简单的请求处理器,该处理器会对每个请求都回复"404 page not found"。redirecthandler
返回一个请求处理器,该处理器会对每个请求都使用状态码code重定向到网址url。
接着,我们来看两个简单的样例:
hello.go
package main import ( "io" "log" "net/http" ) func hellogoserver(w http.responsewriter, req *http.request) { io.writestring(w, "hello, this is a goserver") } func main() { http.handlefunc("/", hellogoserver) err := http.listenandserve(":9090", nil) if err != nil { log.fatal("listenandserver ", err) } }
浏览器看看我们的hello程序:
1、 http.handlefunc("/", hellogoserver)
http提供的外部方法handlefunc实际也是调用servemux的内部方法,只是它使用的是http包默认的servemux,注册一个处理器函数handler(hellogoserver
)和对应的模式pattern(/
)(注册到defaultservemux
)。servemux的文档解释了模式的匹配机制。
2、http.listenandserve(":9090", nil)
listenandserve同字面意思监听并服务。这里是监听9090端口,它其实也是一个外部方法,调用内部server类型的listenandserve。
redirect.go
package main import ( "log" "net/http" ) func main() { mux := http.newservemux() rh := http.redirecthandler("http://www.baidu.com", 307) mux.handle("/foo", rh) log.println("listening...") http.listenandserve(":3000", mux) }
1、这个样例中我们没用默认的servemux,而是通过 http.newservemux 函数来创建一个空的 servemux。
2、http.redirecthandler 函数创建了一个重定向处理器,这个处理器会对收到的所有请求,都执行307重定向操作到 http://www.baidu.com。
3、servemux.handle 函数将处理器注册到新创建的 servemux,所以它在 url 路径/foo 上收到所有的请求都交给这个处理器。
4、最后通过 http.listenandserve 函数启动服务处理请求,通过传递刚才创建的 servemux来为请求去匹配对应处理器。
键入后你会跳转到百度。
http-cache-server 实现
最后来实现我们的cache-server
cache已经有了,我们只需要写一个http的handler来分别处理get
,put
,delete
请求即可。
上面提过任何满足了http.handler接口的对象即servehttp(http.responsewriter, *http.request)
都可作为一个处理器,那么我们先来看看这个接口的参数.
responsewriter
接口被http处理器用于构造http回复。
type responsewriter interface { // header返回一个header类型值,该值会被writeheader方法发送。 // 在调用writeheader或write方法后再改变该对象是没有意义的。 header() header // writeheader该方法发送http回复的头域和状态码。 // 如果没有被显式调用,第一次调用write时会触发隐式调用writeheader(http.statusok) // writerheader的显式调用主要用于发送错误码。 writeheader(int) // write向连接中写入作为http的一部分回复的数据。 // 如果被调用时还未调用writeheader,本方法会先调用writeheader(http.statusok) // 如果header中没有"content-type"键, // 本方法会使用包函数detectcontenttype检查数据的前512字节,将返回值作为该键的值。 write([]byte) (int, error) }
request
类型代表一个服务端接受到的或者客户端发送出去的http请求。request各字段的意义和用途在服务端和客户端是不同的。
type request struct { // method指定http方法(get、post、put等)。对客户端,""代表get。 method string // url在服务端表示被请求的uri,在客户端表示要访问的url。 // // 在服务端,url字段是解析请求行的uri(保存在requesturi字段)得到的, // 对大多数请求来说,除了path和rawquery之外的字段都是空字符串。 // (参见rfc 2616, section 5.1.2) // // 在客户端,url的host字段指定了要连接的服务器, // 而request的host字段(可选地)指定要发送的http请求的host头的值。 url *url.url // 接收到的请求的协议版本。本包生产的request总是使用http/1.1 proto string // "http/1.0" protomajor int // 1 protominor int // 0 // header字段用来表示http请求的头域。如果头域(多行键值对格式)为: // accept-encoding: gzip, deflate // accept-language: en-us // connection: keep-alive // 则: // header = map[string][]string{ // "accept-encoding": {"gzip, deflate"}, // "accept-language": {"en-us"}, // "connection": {"keep-alive"}, // } // http规定头域的键名(头名)是大小写敏感的,请求的解析器通过规范化头域的键名来实现这点。 // 在客户端的请求,可能会被自动添加或重写header中的特定的头,参见request.write方法。 header header // body是请求的主体。 // // 在客户端,如果body是nil表示该请求没有主体买入get请求。 // client的transport字段会负责调用body的close方法。 // // 在服务端,body字段总是非nil的;但在没有主体时,读取body会立刻返回eof。 // server会关闭请求的主体,servehttp处理器不需要关闭body字段。 body io.readcloser // contentlength记录相关内容的长度。 // 如果为-1,表示长度未知,如果>=0,表示可以从body字段读取contentlength字节数据。 // 在客户端,如果body非nil而该字段为0,表示不知道body的长度。 contentlength int64 // transferencoding按从最外到最里的顺序列出传输编码,空切片表示"identity"编码。 // 本字段一般会被忽略。当发送或接受请求时,会自动添加或移除"chunked"传输编码。 transferencoding []string // close在服务端指定是否在回复请求后关闭连接,在客户端指定是否在发送请求后关闭连接。 close bool // 在服务端,host指定url会在其上寻找资源的主机。 // 根据rfc 2616,该值可以是host头的值,或者url自身提供的主机名。 // host的格式可以是"host:port"。 // // 在客户端,请求的host字段(可选地)用来重写请求的host头。 // 如过该字段为"",request.write方法会使用url字段的host。 host string // form是解析好的表单数据,包括url字段的query参数和post或put的表单数据。 // 本字段只有在调用parseform后才有效。在客户端,会忽略请求中的本字段而使用body替代。 form url.values // postform是解析好的post或put的表单数据。 // 本字段只有在调用parseform后才有效。在客户端,会忽略请求中的本字段而使用body替代。 postform url.values // multipartform是解析好的多部件表单,包括上传的文件。 // 本字段只有在调用parsemultipartform后才有效。 // 在客户端,会忽略请求中的本字段而使用body替代。 multipartform *multipart.form // trailer指定了会在请求主体之后发送的额外的头域。 // // 在服务端,trailer字段必须初始化为只有trailer键,所有键都对应nil值。 // (客户端会声明哪些trailer会发送) // 在处理器从body读取时,不能使用本字段。 // 在从body的读取返回eof后,trailer字段会被更新完毕并包含非nil的值。 // (如果客户端发送了这些键值对),此时才可以访问本字段。 // // 在客户端,trail必须初始化为一个包含将要发送的键值对的映射。(值可以是nil或其终值) // contentlength字段必须是0或-1,以启用"chunked"传输编码发送请求。 // 在开始发送请求后,trailer可以在读取请求主体期间被修改, // 一旦请求主体返回eof,调用者就不可再修改trailer。 // // 很少有http客户端、服务端或代理支持http trailer。 trailer header // remoteaddr允许http服务器和其他软件记录该请求的来源地址,一般用于日志。 // 本字段不是readrequest函数填写的,也没有定义格式。 // 本包的http服务器会在调用处理器之前设置remoteaddr为"ip:port"格式的地址。 // 客户端会忽略请求中的remoteaddr字段。 remoteaddr string // requesturi是被客户端发送到服务端的请求的请求行中未修改的请求uri // (参见rfc 2616, section 5.1) // 一般应使用uri字段,在客户端设置请求的本字段会导致错误。 requesturi string // tls字段允许http服务器和其他软件记录接收到该请求的tls连接的信息 // 本字段不是readrequest函数填写的。 // 对启用了tls的连接,本包的http服务器会在调用处理器之前设置tls字段,否则将设tls为nil。 // 客户端会忽略请求中的tls字段。 tls *tls.connectionstate }
golang请求及应答中涉及到的常量.
golang中的http状态码
const ( statuscontinue = 100 statusswitchingprotocols = 101 statusok = 200 statuscreated = 201 statusaccepted = 202 statusnonauthoritativeinfo = 203 statusnocontent = 204 statusresetcontent = 205 statuspartialcontent = 206 statusmultiplechoices = 300 statusmovedpermanently = 301 statusfound = 302 statusseeother = 303 statusnotmodified = 304 statususeproxy = 305 statustemporaryredirect = 307 statusbadrequest = 400 statusunauthorized = 401 statuspaymentrequired = 402 statusforbidden = 403 statusnotfound = 404 statusmethodnotallowed = 405 statusnotacceptable = 406 statusproxyauthrequired = 407 statusrequesttimeout = 408 statusconflict = 409 statusgone = 410 statuslengthrequired = 411 statuspreconditionfailed = 412 statusrequestentitytoolarge = 413 statusrequesturitoolong = 414 statusunsupportedmediatype = 415 statusrequestedrangenotsatisfiable = 416 statusexpectationfailed = 417 statusteapot = 418 statusinternalservererror = 500 statusnotimplemented = 501 statusbadgateway = 502 statusserviceunavailable = 503 statusgatewaytimeout = 504 statushttpversionnotsupported = 505 )
golang 中的http行为常量定义
5 package http 6 7 // common http methods. 8 // 9 // unless otherwise noted, these are defined in rfc 7231 section 4.3. 10 const ( 11 methodget = "get" 12 methodhead = "head" 13 methodpost = "post" 14 methodput = "put" 15 methodpatch = "patch" // rfc 5789 16 methoddelete = "delete" 17 methodconnect = "connect" 18 methodoptions = "options" 19 methodtrace = "trace" 20 )
cachehandler
到这里所有用到的http包中结构都已经说明了,开始写main包,
我们定义一个cachehandler类型,用我们的inmemorycache接口初始化它,并实现他的servehttp方法。
最后将cachehandler类型的cachehandler方法注册到http包默认的servemux路由,绑定端口26316,启动服务。
package main import ( "./cache" "io/ioutil" "net/http" "log" "strings" ) type cachehandler struct { cache.cache } func (h *cachehandler) cachehandler(w http.responsewriter, r *http.request) { log.println("url ", r.url, " method ", r.method) //split get key key := strings.split(r.url.escapedpath(), "/")[2] if len(key) == 0 { w.writeheader(http.statusbadrequest) return } m := r.method if m == http.methodput { h.handleput(key, w, r) return } else if m == http.methodget { h.handleget(key, w, r) return } else if m == http.methoddelete { h.handledelete(key, w, r) return } w.writeheader(http.statusmethodnotallowed) } func (h *cachehandler) handleput(k string, w http.responsewriter, r *http.request){ b, _ := ioutil.readall(r.body) if len(b) != 0 { e := h.set(k, b) if e != nil { log.println(e) w.writeheader(http.statusinternalservererror) } else { w.write([]byte("successful")) } } } func (h *cachehandler) handleget(k string, w http.responsewriter, r *http.request){ b, e := h.get(k) if e != nil { log.println(e) w.writeheader(http.statusinternalservererror) return } if len(b) == 0 { w.writeheader(http.statusnotfound) return } w.write(b) } func (h *cachehandler) handledelete(k string, w http.responsewriter, r *http.request){ e := h.del(k) if e != nil { log.println(e) w.writeheader(http.statusinternalservererror) } else { w.write([]byte("successful")) } } func main() { c := cache.new("inmemory") h := cachehandler{c} http.handlefunc("/cache/", h.cachehandler) http.listenandserve(":26316", nil) }
程序测试
使用postman测试put
浏览器直接测试get
使用postman测试delete
再次get会返回404
与redis的比较
缓存功能的服务已经实现了,那么它的性能怎样呢,键值对缓存服务中比较有名的是redis,我们和它做下比较。
redis是一款in memory数据结构存储,可以被用作数据库、缓存及消息中间件。支持包括字符串、散列、列表及集合在内的多种数据结构、支持范围查询、具备内建的复制功能、lua脚本、lru缓存淘汰策略、事务处理及两种不同的磁盘持久化方案(rdb和aof)还能建立redis集群提供高可用性能。
redis的rdb持久化方案会在指定时间点将内存数据集快照存入磁盘。rdb开始工作时,会自己fork出一个持久化进程,此时原服务进程的一切内存数据相当于保存了一份快照、然后持久化进程将它的内存压缩并写入磁盘。
redis的aof方案则是将服务接受到的所有写操作记入磁盘上的日志文件、将日志文件的格式和redis协议保持一致且只允许添加。
rdb方案对性能的影响比aof小,因为它不占用原服务进程的磁盘io、rdb的缺点在于系统死机时丢失的数据比aof要多,因为它只保留得到数据到上一次持久化进程运行的那个时间点,而aof可以一直记录到系统死机之前的最后一次写操作的数据。
本篇实现的是一个简单的内存缓存,不包含持久化方案,也不会保存进磁盘,一旦服务器重启所有数据就会丢失。
性能方面只有redis的1/4,主要原因在于rest协议的解析上,rest基于http,http基于tcp,而redis是直接建立在tcp上的。
下一篇文章会实现一个基于tcp的缓存协议规范。本系列笔记最终实现的缓存会是使用http/rest协议和tcp混合的接口规范,其中http/rest只用于各种管理功能。
本文源码 :https://github.com/bethlyrosedaisley/go-cache-server/tree/master/http-cache/server
参考资料:
分布式缓存-原理、架构及go语言实现 ----- 胡世杰