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

Spring Boot 3 Redis工具类封装:从原理到生产级实践

  • 首页
  • 资讯中心
  • /
  • Spring Boot 3 Redis工具类封装:从原理到生产级实践

相关资讯

英语写作中extend、expand、enlarge的精准区分与使用指南 2026/8/17 13:51:54
XXL-JOB单机串行策略深度解析:阻塞处理与集群路由实战 2026/8/17 13:51:54
大模型长上下文推理优化:4-bit KV Cache量化技术解析与部署实践 2026/8/17 13:51:54

最新资讯

Windows系统进程管理与网络通信控制技术解析:以极域电子教室为例
遗传算法在电力经济调度中的应用与优化
Azure免费虚拟机申请与避坑指南:12个月B1s实例实战
大厂面试逻辑题深度解析:从海盗分金到费米估算,掌握思维考察核心
2026年论文双降工具测评:重复率AI率双高怎么破
Oracle GoldenGate 19c 实战部署:从零构建高可用数据同步链路

今日推荐

LabVIEW异步调用实战:从原理到生产者消费者模式,解决界面卡顿与并行处理难题
LabVIEW异步调用实战:解决界面卡顿与并行处理难题
飞书局域网文件传输实战:3种方案实现高速点对点传输

本周热门

【文章复现】非线性值迭代自适应动态规划(ADP):离散时间非线性系统的策略迭代自适应动态规划算法研究附Matlab代码
【双层规划,节点出清价,绿证交易,CVaR方法】两级电力市场环境下计及风险的省间交易商最优购电模型附Matlab代码
隐式mpc+自适应mpc+时变mpc,线性时变模型预测控制附Simulink仿真

本月精选

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

Spring Boot 3 Redis工具类封装:从原理到生产级实践

