Redis中渐进式命令scan详解与使用
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">一、为什么需要 scan?先看 keys 命令的 “坑”</a></li><li><a href="#_label1">二、scan 命令的基础用法:从语法到示例</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_0">2.1 核心语法</a></li><li><a href="#_lab2_1_1">2.2 基础示例:遍历所有键</a></li><li><a href="#_lab2_1_2">2.3 过滤场景:MATCH 与 TYPE 的使用</a></li><ul class="third_class_ul"><li><a href="#_label3_1_2_0">场景 1:匹配 “user:” 前缀的键</a></li><li><a href="#_label3_1_2_1">场景 2:只遍历 string 类型的键</a></li></ul></ul><li><a href="#_label2">三、scan 的核心原理:为什么能 “渐进式” 遍历?</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_3">3.1 Redis 的键空间存储:哈希表</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_2_4">3.2 游标与 “高位进位”</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_2_5">3.3 COUNT 参数的 “预期” 特性</a></li><ul class="third_class_ul"></ul></ul><li><a href="#_label3">四、scan 家族命令:不止遍历键</a></li><ul class="second_class_ul"><li><a href="#_lab2_3_6">示例:HSCAN 遍历 Hash 字段</a></li><ul class="third_class_ul"></ul></ul><li><a href="#_label4">五、使用 scan 的注意事项与常见误区</a></li><ul class="second_class_ul"><li><a href="#_lab2_4_7">5.1 避免 “重复” 与 “遗漏”</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_4_8">5.2 合理设置 COUNT 参数</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_4_9">5.3 避免在遍历中使用复杂过滤</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_4_10">5.4 不要依赖返回结果的顺序</a></li><ul class="third_class_ul"></ul></ul></ul></div><blockquote><p>在 Redis 日常开发中,我们经常需要遍历数据库中的键或集合中的元素。传统的keys命令虽然简单直接,但在数据量较大时会严重阻塞 Redis 服务,甚至引发生产事故。而scan命令作为 Redis 2.8 版本引入的渐进式遍历方案,完美解决了这一痛点。</p></blockquote><p class="maodian"><a name="_label0"></a></p><h2>一、为什么需要 scan?先看 keys 命令的 “坑”</h2>
<p>在了解<code>scan</code>之前,我们首先要明确:为什么不能在生产环境中随意使用<code>keys</code>命令?</p>
<p><code>keys</code>命令的工作方式是<strong>全量遍历</strong>—— 它会一次性遍历 Redis 中的所有键,并将符合模式的结果全部返回。这种方式存在两个致命问题:</p>
<ul><li><strong>阻塞服务</strong>:Redis 是单线程模型,<code>keys</code>命令执行期间会占用主线程,导致其他请求(如读写操作)无法响应,数据量越大,阻塞时间越长(毫秒级到秒级不等);</li><li><strong>内存暴涨</strong>:若符合条件的键数量极多(如 100 万 +),<code>keys</code>会一次性将所有结果加载到内存,可能引发 Redis 内存溢出或 OS 级别的 Swap,严重时导致服务崩溃。</li></ul>
<p>而<code>scan</code>命令的设计理念是 **“渐进式遍历”**:将全量遍历拆分为多次小规模遍历,每次只返回少量结果(默认 10 个),既不会阻塞主线程,也不会占用过多内存。这使得它成为生产环境中遍历键的首选方案。</p>
<p class="maodian"><a name="_label1"></a></p><h2>二、scan 命令的基础用法:从语法到示例</h2>
<p class="maodian"><a name="_lab2_1_0"></a></p><h3>2.1 核心语法</h3>
<p><code>scan</code>命令的基本格式如下:</p>
<div class="jb51code"><pre class="brush:sql;">SCAN cursor \ \ \
</pre></div>
<p>各参数的含义如下:</p>
<ul><li><strong>cursor</strong>:游标,是<code>scan</code>的核心标识,用于记录遍历的 “位置”。首次遍历需传入<code>0</code>,后续遍历需传入上一次返回的游标值,直到游标返回<code>0</code>表示遍历结束;</li><li><strong>MATCH pattern</strong>:可选参数,用于过滤符合指定模式的键,支持通配符<code>*</code>(匹配任意字符)、<code>?</code>(匹配单个字符)、<code>[]</code>(匹配指定范围内的字符);</li><li><strong>COUNT count</strong>:可选参数,用于指定每次遍历的 “预期返回数量”,默认值为<code>10</code>。注意:这只是 “预期值”,并非实际返回数量,Redis 会根据内部数据结构调整,可能多也可能少;</li><li><strong>TYPE type</strong>:可选参数,用于过滤指定数据类型的键,支持<code>string</code>、<code>hash</code>、<code>list</code>、<code>set</code>、<code>zset</code>等,Redis 6.0 + 版本支持。</li></ul>
<p class="maodian"><a name="_lab2_1_1"></a></p><h3>2.2 基础示例:遍历所有键</h3>
<p>假设 Redis 中存在以下键:<code>user:100</code>、<code>user:101</code>、<code>order:200</code>、<code>product:300</code>,我们通过<code>scan</code>遍历所有键:</p>
<ol><li><strong>首次遍历</strong>:传入游标<code>0</code>,不指定<code>MATCH</code>和<code>COUNT</code>(默认返回 10 个):</li></ol>
<div class="jb51code"><pre class="brush:sql;">127.0.0.1:6379> SCAN 0
1\) "14"# 下一次遍历的游标
2\) 1) "user:100"
&#x20;2\) "order:200"
&#x20;3\) "product:300"</pre></div>
<p>结果中,第一个元素<code>"14"</code>是下一次遍历的游标,第二个元素是本次返回的键列表(共 3 个,少于默认的 10 个,符合 “预期值” 特性)。</p>
<ol><li><strong>第二次遍历</strong>:传入上一次返回的游标<code>14</code>:</li></ol>
<div class="jb51code"><pre class="brush:sql;">127.0.0.1:6379> SCAN 14
1\) "0"# 游标返回0,遍历结束
2\) 1) "user:101"</pre></div>
<p>此时游标返回<code>0</code>,表示所有键已遍历完成,本次返回剩余的<code>user:101</code>。</p>
<p class="maodian"><a name="_lab2_1_2"></a></p><h3>2.3 过滤场景:MATCH 与 TYPE 的使用</h3>
<p class="maodian"><a name="_label3_1_2_0"></a></p><h4>场景 1:匹配 “user:” 前缀的键</h4>
<p>通过<code>MATCH user:*</code>过滤前缀为<code>user:</code>的键:</p>
<div class="jb51code"><pre class="brush:sql;">127.0.0.1:6379> SCAN 0 MATCH user:\*
1\) "0"
2\) 1) "user:100"
&#x20;2\) "user:101"</pre></div>
<p>由于符合条件的键较少,一次遍历就完成,游标直接返回<code>0</code>。</p>
<p class="maodian"><a name="_label3_1_2_1"></a></p><h4>场景 2:只遍历 string 类型的键</h4>
<p>假设<code>user:100</code>是<code>string</code>类型,<code>order:200</code>是<code>hash</code>类型,通过<code>TYPE string</code>过滤:</p>
<div class="jb51code"><pre class="brush:sql;">127.0.0.1:6379> SCAN 0 TYPE string
1\) "8"
2\) 1) "user:100"</pre></div>
<p class="maodian"><a name="_label2"></a></p><h2>三、scan 的核心原理:为什么能 “渐进式” 遍历?</h2>
<p>要正确使用<code>scan</code>,必须理解其底层原理 ——<strong>基于 Redis 的哈希表结构和游标跳转</strong>。</p>
<p class="maodian"><a name="_lab2_2_3"></a></p><h3>3.1 Redis 的键空间存储:哈希表</h3>
<p>Redis 的键空间(keyspace)是通过<strong>哈希表</strong>实现的,哈希表中的每个 “桶”(bucket)存储若干个键(通过哈希冲突链解决冲突)。<code>scan</code>的遍历本质是<strong>遍历哈希表的桶</strong>,而非直接遍历键。</p>
<p class="maodian"><a name="_lab2_2_4"></a></p><h3>3.2 游标与 “高位进位”</h3>
<p><code>scan</code>的游标并非简单的 “桶索引”,而是基于 <strong>“高位进位”</strong> 的算法设计:</p>
<ul><li>Redis 会为哈希表分配一个固定的 “位数”(如 16 位,对应 65536 个桶),游标是一个无符号整数,长度与哈希表位数一致;</li><li>每次遍历后,游标会按照 “高位进位” 的规则生成下一个游标(例如,16 位游标<code>0000000000000000</code>(0)的下一个游标可能是<code>0000000000001000</code>(8),再下一个是<code>0000000000010000</code>(16)等);</li><li>当游标再次回到<code>0</code>时,表示所有桶已遍历完成,即整个键空间遍历结束。</li></ul>
<p class="maodian"><a name="_lab2_2_5"></a></p><h3>3.3 COUNT 参数的 “预期” 特性</h3>
<p>为什么<code>COUNT</code>是 “预期值” 而非 “固定值”?原因有两点:</p>
<ul><li><strong>哈希冲突</strong>:一个桶中可能存储多个键,遍历该桶时会返回所有键,导致实际数量超过<code>COUNT</code>;</li><li><strong>空桶跳过</strong>:若某个桶中没有键,Redis 会直接跳过,导致实际数量少于<code>COUNT</code>。</li></ul>
<p>例如,设置<code>COUNT 2</code>,但实际返回 3 个键:</p>
<div class="jb51code"><pre class="brush:sql;">127.0.0.1:6379> SCAN 0 COUNT 2
1\) "12"
2\) 1) "user:100"
&#x20;2\) "order:200"
&#x20;3\) "product:300"# 因哈希冲突,桶中键数量超过COUNT</pre></div>
<p class="maodian"><a name="_label3"></a></p><h2>四、scan 家族命令:不止遍历键</h2>
<p><code>scan</code>是一个 “家族”,除了遍历整个键空间的<code>scan</code>命令,还有针对特定数据类型的渐进式遍历命令,用法与<code>scan</code>一致,仅作用对象不同:</p>
<table><thead><tr><th>命令</th><th>作用对象</th><th>用途</th></tr></thead><tbody><tr><td>HSCAN</td><td>Hash 类型的字段</td><td>遍历 Hash 中的 field-value 对</td></tr><tr><td>SSCAN</td><td>Set 类型的元素</td><td>遍历 Set 中的所有元素</td></tr><tr><td>ZSCAN</td><td>Sorted Set 类型的元素</td><td>遍历 ZSet 中的 member-score 对</td></tr></tbody></table>
<p class="maodian"><a name="_lab2_3_6"></a></p><h3>示例:HSCAN 遍历 Hash 字段</h3>
<p>假设<code>user:100</code>是 Hash 类型,存储用户信息:</p>
<div class="jb51code"><pre class="brush:sql;">127.0.0.1:6379> HSET user:100 name "zhangsan" age 25 city "beijing"
(integer) 3</pre></div>
<p>通过<code>HSCAN</code>遍历其字段:</p>
<div class="jb51code"><pre class="brush:sql;">\# 首次遍历:游标0,COUNT 2
127.0.0.1:6379> HSCAN user:100 0 COUNT 2
1\) "2"# 下一次游标
2\) 1) "name"
&#x20;2\) "zhangsan"
&#x20;3\) "age"
&#x20;4\) "25"
\# 第二次遍历:游标2
127.0.0.1:6379> HSCAN user:100 2
1\) "0"# 遍历结束
2\) 1) "city"
&#x20;2\) "beijing"</pre></div>
<p class="maodian"><a name="_label4"></a></p><h2>五、使用 scan 的注意事项与常见误区</h2>
<p class="maodian"><a name="_lab2_4_7"></a></p><h3>5.1 避免 “重复” 与 “遗漏”</h3>
<p><code>scan</code>的设计目标是 “不重复、不遗漏”,但在以下场景可能出现问题:</p>
<ul><li><strong>数据动态变化</strong>:遍历过程中,若键被添加、删除或修改,可能导致某个键被重复遍历或遗漏。这是渐进式遍历的固有特性,无法完全避免,需在业务层做兼容(如通过唯一 ID 去重);</li><li><strong>游标失效</strong>:若两次遍历间隔过长(如超过 Redis 的超时时间),游标可能失效,导致遍历中断。建议缩短遍历间隔,确保游标连续性。</li></ul>
<p class="maodian"><a name="_lab2_4_8"></a></p><h3>5.2 合理设置 COUNT 参数</h3>
<ul><li><strong>小数据量</strong>:默认<code>COUNT 10</code>即可,无需调整;</li><li><strong>大数据量</strong>:若需加快遍历速度,可适当增大<code>COUNT</code>(如<code>COUNT 1000</code>),但需注意:<code>COUNT</code>越大,单次遍历占用的主线程时间越长,需在 “速度” 和 “阻塞风险” 间平衡,建议不超过<code>10000</code>。</li></ul>
<p class="maodian"><a name="_lab2_4_9"></a></p><h3>5.3 避免在遍历中使用复杂过滤</h3>
<p><code>MATCH</code>和<code>TYPE</code>过滤是在 “遍历后” 执行的 ——Redis 会先遍历出一批键,再过滤掉不符合条件的键。若符合条件的键比例极低(如<code>MATCH user:100000*</code>,但实际只有 1 个),会导致大量无效遍历,浪费资源。此时建议在业务层过滤,或通过 Redis 的键命名规范减少无效匹配。</p>
<p class="maodian"><a name="_lab2_4_10"></a></p><h3>5.4 不要依赖返回结果的顺序</h3>
<p><code>scan</code>的返回结果是基于哈希表的桶顺序,与键的插入顺序无关,且每次遍历的顺序可能不同。业务层不应依赖<code>scan</code>的返回顺序。</p>
頁:
[1]