我们在并发HTTP Server的时候,经常有对接口内容做缓存的需求。例如:某些热点内容,我们希望在1分钟内做缓存,避免短时间内不会对相同的内容进行重复性读取与运算,同时也降低系统的整体负载。
有时我们需要把缓存逻辑放在Server内部,而非网观测如Nginx等。是因为这样我们可以根据需求便捷地清除缓存,或者利用Redis等其他储存空间做为缓存後端。
也因此需要快取的资料有两种特徵:
那快取的种类也可依不同类型分为三种:
Client Cache指的是服务器与浏览器之间的快取机制,Server Side透过服务器的快取将不常变动的资讯储存在浏览器快取之中,避免重复下载浪费效能。
Client Cache的设定方式是透过HTTP Request的Header去带params,当浏览器接收到特定params就会采取相对应的快取处理。
如果对Client Cache有兴趣的人可以参考下方连结,胡立大写的很好
https://blog.techbridge.cc/2017/06/17/cache-introduction/
Server Cache主要指的是在Backend 与 Database间资料的Cache。当大量的Query Request进来时,会导致Database的I/O操作过多,因而造成Session堵塞、效能低落等问题,即使进行读写分离在不同的丛集当中,也只能解决部份问题,因此我们会将经常被查询的资料储存在像是Redis之类的key-value资料库,并以LRU等Strategy来进行快取资料的变更,以分担资料库I/O压力。而我们这章节主要也是在讲Gin在Server Cache的实作!
有兴趣者也可以参考Cloudflare的官方资讯,他们解说的挺详细的
https://www.cloudflare.com/zh-tw/cdn/
最後则是Network Cache,他的理念即是User会从离他最近的Server去取资料,用以节省Response Time,也就是CDN(Content Delivery Network)的概念!
这样的缓存场景无非是有缓存时从缓存取,无缓存时从下游服务取,并将数据放入缓存中。这其实是非常通用的逻辑,应该可以将其抽象出来。从而缓存逻辑无需侵入进业务逻辑。
这边我们选用的是yahui大神所重新封装的gin-cache
package,因其可以依据自身要求定义cache key, 且在性能方面也较官方的gin-contrib/cache
优秀,因此选用它。
go get -u [github.com/chenyahui/gin-cache](http://github.com/chenyahui/gin-cache%E3%80%82)
app/config/router.go
package config
import (
cache "github.com/chenyahui/gin-cache"
"github.com/chenyahui/gin-cache/persist"
"github.com/gin-gonic/gin"
"ironman-2021/app/controller"
"ironman-2021/app/middleware"
_ "ironman-2021/docs"
"time"
)
func RouteUsers(r *gin.Engine, m *persist.RedisStore) {
posts := r.Group("/v1/users")
{
posts.POST("/", controller.NewUsersController().CreateUser)
posts.GET("/:id", middleware.JWTAuthMiddleware(), cache.CacheByRequestURI(m, 2*time.Hour),
controller.QueryUsersController().GetUser)
posts.POST("/login", controller.LoginUserController().AuthHandler)
}
}
GET /v1/users/:id
这个endpoint吃得到快取,然後快取时间设定为2小时main.go
package main
import (
"github.com/chenyahui/gin-cache/persist"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/joho/godotenv"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
"ironman-2021/app/config"
"ironman-2021/app/dao"
"ironman-2021/app/middleware"
"ironman-2021/app/model"
"os"
)
// @title Gin swagger
// @version 1.0
// @description Gin swagger
// @contact.name Flynn Sun
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// schemes http
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
func main() {
envErr := godotenv.Load()
if envErr != nil {
panic(envErr)
}
port := os.Getenv("PORT")
dbConfig := os.Getenv("DB_CONFIG")
db, ormErr := dao.Initialize(dbConfig)
if ormErr != nil {
panic(ormErr)
}
migrateErr := db.AutoMigrate(&model.User{})
if migrateErr != nil {
return
}
server := gin.Default()
server.Use(middleware.CORSMiddleware())
server.GET("/hc", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "health check",
})
})
redisStore := persist.NewRedisStore(redis.NewClient(&redis.Options{
Network: "tcp",
Addr: "redis:6379",
DB: 0,
}))
config.RouteUsers(server, redisStore)
url := ginSwagger.URL("http://localhost:8080/swagger/doc.json")
server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
err := server.Run(":" + port)
if err != nil {
panic(err)
}
}
我们可以看到下图在第一次GET /v1/users/:id
与之後几次的Response Time都差距相当的多,因为第二次之後都是从Cache拿取资料
此外我们也可以进去redis的container,并观察到当第一次GET /v1/users/:id
後,会多一个/v1/users/1
的Key,我们就以Key-Value的资料去读取快取
docker exec -it redis bash
root@e2c9049d9ac3:/data# redis-cli -n 0
127.0.0.1:6379> KEYS *
(empty array)
127.0.0.1:6379> KEYS *
1) "/v1/users/1"
127.0.0.1:6379>
题外话,能够如此容易的使用各项服务与监测效能也是我们用docker
与 docker-compose
的好处之一。
cache.CachePage(store, time.Minute, func(c *gin.Context) {
c.String(200, "pong"+fmt.Sprint(time.Now().Unix()))
})
ResponseWriter
的写入函数。每次在gin中调用写入函数时,触发一次性能的获取和追加操作。比较差的。func CachePageAtomic(store persistence.CacheStore, expire time.Duration, handle gin.HandlerFunc) gin.HandlerFunc {
var m sync.Mutex
p := CachePage(store, expire, handle)
return func(c *gin.Context) {
m.Lock()
defer m.Unlock()
p(c)
}
}
在缓存设计中,会遇到一个常见的问题:缓存击穿。缓存击穿指的是:当某个热点Key在其缓存过期的一瞬间,大量的请求将访问不到这个Key对应的缓存。这时请求将直接打到下游的储存或服务当中。一瞬间大量的请求,可能会对下游服务造成极大的压力。
关於这个问题,golang 官方有一个Single Flight:golang.org/x/sync/singleflight,可以有效的解决缓存击穿问题。其原理非常简单,有兴趣的可以直接在 Github 搜源码看就可以了,到此不再展开讨论。
使用Linux CPU 8核,16G内存系统配置下,Cyhone大使用wrk对gin-contrib/cache
与gin-cache
做benchmark压力测试,这边我则撷取他的实验结果来解说,有兴趣者可以去Reference的连结转连。
wrk -c 500 -d 1m -t 5 http://127.0.0.1:8080/hello
gin-cache
提升了23%。gin-cache
相比QPS更提升了30倍左右。当然也使用gin-cache
使用的redis客户端库的性能更好。gin-cache
的优势将更加明显。https://blog.techbridge.cc/2017/06/17/cache-introduction/
https://www.cloudflare.com/zh-tw/cdn/
https://www.cyhone.com/articles/gin-cache/
<<: Day24 X Web Rendering Architectures
Whenever a perfect ERP installation is expected, o...
IF、CASE翻成中文就是「如果」,根据条件来决定要执行的事情, 在各个程序语法理面都会有类似的语法...
问题回答 Vue 是 SPA 框架,而 Nuxt 是 Vue 生态系里的一个能同时实现 SPA 和 ...
今年的疫情蛮严重的,希望大家都过得安好,希望疫情快点过去,能回到一些线下技术聚会的时光~ 今天目标:...
在 build an Android example app 的时候:输入: bazel build...