不能直接用 json.marshal 存多维数组到 redis,因为redis不理解json结构,存入的只是字节流,无法支持按子数组范围查询或原子更新;必须通过业务语义可控扁平化,将多维关系映射为带前缀的key-value对,并用pipeline批量操作确保性能与原子性。

为什么不能直接用 json.Marshal 存多维数组到 Redis
Go 的 json.Marshal 虽然能序列化 []interface{} 或嵌套 slice,但对含 nil 元素、混合类型(比如 []interface{}{1, "a", []int{2, 3}})或多层指针的结构容易出错;更关键的是,Redis 本身不理解 JSON 结构,你存进去的只是字节流,后续想按“某一层子数组”做范围查询或原子更新根本做不到。真正需要的不是通用序列化,而是**按业务语义可控扁平化**——把多维关系映射成一组带前缀的 key-value 对。
用 redis.Pipeline + 自定义键生成函数批量写入
假设你有一个 [][]string,想存成类似 user:123:tags:0、user:123:tags:0:0、user:123:tags:0:1 这样的结构,就得自己拆解维度并生成 key。重点不是“怎么序列化”,而是“怎么设计 key 的层级和分隔符”。
- 避免用冒号
:做唯一分隔符——如果原始数据里本身含:(比如时间戳或 URL),会导致解析歧义;建议用不可见字符如\x00或固定长度编码(如00001表示索引 1) - 写入必须用 pipeline:单个
SET调用网络开销大,且无法保证原子性;redis.Pipeline能合并请求,失败时整批回滚 - 不要在循环里反复调用
client.Set—— 每次都建连接/发命令,性能差一个数量级
// 示例:扁平化 [][]string 到 redis
func flattenAndStore(client *redis.Client, baseKey string, data [][]string) error {
pipe := client.Pipeline()
for i, row := range data {
rowKey := fmt.Sprintf("%s:%d", baseKey, i)
pipe.Set(rowKey, len(row), 0) // 存长度,方便后续读取时预分配
for j, item := range row {
itemKey := fmt.Sprintf("%s:%d:%d", baseKey, i, j)
pipe.Set(itemKey, item, 0)
}
}
_, err := pipe.Exec()
return err
}
读取时如何还原多维结构而不爆内存
Redis 没有“获取所有 user:123:tags:*:*”的原生语法,KEYS 命令在生产环境禁用(阻塞主线程),必须用 SCAN。但 SCAN 返回无序 key,你需要靠 key 命名规则重建索引关系。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 先用
SCAN匹配baseKey + ":*"获取所有行 key,再对每个行 key 扫描其下":*"子项 —— 两层 SCAN 是安全的,但要注意游标管理 - 别一次性
GET所有子项:如果某行有上万列,pipe.Get(...)会把全部值加载进内存;应按需分页,或改用HGETALL把每行存成 hash(更适合列数固定场景) - 注意 TTL:如果给每个子 key 单独设过期时间,容易出现“行头存在但子项已过期”的脏状态;推荐只给顶层 key(如
user:123:tags)设 TTL,子项共享
用 redis.HSet 替代多层 key 的适用边界
如果你的多维数组其实是“固定列名的二维表”(比如 []map[string]string),直接用 HSET user:123:profile name "Alice" age "30" 更合适。Hash 天然支持字段级读写、不存在字段自动忽略,且 HGETALL 返回有序 map,比拼接 key 省事得多。
- 仅当需要按“某列范围”查(如“所有第 2 列值 > 100 的行”)时,才值得用多 key 方案 —— 否则就是给自己加锁
- Hash 不支持嵌套结构,
HSET user:123:tags 0 "go" 1 "rust"可以,但HSET user:123:nested 0:0 "a"会被当成字符串字段名,Redis 不解析冒号 - 如果真要嵌套,考虑用
json.RawMessage存最内层数组,外层用 Hash 字段控制维度,折中兼顾可读性和扁平化
真正麻烦的从来不是“怎么存”,而是“以后怎么删其中一行”或者“怎么给第 i 行第 j 列加个过期时间”——这些操作在 key 命名方案里全得手动推导,漏掉一个 DEL 就留孤儿数据。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










