欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

golang实现分布式缓存笔记(一)基于http的缓存服务

程序员文章站 2022-05-25 19:44:51
[toc] 前言 这个月我想学一下go语言,于是决定学习一个go实现的缓存服务。 首先本文基于golang的http包实现一个简单http的缓存服务,因为用golang自带的http包实现一个处理请求的服务端十分便利,我们只需要写一个简单的map保存数据,写一个http的handler处理请求即可, ......

前言

这个月我想学一下go语言,于是决定学习一个go实现的缓存服务。

首先本文基于golang的http包实现一个简单http的缓存服务,因为用golang自带的http包实现一个处理请求的服务端十分便利,我们只需要写一个简单的map保存数据,写一个http的handler处理请求即可,你不需要考虑任何复杂的并发问题,因为golang的http服务框架会帮你处理好底层的一切。

cache

缓存服务接口

本文实现的简单缓存具备三种基本接口 : set get del 分别通过http协议的putgetdelete、操作进行。

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。

golang实现分布式缓存笔记(一)基于http的缓存服务

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程序:
golang实现分布式缓存笔记(一)基于http的缓存服务
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来为请求去匹配对应处理器。

键入后你会跳转到百度。

golang实现分布式缓存笔记(一)基于http的缓存服务

http-cache-server 实现

最后来实现我们的cache-server
cache已经有了,我们只需要写一个http的handler来分别处理getput,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
golang实现分布式缓存笔记(一)基于http的缓存服务
浏览器直接测试get
golang实现分布式缓存笔记(一)基于http的缓存服务

使用postman测试delete
golang实现分布式缓存笔记(一)基于http的缓存服务

再次get会返回404
golang实现分布式缓存笔记(一)基于http的缓存服务

与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 net/http包

go 中文标准库

分布式缓存-原理、架构及go语言实现 ----- 胡世杰