前言

这周遇到的一个小需求是通过Go实现对Redis的hash field实时上限检查,而因为是线上的服务,所以这个上限检查不能对redis pod造成负担,跟组内导师交流学习后了解到可以通过redis的HScan命令来实现这个需求

需要了解的几个词

  • cursor(游标):数据库中常见的一个概念,通常提供一种从表中检索出的数据进行操作的灵活手段,能从包含数据记录的结果集中每次提取一条记录的机制
  • rehash:在redis的具体实现中,使用了一种叫做**渐进式哈希(rehashing)**的机制来提高dict的缩放效率
  • 迭代完整性:保证完整遍历被遍历对象的性质
  • 生产环境:不敢乱增加负载的环境(跑一下redis keys命令直接重大事故)

Scan命令

Scan命令是什么

SCAN命令是基于游标(cursor)迭代的,SCAN命令并不单纯指代SCAN命令,还包含SSCAN、HSCAN、ZSCAN,每种命令操作对象是有区别的,但用法及功能基本相同

为什么要用Scan命令

当Redis中的数据量很大时,因为Redis是单线程服务,所以一些数据操作会导致Redis服务卡顿,甚至宕机。当某一指令耗时很长时(比如经典的keys *),就会阻塞后续的指令执行。当被积压的指令越来越多时,Redis服务占用CPU将不断升高,最终导致Redis pod崩溃

相比于keys命令,scan命令有两个比较明显的优势:

  • scan命令的时间复杂度虽然也是O(N),但它是分次进行的,不会阻塞线程
  • scan命令提供了limit参数,可以控制每次返回结果的最大条数(但这里也有个坑,下面细讲)

Scan命令的基本使用

通用参数:

  • cursor:迭代游标
  • MATCH:数据匹配模式
  • COUNT:迭代返回数量
命令功能参数返回值
SCAN基于游标迭代DBcursor [MATCH pattern] [COUNT count]返回数组,第一个值是下一次迭代的游标(uint64),第2个值是元素列表(key列表)
SSCAN基于游标迭代Setskey cursor [MATCH pattern] [COUNT count]返回数组,第一个值是下一次迭代的游标(uint64),第2个值是元素列表
HSCAN基于游标迭代Hasheskey cursor [MATCH pattern] [COUNT count]返回数组,第2个值是field-value列表
ZSCAN基于游标迭代ZSetskey cursor [MATCH pattern] [COUNT count]返回数组,第2个值是member-score列表

Scan命令特性

  • 增量迭代:和keys、Smembers等命令的全量迭代区分开,全量迭代对大集合执行时可能阻塞服务很长时间,增量迭代则不会
  • 不保证准确结果:因为增量迭代过程中可能出现迭代元素被更改的情况,所以并不能保证准确结果
  • 基于游标迭代:SCAN基于游标迭代,每次请求将返回下一次需要使用的游标;游标cursor可以比DB元素总量大,可以为负数;使用间断(不是迭代返回的)、负数、超出范围或其他非法游标,迭代不会报错,可能产生未定义行为(无法保证准确性);
  • 迭代结束标记:SCAN返回的游标不一定递增,是无序的**(因为考虑到redis rehash的情况,SCAN命令是以高位加1的方式进行遍历的,防止扩容时的重复遍历)**,某次迭代返回的元素数量可能为0;**返回元素列表为空,不代表迭代结束;一个完整的迭代是SCAN游标从0开始,返回游标为0结束;**迭代状态由返回的游标控制。可以并发执行迭代;可随时终止迭代;
  • 迭代完整性:遍历开始到遍历结束一直存在的数据,一定能被迭代返回;同一个元素可能返回多次,数据去重应由应用程序完成;在迭代过程中增删的元素,可能返回,可能不返回(由于遍历的无序性)
  • 为什么有时count参数失效了(直接返回了整个集合)?:当数据类型是sets(由integer组成)、hashes、sorted sets且集合较小时,迭代将返回整个集合的数据,与count无关(因为数据量较小时Redis的内存优化策略)
  • 参数count:count默认值是10;数据集较大时,如果没有使用match,返回元素为count或比count略大;每次迭代的count参数值可以不同,只要使用上次迭代返回的游标即可

总结

redis Scan命令是在生产环境中操作redis大key最合适的命令,但相应地,使用起来也是有很多坑点需要开发者注意的(我一开始也觉得这个需求几行加个定时任务就结束了,没想到一搞就是两天)