恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Redis大Key优化实战:从原理到解决方案与面试指南

  • 首页
  • 资讯中心
  • /
  • Redis大Key优化实战:从原理到解决方案与面试指南

相关资讯

原神帧率解锁完整指南:genshin-fps-unlock 快速突破 60 帧限制 2026/8/24 2:36:31
ExplorerPatcher完整指南:免费找回Windows 10经典任务栏与开始菜单 2026/8/24 2:36:31
RetroArch语言切换完整指南:3步把模拟器界面改成中文 2026/8/24 2:36:31

最新资讯

基于SpringBoot的膳食搭配营养学知识智能问答小程序的实现源码+文档
嵌入式开发必备:I2S、UART、SPI、I2C四大低速协议核心原理与实战避坑指南
Android背景属性深度解析:从Activity、Window到View的渲染优化实践
适合大体重的跑步机有哪些?十款机型按体重强度对号入座
AI短剧视频分辨率选型与生产注意事项
大模型微调面试核心技术与实战解析

今日推荐

OpenModScan:免费跨平台 Modbus 主站调试工具,让现场通讯验证一键搞定
WechatHook 终极指南:5大核心能力详解,3分钟看懂微信自动化
如何在ThinkPad X390上安装macOS:OpenCore EFI完整指南

本周热门

Nextcloud 桌面客户端:把同步交给它,你只管改文件
如何将 HTML 转成 Word 文档且格式不丢失?html-to-docx 使用教程
Anki 批量操作卡片完整指南:一次搞定上千张,不再逐张修改

本月精选

如何用DamaiHelper实现演唱会门票的智能自动化抢购:完整技术解决方案指南
第4篇:59 倍性能差距的索引瓶颈定位——一次教科书级的全表扫描调优
终极歌词批量下载神器:5分钟解决离线音乐库歌词同步难题

Redis大Key优化实战:从原理到解决方案与面试指南

