面試官:你可以寫一個通用的Redis緩存”裝飾器“么?
本文轉載自微信公眾號「GoLang全棧」,作者小錕哥哥。轉載本文請聯系GoLang全棧公眾號。
今天是小年,先祝大家小年快樂!
所以我得送一篇技術文章慶祝一下,
請看今天我們咋用”裝飾器模式“搞定Redis的緩存。
啥是裝飾器模式?
首先得先搞懂啥是裝飾器,學過 Java 或者 Python 的同學應該不會陌生,比如這樣:
public class Hello implements Shape {
@Override
public void draw() {
System.out.println("Hello");
}
}
里面的那個 @Override 就是裝飾器,具體咋實現的呢?
請詢問資深 Java 工程師去。
為啥叫裝飾器呢?
個人覺得可能看他在方法的上面,像頭飾吧,具體是不是這原因,我也不知道,不對別打我哈。
其實,你可以理解他就是一個閉包方法,要調用被修飾的方法之前就需要先經過他,有點像攔路虎。
聽著是不是很像中間件,其實是差不太多的邏輯啦。
但是為啥我們不直接用中間件來搞緩存呢?
中間件他一般是掛在某個路由組下面的,但是呢,我們要做緩存的又不可能整個路由組都需要做。
于是就想著用裝飾器的思路去搞定這個緩存,我可以在我需要的某個方法之前戴一個裝飾器就可以了。
先實現一個傳統的API
我們這里使用 Gin 框架來搭建:
func UserListHandler() gin.HandlerFunc {
return func(c *gin.Context) {
list := db.GetUserListFromMySQL()
res := gin.H{
"list": list,
}
c.JSON(200, res)
}
}
func UserDetailHandler() gin.HandlerFunc {
return func(c *gin.Context) {
user := db.GetUserDetailListFromMySQL()
res := gin.H{
"user": user,
}
c.JSON(200, res)
}
}
func main() {
r := gin.Default()
r.GET("/user/list/:type", UserListHandler())
r.GET("/user/detail/:id", UserDetailHandler())
r.Run()
}
我們 db 部分我們就寫一個模擬方法,去模擬從數據庫里面讀取數據:
package db
import "fmt"
type User struct {
Id int64
Name string
}
func GetUserListFromMySQL() *[]User {
fmt.Println("模擬從數據庫獲取數據...")
list := make([]User,2)
list[0] = User{
Id: 1,
Name: "張三",
}
list[1] = User{
Id: 2,
Name: "李四",
}
return &list
}
func GetUserDetailListFromMySQL() *User {
fmt.Println("模擬從數據庫獲取數據...")
return &User{
Id: 2,
Name: "李四",
}
}
這樣以來就能跑起來了。
預熱下 Redis
我們使用的庫是:
github.com/gomodule/redigo/redis
如果不知道怎么使用的,請參考我們往期 redis 的教程文章!
這里我粘貼下關鍵代碼:
package k_redis
import (
"github.com/gomodule/redigo/redis"
"time"
)
var RedisDefaultPool *redis.Pool
func newPool(addr string) *redis.Pool {
return &redis.Pool{
MaxIdle: 3,
IdleTimeout: 240*time.Second,
Dial: func() (redis.Conn, error) {
return redis.Dial("tcp", addr, redis.DialPassword("密碼"))
},
}
}
func init() {
RedisDefaultPool = newPool("IP:端口")
}
接下來我們就可以使用 Redis 了:
// 讀
conn := k_redis.RedisDefaultPool.Get()
defer conn.Close()
res, err := redis.String(conn.Do("get", redisKey))
fmt.Println(res)
// 寫
conn.Do("setex", redisKey, 20, resData)
編寫裝飾器
我們的裝飾器咋加呢?
需要在路由方法做手腳,也就是這里:
r.GET("/user/list/:type", UserListHandler())
我們只需要在 UserListHandler 這個方法外面再套一個方法,這個方法就是裝飾器!
這個方法我們需要滿足:傳入的是 gin.HandlerFunc 方法,傳出的也是 gin.HandlerFunc 這個即可!
但是為了通用性,我們需要加三個入參:
1、Redis里面的key規則參數 redisKeyPattern
2、Redis里面的key關鍵字參數 param
3、返回回去的數據參數 empty
開干,代碼如下:
func Decorator(h gin.HandlerFunc, param string, redisKeyPattern string, empty interface{}) gin.HandlerFunc {
return func(c *gin.Context) {
// 取Redis里面的key關鍵字參數
getId := c.Param(param)
// 根據Redis里面key的規則,生成RedisKey
redisKey := fmt.Sprintf(redisKeyPattern, getId)
// 從Redis里面讀取數據
conn := k_redis.RedisDefaultPool.Get()
defer conn.Close()
res, err := redis.String(conn.Do("get", redisKey))
if err != nil { //緩存沒有
log.Println("從數據庫取...",err)
// 執行下一部分
h(c)
dbRes,exists := c.Get("Result")
if !exists {
dbRes = empty
}
// 存緩存 轉成字節流存
resData,_ := json.Marshal(dbRes)
conn.Do("setex", redisKey, 20, resData)
c.JSON(200, dbRes)
}else{
log.Println("從緩存庫取...")
json.Unmarshal(res, &empty)
c.JSON(200, empty)
}
}
}
里面有很多 error 我給忽略了,讀者可自行根據需要處理!
這個裝飾器比較關鍵的點在 c.Get("Result") 這個邏輯,我們之前的兩個控制器方法就需要改造了!
func UserListHandler() gin.HandlerFunc {
return func(c *gin.Context) {
list := db.GetUserListFromMySQL()
res := gin.H{
"list": list,
}
//c.JSON(200, res)
c.Set("Result", res)
}
}
func UserDetailHandler() gin.HandlerFunc {
return func(c *gin.Context) {
user := db.GetUserDetailListFromMySQL()
res := gin.H{
"user": user,
}
//c.JSON(200, res)
c.Set("Result", res)
}
}
我們不能在這里面返回 json 數據了,而是通過 gin 的上下文進行值傳遞。
依次傳遞到裝飾器里面。
所以在裝飾器里面才可以通過 c.Get("Result")來獲取到值!

































