news 2026/2/11 9:07:42

面试题:线上有一个亿级数据的 Redis BigKey,如何进行在线优化?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
面试题:线上有一个亿级数据的 Redis BigKey,如何进行在线优化?

背景: 在一线互联网大厂(阿里、字节等)的面试中,Redis 的 BigKey 优化是必考题。 但面试官通常不会只问“什么是 BigKey”,而是会抛出一个极具挑战性的场景:“线上有一个亿级数据的 BigKey(如 Hash 类型),正在承载核心业务。现在要求你对其进行优化(拆分),要求:1. 业务全程无感知;2. 绝对不能阻塞 Redis;3. 流量不能穿透到数据库。你怎么做?”

这是一道典型的“飞行中换引擎”的架构题。本文将从设计到落地,手把手教你设计一套教科书级的解决方案。

一、 核心挑战分析

在动手写代码之前,我们必须先拆解面试官给出的三个“紧箍咒”:

  1. 不能影响现有业务:意味着不能停机,不能有明显的抖动,必须平滑过渡。
  2. 不能阻塞 Redis:意味着不能使用DELHGETALL等 $O(N)$ 复杂度的命令,必须利用分治思想。
  3. 请求不能大量到库:这是最关键的。在数据迁移过程中,缓存不能失效。如果直接删除老 Key 等待重建,数据库瞬间就会被百万 QPS 打死(缓存雪崩)。

结论:我们必须采用“双写 + 渐进式迁移 + 动态路由 + 异步删除”的组合拳。

二、 总体架构方案

假设我们的 BigKey 是一个存储用户详情的 Hash,Key 为user:info:all,内部包含 1000 万个字段(field 为 userId,value 为 JSON)。

我们的目标是将其拆分为 100 个小 Hash:user:info:0user:info:99

核心步骤:

  1. 双写阶段:修改代码,对写操作同时写入“新 Key”和“老 Key”。
  2. 迁移阶段:启动后台程序,利用HSCAN渐进式地把“老 Key”的数据搬运到“新 Key”。
  3. 切读阶段:利用配置中心(Nacos/Apollo)进行灰度发布,逐步将读流量从“老 Key”切换到“新 Key”。
  4. 清理阶段:确认无误后,异步删除“老 Key”。

三、 详细实施步骤

Step 1:数据分片设计 (Sharding)

首先确定分片策略。最常用的是取模算法。

分片公式shard_id = hash(userId) % 100

Key 命名规则user:info:{shard_id}

这样,原本 1000 万的大 Hash 就变成了 100 个 10 万级的小 Hash,彻底解决了单 Key 热点和阻塞问题。

Step 2:同步双写 (Double Write)

这是“平滑过渡”的基石。在应用层修改写逻辑,新老数据同时更新