发布时间:2026/8/24 2:36:31
Redis大Key优化实战:从原理到解决方案与面试指南 大家好我是专注于后端技术分享的博主。在Java后端开发面试中尤其是秋招季Redis相关的性能优化问题是面试官考察候选人工程实践能力的“必考题”。其中“Redis Key过大如何优化”这个问题看似简单实则能深入考察你对Redis数据结构、内存模型、编码方式以及业务设计的综合理解。很多同学在回答时容易陷入“分拆Key”的单一思路导致回答深度不够难以获得高分。本文将为你系统性地拆解这个问题不仅提供一个结构清晰、内容充实的“满分回答模板”更会深入剖析其背后的原理、多种实战优化方案并附上可运行的代码示例和排查清单。无论你是正在准备面试的应届生还是希望提升Redis应用水平的开发者都能从中获得实用的知识和清晰的回答思路。1. 问题背景与核心影响分析在深入优化方案之前我们首先要理解“Redis Key过大”这个问题的本质及其带来的危害。这不仅是回答面试题的第一步也是在真实项目中定位性能瓶颈的起点。1.1 什么是“大Key”在Redis语境中“大Key”通常指以下两种情况Key对应的Value过大例如一个String类型的Value大小达到几MB甚至几十MB如存储一篇长文章、一张大图片的Base64编码。Key对应的元素数量过多例如一个Hash、List、Set或ZSet中包含了成千上万个成员。业界通常有一个经验性的阈值当单个String类型的Value超过10KB或集合元素数量超过5000个时就需要开始关注其潜在风险。1.2 大Key会带来哪些问题理解危害是优化动机的来源。大Key主要会引发以下几方面问题内存使用不均可能导致Redis实例内存使用率飙升甚至触发OOM影响其他Key的存储。阻塞风险Redis是单线程处理命令的。对一个大Key进行DEL、HGETALL、LRANGE等操作可能会长时间阻塞进程导致后续命令延迟飙升影响整个服务的响应时间。这在集群模式下对某个节点执行KEYS *或FLUSHDB时尤为致命。网络拥塞一次读取或写入大Value会占用大量网络带宽可能导致连接超时或影响其他服务的网络通信。数据迁移困难在Redis Cluster中进行数据迁移或扩容时大Key会成为“钉子户”显著拖慢整个迁移过程。持久化与主从同步压力生成RDB快照或AOF重写时处理大Key会消耗大量CPU和I/O资源。主从同步时一个大Key也可能导致复制缓冲区溢出或同步延迟。2. 环境准备与模拟大Key场景为了更直观地理解问题并验证优化方案我们先搭建一个简单的实验环境。本文示例基于以下常见环境你可以根据实际情况调整。Redis版本6.2 (单机模式便于演示)Java环境JDK 8客户端使用Spring Boot Lettuce (或Jedis)IDEIntelliJ IDEA 或 Eclipse首先我们创建一个Spring Boot项目并添加Redis依赖。!-- pom.xml -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency接下来我们模拟两种典型的大Key场景为后续的优化方案提供问题原型。场景一巨大的String Value假设我们需要缓存一个复杂的、嵌套层级很深的产品详情JSON这个JSON字符串可能非常大。// 模拟生成一个大的JSON字符串约1MB public String generateLargeProductDetailJson() { // 这里简化模拟实际可能来自数据库或外部API StringBuilder sb new StringBuilder(); sb.append({); sb.append(\id\: 12345,); sb.append(\name\: \超大号豪华产品\,); sb.append(\description\: \).append(这是一个非常长的描述...).append(\,); // ... 模拟添加大量属性、规格参数、图片列表等使字符串膨胀 for (int i 0; i 10000; i) { sb.append(\attr).append(i).append(\: \value).append(i).append(\,); } sb.append(\price\: 9999.99); sb.append(}); return sb.toString(); } // 一个错误的使用示例将整个大JSON存入一个Key Autowired private StringRedisTemplate stringRedisTemplate; public void saveLargeKeyBadExample() { String largeJson generateLargeProductDetailJson(); // 问题点将整个1MB的字符串存入一个Key stringRedisTemplate.opsForValue().set(product:detail:12345, largeJson); }场景二元素数量巨大的Hash假设我们需要存储一个用户的所有签到记录用户ID为Keyfield为日期value为签到详情。// 模拟一个用户多年的签到记录 public void saveLargeHashBadExample(String userId) { // 问题点将用户所有签到记录比如1000天放在一个Hash里 for (int i 0; i 1000; i) { String date LocalDate.now().minusDays(i).toString(); String checkInInfo {\time\:\08:00\, \points\:10}; stringRedisTemplate.opsForHash().put(user:checkin: userId, date, checkInInfo); } }运行以上代码后你可以使用Redis命令行工具redis-cli通过memory usage keyname命令来查看这些Key的内存占用直观感受“大Key”的体量。3. 核心优化方案与原理拆解面对大Key问题我们需要一套从诊断到治理的完整方案。下面将分步骤详细讲解。3.1 诊断与发现如何识别大Key优化始于发现。在生产环境中我们不能依赖猜测。以下是几种常见的发现手段使用Redis内置命令redis-cli --bigkeys这是一个扫描整个数据库并统计每种数据类型中最大Key的命令。它执行的是SCAN操作对服务影响相对较小适合在低峰期运行。$ redis-cli --bigkeys # 输出示例 # Biggest string found so far product:detail:12345 with 1048576 bytes # Biggest hash found so far user:checkin:1001 with 1000 fieldsMEMORY USAGE keyname精确计算某个特定Key及其Value在内存中占用的字节数。使用开源工具redis-rdb-tools这是一个Python工具可以离线分析RDB文件生成详细的内存报告精准定位所有大Key。# 生成内存报告 $ rdb -c memory dump.rdb --bytes 1024 memory_report.csv # 然后可以按大小排序查看 $ sort -t, -k4 -nr memory_report.csv | head -20通过监控告警在公司的监控系统如Prometheus中可以配置对Redis实例内存使用率、慢查询slowlog的告警。如果发现某个实例内存增长异常或频繁出现耗时长的命令可能就是大Key操作的信号。3.2 方案一拆分大Key分而治之这是最直观和常用的方法。核心思想是将一个大的数据集合拆分成多个小的、独立的Key。String大Value拆分方法将大文本如JSON、HTML拆分成多个块。例如将一篇长文章按章节或固定大小如10KB分块存储。存储使用多个Key如article:1001:part1,article:1001:part2。元数据需要一个额外的Key如article:1001:metadata来记录总块数、每块大小等信息用于后续的组装读取。// 优化示例拆分大String public void saveLargeArticle(String articleId, String largeContent) { int chunkSize 10 * 1024; // 10KB per chunk ListString chunks splitStringIntoChunks(largeContent, chunkSize); // 1. 存储分块 for (int i 0; i chunks.size(); i) { String chunkKey String.format(article:%s:chunk:%d, articleId, i); stringRedisTemplate.opsForValue().set(chunkKey, chunks.get(i)); } // 2. 存储元数据 MapString, String meta new HashMap(); meta.put(totalChunks, String.valueOf(chunks.size())); meta.put(chunkSize, String.valueOf(chunkSize)); meta.put(originalLength, String.valueOf(largeContent.length())); stringRedisTemplate.opsForHash().putAll(article: articleId :meta, meta); } // 读取时先获取元数据再并发获取所有分块并拼接 public String getLargeArticle(String articleId) { // 获取元数据 MapObject, Object meta stringRedisTemplate.opsForHash().entries(article: articleId :meta); int totalChunks Integer.parseInt((String)meta.get(totalChunks)); // 并发获取所有分块可以使用管道pipeline优化 ListString chunkKeys new ArrayList(); for (int i 0; i totalChunks; i) { chunkKeys.add(String.format(article:%s:chunk:%d, articleId, i)); } ListString chunks stringRedisTemplate.opsForValue().multiGet(chunkKeys); // 拼接并返回 return String.join(, chunks); }集合类大Key拆分方法按业务维度拆分。例如将用户1000天的签到记录按年或按月拆分到不同的Hash中。存储user:checkin:1001:2023,user:checkin:1001:2024。优点对某个时间范围的查询非常高效删除过期数据也方便直接删除整个子Key。// 优化示例按时间维度拆分Hash public void saveCheckInByMonth(String userId, LocalDate date, String checkInInfo) { // 根据日期生成子Key例如按年月 String monthKey date.format(DateTimeFormatter.ofPattern(yyyy-MM)); String hashKey String.format(user:checkin:%s:%s, userId, monthKey); String field date.toString(); stringRedisTemplate.opsForHash().put(hashKey, field, checkInInfo); } // 查询某用户某个月的签到记录 public MapObject, Object getCheckInForMonth(String userId, String yearMonth) { String hashKey String.format(user:checkin:%s:%s, userId, yearMonth); return stringRedisTemplate.opsForHash().entries(hashKey); }3.3 方案二使用更高效的数据结构有时大Key的产生是因为选择了不合适的结构。Redis提供了多种高效的数据结构。将String转换为Hash如果一个大JSON中只有部分字段需要频繁访问将其存储为String每次都要序列化/反序列化整个字符串浪费CPU和网络。可以将其拆解存储到Hash中这样可以使用HGET、HMGET命令只获取需要的字段。// 优化示例使用Hash存储对象的部分字段 public void saveProductDetailAsHash(Long productId, ProductDetail detail) { String hashKey product:hash: productId; MapString, String map new HashMap(); // 只存储需要独立查询或更新的字段 map.put(name, detail.getName()); map.put(price, String.valueOf(detail.getPrice())); map.put(mainImage, detail.getMainImageUrl()); // 将不常变动的、大的描述性文本单独存储或拆分 // map.put(description, detail.getDescription()); // 如果很大考虑拆分 stringRedisTemplate.opsForHash().putAll(hashKey, map); // 大的描述字段单独存一个Key或者按方案一拆分 stringRedisTemplate.opsForValue().set(product:desc: productId, detail.getDescription()); } // 只获取商品名称和价格 public ProductSimpleInfo getProductSimpleInfo(Long productId) { String hashKey product:hash: productId; ListObject fields stringRedisTemplate.opsForHash().multiGet(hashKey, Arrays.asList(name, price)); // ... 构造返回对象 }使用ZSet替代List进行分页如果需要存储一个巨大的列表并提供分页查询使用List的LRANGE命令在数据量大时效率不高。如果列表元素有可排序的分数如时间戳使用ZSet的ZRANGEBYSCORE或ZREVRANGE进行分页会更高效且可以轻松去重。3.4 方案三启用压缩与选择合适的编码Redis底层对不同的数据结构和大小有不同的编码方式理解并利用这一点可以节省内存。Redis Object EncodingRedis会根据Value的大小自动选择内存效率更高的编码。例如小的Hash字段少且每个field和value长度小会使用ziplist压缩列表编码非常节省内存。当元素数量或大小超过hash-max-ziplist-entries和hash-max-ziplist-value配置的阈值时才会转换为标准的hashtable。你可以通过OBJECT ENCODING keyname命令查看Key的编码方式。调整配置以优化内存通过调整Redis配置文件(redis.conf)中的相关参数可以让更多的小集合使用压缩编码。但要注意这可能会略微增加CPU开销。# 让元素数量少于512的Hash使用ziplist hash-max-ziplist-entries 512 # 让每个元素值小于64字节的Hash使用ziplist hash-max-ziplist-value 64 # 类似的配置还有 list-max-ziplist-entries, list-max-ziplist-value, zset-max-ziplist-entries 等注意调整这些参数需要充分测试因为过小的阈值可能导致频繁的编码转换影响性能。客户端压缩对于Value确实是String且非常大的场景可以在写入Redis前在客户端进行压缩如使用GZIP或Snappy读取时再解压。这牺牲了CPU换取了网络和内存的节省适用于存储频率低、读取频率也相对不高的场景。public void saveCompressedValue(String key, String largeValue) throws IOException { ByteArrayOutputStream bos new ByteArrayOutputStream(); try (GZIPOutputStream gzip new GZIPOutputStream(bos)) { gzip.write(largeValue.getBytes(StandardCharsets.UTF_8)); } byte[] compressed bos.toByteArray(); // 存储压缩后的字节数组注意RedisTemplate的序列化器需要能处理byte[] stringRedisTemplate.opsForValue().set(key, Base64.getEncoder().encodeToString(compressed)); }3.5 方案四业务架构优化治本之策技术手段治标业务设计治本。从根本上避免大Key的产生往往是最有效的。数据分级存储并非所有数据都适合放在Redis中。对于体积巨大、访问频率较低的数据如用户上传的原始视频、大型文档应该存储在对象存储如S3、OSS或文件系统中Redis里只保存其访问地址URL和元数据。设置合理的过期时间给缓存数据设置TTLTime-To-Live让不活跃的大Key自动过期淘汰防止其常驻内存。结合LRU等内存淘汰策略保障Redis的稳定运行。异步删除从Redis 4.0开始支持使用UNLINK命令替代DEL命令来删除大Key。UNLINK会将Key从键空间立即移除但实际的内存回收会在后台线程中异步进行避免了同步删除导致的阻塞。// 使用UNLINK进行异步删除 stringRedisTemplate.unlink(large:key:name);定期清理与归档对于历史数据类的大Key如用户操作日志的List建立定期归档清理机制。例如每晚将30天前的数据转移到MySQL或HDFS并从Redis中删除。4. 完整实战一个用户画像缓存优化案例让我们通过一个综合案例将上述方案串联起来。假设我们有一个“用户画像”系统最初的设计是将用户的所有标签可能多达上万个存储在一个大的Set中Key为user:tags:{uid}。初始问题随着用户标签越来越多这个Set变得巨大执行SMEMBERS命令时网络传输和客户端反序列化压力大且影响其他命令。优化目标减少单Key体积。支持高效查询用户是否拥有某个标签。支持分页获取用户标签。优化步骤步骤1诊断与确认使用redis-cli --bigkeys或分析工具确认user:tags:*系列的Key是否过大。步骤2选择优化方案方案一拆分按标签类型或首字母哈希将一个大Set拆分成多个小Set。方案二高效数据结构我们已经在用Set它是高效的。但可以考虑是否需要用Sorted Set来支持按权重排序。方案三编码检查Set的编码如果是hashtable考虑是否可以通过拆分让部分子Set满足intset整数集合的编码条件以节省内存。方案四业务是否所有标签都需要实时缓存可以将冷门标签移出Redis。我们选择**方案一按标签类别拆分和方案四分级存储**结合。步骤3实施优化Service public class UserTagServiceOptimized { Autowired private StringRedisTemplate redisTemplate; // 假设标签有类别如 interest, behavior, demographic public void addUserTag(Long userId, String tagCategory, String tagValue) { String setKey String.format(user:tags:%s:%s, userId, tagCategory); redisTemplate.opsForSet().add(setKey, tagValue); // 同时维护一个元数据Key记录用户有哪些标签类别可选 String metaKey String.format(user:tags:meta:%s, userId); redisTemplate.opsForSet().add(metaKey, tagCategory); } // 判断用户是否拥有某个特定类别的标签 public boolean hasTag(Long userId, String tagCategory, String tagValue) { String setKey String.format(user:tags:%s:%s, userId, tagCategory); return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(setKey, tagValue)); } // 分页获取用户某个类别的所有标签 public SetString getTagsByCategory(Long userId, String tagCategory, int page, int size) { String setKey String.format(user:tags:%s:%s, userId, tagCategory); // 注意Set是无序的直接分页可能每次结果顺序不一致。 // 如果需要稳定分页可以考虑使用ZSet并为每个标签添加一个固定的分数如添加时间戳。 // 这里演示使用SRANDMEMBER但这不是严格的分页。 // 更优方案是使用ZSet的ZRANGE。 long start (page - 1) * size; long end start size - 1; // 对于Set我们可以先获取全部然后在内存中分页仅当单个子Set不大时可行 SetString allTags redisTemplate.opsForSet().members(setKey); if (allTags null) return Collections.emptySet(); return allTags.stream().skip(start).limit(size).collect(Collectors.toSet()); } // 定期清理冷门标签类别业务优化 public void archiveColdTags(Long userId, String coldCategory) { String setKey String.format(user:tags:%s:%s, userId, coldCategory); // 1. 将数据持久化到数据库 SetString coldTags redisTemplate.opsForSet().members(setKey); // ... 保存到MySQL的代码 ... // 2. 从Redis中删除这个子Key redisTemplate.unlink(setKey); // 使用异步删除 // 3. 更新元数据 String metaKey String.format(user:tags:meta:%s, userId); redisTemplate.opsForSet().remove(metaKey, coldCategory); } }步骤4验证效果优化后原先的user:tags:1001这个大Key被拆分为user:tags:1001:interest、user:tags:1001:behavior等多个小Key。每个子Key的体积可控执行SMEMBERS或SISMEMBER命令的速度更快网络传输量更小。同时按类别查询也更符合业务逻辑。5. 常见问题与排查思路在实际优化过程中你可能会遇到以下问题问题现象可能原因排查与解决思路执行DEL大Key时Redis卡顿同步删除大Key阻塞主线程1. 使用UNLINK命令异步删除。2. 对于Redis 4.0以下版本可以渐进式删除对于Hash/Set等先用HSCAN/SSCAN迭代删除元素最后删Key。redis-cli --bigkeys扫描不出数据1. 数据库为空。2. 扫描期间有大量写入/删除。3. Key过期被淘汰。1. 确认数据库有数据。2. 在业务低峰期执行。3. 使用rdb工具分析持久化文件更准确。拆分Key后查询性能反而下降1. 拆分过细导致查询需要多次网络往返。2. 没有使用管道pipeline或Lua脚本保证原子性。1. 评估拆分粒度避免一个查询需要聚合数十个Key。2. 对于需要原子性获取多个子Key的场景使用MGET、管道或Lua脚本。调整*-max-ziplist-*参数后内存下降不明显1. 大Key的主要部分不满足ziplist条件。2. 内存碎片严重。1. 使用OBJECT ENCODING确认Key的编码是否已改变。2. 考虑使用MEMORY PURGERedis 4.0或重启来清理内存碎片。客户端反序列化大Key时OOM客户端应用程序内存不足。1. 根本解决优化Redis存储避免返回过大Value。2. 临时方案增加客户端JVM堆内存。3. 使用流式读取如果客户端支持。6. 最佳实践与工程建议将大Key优化方案融入日常开发流程才能防患于未然。设计规范先行在项目初期制定Redis使用规范明确禁止存储超过一定大小如1MB的Value。规定集合类元素数量的上限如List/Hash/Set元素不超过5000个。在Code Review环节加入对Redis Key设计的检查。监控与告警常态化将redis-cli --bigkeys的扫描任务集成到运维监控平台定期如每天凌晨执行并报告结果。对Redis实例设置慢查询日志(slowlog)监控告警阈值设置为例如10毫秒及时发现并处理慢查询。选择合理的序列化方案在Spring Boot中默认的JdkSerializationRedisSerializer效率低且体积大。优先使用StringRedisSerializer存储字符串或GenericJackson2JsonRedisSerializer存储JSON对象。对于极度追求性能的场景可以考虑Kryo或Protostuff等二进制序列化方案但要注意可读性和兼容性。使用管道与Lua脚本当优化方案涉及多次读写多个拆分后的Key时应使用管道Pipeline来减少网络往返次数提升性能。ListObject results redisTemplate.executePipelined((RedisCallbackObject) connection - { for (int i 0; i 100; i) { connection.stringCommands().set((key: i).getBytes(), (value: i).getBytes()); } return null; });对于需要原子性执行的复杂操作如先检查再设置多个Key应使用Lua脚本保证在服务器端原子执行。容量规划与治理对线上Redis实例做好容量规划预留一定的Buffer如30%。建立大Key治理流程发现 - 评估 - 优化/迁移 - 验证 - 清理。7. 面试满分回答模板与思路扩展最后回到我们文章的起点——面试。当被问到“Redis Key过大如何优化”时你可以按照以下结构组织你的回答展现你的系统思维和工程能力。回答模板“面试官您好关于Redis大Key的优化我会从发现问题、分析问题、解决问题和预防问题四个层面来阐述。首先如何发现大Key我会通过线上监控、定期使用redis-cli --bigkeys扫描或者利用redis-rdb-tools分析RDB文件来主动发现潜在的大Key。同时关注慢查询日志也是重要手段。其次分析大Key带来的具体问题。大Key主要会引发内存不均、命令阻塞特别是DEL、HGETALL、网络流量突增、数据迁移困难以及持久化压力大等问题影响Redis的稳定性和性能。然后是核心的解决方案我会根据场景选择或组合使用拆分这是最常用的方法。对于大String可以分块存储对于大集合可以按业务维度如时间、用户ID哈希拆分成多个小Key。选用合适数据结构比如将需要部分字段查询的大JSON从String转为Hash用ZSet代替List实现有序且高效的分页。利用Redis编码优化理解并合理配置hash-max-ziplist-entries等参数让小的集合使用更节省内存的ziplist或intset编码。客户端压缩对于确实需要存放大文本且访问不频繁的场景可以在客户端压缩后再存储。业务架构优化这是治本之策。包括数据分级存储热数据放Redis冷数据放数据库/对象存储、设置合理的过期时间、使用UNLINK异步删除以及建立定期的数据归档清理机制。最后如何预防我会在项目中制定开发规范明确Key的设计原则和大小限制并将大Key扫描纳入常态化的监控告警体系在Code Review中也会重点关注Redis的使用方式。举个例子我之前在处理一个用户标签系统时发现存储所有标签的Set过大。我的优化方案是按标签类别拆分到不同的Set中并为每个用户维护一个标签类别的元数据集合。这样既控制了单个Key的大小也使得按类别查询标签更加高效。”通过这样的回答你不仅展示了知识广度从发现到解决也体现了深度多种方案及其原理更通过实际案例证明了你的实践能力这样的回答无疑会是面试中的亮点。希望这篇详尽的指南能帮助你在面试和实际工作中游刃有余地应对Redis大Key挑战。理解原理灵活运用才能打造出高性能、高可用的缓存系统。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号