03 核心数据类型与选型
根据操作选结构,而不是背五种类型
Section titled “根据操作选结构,而不是背五种类型”学习目标 · 能从“我要做什么操作”反推数据结构,读懂复杂度,并控制一次命令处理的数据量。
3.1 先看操作,再决定结构
Section titled “3.1 先看操作,再决定结构”Redis 的 Key 通常用字符串命名,Value 才有 String、Hash 等类型。这里的 Hash 不是数据库表;它是一个 Key 下的字段集合。复杂度中的 N 是集合元素数,M 是本次返回或处理的元素数。O(1) 不是“无论多少字节都一样快”:大 Value 的网络传输、内存分配和反序列化仍然很贵。
| 类型 | 前端类比 | 核心操作与常见复杂度 | 场景 |
|---|---|---|---|
| String | 字符串 / 序列化 JSON | GET、SET、INCR 通常 O(1);MGET 为 O(N) | 整体对象缓存、计数、短期令牌 |
| Hash | 扁平对象 | 单字段 HGET/HSET 平均 O(1);HGETALL 为 O(N) | 局部字段读写、统计字段 |
| List | 两端可操作的序列 | 单元素 LPUSH/RPOP O(1);LRANGE 与遍历、返回量有关 | 最近访问记录、简单工作队列 |
| Set | JS Set | 单成员 SADD/SISMEMBER 平均 O(1);SMEMBERS O(N) | 去重、点赞成员、共同关注 |
| ZSet | 带分数的唯一成员集合 | 单成员 ZADD O(log N);按排名 ZRANGE 为 O(log N + M) | 排行榜、按时间排序的事件 |
3.2 String 与 Hash:整块替换还是局部更新
Section titled “3.2 String 与 Hash:整块替换还是局部更新”JSON 示例返回整段字符串,价格用分表示。Hash 的 HSET 首次创建两个字段返回 2;HGET 返回 Alice;HINCRBY 后 level 为 2。Hash 字段的值仍然是字节数据,嵌套对象需自行序列化。选择 JSON 适合整块读写;选择 Hash 适合经常单独改字段。不要同时维护两份同一对象,除非你能处理同步。
教程统一用整个 Key 的 TTL。Redis 7.4 起支持 Hash 字段过期相关命令,但旧服务端和客户端未必支持;不要把“Hash 永远不能字段过期”背成定律。
3.3 List 与 Set:顺序和去重是两件事
Section titled “3.3 List 与 Set:顺序和去重是两件事”在新 Key 上,List 返回顺序是 product-102、product-101,因为 LPUSH 从左边依次放入。LTRIM 保留最近 100 条,索引从 0 开始、结束位置包含在内。Set 第一次 SADD 返回 1,重复添加返回 0;SCARD 是成员数量,不需要把所有成员取回来数。
LPUSH + RPOP 可以表示先进先出,但取出后消费者崩溃会丢任务。需要可靠处理时,考虑有待处理列表与确认机制的 Stream,或者符合业务需求的专用消息队列。Pub/Sub 没有离线补收保证,不能代替可靠任务队列。
3.4 ZSet:成员唯一,分数可变
Section titled “3.4 ZSet:成员唯一,分数可变”新 Key 下 user-8 变成 105 分,降序排名第一;ZREVRANK 返回 0,展示给用户时通常加 1。相同分数依成员字典序排序,REV 时反向,不能自动保证“先到先得”。分数是双精度浮点数;不要把毫秒时间戳和超长业务 ID 随意拼成一个巨大整数作为 score。
3.5 进阶结构:知道适用条件即可
Section titled “3.5 进阶结构:知道适用条件即可”| 结构 | 最小示例(redis-cli) | 用它解决什么 | 不能忽略什么 |
|---|---|---|---|
| Bitmap | SETBIT learn:sign:7:202609 20 1 | 用 bit 表示当月第 21 天签到;BITCOUNT 计天数 | offset 从 0 开始;极稀疏大 ID 会撑大底层 String |
| HyperLogLog | PFADD learn:uv:day user-7 user-8;PFCOUNT learn:uv:day | 近似去重计数,如 UV | 有误差,不能列成员,也不能判某人是否出现过 |
| GEO | GEOADD learn:stores 116.40 39.90 store-1 | 保存经纬度并用 GEOSEARCH 找附近 | 不是完整 GIS,不处理路网距离 |
| Stream | XADD learn:events * type visit user 7 | 带 ID 的追加记录、消费者组 | 必须设计确认、重试、积压与裁剪 |
Stream 的最小消费过程如下。只在新练习 Key 中执行,返回的消息 ID 用于下一条 XACK;0 表示消费组从历史起点开始,> 表示读取尚未投递给该组的新消息。
处理成功后执行 XACK learn:events learners 实际消息ID。失败消息留在 Pending,需由原消费者重读或其他消费者按策略 XAUTOCLAIM 接管;业务可能重复执行,所以消费者仍需幂等。重复创建同名组会报 BUSYGROUP,这是组已存在,不要直接忽略所有异常。
- “拿全量再在 Java 里分页”:大集合的 HGETALL、SMEMBERS 和全量 LRANGE 会增加单次工作量和响应体积。
- “用了 Set 就能永久去重”:Key 过期、淘汰、故障丢失后去重状态也消失。
- “数据类型等于底层实现”:小 Hash 可能用紧凑编码,达到条件后再转换,后面会用 OBJECT ENCODING 观察。
给三名用户初始化 10、20、30 分;给第一名用户加 25 分,查询前两名和第二个用户的排名。再设计“用户这个月是否签到”的 Key,说明为什么不直接把全站 userId 当 offset。
自测与答案 · 点赞为什么优先 Set?排行榜为什么不用 List?
Set 的成员唯一,重复 SADD 不会重复点赞;SCARD 可直接计数。List 不按业务分数维护排序,更新积分后需要自行排序,ZSet 已提供这些操作。练习中的分数变为 35、20、30,前两名是用户一、用户三,用户二的降序排名是 2,界面显示第 3。签到可用每用户每月一个 Bitmap,offset 为日序号减一,控制稀疏跨度。