--- id: backend.redis.data-structures title: 数据结构和常用命令:String、Hash、List、Set、ZSet、Stream type: lesson domain: backend stack: [redis] tags: [String, Hash, ZSet, Stream, 过期时间] level: basic est_hours: 2 related: [backend.redis.cache-patterns, backend.redis.interview] week: [3] order: 1 --- # 数据结构和常用命令:String、Hash、List、Set、ZSet、Stream ## 解释说明 | 类型 | 是什么 | 常见用途 | 常用命令 | | --- | --- | --- | --- | | String | 字符串 / 数字 | 缓存 JSON、计数器、验证码 | `SET k v EX 60 NX`、`GET`、`INCR`、`INCRBY` | | Hash | 字段 → 值 | 对象(用户资料、购物车) | `HSET`、`HGET`、`HGETALL`、`HINCRBY` | | List | 双端链表 | 最新列表、简单队列 | `LPUSH`、`RPOP`、`LRANGE`、`BRPOP` | | Set | 无序去重集合 | 标签、点赞用户、共同好友 | `SADD`、`SISMEMBER`、`SINTER` | | ZSet | 按分数排序的集合 | 排行榜、延迟队列(分数=时间) | `ZADD`、`ZINCRBY`、`ZREVRANGE`、`ZRANGEBYSCORE` | | Stream | 追加式日志,支持消费组 | 轻量消息队列 | `XADD`、`XREADGROUP`、`XACK` | 还有 Bitmap(签到)、HyperLogLog(UV 近似计数)、Geo(附近的人)。 **关键概念** - **单线程执行命令**:每条命令是原子的;多条命令要原子执行用 Lua 脚本或 `MULTI/EXEC`。 - **过期时间**:`EX` 秒 / `PX` 毫秒;`TTL` 查看剩余时间。过期删除 = 惰性删除(访问时检查)+ 定期抽样删除。 - **内存淘汰策略**:内存满时按 `maxmemory-policy` 淘汰,缓存场景常用 `allkeys-lru` 或 `allkeys-lfu`。 - **持久化**:RDB(定时快照)和 AOF(追加命令日志),可以同时开启。 ## 案例 - 文章阅读数:`INCR article:42:views`,定时批量写回数据库。 - 积分排行榜:`ZINCRBY rank:2026-10 10 user:7`,`ZREVRANGE rank:2026-10 0 9 WITHSCORES` 取前 10。 - 登录验证码:`SET sms:13800000000 123456 EX 300 NX`,NX 保证 5 分钟内不重复发送。 ## 代码示例 ```go package main import ( "context" "fmt" "os" "github.com/redis/go-redis/v9" ) func main() { url := os.Getenv("REDIS_URL") // 例如 redis://localhost:6379/0 if url == "" { fmt.Println("未设置 REDIS_URL,跳过") return } opt, err := redis.ParseURL(url) if err != nil { panic(err) } rdb := redis.NewClient(opt) defer rdb.Close() ctx := context.Background() // 计数器 n, _ := rdb.Incr(ctx, "demo:views:42").Result() fmt.Println("阅读数", n) // 排行榜 rdb.ZIncrBy(ctx, "demo:rank", 30, "alice") rdb.ZIncrBy(ctx, "demo:rank", 50, "bob") rdb.ZIncrBy(ctx, "demo:rank", 10, "alice") top, _ := rdb.ZRevRangeWithScores(ctx, "demo:rank", 0, 9).Result() for i, z := range top { fmt.Printf("第 %d 名 %v %.0f 分\n", i+1, z.Member, z.Score) } // 对象用 Hash rdb.HSet(ctx, "demo:user:7", map[string]any{"name": "张三", "level": 3}) u, _ := rdb.HGetAll(ctx, "demo:user:7").Result() fmt.Println(u["name"], u["level"]) rdb.Del(ctx, "demo:views:42", "demo:rank", "demo:user:7") } ``` ## 易错点 - **大 key**:一个 Hash / ZSet 里放几百万元素,`HGETALL`、`DEL` 会阻塞 Redis。拆分 key,删除用 `UNLINK`。 - **`KEYS *`**:线上会阻塞,用 `SCAN` 分批遍历。 - **忘记设过期时间**:缓存类 key 一律带 TTL,否则内存只增不减。 - **用 List 做可靠队列**:`RPOP` 后消费者崩溃,消息就丢了;需要可靠性用 Stream(消费组 + ACK)或专业 MQ。 - **把 Redis 当主存储**:持久化有丢数据窗口,重要数据以数据库为准。 ## 面试常问 见 [Redis 面试题](interview.md):Redis 为什么快、有哪些数据结构和场景、过期和淘汰策略、持久化方式。