写在前面

嘛,好久没有写文章了,今年还是第二篇文章,已经五月份了捏,时间过得真快。

我呢,好不容易有了自己的时间,开始写我自己的项目,之前都在弄学校的东西或者在比赛,忙得要死。

现在这个项目用 GoFrame 写的,我已经写了近万行有关 ORM 和 Redis 的代码,结果发现有尼玛更简单的写法(我直接摆烂,焯)。

其实吧,GoFrame 配合 Redis 缓存这个事儿,说简单也简单,说复杂也复杂。主要还是我当时太懒了,文档都没好好看(典型的程序员通病,谁让我们都喜欢直接上手呢 🤷‍♂️)。

结果就把缓存这块写得跟裹脚布一样,又臭又长。

友情提示:本文默认你已经会 GoFrame 的基础操作了,不会的话…先去补课吧(手动狗头)

项目准备

开始搞事情之前,你需要准备两样东西:数据库配置(废话)和用 gf gen 生成的 ORM 代码(再废话一次)。

数据库配置连接

这玩意儿放在 manifest/config/config.yaml 文件里,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
database:
default:
debug: true
host: "127.0.0.1"
port: "5432"
user: "bamboo-service"
pass: "bamboo-service"
name: "bamboo-service"
type: "pgsql"

redis:
default:
address: 127.0.0.1:6379
db: 1

(看到这个配置是不是很眼熟?对,就是最基础的那种,没有花里胡哨的东西)

配置代码生成

这个配置文件藏在 hark/config.yaml 里:

1
2
3
4
5
6
7
8
gfcli:
gen:
dao:
- link: "pgsql:bamboo-service:bamboo-service@tcp(127.0.0.1:5432)/bamboo-service"
descriptionTag: true
jsonCase: "Snake"
removePrefix: "fy_"
gJsonSupport: true

配置完就可以愉快地 gf gen dao 了,生成一堆代码,爽歪歪~

简单使用

按照 GoFrame 官方文档(我这次真的看了,不骗你),最基础的 ORM 操作长这样:

1
2
3
4
5
6
7
8
9
10
11
var userEntity *entity.User
sqlErr := dao.User.Ctx(ctx).Where(&do.User{UserUUID: %UserUuidValue%}).Scan(&userEntity)
if sqlErr != nil {
g.Log().Error(ctx, sqlErr)
return
}
if userEntity == nil {
return
}

// 这里就是你的业务逻辑(想干啥就干啥)

而单纯使用 Redis 的话,用 g.Redis() 就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cacheData, redisErr := g.Redis().GetEX(ctx, fmt.Sprintf("role:name:%s", roleName))
if redisErr != nil {
g.Log().Error(ctx, redisErr)
return
}

if cacheData.IsNil() || cacheData.IsEmpty() {
return // 数据不存在就直接溜了
}

// 数据转换为结构体(这步比较关键)
var roleEntity *entity.Role
roleEntity, operateErr := butil.MapToStruct(cacheData.Map(), roleEntity)
if operateErr != nil {
g.Log().Error(ctx, operateErr)
return
}

// 业务逻辑继续...

看起来很简单对吧?分开写确实没什么难度。

问题来了(重点来了)

但是!(这里要大声说)如果我想要第一次从数据库拿数据,后面都从缓存拿,减少数据库的 I/O 压力呢?

这时候代码就开始变得"有趣"了…我当时写出来的代码,现在看看都想给自己一拳 😅

下面是一个反面教材,展示如何通过 UUID 获取用户信息,缓存不存在就查数据库:

注意哦,这段代码是写在 dao 层的 user.go 文件里的,就是用 gf gen dao 生成的那堆代码里。
不要写到 interface 文件里去了(血的教训,别问我怎么知道的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// MapToStruct 数据转换工具(反正就是个工具函数)
//
// 参数:
// - value: map[string]interface{} 格式的数据
// - target: 要转换的目标结构体
//
// 返回:
// - 转换后的结构体
// - 可能出现的错误(总是要处理错误的嘛)
func MapToStruct[E interface{}](value map[string]interface{}, target E) (E, error) {
for k, v := range value {
if v == "null" {
value[k] = nil
}
}
var newTarget E
err := gconv.Struct(value, &newTarget)
return newTarget, err
}

// GetUserByUUID 通过 UUID 获取用户信息(麻烦的版本)
func (cDao *userDao) GetUserByUUID(ctx context.Context, userUUID string) (*entity.User, error) {
// 先试试缓存里有没有
cacheData, redisErr := g.Redis().GetEX(ctx, fmt.Sprintf("user:uuid:%s", userUUID))
if redisErr != nil {
g.Log().Error(ctx, redisErr)
return nil, redisErr
}

var userEntity *entity.User
if cacheData.IsNil() || cacheData.IsEmpty() {
// 缓存没有,只能去数据库了(唉)
sqlErr := dao.User.Ctx(ctx).Where(&do.User{UserUUID: userUUID}).Scan(&userEntity)
if sqlErr != nil {
g.Log().Error(ctx, sqlErr)
return nil, sqlErr
}
} else {
// 缓存有数据,转换一下
userEntity, operateErr := butil.MapToStruct(cacheData.Map(), userEntity)
if operateErr != nil {
g.Log().Error(ctx, operateErr)
return nil, operateErr
}
}

// 最后检查一下数据
if userEntity == nil {
return nil, gerror.New("user not found")
}

return userEntity, nil
}

写完这段代码,我当时还挺得意的(现在想想真是年轻啊 🤦‍♂️)

更优雅的解决方案(终于来了)

后来我重新翻了翻文档 ORM链式操作-查询缓存,发现自己就是个…算了,不骂自己了。

GoFrame 人家早就给你准备好了缓存功能,我却在这里重复造轮子 🤡

启用缓存(重要步骤)

在你的 main.go 文件里,加上这几行代码就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
initCtx := gctx.GetInitCtx()

err := gtime.SetTimeZone("Asia/Shanghai")
if err != nil {
panic(err)
}

su := setup.New(initCtx)
su.CheckUserSuperAdminExist()
su.GetVariableSetup()

/* 缓存大法好!就这两行代码 */
redisCache := gcache.NewAdapterRedis(g.Redis())
g.DB().GetCache().SetAdapter(redisCache)

cmd.Main.Run(initCtx)
}

