跳转到内容
seren-spark

03 核心数据类型与选型

根据操作选结构,而不是背五种类型

Section titled “根据操作选结构,而不是背五种类型”

学习目标 · 能从“我要做什么操作”反推数据结构,读懂复杂度,并控制一次命令处理的数据量。

Redis 的 Key 通常用字符串命名,Value 才有 String、Hash 等类型。这里的 Hash 不是数据库表;它是一个 Key 下的字段集合。复杂度中的 N 是集合元素数,M 是本次返回或处理的元素数。O(1) 不是“无论多少字节都一样快”:大 Value 的网络传输、内存分配和反序列化仍然很贵。

类型前端类比核心操作与常见复杂度场景
String字符串 / 序列化 JSONGET、SET、INCR 通常 O(1);MGET 为 O(N)整体对象缓存、计数、短期令牌
Hash扁平对象单字段 HGET/HSET 平均 O(1);HGETALL 为 O(N)局部字段读写、统计字段
List两端可操作的序列单元素 LPUSH/RPOP O(1);LRANGE 与遍历、返回量有关最近访问记录、简单工作队列
SetJS 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:整块替换还是局部更新”
SET learn:product:101:json '{"id":101,"name":"Keyboard","price":29900}' EX 300
GET learn:product:101:json
HSET learn:user:7 name Alice level 1
HGET learn:user:7 name
HINCRBY learn:user:7 level 1
HMGET learn:user:7 name level
EXPIRE learn:user:7 600

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:顺序和去重是两件事”
LPUSH learn:recent:7 product-101 product-102
LRANGE learn:recent:7 0 9
LTRIM learn:recent:7 0 99
SADD learn:post:88:likes user-7
SADD learn:post:88:likes user-7
SISMEMBER learn:post:88:likes user-7
SCARD learn:post:88:likes
SREM learn:post:88:likes user-7

在新 Key 上,List 返回顺序是 product-102、product-101,因为 LPUSH 从左边依次放入。LTRIM 保留最近 100 条,索引从 0 开始、结束位置包含在内。Set 第一次 SADD 返回 1,重复添加返回 0;SCARD 是成员数量,不需要把所有成员取回来数。

LPUSH + RPOP 可以表示先进先出,但取出后消费者崩溃会丢任务。需要可靠处理时,考虑有待处理列表与确认机制的 Stream,或者符合业务需求的专用消息队列。Pub/Sub 没有离线补收保证,不能代替可靠任务队列。

ZADD learn:rank:weekly 100 user-7 90 user-8
ZINCRBY learn:rank:weekly 15 user-8
ZRANGE learn:rank:weekly 0 9 REV WITHSCORES
ZREVRANK learn:rank:weekly user-8
ZSCORE learn:rank:weekly user-8

新 Key 下 user-8 变成 105 分,降序排名第一;ZREVRANK 返回 0,展示给用户时通常加 1。相同分数依成员字典序排序,REV 时反向,不能自动保证“先到先得”。分数是双精度浮点数;不要把毫秒时间戳和超长业务 ID 随意拼成一个巨大整数作为 score。

3.5 进阶结构:知道适用条件即可

Section titled “3.5 进阶结构:知道适用条件即可”
结构最小示例(redis-cli)用它解决什么不能忽略什么
BitmapSETBIT learn:sign:7:202609 20 1用 bit 表示当月第 21 天签到;BITCOUNT 计天数offset 从 0 开始;极稀疏大 ID 会撑大底层 String
HyperLogLogPFADD learn:uv:day user-7 user-8;PFCOUNT learn:uv:day近似去重计数,如 UV有误差,不能列成员,也不能判某人是否出现过
GEOGEOADD learn:stores 116.40 39.90 store-1保存经纬度并用 GEOSEARCH 找附近不是完整 GIS,不处理路网距离
StreamXADD learn:events * type visit user 7带 ID 的追加记录、消费者组必须设计确认、重试、积压与裁剪

Stream 的最小消费过程如下。只在新练习 Key 中执行,返回的消息 ID 用于下一条 XACK;0 表示消费组从历史起点开始,> 表示读取尚未投递给该组的新消息。

XADD learn:events * type visit user 7
XGROUP CREATE learn:events learners 0 MKSTREAM
XREADGROUP GROUP learners worker-1 COUNT 10 STREAMS learn:events >
XPENDING learn:events learners

处理成功后执行 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 为日序号减一,控制稀疏跨度。

依据:String · Hash · List · Set · ZSet · Stream