前言
這個月我想學一下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語言實現 ----- 胡世傑