如何手写一个Golang缓存服务?

2022年6月5日 317点热度 0人点赞 0条评论

大家好,我是张大鹏。今天给大家分享的是如何手写一个Golang的缓存服务。


提到缓存,大家应该第一时间想到的就是Redis,非常的轻量级,而且性能特别的高。


最开始的时候,作者也喜欢使用Redis,当然,现在也喜欢。不过分享一下个人的观点:Redis是有缺点的,那就是必须要部署才能够使用。


那么有没有一种缓存服务,不需要部署,只要在项目运行的时候,就能够使用呢?能不能跨项目使用呢?比如说,我用Golang写了两个API,能不能第一个API启动的时候,缓存服务启动,这个缓存服务还能同时被第二个API使用呢?


这个需求是真实存在的,不过不一定每个项目都需要。这个主要能够解决多个项目之间数据临时存储的问题。


基于这种需求,今天写了一个下午的代码,终于开发出了一个Golang的缓存服务,在这里分享给大家。


这个项目的地址是:https://github.com/zhangdapeng520/zdpgo_cache_http


首先介绍基本的使用。


通过以下代码,能够启动缓存服务:

package main
import ( "github.com/zhangdapeng520/zdpgo_cache_http")
/*@Time : 2022/6/5 14:04@Author : 张大鹏@File : main@Software: Goland2021.3.1@Description: HTTP缓存服务*/
func main() { cache := zdpgo_cache_http.NewWithConfig(&zdpgo_cache_http.Config{ Debug: true, Server: zdpgo_cache_http.HttpInfo{ Port: 3333, }, }) err := cache.Run() if err != nil { panic(err) }}

通过以下代码能够获取客户端,进行缓存操作:

