
protoc-gen-redis 是一个 protoc 插件:输入 .proto,输出 Redis Hash 的存取代码。本文讲它解决的问题、存储布局,以及序列化和引擎兼容性的设计取舍。
游戏数据层的两级存储问题
游戏业务最常见的数据层是「Redis 缓存 + MySQL/Mongo 持久化」。同一份数据存在两个地方,业务层就得维护一致性:先写缓存还是先写库、缓存什么时候失效、崩溃后怎么回源。热更、并发刷数据的时候,这里最容易出脏数据和并发覆盖;而且这套一致性代码不属于业务规则(双写、失效、回源是通用问题),却没有标准组件,每个项目都得自己实现一遍。
换个思路:Redis 就是数据库
数据只存一份,就没有一致性问题。把 Redis 当数据库用:结构用 protobuf 定义,一个 message 对应一个 Redis Hash,字段对应 Hash field。业务层只有一层数据要写,读写都是 Redis 命令。
内存 Redis 贵。生产环境可以换兼容 Redis 协议、支持磁盘持久化的引擎(比如腾讯 Tendis),命令语法不变,应用层零改动。后面单独说兼容性。

protoc-gen-redis 做什么
这是一个 protoc 插件:输入 .proto,输出 Redis Hash 存取代码。生成文件是自包含的——message 结构体、枚举、字段常量、存取方法全部重新声明,不依赖 protoc-gen-go 的输出,输出到独立目录即可直接用。
每个 message 生成一组方法:
GetFields/SetFields:按字段读写。字段用Field<Message>_<字段名>常量标识(值就是 proto tag),只传要操作的字段就是按需读写,读写集合字段时自动走元素级存储- 集合字段(map/repeated)元素级操作:
Set<Field>/Get<Field>/Del<Field>/Append<Field>,单元素 O(1);Set<Field>All整体替换用 Lua 脚本原子完成 MarshalRedisProto/UnmarshalRedisProto:嵌套 message 按标准 protobuf wire format 编解码,语言无关
存储布局

| 数据类型 | Hash field 名 | Value |
|---|---|---|
| 标量 / 枚举 / string / bytes | proto tag(如 1、3) | 十进制字符串 / 原样 |
| 嵌套 message | proto tag(如 13) | protobuf wire format 字节 |
| map<K,V> | <tag>:<key>(如 9:sound) | 元素值 |
| repeated T | <tag>:<下标>(如 8:0) | 元素值 |
Hash key 形如 REDB#<维度1>:<维度2>:<维度3>,三个维度怎么用完全由业务自定义,插件只负责把 key_format 的占位符按顺序填上。游戏开发里常见的划分是:系统 ID 区分业务(家园系统=1、好友系统=2)、玩家 UID、二级区分 ID(赛季、角色等,不需要就填 0)——换赛季直接换 key,老数据不掺和。格式可用 --redis_opt=key_format=... 定制。
代码示例
proto 定义:
syntax = "proto3";
package user;
message UserBaseInfo {
int32 user_id = 1;
string username = 2;
Gender gender = 3; // 枚举
repeated string friends = 8; // 集合:元素级存储
map<string, string> settings = 9;
Weapon weapon = 13; // 嵌套 message:protobuf 序列化
repeated Weapon weapons = 12; // message 元素
}
Go 侧使用:
u := &cmddb.UserBaseInfo{
UserId: 1001, Username: "alice",
Friends: []string{"bob"},
Weapon: cmddb.Weapon{Name: "sword", Damage: 10},
}
u.SetFields(conn, 1, 123456, 3) // 整体写入(家园系统=1,赛季 3)
got := &cmddb.UserBaseInfo{}
got.GetFields(conn, 1, 123456, 3, cmddb.FieldUserBaseInfo_Username) // 按需读
idx, _ := got.AppendFriends(conn, 1, 123456, 3, "carol") // 元素级追加
got.SetSettings(conn, 1, 123456, 3, "sound", "80") // map 单键写
got.SetFriendsAll(conn, 1, 123456, 3, []string{"x", "y"}) // Lua 原子整体替换
序列化为什么用 protobuf wire format
嵌套 message 存进 Redis 的字节是标准 protobuf 编码,编码遵循 proto3 语义(零值标量不编码、message 恒编码、repeated 逐元素编码),解码时未知字段跳过、兼容 packed repeated。游戏服务器常见 Go / C++ / Lua 混布,其他语言拿同一份 .proto 就能直接解析这些字节,不需要和 Go 端协商序列化格式——这是把 gob 换成 protobuf 的原因,gob 只有 Go 能读。
Tendis 兼容性
生成代码的命令分两类,对兼容引擎的依赖不同:
- 纯命令(HMGET / HSET / HGET / HDEL / HSCAN):任何 RESP 兼容引擎都能跑,包括标量字段读写、元素级操作、GetAll
- Lua 脚本(EVAL):集合字段的 SetFields、Set/Del<Field>All、Append<Field>,依赖引擎的 Lua 支持
Tendis 各版本对 Lua 的支持不一样,选型前先确认:
| Tendis 版本 | Lua / EVAL | 影响 |
|---|---|---|
| 腾讯云存储版 | 不支持 | 集合字段批量操作不可用 |
| 腾讯云混合存储版 | 支持(不跨 slot) | 脚本只操作单 key,天然满足 |
| 开源 Tendisplus | 完整支持 | 无影响 |
磁盘引擎上 HSCAN 是全量遍历,GetAll 的性能比内存 Redis 差,Hash 大时要按业务评估。
使用
go build -o protoc-gen-redis.exe .
protoc --plugin=./protoc-gen-redis.exe --redis_out=. --redis_opt=paths=source_relative user.proto
仓库:github.com/beijian128/protoc-gen-redis。配套的单元测试用 golden 文件对比生成结果,集成测试直连 Redis(或指向 Tendis,存储版会挂掉全部 Lua 用例,正好当兼容性冒烟测试)。



