Skip to content

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/v9

4.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/AOFlocalStorage(有限,字符串)
过期原生 TTL需手动 setTimeout 清理
并发服务端单线程原子单线程(浏览器)

关键差异

  1. Redis 是独立服务,多实例共享,前端无法直接访问(通过 API / WebSocket)
  2. Redis 命令原子,适合计数器、排行榜,无需应用层加锁
  3. 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 实时状态Hashdevice:AGV-001:state
在线设备列表Setdevices:online
最近 10 条告警Listdevice:AGV-001:alerts
速度排行榜Sorted Setagv:speed:ranking
设备配置缓存String (JSON)cache:device:AGV-001:config
心跳超时Hash + TTLdevice:AGV-001:state EX 60

9. 深入理解

9.1 Redis 与 PostgreSQL 分工

维度RedisPostgreSQL
速度极快(内存)较快(磁盘 + 缓存)
容量受 RAM 限制TB 级
查询键值 + 简单结构操作复杂 SQL / JOIN
持久性可选,非主要场景强持久
典型角色缓存、实时状态、队列权威数据源

9.2 内存估算

1000 台 AGV,每台 Hash 10 字段,约 1KB → 总共 ~1MB。Redis 内存通常不是数字孪生项目的首要瓶颈,但无 TTL 的缓存键会累积。

9.3 原子性

Redis 单命令原子(如 INCRHSET)。多命令原子性用 MULTI/EXEC 事务或 Lua 脚本。

9.4 Docker 快速启动

bash
docker run -d --name redis-digital-twin -p 6379:6379 redis:7

10. 练习

请独立完成,不要查看答案。代码放在 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. 学习检查

完成练习后,确认你能回答:

  1. Redis 为什么适合存 AGV 实时状态而非 PostgreSQL?
  2. Hash 和 String(存 JSON)各适合什么场景?
  3. Set 和 Sorted Set 的区别是什么?
  4. TTL 为 -1 和 -2 分别表示什么?
  5. 如何用 Redis 实现「60 秒无心跳则视为离线」?
  6. redis.Nil 应该如何处理?

12. 下一步

已完成下一知识点关系
Redis 基础, String, Hash, List, Set, ZSet, TTLCache缓存模式与一致性
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