public void updateUserInfo(Long uid, UserInfo info) {String value = JSON.toJSONString(info);// 1. 【新逻辑】写入分片后的新 Keyint shardId = Math.abs(uid.hashCode() % 100);String newKey = "user:info:" + shardId;redis.hset(newKey, uid.toString(), value);// 2. 【旧逻辑】同时写入老 Key(保持老数据最新,供读取和兜底)String oldKey = "user:info:all";redis.hset(oldKey, uid.toString(), value);}

注意:此时的读操作依然完全读取user:info:all,业务完全无感知。

Step 3:渐进式数据迁移 (The Migration)

这是最考验技术细节的一步。我们需要一个后台任务(Worker),将老数据搬运到新 Key 中。

绝对禁忌

  • ❌ 禁止使用HGETALL一次性拉取所有数据(会阻塞 Redis 主线程,导致故障)。
  • ❌ 禁止在迁移后立即删除老数据(会导致读请求击穿到 DB)。

正确姿势:使用HSCAN命令。

# 伪代码:后台迁移脚本cursor = 0old_key = "user:info:all"while True:# 1. 使用 HSCAN 每次只拉取 1000 条,避免阻塞# cursor 是游标,每次返回新的游标和数据cursor, data = redis.hscan(old_key, cursor=cursor, count=1000)if not data:break # 数据为空,结束# 2. 在内存中进行分片计算pipeline = redis.pipeline()for uid, info_json in data.items():shard_id = hash(uid) % 100new_key = f"user:info:{shard_id}"# 3. 批量写入新 Keypipeline.hset(new_key, uid, info_json)pipeline.execute()# 4. 稍微休眠一下,给 Redis 喘息机会(控制迁移速率)time.sleep(0.05)if cursor == 0:break # 游标归零,全量扫描结束

Step 4:灰度切读与多级兜底 (Gray Switch)

数据迁移完成后,新 Key 中已经有了全量数据。但为了保险,我们不能“一刀切”。

我们需要引入灰度开关(Switch Ratio),并设计多级兜底策略,这是满足“请求不穿透到 DB”的核心。

public UserInfo getUserInfo(Long uid) {// 1. 获取灰度比例 (例如 10 代表 10% 的流量走新逻辑)int switchRatio = configService.getInt("bigkey.switch.ratio", 0);// 2. 流量路由if (ThreadLocalRandom.current().nextInt(100) < switchRatio) {try {// --- 尝试读新 Key ---int shardId = Math.abs(uid.hashCode() % 100);String newKey = "user:info:" + shardId;String value = redis.hget(newKey, uid.toString());if (value != null) {return JSON.parseObject(value, UserInfo.class);}} catch (Exception e) {// 记录日志,不要抛出,降级到老逻辑log.error("Read new key failed", e);}}// 3. 【一级兜底】如果没命中新 Key,或者不在灰度范围内,查老 Key// 只要老 Key 还在,请求就绝对不会击穿到数据库!String oldValue = redis.hget("user:info:all", uid.toString());if (oldValue != null) {return JSON.parseObject(oldValue, UserInfo.class);}// 4. 【二级兜底】查数据库(最后防线)return userMapper.selectById(uid);}

操作流程

  1. 初始状态:比例 0%,全读老 Key。
  2. 观察期:调至 1%,观察日志、Redis 命中率、业务报错。
  3. 放量期:逐步调至 10% -> 50% -> 100%。
  4. 全量后:保持运行一段时间,确保新 Key 数据完全正确。

Step 5:非阻塞清理 (Async Delete)

当读写流量全部切换到新 Key,且稳定运行一周后,可以下线“双写逻辑”中的老 Key 写入,并删除老 Key。

绝对禁忌

  • ❌ 禁止直接使用DEL user:info:all。删除一个 5GB 的 Key 会导致 Redis 主线程阻塞数秒甚至数分钟,引发线上故障。

正确姿势

  • Redis 4.0+:使用UNLINK命令
UNLINK user:info:all

原理:Redis 会将 Key 从元数据中卸载,真正的内存回收由后台线程(Lazy Free)异步执行,不阻塞主线程。

  • Redis 4.0 以下:使用HSCAN+HDEL写一个脚本,每次 scan 1000 个字段,然后 delete 这 1000 个字段,循环执行,直到删空。

四、 总结与防坑指南

回顾我们的方案,是如何完美解决面试官的三个难题的:

挑战

解决方案

不影响业务

双写机制:保证新老数据实时同步;灰度切读:控制风险,随时可回滚。

不阻塞 Redis

HSCAN 迁移:化整为零,分批搬运;UNLINK 删除:异步回收内存。

不穿透数据库

一级兜底策略:新 Key 查不到时,强制回源查老 Key(因为老 Key 一直没删),从而保护了数据库。

最后的防坑 Tips:

  1. 迁移脚本的幂等性:迁移脚本可能会中断重启,代码必须设计为可重入的(Set 操作本身就是幂等的,这很好)。
  2. 过期时间:如果老 Key 有过期时间,新 Key 必须继承(甚至设置得稍微长一点)。
  3. Hash Tag:如果你使用的是 Redis Cluster,且需要在 Lua 脚本中同时操作多个新 Key,记得在 Key 设计时加上 Hash Tag,例如{user:info}:1,但在纯分片场景下通常不需要。

掌握了这套“分片+双写+迁移+兜底+异步删”的组合拳,你不仅能搞定 BigKey,还能解决绝大多数数据迁移类的架构难题。

https://mp.weixin.qq.com/s/niJ7M9FKvnB-EkK8Ci8CuQ

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/10 3:58:29

如何实现TensorRT推理服务的影子流量测试?

如何实现TensorRT推理服务的影子流量测试&#xff1f; 在AI模型频繁迭代的今天&#xff0c;一次看似微小的推理引擎升级&#xff0c;可能带来意想不到的后果&#xff1a;某个推荐场景下的点击率突然下降、语音识别在特定口音上出现批量误判&#xff0c;或是自动驾驶感知模块对雨…

作者头像 李华
网站建设 2026/2/5 8:39:30

Scarab模组管理:打造专属空洞骑士冒险的终极指南

Scarab模组管理&#xff1a;打造专属空洞骑士冒险的终极指南 【免费下载链接】Scarab An installer for Hollow Knight mods written in Avalonia. 项目地址: https://gitcode.com/gh_mirrors/sc/Scarab 还在为《空洞骑士》模组安装的复杂流程而头疼吗&#xff1f;想象一…

作者头像 李华
网站建设 2026/2/9 17:24:24

如何通过TensorRT实现推理服务的请求限流?

如何通过TensorRT实现推理服务的请求限流&#xff1f; 在AI模型大规模部署的今天&#xff0c;一个常见的场景是&#xff1a;你的图像分类服务突然被上千个并发请求淹没——来自监控摄像头、移动端上传、自动化脚本……GPU显存瞬间飙红&#xff0c;延迟从50ms飙升到2秒以上&…

作者头像 李华
网站建设 2026/2/6 1:55:32

北斗卫星导航定位从核心框架到定位流程详解(一)

hello~这里是维构lbs智能定位&#xff0c;如果有项目需求和技术交流欢迎来私信我们~点击文章最下方可获取免费获取技术文档和解决方案我国的北斗卫星导航系统&#xff08;BDS&#xff09;的定位核心原理是“空间星座地面控制用户终端”协同&#xff0c;以伪距测量与空间后方交会…

作者头像 李华
网站建设 2026/2/9 23:40:26

如何评估TensorRT对模型公平性的影响?

如何评估TensorRT对模型公平性的影响&#xff1f; 在金融信贷审批、医疗诊断辅助、招聘筛选和公共安防等高风险场景中&#xff0c;AI模型的每一次预测都可能深刻影响个体命运。随着这些系统越来越多地部署到生产环境&#xff0c;人们不再只关注“模型是否准确”&#xff0c;更关…

作者头像 李华