外观
Redis 数据类型
Phase 05 — Redis 涵盖:
Redis 基础·String·Hash·List·Set·Sorted Set·TTL
1. 学习目标
完成本知识点后,你应该能够:
- 理解 Redis 作为内存数据库的定位与特点
- 熟练使用六种核心数据结构:String、Hash、List、Set、Sorted Set
- 使用 TTL 设置键过期时间
- 用 go-redis 库在 Go 中操作 Redis
- 为数字孪生场景选择合适的 Redis 数据类型
2. 为什么需要
PostgreSQL 擅长持久化,但高频读写和低延迟场景下,每次都查数据库会成为瓶颈:
- AGV 实时位置:每秒更新,前端轮询或 WebSocket 推送
- 设备在线状态:频繁变更,需要毫秒级响应
- 排行榜:按速度/任务数排序
Redis 是内存键值数据库,读写延迟通常在亚毫秒级,支持丰富的数据结构。它通常不作为唯一数据源,而是作为 PostgreSQL 的缓存层或实时状态存储。
3. 核心概念
3.1 Redis 基础
| 特性 | 说明 |
|---|---|
| 内存存储 | 数据主要在 RAM,极快 |
| 单线程模型 | 命令原子执行,无锁竞争 |
| 持久化 | RDB 快照 + AOF 日志(可选) |
| 数据结构 | String / Hash / List / Set / Sorted Set 等 |
| 过期 | 任意 key 可设 TTL 自动删除 |
键名命名建议:业务:实体:ID:字段,如 device:AGV-001:status。
3.2 String(字符串)
最基础类型,可存字符串、整数、浮点数:
| 命令 | 作用 |
|---|---|
SET key value | 设置值 |
GET key | 获取值 |
INCR key | 整数 +1 |
SETEX key seconds value | 设置值并指定 TTL |
用途:简单缓存、计数器、分布式锁。
3.3 Hash(哈希表)
类似小型 JSON 对象,适合存储对象的多个字段:
| 命令 | 作用 |
|---|---|
HSET key field value | 设置字段 |
HGET key field | 获取字段 |
HGETALL key | 获取所有字段 |
HMSET key f1 v1 f2 v2 | 批量设置 |
用途:AGV 状态(x, y, speed, battery 存在一个 Hash 里)。
3.4 List(列表)
有序字符串列表,双端操作:
| 命令 | 作用 |
|---|---|
LPUSH key value | 左侧插入 |
RPUSH key value | 右侧插入 |
LPOP key | 左侧弹出 |
LRANGE key 0 -1 | 获取范围 |
用途:最近 N 条消息、任务队列。
3.5 Set(集合)
无序、不重复字符串集合:
| 命令 | 作用 |
|---|---|
SADD key member | 添加成员 |
SREM key member | 移除成员 |
SISMEMBER key member | 是否包含 |
SMEMBERS key | 所有成员 |
用途:在线设备集合、标签。
3.6 Sorted Set(有序集合)
每个成员关联一个 score,按 score 排序:
| 命令 | 作用 |
|---|---|
ZADD key score member | 添加/更新 |
ZRANGE key 0 -1 WITHSCORES | 按 score 升序 |
ZREVRANGE key 0 9 WITHSCORES | 按 score 降序 Top 10 |
ZRANK key member | 获取排名 |
用途:排行榜、优先级队列、按时间戳排序的事件。
3.7 TTL(Time To Live)
| 命令 | 作用 |
|---|---|
EXPIRE key seconds | 设置过期秒数 |
TTL key | 查看剩余秒数(-1 永不过期,-2 不存在) |
SET key value EX 3600 | 创建时设 TTL |
用途:缓存自动失效、临时 Session、心跳超时检测。
4. 基础语法
4.1 项目初始化
bash
mkdir -p workspace/phase-05/redis-demo
cd workspace/phase-05/redis-demo
go mod init redis-demo
go get github.com/redis/go-redis/v94.2 连接 Redis
go
package main
import (
"context"
"fmt"
"log"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
defer rdb.Close()
if err := rdb.Ping(ctx).Err(); err != nil {
log.Fatal("redis unreachable:", err)
}
fmt.Println("connected to Redis")
}4.3 String 操作
go
// 缓存设备配置 JSON
err := rdb.Set(ctx, "device:AGV-001:config", `{"max_speed":2.0}`, 0).Err()
val, err := rdb.Get(ctx, "device:AGV-001:config").Result()
// val = `{"max_speed":2.0}`
// 计数器:今日任务数
count, err := rdb.Incr(ctx, "stats:tasks:today").Result()4.4 Hash 操作 — AGV 状态
go
key := "device:AGV-001:state"
err := rdb.HSet(ctx, key, map[string]interface{}{
"x": 12.5,
"y": 3.8,
"speed": 1.2,
"battery": 85,
"status": "moving",
}).Err()
x, err := rdb.HGet(ctx, key, "x").Float64()
state, err := rdb.HGetAll(ctx, key).Result()
// state = map[string]string{"x":"12.5", "y":"3.8", ...}4.5 List 操作 — 最近告警
go
alertKey := "device:AGV-001:recent_alerts"
rdb.LPush(ctx, alertKey, "low battery at 14:30", "obstacle detected at 14:25")
rdb.LTrim(ctx, alertKey, 0, 9) // 只保留最近 10 条
alerts, err := rdb.LRange(ctx, alertKey, 0, -1).Result()4.6 Set 操作 — 在线设备
go
onlineKey := "devices:online"
rdb.SAdd(ctx, onlineKey, "AGV-001", "AGV-002")
rdb.SRem(ctx, onlineKey, "AGV-002")
isOnline, err := rdb.SIsMember(ctx, onlineKey, "AGV-001").Result()
online, err := rdb.SMembers(ctx, onlineKey).Result()4.7 Sorted Set — 速度排行榜
go
leaderboard := "agv:speed:ranking"
rdb.ZAdd(ctx, leaderboard, redis.Z{Score: 1.8, Member: "AGV-001"})
rdb.ZAdd(ctx, leaderboard, redis.Z{Score: 2.1, Member: "AGV-003"})
rdb.ZAdd(ctx, leaderboard, redis.Z{Score: 1.5, Member: "AGV-002"})
// Top 3 速度最快
top, err := rdb.ZRevRangeWithScores(ctx, leaderboard, 0, 2).Result()4.8 TTL 操作
go
// AGV 状态 60 秒无更新则过期(心跳检测)
rdb.HSet(ctx, "device:AGV-001:state", "status", "moving")
rdb.Expire(ctx, "device:AGV-001:state", 60*time.Second)
ttl, err := rdb.TTL(ctx, "device:AGV-001:state").Result()
// ttl = 剩余时间
// 创建时直接设 TTL
rdb.Set(ctx, "cache:device:AGV-001", jsonData, 5*time.Minute)5. 代码解析
go
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})go-redis v9 所有命令第一个参数是 context.Context,支持超时和取消。
go
rdb.HSet(ctx, key, map[string]interface{}{"x": 12.5, ...})Hash 适合部分更新——只改 battery 而不重写整个 JSON。比 String 存 JSON 更高效。
go
rdb.LTrim(ctx, alertKey, 0, 9)List 用作固定长度队列:新告警 LPUSH,LTRIM 保留最新 N 条,避免无限增长。
go
rdb.ZRevRangeWithScores(ctx, leaderboard, 0, 2)Sorted Set 的 score 可以是速度、时间戳、优先级。ZRevRange 取 score 最高的成员。
go
rdb.Set(ctx, key, value, 5*time.Minute)go-redis 的 Set 第四个参数是 expiration,0 表示永不过期。
6. JavaScript / TypeScript 对比
| 概念 | Redis (go-redis) | 前端 |
|---|---|---|
| 存储位置 | 服务端内存 | 无直接等价(内存变量重启丢失) |
| 数据结构 | 丰富(Hash/Set/ZSet) | Object / Array / Map / Set |
| 持久化 | 可选 RDB/AOF | localStorage(有限,字符串) |
| 过期 | 原生 TTL | 需手动 setTimeout 清理 |
| 并发 | 服务端单线程原子 | 单线程(浏览器) |
关键差异:
- Redis 是独立服务,多实例共享,前端无法直接访问(通过 API / WebSocket)
- Redis 命令原子,适合计数器、排行榜,无需应用层加锁
- Hash 类似 JS Object,但值都是 string(go-redis 提供类型转换方法)
7. 常见错误
错误 1:用 String 存大 JSON 且频繁部分更新
go
// ❌ 每次改 battery 都要读写整个 JSON
data, _ := rdb.Get(ctx, key).Result()
// 解析 JSON → 改字段 → 序列化 → Set
// ✅ 用 Hash 只更新需要的字段
rdb.HSet(ctx, key, "battery", 80)错误 2:键名无命名规范
go
// ❌ 难以管理和批量操作
rdb.Set(ctx, "AGV-001", data, 0)
// ✅ 层级命名
rdb.HSet(ctx, "device:AGV-001:state", ...)错误 3:忘记设置 TTL 导致内存泄漏
go
// ❌ 临时缓存永不过期
rdb.Set(ctx, "cache:report:"+id, data, 0)
// ✅ 设置合理 TTL
rdb.Set(ctx, "cache:report:"+id, data, 10*time.Minute)错误 4:List 不做长度限制
go
// ❌ 无限 LPUSH,内存爆炸
rdb.LPush(ctx, "events", event)
// ✅ LPUSH + LTRIM
rdb.LPush(ctx, "events", event)
rdb.LTrim(ctx, "events", 0, 999)错误 5:不处理 redis.Nil
go
val, err := rdb.Get(ctx, key).Result()
if err != nil {
return err // ❌ key 不存在时 err == redis.Nil,不一定是错误
}
// ✅
val, err := rdb.Get(ctx, key).Result()
if err == redis.Nil {
// key 不存在
} else if err != nil {
return err
}8. 实际应用
数字孪生 — AGV 状态缓存
AGV 上报位置
↓
Go Handler
↓
Redis HSET device:AGV-001:state (x, y, speed, battery) ← 毫秒级,供实时查询
↓
异步写入 PostgreSQL telemetry 表 ← 持久化历史
↓
WebSocket 推送给 Three.js 前端数据类型选择:
| 场景 | Redis 类型 | 键示例 |
|---|---|---|
| AGV 实时状态 | Hash | device:AGV-001:state |
| 在线设备列表 | Set | devices:online |
| 最近 10 条告警 | List | device:AGV-001:alerts |
| 速度排行榜 | Sorted Set | agv:speed:ranking |
| 设备配置缓存 | String (JSON) | cache:device:AGV-001:config |
| 心跳超时 | Hash + TTL | device:AGV-001:state EX 60 |
9. 深入理解
9.1 Redis 与 PostgreSQL 分工
| 维度 | Redis | PostgreSQL |
|---|---|---|
| 速度 | 极快(内存) | 较快(磁盘 + 缓存) |
| 容量 | 受 RAM 限制 | TB 级 |
| 查询 | 键值 + 简单结构操作 | 复杂 SQL / JOIN |
| 持久性 | 可选,非主要场景 | 强持久 |
| 典型角色 | 缓存、实时状态、队列 | 权威数据源 |
9.2 内存估算
1000 台 AGV,每台 Hash 10 字段,约 1KB → 总共 ~1MB。Redis 内存通常不是数字孪生项目的首要瓶颈,但无 TTL 的缓存键会累积。
9.3 原子性
Redis 单命令原子(如 INCR、HSET)。多命令原子性用 MULTI/EXEC 事务或 Lua 脚本。
9.4 Docker 快速启动
bash
docker run -d --name redis-digital-twin -p 6379:6379 redis:710. 练习
请独立完成,不要查看答案。代码放在
workspace/phase-05/redis-demo/。
Level 1 — 基础
练习 1.1:连接 Redis,Ping 成功后打印 "connected"。
练习 1.2:用 String 缓存 "device:AGV-001:name" = "Alpha AGV",读取并打印。
练习 1.3:用 Hash 存储 AGV 状态(x, y, speed, battery, status),读取 battery 字段。
练习 1.4:为 Hash 键设置 30 秒 TTL,验证 TTL 命令返回值。
Level 2 — 应用
练习 2.1:用 Set 维护在线设备,实现「设备上线」「设备下线」「查询是否在线」三个函数。
练习 2.2:用 List 实现最近 5 条告警记录(LPUSH + LTRIM)。
练习 2.3:用 Sorted Set 实现 AGV 速度排行榜,返回 Top 3。
练习 2.4:正确处理 redis.Nil:查询不存在的 key 时不 panic。
Level 3 — 综合
练习 3.1:编写 UpdateAGVState(ctx, id, x, y, speed, battery) 函数,更新 Hash 并重置 60 秒 TTL。
练习 3.2:编写 GetAllOnlineAGVStates(ctx) 函数:从 Set 取在线 ID,批量 HGETALL 各设备状态。
练习 3.3:对比 String 存 JSON 与 Hash 存字段:分别实现更新 battery 的操作,体会差异。
Level 4 — 项目实践
练习 4.1:搭建 redis-demo 项目:封装 DeviceStateCache 结构体,提供状态读写、在线管理、排行榜、告警列表四个功能,编写 main.go 演示完整流程。
11. 学习检查
完成练习后,确认你能回答:
- Redis 为什么适合存 AGV 实时状态而非 PostgreSQL?
- Hash 和 String(存 JSON)各适合什么场景?
- Set 和 Sorted Set 的区别是什么?
- TTL 为 -1 和 -2 分别表示什么?
- 如何用 Redis 实现「60 秒无心跳则视为离线」?
redis.Nil应该如何处理?
12. 下一步
| 已完成 | 下一知识点 | 关系 |
|---|---|---|
| Redis 基础, String, Hash, List, Set, ZSet, TTL | Cache | 缓存模式与一致性 |
| Session | 用户会话存储 | |
| Pub/Sub | 实时消息广播 |
下一文档将讲解 Cache-Aside 模式、Session 管理和 Pub/Sub 在数字孪生实时推送中的应用。
前置知识提醒
- 已完成 Phase 04(PostgreSQL / database/sql),理解 Redis 作为补充而非替代
- 本地或 Docker 中已安装 Redis
- 了解 Go context 和 error 处理(Phase 02)
学习导航
上一篇:Repository 与 ORM · 对应练习 · 下一篇:Redis 缓存、Session 与 Pub/Sub