就这样,缓存就启用了!是不是简单到让人想哭?

使用缓存(爽到飞起)

为了测试效果,我写了个简单的 Debug 接口:

1
2
3
4
5
6
7
8
9
10
11
12
func (c *ControllerV1) BaseDebug(ctx context.Context, req *v1.BaseDebugReq) (res *v1.BaseDebugRes, err error) {
var systemEntity *entity.System
blog.ControllerDebug(ctx, "BaseDebug", "基础调试信息获取")
// 看这里!就加了个 .Cache() 方法,就这么简单!
sqlErr := dao.System.Ctx(ctx).Cache(gdb.CacheOption{Duration: 1 * time.Minute}).Where(&do.System{Key: "author_name"}).Scan(&systemEntity)
if sqlErr != nil {
return nil, sqlErr
}
return &v1.BaseDebugRes{
ResponseDTO: bresult.Success(ctx, "基础调试信息获取成功"),
}, nil
}

测试效果(见证奇迹的时刻)

打开浏览器访问 http://localhost:8080/api/v1/debug,效果立竿见影!

第一次访问:慢吞吞的,因为要查数据库
第二次访问:飞一般的感觉,直接从缓存拿




对缓存参数的深度解剖(技术宅时间)

首先,我们来看看 GoFrame 给我们生成的默认缓存键:SelectCache:fy_system@default#bamboo-service:5828117350005325686

这个缓存键看起来就像是程序员的身份证号码一样复杂,让我们来"解剖"一下这个神秘的字符串:

  • SelectCache - 这是在说"我是个查询缓存"(就像在自我介绍)
  • fy_system - 表名,告诉我们这是哪张表的数据
  • default - 数据库连接名,就像是说"我来自默认连接"
  • bamboo-service - 数据库名称,这是我们的"家"
  • 5828117350005325686 - 查询条件的哈希值(我猜测的,反正是个很大的数字,像是彩票号码)

当然,如果你觉得这个默认的缓存键太"随意",你也可以给它起个更有意义的名字:

1
dao.System.Ctx(ctx).Cache(gdb.CacheOption{Duration: 1 * time.Minute, Name: "custom_cache_key"}).Where(&do.System{Key: "author_name"}).Scan(&systemEntity)

这样缓存键就变成了 SelectCache:custom_cache_key,简洁多了。

但是等等!这样写有个问题 - 所有使用这个缓存键的查询都会被缓存到同一个地方,就像所有人都用同一个储物柜,容易拿错东西。

所以更好的做法是这样:

1
2
var userUUID string
dao.User.Ctx(ctx).Cache(gdb.CacheOption{Duration: 1 * time.Minute, Name: fmt.Sprintf("user:uuid:%s", userUUID)}).Where(&do.User{UserUUID: userUUID}).Scan(&userEntity)

这样缓存键就是 SelectCache:user:uuid:具体的UUID值,每个用户都有自己专属的缓存"储物柜"。

说实话,GoFrame 默认生成的缓存键其实挺聪明的,它会根据查询条件自动生成唯一的键,就像是给每个查询都分配了一个专属的"身份证"。

不过,如果你的项目比较复杂,比如:

  • 多个系统共用一个 Redis(就像多个室友共用一个冰箱)
  • 不同语言的项目混合开发(Java 的 SpringBoot 和 Go 的 GoFrame 在一起工作)

这时候自定义缓存键就很有必要了,否则可能会出现"张三的数据被李四拿走了"的尴尬情况。毕竟 SpringBoot 可不认识 GoFrame 的缓存键命名规则!

总结(终于到总结了)

写完这篇文章,我的内心是崩溃的…之前那些复杂的缓存代码,原来可以这么简单就搞定!

通过本次"踩坑"之旅,我们学到了:

  1. RTFM 很重要:Read The F***ing Manual,文档真的要好好看(血泪教训)
  2. 框架缓存香得很:GoFrame 内置的缓存功能简单粗暴,两行代码搞定
  3. 配置超级简单
    • gcache.NewAdapterRedis(g.Redis()) 设置缓存适配器
    • .Cache(gdb.CacheOption{Duration: 1 * time.Minute}) 启用查询缓存
    • GoFrame 自动处理所有缓存逻辑
  4. 性能提升显著:缓存命中后,查询速度飞起来了

最重要的一点:别像我一样,遇到问题就开始重复造轮子。先看看官方文档,说不定人家早就给你准备好了更优雅的解决方案 😂

这种内置缓存的方式,既简单又高效,避免了手动编写缓存逻辑的各种坑。以后用 GoFrame 开发,这就是我推荐的缓存使用姿势!