网站建设有免费的吗,asp企业建站系统,网站后台管理怎么做,重庆在线app背景#xff1a; 在一线互联网大厂#xff08;阿里、字节等#xff09;的面试中#xff0c;Redis 的 BigKey 优化是必考题。 但面试官通常不会只问“什么是 BigKey”#xff0c;而是会抛出一个极具挑战性的场景#xff1a; “线上有一个亿级数据的 BigKey#xff08;如 …背景 在一线互联网大厂阿里、字节等的面试中Redis 的 BigKey 优化是必考题。 但面试官通常不会只问“什么是 BigKey”而是会抛出一个极具挑战性的场景“线上有一个亿级数据的 BigKey如 Hash 类型正在承载核心业务。现在要求你对其进行优化拆分要求1. 业务全程无感知2. 绝对不能阻塞 Redis3. 流量不能穿透到数据库。你怎么做”这是一道典型的“飞行中换引擎”的架构题。本文将从设计到落地手把手教你设计一套教科书级的解决方案。一、 核心挑战分析在动手写代码之前我们必须先拆解面试官给出的三个“紧箍咒”不能影响现有业务意味着不能停机不能有明显的抖动必须平滑过渡。不能阻塞 Redis意味着不能使用DEL、HGETALL等 $O(N)$ 复杂度的命令必须利用分治思想。请求不能大量到库这是最关键的。在数据迁移过程中缓存不能失效。如果直接删除老 Key 等待重建数据库瞬间就会被百万 QPS 打死缓存雪崩。结论我们必须采用“双写 渐进式迁移 动态路由 异步删除”的组合拳。二、 总体架构方案假设我们的 BigKey 是一个存储用户详情的 HashKey 为user:info:all内部包含 1000 万个字段field 为 userIdvalue 为 JSON。我们的目标是将其拆分为 100 个小 Hashuser:info:0到user:info:99。核心步骤双写阶段修改代码对写操作同时写入“新 Key”和“老 Key”。迁移阶段启动后台程序利用HSCAN渐进式地把“老 Key”的数据搬运到“新 Key”。切读阶段利用配置中心Nacos/Apollo进行灰度发布逐步将读流量从“老 Key”切换到“新 Key”。清理阶段确认无误后异步删除“老 Key”。三、 详细实施步骤Step 1数据分片设计 (Sharding)首先确定分片策略。最常用的是取模算法。分片公式shard_id hash(userId) % 100Key 命名规则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:allwhile True:# 1. 使用 HSCAN 每次只拉取 1000 条避免阻塞# cursor 是游标每次返回新的游标和数据cursor, data redis.hscan(old_key, cursorcursor, count1000)if not data:break # 数据为空结束# 2. 在内存中进行分片计算pipeline redis.pipeline()for uid, info_json in data.items():shard_id hash(uid) % 100new_key fuser: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);}操作流程初始状态比例 0%全读老 Key。观察期调至 1%观察日志、Redis 命中率、业务报错。放量期逐步调至 10% - 50% - 100%。全量后保持运行一段时间确保新 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 以下使用HSCANHDEL。写一个脚本每次 scan 1000 个字段然后 delete 这 1000 个字段循环执行直到删空。四、 总结与防坑指南回顾我们的方案是如何完美解决面试官的三个难题的挑战解决方案不影响业务双写机制保证新老数据实时同步灰度切读控制风险随时可回滚。不阻塞 RedisHSCAN 迁移化整为零分批搬运UNLINK 删除异步回收内存。不穿透数据库一级兜底策略新 Key 查不到时强制回源查老 Key因为老 Key 一直没删从而保护了数据库。最后的防坑 Tips迁移脚本的幂等性迁移脚本可能会中断重启代码必须设计为可重入的Set 操作本身就是幂等的这很好。过期时间如果老 Key 有过期时间新 Key 必须继承甚至设置得稍微长一点。Hash Tag如果你使用的是 Redis Cluster且需要在 Lua 脚本中同时操作多个新 Key记得在 Key 设计时加上 Hash Tag例如{user:info}:1但在纯分片场景下通常不需要。掌握了这套“分片双写迁移兜底异步删”的组合拳你不仅能搞定 BigKey还能解决绝大多数数据迁移类的架构难题。https://mp.weixin.qq.com/s/niJ7M9FKvnB-EkK8Ci8CuQ