ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Redis核心实战:数据类型、持久化、高可用、缓存治理与分布式锁

Redis核心实战:数据类型、持久化、高可用、缓存治理与分布式锁 写下这篇 Redis 学习日志的时候我手头正攒着好几个踩坑现场一次是 redis-cli 连接超时排查了半天一次是缓存穿透把数据库打挂、被群里老哥拉去复盘还有一次是 Docker 里起的 Redis 容器数据全没了的“惨案”。所以这篇日志打算把从零学 Redis 到日常使用中最值得记录的东西按一条真实的学习路径沉淀下来。不讲教科书式的堆概念而是尽力还原我当时是怎么理解、怎么上手、又怎么吃了教训的——包括数据类型、持久化、高可用、缓存治理、分布式锁这五大主线以及面试八股和排错经验。适合正在学 Redis 的开发者也适合想看坑位总结的老手。1. 学习路线与整体理解1.1 为什么必须学 Redis它到底解决了什么问题我在学 Redis 之前先记住了一个词内存中的数据结构服务器。这句官方定位信息量其实很大——核心在“内存”所以它快核心在“数据结构”所以它比单纯 KV 都比别的多了一层灵活性核心在“服务器”所以它被设计成独立服务而不是嵌入在应用里的一个库。Redis 解决的问题通俗点讲是这三类一是把热点数据从数据库搬到内存把高并发读扛住给后端数据库留出喘息空间二是借助 Redis 的过期策略、分布式锁、发布订阅、队列等能力完成跨服务的状态协调三是通过持久化和主从复制在极端场景下保数据不丢、服务可用。我记得第一次用它就是在秒杀场景里做了个简单的缓存预热把商品库存快照放进 Redis直接把接口耗时从 120ms 拉到 3ms 以内那一刻对“内存快”就有了体感。1.2 我给自己定的学习路线学 Redis 很容易掉进“只会 set/get”的舒适区。为了不浅尝辄止我当时给自己定了六步路线每一步对应一套实际问题先装起来Windows 或 Linux 本机可跑理解 redis-server 与 redis-cli 的关系。掌握数据类型不只是 String而是把 Hash、List、Set、ZSet 在真实场景里各用一遍。搞懂持久化与淘汰策略弄明白 RDB、AOF 的取舍以及 Redis 如何“在内存放不下时”保护自己。搭建高可用主从复制、哨兵、集群模式都用 Docker 实际起一遍。深入缓存治理穿透、击穿、雪崩、双写一致性这四件事必须形成自己的解决方案。用 Redis 搞定分布式锁从 SETNX 手写锁到 Redisson 看门狗机制层层递进。这条路线走完之后我面试时被问 Redis 就直接闭眼能讲半小时。所以这篇日志也按这个顺序展开每个阶段我都会补上自己踩坑的第一手经验。2. 安装篇Windows、macOS、Docker 三种实操记录2.1 Windows 下安装 Redis 的一个干净办法Windows 官方不提供 Redis 二进制这是个老坑。我一开始搜 redis下载跑进各种“Windows 安装包”站点下载下来的却是一堆破解软件弹窗后来学乖了只认两个靠谱渠道一是 Microsoft 的 Open Tech 项目维护的 Win64 分支二是 Redis 官方推荐的 WSL 方案。当时我用的版本是 5.0.14.1下载 zip 解压目录下 redis-server.exe 和 redis-cli.exe 都在双击 redis-server.exe 默认端口 6379 就跑起来了redis-cli.exe 里 ping 一下返回 PONG就算通了。需要特别提一句的是 Windows 下设置 Redis 密码。直接在 redis.windows.conf 文件里搜索requirepass把这行注释打开改成requirepass 你的密码然后用redis-server.exe redis.windows.conf启动。这时候 redis-cli 里直接 ping 会报 NOAUTH你得先执行AUTH 你的密码才能继续操作。这个配置文件路径很容易搞混我最初以为要修改根目录的 redis.conf结果文件里根本没有 requirepass 字段折腾了十分钟。2.2 macOS 安装 Redis 两种方式实测mac 上最省事的方式是用 Homebrewbrew install redis。安装后默认会执行redis-server开一个终端保持前台运行。每次开机想用 Redis 的话可以brew services start redis让它常驻后台。我实际更推荐在开发机上让它跑在后台因为项目里 Redis 是常态依赖不需要每次都手动启动。另一种方式是 Dockerdocker run -d --name redis-test -p 6379:6379 redis。macOS 上 Docker 跑 Redis 需要注意一个点如果你是 Apple Silicon 芯片镜像平台不要指定错了直接拉官方 redis 镜像是没问题的我之前手动加--platform linux/amd64反而会额外多一层模拟层性能亏一点。还有一个小技巧docker exec -it redis-test redis-cli这条命令可以在宿主机上直接进入容器里的 redis-cli省去先 docker exec 开 shell 再敲 redis-cli 的两步操作。2.3 Docker 安装 Redis 主从时我遇到的 500 错误我照着教程准备用 Docker 搭 Redis 主从时第一条命令docker search redis就回报了request returned 500 Internal Server Error for API route ... /images/search?termredis。查日志才知道是 Docker Desktop 和 Docker Hub 之间的连接出了问题和命令本身无关。解决办法是检查 Docker Desktop 是否退出重启过、确认当前网络到 Docker Hub 通畅、然后重启 Docker Desktop 再试。如果重启还不行就在 /etc/docker/daemon.json 配置镜像加速地址改完重启 Docker Daemon。主从配置我最终没有依赖docker search而是直接docker pull redis:7.2-alpine然后起两个容器第二个容器启动时加入参数--replicaof 主容器名 6379或者挂载一个 redis.conf里面写上replicaof redis-master 6379。启动后进从库执行info replication看到role:slave和master_link_status:up就代表主从已经通了。注意容器间要通必须让它们在同一自定义 bridge 网络里docker network create redis-net两个容器都--network redis-net才行。我一开始直接使用--link老参数发现新版 Docker 对redis-cli -h 容器名已经能通但 Sentinel 探测结构还是建议用自定义网络实测省心得多。3. 数据类型五种基础类型背后的场景逻辑3.1 String 不是只能存字符串String 是 Redis 最常用的类型底层是 SDS简单动态字符串支持存二进制安全的内容。实际开发中它不只是存取普通文本还能存 JSON 序列化后的对象、计数器等等。我最早的一个误区是以为 String 只能英文数字后来才知道它存中文字符也毫无问题Redis 存的是字节序列。String 几个实测实用的命令SET key value [EX 秒] [NX]原子设置NX 表示不存在才设置这是后面做分布式锁的基础。SETEX key 秒 value设置同时指定过期时间。INCR / DECR原子自增/自减适合做点击量、库存扣减。GETSET key value取旧值同时设新值。我最常用到的场景是缓存 token登录成功生成一个 UUIDSET token 用户ID EX 7200每次请求过来拿到用户 ID不用频繁查库。这里有个隐蔽的坑SET的EX参数要求逗号前面是数字但有些人会写成SET token abc EX 7200 NX注意EX和NX之间的顺序无所谓Redis 命令解析是按参数名识别的但如果你写SET token abc EX NX 7200那就报参数数量不对了。刚开始学的时候这里容易栽跟头。3.2 Hash最能命中“对象”语义的类型Hash 是 Redis 里最实用的一个类型底层是哈希表加 ziplist元素少、值短时自动使用压缩列表。它存储的是一组 field-value 对天然适合表示一个对象。比如用户信息user:1001里面存name、age、gender本质上是把一个 Java 对象铺展成多个字段避免了整个对象 JSON 序列化和反序列化的开销。实际开发中如果你只需要修改对象的某一个字段用 String 全量覆盖会很浪费而 Hash 的HINCRBY可以只对某个字段做原子递增比如在一个对象的age字段上做HINCRBY user:1001 age 1。我用它做过购物车hset cart:1001 sku_001 2要加一件就让HINCRBY cart:1001 sku_001 1。底层实现细节上当 Hash 对象满足hash-max-ziplist-entries和hash-max-ziplist-value配置时内部使用 ziplist 节省内存超过阈值后转成 hashtable 保证访问效率这一点面试常考实际调优也有意义。3.3 List 的队列属性和阻塞读取List 在 Redis 内部是一个双向链表或者压缩双端链表quicklist。Java 里的 LinkedList 朋友很熟悉但它多了专门用于队列消费的命令LPUSH往左边塞RPOP从右边取这就是一个标准的 FIFO 队列。Redis 的 List 还提供了阻塞版本BRPOP和BLPOP这是我在项目里用它做简单任务队列的最大亮点。消费端使用BRPOP queue 0时如果队列为空线程是被阻塞的不会疯狂轮询给 Redis 打压力。这里有个细节BRPOP queue 0的 0 表示无限等待如果是 5 就表示最多阻塞 5 秒超时后返回 nil实际业务里可以根据消费频率来设置太短会导致很弱的空转太长又可能堆积处理延迟我偏好在低峰期设置 10 秒高峰期设置 3 秒。3.4 Set 与 ZSet去重、标签、排行榜Set 是字符串的无序集合适合做去重。比如点赞记录用SADD post:1001:likes user_1再SISMEMBER判断这个用户是否已经点过赞。ZSet 在 Set 基础上给每个元素挂了 score底层是跳跃表加哈希表适合做排行榜ZADD rank 100 user_1用户得分变化就ZINCRBY rank 10 user_1。要拿前 10 名用ZREVRANGE rank 0 9 WITHSCORES。这里要特别说下 ZSet 的排序原则当多个元素 score 相同时按字典序排序这个对排行榜的稳定性很有影响。有一次我写周榜发现有两个用户积分相同时排名顺序居然是按用户名 ASCII 码排的产品那边差点当成 bug后来在需求里明确加上“同分按时间先后”于是我把 score 设计成了积分 时间戳的小数组合比如1000.00000123456这样同分时先到的就能排前面。这个技巧常见于积分榜的需求设计值得记下来。3.5 其他实用的新类型Bitmap 与 HyperLogLog除了五大基础类型后面认识的 Bitmap 和 HyperLogLog 也很实用。Bitmap 不是新数据结构本质是 String 类型的按位操作但适合做打卡、在线状态这类只有 0/1 的场景。用户连续签到可以用SETBIT sign:2024:10 1 1要查某天是否签到就是GETBIT做统计就是BITCOUNT。一亿用户一天的在线状态Bitmap 只需要约 12MB 内存这个账一算就能打动业务方。HyperLogLog 做 UV 统计很微妙它用固定大小的内存理论最多 12KB去估算去重数量误差在 0.81% 左右。我看过一个案例用PFADD记录当天访问者 IDPFCOUNT直接拿到 UV 估算值既不用 Set 那样在大量元素下占用高内存准确度也足以支撑业务决策。但要注意它只能做基数统计不能查具体有哪些元素如果是精度要求极高的统计比如对账就不适合用它还是老老实实用SADD的那套。4. 持久化机制RDB 与 AOF 的取舍选择4.1 RDB 快照与 COW 机制的本质理解Redis 默认持久化方式是 RDB它会 fork 一个子进程把当前内存中的全量数据生成一个二进制快照 dump.rdb。子进程持久化时主进程还能继续处理命令这是靠的是 fork 时的写时复制Copy On Write机制——fork 的瞬间父子进程共享同一份内存页只有数据被修改时才会复制被改的页所以 fork 不阻塞服务。这里有三个配置项要重点看save 900 1900 秒内至少 1 次写命令就触发一次快照。save 300 10300 秒内至少 10 次写命令触发。stop-writes-on-bgsave-error yes当后台保存失败时Redis 默认会拒绝写命令这在实际生产中经常把人搞懵。有次我在磁盘满了情况下业务日志里突然报大量只读错误排查半天发现是磁盘问题被 RDB 保存机制夹杂进来。RDB 文件最大的缺点是两次快照之间的数据会丢失。如果 5 分钟做一次快照服务挂掉就可能丢 5 分钟的数据这在支付类场景很难接受所以单独靠 RDB 不够还要有 AOF。4.2 AOF 追加日志与 fsync 策略AOFAppend Only File把每条写命令追加到文件末尾恢复时重新执行一遍写命令保证数据能恢复到某时刻的完整状态。但它也有性能开销所以设计了appendfsync三个档位always每次写命令都调用 fsync 同步到磁盘最安全但慢。everysec每秒 fsync 一次性能与安全的折中这是默认值。no由操作系统决定磁盘刷新时机数据丢失风险更大。我对这三档真实体感是always在写频繁的业务下会让 Redis 吞吐从十万级掉到万级甚至更低场景非常受限我见到的实际生产环境九成都是everysec同时配合appendonly yes。AOF 文件会随写入不断增大所以有重写机制auto-aof-rewrite-percentage 100和auto-aof-rewrite-min-size 64mb触发后压缩成新的最小化文件等价于合并历史写命令。4.3 混合持久化是目前最常用的配置Redis 4.0 之后支持混合持久化aof-use-rdb-preamble yes。开启后 AOF 文件前面先放一个 RDB 二进制块之后才是增量命令日志。恢复时先加载 RDB 快再回放增量加载速度比纯 AOF 快很多数据安全性也接近 AOF 的 everysec 级别。我现在的生产配置基本固定成appendonly yes appendfsync everysec aof-use-rdb-preamble yes这套组合对大多数业务足够稳。如果你对数据丢失零容忍再考虑打开 always但一定要做性能压测别想当然直接换。4.4 踩过的一次“redis 数据全没了”的坑我印象最深的一次线上事故就是没配好持久化。当时用 Docker 起 Redis挂在容器里的数据目录是匿名的容器一删dump.rdb 也没了业务缓存全部打回源数据库瞬间压力升高。后来意识到这是持久化和数据卷两个概念混在一起造成的问题。正确的做法是启动容器时挂载-v /宿主机路径:/data并确保 redis.conf 里dir /data。这样的话即使容器没有了数据文件还在宿主机磁盘上。重启容器后再redis-cli --bigkeys核查数据量发现 key 数对得上才算放心。想快速验证数据是否真的持久化成功可以写个测试 keySAVE再重启容器GET一下确认。不要嫌麻烦这是最基础也最容易被忽略的容灾细节。5. 缓存治理穿透、击穿、雪崩、双写一致性5.1 缓存穿透查一个不存在的数据缓存穿透是指请求查询一个数据库里也不存在的数据缓存没有任何记录于是每次请求都会打到数据库高并发下直接把 DB 打崩。比如商品 ID 是自增的攻击者专门请求一个不存在的负数 ID缓存查不到数据库每次也查不到流量全部穿透。处理方案有四种主流做法。一是缓存空值对不存在的数据也缓存一个空值并设置较短的过期时间比如 60 秒防止恶意请求持续打库。二是布隆过滤器请求先走过滤器判断 key 是否存在不存在直接返回因为存储层面用位数组千万级别的 key 也就占用几十 MB基本不会成为性能瓶颈。三是参数校验在入口拦截明显非法的 ID。四是数据库层的限流降级作为最后的兜底。我当时在项目里把方案一和方案三结合入口先校验 ID 合法性内存缓存再做一层短暂空值最后数据库前面加轻量级限流。空值缓存有个坑就是如果一个 key 本来不存在但后续真的插入数据了空值缓存还没过期可能会导致数据短时间不一致所以空值过期时间不能设太长我一般采用 30 到 60 秒。5.2 缓存击穿热点 key 过期瞬间被打爆缓存击穿和穿透很像但文件完全不同击穿是某个热点的 key 刚好失效比如weibo:hot这种一瞬间上万人同时去数据库查。解决思路是在“重建缓存”这个步骤上做控制保证同一时刻只有一个线程去查数据库其他人等待或直接用旧值。我用过三种具体办法互斥锁Redis SETNX发现缓存没值先尝试设置锁拿到锁的线程查库回填缓存没拿到锁的线程短暂 sleep 后重查缓存。缺点是加了等待对访问量稍大的接口延迟会上升。逻辑过期时间缓存里额外存一个过期逻辑时间比如hotkey:{value,expireAt}访问时如果发现逻辑过期只让一个线程去刷新其他线程仍返回旧值。这样能保证永远有数据返回但会造成短暂的数据不一致。永久热点 key 不过期后台任务定时主动刷新。最简单的做法但不适合需要实时性的数据。生产上我偏向用逻辑过期时间因为对调用方感知最小。一个要注意的坑互斥锁方案里如果查库线程异常崩溃锁没有被释放可能会造成死锁。所以锁必须设置过期时间通常是 3 到 5 秒还要加上唯一标识判断防止误删别人持有的锁这已经是分布式锁的雏形了和后面聊的锁问题天然连在一起。5.3 缓存雪崩大面积失效引发数据库洪峰雪崩和击穿的区别在于规模。击穿是单个热点 key 失效雪崩是大批 key 在同一时间失效或者 Redis 整个实例宕机后台流量直接倾泻到数据库。解决思路主要有三个方向第一过期时间打散。如果所有 key 都是固定 1 小时过期那一小时内某一秒就会集中大量过期非常容易触发雪崩。我为每条缓存数据加一个随机偏移比如 TTL 设置为base random(0,300)秒把过期时间均匀打散。第二多级缓存。在 Redis 前面加一层本地 Caffeine即使 Redis 挂了本地缓存还能扛住一部分给数据库争取恢复时间。第三Redis 高可用。一个 Redis 挂了还有其他主从或集群节点不至于全是单点的不可用。我第一次搭哨兵也是为了防雪崩虽然实际中 Redis 宕机概率低但一挂就是所有 key 同时失效确实是灾难级别的事故。5.4 双写一致性数据库与缓存的最终一致性缓存和数据库双写时最容易出现不一致的场景是先更新数据库再删除缓存这中间有一个时间窗其他请求可能读到旧缓存。反过来如果先删缓存再更新库更新库期间会有请求把旧值回填到缓存同样会造成不一致。我比较推荐的策略是 Cache Aside Pattern读的时候先读缓存没有就读库然后写缓存写的时候先更新库再删缓存。注意是“删缓存”而不是“更新缓存”因为更新缓存可能在并发下有脏数据覆盖问题删掉后让下次读请求自然回填反而更安全。如果想进一步降低数据不一致的概率可以用延迟双删更新库后删除缓存等几百毫秒再删除一次把并发期间可能回填的旧缓存再清一遍。这个几百毫秒的延迟时间要根据实际业务对一致性的要求调节我一般用 500ms。6. 高可用实践主从、哨兵、Cluster 集群6.1 主从复制的作用与异步机制主从复制解决了读写分离和基础容灾问题。主库负责写从库负责读从库同步是异步的默认情况下主库只要成功执行写命令就返回不等待从库确认所以从库会有一定延迟。replica-read-only yes默认打开从库不做写操作。异步复制带来的常见问题是主从数据延迟主库刚写的 key还没复制到从库读请求打到从库就查不到。这种场景下如果业务对强一致有要求就必须读主库不能用从库。我在做秒杀库存查询时就犯过这个错后来把读库存的入口直接指向主库才消除了瞬时超卖误报的问题。复制过程中如果主从断连从库会尝试增量同步增量复制不成功则退化为全量同步全量同步期间会生成 RDB 快照发给从库数据量大的时候对主库会有明显的 I/O 压力。所以初次建立主从还是放在业务低峰期为好否则最后被迫实践一把“复制风暴”。6.2 哨兵模式自动故障转移哨兵是一个独立进程专门用来监控 Redis 主库状态。当主库挂了哨兵会在从库中选出一个提升为新主库然后把其他从库的复制目标切到新主库。客户端也需要感知新的主库地址所以哨兵还承担了“服务发现”的功能。哨兵的“主观下线”是指单个哨兵发现主库心跳超时但它不会立刻切换要等超过一半的哨兵都认为主库下线才判定为“客观下线”然后进入故障转移流程。这就是为什么生产环境至少要部署三个哨兵实例否则两个哨兵里一个宕机剩下一个无法形成多数派故障转移就永远不触发。我在搭建哨兵时踩过一个浅坑哨兵配置文件里sentinel monitor mymaster 127.0.0.1 6379 2这个参数含义是“2 个哨兵判定下线就客观下线”如果你只有 1 个哨兵就算配了2也永远不触发自动切换。所以规则是哨兵数量必须大于 quorum。这个在纸上很好懂但真搭环境时特别容易忽略。6.3 Redis Cluster去中心化与槽位分配当单机内存和并发都到了瓶颈就需要 Redis Cluster。Cluster 模式把数据分布到多个主节点上默认有 16384 个哈希槽每个 key 通过CRC16(key) % 16384计算落到的槽位每个主节点负责一部分槽位。Cluster 最醒目的特点是去中心化每个节点都保存了完整的槽位映射关系客户端访问任意节点都能被重定向到正确的节点。重定向响应有两种MOVED 表示目标节点永久换了位置客户端需要更新本地缓存ASK 表示数据正在迁移中客户端只做这一次的临时转发。我在 K8s 里部署 Redis Cluster 的经验是别把每个 Pod 搞成有状态单独部署最好用 StatefulSet这样 Pod 的 DNS 名称和编号是稳定的Cluster 各节点之间可以通过服务名通信。pod 重启后 IP 可能会变如果没有稳定的域名整个 Cluster 拓扑会乱套那一次我花了整整一天反复cluster meet教训极深。6.4 可视化工具与服务连接RedisInsight 和 Another Redis Desktop Manager这个算不上核心原理但是开发日常的实际痛点。命令行工具redis-cli用顺手后可视化工具仍然是排查线上数据分布、查看 key 过期情况、内存分析的有效工具。我个人最常用的是 RedisInsight官方维护跨平台功能含 GUI 的 Redis 浏览器、慢日志分析、内存分析最实用的是内置的 CLI 面板。Another Redis Desktop Manager 免费且支持直连和 SSH 隧道方式在 Windows 下体验流畅部署到公司内网环境也常见。无论用哪个工具连接之前先明确认证信息和安全配置默认没设 requirepass 时可以直接连设了密码要正确填写生产环境最好确认一下连接走的是 6379 端口还是内部 SGW 端口有时候网络层做了隔离。工具连不上时还有一个最常见的排查点目标 Redis 是否绑定了bind 127.0.0.1如果只绑了本地远程工具是无论如何连不上的。需要改成bind 0.0.0.0或指定网卡同时要注意网络安全不要裸奔到公网。这个问题在主机或者虚拟机里尤其常见。6.5 单机部署与配置调优速记单机部署不等于把安装包跑起来就完事。我整理了一份适合本机/小团队初始化的配置清单daemonize yes后台运行避免被终端挂掉。requirepass设置访问密码。maxmemory设置最大内存比如 2gb。不加这个配置时Redis 默认在 64 位系统里是无限制的等内存扛不住就可能触发操作系统 OOM非常被动。maxmemory-policy内存上限到达后怎么淘汰默认noeviction是不淘汰直接报错所以必须显式配置。maxmemory-policy里我常用的几种allkeys-lru适合当作纯缓存把所有 key 一起按最近最少使用淘汰allkeys-random适合访问分布均匀的情况volatile-lru只淘汰带 TTL 的 key适合保留那些不想被淘汰的数据。这个选择本质上决定 Redis 的角色它是“缓存”还是“数据库”。如果你把它当数据库用那 maxmemory-policy 一般就不应该是淘汰策略而应该用 Redis 自身的持久化 外部存储兜底。7. 分布式锁从 SETNX 到 Redisson 看门狗7.1 为什么单机锁不够用SETNX 怎么演变在单机单进程里可以用 Java 的 synchronized 或者 JUC 的 Lock但在多服务实例部署或跨进程共享资源时就不行了。分布式锁要求多个进程之间能互斥访问某个资源同一时刻只允许一个客户端持有锁。最早大家用SETNX lock unique_value但只 SETNX 太简陋。假设持有锁的进程崩溃了锁永远不释放其他进程就得死等。于是有人给锁加过期时间SETEX lock 30 unique_value却又带来另一个问题如果线程 A 执行任务超过了 30 秒锁自动过期线程 B 拿到锁此时 A 执行完又去释放锁会把 B 刚刚持有的锁误删掉。正确的解除方式是用 Redis 官方推荐的原子命令SET lock unique_value NX EX 30释放锁时先比对 value 是本人持有再 DEL中间要保证原子性所以释放操作要用 Lua 脚本实现if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这个方案我踩过坑Java 生成了唯一标识后我没用 Lua而是先 GET 再 DEL中间间隔了几毫秒并发压测时真出现了误删锁后来才明白 GET 和 DEL 两条命令放在一起必须保证原子性否则永远有隐患。7.2 Redisson 的看门狗机制与公平锁手写锁要解决锁续期、重入、阻塞等待这些复杂问题非常繁琐。生产环境我更推荐用 Redisson 库它的核心卖点是看门狗机制客户端拿到锁后后台守护线程会不断给锁续期比如默认锁超时时间为 30 秒每 10 秒续订一次保证业务任务还没执行完锁不会因为过期被其他线程趁虚而入。这就是为什么建议在真实业务中不要用“固定 30 秒过期”的手写方案而是用lock redisson.getLock(order:pay); lock.lock();如果业务执行时间极长看门狗会一直续期直到业务线程 finally 里 unlock。如果 Redisson 客户端进程宕机守护线程也就停了锁会自然超过 30 秒后过期释放。Redisson 还提供tryLock(waitTime, leaseTime, TimeUnit)支持拿锁等待超时而不是傻等以及getFairLock公平锁让请求按队列顺序拿锁适合低并发的强公平场景。不过公平锁性能不如普通锁我个人用到的情况不多。7.3 主从复制场景下锁的安全性与 RedLock如果锁建立在主从架构上有一个极端风险客户端 A 在主库上拿到锁主库还没来得及同步到从库就挂了哨兵把从库提升为主库此时客户端 B 再来拿锁新主库上其实没有锁记录于是 A 和 B 同时有锁分布式锁失效。针对这一问题Redis 官方提出了 RedLock 算法思路在 N 个彼此独立的 Redis 节点上依次加锁大部分节点N/21加锁成功客户端才认为自己成功获得锁。但 RedLock 本身在业界也有争议因为它依赖绝对时钟和网络复杂度高。我个人的建议是对绝大多数业务来说主从切换瞬间双锁存在的概率极低业务幂等校验比如数据库唯一索引比再去引入 RedLock 更实在。分布式锁不是玄学它只是性能兜底最终的数据正确性还要靠业务层的唯一约束和幂等设计。7.4 分布式锁常见面试案例分析面试中这个点经常被延伸出三个问题我都亲自回答过锁过期时间该怎么设理想是业务预估最大执行时间的 3 倍以上但更稳妥是用看门狗自动续期不要依赖固定时间。业务执行时间超过锁的过期时间怎么办绝不要为了让锁等任务而把过期时间设得无限大正确做法是续期或逻辑分段。锁释放时误删其他线程的锁怎么避免保存唯一标识释放前比对比对和删除要保证原子性。以上三点我在手写锁方案里全踩过一遍后来老老实实切换到了 Redisson线上再也没出过锁相关的事故。8. 命令行超时、慢日志与连接工具排错记录8.1 错误提示 RedisCommandTimeoutException 排查全过程工作里最常见的报错是Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException。刚看到这个异常很多人会第一时间想是不是 Redis 挂了但 redis-cli 单独执行命令都很快说明服务本身是健康的问题更大概率出在应用连接 Redis 的链路或客户端的超时配置上。我从这个异常中学到了系统性的排查顺序看 Redis 服务端状态内存使用率、连接的客户端数量、慢日志。如果内存快满可能触发淘汰风暴导致命令执行变慢。看网络层应用服务器到 Redis 的 RTT 是否正常是不是跨机房、跨 VPC。我遇到过一款内部老项目把 Redis 放在另一地域即使 ping 延迟只有 5ms因为 TCP 连接数暴增整体吞吐也起不来。看客户端配置Lettuce 默认超时时间比较短要确认客户端的 commandTimeout 是否与业务模型的响应时长匹配。如果缓存查询接口要求 QPS 高超时时间设置过短也会频繁误判。看 Redis 连接池和响应算子如果代码里某一循环并发获取连接连接池被打满新命令会等连接释放直到超过默认超时时间。最后那次事故定位到的是一个后台报表查询它循环拿 key 去批量 MGET结果 key 数量过大底层 Redis 单线程执行 MGET 时其他命令全部排队表现就是 Lettuce 客户端大面积命令超时。解决办法是把大 MGET 拆成小批次同时给 Redis 加监控看慢查询曲线。8.2 慢日志定位性能奇点Redis 单线程模型意味着一个慢命令就能拖慢整个实例上所有的 key这也是为什么排查超时的时候要优先看慢查询。SLOWLOG GET 20可以取最近 20 条慢命令SLOWLOG RESET清零历史记录。我在线上遇到过用KEYS pattern*在几十万 key 的库里去匹配前缀的代码直接把 Redis 卡了近 10 秒。这个命令会遍历全部 key千万不要在生产环境使用。解决办法是SCAN cursor MATCH pattern通过游标迭代每次只返回一小批不阻塞服务。同类型的坑包括大 key单个 string 超过 100KB在 GET/DEL 时会产生明显的阻塞以及SMEMBERS在超大的 Set 上一次全量取出。以上都是“命令设计”层面的问题和 Redis 本身没有关系。8.3 Redis Desktop Manager 连接慢与连不上优先查四件事普通开发者在本地排查 Redis 连接问题时最爱用可视化客户端一旦连不上会非常沮丧。我建议按顺序排查四个点bind 配置服务端没监听目标网络接口。requirepass 配置客户端密码未配置或错误。防火墙/安全组端口 6379 被拦截。本机虽然没防火墙但云服务器默认安全组规则很可能没放行。Redis 服务是不是真的在运行Windows 上最常见的是redis-server进程被关Linux 上可能是没配 daemonize 导致一关终端就退出。8.4 Docker 容器内连接 Redis 的小技巧平时用 Docker 体验 Redis不需要总去查容器 IP。在同一宿主机上用docker exec -it redis 容器名 redis-cli直接进入容器内命令行也可以从本机用-p 6379:6379映射后通过宿主 127.0.0.1 去连。如果想在另一个容器内访问 Redis就用上面提到的--network自定义桥接网络并直接使用容器名作为 hostname。这几个方法我在多个环境里使用的频率非常高建议记下来。9. 面试八股清单与高频考点自查9.1 为什么 Redis 是单线程却这么快很多人会答“因为内存操作快”这只是一个面。真正要完整说清需要覆盖这几点Redis 命令执行是单线程避免了多线程上下文切换和锁竞争这是设计上的取舍。底层基于 I/O 多路复用支持大量客户端连接有事件驱动机制来处理网络读写不会因为阻塞等待拖慢服务。内存中存储数据天然省去磁盘 I/O。数据结构经过专门设计比如 SDS、跳表、压缩列表根据数据规模自动选择更高效的内存布局减少访问开销。但要补充一句Redis 6.0 以后引入了多线程 I/O但那不是用来执行命令的而是把网络数据读写从主线程分离命令执行仍然单线程。这点在面试里及时补充会让回答更完整。9.2 Redis 过期删除策略与内存淘汰策略过期键的删除不是定时的而是惰性删除 定期删除的组合。惰性删除指当 key 被访问时才发现过期再删定期删除指 Redis 每隔一段很短的时间抽查一批带过期时间的 key删掉其中已过期的部分。这套组合的目的是平衡 CPU 占用和内存占用。如果过期 key 没来得及删掉且内存到达 maxmemory就需要由淘汰策略来决定怎么腾空间。面试里被问到淘汰策略时不能只背名字至少要结合业务说明理由。比如一个“用户最近浏览记录”场景适合allkeys-lru一个“排行榜且不允许淘汰”的场景就不要设置 maxmemory-policy 的淘汰而是配合持久化和外部扩容来做。9.3 热词里的“Jedis、Lettuce、Redisson 有什么区别”这个问题在 Redis 客户端选型时经常被问到。Jedis 是老牌客户端直连 Redis线程不安全需要使用连接池管理Lettuce 默认基于 Netty连接可以共享支持同步、异步和响应式 APISpring Boot 2 后的默认客户端Redisson 是分布式工具集封装了锁、队列、分布式对象等高级功能但它的底层更多面向加锁和分布式协作场景而不是做最基础的数据读写传输。我自己的经验是普通 CRUD 缓存场景用 Lettuce 就够数据读写简单稳定分布式锁、多级缓存需要更丰富的分布式数据结构时再引入 RedissonJedis 在很多遗留项目里还在用它的 API 风格更贴近原生命令学习 Redis 命令时也可以当参考。9.4 Redis 事务与 MULTI 的局限性Redis 事务和关系型数据库事务完全不同。MULTI开启之后命令进入队列EXEC一次性执行。它的重点是不支持回滚执行过程中如果某条命令因为语法错误或类型错误失败其他命令照常执行。这是设计上的选择因为 Redis 认为命令错误应该在开发阶段就发现不需要运行时回滚。相关进阶问题时还会问到 WATCH 命令它带乐观锁语义在WATCH key之后EXEC 前如果 key 被其他客户端修改事务会被打断返回 nil。过去做秒杀扣减库存就有人用 WATCH 循环重试来保证一致性但现在性能和代码复杂度上都不如直接用 Lua 脚本或 Redis 原子命令来得干净。9.5 Redis 做中间件与消息队列的边界热词里还有“redis做中间件”和“redis做消息队列”这是我实战中比较有感受的一块。Redis 的 List 和发布订阅确实可以模拟简单任务队列但和真正消息中间件如 Kafka、RabbitMQ的边界很清晰Redis 的 PUB/SUB 消息即发即弃订阅者离线后消息就丢了没有持久化回放机制。List 的 BRPOPLPUSH 可以充当可靠队列把消息从一个 list 转到一个备份 list消费者处理完再删除但这要自己保证消息处理的事务边界实现成本高。Redis Stream 5.0 之后提供了更完整的消息模型支持消费者组、pending 列表、ACK很多轻量场景可以替代 Kafka。我做过一个订单事件流转的小项目用 Stream XREADGROUP 消费消息能实现小型业务的异步解耦。如果消息量不大、业务延迟容忍度还行Redis Stream 完全能胜任省去维护独立消息队列集群的复杂度。但这不意味着它要能与 Kafka 有上百万吞吐的场景竞争选型时心里要有一杆秤。10. 日常常用命令清单与速查表每次切换环境或者隔一段时间不碰 Redis总会忘掉某个命令的细节。我把高频命令整理成了一张速查表贴在代码仓库 README 里很管用10.1 基础命令redis-server启动服务端。带配置文件启动redis-server /path/redis.conf。redis-cli -h host -p port -a password带密码连接。redis-cli --raw输出原始字符串不再带包裹引号看中文时体验好。redis-cli --bigkeys扫描大 key特别适合初查缓存热点与 OOM。redis-cli --scan --pattern user:*安全遍历 key别再用 KEYS。10.2 数据类型命令快速分类通用EXISTS、TTL、EXPIRE key 秒、PERSIST key、TYPE key。StringGET、SET、GETSET、MSET、MGET、INCR、DECR、STRLEN。HashHSET、HGET、HGETALL、HINCRBY、HDEL、HEXISTS。ListLPUSH、RPUSH、LPOP、RPOP、LRANGE、LLEN、BLPOP、BRPOP。SetSADD、SREM、SMEMBERS注意大 Set 会阻塞、SISMEMBER、SINTER、SUNION。ZSetZADD、ZRANGE、ZREVRANGE、ZSCORE、ZINCRBY、ZREM、ZRANK。StreamXADD、XLEN、XREAD、XGROUP、XREADGROUP、XACK。10.3 性能诊断命令INFO内存、连接、持久化、主从复制的全量状态排查问题第一步。INFO commandstats命令调用次数和耗时统计能看出哪个命令拖慢了服务。SLOWLOG GET慢查询日志。MONITOR实时打印所有命令调试阶段使用绝不能在高流量长时间开着因为会拖垮 Redis 性能。CONFIG GET parameter在线查看配置项CONFIG SET parameter value可以临时改配置但不持久化。我在每次面试或实际调优前会过一遍这个速查表基本都是条件反射式的记忆。用多了之后会发现命令本身只是接入 Redis 世界的最小触点真正的知识密度在内存模型、持久化、复制协议、内存淘汰和系统设计这些更深的地方。11. 个人体感与后续打算如果不是亲手搭过一遍主从、见到过缓存穿透把数据库打崩的现场、被 Redis 超时异常折腾到凌晨我对 Redis 的理解很可能一直停留在“能 set/get、能排行榜”的层面。这个项目日志记录到现在最大的收获不是会了几条命令而是学会了如何看待一个缓存系统单线程模型决定了命令设计必须克制内存有限意味着每个 key 都要算成本异步复制和持久化之间永远存在一致性与性能的博弈。这些认知在实际工作中比背十个命令有价值得多。最后再分享两个我个人的实操习惯第一任何 Redis 环境变更先用CONFIG GET save和CONFIG GET appendonly确认持久化策略再动数据操作防止“测试没问题一到生产重启就丢数据”第二可视化工具可以帮你看数据但排查慢命令、分析大 key、理清主从关系时还是回去看redis-cli的输出命令行给你的信息最诚实也最精确。后面我打算接着补充 Redis 7.0 的新特性、多线程 I/O 下的性能调优以及 Cluster 迁移演练场景日志还会持续更新有同样在学 Redis 的朋友欢迎一起交流踩坑经验。
返回列表