Redis 7 天学习笔记 · Day 2主题五种数据结构——每种只学“什么时候非它不可”环境Ubuntu 虚拟机 Python 3 Redis一句话总结数据结构选错代码会变丑选对一行命令解决的事不用写十行。一、今日核心认知Day 1 只用了 String把整个用户对象json.dumps塞进去。要改一个email字段你得读出整个 JSON → 反序列化 → 改字段 → 再序列化 → 写回去。今天认识四种结构目标只有一个拿到一个业务场景能在 10 秒内判断该用哪个并且能说清楚“为什么不是另一个”。先记住三条铁律String 是万能但“万能”往往意味着“不是最优”。结构化数据优先 Hash去重优先 Set排序优先 ZSet。每种结构都有它“会出事”的边界。Hash 字段过多、List 无限增长、Set 成员过多都会变成 BigKey。Python 返回值类型不同hgetall返回 dictsmembers返回 setzrange返回 list。二、五种数据结构速记与 Python 实操1. String最基础的万能型但别滥用结构特点一个 key 对应一个 value。value 可以是字符串、数字、二进制。数字支持原子增减。一句话定位计数、缓存单个值、验证码。结构化数据不要用它。import redis r redis.Redis(host127.0.0.1, port6379, db0, decode_responsesTrue) # String 核心操作 # 存一个键值对。set 会覆盖旧值。 r.set(article:42:title, Redis入门) # 取一个键的值。key 不存在时返回 None不是报错。 title r.get(article:42:title) print(title) # Redis入门 # setex set expire存的同时设过期时间秒。 # 用于验证码、Token 这类“过一段时间就该消失”的数据。 r.setex(verify:code:13800138000, 300, 123456) # 5分钟后自动消失 # setnx set if not existskey 不存在才写入。 # 返回 True 表示写入成功False 表示 key 已存在、本次没写。 # 分布式锁的雏形就是这个命令。 locked r.setnx(lock:order:1001, 1) print(locked) # True第一次 locked2 r.setnx(lock:order:1001, 1) print(locked2) # False已存在没写进去 # incr 是原子加 1。多个请求同时 incr不会丢计数。 # 用于阅读量、点赞数、库存扣减。 r.set(article:42:views, 100) new_views r.incr(article:42:views) # 返回 101 print(new_views) # mset / mget 批量操作减少网络往返次数。 # 一次 mget 拿多个 key比循环 get 快一个数量级。 r.mset({user:1:name: 张三, user:1:age: 25}) values r.mget([user:1:name, user:1:age]) print(values) # [张三, 25]什么时候不该用要存一个有多个字段的对象用户、商品用Hash。每次改一个字段都要读出整个 JSON既浪费网络又容易并发覆盖。2. Hash存对象改一个字段不用动全身结构特点一个 key 下面是一张 field-value 映射表相当于“key 嵌套了一个字典”。Redis 7 中小的 Hash 用 ListPack 紧凑存储省内存字段多了会转成 HashTable。一句话定位用户信息、商品属性、配置中心。结构化对象的首选。# Hash 核心操作 # hset 设置一个字段。 # 返回 1 表示新建了字段0 表示字段已存在、只是更新了值。 r.hset(user:1001, name, 张三) # hset 也可以批量设置多个字段用 mapping 参数。 r.hset(user:1001, mapping{ age: 25, email: zhangsanexample.com, city: 北京 }) # hget 只取一个字段。 # 改 email 只需要这一次 hset不用读整个对象再写回去。 r.hset(user:1001, email, newexample.com) print(r.hget(user:1001, email)) # newexample.com # hgetall 拿全部字段返回的是 Python dict。 # ⚠️ 字段数过万时不要用会阻塞 Redis。大 Hash 用 hscan 分批遍历。 user r.hgetall(user:1001) print(user) # {name: 张三, age: 25, email: newexample.com, city: 北京} # hincrby 对某个字段做原子增减。 # 用于“用户积分 10”“库存 -1”这类场景。 r.hincrby(user:1001, age, 1) # age 从 25 变成 26 # hlen 统计字段数量。 print(r.hlen(user:1001)) # 4 # hdel 删除一个或多个字段。 r.hdel(user:1001, city) # hscan 分批遍历大 Hash避免 hgetall 一次性拉全部。 # cursor 从 0 开始返回 0 表示遍历结束。 cursor 0 while True: cursor, data r.hscan(user:1001, cursorcursor, count10) print(data) if cursor 0: break坑点hgetall在字段数很大时会阻塞 Redis。日常用户对象只有几十个字段没问题但如果一个 Hash 有上万个字段就必须用 hscan。3. List有序可重复轻量队列结构特点双向链表两端增删都是 O(1)。可以当栈LIFO也可以当队列FIFO。一句话定位最新 N 条记录、简单消息队列、时间线。不要用它做“按索引随机查中间元素”的事lindex是 O(n)。# List 核心操作 # rpush 从右边插入。元素顺序a - b - c r.rpush(article:42:comments, 评论A, 评论B, 评论C) # lpush 从左边插入。lpush D 后列表变成 D, A, B, C r.lpush(article:42:comments, 评论D) # lrange 按索引范围取。 # 0 是第一个-1 是最后一个。lrange 0 -1 取全部。 comments r.lrange(article:42:comments, 0, -1) print(comments) # [评论D, 评论A, 评论B, 评论C] # 取最新 3 条从左边数前 3 个。 latest r.lrange(article:42:comments, 0, 2) print(latest) # [评论D, 评论A, 评论B] # lpop / rpop 从两端弹出并删除。 # r.lpop 取走最左边r.rpop 取走最右边。 r.lpop(article:42:comments) # 取走 评论D r.rpop(article:42:comments) # 取走 评论C # llen 获取列表长度。 print(r.llen(article:42:comments)) # ⚠️ lindex 是 O(n)。不要用 lindex 查一个大 List 的中间元素。 # 如果业务需要“按位置随机访问”说明 List 选错了。坑点List 没有“长度上限”。如果只做lpush不做ltrim这个 List 会无限增长变成 BigKey。用 List 存“最新 N 条”时一定配一个ltrim把范围外的裁掉# 只保留最新的 100 条超出范围的自动裁掉。 r.ltrim(article:42:comments, 0, 99)4. Set无序去重专治“不重复”和“交集”结构特点无序、元素唯一。支持交集、并集、差集。整数集合用 IntSet 紧凑存储元素多了或出现非整数后转 HashTable。一句话定位去重、共同好友、标签、抽奖。不需要排序时Set 比 ZSet 省内存。# Set 核心操作 # sadd 添加元素。自动去重重复的会被忽略。 # 返回实际新加入的元素个数。 r.sadd(user:1001:follows, 1002, 1003, 1004) r.sadd(user:1002:follows, 1001, 1003, 1005) # 重复添加 1002返回 0集合不变。 r.sadd(user:1001:follows, 1002) # 返回 0 # smembers 查看全部元素返回 Python set。 print(r.smembers(user:1001:follows)) # {1002, 1003, 1004} # sismember 判断元素是否存在返回 True / False。 # O(1)比用 List 遍历判断快得多。 print(r.sismember(user:1001:follows, 1002)) # True # sinter 交集两个用户共同关注了谁。 common r.sinter(user:1001:follows, user:1002:follows) print(common) # {1003} # sunion 并集合并去重。 union r.sunion(user:1001:follows, user:1002:follows) # sdiff 差集A 有但 B 没有的。 only_1001 r.sdiff(user:1001:follows, user:1002:follows) print(only_1001) # {1002, 1004} # srandmember 随机取 N 个不删除。抽奖“抽中但还能再抽”用这个。 winners r.srandmember(lottery, 2) # spop 随机弹出 1 个并删除。抽奖“抽走就没了”用这个。 winner r.spop(lottery)坑点smembers在大 Set 上同样会阻塞大 Set 用sscan分批遍历。5. ZSet带分数专为排序而生结构特点元素唯一每个元素带一个 score按 score 自动排序。底层是跳跃表 哈希表范围查询和排名都是 O(log N)。一句话定位排行榜、积分、延时队列。只要业务里有“排名”或“按时间排序”就是它。# ZSet 核心操作 # zadd 添加元素score 在前值在后。用 dict 一次传多个。 r.zadd(rank:game:2026, { 张三: 95, 李四: 87, 王五: 95, # score 可以重复但元素名不能重复 赵六: 72 }) # zrevrange 按 score 从高到低取。 # 0 到 2 取前三名。返回的是元素名列表。 top3 r.zrevrange(rank:game:2026, 0, 2) print(top3) # [张三, 王五, 李四]score 相同时按字典序 # 带分数一起取加上 withscoresTrue返回的是“元素-分数”列表。 top3_with_score r.zrevrange(rank:game:2026, 0, 2, withscoresTrue) print(top3_with_score) # [(张三, 95.0), (王五, 95.0), (李四, 87.0)] # zscore 查单个元素的分数。 print(r.zscore(rank:game:2026, 张三)) # 95.0 # zrevrank 查排名从高到低0 表示第一名。 print(r.zrevrank(rank:game:2026, 张三)) # 0 # zincrby 对某个元素的 score 原子增加。 # 用于“用户又得了 5 分”“视频播放量 1”且需要排序的场景。 r.zincrby(rank:game:2026, 5, 李四) # 李四 87 - 92 # zrangebyscore 按分数范围取。 # 取 90 分以上的所有人min 和 max 是分数不是排名。 high_scorers r.zrangebyscore(rank:game:2026, 90, 100) print(high_scorers) # [张三, 王五, 李四] # zcard 统计元素个数。 print(r.zcard(rank:game:2026)) # 4 # zrem 删除指定元素。 r.zrem(rank:game:2026, 赵六)ZSet 做延时队列的思路import time # score 存“任务应该执行的时间戳”。 # 消费端反复查 score ≤ 当前时间的任务取到就执行执行完删掉。 now int(time.time()) r.zadd(delay:queue, {task:1001: now 10}) # 10 秒后执行 ready r.zrangebyscore(delay:queue, 0, int(time.time())) if ready: task ready[0] # 执行任务... r.zrem(delay:queue, task) # 执行完删除坑点score 是浮点数。如果业务需要精确的整数排序比如订单号最好直接用整数作为 score避免浮点精度带来的意外并列。三、场景选型对照表场景选谁为什么不选另一个文章阅读量 1、验证码StringHash 做计数器是杀鸡用牛刀Set 不能存数字值存一个用户对象只改 emailHashString 存 JSON 要读出整个改完再写回改一个字段发一次 hset 就够了最新 20 条评论ListSet 无序ZSet 带分数但这里不需要排序两个人的共同关注SetList 不能做交集且允许重复ZSet 的 score 在这里没用游戏排行榜前 10ZSetSet 无序拿不到“前 10”List 无法按分数排序去重如抽奖名单SetList 允许重复Hash 的 field 虽然唯一但访问方式不对四、验证实验代码跑完后在redis-cli里逐个确认TYPE user:1001 # hash TYPE rank:game:2026 # zset TYPE article:42:comments # list TYPE user:1001:follows # set HGETALL user:1001 ZREVRANGE rank:game:2026 0 -1 WITHSCORES LRANGE article:42:comments 0 -1 SMEMBERS user:1001:follows再用OBJECT ENCODING看一眼底层编码的变化OBJECT ENCODING user:1001 # listpack字段少时或 hashtable OBJECT ENCODING rank:game:2026 # listpack元素少时或 skiplist五、今日踩坑 / 待确认问题原因处理hgetall返回的是 dict我当列表用了redis-py对 Hash 的返回值做了类型映射hgetall→ dicthmget→ listhget→ str/Nonesmembers返回set不能直接 JSON 序列化返回值是 Pythonset先list(r.smembers(...))再序列化ZSet 的 score 取出来是95.0不是95score 本质是浮点数需要整数时自己int()转换hgetall把 Redis 卡住了大 Hash 一次性拉全量字段改用hscan分批遍历List 越来越大内存持续上涨只lpush没有ltrim用ltrim限制保留范围六、交付物 Checklist截图 15 个结构各一段运行输出能看出hgetall返回 dict、smembers返回 set、zrevrange withscores返回列表一句话从对照表里挑一行解释“为什么不是另一个”