读完这篇,你能做什么
能为会话、点赞和排行榜选择结构,并用最小数据验证命令行为。
学习理由、准备要求与知识点
为什么学它
Redis 的数据类型直接决定可执行的操作、查询成本和后续缓存与秒杀设计。
先读这些内容
知识点清单
String- Hash
ListSet- Sorted
Set - Stream
- 成员唯一性
- 键命名与类型
运行前先确认Redis7.4、Java17;Jedis源码固定v5.2.0。示例沿用Redis环境篇的独立实例,宿主端口16379;命令进入redis-cli交互模式后执行。算法只接受ASCII成员与有限整数分数。
01先弄懂它是什么
选 Redis 类型时,先列出要做的操作。会话需要凭令牌读取少量用户信息,可以用 String 存序列化对象,也可以用 Hash 分字段存;点赞要去重并检查某人是否点过,Set 更直接;排行榜要改分数并按分数读取,Sorted Set 更合适。不要一开始就把整个 Java 对象塞进去,再想怎么查。
一个 Redis 键对应一种值类型,Hash 字段或 Set 成员并不是关系数据库里独立的一行。HGETALL 会取出整个 Hash,SMEMBERS 会取出整个 Set;内容太大时,服务端、网络和客户端内存都会增加负担。给键写清业务、对象编号和用途,也要约定最多放多少成员、字段多大,以及什么时候删除。
Redis 的 String 也能保存字节数据;使用整数命令时,存进去的内容还得符合命令的数字格式。Sorted Set 中每个成员只有一份,但不同成员可以同分,给已有成员改分数不会再新增一份。分数用双精度浮点数保存,能精确表示的整数范围有限;不能把任意长整数,或拼起来的时间和 ID,直接当分数并认定不会失真。同分成员按字节顺序排列,也不是自动按数字 ID 大小排列。
文中这几个词,先认一认
- Key类型
- 一个键当前保存的数据结构;拿 Hash 命令操作
String等类型不匹配时会报 WRONGTYPE。 - 成员唯一
Set和 SortedSet按成员本身去重;SortedSet可以有多个成员使用相同分数。- 操作驱动建模
- 先列查询、修改、去重、排序和数据量需求,再决定用什么结构保存。
02看看它怎么工作
先完成Redis起步篇,确认独立实例PING返回PONG。若使用已命名容器,docker exec -it qh-redis-study redis-cli可进入容器内客户端;宿主redis-cli则连接127.0.0.1:16379,实际容器名与端口以环境篇为准。进入后用TYPE检查键类型,避免把旧实验键当成新的结构直接操作。
会话示例可用HSET study:session:demo id 101 name learner,再HGET读取字段;这是无真实认证能力的结构练习,正式登录令牌必须安全生成。点赞用SADD study:likes:note:1 101与SISMEMBER观察重复成员。
排行榜用ZADD study:rank 10 alice 10 bob 20 carol,再ZRANGE study:rank 0 2 REV WITHSCORES观察分数与同分次序。
结构选择还要看一致性边界。给Set加成员和给另一Key计数是两次操作,不能因为都在Redis就自动原子一致。排行榜分数更新与持久化业务积分也可能跨系统。先选权威来源,再决定派生统计如何修复;结构提供的单命令原子性并不等于整个业务用例已完整保护。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["需要的操作"] --> B{"主要语义"}
B --> C["整值读取:String"]
B --> D["字段访问:Hash"]
B --> E["成员去重:Set"]
B --> F["分数与排名:Sorted Set"]
C --> G["容量与生命周期"]
D --> G
E --> G
F --> G
G --> H["跨键及权威数据一致性"]按顺序看,每一步做了什么
- 确认独立Redis环境和实际连接地址。
- 按用例列出读、写、去重和排序操作。
- 用小数据执行命令并查看TYPE及返回值。
- 设置键名、成员大小、容量和TTL规则。
- 检查跨键和跨数据库更新是否需要额外协议。
03对照实现代码
Jedis v5.2.0 · UnifiedJedis#hset、#zadd、#zrank
原创Java命令模型,非源码节选。真实类构建命令对象并交给执行器;本例只表现成员和分数是独立参数,不实现Redis服务端结构。
import java.util.*;
class SourceModel {
record Command(String name,List<String> args) {}
static Command zadd(String key,double score,String member) {
if (!Double.isFinite(score)) throw new IllegalArgumentException("finite score");
// 教学应用主动限制有限分数,真实Redis选项与范围需看命令规范。
List<String> args=List.of(key,Double.toString(score),member);
// 分数与成员分开编码,成员身份不能由分数代替。
return new Command("ZADD",args);
}
// 构建成功只形成请求,不表示服务端已经接收和执行。
}UnifiedJedis的hset、zadd等入口委派给commandObjects和executeCommand,数据结构实际由Redis服务端处理。
Java方法的返回类型反映命令结果,例如ZADD常返回新增成员计数;更新已有成员与新建成员不能混为一谈,选项会影响返回语义。
String与byte[]重载体现编码边界。Redis同分成员排序依据字节,不保证等于任意Java字符串比较器的字符顺序。
客户端调用方便不代表可以无界读取。HGETALL、成员全量列表或排名范围仍需应用控制结果规模与超时。
代码先调用谁,再调用谁
- 应用调用hset或zadd
commandObjects构建命令executeCommand选择执行器- 连接发送协议
- 读取Redis返回值
04动手写一个例子
双索引维护唯一成员排行榜
实现容量有限的单线程排行榜:成员名仅ASCII字母数字、下划线或横线,分数为±十亿内整数。更新分数不增加成员;按分数降序,同分按成员名降序,读取前K名。此限制让成员顺序与Redis的字节序一致。
- 输入 1
alice=10、bob=10、carol=20- 输出 1
carol、bob、alice
- 输入 2
alice更新为30- 输出 2
alice、carol、bob,成员仍3个
用Map按成员定位旧分数,用有序集合按分数和成员排序。两种访问需求对应两个索引,不能只用分数作集合比较,否则同分成员会被误判成同一项。
更新先移除旧的有序条目,再写新分数并加入新条目,保持两份索引一致。只有新成员占用新增容量,满容量时更新现有成员仍允许。
读取从降序比较器定义的集合头部迭代,最多取K项。成员仅ASCII使Java比较与Redis字节比较一致;一般Unicode成员需要按实际编码字节比较,不能无说明复用本题比较器。
为什么这样写是对的Map和有序集合成员完全一致,每个成员恰好对应一个当前分数;集合按分数和同分成员的双重顺序排列。
Map定位均摊常数,树集合删除与插入为对数;读取最多K项,保存两份线性索引。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.*;
public class Main {
record Entry(String member,long score) {}
static class Board {
final int capacity;
final Map<String,Long> scores=new HashMap<>();
final TreeSet<Entry> order=new TreeSet<>(
Comparator.comparingLong(Entry::score).reversed()
.thenComparing(Entry::member,Comparator.reverseOrder()));
Board(int capacity) {
if (capacity<1) throw new IllegalArgumentException("capacity");
this.capacity=capacity;
}
void put(String member,long score) {
if (member==null||!member.matches("[A-Za-z0-9_-]+")||
score < -1_000_000_000L||score > 1_000_000_000L)
throw new IllegalArgumentException("value");
Long old=scores.get(member);
if (old==null&&scores.size()==capacity) throw new IllegalStateException("full");
// 更新前移除旧排序项,防止同一成员出现两个分数。
if (old!=null) order.remove(new Entry(member,old));
scores.put(member,score);
order.add(new Entry(member,score));
}
void remove(String member) {
Long old=scores.remove(member);
// 删除同时维护两个索引;不存在成员不产生新条目。
if (old!=null) order.remove(new Entry(member,old));
}
List<String> top(int k) {
if (k<0) throw new IllegalArgumentException("k");
List<String> result=new ArrayList<>();
for (Entry e:order) {
if (result.size()==k) break;
result.add(e.member()); // 迭代顺序已经包含同分规则。
}
return List.copyOf(result);
}
}
static void eq(Object expected,Object actual) {
if (!Objects.equals(expected,actual)) throw new AssertionError(actual);
}
public static void main(String[] args) {
Board b=new Board(3);
b.put("alice",10); b.put("bob",10); b.put("carol",20);
eq(List.of("carol","bob","alice"),b.top(5));
b.put("alice",30);
eq(List.of("alice","carol","bob"),b.top(3)); eq(3,b.scores.size());
b.put("bob",10); eq(3,b.order.size());
try { b.put("dave",40); throw new AssertionError("capacity"); }
catch (IllegalStateException expected) {}
b.remove("carol"); eq(List.of("alice","bob"),b.top(3));
eq(List.of(),b.top(0));
try { b.put("中文",1); throw new AssertionError("encoding boundary"); }
catch (IllegalArgumentException expected) {}
System.out.println("PASS 8 leaderboard tests");
}
}这些特殊情况也要试一试
- 相同分数允许多个成员,比较器必须包含成员身份。
- 容量满时可更新旧成员,新增成员拒绝。
- K为零返回空;大于成员数返回全部,Unicode输入按边界拒绝。
进阶选读 · 跨分片全局排行榜与分数精度
场景多个分片各有排名,需要查询全局前K并处理高精度业务分数。
做法从每个分片读取局部前K,再按一致比较规则合并;高精度分数选择明确缩放、范围检查或独立精确存储,不能随意压入double。
复杂度P个分片候选最多PK项,堆合并约O(K log P),网络与快照成本另计。
好处与代价各分片读取时点不同可能导致非同一快照的全局排名,需声明一致性与更新可见性。
05项目里怎么用
键名也属于数据设计,宜包含业务前缀、对象编号和用途,别让同一键先后混用不同类型。为 Hash、Set 和排行限定数量,过大时采用分页或扫描。SCAN 一类扫描不是冻结快照,扫描期间数据变化可能影响结果,也要处理重复返回等规则。
会话Hash不要存完整用户实体和敏感字段,保留业务需要的最小DTO并设置生命周期。普通HSET修改字段通常保留Key现有TTL,但不会自动为新键提供过期;是否使用字段过期等新能力要与明确Redis版本和命令支持对应。
点赞数与成员列表要说明以哪份数据为准。若计数由列表推导,就应能按列表或数据库重建;若积分由数据库决定,Redis 排行只是方便查询的副本,更新失败后要能重放或重建。某条命令执行很快,不等于多份数据已经保持一致。
容易踩的坑
- 分数作唯一身份导致同分成员丢失。
- 把长整数直接转
double并假定任意值精确。 - 无界使用HGETALL或SMEMBERS读取巨大结构。
- 两个Key分别更新却宣称整组操作原子一致。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
String序列化对象 | 整值读写直观。 | 局部更新需重写且可能冲突。 | 整体读取的小快照。 |
| Hash字段结构 | 按字段读取与修改。 | 要维护字段类型和生命周期。 | 小型对象属性。 |
Sorted Set | 成员唯一并支持分数排序。 | 需要分数精度与容量约束。 | 榜单和有序成员查询。 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01Sorted Set成员能同分吗?
展开参考答案与推导
能,成员唯一而分数可以相同,同分再按成员字节顺序排列。
02HSET会自动创建登录TTL吗?
展开参考答案与推导
不会自动给新Key设置预期有效期,应明确过期操作。
03Key类型选定后就解决跨库一致性了吗?
展开参考答案与推导
没有,数据结构只提供相应单命令能力,跨系统仍需协议。
换个情况,还会处理吗
01同分排行只剩一个人怎么办?
展开参考答案与推导
检查自写比较器是否只比较分数,需要加入唯一成员次序。
02大Hash查询引起延迟怎么办?
展开参考答案与推导
检查完整读取规模,按字段读取或分批处理,并限制结构容量。
03积分落库成功而排行更新失败怎么办?
展开参考答案与推导
以权威积分为准,可靠重放更新或重建排行,不直接改写已确认业务结果。
写代码试一试
01怎样验证改分不增加成员?
- 输入 / 条件
alice10改为30- 输出 / 预期
成员数不变,顺序改变
提示 删除旧有序条目。
展开参考答案与推导
同时断言Map与TreeSet大小以及最终排名。
02满容量时旧成员更新能通过吗?
- 输入 / 条件
容量3已满,更新alice- 输出 / 预期
允许
提示 区分新增和更新。
展开参考答案与推导
只有old为空的新成员才检查额外容量。
03支持任意Unicode成员需改哪里?
- 输入 / 条件
含补充字符与普通汉字- 输出 / 预期
按Redis实际字节顺序
提示 UTF-16比较不一定等于UTF-8字节序。
展开参考答案与推导
按明确编码生成字节并做无符号字节字典序比较,增加边界用例。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能为会话、点赞和排行榜选择结构,并用最小数据验证命令行为。
本篇重点加练
事件流与成员唯一性分别表达变化和当前身份,需要防止把结构特性误当作业务去重。
- Stream
- 成员唯一性
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
用户资料包含昵称、版本号与头像,另有一次性下载凭证。为两类数据比较
String与 Hash 表示的读写原子粒度和更新代价。- 提交内容
- 模型对照表
- 怎样算完成
- 列出实际读取和更新操作
- 说明缺字段与空值口径
练到的知识点String · Hash
- 作业 02代码推演
任务候选人需要保持加入顺序,而活动报名需要判断某人是否已参加。构造重复加入、退出和再次加入的序列,比较
List与Set的状态。- 提交内容
- 操作序列和状态表
- 怎样算完成
- 区分顺序与成员唯一性
- 包含空结构行为
练到的知识点List · Set
- 作业 03面试场景
两个模块都使用 user:42 键,一个写 Hash,另一个写
String。设计键命名登记表和类型检查,说明版本升级时如何识别旧格式。- 提交内容
- 命名规范与冲突用例
- 怎样算完成
- 包含环境及用途边界
- 成员身份规则明确
练到的知识点成员唯一性 · 键命名与类型
- 作业 04原理问答
同一商品以两个别名进入集合,字符串不同但业务上是同一商品。设计成员规范化契约,说明结构去重与实体去重的不同证据。
- 提交内容
- 成员身份及规范化规则
- 怎样算完成
- 覆盖别名迁移和大小写
- 规范化冲突有处理口径
练到的知识点成员唯一性
综合应用
- 作业 05设计评审
排行榜要求按分数查询前十,审计日志要求保存每次积分变动供不同处理者读取。评审 Sorted
Set和 Stream 分别适合保存什么事实。- 提交内容
- 数据流与字段契约
- 怎样算完成
- 区分当前分数和变动事件
- 覆盖重复事件身份
练到的知识点Sorted Set · Stream
- 作业 06编程实践
在本地 Redis 建立小型读书活动数据:资料、待处理任务、报名、积分榜和审计流;为每类操作准备重复及非法输入。
- 提交内容
- 命令脚本与结构选择说明
- 怎样算完成
- 实测各键类型
- 注明跨结构操作是否有共同原子边界
练到的知识点String · Hash · List · Set · Sorted Set · Stream · 成员唯一性 · 键命名与类型
- 作业 07设计评审
积分事件进入 Stream 后需要支持审计与投影重建,但运营只想保留最新分数。评审裁剪事件与保存状态各能支持的后续查询。
- 提交内容
- 查询能力与保留矩阵
- 怎样算完成
- 包含删除及更正事件
- 区分当前状态和完整变化过程
练到的知识点Stream
- 作业 08编程实践
用本地 Redis 或明确标注的 Java 模型实现别名商品的事件导入,分别记录事件身份和实体身份,测试别名变更后重放的效果。
- 提交内容
- 代码和身份对账报告
- 怎样算完成
- 输入规模受限
- 模拟时说明不覆盖真实 Stream 投递协议
练到的知识点Stream · 成员唯一性
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
一次业务升级将积分规则从整数改为含小数,历史审计必须保留。制定数据格式版本与迁移校验方案。
- 提交内容
- 迁移计划及对账口径
- 怎样算完成
- 覆盖旧新客户端并存
- 不丢失成员身份和历史变动来源
练到的知识点Hash · Sorted Set · Stream · 成员唯一性 · 键命名与类型
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 跨域迁移
把 Redis 成员建模迁移到多来源文档库,相同内容可能有不同 URL,URL 内容也会变化。如何重新定义事件、文档和版本的唯一性?
- 提交内容
- 跨域身份模型
- 怎样算完成
- 包含内容变化与别名
- 避免用单一字符串承担全部身份
练到的知识点Stream · 成员唯一性
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读教程英文
Redis:数据类型 (新标签页)↗
按业务操作比较
String、Hash、Set、SortedSet与 Stream,避免只按名称选型。 - 实践教程英文
Redis:Hashes (新标签页)↗
阅读字段读写与对象建模,确定整体 TTL 和部分更新的设计边界。
- 深入教程英文
Redis:Sorted Sets (新标签页)↗
阅读分数排序、成员唯一与范围查询,设计排行榜和时间线索引。
- 深入参考文档英文
Redis ZADD (新标签页)↗
确认分数精度、选项和新增与更新返回语义。
- 深入参考文档英文
Jedis UnifiedJedis源码 (新标签页)↗
查看Java命令入口委派,区分客户端与服务端结构实现。