发布时间:2026/8/17 13:51:54
Spring Boot 3 Redis工具类封装:从原理到生产级实践 1. 项目概述为什么我们需要一个自己的Redis工具类在基于Spring Boot 3开发后端服务时Redis几乎是缓存、会话管理和分布式锁等场景下的标配。官方提供的RedisTemplate功能强大但直接使用它代码里会充斥着大量样板代码比如序列化配置、连接获取、异常处理等。更头疼的是不同开发者对opsForValue()、opsForHash()等API的调用习惯不一导致项目中的Redis操作代码风格各异维护起来像在读天书。“基于SpringBoot3引入Redis并封装常用的操作RedisUtils”这个项目核心目标就是解决这些痛点。它不是一个简单的工具类搬运而是一次针对生产环境的深度定制。通过封装我们将散落在各处的Redis操作收敛到一个统一的工具类中提供一套简洁、健壮、符合团队编码规范的API。这不仅能提升开发效率减少重复代码更能统一异常处理、连接管理和监控埋点为线上服务的稳定性打下基础。无论你是刚接触Spring Boot和Redis的新手还是寻求最佳实践的老鸟构建一个属于自己的RedisUtils都是值得投入的“基础设施”投资。2. 整体设计与核心思路拆解2.1 技术栈选型与版本考量首先明确我们的技术栈Spring Boot 3.x 和 Redis。Spring Boot 3基于Spring Framework 6和Java 17带来了诸多新特性比如对GraalVM原生镜像的更好支持、全新的HTTP客户端等。在依赖选择上我们使用spring-boot-starter-data-redis它是Spring Data Redis的Starter默认使用Lettuce作为连接客户端。为什么是Lettuce而不是JedisLettuce基于Netty实现支持响应式编程连接是线程安全的可以在多个线程间共享性能更高特别是在高并发场景下。对于绝大多数项目Lettuce是更现代、更推荐的选择。关于Redis服务器版本建议使用5.0及以上以支持更丰富的命令和数据结构。工具类的设计需要兼顾通用性和扩展性既要覆盖字符串、哈希、列表、集合、有序集合等基本数据类型的CRUD也要为分布式锁、限流器等高级功能预留接口。2.2 工具类的核心设计原则在设计RedisUtils时我遵循以下几个核心原则这些原则直接决定了工具类的可用性和生命力简洁直观的API方法名应见名知意如set(String key, Object value)、hGet(String key, String field)。避免让使用者去记忆复杂的Spring Data Redis原生API。泛型支持与自动序列化工具类应能智能处理Java对象与Redis存储字节之间的转换。我们利用Spring Boot配置的RedisTemplate的序列化器但对外提供泛型方法让使用者无需关心序列化细节。统一的异常处理Redis操作可能因网络、超时、命令错误等失败。工具类不应把RedisConnectionFailureException或SerializationException直接抛给业务代码而应进行包装或至少提供更友好的错误信息并确保连接池能正确释放。连接资源管理虽然RedisTemplate和Lettuce封装了连接池但在工具类中执行多条命令时仍需注意避免不必要的连接获取与释放。对于需要事务或管道pipelining的场景设计要格外小心。可扩展性工具类不应是一个“黑盒”。它应该允许使用者方便地获取底层的RedisTemplate或RedisConnectionFactory以应对那些未被封装的特殊命令。同时设计应便于添加新的操作方法如基于Redis的布隆过滤器、位图操作等。基于这些原则我们的RedisUtils通常会设计成静态方法工具类内部持有一个由Spring注入的RedisTemplate实例。这里的关键是如何在静态方法中访问Spring容器管理的Bean。3. 核心细节解析与实操要点3.1 依赖引入与基础配置在pom.xml中引入必要的依赖。除了spring-boot-starter-data-redis为了更方便地处理JSON序列化我强烈推荐引入Jackson依赖。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency !-- 如果你使用Redis的Jackson序列化可能需要这个 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-json/artifactId /dependency接下来是application.yml的配置。这里有几个关键点直接影响性能和稳定性spring: data: redis: host: localhost # Redis服务器地址 port: 6379 # 端口 password: # 密码无则留空 database: 0 # 数据库索引默认0 lettuce: pool: max-active: 8 # 连接池最大连接数使用负值表示没有限制。根据并发量调整不建议过大。 max-idle: 8 # 连接池中的最大空闲连接 min-idle: 0 # 连接池中的最小空闲连接 max-wait: -1ms # 连接池最大阻塞等待时间使用负值表示没有限制 shutdown-timeout: 100ms # 关闭超时时间 timeout: 2000ms # 连接超时时间注意max-active参数需要根据实际QPS和业务场景调整。设置过小会导致等待连接过大则浪费资源并可能压垮Redis。一个经验公式是max-active ≈ 最大QPS * 平均命令耗时(秒)。例如QPS为1000平均命令耗时1ms则大约需要10个连接。通常从8-16开始测试。3.2 序列化方案选择与陷阱序列化是Redis工具类的基石。Spring Boot默认使用JdkSerializationRedisSerializer它序列化后的值不可读且依赖Java类路径不同服务间兼容性差。生产环境绝对不要用。推荐方案GenericJackson2JsonRedisSerializer这是最通用的方案。它能将对象序列化为JSON字符串存储并附带类类型信息反序列化时能自动还原为原类型。配置如下Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(connectionFactory); // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值 Jackson2JsonRedisSerializerObject serializer new Jackson2JsonRedisSerializer(Object.class); ObjectMapper mapper new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 此项必须配置否则反序列化时如果类型信息缺失会报错 mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(mapper); // 设置key和hash key采用String序列化 template.setKeySerializer(RedisSerializer.string()); template.setHashKeySerializer(RedisSerializer.string()); // 设置value和hash value采用Jackson序列化 template.setValueSerializer(serializer); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } }实操心得mapper.activateDefaultTyping(...)这行代码是关键。它会在JSON中加入class字段来存储类型信息。但这也带来了安全风险反序列化漏洞和存储空间开销。对于纯粹存储String、Map、List等简单类型的场景可以考虑使用StringRedisSerializer配合手动JSON转换牺牲一点便利性换取安全和效率。3.3 静态工具类的Spring Bean注入难题这是一个经典问题。工具类RedisUtils是静态的但RedisTemplate需要Spring容器注入。常见的解决方案有几种静态持有ApplicationContext在工具类初始化时通过一个实现了ApplicationContextAware的配置类将ApplicationContext保存到静态变量中。使用时再通过context.getBean(RedisTemplate.class)获取。这种方式简单但不够优雅且存在循环依赖风险。PostConstruct初始化将工具类本身也声明为Component在其中定义一个静态的RedisTemplate变量在PostConstruct方法中将注入的实例赋值给静态变量。推荐方案基于“InitializingBean”的优雅持有我更喜欢下面这种方式它更清晰地将初始化和使用分离。Component public class RedisUtils implements InitializingBean { private static RedisTemplateString, Object redisTemplateStatic; Autowired private RedisTemplateString, Object redisTemplate; Override public void afterPropertiesSet() { redisTemplateStatic this.redisTemplate; } // 接下来所有的静态方法都通过 redisTemplateStatic 来操作 public static Boolean set(String key, Object value) { try { redisTemplateStatic.opsForValue().set(key, value); return true; } catch (Exception e) { log.error(Redis set key: {} error, key, e); return false; } } // ... 其他静态方法 }这种方式确保了RedisTemplate在Spring容器完全初始化后才被静态变量引用避免了潜在的NullPointerException。4. 实操过程封装核心操作方法下面我们开始构建RedisUtils的骨架并实现最常用的几类操作。我会为每个方法附上详细的说明和注意事项。4.1 基础Key操作封装这些操作不关心value的具体类型只针对key本身。public static Boolean hasKey(String key) { try { return redisTemplateStatic.hasKey(key); } catch (Exception e) { log.error(Redis判断key: {} 是否存在失败, key, e); return false; } } public static Boolean delete(String key) { try { return redisTemplateStatic.delete(key); } catch (Exception e) { log.error(Redis删除key: {} 失败, key, e); return false; } } public static Boolean expire(String key, long time, TimeUnit unit) { try { if (time 0) { return redisTemplateStatic.expire(key, time, unit); } return false; } catch (Exception e) { log.error(Redis设置key: {} 过期时间失败, key, e); return false; } } public static Long getExpire(String key, TimeUnit unit) { try { return redisTemplateStatic.getExpire(key, unit); } catch (Exception e) { log.error(Redis获取key: {} 过期时间失败, key, e); return null; } }注意事项expire命令对于不存在的key会返回false。在我们的封装中如果传入的time0也直接返回false因为这是一个无效操作。getExpire方法对于不存在的key或永不过期的key会返回-1或-2需要调用方注意处理这些特殊值。4.2 String类型操作封装这是使用最频繁的数据结构。public static Boolean set(String key, Object value) { try { redisTemplateStatic.opsForValue().set(key, value); return true; } catch (Exception e) { log.error(Redis设置key: {} 失败, key, e); return false; } } /** * 带过期时间的设置 * param time 时间如果time小于等于0则为无限期 */ public static Boolean set(String key, Object value, long time, TimeUnit unit) { try { if (time 0) { redisTemplateStatic.opsForValue().set(key, value, time, unit); } else { set(key, value); } return true; } catch (Exception e) { log.error(Redis设置key: {} (带过期时间)失败, key, e); return false; } } /** * 获取对象 * param key 键 * return 对象如果key不存在或反序列化失败返回null */ public static T T get(String key) { try { return (T) redisTemplateStatic.opsForValue().get(key); } catch (Exception e) { log.error(Redis获取key: {} 失败, key, e); return null; } } /** * 递增 * param delta 要增加几大于0 */ public static Long incr(String key, long delta) { if (delta 0) { throw new RuntimeException(递增因子必须大于0); } try { return redisTemplateStatic.opsForValue().increment(key, delta); } catch (Exception e) { log.error(Redis key: {} 递增失败, key, e); return null; } } /** * 递减 * param delta 要减少几大于0 */ public static Long decr(String key, long delta) { if (delta 0) { throw new RuntimeException(递减因子必须大于0); } try { return redisTemplateStatic.opsForValue().decrement(key, delta); } catch (Exception e) { log.error(Redis key: {} 递减失败, key, e); return null; } }实操心得set方法的重载设计很重要。将time和unit作为参数比单独提供一个setWithTimeout方法更符合使用习惯。在incr和decr方法中我们对delta参数做了校验这是业务逻辑的防护。虽然Redis命令本身支持负数但业务上“递增一个负数”通常意味着逻辑错误提前抛出异常有助于快速定位问题。4.3 Hash类型操作封装适合存储对象或需要频繁更新部分字段的场景。public static Boolean hSet(String key, String hashKey, Object value) { try { redisTemplateStatic.opsForHash().put(key, hashKey, value); return true; } catch (Exception e) { log.error(Redis Hash设置 key: {}, field: {} 失败, key, hashKey, e); return false; } } /** * 向一张hash表中放入数据如果不存在将创建 * param time 过期时间注意这里设置的是整个hash key的过期时间 */ public static Boolean hSet(String key, String hashKey, Object value, long time, TimeUnit unit) { try { redisTemplateStatic.opsForHash().put(key, hashKey, value); if (time 0) { expire(key, time, unit); // 设置整个hash的过期时间 } return true; } catch (Exception e) { log.error(Redis Hash设置 key: {}, field: {} (带过期时间)失败, key, hashKey, e); return false; } } public static T T hGet(String key, String hashKey) { try { return (T) redisTemplateStatic.opsForHash().get(key, hashKey); } catch (Exception e) { log.error(Redis Hash获取 key: {}, field: {} 失败, key, hashKey, e); return null; } } /** * 获取整个Hash */ public static HK, HV MapHK, HV hGetAll(String key) { try { return redisTemplateStatic.opsForHash().entries(key); } catch (Exception e) { log.error(Redis获取整个Hash key: {} 失败, key, e); return null; } } /** * 删除hash表中的字段 */ public static Long hDelete(String key, Object... hashKeys) { try { return redisTemplateStatic.opsForHash().delete(key, hashKeys); } catch (Exception e) { log.error(Redis Hash删除 key: {} 中的字段失败, key, e); return 0L; } }注意事项hSet带过期时间的方法其过期时间是作用于整个key即整个Hash结构而不是单个hashKey。Redis本身不支持为Hash中的单个field设置TTL。如果需要field级别的过期需要考虑其他数据结构如为每个field单独设置一个String类型的key。4.4 List、Set、ZSet操作封装示例这些结构的使用频率相对较低但封装原则一致。以List为例public static Boolean lPush(String key, Object value) { try { redisTemplateStatic.opsForList().leftPush(key, value); return true; } catch (Exception e) { log.error(Redis List左推入 key: {} 失败, key, e); return false; } } public static T T lPop(String key) { try { return (T) redisTemplateStatic.opsForList().leftPop(key); } catch (Exception e) { log.error(Redis List左弹出 key: {} 失败, key, e); return null; } } /** * 获取列表指定范围内的元素 * param start 开始位置0表示第一个 * param end 结束位置-1表示最后一个 */ public static T ListT lRange(String key, long start, long end) { try { return (ListT) redisTemplateStatic.opsForList().range(key, start, end); } catch (Exception e) { log.error(Redis获取List范围 key: {}, start:{}, end:{} 失败, key, start, end, e); return Collections.emptyList(); } }Set和ZSet的封装类似主要涉及opsForSet()和opsForZSet()的相关方法。关键在于方法命名要清晰比如sAdd、sMembers、zAdd、zRangeByScore等。4.5 高级功能分布式锁的简易实现分布式锁是Redis的经典应用场景。虽然生产环境建议使用Redisson等成熟客户端但理解其原理并实现一个基础版对学习很有帮助。/** * 尝试获取分布式锁简化版未解决锁续期等问题适用于短时任务 * param lockKey 锁的key * param requestId 请求标识可使用UUID用于保证解锁的是加锁的客户端 * param expireTime 锁的过期时间单位秒 * return 是否获取成功 */ public static Boolean tryLock(String lockKey, String requestId, long expireTime) { try { // 使用SET命令的NX不存在才设置和EX过期时间参数 Boolean result redisTemplateStatic.execute((RedisCallbackBoolean) connection - { RedisSerializerString serializer redisTemplateStatic.getStringSerializer(); byte[] key serializer.serialize(lockKey); byte[] value serializer.serialize(requestId); // 对应命令SET lockKey requestId NX EX expireTime return connection.stringCommands().set(key, value, Expiration.seconds(expireTime), SetOption.SET_IF_ABSENT); }); return Boolean.TRUE.equals(result); } catch (Exception e) { log.error(Redis尝试获取锁 key: {} 失败, lockKey, e); return false; } } /** * 释放分布式锁 * 使用Lua脚本保证原子性只有锁的value与传入的requestId相同时才删除 */ public static Boolean releaseLock(String lockKey, String requestId) { // Lua脚本 String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; DefaultRedisScriptLong redisScript new DefaultRedisScript(); redisScript.setScriptText(script); redisScript.setResultType(Long.class); try { Long result redisTemplateStatic.execute(redisScript, Collections.singletonList(lockKey), requestId); return result ! null result 1L; } catch (Exception e) { log.error(Redis释放锁 key: {} 失败, lockKey, e); return false; } }重要警告这是一个非常基础的锁实现不适用于严格的线上生产环境。它缺少了锁续期看门狗机制如果业务执行时间超过expireTime锁会自动释放可能导致多个客户端同时持有锁。生产环境请务必使用Redisson的RLock它解决了这些复杂问题。5. 常见问题与排查技巧实录在实际使用封装好的RedisUtils时你肯定会遇到各种“坑”。下面是我踩过的一些典型问题及解决方案。5.1 序列化导致的“灵异”事件问题描述使用RedisUtils.set(“user:1”, userObj)存储一个User对象后通过Redis Desktop Manager查看发现value是一串乱码或者奇怪的字符串而不是预期的JSON。排查与解决检查配置首先确认你的RedisConfig配置类是否生效RedisTemplate的valueSerializer是否设置为了GenericJackson2JsonRedisSerializer或Jackson2JsonRedisSerializer。检查Bean注入确认工具类中持有的redisTemplateStatic是否是你配置的那个Bean。可以在工具类加载时打印一下它的序列化器信息。最常见原因你可能在项目的其他地方比如另一个Configuration类又定义了一个RedisTemplateBeanSpring容器中存在多个同类型Bean导致注入的不是你期望的那个。使用Primary注解或在注入时使用Qualifier指定。技巧在RedisConfig中为你的RedisTemplateBean定义一个特定的名字如Bean(“myRedisTemplate”)然后在工具类中通过Qualifier(“myRedisTemplate”)来注入可以避免混淆。5.2 连接超时与池化配置不当问题描述服务运行一段时间后偶尔出现RedisCommandTimeoutException或获取连接超时。排查与解决检查网络与Redis状态使用redis-cli -h host -p port ping确认Redis服务本身是否正常。分析连接池配置回顾application.yml中的lettuce.pool配置。max-active是否设置过小在高并发场景下连接被快速耗尽新的请求就需要等待max-wait如果等待时间超过配置或命令执行超时timeout就会报错。检查连接泄漏是否在代码中手动获取了RedisConnection而没有关闭虽然RedisTemplate会管理连接但如果你直接使用RedisConnectionFactory获取连接务必在finally块中关闭。监控连接数使用redis-cli的info clients命令或通过监控工具查看Redis服务器的连接数。如果连接数持续增长不释放很可能存在泄漏。经验参数对于大多数Web应用max-active设置在20-50之间max-idle和max-active可以设为相同值min-idle可以设为max-active的一半以保持一定数量的热连接。5.3 泛型转换与ClassCastException问题描述调用RedisUtils.get(“key”)期望得到一个User对象却抛出了ClassCastException提示无法将LinkedHashMap转换为User。排查与解决原因分析这通常是因为存储时和读取时的类型信息不匹配。你可能用默认的JdkSerializationRedisSerializer或没有开启DefaultTyping的Jackson序列化器存储了一个User对象。当用配置了DefaultTyping的序列化器去读取时Jackson可能因为找不到class信息而将JSON反序列化为最通用的LinkedHashMap。解决方案确保整个应用使用统一配置的RedisTemplate。如果数据已经被错误序列化可能需要手动清理或编写迁移脚本。更安全的做法对于明确类型的读取可以使用带类型参数的方法public static T T get(String key, ClassT clazz) { try { Object value redisTemplateStatic.opsForValue().get(key); return objectMapper.convertValue(value, clazz); // 使用配置好的ObjectMapper进行安全转换 } catch (Exception e) { log.error(Redis获取key: {} 失败, key, e); return null; } } // 使用User user RedisUtils.get(“user:1”, User.class);5.4 事务与管道Pipeline的支持问题描述业务中需要一次性执行多个Redis命令希望保证原子性或提升性能但RedisUtils的单个方法封装不支持。解决方案工具类应该提供获取原生RedisTemplate的入口以便进行高级操作。/** * 获取底层的RedisTemplate用于执行更复杂的操作如事务、管道、执行原生命令 * 注意使用此方法需要调用者对Spring Data Redis有一定了解。 */ public static RedisTemplateString, Object getRedisTemplate() { return redisTemplateStatic; } // 使用事务示例 ListObject txResults RedisUtils.getRedisTemplate().execute(new SessionCallbackListObject() { Override public ListObject execute(RedisOperations operations) throws DataAccessException { operations.multi(); // 开启事务 operations.opsForValue().set(“key1”, “value1”); operations.opsForValue().increment(“key2”); return operations.exec(); // 执行事务 } }); // 使用管道示例 ListObject pipeResults RedisUtils.getRedisTemplate().executePipelined(new SessionCallbackObject() { Override public Object execute(RedisOperations operations) throws DataAccessException { for (int i 0; i 100; i) { operations.opsForValue().set(“pipe:” i, “value” i); } return null; } });提示事务multi/exec在Redis中并非像数据库那样保证强一致性它只是将命令打包顺序执行期间不会被其他客户端打断。管道pipelining主要用于提升批量操作的网络性能它不保证原子性。5.5 工具类的日志与监控一个健壮的工具类离不开良好的日志和监控。我们已经在每个方法中捕获了异常并打印了错误日志。但在生产环境这还不够。增强建议关键指标埋点使用Micrometer等指标库在set、get、delete等方法中记录调用次数、成功失败次数、耗时分布Histogram。这对于定位性能瓶颈和异常流量至关重要。慢查询日志在配置中开启Redis的慢查询日志slowlog-log-slower-than定期检查是否有命令执行超时。连接池监控通过LettucePoolingClientConfiguration可以获取连接池的监控数据如活跃连接数、空闲连接数、等待线程数等集成到你的应用监控大盘中。封装RedisUtils的旅程就像为自己打造一件称手的兵器。从最初的简单方法聚合到考虑序列化、异常、连接池、事务再到思考监控和扩展性每一步都是对Redis和Spring Boot生态理解加深的过程。这个工具类会随着项目一起成长逐渐沉淀为团队最核心的基础组件之一。最后记住没有银弹封装带来的便利性有时会牺牲一些灵活性了解其原理和边界才能让它发挥最大价值。

关于恒美微站

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

快速链接

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

服务项目

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

联系方式

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

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