{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "CodingMap",
  "home_page_url": "https://codingmap.strova.top/",
  "feed_url": "https://codingmap.strova.top/feed.json",
  "description": "CodingMap 是协奏曲维护的开发者知识库，覆盖前端、后端、Java、数据库、Redis、分布式、系统设计、计算机基础和 AI 应用开发等内容。",
  "favicon": "https://codingmap.strova.top/favicon.ico",
  "items": [
    {
      "title": "Java 知识体系",
      "url": "https://codingmap.strova.top/home.html",
      "id": "https://codingmap.strova.top/home.html",
      "summary": "CodingMap 首页聚合前端、后端、Java、计算机基础、数据库、Redis、分布式、系统设计、高性能、高可用和 AI 应用开发内容，帮助读者快速定位重点内容。",
      "content_html": "<!-- markdownlint-disable MD024 MD033 -->\n<p>CodingMap 是协奏曲维护的开发者知识库，本页整理系统化的前端、后端、Java、计算机基础、数据库、分布式、系统设计与 AI 应用开发资料。当前内容以 Java 与后端工程为主，后续会继续补充前端开发、项目复盘和原创学习笔记。</p>\n<p>如果你正在准备校招、社招或跳槽面试，可以按下面的模块逐步复习高频开发基础、工程实践和面试题。</p>\n<p>本站 UI、工程搭建和学习整理方式参考 <a href=\"https://javaguide.cn/\" target=\"_blank\">JavaGuide</a> 与 <a href=\"https://www.mianshiya.com/\" target=\"_blank\">面试鸭</a>；因个人学习、整理笔记与沉淀知识体系而开发(注意：本站的内容只适合有一定基础的同学查看，如您从未学过或看过，那么请你去JavaGuide或者面试鸭进行问题的详解)，本站内容由 CodingMap 自行编写与维护。</p>\n<ul>\n<li><strong>站点名称</strong>：CodingMap</li>\n<li><strong>维护者</strong>：协奏曲</li>\n<li><strong>参考说明</strong>：UI、工程搭建和学习整理方式参考 JavaGuide 与面试鸭；本站内容由 CodingMap 自行编写与维护。</li>\n</ul>\n<h2>Java</h2>\n<h3>基础</h3>\n<p><strong>知识点/面试题总结</strong> : (必看👍 )：</p>\n<ul>\n<li><a href=\"/java/basis/java-basic-questions-01.html\" target=\"_blank\">Java 基础常见知识点&amp;面试题总结(上)</a></li>\n<li><a href=\"/java/basis/java-basic-questions-02.html\" target=\"_blank\">Java 基础常见知识点&amp;面试题总结(中)</a></li>\n<li><a href=\"/java/basis/java-basic-questions-03.html\" target=\"_blank\">Java 基础常见知识点&amp;面试题总结(下)</a></li>\n</ul>\n<p><strong>重要知识点详解</strong>：</p>\n<ul>\n<li><a href=\"/java/basis/why-there-only-value-passing-in-java.html\" target=\"_blank\">为什么 Java 中只有值传递？</a></li>\n<li><a href=\"/java/basis/serialization.html\" target=\"_blank\">Java 序列化详解</a></li>\n<li><a href=\"/java/basis/generics-and-wildcards.html\" target=\"_blank\">泛型&amp;通配符详解</a></li>\n<li><a href=\"/java/basis/reflection.html\" target=\"_blank\">Java 反射机制详解</a></li>\n<li><a href=\"/java/basis/proxy.html\" target=\"_blank\">Java 代理模式详解</a></li>\n<li><a href=\"/java/basis/bigdecimal.html\" target=\"_blank\">BigDecimal 详解</a></li>\n<li><a href=\"/java/basis/unsafe.html\" target=\"_blank\">Java 魔法类 Unsafe 详解</a></li>\n<li><a href=\"/java/basis/spi.html\" target=\"_blank\">Java SPI 机制详解</a></li>\n<li><a href=\"/java/basis/syntactic-sugar.html\" target=\"_blank\">Java 语法糖详解</a></li>\n</ul>\n<h3>集合</h3>\n<p><strong>知识点/面试题总结</strong>：</p>\n<ul>\n<li><a href=\"/java/collection/java-collection-questions-01.html\" target=\"_blank\">Java 集合常见知识点&amp;面试题总结(上)</a> (必看 👍)</li>\n<li><a href=\"/java/collection/java-collection-questions-02.html\" target=\"_blank\">Java 集合常见知识点&amp;面试题总结(下)</a> (必看 👍)</li>\n<li><a href=\"/java/collection/java-collection-precautions-for-use.html\" target=\"_blank\">Java 集合使用注意事项总结</a></li>\n</ul>\n<p><strong>源码分析</strong>：</p>\n<ul>\n<li><a href=\"/java/collection/arraylist-source-code.html\" target=\"_blank\">ArrayList 核心源码+扩容机制分析</a></li>\n<li><a href=\"/java/collection/linkedlist-source-code.html\" target=\"_blank\">LinkedList 核心源码分析</a></li>\n<li><a href=\"/java/collection/hashmap-source-code.html\" target=\"_blank\">HashMap 核心源码+底层数据结构分析</a></li>\n<li><a href=\"/java/collection/concurrent-hash-map-source-code.html\" target=\"_blank\">ConcurrentHashMap 核心源码+底层数据结构分析</a></li>\n<li><a href=\"/java/collection/linkedhashmap-source-code.html\" target=\"_blank\">LinkedHashMap 核心源码分析</a></li>\n<li><a href=\"/java/collection/copyonwritearraylist-source-code.html\" target=\"_blank\">CopyOnWriteArrayList 核心源码分析</a></li>\n<li><a href=\"/java/collection/arrayblockingqueue-source-code.html\" target=\"_blank\">ArrayBlockingQueue 核心源码分析</a></li>\n<li><a href=\"/java/collection/priorityqueue-source-code.html\" target=\"_blank\">PriorityQueue 核心源码分析</a></li>\n<li><a href=\"/java/collection/priorityqueue-source-code.html\" target=\"_blank\">DelayQueue 核心源码分析</a></li>\n</ul>\n<h3>IO</h3>\n<ul>\n<li><a href=\"/java/io/io-basis.html\" target=\"_blank\">IO 基础知识总结</a></li>\n<li><a href=\"/java/io/io-design-patterns.html\" target=\"_blank\">IO 设计模式总结</a></li>\n<li><a href=\"/java/io/io-model.html\" target=\"_blank\">IO 模型详解</a></li>\n<li><a href=\"/java/io/nio-basis.html\" target=\"_blank\">NIO 核心知识总结</a></li>\n</ul>\n<h3>并发</h3>\n<p><strong>知识点/面试题总结</strong> : (必看 👍)</p>\n<ul>\n<li><a href=\"/java/concurrent/java-concurrent-questions-01.html\" target=\"_blank\">Java 并发常见知识点&amp;面试题总结（上）</a></li>\n<li><a href=\"/java/concurrent/java-concurrent-questions-02.html\" target=\"_blank\">Java 并发常见知识点&amp;面试题总结（中）</a></li>\n<li><a href=\"/java/concurrent/java-concurrent-questions-03.html\" target=\"_blank\">Java 并发常见知识点&amp;面试题总结（下）</a></li>\n</ul>\n<p><strong>重要知识点详解</strong>：</p>\n<ul>\n<li><a href=\"/java/concurrent/optimistic-lock-and-pessimistic-lock.html\" target=\"_blank\">乐观锁和悲观锁详解</a></li>\n<li><a href=\"/java/concurrent/cas.html\" target=\"_blank\">CAS 详解</a></li>\n<li><a href=\"/java/concurrent/jmm.html\" target=\"_blank\">JMM（Java 内存模型）详解</a></li>\n<li><strong>线程池</strong>：<a href=\"/java/concurrent/java-thread-pool-summary.html\" target=\"_blank\">Java 线程池详解</a>、<a href=\"/java/concurrent/java-thread-pool-best-practices.html\" target=\"_blank\">Java 线程池最佳实践</a></li>\n<li><a href=\"/java/concurrent/threadlocal.html\" target=\"_blank\">ThreadLocal 详解</a></li>\n<li><a href=\"/java/concurrent/java-concurrent-collections.html\" target=\"_blank\">Java 并发容器总结</a></li>\n<li><a href=\"/java/concurrent/atomic-classes.html\" target=\"_blank\">Atomic 原子类总结</a></li>\n<li><a href=\"/java/concurrent/aqs.html\" target=\"_blank\">AQS 详解</a></li>\n<li><a href=\"/java/concurrent/completablefuture-intro.html\" target=\"_blank\">CompletableFuture 详解</a></li>\n</ul>\n<h3>JVM (必看 👍)</h3>\n<p>JVM 这部分内容主要参考 <a href=\"https://docs.oracle.com/javase/specs/jvms/se8/html/index.html\" target=\"_blank\" rel=\"noopener noreferrer\">JVM 虚拟机规范-Java8</a> 和周志明老师的<a href=\"https://book.douban.com/subject/34907497/\" target=\"_blank\" rel=\"noopener noreferrer\">《深入理解 Java 虚拟机（第 3 版）》</a> （强烈建议阅读多遍！）。</p>\n<ul>\n<li><strong><a href=\"/java/jvm/memory-area.html\" target=\"_blank\">Java 内存区域</a></strong></li>\n<li><strong><a href=\"/java/jvm/jvm-garbage-collection.html\" target=\"_blank\">JVM 垃圾回收</a></strong></li>\n<li><a href=\"/java/jvm/class-file-structure.html\" target=\"_blank\">类文件结构</a></li>\n<li><strong><a href=\"/java/jvm/class-loading-process.html\" target=\"_blank\">类加载过程</a></strong></li>\n<li><a href=\"/java/jvm/classloader.html\" target=\"_blank\">类加载器</a></li>\n<li><a href=\"/java/jvm/jvm-parameters-intro.html\" target=\"_blank\">【待完成】最重要的 JVM 参数总结（翻译完善了一半）</a></li>\n<li><a href=\"/java/jvm/jvm-intro.html\" target=\"_blank\">【加餐】大白话带你认识 JVM</a></li>\n<li><a href=\"/java/jvm/jdk-monitoring-and-troubleshooting-tools.html\" target=\"_blank\">JDK 监控和故障处理工具</a></li>\n</ul>\n<h3>新特性</h3>\n<ul>\n<li><strong>Java 8</strong>：<a href=\"/java/new-features/java8-tutorial-translate.html\" target=\"_blank\">Java 8 新特性总结（翻译）</a>、<a href=\"/java/new-features/java8-common-new-features.html\" target=\"_blank\">Java8 常用新特性总结</a></li>\n<li><a href=\"/java/new-features/java9.html\" target=\"_blank\">Java 9 新特性概览</a></li>\n<li><a href=\"/java/new-features/java10.html\" target=\"_blank\">Java 10 新特性概览</a></li>\n<li><a href=\"/java/new-features/java11.html\" target=\"_blank\">Java 11 新特性概览</a></li>\n<li><a href=\"/java/new-features/java12-13.html\" target=\"_blank\">Java 12 &amp; 13 新特性概览</a></li>\n<li><a href=\"/java/new-features/java14-15.html\" target=\"_blank\">Java 14 &amp; 15 新特性概览</a></li>\n<li><a href=\"/java/new-features/java16.html\" target=\"_blank\">Java 16 新特性概览</a></li>\n<li><a href=\"/java/new-features/java17.html\" target=\"_blank\">Java 17 新特性概览</a></li>\n<li><a href=\"/java/new-features/java18.html\" target=\"_blank\">Java 18 新特性概览</a></li>\n<li><a href=\"/java/new-features/java19.html\" target=\"_blank\">Java 19 新特性概览</a></li>\n<li><a href=\"/java/new-features/java20.html\" target=\"_blank\">Java 20 新特性概览</a></li>\n<li><a href=\"/java/new-features/java21.html\" target=\"_blank\">Java 21 新特性概览</a></li>\n<li><a href=\"/java/new-features/java22-23.html\" target=\"_blank\">Java 22 &amp; 23 新特性概览</a></li>\n<li><a href=\"/java/new-features/java24.html\" target=\"_blank\">Java 24 新特性概览</a></li>\n<li><a href=\"/java/new-features/java25.html\" target=\"_blank\">Java 25 新特性概览</a></li>\n</ul>\n<h2>计算机基础</h2>\n<blockquote>\n<p>计算机基础（计算机网络、操作系统、数据结构与算法）已独立为单独模块，详见 <a href=\"/cs-basics/\" target=\"_blank\">计算机基础知识总结</a>。</p>\n</blockquote>\n<h2>数据库</h2>\n<h3>基础</h3>\n<ul>\n<li><a href=\"/database/basis.html\" target=\"_blank\">数据库基础知识总结</a></li>\n<li><a href=\"/database/nosql.html\" target=\"_blank\">NoSQL 基础知识总结</a></li>\n<li><a href=\"/database/character-set.html\" target=\"_blank\">字符集详解</a></li>\n<li>SQL :\n<ul>\n<li><a href=\"/database/sql/sql-syntax-summary.html\" target=\"_blank\">SQL 语法基础知识总结</a></li>\n<li><a href=\"/database/sql/sql-questions-01.html\" target=\"_blank\">SQL 常见面试题总结</a></li>\n</ul>\n</li>\n</ul>\n<h3>MySQL</h3>\n<p><strong>知识点/面试题总结：</strong></p>\n<ul>\n<li><strong><a href=\"/database/mysql/mysql-questions-01.html\" target=\"_blank\">MySQL 常见知识点&amp;面试题总结</a></strong> (必看 👍)</li>\n<li><a href=\"/database/mysql/mysql-high-performance-optimization-specification-recommendations.html\" target=\"_blank\">MySQL 高性能优化规范建议总结</a></li>\n</ul>\n<p><strong>重要知识点：</strong></p>\n<ul>\n<li><a href=\"/database/mysql/mysql-index.html\" target=\"_blank\">MySQL 索引详解</a></li>\n<li><a href=\"/database/mysql/mysql-index-invalidation.html\" target=\"_blank\">MySQL 索引失效场景总结</a></li>\n<li><a href=\"/database/mysql/transaction-isolation-level.html\" target=\"_blank\">MySQL 事务隔离级别图文详解)</a></li>\n<li><a href=\"/database/mysql/mysql-logs.html\" target=\"_blank\">MySQL 三大日志(binlog、redo log 和 undo log)详解</a></li>\n<li><a href=\"/database/mysql/innodb-implementation-of-mvcc.html\" target=\"_blank\">InnoDB 存储引擎对 MVCC 的实现</a></li>\n<li><a href=\"/database/mysql/how-sql-executed-in-mysql.html\" target=\"_blank\">SQL 语句在 MySQL 中的执行过程</a></li>\n<li><a href=\"/database/mysql/mysql-query-cache.html\" target=\"_blank\">MySQL 查询缓存详解</a></li>\n<li><a href=\"/database/mysql/mysql-query-execution-plan.html\" target=\"_blank\">MySQL 执行计划分析</a></li>\n<li><a href=\"/database/mysql/mysql-auto-increment-primary-key-continuous.html\" target=\"_blank\">MySQL 自增主键一定是连续的吗</a></li>\n<li><a href=\"/database/mysql/some-thoughts-on-database-storage-time.html\" target=\"_blank\">MySQL 时间类型数据存储建议</a></li>\n<li><a href=\"/database/mysql/index-invalidation-caused-by-implicit-conversion.html\" target=\"_blank\">MySQL 隐式转换造成索引失效</a></li>\n</ul>\n<h3>Redis</h3>\n<p><strong>知识点/面试题总结</strong> : (必看👍 )：</p>\n<ul>\n<li><a href=\"/database/redis/redis-questions-01.html\" target=\"_blank\">Redis 常见知识点&amp;面试题总结(上)</a></li>\n<li><a href=\"/database/redis/redis-questions-02.html\" target=\"_blank\">Redis 常见知识点&amp;面试题总结(下)</a></li>\n</ul>\n<p><strong>重要知识点：</strong></p>\n<ul>\n<li><a href=\"/database/redis/3-commonly-used-cache-read-and-write-strategies.html\" target=\"_blank\">3 种常用的缓存读写策略详解</a></li>\n<li><a href=\"/database/redis/redis-stream-mq.html\" target=\"_blank\">Redis 能做消息队列吗？怎么实现？</a></li>\n<li><a href=\"/database/redis/redis-data-structures-01.html\" target=\"_blank\">Redis 5 种基本数据结构详解</a></li>\n<li><a href=\"/database/redis/redis-data-structures-02.html\" target=\"_blank\">Redis 3 种特殊数据结构详解</a></li>\n<li><a href=\"/database/redis/redis-persistence.html\" target=\"_blank\">Redis 持久化机制详解</a></li>\n<li><a href=\"/database/redis/redis-memory-fragmentation.html\" target=\"_blank\">Redis 内存碎片详解</a></li>\n<li><a href=\"/database/redis/redis-common-blocking-problems-summary.html\" target=\"_blank\">Redis 常见阻塞原因总结</a></li>\n<li><a href=\"/database/redis/redis-cluster.html\" target=\"_blank\">Redis 集群详解</a></li>\n</ul>\n<h3>MongoDB</h3>\n<ul>\n<li><a href=\"/database/mongodb/mongodb-questions-01.html\" target=\"_blank\">MongoDB 常见知识点&amp;面试题总结(上)</a></li>\n<li><a href=\"/database/mongodb/mongodb-questions-02.html\" target=\"_blank\">MongoDB 常见知识点&amp;面试题总结(下)</a></li>\n</ul>\n<h2>搜索引擎</h2>\n<p><a href=\"/database/elasticsearch/elasticsearch-questions-01.html\" target=\"_blank\">Elasticsearch 常见面试题总结(付费)</a></p>\n<h2>开发工具</h2>\n<h3>Maven</h3>\n<ul>\n<li><a href=\"/tools/maven/maven-core-concepts.html\" target=\"_blank\">Maven 核心概念总结</a></li>\n<li><a href=\"/tools/maven/maven-best-practices.html\" target=\"_blank\">Maven 最佳实践</a></li>\n</ul>\n<h3>Gradle</h3>\n<p><a href=\"/tools/gradle/gradle-core-concepts.html\" target=\"_blank\">Gradle 核心概念总结</a>（可选，目前国内还是使用 Maven 普遍一些）</p>\n<h3>Docker</h3>\n<ul>\n<li><a href=\"/tools/docker/docker-intro.html\" target=\"_blank\">Docker 核心概念总结</a></li>\n<li><a href=\"/tools/docker/docker-in-action.html\" target=\"_blank\">Docker 实战</a></li>\n</ul>\n<h3>Git</h3>\n<ul>\n<li><a href=\"/tools/git/git-intro.html\" target=\"_blank\">Git 核心概念总结</a></li>\n<li><a href=\"/tools/git/github-tips.html\" target=\"_blank\">GitHub 实用小技巧总结</a></li>\n</ul>\n<h2>系统设计</h2>\n<ul>\n<li><a href=\"/system-design/system-design-questions.html\" target=\"_blank\">⭐系统设计常见面试题总结</a></li>\n<li><a href=\"/system-design/design-pattern.html\" target=\"_blank\">⭐设计模式常见面试题总结</a></li>\n</ul>\n<h3>基础</h3>\n<ul>\n<li><a href=\"/system-design/basis/RESTfulAPI.html\" target=\"_blank\">RestFul API 简明教程</a></li>\n<li><a href=\"/system-design/basis/software-engineering.html\" target=\"_blank\">软件工程简明教程</a></li>\n<li><a href=\"/system-design/basis/naming.html\" target=\"_blank\">代码命名指南</a></li>\n<li><a href=\"/system-design/basis/refactoring.html\" target=\"_blank\">代码重构指南</a></li>\n<li><a href=\"/system-design/basis/unit-test.html\" target=\"_blank\">单元测试指南</a></li>\n</ul>\n<h3>常用框架</h3>\n<h4>Spring/SpringBoot (必看 👍)</h4>\n<p><strong>知识点/面试题总结</strong> :</p>\n<ul>\n<li><a href=\"/system-design/framework/spring/spring-knowledge-and-questions-summary.html\" target=\"_blank\">Spring 常见知识点&amp;面试题总结</a></li>\n<li><a href=\"/system-design/framework/spring/springboot-knowledge-and-questions-summary.html\" target=\"_blank\">SpringBoot 常见知识点&amp;面试题总结</a></li>\n<li><a href=\"/system-design/framework/spring/spring-common-annotations.html\" target=\"_blank\">Spring/Spring Boot 常用注解总结</a></li>\n<li><a href=\"https://github.com/Snailclimb/springboot-guide\" target=\"_blank\" rel=\"noopener noreferrer\">SpringBoot 入门指南</a></li>\n</ul>\n<p><strong>重要知识点详解</strong>：</p>\n<ul>\n<li><a href=\"/system-design/framework/spring/ioc-and-aop.html\" target=\"_blank\">IoC &amp; AOP 详解（快速搞懂）</a></li>\n<li><a href=\"/system-design/framework/spring/spring-transaction.html\" target=\"_blank\">Spring 事务详解</a></li>\n<li><a href=\"/system-design/framework/spring/spring-design-patterns-summary.html\" target=\"_blank\">Spring 中的设计模式详解</a></li>\n<li><a href=\"/system-design/framework/spring/spring-boot-auto-assembly-principles.html\" target=\"_blank\">SpringBoot 自动装配原理详解</a></li>\n</ul>\n<h4>MyBatis</h4>\n<p><a href=\"/system-design/framework/mybatis/mybatis-interview.html\" target=\"_blank\">MyBatis 常见面试题总结</a></p>\n<h3>安全</h3>\n<h4>认证授权</h4>\n<ul>\n<li><a href=\"/system-design/security/basis-of-authority-certification.html\" target=\"_blank\">认证授权基础概念详解</a></li>\n<li><a href=\"/system-design/security/jwt-intro.html\" target=\"_blank\">JWT 基础概念详解</a></li>\n<li><a href=\"/system-design/security/advantages-and-disadvantages-of-jwt.html\" target=\"_blank\">JWT 优缺点分析以及常见问题解决方案</a></li>\n<li><a href=\"/system-design/security/sso-intro.html\" target=\"_blank\">SSO 单点登录详解</a></li>\n<li><a href=\"/system-design/security/design-of-authority-system.html\" target=\"_blank\">权限系统设计详解</a></li>\n</ul>\n<h4>数据安全</h4>\n<ul>\n<li><a href=\"/system-design/security/encryption-algorithms.html\" target=\"_blank\">常见加密算法总结</a></li>\n<li><a href=\"/system-design/security/sentive-words-filter.html\" target=\"_blank\">敏感词过滤方案总结</a></li>\n<li><a href=\"/system-design/security/data-desensitization.html\" target=\"_blank\">数据脱敏方案总结</a></li>\n<li><a href=\"/system-design/security/data-validation.html\" target=\"_blank\">为什么前后端都要做数据校验</a></li>\n<li><a href=\"/system-design/security/why-password-reset-instead-of-retrieval.html\" target=\"_blank\">为什么忘记密码时只能重置，不能告诉你原密码？</a></li>\n</ul>\n<h3>定时任务</h3>\n<p><a href=\"/system-design/schedule-task.html\" target=\"_blank\">Java 定时任务详解</a></p>\n<h3>Web 实时消息推送</h3>\n<p><a href=\"/system-design/web-real-time-message-push.html\" target=\"_blank\">Web 实时消息推送详解</a></p>\n<h2>分布式</h2>\n<ul>\n<li><a href=\"/distributed-system/distributed-system-interview-questions.html\" target=\"_blank\">⭐分布式高频面试题</a></li>\n</ul>\n<h3>理论&amp;算法&amp;协议</h3>\n<ul>\n<li><a href=\"/distributed-system/protocol/cap-and-base-theorem.html\" target=\"_blank\">CAP 理论和 BASE 理论解读</a></li>\n<li><a href=\"/distributed-system/protocol/paxos-algorithm.html\" target=\"_blank\">Paxos 算法解读</a></li>\n<li><a href=\"/distributed-system/protocol/raft-algorithm.html\" target=\"_blank\">Raft 算法解读</a></li>\n<li><a href=\"/distributed-system/protocol/zab.html\" target=\"_blank\">ZAB 协议解读</a></li>\n<li><a href=\"/distributed-system/protocol/gossip-protocol.html\" target=\"_blank\">Gossip 协议详解</a></li>\n<li><a href=\"/distributed-system/protocol/consistent-hashing.html\" target=\"_blank\">一致性哈希算法详解</a></li>\n</ul>\n<h3>RPC</h3>\n<ul>\n<li><a href=\"/distributed-system/rpc/rpc-intro.html\" target=\"_blank\">RPC 基础知识总结</a></li>\n<li><a href=\"/distributed-system/rpc/dubbo.html\" target=\"_blank\">Dubbo 常见知识点&amp;面试题总结</a></li>\n</ul>\n<h3>ZooKeeper</h3>\n<blockquote>\n<p>这两篇文章可能有内容重合部分，推荐都看一遍。</p>\n</blockquote>\n<ul>\n<li><a href=\"/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.html\" target=\"_blank\">ZooKeeper 相关概念总结(入门)</a></li>\n<li><a href=\"/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.html\" target=\"_blank\">ZooKeeper 相关概念总结(进阶)</a></li>\n</ul>\n<h3>API 网关</h3>\n<ul>\n<li><a href=\"/distributed-system/api-gateway.html\" target=\"_blank\">API 网关基础知识总结</a></li>\n<li><a href=\"/distributed-system/spring-cloud-gateway-questions.html\" target=\"_blank\">Spring Cloud Gateway 常见知识点&amp;面试题总结</a></li>\n</ul>\n<h3>分布式 ID</h3>\n<ul>\n<li><a href=\"/distributed-system/distributed-id.html\" target=\"_blank\">分布式 ID 常见知识点&amp;面试题总结</a></li>\n<li><a href=\"/distributed-system/distributed-id-design.html\" target=\"_blank\">分布式 ID 设计指南</a></li>\n</ul>\n<h3>分布式锁</h3>\n<ul>\n<li><a href=\"/distributed-system/distributed-lock.html\" target=\"_blank\">分布式锁介绍</a></li>\n<li><a href=\"/distributed-system/distributed-lock-implementations.html\" target=\"_blank\">分布式锁常见实现方案总结</a></li>\n</ul>\n<h3>分布式事务</h3>\n<p><a href=\"/distributed-system/distributed-transaction.html\" target=\"_blank\">分布式事务常见知识点&amp;面试题总结</a></p>\n<h3>分布式配置中心</h3>\n<p><a href=\"/distributed-system/distributed-configuration-center.html\" target=\"_blank\">分布式配置中心常见知识点&amp;面试题总结</a></p>\n<h2>高性能</h2>\n<h3>数据库优化</h3>\n<ul>\n<li><a href=\"/high-performance/read-and-write-separation-and-library-subtable.html\" target=\"_blank\">数据库读写分离和分库分表</a></li>\n<li><a href=\"/high-performance/data-cold-hot-separation.html\" target=\"_blank\">数据冷热分离</a></li>\n<li><a href=\"/high-performance/sql-optimization.html\" target=\"_blank\">常见 SQL 优化手段总结</a></li>\n<li><a href=\"/high-performance/deep-pagination-optimization.html\" target=\"_blank\">深度分页介绍及优化建议</a></li>\n</ul>\n<h3>负载均衡</h3>\n<p><a href=\"/high-performance/load-balancing.html\" target=\"_blank\">负载均衡常见知识点&amp;面试题总结</a></p>\n<h3>CDN</h3>\n<p><a href=\"/high-performance/cdn.html\" target=\"_blank\">CDN（内容分发网络）常见知识点&amp;面试题总结</a></p>\n<h3>消息队列</h3>\n<ul>\n<li><a href=\"/high-performance/message-queue/message-queue.html\" target=\"_blank\">消息队列基础知识总结</a></li>\n<li><a href=\"/high-performance/message-queue/disruptor-questions.html\" target=\"_blank\">Disruptor 常见知识点&amp;面试题总结</a></li>\n<li><a href=\"/high-performance/message-queue/rabbitmq-questions.html\" target=\"_blank\">RabbitMQ 常见知识点&amp;面试题总结</a></li>\n<li><a href=\"/high-performance/message-queue/rocketmq-questions.html\" target=\"_blank\">RocketMQ 常见知识点&amp;面试题总结</a></li>\n<li><a href=\"/high-performance/message-queue/kafka-questions-01.html\" target=\"_blank\">Kafka 常见知识点&amp;面试题总结</a></li>\n</ul>\n<h2>高可用</h2>\n<p><a href=\"/high-availability/high-availability-system-design.html\" target=\"_blank\">高可用系统设计指南</a></p>\n<h3>冗余设计</h3>\n<p><a href=\"/high-availability/redundancy.html\" target=\"_blank\">冗余设计详解</a></p>\n<h3>限流</h3>\n<p><a href=\"/high-availability/limit-request.html\" target=\"_blank\">服务限流详解</a></p>\n<h3>降级&amp;熔断</h3>\n<p><a href=\"/high-availability/fallback-and-circuit-breaker.html\" target=\"_blank\">降级&amp;熔断详解</a></p>\n<h3>超时&amp;重试</h3>\n<p><a href=\"/high-availability/timeout-and-retry.html\" target=\"_blank\">超时&amp;重试详解</a></p>\n<h3>集群</h3>\n<p>相同的服务部署多份，避免单点故障。</p>\n<h3>灾备设计和异地多活</h3>\n<p><strong>灾备</strong> = 容灾 + 备份。</p>\n<ul>\n<li><strong>备份</strong>：将系统所产生的所有重要数据多备份几份。</li>\n<li><strong>容灾</strong>：在异地建立两个完全相同的系统。当某个地方的系统突然挂掉，整个应用系统可以切换到另一个，这样系统就可以正常提供服务了。</li>\n</ul>\n<p><strong>异地多活</strong> 描述的是将服务部署在异地并且服务同时对外提供服务。和传统的灾备设计的最主要区别在于“多活”，即所有站点都是同时在对外提供服务的。异地多活是为了应对突发状况比如火灾、地震等自然或者人为灾害。</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": []
    },
    {
      "title": "AI 应用开发知识体系：大模型、Agent、RAG、MCP、Prompt 工程与系统设计",
      "url": "https://codingmap.strova.top/ai/",
      "id": "https://codingmap.strova.top/ai/",
      "summary": "AI 应用开发面试与学习路线，面向后端开发者梳理大模型调用、Agent、RAG、MCP、Prompt 工程、向量数据库、评测和系统设计。重点围绕 适合谁看、几个容易踩坑的地方、建议阅读顺序、核心文章、AI 等内容展开。结合 CodingMap 知识体系梳理 AI 的核心概念、实践方法、常见问题和高频面试考点等核心内容。",
      "content_html": "<p>做 AI 应用不是把 Prompt 塞进接口就结束了。真到项目里，马上会遇到上下文长度、结构化输出、RAG 召回、工具权限、评测回归、成本和稳定性这些问题。</p>\n<p>这些问题没法各解各的。大模型基础、Agent、RAG、工具调用、系统设计必须连起来理解——只懂调用 API，到了架构评审会卡住；只熟 RAG 论文，到了知识库维护还是不知道怎么处理增量更新和版本去重。</p>\n<p>如果时间有限，先看 <a href=\"/ai/interview-questions/ai-interview-guide.html\" target=\"_blank\">AI 应用开发面试指南</a>，把大模型、Agent、RAG、MCP 和 AI 系统设计里最容易被追问的问题过一遍；如果想补得扎实一些，再按下面的阅读顺序推进。</p>\n<p>这应该是当前最全面系统的讲解，每一篇都花费了大量时间完善和优化，每篇文章都画了大量配图辅助理解：</p>\n<p><img src=\"https://oss.javaguide.cn/github/aiguide/aiguide-overview.png\" alt=\"AIGuide 内容概览，大量配图\"></p>\n<p>本专栏所属 AIGuide 项目，对标 JavaGuide 质量（免费开源，欢迎 Star 鼓励）：</p>\n<ul>\n<li><strong>项目地址</strong>：<a href=\"https://github.com/Snailclimb/AIGuide\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Snailclimb/AIGuide</a></li>\n<li><strong>在线阅读</strong>：<a href=\"https://javaguide.cn/ai-coding/\" target=\"_blank\" rel=\"noopener noreferrer\">https://javaguide.cn/ai-coding/</a></li>\n</ul>\n<p>发布之后，也是收到了很多读者朋友的好评和推荐。非常感谢，一定会持续用心维护！</p>\n<p><img src=\"https://oss.javaguide.cn/github/aiguide/ai-guide-received-many-positive-reviews-and-recommendations-from-readers.png\" alt=\"AIGuide 收到了很多读者朋友的好评和推荐\"></p>\n<h2>适合谁看</h2>\n<ul>\n<li>正在从后端开发转向 AI 应用开发，想补齐大模型、Agent、RAG 和系统设计主线的工程师。</li>\n<li>准备 AI 工程师、AI 应用开发、后端转 AI 相关岗位面试的同学。</li>\n<li>做过 Prompt Demo，但对模型调用链路、结构化输出、RAG 检索优化和评测闭环还不够熟的开发者。</li>\n<li>想把 MCP、Function Calling、Tool Calling、向量数据库、模型网关这些概念放到真实项目里理解的读者。</li>\n<li>已经在项目中接入大模型，但开始遇到稳定性、成本、安全治理和质量回归问题的团队成员。</li>\n</ul>\n<h2>几个容易踩坑的地方</h2>\n<p>大模型真不能只当成一个黑盒 API 来调。Token 被截断、采样参数一变输出就飘、说好返回 JSON 结果还是乱了，这些问题靠 Prompt 很难彻底兜住。你在提示词里加一句“请严格按照 JSON 输出”，只能算第一层约束，真正上线时还是得在调用链路里做格式校验、重试、兜底和异常处理。</p>\n<p>Agent 也不是能自动调工具就完事了。真正难的是 Memory 和 Context Engineering。上下文没管好，Agent 跑几轮之后就容易偏题，前面说过什么、当前任务做到哪一步、哪些工具结果还能用，全都可能乱掉。长任务里更明显，有时候它不是不会做，而是循环几次之后自己把自己绕进去了，一直跑到 token 快耗完才停。</p>\n<p>RAG 答非所问，很多时候也别急着怪模型。大部分问题其实出在召回阶段：Chunk 切得太粗、Query 没改写、关键词检索和向量检索没结合、重排没做好。这个时候一项一项排查召回链路，往往比直接换一个更贵的模型有用。</p>\n<p>MCP、Function Calling、Tool Calling 这些东西，解决的是工具怎么接进来的问题。协议统一之后，接工具确实方便了，但真到生产环境，麻烦的地方反而在后面：谁能调用这个工具、能操作哪些数据、调用记录怎么审计、失败了怎么回滚。这些如果没设计好，协议再标准也不够用。</p>\n<p>AI 应用一旦上线，稳定性、可观测、成本控制、质量回归这些问题都会冒出来。Demo 阶段通常感受不到，因为调用量小、场景也干净。等真正接到业务流量里，第一次做生产级 AI 应用的团队，基本都会被这些问题教育一次。</p>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/ai/interview-questions/ai-interview-guide.html\" target=\"_blank\">AI 应用开发面试指南</a>：先建立高频问题清单，知道面试和项目复盘最常被追问哪些点。</li>\n<li><a href=\"/ai/llm-basis/llm-operation-mechanism.html\" target=\"_blank\">万字拆解 LLM 运行机制</a>、<a href=\"/ai/llm-basis/llm-api-engineering.html\" target=\"_blank\">大模型 API 调用工程实践</a>：理解模型调用链路、上下文和结构化返回。</li>\n<li><a href=\"/ai/agent/agent-basis.html\" target=\"_blank\">一文搞懂 AI Agent 核心概念</a>、<a href=\"/ai/agent/prompt-engineering.html\" target=\"_blank\">大模型提示词工程实践指南</a>、<a href=\"/ai/agent/context-engineering.html\" target=\"_blank\">上下文工程实战指南</a>：建立 Agent 和 Prompt/Context 的基础认知。</li>\n<li><a href=\"/ai/rag/rag-basis.html\" target=\"_blank\">万字详解 RAG 基础概念</a>、<a href=\"/ai/rag/rag-document-processing.html\" target=\"_blank\">RAG 文档处理与切分策略</a>、<a href=\"/ai/rag/rag-optimization.html\" target=\"_blank\">万字详解 RAG 检索优化</a>：补齐企业知识库问答主线。</li>\n<li><a href=\"/ai/system-design/ai-application-architecture.html\" target=\"_blank\">AI 应用系统设计</a>、<a href=\"/ai/system-design/llm-gateway.html\" target=\"_blank\">大模型网关详解</a>、<a href=\"/ai/llm-basis/llm-evaluation.html\" target=\"_blank\">AI 应用评测体系</a>：把 Demo 放进真实后端系统里，补齐网关、评测和治理。</li>\n</ol>\n<h2>核心文章</h2>\n<h3>面试与复习路线</h3>\n<ul>\n<li><a href=\"/ai/interview-questions/\" target=\"_blank\">AI 应用开发面试题专题</a>：按大模型基础、AI Agent、RAG 和 AI 系统设计组织复习路线。</li>\n<li><a href=\"/ai/interview-questions/ai-interview-guide.html\" target=\"_blank\">AI 应用开发面试指南</a>：把 AI 应用开发常见追问放到一条复习路线里，适合先看。</li>\n<li><a href=\"/ai/interview-questions/llm-interview-questions.html\" target=\"_blank\">大模型基础面试题总结</a>：覆盖 Token、上下文窗口、采样参数、API 调用、结构化输出和评测体系。</li>\n<li><a href=\"/ai/interview-questions/agent-interview-questions.html\" target=\"_blank\">AI Agent 面试题总结</a>：覆盖 Agent Loop、Memory、Prompt、Context、MCP、Skills、Harness Engineering 和工作流。</li>\n<li><a href=\"/ai/interview-questions/rag-interview-questions.html\" target=\"_blank\">RAG 面试题总结</a>：覆盖 RAG 基础、向量数据库、文档处理、检索优化、GraphRAG、知识库更新和评测。</li>\n<li><a href=\"/ai/interview-questions/ai-system-design-interview-questions.html\" target=\"_blank\">AI 系统设计面试题总结</a>：覆盖生产级 AI 应用架构、模型网关、可观测、评测、安全治理和实时语音 Agent。</li>\n</ul>\n<h3>大模型基础</h3>\n<ul>\n<li><a href=\"/ai/llm-basis/\" target=\"_blank\">大模型基础专题</a>：从模型运行机制、API 调用、结构化输出到 AI 应用评测，先把调用链路看明白。</li>\n<li><a href=\"/ai/llm-basis/llm-operation-mechanism.html\" target=\"_blank\">万字拆解 LLM 运行机制</a>：把 Token、上下文窗口、Temperature 等概念还原为清晰、可控的工程参数。</li>\n<li><a href=\"/ai/llm-basis/llm-api-engineering.html\" target=\"_blank\">大模型 API 调用工程实践</a>：拆解 Prompt 组装、模型网关、流式响应、重试限流和结构化返回。</li>\n<li><a href=\"/ai/llm-basis/structured-output-function-calling.html\" target=\"_blank\">大模型结构化输出详解</a>：讲清 JSON Schema、Function Calling、Tool Calling 与 MCP 的底层链路。</li>\n<li><a href=\"/ai/llm-basis/llm-evaluation.html\" target=\"_blank\">AI 应用评测体系</a>：覆盖 Golden Set、LLM-as-Judge、RAG/Agent 指标、Trace 回放和线上灰度闭环。</li>\n</ul>\n<h3>AI Agent</h3>\n<ul>\n<li><a href=\"/ai/agent/\" target=\"_blank\">AI Agent 专题</a>：从 Agent 基础概念、Memory、Prompt、Context 到 MCP、Skills 和 Harness Engineering。</li>\n<li><a href=\"/ai/agent/agent-basis.html\" target=\"_blank\">一文搞懂 AI Agent 核心概念</a>：理解 Agent 和传统编程、Workflow 的区别，以及 Agent Loop、Tools 注册等核心概念。</li>\n<li><a href=\"/ai/agent/agent-memory.html\" target=\"_blank\">AI Agent 记忆系统</a>：深入理解短期记忆、长期记忆、记忆生命周期和生产级优化策略。</li>\n<li><a href=\"/ai/agent/prompt-engineering.html\" target=\"_blank\">大模型提示词工程实践指南</a>：掌握 Prompt 四要素、常见技巧和 Prompt 注入防护。</li>\n<li><a href=\"/ai/agent/context-engineering.html\" target=\"_blank\">上下文工程实战指南</a>：理解静态规则编排、动态信息挂载、Token 预算降级和上下文持久化。</li>\n<li><a href=\"/ai/agent/mcp.html\" target=\"_blank\">万字拆解 MCP 协议</a>：理解 MCP 的分层架构、核心能力和 MCP Server 生产实践。</li>\n<li><a href=\"/ai/agent/skills.html\" target=\"_blank\">万字详解 Agent Skills</a>：理解 Skills 与 Prompt、MCP、Function Calling 的本质区别。</li>\n<li><a href=\"/ai/agent/harness-engineering.html\" target=\"_blank\">一文搞懂 Harness Engineering</a>：拆解 Model + Harness 的工程化架构和一线团队实践。</li>\n<li><a href=\"/ai/agent/workflow-graph-loop.html\" target=\"_blank\">AI 工作流中的 Workflow、Graph 与 Loop</a>：理解 AI 工作流的节点、边、状态、安全边界和实现方式。</li>\n</ul>\n<h3>RAG 检索增强生成</h3>\n<ul>\n<li><a href=\"/ai/rag/\" target=\"_blank\">RAG 专题</a>：围绕企业知识库问答，梳理文档处理、向量数据库、GraphRAG、检索优化和知识库更新。</li>\n<li><a href=\"/ai/rag/rag-basis.html\" target=\"_blank\">万字详解 RAG 基础概念</a>：理解 RAG 是什么、为什么需要它、核心优势和局限性。</li>\n<li><a href=\"/ai/rag/rag-document-processing.html\" target=\"_blank\">RAG 文档处理与切分策略</a>：覆盖文档解析、清洗、结构化、Chunking 和多模态内容处理。</li>\n<li><a href=\"/ai/rag/rag-vector-store.html\" target=\"_blank\">万字详解 RAG 向量索引算法和向量数据库</a>：掌握 HNSW、IVFFLAT 等索引算法和向量数据库选型。</li>\n<li><a href=\"/ai/rag/rag-optimization.html\" target=\"_blank\">万字详解 RAG 检索优化</a>：覆盖 Chunk 策略、Hybrid Search、Query Rewrite、Rerank 和上下文压缩。</li>\n<li><a href=\"/ai/rag/graphrag.html\" target=\"_blank\">万字详解 GraphRAG</a>：理解实体、关系、社区发现、全局检索与局部检索。</li>\n<li><a href=\"/ai/rag/rag-knowledge-update.html\" target=\"_blank\">RAG 知识库文档更新策略</a>：掌握增量更新、版本控制、去重和全量重建。</li>\n</ul>\n<h3>AI 系统设计</h3>\n<ul>\n<li><a href=\"/ai/system-design/\" target=\"_blank\">AI 系统设计专题</a>：把 Prompt Demo 放进真实后端系统里看，重点关注架构、模型网关、语音链路、可观测、评测和安全治理。</li>\n<li><a href=\"/ai/system-design/ai-application-architecture.html\" target=\"_blank\">AI 应用系统设计</a>：把 Prompt Demo 放进生产链路，覆盖 Prompt 管理、模型网关、RAG、Memory、Tool 调用、可观测、评测和安全合规。</li>\n<li><a href=\"/ai/system-design/llm-gateway.html\" target=\"_blank\">大模型网关详解</a>：理解 LLM Gateway 的多模型路由、fallback、限流配额、成本归因、观测审计和缓存策略。</li>\n<li><a href=\"/ai/system-design/ai-voice.html\" target=\"_blank\">AI 语音技术详解</a>：拆解 VAD、ASR、LLM、TTS、流式播放、打断处理和端云混合选型。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>大模型的 Token、上下文窗口、Temperature、Top P 分别会影响什么？</li>\n<li>为什么结构化输出不能只依赖 Prompt？JSON Schema、Function Calling 和服务端校验分别解决什么问题？</li>\n<li>Agent 和 Workflow 有什么区别？Agent Loop 中观察、规划、行动、反思如何协作？</li>\n<li>Prompt Engineering 和 Context Engineering 有什么区别？</li>\n<li>MCP 解决了什么问题？它和 Function Calling、Tool Calling 是什么关系？</li>\n<li>RAG 为什么会答非所问？应该从召回、排序、上下文压缩还是生成阶段排查？</li>\n<li>向量数据库如何选型？HNSW、IVFFLAT 这些索引适合什么场景？</li>\n<li>AI 应用怎么评测？Golden Set、LLM-as-Judge、线上灰度和 Trace 回放如何串起来？</li>\n<li>生产级 AI 应用为什么需要模型网关？如何做限流、fallback、成本控制和审计？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/ai-coding/\" target=\"_blank\">AI 编程实战指南</a></li>\n<li><a href=\"/system-design/\" target=\"_blank\">系统设计</a></li>\n<li><a href=\"/high-availability/\" target=\"_blank\">高可用系统知识体系</a></li>\n<li><a href=\"/high-performance/\" target=\"_blank\">高性能系统知识体系</a></li>\n<li><a href=\"/distributed-system/\" target=\"_blank\">分布式系统知识体系</a></li>\n</ul>\n",
      "image": "https://oss.javaguide.cn/github/aiguide/aiguide-overview.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI"
      ]
    },
    {
      "title": "技术书籍精选：Java、数据库、分布式系统、计算机基础与软件质量",
      "url": "https://codingmap.strova.top/books/",
      "id": "https://codingmap.strova.top/books/",
      "summary": "后端技术书籍推荐与学习路线，精选 Java、数据库、分布式系统、计算机基础、搜索引擎和软件质量等方向，适合面试复习和工程能力提升。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章 等内容展开。结合 CodingMap 知识体系梳理 技术书籍 的核心概念、实践方法、常见问题和高频面试考点，覆盖原理分析、使用场景等核。",
      "content_html": "<p>这份 <strong>技术书籍精选</strong> 面向程序员系统学习和长期成长，整理 Java、数据库、分布式系统、计算机基础、搜索引擎、软件质量等方向的优质书单。</p>\n<p>书单来自开源项目 <a href=\"https://github.com/CodingDocs/awesome-cs\" target=\"_blank\" rel=\"noopener noreferrer\">CodingDocs/awesome-cs</a>，会持续更新。欢迎在项目 <a href=\"https://github.com/CodingDocs/awesome-cs/issues\" target=\"_blank\" rel=\"noopener noreferrer\">issues 区</a> 推荐你认可的技术书籍，一起维护一个更高质量的中文技术书单。</p>\n<h2>适合谁看</h2>\n<ul>\n<li>想系统补基础，但不知道该读哪些书的程序员。</li>\n<li>准备校招、社招、中大厂后端面试的同学。</li>\n<li>希望从碎片化文章过渡到体系化学习的后端开发者。</li>\n<li>想提升架构、数据库、代码质量、工程协作能力的工程师。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>书单不是越长越好，优先选择和当前阶段最匹配的一两本精读。</li>\n<li>计算机基础、Java、数据库是后端开发最常用的底层能力。</li>\n<li>分布式系统和搜索引擎适合在有一定工程经验后深入学习。</li>\n<li>软件质量类书籍适合用来提升代码可维护性、测试意识和团队协作能力。</li>\n<li>读书要结合项目、面试题和源码实践，否则容易停留在概念层面。</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/books/cs-basics.html\" target=\"_blank\">计算机基础书籍推荐</a>：先补操作系统、网络、数据结构与算法等通用基础。</li>\n<li><a href=\"/books/java.html\" target=\"_blank\">Java 书籍推荐</a>：再系统学习 Java 基础、并发、JVM、框架和性能优化。</li>\n<li><a href=\"/books/database.html\" target=\"_blank\">数据库书籍推荐</a>：掌握 MySQL、Redis、NoSQL 和数据工程相关知识。</li>\n<li><a href=\"/books/distributed-system.html\" target=\"_blank\">分布式系统书籍推荐</a>：有项目经验后再深入分布式理论、架构和中间件。</li>\n<li><a href=\"/books/software-quality.html\" target=\"_blank\">软件质量书籍推荐</a>：用重构、测试、工程化和协作能力提升长期产出质量。</li>\n</ol>\n<h2>核心文章</h2>\n<ul>\n<li><a href=\"/books/cs-basics.html\" target=\"_blank\">计算机基础书籍推荐</a>：操作系统、计算机网络、数据结构与算法等基础书单，适合打底和面试复习。</li>\n<li><a href=\"/books/java.html\" target=\"_blank\">Java 书籍推荐</a>：覆盖 Java 基础、并发、JVM、框架、性能优化等方向的经典书籍。</li>\n<li><a href=\"/books/database.html\" target=\"_blank\">数据库书籍推荐</a>：整理 MySQL、Redis、NoSQL、数据工程等后端常用数据库方向书籍。</li>\n<li><a href=\"/books/distributed-system.html\" target=\"_blank\">分布式系统书籍推荐</a>：覆盖分布式理论、系统架构、中间件和工程实践。</li>\n<li><a href=\"/books/search-engine.html\" target=\"_blank\">搜索引擎书籍推荐</a>：整理信息检索、搜索架构、Elasticsearch 等相关书籍与资料。</li>\n<li><a href=\"/books/software-quality.html\" target=\"_blank\">软件质量书籍推荐</a>：覆盖代码质量、重构、测试、工程化和团队协作相关书籍。</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/cs-basics/\" target=\"_blank\">计算机基础知识体系</a></li>\n<li><a href=\"/java/\" target=\"_blank\">Java 知识体系</a></li>\n<li><a href=\"/database/\" target=\"_blank\">数据库知识体系</a></li>\n<li><a href=\"/distributed-system/\" target=\"_blank\">分布式系统知识体系</a></li>\n<li><a href=\"/high-quality-technical-articles/\" target=\"_blank\">高质量技术文章</a></li>\n<li><a href=\"/open-source-project/\" target=\"_blank\">Java 开源项目精选</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "计算机书籍"
      ]
    },
    {
      "title": "计算机基础必读经典书籍",
      "url": "https://codingmap.strova.top/books/cs-basics.html",
      "id": "https://codingmap.strova.top/books/cs-basics.html",
      "summary": "计算机基础书籍推荐，操作系统、计算机网络、算法与数据结构、编译原理等核心课程经典教材和学习资源汇总。重点围绕 操作系统、计算机网络、算法、数据结构 等内容展开。结合 CodingMap 知识体系梳理 技术书籍 / Cs Basics.Html 的核心概念、实践方法、常见问题和高频面试考点，覆盖原理分析、使用场景等核心。",
      "content_html": "<p>考虑到很多同学比较喜欢看视频，因此，这部分内容我不光会推荐书籍，还会顺便推荐一些我觉得不错的视频教程和各大高校的 Project。</p>\n<h2>操作系统</h2>\n<p><strong>为什么要学习操作系统？</strong></p>\n<p><strong>从对个人能力方面提升来说</strong>，操作系统中的很多思想、很多经典的算法，你都可以在我们日常开发使用的各种工具或者框架中找到它们的影子。比如说我们开发的系统使用的缓存（比如 Redis）和操作系统的高速缓存就很像。CPU 中的高速缓存有很多种，不过大部分都是为了解决 CPU 处理速度和内存处理速度不对等的问题。我们还可以把内存可以看作外存的高速缓存，程序运行的时候我们把外存的数据复制到内存，由于内存的处理速度远远高于外存，这样提高了处理速度。同样地，我们使用的 Redis 缓存就是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。高速缓存一般会按照局部性原理（2-8 原则）根据相应的淘汰算法保证缓存中的数据是经常会被访问的。我们平常使用的 Redis 缓存很多时候也会按照 2-8 原则去做，很多淘汰算法都和操作系统中的类似。既说了 2-8 原则，那就不得不提命中率了，这是所有缓存概念都通用的。简单来说也就是你要访问的数据有多少能直接在缓存中直接找到。命中率高的话，一般表明你的缓存设计比较合理，系统处理速度也相对较快。</p>\n<p><strong>从面试角度来说</strong>，尤其是校招，对于操作系统方面知识的考察是非常非常多的。</p>\n<p><strong>简单来说，学习操作系统能够提高自己思考的深度以及对技术的理解力，并且，操作系统方面的知识也是面试必备。</strong></p>\n<p>如果你要系统地学习操作系统的话，最硬核最权威的书籍是 <strong><a href=\"https://book.douban.com/subject/33463930/\" target=\"_blank\" rel=\"noopener noreferrer\">《操作系统导论》</a></strong> 。你可以再配套一个 <strong><a href=\"https://book.douban.com/subject/1230413/\" target=\"_blank\" rel=\"noopener noreferrer\">《深入理解计算机系统》</a></strong> 加深你对计算机系统本质的认识，美滋滋！</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20201012191645919.png\" alt></p>\n<p>另外，去年新出的一本国产的操作系统书籍也很不错：<strong><a href=\"https://book.douban.com/subject/35208251/\" target=\"_blank\" rel=\"noopener noreferrer\">《现代操作系统：原理与实现》</a></strong> （夏老师和陈老师团队的力作，值得推荐）。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/20210406132050845.png\" alt></p>\n<p>如果你比较喜欢动手，对于理论知识比较抵触的话，我推荐你看看 <strong><a href=\"https://book.douban.com/subject/11530329/\" target=\"_blank\" rel=\"noopener noreferrer\">《30 天自制操作系统》</a></strong> ，这本书会手把手教你编写一个操作系统。</p>\n<p>纸上学来终觉浅 绝知此事要躬行！强烈推荐 CS 专业的小伙伴一定要多多实践！！！</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20220409123802972.png\" alt></p>\n<p>其他相关书籍推荐：</p>\n<ul>\n<li><strong><a href=\"https://book.douban.com/subject/1422377/\" target=\"_blank\" rel=\"noopener noreferrer\">《自己动手写操作系统》</a></strong>：不光会带着你详细分析操作系统原理的基础，还会用丰富的实例代码，一步一步地指导你用 C 语言和汇编语言编写出一个具备操作系统基本功能的操作系统框架。</li>\n<li><strong><a href=\"https://book.douban.com/subject/3852290/\" target=\"_blank\" rel=\"noopener noreferrer\">《现代操作系统》</a></strong>：内容很不错，不过，翻译的一般。如果你是精读本书的话，建议把课后习题都做了。</li>\n<li><strong><a href=\"https://book.douban.com/subject/26745156/\" target=\"_blank\" rel=\"noopener noreferrer\">《操作系统真象还原》</a></strong>：这本书的作者毕业于北京大学，前百度运维高级工程师。因为在大学期间曾重修操作系统这一科，后对操作系统进行深入研究，著下此书。</li>\n<li><strong><a href=\"https://book.douban.com/subject/25743846/\" target=\"_blank\" rel=\"noopener noreferrer\">《深度探索 Linux 操作系统》</a></strong>：跟着这本书的内容走，可以让你对如何制作一套完善的 GNU/Linux 系统有了清晰的认识。</li>\n<li><strong><a href=\"https://book.douban.com/subject/2044818/\" target=\"_blank\" rel=\"noopener noreferrer\">《操作系统设计与实现》</a></strong>：操作系统的权威教学教材。</li>\n<li><strong><a href=\"https://book.douban.com/subject/3735649/\" target=\"_blank\" rel=\"noopener noreferrer\">《Orange'S:一个操作系统的实现》</a></strong>：从只有二十行的引导扇区代码出发，一步一步地向读者呈现一个操作系统框架的完成过程。配合《操作系统设计与实现》一起食用更佳！</li>\n</ul>\n<p>如果你比较喜欢看视频的话，推荐哈工大李治军老师主讲的慕课 <a href=\"https://www.icourse163.org/course/HIT-1002531008\" target=\"_blank\" rel=\"noopener noreferrer\">《操作系统》</a>，内容质量吊打一众国家精品课程。</p>\n<p>课程的大纲如下：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220414144527747.png\" alt=\"课程大纲\"></p>\n<p>主要讲了一个基本操作系统中的六个基本模块：CPU 管理、内存管理、外设管理、磁盘管理与文件系统、用户接口和启动模块 。</p>\n<p>课程难度还是比较大的，尤其是课后的 lab。如果大家想要真正搞懂操作系统底层原理的话，对应的 lab 能做尽量做一下。正如李治军老师说的那样：“纸上得来终觉浅，绝知此事要躬行”。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220414145210679.png\" alt></p>\n<p>如果你能独立完成几个 lab 的话，我相信你对操作系统的理解绝对要上升几个台阶。当然了，如果你仅仅是为了突击面试的话，那就不需要做 lab 了。</p>\n<p>说点心里话，我本人非常喜欢李治军老师讲的课，我觉得他是国内不可多得的好老师。他知道我们国内的教程和国外的差距在哪里，也知道国内的学生和国外学生的差距在哪里，他自己在努力着通过自己的方式来缩小这个差距。真心感谢，期待李治军老师的下一个课程。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220414145249714.png\" alt></p>\n<p>还有下面这个国外的课程 <a href=\"https://www.bilibili.com/video/av31289365?from=search&amp;seid=16298868573410423104\" target=\"_blank\" rel=\"noopener noreferrer\">《深入理解计算机系统 》</a> 也很不错。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20201204140653318.png\" alt></p>\n<h2>计算机网络</h2>\n<p>计算机网络是一门系统性比较强的计算机专业课，各大名校的计算机网络课程打磨的应该都比较成熟。</p>\n<p>要想学好计算机网络，首先要了解的就是 OSI 七层模型或 TCP/IP 五层模型，即应用层（应用层、表示层、会话层）、传输层、网络层、数据链路层、物理层。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksosi七层模型2.png\" alt=\"osi七层模型\"></p>\n<p>关于这门课，首先强烈推荐参考书是<strong>机械工业出版社的《计算机网络——自顶向下方法》</strong>。该书目录清晰，按照 TCP/IP 五层模型逐层讲解，对每层涉及的技术都展开了详细讨论，基本上高校里开设的课程的教学大纲就是这本书的目录了。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20220409123250570.png\" alt></p>\n<p>如果你觉得上面这本书看着比较枯燥的话，我强烈推荐+安利你看看下面这两本非常有趣的网络相关的书籍：</p>\n<ul>\n<li><a href=\"https://book.douban.com/subject/25863515/\" title=\"《图解 HTTP》\" target=\"_blank\" rel=\"noopener noreferrer\">《图解 HTTP》</a>：讲漫画一样的讲 HTTP，很有意思，不会觉得枯燥，大概也涵盖也 HTTP 常见的知识点。因为篇幅问题，内容可能不太全面。不过，如果不是专门做网络方向研究的小伙伴想研究 HTTP 相关知识的话，读这本书的话应该来说就差不多了。</li>\n<li><a href=\"https://book.douban.com/subject/26941639/\" title=\"《网络是怎样连接的》\" target=\"_blank\" rel=\"noopener noreferrer\">《网络是怎样连接的》</a>：从在浏览器中输入网址开始，一路追踪了到显示出网页内容为止的整个过程，以图配文，讲解了网络的全貌，并重点介绍了实际的网络设备和软件是如何工作的。</li>\n</ul>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20201011215144139.png\" alt></p>\n<p>除了理论知识之外，学习计算机网络非常重要的一点就是：“<strong>动手实践</strong>”。这点和我们编程差不多。</p>\n<p>GitHub 上就有一些名校的计算机网络试验/Project：</p>\n<ul>\n<li><a href=\"https://github.com/rccoder/HIT-Computer-Network\" target=\"_blank\" rel=\"noopener noreferrer\">哈工大计算机网络实验</a></li>\n<li><a href=\"https://github.com/moranzcw/Computer-Networking-A-Top-Down-Approach-NOTES\" target=\"_blank\" rel=\"noopener noreferrer\">《计算机网络－自顶向下方法(原书第 6 版)》编程作业，Wireshark 实验文档的翻译和解答。</a></li>\n<li><a href=\"https://github.com/KevinWang15/network-pj-chatroom\" target=\"_blank\" rel=\"noopener noreferrer\">计算机网络的期末 Project，用 Python 编写的聊天室</a></li>\n<li><a href=\"https://computer-networks.github.io/sp19/lectures.html\" target=\"_blank\" rel=\"noopener noreferrer\">CMU 的计算机网络课程</a></li>\n</ul>\n<p>我知道，还有很多小伙伴可能比较喜欢边看视频边学习。所以，我这里再推荐几个顶好的计算机网络视频讲解。</p>\n<p><strong>1、<a href=\"http://www.icourse163.org/course/HIT-154005\" target=\"_blank\" rel=\"noopener noreferrer\">哈工大的计算机网络课程</a></strong>：国家精品课程，截止目前已经开了 10 次课了。大家对这门课的评价都非常高！所以，非常推荐大家看一下！</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20201218141241911.png\" alt></p>\n<p><strong>2、<a href=\"https://www.bilibili.com/video/BV19E411D78Q?from=search&amp;seid=17198507506906312317\" target=\"_blank\" rel=\"noopener noreferrer\">王道考研的计算机网络</a></strong>：非常适合 CS 专业考研的小朋友！这个视频目前在哔哩哔哩上已经有 1.6w+ 的点赞。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20201218141652837.png\" alt></p>\n<h2>算法</h2>\n<p>先来看三本入门书籍。 这三本入门书籍中的任何一本拿来作为入门学习都非常好。</p>\n<ol>\n<li><a href=\"https://book.douban.com/subject/30357170/\" target=\"_blank\" rel=\"noopener noreferrer\">《我的第一本算法书》</a></li>\n<li><a href=\"https://book.douban.com/subject/26979890/\" target=\"_blank\" rel=\"noopener noreferrer\">《算法图解》</a></li>\n<li><a href=\"https://book.douban.com/subject/25894685/\" target=\"_blank\" rel=\"noopener noreferrer\">《啊哈!算法》</a></li>\n</ol>\n<p><img src=\"https://oss.javaguide.cn/java-guide-blog/image-20210327104418851.png\" alt></p>\n<p>我个人比较倾向于 <strong><a href=\"https://book.douban.com/subject/30357170/\" target=\"_blank\" rel=\"noopener noreferrer\">《我的第一本算法书》</a></strong> 这本书籍，虽然它相比于其他两本书集它的豆瓣评分略低一点。我觉得它的配图以及讲解是这三本书中最优秀，唯一比较明显的问题就是没有代码示例。但是，我觉得这不影响它是一本好的算法书籍。因为本身下面这三本入门书籍的目的就不是通过代码来让你的算法有多厉害，只是作为一本很好的入门书籍让你进入算法学习的大门。</p>\n<p>再推荐几本比较经典的算法书籍。</p>\n<p><strong><a href=\"https://book.douban.com/subject/19952400/\" target=\"_blank\" rel=\"noopener noreferrer\">《算法》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20220409123422140.png\" alt></p>\n<p>这本书内容非常清晰易懂，适合数据结构和算法小白阅读。书中把一些常用的数据结构和算法都介绍到了！</p>\n<p>我在大二的时候被我们的一个老师强烈安利过！自己也在当时购买了一本放在宿舍，到离开大学的时候自己大概看了一半多一点。因为内容实在太多了！另外，这本书还提供了详细的 Java 代码，非常适合学习 Java 的朋友来看，可以说是 Java 程序员的必备书籍之一了。</p>\n<blockquote>\n<p><strong>下面这些书籍都是经典中的经典，但是阅读起来难度也比较大，不做太多阐述，神书就完事了！</strong></p>\n<p><strong>如果你仅仅是准备算法面试的话，不建议你阅读下面这些书籍。</strong></p>\n</blockquote>\n<p><strong><a href=\"https://book.douban.com/subject/3227098/\" target=\"_blank\" rel=\"noopener noreferrer\">《编程珠玑》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20220409145334093.png\" alt></p>\n<p>经典名著，ACM 冠军、亚军这种算法巨佬都强烈推荐的一本书籍。这本书的作者也非常厉害，Java 之父 James Gosling 就是他的学生。</p>\n<p>很多人都说这本书不是教你具体的算法，而是教你一种编程的思考方式。这种思考方式不仅仅在编程领域适用，在其他同样适用。</p>\n<p><strong><a href=\"https://book.douban.com/subject/4048566/\" target=\"_blank\" rel=\"noopener noreferrer\">《算法设计手册》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20220409145411049.png\" alt></p>\n<p>这是一本被 GitHub 上的爆火的计算机自学项目 <a href=\"https://link.zhihu.com/?target=https%3A//teachyourselfcs.com/\" target=\"_blank\" rel=\"noopener noreferrer\">Teach Yourself Computer Science</a> 强烈推荐的一本算法书籍。</p>\n<p>类似的神书还有 <a href=\"https://book.douban.com/subject/20432061/\" target=\"_blank\" rel=\"noopener noreferrer\">《算法导论》</a>、<a href=\"https://book.douban.com/subject/1130500/\" target=\"_blank\" rel=\"noopener noreferrer\">《计算机程序设计艺术（第 1 卷）》</a> 。</p>\n<p><strong>如果说你要准备面试的话，下面这几本书籍或许对你有帮助！</strong></p>\n<p><strong><a href=\"https://book.douban.com/subject/6966465/\" target=\"_blank\" rel=\"noopener noreferrer\">《剑指 Offer》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20220409145506482.png\" alt></p>\n<p>这本面试宝典上面涵盖了很多经典的算法面试题，如果你要准备大厂面试的话一定不要错过这本书。</p>\n<p>《剑指 Offer》 对应的算法编程题部分的开源项目解析：<a href=\"https://link.zhihu.com/?target=https%3A//github.com/gatieme/CodingInterviews\" target=\"_blank\" rel=\"noopener noreferrer\">CodingInterviews</a> 。</p>\n<p><strong><a href=\"https://book.douban.com/subject/30422021/\" target=\"_blank\" rel=\"noopener noreferrer\">《程序员代码面试指南（第 2 版）》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20220409145622758.png\" alt></p>\n<p>《程序员代码面试指南（第 2 版）》里的大部分题目相比于《剑指 offer》 来说要难很多，题目涵盖面相比于《剑指 offer》也更加全面。全书一共有将近 300 道真实出现过的经典代码面试题。</p>\n<p>视频的话，推荐北京大学的国家精品课程—<strong><a href=\"https://www.icourse163.org/course/PKU-1001894005\" target=\"_blank\" rel=\"noopener noreferrer\">程序设计与算法（二）算法基础</a></strong>，讲的非常好！</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/22ce4a17dc0c40f6a3e0d58002261b7a.png\" alt></p>\n<p>这个课程把七种基本的通用算法（枚举、二分、递归、分治、动态规划、搜索、贪心）都介绍到了。各种复杂算法问题的解决，都可能用到这些基本的思想。并且，这个课程的一部分的例题和 ACM 国际大学生程序设计竞赛中的中等题相当，如果你能够解决这些问题，那你的算法能力将超过绝大部分的高校计算机专业本科毕业生。</p>\n<h2>数据结构</h2>\n<p>其实，上面提到的很多算法类书籍（比如 <strong>《算法》</strong> 和 <strong>《算法导论》</strong>）都详细地介绍了常用的数据结构。</p>\n<p>我这里再另外补充基本和数据结构相关的书籍。</p>\n<p><strong><a href=\"https://book.douban.com/subject/6424904/\" target=\"_blank\" rel=\"noopener noreferrer\">《大话数据结构》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20220409145803440.png\" alt></p>\n<p>入门类型的书籍，读起来比较浅显易懂，适合没有数据结构基础或者说数据结构没学好的小伙伴用来入门数据结构。</p>\n<p><strong><a href=\"https://book.douban.com/subject/3351237/\" target=\"_blank\" rel=\"noopener noreferrer\">《数据结构与算法分析：Java 语言描述》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20220409145823973.png\" alt></p>\n<p>质量很高，介绍了常用的数据结构和算法。</p>\n<p>类似的还有 <strong><a href=\"https://book.douban.com/subject/1139426/\" target=\"_blank\" rel=\"noopener noreferrer\">《数据结构与算法分析：C 语言描述》</a></strong>、<strong><a href=\"https://book.douban.com/subject/1971825/\" target=\"_blank\" rel=\"noopener noreferrer\">《数据结构与算法分析：C++ 描述》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/d9c450ccc5224a5fba77f4fa937f7b9c.png\" alt></p>\n<p>视频的话推荐你看浙江大学的国家精品课程—<strong><a href=\"https://www.icourse163.org/course/ZJU-93001#/info\" target=\"_blank\" rel=\"noopener noreferrer\">《数据结构》</a></strong> 。</p>\n<p>姥姥的数据结构讲的非常棒！不过，还是有一些难度的，尤其是课后练习题。</p>\n<h2>计算机专业基础课</h2>\n<p>数学和英语属于通用课，一般在大一和大二两学年就可以全部修完，大二大三逐渐接触专业课。通用课作为许多高中生升入大学的第一门课，算是高中阶段到本科阶段的一个过渡，从职业生涯重要性上来说，远不及专业课重要，但是在本科阶段的学习生活规划中，有着非常重要的地位。由于通用课的课程多，学分重，占据了本科阶段绩点的主要部分，影响到学生在前两年的专业排名，也影响到大三结束时的推免资格分配，也就是保研。而从升学角度来看，对于攻读研究生和博士生的小伙伴来说，数学和英语这两大基础课，还是十分有用的。</p>\n<h3>数学</h3>\n<h4>微积分（高等数学）</h4>\n<p>微积分，即传说中的高数，成为了无数新大一心中的痛。但好在，大学的课程考核没那么严格，期末想要拿高分，也不至于像高中那样刷题刷的那么狠。微积分对于计算机专业学生的重要性，主要体现在计算机图形学中的函数变换，机器学习中的梯度算法，信号处理等领域。</p>\n<p>微积分的知识体系包括微分和积分两部分，一般会先学微分，再学积分，也有的学校把高数分为两个学期。微分就是高中的导数的升级版，对于大一萌新来说还算比较友好。积分恰好是微分的逆运算，思想上对大一萌新来说比较新，一时半会可能接受不了。不过这门课所有的高校都有开设，而且大部分的名校都有配套的网课，教材也都打磨的非常出色，结合网课和教材的“啃书”学习模式，这门课一定不会落下。</p>\n<p>书籍的话，推荐《普林斯顿微积分读本》。这本书详细讲解了微积分基础、极限、连续、微分、导数的应用、积分、无穷级数、泰勒级数与幂级数等内容。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20220409155056751.png\" alt></p>\n<h4>线性代数（高等代数）</h4>\n<p>线性代数的思维模式就更加复杂了一些，它定义了一个全新的数学世界，所有的符号、定理都是全新的，唯一能尝试的去理解的方式，大概就是用几何的方式去理解线性代数了。由于线性代数和几何学有着密不可分的关系，比如空间变换的理论支撑就是线性代数，因此，网上有着各种“可视化学习线性代数”的学习资源，帮助理解线性代数的意义，有助于公式的记忆。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20220409153940473.png\" alt></p>\n<p>书籍的话，推荐中科大李尚志老师的 <strong><a href=\"https://book.douban.com/subject/26390093/\" target=\"_blank\" rel=\"noopener noreferrer\">《线性代数学习指导》</a></strong> 。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20220409155325251.png\" alt></p>\n<h4>概率论与数理统计</h4>\n<p>对于计算机专业的小伙伴来说，这门课可能是概率论更有用一点，而非数理统计。可能某些学校只开设概率论课程，也可能数理统计也教，但仅仅是皮毛。概率论的学习路线和微积分相似，就是一个个公式辅以实例，不像线性代数那么抽象，比较贴近生活。在现在的就业形势下，概率论与数理统计专业的学生，应该是数学专业最好就业的了，他们通常到岗位上会做一些数据分析的工作，因此，<strong>这门课程确实是数据分析的重要前置课程，概率论在机器学习中的重要性也就不言而喻了。</strong></p>\n<p>书籍的话，推荐 <strong><a href=\"https://book.douban.com/subject/34897672/\" target=\"_blank\" rel=\"noopener noreferrer\">《概率论与数理统计教程》</a></strong> 。这本书共八章，前四章为概率论部分，主要叙述各种概率分布及其性质，后四章为数理统计部分，主要叙述各种参数估计与假设检验。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20220409155738505.png\" alt></p>\n<h4>离散数学（集合论、图论、近世代数等）</h4>\n<p>离散数学是计算机专业的专属数学，但实际上对于本科毕业找工作的小伙伴来说，离散数学还并没有发挥它的巨大作用。离散数学的作用主要在在图研究等领域，理论性极强，需要读研深造的小伙伴尽可能地扎实掌握。</p>\n<h3>英语</h3>\n<p>英语算是大学里面比较灵活的一项技能了，有的人会说，“英语学的越好，对个人发展越有利”，此话说的没错，但是对于一些有着明确发展目标的小伙伴，可能英语技能并不在他们的技能清单内。接下来的这些话只针对计算机专业的小伙伴们哦。</p>\n<p>英语课在大学本科一般只有前两年开设，小伙伴们可以记住，<strong>想用英语课来提升自己的英语水平的，可以打消这个念头了。</strong> 英语水平的提高全靠自己平时的积累和练习，以及有针对性的刷题。</p>\n<p><strong>英语的大学四六级一定要过。</strong> 这是必备技能，绝大部分就业岗位都要看四六级水平的，最起码要通过的。四级比高中英语稍微难一些，一般的小伙伴可能会卡在六级上，六级需要针对性的训练一下，因为大学期间能接触英语的实在太少了，每学期一门英语课是不足以保持自己的英语水平的。对于一些来自于偏远地区，高中英语基础薄弱的，考四六级会更加吃力。建议考前集中训练一下历年真题，辅以背一下高频词汇，四六级通过只需要 425 分，这个分数线还是比较容易达到的。稍微好一点的小伙伴可能冲一下 500 分，要是能考到 600 分的话，那是非常不错的水平了，算是简历上比较有亮点的一项。</p>\n<p>英语的雅思托福考试只限于想要出国的小伙伴，以及应聘岗位对英语能力有特殊要求的。雅思托福考试裸考不容易通过，花钱去比较靠谱的校外补课班应该是一个比较好的选择。</p>\n<p>对于计算机专业的小伙伴来说，英语能力还是比较重要的，虽然应聘的时候不会因为没有雅思托福成绩卡人，但是你起码要能够：</p>\n<ul>\n<li><strong>熟练使用英文界面的软件、系统等</strong></li>\n<li><strong>对于外网的一些博客、bug 解决方案等，阅读无压力</strong></li>\n<li><strong>熟练阅读英文文献</strong></li>\n<li><strong>具备一定的英文论文的撰写能力</strong></li>\n</ul>\n<p>毕竟计算机语言就是字符语言，听说读写中最起码要满足<strong>读写</strong>这两项不过分吧。</p>\n<h3>编译原理</h3>\n<p>编译原理相比于前面介绍的专业课，地位显得不那么重要了。编译原理的重要性主要体现在：</p>\n<ul>\n<li>底层语言、引擎或高级语言的开发，如 MySQL，Java 等</li>\n<li>操作系统或嵌入式系统的开发</li>\n<li>词法、语法、语义的思想，以及自动机思想</li>\n</ul>\n<p><strong>编译原理的重要前置课程就是形式语言与自动机，自动机的思想在词法分析当中有着重要应用，学习了这门课后，应该就会发现许多场景下，自动机算法的妙用了。</strong></p>\n<p>总的来说，这门课对于各位程序员的职业发展来说，相对不那么重要，但是从难度上来说，学习这门课可以对编程思想有一个较好的巩固。学习资源的话，除了课堂上的幻灯片课件以外，还可以把 《编译原理》 这本书作为参考书，用以辅助自己学不懂的地方（大家口中的龙书，想要啃下来还是有一定难度的）。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/20210406152148373.png\" alt></p>\n<p>其他书籍推荐:</p>\n<ul>\n<li><strong><a href=\"https://book.douban.com/subject/30191414/\" target=\"_blank\" rel=\"noopener noreferrer\">《现代编译原理》</a></strong>：编译原理的入门书。</li>\n<li><strong><a href=\"https://book.douban.com/subject/20436488/\" target=\"_blank\" rel=\"noopener noreferrer\">《编译器设计》</a></strong>：覆盖了编译器从前端到后端的全部主题。</li>\n</ul>\n<p>我上面推荐的书籍的难度还是比较高的，真心很难坚持看完。这里强烈推荐<a href=\"https://www.icourse163.org/course/HIT-1002123007\" target=\"_blank\" rel=\"noopener noreferrer\">哈工大的编译原理视频课程</a>，真心不错，还是国家精品课程，关键还是又漂亮有温柔的美女老师讲的！</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/20210406152847824.png\" alt></p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/booksimage-20201012191645919.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "计算机书籍"
      ]
    },
    {
      "title": "数据库必读经典书籍",
      "url": "https://codingmap.strova.top/books/database.html",
      "id": "https://codingmap.strova.top/books/database.html",
      "summary": "数据库书籍推荐，MySQL、PostgreSQL、Redis等数据库经典书籍，涵盖入门教程、原理剖析、性能优化等内容。重点围绕 数据库基础、MySQL、PostgreSQL、Redis 等内容展开。结合 CodingMap 知识体系梳理 技术书籍 / Database.Html 的核心概念、实践方法等核心内容。",
      "content_html": "<h2>数据库基础</h2>\n<p>数据库基础这块，如果你觉得书籍比较枯燥，自己坚持不下来的话，我推荐你可以先看看一些不错的视频，北京师范大学的<a href=\"https://www.icourse163.org/course/BNU-1002842007\" target=\"_blank\" rel=\"noopener noreferrer\">《数据库系统原理》</a>、哈尔滨工业大学的<a href=\"https://www.icourse163.org/course/HIT-1001578001\" target=\"_blank\" rel=\"noopener noreferrer\">《数据库系统（下）：管理与技术》</a>就很不错。</p>\n<p><a href=\"https://www.icourse163.org/course/BNU-1002842007\" target=\"_blank\" rel=\"noopener noreferrer\">《数据库系统原理》</a>这个课程的老师讲的非常详细，而且每一小节的作业设计的也与所讲知识很贴合，后面还有很多配套实验。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/up-e113c726a41874ef5fb19f7ac14e38e16ce.png\" alt></p>\n<p>如果你比较喜欢动手，对于理论知识比较抵触的话，推荐你看看<a href=\"https://cstack.github.io/db_tutorial/\" target=\"_blank\" rel=\"noopener noreferrer\">《如何开发一个简单的数据库》</a> ，这个 project 会手把手教你编写一个简单的数据库。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/up-11de8cb239aa7201cc8d78fa28928b9ec7d.png\" alt></p>\n<p>GitHub 上也已经有大佬用 Java 实现过一个简易的数据库，介绍的挺详细的，感兴趣的朋友可以去看看。地址：<a href=\"https://github.com/alchemystar/Freedom\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/alchemystar/Freedom</a> 。</p>\n<p>除了这个用 Java 写的之外，<strong><a href=\"https://github.com/cstack/db_tutorial\" target=\"_blank\" rel=\"noopener noreferrer\">db_tutorial</a></strong> 这个项目是国外的一个大佬用 C 语言写的，朋友们也可以去瞅瞅。</p>\n<p><strong>只要利用好搜索引擎，你可以找到各种语言实现的数据库玩具。</strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/up-d32d853f847633ac7ed0efdecf56be1f1d2.png\" alt></p>\n<p><strong>纸上学来终觉浅 绝知此事要躬行！强烈推荐 CS 专业的小伙伴一定要多多实践！！！</strong></p>\n<h3>《数据库系统概念》</h3>\n<p><a href=\"https://book.douban.com/subject/10548379/\" target=\"_blank\" rel=\"noopener noreferrer\">《数据库系统概念》</a>这本书涵盖了数据库系统的全套概念，知识体系清晰，是学习数据库系统非常经典的教材！不是参考书！</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/booksimage-20220409150441742.png\" alt></p>\n<h3>《数据库系统实现》</h3>\n<p>如果你也想要研究 MySQL 底层原理的话，我推荐你可以先阅读一下<a href=\"https://book.douban.com/subject/4838430/\" target=\"_blank\" rel=\"noopener noreferrer\">《数据库系统实现》</a>。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/database-system-implementation.png\" alt></p>\n<p>不管是 MySQL 还是 Oracle ，它们总体的架子是差不多的，不同的是其内部的实现比如数据库索引的数据结构、存储引擎的实现方式等等。</p>\n<p>这本书有些地方还是翻译的比较蹩脚，有能力看英文版的还是建议上手英文版。</p>\n<p>《数据库系统实现》 这本书是斯坦福的教材，另外还有一本<a href=\"https://book.douban.com/subject/3923575/\" target=\"_blank\" rel=\"noopener noreferrer\">《数据库系统基础教程》</a>是前置课程，可以带你入门数据库。</p>\n<h2>MySQL</h2>\n<p>我们网站或者 APP 的数据都是需要使用数据库来存储数据的。</p>\n<p>一般企业项目开发中，使用 MySQL 比较多。如果你要学习 MySQL 的话，可以看下面这 3 本书籍：</p>\n<ul>\n<li><strong><a href=\"https://book.douban.com/subject/3354490/\" target=\"_blank\" rel=\"noopener noreferrer\">《MySQL 必知必会》</a></strong>：非常薄！非常适合 MySQL 新手阅读，很棒的入门教材。</li>\n<li><strong><a href=\"https://book.douban.com/subject/23008813/\" target=\"_blank\" rel=\"noopener noreferrer\">《高性能 MySQL》</a></strong>：MySQL 领域的经典之作！学习 MySQL 必看！属于进阶内容，主要教你如何更好地使用 MySQL 。既有有理论，又有实践！如果你没时间都看一遍的话，我建议第 5 章（创建高性能的索引）、第 6 章（查询性能优化） 你一定要认真看一下。</li>\n<li><strong><a href=\"https://book.douban.com/subject/24708143/\" target=\"_blank\" rel=\"noopener noreferrer\">《MySQL 技术内幕》</a></strong>：你想深入了解 MySQL 存储引擎的话，看这本书准没错！</li>\n</ul>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/up-3d31e762933f9e50cc7170b2ebd8433917b.png\" alt></p>\n<p>视频的话，你可以看看动力节点的 <a href=\"https://www.bilibili.com/video/BV1fx411X7BD\" target=\"_blank\" rel=\"noopener noreferrer\">《MySQL 数据库教程视频》</a>。这个视频基本上把 MySQL 的相关一些入门知识给介绍完了。</p>\n<p>另外，强推一波 <strong><a href=\"https://book.douban.com/subject/35231266/\" target=\"_blank\" rel=\"noopener noreferrer\">《MySQL 是怎样运行的》</a></strong> 这本书，内容很适合拿来准备面试。讲的很细节，但又不枯燥，内容非常良心！</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/csdn/20210703120643370.png\" alt></p>\n<h2>PostgreSQL</h2>\n<p>和 MySQL 一样，PostgreSQL 也是开源免费且功能强大的关系型数据库。PostgreSQL 的 Slogan 是“<strong>世界上最先进的开源关系型数据库</strong>” 。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220702144954370.png\" alt></p>\n<p>最近几年，由于 PostgreSQL 的各种新特性过于优秀，使用 PostgreSQL 代替 MySQL 的项目越来越多了。</p>\n<p>如果你还在纠结是否尝试一下 PostgreSQL 的话，建议你看看这个知乎话题：<a href=\"https://www.zhihu.com/question/20010554\" target=\"_blank\" rel=\"noopener noreferrer\">PostgreSQL 与 MySQL 相比，优势何在？ - 知乎</a> 。</p>\n<h3>《PostgreSQL 指南：内幕探索》</h3>\n<p><a href=\"https://book.douban.com/subject/33477094/\" target=\"_blank\" rel=\"noopener noreferrer\">《PostgreSQL 指南：内幕探索》</a>这本书主要介绍了 PostgreSQL 内部的工作原理，包括数据库对象的逻辑组织与物理实现，进程与内存的架构。</p>\n<p>刚工作那会需要用到 PostgreSQL ，看了大概 1/3 的内容，感觉还不错。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/PostgreSQL-Guide.png\" alt></p>\n<h3>《PostgreSQL 技术内幕：查询优化深度探索》</h3>\n<p><a href=\"https://book.douban.com/subject/30256561/\" target=\"_blank\" rel=\"noopener noreferrer\">《PostgreSQL 技术内幕：查询优化深度探索》</a>这本书主要讲了 PostgreSQL 在查询优化上的一些技术实现细节，可以让你对 PostgreSQL 的查询优化器有深层次的了解。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/PostgreSQL-TechnologyInsider.png\" alt=\"《PostgreSQL 技术内幕：查询优化深度探索》\"></p>\n<h2>Redis</h2>\n<p><strong>Redis 就是一个使用 C 语言开发的数据库</strong>，不过与传统数据库不同的是 <strong>Redis 的数据是存在内存中的</strong> ，也就是它是内存数据库，所以读写速度非常快，因此 Redis 被广泛应用于缓存方向。</p>\n<p>如果你要学习 Redis 的话，强烈推荐下面这两本书：</p>\n<ul>\n<li><a href=\"https://book.douban.com/subject/25900156/\" target=\"_blank\" rel=\"noopener noreferrer\">《Redis 设计与实现》</a> ：主要是 Redis 理论知识相关的内容，比较全面。我之前写过一篇文章 <a href=\"https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&amp;mid=2247507030&amp;idx=1&amp;sn=0a5fd669413991b30163ab6f5834a4ad&amp;chksm=cea1939df9d61a8b93925fae92f4cee0838c449534e60731cfaf533369831192e296780b32a6&amp;token=709354671&amp;lang=zh_CN&amp;scene=21#wechat_redirect\" target=\"_blank\" rel=\"noopener noreferrer\">《7 年前，24 岁，出版了一本 Redis 神书》</a> 来介绍这本书。</li>\n<li><a href=\"https://book.douban.com/subject/26612779/\" target=\"_blank\" rel=\"noopener noreferrer\">《Redis 核心原理与实践》</a>：主要是结合源码来分析 Redis 的重要知识点比如各种数据结构和高级特性。</li>\n</ul>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/redis-books.png\" alt=\"《Redis 设计与实现》和《Redis 设计与实现》\"></p>\n<p>另外，<a href=\"https://book.douban.com/subject/26971561/\" target=\"_blank\" rel=\"noopener noreferrer\">《Redis 开发与运维》</a> 这本书也非常不错，既有基础介绍，又有一线开发运维经验分享。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/redis-kaifa-yu-yunwei.png\" alt=\"《Redis 开发与运维》\"></p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/books/up-e113c726a41874ef5fb19f7ac14e38e16ce.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "计算机书籍"
      ]
    },
    {
      "title": "分布式必读经典书籍",
      "url": "https://codingmap.strova.top/books/distributed-system.html",
      "id": "https://codingmap.strova.top/books/distributed-system.html",
      "summary": "分布式系统书籍推荐，DDIA、分布式事务、共识算法、微服务架构等经典书籍，掌握分布式系统设计核心知识。重点围绕 《深入理解分布式系统》、《数据密集型应用系统设计》、《深入理解分布式事务》、《从 Paxos 到 Zookeeper》 等内容展开。结合 CodingMap 知识体系梳理 技术书籍 / Distribute。",
      "content_html": "<h2>《深入理解分布式系统》</h2>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/deep-understanding-of-distributed-system.png\" alt></p>\n<p><strong><a href=\"https://book.douban.com/subject/35794814/\" target=\"_blank\" rel=\"noopener noreferrer\">《深入理解分布式系统》</a></strong> 是 2022 年出版的一本分布式中文原创书籍，主要讲的是分布式领域的基本概念、常见挑战以及共识算法。</p>\n<p>作者用了大量篇幅来介绍分布式领域中非常重要的共识算法，并且还会基于 Go 语言带着你从零实现了一个共识算法的鼻祖 Paxos 算法。</p>\n<p>实话说，我还没有开始看这本书。但是！这本书的作者的博客上的分布式相关的文章我几乎每一篇都认真看过。作者从 2019 年开始构思《深入理解分布式系统》，2020 年开始动笔，花了接近两年的时间才最终交稿。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220706121952258.png\" alt></p>\n<p>作者专门写了一篇文章来介绍这本书的背后的故事，感兴趣的小伙伴可以自行查阅：<a href=\"https://zhuanlan.zhihu.com/p/487534882\" target=\"_blank\" rel=\"noopener noreferrer\">https://zhuanlan.zhihu.com/p/487534882</a> 。</p>\n<p>最后，放上这本书的代码仓库和勘误地址：<a href=\"https://github.com/tangwz/DistSysDeepDive\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/tangwz/DistSysDeepDive</a> 。</p>\n<h2>《数据密集型应用系统设计》</h2>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/ddia.png\" alt></p>\n<p>强推一波 <strong><a href=\"https://book.douban.com/subject/30329536/\" target=\"_blank\" rel=\"noopener noreferrer\">《Designing Data-Intensive Application》</a></strong> （DDIA，数据密集型应用系统设计），值得读很多遍！豆瓣有接近 90% 的人看了这本书之后给了五星好评。</p>\n<p>这本书主要讲了分布式数据库、数据分区、事务、分布式系统等内容。</p>\n<p>书中介绍的大部分概念你可能之前都听过，但是在看了书中的内容之后，你可能会豁然开朗：“哇塞！原来是这样的啊！这不是某技术的原理么？”。</p>\n<p>这本书我之前专门写过知乎回答介绍和推荐，没看过的朋友可以看看：<a href=\"https://www.zhihu.com/question/50408698/answer/2278198495\" target=\"_blank\" rel=\"noopener noreferrer\">有哪些你看了以后大呼过瘾的编程书？</a> 。另外，如果你在阅读这本书的时候感觉难度比较大，很多地方读不懂的话，我这里推荐一下《深入理解分布式系统》作者写的<a href=\"https://ddia.qtmuniao.com\" target=\"_blank\" rel=\"noopener noreferrer\">《DDIA 逐章精读》小册</a>。</p>\n<h2>《深入理解分布式事务》</h2>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/In-depth-understanding-of-distributed-transactions-xiaoyu.png\" alt></p>\n<p><strong><a href=\"https://book.douban.com/subject/35626925/\" target=\"_blank\" rel=\"noopener noreferrer\">《深入理解分布式事务》</a></strong> 这本书的其中一位作者是 Apache ShenYu（incubating）网关创始人、Hmily、RainCat、Myth 等分布式事务框架的创始人。</p>\n<p>学习分布式事务的时候，可以参考一下这本书。虽有一些小错误以及逻辑不通顺的地方，但对于各种分布式事务解决方案的介绍，总体来说还是不错的。</p>\n<h2>《从 Paxos 到 Zookeeper》</h2>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20211216161350118.png\" alt></p>\n<p><strong><a href=\"https://book.douban.com/subject/26292004/\" target=\"_blank\" rel=\"noopener noreferrer\">《从 Paxos 到 Zookeeper》</a></strong> 是一本带你入门分布式理论的好书。这本书主要介绍几种典型的分布式一致性协议，以及解决分布式一致性问题的思路，其中重点讲解了 Paxos 和 ZAB 协议。</p>\n<p>PS：Zookeeper 现在用的不多，可以不用重点学习，但 Paxos 和 ZAB 协议还是非常值得深入研究的。</p>\n<h2>《深入理解分布式共识算法》</h2>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/deep-dive-into-distributed-consensus-algorithms.png\" alt></p>\n<p><strong><a href=\"https://book.douban.com/subject/36335459/\" target=\"_blank\" rel=\"noopener noreferrer\">《深入理解分布式共识算法》</a></strong> 详细剖析了 Paxos、Raft、Zab 等主流分布式共识算法的核心原理和实现细节。如果你想要了解分布式共识算法的话，不妨参考一下这本书的总结。</p>\n<h2>《微服务架构设计模式》</h2>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/microservices-patterns.png\" alt></p>\n<p><strong><a href=\"https://book.douban.com/subject/33425123/\" target=\"_blank\" rel=\"noopener noreferrer\">《微服务架构设计模式》</a></strong> 的作者 Chris Richardson 被评为世界十大软件架构师之一、微服务架构先驱。这本书汇集了 44 个经过实践验证的架构设计模式，这些模式用来解决诸如服务拆分、事务管理、查询和跨服务通信等难题。书中的内容不仅理论扎实，还通过丰富的 Java 代码示例，引导读者一步步掌握开发和部署生产级别的微服务架构应用。</p>\n<h2>《凤凰架构》</h2>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/f5bec14d3b404ac4b041d723153658b5.png\" alt></p>\n<p><strong><a href=\"https://book.douban.com/subject/35492898/\" target=\"_blank\" rel=\"noopener noreferrer\">《凤凰架构》</a></strong> 这本书是周志明老师多年架构和研发经验的总结，内容非常干货，深度与广度并存，理论结合实践！</p>\n<p>正如书名的副标题“构建可靠的大型分布式系统”所说的那样，这本书的主要内容就是讲：“如何构建一套可靠的分布式大型软件系统” ，涵盖了下面这些方面的内容：</p>\n<ul>\n<li>软件架构从单体到微服务再到无服务的演进之路。</li>\n<li>架构师应该在架构设计时应该注意哪些问题，有哪些比较好的实践。</li>\n<li>分布式的基石比如常见的分布式共识算法 Paxos、Multi Paxos。</li>\n<li>不可变基础设施比如虚拟化容器、服务网格。</li>\n<li>向微服务迈进的避坑指南。</li>\n</ul>\n<p>这本书我推荐过很多次了。详见历史文章：</p>\n<ul>\n<li><a href=\"https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&amp;mid=2247505254&amp;idx=1&amp;sn=04faf3093d6002354f06fffbfc2954e0&amp;chksm=cea19aadf9d613bbba7ed0e02ccc4a9ef3a30f4d83530e7ad319c2cc69cd1770e43d1d470046&amp;scene=178&amp;cur_album_id=1646812382221926401#rd\" target=\"_blank\" rel=\"noopener noreferrer\">周志明老师的又一神书！发现宝藏！</a></li>\n<li><a href=\"https://mp.weixin.qq.com/s/9nbzfZGAWM9_qIMp1r6uUQ\" target=\"_blank\" rel=\"noopener noreferrer\">Java 领域的又一神书！周志明老师 YYDS！</a></li>\n</ul>\n<h2>其他</h2>\n<ul>\n<li><a href=\"https://book.douban.com/subject/21624776/\" target=\"_blank\" rel=\"noopener noreferrer\">《分布式系统 : 概念与设计》</a>：偏教材类型，内容全而无趣，可作为参考书籍；</li>\n<li><a href=\"https://book.douban.com/subject/35689350/\" target=\"_blank\" rel=\"noopener noreferrer\">《分布式架构原理与实践》</a>：2021 年出版的，没什么热度，我也还没看过。</li>\n</ul>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/books/deep-understanding-of-distributed-system.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "计算机书籍"
      ]
    },
    {
      "title": "Java 必读经典书籍",
      "url": "https://codingmap.strova.top/books/java.html",
      "id": "https://codingmap.strova.top/books/java.html",
      "summary": "Java程序员必读书籍推荐，Java基础、并发编程、JVM虚拟机、Spring/SpringBoot框架、Netty网络编程、性能调优等经典书籍精选。重点围绕 Java 基础、Java 并发、JVM、常用工具 等内容展开。结合 CodingMap 知识体系梳理 技术书籍 / Java.Html 的核心概念、实践方法等。",
      "content_html": "<h2>Java 基础</h2>\n<p><strong><a href=\"https://book.douban.com/subject/2000732/\" target=\"_blank\" rel=\"noopener noreferrer\">《Head First Java》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424103035793.png\" alt=\"《Head First Java》-豆瓣\"></p>\n<p>《Head First Java》这本书的内容很轻松有趣，可以说是我学习编程初期最喜欢的几本书之一了。同时，这本书也是我的 Java 启蒙书籍。我在学习 Java 的初期多亏了这本书的帮助，自己才算是跨进 Java 语言的大门。</p>\n<p>我觉得我在 Java 这块能够坚持下来，这本书有很大的功劳。我身边的很多朋友学习 Java 初期都是看的这本书。</p>\n<p>有很多小伙伴就会问了：<strong>这本书适不适合编程新手阅读呢？</strong></p>\n<p>我个人觉得这本书还是挺适合编程新手阅读的，毕竟是 “Head First” 系列。</p>\n<p><strong><a href=\"https://book.douban.com/subject/34898994/\" target=\"_blank\" rel=\"noopener noreferrer\">《Java 核心技术卷 1 + 卷 2》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424101217849.png\" alt=\"《Java 核心技术卷 1》-豆瓣\"></p>\n<p>这两本书也非常不错。不过，这两本书的内容很多，全看的话比较费时间。我现在是把这两本书当做工具书来用，就比如我平时写文章的时候，碰到一些 Java 基础方面的问题，经常就翻看这两本来当做参考！</p>\n<p>我当时在大学的时候就买了两本放在寝室，没事的时候就翻翻。建议有点 Java 基础之后再读，介绍的还是比较深入和全面的，非常推荐。</p>\n<p><strong><a href=\"https://book.douban.com/subject/2130190/\" target=\"_blank\" rel=\"noopener noreferrer\">《Java 编程思想》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424103124893.png\" alt=\"《Java 编程思想》-豆瓣\"></p>\n<p>另外，这本书的作者去年新出版了<a href=\"https://book.douban.com/subject/35751619/\" target=\"_blank\" rel=\"noopener noreferrer\">《On Java》</a>，我更推荐这本，内容更新，介绍了 Java 的 3 个长期支持版（Java 8、11、17）。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/on-java/6171657600353_.pic_hd.jpg\" alt></p>\n<p>毕竟，这是市面上目前唯一一本介绍了 Java 的 3 个长期支持版（Java 8、11、17）的技术书籍。</p>\n<p><strong><a href=\"https://book.douban.com/subject/26772632/\" target=\"_blank\" rel=\"noopener noreferrer\">《Java 8 实战》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424103202625.png\" alt=\"《Java 8实战》-豆瓣\"></p>\n<p>Java 8 算是一个里程碑式的版本，现在一般企业还是用 Java 8 比较多。掌握 Java 8 的一些新特性比如 Lambda、Stream API 还是挺有必要的。这块的话，我推荐 <strong><a href=\"https://book.douban.com/subject/26772632/\" target=\"_blank\" rel=\"noopener noreferrer\">《Java 8 实战》</a></strong> 这本书。</p>\n<p><strong><a href=\"https://book.douban.com/subject/30133440/\" target=\"_blank\" rel=\"noopener noreferrer\">《Java 编程的逻辑》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20230721153650488.png\" alt=\"《Java编程的逻辑》\"></p>\n<p>一本非常低调的好书，相比于入门书来说，内容更有深度。适合初学者，同时也适合大家拿来复习 Java 基础知识。</p>\n<h2>Java 并发</h2>\n<p><strong><a href=\"https://book.douban.com/subject/30351286/\" target=\"_blank\" rel=\"noopener noreferrer\">《Java 并发编程之美》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424112413660.png\" alt=\"《Java 并发编程之美》-豆瓣\"></p>\n<p>这本书还是非常适合我们用来学习 Java 多线程的，讲解非常通俗易懂，作者从并发编程基础到实战都是信手拈来。</p>\n<p>另外，这本书的作者加多自身也会经常在网上发布各种技术文章。这本书也是加多大佬这么多年在多线程领域的沉淀所得的结果吧！他书中的内容基本都是结合代码讲解，非常有说服力！</p>\n<p><strong><a href=\"https://book.douban.com/subject/30358019/\" target=\"_blank\" rel=\"noopener noreferrer\">《实战 Java 高并发程序设计》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424112554830.png\" alt=\"《实战 Java 高并发程序设计》-豆瓣\"></p>\n<p>这个是我第二本要推荐的书籍，比较适合作为多线程入门/进阶书籍来看。这本书内容同样是理论结合实战，对于每个知识点的讲解也比较通俗易懂，整体结构也比较清。</p>\n<p><strong><a href=\"https://github.com/RedSpider1/concurrent\" target=\"_blank\" rel=\"noopener noreferrer\">《深入浅出 Java 多线程》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424112927759.png\" alt=\"《深入浅出 Java 多线程》在线阅读\"></p>\n<p>这本开源书籍是几位大厂的大佬开源的。这几位作者为了写好《深入浅出 Java 多线程》这本书阅读了大量的 Java 多线程方面的书籍和博客，然后再加上他们的经验总结、Demo 实例、源码解析，最终才形成了这本书。</p>\n<p>这本书的质量也是非常过硬！给作者们点个赞！这本书有统一的排版规则和语言风格、清晰的表达方式和逻辑。并且每篇文章初稿写完后，作者们就会互相审校，合并到主分支时所有成员会再次审校，最后再通篇修订了三遍。</p>\n<p>在线阅读：<a href=\"https://redspider.gitbook.io/concurrent/\" target=\"_blank\" rel=\"noopener noreferrer\">https://redspider.gitbook.io/concurrent/</a>。</p>\n<p><strong><a href=\"https://book.douban.com/subject/35013531/\" target=\"_blank\" rel=\"noopener noreferrer\">《Java 并发实现原理：JDK 源码剖析》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/0b1b046af81f4c94a03e292e66dd6f7d.png\" alt=\"《Java 并发实现原理：JDK 源码剖析》-豆瓣\"></p>\n<p>这本书主要是对 Java Concurrent 包中一些比较重要的源码进行了讲解，另外，像 JMM、happen-before、CAS 等等比较重要的并发知识这本书也都会一并介绍到。</p>\n<p>不论是你想要深入研究 Java 并发，还是说要准备面试，你都可以看看这本书。</p>\n<h2>JVM</h2>\n<p><strong><a href=\"https://book.douban.com/subject/34907497/\" target=\"_blank\" rel=\"noopener noreferrer\">《深入理解 Java 虚拟机》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/20210710104655705.png\" alt=\"《深入理解 Java 虚拟机》-豆瓣\"></p>\n<p>这本书就一句话形容：<strong>国产书籍中的战斗机，实实在在的优秀！</strong> （真心希望国内能有更多这样的优质书籍出现！加油！💪）</p>\n<p>这本书的第 3 版 2019 年底已经出来了，新增了很多实在的内容比如 ZGC 等新一代 GC 的原理剖析。目前豆瓣上是 9.5 的高分，🐂 不 🐂 我就不多说了！</p>\n<p>不论是你面试还是你想要在 Java 领域学习的更深，你都离不开这本书籍。这本书不光要看，你还要多看几遍，里面都是干货。这本书里面还有一些需要自己实践的东西，我建议你也跟着实践一下。</p>\n<p>类似的书籍还有 <strong><a href=\"https://book.douban.com/subject/26354292/\" target=\"_blank\" rel=\"noopener noreferrer\">《实战 Java 虚拟机》</a></strong>、<strong><a href=\"https://book.douban.com/subject/34935105/\" target=\"_blank\" rel=\"noopener noreferrer\">《虚拟机设计与实现:以 JVM 为例》</a></strong> ，这两本都是非常不错的！</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424113158144.png\" alt=\"《实战 Java 虚拟机》-豆瓣\"></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424113210153.png\" alt=\"《虚拟机设计与实现:以 JVM 为例》-豆瓣\"></p>\n<p>如果你对实战比较感兴趣，想要自己动手写一个简易的 JVM 的话，可以看看 <strong><a href=\"https://book.douban.com/subject/26802084/\" target=\"_blank\" rel=\"noopener noreferrer\">《自己动手写 Java 虚拟机》</a></strong> 这本书。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424113445246.png\" alt=\"《自己动手写 Java 虚拟机》-豆瓣\"></p>\n<p>书中的代码是基于 Go 语言实现的，搞懂了原理之后，你可以使用 Java 语言模仿着写一个，也算是练练手！ 如果你当前没有能力独立使用 Java 语言模仿着写一个的话，你也可以在网上找到很多基于 Java 语言版本的实现，比如<a href=\"https://zachaxy.github.io/tags/JVM/\" target=\"_blank\" rel=\"noopener noreferrer\">《zachaxy 的手写 JVM 系列》</a> 。</p>\n<p>这本书目前在豆瓣有 8.2 的评分，我个人觉得张秀宏老师写的挺好的，这本书值得更高的评分。</p>\n<p>另外，R 大在豆瓣发的<a href=\"https://www.douban.com/doulist/2545443/\" target=\"_blank\" rel=\"noopener noreferrer\">《从表到里学习 JVM 实现》</a>这篇文章中也推荐了很多不错的 JVM 相关的书籍，推荐小伙伴们去看看。</p>\n<h2>常用工具</h2>\n<p>非常重要！非常重要！特别是 Git 和 Docker。</p>\n<ul>\n<li><strong>IDEA</strong>：熟悉基本操作以及常用快捷。相关资料： <a href=\"https://github.com/judasn/IntelliJ-IDEA-Tutorial\" target=\"_blank\" rel=\"noopener noreferrer\">《IntelliJ IDEA 简体中文专题教程》</a> 。</li>\n<li><strong>Maven</strong>：强烈建议学习常用框架之前可以提前花几天时间学习一下<strong>Maven</strong>的使用。（到处找 Jar 包，下载 Jar 包是真的麻烦费事，使用 Maven 可以为你省很多事情）。相关阅读：<a href=\"https://javaguide.cn/tools/maven/maven-core-concepts.html\" target=\"_blank\" rel=\"noopener noreferrer\">Maven 核心概念总结</a>。</li>\n<li><strong>Git</strong>：基本的 Git 技能也是必备的，试着在学习的过程中将自己的代码托管在 Github 上。相关阅读：<a href=\"https://javaguide.cn/tools/git/git-intro.html\" target=\"_blank\" rel=\"noopener noreferrer\">Git 核心概念总结</a>。</li>\n<li><strong>Docker</strong>：学着用 Docker 安装学习中需要用到的软件比如 MySQL ,这样方便很多，可以为你节省不少时间。相关资料：<a href=\"https://yeasy.gitbook.io/docker_practice/\" target=\"_blank\" rel=\"noopener noreferrer\">《Docker - 从入门到实践》</a> 。</li>\n</ul>\n<p>除了这些工具之外，我强烈建议你一定要搞懂 GitHub 的使用。一些使用 GitHub 的小技巧，你可以看<a href=\"https://javaguide.cn/tools/git/github-tips.html\" target=\"_blank\" rel=\"noopener noreferrer\">Github 实用小技巧总结</a>这篇文章。</p>\n<h2>常用框架</h2>\n<p>框架部分建议找官方文档或者博客来看。</p>\n<h3>Spring/SpringBoot</h3>\n<p><strong>Spring 和 SpringBoot 真的很重要！</strong></p>\n<p>一定要搞懂 AOP 和 IOC 这两个概念。Spring 中 bean 的作用域与生命周期、SpringMVC 工作原理详解等等知识点都是非常重要的，一定要搞懂。</p>\n<p>企业中做 Java 后端，你一定离不开 SpringBoot ，这个是必备的技能了！一定一定一定要学好！</p>\n<p>像 SpringBoot 和一些常见技术的整合你也要知识怎么做，比如 SpringBoot 整合 MyBatis、 ElasticSearch、SpringSecurity、Redis 等等。</p>\n<p>下面是一些比较推荐的书籍/专栏。</p>\n<p><strong><a href=\"https://book.douban.com/subject/34949443/\" target=\"_blank\" rel=\"noopener noreferrer\">《Spring 实战》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424113512453.png\" alt=\"《Spring 实战》-豆瓣\"></p>\n<p>不建议当做入门书籍读，入门的话可以找点国人的书或者视频看。这本定位就相当于是关于 Spring 的一个概览，只有一些基本概念的介绍和示例，涵盖了 Spring 的各个方面，但都不够深入。就像作者在最后一页写的那样：“学习 Spring，这才刚刚开始”。</p>\n<p><strong><a href=\"https://book.douban.com/subject/30452637/\" target=\"_blank\" rel=\"noopener noreferrer\">《Spring 5 高级编程》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/20210328171223638.png\" alt></p>\n<p>对于 Spring5 的新特性介绍的比较详细，也说不上好。另外，感觉全书翻译的有一点蹩脚的味道，还有一点枯燥。全书的内容比较多，我一般拿来当做工具书参考。</p>\n<p><strong><a href=\"https://book.douban.com/subject/33390560/\" target=\"_blank\" rel=\"noopener noreferrer\">《Spring Boot 编程思想（核心篇）》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424113546513.png\" alt=\"《Spring Boot 编程思想（核心篇）》-豆瓣\"></p>\n<p><em>稍微有点啰嗦，但是原理介绍的比较清楚。</em></p>\n<p>SpringBoot 解析，不适合初学者。我是去年入手的，现在就看了几章，后面没看下去。书很厚，感觉很多很多知识点的讲解过于啰嗦和拖沓，不过，这本书对于 SpringBoot 内部原理讲解的还是很清楚。</p>\n<p><strong><a href=\"https://book.douban.com/subject/26857423/\" target=\"_blank\" rel=\"noopener noreferrer\">《Spring Boot 实战》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424113614768.png\" alt=\"《Spring Boot 实战》-豆瓣\"></p>\n<p>比较一般的一本书，可以简单拿来看一下。</p>\n<h3>MyBatis</h3>\n<p>MyBatis 国内用的挺多的，我的建议是不需要花太多时间在上面。当然了，MyBatis 的源码还是非常值得学习的，里面有很多不错的编码实践。这里推荐两本讲解 MyBatis 源码的书籍。</p>\n<p><strong><a href=\"https://book.douban.com/subject/36243250/\" target=\"_blank\" rel=\"noopener noreferrer\">《手写 MyBatis：渐进式源码实践》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20230724123402784.png\" alt=\"《手写MyBatis：渐进式源码实践》\"></p>\n<p>我的好朋友小傅哥出版的一本书。这本书以实践为核心，摒弃 MyBatis 源码中繁杂的内容，聚焦于 MyBaits 中的核心逻辑，简化代码实现过程，以渐进式的开发方式，逐步实现 MyBaits 中的核心功能。</p>\n<p>这本书的配套项目的仓库地址：<a href=\"https://github.com/fuzhengwei/small-mybatis\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/fuzhengwei/small-mybatis</a> 。</p>\n<p><strong><a href=\"https://book.douban.com/subject/35138963/\" target=\"_blank\" rel=\"noopener noreferrer\">《通用源码阅读指导书――MyBatis 源码详解》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20230724123416741.png\" alt=\"《通用源码阅读指导书――MyBatis源码详解》\"></p>\n<p>这本书通过 MyBatis 开源代码讲解源码阅读的流程和方法！一共对 MyBatis 源码中的 300 多个类进行了详细解析，包括其背景知识、组织方式、逻辑结构、实现细节。</p>\n<p>这本书的配套示例仓库地址：<a href=\"https://github.com/yeecode/MyBatisDemo\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/yeecode/MyBatisDemo</a> 。</p>\n<h3>Netty</h3>\n<p><strong><a href=\"https://book.douban.com/subject/27038538/\" target=\"_blank\" rel=\"noopener noreferrer\">《Netty 实战》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424113715369.png\" alt=\"《Netty 实战》-豆瓣\"></p>\n<p>这本书可以用来入门 Netty ，内容从 BIO 聊到了 NIO、之后才详细介绍为什么有 Netty、Netty 为什么好用以及 Netty 重要的知识点讲解。</p>\n<p>这本书基本把 Netty 一些重要的知识点都介绍到了，而且基本都是通过实战的形式讲解。</p>\n<p><strong><a href=\"https://book.douban.com/subject/30381214/\" target=\"_blank\" rel=\"noopener noreferrer\">《Netty 进阶之路：跟着案例学 Netty》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424113747345.png\" alt=\"《Netty 进阶之路：跟着案例学 Netty》-豆瓣\"></p>\n<p>内容都是关于使用 Netty 的实践案例比如内存泄露这些东西。如果你觉得你的 Netty 已经完全入门了，并且你想要对 Netty 掌握的更深的话，推荐你看一下这本书。</p>\n<p><strong><a href=\"https://book.douban.com/subject/35752082/\" target=\"_blank\" rel=\"noopener noreferrer\">《跟闪电侠学 Netty：Netty 即时聊天实战与底层原理》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/open-source-project/image-20220503085034268.png\" alt></p>\n<p>2022 年 3 月出版的一本书。这本书分为上下两篇，上篇通过一个即时聊天系统的实战案例带你入门 Netty，下篇通过 Netty 源码分析带你搞清 Netty 比较重要的底层原理。</p>\n<h2>性能调优</h2>\n<p><strong><a href=\"https://book.douban.com/subject/26740520/\" target=\"_blank\" rel=\"noopener noreferrer\">《Java 性能权威指南》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/image-20220424113809644.png\" alt=\"《Java 性能权威指南》-豆瓣\"></p>\n<p><em>希望能有更多这 Java 性能优化方面的好书！</em></p>\n<p>O'Reilly 家族书，性能调优的入门书，我个人觉得性能调优是每个 Java 从业者必备知识。</p>\n<p>这本书介绍的实战内容很不错，尤其是 JVM 调优，缺点也比较明显，就是内容稍微有点老。市面上这种书很少。这本书不适合初学者，建议对 Java 语言已经比价掌握了再看。另外，阅读之前，最好先看看周志明大佬的《深入理解 Java 虚拟机》。</p>\n<h2>网站架构</h2>\n<p>看过很多网站架构方面的书籍，比如《大型网站技术架构：核心原理与案例分析》、《亿级流量网站架构核心技术》、《架构修炼之道——亿级网关、平台开放、分布式、微服务、容错等核心技术修炼实践》等等。</p>\n<p>目前我觉得能推荐的只有李运华老师的 <strong><a href=\"https://book.douban.com/subject/30335935/\" target=\"_blank\" rel=\"noopener noreferrer\">《从零开始学架构》</a></strong> 和 余春龙老师的 <strong><a href=\"https://book.douban.com/subject/30443578/\" title=\"《软件架构设计：大型网站技术架构与业务架构融合之道》\" target=\"_blank\" rel=\"noopener noreferrer\">《软件架构设计：大型网站技术架构与业务架构融合之道》</a></strong> 。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/20210412224443177.png\" alt></p>\n<p>《从零开始学架构》这本书对应的有一个极客时间的专栏—《从零开始学架构》，里面的很多内容都是这个专栏里面的，两者买其一就可以了。我看了很小一部分，内容挺全面的，是一本真正在讲如何做架构的书籍。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/20210412232441459.png\" alt></p>\n<p>事务与锁、分布式（CAP、分布式事务……）、高并发、高可用 《软件架构设计：大型网站技术架构与业务架构融合之道》 这本书都有介绍到。</p>\n<h2>面试</h2>\n<p><strong>《JavaGuide 面试突击版》</strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide-mianshituji/image-20220830103023493.png\" alt></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide-mianshituji/image-20220830102925775.png\" alt></p>\n<p><a href=\"https://javaguide.cn/\" target=\"_blank\" rel=\"noopener noreferrer\">JavaGuide</a> 的面试版本，涵盖了 Java 后端方面的大部分知识点比如 集合、JVM、多线程还有数据库 MySQL 等内容。</p>\n<p>公众号后台回复：“<strong>面试突击</strong>” 即可免费获取，无任何套路。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png\" alt=\"JavaGuide 官方公众号\"></p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/books/image-20220424103035793.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "计算机书籍"
      ]
    },
    {
      "title": "搜索引擎必读经典书籍",
      "url": "https://codingmap.strova.top/books/search-engine.html",
      "id": "https://codingmap.strova.top/books/search-engine.html",
      "summary": "搜索引擎书籍推荐，Lucene入门、Elasticsearch核心技术与实战、源码解析与优化实战等经典书籍精选。重点围绕 Lucene、Elasticsearch 等内容展开。结合 CodingMap 知识体系梳理 技术书籍 / Search Engine.Html 的核心概念、实践方法、常见问题和高频面试考点等核心。",
      "content_html": "<h2>Lucene</h2>\n<p>Elasticsearch 在 Apache Lucene 的基础上开发而成，学习 ES 之前，建议简单了解一下 Lucene 的相关概念。</p>\n<p><strong><a href=\"https://book.douban.com/subject/6440615/\" target=\"_blank\" rel=\"noopener noreferrer\">《Lucene 实战》</a></strong> 是国内为数不多的中文版本讲 Lucene 的书籍，适合用来学习和了解 Lucene 相关的概念和常见操作。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/vAJkdYEyol4e6Nr.png\" alt=\"《Lucene实战》-实战\"></p>\n<h2>Elasticsearch</h2>\n<p><strong><a href=\"https://book.douban.com/subject/36716996/\" target=\"_blank\" rel=\"noopener noreferrer\">《一本书讲透 Elasticsearch：原理、进阶与工程实践》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/one-book-guide-to-elasticsearch.png\" alt></p>\n<p>基于 8.x 版本编写，目前全网最新的 Elasticsearch 讲解书籍。内容覆盖 Elastic 官方认证的核心知识点，源自真实项目案例和企业级问题解答。</p>\n<p><strong><a href=\"http://gk.link/a/10bcT\" title=\"《Elasticsearch 核心技术与实战》\" target=\"_blank\" rel=\"noopener noreferrer\">《Elasticsearch 核心技术与实战》</a></strong></p>\n<p>极客时间的这门课程基于 Elasticsearch 7.1 版本讲解，还算比较新。并且，作者是 eBay 资深技术专家，有 20 年的行业经验，课程质量有保障！</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/csdn/20210420231125225.png\" alt=\"《Elasticsearch 核心技术与实战》-极客时间\"></p>\n<p><strong><a href=\"https://book.douban.com/subject/30386800/\" target=\"_blank\" rel=\"noopener noreferrer\">《Elasticsearch 源码解析与优化实战》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/p3-juejin/f856485931a945639d5c23aaed74fb38~tplv-k3u1fbpfcp-zoom-1.png\" alt=\"《Elasticsearch 源码解析与优化实战》-豆瓣\"></p>\n<p>如果你想进一步深入研究 Elasticsearch 原理的话，可以看看张超老师的这本书。这是市面上唯一一本写 Elasticsearch 源码的书。</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/books/vAJkdYEyol4e6Nr.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "计算机书籍"
      ]
    },
    {
      "title": "软件质量必读经典书籍",
      "url": "https://codingmap.strova.top/books/software-quality.html",
      "id": "https://codingmap.strova.top/books/software-quality.html",
      "summary": "软件质量与代码整洁书籍推荐，重构、Clean Code、Effective Java、架构整洁之道等经典书籍，提升代码质量和架构设计能力。重点围绕 代码整洁之道、程序员职业素养、架构整洁之道、项目管理 等内容展开。结合 CodingMap 知识体系梳理 技术书籍 / Software Quality.Html 的核心。",
      "content_html": "<p>下面推荐都是我看过并且我觉得值得推荐的书籍。</p>\n<p>不过，这些书籍都比较偏理论，只能帮助你建立一个写优秀代码的意识标准。 如果你想要编写更高质量的代码、更高质量的软件，还是应该多去看优秀的源码，多去学习优秀的代码实践。</p>\n<h2>代码整洁之道</h2>\n<p><strong><a href=\"https://book.douban.com/subject/30468597/\" target=\"_blank\" rel=\"noopener noreferrer\">《重构》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/20210328174841577.png\" alt></p>\n<p>必看书籍！无需多言。编程书籍领域的瑰宝。</p>\n<p>世界顶级、国宝级别的 Martin Fowler 的书籍，可以说是软件开发领域最经典的几本书之一。目前已经出了第二版。</p>\n<p>这是一本值得你看很多遍的书籍。</p>\n<p><strong><a href=\"https://book.douban.com/subject/4199741/\" target=\"_blank\" rel=\"noopener noreferrer\">《Clean Code》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/20210328174824891.png\" alt></p>\n<p>《Clean Code》是 Bob 大叔的一本经典著作，强烈建议小伙伴们一定要看看。</p>\n<p>Bob 大叔将自己对整洁代码的理解浓缩在了这本书中，真可谓是对后生的一大馈赠。</p>\n<p><strong><a href=\"https://book.douban.com/subject/30412517/\" target=\"_blank\" rel=\"noopener noreferrer\">《Effective Java 》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/82d510c951384383b325080428af6c0a.png\" alt></p>\n<p>《Effective Java 》这本书是 Java 领域国宝级别的书，非常经典。Java 程序员必看！</p>\n<p>这本书主要介绍了在 Java 编程中很多极具实用价值的经验规则，这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。这篇文章能够非常实际地帮助你写出更加清晰、健壮和高效的代码。本书中的每条规则都以简短、独立的小文章形式出现，并通过例子代码加以进一步说明。</p>\n<p><strong><a href=\"https://book.douban.com/subject/1477390/\" target=\"_blank\" rel=\"noopener noreferrer\">《代码大全》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/20210314173253221.png\" alt></p>\n<p>其实，《代码大全（第 2 版）》这本书我本身是不太想推荐给大家了。但是，看在它的豆瓣评分这么高的份上，还是拿出来说说吧！</p>\n<p>这也是一本非常经典的书籍，第二版对第一版进行了重写。</p>\n<p>我简单地浏览过全书的内容，感觉内容总体比较虚，对于大部分程序员的作用其实不大。如果你想要切实地提高自己的代码质量，《Clean Code》和 《编写可读代码的艺术》我觉得都要比《代码大全》这本书更好。</p>\n<p>不过，最重要的还是要多看优秀的源码，多学习优秀的代码实践。</p>\n<p><strong><a href=\"https://book.douban.com/subject/10797189/\" target=\"_blank\" rel=\"noopener noreferrer\">《编写可读代码的艺术》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/20210314175536443.png\" alt></p>\n<p>《编写可读代码的艺术》这本书要表达的意思和《Clean Code》很像，你看它俩的目录就可以看出来了。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70-20230309230739963.png\" alt></p>\n<p>在我看来，如果你看过 《Clean Code》 的话，就不需要再看这本书了。当然，如果你有时间和精力，也可以快速过一遍。</p>\n<p>另外，我这里还要推荐一个叫做 <strong><a href=\"https://github.com/biezhi/write-readable-code\" target=\"_blank\" rel=\"noopener noreferrer\">write-readable-code</a></strong> 的仓库。这个仓库的作者免费分享了一系列基于《编写可读代码的艺术》这本书的视频。这一系列视频会基于 Java 语言来教你如何优化咱们的代码。</p>\n<p>在实践中学习的效果肯定会更好！推荐小伙伴们都抓紧学起来啊！</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70-20230309230743258.png\" alt></p>\n<h2>程序员职业素养</h2>\n<p><strong><a href=\"https://book.douban.com/subject/26919457/\" target=\"_blank\" rel=\"noopener noreferrer\">《The Clean Coder》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/20210314191210273.png\" alt></p>\n<p>《 The Clean Coder》是 Bob 大叔的又一经典著作。</p>\n<p>《Clean Code》和《 The Clean Coder》这两本书在国内都翻译为 《代码整洁之道》，我觉得这个翻译还是不够优雅的。</p>\n<p>另外，两者的内容差异也很大。《Clean Code》这本书从代码层面来讲解如何提高自己的代码质量。而《The Clean Coder》这本书则是从如何成为一名更优秀的开发者的角度来写的，比如这书会教你如何在自己的领域更专业、如何说不、如何做时间管理、如何处理压力等等。</p>\n<h2>架构整洁之道</h2>\n<p><strong><a href=\"https://book.douban.com/subject/30333919/\" target=\"_blank\" rel=\"noopener noreferrer\">《架构整洁之道》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/books/2021031412342771.png\" alt></p>\n<p>你没看错，《架构整洁之道》这本书又是 Bob 大叔的经典之作。</p>\n<p>这本书我强烈安利！认真读完之后，我保证你对编程本质、编程语言的本质、软件设计、架构设计可以有进一步的认识。</p>\n<p>国内的很多书籍和专栏都借鉴了《架构整洁之道》 这本书。毫不夸张地说，《架构整洁之道》就是架构领域最经典的书籍之一。</p>\n<p>正如作者说的那样：</p>\n<blockquote>\n<p>如果深入研究计算机编程的本质，我们就会发现这 50 年来，计算机编程基本没有什么大的变化。编程语言稍微进步了一点，工具的质量大大提升了，但是计算机程序的基本构造没有什么变化。</p>\n<p>虽然我们有了新的编程语言、新的编程框架、新的编程范式，但是软件架构的规则仍然和 1946 年阿兰·图灵写下第一行机器代码的时候一样。</p>\n<p>这本书就是为了把这些永恒不变的软件架构规则展现出来。</p>\n</blockquote>\n<h2>项目管理</h2>\n<p><strong><a href=\"https://book.douban.com/subject/1102259/\" target=\"_blank\" rel=\"noopener noreferrer\">《人月神话》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/2021/03/8ece325c-4491-4ffd-9d3d-77e95159ec40.png\" alt></p>\n<p>这本书主要描述了软件开发的基本定律：<strong>一个需要 10 天才能干完的活，不可能让 10 个人在 1 天干完！</strong></p>\n<p>看书名的第一眼，感觉不像是技术类的书籍。但是，就是这样一个看似和编程不沾边的书名，却成了编程领域长久相传的经典。</p>\n<p><strong>这本书对于现代软件尤其是复杂软件的开发的规范化有深刻的意义。</strong></p>\n<p><strong><a href=\"https://book.douban.com/subject/5344973/\" target=\"_blank\" rel=\"noopener noreferrer\">《领域驱动设计:软件核心复杂性应对之道》</a></strong></p>\n<p><img src=\"https://oss.javaguide.cn/2021/03/7e80418d-20b1-4066-b9af-cfe434b1bf1a.png\" alt></p>\n<p>这本领域驱动设计方面的经典之作一直被各种推荐，但是我还来及读。</p>\n<h2>其他</h2>\n<ul>\n<li><a href=\"https://book.douban.com/subject/24536403/\" target=\"_blank\" rel=\"noopener noreferrer\">《代码的未来》</a>：这本书的作者是 Ruby 之父松本行弘，算是一本年代比较久远的书籍（13 年出版），不过，还是非常值得一读。这本书的内容主要介绍是编程/编程语言的本质。我个人还是比较喜欢松本行弘的文字风格，并且，你看他的文章也确实能够有所收获。</li>\n<li><a href=\"https://book.douban.com/subject/1488876/\" target=\"_blank\" rel=\"noopener noreferrer\">《深入浅出设计模式》</a>：比较有趣的风格，适合设计模式入门。</li>\n<li><a href=\"https://book.douban.com/subject/30443578/\" target=\"_blank\" rel=\"noopener noreferrer\">《软件架构设计:大型网站技术架构与业务架构融合之道》</a>：内容非常全面。适合面试前突击一些比较重要的理论知识，也适合拿来扩充/完善自己的技术广度。</li>\n<li><a href=\"https://book.douban.com/subject/33425123/\" target=\"_blank\" rel=\"noopener noreferrer\">《微服务架构设计模式》</a>：这本书是世界十大软件架构师之一、微服务架构先驱 Chris Richardson 亲笔撰写，豆瓣评分 9.6。示例代码使用 Java 语言和 Spring 框架。帮助你设计、实现、测试和部署基于微服务的应用程序。</li>\n</ul>\n<p>最后再推荐两个相关的文档：</p>\n<ul>\n<li><strong>阿里巴巴 Java 开发手册</strong>：<a href=\"https://github.com/alibaba/p3c\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/alibaba/p3c</a></li>\n<li><strong>Google Java 编程风格指南</strong>：<a href=\"http://www.hawstein.com/posts/google-java-style.html\" target=\"_blank\" rel=\"noopener noreferrer\">http://www.hawstein.com/posts/google-java-style.html</a></li>\n</ul>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/books/20210328174841577.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "计算机书籍"
      ]
    },
    {
      "title": "计算机基础知识体系：计算机网络、操作系统、数据结构与算法",
      "url": "https://codingmap.strova.top/cs-basics/",
      "id": "https://codingmap.strova.top/cs-basics/",
      "summary": "计算机基础面试与学习路线，涵盖计算机网络、操作系统、数据结构、算法、Linux、TCP/IP、HTTP、DNS 等内容，适合校招和社招复习。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章 等内容展开。结合 CodingMap 知识体系梳理 计算机基础 的核心概念、实践方法、常见问题和高频面试考点，覆盖原理分析等。",
      "content_html": "<p>这份 <strong>计算机基础知识体系</strong> 面向后端学习和面试复习，按“计算机网络 -&gt; 操作系统 -&gt; 数据结构 -&gt; 算法”的顺序整理本站计算机基础相关文章。</p>\n<p>如果你时间有限，建议先看 <a href=\"/cs-basics/network/other-network-questions.html\" target=\"_blank\">计算机网络常见面试题总结</a> 和 <a href=\"/cs-basics/operating-system/operating-system-basic-questions-01.html\" target=\"_blank\">操作系统常见面试题总结</a>，快速建立高频问题清单；如果你想系统补基础，可以按下面的专题顺序推进。</p>\n<p>整站配有 <strong>300+ 张技术配图</strong>，用图解的方式把抽象概念讲清楚，不是干巴巴的文字堆砌。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/cs-basics/network/cs-basics-overview.png\" alt=\"计算机基础知识总结内容概览\"></p>\n<h2>适合谁看</h2>\n<ul>\n<li>正在系统补齐计算机基础的后端开发者。</li>\n<li>准备校招、社招、中大厂后端面试的同学。</li>\n<li>想把网络、操作系统、数据结构和算法串成完整知识体系的读者。</li>\n<li>已经写过业务代码，但对 TCP/IP、HTTP、进程线程、内存管理、树图、排序等基础不够扎实的工程师。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>计算机网络重点理解分层模型、TCP/UDP、HTTP/HTTPS、DNS、ARP、NAT 和常见网络安全问题。</li>\n<li>操作系统重点理解进程线程、内存管理、文件系统、Linux 基础和 Shell 使用。</li>\n<li>数据结构重点理解数组、链表、栈、队列、树、图、堆、红黑树和布隆过滤器的特点与适用场景。</li>\n<li>算法重点理解常见算法思想、复杂度分析、排序、字符串、链表和 LeetCode 高频题。</li>\n<li>面试中要能把“概念 -&gt; 原理 -&gt; 对比 -&gt; 场景 -&gt; 常见问题”串成完整回答。</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/cs-basics/network/\" target=\"_blank\">计算机网络专题</a>：先从分层模型、HTTP、TCP、DNS 和常见网络面试题入手，建立网络通信的整体认知。</li>\n<li><a href=\"/cs-basics/operating-system/\" target=\"_blank\">操作系统专题</a>：理解进程线程、内存、文件系统、Linux 和 Shell，为并发编程、JVM、数据库打基础。</li>\n<li><a href=\"/cs-basics/data-structure/\" target=\"_blank\">数据结构专题</a>：掌握线性表、树、图、堆、红黑树、布隆过滤器等常见结构。</li>\n<li><a href=\"/cs-basics/algorithms/\" target=\"_blank\">算法专题</a>：结合经典算法思想和 LeetCode 高频题进行练习。</li>\n<li>回到面试题做查缺补漏：重点复盘网络和操作系统高频问题，再把数据结构与算法题按类型刷一遍。</li>\n</ol>\n<h2>核心文章</h2>\n<h3>计算机网络</h3>\n<ul>\n<li><a href=\"/cs-basics/network/\" target=\"_blank\">计算机网络专题</a>：按协议层梳理计算机网络核心知识和面试高频题。</li>\n<li><a href=\"/cs-basics/network/other-network-questions.html\" target=\"_blank\">计算机网络常见面试题总结（上）</a>：覆盖 OSI/TCP-IP 模型、HTTP、HTTPS、DNS 等基础问题。</li>\n<li><a href=\"/cs-basics/network/other-network-questions2.html\" target=\"_blank\">计算机网络常见面试题总结（下）</a>：继续补充 TCP、UDP、网络安全、Socket 等常见问题。</li>\n<li><a href=\"/cs-basics/network/osi-and-tcp-ip-model.html\" target=\"_blank\">OSI 七层模型与 TCP/IP 四层模型详解</a>：建立网络分层和协议职责认知。</li>\n<li><a href=\"/cs-basics/network/the-whole-process-of-accessing-web-pages.html\" target=\"_blank\">从输入 URL 到页面展示到底发生了什么？</a>：用一次完整请求串联 DNS、TCP、HTTP、浏览器渲染等知识点。</li>\n<li><a href=\"/cs-basics/network/http-vs-https.html\" target=\"_blank\">HTTP vs HTTPS</a>、<a href=\"/cs-basics/network/http1.0-vs-http1.1.html\" target=\"_blank\">HTTP 1.0 vs HTTP 1.1</a>、<a href=\"/cs-basics/network/http-status-codes.html\" target=\"_blank\">HTTP 常见状态码总结</a>：集中理解 HTTP 相关高频考点。</li>\n<li><a href=\"/cs-basics/network/tcp-connection-and-disconnection.html\" target=\"_blank\">TCP 三次握手和四次挥手</a>、<a href=\"/cs-basics/network/tcp-reliability-guarantee.html\" target=\"_blank\">TCP 传输可靠性保障</a>：掌握 TCP 最核心的连接管理和可靠传输机制。</li>\n</ul>\n<h3>操作系统</h3>\n<ul>\n<li><a href=\"/cs-basics/operating-system/\" target=\"_blank\">操作系统专题</a>：从操作系统基础讲到 Linux 常见问题。</li>\n<li><a href=\"/cs-basics/operating-system/operating-system-basic-questions-01.html\" target=\"_blank\">操作系统常见面试题总结（上）</a>：覆盖操作系统基础、进程线程、死锁、内存管理等问题。</li>\n<li><a href=\"/cs-basics/operating-system/operating-system-basic-questions-02.html\" target=\"_blank\">操作系统常见面试题总结（下）</a>：继续整理文件系统、IO、Linux 等面试考点。</li>\n<li><a href=\"/cs-basics/operating-system/linux-intro.html\" target=\"_blank\">Linux 基础知识总结</a>：掌握 Linux 目录、文件权限、常用命令和系统基础。</li>\n<li><a href=\"/cs-basics/operating-system/shell-intro.html\" target=\"_blank\">Shell 编程基础知识总结</a>：补齐脚本编写、变量、流程控制和常用命令能力。</li>\n</ul>\n<h3>数据结构</h3>\n<ul>\n<li><a href=\"/cs-basics/data-structure/\" target=\"_blank\">数据结构专题</a>：按结构类型整理常见数据结构及图解。</li>\n<li><a href=\"/cs-basics/data-structure/linear-data-structure.html\" target=\"_blank\">线性数据结构详解</a>：理解数组、链表、栈、队列的存储特点和操作复杂度。</li>\n<li><a href=\"/cs-basics/data-structure/tree.html\" target=\"_blank\">树结构详解</a>：掌握二叉树、二叉搜索树、AVL、B 树、B+ 树等常见树结构。</li>\n<li><a href=\"/cs-basics/data-structure/graph.html\" target=\"_blank\">图详解</a>：理解图的表示、DFS、BFS 和最短路径等基础算法。</li>\n<li><a href=\"/cs-basics/data-structure/heap.html\" target=\"_blank\">堆详解</a>、<a href=\"/cs-basics/data-structure/red-black-tree.html\" target=\"_blank\">红黑树详解</a>、<a href=\"/cs-basics/data-structure/bloom-filter.html\" target=\"_blank\">布隆过滤器详解</a>：补齐高频工程结构和面试考点。</li>\n</ul>\n<h3>算法</h3>\n<ul>\n<li><a href=\"/cs-basics/algorithms/\" target=\"_blank\">算法专题</a>：整理常见算法思想、LeetCode 高频题和经典排序。</li>\n<li><a href=\"/cs-basics/algorithms/classical-algorithm-problems-recommendations.html\" target=\"_blank\">经典算法思想总结</a>：覆盖二分、双指针、滑动窗口、回溯、动态规划等常见思想。</li>\n<li><a href=\"/cs-basics/algorithms/common-data-structures-leetcode-recommendations.html\" target=\"_blank\">常见数据结构经典 LeetCode 题目推荐</a>：按数据结构类型整理刷题路线。</li>\n<li><a href=\"/cs-basics/algorithms/string-algorithm-problems.html\" target=\"_blank\">几道常见的字符串算法题</a>、<a href=\"/cs-basics/algorithms/linkedlist-algorithm-problems.html\" target=\"_blank\">几道常见的链表算法题</a>：集中练习高频题型。</li>\n<li><a href=\"/cs-basics/algorithms/the-sword-refers-to-offer.html\" target=\"_blank\">剑指 Offer 部分编程题</a>、<a href=\"/cs-basics/algorithms/10-classical-sorting-algorithms.html\" target=\"_blank\">十大经典排序算法总结</a>：适合面试前复盘。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>OSI 七层模型和 TCP/IP 四层模型分别是什么？每层解决什么问题？</li>\n<li>从输入 URL 到页面展示，中间经历了哪些步骤？</li>\n<li>HTTP 和 HTTPS 有什么区别？HTTPS 为什么更安全？</li>\n<li>TCP 三次握手、四次挥手分别解决什么问题？TIME_WAIT 为什么存在？</li>\n<li>TCP 如何保证可靠传输？TCP 和 UDP 如何选型？</li>\n<li>进程和线程有什么区别？什么是死锁，如何避免？</li>\n<li>操作系统内存管理、虚拟内存、分页和分段分别是什么？</li>\n<li>数组、链表、栈、队列、树、图、堆分别适合什么场景？</li>\n<li>红黑树、B+ 树、布隆过滤器在工程中常用在哪里？</li>\n<li>刷算法题时如何按题型建立解题模板？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/java/\" target=\"_blank\">Java 知识体系</a></li>\n<li><a href=\"/database/\" target=\"_blank\">数据库知识体系</a></li>\n<li><a href=\"/distributed-system/\" target=\"_blank\">分布式系统知识体系</a></li>\n<li><a href=\"/high-performance/\" target=\"_blank\">高性能系统知识体系</a></li>\n<li><a href=\"/system-design/\" target=\"_blank\">系统设计</a></li>\n<li><a href=\"/books/cs-basics.html\" target=\"_blank\">计算机基础书籍推荐</a></li>\n</ul>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/cs-basics/network/cs-basics-overview.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": []
    },
    {
      "title": "数据库知识体系：SQL、MySQL、Redis、MongoDB 与 Elasticsearch",
      "url": "https://codingmap.strova.top/database/",
      "id": "https://codingmap.strova.top/database/",
      "summary": "数据库面试与知识体系学习路线，涵盖 SQL、MySQL 索引、事务、日志、MVCC、执行计划、Redis 缓存、MongoDB 和 Elasticsearch。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章、数据库 等内容展开。结合 CodingMap 知识体系梳理 数据库 的核心概念、实践方法等核心内容。",
      "content_html": "<p>这份 <strong>数据库知识体系</strong> 面向后端学习、工程实践和面试复习，按“数据库基础 -&gt; SQL -&gt; MySQL -&gt; Redis -&gt; NoSQL 与搜索”的顺序整理本站数据库相关文章。</p>\n<p>如果你时间有限，建议先看 <a href=\"/database/basis.html\" target=\"_blank\">数据库基础常见面试题总结</a>、<a href=\"/database/sql/sql-syntax-summary.html\" target=\"_blank\">SQL 语法基础知识总结</a>、<a href=\"/database/mysql/mysql-questions-01.html\" target=\"_blank\">MySQL 常见面试题总结</a> 和 <a href=\"/database/redis/redis-questions-01.html\" target=\"_blank\">Redis 常见面试题总结（上）</a>，快速建立高频问题清单。</p>\n<h2>适合谁看</h2>\n<ul>\n<li>正在系统学习数据库基础、SQL、MySQL 和 Redis 的后端开发者。</li>\n<li>准备校招、社招、中大厂后端面试的同学。</li>\n<li>想补齐索引、事务、日志、执行计划、缓存一致性、Redis 持久化和集群能力的工程师。</li>\n<li>已经写过业务 CRUD，但对数据库底层原理、性能优化和 NoSQL 选型不够熟的读者。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>关系型数据库和 NoSQL 分别适合解决什么问题，常见选型边界在哪里？</li>\n<li>SQL 的查询、聚合、连接、子查询和事务语义应该如何掌握？</li>\n<li>MySQL 索引、事务隔离、MVCC、三大日志和执行计划如何串成一条主线？</li>\n<li>Redis 为什么快，常用数据结构、缓存策略、持久化、阻塞问题和集群机制如何理解？</li>\n<li>MongoDB、Elasticsearch 这类 NoSQL/搜索系统在后端面试和工程选型中常考哪些点？</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/database/basis.html\" target=\"_blank\">数据库基础常见面试题总结</a> 和 <a href=\"/database/nosql.html\" target=\"_blank\">NoSQL 基础常见面试题总结</a>：先理解数据库分类、事务、范式、NoSQL 类型和典型应用场景。</li>\n<li><a href=\"/database/sql/sql-syntax-summary.html\" target=\"_blank\">SQL 语法基础知识总结</a>：补齐查询、过滤、排序、聚合、连接、子查询、插入、更新和删除等 SQL 基本功。</li>\n<li><a href=\"/database/mysql/\" target=\"_blank\">MySQL 专题</a>：重点学习索引、事务隔离、MVCC、三大日志、执行过程和执行计划。</li>\n<li><a href=\"/database/redis/\" target=\"_blank\">Redis 专题</a>：重点学习缓存基础、数据结构、缓存读写策略、持久化、阻塞问题、内存碎片和集群。</li>\n<li><a href=\"/database/mongodb/\" target=\"_blank\">MongoDB 专题</a> 和 <a href=\"/database/elasticsearch/elasticsearch-questions-01.html\" target=\"_blank\">Elasticsearch 常见面试题总结</a>：根据岗位要求补充文档数据库和搜索引擎相关知识。</li>\n</ol>\n<h2>核心文章</h2>\n<h3>数据库基础与 SQL</h3>\n<p>这部分适合先建立数据库通用认知，重点理解数据库类型、事务语义、字符集、SQL 基础和常见查询题。</p>\n<ul>\n<li><a href=\"/database/basis.html\" target=\"_blank\">数据库基础常见面试题总结</a>：梳理数据库基础概念、事务特性、并发控制、范式和常见面试问题。</li>\n<li><a href=\"/database/nosql.html\" target=\"_blank\">NoSQL 基础常见面试题总结</a>：理解键值、文档、列族、图数据库等 NoSQL 类型及适用场景。</li>\n<li><a href=\"/database/character-set.html\" target=\"_blank\">字符集详解：字符集是什么？怎么用？</a>：理解字符集、编码、乱码原因以及 MySQL 字符集设置。</li>\n<li><a href=\"/database/sql/\" target=\"_blank\">SQL 专题</a>：从 SQL 语法基础讲到常见 SQL 面试题。</li>\n<li><a href=\"/database/sql/sql-syntax-summary.html\" target=\"_blank\">SQL 语法基础知识总结</a>：覆盖查询、过滤、排序、聚合、分组、连接、子查询和数据修改。</li>\n</ul>\n<h3>MySQL</h3>\n<p>MySQL 是后端开发最核心的关系型数据库考点之一，学习时建议把“索引 -&gt; 执行计划 -&gt; 事务 -&gt; MVCC -&gt; 日志 -&gt; 性能优化”连起来。</p>\n<ul>\n<li><a href=\"/database/mysql/\" target=\"_blank\">MySQL 专题</a>：串联 MySQL 索引、事务、MVCC、日志、执行计划和性能优化。</li>\n<li><a href=\"/database/mysql/mysql-questions-01.html\" target=\"_blank\">MySQL 常见面试题总结</a>：快速建立 MySQL 高频问题清单。</li>\n<li><a href=\"/database/mysql/mysql-index.html\" target=\"_blank\">MySQL 索引详解</a>：理解索引数据结构、最左前缀、覆盖索引、回表和索引设计原则。</li>\n<li><a href=\"/database/mysql/transaction-isolation-level.html\" target=\"_blank\">MySQL 事务隔离级别详解</a>：理解脏读、不可重复读、幻读和不同隔离级别的权衡。</li>\n<li><a href=\"/database/mysql/innodb-implementation-of-mvcc.html\" target=\"_blank\">InnoDB 存储引擎对 MVCC 的实现</a>：理解 Read View、隐藏字段、undo log 和快照读。</li>\n<li><a href=\"/database/mysql/mysql-logs.html\" target=\"_blank\">MySQL 三大日志详解</a>：理解 binlog、redo log、undo log 的作用和关系。</li>\n<li><a href=\"/database/mysql/mysql-query-execution-plan.html\" target=\"_blank\">MySQL 执行计划分析</a>：掌握 EXPLAIN 常见字段和慢 SQL 分析入口。</li>\n</ul>\n<h3>Redis</h3>\n<p>Redis 既是缓存，也是高频中间件考点。学习时不要只背命令，更要理解缓存策略、数据结构、持久化、阻塞原因和集群机制。</p>\n<ul>\n<li><a href=\"/database/redis/\" target=\"_blank\">Redis 专题</a>：围绕缓存、数据结构、持久化、集群、阻塞问题和工程实践展开。</li>\n<li><a href=\"/database/redis/cache-basics.html\" target=\"_blank\">缓存基础常见面试题总结</a>：理解缓存使用场景、缓存穿透、击穿、雪崩和一致性问题。</li>\n<li><a href=\"/database/redis/redis-questions-01.html\" target=\"_blank\">Redis 常见面试题总结（上）</a> 和 <a href=\"/database/redis/redis-questions-02.html\" target=\"_blank\">Redis 常见面试题总结（下）</a>：快速建立 Redis 高频问题清单。</li>\n<li><a href=\"/database/redis/redis-data-structures-01.html\" target=\"_blank\">Redis 5 种基本数据类型详解</a>：理解 String、List、Hash、Set、Sorted Set 的应用场景。</li>\n<li><a href=\"/database/redis/redis-persistence.html\" target=\"_blank\">Redis 持久化机制详解</a>：理解 RDB、AOF、AOF 重写和混合持久化。</li>\n<li><a href=\"/database/redis/redis-cluster.html\" target=\"_blank\">Redis 集群详解</a>：理解主从复制、哨兵、Cluster、槽位和故障转移。</li>\n</ul>\n<h3>NoSQL 与搜索</h3>\n<p>这部分适合在掌握关系型数据库和缓存之后补充，用来理解文档数据库、搜索引擎和非关系型存储的选型边界。</p>\n<ul>\n<li><a href=\"/database/mongodb/\" target=\"_blank\">MongoDB 专题</a>：整理 MongoDB 文档模型、索引、副本集、分片、事务和常见面试问题。</li>\n<li><a href=\"/database/mongodb/mongodb-questions-01.html\" target=\"_blank\">MongoDB 常见面试题总结（上）</a> 和 <a href=\"/database/mongodb/mongodb-questions-02.html\" target=\"_blank\">MongoDB 常见面试题总结（下）</a>：理解 MongoDB 核心概念和工程实践。</li>\n<li><a href=\"/database/elasticsearch/elasticsearch-questions-01.html\" target=\"_blank\">Elasticsearch 常见面试题总结</a>：理解倒排索引、分片、副本、写入查询流程和搜索场景。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>关系型数据库和 NoSQL 有什么区别？分别适合哪些场景？</li>\n<li>数据库事务 ACID 是什么？隔离级别分别解决什么问题？</li>\n<li>SQL 中 WHERE、GROUP BY、HAVING、ORDER BY 的执行顺序如何理解？</li>\n<li>MySQL 索引为什么能加速查询？什么情况下会索引失效？</li>\n<li>InnoDB 如何通过 MVCC 实现非锁定一致性读？</li>\n<li>binlog、redo log、undo log 分别解决什么问题？</li>\n<li>如何通过 EXPLAIN 分析 SQL 执行计划？</li>\n<li>Redis 为什么快？缓存穿透、击穿、雪崩如何处理？</li>\n<li>Redis 持久化、主从复制、哨兵和 Cluster 分别解决什么问题？</li>\n<li>MongoDB 和 Elasticsearch 分别适合什么业务场景？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/high-performance/\" target=\"_blank\">高性能系统知识体系</a></li>\n<li><a href=\"/high-availability/\" target=\"_blank\">高可用系统知识体系</a></li>\n<li><a href=\"/distributed-system/\" target=\"_blank\">分布式系统知识体系</a></li>\n<li><a href=\"/system-design/\" target=\"_blank\">系统设计</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "数据库"
      ]
    },
    {
      "title": "数据库基础常见面试题总结",
      "url": "https://codingmap.strova.top/database/basis.html",
      "id": "https://codingmap.strova.top/database/basis.html",
      "summary": "数据库基础面试题和知识点总结，包括数据库、DBMS、数据库系统、DBA的概念区别，DBMS核心功能，元组、码、主键外键等关系型数据库核心概念，以及ER图的使用方法。重点围绕 什么是数据库, 数据库管理系统, 数据库系统, 数据库管理员?、DBMS 有哪些主要的功能、你知道哪些类型的 DBMS？",
      "content_html": "<p>数据库知识基础，这部分内容一定要理解记忆。虽然这部分内容只是理论知识，但是非常重要，这是后面学习 MySQL 数据库的基础。PS: 这部分内容由于涉及太多概念性内容，所以参考了维基百科和百度百科相应的介绍。</p>\n<h2>什么是数据库, 数据库管理系统, 数据库系统, 数据库管理员?</h2>\n<p>这四个概念描述了从数据本身到管理整个体系的不同层次，我们常用一个图书馆的例子来把它们串联起来理解。</p>\n<ul>\n<li><strong>数据库 (Database - DB):</strong> 它就像是图书馆里，书架上存放的所有书籍和资料。从技术上讲，数据库就是按照一定数据模型组织、描述和储存起来的、可以被各种用户共享的结构化数据的集合。它就是我们最终要存取的核心——信息本身。</li>\n<li><strong>数据库管理系统 (Database Management System - DBMS):</strong> 它就像是整个图书馆的管理系统，包括图书的分类编目规则、借阅归还流程、安全检查系统等等。从技术上讲，DBMS 是一种大型软件，比如我们常用的 MySQL、Oracle、PostgreSQL 软件。它的核心职责是科学地组织和存储数据、高效地获取和维护数据；为我们屏蔽了底层文件操作的复杂性，提供了一套标准接口（如 SQL）来操纵数据，并负责并发控制、事务管理、权限控制等复杂问题。</li>\n<li><strong>数据库系统 (Database System - DBS):</strong> 它就是整个正常运转的图书馆。这是一个更大的概念，不仅包括书(DB)和管理系统(DBMS)，还包括了硬件、应用和使用的人。</li>\n<li><strong>数据库管理员 (Database Administrator - DBA ):</strong> 他就是图书馆的馆长，负责整个数据库系统正常运行。他的职责非常广泛，包括数据库的设计、安装、监控、性能调优、备份与恢复、安全管理等等，确保整个系统的稳定、高效和安全。</li>\n</ul>\n<p>DB 和 DBMS 我们通常会搞混，这里再简单提一下：<strong>通常我们说“用 MySQL 数据库”，其实是用 MySQL（DBMS）来管理一个或多个数据库（DB）。</strong></p>\n<h2>DBMS 有哪些主要的功能</h2>\n<p>DBMS 通常提供四大核心功能：</p>\n<ol>\n<li><strong>数据定义：</strong> 这是 DBMS 的基础。它提供了一套数据定义语言（Data Definition Language - DDL），让我们能够创建、修改和删除数据库中的各种对象。这不仅仅是定义表的结构（比如字段名、数据类型），还包括定义视图、索引、触发器、存储过程等。</li>\n<li><strong>数据操作：</strong> 这是我们作为开发者日常使用最多的功能。它提供了一套数据操作语言（Data Manipulation Language - DML），核心就是我们熟悉的增、删、改、查（CRUD）操作。它让我们能够方便地对数据库中的数据进行操作和检索。</li>\n<li><strong>数据控制：</strong> 这是保证数据正确、安全、可靠的关键。通常包含并发控制、事务管理、完整性约束、权限控制、安全性限制等功能。</li>\n<li><strong>数据库维护：</strong> 这部分功能是为了保障数据库系统的长期稳定运行。它包括了数据的导入导出、数据库的备份与恢复、性能监控与分析、以及系统日志管理等。</li>\n</ol>\n<h2>你知道哪些类型的 DBMS？</h2>\n<h3>关系型数据库</h3>\n<p>除了我们最常用的关系型数据库（RDBMS），比如 MySQL（开源首选）、PostgreSQL（功能最全）、Oracle（企业级），它们基于严格的表结构和 SQL，非常适合结构化数据和需要事务保证的场景，例如银行交易、订单系统。</p>\n<p>近年来，为了应对互联网应用带来的海量数据、高并发和多样化数据结构的需求，涌现出了一大批 NoSQL 和 NewSQL 数据库。</p>\n<h3>NoSQL 数据库</h3>\n<p>它们的共同特点是为了极致的性能和水平扩展能力，在某些方面（通常是事务）做了妥协。</p>\n<p><strong>1. 键值数据库，代表是 Redis。</strong></p>\n<ul>\n<li><strong>特点：</strong> 数据模型极其简单，就是一个巨大的 Map，通过 Key 来存取 Value。内存操作，性能极高。</li>\n<li><strong>适用场景：</strong> 非常适合做缓存、会话存储、计数器等对读写性能要求极高的场景。</li>\n</ul>\n<p><strong>2. 文档数据库，代表是 MongoDB。</strong></p>\n<ul>\n<li><strong>特点：</strong> 它存储的是半结构化的文档（比如 JSON/BSON），结构灵活，不需要预先定义表结构。</li>\n<li><strong>适用场景：</strong> 特别适合那些数据结构多变、快速迭代的业务，比如用户画像、内容管理系统、日志存储等。</li>\n</ul>\n<p><strong>3. 列式数据库，代表是 HBase, Cassandra。</strong></p>\n<ul>\n<li><strong>特点：</strong> 数据是按列族而不是按行来存储的。这使得它在对大量行进行少量列的读取时，性能极高。</li>\n<li><strong>适用场景：</strong> 专为海量数据存储和分析设计，非常适合做大数据分析、监控数据存储、推荐系统等需要高吞吐量写入和范围扫描的场景。</li>\n</ul>\n<p><strong>4. 图形数据库，代表是 Neo4j。</strong></p>\n<ul>\n<li><strong>特点：</strong> 数据模型是节点（Nodes）和边（Edges），专门用来存储和查询实体之间的复杂关系。</li>\n<li><strong>适用场景：</strong> 在社交网络（好友关系）、推荐引擎（用户-商品关系）、知识图谱、欺诈检测（资金流动关系）等场景下，表现远超关系型数据库。</li>\n</ul>\n<h3>NewSQL 数据库</h3>\n<p>由于 NoSQL 不支持事务，很多对于数据安全要求非常高的系统（比如财务系统、订单系统、交易系统）就不太适合使用了。不过，这类系统往往有存储大量数据的需求。</p>\n<p>这些系统往往只能通过购买性能更强大的计算机，或者通过数据库中间件来提高存储能力。不过，前者的金钱成本太高，后者的开发成本太高。</p>\n<p>于是，<strong>NewSQL</strong> 就来了！</p>\n<p>简单来说，NewSQL 就是：<strong>分布式存储+SQL+事务</strong> 。NewSQL 不仅具有 NoSQL 对海量数据的存储管理能力，还保持了传统数据库支持 ACID 和 SQL 等特性。因此，NewSQL 也可以称为 <strong>分布式关系型数据库</strong>。</p>\n<p>NewSQL 数据库设计的一些目标：</p>\n<ol>\n<li>横向扩展（Scale Out） ： 通过增加机器的方式来提高系统的负载能力。与之类似的是 Scale Up(纵向扩展)，升级硬件设备的方式来提高系统的负载能力。</li>\n<li>强一致性（Strict Consistency）：在任意时刻，所有节点中的数据是一样的。</li>\n<li>高可用（High Availability）：系统几乎可以一直提供服务。</li>\n<li>支持标准 SQL（Structured Query Language） ：PostgreSQL、MySQL、Oracle 等关系型数据库都支持 SQL 。</li>\n<li>事务（ACID） ： 原子性（Atomicity）、一致性（Consistency）、 隔离性（Isolation）; 持久性（Durability）。</li>\n<li>兼容主流关系型数据库 ： 兼容 MySQL、Oracle、PostgreSQL 等常用关系型数据库。</li>\n<li>云原生 （Cloud Native）：可在公有云、私有云、混合云中实现部署工具化、自动化。</li>\n<li>HTAP（Hybrid Transactional/Analytical Processing） ：支持 OLTP 和 OLAP 混合处理。</li>\n</ol>\n<p>NewSQL 数据库代表：Google 的 F1/Spanner、阿里的 <a href=\"https://open.oceanbase.com/\" target=\"_blank\" rel=\"noopener noreferrer\">OceanBase</a>、PingCAP 的 <a href=\"https://pingcap.com/zh/product-community/\" target=\"_blank\" rel=\"noopener noreferrer\">TiDB</a> 。</p>\n<h2>什么是元组, 码, 候选码, 主码, 外码, 主属性, 非主属性？</h2>\n<p>在关系型数据库理论中，理解元组、码、候选码、主码、外码、主属性和非主属性这些核心概念，对于数据库设计和规范化至关重要。这些概念构成了关系数据库的理论基础。</p>\n<h3>基础概念</h3>\n<ul>\n<li><strong>元组（Tuple）：</strong> 元组是关系数据库中的基本单位，在二维表中对应一行记录。每个元组包含了一个实体的完整信息。例如，在学生表中，每个学生的完整信息（学号、姓名、年龄等）构成一个元组。</li>\n<li><strong>码（Key）：</strong> 码是能够唯一标识关系中元组的一个或多个属性的集合。码的主要作用是保证数据的唯一性和完整性。</li>\n</ul>\n<h3>码的分类</h3>\n<ul>\n<li><strong>候选码（Candidate Key）：</strong> 候选码是能够唯一标识元组的最小属性集合，其任何真子集都不能唯一标识元组。一个关系可能有多个候选码。例如，在学生表中，如果&quot;学号&quot;能唯一标识学生，同时&quot;身份证号&quot;也能唯一标识学生，那么{学号}和{身份证号}都是候选码。</li>\n<li><strong>主码/主键（Primary Key）：</strong> 主码是从候选码中选择的一个，用于唯一标识关系中的元组。每个关系只能有一个主码，但可以有多个候选码。选择主码时通常考虑：简单性、稳定性、无业务含义等因素。</li>\n<li><strong>外码/外键（Foreign Key）：</strong> 外码是一个关系中的属性或属性组，它对应另一个关系的主码。外码用于建立和维护两个关系之间的联系，是实现参照完整性的重要机制。例如，在选课表中的&quot;学号&quot;如果引用学生表的主码&quot;学号&quot;，则选课表中的&quot;学号&quot;就是外码。</li>\n</ul>\n<h3>属性分类</h3>\n<ul>\n<li><strong>主属性（Prime Attribute）：</strong> 主属性是包含在任何一个候选码中的属性。如果一个关系有多个候选码，那么这些候选码中出现的所有属性都是主属性。例如，工人关系（工号，身份证号，姓名，性别，部门）中，如果{工号}和{身份证号}都是候选码，那么&quot;工号&quot;和&quot;身份证号&quot;都是主属性。</li>\n<li><strong>非主属性（Non-prime Attribute）：</strong> 非主属性是不包含在任何候选码中的属性。这些属性完全依赖于候选码来确定其值。在上述工人关系中，&quot;姓名&quot;、&quot;性别&quot;、&quot;部门&quot;都是非主属性。</li>\n</ul>\n<h2>什么是 ER 图？</h2>\n<p>我们做一个项目的时候一定要试着画 ER 图来捋清数据库设计，这个也是面试官问你项目的时候经常会被问到的。</p>\n<p><strong>ER 图</strong> 全称是 Entity Relationship Diagram（实体联系图），提供了表示实体类型、属性和联系的方法。</p>\n<p>ER 图由下面 3 个要素组成：</p>\n<ul>\n<li><strong>实体</strong>：通常是现实世界的业务对象，当然使用一些逻辑对象也可以。比如对于一个校园管理系统，会涉及学生、教师、课程、班级等等实体。在 ER 图中，实体使用矩形框表示。</li>\n<li><strong>属性</strong>：即某个实体拥有的属性，属性用来描述组成实体的要素，对于产品设计来说可以理解为字段。在 ER 图中，属性使用椭圆形表示。</li>\n<li><strong>联系</strong>：即实体与实体之间的关系，在 ER 图中用菱形表示，这个关系不仅有业务关联关系，还能通过数字表示实体之间的数量对照关系。例如，一个班级会有多个学生就是一种实体间的联系。</li>\n</ul>\n<p>下图是一个学生选课的 ER 图，每个学生可以选若干门课程，同一门课程也可以被若干人选择，所以它们之间的关系是多对多（M: N）。另外，还有其他两种实体之间的关系是：1 对 1（1:1）、1 对多（1: N）。</p>\n<h2>数据库范式了解吗?</h2>\n<p>数据库范式有 3 种：</p>\n<ul>\n<li>1NF(第一范式)：属性不可再分。</li>\n<li>2NF(第二范式)：1NF 的基础之上，消除了非主属性对于码的部分函数依赖。</li>\n<li>3NF(第三范式)：3NF 在 2NF 的基础之上，消除了非主属性对于码的传递函数依赖 。</li>\n</ul>\n<h3>1NF(第一范式)</h3>\n<p>属性（对应于表中的字段）不能再被分割，也就是这个字段只能是一个值，不能再分为多个其他的字段了。<strong>1NF 是所有关系型数据库的最基本要求</strong> ，也就是说关系型数据库中创建的表一定满足第一范式。</p>\n<h3>2NF(第二范式)</h3>\n<p>2NF 在 1NF 的基础之上，消除了非主属性对于码的部分函数依赖。如下图所示，展示了第一范式到第二范式的过渡。第二范式在第一范式的基础上增加了一个列，这个列称为主键，非主属性都依赖于主键。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/csdn/bd1d31be3779342427fc9e462bf7f05c.png\" alt=\"第二范式\"></p>\n<p>一些重要的概念：</p>\n<ul>\n<li><strong>函数依赖（functional dependency）</strong>：若在一张表中，在属性（或属性组）X 的值确定的情况下，必定能确定属性 Y 的值，那么就可以说 Y 函数依赖于 X，写作 X → Y。</li>\n<li><strong>部分函数依赖（partial functional dependency）</strong>：如果 X→Y，并且存在 X 的一个真子集 X0，使得 X0→Y，则称 Y 对 X 部分函数依赖。比如学生基本信息表 R 中（学号，身份证号，姓名）当然学号属性取值是唯一的，在 R 关系中，（学号，身份证号）-&gt;（姓名），（学号）-&gt;（姓名），（身份证号）-&gt;（姓名）；所以姓名部分函数依赖于（学号，身份证号）；</li>\n<li><strong>完全函数依赖(Full functional dependency)</strong>：在一个关系中，若某个非主属性数据项依赖于全部关键字称之为完全函数依赖。比如学生基本信息表 R（学号，班级，姓名）假设不同的班级学号有相同的，班级内学号不能相同，在 R 关系中，（学号，班级）-&gt;（姓名），但是（学号）-&gt;(姓名)不成立，（班级）-&gt;(姓名)不成立，所以姓名完全函数依赖与（学号，班级）；</li>\n<li><strong>传递函数依赖</strong>：在关系模式 R(U)中，设 X，Y，Z 是 U 的不同的属性子集，如果 X 确定 Y、Y 确定 Z，且有 X 不包含 Y，Y 不确定 X，（X∪Y）∩Z=空集合，则称 Z 传递函数依赖(transitive functional dependency) 于 X。传递函数依赖会导致数据冗余和异常。传递函数依赖的 Y 和 Z 子集往往同属于某一个事物，因此可将其合并放到一个表中。比如在关系 R(学号 , 姓名, 系名，系主任)中，学号 → 系名，系名 → 系主任，所以存在非主属性系主任对于学号的传递函数依赖。</li>\n</ul>\n<h3>3NF(第三范式)</h3>\n<p>3NF 在 2NF 的基础之上，消除了非主属性对于码的传递函数依赖 。符合 3NF 要求的数据库设计，<strong>基本</strong>上解决了数据冗余过大，插入异常，修改异常，删除异常的问题。比如在关系 R(学号 , 姓名, 系名，系主任)中，学号 → 系名，系名 → 系主任，所以存在非主属性系主任对于学号的传递函数依赖，所以该表的设计，不符合 3NF 的要求。</p>\n<h2>主键和外键有什么区别?</h2>\n<p>从定义和属性上看，它们的区别是：</p>\n<ul>\n<li><strong>主键 (Primary Key):</strong> 它的核心作用是唯一标识表中的每一行数据。因此，主键列的值必须是唯一的 (Unique) 且不能为空 (Not Null)。一张表只能有一个主键。主键保证了实体完整性。</li>\n<li><strong>外键 (Foreign Key):</strong> 它的核心作用是建立并强制两张表之间的关联关系。一张表中的外键列，其值必须对应另一张表中某行的候选键值（通常是主键，也可以是唯一键），或者是一个 NULL 值。因此，外键的值可以重复，也可以为空。一张表可以有多个外键，分别关联到不同的表。外键保证了引用完整性。</li>\n</ul>\n<p>用一个简单的电商例子来说明：假设我们有两张表：<code>users</code> (用户表) 和 <code>orders</code> (订单表)。</p>\n<ul>\n<li>在 <code>users</code> 表中，<code>user_id</code> 列是<strong>主键</strong>。每个用户的 <code>user_id</code> 都是独一无二的，我们用它来区分张三和李四。</li>\n<li>在 <code>orders</code> 表中，<code>order_id</code> 是它自己的<strong>主键</strong>。同时，它会有一个 <code>user_id</code> 列，这个列就是一个<strong>外键</strong>，它引用了 <code>users</code> 表的 <code>user_id</code> 主键。</li>\n</ul>\n<p>这个外键约束就保证了：</p>\n<ol>\n<li>你不能创建一个不属于任何已知用户的订单（ <code>user_id</code> 在 <code>users</code> 表中不存在）。</li>\n<li>你不能删除一个已经下了订单的用户（除非设置了级联删除等特殊规则）。</li>\n</ol>\n<h2>为什么不推荐使用外键与级联？</h2>\n<p>对于外键和级联，阿里巴巴开发手册这样说到：</p>\n<blockquote>\n<p>【强制】不得使用外键与级联，一切外键概念必须在应用层解决。</p>\n<p>说明: 以学生和成绩的关系为例，学生表中的 student_id 是主键，那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id，同时触发成绩表中的 student_id 更新，即为级联更新。外键与级联更新适用于单机低并发，不适合分布式、高并发集群；级联更新是强阻塞，存在数据库更新风暴的风险；外键影响数据库的插入速度</p>\n</blockquote>\n<p>为什么不要用外键呢？大部分人可能会这样回答：</p>\n<ol>\n<li><strong>增加了复杂性：</strong> a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束，会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的，假如哪天需求有变化，数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。</li>\n<li><strong>增加了额外工作</strong>：数据库需要增加维护外键的工作，比如当我们做一些涉及外键字段的增，删，更新操作之后，需要触发相关操作去检查，保证数据的一致性和正确性，这样会不得不消耗数据库资源。如果在应用层面去维护的话，可以减小数据库压力；</li>\n<li><strong>对分库分表不友好</strong>：因为分库分表下外键是无法生效的。</li>\n<li>……</li>\n</ol>\n<p>我个人觉得上面这种回答不是特别的全面，只是说了外键存在的一个常见的问题。实际上，我们知道外键也是有很多好处的，比如：</p>\n<ol>\n<li>保证了数据库数据的一致性和完整性；</li>\n<li>级联操作方便，减轻了程序代码量；</li>\n<li>……</li>\n</ol>\n<p>所以说，不要一股脑的就抛弃了外键这个概念，既然它存在就有它存在的道理，如果系统不涉及分库分表，并发量不是很高的情况还是可以考虑使用外键的。</p>\n<h2>什么是存储过程?</h2>\n<p>存储过程是数据库中预编译的SQL语句集合，它将多条SQL语句和程序逻辑控制语句（如IF-ELSE、WHILE循环等）封装在一起，形成一个可重复调用的数据库对象。</p>\n<p><strong>存储过程的优势：</strong></p>\n<p>在传统企业级应用中，存储过程具有一定的实用价值。当业务逻辑复杂时，需要执行大量SQL语句才能完成一个业务操作，此时可以将这些语句封装成存储过程，简化调用过程。由于存储过程在创建时就已经编译并存储在数据库中，执行时无需重新编译，因此相比动态SQL语句具有更好的执行性能。同时，一旦存储过程调试完成，其运行相对稳定可靠。</p>\n<p><strong>存储过程的局限性：</strong></p>\n<p>然而，在现代互联网架构中，存储过程的使用越来越少。主要原因包括：调试困难，缺乏成熟的调试工具；扩展性差，修改业务逻辑需要直接修改数据库对象；移植性差，不同数据库系统的存储过程语法差异较大；占用数据库资源，增加数据库服务器负担；版本管理困难，不便于进行代码版本控制。</p>\n<p><strong>行业规范：</strong></p>\n<p>基于以上原因，许多互联网公司的开发规范中明确限制或禁止使用存储过程。例如，《阿里巴巴Java开发手册》中明确规定禁止使用存储过程，推荐将业务逻辑放在应用层实现，保持数据库的简单和高效。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/csdn/0fa082bc4d4f919065767476a41b2156.png\" alt=\"阿里巴巴Java开发手册: 禁止存储过程\"></p>\n<h2>DROP、DELETE、TRUNCATE 有什么区别？</h2>\n<p>在数据库操作中，<code>DROP</code>、<code>DELETE</code> 和 <code>TRUNCATE</code> 是三个常用的数据删除命令，它们在功能、性能和使用场景上存在显著差异。</p>\n<p><strong>DROP命令：</strong></p>\n<ul>\n<li>语法：<code>DROP TABLE 表名</code></li>\n<li>作用：完全删除整个表，包括表结构、数据、索引、触发器、约束等所有相关对象</li>\n<li>使用场景：当表不再需要时使用</li>\n</ul>\n<p><strong>TRUNCATE命令：</strong></p>\n<ul>\n<li>语法：<code>TRUNCATE TABLE 表名</code></li>\n<li>作用：清空表中所有数据，但保留表结构</li>\n<li>特点：自增长字段（AUTO_INCREMENT）会重置为初始值（通常为1）</li>\n<li>使用场景：需要快速清空表数据但保留表结构时使用</li>\n</ul>\n<p><strong>DELETE命令：</strong></p>\n<ul>\n<li>语法：<code>DELETE FROM 表名 WHERE 条件</code></li>\n<li>作用：删除满足条件的数据行，不带WHERE子句时删除所有数据</li>\n<li>特点：自增长字段不会重置，继续从之前的值递增</li>\n<li>使用场景：需要有选择地删除部分数据时使用</li>\n</ul>\n<p><code>TRUNCATE</code> 和不带 <code>WHERE</code>子句的 <code>DELETE</code>、以及 <code>DROP</code> 都会删除表内的数据，但是 <strong><code>TRUNCATE</code> 和 <code>DELETE</code> 只删除数据不删除表的结构(定义)，执行 <code>DROP</code> 语句，此表的结构也会删除，也就是执行<code>DROP</code> 之后对应的表不复存在。</strong></p>\n<h3>对表结构的影响</h3>\n<ul>\n<li><code>DROP</code>：删除表结构和所有数据，表将不复存在</li>\n<li><code>TRUNCATE</code>：仅删除数据，保留表结构和定义</li>\n<li><code>DELETE</code>：仅删除数据，保留表结构和定义</li>\n</ul>\n<h3>触发器</h3>\n<ul>\n<li><code>DELETE</code> 操作会触发相关的DELETE触发器</li>\n<li><code>TRUNCATE</code> 和 <code>DROP</code> 不会触发DELETE触发器</li>\n</ul>\n<h3>事务和回滚</h3>\n<ul>\n<li><code>DROP</code> 和 <code>TRUNCATE</code> 属于DDL操作，执行后立即生效，不能回滚</li>\n<li><code>DELETE</code> 属于DML操作，可以回滚（在事务中）</li>\n</ul>\n<h3>执行速度</h3>\n<p>一般来说：<code>DROP</code> &gt; <code>TRUNCATE</code> &gt; <code>DELETE</code>（这个我没有实际测试过）。</p>\n<ul>\n<li><code>DELETE</code>命令执行的时候会产生数据库的<code>binlog</code>日志，而日志记录是需要消耗时间的，但是也有个好处方便数据回滚恢复。</li>\n<li><code>TRUNCATE</code>命令执行的时候不会产生数据库日志，因此比<code>DELETE</code>要快。除此之外，还会把表的自增值重置和索引恢复到初始大小等。</li>\n<li><code>DROP</code>命令会把表占用的空间全部释放掉。</li>\n</ul>\n<p>Tips：你应该更多地关注在使用场景上，而不是执行效率。</p>\n<h2>DML 语句和 DDL 语句区别是？</h2>\n<ul>\n<li>DML 是数据库操作语言（Data Manipulation Language）的缩写，是指对数据库中表记录的操作，主要包括表记录的插入、更新、删除和查询，是开发人员日常使用最频繁的操作。</li>\n<li>DDL （Data Definition Language）是数据定义语言的缩写，简单来说，就是对数据库内部的对象进行创建、删除、修改的操作语言。它和 DML 语言的最大区别是 DML 只是对表内部数据的操作，而不涉及到表的定义、结构的修改，更不会涉及到其他对象。DDL 语句更多的被数据库管理员（DBA）所使用，一般的开发人员很少使用。</li>\n</ul>\n<p>另外，由于<code>SELECT</code>不会对表进行破坏，所以有的地方也会把<code>SELECT</code>单独区分开叫做数据库查询语言 DQL（Data Query Language）。</p>\n<h2>数据库设计通常分为哪几步?</h2>\n<h3>1. 需求分析阶段</h3>\n<p><strong>目标：</strong> 深入了解和分析用户需求，明确系统边界<br>\n<strong>主要工作：</strong></p>\n<ul>\n<li>收集和分析数据需求：确定需要存储哪些数据，数据量大小，数据更新频率</li>\n<li>明确功能需求：系统需要支持哪些业务操作，各操作的优先级</li>\n<li>定义性能需求：响应时间要求，并发用户数，数据吞吐量</li>\n<li>确定安全需求：数据访问权限，加密要求，审计要求<br>\n<strong>产出物：</strong> 需求规格说明书、数据字典初稿</li>\n</ul>\n<h3>2. 概念结构设计阶段</h3>\n<p><strong>目标：</strong> 将需求转化为信息世界的概念模型<br>\n<strong>主要工作：</strong></p>\n<ul>\n<li>识别实体：确定系统中的主要对象</li>\n<li>定义属性：明确每个实体的特征</li>\n<li>建立联系：确定实体之间的关系（一对一、一对多、多对多）</li>\n<li>绘制E-R图（实体-关系图）<br>\n<strong>产出物：</strong> E-R图、概念数据模型文档</li>\n</ul>\n<h3>3. 逻辑结构设计阶段</h3>\n<p><strong>目标：</strong> 将概念模型转换为特定DBMS支持的逻辑模型<br>\n<strong>主要工作：</strong></p>\n<ul>\n<li>E-R图向关系模型转换：将实体转换为表，属性转换为字段</li>\n<li>规范化处理：通过范式化消除数据冗余和更新异常（通常达到3NF）</li>\n<li>定义完整性约束：主键、外键、唯一性约束、检查约束</li>\n<li>优化模型：根据性能需求进行适当的反规范化<br>\n<strong>产出物：</strong> 逻辑数据模型、表结构设计文档</li>\n</ul>\n<h3>4. 物理结构设计阶段</h3>\n<p><strong>目标：</strong> 确定数据的物理存储方案和访问方法<br>\n<strong>主要工作：</strong></p>\n<ul>\n<li>选择存储引擎：如MySQL的InnoDB、MyISAM等</li>\n<li>设计索引策略：确定需要建立的索引类型和字段</li>\n<li>分区设计：对大表进行分区以提高性能</li>\n<li>确定存储参数：表空间大小、数据文件位置、缓冲区配置</li>\n<li>制定备份策略：全量备份、增量备份的频率和方式<br>\n<strong>产出物：</strong> 物理设计文档、索引设计方案</li>\n</ul>\n<h3>5. 数据库实施阶段</h3>\n<p><strong>目标：</strong> 将设计转化为实际运行的数据库系统<br>\n<strong>主要工作：</strong></p>\n<ul>\n<li>创建数据库和表结构：编写和执行DDL语句</li>\n<li>开发存储过程和触发器（如需要）</li>\n<li>编写应用程序接口</li>\n<li>导入初始数据</li>\n<li>系统集成测试：功能测试、性能测试、压力测试</li>\n<li>用户培训和文档编写<br>\n<strong>产出物：</strong> 数据库脚本、测试报告、用户手册</li>\n</ul>\n<h3>6. 运行和维护阶段</h3>\n<p><strong>目标：</strong> 确保数据库系统稳定高效运行<br>\n<strong>主要工作：</strong></p>\n<ul>\n<li>日常监控：性能监控、空间监控、错误日志分析</li>\n<li>性能优化：查询优化、索引调整、参数调优</li>\n<li>数据备份和恢复：定期备份、恢复演练</li>\n<li>安全管理：权限管理、安全补丁更新、审计</li>\n<li>容量规划：预测数据增长，提前扩容</li>\n<li>变更管理：需求变更的评估和实施<br>\n<strong>产出物：</strong> 运维报告、优化方案、变更记录</li>\n</ul>\n<h3>设计原则</h3>\n<p>在整个设计过程中应遵循：数据独立性原则、完整性原则、安全性原则、可扩展性原则和标准化原则。</p>\n<h2>参考</h2>\n<ul>\n<li><a href=\"https://blog.csdn.net/rl529014/article/details/48391465\" target=\"_blank\" rel=\"noopener noreferrer\">https://blog.csdn.net/rl529014/article/details/48391465</a></li>\n<li><a href=\"https://www.zhihu.com/question/24696366/answer/29189700\" target=\"_blank\" rel=\"noopener noreferrer\">https://www.zhihu.com/question/24696366/answer/29189700</a></li>\n<li><a href=\"https://blog.csdn.net/bieleyang/article/details/77149954\" target=\"_blank\" rel=\"noopener noreferrer\">https://blog.csdn.net/bieleyang/article/details/77149954</a></li>\n</ul>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/csdn/bd1d31be3779342427fc9e462bf7f05c.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "数据库"
      ]
    },
    {
      "title": "字符集详解：字符集是什么？怎么用？",
      "url": "https://codingmap.strova.top/database/character-set.html",
      "id": "https://codingmap.strova.top/database/character-set.html",
      "summary": "详解字符集与字符编码原理，深入分析ASCII、GB2312、GBK、UTF-8、UTF-16等常见编码，解释MySQL中utf8与utf8mb4的区别以及emoji存储问题的解决方案。重点围绕 字符集是什么？、字符编码是什么？、有哪些常见的字符集？、MySQL 字符集、数据库基础 等内容展开。",
      "content_html": "<p>MySQL 字符编码集中有两套 UTF-8 编码实现：<strong><code>utf8</code></strong> 和 <strong><code>utf8mb4</code></strong>。</p>\n<p>如果使用 <strong><code>utf8</code></strong> 的话，存储 emoji 符号和一些比较复杂的汉字、繁体字就会出错。</p>\n<p>为什么会这样呢？这篇文章可以从源头给你解答。</p>\n<h2>字符集是什么？</h2>\n<p>字符是各种文字和符号的统称，包括各个国家文字、标点符号、表情、数字等等。 <strong>字符集</strong> 就是一系列字符的集合。字符集的种类较多，每个字符集可以表示的字符范围通常不同，就比如说有些字符集是无法表示汉字的。</p>\n<p><strong>计算机只能存储二进制的数据，那英文、汉字、表情等字符应该如何存储呢？</strong></p>\n<p>我们要将这些字符和二进制的数据一一对应起来，比如说字符“a”对应“01100001”，反之，“01100001”对应 “a”。我们将字符对应二进制数据的过程称为&quot;<strong>字符编码</strong>&quot;，反之，二进制数据解析成字符的过程称为“<strong>字符解码</strong>”。</p>\n<h2>字符编码是什么？</h2>\n<p>字符编码是一种将字符集中的字符与计算机中的二进制数据相互转换的方法，可以看作是一种映射规则。也就是说，字符编码的目的是为了让计算机能够存储和传输各种文字信息。</p>\n<p>每种字符集都有自己的字符编码规则，常用的字符集编码规则有 ASCII 编码、 GB2312 编码、GBK 编码、GB18030 编码、Big5 编码、UTF-8 编码、UTF-16 编码等。</p>\n<h2>有哪些常见的字符集？</h2>\n<p>常见的字符集有：ASCII、GB2312、GB18030、GBK、Unicode……。</p>\n<p>不同的字符集的主要区别在于：</p>\n<ul>\n<li>可以表示的字符范围</li>\n<li>编码方式</li>\n</ul>\n<h3>ASCII</h3>\n<p><strong>ASCII</strong> (<strong>A</strong>merican <strong>S</strong>tandard <strong>C</strong>ode for <strong>I</strong>nformation <strong>I</strong>nterchange，美国信息交换标准代码) 是一套主要用于现代美国英语的字符集（这也是 ASCII 字符集的局限性所在）。</p>\n<p><strong>为什么 ASCII 字符集没有考虑到中文等其他字符呢？</strong> 因为计算机是美国人发明的，当时，计算机的发展还处于比较雏形的时代，还未在其他国家大规模使用。因此，美国发布 ASCII 字符集的时候没有考虑兼容其他国家的语言。</p>\n<p>ASCII 字符集至今为止共定义了 128 个字符，其中有 33 个控制字符（比如回车、删除）无法显示。</p>\n<p>一个 ASCII 码长度是一个字节也就是 8 个 bit，比如“a”对应的 ASCII 码是“01100001”。不过，最高位是 0 仅仅作为校验位，其余 7 位使用 0 和 1 进行组合，所以，ASCII 字符集可以定义 128（2^7）个字符。</p>\n<p>由于，ASCII 码可以表示的字符实在是太少了。后来，人们对其进行了扩展得到了 <strong>ASCII 扩展字符集</strong> 。ASCII 扩展字符集使用 8 位（bits）表示一个字符，所以，ASCII 扩展字符集可以定义 256（2^8）个字符。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/csdn/c1c6375d08ca268690cef2b13591a5b4.png\" alt=\"ASCII字符编码\"></p>\n<h3>GB2312</h3>\n<p>我们上面说了，ASCII 字符集是一种现代美国英语适用的字符集。因此，很多国家都捣鼓了一个适合自己国家语言的字符集。</p>\n<p>GB2312 字符集是一种对汉字比较友好的字符集，共收录 6700 多个汉字，基本涵盖了绝大部分常用汉字。不过，GB2312 字符集不支持绝大部分的生僻字和繁体字。</p>\n<p>对于英语字符，GB2312 编码和 ASCII 码是相同的，1 字节编码即可。对于非英字符，需要 2 字节编码。</p>\n<h3>GBK</h3>\n<p>GBK 字符集可以看作是 GB2312 字符集的扩展，兼容 GB2312 字符集，共收录了 20000 多个汉字。</p>\n<p>GBK 中 K 是汉语拼音 Kuo Zhan（扩展）中的“Kuo”的首字母。</p>\n<h3>GB18030</h3>\n<p>GB18030 完全兼容 GB2312 和 GBK 字符集，纳入中国国内少数民族的文字，且收录了日韩汉字，是目前为止最全面的汉字字符集，共收录汉字 70000 多个。</p>\n<h3>BIG5</h3>\n<p>BIG5 主要针对的是繁体中文，收录了 13000 多个汉字。</p>\n<h3>Unicode &amp; UTF-8</h3>\n<p>为了更加适合本国语言，诞生了很多种字符集。</p>\n<p>我们上面也说了不同的字符集可以表示的字符范围以及编码规则存在差异。这就导致了一个非常严重的问题：<strong>使用错误的编码方式查看一个包含字符的文件就会产生乱码现象。</strong></p>\n<p>就比如说你使用 UTF-8 编码方式打开 GB2312 编码格式的文件就会出现乱码。示例：“牛”这个汉字 GB2312 编码后的十六进制数值为 “C5A3”，而 “C5A3” 用 UTF-8 解码之后得到的却是 “ţ”。</p>\n<p>你可以通过这个网站在线进行编码和解码：<a href=\"https://www.haomeili.net/HanZi/ZiFuBianMaZhuanHuan\" target=\"_blank\" rel=\"noopener noreferrer\">https://www.haomeili.net/HanZi/ZiFuBianMaZhuanHuan</a></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/csdn/836c49b117ee4408871b0020b74c991d.png\" alt></p>\n<p>这样我们就搞懂了乱码的本质：<strong>编码和解码时用了不同或者不兼容的字符集</strong> 。</p>\n<p><img src=\"https://oss.javaguide.cn/javaguide/a8808cbabeea49caa3af27d314fa3c02-1.jpg\" alt></p>\n<p>为了解决这个问题，人们就想：“如果我们能够有一种字符集将世界上所有的字符都纳入其中就好了！”。</p>\n<p>然后，<strong>Unicode</strong> 带着这个使命诞生了。</p>\n<p>Unicode 字符集中包含了世界上几乎所有已知的字符。不过，Unicode 字符集并没有规定如何存储这些字符（也就是如何使用二进制数据表示这些字符）。</p>\n<p>然后，就有了 <strong>UTF-8</strong>（<strong>8</strong>-bit <strong>U</strong>nicode <strong>T</strong>ransformation <strong>F</strong>ormat）。类似的还有 UTF-16、 UTF-32。</p>\n<p>UTF-8 使用 1 到 4 个字节为每个字符编码， UTF-16 使用 2 或 4 个字节为每个字符编码，UTF-32 固定位 4 个字节为每个字符编码。</p>\n<p>UTF-8 可以根据不同的符号自动选择编码的长短，像英文字符只需要 1 个字节就够了，这一点 ASCII 字符集一样 。因此，对于英语字符，UTF-8 编码和 ASCII 码是相同的。</p>\n<p>UTF-32 的规则最简单，不过缺陷也比较明显，对于英文字母这类字符消耗的空间是 UTF-8 的 4 倍之多。</p>\n<p><strong>UTF-8</strong> 是目前使用最广的一种字符编码。</p>\n<p><img src=\"https://oss.javaguide.cn/javaguide/1280px-Utf8webgrowth.svg.png\" alt></p>\n<h2>MySQL 字符集</h2>\n<p>MySQL 支持很多种字符集的方式，比如 GB2312、GBK、BIG5、多种 Unicode 字符集（UTF-8 编码、UTF-16 编码、UCS-2 编码、UTF-32 编码等等）。</p>\n<h3>查看支持的字符集</h3>\n<p>你可以通过 <code>SHOW CHARSET</code> 命令来查看，支持 like 和 where 子句。</p>\n<p><img src=\"https://oss.javaguide.cn/javaguide/image-20211008164229671.png\" alt></p>\n<h3>默认字符集</h3>\n<p>在 MySQL5.7 中，默认字符集是 <code>latin1</code> ；在 MySQL8.0 中，默认字符集是 <code>utf8mb4</code></p>\n<h3>字符集的层次级别</h3>\n<p>MySQL 中的字符集有以下的层次级别：</p>\n<ul>\n<li><code>server</code>（MySQL 实例级别）</li>\n<li><code>database</code>（库级别）</li>\n<li><code>table</code>（表级别）</li>\n<li><code>column</code>（字段级别）</li>\n</ul>\n<p>它们的优先级可以简单的认为是从上往下依次增大，也即 <code>column</code> 的优先级会大于 <code>table</code> 等其余层次的。如指定 MySQL 实例级别字符集是<code>utf8mb4</code>，指定某个表字符集是<code>latin1</code>，那么这个表的所有字段如果不指定的话，编码就是<code>latin1</code>。</p>\n<h4>server</h4>\n<p>不同版本的 MySQL 其 <code>server</code> 级别的字符集默认值不同，在 MySQL5.7 中，其默认值是 <code>latin1</code> ；在 MySQL8.0 中，其默认值是 <code>utf8mb4</code> 。</p>\n<p>当然也可以通过在启动 <code>mysqld</code> 时指定 <code>--character-set-server</code> 来设置 <code>server</code> 级别的字符集。</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">mysqld</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">mysqld</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> --character-set-server=utf8mb4</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">mysqld</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> --character-set-server=utf8mb4</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\"> \\</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">  --collation-server=utf8mb4_0900_ai_ci</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>或者如果你是通过源码构建的方式启动的 MySQL，你可以在 <code>cmake</code> 命令中指定选项：</p>\n<div class=\"language-sh line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sh\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sh\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">cmake</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> .</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> -DDEFAULT_CHARSET=latin1</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">或者</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">cmake</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> .</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> -DDEFAULT_CHARSET=latin1</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\"> \\</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">  -DDEFAULT_COLLATION=latin1_german1_ci</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>此外，你也可以在运行时改变 <code>character_set_server</code> 的值，从而达到修改 <code>server</code> 级别的字符集的目的。</p>\n<p><code>server</code> 级别的字符集是 MySQL 服务器的全局设置，它不仅会作为创建或修改数据库时的默认字符集（如果没有指定其他字符集），还会影响到客户端和服务器之间的连接字符集，具体可以查看 <a href=\"https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-charsets.html\" target=\"_blank\" rel=\"noopener noreferrer\">MySQL Connector/J 8.0 - 6.7 Using Character Sets and Unicode</a>。</p>\n<h4>database</h4>\n<p><code>database</code> 级别的字符集是我们在创建数据库和修改数据库时指定的：</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">CREATE</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> DATABASE</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> db_name</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    [[DEFAULT]</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> CHARACTER</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> SET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> charset_name]</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    [[DEFAULT]</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> COLLATE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> collation_name]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">ALTER</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> DATABASE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> db_name</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    [[DEFAULT]</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> CHARACTER</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> SET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> charset_name]</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    [[DEFAULT]</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> COLLATE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> collation_name]</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>如前面所说，如果在执行上述语句时未指定字符集，那么 MySQL 将会使用 <code>server</code> 级别的字符集。</p>\n<p>可以通过下面的方式查看某个数据库的字符集：</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">USE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> db_name;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SELECT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> @@character_set_database, @@collation_database;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SELECT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">FROM</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> INFORMATION_SCHEMA</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">SCHEMATA</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> WHERE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> SCHEMA_NAME </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 'db_name'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h4>table</h4>\n<p><code>table</code> 级别的字符集是在创建表和修改表时指定的：</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">CREATE</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> TABLE</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> tbl_name</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (column_list)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    [[DEFAULT]</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> CHARACTER</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> SET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> charset_name]</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    [COLLATE collation_name]</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">ALTER</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> TABLE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> tbl_name</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    [[DEFAULT]</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> CHARACTER</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> SET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> charset_name]</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    [COLLATE collation_name]</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>如果在创建表和修改表时未指定字符集，那么将会使用 <code>database</code> 级别的字符集。</p>\n<h4>column</h4>\n<p><code>column</code> 级别的字符集同样是在创建表和修改表时指定的，只不过它是定义在列中。下面是个例子：</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">CREATE</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> TABLE</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> t1</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    col1 </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">VARCHAR</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">5</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      CHARACTER</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> SET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> latin1</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      COLLATE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> latin1_german1_ci</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>如果未指定列级别的字符集，那么将会使用表级别的字符集。</p>\n<h3>连接字符集</h3>\n<p>前面说到了字符集的层次级别，它们是和存储相关的。而连接字符集涉及的是和 MySQL 服务器的通信。</p>\n<p>连接字符集与下面这几个变量息息相关：</p>\n<ul>\n<li><code>character_set_client</code> ：描述了客户端发送给服务器的 SQL 语句使用的是什么字符集。</li>\n<li><code>character_set_connection</code> ：描述了服务器接收到 SQL 语句时使用什么字符集进行翻译。</li>\n<li><code>character_set_results</code> ：描述了服务器返回给客户端的结果使用的是什么字符集。</li>\n</ul>\n<p>它们的值可以通过下面的 SQL 语句查询：</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SELECT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> * </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">FROM</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> performance_schema</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">session_variables</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">WHERE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> VARIABLE_NAME </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">IN</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'character_set_client'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'character_set_connection'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'character_set_results'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'collation_connection'</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">ORDER BY</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> VARIABLE_NAME;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">SHOW </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SESSION</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> VARIABLES </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">LIKE</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 'character\\_set\\_%'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>如果要想修改前面提到的几个变量的值，有以下方式：</p>\n<p>1、修改配置文件</p>\n<div class=\"language-properties line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"properties\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-properties\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">[mysql]</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"># 只针对MySQL客户端程序</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#C678DD\">default-character-set</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#98C379\">utf8mb4</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>2、使用 SQL 语句</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">set</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> names utf8mb4</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"># 或者一个个进行修改</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"># </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> character_set_client </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> utf8mb4;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"># </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> character_set_results </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> utf8mb4;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"># </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> collation_connection </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> utf8mb4;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h3>JDBC 对连接字符集的影响</h3>\n<p>不知道你们有没有碰到过存储 emoji 表情正常，但是使用类似 Navicat 之类的软件的进行查询的时候，发现 emoji 表情变成了问号的情况。这个问题很有可能就是 JDBC 驱动引起的。</p>\n<p>根据前面的内容，我们知道连接字符集也是会影响我们存储的数据的，而 JDBC 驱动会影响连接字符集。</p>\n<p><code>mysql-connector-java</code> （JDBC 驱动）主要通过这几个属性影响连接字符集：</p>\n<ul>\n<li><code>characterEncoding</code></li>\n<li><code>characterSetResults</code></li>\n</ul>\n<p>以 <code>DataGrip 2023.1.2</code> 来说，在它配置数据源的高级对话框中，可以看到 <code>characterSetResults</code> 的默认值是 <code>utf8</code> ，在使用 <code>mysql-connector-java 8.0.25</code> 时，连接字符集最后会被设置成 <code>utf8mb3</code> 。那么这种情况下 emoji 表情就会被显示为问号，并且当前版本驱动还不支持把 <code>characterSetResults</code> 设置为 <code>utf8mb4</code> ，不过换成 <code>mysql-connector-java driver 8.0.29</code> 却是允许的。</p>\n<p>具体可以看一下 StackOverflow 的 <a href=\"https://stackoverflow.com/questions/54815419/datagrip-mysql-stores-emojis-correctly-but-displays-them-as\" target=\"_blank\" rel=\"noopener noreferrer\">DataGrip MySQL stores emojis correctly but displays them as?</a>这个回答。</p>\n<h3>UTF-8 使用</h3>\n<p>通常情况下，我们建议使用 UTF-8 作为默认的字符编码方式。</p>\n<p>不过，这里有一个小坑。</p>\n<p>MySQL 字符编码集中有两套 UTF-8 编码实现：</p>\n<ul>\n<li><strong><code>utf8</code></strong>：<code>utf8</code>编码只支持<code>1-3</code>个字节 。 在 <code>utf8</code> 编码中，中文是占 3 个字节，其他数字、英文、符号占一个字节。但 emoji 符号占 4 个字节，一些较复杂的文字、繁体字也是 4 个字节。</li>\n<li><strong><code>utf8mb4</code></strong>：UTF-8 的完整实现，正版！最多支持使用 4 个字节表示字符，因此，可以用来存储 emoji 符号。</li>\n</ul>\n<p><strong>为什么有两套 UTF-8 编码实现呢？</strong> 原因如下：</p>\n<p><img src=\"https://oss.javaguide.cn/javaguide/image-20211008164542347.png\" alt></p>\n<p>因此，如果你需要存储<code>emoji</code>类型的数据或者一些比较复杂的文字、繁体字到 MySQL 数据库的话，数据库的编码一定要指定为<code>utf8mb4</code> 而不是<code>utf8</code> ，要不然存储的时候就会报错了。</p>\n<p>演示一下吧！（环境：MySQL 5.7+）</p>\n<p>建表语句如下，我们指定数据库 CHARSET 为 <code>utf8</code> 。</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">CREATE</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> TABLE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> `</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">user</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">` (</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">  `id`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> varchar</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">66</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">CHARACTER</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> SET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> utf8mb3 </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">NOT NULL</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">  `name`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> varchar</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">33</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">CHARACTER</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> SET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> utf8mb3 </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">NOT NULL</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">  `phone`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> varchar</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">33</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">CHARACTER</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> SET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> utf8mb3 </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">DEFAULT</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> NULL</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">  `password`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> varchar</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">100</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">CHARACTER</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> SET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> utf8mb3 </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">DEFAULT</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> NULL</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) ENGINE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">InnoDB </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">DEFAULT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> CHARSET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">utf8;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>当我们执行下面的 insert 语句插入数据到数据库时，果然报错！</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">INSERT INTO</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> `user`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`id`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`name`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`phone`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`password`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">VALUES</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'A00003'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'guide哥😘😘😘'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'181631312312'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'123456'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>报错信息如下：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>Incorrect string value: '\\xF0\\x9F\\x98\\x98\\xF0\\x9F...' for column 'name' at row 1</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><h2>参考</h2>\n<ul>\n<li>字符集和字符编码（Charset &amp; Encoding）：<a href=\"https://www.cnblogs.com/skynet/archive/2011/05/03/2035105.html\" target=\"_blank\" rel=\"noopener noreferrer\">https://www.cnblogs.com/skynet/archive/2011/05/03/2035105.html</a></li>\n<li>十分钟搞清字符集和字符编码：<a href=\"http://cenalulu.github.io/linux/character-encoding/\" target=\"_blank\" rel=\"noopener noreferrer\">http://cenalulu.github.io/linux/character-encoding/</a></li>\n<li>Unicode-维基百科：<a href=\"https://zh.wikipedia.org/wiki/Unicode\" target=\"_blank\" rel=\"noopener noreferrer\">https://zh.wikipedia.org/wiki/Unicode</a></li>\n<li>GB2312-维基百科：<a href=\"https://zh.wikipedia.org/wiki/GB_2312\" target=\"_blank\" rel=\"noopener noreferrer\">https://zh.wikipedia.org/wiki/GB_2312</a></li>\n<li>UTF-8-维基百科：<a href=\"https://zh.wikipedia.org/wiki/UTF-8\" target=\"_blank\" rel=\"noopener noreferrer\">https://zh.wikipedia.org/wiki/UTF-8</a></li>\n<li>GB18030-维基百科: <a href=\"https://zh.wikipedia.org/wiki/GB_18030\" target=\"_blank\" rel=\"noopener noreferrer\">https://zh.wikipedia.org/wiki/GB_18030</a></li>\n<li>MySQL8 文档：<a href=\"https://dev.mysql.com/doc/refman/8.0/en/charset.html\" target=\"_blank\" rel=\"noopener noreferrer\">https://dev.mysql.com/doc/refman/8.0/en/charset.html</a></li>\n<li>MySQL5.7 文档：<a href=\"https://dev.mysql.com/doc/refman/5.7/en/charset.html\" target=\"_blank\" rel=\"noopener noreferrer\">https://dev.mysql.com/doc/refman/5.7/en/charset.html</a></li>\n<li>MySQL Connector/J 文档：<a href=\"https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-charsets.html\" target=\"_blank\" rel=\"noopener noreferrer\">https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-charsets.html</a></li>\n</ul>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/csdn/c1c6375d08ca268690cef2b13591a5b4.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "数据库"
      ]
    },
    {
      "title": "NoSQL基础常见面试题总结",
      "url": "https://codingmap.strova.top/database/nosql.html",
      "id": "https://codingmap.strova.top/database/nosql.html",
      "summary": "NoSQL数据库基础面试题和知识总结，包括NoSQL与SQL的区别、NoSQL的优势、四种NoSQL数据库类型（键值、文档、图形、宽列）及其代表产品Redis、MongoDB、Neo4j等的应用场景。重点围绕 NoSQL 是什么？、SQL 和 NoSQL 有什么区别？、NoSQL 数据库有什么优势？",
      "content_html": "<h2>NoSQL 是什么？</h2>\n<p>NoSQL（Not Only SQL 的缩写）泛指非关系型的数据库，主要针对的是键值、文档以及图形类型数据存储。并且，NoSQL 数据库天生支持分布式，数据冗余和数据分片等特性，旨在提供可扩展的高可用高性能数据存储解决方案。</p>\n<p>一个常见的误解是 NoSQL 数据库或非关系型数据库不能很好地存储关系型数据。NoSQL 数据库可以存储关系型数据—它们与关系型数据库的存储方式不同。</p>\n<p>NoSQL 数据库代表：HBase、Cassandra、MongoDB、Redis。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/database/mongodb/sql-nosql-tushi.png\" alt></p>\n<h2>SQL 和 NoSQL 有什么区别？</h2>\n<p>|              | SQL 数据库                                                                 | NoSQL 数据库                                                                                                                            |<br>\n| :</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/database/mongodb/sql-nosql-tushi.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "数据库"
      ]
    },
    {
      "title": "AI 编程实战指南：Claude Code、Cursor、Codex、Trae 使用技巧与面试题",
      "url": "https://codingmap.strova.top/ai-coding/",
      "id": "https://codingmap.strova.top/ai-coding/",
      "summary": "AI 编程实战指南：Claude Code 使用技巧与核心命令、Vibe Coding 技巧、OpenAI Codex 最佳实践、Spec Coding 规范驱动编程、AI 代码审查与提交管理。覆盖工具选型、真实项目案例和 AI 编程面试题，帮你把 AI 编程工具用稳、用到生产级。重点围绕 适合谁看等核心内容。",
      "content_html": "<p>AI 编程工具好不好用，真不全看模型。很多时候，差别反而出在你怎么给上下文、怎么拆任务、怎么看 diff。</p>\n<p>当然了，这不是说模型不重要，模型的质量是根本。但使用同样的模型，在不同的人手中最终达到的效果是差别很大的。</p>\n<p>别把它想成“我把需求丢给 AI，然后代码自己就写好了”。真实项目里没这么轻松。更常见的是：AI 写到一半方向歪了，你得接回来；一次改太多文件，你得拆小；测试没过，你还得顺着错误往回追；它一本正经瞎编的时候，你得能看出来。</p>\n<p>所以这个专题不会只聊“哪个工具最强”。Claude Code、Cursor、OpenAI Codex、Trae 都有能帮上忙的地方，关键是你得知道：什么时候让 AI 写代码，什么时候让它查资料、读代码，什么时候该自己上手。还有更重要的一点，出问题以后怎么回滚，怎么把影响控制住。</p>\n<p>本专栏属于 AIGuide 项目，对标 JavaGuide 质量，免费开源，欢迎 Star 支持：</p>\n<ul>\n<li><strong>项目地址</strong>：<a href=\"https://github.com/Snailclimb/AIGuide\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Snailclimb/AIGuide</a></li>\n<li><strong>在线阅读</strong>：<a href=\"https://javaguide.cn/ai-coding/\" target=\"_blank\" rel=\"noopener noreferrer\">https://javaguide.cn/ai-coding/</a></li>\n</ul>\n<h2>适合谁看</h2>\n<ul>\n<li>已经在用 Claude Code、Cursor、Codex、Trae，但总觉得“能用，就是不太稳”。</li>\n<li>想把 AI 编程工具用到真实项目里，而不是只拿来写几个 Demo。</li>\n<li>正在纠结 CLI 和 IDE 怎么选，或者不知道什么时候该开多 Agent 并行。</li>\n<li>准备 AI 编程、AI IDE、AI 辅助开发相关面试题，想把工具经验讲得更像真实项目经历。</li>\n<li>带团队，想知道 AI 生成的代码怎么审、怎么测、怎么控制提交粒度。</li>\n</ul>\n<h2>几个容易想错的地方</h2>\n<p>CLI 和 IDE 没有谁一定比谁强，主要看当前任务是什么。跨文件重构、批量修改、长任务自动化，用 CLI 会更顺手；局部补全、边看边改、随时调整，IDE 体验通常更好。把这条线分清楚，选工具就没那么纠结。</p>\n<p>多模型协同也不是把所有任务都丢给最贵的模型。写代码、看架构、审 diff、排查问题，需要的能力不一样。分工清楚，多模型能放大效率；分工不清楚，它也会把错误一起放大。</p>\n<p>AI 生成的代码，一定要过测试、审查和可回滚的提交管理。“看起来能跑”只是第一步。真正麻烦的不是它某一行写错了，而是一次改了几百行，最后出问题时你根本不知道从哪儿查。</p>\n<p>面试里如果被问到“AI 对开发效率的影响”，也别只说“提升了多少多少”。更好的回答是讲清楚：它在哪些环节确实省时间，哪些环节反而增加了审查成本，以及你是怎么兜住风险的。</p>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/ai-coding/ai-ide.html\" target=\"_blank\">AI 编程开放性面试题</a>：先看面试会怎么问，也顺便校准自己到底会不会用。</li>\n<li><a href=\"/ai-coding/cli-vs-ide.html\" target=\"_blank\">AI 编程选 CLI 还是 IDE？</a>：再把工具路线分清楚，别一上来就陷入工具名之争。</li>\n<li><a href=\"/ai-coding/claudecode-tips.html\" target=\"_blank\">Claude Code 使用指南</a>、<a href=\"/ai-coding/claudecode-commands.html\" target=\"_blank\">Claude Code 核心命令详解</a>：如果你主用 Claude Code，这两篇可以直接当操作手册看。</li>\n<li><a href=\"/ai-coding/codex-best-practices.html\" target=\"_blank\">OpenAI Codex 最佳实践指南</a>、<a href=\"/ai-coding/programmer-essential-skills.html\" target=\"_blank\">AI 编程必备 Skills 推荐</a>：想把自动化、代码审查和 Skills 玩起来，再看这两篇。</li>\n<li>工具栈确定后，再按需看 Qoder、Trae、DeepSeek V4 + Claude Code、Claude Code 接入第三方模型等实战案例。</li>\n</ol>\n<h2>核心文章</h2>\n<h3>工具选型与方法论</h3>\n<ul>\n<li><a href=\"/ai-coding/ai-ide.html\" target=\"_blank\">AI 编程开放性面试题</a>：把 Cursor、Claude Code 等工具怎么用、AI 对后端开发有什么影响这些问题放在一起讲。</li>\n<li><a href=\"/ai-coding/cli-vs-ide.html\" target=\"_blank\">AI 编程选 CLI 还是 IDE？</a>：对比 Claude Code、Cursor、Kiro、TRAE 等工具，重点看 CLI 和 IDE 到底适合什么活。</li>\n<li><a href=\"/ai-coding/programmer-essential-skills.html\" target=\"_blank\">AI 编程必备 Skills 推荐</a>：整理 TDD、代码审查、UI 设计、网页自动化和 Skill 开发这些常用工作流。</li>\n<li><a href=\"/ai-coding/spec-coding.html\" target=\"_blank\">Spec Coding 规范驱动编程</a>：系统梳理 Vibe Coding 和 Spec Coding 的区别，从四步落地到多代理协作的完整实战指南。</li>\n</ul>\n<h3>Claude Code 与 Codex 实战</h3>\n<ul>\n<li><a href=\"/ai-coding/claudecode-tips.html\" target=\"_blank\">Claude Code 使用指南</a>：从配置、能力扩展到常用工作流，适合刚开始认真用 Claude Code 的读者。</li>\n<li><a href=\"/ai-coding/claudecode-commands.html\" target=\"_blank\">Claude Code 核心命令详解</a>：专门讲 <code>/simplify</code>、<code>/review</code>、<code>/loop</code>、<code>/batch</code> 这些命令怎么用。</li>\n<li><a href=\"/ai-coding/codex-best-practices.html\" target=\"_blank\">OpenAI Codex 最佳实践指南</a>：讲 Codex 云端智能体和 CLI 怎么配提示词、工具权限和安全策略。</li>\n<li><a href=\"/ai-coding/practices/claudecode-agentview.html\" target=\"_blank\">Claude Code Agent View 多会话管理</a>：多 Agent 并行时，最怕状态乱、权限确认乱，这篇主要解决这个问题。</li>\n</ul>\n<h3>真实项目案例</h3>\n<ul>\n<li><a href=\"/ai-coding/idea-qoder-plugin.html\" target=\"_blank\">IDEA 搭配 Qoder 插件实战</a>：看 AI 怎么在 JetBrains IDE 里做接口优化和代码重构。</li>\n<li><a href=\"/ai-coding/trae-m2.7.html\" target=\"_blank\">Trae + MiniMax 多场景实战</a>：用 Redis 故障排查、跨语言重构这些场景，看 AI 辅助编程能做到哪一步。</li>\n<li><a href=\"/ai-coding/cc-glm5.1.html\" target=\"_blank\">Claude Code 接入第三方模型实战</a>：通过 GLM-5.1 做 JVM 智能诊断助手和慢查询治理。</li>\n<li><a href=\"/ai-coding/deepseek-v4-claude-code.html\" target=\"_blank\">DeepSeek V4 + Claude Code 实战</a>：实测代码审计、Flyway 集成、多模型协同这些更贴近项目的任务。</li>\n<li><a href=\"/ai-coding/project/cc-guide.html\" target=\"_blank\">IDEA + CC GUI 插件实战</a>：想在 IDEA 里用 GUI 管 Claude Code 和 Codex，可以看这个开源插件案例。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>AI 编程工具到底适合做代码生成、代码审查、重构、排错还是文档整理？</li>\n<li>Claude Code、Cursor、Codex、Trae、Qoder 分别适合什么场景？</li>\n<li>CLI 和 IDE 的核心差异是什么？为什么长任务更依赖上下文管理？</li>\n<li>如何给 AI 提供足够但不过量的上下文？</li>\n<li>AI 修改大仓库时，如何控制变更范围，避免越改越乱？</li>\n<li>多模型协同什么时候有价值？如何避免模型之间互相放大错误？</li>\n<li>AI 生成代码应该如何验收？测试、Diff、代码审查和提交粒度怎么配合？</li>\n<li>AI 编程会削弱程序员能力吗？后端开发者应该保留哪些判断力和工程基本功？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/ai/\" target=\"_blank\">AI 应用开发知识体系</a></li>\n<li><a href=\"/system-design/\" target=\"_blank\">系统设计</a></li>\n<li><a href=\"/system-design/basis/\" target=\"_blank\">系统设计基础</a></li>\n<li><a href=\"/java/basis/java-basic-questions-01.html\" target=\"_blank\">Java 基础常见面试题</a></li>\n<li><a href=\"/tools/\" target=\"_blank\">常用开发工具</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 编程"
      ]
    },
    {
      "title": "10 道 AI 编程相关的开放性面试问题",
      "url": "https://codingmap.strova.top/ai-coding/ai-ide.html",
      "id": "https://codingmap.strova.top/ai-coding/ai-ide.html",
      "summary": "涵盖 Cursor、Claude Code、Trae 等 AI 编程 IDE 使用技巧，Spec Coding 与 Vibe Coding 区别，以及 AI 对后端开发影响等高频面试问题。重点围绕 AI 编程 IDE 使用技巧、AI 编程对程序员的影响、总结 等内容展开。结合 CodingMap 知识体系梳理 AI 。",
      "content_html": "<p>腾讯面试的时候，面试官问我：“用过什么 AI 编程工具？”。我说：“Trae。”</p>\n<p>空气突然安静了两秒。我搞不清楚为什么面试官沉默了，当时我还在想：“是不是我回答得不够高级？”。</p>\n<p>面试被挂后才意识到：Trae 是字节的，腾讯家的是 CodeBuddy，阿里家的是 Qoder。</p>\n<p>段子归段子！今天 Guide 分享 9 道当下校招和社招技术面试中经常会被问到的 AI 编程开放性问题，希望对你有帮助。</p>\n<ol>\n<li>⭐ <strong>AI 编程 IDE</strong>：Cursor、Claude Code 等工具的使用技巧</li>\n<li>⭐ <strong>AI 对后端开发的影响</strong>：AI 会淘汰初级程序员吗？最大风险是什么？</li>\n<li>⭐ <strong>未来核心竞争力</strong>：3 年后端工程师的核心竞争力是什么？</li>\n</ol>\n<h2>AI 编程 IDE 使用技巧</h2>\n<h3>用过什么 AI 编程 IDE 吗？什么感觉？</h3>\n<p>目前整体感觉是：AI 编程能力进步很快。它已经从几年前简单的代码补全，进化成了一个可以深度协作的工程助手。</p>\n<p>我总结了一套自己的使用方法论：</p>\n<ol>\n<li>在接手复杂项目或模块时，我不会直接让 AI 写代码，而是先让 Cursor 分析整个代码库，生成一份包含核心架构、模块职责和数据流的文档。这一步非常关键，因为它决定了后续协作的质量。只有当我和 AI 对项目有一致理解时，后续产出才会稳定、高质量。</li>\n<li>对于每个独立的开发任务，开启一个新的对话，并提供必要的上下文，包括需求背景、涉及模块和约束条件。这种方式能减少上下文污染，让 AI 生成的代码更精准。</li>\n<li>定期删除冗余实现和废弃代码。旧代码会误导 AI 的判断，增加上下文噪音。</li>\n</ol>\n<h3>AI 编程的核心原则</h3>\n<p>AI 是一个强大的知识库和辅助工具，可以帮我们快速实现功能、学习新知识。但如果完全依赖 AI 写代码而不理解其原理，个人技术能力可能会退化。</p>\n<p>几个原则：</p>\n<ul>\n<li>AI 生成代码之后必须人工 Review。</li>\n<li>关键逻辑必要时自己重写。</li>\n<li>核心路径必须做压测和边界测试。</li>\n</ul>\n<p>我希望效率提升，但不以牺牲技术能力为代价。</p>\n<h3>⭐ Cursor 实战技巧</h3>\n<blockquote>\n<p>这里是以 Cursor 为例，其他 AI IDE 都是类似的。</p>\n</blockquote>\n<ol>\n<li><strong>先理架构再动手</strong>：无论是自己写代码还是让 AI 生成代码，都必须先明确需求、整体架构和模块边界。如果在架构模糊的情况下直接编码，很容易出现重复实现或职责冲突，后期修改成本反而更高。</li>\n<li><strong>单 Chat 专注单功能</strong>：新功能或大改动开启新的 Chat，并在开头引入项目结构说明或关键文档作为上下文。这样可以避免历史对话干扰。</li>\n<li><strong>功能落地后写指南</strong>：让 AI 总结实现过程，抽象出通用步骤。比如新增接口的标准流程、文件导出的统一实现方式等。这些内容可以在后续类似需求中快速复用。</li>\n<li><strong>不依赖 AI，主动复盘</strong>：AI 仅作辅助，代码生成后需认真 Review，理解原理、优化不合理处。</li>\n<li><strong>定期删无用代码</strong>：清理冗余代码，减少对 AI 的误导和上下文干扰，提升开发效率。</li>\n<li><strong>用好配置文件</strong>：<code>.cursorrules</code> 定义 AI 生成代码的规则、风格和常用片段；<code>.cursorignore</code> 指定不允许 AI 修改的文件 / 目录，保护核心代码。</li>\n<li><strong>持续维护文档</strong>：项目重大变更后，让 AI 同步更新文档、记录 “踩坑” 经验。</li>\n<li><strong>让 AI 先”学”项目</strong>：大型项目先让 Cursor 分析代码库，生成含架构、目录职责、核心类的结构文档，作为后续开发的基础上下文。</li>\n</ol>\n<h3>⭐Claude Code 使用技巧</h3>\n<ol>\n<li><strong>上下文窗口是你最贵的资源</strong>——所有技巧本质上都在帮你把这块白板用得更高效。</li>\n<li><strong>先规划后执行</strong>——Plan Mode 投资的是后面的时间。</li>\n<li><strong><code>CLAUDE.md</code> 自我进化</strong>——把纠正转化为规则，让 AI 越用越顺手。</li>\n<li><strong>并行是最大的效率杠杆</strong>——多实例 + Worktree + 子代理。</li>\n<li><strong>验证优于信任</strong>——给 Claude 验收标准，让它自己检查。</li>\n<li><strong><code>/compact</code> 比反复纠正更有效</strong>——上下文被污染后，压缩或清空重来更好。</li>\n</ol>\n<p>Claude Code 详细内容我单独分享过：<a href=\"https://javaguide.cn/ai-coding/claudecode-tips.html\" target=\"_blank\" rel=\"noopener noreferrer\">Claude Code 使用指南</a>。</p>\n<h2>AI 编程对程序员的影响</h2>\n<h3>你如何看待 AI 对后端开发的影响</h3>\n<p>AI 不会取代后端工程师，但会改变后端工程师的工作方式和能力结构。</p>\n<p>AI 能帮我们处理重复的、模式化的工作：</p>\n<ul>\n<li><strong>在编码层面</strong>：AI 工具在生成<strong>模式化代码（Boilerplate）</strong>方面表现不错，CRUD、单元测试、胶水代码的编写效率可提升 50%~70%。但在<strong>分布式约束</strong>（如分布式锁的超时续租、消息队列的 Exactly-once 语义、接口幂等性设计）上，AI 存在显著的<strong>”幻觉”风险</strong>——它往往只给出 Happy Path 代码，忽略了生产环境中的异常补偿逻辑、竞态条件处理和分布式事务边界控制。</li>\n<li><strong>在架构层面</strong>：AI 正在催生新的应用范式，比如智能体（Agent）驱动的自动化业务流程，后端需要提供更灵活、更原子化的能力接口。传统的”大而全”接口正逐步拆解为可被 AI 调用的原子化能力。</li>\n<li><strong>在运维与排障层面</strong>：AI 可以辅助分析日志、监控告警，甚至预测系统瓶颈。例如，基于 AIOps 的工具可以自动分析异常日志模式，定位根因。</li>\n</ul>\n<p>AI 让后端工程师能更专注于业务建模、复杂系统设计和架构决策这些更具创造性的核心工作。</p>\n<p>拿我自己来说，我经常会和 AI 讨论业务和技术方案，它总能给我不错的启发——尤其是在需求拆解和技术选型时，AI 能提供多角度的思考。</p>\n<p>从实战经验来看，AI 辅助编程的能力可以归纳为两个维度：</p>\n<ul>\n<li><strong>从 0 到 1 的规划与交付</strong>：给出需求描述，AI 可以自主完成技术选型和架构设计，适合快速验证构想，但方案仍需人工评审。</li>\n<li><strong>既有代码的增量优化</strong>：在已有复杂度的代码库中，AI 能够理解既有架构、定位问题、完成优化。但 AI 给出的方案”看起来对”，上生产就翻车的情况并不少见。</li>\n</ul>\n<h3>前后端开发者的核心竞争力已经变了</h3>\n<p>说句实话，前后端开发者的核心竞争力已经变了。</p>\n<p>以前前端拼手速和还原度，后端拼 CRUD 和八股文。现在这些东西 AI 全能做，而且又快又不喊累，就废点 Token。你花半天切的页面，AI 十分钟搞定；你写两小时的增删改查，AI 三分钟交卷。不是说这些技能没用了，而是不稀缺了，就不值钱。</p>\n<p>前端受冲击最直接。页面还原、组件编写、样式调整，模式化程度太高，大模型最擅长这类活。但死掉的不是前端这个岗位，是“只会写页面”的前端。</p>\n<p>有竞争力的前端往两个方向走：要么往深扎——性能优化、渲染管线分析、工程化基建，AI 替代不了；要么往难走——WebGL、大规模可视化、跨端底层原理，AI 生成质量差，反而是护城河。</p>\n<p>后端稍好，但也别乐观。AI 写单个接口已经很强了，它的短板是系统级思考——服务怎么拆、数据模型怎么设计、缓存一致性怎么保证、容量瓶颈在哪。这些需要结合业务场景和技术债综合判断，AI 给的方案“看起来对”，上生产就翻车。</p>\n<p>后端的核心竞争力在往系统设计、稳定性治理、复杂业务建模转。</p>\n<p>不管前端后端，有一件事已经是基本功：高效跟 AI 协作。不是会用 ChatGPT 就行，而是能拆解问题、引导输出、判断结果靠不靠谱、识别安全隐患。你从“写代码的人”变成了“AI 的技术审核官”。</p>\n<p>那些生成代码不看逻辑的人，短期效率高，长期在给自己埋雷——线上出问题只会反复问 AI，自己毫无排查思路。</p>\n<h3>AI 会淘汰初级程序员吗</h3>\n<p>短期内不会淘汰，但会彻底改变初级程序员的能力结构。</p>\n<p>以前初级工程师的价值在于：</p>\n<ul>\n<li>写 CRUD 增删改查</li>\n<li>写基础接口</li>\n<li>写 SQL 查询语句</li>\n<li>写基础工具类/配置</li>\n</ul>\n<p>现在这些工作 AI 都能做得很好，甚至更高效、更少出错。但初级程序员不会被淘汰，只是价值创造点发生了迁移。</p>\n<p>未来初级工程师需要具备：</p>\n<ul>\n<li><strong>需求拆解能力</strong>：将模糊的业务需求转化为清晰的技术任务。</li>\n<li><strong>业务理解能力</strong>：理解领域模型和业务规则，而不仅是“翻译需求”。</li>\n<li><strong>架构感知能力</strong>：理解系统整体架构，知道自己代码在系统中的位置。</li>\n<li><strong>Prompt 表达能力</strong>：能精准地描述问题，从 AI 获取高质量答案。</li>\n</ul>\n<p>AI 让编程门槛变低，但对“理解能力”的要求反而更高。未来的初级工程师更像是一个“AI 协调者”，而非单纯的“代码编写者”。</p>\n<p>从企业招聘角度看，纯编码能力的需求会减少，但对“能利用 AI 快速交付业务价值”的工程师需求会增加。</p>\n<h3>AI 带来的最大风险是什么</h3>\n<p>我认为主要有三个层面：</p>\n<p><strong>1. 技术能力退化</strong></p>\n<p>过度依赖 AI 会导致工程师自身技术能力的退化，尤其是：</p>\n<ul>\n<li><strong>调试能力下降</strong>：习惯让 AI 排查问题，自身对底层原理的理解变浅。</li>\n<li><strong>代码敏感度下降</strong>：对“好代码”和“坏代码”的判断能力变弱，甚至不知道什么是好代码。</li>\n<li><strong>架构思维退化</strong>：长期只关注功能实现，忽视架构设计和扩展性。</li>\n</ul>\n<p><strong>2. 架构失控</strong></p>\n<p>AI 生成的代码往往关注“当前功能可用”，容易忽视长期架构健康度。这很大程度上源于 <strong>Vibe Coding（氛围编程）</strong>——依赖模糊意图让 AI“自由发挥”。</p>\n<ul>\n<li>\n<p><strong>模块边界模糊</strong>：AI 倾向于“快速完成功能”，可能将多个职责混入同一模块。建议在编码前明确模块职责（DDD 风格的 Context Boundary），通过预先定义的接口契约约束 AI 生成范围。</p>\n</li>\n<li>\n<p><strong>技术债务累积</strong>：为快速实现功能，AI 可能使用硬编码、绕过标准异常处理、引入不必要的循环依赖等反模式。这些债务在项目规模增长后会显著增加重构成本。</p>\n</li>\n<li>\n<p><strong>风格一致性缺失</strong>：不同 Chat 会话中生成的代码可能采用不同的命名规范、错误处理模式和日志格式。建议通过 <strong>Spec Coding</strong> 的方式，预先定义统一的技术规范和代码风格（如 <code>.cursorrules</code>），让 AI 始终在同一套规则下工作。</p>\n</li>\n<li>\n<p><strong>资源治理缺失</strong>：AI 不会自动考虑连接池大小、线程池队列长度、缓存过期策略等资源约束。例如，生成的代码可能创建大量线程但无界队列，在流量激增时导致内存溢出；或使用默认数据库连接池配置，在高并发下成为瓶颈。</p>\n</li>\n<li>\n<p><strong>工程规范适配</strong>：AI 生成的代码架构虽然合理，但与既有工程规范的适配往往需要人工把关。比如文件名组织、代码风格差异、依赖管理策略——这些“看起来没问题”的代码，可能在团队协作中制造麻烦。</p>\n</li>\n</ul>\n<p><strong>3. 安全风险（尤其需要重视）</strong></p>\n<ul>\n<li><strong>代码漏洞</strong>：AI 可能生成包含安全漏洞的代码，常见问题包括：\n<ul>\n<li><strong>SQL 注入</strong>：使用字符串拼接而非参数化查询</li>\n<li><strong>XSS</strong>：未对用户输入进行 HTML 转义</li>\n<li><strong>权限校验缺失</strong>：缺少接口级/方法级权限检查</li>\n<li><strong>敏感信息泄露</strong>：日志中打印密钥、Token 或密码</li>\n<li><strong>依赖漏洞</strong>：引入存在已知 CVE 的第三方库</li>\n</ul>\n</li>\n<li><strong>数据泄露</strong>：不当使用可能泄露公司代码、业务逻辑给外部模型（尤其是云端托管的 AI 服务）。</li>\n<li><strong>供应链风险</strong>：AI 推荐的依赖包可能存在已知漏洞或恶意代码。</li>\n<li><strong>密钥泄露</strong>：AI 生成的代码可能硬编码密钥、Token 等敏感信息。</li>\n</ul>\n<p><strong>4. 分布式场景下的失效模式（尤其危险）</strong></p>\n<p>AI 生成的代码在分布式环境中极易忽略关键约束，导致生产事故：</p>\n<p>| 失效模式               | AI 常见问题                    | 生产风险                               |<br>\n|</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 应用开发"
      ]
    },
    {
      "title": "Claude Code 接入第三方模型实战：JVM 智能诊断与慢查询治理",
      "url": "https://codingmap.strova.top/ai-coding/cc-glm5.1.html",
      "id": "https://codingmap.strova.top/ai-coding/cc-glm5.1.html",
      "summary": "通过 Claude Code 接入 GLM-5.1 模型，完成 JVM 智能诊断助手从零搭建和百万级数据量慢查询治理两个实战任务，分享 AI 辅助编程的工作方法与踩坑经验。重点围绕 环境准备：Claude Code 接入第三方模型、场景一：从零搭建 JVM 智能诊断 Agent、场景二：百万级数据量下的慢查询治理等核。",
      "content_html": "<p>大家好，我是 Guide。前面分享过 <a href=\"/ai-coding/idea-qoder-plugin.html\" target=\"_blank\">IDEA 搭配 Qoder 插件的实战</a>和 <a href=\"/ai-coding/trae-m2.7.html\" target=\"_blank\">Trae 接入大模型的实战</a>，分别覆盖了 JetBrains 体系和 VS Code 体系下的 AI 辅助编码。这篇换个角度，聊聊 <strong>Claude Code 接入第三方模型</strong> 的实战体验。</p>\n<p>Claude Code 本身是 Anthropic 官方的 CLI 编码工具，但它支持通过环境变量切换底层模型。这意味着你不必局限于 Claude 系列，完全可以接入其他模型来使用。本文以 GLM-5.1 作为示例，但接入方式是通用的——换成其他兼容模型，流程基本一致。</p>\n<p>我选了两个比较有代表性的复杂场景来验证：</p>\n<ul>\n<li><strong>场景一</strong>：从零搭建一个基于 Arthas 的 JVM 智能诊断 Agent，涵盖技术选型、架构设计、编码落地的完整流程</li>\n<li><strong>场景二</strong>：在百万级数据量的既有订单系统中定位并治理慢查询，考验 AI 对现有代码库的理解和增量优化能力</li>\n</ul>\n<p>一个是从零开始的工程交付，另一个是面对既有系统的性能治理，正好覆盖 AI 辅助编程的两种典型工作模式。</p>\n<h2>环境准备：Claude Code 接入第三方模型</h2>\n<p>在正式开始之前，需要完成 Claude Code 与第三方模型的对接。整个配置过程分三步：</p>\n<p><strong>第一步</strong>：安装 Claude Code</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">npm</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> i</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> -g</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> @anthropic-ai/claude-code@latest</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p><strong>第二步</strong>：安装 cc-switch 完成模型切换（macOS 用户可通过 homebrew 安装，详情参考 cc-switch 官方文档：<a href=\"https://github.com/farion1231/cc-switch/blob/main/README_ZH.md\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/farion1231/cc-switch/blob/main/README_ZH.md</a>）</p>\n<p><strong>第三步</strong>：按照模型提供方的说明，完成 Claude Code 内部模型环境变量与目标模型的对应关系配置。以 GLM-5.1 为例，参考：<a href=\"https://docs.bigmodel.cn/cn/coding-plan/tool/claude\" target=\"_blank\" rel=\"noopener noreferrer\">https://docs.bigmodel.cn/cn/coding-plan/tool/claude</a></p>\n<p>配置过程截图如下：</p>\n<p>点击加号添加模型：</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/add-model-entry.png\" alt=\"点击添加模型\"></p>\n<p>选择对应的模型：</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/select-model.png\" alt=\"选择模型\"></p>\n<p>配置参数：</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/config-params.png\" alt=\"配置参数\"></p>\n<p>Claude Code 内部模型环境变量与目标模型对应关系的 JSON 配置：</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/model-env-json-config.png\" alt=\"Claude Code 内部模型环境变量与模型对应关系 JSON 配置\"></p>\n<p>如果你更偏向页面开发，推荐通过 VSCode + Claude Code for VS Code 方式进行交互和编码验收。完成插件安装之后，可以直接在 IDE 中与模型对话和代码审查，相对于 CLI 界面会更直观一些：</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/vscode-claude-code.png\" alt=\"VSCode + Claude Code for VS Code\"></p>\n<h2>场景一：从零搭建 JVM 智能诊断 Agent</h2>\n<h3>为什么需要 JVM 智能诊断助手？</h3>\n<p>JVM 线上诊断一直以来都是 Java 开发最棘手的问题。在传统开发模式下，面对性能瓶颈或线上故障，研发人员的排查路径基本固定：</p>\n<ol>\n<li>查看 Grafana 监控面板，初步定位异常方向</li>\n<li>登录线上服务器，排查 CPU、内存、GC 等各项指标</li>\n<li>明确 Java 应用层面的问题后，启动 Arthas 执行一系列诊断指令，逐步缩小问题范围</li>\n<li>定位到具体代码段，分析根因并制定修复方案</li>\n</ol>\n<p>在 AI 出现以前，这套流程虽然繁琐，但确实是最直接有效的手段。但随着业务越来越复杂，故障响应时效要求也越来越高，传统模式的弊端越来越明显：</p>\n<ul>\n<li><strong>监控指标过于主观</strong>：面对 CPU 飙升、内存泄漏、OOM 等千奇百怪的问题，监控面板上的指标繁多，研发人员往往依赖经验做主观推断，缺乏系统化的诊断方法论</li>\n<li><strong>诊断链路过于冗长</strong>：从 Grafana 面板到线上服务器再到 Arthas 诊断，整个排查链路涉及多个工具的切换和衔接，不仅耗时，对于紧急的线上故障止血来说显得非常低效</li>\n<li><strong>高度依赖工程师经验</strong>：Arthas 确实是一款强大的 JVM 诊断利器，内置各种增强指令可以深入字节码查看运行时细节。但代价是开发人员必须熟悉各种指令参数和推理路径，才能准确完成问题定位</li>\n</ul>\n<p>随着 AI 技术的演进，特别是 Agent 和 Skill 等概念的成熟，笔者就有了一个工程化的构想：能否借助 AI 将诊断经验沉淀复用，让 AI 根据既有经验构建明确的决策路径？同时结合它的决策方案赋予对应的工具，使其基于用户给定的服务名和故障表象，自动化连接线上服务器完成诊断，定位具体代码段，最终输出问题根因和解决方案。</p>\n<h3>需求交付与架构设计</h3>\n<p>有了构想之后，接下来就是技术选型和方案落地。笔者将完整的需求描述交给 AI：</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">研发一款基于Arthas的智能体诊断工具，该工具需实现以下核心功能：</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">1.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 当用户输入线上故障服务名称及具体故障现象后，系统能够自动定位至目标故障服务器，主动对目标服务进行实时监控与深度分析。</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">2.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 通过集成Arthas的反编译功能，精准定位到引发故障的具体代码段</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">3.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 基于分析结果生成包含问题根因、代码修复建议及实施步骤的完整解决思路。</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">请提供该工具的技术选型方案，包括但不限于开发语言（优先考虑Java技术栈）、核心框架、数据库表设计、部署架构等，并设计详细的系统实现方案，涵盖功能模块划分、数据流程设计、关键技术难点及解决方案等内容。</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>AI 收到需求后，没有立刻开始写代码，而是先结合项目上下文（完全空的文件夹）进行推理分析，自主完成了一份包含十几个阶段的完整技术方案。”给一个目标，AI 自己拆出整条路径”——这是 AI 辅助编程的一大优势，你可以把精力放在需求描述和方案评审上，让 AI 负责路径规划。</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/ai-tech-plan.png\" alt=\"AI 自主完成技术方案规划\"></p>\n<p>AI 结合需求，针对 Agent 拆解出技术选型和 Arthas 集成方案的检索。从检索关键字可以看出，它在方案选取上优先考虑成熟稳定的解决方案：</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/agent-arthas-integration-research.png\" alt=\"AI 检索 Agent 技术选型和 Arthas 集成方案\"></p>\n<p>AI 检索了大量资料和 Arthas 官方文档后，输出了下面这份系统架构设计图。从上到下分三层：用户层输入服务名和故障现象，Agent 层由 Skill 引擎、Arthas HTTP Client 和 AI 分析引擎三大核心模块协同工作，最底层通过 Arthas 内置 HTTP API 对接多个目标服务实例。架构的模块划分和职责边界清晰，从故障输入到定位代码再到生成报告的完整链路设计到位：</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/system-architecture-design.png\" alt=\"AI 输出的系统架构设计图\"></p>\n<p>AI 给出了架构图之后，还进一步拆解了 6 个核心组件的职责分工——从 AI Agent Server 的流程编排，到 Arthas HTTP Client 的会话管理，到 Skill 引擎的诊断步骤链定义，再到 AI 分析引擎的报告生成，每个组件的边界和协作关系都交代得比较清楚：</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/core-component-roles.png\" alt=\"AI 输出的核心角色分工表\"></p>\n<p>最后来看最重要的数据流设计。架构设计明确之后，只要数据流链路完整清晰，基本就可以着手开发了。AI 结合一个常见的 RT 超时场景，给出了完整的诊断链路——从 Skill 匹配、诊断步骤执行、问题追踪、根因定位，到 Arthas 反编译和最终的诊断报告输出。AI 针对 Arthas HTTP API 设计了完整的会话模式交互流程（init_session → async_exec → pull_results → interrupt_job → close_session），连<code>watch</code>、<code>trace</code>这类持续监听型命令的异步轮询机制都考虑到了。这一点在评审时需要重点关注——如果 AI 对底层工具的通信模型理解有偏差，后续编码阶段就会出现问题：</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/data-flow-design.png\" alt=\"AI 输出的数据流设计\"></p>\n<p>其他细节就不多做赘述了。整体来说，架构和数据流链路都比较到位。AI 不仅针对既有需求给出了方案，还主动输出了 6 个后续扩展方向——WebSocket 实时推送、诊断知识库向量化存储、已知 Pattern 的自动修复补丁、告警联动自动触发诊断、自定义 Skill 市场、多语言支持。这些扩展方向都紧扣当前架构的技术延伸：知识库基于现有的诊断报告数据，自动修复基于已有的 Skill 引擎，告警联动基于现有的服务实例查询机制。</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/extension-suggestions.png\" alt=\"AI 给出的后续扩展建议\"></p>\n<h3>编码交付与工程结构</h3>\n<p>确认方案没有问题后，笔者直接下达开发指令：</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">整体方案没有问题，请完成开发工作吧</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>AI 收到指令后，开始自主编码。按照之前的架构设计，逐模块推进——从父 POM 和 Maven 多模块骨架搭建，到通用工具类、数据模型、数据访问层、Arthas 客户端封装、Skill 引擎、AI 分析引擎、业务逻辑层、Web 控制器，直到启动模块和部署配置，11 个子步骤全部完成：</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/ai-coding-process.png\" alt=\"AI 自主编码过程\"></p>\n<p>片刻之后，AI 完成了全部编码工作，并输出了一份详细的交付清单。9 个模块、46 个文件全部到位——从通用工具类到 7 个内置诊断 Skill，从 Arthas HTTP API 的 exec+session 双模式封装到 Spring AI Alibaba 诊断分析器，一个不少：</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/delivery-checklist.png\" alt=\"AI 完成编码后输出的交付清单\"></p>\n<p>先看整体模块结构，AI 按照 Java 多模块的标准规范完成了工程划分，从上到下严格遵循 common→model→dal→client→skill→ai→service→web→bootstrap 的依赖层级，命名规范统一。</p>\n<p>agent-skill 模块值得关注，AI 设计了 Skill 引擎的抽象接口，并内置了 7 个覆盖常见 JVM 故障场景的诊断技能（CPU 飙高、OOM、死锁、慢接口、GC 异常、线程泄漏、类找不到），每个 Skill 都定义了完整的诊断步骤链。这种”框架 + 内置实现”的设计思路，扩展性不错：</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">jvm-ai-agent/</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">├──</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> jvm-ai-agent-server/</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                 # 智能体服务端（核心）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">│</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">   ├──</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> agent-common/</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                    # 通用模块：工具类、常量、DTO</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">│</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">   ├──</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> agent-model/</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                     # 数据模型：实体、数据库映射</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">│</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">   ├──</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> agent-dal/</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                       # 数据访问层：Mapper、Repository</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">│</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">   ├──</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> agent-arthas-client/</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">             # Arthas HTTP API 客户端封装</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">│</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">   ├──</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> agent-skill/</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                     # Skill 引擎（诊断方法论）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">│</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">   ├──</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> agent-ai/</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                        # AI 分析引擎</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">│</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">   ├──</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> agent-service/</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                   # 业务逻辑层（含服务实例查询）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">│</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">   ├──</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> agent-web/</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                       # Web 层：REST API、WebSocket</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">│</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">   └──</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> agent-server-bootstrap/</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">          # 启动模块</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">│</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">└──</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> pom.xml</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                              # 父 POM</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>再看诊断核心逻辑，AI 严格按照架构设计中定义的数据流完成了完整的诊断业务链开发。整个 <code>executeDiagnosis</code> 方法按照 Skill 匹配、实例定位、诊断链执行、动态命令解析、AI 分析、报告生成的流程推进，异常处理也考虑到了非关键步骤失败时继续执行的容错策略：</p>\n<ol>\n<li><strong>Skill 匹配</strong>：通过<code>DefaultSkillMatcher</code>根据故障现象关键词匹配最佳诊断技能</li>\n<li><strong>实例定位</strong>：通过<code>ServiceInstanceLocator</code>根据服务名解析目标实例 IP 和 Arthas 端口</li>\n<li><strong>诊断链执行</strong>：遍历 Skill 定义的诊断步骤链，依次执行 Arthas 命令并收集结果</li>\n<li><strong>动态命令解析</strong>：从 Arthas 输出中提取类名、方法名等上下文变量，注入后续步骤的动态命令模板</li>\n<li><strong>AI 分析报告</strong>：将全部诊断数据交给 AI 分析引擎，生成包含根因、修复建议、严重程度的结构化报告</li>\n</ol>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> executeDiagnosis</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">DiagnosisRecord</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> record</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> DiagnosisRequest</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> request) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    try</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 1. 匹配 Skill</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        Optional</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">SkillDefinition</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">></span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> skillOpt </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> skillMatcher</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">findBestMatch</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">request</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getSymptom</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">skillOpt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">isEmpty</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">            failDiagnosis</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(record</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"无法匹配到合适的诊断技能\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        SkillDefinition</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> skill </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> skillOpt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // ......</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 2. 定位目标实例</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        ServiceRegistry</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> instance </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> instanceLocator</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">resolveInstance</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">                request</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getServiceName</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(), </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">request</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getInstanceIp</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // ......</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 3. 执行诊断步骤链</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        List</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">DiagnosticStep</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">></span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> chain </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> skill</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getDiagnosticChain</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        StringBuilder</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> allDiagnosticData </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> StringBuilder</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> decompiledCode </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        Map</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">></span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> contextVars </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> HashMap</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;></span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> i </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> i </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> chain</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">size</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">++</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">            DiagnosticStep</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> step </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> chain</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(i);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">            // ...... 初始化步骤实体</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            try</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                // 解析动态命令（支持上下文变量注入）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">                String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> command </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> resolveCommand</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(step</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> contextVars)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                // ......</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                // 执行Arthas命令并记录耗时</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">                String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> result </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> executeStep</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(host</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> port</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> step</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> command)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                // 如果是 jad 结果，记录为反编译代码</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">                if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"jad\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">equals</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">step</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getResultType</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">())</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">                    decompiledCode </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> result</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">                }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                // 从结果中提取上下文变量供后续步骤使用</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">                extractContextVars</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(result</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> contextVars)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">            } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">catch</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Exception</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                // 非关键步骤失败时继续执行</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                // ......</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">            }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">        }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 4. AI 分析</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> report </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> diagnosisAnalyzer</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">analyze</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">                request</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getSymptom</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(), </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">allDiagnosticData</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">toString</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(), decompiledCode, skill);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 5. 保存报告（从Markdown报告中提取根因、严重程度等结构化字段）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // ......</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 6. 更新诊断记录状态</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        record</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">setStatus</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">DiagnosisStatus</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">COMPLETED</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getCode</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // ......</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">catch</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Exception</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">        failDiagnosis</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(record</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getMessage</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h3>Agent 交互页面集成</h3>\n<p>在 AI 编码期间，笔者查阅了 Spring AI Alibaba 的官方文档，发现它提供了现成的 Agent Chat UI。与其让 AI 从头生成前端页面，不如直接集成这个交互组件，实现 SSE 流式输出的诊断体验。于是笔者给了一条简短的指令：</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">根据Spring</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> AI</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> Alibaba官方文档（参考链接https://java2ai.com/docs/frameworks/studio/quick-start：），实现agent智能体交互页面开发工作</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>只给了一个文档链接和一句话，AI 就自己去读官方文档、理解集成步骤、完成了页面开发。这也是使用 AI 辅助编程的一个实用技巧：当你只需要集成某个现成组件时，直接给出文档链接往往比详细描述需求更高效。</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/agent-chat-ui-integration.png\" alt=\"AI 完成 Agent Chat UI 页面集成\"></p>\n<p>到这里，一个完整的智能诊断 Agent 就构建完成了。为了验收功能，笔者在本地起了一个 CPU 飙升的测试接口：</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Slf4j</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">RestController</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> TestController</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">RequestMapping</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"cpu-100\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> cpu</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        while</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">true</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">){</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>启动 Agent 服务，访问 <code>http://localhost:{应用端口}/chatui/index.html</code>，在聊天框输入：<code>order-service 程序CPU飙升,请协助排查</code>。Agent 在收到故障表象后，完成了完整的诊断链路——先通过 Dashboard 获取概览定位到 CPU 占用最高的线程 ID，再基于线程栈帧信息定位到问题代码段，最后通过 Arthas 反编译（jad）输出热点代码并生成包含根因分析和修复建议的完整诊断报告。整个过程 Agent 全程自主完成，SSE 流式输出让每一步诊断进度都清晰可见：</p>\n<p><img src=\"https://oss.javaguide.cn/ai/coding/glm5.1-cc/agent-diagnosis-demo.png\" alt=\"Agent 诊断效果演示\"></p>\n<h2>场景二：百万级数据量下的慢查询治理</h2>\n<p>场景一验证的是 AI”从 0 到 1 的规划与交付能力”，那场景二要验证的就是另一个维度：<strong>在一个已有一定复杂度的代码库中，AI 能否准确理解既有架构、定位问题、并完成增量优化。</strong></p>\n<h3>问题定位：搜索接口耗时 18 秒</h3>\n<p>这是一个基于 Spring Boot + MyBatis 的订单查询服务（glm-testing-service），核心业务围绕订单的查询和分析展开，包含四个接口：</p>\n<p>| 接口         | 路径                           | 说明                                 |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/ai/coding/glm5.1-cc/add-model-entry.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 编程实战"
      ]
    },
    {
      "title": "Claude Code 核心命令详解：simplify、review、loop、batch、run、verify",
      "url": "https://codingmap.strova.top/ai-coding/claudecode-commands.html",
      "id": "https://codingmap.strova.top/ai-coding/claudecode-commands.html",
      "summary": "深入解析 Claude Code 核心命令，涵盖 /simplify、/review、/loop、/batch、/run、/verify、/debug 等实用命令的使用方法与实战技巧。重点围绕 先理清 Claude Code 的命令体系、/simplify：代码简化与重构、/review：代码审查等核心内容。",
      "content_html": "<p>说实话，Claude Code 里有些命令我用了一次就离不开了，但问身边朋友知道的人反而不多。这个系列文章就来聊聊这些被严重低估的命令——<code>/simplify</code>、<code>/review</code>、<code>/loop</code>、<code>/batch</code>、<code>/run</code>、<code>/verify</code>。</p>\n<p>这些命令你知道有就行了，不用硬背。打个斜杠 <code>/</code> 就出来了，比你吭哧吭哧打字快多了。</p>\n<blockquote>\n<p><strong>版本说明</strong>：本文基于 2026 年 5 月 Claude Code 官方 Commands 文档和当前客户端行为整理。Claude Code 命令更新很快，最终以 <code>/help</code>、<code>/</code> 命令列表和官方 Commands 页面为准。</p>\n</blockquote>\n<h2>先理清 Claude Code 的命令体系</h2>\n<p>Claude Code 里 <code>/</code> 开头的东西，来源有两层：</p>\n<ul>\n<li><strong>Commands（硬编码命令）</strong>——<code>/clear</code>、<code>/compact</code>、<code>/model</code>、<code>/cost</code>、<code>/help</code>、<code>/review</code>、<code>/diff</code>、<code>/context</code>、<code>/permissions</code> 等。逻辑写死在 CLI 代码里，直接与终端交互，不涉及 AI 推理，执行速度快且不消耗 Token。</li>\n<li><strong>Bundled Skills（捆绑技能）</strong>——<code>/simplify</code>、<code>/batch</code>、<code>/debug</code>、<code>/loop</code>、<code>/run</code>、<code>/verify</code>、<code>/code-review</code>、<code>/claude-api</code>。本质是基于 Prompt 的能力：调用时，Claude 会载入特定的 Markdown 指令集到上下文，然后调动子代理（Sub-agents）执行多步工作流。</li>\n</ul>\n<blockquote>\n<p><strong>注意</strong>：<code>/review</code> 是内置 PR review 命令，不是 bundled skill；深度多 Agent 审查应使用 <code>/ultrareview</code>。</p>\n</blockquote>\n<p>下面详细介绍这几个实用的内置能力。</p>\n<h2>/simplify：代码简化与重构</h2>\n<p><code>/simplify</code> 做的事很简单：审查你刚写的代码，找出隐藏的问题，然后直接帮你改掉。现在官方文档已把 <code>/simplify</code> 列为 bundled skill。</p>\n<h3>工作机制：三步走</h3>\n<p><strong>第一步：确定审查范围。</strong> 通常围绕最近变更文件工作；不带参数时，它跑 <code>git diff</code> 拿增量变更；如果工作区没有未提交的修改，它会自动审查最近一次 commit。指定具体类名时（比如 <code>/simplify MarketDataService</code>），它会读取整个文件做全量审查。具体范围以当前 Claude Code 版本行为为准。</p>\n<p><strong>第二步：并行启动三个审查 Agent。</strong> 不是串行地逐条检查，而是同时派出三个&quot;审查员&quot;，各自带着不同的视角去读同一份 diff：</p>\n<p>三个 Agent 各管一摊：</p>\n<ul>\n<li><strong>Code Reuse Agent</strong>：看你的代码是不是在重复造轮子。比如你手写了一个 <code>requireNonBlank()</code>，它会在项目里搜一圈，发现已经有一个 <code>InputValidator.requireNonBlank()</code> 做了同样的事。</li>\n<li><strong>Code Quality Agent</strong>：看代码设计有没有问题。比如同一个字符串硬编码写了三遍、两个方法长得几乎一样、一个类既管认证又管发邮件——该拆没拆、该抽象没抽象的地方，它都会指出来。</li>\n<li><strong>Efficiency Agent</strong>：看代码跑起来会不会有性能问题。比如循环里反复创建同一对象，单线程场景非要用 <code>ConcurrentHashMap</code>、该用缓存的结果每次都重新算。</li>\n</ul>\n<p><strong>第三步：汇总并修复。</strong> 三个 Agent 各自报告发现，Claude Code 会自动判断哪些是真问题、哪些是误报，然后直接动手改代码。</p>\n<blockquote>\n<p>⚠️ <strong>风险提示</strong>：<code>/simplify</code> 会应用修复，但仍建议通过 diff、测试和 review 复核，尤其是涉及事务、安全、并发的改动。它是 prompt-based skill，可能误判。</p>\n</blockquote>\n<h3>指定关注方向</h3>\n<p>也可以给它指定关注方向：</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">/simplify</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> thread</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> safety</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">/simplify</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> SQL</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> performance</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">/simplify</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> exception</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> swallowing</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">/simplify</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> MarketDataService</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>在你已经知道哪块大概有问题、想让 AI 帮你精确定位的时候，这个功能很实用。</p>\n<h3>实战案例：Spring 事务失效</h3>\n<p>有一次我写了一个用户认证模块，自测通过就准备提交了。习惯性地先跑了一遍 <code>/simplify</code>，它直接帮我找到了 6 个潜在问题，经过确认，确实都是实际存在的问题。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/claudecode/simplify-command-run.png\" alt=\"直接运行 /simplify 命令\"></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/claudecode/simplify-issues-found.png\" alt=\"扫描到的问题\"></p>\n<p>最值得说的是一个 <strong>Spring 事务失效</strong> 的问题。三个 Agent 中有两个独立地从不同角度捕获到了同一个 Bug。</p>\n<p>问题代码是这样的——<code>WatchlistService</code> 里，外层方法获取 Redis 分布式锁做 double-check，内部调一个 <code>protected</code> 方法执行数据库写入：</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> initializeDefaultWatchlist</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Long</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> userId) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // Redis 分布式锁 + double-check（幂等）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // ...</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">    doInitializeDefaultWatchlist</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(userId)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  // 同一类内部调用</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // ...</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Transactional</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">rollbackFor</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Exception</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">class</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">protected</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> doInitializeDefaultWatchlist</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Long</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> userId) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    groupService</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">save</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(defaultGroup);</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // INSERT 分组</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    stockService</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">saveBatch</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(initialStocks);</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  // INSERT 5 只股票</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>代码结构看起来合理：外层管锁和幂等，内层管事务。但 <code>@Transactional</code> 写在这实际上<strong>完全不起作用</strong>——因为 Spring AOP 基于动态代理，同一个类内部的直接调用会绕过代理，注解根本不会被拦截到。</p>\n<p>这意味着如果 <code>saveBatch</code> 中途抛异常，<code>save</code> 已经提交的分组记录不会回滚，数据库里会出现一个没有股票的空壳分组。</p>\n<blockquote>\n<p><strong>前提条件</strong>：在 Spring 默认代理式 AOP 下，同类内部直接调用会绕过代理，<code>@Transactional</code> 不会生效；如果使用 AspectJ weaving 或通过代理对象调用，结论不同。</p>\n</blockquote>\n<ul>\n<li><strong>Code Quality Agent</strong> 标记了自调用导致 <code>@Transactional</code> 失效，评为高严重性。</li>\n<li><strong>Efficiency Agent</strong> 排除了锁 TTL 不足的可能，精准定位事务失效是根因。</li>\n<li><strong>Code Reuse Agent</strong> 确认手写的分布式锁没有可复用替代，实现合理。</li>\n</ul>\n<p><code>/simplify</code> 给出的修复方案是把声明式事务换成<strong>编程式事务</strong>，用 <code>TransactionTemplate</code> 直接控制事务边界。其他修复方式包括：把事务方法移动到另一个 Spring Bean、通过代理对象调用、调整事务边界到外层 public 方法。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">RequiredArgsConstructor</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> WatchlistService</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> final</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> TransactionTemplate</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> transactionTemplate</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> doInitializeDefaultWatchlist</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Long</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> userId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        transactionTemplate</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">executeWithoutResult</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(status </span><span style=\"--shiki-light:#C18401;--shiki-dark:#C678DD\">-></span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            groupService</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">save</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(defaultGroup);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            stockService</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">saveBatch</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(initialStocks);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        });</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/claudecode/simplify-optimization-start.png\" alt=\"开启优化\"></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/claudecode/simplify-all-fixes-done.png\" alt=\"所有修改完成\"></p>\n<p>这次扫描还发现了另外 5 个问题，涵盖代码复用、安全性和效率：</p>\n<p>| 发现                                                                                       | Agent                | 修复方式                                              |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/coding/claudecode/simplify-command-run.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 编程技巧"
      ]
    },
    {
      "title": "Claude Code 使用指南：配置、工作流与进阶技巧",
      "url": "https://codingmap.strova.top/ai-coding/claudecode-tips.html",
      "id": "https://codingmap.strova.top/ai-coding/claudecode-tips.html",
      "summary": "结合 Anthropic 官方文档和真实项目用法，讲清 Claude Code 的配置、权限、MCP、Skills、Sub-Agent、上下文管理和常见工作流。重点围绕 CLAUDE.md 非常重要、权限管理要重视、用 MCP、Skills、Sub-Agent 和插件解决不同的问题、最常用的工作流 等内容展开。",
      "content_html": "<p>大家好，我是 Guide。前面写过 <a href=\"/ai-coding/idea-qoder-plugin.html\" target=\"_blank\">IDEA 搭配 Qoder 插件的实战</a>、<a href=\"/ai-coding/trae-m2.7.html\" target=\"_blank\">Trae 接入大模型的实战</a> 和 <a href=\"/ai-coding/cc-glm5.1.html\" target=\"_blank\">Claude Code 接入第三方模型的实战</a>，这篇专门把 Claude Code 的日常用法拎出来讲一遍。</p>\n<p>很多人第一次用 Claude Code，会把它当成“能在终端里聊天的 AI”。这其实低估它了。</p>\n<p>它好用的地方，是能自己读仓库、搜代码、跑命令、看报错、改文件，再根据验证结果继续迭代。你给它一句“修一下登录超时后无法刷新 token 的问题”，它会去找认证流程、读测试、改实现、跑命令。问题也在这里：如果上下文、权限和验收标准没管好，它会越跑越远，最后你面对一堆 diff 发呆。</p>\n<p>这篇文章按 2026-05-29 的 Anthropic 官方文档整理，并结合我自己的使用习惯重写。</p>\n<p>Claude Code 迭代很快，部分命令和版本门槛会变，具体以 <code>claude --version</code> 和官方文档为准。比如 <code>/run</code>、<code>/verify</code> 需要 Claude Code v2.1.145+；新版 <code>/simplify</code> 的官方说明出现在 v2.1.154+ 之后；<code>--enable-auto-mode</code> 已经移除，现在用 <code>--permission-mode auto</code>。</p>\n<p>国内使用 Claude Code 还有现实门槛：账号、网络、成本、第三方中转稳定性都要考虑。GLM、MiniMax、Kimi、DeepSeek 这类模型可以作为替代或补充，但如果任务是大规模代码修改、复杂重构、长链路排错，Claude 目前仍然是很值得单独研究的工具。</p>\n<h2><code>CLAUDE.md</code> 非常重要</h2>\n<p><code>CLAUDE.md</code> 可以理解成写给 Claude Code 的项目规则文件。</p>\n<p>它不是普通 README，也不是越长越好。Claude 每次启动都会读它，所以这里面只应该放每次会话都值得知道的东西。</p>\n<p>千万别写成项目说明书！应该写 Claude 容易猜错、代码里读不出来、团队又必须遵守的规则。重点放技术栈版本、常用命令、架构取舍、团队约定和项目坑点；别塞空话、默认行为和大段文档。</p>\n<p>判断标准很简单：这行删掉后，Claude 会不会更容易犯错？</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/claude-agents-md.png\" alt=\"多智能股票分析项目中的 CLAUDE.md 和 AGENTS.md\"></p>\n<h3>放在哪里</h3>\n<p>Claude Code 的配置有几层作用域，不同文件解决不同问题：</p>\n<p>| 作用域  | 位置                                             | 适合放什么                                       |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/coding/claude-agents-md.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 编程实战"
      ]
    },
    {
      "title": "AI 编程选 CLI 还是 IDE？一文帮你彻底搞清楚",
      "url": "https://codingmap.strova.top/ai-coding/cli-vs-ide.html",
      "id": "https://codingmap.strova.top/ai-coding/cli-vs-ide.html",
      "summary": "深度对比 Claude Code、Cursor、Kiro、TRAE 等主流 AI 编程工具，解析 CLI 与 IDE 的核心差异、适用场景与选型建议。重点围绕 先搞清楚：CLI 和 IDE 到底是什么、这场争论是怎么开始的、各有什么产品值得关注、CLI 到底强在哪 等内容展开。结合 CodingMap 知识体系梳理 。",
      "content_html": "<p>说实话，这个话题我酝酿很久了。很早就想聊聊，但一直拖着没有抽出时间写（其实就是懒！）。</p>\n<p>每次在群里聊 AI Coding 或者公众号分享 AI Coding 技巧，总有人问：&quot;Claude Code 那个黑窗口到底好在哪？我 Cursor 用得好好的为什么要换？&quot; 然后另一边马上有人回：&quot;都 2026 年了还在用 IDE？CLI 才是正道。&quot;</p>\n<p>两边都有道理，但两边说的又都不全面。今天我把自己这大半年从 IDE 到 CLI 再到两者混用的经历，结合最近行业里几款重磅产品的实际体验，一次性讲清楚。</p>\n<h2>先搞清楚：CLI 和 IDE 到底是什么</h2>\n<p>在 AI 编程的语境下，这两个词的含义和传统开发稍有不同，别搞混了。</p>\n<p><strong>AI IDE 工具</strong>，就是带图形界面的编程环境，代码编辑、运行调试，AI 对话全整合在一个窗口里。你熟悉的 Cursor、Kiro、Qoder、TRAE，Windsurf 都属于这类。其中大部分（Cursor，Windsurf、Kiro、TRAE）是基于 VS Code 二次开发的，界面风格和操作逻辑与 VS Code 一脉相承；另一类则是独立开发的原生产品，如 Zed、JetBrains + Qoder 插件。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/qoder-view.png\" alt=\"Qoder 主界面\"></p>\n<p><strong>AI CLI 工具</strong>，就是纯终端交互的命令行工具，没有图形界面。Claude Code、Codex、Qwen Code、OpenCode 都属于这类。你在终端里输入自然语言指令，AI 直接读仓库、改代码、跑测试，看报错，再改——全程在黑窗口里完成，你的角色从&quot;写代码的人&quot;变成了&quot;指挥 AI 干活的人&quot;。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/claudecode/simplify-command-run.png\" alt=\"Claude Code 运行 /simplify 命令\"></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/claudecode/simplify-optimization-start.png\" alt=\"Claude Code 开启优化代码\"></p>\n<p>一句话区分：<strong>CLI 适合&quot;告诉 AI 要什么，等它交付&quot;的场景；IDE 适合&quot;边看边改、逐行审核&quot;的场景。</strong></p>\n<p>|   维度   |           AI IDE 工具           |            AI CLI 工具             |<br>\n| :</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/coding/qoder-view.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 编程技巧"
      ]
    },
    {
      "title": "OpenAI Codex 最佳实践指南：提示工程、工具配置与安全策略",
      "url": "https://codingmap.strova.top/ai-coding/codex-best-practices.html",
      "id": "https://codingmap.strova.top/ai-coding/codex-best-practices.html",
      "summary": "综合官方文档与实战经验，系统梳理 OpenAI Codex 云端智能体和 CLI 的提示工程、工具配置、AGENTS.md 分层机制、安全模型与 API 高级特性。重点围绕 一、认识 Codex：两条产品线与一个核心理念、二、提示工程：让 Codex 高效工作的核心、三、工具配置：影响性能的关键环节、四等核心内容。",
      "content_html": "\n<p>大家好，我是 Guide。前面聊了 <a href=\"/ai-coding/claudecode-tips.html\" target=\"_blank\">Claude Code 的使用技巧</a>，这篇来看看 OpenAI 阵营的主力编程工具——<strong>Codex</strong>。</p>\n<p>OpenAI 在 2025 年推出了 Codex 系列产品线，涵盖基于 o3 模型的云端软件工程智能体（codex-1）和开源的终端编码助手 Codex CLI。它和传统的代码补全不同，能自己读代码、跑测试、提 PR，完成从理解到交付的完整闭环。但想让它真正好用，提示工程、工具配置、安全策略这几环缺一不可。</p>\n<p>这篇文章综合 OpenAI 官方博客、Codex CLI 开源仓库 README、官方提示工程指南等多个来源，整理成一份实践指南。通过本文你将搞懂：</p>\n<ol>\n<li>⭐ <strong>Codex 云端智能体和 CLI 的定位差异</strong>：各适合什么场景</li>\n<li>⭐ <strong>提示工程的核心原则</strong>：行动优先、上下文收集、代码质量标准</li>\n<li>⭐ <strong><a href=\"http://AGENTS.md\" target=\"_blank\" rel=\"noopener noreferrer\">AGENTS.md</a> 的分层机制</strong>：怎么组织项目级指令</li>\n<li><strong>安全模型的三级审批</strong>：从建议到全自动的安全边界</li>\n<li><strong>GPT-5.3 Codex API 的高级特性</strong>：上下文压缩、Phase 机制、推理强度</li>\n</ol>\n<h2>一、认识 Codex：两条产品线与一个核心理念</h2>\n<h3>Codex 云端智能体（codex-1）</h3>\n<p>OpenAI 发布了基于 o3 模型微调的 codex-1 云端智能体。它运行在 OpenAI 的安全沙箱中，可以读写代码、运行测试和命令行工具，甚至直接提交 Pull Request。三个核心特性：</p>\n<ul>\n<li><strong>自主执行</strong>：你给出任务描述，它自行收集上下文、编写代码、运行测试，全程无需人工逐步引导</li>\n<li><strong>安全沙箱</strong>：每个任务在独立的容器环境中运行，没有网络访问权限，防止对生产环境造成影响</li>\n<li><strong><a href=\"http://AGENTS.md\" target=\"_blank\" rel=\"noopener noreferrer\">AGENTS.md</a> 指令机制</strong>：类似于 <code>.cursorrules</code> 或 <code>CLAUDE.md</code>，你可以在仓库中放置 <a href=\"http://AGENTS.md\" target=\"_blank\" rel=\"noopener noreferrer\">AGENTS.md</a> 文件来定义项目级别的编码规范和约束</li>\n</ul>\n<p>Codex 云端智能体目前通过 ChatGPT Pro、Business 和 Enterprise 计划提供访问，Plus 计划也于 2025 年 6 月起陆续开放。它支持两种工作模式：交互式对话和后台任务。后台模式下，你可以同时派发多个任务，每个任务在独立容器中并行执行。</p>\n<blockquote>\n<p>一句话区分：<strong>云端智能体适合“挂后台跑大任务”，CLI 适合“坐电脑前盯着改代码”。</strong> 两者定位不同，核心理念一致——长期自主、减少人工干预、以可交付的代码为目标。</p>\n</blockquote>\n<h3>Codex CLI：开源终端编码助手</h3>\n<p>Codex CLI 是一个完全开源的终端工具，用 Rust 编写，可以在本地机器上执行代码修改和 shell 命令。跟云端智能体的区别主要在运行环境和安全模型上：</p>\n<p>| 维度     | Codex 云端智能体             | Codex CLI                        |<br>\n|</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 编程实战"
      ]
    },
    {
      "title": "DeepSeek V4 + Claude Code 实战：代码能力深度测评",
      "url": "https://codingmap.strova.top/ai-coding/deepseek-v4-claude-code.html",
      "id": "https://codingmap.strova.top/ai-coding/deepseek-v4-claude-code.html",
      "summary": "深入体验 DeepSeek V4 与 Claude Code 的集成，实测代码审计、数据库迁移、模型升级等多个场景，评估 V4-Pro 和 V4-Flash 的真实代码能力。重点围绕 Claude Code 接入 DeepSeek V4、实战一：升级 LLM 多 Provider 预设模型列表等核心内容。",
      "content_html": "<p>这几天 AI 圈基本被一件事刷屏了——DeepSeek V4 发布，同步开源。从技术报告里的 benchmark 数据到社区的实测反馈，到处都在讨论。</p>\n<p>开源模型在对话和写作上已经做得相当成熟，各家你追我赶，迭代速度肉眼可见。但 Agent Coding 是另一回事。</p>\n<p>让模型自主分析项目结构、理解多文件依赖、给出能直接落地的工程方案——这种活没有捷径，全靠硬实力。</p>\n<p>之前各家模型在这个方向上一直在进步，但实际用过就知道，离&quot;放心交给它独立完成&quot;始终还差那么一点。</p>\n<p>所以这次 V4 发布，Guide 第一反应就是直接接入 Claude Code 上手干活。</p>\n<p>这篇文章接近 <strong>7000 字</strong>，建议收藏，通过本文你将搞懂：</p>\n<ol>\n<li><strong>Claude Code 接入 DeepSeek V4 的两种方式</strong>：配置文件法 + CC Switch 可视化切换</li>\n<li><strong>五个真实开发任务的实战记录</strong>：V4-Pro 干起活来到底怎么样</li>\n<li><strong>DeepSeek V4-Pro 和 Flash 的核心参数与定价</strong>：值不值得切</li>\n<li><strong>场景建议</strong>：什么时候该用，什么时候先观望</li>\n</ol>\n<h2>Claude Code 接入 DeepSeek V4</h2>\n<p>Claude Code 强在它的工具链和执行力，但 Claude 官方模型太贵，加上现在 Claude 太容易封号。这次 DeepSeek V4 提供了一个 <strong>Anthropic 兼容接口</strong>，这意味着 Claude Code 可以直接对接 DeepSeek，不需要任何第三方适配层。</p>\n<h3>方式一：配置文件法（推荐）</h3>\n<p>如果你本机没有安装 Claude Code 的话，先运行下面这行命令安装（Node.js 18+）：</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">npm</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> install</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> -g</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> @anthropic-ai/claude-code</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>编辑或新增 Claude Code 配置文件 <code>~/.claude/settings.json</code>，添加 <code>env</code> 字段，把后端地址、模型和 API Key 都写进去：</p>\n<div class=\"language-json line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"json\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-json\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"env\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">    \"ANTHROPIC_AUTH_TOKEN\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"your_deepseek_api_key\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">    \"ANTHROPIC_BASE_URL\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"https://api.deepseek.com/anthropic\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">    \"ANTHROPIC_MODEL\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"DeepSeek-V4-Pro\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">    \"API_TIMEOUT_MS\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"3000000\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">    \"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"1\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>注意替换 <code>your_deepseek_api_key</code> 为你的 DeepSeek API Key。</p>\n<p>API Key 创建地址：<a href=\"https://platform.deepseek.com/\" target=\"_blank\" rel=\"noopener noreferrer\">https://platform.deepseek.com/</a> 。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/deepseek-api-keys.png\" alt=\"DeepSeek 创建 API Key\"></p>\n<p>如果你使用的是 DeepSeek-V4-Flash，把 <code>ANTHROPIC_MODEL</code> 改为 <code>DeepSeek-V4-Flash</code> 即可。</p>\n<p>配置完成后启动 Claude Code：</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">claude</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>首次启动需要选择信任当前文件夹。</p>\n<h3>方式二：CC Switch（可视化切换）</h3>\n<p>如果你想在 DeepSeek、Claude、MiniMax 等多个 Provider 之间灵活切换，推荐安装 <strong>CC Switch</strong>。这是一个专门管理 Claude Code 模型切换的小工具，支持一键横跳，还支持管理 Skills、MCP 和提示词。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/cc-switch-main-interface.png\" alt=\"CC Switch 主界面\"></p>\n<p>启动 CC Switch，点击右上角 <strong>&quot;+&quot;</strong> ，选择自定义供应商，Base URL 填写 <code>https://api.deepseek.com/anthropic</code>，API Key 填写你的 DeepSeek API Key。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/deepseek-v4/cc-switch-add-deepseek-provider.png\" alt=\"CC Switch 添加 DeepSeek Provider\"></p>\n<p>将模型名称改为 <code>DeepSeek-V4-Pro</code>（或 <code>DeepSeek-V4-Flash</code>），完成后点击右下角的&quot;添加&quot;。</p>\n<h3>验证是否生效</h3>\n<p>直接在命令行输入 <code>claude</code> 或者进入 Claude Code 界面之后再次输入 <code>/status</code> 确认，model 为 <code>DeepSeek-V4-Pro</code> 即表示接入成功。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/deepseek-v4/verify-deepseek-v4-ready.png\" alt=\"验证是否生效\"></p>\n<p>之后你就可以用 DeepSeek V4-Pro 来驱动 Claude Code 的所有能力了。</p>\n<h2>实战一：升级 LLM 多 Provider 预设模型列表</h2>\n<p>我手头有一个多智能体股票分析项目，已经快一个月没启动了。这次重新启动，第一件事就是把过时的模型配置更新掉。</p>\n<p>项目 Settings 页面之前只有一个纯文本输入框让用户手动填写模型名，不够友好。</p>\n<p>我需要做两件事：<strong>搜索各家 LLM 的最新模型版本</strong>，然后<strong>给前端加一个下拉选择</strong>。</p>\n<p>提示词很简单：</p>\n<blockquote>\n<p>/tavily-search 搜索当前 deepseek、glm 和 openai 最新的模型，然后调整全局配置中默认模型推荐和示例。并且，当前这几个 LLM 图标太 AI 味了，帮我换一个上档次点。</p>\n</blockquote>\n<p>任务不大，但有个细节值得说——如果不配 <code>/tavily-search</code> Skill，单纯靠大模型的训练数据截止日期来猜最新版本，大概率会出错。我之前用其他模型没配 Tavily 的时候，反复提示了好几遍才把各家最新模型版本搞对。</p>\n<p>关于 Tavily 的使用可以参考：<a href=\"https://mp.weixin.qq.com/s/kAk7lLVgYzZrD9xJs3AUkQ\" target=\"_blank\" rel=\"noopener noreferrer\">Claude Code 对接 AI Agent 搜索引擎 Tavily 实现高质量搜索</a>。</p>\n<p>DeepSeek V4-Pro <strong>一次搞定</strong>。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/deepseek-v4/search-and-update-latest-models.png\" alt=\"搜索并更新最新 LLM 模型\"></p>\n<p>模型配置全部更新成功，各家推荐的模型示例都切到了最新版本。改了三个文件：</p>\n<ol>\n<li><strong><code>application.yml</code></strong>——新增 DeepSeek 预设 Provider，GLM 默认模型升级到 <code>glm-5</code></li>\n<li><strong><code>.env.example</code></strong>——补上 DeepSeek 环境变量，Kimi 默认改为 <code>kimi-k2.6</code></li>\n<li><strong><code>SettingsPage.tsx</code></strong>——加了 <code>PROVIDER_PRESETS</code> 常量，Model 和 Embedding Model 改成 combo box</li>\n</ol>\n<p>最终四个 Provider 的推荐模型列表（截至 2026.04.25）：</p>\n<p>| Provider  | 推荐模型                                                        |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/coding/deepseek-api-keys.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 编程实战"
      ]
    },
    {
      "title": "IDEA + Qoder 插件多场景实战：接口优化与代码重构",
      "url": "https://codingmap.strova.top/ai-coding/idea-qoder-plugin.html",
      "id": "https://codingmap.strova.top/ai-coding/idea-qoder-plugin.html",
      "summary": "通过两个真实实战案例，展示 IDEA 搭配 Qoder 插件在深分页优化、祖传代码重构等场景下的实际效果，分享从执行者到指挥者的工作模式转变。重点围绕 Qoder JetBrains 插件上手教程、能力拆解：Qoder 在这个示例中做了什么、总结 等内容展开。结合 CodingMap 知识体系梳理 AI 编程 / I。",
      "content_html": "<p>大家好，我是 Guide。如果你是 JetBrains IDE 的重度用户，大概率有过这样的纠结：想用 AI 辅助编程，但主流工具——Cursor、Trae、Qoder——大多基于 VS Code。切过去？舍不得 JetBrains 调试和重构体验。不切？又感觉错过了 AI 的效率红利。</p>\n<p>有朋友会说：Claude Code、Gemini CLI 这些终端工具不是挺香的吗？确实香，但说实话，CLI 模式也有明显的短板：没有原生 UI 交互，看代码、审 diff 都不够直观。虽然可以通过一些开源项目（如 vibe kanban、1Code）来缓解，但在做复杂项目时，还是存在一些局限性。</p>\n<p>现在的后端开发者，大致分成了四大阵营：</p>\n<p>| 阵营           | 工具组合                                        | 特点                         |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/coding/qoder/idea-plugin/plugin-install-interface.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 编程实战"
      ]
    },
    {
      "title": "AI 编程必备 Skills 推荐：TDD、代码审查与网页自动化实战",
      "url": "https://codingmap.strova.top/ai-coding/programmer-essential-skills.html",
      "id": "https://codingmap.strova.top/ai-coding/programmer-essential-skills.html",
      "summary": "实战分享 6 个 AI 编程 Skills 工具，覆盖 TDD 开发流程、代码审查、UI 设计、网页自动化与 Skill 开发，让 AI 编程 Agent 真正成为生产力利器。重点围绕 Superpowers、Everything Claude Code、UI UX Pro Max、sanyuan-skills 等内。",
      "content_html": "<p>之前写了篇<a href=\"/ai/agent/skills.html\" target=\"_blank\">万字详解 Agent Skills</a>，聊了 Skills 是什么、怎么用、和 Prompt / MCP 有什么区别。这篇不聊概念，直接分享 6 个我日常在用的 Skills，覆盖开发流程、代码审查、UI 设计、网页操作这些场景：</p>\n<ul>\n<li>让 AI 自动遵循 TDD 流程，先写测试再写实现</li>\n<li>一键生成符合行业标准的设计系统</li>\n<li>对代码进行多维度专业审查（SOLID、安全性、性能）</li>\n<li>解决 AI 聊太久会”失忆”的上下文腐化问题</li>\n<li>给 AI 加上完整的网页浏览和自动化操作能力</li>\n</ul>\n<p>下面一个个来看。</p>\n<h2>Superpowers</h2>\n<p>Superpowers 是一个专为 AI 编程 Agent（Claude Code、Cursor 等）设计的软件开发工作流框架，把 TDD、Code Review、Spec-Driven、Git Worktree、子 Agent 协作等实践封装成 Skills。内置的核心技能如下：</p>\n<p>| 技能名称                           | 触发方式                       | 核心功能                                                                                       |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/superpowers/superpowers-download.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 编程实战"
      ]
    },
    {
      "title": "Spec Coding 规范驱动编程实战：从 Vibe Coding 到 AI 代码规范",
      "url": "https://codingmap.strova.top/ai-coding/spec-coding.html",
      "id": "https://codingmap.strova.top/ai-coding/spec-coding.html",
      "summary": "系统梳理 Spec Coding 规范驱动编程的核心思路与落地流程，涵盖 Vibe Coding 与 Spec Coding 的区别、四步落地方法、AI IDE 规范文件配置、三色标签权限控制、Spec 分层管理和多代理协作避坑经验。重点围绕 Vibe Coding 不是不能用、Spec Coding 到底是什么等核。",
      "content_html": "<p>你好，我是小 G。拖了蛮久，来填坑了。</p>\n<p>Spec Coding 很早之前就有群友提到说建议写一下。确实还蛮重要的，工作中能用到，面试也开始问了。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/cs-basics/network/readers-urging-spec-coding-to-be-written.png\" alt=\"spec coding 被读者催写\"></p>\n<p>上周和同事聊天，他问我还在折腾 Spec Coding 干嘛。原话大概是：“Claude Code 都能自己写代码了，你花时间写规范不是多此一举？”</p>\n<p>我当时没忍住怼了一句：“AI 写出来的屎山代码，你来维护？”</p>\n<p>他愣了一下。</p>\n<p>说实话我理解他的想法。AI 写代码确实快，扔一句需求过去，几秒钟一个函数就出来了，跑起来还挺像那么回事。Demo、脚本、一次性页面，这么搞没毛病。</p>\n<p>但问题是——你把这套玩法搬到一个多人协作、要长期维护的项目里，过两周再回来看那段代码，你大概率不想碰，也不敢碰。因为需求里没写清的部分，AI 自己脑补了；边界条件你没提，它按“常见写法”猜了一个；你们团队的错误码格式、权限校验约定，AI 一个都不知道。它只是按照训练数据里出现频率最高的方案，给你拼了一段“看起来能跑”的代码。</p>\n<p>这篇文章聊 Spec Coding 的核心思路，内容不少，建议收藏。通过本文你将搞懂：</p>\n<ol>\n<li>Vibe Coding 和 Spec Coding 的实际差别，以及什么时候该用哪个</li>\n<li>完整的 Spec Coding 落地流程，从写需求到让 AI 按规矩执行</li>\n<li>Spec 在主流 AI IDE（Cursor、Claude Code、Copilot 等）里怎么配、怎么管、怎么防止 AI 越界</li>\n</ol>\n<h2>Vibe Coding 不是不能用</h2>\n<p>Vibe Coding（氛围编程），凭感觉走。给 AI 一句模糊的意图，它就直接开始输出代码。</p>\n<p>Karpathy 最早提这个词时，说的也是那种把需求丢给 AI、顺着感觉不断调整、甚至暂时不太管代码细节的写法。</p>\n<p>Vibe Coding 不是原罪。下面这些场景，用它反而很合适：</p>\n<ul>\n<li>验证一个想法，先写个 Demo 看看效果</li>\n<li>写一次性脚本，跑完就扔</li>\n<li>做内部小工具，影响面很小</li>\n<li>AI 写完后，有完整测试兜底，而且不直接暴露给外部用户</li>\n</ul>\n<p>这些情况下，硬写一大堆 Spec 反而是浪费时间。</p>\n<p>真正的问题是：很多人验证完想法之后，顺手就把 Vibe 出来的代码推上了生产。</p>\n<p>这就不一样了。</p>\n<p>Demo 阶段你可以靠感觉走，因为错了就改，坏了就删。生产代码不行，它后面会接数据库、接支付、接用户数据、接别人的维护成本。你今天省下来的半小时，可能会变成后面几天的排查时间。</p>\n<p>我的判断标准就一条： <strong>这段代码要活多久？</strong></p>\n<ul>\n<li><strong>两天就扔掉</strong>的脚本，Vibe 够了。写 Spec 反而拖效率。</li>\n<li><strong>3-5 天</strong>这种中间地带，可以写轻量 Spec。不用展开完整设计，只写关键约束和验收标准，半小时差不多能搞定。</li>\n<li><strong>超过一周</strong>的代码，只要需要别人维护、涉及数据持久化、接入外部接口，就别裸 Vibe。至少要把约束、边界和验收标准写清楚。</li>\n</ul>\n<p>而且，很多时候搭配轻量级 Spec 也没问题，不需要太死板。</p>\n<p>轻量 Spec 可以简单到这种程度：</p>\n<div class=\"language-markdown line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"markdown\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-markdown\"><span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">## 任务目标</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">实现一个订单导出接口，支持按时间范围导出 CSV。</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">## 关键约束</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> 单次导出最多 5000 条</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> 时间范围不能超过 31 天</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> 必须校验用户权限，只能导出当前租户的数据</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> 查询必须命中 order_tenant_time_idx 联合索引</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> 导出失败要记录失败原因，不能只返回 unknown error</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">## 验收标准</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> 正常导出 CSV，字段顺序符合产品约定</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> 超过 5000 条时返回明确错误</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> 越权租户数据不能被导出</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> 单元测试覆盖空时间、越界时间、无权限、无数据四种场景</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h2>Spec Coding 到底是什么</h2>\n<p>Spec Coding，直译过来叫规范驱动编程。简单来说就是：先把规范写清楚，再让 AI 干活。</p>\n<p>平时让 AI 写代码，很多人会直接丢一句：</p>\n<blockquote>\n<p>帮我做一个用户系统。</p>\n</blockquote>\n<p>AI 当然能写，而且看起来还挺像那么回事。但问题也在这里：你没告诉它用户系统到底长什么样，它就只能自己猜。</p>\n<p>用户怎么注册？邮箱能不能重复？密码怎么存？接口失败时返回什么格式？哪些功能这期不做？管理员有没有禁用用户的能力？这些东西如果一开始没写清楚，AI 不会停下来反问你，它大概率会先补一套自己觉得合理的方案。</p>\n<p>Spec Coding 做的事情，就是把这些规则提前写下来。</p>\n<p>这里的 Spec 不是随便写两句需求，而是一份 AI 能照着执行的技术约定。接口、数据结构、错误码、边界条件、安全要求、技术栈限制，甚至哪些操作不允许碰，都要写在里面。</p>\n<p>它和 Vibe Coding 的差别，也就在这。</p>\n<p>Vibe Coding 更像是你给 AI 一个大方向，然后让它自由发挥。代码生成出来以后，你再去验收、改 bug、补细节。短平快的小脚本这么干没啥问题，甚至很爽。</p>\n<p>但项目稍微复杂一点，就容易出事。等你发现 AI 理解错了，代码已经写了一堆。你回头查，也很难说清楚到底是需求没讲明白，还是 AI 自己乱发挥。</p>\n<p>简单总结下 Spec Coding 和 Vibe Coding 的差别：<strong>AI 的行为是由你定义的，还是由它猜的？</strong></p>\n<h2>四步落地</h2>\n<p>一般会把 Spec Coding 拆成四步：Specify、Plan、Tasks、Implement。</p>\n<p>| 阶段          | 干什么   | 产出              | 关键动作                         |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/cs-basics/network/readers-urging-spec-coding-to-be-written.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 编程技巧"
      ]
    },
    {
      "title": "Trae + MiniMax 多场景实战：Redis 故障排查与跨语言重构",
      "url": "https://codingmap.strova.top/ai-coding/trae-m2.7.html",
      "id": "https://codingmap.strova.top/ai-coding/trae-m2.7.html",
      "summary": "使用 Trae IDE 接入 MiniMax 大模型，通过 Redis 连接池故障排查和 Redis C 源码到 Go 跨语言重构两个真实场景，分享 AI 辅助编程的实战经验与工作技巧。重点围绕 快速上手：Trae 接入大模型、场景一：接口超时问题快速止血与根因定位、场景2：从Redis C源码到Go实现的跨语言重构。",
      "content_html": "<p>大家好，我是 Guide。前面分享过一篇 <a href=\"/ai-coding/idea-qoder-plugin.html\" target=\"_blank\">IDEA 搭配 Qoder 插件的实战</a>，那篇主要讲在 JetBrains 体系内用 AI 辅助编码。这篇换个角度，聊聊 <strong>Trae IDE 接入大模型</strong> 的实战体验。</p>\n<p>Trae 是字节跳动推出的 AI 编程 IDE，基于 VS Code 生态，支持接入多种大模型。本文使用 MiniMax M2.7 作为示例，但 Trae 的接入方式是通用的——换成 Claude、GPT 等其他模型，流程基本一致。</p>\n<p>我这里使用 MiniMax 是因为我刚好订阅了 MiniMax Code Plan 想要实际测试一些，并非广告，你可以换成其他模型，思路都是一样的。</p>\n<p>我选了两个比较有代表性的复杂场景来实际验证：</p>\n<ul>\n<li><strong>场景一</strong>：接口突然大量超时，日志只指向 Redis，但项目里多处都在用 Redis，很难快速定位根因。</li>\n<li><strong>场景二</strong>：把 Redis 的慢查询指令从 C 语言源码完整复刻到 Go 实现，考验跨语言重构和上下文理解能力。</li>\n</ul>\n<h2>快速上手：Trae 接入大模型</h2>\n<p>Trae 支持接入多种大模型，下面以接入自定义模型为例，演示通用配置流程。</p>\n<p><strong>第一步</strong>：到 Trae 官网下载安装并完成初始化，同时到对应模型平台完成注册和 API Key 创建（本文示例使用 MiniMax 平台）：</p>\n<p><a href=\"https://platform.minimaxi.com/subscribe/token-plan\" target=\"_blank\" rel=\"noopener noreferrer\">https://platform.minimaxi.com/subscribe/token-plan</a></p>\n<p><strong>第二步</strong>：在 Trae 中点击&quot;Add Model&quot;添加自定义模型：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/trae-add-model-entry.png\" alt=\"Trae添加模型入口\"></p>\n<p><strong>第三步</strong>：选择&quot;Other Models&quot;并手动输入模型 ID 和 API Key：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/select-other-models.png\" alt=\"选择Other Models\"></p>\n<p><strong>第四步</strong>：输入模型 ID（如 <code>MiniMax-M2.7</code>）和申请的 API Key，点击&quot;Add Model&quot;。若无报错提示，即表示接入成功：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/input-minimax-m2.7-api-key.png\" alt=\"输入模型ID和API Key\"></p>\n<p>接入完成后，就可以在 Trae 中使用该模型进行 AI 辅助编程了。接下来通过两个实战场景，分享具体的使用方式和技巧。</p>\n<h2>场景一：接口超时问题快速止血与根因定位</h2>\n<h3>问题定位</h3>\n<p>第一个案例是某次真实线上故障的复现（已脱敏）。当时部门同学反馈某列表查询接口报错，页面无数据。线上监控系统定位到接口信息如下：</p>\n<p>接口：<code>GET http://localhost:8080/api/rbac/user/list</code></p>\n<p>返回结果：</p>\n<div class=\"language- line-numbers-mode\" data-highlighter=\"shiki\" data-ext style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-\"><span class=\"line\"><span>{</span></span>\n<span class=\"line\"><span>    \"code\": 500,</span></span>\n<span class=\"line\"><span>    \"message\": \"系统繁忙，请稍后重试\",</span></span>\n<span class=\"line\"><span>    \"data\": null,</span></span>\n<span class=\"line\"><span>    \"timestamp\": \"2026-03-19T10:11:02.632242\"</span></span>\n<span class=\"line\"><span>}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>结合异常堆栈信息关键字<code>Read timed out</code>，以及对应代码段的<code>get(key)</code>操作，我们可以初步认为该报错只是表象并非根因。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Override</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> getConfigValue</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> configKey</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> environment) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> cacheKey </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> CONFIG_CACHE_PREFIX </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> configKey </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \":\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> environment</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> value </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> stringRedisTemplate</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">opsForValue</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">().</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(cacheKey);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (value </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> value</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 后续逻辑省略</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>按照常规处理流程，我们需要快速定位问题根因、完成止血，再联系运维深入排查。但项目中多处用到Redis，逐一排查耗时长，期间可能影响业务稳定性。</p>\n<p>为了验证 AI 辅助排查的实际效果，笔者复刻了该故障场景（已脱敏），让模型接手处理。按照企业级线上故障处理流程，首先需要定位根因并完成止血。于是向模型下达了第一条指令：</p>\n<div class=\"language- line-numbers-mode\" data-highlighter=\"shiki\" data-ext style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-\"><span class=\"line\"><span>针对访问 http://localhost:8080/api/rbac/user/list 接口时出现的500错误（错误信息：\"系统繁忙，请稍后重试\"），请执行以下操作：</span></span>\n<span class=\"line\"><span>1. 分析提供的异常堆栈信息，准确定位导致服务器内部错误的根本原因；</span></span>\n<span class=\"line\"><span>2. 提供详细的线上紧急止血方案，包括但不限于：临时回滚策略、流量限制措施、服务降级方案或紧急重启流程；</span></span>\n<span class=\"line\"><span>3. 解释错误产生的技术原因，指出具体的代码模块或配置问题；</span></span>\n<span class=\"line\"><span></span></span>\n<span class=\"line\"><span>...... 异常堆栈关键信息：`java.net.SocketTimeoutException: Read timed out`</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-diagnostic-instruction.png\" alt=\"向M2.7下达的诊断指令截图\"></p>\n<p>模型收到请求后，很快定位到指定代码的上下文，并推理出4种可能的根因：</p>\n<ul>\n<li>Redis 服务器宕机或无响应</li>\n<li>连接池配置太小，高并发下耗尽</li>\n<li>Redis 连接泄漏（连接未正确关闭）</li>\n<li>Redis 服务器负载过高</li>\n</ul>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-inference-result.png\" alt=\"M2.7推理结果截图\"></p>\n<p>到这一步，模型已经把问题空间从&quot;N处Redis调用&quot;压缩到了&quot;4种可能根因&quot;——这种<strong>快速收敛问题范围</strong>的能力，是 AI 辅助排查的核心价值。接下来看它的止血思路。</p>\n<h3>止血</h3>\n<p>模型针对既定异常栈帧快速梳理了代码调用逻辑，准确地指出：列表查询接口被切面拦截，连接池耗尽是500错误的根因。另外一个关键点，它指出了这段代码缺乏降级策略——这一点笔者是在复盘会上才意识到的。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-call-chain-analysis.png\" alt=\"M2.7代码调用链路分析截图\"></p>\n<p>针对线上问题，止血策略是最关键的环节。模型给出了几个解决方案，第一个就是临时关闭权限校验开关——原因在于方案一需要清除Redis缓存数据。虽然方案有些激进，不过，它详细指出了代码的调用链路和表结构信息，这也能很好地辅助我通过业务语义猜测可能的场景和原因。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-call-chain-analysis-2.png\" alt=\"M2.7调用链路分析\"></p>\n<p>基于模型提供的调用链路信息，笔者进一步询问方案一的技术依据，确保业务理解上快速对齐：</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">结合代码开发的完整工作流程，详细阐述方案一的技术依据、设计思路及实施合理性。</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>这也是让笔者比较满意的地方，模型给出了问题代码的调用链路图，让我快速了解到列表查询期间所经过的完整切面和具体故障所处位置，帮助理解当前问题的影响面以及本次异常的直接原因。</p>\n<p>经过不到10分钟的交互，笔者不仅迅速获得一个宏观的架构视角，理解了当前复杂架构的故障和各解决方案的依据，例如方案一：通过修改数据库配置重启刷新缓存来规避权限校验。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-call-chain-diagram.png\" alt=\"M2.7调用链路图截图\"></p>\n<p>我们再来看看方案三的思路：当Redis不可用时，使用本地缓存或默认值，避免级联失败。模型结合当前工程代码段给出了修改建议：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-solution-3-code.png\" alt=\"M2.7方案三代码片段\"></p>\n<p>模型分析后，我们对问题有了初步的判断：Redis客户端连接池耗尽，导致日常业务接口基于缓存开关查询逻辑崩溃，进而引发雪崩效应。综合模型的多个建议，本着保守、快速止血、业务高峰期不压垮数据库的原则，得出以下hotfix方案：</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">根据提供的方案，创建一个hotfix止血分支，用于紧急修复Redis异常问题。具体实施步骤如下：</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">1.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 基于当前生产环境代码创建hotfix分支，命名规范为\"hotfix/redis-exception-handler\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">2.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 按照方案三实现Redis异常捕获机制，在所有Redis操作处添加try-catch块</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">3.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 当捕获到Redis异常时，自动降级为直接查询数据库获取数据</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">4.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 实现JVM本地缓存机制，将查询结果缓存至内存中，设置合理的缓存过期时间</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">5.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 完成单元测试和集成测试，覆盖率需达到80%以上</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">6.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 准备回滚方案，确保在紧急情况下能够快速恢复到上一版本</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/hotfix-instruction.png\" alt=\"hotfix方案指令\"></p>\n<p>模型收到指令后，准确理解了问题，完成任务拆解并逐步执行：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-task-breakdown.png\" alt=\"M2.7任务拆解过程\"></p>\n<p>最终输出的代码结果如下：模型在原有权限校验逻辑中整合了数据库降级查询，对权限校验逻辑的理解和复杂设计的整合做得比较到位。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Around</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"permissionCheck()\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Object</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> checkPermission</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">ProceedingJoinPoint</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> joinPoint) throws Throwable {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    try</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 从配置中心读取权限校验开关</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> checkEnabled </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> configService</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getConfigValue</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"permission.check.enabled\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"PROD\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"true\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">equalsIgnoreCase</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(checkEnabled)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> joinPoint</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">proceed</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">        }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // ... 原有权限校验逻辑 ...</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 尝试从Redis缓存获取权限信息</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        Boolean</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> hasPermission </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> checkPermissionFromCache</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(redisKey)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (hasPermission </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">            // ... 命中缓存处理 ...</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">        }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 降级：从数据库查询权限</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        boolean</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> hasPermissionFromDB </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> checkPermissionFromDatabase</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(userId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> apiPath</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> httpMethod)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // ... 降级逻辑处理 ...</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">catch</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Exception</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (e </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">instanceof</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> RuntimeException </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x26;&#x26;</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"无权限访问\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">equals</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getMessage</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">())</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            throw</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 发生异常时，触发监控告警并采用保守策略放行</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        AlertManager</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">notify</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"PERMISSION_CHECK_ERROR\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getMessage</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> joinPoint</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">proceed</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>getConfigValue同样补充了本地缓存逻辑，多级缓存设计在容错处理上做得不错。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">/**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * 获取配置值（指定环境）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Override</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> getConfigValue</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> configKey</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> environment) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> cacheKey </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> CONFIG_CACHE_PREFIX </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> configKey </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \":\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> environment</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 【第一步：尝试从本地缓存获取】</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> localValue </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> localCacheManager</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(cacheKey);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (localValue </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> localValue</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 【第二步：尝试从Redis获取】</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    try</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">isRedisAvailable</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">()) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">            String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> value </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> stringRedisTemplate</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">opsForValue</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">().</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(cacheKey);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (value </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">                localCacheManager</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">put</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(cacheKey, value, LOCAL_CACHE_TTL);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">                return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> value</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">            }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">catch</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Exception</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // Redis异常，降级到数据库</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">        handleRedisFailure</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(e)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 【第三步：降级到数据库】</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // ... 其他逻辑 ...</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> getConfigValueFromDatabaseWithFallback</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(configKey</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> environment)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>这其中值得注意的一个细节是本地缓存的设计：模型采用开闭原则，基于ConcurrentHashMap完成了本地缓存工具类的封装，考虑到了堆内存溢出风险，配合LRU算法实现缓存清理：</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Component</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> LocalCacheManager</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 核心存储：ConcurrentHashMap保证线程安全</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> final</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Map</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> CacheEntry</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">></span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> cache </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ConcurrentHashMap</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;></span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> final</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ScheduledExecutorService</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> cleanupExecutor</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 缓存配置</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> final</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> long</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> DEFAULT_TTL_MILLIS </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 300000</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> // 5分钟</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> final</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> long</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> MAX_CACHE_SIZE </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 10000</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> LocalCacheManager</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 守护线程执行定时清理</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        this</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">cleanupExecutor</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Executors</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">newSingleThreadScheduledExecutor</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(r </span><span style=\"--shiki-light:#C18401;--shiki-dark:#C678DD\">-></span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">            Thread</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> t</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> Thread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(r, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"local-cache-cleanup\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            t</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">setDaemon</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">true</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> t;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        });</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        this</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">cleanupExecutor</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">scheduleAtFixedRate</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">this</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">::</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">cleanupExpiredEntries, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">TimeUnit</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">MINUTES</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> put</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> key</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> value</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">        put</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(key, value, DEFAULT_TTL_MILLIS);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> put</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> key</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> value</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">long</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> ttlMillis</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 容量满时触发LRU清理</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">cache</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">size</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">>=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> MAX_CACHE_SIZE) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">            cleanupExpiredEntries</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">cache</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">size</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">>=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> MAX_CACHE_SIZE) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">                evictOldestHalf</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">            }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        cache</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">put</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(key, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> CacheEntry</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(value, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">currentTimeMillis</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> ttlMillis));</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> get</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> key</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        CacheEntry</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> entry</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> cache</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(key);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (entry </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">==</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> ||</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> entry</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">isExpired</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            cache</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">remove</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(key);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            return</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> entry</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getValue</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // ... 其他方法省略 ...</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // LRU清理：删除最老的50%数据</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> evictOldestHalf</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // ...... 省略排序和清理逻辑 ......</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 缓存条目</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> CacheEntry</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> final</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> value</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> final</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> long</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> expirationTime</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        public</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> CacheEntry</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> value</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">long</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> expirationTime</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            this</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">value</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> value;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            this</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">expirationTime</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> expirationTime;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> getValue</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> value;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> boolean</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> isExpired</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">currentTimeMillis</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">></span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> expirationTime;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h3>根因定位</h3>\n<p>通过hotfix分支针对线上故障止血之后，我们再来深入排查Redis连接池耗尽的原因。按照模型的输出结果和推断，一个常规的get指令操作按照Redis 10w qps的性能表现来看，10个连接（平均每个指令1~2ms），理想情况下每秒处理约6600条指令，远低于Redis的极限处理能力，所以问题可能出在代码层面，我们需要进一步推断项目中是否存在不合理的Redis操作：</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">结合本次发生的具体故障现象和表现特征，对项目进行全面的系统性全局分析。分析范围应覆盖项目架构、代码实现、依赖管理、环境配置、数据交互等多个维度，重点识别并输出可能导致生产故障的直接原因。</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-global-analysis-instruction.png\" alt=\"M2.7全局分析指令\"></p>\n<p>此时模型开始基于全局项目结构和上下文进行详细的阅读和推理分析：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-project-structure-analysis.png\" alt=\"M2.7项目结构分析\"></p>\n<p>最终模型给出了详细的故障分析报告，指出根因：不当的Redis数据结构设计使用scan操作导致连接池夯死。同时，还结合上下文给出了该操作的业务流程，便于我们迅速理解这条故障链路：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-root-cause-analysis.png\" alt=\"M2.7故障根因分析\"></p>\n<p>而解决方案也是非常干净利落，通过优化数据结构的方式降低Redis读写操作的时间复杂度，避免连接池夯死：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-optimization-suggestion.png\" alt=\"M2.7优化方案建议\"></p>\n<p>场景一整体体验不错。从N处Redis调用中精准定位根因，到给出完整止血方案，整个推理链条清晰完整。</p>\n<p>不过也发现了一些问题：它给出的方案一（清除Redis缓存）略显激进，实际生产环境可能需要更保守的策略。另外，部分边界条件的防御性代码还是需要人工补充——AI能帮你走到90%，剩下的10%还得靠自己。</p>\n<h2>场景2：从Redis C源码到Go实现的跨语言重构</h2>\n<h3>背景说明</h3>\n<p>接下来我们再来一个高难度场景——复刻Redis慢查询指令。mini-redis是采用Go语言goroutine-per-connection理念提升吞吐量，并以C语言的风格实现符合RESP协议的缓存中间件，由于语言在设计理念上存在偏差，涉及复杂逻辑梳理和异构方案落地。用于验证大模型的跨语言架构设计能力再合适不过。</p>\n<h3>需求梳理与方案设计</h3>\n<p>针对项目重构类需求，按传统开发流程，我们需要大量时间阅读源代码梳理逻辑，期间因历史原因代码无注释，需结合上下文推理调试。了解原有逻辑后，还需结合新项目架构制定实施步骤，并设计单元测试确保既有逻辑稳定运行。整个流程（研发、测试到发布）保守估计需要3个工作日。抱着试试看的心态，笔者将源代码阅读和技术文档整理工作交给 AI 负责。</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">我现在需要通过Go语言复刻Redis慢查询指令的实现。请你详细阅读Redis源代码，深入理解慢查询功能的完整实现原理、数据结构设计、处理流程和关键步骤。具体包括但不限于：慢查询日志的存储机制、慢查询阈值的配置与调整、慢查询命令的收集与记录流程、相关API接口的设计与实现，以及慢查询信息的查询与展示方式。请基于这些理解，整理出清晰的技术文档，包括核心原理说明、关键数据结构分析、实现步骤分解以及可能的性能优化考量。</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>等待片刻后，模型明确指出技术要求，自底向上地介绍数据结构到执行链路，进行了详尽的分析和介绍：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-slowlog-data-structure.png\" alt=\"M2.7慢查询数据结构分析\"></p>\n<p>查看其对慢查询切面逻辑的定位非常准确，在主流程上输出了必要的注释，让我快速了解慢查询的整体处理流程：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-slowlog-aspect-logic.png\" alt=\"M2.7慢查询切面逻辑\"></p>\n<p>再看其对slot get指令的理解，也非常到位，思路和资深开发一样，抓大放小，明确核心逻辑，在主流程上输出必要的注释：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-slot-get-instruction.png\" alt=\"M2.7 slot get指令分析\"></p>\n<p>确认模型对慢查询有了准确的理解后，接下来让它以开发专家的视角进行功能拆解、落地、测试回归的完整设计文档：</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">按照测试驱动开发(TDD</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)方法论，使用Go语言创建一个全面详细的开发教程文档，指导复刻Redis的实现。该教程必须符合以下规范：</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">1.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 开发方法：</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 严格执行测试驱动开发工作流程：先编写会失败的测试，然后实现最简代码以通过测试，最后进行重构</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 采用类似于原始Redis</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> C语言实现的面向过程的编程风格</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 尽可能使用纯Go语法和标准库</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">2.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 教程结构：</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 从项目设置和环境配置说明开始</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 按Redis功能拆分为逻辑模块进行开发</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 针对每个模块/特性，提供：</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">     a.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 明确的测试用例定义，包含预期输入和输出</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">     b.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 逐步的代码实现，附带逐行解释</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">     c.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 明确的测试命令和验证流程</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">     d.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 预期测试结果和成功标准</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">3.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 技术要求：</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 包含所有组件的完整代码片段</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 指定确切的文件结构和命名规范</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 详细说明编译和测试命令</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 解释常见问题的调试流程</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 在适用时参考相关的Redis</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> C源代码模式</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">4.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 实现细节：</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 从核心数据结构（字符串、列表、哈希等）开始</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 逐步推进到命令处理和协议实现</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 包含网络层和客户端-服务器通信</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 涵盖持久化机制（RDB/AOF）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 按照相同的行为模式实现基本的Redis命令</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">5.</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 测试要求：</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 为每个组件提供完整的测试代码</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 解释测试断言和验证方法</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 包含单元测试和集成测试</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 指定如何运行测试并解读结果</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">   -</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 详细说明如何根据Redis规范验证正确行为</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">该教程应足够全面，让具备中级Go知识的开发者能够按照指定方法成功构建一个功能类似的Redis系统。</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>等待片刻后，我们收到一份设计文档。模型结合Redis源代码上下文，梳理出慢查询的核心脉络和关键定义，并规划出完整的开发步骤：<br>\n<img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-slowlog-design-doc.png\" alt=\"慢查询设计文档\"></p>\n<h3>编码实现</h3>\n<p>我们从Redis源代码中抽取设计文档后，为确保C语言工程的设计思路能在个人Go语言项目工程规范中准确落地，将其复制到mini-redis项目，让模型分析方案的可行性和修改建议：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-feasibility-analysis.png\" alt=\"M2.7可行性分析\"></p>\n<p>等待片刻后模型完成文档最后的可行性分析和整理，我们开始对其设计方案进行进一步的复核确认。从项目概述上可以看到，模型针对mini-redis项目结构进行了分析，准确地定位到慢查询可以直接复用的链表结构体并完成文档微调：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-linked-list-structure.png\" alt=\"M2.7链表结构体分析\"></p>\n<p>再来看看最关键的数据结构实现思路，模型也结合mini-redis的编码规范，生成了Go语言风格的结构体：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-go-style-struct.png\" alt=\"M2.7 Go风格结构体\"></p>\n<p>针对慢查询时间测量，有个细节值得提一下。个人实现的指令处理入口和原生Redis有些设计上的出入：由于Go语言语法糖特性，笔者对指针、指针函数以及文件编排做了特殊处理。模型准确地基于笔者的协程模型定位到时间测量的切面，完成前置计时和后置统计，实现慢查询监控。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-time-measurement-aspect.png\" alt=\"M2.7时间测量切面\"></p>\n<p>最后就是核心的慢查询指令实现，无论是参数解析还是指令查询和响应处理函数，模型都结合笔者的当前项目封装的逻辑给出了明确的编码方案：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-slowlog-command-implementation.png\" alt=\"M2.7慢查询指令实现\"></p>\n<p>经过仔细复核设计文档，整体开发思路基本一致，但在代码组织细节上仍有调优空间——例如模型将<code>slowlog</code>指令独立成文件，而未遵循项目惯例统一放入<code>command.go</code>。考虑到慢查询功能并非核心内存读写指令，且其日志管理逻辑相对独立，这一处理也算合理折中。权衡之后，我们决定保留模型的实现方式，同时手动调整部分文件布局以符合既有工程规范，随后推进剩余开发工作。</p>\n<p>这一细节也说明：AI生成的代码架构虽然合理，但与既有工程规范的适配仍然需要人工把关。</p>\n<p>另外提一句，整个慢查询功能的实现过程中，模型有两次生成了不符合项目风格的代码（比如错误处理方式），需要手动调整。这不是大问题，但说明完全依赖AI生成还是不行的。</p>\n<h3>验收</h3>\n<p>因为笔者明确指定了TDD的开发模型，所以模型在这期间结合输出反馈和文档说明完成自循环修复，最终结合mini-redis的项目风格完成了慢查询指令的复刻。</p>\n<p>得益于 AI 的推理和重构能力，在验收过程中我们有了更多的构思空间。之前一直因为源代码梳理总结和技术验收成本过大，导致 redis.conf 配置加载逻辑一直没有实现。</p>\n<p>因为笔者需要将慢查询时间设置为0，方便对慢查询指令做最后的验收工作，所以笔者索性再次对其提出加载配置的需求：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/m2.7-config-loading.png\" alt=\"M2.7配置加载实现\"></p>\n<p>整个逻辑梳理和开发工作不到1小时，笔者顺利完成了慢查询指令复刻和验收，为了演示慢查询功能，将mini-redis的慢查询阈值设置为0：</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"># 慢查询阈值（微秒）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"># 执行时间超过此值的命令会被记录到慢查询日志中</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"># 负值表示禁用慢查询日志，0 表示记录所有命令</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"># 默认值：10000（10毫秒）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">slowlog-log-slower-than</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>启动mini-redis服务端后，键入slowlog get 默认返回空：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/slowlog-get-initial-state.png\" alt=\"slowlog get初始状态\"></p>\n<p>执行简单的set操作后，键入slowlog get，这条指令如预期被判定为慢查询指令并输出：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/slowlog-get-record-set-command.png\" alt=\"slowlog get记录set命令\"></p>\n<p>同理，我们依次键入后续几条指令，也都准确按照链表头插法入队，实现按照时间降序排列输出：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/slowlog-get-multiple-records.png\" alt=\"slowlog get多条记录\"></p>\n<h2>实战总结：AI 辅助编程的工作流思考</h2>\n<p>通过两个典型场景的实战，总结一下使用 Trae + 大模型辅助编程的一些经验和思考。</p>\n<h3>AI 辅助编程能做什么</h3>\n<p>在上述两个场景中，AI 辅助编程体现了几个核心能力：</p>\n<p>| 能力维度       | 场景表现                                 | 说明                                     |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/coding/m2.7/trae-add-model-entry.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 编程实战"
      ]
    },
    {
      "title": "分布式系统知识体系：理论、协议、RPC、网关、分布式锁、事务与 ID",
      "url": "https://codingmap.strova.top/distributed-system/",
      "id": "https://codingmap.strova.top/distributed-system/",
      "summary": "分布式系统面试与学习路线，涵盖 CAP、BASE、Paxos、Raft、ZAB、Gossip、一致性哈希、RPC、API 网关、分布式锁、分布式事务和 ZooKeeper。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章、分布式 等内容展开。结合 CodingMap 知识体系梳理 分布式系统 的核心概念、实践方法。",
      "content_html": "<p>这份 <strong>分布式系统知识体系</strong> 面向后端学习、系统设计和面试复习，按“理论基础 -&gt; 通信调用 -&gt; 服务治理 -&gt; 一致性与协调 -&gt; 工程实践”的顺序整理本站分布式相关文章。</p>\n<p>如果你时间有限，建议先看 <a href=\"/distributed-system/distributed-system-interview-questions.html\" target=\"_blank\">分布式系统面试题总结</a>，快速建立高频问题清单；如果你想系统补基础，可以按下面的专题顺序阅读。</p>\n<h2>适合谁看</h2>\n<ul>\n<li>正在系统学习分布式系统的后端开发者。</li>\n<li>准备校招、社招、中大厂后端面试的同学。</li>\n<li>想补齐分布式理论、工程实践和方案选型能力的工程师。</li>\n<li>已经写过业务代码，但对 RPC、分布式锁、分布式事务、配置中心等底层原理不够熟的读者。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>分布式系统为什么需要在一致性、可用性、性能和复杂度之间做取舍？</li>\n<li>CAP、BASE、Paxos、Raft、ZAB、Gossip、一致性哈希分别解决什么问题？</li>\n<li>RPC、API 网关、配置中心、注册中心在微服务体系里分别承担什么职责？</li>\n<li>分布式 ID、分布式锁、分布式事务在真实业务里有哪些常见方案和坑？</li>\n<li>面试中如何把“概念、原理、场景、方案对比、落地经验”串成完整回答？</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/distributed-system/distributed-system-interview-questions.html\" target=\"_blank\">分布式系统面试题总结</a>：先建立高频问题清单，知道面试最常考哪些点。</li>\n<li><a href=\"/distributed-system/protocol/cap-and-base-theorem.html\" target=\"_blank\">CAP 定理与 BASE 理论详解</a>：理解分布式系统最核心的取舍逻辑。</li>\n<li><a href=\"/distributed-system/rpc/rpc-intro.html\" target=\"_blank\">RPC 远程过程调用详解</a>：掌握服务之间如何通信，以及 RPC 框架解决了哪些工程问题。</li>\n<li><a href=\"/distributed-system/distributed-id.html\" target=\"_blank\">分布式 ID 生成方案详解</a>、<a href=\"/distributed-system/distributed-lock.html\" target=\"_blank\">分布式锁入门</a>、<a href=\"/distributed-system/distributed-transaction.html\" target=\"_blank\">分布式事务解决方案详解</a>：补齐高频工程实践。</li>\n<li><a href=\"/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.html\" target=\"_blank\">ZooKeeper 入门指南</a> 和 <a href=\"/distributed-system/distributed-configuration-center.html\" target=\"_blank\">分布式配置中心详解</a>：理解分布式协调和配置治理。</li>\n</ol>\n<h2>核心文章</h2>\n<h3>分布式理论与协议</h3>\n<p>这部分适合先建立分布式系统的底层认知，重点理解一致性、可用性、分区容错、共识算法和数据分布。</p>\n<ul>\n<li><a href=\"/distributed-system/protocol/\" target=\"_blank\">分布式理论、算法与协议专题</a>：把 CAP、BASE、Paxos、Raft、ZAB、Gossip 和一致性哈希放在同一条学习线上。</li>\n<li><a href=\"/distributed-system/protocol/cap-and-base-theorem.html\" target=\"_blank\">CAP 定理与 BASE 理论详解</a>：理解一致性、可用性、分区容错和最终一致性。</li>\n<li><a href=\"/distributed-system/protocol/raft-algorithm.html\" target=\"_blank\">Raft 算法详解</a>：用更易理解的共识算法入门 Leader 选举和日志复制。</li>\n<li><a href=\"/distributed-system/protocol/paxos-algorithm.html\" target=\"_blank\">Paxos 算法详解</a>：补齐经典共识算法的角色、流程和难点。</li>\n<li><a href=\"/distributed-system/protocol/zab.html\" target=\"_blank\">ZAB 协议详解</a>：理解 ZooKeeper 的原子广播、崩溃恢复和事务日志机制。</li>\n<li><a href=\"/distributed-system/protocol/gossip-protocol.html\" target=\"_blank\">Gossip 协议详解</a>：理解大规模节点之间的信息传播和最终一致性。</li>\n<li><a href=\"/distributed-system/protocol/consistent-hashing.html\" target=\"_blank\">一致性哈希算法详解</a>：理解分布式缓存、负载均衡和分库分表中的数据分布问题。</li>\n</ul>\n<h3>RPC 与服务调用</h3>\n<p>RPC 解决的是远程服务调用的工程复杂度，包括序列化、网络传输、服务发现、负载均衡、超时重试和服务治理。</p>\n<ul>\n<li><a href=\"/distributed-system/rpc/\" target=\"_blank\">RPC 专题</a>：从 RPC 基础、Dubbo 到 HTTP 与 RPC 的关系，建立服务调用完整认知。</li>\n<li><a href=\"/distributed-system/rpc/rpc-intro.html\" target=\"_blank\">RPC 远程过程调用详解</a>：理解 RPC 调用流程、动态代理、序列化、网络传输和框架选型。</li>\n<li><a href=\"/distributed-system/rpc/dubbo.html\" target=\"_blank\">Dubbo 面试题总结</a>：串联 Dubbo 架构、服务暴露与引用、SPI、负载均衡和集群容错。</li>\n<li><a href=\"/cs-basics/network/http-vs-rpc.html\" target=\"_blank\">有了 HTTP 协议，为什么还要 RPC？</a>：厘清 HTTP 和 RPC 的层次关系与选型边界。</li>\n</ul>\n<h3>API 网关与流量入口</h3>\n<p>API 网关负责统一接入、路由转发、认证鉴权、限流熔断、灰度发布和跨域处理，是微服务体系里的重要入口层。</p>\n<ul>\n<li><a href=\"/distributed-system/api-gateway.html\" target=\"_blank\">API 网关详解</a>：理解请求路由、认证鉴权、限流熔断、负载均衡和双层网关架构。</li>\n<li><a href=\"/distributed-system/spring-cloud-gateway-questions.html\" target=\"_blank\">Spring Cloud Gateway 面试题总结</a>：掌握 Predicate、GatewayFilter、GlobalFilter、限流熔断和常见生产问题。</li>\n</ul>\n<h3>分布式 ID、锁与事务</h3>\n<p>这部分偏工程落地，常见于订单、支付、秒杀、库存扣减、数据一致性和跨服务协作场景。</p>\n<ul>\n<li><a href=\"/distributed-system/distributed-id.html\" target=\"_blank\">分布式 ID 生成方案详解</a>：对比 UUID、数据库自增、号段模式、Redis、Snowflake、Leaf、Tinyid 等方案。</li>\n<li><a href=\"/distributed-system/distributed-id-design.html\" target=\"_blank\">分布式 ID 设计实战</a>：结合订单号、支付码、优惠券、一码付等业务场景理解业务 ID 设计。</li>\n<li><a href=\"/distributed-system/distributed-lock.html\" target=\"_blank\">分布式锁入门</a>：理解互斥语义、锁粒度、安全释放、超时续约和 Fencing Token。</li>\n<li><a href=\"/distributed-system/distributed-lock-implementations.html\" target=\"_blank\">分布式锁实现方案详解</a>：对比 Redis、Redisson、Redlock、ZooKeeper 和 Curator 分布式锁。</li>\n<li><a href=\"/distributed-system/distributed-transaction.html\" target=\"_blank\">分布式事务解决方案详解</a>：系统理解 XA、AT、TCC、Saga、本地消息表、事务消息和最大努力通知。</li>\n</ul>\n<h3>配置中心与 ZooKeeper</h3>\n<p>配置中心和 ZooKeeper 主要解决服务配置、注册发现、分布式协调和集群一致性问题。</p>\n<ul>\n<li><a href=\"/distributed-system/distributed-configuration-center.html\" target=\"_blank\">分布式配置中心详解</a>：对比 Apollo、Nacos、Spring Cloud Config 和 Kubernetes ConfigMap。</li>\n<li><a href=\"/distributed-system/distributed-process-coordination/zookeeper/\" target=\"_blank\">ZooKeeper 专题</a>：从 ZooKeeper 核心概念讲到 ZAB、Leader 选举、Curator 和分布式锁实践。</li>\n<li><a href=\"/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.html\" target=\"_blank\">ZooKeeper 入门指南</a>：掌握 ZNode、Watcher、ACL 和典型应用场景。</li>\n<li><a href=\"/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.html\" target=\"_blank\">ZooKeeper 进阶详解</a>：理解 ZAB 协议、Leader 选举、集群部署和会话管理。</li>\n<li><a href=\"/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.html\" target=\"_blank\">ZooKeeper 实战教程</a>：通过 Docker、zkCli、四字命令和 Curator 完成实践。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>分布式系统为什么不能同时完美满足一致性、可用性和分区容错？</li>\n<li>CAP 和 BASE 是什么关系？最终一致性适合哪些场景？</li>\n<li>Paxos、Raft、ZAB 有什么区别，分别用在什么地方？</li>\n<li>RPC 和 HTTP 有什么区别？为什么内部服务调用常用 RPC？</li>\n<li>分布式 ID 如何保证全局唯一、趋势递增和业务可读？</li>\n<li>Redis 分布式锁有哪些坑？Redlock 是否一定可靠？</li>\n<li>分布式事务有哪些方案？TCC、Saga、本地消息表分别适合什么场景？</li>\n<li>配置中心和注册中心有什么区别？Apollo、Nacos、Spring Cloud Config 如何选型？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/system-design/\" target=\"_blank\">系统设计</a></li>\n<li><a href=\"/high-availability/\" target=\"_blank\">高可用系统设计</a></li>\n<li><a href=\"/high-performance/\" target=\"_blank\">高性能系统设计</a></li>\n<li><a href=\"/cs-basics/network/\" target=\"_blank\">计算机网络</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "分布式"
      ]
    },
    {
      "title": "API 网关详解：核心功能、工作原理与 Spring Cloud Gateway / Kong / APISIX 选型",
      "url": "https://codingmap.strova.top/distributed-system/api-gateway.html",
      "id": "https://codingmap.strova.top/distributed-system/api-gateway.html",
      "summary": "API 网关核心知识详解，涵盖请求路由、认证鉴权、限流熔断、负载均衡、灰度发布、双层网关架构，以及 Spring Cloud Gateway、Kong、APISIX、ShenYu 等常见网关选型对比。重点围绕 什么是网关？、网关能提供哪些功能？、有哪些常见的网关系统？、如何选择？、API 网关 等内容展开。",
      "content_html": "<h2>什么是网关？</h2>\n<p>API 网关（API Gateway）是位于客户端与后端服务之间的<strong>统一入口</strong>，所有客户端请求先经过网关，再由网关路由到具体的目标服务。</p>\n<h3>核心价值</h3>\n<p>在微服务架构下，一个系统被拆分为多个服务。像<strong>安全认证、流量控制、日志、监控</strong>等功能是每个服务都需要的。如果没有网关，我们需要在每个服务中单独实现这些功能，导致：</p>\n<ul>\n<li><strong>代码重复</strong>：相同逻辑在多个服务中冗余实现</li>\n<li><strong>管理分散</strong>：缺乏统一的配置和监控视图</li>\n<li><strong>维护成本高</strong>：功能变更需要修改所有服务</li>\n</ul>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway-overview.png\" alt=\"网关示意图\"></p>\n<h3>核心职责</h3>\n<p>网关的功能虽然繁多，但核心可以概括为两件事：</p>\n<p>| 职责         | 说明                                | 典型功能                               |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway-overview.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "分布式"
      ]
    },
    {
      "title": "分布式配置中心详解：Apollo、Nacos、Spring Cloud Config 与 K8s ConfigMap 对比",
      "url": "https://codingmap.strova.top/distributed-system/distributed-configuration-center.html",
      "id": "https://codingmap.strova.top/distributed-system/distributed-configuration-center.html",
      "summary": "分布式配置中心原理与选型详解，涵盖 Apollo、Nacos、Spring Cloud Config、Kubernetes ConfigMap 的架构差异、配置推送机制、灰度发布、高可用设计和面试高频考点。重点围绕 为什么要用配置中心？、常见的配置中心有哪些？如何选择？、配置中心、注册中心与 K8s ConfigMa。",
      "content_html": "<h2>为什么要用配置中心？</h2>\n<p>微服务架构下，应用被拆分为大量独立部署的服务，每个服务都有自己的配置（服务地址、数据库参数、功能开关等）。配置项数量会随着服务数量、环境数量和集群数量一起增长。传统配置文件方式存在以下问题：</p>\n<ul>\n<li><strong>修改需重启</strong>：无论配置在代码库还是外部文件中，很多应用都需要重启进程才能让新配置生效。</li>\n<li><strong>与发版耦合</strong>：如果配置放在代码库中，配置变更往往要跟代码发版绑定，难以独立灰度和回滚。</li>\n<li><strong>安全性不足</strong>：敏感配置（数据库密码、API Key）直接写在代码库中容易泄露。</li>\n<li><strong>缺乏权限控制</strong>：无法对配置的查看、修改、发布等操作进行细粒度权限管控。</li>\n<li><strong>配置分散难管理</strong>：多环境（开发/测试/生产）、多集群的配置分散在各处，难以统一维护。</li>\n</ul>\n<p>此外，配置中心通常提供以下增强能力：</p>\n<ul>\n<li><strong>版本管理</strong>：记录每次配置变更的修改人、修改时间、修改内容，支持一键回滚。</li>\n<li><strong>灰度发布</strong>：先将配置推送给部分实例验证，降低变更风险（Apollo、Nacos 1.1.0+ 支持）。</li>\n</ul>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/config-center/view-release-history.png\" alt=\"Apollo 配置中心\"></p>\n<p>当然，不是所有系统都需要上配置中心。单体应用、单环境、配置项很少且变更频率低的场景，<code>application-{profile}.yml</code>、环境变量或 Kubernetes ConfigMap + 滚动重启通常就够了。配置中心会带来额外的运维成本、故障域和排查链路，小团队或低频配置场景不必过度工程化。</p>\n<h2>常见的配置中心有哪些？如何选择？</h2>\n<p>| 方案                                                                | 状态       | 特点                                   |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/config-center/view-release-history.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "分布式"
      ]
    },
    {
      "title": "分布式 ID 设计实战：订单号、优惠券、一码付与业务 ID 生成策略",
      "url": "https://codingmap.strova.top/distributed-system/distributed-id-design.html",
      "id": "https://codingmap.strova.top/distributed-system/distributed-id-design.html",
      "summary": "分布式 ID 设计实战指南，结合订单号、支付码、优惠券、一码付等业务场景，讲解全局唯一 ID 的设计原则、业务语义、容量规划、可读性和生成策略。重点围绕 场景一：订单系统、场景二：Tracing、场景三：短网址、分布式 ID 等内容展开。结合 CodingMap 知识体系梳理 分布式系统 / Distributed 。",
      "content_html": "<div class=\"hint-container tip\">\n<p class=\"hint-container-title\">提示</p>\n<p>看到百度 Geek 说的一篇结合具体场景聊分布式 ID 设计的文章，感觉挺不错的。于是，我将这篇文章的部分内容整理到了这里。原文传送门：<a href=\"https://mp.weixin.qq.com/s/bFDLb6U6EgI-DvCdLTq_QA\" target=\"_blank\" rel=\"noopener noreferrer\">分布式 ID 生成服务的技术原理和项目实战</a> 。</p>\n</div>\n<p>网上绝大多数的分布式 ID 生成服务，一般着重于技术原理剖析，很少见到根据具体的业务场景去选型 ID 生成服务的文章。</p>\n<p>本文结合一些使用场景，进一步探讨业务场景中对 ID 有哪些具体的要求。</p>\n<h2>场景一：订单系统</h2>\n<p>我们在商场买东西一码付二维码，下单生成的订单号，使用到的优惠券码，联合商品兑换券码，这些是在网上购物经常使用到的单号，那么为什么有些单号那么长，有些只有几位数？有些单号一看就知道年月日的信息，有些却看不出任何意义？下面展开分析下订单系统中不同场景的 id 服务的具体实现。</p>\n<h3>1、一码付</h3>\n<p>我们常见的一码付，指的是一个二维码可以使用支付宝或者微信进行扫码支付。</p>\n<p>二维码的本质是一个字符串。聚合码的本质就是一个链接地址。用户使用支付宝微信直接扫一个码付钱，不用担心拿支付宝扫了微信的收款码或者用微信扫了支付宝的收款码，这极大减少了用户扫码支付的时间。</p>\n<p>实现原理是当客户用 APP 扫码后，网站后台就会判断客户的扫码环境。（微信、支付宝、QQ 钱包、京东支付、云闪付等）。</p>\n<p>判断扫码环境的原理就是根据打开链接浏览器的 HTTP header。任何浏览器打开 http 链接时，请求的 header 都会有 User-Agent(UA、用户代理)信息。</p>\n<p>UA 是一个特殊字符串头，服务器依次可以识别出客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等很多信息。</p>\n<p>各渠道对应支付产品的名称不一样，一定要仔细看各支付产品的 API 介绍。</p>\n<ol>\n<li>微信支付：JSAPI 支付支付</li>\n<li>支付宝：手机网站支付</li>\n<li>QQ 钱包：公众号支付</li>\n</ol>\n<p>其本质均为在 APP 内置浏览器中实现 HTML5 支付。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-design-pay-one-card.png\" alt=\"文库会员支付示例\"></p>\n<p>文库的研发同学在这个思路上，做了优化迭代。动态生成一码付的二维码预先绑定用户所选的商品信息和价格，根据用户所选的商品动态更新。这样不仅支持一码多平台调起支付，而且不用用户选择商品输入金额，即可完成订单支付的功能，很丝滑。用户在真正扫码后，服务端才通过前端获取用户 UID，结合二维码绑定的商品信息，真正的生成订单，发送支付信息到第三方（qq、微信、支付宝），第三方生成支付订单推给用户设备，从而调起支付。</p>\n<p>区别于固定的一码付，在文库的应用中，使用到了动态二维码，二维码本质是一个短网址，ID 服务提供短网址的唯一标志参数。唯一的短网址映射的 ID 绑定了商品的订单信息，技术和业务的深度结合，缩短了支付流程，提升用户的支付体验。</p>\n<h3>2、订单号</h3>\n<p>订单号在实际的业务过程中作为一个订单的唯一标识码存在，一般实现以下业务场景：</p>\n<ol>\n<li>用户订单遇到问题，需要找客服进行协助；</li>\n<li>对订单进行操作，如线下收款，订单核销；</li>\n<li>下单，改单，成单，退单，售后等系统内部的订单流程处理和跟进。</li>\n</ol>\n<p>很多时候搜索订单相关信息的时候都是以订单 ID 作为唯一标识符，这是由于订单号的生成规则的唯一性决定的。从技术角度看，除了 ID 服务必要的特性之外，在订单号的设计上需要体现几个特性：</p>\n<p><strong>（1）信息安全</strong></p>\n<p>编号不能透露公司的运营情况，比如日销、公司流水号等信息，以及商业信息和用户手机号，身份证等隐私信息。并且不能有明显的整体规律（可以有局部规律），任意修改一个字符就能查询到另一个订单信息，这也是不允许的。</p>\n<p>类比于我们高考时候的考生编号的生成规则，一定不能是连号的，否则只需要根据顺序往下查询就能搜索到别的考生的成绩，这是绝对不可允许。</p>\n<p><strong>（2）部分可读</strong></p>\n<p>位数要便于操作，因此要求订单号的位数适中，且局部有规律。这样可以方便在订单异常，或者退货时客服查询。</p>\n<p>过长的订单号或易读性差的订单号会导致客服输入困难且易错率较高，影响用户体验的售后体验。因此在实际的业务场景中，订单号的设计通常都会适当携带一些允许公开的对使用场景有帮助的信息，如时间，星期，类型等等，这个主要根据所涉及的编号对应的使用场景来。</p>\n<p>而且像时间、星期这些自增长的属于作为订单号的设计的一部分元素，有助于解决业务累积而导致的订单号重复的问题。</p>\n<p><strong>（3）查询效率</strong></p>\n<p>常见的电商平台订单号大多是纯数字组成，兼具可读性的同时，int 类型相对 varchar 类型的查询效率更高，对在线业务更加友好。</p>\n<h3>3、优惠券和兑换券</h3>\n<p>优惠券、兑换券是运营推广最常用的促销工具之一，合理使用它们，可以让买家得到实惠，商家提升商品销量。常见场景有：</p>\n<ol>\n<li>在文库购买【文库 VIP+QQ 音乐年卡】联合商品，支付成功后会得到 QQ 音乐年卡的兑换码，可以去 QQ 音乐 App 兑换音乐会员年卡；</li>\n<li>疫情期间，部分地方政府发放的消费券；</li>\n<li>瓶装饮料经常会出现输入优惠编码兑换奖品。</li>\n</ol>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-design-coupon.png\" alt=\"优惠编码兑换奖品\"></p>\n<p>从技术角度看，有些场景适合 ID 即时生成，比如电商平台购物领取的优惠券，只需要在用户领取时分配优惠券信息即可。有些线上线下结合的场景，比如疫情优惠券，瓶盖开奖，京东卡，超市卡这种，则需要预先生成，预先生成的券码具备以下特性：</p>\n<p>1.预先生成，在活动正式开始前提供出来进行活动预热；</p>\n<p>2.优惠券体量大，以万为单位，通常在 10 万级别以上；</p>\n<p>3.不可破解、仿制券码；</p>\n<p>4.支持用后核销；</p>\n<p>5.优惠券、兑换券属于广撒网的策略，所以利用率低，也就不适合使用数据库进行存储 <strong>（占空间，有效的数据又少）</strong>。</p>\n<p>设计思路上，需要设计一种有效的兑换码生成策略，支持预先生成，支持校验，内容简洁，生成的兑换码都具有唯一性，那么这种策略就是一种特殊的编解码策略，按照约定的编解码规则支撑上述需求。</p>\n<p>既然是一种编解码规则，那么需要约定编码空间(也就是用户看到的组成兑换码的字符)，编码空间由字符 a-z,A-Z,数字 0-9 组成，为了增强兑换码的可识别度，剔除大写字母 O 以及 I,可用字符如下所示，共 60 个字符：</p>\n<p>abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXZY0123456789</p>\n<p>之前说过，兑换码要求尽可能简洁，那么设计时就需要考虑兑换码的字符数，假设上限为 12 位，而字符空间有 60 位，那么可以表示的空间范围为 60^12=130606940160000000000000(也就是可以 12 位的兑换码可以生成天量,应该够运营同学挥霍了)，转换成 2 进制：</p>\n<p>1001000100000000101110011001101101110011000000000000000000000(61 位)</p>\n<p><strong>兑换码组成成分分析</strong></p>\n<p>兑换码可以预先生成，并且不需要额外的存储空间保存这些信息，每一个优惠方案都有独立的一组兑换码(指运营同学组织的每一场运营活动都有不同的兑换码,不能混合使用, 例如双 11 兑换码不能使用在双 12 活动上)，每个兑换码有自己的编号，防止重复，为了保证兑换码的有效性，对兑换码的数据需要进行校验，当前兑换码的数据组成如下所示：</p>\n<p>优惠方案 ID + 兑换码序列号 i + 校验码</p>\n<p><strong>编码方案</strong></p>\n<ol>\n<li>兑换码序列号 i，代表当前兑换码是当前活动中第 i 个兑换码，兑换码序列号的空间范围决定了优惠活动可以发行的兑换码数目，当前采用 30 位 bit 位表示，可表示范围：1073741824（10 亿个券码）。</li>\n<li>优惠方案 ID, 代表当前优惠方案的 ID 号，优惠方案的空间范围决定了可以组织的优惠活动次数，当前采用 15 位表示，可以表示范围：32768（考虑到运营活动的频率，以及 ID 的初始值 10000，15 位足够，365 天每天有运营活动，可以使用 54 年）。</li>\n<li>校验码，校验兑换码是否有效，主要为了快捷的校验兑换码信息的是否正确，其次可以起到填充数据的目的，增强数据的散列性，使用 13 位表示校验位，其中分为两部分，前 6 位和后 7 位。</li>\n</ol>\n<p>深耕业务还会有区分通用券和单独券的情况，分别具备以下特点，技术实现需要因地制宜地思考。</p>\n<ol>\n<li>通用券：多个玩家都可以输入兑换，然后有总量限制，期限限制。</li>\n<li>单独券：运营同学可以在后台设置兑换码的奖励物品、期限、个数，然后由后台生成兑换码的列表，兑换之后核销。</li>\n</ol>\n<h2>场景二：Tracing</h2>\n<h3>1、日志跟踪</h3>\n<p>在分布式服务架构下，一个 Web 请求从网关流入，有可能会调用多个服务对请求进行处理，拿到最终结果。这个过程中每个服务之间的通信又是单独的网络请求，无论请求经过的哪个服务出了故障或者处理过慢都会对前端造成影响。</p>\n<p>处理一个 Web 请求要调用的多个服务，为了能更方便的查询哪个环节的服务出现了问题，现在常用的解决方案是为整个系统引入分布式链路跟踪。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-design-tracing.png\" alt=\"在分布式链路跟踪\"></p>\n<p>在分布式链路跟踪中有两个重要的概念：跟踪（trace）和 跨度（ span)。trace 是请求在分布式系统中的整个链路视图，span 则代表整个链路中不同服务内部的视图，span 组合在一起就是整个 trace 的视图。</p>\n<p>在整个请求的调用链中，请求会一直携带 traceid 往下游服务传递，每个服务内部也会生成自己的 spanid 用于生成自己的内部调用视图，并和 traceid 一起传递给下游服务。</p>\n<h3>2、TraceId 生成规则</h3>\n<p>这种场景下，生成的 ID 除了要求唯一之外，还要求生成的效率高、吞吐量大。traceid 需要具备接入层的服务器实例自主生成的能力，如果每个 trace 中的 ID 都需要请求公共的 ID 服务生成，纯纯的浪费网络带宽资源。且会阻塞用户请求向下游传递，响应耗时上升，增加了没必要的风险。所以需要服务器实例最好可以自行计算 tracid，spanid，避免依赖外部服务。</p>\n<p>产生规则：服务器 IP + ID 产生的时间 + 自增序列 + 当前进程号 ，比如：</p>\n<p>0ad1348f1403169275002100356696</p>\n<p>前 8 位 0ad1348f 即产生 TraceId 的机器的 IP，这是一个十六进制的数字，每两位代表 IP 中的一段，我们把这个数字，按每两位转成 10 进制即可得到常见的 IP 地址表示方式 10.209.52.143，您也可以根据这个规律来查找到请求经过的第一个服务器。</p>\n<p>后面的 13 位 1403169275002 是产生 TraceId 的时间。之后的 4 位 1003 是一个自增的序列，从 1000 涨到 9000，到达 9000 后回到 1000 再开始往上涨。最后的 5 位 56696 是当前的进程 ID，为了防止单机多进程出现 TraceId 冲突的情况，所以在 TraceId 末尾添加了当前的进程 ID。</p>\n<h3>3、SpanId 生成规则</h3>\n<p>span 是层的意思，比如在第一个实例算是第一层， 请求代理或者分流到下一个实例处理，就是第二层，以此类推。通过层，SpanId 代表本次调用在整个调用链路树中的位置。</p>\n<p>假设一个 服务器实例 A 接收了一次用户请求，代表是整个调用的根节点，那么 A 层处理这次请求产生的非服务调用日志记录 spanid 的值都是 0，A 层需要通过 RPC 依次调用 B、C、D 三个服务器实例，那么在 A 的日志中，SpanId 分别是 0.1，0.2 和 0.3，在 B、C、D 中，SpanId 也分别是 0.1，0.2 和 0.3；如果 C 系统在处理请求的时候又调用了 E，F 两个服务器实例，那么 C 系统中对应的 spanid 是 0.2.1 和 0.2.2，E、F 两个系统对应的日志也是 0.2.1 和 0.2.2。</p>\n<p>根据上面的描述可以知道，如果把一次调用中所有的 SpanId 收集起来，可以组成一棵完整的链路树。</p>\n<p><strong>spanid 的生成本质：在跨层传递透传的同时，控制大小版本号的自增来实现的。</strong></p>\n<h2>场景三：短网址</h2>\n<p>短网址主要功能包括网址缩短与还原两大功能。相对于长网址，短网址可以更方便地在电子邮件，社交网络，微博和手机上传播，例如原来很长的网址通过短网址服务即可生成相应的短网址，避免折行或超出字符限制。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-design-short-url.png\" alt=\"短网址作用\"></p>\n<p>常用的 ID 生成服务比如：MySQL ID 自增、 Redis 键自增、号段模式，生成的 ID 都是一串数字。短网址服务把客户的长网址转换成短网址，</p>\n<p>实际是在 <a href=\"http://dwz.cn\" target=\"_blank\" rel=\"noopener noreferrer\">dwz.cn</a> 域名后面拼接新产生的数字类型 ID，直接用数字 ID，网址长度也有些长，服务可以通过数字 ID 转更高进制的方式压缩长度。这种算法在短网址的技术实现上越来越多了起来，它可以进一步压缩网址长度。转进制的压缩算法在生活中有广泛的应用场景，举例：</p>\n<ul>\n<li>客户的长网址：<a href=\"https://wenku.baidu.com/ndbusiness/browse/wenkuvipcashier?cashier_code=PCoperatebanner\" target=\"_blank\" rel=\"noopener noreferrer\">https://wenku.baidu.com/ndbusiness/browse/wenkuvipcashier?cashier_code=PCoperatebanner</a></li>\n<li>ID 映射的短网址：<a href=\"https://dwz.cn/2047601319t66\" target=\"_blank\" rel=\"noopener noreferrer\">https://dwz.cn/2047601319t66</a> (演示使用，可能无法正确打开)</li>\n<li>转进制后的短网址：<a href=\"https://dwz.cn/2ezwDJ0\" target=\"_blank\" rel=\"noopener noreferrer\">https://dwz.cn/2ezwDJ0</a> (演示使用，可能无法正确打开)</li>\n</ul>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-design-pay-one-card.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "分布式"
      ]
    },
    {
      "title": "分布式 ID 生成方案详解：UUID、Snowflake、号段模式、Leaf 与 Tinyid 对比",
      "url": "https://codingmap.strova.top/distributed-system/distributed-id.html",
      "id": "https://codingmap.strova.top/distributed-system/distributed-id.html",
      "summary": "分布式 ID 生成方案详解，系统对比 UUID、数据库自增、数据库号段模式、Redis、MongoDB ObjectId、Snowflake、Leaf、Tinyid、UidGenerator、IdGenerator 的原理、优缺点和适用场景。重点围绕 分布式 ID 介绍、基于数据库的生成方案（有状态）等核心内容。",
      "content_html": "<h2>分布式 ID 介绍</h2>\n<h3>什么是 ID？</h3>\n<p>日常开发中，我们需要对系统中的各种数据使用 ID 唯一表示，比如用户 ID 唯一标识一个用户，商品 ID 唯一标识一件商品，订单 ID 唯一标识一笔订单。</p>\n<p>我们现实生活中也有各种 ID，比如身份证 ID 唯一标识一个人，地址 ID 唯一标识一个地址。</p>\n<p>简单来说，<strong>ID 就是数据的唯一标识</strong>。</p>\n<h3>什么是分布式 ID？</h3>\n<p>这里说的分布式 ID，主要指分布式系统中用于跨节点、跨库、跨服务唯一标识数据的 ID。它解决的是多节点并发生成 ID 时不能冲突的问题。</p>\n<p>我简单举一个分库分表的例子。</p>\n<p>我司的一个项目，使用的是单机 MySQL。但是，没想到的是，项目上线一个月之后，随着使用人数越来越多，整个系统的数据量将越来越大。单机 MySQL 已经没办法支撑了，需要进行分库分表，可以考虑 Apache ShardingSphere-JDBC 这类方案，具体还要看 SQL 复杂度、事务要求、运维能力和团队经验。</p>\n<p>在分库之后，数据分散在不同数据库节点上，数据库的自增主键已经没办法满足生成的主键唯一了。<strong>我们如何为不同的数据节点生成全局唯一主键呢？</strong></p>\n<p>这个时候就需要生成<strong>分布式 ID</strong>了。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/id-after-the-sub-table-not-conflict.png\" alt></p>\n<h3>分布式 ID 需要满足哪些要求?</h3>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-requirements.png\" alt></p>\n<p>分布式 ID 作为分布式系统中必不可少的一环，很多地方都要用到分布式 ID。</p>\n<p>一个最基本的分布式 ID 需要满足下面这些要求：</p>\n<ul>\n<li><strong>全局唯一</strong>：ID 的全局唯一性是首先要满足的。</li>\n<li><strong>高性能</strong>：分布式 ID 的生成速度要快，对本地资源消耗要小。</li>\n<li><strong>高可用</strong>：发号服务要具备较高可用性，避免成为业务链路的单点。</li>\n<li><strong>方便易用</strong>：拿来即用，使用方便，快速接入！</li>\n</ul>\n<p>除了这些之外，一个比较好的分布式 ID 还应保证：</p>\n<ul>\n<li><strong>安全</strong>：ID 中不包含敏感信息。</li>\n<li><strong>趋势递增</strong>：如果要把 ID 存放在数据库的话，趋势递增的 ID 通常更利于 B+ 树索引写入。很多数据库主键场景更需要趋势递增，而不是全局严格递增；严格递增虽然方便排序，但通常会引入中心化发号器或强协调，成本更高。</li>\n<li><strong>业务含义可控</strong>：是否嵌入业务含义要谨慎。业务含义有助于排查问题，但也可能泄露业务规模、地区、时间、渠道等信息，并让 ID 规则和业务强耦合。很多系统更倾向于让 ID 保持无语义，把业务信息放到单独字段。</li>\n<li><strong>独立部署</strong>：也就是分布式系统单独有一个发号器服务，专门用来生成分布式 ID。这样，生成 ID 的服务就可以和业务相关的服务解耦。不过，这样同样带来了网络调用消耗增加的问题。总的来说，如果需要用到分布式 ID 的场景比较多的话，独立部署的发号器服务还是很有必要的。</li>\n</ul>\n<p>还需要注意，不同方案的“唯一性”来源并不一样：</p>\n<ul>\n<li>数据库自增、数据库号段模式依赖中心存储和事务分配。</li>\n<li>Redis 方案依赖单 key 原子递增、持久化策略和主从一致性。</li>\n<li>Snowflake 依赖时间戳、Worker ID、sequence 三者组合不冲突。</li>\n<li>UUID v4/v7 依赖随机数质量和生成器实现策略。</li>\n<li>对不能容忍重复的主键场景，最终落库时仍建议用数据库唯一约束兜底。</li>\n</ul>\n<h2>基于数据库的生成方案（有状态）</h2>\n<h3>数据库主键自增</h3>\n<p>这种方式就比较简单直白了，就是通过关系型数据库的自增主键来生成唯一 ID。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/the-primary-key-of-the-database-increases-automatically.png\" alt=\"数据库主键自增\"></p>\n<p>以 MySQL 举例，我们通过下面的方式即可。</p>\n<p><strong>1. 创建一个数据库表。</strong></p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">CREATE</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> TABLE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> `</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">sequence_id</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">` (</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">  `id`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> BIGINT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> UNSIGNED </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">NOT NULL</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> AUTO_INCREMENT,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">  `stub`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> char</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">10</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">NOT NULL</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> DEFAULT</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> ''</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  PRIMARY KEY</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`id`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">),</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  UNIQUE</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> KEY</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> `stub`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`stub`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) ENGINE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">InnoDB </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">DEFAULT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> CHARSET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">utf8mb4;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><code>stub</code> 字段无意义，只是为了占位，便于我们插入或者修改数据。并且，给 <code>stub</code> 字段创建了唯一索引，保证其唯一性。</p>\n<p><strong>2. 通过 <code>REPLACE INTO</code> 来插入数据。</strong></p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">BEGIN</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">REPLACE</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> INTO</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> sequence_id</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (stub) </span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">VALUES</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'stub'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">SELECT</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> LAST_INSERT_ID</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">COMMIT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><strong>⚠️ REPLACE INTO 的生产隐患</strong>：</p>\n<p><code>REPLACE INTO</code> 的语义是：如果新行不会和 <code>PRIMARY KEY</code> 或 <code>UNIQUE</code> 索引冲突，就直接插入；如果冲突，则先删除旧行，再插入新行。受影响行数可能是删除行数加插入行数。</p>\n<ul>\n<li>每次操作都会触发索引删除和重建，对数据库压力较大。</li>\n<li>如果表上有触发器，DELETE 操作会意外触发。</li>\n</ul>\n<p><strong>替代方案</strong>：如果只是为了推进序列表，可以使用 <code>INSERT ... ON DUPLICATE KEY UPDATE</code> 或单行 <code>UPDATE</code> 来避免 <code>REPLACE</code> 的删除语义。生产环境更常见的是号段模式：一次更新 <code>current_max_id = current_max_id + step</code>，再在内存中分配。</p>\n<p>这种方式的优缺点也比较明显：</p>\n<ul>\n<li><strong>优点</strong>：实现起来比较简单、ID 有序递增、存储消耗空间小。</li>\n<li><strong>缺点</strong>：支持的并发量不大、存在数据库单点问题（可以通过主备、MGR、分库多发号段等方式提高可用性，但要处理主从切换、事务提交、重复发号和号段浪费问题）、ID 没有具体业务含义、安全问题（比如根据订单 ID 的递增规律就能推算出每天的订单量，商业机密啊！ ）、每次获取 ID 都要访问一次数据库（增加了对数据库的压力，获取速度也慢）。</li>\n</ul>\n<h3>数据库号段模式</h3>\n<p>数据库主键自增这种模式，每次获取 ID 都要访问一次数据库，ID 需求比较大的时候，肯定是不行的。</p>\n<p>如果我们可以批量获取，然后存在内存里面，需要用到的时候直接从内存里拿，就能减少访问数据库的次数，延迟和数据库压力都会下降。这也就是我们说的 <strong>基于数据库的号段模式来生成分布式 ID</strong>。</p>\n<p>数据库号段模式是目前比较主流的一种分布式 ID 生成方式。像滴滴开源的 <a href=\"https://github.com/didi/tinyid/wiki/tinyid%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D\" target=\"_blank\" rel=\"noopener noreferrer\">Tinyid</a> 就是基于这种方式来做的。不过，Tinyid 使用了双号段缓存、增加多数据库支持等方式来进一步优化。</p>\n<p>以 MySQL 举例，我们通过下面的方式即可。</p>\n<p><strong>1. 创建一个数据库表。</strong></p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">CREATE</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> TABLE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> `</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">sequence_id_generator</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">` (</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">  `id`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> INT</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> NOT NULL</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">  `current_max_id`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> BIGINT</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> NOT NULL</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> COMMENT </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'当前最大ID'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">  `step`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> INT</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> NOT NULL</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> COMMENT </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'号段的长度'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">  `version`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> INT</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> NOT NULL</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> COMMENT </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'版本号'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">  `biz_type`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> INT</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> NOT NULL</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> COMMENT </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'业务类型'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  PRIMARY KEY</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`id`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">),</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  UNIQUE</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> KEY</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> `uk_biz_type`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`biz_type`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) ENGINE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">InnoDB </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">DEFAULT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> CHARSET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">utf8mb4;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><code>current_max_id</code> 字段和 <code>step</code> 字段主要用于获取批量 ID。获取的批量 ID 区间为 <code>(current_max_id, current_max_id + step]</code>，也就是不包含 <code>current_max_id</code> 的旧值本身。例如，旧 <code>current_max_id = 0</code>、<code>step = 100</code> 时，成功更新后本次可分配的 ID 区间为 <code>1~100</code>。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/database-number-segment-mode.png\" alt=\"数据库号段模式\"></p>\n<p><code>version</code> 字段主要用于解决并发问题（乐观锁），完整流程如下：</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">-- 1. 读取当前值</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SELECT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> current_max_id, step, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">version</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> FROM</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> sequence_id_generator </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">WHERE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> biz_type </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 101</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">-- 2. CAS 更新（version 作为乐观锁版本号）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">UPDATE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> sequence_id_generator</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> current_max_id </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> current_max_id + step, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> + </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">WHERE</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {当前读取的version} </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">AND</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> biz_type </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 101</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">-- 3. 检查 affected_rows，为 1 表示成功，为 0 表示被其他线程抢先，需重试</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><code>UPDATE ... WHERE version = ?</code> 执行后必须检查 <code>affected_rows</code>。如果结果为 0，说明号段已经被其他实例抢走，需要重新读取 <code>current_max_id</code> 和 <code>version</code> 后再重试。</p>\n<blockquote>\n<p><strong>⚠️ 高并发重试提醒</strong>：在号段耗尽瞬间，多个线程可能同时争抢新号段，CAS 更新可能失败。代码层面需要实现<strong>有限次数的重试循环</strong>（如 3 次）和指数退避，确保请求稳定性。若重试仍失败，应阻塞等待下一个号段加载完成，或触发告警并进入熔断降级流程。<strong>不建议返回所谓“降级 ID”</strong>，否则可能破坏全局唯一性保证。</p>\n</blockquote>\n<p><code>biz_type</code> 主要用于表示业务类型。</p>\n<p><strong>2. 先插入一行数据。</strong></p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">INSERT INTO</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> `sequence_id_generator`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`id`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`current_max_id`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`step`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`version`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`biz_type`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">VALUES</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">100</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">101</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><strong>3. 通过 SELECT 获取指定业务下的批量唯一 ID</strong></p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SELECT</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> `current_max_id`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`step`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`version`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> FROM</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> `sequence_id_generator`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> WHERE</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> `biz_type`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 101</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>结果：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>id current_max_id step version biz_type</span></span>\n<span class=\"line\"><span>1 0 100 0 101</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><strong>4. 不够用的话，更新之后重新 SELECT 即可。</strong></p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">UPDATE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> sequence_id_generator </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SET</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> current_max_id </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> + </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">100</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> + </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> WHERE</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> AND</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> `biz_type`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 101</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SELECT</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> `current_max_id`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`step`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`version`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> FROM</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> `sequence_id_generator`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> WHERE</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> `biz_type`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 101</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>结果：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>id current_max_id step version biz_type</span></span>\n<span class=\"line\"><span>1 100 100 1 101</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>相比于数据库主键自增的方式，<strong>数据库的号段模式对于数据库的访问次数更少，数据库压力更小。</strong></p>\n<p>另外，为了避免单点问题，可以通过主从模式或多库部署提高可用性。如果使用主从模式，发号更新必须走主库，并确保故障切换后不会回到旧的 <code>current_max_id</code>；多库模式则要保证每个库分配的号段范围、步长或业务分片互不重叠。</p>\n<p>号段模式还会带来“跳号”问题：实例申请号段后通常会缓存在内存中，实例宕机或重启时，未使用完的号段会被浪费。浪费号段不影响唯一性，但会导致 ID 不连续；不要为了追求连续性回收已分配但未使用的号段，否则可能产生重复 ID。生产环境需要监控号段消耗速度、加载失败率和重试次数。</p>\n<p><strong>数据库号段模式的优缺点:</strong></p>\n<ul>\n<li><strong>优点</strong>：ID 趋势递增、存储消耗空间小。</li>\n<li><strong>缺点</strong>：存在数据库单点问题（可以通过主从、多库等方式提高可用性，不过增加了复杂度）、ID 没有具体业务含义、安全问题（比如根据订单 ID 的递增规律就能推算出每天的订单量，商业机密啊！ ）。</li>\n</ul>\n<h3>NoSQL</h3>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/nosql-distributed-id.png\" alt></p>\n<p>一般情况下，NoSQL 方案使用 Redis 多一些。我们通过 Redis 的 <code>INCR</code> 命令即可实现对 ID 原子顺序递增。</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">127.0.0.1:6379</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">> </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">set</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> sequence_id_biz_type</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">OK</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">127.0.0.1:6379</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">> </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">incr</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> sequence_id_biz_type</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">integer</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">2</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">127.0.0.1:6379</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">> </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">get</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> sequence_id_biz_type</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">\"2\"</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>为了提高可用性和并发，我们可以使用 Redis Cluster。Redis Cluster 是 Redis 官方提供的 Redis 集群解决方案（3.0+版本）。</p>\n<p>Codis 曾经是常见的开源 Redis 集群方案，但项目长期不活跃。新项目一般优先评估 Redis Cluster、云厂商 Redis 集群或兼容 Redis 协议的托管服务；Codis 更适合存量系统继续维护，使用前需要单独评估维护状态。</p>\n<p>除了高可用和并发之外，我们知道 Redis 基于内存，我们需要持久化数据，避免重启机器或者机器故障后数据丢失。Redis 支持两种不同的持久化方式：<strong>快照（snapshotting，RDB）</strong>、<strong>只追加文件（append-only file, AOF）</strong>。并且，Redis 4.0 开始支持 <strong>RDB 和 AOF 的混合持久化</strong>，由配置项 <code>aof-use-rdb-preamble</code> 控制：Redis 4.0 示例配置默认关闭，Redis 5.0+ 示例配置默认开启。具体默认值要以目标 Redis 版本、配置文件以及云厂商托管版配置为准。</p>\n<p>关于 Redis 持久化，我这里就不过多介绍。不了解这部分内容的小伙伴，可以看看 <a href=\"https://javaguide.cn/database/redis/redis-persistence.html\" target=\"_blank\" rel=\"noopener noreferrer\">Redis 持久化机制详解</a>这篇文章。</p>\n<p>虽然 Redis <code>INCR</code> 性能优异，但 Redis 持久化只能降低进程重启后的数据丢失风险，不能完全消除 ID 回退。尤其是 <code>appendfsync everysec</code>、RDB 快照、主从异步复制和故障切换场景，都可能丢失最近一段 <code>INCR</code> 结果。下面这些失败路径需要特别注意：</p>\n<ol>\n<li>\n<p><strong>持久化延迟导致 ID 回退</strong></p>\n<ul>\n<li><strong>场景</strong>：执行 <code>INCR</code> 后，Redis 在 RDB/AOF 刷盘前崩溃。</li>\n<li><strong>后果</strong>：重启后 ID 回退到上次持久化的值，可能产生重复 ID。</li>\n</ul>\n</li>\n<li>\n<p><strong>AOF 重写导致短暂阻塞</strong></p>\n<ul>\n<li><strong>场景</strong>：AOF 文件过大触发重写。</li>\n<li><strong>后果</strong>：主进程 fork 子进程可能导致短暂的性能抖动。</li>\n</ul>\n</li>\n<li>\n<p><strong>Redis Cluster 单分片热点</strong></p>\n<ul>\n<li><strong>场景</strong>：单个计数 key 始终落在集群的单一分片上。</li>\n<li><strong>后果</strong>：高并发下该分片可能成为瓶颈。</li>\n</ul>\n</li>\n<li>\n<p><strong>主从异步复制故障切换</strong></p>\n<ul>\n<li><strong>场景</strong>：主节点故障切换到从节点时，从节点的 <code>INCR</code> 值可能落后于主节点。</li>\n<li><strong>后果</strong>：新主节点上的 ID 可能回退到旧值。</li>\n</ul>\n</li>\n</ol>\n<p><strong>生产配置建议</strong>：</p>\n<div class=\"language-conf line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"conf\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-conf\"><span class=\"line\"><span># Redis 7.0+ 可参考配置</span></span>\n<span class=\"line\"><span>appendonly yes</span></span>\n<span class=\"line\"><span>appendfsync everysec</span></span>\n<span class=\"line\"><span>aof-use-rdb-preamble yes  # 混合持久化（RDB+AOF 组合）</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><ul>\n<li><strong>Redis 7.0+ 优化</strong>：多部分 AOF（Multi-part AOF）改善了 AOF 重写期间 base/incr 文件的组织和管理方式，但 fork、磁盘 IO、<code>fsync</code> 策略仍可能带来抖动。</li>\n<li><strong>适用边界</strong>：Redis 适合流水号、短周期计数、可业务兜底去重的场景。若对 ID 唯一性要求极高，例如金融订单号、支付流水这类核心主键，需要结合业务去重、持久化策略、主从一致性策略和落库唯一约束，或改用数据库号段模式、Leaf 等方案。</li>\n</ul>\n<p><strong>Redis 方案的优缺点：</strong></p>\n<ul>\n<li><strong>优点</strong>：性能不错，并且单 key、单主正常运行时生成的 ID 是递增的。</li>\n<li><strong>缺点</strong>：和数据库主键自增方案的缺点类似，且存在持久化延迟、单分片热点、主从切换导致 ID 回退的风险。</li>\n</ul>\n<p>除了 Redis 之外，MongoDB ObjectId 经常也会被拿来当做分布式 ID 的解决方案。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/mongodb9-objectId-distributed-id.png\" alt=\"MongoDB ObjectId Specification\"></p>\n<p>MongoDB ObjectId 一共需要 12 个字节存储：</p>\n<ul>\n<li>0~3：Unix 秒级时间戳（4 字节）</li>\n<li>4~8：随机值（5 字节，每个客户端进程生成一次，用于区分机器和进程）</li>\n<li>9~11：自增计数器（3 字节，每个客户端进程内递增）</li>\n</ul>\n<p>ObjectId 是无需中心协调的近似有序唯一标识，通常可满足文档 <code>_id</code> 场景，但它不是全局严格单调序列：它只有秒级时间精度，同一秒内生成的 ObjectId 不保证严格顺序；不同客户端机器的系统时间也可能不同。</p>\n<p><strong>MongoDB 方案的优缺点：</strong></p>\n<ul>\n<li><strong>优点</strong>：性能不错并且生成的 ID 按创建时间近似有序。</li>\n<li><strong>缺点</strong>：不是严格单调递增，当机器时间不一致时排序结果可能不符合真实创建顺序；另外，ID 中包含时间信息，存在一定规律性。</li>\n</ul>\n<h2>基于算法的生成方案（无状态）</h2>\n<h3>UUID</h3>\n<p>UUID 是 Universally Unique Identifier（通用唯一标识符）的缩写，本质上是一个 128 bit 的标识符。它的标准字符串形式通常是 36 个字符（包含连字符），去掉连字符后是 32 个十六进制字符。</p>\n<p>JDK 就提供了现成的生成 UUID 的方法，一行代码就行了。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">//输出示例：cb4a9ede-fa5e-4585-b9bb-d60bce986eaa</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">UUID</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">randomUUID</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><a href=\"https://www.rfc-editor.org/rfc/rfc9562.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9562</a> 已经取代 <a href=\"https://tools.ietf.org/html/rfc4122\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 4122</a>，重新规范了 UUID，并新增了 v6、v7、v8。旧资料里仍会看到 RFC 4122 的说法，但新文章建议以 RFC 9562 为主。RFC 9562 中关于 UUID 的示例是这样的：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/rfc-4122-uuid.png\" alt></p>\n<p>我们这里重点关注一下这个 Version（版本），不同的版本对应的 UUID 的生成规则是不同的。</p>\n<p>8 种不同的 Version（版本）值分别对应的含义（参考<a href=\"https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E5%94%AF%E4%B8%80%E8%AF%86%E5%88%AB%E7%A0%81\" target=\"_blank\" rel=\"noopener noreferrer\">维基百科对于 UUID 的介绍</a>）：</p>\n<ul>\n<li><strong>版本 1（基于时间和节点 ID）</strong>：基于时间戳（通常是当前时间）和节点 ID（通常为设备的 MAC 地址）生成。当包含 MAC 地址时，可以保证全球唯一性，但也因此存在隐私泄露的风险。</li>\n<li><strong>版本 2（基于标识符、时间和节点 ID）</strong>：与版本 1 类似，也基于时间和节点 ID，但额外包含了本地标识符（例如用户 ID 或组 ID）。</li>\n<li><strong>版本 3（基于命名空间和名称的 MD5 哈希）</strong>：使用 MD5 哈希算法，将命名空间标识符（一个 UUID）和名称字符串组合计算得到。相同的命名空间和名称总是生成相同的 UUID（<strong>确定性生成</strong>）。</li>\n<li><strong>版本 4（基于随机数）</strong>：使用伪随机数生成器（PRNG）或加密安全随机数生成器（CSPRNG）来生成。UUID v4 有 122 bit 随机位，取值空间为 2^122；按生日悖论计算，约生成 2^61 个 UUID 时碰撞概率才接近 50%。实际应用中可认为唯一，但理论上仍是概率保证。</li>\n<li><strong>版本 5（基于命名空间和名称的 SHA-1 哈希）</strong>：类似于版本 3，但使用 SHA-1 哈希算法。</li>\n<li><strong>版本 6（基于时间戳、计数器和节点 ID）</strong>：改进了版本 1，将时间戳放在最高有效位（Most Significant Bit，MSB），使得 UUID 可以直接按时间排序。</li>\n<li><strong>版本 7（基于 Unix 毫秒时间戳）</strong>：高位是 48 位 Unix 毫秒时间戳，剩余位在扣除版本和变体位后用于随机数，也允许实现使用亚毫秒时间或计数器来增强同一毫秒内的单调性。对需要时间排序且没有特殊兼容要求的新系统，UUID v7 通常比 v1/v6 更推荐；但存量系统、协议兼容和已有数据格式仍要单独评估。</li>\n<li><strong>版本 8（实验性/供应商定制）</strong>：<strong>122 位留给实现自定义</strong>，仅要求版本和变体位固定。适用于嵌入额外信息或特殊应用限制的场景。<strong>唯一性由实现保证，不可假设</strong>。</li>\n</ul>\n<p>下面是 UUID v1 生成结果的示例：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/version1-uuid.png\" alt=\"UUID v1 生成结果的示例\"></p>\n<p>JDK 中通过 <code>UUID</code> 的 <code>randomUUID()</code> 方法生成的 UUID 的版本默认为 4。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">UUID</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> uuid </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> UUID</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">randomUUID</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> version </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> uuid</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 4</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>另外，Variant（变体）也有 4 种不同的值，这种值分别对应不同的含义。这里就不介绍了，貌似平时也不怎么需要关注。</p>\n<p>需要用到的时候，去看看维基百科对于 UUID 的 Variant（变体）相关的介绍即可。</p>\n<p>从上面的介绍中可以看出，UUID 在正确实现和足够随机性的前提下，工程上可认为唯一。v4 的唯一性本质上是概率保证；v1/v6 依赖节点 ID 和时间戳，可能存在隐私泄露风险；v7/v8 的唯一性则依赖实现策略。</p>\n<p>虽然，UUID 在工程上可认为全局唯一，但是，我们一般很少会使用它。</p>\n<p>比如使用 UUID v4 这类非时序 UUID 作为 MySQL 数据库主键的时候就非常不合适：</p>\n<ul>\n<li>数据库主键要尽量越短越好。UUID 本质是 128 bit，如果用字符串存储，通常是 36 字符含连字符，或 32 个十六进制字符；如果用二进制存储，可以压到 16 字节。</li>\n<li>UUID v4 这类非时序 UUID 是无序的，InnoDB 引擎下，数据库主键的无序性会严重影响数据库性能。</li>\n</ul>\n<p>UUID v7（<a href=\"https://www.rfc-editor.org/rfc/rfc9562\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9562</a>）是一个标准化、趋势有序、无需 Worker ID 分配的可选方案：</p>\n<p>UUID v7 不需要像 Snowflake 一样分配 Worker ID，接入成本低；但它仍然依赖随机数质量和生成器实现。对不能容忍重复的主键场景，数据库唯一约束仍然不能省。</p>\n<p>| 特性               | Snowflake                 | UUID v7                                                  |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/id-after-the-sub-table-not-conflict.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "分布式"
      ]
    },
    {
      "title": "分布式锁实现方案详解：Redis、Redlock、ZooKeeper 与 Redisson 看门狗",
      "url": "https://codingmap.strova.top/distributed-system/distributed-lock-implementations.html",
      "id": "https://codingmap.strova.top/distributed-system/distributed-lock-implementations.html",
      "summary": "分布式锁实现方案详解，覆盖 Redis SET NX EX、Lua 安全释放、Redisson Watch Dog、Redlock、ZooKeeper 临时顺序节点、Curator 可重入锁和 Fencing Token 等生产实践。重点围绕 基于 Redis 实现分布式锁、基于 ZooKeeper 实现分布式锁等核。",
      "content_html": "<p>通常情况下，我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁，Redis 用的要更多一点，我这里也先以 Redis 为例介绍分布式锁的实现。</p>\n<h2>基于 Redis 实现分布式锁</h2>\n<h3>如何基于 Redis 实现一个最简易的分布式锁？</h3>\n<p>不论是本地锁还是分布式锁，核心都在于“互斥”。</p>\n<p>在 Redis 中，<code>SETNX</code> 命令可以帮助我们实现互斥。<code>SETNX</code> 即 <strong>SET</strong> if <strong>N</strong>ot e<strong>X</strong>ists（对应 Java 中的 <code>setIfAbsent</code> 方法），如果 key 不存在的话，才会设置 key 的值。如果 key 已经存在，<code>SETNX</code> 啥也不做。</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">> SETNX lockKey uniqueValue</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">integer</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">1</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">> SETNX lockKey uniqueValue</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">integer</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">0</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>释放锁的话，直接通过 <code>DEL</code> 命令删除对应的 key 即可。</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">> DEL lockKey</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">integer</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">1</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>为了防止误删到其他的锁，这里我们建议使用 Lua 脚本先比对 key 对应的 value 是否为加锁时写入的唯一值，校验通过后再删除。</p>\n<p>选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时，可以以原子性的方式执行，从而保证了锁释放操作的原子性。</p>\n<div class=\"language-lua line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"lua\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-lua\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">-- 释放锁时，先比对 key 对应的 value 是否为加锁时写入的唯一值，校验通过后再删除，避免误删其他客户端持有的锁</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">if</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> redis</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">call</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"get\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">KEYS</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">[</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">]) == </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">ARGV</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">[</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">] </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">then</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> redis</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">call</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"del\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">KEYS</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">[</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">])</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">else</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">end</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-setnx.png\" alt=\"Redis 实现简易分布式锁\"></p>\n<p>这是一种最简易的 Redis 分布式锁实现，实现方式比较简单，性能也很高效。不过，这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉，可能会导致锁无法被释放，进而造成共享资源无法再被其他线程/进程访问。</p>\n<h3>为什么要给锁设置一个过期时间？</h3>\n<p>为了避免锁无法被释放，我们可以想到的一个解决办法就是：<strong>给这个 key（也就是锁）设置一个过期时间</strong>。</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">127.0.0.1:6379</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">> </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">SET</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> lockKey</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> uniqueValue</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> EX</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 3</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> NX</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">OK</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><ul>\n<li><strong>lockKey</strong>：加锁的锁名；</li>\n<li><strong>uniqueValue</strong>：能够唯一标识锁的随机字符串；</li>\n<li><strong>NX</strong>：只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功；</li>\n<li><strong>EX</strong>：过期时间设置（秒为单位）。<code>EX 3</code> 表示这个锁有一个 3 秒的自动过期时间。与 <code>EX</code> 对应的是 <code>PX</code>（毫秒为单位），这两个都是过期时间设置。</li>\n</ul>\n<p><strong>一定要保证设置指定 key 的值和过期时间是一个原子操作！！！</strong> 不然的话，依然可能会出现锁无法被释放的问题。</p>\n<p>为什么要用 <code>SET NX EX</code>，而不是先 <code>SETNX</code> 再 <code>EXPIRE</code> 呢？早期常见的 <code>SETNX</code> 后再 <code>EXPIRE</code> 是两步操作，如果客户端在 <code>SETNX</code> 成功后、<code>EXPIRE</code> 前崩溃，会留下永久锁。Redis 2.6.12 起支持 <code>SET key value NX EX seconds</code> 这类原子写法，应优先使用这种方式避免死锁风险。</p>\n<p>这样确实可以解决问题，不过，这种解决办法同样存在漏洞：<strong>如果操作共享资源的时间大于过期时间，就会出现锁提前过期的问题，进而导致分布式锁直接失效。如果锁的超时时间设置过长，又会影响到性能。</strong></p>\n<p>这也是后文需要 Fencing Token 的根源：单纯延长 TTL 不能消除“客户端 A 仍以为自己持锁，但锁实际已过期并被客户端 B 获取”的窗口。</p>\n<p>你或许在想：<strong>如果操作共享资源的操作还未完成，锁过期时间能够自己续期就好了！</strong></p>\n<h3>如何实现锁的优雅续期？</h3>\n<p>对于 Java 开发的小伙伴来说，已经有了现成的解决方案：<strong><a href=\"https://github.com/redisson/redisson\" target=\"_blank\" rel=\"noopener noreferrer\">Redisson</a></strong>。其他语言的解决方案，可以在 Redis 官方文档中找到，地址：<a href=\"https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/\" target=\"_blank\" rel=\"noopener noreferrer\">https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/</a>。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/redis-distributed-lock.png\" alt=\"Distributed locks with Redis\"></p>\n<p>Redisson 是一个开源的 Java 语言 Redis 客户端，提供了很多开箱即用的功能，不仅仅包括多种分布式锁的实现。并且，Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。</p>\n<p>Redisson 中的分布式锁自带自动续期机制，使用起来非常简单，原理也比较简单，其提供了一个专门用来监控和续期锁的 <strong>Watch Dog（看门狗）</strong>，如果操作共享资源的线程还未执行完成的话，Watch Dog 会不断地延长锁的过期时间，进而保证锁不会因为超时而被释放。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-redisson-renew-expiration.png\" alt=\"Redisson 看门狗自动续期\"></p>\n<p>看门狗名字的由来于 <code>getLockWatchdogTimeout()</code> 方法，这个方法返回的是看门狗给锁续期的过期时间，默认为 30 秒（基于 <a href=\"https://github.com/redisson/redisson/releases/tag/redisson-3.17.6\" target=\"_blank\" rel=\"noopener noreferrer\">redisson-3.17.6</a>）。Redisson 当前已进入 4.x 版本，具体配置语义建议以项目实际使用版本的 <code>Config#getLockWatchdogTimeout</code> 和 <code>RedissonBaseLock#renewExpiration</code> 源码为准。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 默认 30 秒，支持修改</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> long</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> lockWatchdogTimeout </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 30</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> *</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1000</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Config</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> setLockWatchdogTimeout</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">long</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> lockWatchdogTimeout) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    this</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">lockWatchdogTimeout</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> lockWatchdogTimeout</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> this</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> long</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> getLockWatchdogTimeout</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">() {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">   return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> lockWatchdogTimeout</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><code>renewExpiration()</code> 方法包含了看门狗的主要逻辑：</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> renewExpiration</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">() {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">         //......</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        Timeout</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> task </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> commandExecutor</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getConnectionManager</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">().</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">newTimeout</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> TimerTask</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">            @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Override</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> run</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Timeout</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> timeout</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> throws</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Exception</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                //......</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                // 异步续期，基于 Lua 脚本</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">                CompletionStage</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Boolean</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">> </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">future</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> renewExpirationAsync</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(threadId);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">                future</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">whenComplete</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">((res, e) </span><span style=\"--shiki-light:#C18401;--shiki-dark:#C678DD\">-></span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">                    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (e </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                        // 无法续期</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">                        log</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">error</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"Can't update lock \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> getRawName</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \" expiration\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, e);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">                        EXPIRATION_RENEWAL_MAP</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">remove</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getEntryName</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">                        return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">                    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">                    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (res) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                        // 通过定时器回调链式触发下一次续期，非栈式递归，不会导致调用栈无限增长</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">                        renewExpiration</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">                    } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">else</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">                        // 取消续期</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">                        cancelExpirationRenewal</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">                    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">                });</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">            }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">         // 延迟 internalLockLeaseTime/3（默认 10s，也就是 30/3）再调用</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }, internalLockLeaseTime </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">/</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 3</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">TimeUnit</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">MILLISECONDS</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        ee</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">setTimeout</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(task);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    }</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>默认情况下，每过 10 秒，看门狗就会执行续期操作，将锁的超时时间设置为 30 秒。看门狗续期前也会先判断是否需要执行续期操作，需要才会执行续期，否则取消续期操作。</p>\n<p>Watch Dog 通过调用 <code>renewExpirationAsync()</code> 方法实现锁的异步续期：</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">protected</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> CompletionStage</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">Boolean</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">></span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> renewExpirationAsync</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">long</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> threadId) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> evalWriteAsync</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getRawName</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> LongCodec</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">INSTANCE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> RedisCommands</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">EVAL_BOOLEAN</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">            // 判断是否为持锁线程，如果是就执行续期操作，将锁的过期时间设置为 30s（默认）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">            \"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">                    \"redis.call('pexpire', KEYS[1], ARGV[1]); \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">                    \"return 1; \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">                    \"end; \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">                    \"return 0;\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            Collections</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">singletonList</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getRawName</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()),</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">            internalLockLeaseTime</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> getLockName</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(threadId))</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>可以看出，<code>renewExpirationAsync</code> 方法其实是调用 Lua 脚本实现的续期，这样做主要是为了保证续期操作的原子性。</p>\n<p>我这里以 Redisson 的可重入锁实现 <code>RLock</code> 为例来说明如何使用 Redisson 实现分布式锁：</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 1.获取指定的分布式锁对象</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">RLock</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> lock </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> redisson</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getLock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"lock\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 2.拿锁且不设置锁超时时间，具备 Watch Dog 自动续期机制</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">lock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">lock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 3.执行业务</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">...</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 4.释放锁</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">lock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">unlock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>只有未指定锁超时时间，才会使用到 Watch Dog 自动续期机制。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 手动给锁设置过期时间，不具备 Watch Dog 自动续期机制</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">lock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">lock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">10</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">TimeUnit</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">SECONDS</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>如果使用 Redis 来实现分布式锁的话，还是比较推荐直接基于 Redisson 来做的。</p>\n<h3>如何实现可重入锁？</h3>\n<p>所谓可重入锁指的是在一个线程中可以多次获取同一把锁，比如一个线程在执行一个带锁的方法，该方法中又调用了另一个需要相同锁的方法，则该线程可以直接执行调用的方法即可重入，而无需重新获得锁。像 Java 中的 <code>synchronized</code> 和 <code>ReentrantLock</code> 都属于可重入锁。</p>\n<p><strong>不可重入的分布式锁基本可以满足绝大部分业务场景了，一些特殊的场景可能会需要使用可重入的分布式锁。</strong></p>\n<p>可重入分布式锁的实现核心思路是线程在获取锁的时候判断是否为自己的锁，如果是的话，就不用再重新获取了。为此，我们可以为每个锁关联一个可重入计数器和一个占有它的线程。当可重入计数器大于 0 时，则锁被占有，需要判断占有该锁的线程和请求获取锁的线程是否为同一个。</p>\n<p>实际项目中，我们不需要自己手动实现，推荐使用我们上面提到的 <strong>Redisson</strong>，其内置了多种类型的锁比如可重入锁（Reentrant Lock）、自旋锁（Spin Lock）、公平锁（Fair Lock）、多重锁（MultiLock）、红锁（RedLock）、读写锁（ReadWriteLock）。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/redisson-readme-locks.png\" alt></p>\n<h3>Redis 如何解决集群情况下分布式锁的可靠性？</h3>\n<p>为了避免单点故障，生产环境下的 Redis 服务通常是集群化部署的。</p>\n<p>Redis 集群下，上面介绍到的分布式锁的实现会存在一些问题。Redis 主从复制默认是异步的：主节点写入锁成功后会立即返回客户端，再异步同步给从节点。如果主节点在同步前宕机，Sentinel 或 Redis Cluster 故障转移可能把尚未收到锁数据的从节点提升为新主，导致原锁丢失，其他客户端可以再次加锁，从而破坏互斥性。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/redis-master-slave-distributed-lock.png\" alt></p>\n<p>针对这个问题，Redis 之父 antirez 设计了 <a href=\"https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/\" target=\"_blank\" rel=\"noopener noreferrer\">Redlock 算法</a> 来解决。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-redis.io-realock.png\" alt></p>\n<p>Redlock 算法的思想是让客户端向多个相互独立的 Redis master 依次请求申请加锁，如果客户端能够在严格多数派实例上成功完成加锁操作，那么就可以认为客户端成功获得分布式锁，否则加锁失败。</p>\n<p>Redlock 的完整判断条件如下：</p>\n<ul>\n<li>使用 N 个相互独立的 Redis master（不是 Redis Cluster 分片）。</li>\n<li>客户端记录开始时间。</li>\n<li>依次向各节点用相同的 key、value、TTL 加锁，每次请求设置较短超时，避免单个节点拖慢整体加锁。</li>\n<li>只有获得至少 <code>N/2 + 1</code> 个节点成功，且总耗时小于 TTL，才算加锁成功。</li>\n<li>锁的实际有效时间约为 <code>TTL - 加锁耗时 - 时钟漂移余量</code>。</li>\n<li>失败时要向所有节点发起释放，包括加锁失败或请求超时的节点。</li>\n</ul>\n<p>Redlock 是直接操作独立 Redis 节点的，并不是通过 Redis Cluster 操作的，这样才可以避免单个主从分片故障转移导致的锁丢失问题。</p>\n<p>Redlock 实现比较复杂，性能比较差，发生时钟变迁的情况下还存在安全性隐患。《数据密集型应用系统设计》一书的作者 Martin Kleppmann 曾经专门发文（<a href=\"https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html\" target=\"_blank\" rel=\"noopener noreferrer\">How to do distributed locking - Martin Kleppmann - 2016</a>）批评过 Redlock，他认为这是一个很差的分布式锁实现。感兴趣的朋友可以看看<a href=\"https://mp.weixin.qq.com/s?__biz=Mzg3NjU3NTkwMQ==&amp;mid=2247505097&amp;idx=1&amp;sn=5c03cb769c4458350f4d4a321ad51f5a&amp;source=41#wechat_redirect\" target=\"_blank\" rel=\"noopener noreferrer\">Redis 锁从面试连环炮聊到神仙打架</a>这篇文章，有详细介绍到 antirez 和 Martin Kleppmann 关于 Redlock 的讨论。</p>\n<p>如何判断是否该用 Redlock？核心是先区分锁的使用场景：</p>\n<ol>\n<li>如果是<strong>效率型加锁</strong>，也就是锁失效时只是造成重复计算、重复执行任务这类可接受后果，可以使用 Redis 单实例、Sentinel 或 Cluster 方案，并显式接受极端情况下偶发并发执行的风险。</li>\n<li>如果是<strong>正确性型加锁</strong>，也就是锁失效会造成库存错误、资金错误、数据损坏这类不可接受后果，应优先考虑 ZooKeeper/etcd，并配合 Fencing Token，而不是依赖 Redis 主从故障转移保证强互斥。</li>\n<li>Redlock 依赖有界网络延迟、有界进程暂停、有界时钟漂移。GC 长停顿、时钟跳变、延迟报文都会削弱其正确性。</li>\n<li>Martin Kleppmann 的核心观点是：正确性型锁必须配合 Fencing Token；antirez 的反驳重点在于工程实践中时钟漂移影响有限、随机 value 可防误删。读者应先区分场景，再选方案。</li>\n</ol>\n<h2>基于 ZooKeeper 实现分布式锁</h2>\n<p>ZooKeeper 相比于 Redis 实现分布式锁，除了提供相对更高的可靠性之外，在功能层面还有一个非常有用的特性：<strong>Watch 机制</strong>。这个机制可以用来实现公平的分布式锁。不过，使用 ZooKeeper 实现的分布式锁在性能方面相对较差，因此如果对性能要求比较高的话，ZooKeeper 可能就不太适合了。</p>\n<h3>如何基于 ZooKeeper 实现分布式锁？</h3>\n<p>ZooKeeper 分布式锁是基于 <strong>临时顺序节点</strong> 和 <strong>Watcher（事件监听器）</strong> 实现的。</p>\n<p>获取锁：</p>\n<ol>\n<li>首先我们要有一个持久节点 <code>/locks</code>，客户端获取锁就是在 <code>/locks</code> 下创建临时顺序节点。</li>\n<li>假设客户端 1 创建了 <code>/locks/lock1</code> 节点，创建成功之后，会判断 <code>lock1</code> 是否是 <code>/locks</code> 下最小的子节点。</li>\n<li>如果 <code>lock1</code> 是最小的子节点，则获取锁成功。否则，获取锁失败。</li>\n<li>如果获取锁失败，则说明有其他的客户端已经成功获取锁。客户端 1 并不会不停地循环去尝试加锁，而是在前一个节点比如 <code>/locks/lock0</code> 上注册一个事件监听器。这个监听器的作用是当前一个节点释放锁之后通知客户端 1（避免无效自旋），这样客户端 1 就加锁成功了。</li>\n</ol>\n<p>释放锁：</p>\n<ol>\n<li>成功获取锁的客户端在执行完业务流程之后，会将对应的子节点删除。</li>\n<li>成功获取锁的客户端在出现故障之后，对应的子节点由于是临时顺序节点，也会被自动删除，避免了锁无法被释放。</li>\n<li>我们前面说的事件监听器其实监听的就是这个子节点删除事件，子节点删除就意味着锁被释放。</li>\n</ol>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-zookeeper.png\" alt></p>\n<p>实际项目中，推荐使用 Curator 来实现 ZooKeeper 分布式锁。Curator 是 Netflix 公司开源的一套 ZooKeeper Java 客户端框架，相比于 ZooKeeper 自带的客户端 zookeeper 来说，Curator 的封装更加完善，各种 API 都可以比较方便地使用。</p>\n<p><code>Curator</code>主要实现了下面四种锁：</p>\n<ul>\n<li><code>InterProcessMutex</code>：分布式可重入排它锁</li>\n<li><code>InterProcessSemaphoreMutex</code>：分布式不可重入排它锁</li>\n<li><code>InterProcessReadWriteLock</code>：分布式读写锁</li>\n<li><code>InterProcessMultiLock</code>：将多个锁作为单个实体管理的容器，获取锁的时候获取所有锁，释放锁也会释放所有锁资源（忽略释放失败的锁）。</li>\n</ul>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">CuratorFramework</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> client </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> ZKUtils</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getClient</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">client</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">start</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 分布式可重入排它锁</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">InterProcessLock</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> lock1 </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> InterProcessMutex</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(client</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> lockPath1)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 分布式不可重入排它锁</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">InterProcessLock</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> lock2 </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> InterProcessSemaphoreMutex</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(client</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> lockPath2)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 将多个锁作为一个整体</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">InterProcessMultiLock</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> lock </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> InterProcessMultiLock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">Arrays</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">asList</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(lock1, lock2)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">lock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">acquire</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">10</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">TimeUnit</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">SECONDS</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">   throw</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> IllegalStateException</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"不能获取多锁\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"已获取多锁\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"是否有第一个锁: \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> lock1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">isAcquiredInThisProcess</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"是否有第二个锁: \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> lock2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">isAcquiredInThisProcess</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">try</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 资源操作</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    resource</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">use</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">} </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">finally</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"释放多个锁\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    lock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">release</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"是否有第一个锁: \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> lock1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">isAcquiredInThisProcess</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"是否有第二个锁: \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> lock2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">isAcquiredInThisProcess</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">client</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">close</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h3>为什么要用临时顺序节点？</h3>\n<p>每个数据节点在 ZooKeeper 中被称为 <strong>znode</strong>，它是 ZooKeeper 中数据的最小单元。</p>\n<p>我们通常是将 znode 分为 4 大类：</p>\n<ul>\n<li><strong>持久（PERSISTENT）节点</strong>：一旦创建就一直存在即使 ZooKeeper 集群宕机，直到将其删除。</li>\n<li><strong>临时（EPHEMERAL）节点</strong>：临时节点的生命周期是与 <strong>客户端会话（session）</strong> 绑定的，<strong>会话消失则节点消失</strong>。并且，<strong>临时节点只能做叶子节点</strong>，不能创建子节点。</li>\n<li><strong>持久顺序（PERSISTENT_SEQUENTIAL）节点</strong>：除了具有持久（PERSISTENT）节点的特性之外，子节点的名称还具有顺序性。比如 <code>/node1/app0000000001</code>、<code>/node1/app0000000002</code>。</li>\n<li><strong>临时顺序（EPHEMERAL_SEQUENTIAL）节点</strong>：除了具备临时（EPHEMERAL）节点的特性之外，子节点的名称还具有顺序性。</li>\n</ul>\n<p>可以看出，临时节点相比持久节点，最主要的是对会话失效的情况处理不一样，临时节点会话消失则对应的节点消失。这样的话，如果客户端发生异常导致没来得及释放锁也没关系，会话失效节点会自动被删除，可以避免客户端进程崩溃后永久占锁。</p>\n<p>不过，ZooKeeper 同样需要考虑 GC 停顿、网络分区和 session timeout。客户端长时间 GC 或网络分区导致 session 过期时，ZooKeeper 会删除临时节点并允许新客户端加锁，而旧客户端可能还没感知到会话失效，仍以为自己持锁。对于正确性要求高的场景，仍应结合 Fencing Token 防止旧客户端恢复后写入陈旧数据。</p>\n<p>使用 Redis 实现分布式锁的时候，我们是通过过期时间来避免锁无法被释放导致死锁问题的，而 ZooKeeper 可以利用临时节点的特性处理客户端崩溃后的锁释放问题。</p>\n<p>假设不使用顺序节点的话，所有尝试获取锁的客户端都会对持有锁的子节点加监听器。当该锁被释放之后，势必会造成所有尝试获取锁的客户端来争夺锁，这样对性能不友好。使用顺序节点之后，只需要监听前一个节点就好了，对性能更友好。</p>\n<h3>为什么要设置对前一个节点的监听？</h3>\n<blockquote>\n<p>Watcher（事件监听器），是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher，并且在一些特定事件触发的时候，ZooKeeper 服务端会将事件通知到感兴趣的客户端上去，该机制是 ZooKeeper 实现分布式协调服务的重要特性。</p>\n</blockquote>\n<p>同一时间段内，可能会有很多客户端同时获取锁，但只有一个可以获取成功。如果获取锁失败，则说明有其他的客户端已经成功获取锁。获取锁失败的客户端并不会不停地循环去尝试加锁，而是在前一个节点注册一个事件监听器。</p>\n<p>这个事件监听器的作用是：<strong>当前一个节点对应的客户端释放锁之后（也就是前一个节点被删除之后，监听的是删除事件），通知获取锁失败的客户端（唤醒等待的线程，Java 中的 <code>wait/notifyAll</code>），让它尝试去获取锁，然后就成功获取锁了。</strong></p>\n<h3>如何实现可重入锁？</h3>\n<p>这里以 Curator 的 <code>InterProcessMutex</code> 对可重入锁的实现来介绍（源码地址：<a href=\"https://github.com/apache/curator/blob/master/curator-recipes/src/main/java/org/apache/curator/framework/recipes/locks/InterProcessMutex.java\" target=\"_blank\" rel=\"noopener noreferrer\">InterProcessMutex.java</a>）。</p>\n<p>当我们调用 <code>InterProcessMutex#acquire</code> 方法获取锁的时候，会调用 <code>InterProcessMutex#internalLock</code> 方法。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 获取可重入互斥锁，直到获取成功为止</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Override</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> acquire</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">() throws Exception {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">internalLock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    throw</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> IOException</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"Lost connection while trying to acquire lock: \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> basePath)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><code>internalLock</code> 方法会先获取当前请求锁的线程，然后从 <code>threadData</code>（<code>ConcurrentMap&lt;Thread, LockData&gt;</code> 类型）中获取当前线程对应的 <code>lockData</code>。<code>lockData</code> 包含锁的信息和加锁的次数，是实现可重入锁的关键。</p>\n<p>第一次获取锁的时候，<code>lockData</code> 为 <code>null</code>。获取锁成功之后，会将当前线程和对应的 <code>lockData</code> 放到 <code>threadData</code> 中。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> boolean</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> internalLock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">long</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> time</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> TimeUnit</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> unit) throws Exception {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  // 获取当前请求锁的线程</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">  Thread</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> currentThread </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Thread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">currentThread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  // 拿对应的 lockData</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">  LockData</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> lockData </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> threadData</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(currentThread);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  // 第一次获取锁的话，lockData 为 null</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (lockData </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 当前线程获取过一次锁之后</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 因为当前线程的锁存在，lockCount 自增后返回，实现锁重入</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    lockData</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">lockCount</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">incrementAndGet</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> true</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  // 尝试获取锁</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">  String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> lockPath </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> internals</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">attemptLock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(time, unit, </span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getLockNodeBytes</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (lockPath </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    LockData</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> newLockData </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> LockData</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(currentThread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> lockPath)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 获取锁成功之后，将当前线程和对应的 lockData 放到 threadData 中</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    threadData</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">put</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(currentThread, newLockData);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> true</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  return</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> false</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><code>LockData</code> 是 <code>InterProcessMutex</code> 中的一个静态内部类。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> final</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ConcurrentMap</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Thread</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> LockData</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">></span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> threadData </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Maps</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">newConcurrentMap</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> LockData</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 当前持有锁的线程</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    final</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Thread</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> owningThread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 锁对应的子节点</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    final</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> lockPath</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 加锁的次数</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    final</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> AtomicInteger</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> lockCount </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> AtomicInteger</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    private</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> LockData</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Thread</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> owningThread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> lockPath</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">      this</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">owningThread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> owningThread;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">      this</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">lockPath</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> lockPath;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>如果已经获取过一次锁，后面再来获取锁的话，直接就会在 <code>if (lockData != null)</code> 这里被拦下了，然后就会执行 <code>lockData.lockCount.incrementAndGet();</code> 将加锁次数加 1。</p>\n<p>整个可重入锁的实现逻辑非常简单，直接在客户端判断当前线程有没有获取锁，有的话直接将加锁次数加 1 就可以了。</p>\n<p>需要注意可重入的边界：Curator <code>InterProcessMutex</code> 的可重入仅限同一 JVM 内的同一线程。Redisson 的 <code>RLock</code> 也是通过 <code>UUID:threadId</code> 记录持锁线程和重入计数，并不表示同一业务用户跨 JVM、跨线程天然可重入。如果需要跨进程或跨 JVM 的业务级可重入，需要在应用层设计业务身份、幂等和去重逻辑。</p>\n<h2>Fencing Token 的工程落地</h2>\n<p>Fencing Token（隔离令牌）本身只是一个单调递增的版本号，只有资源端配合校验才能发挥作用。客户端访问数据库、对象存储或外部资源时必须携带 token；资源端需要保存已见过的最大 token，并拒绝任何更小 token 的写入。否则，单独生成 token 没有意义。</p>\n<p>常见落地方式如下：</p>\n<p>| 存储/系统 | Token 来源                              | 校验方式                                                                                           |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-setnx.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "分布式"
      ]
    },
    {
      "title": "分布式锁入门：为什么需要分布式锁、锁粒度、超时续约与应用场景",
      "url": "https://codingmap.strova.top/distributed-system/distributed-lock.html",
      "id": "https://codingmap.strova.top/distributed-system/distributed-lock.html",
      "summary": "分布式锁基础入门，讲解为什么需要分布式锁、互斥语义、锁粒度、owner token、安全释放、超时续约、Fencing Token，以及秒杀、库存扣减等典型应用场景。重点围绕 为什么需要分布式锁？、分布式锁应该具备哪些条件？、分布式锁的常见实现方式有哪些？、分布式锁面试怎么答？、分布式锁 等内容展开。",
      "content_html": "<p>网上有很多分布式锁相关的文章，这里写了一个相对简洁易懂的版本。面向面试和日常工作场景，先把最常见的概念和边界讲清楚。</p>\n<p>这篇文章我们先介绍一下分布式锁的基本概念。</p>\n<h2>为什么需要分布式锁？</h2>\n<p>在多线程环境中，如果多个线程同时访问并修改同一份共享资源（例如商品库存、外卖订单），且没有互斥、原子更新、乐观锁或唯一约束等保护，就可能出现数据不一致、重复处理、超卖等问题，影响程序的正确性和稳定性。</p>\n<p>举个例子，假设现在有 100 个用户参与某个限时秒杀活动，每位用户限购 1 件商品，且商品的数量只有 3 个。如果不对共享资源进行互斥访问，就可能出现以下情况：</p>\n<ul>\n<li>线程 1、2、3 等多个线程同时进入抢购方法，每个线程对应一个用户。</li>\n<li>线程 1 和线程 2 分别代表两个不同用户，它们几乎同时读到库存还剩 1 件，于是都通过库存校验，继续创建订单、扣减库存。</li>\n<li>线程 1 继续执行，将库存数量减少 1 个，然后返回成功。</li>\n<li>线程 2 也继续执行，将库存数量减少 1 个，然后返回成功。</li>\n<li>最终两个请求都成功，但库存只够卖 1 件，于是发生超卖。</li>\n</ul>\n<p>这里的限购校验和库存扣减是两个不同的约束：限购主要解决同一用户重复购买的问题，库存扣减主要解决多个用户竞争同一份库存的问题。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/oversold-without-locking.png\" alt=\"共享资源未互斥访问导致出现问题\"></p>\n<p>锁的思路是把某段临界区串行化：同一时刻只允许一个执行单元进入这段逻辑。它能降低并发冲突，但也会牺牲吞吐；如果能用数据库原子更新、唯一约束、乐观锁、CAS 或消息串行化解决，就不一定要上分布式锁。</p>\n<p>比如防超卖不一定要用分布式锁：数据库条件更新 <code>UPDATE stock SET count = count - 1 WHERE sku_id = ? AND count &gt; 0</code> 可以保证库存不扣成负数；用户限购可以对 <code>user_id + activity_id</code> 建唯一索引；创建订单可以使用幂等键防重复提交。高并发场景还可以结合 Redis 预扣库存、MQ 异步落库和对账补偿。</p>\n<p>这里讨论的分布式锁，本质上是一种悲观互斥方案：先拿到锁，再进入临界区，拿不到锁就等待、失败或重试。</p>\n<p>悲观锁总是假设最坏的情况，认为共享资源每次被访问的时候都可能出现问题（比如共享数据被修改），所以每次在获取资源操作的时候都会上锁，这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说，<strong>共享资源每次只给一个线程使用，其他线程阻塞，用完后再把资源转让给其他线程</strong>。</p>\n<p>对于单机多线程来说，在 Java 中，我们通常使用 <code>ReentrantLock</code> 类、<code>synchronized</code> 关键字这类 JDK 自带的 <strong>本地锁</strong> 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。</p>\n<p>下面是我对本地锁画的一张示意图。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/jvm-local-lock.png\" alt=\"本地锁\"></p>\n<p>从图中可以看出，这些线程访问共享资源是互斥的，同一时刻只有一个线程可以获取到本地锁访问共享资源。</p>\n<p>分布式系统下，不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源，使用本地锁就没办法实现资源的互斥访问。这时就需要把锁的状态放到所有进程都能访问的外部系统中，也就是 <strong>分布式锁</strong>。</p>\n<p>举个例子：系统的订单服务一共部署了 3 份，都对外提供服务。为了防止超卖，需要保护的不是单独的“检查库存”，而是“校验库存 → 扣减库存 → 记录购买/创建订单”这段临界区；否则只锁查询、不锁扣减，仍然可能并发写错。由于订单服务位于不同的 JVM 进程中，本地锁在这种情况下就没办法正常工作。我们需要用到分布式锁，这样即使多个线程不在同一个 JVM 进程中，也能获取到同一把锁，进而实现共享资源的互斥访问。</p>\n<p>下面是我对分布式锁画的一张示意图。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock.png\" alt=\"分布式锁\"></p>\n<p>从图中可以看出，这些独立的进程中的线程访问共享资源是互斥的，同一时刻只有一个线程可以获取到分布式锁访问共享资源。</p>\n<h2>分布式锁应该具备哪些条件？</h2>\n<p>一个最基本的分布式锁需要满足：</p>\n<ul>\n<li><strong>互斥</strong>：对同一个资源对应的同一个 lock key，同一时刻只能有一个有效持有者。lock key 要按资源粒度设计，例如 <code>stock:{skuId}</code>、<code>order:{orderId}</code>，避免把无关资源都塞进一把全局大锁。</li>\n<li><strong>高可用和防死锁</strong>：锁服务本身要尽量可用；同时要有过期时间、会话机制或租约机制，避免客户端崩溃后锁永久不释放。但过期时间必须和业务执行时间、续约机制一起设计，否则可能出现锁提前过期、两个客户端同时进入临界区的问题。</li>\n<li><strong>安全释放</strong>：释放锁时必须校验锁持有者身份，只能释放自己持有的锁。以 Redis 为例，获取锁时写入随机 value，释放时用 Lua 脚本先比较 value，再删除 key。</li>\n</ul>\n<p>除了上面这三个基本条件之外，一个好的分布式锁还需要满足下面这些条件：</p>\n<ul>\n<li><strong>可重入</strong>：不是所有场景都必须具备，但如果同一线程/请求链路可能重复进入同一临界区，就需要记录锁持有者和重入次数，避免自己把自己阻塞。</li>\n<li><strong>高性能</strong>：获取和释放锁的操作应该快速完成，并且不应该对整个系统的性能造成过大影响。</li>\n<li><strong>获取语义明确</strong>：获取锁可以是阻塞等待、限时等待，也可以是立即失败。生产中通常要设置最大等待时间和重试退避，不能无限等待。</li>\n<li><strong>续约机制</strong>：锁 TTL 要结合业务临界区的 P99 执行时间设置；临界区可能超过 TTL 时，需要看门狗/租约续约，或者缩短临界区。</li>\n<li><strong>Fencing Token</strong>：更严格的场景还需要 Fencing Token。每次成功获取锁时生成一个单调递增 token，下游资源只接受 token 更大的写入，用来拦截锁过期后旧持有者的迟到写。</li>\n</ul>\n<h2>分布式锁的常见实现方式有哪些？</h2>\n<p>常见分布式锁实现方案如下：</p>\n<ul>\n<li>基于关系型数据库比如 MySQL 实现分布式锁。</li>\n<li>基于分布式协调服务 ZooKeeper 实现分布式锁。</li>\n<li>基于 Redis 这类高性能键值存储（Key-Value Store），或 etcd 这类分布式一致性键值存储实现分布式锁。</li>\n</ul>\n<p>数据库实现大致有三类：唯一索引插入锁表、基于事务的 <code>SELECT ... FOR UPDATE</code> 行锁、MySQL <code>GET_LOCK()</code> 这类命名锁。它们都能实现一定程度的互斥，但性能、释放时机、超时语义和故障恢复方式不同。</p>\n<p>数据库方案不是不能做失效，而是失效语义和性能通常不如 Redis/ZooKeeper/etcd 这类方案自然。比如锁表可以加过期时间字段，但要处理过期锁抢占、时钟一致性、清理任务和事务隔离；<code>GET_LOCK()</code> 依赖 MySQL 连接/session 语义，不适合所有业务链路。</p>\n<p>Redis 锁更常用于高性能、短临界区、允许通过业务幂等兜底的场景；ZooKeeper/etcd 更适合需要会话语义、顺序节点、租约和更强一致性的协调场景，但吞吐、延迟和运维成本通常更高。我专门写了一篇文章来详细介绍 Redis 和 ZooKeeper 这两种方案：<a href=\"/distributed-system/distributed-lock-implementations.html\" target=\"_blank\">分布式锁常见实现方案总结</a>。</p>\n<p>最后提醒一句：<strong>分布式锁不是分布式事务。锁只能控制临界区并发进入，不保证数据库提交一定成功，也不保证消息发送和订单写入原子一致。业务一致性仍要依赖本地事务、幂等、状态机、补偿任务等机制。</strong></p>\n<h2>分布式锁面试怎么答？</h2>\n<p>面试可以按“为什么需要、怎么实现、有什么坑、如何兜底”来回答。</p>\n<p>为什么需要：</p>\n<ul>\n<li>单机锁只能保护一个 JVM 内的线程。</li>\n<li>分布式部署后，同一业务资源可能被多个服务实例同时操作，需要跨进程互斥。</li>\n<li>典型场景包括库存扣减、订单防重复处理、定时任务防多节点重复执行、缓存重建防击穿。</li>\n</ul>\n<p>常见实现：</p>\n<ul>\n<li><strong>Redis</strong>：使用 <code>SET key value NX PX ttl</code> 获取锁，用 Lua 脚本校验 value 后删除锁。优点是性能好、实现简单；缺点是要处理 TTL、续约、主从切换和业务幂等。</li>\n<li><strong>ZooKeeper</strong>：使用临时顺序节点实现锁，客户端会话断开后临时节点自动删除。优点是会话语义清晰、公平性更容易做；缺点是性能和运维成本相对更高。</li>\n<li><strong>etcd</strong>：基于租约和事务机制实现锁，适合强一致协调场景。</li>\n<li><strong>数据库</strong>：可以用唯一索引、行锁、命名锁等方式实现，但通常吞吐和故障恢复语义不如专门的协调组件。</li>\n</ul>\n<p>Redis 锁必须注意：</p>\n<ul>\n<li>加锁要有过期时间，避免服务宕机后死锁。</li>\n<li>value 要用唯一随机值或 owner token，释放锁时只能释放自己的锁。</li>\n<li>释放锁的“判断 value + 删除 key”要用 Lua 保证原子性。</li>\n<li>业务执行时间可能超过 TTL 时，要有续约机制，或缩短临界区。</li>\n<li>锁过期后旧持有者仍继续写入时，可能出现并发写错，严格场景要加 Fencing Token 或业务版本号。</li>\n</ul>\n<p>兜底手段：</p>\n<ul>\n<li>数据库唯一索引防重复。</li>\n<li>状态机防止状态回退。</li>\n<li>乐观锁/版本号防覆盖。</li>\n<li>幂等表或业务幂等号防重复请求。</li>\n<li>异常补偿和对账任务修复半成功状态。</li>\n</ul>\n<h2>总结</h2>\n<p>这篇文章我们主要介绍了：</p>\n<ul>\n<li>分布式锁的用途：分布式系统下，不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话，使用本地锁就没办法实现资源的互斥访问了。</li>\n<li>分布式锁应该具备的条件：互斥、高可用和防死锁、安全释放、可重入、高性能、获取语义明确、续约机制。更严格的场景还要配合 Fencing Token。</li>\n<li>分布式锁的常见实现方式：关系型数据库比如 MySQL、分布式协调服务 ZooKeeper、Redis 这类高性能键值存储、etcd 这类分布式一致性键值存储。</li>\n</ul>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/oversold-without-locking.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "分布式"
      ]
    },
    {
      "title": "2026 最新分布式系统面试题总结：CAP、Raft、RPC、分布式锁、事务与 ID",
      "url": "https://codingmap.strova.top/distributed-system/distributed-system-interview-questions.html",
      "id": "https://codingmap.strova.top/distributed-system/distributed-system-interview-questions.html",
      "summary": "2026 最新分布式系统面试题和复习路线汇总，覆盖 CAP、BASE、Paxos、Raft、ZAB、Gossip、一致性哈希、RPC、API 网关、分布式 ID、分布式锁、分布式事务、配置中心和 ZooKeeper 等高频考点。重点围绕 分布式面试先抓主线、第一阶段：分布式理论与算法、第二阶段：RPC 与 API 网。",
      "content_html": "<p>准备分布式系统面试，最容易踩的坑是把知识点背成一堆孤立概念：CAP 是一个点、RPC 是一个点、分布式锁是一个点、分布式事务又是一个点。</p>\n<p>真正到面试里，面试官更关心的是：<strong>你能不能把这些技术放回真实系统里，讲清楚它们解决什么问题、带来什么代价、适合什么场景</strong>。</p>\n<p>这篇文章是 JavaGuide 分布式系统内容的面试复习导航，不会重复搬运所有答案，而是帮你把分布式相关文章串起来，按面试准备顺序建立一条清晰路径。</p>\n<h2>分布式面试先抓主线</h2>\n<p>分布式系统面试通常围绕 4 条主线展开：</p>\n<ol>\n<li><strong>一致性与可用性的权衡</strong>：CAP、BASE、最终一致性、共识算法。</li>\n<li><strong>跨节点通信与治理</strong>：RPC、注册发现、API 网关、配置中心。</li>\n<li><strong>分布式数据一致性问题</strong>：分布式 ID、分布式锁、分布式事务。</li>\n<li><strong>典型中间件与落地场景</strong>：ZooKeeper、Dubbo、Spring Cloud Gateway 等。</li>\n</ol>\n<p>其中，最重要的是：<strong>API 网关、配置中心、分布式 ID、分布式锁和分布式事务</strong>。这几块内容需要你花费更多的时间。</p>\n<p>如果时间有限，建议先看面试突击版：<a href=\"https://interview.javaguide.cn/distributed-system/distributed-system.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式系统常见面试题总结</a>。它已经把面试最高频的问题整理了出来，非常适合时间有限的情况下面试突击。</p>\n<p>如果你还有时间系统补基础，可以按下面这条路线阅读 JavaGuide 正站文章。</p>\n<h2>第一阶段：分布式理论与算法</h2>\n<p>分布式理论是很多系统设计题的底层语言。它不一定每天都写进代码，但会决定你回答问题时有没有“架构感”。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/gossip-centralized-vs-decentralized.png\" alt=\"分布式系统通信机制：中心化 vs 去中心化\"></p>\n<p>重点文章：</p>\n<ul>\n<li><a href=\"https://javaguide.cn/distributed-system/protocol/cap-and-base-theorem.html\" target=\"_blank\" rel=\"noopener noreferrer\">CAP 理论和 BASE 理论解读</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/protocol/paxos-algorithm.html\" target=\"_blank\" rel=\"noopener noreferrer\">Paxos 算法解读</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/protocol/raft-algorithm.html\" target=\"_blank\" rel=\"noopener noreferrer\">Raft 算法解读</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/protocol/zab.html\" target=\"_blank\" rel=\"noopener noreferrer\">ZAB 协议详解</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/protocol/gossip-protocol.html\" target=\"_blank\" rel=\"noopener noreferrer\">Gossip 协议详解</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/protocol/consistent-hashing.html\" target=\"_blank\" rel=\"noopener noreferrer\">一致性哈希算法详解</a></li>\n</ul>\n<p>高频面试问题：</p>\n<ul>\n<li>CAP 是不是“三选二”？为什么说 P 在分布式系统里基本无法回避？</li>\n<li>BASE 和 ACID 的区别是什么？最终一致性如何落地？</li>\n<li>Paxos、Raft、ZAB 分别解决什么问题？为什么 Raft 更容易理解和工程实现？</li>\n<li>Gossip 协议为什么适合节点发现和状态传播？</li>\n<li>一致性哈希解决了什么问题？虚拟节点有什么作用？</li>\n</ul>\n<p>这一阶段的复习重点不是背定义，而是能说清楚：<strong>当网络不可靠、节点会宕机、数据要多副本存储时，系统为什么必须做取舍</strong>。</p>\n<h2>第二阶段：RPC 与 API 网关</h2>\n<p>微服务面试里，RPC 和 API 网关经常一起出现。RPC 关注服务之间如何调用，网关关注外部流量如何进入系统。</p>\n<p>API 网关示意图如下：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway-overview.png\" alt=\"网关示意图\"></p>\n<p>RPC 示意图如下：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/rpc-overview.png\" alt=\"RPC 概览\"></p>\n<p>重点文章：</p>\n<ul>\n<li><a href=\"https://javaguide.cn/distributed-system/rpc/rpc-intro.html\" target=\"_blank\" rel=\"noopener noreferrer\">RPC 基础常见面试题总结</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/rpc/dubbo.html\" target=\"_blank\" rel=\"noopener noreferrer\">Dubbo 常见面试题总结</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/rpc/http&amp;rpc.html\" target=\"_blank\" rel=\"noopener noreferrer\">HTTP 和 RPC 有什么区别？</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/api-gateway.html\" target=\"_blank\" rel=\"noopener noreferrer\">API 网关基础知识总结</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/spring-cloud-gateway-questions.html\" target=\"_blank\" rel=\"noopener noreferrer\">Spring Cloud Gateway 常见问题总结</a></li>\n</ul>\n<p>高频面试问题：</p>\n<ul>\n<li>RPC 和 HTTP 有什么区别？为什么服务内部调用常用 RPC？</li>\n<li>一个 RPC 框架通常包含哪些核心模块？</li>\n<li>服务注册与发现、负载均衡、序列化、超时重试分别解决什么问题？</li>\n<li>API 网关和 Nginx、负载均衡器、BFF 的边界是什么？</li>\n<li>Spring Cloud Gateway 的过滤器链、路由匹配、限流熔断如何理解？</li>\n</ul>\n<p>这一阶段建议把“调用链路”画出来：客户端请求如何进网关，网关如何路由到服务，服务之间如何通过 RPC 调用，失败时如何超时、重试、降级。</p>\n<h2>第三阶段：分布式 ID、锁和事务</h2>\n<p>这部分是后端面试的高频区，也是最容易被追问工程细节的部分，一定一定要花费更多时间准备。</p>\n<p>重点文章：</p>\n<ul>\n<li><a href=\"https://javaguide.cn/distributed-system/distributed-id.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式ID介绍&amp;实现方案总结</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/distributed-id-design.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式 ID 设计指南</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/distributed-lock.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式锁介绍</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/distributed-lock-implementations.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式锁常见实现方案总结</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/distributed-transaction.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式事务解决方案总结</a></li>\n</ul>\n<p>高频面试问题：</p>\n<ul>\n<li>为什么分库分表后不能继续依赖数据库自增 ID？</li>\n<li>UUID、数据库号段、Redis、Snowflake 各有什么优缺点？</li>\n<li>Redis 分布式锁为什么要设置过期时间？为什么要用 Lua 保证释放锁的原子性？</li>\n<li>Redisson 看门狗解决了什么问题？它又引入了哪些边界？</li>\n<li>2PC、TCC、本地消息表、事务消息、Saga 分别适合什么场景？</li>\n</ul>\n<p>准备这部分时，一定要讲“异常路径”：网络超时、锁过期、业务执行一半失败、消息重复投递、事务补偿失败。这些才是面试官真正想听的工程判断。</p>\n<h2>第四阶段：配置中心与 ZooKeeper</h2>\n<p>配置中心和 ZooKeeper 通常不是单独考一个大题，而是穿插在注册发现、配置变更、分布式协调、Leader 选举等问题里。</p>\n<p>ZooKeeper 可以选择跳过，目前面试问的不多。有一种情况必须准备，那就是你的项目明确用到了或者你的技能介绍中提到了。</p>\n<p>重点文章：</p>\n<ul>\n<li><a href=\"https://javaguide.cn/distributed-system/distributed-configuration-center.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式配置中心面试题总结</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.html\" target=\"_blank\" rel=\"noopener noreferrer\">ZooKeeper相关概念总结(入门)</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.html\" target=\"_blank\" rel=\"noopener noreferrer\">ZooKeeper相关概念总结(进阶)</a></li>\n</ul>\n<p>高频面试问题：</p>\n<ul>\n<li>配置中心为什么不能只是一个配置文件仓库？</li>\n<li>配置变更如何推送？如何保证客户端拿到的是新配置？</li>\n<li>ZooKeeper 的临时节点、顺序节点、Watcher 分别能解决什么问题？</li>\n<li>ZooKeeper 为什么适合做分布式协调？</li>\n<li>ZooKeeper 和注册中心、配置中心之间是什么关系？</li>\n</ul>\n<h2>推荐复习顺序</h2>\n<p>如果你是临近面试，建议先用“高频题目定范围，再用专题文章补细节”的方式复习：</p>\n<ol>\n<li>先看 <a href=\"https://interview.javaguide.cn/distributed-system/distributed-system.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式系统常见面试题总结</a>，快速建立高频问题清单，知道哪些内容最容易被问到。</li>\n<li>再补理论基础：<a href=\"https://javaguide.cn/distributed-system/protocol/cap-and-base-theorem.html\" target=\"_blank\" rel=\"noopener noreferrer\">CAP 理论和 BASE 理论解读</a>、<a href=\"https://javaguide.cn/distributed-system/protocol/raft-algorithm.html\" target=\"_blank\" rel=\"noopener noreferrer\">Raft 算法解读</a>、<a href=\"https://javaguide.cn/distributed-system/protocol/consistent-hashing.html\" target=\"_blank\" rel=\"noopener noreferrer\">一致性哈希算法详解</a>。这一步重点是理解系统为什么要在一致性、可用性和扩展性之间做取舍。</li>\n<li>然后看通信与流量入口：<a href=\"https://javaguide.cn/distributed-system/rpc/rpc-intro.html\" target=\"_blank\" rel=\"noopener noreferrer\">RPC 基础常见面试题总结</a>、<a href=\"https://javaguide.cn/distributed-system/api-gateway.html\" target=\"_blank\" rel=\"noopener noreferrer\">API 网关基础知识总结</a>。这一步要能讲清楚一次请求从网关进入系统，再到服务之间互相调用的完整链路。</li>\n<li>最后重点啃工程落地高频题：<a href=\"https://javaguide.cn/distributed-system/distributed-id.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式 ID</a>、<a href=\"https://javaguide.cn/distributed-system/distributed-lock.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式锁</a>、<a href=\"https://javaguide.cn/distributed-system/distributed-transaction.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式事务</a>。这一步不要只背方案优缺点，更要准备异常场景和兜底策略。</li>\n</ol>\n<p>如果你准备的是社招或中高级岗位，不要只背标准答案。更重要的是能把方案放进具体业务场景里，讲清楚为什么这么选、失败后怎么兜底、系统压力上来后怎么扩展。</p>\n<h2>面试突击版推荐</h2>\n<p>这篇文章主要是 JavaGuide 正站的分布式学习导航。如果你想直接刷高频问答，可以看我已经整理好的面试突击版： <a href=\"https://interview.javaguide.cn/distributed-system/distributed-system.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式系统常见面试题总结</a>。</p>\n<p>面试突击版更适合临考前快速过一遍；正站文章更适合系统学习和补齐细节。两者配合使用，效果会更稳。</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/gossip-centralized-vs-decentralized.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "分布式"
      ]
    },
    {
      "title": "分布式事务解决方案详解：XA、AT、TCC、Saga、本地消息表与事务消息",
      "url": "https://codingmap.strova.top/distributed-system/distributed-transaction.html",
      "id": "https://codingmap.strova.top/distributed-system/distributed-transaction.html",
      "summary": "分布式事务解决方案详解，覆盖 2PC、3PC、XA、Seata AT、TCC、Saga、本地消息表、RocketMQ 事务消息、最大努力通知等方案的原理、优缺点、适用场景和面试考点。重点围绕 事务、数据库事务、分布式事务、分布式事务基础理论、分布式事务 等内容展开。结合 CodingMap 知识体系梳理 分布式系统 。",
      "content_html": "<p><strong>网上已经有很多关于分布式事务的文章了，为啥还要写一篇？</strong></p>\n<ol>\n<li>第一是我觉得大部分文章理解起来挺难的，不太适合一些经验不多的小伙伴。这篇文章我的目标就是让即使是没啥工作经验的小伙伴们都能真正看懂分布式事务。</li>\n<li>第二是我觉得大部分文章介绍的不够详细，很多分布式事务相关的重要概念都没有提到。</li>\n</ol>\n<p>开始聊分布式事务之前，我们先来回顾一下事务相关的概念。</p>\n<blockquote>\n<p><strong>版本说明</strong>：本文 Seata 相关内容基于 1.7+ / 2.x 文档，RocketMQ 事务消息基于 4.9+ / 5.x 文档。不同版本的默认参数、API 和部署方式可能存在差异，落地时要以项目实际版本文档为准。</p>\n</blockquote>\n<h2>事务</h2>\n<p>我们设想一个场景，这个场景中我们需要插入多条相关联的数据到数据库，不幸的是，这个过程可能会遇到下面这些问题：</p>\n<ul>\n<li>数据库中途突然因为某些原因挂掉了。</li>\n<li>客户端突然因为网络原因连接不上数据库了。</li>\n<li>并发访问数据库时，多个线程同时写入数据库，覆盖了彼此的更改。</li>\n<li>……</li>\n</ul>\n<p>上面的任何一个问题都可能会导致数据的不一致性。为了保证数据的一致性，系统必须能够处理这些问题。事务就是我们抽象出来简化这些问题的首选机制。事务的概念起源于数据库，目前，已经成为一个比较广泛的概念。</p>\n<p><strong>何为事务？</strong> 一言以蔽之，<strong>事务是逻辑上的一组操作，要么都执行，要么都不执行。</strong></p>\n<p>事务最经典、最常被引用的例子就是转账。假如小明要给小红转账 1000 元，这个转账会涉及到两个关键操作，这两个操作必须都成功或者都失败。</p>\n<ol>\n<li>将小明的余额减少 1000 元</li>\n<li>将小红的余额增加 1000 元。</li>\n</ol>\n<p>事务会把这两个操作看成逻辑上的一个整体，这个整体包含的操作要么都成功，要么都失败。这样就不会出现小明余额减少而小红的余额却并没有增加的情况。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/mysql/事务示意图.png\" alt></p>\n<h2>数据库事务</h2>\n<p>大多数情况下，我们在谈论事务的时候，如果没有特指<strong>分布式事务</strong>，往往指的就是<strong>数据库事务</strong>。</p>\n<p>数据库事务在我们日常开发中接触的最多了。如果你的项目属于单体架构的话，你接触到的往往就是数据库事务了。</p>\n<p><strong>那数据库事务有什么作用呢？</strong></p>\n<p>简单来说，数据库事务可以保证多个对数据库的操作（也就是 SQL 语句）构成一个逻辑上的整体。构成这个逻辑上的整体的这些数据库操作遵循：<strong>要么全部执行成功，要么全部不执行</strong>。</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"># 开启一个事务</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">START TRANSACTION</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"># 多条 </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SQL</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> 语句</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">SQL1,SQL2...</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">## 提交事务</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">COMMIT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><img src=\"https://oss.javaguide.cn/github/javaguide/mysql/数据库事务示意图.png\" alt=\"数据库事务示意图\"></p>\n<p>另外，关系型数据库（例如：<code>MySQL</code>、<code>SQL Server</code>、<code>Oracle</code> 等）事务都有 <strong>ACID</strong> 特性：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/mysql/ACID.png\" alt=\"ACID\"></p>\n<ol>\n<li><strong>原子性</strong>（<code>Atomicity</code>）：事务是最小的执行单位，不允许分割。事务的原子性确保动作要么全部完成，要么完全不起作用；</li>\n<li><strong>一致性</strong>（<code>Consistency</code>）：执行事务前后，数据保持一致，例如转账业务中，无论事务是否成功，转账者和收款人的总额应该是不变的；</li>\n<li><strong>隔离性</strong>（<code>Isolation</code>）：并发访问数据库时，一个用户的事务不被其他事务所干扰，各并发事务之间数据库是独立的；</li>\n<li><strong>持久性</strong>（<code>Durability</code>）：一个事务被提交之后。它对数据库中数据的改变是持久的，即使数据库发生故障也不应该对其有任何影响。</li>\n</ol>\n<p>🌈 这里要额外补充一点：<strong>只有保证了事务的持久性、原子性、隔离性之后，一致性才能得到保障。也就是说 A、I、D 是手段，C 是目的！</strong> 想必大家和我一样，被 ACID 这个概念误导了很久！我也是看周志明老师的公开课<a href=\"https://time.geekbang.org/opencourse/intro/100064201\" target=\"_blank\" rel=\"noopener noreferrer\">《周志明的软件架构课》</a>才搞清楚的（多看好书！！！）。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/mysql/AID-&gt;C.png\" alt=\"AID-&gt;C\"></p>\n<p>另外，DDIA 也就是 <a href=\"https://book.douban.com/subject/30329536/\" target=\"_blank\" rel=\"noopener noreferrer\">《Designing Data-Intensive Application（数据密集型应用系统设计）》</a> 的作者在他的这本书中如是说：</p>\n<blockquote>\n<p>Atomicity, isolation, and durability are properties of the database, whereas consis‐<br>\ntency (in the ACID sense) is a property of the application. The application may rely<br>\non the database’s atomicity and isolation properties in order to achieve consistency,<br>\nbut it’s not up to the database alone.</p>\n<p>翻译过来的意思是：原子性，隔离性和持久性是数据库的属性，而一致性（在 ACID 意义上）是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性，但这并不仅取决于数据库。因此，字母 C 不属于 ACID 。</p>\n</blockquote>\n<p>《Designing Data-Intensive Application（数据密集型应用系统设计）》这本书强推一波，值得读很多遍！豆瓣有接近 90% 的人看了这本书之后给了五星好评。另外，中文翻译版本已经在 GitHub 开源，地址：<a href=\"https://github.com/Vonng/ddia\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Vonng/ddia</a>。</p>\n<p><img src=\"https://img-blog.csdnimg.cn/20210526162552353.png\" alt></p>\n<p><strong>数据库事务的实现原理呢？</strong></p>\n<p>我们这里以 MySQL 的 InnoDB 引擎为例来简单说一下。</p>\n<p>MySQL InnoDB 引擎使用 <strong>redo log（重做日志）</strong> 保证事务的<strong>持久性</strong>，使用 <strong>undo log（回滚日志）</strong> 来保证事务的<strong>原子性</strong>。MySQL InnoDB 引擎通过 <strong>锁机制</strong>、<strong>MVCC</strong> 等手段来保证事务的隔离性（默认支持的隔离级别是 <strong><code>REPEATABLE-READ</code></strong>）。</p>\n<h2>分布式事务</h2>\n<p>微服务架构下，一个系统被拆分为多个小的微服务。每个微服务都可能存在不同的机器上，并且每个微服务可能都有一个单独的数据库供自己使用。这种情况下，一组操作可能会涉及到多个微服务以及多个数据库。举个例子：电商系统中，你创建一个订单往往会涉及到订单服务（订单数加一）、库存服务（库存减一）等等服务，这些服务会有供自己单独使用的数据库。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-transaction/distributed-transaction-with-two-services.png\" alt=\"分布式事务示意图\"></p>\n<p><strong>那么如何保证这一组操作要么都执行成功，要么都执行失败呢？</strong></p>\n<p>这个时候单单依靠数据库事务就不行了！我们就需要引入 <strong>分布式事务</strong> 这个概念了！</p>\n<p>实际上，只要跨数据库的场景都需要用到引入分布式事务。比如说单个数据库的性能达到瓶颈或者数据量太大的时候，我们需要进行 <strong>分库</strong>。分库之后，同一个数据库中的表分布在了不同的数据库中，如果单个操作涉及到多个数据库，那么数据库自带的事务就无法满足我们的要求了。</p>\n<p>一言以蔽之，<strong>分布式事务的终极目标就是保证系统中多个相关联的数据库中的数据的一致性！</strong></p>\n<p>那既然分布式事务也属于事务，理论上就应该遵守事务的 ACID 四大特性。但是，考虑到性能、可用性等各方面因素，我们往往是无法完全满足 ACID 的，只能选择一个比较折中的方案。</p>\n<p>针对分布式事务，又诞生了一些新的理论。</p>\n<h2>分布式事务基础理论</h2>\n<h3>CAP 理论和 BASE 理论</h3>\n<p>CAP 理论和 BASE 理论是分布式领域非常非常重要的两个理论。不夸张地说，只要问到分布式相关的内容，面试官几乎是必定会问这两个分布式相关的理论。</p>\n<p>不论是你面试也好，工作也罢，都非常有必要将这两个理论搞懂，并且能够用自己的理解给别人讲出来。</p>\n<p>我这里就不多提这两个理论了，不了解的小伙伴，可以看我前段时间写过的一篇相关的文章：<a href=\"https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&amp;mid=2247495298&amp;idx=1&amp;sn=965be0f54ab44bda818656db1f21a39f&amp;chksm=cea1a149f9d6285f1169413ab7663ca2a9c1a8440a5ae5816566eb66b20e4d86f5db1002f66c&amp;token=657875872&amp;lang=zh_CN#rd\" target=\"_blank\" rel=\"noopener noreferrer\">《CAP 和 BASE 理论了解么？可以结合实际案例说下不？》</a> 。</p>\n<h3>一致性的 3 种级别</h3>\n<p>我们可以把对于系统一致性的要求分为下面 3 种级别：</p>\n<ul>\n<li><strong>强一致性</strong>：系统写入了什么，读出来的就是什么。</li>\n<li><strong>弱一致性</strong>：不一定可以读取到最新写入的值，也不保证多少时间之后读取到的数据是最新的，只是会尽量保证某个时刻达到数据一致的状态。</li>\n<li><strong>最终一致性</strong>：弱一致性的升级版。系统会保证在一定时间内达到数据一致的状态。</li>\n</ul>\n<p>除了上面这 3 个比较常见的一致性级别之外，还有读写一致性、因果一致性等一致性模型，具体可以参考<a href=\"https://es.cs.uni-kl.de/publications/datarsg/Senf13.pdf\" target=\"_blank\" rel=\"noopener noreferrer\">《Operational Characterization of Weak Memory Consistency Models》</a>这篇论文。因为日常工作中这些一致性模型很少见，我这里就不多做阐述（因为我自己也不是特别了解 😅）。</p>\n<p>业界比较推崇是 <strong>最终一致性</strong>，但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。</p>\n<h3>柔性事务</h3>\n<p>互联网应用最关键的就是要保证高可用，分布式系统几秒钟无法使用都可能造成巨大损失。在此场景下，一些大佬们在 CAP 理论和 BASE 理论的基础上，提出了 <strong>柔性事务</strong> 的概念。<strong>柔性事务追求的是最终一致性。</strong></p>\n<p>实际上，柔性事务就是 <strong>BASE 理论 + 业务实践</strong>。柔性事务追求的目标是：我们根据自身业务特性，通过适当的方式来保证系统数据的最终一致性。像 <strong>TCC</strong>、<strong>Saga</strong>、<strong>MQ 事务</strong>、<strong>本地消息表</strong> 就属于柔性事务。</p>\n<h3>刚性事务</h3>\n<p>与柔性事务相对的就是 <strong>刚性事务</strong> 了。前面我们说了，<strong>柔性事务追求的是最终一致性</strong>。那么，与之对应，刚性事务追求的就是 <strong>强一致性</strong>。像 <strong>2PC</strong>、<strong>3PC</strong> 就属于刚性事务。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-transaction/distributed-transaction-solution-summary.png\" alt=\"分布式事务解决方案总结\"></p>\n<h2>分布式事务解决方案</h2>\n<p>分布式事务的解决方案有很多，比如：<strong>XA / 2PC</strong>、<strong>3PC</strong>、<strong>AT</strong>、<strong>TCC</strong>、<strong>Saga</strong>、<strong>本地消息表</strong>、<strong>MQ 事务</strong>（Kafka 和 RocketMQ 都提供了事务相关功能）、<strong>最大努力通知</strong>等等。</p>\n<p>2PC、3PC 属于业务代码无侵入方案。XA 规范是 X/Open 组织定义的分布式事务处理（DTP，Distributed Transaction Processing）标准，定义了 TM 与 RM 之间的接口，并通常通过 2PC 完成提交。TCC、Saga 属于业务侵入方案，AT 介于 XA 与 TCC 之间，MQ 事务依赖于使用消息队列的场景，本地消息表和最大努力通知主要追求最终一致性，不支持自动回滚。</p>\n<p>这些方案的适用场景有所区别，我们需要根据具体的场景选择适合自己项目的解决方案。</p>\n<p>一个简单的选型思路：</p>\n<ul>\n<li><strong>不能容忍最终一致性</strong>：优先考虑 XA / 2PC，适合金融、账务等短事务强一致场景，但要接受性能和可用性成本。</li>\n<li><strong>希望业务少改造</strong>：可以评估 Seata AT、本地消息表或 MQ 事务。AT 对业务代码侵入较低，但对数据库表结构、SQL 支持范围和全局锁有要求。</li>\n<li><strong>可以接受业务侵入，且链路较短</strong>：可以考虑 TCC，典型场景是账户冻结、库存预留、优惠券锁定等需要资源预留的业务。</li>\n<li><strong>链路较长、步骤较多</strong>：可以考虑 Saga，典型场景是订单履约、旅行预订、审批流等长事务流程。</li>\n<li><strong>只需要通知对方最终完成</strong>：可以考虑最大努力通知，例如支付回调、物流状态通知、积分发放等。</li>\n</ul>\n<p>开始介绍 2PC 和 3PC 之前，我们先来介绍一下 2PC 和 3PC 涉及到的一些角色（XA 规范的角色组成）：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-transaction/xa-specification-roles.png\" alt></p>\n<ul>\n<li><strong>AP（Application Program）</strong>：应用程序本身。</li>\n<li><strong>RM（Resource Manager）</strong>：资源管理器，也就是事务的参与者，绝大部分情况下就是指数据库（后文会以关系型数据库为例），一个分布式事务往往涉及到多个 RM。</li>\n<li><strong>TM（Transaction Manager）</strong>：事务管理器，负责管理全局事务，分配事务唯一标识，监控事务的执行进度，并负责事务的提交、回滚、失败恢复等。</li>\n</ul>\n<h3>2PC（两阶段提交协议）</h3>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-transaction/2pc-work-flow.png\" alt></p>\n<p>2PC（Two-Phase Commit）这三个字母的含义：</p>\n<ul>\n<li><strong>2</strong> -&gt; 指代事务提交的 2 个阶段</li>\n<li><strong>P</strong> -&gt; Prepare（准备阶段）</li>\n<li><strong>C</strong> -&gt; Commit（提交阶段）</li>\n</ul>\n<p>2PC 将事务的提交过程分为 2 个阶段：<strong>准备阶段</strong> 和 <strong>提交阶段</strong> 。</p>\n<h4>准备阶段（Prepare）</h4>\n<p>准备阶段的核心是“询问”事务参与者执行本地数据库事务操作是否成功。</p>\n<p>准备阶段的工作流程：</p>\n<ol>\n<li><strong>事务协调者/管理者（后文简称 TM）</strong> 向所有涉及到的 <strong>事务参与者（后文简称 RM）</strong> 发送消息询问：“你是否可以执行事务操作呢？”，并等待其答复。</li>\n<li><strong>RM</strong> 接收到消息之后，开始执行本地数据库事务预操作比如写 redo log/undo log 日志，<strong>此时并不会提交事务</strong> 。</li>\n<li><strong>RM</strong> 如果执行本地数据库事务操作成功，那就回复“Yes”表示我已就绪，否则就回复“No”表示我未就绪。</li>\n</ol>\n<h4>提交阶段（Commit）</h4>\n<p>提交阶段的核心是“询问”事务参与者提交本地事务是否成功。</p>\n<p>当所有事务参与者都是“就绪”状态的话：</p>\n<ol>\n<li><strong>TM</strong> 向所有参与者发送消息：“你们可以提交事务啦！”（<strong>Commit 消息</strong>）</li>\n<li><strong>RM</strong> 接收到 <strong>Commit 消息</strong> 后执行 <strong>提交本地数据库事务</strong> 操作，执行完成之后 <strong>释放整个事务期间所占用的资源</strong>。</li>\n<li><strong>RM</strong> 回复：“事务已经提交” （<strong>ACK 消息</strong>）。</li>\n<li><strong>TM</strong> 收到所有 <strong>事务参与者</strong> 的 <strong>ACK 消息</strong> 之后，整个分布式事务过程正式结束。</li>\n</ol>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-transaction/distributed-transaction-2pc-ready.png\" alt=\"2PC示意图-就绪\"></p>\n<p>当任一事务参与者是“未就绪”状态的话：</p>\n<ol>\n<li><strong>TM</strong> 向所有参与者发送消息：“你们可以执行回滚操作了！”（<strong>Rollback 消息</strong>）。</li>\n<li><strong>RM</strong> 接收到 <strong>Rollback 消息</strong> 后执行 <strong>本地数据库事务回滚</strong> 执行完成之后 <strong>释放整个事务期间所占用的资源</strong>。</li>\n<li><strong>RM</strong> 回复：“事务已经回滚” （<strong>ACK 消息</strong>）。</li>\n<li><strong>TM</strong> 收到所有 <strong>RM</strong> 的 <strong>ACK 消息</strong> 之后，中断事务。</li>\n</ol>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-transaction/distributed-transaction-2pc-not-ready.png\" alt=\"2PC示意图-未就绪\"></p>\n<h4>总结</h4>\n<p>简单总结一下 <strong>2PC</strong> 两阶段中比较重要的一些点：</p>\n<ol>\n<li><strong>准备阶段</strong> 的主要目的是测试 <strong>RM</strong> 能否执行 <strong>本地数据库事务</strong> 操作（!!!注意：这一步并不会提交事务）。</li>\n<li><strong>提交阶段</strong> 中 <strong>TM</strong> 会根据 <strong>准备阶段</strong> 中 <strong>RM</strong> 的消息来决定是执行事务提交还是回滚操作。</li>\n<li><strong>提交阶段</strong> 之后一定会结束当前的分布式事务</li>\n</ol>\n<p><strong>2PC 的优点：</strong></p>\n<ul>\n<li>理论模型简单，便于理解和实现。</li>\n<li>主流数据库（如 MySQL InnoDB、Oracle、PostgreSQL）通常都支持 XA，可以作为 2PC 中的 RM，由外部 TM 协调提交或回滚。</li>\n</ul>\n<p><strong>2PC 的权衡：</strong></p>\n<ul>\n<li>2PC 的设计目标是数据强一致性。但在工程实现中，由于网络分区、TM 宕机、RM 超时等极端情况，仍可能出现数据不一致或事务长时间阻塞，并不等于“天然绝对强一致”。</li>\n</ul>\n<p><strong>2PC 存在的问题：</strong></p>\n<ul>\n<li><strong>同步阻塞</strong>：事务参与者会在正式提交事务之前会一直占用相关的资源。比如用户小明转账给小红，那其他事务也要操作用户小明或小红的话，就会阻塞。</li>\n<li><strong>数据不一致</strong>：由于网络问题或者 TM 宕机都有可能会造成数据不一致的情况。比如在第 2 阶段（提交阶段），部分网络出现问题导致部分参与者收不到 Commit/Rollback 消息的话，就会导致数据不一致。</li>\n<li><strong>单点问题</strong>：TM 在其中也是一个很重要的角色，如果 TM 在准备（Prepare）阶段完成之后挂掉的话，事务参与者就会一直卡在提交（Commit）阶段。</li>\n</ul>\n<h3>XA 模式</h3>\n<p>XA 可以理解为 2PC 在数据库等资源层面的标准化落地。2PC 是提交协议，XA 是 X/Open 定义的 DTP 接口规范，规定了 TM 如何协调多个 RM 参与同一个全局事务。</p>\n<p>典型流程是：</p>\n<ol>\n<li>AP 发起全局事务，TM 负责生成全局事务上下文。</li>\n<li>各个 RM（例如不同数据库）执行本地事务，但先不真正提交。</li>\n<li>TM 调用各个 RM 的 <code>prepare</code>，所有 RM 都准备成功后再调用 <code>commit</code>，否则调用 <code>rollback</code>。</li>\n</ol>\n<p>XA 的优势是业务侵入低，数据库本身负责事务隔离和回滚能力，适合短事务、强一致、并发压力不太高的场景。缺点也很明显：事务期间数据库资源会被长时间占用，性能和可用性都容易受到影响。</p>\n<p>Seata 从 1.2 版本开始支持 XA 模式。Seata XA 模式利用数据库、消息服务等资源对 XA 协议的支持来管理分支事务，整体一致性更强，但吞吐通常不如 AT / TCC / Saga 这类柔性方案。</p>\n<h3>3PC（三阶段提交协议）</h3>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-transaction/3pc-work-flow.png\" alt></p>\n<p>3PC 是在 2PC 基础上的优化版本。它将 2PC 的 <strong>Prepare 阶段</strong>拆成两个独立阶段：CanCommit（只询问能否提交，不执行事务预操作）和 PreCommit（执行事务预操作、写 redo/undo log）。再加上最后的 DoCommit，共三个阶段：</p>\n<ol>\n<li>CanCommit（询问能否提交）</li>\n<li>PreCommit（执行事务预操作）</li>\n<li>DoCommit（真正提交）</li>\n</ol>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-transaction/distributed-transaction-3pc-ready.png\" alt=\"3PC示意图-就绪\"></p>\n<h4>准备阶段(CanCommit)</h4>\n<p>准备阶段 RM 不会执行事务操作，TM 只是向 RM 发送 <strong>准备请求</strong> ，顺便询问一些信息比如事务参与者能否执行本地数据库事务操作。RM 回复“Yes”、“No”或者直接超时未回复。</p>\n<h4>预提交阶段(PreCommit)</h4>\n<p>如果准备阶段所有的 RM 回复 “Yes”的话，TM 就会向所有的 RM 发送 <strong>PreCommit 消息（预提交请求）</strong> ，RM 收到消息之后会执行本地数据库事务预操作比如写 redo log/undo log 日志。</p>\n<p>如果准备阶段有任一 RM 回复“NO” 或者直接超时未回复的话，TM 就会给所有 RM 发送 <strong>Abort 消息（中断请求）</strong> ，RM 收到消息后直接中断事务。这样其实对 RM 来说损失并不大，因为本质上 RM 到现在还并没有实际做什么事情。</p>\n<p>如果 RM 成功的执行了事务预操作，就返回 “YES”。否则，返回“No”（最后的反悔机会）。</p>\n<p>预提交阶段 TM 与 RM 都引入了超时机制，如果 <strong>参与者</strong> 没有收到 TM 的 PreCommit 消息，或者 TM 没有收到参与者返回的预执行结果状态，那么在超过等待时间后，事务就会中断，这就避免了事务的阻塞。</p>\n<h4>执行事务提交阶段（DoCommit）</h4>\n<p><strong>执行事务提交（DoCommit）</strong> 阶段就开始进行真正的事务提交。</p>\n<p>如果预提交阶段所有的 RM 回复 “YES”的话，TM 就会向所有的 RM 发送 <strong>DoCommit 消息（执行事务提交请求）</strong> ，RM 收到消息之后会执行本地数据库事务提交，并在完成后释放占用的资源。当事务提交成功后，RM 会返回 “YES”。</p>\n<p>如果预提交阶段有任一 RM 回复“NO”或者直接超时未回复的话，TM 就会给所有 RM 发送 <strong>Abort 消息（中断请求）</strong> ，RM 收到消息后会进行事务回滚，释放资源，中断本次事务。</p>\n<p>如果 RM 在设定时间内没有收到 TM 的 DoCommit 消息，RM 会认为 TM 可能发生了故障，会直接进行事务提交。</p>\n<p>只要预提交阶段所有 RM 都返回了 <code>Yes</code>，那么进入第三阶段后，事务大概率可以执行成功。</p>\n<p>但这里要特别注意：<strong>RM 超时后默认提交是 3PC 的“双刃剑”</strong>。它缓解了 2PC 中 TM 宕机导致 RM 永久阻塞的问题，但也引入了新的不一致风险。比如 TM 原本决定 Abort，但只有部分 RM 收到 Abort 消息，其他 RM 因超时默认提交，就会造成同一个事务在不同 RM 上出现“部分提交、部分回滚”的状态。这也是 3PC 在工程上很少真正落地的重要原因。</p>\n<h4>总结</h4>\n<p><strong>3PC 除了将 2PC 中的 Prepare 阶段做了进一步拆分之外，还做了哪些改进？</strong></p>\n<p>3PC 同时在 TM 和 RM 中引入了 <strong>超时机制</strong>。如果 TM 在一定时间内没有收到 RM 的消息，就默认失败并中断事务；如果 RM 长时间收不到 TM 的下一步指令，也会根据所处阶段选择中断或提交，尽量避免资源长期阻塞。</p>\n<p>不过，3PC 并没有完美解决 2PC 的阻塞问题，还引入了性能更差、仍可能数据不一致等新问题。因此，3PC 的实际应用并不广泛。工程上更主流的方向是使用 Paxos / Raft 等共识协议（通常结合复制状态机模式）来解决协调者单点和状态一致性问题，而不是直接使用 3PC。</p>\n<h3>TCC（补偿事务）</h3>\n<p>TCC 属于目前比较常见的一种柔性事务解决方案。数据库专家帕特 · 赫兰德（Pat Helland）于 2007 年发表的 <a href=\"https://www.ics.uci.edu/~cs223/papers/cidr07p15.pdf\" target=\"_blank\" rel=\"noopener noreferrer\">《Life beyond Distributed Transactions: an Apostate’s Opinion》</a> 讨论了避免传统分布式事务、改用业务补偿的思路，感兴趣的小伙伴可以阅读一下这篇论文。</p>\n<p>简单来说，TCC 是 Try、Confirm、Cancel 三个词的缩写，它分为三个阶段：</p>\n<ol>\n<li><strong>Try（尝试）阶段</strong>：尝试执行。完成业务检查，并预留好必需的业务资源。</li>\n<li><strong>Confirm（确认）阶段</strong>：确认执行。当所有事务参与者的 Try 阶段执行成功就会执行 Confirm，Confirm 阶段会处理 Try 阶段预留的业务资源。否则，就会执行 Cancel。</li>\n<li><strong>Cancel（取消）阶段</strong>：取消执行，释放 Try 阶段预留的业务资源。</li>\n</ol>\n<p>每个阶段由业务代码控制，这样可以避免数据库层面的长事务和长期持锁，但代价是业务代码侵入更强，开发者需要自己处理资源预留、确认、取消、重试和异常补偿。</p>\n<p>我们拿转账场景来说：</p>\n<ol>\n<li><strong>Try（尝试）阶段</strong>：在转账场景下，Try 要做的事情就是检查账户余额是否充足，预留的资源就是转账资金。</li>\n<li><strong>Confirm（确认）阶段</strong>：如果 Try 阶段执行成功的话，Confirm 阶段就会执行真正的扣钱操作。</li>\n<li><strong>Cancel（取消）阶段</strong>：释放 Try 阶段预留的转账资金。</li>\n</ol>\n<p>一般情况下，当我们使用 <code>TCC</code> 模式的时候，需要自己实现 <code>try</code>、<code>confirm</code>、<code>cancel</code> 这三个方法，来达到最终一致性。</p>\n<p>正常情况下，会执行 <code>try</code>、<code>confirm</code> 方法。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-transaction/distributed-transaction-tcc-confirm.png\" alt></p>\n<p>出现异常的话，会执行 <code>try</code>、<code>cancel</code> 方法。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-transaction/distributed-transaction-tcc-cancel.png\" alt></p>\n<p>Try 阶段出现问题的话，可以执行 Cancel。<strong>那如果 Confirm 或者 Cancel 阶段失败了怎么办呢？</strong></p>\n<p>TCC 会记录事务日志并持久化事务日志到某种存储介质上比如本地文件、关系型数据库、ZooKeeper，事务日志包含了事务的执行状态，通过事务执行状态可以判断出事务是提交成功了还是提交失败了，以及具体失败在哪一步。如果发现是 Confirm 或者 Cancel 阶段失败的话，会进行重试，继续尝试执行 Confirm 或者 Cancel 阶段的逻辑。重试次数由具体框架决定，超过最大重试次数仍未成功的，通常需要告警并进入人工介入流程。</p>\n<p>如果代码没有特殊 Bug 的话，Confirm 或者 Cancel 阶段出现问题的概率是比较小的。</p>\n<p>TCC 落地时有三个非常经典的工程问题：</p>\n<ol>\n<li><strong>幂等性</strong>：Confirm 和 Cancel 可能因为网络超时、TC 重试等原因被重复调用，必须保证多次执行结果一致。常见做法是在数据库中维护事务状态表，每次执行前先检查状态。</li>\n<li><strong>空回滚</strong>：Try 请求因为网络问题没有真正到达 RM，但 TM 已经发起 Cancel。此时 Cancel 面对的是“从未 Try 过的事务”，需要识别并直接返回成功，避免误执行回滚逻辑。</li>\n<li><strong>悬挂</strong>：Cancel 比 Try 先到达 RM，随后 Try 才姗姗来迟。如果 Try 继续预留资源，这份资源就可能永远没人 Confirm/Cancel。解决思路是在 Try 中先检查该事务是否已经被 Cancel 过，若已 Cancel 则拒绝执行 Try。</li>\n</ol>\n<p><strong>事务日志会被删除吗？</strong> 会的。如果事务提交成功（没有抛出任何异常），就可以删除对应的事务日志，节省资源。</p>\n<p><strong>TCC 模式不需要依赖于底层数据资源的事务支持，但是需要我们手动实现更多的代码</strong>，属于 <strong>侵入业务代码</strong> 的一种分布式解决方案。</p>\n<p>TCC 事务模型的思想类似 2PC，我简单花了一张图对比一下二者。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-transaction/2pc-vs-tcc.png\" alt=\"2PC 对比 TCC\"></p>\n<p><strong>TCC 和 2PC/3PC 有什么区别呢？</strong></p>\n<ul>\n<li>2PC/3PC 依靠数据库或者存储资源层面的事务，TCC 主要通过修改业务代码来实现。</li>\n<li>2PC/3PC 属于业务代码无侵入的，TCC 对业务代码有侵入。</li>\n<li>2PC/3PC 追求的是强一致性，在两阶段提交的整个过程中，一直会持有数据库的锁。TCC 追求的是最终一致性，不会一直持有各个业务资源的锁。</li>\n</ul>\n<p>针对 TCC 的实现，业界也有一些不错的开源框架。不同的框架对于 TCC 的实现可能略有不同，不过大致思想都一样。</p>\n<ol>\n<li><strong><a href=\"https://github.com/liuyangming/ByteTCC\" target=\"_blank\" rel=\"noopener noreferrer\">ByteTCC</a></strong>：ByteTCC 是基于 Try-Confirm-Cancel（TCC）机制的分布式事务管理器的实现。相关阅读：<a href=\"https://www.bytesoft.org/how-to-impl-tcc/\" target=\"_blank\" rel=\"noopener noreferrer\">关于如何实现一个 TCC 分布式事务框架的一点思考</a></li>\n<li><strong><a href=\"https://seata.apache.org/zh-cn/\" target=\"_blank\" rel=\"noopener noreferrer\">Seata</a></strong>：Seata 是一款开源的分布式事务解决方案，同时支持 AT、TCC、Saga、XA 四种模式。这里说的是它的 TCC 模式。</li>\n<li><strong><a href=\"https://gitee.com/dromara/hmily\" target=\"_blank\" rel=\"noopener noreferrer\">Hmily</a></strong>：金融级分布式事务解决方案。新项目选型时建议同时评估社区活跃度和 Seata 等替代方案。</li>\n</ol>\n<h3>AT 模式（自动补偿）</h3>\n<p>AT（Automatic Transaction）模式是 Seata 的核心模式之一，目标是在尽量少改业务代码的前提下提供最终一致能力。它可以理解为“自动补偿”方案：业务仍然按本地事务提交，但框架会记录回滚所需的信息。</p>\n<p>AT 模式大致分为两个阶段：</p>\n<ol>\n<li><strong>一阶段</strong>：业务 SQL 正常执行并提交本地事务，同时 Seata 代理数据源会解析 SQL，记录数据的 before image / after image 到 <code>undo_log</code> 表，并向 TC 注册分支事务。</li>\n<li><strong>二阶段提交</strong>：如果全局事务提交，TC 通知 RM 异步删除 <code>undo_log</code>。</li>\n<li><strong>二阶段回滚</strong>：如果全局事务回滚，RM 根据 <code>undo_log</code> 生成反向补偿 SQL，将数据恢复到 before image。</li>\n</ol>\n<p>AT 模式的优点是业务侵入低，适合大量基于关系型数据库的常规 CRUD 场景；缺点是对 SQL 类型、表主键、全局锁、隔离级别等有要求，不适合所有复杂 SQL 和跨非关系型资源场景。</p>\n<h3>TCC vs Saga</h3>\n<p>| 维度         | TCC                                  | Saga                                   |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/mysql/%E4%BA%8B%E5%8A%A1%E7%A4%BA%E6%84%8F%E5%9B%BE.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "分布式"
      ]
    },
    {
      "title": "Spring Cloud Gateway 面试题总结：路由、Predicate、Filter、限流熔断与工作原理",
      "url": "https://codingmap.strova.top/distributed-system/spring-cloud-gateway-questions.html",
      "id": "https://codingmap.strova.top/distributed-system/spring-cloud-gateway-questions.html",
      "summary": "Spring Cloud Gateway 高频面试题总结，覆盖核心概念、路由匹配、Predicate、GatewayFilter、GlobalFilter、限流熔断、负载均衡、跨域处理和常见生产问题。重点围绕 什么是 Spring Cloud Gateway？、Spring Cloud Gateway 的工作流程？",
      "content_html": "<blockquote>\n<p>本文重构完善自<a href=\"https://mp.weixin.qq.com/s/XjFYsP1IUqNzWqXZdJn-Aw\" target=\"_blank\" rel=\"noopener noreferrer\">6000 字 | 16 图 | 深入理解 Spring Cloud Gateway 的原理 - 悟空聊架构</a>这篇文章。</p>\n</blockquote>\n<h2>什么是 Spring Cloud Gateway？</h2>\n<p>Spring Cloud Gateway 属于 Spring Cloud 生态系统中的网关，其诞生的目标主要是为了替代 <strong>Zuul 1.x</strong>。Zuul 1.x 基于 Servlet 阻塞 I/O 架构，在高并发场景下性能有限。而 Zuul 2.x 虽然采用了 Netty 非阻塞架构，但 Spring Cloud 官方并未正式集成 Zuul 2.x。Spring Cloud Gateway 起步要比 Zuul 2.x 更早。</p>\n<p>为了提升网关的性能，Spring Cloud Gateway 基于 Spring WebFlux 。Spring WebFlux 使用 Reactor 库来实现响应式编程模型，底层基于 Netty 实现同步非阻塞的 I/O。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/springcloud-gateway- demo.png\" alt></p>\n<p>Spring Cloud Gateway 不仅提供统一的路由方式，并且基于 Filter 链的方式提供了网关基本的功能，例如：安全，监控/指标，限流。</p>\n<p>Spring Cloud Gateway 和 Zuul 2.x 的差别不大，也是通过过滤器来处理请求。不过，目前更加推荐使用 Spring Cloud Gateway 而非 Zuul，Spring Cloud 生态对其支持更加友好。</p>\n<ul>\n<li>GitHub 地址： <a href=\"https://github.com/spring-cloud/spring-cloud-gateway\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/spring-cloud/spring-cloud-gateway</a></li>\n<li>官网： <a href=\"https://spring.io/projects/spring-cloud-gateway\" target=\"_blank\" rel=\"noopener noreferrer\">https://spring.io/projects/spring-cloud-gateway</a></li>\n</ul>\n<h2>Spring Cloud Gateway 的工作流程？</h2>\n<p>Spring Cloud Gateway 的工作流程如下图所示：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-workflow.png\" alt=\"Spring Cloud Gateway 的工作流程\"></p>\n<p>这是 Spring 官方博客中的一张图，原文地址：<a href=\"https://spring.io/blog/2022/08/26/creating-a-custom-spring-cloud-gateway-filter\" target=\"_blank\" rel=\"noopener noreferrer\">https://spring.io/blog/2022/08/26/creating-a-custom-spring-cloud-gateway-filter</a>。</p>\n<p>具体的流程分析：</p>\n<ol>\n<li><strong>路由判断</strong>：客户端的请求到达网关后，先经过 Gateway Handler Mapping 处理，这里面会做断言（Predicate）判断，看下符合哪个路由规则，这个路由映射后端的某个服务。</li>\n<li><strong>请求过滤</strong>：然后请求到达 Gateway Web Handler，这里面有很多过滤器，组成过滤器链（Filter Chain），这些过滤器可以对请求进行拦截和修改，比如添加请求头、参数校验等等，有点像净化污水。然后将请求转发到实际的后端服务。这些过滤器逻辑上可以称作 Pre-Filters，Pre 可以理解为“在...之前”。</li>\n<li><strong>服务处理</strong>：后端服务会对请求进行处理。</li>\n<li><strong>响应过滤</strong>：后端处理完结果后，返回给 Gateway 的过滤器再次做处理，逻辑上可以称作 Post-Filters，Post 可以理解为“在...之后”。</li>\n<li><strong>响应返回</strong>：响应经过过滤处理后，返回给客户端。</li>\n</ol>\n<p>总结：客户端的请求先通过匹配规则找到合适的路由，就能映射到具体的服务。然后请求经过过滤器处理后转发给具体的服务，服务处理后，再次经过过滤器处理，最后返回给客户端。</p>\n<h2>Spring Cloud Gateway 的断言是什么？</h2>\n<p>断言（Predicate）这个词听起来极其深奥，它是一种编程术语，我们生活中根本就不会用它。说白了它就是对一个表达式进行 if 判断，结果为真或假，如果为真则做这件事，否则做那件事。</p>\n<p>在 Gateway 中，如果客户端发送的请求满足了断言的条件，则映射到指定的路由器，就能转发到指定的服务上进行处理。</p>\n<p>断言配置的示例如下，配置了两个路由规则，有一个 predicates 断言配置，当请求 url 中包含 <code>api/thirdparty</code>，就匹配到了第一个路由 <code>route_thirdparty</code>。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-predicate-example.png\" alt=\"断言配置示例\"></p>\n<p>常见的路由断言规则如下图所示：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-predicate-rules.png\" alt=\"Spring Cloud GateWay 路由断言规则\"></p>\n<h2>Spring Cloud Gateway 的路由和断言是什么关系？</h2>\n<p>Route 路由和 Predicate 断言的对应关系如下：：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-predicate-route.png\" alt=\"路由和断言的对应关系\"></p>\n<ul>\n<li><strong>一对多</strong>：一个路由规则可以包含多个断言。如上图中路由 Route1 配置了三个断言 Predicate。</li>\n<li><strong>同时满足</strong>：如果一个路由规则中有多个断言，则需要同时满足才能匹配。如上图中路由 Route2 配置了两个断言，客户端发送的请求必须同时满足这两个断言，才能匹配路由 Route2。</li>\n<li><strong>第一个匹配成功</strong>：如果一个请求可以匹配多个路由，则映射第一个匹配成功的路由。如上图所示，客户端发送的请求满足 Route3 和 Route4 的断言，但是 Route3 的配置在配置文件中靠前，所以只会匹配 Route3。</li>\n</ul>\n<h2>Spring Cloud Gateway 如何实现动态路由？</h2>\n<p>在使用 Spring Cloud Gateway 的时候，官方文档提供的方案总是基于配置文件或代码配置的方式。</p>\n<p>Spring Cloud Gateway 作为微服务的入口，需要尽量避免重启，而现在配置更改需要重启服务不能满足实际生产过程中的动态刷新、实时变更的业务需求，所以我们需要在 Spring Cloud Gateway 运行时动态配置网关。</p>\n<p>实现动态路由的方式有很多种，其中一种推荐的方式是基于 Nacos 注册中心来做。 Spring Cloud Gateway 可以从注册中心获取服务的元数据（例如服务名称、路径等），然后根据这些信息自动生成路由规则。这样，当你添加、移除或更新服务实例时，网关会自动感知并相应地调整路由规则，无需手动维护路由配置。</p>\n<p>其实这些复杂的步骤并不需要我们手动实现，通过 Nacos Server 和 Spring Cloud Alibaba Nacos Config 即可实现配置的动态变更，官方文档地址：<a href=\"https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-config\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-config</a> 。</p>\n<h2>Spring Cloud Gateway 的过滤器有哪些？</h2>\n<p>过滤器 Filter 按照请求和响应可以分为两种：</p>\n<ul>\n<li><strong>Pre 类型</strong>：在请求被转发到微服务之前，对请求进行拦截和修改，例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。</li>\n<li><strong>Post 类型</strong>：微服务处理完请求后，返回响应给网关，网关可以再次进行处理，例如修改响应内容或响应头、日志输出、流量监控等。</li>\n</ul>\n<p>另外一种分类是按照过滤器 Filter 作用的范围进行划分：</p>\n<ul>\n<li><strong>GatewayFilter</strong>：局部过滤器，应用在单个路由或一组路由上的过滤器。标红色表示比较常用的过滤器。</li>\n<li><strong>GlobalFilter</strong>：全局过滤器，应用在所有路由上的过滤器。</li>\n</ul>\n<h3>局部过滤器</h3>\n<p>常见的局部过滤器如下图所示：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-gatewayfilters.png\" alt></p>\n<p>具体怎么用呢？这里有个示例，如果 URL 匹配成功，则去掉 URL 中的 “api”。</p>\n<div class=\"language-yaml line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"yaml\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-yaml\"><span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">filters</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">#过滤器</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  - </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">RewritePath=/api/(?&#x3C;segment>.*),/$\\{segment}</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> # 将跳转路径中包含的 “api” 替换成空</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>当然我们也可以自定义过滤器，本篇不做展开。</p>\n<h3>全局过滤器</h3>\n<p>常见的全局过滤器如下图所示：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-globalfilters.png\" alt></p>\n<p>全局过滤器最常见的用法是进行负载均衡。配置如下所示：</p>\n<div class=\"language-yaml line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"yaml\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-yaml\"><span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">spring</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">:</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  cloud</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">:</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">    gateway</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">:</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">      routes</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">:</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        - </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">id</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">route_member</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> # 第三方微服务路由规则</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">          uri</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">lb://passjava-member</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> # 负载均衡，将请求转发到注册中心注册的 passjava-member 服务</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">          predicates</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"># 断言</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">            - </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">Path=/api/member/**</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> # 如果前端请求路径包含 api/member，则应用这条路由规则</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">          filters</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">#过滤器</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">            - </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">RewritePath=/api/(?&#x3C;segment>.*),/$\\{segment}</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> # 将跳转路径中包含的api替换成空</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>这里有个关键字 <code>lb</code>，用到了全局过滤器 <code>LoadBalancerClientFilter</code>，当匹配到这个路由后，会将请求转发到 passjava-member 服务，且支持负载均衡转发，也就是先将 passjava-member 解析成实际的微服务的 host 和 port，然后再转发给实际的微服务。</p>\n<h2>Spring Cloud Gateway 支持限流吗？</h2>\n<p>Spring Cloud Gateway 自带了限流过滤器，对应的接口是 <code>RateLimiter</code>，<code>RateLimiter</code> 接口只有一个实现类 <code>RedisRateLimiter</code> （基于 Redis + Lua 实现的限流），提供的限流功能比较简易且不易使用。</p>\n<p>从 Sentinel 1.6.0 版本开始，Sentinel 引入了 Spring Cloud Gateway 的适配模块，可以提供两种资源维度的限流：route 维度和自定义 API 维度。也就是说，Spring Cloud Gateway 可以结合 Sentinel 实现更强大的网关流量控制。</p>\n<h2>Spring Cloud Gateway 如何自定义全局异常处理？</h2>\n<p>在 SpringBoot 项目中，我们捕获全局异常只需要在项目中配置 <code>@RestControllerAdvice</code>和 <code>@ExceptionHandler</code>就可以了。不过，这种方式在 Spring Cloud Gateway 下不适用。</p>\n<p>Spring Cloud Gateway 提供了多种全局处理的方式，比较常用的一种是实现<code>ErrorWebExceptionHandler</code>并重写其中的<code>handle</code>方法。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Order</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Component</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">RequiredArgsConstructor</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> GlobalErrorWebExceptionHandler</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> implements</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ErrorWebExceptionHandler</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> final</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ObjectMapper</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> objectMapper</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Override</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Mono</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Void</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> handle</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">ServerWebExchange</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> exchange</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Throwable</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> ex</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // ...</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h2>参考</h2>\n<ul>\n<li>Spring Cloud Gateway 官方文档：<a href=\"https://cloud.spring.io/spring-cloud-gateway/reference/html/\" target=\"_blank\" rel=\"noopener noreferrer\">https://cloud.spring.io/spring-cloud-gateway/reference/html/</a></li>\n<li>Creating a custom Spring Cloud Gateway Filter：<a href=\"https://spring.io/blog/2022/08/26/creating-a-custom-spring-cloud-gateway-filter\" target=\"_blank\" rel=\"noopener noreferrer\">https://spring.io/blog/2022/08/26/creating-a-custom-spring-cloud-gateway-filter</a></li>\n<li>全局异常处理: <a href=\"https://zhuanlan.zhihu.com/p/347028665\" target=\"_blank\" rel=\"noopener noreferrer\">https://zhuanlan.zhihu.com/p/347028665</a></li>\n</ul>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/springcloud-gateway-%20demo.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "分布式"
      ]
    },
    {
      "title": "高可用系统知识体系：SLA、限流、熔断、降级、重试、幂等、冗余与压测",
      "url": "https://codingmap.strova.top/high-availability/",
      "id": "https://codingmap.strova.top/high-availability/",
      "summary": "高可用系统面试与架构学习路线，涵盖 SLA、可用性指标、单点故障、冗余容灾、限流、降级、熔断、超时重试、接口幂等、压测和故障演练。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章、高可用 等内容展开。结合 CodingMap 知识体系梳理 高可用 的核心概念、实践方法、常见问题和高频面试考点，覆盖原理分析等核心内。",
      "content_html": "<p>这份 <strong>高可用系统知识体系</strong> 面向后端学习、系统设计和面试复习，围绕“减少故障、隔离故障、快速恢复、降低重复请求和异常流量影响”整理本站高可用相关文章。</p>\n<p>如果你时间有限，建议先看 <a href=\"/high-availability/high-availability-interview-questions.html\" target=\"_blank\">高可用系统设计面试题总结</a>，快速建立高频问题清单；如果你想系统补基础，可以按下面的阅读顺序推进。</p>\n<h2>适合谁看</h2>\n<ul>\n<li>正在系统学习高可用系统设计的后端开发者。</li>\n<li>准备校招、社招、中大厂后端面试的同学。</li>\n<li>想补齐限流、降级、熔断、重试、幂等、容灾和压测能力的工程师。</li>\n<li>已经遇到接口超时、重复请求、下游故障、流量突增、系统雪崩等问题，但缺少系统解法的读者。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>高可用不是“不出故障”，而是故障发生时尽量可控、可隔离、可恢复。</li>\n<li>SLA、可用性指标、RTO、RPO 是衡量高可用设计的重要语言。</li>\n<li>限流、降级、熔断、超时重试分别解决不同层面的稳定性问题。</li>\n<li>幂等设计是应对重复请求、自动重试、消息重复消费和支付回调的基础能力。</li>\n<li>冗余容灾、性能压测、故障演练和灰度发布决定系统是否能扛住真实生产风险。</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/high-availability/high-availability-interview-questions.html\" target=\"_blank\">高可用系统设计面试题总结</a>：先建立 SLA、限流、熔断、重试、幂等、容灾和压测的高频问题清单。</li>\n<li><a href=\"/high-availability/high-availability-system-design.html\" target=\"_blank\">高可用系统设计详解</a>：系统理解高可用架构的整体设计方法。</li>\n<li><a href=\"/high-availability/limit-request.html\" target=\"_blank\">服务限流详解</a>、<a href=\"/high-availability/fallback-and-circuit-breaker.html\" target=\"_blank\">服务降级与熔断详解</a>、<a href=\"/high-availability/timeout-and-retry.html\" target=\"_blank\">超时和重试机制详解</a>：掌握稳定性治理三件套。</li>\n<li><a href=\"/high-availability/idempotency.html\" target=\"_blank\">接口幂等性设计详解</a>：理解重复请求、业务幂等和支付回调等场景。</li>\n<li><a href=\"/high-availability/redundancy.html\" target=\"_blank\">冗余设计详解</a> 和 <a href=\"/high-availability/performance-test.html\" target=\"_blank\">性能测试入门</a>：补齐容灾、压测、容量评估和故障验证。</li>\n</ol>\n<h2>核心文章</h2>\n<h3>总览与面试路线</h3>\n<ul>\n<li><a href=\"/high-availability/high-availability-interview-questions.html\" target=\"_blank\">高可用系统设计面试题总结</a>：串联 SLA、单点故障、限流降级熔断、超时重试、接口幂等、冗余容灾和性能压测。</li>\n<li><a href=\"/high-availability/high-availability-system-design.html\" target=\"_blank\">高可用系统设计详解</a>：讲解 SLA、多少个 9、单点故障治理、缓存高可用、异步削峰、灰度发布和故障恢复。</li>\n<li><a href=\"/high-availability/high-availability-system-interview-questions.html\" target=\"_blank\">2026 最新高可用系统设计面试题总结</a>：覆盖高可用系统设计的高频问答和复习重点。</li>\n</ul>\n<h3>稳定性治理</h3>\n<ul>\n<li><a href=\"/high-availability/limit-request.html\" target=\"_blank\">服务限流详解</a>：理解固定窗口、滑动窗口、令牌桶、漏桶、Sentinel、Redis Lua 和 Redisson 限流。</li>\n<li><a href=\"/high-availability/fallback-and-circuit-breaker.html\" target=\"_blank\">服务降级与熔断详解</a>：理解 Fallback、降级开关、熔断器状态机、雪崩效应、隔离策略和框架选型。</li>\n<li><a href=\"/high-availability/timeout-and-retry.html\" target=\"_blank\">超时和重试机制详解</a>：理解连接超时、读取超时、指数退避、随机抖动、重试风暴和幂等性。</li>\n</ul>\n<h3>幂等、容灾与验证</h3>\n<ul>\n<li><a href=\"/high-availability/idempotency.html\" target=\"_blank\">接口幂等性设计详解</a>：讲解幂等键、Token、唯一索引、去重表、乐观锁、悲观锁、分布式锁和支付回调。</li>\n<li><a href=\"/high-availability/redundancy.html\" target=\"_blank\">冗余设计详解</a>：讲解硬件冗余、服务冗余、数据冗余、地域冗余、同城灾备、异地灾备和多活架构。</li>\n<li><a href=\"/high-availability/performance-test.html\" target=\"_blank\">性能测试入门</a>：理解 QPS、TPS、RT、P90/P99/P999、容量评估、压力测试、稳定性测试和压测工具。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>高可用系统如何定义可用性？几个 9 分别意味着什么？</li>\n<li>RTO 和 RPO 有什么区别？</li>\n<li>限流、降级、熔断分别解决什么问题？</li>\n<li>令牌桶和漏桶有什么区别？分布式限流如何实现？</li>\n<li>超时和重试为什么可能导致雪崩？</li>\n<li>接口幂等有哪些实现方案？支付回调如何保证幂等？</li>\n<li>冗余、容灾、同城多活、异地多活分别适合什么场景？</li>\n<li>性能测试、压力测试、稳定性测试有什么区别？</li>\n<li>如何通过故障演练验证高可用设计是否有效？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/high-performance/\" target=\"_blank\">高性能系统知识体系</a></li>\n<li><a href=\"/distributed-system/\" target=\"_blank\">分布式系统知识体系</a></li>\n<li><a href=\"/system-design/\" target=\"_blank\">系统设计</a></li>\n<li><a href=\"/high-performance/message-queue/\" target=\"_blank\">消息队列专题</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高可用"
      ]
    },
    {
      "title": "服务降级与熔断详解：Fallback、熔断器状态机与 Sentinel / Hystrix / Resilience4j 选型",
      "url": "https://codingmap.strova.top/high-availability/fallback-and-circuit-breaker.html",
      "id": "https://codingmap.strova.top/high-availability/fallback-and-circuit-breaker.html",
      "summary": "服务降级与熔断机制详解，讲解 Fallback 兜底、降级开关、熔断器 Closed/Open/Half-Open 状态机、雪崩效应、隔离策略，以及 Sentinel、Hystrix、Resilience4j 的选型对比。重点围绕 什么是降级？、大规模分布式系统如何降级？、什么是 Fallback？、什么是熔断？",
      "content_html": "<p>在微服务架构里，一个请求往往要经过好几个服务的调用链。如果链路上某个服务出了问题——比如响应变慢、超时甚至直接挂掉——很容易把上游的资源也拖垮，最后演变成整条链路雪崩。服务降级和熔断就是应对这类问题的两道防线：降级负责在系统压力大的时候主动&quot;丢车保帅&quot;，熔断负责在下游持续异常的时候及时&quot;断路&quot;止损。</p>\n<p>这篇文章会把降级和熔断的核心原理讲清楚，包括 Fallback 兜底、降级开关、熔断器状态机、隔离策略，以及 Sentinel、Hystrix、Resilience4j 的选型对比。</p>\n<h2>什么是降级？</h2>\n<p>服务降级，说白了就是：<strong>系统快扛不住的时候，先牺牲不重要的功能，把资源留给核心链路。</strong></p>\n<p>比如电商大促时，推荐模块、广告位、活动氛围图这些非核心功能可以先关掉，但下单、支付、库存扣减这些链路必须尽量保住。用户少看一个推荐还能接受，但下不了单就是真事故。</p>\n<p>所以，降级是一套提前设计好的兜底方案，不是系统挂了以后随便返回个默认值。什么时候触发、关哪些功能、返回什么内容、怎么恢复，都应该提前想清楚。</p>\n<h3>降级一般在什么情况下触发？</h3>\n<p>很多人会把降级理解成“机器负载太高才降级”，其实不止这一种情况。</p>\n<p>常见触发场景有几类：</p>\n<ul>\n<li>系统整体压力太大，比如 CPU 飙高、线程池打满、接口响应时间明显变长；</li>\n<li>某个下游服务异常，比如库存服务、价格服务、推荐服务突然大量超时；</li>\n<li>某个机房、区域或网络链路出问题；</li>\n<li>大促、秒杀、活动上线前，提前按预案关闭部分非核心功能；</li>\n<li>运营临时需要控制某些功能入口。</li>\n</ul>\n<p>不管触发原因是什么，降级的核心目标都一样：<strong>保核心，弃非核心。</strong></p>\n<h3>降级可以降到什么粒度？</h3>\n<p>降级的粒度可以粗，也可以细。</p>\n<p>粗一点，可以直接降级整个服务。比如推荐服务异常时，所有推荐结果都返回空列表。</p>\n<p>细一点，可以只降级页面里的某个区块。比如商品详情页还正常展示，但“猜你喜欢”区域先隐藏。</p>\n<p>再细一点，可以只降级某个接口、某个功能开关，甚至某一类用户、某一个业务场景。</p>\n<p>实际项目里，通常会给功能分优先级。比如：</p>\n<ul>\n<li>推荐、广告、排行榜：优先级较低，出问题可以先关；</li>\n<li>商品详情、购物车：优先级较高，尽量保；</li>\n<li>下单、支付、库存：核心链路，最后才考虑降级。</li>\n</ul>\n<p>这里的 L0-L4、1-10 级都只是团队内部约定，不是行业统一标准。关键不在于怎么命名，而是要提前分清楚：<strong>哪些功能可以牺牲，哪些功能必须保。</strong></p>\n<h3>降级方式有哪些？</h3>\n<h4>1. 延迟处理</h4>\n<p>有些操作没必要同步完成，可以先放到 MQ 里慢慢处理。</p>\n<p>比如评论后发积分、统计用户行为、生成报表，这类操作即使晚几秒完成，用户一般也感知不到。主流程只要先返回成功，后面的事情交给异步任务处理即可。</p>\n<p>但这里有个坑：MQ 也可能积压。如果消费者已经处理不过来了，生产者还一直重试，反而会把系统打得更严重。所以重试要加退避和随机抖动，必要时还要限流。</p>\n<h4>2. 页面片段降级</h4>\n<p>这是最直观的一种方式：页面主体保留，把非核心区块关掉。</p>\n<p>比如商品详情页里，商品标题、价格、库存要保；推荐区、广告位、活动挂件可以先隐藏。</p>\n<p>这种方式简单有效，但前提是你提前做好了开关和兜底逻辑。真出问题时再改代码、再发版，基本就来不及了。</p>\n<h4>3. 接口兜底降级</h4>\n<p>页面里有些接口是异步加载的，比如配送时效、价格预测、推荐列表。它们如果超时，可以直接返回默认值或缓存值。</p>\n<p>比如配送时效接口异常时，可以先展示“预计 2-3 天送达”；推荐接口异常时，可以返回缓存里的热门商品。</p>\n<p>这里要注意一点：兜底数据最好提前准备好。不要等接口已经挂了，才临时去查数据库或重新生成缓存，那样很可能把故障继续放大。</p>\n<h4>4. 页面跳转降级</h4>\n<p>如果完整页面太重，可以把用户导到一个简版页面或静态页面。</p>\n<p>比如大促活动页访问量太大时，可以临时切到静态活动页；系统维护时，可以跳到维护提示页。</p>\n<p>这种方案看起来简单，但一定要提前准备好静态页、路由规则和切换开关。线上出事后再临时做，基本不现实。</p>\n<h4>5. 写降级</h4>\n<p>写降级适合一些“可以晚一点落库”的场景。</p>\n<p>比如积分发放、消息通知、库存流水这类操作，可以先写 Redis、消息表或本地持久化队列，再由 MQ 或定时任务慢慢同步到数据库。</p>\n<p>但写降级的代价也比较明显：你要处理最终一致性问题。比如消息有没有丢、数据有没有重复写、同步失败后怎么补偿、对账怎么做，这些都要提前设计。</p>\n<p>尤其要注意，<strong>纯内存队列不适合保存关键数据</strong>。节点一重启，数据就没了。</p>\n<h4>6. 读降级</h4>\n<p>读降级通常是“只读缓存，不再访问后端服务”。</p>\n<p>比如商品详情、店铺信息、热门榜单这种读多写少的数据，就很适合做缓存兜底。后端服务异常时，先返回缓存里的旧数据，至少页面还能打开。</p>\n<p>如果缓存也没命中，就不要继续硬调下游了。可以直接返回默认值、空结果或降级页。否则用户一次请求可能会把一串后端服务都拖进去。</p>\n<h3>降级开关怎么做？</h3>\n<p>降级必须能快速打开和关闭。不能每次都靠改代码、发版、重启。</p>\n<p>常见方案有这些：</p>\n<p>| <strong>方案</strong>        | <strong>特点</strong>                 | <strong>适合场景</strong>                 |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/high-availability/fallback-and-circuit-breaker-blockHandler-vs-Fallback.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高可用"
      ]
    },
    {
      "title": "高可用系统设计面试题总结：限流、熔断、重试、幂等、容灾与压测",
      "url": "https://codingmap.strova.top/high-availability/high-availability-interview-questions.html",
      "id": "https://codingmap.strova.top/high-availability/high-availability-interview-questions.html",
      "summary": "高可用系统设计面试复习路线，串联 SLA、单点故障、限流降级熔断、超时重试、接口幂等、冗余容灾、性能压测和故障演练等 Java 后端高频考点。重点围绕 高可用面试先建立故障视角、第一阶段：高可用系统设计总览、第二阶段：限流、降级和熔断、第三阶段：超时、重试和幂等、高可用 等内容展开。结合 CodingMap 知识体系。",
      "content_html": "<p>高可用系统面试考的不是“系统永远不出问题”，而是你是否理解：<strong>故障一定会发生，关键是系统能不能限制故障范围、快速恢复，并避免把小故障放大成全站事故</strong>。</p>\n<p>这篇文章把 JavaGuide 现有高可用相关文章串成一条面试复习路线，适合准备后端开发、系统设计和中高级岗位面试。</p>\n<h2>高可用面试先建立故障视角</h2>\n<p>高可用设计可以从 5 个问题开始拆：</p>\n<ol>\n<li><strong>请求太多怎么办？</strong> 限流、排队、削峰。</li>\n<li><strong>下游变慢怎么办？</strong> 超时、重试、熔断、隔离。</li>\n<li><strong>核心服务挂了怎么办？</strong> 降级、冗余、故障转移。</li>\n<li><strong>重复请求怎么办？</strong> 幂等、防重、状态机。</li>\n<li><strong>上线前怎么证明系统扛得住？</strong> 压测、监控、演练、容量评估。</li>\n</ol>\n<p>只要围绕这 5 个问题展开，高可用系统设计题就不会答散。</p>\n<h2>第一阶段：高可用系统设计总览</h2>\n<p>先建立整体框架，再看具体手段。</p>\n<p>重点文章：</p>\n<ul>\n<li><a href=\"/high-availability/high-availability-system-design.html\" target=\"_blank\">高可用系统设计指南</a></li>\n<li><a href=\"/high-availability/redundancy.html\" target=\"_blank\">冗余设计详解</a></li>\n<li><a href=\"/high-availability/performance-test.html\" target=\"_blank\">性能测试详解</a></li>\n</ul>\n<p>高频面试问题：</p>\n<ul>\n<li>什么是高可用？可用性 99.9%、99.99% 分别意味着什么？（99.9% 每月约 43 分钟不可用，99.99% 每月约 4 分钟不可用）</li>\n<li>高可用和高性能有什么区别？</li>\n<li>单点故障如何识别和消除？</li>\n<li>冗余设计为什么不是简单多部署几台机器？</li>\n<li>压测应该测什么？如何避免压测结果和线上表现差距过大？</li>\n</ul>\n<p>这一阶段重点理解“系统会坏”这个前提。高可用设计的核心不是追求零故障，而是让故障可预期、可隔离、可恢复。</p>\n<h2>第二阶段：限流、降级和熔断</h2>\n<p>限流、降级、熔断经常一起被问，但它们解决的问题不一样。</p>\n<p>重点文章：</p>\n<ul>\n<li><a href=\"/high-availability/limit-request.html\" target=\"_blank\">限流算法详解</a></li>\n<li><a href=\"/high-availability/fallback-and-circuit-breaker.html\" target=\"_blank\">服务降级和熔断详解</a></li>\n</ul>\n<p>高频面试问题：</p>\n<ul>\n<li>固定窗口、滑动窗口、漏桶、令牌桶有什么区别？</li>\n<li>为什么固定窗口限流可能出现临界突刺？比如每分钟限 100 次，在窗口末尾和下个窗口开头的 2 秒内可能放行 200 次。</li>\n<li>令牌桶为什么更适合处理突发流量？</li>\n<li>降级和熔断有什么区别？</li>\n<li>熔断器通常有哪些状态？半开状态解决什么问题？</li>\n<li>什么场景适合返回兜底数据？什么场景必须失败提示？</li>\n</ul>\n<p>回答这类问题时，要说明触发条件和恢复机制。比如熔断不是“一失败就立刻切断”，而是需要错误率、慢调用比例、最小请求数、恢复探测等规则配合。不同实现（如 Sentinel、Resilience4j）的默认参数和窗口机制有差异，面试时最好能说出你用过的一种具体配置。</p>\n<h2>第三阶段：超时、重试和幂等</h2>\n<p>超时、重试、幂等是线上事故里非常常见的组合题。</p>\n<p>重点文章：</p>\n<ul>\n<li><a href=\"/high-availability/timeout-and-retry.html\" target=\"_blank\">超时和重试机制详解</a></li>\n<li><a href=\"/high-availability/idempotency.html\" target=\"_blank\">接口幂等性详解</a></li>\n</ul>\n<p>高频面试问题：</p>\n<ul>\n<li>为什么所有远程调用都应该设置超时？</li>\n<li>重试为什么可能放大故障？</li>\n<li>哪些错误可以重试？哪些错误不应该重试？</li>\n<li>指数退避和随机抖动解决什么问题？</li>\n<li>什么是幂等？查询天然幂等吗？</li>\n<li>支付、下单、发券这类接口如何设计幂等？常见方案包括唯一请求 ID + 去重表、数据库唯一约束、状态机控制、乐观锁等。</li>\n</ul>\n<p>这部分最重要的结论是：<strong>重试必须和超时、限流、幂等一起设计</strong>。没有幂等的重试可能造成重复下单、重复扣款；没有退避的重试可能把下游彻底打挂。读操作重试相对安全，写操作重试必须配合幂等，重试次数通常建议控制在 3 次以内。</p>\n<h2>第四阶段：压测、监控和故障演练</h2>\n<p>很多同学准备高可用面试时只背方案，忽略验证视角。真实系统里，如果没有压测和监控，高可用设计就只是纸面设计。</p>\n<p>重点文章：</p>\n<ul>\n<li><a href=\"/high-availability/performance-test.html\" target=\"_blank\">性能测试详解</a></li>\n<li><a href=\"/high-availability/high-availability-system-design.html\" target=\"_blank\">高可用系统设计指南</a></li>\n</ul>\n<p>高频面试问题：</p>\n<ul>\n<li>压测指标应该关注哪些？QPS、RT、错误率和资源使用率各自的含义是什么？</li>\n<li>如何做容量评估？</li>\n<li>为什么要区分单接口压测和全链路压测？</li>\n<li>线上监控应该覆盖哪些指标？</li>\n<li>故障演练和混沌工程解决什么问题？</li>\n</ul>\n<p>面试里可以这样收束答案：上线前通过压测确认容量边界，上线后通过监控发现异常，通过限流降级保护系统，通过预案和演练缩短恢复时间。</p>\n<h2>高可用系统设计题回答模板</h2>\n<p>遇到“如何设计一个高可用系统”这类题，可以按下面的顺序回答：</p>\n<ol>\n<li><strong>明确业务场景和 SLA 目标</strong>：不同业务、不同可用性等级决定了完全不同的复杂度和成本。</li>\n<li><strong>识别核心链路</strong>：哪些功能不能挂，哪些功能可以降级。</li>\n<li><strong>消除单点故障</strong>：服务、数据库、缓存、消息队列都要考虑冗余。</li>\n<li><strong>控制入口流量</strong>：限流、排队、削峰，避免系统被瞬时流量打穿。</li>\n<li><strong>保护下游依赖</strong>：超时、重试、熔断、隔离，防止故障扩散。</li>\n<li><strong>保证请求安全</strong>：幂等、防重、状态机，避免重复执行。</li>\n<li><strong>建立观测和恢复能力</strong>：监控、告警、压测、演练、预案。</li>\n</ol>\n<p>这样答的好处是不会堆概念，而是沿着故障传播路径一步步收敛风险。</p>\n<h2>推荐复习顺序</h2>\n<p>临近面试可以按这个顺序复习：</p>\n<ol>\n<li>先看 <a href=\"/high-availability/high-availability-system-design.html\" target=\"_blank\">高可用系统设计指南</a>，建立整体框架。</li>\n<li>再看 <a href=\"/high-availability/limit-request.html\" target=\"_blank\">限流算法详解</a>、<a href=\"/high-availability/fallback-and-circuit-breaker.html\" target=\"_blank\">服务降级和熔断详解</a>。</li>\n<li>然后看 <a href=\"/high-availability/timeout-and-retry.html\" target=\"_blank\">超时和重试机制详解</a>、<a href=\"/high-availability/idempotency.html\" target=\"_blank\">接口幂等性详解</a>。</li>\n<li>最后补 <a href=\"/high-availability/redundancy.html\" target=\"_blank\">冗余设计详解</a>、<a href=\"/high-availability/performance-test.html\" target=\"_blank\">性能测试详解</a>。</li>\n</ol>\n<p>高可用面试的核心不是把每个概念背得很长，而是能把它们串成一套故障治理体系：<strong>出问题前能预防，出问题时能止损，出问题后能恢复和复盘</strong>。</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高可用"
      ]
    },
    {
      "title": "高可用系统设计详解：SLA、限流熔断、降级容灾、缓存与灰度发布",
      "url": "https://codingmap.strova.top/high-availability/high-availability-system-design.html",
      "id": "https://codingmap.strova.top/high-availability/high-availability-system-design.html",
      "summary": "高可用系统设计指南，讲解 SLA 和多少个 9、单点故障治理、限流熔断、服务降级、缓存高可用、异步削峰、冗余容灾、灰度发布和故障恢复等核心方案。重点围绕 什么是高可用？可用性的判断标准是啥？、哪些情况会导致系统不可用？、有哪些提高系统可用性的方法？、高可用、系统设计 等内容展开。结合 CodingMap 知识体系梳理。",
      "content_html": "<p>不管是平时刷的购物网站、用的在线支付，还是公司内部的核心业务系统，用户对&quot;服务不可用&quot;的容忍度越来越低。一次持续几分钟的故障，可能就会带来大量用户流失甚至直接的经济损失。</p>\n<p>所以，如何让系统在各种异常情况下依然能提供稳定的服务，就成了后端开发和架构设计中绕不开的话题。这篇文章会把高可用系统设计的核心思路和常见方案梳理一遍，包括 SLA 指标、单点故障治理、限流熔断、服务降级、缓存高可用、异步削峰、冗余容灾、灰度发布和故障恢复等。</p>\n<h2>什么是高可用？可用性的判断标准是啥？</h2>\n<p><strong>高可用（High Availability，简称 HA）</strong> 是指系统在绝大部分时间内能够持续提供正常服务的能力。高可用代表系统即使在发生硬件故障或者系统升级的时候，服务仍然是可用的。</p>\n<p>一般情况下，我们使用 <strong>多少个 9</strong> 来评判一个系统的可用性，比如 99.9999% 就是代表该系统在所有的运行时间中只有 0.0001% 的时间是不可用的，这样的系统就是非常非常高可用的了！可用性较差的系统可能连 90%（1 个 9）都达不到。</p>\n<p>| 可用性等级 | 可用性百分比 | 年度停机时间 | 典型场景     |<br>\n|</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高可用"
      ]
    },
    {
      "title": "2026 最新高可用系统设计面试题总结：SLA、限流熔断、重试幂等与容灾",
      "url": "https://codingmap.strova.top/high-availability/high-availability-system-interview-questions.html",
      "id": "https://codingmap.strova.top/high-availability/high-availability-system-interview-questions.html",
      "summary": "2026 最新高可用系统设计面试题总结，覆盖 SLA、可用性指标、单点故障、RTO/RPO、限流降级熔断、超时重试、接口幂等、性能压测、故障演练和容灾架构等高频考点。重点围绕 高可用基础、冗余与容灾、限流、降级与熔断、高可用 等内容展开。结合 CodingMap 知识体系梳理 高可用 / High Availabil。",
      "content_html": "<p>这部分内容摘自 <a href=\"https://javaguide.cn/\" target=\"_blank\" rel=\"noopener noreferrer\">JavaGuide</a> 下面几篇文章的重点：</p>\n<p>高可用设计：</p>\n<ul>\n<li><a href=\"https://javaguide.cn/high-availability/high-availability-system-design.html\" target=\"_blank\" rel=\"noopener noreferrer\">高可用系统设计指南</a></li>\n<li><a href=\"https://javaguide.cn/high-availability/redundancy.html\" target=\"_blank\" rel=\"noopener noreferrer\">冗余设计详解</a></li>\n<li><a href=\"https://javaguide.cn/high-availability/performance-test.html\" target=\"_blank\" rel=\"noopener noreferrer\">性能测试入门</a></li>\n</ul>\n<p>限流、降级、熔断：</p>\n<ul>\n<li><a href=\"https://javaguide.cn/high-availability/limit-request.html\" target=\"_blank\" rel=\"noopener noreferrer\">服务限流详解</a></li>\n<li><a href=\"https://javaguide.cn/high-availability/fallback-and-circuit-breaker.html\" target=\"_blank\" rel=\"noopener noreferrer\">降级&amp;熔断详解</a></li>\n</ul>\n<p>超时、重试、幂等：</p>\n<ul>\n<li><a href=\"https://javaguide.cn/high-availability/timeout-and-retry.html\" target=\"_blank\" rel=\"noopener noreferrer\">超时&amp;重试详解</a></li>\n<li><a href=\"https://javaguide.cn/high-availability/idempotency.html\" target=\"_blank\" rel=\"noopener noreferrer\">接口幂等方案总结</a></li>\n</ul>\n<h2>高可用基础</h2>\n<h3>⭐️什么是高可用？</h3>\n<p>高可用（High Availability）指系统在面对机器故障、网络抖动、依赖异常、流量突增等情况时，仍然能够尽可能持续提供服务。</p>\n<p>它不是追求系统永远不出故障，而是关注 3 件事：</p>\n<ol>\n<li><strong>故障尽量少发生</strong>：通过测试、评审、容量规划、灰度发布降低故障概率。</li>\n<li><strong>故障发生后影响尽量小</strong>：通过隔离、限流、降级、熔断控制故障范围。</li>\n<li><strong>故障发生后恢复尽量快</strong>：通过监控、告警、预案、自动故障转移缩短恢复时间。</li>\n</ol>\n<p>面试里不要把高可用理解成“多部署几台机器”。多副本只能减少单点故障，但真正的高可用还要覆盖流量控制、依赖保护、数据安全、故障发现和恢复闭环。</p>\n<h3>可用性 99.9%、99.99% 分别意味着什么？</h3>\n<p>可用性通常用一段时间内系统可正常服务的比例衡量。</p>\n<p>粗略换算：</p>\n<p>| 可用性  | 年不可用时间量级 |<br>\n|</p>\n",
      "image": "https://static001.infoq.cn/resource/image/8d/15/8ded7a2b90e1482093f92fff555b3615.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高可用"
      ]
    },
    {
      "title": "接口幂等性设计详解：幂等键、Token、唯一索引、去重表与支付回调",
      "url": "https://codingmap.strova.top/high-availability/idempotency.html",
      "id": "https://codingmap.strova.top/high-availability/idempotency.html",
      "summary": "接口幂等性设计详解，讲解幂等概念、幂等键、重复请求来源、Token 机制、唯一索引、去重表、乐观锁、悲观锁、分布式锁，以及订单和支付回调等场景的幂等方案。重点围绕 什么是幂等（idempotency）？、如何保证接口幂等性？、如何验证幂等方案？、参考、接口幂等 等内容展开。结合 CodingMap 知识体系梳理 高可。",
      "content_html": "<p>接口幂等性是面试中常见的问题，也是日常开发过程中经常需要解决的问题。</p>\n<h2>什么是幂等（idempotency）？</h2>\n<p>幂等（idempotency）本身是一个数学概念，常见于抽象代数中，表示一个操作执行一次和执行多次产生的效果相同。放到函数上，可以理解为 $f(f(x)) = f(x)$。例如，$f(x)=|x|$ 就是幂等的，因为对一个数取一次绝对值和取多次绝对值，结果都一样。</p>\n<p>在接口语义里，幂等主要指多次相同请求对服务端资源产生的预期效果，和执行一次请求一致。至于响应内容是否完全相同，要看业务设计；资金类接口通常会额外要求返回稳定的业务结果。</p>\n<p>针对数据操作来说：</p>\n<ul>\n<li>插入类操作要依赖业务唯一键或幂等键，避免重复创建数据。</li>\n<li>更新类操作要区分赋值更新和累加更新。<code>update set status = 'PAID'</code> 这类赋值更新更容易做到幂等，<code>update set balance = balance - 100</code> 这类累加更新必须额外控制重复执行。</li>\n</ul>\n<p>接口幂等性问题通常由网络波动、用户重复操作、客户端/网关超时重试、消息重复投递等触发；响应慢会放大这些重复请求的概率。</p>\n<p><strong>不保证幂等会有什么后果？</strong></p>\n<p>没有保证幂等会导致产生严重的生产级别的 Bug，比较典型的就是涉及到钱的业务场景。就比如在没有保证幂等性的情况下，我作为用户在付款的时候，同时点击了多次付款按钮，后端处理了多次相同的扣款请求，结果导致账户被扣了多次钱。这类问题在资金类业务中属于高风险生产事故。</p>\n<p>所以，资金、库存、订单、优惠券这类场景都需要认真处理幂等。另外，保证幂等性并不是说前端做了就可以，后端同样也要做。</p>\n<h2>如何保证接口幂等性？</h2>\n<p>前端可以在用户提交请求后将按钮置灰，或者将按钮置为不可点击。这只能降低重复提交概率，不能作为幂等保证。用户可以刷新页面、重放请求、绕过前端，也可能由网关超时重试、消息队列重复投递触发重复调用。真正的幂等边界必须放在服务端，尤其是资金、库存、订单这类会改变业务状态的接口。</p>\n<p>后端保证幂等性就稍微麻烦一点，方法也有很多种，比如悲观锁、唯一索引、去重表、乐观锁、分布式锁、Token 机制等。</p>\n<p>悲观锁和分布式锁的核心思想都是通过加锁来保证同一时刻只有一个请求能被执行。但仅仅这样是不够的，还需要配合根据业务逻辑进行幂等性判断，例如，注册场景检测指定的电话/邮箱/用户名是否已经被注册、订单支付场景检测订单的状态。</p>\n<p>实际项目里更常见的是组合方案：<strong>业务唯一键 / 幂等号负责识别重复请求，数据库唯一索引或去重表负责兜底，状态机负责防止状态回退；分布式锁只在需要串行化同一业务资源操作时使用。</strong></p>\n<p>常见场景可以先这样理解：</p>\n<ul>\n<li><strong>表单重复提交</strong>：前端按钮置灰只能减少重复点击，服务端最好再配一次性 Token，保证同一张表单只能成功提交一次。</li>\n<li><strong>创建订单</strong>：订单号、请求号这类业务唯一键一定要有，数据库再加唯一索引兜底。重复请求进来时，不是再创建一笔订单，而是返回已有订单结果。</li>\n<li><strong>支付回调</strong>：第三方交易流水号要能唯一标识一笔回调，再配合订单状态机和去重表，避免同一笔支付被重复入账。</li>\n<li><strong>库存扣减</strong>：重点是防止重复扣和超卖，可以结合状态校验、乐观锁、Redis Lua 或数据库事务来做。</li>\n<li><strong>消息消费</strong>：消息可能重复投递，所以要用消息 ID 或业务流水号记录消费结果。已经消费过的消息，直接跳过或者返回成功。</li>\n</ul>\n<p>方案选择时可以遵循几个优先级：</p>\n<ol>\n<li>能用业务唯一键解决的，优先用业务唯一键。</li>\n<li>能用数据库唯一索引兜底的，不要只依赖缓存。</li>\n<li>涉及状态流转的，用状态机防止重复执行和状态回退。</li>\n<li>需要保护同一业务资源的临界区时，再考虑分布式锁。</li>\n<li>高风险资金场景要多层兜底，不要押宝单一机制。</li>\n</ol>\n<h3>悲观锁</h3>\n<p>在 Java 中，可以使用 <code>ReentrantLock</code> 类、<code>synchronized</code> 关键字这类 JDK 自带的悲观锁来保证同一时刻只有一个线程能够进行修改。不过，JDK 自带的锁属于本地锁，分布式环境下无法使用。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/jvm-local-lock.png\" alt=\"本地锁\"></p>\n<p>除了利用 JDK 提供的悲观锁之外，数据库自身也自带排他锁（X 锁）。排他锁又称写锁/独占锁，事务在修改记录的时候获取排他锁，不允许多个事务同时获取。如果一条记录已经被加了排他锁，其他事务不能再对这条记录加不兼容的锁。</p>\n<p>在 MySQL 里使用排他锁：</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SELECT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> ... </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">FOR</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> UPDATE</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>排他锁只能在支持事务的存储引擎（如 InnoDB）中使用，且只能在事务中使用。<code>SELECT ... FOR UPDATE</code> 最好命中合适索引，否则 InnoDB 可能扫描并锁住大量记录，甚至接近全表锁定效果，严重影响并发。在 MySQL 默认的 RR（REPEATABLE READ）隔离级别下，InnoDB 可能使用 Next-Key Lock（记录锁 + 间隙锁），更要通过 <code>EXPLAIN</code> 确认执行计划命中索引。</p>\n<p>高并发的场景下，激烈的锁竞争会造成线程阻塞，大量阻塞线程会导致系统的上下文切换，增加系统的性能开销。并且，悲观锁还可能会存在死锁问题，影响代码的正常运行。使用悲观锁时，要保证加锁顺序一致、事务尽量短、查询命中索引，并结合 <code>EXPLAIN</code> 和死锁日志排查锁等待问题。</p>\n<h3>乐观锁</h3>\n<p>乐观锁一般会使用版本号机制或 CAS 算法实现。</p>\n<p>拿版本号机制来说，通过在表中增加一个版本号字段，每次更新数据时，检查当前版本号是否和数据库中的一致。如果一致，则更新成功，并且版本号加一。如果不一致，则更新失败，表示数据已经被其他请求修改过。</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">-- 更新数据，修改 price 并将 version 加一，并且检查 version 是否为 1（假设 version 的初始值为 1）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">update</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> goods </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">set</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> price </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> price + </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">100</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> + </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> where</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> id </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> and</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">-- 由于 version 已经变为 2，因此，下面的 SQL 执行无效</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">update</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> goods </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">set</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> price </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> price + </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">100</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> + </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> where</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> id </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> and</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>高并发的场景下，乐观锁通常不会像悲观锁那样长时间阻塞等待，能减少锁竞争带来的线程堆积，在性能上往往会更胜一筹。但数据库更新仍可能涉及行锁，复杂事务下不能说完全没有死锁风险。如果冲突频繁发生（写占比非常多的情况），会频繁失败和重试（悲观锁的开销是固定的），这样同样会非常影响性能，导致 CPU 飙升。不过，这种方法主要适用于更新数据的场景。</p>\n<p>乐观锁更新失败后要提前设计处理策略：可以返回明确的冲突提示，由调用方决定是否重试；也可以在服务端有限重试 2～3 次；还可以查询当前状态并返回已有结果。资金类场景不建议盲目自动重试，应优先返回冲突或查询最终状态。</p>\n<h3>唯一索引</h3>\n<p>通过在表中加上唯一索引，保证数据的唯一性。如果有重复的数据插入，会抛出异常，程序可以捕获异常并处理。不过，这种方法只适用于插入数据的场景。</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">create</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> table</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> t_order</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    id </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> unsigned </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">PRIMARY KEY</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> AUTO_INCREMENT COMMENT </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"主键\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">    `code`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> varchar</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">200</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">not null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> COMMENT </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"流水号\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">    `customer_id`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  int</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> unsigned COMMENT </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"会员id\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">    `amount`</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> decimal</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">10</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) unsigned </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">not null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> COMMENT </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"总金额\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    -- 省略其他订单字段</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    -- 省略其他索引</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    -- 订单流水号唯一</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    UNIQUE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> unq_code(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">`code`</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) COMMENT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"订单表\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>不要只依靠唯一索引完成全部幂等逻辑，但插入类场景建议把唯一索引作为数据库层兜底，避免并发下产生重复数据。</p>\n<h3>去重表</h3>\n<p>去重表本质上也是一种唯一索引方案。去重表是一张专门用于记录请求信息的表，其中某个字段需要建立唯一索引，用于标识请求的唯一性。当客户端发出请求时，服务端会将这次请求的一些信息（如订单号、交易流水号等）插入到去重表中，如果插入成功，说明这是第一次请求，可以执行后续的业务逻辑；如果插入失败，说明这是重复请求，可以直接返回或者忽略。</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">CREATE</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> TABLE</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> deduplication_table</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    id </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> unsigned </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">PRIMARY KEY</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> AUTO_INCREMENT COMMENT </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"主键\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    processed_code </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">varchar</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">200</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">not null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> COMMENT </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"已处理的订单流水号\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    -- 省略其他字段</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    UNIQUE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> unq_processed_code(processed_code)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) COMMENT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"去重表\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>去重表的插入要和后续业务操作放在同一个数据库事务中。事务提交成功，才表示“去重记录存在 = 业务已经处理完成”；如果业务逻辑失败回滚，去重记录也要一起回滚，后续请求才能正常重入。否则可能出现“去重记录已写入，但业务实际没完成”的不一致状态。分库分表导致去重表和业务表不在同一物理库时，需要额外考虑 TCC / Saga 等分布式事务方案，或者改用其他能保证一致性的设计。</p>\n<h3>分布式锁</h3>\n<p>分布式系统下，不同的服务/客户端通常运行在独立的 JVM 进程上。如果需要跨 JVM 串行化同一业务资源的操作，就可以考虑使用<strong>分布式锁</strong>。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock.png\" alt=\"分布式锁\"></p>\n<p>基于 MySQL 虽然也能实现分布式锁（如利用 <code>SELECT ... FOR UPDATE</code> 或唯一索引），但性能和可靠性通常不如专门的协调服务，生产中较少采用。</p>\n<p>通常情况下，我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁，Redis 用的要更多一点。</p>\n<p>关系型数据库也可以参与分布式场景下的并发控制，比如唯一索引兜底、状态机校验、行锁串行化；但乐观锁和唯一索引通常不归类为分布式锁。</p>\n<p>关于分布式锁的详细介绍以及如何基于 Redis 和 ZooKeeper 实现分布式锁，我写过专门的文章介绍，推荐看看：</p>\n<ul>\n<li><a href=\"https://javaguide.cn/distributed-system/distributed-lock.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式锁介绍</a></li>\n<li><a href=\"https://javaguide.cn/distributed-system/distributed-lock-implementations.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式锁常见实现方案总结</a></li>\n</ul>\n<p>需要注意的是，这里的分布式锁是根据唯一标识（比如订单号）生成的。获取到锁只说明当前可以进入临界区，不代表历史上没有处理过。进入临界区后仍必须查询业务状态或幂等记录。例如支付接口要检查订单是否已支付、交易流水号是否已处理，再决定执行业务逻辑还是直接返回历史结果。</p>\n<p>以下示例基于 Redisson 3.x，使用 <code>RLock</code> 实现幂等的伪代码如下：</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 唯一标识</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> uniqueId </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"order123\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 1. 根据唯一标识生成分布式锁对象</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">RLock</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> lock </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> redisson</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getLock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"lock:\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> uniqueId);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">boolean</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> locked </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> false</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">try</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 2. 尝试获取锁，最多等待 3 秒，拿到锁后 30 秒自动释放</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    locked </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> lock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">tryLock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">3</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">30</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">TimeUnit</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">SECONDS</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">locked) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 获取锁失败：返回处理中、稍后重试，或查询历史处理结果</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 3. 进入临界区后，仍要做业务幂等判断</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 例如：查询订单状态 / 幂等流水是否已存在</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 未处理才执行业务逻辑，已处理则直接返回历史结果</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    ...</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">} </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">finally</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 4. 只有当前线程持有锁时才能释放</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (locked </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x26;&#x26;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> lock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">isHeldByCurrentThread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        lock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">unlock</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>如果不显式指定 <code>leaseTime</code>，Redisson 会通过 Watchdog 给持有中的锁续期；如果指定了 <code>leaseTime</code>，锁会在租约时间后自动释放。上面的 30 秒只是示例值，实际使用时应根据业务最长执行时间选择，通常建议设置为预估耗时的 2～3 倍。</p>\n<p>使用分布式锁还要额外关注这些问题：</p>\n<ul>\n<li>锁粒度：按订单号、用户 ID、资源 ID 加锁，不要使用全局锁。</li>\n<li>锁超时：业务执行时间超过 <code>leaseTime</code> 时，锁可能提前释放。</li>\n<li>锁释放：必须校验当前线程 / 当前 owner，避免释放别人的锁。</li>\n<li>拿锁失败：要决定返回“处理中”、提示稍后重试，还是查询历史处理结果。</li>\n<li>业务兜底：进入锁后仍要查订单状态或幂等流水，不能只依赖锁。</li>\n</ul>\n<h3>Token 机制</h3>\n<p>Token 机制的核心思想是为每一次操作生成一个唯一性的凭证 token。Token 应由服务端生成，保证随机性、不可预测和短有效期。如果服务端存储 token，可以用 Redis 记录并一次性消费；如果做无状态 token，才需要考虑签名、防篡改和过期时间。</p>\n<p>这样的话，就需要两次请求才能完成一次业务操作：</p>\n<ol>\n<li>请求获取服务器端 token，token 需要设置有效时间（可以设置短一点），服务端将该 token 保存起来（通常保存在缓存中）。</li>\n<li>执行真正的请求，将上一步获取到的 token 放到 header 或者作为请求参数。服务端验证 token 的有效性，如果有效，执行业务逻辑；如果无效，拒绝请求，返回提示信息。</li>\n</ol>\n<p>服务端验证 token 时，不能先 <code>GET</code> 再 <code>DEL</code>。Redis 6.2.0 及以上版本可以使用 <code>GETDEL</code>，低版本可以使用 Lua 脚本完成“校验 + 删除”的原子操作，确保同一个 token 只能被一个请求消费。</p>\n<p>得物技术的<a href=\"https://mp.weixin.qq.com/s/yvKASWcRLfOok-NFPrIRsw\" target=\"_blank\" rel=\"noopener noreferrer\">分布式系统设计中的并发访问解决方案</a>这篇文章把这个过程图解得挺清晰。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/distributed-system/idempotent-token.png\" alt></p>\n<p>先执行业务逻辑再删除 token 还是先删除 token 再执行业务逻辑呢？两者似乎都有风险：</p>\n<ul>\n<li>先执行业务逻辑的话，客户端可能会在该 token 还存在的时候又携带 token 发起请求，由于 token 还存在，第二次请求也会验证通过。</li>\n<li>先删除 token 的话，如果业务逻辑执行超时或者出现网络波动，客户端需要重试请求，那 token 就用不成了。</li>\n</ul>\n<p>一般更推荐先原子消费 token，再执行业务逻辑；但要配套失败提示、重新获取 token、业务事务回滚和幂等流水记录。资金类场景不能只靠 token，还要用订单状态和交易流水兜底。</p>\n<p>Token 机制依赖 Redis 保存 token 生命周期，也要提前设计 Redis 不可用时的降级策略。普通表单可以提示稍后重试；资金类接口不建议熔断放行，宁可返回失败，也要避免重复扣款或重复入账。</p>\n<h3>支付回调幂等流程</h3>\n<p>支付回调是幂等设计里非常典型的场景，第三方支付平台可能因为网络超时、响应丢失等原因多次回调同一笔交易。一个更稳妥的流程如下：</p>\n<ol>\n<li>回调请求携带第三方交易流水号 <code>transaction_id</code>。</li>\n<li>回调记录表对 <code>transaction_id</code> 建唯一索引。</li>\n<li>查询订单状态，只有 <code>UNPAID -&gt; PAID</code> 这类合法状态流转才允许更新。</li>\n<li>更新订单状态、写资金流水、写回调记录放在同一个数据库事务中。</li>\n<li>重复回调直接返回成功，避免第三方继续重试。</li>\n</ol>\n<h2>如何验证幂等方案？</h2>\n<p>幂等方案不能只看代码，还要通过测试和数据校验确认它真的有效。尤其是订单、支付、库存这类场景，不能只测“正常请求成功一次”，还要故意制造重复请求、超时、失败和回滚。</p>\n<p>比较实用的验证方式有这几类：</p>\n<ol>\n<li><strong>并发压测</strong>：同一个订单号、支付流水号或幂等 key，同时发起 50～100 个请求，最终只能有一次真正执行业务逻辑。其他请求要么返回相同结果，要么返回“处理中”或“已处理”，不能产生多条订单、多条资金流水或多次库存扣减。</li>\n<li><strong>重试测试</strong>：模拟客户端超时重试、网关重试、第三方回调重试、MQ 重复投递等情况。重点看重复请求进来后，是不是能命中幂等记录或业务状态，而不是重新执行业务逻辑。</li>\n<li><strong>异常测试</strong>：在业务执行到一半时制造异常，比如订单状态已更新但流水写入失败、Token 已消费但事务回滚、拿到分布式锁后服务重启。恢复后再次请求，数据仍然要能对齐，不能出现“半成功半失败”的脏状态。</li>\n<li><strong>数据校验</strong>：压测或异常测试结束后，对订单表、流水表、库存表、去重表做交叉校验。例如同一笔支付只能有一条成功流水，订单只能从 <code>UNPAID</code> 变成 <code>PAID</code> 一次，库存扣减数量要和成功业务单数一致。</li>\n<li><strong>日志审计</strong>：日志里要能查到幂等 key、请求 ID、业务单号、处理状态和返回结果。线上真出问题时，能根据这些字段判断请求是第一次处理、重复处理、处理中，还是被幂等逻辑拦截了。</li>\n</ol>\n<p>简单来说，验证幂等不是看“接口会不会返回成功”，而是看重复请求和异常路径下，业务数据是否始终只被正确处理一次。</p>\n<h2>参考</h2>\n<ul>\n<li>高并发下如何保证接口的幂等性？：<a href=\"https://mp.weixin.qq.com/s/7P2KbWjjX5YPZCInoox-xQ\" target=\"_blank\" rel=\"noopener noreferrer\">https://mp.weixin.qq.com/s/7P2KbWjjX5YPZCInoox-xQ</a></li>\n<li>解决幂等问题，只需要记住这个口诀！：<a href=\"https://mp.weixin.qq.com/s/EatpiCzNlTw1viO_flQIpg\" target=\"_blank\" rel=\"noopener noreferrer\">https://mp.weixin.qq.com/s/EatpiCzNlTw1viO_flQIpg</a></li>\n</ul>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/jvm-local-lock.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高可用"
      ]
    },
    {
      "title": "服务限流详解：固定窗口、滑动窗口、令牌桶、漏桶与分布式限流",
      "url": "https://codingmap.strova.top/high-availability/limit-request.html",
      "id": "https://codingmap.strova.top/high-availability/limit-request.html",
      "summary": "服务限流原理与实现详解，系统讲解固定窗口、滑动窗口、令牌桶、漏桶等限流算法，以及 Guava RateLimiter、Bucket4j、Sentinel、Redis Lua、Redisson 的单机限流和分布式限流方案。重点围绕 常见限流算法有哪些？、针对什么来进行限流？、单机限流怎么做？、分布式限流怎么做？",
      "content_html": "<p>针对软件系统来说，限流就是对请求的速率进行限制，避免瞬时的大量请求击垮软件系统。毕竟，软件系统的处理能力是有限的。如果请求量超过了系统的处理能力，软件系统可能直接就挂掉了。</p>\n<p>限流可能会导致用户的请求无法被正确处理或者无法立即被处理，不过，这往往也是权衡了软件系统的稳定性之后得到的最优解。</p>\n<p>现实生活中，处处都有限流的实际应用，就比如排队买票是为了避免大量用户涌入购票而导致售票员无法处理。</p>\n<h2>常见限流算法有哪些？</h2>\n<p>简单介绍 4 种非常好理解并且容易实现的限流算法！</p>\n<blockquote>\n<p>图片来源于 InfoQ 的一篇文章<a href=\"https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673\" target=\"_blank\" rel=\"noopener noreferrer\">《分布式服务限流实战，已经为你排好坑了》</a>。</p>\n</blockquote>\n<h3>固定窗口计数器算法</h3>\n<p>固定窗口其实就是时间窗口，其原理是将时间划分为固定大小的窗口，在每个窗口内限制请求的数量或速率，即固定窗口计数器算法规定了系统单位时间处理的请求数量。</p>\n<p>假如我们规定系统中某个接口 1 分钟只能被访问 33 次的话，使用固定窗口计数器算法的实现思路如下：</p>\n<ul>\n<li>将时间划分固定大小窗口，这里是 1 分钟一个窗口。</li>\n<li>给定一个变量 <code>counter</code> 来记录当前接口处理的请求数量，初始值为 0（代表接口当前 1 分钟内还未处理请求）。</li>\n<li>1 分钟之内每处理一个请求之后就将 <code>counter+1</code> ，当 <code>counter=33</code> 之后（也就是说在这 1 分钟内接口已经被访问 33 次的话），后续的请求就会被全部拒绝。</li>\n<li>等到 1 分钟结束后，将 <code>counter</code> 重置 0，重新开始计数。</li>\n</ul>\n<p><img src=\"https://static001.infoq.cn/resource/image/8d/15/8ded7a2b90e1482093f92fff555b3615.png\" alt=\"固定窗口计数器算法\"></p>\n<p>优点：实现简单，易于理解。</p>\n<p>缺点：</p>\n<ul>\n<li>限流不够平滑。例如，我们限制某个接口每分钟只能访问 30 次，假设前 30 秒就有 30 个请求到达的话，那后续 30 秒将无法处理请求，这是不可取的，用户体验极差！</li>\n<li>存在窗口边界突刺问题。例如，我们限制某个接口 1 分钟只能访问 1000 次，如果第 1 分钟最后 1 秒进入了 1000 个请求，第 2 分钟第 1 秒又进入了 1000 个请求，由于新窗口的计数器已重置，短短 2 秒内就可能放行 2000 个请求，等效 QPS 远超预期的 <code>1000/60≈16.7</code>。</li>\n</ul>\n<h3>滑动窗口计数器算法</h3>\n<p><strong>滑动窗口计数器算法</strong> 算得上是固定窗口计数器算法的升级版，限流的粒度更小。</p>\n<p>滑动窗口计数器算法相比于固定窗口计数器算法的优化在于：<strong>它将一个固定窗口划分为多个更小的子窗口（格子）</strong> 。</p>\n<p>例如我们的接口限流每分钟处理 60 个请求，我们可以把 1 分钟分为 60 个窗口。每隔 1 秒移动一次，每个窗口一秒只能处理不大于 <code>60(请求数)/60（窗口数）</code> 的请求， 如果当前窗口的请求计数总和超过了限制的数量的话就不再处理其他请求。</p>\n<p>很显然， <strong>当滑动窗口的格子划分得越多，滑动窗口的滚动就越平滑，限流的统计就会越精确。</strong></p>\n<p><img src=\"https://static001.infoq.cn/resource/image/ae/15/ae4d3cd14efb8dc7046d691c90264715.png\" alt=\"滑动窗口计数器算法\"></p>\n<p>优点：</p>\n<ul>\n<li>相比于固定窗口算法，滑动窗口计数器算法能显著缓解窗口边界处的突发问题。</li>\n<li>相比于固定窗口算法，滑动窗口计数器算法的粒度更小，可以提供更精确的限流控制。</li>\n</ul>\n<p>缺点：</p>\n<ul>\n<li>与固定窗口计数器算法类似，滑动窗口计数器算法依然存在限流不够平滑的问题。</li>\n<li>相比较于固定窗口计数器算法，滑动窗口计数器算法实现和理解起来更复杂一些。</li>\n</ul>\n<h3>漏桶算法</h3>\n<p>我们可以把发请求的动作比作成注水到桶中，我们处理请求的过程可以比喻为漏桶漏水。我们往桶中以任意速率流入水，以一定速率流出水。当流入的水超过桶的容量时，多余的水会被丢弃，因为桶容量是不变的，保证了整体的速率。</p>\n<p>如果想要实现这个算法的话也很简单，准备一个队列用来保存请求，然后我们定期从队列中拿请求来执行就好了（和消息队列削峰/限流的思想是一样的）。</p>\n<p><img src=\"https://static001.infoq.cn/resource/image/75/03/75938d1010138ce66e38c6ed0392f103.png\" alt=\"漏桶算法\"></p>\n<p>优点：</p>\n<ul>\n<li>实现简单，易于理解。</li>\n<li>可以控制限流速率，避免网络拥塞和系统过载。</li>\n</ul>\n<p>缺点：</p>\n<ul>\n<li>无法应对突然激增的流量，因为只能以固定的速率处理请求，对系统资源利用不够友好。</li>\n<li>桶流入水（发请求）的速率如果一直大于桶流出水（处理请求）的速率的话，那么桶会一直是满的，一部分新的请求会被丢弃，导致服务质量下降。</li>\n</ul>\n<p>实际业务场景中，漏桶算法的适用范围相对窄一些。当业务需要允许一定程度的突发流量时，令牌桶通常更合适；但在需要严格控制输出速率的场景（如对接有明确 QPS 限制的第三方 API）中，漏桶仍然是合适的方案。</p>\n<h3>令牌桶算法</h3>\n<p>令牌桶算法也比较简单。和漏桶算法一样，我们的主角还是桶（这限流算法和桶过不去啊）。不过现在桶里装的是令牌了，请求在被处理之前需要从桶中获取一个令牌（获取即消费）。如果桶中没有令牌，请求就需要等待或被拒绝。我们根据限流大小，按照一定的速率往桶里添加令牌。如果桶装满了，就不能继续往里面继续添加令牌了。</p>\n<p><img src=\"https://static001.infoq.cn/resource/image/ec/93/eca0e5eaa35dac938c673fecf2ec9a93.png\" alt=\"令牌桶算法\"></p>\n<p>优点：</p>\n<ul>\n<li>可以限制平均速率，也允许在桶容量范围内处理短时突发流量。</li>\n<li>可以动态调整生成令牌的速率。</li>\n</ul>\n<p>缺点：</p>\n<ul>\n<li>如果令牌产生速率和桶的容量设置不合理，可能会出现问题比如大量的请求被丢弃、系统过载。</li>\n<li>相比于其他限流算法，实现和理解起来更复杂一些。</li>\n</ul>\n<h2>针对什么来进行限流？</h2>\n<p>实际项目中，还需要确定限流对象，也就是针对什么来进行限流。常见的限流对象如下：</p>\n<ul>\n<li>IP ：针对 IP 进行限流，适用面较广，简单粗暴。</li>\n<li>业务 ID：挑选唯一的业务 ID 以实现更针对性地限流。例如，基于用户 ID 进行限流。</li>\n<li>个性化：根据用户的属性或行为，进行不同的限流策略。例如， VIP 用户不限流，而普通用户限流。根据系统的运行指标（如 QPS、并发调用数、系统负载等），动态调整限流策略。例如，当系统负载较高的时候，控制每秒通过的请求减少。</li>\n</ul>\n<p>针对 IP 进行限流是目前比较常用的一个方案。不过，实际应用中需要注意用户真实 IP 地址的正确获取。常用的真实 IP 获取方法有 X-Forwarded-For 和 TCP Options 字段承载真实源 IP 信息。虽然 X-Forwarded-For 字段可能会被伪造，但因为其实现简单方便，很多项目还是直接用的这种方法。</p>\n<p>除了我上面介绍到的限流对象之外，还有一些其他较为复杂的限流对象策略，比如阿里的 Sentinel 还支持 <a href=\"https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6#%E5%9F%BA%E4%BA%8E%E8%B0%83%E7%94%A8%E5%85%B3%E7%B3%BB%E7%9A%84%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6\" target=\"_blank\" rel=\"noopener noreferrer\">基于调用关系的限流</a>（包括基于调用方限流、基于调用链入口限流、关联流量限流等）以及更细维度的 <a href=\"https://github.com/alibaba/Sentinel/wiki/%E7%83%AD%E7%82%B9%E5%8F%82%E6%95%B0%E9%99%90%E6%B5%81\" target=\"_blank\" rel=\"noopener noreferrer\">热点参数限流</a>（实时的统计热点参数并针对热点参数的资源调用进行流量控制）。</p>\n<p>另外，一个项目可以根据具体的业务需求选择多种不同的限流对象搭配使用。</p>\n<h2>单机限流怎么做？</h2>\n<p>单机限流针对的是单体架构应用。</p>\n<p>单机限流可以直接使用 Google Guava 自带的限流工具类 <code>RateLimiter</code> 。 <code>RateLimiter</code> 基于令牌桶算法，可以应对突发流量。</p>\n<blockquote>\n<p>Guava 地址：<a href=\"https://github.com/google/guava\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/google/guava</a></p>\n</blockquote>\n<p>除了最基本的令牌桶算法(平滑突发限流)实现之外，Guava 的<code>RateLimiter</code>还提供了 <strong>平滑预热限流</strong> 的算法实现。</p>\n<p>平滑突发限流就是按照指定的速率放令牌到桶里，而平滑预热限流会有一段预热时间，预热时间之内，速率会逐渐提升到配置的速率。</p>\n<p>下面通过两个简单的示例来了解。</p>\n<p>我们直接在项目中引入 Guava 相关依赖即可使用。</p>\n<div class=\"language-xml line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"xml\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-xml\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">dependency</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    &#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">groupId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">>com.google.guava&#x3C;/</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">groupId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    &#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">artifactId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">>guava&#x3C;/</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">artifactId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    &#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">>31.0.1-jre&#x3C;/</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">version</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;/</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">dependency</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><blockquote>\n<p>本文示例基于 Guava 31.x，<code>RateLimiter</code> 的核心 API 在更高版本中仍保持兼容。</p>\n</blockquote>\n<p>下面是一个简单的 Guava 平滑突发限流的 Demo。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">import</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> com.google.common.util.concurrent.RateLimiter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">/**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * 微信搜 JavaGuide 回复\"面试突击\"即可免费领取个人原创的 Java 面试手册</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> *</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * </span><span style=\"--shiki-light:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#C678DD;--shiki-dark-font-style:italic\">@author</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> Guide哥</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * @date 2021/10/08 19:12</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> **/</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> RateLimiterDemo</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> main</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">[] </span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\">args</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 1s 放 5 个令牌到桶里也就是 0.2s 放 1个令牌到桶里</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        RateLimiter</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> rateLimiter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> RateLimiter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">create</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">5</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 10</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i++) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            double</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> sleepingTime</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> rateLimiter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">acquire</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">printf</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"get 1 tokens: %ss%n\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, sleepingTime);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>输出：</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.0s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.188413s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.197811s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.198316s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.19864s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.199363s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.193997s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.199623s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.199357s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.195676s</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>需要注意的是，<code>acquire()</code> 是阻塞式获取令牌，在生产环境中通常更建议使用 <code>tryAcquire(timeout)</code> 设置等待上限，避免线程长时间阻塞。第一次输出等待时间为 <code>0.0s</code>，是因为 <code>RateLimiter.create(5)</code> 创建后桶里初始就有可用令牌。</p>\n<p>下面是一个简单的 Guava 平滑预热限流的 Demo。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">import</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> com.google.common.util.concurrent.RateLimiter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">import</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> java.util.concurrent.TimeUnit</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">/**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * 微信搜 JavaGuide 回复\"面试突击\"即可免费领取个人原创的 Java 面试手册</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> *</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * </span><span style=\"--shiki-light:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#C678DD;--shiki-dark-font-style:italic\">@author</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> Guide哥</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * @date 2021/10/08 19:12</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> **/</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> RateLimiterDemo</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> main</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">[] </span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\">args</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 1s 放 5 个令牌到桶里也就是 0.2s 放 1个令牌到桶里</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 预热时间为3s,也就说刚开始的 3s 内发牌速率会逐渐提升到 0.2s 放 1 个令牌到桶里</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        RateLimiter</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> rateLimiter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> RateLimiter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">create</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">5</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">3</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">TimeUnit</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">SECONDS</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 20</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i++) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            double</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> sleepingTime</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> rateLimiter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">acquire</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">printf</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"get 1 tokens: %sds%n\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, sleepingTime);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>输出：</p>\n<div class=\"language-bash line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"bash\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-bash\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.0s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.561919s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.516931s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.463798s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.41286s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.356172s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.300489s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.252545s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.203996s</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> tokens:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 0.198359s</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>另外，<strong>Bucket4j</strong> 是一个非常不错的基于令牌/漏桶算法的限流库。</p>\n<blockquote>\n<p>Bucket4j 地址：<a href=\"https://github.com/vladimir-bukhtoyarov/bucket4j\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/vladimir-bukhtoyarov/bucket4j</a></p>\n</blockquote>\n<p>相对于 Guava 的限流工具类来说，Bucket4j 提供的限流功能更加全面。不仅支持单机限流和分布式限流，还可以集成监控，搭配 Prometheus 和 Grafana 使用。</p>\n<p>不过，毕竟 Guava 也只是一个功能全面的工具类库，其提供的开箱即用的限流功能在很多单机场景下还是比较实用的。</p>\n<p>Spring Cloud Gateway 的内置限流器 <code>RedisRateLimiter</code> 基于 Redis + Lua 实现。Bucket4j 和 Resilience4j 更多是通过独立扩展或 Spring 生态组件与网关集成，并不是 Spring Cloud Gateway 内置限流器的替代演进关系。</p>\n<p>Resilience4j 是一个轻量级的容错组件，其灵感来自于 Hystrix。自<a href=\"https://github.com/Netflix/Hystrix/commit/a7df971cbaddd8c5e976b3cc5f14013fe6ad00e6\" target=\"_blank\" rel=\"noopener noreferrer\">Netflix 宣布不再积极开发 Hystrix</a> 之后，Spring 官方和 Netflix 都更推荐使用 Resilience4j 来做限流熔断。</p>\n<blockquote>\n<p>Resilience4j 地址: <a href=\"https://github.com/resilience4j/resilience4j\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/resilience4j/resilience4j</a></p>\n</blockquote>\n<p>一般情况下，为了保证系统的高可用，项目的限流和熔断都是要一起做的。</p>\n<p>Resilience4j 不仅提供限流，还提供了熔断、负载保护、自动重试等保障系统高可用开箱即用的功能。并且，Resilience4j 的生态也更好，很多网关都使用 Resilience4j 来做限流熔断的。</p>\n<p>因此，在绝大部分场景下 Resilience4j 或许会是更好的选择。如果是一些比较简单的限流场景的话，Guava 或者 Bucket4j 也是不错的选择。</p>\n<h2>分布式限流怎么做？</h2>\n<p>分布式限流针对的是分布式/微服务应用架构。在这种架构下，一种服务可能会被部署多份，如果需要控制全局配额，就要让多个实例共享限流状态。不过，并非所有微服务场景都必须使用分布式限流：如果配额可以按实例数均分（例如总配额 1000 QPS，10 个实例各限制 100 QPS），单机限流在性能和实现复杂度上反而更有优势。</p>\n<p>分布式限流常见的方案：</p>\n<ul>\n<li><strong>借助中间件限流</strong>：可以借助 Sentinel 或者使用 Redis 来自己实现对应的限流逻辑。</li>\n<li><strong>网关层限流</strong>：比较常用的一种方案，直接在网关层把限流给安排上了。不过，网关层限流通常也需要借助中间件/框架。就比如 Spring Cloud Gateway 的分布式限流实现 <code>RedisRateLimiter</code> 就是基于 Redis+Lua 来实现的，再比如 Spring Cloud Gateway 还可以整合 Sentinel 来做限流。</li>\n</ul>\n<p>如果你要基于 Redis 来手动实现限流逻辑的话，建议配合 Lua 脚本来做。</p>\n<p><strong>为什么建议 Redis+Lua 的方式？</strong> 主要有两点原因：</p>\n<ul>\n<li><strong>减少了网络开销</strong>：我们可以利用 Lua 脚本来批量执行多条 Redis 命令，这些 Redis 命令会被提交到 Redis 服务器一次性执行完成，大幅减小了网络开销。</li>\n<li><strong>原子性</strong>：一段 Lua 脚本可以视作一条命令执行，一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行，保证了操作不会被其他指令插入或打扰。</li>\n</ul>\n<p>需要注意的是，Lua 脚本在 Redis 中会原子执行，也意味着脚本执行期间会阻塞 Redis 的其他操作。因此，限流脚本要尽量保持轻量，避免复杂计算或大范围遍历，并结合 Redis 的 <code>lua-time-limit</code> 配置防止脚本执行时间失控。</p>\n<p>我这里就不放具体的限流脚本代码了，网上也有很多现成的优秀的限流脚本供你参考，就比如 Apache 网关项目 ShenYu 的 RateLimiter 限流插件就基于 Redis + Lua 实现了令牌桶算法/并发令牌桶算法、漏桶算法、滑动窗口算法。</p>\n<blockquote>\n<p>ShenYu 地址: <a href=\"https://github.com/apache/incubator-shenyu\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/apache/incubator-shenyu</a></p>\n</blockquote>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-availability/limit-request/shenyu-ratelimit-lua-scripts.png\" alt=\"ShenYu 限流脚本\"></p>\n<p>另外，如果不想自己写 Lua 脚本的话，也可以直接利用 Redisson 中的 <code>RRateLimiter</code> 来实现分布式限流，其底层实现就是基于 Lua 代码+令牌桶算法。</p>\n<p>Redisson 是一个开源的 Java 语言 Redis 客户端，提供了很多开箱即用的功能，比如 Java 中常用的数据结构实现、分布式锁、延迟队列等等。并且，Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。</p>\n<p><code>RRateLimiter</code> 的使用方式非常简单。我们首先需要获取一个<code>RRateLimiter</code>对象，直接通过 Redisson 客户端获取即可。然后，设置限流规则就好。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 创建一个 Redisson 客户端实例</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">RedissonClient</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> redissonClient </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Redisson</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">create</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 获取一个名为 \"javaguide.limiter\" 的限流器对象</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">RRateLimiter</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> rateLimiter </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> redissonClient</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getRateLimiter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"javaguide.limiter\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 尝试设置限流器的速率为每小时 100 次</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// RateType 有两种，OVERALL 是全局限流，PER_CLIENT 是单 Client 限流（可以认为就是单机限流）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">rateLimiter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">trySetRate</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">RateType</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">OVERALL</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">100</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">RateIntervalUnit</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">HOURS</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>接下来我们调用<code>acquire()</code>方法或<code>tryAcquire()</code>方法即可获取许可。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 获取一个许可，如果超过限流器的速率则会等待</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// acquire()是同步方法，对应的异步方法：acquireAsync()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">rateLimiter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">acquire</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 尝试在 5 秒内获取一个许可，如果成功则返回 true，否则返回 false</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// tryAcquire()是同步方法，对应的异步方法：tryAcquireAsync()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">boolean</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> res </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> rateLimiter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">tryAcquire</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">5</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">TimeUnit</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">SECONDS</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><code>RRateLimiter</code> 的限流精度会受到 Redis 性能和网络延迟影响，在超高并发场景下可能出现轻微超限。Redis 故障或主从切换时，<code>tryAcquire()</code> 可能抛出异常，业务层需要提前确定降级策略：回退到本地限流、直接拒绝，还是短时间放行，取决于业务对稳定性和可用性的取舍。</p>\n<h2>总结</h2>\n<p>这篇文章主要介绍了常见的限流算法、限流对象的选择以及单机限流和分布式限流分别应该怎么做。</p>\n<h2>参考</h2>\n<ul>\n<li>服务治理之轻量级熔断框架 Resilience4j：<a href=\"https://xie.infoq.cn/article/14786e571c1a4143ad1ef8f19\" target=\"_blank\" rel=\"noopener noreferrer\">https://xie.infoq.cn/article/14786e571c1a4143ad1ef8f19</a></li>\n<li>超详细的 Guava RateLimiter 限流原理解析：<a href=\"https://cloud.tencent.com/developer/article/1408819\" target=\"_blank\" rel=\"noopener noreferrer\">https://cloud.tencent.com/developer/article/1408819</a></li>\n<li>实战 Spring Cloud Gateway 之限流篇 👍：<a href=\"https://www.aneasystone.com/archives/2020/08/spring-cloud-gateway-current-limiting.html\" target=\"_blank\" rel=\"noopener noreferrer\">https://www.aneasystone.com/archives/2020/08/spring-cloud-gateway-current-limiting.html</a></li>\n<li>详解 Redisson 分布式限流的实现原理：<a href=\"https://juejin.cn/post/7199882882138898489\" target=\"_blank\" rel=\"noopener noreferrer\">https://juejin.cn/post/7199882882138898489</a></li>\n<li>一文详解 Java 限流接口实现 - 阿里云开发者：<a href=\"https://mp.weixin.qq.com/s/A5VYjstIDeVvizNK2HkrTQ\" target=\"_blank\" rel=\"noopener noreferrer\">https://mp.weixin.qq.com/s/A5VYjstIDeVvizNK2HkrTQ</a></li>\n<li>分布式限流方案的探索与实践 - 腾讯云开发者：<a href=\"https://mp.weixin.qq.com/s/MJbEQROGlThrHSwCjYB_4Q\" target=\"_blank\" rel=\"noopener noreferrer\">https://mp.weixin.qq.com/s/MJbEQROGlThrHSwCjYB_4Q</a></li>\n</ul>\n",
      "image": "https://static001.infoq.cn/resource/image/8d/15/8ded7a2b90e1482093f92fff555b3615.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高可用"
      ]
    },
    {
      "title": "性能测试入门：QPS、TPS、P99、负载测试、压力测试与压测工具",
      "url": "https://codingmap.strova.top/high-availability/performance-test.html",
      "id": "https://codingmap.strova.top/high-availability/performance-test.html",
      "summary": "性能测试入门指南，讲解 QPS、TPS、RT、P90/P99/P999、Little's Law、容量评估、曲棍球棒曲线、背压与自愈验证，以及负载测试、压力测试、稳定性测试和 JMeter、Gatling、ab 等压测工具。重点围绕 不同角色看网站性能、性能测试需要注意的点、常见性能指标、系统活跃度指标等核心内容。",
      "content_html": "<p>性能测试通常由专业的测试团队负责，那还需要我们开发学这个干嘛呢？了解性能测试的指标、分类以及工具等知识有助于我们更好地去写出性能更好的程序，另外作为开发这个角色，如果你会性能测试的话，相信也会为你的履历加分不少。</p>\n<p>本文结合了笔者的实践经验以及向测试团队请教所得，除此之外，我还借鉴了一些优秀书籍，希望对你有帮助。</p>\n<h2>不同角色看网站性能</h2>\n<h3>用户</h3>\n<p>当用户打开一个网站的时候，最关注的是什么？当然是 <strong>网站响应速度的快慢</strong>。比如我们点击了淘宝的主页，淘宝需要多久将首页的内容呈现在我的面前，我点击了提交订单按钮需要多久返回结果等等。</p>\n<p>所以，用户在体验我们系统的时候往往根据你的响应速度的快慢来评判你的网站的性能。</p>\n<h3>开发人员</h3>\n<p>用户与开发人员都关注速度，这个速度实际上就是我们的系统 <strong>处理用户请求的速度</strong>。</p>\n<p>开发人员一般情况下很难直观的去评判自己网站的性能，我们往往会根据网站当前的架构以及基础设施情况给一个大概的值，比如：</p>\n<ol>\n<li>项目架构是分布式的吗？</li>\n<li>用到了缓存和消息队列没有？</li>\n<li>高并发的业务有没有特殊处理？</li>\n<li>数据库设计是否合理？</li>\n<li>系统用到的算法是否还需要优化？</li>\n<li>系统是否存在内存泄漏的问题？</li>\n<li>项目使用的 Redis 缓存多大？服务器性能如何？用的是机械硬盘还是固态硬盘？</li>\n<li>……</li>\n</ol>\n<h3>测试人员</h3>\n<p>测试人员一般会根据性能测试工具来测试，然后一般会做出一个表格。这个表格可能会涵盖下面这些重要的内容：</p>\n<ol>\n<li>响应时间；</li>\n<li>请求成功率；</li>\n<li>吞吐量；</li>\n<li>……</li>\n</ol>\n<h3>运维人员</h3>\n<p>运维人员会倾向于根据 <strong>基础设施和资源的利用率</strong> 来判断网站的性能，比如我们的服务器资源使用是否合理、数据库资源是否存在滥用的情况、当然，这是传统的运维人员，现在 Devops 火起来后，单纯干运维的很少了。我们这里暂且还保留有这个角色。</p>\n<h2>性能测试需要注意的点</h2>\n<p>几乎没有文章在讲性能测试的时候提到这个问题，大家都会讲如何去性能测试，有哪些性能测试指标这些东西。</p>\n<h3>了解系统的业务场景</h3>\n<p><strong>性能测试之前更需要你了解当前的系统的业务场景。</strong> 对系统业务了解得不够深刻，我们很容易犯测试方向偏执的错误，从而导致我们忽略了对系统某些更需要性能测试的地方进行测试。</p>\n<p>比如我们的系统可以为用户提供发送邮件的功能，用户配置成功邮箱后只需输入相应的邮箱之后就能发送，系统每天大概能处理上万次发邮件的请求。很多人看到这个可能就直接开始使用相关工具测试邮箱发送接口，但是，发送邮件这个场景可能不是当前系统的性能瓶颈，这么多人用我们的系统发邮件，还可能有很多人一起发邮件，单单这个场景就这么人用，那用户管理可能才是性能瓶颈吧！</p>\n<h3>历史数据非常有用</h3>\n<p>当前系统所留下的历史数据非常重要，一般情况下，我们可以通过一些历史数据初步判定这个系统哪些接口调用的比较多、哪些服务承受的压力最大，这样的话，我们就可以针对这些地方进行更细致的性能测试与分析。</p>\n<p>另外，这些地方也就像这个系统的一个短板一样，优化好了这些地方会为我们的系统带来质的提升。</p>\n<h2>常见性能指标</h2>\n<p>性能指标是衡量系统性能的核心度量标准，理解各指标之间的关系对于性能分析至关重要。</p>\n<h3>响应时间</h3>\n<p><strong>响应时间 RT（Response Time）</strong> 是用户发出请求到收到系统处理结果所需的时间，包括网络传输、服务端处理与客户端渲染等环节。</p>\n<p><strong>响应时间指标（Latency Percentiles）</strong>：生产环境中看平均 RT 毫无意义，必须监控 <strong>P90、P99 和 P999</strong> 分位值。例如 P99 = 500ms 意味着 99% 的请求在 500ms 内返回。剩下 1% 的长尾慢调用（可能由 Cache Miss、慢 SQL 或 GC STW 引起）会长时间占用连接和工作线程；在极高并发下，线程池耗尽会导致新请求排队或被拒绝，上游继续超时重试后又会放大流量，最终可能引发雪崩。大量超快响应会拉低平均值，掩盖致命的长尾问题，此为典型的 <strong>“均值陷阱”</strong>。</p>\n<p>分位值参考标准如下：</p>\n<p>| 分位值 | RT 范围（示例） | 说明                     |<br>\n|</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高可用"
      ]
    },
    {
      "title": "冗余设计详解：RTO/RPO、高可用集群、同城灾备与异地多活",
      "url": "https://codingmap.strova.top/high-availability/redundancy.html",
      "id": "https://codingmap.strova.top/high-availability/redundancy.html",
      "summary": "冗余设计详解，讲解硬件冗余、服务冗余、数据冗余和地域冗余，覆盖 RTO/RPO、高可用集群、主备模式、主主模式、同城灾备、异地灾备、同城多活、异地多活和故障转移。重点围绕 什么是冗余？、容灾核心指标：RTO 和 RPO、冗余架构方案对比、故障转移机制、冗余设计 等内容展开。结合 CodingMap 知识体系梳理 高可。",
      "content_html": "<p>不管是硬件故障、机房断电，还是某个服务突然挂掉，单点永远是系统里最脆弱的环节。想让系统在各种意外情况下还能扛得住，最基本也最有效的办法就是 <strong>冗余</strong>——关键资源多备几份，坏了一份还有其他的顶上。</p>\n<p>这篇文章会把冗余设计的核心概念和常见方案梳理一遍：从 RTO/RPO 这两个容灾指标，到高可用集群、同城灾备、异地多活，再到 Redis Sentinel、Keepalived 这些具体的故障转移方案。</p>\n<h2>什么是冗余？</h2>\n<p><strong>冗余（Redundancy）</strong> 是提升系统可用性和数据持久性的常见手段，其核心思想是 <strong>通过部署多份相同的资源，当某一份资源出现故障时，其他资源可以接管其工作，从而保证系统的持续可用</strong>。</p>\n<p>冗余是提升系统可用性的基础手段，但冗余本身不等于高可用。只有配合故障检测、流量切换、数据复制、恢复演练和监控告警，才能真正降低故障影响。</p>\n<p>冗余设计可以从以下几个维度来理解：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-availability/redundancy-optimized-redundancy-types.png\" alt=\"冗余设计类型\"></p>\n<p>| 冗余类型     | 说明                   | 典型实现                                                     |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/high-availability/redundancy-optimized-redundancy-types.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高可用"
      ]
    },
    {
      "title": "超时和重试机制详解：超时设置、指数退避、随机抖动、重试风暴与幂等",
      "url": "https://codingmap.strova.top/high-availability/timeout-and-retry.html",
      "id": "https://codingmap.strova.top/high-availability/timeout-and-retry.html",
      "summary": "超时与重试机制详解，讲解连接超时、读取超时、超时时间设置、固定间隔重试、线性退避、指数退避、随机抖动、重试风暴、雪崩效应、幂等性设计，以及 Spring Retry、Resilience4j 等 Java 重试框架。重点围绕 超时机制、重试机制、线上观测指标、参考、超时重试 等内容展开。结合 CodingMap 知识。",
      "content_html": "<p>由于网络抖动、硬件故障、进程异常、依赖服务不可用等问题的不确定性，我们的系统或者服务永远不可能保证时刻都是可用的状态。</p>\n<p>为了最大限度地减小系统或者服务出现故障之后带来的影响，我们需要用到 <strong>超时（Timeout）</strong> 和 <strong>重试（Retry）</strong> 机制。</p>\n<p>超时和重试的核心思想确实不难理解，但在生产环境中正确使用它们却有不少门道。你平时接触到的绝大部分涉及远程调用的系统或者服务都会应用超时和重试机制。尤其是对于微服务系统来说，正确设置超时和重试非常重要。单体服务通常只涉及数据库、缓存、第三方 API、中间件等的网络调用，而微服务系统内部各个服务之间还存在着网络调用。</p>\n<h2>超时机制</h2>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-availability/timeout-mechanism-overview.png\" alt=\"超时机制\"></p>\n<h3>什么是超时机制？</h3>\n<p><strong>超时机制</strong> 说的是当一个请求超过指定的时间（比如 1s）还没有被处理的话，这个请求就会直接被取消并抛出指定的异常或者错误（比如 <code>504 Gateway Timeout</code>）。</p>\n<p>我们平时接触到的超时可以简单分为下面 2 种：</p>\n<p>| 超时类型                        | 说明                                               | 建议值          |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/high-availability/timeout-mechanism-overview.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高可用"
      ]
    },
    {
      "title": "高性能系统知识体系：CDN、负载均衡、数据库优化、缓存与消息队列",
      "url": "https://codingmap.strova.top/high-performance/",
      "id": "https://codingmap.strova.top/high-performance/",
      "summary": "高性能系统面试与架构学习路线，涵盖 CDN、负载均衡、读写分离、分库分表、冷热分离、深度分页、SQL 优化、消息队列和主流 MQ。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章、高性能 等内容展开。结合 CodingMap 知识体系梳理 高性能 的核心概念、实践方法、常见问题和高频面试考点，覆盖原理分析等核心内。",
      "content_html": "<p>这份 <strong>高性能系统知识体系</strong> 面向后端学习、系统设计和面试复习，围绕“减少延迟、提升吞吐、削峰填谷、降低数据库压力、优化数据访问路径”整理本站高性能相关文章。</p>\n<p>如果你时间有限，建议先看 <a href=\"/high-performance/high-performance-interview-questions.html\" target=\"_blank\">高性能系统设计面试题总结</a>，快速建立高频问题清单；如果你想系统补基础，可以按下面的阅读顺序推进。</p>\n<h2>适合谁看</h2>\n<ul>\n<li>正在系统学习高性能系统设计的后端开发者。</li>\n<li>准备校招、社招、中大厂后端面试的同学。</li>\n<li>想补齐 CDN、负载均衡、数据库优化、消息队列等工程能力的工程师。</li>\n<li>已经遇到慢 SQL、深分页、热点流量、消息积压、数据库压力等问题，但缺少系统解法的读者。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>高性能系统到底是在优化延迟、吞吐、资源利用率，还是在优化用户感知？</li>\n<li>CDN、负载均衡、缓存、数据库优化、消息队列分别解决链路上的哪些瓶颈？</li>\n<li>读写分离、分库分表、冷热分离、深度分页优化分别适合什么场景？</li>\n<li>Kafka、RocketMQ、RabbitMQ、Disruptor 的定位和选型差异是什么？</li>\n<li>面试中如何从“瓶颈定位 -&gt; 方案选择 -&gt; 取舍分析 -&gt; 落地风险”回答高性能问题？</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/high-performance/high-performance-interview-questions.html\" target=\"_blank\">高性能系统设计面试题总结</a>：先建立缓存、数据库、消息队列、负载均衡等高频问题清单。</li>\n<li><a href=\"/high-performance/cdn.html\" target=\"_blank\">CDN 工作原理详解</a> 和 <a href=\"/high-performance/load-balancing.html\" target=\"_blank\">负载均衡原理及算法详解</a>：理解流量入口和请求分发。</li>\n<li><a href=\"/high-performance/read-and-write-separation-and-library-subtable.html\" target=\"_blank\">读写分离和分库分表详解</a>、<a href=\"/high-performance/sql-optimization.html\" target=\"_blank\">常见 SQL 优化手段总结</a>、<a href=\"/high-performance/deep-pagination-optimization.html\" target=\"_blank\">深度分页介绍及优化建议</a>：补齐数据库性能优化主线。</li>\n<li><a href=\"/high-performance/message-queue/message-queue.html\" target=\"_blank\">消息队列基础知识总结</a>：理解异步处理、解耦、削峰、消息可靠性、顺序性和幂等。</li>\n<li>再根据技术栈深入 <a href=\"/high-performance/message-queue/kafka-questions-01.html\" target=\"_blank\">Kafka 常见问题总结</a>、<a href=\"/high-performance/message-queue/rocketmq-questions.html\" target=\"_blank\">RocketMQ 常见问题总结</a>、<a href=\"/high-performance/message-queue/rabbitmq-questions.html\" target=\"_blank\">RabbitMQ 常见问题总结</a>。</li>\n</ol>\n<h2>核心文章</h2>\n<h3>流量入口与请求分发</h3>\n<ul>\n<li><a href=\"/high-performance/cdn.html\" target=\"_blank\">CDN 工作原理详解</a>：理解 GSLB 调度、缓存策略、预热刷新、命中率优化和防盗链。</li>\n<li><a href=\"/high-performance/load-balancing.html\" target=\"_blank\">负载均衡原理及算法详解</a>：理解四层/七层负载均衡、服务端/客户端负载均衡和常见调度算法。</li>\n</ul>\n<h3>数据库与数据访问优化</h3>\n<ul>\n<li><a href=\"/high-performance/read-and-write-separation-and-library-subtable.html\" target=\"_blank\">读写分离和分库分表详解</a>：理解主从复制、读写分离、垂直拆分、水平拆分和分库分表后的问题。</li>\n<li><a href=\"/high-performance/data-cold-hot-separation.html\" target=\"_blank\">数据冷热分离详解</a>：理解冷热数据判定、分层存储、数据迁移一致性和冷数据查询优化。</li>\n<li><a href=\"/high-performance/sql-optimization.html\" target=\"_blank\">常见 SQL 优化手段总结</a>：梳理慢 SQL 定位、索引优化、查询重写和分页优化等实战方法。</li>\n<li><a href=\"/high-performance/deep-pagination-optimization.html\" target=\"_blank\">深度分页介绍及优化建议</a>：理解深分页性能问题，以及范围查询、子查询优化、延迟关联和覆盖索引方案。</li>\n</ul>\n<h3>消息队列与异步削峰</h3>\n<ul>\n<li><a href=\"/high-performance/message-queue/\" target=\"_blank\">消息队列专题</a>：从消息队列基础讲到 Kafka、RocketMQ、RabbitMQ 和 Disruptor 的使用边界。</li>\n<li><a href=\"/high-performance/message-queue/message-queue.html\" target=\"_blank\">消息队列基础知识总结</a>：理解应用场景、消息模型、消息可靠性、顺序性、幂等和积压处理。</li>\n<li><a href=\"/high-performance/message-queue/kafka-questions-01.html\" target=\"_blank\">Kafka 常见问题总结</a>：掌握 Kafka 架构、高性能原理、消息可靠性、顺序性和 Rebalance。</li>\n<li><a href=\"/high-performance/message-queue/rocketmq-questions.html\" target=\"_blank\">RocketMQ 常见问题总结</a>：理解 RocketMQ 架构、消息类型、存储机制、可靠性和 5.x 新特性。</li>\n<li><a href=\"/high-performance/message-queue/rabbitmq-questions.html\" target=\"_blank\">RabbitMQ 常见问题总结</a>：理解 AMQP、Exchange 类型、确认机制、死信队列、延迟队列和镜像队列。</li>\n<li><a href=\"/high-performance/message-queue/disruptor-questions.html\" target=\"_blank\">Disruptor 常见问题总结</a>：理解 RingBuffer、Sequencer、WaitStrategy、无锁设计和缓存行填充。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>高性能系统优化时，应该先定位哪些指标？</li>\n<li>CDN 和负载均衡分别解决什么问题？</li>\n<li>四层负载均衡和七层负载均衡有什么区别？</li>\n<li>读写分离会带来哪些一致性问题？如何处理主从延迟？</li>\n<li>分库分表后如何处理分布式 ID、跨库 JOIN 和分布式事务？</li>\n<li>深度分页为什么慢？有哪些优化方案？</li>\n<li>消息队列如何保证消息不丢、不重复、不乱序？</li>\n<li>Kafka、RocketMQ、RabbitMQ 如何选型？</li>\n<li>消息积压应该如何定位和处理？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/high-availability/\" target=\"_blank\">高可用系统知识体系</a></li>\n<li><a href=\"/distributed-system/\" target=\"_blank\">分布式系统知识体系</a></li>\n<li><a href=\"/database/\" target=\"_blank\">数据库</a></li>\n<li><a href=\"/system-design/\" target=\"_blank\">系统设计</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高性能"
      ]
    },
    {
      "title": "CDN工作原理详解",
      "url": "https://codingmap.strova.top/high-performance/cdn.html",
      "id": "https://codingmap.strova.top/high-performance/cdn.html",
      "summary": "本文详解 CDN（内容分发网络）的核心原理，涵盖 GSLB 全局负载均衡调度机制、CDN 缓存策略（预热/回源/刷新）、命中率与回源率优化，以及 Referer 防盗链与时间戳防盗链等安全机制，帮助你全面掌握 CDN 加速技术。重点围绕 什么是 CDN ？、为什么不直接将服务部署在多个不同的地方？",
      "content_html": "<h2>什么是 CDN ？</h2>\n<p><strong>CDN</strong> 全称是 Content Delivery Network/Content Distribution Network，翻译过的意思是 <strong>内容分发网络</strong> 。</p>\n<p>我们可以将内容分发网络拆开来看：</p>\n<ul>\n<li><strong>内容</strong>：指的是静态资源，包括图片、视频、文档、JS、CSS、HTML 等。</li>\n<li><strong>分发网络</strong>：指的是将这些静态资源分发到位于多个不同地理位置机房中的服务器上，从而实现<strong>就近访问</strong>——例如北京的用户直接访问北京机房的数据。</li>\n</ul>\n<p>简单来说，<strong>CDN 就是将静态资源分发到多个不同的地方以实现就近访问，进而加快静态资源的访问速度，减轻源站服务器以及带宽的负担。</strong></p>\n<p>类似于京东建立的庞大仓储运输体系，京东物流在全国拥有非常多的仓库，仓储网络几乎覆盖全国所有区县。这样的话，用户下单的第一时间，商品就从距离用户最近的仓库直接发往对应的配送站，再由京东小哥送到你家。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/cdn/jingdong-wuliu-cangpei.png\" alt=\"京东仓配系统\"></p>\n<p>你可以将 CDN 看作是服务上一层的<strong>特殊缓存服务</strong>，分布在全国各地，主要用来处理静态资源的请求。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-101.png\" alt=\"CDN 简易示意图\"></p>\n<p>我们经常拿全站加速和内容分发网络做对比，不要把两者搞混了！<strong>全站加速</strong>（不同云服务商叫法不同，腾讯云叫 ECDN、阿里云叫 DCDN）既可以加速静态资源又可以加速动态资源，而<strong>内容分发网络（CDN）</strong> 主要针对的是 <strong>静态资源</strong> 。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-aliyun-dcdn.png\" alt=\"阿里云文档：https://help.aliyun.com/document_detail/64836.html\"></p>\n<p>绝大部分公司都会在项目开发中使用 CDN 服务，但很少会有自建 CDN 服务的公司。基于成本、稳定性和易用性考虑，建议直接选择专业的云厂商（比如阿里云、腾讯云、华为云、青云）或者 CDN 厂商（比如网宿、蓝汛）提供的开箱即用的 CDN 服务。</p>\n<h2>为什么不直接将服务部署在多个不同的地方？</h2>\n<p>很多朋友可能要问了：<strong>既然是就近访问，为什么不直接将服务部署在多个不同的地方呢？</strong></p>\n<p>这涉及到<strong>静态资源与动态请求的架构分离</strong>问题：</p>\n<ol>\n<li><strong>成本问题</strong>：多地部署完整服务需要部署多套应用、数据库、中间件，成本极高；而 CDN 只需存储静态资源，成本可控。</li>\n<li><strong>资源特性不同</strong>：静态资源（图片、JS、CSS）具有<strong>体积大、访问频繁、内容不变</strong>的特点，非常适合缓存分发；动态请求需要实时计算，必须回源处理。</li>\n<li><strong>系统资源消耗</strong>：如果用应用服务器直接处理静态资源请求，会大量占用 CPU、内存和带宽资源，可能影响核心业务的正常运行。</li>\n<li><strong>专业优化</strong>：CDN 针对静态资源传输进行了大量优化（如智能压缩、协议优化、边缘计算），这些能力是普通应用服务器不具备的。</li>\n</ol>\n<blockquote>\n<p><strong>注意</strong>：同一个服务在多个不同地方部署多份（比如同城灾备、异地灾备、同城多活、异地多活）是为了实现系统的<strong>高可用</strong>，而不是就近访问。</p>\n</blockquote>\n<h2>CDN 工作原理是什么？</h2>\n<p>理解 CDN 的工作原理，需要搞懂以下三个核心问题：</p>\n<ol>\n<li>静态资源是如何被缓存到 CDN 节点中的？</li>\n<li>如何找到最合适的 CDN 节点？</li>\n<li>如何防止静态资源被盗用？</li>\n</ol>\n<h3>静态资源是如何被缓存到 CDN 节点中的？</h3>\n<p>CDN 缓存静态资源的方式主要有两种：<strong>预热</strong>和<strong>回源</strong>。</p>\n<ul>\n<li>\n<p><strong>预热（Prefetch）</strong>：主动将源站的资源推送到 CDN 节点中。这样用户首次请求资源时可以直接从 CDN 节点获取，无需回源，适用于大促活动、热点内容发布等场景。</p>\n</li>\n<li>\n<p><strong>回源（Origin Pull）</strong>：当 CDN 节点上没有用户请求的资源或该资源的缓存已过期时，CDN 节点需要从源站获取最新的资源内容。</p>\n</li>\n</ul>\n<blockquote>\n<p><strong>注意</strong>：当用户请求触发回源时，该请求的响应速度会比未使用 CDN 还慢，因为相比于直接访问源站，多了一层 CDN 节点的调用流程。因此，提高<strong>缓存命中率</strong>是 CDN 优化的关键目标。</p>\n</blockquote>\n<p>CDN 缓存的完整生命周期如下图所示：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-full-life-cycle-of-cdn-cache.png\" alt=\"CDN 缓存的完整生命周期\"></p>\n<p>如果资源有更新，可以对其进行<strong>刷新</strong>操作，删除 CDN 节点上缓存的旧资源，并强制 CDN 节点在下次请求时回源获取最新资源。</p>\n<p>几乎所有云厂商提供的 CDN 服务都具备缓存的刷新和预热功能（下图是阿里云 CDN 服务提供的相应功能）：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-refresh-warm-up.png\" alt=\"CDN 缓存的刷新和预热\"></p>\n<p><strong>命中率</strong>和<strong>回源率</strong>是衡量 CDN 服务质量的两个核心指标：</p>\n<ul>\n<li><strong>命中率</strong>：用户请求直接由 CDN 节点响应的比例，<strong>越高越好</strong>。</li>\n<li><strong>回源率</strong>：用户请求需要回源站获取的比例，<strong>越低越好</strong>。</li>\n</ul>\n<h3>如何找到最合适的 CDN 节点？</h3>\n<p><strong>GSLB（Global Server Load Balance，全局负载均衡）</strong> 是 CDN 的大脑，负责多个 CDN 节点之间的协调调度，最常用的实现方式是<strong>基于 DNS 的 GSLB</strong>。</p>\n<p>CDN 请求的完整调度流程如下图所示：</p>\n<p><strong>详细流程说明</strong>：</p>\n<ol>\n<li>用户浏览器向本地 DNS 服务器发送域名解析请求。</li>\n<li>本地 DNS 向权威 DNS 查询，发现该域名配置了 <strong>CNAME（Canonical Name）别名记录</strong>，指向 CDN 服务商的域名。</li>\n<li>本地 DNS 继续向 CDN 的 <strong>GSLB</strong> 发起解析请求。</li>\n<li>GSLB 根据<strong>用户 IP 地址、CDN 节点状态（负载、性能、响应时间、带宽）</strong> 等指标，综合判断并返回最优 CDN 节点的 IP 地址。</li>\n<li>用户浏览器直接向该 CDN 节点（边缘服务器）发起资源请求。</li>\n<li>CDN 节点检查本地缓存，若命中则直接返回；若未命中或已过期，则回源获取后再返回给用户。</li>\n</ol>\n<blockquote>\n<p><strong>补充说明</strong>：上图做了一定简化。实际上，GSLB 内部可以看作是 <strong>CDN 专用 DNS 服务器</strong>和<strong>负载均衡系统</strong>的组合。CDN 专用 DNS 服务器会返回负载均衡系统的 IP 地址，浏览器通过该 IP 请求负载均衡系统，进而找到对应的 CDN 节点。</p>\n</blockquote>\n<h3>如何防止资源被盗刷？</h3>\n<p>如果静态资源被其他用户或网站非法盗刷，将会产生大量额外的带宽费用。常见的防盗链机制有以下几种：</p>\n<p>| 防盗链机制         | 原理                                          | 安全强度 | 实现成本 | 绕过难度                   |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/high-performance/cdn/jingdong-wuliu-cangpei.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高性能"
      ]
    },
    {
      "title": "数据冷热分离详解",
      "url": "https://codingmap.strova.top/high-performance/data-cold-hot-separation.html",
      "id": "https://codingmap.strova.top/high-performance/data-cold-hot-separation.html",
      "summary": "本文详解数据冷热分离的核心原理与实践方案，涵盖冷热数据判定策略、多级分层设计、数据迁移一致性保障、冷数据查询优化、存储选型（HBase/TiDB/对象存储），以及订单/日志/内容系统的典型落地案例。重点围绕 什么是数据冷热分离？、冷数据迁移、冷数据如何存储？、冷数据如何查询？ 等内容展开。结合 CodingMap 知。",
      "content_html": "<h2>什么是数据冷热分离？</h2>\n<p>数据冷热分离是指根据数据的<strong>访问频率</strong>和<strong>业务重要性</strong>，将数据划分为冷数据和热数据，并分别存储在不同性能和成本的存储介质中的架构策略。</p>\n<p>这种架构的核心目标有三个：</p>\n<ol>\n<li><strong>提升查询性能</strong>：热数据存储在高性能介质（如 SSD、内存）中，保障核心业务的响应速度。</li>\n<li><strong>降低存储成本</strong>：冷数据迁移至低成本介质（如 HDD、对象存储），大幅削减存储开支。</li>\n<li><strong>满足合规要求</strong>：部分行业（如金融、医疗）要求数据长期归档，冷热分离可兼顾合规与成本。</li>\n</ol>\n<h3>冷数据和热数据</h3>\n<p><strong>热数据</strong>是指被频繁访问和修改、且需要快速响应的数据；<strong>冷数据</strong>是指访问频率极低、对当前业务价值较小、但需要长期保留的数据。</p>\n<p>冷热数据的区分方法主要有两种：</p>\n<ol>\n<li><strong>时间维度区分</strong>：按照数据的创建时间、更新时间或过期时间划分。例如，订单系统将一段时间前（如 90 天或 1 年）的订单数据标记为冷数据。该方法适用于<strong>数据访问频率与时间强相关</strong>的场景，实现简单、成本低。</li>\n<li><strong>访问频率区分</strong>：将高频访问的数据视为热数据，低频访问的数据视为冷数据。例如，内容系统将<strong>浏览量低于阈值</strong>的文章标记为冷数据。该方法需要额外记录访问频率，适用于<strong>访问频率与数据本身特性强相关</strong>的场景。</li>\n</ol>\n<p><strong>如何选择区分策略？</strong></p>\n<ul>\n<li>若业务数据天然具有时效性（如订单、日志、账单），优先选择<strong>时间维度</strong>，实现成本最低。</li>\n<li>若数据价值与时间无关（如文章、商品、用户画像），需结合<strong>访问频率</strong>进行判定。</li>\n<li>实际项目中，可将两者结合使用：以时间维度为主、访问频率为辅，覆盖更多业务场景。</li>\n</ul>\n<h3>冷热分离的多级分层策略</h3>\n<p>实际落地时，&quot;冷&quot;与&quot;热&quot;往往不是非此即彼的二分法，而是<strong>渐进式多级分层</strong>：</p>\n<p>| 层级         | 数据特性             | 判定规则示例                | 存储策略               |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/high-performance/data-cold-hot-separation.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高性能"
      ]
    },
    {
      "title": "深度分页介绍及优化建议",
      "url": "https://codingmap.strova.top/high-performance/deep-pagination-optimization.html",
      "id": "https://codingmap.strova.top/high-performance/deep-pagination-optimization.html",
      "summary": "深度分页是指查询偏移量过大导致性能下降的场景，本文详解深度分页产生的原因及四种优化方案：范围查询、子查询优化、INNER JOIN 延迟关联、覆盖索引，并分析各方案的适用场景与优缺点。重点围绕 什么是深度分页？怎么导致的？、深度分页优化建议、生产落地建议、总结 等内容展开。结合 CodingMap 知识体系梳理 高性。",
      "content_html": "<h2>什么是深度分页？怎么导致的？</h2>\n<p>查询偏移量过大的场景我们称为深度分页，这会导致查询性能较低，例如：</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"># MySQL 在无法利用索引的情况下跳过1000000条记录后，再获取10条记录</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SELECT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> * </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">FROM</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> t_order </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">ORDER BY</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> id </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">LIMIT</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1000000</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">10</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>当查询偏移量过大时，MySQL 的查询优化器可能会选择全表扫描而不是利用索引来优化查询。</p>\n<p><strong>深度分页变慢的根本原因</strong>在于 MySQL 的执行机制：对于 <code>LIMIT offset, N</code>，MySQL 并非直接跳到 <code>offset</code> 处，而是必须从头扫描 <code>offset + N</code> 条记录。如果查询依赖二级索引且不满足覆盖索引，这意味着 MySQL 需要对前 <code>offset</code> 条记录执行毫无意义的<strong>回表查询（产生海量的随机 I/O）</strong>，最后再将这些辛苦查出的数据丢弃。即便优化器最终因代价过高退化为全表扫描，顺序扫描百万行的成本依然巨大。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/mysql/deep-pagination-phenomenon.png\" alt=\"深度分页问题\"></p>\n<p>不同机器上这个查询偏移量过大的临界点可能不同，取决于多个因素，包括硬件配置（如 CPU 性能、磁盘速度）、表的大小、索引的类型和统计信息等。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/mysql/deep-pagination-phenomenon-critical-point.png\" alt=\"转全表扫描的临界点\"></p>\n<p>MySQL 的查询优化器采用基于成本的策略来选择最优的查询执行计划。它会根据 CPU 和 I/O 的成本来决定是否使用索引扫描或全表扫描。如果优化器认为全表扫描的成本更低，它就会放弃使用索引。不过，即使偏移量很大，如果查询中使用了覆盖索引（covering index），MySQL 仍然可能会使用索引，避免回表操作。</p>\n<h2>深度分页优化建议</h2>\n<blockquote>\n<p><strong>本文基于 MySQL 8.0 + InnoDB 存储引擎</strong>，不同版本优化器行为可能存在差异。</p>\n</blockquote>\n<h3>范围查询（游标分页）</h3>\n<p>通过记录上一页最后一条记录的 ID，使用 <code>WHERE id &gt; last_id LIMIT n</code> 获取下一页数据：</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"># 通过记录上次查询结果的最后一条记录的 ID 进行下一页的查询</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SELECT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> * </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">FROM</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> t_order </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">WHERE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> id </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">></span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 100000</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> ORDER BY</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> id </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">LIMIT</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 10</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><strong>游标分页的核心优势</strong>：<strong>不依赖 ID 的连续性</strong>。MySQL 只需要在 B+ 树上定位到 <code>last_id</code> 的位置，然后顺序向后读取 <code>n</code> 条记录即可，中间是否有断层（如 ID 被删除）完全不影响结果的准确性和性能。</p>\n<p>这种方式的限制：</p>\n<ol>\n<li><strong>不支持跳页</strong>：无法直接跳转到第 N 页，只能逐页向后（或向前）翻页。</li>\n<li><strong>排序字段受限</strong>：如果查询需要按照其他字段（如创建时间）排序而非 ID 排序，需使用联合游标 <code>(sort_field, id)</code> 保证唯一性和顺序。</li>\n<li><strong>并发场景</strong>：当分页查询期间有新数据插入或删除时，可能出现：\n<ul>\n<li><strong>数据遗漏</strong>：查询第二页时，有新数据插入到第一页范围内，导致该数据被&quot;挤&quot;到第二页，但第二页查询已基于旧的最后 ID 跳过它。</li>\n<li><strong>数据重复</strong>：查询第二页时，第一页末尾有数据被删除，原第二页的第一条数据&quot;升&quot;到第一页末尾，导致第二页查询再次返回它。</li>\n</ul>\n</li>\n</ol>\n<h3>子查询</h3>\n<p>我们先查询出 limit 第一个参数对应的主键值，再根据这个主键值再去过滤并 limit，这样效率会更快一些。</p>\n<p>阿里巴巴《Java 开发手册》中也有对应的描述：</p>\n<blockquote>\n<p>利用延迟关联或者子查询优化超多分页场景。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/mysql/alibaba-java-development-handbook-paging.png\" alt></p>\n</blockquote>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">-- 先通过子查询在主键索引上进行偏移，快速找到起始ID</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SELECT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> * </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">FROM</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> t_order</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">WHERE</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> id </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">>=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    SELECT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> id </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">FROM</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> t_order </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">ORDER BY</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> id </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">LIMIT</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1000000</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">ORDER BY</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> id </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">LIMIT</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 10</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><strong>工作原理</strong>:</p>\n<ol>\n<li>子查询 <code>(SELECT id FROM t_order ORDER BY id LIMIT 1000000, 1)</code> 利用主键索引扫描并跳过前 1000000 条记录，返回第 1000001 条记录的主键值。</li>\n<li>主查询 <code>SELECT * FROM t_order WHERE id &gt;= ... ORDER BY id LIMIT 10</code> 以该主键为起点，获取后续 10 条完整记录。</li>\n</ol>\n<p>不过，某些情况下子查询可能会产生临时表，影响性能，因此在复杂查询中建议优先考虑延迟关联。</p>\n<blockquote>\n<p><strong>复杂过滤场景</strong>：在包含复杂过滤条件的分页场景中（如 <code>WHERE status = 1 ORDER BY id LIMIT 1000000, 10</code>），符合条件的 ID 往往是离散的。此时子查询的优势更加明显：通过在子查询中利用联合索引（如 <code>(status, id)</code>）实现覆盖索引扫描，可以高效地跳过前 100 万条符合条件的记录，定位到目标 ID 后，主查询只需回表 10 次。</p>\n</blockquote>\n<p>当然，我们也可以利用子查询先去获取目标分页的 ID 集合，然后再根据 ID 集合获取内容，但这种写法非常繁琐，不如使用 INNER JOIN 延迟关联。</p>\n<h3>延迟关联</h3>\n<p>延迟关联与子查询的优化思路类似，都是通过将 <code>LIMIT</code> 操作转移到主键索引树上，减少回表次数。相比直接使用子查询，延迟关联通过 <code>INNER JOIN</code> 将子查询结果集成到主查询中，避免了子查询可能产生的临时表。在执行 <code>INNER JOIN</code> 时，MySQL 优化器能够利用索引进行高效的连接操作（如索引扫描或其他优化策略），因此在深度分页场景下，性能通常优于直接使用子查询。</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">-- 使用 INNER JOIN 进行延迟关联</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SELECT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> t1.*</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">FROM</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> t_order t1</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">INNER JOIN</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    -- 这里的子查询可以利用覆盖索引，性能极高</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    SELECT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> id </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">FROM</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> t_order </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">ORDER BY</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> id </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">LIMIT</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1000000</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">10</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) t2 </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">ON</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> t1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">id</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> t2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">id</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><strong>工作原理</strong>:</p>\n<ol>\n<li>子查询 <code>(SELECT id FROM t_order ORDER BY id LIMIT 1000000, 10)</code> 利用主键索引扫描并跳过前 1000000 条记录，返回目标分页的 10 条记录的 ID。</li>\n<li>通过 <code>INNER JOIN</code> 将子查询结果与主表 <code>t_order</code> 关联，获取完整的记录数据。</li>\n</ol>\n<p>除了使用 INNER JOIN 之外，还可以使用逗号连接子查询。</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">-- 使用逗号进行延迟关联</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SELECT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> t1.* </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">FROM</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> t_order t1,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SELECT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> id </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">FROM</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> t_order </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">ORDER BY</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> id </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">LIMIT</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1000000</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">10</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) t2</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">WHERE</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> t1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">id</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> t2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">id</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><strong>注意</strong>: 虽然逗号连接子查询也能实现类似的效果，但为了代码可读性和可维护性，建议使用更规范的 <code>INNER JOIN</code> 语法。</p>\n<h3>覆盖索引</h3>\n<p>索引中已经包含了所有需要获取的字段的查询方式称为覆盖索引。</p>\n<p><strong>覆盖索引的好处：</strong></p>\n<ul>\n<li><strong>避免 InnoDB 表进行索引的二次查询，也就是回表操作</strong>：InnoDB 是以聚集索引的顺序来存储的，对于 InnoDB 来说，二级索引在叶子节点中所保存的是行的主键信息，如果是用二级索引查询数据的话，在查找到相应的键值后，还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中，二级索引的键值中可以获取所有的数据，避免了对主键的二次查询（回表），减少了 IO 操作，提升了查询效率。</li>\n<li><strong>减少回表带来的随机 IO</strong>：通过覆盖索引直接返回数据，避免了根据二级索引的主键值回表查询聚簇索引的随机 IO 操作。回表时每次按主键值查找聚簇索引，本质上是随机 IO。</li>\n</ul>\n<p>假设建立了 <code>(code, type)</code> 联合索引，下面的查询即可使用覆盖索引：</p>\n<div class=\"language-sql line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"sql\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-sql\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"># 在 InnoDB 中，辅助索引天然包含主键 id</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"># 如果只需要查询 id, code, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">type</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> 这三列，只需建立 (code, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">type</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) 的联合索引即可实现覆盖</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">SELECT</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> id, code, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">type</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> FROM</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> t_order</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">ORDER BY</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> code</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">LIMIT</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1000000</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">10</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><strong>⚠️注意</strong>:</p>\n<ul>\n<li>当查询的结果集占表的总行数的很大一部分时，MySQL 查询优化器可能选择放弃使用索引，自动转换为全表扫描。</li>\n<li>虽然可以使用 <code>FORCE INDEX</code> 强制查询优化器走索引，但这种方式可能会导致查询优化器无法选择更优的执行计划，效果并不总是理想。</li>\n</ul>\n<h2>生产落地建议</h2>\n<h3>监控与告警</h3>\n<ul>\n<li><strong>慢查询监控</strong>：监控慢查询日志中 <code>LIMIT</code> 偏移量过大的 SQL，及时发现问题。</li>\n<li><strong>阈值告警</strong>：设置 <code>long_query_time</code> 阈值捕获深度分页查询。</li>\n<li><strong>执行计划检查</strong>：使用 <code>EXPLAIN</code> 定期检查关键分页 SQL 的执行计划，确保优化器按预期使用索引。</li>\n</ul>\n<h3>常见误区</h3>\n<p>| 误区                              | 事实                                                 |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/mysql/deep-pagination-phenomenon.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高性能"
      ]
    },
    {
      "title": "高性能系统设计面试题总结：缓存、读写分离、分库分表、负载均衡、消息队列",
      "url": "https://codingmap.strova.top/high-performance/high-performance-interview-questions.html",
      "id": "https://codingmap.strova.top/high-performance/high-performance-interview-questions.html",
      "summary": "高性能系统面试题和复习路线汇总，覆盖 CDN、负载均衡、数据库优化、读写分离、分库分表、冷热分离、深度分页、SQL 优化、消息队列、Kafka、RocketMQ、RabbitMQ 等高频考点。重点围绕 高性能系统设计面试怎么准备？、第一阶段：CDN 和负载均衡、第二阶段：数据库性能优化、第三阶段：消息队列与削峰等核心。",
      "content_html": "<p>高性能系统面试不是问你会不会背几个优化手段，而是看你能不能把请求链路拆开：<strong>用户请求进来后，哪里可能慢，哪里可能扛不住，哪里需要削峰，哪里需要减少数据库压力，哪里需要用监控和压测验证效果</strong>。</p>\n<p>这篇文章把 JavaGuide 现有高性能相关文章串成一条面试复习路线，适合准备后端开发、系统设计和架构设计相关面试。</p>\n<h2>高性能系统设计面试怎么准备？</h2>\n<p>高性能问题通常可以按请求链路拆成 4 层：</p>\n<ol>\n<li><strong>入口层</strong>：CDN、负载均衡、网关。</li>\n<li><strong>应用层</strong>：限流、异步化、线程池、批处理。</li>\n<li><strong>数据层</strong>：索引、SQL 优化、读写分离、分库分表、冷热分离。</li>\n<li><strong>削峰层</strong>：消息队列、延迟处理、重试和补偿。</li>\n</ol>\n<p>面试回答时，不要一上来就说“加缓存”。更好的方式是先确认场景和指标：QPS 多大、RT 和 P99 要求是多少、读写比例如何、数据量级多大、瓶颈出现在 CPU、I/O、数据库还是外部依赖。只有先判断瓶颈在哪里，后面的方案才不会像硬套模板。</p>\n<h2>第一阶段：CDN 和负载均衡</h2>\n<p>入口层优化解决的是“流量怎么更快、更稳地进来”。这一层通常不直接改业务代码，但会明显影响用户访问延迟、源站压力和系统可用性。</p>\n<p>重点文章：</p>\n<ul>\n<li><a href=\"https://javaguide.cn/high-performance/cdn.html\" target=\"_blank\" rel=\"noopener noreferrer\">CDN 工作原理详解</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/load-balancing.html\" target=\"_blank\" rel=\"noopener noreferrer\">负载均衡原理及算法详解</a></li>\n</ul>\n<p>高频面试问题：</p>\n<ul>\n<li>CDN 为什么能加速静态资源访问？回源是什么意思？</li>\n<li>浏览器缓存、CDN 缓存、源站缓存分别解决什么问题？</li>\n<li>为什么 HTML 不适合长期缓存，而 hash JS/CSS 可以长期缓存？</li>\n<li>四层负载均衡和七层负载均衡有什么区别？</li>\n<li>轮询、加权轮询、最少连接、一致性哈希分别适合什么场景？</li>\n</ul>\n<p>准备这一部分时，可以把一次页面访问拆成：DNS 解析、CDN 命中、回源、浏览器缓存、静态资源加载。再补上“命中率下降怎么办”“某个机房故障如何摘流量”“负载均衡算法为什么会影响后端压力分布”这类追问，答案会更像真实项目经验。</p>\n<h2>第二阶段：数据库性能优化</h2>\n<p>数据库是后端系统最常见的性能瓶颈，也是面试最爱追问的地方。很多高性能方案最终都会落到一个问题上：如何在数据规模变大之后，继续把查询、写入和存储成本控制住。</p>\n<p>重点文章：</p>\n<ul>\n<li><a href=\"https://javaguide.cn/high-performance/read-and-write-separation-and-library-subtable.html\" target=\"_blank\" rel=\"noopener noreferrer\">读写分离和分库分表详解</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/data-cold-hot-separation.html\" target=\"_blank\" rel=\"noopener noreferrer\">数据冷热分离详解</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/sql-optimization.html\" target=\"_blank\" rel=\"noopener noreferrer\">常见 SQL 优化手段总结</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/deep-pagination-optimization.html\" target=\"_blank\" rel=\"noopener noreferrer\">深度分页介绍及优化建议</a></li>\n</ul>\n<p>高频面试问题：</p>\n<ul>\n<li>读写分离能解决什么问题？主从延迟怎么处理？</li>\n<li>分库分表解决什么问题？会带来哪些复杂度？</li>\n<li>分片键怎么选？范围分片和哈希分片有什么区别？</li>\n<li>深度分页为什么慢？游标分页、延迟关联、覆盖索引分别怎么优化？</li>\n<li>冷热数据如何划分？归档后如何保证查询体验？</li>\n<li>SQL 优化时，如何结合执行计划判断索引是否生效？</li>\n</ul>\n<p>数据库优化不要只记方案名。面试官通常会继续问：<strong>数据量多大、查询模式是什么、写入压力如何、是否能接受最终一致、迁移成本怎么控制</strong>。这些边界决定方案是否靠谱。</p>\n<p>这一阶段建议重点准备 3 类回答：慢 SQL 如何定位和优化、单库压力太大如何拆、历史数据太多如何迁移和归档。能把这 3 类问题讲清楚，基本就覆盖了大部分数据库性能追问。</p>\n<h2>第三阶段：消息队列与削峰</h2>\n<p>消息队列的核心价值不是“解耦”两个字，而是让系统在高峰流量下有缓冲、有重试、有异步处理能力。它适合处理“写入突增、下游处理慢、多个系统需要协作”的场景，但也会引入一致性、重复消费和运维复杂度。</p>\n<p>重点文章：</p>\n<ul>\n<li><a href=\"https://javaguide.cn/high-performance/message-queue/message-queue.html\" target=\"_blank\" rel=\"noopener noreferrer\">消息队列基础常见问题总结</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/message-queue/kafka-questions-01.html\" target=\"_blank\" rel=\"noopener noreferrer\">Kafka 常见面试题总结</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/message-queue/rocketmq-questions.html\" target=\"_blank\" rel=\"noopener noreferrer\">RocketMQ 常见面试题总结</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/message-queue/rabbitmq-questions.html\" target=\"_blank\" rel=\"noopener noreferrer\">RabbitMQ 常见面试题总结</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/message-queue/disruptor-questions.html\" target=\"_blank\" rel=\"noopener noreferrer\">Disruptor 常见面试题总结</a></li>\n</ul>\n<p>高频面试问题：</p>\n<ul>\n<li>消息队列有哪些典型使用场景？异步、削峰、解耦分别怎么理解？</li>\n<li>消息丢失、重复消费、顺序消费分别怎么解决？</li>\n<li>Kafka 为什么吞吐高？它的分区、副本、ISR 分别解决什么问题？</li>\n<li>RocketMQ 的事务消息解决什么问题？</li>\n<li>RabbitMQ 的交换机、队列、路由键分别是什么？</li>\n<li>MQ 积压了怎么办？消费者扩容一定有用吗？</li>\n</ul>\n<p>这一部分最重要的是讲清楚“可靠性”。生产环境里，消息系统真正难的不是发出去，而是失败、重复、乱序、积压之后系统还能恢复。</p>\n<p>复习时不要只比较 Kafka、RocketMQ、RabbitMQ 的概念差异，还要能结合场景说明选择依据：日志采集更看重吞吐，交易链路更看重可靠性和事务语义，普通业务异步化更看重接入成本和运维复杂度。</p>\n<h2>第四阶段：把高性能方案放回系统设计题</h2>\n<p>面试里的高性能问题经常以系统设计题出现，比如：</p>\n<ul>\n<li>如何设计一个秒杀系统？</li>\n<li>如何优化一个慢查询很多的订单系统？</li>\n<li>如何支撑首页高并发访问？</li>\n<li>如何处理消息积压？</li>\n<li>如何设计高吞吐日志采集链路？</li>\n</ul>\n<p>回答这类问题时，建议按这个顺序展开：</p>\n<ol>\n<li><strong>先确认业务目标、读写比例、峰值流量、数据规模和一致性要求</strong>。</li>\n<li><strong>入口层用 CDN、负载均衡、网关承接流量</strong>。</li>\n<li><strong>应用层用缓存、限流、异步化削峰</strong>。</li>\n<li><strong>数据层用索引、读写分离、分库分表、冷热分离降低数据库压力</strong>。</li>\n<li><strong>消息层用 MQ 做削峰、解耦、重试和补偿</strong>。</li>\n<li><strong>最后补监控、压测、降级、容量预估和成本评估</strong>。</li>\n</ol>\n<p>这样回答不会显得像在背八股，而是在做真实系统设计。尤其是社招和中高级岗位，面试官更关注你是否能讲清楚取舍：为什么用这个方案，不用它会发生什么，它会引入什么新问题，后续如何验证和治理。</p>\n<h2>推荐复习顺序</h2>\n<p>临近面试可以按“先数据层，再消息队列，最后入口层”的顺序复习。原因很简单：数据库优化和消息队列最容易被追问工程细节，入口层更适合放在最后补齐系统设计链路。</p>\n<ol>\n<li>先看 <a href=\"https://javaguide.cn/high-performance/sql-optimization.html\" target=\"_blank\" rel=\"noopener noreferrer\">常见 SQL 优化手段总结</a> 和 <a href=\"https://javaguide.cn/high-performance/deep-pagination-optimization.html\" target=\"_blank\" rel=\"noopener noreferrer\">深度分页介绍及优化建议</a>，掌握慢 SQL 定位、索引优化和分页优化这些高频问题。</li>\n<li>再看 <a href=\"https://javaguide.cn/high-performance/read-and-write-separation-and-library-subtable.html\" target=\"_blank\" rel=\"noopener noreferrer\">读写分离和分库分表详解</a>、<a href=\"https://javaguide.cn/high-performance/data-cold-hot-separation.html\" target=\"_blank\" rel=\"noopener noreferrer\">数据冷热分离详解</a>，重点理解数据量继续变大后，系统如何拆库、拆表、归档和迁移。</li>\n<li>然后补 <a href=\"https://javaguide.cn/high-performance/message-queue/message-queue.html\" target=\"_blank\" rel=\"noopener noreferrer\">消息队列基础常见问题总结</a>、<a href=\"https://javaguide.cn/high-performance/message-queue/kafka-questions-01.html\" target=\"_blank\" rel=\"noopener noreferrer\">Kafka 常见面试题总结</a>、<a href=\"https://javaguide.cn/high-performance/message-queue/rocketmq-questions.html\" target=\"_blank\" rel=\"noopener noreferrer\">RocketMQ 常见面试题总结</a>，把异步、削峰、可靠投递、重复消费和消息积压准备扎实。</li>\n<li>最后看 <a href=\"https://javaguide.cn/high-performance/cdn.html\" target=\"_blank\" rel=\"noopener noreferrer\">CDN 工作原理详解</a> 和 <a href=\"https://javaguide.cn/high-performance/load-balancing.html\" target=\"_blank\" rel=\"noopener noreferrer\">负载均衡原理及算法详解</a>，把入口层链路补齐，能够从用户访问一路讲到后端服务承接流量。</li>\n</ol>\n<p>如果你能把这些内容串成一条完整链路：<strong>入口流量如何进来、应用如何削峰、数据库如何减压、消息系统如何兜底、系统效果如何验证</strong>，高性能系统设计题就会好答很多。</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高性能"
      ]
    },
    {
      "title": "高性能系统常见面试题总结",
      "url": "https://codingmap.strova.top/high-performance/high-performance-system-interview-questions.html",
      "id": "https://codingmap.strova.top/high-performance/high-performance-system-interview-questions.html",
      "summary": "高性能系统常见面试题总结：涵盖 CDN、负载均衡、读写分离、分库分表、SQL 优化、深度分页、冷热分离、消息队列、Kafka、RocketMQ、RabbitMQ 等核心知识点。重点围绕 高性能基础、数据库性能优化、消息队列、系统设计综合题、高性能 等内容展开。结合 CodingMap 知识体系梳理 高性能 / Hig。",
      "content_html": "<p>这部分内容摘自 <a href=\"https://javaguide.cn/\" target=\"_blank\" rel=\"noopener noreferrer\">JavaGuide</a> 下面几篇文章的重点：</p>\n<p>高性能基础：</p>\n<ul>\n<li><a href=\"https://javaguide.cn/high-performance/cdn.html\" target=\"_blank\" rel=\"noopener noreferrer\">CDN 工作原理详解</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/load-balancing.html\" target=\"_blank\" rel=\"noopener noreferrer\">负载均衡原理及算法详解</a></li>\n</ul>\n<p>数据库性能优化：</p>\n<ul>\n<li><a href=\"https://javaguide.cn/high-performance/read-and-write-separation-and-library-subtable.html\" target=\"_blank\" rel=\"noopener noreferrer\">读写分离和分库分表详解</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/data-cold-hot-separation.html\" target=\"_blank\" rel=\"noopener noreferrer\">数据冷热分离详解</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/sql-optimization.html\" target=\"_blank\" rel=\"noopener noreferrer\">常见 SQL 优化手段总结</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/deep-pagination-optimization.html\" target=\"_blank\" rel=\"noopener noreferrer\">深度分页介绍及优化建议</a></li>\n</ul>\n<p>消息队列：</p>\n<ul>\n<li><a href=\"https://javaguide.cn/high-performance/message-queue/message-queue.html\" target=\"_blank\" rel=\"noopener noreferrer\">消息队列基础常见问题总结</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/message-queue/kafka-questions-01.html\" target=\"_blank\" rel=\"noopener noreferrer\">Kafka 常见面试题总结</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/message-queue/rocketmq-questions.html\" target=\"_blank\" rel=\"noopener noreferrer\">RocketMQ 常见面试题总结</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/message-queue/rabbitmq-questions.html\" target=\"_blank\" rel=\"noopener noreferrer\">RabbitMQ 常见面试题总结</a></li>\n<li><a href=\"https://javaguide.cn/high-performance/message-queue/disruptor-questions.html\" target=\"_blank\" rel=\"noopener noreferrer\">Disruptor 常见面试题总结</a></li>\n</ul>\n<h2>高性能基础</h2>\n<h3>⭐️什么是 CDN？为什么 CDN 能提升访问速度？</h3>\n<p>CDN（Content Delivery Network，内容分发网络）本质上是把静态资源缓存到离用户更近的边缘节点，用户访问资源时不一定回源站，而是优先从附近 CDN 节点获取。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-101.png\" alt=\"CDN 简易示意图\"></p>\n<p>它能提升访问速度，主要靠两点：</p>\n<ol>\n<li><strong>距离更近</strong>：用户不需要跨地域访问源站，网络时延更低。</li>\n<li><strong>缓存命中</strong>：静态资源在 CDN 节点命中后，可以直接返回，源站压力也会下降。</li>\n</ol>\n<p>典型适合 CDN 缓存的内容包括图片、CSS、JS、字体、下载文件等。HTML 是否缓存要谨慎，尤其是 VuePress/Vite 这类带 hash 静态资源的站点，HTML 长期缓存可能引用到已经不存在的旧资源。</p>\n<p>面试里如果继续追问“CDN 为什么一定能更快”，可以从 <strong>就近接入、边缘缓存、跨运营商优化、源站减压</strong> 这 4 个角度回答。它不是让源站本身变快，而是把大量重复请求拦在离用户更近的位置。</p>\n<h3>CDN 回源是什么意思？</h3>\n<p>当 CDN 节点没有缓存用户请求的资源，或者缓存过期时，CDN 节点会向源站请求资源，这个过程叫 <strong>回源</strong>。</p>\n<p>回源后，CDN 会根据缓存规则决定是否把资源缓存到边缘节点。后续用户再访问同一个资源时，如果缓存命中，就不需要再访问源站。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-full-life-cycle-of-cdn-cache.png\" alt=\"CDN 缓存的完整生命周期\"></p>\n<p>面试里可以这样总结：</p>\n<ul>\n<li><strong>命中缓存</strong>：用户 -&gt; CDN 节点 -&gt; 用户。</li>\n<li><strong>未命中缓存</strong>：用户 -&gt; CDN 节点 -&gt; 源站 -&gt; CDN 节点 -&gt; 用户。</li>\n</ul>\n<p>回源并不是坏事，但回源量过大通常说明缓存策略、资源 URL 设计或刷新预热策略有问题。比如资源没有设置合理 <code>Cache-Control</code>、静态资源文件名没有 hash、部署后没有预热热点资源，都可能让 CDN 边缘节点频繁回源。</p>\n<h3>⭐️HTML、JS、CSS、图片的缓存策略有什么区别？</h3>\n<p>推荐策略如下：</p>\n<p>| 资源类型    | 推荐缓存策略                         | 原因                                      |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-101.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高性能"
      ]
    },
    {
      "title": "负载均衡原理及算法详解",
      "url": "https://codingmap.strova.top/high-performance/load-balancing.html",
      "id": "https://codingmap.strova.top/high-performance/load-balancing.html",
      "summary": "本文详解负载均衡的核心原理，涵盖四层/七层负载均衡区别、服务端与客户端负载均衡对比，深入讲解轮询、加权轮询、随机、一致性哈希等常见负载均衡算法，以及 Nginx、LVS 等主流实现方案。重点围绕 什么是负载均衡？、负载均衡分为哪几种？、负载均衡常见的算法有哪些？、七层负载均衡可以怎么做？ 等内容展开。",
      "content_html": "<h2>什么是负载均衡？</h2>\n<p><strong>负载均衡</strong> 指的是将用户请求分摊到不同的服务器上处理，以提高系统整体的并发处理能力以及可靠性。负载均衡服务可以有由专门的软件或者硬件来完成，一般情况下，硬件的性能更好，软件的价格更便宜（后文会详细介绍到）。</p>\n<p>下图是<a href=\"https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&amp;mid=2247519384&amp;idx=1&amp;sn=bc7e71af75350b755f04ca4178395b1a&amp;chksm=cea1c353f9d64a458f797696d4144b4d6e58639371a4612b8e4d106d83a66d2289e7b2cd7431&amp;token=660789642&amp;lang=zh_CN&amp;scene=21#wechat_redirect\" target=\"_blank\" rel=\"noopener noreferrer\">《Java 面试指北》</a> 「高并发篇」中的一篇文章的配图，从图中可以看出，系统的商品服务部署了多份在不同的服务器上，为了实现访问商品服务请求的分流，我们用到了负载均衡。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/multi-service-load-balancing.drawio.png\" alt=\"多服务实例-负载均衡\"></p>\n<p>负载均衡是一种比较常用且实施起来较为简单的提高系统并发能力和可靠性的手段，不论是单体架构的系统还是微服务架构的系统几乎都会用到。</p>\n<h2>负载均衡分为哪几种？</h2>\n<p>负载均衡可以简单分为 <strong>服务端负载均衡</strong> 和 <strong>客户端负载均衡</strong> 这两种。</p>\n<p>服务端负载均衡涉及到的知识点更多，工作中遇到的也比较多，因此，我会花更多时间来介绍。</p>\n<h3>服务端负载均衡</h3>\n<p><strong>服务端负载均衡</strong> 主要应用在 <strong>系统外部请求</strong> 和 <strong>网关层</strong> 之间，可以使用 <strong>软件</strong> 或者 <strong>硬件</strong> 实现。</p>\n<p>下图是我画的一个简单的基于 Nginx 的服务端负载均衡示意图：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/server-load-balancing.png\" alt=\"基于 Nginx 的服务端负载均衡\"></p>\n<p><strong>硬件负载均衡</strong> 通过专门的硬件设备（比如 <strong>F5、A10、Array</strong> ）实现负载均衡功能。</p>\n<p>硬件负载均衡的优势是性能很强且稳定，缺点就是实在是太贵了。像基础款的 F5 最低也要 20 多万，绝大部分公司是根本负担不起的，业务量不大的话，真没必要非要去弄个硬件来做负载均衡，用软件负载均衡就足够了！</p>\n<p>在我们日常开发中，一般很难接触到硬件负载均衡，接触的比较多的还是 <strong>软件负载均衡</strong> 。软件负载均衡通过软件（比如 <strong>LVS、Nginx、HAproxy</strong> ）实现负载均衡功能，性能虽然差一些，但价格便宜啊！像基础款的 Linux 服务器也就几千，性能好一点的 2~3 万的就很不错了。</p>\n<p>根据 OSI 模型，服务端负载均衡还可以分为：</p>\n<ul>\n<li>二层负载均衡</li>\n<li>三层负载均衡</li>\n<li>四层负载均衡</li>\n<li>七层负载均衡</li>\n</ul>\n<p>最常见的是四层和七层负载均衡，因此，本文也是重点介绍这两种负载均衡。</p>\n<blockquote>\n<p>Nginx 官网对四层负载和七层负载均衡均衡做了详细介绍，感兴趣的可以看看。</p>\n<ul>\n<li><a href=\"https://www.nginx.com/resources/glossary/layer-4-load-balancing/\" target=\"_blank\" rel=\"noopener noreferrer\">What Is Layer 4 Load Balancing?</a></li>\n<li><a href=\"https://www.nginx.com/resources/glossary/layer-7-load-balancing/\" target=\"_blank\" rel=\"noopener noreferrer\">What Is Layer 7 Load Balancing?</a></li>\n</ul>\n</blockquote>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/cs-basics/network/osi-7-model.png\" alt=\"OSI 七层模型\"></p>\n<ul>\n<li><strong>四层负载均衡</strong> 工作在 OSI 模型第四层，也就是传输层，这一层的主要协议是 TCP/UDP，负载均衡器在这一层能够看到数据包里的源端口地址以及目的端口地址，会基于这些信息通过一定的负载均衡算法将数据包转发到后端真实服务器。也就是说，四层负载均衡的核心就是 IP+端口层面的负载均衡，不涉及具体的报文内容。</li>\n<li><strong>七层负载均衡</strong> 工作在 OSI 模型第七层，也就是应用层，这一层的主要协议是 HTTP 。这一层的负载均衡比四层负载均衡路由网络请求的方式更加复杂，它会读取报文的数据部分（比如说我们的 HTTP 部分的报文），然后根据读取到的数据内容（如 URL、Cookie）做出负载均衡决策。也就是说，七层负载均衡器的核心是报文内容（如 URL、Cookie）层面的负载均衡，执行第七层负载均衡的设备通常被称为 <strong>反向代理服务器</strong> 。</li>\n</ul>\n<p>七层负载均衡比四层负载均衡会消耗更多的性能，不过，也相对更加灵活，能够更加智能地路由网络请求，比如说你可以根据请求的内容进行优化如缓存、压缩、加密。</p>\n<p>简单来说，<strong>四层负载均衡性能很强，七层负载均衡功能更强！</strong> 不过，对于绝大部分业务场景来说，四层负载均衡和七层负载均衡的性能差异基本可以忽略不计的。</p>\n<p>下面这段话摘自 Nginx 官网的 <a href=\"https://www.nginx.com/resources/glossary/layer-4-load-balancing/\" target=\"_blank\" rel=\"noopener noreferrer\">What Is Layer 4 Load Balancing?</a> 这篇文章。</p>\n<blockquote>\n<p>Layer 4 load balancing was a popular architectural approach to traffic handling when commodity hardware was not as powerful as it is now, and the interaction between clients and application servers was much less complex. It requires less computation than more sophisticated load balancing methods (such as Layer 7), but CPU and memory are now sufficiently fast and cheap that the performance advantage for Layer 4 load balancing has become negligible or irrelevant in most situations.</p>\n<p>第 4 层负载平衡是一种流行的流量处理体系结构方法，当时商用硬件没有现在这么强大，客户端和应用程序服务器之间的交互也不那么复杂。它比更复杂的负载平衡方法(如第 7 层)需要更少的计算量，但是 CPU 和内存现在足够快和便宜，在大多数情况下，第 4 层负载平衡的性能优势已经变得微不足道或无关紧要。</p>\n</blockquote>\n<p>在工作中，我们通常会使用 <strong>Nginx</strong> 来做七层负载均衡，LVS(Linux Virtual Server 虚拟服务器， Linux 内核的 4 层负载均衡)来做四层负载均衡。</p>\n<p>关于 Nginx 的常见知识点总结，<a href=\"https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html\" target=\"_blank\" rel=\"noopener noreferrer\">《Java 面试指北》</a> 中「技术面试题篇」中已经有对应的内容了，感兴趣的小伙伴可以去看看。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/image-20220328105759300.png\" alt></p>\n<p>不过，LVS 这个绝大部分公司真用不上，像阿里、百度、腾讯、eBay 等大厂才会使用到，用的最多的还是 Nginx。</p>\n<h3>客户端负载均衡</h3>\n<p><strong>客户端负载均衡</strong> 主要应用于系统内部的不同的服务之间，可以使用现成的负载均衡组件来实现。</p>\n<p>在客户端负载均衡中，客户端会自己维护一份服务器的地址列表，发送请求之前，客户端会根据对应的负载均衡算法来选择具体某一台服务器处理请求。</p>\n<p>客户端负载均衡器和服务运行在同一个进程或者说 Java 程序里，不存在额外的网络开销。不过，客户端负载均衡的实现会受到编程语言的限制，比如说 Spring Cloud Load Balancer 就只能用于 Java 语言。</p>\n<p>Java 领域主流的微服务框架 Dubbo、Spring Cloud 等都内置了开箱即用的客户端负载均衡实现。Dubbo 属于是默认自带了负载均衡功能，Spring Cloud 是通过组件的形式实现的负载均衡，属于可选项，比较常用的是 Spring Cloud Load Balancer（官方，推荐） 和 Ribbon（Netflix，已被弃用）。</p>\n<p>下图是我画的一个简单的基于 Spring Cloud Load Balancer（Ribbon 也类似） 的客户端负载均衡示意图：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/spring-cloud-lb-gateway.png\" alt></p>\n<h2>负载均衡常见的算法有哪些？</h2>\n<h3>随机法</h3>\n<p><strong>随机法</strong> 是最简单粗暴的负载均衡算法。</p>\n<p>如果没有配置权重的话，所有的服务器被访问到的概率都是相同的。如果配置权重的话，权重越高的服务器被访问的概率就越大。</p>\n<p>未加权重的随机算法适合于服务器性能相近的集群，其中每个服务器承载相同的负载。加权随机算法适合于服务器性能不等的集群，权重的存在可以使请求分配更加合理化。</p>\n<p>不过，随机算法有一个比较明显的缺陷：部分机器在一段时间之内无法被随机到，毕竟是概率算法，就算是大家权重一样， 也可能会出现这种情况。</p>\n<p>于是，<strong>轮询法</strong> 来了！</p>\n<h3>轮询法</h3>\n<p>轮询法是挨个轮询服务器处理，也可以设置权重。</p>\n<p>如果没有配置权重的话，每个请求按时间顺序逐一分配到不同的服务器处理。如果配置权重的话，权重越高的服务器被访问的次数就越多。</p>\n<p>未加权重的轮询算法适合于服务器性能相近的集群，其中每个服务器承载相同的负载。加权轮询算法适合于服务器性能不等的集群，权重的存在可以使请求分配更加合理化。</p>\n<p>在加权轮询的基础上，还有进一步改进得到的负载均衡算法，比如平滑的加权轮训算法。</p>\n<p>平滑的加权轮训算法最早是在 Nginx 中被实现，可以参考这个 commit：<a href=\"https://github.com/phusion/nginx/commit/27e94984486058d73157038f7950a0a36ecc6e35\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/phusion/nginx/commit/27e94984486058d73157038f7950a0a36ecc6e35</a>。如果你认真学习过 Dubbo 负载均衡策略的话，就会发现 Dubbo 的加权轮询就借鉴了该算法实现并进一步做了优化。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/dubbo-round-robin-load-balance.png\" alt=\"Dubbo 加权轮询负载均衡算法\"></p>\n<h3>两次随机法</h3>\n<p>两次随机法在随机法的基础上多增加了一次随机，多选出一个服务器。随后再根据两台服务器的负载等情况，从其中选择出一个最合适的服务器。</p>\n<p>两次随机法的好处是可以动态地调节后端节点的负载，使其更加均衡。如果只使用一次随机法，可能会导致某些服务器过载，而某些服务器空闲。</p>\n<h3>哈希法</h3>\n<p>将请求的参数信息通过哈希函数转换成一个哈希值，然后根据哈希值来决定请求被哪一台服务器处理。</p>\n<p>在服务器数量不变的情况下，相同参数的请求总是发到同一台服务器处理，比如同个 IP 的请求、同一个用户的请求。</p>\n<h3>一致性 Hash 法</h3>\n<p>和哈希法类似，一致性 Hash 法也可以让相同参数的请求总是发到同一台服务器处理。不过，它解决了哈希法存在的一些问题。</p>\n<p>常规哈希法在服务器数量变化时，哈希值会重新落在不同的服务器上，这明显违背了使用哈希法的本意。而一致性哈希法的核心思想是将数据和节点都映射到一个哈希环上，然后根据哈希值的顺序来确定数据属于哪个节点。当服务器增加或删除时，只影响该服务器的哈希，而不会导致整个服务集群的哈希键值重新分布。</p>\n<h3>最小连接法</h3>\n<p>当有新的请求出现时，遍历服务器节点列表并选取其中连接数最小的一台服务器来响应当前请求。相同连接的情况下，可以进行加权随机。</p>\n<p>最少连接数基于一个服务器连接数越多，负载就越高这一理想假设。然而， 实际情况是连接数并不能代表服务器的实际负载，有些连接耗费系统资源更多，有些连接不怎么耗费系统资源。</p>\n<h3>最少活跃法</h3>\n<p>最少活跃法和最小连接法类似，但要更科学一些。最少活跃法以活动连接数为标准，活动连接数可以理解为当前正在处理的请求数。活跃数越低，说明处理能力越强，这样就可以使处理能力强的服务器处理更多请求。相同活跃数的情况下，可以进行加权随机。</p>\n<h3>最快响应时间法</h3>\n<p>不同于最小连接法和最少活跃法，最快响应时间法以响应时间为标准来选择具体是哪一台服务器处理。客户端会维持每个服务器的响应时间，每次请求挑选响应时间最短的。相同响应时间的情况下，可以进行加权随机。</p>\n<p>这种算法可以使得请求被更快处理，但可能会造成流量过于集中于高性能服务器的问题。</p>\n<h2>七层负载均衡可以怎么做？</h2>\n<p>简单介绍两种项目中常用的七层负载均衡解决方案：DNS 解析和反向代理。</p>\n<p>除了我介绍的这两种解决方案之外，HTTP 重定向等手段也可以用来实现负载均衡，不过，相对来说，还是 DNS 解析和反向代理用的更多一些，也更推荐一些。</p>\n<h3>DNS 解析</h3>\n<p>DNS 解析是比较早期的七层负载均衡实现方式，非常简单。</p>\n<p>DNS 解析实现负载均衡的原理是这样的：在 DNS 服务器中为同一个主机记录配置多个 IP 地址，这些 IP 地址对应不同的服务器。当用户请求域名的时候，DNS 服务器采用轮询算法返回 IP 地址，这样就实现了轮询版负载均衡。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/6997605302452f07e8b28d257d349bf0.png\" alt></p>\n<p>现在的 DNS 解析几乎都支持 IP 地址的权重配置，这样的话，在服务器性能不等的集群中请求分配会更加合理化。像我自己目前正在用的阿里云 DNS 就支持权重配置。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/aliyun-dns-weight-setting.png\" alt></p>\n<h3>反向代理</h3>\n<p>客户端将请求发送到反向代理服务器，由反向代理服务器去选择目标服务器，获取数据后再返回给客户端。对外暴露的是反向代理服务器地址，隐藏了真实服务器 IP 地址。反向代理“代理”的是目标服务器，这一个过程对于客户端而言是透明的。</p>\n<p>Nginx 就是最常用的反向代理服务器，它可以将接收到的客户端请求以一定的规则（负载均衡策略）均匀地分配到这个服务器集群中所有的服务器上。</p>\n<p>反向代理负载均衡同样属于七层负载均衡。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/nginx-load-balance.png\" alt></p>\n<h2>客户端负载均衡通常是怎么做的？</h2>\n<p>我们上面也说了，客户端负载均衡可以使用现成的负载均衡组件来实现。</p>\n<p><strong>Netflix Ribbon</strong> 和 <strong>Spring Cloud Load Balancer</strong> 就是目前 Java 生态最流行的两个负载均衡组件。</p>\n<p>Ribbon 是老牌负载均衡组件，由 Netflix 开发，功能比较全面，支持的负载均衡策略也比较多。 Spring Cloud Load Balancer 是 Spring 官方为了取代 Ribbon 而推出的，功能相对更简单一些，支持的负载均衡也少一些。</p>\n<p>Ribbon 支持的 7 种负载均衡策略：</p>\n<ul>\n<li><code>RandomRule</code>：随机策略。</li>\n<li><code>RoundRobinRule</code>（默认）：轮询策略</li>\n<li><code>WeightedResponseTimeRule</code>：权重（根据响应时间决定权重）策略</li>\n<li><code>BestAvailableRule</code>：最小连接数策略</li>\n<li><code>RetryRule</code>：重试策略（按照轮询策略来获取服务，如果获取的服务实例为 null 或已经失效，则在指定的时间之内不断地进行重试来获取服务，如果超过指定时间依然没获取到服务实例则返回 null）</li>\n<li><code>AvailabilityFilteringRule</code>：可用敏感性策略（先过滤掉非健康的服务实例，然后再选择连接数较小的服务实例）</li>\n<li><code>ZoneAvoidanceRule</code>：区域敏感性策略（根据服务所在区域的性能和服务的可用性来选择服务实例）</li>\n</ul>\n<p>Spring Cloud Load Balancer 支持的 2 种负载均衡策略：</p>\n<ul>\n<li><code>RandomLoadBalancer</code>：随机策略</li>\n<li><code>RoundRobinLoadBalancer</code>（默认）：轮询策略</li>\n</ul>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> CustomLoadBalancerConfiguration</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Bean</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ReactorLoadBalancer</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">ServiceInstance</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> randomLoadBalancer</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Environment</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> environment</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">            LoadBalancerClientFactory</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> loadBalancerClientFactory</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> name</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> environment</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getProperty</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">LoadBalancerClientFactory</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">PROPERTY_NAME</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        return</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> RandomLoadBalancer</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(loadBalancerClientFactory</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">                .</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getLazyProvider</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(name, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">ServiceInstanceListSupplier</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">class</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">),</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">                name);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>不过，Spring Cloud Load Balancer 支持的负载均衡策略其实不止这两种，<code>ServiceInstanceListSupplier</code> 的实现类同样可以让其支持类似于 Ribbon 的负载均衡策略。这个应该是后续慢慢完善引入的，不看官方文档还真发现不了，所以说阅读官方文档真的很重要！</p>\n<p>这里举两个官方的例子：</p>\n<ul>\n<li><code>ZonePreferenceServiceInstanceListSupplier</code>：实现基于区域的负载平衡</li>\n<li><code>HintBasedServiceInstanceListSupplier</code>：实现基于 hint 提示的负载均衡</li>\n</ul>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> CustomLoadBalancerConfiguration</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 使用基于区域的负载平衡方法</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Bean</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ServiceInstanceListSupplier</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> discoveryClientServiceInstanceListSupplier</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">            ConfigurableApplicationContext</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> context</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> ServiceInstanceListSupplier</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">builder</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">                    .</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">withDiscoveryClient</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">                    .</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">withZonePreference</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">                    .</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">withCaching</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">                    .</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">build</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(context);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>关于 Spring Cloud Load Balancer 更详细更新的介绍，推荐大家看看官方文档：<a href=\"https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer\" target=\"_blank\" rel=\"noopener noreferrer\">https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer</a> ，一切以官方文档为主。</p>\n<p>轮询策略基本可以满足绝大部分项目的需求，我们的实际项目中如果没有特殊需求的话，通常使用的就是默认的轮询策略。并且，Ribbon 和 Spring Cloud Load Balancer 都支持自定义负载均衡策略。</p>\n<p>个人建议如非必需 Ribbon 某个特有的功能或者负载均衡策略的话，就优先选择 Spring 官方提供的 Spring Cloud Load Balancer。</p>\n<p>最后再说说为什么我不太推荐使用 Ribbon 。</p>\n<p>Spring Cloud 2020.0.0 版本移除了 Netflix 除 Eureka 外的所有组件。Spring Cloud Hoxton.M2 是第一个支持 Spring Cloud Load Balancer 来替代 Netfix Ribbon 的版本。</p>\n<p>我们早期学习微服务，肯定接触过 Netflix 公司开源的 Feign、Ribbon、Zuul、Hystrix、Eureka 等知名的微服务系统构建所必须的组件，直到现在依然有非常非常多的公司在使用这些组件。不夸张地说，Netflix 公司引领了 Java 技术栈下的微服务发展。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/SpringCloudNetflix.png\" alt></p>\n<p><strong>那为什么 Spring Cloud 这么急着移除 Netflix 的组件呢？</strong> 主要是因为在 2018 年的时候，Netflix 宣布其开源的核心组件 Hystrix、Ribbon、Zuul、Eureka 等进入维护状态，不再进行新特性开发，只修 BUG。于是，Spring 官方不得不考虑移除 Netflix 的组件。</p>\n<p><strong>Spring Cloud Alibaba</strong> 是一个不错的选择，尤其是对于国内的公司和个人开发者来说。</p>\n<h2>参考</h2>\n<ul>\n<li>干货 | eBay 的 4 层软件负载均衡实现：<a href=\"https://mp.weixin.qq.com/s/bZMxLTECOK3mjdgiLbHj-g\" target=\"_blank\" rel=\"noopener noreferrer\">https://mp.weixin.qq.com/s/bZMxLTECOK3mjdgiLbHj-g</a></li>\n<li>HTTP Load Balancing（Nginx 官方文档）：<a href=\"https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/\" target=\"_blank\" rel=\"noopener noreferrer\">https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/</a></li>\n<li>深入浅出负载均衡 - vivo 互联网技术：<a href=\"https://www.cnblogs.com/vivotech/p/14859041.html\" target=\"_blank\" rel=\"noopener noreferrer\">https://www.cnblogs.com/vivotech/p/14859041.html</a></li>\n</ul>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/multi-service-load-balancing.drawio.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高性能"
      ]
    },
    {
      "title": "读写分离和分库分表详解",
      "url": "https://codingmap.strova.top/high-performance/read-and-write-separation-and-library-subtable.html",
      "id": "https://codingmap.strova.top/high-performance/read-and-write-separation-and-library-subtable.html",
      "summary": "本文深入讲解数据库读写分离与分库分表的核心原理，涵盖主从复制机制、读写分离实现方案（代理/组件）、垂直分库分表与水平分库分表的区别，以及分库分表后的分布式事务、分布式ID、跨库JOIN等常见问题的解决方案。重点围绕 读写分离、分库分表、总结 等内容展开。结合 CodingMap 知识体系梳理 高性能 / Read A。",
      "content_html": "<h2>读写分离</h2>\n<h3>什么是读写分离？</h3>\n<p>顾名思义，根据读写分离的名字，我们就可以知道：<strong>读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。</strong> 这样的话，就能够小幅提升写性能，大幅提升读性能。</p>\n<p>我简单画了一张图来帮助不太清楚读写分离的小伙伴理解。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/read-and-write-separation-and-library-subtable/read-and-write-separation.png\" alt=\"读写分离示意图\"></p>\n<p>一般情况下，我们都会选择一主多从，也就是一台主数据库负责写，其他的从数据库负责读。主库和从库之间会进行数据同步，以保证从库中数据的准确性。这样的架构实现起来比较简单，并且也符合系统的写少读多的特点。</p>\n<h3>如何实现读写分离？</h3>\n<p>不论是使用哪一种读写分离具体的实现方案，想要实现读写分离一般包含如下几步：</p>\n<ol>\n<li>部署多台数据库，选择其中的一台作为主数据库，其他的一台或者多台作为从数据库。</li>\n<li>保证主数据库和从数据库之间的数据是实时同步的，这个过程也就是我们常说的<strong>主从复制</strong>。</li>\n<li>系统将写请求交给主数据库处理，读请求交给从数据库处理。</li>\n</ol>\n<p>落实到项目本身的话，常用的方式有两种：</p>\n<p><strong>1. 代理方式</strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/read-and-write-separation-and-library-subtable/read-and-write-separation-proxy.png\" alt=\"代理方式实现读写分离\"></p>\n<p>我们可以在应用和数据中间加了一个代理层。应用程序所有的数据请求都交给代理层处理，代理层负责分离读写请求，将它们路由到对应的数据库中。</p>\n<p>提供类似功能的中间件有 <strong>MySQL Router</strong>（官方， MySQL Proxy 的替代方案）、<strong>Atlas</strong>（基于 MySQL Proxy）、<strong>MaxScale</strong>、<strong>MyCat</strong>。</p>\n<p>关于 MySQL Router 多提一点：在 MySQL 8.2 的版本中，MySQL Router 能自动分辨对数据库读写/操作并把这些操作路由到正确的实例上。这是一项有价值的功能，可以优化数据库性能和可扩展性，而无需在应用程序中进行任何更改。具体介绍可以参考官方博客：<a href=\"https://blogs.oracle.com/mysql/post/mysql-82-transparent-readwrite-splitting\" target=\"_blank\" rel=\"noopener noreferrer\">MySQL 8.2 – transparent read/write splitting</a>。</p>\n<p><strong>2. 组件方式</strong></p>\n<p>在这种方式中，我们可以通过引入第三方组件来实现读写请求的路由。</p>\n<p>这也是我比较推荐的一种方式。这种方式目前在各种互联网公司中用的最多的，相关的实际的案例也非常多。如果你要采用这种方式的话，推荐使用 <strong>ShardingSphere-JDBC</strong> ，直接引入 jar 包即可使用，非常方便。同时，也节省了很多运维的成本。</p>\n<p>你可以在 ShardingSphere 官方找到 <a href=\"https://shardingsphere.apache.org/document/current/cn/features/readwrite-splitting/\" target=\"_blank\" rel=\"noopener noreferrer\">ShardingSphere-JDBC 读写分离配置</a>。</p>\n<h3>主从复制原理是什么？</h3>\n<p>MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据库中数据的所有变化(数据库执行的所有 DDL 和 DML 语句)。因此，我们根据主库的 MySQL binlog 日志就能够将主库的数据同步到从库中。</p>\n<p>更具体和详细的过程是这个样子的（图片来自于：<a href=\"https://www.toptal.com/mysql/mysql-master-slave-replication-tutorial\" target=\"_blank\" rel=\"noopener noreferrer\">《MySQL Master-Slave Replication on the Same Machine》</a>）：</p>\n<p><img src=\"https://oss.javaguide.cn/java-guide-blog/78816271d3ab52424bfd5ad3086c1a0f.png\" alt=\"MySQL主从复制\"></p>\n<ol>\n<li>主库将数据库中数据的变化写入到 binlog</li>\n<li>从库连接主库</li>\n<li>从库会创建一个 I/O 线程向主库请求更新的 binlog</li>\n<li>主库会创建一个 binlog dump 线程来发送 binlog ，从库中的 I/O 线程负责接收</li>\n<li>从库的 I/O 线程将接收的 binlog 写入到 relay log 中。</li>\n<li>从库的 SQL 线程读取 relay log 同步数据到本地（也就是再执行一遍 SQL ）。</li>\n</ol>\n<p>怎么样？看了我对主从复制这个过程的讲解，你应该搞明白了吧!</p>\n<p>你一般看到 binlog 就要想到主从复制。当然，除了主从复制之外，binlog 还能帮助我们实现数据恢复。</p>\n<p>🌈 拓展一下：</p>\n<p>不知道大家有没有使用过阿里开源的一个叫做 canal 的工具。这个工具可以帮助我们实现 MySQL 和其他数据源比如 Elasticsearch 或者另外一台 MySQL 数据库之间的数据同步。很显然，这个工具的底层原理肯定也是依赖 binlog。canal 的原理就是模拟 MySQL 主从复制的过程，解析 binlog 将数据同步到其他的数据源。</p>\n<p>另外，像咱们常用的分布式缓存组件 Redis 也是通过主从复制实现的读写分离。</p>\n<p>🌕 简单总结一下：</p>\n<p><strong>MySQL 主从复制是依赖于 binlog 。另外，常见的一些同步 MySQL 数据到其他数据源的工具（比如 canal）的底层一般也是依赖 binlog 。</strong></p>\n<h3>如何避免主从延迟？</h3>\n<p>读写分离对于提升数据库的并发非常有效，但是，同时也会引来一个问题：主库和从库的数据存在延迟，比如你写完主库之后，主库的数据同步到从库是需要时间的，这个时间差就导致了主库和从库的数据不一致性问题。这也就是我们经常说的 <strong>主从同步延迟</strong> 。</p>\n<p>如果我们的业务场景无法容忍主从同步延迟的话，应该如何避免呢（注意：我这里说的是避免而不是减少延迟）？</p>\n<p>这里提供两种我知道的方案（能力有限，欢迎补充），你可以根据自己的业务场景参考一下。</p>\n<h4>强制将读请求路由到主库处理</h4>\n<p>对于极少数必须强一致的业务（如支付后立刻查询余额），可以通过 Hint 强制查主库。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// ShardingSphere-JDBC 强制读主库</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">HintManager</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> hintManager </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> HintManager</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getInstance</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">hintManager</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">setMasterRouteOnly</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 继续JDBC操作</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><blockquote>\n<p>⚠️ <strong>注意</strong>：严禁大范围使用此方案！读写分离的初衷就是为了分担主库的读压力，若大量读请求因延迟而回退到主库，在促销、秒杀等高并发场景下极易压垮主库导致全站宕机。<strong>正确的 Trade-off</strong>：仅核心强一致链路读主库，非核心链路必须在业务层容忍最终一致性（如页面提示&quot;数据同步中&quot;）。</p>\n</blockquote>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">HintManager</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> hintManager </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> HintManager</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getInstance</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">hintManager</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">setMasterRouteOnly</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 继续JDBC操作</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>对于这种方案，你可以将那些必须获取最新数据的读请求都交给主库处理。</p>\n<h4>延迟读取</h4>\n<p>还有一些朋友肯定会想既然主从同步存在延迟，那我就在延迟之后读取啊，比如主从同步延迟 0.5s,那我就 1s 之后再读取数据。这样多方便啊！方便是方便，但是也很扯淡。</p>\n<p>不过，如果你是这样设计业务流程就会好很多：对于一些对数据比较敏感的场景，你可以在完成写请求之后，避免立即进行请求操作。比如你支付成功之后，跳转到一个支付成功的页面，当你点击返回之后才返回自己的账户。</p>\n<h4>总结</h4>\n<p>关于如何避免主从延迟，我们这里介绍了两种方案。实际上，延迟读取这种方案没办法完全避免主从延迟，只能说可以减少出现延迟的概率而已，实际项目中一般不会使用。</p>\n<p>总的来说，要想不出现延迟问题，一般还是要强制将那些必须获取最新数据的读请求都交给主库处理。如果你的项目的大部分业务场景对数据准确性要求不是那么高的话，这种方案还是可以选择的。</p>\n<h3>什么情况下会出现主从延迟？如何尽量减少延迟？</h3>\n<p>我们在上面的内容中也提到了主从延迟以及避免主从延迟的方法，这里我们再来详细分析一下主从延迟出现的原因以及应该如何尽量减少主从延迟。</p>\n<p>要搞懂什么情况下会出现主从延迟，我们需要先搞懂什么是主从延迟。</p>\n<p>MySQL 主从同步延时是指从库的数据落后于主库的数据，这种情况可能由以下两个原因造成：</p>\n<ol>\n<li>从库 I/O 线程接收 binlog 的速度跟不上主库写入 binlog 的速度，导致从库 relay log 的数据滞后于主库 binlog 的数据；</li>\n<li>从库 SQL 线程执行 relay log 的速度跟不上从库 I/O 线程接收 binlog 的速度，导致从库的数据滞后于从库 relay log 的数据。</li>\n</ol>\n<p>与主从同步有关的时间点主要有 3 个：</p>\n<ol>\n<li>主库执行完一个事务，写入 binlog，将这个时刻记为 T1；</li>\n<li>从库 I/O 线程接收到 binlog 并写入 relay log 的时刻记为 T2；</li>\n<li>从库 SQL 线程读取 relay log 同步数据本地的时刻记为 T3。</li>\n</ol>\n<blockquote>\n<p><strong>注意</strong>：上述描述基于 MySQL 默认的<strong>异步复制</strong>模式。如果在 MySQL 5.7+ 开启了增强半同步复制（<code>rpl_semi_sync_master_wait_point=AFTER_SYNC</code>），主库在写入 binlog 后会等待至少一个从库接收并写入 relay log 才向客户端返回提交成功，这在一定程度上将 T2-T1 的网络传输时间算入了主库事务的响应时间中，从而牺牲写性能换取更高的数据安全性。</p>\n</blockquote>\n<p>结合我们上面讲到的主从复制原理，可以得出：</p>\n<ul>\n<li>T2 和 T1 的差值反映了从库 I/O 线程的性能和网络传输的效率，这个差值越小说明从库 I/O 线程的性能和网络传输效率越高。</li>\n<li>T3 和 T2 的差值反映了从库 SQL 线程执行的速度，这个差值越小，说明从库 SQL 线程执行速度越快。</li>\n</ul>\n<p>那什么情况下会出现出从延迟呢？这里列举几种常见的情况：</p>\n<ol>\n<li><strong>从库机器性能比主库差</strong>：从库接收 binlog 并写入 relay log 以及执行 SQL 语句的速度会比较慢（也就是 T2-T1 和 T3-T2 的值会较大），进而导致延迟。解决方法是选择与主库一样规格或更高规格的机器作为从库，或者对从库进行性能优化，比如调整参数、增加缓存、使用 SSD 等。</li>\n<li><strong>从库处理的读请求过多</strong>：从库需要执行主库的所有写操作，同时还要响应读请求，如果读请求过多，会占用从库的 CPU、内存、网络等资源，影响从库的复制效率（也就是 T2-T1 和 T3-T2 的值会较大，和前一种情况类似）。解决方法是引入缓存（推荐）、使用一主多从的架构，将读请求分散到不同的从库，或者使用其他系统来提供查询的能力，比如将 binlog 接入到 Hadoop、Elasticsearch 等系统中。</li>\n<li><strong>大事务</strong>：运行时间比较长，长时间未提交的事务就可以称为大事务。由于大事务执行时间长，并且从库上的大事务会比主库上的大事务花费更多的时间和资源，因此非常容易造成主从延迟。解决办法是避免大批量修改数据，尽量分批进行。类似的情况还有执行时间较长的慢 SQL ，实际项目遇到慢 SQL 应该进行优化。</li>\n<li><strong>从库太多</strong>：主库需要将 binlog 同步到所有的从库，如果从库数量太多，会增加同步的时间和开销（也就是 T2-T1 的值会比较大，但这里是因为主库同步压力大导致的）。解决方案是减少从库的数量，或者将从库分为不同的层级，让上层的从库再同步给下层的从库，减少主库的压力。</li>\n<li><strong>网络延迟</strong>：如果主从之间的网络传输速度慢，或者出现丢包、抖动等问题，那么就会影响 binlog 的传输效率，导致从库延迟。解决方法是优化网络环境，比如提升带宽、降低延迟、增加稳定性等。</li>\n<li><strong>单线程复制</strong>：MySQL 5.5 及之前，只支持单线程复制。为了优化复制性能，MySQL 5.6 引入了 <strong>多线程复制</strong>，但仅支持按库并行（<code>slave_parallel_type=DATABASE</code>）。MySQL 5.7 进一步完善，支持按组提交并行（<code>slave_parallel_type=LOGICAL_CLOCK</code>），大幅提升并行效率。建议在从库配置 <code>slave_parallel_workers &gt; 0</code> 启用并行复制。</li>\n<li><strong>复制模式</strong>：MySQL 默认的复制是异步的，必然会存在延迟问题。全同步复制不存在延迟问题，但性能太差了。半同步复制是一种折中方案，相对于异步复制，半同步复制提高了数据的安全性，减少了主从延迟（还是有一定程度的延迟）。MySQL 5.5 开始，MySQL 以插件的形式支持 <strong>semi-sync 半同步复制</strong>。并且，MySQL 5.7 引入了 <strong>增强半同步复制</strong> 。</li>\n<li>……</li>\n</ol>\n<h2>分库分表</h2>\n<p>读写分离主要应对的是数据库读并发，没有解决数据库存储问题。试想一下：<strong>如果 MySQL 一张表的数据量过大怎么办?</strong></p>\n<p>换言之，<strong>我们该如何解决 MySQL 的存储压力呢？</strong></p>\n<p>答案之一就是 <strong>分库分表</strong>。</p>\n<h3>什么是分库？</h3>\n<p><strong>分库</strong> 就是将数据库中的数据分散到不同的数据库上，可以垂直分库，也可以水平分库。</p>\n<p><strong>垂直分库</strong> 就是把单一数据库按照业务进行划分，不同的业务使用不同的数据库，进而将一个数据库的压力分担到多个数据库。</p>\n<p>举个例子：说你将数据库中的用户表、订单表和商品表分别单独拆分为用户数据库、订单数据库和商品数据库。</p>\n<p></p>\n<p><strong>水平分库</strong> 是把同一个表按一定规则拆分到不同的数据库中，每个库可以位于不同的服务器上，这样就实现了水平扩展，解决了单表的存储和性能瓶颈的问题。</p>\n<p>举个例子：订单表数据量太大，你对订单表进行了水平切分（水平分表），然后将切分后的 2 张订单表分别放在两个不同的数据库。</p>\n<p></p>\n<h3>什么是分表？</h3>\n<p><strong>分表</strong> 就是对单表的数据进行拆分，可以是垂直拆分，也可以是水平拆分。</p>\n<p><strong>垂直分表</strong> 是对数据表列的拆分，把一张列比较多的表拆分为多张表。</p>\n<p>举个例子：我们可以将用户信息表中的一些列单独抽出来作为一个表。</p>\n<p><strong>水平分表</strong> 是对数据表行的拆分，把一张行比较多的表拆分为多张表，可以解决单一表数据量过大的问题。</p>\n<p>举个例子：我们可以将用户信息表拆分成多个用户信息表，这样就可以避免单一表数据量过大对性能造成影响。</p>\n<p>水平拆分只能解决单表数据量大的问题，为了提升性能，我们通常会选择将拆分后的多张表放在不同的数据库中。也就是说，水平分表通常和水平分库同时出现。</p>\n<p></p>\n<h3>什么情况下需要分库分表？</h3>\n<p>遇到下面几种场景可以考虑分库分表：</p>\n<ul>\n<li>单表的数据量达到千万级别以上（具体阈值取决于表结构复杂度、索引数量、硬件配置等），数据库读写速度明显下降。</li>\n<li>数据库中的数据占用的空间越来越大，备份时间越来越长。</li>\n<li>应用的并发量太大（应该优先考虑其他性能优化方法，而非分库分表）。</li>\n</ul>\n<p>不过，分库分表的成本太高，如非必要尽量不要采用。而且，并不一定是单表千万级数据量就要分表，毕竟每张表包含的字段不同，它们在不错的性能下能够存放的数据量也不同，还是要具体情况具体分析。</p>\n<p>之前看过一篇文章分析 “<a href=\"https://juejin.cn/post/7165689453124517896\" target=\"_blank\" rel=\"noopener noreferrer\">InnoDB 中高度为 3 的 B+ 树最多可以存多少数据</a>”，写的挺不错，感兴趣的可以看看。</p>\n<h3>常见的分片算法有哪些？</h3>\n<p>分片算法主要解决了数据被水平分片之后，数据究竟该存放在哪个表的问题。</p>\n<p>常见的分片算法有：</p>\n<ul>\n<li><strong>哈希分片</strong>：求指定分片键的哈希，然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景，不太适合经常需要范围查询的场景。哈希分片可以使每个表的数据分布相对均匀，但对动态伸缩（例如新增一个表或者库）不友好。</li>\n<li><strong>范围分片</strong>：按照特定的范围区间（比如时间区间、ID 区间）来分配数据，比如 将 <code>id</code> 为 <code>1~299999</code> 的记录分到第一个表， <code>300000~599999</code> 的分到第二个表。范围分片适合需要经常进行范围查找且数据分布均匀的场景，不太适合随机读写的场景（数据未被分散，容易出现热点数据的问题）。</li>\n<li><strong>一致性哈希分片</strong>：将哈希空间组织成一个环形结构，将分片键和节点（数据库或表）都映射到这个环上，然后根据顺时针的规则确定数据或请求应该分配到哪个节点上，解决了传统哈希对动态伸缩不友好的问题。</li>\n</ul>\n<p>在上述基础算法之上，还可以结合业务衍生出更复杂的路由策略：</p>\n<ul>\n<li><strong>映射表路由</strong>：维护一张独立的路由表来记录分片键与数据节点的映射关系，极其灵活但存在单点性能瓶颈。</li>\n<li><strong>地域路由</strong>：以地理位置作为分片键，结合范围或映射表机制，将数据就近存放在特定机房（常用于 NewSQL 多活架构）。</li>\n</ul>\n<h3>分片键如何选择？</h3>\n<p>分片键（Sharding Key）是数据分片的关键字段。分片键的选择非常重要，它关系着数据的分布和查询效率。一般来说，分片键应该具备以下特点：</p>\n<ul>\n<li>具有共性，即能够覆盖绝大多数的查询场景，尽量减少单次查询所涉及的分片数量，降低数据库压力；</li>\n<li>具有离散性，即能够将数据均匀地分散到各个分片上，避免数据倾斜和热点问题；</li>\n<li>具有稳定性，即分片键的值不会发生变化，避免数据迁移和一致性问题；</li>\n<li>具有扩展性，即能够支持分片的动态增加和减少，避免数据重新分片的开销。</li>\n</ul>\n<p>实际项目中，分片键很难满足上面提到的所有特点，需要权衡一下。并且，分片键可以是表中多个字段的组合，例如取用户 ID 后四位作为订单 ID 后缀。</p>\n<h3>分库分表会带来什么问题呢？</h3>\n<p>记住，你在公司做的任何技术决策，不光是要考虑这个技术能不能满足我们的要求，是否适合当前业务场景，还要重点考虑其带来的成本。</p>\n<p>引入分库分表之后，会给系统带来什么挑战呢？</p>\n<ul>\n<li><strong>join 操作</strong>：同一个数据库中的表分布在了不同的数据库中，导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装，比如你在一个数据库中查询到一个数据之后，再根据这个数据去另外一个数据库中找对应的数据。不过，很多大厂的资深 DBA 都是建议尽量不要使用 join 操作。因为 join 的效率低，并且会对分库分表造成影响。对于需要用到 join 操作的地方，可以采用多次查询业务层进行数据组装的方法。不过，这种方法需要考虑业务上多次查询的事务性的容忍度。</li>\n<li><strong>事务问题</strong>：同一个数据库中的表分布在了不同的数据库中，如果单个操作涉及到多个数据库，那么数据库自带的事务就无法满足我们的要求了。这个时候，我们就需要引入分布式事务了。关于分布式事务常见解决方案总结，网站上也有对应的总结：<a href=\"https://javaguide.cn/distributed-system/distributed-transaction.html\" target=\"_blank\" rel=\"noopener noreferrer\">https://javaguide.cn/distributed-system/distributed-transaction.html</a> 。</li>\n<li><strong>分布式 ID</strong>：分库之后， 数据遍布在不同服务器上的数据库，数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢？这个时候，我们就需要为我们的系统引入分布式 ID 了。关于分布式 ID 的详细介绍&amp;实现方案总结，可以看我写的这篇文章：<a href=\"https://javaguide.cn/distributed-system/distributed-id.html\" target=\"_blank\" rel=\"noopener noreferrer\">分布式 ID 介绍&amp;实现方案总结</a>。</li>\n<li><strong>跨库聚合查询问题</strong>：分库分表会导致常规聚合查询操作，如 group by，order by 等变得异常复杂。这是因为这些操作需要在多个分片上进行数据汇总和排序，而不是在单个数据库上进行。为了实现这些操作，需要编写复杂的业务代码，或者使用中间件来协调分片间的通信和数据传输。这样会增加开发和维护的成本，以及影响查询的性能和可扩展性。</li>\n<li><strong>动态扩缩容困难（Resharding）</strong>：尤其是采用传统 Hash 取模算法时，一旦现有分片容量打满需要增加新节点，会导致绝大多数数据的 Hash 映射失效，引发极其痛苦的全量数据洗牌与迁移。解决方案包括：预分足够的分片（如 1024 个逻辑分表）、采用一致性哈希、或使用支持自动 Rebalance 的分布式数据库（如 TiDB）。</li>\n<li>……</li>\n</ul>\n<p>另外，引入分库分表之后，一般需要 DBA 的参与，同时还需要更多的数据库服务器，这些都属于成本。</p>\n<h3>分库分表有没有什么比较推荐的方案？</h3>\n<p>Apache ShardingSphere 是一款分布式的数据库生态系统， 可以将任意数据库转换为分布式数据库，并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。</p>\n<p>ShardingSphere 项目（包括 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar）是当当捐入 Apache 的，目前主要由京东数科的一些巨佬维护。</p>\n<p>ShardingSphere 绝对可以说是当前分库分表的首选！ShardingSphere 的功能完善，除了支持读写分离和分库分表，还提供分布式事务、数据库治理、影子库、数据加密和脱敏等功能。</p>\n<p>ShardingSphere 提供的功能如下：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/high-performance/shardingsphere-features.png\" alt=\"ShardingSphere 提供的功能\"></p>\n<p>ShardingSphere 的优势如下（摘自 ShardingSphere 官方文档：<a href=\"https://shardingsphere.apache.org/document/current/cn/overview/\" target=\"_blank\" rel=\"noopener noreferrer\">https://shardingsphere.apache.org/document/current/cn/overview/</a>）：</p>\n<ul>\n<li>极致性能：驱动程序端历经长年打磨，效率接近原生 JDBC，性能极致。</li>\n<li>生态兼容：代理端支持任何通过 MySQL/PostgreSQL 协议的应用访问，驱动程序端可对接任意实现 JDBC 规范的数据库。</li>\n<li>业务零侵入：面对数据库替换场景，ShardingSphere 可满足业务无需改造，实现平滑业务迁移。</li>\n<li>运维低成本：在保留原技术栈不变前提下，对 DBA 学习、管理成本低，交互友好。</li>\n<li>安全稳定：基于成熟数据库底座之上提供增量能力，兼顾安全性及稳定性。</li>\n<li>弹性扩展：具备计算、存储平滑在线扩展能力，可满足业务多变的需求。</li>\n<li>开放生态：通过多层次（内核、功能、生态）插件化能力，为用户提供可定制满足自身特殊需求的独有系统。</li>\n</ul>\n<p>另外，ShardingSphere 的生态体系完善，社区活跃，文档完善，更新和发布比较频繁。</p>\n<p>不过，还是要多提一句：<strong>现在很多公司都是用的类似于 TiDB 这种分布式关系型数据库，不需要我们手动进行分库分表（数据库层面已经帮我们做了），也不需要解决手动分库分表引入的各种问题，直接一步到位，内置很多实用的功能（如无感扩容和缩容、冷热存储分离）！如果公司条件允许的话，个人也是比较推荐这种方式！</strong></p>\n<h3>分库分表后，数据怎么迁移呢？</h3>\n<p>分库分表之后，我们如何将老库（单库单表）的数据迁移到新库（分库分表后的数据库系统）呢？</p>\n<p>比较简单同时也是非常常用的方案就是<strong>停机迁移</strong>，写个脚本老库的数据写到新库中。比如你在凌晨 2 点，系统使用的人数非常少的时候，挂一个公告说系统要维护升级预计 1 小时。然后，你写一个脚本将老库的数据都同步到新库中。</p>\n<p>如果你不想停机迁移数据的话，也可以考虑<strong>双写方案</strong>。双写方案是针对那种不能停机迁移的场景，实现起来要稍微麻烦一些。具体原理是这样的：</p>\n<ul>\n<li>我们对老库的更新操作（增删改），同时也要写入新库（双写）。如果操作的数据在新库中不存在，则执行插入；若已存在，则执行更新。这样就能保证新库捕获到最新的变更。</li>\n<li>在迁移过程，双写只会让被更新操作过的老库中的数据同步到新库，我们还需要自己写脚本将老库中的数据和新库的数据做比对。如果新库中没有，那咱们就把数据插入到新库。如果新库有，旧库没有，就把新库对应的数据删除（冗余数据清理）。</li>\n<li>重复上一步的操作，直到老库和新库的数据一致为止。</li>\n</ul>\n<blockquote>\n<p><strong>⚠️注意</strong>：</p>\n<ul>\n<li>双写应尽量保证原子性：可以先写老库成功后再异步写新库，若新库写入失败则记录日志待重试；</li>\n<li>数据比对应在业务低峰期进行，避免比对期间新写入导致的数据不一致；</li>\n<li>建议借助 Canal 等工具监听 binlog 实现增量同步，降低双写的开发和维护成本。</li>\n</ul>\n<p><strong>双写并发问题如何解决？</strong> 在存量数据迁移和增量双写并行的阶段，极易发生旧数据覆盖新数据的并发问题。必须在新库表中引入 <code>update_time</code> 或 <code>version</code> 字段，无论是双写还是脚本补齐，写入新库前必须带上条件 <code>WHERE new_version &lt; old_version</code>（乐观锁校验），确保只有较新的数据才能写入。</p>\n</blockquote>\n<p>想要在项目中实施双写还是比较麻烦的，很容易会出现问题。我们可以借助上面提到的数据库同步工具 Canal 做增量数据迁移（还是依赖 binlog，开发和维护成本较低）。</p>\n<h2>总结</h2>\n<ul>\n<li>读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。 这样的话，就能够小幅提升写性能，大幅提升读性能。</li>\n<li>读写分离基于主从复制，MySQL 主从复制是依赖于 binlog 。</li>\n<li><strong>分库</strong> 就是将数据库中的数据分散到不同的数据库上。<strong>分表</strong> 就是对单表的数据进行拆分，可以是垂直拆分，也可以是水平拆分。</li>\n<li>引入分库分表之后，需要系统解决事务、分布式 id、无法 join 操作问题。</li>\n<li>现在很多公司都是用的类似于 TiDB 这种分布式关系型数据库，不需要我们手动进行分库分表（数据库层面已经帮我们做了），也不需要解决手动分库分表引入的各种问题，直接一步到位，内置很多实用的功能（如无感扩容和缩容、冷热存储分离）！如果公司条件允许的话，个人也是比较推荐这种方式！</li>\n<li>如果必须要手动分库分表的话，ShardingSphere 是首选！ShardingSphere 的功能完善，除了支持读写分离和分库分表，还提供分布式事务、数据库治理等功能。另外，ShardingSphere 的生态体系完善，社区活跃，文档完善，更新和发布比较频繁。</li>\n</ul>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/high-performance/read-and-write-separation-and-library-subtable/read-and-write-separation.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高性能"
      ]
    },
    {
      "title": "常见SQL优化手段总结",
      "url": "https://codingmap.strova.top/high-performance/sql-optimization.html",
      "id": "https://codingmap.strova.top/high-performance/sql-optimization.html",
      "summary": "本文系统总结常见的 SQL 优化手段，涵盖慢 SQL 定位与分析（EXPLAIN、Show Profile）、索引优化策略、查询重写技巧、分页优化等实战方法，帮助你快速提升数据库查询性能。重点围绕 避免使用 SELECT *、尽量避免多表做 join、深度分页优化、建议不要使用外键与级联 等内容展开。",
      "content_html": "<h2>避免使用 SELECT *</h2>\n<ul>\n<li><code>SELECT *</code> 会消耗更多的 CPU。</li>\n<li><code>SELECT *</code> 无用字段增加网络带宽资源消耗，增加数据传输时间，尤其是大字段（如 varchar、blob、text）。</li>\n<li><code>SELECT *</code> 无法使用 MySQL 优化器覆盖索引的优化（基于 MySQL 优化器的“覆盖索引”策略又是速度极快，效率极高，业界极为推荐的查询优化方式）</li>\n<li><code>SELECT &lt;字段列表&gt;</code> 可减少表结构变更带来的影响。</li>\n</ul>\n<h2>尽量避免多表做 join</h2>\n<p>阿里巴巴《Java 开发手册》中有这样一段描述：</p>\n<blockquote>\n<p>【强制】超过三个表禁止 join。需要 join 的字段，数据类型保持绝对一致;多表关联查询时，保证被关联 的字段需要有索引。</p>\n</blockquote>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/mysql/alibaba-java-development-handbook-multi-table-join.png\" alt=\"尽量避免多表做 join\"></p>\n<p>join 的效率比较低，主要原因是因为其使用嵌套循环（Nested Loop）来实现关联查询，以前常见的实现效率都不是很高：</p>\n<ul>\n<li><strong>Simple Nested-Loop Join</strong> ：直接使用笛卡尔积实现 join，逐行遍历/全表扫描，效率最低。</li>\n<li><strong>Block Nested-Loop Join (BNL)</strong> ：利用 JOIN BUFFER 进行优化。<strong>注意：在 MySQL 8.0.20 及更高版本中，BNL 已被 Hash Join 取代</strong>，Hash Join 通常能将非索引列关联的复杂度从 O(M*N) 降低到接近 O(M+N)。</li>\n<li><strong>Index Nested-Loop Join</strong> ：在必要的字段上增加索引，性能得到进一步提升。</li>\n</ul>\n<p>实际业务场景避免多表 join 常见的做法有两种：</p>\n<ol>\n<li><strong>单表查询后在内存中自己做关联</strong> ：对数据库做单表查询，再根据查询结果进行二次查询，以此类推，最后再进行关联。</li>\n<li><strong>数据冗余</strong>，把一些重要的数据在表中做冗余，尽可能地避免关联查询。很笨的一种做法，表结构比较稳定的情况下才会考虑这种做法。进行冗余设计之前，思考一下自己的表结构设计的是否有问题。</li>\n</ol>\n<p>更加推荐第一种，这种在实际项目中的使用率比较高，除了性能不错之外，还有如下优势：</p>\n<ol>\n<li><strong>拆分后的单表查询代码可复用性更高</strong> ：join 联表 SQL 基本不太可能被复用。</li>\n<li><strong>单表查询更利于后续的维护</strong> ：不论是后续修改表结构还是进行分库分表，单表查询维护起来都更容易。</li>\n</ol>\n<p>不过，如果系统要求的并发量不大的话，我觉得多表 join 也是没问题的。很多公司内部复杂的系统，要求的并发量不高，很多数据必须 join 5 张以上的表才能查出来。</p>\n<h2>深度分页优化</h2>\n<p>深度分页问题的根本原因在于：当 <code>LIMIT</code> 的偏移量过大时，MySQL 需要扫描并跳过大量记录才能获取目标数据，查询优化器可能放弃索引而选择全表扫描。此时即使有索引，也无法避免大量的回表操作，导致查询性能急剧下降。</p>\n<p>本文介绍了四种常见的深度分页优化方案，各方案的特点及适用场景对比如下：</p>\n<p>| 优化方案     | 核心思路                                                            | 适用场景                       | 限制                                             |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/mysql/alibaba-java-development-handbook-multi-table-join.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "高性能"
      ]
    },
    {
      "title": "高质量技术文章精选：程序员成长、工作经验、面试复盘与职业思考",
      "url": "https://codingmap.strova.top/high-quality-technical-articles/",
      "id": "https://codingmap.strova.top/high-quality-technical-articles/",
      "summary": "程序员成长与面试复盘文章精选，整理个人经历、职场经验、技术学习方法、职业发展思考等内容，适合长期成长与阶段性复盘。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章 等内容展开。结合 CodingMap 知识体系梳理 High Quality Technical Articles 的核心概念、实践方法等核心内容。",
      "content_html": "<p>这份 <strong>高质量技术文章精选</strong> 收录程序员成长、工作经验、面试复盘、职业选择和技术学习方法相关内容。它不是具体技术点清单，更适合在学习、求职、入职、转岗、晋升等阶段反复阅读。</p>\n<p>如果你时间有限，建议先看 <a href=\"/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.html\" target=\"_blank\">程序员如何快速学习新技术</a> 和 <a href=\"/high-quality-technical-articles/interview/technical-preliminary-preparation.html\" target=\"_blank\">从面试官和候选者的角度谈如何准备技术初试</a>，一个解决学习方法，一个解决面试准备。</p>\n<h2>适合谁看</h2>\n<ul>\n<li>正在从学生转向工程师、需要建立职业认知的同学。</li>\n<li>准备面试，希望理解面试官视角和真实候选人经验的读者。</li>\n<li>工作 1 到 5 年，想提升学习效率、业务理解和技术成长速度的程序员。</li>\n<li>面临入职适应、绩效考核、职业选择、成长瓶颈的工程师。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>技术成长不只是多学框架，更包括学习方法、业务理解、表达能力和工程判断。</li>\n<li>面试准备要同时站在候选人和面试官视角，既要能答题，也要能呈现真实项目能力。</li>\n<li>个人经历类文章适合用来理解不同阶段的选择、试错和复盘方式。</li>\n<li>工作经验类文章重点看“如何快速进入状态、如何和团队协作、如何面对绩效与反馈”。</li>\n<li>职业发展需要长期复盘，不要把短期 offer、职级或绩效当成唯一目标。</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.html\" target=\"_blank\">程序员如何快速学习新技术</a>：先建立学习新技术的方法。</li>\n<li><a href=\"/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.html\" target=\"_blank\">程序员的技术成长战略</a> 和 <a href=\"/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.html\" target=\"_blank\">给想成长为高级别开发同学的七条建议</a>：理解技术成长的长期路线。</li>\n<li><a href=\"/high-quality-technical-articles/interview/technical-preliminary-preparation.html\" target=\"_blank\">从面试官和候选者的角度谈如何准备技术初试</a>：用面试官视角校准准备方向。</li>\n<li><a href=\"/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.html\" target=\"_blank\">斩获 20+ 大厂 offer 的面试经验分享</a> 和 <a href=\"/high-quality-technical-articles/interview/summary-of-spring-recruitment.html\" target=\"_blank\">普通人的春招总结</a>：参考真实求职复盘。</li>\n<li><a href=\"/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.html\" target=\"_blank\">新入职一家公司如何快速进入工作状态</a>：拿到机会后继续关注入职、协作和长期成长。</li>\n</ol>\n<h2>核心文章</h2>\n<h3>练级攻略</h3>\n<ul>\n<li><a href=\"/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.html\" target=\"_blank\">程序员如何快速学习新技术</a>：讲学习路径、资料选择、实践验证和避免低效学习。</li>\n<li><a href=\"/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.html\" target=\"_blank\">程序员的技术成长战略</a>：从长期视角理解技术成长和能力沉淀。</li>\n<li><a href=\"/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.html\" target=\"_blank\">十年大厂成长之路</a>：复盘长期职业发展中的选择与积累。</li>\n<li><a href=\"/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.html\" target=\"_blank\">美团三年，总结的 10 条血泪教训</a>：从真实经历里看工程师成长常见坑。</li>\n<li><a href=\"/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.html\" target=\"_blank\">给想成长为高级别开发同学的七条建议</a>：聚焦高级别工程师需要的能力。</li>\n<li><a href=\"/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.html\" target=\"_blank\">糟糕程序员的 20 个坏习惯</a>：帮助识别影响成长和协作的坏习惯。</li>\n<li><a href=\"/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.html\" target=\"_blank\">工作五年之后，对技术和业务的思考</a>：理解技术、业务和个人发展的关系。</li>\n</ul>\n<h3>个人经历</h3>\n<ul>\n<li><a href=\"/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.html\" target=\"_blank\">从校招入职腾讯的四年工作总结</a>：复盘大厂工作、成长和选择。</li>\n<li><a href=\"/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.html\" target=\"_blank\">我在滴滴和头条的两年后端研发工作经验分享</a>：了解不同团队和业务环境下的后端研发经验。</li>\n<li><a href=\"/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.html\" target=\"_blank\">一个中科大差生的 8 年程序员工作总结</a>：从较长周期看程序员成长路径。</li>\n<li><a href=\"/high-quality-technical-articles/personal-experience/huawei-od-275-days.html\" target=\"_blank\">华为 OD 275 天后，我进了腾讯！</a>：参考真实求职和职业转换经历。</li>\n</ul>\n<h3>面试与工作</h3>\n<ul>\n<li><a href=\"/high-quality-technical-articles/interview/technical-preliminary-preparation.html\" target=\"_blank\">从面试官和候选者的角度谈如何准备技术初试</a>：理解技术初试到底考什么。</li>\n<li><a href=\"/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.html\" target=\"_blank\">斩获 20+ 大厂 offer 的面试经验分享</a>：参考完整求职准备和复盘方式。</li>\n<li><a href=\"/high-quality-technical-articles/interview/screen-candidates-for-packaging.html\" target=\"_blank\">包装严重的 IT 行业，作为面试官，我是如何甄别应聘者的包装程度</a>：理解面试官如何识别项目真实性。</li>\n<li><a href=\"/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.html\" target=\"_blank\">新入职一家公司如何快速进入工作状态</a>：帮助入职后快速熟悉业务、代码和团队节奏。</li>\n<li><a href=\"/high-quality-technical-articles/work/32-tips-improving-career.html\" target=\"_blank\">32 条总结教你提升职场经验</a>：补充沟通、协作、汇报和成长细节。</li>\n<li><a href=\"/high-quality-technical-articles/work/employee-performance.html\" target=\"_blank\">聊聊大厂的绩效考核</a>：理解绩效机制和工作反馈。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>程序员如何快速学习一门新技术，而不是只收藏资料？</li>\n<li>工作几年后，如何判断自己是在成长还是重复劳动？</li>\n<li>准备技术面试时，应该如何平衡八股文、项目和算法？</li>\n<li>面试官如何判断项目是不是候选人真实做过？</li>\n<li>新入职一家公司，前几周应该重点做什么？</li>\n<li>如何面对绩效、晋升、业务压力和职业焦虑？</li>\n<li>技术能力和业务理解哪个更重要？不同阶段如何取舍？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/java/\" target=\"_blank\">Java 知识体系</a></li>\n<li><a href=\"/cs-basics/\" target=\"_blank\">计算机基础知识体系</a></li>\n<li><a href=\"/books/\" target=\"_blank\">技术书籍精选</a></li>\n<li><a href=\"/open-source-project/\" target=\"_blank\">Java 开源项目精选</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "程序人生"
      ]
    },
    {
      "title": "Java 知识体系：基础、集合、并发、JVM、IO 与新特性",
      "url": "https://codingmap.strova.top/java/",
      "id": "https://codingmap.strova.top/java/",
      "summary": "Java 面试与知识体系学习路线，涵盖 Java 基础、集合源码、并发编程、JVM、IO/NIO 和 Java 新特性，适合校招、社招和 Java 后端面试复习。重点围绕 学习重点、建议阅读顺序、核心文章、高频问题、Java 等内容展开。结合 CodingMap 知识体系梳理 Java 的核心概念、实践方法等核心内容。",
      "content_html": "<p>这份 <strong>Java 知识体系</strong> 面向 Java 后端学习和面试复习，按“基础语法 -&gt; 集合容器 -&gt; 并发编程 -&gt; IO/NIO -&gt; JVM -&gt; 新特性”的顺序整理本站 Java 相关文章。</p>\n<p>如果你时间有限，建议先看 Java 基础、集合、并发和 JVM 的面试题总结，快速建立高频问题清单；如果你想系统补基础，可以按下面的专题顺序阅读。</p>\n<h2>学习重点</h2>\n<ul>\n<li>Java 基础语法、面向对象、异常、泛型、反射、代理、序列化等核心机制。</li>\n<li>List、Map、Queue、并发容器的使用边界、源码实现和常见面试题。</li>\n<li>Java 线程、锁、JMM、CAS、AQS、线程池、CompletableFuture 和虚拟线程。</li>\n<li>JVM 内存区域、类加载、垃圾回收、参数配置、监控工具和线上问题排查。</li>\n<li>BIO、NIO、AIO、IO 模型，以及装饰器、适配器等 IO 相关设计模式。</li>\n<li>Java 8 到 Java 26 的重要新特性，以及哪些特性真正影响日常开发。</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/java/basis/\" target=\"_blank\">Java 基础专题</a>：先掌握语法、面向对象、泛型、反射、代理、序列化等基础能力。</li>\n<li><a href=\"/java/collection/\" target=\"_blank\">Java 集合专题</a>：理解 ArrayList、LinkedList、HashMap、ConcurrentHashMap 等常用容器的使用和源码。</li>\n<li><a href=\"/java/concurrent/\" target=\"_blank\">Java 并发编程专题</a>：系统学习线程、锁、JMM、CAS、AQS、线程池和并发工具类。</li>\n<li><a href=\"/java/jvm/\" target=\"_blank\">JVM 专题</a>：理解内存区域、类加载、垃圾回收、JVM 参数和线上排查。</li>\n<li><a href=\"/java/io/\" target=\"_blank\">Java IO 专题</a>：补齐 BIO、NIO、AIO、Reactor、多路复用和 IO 设计模式。</li>\n<li><a href=\"/java/new-features/\" target=\"_blank\">Java 新特性专题</a>：按版本梳理 Lambda、Stream、模块化、var、Record、虚拟线程等关键特性。</li>\n</ol>\n<h2>核心文章</h2>\n<h3>Java 基础</h3>\n<ul>\n<li><a href=\"/java/basis/\" target=\"_blank\">Java 基础专题</a>：从基础语法讲到核心机制和常见 Java 面试题。</li>\n<li><a href=\"/java/basis/java-basic-questions-01.html\" target=\"_blank\">Java基础常见面试题总结(上)</a>：覆盖 Java 语言特点、基础语法、面向对象和常用类。</li>\n<li><a href=\"/java/basis/java-basic-questions-02.html\" target=\"_blank\">Java基础常见面试题总结(中)</a>：继续梳理异常、泛型、反射、注解和常见细节。</li>\n<li><a href=\"/java/basis/java-basic-questions-03.html\" target=\"_blank\">Java基础常见面试题总结(下)</a>：补齐高级基础知识和常见易错点。</li>\n<li><a href=\"/java/basis/why-there-only-value-passing-in-java.html\" target=\"_blank\">Java 值传递详解</a>：厘清值传递、引用变量和对象修改之间的关系。</li>\n<li><a href=\"/java/basis/serialization.html\" target=\"_blank\">Java 序列化详解</a>：理解序列化机制、serialVersionUID、安全风险和替代方案。</li>\n<li><a href=\"/java/basis/reflection.html\" target=\"_blank\">Java 反射机制详解</a> 和 <a href=\"/java/basis/proxy.html\" target=\"_blank\">Java 代理模式详解</a>：掌握框架底层常见机制。</li>\n</ul>\n<h3>Java 集合</h3>\n<ul>\n<li><a href=\"/java/collection/\" target=\"_blank\">Java 集合专题</a>：串联集合框架、使用注意事项和常见源码分析。</li>\n<li><a href=\"/java/collection/java-collection-questions-01.html\" target=\"_blank\">Java集合常见面试题总结(上)</a> 和 <a href=\"/java/collection/java-collection-questions-02.html\" target=\"_blank\">Java集合常见面试题总结(下)</a>：覆盖 List、Set、Map、Queue 和并发集合高频问题。</li>\n<li><a href=\"/java/collection/java-collection-precautions-for-use.html\" target=\"_blank\">Java集合使用注意事项总结</a>：总结集合判空、遍历、扩容、线程安全和性能相关注意点。</li>\n<li><a href=\"/java/collection/arraylist-source-code.html\" target=\"_blank\">ArrayList 源码分析</a>、<a href=\"/java/collection/hashmap-source-code.html\" target=\"_blank\">HashMap 源码分析</a>、<a href=\"/java/collection/concurrent-hash-map-source-code.html\" target=\"_blank\">ConcurrentHashMap 源码分析</a>：从源码理解常用容器的设计取舍。</li>\n</ul>\n<h3>Java 并发</h3>\n<ul>\n<li><a href=\"/java/concurrent/\" target=\"_blank\">Java 并发编程专题</a>：围绕线程、锁、内存模型、线程池和并发工具展开。</li>\n<li><a href=\"/java/concurrent/java-concurrent-questions-01.html\" target=\"_blank\">Java并发常见面试题总结（上）</a>、<a href=\"/java/concurrent/java-concurrent-questions-02.html\" target=\"_blank\">Java并发常见面试题总结（中）</a>、<a href=\"/java/concurrent/java-concurrent-questions-03.html\" target=\"_blank\">Java并发常见面试题总结（下）</a>：建立并发面试问题清单。</li>\n<li><a href=\"/java/concurrent/jmm.html\" target=\"_blank\">JMM（Java 内存模型）详解</a>：理解可见性、原子性、有序性和 happens-before。</li>\n<li><a href=\"/java/concurrent/cas.html\" target=\"_blank\">CAS 详解</a>、<a href=\"/java/concurrent/aqs.html\" target=\"_blank\">AQS 详解</a>、<a href=\"/java/concurrent/java-thread-pool-summary.html\" target=\"_blank\">Java 线程池详解</a>：掌握并发底层高频考点。</li>\n<li><a href=\"/java/concurrent/virtual-thread.html\" target=\"_blank\">虚拟线程常见问题总结</a>：理解 Project Loom 对并发模型的影响。</li>\n</ul>\n<h3>JVM 与 IO</h3>\n<ul>\n<li><a href=\"/java/jvm/\" target=\"_blank\">JVM 专题</a>：围绕内存、类加载、GC、参数、工具和线上排查展开。</li>\n<li><a href=\"/java/jvm/memory-area.html\" target=\"_blank\">Java内存区域详解（重点）</a>：理解程序计数器、虚拟机栈、本地方法栈、堆和方法区。</li>\n<li><a href=\"/java/jvm/jvm-garbage-collection.html\" target=\"_blank\">JVM垃圾回收详解（重点）</a>：理解对象存活判断、垃圾收集算法和主流垃圾收集器。</li>\n<li><a href=\"/java/jvm/class-loading-process.html\" target=\"_blank\">类加载过程详解</a> 和 <a href=\"/java/jvm/classloader.html\" target=\"_blank\">类加载器详解（重点）</a>：掌握类生命周期和双亲委派模型。</li>\n<li><a href=\"/java/io/\" target=\"_blank\">Java IO 专题</a>：从 BIO、NIO、AIO 讲到 IO 模型和 IO 设计模式。</li>\n<li><a href=\"/java/io/io-basis.html\" target=\"_blank\">Java IO 基础知识总结</a>、<a href=\"/java/io/nio-basis.html\" target=\"_blank\">Java NIO 核心知识总结</a>、<a href=\"/java/io/io-model.html\" target=\"_blank\">Java IO 模型详解</a>：补齐网络编程和中间件学习前置知识。</li>\n</ul>\n<h3>Java 新特性</h3>\n<ul>\n<li><a href=\"/java/new-features/\" target=\"_blank\">Java 新特性专题</a>：按版本梳理 Java 8 之后的重要语言、标准库和 JVM 特性。</li>\n<li><a href=\"/java/new-features/java8-common-new-features.html\" target=\"_blank\">Java8 新特性实战</a>：掌握 Lambda、Stream、Optional、接口默认方法和新日期 API。</li>\n<li><a href=\"/java/new-features/java11.html\" target=\"_blank\">Java 11 新特性概览（重要）</a>、<a href=\"/java/new-features/java17.html\" target=\"_blank\">Java 17 新特性概览（重要）</a>、<a href=\"/java/new-features/java21.html\" target=\"_blank\">Java 21 新特性概览(重要)</a>：优先关注 LTS 版本中的长期可用特性。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>Java 为什么是值传递？对象引用作为参数传递时到底发生了什么？</li>\n<li><code>String</code>、<code>StringBuilder</code>、<code>StringBuffer</code> 有什么区别？</li>\n<li><code>equals()</code> 和 <code>hashCode()</code> 有什么关系？</li>\n<li><code>ArrayList</code> 和 <code>LinkedList</code> 如何选择？<code>HashMap</code> 为什么线程不安全？</li>\n<li><code>ConcurrentHashMap</code> 在 JDK 7 和 JDK 8 中有什么变化？</li>\n<li><code>synchronized</code> 和 <code>ReentrantLock</code> 有什么区别？</li>\n<li>JMM 如何保证可见性、有序性和原子性？</li>\n<li>线程池核心参数如何配置？为什么不建议直接使用 <code>Executors</code>？</li>\n<li>JVM 内存区域如何划分？哪些区域可能发生 OOM？</li>\n<li>G1、ZGC、Shenandoah 分别适合什么场景？</li>\n<li>BIO、NIO、AIO 有什么区别？Reactor 模型解决什么问题？</li>\n<li>Java 8、11、17、21 中哪些新特性最值得掌握？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/cs-basics/\" target=\"_blank\">计算机基础</a></li>\n<li><a href=\"/system-design/\" target=\"_blank\">系统设计</a></li>\n<li><a href=\"/database/\" target=\"_blank\">数据库</a></li>\n<li><a href=\"/distributed-system/\" target=\"_blank\">分布式系统知识体系</a></li>\n<li><a href=\"/high-performance/\" target=\"_blank\">高性能系统知识体系</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "Java"
      ]
    },
    {
      "title": "Java 开源项目精选：实战项目、技术教程、系统设计、工具库与开发工具",
      "url": "https://codingmap.strova.top/open-source-project/",
      "id": "https://codingmap.strova.top/open-source-project/",
      "summary": "Java 开源项目推荐与实战学习路线，精选 GitHub 和 Gitee 上的实战项目、技术教程、系统设计、工具类库、AI、大数据和开发工具。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章 等内容展开。结合 CodingMap 知识体系梳理 开源项目 的核心概念、实践方法、常见问题和高频面试考点，覆盖原理分析等。",
      "content_html": "<p>这份 <strong>Java 开源项目精选</strong> 面向 Java 后端学习、项目实战和工具选型，整理 GitHub 和 Gitee 上优质的 Java 开源项目、教程资料、工具库和开发工具。</p>\n<p>内容来自开源项目 <a href=\"https://github.com/CodingDocs/awesome-java\" target=\"_blank\" rel=\"noopener noreferrer\">CodingDocs/awesome-java</a>，可以看作是 <a href=\"https://github.com/akullpp/awesome-java\" target=\"_blank\" rel=\"noopener noreferrer\">awesome-java</a> 的中文本土版本，项目类型更全面，也加入了更多中文开源项目。欢迎在项目 <a href=\"https://github.com/CodingDocs/awesome-java/issues\" target=\"_blank\" rel=\"noopener noreferrer\">issues 区</a> 推荐你认可的 Java 开源项目。</p>\n<h2>适合谁看</h2>\n<ul>\n<li>想找 Java 后端练手项目、简历项目或毕设项目的同学。</li>\n<li>准备校招、社招，需要通过项目补齐工程经验的读者。</li>\n<li>想学习优秀开源项目代码结构、架构设计和工程实践的后端开发者。</li>\n<li>需要快速选择 Java 工具库、开发工具或技术教程的工程师。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>选开源项目不要只看 Star，要看技术栈、文档质量、代码活跃度和能否真正跑起来。</li>\n<li>实战项目适合补业务建模、接口设计、权限、缓存、消息队列、部署等综合能力。</li>\n<li>系统设计项目适合理解框架、中间件、数据库、搜索引擎等基础设施的实现思路。</li>\n<li>工具类库和开发工具适合提升日常开发效率，但要理解引入成本和维护风险。</li>\n<li>简历项目要能讲清楚业务背景、技术选型、核心难点、压测结果和个人贡献。</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/open-source-project/tutorial.html\" target=\"_blank\">Java 优质开源技术教程</a>：先用教程和学习路线补基础，明确自己要学什么。</li>\n<li><a href=\"/open-source-project/practical-project.html\" target=\"_blank\">Java 优质开源实战项目</a>：选择一个能跑起来、能改造、能写进简历的项目。</li>\n<li><a href=\"/open-source-project/system-design.html\" target=\"_blank\">Java 优质开源系统设计项目</a>：进一步学习 Web 框架、微服务、消息队列、搜索引擎、数据库等基础架构项目。</li>\n<li><a href=\"/open-source-project/tool-library.html\" target=\"_blank\">Java 优质开源工具类库</a>：补齐常用工具库认知，提高开发效率。</li>\n<li><a href=\"/open-source-project/tools.html\" target=\"_blank\">程序员必备开发工具</a>：完善 IDE、调试、文档、效率和在线工具链。</li>\n</ol>\n<h2>核心文章</h2>\n<ul>\n<li><a href=\"/open-source-project/tutorial.html\" target=\"_blank\">Java 优质开源技术教程</a>：优质面试资料、技术教程和学习路线整理，适合系统学习与查缺补漏。</li>\n<li><a href=\"/open-source-project/practical-project.html\" target=\"_blank\">Java 优质开源实战项目</a>：简历友好、可落地的实战项目精选，覆盖后台管理、电商、权限、网盘、社区等类型。</li>\n<li><a href=\"/open-source-project/machine-learning.html\" target=\"_blank\">Java AI 相关优质开源项目</a>：整理 Java AI 开发框架和实战项目。</li>\n<li><a href=\"/open-source-project/system-design.html\" target=\"_blank\">Java 优质开源系统设计项目</a>：涵盖 Web 框架、微服务、消息队列、搜索引擎、数据库等基础架构组件。</li>\n<li><a href=\"/open-source-project/tool-library.html\" target=\"_blank\">Java 优质开源工具类库</a>：涵盖 Lombok、Guava、Hutool、Arthas 等常用工具。</li>\n<li><a href=\"/open-source-project/tools.html\" target=\"_blank\">程序员必备开发工具</a>：整理 IDE、调试、文档、效率、在线工具等开发工具。</li>\n<li><a href=\"/open-source-project/big-data.html\" target=\"_blank\">Java 大数据相关优质开源项目</a>：补充大数据方向的 Java 开源项目与工具。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>找 Java 练手项目时，应该优先看业务项目还是基础设施项目？</li>\n<li>如何判断一个开源项目是否适合写进简历？</li>\n<li>Star 数高的项目一定值得学习吗？</li>\n<li>克隆项目跑起来之后，应该从哪些地方开始改造？</li>\n<li>面试官问项目难点时，如何避免只说 CRUD？</li>\n<li>工具库应该如何选型？什么时候不应该引入新依赖？</li>\n<li>想提升源码阅读能力，应该从哪些开源项目开始？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/java/\" target=\"_blank\">Java 知识体系</a></li>\n<li><a href=\"/system-design/\" target=\"_blank\">系统设计</a></li>\n<li><a href=\"/distributed-system/\" target=\"_blank\">分布式系统知识体系</a></li>\n<li><a href=\"/high-performance/\" target=\"_blank\">高性能系统知识体系</a></li>\n<li><a href=\"/books/\" target=\"_blank\">技术书籍精选</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "开源项目"
      ]
    },
    {
      "title": "Java 优质开源大数据项目",
      "url": "https://codingmap.strova.top/open-source-project/big-data.html",
      "id": "https://codingmap.strova.top/open-source-project/big-data.html",
      "summary": "Java优质开源大数据项目推荐，涵盖Spark、Flink、HBase、Storm等主流大数据处理框架介绍与对比。结合 CodingMap 知识体系梳理 开源项目 / Big Data.Html 的核心概念、实践方法、常见问题和高频面试考点，覆盖原理分析、使用场景、方案对比与经验总结，适合开发者系统学习、面试复习等核。",
      "content_html": "<ul>\n<li><strong><a href=\"https://github.com/apache/spark\" target=\"_blank\" rel=\"noopener noreferrer\">Spark</a></strong> :Spark 是用于大规模数据处理的统一分析引擎。</li>\n<li><strong><a href=\"https://github.com/apache/flink\" title=\"flink\" target=\"_blank\" rel=\"noopener noreferrer\">Flink</a></strong>：Apache Flink 是一个框架和分布式处理引擎，用于在<em>无边界和有边界</em>数据流上进行有状态的计算。Flink 能在所有常见集群环境中运行，并能以内存速度和任意规模进行计算。</li>\n<li><strong><a href=\"https://hbase.apache.org/\" target=\"_blank\" rel=\"noopener noreferrer\">HBase</a></strong>：HBase – Hadoop Database，是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统，利用 HBase 技术可在廉价 PC Server 上搭建起大规模结构化存储集群。</li>\n<li><strong><a href=\"https://flume.apache.org/\" target=\"_blank\" rel=\"noopener noreferrer\">Flume</a></strong> :Apache Flume 是一个分布式的、可靠的、可用的，从多种不同的源收集、聚集、移动大量日志数据到集中数据存储的系统。</li>\n<li><strong><a href=\"https://storm.apache.org/\" target=\"_blank\" rel=\"noopener noreferrer\">Storm</a></strong> : 一个分布式，高容错的实时计算系统。</li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "开源项目"
      ]
    },
    {
      "title": "Java 优质开源 AI 项目",
      "url": "https://codingmap.strova.top/open-source-project/machine-learning.html",
      "id": "https://codingmap.strova.top/open-source-project/machine-learning.html",
      "summary": "Java优质开源AI项目推荐，涵盖Spring AI、LangChain4j、Deeplearning4j等Java人工智能和机器学习框架介绍。重点围绕 基础框架、实战 等内容展开。结合 CodingMap 知识体系梳理 开源项目 / Machine Learning.Html 的核心概念、实践方法等核心内容。",
      "content_html": "<p>很多小伙伴私下问我：现在 AI 这么火，咱们写 Java 的是不是只能在旁边看戏？</p>\n<p><strong>说实话，以前确实有点难受。</strong> 毕竟主流的 AI 框架大多是 Python 的天下。但现在，时代变了！随着 Spring AI 以及各种 Java AI 框架的爆发，咱们 Java 开发者完全可以像平时写 CRUD 一样，优雅地把大模型集成到应用里。</p>\n<p>今天就带大家盘点一下，目前 Java 生态里最硬核的几个 AI 框架。</p>\n<h2>基础框架</h2>\n<h3>Spring AI</h3>\n<p><a href=\"https://github.com/spring-projects/spring-ai\" target=\"_blank\" rel=\"noopener noreferrer\">Spring AI</a> 是 Spring 官方亲自下场打造的 AI 应用开发框架 。它的核心哲学非常直观：<strong>将 AI 能力无缝集成到 Spring 生态中</strong> 。</p>\n<p>对于习惯了 Spring Boot 的开发者来说，这玩意儿几乎没有学习门槛。它提供了一套构建 AI 应用所需的“底层原子能力抽象” ：</p>\n<ul>\n<li><strong>模型通信 (ChatClient):</strong> 提供了统一的接口与不同的大语言模型（如 OpenAI GPT、Ollama、Google Gemini）进行对话。</li>\n<li><strong>提示词 (Prompt):</strong> 结构化地管理和构建发送给模型的提示词。</li>\n<li><strong>检索增强生成 (RAG):</strong> 通过 <code>VectorStore</code> 等抽象，方便地实现 RAG 模式，将外部知识库与模型结合，提升回答的准确性和时效性。</li>\n<li><strong>工具调用 (Function Calling):</strong> 允许模型调用 Java 应用中定义好的方法，实现与外部世界的交互。</li>\n<li><strong>记忆 (ChatMemory):</strong> 管理多轮对话的上下文历史。</li>\n</ul>\n<p>官方文档：<a href=\"https://spring.io/projects/spring-ai#learn\" target=\"_blank\" rel=\"noopener noreferrer\">https://spring.io/projects/spring-ai#learn</a>。</p>\n<h3>Spring AI Alibaba</h3>\n<p><a href=\"https://github.com/alibaba/spring-ai-alibaba\" target=\"_blank\" rel=\"noopener noreferrer\">Spring AI Alibaba</a> 集成 Spring AI 生态，它是一个专为多智能体系统和工作流编排设计的项目。项目从架构上包含如下三层：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/open-source-project/ai/springai-alibaba-architecture-new.png\" alt=\"Spring AI Alibaba 架构\"></p>\n<ul>\n<li><strong>Agent Framework</strong>：以 ReactAgent 设计理念为核心的 Agent 开发框架，构建具备自动上下文工程和人机交互能力的 Agent。</li>\n<li><strong>Graph</strong>：低级别的工作流和多代理协调框架，是 Agent Framework 的底层运行时基座，帮助实现复杂的应用程序编排。</li>\n<li><strong>Augmented LLM</strong>：基于 Spring AI 底层抽象，提供模型、工具、多模态组件（MCP）、向量存储等基础支持。</li>\n</ul>\n<p>另外它还有非常“工程化”的组件：</p>\n<ul>\n<li><strong>Admin</strong>：一站式 Agent 平台，支持可视化开发、可观测、评估、MCP 管理，甚至与 Dify 等低代码平台集成，支持 DSL 迁移。</li>\n<li><strong>A2A（Agent-to-Agent）</strong>：支持 Agent 间通信，并可与 Nacos 集成做分布式协调。</li>\n</ul>\n<p>官方文档：<a href=\"https://java2ai.com/\" target=\"_blank\" rel=\"noopener noreferrer\">https://java2ai.com/</a>。</p>\n<h3>LangChain4j</h3>\n<p>如果说 Spring AI 是官方正规军，那 <a href=\"https://github.com/langchain4j/langchain4j\" target=\"_blank\" rel=\"noopener noreferrer\">LangChain4j</a> 就是目前社区里非常强势的 Java LLM 框架，它是 LangChain 的 Java 版本。</p>\n<p>它的优势在于功能全面，各种大模型的适配速度快得离谱，但在 Spring 体系里总有一种“外来客”的违和感。</p>\n<p>如果你追求“多模型快速切换 + 能力覆盖面广 + 原型推进快”，LangChain4j 通常是第一梯队选择；代价是你需要自己在工程结构、治理、可观测、平台化上多做一点“工程化拼装”。</p>\n<p>官方文档：<a href=\"https://docs.langchain4j.dev/\" target=\"_blank\" rel=\"noopener noreferrer\">https://docs.langchain4j.dev/</a>。</p>\n<h3>AgentScope</h3>\n<p><a href=\"https://github.com/agentscope-ai/agentscope-java\" target=\"_blank\" rel=\"noopener noreferrer\">AgentScope</a> 是一个多智能体框架，旨在提供一种简单高效的方式来构建基于大语言模型的智能体应用程序。</p>\n<p>如果说大模型（LLM）是 AI 应用的大脑，那么 AgentScope 就是它的“中枢神经系统”和“手脚”。它不仅提供了多智能体协作的架构，还内置了 ReAct 推理、工具调用、记忆管理等核心能力。</p>\n<p>AgentScope 提供了 Python 和 Java 版本，二者核心能力完全对齐！</p>\n<p><strong>AgentScope 也是阿里开源的，那和 Spring AI Alibaba 有何不同呢？</strong></p>\n<ul>\n<li><strong>AgentScope Java</strong>：原生为 <strong>Agentic（智能体）范式</strong>设计。它的核心是“Agent”，强调的是自主性、推理循环（ReAct）和多智能体之间的复杂博弈与协作。</li>\n<li><strong>Spring AI Alibaba</strong>：更侧重于 <strong>Workflow（工作流）编排</strong>。它基于 Spring AI 生态，擅长将 AI 能力作为工具融入到预定义的业务流中。</li>\n</ul>\n<p>官方文档：<a href=\"https://java.agentscope.io/zh/intro.html\" target=\"_blank\" rel=\"noopener noreferrer\">https://java.agentscope.io/zh/intro.html</a>。</p>\n<h3>其他</h3>\n<ul>\n<li><a href=\"https://github.com/opensolon/solon-ai\" target=\"_blank\" rel=\"noopener noreferrer\">Solon-AI</a>：Java AI 应用开发框架（支持 LLM，RAG，MCP，Agent），同时兼容 Java8 ~ Java25，支持 SpringBoot、jFinal、Vert.x、Quarkus 等框架。</li>\n<li><a href=\"https://github.com/agents-flex/agents-flex\" target=\"_blank\" rel=\"noopener noreferrer\">Agent-Flex</a>：一个优雅的 LLM（大语言模型）应用开发框架，对标 LangChain、使用 Java 开发、简单、轻量。</li>\n<li><a href=\"https://github.com/eclipse/deeplearning4j\" target=\"_blank\" rel=\"noopener noreferrer\">Deeplearning4j</a>：Deeplearning4j 是第一个为 Java 和 Scala 编写的商业级，开源，分布式深度学习库。</li>\n<li><a href=\"https://github.com/haifengl/smile\" target=\"_blank\" rel=\"noopener noreferrer\">Smile</a>：基于 Java 和 Scala 的机器学习库。</li>\n<li><a href=\"https://github.com/libgdx/gdx-ai\" target=\"_blank\" rel=\"noopener noreferrer\">GdxAI</a>：完全用 Java 编写的人工智能框架，用于使用 libGDX 进行游戏开发。</li>\n</ul>\n<h3>对比</h3>\n<p>| <strong>框架名称</strong>          | <strong>核心特点</strong>                                                                                                       | <strong>适用场景</strong>                                               |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/open-source-project/ai/springai-alibaba-architecture-new.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "开源项目"
      ]
    },
    {
      "title": "Java 优质开源实战项目",
      "url": "https://codingmap.strova.top/open-source-project/practical-project.html",
      "id": "https://codingmap.strova.top/open-source-project/practical-project.html",
      "summary": "Java优质开源实战项目推荐，涵盖快速开发平台、电商系统、权限管理等可用于学习和简历的实战项目精选。重点围绕 AI、快速开发平台、博客/论坛系统、Wiki/文档系统 等内容展开。结合 CodingMap 知识体系梳理 开源项目 / Practical Project.Html 的核心概念、实践方法等核心内容。",
      "content_html": "<h2>AI</h2>\n<ul>\n<li><a href=\"https://github.com/Snailclimb/interview-guide\" target=\"_blank\" rel=\"noopener noreferrer\">interview-guide</a>：基于 Spring Boot 4.0 + Java 21 + Spring AI + PostgreSQL + pgvector + RustFS + Redis，实现简历智能分析、AI 模拟面试、知识库 RAG 检索等核心功能。非常适合作为学习和简历项目，学习门槛低。</li>\n<li><a href=\"https://github.com/itwanger/PaiAgent\" target=\"_blank\" rel=\"noopener noreferrer\">PaiAgent</a>：一个企业级的 AI 工作流可视化编排平台，让 AI 能力的组合和调度变得简单高效。通过直观的拖拽式界面，开发者和业务人员都能快速构建复杂的 AI 处理流程，无需编写代码即可实现多种大模型的协同工作。</li>\n</ul>\n<h2>快速开发平台</h2>\n<ul>\n<li><a href=\"https://gitee.com/xiaonuobase/snowy\" target=\"_blank\" rel=\"noopener noreferrer\">Snowy</a>：国内首个国密前后端分离快速开发平台。详细介绍：<a href=\"https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&amp;mid=2247534316&amp;idx=1&amp;sn=69938397674fc33ecda43c8c9d0a4039&amp;chksm=cea10927f9d68031bc862485c6be984ade5af233d4d871d498c38f22164a84314678c0c67cd7&amp;token=1464380539&amp;lang=zh_CN#rd\" target=\"_blank\" rel=\"noopener noreferrer\">5.1k！这是我见过最强的前后端分离快速开发脚手架！！</a>。</li>\n<li><a href=\"https://github.com/elunez/eladmin\" target=\"_blank\" rel=\"noopener noreferrer\">eladmin</a> : 前后端分离的后台管理系统，项目采用分模块开发方式， 权限控制采用 RBAC，支持数据字典与数据权限管理，支持一键生成前后端代码，支持动态路由。</li>\n<li><a href=\"https://gitee.com/y_project/RuoYi\" target=\"_blank\" rel=\"noopener noreferrer\">RuoYi</a>：RuoYi 一款基于基于 SpringBoot 的权限管理系统 易读易懂、界面简洁美观，直接运行即可用 。</li>\n<li><a href=\"https://github.com/valarchie/AgileBoot-Back-End\" target=\"_blank\" rel=\"noopener noreferrer\">AgileBoot-Back-End</a>：基于 Ruoyi 做了大量重构优化的基础快速开发框架。</li>\n<li><a href=\"https://gitee.com/lab1024/smart-admin\" target=\"_blank\" rel=\"noopener noreferrer\">SmartAdmin</a> : 一套简洁、易用的低代码中后台解决方案。</li>\n<li><a href=\"https://gitee.com/zhaoeryu/eu-backend\" target=\"_blank\" rel=\"noopener noreferrer\">EuBackend</a>：基于 SpringBoot 开发的轻量级快速开发平台。</li>\n<li><a href=\"https://github.com/YunaiV/ruoyi-vue-pro\" target=\"_blank\" rel=\"noopener noreferrer\">RuoYi-Vue-Pro</a>：RuoYi-Vue 全新 Pro 版本，优化重构所有功能，支持数据权限、SaaS 多租户、Flowable 工作流、三方登录、支付等功能。</li>\n<li><a href=\"https://gitee.com/dromara/RuoYi-Vue-Plus\" target=\"_blank\" rel=\"noopener noreferrer\">RuoYi-Vue-Plus</a>：RuoYi-Vue 全新 Plus 版本，重写了 RuoYi-Vue 所有功能，集成了 Sa-Token、Mybatis-Plus、Jackson、SpringDoc、Hutool、OSS 定期同步等。</li>\n<li><a href=\"https://gitee.com/log4j/pig\" title=\"pig\" target=\"_blank\" rel=\"noopener noreferrer\">pig</a>：基于 Spring Boot + Spring Cloud + OAuth2 的 RBAC 权限管理系统。</li>\n<li><a href=\"https://gitee.com/stylefeng/guns\" target=\"_blank\" rel=\"noopener noreferrer\">Guns</a>：现代化的 Java 应用开发基础框架。</li>\n<li><a href=\"https://github.com/zhangdaiscott/jeecg-boot\" target=\"_blank\" rel=\"noopener noreferrer\">JeecgBoot</a>：一款基于代码生成器的 J2EE 低代码快速开发平台，支持生成前后端分离架构的项目。</li>\n<li><a href=\"https://gitee.com/erupt/erupt\" target=\"_blank\" rel=\"noopener noreferrer\">Erupt</a> : 低代码全栈类框架，它使用 Java 注解 动态生成页面以及增、删、改、查、权限控制等后台功能。</li>\n<li><a href=\"https://github.com/ballcat-projects/ballcat\" target=\"_blank\" rel=\"noopener noreferrer\">BallCat</a>：一个功能完善的快速开发脚手架！除了最基本的权限管理，定时任务功能之外，还额外支持 XSS 过滤，SQL 防注入、数据脱敏等多种功能</li>\n<li><a href=\"https://github.com/jhipster/generator-jhipster\" target=\"_blank\" rel=\"noopener noreferrer\">JHipster</a> :开源应用程序平台，可在几秒钟内创建 Spring Boot + Angular / React 项目。</li>\n</ul>\n<h2>博客/论坛系统</h2>\n<p>下面这几个项目都是非常适合 Spring Boot 初学者学习的，下面的大部分项目的总体代码架构我都看过，个人觉得还算不错，不会误导没有实际做过项目的朋友。</p>\n<ul>\n<li><a href=\"https://github.com/itwanger/paicoding\" target=\"_blank\" rel=\"noopener noreferrer\">paicoding</a>：一款好用又强大的开源社区，基于 Spring Boot 系列主流技术栈，附详细的教程。</li>\n<li><a href=\"https://github.com/rymcu/forest\" target=\"_blank\" rel=\"noopener noreferrer\">forest</a>：下一代的知识社区系统，可以自定义专题和作品集。后端基于 SpringBoot + Shrio + MyBatis + JWT + Redis，前端基于 Vue + NuxtJS + Element-UI。</li>\n<li><a href=\"https://github.com/codedrinker/community\" target=\"_blank\" rel=\"noopener noreferrer\">community</a>：开源论坛、问答系统，现有功能提问、回复、通知、最新、最热、消除零回复功能。功能持续更新中…… 技术栈 Spring、Spring Boot、MyBatis、MySQL/H2、Bootstrap。</li>\n<li><a href=\"https://gitee.com/yadong.zhang/DBlog\" target=\"_blank\" rel=\"noopener noreferrer\">OneBlog</a>：简洁美观、功能强大并且自适应的博客系统，支持广告位、SEO、实时通讯等功能。</li>\n<li><a href=\"https://github.com/lenve/VBlog\" target=\"_blank\" rel=\"noopener noreferrer\">VBlog</a>：V 部落，Vue+SpringBoot 实现的多用户博客管理平台!</li>\n<li><a href=\"https://github.com/ZHENFENG13/My-Blog\" target=\"_blank\" rel=\"noopener noreferrer\">My-Blog</a>： SpringBoot + Mybatis + Thymeleaf 等技术实现的 Java 博客系统，页面美观、功能齐全、部署简单及完善的代码，一定会给使用者无与伦比的体验。</li>\n</ul>\n<h2>Wiki/文档系统</h2>\n<ul>\n<li><a href=\"https://gitee.com/dromara/zyplayer-doc\" target=\"_blank\" rel=\"noopener noreferrer\">zyplayer-doc</a>：适合团队和个人私有化部署使用的知识库、笔记、WIKI 文档管理工具，同时还包含数据库管理、Api 接口管理等模块。</li>\n<li><a href=\"https://gitee.com/kekingcn/file-online-preview\" target=\"_blank\" rel=\"noopener noreferrer\">kkFileView</a>：文档在线预览解决方案，支持几乎所有主流文档格式预览，例如 doc、docx、ppt、pptx、wps、xls、xlsx、zip、rar、ofd、xmind、bpmn 、eml 、epub、3ds、dwg、psd 、mp4、mp3 等等。</li>\n</ul>\n<h2>文件管理系统/网盘</h2>\n<ul>\n<li><a href=\"https://gitee.com/SnailClimb/cloud-drive\" target=\"_blank\" rel=\"noopener noreferrer\">cloud-drive</a>：一个极简的现代化云存储系统，基于阿里云 OSS，提供文件上传、下载、分享等功能。系统采用前后端分离架构，提供安全可靠的文件存储服务。</li>\n<li><a href=\"https://gitee.com/qiwen-cloud/qiwen-file\" target=\"_blank\" rel=\"noopener noreferrer\">qiwen-file</a>：基于 SpringBoot+Vue 实现的分布式文件系统，支持本地磁盘、阿里云 OSS 对象存储、FastDFS 存储、MinIO 存储等多种存储方式，支持 office 在线编辑、分片上传、技术秒传、断点续传等功能。</li>\n<li><a href=\"https://gitee.com/dh_free/free-fs\" target=\"_blank\" rel=\"noopener noreferrer\">free-fs</a>：基于 SpringBoot + MyBatis Plus + MySQL + Sa-Token + Layui 等搭配七牛云， 阿里云 OSS 实现的云存储管理系统。 包含文件上传、删除、在线预览、云资源列表查询、下载、文件移动、重命名、目录管理、登录、注册、以及权限控制等功能。</li>\n<li><a href=\"https://github.com/zfile-dev/zfile\" target=\"_blank\" rel=\"noopener noreferrer\">zfile</a>：基于 Spring Boot + Vue 实现的在线网盘，支持对接 S3、OneDrive、SharePoint、Google Drive、多吉云、又拍云、本地存储、FTP、SFTP 等存储源，支持在线浏览图片、播放音视频，文本文件、Office、obj（3d）等文件类型。</li>\n</ul>\n<h2>考试/刷题系统</h2>\n<ul>\n<li><a href=\"https://github.com/PlayEdu/PlayEdu\" target=\"_blank\" rel=\"noopener noreferrer\">PlayEdu</a>：一款适用于搭建内部培训平台的开源系统，旨在为企业/机构打造自己品牌的内部培训平台。</li>\n<li><a href=\"https://gitee.com/himitzh0730/hoj\" target=\"_blank\" rel=\"noopener noreferrer\">HOJ</a>：分布式架构的在线测评平台 OJ ，功能非常全面，支持刷题、训练、比赛、评测等功能。</li>\n<li><a href=\"https://github.com/simplefanC/voj\" target=\"_blank\" rel=\"noopener noreferrer\">VOJ</a>：基于微服务架构的高性能在线评测系统。拥有本地判题服务，同时支持其它知名 OJ (HDU、POJ...) 的远程判题。采用现阶段流行技术实现，采用 Docker 容器化部署。</li>\n<li><a href=\"https://github.com/SDUOJ/OnlineJudge\" target=\"_blank\" rel=\"noopener noreferrer\">OnlineJudge</a>：基于微服务架构的在线评测系统，支持多种国际赛制支持（ICPC/OI/IOI），采用 Docker 容器化部署。</li>\n<li><a href=\"https://gitee.com/wells2333/sg-exam\" target=\"_blank\" rel=\"noopener noreferrer\">sg-exam</a>：方便易用、高颜值的教学管理平台，提供多租户、权限管理、考试、练习、在线学习等功能。</li>\n<li><a href=\"https://gitee.com/mindskip/uexam\" target=\"_blank\" rel=\"noopener noreferrer\">uexam</a>：功能全面的在线考试系统，开发部署简单快捷、界面设计友好、代码结构清晰。相关阅读：<a href=\"http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzg2OTA0Njk0OA%3D%3D%26mid%3D2247491585%26idx%3D1%26sn%3D8d3c6768c22e72d6bfcbeee9624886a7%26chksm%3Dcea1afcaf9d626dc918760289c37025ad526f6255786bc198d2402203df64c873ad7934f58df%26scene%3D178%26cur_album_id%3D1345382825083895808%23rd\" target=\"_blank\" rel=\"noopener noreferrer\">好一个 Spring Boot 开源在线考试系统！解决了我的燃眉之急</a> 。</li>\n<li><a href=\"https://github.com/Jackson0714/PassJava-Platform\" target=\"_blank\" rel=\"noopener noreferrer\">PassJava-Platform</a>：基于微服务架构的面试刷题小程序！相关阅读：<a href=\"http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzg2OTA0Njk0OA%3D%3D%26mid%3D2247497045%26idx%3D1%26sn%3D577175bfd6c040a0df5a494fce6f9758%26chksm%3Dcea1ba9ef9d633883a2e213c0fb9a88bdc87051347d4b3fad2c2befb65d8b16e1ea81d8146dd%26scene%3D178%26cur_album_id%3D1345382825083895808%23rd\" target=\"_blank\" rel=\"noopener noreferrer\">一个基于 Spring Cloud 的面试刷题系统。面试、毕设、项目经验一网打尽</a>。</li>\n</ul>\n<h2>商城系统</h2>\n<p>下面的商城系统大多比较复杂比如 mall ,如果没有 Java 基础和 Spring Boot 都还没有摸熟的话不推荐过度研究下面几个项目或者使用这些项目当作毕业设计。</p>\n<ul>\n<li><a href=\"https://gitee.com/nageoffer/congomall\" target=\"_blank\" rel=\"noopener noreferrer\">congomall</a>：不一样的 TOC 商城系统，SpringCloud-Alibaba 微服务架构设计，基于 DDD 领域驱动模型开发，代码设计优雅，涵盖商城核心业务。</li>\n<li><a href=\"https://github.com/macrozheng/mall\" title=\"mall\" target=\"_blank\" rel=\"noopener noreferrer\">mall</a>：mall 项目是一套电商系统，包括前台商城系统及后台管理系统，基于 SpringBoot+MyBatis 实现。</li>\n<li><a href=\"https://github.com/macrozheng/mall-swarm\" title=\"mall-swarm\" target=\"_blank\" rel=\"noopener noreferrer\">mall-swarm</a> : mall-swarm 是一套微服务商城系统，采用了 Spring Cloud Greenwich、Spring Boot 2、MyBatis、Docker、Elasticsearch 等核心技术，同时提供了基于 Vue 的管理后台方便快速搭建系统。</li>\n<li><a href=\"https://github.com/linlinjava/litemall\" title=\"litemall\" target=\"_blank\" rel=\"noopener noreferrer\">litemall</a>：又一个小商城。litemall = Spring Boot 后端 + Vue 管理员前端 + 微信小程序用户前端 + Vue 用户移动端。</li>\n<li><a href=\"https://github.com/newbee-ltd/newbee-mall\" target=\"_blank\" rel=\"noopener noreferrer\">newbee-mall</a> :newbee-mall 项目（新蜂商城）是一套电商系统，包括 newbee-mall 商城系统及 newbee-mall-admin 商城后台管理系统，基于 Spring Boot 2.X 及相关技术栈开发。</li>\n</ul>\n<h2>售票系统</h2>\n<ul>\n<li><a href=\"https://gitee.com/nageoffer/12306\" target=\"_blank\" rel=\"noopener noreferrer\">12306</a> ：基于 JDK17 + SpringBoot3 + SpringCloud 微服务架构的高并发 12306 购票服务。</li>\n<li><a href=\"https://gitee.com/java-up-up/damai\" target=\"_blank\" rel=\"noopener noreferrer\">大麦</a>：提供热门演唱会的购票功能，并且对如何解决高并发下的抢票而产生的各种问题，从而设计出了实际落地的解决方案。</li>\n</ul>\n<h2>造轮子</h2>\n<ul>\n<li><a href=\"https://github.com/Snailclimb/guide-rpc-framework\" target=\"_blank\" rel=\"noopener noreferrer\">guide-rpc-framework</a>：一款基于 Netty+Kyro+Zookeeper 实现的自定义 RPC 框架-附详细实现过程和相关教程。</li>\n<li><a href=\"https://github.com/DerekYRC/mini-spring\" target=\"_blank\" rel=\"noopener noreferrer\">mini-spring</a>：简化版的 Spring 框架，能帮助你快速熟悉 Spring 源码和掌握 Spring 的核心原理。代码极度简化，保留了 Spring 的核心功能，如 IoC 和 AOP、资源加载器等核心功能。</li>\n<li><a href=\"https://github.com/DerekYRC/mini-spring-cloud\" target=\"_blank\" rel=\"noopener noreferrer\">mini-spring-cloud</a>：一个手写的简化版的 Spring Cloud，旨在帮助你快速熟悉 Spring Cloud 源码及掌握其核心原理。相关阅读：<a href=\"https://mp.weixin.qq.com/s/v3FUp-keswE2EhcTaLpSMQ\" target=\"_blank\" rel=\"noopener noreferrer\">手写一个简化版的 Spring Cloud！</a> 。</li>\n<li><a href=\"https://github.com/FranzHaidnor/haidnorJVM\" target=\"_blank\" rel=\"noopener noreferrer\">haidnorJVM</a>：使用 Java 实现的简易版 Java 虚拟机，介绍：<a href=\"https://www.zhihu.com/question/28125278/answer/3137240457\" target=\"_blank\" rel=\"noopener noreferrer\">https://www.zhihu.com/question/28125278/answer/3137240457</a>。</li>\n<li><a href=\"https://github.com/fuzhengwei/itstack-demo-jvm\" target=\"_blank\" rel=\"noopener noreferrer\">itstack-demo-jvm</a>：通过 Java 代码来实现 JVM 的基础功能（搜索解析 class 文件、字节码命令、运行时数据区等。相关阅读：<a href=\"https://zachaxy.github.io/tags/JVM/\" target=\"_blank\" rel=\"noopener noreferrer\">《zachaxy 的手写 JVM 系列》</a>。</li>\n<li><a href=\"https://github.com/alchemystar/Freedom\" target=\"_blank\" rel=\"noopener noreferrer\">Freedom</a>：自己 DIY 一个具有 ACID 的数据库。相关项目：<a href=\"https://github.com/CN-GuoZiyang/MYDB\" target=\"_blank\" rel=\"noopener noreferrer\">MYDB</a>（一个简单的数据库实现）、<a href=\"https://github.com/erikgrinaker/toydb\" target=\"_blank\" rel=\"noopener noreferrer\">toyDB</a>（Rust 实现的分布式 SQL 数据库）。</li>\n<li><a href=\"https://github.com/stateIs0/lu-raft-kv\" target=\"_blank\" rel=\"noopener noreferrer\">lu-raft-kv</a>：一个 Java 版本的 Raft(CP) KV 分布式存储实现，非常适合想要深入学习 Raft 协议的小伙伴研究。lu-raft-kv 已经实现了 Raft 协议其中的两个核心功能：leader 选举和日志复制。如果你想要学习这个项目的话，建议你提前看一下作者写的项目介绍，比较详细，地址：<a href=\"http://thinkinjava.cn/2019/01/12/2019/2019-01-12-lu-raft-kv/\" target=\"_blank\" rel=\"noopener noreferrer\">http://thinkinjava.cn/2019/01/12/2019/2019-01-12-lu-raft-kv/</a> 。</li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "开源项目"
      ]
    },
    {
      "title": "Java 优质开源系统设计项目",
      "url": "https://codingmap.strova.top/open-source-project/system-design.html",
      "id": "https://codingmap.strova.top/open-source-project/system-design.html",
      "summary": "Java优质开源系统设计项目推荐，涵盖Web框架、微服务、消息队列、搜索引擎、数据库等基础架构组件精选。重点围绕 基础框架、认证授权、网络通讯、数据库 等内容展开。结合 CodingMap 知识体系梳理 开源项目 / System Design.Html 的核心概念、实践方法、常见问题和高频面试考点，覆盖原理分析等核。",
      "content_html": "<h2>基础框架</h2>\n<h3>Web 框架</h3>\n<ul>\n<li><a href=\"https://github.com/spring-projects/spring-boot\" title=\"spring-boot\" target=\"_blank\" rel=\"noopener noreferrer\">Spring Boot</a>：Spring Boot 可以轻松创建独立的生产级基于 Spring 的应用程序，内置 web 服务器让你可以像运行普通 Java 程序一样运行项 目。另外，大部分 Spring Boot 项目只需要少量的配置即可，这有别于 Spring 的重配置。</li>\n<li><a href=\"https://github.com/sofastack/sofa-boot\" target=\"_blank\" rel=\"noopener noreferrer\">SOFABoot</a>：SOFABoot 基于 Spring Boot ，不过在其基础上增加了 Readiness Check，类隔离，日志空间隔离等等能力。 配套提供的还有：SOFARPC（RPC 框架）、SOFABolt（基于 Netty 的远程通信框架）、SOFARegistry（注册中心）...详情请参考：<a href=\"https://github.com/sofastack\" target=\"_blank\" rel=\"noopener noreferrer\">SOFAStack</a> 。</li>\n<li><a href=\"https://gitee.com/opensolon/solon\" target=\"_blank\" rel=\"noopener noreferrer\">Solon</a>：国产面向全场景的 Java 企业级应用开发框架。</li>\n<li><a href=\"https://github.com/tipsy/javalin\" target=\"_blank\" rel=\"noopener noreferrer\">Javalin</a>：一个轻量级的 Web 框架，同时支持 Java 和 Kotlin，被微软、红帽、Uber 等公司使用。</li>\n<li><a href=\"https://github.com/playframework/playframework\" target=\"_blank\" rel=\"noopener noreferrer\">Play Framework</a>：面向 Java 和 Scala 的高速 Web 框架。</li>\n<li><a href=\"https://github.com/lets-blade/blade\" target=\"_blank\" rel=\"noopener noreferrer\">Blade</a>：一款追求简约、高效的 Web 框架，基于 Java8 + Netty4。</li>\n</ul>\n<h3>微服务/云原生</h3>\n<ul>\n<li><a href=\"https://github.com/line/armeria\" target=\"_blank\" rel=\"noopener noreferrer\">Armeria</a>：适合任何情况的微服务框架。你可以用你喜欢的技术构建任何类型的微服务，包括<a href=\"https://grpc.io/\" target=\"_blank\" rel=\"noopener noreferrer\">gRPC</a>、 <a href=\"https://thrift.apache.org/\" target=\"_blank\" rel=\"noopener noreferrer\">Thrift</a>、<a href=\"https://kotlinlang.org/\" target=\"_blank\" rel=\"noopener noreferrer\">Kotlin</a>、 <a href=\"https://square.github.io/retrofit/\" target=\"_blank\" rel=\"noopener noreferrer\">Retrofit</a>、<a href=\"https://www.reactive-streams.org/\" target=\"_blank\" rel=\"noopener noreferrer\">Reactive Streams</a>、 <a href=\"https://spring.io/projects/spring-boot\" target=\"_blank\" rel=\"noopener noreferrer\">Spring Boot</a>和<a href=\"https://www.dropwizard.io/\" target=\"_blank\" rel=\"noopener noreferrer\">Dropwizard</a></li>\n<li><a href=\"https://github.com/quarkusio/quarkus\" target=\"_blank\" rel=\"noopener noreferrer\">Quarkus</a> : 用于编写 Java 应用程序的云原生和容器优先的框架。</li>\n<li><a href=\"https://github.com/helidon-io/helidon\" target=\"_blank\" rel=\"noopener noreferrer\">Helidon</a>：一组用于编写微服务的 Java 库，支持 Helidon MP 和 Helidon SE 两种编程模型。</li>\n</ul>\n<h3>API 文档</h3>\n<ul>\n<li><a href=\"https://swagger.io/\" target=\"_blank\" rel=\"noopener noreferrer\">Swagger</a> ：较主流的 RESTful 风格的 API 文档工具，提供了一套工具和规范，让开发人员能够更轻松地创建和维护可读性强、易于使用和交互的 API 文档。</li>\n<li><a href=\"https://doc.xiaominfo.com/\" target=\"_blank\" rel=\"noopener noreferrer\">Knife4j</a>：集 Swagger2 和 OpenAPI3 为一体的增强解决方案。</li>\n</ul>\n<h3>Bean 映射</h3>\n<ul>\n<li><a href=\"https://github.com/mapstruct/mapstruct\" target=\"_blank\" rel=\"noopener noreferrer\">MapStruct</a>（推荐）：满足 JSR269 规范的一个 Java 注解处理器，用于为 Java Bean 生成类型安全且高性能的映射。它基于编译阶段生成 get/set 代码，此实现过程中没有反射，不会造成额外的性能损失。</li>\n<li><a href=\"https://github.com/linpeilie/mapstruct-plus\" target=\"_blank\" rel=\"noopener noreferrer\">MapStruct Plus</a>：MapStruct 增强版本，支持自动生成 Mapper 接口。</li>\n<li><a href=\"https://github.com/jmapper-framework/jmapper-core\" target=\"_blank\" rel=\"noopener noreferrer\">JMapper</a> : 一个高性能且易于使用的 Bean 映射框架。</li>\n</ul>\n<h3>其他</h3>\n<ul>\n<li><a href=\"https://github.com/google/guice\" target=\"_blank\" rel=\"noopener noreferrer\">Guice</a>：Google 开源的一个轻量级依赖注入框架，相当于一个功能极简化的轻量级 Spring Boot。在某些情况下非常实用，就比如说我们的项目只需要使用依赖注入，不需要 AOP 等功能特性。</li>\n<li><a href=\"https://github.com/spring-projects/spring-batch\" target=\"_blank\" rel=\"noopener noreferrer\">Spring Batch</a> : Spring Batch 是一个轻量级但功能又十分全面的批处理框架，主要用于批处理场景比如从数据库、文件或队列中读取大量记录。不过，需要注意的是：Spring Batch 不是调度框架。商业和开源领域都有许多优秀的企业调度框架比如 Quartz、XXL-JOB、Elastic-Job。它旨在与调度程序一起工作，而不是取代调度程序。</li>\n</ul>\n<h2>认证授权</h2>\n<h3>权限认证</h3>\n<ul>\n<li><a href=\"https://github.com/dromara/sa-token\" target=\"_blank\" rel=\"noopener noreferrer\">Sa-Token</a>：轻量级 Java 权限认证框架。支持认证授权、单点登录、踢人下线、自动续签等功能。相比于 Spring Security 和 Shiro 来说，Sa-Token 内置的开箱即用的功能更多，使用也更简单。</li>\n<li><a href=\"https://github.com/spring-projects/spring-security\" target=\"_blank\" rel=\"noopener noreferrer\">Spring Security</a>：Spring 官方安全框架，能够用于身份验证、授权、加密和会话管理，是目前使用最广泛的 Java 安全框架。</li>\n<li><a href=\"https://github.com/apache/shiro\" target=\"_blank\" rel=\"noopener noreferrer\">Shiro</a>：Java 安全框架，功能和 Spring Security 类似，但使用起来更简单。</li>\n</ul>\n<h3>第三方登录</h3>\n<ul>\n<li><a href=\"https://github.com/Wechat-Group/WxJava\" target=\"_blank\" rel=\"noopener noreferrer\">WxJava</a> : WxJava （微信开发 Java SDK），支持包括微信支付、开放平台、小程序、企业微信/企业号和公众号等的后端开发。</li>\n<li><a href=\"https://github.com/justauth/JustAuth\" target=\"_blank\" rel=\"noopener noreferrer\">JustAuth</a>：小而全而美的第三方登录开源组件。目前已经集成了诸如：GitHub、Gitee、支付宝、新浪微博、微信、Google、Facebook、Twitter、StackOverflow 等国内外数十家第三方平台。</li>\n</ul>\n<h3>单点登录（SSO）</h3>\n<ul>\n<li><a href=\"https://github.com/apereo/cas\" target=\"_blank\" rel=\"noopener noreferrer\">CAS</a>：企业多语言网络单点登录解决方案。</li>\n<li><a href=\"https://gitee.com/dromara/MaxKey\" target=\"_blank\" rel=\"noopener noreferrer\">MaxKey</a>：单点登录认证系统，提供安全、标准和开放的用户身份管理(IDM)、身份认证(AM)、单点登录(SSO)、RBAC 权限管理和资源管理等。</li>\n<li><a href=\"https://github.com/keycloak/keycloak\" target=\"_blank\" rel=\"noopener noreferrer\">Keycloak</a>：免费、开源身份认证和访问管理系统，支持高度可配置的单点登录功能。</li>\n</ul>\n<h2>网络通讯</h2>\n<ul>\n<li><a href=\"https://github.com/netty/netty\" target=\"_blank\" rel=\"noopener noreferrer\">Netty</a> : 一个基于 NIO 的 client-server(客户端服务器)框架，使用它可以快速简单地开发网络应用程序。</li>\n<li><a href=\"https://github.com/square/retrofit\" target=\"_blank\" rel=\"noopener noreferrer\">Retrofit</a>：适用于 Android 和 Java 的类型安全的 HTTP 客户端。Retrofit 的 HTTP 请求使用的是 <a href=\"https://square.github.io/okhttp/\" target=\"_blank\" rel=\"noopener noreferrer\">OkHttp</a> 库（一款被广泛使用网络框架）。</li>\n<li><a href=\"https://gitee.com/dromara/forest\" target=\"_blank\" rel=\"noopener noreferrer\">Forest</a>：轻量级 HTTP 客户端 API 框架，让 Java 发送 HTTP/HTTPS 请求不再难。它比 OkHttp 和 HttpClient 更高层，是封装调用第三方 restful api client 接口的好帮手，是 retrofit 和 feign 之外另一个选择。</li>\n<li><a href=\"https://github.com/YeautyYE/netty-websocket-spring-boot-starter\" target=\"_blank\" rel=\"noopener noreferrer\">netty-websocket-spring-boot-starter</a> :帮助你在 Spring Boot 中使用 Netty 来开发 WebSocket 服务器，并像 spring-websocket 的注解开发一样简单。</li>\n</ul>\n<h2>数据库</h2>\n<h3>数据库连接池</h3>\n<ul>\n<li><a href=\"https://github.com/alibaba/druid\" target=\"_blank\" rel=\"noopener noreferrer\">Druid</a> : 阿里巴巴数据库事业部出品，为监控而生的数据库连接池。</li>\n<li><a href=\"https://github.com/brettwooldridge/HikariCP\" target=\"_blank\" rel=\"noopener noreferrer\">HikariCP</a> : 一个可靠的高性能 JDBC 连接池。Springboot 2.0 选择 HikariCP 作为默认数据库连接池。</li>\n</ul>\n<h3>数据库框架</h3>\n<ul>\n<li><a href=\"https://github.com/baomidou/mybatis-plus\" target=\"_blank\" rel=\"noopener noreferrer\">MyBatis-Plus</a> : <a href=\"http://www.mybatis.org/mybatis-3/\" target=\"_blank\" rel=\"noopener noreferrer\">MyBatis</a> 增强工具，在 MyBatis 的基础上只做增强不做改变，为简化开发、提高效率而生。</li>\n<li><a href=\"https://gitee.com/mybatis-flex/mybatis-flex\" target=\"_blank\" rel=\"noopener noreferrer\">MyBatis-Flex</a>：一个优雅的 MyBatis 增强框架，无其他任何第三方依赖，支持 CRUD、分页查询、多表查询、批量操作。</li>\n<li><a href=\"https://github.com/jOOQ/jOOQ\" target=\"_blank\" rel=\"noopener noreferrer\">jOOQ</a>：用 Java 编写 SQL 的最佳方式。</li>\n<li><a href=\"https://github.com/redisson/redisson\" title=\"redisson\" target=\"_blank\" rel=\"noopener noreferrer\">Redisson</a>：Redisson 是一款架设在 Redis 基础之上的 Java 驻内存数据网格 (In-Memory Data Grid)，它充分利用了 Redis 键值数据库的优势，为 Java 开发者提供了一系列具有分布式特性的常用工具类。例如，分布式 Java 对象（<code>Set</code>，<code>SortedSet</code>，<code>Map</code>，<code>List</code>，<code>Queue</code>，<code>Deque</code> 等）、分布式锁等。详细介绍请看：<a href=\"https://github.com/redisson/redisson/wiki/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D\" title=\"Redisson项目介绍\" target=\"_blank\" rel=\"noopener noreferrer\">Redisson 项目介绍</a>。</li>\n</ul>\n<h3>数据同步</h3>\n<ul>\n<li><a href=\"https://github.com/alibaba/canal\" title=\"canal\" target=\"_blank\" rel=\"noopener noreferrer\">Canal</a> [kə'næl] : Canal 译意为水道/管道/沟渠，主要用途是基于 MySQL 数据库增量日志解析，提供增量数据订阅和消费。</li>\n<li><a href=\"https://github.com/alibaba/DataX\" title=\"DataX\" target=\"_blank\" rel=\"noopener noreferrer\">DataX</a>：DataX 是阿里巴巴集团内被广泛使用的离线数据同步工具/平台，实现包括 MySQL、Oracle、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、DRDS 等各种异构数据源之间高效的数据同步功能。相关项目：<a href=\"https://github.com/WeiYe-Jing/datax-web\" target=\"_blank\" rel=\"noopener noreferrer\">DataX-Web</a> （DataX 集成可视化页面，选择数据源即可一键生成数据同步任务）。</li>\n</ul>\n<p>其他：<a href=\"https://github.com/DTStack/flinkx\" target=\"_blank\" rel=\"noopener noreferrer\">Flinkx</a> （基于 Flink 的分布式数据同步工具）。</p>\n<h3>时序数据库</h3>\n<ul>\n<li><a href=\"https://github.com/apache/iotdb\" target=\"_blank\" rel=\"noopener noreferrer\">IoTDB</a>：一款 Java 语言编写的国产时序数据库，为用户提供数据收集、存储和分析等服务。与 Hadoop、Spark 和可视化工具(如 Grafana)无缝集成，满足了工业 IoT 领域中海量数据存储、高吞吐量数据写入和复杂数据查询分析的需求。</li>\n<li><a href=\"https://github.com/kairosdb/kairosdb\" target=\"_blank\" rel=\"noopener noreferrer\">KairosDB</a>：一个基于 Cassandra 的快速分布式可扩展时间序列数据库。</li>\n</ul>\n<h2>搜索引擎</h2>\n<ul>\n<li><a href=\"https://github.com/elastic/elasticsearch\" title=\"elasticsearch\" target=\"_blank\" rel=\"noopener noreferrer\">Elasticsearch</a> （推荐）：开源，分布式，RESTful 搜索引擎。</li>\n<li><a href=\"https://github.com/meilisearch/meilisearch\" target=\"_blank\" rel=\"noopener noreferrer\">Meilisearch</a>：一个功能强大、快速、开源、易于使用和部署的搜索引擎，支持中文搜索（不需要添加额外的配置）。</li>\n<li><a href=\"https://lucene.apache.org/solr/\" target=\"_blank\" rel=\"noopener noreferrer\">Solr</a> : Solr（读作“solar”）是 Apache Lucene 项目的开源企业搜索平台。</li>\n<li><a href=\"https://gitee.com/dromara/easy-es\" target=\"_blank\" rel=\"noopener noreferrer\">Easy-ES</a>：傻瓜级 ElasticSearch 搜索引擎 ORM 框架。</li>\n</ul>\n<h2>测试</h2>\n<h3>测试框架</h3>\n<ul>\n<li><a href=\"http://junit.org/\" target=\"_blank\" rel=\"noopener noreferrer\">JUnit</a> : Java 测试框架。</li>\n<li><a href=\"https://github.com/mockito/mockito\" target=\"_blank\" rel=\"noopener noreferrer\">Mockito</a>：Mockito 是一个模拟测试框架，可以让你用优雅，简洁的接口写出漂亮的单元测试。（对那些不容易构建的对象用一个虚拟对象来代替，使其在调试期间用来作为真实对象的替代品）</li>\n<li><a href=\"https://github.com/powermock/powermock\" target=\"_blank\" rel=\"noopener noreferrer\">PowerMock</a>：编写单元测试仅靠 Mockito 是不够。因为 Mockito 无法 mock 私有方法、final 方法及静态方法等。PowerMock 这个 framework，主要是为了扩展其他 mock 框架，如 Mockito、EasyMock。它使用一个自定义的类加载器，纂改字节码，突破 Mockito 无法 mock 静态方法、构造方法、final 类、final 方法以及私有方法的限制。</li>\n<li><a href=\"https://github.com/tomakehurst/wiremock\" target=\"_blank\" rel=\"noopener noreferrer\">WireMock</a>：模拟 HTTP 服务的工具（Mock your APIs）。</li>\n<li><a href=\"https://github.com/testcontainers/testcontainers-java\" target=\"_blank\" rel=\"noopener noreferrer\">Testcontainers</a>：一个支持 JUnit 的测试工具库，提供轻量级的且一次性的常见数据库测试支持、Selenium Web 浏览器或者其他任何可以在 Docker 容器中运行的实例支持。</li>\n</ul>\n<p>相关阅读：</p>\n<ul>\n<li><a href=\"https://martinfowler.com/articles/practical-test-pyramid.html\" target=\"_blank\" rel=\"noopener noreferrer\">The Practical Test Pyramid- Martin Fowler</a> (很赞的一篇文章，不过是英文的)</li>\n<li><a href=\"https://juejin.im/post/6844903982058618894\" target=\"_blank\" rel=\"noopener noreferrer\">浅谈测试之 PowerMock</a></li>\n</ul>\n<h3>测试平台</h3>\n<ul>\n<li><a href=\"https://github.com/metersphere/metersphere\" target=\"_blank\" rel=\"noopener noreferrer\">MeterSphere</a> : 一站式开源持续测试平台，涵盖测试跟踪、接口测试、性能测试、团队协作等功能，全面兼容 JMeter、Postman、Swagger 等开源、主流标准。</li>\n<li><a href=\"https://www.apifox.cn/\" target=\"_blank\" rel=\"noopener noreferrer\">Apifox</a>：API 文档、API 调试、API Mock、API 自动化测试。</li>\n</ul>\n<h3>API 调试</h3>\n<ul>\n<li><a href=\"https://reqable.com/zh-CN/\" target=\"_blank\" rel=\"noopener noreferrer\">Reqable</a>：新一代开源 API 开发工具。Reqable = Fiddler + Charles + Postman, 让 API 调试更快。</li>\n<li><a href=\"https://insomnia.rest/\" target=\"_blank\" rel=\"noopener noreferrer\">Insomnia</a> :像人类而不是机器人一样调试 API。我平时经常用的一款 API 开发工具，界面美观且轻量，总之很喜欢。</li>\n<li><a href=\"https://paw.cloud/\" target=\"_blank\" rel=\"noopener noreferrer\">RapidAPI</a>：一款功能齐全的 HTTP 客户端，但仅支持 Mac。</li>\n<li><a href=\"https://github.com/Postcatlab/postcat\" target=\"_blank\" rel=\"noopener noreferrer\">Postcat</a>：一个可扩展的开源 API 工具平台。</li>\n<li><a href=\"https://www.getpostman.com/\" target=\"_blank\" rel=\"noopener noreferrer\">Postman</a>：开发者最常用的 API 测试工具之一。</li>\n<li><a href=\"https://github.com/liyasthomas/postwoman\" title=\"postwoman\" target=\"_blank\" rel=\"noopener noreferrer\">Hoppscotch</a>（原 Postwoman）：开源 API 测试工具。官方定位是 Postman、Insomnia 等产品的开源替代品。</li>\n<li><a href=\"https://gitee.com/dromara/fast-request\" target=\"_blank\" rel=\"noopener noreferrer\">Restful Fast Request</a>：IDEA 版 Postman，API 调试工具 + API 管理工具 + API 搜索工具。</li>\n</ul>\n<h2>任务调度</h2>\n<ul>\n<li><a href=\"https://github.com/quartz-scheduler/quartz\" target=\"_blank\" rel=\"noopener noreferrer\">Quartz</a>：一个很火的开源任务调度框架，Java 定时任务领域的老大哥或者说参考标准， 很多其他任务调度框架都是基于 <code>quartz</code> 开发的，比如当当网的<code>elastic-job</code>就是基于<code>quartz</code>二次开发之后的分布式调度解决方案</li>\n<li><a href=\"https://github.com/xuxueli/xxl-job\" target=\"_blank\" rel=\"noopener noreferrer\">XXL-JOB</a> :XXL-JOB 是一个分布式任务调度平台，其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线，开箱即用。</li>\n<li><a href=\"http://elasticjob.io/index_zh.html\" target=\"_blank\" rel=\"noopener noreferrer\">Elastic-Job</a>：Elastic-Job 是当当网开源的一个基于 Quartz 和 Zookeeper 的分布式调度解决方案，由两个相互独立的子项目 Elastic-Job-Lite 和 Elastic-Job-Cloud 组成，一般我们只要使用 Elastic-Job-Lite 就好。</li>\n<li><a href=\"https://github.com/analysys/EasyScheduler\" title=\"EasyScheduler\" target=\"_blank\" rel=\"noopener noreferrer\">EasyScheduler</a> （已经更名为 DolphinScheduler，已经成为 Apache 孵化器项目）：分布式易扩展的可视化工作流任务调度平台，主要解决“复杂任务依赖但无法直接监控任务健康状态”的问题。</li>\n<li><a href=\"https://gitee.com/KFCFans/PowerJob\" target=\"_blank\" rel=\"noopener noreferrer\">PowerJob</a>：新一代分布式任务调度与计算框架，支持 CRON、API、固定频率、固定延迟等调度策略，提供工作流来编排任务解决依赖关系，使用简单，功能强大，文档齐全，欢迎各位接入使用！<a href=\"http://www.powerjob.tech/\" target=\"_blank\" rel=\"noopener noreferrer\">http://www.powerjob.tech/</a> 。</li>\n</ul>\n<h2>工作流</h2>\n<ol>\n<li><a href=\"https://github.com/flowable/flowable-engine\" target=\"_blank\" rel=\"noopener noreferrer\">Flowable</a> ：Activiti5 的一个分支发展而来，功能丰富，在 Activiti 的基础上，引入了更多高级功能，如更强大的 CMMN（案例管理模型与符号）、DMN（决策模型与符号）支持，以及更灵活的集成选项。</li>\n<li><a href=\"https://github.com/Activiti/Activiti\" target=\"_blank\" rel=\"noopener noreferrer\">Activiti</a>：功能扩展相对保守，适合需要稳定 BPMN 2.0 工作流引擎的传统企业应用。</li>\n<li><a href=\"https://gitee.com/dromara/warm-flow\" target=\"_blank\" rel=\"noopener noreferrer\">Warm-Flow</a>：国产开源工作流引擎，其特点简洁轻量但又不简单，五脏俱全，组件独立，可扩展。</li>\n<li><a href=\"https://gitee.com/aizuda/flowlong\" target=\"_blank\" rel=\"noopener noreferrer\">FlowLong</a>：国产开源工作流引擎，专门中国特色流程审批打造。</li>\n</ol>\n<h2>分布式</h2>\n<h3>API 网关</h3>\n<ul>\n<li><a href=\"https://github.com/Kong/kong\" title=\"kong\" target=\"_blank\" rel=\"noopener noreferrer\">Kong</a>：Kong 是一个云原生、快速的、可伸缩的分布式微服务抽象层(也称为 API 网关、API 中间件或在某些情况下称为服务网格)。2015 年作为开源项目发布，其核心价值是高性能和可扩展性。</li>\n<li><a href=\"https://github.com/Dromara/soul\" title=\"soul\" target=\"_blank\" rel=\"noopener noreferrer\">ShenYu</a>：适用于所有微服务的可伸缩、高性能、响应性 API 网关解决方案。</li>\n<li><a href=\"https://github.com/spring-cloud/spring-cloud-gateway\" target=\"_blank\" rel=\"noopener noreferrer\">Spring Cloud Gateway</a> : 基于 Spring Framework 5.x 和 Spring Boot 2.x 构建的高性能网关。</li>\n<li><a href=\"https://github.com/Netflix/zuul\" target=\"_blank\" rel=\"noopener noreferrer\">Zuul</a> : Zuul 是一个 L7 应用程序网关，它提供了动态路由，监视，弹性，安全性等功能。</li>\n</ul>\n<h3>配置中心</h3>\n<ul>\n<li><a href=\"https://github.com/ctripcorp/apollo\" title=\"apollo\" target=\"_blank\" rel=\"noopener noreferrer\">Apollo</a>（推荐）：Apollo（阿波罗）是携程框架部门研发的分布式配置中心，能够集中化管理应用不同环境、不同集群的配置，配置修改后能够实时推送到应用端，并且具备规范的权限、流程治理等特性，适用于微服务配置管理场景。</li>\n<li><a href=\"https://github.com/alibaba/nacos\" target=\"_blank\" rel=\"noopener noreferrer\">Nacos</a>（推荐）：Nacos 是 Spring Cloud Alibaba 提供的服务注册发现组件，类似于 Consul、Eureka。并且，提供了分布式配置管理功能。</li>\n<li><a href=\"https://github.com/spring-cloud/spring-cloud-config\" target=\"_blank\" rel=\"noopener noreferrer\">Spring Cloud Config</a>：Spring Cloud Config 是 Spring Cloud 家族中最早的配置中心，虽然后来又发布了 Consul 可以代替配置中心功能，但是 Config 依然适用于 Spring Cloud 项目，通过简单的配置即可实现功能。</li>\n<li><a href=\"https://github.com/hashicorp/consul\" target=\"_blank\" rel=\"noopener noreferrer\">Consul</a>：Consul 是 HashiCorp 公司推出的开源软件，提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用，也可以一起使用以构建全方位的服务网格，总之 Consul 提供了一种完整的服务网格解决方案。</li>\n</ul>\n<h3>链路追踪</h3>\n<ul>\n<li><a href=\"https://github.com/apache/skywalking\" title=\"skywalking\" target=\"_blank\" rel=\"noopener noreferrer\">Skywalking</a> : 针对分布式系统的应用性能监控，尤其是针对微服务、云原生和面向容器的分布式系统架构。</li>\n<li><a href=\"https://github.com/openzipkin/zipkin\" title=\"zipkin\" target=\"_blank\" rel=\"noopener noreferrer\">Zipkin</a>：Zipkin 是一个分布式跟踪系统。它有助于收集解决服务体系结构中的延迟问题所需的时序数据。功能包括该数据的收集和查找。</li>\n<li><a href=\"https://github.com/dianping/cat\" title=\"cat\" target=\"_blank\" rel=\"noopener noreferrer\">CAT</a>：CAT 作为服务端项目基础组件，提供了 Java, C/C++, Node.js, Python, Go 等多语言客户端，已经在美团点评的基础架构中间件框架（MVC 框架，RPC 框架，数据库框架，缓存框架等，消息队列，配置系统等）深度集成，为美团点评各业务线提供系统丰富的性能指标、健康状况、实时告警等。</li>\n</ul>\n<p>相关阅读：<a href=\"https://skywalking.apache.org/zh/blog/2019-03-29-introduction-of-skywalking-and-simple-practice.html\" target=\"_blank\" rel=\"noopener noreferrer\">Skywalking 官网对于主流开源链路追踪系统的对比</a></p>\n<h3>分布式锁</h3>\n<ul>\n<li><a href=\"https://gitee.com/baomidou/lock4j\" target=\"_blank\" rel=\"noopener noreferrer\">Lock4j</a>：支持 Redisson、ZooKeeper 等不同方案的高性能分布式锁。</li>\n<li><a href=\"https://github.com/redisson/redisson\" title=\"redisson\" target=\"_blank\" rel=\"noopener noreferrer\">Redisson</a>：Redisson 在分布式锁方面提供全面且强大的支持，超越了简单的 Redis 锁实现。</li>\n</ul>\n<h2>高性能</h2>\n<h3>多线程</h3>\n<ul>\n<li><a href=\"https://github.com/opengoofy/hippo4j\" target=\"_blank\" rel=\"noopener noreferrer\">Hippo4j</a>：异步线程池框架，支持线程池动态变更&amp;监控&amp;报警，无需修改代码轻松引入。支持多种使用模式，轻松引入，致力于提高系统运行保障能力。</li>\n<li><a href=\"https://github.com/dromara/dynamic-tp\" target=\"_blank\" rel=\"noopener noreferrer\">Dynamic Tp</a>：轻量级动态线程池，内置监控告警功能，集成三方中间件线程池管理，基于主流配置中心（已支持 Nacos、Apollo，Zookeeper、Consul、Etcd，可通过 SPI 自定义实现）。</li>\n<li><a href=\"https://gitee.com/jd-platform-opensource/asyncTool\" target=\"_blank\" rel=\"noopener noreferrer\">asyncTool</a> : 京东的一位大佬开源的多线程工具库，里面大量使用到了 <code>CompletableFuture</code> ，可以解决任意的多线程并行、串行、阻塞、依赖、回调的并行框架，可以任意组合各线程的执行顺序，带全链路执行结果回调。</li>\n</ul>\n<h3>缓存</h3>\n<h4>本地缓存</h4>\n<ul>\n<li><a href=\"https://github.com/ben-manes/caffeine\" target=\"_blank\" rel=\"noopener noreferrer\">Caffeine</a> : 一款强大的本地缓存解决方案，性能非常强大。</li>\n<li><a href=\"https://github.com/google/guava\" target=\"_blank\" rel=\"noopener noreferrer\">Guava</a>：Google Java 核心库，内置了比较完善的本地缓存实现。</li>\n<li><a href=\"https://github.com/snazy/ohc\" target=\"_blank\" rel=\"noopener noreferrer\">OHC</a> ：Java 堆外缓存解决方案（项目从 2021 年开始就不再进行维护了）。</li>\n</ul>\n<h4>分布式缓存</h4>\n<ul>\n<li><a href=\"https://github.com/redis/redis\" target=\"_blank\" rel=\"noopener noreferrer\">Redis</a>：一个使用 C 语言开发的内存数据库，分布式缓存首选。</li>\n<li><a href=\"https://github.com/dragonflydb/dragonfly\" target=\"_blank\" rel=\"noopener noreferrer\">Dragonfly</a>：一种针对现代应用程序负荷需求而构建的内存数据库，完全兼容 Redis 和 Memcached 的 API，迁移时无需修改任何代码，号称全世界最快的内存数据库。</li>\n<li><a href=\"https://github.com/Snapchat/KeyDB\" target=\"_blank\" rel=\"noopener noreferrer\">KeyDB</a>： Redis 的一个高性能分支，专注于多线程、内存效率和高吞吐量。</li>\n</ul>\n<h4>多级缓存</h4>\n<ul>\n<li><a href=\"https://gitee.com/ld/J2Cache\" target=\"_blank\" rel=\"noopener noreferrer\">J2Cache</a>：基于本地内存和 Redis 的两级 Java 缓存框架。</li>\n<li><a href=\"https://github.com/alibaba/jetcache\" target=\"_blank\" rel=\"noopener noreferrer\">JetCache</a>：阿里开源的缓存框架，支持多级缓存、分布式缓存自动刷新、 TTL 等功能。</li>\n</ul>\n<h3>消息队列</h3>\n<p><strong>分布式队列</strong>：</p>\n<ul>\n<li><a href=\"https://github.com/apache/rocketmq\" title=\"RocketMQ\" target=\"_blank\" rel=\"noopener noreferrer\">RocketMQ</a>：阿里巴巴开源的一款高性能、高吞吐量的分布式消息中间件。</li>\n<li><a href=\"https://github.com/apache/kafka\" title=\"Kafka\" target=\"_blank\" rel=\"noopener noreferrer\">Kafka</a>: Kafka 是一种分布式的，基于发布 / 订阅的消息系统。</li>\n<li><a href=\"https://github.com/rabbitmq\" title=\"RabbitMQ\" target=\"_blank\" rel=\"noopener noreferrer\">RabbitMQ</a> :由 erlang 开发的基于 AMQP（Advanced Message Queue 高级消息队列协议）协议实现的消息队列。</li>\n</ul>\n<p><strong>内存队列</strong>：</p>\n<ul>\n<li><a href=\"https://github.com/LMAX-Exchange/disruptor\" target=\"_blank\" rel=\"noopener noreferrer\">Disruptor</a>：Disruptor 是英国外汇交易公司 LMAX 开发的一个高性能队列，研发的初衷是解决内存队列的延迟问题（在性能测试中发现竟然与 I/O 操作处于同样的数量级）。</li>\n</ul>\n<h3>读写分离和分库分表</h3>\n<ul>\n<li><a href=\"https://github.com/apache/shardingsphere\" target=\"_blank\" rel=\"noopener noreferrer\">ShardingSphere</a>：ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈，它由 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar（计划中）这 3 款相互独立的产品组成。</li>\n<li><a href=\"https://github.com/MyCatApache/MyCat2\" target=\"_blank\" rel=\"noopener noreferrer\">MyCat</a> : MyCat 是数据库分库分表的中间件，MyCat 使用最多的两个功能是：读写分离和分库分表。MyCat 是一些社区爱好者在阿里 Cobar 的基础上进行二次开发，解决了 Cobar 当时存 在的一些问题，并且加入了许多新的功能在其中。</li>\n<li><a href=\"https://github.com/baomidou/dynamic-datasource-spring-boot-starter\" target=\"_blank\" rel=\"noopener noreferrer\">dynamic-datasource-spring-boot-starter</a>：一个基于 Spring Boot 的快速集成多数据源的启动器，支持多数据源、动态数据源、主从分离、读写分离和分布式事务。</li>\n</ul>\n<h2>高可用</h2>\n<h3>限流</h3>\n<p>分布式限流：</p>\n<ul>\n<li><a href=\"https://github.com/alibaba/Sentinel\" target=\"_blank\" rel=\"noopener noreferrer\">Sentinel</a>（推荐）：面向分布式服务架构的高可用防护组件，主要以流量为切入点，从流量控制、熔断降级、系统自适应保护等多个维度来帮助用户保障微服务的稳定性。</li>\n<li><a href=\"https://github.com/Netflix/Hystrix\" target=\"_blank\" rel=\"noopener noreferrer\">Hystrix</a>：类似于 Sentinel 。</li>\n</ul>\n<p>相关阅读：<a href=\"https://sentinelguard.io/zh-cn/blog/sentinel-vs-hystrix.html\" target=\"_blank\" rel=\"noopener noreferrer\">Sentinel 与 Hystrix 的对比</a>。</p>\n<p>单机限流：</p>\n<ul>\n<li><a href=\"https://github.com/vladimir-bukhtoyarov/bucket4j\" target=\"_blank\" rel=\"noopener noreferrer\">Bucket4j</a>：一个非常不错的基于令牌/漏桶算法的限流库。</li>\n<li><a href=\"https://github.com/resilience4j/resilience4j\" target=\"_blank\" rel=\"noopener noreferrer\">Resilience4j</a>：一个轻量级的容错组件，其灵感来自于 Hystrix。</li>\n</ul>\n<h3>监控</h3>\n<ul>\n<li><a href=\"https://github.com/codecentric/spring-boot-admin\" target=\"_blank\" rel=\"noopener noreferrer\">Spring Boot Admin</a>：管理和监控 Spring Boot 应用程序。</li>\n<li><a href=\"https://github.com/dropwizard/metrics\" target=\"_blank\" rel=\"noopener noreferrer\">Metrics</a>：捕获 JVM 和应用程序级别的指标。所以你知道发生了什么事。</li>\n</ul>\n<h3>日志</h3>\n<ul>\n<li>EKL 老三件套 : 最原始的时候，ELK 是由 3 个开源项目的首字母构成，分别是 Elasticsearch、Logstash、Kibana。</li>\n<li>新一代 ELK 架构 : Elasticsearch+Logstash+Kibana+Beats。</li>\n<li>EFK : EFK 中的 F 代表的是 <a href=\"https://github.com/fluent/fluentd\" target=\"_blank\" rel=\"noopener noreferrer\">Fluentd</a>。</li>\n<li><a href=\"https://gitee.com/dromara/TLog\" target=\"_blank\" rel=\"noopener noreferrer\">TLog</a>：一个轻量级的分布式日志标记追踪神器，10 分钟即可接入，自动对日志打标签完成微服务的链路追踪。</li>\n</ul>\n<h2>字节码操作</h2>\n<ul>\n<li><a href=\"https://asm.ow2.io/\" target=\"_blank\" rel=\"noopener noreferrer\">ASM</a>：通用 Java 字节码操作和分析框架。它可用于直接以二进制形式修改现有类或动态生成类。</li>\n<li><a href=\"https://github.com/raphw/byte-buddy\" target=\"_blank\" rel=\"noopener noreferrer\">Byte Buddy</a>：Java 字节码生成和操作库，用于在 Java 应用程序运行时创建和修改 Java 类，无需使用编译器</li>\n<li><a href=\"https://github.com/jboss-javassist/javassist\" target=\"_blank\" rel=\"noopener noreferrer\">Javassist</a>：动态编辑 Java 字节码的类库。</li>\n<li><a href=\"https://github.com/Col-E/Recaf\" target=\"_blank\" rel=\"noopener noreferrer\">Recaf</a>：现代 Java 字节码编辑器，基于 ASM（Java 字节码操作框架） 来修改字节码，可简化编辑已编译 Java 应用程序的过程。</li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "开源项目"
      ]
    },
    {
      "title": "Java 优质开源工具类",
      "url": "https://codingmap.strova.top/open-source-project/tool-library.html",
      "id": "https://codingmap.strova.top/open-source-project/tool-library.html",
      "summary": "Java优质开源工具类库推荐，涵盖Lombok、Guava、Hutool、Arthas等提升开发效率和代码质量的常用工具。重点围绕 代码质量、问题排查和性能优化、文档处理、图片处理 等内容展开。结合 CodingMap 知识体系梳理 开源项目 / Tool Library.Html 的核心概念、实践方法等核心内容。",
      "content_html": "<h2>代码质量</h2>\n<ul>\n<li><a href=\"https://github.com/rzwitserloot/lombok\" target=\"_blank\" rel=\"noopener noreferrer\">Lombok</a> :一个能够简化 Java 代码的强大工具库。通过使用 Lombok 的注解，我们可以自动生成常用的代码逻辑，例如 <code>getter</code>、<code>setter</code>、<code>equals</code>、<code>hashCode</code>、<code>toString</code> 方法，以及构造器、日志变量等内容。</li>\n<li><a href=\"https://github.com/google/guava\" title=\"guava\" target=\"_blank\" rel=\"noopener noreferrer\">Guava</a>： Google 开发的一组功能强大的核心库，扩展了 Java 的标准库功能。它提供了许多有用的工具类和集合类型，例如 <code>Multimap</code>（多值映射）、<code>Multiset</code>（多重集合）、<code>BiMap</code>（双向映射）和不可变集合，此外还包含图形处理库和并发工具。Guava 还支持 I/O 操作、哈希算法、字符串处理、缓存等多种实用功能。</li>\n<li><a href=\"https://github.com/looly/hutool\" title=\"hutool\" target=\"_blank\" rel=\"noopener noreferrer\">Hutool</a> : 一个全面且用户友好的 Java 工具库，旨在通过最小的依赖简化开发任务。它封装了许多实用的功能，例如文件操作、缓存、加密/解密、日志、文件操作。</li>\n</ul>\n<h2>问题排查和性能优化</h2>\n<ul>\n<li><a href=\"https://github.com/alibaba/arthas\" title=\"arthas\" target=\"_blank\" rel=\"noopener noreferrer\">Arthas</a>：Alibaba 开源的 Java 诊断工具，可以实时监控和诊断 Java 应用程序。它提供了丰富的命令和功能，用于分析应用程序的性能问题，包括启动过程中的资源消耗和加载时间。</li>\n<li><a href=\"https://github.com/async-profiler/async-profiler\" target=\"_blank\" rel=\"noopener noreferrer\">Async Profiler</a>：低开销的异步 Java 性能分析工具，用于收集和分析应用程序的性能数据。</li>\n<li><a href=\"https://github.com/maciejwalkowiak/spring-boot-startup-report\" target=\"_blank\" rel=\"noopener noreferrer\">Spring Boot Startup Report</a>：用于生成 Spring Boot 应用程序启动报告的工具。它可以提供详细的启动过程信息，包括每个 bean 的加载时间、自动配置的耗时等，帮助你分析和优化启动过程。</li>\n<li><a href=\"https://github.com/linyimin0812/spring-startup-analyzer/blob/main/README_ZH.md\" target=\"_blank\" rel=\"noopener noreferrer\">Spring Startup Analyzer</a>：采集 Spring 应用启动过程数据，生成交互式分析报告(HTML)，用于分析 Spring 应用启动卡点，支持 Spring Bean 异步初始化，减少优化 Spring 应用启动时间。UI 参考<a href=\"https://github.com/maciejwalkowiak/spring-boot-startup-report\" target=\"_blank\" rel=\"noopener noreferrer\">Spring Boot Startup Report</a>实现。</li>\n</ul>\n<h2>文档处理</h2>\n<h3>文档解析</h3>\n<ul>\n<li><a href=\"https://github.com/apache/tika\" target=\"_blank\" rel=\"noopener noreferrer\">Tika</a>：Apache Tika 工具包能够检测并提取来自超过一千种不同文件类型（如 PPT、XLS 和 PDF）的元数据和文本内容。</li>\n</ul>\n<h3>Excel</h3>\n<ul>\n<li><a href=\"https://github.com/alibaba/easyexcel\" target=\"_blank\" rel=\"noopener noreferrer\">EasyExcel</a> :快速、简单避免 OOM 的 Java 处理 Excel 工具。不过，这个个项目不再维护，迁移至了 <a href=\"https://github.com/fast-excel/fastexcel\" target=\"_blank\" rel=\"noopener noreferrer\">FastExcel</a>。</li>\n<li><a href=\"https://github.com/pig-mesh/excel-spring-boot-starter\" target=\"_blank\" rel=\"noopener noreferrer\">Excel Spring Boot Starter</a>：基于 FastExcel 实现的 Spring Boot Starter，用于简化 Excel 的读写操作。</li>\n<li><a href=\"https://github.com/monitorjbl/excel-streaming-reader\" target=\"_blank\" rel=\"noopener noreferrer\">Excel Streaming Reader</a>：Excel 流式代码风格读取工具（只支持读取 XLSX 文件），基于 Apache POI 封装，同时保留标准 POI API 的语法。</li>\n<li><a href=\"https://github.com/liaochong/myexcel\" target=\"_blank\" rel=\"noopener noreferrer\">MyExcel</a>：一个集导入、导出、加密 Excel 等多项功能的工具包。</li>\n</ul>\n<h3>Word</h3>\n<ul>\n<li><a href=\"https://github.com/Sayi/poi-tl\" target=\"_blank\" rel=\"noopener noreferrer\">poi-tl</a>：基于 Apache POI 的 Word 模板引擎，可以根据 Word 模板和数据生成 Word 文档，所见即所得！</li>\n</ul>\n<h3>JSON</h3>\n<ul>\n<li><a href=\"https://github.com/json-path/JsonPath\" target=\"_blank\" rel=\"noopener noreferrer\">JsonPath</a>：处理 JSON 数据的工具库。</li>\n</ul>\n<h3>PDF</h3>\n<p>对于简单的 PDF 创建需求，OpenPDF 是一个不错的选择，它开源免费，API 简单易用。对于需要解析、转换和提取文本等操作的复杂场景，可以选择 Apache PDFBox。当然了，复杂场景如果不介意 LGPL 许可也可以选择 iText。</p>\n<ul>\n<li><a href=\"https://gitee.com/dromara/x-easypdf\" target=\"_blank\" rel=\"noopener noreferrer\">x-easypdf</a>：一个用搭积木的方式构建 PDF 的框架（基于 pdfbox/fop），支持 PDF 导出和编辑，适合简单的 PDF 文档生成场景。</li>\n<li><a href=\"https://github.com/itext/itext7\" target=\"_blank\" rel=\"noopener noreferrer\">iText</a>：一个用于创建、编辑和增强 PDF 文档的 Java 库。iText 7 社区版采用 AGPL 许可证，如果你的项目是闭源商业项目，需要购买商业许可证。 iText 5 仍然是 LGPL 许可，可以免费用于商业用途，但已经停止维护。</li>\n<li><a href=\"https://github.com/LibrePDF/OpenPDF\" target=\"_blank\" rel=\"noopener noreferrer\">OpenPDF</a>：完全开源免费 (LGPL/MPL 双重许可)，基于 iText 的一个分支，可以作为 iText 的替代品，简单易用，但功能相比于 iText 更少一些（对于大多数场景已经足够）。</li>\n<li><a href=\"https://github.com/apache/pdfbox\" target=\"_blank\" rel=\"noopener noreferrer\">Apache PDFBox</a> :完全开源免费 (Apache 许可证)，功能强大，支持 PDF 的创建、解析、转换和提取文本等。不过，由于其功能过于丰富，因此 API 设计相对复杂，学习难度会大一些。</li>\n<li><a href=\"https://xmlgraphics.apache.org/fop/\" target=\"_blank\" rel=\"noopener noreferrer\">FOP</a> : Apache FOP 用于将 XSL-FO（Extensible Stylesheet Language Formatting Objects）格式化对象转换为多种输出格式，最常见的是 PDF。</li>\n</ul>\n<h2>图片处理</h2>\n<ul>\n<li><a href=\"https://github.com/coobird/thumbnailator\" target=\"_blank\" rel=\"noopener noreferrer\">Thumbnailator</a>：一个图像处理工具库，主要功能是缩放图像、添加水印、旋转图像、调整图片大小以及区域裁剪。</li>\n<li><a href=\"https://github.com/nackily/imglib\" target=\"_blank\" rel=\"noopener noreferrer\">Imglib</a>：一个轻量级的 JAVA 图像处理库，致力于简化对图像的常见处理，主要提供三部分的能力：图像收集、图像处理（基于 Thumbnailator 实现）、聚合与分裂。</li>\n</ul>\n<h2>验证码</h2>\n<ul>\n<li><a href=\"https://gitee.com/whvse/EasyCaptcha\" target=\"_blank\" rel=\"noopener noreferrer\">EasyCaptcha</a>：Java 图形验证码，支持 gif、中文、算术等类型，可用于 Java Web、JavaSE 等项目。</li>\n<li><a href=\"https://gitee.com/anji-plus/captcha\" target=\"_blank\" rel=\"noopener noreferrer\">AJ-Captcha</a>：行为验证码(滑动拼图、点选文字)，前后端(java)交互。</li>\n<li><a href=\"https://gitee.com/tianai/tianai-captcha\" target=\"_blank\" rel=\"noopener noreferrer\">tianai-captcha</a>：好看又好用的滑块验证码。</li>\n</ul>\n<h2>短信&amp;邮件</h2>\n<ul>\n<li><a href=\"https://github.com/dromara/SMS4J\" target=\"_blank\" rel=\"noopener noreferrer\">SMS4J</a>：短信聚合框架，解决接入多个短信 SDK 的繁琐流程。</li>\n<li><a href=\"https://github.com/bbottema/simple-java-mail\" target=\"_blank\" rel=\"noopener noreferrer\">Simple Java Mail</a>：最简单的 Java 轻量级邮件库，同时能够发送复杂的电子邮件。</li>\n</ul>\n<h2>在线支付</h2>\n<ul>\n<li><a href=\"https://gitee.com/jeequan/jeepay\" target=\"_blank\" rel=\"noopener noreferrer\">Jeepay</a>：一套适合互联网企业使用的开源支付系统，已实现交易、退款、转账、分账等接口，支持服务商特约商户和普通商户接口。已对接微信，支付宝，云闪付官方接口，支持聚合码支付。</li>\n<li><a href=\"https://gitee.com/YunGouOS/YunGouOS-PAY-SDK\" target=\"_blank\" rel=\"noopener noreferrer\">YunGouOS-PAY-SDK</a>：YunGouOS 微信支付接口、微信官方个人支付接口、非二维码收款，非第四方清算。个人用户可提交资料开通微信支付商户，完成对接。</li>\n<li><a href=\"https://gitee.com/javen205/IJPay\" target=\"_blank\" rel=\"noopener noreferrer\">IJPay</a>：聚合支付，IJPay 让支付触手可及，封装了微信支付、QQ 支付、支付宝支付、京东支付、银联支付、PayPal 支付等常用的支付方式以及各种常用的接口。</li>\n</ul>\n<h2>其他</h2>\n<ul>\n<li><a href=\"https://github.com/oshi/oshi\" title=\"oshi\" target=\"_blank\" rel=\"noopener noreferrer\">oshi</a>：一款为 Java 语言提供的基于 JNA 的（本机）操作系统和硬件信息库。</li>\n<li><a href=\"https://github.com/lionsoul2014/ip2region\" target=\"_blank\" rel=\"noopener noreferrer\">ip2region</a> :最自由的 ip 地址查询库，ip 到地区的映射库，提供 Binary,B 树和纯内存三种查询算法，妈妈再也不用担心我的 ip 地址定位。</li>\n<li><a href=\"https://github.com/real-logic/agrona\" target=\"_blank\" rel=\"noopener noreferrer\">agrona</a>：Java 高性能数据结构（<code>Buffers</code>、<code>Lists</code>、<code>Maps</code>、<code>Scalable Timer Wheel</code>……）和实用方法。</li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "开源项目"
      ]
    },
    {
      "title": "Java 优质开源开发工具",
      "url": "https://codingmap.strova.top/open-source-project/tools.html",
      "id": "https://codingmap.strova.top/open-source-project/tools.html",
      "summary": "Java优质开源开发工具推荐，涵盖代码质量检查、项目构建、测试框架、容器化部署等开发必备工具精选。重点围绕 代码质量、项目构建、反编译、数据库 等内容展开。结合 CodingMap 知识体系梳理 开源项目 / Tools.Html 的核心概念、实践方法、常见问题和高频面试考点，覆盖原理分析、使用场景等核心内容。",
      "content_html": "<h2>代码质量</h2>\n<ul>\n<li><a href=\"https://github.com/SonarSource/sonarqube\" title=\"sonarqube\" target=\"_blank\" rel=\"noopener noreferrer\">SonarQube</a>：静态代码检查工具，，帮助检查代码缺陷，可以快速的定位代码中潜在的或者明显的错误，改善代码质量，提高开发速度。</li>\n<li><a href=\"https://github.com/diffplug/spotless\" target=\"_blank\" rel=\"noopener noreferrer\">Spotless</a>：Spotless 是支持多种语言的代码格式化工具，支持 Maven 和 Gradle 以 Plugin 的形式构建。</li>\n<li><a href=\"https://github.com/checkstyle/checkstyle\" title=\"checkstyle\" target=\"_blank\" rel=\"noopener noreferrer\">CheckStyle</a> : 类似于 Spotless，可帮助程序员编写符合编码标准的 Java 代码。</li>\n<li><a href=\"https://github.com/pmd/pmd\" title=\"pmd\" target=\"_blank\" rel=\"noopener noreferrer\">PMD</a> : 可扩展的多语言静态代码分析器。</li>\n<li><a href=\"https://github.com/spotbugs/spotbugs\" title=\"spotbugs\" target=\"_blank\" rel=\"noopener noreferrer\">SpotBugs</a> : FindBugs 的继任者。静态分析工具，用于查找 Java 代码中的错误。</li>\n<li><a href=\"https://github.com/alibaba/p3c\" title=\"p3c\" target=\"_blank\" rel=\"noopener noreferrer\">P3C</a>：Alibaba Java Coding Guidelines pmd implements and IDE plugin。Eclipse 和 IDEA 上都有该插件。</li>\n</ul>\n<h2>项目构建</h2>\n<ul>\n<li><a href=\"https://maven.apache.org/\" target=\"_blank\" rel=\"noopener noreferrer\">Maven</a>：一个软件项目管理和理解工具。基于项目对象模型 (Project Object Model，POM) 的概念，Maven 可以从一条中心信息管理项目的构建、报告和文档。详细介绍：<a href=\"https://javaguide.cn/tools/maven/maven-core-concepts.html\" target=\"_blank\" rel=\"noopener noreferrer\">Maven 核心概念总结</a>。</li>\n<li><a href=\"https://gradle.org/\" target=\"_blank\" rel=\"noopener noreferrer\">Gradle</a> ：一个开源的构建自动化工具，它足够灵活，可以构建几乎任何类型的软件。Gradle 对你要构建什么或者如何构建它做了很少的假设，这使得 Gradle 特别灵活。详细介绍：<a href=\"https://javaguide.cn/tools/gradle/gradle-core-concepts.html\" target=\"_blank\" rel=\"noopener noreferrer\">Gradle 核心概念总结</a>。</li>\n</ul>\n<h2>反编译</h2>\n<ul>\n<li><a href=\"https://github.com/skylot/jadx\" target=\"_blank\" rel=\"noopener noreferrer\">JADX</a>：用于从 Android Dex 和 Apk 文件生成 Java 源代码的命令行和 GUI 工具。</li>\n<li><a href=\"https://github.com/java-decompiler/jd-gui\" target=\"_blank\" rel=\"noopener noreferrer\">JD-GUI</a>:一个独立的 GUI 工具，可显示 CLASS 文件中的 Java 源代码。</li>\n</ul>\n<h2>数据库</h2>\n<h3>数据库建模</h3>\n<ul>\n<li><a href=\"https://gitee.com/robergroup/chiner\" target=\"_blank\" rel=\"noopener noreferrer\">CHINER</a>：开源免费的国产数据库建模工具。目标是做一款丰富数据库生态，独立于具体数据库之外的，数据库关系模型设计平台。前生是 <a href=\"https://gitee.com/robergroup/pdman\" target=\"_blank\" rel=\"noopener noreferrer\">PDMan</a>，定位为 PowerDesigner 的免费替代方案。</li>\n</ul>\n<p>开源的数据库建模工具比较少，以下是一些非开源的数据库建模工具（部分需要付费才能使用） :</p>\n<ul>\n<li><a href=\"https://www.mysql.com/products/workbench/\" target=\"_blank\" rel=\"noopener noreferrer\">MySQL Workbench</a> : MySQL 官方为数据库架构师、开发人员和 DBA 提供的一个可视化工具。 MySQL Workbench 支持数据建模，SQL 开发以及服务器配置、用户管理、性能优化、数据库备份以及迁移等功能，支持 Windows、Linux 和 Mac OS X 平台。</li>\n<li><a href=\"https://www.navicat.com.cn/products/navicat-data-modeler\" target=\"_blank\" rel=\"noopener noreferrer\">Navicat Data Modeler</a> : 一款强大的和符合成本效益的数据库设计工具，它能帮助用户创建高质素的概念、逻辑和物理数据模型。让你可视化地设计数据库结构、执行逆向或正向工程程序、从 ODBC 数据源导入模型、生成复杂的 SQL/DDL 和打印模型到文件等。付费。</li>\n<li><a href=\"https://dbschema.com/\" target=\"_blank\" rel=\"noopener noreferrer\">DbSchema</a> : 一款功能强大的数据库设计和管理的可视化工具，支持几乎所有的关系型和 NoSQL 数据库。付费。</li>\n<li><a href=\"https://dbdiagram.io/home\" target=\"_blank\" rel=\"noopener noreferrer\">dbdiagram.io</a> : 是一款简单免费的在线 ER 图绘制工具，通过编写代码创建模型，专为开发人员和数据分析师而设计。它通过一个简单的自定义语言来生成数据模型，支持 MySQL、PostgreSQL、SQL Server 数据库 DDL 文件的正向工程和逆向工程、版本历史、在线共享、导出图片或者 PDF 等功能。<a href=\"http://dbdiagram.io\" target=\"_blank\" rel=\"noopener noreferrer\">dbdiagram.io</a> 提供了免费版。</li>\n</ul>\n<h3>数据库管理</h3>\n<ul>\n<li><a href=\"https://github.com/alibaba/Chat2DB\" target=\"_blank\" rel=\"noopener noreferrer\">Chat2DB</a>：阿里巴巴开源的一款智能的通用数据库工具和 SQL 客户端，支持 Windows、Mac 本地安装，也支持服务器端部署，Web 网页访问。和传统的数据库客户端软件 Navicat、DBeaver 相比 Chat2DB 集成了 AIGC 的能力，支持自然语言生成 SQL、SQL 性能优化等功能。</li>\n<li><a href=\"https://github.com/beekeeper-studio/beekeeper-studio\" target=\"_blank\" rel=\"noopener noreferrer\">Beekeeper Studio</a>：跨平台数据库管理工具，颜值高，支持 SQLite、MySQL、MariaDB、Postgres、CockroachDB、SQL Server、Amazon Redshift。</li>\n<li><a href=\"https://github.com/sequelpro/sequelpro\" target=\"_blank\" rel=\"noopener noreferrer\">Sequel Pro</a>：适用于 macOS 的 MySQL/MariaDB 数据库管理工具。</li>\n<li><a href=\"https://github.com/dbeaver/dbeaver\" target=\"_blank\" rel=\"noopener noreferrer\">DBeaver</a>：一个基于 Java 开发 ，并且支持几乎所有的数据库产品的开源数据库管理工具。DBeaver 社区版不光支持关系型数据库比如 MySQL、PostgreSQL、MariaDB、SQLite、Oracle、Db2、SQL Server，还比如 SQLite、H2 这些内嵌数据库。还支持常见的全文搜索引擎比如 Elasticsearch 和 Solr、大数据相关的工具比如 Hive 和 Spark。</li>\n<li><a href=\"https://gitee.com/dbkangaroo/kangaroo\" target=\"_blank\" rel=\"noopener noreferrer\">Kangaroo</a>：袋鼠是一款为热门数据库系统打造的管理客户端(SQLite / MySQL / PostgreSQL / ...) ，支持建表、查询、模型、同步、导入导出等功能，支持 Windows / Mac / Linux 等操作系统，力求打造成好用、好玩、开发友好的 SQL 工具。</li>\n<li><a href=\"https://arctype.com/\" target=\"_blank\" rel=\"noopener noreferrer\">Arctype</a>：一个桌面的数据库查询工具，可以连接各种数据库，在其中执行 SQL 语句，以可视化形式展示数据。</li>\n<li><a href=\"https://github.com/RenzHoly/Mongood\" target=\"_blank\" rel=\"noopener noreferrer\">Mongood</a> : MongoDB 图形化的管理工具。基于微软 Fluent UI，支持自动黑暗模式。</li>\n</ul>\n<h3>Redis</h3>\n<ul>\n<li><a href=\"https://github.com/qishibo/AnotherRedisDesktopManager/blob/master/README.zh-CN.md\" target=\"_blank\" rel=\"noopener noreferrer\">Another Redis Desktop Manager</a>：更快、更好、更稳定的 Redis 桌面(GUI)管理客户端，兼容 Windows、Mac、Linux。</li>\n<li><a href=\"https://github.com/tiny-craft/tiny-rdm\" target=\"_blank\" rel=\"noopener noreferrer\">Tiny RDM</a>：一个更现代化的 Redis 桌面(GUI)管理客户端，基于 Webview2，兼容 Windows、Mac、Linux。</li>\n<li><a href=\"https://github.com/ngbdf/redis-manager\" target=\"_blank\" rel=\"noopener noreferrer\">Redis Manager</a>：Redis 一站式管理平台，支持集群（cluster、master-replica、sentinel）的监控、安装（除 sentinel）、管理、告警以及基本的数据操作功能。</li>\n<li><a href=\"https://github.com/sohutv/cachecloud\" target=\"_blank\" rel=\"noopener noreferrer\">CacheCloud</a>：一个 Redis 云管理平台，支持 Redis 多种架构(Standalone、Sentinel、Cluster)高效管理、有效降低大规模 Redis 运维成本，提升资源管控能力和利用率。</li>\n<li><a href=\"https://github.com/tair-opensource/RedisShake\" target=\"_blank\" rel=\"noopener noreferrer\">RedisShake</a>：一个用于处理和迁移 Redis 数据的工具。</li>\n</ul>\n<h2>Docker</h2>\n<ul>\n<li><a href=\"https://github.com/portainer/portainer\" target=\"_blank\" rel=\"noopener noreferrer\">Portainer</a>：可视化管理 Docker，Web 应用的形式。</li>\n<li><a href=\"https://github.com/jesseduffield/lazydocker\" target=\"_blank\" rel=\"noopener noreferrer\">lazydocker</a>：适用于 docker 和 docker-compose 的简单终端 UI。</li>\n</ul>\n<h2>ZooKeeper</h2>\n<ul>\n<li><a href=\"https://github.com/vran-dev/PrettyZoo\" target=\"_blank\" rel=\"noopener noreferrer\">PrettyZoo</a>：一个基于 Apache Curator 和 JavaFX 实现的 ZooKeeper 图形化管理客户端，颜值非常高，支持 Mac / Windows / Linux 。你可以使用 PrettyZoo 来实现对 ZooKeeper 的可视化增删改查。</li>\n<li><a href=\"https://zktools.readthedocs.io/en/latest/#installing\" target=\"_blank\" rel=\"noopener noreferrer\">zktools</a>：一个低延迟的 ZooKeeper 图形化管理客户端，颜值非常高，支持 Mac / Windows / Linux 。你可以使用 zktools 来实现对 ZooKeeper 的可视化增删改查。</li>\n</ul>\n<h2>Kafka</h2>\n<ul>\n<li><a href=\"https://github.com/provectus/kafka-ui\" target=\"_blank\" rel=\"noopener noreferrer\">Kafka UI</a>：免费的开源 Web UI，用于监控和管理 Apache Kafka 集群。</li>\n<li><a href=\"https://github.com/obsidiandynamics/kafdrop\" target=\"_blank\" rel=\"noopener noreferrer\">Kafdrop</a> : 一个用于查看 Kafka 主题和浏览消费者组的 Web UI。</li>\n<li><a href=\"https://github.com/smartloli/EFAK\" target=\"_blank\" rel=\"noopener noreferrer\">EFAK</a> （Eagle For Apache Kafka，以前叫做 Kafka Eagle）：一个简单的高性能监控系统，用于对 Kafka 集群进行全面的监控和管理。</li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "开源项目"
      ]
    },
    {
      "title": "Java 优质开源技术教程",
      "url": "https://codingmap.strova.top/open-source-project/tutorial.html",
      "id": "https://codingmap.strova.top/open-source-project/tutorial.html",
      "summary": "Java优质开源技术教程推荐，涵盖Java核心知识、计算机基础、算法、系统设计等领域的高质量学习资源汇总。重点围绕 Java、计算机基础、系统设计、大数据 等内容展开。结合 CodingMap 知识体系梳理 开源项目 / Tutorial.Html 的核心概念、实践方法、常见问题和高频面试考点，覆盖原理分析、使用场景。",
      "content_html": "<h2>Java</h2>\n<ul>\n<li><a href=\"https://github.com/Snailclimb/JavaGuide\" title=\"JavaGuide\" target=\"_blank\" rel=\"noopener noreferrer\">JavaGuide</a> :【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。</li>\n<li><a href=\"https://github.com/itwanger/toBeBetterJavaer\" target=\"_blank\" rel=\"noopener noreferrer\">toBeBetterJavaer</a>：一份通俗易懂、风趣幽默的 Java 学习指南，内容涵盖 Java 基础、Java 集合框架、Java 并发编程、JVM、Java 企业级开发（Git、SSM、Spring Boot）等知识点。</li>\n<li><a href=\"https://github.com/csguide-dabai/interview-guide\" target=\"_blank\" rel=\"noopener noreferrer\">interview-guide</a>：总结了后端面试八股文中的重点，希望能帮助各位准备互联网开发岗校招面试的同学。</li>\n<li><a href=\"https://github.com/doocs/advanced-java\" title=\"advanced-java\" target=\"_blank\" rel=\"noopener noreferrer\">advanced-java</a> :互联网 Java 工程师进阶知识完全扫盲：涵盖高并发、分布式、高可用、微服务、海量数据处理等领域知识。</li>\n<li><a href=\"https://github.com/hollischuang/toBeTopJavaer\" title=\"toBeTopJavaer\" target=\"_blank\" rel=\"noopener noreferrer\">toBeTopJavaer</a>：Java 工程师成神之路 。</li>\n<li><a href=\"https://github.com/aalansehaiyang/technology-talk\" target=\"_blank\" rel=\"noopener noreferrer\">technology-talk</a> : 汇总 java 生态圈常用技术框架、开源中间件，系统架构、数据库、大公司架构案例、常用三方类库、项目管理、线上问题排查、个人成长、思考等知识</li>\n<li><a href=\"https://github.com/crossoverJie/JCSprout\" target=\"_blank\" rel=\"noopener noreferrer\">JCSprout</a> :处于萌芽阶段的 Java 核心知识库。</li>\n<li><a href=\"https://github.com/crisxuan/bestJavaer\" target=\"_blank\" rel=\"noopener noreferrer\">bestJavaer</a> : 这是一个成为更好的 Java 程序员的系列教程。</li>\n<li><a href=\"https://github.com/iluwatar/java-design-patterns\" title=\"java-design-patterns\" target=\"_blank\" rel=\"noopener noreferrer\">java-design-patterns</a>：用 Java 实现的设计模式。</li>\n</ul>\n<h2>计算机基础</h2>\n<ul>\n<li><a href=\"https://github.com/PKUFlyingPig/cs-self-learning\" target=\"_blank\" rel=\"noopener noreferrer\">cs-self-learning</a>：计算机自学指南，汇总欧美众多名校高质量计算机课程。</li>\n<li><a href=\"https://github.com/CyC2018/CS-Notes\" title=\"CS-Notes\" target=\"_blank\" rel=\"noopener noreferrer\">CS-Notes</a>：技术面试必备基础知识、Leetcode 题解、后端面试、Java 面试、春招、秋招、操作系统、计算机网络、系统设计。</li>\n<li><a href=\"https://github.com/wolverinn/Waking-Up\" target=\"_blank\" rel=\"noopener noreferrer\">Waking-Up</a>：计算机基础（计算机网络/操作系统/数据库/Git...）面试问题全面总结。</li>\n</ul>\n<h2>系统设计</h2>\n<h3>SpringBoot</h3>\n<ul>\n<li><a href=\"https://github.com/Snailclimb/springboot-guide\" target=\"_blank\" rel=\"noopener noreferrer\">springboot-guide</a>：SpringBoot 核心知识点总结。 基于 Spring Boot 2.19+。</li>\n<li><a href=\"https://github.com/wuyouzhuguli/SpringAll\" title=\"SpringAll\" target=\"_blank\" rel=\"noopener noreferrer\">SpringAll</a>：循序渐进，学习 Spring Boot、Spring Boot &amp; Shiro、Spring Cloud、Spring Security &amp; Spring Security OAuth2，博客 Spring 系列源码。</li>\n<li><a href=\"https://github.com/chengxy-nds/Springboot-Notebook\" target=\"_blank\" rel=\"noopener noreferrer\">Springboot-Notebook</a> :一系列以 Spring Boot 为基础开发框架，整合 Redis、 Rabbitmq、ES、MongoDB、Spring Cloud、Kafka、Skywalking 等互联网主流技术，实现各种常见功能点的综合性案例。</li>\n<li><a href=\"https://github.com/JeffLi1993/springboot-learning-example\" title=\"springboot-learning-example\" target=\"_blank\" rel=\"noopener noreferrer\">springboot-learning-example</a>：Spring Boot 实践学习案例，是 Spring Boot 初学者及核心技术巩固的最佳实践。</li>\n<li><a href=\"https://github.com/xkcoding/spring-boot-demo\" title=\"spring-boot-demo\" target=\"_blank\" rel=\"noopener noreferrer\">spring-boot-demo</a>：spring boot demo 是一个用来深度学习并实战 spring boot 的项目，目前总共包含 63 个集成 demo，已经完成 52 个。</li>\n<li><a href=\"https://github.com/YunaiV/SpringBoot-Labs\" target=\"_blank\" rel=\"noopener noreferrer\">SpringBoot-Labs</a>：Spring Boot 系列教程。</li>\n</ul>\n<p>相关文章：<a href=\"https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&amp;mid=2247488298&amp;idx=3&amp;sn=0a8fd88ec5a050de131c2a3305482ac4&amp;chksm=cea25ce1f9d5d5f7f53a0237d27489326bce4546353b038085c03b086d91ef396bf824d3a155&amp;token=496868067&amp;lang=zh_CN#rd\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub 点赞接近 100k 的 SpringBoot 学习教程+实战推荐！牛批！</a></p>\n<h3>SpringCloud</h3>\n<ul>\n<li><a href=\"https://github.com/forezp/SpringCloudLearning\" title=\"SpringCloudLearning\" target=\"_blank\" rel=\"noopener noreferrer\">SpringCloudLearning</a> : 方志朋的《史上最简单的 Spring Cloud 教程源码》。</li>\n<li><a href=\"https://github.com/macrozheng/springcloud-learning\" target=\"_blank\" rel=\"noopener noreferrer\">springcloud-learning</a> : 一套涵盖大部分核心组件使用的 Spring Cloud 教程。</li>\n<li><a href=\"https://github.com/zhoutaoo/SpringCloud\" title=\"SpringCloud\" target=\"_blank\" rel=\"noopener noreferrer\">SpringCloud</a>：基于 SpringCloud2.1 的微服务开发脚手架，整合了 spring-security-oauth2、nacos、feign、sentinel、springcloud-gateway 等。</li>\n</ul>\n<p>相关文章：<a href=\"https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&amp;mid=2247488377&amp;idx=1&amp;sn=0fb33ef330159db5a9c8bc0f029cd739&amp;chksm=cea25cb2f9d5d5a4c7bacc9dcfc90ed86e89f4262e32b40c7aa47af84c747cb6c0429f753e1d&amp;token=496868067&amp;lang=zh_CN#rd\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub 点赞接近 70k 的 Spring Cloud 学习教程+实战项目推荐！牛批！</a></p>\n<h3>Nginx</h3>\n<ul>\n<li><a href=\"https://github.com/dunwu/nginx-tutorial\" target=\"_blank\" rel=\"noopener noreferrer\">nginx-tutorial</a>：一系列 Nginx 极简教程，包含 HTTP 反向代理、HTTPS 反向代理、负载均衡、静态站点、文件服务器搭建等实战内容。</li>\n</ul>\n<h2>大数据</h2>\n<ul>\n<li><a href=\"https://github.com/datawhalechina/juicy-bigdata\" target=\"_blank\" rel=\"noopener noreferrer\">juicy-bigdata</a>：妙趣横生大数据，大数据技术相关内容的导论课程。</li>\n<li><a href=\"https://github.com/zhisheng17/flink-learning\" title=\"flink-learning\" target=\"_blank\" rel=\"noopener noreferrer\">flink-learning</a>：含 Flink 入门、概念、原理、实战、性能调优、源码解析等内容。</li>\n</ul>\n<h2>开源书籍</h2>\n<ul>\n<li><a href=\"https://github.com/johnlui/PPHC\" target=\"_blank\" rel=\"noopener noreferrer\">《高并发的哲学原理》</a>：本书的目标是在作者有限的认知范围内，讨论一下高并发问题背后隐藏的一个哲学原理——找出单点，进行拆分。</li>\n<li><a href=\"https://github.com/clxering/Effective-Java-3rd-edition-Chinese-English-bilingual\" target=\"_blank\" rel=\"noopener noreferrer\">《Effective Java（第 3 版）》中英对照版</a>：《Effective Java（第 3 版）各章节的中英文学习参考。</li>\n<li><a href=\"https://github.com/Vonng/ddia\" target=\"_blank\" rel=\"noopener noreferrer\">《DDIA（设计数据密集型应用）》中文版</a>：《Designing Data-Intensive Application》DDIA 中文翻译。</li>\n<li><a href=\"https://github.com/fenixsoft/awesome-fenix\" target=\"_blank\" rel=\"noopener noreferrer\">《凤凰架构》</a>：讨论如何构建一套可靠的大型分布式系统。</li>\n<li><a href=\"https://github.com/dreamhead/patterns-of-distributed-systems\" target=\"_blank\" rel=\"noopener noreferrer\">《分布式系统模式》中文版</a>：《Patterns of Distributed Systems》中文翻译。</li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "开源项目"
      ]
    },
    {
      "title": "J2EE 基础知识",
      "url": "https://codingmap.strova.top/system-design/J2EE%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.html",
      "id": "https://codingmap.strova.top/system-design/J2EE%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.html",
      "summary": "J2EE基础知识详解，涵盖Servlet生命周期、请求转发与重定向、Session与Cookie机制等Java Web核心概念。重点围绕 阐述 Servlet 和 CGI 的区别?、Servlet 接口中有哪些方法及 Servlet 生命周期探秘、GET 和 POST 的区别、什么情况下调用 doGet()和 doP。",
      "content_html": "\n<p>在 Java Web 程序中，<strong>Servlet</strong>主要负责接收用户请求 <code>HttpServletRequest</code>,在<code>doGet()</code>,<code>doPost()</code>中做相应的处理，并将回应<code>HttpServletResponse</code>反馈给用户。<strong>Servlet</strong> 可以设置初始化参数，供 Servlet 内部使用。一个 Servlet 类只会有一个实例，在它初始化时调用<code>init()</code>方法，销毁时调用<code>destroy()</code>方法<strong>。</strong>Servlet 需要在 web.xml 中配置（MyEclipse 中创建 Servlet 会自动配置），<strong>一个 Servlet 可以设置多个 URL 访问</strong>。<strong>Servlet 不是线程安全</strong>，因此要谨慎使用类变量。</p>\n<h2>阐述 Servlet 和 CGI 的区别?</h2>\n<h3>CGI 的不足之处</h3>\n<p>1，需要为每个请求启动一个操作 CGI 程序的系统进程。如果请求频繁，这将会带来很大的开销。</p>\n<p>2，需要为每个请求加载和运行一个 CGI 程序，这将带来很大的开销</p>\n<p>3，需要重复编写处理网络协议的代码以及编码，这些工作都是非常耗时的。</p>\n<h3>Servlet 的优点</h3>\n<p>1，只需要启动一个操作系统进程以及加载一个 JVM，大大降低了系统的开销</p>\n<p>2，如果多个请求需要做同样处理的时候，这时候只需要加载一个类，这也大大降低了开销</p>\n<p>3，所有动态加载的类可以实现对网络协议以及请求解码的共享，大大降低了工作量。</p>\n<p>4，Servlet 能直接和 Web 服务器交互，而普通的 CGI 程序不能。Servlet 还能在各个程序之间共享数据，使数据库连接池之类的功能很容易实现。</p>\n<p>补充：Sun Microsystems 公司在 1996 年发布 Servlet 技术就是为了和 CGI 进行竞争，Servlet 是一个特殊的 Java 程序，一个基于 Java 的 Web 应用通常包含一个或多个 Servlet 类。Servlet 不能够自行创建并执行，它是在 Servlet 容器中运行的，容器将用户的请求传递给 Servlet 程序，并将 Servlet 的响应回传给用户。通常一个 Servlet 会关联一个或多个 JSP 页面。以前 CGI 经常因为性能开销上的问题被诟病，然而 Fast CGI 早就已经解决了 CGI 效率上的问题，所以面试的时候大可不必信口开河的诟病 CGI，事实上有很多你熟悉的网站都使用了 CGI 技术。</p>\n<p>参考：《javaweb 整合开发王者归来》P7</p>\n<h2>Servlet 接口中有哪些方法及 Servlet 生命周期探秘</h2>\n<p>Servlet 接口定义了 5 个方法，其中<strong>前三个方法与 Servlet 生命周期相关</strong>：</p>\n<ul>\n<li><code>void init(ServletConfig config) throws ServletException</code></li>\n<li><code>void service(ServletRequest req, ServletResponse resp) throws ServletException, java.io.IOException</code></li>\n<li><code>void destroy()</code></li>\n<li><code>java.lang.String getServletInfo()</code></li>\n<li><code>ServletConfig getServletConfig()</code></li>\n</ul>\n<p><strong>生命周期：</strong> <strong>Web 容器加载 Servlet 并将其实例化后，Servlet 生命周期开始</strong>，容器运行其<strong>init()方法</strong>进行 Servlet 的初始化；请求到达时调用 Servlet 的<strong>service()方法</strong>，service()方法会根据需要调用与请求对应的<strong>doGet 或 doPost</strong>等方法；当服务器关闭或项目被卸载时服务器会将 Servlet 实例销毁，此时会调用 Servlet 的<strong>destroy()方法</strong>。<strong>init 方法和 destroy 方法只会执行一次，service 方法客户端每次请求 Servlet 都会执行</strong>。Servlet 中有时会用到一些需要初始化与销毁的资源，因此可以把初始化资源的代码放入 init 方法中，销毁资源的代码放入 destroy 方法中，这样就不需要每次处理客户端的请求都要初始化与销毁资源。</p>\n<p>参考：《javaweb 整合开发王者归来》P81</p>\n<h2>GET 和 POST 的区别</h2>\n<p>这个问题在知乎上被讨论的挺火热的，地址：<a href=\"https://www.zhihu.com/question/28586791\" target=\"_blank\" rel=\"noopener noreferrer\">https://www.zhihu.com/question/28586791</a> 。</p>\n<p><img src=\"https://static001.geekbang.org/infoq/04/0454a5fff1437c32754f1dfcc3881148.png\" alt></p>\n<p>GET 和 POST 是 HTTP 协议中两种常用的请求方法，它们在不同的场景和目的下有不同的特点和用法。一般来说，可以从以下几个方面来区分它们：</p>\n<ul>\n<li>语义上的区别：GET 通常用于获取或查询资源，而 POST 通常用于创建或修改资源。GET 请求应该是幂等的，即多次重复执行不会改变资源的状态，而 POST 请求则可能有副作用，即每次执行可能会产生不同的结果或影响资源的状态。</li>\n<li>格式上的区别：GET 请求的参数通常放在 URL 中，形成查询字符串（querystring），而 POST 请求的参数通常放在请求体（body）中，可以有多种编码格式，如 application/x-www-form-urlencoded、multipart/form-data、application/json 等。GET 请求的 URL 长度受到浏览器和服务器的限制，而 POST 请求的 body 大小则没有明确的限制。</li>\n<li>缓存上的区别：由于 GET 请求是幂等的，它可以被浏览器或其他中间节点（如代理、网关）缓存起来，以提高性能和效率。而 POST 请求则不适合被缓存，因为它可能有副作用，每次执行可能需要实时的响应。</li>\n<li>安全性上的区别：GET 请求和 POST 请求都不是绝对安全的，因为 HTTP 协议本身是明文传输的，无论是 URL、header 还是 body 都可能被窃取或篡改。为了保证安全性，必须使用 HTTPS 协议来加密传输数据。不过，在一些场景下，GET 请求相比 POST 请求更容易泄露敏感数据，因为 GET 请求的参数会出现在 URL 中，而 URL 可能会被记录在浏览器历史、服务器日志、代理日志等地方。因此，一般情况下，私密数据传输应该使用 POST + body。</li>\n</ul>\n<p>重点搞清了，两者在语义上的区别即可。不过，也有一些项目所有的请求都用 POST，这个并不是固定的，项目组达成共识即可。</p>\n<h2>什么情况下调用 doGet()和 doPost()</h2>\n<p>Form 标签里的 method 的属性为 get 时调用 doGet()，为 post 时调用 doPost()。</p>\n<h2>转发(Forward)和重定向(Redirect)的区别</h2>\n<p><strong>转发是服务器行为，重定向是客户端行为。</strong></p>\n<p><strong>转发（Forward）</strong><br>\n通过 RequestDispatcher 对象的 forward（HttpServletRequest request,HttpServletResponse response）方法实现的。RequestDispatcher 可以通过 HttpServletRequest 的 getRequestDispatcher()方法获得。例如下面的代码就是跳转到 login_success.jsp 页面。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">     request</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getRequestDispatcher</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"login_success.jsp\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">).</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">forward</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(request, response);</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p><strong>重定向（Redirect）</strong> 是利用服务器返回的状态码来实现的。客户端浏览器请求服务器的时候，服务器会返回一个状态码。服务器通过 <code>HttpServletResponse</code> 的 <code>setStatus(int status)</code> 方法设置状态码。如果服务器返回 301 或者 302，则浏览器会到新的网址重新请求该资源。</p>\n<ol>\n<li>\n<p><strong>从地址栏显示来说</strong></p>\n<p>forward 是服务器请求资源,服务器直接访问目标地址的 URL,把那个 URL 的响应内容读取过来,然后把这些内容再发给浏览器.浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址.<br>\nredirect 是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址.所以地址栏显示的是新的 URL.</p>\n</li>\n<li>\n<p><strong>从数据共享来说</strong></p>\n<p>forward:转发页面和转发到的页面可以共享 request 里面的数据.<br>\nredirect:不能共享数据.</p>\n</li>\n<li>\n<p><strong>从运用地方来说</strong></p>\n<p>forward:一般用于用户登陆的时候,根据角色转发到相应的模块.<br>\nredirect:一般用于用户注销登陆时返回主页面和跳转到其它的网站等</p>\n</li>\n<li>\n<p>从效率来说</p>\n<p>forward:高.<br>\nredirect:低.</p>\n</li>\n</ol>\n<h2>自动刷新(Refresh)</h2>\n<p>自动刷新不仅可以实现一段时间之后自动跳转到另一个页面，还可以实现一段时间之后自动刷新本页面。Servlet 中通过 HttpServletResponse 对象设置 Header 属性实现自动刷新例如：</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">Response</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">setHeader</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"Refresh\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"5;URL=http://localhost:8080/servlet/example.htm\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>其中 5 为时间，单位为秒。URL 指定就是要跳转的页面（如果设置自己的路径，就会实现每过 5 秒自动刷新本页面一次）</p>\n<h2>Servlet 与线程安全</h2>\n<p><strong>Servlet 不是线程安全的，多线程并发的读写会导致数据不同步的问题。</strong> 解决的办法是尽量不要定义 name 属性，而是要把 name 变量分别定义在 doGet()和 doPost()方法内。虽然使用 synchronized(name){}语句块可以解决问题，但是会造成线程的等待，不是很科学的办法。<br>\n注意：多线程的并发的读写 Servlet 类属性会导致数据不同步。但是如果只是并发地读取属性而不写入，则不存在数据不同步的问题。因此 Servlet 里的只读属性最好定义为 final 类型的。</p>\n<p>参考：《javaweb 整合开发王者归来》P92</p>\n<h2>JSP 和 Servlet 是什么关系</h2>\n<p>其实这个问题在上面已经阐述过了，Servlet 是一个特殊的 Java 程序，它运行于服务器的 JVM 中，能够依靠服务器的支持向浏览器提供显示内容。JSP 本质上是 Servlet 的一种简易形式，JSP 会被服务器处理成一个类似于 Servlet 的 Java 程序，可以简化页面内容的生成。Servlet 和 JSP 最主要的不同点在于，Servlet 的应用逻辑是在 Java 文件中，并且完全从表示层中的 HTML 分离开来。而 JSP 的情况是 Java 和 HTML 可以组合成一个扩展名为.jsp 的文件。有人说，Servlet 就是在 Java 中写 HTML，而 JSP 就是在 HTML 中写 Java 代码，当然这个说法是很片面且不够准确的。JSP 侧重于视图，Servlet 更侧重于控制逻辑，在 MVC 架构模式中，JSP 适合充当视图（view）而 Servlet 适合充当控制器（controller）。</p>\n<h2>JSP 工作原理</h2>\n<p>JSP 是一种 Servlet，但是与 HttpServlet 的工作方式不太一样。HttpServlet 是先由源代码编译为 class 文件后部署到服务器下，为先编译后部署。而 JSP 则是先部署后编译。JSP 会在客户端第一次请求 JSP 文件时被编译为 HttpJspPage 类（接口 Servlet 的一个子类）。该类会被服务器临时存放在服务器工作目录里面。下面通过实例给大家介绍。<br>\n工程 JspLoginDemo 下有一个名为 login.jsp 的 Jsp 文件，把工程第一次部署到服务器上后访问这个 Jsp 文件，我们发现这个目录下多了下图这两个东东。<br>\n.class 文件便是 JSP 对应的 Servlet。编译完毕后再运行 class 文件来响应客户端请求。以后客户端访问 login.jsp 的时候，Tomcat 将不再重新编译 JSP 文件，而是直接调用 class 文件来响应客户端请求。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/1.jpeg\" alt=\"JSP工作原理\"></p>\n<p>由于 JSP 只会在客户端第一次请求的时候被编译 ，因此第一次请求 JSP 时会感觉比较慢，之后就会感觉快很多。如果把服务器保存的 class 文件删除，服务器也会重新编译 JSP。</p>\n<p>开发 Web 程序时经常需要修改 JSP。Tomcat 能够自动检测到 JSP 程序的改动。如果检测到 JSP 源代码发生了改动。Tomcat 会在下次客户端请求 JSP 时重新编译 JSP，而不需要重启 Tomcat。这种自动检测功能是默认开启的，检测改动会消耗少量的时间，在部署 Web 应用的时候可以在 web.xml 中将它关掉。</p>\n<p>参考：《javaweb 整合开发王者归来》P97</p>\n<h2>JSP 有哪些内置对象、作用分别是什么</h2>\n<p><a href=\"http://blog.csdn.net/qq_34337272/article/details/64310849\" target=\"_blank\" rel=\"noopener noreferrer\">JSP 内置对象 - CSDN 博客</a></p>\n<p>JSP 有 9 个内置对象：</p>\n<ul>\n<li>request：封装客户端的请求，其中包含来自 GET 或 POST 请求的参数；</li>\n<li>response：封装服务器对客户端的响应；</li>\n<li>pageContext：通过该对象可以获取其他对象；</li>\n<li>session：封装用户会话的对象；</li>\n<li>application：封装服务器运行环境的对象；</li>\n<li>out：输出服务器响应的输出流对象；</li>\n<li>config：Web 应用的配置对象；</li>\n<li>page：JSP 页面本身（相当于 Java 程序中的 this）；</li>\n<li>exception：封装页面抛出异常的对象。</li>\n</ul>\n<h2>Request 对象的主要方法有哪些</h2>\n<ul>\n<li><code>setAttribute(String name,Object)</code>：设置名字为 name 的 request 的参数值</li>\n<li><code>getAttribute(String name)</code>：返回由 name 指定的属性值</li>\n<li><code>getAttributeNames()</code>：返回 request 对象所有属性的名字集合，结果是一个枚举的实例</li>\n<li><code>getCookies()</code>：返回客户端的所有 Cookie 对象，结果是一个 Cookie 数组</li>\n<li><code>getCharacterEncoding()</code>：返回请求中的字符编码方式 = getContentLength()`：返回请求的 Body 的长度</li>\n<li><code>getHeader(String name)</code>：获得 HTTP 协议定义的文件头信息</li>\n<li><code>getHeaders(String name)</code>：返回指定名字的 request Header 的所有值，结果是一个枚举的实例</li>\n<li><code>getHeaderNames()</code>：返回所以 request Header 的名字，结果是一个枚举的实例</li>\n<li><code>getInputStream()</code>：返回请求的输入流，用于获得请求中的数据</li>\n<li><code>getMethod()</code>：获得客户端向服务器端传送数据的方法</li>\n<li><code>getParameter(String name)</code>：获得客户端传送给服务器端的有 name 指定的参数值</li>\n<li><code>getParameterNames()</code>：获得客户端传送给服务器端的所有参数的名字，结果是一个枚举的实例</li>\n<li><code>getParameterValues(String name)</code>：获得有 name 指定的参数的所有值</li>\n<li><code>getProtocol()</code>：获取客户端向服务器端传送数据所依据的协议名称</li>\n<li><code>getQueryString()</code>：获得查询字符串</li>\n<li><code>getRequestURI()</code>：获取发出请求字符串的客户端地址</li>\n<li><code>getRemoteAddr()</code>：获取客户端的 IP 地址</li>\n<li><code>getRemoteHost()</code>：获取客户端的名字</li>\n<li><code>getSession([Boolean create])</code>：返回和请求相关 Session</li>\n<li><code>getServerName()</code>：获取服务器的名字</li>\n<li><code>getServletPath()</code>：获取客户端所请求的脚本文件的路径</li>\n<li><code>getServerPort()</code>：获取服务器的端口号</li>\n<li><code>removeAttribute(String name)</code>：删除请求中的一个属性</li>\n</ul>\n<h2>request.getAttribute()和 request.getParameter()有何区别</h2>\n<p><strong>从获取方向来看：</strong></p>\n<p><code>getParameter()</code>是获取 POST/GET 传递的参数值；</p>\n<p><code>getAttribute()</code>是获取对象容器中的数据值；</p>\n<p><strong>从用途来看：</strong></p>\n<p><code>getParameter()</code>用于客户端重定向时，即点击了链接或提交按扭时传值用，即用于在用表单或 url 重定向传值时接收数据用。</p>\n<p><code>getAttribute()</code> 用于服务器端重定向时，即在 sevlet 中使用了 forward 函数,或 struts 中使用了<br>\nmapping.findForward。 getAttribute 只能收到程序用 setAttribute 传过来的值。</p>\n<p>另外，可以用 <code>setAttribute()</code>,<code>getAttribute()</code> 发送接收对象.而 <code>getParameter()</code> 显然只能传字符串。<br>\n<code>setAttribute()</code> 是应用服务器把这个对象放在该页面所对应的一块内存中去，当你的页面服务器重定向到另一个页面时，应用服务器会把这块内存拷贝另一个页面所对应的内存中。这样<code>getAttribute()</code>就能取得你所设下的值，当然这种方法可以传对象。session 也一样，只是对象在内存中的生命周期不一样而已。<code>getParameter()</code>只是应用服务器在分析你送上来的 request 页面的文本时，取得你设在表单或 url 重定向时的值。</p>\n<p><strong>总结：</strong></p>\n<p><code>getParameter()</code>返回的是 String,用于读取提交的表单中的值;（获取之后会根据实际需要转换为自己需要的相应类型，比如整型，日期类型啊等等）</p>\n<p><code>getAttribute()</code>返回的是 Object，需进行转换,可用<code>setAttribute()</code>设置成任意对象，使用很灵活，可随时用</p>\n<h2>include 指令 include 的行为的区别</h2>\n<p><strong>include 指令：</strong> JSP 可以通过 include 指令来包含其他文件。被包含的文件可以是 JSP 文件、HTML 文件或文本文件。包含的文件就好像是该 JSP 文件的一部分，会被同时编译执行。 语法格式如下：<br>\n&lt;%@ include file=&quot;文件相对 url 地址&quot; %&gt;</p>\n<p>i<strong>nclude 动作：</strong> <code>&lt;jsp:include&gt;</code>动作元素用来包含静态和动态的文件。该动作把指定文件插入正在生成的页面。语法格式如下：<br>\n&lt;jsp:include page=&quot;相对 URL 地址&quot; flush=&quot;true&quot; /&gt;</p>\n<h2>JSP 九大内置对象，七大动作，三大指令</h2>\n<p><a href=\"http://blog.csdn.net/qq_34337272/article/details/64310849\" target=\"_blank\" rel=\"noopener noreferrer\">JSP 九大内置对象，七大动作，三大指令总结</a></p>\n<h2>讲解 JSP 中的四种作用域</h2>\n<p>JSP 中的四种作用域包括 page、request、session 和 application，具体来说：</p>\n<ul>\n<li><strong>page</strong>代表与一个页面相关的对象和属性。</li>\n<li><strong>request</strong>代表与 Web 客户机发出的一个请求相关的对象和属性。一个请求可能跨越多个页面，涉及多个 Web 组件；需要在页面显示的临时数据可以置于此作用域。</li>\n<li><strong>session</strong>代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的 session 中。</li>\n<li><strong>application</strong>代表与整个 Web 应用程序相关的对象和属性，它实质上是跨越整个 Web 应用程序，包括多个页面、请求和会话的一个全局作用域。</li>\n</ul>\n<h2>如何实现 JSP 或 Servlet 的单线程模式</h2>\n<p>对于 JSP 页面，可以通过 page 指令进行设置。<br>\n<code>&lt;%@page isThreadSafe=&quot;false&quot;%&gt;</code></p>\n<p>对于 Servlet，可以让自定义的 Servlet 实现 SingleThreadModel 标识接口。</p>\n<p>说明：如果将 JSP 或 Servlet 设置成单线程工作模式，会导致每个请求创建一个 Servlet 实例，这种实践将导致严重的性能问题（服务器的内存压力很大，还会导致频繁的垃圾回收），所以通常情况下并不会这么做。</p>\n<h2>实现会话跟踪的技术有哪些</h2>\n<ol>\n<li>\n<p><strong>使用 Cookie</strong></p>\n<p>向客户端发送 Cookie</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Cookie</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> c </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> Cookie</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"name\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"value\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> //创建Cookie</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">c</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">setMaxAge</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">60</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">*</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">60</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">*</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">24</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> //设置最大时效，此处设置的最大时效为一天</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">response</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">addCookie</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(c);</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> //把Cookie放入到HTTP响应中</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>从客户端读取 Cookie</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> name </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"name\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Cookie</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">[]cookies </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">request</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getCookies</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(cookies </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">){</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">   for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">cookies</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">++</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">){</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    Cookie</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> cookie </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">cookies[i]</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">name</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">equals</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">cookis</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getName</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">())</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    //something is here.</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    //you can get the value</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    cookie</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getValue</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">   }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> }</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><strong>优点:</strong> 数据可以持久保存，不需要服务器资源，简单，基于文本的 Key-Value</p>\n<p><strong>缺点:</strong> 大小受到限制，用户可以禁用 Cookie 功能，由于保存在本地，有一定的安全风险。</p>\n</li>\n<li>\n<p>URL 重写</p>\n<p>在 URL 中添加用户会话的信息作为请求的参数，或者将唯一的会话 ID 添加到 URL 结尾以标识一个会话。</p>\n<p><strong>优点：</strong> 在 Cookie 被禁用的时候依然可以使用</p>\n<p><strong>缺点：</strong> 必须对网站的 URL 进行编码，所有页面必须动态生成，不能用预先记录下来的 URL 进行访问。</p>\n</li>\n<li>\n<p>隐藏的表单域</p>\n<div class=\"language-html line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"html\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-html\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">input</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> type</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">=</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"hidden\"</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> name</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">=</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"session\"</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> value</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">=</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"...\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> /></span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p><strong>优点：</strong> Cookie 被禁时可以使用</p>\n<p><strong>缺点：</strong> 所有页面必须是表单提交之后的结果。</p>\n</li>\n<li>\n<p>HttpSession</p>\n<p>在所有会话跟踪技术中，HttpSession 对象是最强大也是功能最多的。当一个用户第一次访问某个网站时会自动创建 HttpSession，每个用户可以访问他自己的 HttpSession。可以通过 HttpServletRequest 对象的 getSession 方 法获得 HttpSession，通过 HttpSession 的 setAttribute 方法可以将一个值放在 HttpSession 中，通过调用 HttpSession 对象的 getAttribute 方法，同时传入属性名就可以获取保存在 HttpSession 中的对象。与上面三种方式不同的 是，HttpSession 放在服务器的内存中，因此不要将过大的对象放在里面，即使目前的 Servlet 容器可以在内存将满时将 HttpSession 中的对象移到其他存储设备中，但是这样势必影响性能。添加到 HttpSession 中的值可以是任意 Java 对象，这个对象最好实现了 Serializable 接口，这样 Servlet 容器在必要的时候可以将其序列化到文件中，否则在序列化时就会出现异常。</p>\n</li>\n</ol>\n<h2>Cookie 和 Session 的区别</h2>\n<p>Cookie 和 Session 都是用来跟踪浏览器用户身份的会话方式，但是两者的应用场景不太一样。</p>\n<p><strong>Cookie 一般用来保存用户信息</strong> 比如 ① 我们在 Cookie 中保存已经登录过得用户信息，下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了；② 一般的网站都会有保持登录也就是说下次你再访问网站的时候就不需要重新登录了，这是因为用户登录的时候我们可以存放了一个 Token 在 Cookie 中，下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑，重新登录一般要将 Token 重写)；③ 登录一次网站后访问网站其他页面不需要重新登录。<strong>Session 的主要作用就是通过服务端记录用户的状态。</strong> 典型的场景是购物车，当你要添加商品到购物车的时候，系统不知道是哪个用户操作的，因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。</p>\n<p>Cookie 数据保存在客户端(浏览器端)，Session 数据保存在服务器端。</p>\n<p>Cookie 存储在客户端中，而 Session 存储在服务器上，相对来说 Session 安全性更高。如果使用 Cookie 的一些敏感信息不要写入 Cookie 中，最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。</p>\n",
      "image": "https://static001.geekbang.org/infoq/04/0454a5fff1437c32754f1dfcc3881148.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "系统设计"
      ]
    },
    {
      "title": "系统设计知识体系：设计模式、工程基础、认证授权、数据安全与常用框架",
      "url": "https://codingmap.strova.top/system-design/",
      "id": "https://codingmap.strova.top/system-design/",
      "summary": "系统设计面试与知识体系学习路线，涵盖设计模式、RESTful API、软件工程、代码重构、单元测试、认证授权、数据安全、Spring 和实时消息推送。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章、系统设计 等内容展开。结合 CodingMap 知识体系梳理 系统设计 的核心概念、实践方法等核心内容。",
      "content_html": "<p>这份 <strong>系统设计知识体系</strong> 面向后端学习、工程实践和面试复习，围绕“把业务需求拆成稳定、可维护、可扩展的工程方案”整理本站系统设计相关文章。</p>\n<p>系统设计不是只背秒杀、短链、Feed 流这类场景题。真正落到后端开发里，还需要理解 API 设计、代码质量、设计模式、框架原理、认证授权、数据安全、定时任务和实时消息推送等基础能力。</p>\n<p>如果你时间有限，建议先看 <a href=\"/system-design/system-design-questions.html\" target=\"_blank\">系统设计常见面试题总结</a>，快速建立高频问题清单；如果想系统补基础，可以按下面的阅读顺序推进。</p>\n<h2>适合谁看</h2>\n<ul>\n<li>正在系统学习后端系统设计的开发者。</li>\n<li>准备校招、社招、中大厂后端面试的同学。</li>\n<li>想把“会写业务代码”进一步提升到“能设计模块和方案”的工程师。</li>\n<li>已经接触 Spring、MyBatis、权限系统、定时任务等技术，但缺少系统化梳理的读者。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>系统设计面试中，如何从需求澄清、核心流程、数据模型、接口设计、扩展性和风险点组织答案？</li>\n<li>RESTful API、命名、重构、单元测试这些工程基础，如何影响长期维护成本？</li>\n<li>设计模式不是背 UML 图，而是理解常见变化点如何被封装。</li>\n<li>Spring、MyBatis、Netty 等框架分别解决了哪些工程复杂度？</li>\n<li>认证授权、JWT、SSO、权限系统和数据安全分别覆盖安全链路的哪些部分？</li>\n<li>定时任务和实时消息推送在生产环境里有哪些选型和稳定性问题？</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/system-design/system-design-questions.html\" target=\"_blank\">系统设计常见面试题总结</a>：先建立系统设计场景题和高频问题清单。</li>\n<li><a href=\"/system-design/basis/\" target=\"_blank\">系统设计基础专题</a>：补齐 RESTful API、软件工程、代码命名、代码重构和单元测试。</li>\n<li><a href=\"/system-design/design-pattern.html\" target=\"_blank\">设计模式常见面试题总结</a>：理解常见设计模式适合解决哪些变化点。</li>\n<li><a href=\"/system-design/framework/spring/\" target=\"_blank\">Spring &amp; Spring Boot 专题</a>：掌握 IoC、AOP、事务、自动装配、常用注解和框架设计模式。</li>\n<li><a href=\"/system-design/security/\" target=\"_blank\">认证授权与数据安全专题</a>：系统理解认证授权、JWT、SSO、权限系统、加密、脱敏和数据校验。</li>\n<li>再根据项目需要阅读 <a href=\"/system-design/schedule-task.html\" target=\"_blank\">Java 定时任务详解</a> 和 <a href=\"/system-design/web-real-time-message-push.html\" target=\"_blank\">Web 实时消息推送详解</a>。</li>\n</ol>\n<h2>核心文章</h2>\n<h3>系统设计与工程基础</h3>\n<ul>\n<li><a href=\"/system-design/system-design-questions.html\" target=\"_blank\">系统设计常见面试题总结</a>：覆盖短链系统、秒杀系统、海量数据处理等系统设计场景题。</li>\n<li><a href=\"/system-design/basis/\" target=\"_blank\">系统设计基础专题</a>：从 RESTful API、软件工程讲到代码命名、代码重构和单元测试。</li>\n<li><a href=\"/system-design/basis/RESTfulAPI.html\" target=\"_blank\">RestFul API 简明教程</a>：理解资源建模、HTTP 方法、状态码和接口设计规范。</li>\n<li><a href=\"/system-design/basis/refactoring.html\" target=\"_blank\">代码重构指南</a>：理解代码坏味道、重构原则和常见重构手法。</li>\n<li><a href=\"/system-design/basis/unit-test.html\" target=\"_blank\">单元测试到底是什么？应该怎么做？</a>：理解单元测试、Mock、Stub、测试金字塔和 JUnit 基础。</li>\n<li><a href=\"/system-design/design-pattern.html\" target=\"_blank\">设计模式常见面试题总结</a>：梳理单例、工厂、代理、责任链、策略、观察者等常见设计模式。</li>\n</ul>\n<h3>常用框架</h3>\n<ul>\n<li><a href=\"/system-design/framework/spring/\" target=\"_blank\">Spring &amp; Spring Boot 专题</a>：串联 Spring、Spring MVC、Spring Boot 的常见问题。</li>\n<li><a href=\"/system-design/framework/spring/spring-knowledge-and-questions-summary.html\" target=\"_blank\">Spring常见面试题总结</a>：覆盖 IoC、AOP、Bean 生命周期、依赖注入等核心问题。</li>\n<li><a href=\"/system-design/framework/spring/springboot-knowledge-and-questions-summary.html\" target=\"_blank\">SpringBoot常见面试题总结</a>：覆盖自动配置、Starter、配置加载和 Actuator 等问题。</li>\n<li><a href=\"/system-design/framework/mybatis/mybatis-interview.html\" target=\"_blank\">MyBatis常见面试题总结</a>：理解 <code>#{}</code>、<code>${}</code>、动态 SQL、缓存、分页插件和 Mapper 映射。</li>\n<li><a href=\"/system-design/framework/netty.html\" target=\"_blank\">Netty常见面试题总结</a>：了解高性能网络编程、Reactor 模型、事件循环和 ChannelPipeline。</li>\n</ul>\n<h3>认证授权与数据安全</h3>\n<ul>\n<li><a href=\"/system-design/security/\" target=\"_blank\">认证授权与数据安全专题</a>：围绕认证授权、JWT、SSO、权限系统、加密、脱敏和数据校验展开。</li>\n<li><a href=\"/system-design/security/basis-of-authority-certification.html\" target=\"_blank\">认证授权基础概念详解</a>：理解认证、授权、Session、Token、OAuth2 等基础概念。</li>\n<li><a href=\"/system-design/security/jwt-intro.html\" target=\"_blank\">JWT 基础概念详解</a>：理解 JWT 组成、签名、工作流程和登录鉴权场景。</li>\n<li><a href=\"/system-design/security/design-of-authority-system.html\" target=\"_blank\">权限系统设计详解</a>：理解 RBAC 权限模型和权限系统设计。</li>\n<li><a href=\"/system-design/security/encryption-algorithms.html\" target=\"_blank\">常见加密算法总结</a>：理解对称加密、非对称加密、哈希算法和常见应用场景。</li>\n<li><a href=\"/system-design/security/data-desensitization.html\" target=\"_blank\">数据脱敏方案总结</a>：理解常见敏感数据脱敏规则和工程实现。</li>\n</ul>\n<h3>任务调度与消息推送</h3>\n<ul>\n<li><a href=\"/system-design/schedule-task.html\" target=\"_blank\">Java 定时任务详解</a>：梳理 Timer、ScheduledThreadPoolExecutor、DelayQueue、时间轮、Spring @Scheduled 和分布式任务调度框架。</li>\n<li><a href=\"/system-design/web-real-time-message-push.html\" target=\"_blank\">Web 实时消息推送详解</a>：理解短轮询、长轮询、SSE、WebSocket 和消息推送选型。</li>\n<li><a href=\"/system-design/J2EE%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.html\" target=\"_blank\">J2EE 基础知识</a>：回顾 Servlet、请求转发与重定向、Session 和 Cookie 等 Java Web 基础。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>系统设计题应该按什么结构回答？</li>\n<li>RESTful API 设计里，资源路径、HTTP 方法和状态码应该如何使用？</li>\n<li>代码命名、重构和单元测试为什么会影响系统可维护性？</li>\n<li>常见设计模式分别解决什么问题？在 Spring 里有哪些应用？</li>\n<li>Spring IoC、AOP、Bean 生命周期和事务机制怎么理解？</li>\n<li>Spring Boot 自动装配的核心流程是什么？</li>\n<li>MyBatis 的 <code>#{}</code> 和 <code>${}</code> 有什么区别？一级缓存和二级缓存如何工作？</li>\n<li>认证和授权有什么区别？Session、Token、JWT、OAuth2 分别适合什么场景？</li>\n<li>RBAC 权限系统应该如何设计？</li>\n<li>数据脱敏、数据校验和加密算法分别解决什么安全问题？</li>\n<li>定时任务框架应该如何选型？分布式任务调度要注意哪些问题？</li>\n<li>Web 实时消息推送有哪些方案？短轮询、SSE 和 WebSocket 如何取舍？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/high-performance/\" target=\"_blank\">高性能系统知识体系</a></li>\n<li><a href=\"/high-availability/\" target=\"_blank\">高可用系统知识体系</a></li>\n<li><a href=\"/distributed-system/\" target=\"_blank\">分布式系统知识体系</a></li>\n<li><a href=\"/database/\" target=\"_blank\">数据库</a></li>\n<li><a href=\"/cs-basics/network/other-network-questions.html\" target=\"_blank\">计算机网络</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "系统设计"
      ]
    },
    {
      "title": "设计模式常见面试题总结",
      "url": "https://codingmap.strova.top/system-design/design-pattern.html",
      "id": "https://codingmap.strova.top/system-design/design-pattern.html",
      "summary": "设计模式(Design pattern)代表了最佳的实践，通常被有经验的面向对象 的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临 的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当⻓的 一段时间的试验和错误总结出来的。结合 CodingMap 知识体系梳理 系统设计 / Design Pa。",
      "content_html": "<p><strong>设计模式</strong> 相关的面试题已经整理到了 PDF 手册中，你可以在我的公众号“<strong>JavaGuide</strong>”后台回复“<strong>PDF</strong>” 获取。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png\" alt=\"JavaGuide 官方公众号\"></p>\n<p><strong>《设计模式》PDF 电子书内容概览</strong>：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/design-pattern-pdf.png\" alt=\"《设计模式》PDF文档概览\"></p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "系统设计"
      ]
    },
    {
      "title": "Java 定时任务详解",
      "url": "https://codingmap.strova.top/system-design/schedule-task.html",
      "id": "https://codingmap.strova.top/system-design/schedule-task.html",
      "summary": "系统讲解 Java 定时任务与延时任务：Timer、ScheduledThreadPoolExecutor、DelayQueue、时间轮、Spring @Scheduled（Cron 表达式），以及 Quartz、XXL-JOB、ElasticJob、PowerJob 等分布式任务调度框架的选型对比与适用场景（订单超。",
      "content_html": "<h2>为什么需要定时任务？</h2>\n<p>我们来看一下几个非常常见的业务场景：</p>\n<ol>\n<li>某系统凌晨 1 点要进行数据备份。</li>\n<li>某电商平台，用户下单半个小时未支付的情况下需要自动取消订单。</li>\n<li>某媒体聚合平台，每 10 分钟动态抓取某某网站的数据为自己所用。</li>\n<li>某博客平台，支持定时发送文章。</li>\n<li>某基金平台，每晚定时计算用户当日收益情况并推送给用户最新的数据。</li>\n<li>……</li>\n</ol>\n<p>这些场景往往都要求我们在某个特定的时间去做某个事情，也就是定时或者延时去做某个事情。</p>\n<ul>\n<li>定时任务：在指定时间点执行特定的任务，例如每天早上 8 点，每周一下午 3 点等。定时任务可以用来做一些周期性的工作，如数据备份，日志清理，报表生成等。</li>\n<li>延时任务：一定的延迟时间后执行特定的任务，例如 10 分钟后，3 小时后等。延时任务可以用来做一些异步的工作，如订单取消，推送通知，红包撤回等。</li>\n</ul>\n<p>尽管二者的适用场景有所区别，但它们的核心思想都是将任务的执行时间安排在未来的某个点上，以达到预期的调度效果。</p>\n<h2>单机定时任务</h2>\n<h3>Timer</h3>\n<p><code>java.util.Timer</code>是 JDK 1.3 开始就已经支持的一种定时任务的实现方式。</p>\n<p><code>Timer</code> 内部使用一个叫做 <code>TaskQueue</code> 的类存放定时任务，它是一个基于最小堆实现的优先级队列。<code>TaskQueue</code> 会按照任务距离下一次执行时间的大小将任务排序，保证在堆顶的任务最先执行。这样在需要执行任务时，每次只需要取出堆顶的任务运行即可！</p>\n<p><code>Timer</code> 使用起来比较简单，通过下面的方式我们就能创建一个 1s 之后执行的定时任务。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 示例代码：</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">TimerTask</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> task </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> TimerTask</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">() {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> run</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"当前时间: \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> Date</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"n\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">                \"线程名称: \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Thread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">currentThread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">().</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getName</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"当前时间: \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> Date</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"n\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">        \"线程名称: \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Thread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">currentThread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">().</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getName</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Timer</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> timer </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> Timer</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"Timer\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">long</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> delay </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1000L</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">timer</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">schedule</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(task, delay);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">//输出：</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">当前时间</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Fri</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> May </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">28</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 15</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">18</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">47</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> CST 2021n线程名称</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> main</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">当前时间</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Fri</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> May </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">28</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 15</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">18</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">48</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> CST 2021n线程名称</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Timer</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>不过其缺陷较多，比如一个 <code>Timer</code> 一个线程，这就导致 <code>Timer</code> 的任务的执行只能串行执行，一个任务执行时间过长的话会影响其他任务（性能非常差），再比如发生异常时任务直接停止（<code>Timer</code> 只捕获了 <code>InterruptedException</code> ）。</p>\n<p><code>Timer</code> 类上的有一段注释是这样写的：</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> *</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E06C75\"> This </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> does</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E06C75\"> not offer real-time guarantees: it schedules</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E06C75\"> * tasks using the </span><span style=\"--shiki-light:#C18401;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">tt</span><span style=\"--shiki-light:#C18401;--shiki-dark:#ABB2BF\">></span><span style=\"--shiki-light:#C18401;--shiki-dark:#E06C75\">Object.wait(long)</span><span style=\"--shiki-light:#C18401;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E06C75\">/</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">tt</span><span style=\"--shiki-light:#C18401;--shiki-dark:#ABB2BF\">></span><span style=\"--shiki-light:#C18401;--shiki-dark:#E06C75\"> method.</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E06C75\"> *Java 5.0 introduced the </span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">{@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">code</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> java</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">util</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">concurrent</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> package</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\"> and</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\"> * one of the concurrency utilities therein is the {@link</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\"> * java.util.concurrent.</span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">S</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">cheduled</span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">T</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">hread</span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">P</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">ool</span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">E</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">xecutor</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\"> * </span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">S</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">cheduled</span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">T</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">hread</span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">P</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">ool</span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">E</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">xecutor} which is a thread pool </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#FFFFFF\">for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\"> repeatedly</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\"> * executing tasks at a given rate or delay.  </span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">I</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">t is effectively a more</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\"> * versatile replacement </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#FFFFFF\">for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\"> the {@code </span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">T</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">imer}/{@code </span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">T</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">imer</span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">T</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">ask}</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\"> * combination, as it allows multiple service threads, accepts various</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\"> * time units, and doesn't require subclassing {@code </span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">T</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">imer</span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">T</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">ask} (just</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\"> * implement {@code </span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">R</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">unnable}).  </span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">C</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">onfiguring {@code</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\"> * </span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">S</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">cheduled</span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">T</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">hread</span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">P</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">ool</span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">E</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">xecutor} with one thread makes it equivalent to</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\"> * {@code </span><span style=\"--shiki-light:#000000;--shiki-dark:#FFFFFF\">T</span><span style=\"--shiki-light:#383A42;--shiki-dark:#C678DD\">imer}.</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>大概的意思就是：<code>ScheduledThreadPoolExecutor</code> 支持多线程执行定时任务并且功能更强大，是 <code>Timer</code> 的替代品。</p>\n<h3>ScheduledExecutorService</h3>\n<p><code>ScheduledExecutorService</code> 是一个接口，有多个实现类，比较常用的是 <code>ScheduledThreadPoolExecutor</code> 。</p>\n<p><img src=\"https://oss.javaguide.cn/javaguide/20210607154324712.png\" alt></p>\n<p><code>ScheduledThreadPoolExecutor</code> 本身就是一个线程池，支持任务并发执行。并且，其内部使用 <code>DelayedWorkQueue</code> 作为任务队列。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 示例代码：</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">TimerTask</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> repeatedTask </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> TimerTask</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">() {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">SneakyThrows</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> run</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"当前时间: \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> Date</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"n\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">                \"线程名称: \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Thread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">currentThread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">().</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getName</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"当前时间: \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> Date</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"n\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">        \"线程名称: \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Thread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">currentThread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">().</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getName</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">ScheduledExecutorService</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> executor </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Executors</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">newScheduledThreadPool</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">3</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">long</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> delay  </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1000L</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">long</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> period </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1000L</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">executor</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">scheduleAtFixedRate</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(repeatedTask, delay, period, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">TimeUnit</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">MILLISECONDS</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">Thread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">sleep</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(delay </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> period </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">*</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 5</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">executor</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">shutdown</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">//输出：</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">当前时间</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Fri</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> May </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">28</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 15</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">40</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">46</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> CST 2021n线程名称</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> main</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">当前时间</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Fri</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> May </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">28</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 15</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">40</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">47</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> CST 2021n线程名称</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> pool</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">thread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">当前时间</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Fri</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> May </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">28</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 15</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">40</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">48</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> CST 2021n线程名称</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> pool</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">thread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">当前时间</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Fri</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> May </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">28</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 15</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">40</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">49</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> CST 2021n线程名称</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> pool</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">thread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">2</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">当前时间</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Fri</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> May </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">28</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 15</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">40</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">50</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> CST 2021n线程名称</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> pool</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">thread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">2</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">当前时间</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Fri</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> May </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">28</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 15</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">40</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">51</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> CST 2021n线程名称</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> pool</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">thread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">2</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">当前时间</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Fri</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> May </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">28</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 15</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">40</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">52</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> CST 2021n线程名称</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> pool</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">thread</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">2</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>不论是使用 <code>Timer</code> 还是 <code>ScheduledExecutorService</code> 都无法使用 Cron 表达式指定任务执行的具体时间。</p>\n<h3>DelayQueue</h3>\n<p><code>DelayQueue</code> 是 JUC 包(<code>java.util.concurrent)</code>为我们提供的延迟队列，用于实现延时任务比如订单下单 15 分钟未支付直接取消。它是 <code>BlockingQueue</code> 的一种，底层是一个基于 <code>PriorityQueue</code> 实现的一个无界队列，是线程安全的。关于<code>PriorityQueue</code>可以参考笔者编写的这篇文章：<a href=\"https://javaguide.cn/java/collection/priorityqueue-source-code.html\" target=\"_blank\" rel=\"noopener noreferrer\">PriorityQueue 源码分析</a> 。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/java/collection/blocking-queue-hierarchy.png\" alt=\"BlockingQueue 的实现类\"></p>\n<p><code>DelayQueue</code> 和 <code>Timer/TimerTask</code> 都可以用于实现定时任务调度，但是它们的实现方式不同。<code>DelayQueue</code> 是基于优先级队列和堆排序算法实现的，可以实现多个任务按照时间先后顺序执行；而 <code>Timer/TimerTask</code> 是基于单线程实现的，只能按照任务的执行顺序依次执行，如果某个任务执行时间过长，会影响其他任务的执行。另外，<code>DelayQueue</code> 还支持动态添加和移除任务，而 <code>Timer/TimerTask</code> 只能在创建时指定任务。</p>\n<p>关于 <code>DelayQueue</code> 的详细介绍，请参考我写的这篇文章：<a href=\"https://javaguide.cn/java/collection/delayqueue-source-code.html\" target=\"_blank\" rel=\"noopener noreferrer\"><code>DelayQueue</code> 源码分析</a>。</p>\n<h3>Spring Task</h3>\n<p>我们直接通过 Spring 提供的 <code>@Scheduled</code> 注解即可定义定时任务，非常方便！</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">/**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * cron：使用Cron表达式。　每分钟的1，2秒运行</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Scheduled</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">cron</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"1-2 * * * * ? \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> reportCurrentTimeWithCronExpression</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">() {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">  log</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">info</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"Cron Expression: The time is now {}\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">dateFormat</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">format</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> Date</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()));</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>我在大学那会做的一个 SSM 的企业级项目，就是用的 Spring Task 来做的定时任务。</p>\n<p>并且，Spring Task 还是支持 <strong>Cron 表达式</strong> 的。Cron 表达式主要用于定时作业(定时任务)系统定义执行时间或执行频率的表达式，非常厉害，你可以通过 Cron 表达式进行设置定时任务每天或者每个月什么时候执行等等操作。咱们要学习定时任务的话，Cron 表达式是一定是要重点关注的。推荐一个在线 Cron 表达式生成器：<a href=\"http://cron.qqe2.com/\" target=\"_blank\" rel=\"noopener noreferrer\">http://cron.qqe2.com/</a> 。</p>\n<p>但是，Spring 自带的定时调度只支持单机，并且提供的功能比较单一。之前写过一篇文章:<a href=\"https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&amp;mid=2247485563&amp;idx=1&amp;sn=7419341f04036a10b141b74624a3f8c9&amp;chksm=cea247b0f9d5cea6440759e6d49b4e77d06f4c99470243a10c1463834e873ca90266413fbc92&amp;token=2133161636&amp;lang=zh_CN#rd\" target=\"_blank\" rel=\"noopener noreferrer\">《5 分钟搞懂如何在 Spring Boot 中 Schedule Tasks》</a> ，不了解的小伙伴可以参考一下。</p>\n<p>Spring Task 底层是基于 JDK 的 <code>ScheduledThreadPoolExecutor</code> 线程池来实现的。</p>\n<p><strong>优缺点总结：</strong></p>\n<ul>\n<li>优点：简单，轻量，支持 Cron 表达式</li>\n<li>缺点：功能单一</li>\n</ul>\n<h3>时间轮</h3>\n<p>Kafka、Dubbo、ZooKeeper、Netty、Caffeine、Akka 中都有对时间轮的实现。</p>\n<p>时间轮简单来说就是一个环形的队列（底层一般基于数组实现），队列中的每一个元素（时间格）都可以存放一个定时任务列表。</p>\n<p>时间轮中的每个时间格代表了时间轮的基本时间跨度或者说时间精度，假如时间一秒走一个时间格的话，那么这个时间轮的最高精度就是 1 秒（也就是说 3 s 和 3.9s 会在同一个时间格中）。</p>\n<p>下图是一个有 12 个时间格的时间轮，转完一圈需要 12 s。当我们需要新建一个 3s 后执行的定时任务，只需要将定时任务放在下标为 3 的时间格中即可。当我们需要新建一个 9s 后执行的定时任务，只需要将定时任务放在下标为 9 的时间格中即可。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/schedule-task/one-layers-of-time-wheel.png\" alt></p>\n<p>那当我们需要创建一个 13s 后执行的定时任务怎么办呢？这个时候可以引入一叫做 <strong>圈数/轮数</strong> 的概念，也就是说这个任务还是放在下标为 1 的时间格中， 不过它的圈数为 2 。</p>\n<p>除了增加圈数这种方法之外，还有一种 <strong>多层次时间轮</strong> （类似手表），Kafka 采用的就是这种方案。</p>\n<p>针对下图的时间轮，我来举一个例子便于大家理解。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/schedule-task/three-layers-of-time-wheel.png\" alt></p>\n<p>上图的时间轮(ms -&gt; s)，第 1 层的时间精度为 1 ，第 2 层的时间精度为 20 ，第 3 层的时间精度为 400。假如我们需要添加一个 350s 后执行的任务 A 的话（当前时间是 0s），这个任务会被放在第 2 层（因为第二层的时间跨度为 20*20=400&gt;350）的第 350/20=17 个时间格子。</p>\n<p>当第一层转了 17 圈之后，时间过去了 340s ，第 2 层的指针此时来到第 17 个时间格子。此时，第 2 层第 17 个格子的任务会被移动到第 1 层。</p>\n<p>任务 A 当前是 10s 之后执行，因此它会被移动到第 1 层的第 10 个时间格子。</p>\n<p>这里在层与层之间的移动也叫做时间轮的升降级。参考手表来理解就好！</p>\n<p><strong>时间轮比较适合任务数量比较多的定时任务场景，它的任务写入和执行的时间复杂度都是 0（1）。</strong></p>\n<h2>分布式定时任务</h2>\n<h3>Redis</h3>\n<p>Redis 是可以用来做延时任务的，基于 Redis 实现延时任务的功能无非就下面两种方案：</p>\n<ol>\n<li>Redis 过期事件监听</li>\n<li>Redisson 内置的延时队列</li>\n</ol>\n<p>这部分内容的详细介绍我放在了<a href=\"https://javaguide.cn/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.html\" target=\"_blank\" rel=\"noopener noreferrer\">《后端面试高频系统设计&amp;场景题》</a>中，有需要的同学可以进入星球后阅读学习。篇幅太多，这里就不重复分享了。</p>\n<p><img src=\"https://oss.javaguide.cn/xingqiu/back-end-interview-high-frequency-system-design-and-scenario-questions-fengmian.png\" alt=\"《后端面试高频系统设计&amp;场景题》\"></p>\n<h3>MQ</h3>\n<p>大部分消息队列，例如 RocketMQ、RabbitMQ，都支持定时/延时消息。定时消息和延时消息本质其实是相同的，都是服务端根据消息设置的定时时间在某一固定时刻将消息投递给消费者消费。</p>\n<p>不过，在使用 MQ 定时消息之前一定要看清楚其使用限制，以免不适合项目需求，例如 RocketMQ 定时时长最大值默认为 24 小时且不支持自定义修改、只支持 18 个 Level 的延时并不支持任意时间。</p>\n<p><strong>优缺点总结：</strong></p>\n<ul>\n<li><strong>优点</strong>：可以与 Spring 集成、支持分布式、支持集群、性能不错</li>\n<li><strong>缺点</strong>：功能性较差、不灵活、需要保障消息可靠性</li>\n</ul>\n<h2>分布式任务调度框架</h2>\n<p>如果我们需要一些高级特性比如支持任务在分布式场景下的分片和高可用的话，我们就需要用到分布式任务调度框架了。</p>\n<p>通常情况下，一个分布式定时任务的执行往往涉及到下面这些角色：</p>\n<ul>\n<li><strong>任务</strong>：首先肯定是要执行的任务，这个任务就是具体的业务逻辑比如定时发送文章。</li>\n<li><strong>调度器</strong>：其次是调度中心，调度中心主要负责任务管理，会分配任务给执行器。</li>\n<li><strong>执行器</strong>：最后就是执行器，执行器接收调度器分派的任务并执行。</li>\n</ul>\n<h3>Quartz</h3>\n<p>一个很火的开源任务调度框架，完全由 Java 写成。Quartz 可以说是 Java 定时任务领域的老大哥或者说参考标准，其他的任务调度框架基本都是基于 Quartz 开发的，比如当当网的<code>elastic-job</code>就是基于 Quartz 二次开发之后的分布式调度解决方案。</p>\n<p>使用 Quartz 可以很方便地与 Spring 集成，并且支持动态添加任务和集群。但是，Quartz 使用起来也比较麻烦，API 繁琐。</p>\n<p>并且，Quartz 并没有内置 UI 管理控制台，不过你可以使用 <a href=\"https://github.com/zhaopeiym/quartzui\" target=\"_blank\" rel=\"noopener noreferrer\">quartzui</a> 这个开源项目来解决这个问题。</p>\n<p>另外，Quartz 虽然也支持分布式任务。但是，它是在数据库层面，通过数据库的锁机制做的，有非常多的弊端比如系统侵入性严重、节点负载不均衡。有点伪分布式的味道。</p>\n<p><strong>优缺点总结：</strong></p>\n<ul>\n<li>优点：可以与 Spring 集成，并且支持动态添加任务和集群。</li>\n<li>缺点：分布式支持不友好，不支持任务可视化管理、使用麻烦（相比于其他同类型框架来说）</li>\n</ul>\n<h3>Elastic-Job</h3>\n<p>ElasticJob 当当网开源的一个面向互联网生态和海量任务的分布式调度解决方案，由两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。</p>\n<p>ElasticJob-Lite 和 ElasticJob-Cloud 两者的对比如下：</p>\n<p>|          | ElasticJob-Lite | ElasticJob-Cloud  |<br>\n| :</p>\n",
      "image": "https://oss.javaguide.cn/javaguide/20210607154324712.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "系统设计"
      ]
    },
    {
      "title": "系统设计常见面试题总结(付费)",
      "url": "https://codingmap.strova.top/system-design/system-design-questions.html",
      "id": "https://codingmap.strova.top/system-design/system-design-questions.html",
      "summary": "系统设计高频面试题解析，涵盖短链系统、秒杀系统、海量数据处理等场景题的设计思路与解决方案。结合 CodingMap 知识体系梳理 系统设计 / System Design Questions.Html 的核心概念、实践方法、常见问题和高频面试考点，覆盖原理分析、使用场景、方案对比与经验总结，适合开发者系统学习等核心内。",
      "content_html": "<p><strong>系统设计</strong> 相关的面试题为我的<a href=\"https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html\" target=\"_blank\" rel=\"noopener noreferrer\">知识星球</a>（点击链接即可查看详细介绍以及加入方法）专属内容，已经整理到了<a href=\"https://javaguide.cn/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.html\" target=\"_blank\" rel=\"noopener noreferrer\">《后端面试高频系统设计&amp;场景题》</a>中。</p>\n<p><strong>《后端面试高频系统设计&amp;场景题》</strong> 包含了常见的系统设计案例比如短链系统、秒杀系统以及高频的场景题比如海量数据去重、第三方授权登录。</p>\n<p><img src=\"https://oss.javaguide.cn/xingqiu/back-end-interview-high-frequency-system-design-and-scenario-questions-fengmian.png\" alt=\"《后端面试高频系统设计&amp;场景题》\"></p>\n<p>近年来，随着国内的技术面试越来越卷，越来越多的公司开始在面试中考察系统设计和场景问题，以此来更全面的考察求职者，不论是校招还是社招。不过，正常面试全是场景题的情况还是极少的，面试官一般会在面试中穿插一两个系统设计和场景题来考察你。</p>\n<p>于是，我总结了这份《后端面试高频系统设计&amp;场景题》，包含了常见的系统设计案例比如短链系统、秒杀系统以及高频的场景题比如海量数据去重、第三方授权登录。</p>\n<p>即使不是准备面试，我也强烈推荐你认真阅读这一系列文章，这对于提升自己系统设计思维和解决实际问题的能力还是非常有帮助的。并且，涉及到的很多案例都可以用到自己的项目上比如抽奖系统设计、第三方授权登录、Redis 实现延时任务的正确方式。</p>\n<p>《后端面试高频系统设计&amp;场景题》本身是属于<a href=\"https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html\" target=\"_blank\" rel=\"noopener noreferrer\">《Java 面试指北》</a>的一部分，后面由于内容篇幅较多，因此被单独提了出来。</p>\n",
      "image": "https://oss.javaguide.cn/xingqiu/back-end-interview-high-frequency-system-design-and-scenario-questions-fengmian.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "Java面试指北"
      ]
    },
    {
      "title": "Web 实时消息推送详解",
      "url": "https://codingmap.strova.top/system-design/web-real-time-message-push.html",
      "id": "https://codingmap.strova.top/system-design/web-real-time-message-push.html",
      "summary": "消息推送通常是指网站的运营工作等人员，通过某种工具对用户当前网页或移动设备 APP 进行的主动消息推送。重点围绕 什么是消息推送？、消息推送常见方案、总结 等内容展开。结合 CodingMap 知识体系梳理 系统设计 / Web Real Time Message Push.Html 的核心概念、实践方法等核心内容。",
      "content_html": "<blockquote>\n<p>原文地址：<a href=\"https://juejin.cn/post/7122014462181113887%EF%BC%8CJavaGuide\" target=\"_blank\" rel=\"noopener noreferrer\">https://juejin.cn/post/7122014462181113887，JavaGuide</a> 对本文进行了完善总结。</p>\n</blockquote>\n<p>我有一个朋友做了一个小破站，现在要实现一个站内信 Web 消息推送的功能，对，就是下图这个小红点，一个很常用的功能。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192380.png\" alt=\"站内信 Web 消息推送\"></p>\n<p>不过他还没想好用什么方式做，这里我帮他整理了一下几种方案，并简单做了实现。</p>\n<h2>什么是消息推送？</h2>\n<p>推送的场景比较多，比如有人关注我的公众号，这时我就会收到一条推送消息，以此来吸引我点击打开应用。</p>\n<p>消息推送通常是指网站的运营工作等人员，通过某种工具对用户当前网页或移动设备 APP 进行的主动消息推送。</p>\n<p>消息推送一般又分为 Web 端消息推送和移动端消息推送。</p>\n<p>移动端消息推送示例：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/IKleJ9auR1Ojdicyr0bH.png\" alt=\"移动端消息推送示例\"></p>\n<p>Web 端消息推送示例：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/image-20220819100512941.png\" alt=\"Web 端消息推送示例\"></p>\n<p>在具体实现之前，咱们再来分析一下前边的需求，其实功能很简单，只要触发某个事件（主动分享了资源或者后台主动推送消息），Web 页面的通知小红点就会实时的 <code>+1</code> 就可以了。</p>\n<p>通常在服务端会有若干张消息推送表，用来记录用户触发不同事件所推送不同类型的消息，前端主动查询（拉）或者被动接收（推）用户所有未读的消息数。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192384.png\" alt=\"消息推送表\"></p>\n<p>消息推送无非是推（push）和拉（pull）两种形式，下边我们逐个了解下。</p>\n<h2>消息推送常见方案</h2>\n<h3>短轮询</h3>\n<p><strong>轮询(polling)</strong> 应该是实现消息推送方案中最简单的一种，这里我们暂且将轮询分为短轮询和长轮询。</p>\n<p>短轮询很好理解，指定的时间间隔，由浏览器向服务器发出 HTTP 请求，服务器实时返回未读消息数据给客户端，浏览器再做渲染显示。</p>\n<p>一个简单的 JS 定时器就可以搞定，每秒钟请求一次未读消息数接口，返回的数据展示即可。</p>\n<div class=\"language-typescript line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"typescript\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-typescript\"><span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">setInterval</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(() </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">=></span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  // 方法请求</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">  messageCount</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">().</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">then</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">((</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\">res</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">=></span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">res</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">code</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\"> ===</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 200</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">      this</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">messageCount</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\"> res</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">data</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  });</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1000</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>效果还是可以的，短轮询实现固然简单，缺点也是显而易见，由于推送数据并不会频繁变更，无论后端此时是否有新的消息产生，客户端都会进行请求，势必会对服务端造成很大压力，浪费带宽和服务器资源。</p>\n<h3>长轮询</h3>\n<p>长轮询是对上边短轮询的一种改进版本，在尽可能减少对服务器资源浪费的同时，保证消息的相对实时性。长轮询在中间件中应用的很广泛，比如 Nacos 和 Apollo 配置中心，消息队列 Kafka、RocketMQ 中都有用到长轮询。</p>\n<p><a href=\"https://mp.weixin.qq.com/s/94ftESkDoZI9gAGflLiGwg\" target=\"_blank\" rel=\"noopener noreferrer\">Nacos 配置中心交互模型是 push 还是 pull？</a>一文中我详细介绍过 Nacos 长轮询的实现原理，感兴趣的小伙伴可以瞅瞅。</p>\n<p>长轮询其实原理跟轮询差不多，都是采用轮询的方式。不过，如果服务端的数据没有发生变更，会 一直 hold 住请求，直到服务端的数据发生变化，或者等待一定时间超时才会返回。返回后，客户端又会立即再次发起下一次长轮询。</p>\n<p>这次我使用 Apollo 配置中心实现长轮询的方式，应用了一个类<code>DeferredResult</code>，它是在 Servlet3.0 后经过 Spring 封装提供的一种异步请求机制，直意就是延迟结果。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192386.png\" alt=\"长轮询示意图\"></p>\n<p><code>DeferredResult</code>可以允许容器线程快速释放占用的资源，不阻塞请求线程，以此接受更多的请求提升系统的吞吐量，然后启动异步工作线程处理真正的业务逻辑，处理完成调用<code>DeferredResult.setResult(200)</code>提交响应结果。</p>\n<p>下边我们用长轮询来实现消息推送。</p>\n<p>因为一个 ID 可能会被多个长轮询请求监听，所以我采用了 Guava 包提供的<code>Multimap</code>结构存放长轮询，一个 key 可以对应多个 value。一旦监听到 key 发生变化，对应的所有长轮询都会响应。前端得到非请求超时的状态码，知晓数据变更，主动查询未读消息数接口，更新页面数据。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Controller</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">RequestMapping</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"/polling\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> PollingController</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 存放监听某个Id的长轮询集合</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 线程同步结构</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Multimap</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> DeferredResult</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">>></span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> watchRequests </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Multimaps</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">synchronizedMultimap</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">HashMultimap</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">create</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    /**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">     * 设置监听</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">     */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">GetMapping</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">path</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"watch/{id}\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">ResponseBody</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> DeferredResult</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> watch</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">PathVariable</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> id</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 延迟对象设置超时时间</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        DeferredResult</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">> </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">deferredResult</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> DeferredResult</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;>(TIME_OUT);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 异步请求完成时移除 key，防止内存溢出</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        deferredResult</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">onCompletion</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(() </span><span style=\"--shiki-light:#C18401;--shiki-dark:#C678DD\">-></span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            watchRequests</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">remove</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(id, deferredResult);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        });</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 注册长轮询请求</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        watchRequests</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">put</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(id, deferredResult);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> deferredResult;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    /**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">     * 变更数据</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">     */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">GetMapping</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">path</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"publish/{id}\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">ResponseBody</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> publish</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">PathVariable</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> id</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 数据变更 取出监听ID的所有长轮询请求，并一一响应处理</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">watchRequests</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">containsKey</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(id)) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">            Collection</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">DeferredResult</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">>> </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">deferredResults</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> watchRequests</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(id);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">DeferredResult</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">> </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">deferredResult</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> :</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> deferredResults) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">                deferredResult</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">setResult</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"我更新了\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> Date</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">            }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        return</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"success\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>当请求超过设置的超时时间，会抛出<code>AsyncRequestTimeoutException</code>异常，这里直接用<code>@ControllerAdvice</code>全局捕获统一返回即可，前端获取约定好的状态码后再次发起长轮询请求，如此往复调用。</p>\n<div class=\"language-kotlin line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"kotlin\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-kotlin\"><span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">@ControllerAdvice</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> AsyncRequestTimeoutHandler</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    @ResponseStatus</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(HttpStatus.NOT_MODIFIED)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    @ResponseBody</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    @ExceptionHandler</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(AsyncRequestTimeoutException.class)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> String </span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">asyncRequestTimeoutHandler</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(AsyncRequestTimeoutException e) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        System.out.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"异步请求超时\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        return</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"304\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>我们来测试一下，首先页面发起长轮询请求<code>/polling/watch/10086</code>监听消息更变，请求被挂起，不变更数据直至超时，再次发起了长轮询请求；紧接着手动变更数据<code>/polling/publish/10086</code>，长轮询得到响应，前端处理业务逻辑完成后再次发起请求，如此循环往复。</p>\n<p>长轮询相比于短轮询在性能上提升了很多，但依然会产生较多的请求，这是它的一点不完美的地方。</p>\n<h3>iframe 流</h3>\n<p>iframe 流就是在页面中插入一个隐藏的<code>&lt;iframe&gt;</code>标签，通过在<code>src</code>中请求消息数量 API 接口，由此在服务端和客户端之间创建一条长连接，服务端持续向<code>iframe</code>传输数据。</p>\n<p>传输的数据通常是 HTML、或是内嵌的 JavaScript 脚本，来达到实时更新页面的效果。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192388.png\" alt=\"iframe 流示意图\"></p>\n<p>这种方式实现简单，前端只要一个<code>&lt;iframe&gt;</code>标签搞定了</p>\n<div class=\"language-html line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"html\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-html\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">iframe</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> src</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">=</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"/iframe/message\"</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> style</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">=</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#98C379\">display:none</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">>&#x3C;/</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">iframe</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>服务端直接组装 HTML、JS 脚本数据向 response 写入就行了</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Controller</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">RequestMapping</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"/iframe\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> IframeController</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">GetMapping</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">path</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"message\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> message</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">HttpServletResponse</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> response</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> throws</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> IOException</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> InterruptedException</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        while</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">true</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            response</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">setHeader</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"Pragma\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"no-cache\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            response</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">setDateHeader</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"Expires\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            response</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">setHeader</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"Cache-Control\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"no-cache,no-store\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            response</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">setStatus</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">HttpServletResponse</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">SC_OK</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            response</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getWriter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">().</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">print</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\" &#x3C;script type=</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\">\\\"</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">text/javascript</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\">\\\"</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">></span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\">\\n</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">                    \"parent.document.getElementById('clock').innerHTML = </span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\">\\\"</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> count</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\">\\\"</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">;\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">                    \"parent.document.getElementById('count').innerHTML = </span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\">\\\"</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> count</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\">\\\"</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">;\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">                    \"&#x3C;/script>\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>iframe 流的服务器开销很大，而且 IE、Chrome 等浏览器一直会处于 loading 状态，图标会不停旋转，简直是强迫症杀手。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192389.png\" alt=\"iframe 流效果\"></p>\n<p>iframe 流非常不友好，强烈不推荐。</p>\n<h3>SSE (推荐)</h3>\n<p>很多人可能不知道，服务端向客户端推送消息，其实除了可以用<code>WebSocket</code>这种耳熟能详的机制外，还有一种服务器发送事件(Server-Sent Events)，简称 SSE。这是一种服务器端到客户端(浏览器)的单向消息推送。</p>\n<p>大名鼎鼎的 ChatGPT 就是采用的 SSE。对于需要长时间等待响应的对话场景，ChatGPT 采用了一种巧妙的策略：它会将已经计算出的数据“推送”给用户，并利用 SSE 技术在计算过程中持续返回数据。这样做的好处是可以避免用户因等待时间过长而选择关闭页面。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/chatgpt-sse.png\" alt=\"ChatGPT 使用 SSE 实现对话\"></p>\n<p>SSE 基于 HTTP 协议的，我们知道一般意义上的 HTTP 协议是无法做到服务端主动向客户端推送消息的，但 SSE 是个例外，它变换了一种思路。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192390.png\" alt=\"SSE 图解\"></p>\n<p>SSE 在服务器和客户端之间打开一个单向通道，服务端响应的不再是一次性的数据包而是<code>text/event-stream</code>类型的数据流信息，在有数据变更时从服务器流式传输到客户端。</p>\n<p>整体的实现思路有点类似于在线视频播放，视频流会连续不断的推送到浏览器，你也可以理解成，客户端在完成一次用时很长（网络不畅）的下载。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192391.png\" alt=\"SSE 示意图\"></p>\n<p>SSE 与 WebSocket 作用相似，都可以建立服务端与浏览器之间的通信，实现服务端向客户端推送消息，但还是有些许不同：</p>\n<ul>\n<li>SSE 是基于 HTTP 协议的，它们不需要特殊的协议或服务器实现即可工作；WebSocket 需单独服务器来处理协议。</li>\n<li>SSE 单向通信，只能由服务端向客户端单向通信；WebSocket 全双工通信，即通信的双方可以同时发送和接受信息。</li>\n<li>SSE 实现简单开发成本低，无需引入其他组件；WebSocket 传输数据需做二次解析，开发门槛高一些。</li>\n<li>SSE 默认支持断线重连；WebSocket 则需要自己实现。</li>\n<li>SSE 只能传送文本消息，二进制数据需要经过编码后传送；WebSocket 默认支持传送二进制数据。</li>\n</ul>\n<p><strong>SSE 与 WebSocket 该如何选择？</strong></p>\n<blockquote>\n<p>技术并没有好坏之分，只有哪个更合适。</p>\n</blockquote>\n<p>SSE 好像一直不被大家所熟知，一部分原因是出现了 WebSocket，这个提供了更丰富的协议来执行双向、全双工通信。对于游戏、即时通信以及需要双向近乎实时更新的场景，拥有双向通道更具吸引力。</p>\n<p>但是，在某些情况下，不需要从客户端发送数据。而你只需要一些服务器操作的更新。比如：站内信、未读消息数、状态更新、股票行情、监控数量等场景，SSE 不管是从实现的难易和成本上都更加有优势。此外，SSE 具有 WebSocket 在设计上缺乏的多种功能，例如：自动重新连接、事件 ID 和发送任意事件的能力。</p>\n<p>前端只需进行一次 HTTP 请求，带上唯一 ID，打开事件流，监听服务端推送的事件就可以了</p>\n<div class=\"language-javascript line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"javascript\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-javascript\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">script</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    let source = null;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    let userId = 7777</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    if (window.EventSource) </span><span style=\"--shiki-light:#CA1243;--shiki-dark:#C678DD\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 建立连接</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">        source</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> EventSource</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'http://localhost:7777/sse/sub/'</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">userId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">        setMessageInnerHTML</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"连接用户=\"</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> userId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        /**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">         * 连接一旦建立，就会触发open事件</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">         * 另一种写法：source.onopen = function (event) {}</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">         */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">        source</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">addEventListener</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'open'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">function</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\">e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">            setMessageInnerHTML</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"建立连接。。。\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }, </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">false</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        /**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">         * 客户端收到服务器发来的数据</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">         * 另一种写法：source.onmessage = function (event) {}</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">         */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">        source</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">addEventListener</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'message'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">function</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\">e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">            setMessageInnerHTML</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">data</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        });</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#CA1243;--shiki-dark:#C678DD\">    }</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> else </span><span style=\"--shiki-light:#CA1243;--shiki-dark:#C678DD\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">        setMessageInnerHTML</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"你的浏览器不支持SSE\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#CA1243;--shiki-dark:#C678DD\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;/</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">script</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>服务端的实现更简单，创建一个<code>SseEmitter</code>对象放入<code>sseEmitterMap</code>进行管理</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Map</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> SseEmitter</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">></span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> sseEmitterMap </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ConcurrentHashMap</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;></span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">/**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * 创建连接</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> SseEmitter</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> connect</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> userId) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    try</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 设置超时时间，0表示不过期。默认30秒</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        SseEmitter</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> sseEmitter </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> SseEmitter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0L</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 注册回调</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        sseEmitter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">onCompletion</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">completionCallBack</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(userId));</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        sseEmitter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">onError</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">errorCallBack</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(userId));</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        sseEmitter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">onTimeout</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">timeoutCallBack</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(userId));</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        sseEmitterMap</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">put</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(userId, sseEmitter);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        count</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getAndIncrement</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> sseEmitter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">catch</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Exception</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        log</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">info</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"创建新的sse连接异常，当前用户：{}\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, userId);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">/**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * 给指定用户发送消息</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> sendMessage</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> userId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> message) {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">sseEmitterMap</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">containsKey</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(userId)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        try</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            sseEmitterMap</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(userId).</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">send</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(message);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">        } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">catch</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">IOException</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            log</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">error</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"用户[{}]推送异常:{}\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, userId, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getMessage</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">            removeUser</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(userId)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><strong>注意：</strong> SSE 不支持 IE 浏览器，对其他主流浏览器兼容性做的还不错。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192393.png\" alt=\"SSE 兼容性\"></p>\n<h3>Websocket</h3>\n<p>Websocket 应该是大家都比较熟悉的一种实现消息推送的方式，上边我们在讲 SSE 的时候也和 Websocket 进行过比较。</p>\n<p>这是一种在 TCP 连接上进行全双工通信的协议，建立客户端和服务器之间的通信渠道。浏览器和服务器仅需一次握手，两者之间就直接可以创建持久性的连接，并进行双向数据传输。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192394.png\" alt=\"Websocket 示意图\"></p>\n<p>WebSocket 的工作过程可以分为以下几个步骤：</p>\n<ol>\n<li>客户端向服务器发送一个 HTTP 请求，请求头中包含 <code>Upgrade: websocket</code> 和 <code>Sec-WebSocket-Key</code> 等字段，表示要求升级协议为 WebSocket；</li>\n<li>服务器收到这个请求后，会进行升级协议的操作，如果支持 WebSocket，它将回复一个 HTTP 101 状态码，响应头中包含 ，<code>Connection: Upgrade</code>和 <code>Sec-WebSocket-Accept: xxx</code> 等字段、表示成功升级到 WebSocket 协议。</li>\n<li>客户端和服务器之间建立了一个 WebSocket 连接，可以进行双向的数据传输。数据以帧（frames）的形式进行传送，而不是传统的 HTTP 请求和响应。WebSocket 的每条消息可能会被切分成多个数据帧（最小单位）。发送端会将消息切割成多个帧发送给接收端，接收端接收消息帧，并将关联的帧重新组装成完整的消息。</li>\n<li>客户端或服务器可以主动发送一个关闭帧，表示要断开连接。另一方收到后，也会回复一个关闭帧，然后双方关闭 TCP 连接。</li>\n</ol>\n<p>另外，建立 WebSocket 连接之后，通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。</p>\n<p>SpringBoot 整合 WebSocket，先引入 WebSocket 相关的工具包，和 SSE 相比有额外的开发成本。</p>\n<div class=\"language-xml line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"xml\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-xml\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">&#x3C;!-- 引入websocket --></span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">dependency</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    &#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">groupId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">>org.springframework.boot&#x3C;/</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">groupId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    &#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">artifactId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">>spring-boot-starter-websocket&#x3C;/</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">artifactId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;/</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">dependency</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>服务端使用<code>@ServerEndpoint</code>注解标注当前类为一个 WebSocket 服务器，客户端可以通过<code>ws://localhost:7777/webSocket/10086</code>来连接到 WebSocket 服务器端。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Component</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Slf4j</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">ServerEndpoint</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"/websocket/{userId}\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> WebSocketServer</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    //与某个客户端的连接会话，需要通过它来给客户端发送数据</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    private</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Session</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> session</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> final</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> CopyOnWriteArraySet</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">WebSocketServer</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">></span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> webSockets </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> CopyOnWriteArraySet</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;></span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 用来存在线连接数</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> final</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Map</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Session</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">></span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> sessionPool </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> HashMap</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Session</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    /**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">     * 链接成功调用的方法</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">     */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">OnOpen</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> onOpen</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Session</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> session</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">PathParam</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">value</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"userId\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> userId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        try</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            this</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">session</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> session;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            webSockets</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">add</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">this</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            sessionPool</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">put</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(userId, session);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">            log</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">info</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"websocket消息: 有新的连接，总数为:\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> webSockets</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">size</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">());</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">catch</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Exception</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    /**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">     * 收到客户端消息后调用的方法</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">     */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">OnMessage</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> onMessage</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> message</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        log</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">info</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"websocket消息: 收到客户端消息:\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> message);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    /**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">     * 此为单点消息</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">     */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> sendOneMessage</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> userId</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> message</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">        Session</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> session</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> sessionPool</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">get</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(userId);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (session </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> &#x26;&#x26;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> session</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">isOpen</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            try</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">                log</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">info</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"websocket消: 单点消息:\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> message);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">                session</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">getAsyncRemote</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">().</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">sendText</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(message);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">            } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">catch</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Exception</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">                e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">printStackTrace</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">            }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>服务端还需要注入<code>ServerEndpointerExporter</code>，这个 Bean 就会自动注册使用了<code>@ServerEndpoint</code>注解的 WebSocket 服务器。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">@</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Configuration</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> WebSocketConfiguration</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    /**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">     * 用于注册使用了 @ServerEndpoint 注解的 WebSocket 服务器</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">     */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    @</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#E5C07B\">Bean</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ServerEndpointExporter</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> serverEndpointExporter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        return</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> ServerEndpointExporter</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>前端初始化打开 WebSocket 连接，并监听连接状态，接收服务端数据或向服务端发送数据。</p>\n<div class=\"language-javascript line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"javascript\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-javascript\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">script</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    var ws = new WebSocket('ws://localhost:7777/webSocket/10086');</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    // 获取连接状态</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    console.log('ws连接状态：' + ws.readyState);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    //监听是否连接成功</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    ws.onopen = function () </span><span style=\"--shiki-light:#CA1243;--shiki-dark:#C678DD\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">        console</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">log</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'ws连接状态：'</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\"> ws</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">readyState</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        //连接成功则发送一个数据</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">        ws</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">send</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'test1'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#CA1243;--shiki-dark:#C678DD\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    // 接听服务器发回的信息并处理展示</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    ws.onmessage = function (data) </span><span style=\"--shiki-light:#CA1243;--shiki-dark:#C678DD\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">        console</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">log</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'接收到来自服务器的消息：'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">        console</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">log</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">data</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        //完成通信后关闭WebSocket连接</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">        ws</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">close</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#CA1243;--shiki-dark:#C678DD\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    // 监听连接关闭事件</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    ws.onclose = function () </span><span style=\"--shiki-light:#CA1243;--shiki-dark:#C678DD\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 监听整个过程中websocket的状态</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">        console</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">log</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">'ws连接状态：'</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\"> ws</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">readyState</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#CA1243;--shiki-dark:#C678DD\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    // 监听并处理error事件</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    ws.onerror = function (error) </span><span style=\"--shiki-light:#CA1243;--shiki-dark:#C678DD\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">        console</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">log</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">error</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#CA1243;--shiki-dark:#C678DD\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    function sendMessage() </span><span style=\"--shiki-light:#CA1243;--shiki-dark:#C678DD\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">        var</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> content</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> $</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"#message\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">).</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">val</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">        $</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">ajax</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">({</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">            url</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#ABB2BF\">:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> '/socket/publish?userId=10086&#x26;message='</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> content</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">            type</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#ABB2BF\">:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> 'GET'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">            data</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#ABB2BF\">:</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> { </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"id\"</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#ABB2BF\">:</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"7777\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"content\"</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#ABB2BF\">:</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> content</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> },</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">            success</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#ABB2BF\">:</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> function</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\">data</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E5C07B\">                console</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">log</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">data</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">            }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        })</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#CA1243;--shiki-dark:#C678DD\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;/</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">script</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">></span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>页面初始化建立 WebSocket 连接，之后就可以进行双向通信了，效果还不错。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192395.png\" alt></p>\n<h3>MQTT</h3>\n<p><strong>什么是 MQTT 协议？</strong></p>\n<p>MQTT (Message Queue Telemetry Transport)是一种基于发布/订阅（publish/subscribe）模式的轻量级通讯协议，通过订阅相应的主题来获取消息，是物联网（Internet of Thing）中的一个标准传输协议。</p>\n<p>该协议将消息的发布者（publisher）与订阅者（subscriber）进行分离，因此可以在不可靠的网络环境中，为远程连接的设备提供可靠的消息服务，使用方式与传统的 MQ 有点类似。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000022986325.png\" alt=\"MQTT 协议示例\"></p>\n<p>TCP 协议位于传输层，MQTT 协议位于应用层，MQTT 协议构建于 TCP/IP 协议上，也就是说只要支持 TCP/IP 协议栈的地方，都可以使用 MQTT 协议。</p>\n<p><strong>为什么要用 MQTT 协议？</strong></p>\n<p>MQTT 协议为什么在物联网（IOT）中如此受偏爱？而不是其它协议，比如我们更为熟悉的 HTTP 协议呢？</p>\n<ul>\n<li>首先 HTTP 协议它是一种同步协议，客户端请求后需要等待服务器的响应。而在物联网（IOT）环境中，设备会很受制于环境的影响，比如带宽低、网络延迟高、网络通信不稳定等，显然异步消息协议更为适合 IOT 应用程序。</li>\n<li>HTTP 是单向的，如果要获取消息客户端必须发起连接，而在物联网（IOT）应用程序中，设备或传感器往往都是客户端，这意味着它们无法被动地接收来自网络的命令。</li>\n<li>通常需要将一条命令或者消息，发送到网络上的所有设备上。HTTP 要实现这样的功能不但很困难，而且成本极高。</li>\n</ul>\n<p>具体的 MQTT 协议介绍和实践，这里我就不再赘述了，大家可以参考我之前的两篇文章，里边写的也都很详细了。</p>\n<ul>\n<li>MQTT 协议的介绍：<a href=\"https://mp.weixin.qq.com/s/udFE6k9pPetIWsa6KeErrA\" target=\"_blank\" rel=\"noopener noreferrer\">我也没想到 SpringBoot + RabbitMQ 做智能家居，会这么简单</a></li>\n<li>MQTT 实现消息推送：<a href=\"https://mp.weixin.qq.com/s/U-fUGr9i1MVa4PoVyiDFCg\" target=\"_blank\" rel=\"noopener noreferrer\">未读消息（小红点），前端 与 RabbitMQ 实时消息推送实践，贼简单~</a></li>\n</ul>\n<h2>总结</h2>\n<blockquote>\n<p>以下内容为 JavaGuide 补充</p>\n</blockquote>\n<p>|           | 介绍                                                                                                          | 优点                   | 缺点                                                 |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192380.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "系统设计"
      ]
    },
    {
      "title": "开发工具知识体系：Maven、Gradle、Git、GitHub、Docker 与 IDEA",
      "url": "https://codingmap.strova.top/tools/",
      "id": "https://codingmap.strova.top/tools/",
      "summary": "后端开发工具学习路线，涵盖 Maven、Gradle、Git、GitHub、Docker、IDEA、项目构建、依赖管理、版本控制、代码协作和容器化部署。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章、开发工具 等内容展开。结合 CodingMap 知识体系梳理 开发工具 的核心概念、实践方法等核心内容。",
      "content_html": "<p>这份 <strong>开发工具知识体系</strong> 面向后端学习和日常开发，围绕“项目构建 -&gt; 依赖管理 -&gt; 版本控制 -&gt; 协作提效 -&gt; 容器化交付”的顺序整理本站开发工具相关文章。</p>\n<p>开发工具不只是会敲几个命令，更重要的是理解它们在团队协作、工程规范、环境一致性和交付效率中的作用。</p>\n<h2>适合谁看</h2>\n<ul>\n<li>正在学习后端开发，需要补齐常用工程工具的同学。</li>\n<li>准备校招、社招，想把 Maven、Git、Docker 等工具问题答得更扎实的读者。</li>\n<li>已经能写业务代码，但对依赖冲突、Git 分支协作、Docker 镜像和容器管理不够熟的开发者。</li>\n<li>想提升项目构建、代码协作、环境交付和日常开发效率的工程师。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>Maven 和 Gradle 解决的是项目构建、依赖管理、生命周期和插件扩展问题。</li>\n<li>Git 是团队协作的基础能力，重点不是背命令，而是理解工作区、暂存区、提交、分支、合并和冲突处理。</li>\n<li>GitHub 不只是代码托管平台，也承载开源协作、个人展示、代码阅读和项目管理。</li>\n<li>Docker 主要解决环境一致性、部署隔离、镜像分发和本地快速搭建依赖服务的问题。</li>\n<li>工具类知识最好结合真实项目练习，单独看概念容易会看不会用。</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/tools/git/git-intro.html\" target=\"_blank\">Git 核心概念总结</a>：先掌握版本控制、提交、分支、合并和协作流程。</li>\n<li><a href=\"/tools/maven/maven-core-concepts.html\" target=\"_blank\">Maven 核心概念总结</a>：理解 Java 项目构建、POM、坐标、仓库、依赖和生命周期。</li>\n<li><a href=\"/tools/maven/maven-best-practices.html\" target=\"_blank\">Maven 最佳实践</a>：补齐依赖版本管理、多模块项目和日常使用规范。</li>\n<li><a href=\"/tools/docker/docker-intro.html\" target=\"_blank\">Docker 核心概念总结</a>：建立镜像、容器、仓库和 Docker 引擎的基本认知。</li>\n<li><a href=\"/tools/docker/docker-in-action.html\" target=\"_blank\">Docker 实战</a>：通过命令和场景练习容器管理、镜像构建和服务部署。</li>\n<li><a href=\"/tools/gradle/gradle-core-concepts.html\" target=\"_blank\">Gradle 核心概念总结</a> 和 <a href=\"/tools/git/github-tips.html\" target=\"_blank\">GitHub 实用小技巧总结</a>：按项目需要补充 Gradle 与 GitHub 使用技巧。</li>\n</ol>\n<h2>核心文章</h2>\n<h3>项目构建与依赖管理</h3>\n<ul>\n<li><a href=\"/tools/maven/\" target=\"_blank\">Maven 专题</a>：讲清 Maven 核心概念和最佳实践，是 Java 后端项目构建最常用的工具专题。</li>\n<li><a href=\"/tools/maven/maven-core-concepts.html\" target=\"_blank\">Maven 核心概念总结</a>：理解 POM、坐标、仓库、依赖范围、生命周期、插件和多模块项目。</li>\n<li><a href=\"/tools/maven/maven-best-practices.html\" target=\"_blank\">Maven 最佳实践</a>：整理标准目录结构、编译插件、依赖管理、多模块管理和常用实践建议。</li>\n<li><a href=\"/tools/gradle/gradle-core-concepts.html\" target=\"_blank\">Gradle 核心概念总结</a>：了解 Gradle、Groovy、Gradle Wrapper、插件和 Task 等核心概念。</li>\n</ul>\n<h3>版本控制与代码协作</h3>\n<ul>\n<li><a href=\"/tools/git/\" target=\"_blank\">Git 专题</a>：围绕 Git 核心概念、工作流和 GitHub 提效技巧展开。</li>\n<li><a href=\"/tools/git/git-intro.html\" target=\"_blank\">Git 核心概念总结</a>：理解版本控制、工作区、暂存区、提交、分支、合并、冲突和远程仓库。</li>\n<li><a href=\"/tools/git/github-tips.html\" target=\"_blank\">GitHub 实用小技巧总结</a>：整理个人主页、项目徽章、代码阅读、搜索和开源协作相关技巧。</li>\n</ul>\n<h3>容器化与本地环境</h3>\n<ul>\n<li><a href=\"/tools/docker/\" target=\"_blank\">Docker 专题</a>：从核心概念到实战操作，帮助理解容器化交付和环境一致性。</li>\n<li><a href=\"/tools/docker/docker-intro.html\" target=\"_blank\">Docker 核心概念总结</a>：理解容器、镜像、仓库、Docker 引擎以及容器和虚拟机的区别。</li>\n<li><a href=\"/tools/docker/docker-in-action.html\" target=\"_blank\">Docker 实战</a>：通过镜像、容器、网络、数据卷和常见命令完成 Docker 入门实践。</li>\n</ul>\n<h3>IDE 与效率工具</h3>\n<ul>\n<li><a href=\"https://gitee.com/SnailClimb/awesome-idea-tutorial\" target=\"_blank\" rel=\"noopener noreferrer\">IDEA 教程</a>：整理 IntelliJ IDEA 常用配置、插件、快捷键和提效技巧。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>Maven 的 POM、坐标、仓库、依赖范围分别是什么？</li>\n<li>Maven 生命周期和插件是什么关系？</li>\n<li>Maven 多模块项目如何管理公共依赖版本？</li>\n<li>Gradle 和 Maven 有什么区别？什么时候需要了解 Gradle？</li>\n<li>Git 工作区、暂存区、本地仓库和远程仓库分别是什么？</li>\n<li>Git merge 和 rebase 有什么区别？冲突应该如何处理？</li>\n<li>GitHub 除了托管代码，还能帮助开发者做哪些事？</li>\n<li>Docker 镜像和容器是什么关系？容器和虚拟机有什么区别？</li>\n<li>Docker 为什么能解决开发、测试、部署环境不一致的问题？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/java/basis/java-basic-questions-01.html\" target=\"_blank\">Java 基础</a></li>\n<li><a href=\"/system-design/framework/spring/\" target=\"_blank\">Spring&amp;Spring Boot</a></li>\n<li><a href=\"/open-source-project/\" target=\"_blank\">开源项目</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "开发工具"
      ]
    },
    {
      "title": "大模型基础专题：运行机制、API 调用、结构化输出与评测",
      "url": "https://codingmap.strova.top/ai/llm-basis/",
      "id": "https://codingmap.strova.top/ai/llm-basis/",
      "summary": "大模型基础面试与学习路线，涵盖 LLM 运行机制、API 调用工程实践、结构化输出、Function Calling、Tool Calling 和 AI 应用评测。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章、大模型 等内容展开。结合 CodingMap 知识体系梳理 AI / Llm Basis 的核心概念。",
      "content_html": "<p>大模型 API 看起来只是一段请求和一段返回，实际落地时问题都藏在细节里：Token 怎么花掉、上下文为什么塞不下、采样参数会不会让答案飘、结构化输出为什么偶发失败、上线前怎么证明效果真的变好了。</p>\n<p>这份 <strong>大模型基础专题</strong> 面向 AI 应用开发入门和工程落地，先把这些基础问题讲清楚，再进入 Agent、RAG 和系统设计会顺很多。</p>\n<h2>适合谁看</h2>\n<ul>\n<li>正在学习 LLM 基础概念和大模型 API 调用的开发者。</li>\n<li>做过 Prompt Demo，但对生产级调用链路、结构化输出和评测不熟的工程师。</li>\n<li>准备大模型基础、Function Calling、Tool Calling、AI 应用评测相关面试题的同学。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>Token、上下文窗口、Temperature、Top P、停止词等参数如何影响模型输出。</li>\n<li>生产级大模型 API 调用需要处理流式响应、超时、重试、限流、fallback、日志和审计。</li>\n<li>结构化输出不能只靠 Prompt，还要结合 JSON Schema、Function Calling、Tool Calling 和服务端校验；即便这样，也要准备失败兜底。</li>\n<li>AI 应用评测要区分离线 Golden Set、LLM-as-Judge、线上灰度、Trace 回放和持续回归。</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/ai/llm-basis/llm-operation-mechanism.html\" target=\"_blank\">万字拆解 LLM 运行机制</a>：先理解 Token、上下文窗口和采样参数。</li>\n<li><a href=\"/ai/llm-basis/llm-api-engineering.html\" target=\"_blank\">大模型 API 调用工程实践</a>：再看大模型调用如何接入真实后端链路。</li>\n<li><a href=\"/ai/llm-basis/structured-output-function-calling.html\" target=\"_blank\">大模型结构化输出详解</a>：补齐结构化返回和工具调用基础。</li>\n<li><a href=\"/ai/llm-basis/llm-evaluation.html\" target=\"_blank\">AI 应用评测体系</a>：最后建立质量评估和上线回归方法。</li>\n</ol>\n<h2>核心文章</h2>\n<ul>\n<li><a href=\"/ai/llm-basis/llm-operation-mechanism.html\" target=\"_blank\">万字拆解 LLM 运行机制</a>：把 Token、上下文窗口、Temperature 等概念还原为可观察、可调试的工程参数。</li>\n<li><a href=\"/ai/llm-basis/llm-api-engineering.html\" target=\"_blank\">大模型 API 调用工程实践</a>：拆解 AI 应用调用大模型 API 的生产链路，覆盖流式输出、重试、限流、结构化返回与后端工程落地。</li>\n<li><a href=\"/ai/llm-basis/structured-output-function-calling.html\" target=\"_blank\">大模型结构化输出详解</a>：讲清 JSON Schema、Function Calling、Tool Calling 与 MCP 在一次工具调用里分别负责什么。</li>\n<li><a href=\"/ai/llm-basis/llm-evaluation.html\" target=\"_blank\">AI 应用评测体系</a>：从 Golden Set、LLM-as-Judge、RAG/Agent 指标、Trace 回放到 CI 回归，说明 AI 应用该怎么验收。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>为什么同一个 Prompt 每次输出不一样？</li>\n<li>上下文窗口变大是不是就一定更好？</li>\n<li>为什么结构化输出会偶发失败？如何做兜底和校验？</li>\n<li>Function Calling、Tool Calling、MCP 分别解决什么问题？</li>\n<li>AI 应用上线前如何证明“效果变好了”而不是只凭感觉？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/ai/\" target=\"_blank\">AI 应用开发知识体系</a></li>\n<li><a href=\"/ai/agent/\" target=\"_blank\">AI Agent 专题</a></li>\n<li><a href=\"/ai/rag/\" target=\"_blank\">RAG 专题</a></li>\n<li><a href=\"/ai/interview-questions/\" target=\"_blank\">AI 应用开发面试题专题</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI"
      ]
    },
    {
      "title": "大模型 API 调用工程实践：流式输出、重试、限流与结构化返回",
      "url": "https://codingmap.strova.top/ai/llm-basis/llm-api-engineering.html",
      "id": "https://codingmap.strova.top/ai/llm-basis/llm-api-engineering.html",
      "summary": "系统拆解 AI 应用调用大模型 API 的生产链路，覆盖业务请求、Prompt 组装、模型网关、流式输出、重试、限流、结构化返回、观测与 Java 后端落地。重点围绕 一次生产级 LLM 调用包含哪些阶段？、同步返回和流式返回有什么区别？、⭐️ SSE、WebSocket 和 HTTP chunked 这三种流式协议。",
      "content_html": "<p>很多 AI 应用的第一个版本都很“顺”：本地调通一个大模型 API，页面上能看到回答，Demo 就算跑起来了。</p>\n<p>但一上生产，麻烦马上变得具体：</p>\n<ul>\n<li>用户等了 8 秒还看不到第一个字，以为系统卡死，直接刷新页面。</li>\n<li>模型返回了一半 JSON，前端解析失败，后端日志里只有一串残缺的 <code>{&quot;answer&quot;: &quot;根因是</code>。</li>\n<li>供应商偶发 429，你的服务开始疯狂重试，越重试越被限流。</li>\n<li>用户点了取消，浏览器断开了，但后端还在消耗 Token。</li>\n<li>同一个业务请求因为重试执行了两次，落库、扣费、发通知全重复了。</li>\n</ul>\n<p>Guide 见过太多这样的事故。真正难的并非”怎么发一个 HTTP 请求给模型”，难点在于<strong>如何把大模型 API 当成一个不稳定、昂贵、受配额约束的外部依赖来治理</strong>。</p>\n<p>本文覆盖：</p>\n<ol>\n<li><strong>完整链路</strong>：一次 AI 请求从业务入口、Prompt 组装、模型网关、供应商 API 到流式响应、解析、落库、观测是怎么跑起来的。</li>\n<li><strong>流式输出</strong>：Streaming 为什么能降低 TTFT，SSE、WebSocket、HTTP chunked 分别适合什么场景，后端如何处理取消、超时、断流和重连。</li>\n<li><strong>重试与幂等</strong>：哪些错误可以重试，哪些不能，指数退避、抖动、幂等 Key、请求去重和重复响应怎么设计。</li>\n<li><strong>限流与配额</strong>：用户级、租户级、模型级、供应商级限流怎么分层，Token 预算、429 处理、排队、降级和熔断怎么落地。</li>\n<li><strong>结构化返回</strong>：JSON Mode、JSON Schema、Structured Outputs 和 Function Calling 的工程价值，以及失败兜底策略。</li>\n</ol>\n<p>上文默认你理解 Token、上下文窗口、Temperature、Top-p 等基础概念。如果还有疑问，建议先看<a href=\"/ai/llm-basis/llm-operation-mechanism.html\" target=\"_blank\">《万字拆解 LLM 运行机制》</a>和<a href=\"/ai/agent/prompt-engineering.html\" target=\"_blank\">《大模型提示词工程实践指南》</a>。</p>\n<p>说明：OpenAI、Anthropic、Gemini 等供应商能力和参数变化较快，生产系统应从控制台、响应头或配置中心动态管理，而非依赖文档里的静态数字。</p>\n<h2>一次生产级 LLM 调用包含哪些阶段？</h2>\n<p>很多人排查大模型调用问题时，只盯着供应商返回了什么。这个视角太窄。</p>\n<p>一次生产级 LLM 调用，本质上是一条跨业务系统、上下文系统、模型网关、外部供应商和前端展示层的链路。任何一段没有治理好，最后都会表现成“模型不稳定”。</p>\n<p>拆开看，一次请求通常包含 8 个阶段：</p>\n<ol>\n<li><strong>业务请求进入</strong>：校验用户身份、租户、套餐、功能权限、请求大小。</li>\n<li><strong>上下文组装</strong>：拼 System Prompt、用户输入、历史消息、RAG 证据、工具 Schema、输出格式约束。</li>\n<li><strong>Token 预算预估</strong>：估算输入 Token，预留输出 Token，决定是否裁剪历史、压缩上下文或换小模型。</li>\n<li><strong>模型网关路由</strong>：选择模型、供应商、区域、超时参数、重试策略、限流桶。</li>\n<li><strong>供应商 API 调用</strong>：同步返回或流式返回，可能经过 SSE、WebSocket 或普通 HTTP 响应体。</li>\n<li><strong>响应解析</strong>：处理 delta、finish reason、tool call、usage、拒答、结构化 JSON、异常中断。</li>\n<li><strong>状态回写</strong>：保存完整回答、增量片段、Token 用量、调用成本、失败原因和业务状态。</li>\n<li><strong>观测与告警</strong>：记录 traceId、providerRequestId、TTFT、总耗时、重试次数、429 次数、解析失败率。</li>\n</ol>\n<p>很多团队栽的最多的一件事：<strong>把模型网关当成透明代理</strong>。它不是代理，它是 AI 应用的稳定性控制面。</p>\n<p>如果没有网关，每个业务系统都会自己处理 API Key、超时、重试、限流、日志、供应商切换。短期看省事，长期一定变成事故放大器。Guide 的建议是：哪怕第一版很轻，也要把模型调用收口到一个统一的 <code>LLMGateway</code>。</p>\n<h2>同步返回和流式返回有什么区别？</h2>\n<p>默认的同步调用很好理解：后端发起请求，模型生成完全部内容后，一次性返回完整结果。</p>\n<p>流式输出则是边生成边返回。模型每产生一段文本或一个事件，供应商就通过长连接把增量推给调用方。OpenAI 官方文档把 HTTP streaming 放在 SSE 场景下描述；Anthropic Messages API 也支持通过 SSE 增量返回事件；Gemini API 同样提供标准、流式和实时相关接口。具体字段和模型能力会变，<strong>以官方文档最新展示为准</strong>。</p>\n<p><strong>为什么 Streaming 能降低 TTFT？</strong></p>\n<p>TTFT（Time To First Token）指从请求发出到收到第一个可展示 Token 的时间。</p>\n<p>同步返回时，用户要等模型生成完整答案。例如模型要生成 800 个 Token，后端必须等这 800 个 Token 都完成才把结果返回。</p>\n<p>流式返回时，用户只要等模型开始生成第一个片段，就能看到内容逐步出现。</p>\n<p>流式输出不是性能魔法。它没有让模型少算 Token，也不会天然省钱。它只是把等待过程拆成了可感知的进度，让用户觉得系统“活着”。</p>\n<p>| 对比项       | 同步返回                   | 流式返回                             |<br>\n|</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 应用开发"
      ]
    },
    {
      "title": "AI 应用评测体系：从 Golden Set 构建到线上灰度闭环",
      "url": "https://codingmap.strova.top/ai/llm-basis/llm-evaluation.html",
      "id": "https://codingmap.strova.top/ai/llm-basis/llm-evaluation.html",
      "summary": "从“没有评测集就没有信心上线”讲起，系统拆解 AI 应用评测的完整闭环：Golden Set 构建、三种评测方法、RAG/Agent/结构化输出分领域指标、LLM-as-Judge 实战、Trace 回放与 CI 自动回归落地。重点围绕 为什么公开 benchmark 不够用？、Golden Set 怎么构建？",
      "content_html": "<p>有个做智能客服的团队，花了三个月把 RAG 知识库从向量检索升级到混合检索，再加了一层 Reranker。上线前，工程师在本地测了几十条问题，感觉效果好了不少，于是就推了上线。</p>\n<p>一周后，业务方反馈：“有些问题感觉还不如以前准。”</p>\n<p>这句话最麻烦的地方，不是“效果变差了”，而是没人知道它到底有没有变差。旧版本质量是什么水平？新版本是哪类问题退步了？业务方说的“不如以前准”，是真退步，还是用户预期变高了？一查才发现，历史质量数据几乎没有。</p>\n<p>很多 AI 应用早期都是这样：靠体感上线，靠体感判断好坏，靠体感决定改完之后是不是进步了。</p>\n<p>这就像在黑盒里飞行。</p>\n<p>这篇文章讲 AI 应用评测的完整闭环，主要包括：为什么公开 benchmark 替代不了自己的评测集；Golden Set 怎么构建；人工评测、规则评测、LLM-as-Judge 分别适合什么场景；LLM-as-Judge 的偏差和可靠用法；RAG、Agent、结构化输出、成本延迟、安全分别看哪些指标；以及离线评测、Trace 回放、线上灰度和 CI 自动回归怎么串起来。</p>\n<p>说明一下：RAGAS、TruLens、LangSmith、Langfuse 等评测框架都在持续演进，生产系统要以官方文档最新说明为准。本文重点讲评测方法论和指标设计，不做工具横向测评，也不引用未经验证的 benchmark 数字。</p>\n<h2>为什么公开 benchmark 不够用？</h2>\n<p>很多团队选模型的方式很直接：打开某个评测榜单，找分数最高的，接进来用。</p>\n<p>这个方法可以做粗筛，但用它判断“模型能不能做好我的业务”，经常靠不住。</p>\n<p>公开 benchmark 优化的，不一定是你的数据分布。它通常使用固定数据集和固定任务类型，这些数据集上的排名，不一定能推断到真实用户行为。比如一个中文电商客服应用，用户问题高度集中在退换货流程、快递时效、促销规则、商品参数比较这些场景。选模型时只看英文推理榜，参考价值就很有限。</p>\n<p>还有一个更隐蔽的问题：benchmark 数据通常比较干净，但生产数据不干净。真实用户输入里会有错别字、口语缩写、图文混排、多语言夹杂、前后矛盾的描述。模型在干净测试集上的表现，和它在真实脏数据里的表现，可能差很多。</p>\n<p>业务里的失败模式也很特定。公开评测衡量的是平均能力，但业务真正敏感的往往不是平均分。</p>\n<p>比如：</p>\n<ul>\n<li>合同审查 AI：最重要的失败是漏掉高风险条款，不是平均流畅度低了 5%。</li>\n<li>智能客服：最重要的失败是把退款流程说错，不是 BLEU 分数低了 0.03。</li>\n<li>代码 Agent：最重要的失败是执行了危险命令，不是代码生成平均准确率低了几个点。</li>\n</ul>\n<p>这类高权重失败，在通用 benchmark 里基本看不出来。</p>\n<p>所以公开榜单可以用来排除明显不合适的模型，但决定一个模型能不能上你的业务，还是要靠自己的评测集。</p>\n<h2>Golden Set 怎么构建？</h2>\n<p>Golden Set 是用来衡量 AI 应用质量的标准测试集。它的重点不是“样本很多”，而是每条样本都有明确输入，以及判断输出好坏的标准。</p>\n<p>这个标准不一定是唯一正确答案。它可以是参考答案、评分维度、验证规则，也可以是一段人工判断说明。只要能让后续评测有一致标准，就有价值。</p>\n<h3>数据从哪来？</h3>\n<p><strong>第一类来源是生产日志分层采样。</strong></p>\n<p>如果系统已经上线，生产日志通常是最有价值的数据源。采样时不要只取高频问题，因为高频问题往往是比较好处理的。真正容易出问题的，常常藏在低频、边缘和异常输入里。</p>\n<p>建议重点看几类样本：用户点了“不满意”的，出现补充追问的，最后转人工的，以及那些看起来“差点失败”的边缘案例。</p>\n<p>我遇到过一次，我们只从正常对话流里采样构建 Golden Set，结果漏掉了一类占生产流量 8% 的图文混排查询。这类查询的失败率比平均值高 3 倍，但在 Golden Set 里完全没有覆盖。后面连续两个版本所谓的“质量提升”，其实都是假提升。</p>\n<p><strong>第二类来源是人工构造。</strong></p>\n<p>新功能还没上线，或者某些高风险场景很少在日志里出现，就需要人工构造样本。</p>\n<p>人工构造时至少覆盖三类：</p>\n<ul>\n<li>正常路径样本：常见、结果清晰、能代表主要功能。</li>\n<li>边缘样本：信息不完整、有歧义、跨场景混合。</li>\n<li>对抗样本：故意让模型犯错，比如领域外问题、越权请求、Prompt 注入尝试。</li>\n</ul>\n<p><strong>第三类来源是失败案例回填。</strong></p>\n<p>上线后遇到的真实失败案例，是 Golden Set 最珍贵的补充来源。每次处理用户投诉时，都应该顺手问一句：这个案例能不能加进评测集？</p>\n<p>失败案例回填能让 Golden Set 持续覆盖真实的模型软肋，而不是停留在最初构造时的主观想象里。</p>\n<p>如果系统还没上线，也可以用合成数据做冷启动。比如先从知识库文档中生成一批问题、参考答案和难例，再由人工抽样审核后加入候选集。RAGAS 这类工具提供了测试集生成能力，适合帮你快速铺出第一版覆盖面。</p>\n<p>但合成数据只能当辅助。它很容易继承生成模型自己的偏好，覆盖不到真实用户的脏输入和奇怪问法。真正用于发布门禁的 Golden Set，最终还是要被生产日志、失败案例和人工审核不断校准。</p>\n<h3>多少条够用？</h3>\n<p>这个问题没有绝对答案，但可以有工程上的起点。</p>\n<p>少于 50 条的 Golden Set，统计方差会很大。模型输出的一点随机波动，就可能让你误判质量变化方向。</p>\n<p>50 到 200 条，通常可以作为很多场景的起点。它能覆盖主要功能路径，跑一次评测的成本也还可控，结论基本有参考价值。随着业务扩展，再逐步扩大到 500 条以上。</p>\n<p>不过，比总量更重要的是分布。200 条全是同一类问题，不如 100 条覆盖 10 类场景。</p>\n<h3>分层比总量更关键</h3>\n<p>| 分层       | 典型内容               | 建议占比 |<br>\n|</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 应用开发"
      ]
    },
    {
      "title": "LLM 运行机制：Token、上下文窗口与采样参数怎么影响输出",
      "url": "https://codingmap.strova.top/ai/llm-basis/llm-operation-mechanism.html",
      "id": "https://codingmap.strova.top/ai/llm-basis/llm-operation-mechanism.html",
      "summary": "从结构化输出不稳定、长上下文失忆和采样参数失控等真实问题出发，拆解 Token、上下文窗口、Temperature、Top-p、Top-k 与 Token 预算的工程影响。重点围绕 ⭐️ Token 和上下文为什么决定成本与效果？、⭐️ 采样参数如何影响输出稳定性？、总结 等内容展开。结合 CodingMap 知识体。",
      "content_html": "<p>在探讨 RAG、Agent 工作流、MCP 协议这些高深概念之前，我想先聊聊一个让 Guide 踩过不少坑的基础问题：明明设置了温度为 0，结构化输出还是崩；往模型里塞了一堆文档，它好像直接失忆，关键指令全当空气。</p>\n<p>说到底，还是底层原理没搞清楚。</p>\n<p>万丈高楼平地起。这篇文章就是来填这个坑的。我们暂时把顶层架构放一放，回到 LLM 的基本面上来：Token 怎么算、上下文窗口怎么管、采样参数怎么调。</p>\n<p>本文会沿着一条主线展开：先看模型为什么被 Token 和上下文窗口限制，再看采样参数如何影响输出稳定性，最后落到 Token 预算和参数配置建议。</p>\n<p>具体会讲清楚：</p>\n<ol>\n<li>大模型（LLM）到底在做什么？</li>\n<li>Token 是什么？为什么中文和英文的 Token 消耗差很多？</li>\n<li>上下文窗口是什么？为什么会有上限？</li>\n<li>Temperature、Top-p、Top-k 这些采样参数怎么影响输出？</li>\n<li>Token 预算怎么做？</li>\n</ol>\n<h2>⭐️ Token 和上下文为什么决定成本与效果？</h2>\n<p>当你在输入法里打“今天天气真”，它会自动建议“好”——大模型做的事情本质上一样。只不过它看的不是前面几个字，而是前面几千甚至几十万个字。每次只“补”一个 Token（文本碎片），然后把这个碎片加进上下文，再预测下一个，如此循环，直到生成完整回答。</p>\n<p>这个过程叫做<strong>自回归生成（Autoregressive Generation）</strong>。</p>\n<p>理解了自回归生成，后面所有概念都好办了：</p>\n<ul>\n<li><strong>Token</strong>：模型每一步“补”的文本碎片。</li>\n<li><strong>上下文窗口</strong>：模型在“补”之前能看到多少文本。</li>\n<li><strong>Temperature / Top-p</strong>：模型选哪个候选碎片的策略。</li>\n<li><strong>Max Tokens</strong>：允许模型最多“补”多少步。</li>\n</ul>\n<p>你可以把 Token 理解为“模型的阅读单位”。我们人类读中文是一个字一个字地看，读英文是一个词一个词地看。但模型既不按字、也不按词——它用一套自己的“拆字规则”（叫 Tokenizer）把文本切成大小不等的碎片，每个碎片就是一个 Token。</p>\n<p>为什么不直接按字或按词切？因为模型需要在“词表大小”和“序列长度”之间取平衡：</p>\n<ul>\n<li>每个汉字都是一个 Token，词表小、但序列长（模型要“补”更多步）。</li>\n<li>每个词都是一个 Token，序列短、但词表会爆炸（中文词组太多了）。</li>\n</ul>\n<p>所以实际用的是折中方案——<strong>子词切分算法</strong>（如 BPE、Unigram），高频词保留为整体，低频词拆成更小片段。</p>\n<p>你可以把 Token 想象成乐高积木。常用的“积木块”比较大（比如“你好”可能是一个 Token），不常用的词会被拆成更小的基础块拼起来。</p>\n<p>Token 不是“一个字”或“一个词”的严格等价物：</p>\n<ul>\n<li>英文可能一个单词被拆成多个 Token。</li>\n<li>中文可能一个词被拆成多个 Token，也可能多个字合并成一个 Token（取决于词频与词表）。</li>\n</ul>\n<p>工程上通常用<strong>经验估算</strong>做容量规划，用<strong>实际 API 返回的 usage</strong>做精确计费与监控。</p>\n<p><strong>经验估算（仅用于粗略规划）</strong>：</p>\n<ul>\n<li>英文：1 Token 大约对应 3~4 个字符（与文本类型相关）。</li>\n<li>中文：1 Token 常见在 1~2 个汉字上下波动（与混排比例强相关）。</li>\n</ul>\n<p>DeepSeek 官方数据：1 个英文字符约消耗 0.3 Token，1 个中文字符约消耗 0.6 Token。换算过来，1 个 Token 约等于 3.3 个英文字符或 1.7 个中文字符，与上述经验值吻合。</p>\n<p>成本趋势提示：Token 成本与 Tokenizer 版本强相关。早期模型（如 GPT-3.5）中文压缩率较低（约 1 字 1.5~2 Token）。GPT-4o 使用 o200k_base Tokenizer（词表约 20 万），对中文压缩率有进一步提升；Qwen2.5 词表约 15 万，对中文常用词也有优化。实测数据因文本类型而异：新闻类约 1.5 字/Token，技术文档约 1.2 字/Token。</p>\n<p>“趋近 1 字 1 Token”只适用于高频词汇，别拿它当成本估算基准。做预算前查一下当前模型版本的官方 Tokenizer 演示。</p>\n<p>Token 划分直接影响模型理解能力。中文分词歧义和生僻字/低频专业术语的切分粒度，都会影响语义理解效果。</p>\n<p><strong>Token 化过程示例</strong>：</p>\n<ul>\n<li>原文：<code>你好，我是 Guide。</code></li>\n<li>切分：<code>[你好]</code> <code>[，]</code> <code>[我是]</code> <code>[Guide]</code> <code>[。]</code></li>\n<li>统计：原文 12 字符 → Token 数 5 个 → 压缩比约 2.4 倍</li>\n</ul>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/llm/llm-token-process.png\" alt=\"Token 化过程示例\"></p>\n<p>注意：实际 Token 切分由模型供应商的 Tokenizer 实现，不同供应商对相同文本可能产生不同的 Token 序列。</p>\n<p>OpenAI 官方网页端 Tokenizer 工具：<a href=\"https://platform.openai.com/tokenizer\" target=\"_blank\" rel=\"noopener noreferrer\">OpenAI Tokenizer</a></p>\n<p><strong>特殊 Token</strong>：除了文本内容对应的 Token，模型内部还会使用一些特殊标记，这些也会计入 Token 总数：</p>\n<p>| 特殊 Token                   | 用途                  | 示例           |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/llm/llm-token-process.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 应用开发"
      ]
    },
    {
      "title": "大模型结构化输出：从 JSON 契约到 Function Calling 落地",
      "url": "https://codingmap.strova.top/ai/llm-basis/structured-output-function-calling.html",
      "id": "https://codingmap.strova.top/ai/llm-basis/structured-output-function-calling.html",
      "summary": "从“请返回 JSON”在生产环境为什么不可靠讲起，拆解 Structured Outputs、JSON Schema、Function Calling、MCP 与 Java 后端工具调用的工程落地。重点围绕 ⭐️ 为什么“请返回 JSON”不可靠？、⭐️ 怎样把 JSON 从格式要求变成工程契约？",
      "content_html": "<p>很多开发者第一次接大模型到业务系统里，都会经历一个很尴尬的阶段：本地 Demo 跑得挺顺，Prompt 里写一句“请返回 JSON”，模型也乖乖吐出一个对象；一到生产环境，问题就开始冒头。</p>\n<p>有时它会在 JSON 前面加一句“好的，以下是结果”；有时少一个必填字段；有时本来应该是数字的 <code>orderId</code> 变成字符串；更麻烦的是，边界条件一复杂，模型会补出一个业务系统根本不认识的枚举值。解析器一报错，整条链路就断了。</p>\n<p>问题不在于模型“不听话”，而在于我们把<strong>自然语言承诺</strong>错当成了<strong>工程契约</strong>。</p>\n<p>结构化输出要解决的核心问题，是把“模型看起来像返回 JSON”升级成“后端可以稳定消费的结构化数据”。RAG 要靠它抽取证据，Agent 要靠它选择工具，客服系统要靠它分类工单，订单系统要靠它把自然语言请求变成可校验的参数。</p>\n<p>本文会沿着一条主线展开：先看“只靠 Prompt 要 JSON”为什么不稳，再看怎么用 Schema 把输出变成契约，最后落到 Function Calling、MCP 和 Java 后端工具执行。</p>\n<p>具体会讲清楚：</p>\n<ol>\n<li><strong>为什么“请返回 JSON”不可靠</strong>：格式漂移、字段缺失、类型错误、额外解释文本和边界条件崩溃分别怎么发生。</li>\n<li><strong>JSON Mode、JSON Schema、Structured Outputs 的区别</strong>：各自约束什么，不约束什么。</li>\n<li><strong>Function Calling / Tool Calling 的底层链路</strong>：模型只生成调用意图，真正执行工具的是业务侧。</li>\n<li><strong>Function Calling、MCP Tool、普通 HTTP API、Agent Skill 的关系</strong>：层次和边界。</li>\n<li><strong>结构化输出的工程落地</strong>：Schema 设计、服务端校验、失败重试、降级策略和工具调用安全。</li>\n</ol>\n<p>说明：OpenAI、Anthropic、Gemini、MCP 等产品和协议都在持续演进，生产系统应从官方文档最新展示获取能力描述。本文不引用未经验证的 benchmark，也不做绝对化性能结论。</p>\n<h2>⭐️ 为什么“请返回 JSON”不可靠？</h2>\n<p>先看一个非常常见的 Prompt：</p>\n<div class=\"language-text line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"text\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-text\"><span class=\"line\"><span>请判断下面用户反馈属于哪类工单，返回 JSON。</span></span>\n<span class=\"line\"><span></span></span>\n<span class=\"line\"><span>用户反馈：我付款成功了，但是订单一直显示待支付。</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>模型可能返回：</p>\n<div class=\"language-json line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"json\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-json\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"category\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"payment\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"priority\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"high\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"reason\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"用户付款成功但订单状态未更新\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>看起来没问题。但这只是“看起来”。</p>\n<p>当你把它接进后端系统，真正需要的是一份可以被程序稳定消费的契约。比如：</p>\n<ul>\n<li><code>category</code> 只能是 <code>PAYMENT</code>、<code>LOGISTICS</code>、<code>AFTER_SALE</code>、<code>ACCOUNT</code>。</li>\n<li><code>priority</code> 只能是 <code>LOW</code>、<code>MEDIUM</code>、<code>HIGH</code>。</li>\n<li><code>confidence</code> 必须是 <code>0</code> 到 <code>1</code> 之间的小数。</li>\n<li><code>reason</code> 可以为空吗？最大长度是多少？</li>\n<li>如果用户输入缺少信息，应该返回 <code>NEED_MORE_INFO</code>，还是继续猜？</li>\n</ul>\n<p>自然语言 Prompt 很难长期守住这些边界。常见翻车点主要有 5 类。</p>\n<h3>格式漂移</h3>\n<p>你要求模型返回 JSON，它大部分时候会返回 JSON，但不代表每次都只返回 JSON。</p>\n<p>常见输出长这样：</p>\n<div class=\"language-text line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"text\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-text\"><span class=\"line\"><span>以下是分类结果：</span></span>\n<span class=\"line\"><span>{</span></span>\n<span class=\"line\"><span>  \"category\": \"PAYMENT\",</span></span>\n<span class=\"line\"><span>  \"priority\": \"HIGH\"</span></span>\n<span class=\"line\"><span>}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>人看没问题，程序解析直接失败。尤其在流式输出、长上下文、多轮对话里，模型很容易把之前学到的“解释型回答习惯”带回来。</p>\n<h3>字段缺失</h3>\n<p>你要求：</p>\n<div class=\"language-json line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"json\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-json\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"category\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"PAYMENT\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"priority\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"HIGH\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"confidence\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0.92</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"reason\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"用户已支付但订单状态未同步\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>它可能返回：</p>\n<div class=\"language-json line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"json\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-json\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"category\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"PAYMENT\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"reason\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"用户已支付但订单状态未同步\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>这在模型视角里不一定是“错误”。它可能觉得 <code>priority</code> 没有把握，所以省略；也可能觉得 <code>confidence</code> 不重要。但后端 DTO 反序列化、规则引擎、数据库写入都不会因为它“没把握”就自动补齐。</p>\n<h3>类型错误</h3>\n<p>结构化输出里最隐蔽的错误是类型错位：</p>\n<div class=\"language-json line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"json\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-json\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"orderId\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"1029384756\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"needManualReview\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"false\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"confidence\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"0.87\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>JSON 语法是合法的，但业务类型不合法。<code>needManualReview</code> 是字符串，不是布尔值；<code>confidence</code> 是字符串，不是数字。很多系统会在反序列化时自动转换，看似更“宽容”，实际上会把上游错误静默吞掉，后续排查更痛苦。</p>\n<h3>额外解释文本</h3>\n<p>模型天然喜欢解释，尤其当问题涉及不确定性时。它可能在结构化结果外补一句：</p>\n<div class=\"language-text line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"text\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-text\"><span class=\"line\"><span>我认为这个问题主要和支付回调有关，但还需要进一步核实。</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>如果这是给人看的，很好；如果这是给程序解析的，就是噪声。结构化输出场景里，<strong>可读性不是第一目标，可解析性才是第一目标</strong>。</p>\n<h3>边界条件崩溃</h3>\n<p>用户输入越规整，模型越稳定；用户输入一旦模糊、矛盾或带攻击性，结构就容易崩。</p>\n<p>比如用户说：</p>\n<div class=\"language-text line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"text\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-text\"><span class=\"line\"><span>我不想提供订单号，你们自己查。另外别给我返回 JSON，直接告诉我怎么赔。</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>如果没有强约束，模型可能顺着用户走，放弃原本格式。这个问题和 Prompt 注入、上下文优先级、工具权限都有关，不能只靠一句“必须返回 JSON”解决。</p>\n<p>核心结论：Prompt 可以表达意图，但不能替代 Schema、校验器、重试机制和权限控制。结构化输出的本质，是把大模型输出纳入工程契约。</p>\n<h2>⭐️ 怎样把 JSON 从格式要求变成工程契约？</h2>\n<p>很多人把 JSON Mode、JSON Schema、Structured Outputs 混着说，面试时也容易答散。但它们其实不在同一层：</p>\n<ul>\n<li><strong>JSON Mode</strong> 是一种输出模式，约束模型返回合法 JSON。</li>\n<li><strong>JSON Schema</strong> 是一种结构描述规范，用来定义 JSON 应该包含哪些字段、字段类型是什么、哪些必填、枚举值有哪些、是否允许额外字段。</li>\n<li><strong>Structured Outputs</strong> 是模型供应商提供的结构化生成能力，它接收 JSON Schema 或类似 Schema，让模型在生成阶段尽量或严格贴合这份结构。</li>\n</ul>\n<p>也就是说，JSON Schema 不是结构化输出方式本身，而是结构化输出常用的“契约格式”。真正让模型按契约生成的，是 Structured Outputs、Function Calling / Tool Calling 等模型 API 能力。</p>\n<h3>JSON Mode 只能保证什么？</h3>\n<p>JSON Mode 的目标通常是让模型输出合法 JSON。</p>\n<p>所以 JSON Mode 能解决这类问题：</p>\n<div class=\"language-text line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"text\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-text\"><span class=\"line\"><span>好的，以下是结果：</span></span>\n<span class=\"line\"><span>{ ... }</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>但不能稳定解决这类问题：</p>\n<div class=\"language-json line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"json\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-json\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"category\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"pay\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"priority\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"urgent\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"confidence\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"very high\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>它是合法 JSON，但不是合法业务数据。</p>\n<h3>JSON Schema 负责定义什么？</h3>\n<p>JSON Schema 是一种描述 JSON 文档结构的规范。根据 JSON Schema 官方文档，<code>properties</code> 用来定义对象有哪些属性，<code>required</code> 用来声明必填字段，<code>additionalProperties</code> 可以控制是否允许未声明字段，<code>enum</code> 可以把取值限制在固定集合里。</p>\n<p>一个工单分类 Schema 可以这样写：</p>\n<div class=\"language-json line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"json\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-json\"><span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"type\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"object\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"properties\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">    \"category\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">      \"type\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"string\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">      \"enum\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: [</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">        \"PAYMENT\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">        \"LOGISTICS\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">        \"AFTER_SALE\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">        \"ACCOUNT\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">        \"NEED_MORE_INFO\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      ],</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">      \"description\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"工单分类。信息不足时选择 NEED_MORE_INFO。\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    },</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">    \"priority\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">      \"type\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"string\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">      \"enum\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: [</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"LOW\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"MEDIUM\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"HIGH\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">],</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">      \"description\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"处理优先级。涉及资金损失、无法下单、批量影响时优先级更高。\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    },</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">    \"confidence\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">      \"type\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"number\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">      \"minimum\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">      \"maximum\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">      \"description\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"分类置信度，范围为 0 到 1。\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    },</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">    \"reason\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">      \"type\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"string\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">      \"description\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"分类依据，控制在 80 个中文字符以内。\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  },</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"required\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: [</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"category\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"priority\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"confidence\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"reason\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">],</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">  \"additionalProperties\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">: </span><span style=\"--shiki-light:#0184BC;--shiki-dark:#D19A66\">false</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>这份 Schema 对后端很有价值，但它本身不会让模型“自动听话”。你需要把它传给支持结构化输出的 API，或者在服务端用校验器校验模型输出。</p>\n<h3>Structured Outputs 能前移哪些约束？</h3>\n<p>Structured Outputs 通常指供应商提供的结构化输出能力。它会把 JSON Schema 或类似 Schema 传入模型调用，让模型输出符合指定结构的数据。不同厂商对&quot;符合 Schema&quot;的保证强度不同：OpenAI strict 模式在解码阶段做约束，理论上语法层零违规；其他厂商更多依赖 prompting 加解码偏置，长文本和复杂工具组合场景下仍可能出现枚举越界或字段缺失。</p>\n<p>这里要注意一个工程细节：<strong>不同供应商支持的 JSON Schema 子集并不完全一致</strong>。比如某些关键字（<code>pattern</code>、<code>format</code>）、递归 <code>$ref</code>、组合关键字（<code>allOf</code> / <code>oneOf</code> / <code>anyOf</code>）在不同 API 中支持程度不同。真正落地时，不要照搬完整 JSON Schema 规范的所有能力，先读对应供应商的&quot;supported schemas&quot;或工具定义文档。</p>\n<h3>生成阶段的三层约束对比</h3>\n<p>| 对比维度             | JSON Mode      | JSON Schema                        | Structured Outputs                       |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/llm/structured-output-function-calling-three-layer-constraint.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 应用开发"
      ]
    },
    {
      "title": "AI 应用开发面试题专题",
      "url": "https://codingmap.strova.top/ai/interview-questions/",
      "id": "https://codingmap.strova.top/ai/interview-questions/",
      "summary": "AI 应用开发面试题与复习路线，围绕大模型基础、AI Agent、RAG、AI 系统设计梳理常见追问，适合 AI 工程师和后端转 AI 岗位。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章、AI 等内容展开。结合 CodingMap 知识体系梳理 AI / Interview Questions 的核心概念等核。",
      "content_html": "<p>AI 应用开发面试很少只问“概念是什么”。更常见的是顺着一个项目往下追：为什么这样设计，出了问题怎么排查，上线后怎么评测，成本和安全怎么管。</p>\n<p>这份 <strong>AI 应用开发面试题专题</strong> 面向 AI 工程师、AI 应用开发和后端转 AI 岗位复习，把“大模型基础、AI Agent、RAG、AI 系统设计”这些问题串成一条复习路线。</p>\n<h2>适合谁看</h2>\n<ul>\n<li>准备 AI 应用开发、AI 工程师、后端转 AI 相关岗位面试的同学。</li>\n<li>已经看过一些 AI 概念，但回答面试题时容易说散、说浅，或者只停留在 Demo 层面的读者。</li>\n<li>想把项目经历整理成“问题 -&gt; 原理 -&gt; 方案 -&gt; 取舍 -&gt; 落地”表达方式的开发者。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>大模型基础题重点讲清 Token、上下文、采样参数、结构化输出、模型调用和评测。</li>\n<li>Agent 题重点讲清 Agent Loop、Memory、Prompt、Context、MCP、Skills 和工作流。</li>\n<li>RAG 题重点讲清文档处理、向量检索、混合检索、Rerank、GraphRAG、知识库更新和评测。</li>\n<li>系统设计题重点讲清模型网关、可观测、成本、安全、灰度和实时语音 Agent。</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/ai/interview-questions/ai-interview-guide.html\" target=\"_blank\">AI 应用开发面试指南</a>：先看总入口，建立整体复习地图。</li>\n<li><a href=\"/ai/interview-questions/llm-interview-questions.html\" target=\"_blank\">大模型基础面试题总结</a>：补齐 LLM 基础概念和 API 调用链路。</li>\n<li><a href=\"/ai/interview-questions/agent-interview-questions.html\" target=\"_blank\">AI Agent 面试题总结</a>：掌握 Agent 相关高频概念和工程化问题。</li>\n<li><a href=\"/ai/interview-questions/rag-interview-questions.html\" target=\"_blank\">RAG 面试题总结</a>：围绕企业知识库问答，复习召回、排序、更新和评测问题。</li>\n<li><a href=\"/ai/interview-questions/ai-system-design-interview-questions.html\" target=\"_blank\">AI 系统设计面试题总结</a>：把前面的模块串成生产级系统设计表达。</li>\n</ol>\n<h2>核心文章</h2>\n<ul>\n<li><a href=\"/ai/interview-questions/ai-interview-guide.html\" target=\"_blank\">AI 应用开发面试指南</a>：AI 应用开发面试题总入口，按大模型基础、AI Agent、RAG、AI 系统设计组织复习路线。</li>\n<li><a href=\"/ai/interview-questions/llm-interview-questions.html\" target=\"_blank\">大模型基础面试题总结</a>：覆盖 Token、上下文窗口、采样参数、API 调用、结构化输出、Function Calling、MCP 与 AI 应用评测。</li>\n<li><a href=\"/ai/interview-questions/agent-interview-questions.html\" target=\"_blank\">AI Agent 面试题总结</a>：覆盖 Agent 核心概念、Memory、Prompt Engineering、Context Engineering、MCP、Agent Skills、Harness Engineering 与 AI 工作流。</li>\n<li><a href=\"/ai/interview-questions/rag-interview-questions.html\" target=\"_blank\">RAG 面试题总结</a>：覆盖 RAG 基础、Embedding、向量数据库、Chunk 策略、文档处理、检索优化、GraphRAG、知识库更新与 RAG 评测。</li>\n<li><a href=\"/ai/interview-questions/ai-system-design-interview-questions.html\" target=\"_blank\">AI 系统设计面试题总结</a>：覆盖生产级架构、模型网关、Prompt 管理、可观测、评测、安全治理与实时语音 Agent。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>面试官问“你做过 AI 应用吗”，如何从业务场景、架构、效果评测和上线治理回答？</li>\n<li>大模型 API 调用为什么需要重试、限流、fallback 和结构化校验？</li>\n<li>Agent 和普通工作流的区别是什么？</li>\n<li>RAG 检索不到、检索错、生成错分别怎么排查？</li>\n<li>AI 应用系统设计如何体现稳定性、可观测性、成本控制和安全治理？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/ai/\" target=\"_blank\">AI 应用开发知识体系</a></li>\n<li><a href=\"/ai/llm-basis/\" target=\"_blank\">大模型基础专题</a></li>\n<li><a href=\"/ai/agent/\" target=\"_blank\">AI Agent 专题</a></li>\n<li><a href=\"/ai/rag/\" target=\"_blank\">RAG 专题</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI"
      ]
    },
    {
      "title": "AI Agent 面试题总结",
      "url": "https://codingmap.strova.top/ai/interview-questions/agent-interview-questions.html",
      "id": "https://codingmap.strova.top/ai/interview-questions/agent-interview-questions.html",
      "summary": "系统整理 AI Agent 高频面试题，覆盖 Agent 核心概念、Agent Loop、Memory、Prompt Engineering、Context Engineering、MCP、Agent Skills、Harness Engineering、Workflow、Graph、Loop 等核心考点等核心内容。",
      "content_html": "<p>AI Agent 面试最容易出现两种极端：一种是把 Agent 讲得像“全自动数字员工”，什么都能自己规划、自己执行；另一种是把 Agent 讲得像“几个 Prompt 串起来”，完全看不出和普通工作流有什么区别。</p>\n<p>真正好的回答要落到中间：<strong>Agent 的核心不是神秘的自主意识，而是一套围绕大模型构建的任务执行系统</strong>。它要有运行循环、上下文供给、记忆机制、工具调用、安全边界、失败恢复和评测闭环。</p>\n<p>这份 AI Agent 面试题根据 AI 专栏现有文章整理，重点不是让你背“Agent 是什么”，而是帮你学会这样回答：</p>\n<ol>\n<li>Agent 为什么需要 Loop？</li>\n<li>Agent 为什么离不开 Context Engineering？</li>\n<li>Memory、Tools、MCP、Skills 分别解决什么问题？</li>\n<li>什么时候应该用 Workflow，而不是直接上纯 Agent？</li>\n<li>Agent 上生产后，怎么控制成本、风险和不确定性？</li>\n</ol>\n<p>如果能沿着这条线回答，面试官通常会觉得你不是只看过概念，而是真的思考过工程落地。</p>\n<h2>面试官真正想考什么</h2>\n<p>Agent 题本质上在考“复杂 AI 应用怎么编排”。可以按下面几个层次准备。</p>\n<p>| 考察方向            | 面试官想确认什么                                | 常见扣分点                                |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/agent/agent-core-arch.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI"
      ]
    },
    {
      "title": "2026 大模型面试题 | Agent 面试题 | RAG 面试题 | AI 应用开发面试指南（含答案与图解）",
      "url": "https://codingmap.strova.top/ai/interview-questions/ai-interview-guide.html",
      "id": "https://codingmap.strova.top/ai/interview-questions/ai-interview-guide.html",
      "summary": "2026 AI 应用开发面试指南，系统整理大模型面试题、AI Agent 面试题、RAG 面试题、AI 系统设计面试题、MCP 面试题、Prompt 工程面试题等高频考点，包含答案思路、图解和参考文章。重点围绕 面试题目录、AI 应用开发面试考什么？、怎么用这套面试题复习？、不同经验阶段怎么复习？",
      "content_html": "<p>AI 应用开发面试和传统后端面试不太一样。</p>\n<p>传统后端面试更多围绕 Java、JVM、并发、MySQL、Redis、消息队列、分布式和系统设计展开。AI 应用开发面试除了这些基础，还会继续追问：</p>\n<ul>\n<li>大模型 Token 是怎么计算的？上下文窗口越大越好吗？</li>\n<li>Function Calling 和 MCP 有什么区别？工具调用怎么做权限控制？</li>\n<li>RAG 召回率低怎么排查？Chunk 怎么切？Rerank 解决什么问题？</li>\n<li>Agent 的 Memory 怎么设计？长任务上下文溢出怎么办？</li>\n<li>如何设计一个生产级 AI 应用？模型网关、评测、可观测怎么做？</li>\n</ul>\n<p>这些题不是背几个术语就能过的。AI 应用开发面试更看重的是：<strong>你能不能把大模型、RAG、Agent、工具调用和系统设计放到真实工程里理解。</strong></p>\n<p>所以，这篇文章会作为 AI 面试题的总入口。你可以先通过这里建立知识地图，再进入具体模块刷题和回到原文补底层理解。</p>\n<h2>面试题目录</h2>\n<p>| 面试题模块                                                         | 适合重点复习的人群                                     | 主要覆盖内容                                                                                                               |<br>\n|</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI"
      ]
    },
    {
      "title": "AI 系统设计面试题总结",
      "url": "https://codingmap.strova.top/ai/interview-questions/ai-system-design-interview-questions.html",
      "id": "https://codingmap.strova.top/ai/interview-questions/ai-system-design-interview-questions.html",
      "summary": "系统整理 AI 应用系统设计高频面试题，覆盖生产级 AI 应用架构、模型网关、Prompt 管理、RAG、Memory、Tool Calling、可观测、评测、安全合规、实时语音 Agent 等核心考点，并附对应参考文章。重点围绕 面试官真正想考什么、生产级 AI 应用架构、稳定性、成本与安全治理、评测与持续迭代等核。",
      "content_html": "<p>AI 系统设计题和传统后端系统设计很像，但多了一个特别麻烦的变量：大模型。</p>\n<p>传统服务通常遵循确定性的输入输出，出了问题可以按日志、链路、数据库状态逐步定位。AI 应用不一样，模型输出有随机性，Prompt 会影响行为，RAG 证据会影响答案，工具调用可能失败，供应商可能限流，评测还不能只靠单元测试。</p>\n<p>所以，AI 系统设计面试真正考的是：<strong>你能不能把一个 Prompt Demo 设计成稳定、可观测、可评测、可回滚、可治理的生产系统。</strong></p>\n<p>这份 AI 系统设计面试题根据 AI 专栏现有文章整理，适合 2 年以上开发者复习。建议你按这条主线准备：</p>\n<ol>\n<li>先讲清 Prompt Demo 和生产系统的差距。</li>\n<li>再拆整体架构：入口、编排、上下文、RAG、Memory、Tool、模型网关、异步任务、观测评测。</li>\n<li>接着讲关键链路：一次请求如何鉴权、检索、组装上下文、调用模型、校验输出、记录 Trace。</li>\n<li>然后讲治理能力：成本、限流、降级、安全、审计、灰度、回滚。</li>\n<li>最后讲评测闭环：Golden Set、Trace 回放、线上灰度和人工复核。</li>\n</ol>\n<h2>面试官真正想考什么</h2>\n<p>AI 系统设计题一般不会满足于“我用 LangChain 搭一个 RAG”。面试官更想看你是否有生产级架构意识。</p>\n<p>| 考察方向        | 面试官想确认什么                         | 常见扣分点                   |<br>\n|</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI"
      ]
    },
    {
      "title": "大模型基础面试题总结",
      "url": "https://codingmap.strova.top/ai/interview-questions/llm-interview-questions.html",
      "id": "https://codingmap.strova.top/ai/interview-questions/llm-interview-questions.html",
      "summary": "系统整理大模型/LLM 高频面试题，覆盖 Token、上下文窗口、采样参数、API 调用、流式输出、结构化输出、Function Calling、MCP、AI 应用评测等核心考点，并附对应参考文章。重点围绕 面试官真正想考什么、LLM 运行机制、API 调用工程、结构化输出与工具调用、大模型面试 等内容展开。",
      "content_html": "<p>很多同学准备大模型面试时，第一反应是去背 Transformer、Attention、RLHF 这些词。不是说这些不重要，但对大部分后端转 AI 应用开发、AI 工程应用岗位来说，面试官更关心的是另一件事：</p>\n<p><strong>你是不是真的理解大模型调用链路里的工程约束。</strong></p>\n<p>比如 Token 为什么会影响成本和延迟？上下文窗口为什么不是越大越好？Temperature 为什么会影响结构化输出稳定性？Function Calling 为什么不能让模型直接执行真实业务操作？这些问题看起来基础，答不好就会暴露一个信号：你可能只是调过 API，还没有把大模型当作生产系统里的一个不稳定外部依赖来治理。</p>\n<p>这份大模型基础面试题主要根据 AI 专栏现有文章整理。它不是让你机械背题，而是帮你建立一条复习主线：</p>\n<ol>\n<li>先理解 <strong>Token、上下文窗口、采样参数</strong>，知道模型为什么会不稳定。</li>\n<li>再理解 <strong>API 调用工程</strong>，知道一次模型调用在生产里要经过哪些治理环节。</li>\n<li>接着理解 <strong>结构化输出与工具调用</strong>，知道怎么让模型输出能被程序消费。</li>\n<li>最后理解 <strong>AI 应用评测</strong>，知道怎么判断你的 AI 应用到底有没有变好。</li>\n</ol>\n<h2>面试官真正想考什么</h2>\n<p>大模型基础题表面上问概念，实际考的是工程判断。你可以按下面这张表来理解。</p>\n<p>| 考察方向       | 面试官想确认什么                         | 常见扣分点                               |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/llm/llm-token-process.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI"
      ]
    },
    {
      "title": "RAG 面试题总结",
      "url": "https://codingmap.strova.top/ai/interview-questions/rag-interview-questions.html",
      "id": "https://codingmap.strova.top/ai/interview-questions/rag-interview-questions.html",
      "summary": "系统整理 RAG 高频面试题，覆盖 RAG 基础、Embedding、向量数据库、Chunk 策略、文档处理、Hybrid Search、Query Rewrite、Rerank、GraphRAG、知识库更新与 RAG 评测等核心考点，并附对应参考文章。重点围绕 面试官真正想考什么、RAG 基础、向量数据库与索引等核。",
      "content_html": "<p>RAG 是 AI 应用开发里最容易被低估的模块。</p>\n<p>很多人以为 RAG 就是“文档切块 -&gt; 转向量 -&gt; 存向量库 -&gt; 检索 -&gt; 拼 Prompt”。Demo 阶段这么理解没问题，但一到真实业务，问题马上变复杂：文档解析不干净、Chunk 切碎了语义、Embedding 模型选错、召回结果不准、权限过滤漏了、知识库更新后旧版本还在、模型拿到证据却没有正确回答。</p>\n<p>所以，RAG 面试真正考的不是“你会不会接向量数据库”，而是：<strong>你能不能把一个检索增强生成系统拆成可定位、可优化、可评测、可更新的工程链路。</strong></p>\n<p>这份 RAG 面试题根据 AI 专栏现有文章整理。建议你用下面这条主线复习：</p>\n<ol>\n<li>先理解 RAG 解决什么问题，以及它和微调、长上下文、传统搜索的区别。</li>\n<li>再理解 Embedding、相似度、ANN 索引和向量数据库选型。</li>\n<li>接着理解文档处理、Chunk 策略、元数据和权限过滤。</li>\n<li>然后掌握 Hybrid Search、Query Rewrite、Rerank、上下文压缩等优化手段。</li>\n<li>最后补上 GraphRAG、知识库更新和评测闭环。</li>\n</ol>\n<h2>面试官真正想考什么</h2>\n<p>RAG 题通常会从概念开始，但很快会追到排查和优化。你可以按下面几个层次准备。</p>\n<p>| 考察方向         | 面试官想确认什么                                  | 常见扣分点                      |<br>\n|</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI"
      ]
    },
    {
      "title": "AI 系统设计专题：生产级架构、模型网关、评测治理与语音 Agent",
      "url": "https://codingmap.strova.top/ai/system-design/",
      "id": "https://codingmap.strova.top/ai/system-design/",
      "summary": "AI 系统设计面试与架构学习路线，涵盖生产级 AI 应用架构、模型网关、多模型路由、fallback、限流、成本控制、可观测和实时语音 Agent。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章、AI 系统设计 等内容展开。结合 CodingMap 知识体系梳理 AI / 系统设计 的核心概念、实践方法等核心内。",
      "content_html": "<p>Prompt Demo 能跑起来，不代表系统能长期稳定地跑。生产环境还会追问模型怎么路由、失败怎么兜底、Token 成本怎么算、回答质量怎么回归、敏感工具怎么管。</p>\n<p>这份 <strong>AI 系统设计专题</strong> 面向想把 Prompt Demo 做成生产级 AI 应用的开发者，重点关注架构分层、模型网关、RAG、Memory、Tool 调用、可观测、评测闭环、安全治理和实时语音链路。</p>\n<h2>适合谁看</h2>\n<ul>\n<li>已经做过 AI Demo，想进一步理解生产级 AI 应用架构的开发者。</li>\n<li>需要在项目中落地模型网关、多模型路由、fallback、限流和成本控制的工程师。</li>\n<li>准备 AI 系统设计、模型网关、实时语音 Agent 相关面试题的同学。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>生产级 AI 应用要证明系统能长期、稳定、可控地回答，而不是只证明模型在某次 Demo 里答对了。</li>\n<li>模型网关负责统一治理多模型路由、fallback、限流、缓存、成本归因、观测审计和安全策略。</li>\n<li>AI 应用系统设计需要把 Prompt 管理、RAG、Memory、Tool 调用、异步任务、评测和可观测串成闭环；这部分没有统一银弹，通常要按业务风险取舍。</li>\n<li>实时语音 Agent 要同时处理 VAD、ASR、LLM、TTS、流式播放、打断处理和端云混合选型。</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/ai/system-design/ai-application-architecture.html\" target=\"_blank\">AI 应用系统设计</a>：先理解从 Prompt Demo 到生产级架构的完整链路。</li>\n<li><a href=\"/ai/system-design/llm-gateway.html\" target=\"_blank\">大模型网关详解</a>：再聚焦模型调用治理和多模型路由。</li>\n<li><a href=\"/ai/system-design/ai-voice.html\" target=\"_blank\">AI 语音技术详解</a>：最后看实时语音 Agent 的端到端工程化链路。</li>\n</ol>\n<h2>核心文章</h2>\n<ul>\n<li><a href=\"/ai/system-design/ai-application-architecture.html\" target=\"_blank\">AI 应用系统设计</a>：覆盖 Prompt 管理、模型网关、RAG、Memory、Tool 调用、异步任务、可观测性、评测、安全合规等生产环节。</li>\n<li><a href=\"/ai/system-design/llm-gateway.html\" target=\"_blank\">大模型网关详解</a>：拆解 LLM Gateway 的模型路由、fallback、限流配额、Token 预算、成本归因、观测审计、缓存策略和主流方案选型。</li>\n<li><a href=\"/ai/system-design/ai-voice.html\" target=\"_blank\">AI 语音技术详解</a>：拆解语音系统完整链路，涵盖 VAD、ASR、TTS、流式播放、打断处理与端云混合选型。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>Prompt Demo 和生产级 AI 应用的本质差距是什么？</li>\n<li>为什么 AI 应用需要模型网关？</li>\n<li>多模型路由、fallback、限流和缓存分别解决什么问题？</li>\n<li>AI 应用如何做可观测、Trace 回放和评测闭环？</li>\n<li>实时语音 Agent 为什么比普通文本 Agent 更复杂？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/ai/\" target=\"_blank\">AI 应用开发知识体系</a></li>\n<li><a href=\"/ai/llm-basis/\" target=\"_blank\">大模型基础专题</a></li>\n<li><a href=\"/ai/agent/\" target=\"_blank\">AI Agent 专题</a></li>\n<li><a href=\"/ai/rag/\" target=\"_blank\">RAG 专题</a></li>\n<li><a href=\"/ai/interview-questions/\" target=\"_blank\">AI 应用开发面试题专题</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI"
      ]
    },
    {
      "title": "AI 应用系统设计：从 Prompt Demo 到生产级架构",
      "url": "https://codingmap.strova.top/ai/system-design/ai-application-architecture.html",
      "id": "https://codingmap.strova.top/ai/system-design/ai-application-architecture.html",
      "summary": "深入拆解生产级 AI 应用系统设计，覆盖 Prompt 管理、模型网关、RAG、Memory、Tool、异步任务、可观测、评测、安全合规与 Java 后端落地方案。重点围绕 Demo 架构为什么扛不住生产流量、生产级 AI 应用的标准分层架构、同步、流式、异步三种交互模式怎么选、Prompt 管理：从模板字符串到版本。",
      "content_html": "<p>大家好，我是 Guide。</p>\n<p>很多团队做 AI 应用时，第一天都很兴奋：写一个 Prompt，调一下大模型 API，页面上很快就能跑出一个“智能客服”“知识库问答”或者“报告生成助手”。</p>\n<p>然后进入第二周，问题开始冒出来：同一个问题今天答对、明天答偏；用户没有权限的资料被检索进上下文；Prompt 改了一行，线上效果突然变差却回滚不了；模型调用超时，前端一直转圈；Token 账单飙升，没人知道钱花在哪；出了事故，只能从一堆日志里猜当时模型到底看到了什么。</p>\n<p>分水岭就在这里：<strong>Prompt Demo 证明的是模型能回答，生产系统要证明的是系统能长期、稳定、可控地回答</strong>。</p>\n<p>本文接近 1.5w 字，建议收藏，通过本文你将搞懂：</p>\n<ol>\n<li><strong>Prompt Demo 和生产系统差距为什么巨大</strong>：稳定性、权限、成本、观测、评测和数据治理分别卡在哪里。</li>\n<li><strong>生产级 AI 应用应该怎么分层</strong>：入口层、业务编排、模型网关、Prompt/Context、RAG、Memory、Tool、异步任务、评测观测如何协作。</li>\n<li><strong>同步、流式、异步三种交互模式怎么选</strong>：不要把所有请求都做成“等模型返回”。</li>\n<li><strong>模型网关、工具权限、RAG 与 Memory 的关键设计</strong>：让 AI 应用从“能跑”变成“可管”。</li>\n<li><strong>Java 后端如何落地</strong>：模块拆分、核心表设计、服务接口和面试回答思路。</li>\n</ol>\n<h2>Demo 架构为什么扛不住生产流量</h2>\n<p>先看一个最常见的 Demo：</p>\n<div class=\"language-text line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"text\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-text\"><span class=\"line\"><span>前端输入问题 -> 后端拼 Prompt -> 调用模型 API -> 返回答案</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>这条链路能演示产品想法，但它缺了生产系统最关键的 6 件事。</p>\n<p>| 维度     | Prompt Demo                | 生产级架构                                                   |<br>\n|</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 应用开发"
      ]
    },
    {
      "title": "AI 语音技术详解：从 ASR、TTS 到实时语音 Agent 的工程化落地",
      "url": "https://codingmap.strova.top/ai/system-design/ai-voice.html",
      "id": "https://codingmap.strova.top/ai/system-design/ai-voice.html",
      "summary": "深入拆解 AI 语音系统底层链路，涵盖音频采集、VAD、ASR、LLM、TTS、流式播放、打断处理、低延迟优化以及云端 API、本地模型、端云混合选型。重点围绕 术语说明、AI 语音系统到底解决了什么问题？、语音识别（ASR）是怎么把声音变成文字的？、语音合成（TTS）是怎么把文字变成声音的？ 等内容展开。",
      "content_html": "<p>大家好，我是 Guide。</p>\n<p>很多开发者第一次做 AI 语音应用时，都会有一个很朴素的想法：用户说话，转成文字，丢给大模型，再把回答播出来。</p>\n<p>听起来就是三段调用：<strong>ASR -&gt; LLM -&gt; TTS</strong>。</p>\n<p>真推到生产环境，问题马上来了：用户还没说完，系统已经误判结束；用户想打断，AI 还在自顾自朗读；会议室里有空调声和键盘声，ASR 开始胡乱转写；网络稍微抖一下，下行音频就卡成一段一段；看起来模型很聪明，真正说话时却像慢半拍的电话客服。</p>\n<p>AI 语音系统最折磨人的地方就在这里：<strong>它不是把文本 Agent 接上麦克风和扬声器这么简单，而是一套实时音频工程、语音模型、对话状态和端云协同共同组成的系统</strong>。</p>\n<p>本文接近 2w 字，建议收藏，通过本文你将搞懂：</p>\n<ol>\n<li>ASR、TTS、VAD 的核心原理，以及云端 API 和本地模型该怎么选。</li>\n<li>实时语音交互的核心难点：延迟、打断、噪声、上下文和端侧能力各自卡在哪里。</li>\n<li>从 interview-guide 项目看基础版语音 Agent 是怎么一步步实现的。</li>\n<li>WebRTC 在端侧音频处理中的实际作用和配置选择。</li>\n<li>状态机设计、打断处理、成本控制等生产级落地要点。</li>\n<li>语音 Agent 的后续演进方向。</li>\n</ol>\n<h2>术语说明</h2>\n<p>为避免阅读时产生困惑，本文涉及的核心术语做如下说明：</p>\n<ul>\n<li><strong>端侧</strong> = 客户端（浏览器/App），指用户设备上的前端代码</li>\n<li><strong>Barge-in</strong> = 打断/插话打断，即用户在大模型响应过程中主动中断 AI 说话</li>\n<li><strong>增量结果</strong> = 流式输出 = partial results，指 ASR 实时返回的识别中间结果</li>\n<li><strong>级联方案</strong> = ASR + LLM + TTS 分阶段串联的架构</li>\n<li><strong>原生 Realtime API</strong> = Speech-to-Speech，端到端多模态模型，直接音频进、音频出</li>\n</ul>\n<h2>AI 语音系统到底解决了什么问题？</h2>\n<p>在说技术之前，先搞清楚我们到底在解决什么问题。</p>\n<p>语音 Agent 的本质目标是<strong>让机器能像人一样自然地对话</strong>。这听起来简单，但和文字对话相比，语音多了几个维度：</p>\n<ul>\n<li><strong>实时性</strong>：用户说话的时候，系统就得开始工作，不能等用户说完再反应。</li>\n<li><strong>多模态信息</strong>：语气、停顿、情绪，这些在文字里都丢了。</li>\n<li><strong>打断能力</strong>：人说话可以互相插嘴，机器也得支持。</li>\n<li><strong>端到端延迟</strong>：文字聊天慢 1 秒用户还能忍，语音慢 1 秒就感觉对方“没反应”。</li>\n</ul>\n<p>市面上常见的语音交互有两类：</p>\n<ol>\n<li><strong>传统语音助手</strong>：Siri、小爱同学、车载语音。你说“打开空调”，它执行固定命令。本质是个语音版的菜单系统。</li>\n<li><strong>大模型语音 Agent</strong>：能理解开放问题、调用工具、持续多轮对话。你问“帮我看看上周那个接口超时是怎么回事”，它需要理解意图、检索上下文、生成回答、还要用语音和你来回确认。</li>\n</ol>\n<p>这两者的底层逻辑完全不同。本文主要讨论后者，也就是大模型语音 Agent 的工程化落地。</p>\n<h2>语音识别（ASR）是怎么把声音变成文字的？</h2>\n<p>ASR（Automatic Speech Recognition）看起来就是“音频进、文字出”，但背后至少包含三个判断：</p>\n<ol>\n<li>这段音频说的是什么字。</li>\n<li>这些字怎么切分成词和句子。</li>\n<li>标点、数字、英文、技术名词怎么规范化。</li>\n</ol>\n<p>比如用户说“帮我查一下 Java 21 的虚拟线程”，ASR 要同时识别中文、英文、数字和技术词。如果识别成“加瓦二十一的虚拟线程”，后面的 LLM 再强也得先猜半天。</p>\n<h3>ASR 的三条技术路线</h3>\n<p>| 类型         | 代表方案                                                                                                                           | 优势                                                                  | 短板                                                             | 适合场景                       |<br>\n|</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 应用开发"
      ]
    },
    {
      "title": "大模型网关详解：多模型路由、fallback、限流与成本控制",
      "url": "https://codingmap.strova.top/ai/system-design/llm-gateway.html",
      "id": "https://codingmap.strova.top/ai/system-design/llm-gateway.html",
      "summary": "深入拆解 LLM Gateway 的概念、模型路由、fallback、限流配额、成本统计、观测审计、缓存策略、Java 后端落地方案和主流方案选型。重点围绕 大模型网关基础、为什么不能所有请求都用最强模型？、LLM Gateway 需要具备哪些能力？、如何让你设计一个 LLM Gateway，你会怎么做？ 等内容展开。",
      "content_html": "<p>面试官看了一眼我的 AI 项目架构图，突然停住了。</p>\n<p>“你这个 Agent，每次都是调用旗舰模型？”</p>\n<p>我点点头：“对啊，肯定得用地表最强的啊，效果好。”</p>\n<p>他沉默了两秒，然后开口：“那意图分类、标题生成、JSON 修复、简单摘要，也全走 Opus？”</p>\n<p>我开始有点心虚：“主要是为了稳定……”</p>\n<p>面试官没说话，等了几秒，又问：“那如果哪天旗舰模型限流了呢？意图分类这种小任务，每个月烧掉的钱你算过吗？”</p>\n<p>很多朋友第一次做 AI 应用都会踩这个坑：以为模型越强，系统越稳。实际上，生产环境里真正难的不是“选一个最强模型”，而是<strong>根据任务类型、成本、延迟、风险，把不同请求送到合适的模型上</strong>。</p>\n<p>这就是 LLM Gateway 要解决的问题。</p>\n<p>本文接近 1.2w 字，建议收藏，通过本文你将搞懂：</p>\n<ol>\n<li><strong>LLM Gateway 到底是什么</strong>：它和传统 API 网关、LLM Router、RAG、Agent、MCP 分别是什么关系。</li>\n<li><strong>为什么不能所有请求都用最强模型</strong>：如何按任务类型、成本、延迟、风险做多模型路由。</li>\n<li><strong>生产级 LLM Gateway 需要哪些能力</strong>：统一接入、fallback、限流、Token 预算、成本归因、观测审计和缓存。</li>\n<li><strong>如果让你设计一个 LLM Gateway，应该怎么拆</strong>：组件拆分、请求生命周期、路由演进路线和路由错误兜底。</li>\n<li><strong>主流方案怎么选</strong>：自研、LiteLLM、Cloudflare AI Gateway、Kong AI Gateway、Inworld Router、LLMRouter 各自适合什么团队。</li>\n</ol>\n<h2>大模型网关基础</h2>\n<h3>LLM Gateway 到底是什么？</h3>\n<p>LLM Gateway 可以简单理解成：<strong>API 网关 + 模型调用控制面</strong>。</p>\n<p>传统 API 网关是位于客户端与后端服务之间的<strong>统一入口</strong>，所有客户端请求先经过网关，再由网关路由到具体的目标服务，主要管 HTTP 流量：鉴权、限流、转发、日志、熔断。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway-overview.png\" alt=\"传统 API 网关示意图\"></p>\n<p>LLM Gateway 则面对的是大模型调用，它除了处理普通 API 问题，还要处理模型特有的问题：模型选择、Token 预算、上下文长度、供应商差异、流式输出、工具调用、结构化响应、成本统计、Prompt 版本和输出质量。</p>\n<p>更准确地说，<strong>LLM Gateway 是应用层和模型供应商之间的一层控制面。</strong></p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/llm/llm-gateway-overview.png\" alt=\"LLM 网关示意图\"></p>\n<p>业务代码不直接关心 OpenAI、Anthropic、Gemini、Qwen、DeepSeek、私有化模型分别怎么调，而是统一向 Gateway 发一个标准请求。Gateway 根据场景、预算、延迟、模型可用性和业务策略，决定调用哪个模型、走哪个供应商、是否需要重试、是否需要降级、怎么记录日志。</p>\n<p>一个最简易版的 Gateway 可能只有统一封装和日志；一个生产级 Gateway 往往还会管理模型路由、Token 预算、限流、成本归因、缓存、审计和安全策略。</p>\n<p>所以，LLM Gateway 不是“把请求转发一下”。</p>\n<p>它是 AI 应用的模型调用控制面。</p>\n<h3>为什么需要 LLM Gateway？</h3>\n<p>很多团队第一次做 AI 应用时，会直接在业务服务里写模型调用：</p>\n<div class=\"language-text line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"text\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-text\"><span class=\"line\"><span>Controller -> Service -> OpenAI SDK -> 返回答案</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>这条链路很短，开发体验也好。但只要线上规模稍微起来，问题会集中暴露。</p>\n<p>| 直连模型的典型问题 | 线上表现                                            | Gateway 对应能力                 |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway-overview.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 应用开发"
      ]
    },
    {
      "title": "RAG 专题：文档处理、向量数据库、GraphRAG、检索优化与知识库更新",
      "url": "https://codingmap.strova.top/ai/rag/",
      "id": "https://codingmap.strova.top/ai/rag/",
      "summary": "RAG 面试与检索增强生成学习路线，涵盖文档处理、向量数据库、GraphRAG、检索优化、知识库更新和 RAG 评测。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章、RAG 等内容展开。结合 CodingMap 知识体系梳理 AI / RAG 的核心概念、实践方法、常见问题和高频面试考点，覆盖原理分析、使用场景。",
      "content_html": "<p>RAG 最容易被低估的地方，是它看起来像“文档切块 + 向量检索”，但真正影响效果的环节远不止这两个。</p>\n<p>这份 <strong>RAG 专题</strong> 面向企业知识库问答、智能客服、文档助手和内部搜索等场景，按文档进入系统后的真实链路展开：解析、清洗、切分、向量化、索引、召回、重排、生成、更新和评测。</p>\n<h2>适合谁看</h2>\n<ul>\n<li>正在学习或落地 RAG 知识库问答的开发者。</li>\n<li>做过“文档切块 + 向量检索”Demo，但对召回质量、文档更新、一致性和评测不熟的工程师。</li>\n<li>准备 RAG、向量数据库、GraphRAG、企业知识库相关面试题的同学。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>RAG 的效果问题要分段排查：文档处理、Chunk、Embedding、召回、Rerank、上下文压缩和生成。</li>\n<li>向量数据库选型要结合数据规模、过滤条件、更新频率、延迟要求和运维成本。</li>\n<li>GraphRAG 更适合实体关系强、全局问题多、需要跨文档推理的场景。</li>\n<li>知识库更新不是简单覆盖文件，还要考虑版本、去重、增量索引、回滚和灰度。</li>\n<li>RAG 评测要同时看检索指标和生成指标，不能只凭最终回答是否“像那么回事”来判断。</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/ai/rag/rag-basis.html\" target=\"_blank\">万字详解 RAG 基础概念</a>：先理解 RAG 的核心流程、优势和局限。</li>\n<li><a href=\"/ai/rag/rag-document-processing.html\" target=\"_blank\">RAG 文档处理与切分策略</a>：理解文档进入索引前的处理链路。</li>\n<li><a href=\"/ai/rag/rag-vector-store.html\" target=\"_blank\">万字详解 RAG 向量索引算法和向量数据库</a>：补齐向量索引和数据库选型基础。</li>\n<li><a href=\"/ai/rag/rag-optimization.html\" target=\"_blank\">万字详解 RAG 检索优化</a>：掌握召回、重排、改写和上下文压缩。</li>\n<li><a href=\"/ai/rag/graphrag.html\" target=\"_blank\">万字详解 GraphRAG</a>、<a href=\"/ai/rag/rag-knowledge-update.html\" target=\"_blank\">RAG 知识库文档更新策略</a>：进一步理解复杂知识组织和持续更新。</li>\n</ol>\n<h2>核心文章</h2>\n<ul>\n<li><a href=\"/ai/rag/rag-basis.html\" target=\"_blank\">万字详解 RAG 基础概念</a>：理解 RAG 的工作流程、适用场景和局限性。</li>\n<li><a href=\"/ai/rag/rag-document-processing.html\" target=\"_blank\">RAG 文档处理与切分策略</a>：涵盖文件解析、清洗、结构化、Chunking 策略与多模态内容处理。</li>\n<li><a href=\"/ai/rag/rag-vector-store.html\" target=\"_blank\">万字详解 RAG 向量索引算法和向量数据库</a>：掌握 HNSW、IVFFLAT 等索引算法原理，学会选择合适的向量数据库。</li>\n<li><a href=\"/ai/rag/rag-optimization.html\" target=\"_blank\">万字详解 RAG 检索优化</a>：围绕 Chunk 策略、Hybrid Search、Query Rewrite、Rerank、上下文压缩排查召回问题。</li>\n<li><a href=\"/ai/rag/graphrag.html\" target=\"_blank\">万字详解 GraphRAG</a>：理解知识图谱驱动的 RAG，掌握实体、关系、社区发现、全局检索与局部检索。</li>\n<li><a href=\"/ai/rag/rag-knowledge-update.html\" target=\"_blank\">RAG 知识库文档更新策略</a>：涵盖增量更新、版本回滚、去重与灰度发布。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>RAG 为什么还会幻觉？应该从哪些环节排查？</li>\n<li>Chunk 切大还是切小？如何处理标题、表格、代码块和多模态内容？</li>\n<li>向量检索、关键词检索、混合检索分别适合什么场景？</li>\n<li>Rerank 的作用是什么？什么时候值得引入？</li>\n<li>GraphRAG 和普通 RAG 有什么区别？</li>\n<li>知识库更新如何保证一致性、可回滚和不停机？</li>\n<li>RAG 应用如何评测召回质量和最终回答质量？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/ai/\" target=\"_blank\">AI 应用开发知识体系</a></li>\n<li><a href=\"/ai/llm-basis/\" target=\"_blank\">大模型基础专题</a></li>\n<li><a href=\"/ai/agent/\" target=\"_blank\">AI Agent 专题</a></li>\n<li><a href=\"/ai/interview-questions/\" target=\"_blank\">AI 应用开发面试题专题</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI"
      ]
    },
    {
      "title": "万字详解 GraphRAG：为什么只靠向量检索撑不起复杂知识问答",
      "url": "https://codingmap.strova.top/ai/rag/graphrag.html",
      "id": "https://codingmap.strova.top/ai/rag/graphrag.html",
      "summary": "深入解析 GraphRAG 核心概念，讲清楚知识图谱、实体、关系、社区发现、全局检索、局部检索，以及 GraphRAG 与传统向量 RAG 的本质区别和工程落地成本。重点围绕 什么是 RAG？、什么是 GraphRAG？、传统向量 RAG 有什么局限性？、GraphRAG 和传统向量 RAG 的本质区别 等内容展开。",
      "content_html": "<p>第一次做企业知识库问答时，通常会经历一个很相似的阶段：文档切块、Embedding、向量库、Top-K 检索、把片段塞给大模型。</p>\n<p>Demo 很顺，领导问几个制度类问题也能回答。然后业务同事突然问：</p>\n<blockquote>\n<p>“这几个部门过去半年反复提到的风险点是什么？它们之间有什么关联？”</p>\n</blockquote>\n<p>向量 RAG 就开始力不从心了。</p>\n<p>它可能找到几个相似片段，却很难把“部门”“风险”“项目”“供应商”“时间线”这些对象串成一张关系网。更麻烦的是，答案往往来自多份文档的组合推理，而不是某一个 Chunk 里现成的一句话。</p>\n<p>这就是 GraphRAG 要解决的问题。</p>\n<p>下面 Guide 会把 GraphRAG 的核心概念和工程实践拆开讲清楚，重点放在它和传统向量 RAG 到底差在哪、什么时候该上、什么时候别碰。</p>\n<p>全文接近 1w 字，建议先收藏。主要覆盖：</p>\n<ol>\n<li>RAG 和 GraphRAG 的区别；</li>\n<li>知识图谱里的实体关系和社区发现；</li>\n<li>全局检索和局部检索各适合什么问题；</li>\n<li>GraphRAG 的工程落地路线和成本、以及它真正难落地的地方。</li>\n</ol>\n<h2>什么是 RAG？</h2>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/rag/rag-simplified-architecture-diagram.jpeg\" alt=\"什么是 RAG？\"></p>\n<p>RAG（Retrieval-Augmented Generation，检索增强生成）就是把信息检索和生成式大语言模型结合起来的框架。</p>\n<p>它的核心思想是：在让 LLM 回答问题或生成文本之前，先从数据库、文档集合、企业知识库等外部知识源中检索相关上下文，再把“原始问题 + 检索上下文”一起交给 LLM。这样可以让模型回答得更准确、更及时，也更符合特定领域知识。</p>\n<p>传统 RAG 的检索对象通常是 Chunk，也就是一个个文本片段。它很适合回答“答案就在某几个片段里”的问题，比如制度问答、API 文档问答、知识库局部事实查询。</p>\n<h2>什么是 GraphRAG？</h2>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/rag/graphrag-simplified-architecture-diagram.png\" alt=\"什么是 GraphRAG？\"></p>\n<p>GraphRAG（Graph-based Retrieval-Augmented Generation）可以理解为：<strong>在传统向量检索之外引入知识图谱，把文档中的实体、关系和结构化上下文显式建模。检索时除了召回相似片段，还会沿着图关系收集证据，再交给大模型生成答案。</strong></p>\n<p>注意，GraphRAG 的重点不是“用了图数据库”，而是<strong>检索对象变了</strong>。</p>\n<p>传统向量 RAG 检索的是 Chunk，也就是一个个文本片段。GraphRAG 检索的是一张“知识关系网”里的节点、边、路径、社区摘要，再结合原始文本证据回答问题。</p>\n<p>打个比方：</p>\n<ul>\n<li><strong>向量 RAG</strong> 像在图书馆里按语义找几页相似内容。</li>\n<li><strong>GraphRAG</strong> 像先整理出人物关系图、事件时间线和主题目录，再沿着关系线索找证据。</li>\n</ul>\n<p>向量 RAG 擅长判断“这段话和我的问题像不像”，GraphRAG 更擅长理解“这些对象之间到底怎么连起来”。</p>\n<h2>传统向量 RAG 有什么局限性？</h2>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/rag/graphrag-vector-rag-limitation.png\" alt=\"传统向量 RAG 的局限性\"></p>\n<p>向量 RAG 的底层逻辑很直接：</p>\n<ol>\n<li>把文档切成 Chunk。</li>\n<li>用 Embedding 模型把 Chunk 转成向量。</li>\n<li>用户提问时，把问题也转成向量。</li>\n<li>按相似度召回 Top-K Chunk。</li>\n<li>把 Chunk 塞给 LLM 生成答案。</li>\n</ol>\n<p>这套方案在“局部事实问答”里很好用。比如：</p>\n<ul>\n<li>“退款流程是什么？”</li>\n<li>“某个 API 的限流规则是多少？”</li>\n<li>“Spring AI 里怎么配置向量数据库？”</li>\n</ul>\n<p>因为答案大概率藏在某几个局部片段里，只要召回足够准，模型就能整理出结果。</p>\n<p>但复杂知识问答的问题是：<strong>答案往往不在一个片段里，而在片段之间的关系里。</strong></p>\n<h3>1. Chunk 是信息孤岛</h3>\n<p>切块是向量 RAG 的必要工程手段，但它天然会打断上下文。</p>\n<p>一份文档里，第一章定义了某个系统，第三章写了负责人，第五章提到它依赖的数据库，第七章记录了最近一次事故。切成 Chunk 之后，这些信息分散在不同文本块里。</p>\n<p>向量检索只能判断“哪个文本块和问题最像”，却不知道这些文本块在业务上属于同一个对象。</p>\n<p>这就是向量 RAG 的典型盲点：<strong>语义相似不等于关系完整。</strong></p>\n<h3>2. 向量相似度不擅长多跳推理</h3>\n<p>假设用户问：</p>\n<blockquote>\n<p>“A 系统的负责人最近参与过哪些和支付链路相关的故障复盘？”</p>\n</blockquote>\n<p>这个问题至少包含几层跳转：</p>\n<ol>\n<li>找到 A 系统。</li>\n<li>找到 A 系统负责人。</li>\n<li>找到这个负责人参与过的故障复盘。</li>\n<li>过滤出和支付链路相关的复盘。</li>\n</ol>\n<p>向量 RAG 可能召回“A 系统说明”或“支付故障复盘”，但它不天然具备沿着“系统 -&gt; 负责人 -&gt; 复盘 -&gt; 链路”这条关系链路扩展证据的能力。</p>\n<h3>3. 全局性问题很难靠 Top-K 片段回答</h3>\n<p>还有一类问题更麻烦：</p>\n<ul>\n<li>“这批客户投诉主要集中在哪几类问题？”</li>\n<li>“过去一年公司知识库里反复出现的架构风险是什么？”</li>\n<li>“这几份报告背后共同指向的战略主题是什么？”</li>\n</ul>\n<p>这类问题不是找“最相似的几段话”，而是要对整个语料做聚合、归纳和主题分析。Top-K 检索只能看到局部窗口，容易出现两种失败：</p>\n<ul>\n<li>召回片段太少，看不到整体模式。</li>\n<li>召回片段太多，Token 成本和噪声一起爆炸。</li>\n</ul>\n<p>很多人这时会把 Top-K 从 5 调到 20，再加 rerank，再加查询改写。短期能缓解，但底层问题还在：<strong>你仍然在用片段相似度解决结构推理问题。</strong></p>\n<h2>GraphRAG 和传统向量 RAG 的本质区别</h2>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/rag/graphrag-vs-rag.png\" alt=\"GraphRAG 和传统向量 RAG 的本质区别\"></p>\n<p>| 维度     | 传统向量 RAG                 | GraphRAG                               |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/rag/rag-simplified-architecture-diagram.jpeg",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 应用开发"
      ]
    },
    {
      "title": "万字详解 RAG 基础概念",
      "url": "https://codingmap.strova.top/ai/rag/rag-basis.html",
      "id": "https://codingmap.strova.top/ai/rag/rag-basis.html",
      "summary": "深入解析 RAG（检索增强生成）核心概念，涵盖 RAG 工作原理、Embedding、相似度度量、RAG vs 微调、RAG vs 长上下文、核心优势与局限性等高频面试考点。重点围绕 什么是 RAG？、为什么需要 RAG？、RAG 的常见用途有哪些？、为什么有些企业还是宁愿用传统搜索而不是 RAG？ 等内容展开。",
      "content_html": "<p>做企业知识库问答时，很多团队的第一反应都是：把文档全塞给大模型，让它自己读。</p>\n<p>文档少的时候，这招确实能跑。一旦知识库涨到几十万字，问题很快就出来了：每次请求都可能撞 Token 上限，刚更新的内容模型也不一定知道。更现实一点，企业文档还要考虑权限、溯源、成本和延迟，不能靠“全塞进去”硬扛。</p>\n<p>RAG 要做的事其实很直接：在让大模型回答之前，先从知识库里找出相关内容，再把这些内容交给模型，让它基于证据生成答案。</p>\n<p>这篇文章接近 6200 字，主要讲清楚几件事：</p>\n<ol>\n<li>RAG 是什么、为什么需要它；</li>\n<li>检索、增强、生成三个环节怎么配合；</li>\n<li>Embedding 和相似度度量到底在做什么；</li>\n<li>RAG 和传统搜索、微调、长上下文分别适合什么场景；</li>\n<li>RAG 的优势和坑分别在哪里。</li>\n</ol>\n<h2>什么是 RAG？</h2>\n<p><strong>RAG（Retrieval-Augmented Generation，检索增强生成）</strong> 就是把信息检索和大语言模型绑在一起用。系统先从知识库里检索出和当前问题相关的片段，知识库可以是数据库、文档集合，也可以是企业内部系统。然后把这些片段和原始问题一起喂给 LLM，让模型基于检索内容回答，而不是只靠训练时记住的知识。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/rag/rag-simplified-architecture-diagram.jpeg\" alt=\"RAG 示意图\"></p>\n<h2>为什么需要 RAG？</h2>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/rag/rag-llm-challenges.png\" alt=\"RAG（检索增强生成）如何解决 LLM 的核心挑战\"></p>\n<p>LLM 训练数据再大，也绕不开几个问题。RAG 正好可以在这些地方进行弥补。</p>\n<p><strong>第一是知识时效性。</strong></p>\n<p>预训练模型的知识会停在训练数据截止时间点。训练后发生的新事件、新政策、新产品文档，模型默认是不知道的，除非通过联网、工具调用或外部知识注入来补。RAG 的做法是动态检索外部知识源，把最新的相关内容直接送给 LLM，让它不用只依赖参数里的旧知识。</p>\n<p><strong>第二是私有数据访问。</strong></p>\n<p>企业内部的产品文档、知识库、客户数据，不可能让公开 LLM 随便访问。RAG 在用户提问时只提取和问题相关的片段给 LLM，不需要暴露全部数据，模型也能基于企业自己的知识回答。</p>\n<p><strong>第三是幻觉问题。</strong></p>\n<p>LLM 编造事实这件事大家都遇到过。RAG 通过提供明确参考文本，让模型尽量基于证据回答，确实能降低幻觉概率。但别指望它彻底消除幻觉。检索错误、上下文噪声、引用错配、模型不遵循指令，都可能导致错误答案。生产级 RAG 通常还要配引用校验、答案评估、拒答机制和人工反馈闭环。</p>\n<h2>RAG 的常见用途有哪些？</h2>\n<p>RAG 最适合“答案依赖外部资料，并且资料会变化或很长”的场景。它先从知识库里检索相关内容，再让大模型基于检索结果生成回答，减少胡编，同时提高可追溯性。</p>\n<p>常见场景包括这些：</p>\n<ul>\n<li>客服机器人：基于产品知识库做问答、排障、流程引导，比如“如何退换货”“某型号设备报错码怎么处理”。</li>\n<li>研发 / 运维 Copilot：检索代码库、接口文档、告警手册，辅助定位问题和生成修复建议。</li>\n<li>医疗助手：检索指南、药品说明、院内规范后生成辅助建议，但不做最终诊断，比如“某药禁忌是什么”“依据指南解释检查指标含义”。</li>\n<li>法律咨询：基于法规条文、案例、合同模板检索，生成条款解释和风险提示。</li>\n<li>教育辅导：从教材、讲义、题库中检索知识点，生成讲解和例题步骤。</li>\n<li>企业内部助手：连接制度、SOP、会议纪要、技术文档，做检索、总结、对比。</li>\n<li>投研、合规、审计、销售方案支持：处理报告、披露、内控、产品手册、标书模板等资料。</li>\n</ul>\n<h2>为什么有些企业还是宁愿用传统搜索而不是 RAG？</h2>\n<p>不是所有问题都值得上 RAG。很多企业保留传统搜索，不是因为不知道 RAG 好用，而是用户需求本来就没到“生成答案”这一步。</p>\n<p>如果用户只是想找一份制度原文、某个接口文档、一个合同模板，搜索框反而更直接。输入关键词，返回文档列表，用户自己点开确认，链路短、成本低、结果也更可控。RAG 则要先检索，再组织上下文，最后交给 LLM 生成答案。只要经过生成，就会多出延迟、Token 成本和总结偏差的风险。</p>\n<p>所以选传统搜索还是 RAG，先看用户到底想要什么：是“帮我找到材料”，还是“帮我读完材料并给出结论”。</p>\n<p>| 维度            | 传统搜索（搜索框）                         | RAG（检索 + 生成）                               |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/rag/rag-simplified-architecture-diagram.jpeg",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 应用开发"
      ]
    },
    {
      "title": "RAG 文档处理与切分策略：从解析、清洗、Chunking 到多模态内容处理",
      "url": "https://codingmap.strova.top/ai/rag/rag-document-processing.html",
      "id": "https://codingmap.strova.top/ai/rag/rag-document-processing.html",
      "summary": "深入解析 RAG 文档进入索引前的完整链路，涵盖文件解析、清洗、结构化、Chunking 策略、语义丢失处理、分层校验与多模态内容处理等工程化实践。重点围绕 文档从上传到入库要经过哪些环节？、如何选择合适的 Chunking 策略？、什么是语义丢失，为什么会发生？、如何处理结构丢失问题？ 等内容展开。",
      "content_html": "<blockquote>\n<p><strong>术语约定</strong>：本文中 &quot;Chunking&quot; 与“切分”、&quot;Embedding&quot; 与“嵌入”、&quot;Chunk&quot; 与“块” 含义相同，统一使用中文表述以保持可读性。</p>\n</blockquote>\n<p>很多团队第一次搭 RAG 系统时，都会经历一个特别有意思的阶段：买最贵的向量数据库、调最牛的 embedding 模型、上线之后发现答案还是一塌糊涂。</p>\n<p>根因往往不在检索环节，而在更上游——文档根本没有被正确解析，切分的时候把表格列拆散了，Chunk 把条件和结论切成两半，页眉页脚被当成正文入了索引。</p>\n<p>换句话说：<strong>RAG 的瓶颈通常不在检索层，而在文档进入索引之前的那段管线。</strong></p>\n<p>这个问题在 PDF 多栏布局、Word 标题层级、Excel 字段关联、扫描件 OCR 等场景下尤其突出。很多团队以为换了更强的 embedding 模型就能解决，实际上只是让错误表达得更稳定而已。</p>\n<p>这篇文章就把这条管线从头到尾拆开来看。接近 1w 字，建议收藏，主要覆盖这几块：</p>\n<ol>\n<li>文档从上传到入库的完整链路和每个环节的坑；</li>\n<li>各种 Chunking 策略的适用场景和实测数据；</li>\n<li>语义丢失为什么发生以及怎么应对；</li>\n<li>表格和多栏这类结构丢失问题；</li>\n<li>分层校验怎么做；</li>\n<li>图片表格图表怎么变成可检索内容。</li>\n</ol>\n<h2>文档从上传到入库要经过哪些环节？</h2>\n<p>在说具体策略之前，先把链路画清楚。文档从上传到进入向量库，中间要经过至少六个环节：</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/rag/rag-document-processing-overall-link.png\" alt=\"RAG 文档处理总链路：上传前半段决定了后半段效果上限\"></p>\n<p>这张图里有个容易忽略的点：质量校验不应该只发生在入库之后。在 Chunking 阶段做完采样校验，能提前发现问题，避免把低质量数据大批量写入向量库。</p>\n<blockquote>\n<p>注：本图简化展示了 Chunking 阶段的校验，完整的分层校验策略见后文“如何设计分层校验策略”章节，涵盖格式校验、解析校验和 Chunking 校验三层。</p>\n</blockquote>\n<p>每个环节的核心风险：</p>\n<p>| 环节        | 典型问题                           | 最终影响                   |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/rag/rag-document-processing-overall-link.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 应用开发"
      ]
    },
    {
      "title": "RAG 知识库文档如何更新：增量更新、版本控制、去重与全量重建",
      "url": "https://codingmap.strova.top/ai/rag/rag-knowledge-update.html",
      "id": "https://codingmap.strova.top/ai/rag/rag-knowledge-update.html",
      "summary": "深入解析 RAG 知识库更新的核心目标与工程实践，涵盖 Embedding 模型一致性、元数据设计、同步机制、增量更新与全量重建对比、生产级灰度发布与回滚方案，以及常见踩坑点。重点围绕 知识库更新要解决哪些问题？、为什么 Embedding 模型必须保持一致？、如何设计支持更新的元数据体系？",
      "content_html": "<p>第一个企业知识库 RAG 系统上线后，很多团队都会碰到一个很真实的问题：文档明明更新了，回答还是老样子。</p>\n<p>这时候先别急着怪 LLM。更常见的原因是知识库没有同步更新，或者更新链路只做了“写入新内容”，没有处理旧版本、权限、索引一致性这些细节。文档变更频繁之后，问题会更明显：每次都全量重建索引，成本和耗时扛不住；只更新变化部分，又怕漏掉旧块；只插入新向量，不清理旧版本，过期内容还会继续被召回；换了 Embedding 模型，历史数据到底要不要全部重索引，也绕不开。</p>\n<p>这些问题背后，其实是 RAG 知识库的动态性、准确性、一致性、可回滚、可观测这几件事没有处理好。</p>\n<p>这篇文章讲 RAG 知识库更新的工程实践，全文接近 8000 字。重点看几个问题：</p>\n<ol>\n<li>知识库更新到底要解决什么；</li>\n<li>为什么 Embedding 模型一致性是第一条硬规则；</li>\n<li>元数据怎么设计，才能支持增量更新和版本回滚；</li>\n<li>文档新增、修改、删除怎么同步到向量库和全文索引；</li>\n<li>增量更新和全量重建各适合什么场景；灰度发布、回滚和可观测性怎么落地；</li>\n<li>生产里最容易踩的几个坑。</li>\n</ol>\n<h2>知识库更新要解决哪些问题？</h2>\n<p>在讲具体方案之前，先把目标说清楚。</p>\n<p><strong>知识库更新要解决的不是“怎么写一个同步任务”，而是更新之后，系统回答还能保持准、快、不越权，并且出了问题能定位、能恢复。</strong></p>\n<p>动态性指的是，文档变了，索引要能跟上。这个“及时”不一定都是秒级，可能是分钟级，也可能是天级，取决于业务对实时性的要求。内部制度库也许一天同步一次就够，客服知识库和合规条款就可能需要更快。</p>\n<p>准确性指的是，更新后召回的内容要和当前文档一致，不能文档已经改了，模型还在引用旧版本。这个问题一旦发生，用户感知会很明显。</p>\n<p>一致性更麻烦。同一个文档有不同版本，向量库、元数据库、全文检索又是不同系统，任何一端漏写或延迟，都可能导致结果不一致。</p>\n<p>可回滚是为了出故障时能快速切回上一个健康状态，而不是靠人工临时修数据。可观测则要求更新过程能监控，更新结果能评估，失败原因能追到具体环节。</p>\n<p>这些目标看起来像常识，但很多项目只做了第一步“更新”，后面几步全靠运气。结果就是文档改了十版，回答还停在第一版；删了一篇敏感文档，过了几个月还能被召回出来。</p>\n<h2>为什么 Embedding 模型必须保持一致？</h2>\n<p>这一点要单独拎出来讲：索引时用的 Embedding 模型，必须和查询时用的模型一致。</p>\n<p>Embedding 模型会把文本转成向量，不同模型的向量空间并不通用。同一句话用 OpenAI 的 <code>text-embedding-3-small</code> 编码，和用 sentence-transformers 的 <code>all-MiniLM-L6-v2</code> 编码，得到的向量没有可比性。如果索引用模型 A，查询用模型 B，就等于在两个不同空间里算相似度。</p>\n<p>具体表现还要看向量维度。如果维度不同，通常无法放进同一个索引，很多向量库会直接拒绝插入或查询。如果维度相同但模型不同，相似度分数也不具备可比性，召回结果不能信。它不是简单的“随机”，而是整个排序基础已经坏了。</p>\n<p>生产里最容易忽视的有两个场景。</p>\n<p><strong>第一个是模型升级。</strong> 业务方觉得新模型效果更好，想从 <code>text-embedding-3-small</code> 切到 <code>text-embedding-3-large</code>。这意味着历史数据必须重新编码、重新入索引。工程上可以用双索引并行和灰度切流降低风险，但重建这一步绕不过去。</p>\n<p><strong>第二个是本地模型和 API 模型混用。</strong> 测试环境用本地 sentence-transformers，生产环境用 OpenAI API。这种差异在团队协作里特别常见，测试看起来正常，上线后召回率直接腰斩。</p>\n<p>比较稳的做法是把 Embedding 模型信息写进元数据，每次查询时都校验模型版本。不匹配时，要么拒绝查询，要么打警告日志并降级到更保守的召回策略。</p>\n<p>| 字段                      | 说明     | 示例                     |<br>\n|</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 应用开发"
      ]
    },
    {
      "title": "万字详解 RAG 优化：从召回、重排到上下文工程的系统调优",
      "url": "https://codingmap.strova.top/ai/rag/rag-optimization.html",
      "id": "https://codingmap.strova.top/ai/rag/rag-optimization.html",
      "summary": "深入拆解 RAG 优化的系统工程方法，覆盖 Chunk 策略、Metadata、Hybrid Search、Query Rewrite、Rerank、上下文压缩、答案评估与生产排查路径。重点围绕 RAG 优化到底在优化什么？、RAG 优化闭环、先做数据治理，再谈检索优化、Chunk 策略：别把知识切碎了 等内容展开。",
      "content_html": "<p>第一次做 RAG 时，很多人的体验都差不多：文档切了，向量库建了，Top-K 也调大了，模型还是一本正经地胡说八道。</p>\n<p>更难受的是，问题可能出在文档解析、Chunk 切分、上下文质量等多个环节，而不是单纯的 embedding 或 Top-K 参数。</p>\n<p>调一个企业知识库问答时，很容易陷入一个误区：一开始疯狂换 embedding 模型，结果线上错误率没明显下降。把失败样本拆开看才发现，60% 的问题根本不是向量相似度不够，而是 PDF 表格被解析坏了、Chunk 把条件和结论切开了、重排前的候选池里没有正确片段。</p>\n<p>RAG 优化的第一条经验是：<strong>它本质上是数据、切分、索引、召回、重排、上下文、生成、评估共同组成的系统工程，不是单点调参。</strong></p>\n<p>这篇文章就把这条链路上每个环节的优化方法拆开来讲。接近 1.5w 字，建议收藏。主要内容：</p>\n<ol>\n<li>为什么 RAG 优化不能只盯着 embedding、Top-K 和大模型参数</li>\n<li>Chunk、Metadata、Hybrid Search、Query Rewrite、Rerank、上下文压缩、答案评估各环节的作用</li>\n<li>生产环境里遇到 RAG 效果差时，应该按什么路径排查和收敛</li>\n</ol>\n<h2>RAG 优化到底在优化什么？</h2>\n<p>先把心智模型摆正。</p>\n<p>RAG 更像一条证据加工流水线：原始资料先被解析、清洗、切块、打标签、建索引；用户问题进来后，再经过查询理解、召回、重排、上下文构建，最后才交给 LLM 生成答案。</p>\n<p>这条链路里任何一环出问题，都会传染到下游。</p>\n<p>| 环节       | 典型问题                             | 最终表现                           |<br>\n|</p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 应用开发"
      ]
    },
    {
      "title": "万字详解 RAG 向量索引算法和向量数据库",
      "url": "https://codingmap.strova.top/ai/rag/rag-vector-store.html",
      "id": "https://codingmap.strova.top/ai/rag/rag-vector-store.html",
      "summary": "深入解析 RAG 场景下的向量数据库选型与使用，涵盖向量索引算法（HNSW、IVFFLAT）、ANN 近似检索原理、pgvector 实践等高频面试考点。重点围绕 Embedding 和向量检索是什么关系？、RAG 场景为什么需要向量数据库？、向量相似度和距离度量怎么选？、什么是向量索引算法？ 等内容展开。",
      "content_html": "<p>前段时间面某大厂的时候，面试官问我：“你们 RAG 系统的向量检索怎么做的？”</p>\n<p>我当时回答：“用 MySQL 存 Embedding，查询时遍历计算相似度。”</p>\n<p>面试官的表情已经说明问题了。我们当时知识库有 50 多万条 Chunk，每次查询都要全表扫描，平均响应时间 3 秒以上。对一个问答系统来说，这个延迟基本等于劝退用户。</p>\n<p>后来才意识到，这就是典型的暴力搜索。Demo 阶段能跑，生产环境根本扛不住。真正上线时，至少要考虑向量数据库和 ANN 索引。</p>\n<p>向量存储和向量索引是大多数 RAG 应用绕不开的基础设施。数据规模、延迟要求、召回要求一上来，靠遍历计算相似度很快就会出问题。</p>\n<p>这篇文章围绕几个面试高频问题展开：</p>\n<ol>\n<li>RAG 为什么需要向量数据库；</li>\n<li>Embedding 和向量检索是什么关系；</li>\n<li>余弦距离、内积、欧氏距离怎么选；</li>\n<li>向量索引算法是什么，常见算法有哪些；</li>\n<li>项目里为什么用 HNSW，HNSW 和 IVFFLAT 有什么区别；</li>\n<li>有哪些向量数据库，为什么选择 PostgreSQL + pgvector，为什么不直接用 MySQL 来做。</li>\n</ol>\n<h2>Embedding 和向量检索是什么关系？</h2>\n<p>向量数据库并不是直接理解文本。它存储和检索的是 Embedding。</p>\n<p>Embedding 的过程是：把一段文本交给 Embedding 模型，模型输出一个固定维度的稠密向量。可以粗略理解成“文本语义坐标”。两段文本语义越接近，它们在向量空间里的距离通常也越近。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/rag/rag-embedding-vector-retrieval.png\" alt=\"Embedding 和向量检索是什么关系？\"></p>\n<p>RAG 的向量检索链路可以简化成这样：</p>\n<div class=\"language-text line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"text\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-text\"><span class=\"line\"><span>文档 Chunk -> Embedding 模型 -> 文档向量 -> 写入向量数据库</span></span>\n<span class=\"line\"><span>用户问题 -> Embedding 模型 -> 查询向量 -> 检索最相似的 Top-K 文档向量</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>基础概念可以看 <a href=\"/ai/rag/rag-basis.html\" target=\"_blank\">RAG 基础篇</a>。本文重点放在后半段：这些向量怎么高效存储、索引和检索。</p>\n<h2>RAG 场景为什么需要向量数据库？</h2>\n<p>RAG（Retrieval-Augmented Generation）的核心是语义检索。系统把文档和用户问题都转成高维向量，再找出最相似的 Top-K 片段，作为 LLM 的上下文。</p>\n<p>所以 RAG 场景里真正要解决的，不只是“能不能存 Embedding”，而是能不能在大规模高维向量里，低延迟找出最相关的 Top-K。</p>\n<p>传统关系型数据库可以存向量，也可以通过函数或 SQL 表达式计算相似度。但如果没有专门的向量索引，通常只能全表扫描，很难支撑生产级低延迟检索。当 Chunk 数量达到几十万、百万甚至更高时，就需要引入向量数据库、向量搜索引擎，或者 PostgreSQL + pgvector 这类带向量索引能力的数据库扩展。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/ai/rag/rag-why-need-vector-store.png\" alt=\"RAG 场景为什么需要向量数据库？\"></p>\n<h3>高维向量相似度搜索</h3>\n<p>Embedding 通常是 768 到 3072 维的稠密向量。没有向量索引时，即使数据库能计算余弦相似度、内积或欧氏距离，也很难在大规模数据上快速完成 Top-K 检索。</p>\n<p>暴力搜索就是遍历全表计算距离，复杂度是 O(n)。以 100 万条 1024 维向量为例，单次查询大约要做：</p>\n<div class=\"language-text line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"text\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-text\"><span class=\"line\"><span>1,000,000 × 1,024 次乘法运算</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><p>实际延迟很容易到秒级，具体取决于硬件和实现。对实时问答系统来说，秒级延迟基本不可接受。</p>\n<p>ANN（Approximate Nearest Neighbor，近似最近邻）检索就是为了解这个问题。向量数据库通过图导航、空间划分、量化等方式减少距离计算次数，不再每次都把所有向量算一遍。</p>\n<p>ANN 的价值不在于永远返回 100% 精确的最近邻，而是在召回率、延迟和资源消耗之间做工程取舍。在合适的索引参数和硬件条件下，ANN 通常能把百万级向量检索从秒级暴力扫描优化到几十毫秒甚至更低。不过具体效果必须拿业务数据、Top-K、过滤条件、并发和召回率目标来测，不能只看理论复杂度。</p>\n<p>| 指标     | 暴力搜索       | ANN 索引检索                     |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/ai/rag/rag-embedding-vector-retrieval.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "AI 应用开发"
      ]
    },
    {
      "title": "十大经典排序算法总结",
      "url": "https://codingmap.strova.top/cs-basics/algorithms/10-classical-sorting-algorithms.html",
      "id": "https://codingmap.strova.top/cs-basics/algorithms/10-classical-sorting-algorithms.html",
      "summary": "系统梳理十大经典排序算法，附复杂度与稳定性对比，覆盖比较类与非比较类排序的核心原理与实现场景，帮助快速选型与优化。重点围绕 引言、简介、冒泡排序 (Bubble Sort)、选择排序 (Selection Sort)、算法 等内容展开。结合 CodingMap 知识体系梳理 计算机基础 / 算法 / 10 Class。",
      "content_html": "<blockquote>\n<p>本文转自：<a href=\"http://www.guoyaohua.com/sorting.html\" target=\"_blank\" rel=\"noopener noreferrer\">http://www.guoyaohua.com/sorting.html</a>，JavaGuide 对其做了补充完善。</p>\n</blockquote>\n<!-- markdownlint-disable MD024 -->\n<h2>引言</h2>\n<p>所谓排序，就是使一串记录，按照其中的某个或某些关键字的大小，递增或递减的排列起来的操作。排序算法，就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视，尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范，要得到一个符合实际的优秀算法，得经过大量的推理和分析。</p>\n<h2>简介</h2>\n<h3>排序算法总结</h3>\n<p>常见的内部排序算法有：<strong>插入排序</strong>、<strong>希尔排序</strong>、<strong>选择排序</strong>、<strong>冒泡排序</strong>、<strong>归并排序</strong>、<strong>快速排序</strong>、<strong>堆排序</strong>、<strong>基数排序</strong>等，本文只讲解内部排序算法。用一张表格概括：</p>\n<p>| 排序算法 | 时间复杂度（平均） | 时间复杂度（最差） | 时间复杂度（最好） | 空间复杂度 | 排序方式 | 稳定性 |<br>\n|</p>\n",
      "image": "https://oss.javaguide.cn/github/javaguide/cs-basics/sorting-algorithms/sort2.png",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "计算机基础"
      ]
    },
    {
      "title": "算法专题：经典算法思想、LeetCode 高频题、字符串、链表、剑指 Offer 与排序",
      "url": "https://codingmap.strova.top/cs-basics/algorithms/",
      "id": "https://codingmap.strova.top/cs-basics/algorithms/",
      "summary": "算法面试与刷题路线，涵盖经典算法思想、LeetCode 高频题、字符串算法、链表算法、剑指 Offer 和十大经典排序算法。重点围绕 适合谁看、学习重点、建议阅读顺序、核心文章、算法 等内容展开。结合 CodingMap 知识体系梳理 计算机基础 / 算法 的核心概念、实践方法、常见问题和高频面试考点，覆盖原理分析等。",
      "content_html": "<p>这份 <strong>算法专题</strong> 面向算法面试和基础能力训练，整理经典算法思想、常见数据结构 LeetCode 题目、字符串、链表、剑指 Offer 和排序算法。</p>\n<h2>适合谁看</h2>\n<ul>\n<li>正在准备校招、社招算法题的同学。</li>\n<li>想按题型系统刷 LeetCode，而不是随机刷题的读者。</li>\n<li>数据结构已经有基础，但缺少算法解题模板的后端开发者。</li>\n<li>需要在面试前快速复盘字符串、链表、排序和常见算法思想的工程师。</li>\n</ul>\n<h2>学习重点</h2>\n<ul>\n<li>算法题要先按题型建立模板，再通过变体题训练迁移能力。</li>\n<li>常见思想包括二分、双指针、滑动窗口、递归、回溯、贪心、动态规划、图搜索等。</li>\n<li>字符串和链表题高频且容易考察边界处理。</li>\n<li>排序算法不只要背复杂度，还要理解稳定性、原地排序和适用场景。</li>\n<li>刷题复盘比刷题数量更重要，要记录错误原因、模板和边界条件。</li>\n</ul>\n<h2>建议阅读顺序</h2>\n<ol>\n<li><a href=\"/cs-basics/algorithms/classical-algorithm-problems-recommendations.html\" target=\"_blank\">经典算法思想总结</a>：先按思想建立算法题分类框架。</li>\n<li><a href=\"/cs-basics/algorithms/common-data-structures-leetcode-recommendations.html\" target=\"_blank\">常见数据结构经典 LeetCode 题目推荐</a>：结合数组、链表、栈、队列、树、图等结构刷题。</li>\n<li><a href=\"/cs-basics/algorithms/string-algorithm-problems.html\" target=\"_blank\">几道常见的字符串算法题</a>：集中练习字符串匹配、回文、子串等高频题。</li>\n<li><a href=\"/cs-basics/algorithms/linkedlist-algorithm-problems.html\" target=\"_blank\">几道常见的链表算法题</a>：掌握反转链表、快慢指针、环检测、合并链表等题型。</li>\n<li><a href=\"/cs-basics/algorithms/the-sword-refers-to-offer.html\" target=\"_blank\">剑指 Offer 部分编程题</a> 和 <a href=\"/cs-basics/algorithms/10-classical-sorting-algorithms.html\" target=\"_blank\">十大经典排序算法总结</a>：用于面试前复盘。</li>\n</ol>\n<h2>核心文章</h2>\n<ul>\n<li><a href=\"/cs-basics/algorithms/classical-algorithm-problems-recommendations.html\" target=\"_blank\">经典算法思想总结</a>：整理常见算法思想和对应 LeetCode 题目。</li>\n<li><a href=\"/cs-basics/algorithms/common-data-structures-leetcode-recommendations.html\" target=\"_blank\">常见数据结构经典 LeetCode 题目推荐</a>：按数据结构类型推荐经典题目。</li>\n<li><a href=\"/cs-basics/algorithms/string-algorithm-problems.html\" target=\"_blank\">几道常见的字符串算法题</a>：覆盖字符串处理、匹配、回文和子串相关问题。</li>\n<li><a href=\"/cs-basics/algorithms/linkedlist-algorithm-problems.html\" target=\"_blank\">几道常见的链表算法题</a>：覆盖链表反转、合并、环检测、删除节点等高频题。</li>\n<li><a href=\"/cs-basics/algorithms/the-sword-refers-to-offer.html\" target=\"_blank\">剑指 Offer 部分编程题</a>：整理经典面试编程题。</li>\n<li><a href=\"/cs-basics/algorithms/10-classical-sorting-algorithms.html\" target=\"_blank\">十大经典排序算法总结</a>：讲解冒泡、选择、插入、归并、快速、堆排序等经典排序算法。</li>\n</ul>\n<h2>高频问题</h2>\n<ul>\n<li>刷算法题应该按题型刷，还是按难度随机刷？</li>\n<li>二分查找最容易错在哪里？如何处理边界？</li>\n<li>双指针和滑动窗口分别适合什么问题？</li>\n<li>回溯、DFS、动态规划有什么区别？</li>\n<li>链表题为什么容易出错？如何处理虚拟头节点和边界？</li>\n<li>字符串题常见解法有哪些？</li>\n<li>常见排序算法的时间复杂度、空间复杂度和稳定性分别是什么？</li>\n<li>面试前如何用有限时间复盘算法题？</li>\n</ul>\n<h2>相关专题</h2>\n<ul>\n<li><a href=\"/cs-basics/\" target=\"_blank\">计算机基础知识体系</a></li>\n<li><a href=\"/cs-basics/data-structure/\" target=\"_blank\">数据结构专题</a></li>\n<li><a href=\"/java/collection/java-collection-questions-01.html\" target=\"_blank\">Java 集合</a></li>\n<li><a href=\"/books/cs-basics.html\" target=\"_blank\">计算机基础书籍推荐</a></li>\n</ul>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "计算机基础"
      ]
    },
    {
      "title": "经典算法思想总结（含LeetCode题目推荐）",
      "url": "https://codingmap.strova.top/cs-basics/algorithms/classical-algorithm-problems-recommendations.html",
      "id": "https://codingmap.strova.top/cs-basics/algorithms/classical-algorithm-problems-recommendations.html",
      "summary": "总结常见算法思想与解题模板，配合典型题目推荐，强调思维路径与复杂度权衡，快速构建解题体系。重点围绕 贪心算法、动态规划、回溯算法、分治算法、算法 等内容展开。结合 CodingMap 知识体系梳理 计算机基础 / 算法 / Classical Algorithm Problems Recommendations.Ht。",
      "content_html": "<h2>贪心算法</h2>\n<h3>算法思想</h3>\n<p>贪心的本质是选择每一阶段的局部最优，从而达到全局最优。</p>\n<h3>一般解题步骤</h3>\n<ul>\n<li>将问题分解为若干个子问题</li>\n<li>找出适合的贪心策略</li>\n<li>求解每一个子问题的最优解</li>\n<li>将局部最优解堆叠成全局最优解</li>\n</ul>\n<h3>LeetCode</h3>\n<p>455.分发饼干：<a href=\"https://leetcode.cn/problems/assign-cookies/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/assign-cookies/</a></p>\n<p>121.买卖股票的最佳时机：<a href=\"https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/</a></p>\n<p>122.买卖股票的最佳时机 II：<a href=\"https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/</a></p>\n<p>55.跳跃游戏：<a href=\"https://leetcode.cn/problems/jump-game/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/jump-game/</a></p>\n<p>45.跳跃游戏 II：<a href=\"https://leetcode.cn/problems/jump-game-ii/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/jump-game-ii/</a></p>\n<h2>动态规划</h2>\n<h3>算法思想</h3>\n<p>动态规划中每一个状态一定是由上一个状态推导出来的，这一点就区分于贪心，贪心没有状态推导，而是从局部直接选最优的。</p>\n<p>经典题目：01 背包、完全背包</p>\n<h3>一般解题步骤</h3>\n<ul>\n<li>确定 dp 数组（dp table）以及下标的含义</li>\n<li>确定递推公式</li>\n<li>dp 数组如何初始化</li>\n<li>确定遍历顺序</li>\n<li>举例推导 dp 数组</li>\n</ul>\n<h3>LeetCode</h3>\n<p>509.斐波那契数：<a href=\"https://leetcode.cn/problems/fibonacci-number/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/fibonacci-number/</a></p>\n<p>746.使用最小花费爬楼梯：<a href=\"https://leetcode.cn/problems/min-cost-climbing-stairs/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/min-cost-climbing-stairs/</a></p>\n<p>416.分割等和子集：<a href=\"https://leetcode.cn/problems/partition-equal-subset-sum/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/partition-equal-subset-sum/</a></p>\n<p>518.零钱兑换：<a href=\"https://leetcode.cn/problems/coin-change-ii/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/coin-change-ii/</a></p>\n<p>647.回文子串：<a href=\"https://leetcode.cn/problems/palindromic-substrings/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/palindromic-substrings/</a></p>\n<p>516.最长回文子序列：<a href=\"https://leetcode.cn/problems/longest-palindromic-subsequence/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/longest-palindromic-subsequence/</a></p>\n<h2>回溯算法</h2>\n<h3>算法思想</h3>\n<p>回溯算法实际上一个类似枚举的搜索尝试过程，主要是在搜索尝试过程中寻找问题的解，当发现已不满足求解条件时，就 “回溯” 返回，尝试别的路径。其本质就是穷举。</p>\n<p>经典题目：8 皇后</p>\n<h3>一般解题步骤</h3>\n<ul>\n<li>针对所给问题，定义问题的解空间，它至少包含问题的一个（最优）解。</li>\n<li>确定易于搜索的解空间结构，使得能用回溯法方便地搜索整个解空间。</li>\n<li>以深度优先的方式搜索解空间，并且在搜索过程中用剪枝函数避免无效搜索。</li>\n</ul>\n<h3>LeetCode</h3>\n<p>77.组合：<a href=\"https://leetcode.cn/problems/combinations/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/combinations/</a></p>\n<p>39.组合总和：<a href=\"https://leetcode.cn/problems/combination-sum/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/combination-sum/</a></p>\n<p>40.组合总和 II：<a href=\"https://leetcode.cn/problems/combination-sum-ii/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/combination-sum-ii/</a></p>\n<p>78.子集：<a href=\"https://leetcode.cn/problems/subsets/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/subsets/</a></p>\n<p>90.子集 II：<a href=\"https://leetcode.cn/problems/subsets-ii/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/subsets-ii/</a></p>\n<p>51.N 皇后：<a href=\"https://leetcode.cn/problems/n-queens/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/n-queens/</a></p>\n<h2>分治算法</h2>\n<h3>算法思想</h3>\n<p>将一个规模为 N 的问题分解为 K 个规模较小的子问题，这些子问题相互独立且与原问题性质相同。求出子问题的解，就可得到原问题的解。</p>\n<p>经典题目：二分查找、汉诺塔问题</p>\n<h3>一般解题步骤</h3>\n<ul>\n<li>将原问题分解为若干个规模较小，相互独立，与原问题形式相同的子问题；</li>\n<li>若子问题规模较小而容易被解决则直接解，否则递归地解各个子问题；</li>\n<li>将各个子问题的解合并为原问题的解。</li>\n</ul>\n<h3>LeetCode</h3>\n<p>108.将有序数组转换成二叉搜索数：<a href=\"https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/</a></p>\n<p>148.排序列表：<a href=\"https://leetcode.cn/problems/sort-list/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/sort-list/</a></p>\n<p>23.合并 k 个升序链表：<a href=\"https://leetcode.cn/problems/merge-k-sorted-lists/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/merge-k-sorted-lists/</a></p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "计算机基础"
      ]
    },
    {
      "title": "常见数据结构经典LeetCode题目推荐",
      "url": "https://codingmap.strova.top/cs-basics/algorithms/common-data-structures-leetcode-recommendations.html",
      "id": "https://codingmap.strova.top/cs-basics/algorithms/common-data-structures-leetcode-recommendations.html",
      "summary": "按数据结构类别整理经典 LeetCode 题目清单，聚焦高频与核心考点，助力系统化刷题与巩固。重点围绕 数组、链表、栈与队列、二叉树、算法 等内容展开。结合 CodingMap 知识体系梳理 计算机基础 / 算法 / Common Data Structures Leetcode Recommendations.Ht。",
      "content_html": "<h2>数组</h2>\n<p>704.二分查找：<a href=\"https://leetcode.cn/problems/binary-search/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/binary-search/</a></p>\n<p>80.删除有序数组中的重复项 II：<a href=\"https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii</a></p>\n<p>977.有序数组的平方：<a href=\"https://leetcode.cn/problems/squares-of-a-sorted-array/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/squares-of-a-sorted-array/</a></p>\n<h2>链表</h2>\n<p>707.设计链表：<a href=\"https://leetcode.cn/problems/design-linked-list/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/design-linked-list/</a></p>\n<p>206.反转链表：<a href=\"https://leetcode.cn/problems/reverse-linked-list/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/reverse-linked-list/</a></p>\n<p>92.反转链表 II：<a href=\"https://leetcode.cn/problems/reverse-linked-list-ii/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/reverse-linked-list-ii/</a></p>\n<p>61.旋转链表：<a href=\"https://leetcode.cn/problems/rotate-list/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/rotate-list/</a></p>\n<h2>栈与队列</h2>\n<p>232.用栈实现队列：<a href=\"https://leetcode.cn/problems/implement-queue-using-stacks/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/implement-queue-using-stacks/</a></p>\n<p>225.用队列实现栈：<a href=\"https://leetcode.cn/problems/implement-stack-using-queues/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/implement-stack-using-queues/</a></p>\n<p>347.前 K 个高频元素：<a href=\"https://leetcode.cn/problems/top-k-frequent-elements/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/top-k-frequent-elements/</a></p>\n<p>239.滑动窗口最大值：<a href=\"https://leetcode.cn/problems/sliding-window-maximum/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/sliding-window-maximum/</a></p>\n<h2>二叉树</h2>\n<p>105.从前序与中序遍历构造二叉树：<a href=\"https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/</a></p>\n<p>117.填充每个节点的下一个右侧节点指针 II：<a href=\"https://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii</a></p>\n<p>236.二叉树的最近公共祖先：<a href=\"https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/</a></p>\n<p>129.求根节点到叶节点数字之和：<a href=\"https://leetcode.cn/problems/sum-root-to-leaf-numbers/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/sum-root-to-leaf-numbers/</a></p>\n<p>102.二叉树的层序遍历：<a href=\"https://leetcode.cn/problems/binary-tree-level-order-traversal/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/binary-tree-level-order-traversal/</a></p>\n<p>530.二叉搜索树的最小绝对差：<a href=\"https://leetcode.cn/problems/minimum-absolute-difference-in-bst/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/minimum-absolute-difference-in-bst/</a></p>\n<h2>图</h2>\n<p>200.岛屿数量：<a href=\"https://leetcode.cn/problems/number-of-islands/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/number-of-islands/</a></p>\n<p>207.课程表：<a href=\"https://leetcode.cn/problems/course-schedule/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/course-schedule/</a></p>\n<p>210.课程表 II：<a href=\"https://leetcode.cn/problems/course-schedule-ii/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/course-schedule-ii/</a></p>\n<h2>堆</h2>\n<p>215.数组中的第 K 个最大元素：<a href=\"https://leetcode.cn/problems/kth-largest-element-in-an-array/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/kth-largest-element-in-an-array/</a></p>\n<p>216.数据流的中位数：<a href=\"https://leetcode.cn/problems/find-median-from-data-stream/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/find-median-from-data-stream/</a></p>\n<p>217.前 K 个高频元素：<a href=\"https://leetcode.cn/problems/top-k-frequent-elements/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode.cn/problems/top-k-frequent-elements/</a></p>\n",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "计算机基础"
      ]
    },
    {
      "title": "几道常见的链表算法题",
      "url": "https://codingmap.strova.top/cs-basics/algorithms/linkedlist-algorithm-problems.html",
      "id": "https://codingmap.strova.top/cs-basics/algorithms/linkedlist-algorithm-problems.html",
      "summary": "精选链表高频题的思路与实现，覆盖两数相加、反转、环检测等场景，强调边界处理与复杂度分析。重点围绕 1. 两数相加、2. 翻转链表、3. 链表中倒数第 k 个节点、4. 删除链表的倒数第 N 个节点、算法 等内容展开。结合 CodingMap 知识体系梳理 计算机基础 / 算法 / Linkedlist Algorit。",
      "content_html": "<!-- markdownlint-disable MD024 -->\n<h2>1. 两数相加</h2>\n<h3>题目描述</h3>\n<blockquote>\n<p>Leetcode:给定两个非空链表来表示两个非负整数。位数按照逆序方式存储，它们的每个节点只存储单个数字。将两数相加返回一个新的链表。</p>\n<p>你可以假设除了数字 0 之外，这两个数字都不会以零开头。</p>\n</blockquote>\n<p>示例：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>输入：(2 -> 4 -> 3) + (5 -> 6 -> 4)</span></span>\n<span class=\"line\"><span>输出：7 -> 0 -> 8</span></span>\n<span class=\"line\"><span>原因：342 + 465 = 807</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h3>问题分析</h3>\n<p>Leetcode 官方详细解答地址：</p>\n<p><a href=\"https://leetcode-cn.com/problems/add-two-numbers/solution/\" target=\"_blank\" rel=\"noopener noreferrer\">https://leetcode-cn.com/problems/add-two-numbers/solution/</a></p>\n<blockquote>\n<p>要对头结点进行操作时，考虑创建哑节点 dummy，使用 dummy-&gt;next 表示真正的头节点。这样可以避免处理头节点为空的边界问题。</p>\n</blockquote>\n<p>我们使用变量来跟踪进位，并从包含最低有效位的表头开始模拟逐位相加的过程。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/34910956.jpg\" alt=\"图1，对两数相加方法的可视化: 342 + 465 = 807， 每个结点都包含一个数字，并且数字按位逆序存储。\"></p>\n<h3>Solution</h3>\n<p><strong>我们首先从最低有效位也就是列表 l1 和 l2 的表头开始相加。注意需要考虑到进位的情况！</strong></p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">/**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * Definition for singly-linked list.</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * public class ListNode {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> *     int val;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> *     ListNode next;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> *     ListNode(int x) { val = x; }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> //https://leetcode-cn.com/problems/add-two-numbers/description/</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Solution</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ListNode</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> addTwoNumbers</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">ListNode</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> l1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">ListNode</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> l2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> dummyHead</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> ListNode</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> p</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> l1, q </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> l2, curr </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> dummyHead;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    //carry 表示进位数</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> carry</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    while</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (p </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> ||</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> q </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> x</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (p </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">?</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> p</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">val</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> :</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> y</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (q </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">?</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> q</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">val</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> :</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> sum</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> carry </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> x </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> y;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        //进位数</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        carry </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> sum </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">/</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 10</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        //新节点的数值为sum % 10</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        curr</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> ListNode</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(sum </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">%</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 10</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        curr </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> curr</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (p </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) p </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> p</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (q </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) q </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> q</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (carry </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">></span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        curr</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> ListNode</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(carry);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> dummyHead</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h2>2. 翻转链表</h2>\n<h3>题目描述</h3>\n<blockquote>\n<p>剑指 offer:输入一个链表，反转链表后，输出链表的所有元素。</p>\n</blockquote>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/81431871.jpg\" alt=\"翻转链表\"></p>\n<h3>问题分析</h3>\n<p>这道算法题，说直白点就是：如何让后一个节点指向前一个节点！在下面的代码中定义了一个 next 节点，该节点主要是保存要反转到头的那个节点，防止链表 “断裂”。</p>\n<h3>Solution</h3>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ListNode</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> val</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">  ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> next </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">  ListNode</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> val</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    this</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">val</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> val;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">/**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> *</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * </span><span style=\"--shiki-light:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#C678DD;--shiki-dark-font-style:italic\">@author</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> Snailclimb</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * @date 2018年9月19日</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * @Description: TODO</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Solution</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ListNode</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> ReverseList</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">ListNode</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> head</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> pre</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    while</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (head </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">      // 保存要反转到头的那个节点</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      next </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> head</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">      // 要反转的那个节点指向已经反转的上一个节点(备注:第一次反转的时候会指向null)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">      head</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> pre;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">      // 上一个已经反转到头部的节点</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      pre </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> head;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">      // 一直向链表尾走</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      head </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> next;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> pre;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>测试方法：</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> main</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">[] args) {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> a </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> ListNode</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> b </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> ListNode</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> c </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> ListNode</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">3</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> d </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> ListNode</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">4</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> e </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> ListNode</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">5</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    a</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> b</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    b</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> c</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    c</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> d</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    d</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> Solution</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">()</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">ReverseList</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(a);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    while</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\"> (e </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">      System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">val</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">      e </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> e</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#E06C75\">  }</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>输出：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>5</span></span>\n<span class=\"line\"><span>4</span></span>\n<span class=\"line\"><span>3</span></span>\n<span class=\"line\"><span>2</span></span>\n<span class=\"line\"><span>1</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h2>3. 链表中倒数第 k 个节点</h2>\n<h3>题目描述</h3>\n<blockquote>\n<p>剑指 offer: 输入一个链表，输出该链表中倒数第 k 个结点。</p>\n</blockquote>\n<h3>问题分析</h3>\n<blockquote>\n<p><strong>链表中倒数第 k 个节点也就是正数第(L-K+1)个节点，知道了这一点，这一题基本就没问题！</strong></p>\n</blockquote>\n<p>首先两个节点/指针，一个节点 node1 先开始跑，指针 node1 跑到 k-1 个节点后，另一个节点 node2 开始跑，当 node1 跑到最后时，node2 所指的节点就是倒数第 k 个节点也就是正数第(L-K+1)个节点。</p>\n<h3>Solution</h3>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">/*</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">public class ListNode {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    int val;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    ListNode next = null;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    ListNode(int val) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        this.val = val;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">}*/</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 时间复杂度O(n),一次遍历即可</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&#x26;tqId=11167&#x26;tPage=1&#x26;rp=1&#x26;ru=/ta/coding-interviews&#x26;qru=/ta/coding-interviews/question-ranking</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Solution</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ListNode</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> FindKthToTail</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">ListNode</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> head</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> k</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 如果链表为空或者k小于等于0</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (head </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">==</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> ||</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> k </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      return</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 声明两个指向头结点的节点</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> node1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> head, node2 </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> head;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 记录节点的个数</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> count</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 记录k值，后面要使用</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> index</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> k;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // p指针先跑，并且记录节点数，当node1节点跑了k-1个节点后，node2节点开始跑，</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 当node1节点跑到最后时，node2节点所指的节点就是倒数第k个节点</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    while</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (node1 </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      node1 </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> node1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      count++;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (k </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        node2 </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> node2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      k--;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 如果节点个数小于所求的倒数第k个节点，则返回空</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (count </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> index)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      return</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> node2;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h2>4. 删除链表的倒数第 N 个节点</h2>\n<blockquote>\n<p>Leetcode:给定一个链表，删除链表的倒数第 n 个节点，并且返回链表的头结点。</p>\n</blockquote>\n<p><strong>示例：</strong></p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>给定一个链表: 1->2->3->4->5, 和 n = 2.</span></span>\n<span class=\"line\"><span></span></span>\n<span class=\"line\"><span>当删除了倒数第二个节点后，链表变为 1->2->3->5.</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><strong>说明：</strong></p>\n<p>给定的 n 保证是有效的。</p>\n<p><strong>进阶：</strong></p>\n<p>你能尝试使用一趟扫描实现吗？</p>\n<p>该题在 LeetCode 上有详细解答，具体可参考 LeetCode。</p>\n<h3>问题分析</h3>\n<p>我们注意到这个问题可以容易地简化成另一个问题：删除从列表开头数起的第 (L - n + 1)个结点，其中 L 是列表的长度。只要我们找到列表的长度 L，这个问题就很容易解决。</p>\n<p><img src=\"https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/94354387.jpg\" alt=\"图 1. 删除列表中的第 L - n + 1 个元素\"></p>\n<h3>Solution</h3>\n<p><strong>两次遍历法</strong></p>\n<p>首先我们将添加一个 <strong>哑结点</strong> 作为辅助，该结点位于列表头部。哑结点用来简化某些极端情况，例如列表中只含有一个结点，或需要删除列表的头部。在第一次遍历中，我们找出列表的长度 L。然后设置一个指向哑结点的指针，并移动它遍历列表，直至它到达第 (L - n) 个结点那里。<strong>我们把第 (L - n)个结点的 next 指针重新链接至第 (L - n + 2)个结点，完成这个算法。</strong></p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">/**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * Definition for singly-linked list.</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * public class ListNode {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> *     int val;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> *     ListNode next;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> *     ListNode(int x) { val = x; }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/description/</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Solution</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ListNode</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> removeNthFromEnd</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">ListNode</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> head</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> n</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 哑结点，哑结点用来简化某些极端情况，例如列表中只含有一个结点，或需要删除列表的头部</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> dummy</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> ListNode</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 哑结点指向头结点</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    dummy</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> head;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 保存链表长度</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> len</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> head;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    while</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (len </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      length++;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      len </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> len</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    length </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> length </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> n;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> target</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> dummy;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 找到 L-n 位置的节点</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    while</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (length </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">></span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      target </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> target</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      length--;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 把第 (L - n)个结点的 next 指针重新链接至第 (L - n + 2)个结点</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    target</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> target</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> dummy</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p><strong>进阶——一次遍历法：</strong></p>\n<blockquote>\n<p>链表中倒数第 N 个节点也就是正数第(L - n + 1)个节点。</p>\n</blockquote>\n<p>其实这种方法就和我们上面第四题找“链表中倒数第 k 个节点”所用的思想是一样的。<strong>基本思路就是：</strong> 定义两个节点 node1、node2；node1 节点先跑，node1 节点跑到第 n+1 个节点的时候，node2 节点开始跑。当 node1 节点跑到最后一个节点时，node2 节点所在的位置就是第（L - n）个节点（L 代表总链表长度，也就是倒数第 n + 1 个节点）。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">/**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * Definition for singly-linked list.</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * public class ListNode {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> *     int val;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> *     ListNode next;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> *     ListNode(int x) { val = x; }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Solution</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ListNode</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> removeNthFromEnd</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">ListNode</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> head</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> n</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> dummy</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> ListNode</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    dummy</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> head;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 声明两个指向头结点的节点</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    ListNode</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> node1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> dummy, node2 </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> dummy;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // node1 节点先跑，node1节点 跑到第 n 个节点的时候,node2 节点开始跑</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 当node1 节点跑到最后一个节点时，node2 节点所在的位置就是第 （L-n ） 个节点，也就是倒数第 n+1（L代表总链表长度）</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    while</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (node1 </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      node1 </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> node1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (n </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> &#x26;&#x26;</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> node1 </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        node2 </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> node2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      n--;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    node2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> node2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> dummy</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h2>5. 合并两个排序的链表</h2>\n<h3>题目描述</h3>\n<blockquote>\n<p>剑指 offer:输入两个单调递增的链表，输出两个链表合成后的链表，当然我们需要合成后的链表满足单调不减规则。</p>\n</blockquote>\n<h3>问题分析</h3>\n<p>我们可以这样分析：</p>\n<ol>\n<li>假设我们有两个链表 A，B；</li>\n<li>A 的头节点 A1 的值与 B 的头结点 B1 的值比较，假设 A1 小，则 A1 为头节点；</li>\n<li>A2 再和 B1 比较，假设 B1 小，则 A1 指向 B1；</li>\n<li>A2 再和 B2 比较</li>\n<li>就这样循环往复就行了，应该还算好理解。</li>\n</ol>\n<p>考虑通过递归的方式实现！</p>\n<h3>Solution</h3>\n<p><strong>递归版本：</strong></p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">/*</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">public class ListNode {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    int val;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    ListNode next = null;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    ListNode(int val) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        this.val = val;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">}*/</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">//https://www.nowcoder.com/practice/d8b6b4358f774294a89de2a6ac4d9337?tpId=13&#x26;tqId=11169&#x26;tPage=1&#x26;rp=1&#x26;ru=/ta/coding-interviews&#x26;qru=/ta/coding-interviews/question-ranking</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Solution</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> ListNode</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> Merge</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">ListNode</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> list1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">ListNode</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> list2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (list1 </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">==</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> list2;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (list2 </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">==</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> list1;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">list1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">val</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> &#x3C;=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> list2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">val</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">      list1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> Merge</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">list1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, list2);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> list1;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">else</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">      list2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> Merge</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(list1, </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">list2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">next</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> list2;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div>",
      "image": "https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/34910956.jpg",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "计算机基础"
      ]
    },
    {
      "title": "几道常见的字符串算法题",
      "url": "https://codingmap.strova.top/cs-basics/algorithms/string-algorithm-problems.html",
      "id": "https://codingmap.strova.top/cs-basics/algorithms/string-algorithm-problems.html",
      "summary": "总结字符串高频算法与题型，重点讲解 KMP/BM 原理、滑动窗口等技巧，助力高效匹配与实现。重点围绕 1. KMP 算法、2. 替换空格、3. 最长公共前缀、4. 回文串、算法 等内容展开。结合 CodingMap 知识体系梳理 计算机基础 / 算法 / String Algorithm Problems.Html 。",
      "content_html": "<blockquote>\n<p>作者：wwwxmu</p>\n<p>原文地址：<a href=\"https://www.weiweiblog.cn/13string/\" target=\"_blank\" rel=\"noopener noreferrer\">https://www.weiweiblog.cn/13string/</a></p>\n</blockquote>\n<h2>1. KMP 算法</h2>\n<p>谈到字符串问题，不得不提的就是 KMP 算法，它是用来解决字符串查找的问题，可以在一个字符串（S）中查找一个子串（W）出现的位置。KMP 算法把字符匹配的时间复杂度缩小到 O(m+n)，而空间复杂度也只有 O(m)。因为 “暴力搜索” 的方法会反复回溯主串，导致效率低下，而 KMP 算法可以利用已经部分匹配这个有效信息，保持主串上的指针不回溯，通过修改子串的指针，让模式串尽量地移动到有效的位置。</p>\n<p>具体算法细节请参考：</p>\n<ul>\n<li><a href=\"https://blog.csdn.net/v_july_v/article/details/7041827\" target=\"_blank\" rel=\"noopener noreferrer\">从头到尾彻底理解 KMP:</a></li>\n<li><a href=\"https://www.zhihu.com/question/21923021\" target=\"_blank\" rel=\"noopener noreferrer\">如何更好的理解和掌握 KMP 算法?</a></li>\n<li><a href=\"https://blog.sengxian.com/algorithms/kmp\" target=\"_blank\" rel=\"noopener noreferrer\">KMP 算法详细解析</a></li>\n<li><a href=\"http://blog.jobbole.com/76611/\" target=\"_blank\" rel=\"noopener noreferrer\">图解 KMP 算法</a></li>\n<li><a href=\"https://www.bilibili.com/video/av3246487/?from=search&amp;seid=17173603269940723925\" target=\"_blank\" rel=\"noopener noreferrer\">汪都能听懂的 KMP 字符串匹配算法【双语字幕】</a></li>\n<li><a href=\"https://www.bilibili.com/video/av11866460?from=search&amp;seid=12730654434238709250\" target=\"_blank\" rel=\"noopener noreferrer\">KMP 字符串匹配算法 1</a></li>\n</ul>\n<p><strong>除此之外，再来了解一下 BM 算法！</strong></p>\n<blockquote>\n<p>BM 算法也是一种精确字符串匹配算法，它采用从右向左比较的方法，同时应用到了两种启发式规则，即坏字符规则和好后缀规则，来决定向右跳跃的距离。基本思路就是从右往左进行字符匹配，遇到不匹配的字符后从坏字符表和好后缀表找一个最大的右移值，将模式串右移继续匹配。<br>\n《字符串匹配的 KMP 算法》：<a href=\"http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html\" target=\"_blank\" rel=\"noopener noreferrer\">http://www.ruanyifeng.com/blog/2013/05/Knuth–Morris–Pratt_algorithm.html</a></p>\n</blockquote>\n<h2>2. 替换空格</h2>\n<blockquote>\n<p>剑指 offer：请实现一个函数，将一个字符串中的每个空格替换成 &quot;%20&quot;。例如，当字符串为 We Are Happy.则经过替换之后的字符串为 We%20Are%20Happy。</p>\n</blockquote>\n<p>这里我提供了两种方法：① 常规方法；② 利用 API 解决。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">//https://www.weiweiblog.cn/replacespace/</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Solution</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  /**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">   * 第一种方法：常规方法。利用String.charAt(i)以及String.valueOf(char).equals(\" \"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">   * )遍历字符串并判断元素是否为空格。是则替换为\"%20\",否则不替换</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">   */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> replaceSpace</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">StringBuffer</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> str</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> str</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // System.out.println(\"length=\" + length);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    StringBuffer</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> result</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> StringBuffer</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> length; i++) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      char</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> b</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> str</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">charAt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(i);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">valueOf</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(b).</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">equals</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\" \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        result</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">append</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"%20\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">else</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        result</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">append</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(b);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> result</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">toString</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  /**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">   * 第二种方法：利用API替换掉所用空格，一行代码解决问题</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">   */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> replaceSpace2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">StringBuffer</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> str</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> str</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">toString</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">().</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">replaceAll</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"</span><span style=\"--shiki-light:#0184BC;--shiki-dark:#56B6C2\">\\\\</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">s\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"%20\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>对于替换固定字符（比如空格）的情况，第二种方法其实可以使用 <code>replace</code> 方法替换，性能更好！</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">str</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">toString</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">().</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">replace</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\" \"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"%20\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div></div></div><h2>3. 最长公共前缀</h2>\n<blockquote>\n<p>Leetcode: 编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀，返回空字符串 &quot;&quot;。</p>\n</blockquote>\n<p>示例 1：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>输入: [\"flower\",\"flow\",\"flight\"]</span></span>\n<span class=\"line\"><span>输出: \"fl\"</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>示例 2：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>输入: [\"dog\",\"racecar\",\"car\"]</span></span>\n<span class=\"line\"><span>输出: \"\"</span></span>\n<span class=\"line\"><span>解释: 输入不存在公共前缀。</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>思路很简单！先利用 <code>Arrays.sort(strs)</code> 为数组排序，再将数组第一个元素和最后一个元素的字符从前往后对比即可！</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Main</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> replaceSpace</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">[] </span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\">strs</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  // 如果检查值不合法及就返回空串</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">checkStrs</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(strs)) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">   return</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  // 数组长度</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> len</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> strs</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  // 用于保存结果</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">  StringBuilder</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> res</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> StringBuilder</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  // 给字符串数组的元素按照升序排序(包含数字的话，数字会排在前面)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">  Arrays</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">sort</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(strs);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> m</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> strs[</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">].</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> n</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> strs[len </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">].</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> num</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Math</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">min</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(m, n);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> num; i++) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">   if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (strs[</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">].</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">charAt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(i) </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">==</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> strs[len </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">].</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">charAt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(i)) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    res</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">append</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(strs[</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">].</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">charAt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(i));</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">   } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">else</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    break</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> res</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">toString</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> boolean</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> checkStrs</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">[] </span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\">strs</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  boolean</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> flag</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> false</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (strs </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">   // 遍历strs检查元素值</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">   for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> strs</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i++) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (strs[i] </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> null</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> &#x26;&#x26;</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> strs[i].</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">     flag </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> true</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">else</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">     flag </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> false</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">     break</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">   }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> flag;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> // 测试</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> main</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">[] </span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\">args</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">  String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">[] </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">strs</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> { </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"customer\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"car\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"cat\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> };</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  // String[] strs = { \"customer\", \"car\", null };//空串</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  // String[] strs = {};//空串</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">  // String[] strs = null;//空串</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">  System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">Main</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">replaceSpace</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(strs));</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// c</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h2>4. 回文串</h2>\n<h3>4.1. 最长回文串</h3>\n<blockquote>\n<p>LeetCode: 给定一个包含大写字母和小写字母的字符串，找到通过这些字母构造成的最长的回文串。在构造过程中，请注意区分大小写。比如 <code>&quot;Aa&quot;</code> 不能当做一个回文字符串。注意：假设字符串的长度不会超过 1010。</p>\n<p>回文串：“回文串” 是一个正读和反读都一样的字符串，比如 &quot;level&quot; 或者 &quot;noon&quot; 等等就是回文串。——百度百科 地址：<a href=\"https://baike.baidu.com/item/%E5%9B%9E%E6%96%87%E4%B8%B2/1274921?fr=aladdin\" target=\"_blank\" rel=\"noopener noreferrer\">https://baike.baidu.com/item/回文串/1274921?fr=aladdin</a></p>\n</blockquote>\n<p>示例 1：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>输入:</span></span>\n<span class=\"line\"><span>\"abccccdd\"</span></span>\n<span class=\"line\"><span></span></span>\n<span class=\"line\"><span>输出:</span></span>\n<span class=\"line\"><span>7</span></span>\n<span class=\"line\"><span></span></span>\n<span class=\"line\"><span>解释:</span></span>\n<span class=\"line\"><span>我们可以构造的最长的回文串是\"dccaccd\", 它的长度是 7。</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>我们上面已经知道了什么是回文串？现在我们考虑一下可以构成回文串的两种情况：</p>\n<ul>\n<li>字符出现次数为双数的组合</li>\n<li><strong>字符出现次数为偶数的组合+单个字符中出现次数最多且为奇数次的字符</strong>（参见 <strong><a href=\"https://github.com/Snailclimb/JavaGuide/issues/665\" target=\"_blank\" rel=\"noopener noreferrer\">issue665</a></strong>）</li>\n</ul>\n<p>统计字符出现的次数即可，双数才能构成回文。因为允许中间一个数单独出现，比如 &quot;abcba&quot;，所以如果最后有字母落单，总长度可以加 1。首先将字符串转变为字符数组。然后遍历该数组，判断对应字符是否在 hashset 中，如果不在就加进去，如果在就让 count++，然后移除该字符！这样就能找到出现次数为双数的字符个数。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">//https://leetcode-cn.com/problems/longest-palindrome/description/</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Solution</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  int</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> longestPalindrome</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">==</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      return</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 用于存放字符</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    HashSet</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Character</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">> </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">hashset</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> HashSet</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">&#x3C;</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">Character</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">>();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#C678DD\">    char</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">[] </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">chars</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">toCharArray</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> count</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> chars</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i++) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">hashset</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">contains</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(chars[i])) {</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 如果hashset没有该字符就保存进去</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        hashset</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">add</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(chars[i]);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">else</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 如果有,就让count++（说明找到了一个成对的字符），然后把该字符移除</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">        hashset</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">remove</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(chars[i]);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        count++;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> hashset</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">isEmpty</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">?</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> count </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">*</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 2</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> :</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> count </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">*</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h3>4.2. 验证回文串</h3>\n<blockquote>\n<p>LeetCode: 给定一个字符串，验证它是否是回文串，只考虑字母和数字字符，可以忽略字母的大小写。说明：本题中，我们将空字符串定义为有效的回文串。</p>\n</blockquote>\n<p>示例 1：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>输入: \"A man, a plan, a canal: Panama\"</span></span>\n<span class=\"line\"><span>输出: true</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>示例 2：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>输入: \"race a car\"</span></span>\n<span class=\"line\"><span>输出: false</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">//https://leetcode-cn.com/problems/valid-palindrome/description/</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Solution</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  boolean</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> isPalindrome</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">==</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      return</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> true</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> l</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, r </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    while</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (l </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> r) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">      // 从头和尾开始向中间遍历</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">Character</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">isLetterOrDigit</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">charAt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(l))) {</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 字符不是字母和数字的情况</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        l++;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">else</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">Character</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">isLetterOrDigit</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">charAt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(r))) {</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 字符不是字母和数字的情况</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        r--;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">else</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">        // 判断二者是否相等</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">Character</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">toLowerCase</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">charAt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(l)) </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Character</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">toLowerCase</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">charAt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(r)))</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">          return</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> false</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        l++;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        r--;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> true</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h3>4.3. 最长回文子串</h3>\n<blockquote>\n<p>LeetCode: 最长回文子串 给定一个字符串 s，找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。</p>\n</blockquote>\n<p>示例 1：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>输入: \"babad\"</span></span>\n<span class=\"line\"><span>输出: \"bab\"</span></span>\n<span class=\"line\"><span>注意: \"aba\"也是一个有效答案。</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>示例 2：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>输入: \"cbbd\"</span></span>\n<span class=\"line\"><span>输出: \"bb\"</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>以某个元素为中心，分别计算偶数长度的回文最大长度和奇数长度的回文最大长度。</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">//https://leetcode-cn.com/problems/longest-palindromic-substring/description/</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Solution</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  private</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> index</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">,</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> len</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> String</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> longestPalindrome</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> s;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i++) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">      PalindromeHelper</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(s, i, i);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">      PalindromeHelper</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(s, i, i </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    return</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">substring</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(index, index </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> len);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> PalindromeHelper</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> l</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> r</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    while</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (l </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">>=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> &#x26;&#x26;</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> r </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x26;&#x26;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">charAt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(l) </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">==</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">charAt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(r)) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      l--;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      r++;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (len </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> r </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> l </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      index </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> l </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      len </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> r </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> l </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h3>4.4. 最长回文子序列</h3>\n<blockquote>\n<p>LeetCode: 最长回文子序列<br>\n给定一个字符串 s，找到其中最长的回文子序列。可以假设 s 的最大长度为 1000。<br>\n<strong>最长回文子序列和上一题最长回文子串的区别是，子串是字符串中连续的一个序列，而子序列是字符串中保持相对位置的字符序列，例如，&quot;bbbb&quot; 可以是字符串 &quot;bbbab&quot; 的子序列但不是子串。</strong></p>\n</blockquote>\n<p>给定一个字符串 s，找到其中最长的回文子序列。可以假设 s 的最大长度为 1000。</p>\n<p>示例 1：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>输入:</span></span>\n<span class=\"line\"><span>\"bbbab\"</span></span>\n<span class=\"line\"><span>输出:</span></span>\n<span class=\"line\"><span>4</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>一个可能的最长回文子序列为 &quot;bbbb&quot;。</p>\n<p>示例 2：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>输入:</span></span>\n<span class=\"line\"><span>\"cbbd\"</span></span>\n<span class=\"line\"><span>输出:</span></span>\n<span class=\"line\"><span>2</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>一个可能的最长回文子序列为 &quot;bb&quot;。</p>\n<p><strong>动态规划：</strong> <code>dp[i][j] = dp[i+1][j-1] + 2 if s.charAt(i) == s.charAt(j) otherwise, dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1])</code></p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Solution</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> int</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> longestPalindromeSubseq</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> len</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#C678DD\">        int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\"> [][] </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">dp</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#C18401;--shiki-dark:#C678DD\"> int</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">[len][len];</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> len </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">>=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i--){</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">            dp[i][i] </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">            for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> j</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; j </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> len; j++){</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">                if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">charAt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(i) </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">==</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">charAt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(j))</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">                    dp[i][j] </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> dp[i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">][j</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">] </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">                else</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">                    dp[i][j] </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Math</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">max</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(dp[i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">+</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">][j], dp[i][j</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">]);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">            }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> dp[</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">][len</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">];</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h2>5. 括号匹配深度</h2>\n<blockquote>\n<p>爱奇艺 2018 秋招 Java：<br>\n一个合法的括号匹配序列有以下定义：</p>\n<ol>\n<li>空串 &quot;&quot; 是一个合法的括号匹配序列</li>\n<li>如果 &quot;X&quot; 和 &quot;Y&quot; 都是合法的括号匹配序列，&quot;XY&quot; 也是一个合法的括号匹配序列</li>\n<li>如果 &quot;X&quot; 是一个合法的括号匹配序列，那么 &quot;(X)&quot; 也是一个合法的括号匹配序列</li>\n<li>每个合法的括号序列都可以由以上规则生成。</li>\n</ol>\n<p>例如：&quot;&quot;，&quot;()&quot;，&quot;()()&quot;，&quot;((()))&quot; 都是合法的括号序列。<br>\n对于一个合法的括号序列我们又有以下定义它的深度：</p>\n<ol>\n<li>空串 &quot;&quot; 的深度是 0</li>\n<li>如果字符串 &quot;X&quot; 的深度是 x，字符串 &quot;Y&quot; 的深度是 y，那么字符串 &quot;XY&quot; 的深度为 max(x, y)</li>\n<li>如果 &quot;X&quot; 的深度是 x，那么字符串 &quot;(X)&quot; 的深度是 x+1</li>\n</ol>\n<p>例如：&quot;()()()&quot; 的深度是 1，&quot;((()))&quot; 的深度是 3。牛牛现在给你一个合法的括号序列，需要你计算出其深度。</p>\n</blockquote>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>输入描述:</span></span>\n<span class=\"line\"><span>输入包括一个合法的括号序列s,s长度length(2 ≤ length ≤ 50),序列中只包含'('和')'。</span></span>\n<span class=\"line\"><span></span></span>\n<span class=\"line\"><span>输出描述:</span></span>\n<span class=\"line\"><span>输出一个正整数,即这个序列的深度。</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>示例：</p>\n<div class=\"language-plain line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"plain\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-plain\"><span class=\"line\"><span>输入:</span></span>\n<span class=\"line\"><span>(())</span></span>\n<span class=\"line\"><span>输出:</span></span>\n<span class=\"line\"><span>2</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><p>代码如下：</p>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">import</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> java.util.Scanner</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">/**</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * https://www.nowcoder.com/test/8246651/summary</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> *</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * </span><span style=\"--shiki-light:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#C678DD;--shiki-dark-font-style:italic\">@author</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> Snailclimb</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * @date 2018年9月6日</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> * @Description: TODO 求给定合法括号序列的深度</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\"> */</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Main</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> main</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">[] </span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\">args</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    Scanner</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> sc</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> new</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> Scanner</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">in</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> sc</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">nextLine</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> cnt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, max </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">, i;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (i </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(); ++i) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">charAt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(i) </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">==</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> '('</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        cnt++;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      else</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        cnt--;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      max </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Math</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">max</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(max, cnt);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    sc</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">close</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(max);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div><h2>6. 把字符串转换成整数</h2>\n<blockquote>\n<p>剑指 offer: 将一个字符串转换成一个整数（实现 <code>Integer.valueOf(string)</code> 的功能，但是 string 不符合数字要求时返回 0），要求不能使用字符串转换整数的库函数。数值为 0 或者字符串不是一个合法的数值则返回 0。</p>\n</blockquote>\n<div class=\"language-java line-numbers-mode\" data-highlighter=\"shiki\" data-ext=\"java\" style=\"--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34\"><pre class=\"shiki shiki-themes one-light one-dark-pro vp-code\"><code class=\"language-java\"><span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">//https://www.weiweiblog.cn/strtoint/</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> class</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\"> Main</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> int</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> StrToInt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\"> str</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">str</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">() </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">==</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      return</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#C678DD\">    char</span><span style=\"--shiki-light:#E45649;--shiki-dark:#ABB2BF\">[] </span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\">chars</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> str</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">toCharArray</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // 判断是否存在符号位</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> flag</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (chars[</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">] </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">==</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> '+'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      flag </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    else</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (chars[</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\">0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">] </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">==</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> '-'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      flag </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 2</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> start</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> flag </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">></span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> ?</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 1</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> :</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> res</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 保存结果</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    for</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> i</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> start; i </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">&#x3C;</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> chars</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">length</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">; i++) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">      if</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> (</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">Character</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">isDigit</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(chars[i])) {</span><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">// 调用Character.isDigit(char)方法判断是否是数字，是返回True，否则False</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> temp</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> chars[i] </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">-</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> '0'</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">        res </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">=</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> res </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">*</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 10</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> temp;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      } </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">else</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">        return</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 0</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">      }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">    }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">   return</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> flag </span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\">!=</span><span style=\"--shiki-light:#986801;--shiki-dark:#D19A66\"> 2</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> ?</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> res </span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">:</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> -</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">res;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">  public</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> static</span><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\"> void</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\"> main</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">String</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">[] </span><span style=\"--shiki-light:#383A42;--shiki-light-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic\">args</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">)</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic\">    // TODO Auto-generated method stub</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#C18401;--shiki-dark:#E5C07B\">    String</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> s</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\"> \"-12312312\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"使用库函数转换：\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Integer</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">valueOf</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(s));</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#A626A4;--shiki-dark:#C678DD\">    int</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E06C75\"> res</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> =</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\"> Main</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">StrToInt</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(s);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">    System</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#E45649;--shiki-dark:#E5C07B\">out</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">.</span><span style=\"--shiki-light:#4078F2;--shiki-dark:#61AFEF\">println</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">(</span><span style=\"--shiki-light:#50A14F;--shiki-dark:#98C379\">\"使用自己写的方法转换：\"</span><span style=\"--shiki-light:#383A42;--shiki-dark:#56B6C2\"> +</span><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\"> res);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#383A42;--shiki-dark:#ABB2BF\">}</span></span></code></pre>\n<div class=\"line-numbers\" aria-hidden=\"true\" style=\"counter-reset:line-number 0\"><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div><div class=\"line-number\"></div></div></div>",
      "date_published": "2026-06-05T13:49:21.000Z",
      "date_modified": "2026-06-05T13:49:21.000Z",
      "authors": [],
      "tags": [
        "计算机基础"
      ]
    }
  ]
}