news 2026/4/3 4:38:50

面试题:线上有一个亿级数据的 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/3/17 15:07:33

一图说清HRBP如何赋能业务

一图说清楚HRBP如何赋能业务&#xff01; 赋能阶梯&#xff1a;由基础到高阶&#xff1a;知识工具层&#xff0c;制度体系层&#xff0c;方法路径层&#xff0c;思维认知层&#xff0c;最终上升到哲学价值观层&#xff01; https://mp.weixin.qq.com/s/FDJ0OwxKpB_nU3wNDd4Gcw

作者头像 李华
网站建设 2026/4/2 13:39:27

STM32CubeMX安装教程:配合Keil MDK的集成设置

从零开始搭建STM32开发环境&#xff1a;CubeMX Keil的黄金组合实战指南 你是不是也经历过这样的场景&#xff1f;刚拿到一块崭新的STM32开发板&#xff0c;满心欢喜地打开Keil准备写代码&#xff0c;结果卡在了第一步——时钟怎么配&#xff1f;GPIO初始化写错了没发现&#…

作者头像 李华
网站建设 2026/4/1 20:08:51

大模型推理服务灰度审批流程自动化

大模型推理服务灰度审批流程自动化 在现代AI系统中&#xff0c;大模型的上线早已不再是“训练完就部署”的简单操作。尤其是在金融、电商、搜索推荐等高并发场景下&#xff0c;一次未经充分验证的模型发布可能引发延迟飙升、GPU显存溢出甚至服务雪崩。如何在保证用户体验的前提…

作者头像 李华
网站建设 2026/3/13 5:42:12

2025最新!8个AI论文工具测评:研究生开题报告必备攻略

2025最新&#xff01;8个AI论文工具测评&#xff1a;研究生开题报告必备攻略 2025年AI论文工具测评&#xff1a;助力研究生高效完成开题报告 随着人工智能技术的不断发展&#xff0c;AI论文工具已经成为研究生在撰写开题报告、论文写作等环节中不可或缺的辅助工具。然而&#x…

作者头像 李华
网站建设 2026/3/13 8:37:38

使用TensorRT优化增量学习模型的推理阶段

使用TensorRT优化增量学习模型的推理阶段 在现代AI系统中&#xff0c;模型不再是一次训练、长期使用的“静态资产”&#xff0c;而是持续演进的“动态服务”。尤其在推荐系统、智能监控和个性化内容生成等场景下&#xff0c;增量学习&#xff08;Incremental Learning&#xf…

作者头像 李华
网站建设 2026/4/3 4:31:10

STM32上拉电阻对信号稳定性的影响分析

上拉电阻&#xff1a;STM32系统中被低估的“稳定器”你有没有遇到过这样的情况&#xff1f;一个简单的按键电路&#xff0c;明明没按&#xff0c;MCU却频繁检测到“按下”&#xff1b;IC总线在实验室工作正常&#xff0c;一上产线就通信失败&#xff1b;或者&#xff0c;设备长…

作者头像 李华