
先把话说在前头很多同学学 Redis 的时候打开文档背了一圈命令自认为都会了结果一上生产就翻车——要么缓存穿透把数据库打挂要么分布式锁在并发下失效要么一次 KEYS 命令把整个 Redis 卡了几十秒。这些事故十有八九不是因为你命令背得不够而是因为你不理解 Redis 的单线程模型不知道每条指令背后的数据结构长什么样更不清楚这些基础概念如何组合成真正能落地的方案。这一课我没有按官方文档目录来念是从我们实际项目里最常用、最常踩坑的部分拆开讲单线程模型怎么读懂、五种数据结构的底层编码怎么切换、核心指令到底怎么选怎么用最后把分布式锁和缓存穿透这些高频话题一次捋清楚。1. 单线程模型Redis 是快在内存还是快在“单线程”很多人刚接触 Redis 时都会有个疑问一个基于内存的数据库为什么偏偏用单线程这看起来像是在“自我降速”。实际用下来你才会明白单线程不是 Redis 的短板反而是它维持稳定性的关键前提。1.1 快在内存和事件循环而不是多线程Redis 快的核心原因有三个按重要性排序数据在内存里。内存的随机访问延迟是纳秒级磁盘是毫秒级光这一条就甩开传统数据库几条街。非阻塞 IO 多路复用。Redis 主线程通过 epoll/kqueue 这类多路复用器在一个线程里同时监听成千上万个客户端连接。网络事件到了就交给事件循环处理没事做的时候线程就休眠CPU 占用极低。单线程避免了上下文切换和锁竞争。多线程程序里线程切换、加锁、解锁都要消耗 CPU而且高并发下锁竞争会直接放大延迟。Redis 把命令执行放在一条线程上天然没有这些开销。打个比方多线程像很多人分工搬砖但搬砖过程中互相要让路、要签到签退Redis 单线程像一个熟练工流水线作业事件循环就是传送带来一件处理一件偶尔等人送货网络 IO但不会因为“抢同一块砖”而打架。这也是为什么 Redis 官方测试里单线程实例每秒执行几十万次 SET/GET 很常见。如果你把同样数据放到磁带或 SSD 上再好的调度也白搭。1.2 单线程的代价哪些命令会把所有请求都“堵死”单线程模型最大的代价是一条慢命令会让后面所有命令排队等待。我见过一次事故同事在线上执行 KEYS *当时实例里有上亿 key一条命令跑了接近 30 秒期间线上所有缓存请求全部超时。这就是单线程模型的“放大器”效应。需要重点避开的命令和场景KEYS *在 key 数量大时全表扫描必须用SCAN系列命令分批遍历。大 key 的集合操作比如一个 List 里有几百万元素执行LRANGE key 0 -1会产生超大响应拖慢网络和序列化。一次性删除大 key旧版本DEL是同步删除大 key 删除时会卡住。Redis 4.0 后可以用UNLINK它是异步删除主线程发个通知就走不阻塞。热点 key 的复杂计算比如某个 Hash 字段极多且频繁执行HGETALL每次都要把所有字段全返回。我在线上排障时有个习惯先看当前所有客户端在等什么。如果是只读命令慢多半是慢查询如果所有命令都在阻塞大概率是某条恶魔命令正在执行。这个排查思路比盲目重启重要得多。1.3 Redis 6 引入 IO 多线程为什么命令执行还是单线程Redis 6.0 开始支持 IO 多线程很多初学者以为 Redis 终于“多线程”了。其实它只是把网络读、网络写这些耗时操作分给多个 IO 线程处理核心命令解析、执行、数据操作仍然在主线程。原因很实际Redis 的命令复杂度普遍是 O(1) 或 O(log N)如果真的把命令执行也拆到多线程就必须考虑锁、并发安全、数据一致性复杂度会指数级上升收益反而不明显。网络 IO 那部分才是很多时候的瓶颈——客户端多、小包多单线程读 socket 容易成为瓶颈。所以 Redis 把最耗时的网络读写并行化了执行还是“单线程 事件循环”。明白这一点你调优时就有方向如果你的 QPS 很高但 CPU 没跑满可以把io-threads打开并设置io-threads-do-reads yes。如果问题是命令本身太重比如大 key 读取开多 IO 线程没有作用只能从数据模型和命令选型上解决。2. 数据结构从 API 表象看到底层编码的切换Redis 对外只给了五种数据类型但内部实现要复杂得多。理解底层编码不是为了写底层代码而是为了回答一个实际问题为什么同样存一万个整数Set 和 List 的内存差别能到好几倍为什么小哈希很快大哈希有时候突然内存暴涨2.1 String 的三种编码与 SDS字符串是 Redis 用得最多的类型底层编码有三种编码触发条件说明INT字符串能解析为 long 范围内的整数按整数存储节省内存支持 INCR/DECREMBSTR长度小于等于 44 字节一次性分配连续内存读写快RAW长度大于 44 字节或无法转为整数普通动态字符串需要两次分配你SET age 18Redis 可能底层存的就是一个 long 整数而不是字符串。这解释了为什么INCR age能原子 1。字符串底层结构叫 SDSSimple Dynamic String不是 C 语言原生char*。SDS 有三个明显优势O(1) 获取长度不用遍历。修改时如果容量不够会自动扩容并预留空间减少反复分配内存。用 len 而不是\0判断结尾所以可以安全存储二进制数据比如图片、序列化对象。日常开发中建议记住所有字符串操作都是二进制安全的你可以把任何字节流塞进去但取出来时别指望它自动还原成对象序列化是你自己的事。2.2 List 和 Hash 的压缩与转换List 在 Redis 3.2 之后使用 QuickList 结构本质是多个压缩列表节点组成的双向链表。7.0 之后压缩列表节点逐步替换为 listpack。之所以不用纯链表是因为传统链表每个节点都要存前后指针内存开销大、缓存不友好纯压缩列表在元素追加、中间插入时又有性能风险。QuickList 是两者的折中每个节点内部紧凑存储一批元素节点间通过指针串联。Hash 的底层在小数据量时使用 listpack旧版是 ziplist数据量大时切换为 hashtable。触发切换的核心配置是hash-max-listpack-entries默认 128。hash-max-listpack-value默认 64。也就是说当哈希里字段数少于 128 且字段名和值都小于 64 字节时Redis 会用紧凑的内存布局超过阈值后为了查询性能转为真正的哈希表。这个转换是自动的但要意识到转换前后内存差异可能很大。用 Hash 存对象有个好处比如用户对象有name、age、email你更新其中一个字段只需要HSET user:1001 age 30不用像 String 那样把整个 JSON 读出来改完再写回。这在字段频繁更新的场景下能省大量网络和序列化开销。2.3 Set 和 ZSet 的 IntSet、SkipList 关键细节Set 底层有两种编码当所有元素都是整数且数量较少时使用 IntSet。否则使用哈希表value 为空利用哈希的 key 去重。set-max-intset-entries默认 512。如果超过 512 个整数或者插入一个非整数元素IntSet 会升级为哈希表。这个升级是一次性重建数据量大时会有一瞬间阻塞线上添加大量数据前最好先评估。ZSet 是 Redis 里最“聪明”的数据结构它需要按分数排序、支持范围查询、还要快速拿到某个成员的分数。实现上同时用了一个哈希表和一个跳表SkipList。小数据量时也走 listpack超过阈值后用跳表。为什么选跳表而不是红黑树我看过很多面试解读核心有三点跳表实现和调试成本低区间遍历ZRANGE时只需要沿着链表走。做范围查询时红黑树需要中序遍历跳到下一个节点不如链表直观。跳表可以用随机层数控制树高内存上可控。我经常用 ZSet 做排行榜、延时队列、滑动窗口限流。它的最大价值是“按分数排序 按分数范围切片”这一套组合拳其他数据类型很难替代。2.4 Redis 7.0 的 listpack 重构带来的变化Redis 7.0 把 ziplist 替换成了 listpack主要原因是 ziplist 存在连锁更新问题当一个节点的内容长度变化时会导致后续所有节点的 prevlen 字段更新极端情况下从 O(1) 退化成 O(n)。listpack 的每个 entry 只记录自身长度不依赖前一个节点的长度信息从设计上消除了连锁更新。这个变化不用改你的代码但会影响你对“小集合是否安全”的判断。过去很多人为了省内存故意把 listpack 阈值调大7.0 之后可以更放心地使用紧凑结构。不过还是要提醒任何底层编码都只是实现细节你对外该用TYPE和OBJECT ENCODING来观察而不是猜。3. 核心指令手册高频场景下的命令选型与组合命令不用全背但核心指令一定要形成肌肉记忆。下面这些是生产环境里最常出现的高频命令我按场景拆开讲。3.1 字符串与全局命令SET NX EX、INCR 与过期时间字符串命令里最值得记住的写法是SET lock:order 10001 NX EX 10这条命令同时做了“存在才设置”和“10 秒过期”两件事。NX表示只有当 key 不存在时才能成功EX 10表示过期时间是 10 秒。这比先SETNX再EXPIRE安全得多因为两步操作之间崩溃会让 key 变成永远不释放的锁。其他高频命令我来一份速查表命令作用示例SET key value [EX s]设置值可带过期时间SET session:1 ok EX 3600GET读取值GET session:1MSET/MGET批量写/读减少 RTTMSET a 1 b 2INCR/DECR原子自增/自减INCR page:viewINCRBY/HINCRBY按指定步长增加INCRBY user:1001:count 5APPEND追加字符串APPEND msg worldSTRLEN获取字符串长度STRLEN msgSETEX/PSETEX设置值并带过期时间SETEX token abc 60SETRANGE/GETRANGE字符串子串操作SETRANGE key 0 abcSETBIT/GETBIT位图操作SETBIT sign:2024 0 1全局命令里EXISTS判断存在、DEL删除、TYPE看类型、TTL查剩余过期时间、PERSIST去除过期时间。排查问题时我几乎每次都会用TTLTYPE先看数据类型再看剩余时间避免对不存在或者过期时机不对的 key 做错误操作。3.2 Hash 和 List缓存对象与消息队列怎么选Hash 的基本命令HSET key field value/HGET key field/HMGET key field1 field2HDEL key field删除字段HLEN字段数量HINCRBY key field n字段自增HSCAN在大哈希里遍历实际项目里我用 Hash 存“可部分更新的对象”购物车、用户资料、商品扩展信息。一次HGETALL能拿到整对象修改单个字段只传该字段省流量也省 CPU。List 则非常适合当队列用LPUSH task_queue task_id BRPOP task_queue 0BRPOP是阻塞式读取如果队列为空就一直等不会像RPOP那样立刻返回 nil也避免了轮询浪费 CPU。List 作为轻量队列最大的问题是消息没有确认机制消费者读到消息后如果崩溃消息就丢了。业务要求不高的日志队列、异步通知用 List 很顺手要可靠投递、消费分组、消息 ACK 的场景还是应该上 Redis Stream 或专业消息队列。3.3 Set 和 ZSet去重统计与排行榜Set 的命令核心是集合运算SADD/SREM/SISMEMBERSMEMBERS取所有成员大集合慎用SCARD成员数SINTER/SUNION/SDIFF交集、并集、差集签到、抽奖、去重、共同好友用 Set 都很自然。比如抽奖SADD draw_pool uid_1 uid_2 SRANDMEMBER draw_pool 1ZSet 的命令核心是分数和排名ZADD key score memberZINCRBY key incr member原子加分ZRANK key member/ZREVRANK key member正序和倒序排名ZRANGE key start stop [WITHSCORES]按排名范围取ZRANGEBYSCORE key min max按分数范围取ZREMRANGEBYSCORE key min max删除分数区间内元素排行榜最常用的是ZREVRANGE leaderboard 0 9 WITHSCORES一条命令拿到 Top10 和对应分数。注意WITHSCORES会连分数一起返回解析时别漏。3.4 生产级组合秒杀库存、限流与 Lua 脚本命令单用是“积木”组合起来才能搭业务。举两个例子。秒杀库存扣减。直接DECR会扣成负数正确做法是用 Lua 脚本把“查库存 扣减”变成原子操作。示例local stock tonumber(redis.call(GET, KEYS[1]) or 0) if stock 0 then redis.call(DECR, KEYS[1]) return 1 else return 0 end用redis-cli --eval stock.lua seckill:stock执行。这样单个实例内不会出现库存超卖而且整个判断在一瞬间完成对其他命令的阻塞影响极小。滑动窗口限流。用 ZSet 记录请求时间戳local key KEYS[1] local now tonumber(ARGV[1]) local window tonumber(ARGV[2]) local limit tonumber(ARGV[3]) redis.call(ZREMRANGEBYSCORE, key, 0, now - window) if redis.call(ZCARD, key) limit then return 0 else redis.call(ZADD, key, now, now .. - .. math.random(100000)) redis.call(EXPIRE, key, window) return 1 end这个组合里ZREMRANGEBYSCORE清理过期窗口ZCARD统计当前窗口请求数ZADD记录当前请求。相比单条命令要查、要算、要写Lua 脚本能保证原子性不用额外加分布式锁。4. 分布式锁、缓存穿透这些热搜问题根上都是指令组合热搜词里反复出现分布式锁、缓存穿透、缓存治理这些不是孤立问题本质都是几个核心命令的组合使用。这部分我专门展开讲。4.1 分布式锁SETNXEXPIRE 的原子性陷阱业界最常见的错误写法是Boolean ok redis.setIfAbsent(lock:order, id); if (ok) { redis.expire(lock:order, 30); }如果setIfAbsent成功后程序在expire之前崩溃锁就永远不释放。正确写法是一条命令SET lock:order unique_id NX EX 30释放锁也不是DEL这么简单。如果线程 A 的锁刚过期线程 B 加锁成功此时线程 A 执行DEL会把线程 B 的锁删掉。所以释放之前必须比较锁的 value 是否还是自己的并且比较和删除要原子执行if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 endvalue 用 UUID 线程 ID 生成释放时把 value 作为参数传进 Lua。这套“加锁用 SET NX EX释放用 Lua 比较后删”是我在生产上反复确认过的基础实现。更复杂的续期、可重入就直接上 Redisson 的RLock不建议自己重复造轮子。4.2 缓存穿透布隆过滤器与空值缓存实操缓存穿透的本质是查询一个缓存和数据库都不存在的数据。比如恶意请求用不存在的用户 ID 刷接口每次都会穿透缓存打到数据库。最直接的防御空值缓存查询结果为空时也往缓存里写一个占位符并设置 60 秒左右的短 TTL。这样短时间内同一个 key 不会反复打数据库。布隆过滤器启动时把存量 ID 全部加载到布隆过滤器请求进来先判断 ID 是否存在不存在直接拦截。布隆过滤器可以用 Redis 的 Bitmaps 自己实现也可以用专门的模块。接口层参数校验明显非法的 ID如负数、超长字符串在入口直接拒绝。空值缓存的代价是会占用少量内存但能挡住绝大多数穿透。布隆过滤器有误判率它说“不存在”一定不存在说“存在”可能不存在所以一般是放在缓存前面做粗糙过滤。4.3 缓存击穿与雪崩互斥锁、逻辑过期与过期时间随机化缓存击穿是某个热点 key 正好过期瞬时大量请求同时打到底层数据库。缓存雪崩则是一大批 key 在同一时间过期造成数据库瞬时压力。前者是单点后者是群击。击穿的常见解法是互斥锁当缓存读不到数据时先尝试用SET NX EX加锁加锁成功的那个请求去加载数据库并回写缓存其他请求要么短暂等待要么返回旧数据。用前面学的命令组合就是SET cache:hot timestamp NX EX 5或者用逻辑过期缓存里存一个“逻辑过期时间”后台异步任务发现快过期了就刷新。这种方式不会阻塞请求但实现上要复杂一些。雪崩的解法核心是“错峰过期”过期时间不要写死。redis.set(key, value, Duration.ofSeconds(300 new Random().nextInt(300)));另外配套多级缓存、Redis 高可用集群也可以在数据库前面加一层本地缓存。这不是单条命令能解决的但过期时间随机化是最便宜的一步。4.4 慢查询排查为什么监控指令比背命令更关键单线程模型下慢查询是生产事故的头号敌人。Redis 提供了慢查询日志SLOWLOG GET 10 SLOWLOG LEN SLOWLOG RESET同时可以用CONFIG SET slowlog-log-slower-than 10000把超过 10 毫秒的命令记录下来。我排障时最常用的流程是redis-cli -h host -p port SLOWLOG GET 20看最近慢命令。从慢日志里找出代价大的 key用DEBUG OBJECT key查看底层编码和大小。判断是命令本身慢还是大 key 导致慢。优化方向换命令、拆 key、调阈值、加缓存。这一套组合拳比临时猜问题要高效得多。记住监控列表里反复出现的命令才是你需要重构业务逻辑的地方。5. 环境搭建与调试Redis 安装、主从、可视化工具热搜词里有大量安装配置相关词比如 Windows 安装、Redis Desktop Manager、Docker 部署。这些是新手最容易卡住的地方这里一并讲清。5.1 Windows / macOS / Linux 安装与配置Windows 上官方并没有原生支持方案但实际开发时通常有三种办法WSL 2 里安装 Redissudo apt install redis最接近 Linux 环境推荐。使用 Memurai它是 Windows 原生 Redis 兼容实现可以作为开发学习。使用 Docker Desktop 跑 Linux 容器也是目前最省心的方式。macOS 上最简单的是 Homebrewbrew install redis brew services start redisLinux 上apt install redis-server systemctl enable redis-server systemctl start redis-server装好后先改两点requirepass设置密码bind 127.0.0.1只允许本机访问除非你确定内网安全。否则 Redis 一旦暴露到公网被写入 crontab 挖矿的事这几年时有发生。连接测试redis-cli -h 127.0.0.1 -p 6379 -a yourpassword ping如果返回PONG基本环境就没问题。5.2 Docker 部署和主从复制快速落地方案Docker 部署 Redis 的核心命令docker run -d --name redis \ -p 6379:6379 \ -v /data/redis:/data \ redis:7 \ --requirepass yourpassword \ --appendonly yes这里--appendonly yes开启 AOF 持久化防止容器重启丢数据。-v把数据目录挂载出来方便备份。主从复制最简单地用一个 Redis 容器加配置redis-server --port 6380 --replicaof 127.0.0.1 6379Redis 5 之前的主从配置是slaveof5.0 之后叫replicaof。要注意从节点默认是只读的如果误执行写命令会报错。主从只能解决“读多写少”的横向扩展并不能自动故障转移。要做高可用得引入哨兵Sentinel或直接上 Redis Cluster。生产规模不大时先用“一主一从 Sentinel”最稳妥。5.3 可视化客户端的选型RDM 与 Another Redis Desktop Manager很多人问 Windows 下用什么连 Redis我推荐两个Redis Desktop ManagerRDM老牌工具界面成熟但现在很多版本只对企业用户开放。Another Redis Desktop ManagerARDM开源、免费、跨平台支持 Windows/macOS/Linux颜值和稳定度都不错是热搜词里那个长长名字的主角。用可视化客户端时要注意连接 Redis 需要填对 host、端口、密码以及数据库序号默认 0。很多新手连不上不是密码错而是没填数据库序号或没开远程访问。另外可视化工具虽然直观但我不建议拿它执行KEYS *尤其是生产环境原因在单线程模型那段已经讲过了。真要逛 key用redis-cli --scan --pattern user:*更安全。6. 序列化、大 Key 与我的升级路线6.1 为什么 Redis Desktop Manager 里看到一堆二进制乱码很多新手第一次用 Redis 存 Java 对象后在可视化工具里看到一串乱码第一反应是“Redis 坏了”。其实 Redis 什么都没做它只负责存字节不负责理解你的对象。如果项目里用了 JDK 自带的序列化Redis 里存的就是一串带类信息的二进制比如\xAC\xED\x00\x05t...。这不可读而且兼容性差。我建议按场景选序列化方案只存简单字符串使用StringRedisSerializer直接看到可读内容。存 JSON使用 Jackson 或 Fastjson2Redis 里是{name:xxx}的人类可读文本。高吞吐场景使用 Protobuf体积小、序列化快但调试时也不可读。序列化方案还要注意 key 的规范。我常用的规范是“业务模块:实体:ID:字段”比如order:info:123、user:login:456。这样在缓存治理和按前缀统计时都很方便。6.2 大 Key 与缓存治理的日常命令大 key 是线上 Redis 的一个隐形杀手。判断方法很简单用redis-cli --bigkeys它会遍历实例并找出各个类型下最大的 key。找到后要先判断“大”的定义不同业务标准不同通常String 超过 10 MB。Hash 有超过 1 万个字段。ZSet 有超过 1 万个成员。大 key 的危害有三个单线程下读写容易阻塞、内存分配不均衡、主从复制时全量同步压力大。治理方式我总结为四步拆分按业务维度把一个大 key 拆成多个小 key。压缩String 存 JSON 时去掉不必要字段或换更紧凑的序列化。异步删除确认要清理时用UNLINK不要用DEL。设置合理过期所有缓存 key 都要有过期策略避免永久 key 堆积。配合上一节的序列化规范缓存治理的核心就是key 可读、ttl 可控、大字段可拆、慢操作可监控。这四句话基本涵盖了我见过的绝大多数缓存问题。6.3 一条从菜鸟到大师的路线图学 Redis 不要一上来就钻源码。我建议的路线是先把核心指令用熟这一课的内容反复敲。理解单线程模型遇到慢查询时才能定位问题方向。掌握数据结构底层选型时心里有数不靠猜。做真实场景分布式锁、缓存穿透、排行榜、限流每个都写一份自己理解的实现。再看持久化与集群RDB、AOF、主从、哨兵、Cluster一个个搭起来试。最后研究源码与通信协议RESP 协议、事件循环、内存淘汰策略。我个人在踩过几次大坑之后的体会是Redis 的真本事不在于你知道多少条命令而在于你知道某个问题应该用哪几个命令组合、为什么这样组合、组合之后会有什么副作用。把这一课的内容吃透再去看分布式锁、缓存治理、集群方案你会发现之前那些“热搜问题”突然都串起来了。