前端缓存 抽象的节约成本方案

前言

由于一些敏感问题不方便在此提具体名称,所以我会在下面把人名用某人代替。这并不完全是技术性文章,该项目根据我们自身情况定制,不具有太大参考价值

起因

大概两个月之前某个人想了个花活出来,即前端缓存,挺抽象的一东西,但最终结果似乎用起来还成,谁知道呢,反正前端不是我写

原理

故名思意,即缓存前端的内容,在页面加载的同时向该缓存后端获取不同维度的数据,预测可能会访问的页面,并在访问不同页面的同时发送新请求进一步缓存内容,减轻主站后端的压力。

该缓存后端可以即开即用且无数据保密需求,所以可以放在赞助商或者月抛的机器上,进一步做到分布式,分散请求。

某人说这个东西各家大厂也有在做,也可以用在游戏服务器领域,当然我是不太清楚了,大厂肯定比我们的要高级的多。

技术栈

语言仍然选择Golang ,已经变成Golang的形状了

我一共写了两个版本,第一个版本由于需求不够明确,写了很多冗余语句,写的很烂,用了很多莫名其妙的If语句,当了风滚草源码怪,第一个版本使用Gin+Gorm,这里不多做赘述,以下主要关于重构版本

使用框架:

Go-Zero 项目地址: go-zero 缩短从需求到上线的距离

Ent Orm 项目地址:ent (entgo.io)

*Go-Zero架构图

为什么这样选择技术栈?

Go-Zero是新兴的微服务框架,同时也是我们使用Golang重构社区后端的选用框架,框架优点可以自行去Go-Zero项目地址查看,选用它作为前端缓存的框架原因主要在于其高性能以及便于后期接入重构主站后端的鉴权。

Ent 相对于Gorm总体相对来说要大一些,但是性能总体来说甚至会略微优于Gorm,且有常用封装以及其是类型安全。但总体来说还是我的个人偏好,使用Gorm还是Sqlx并不有多大影响。

实现

总体来说就是各种操作数据库,由于项目未开源,不在此具体写明,主要还是讲一些实现过程中需要注意的地方。

Handler 序列化

在该项目中,为了方便Cloudflare缓存我们的缓存数据,所以需要返回string而非结构体/Json数据,但是Go-Zero其响应格式默认为application/json,即使返回的并非标准json也会被序列化为json,就有了以下问题:

// 修改前
// Logic
func (l *GetCacheLogic) GetCache(req *types.CacheGetReq) (resp string, err error)
// Hanler
func GetCacheHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.CacheGetReq
		if err := httpx.Parse(r, &req, true); err != nil {
			httpx.ErrorCtx(r.Context(), w, err)
			return
		}

		l := cache.NewGetCacheLogic(r.Context(), svcCtx)
		resp, err := l.GetCache(&req)
		if err != nil {
			httpx.ErrorCtx(r.Context(), w, err)
		} else {
			httpx.OkJsonCtx(r.Context(), w, resp)
		}
	}
}

Logic返回一个String数据给Handler,但由于上方提到的特性,会导致进行请求返回的数据实际上为"xxxxxxxxxxxxxxx"的格式,没错,会加上"",这会导致前端无法序列化这个数据,导致错误。

正常也不会用到仅返回Sring的情况,所以在Go-Zero的issue自然也没有这个问题的直接解决方法,所以需要使用自定义响应参数。

文档相关页面:响应参数 | go-zero Documentation

// 修改后
// handler
func GetCacheHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.CacheGetReq
		if err := httpx.Parse(r, &req, true); err != nil {
			httpx.ErrorCtx(r.Context(), w, err)
			return
		}

		l := cache.NewGetCacheLogic(r.Context(), svcCtx)
		resp, err := l.GetCache(&req)
		if err != nil {
			httpx.ErrorCtx(r.Context(), w, err)
		} else {
			w.WriteHeader(200)
			_, err = w.Write([]byte(resp))
			if err != nil {
				httpx.ErrorCtx(r.Context(), w, err)
				return
			}
		}
	}
}

在此我们通过http.ResponseWriterWrite 方法来返回响应参数,以及Header方法来返回响应头。

通过这样就能正确返回一个Content-Type为text/plain响应参数

总结

总体来说并没有遇到比较离谱的问题,但是有不少细节需要自己修改,实现过程也挺顺利,前端逻辑会相对麻烦一些,但这就不是我的工作了。

需要注意的是该项目并没有用到微服务中的RPC服务,而是直接使用API作为单体服务。

目前该项目已经作为社区的基础设施。

也不算多有技术性,写这个的主要目的其实也就是往Blog水点文章,所以随便看看就行。