CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
综合练习 — CodeRoadMap
综合练习:五大场景的一次完整实战
这一课你将能做到
用 redis-cli 把限流、验证码、做题进度、排行榜、分布式锁串成一条完整业务链
对照上线 checklist 逐项检查键设计、过期、持久化与监控
独立为一个新需求设计 Redis 键方案并说出取舍理由
为什么要学
单个命令你已经很熟了,但真实开发中接到的需求从来不是「用一下 ZADD」,而是:「用户提交代码,要防刷、要排队、判完要通知、分数要上榜、进度要更新、重复提交要拦住」。面试官也更爱问这种端到端问题,因为它能区分「背过命令」和「真的做过」。
本课是结课实战:我们以「用户 1001 提交第 501 题」为主线,把前 11 课的结构全部走一遍。建议打开三个终端:终端 1 当网站、终端 2 当判题机、终端 3 做监控。
先建立图像
① 发验证码登录 captcha:13800000000 String + TTL
② 提交前限流 ratelimit:judge:user:1001 Lua 原子计数
③ 抢分布式锁 lock:problem:501:user:1001 SET NX EX
④ 任务入队 queue:judge:jobs Stream(消费组)
⑤ 判完更新进度 progress:user:1001:course:7 Hash
⑥ 积分上双榜 rank:{course:7}:weekly/all ZSET(同槽)
⑦ Pub 通知前端 judge:result:1001 Pub/Sub
⑧ 释放锁 Lua 比对持有者删除
跟着做
步骤 1:验证码环节
终端 1(网站):
127.0.0.1:6379> SET captcha:13800000000 482916 EX 300
OK
127.0.0.1:6379> GET captcha:13800000000
"482916"
模拟用户提交验证码,比对一致后立即删除:
127.0.0.1:6379> DEL captcha:13800000000
(integer) 1
步骤 2:提交限流(Lua 原子版)
准备限流脚本并加载:
127.0.0.1:6379> SCRIPT LOAD "local c=redis.call('INCR',KEYS[1]); if c==1 then redis.call('EXPIRE',KEYS[1],ARGV[1]) end; if c>tonumber(ARGV[2]) then return 0 else return 1 end"
"a1b2c3..."
连续调用 6 次,观察最后一次返回 0:
127.0.0.1:6379> EVALSHA a1b2c3... 1 ratelimit:judge:user:1001 10 5
(integer) 1
...
127.0.0.1:6379> EVALSHA a1b2c3... 1 ratelimit:judge:user:1001 10 5
(integer) 0
返回 0 时网站直接拒绝,后续步骤都不执行。
步骤 3:加锁防重复提交
127.0.0.1:6379> SET lock:problem:501:user:1001 token-8899 NX EX 30
OK
立刻再点一次提交(或脚本重放):
127.0.0.1:6379> SET lock:problem:501:user:1001 token-8900 NX EX 30
(nil)
第二次拿不到锁,说明防重复提交生效。
步骤 4:任务进 Stream,判题机消费
建流和消费组:
127.0.0.1:6379> XGROUP CREATE stream:judge:events g1 0 MKSTREAM
OK
终端 1 投递任务:
127.0.0.1:6379> XADD stream:judge:events MAXLEN ~ 10000 * user 1001 problem 501 code "bWFpbigp"
"1760000200000-0"
127.0.0.1:6379> XREADGROUP GROUP g1 judge-1 COUNT 1 STREAMS stream:judge:events >
步骤 5:判题完成——更新进度与榜单 127.0.0.1:6379> HSET progress:user:1001:course:7 solved 13 total 50 acRate 0.78 lastQuestion 501 status doing
OK
127.0.0.1:6379> EXPIRE progress:user:1001:course:7 2592000
(integer) 1
用 hash tag 保证周榜与总榜同槽后分别加分:
127.0.0.1:6379> ZINCRBY rank:{course:7}:weekly 10 user:1001
"880"
127.0.0.1:6379> ZINCRBY rank:{course:7}:alltime 10 user:1001
"1540"
127.0.0.1:6379> ZRANGE rank:{course:7}:weekly 0 2 REV WITHSCORES
1) "user:1001"
2) "880"
步骤 6:确认消息、通知前端、释放锁 127.0.0.1:6379> XACK stream:judge:events g1 1760000200000-0
(integer) 1
发布结果通知(终端 1 应提前 SUBSCRIBE judge:result:1001):
127.0.0.1:6379> PUBLISH judge:result:1001 "{\"verdict\":\"AC\",\"score\":10}"
(integer) 1
127.0.0.1:6379> EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 lock:problem:501:user:1001 token-8899
(integer) 1
步骤 7:终端 3 全程监控 127.0.0.1:6379> XLEN stream:judge:events
127.0.0.1:6379> XPENDING stream:judge:events g1
127.0.0.1:6379> SLOWLOG GET 5
127.0.0.1:6379> INFO memory
确认 XPENDING 归零(没有卡住的消息)、无限流误报后,这条业务链才算完整闭环。
步骤 8:异常分支演练 判题机宕机版:消费后不 XACK,直接「宕掉」。另一个消费者用 XAUTOCLAIM 接管:
127.0.0.1:6379> XAUTOCLAIM stream:judge:events g1 judge-2 30000 0 COUNT 10
锁超时版:把锁时间设为 2 秒,业务超时后观察锁自动释放,体会为什么需要看门狗。亲手制造两次事故,比看十遍文档更能建立敬畏心。
讲清楚要点
结构选型对照 :一次性、要过期 → String;有序排队轻量版 → List;唯一与集合运算 → Set;字段化对象 → Hash;按分排序 → ZSET;可靠消息 → Stream;实时广播 → Pub/Sub;复合原子 → Lua。
键设计三件事每次都要回答 :键名叫什么、值放什么结构、TTL 多久。回答不出 TTL 的键大概率有问题。
一条业务链里多个结构各司其职 :不要试图用一个结构包打天下,也不要为简单需求过度设计(验证码用不上 ZSET)。
幂等与防重要前置 :锁、限流放在写入之前;错误返回要让客户端能理解并可重试。
上线前必须可观测 :每个高频键都要能看到数量、延迟、失败率;关键流程要有兜底(定时对账、超时接管)。
你可能会懵的地方
这些操作要做那么多步,会不会很慢?
单条命令通常微秒级,8 步合计往往不到 2 毫秒,远快于一次磁盘数据库事务。真正要优化的是减少网络往返:可以用 pipeline 把无依赖的命令合并,有原子要求的用 Lua。
万一某一步成功、下一步失败,数据不就半截了?
这正是需要幂等设计和兜底任务的原因:进度更新可重放、XACK 可补、锁会过期、限流会归零。Redis 不提供跨结构事务,工程上靠「可重试 + 对账」收敛,而不是追求一步不错。
周榜和总榜为什么要同槽?
只有同槽才能在集群里一起做范围处理或用脚本同时更新。通过 hash tag 实现;但并非所有键都要同槽,仅在需要原子联动时才绑定。
判题机处理到一半真宕机,用户看到什么?
网站应在超时后把提交标记为「未知,请稍后查看」,后台靠 XAUTOCLAIM 重投任务;最终结果以进度和榜单为准。绝不能让用户无限等待。
课程学完之后怎么继续提升?
在真实项目里用客户端 SDK 重练这套链路(连接池、序列化、超时重试),再深入 Redis Function、模块生态和性能调优,命令基础已经足够支撑这些进阶。
错法对照 错法 问题 正解 先写库再想限流和锁 防护在事故之后 限流/加锁放在最前 消息处理完忘记 XACK 消息反复重投 确认放在流程末尾并监控 XPENDING 所有键都不设 TTL 内存只涨不降 逐键设计生命周期 用一个大 Hash/大 List 存全量 阻塞、热点、难迁移 拆成海量小键 上线无监控无告警 出事靠用户通知 数量、延迟、失败率全监控 演练只走「快乐路径」 异常一来全线懵 必练宕机、超时、重复提交分支
卡点急救
整条链路卡在某一步 :从 SLOWLOG 和 INFO stats 定位是慢命令还是流量打满;逐键检查上一步是否真的成功(返回值逐个核对)。
数据对不上(榜分与进度不一致) :以可重放的步骤重新对账——重放判题事件、核对 ZSCORE 与 Hash 字段,差哪个补哪个。
用户反馈重复扣分/重复上榜 :检查锁是否所有入口统一、token 是否唯一且过期合理;补偿时保证加分操作幂等(同一提交只加一次)。
面试常问
设计一个判题系统,Redis 在其中承担什么?
验证码(TTL String)、提交限流(Lua 原子计数)、防重(分布式锁 NX EX + 持有者校验)、任务排队(Stream 消费组、XACK、故障接管)、进度(Hash 字段更新)、排行榜(ZSET 双榜、定时重置)、结果通知(Pub/Sub)。再讲清持久化与监控兜底。
上线前你的 Redis checklist 有哪些?
键命名统一且全部有 TTL 结论;maxmemory 与淘汰策略保护持久键;AOF everysec + RDB 异地备份;密码/ACL 与内网限制;慢日志阈值与监控告警;消费组 PEL 与超时接管;连接池与超时重试配置。
怎么保证分布式环境下不重复处理任务?
入口加分布式锁(互斥、过期、标识释放);消费组保证一条消息只投递给一个消费者;处理成功才 XACK;宕机消息经 XAUTOCLAIM 幂等重放。
自测三问
不看资料,默写出「提交一道题」的 8 个步骤及每步用到的结构。
判题机在 XACK 前宕机,系统如何发现并恢复?涉及哪两条命令?
为一个新需求(如「每日连签奖励」)设计 Redis 方案,回答键名、结构、TTL、原子性、兜底五个问题。
小结一句话 Redis 的真功夫不在单条命令,而在「按需求选对结构、用 TTL 和锁守住边界、用 Lua 保证原子、用监控和兜底收场」这套组合能力——走完这条链路,本课程就正式结业了。
延伸