尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Redis核心进阶:特殊数据类型、SCAN渐进式遍历与库管理

Redis核心进阶:特殊数据类型、SCAN渐进式遍历与库管理 做Redis开发这几年我自己最大的感触就是Redis入门容易但真正往深了走很多知识点是零零散散捡起来的。大部分教程翻来覆去就是五种基础类型String、Hash、List、Set、ZSet讲得滚瓜烂熟但一旦线上遇到“统计一个大集合里的独立访客”、“搜索附近的门店”、“快速清点某一个库里的key数量”这类需求很多人就卡住了甚至开始用错误的方式硬撑——比如直接keys *扫全库或者把几百万数据塞进一个Set里做交集。这次把Redis里容易被忽略但又非常关键的三个主题一次性讲透其它数据类型Bitmaps、HyperLogLog、GEO、Stream、渐进式遍历SCAN家族命令、数据库管理多库切换与日常维护。这三个东西在Redis的官方文档里占比不大但在真实业务里的出场率相当高而且面试也爱问。文章不会绕弯子直接按我的实操经验来写命令、场景、坑点一起上你在自己环境里按着敲一遍就能用上。1. 内容整体设计与思路拆解1.1 为什么这三块内容要放在一起讲很多人看到这个标题会觉得奇怪其它数据类型、渐进式遍历、数据库管理这三件事有什么关系其实它们在实战中是环环相扣的。数据类型决定你用什么姿势存数据。但存进去之后你得读、得查、得统计。这时候如果还用最原始的keys pattern去匹配key数据量小没事数据量一上来redis的单线程模型就直接被拖垮。于是渐进式遍历成了唯一靠谱的扫描方式。再往后项目里环境多了、key多了你就得考虑怎么隔离数据、怎么管理多个逻辑空间——这就是数据库管理要解决的问题。我见过不少团队Redis里就一个db0几百个业务模块的key全堆一起前缀倒是加了但排查问题的时候想死的心都有。也见过有人乱用select切到db5结果某个key在哪个库自己都忘了。所以这三块内容放一起讲其实是帮你在心里搭一个完整的Redis使用框架存进去、扫得动、管得好。1.2 通读全文需要的前置知识这文章不是零基础教程默认你至少知道Redis怎么启动、怎么用redis-cli连上去、五种基础结构的基本命令。如果这些还不熟建议先花一个小时把String、List、Hash、Set、ZSet的增删改查过一遍再回来看这篇。如果你已经有基础但没用过今天讲的这几种数据类型那正好。我会在讲每一种类型的时候把它的底层结构、适用场景、命令示例、坑点全部串起来讲。你不用去翻十几个网页这篇文章就是一份浓缩的实战手册。2. 其它数据类型逐个拆解2.1 Bitmaps用一个bit位搞定海量状态统计Bitmaps其实并不是一种全新的数据结构它底层就是String类型只不过把字符串当成一个bit数组来用。一个字节8个bit一个普通的字符串可以扩展成上亿个bit位。它最常见的场景就是打卡、签到、在线状态这类“非0即1”的判断。我之前做过一个签到系统当时用户量差不多500万如果用Set存每个用户的签到记录每天就是500万个字符串key光内存就扛不住。后来换成Bitmapskey设计成sign:20250101用户ID作为offset签到置为1一个月下来这个key占用的内存也只有500万bit换算一下差不多0.6MB。这个差距是数量级的。命令也简单# 设置第10001个用户当天已签到 SETBIT sign:20250101 10001 1 # 查询第10001个用户当天是否签到 GETBIT sign:20250101 10001 # 统计当天签到总人数 BITCOUNT sign:20250101还要注意一个细节SETBIT的offset范围是0到2^32-1也就是说单个bitmap最多能支撑42亿个bit位普通业务根本用不完。但是有个坑——如果你设置的offset非常大比如几个亿Redis会立刻为这个key分配对应的内存可能导致一瞬间内存暴涨。我在线上见过有人把用户ID直接当offset用结果用户ID是自增的几千万看似没事但如果某个ID突然是十位数内存马上就出问题。稳妥的做法是先把用户ID做一次连续性映射或者对用户ID做取模分段。2.2 HyperLogLog用12KB换千万级去重计数HyperLogLog是Redis里我最喜欢的类型之一因为它解决了一个非常经典的问题大规模数据去重计数。你可以理解为它只做一件事——给你一个近似值告诉你“大概有多少个不同的元素”误差在0.81%以内。底层原理是对每个元素做哈希然后根据哈希值里第一个1出现的位置来估算基数。它不需要存原始元素只存一些用于估算的寄存器。所有key加起来也就12KB左右不管你塞进去100万条还是10亿条内存消耗基本恒定。最典型的场景就是UV统计。比如统计某篇文章的独立访客数# 用户访问时执行 PFADD article:uv:20250101 user_10001 user_10002 user_10001 # 获取今天的UV PFCOUNT article:uv:20250101这里有个容易踩的坑PFADD的误差对极小数据量不友好。比如你只添加了10个元素统计出来的结果可能是9个或者11个相对误差就很大了。所以如果你的数据量少于几百建议直接用Set如果你需要精确结果也千万别用HyperLogLog。另外一个实用技巧是PFMERGE。我做过一个需求要看过去7天的总去重用户数不是每天UV相加而是要跨天去重。当时就是每天一个key最后用PFMERGE合成一个临时key再PFCOUNT非常快几行命令就搞定了。# 合并周一和周二的数据得到两天的去重总数 PFMERGE week_uv monday_uv tuesday_uv PFCOUNT week_uv2.3 GEO地理位置计算从未如此简单做LBS相关功能的人应该都经历过自己算经纬度距离的痛苦。在Redis 3.2以前要么用MySQL算要么用第三方地图API要么自己写球面距离公式。有了GEO之后这些全都可以在Redis里一条命令完成。GEO底层是用ZSet实现的每个成员的score是一个52位的GeoHash编码值。基于这个编码ZSet天然支持按距离排序。命令如下# 添加店铺坐标 GEOADD shop:locations 116.397128 39.916527 store_1 GEOADD shop:locations 116.392456 39.905919 store_2 # 计算两个店铺的距离单位可选 m/km/mi/ft GEODIST shop:locations store_1 store_2 km # 查找某个坐标附近5公里内的店铺 GEOSEARCH shop:locations FROMLONLAT 116.40 39.90 BYRADIUS 5 km ASC我特别强调一下GEOSEARCH这个命令它在Redis 6.2版本里才开始提供比老版的GEORADIUS更好用参数更直观而且可以按距离排序。如果你还在用6.0以前的版本就得用GEORADIUS用法也差不多。GEO有个需要注意的地方它存的坐标是有限的精度在小数点后5位左右对于“附近的人/店”这种场景完全够用。但如果你需要的是行政区划级别的精确判断或者路网距离那Redis GEO做不到别硬上。还有一点GEO的ZSet底层结构意味着你可以复用ZSet的命令来管理比如用ZREM删除某个坐标点用ZCARD查看总数。这点刚接触的人容易忽略以为只能通过GEO开头的命令操作。2.4 StreamRedis自己的轻量消息队列在Stream出现之前Redis做消息队列要么用List的LPUSH/BRPOP要么用Pub/Sub。前者不支持多消费者组后者消息即发即弃没有持久化消费者挂了消息就丢了。Redis 5.0引入的Stream就是为了解决这个尴尬。Stream本质上是一个追加型的日志结构每个消息都有一个自动生成的ID格式是时间戳-序号保证全局有序且唯一。它可以持久化支持消费者组还支持消息确认机制。最简单的生产和消费# 生产消息 XADD order:events * event_type create order_id 10086 # 读取消息从头开始读 XRANGE order:events - # 阻塞式消费新消息 XREAD COUNT 10 BLOCK 5000 STREAMS order:events $消费者组是Stream最核心的用法它可以实现一条消息只被一个消费者处理一次并且消费者崩溃后消息还能被重新消费# 创建消费者组 XGROUP CREATE order:events order_group 0 # 消费者读消息 XREADGROUP GROUP order_group consumer_1 COUNT 10 STREAMS order:events 这里的表示只读从未投递过的消息。读完需要XACK确认XACK order:events order_group 1650000000000-0如果消费者处理到一半挂了没确认的消息会进Pending列表。你可以用XPENDING查看然后通过XCLAIM把超时消息转移到其他消费者处理。这套机制做消息可靠性投递非常顺手。生产环境用Stream要注意内存策略。默认情况下消息只追加不删除时间长了内存会膨胀。要么消费完手动删除要么用XADD ... MAXLEN 100000限制长度只保留最近10万条。想精确按时间清理就用XTRIM order:events MINID 1650000000000。3. 渐进式遍历别再keys *了3.1 keys命令为什么会阻塞RedisRedis是单线程处理命令的这就意味着任何命令只要执行时间太长后面所有命令都得排队等着。KEYS pattern这个命令的问题就在这它会遍历整个键空间做匹配数据量越大耗时越长。我在一个大概有两三百万个key的环境里试过KEYS user:*这条命令跑一次要好几秒。线上这个时间段内所有读写请求全部卡住监控图上直接出现一条长长的平直线那种感觉真的会让人后背发凉。所以生产环境里KEYS命令基本是禁止的除非你确定这个实例的数据量极小、访问量极低。3.2 SCAN的游标机制与正确用法SCAN就是为了解决KEYS的阻塞问题而生的。它的思路是不一次遍历完而是每次返回一小部分数据同时返回一个游标下次用这个游标继续遍历直到游标变为0。SCAN 0 MATCH user:* COUNT 1000第一次执行返回的第一个值是游标第二个值是本次匹配到的key列表。注意COUNT并不是每次返回条数它只是提示每次遍历的槽位数实际返回的key数量可能多可能少。我实测过空槽位多的情况下一次SCAN返回0个key但游标还是会往前走。SCAN能保证两件事一是从开始到结束的整个遍历过程中一直存在的key一定会被返回出来二是返回过程中新增或删除的key不保证能被遍历到。这对于大部分场景已经足够。用法上要循环执行redis-cli --scan --pattern user:* --count 1000或者用脚本import redis r redis.Redis(host127.0.0.1, port6379) cursor 0 while True: cursor, keys r.scan(cursor, matchuser:*, count1000) for key in keys: print(key) if cursor 0: break3.3 SCAN家族HSCAN、SSCAN、ZSCAN除了扫描全局键空间Redis还提供了针对hash、set、zset内部元素的渐进式遍历命令HSCAN、SSCAN、ZSCAN。它们的原理和SCAN一样都是游标式遍历解决的是避免HGETALL、SMEMBERS、ZRANGE在数据量大时阻塞的问题。举个实际例子我以前维护过一个hash结构里面存了大量用户扩展属性单key下有几十万字段。排查线上问题时需要遍历所有字段但HGETALL一次全取出来要阻塞好几秒。换成HSCAN之后每次取一百个既不影响线上又能慢慢把所有数据摸完。HSCAN user:extended 0 COUNT 100SSCAN和ZSCAN用法完全一致都是第一个参数是key第二个参数是游标。对于ZSet如果你只是要取排名前几的数据用ZRANGE没问题但如果你要扫全量那ZSCAN才是正确选择。3.4 渐进式遍历的隐性坑SCAN虽然不阻塞但有个实际问题多次SCAN返回的key之间可能重复。因为游标是按哈希槽位推进的如果在遍历过程中某些key发生了rehash同一个key可能在两个槽位里被访问到。去重是在客户端做的别指望它一个key只出一次。另外一个坑是单次COUNT值设太大。虽然SCAN不会像KEYS那样一次性全局阻塞但如果你一次取几十万条生成的网络响应包巨大传输耗时也长。我这里建议常规情况下COUNT设500到2000数据量特别大的实例设100到500。需要全量扫描时用redis-cli --scan配合管道更加稳妥。4. Redis的数据库管理与维护4.1 多数据库机制select切换的是逻辑空间Redis从早期版本就支持多数据库默认配置有16个编号从0到15。通过SELECT命令可以切换SELECT 3切换之后后续所有命令都在db3里执行。每个db之间key是隔离的理论上互不影响。但有个关键点要明白这些db并不是独立的Redis实例它们共享同一个进程、同一份内存、同一个持久化文件。这意味着如果你用SAVE或BGSAVE做持久化所有db的数据都会一起落盘没法只备份db3而不管db0。同样你清库的时候如果用了FLUSHALL那所有db的数据都没了这种事我在运维群见过不止一次。4.2 多库的实践建议能用db0就别乱切换我知道网上很多教程会教你说“用不同db隔离不同业务”比如db0缓存、db1会话、db2队列。听起来很美好但实际维护起来很痛苦。首先是没法对不同db设置不同的访问密码或者ACL权限。Redis 6.0引入了ACL功能但它针对的是用户不是db。你想让运维同事只能看db1的数据、碰不了db0的数据用多库做不到。其次是排查问题难度上升。同一个key在不同db里查出来的结果可能完全不一样而很多可视化工具的key搜索默认只搜db0你如果习惯性切库经常会出现“key明明存在却搜不到”的幻觉。所以我的建议是生产环境只用db0多个业务的数据隔离靠key前缀比如order:123、user:456。Cluster模式更是只能支持db0提前养成单库好习惯能省掉很多迁移成本。如果确实需要不同业务绝对隔离就起不同实例物理隔离比逻辑隔离更可靠。4.3 常用数据库管理命令一览列几个我日常用得比较多的管理命令顺手保存一份# 查看当前库key数量 DBSIZE # 清空当前库 FLUSHDB # 清空所有库 FLUSHALL # 交换两个db的数据 SWAPDB 0 1 # 查看当前库redis-cli进入后可执行 SELECT 3 CLIENT GETNAMESWAPDB这个命令知道的人不多但很实用。比如我做过一次版本切换新数据写到db1验证无误后用SWAPDB 0 1把新数据切到db0旧数据挪到db1业务无感知完成切换这个操作非常快秒级完成。但注意SWAPDB在Cluster集群模式下不可用只适用于单机或主从架构。别把它当万能钥匙。4.4 数据库管理的危险操作与误删恢复FLUSHALL和FLUSHDB是危险命令。Redis没有原生的回收站机制一旦执行数据立刻消失。网上那些“误删恢复教程”基本都是靠持久化文件如果你恰好没开RDB或AOF删了就真没了。我强烈建议生产环境开启rename-command配置把危险命令改掉或者禁掉# 在redis.conf里把FLUSHALL禁用把FLUSHDB改成别的名字 rename-command FLUSHALL rename-command FLUSHDB zkq_clear_db_2025另外运维和开发用的账号做区分开发账号用ACL限制不允许执行管理命令。不要嫌麻烦这类事一旦出事就是生产事故。4.5 数据库监控与容量评估的实用技巧配合DBSIZE我每周会写个脚本统计各实例的key数量变化趋势。如果某个实例key数量异常上涨说明可能有代码逻辑问题比如该设置过期时间的key没有设。我记得有个项目就是因为一个缓存key漏了TTL一个月时间key数量涨了快十倍内存直接报警。最后用SCAN扫出来才发现是一批带时间戳的key过期时间写错了单位白白占了一个月内存。容量评估有个经验公式长期稳定使用的key要预留30%到50%的余量。Redis的内存如果打满会触发maxmemory策略默认是noeviction——写命令直接报错连删除key都受影响。宁可多观察几天也不要等内存打满再临时清数据。5. 常见问题与排查技巧实录5.1 那些年我踩过的数据类型相关的坑第一个坑是关于HyperLogLog的误用。有次需求方要一个“精确到个位”的去重统计我图省事用了PFCOUNT结果数据量小的时候偏差特别明显被质疑数据不准确。后来老老实实改成Set才把问题解决。记住HyperLogLog永远是近似的它能接受0.81%误差的时候才用它。第二个坑是Bitmaps内存暴涨。前面说过offset设置过大时会立即分配内存。我在测试环境随手设了个SETBIT test 999999999 1Redis瞬间多占用了100多MB当时吓得以为实例挂了。测试无所谓线上千万别这么干一定先确认offset在合理范围内。第三个坑是Stream消息堆积。消费者消费不过来消息在内存里越堆越多实例内存一直在涨。我的经验是Stream一定要设置MAXLEN或者MINID同时监控XLEN的长度。如果消息积压超过一定阈值就要告警了不然Redis撑不住。5.2 SCAN使用中的问题速查问题现象可能原因解决方案SCAN返回重复keyrehash导致客户端去重不要依赖服务端去重循环很久不结束key数量大或COUNT太小调大COUNT比如5000分批次处理MATCH匹配不到数据匹配模式写错或遍历时数据已过期用--scan加pattern先测试注意过期key不被遍历扫描期间CPU偏高count设置过大降低COUNT值避免单次拉太多数据5.3 面试题视角这三块内容的考察方向不少后端岗位面试都会问Redis而“其它数据类型渐进式遍历数据库管理”恰好是高频考点。列举几个典型的你用过Redis哪些数据类型分别解决什么问题——这时候你如果能讲出Bitmaps做签到、HyperLogLog做UV、GEO做附近的人、Stream做消息队列面试官会立刻觉得你有实战经验而不是只会背八股。Redis里的KEYS命令有什么问题SCAN是怎么解决的——考察点是对单线程模型的理解。你要说清楚KEYS会阻塞SCAN是游标式分段遍历但可能有重复、不保证新增key被遍历到。Redis为什么默认16个库你们生产环境用几个库——这个问题没有标准答案但完全没思考过的人很容易踩坑。我一般实话实说默认配置是16个但我们只用db0业务隔离靠前缀。5.4 一条实用排查流程线上redis偶发超时如果你在线上遇到偶发的Redis超时除了网络因素最值得怀疑的就是某些慢命令在阻塞事件循环。我的排查顺序是先通过SLOWLOG GET 10查看最近慢命令。如果慢命令里有KEYS、HGETALL、SMEMBERS这类一次性取大量数据的操作基本可以定位。然后看看是不是某个key的value特别大用DEBUG OBJECT key可以查看序列化长度bigkey的读写也可能造成阻塞。最后根据结果决定优化方案把大key拆分把全量命令改成SCAN渐进式遍历或者给key加过期时间控制增长。这套流程我用了很多年几乎没有失手过。Redis的阻塞问题九成都能从慢日志和bigkey里找到答案。6. 实操总结与个人建议先给一套可以直接落地的Redis使用规范都是我踩坑踩出来的经验拿去就能用键空间设计统一用冒号分隔比如业务:实体:ID方便按前缀SCAN排查问题。能用精确类型就别用模糊类型能用Hash就别把一堆键拼成String用序列化。做去重计数数据量小用Set数据量大用HyperLogLog绝不能混用。所有可能增长的key都要设置过期时间或长度上限Redis不是无限磁盘。禁用KEYS、禁用FLUSHALL除非你非常清楚自己在做什么。只要不查排序需求GEO就用来做附近查找别拿它当坐标存储字段。Redis Stream适合做轻量级消息队列但消息量特别大的场景还是交给专业MQ比如Kafka或RabbitMQ。最后再分享一个小技巧。我平时会把常用的排查命令写成一个shell脚本放到跳板机上一条命令就能看到当前Redis的key数量、内存占用、慢查询、bigkey分布、客户端连接数。排查问题时效率翻倍。脚本内容也不复杂就是把INFO、DBSIZE、SLOWLOG GET、SCAN这些命令的结果格式化一下值得花半小时搞一个。毕竟Redis这类基础设施平时不出事的时候感觉不到它的存在一出事就是大事故工具准备好关键时刻能救命。
返回列表