Files
zhuangang/topics/backend/redis/01-data-structures.md
T

103 lines
3.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 为什么快、有哪些数据结构和场景、过期和淘汰策略、持久化方式。