大家好,我是张大鹏。今天给大家分享的是如何手写一个Golang的缓存服务。
提到缓存,大家应该第一时间想到的就是Redis,非常的轻量级,而且性能特别的高。
最开始的时候,作者也喜欢使用Redis,当然,现在也喜欢。不过分享一下个人的观点:Redis是有缺点的,那就是必须要部署才能够使用。
那么有没有一种缓存服务,不需要部署,只要在项目运行的时候,就能够使用呢?能不能跨项目使用呢?比如说,我用Golang写了两个API,能不能第一个API启动的时候,缓存服务启动,这个缓存服务还能同时被第二个API使用呢?
这个需求是真实存在的,不过不一定每个项目都需要。这个主要能够解决多个项目之间数据临时存储的问题。
基于这种需求,今天写了一个下午的代码,终于开发出了一个Golang的缓存服务,在这里分享给大家。
这个项目的地址是:https://github.com/zhangdapeng520/zdpgo_cache_http
首先介绍基本的使用。
通过以下代码,能够启动缓存服务:
package mainimport ("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 mainimport ("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_httpimport ("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 {*ServerLog *zdpgo_log.Log}// ServeHTTP 实现ServeHTTP接口func (h *cacheHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {// 获取keykey := 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_httpimport ("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 *ConfigRequests *zdpgo_requests.RequestsLog *zdpgo_log.LogBaseUrl 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 Staterr = 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这样的稳定的服务。
好了,今天的分享就到这里了。
谢谢大家!
文章评论