package main
import ( "fmt" "github.com/zhangdapeng520/zdpgo_cache_http")
/*@Time : 2022/6/5 14:46@Author : 张大鹏@File : main@Software: Goland2021.3.1@Description:*/
func main() { cache := zdpgo_cache_http.NewWithConfig(&zdpgo_cache_http.Config{ Debug: true, Client: zdpgo_cache_http.HttpInfo{ Port: 3333, }, })
// 获取缓存信息 client := cache.GetClient() stat := client.GetStat() cache.Log.Debug("获取缓存数据成功", "data", stat)
// 设置键值对 flag := client.Set("username", "zhangdapeng520") if !flag { panic("添加键值对失败") } stat = client.GetStat() cache.Log.Debug("获取缓存数据成功", "data", stat)
// 根据键获取值 username := client.Get("username") fmt.Println("根据键获取的值:", username)
// 根据键删除值 client.Delete("username") stat = client.GetStat() cache.Log.Debug("获取缓存数据成功", "data", stat)}

功能特别的简单,代码也非常的少。主要功能就是设置缓存,获取缓存,删除缓存,获取缓存信息。

代码也不是特别的多,毕竟是只用了一个下午写完的,截图如下:

图片

其中examples下有服务端的案例和客户端的案例。大家如果感兴趣的话可以直接复制粘贴进行使用。

这个项目目前发布的版本是v0.1.0,是能够集成在go module之中的。

下面给大家简单分析一下这个项目的实现思路:首先是缓存的实现方式,这个项目是基于HTTP实现的,后面还会有TCP的实现。基于这两种协议实现的优点非常的明显,那就是能够跨平台,跨语言使用。因为现在不管什么语言,基本上都是支持HTTP的。

底层服务的实现没有用特别复杂的东西,主要用了Go语言自带的map数据结构,key是字符串,值是byte数组。不过,目前实现的客户端,支持的是键值都是字符串。这种结构能够覆盖大多数场景,因为复杂的数据结构,可以转换为json字符串进行存储,如果是文件,也可以转换为base64字符串进行存储。

由于篇幅有限,这里不分析所有代码,只给大家分享一下比较关键的几处。

以下代码是关于缓存的实现:

package zdpgo_cache_http
import ( "github.com/zhangdapeng520/zdpgo_log" "io/ioutil" "net/http" "strings")
/*@Time : 2022/6/5 14:29@Author : 张大鹏@File : handler_cache@Software: Goland2021.3.1@Description: 缓存处理器*/
type cacheHandler struct { *Server Log *zdpgo_log.Log}
// ServeHTTP 实现ServeHTTP接口func (h *cacheHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 获取key key := strings.Split(r.URL.EscapedPath(), "/")[2] if len(key) == 0 { w.WriteHeader(http.StatusBadRequest) return }
// 获取方法 m := r.Method
// 新增或修改值 if m == http.MethodPost { b, _ := ioutil.ReadAll(r.Body) if len(b) != 0 { e := h.Set(key, b) if e != nil { h.Log.Error("新增或修改缓存数失败", "error", e) w.WriteHeader(http.StatusInternalServerError) } } return }
// 获取值 if m == http.MethodGet { b, e := h.Get(key) if e != nil { h.Log.Error("根据键获取缓存的值失败", "error", 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 { h.Log.Error("根据键删除值失败", "error", e) w.WriteHeader(http.StatusInternalServerError) } return }
// 返回响应 w.WriteHeader(http.StatusMethodNotAllowed)}
// 缓存处理器func (s *Server) cacheHandler() http.Handler { return &cacheHandler{ Server: s, Log: s.Log, }}

通过阅读源码不难发现,这里使用了RESTFul风格的接口,使用POST方法表示新增和更新,使用DELETE方法表示删除,使用GET方法表示查询。这里的路由是通过以下代码注册的:

func (c *Cache) Run() error {  // 添加路由  http.Handle("/cache/", c.Server.cacheHandler())  http.Handle("/status", c.Server.statusHandler())
// 启动服务 c.Log.Debug("启动缓存服务", "port", c.Config.Server.Port) address := fmt.Sprintf("%s:%d", c.Config.Server.Host, c.Config.Server.Port) err := http.ListenAndServe(address, nil) if err != nil { c.Log.Error("启动缓存服务失败", "error", err) return err } return nil}

所以接口的地址是/cache/key/value,以下代码是客户端的实现,大家可以作为参考:

package zdpgo_cache_http
import ( "encoding/json" "fmt" "github.com/zhangdapeng520/zdpgo_log" "github.com/zhangdapeng520/zdpgo_requests")
/*@Time : 2022/6/5 14:47@Author : 张大鹏@File : client@Software: Goland2021.3.1@Description:*/
type Client struct { Config *Config Requests *zdpgo_requests.Requests Log *zdpgo_log.Log BaseUrl string}
func NewClient(req *zdpgo_requests.Requests, config *Config) *Client { addr := fmt.Sprintf("http://%s:%d", config.Client.Host, config.Client.Port) return &Client{ Requests: req, Log: req.Log, Config: config, BaseUrl: addr, }}
// GetStat 获取缓存状态信息func (c *Client) GetStat() *Stat { response, err := c.Requests.Any(zdpgo_requests.Request{ Method: "GET", Url: c.BaseUrl + "/status", }) if err != nil { c.Log.Error("请求缓存状态失败", "error", err) return nil }
// 解析json数据 var stat Stat err = json.Unmarshal(response.Content, &stat) if err != nil { c.Log.Error("解析状态信息失败", "error", err) return nil }
// 返回 return &stat}
// Set 设置键值对func (c *Client) Set(key, value string) bool { response, err := c.Requests.AnyText(zdpgo_requests.Request{ Method: "POST", Url: c.BaseUrl + "/cache/" + key, Text: value, }) if err != nil { c.Log.Error("设置缓存键值对失败", "error", err) return false }
// 判断是否添加成功 flag := response.StatusCode == 200
// 返回 return flag}
// Get 根据键获取值func (c *Client) Get(key string) string { response, err := c.Requests.Any(zdpgo_requests.Request{ Method: "GET", Url: c.BaseUrl + "/cache/" + key, }) if err != nil { c.Log.Error("根据键获取缓存键值失败", "error", err) return "" }
// 返回 return response.Text}
// Delete 根据键删除值,不关心删除结果func (c *Client) Delete(key string) { _, err := c.Requests.Any(zdpgo_requests.Request{ Method: "DELETE", Url: c.BaseUrl + "/cache/" + key, }) if err != nil { c.Log.Error("根据键删除值失败", "error", err) return }}

如果读者感兴趣,完全可以开发出Java的客户端,Python的客户端,PHP的客户端。因为这是基于RESTFul的接口的,扩展起来十分的方便。不过比较建议的还是在Golang中使用。这个项目的期望是能够作为组件集成到Golang的项目中,如果是跨语言的,需要部署服务的场景,作者个人也比较推荐使用Redis这样的稳定的服务。

好了,今天的分享就到这里了。

谢谢大家!

77670如何手写一个Golang缓存服务?

这个人很懒,什么都没留下

文章评论