恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
千万级Key的Redis客户端为何卡顿?从SCAN到虚拟列表的优化解析
首页
资讯中心
/
千万级Key的Redis客户端为何卡顿?从SCAN到虚拟列表的优化解析
千万级Key的Redis客户端为何卡顿?从SCAN到虚拟列表的优化解析
发布时间:2026/9/16 7:17:21
先讲一个我实际碰到的场景。去年有个业务团队的 Redis 实例涨到了 1200 万个 Key他们用 Redis Desktop Manager 连上去界面直接卡在白屏转圈转了一分钟最后只能强杀进程。团队的第一反应是数据量太大了这软件带不动。我当时的判断是不是 Redis 带不动是客户端的实现方式在百万级就已经开始勉强到千万级只是把纸糊的窗户捅破而已。同样连一个千万 Key 的 Redis有的桌面客户端能秒开、能翻页、能搜索有的连上就崩差距不在 Redis 本身而在客户端对这四件事的处理怎么取 Key、怎么渲染 Key、怎么编排网络请求、怎么管理内存。这篇文章就把这些账一笔一笔算清楚。1. 千万 Key 的 Redis客户端卡死在哪个环节1.1 一次全量拉取的三笔大账先从最典型的烂实现说起——很多桌面客户端打开连接后的第一反应是把所有 Key 捞回来显示成一棵树。这个流程里至少有三笔账是必亏的。第一笔是遍历账。最常见的捞 Key 方式是 KEYS *。如果你对 Redis 命令手册还有印象KEYS 是一个 O(N) 命令而且在 Redis 的主线程上执行。Redis 是单线程处理命令在 KEYS 遍历期间其他所有读写命令全都排队等待。一千万个 Key 的情况下一条 KEYS * 能让一个正常配置的实例卡住几秒甚至几十秒。生产环境跑一遍 KEYS *等于亲手按下暂停键所以老工程师看到生产环境日志里有 KEYS 命令血压基本都会上来。第二笔是传输账。假设一个 Key 名称平均长度 60 字节一千万个 Key 光 Key 名就要 600MB。如果客户端再贴心地把每个 Key 的 value 也拉回来按平均 100 字节算又是 1GB 多的流量。这还没算 RESP 协议本身的冗余RESP 协议里每条分段数据都要带长度前缀和换行符实际流量会比估算值更高。一个桌面软件启动就要服务器发将近 2GB 数据过来网卡、内存、UI 渲染全都会被拖垮。第三笔是渲染账。即使 Key 数据全部传输完客户端还要渲染到界面上。老的桌面客户端用的是树控件或表格控件一个 Key 对应一行。不要小看这个一行一千万行的每一行都需要创建、布局、绘制内存和时间开销都很可观。Qt 的 QTreeWidget 或者 .NET 的 TreeView 在这种规模下会直接把内存吃满。界面白屏、无响应本质就是 UI 线程在做一个根本不可能完成的大活。除了这三笔账还有一个更容易被忽略的开销数据库编号遍历。Redis 默认有 16 个 dbdb0 到 db15有些客户端为了展示每个 db 里的 Key 数量启动时会逐个 db 执行 DBSIZE。DBSIZE 本身是 O(1) 命令不慢但如果客户端对每个 db 都做全量加载那等于把上面三笔账又乘以 16虽然实际生产环境大多只用 db0但架不住客户端实现得不够聪明。1.2 老牌工具在千万量级翻车的必然性这里不是要贬低 Redis Desktop Manager 这个老牌开源项目它在 Redis 还停留在百万级 Key 的时代是很优秀的工具。但时代变了业务缓存的 Key 越来越细分片越来越多千万级 Key 的实例已经很多见。老牌客户端的设计思路是连接即加载启动时就统计数据再把 Key 列表一次性读入。在小数据量下这种思路体验很好还很一目了然到了千万级连接即加载就变成了连接即崩溃。我实际遇到的情况是RDM 打开一个 500 万 Key 的实例内存占用能从 300MB 一路飙到 4GB 以上切到树形视图还会出现明显卡顿。把这种体验放到 1200 万 Key 的现场白屏强杀就是必然结局。大家如果想自己复现用 Docker 或者官方 Windows 版 Redis 起一个本地实例循环写入几百万个小 Key再拿 RDM 连一次几乎都能看到内存飙升的过程。这一眼就能理解凭什么有的客户端不卡这个问题的分量。2. 拉开差距的第一个技术分水岭SCAN 游标迭代2.1 为什么 SCAN 是不卡的前提从服务端取 Key 的正确姿势是用 SCAN 命令做游标式迭代。SCAN 每次调用只返回一部分 Key同时返回一个游标cursor客户端下次带上这个游标继续取直到游标变成 0遍历结束。这样做有两个关键好处。第一SCAN 不是一次性遍历完整个键空间它每次只扫描 Redis 哈希表里的一部分桶单条命令的执行耗时被限制在一个很小的量级不会长时间占用 Redis 主线程生产环境里可以放心用。第二桌面客户端不需要把一千万个 Key 一次性收到内存里而是一批一批地拿每批一千或者两千个拿到一批先展示一批。我用一个内部巡检脚本做过对比用 KEYS * 去扫描一个 800 万 Key 的实例时慢日志里能看到一条耗时 8000ms 的命令后来改成 SCAN 迭代count 设置 1000最耗时的单次 SCAN 也不过十几毫秒。同一个实例、同一个匹配模式只是取数方式不同对整个系统的影响天差地别。2.2 COUNT 参数的真实含义与调参经验SCAN 命令里有一个 COUNT 参数很多人的理解是一次返回多少个 Key。这个理解不够准确。COUNT 的真实含义是本次迭代要扫描多少个哈希桶槽位它是一个工作量提示并不是返回条目数上限。Redis 会根据 COUNT 去扫描对应数量的槽位槽位里有多少个 Key 就返回多少个返回数量可能大于 COUNT、也可能小于 COUNT因为有些槽位是空的。这个细节对调参非常关键。COUNT 设得太小比如默认 10遍历千万级 Key 需要很多次网络往返远程实例的 RTT 会被放大几千倍加载过程会显得慢吞吞。COUNT 设得太大比如 10 万单次 SCAN 会在服务端扫描大量槽位耗时上升客户端也会一次收到大量 Key内存和 UI 压力又上来了。我在日常使用和写工具时一般从 1000 起步再根据服务器 CPU 负载和客户端流畅度调整到 2000 或 5000。如果你连的是远程实例网络延迟高可以适当调大 COUNT 来减少往返次数如果连的是本机实例COUNT 保持在 500 以下体感也不差。使用 SCAN 时还有一个很常见的终止条件判断需要特别留意只要 cursor 没有变成 0即使这次返回的 keys 列表是空的也必须继续迭代。2.3 SCAN 的隐藏坑空迭代、重复 Key 与模式匹配SCAN 看起来简单踩坑的地方一点都不少。第一个坑是空迭代。因为 SCAN 每轮扫描的槽位可能大量是空的所以会出现游标不为 0 但返回的 key 列表是空的这种情况。如果客户端写成返回为空就结束一千万个 Key 可能只取到一小部分就停了。正确的终止条件只有一个cursor 变成 0。第二个坑是重复 Key。在遍历过程中如果有其他客户端并发新增或删除 KeySCAN 返回的结果里有可能出现重复。这个概率不大但做管理工具时一定要去重否则用户在列表里看到同一个 Key 出现两次会以为是工具坏了。第三个坑和 MATCH 模式有关。MATCH 是服务端的匹配过滤不假但它作用在已经扫描的槽位结果上不会提前终止遍历。举个例子你匹配 user:*实际匹配上的 Key 可能只有 100 个但如果这 100 个 Key 散落在千万级键空间里SCAN 依然要把整个键空间完整走一遍才会返回游标 0。客户端如果用 MATCH 做过滤界面上的加载进度条会长时间停在加载中这不是工具坏了而是全量遍历本身就需要那么久。3. 从全部拿到再显示到显示什么才拿什么UI 工程改命3.1 虚拟列表一千万条数据只渲染 30 条无论服务端怎么取数客户端 UI 层还有一个绕不开的问题即使很优雅地把一千万个 Key 都拿到了内存里也不可能把它们一次渲染到屏幕上。桌面窗口就那么大能同时看到的大概只有 30 到 50 行。把一千万个控件都创建出来是极大的浪费因为大部分控件用户根本不会看到。所以好的客户端都会用虚拟列表方案。虚拟列表的核心思想很朴素只渲染可视区域里的那 30 行滚轮往下滚动时回收离开视口的行把新数据复用给进入视口的行。无论底层数据是一万条还是一千万条UI 层维护的控件数量始终是几十个。这就是你滚动一个流畅的 Redis 客户端时列表唰唰唰跟手却不掉帧的直接原因。很多开源客户端比如 Another Redis Desktop Manager、Redis Insight都是这种方案。它们表面上给你一种我加载了所有 Key的感觉实际上只是在视图里懒加载当前需要看的那一部分。这一点在千万 Key 场景下是用户体验的分水岭前者是秒开后者是转圈。3.2 懒加载与类型探测把最贵的请求留到用户点击时Key 列表本身只是名称真正贵的是 value 的取回。有些客户端为了在列表里显示类型这一列启动时会逐个给每个 Key 发 TYPE 命令。一个 Key 一条命令一千万个 Key 就是一千万条命令哪怕走 pipeline这也是巨大的开销。聪明的做法是把类型探测和 value 获取都改成懒加载。列表区域可以统一显示未知类型或者干脆不显示类型列等用户双击一个 Key 或展开一个节点时客户端才发送 TYPE、TTL、GET 或者 HGETALL 等命令。对大多数用户来说真正会双击查看详情的 Key 可能只有总数的百分之一甚至千分之一这一招能省下 99% 的请求量。我在用 ARDM 的时候观察过它的网络行为在 Key 列表阶段它几乎不会主动请求 value一旦双击响应很快就出来了。这就是典型的按需加载设计。展开一个 Hash 时用 HSCAN 分批取字段展开一个 List 时用 LRANGE 取前几十条而不是一次性把所有数据都灌到界面上。这种设计的代价是少了一些即时感但换来的是千万 Key 下还能继续操作的能力。3.3 搜索过滤先分清楚搜的是已加载还是全量千万 Key 场景下客户端的搜索框同样是个分水岭。如果在已加载的 Key 列表里做客户端本地过滤只要列表本身已经加载本地过滤速度通常很快。很多客户端就是这么做的体验也还不错。但要小心那种输入关键字就重新去服务器拉一遍的实现。如果每敲一个字符就去执行一次 SCAN MATCHSCAN 请求会跟着键盘事件连续轰炸服务器输入法连续触发时查询压力成倍叠加。好的做法是给搜索加防抖用户停止输入几百毫秒后再发起一次 SCAN匹配工作交给服务端的 MATCH 参数或者干脆在已加载的本地数据里过滤本地没有结果时再回源拉取。判断一个客户端是否专业看搜索框在千万 Key 下的行为就能看个大概。4. 网络层与二进制编排看不见的性能大头4.1 多路复用、pipeline 与连接池到了千万 Key 规模客户端和 Redis 之间的网络交互不可能靠一个连接、一条命令、一条命令地等撑住。远程部署场景下一次 RTT往返时延至少 1-10ms如果客户端逐个命令等待做一次 1000 次小操作就可能要几秒钟。现代客户端普遍会做三件事来对抗这个问题。第一连接池同时保持多个可用连接避免频繁建连和断连。第二异步多路复用发送命令后不阻塞 UI 线程响应回来再回调这样即使网络超时界面也不会失去响应。第三pipeline把一批互不依赖的命令打包到一次请求里发送减少网络往返次数。哪怕只是批量读取几百个 Key 的 TTL用 pipeline 和逐个读取的耗时差距就能达到一个数量级。桌面客户端要保证不卡网络层必须把 UI 线程和 IO 线程彻底分离。否则你拖动窗口时一个网络超时就能让整个界面卡住。一款成熟的客户端在架构上一定不是发命令 - 等结果 - 刷新界面这种串行模型而是发命令 - 立刻返回 - 结果回来再更新的异步模型。4.2 大 value 是隐藏杀手一个 Key 拖垮整个界面除了 Key 数量巨大另一个在千万 Key 实例上常见的现象是大 value。有些业务喜欢把几百 KB 甚至几 MB 的对象直接塞进一个 String Key 里当作配置项使用。如果客户端在加载列表时不小心把大 value 也取回来一个 10MB 的 value 就够让界面卡顿好几秒更别提一页 50 个 Key 里混着几个大 value 的效果。我处理过一个典型案例业务方把一个 3MB 的 JSON 字符串存进 Key客户端加载列表本身不慢但用户一双击这个 Key界面直接假死 5 秒。根本原因不是那 3MB 传输慢而是客户端把整个 value 反序列化后渲染成一个大表格。专业客户端的做法是默认截断摘要String 型 value 只显示前几百字节完整内容点击加载全部再取Hash 型用 HSCAN 分页而不是 HGETALL 全量取字段List 型用 LRANGE 取前 N 条Set 型用 SSCAN 迭代ZSet 型用 ZRANGEBYSCORE 按分数段取。这些策略的核心逻辑是一致的无论这个 Key 本身多大我只取能填满当前界面所需的量。4.3 序列化方式如何影响客户端的显示与性能这一条和热词里反复出现的redis序列化直接相关。业务系统写入 Redis 时如果用的是 JDK 原生序列化数据里会带着大量类描述信息体积膨胀严重还经常出现二进制乱码。客户端拉回来后要么显示一堆不可读的内容要么需要反序列化之后才能展示又增加了解析成本。我对比过同一个业务对象JDK 序列化之后存进 Redis 的体积比 JSON 方案大接近 3 到 4 倍。同样的千万 Key 量级value 体积越大客户端加载越吃力。我的建议是只要客户端还需要人肉去排查数据写入端就尽量采用体积小、可读性好的序列化方式比如 JSON、MessagePack、Protobuf如果是长期不读的日志型二进制数据再考虑原生二进制存储。这不仅是给客户端减负也是给 Redis 内存和带宽减负。5. 主流桌面客户端实测对比与选型参考5.1 四款客户端的加载策略与实际体感我最近在一台内存 32GB、网络很稳定的机器上用 1000 万 Key 的实例做了一次桌面客户端横向对比。结果算不上严格的基准测试但很能说明问题覆盖的工具正好也是大家平时搜得最多的几款Redis Desktop Manager、Another Redis Desktop Manager、Redis Insight 和 Tiny RDM。客户端加载策略千万 Key 下实际体感内存占用趋势适合场景RDM连接后加载 Key 到树接近全量打开极慢树展开卡顿有白屏风险随加载明显攀升可达数 GB小规模实例快速查看ARDMSCAN 分批拉取 Key虚拟列表展示首屏秒开翻页顺滑增长平缓保持低位千万级日常管理维护主力Redis InsightSCAN 分页加官方分析加载不错分析功能强大中等做诊断分析、慢查询溯源Tiny RDM按需加载轻量实现表现流畅低轻量运维、快速操作需要说明的是这组对比依赖的实例是单机主从模式远程网络延迟约 2ms只使用了 db0。如果你的实例开了 AOF 重写、切片集群或有很多大的 Hash Key具体表现会有差异但加载策略决定卡不卡这个结论基本是成立的。RDM 在千万 Key 下翻车不是 Redis 变大了而是它仍然停留在我全都要的旧时代。5.2 衡量一个客户端能不能扛住千万 Key我只看三点很多人问我选客户端到底看什么我的判断标准就三条。第一看它首屏是不是只加载少量数据打开连接后是立刻出现列表还是转圈等加载全部这一个动作就够区分了。第二看滚动时是否复用控件把列表快速滚到底如果内存没有明显暴涨说明做了虚拟列表。第三看它取 value 的时机不双击绝不多发命令这是最节约资源的做法。平时我自己的组合是这样的日常快速看 Key、删 Key、改 TTL用 ARDM需要分析内存碎片、查看大 Key 占用、慢查询用 Redis Insight生产环境里临时排查又不想装软件时写一个带简单 Web 页面的本地小脚本用 SCAN 分页查。这套组合跑了大半年没有遇到过客户端卡死的情况。一个小技巧连接远程 Redis 时如果业务的 Key 前缀规划得比较干净比如 user:、order:、goods:我会直接在客户端里按前缀过滤而不是每次全量 SCAN 千万 Key。前缀过滤能把要遍历的目标缩小一个甚至两个数量级配合 COUNT 设置查询体验会明显更好。6. 配套治理经验千万 Key 环境下的客户端友好度优化6.1 大 Key 与过期 Key 治理前面聊了客户端怎么做才不卡反过来Redis 这一侧的卫生情况也直接决定客户端体验。在我接触的千万 Key 实例里最常见的问题是大 Key 和过期 Key 堆积。大 Key 的害处很好理解单 Key value 过大客户端展开详情时会明显变慢Redis 主线程在大 Key 的很多操作上也会卡比如 DEL、SDIFF、SUNION。处理办法是拆分比如把一个大 Hash 拆成多个小 Hash或者按时间分桶。另外删除大 Key 时不要用 DEL标准做法是用 UNLINK让回收在后台线程完成避免阻塞主线程。过期 Key 的影响要隐蔽一些。大量带 TTL 的 Key 到达过期时间后会触发惰性删除和周期删除内存碎片率上升客户端 SCAN 遍历时偶尔会碰到正在清理的 Key。虽然这不是客户端卡顿的主因但不治理的话实例整体响应会慢慢变差不管连哪个客户端都会觉得偶发抖动。我一般会开启 maxmemory-policy根据业务形态调整hz和过期清理参数并且定期用 SCAN 找出明显无用的 Key 批量清理。6.2 命名空间设计分布式锁与缓存治理的连带影响热词里频繁出现的redis分布式锁和redis缓存治理跟客户端体验也有很强的连带关系。分布式锁的 Key 通常很小但如果业务方把锁 Key 和无锁业务的 Key 混在同一个库运维时想筛选锁 Key 就要遍历全库。更麻烦的是如果有人在排查时习惯性用 KEYSlock去搜锁 Key就直接退回到本文开头那个问题整个实例都会被卡住。正确做法是给锁 Key 单独设计前缀比如lock:运维时用 SCAN MATCHlock:*过滤如果锁规模很大可以把锁 Key 记录到一个专门的配置 Key小集合里查锁时直接读小集合完全不用遍历全库。缓存治理也是一样。一个健康的缓存实例Key 总数应该与业务体量匹配而不是无限膨胀。我见过很多 Key 爆炸的案例罪魁祸首都是没设 TTL 的临时 Key。这类 Key 越积越多客户端 SCAN 要遍历的范围越来越大用户体感越来越差。定期治理、控制 TTL、清理通配前缀的僵尸 Key是让客户端不卡的基础条件。6.3 如果面试问到你为什么生产环境不能用 KEYS热词里有redis面试题我想专门回应一下。千万 Key 的 Redis桌面客户端凭什么不卡这个问题本身就很适合当面试题。候选人能说清楚 KEYS 是 O(N) 阻塞命令、SCAN 是游标迭代、COUNT 是槽位工作量提示、虚拟列表是 UI 层的关键、TYPE 探测要懒加载、大 value 要分页截断、序列化会影响体积与读取性能基本就把 Redis 性能优化的一条链路答通了。如果面试官只问为什么生产环境不让我用 KEYS用这篇文章里的计算方式就能讲透主线程阻塞 - 千万级流量放大 - 客户端内存崩溃三步递进。这样答下来比背十道八股文都有说服力因为它背后是一整套真实的性能决策逻辑。最后再分享一点我自己的体会。很多人遇到客户端卡第一反应是换一个性能更好的软件。其实软件只是一个壳壳下面那套取数策略才是灵魂。理解了 SCAN 迭代、虚拟列表、懒加载、网络分层这四件事你不仅会选客户端还会写自己的排查工具更能在 Redis 实例真正出现瓶颈时准确判断问题到底出在服务端还是客户端。这个判断能力才是比任何客户端都值钱的东西。