大家好,我是张大鹏。今天给大家分享的是如何手写一个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这样的稳定的服务。
好了,今天的分享就到这里了。
谢谢大家!
文章评论