1. 本章实现原理
2. REST接口定义
* PUT /cache/<key>
请求正文 **-** <value>
* GET /cache/<key>
响应正文
<value>
* DELETE /cache/<key>
响应正文
<value>
* GET /status
响应正文
JSON格式的缓存状态
3.缓存功能流程
-
SET
-
GET
-
DEL
4. Go语言实现HTTP内存缓存服务
- 本程序的依赖
# 初始化go mod
go mod init cache-server
# 安装第三方依赖库
go get github.com/go-redis/redis
go get github.com/hashicorp/memberlist
go get stathat.com/c/consistent
- main包的实现
- 缓存服务的main包只有一个函数,就是main函数,在Go语言中,如果某个项目需要被编译为可执行程序,那么它的源码就需要有一个main包,如果项目不需要被编译为可执行程序,只是实现一个库,则可以没有main包和main函数
# "main.go" 文件名
package main
func main() {
c := cache.New("inmemory")
}
- cache包的实现
- 我们在cache包中实现服务的缓存功能。在cache包中,我们首先声明一个Cache接口
# "cache/cache.go" 文件夹/文件名
package cache
type Cache interface {
Set(string, []byte) error
Get(string) ([]byte, error)
Del(string) error
GetStat() Stat
}
在go语言中,接口和实现是完全分开的。开发者可以自由声明一个接口,然后以一种或多种方式去实现这个接口,我们看到的就是一个名为Cache的接口名称
- cache包中的Stat结构体实现
# "cache/stat.go"
package cache
type Stat struct {
Count int64
KeySize int64
ValueSize int64
}
func (s *Stat) add(k string, v []byte) {
s.Count += 1
s.KeySize += int64(len(k))
s.ValueSize += int64(len(v))
}
func (s *Stat) del(k string, v []byte) {
s.Count -= 1
s.KeySize -= int64(len(k))
s.ValueSize -= int64(len(v))
}
stat用于记录存储的状态,当我们添加一个key-value时,我们会累积keySize和ValueSize,当我们删除一个key-value时,我们会减去keySize和ValueSize,count则记录我们存储了几对key-value
- cache包中New函数实现
# "cache/new.go"
package cache
import "log"
func New(typ string) Cache {
var c Cache
if typ == "inmemory" {
c = newInMemoryCache()
}
if c == nil {
panic("unknown cache type" + typ)
}
log.Println(typ, "ready to serve")
return c
}
当我们执行New函数时,如果传入的是 "inmemory", 那么我们返回的是一个cache实例,如果是一个空,那么我们就抛出一个错误,内容为 “unknown cache type”。
- cache包中 inMemoryCache 的实现
# "cache/inmemory.go"
package cache
import (
"sync"
)
type inMemoryCache struct {
c map[string][]byte
mutex sync.RWMutex
Stat
}
func (c *inMemoryCache) Set(k string, v []byte) error {
/*
当至少有一个goroutine写时,就会调用这一行,此时他会等待所有其他读写锁释放,然后自己加锁
在它加锁后其他groutine需要加锁时就需要等待它先解锁,这样当同时有两个set的请求时,
第一个先被执行的set会开始加锁,然后第二个set请求会等待第一个set请求执行完并且解锁后才会继续执行
属于自己的set函数
*/
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 *inMemoryCache) Get(k string) ([]byte, error) {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.c[k], nil
}
func (c *inMemoryCache) 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 *inMemoryCache) GetStat() Stat {
return c.Stat
}
func InMemoryCache() *inMemoryCache {
return &inMemoryCache{
make(map[string][]byte),
sync.RWMutex{},
Stat{},
}
}
func newInMemoryCache() *inMemoryCache {
return &inMemoryCache{make(map[string][]byte), sync.RWMutex{}, Stat{}}
}
inMemoryCache 结构体包含一个成员c,类型是以string为key, 以 []byte 为value的map,用来保存键值对;一个mutex,类型是sync.RWMutex,用来对map的并发访问提供读写锁保护;一个Stat,用来记录缓存的状态,stat作为一个结构体,嵌入到inMemoryCache结构体中,相当于类的继承,将一个类的实例化对象,作为另一个类的类属性。
- http包的实现
- http包用来实现我们的HTTP服务,我们在HTTP包里并没有声明接口,而是直接声明了一个Server结构体
# "http/server.go" 文件夹/文件名 http文件夹与main.go 与 cache文件夹同级
package http
import (
"cache-server/cache" // 如果报错为找不到,请检查一下go.mod 文件第一行是否为 module cache-server
"net/http"
)
type Server struct {
cache.Cache
}
func (s *Server) Listen() {
http.Handle("/cache/", s.cacheHandler())
http.Handle("/status", s.statusHandler())
http.ListenAndServe(":12345", nil)
}
func New(c cache.Cache) *Server{
return &Server{c}
}
- http包下的cacheHandler 相关实现
# "http/cache.go"
package http
import (
"io/ioutil"
"log"
"net/http"
"strings"
)
type cacheHandler struct {
*Server
}
func (h *cacheHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
key := strings.Split(r.URL.EscapedPath(), "/")[2]
if len(key) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
m := r.Method
if m == http.MethodPut {
b, _ := ioutil.ReadAll(r.Body)
if len(b) != 0 {
e := h.Set(key, b)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
}
}
return
}
if m == http.MethodGet {
b, e := h.Get(key)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
return
}
if len(b) == 0 {
w.WriteHeader(http.StatusNotFound)
return
}
w.Write(b)
return
}
if m == http.MethodDelete {
e := h.Del(key)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.WriteHeader(http.StatusMethodNotAllowed)
}
func (s *Server) cacheHandler() http.Handler {
return &cacheHandler{s}
}
- http包下statusHandler 相关实现
# "http/status.go"
package http
import (
"encoding/json"
"log"
"net/http"
)
type statusHandler struct {
*Server
}
func (h *statusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
b, e := json.Marshal(h.GetStat())
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(b)
}
func (s *Server) statusHandler() http.Handler {
return &statusHandler{s}
}
5. 功能演示
- 请提前配置好$GOPATH环境变量
$ go build *.go
$ ./main
2021/09/01 00:46:59 inmemory ready to serve
当显示inmemory ready to serve时,我们的服务就启动成功了
- 测试命令
# set 设置key-value
$ curl -v 127.0.0.1:12345/cache/name -XPUT -dxiaofeixia
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 12345 (#0)
> PUT /cache/name HTTP/1.1
> Host: 127.0.0.1:12345
> User-Agent: curl/7.64.1
> Accept: *r'/'*
> Content-Length: 10
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 10 out of 10 bytes
< HTTP/1.1 200 OK
< Date: Tue, 31 Aug 2021 16:50:47 GMT
< Content-Length: 0
<
* Connection #0 to host 127.0.0.1 left intact
* Closing connection 0
# get 获取key-value
$ curl 127.0.0.1:12345/cache/name -XGET
xiaofeixia%
# status 查询存储状态
$ curl 127.0.0.1:12345/status
{"Count":1,"KeySize":4,"ValueSize":10}%
# del 删除key-value 删除后可以在查看一下status,是否清0
$ curl 127.0.0.1:12345/cache/name -XDELETE
6. cache-server 与 redis 的 benchmark 比较
....