diff --git a/README.md b/README.md index c86501d0598..b5e832382ec 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ -推荐你通过在线阅读网站进行阅读,体验更好,速度更快!地址:[javaguide.cn](https://javaguide.cn/)。 +# JavaGuide -[](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) +> I recommend reading through online reading websites for a better experience and faster speed! +> 📚 Address: [javaguide.cn](https://javaguide.cn/) + +[](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)
@@ -10,437 +13,129 @@
-> - **面试专版**:准备 Java 面试的小伙伴可以考虑面试专版:**[《Java 面试指北 》](./docs/zhuanlan/java-mian-shi-zhi-bei.md)** (质量很高,专为面试打造,配合 JavaGuide 食用)。 -> - **知识星球**:专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 **[JavaGuide 知识星球](./docs/about-the-author/zhishixingqiu-two-years.md)**(点击链接即可查看星球的详细介绍,一定确定自己真的需要再加入)。 -> - **使用建议** :有水平的面试官都是顺着项目经历挖掘技术问题。一定不要死记硬背技术八股文!详细的学习建议请参考:[JavaGuide 使用建议](./docs/javaguide/use-suggestion.md)。 -> - **求个Star**:如果觉得 JavaGuide 的内容对你有帮助的话,还请点个免费的 Star,这是对我最大的鼓励,感谢各位一起同行,共勉!Github 地址:[https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) 。 -> - **转载须知**:以下所有文章如非文首说明为转载皆为 JavaGuide 原创,转载请在文首注明出处。如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! +--- + +> - **Interview Special Edition**: Preparing for Java interviews? Check out the Interview Special Edition: **[Java Interview Guide](./docs/zhuanlan/java-mian-shi-zhi-bei.md)** — high quality and tailored for interview prep, meant to be used alongside JavaGuide. +> - **Knowledge Planet**: For exclusive interview booklets, one-on-one communication, resume help, and job-seeking guidance, consider joining **[JavaGuide Knowledge Planet](./docs/about-the-author/zhishixingqiu-two-years.md)**. +> - **Usage Suggestions**: Don't memorize jargon! Interviewers care about real-world application. Check out our [Usage Suggestions](./docs/javaguide/use-suggestion.md). +> - **Request a Star**: If JavaGuide helps you, please ⭐ the project — it means a lot! GitHub: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) +> - **Reprint Notice**: All articles are original unless otherwise noted. Please include a source link when sharing. Unauthorized use may result in legal action.
- +
- - -## 项目相关 - -- [项目介绍](https://javaguide.cn/javaguide/intro.html) -- [使用建议](https://javaguide.cn/javaguide/use-suggestion.html) -- [贡献指南](https://javaguide.cn/javaguide/contribution-guideline.html) -- [常见问题](https://javaguide.cn/javaguide/faq.html) - -## Java - -### 基础 - -**知识点/面试题总结** : (必看:+1: ): - -- [Java 基础常见知识点&面试题总结(上)](./docs/java/basis/java-basic-questions-01.md) -- [Java 基础常见知识点&面试题总结(中)](./docs/java/basis/java-basic-questions-02.md) -- [Java 基础常见知识点&面试题总结(下)](./docs/java/basis/java-basic-questions-03.md) - -**重要知识点详解**: - -- [为什么 Java 中只有值传递?](./docs/java/basis/why-there-only-value-passing-in-java.md) -- [Java 序列化详解](./docs/java/basis/serialization.md) -- [泛型&通配符详解](./docs/java/basis/generics-and-wildcards.md) -- [Java 反射机制详解](./docs/java/basis/reflection.md) -- [Java 代理模式详解](./docs/java/basis/proxy.md) -- [BigDecimal 详解](./docs/java/basis/bigdecimal.md) -- [Java 魔法类 Unsafe 详解](./docs/java/basis/unsafe.md) -- [Java SPI 机制详解](./docs/java/basis/spi.md) -- [Java 语法糖详解](./docs/java/basis/syntactic-sugar.md) - -### 集合 - -**知识点/面试题总结**: - -- [Java 集合常见知识点&面试题总结(上)](./docs/java/collection/java-collection-questions-01.md) (必看 :+1:) -- [Java 集合常见知识点&面试题总结(下)](./docs/java/collection/java-collection-questions-02.md) (必看 :+1:) -- [Java 容器使用注意事项总结](./docs/java/collection/java-collection-precautions-for-use.md) - -**源码分析**: - -- [ArrayList 核心源码+扩容机制分析](./docs/java/collection/arraylist-source-code.md) -- [LinkedList 核心源码分析](./docs/java/collection/linkedlist-source-code.md) -- [HashMap 核心源码+底层数据结构分析](./docs/java/collection/hashmap-source-code.md) -- [ConcurrentHashMap 核心源码+底层数据结构分析](./docs/java/collection/concurrent-hash-map-source-code.md) -- [LinkedHashMap 核心源码分析](./docs/java/collection/linkedhashmap-source-code.md) -- [CopyOnWriteArrayList 核心源码分析](./docs/java/collection/copyonwritearraylist-source-code.md) -- [ArrayBlockingQueue 核心源码分析](./docs/java/collection/arrayblockingqueue-source-code.md) -- [PriorityQueue 核心源码分析](./docs/java/collection/priorityqueue-source-code.md) -- [DelayQueue 核心源码分析](./docs/java/collection/delayqueue-source-code.md) - -### IO - -- [IO 基础知识总结](./docs/java/io/io-basis.md) -- [IO 设计模式总结](./docs/java/io/io-design-patterns.md) -- [IO 模型详解](./docs/java/io/io-model.md) -- [NIO 核心知识总结](./docs/java/io/nio-basis.md) - -### 并发 - -**知识点/面试题总结** : (必看 :+1:) - -- [Java 并发常见知识点&面试题总结(上)](./docs/java/concurrent/java-concurrent-questions-01.md) -- [Java 并发常见知识点&面试题总结(中)](./docs/java/concurrent/java-concurrent-questions-02.md) -- [Java 并发常见知识点&面试题总结(下)](./docs/java/concurrent/java-concurrent-questions-03.md) - -**重要知识点详解**: - -- [乐观锁和悲观锁详解](./docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md) -- [CAS 详解](./docs/java/concurrent/cas.md) -- [JMM(Java 内存模型)详解](./docs/java/concurrent/jmm.md) -- **线程池**:[Java 线程池详解](./docs/java/concurrent/java-thread-pool-summary.md)、[Java 线程池最佳实践](./docs/java/concurrent/java-thread-pool-best-practices.md) -- [ThreadLocal 详解](./docs/java/concurrent/threadlocal.md) -- [Java 并发容器总结](./docs/java/concurrent/java-concurrent-collections.md) -- [Atomic 原子类总结](./docs/java/concurrent/atomic-classes.md) -- [AQS 详解](./docs/java/concurrent/aqs.md) -- [CompletableFuture 详解](./docs/java/concurrent/completablefuture-intro.md) - -### JVM (必看 :+1:) - -JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.com/javase/specs/jvms/se8/html/index.html) 和周志明老师的[《深入理解 Java 虚拟机(第 3 版)》](https://book.douban.com/subject/34907497/) (强烈建议阅读多遍!)。 - -- **[Java 内存区域](./docs/java/jvm/memory-area.md)** -- **[JVM 垃圾回收](./docs/java/jvm/jvm-garbage-collection.md)** -- [类文件结构](./docs/java/jvm/class-file-structure.md) -- **[类加载过程](./docs/java/jvm/class-loading-process.md)** -- [类加载器](./docs/java/jvm/classloader.md) -- [【待完成】最重要的 JVM 参数总结(翻译完善了一半)](./docs/java/jvm/jvm-parameters-intro.md) -- [【加餐】大白话带你认识 JVM](./docs/java/jvm/jvm-intro.md) -- [JDK 监控和故障处理工具](./docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md) - -### 新特性 - -- **Java 8**:[Java 8 新特性总结(翻译)](./docs/java/new-features/java8-tutorial-translate.md)、[Java8 常用新特性总结](./docs/java/new-features/java8-common-new-features.md) -- [Java 9 新特性概览](./docs/java/new-features/java9.md) -- [Java 10 新特性概览](./docs/java/new-features/java10.md) -- [Java 11 新特性概览](./docs/java/new-features/java11.md) -- [Java 12 & 13 新特性概览](./docs/java/new-features/java12-13.md) -- [Java 14 & 15 新特性概览](./docs/java/new-features/java14-15.md) -- [Java 16 新特性概览](./docs/java/new-features/java16.md) -- [Java 17 新特性概览](./docs/java/new-features/java17.md) -- [Java 18 新特性概览](./docs/java/new-features/java18.md) -- [Java 19 新特性概览](./docs/java/new-features/java19.md) -- [Java 20 新特性概览](./docs/java/new-features/java20.md) -- [Java 21 新特性概览](./docs/java/new-features/java21.md) -- [Java 22 & 23 新特性概览](./docs/java/new-features/java22-23.md) -- [Java 24 新特性概览](./docs/java/new-features/java24.md) - -## 计算机基础 - -### 操作系统 - -- [操作系统常见知识点&面试题总结(上)](./docs/cs-basics/operating-system/operating-system-basic-questions-01.md) -- [操作系统常见知识点&面试题总结(下)](./docs/cs-basics/operating-system/operating-system-basic-questions-02.md) -- **Linux**: - - [后端程序员必备的 Linux 基础知识总结](./docs/cs-basics/operating-system/linux-intro.md) - - [Shell 编程基础知识总结](./docs/cs-basics/operating-system/shell-intro.md) - -### 网络 - -**知识点/面试题总结**: - -- [计算机网络常见知识点&面试题总结(上)](./docs/cs-basics/network/other-network-questions.md) -- [计算机网络常见知识点&面试题总结(下)](./docs/cs-basics/network/other-network-questions2.md) -- [谢希仁老师的《计算机网络》内容总结(补充)](./docs/cs-basics/network/computer-network-xiexiren-summary.md) - -**重要知识点详解**: - -- [OSI 和 TCP/IP 网络分层模型详解(基础)](./docs/cs-basics/network/osi-and-tcp-ip-model.md) -- [应用层常见协议总结(应用层)](./docs/cs-basics/network/application-layer-protocol.md) -- [HTTP vs HTTPS(应用层)](./docs/cs-basics/network/http-vs-https.md) -- [HTTP 1.0 vs HTTP 1.1(应用层)](./docs/cs-basics/network/http1.0-vs-http1.1.md) -- [HTTP 常见状态码(应用层)](./docs/cs-basics/network/http-status-codes.md) -- [DNS 域名系统详解(应用层)](./docs/cs-basics/network/dns.md) -- [TCP 三次握手和四次挥手(传输层)](./docs/cs-basics/network/tcp-connection-and-disconnection.md) -- [TCP 传输可靠性保障(传输层)](./docs/cs-basics/network/tcp-reliability-guarantee.md) -- [ARP 协议详解(网络层)](./docs/cs-basics/network/arp.md) -- [NAT 协议详解(网络层)](./docs/cs-basics/network/nat.md) -- [网络攻击常见手段总结(安全)](./docs/cs-basics/network/network-attack-means.md) - -### 数据结构 - -**图解数据结构:** - -- [线性数据结构 :数组、链表、栈、队列](./docs/cs-basics/data-structure/linear-data-structure.md) -- [图](./docs/cs-basics/data-structure/graph.md) -- [堆](./docs/cs-basics/data-structure/heap.md) -- [树](./docs/cs-basics/data-structure/tree.md):重点关注[红黑树](./docs/cs-basics/data-structure/red-black-tree.md)、B-,B+,B\*树、LSM 树 - -其他常用数据结构: - -- [布隆过滤器](./docs/cs-basics/data-structure/bloom-filter.md) - -### 算法 - -算法这部分内容非常重要,如果你不知道如何学习算法的话,可以看下我写的: - -- [算法学习书籍+资源推荐](https://www.zhihu.com/question/323359308/answer/1545320858) 。 -- [如何刷 Leetcode?](https://www.zhihu.com/question/31092580/answer/1534887374) - -**常见算法问题总结**: - -- [几道常见的字符串算法题总结](./docs/cs-basics/algorithms/string-algorithm-problems.md) -- [几道常见的链表算法题总结](./docs/cs-basics/algorithms/linkedlist-algorithm-problems.md) -- [剑指 offer 部分编程题](./docs/cs-basics/algorithms/the-sword-refers-to-offer.md) -- [十大经典排序算法](./docs/cs-basics/algorithms/10-classical-sorting-algorithms.md) - -另外,[GeeksforGeeks](https://www.geeksforgeeks.org/fundamentals-of-algorithms/) 这个网站总结了常见的算法 ,比较全面系统。 - -## 数据库 - -### 基础 - -- [数据库基础知识总结](./docs/database/basis.md) -- [NoSQL 基础知识总结](./docs/database/nosql.md) -- [字符集详解](./docs/database/character-set.md) -- SQL : - - [SQL 语法基础知识总结](./docs/database/sql/sql-syntax-summary.md) - - [SQL 常见面试题总结](./docs/database/sql/sql-questions-01.md) - -### MySQL - -**知识点/面试题总结:** - -- **[MySQL 常见知识点&面试题总结](./docs/database/mysql/mysql-questions-01.md)** (必看 :+1:) -- [MySQL 高性能优化规范建议总结](./docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md) - -**重要知识点:** - -- [MySQL 索引详解](./docs/database/mysql/mysql-index.md) -- [MySQL 事务隔离级别图文详解)](./docs/database/mysql/transaction-isolation-level.md) -- [MySQL 三大日志(binlog、redo log 和 undo log)详解](./docs/database/mysql/mysql-logs.md) -- [InnoDB 存储引擎对 MVCC 的实现](./docs/database/mysql/innodb-implementation-of-mvcc.md) -- [SQL 语句在 MySQL 中的执行过程](./docs/database/mysql/how-sql-executed-in-mysql.md) -- [MySQL 查询缓存详解](./docs/database/mysql/mysql-query-cache.md) -- [MySQL 执行计划分析](./docs/database/mysql/mysql-query-execution-plan.md) -- [MySQL 自增主键一定是连续的吗](./docs/database/mysql/mysql-auto-increment-primary-key-continuous.md) -- [MySQL 时间类型数据存储建议](./docs/database/mysql/some-thoughts-on-database-storage-time.md) -- [MySQL 隐式转换造成索引失效](./docs/database/mysql/index-invalidation-caused-by-implicit-conversion.md) - -### Redis - -**知识点/面试题总结** : (必看:+1: ): - -- [Redis 常见知识点&面试题总结(上)](./docs/database/redis/redis-questions-01.md) -- [Redis 常见知识点&面试题总结(下)](./docs/database/redis/redis-questions-02.md) - -**重要知识点:** - -- [3 种常用的缓存读写策略详解](./docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md) -- [Redis 5 种基本数据结构详解](./docs/database/redis/redis-data-structures-01.md) -- [Redis 3 种特殊数据结构详解](./docs/database/redis/redis-data-structures-02.md) -- [Redis 持久化机制详解](./docs/database/redis/redis-persistence.md) -- [Redis 内存碎片详解](./docs/database/redis/redis-memory-fragmentation.md) -- [Redis 常见阻塞原因总结](./docs/database/redis/redis-common-blocking-problems-summary.md) -- [Redis 集群详解](./docs/database/redis/redis-cluster.md) - -### MongoDB - -- [MongoDB 常见知识点&面试题总结(上)](./docs/database/mongodb/mongodb-questions-01.md) -- [MongoDB 常见知识点&面试题总结(下)](./docs/database/mongodb/mongodb-questions-02.md) - -## 搜索引擎 - -[Elasticsearch 常见面试题总结(付费)](./docs/database/elasticsearch/elasticsearch-questions-01.md) - -![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) - -## 开发工具 - -### Maven - -- [Maven 核心概念总结](./docs/tools/maven/maven-core-concepts.md) -- [Maven 最佳实践](./docs/tools/maven/maven-best-practices.md) - -### Gradle - -[Gradle 核心概念总结](./docs/tools/gradle/gradle-core-concepts.md)(可选,目前国内还是使用 Maven 普遍一些) - -### Docker - -- [Docker 核心概念总结](./docs/tools/docker/docker-intro.md) -- [Docker 实战](./docs/tools/docker/docker-in-action.md) - -### Git - -- [Git 核心概念总结](./docs/tools/git/git-intro.md) -- [GitHub 实用小技巧总结](./docs/tools/git/github-tips.md) - -## 系统设计 - -- [系统设计常见面试题总结](./docs/system-design/system-design-questions.md) -- [设计模式常见面试题总结](./docs/system-design/design-pattern.md) - -### 基础 - -- [RestFul API 简明教程](./docs/system-design/basis/RESTfulAPI.md) -- [软件工程简明教程简明教程](./docs/system-design/basis/software-engineering.md) -- [代码命名指南](./docs/system-design/basis/naming.md) -- [代码重构指南](./docs/system-design/basis/refactoring.md) -- [单元测试指南](./docs/system-design/basis/unit-test.md) - -### 常用框架 - -#### Spring/SpringBoot (必看 :+1:) - -**知识点/面试题总结** : - -- [Spring 常见知识点&面试题总结](./docs/system-design/framework/spring/spring-knowledge-and-questions-summary.md) -- [SpringBoot 常见知识点&面试题总结](./docs/system-design/framework/spring/springboot-knowledge-and-questions-summary.md) -- [Spring/Spring Boot 常用注解总结](./docs/system-design/framework/spring/spring-common-annotations.md) -- [SpringBoot 入门指南](https://github.com/Snailclimb/springboot-guide) - -**重要知识点详解**: - -- [IoC & AOP详解(快速搞懂)](./docs/system-design/framework/spring/ioc-and-aop.md) -- [Spring 事务详解](./docs/system-design/framework/spring/spring-transaction.md) -- [Spring 中的设计模式详解](./docs/system-design/framework/spring/spring-design-patterns-summary.md) -- [SpringBoot 自动装配原理详解](./docs/system-design/framework/spring/spring-boot-auto-assembly-principles.md) - -#### MyBatis - -[MyBatis 常见面试题总结](./docs/system-design/framework/mybatis/mybatis-interview.md) - -### 安全 - -#### 认证授权 - -- [认证授权基础概念详解](./docs/system-design/security/basis-of-authority-certification.md) -- [JWT 基础概念详解](./docs/system-design/security/jwt-intro.md) -- [JWT 优缺点分析以及常见问题解决方案](./docs/system-design/security/advantages-and-disadvantages-of-jwt.md) -- [SSO 单点登录详解](./docs/system-design/security/sso-intro.md) -- [权限系统设计详解](./docs/system-design/security/design-of-authority-system.md) -- [常见加密算法总结](./docs/system-design/security/encryption-algorithms.md) - -#### 数据脱敏 - -数据脱敏说的就是我们根据特定的规则对敏感信息数据进行变形,比如我们把手机号、身份证号某些位数使用 \* 来代替。 - -#### 敏感词过滤 - -[敏感词过滤方案总结](./docs/system-design/security/sentive-words-filter.md) - -### 定时任务 - -[Java 定时任务详解](./docs/system-design/schedule-task.md) - -### Web 实时消息推送 - -[Web 实时消息推送详解](./docs/system-design/web-real-time-message-push.md) - -## 分布式 - -### 理论&算法&协议 - -- [CAP 理论和 BASE 理论解读](https://javaguide.cn/distributed-system/protocol/cap-and-base-theorem.html) -- [Paxos 算法解读](https://javaguide.cn/distributed-system/protocol/paxos-algorithm.html) -- [Raft 算法解读](https://javaguide.cn/distributed-system/protocol/raft-algorithm.html) -- [Gossip 协议详解](https://javaguide.cn/distributed-system/protocol/gossip-protocl.html) - -### RPC - -- [RPC 基础知识总结](https://javaguide.cn/distributed-system/rpc/rpc-intro.html) -- [Dubbo 常见知识点&面试题总结](https://javaguide.cn/distributed-system/rpc/dubbo.html) - -### ZooKeeper - -> 这两篇文章可能有内容重合部分,推荐都看一遍。 - -- [ZooKeeper 相关概念总结(入门)](https://javaguide.cn/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.html) -- [ZooKeeper 相关概念总结(进阶)](https://javaguide.cn/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.html) - -### API 网关 - -- [API 网关基础知识总结](https://javaguide.cn/distributed-system/api-gateway.html) -- [Spring Cloud Gateway 常见知识点&面试题总结](./docs/distributed-system/spring-cloud-gateway-questions.md) +--- -### 分布式 ID +## 🧭 Project Related -- [分布式ID介绍&实现方案总结](https://javaguide.cn/distributed-system/distributed-id.html) -- [分布式 ID 设计指南](https://javaguide.cn/distributed-system/distributed-id-design.html) +- [Project Introduction](https://javaguide.cn/javaguide/intro.html) +- [Usage Suggestions](https://javaguide.cn/javaguide/use-suggestion.html) +- [Contribution Guidelines](https://javaguide.cn/javaguide/contribution-guideline.html) +- [Frequently Asked Questions](https://javaguide.cn/javaguide/faq.html) -### 分布式锁 +--- -- [分布式锁介绍](https://javaguide.cn/distributed-system/distributed-lock.html) -- [分布式锁常见实现方案总结](https://javaguide.cn/distributed-system/distributed-lock-implementations.html) +## ☕ Java -### 分布式事务 +### Basics -[分布式事务常见知识点&面试题总结](https://javaguide.cn/distributed-system/distributed-transaction.html) +**Knowledge Points/Interview Questions Summary (Must-read 👍):** -### 分布式配置中心 +- [Part 1](./docs/java/basis/java-basic-questions-01.md) +- [Part 2](./docs/java/basis/java-basic-questions-02.md) +- [Part 3](./docs/java/basis/java-basic-questions-03.md) -[分布式配置中心常见知识点&面试题总结](./docs/distributed-system/distributed-configuration-center.md) +**Important Concepts Explained:** -## 高性能 +- [Why is there only value passing in Java?](./docs/java/basis/why-there-only-value-passing-in-java.md) +- [Java Serialization](./docs/java/basis/serialization.md) +- [Generics & Wildcards](./docs/java/basis/generics-and-wildcards.md) +- [Reflection](./docs/java/basis/reflection.md) +- [Proxy Pattern](./docs/java/basis/proxy.md) +- [BigDecimal](./docs/java/basis/bigdecimal.md) +- [Unsafe Class](./docs/java/basis/unsafe.md) +- [SPI Mechanism](./docs/java/basis/spi.md) +- [Syntactic Sugar](./docs/java/basis/syntactic-sugar.md) -### 数据库优化 +--- -- [数据库读写分离和分库分表](./docs/high-performance/read-and-write-separation-and-library-subtable.md) -- [数据冷热分离](./docs/high-performance/data-cold-hot-separation.md) -- [常见 SQL 优化手段总结](./docs/high-performance/sql-optimization.md) -- [深度分页介绍及优化建议](./docs/high-performance/deep-pagination-optimization.md) +### Collections -### 负载均衡 +**Knowledge Points/Interview Questions Summary:** -[负载均衡常见知识点&面试题总结](./docs/high-performance/load-balancing.md) +- [Summary of Common Knowledge Points & Interview Questions in Java Collections (Part 1)](./docs/java/collection/java-collection-questions-01.md) +- [Summary of Common Knowledge Points & Interview Questions in Java Collections (Part 2)](./docs/java/collection/java-collection-questions-02.md) +- [Summary of Common Knowledge Points & Interview Questions in Java Collections (Part 3)](./docs/java/collection/java-collection-questions-03.md) -### CDN +**Important Concepts Explained:** -[CDN(内容分发网络)常见知识点&面试题总结](./docs/high-performance/cdn.md) +- [Detailed Explanation of HashMap](./docs/java/collection/hashmap.md) +- [ArrayList vs LinkedList](./docs/java/collection/arraylist-vs-linkedlist.md) +- [Concurrent Collections (ConcurrentHashMap, CopyOnWriteArrayList, etc.)](./docs/java/collection/concurrent-collections.md) +- [Set, List, and Map Differences](./docs/java/collection/set-list-map-differences.md) +- [Fail-Fast vs Fail-Safe](./docs/java/collection/fail-fast-vs-fail-safe.md) +- [Iterator vs Iterable](./docs/java/collection/iterator-vs-iterable.md) -### 消息队列 +--- -- [消息队列基础知识总结](./docs/high-performance/message-queue/message-queue.md) -- [Disruptor 常见知识点&面试题总结](./docs/high-performance/message-queue/disruptor-questions.md) -- [RabbitMQ 常见知识点&面试题总结](./docs/high-performance/message-queue/rabbitmq-questions.md) -- [RocketMQ 常见知识点&面试题总结](./docs/high-performance/message-queue/rocketmq-questions.md) -- [Kafka 常见知识点&面试题总结](./docs/high-performance/message-queue/kafka-questions-01.md) +## 🧵 Multithreading & Concurrency -## 高可用 +- [Java Thread Basics](./docs/java/concurrent/java-thread.md) +- [Thread Safety Concepts](./docs/java/concurrent/thread-safety.md) +- [ThreadPoolExecutor Details](./docs/java/concurrent/thread-pool.md) +- [Volatile Keyword Explained](./docs/java/concurrent/volatile.md) +- [Synchronized vs Lock](./docs/java/concurrent/synchronized-vs-lock.md) -[高可用系统设计指南](./docs/high-availability/high-availability-system-design.md) +--- -### 冗余设计 +## 🔧 JVM -[冗余设计详解](./docs/high-availability/redundancy.md) +- [JVM Architecture](./docs/java/jvm/jvm-architecture.md) +- [Memory Model & GC](./docs/java/jvm/java-memory-area.md) +- [Garbage Collection Algorithms](./docs/java/jvm/gc.md) +- [Class Loading Process](./docs/java/jvm/class-loader.md) -### 限流 +--- -[服务限流详解](./docs/high-availability/limit-request.md) +## 🌱 Spring & Frameworks -### 降级&熔断 +- [Spring Overview](./docs/system-design/framework/spring.md) +- [Spring MVC](./docs/system-design/framework/springmvc.md) +- [Spring Boot](./docs/system-design/framework/springboot.md) +- [Spring Cloud](./docs/system-design/framework/springcloud.md) -[降级&熔断详解](./docs/high-availability/fallback-and-circuit-breaker.md) +--- -### 超时&重试 +## 🗃️ Database -[超时&重试详解](./docs/high-availability/timeout-and-retry.md) +- [MySQL Optimization](./docs/database/mysql/mysql-index.md) +- [SQL Performance Tips](./docs/database/mysql/mysql-performance-tuning.md) +- [Redis Basics](./docs/database/redis/redis-intro.md) +- [Redis Data Types](./docs/database/redis/data-structure.md) -### 集群 +--- -相同的服务部署多份,避免单点故障。 +## 📦 System Design -### 灾备设计和异地多活 +- [CAP Theorem](./docs/system-design/theory/cap.md) +- [High Availability & Scalability](./docs/system-design/design/high-availability.md) +- [Distributed Transactions](./docs/system-design/design/distributed-transaction.md) +- [Design Patterns](./docs/system-design/design-patterns/README.md) -**灾备** = 容灾 + 备份。 +--- -- **备份**:将系统所产生的的所有重要数据多备份几份。 -- **容灾**:在异地建立两个完全相同的系统。当某个地方的系统突然挂掉,整个应用系统可以切换到另一个,这样系统就可以正常提供服务了。 +## ✅ Contributing -**异地多活** 描述的是将服务部署在异地并且服务同时对外提供服务。和传统的灾备设计的最主要区别在于“多活”,即所有站点都是同时在对外提供服务的。异地多活是为了应对突发状况比如火灾、地震等自然或者人为灾害。 +We welcome contributions! Please refer to the [Contribution Guidelines](https://javaguide.cn/javaguide/contribution-guideline.html) to get started. -## Star 趋势 +--- -![Stars](https://api.star-history.com/svg?repos=Snailclimb/JavaGuide&type=Date) +## ⭐ Star History -## 公众号 +Thanks to all who have starred ⭐ and shared 💬 this project! Your support keeps JavaGuide growing! -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 +--- -![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) +If you found this helpful, don’t forget to star 🌟 [JavaGuide on GitHub](https://github.com/Snailclimb/JavaGuide)! - diff --git a/docs/README.md b/docs/README.md index ed6cbec001c..05ea0f684b5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,40 +1,40 @@ --- home: true icon: home -title: Java 面试指南 +title: Java Interview Guide heroImage: /logo.svg heroText: JavaGuide -tagline: 「Java学习 + 面试指南」涵盖 Java 程序员需要掌握的核心知识 +tagline: "Java Learning + Interview Guide" covers the core knowledge that Java programmers need to master. actions: - - text: 开始阅读 + - text: Start Reading link: /home.md type: primary - - text: 知识星球 + - text: Knowledge Planet link: /about-the-author/zhishixingqiu-two-years.md type: default footer: |- - 鄂ICP备2020015769号-1 | 主题: VuePress Theme Hope + EIC Record No. 鄂ICP备2020015769号-1 | Theme: VuePress Theme Hope --- -## 关于网站 +## About the Website -JavaGuide 已经持续维护 6 年多了,累计提交了 **5600+** commit ,共有 **550+** 多位贡献者共同参与维护和完善。真心希望能够把这个项目做好,真正能够帮助到有需要的朋友! +JavaGuide has been maintained for over 6 years, with a total of **5600+** commits and more than **550+** contributors participating in its maintenance and improvement. I sincerely hope to make this project better and truly help those in need! -如果觉得 JavaGuide 的内容对你有帮助的话,还请点个免费的 Star(绝不强制点 Star,觉得内容不错有收获再点赞就好),这是对我最大的鼓励,感谢各位一路同行,共勉!传送门:[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)。 +If you find the content of JavaGuide helpful, please give it a free Star (it's not mandatory to star, just do it if you find the content good and rewarding). This is the greatest encouragement for me. Thank you all for your support! Links: [GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide). -- [项目介绍](./javaguide/intro.md) -- [贡献指南](./javaguide/contribution-guideline.md) -- [常见问题](./javaguide/faq.md) +- [Project Introduction](./javaguide/intro.md) +- [Contribution Guidelines](./javaguide/contribution-guideline.md) +- [Frequently Asked Questions](./javaguide/faq.md) -## 关于作者 +## About the Author -- [我曾经也是网瘾少年](./about-the-author/internet-addiction-teenager.md) -- [害,毕业三年了!](./about-the-author/my-college-life.md) -- [我的知识星球快 3 岁了!](./about-the-author/zhishixingqiu-two-years.md) -- [坚持写技术博客六年了](./about-the-author/writing-technology-blog-six-years.md) +- [I Used to Be a Teenager Addicted to the Internet](./about-the-author/internet-addiction-teenager.md) +- [Oh, It's Been Three Years Since Graduation!](./about-the-author/my-college-life.md) +- [My Knowledge Planet is Almost 3 Years Old!](./about-the-author/zhishixingqiu-two-years.md) +- [I've Been Writing Technical Blogs for Six Years](./about-the-author/writing-technology-blog-six-years.md) -## 公众号 +## Public Account -最新更新会第一时间同步在公众号,推荐关注!另外,公众号上有很多干货不会同步在线阅读网站。 +The latest updates will be synchronized to the public account in real-time, so it's recommended to follow! Additionally, there are many valuable resources on the public account that will not be synchronized to the online reading website. -![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) +![JavaGuide Official Public Account](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) diff --git a/docs/about-the-author/README.md b/docs/about-the-author/README.md index 12f6eab7f3f..0351283f1e2 100644 --- a/docs/about-the-author/README.md +++ b/docs/about-the-author/README.md @@ -1,49 +1,49 @@ --- -title: 个人介绍 Q&A -category: 走近作者 +title: Personal Introduction Q&A +category: Getting to Know the Author --- -这篇文章我会通过 Q&A 的形式简单介绍一下我自己。 +In this article, I will briefly introduce myself in a Q&A format. -## 我是什么时候毕业的? +## When did I graduate? -很多老读者应该比较清楚,我是 19 年本科毕业的,刚毕业就去了某家外企“养老”。 +Many long-time readers should be familiar with the fact that I graduated in 2019 and immediately went to work at a foreign company. -我的学校背景是比较差的,高考失利,勉强过了一本线 20 来分,去了荆州的一所很普通的双非一本。不过,还好我没有因为学校而放弃自己,反倒是比身边的同学都要更努力,整个大学还算过的比较充实。 +My school background is not great; I barely passed the college entrance examination with about 20 points above the cutoff for the first-tier universities, and I went to a very ordinary non-project first-tier university in Jingzhou. However, I am glad that I did not give up on myself because of my school. Instead, I worked harder than my classmates and had a relatively fulfilling university life. -下面这张是当时拍的毕业照(后排最中间的就是我): +The photo below is from my graduation ceremony (I'm in the middle of the back row): ![](https://oss.javaguide.cn/javaguide/%E4%B8%AA%E4%BA%BA%E4%BB%8B%E7%BB%8D.png) -## 我坚持写了多久博客? +## How long have I been blogging? -时间真快啊!我自己是从大二开始写博客的。那时候就是随意地在博客平台上发发自己的学习笔记和自己写的程序。就比如 [谢希仁老师的《计算机网络》内容总结](../cs-basics/network/computer-network-xiexiren-summary.md) 这篇文章就是我在大二学习计算机网络这门课的时候对照着教材总结的。 +Time flies! I started writing my blog in my sophomore year. At that time, I casually shared my study notes and programs I wrote on a blogging platform. For example, [Professor Xie Xiren's Summary of "Computer Networks"](../cs-basics/network/computer-network-xiexiren-summary.md) is an article I summed up while studying the computer networks course in my sophomore year by referring to the textbook. -身边也有很多小伙伴经常问我:“我现在写博客还晚么?” +Many of my friends often ask me, "Is it too late for me to start blogging now?" -我觉得哈!如果你想做什么事情,尽量少问迟不迟,多问自己值不值得,只要你觉得有意义,就尽快开始做吧!人生很奇妙,我们每一步的重大决定,都会对自己未来的人生轨迹产生影响。是好还是坏,也只有我们自己知道了! +I think! If you want to do something, try to ask yourself if it’s worth it rather than if it's too late. As long as you feel it's meaningful, just start doing it as soon as possible! Life is wonderful, and every significant decision we make will affect our future trajectories. Whether it’s good or bad, only we ourselves will know! -对我自己来说,坚持写博客这一项决定对我人生轨迹产生的影响是非常正面的!所以,我也推荐大家养成坚持写博客的习惯。 +For me personally, the decision to stick to blogging has had a very positive impact on my life trajectory! Therefore, I also recommend everyone to develop the habit of consistently writing blogs. -## 我在大学期间赚了多少钱? +## How much money did I earn during college? -在校期间,我还通过办培训班、接私活、技术培训、编程竞赛等方式变现 20w+,成功实现“经济独立”。我用自己赚的钱去了重庆、三亚、恩施、青岛等地旅游,还给家里补贴了很多,减轻了父母的负担。 +During my time at school, I managed to make over 200,000 through running tutoring sessions, taking freelance jobs, technical training, and programming competitions, achieving "economic independence." I used the money I earned to travel to places like Chongqing, Sanya, Enshi, and Qingdao, and I also contributed a lot to my family to ease my parents' burden. -下面这张是我大一下学期办补习班的时候拍的(离开前的最后一顿饭): +The photo below was taken when I was running a tutoring class in my first year of college (the last meal before leaving): -![补习班的最后一顿晚餐](https://oss.javaguide.cn/p3-juejin/f36bfd719b9b4463b2f1d3edc51faa97~tplv-k3u1fbpfcp-zoom-1.jpeg) +![The Last Dinner of the Tutoring Class](https://oss.javaguide.cn/p3-juejin/f36bfd719b9b4463b2f1d3edc51faa97~tplv-k3u1fbpfcp-zoom-1.jpeg) -下面这张是我大三去三亚的时候拍的: +The photo below was taken when I went to Sanya in my junior year: ![](https://oss.javaguide.cn/javaguide/psc.jpeg) -其实,我在大学就这么努力地开始赚钱,也主要是因为家庭条件太一般,父母赚钱都太辛苦了!也正是因为我自己迫切地想要减轻父母的负担,所以才会去尝试这么多赚钱的方法。 +Actually, I started working so hard to earn money in college mainly because my family conditions were quite average, and my parents worked very hard to earn a living! It was precisely because I desperately wanted to lessen my parents' burden that I tried so many ways to earn money. -我发现做咱们程序员这行的,很多人的家庭条件都挺一般的,选择这个行业的很大原因不是因为自己喜欢,而是为了多赚点钱。 +I have noticed that many people in our programming field come from average families, and a significant reason for choosing this industry is not because they like it, but to earn more money. -如果你也想通过接私活变现的话,可以在我的公众号后台回复“**接私活**”来了解一些我的个人经验分享。 +If you also want to earn money through freelance jobs, you can reply "**Freelance**" in the backend of my WeChat public account to learn some personal experience I share. ::: center @@ -51,20 +51,20 @@ category: 走近作者 ::: -## 为什么自称 Guide? +## Why do I call myself Guide? -可能是因为我的项目名字叫做 JavaGuide , 所以导致有很多人称呼我为 **Guide 哥**。 +Perhaps it's because my project is named JavaGuide, which has led many people to call me **Guide Brother**. -后面,为了读者更方便称呼,我就将自己的笔名改成了 **Guide**。 +Later, to make it easier for readers to address me, I changed my pen name to **Guide**. -我早期写文章用的笔名是 SnailClimb 。很多人不知道这个名字是啥意思,给大家拆解一下就清楚了。SnailClimb=Snail(蜗牛)+Climb(攀登)。我从小就非常喜欢听周杰伦的歌曲,特别是他的《蜗牛》🐌 这首歌曲,另外,当年我高考发挥的算是比较失常,上了大学之后还算是比较“奋青”,所以,我就给自己起的笔名叫做 SnailClimb ,寓意自己要不断向上攀登,嘿嘿 😁 +My early pen name was SnailClimb. Many people do not know what this name means, so let me explain it. SnailClimb = Snail (蜗牛) + Climb (攀登). I have loved listening to Jay Chou's songs since I was young, especially his song "Snail" 🐌. Furthermore, I did not perform well in the college entrance examination, and after entering university, I was relatively “ambitious,” so I named myself SnailClimb, meaning I want to keep climbing upwards. Hehe 😁 ![](https://oss.javaguide.cn/p3-juejin/37599546f3b34b92a32db579a225aa45~tplv-k3u1fbpfcp-watermark.png) -## 后记 +## Postscript -凡心所向,素履所往,生如逆旅,一苇以航。 +Wherever the heart desires, the feet must follow; life is like a journey against the current, where we sail with a single reed. -生活本就是有苦有甜。共勉! +Life indeed has its bittersweet moments. Let’s encourage each other! -![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) +![JavaGuide Official WeChat Account](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) diff --git a/docs/about-the-author/dog-that-copies-other-people-essay.md b/docs/about-the-author/dog-that-copies-other-people-essay.md index 653b616eaab..73626ded12c 100644 --- a/docs/about-the-author/dog-that-copies-other-people-essay.md +++ b/docs/about-the-author/dog-that-copies-other-people-essay.md @@ -1,56 +1,56 @@ --- -title: 抄袭狗,你冬天睡觉脚必冷!!! -category: 走近作者 +title: Plagiarists, your feet will definitely be cold when you sleep in winter!!! +category: Meet the Author tag: - - 杂谈 + - Miscellaneous --- -抄袭狗真的太烦了。。。 +Plagiarists are really annoying... -听朋友说我的文章在知乎又被盗了,原封不动地被别人用来引流。 +I heard from friends that my article was stolen on Zhihu again, used by someone else to drive traffic without any modifications. ![](https://oss.javaguide.cn/p3-juejin/39f223bd8d8240b8b7328f7ab6edbc57~tplv-k3u1fbpfcp-zoom-1.png) -而且!!!这还不是最气的。 +And!!! This is not even the worst part. -这人还在文末注明的原出处还不是我的。。。 +The person even cited an original source at the end of the article, but it wasn't mine... ![](https://oss.javaguide.cn/p3-juejin/fa47e0752f4b4b57af424114bc6bc558~tplv-k3u1fbpfcp-zoom-1.png) -也就是说 CSDN 有另外一位抄袭狗盗了我的这篇文章并声明了原创,知乎抄袭狗又原封不动地搬运了这位 CSDN 抄袭狗的文章。 +This means that some other plagiarist on CSDN stole my article and claimed it as original, and the plagiarist on Zhihu directly copied this CSDN plagiarist's article. -真可谓离谱他妈给离谱开门,离谱到家了。 +It's truly outrageous, like opening the door to absurdity, it's absurd to the extreme. ![](https://oss.javaguide.cn/p3-juejin/6f8d281579224b13ad235c28e1d7790e~tplv-k3u1fbpfcp-zoom-1.png) -我打开知乎抄袭狗注明的原出处链接,好家伙,一模一样的内容,还表明了原创。 +I opened the original source link indicated by the Zhihu plagiarist, and wow, the content is identical, and it claims originality. ![](https://oss.javaguide.cn/p3-juejin/6a6d7b206b6a43ec9b0055a8f47a30be~tplv-k3u1fbpfcp-zoom-1.png) -看了一下 CSDN 这位抄袭狗的文章,好家伙,把我高赞回答搬运了一个遍。。。真是很勤奋了。。。 +After looking at this CSDN plagiarist's article, wow, they have copied my highly upvoted answer in its entirety... Truly diligent... -CSDN 我就不想多说了,就一大型文章垃圾场,都是各种不规范转载,各种收费下载的垃圾资源。这号称国内流量最大的技术网站贼恶心,吃香太难看,能不用就不要用吧! +I don't want to say much about CSDN; it's just a large garbage dump of articles filled with various unregulated reprints and all kinds of text that require payment to download. This site, which claims to be the largest tech site in the country, is disgusting, very unpleasant to deal with, so it's better not to use it! -像我自己平时用 Google 搜索的时候,都是直接屏蔽掉 CSDN 这个站点的。只需要下载一个叫做 Personal Blocklist 的 Chrome 插件,然后将 blog.csdn.net 添加进黑名单就可以了。 +When I usually search on Google, I directly block the CSDN site. You just need to download a Chrome extension called Personal Blocklist and add blog.csdn.net to your blacklist. ![](https://oss.javaguide.cn/p3-juejin/be151d93cd024c6e911d1a694212d91c~tplv-k3u1fbpfcp-zoom-1.png) -我的文章基本被盗完了,关键是我自己发没有什么流量,反而是盗我文章的那些人比我这个原作者流量还大。 +Basically, my article has been completely stolen, and the key is that I don't get much traffic from my own posts, while those who steal my articles have more traffic than I do as the original author. -这是什么世道,是人性的扭曲还是道德的沦丧? +What kind of world is this? Is it a distortion of human nature or a decline in morality? -不过,也没啥,CSDN 这垃圾网站不去发文也无妨。 +But, it's okay, it's not a problem if I don't publish on this garbage site CSDN. -看看 CSDN 热榜上的文章都是一些什么垃圾,不是各种广告就是一些毫无质量的拼凑文。 +If you look at the articles on the hot list of CSDN, you will see that they are all garbage; they are either various ads or nonsensical patchwork articles. ![](https://oss.javaguide.cn/p3-juejin/cd07efe86af74ea0a07d29236718ddc8~tplv-k3u1fbpfcp-zoom-1-20230717155426403.png) -当然了,也有极少部分的高质量文章,比如涛哥、二哥、冰河、微观技术等博主的文章。 +Of course, there are a very small number of high-quality articles, such as those by bloggers like Tao Ge, Er Ge, Bing He, and Micro Technology. -还有很多视频平台(比如抖音、哔哩哔哩)上面有很多博主直接把别人的原创拿来做个视频,用来引流或者吸粉。 +There are also many bloggers on various video platforms (like Douyin and Bilibili) who directly use others' original content to make videos for traffic or followers. -今天提到的这篇被盗的文章曾经就被一个培训机构拿去做成了视频用来引流。 +The article that has been stolen today was once taken by a training institution and turned into a video for traffic purposes. ![](https://oss.javaguide.cn/p3-juejin/9dda1e36ceff4cbb9b0bf9501b279be5~tplv-k3u1fbpfcp-zoom-1.png) -作为个体,咱也没啥办法,只能遇到一个举报一个。。。 +As an individual, I have no way to stop it, I can only report them one by one... diff --git a/docs/about-the-author/feelings-after-one-month-of-induction-training.md b/docs/about-the-author/feelings-after-one-month-of-induction-training.md index ed57578a907..2454d8a27a3 100644 --- a/docs/about-the-author/feelings-after-one-month-of-induction-training.md +++ b/docs/about-the-author/feelings-after-one-month-of-induction-training.md @@ -1,24 +1,24 @@ --- -title: 入职培训一个月后的感受 -category: 走近作者 +title: Reflections One Month After Joining +category: Close to the Author tag: - - 个人经历 + - Personal Experience --- -不知不觉已经入职一个多月了,在入职之前我没有在某个公司实习过或者工作过,所以很多东西刚入职工作的我来说还是比较新颖的。学校到职场的转变,带来了角色的转变,其中的差别因人而异。对我而言,在学校的时候课堂上老师课堂上教的东西,自己会根据自己的兴趣选择性接受,甚至很多课程你不想去上的话,还可以逃掉。到了公司就不一样了,公司要求你会的技能你不得不学,除非你不想干了。在学校的时候大部分人编程的目的都是为了通过考试或者找到一份好工作,真正靠自己兴趣支撑起来的很少,到了工作岗位之后我们编程更多的是因为工作的要求,相比于学校的来说会一般会更有挑战而且压力更大。在学校的时候,我们最重要的就是对自己负责,我们不断学习知识去武装自己,但是到了公司之后我们不光要对自己负责,更要对公司负责,毕竟公司出钱请你过来,不是让你一直 on beach 的。 +It has been over a month since I joined the company. Before starting my job, I had never interned or worked at any company, so many things felt quite novel to me. The transition from school to the workplace brought about a shift in roles, which varies from person to person. For me, in school, the things taught by the teacher in class could be selectively absorbed based on my interests. In fact, if there were courses I didn't want to attend, I could even skip them. However, things are different at the company. The skills that the company requires you to have are necessary for you to learn, unless you don't want to continue. In school, most people's programming goal was either to pass exams or to land a good job; very few were genuinely driven by their interests. Once we entered the workforce, we found that we program more out of job requirements, which is generally more challenging and comes with greater pressure. In school, we were predominantly responsible for ourselves; we kept learning to empower ourselves. However, once we joined the company, we are not only responsible for ourselves but also owe responsibility to the company, as they are paying us to work, not to be idle. -刚来公司的时候,因为公司要求,我换上了 Mac 电脑。由于之前一直用的是 Windows 系统,所以非常不习惯。刚开始用 Mac 系统的时候笨手笨脚,自己会很明显的感觉自己的编程效率降低了至少 3 成。当时内心还是挺不爽的,心里也总是抱怨为什么不直接用 Windows 系统或者 Linux 系统。不过也挺奇怪,大概一个星期之后,自己就开始慢慢适应使用 Mac 进行编程,甚至非常喜欢。我这里不想对比 Mac 和 Windows 编程体验哪一个更好,我觉得还是因人而异,相同价位的 Mac 的配置相比于 Windows 确实要被甩几条街。不过 Mac 的编程和使用体验确实不错,当然你也可以选择使用 Linux 进行日常开发,相信一定很不错。 另外,Mac 不能玩一些主流网络游戏,对于一些克制不住自己想玩游戏的朋友是一个不错的选择。 +When I first arrived at the company, I switched to a Mac computer as required. Since I had always used Windows, I found it very uncomfortable. At the beginning, I was clumsy using the Mac system and could clearly feel that my programming efficiency dropped by at least 30%. I was quite frustrated and often complained internally about why we couldn't just use Windows or Linux. However, after about a week, I slowly began to adapt to programming on a Mac and even grew to really like it. I don't want to compare which is better between Mac and Windows for programming, as it really depends on the individual. The specifications of a Mac at the same price point are indeed less powerful compared to Windows. However, the programming and usage experience on Mac is genuinely good; of course, you can also choose to use Linux for daily development, which I believe is also quite nice. Additionally, Mac doesn't support some mainstream online games, which can be a good choice for those who can't resist the urge to play games. -不得不说 ThoughtWorks 的培训机制还是很不错的。应届生入职之后一般都会安排培训,与往年不同的是,今年的培训多了中国本地班(TWU-C)。作为本地班的第一期学员,说句心里话还是很不错。8 周的培训,除了工作需要用到的基本技术比如 ES6、SpringBoot 等等之外,还会增加一些新员工基本技能的培训比如如何高效开会、如何给别人正确的提 Feedback、如何对代码进行重构、如何进行 TDD 等等。培训期间不定期的有活动,比如 Weekend Trip、 City Tour、Cake time 等等。最后三周还会有一个实际的模拟项目,这个项目基本和我们正式工作的实际项目差不多,我个人感觉很不错。目前这个项目已经正式完成了一个迭代,我觉得在做项目的过程中,收获最大的不是项目中使用的技术,而是如何进行团队合作、如何正确使用 Git 团队协同开发、一个完成的迭代是什么样子的、做项目的过程中可能遇到那些问题、一个项目运作的完整流程等等。 +I have to say that ThoughtWorks has a great training mechanism. Generally, new graduates receive training after joining, and unlike past years, this year there is a local class (TWU-C). As a participant in the first local class, I can honestly say that it has been quite good. The 8-week training includes essential skills needed for work, such as ES6, SpringBoot, etc., as well as training on fundamental skills for new employees, like how to run efficient meetings, how to give proper feedback, how to refactor code, and how to do TDD. During the training, there are also various activities, such as Weekend Trips, City Tours, and Cake time. In the last three weeks, we also had a simulated project that closely resembled actual projects we would be working on. Personally, I felt it was quite excellent. This project has already officially completed one iteration, and I believe that the most significant gains from working on the project were not the technologies used but rather understanding teamwork, how to properly use Git for collaborative development, what a completed iteration looks like, potential problems that may arise during the project, and the complete workflow of a project, etc. -ThoughtWorks 非常提倡分享、提倡帮助他人成长,这一点在公司的这段时间深有感触。培训期间,我们每个人会有一个 Trainer 负责,Trainer 就是日常带我们上课和做项目的同事,一个 Trainer 大概会负责 5 - 6 个人。Trainer 不定期都会给我们最近表现的 Feedback (反馈) ,我个人觉得这个并不是这是走走形式,Trainer 们都很负责,很多时候都是在下班之后找我们聊天。同事们也都很热心,如果你遇到问题,向别人询问,其他人如果知道的话一般都会毫无保留的告诉你,如果遇到大部分都不懂的问题,甚至会组织一次技术 Session 分享。上周五我在我们小组内进行了一次关于 Feign 远程调用的技术分享,因为 team 里面大家对这部分知识都不太熟悉,但是后面的项目进展大概率会用到这部分知识。我刚好研究了这部分内容,所以就分享给了组内的其他同事,以便于项目更好的进行。 +ThoughtWorks strongly advocates for sharing and helping others grow, and I've truly felt this during my time at the company. During training, each of us had a Trainer, colleagues who led us in classes and projects, typically responsible for 5-6 people. Trainers periodically provided us with feedback about our recent performance, and I believe these were not mere formalities; the Trainers were very responsible and often sought us out for discussions after work. My colleagues were also very helpful; if you encountered problems and asked others for help, they would usually share their knowledge generously. If a problem arose that few understood, it was common to organize a technical session to share knowledge. Last Friday, I conducted a technical sharing session about Feign remote calls within our group, as everyone was unfamiliar with this topic, but it would likely be necessary for our ongoing project. Since I had researched this area, I shared my findings with my colleagues to aid in the project's progress. -另外,ThoughtWorks 也是一家非常提倡 Feedback (反馈) 文化的公司,反馈是告诉人们我们对他们的表现的看法以及他们应该如何更好地做到这一点。刚开始我并没有太在意,慢慢地自己确实感觉到正确的进行反馈对他人会有很大的帮助。因为人在做很多事情的时候,会很难发现别人很容易看到的一些小问题。就比如一个很有趣的现象一样,假如我们在做项目的时候没有测试这个角色,如果你完成了自己的模块,并且自己对这个模块测试了很多遍,你发现已经没啥问题了。但是,到了实际使用的时候会很大概率出现你之前从来没有注意的问题。解释这个问题的说法是:每个人的视野或多或少都是有盲点的,这与我们的关注点息息相关。对于自己做的东西,很多地方自己测试很多遍都不会发现,但是如果让其他人帮你进行测试的话,就很大可能会发现很多显而易见的问题。 +Moreover, ThoughtWorks is a company that highly promotes a culture of feedback. Feedback is about informing people of our perceptions of their performance and how they can improve. At first, I didn't pay much attention to it, but gradually I realized that providing accurate feedback can be significantly helpful to others. When people are engaged in many tasks, they often have trouble noticing small issues that others can easily see. For instance, an interesting phenomenon occurs during a project without testing certain roles; if you complete your module and have tested it many times without issues, you may think all is well. However, when it comes to actual use, significant problems may arise that you previously overlooked. This issue can be explained by the fact that everyone's perspective has blind spots, which is closely related to our focus. There are many places where you may test your own work repeatedly without noticing any problems, but if others assist in testing, they are likely to identify many apparent issues. ![](https://oss.javaguide.cn/github/about-the-author/feedback.png) -工作之后,平时更新公众号、专栏还有维护 Github 的时间变少了。实际上,很多时候下班回来后,都有自己的时间来干自己的事情,但是自己也总是找工作太累或者时间比较零散的接口来推掉了。到了今天,翻看 Github 突然发现 14 天前别人在 Github 上给我提的 PR 我还没有处理。这一点确实是自己没有做好的地方,没有合理安排好自己的时间。实际上自己有很多想写的东西,后面会慢慢将他们提上日程。工作之后,更加发现下班后的几个小时如何度过确实很重要 ,如果你觉得自己没有完成好自己白天该做的工作的话,下班后你可以继续忙白天没有忙完的工作,如果白天的工作对于你游刃有余的话,下班回来之后,你大可去干自己感兴趣的事情,学习自己感兴趣的技术。做任何事情都要基于自身的基础,切不可好高骛远。 +After starting work, I found that my time for updating my public account, columns, and maintaining GitHub had significantly decreased. In reality, quite often after work, I do have my own time to engage in personal pursuits; however, I often end up making excuses about being too tired from work or that my time is too fragmented. Today, while reviewing GitHub, I suddenly realized that I had not addressed a PR raised by someone else 14 days ago. This was indeed a place where I did not perform well; I hadn't managed my time effectively. I have many things I want to write about, and I will gradually prioritize them. After work, I have realized how important it is to spend the few hours after work effectively. If you feel you haven't completed your work during the day, you can continue working on unfinished tasks after hours. If you find your daytime work manageable, then when you return home, you should certainly engage in things that interest you and learn about technologies you are passionate about. Everything should be based on your own foundation, and one should avoid striving too high without adequate preparation. -工作之后身边也会有很多厉害的人,多从他人身上学习我觉得是每个职场人都应该做的。这一届和我们一起培训的同事中,有一些技术很厉害的,也有一些技术虽然不是那么厉害,但是组织能力以及团队协作能力特别厉害的。有一个特别厉害的同事,在我们还在学 SpringBoot 各种语法的时候,他自己利用业余时间写了一个简化版的 SpringBoot ,涵盖了 Spring 的一些常用注解比如 `@RestController`、`@Autowried`、`@Pathvairable`、`@RestquestParam`等等(已经联系这位同事,想让他开源一下,后面会第一时间同步到公众号,期待一下吧!)。我觉得这位同事对于编程是真的有兴趣,他好像从初中就开始接触编程了,对于各种底层知识也非常感兴趣,自己写过实现过很多比较底层的东西。他的梦想是在 Github 上造一个 20k Star 以上的轮子。我相信以这位同事的能力一定会达成目标的,在这里祝福这位同事,希望他可以尽快实现这个目标。 +After starting work, I also found myself surrounded by many talented individuals, and I believe that learning from others is something every professional should do. Among my training peers, some possess impressive technical skills, while others, although not technically superior, have exceptional organizational and teamwork skills. One particularly remarkable colleague wrote a simplified version of SpringBoot in his spare time while we were still learning various SpringBoot syntax. This version includes some commonly used Spring annotations like `@RestController`, `@Autowired`, `@PathVariable`, and `@RequestParam`, etc. (I have contacted this colleague and hope he will open source it soon; I will synchronize this on the public account as soon as possible—stay tuned!). I genuinely believe this colleague has a real passion for programming; he seems to have started programming in middle school and is very interested in various foundational knowledge, having developed many low-level elements himself. His dream is to create a library with over 20k stars on GitHub. I have no doubt that this colleague will achieve his goal, and I wish him all the best in reaching it soon. -这是我入职一个多月之后的个人感受,很多地方都是一带而过,后面我会抽时间分享自己在公司或者业余学到的比较有用的知识给各位,希望看过的人都能有所收获。 +These are my personal reflections after over a month of employment. I've touched on many aspects rather briefly, and in the future, I will take the time to share useful knowledge I've gained at the company or in my spare time. I hope that everyone who reads this can gain something from it. diff --git a/docs/about-the-author/feelings-of-half-a-year-from-graduation-to-entry.md b/docs/about-the-author/feelings-of-half-a-year-from-graduation-to-entry.md index cc9fe136749..e8fd4bd794e 100644 --- a/docs/about-the-author/feelings-of-half-a-year-from-graduation-to-entry.md +++ b/docs/about-the-author/feelings-of-half-a-year-from-graduation-to-entry.md @@ -1,54 +1,26 @@ --- -title: 从毕业到入职半年的感受 -category: 走近作者 +title: Reflections from Graduation to Six Months into My Job +category: Meet the Author tag: - - 个人经历 + - Personal Experience --- -如果大家看过我之前的介绍的话,就会知道我是 19 年毕业的几百万应届毕业生中的一员。这篇文章主要讲了一下我入职大半年的感受,文中有很多自己的主观感受,如果你们有任何不认同的地方都可以直接在评论区说出来,会很尊重其他人的想法。 +If you have read my previous introduction, you would know that I am one of the millions of graduates from 2019. This article mainly discusses my feelings after working for more than half a year. There are many subjective feelings in this text, and if you disagree with any part, feel free to express your thoughts in the comments; I will respect others' opinions. -简单说一下自己的情况吧!我目前是在一家外企,每天的工作和大部分人一样就是做开发。毕业到现在,差不多也算是工作半年多了,也已经过了公司 6 个月的试用期。目前在公司做过两个偏向于业务方向的项目,其中一个正在做。你很难想象我在公司做的两个业务项目的后端都没有涉及到分布式/微服务,没有接触到 Redis、Kafka 等等比较“高大上”的技术在项目中的实际运用。 +Let me briefly introduce my situation! I am currently working at a foreign company, and like most people, my daily work involves development. Since graduation, I have been working for about six months and have already passed the company's six-month probation period. I have worked on two projects that are more business-oriented, one of which is still ongoing. You might find it hard to believe that the backends of both business projects I worked on did not involve distributed/microservices, nor did I have the opportunity to work with "high-end" technologies like Redis or Kafka in practical applications. -第一个项目做的是公司的内部项目——员工成长系统。抛去员工成长系统这个名字,实际上这个系统做的就是绩效考核比如你在某个项目组的表现。这个项目的技术是 Spring Boot+ JPA + Spring Security + K8S + Docker + React。第二个目前正在做的是一个集成游戏 (cocos)、Web 管理端 (Spring Boot + Vue) 和小程序 (Taro) 项目。 +The first project was an internal project of the company—an employee growth system. Aside from the name, this system is essentially for performance evaluation, such as assessing your performance in a project team. The technologies used in this project are Spring Boot + JPA + Spring Security + K8S + Docker + React. The second project I am currently working on is an integrated project involving a game (Cocos), a web management backend (Spring Boot + Vue), and a mini-program (Taro). -是的,我在工作中的大部分时间都和 CRUD 有关,每天也会写前端页面。之前我认识的一个朋友 ,他听说我做的项目中大部分内容都是写业务代码之后就非常纳闷,他觉得单纯写业务代码得不到提升?what?你一个应届生,连业务代码都写不好你给我说这个!所以,**我就很纳闷不知道为什么现在很多连业务代码都写不好的人为什么人听到 CRUD 就会反感?至少我觉得在我工作这段时间我的代码质量得到了提升、定位问题的能力有了很大的改进、对于业务有了更深的认识,自己也可以独立完成一些前端的开发了。** +Yes, most of my time at work is related to CRUD operations, and I also write frontend pages daily. A friend of mine was quite puzzled when he heard that most of my project work involved writing business code. He thought that simply writing business code wouldn't lead to improvement. What? You, a fresh graduate, can't even write business code well, and you say this! So, **I am quite puzzled as to why many people who can't even write business code feel disgusted when they hear CRUD? At least I feel that during my time at work, my code quality has improved, my problem-solving skills have significantly enhanced, and I have gained a deeper understanding of the business, allowing me to independently complete some frontend development.** -其实,我个人觉得能把业务代码写好也没那么容易,抱怨自己天天做 CRUD 工作之前,看看自己 CRUD 的代码写好没。再换句话说,单纯写 CRUD 的过程中你搞懂了哪些你常用的注解或者类吗?这就像一个只会 `@Service`、`@Autowired`、`@RestController`等等最简单的注解的人说我已经掌握了 Spring Boot 一样。 +In fact, I personally believe that writing good business code is not that easy. Before complaining about doing CRUD work every day, take a look at whether your CRUD code is well-written. In other words, during the process of writing CRUD, did you understand the commonly used annotations or classes? It's like someone who only knows the simplest annotations like `@Service`, `@Autowired`, `@RestController`, etc., claiming they have mastered Spring Boot. -不知道什么时候开始大家都会觉得有实际使用 Redis、MQ 的经验就很牛逼了,这可能和当前的面试环境有关系。你需要和别人有差异,你想进大厂的话,好像就必须要这些技术比较在行,好吧,没有好像,自信点来说对于大部分求职者这些技术都是默认你必备的了。 +I don't know when it started, but everyone seems to think that having practical experience with Redis or MQ is impressive, which may be related to the current interview environment. You need to differentiate yourself from others; if you want to enter a big company, it seems you must be proficient in these technologies. Well, there’s no "seems" about it; confidently speaking, for most job seekers, these technologies are considered essential. -**实话实说,我在大学的时候就陷入过这个“伪命题”中**。在大学的时候,我大二因为加入了一个学校的偏技术方向的校媒才接触到 Java ,当时我们学习 Java 的目的就是开发一个校园通。 大二的时候,编程相当于才入门水平的我才接触 Java,花了一段时间才掌握 Java 基础。然后,就开始学习安卓开发。 +**To be honest, I fell into this "false proposition" during my university years.** In college, I was introduced to Java in my sophomore year after joining a school media organization focused on technology, and our goal was to develop a campus app. At that time, I was just starting to learn Java and spent some time mastering the basics. Then, I began learning Android development. -到了大三上学期,我才真正确定要走 Java 后台的方向,找 Java 后台的开发工作。学习了 3 个月左右的 WEB 开发基础之后,我就开始学习分布式方面内容比如 Redis、Dubbo 这些。我当时是通过看书 + 视频 + 博客的方式学习的,自学过程中通过看视频自己做过两个完整的项目,一个普通的业务系统,一个是分布式的系统。**我当时以为自己做完之后就很牛逼了,我觉得普通的 CRUD 工作已经不符合我当前的水平了。哈哈!现在看来,当时的我过于哈皮!** +By the first semester of my junior year, I had truly decided to pursue a career in Java backend development and started looking for Java backend jobs. After about three months of learning the basics of web development, I began studying distributed technologies like Redis and Dubbo. I learned through books, videos, and blogs, and during my self-study, I completed two full projects: a regular business system and a distributed system. **I thought I was impressive after finishing those projects, and I felt that ordinary CRUD work was beneath my current level. Haha! Looking back now, I realize I was overly optimistic!** -这不!到了大三暑假跟着老师一起做项目的时候就出问题了。大三的时候,我们跟着老师做的是一个绩效考核系统,业务复杂程度中等。这个项目的技术用的是:SSM + Shiro + JSP。当时,做这个项目的时候我遇到各种问题,各种我以为我会写的代码都不会写了,甚至我写一个简单的 CRUD 都要花费好几天的时间。所以,那时候我都是边复习边学习边写代码。虽然很累,但是,那时候学到了很多,也让我在技术面前变得更加踏实。我觉得这“**这个项目已经没有维护的可能性**”这句话是我对我过的这个项目最大的否定了。 +Sure enough! Problems arose when I worked on a project with my teacher during the summer of my junior year. We were working on a performance evaluation system, which had a moderate level of complexity. The technologies used for this project were SSM + Shiro + JSP. During this project, I encountered various issues, and I found that I couldn't write the code I thought I could; even writing a simple CRUD operation took me several days. So, at that time, I was studying, learning, and coding all at once. Although it was exhausting, I learned a lot and became more grounded in my technical skills. I felt that the statement "this project has no possibility of maintenance" was my biggest denial of the project I worked on. -技术千变万化,掌握最核心的才是王道。我们前几年可能还在用 Spring 基于传统的 XML 开发,现在几乎大家都会用 Spring Boot 这个开发利器来提升开发速度,再比如几年前我们使用消息队列可能还在用 ActiveMQ,到今天几乎都没有人用它了,现在比较常用的就是 Rocket MQ、Kafka 。技术更新换代这么快的今天,你是无法把每一个框架/工具都学习一遍的。 - -**很多初学者上来就想通过做项目学习,特别是在公司,我觉得这个是不太可取的。** 如果的 Java 基础或者 Spring Boot 基础不好的话,建议自己先提前学习一下之后再开始看视频或者通过其他方式做项目。 **还有一点就是,我不知道为什么大家都会说边跟着项目边学习做的话效果最好,我觉得这个要加一个前提是你对这门技术有基本的了解或者说你对编程有了一定的了解。** - -**划重点!!!在自己基础没打牢的情况下,单纯跟着视频做一点用没有。你会发现你看完视频之后,让你自己写代码的时候又不会写了。** - -不知道其他公司的程序员是怎么样的?我感觉技术积累很大程度在乎平时,单纯依靠工作绝大部分情况只会加快自己做需求的熟练度,当然,写多了之后或多或少也会提升你对代码质量的认识(前提是你有这个意识)。 - -工作之余,我会利用业余时间来学习自己想学的东西。工作中的例子就是我刚进公司的第一个项目用到了 Spring Security + JWT ,因为当时自己对于这个技术不太了解,然后就在工作之外大概花了一周的时间学习写了一个 Demo 分享了出来,GitHub 地址: 。以次为契机,我还分享了 - -- [《一问带你区分清楚 Authentication、Authorization 以及 Cookie、Session、Token》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485626&idx=1&sn=3247aa9000693dd692de8a04ccffeec1&chksm=cea24771f9d5ce675ea0203633a95b68bfe412dc6a9d05f22d221161147b76161d1b470d54b3&token=684071313&lang=zh_CN&scene=21#wechat_redirect) -- [JWT 身份认证优缺点分析以及常见问题解决方案](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485655&idx=1&sn=583eeeb081ea21a8ec6347c72aa223d6&chksm=cea2471cf9d5ce0aa135f2fb9aa32d98ebb3338292beaccc1aae43d1178b16c0125eb4139ca4&token=1737409938&lang=zh_CN#rd) - -另外一个最近的例子是因为肺炎疫情在家的这段时间,自学了 Kafka,并且正在准备写一系列的入门文章,目前已经完成了: - -1. 大白话 Kafka 入门; -2. Kafka 安装和基本功能体验; -3. Spring Boot 整合 Kafka 发送和接受消息; -4. Spring Boot 整合 Kafka 发送和接受消息的一些事务、错误消息处理等等。 - -还没完成的: - -1. Kafka 高级特性比如工作流程、Kafka 为什么快等等的分析; -2. 源码阅读分析; -3. …… - -**所以,我觉得技术的积累和沉淀很大程度在乎工作之外的时间(大佬和一些本身就特别厉害的除外)。** - -**未来还有很长的路要走,即使再有精力也学不完你想学的所有技术,适当取舍、适当妥协,适当娱乐。** +Technology is ever-changing, and mastering the core aspects is key. A few years ago, we might have been using Spring based on traditional XML development, but now almost everyone uses diff --git a/docs/about-the-author/internet-addiction-teenager.md b/docs/about-the-author/internet-addiction-teenager.md index 78f94e2a483..fa4de71bfbd 100644 --- a/docs/about-the-author/internet-addiction-teenager.md +++ b/docs/about-the-author/internet-addiction-teenager.md @@ -1,152 +1,152 @@ --- -title: 我曾经也是网瘾少年 -category: 走近作者 +title: I Was Once a Teenager Addicted to the Internet +category: Closer to the Author tag: - - 个人经历 + - Personal Experience --- -> 这篇文章写于 2021 年高考前夕。 +> This article was written on the eve of the college entrance examination in 2021. -聊到高考,无数人都似乎有很多话说。今天就假借高考的名义,简单来聊聊我的高中求学经历吧! +When it comes to the college entrance examination, countless people seem to have a lot to say. Today, I’ll borrow the occasion of the examination to briefly talk about my high school experience! -说实话,我自己的高中求学经历真的还不算平淡,甚至有点魔幻,所以还是有很多话想要说的。 +To be honest, my own high school experience was not bland at all; it was even a bit fantastical, so I have a lot to share. -这篇文章大概会从我的初中一直介绍到高中,每一部分我都不会花太多篇幅,就简单聊聊吧! +This article will probably cover my journey from middle school to high school. I won’t spend too much time on each part, just a simple chat! -**以下所有内容皆是事实,没有任何夸大的地方,稍微有一点点魔幻。** +**All the following content is factual, with no exaggerations, just a little bit of fantasy.** -## 刚开始接触电脑 +## First Encounter with Computers -最开始接触电脑是在我刚上五年级的时候,那时候家里没电脑,刚开始上网都是在黑网吧玩的。 +I first encountered computers when I was in the fifth grade. At that time, my family didn’t have a computer, and I started going online in an internet café. -黑网吧大概就是下面这样式儿的,一个没有窗户的房间里放了很多台老式电脑,非常拥挤。 +The internet café was pretty much like this: a windowless room filled with many old computers, and it was very crowded. -![黑网吧](https://oss.javaguide.cn/about-the-author/internet-addiction-teenager/heiwangba.png) +![Internet Cafe](https://oss.javaguide.cn/about-the-author/internet-addiction-teenager/heiwangba.png) -在黑网吧上网的经历也是一波三折,经常会遇到警察来检查或者碰到大孩子骚扰。在黑网吧上网的一年多中,我一共两次碰到警察来检查,主要是看有没有未成年人(当时黑网吧里几乎全是未成年人),实际感觉像是要问黑网吧老板要点好处。碰到大孩子骚扰的次数就比较多,大孩子经常抢我电脑,还威胁我把身上所有的钱给他们。我当时一个人也比较怂,被打了几次之后,就尽量避开大孩子来玩的时间去黑网吧,身上也只带很少的钱。小时候的性格就比较独立,在外遇到事情我一般也不会给家里人说(因为说了也没什么用,家人给我的安全感很少)。 +The experience of going online in the internet café was full of twists and turns. I often encountered police checks or older kids harassing me. During the year I spent online in the internet café, I met police checks twice, mainly to see if there were any minors (almost all the patrons were minors). It felt like they were just looking to ask the café owner for some benefits. I often encountered older kids who would steal my computer and threaten me to give them all the money I had on me. I was pretty timid at that time, and after being beaten a few times, I made an effort to avoid going at times when older kids were around, and I would only bring a little money with me. I was quite independent as a child and generally wouldn’t tell my family about incidents I encountered outside (because it wouldn’t make much difference; there was little sense of security from my family). -我现在已经记不太清当时是被我哥还是我姐带进网吧的,好像是我姐。 +I can’t quite remember whether it was my brother or sister who brought me into the internet café; it seems like it was my sister. -起初的时候,自己就是玩玩流行蝴蝶剑、单机摩托之类的单机游戏。但是,也没有到沉迷的地步,只是觉得这东西确实挺好玩的,一玩就可以玩一下午,恋恋不舍。 +At first, I just played single-player games like "Butterfly Sword" and standalone motorcycle games. However, I didn’t reach an addictive level, just found them quite fun, spending entire afternoons reluctantly playing. ![](https://oss.javaguide.cn/github/javaguide/books2a6021b9-e7a0-41c4-b69e-a652f7bc3e12-20200802173601289.png) -## 小学毕业后开始有网瘾 +## Internet Addiction After Primary School Graduation -开始有网瘾是在小学毕业的时候,在我玩了一款叫做 **QQ 飞车** 的游戏之后(好像是六年级末就开始玩了)。我艹,当时真的被这游戏吸引了。**每天上课都幻想自己坐在车里面飘逸,没错,当时就觉得秋名山车神就是我啦!** +I started to get addicted to the internet when I graduated from primary school, after I played a game called **QQ Speed** (I think I started playing it at the end of sixth grade). Wow, I was really captivated by this game at that time. **Every day in class, I would fantasize about drifting in the car, thinking, no doubt, I was the king of Akina!** -我当时技术还是挺不错的,整个网吧玩这个游戏的貌似还没有可以打败我的(我们当时经常会开放切磋)。 +I was quite skilled back then, and it seemed like no one in the internet café could defeat me (we often had friendly competitions). -QQ 飞车这款戏当时还挺火的,很多 90 后的小伙伴应该比较熟悉。 +QQ Speed was quite popular at that time, and many post-90s friends should be familiar with it. -我记得,那时候上网还不要身份证,10 元办一张网卡就行了,网费也是一元一小时。我就经常不吃早饭,攒钱用来上网。只要口袋里有钱,我都会和我的小伙伴奔跑到网吧一起玩 QQ 飞车。青回啊! +I remember that back then, there was no need for an ID to go online; you just needed 10 yuan to get a network card, and the internet fee was 1 yuan per hour. I often skipped breakfast to save money for internet time. Whenever I had money in my pocket, I would rush to the café with my friends to play QQ Speed. Ah, the nostalgia! -> 说到这,我情不自禁地打开自己的 Windows 电脑,下载了 Wegame ,然后下载了 QQ 飞车。 +> Speaking of this, I couldn’t help but open my Windows computer, download Wegame, and then download QQ Speed. -到了初二的时候,就没玩 QQ 飞车了。我的等级也永久定格在了 **120** 级,这个等级在当时那个升级难的一匹的年代,算的上非常高的等级了。 +By the time I was in the second year of middle school, I stopped playing QQ Speed. My level was permanently fixed at **120**, which was considered quite high back in those days when leveling up was incredibly difficult. ![](https://oss.javaguide.cn/javaguide/b488618c-3c25-4bc9-afd4-7324e27553bd-20200802175534614.png) -## 初二网瘾爆发 +## Internet Addiction Erupts in Second Year of Middle School -网瘾爆发是在上了初中之后。初二的时候,最为猖狂,自己当时真的是太痴迷于 **穿越火线** 这款游戏了,比 QQ 飞车还要更痴迷一些。每天上课都在想像自己拿起枪横扫地方阵营的场景,心完全不在学习上。 +My internet addiction erupted after I entered middle school. In the second year, it was the peak; I was truly obsessed with the game **CrossFire**, even more than with QQ Speed. Every day during class, I would imagine myself with a gun sweeping through enemy camps, completely detached from studying. -我经常每天早上起早去玩别人包夜留下的机子,毕竟那时候上学也没什么钱嘛!我几乎每个周五晚上都会趁家人睡着之后,偷偷跑出去通宵。整个初二我通宵了无数次,我的眼睛就是这样近视的。 +I often woke up early each morning to play on machines left overnight by others since I didn’t have much money for school! Almost every Friday night, I would sneak out to play overnight after my family was asleep. I ended up pulling countless all-nighters throughout my second year, which is how I developed my nearsightedness. -有网瘾真的很可怕,为了上网什么都敢做。当时我家住在顶楼的隔热层,我每次晚上偷偷出去上网,为了不被家里人发现,要从我的房间的窗户爬出去,穿过几栋楼,经过几间无人居住的顶楼隔热层之后再下楼。现在想想,还是比较危险的。而且,我天生比较怕黑。当时为了上网,每次穿过这么多没人居住的顶层隔热层都没怕过。你让我现在再去,我都不敢,实在是佩服当年的自己的啊! +Internet addiction is indeed terrifying; you would do anything to get online. At that time, I lived on the top floor in an insulated layer, and every night I would secretly climb out of my room’s window to avoid being discovered by my family. I would cross several buildings and pass through a few deserted top-floor insulation layers before heading downstairs. Thinking about it now, it was quite dangerous. Also, I was naturally afraid of the dark. Yet, I didn’t feel scared at all going through those deserted layers just to get online. If you made me do that now, I wouldn’t dare; I truly admire my former self! -![我家楼顶拍的雪景](https://oss.javaguide.cn/about-the-author/image-20230429114622340.png) +![Snow Scene from My Rooftop](https://oss.javaguide.cn/about-the-author/image-20230429114622340.png) -周五晚上通宵完之后,我会睡到中午,然后下午继续去网吧玩。到了周日,基本都是直接从早上 8 点玩到晚上 9 点 10 点。那时候精力是真旺盛,真的完全不会感觉比较累,反而乐在其中。 +After all-nighters on Friday, I would sleep until noon and then head to the café again in the afternoon. By Sunday, I’d typically play from 8 AM until 9 or 10 PM. My energy levels were truly high back then; I didn’t feel tired at all, rather, I enjoyed it thoroughly. -我的最终军衔停留在了两个钻石,玩过的小伙伴应该清楚这在当时要玩多少把(现在升级比较简单)。 +I ended up reaching two diamond ranks; those who have played should know how many matches it takes to achieve that (now leveling up is much easier). ![](https://oss.javaguide.cn/about-the-author/cf.png) -ps: 回坑 CF 快一年了,目前的军衔是到了两颗星中校 3 了。 +Ps: I’ve returned to CrossFire for almost a year now, and my rank has reached two-star lieutenant colonel 3. -那时候成绩挺差的。这样说吧!我当时在很普通的一个县级市的高中,全年级有 500 来人,我基本都是在 280 名左右。而且,整个初二我都没有学物理,上物理课就睡觉,考试就交白卷。 +Back then, my grades were quite poor. To put it simply! I was in an ordinary high school in a county-level city with around 500 students in my grade, and I hovered around the 280th position. Moreover, throughout the entire second year, I didn’t study physics at all; I would sleep during physics class and turn in blank sheets for exams. -为什么对物理这么抵触呢?这是因为开学不久的一次物理课,物理老师误会我在上课吃东西还狡辩,扇了我一巴掌。那时候心里一直记仇到大学,想着以后自己早晚有时间把这个物理老师暴打一顿。 +Why was I so resistant to physics? This was because, not long after school started, during one physics class, the teacher mistakenly thought I was eating during class and accused me, then slapped me. I held a grudge against that for years, thinking I would one day have the chance to pay back that physics teacher. -## 初三开启学习模式 +## Awakening to Study in Third Year -初三上学期的时候突然觉悟,像是开窍了一样,当时就突然意识到自己马上就要升高中了,要开始好好搞搞学习了。 +In the first semester of my third year, I suddenly had an epiphany, as if I was enlightened, realizing that I was about to graduate to high school and needed to start taking my studies seriously. -诶,其实也不算是开窍,主要还是为了让自己能在家附近上学,这样上网容易一些。因为当时我家就在我们当地的二中附近,附近有特别特别多的网吧,上网特别特别容易,加上我又能走读。 +Well, actually, it wasn’t really an epiphany; it was mostly because I wanted to attend school near my home, making it easier to go online. My family lived near our local Second Middle School, which had many internet cafés, so going online was incredibly easy since I could be a day student. -像我初中在的那个学校,年级前 80 的话基本才有可能考得上二中。经过努力,初三上学期的第一次月考,我直接从 280 多名进步到了年级 50 多名,有机会考入二中。当时还因为进步太大,被当作 **进步之星** 在讲台上给整个年级做演讲,分享经验。这也是我第一次在这么多人面前讲话,挺紧张的,但是挺爽的,在暗恋对象面前赚足了面子。 +In my middle school, students ranked in the top 80 had a good chance of getting into Second Middle School. After some effort, I improved from the 280th position in the first monthly exam of my third year to around 50th, making it possible for me to go to Second Middle School. I even became a **Star of Progress** and shared my experience with the whole grade on stage because of my significant improvement. This was also my first time speaking in front of so many people, and though I was pretty nervous, it felt great, especially in front of my crush. -其实在初三的时候,我的网瘾还是很大。不过,我去玩游戏的前提都是自己把所有任务做完,并且上课听讲也相对比较认真的听。 +Even though I still had a large internet addiction during my third year, I ensured that I completed all tasks and listened attentively in classes before playing games. -初三那会,我通宵的次数变少了一些,但会经常晚上趁着家人睡觉了,偷偷跑出去玩到凌晨 2 点多回来。 +During that time, my nights out decreased a bit, but I often sneaked out to play until 2 AM while my family was asleep. -当时,我们当地的高中有一个政策是每个学校的成绩比较优秀的学生可以参加 **高中提前招生考试** ,只要考上了就不用参加中考了。我当时也有幸参加了这次考试并成功进入了我们当地的二中。 +At that time, our local high school had a policy that allowed students with good grades to take part in an **Advanced Enrollment Examination**. If you passed, you wouldn't need to take the entrance exam. Fortunately, I was able to participate in this exam and successfully enroll in Second Middle School. -在我参加高中提前考试前的一个晚上,我半夜 12 点趁着妈妈睡着,跑去了网吧玩 CF 到凌晨 3 点多回来。就那一次我被抓了现行,到家之后发现妈妈就坐在客厅等我,训斥一顿后,我就保证以后不再晚上偷偷跑出去了。 +One night before the advanced exam, I snuck out to play CrossFire until after 3 AM, taking advantage of my mom sleeping. That time, I got caught red-handed; after arriving home, I found my mom waiting in the living room and, after a stern lecture, I promised to never sneak out at night again. -> 这里要说明一点:我的智商我自己有自知之明的,属于比较普通的水平吧!前进很大的主要原因是自己基础还行,特别是英语和物理。英语是因为自己喜欢,加上小学就学了很多初中的英语课程。物理的话就很奇怪,虽然初二也不怎么听物理课,也不会物理,但是到了初三之后自己就突然开窍了。真的!我现在都感觉很奇怪。然后,到了高中之后,我的英语和物理依然是我最好的两门课。大学的兼职,我出去做家教都是教的高中物理。 +> Here’s a point to clarify: I’m aware of my own intelligence level; it’s pretty average! The main reason for my significant progress was that I had a solid foundation, especially in English and physics. My English was good because I liked it and had studied many middle school English topics in primary school. The situation with physics is interesting; although I didn’t pay much attention in physics class during my second year and didn’t understand it, I suddenly got it in my third year. Seriously! I still find it quite strange. Then, in high school, English and physics continued to be my two strongest subjects. During college, when I tutored, I taught high school physics. -## 高中从小班掉到平行班 +## Dropped from Small Class to Regular Class in High School -![出高考成绩后回高中母校拍摄](https://oss.javaguide.cn/about-the-author/internet-addiction-teenager/wodegaozhong.png) +![Photo Taken After Receiving Exam Results at My High School](https://oss.javaguide.cn/about-the-author/internet-addiction-teenager/wodegaozhong.png) -由于参加了高中提前招生考试,我提前 4 个月就来到了高中,进入了小班,开始学习高中的课程。 +Because I participated in the advanced enrollment examination, I entered high school four months early, starting in a small class where I began taking high school courses. -上了高中的之后,我上课就偷偷看小说,神印王座、斗罗大陆、斗破苍穹很多小说都是当时看的。中午和晚上回家之后,就在家里玩几把 DNF。当时家里也买了电脑,姥爷给买的,是对自己顺利进入二中的奖励。到我卸载 DNF 的时候,已经练了 4 个满级的号,两个接近满级的号。 +Once I got to high school, I secretly read novels during class, including "God’s Seal," "Douluo Continent," and "Battle Through the Heavens," among many others. In the afternoons and evenings when I returned home, I’d play a few rounds of DNF. My family had also bought a computer, a reward from my grandfather for successfully entering Second Middle School. By the time I uninstalled DNF, I had already leveled up four characters to max level, with two others nearing max level. -当时我的空间专门有一个相册里面放的全是 DNF 的一些照片和截图,无比痴迷于练级和刷图。 +I had a specific album on my social media account that contained a collection of DNF photos and screenshots, as I was incredibly obsessed with leveling up and dungeon raids. -在高中待了不到一个月,我上体育课的时候不小心把腿摔断了,这也是我第一次感受到骨头断裂的头疼,实在是太难受了! +In less than a month in high school, I accidentally broke my leg during a PE class, feeling the pain of a broken bone for the first time—it was absolutely unbearable! -于是,我就开始休学养病。直到高中正式开学一个月之后,我才去上学,也没有参加军训。 +As a result, I had to take a break from school to recuperate. I didn’t return to school until a month after the official start of senior high, and I didn’t attend military training. -由于我耽误了几个月的课程,因此没办法再进入小班,只能转到奥赛班。到了奥赛班之后,我继续把时间和经历都投入在游戏和小说上,于是我的成绩在奥赛班快接近倒数了。等到高二分班的时候,我成功被踢出奥赛班来到了最普通的平行班。 +Since I had missed a few months of classes, I couldn’t go back to the small class and could only transfer to the Olympiad class. After joining the Olympiad class, I continued to invest my time and energy in games and novels, leading my grades to close to the bottom of the class. When the second-year evaluation came around, I was successfully dropped from the Olympiad class to the most ordinary regular class. -**我成功把自己从学校最好的小班玩到奥赛班,然后再到平行班。有点魔幻吧!** +**I successfully played myself from the best small class to the Olympiad class and then back to the regular class. Quite surreal, right?** -## 高二开始奋起直追 +## A Strong Comeback in Senior Year -高中觉悟是在高二下学期的时候,当时是真的觉悟了,就突然觉得游戏不香了,觉得 DNF 也不好玩了,什么杀怪打装备不过是虚无,练了再多满级的 DNF 账号也屁用没有,没钱都是浮云。 +I truly awakened academically in the second semester of senior year; I suddenly felt that games just weren’t appealing anymore, that DNF was no longer fun. Everything involving monster hunting and equipment grinding was just hollow; no matter how many max levels I achieved in DNF, it was meaningless without money. -我妈妈当时还很诧异,还奇怪地问我:“怎么不玩游戏了?”(我妈属于不怎么管我玩游戏的,她觉得这东西还是要靠自觉)。 +My mom was surprised and curiously asked me, “Why aren’t you playing games anymore?” (She didn’t really mind me playing games; she believed it’s something that relies on self-discipline). -于是,我便开始牟足劲学习,每天都沉迷学习无法自拔(豪不夸张),乐在其中。虽然晚自习上完回到家已经差不多 11 点了,但也并不感觉累,反而感觉很快乐,很充实。 +So, I began to study earnestly, immersing myself in learning (not exaggerating), and enjoying it. Even though I returned home around 11 PM after evening self-study sessions, I didn’t feel tired; instead, I felt happy and fulfilled. -**我的付出也很快得到了回报,我顺利返回了奥赛班。** 当时,理科平行班大概有 7 个,每次考试都是平行班之间会单独排一个名次,小班和奥赛班不和我们一起排名次。后面的话,自己基本每次都能在平行班得第一,并且很多时候都是领先第二名 30 来分。由于成绩还算亮眼,高三上学期快结束的时候,我就向年级主任申请去了奥赛班。 +**My efforts quickly paid off, and I successfully returned to the Olympiad class.** At that time, there were about seven science regular classes, and exam rankings were determined separately for these classes; we didn’t compete with the small and Olympiad classes for rankings. From then on, I was able to take first place in my regular class in almost every exam, often leading the second-place student by around 30 points. Given my outstanding grades, I applied to the head of our grade to return to the Olympiad class just before the first semester of senior year ended. -## 高考前的失眠 +## Insomnia Before College Entrance Examination -> **失败之后,不要抱怨外界因素,自始至终实际都是自己的问题,自己不够强大!** 然后,高考前的失眠也是我自己问题,要怪只能怪自己,别的没有任何接口。 +> **After failure, don’t blame external factors; the problem has always been with yourself—not strong enough!** Likewise, the insomnia before my college entrance exam was also my own issue; if I have to point fingers, it’s only at myself—there’s no other outlet. -我的高考经历其实还蛮坎坷的,毫不夸张的说,高考那今天可能是我到现在为止,经历的最难熬的时候,特别是在晚上。 +My college entrance exam experience was quite tumultuous. It’s no exaggeration to say that the days of the exam were the most challenging time I’ve experienced, especially at night. -我在高考那几天晚上都经历了失眠,想睡都睡不着那种痛苦想必很多人或许都体验过。 +During the few nights of the exam, I suffered from insomnia; the pain of being unable to sleep is something that many may have experienced. -其实我在之前是从来没有过失眠的经历的。高考前夕,因为害怕自己睡不着,所以,我提前让妈妈去买了几瓶老师推荐的安神补脑液。我到现在还记得这个安神补脑液是敖东牌的。 +Actually, I had never experienced insomnia before. On the eve of the college entrance exam, fearing I wouldn’t be able to sleep, I had my mom buy several bottles of the nerve-soothing tonic recommended by my teacher. I still remember the brand: Aodong. ![](https://oss.javaguide.cn/about-the-author/internet-addiction-teenager/image-20220625194714247.png) -高考那几天的失眠,我觉得可能和我喝了老师推荐的安神补脑液有关系,又或者是我自己太过于紧张了。因为那几天睡觉总会感觉有很多蚂蚁在身上爬一样,身上还起了一些小痘痘(有点像是过敏)。 +The insomnia during the exam period might have been related to the tonic I drank, or perhaps I was too tense. Those nights, I felt as if many ants were crawling on me whenever I tried to sleep, and I broke out in some small hives (similar to an allergy). -这里要格外说明一点,避免引起误导:**睡不着本身就是自身的问题,上述言论并没有责怪这个补脑液的意思。** 另外, 这款安神补脑液我去各个平台都查了一下,发现大家对他的评价都挺好,和我们老师当时推荐的理由差不多。如果大家需要改善睡眠的话,可以咨询相关医生之后尝试一下。 +Here’s something important to clarify to avoid misunderstandings: **Inability to sleep is a personal issue; none of the above statements are meant to blame the tonic.** Additionally, I’ve checked various platforms, and find that reviews for this tonic are quite favorable, similar to the reasons our teacher recommended it. If anyone needs help with sleep, you might consider consulting a doctor and giving it a try. -高考也确实没发挥好,整个人在考场都是懵的状态。高考成绩出来之后,比我自己预估的还低了几十分,最后只上了一个双非一本。不过,好在专业选的好,吃了一些计算机专业的红利,大学期间也挺努力的。 +I indeed didn’t perform well on the college entrance exam, feeling like I was in a daze throughout. After the results were announced, I scored lower than I had predicted by several points and ultimately only got into a non-prestigious university. However, I was fortunate that I chose a good major and benefited from the computer science field during my university years, studying quite hard. -## 大学生活 +## College Life -大学生活过的还是挺丰富的,我会偶尔通宵敲代码,也会偶尔半夜发疯跑出去和同学一起走走古城墙、去网吧锤一夜的 LOL。 +My college life was quite rich. I would occasionally pull all-nighters coding or frantically head out at night with classmates to walk along the ancient city walls or spend the night in an internet café playing LOL. -大学生活专门写过一篇文章介绍:[害,毕业三年了!](./my-college-life.md) 。 +I wrote a dedicated article about my college life: [Oh no, it’s been three years since graduation!](./my-college-life.md). -## 总结 +## Conclusion -整个初中我都属于有点网瘾少年的状态,不过初三的时候稍微克制一些。到了高二下学期的时候,自己才对游戏真的没有那么沉迷了。 +Throughout middle school, I was somewhat of an internet-addicted teenager, but I was able to restrain myself a bit during the third year. It wasn’t until the second semester of senior year that I truly stopped being addicted to games. -对游戏不那么沉迷,也是因为自己意识到游戏终究只是消遣,学习才是当时最重要的事情。而且,我的游戏技术又不厉害,又不能靠游戏吃饭,什么打怪升级到最后不过是电脑中的二进制数据罢了! +My reduced obsession with games stemmed from the realization that games are ultimately just a pastime, and studying was the most important thing at that time. Besides, my gaming skills weren’t impressive, and I couldn’t make a living through gaming—everything concerning leveling up and monster hunting was merely binary data on a computer! -**这玩意必须你自己意识到,不然,单纯靠父母监督真的很难改变!如果心不在学习上面的话,那同时是不可能学好的!** +**This realization must come from within yourself; otherwise, relying solely on parental supervision is indeed very difficult to change! If your heart isn’t set on studying, it’s impossible to achieve good results!** -我真的很反对父母过于干涉孩子的生活,强烈谴责很多父母把自己孩子的网瘾归咎于网络游戏,把自己孩子的暴力归咎于影视媒体。 +I strongly oppose overly intrusive parenting and vehemently denounce many parents who attribute their children's internet addiction to online games or blame media for their child’s violence. -**时刻把自己的孩子保护起来不是一件靠谱的事情,他终究要独自面对越来越多的诱惑。到了大学,很多被父母保护太好的孩子就直接废了。他们没有独立意识,没有抗拒诱惑的定力!** +**Constantly shielding your child isn’t a reliable solution; they will eventually face more and more temptations on their own. In university, many children who were overly protected by their parents end up failing. They lack independence and the strength to resist temptation!** diff --git a/docs/about-the-author/my-college-life.md b/docs/about-the-author/my-college-life.md index 43d96bd4186..3301c9da385 100644 --- a/docs/about-the-author/my-college-life.md +++ b/docs/about-the-author/my-college-life.md @@ -1,387 +1,63 @@ --- -title: 害,毕业三年了! -category: 走近作者 +title: Oh no, it's been three years since graduation! +category: Meet the Author star: 1 tag: - - 个人经历 + - Personal Experience --- -> 关于初高中的生活,可以看 2020 年我写的 [我曾经也是网瘾少年](./internet-addiction-teenager.md) 这篇文章。 +> For insights into my middle and high school life, you can read the article [I Was Once a Teen Addicted to the Internet](./internet-addiction-teenager.md) that I wrote in 2020. -2019 年 6 月份毕业,距今已经过去了 3 年。趁着高考以及应届生毕业之际,简单聊聊自己的大学生活。 +I graduated in June 2019, and it has been three years since then. With the college entrance examination and the graduation of fresh graduates happening, I want to briefly talk about my university life. -下面是正文。 +Here is the main text. -我本科毕业于荆州校区的长江大学,一所不起眼的双非一本。 +I graduated from Yangtze University in Jingzhou, an inconspicuous non-double first-class university. -在这里度过的四年大学生活还是过的挺开心的,直到现在,我依然非常怀念! +I had a pretty happy four years of university life here, and I still miss it very much! -在学校的这几年的生活,总体来说,还算是比较丰富多彩的。我会偶尔通宵敲代码,也会偶尔半夜发疯跑出去和同学一起走走古城墙、去网吧锤一夜的 LOL。 +Overall, my life at school was quite colorful. I would occasionally stay up all night coding, and sometimes I would go out at midnight with classmates to walk along the ancient city wall or spend the night at an internet café playing LOL. -写下这篇杂文,记录自己逝去的大学生活!希望未来继续砥砺前行,不忘初心! +I am writing this essay to document my past university life! I hope to continue to forge ahead in the future and not forget my original intention! -## 大一 +## Freshman Year -大一那会,我没有把精力放在学习编程上,大部分时间都在参加课外活动。 +During my freshman year, I didn't focus my energy on learning programming; most of my time was spent participating in extracurricular activities. -或许是因为来到了一座新鲜的城市,对周围的一切都充满了兴趣。又或许是因为当时的我还比较懵懂,也没有任何学习方向。 +Perhaps it was because I had come to a new city and was full of interest in everything around me. Or maybe it was because I was still quite naive at that time and had no clear direction for my studies. -这一年,我和班里的一群新同学去逛了荆州的很多地方比如荆州博物馆、长江大桥、张居正故居、关帝庙。 +That year, I went to many places in Jingzhou with a group of new classmates, such as the Jingzhou Museum, Yangtze River Bridge, Zhang Juzheng's Former Residence, and the Guandi Temple. -![大一的一次班级出行](https://oss.javaguide.cn/about-the-author/college-life/41239dd7d18642f7af201292ead94f1a~tplv-k3u1fbpfcp-zoom-1.image.png) +![A class outing in freshman year](https://oss.javaguide.cn/about-the-author/college-life/41239dd7d18642f7af201292ead94f1a~tplv-k3u1fbpfcp-zoom-1.image.png) -即使如此,我当时还是对未来充满了希望,憧憬着工作之后的生活。 +Even so, I was still full of hope for the future, looking forward to life after work. -我还记得当时我们 6 个室友那会一起聊天的时候,其他 5 个室友都觉得说未来找工作能找一个 6k 的就很不错了。我当时就说:“怎么得至少也要 8k 吧!”。他们无言,觉得我的想法太天真。 +I remember when we six roommates were chatting, the other five thought that finding a job with a salary of 6k would be quite good. I said, "At least it should be 8k!" They were speechless, thinking my idea was too naive. -其实,我当时内心想的是至少是月薪 1w 起步,只是不太好意思直接说出来。 +In fact, I was thinking that it should start at a monthly salary of 10k, but I was too shy to say it directly. -我不爱出风头,性格有点内向。刚上大学那会,内心还是有一点不自信,干什么事情都畏畏缩缩,还是迫切希望改变自己的! +I don't like to show off and have a somewhat introverted personality. When I first entered university, I was still a bit insecure, hesitant in everything I did, and I was eager to change myself! -于是,凭借着一腔热血,我尝试了很多我之前从未尝试过的事情:**露营**、**户外烧烤**、**公交车演讲**、**环跑古城墙**、**徒步旅行**、**异地求生**、**圣诞节卖苹果**、**元旦晚会演出**...。 +So, with a passion, I tried many things I had never attempted before: **camping**, **outdoor barbecues**, **bus speeches**, **running around the ancient city wall**, **hiking**, **survival in a different place**, **selling apples on Christmas**, **performing at the New Year's Eve party**... -下面这些都是我和社团的小伙伴利用课外时间自己做的,在圣诞节那周基本都卖完了。我记得,为了能够多卖一些,我们还挨个去每一个寝室推销了一遍。 +The following are things I did with my club friends during our spare time, and we sold out most of them during the week of Christmas. I remember we went to each dormitory to promote our sales. ![](https://oss.javaguide.cn/about-the-author/college-life/7cf1a2da505249a58e1f29834dbac435~tplv-k3u1fbpfcp-zoom-1.image.png) -我还参加了大一元旦晚会,不过,那次演出我还是没放开,说实话,感觉没有表现出应该有的那味。 +I also participated in the New Year's Eve party in freshman year, but I still couldn't let go during the performance. To be honest, I felt I didn't show the flair I should have. ![](https://oss.javaguide.cn/about-the-author/college-life/850cae1f8c644c5d920140f66ae9303d~tplv-k3u1fbpfcp-zoom-1.image.png) -经过这次演出之后,我发现我是真的没有表演的天赋,很僵硬。并且,这种僵硬呆板是自己付出努力之后也没办法改变的。 +After this performance, I realized that I really didn't have a talent for acting; I was very stiff. Moreover, this stiffness and rigidity couldn't be changed even with effort. -下图是某一次社团聚餐,我喝的有点小醉之后,被朋友拍下的。 +The following picture was taken during a club dinner when I got a bit tipsy, captured by a friend. ![](https://oss.javaguide.cn/about-the-author/college-life/82a503e365354bd1bf190540fbf1039a~tplv-k3u1fbpfcp-zoom-1.image.png) -那时候,还经常和和社团的几位小伙伴一起去夜走荆州古城墙。 +At that time, I often went night walking on the ancient city wall with a few friends from the club. -![某一次要去夜走古城墙的路上我拍的](https://oss.javaguide.cn/about-the-author/college-life/007a83e6d26c43b9aa6e0b0266c3314b~tplv-k3u1fbpfcp-zoom-1.image.png) +![A photo I took on the way to night walk on the ancient city wall](https://oss.javaguide.cn/about-the-author/college-life/007a83e6d26c43b9aa6e0b0266c3314b~tplv-k3u1fbpfcp-zoom-1.image.png) -不知道社团的大家现在过得怎么样呢? +I wonder how everyone from the club is doing now? -虽然这些经历对于我未来的工作和发展其实没有任何帮助,但却让我的大学生活更加完整,经历了更多有趣的事情,有了更多可以回忆的经历。 - -我的室友们都窝在寝室玩游戏、玩手机的时候,我很庆幸自己做了这些事情。 - -个人感觉,大一的时候参加一些不错的社团活动,认识一些志同道合的朋友还是很不错的! - -**参加课外活动之余,CS 专业的小伙伴,尽量早一点养成一个好的编程习惯,学好一门编程语言,然后平时没事就刷刷算法题。** - -### 办补习班 - -大一暑假的时候,我作为负责人,在孝感的小乡镇上办过 5 个补习班(本来是 7 个,后来砍掉了 2 个) 。 - -从租房子、租借桌椅再到招生基本都是从零开始做的。 - -每个周末我都会从荆州坐车跑到孝感,在各个县城之间来回跑。绝大部分时候,只有我一个人,偶尔也会有几个社团的小伙伴陪我一起。 - -![](https://oss.javaguide.cn/about-the-author/college-life/6ee6358c236144d8a8a205cc6bc99b9b~tplv-k3u1fbpfcp-zoom-1.image.png) - -记忆犹新,那一年孝感也是闹洪水,还挺严重的。 - -![](https://oss.javaguide.cn/javaguide/image-20210820201908759.png) - -有一次我差点回不去学校参加期末考试。虽然没有备考,但是也没有挂过任何一门课,甚至很多科目考的还不错。不过,这还是对我绩点产生了比较大的影响,导致我后面没有机会拿到奖学金。 - -![](https://oss.javaguide.cn/about-the-author/college-life/3c5fe7af43ba4e348244df1692500fce~tplv-k3u1fbpfcp-zoom-1.image.png) - -这次比较赶时间,所以就坐的是火车回学校。在火车上竟然还和别人撞箱子了! - -![](https://oss.javaguide.cn/about-the-author/college-life/570f5791aeb54fa1a76892b69e46fec2~tplv-k3u1fbpfcp-zoom-1.image.png) - -当时去小乡镇上的时候,自己最差的时候住过 15 元的旅馆。真的是 15 元,你没看错。就那种老旧民房的小破屋,没有独卫,床上用品也很不卫生,还不能洗澡。 - -下面这个还是我住过最豪华的一个,因为当时坐客车去了孝感之后,突然下大雨,我就在车站附近找了一个相对便宜点的。 - -![](https://oss.javaguide.cn/about-the-author/college-life/687c3ede3f094c65a72d812ca0f06bb4~tplv-k3u1fbpfcp-zoom-1.image.png) - -为了以更低的价钱租到房子,我经常和房东砍价砍的面红耳赤。 - -说句心里话,这些都是我不太愿意去做的事情,我本身属于比较爱面子而且不那么自信的人。 - -当时,我需要在各个乡镇来回跑,每天就直接顶着太阳晒 。每次吃饭都特别香,随便炒个蔬菜都能吃几碗米饭。 - -我本身是比较挑食的,这次经历让我真正体会到人饿了之后吃嘛嘛香! - -我一个人给 6 个老师加上 10 来个学生和房东们一家做了一个多月的饭,我的厨艺也因此得到了很大的锻炼。 - -![](https://oss.javaguide.cn/about-the-author/college-life/2e3b6101abcd46a8a213c08782aeac33~tplv-k3u1fbpfcp-zoom-1.image.png) - -这些学生有小学的,也有初中的,都比较听话。有很多还是留守儿童,爸爸妈妈在外打工,跟着爷爷奶奶一起生活。 - -加上我的话,我们一共有 4 位老师,我主要讲的是初中和高中的物理课。 - -学生们都挺听话,没有出现和我们几个老师闹过矛盾。只有两个调皮的小学生被我训斥之后,怀恨在心,写下了一些让我忍俊不禁的话!哈哈哈哈!太可爱了! - -![](https://oss.javaguide.cn/about-the-author/college-life/3680cead2c0f4165bb4865f038326b61~tplv-k3u1fbpfcp-zoom-1.image.png) - -离开之前的前一天的晚上,我和老师们商量请一些近点的同学们来吃饭。我们一大早就出去买菜了,下图是做成后的成品。虽然是比较简单的一顿饭,但我们吃的特别香。 - -![补习班的最后一顿晚餐](https://oss.javaguide.cn/about-the-author/college-life/f36bfd719b9b4463b2f1d3edc51faa97~tplv-k3u1fbpfcp-zoom-1.image.png) - -那天晚上还有几个家长专门跑过来看我做饭,家长们说他们的孩子非常喜欢我做的饭,哈哈哈!我表面淡然说自己做的不好,实则内心暗暗自喜,就很“闷骚”的一个人,哈哈哈! - -不知道这些学生们,现在怎么样呢?怀念啊! - -培训班结束,我回家之后,我爸妈都以为我是逃荒回来的。 - -### 自己赚钱去孤儿院 - -大一尾声的时候,还做了一件非常有意义的事情。我和我的朋友们去了一次孤儿院(荆州私立孤儿教养院)。这个孤儿院曾经还被多家电视台报道过,目前也被百度百科收录。 - -![](https://oss.javaguide.cn/about-the-author/college-life/db8f5c276f4d4a7c9d7bd1e6100de301~tplv-k3u1fbpfcp-zoom-1.image.png) - -孤儿院的孩子们,大多是一些无父无母或者本身有一些疾病被父母遗弃的孩子。 - -去之前,我们买了很多小孩子的玩具、文具、零食这些东西。这些钱的来源也比较有意义,都是我和社团的一些小伙伴自己去外面兼职赚的一些钱。 - -![离开之前和创建孤儿院的老爷爷的一张合照](https://oss.javaguide.cn/about-the-author/college-life/cf43853c49bd489a9fc0ee437a2af432~tplv-k3u1fbpfcp-zoom-1.image.png) - -勿以善小而不为!引用《爱的风险》这首歌的一句歌词:“只要人人都献出一点爱,世界将变成美好的人间” 。 - -我想看看这个孤儿院的现状,于是在网上有搜了一下,看到了去年 1 月份荆州新闻网的一份报道。 - -![](https://oss.javaguide.cn/about-the-author/college-life/0ac27206389c498882dd7f6f440c6abb~tplv-k3u1fbpfcp-zoom-1.image.png) - -孤儿教养院创办 33 年来,累计收养孤儿 85 人,其中有 5 人参军入伍报效祖国,20 人上大学,有的早已参加工作并成家立业。 - -叔叔也慢慢老了,白发越来越多。有点心酸,想哭,希望有机会再回去看看您!一定会的! - -![](https://oss.javaguide.cn/about-the-author/college-life/ea803a99c08149f892ca29e784653503~tplv-k3u1fbpfcp-zoom-1.image.png) - -### 徒步旅行 - -大一那会还有一件让我印象非常深刻的事情——徒步旅行。 - -我和一群社团的小伙伴,徒步走了接近 45 公里。我们从学校的西校区,徒步走到了枝江那边的一个沙滩。 - -![](https://oss.javaguide.cn/about-the-author/college-life/94ca5b6c5ea84dfb9e12b7a718587ea3~tplv-k3u1fbpfcp-zoom-1.image.png) - -是真的全程步行,这还是我第一次走这么远。 - -走到目的地的时候,我的双腿已经不听使唤,脚底被磨了很多水泡。 - -我们在沙滩上露营,烧烤,唱歌跳舞,一直到第二天早上才踏上回学校的路程。 - -![](https://oss.javaguide.cn/about-the-author/college-life/8120d45d30254c908f9db20b3c00f514~tplv-k3u1fbpfcp-zoom-1.image.png) - -## 大二 - -到了大二,我开始把自己的重点转移到编程知识的学习上。 - -不过,我遇到一个让我比较纠结的问题:社团里玩的最好的几个朋友为了能让社团能继续延续下去,希望我和他们一起来继续带这个团队。 - -但是,我当时已经规划好了自己大二要做的事情,真的想把精力都放在编程学习上,想要好好沉淀一下自己的技术。 - -迫于无奈,我最终还是妥协,选择了和朋友一起带社团。毕竟,遇到几个真心的朋友属实不易! - -### 带社团 - -带社团确实需要花费很多业余时间,除了每周要从东校区打车到西校区带着他们跑步之外,我们还需要经常带着他们组织一些活动。 - -比如我们一起去了长江边上烧烤露营。 - -![](https://oss.javaguide.cn/about-the-author/college-life/8a6945ccc087017c1f96ee93f3af8178-20220608154206500.png) - -再比如我们一起去环跑了古城墙。 - -![](https://oss.javaguide.cn/about-the-author/college-life/2cfba22049e8b99e11955bcb7662d790.png) - -大学那会,我还是非常热爱运动的! - -![](https://oss.javaguide.cn/about-the-author/college-life/2dd503a60f814a7a953816bc3b5194cd~tplv-k3u1fbpfcp-zoom-1.image.png) - -大二那会,我就已经环跑了 3 次古城墙。 - -![](https://oss.javaguide.cn/about-the-author/college-life/949543b550e847d5a7314b7e1842489b~tplv-k3u1fbpfcp-zoom-1.image.png) - -### 加入长大在线 - -在大二的时候,我还加入了学校党委宣传部下的组织——长大在线。这是一个比较偏技术性质的组织,主要负责帮学校做做网站、APP 啥的。 - -在百度上,还能搜索到长大在线的词条。 - -![](https://oss.javaguide.cn/about-the-author/college-life/34ecf650120a4289a68b7549eb7d00cc~tplv-k3u1fbpfcp-zoom-1.image.png) - -莫名其妙还被发了一个记者证,哈哈哈! - -![](https://oss.javaguide.cn/about-the-author/college-life/image-20220606121111042.png) - -我选的是安卓组,然后我就开始了学习安卓开发的旅程。 - -刚加入这个组织的时候,我连 HTML、CSS、JS、Java、Linux 这些名词都不知道啥意思。 - -再到后面,我留下来当了副站长,继续为组织服务了大半年多。 - -![](https://oss.javaguide.cn/about-the-author/college-life/image-20220608121413761.png) - -### 第一次参加比赛 - -那会也比较喜欢去参加一些学校的比赛,也获得过一些不错的名次,让我印象最深的是一次 PPT 大赛,这也是我第一次参加学校的比赛。 - -参加比赛之前,自己也是一个 PPT 小白,苦心学了一周多之后,我的一个作品竟然顺利获得了第一名。 - -![](https://oss.javaguide.cn/about-the-author/college-life/image-20220608121446529.png) - -也正是因为这次比赛,我免费拥有了自己的第一个机械键盘,这个键盘陪我度过了后面的大学生活。 - -### 确定技术方向 - -在大二上学期末,我最终确定了自己以后要走的技术方向是走 Java 后端。于是,我就开始制定学习计划,开始了自己的 Java 后端领域的打怪升级之路。 - -每次忙到很晚,一个人走在校园的时候还是很爽的!非常喜欢这种安静的感觉。 - -![](https://oss.javaguide.cn/about-the-author/college-life/336fd489ce314d259d6090194f237e1b~tplv-k3u1fbpfcp-zoom-1.image.png) - -当时身体素质真好,熬夜之后第二天照常起来上课学习。现在熬个夜,后面两天直接就废了! - -到了大三,我基本把 Java 后端领域一些必备的技术都给过了一遍,还用自己学的东西做了两个实战项目。 - -由于缺少正确的人指导,我当时学的时候也走了很多弯路,浪费了不少时间(我很羡慕大家能有我,就很厚脸皮!)。 - -那个时候还贼自恋,没事就喜欢自拍一张。 - -![](https://oss.javaguide.cn/about-the-author/college-life/image-20210820202341008.png) - -国庆节的时候也不回家,继续在学校刷 Java 视频和书籍。 - -我记得那次国庆节的时候效率还是非常高的,学习起来也特别有动力。 - -![](https://oss.javaguide.cn/about-the-author/college-life/WX20210820-203458.png) - -## 大三 - -整个大三,我依然没有周末,基本没有什么娱乐时间。绝大部分时间都是一个人在寝室默默学习,平时偶尔也会去图书馆和办公室。 - -虽然室友经常会玩游戏和看剧什么的,但是我对我并没有什么影响。一个人戴上耳机之后,世界仿佛都是自己的。 - -和很多大佬可能不太一样,比起图书馆和办公室,我在寝室的学习效率更高一些。 - -### JavaGuide 诞生 - -我的开源项目 JavaGuide 和公众号都是这一年启动的。 - -![](https://oss.javaguide.cn/about-the-author/college-life/the-birth-of-javaguide.jpeg) - -目前的话,JavaGuide 也已经 100k star ,我的公众号也已经有 15w+ 的关注。 - -![](https://oss.javaguide.cn/about-the-author/college-life/image-20210820211926742.png) - -### 接私活赚钱 - -一些机遇也让我这一年也接了一些私活赚钱。为了能够顺利交付,偶尔也会熬夜。当时的心态是即使熬夜也还是很开心、充实。每次想到自己通过技术赚到了钱,就会非常有动力。 - -我也曾写过文章分享过接私活的经历:[唠唠嗑!大学那会接私活赚了 3w+](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247499539&idx=1&sn=ff153f9bd98bb3109b1f14e58ed9a785&chksm=cea1b0d8f9d639cee4744f845042df6b1fc319f4383b87eba76a944c2648c81a51c28d25e3b6&token=2114015135&lang=zh_CN#rd) 。 - -不过,我接的几个私活也是比较杂的,并不太适合作为简历上的项目经历。 - -于是,为了能让简历上的项目经历看着更好看一些,我自己也找了两个项目做。一个是我跟着视频一起做的,是一个商城类型的项目。另外一个是自己根据自己的想法做的,是一个视频网站类型的项目。 - -商城类型的项目大概的架构图如下(没有找到当时自己画的原图): - -![](https://oss.javaguide.cn/about-the-author/college-life/206fab84bf5b4c048f8a88bc68c942f6~tplv-k3u1fbpfcp-zoom-1.image.png) - -那会商城项目貌似也已经烂大街了,用的人比较多。为了让自己的商城项目更有竞争力,对照着视频教程做完之后,我加入了很多自己的元素比如更换消息队列 ActiveMQ 为 Kafka、增加二级缓存。 - -在暑假的时候,还和同学老师一起做了一个员工绩效管理的企业真实项目。这个项目和我刚进公司做的项目,非常非常相似,不过公司做得可能更高级点 ,代码质量也要更高一些。实在是太巧了! - -我记得当时自己独立做项目的时候,遇到了很多问题。**就很多时候,你看书很容易就明白的东西,等到你实践的时候,总是会遇到一些小问题。我一般都是通过 Google 搜索解决的,用好搜索引擎真的能解决自己 99% 的问题。** - -### 参加软件设计大赛 - -大三这一年也有遗憾吧!我和几位志同道合的朋友一起参加过一个软件设计大赛,我们花了接近两个月做的系统顺利进入了复赛。 - -不过,我后面因为自己个人觉得再花时间做这个系统学不到什么东西还浪费时间就直接退出了。然后,整个团队就散了。 - -其实,先来回头看也是可以学到东西的,自己当时的心态有点飘了吧,心态有一些好高骛远。 - -现在想来,还是挺对不起那些一起奋斗到深夜的小伙伴。 - -人生就是这样,一生很长,任何时候你回头看过去的自己,肯定都会有让自己后悔的事情。 - -### 放弃读研 - -当时,我也有纠结过是否读研,毕竟学校确实一般,读个研确实能够镀点金,提升一下学历。 - -不过,我最终还是放弃了读研。当时比较自信,心里就觉得自己不需要读研也能够找到好工作。 - -### 实习 - -大三还找了一家离学校不远的公司实习,一位老学长创办的。不过,说实话哈,总体实习体验很差,没有学到什么东西不说,还耽误了自己很多已经计划好的事情。 - -我记得当时这个公司很多项目还是在用 JSP,用的技术很老。如果是老项目还好,我看几个月前启动的项目也还是用的 JSP,就很离谱。。。 - -当时真的很难受,而且一来就想着让你上手干活,活还贼多,干不完还想让你免费加班。。。 - -当时也没办法,因为荆州实在是找不到其他公司可以让你实习,你又没办法跑到其他城市去实习。这也是放弃选择一二线城市的学校带来的问题吧! - -## 大四 - -### 开始找工作 - -找实习找工作时候,才知道大学所在的城市的重要性。 - -由于,我的学校在荆州,而且本身学校就很一般,因此,基本没有什么比较好的企业来招人。 - -当时,唯一一个还算可以的就是苏宁,不过,我遇到的那个苏宁的 HR 还挺恶心的,第一轮面试的时候就开始压薪资了,问我能不能加班。然后,我也就对苏宁没有了想法。 - -秋招我犯了一个比较严重的问题,那就是投递简历开始的太晚。我是把学校的项目差不多做完之后,才开始在网上投递简历。这个时候,暑假差不多已经结束了,秋招基本已经尾声了。 - -可能也和学校环境有一些关系,当时,身边的同学没有参加秋招的。大三暑假的时候,都跑去搞学院组织的实习。我是留在学校做项目,没有去参加那次实习。 - -我觉得学校还是非常有必要提醒学生们把握住秋招这次不错的机会的! - -在网上投递了一些简历之后,很多笔试我觉得做的还可以的都没有回应。 - -我有点慌了!于是,我就从荆州来到武汉,想在武大华科这些不错的学校参加一些宣讲会。 - -到了武汉之后,我花了一天时间找了一个蛋壳公寓住下。第二天,我就跑去武汉理工大学参加宣讲会。 - -![](https://oss.javaguide.cn/about-the-author/college-life/image-20210820204919942.png) - -当天,我就面试了自己求职过程中的第一家公司—**玄武科技**。 - -就是这样一家中小型的公司,当时来求职面试的很多都是武大华科的学生。不过,他们之中一定有很多人和我一样,就是单纯来刷一波经验,找找信心。 - -整个过程也就持续了 3 天左右,我就顺利的拿下了玄武科技的 offer。不过,最终没有签约。 - -### 拿到 Offer - -来武汉之前,我实际上已经在网上投递了 **ThoughtWorks**,并且,作业也已经通过了。 - -当时,我对 ThoughtWorks 是最有好感的,内心的想法就是:“拿下了 ThoughtWorks,就不再面试其他公司了”。 - -奈何 ThoughtWorks 的进度太慢,担心之余,才来武汉面试其他公司留个保底。 - -不过,我最终如愿以偿获得了 ThoughtWorks 的 offer。 - -![](https://oss.javaguide.cn/about-the-author/college-life/9ad97dcc5038499b96239dd826c471b7~tplv-k3u1fbpfcp-zoom-1.image.png) - -面试 ThoughtWorks 的过程就不多说了,我在[《结束了我短暂的秋招,说点自己的感受》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484842&idx=1&sn=4489dfab0ef2479122b71407855afc71&chksm=cea24a61f9d5c3774a8ed67c5fcc3234cb0741fbe831152986e5d1c8fb4f36a003f4fb2f247e&scene=178&cur_album_id=1323354342556057602#rd)这篇文章中有提到。 - -## 几点建议 - -说几点自己的建议,虽然我不优秀,但毕竟你可以更优秀: - -1. 确定好自己的方向,搞清你是要考研还是要找工作。如果你要考研的话,好好上每一门可能是考研的科目,平时有时间也要敲代码,最好也能做一个项目,对你复试还有能力提升都有帮助。找工作的话,尽早确定好自己的方向,心里有一个规划,搞清自己的优势和劣势。 -2. 尽可能早一点以求职为导向来学习,这样更有针对性,并且可以大概率减己处在迷茫的时间,很大程度上还可以让自己少走很多弯路。 -3. 自学很重要,养成自学的习惯,学会学习。 -4. 不要觉得逃课就是坏学生。我大学逃了很多课,逃课的大部分时间都是在学自己觉得更重要的东西,逃的大部分也是不那么重要并且不会影响我毕业的课。 -5. 大学恋爱还是相对来说很纯粹的,遇到合适的可以尝试去了解一下, 别人不喜欢你的话不要死缠烂打,这种东西强求不来。你不得不承认,你了解一个人欲望还是始于他的长相而并不是有趣的灵魂。 -6. 管理自己的身材,没事去跑跑步,别当油腻男。 -7. 别太看重绩点。我觉得绩点对于找工作还有考研实际的作用都可以忽略不计,不过不挂科还是比较重要的。但是,绩点确实在奖学金评选和保研名额选取上占有最大的分量。 -8. 别太功利性。做事情以及学习知识都不要奢求它能立马带给你什么,坚持和功利往往是成反比的。 -9. …… - -## 后记 - -我们在找工作的过程中难免会遇到卡学历的情况,特别是我们这种学校本身就比较一般的。我觉得这真的不可厚非,没有什么不公平,要怪就只能怪自己没有考上好的学校。 - -**考虑到招聘成本和时间,公司一定更愿意在学校本身比较好的人中选拔人才。** - -我也曾抱怨过自己为什么不在 211 或者 985 的学校。但,其实静下心来想一想,本来考不上 211 或者 985 就是自己的问题,而且在我们计算机这个领域,学历本身就相对于其他专业稍微要更加公平一点。 - -我身边专科、三本毕业就进大厂的人也比比皆是。我这句话真不是鸡汤,为了鼓励一些学校出身不太好的朋友。 - -**多行动,少抱怨。** +Although these experiences didn't help my future work diff --git a/docs/about-the-author/writing-technology-blog-six-years.md b/docs/about-the-author/writing-technology-blog-six-years.md index 9e18a67d8c4..b3e1ea67153 100644 --- a/docs/about-the-author/writing-technology-blog-six-years.md +++ b/docs/about-the-author/writing-technology-blog-six-years.md @@ -1,173 +1,171 @@ --- -title: 坚持写技术博客六年了! -category: 走近作者 +title: I've been writing a technical blog for six years! +category: Meet the Author tag: - - 杂谈 + - Miscellaneous --- -坚持写技术博客已经有六年了,也算是一个小小的里程碑了。 +I have been坚持写技术博客已经有六年了,也算是一个小小的里程碑了。 -一开始,我写技术博客就是简单地总结自己课堂上学习的课程比如网络、操作系统。渐渐地,我开始撰写一些更为系统化的知识点详解和面试常见问题总结。 +At first, I wrote technical blogs just to summarize the courses I learned in class, such as networking and operating systems. Gradually, I started writing more systematic knowledge point explanations and summaries of common interview questions. -![JavaGuide 首页](https://oss.javaguide.cn/about-the-author/college-life/image-20230408131717766.png) +![JavaGuide Homepage](https://oss.javaguide.cn/about-the-author/college-life/image-20230408131717766.png) -许多人都想写技术博客,但却不清楚这对他们有何好处。有些人开始写技术博客,却不知道如何坚持下去,也不知道该写些什么。这篇文章我会认真聊聊我对记录技术博客的一些看法和心得,或许可以帮助你解决这些问题。 +Many people want to write technical blogs but are unclear on the benefits it brings them. Some people begin writing technical blogs but don’t know how to persevere or what to write about. In this article, I will seriously discuss my views and insights on recording technical blogs, which may help you address these issues. -## 写技术博客有哪些好处? +## What are the benefits of writing a technical blog? -### 学习效果更好,加深知识点的认识 +### Better learning outcomes and deepening knowledge -**费曼学习法** 大家应该已经比较清楚了,这是一个经过实践证明非常有效的学习方式。费曼学习法的命名源自 Richard Feynman,这位物理学家曾获得过诺贝尔物理学奖,也曾参与过曼哈顿计划。 +You should already be familiar with the **Feynman Technique**, which is a very effective learning method that has been proven through practice. Named after the physicist Richard Feynman, who won a Nobel Prize in Physics and participated in the Manhattan Project, the Feynman Technique involves teaching a newly learned knowledge point as if you were a teacher: using the simplest and most straightforward words to explain complex and obscure knowledge, ideally avoiding technical jargon so that even those outside the field can understand it. To achieve this effect, imagine you are teaching an 80-year-old or an 8-year-old child. -所谓费曼学习法,就是当你学习了一个新知识之后,想象自己是一个老师:用最简单、最浅显直白的话复述、表达复杂深奥的知识,最好不要使用行业术语,让非行业内的人也能听懂。为了达到这种效果,最好想象你是在给一个 80 多岁或 8 岁的小孩子上课,甚至他们都能听懂。 +![Teaching others improves learning outcomes](https://oss.javaguide.cn/about-the-author/college-life/v2-19373c2e61873c5083ee4b1d1523f8f5_720w.png) -![教授别人学习效果最好](https://oss.javaguide.cn/about-the-author/college-life/v2-19373c2e61873c5083ee4b1d1523f8f5_720w.png) +Reading books or watching videos falls into passive learning, which is less effective. The Feynman Technique is an active learning method with excellent results. -看书、看视频这类都属于是被动学习,学习效果比较差。费曼学习方法属于主动学习,学习效果非常好。 +**Writing a technical blog is essentially a way to teach others.** However, when recording a technical blog, it is okay to use technical terms (unless your audience is non-technical), but you need to express it in your own words, making it easy for others to understand. **Avoid copying from books or directly pasting other people's summaries!** -**写技术博客实际就是教别人的一种方式。** 不过,记录技术博客的时候是可以有专业术语(除非你的文章群体是非技术人员),只是你需要用自己的话表述出来,尽量让别人一看就懂。**切忌照搬书籍或者直接复制粘贴其他人的总结!** +If we passively learn a knowledge point, most of the time we are only satisfied with the level of being able to use it; we do not delve into its principles, and many key concepts may not be fully understood. -如果我们被动的学习某个知识点,可能大部分时候都是仅仅满足自己能够会用的层面,你并不会深究其原理,甚至很多关键概念都没搞懂。 +When summarizing what you've learned into a blog, it will certainly deepen your thoughts on that knowledge point. Many times, in order to clearly explain a knowledge point, you will go back and research a lot of materials and even check a lot of source code. These small accumulations subtly enhance your understanding of the subject. -如果你是要将你所学到的知识总结成一篇博客的话,一定会加深你对这个知识点的思考。很多时候,你为了将一个知识点讲清楚,你回去查阅很多资料,甚至需要查看很多源码,这些细小的积累在潜移默化中加深了你对这个知识点的认识。 +In fact, I often encounter this situation: **During the process of writing a blog, I suddenly realize that my understanding of a certain knowledge point is mistaken.** -甚至,我还经常会遇到这种情况:**写博客的过程中,自己突然意识到自己对于某个知识点的理解存在错误。** +**Writing a blog is itself a process of summarizing, reviewing, and reflecting on what you have learned. Documenting your blog is also a record of your learning journey. As time goes by and you grow older, is this not also a valuable mental asset?** -**写博客本身就是一个对自己学习到的知识进行总结、回顾、思考的过程。记录博客也是对于自己学习历程的一种记录。随着时间的流逝、年龄的增长,这又何尝不是一笔宝贵的精神财富呢?** +A friend from Knowledge Planet also mentioned that writing technical blogs helps improve one's knowledge system: -知识星球的一位球友还提到写技术博客有助于完善自己的知识体系: +![Writing technical blogs helps improve one's knowledge system](https://oss.javaguide.cn/about-the-author/college-life/image-20230408121336432.png) -![写技术博客有助于完善自己的知识体系](https://oss.javaguide.cn/about-the-author/college-life/image-20230408121336432.png) +### Helping others while gaining a sense of accomplishment -### 帮助别人的同时获得成就感 +Just like how we programmers hope our products can be recognized and liked by everyone, we also write technical blogs in part to gain recognition from others. -就像我们程序员希望自己的产品能够得到大家的认可和喜欢一样。我们写技术博客在某一方面当然也是为了能够得到别人的认可。 +**When what you write helps others, you will feel a sense of achievement and happiness.** -**当你写的东西对别人产生帮助的时候,你会产生成就感和幸福感。** +![Reader recognition](https://oss.javaguide.cn/about-the-author/college-life/image-20230404181906257.png) -![读者的认可](https://oss.javaguide.cn/about-the-author/college-life/image-20230404181906257.png) +This sense of achievement and happiness serves as **positive feedback**, continuing to motivate you to write blogs. -这种成就感和幸福感会作为 **正向反馈** ,继续激励你写博客。 +However, even when receiving appreciation from many readers, you must remain humble and diligent in learning. There are many readers whose skills surpass yours, so stay open to learning! -但是,即使受到很多读者的赞赏,也要保持谦虚学习的太多。人外有人,比你技术更厉害的读者多了去,一定要虚心学习! +Of course, you might receive a lot of criticism. Some may say your articles lack depth; others might comment that you are just idle, writing things that can be found online or in books. -当然,你可以可能会受到很多非议。可能会有很多人说你写的文章没有深度,还可能会有很多人说你闲的蛋疼,你写的东西网上/书上都有。 +**Face these criticisms calmly, do your own thing, and stay true to your path! Prove yourself through action!** -**坦然对待这些非议,做好自己,走好自己的路就好!用行动自证!** +### There may be additional income -### 可能会有额外的收入 +Writing a blog might also bring you financial rewards. It's the best scenario to provide value while also earning reasonable income! -写博客可能还会为你带来经济收入。输出价值的同时,还能够有合理的经济收入,这是最好的状态! +Why do I say it may be? **Because currently, most people still find it difficult to earn income from blogging in the short term. I also don’t suggest that everyone starts writing blogs with the goal of making money; this kind of utilitarian approach might actually have the opposite effect. For example, if you persist for half a year and find no earnings, you may end up giving up.** -为什么说是可能呢? **因为就目前来看,大部分人还是很难短期通过写博客有收入。我也不建议大家一开始写博客就奔着赚钱的目的,这样功利性太强了,效果可能反而不好。就比如说你坚持了写了半年发现赚不到钱,那你可能就会坚持不下去了。** +I started writing blogs in my sophomore year. In the second semester of my junior year, I began publishing my articles on WeChat public accounts, and it wasn't until the second semester of my senior year that I earned my first income from blogging. -我自己从大二开始写博客,大三下学期开始将自己的文章发布到公众号上,一直到大四下学期,才通过写博客赚到属于自己的第一笔钱。 +My first income was earned through a promotional collaboration with a training institution on WeChat. If I remember correctly, this promotion brought me about **500** yuan. Though it's not a lot, it was very precious for me as a college student. At that time, I realized that writing could indeed be lucrative, which motivated me even more to share my writing. Unfortunately, after accepting two advertisements from this training institution, it went out of business. -第一笔钱是通过微信公众号接某培训机构的推广获得的。没记错的话,当时通过这个推广为自己带来了大约 **500** 元的收入。虽然这不是很多,但对于还在上大学的我来说,这笔钱非常宝贵。那时我才知道,原来写作真的可以赚钱,这也让我更有动力去分享自己的写作。可惜的是,在接了两次这家培训机构的广告之后,它就倒闭了。 +After that, I went a long time without receiving any advertisements. Until NetEase reached out for a course collaboration, paying **1,000 yuan per article**, and I wrote nearly one article a month for almost two years. This became a relatively stable source of income for me during college. -之后,很长一段时间我都没有接到过广告。直到网易的课程合作找上门,一篇文章 1000 元,每个月接近一篇,发了接近两年,这也算是我在大学期间比较稳定的一份收入来源了。 +![NetEase course collaboration](https://oss.javaguide.cn/about-the-author/college-life/image-20230408115720135.png) -![网易的课程合作](https://oss.javaguide.cn/about-the-author/college-life/image-20230408115720135.png) +Most of my old followers probably got to know me through the JavaGuide project, which I started preparing for job hunting interviews in my junior year. I didn't expect this project to become quite popular, even topping the GitHub rankings. Perhaps at that time, there were too few similar open-source documentation tutorial projects in the country, so the project's popularity was very high. -老粉应该大部分都是通过 JavaGuide 这个项目认识我的,这是我在大三开始准备秋招面试时创建的一个项目。没想到这个项目竟然火了一把,一度霸占了 GitHub 榜单。可能当时国内这类开源文档教程类项目太少了,所以这个项目受欢迎程度非常高。 +![JavaGuide Star Trends](https://oss.javaguide.cn/about-the-author/college-life/image-20230408131849198.png) -![JavaGuide Star 趋势](https://oss.javaguide.cn/about-the-author/college-life/image-20230408131849198.png) +After the project gained traction, a large cloud service company in China contacted me, saying they wanted to sponsor the JavaGuide project. I was both surprised and delighted, worried that it might be a scam, and after confirming the contract multiple times, we ultimately agreed to add their company's banner to my project homepage for a monthly fee of 1,000 yuan. -项目火了之后,有一个国内比较大的云服务公司找到我,说是要赞助 JavaGuide 这个项目。我既惊又喜,担心别人是骗子,反复确认合同之后,最终确定以每月 1000 元的费用在我的项目首页加上对方公司的 banner。 +As time went on, and after I wrote some popular articles that appealed to a wider audience, my blog’s visibility and income from blogging significantly increased as well. -随着时间的推移,以及自己后来写了一些比较受欢迎、比较受众的文章,我的博客知名度也有所提升,通过写博客的收入也增加了不少。 +### Increasing personal influence -### 增加个人影响力 +Writing a technical blog is a way to showcase your technical skills and experience, allowing more people to understand your professional knowledge and skills. Consistently sharing high-quality technical articles will undoubtedly increase your personal influence in the tech field. -写技术博客是一种展示自己技术水平和经验的方式,能够让更多的人了解你的专业领域知识和技能。持续分享优质的技术文章,一定能够在技术领域增加个人影响力,这一点是毋庸置疑的。 +Having personal influence can be very helpful when looking for jobs, engaging in paid knowledge sharing, or publishing books later on. -有了个人影响力之后,不论是对你后面找工作,还是搞付费知识分享或者出书,都非常有帮助。 +Speaking of which, many well-known publishers have contacted me to discuss the writing of a book. Such opportunities are likely what many people dream of. However, I have turned them down one by one, feeling that I am far from being capable enough to write a book. -拿我自己来说,已经很多知名出版社的编辑找过我,协商出一本的书的事情。这种机会应该也是很多人梦寐以求的。不过,我都一一拒绝了,因为觉得自己远远没有达到能够写书的水平。 +![Invitation from Electronics Industry Press to publish a book](https://oss.javaguide.cn/about-the-author/college-life/image-20230408121132211.png) -![电子工业出版社编辑邀约出书](https://oss.javaguide.cn/about-the-author/college-life/image-20230408121132211.png) +The main reason I don’t want to publish a book is that I find the whole process cumbersome; there is too much to handle. I tend to be a more laid-back person, and I don’t want to devote all my time to work. -其实不出书最主要的原因还是自己嫌麻烦,整个流程的事情太多了。我自己又是比较佛系随性的人,平时也不想把时间都留给工作。 +## How can I persist in writing a technical blog? -## 怎样才能坚持写技术博客? +**It is undeniable that people can be lazy by nature. We need a goal or motivation to push ourselves.** -**不可否认,人都是有懒性的,这是人的本性。我们需要一个目标/动力来 Push 一下自己。** +For technical writing, your goals can be based on the quantity of technical articles, such as: -就技术写作而言,你的目标可以以技术文章的数量为标准,比如: +- How many technical articles to write in a year. I personally feel that setting goals over the timeframe of a year is too long and makes it hard to find a suitable goal. +- Outputting one high-quality technical article per month. This is relatively easier to achieve; one article a month means you will have twelve articles in a year, which is quite good. -- 一年写多少篇技术文章。我个人觉得一年的范围还是太长了,不太容易定一个比较合适的目标。 -- 每月输出一篇高质量的技术文章。这个相对容易实现一些,每月一篇,一年也有十二篇了,也很不错了。 +However, setting goals based on the quantity of technical articles can be a bit utilitarian, and the quality of the articles is equally important. A high-quality technical article may take a week or even half a month of spare time to complete. You must avoid deliberately pursuing quantity while neglecting quality and losing sight of the essence of technical writing. -不过,以技术文章的数量为目标有点功利化,文章的质量同样很重要。一篇高质量的技术文可能需要花费一周甚至半个月的业余时间才能写完。一定要避免自己刻意追求数量,而忽略质量,迷失技术写作的本心。 +I have set a personal goal: **to write at least one original technical article or seriously revise and improve three past articles each month** (articles such as recommendations for open-source projects, learning experiences, personal experience sharing, interview experience sharing, etc., will not be counted). -我个人给自己定的目标是:**每个月至少写一篇原创技术文章或者认真修改完善过去写的三篇技术文章** (像开源项目推荐、开源项目学习、个人经验分享、面经分享等等类型的文章不会被记入)。 - -我的目标对我来说比较容易完成,因此不会出现为了完成目标而应付任务的情况。在我状态比较好,工作也不是很忙的时候,还会经常超额完成任务。下图是我今年 3 月份完成的任务(任务管理工具:Microsoft To-Do)。除了 gossip 协议是去年写的之外,其他都是 3 月份完成的。 +My goal is relatively easy for me to complete, so I won’t brush off tasks just to meet the target. When I am in a good state and work isn’t too busy, I often exceed my tasks. The following image shows the tasks I completed in March of this year (task management tool: Microsoft To-Do). Aside from the gossip protocol written last year, the rest were completed in March. ![](https://oss.javaguide.cn/about-the-author/college-life/image-20230404181033089.png) -如果觉得以文章数量为标准过于功利的话,也可以比较随性地按照自己的节奏来写作。不过,一般这种情况下,你很可能过段时间就忘了还有这件事,开始慢慢抵触写博客。 +If you feel that setting goals based on the number of articles is too utilitarian, you can write according to your own pace. However, in general, this approach may cause you to forget about it over time and lead to a growing aversion to blogging. -写完一篇技术文章之后,我们不光要同步到自己的博客,还要分发到国内一些常见的技术社区比如博客园、掘金。**分发到其他平台的原因是获得关注进而收获正向反馈(动力来源之一)与建议,这是技术写作能坚持下去的非常重要的一步,一定要重视!!!** +After finishing a technical article, we not only need to synchronize it to our blog but also distribute it to some common domestic technical communities like Blog Garden and Juejin. **The reason for distributing to other platforms is to gain attention and receive positive feedback (one source of motivation) and suggestions, which is a crucial step for persisting in technical writing; it must be emphasized!!!** -说实话,当你写完一篇自认为还不错的文章的幸福感和成就感还是有的。**但是,让自己去做这件事情还是比较痛苦的。** 就好比你让自己出去玩很简单,为了达到这个目的,你可以有各种借口。但是,想要自己老老实实学习,还是需要某个外力来督促自己的。 +To be honest, there is a sense of happiness and accomplishment when you finish writing an article that you believe is decent. **However, pushing yourself to do this can be quite painful.** It’s like getting yourself to go out and have fun is easy; you can come up with various excuses to achieve that goal. But it requires external pressure to keep yourself dedicated to learning. -## 写哪些方向的博客比较好? +## What are some good directions to write blogs about? -通常来说,写下面这些方向的博客会比较好: +In general, writing about the following directions will be more beneficial: -1. **详细讲解某个知识点**:一定要有自己的思考而不是东拼西凑。不仅要介绍知识点的基本概念和原理,还需要适当结合实际案例和应用场景进行举例说明。 -2. **问题排查/性能优化经历**:需要详细描述清楚具体的场景以及解决办法。一定要有足够的细节描述,包括出现问题的具体场景、问题的根本原因、解决问题的思路和具体步骤等等。同时,要注重实践性和可操作性,帮助读者更好地学习理解。 -3. **源码阅读记录**:从一个功能点出发描述其底层源码实现,谈谈你从源码中学到了什么。 +1. **Detailed explanation of a knowledge point**: It's essential to have your own thoughts rather than piecing together ideas. You need to introduce the basic concepts and principles of the knowledge point while integrating real-life cases and application scenarios for illustration. +1. **Problem troubleshooting/performance optimization experiences**: You need to detail the specific scenarios and solutions. It’s important to include sufficient descriptive details, such as the exact circumstances in which the problem arose, the root cause of the issue, and the thought process and specific steps that led to a solution. At the same time, focus on practicality and operability to help the reader learn and understand better. +1. **Source code reading notes**: Describe the low-level source code implementation starting from a functional point and discuss what you have learned from the source code. -最重要的是一定要重视 Markdown 规范,不然内容再好也会显得不专业。 +Most importantly, pay attention to Markdown standards; otherwise, even the best content will appear unprofessional. -详见 [Markdown 规范](../javaguide/contribution-guideline.md) (很重要,尽量按照规范来,对你工作中写文档会非常有帮助) +For details, see [Markdown Standards](../javaguide/contribution-guideline.md) (very important, try to follow the standards; it will greatly help with writing documentation in your work) -## 有没有什么写作技巧分享? +## Are there any writing tips to share? -### 句子不要过长 +### Keep sentences concise -句子不要过长,尽量使用短句(但也不要太短),这样读者更容易阅读和理解。 +Avoid long sentences; try to use short sentences (but don’t make them too short), as this makes them easier for readers to read and understand. -### 尽量让文章更加生动有趣 +### Make the article more engaging and interesting -尽量让文章更加生动有趣,比如你可以适当举一些形象的例子、用一些有趣的段子、歇后语或者网络热词。 +Try to make the article more vivid and interesting by incorporating illustrative examples, fun anecdotes, idioms, or trendy internet phrases. -不过,这个也主要看你的文章风格。 +However, this mainly depends on your writing style. -### 使用简单明了的语言 +### Use simple and clear language -避免使用阅读者可能无法理解的行话或复杂语言。 +Avoid jargon or complex language that readers may not understand. -注重清晰度和说服力,保持简单。简单的写作是有说服力的,一个五句话的好论点会比一百句话的精彩论点更能打动人。为什么格言、箴言这类文字容易让人接受,与简洁、直白也有些关系。 +Focus on clarity and persuasiveness; keep it simple. Simple writing is persuasive—a good five-sentence argument will be more impactful than a hundred sentences of brilliant points. This is partly why proverbs and aphorisms are easily accepted: they are succinct and straightforward. -### 使用视觉效果 +### Use visual effects -图表、图像等视觉效果可以让朴素的文本内容更容易理解。记得在适当的地方使用视觉效果来增强你的文章的表现力。 +Charts, images, and other visual effects can make plain text content easier to understand. Remember to use visual elements appropriately to enhance the expressiveness of your article. ![](https://oss.javaguide.cn/about-the-author/college-life/image-20230404192458759.png) -### 技术文章配图色彩要鲜明 +### Make technical article visuals bright -下面是同样内容的两张图,都是通过 drawio 画的,小伙伴们更喜欢哪一张呢? +Below are two images with the same content, both created with draw.io. Which one do you prefer? -我相信大部分小伙伴都会选择后面一个色彩更鲜明的! +I believe most people will choose the latter, which has brighter colors! -色彩的调整不过花费了我不到 30s 的时间,带来的阅读体验的上升却是非常之大! +Adjusting the colors took me less than 30 seconds, yet the improvement in reading experience was substantial! ![](https://oss.javaguide.cn/2021-1/image-20210104182517226.png) -### 确定你的读者 +### Identify your audience -写作之前,思考一下你的文章的主要受众全体是谁。受众群体确定之后,你可以根据受众的需求和理解水平调整你的写作风格和内容难易程度。 +Before writing, consider who your article's main audience is. Once you determine your audience, adjust your writing style and the difficulty of your content based on their needs and comprehension level. -### 审查和修改 +### Review and edit -在发表之前一定要审查和修改你的文章。这将帮助你发现错误、澄清任何令人困惑的信息并提高文档的整体质量。 +Make sure to review and edit your article before publishing it. This will help you catch errors, clarify any confusing information, and improve the overall quality of your document. -**好文是改出来的,切记!!!** +**Good articles come from revisions, don’t forget!!!** -## 总结 +## Conclusion -总的来说,写技术博客是一件利己利彼的事情。你可能会从中收获到很多东西,你写的东西也可能对别人也有很大的帮助。但是,写技术博客还是比较耗费自己时间的,你需要和工作以及生活做好权衡。 +In summary, writing a technical blog is beneficial to both yourself and others. You may gain a lot from it, and what you write can also be of significant help to others. However, writing a technical blog can be quite time-consuming, and you need to balance it with work and life. diff --git a/docs/about-the-author/zhishixingqiu-two-years.md b/docs/about-the-author/zhishixingqiu-two-years.md index 644478b455a..7ff96c50b86 100644 --- a/docs/about-the-author/zhishixingqiu-two-years.md +++ b/docs/about-the-author/zhishixingqiu-two-years.md @@ -1,148 +1,73 @@ --- -title: 我的知识星球 4 岁了! -category: 知识星球 +title: My Knowledge Planet is 4 Years Old! +category: Knowledge Planet star: 2 --- -在 **2019 年 12 月 29 号**,经过了大概一年左右的犹豫期,我正式确定要开始做一个自己的星球,帮助学习 Java 和准备 Java 面试的同学。一转眼,已经四年多了。感谢大家一路陪伴,我会信守承诺,继续认真维护这个纯粹的 Java 知识星球,不让信任我的读者失望。 +On **December 29, 2019**, after about a year of hesitation, I officially decided to start my own planet to help students learn Java and prepare for Java interviews. In the blink of an eye, it has been over four years. Thank you all for your support along the way. I will keep my promise and continue to diligently maintain this pure Java knowledge planet, ensuring that my readers who trust me are not disappointed. ![](https://oss.javaguide.cn/xingqiu/640-20230727145252757.png) -我是比较早一批做星球的技术号主,也是坚持做下来的那一少部人(大部分博主割一波韭菜就不维护星球了)。最开始的一两年,纯粹靠爱发电。当初定价非常低(一顿饭钱),加上刚工作的时候比较忙,提供的服务也没有现在这么多。 +I was among the early batch of technical account holders to create a planet, and I am also one of the few who have persisted (most bloggers cash out quickly and stop maintaining their planets). In the first couple of years, it was purely driven by passion. The initial pricing was very low (the cost of a meal), and since I was busy with my job, the services provided were not as extensive as they are now. -慢慢的价格提上来,星球的收入确实慢慢也上来了。不过,考虑到我的受众主要是学生,定价依然比同类星球低很多。另外,我也没有弄训练营的打算,虽然训练营对于我这个流量来说可以赚到更多钱。 +Gradually, the price increased, and the income from the planet has indeed risen slowly. However, considering that my audience mainly consists of students, the pricing is still much lower than similar planets. Additionally, I have no plans to create a training camp, even though a training camp could earn me more money given my traffic. -**我有自己的原则,不割韭菜,用心做内容,真心希望帮助到他人!** +**I have my principles: I won't exploit anyone, I focus on creating quality content, and I sincerely hope to help others!** -## 什么是知识星球? +## What is a Knowledge Planet? -简单来说,知识星球就是一个私密交流圈子,主要用途是知识创作者连接铁杆读者/粉丝。相比于微信群,知识星球内容沉淀、信息管理更高效。 +In simple terms, a Knowledge Planet is a private communication circle, primarily used for knowledge creators to connect with their loyal readers/fans. Compared to WeChat groups, the content on a Knowledge Planet is more organized and information management is more efficient. ![](https://oss.javaguide.cn/xingqiu/image-20220211223754566.png) -## 我的知识星球能为你提供什么? +## What can my Knowledge Planet offer you? -努力做一个最优质的 Java 面试交流星球!加入到我的星球之后,你将获得: +I strive to create the highest quality Java interview exchange planet! After joining my planet, you will receive: -1. 6 个高质量的专栏永久阅读,内容涵盖面试,源码解析,项目实战等内容! -2. 多本原创 PDF 版本面试手册免费领取。 -3. 免费的简历修改服务(已经累计帮助 7000+ 位球友修改简历)。 -4. 一对一免费提问交流(专属建议,走心回答)。 -5. 专属求职指南和建议,让你少走弯路,效率翻倍! -6. 海量 Java 优质面试资源分享。 -7. 打卡活动,读书交流,学习交流,让学习不再孤单,报团取暖。 -8. 不定期福利:节日抽奖、送书送课、球友线下聚会等等。 -9. …… +1. Permanent access to 6 high-quality columns covering interview topics, source code analysis, project practice, and more! +1. Free access to multiple original PDF interview handbooks. +1. Free resume modification service (having helped over 7000 members modify their resumes). +1. One-on-one free Q&A (personalized advice, heartfelt answers). +1. Exclusive job-seeking guides and suggestions to help you avoid detours and double your efficiency! +1. A wealth of high-quality Java interview resources. +1. Check-in activities, book discussions, and study exchanges to make learning less lonely and foster community. +1. Irregular benefits: holiday lotteries, book and course giveaways, offline gatherings for members, etc. +1. …… -其中的任何一项服务单独拎出来价值都远超星球门票了。 +Each of these services alone is worth far more than the planet's admission fee. -这里再送一个 **30** 元的星球专属优惠券吧,数量有限(价格即将上调。老用户续费半价 ,微信扫码即可续费)! +Here’s a **30** yuan exclusive coupon for the planet, limited quantity (prices will soon increase. Existing users can renew at half price by scanning the WeChat QR code)! -![知识星球30元优惠卷](https://oss.javaguide.cn/xingqiu/xingqiuyouhuijuan-30.jpg) +![Knowledge Planet 30 Yuan Coupon](https://oss.javaguide.cn/xingqiu/xingqiuyouhuijuan-30.jpg) -### 专属专栏 +### Exclusive Columns -星球更新了 **《Java 面试指北》**、**《Java 必读源码系列》**(目前已经整理了 Dubbo 2.6.x、Netty 4.x、SpringBoot2.1 的源码)、 **《从零开始写一个 RPC 框架》**(已更新完)、**《Kafka 常见面试题/知识点总结》** 等多个优质专栏。 +The planet has updated several high-quality columns including **"Java Interview Guide"**, **"Must-Read Source Code Series"** (currently organized for Dubbo 2.6.x, Netty 4.x, SpringBoot2.1), **"Writing an RPC Framework from Scratch"** (fully updated), and **"Common Kafka Interview Questions/Knowledge Summary"**. ![](https://oss.javaguide.cn/xingqiu/image-20220211231206733.png) -《Java 面试指北》内容概览: +Overview of **"Java Interview Guide"**: ![](https://oss.javaguide.cn/xingqiu/image-20220304102536445.png) -进入星球之后,这些专栏即可免费永久阅读,永久同步更新! +Once you join the planet, these columns will be available for free permanent reading and will be continuously updated! -### PDF 面试手册 +### PDF Interview Handbooks -进入星球就免费赠送多本优质 PDF 面试手册。 +Upon joining the planet, you will receive multiple high-quality PDF interview handbooks for free. -![星球 PDF 面试手册](https://oss.javaguide.cn/xingqiu/image-20220723120918434.png) +![Planet PDF Interview Handbooks](https://oss.javaguide.cn/xingqiu/image-20220723120918434.png) -### 优质精华主题沉淀 +### Quality Thematic Content -星球沉淀了几年的优质精华主题,内容涵盖面经、面试题、工具网站、技术资源、程序员进阶攻略等内容,干货非常多。 +The planet has accumulated high-quality thematic content over the years, covering interview experiences, interview questions, tool websites, technical resources, and programmer advancement strategies, with a wealth of valuable information. ![](https://oss.javaguide.cn/xingqiu/image-20230421154518800.png) -并且,每个月都会整理出当月优质的主题,方便大家阅读学习,避免错过优质的内容。毫不夸张,单纯这些优质主题就足够门票价值了。 +Additionally, each month, I will compile the best themes of the month for easy reading and learning, ensuring you don't miss out on quality content. It’s no exaggeration to say that these quality themes alone are worth the admission fee. -![星球每月优质主题整理概览](https://oss.javaguide.cn/xingqiu/image-20230902091117181.png) +![Monthly Quality Theme Overview](https://oss.javaguide.cn/xingqiu/image-20230902091117181.png) -加入星球之后,一定要记得抽时间把星球精华主题看看,相信你一定会有所收货! - -JavaGuide 知识星球优质主题汇总传送门:(为了避免这里成为知识杂货铺,我会对严格筛选入选的优质主题)。 - -![星球优质主题汇总](https://oss.javaguide.cn/xingqiu/Xnip2023-04-21_15-48-13.png) - -### 简历修改 - -一到面试季,我平均一天晚上至少要看 15 ~30 份简历。过了面试季的话,找我看简历的话会稍微少一些。要不然的话,是真心顶不住! - -![](https://oss.javaguide.cn/xingqiu/image-20220304123156348.png) - -简单统计了一下,到目前为止,我至少帮助 **7000+** 位球友提供了免费的简历修改服务。 - -![](https://oss.javaguide.cn/xingqiu/%E7%AE%80%E5%8E%86%E4%BF%AE%E6%94%B92.jpg) - -我会针对每一份简历给出详细的修改完善建议,用心修改,深受好评! - -![](https://oss.javaguide.cn/xingqiu/image-20220725093504807.png) - -### 一对一提问 - -你可以和我进行一对一免费提问交流,我会很走心地回答你的问题。到目前为止,已经累计回答了 **3000+** 个读者的提问。 - -![](https://oss.javaguide.cn/xingqiu/wecom-temp-151578-45e66ccd48b3b5d3baa8673d33c7b664.jpg) - -![](https://oss.javaguide.cn/xingqiu/image-20220211223559179.png) - -### 学习打卡 - -星球的学习打卡活动可以督促自己和其他球友们一起学习交流。 - -![](https://oss.javaguide.cn/xingqiu/image-20220308143815840.png) - -看球友们的打卡也能有收货,最重要的是这个学习氛围对于自己自律非常有帮助! - -![](https://oss.javaguide.cn/xingqiu/%E7%90%83%E5%8F%8B%E6%AF%8F%E6%97%A5%E6%89%93%E5%8D%A1%E4%B9%9F%E8%83%BD%E5%AD%A6%E5%88%B0%E5%BE%88%E5%A4%9A%E4%B8%9C%E8%A5%BF.jpg) - -![](https://oss.javaguide.cn/xingqiu/%E7%A1%AE%E5%AE%9E%E6%98%AF%E5%AD%A6%E4%B9%A0%E4%BA%A4%E6%B5%81%E7%9A%84%E5%A5%BD%E5%9C%B0%E6%96%B9.jpg) - -### 读书活动 - -定期会举办读书活动(奖励丰厚),我会带着大家一起读一些优秀的技术书籍! - -![](https://oss.javaguide.cn/xingqiu/image-20220211233642079.png) - -每一期读书活动的获奖率都非常非常非常高!直接超过门票价!!! - -### 不定时福利 - -不定时地在星球送书、送专栏、发红包,福利多多, - -![](https://oss.javaguide.cn/xingqiu/1682063464099.png) - -## 是否收费? - -星球是需要付费才能进入的。 **为什么要收费呢?** - -1. 维护好星球是一件费时费力的事情,每到面试季,我经常凌晨还在看简历和回答球友问题。市面上单单一次简历修改服务也至少需要 200+,而简历修改也只是我的星球提供的服务的冰山一角。除此之外,我还要抽时间写星球专属的一些专栏,单单是这些专栏的价值就远超星球门票了。 -2. 星球提供的服务比较多,如果我是免费提供这些服务的话,是肯定忙不过来的。付费这个门槛可以帮我筛选出真正需要帮助的那批人。 -3. 免费的东西才是最贵的,加入星球之后无任何其他需要付费的项目,统统免费! -4. 合理的收费是对我付出劳动的一种正向激励,促进我继续输出!同时,这份收入还可以让我们家人过上更好的生活。虽然累点,但也是值得的! - -另外,这个是一年的,到明年这个时候结束,差不过够用了。如果服务结束的时候你还需要星球服务的话,可以添加我的微信(**javaguide1024**)领取一个续费优惠卷,半价基础再减 10,记得备注 **“续费”** 。 - -## 如何加入? - -这里赠送一个 **30** 元的星球专属优惠券吧,数量有限(价格即将上调。老用户续费半价 ,微信扫码即可续费)! - -![知识星球30元优惠卷](https://oss.javaguide.cn/xingqiu/xingqiuyouhuijuan-30.jpg) - -进入星球之后,记得查看 **[星球使用指南](https://t.zsxq.com/0d18KSarv)** (一定要看!!!) 和 **[星球优质主题汇总](https://t.zsxq.com/12uSKgTIm)** 。 - -**无任何套路,无任何潜在收费项。用心做内容,不割韭菜!** - -不过, **一定要确定需要再进** 。并且, **三天之内觉得内容不满意可以全额退款** 。 +After joining the planet, be sure to take some diff --git a/docs/books/README.md b/docs/books/README.md index 700a7ea0e3e..ff1106ef666 100644 --- a/docs/books/README.md +++ b/docs/books/README.md @@ -1,21 +1,21 @@ --- -title: 技术书籍精选 -category: 计算机书籍 +title: Selected Technical Books +category: Computer Books --- -精选优质计算机书籍。 +A selection of high-quality computer books. -开源的目的是为了大家能一起完善,如果你觉得内容有任何需要完善/补充的地方,欢迎大家在项目 [issues 区](https://github.com/CodingDocs/awesome-cs/issues) 推荐自己认可的技术书籍,让我们共同维护一个优质的技术书籍精选集! +The purpose of this open-source project is to allow everyone to contribute to its improvement. If you think there are any aspects that need enhancement or additions, feel free to recommend your recognized technical books in the [issues section](https://github.com/CodingDocs/awesome-cs/issues) of the project, so we can jointly maintain a high-quality collection of technical books! -- GitHub 地址:[https://github.com/CodingDocs/awesome-cs](https://github.com/CodingDocs/awesome-cs) -- Gitee 地址:[https://gitee.com/SnailClimb/awesome-cs](https://gitee.com/SnailClimb/awesome-cs) +- GitHub address: [https://github.com/CodingDocs/awesome-cs](https://github.com/CodingDocs/awesome-cs) +- Gitee address: [https://gitee.com/SnailClimb/awesome-cs](https://gitee.com/SnailClimb/awesome-cs) -如果内容对你有帮助的话,欢迎给本项目点个 Star。我会用我的业余时间持续完善这份书单,感谢! +If the content is helpful to you, please give this project a Star. I will continue to improve this reading list in my spare time, thank you! -## 公众号 +## Official Account -最新更新会第一时间同步在公众号,推荐关注!另外,公众号上有很多干货不会同步在线阅读网站。 +The latest updates will be synchronized to the official account as soon as they are available, so we recommend following it! Additionally, there are many valuable resources on the official account that will not be shared on online reading sites. -![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) +![JavaGuide Official Account](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) diff --git a/docs/books/cs-basics.md b/docs/books/cs-basics.md index e67ac115964..8300f697d56 100644 --- a/docs/books/cs-basics.md +++ b/docs/books/cs-basics.md @@ -1,274 +1,43 @@ --- -title: 计算机基础必读经典书籍 -category: 计算机书籍 -icon: "computer" +title: Must-Read Classic Books on Computer Fundamentals +category: Computer Books +icon: computer head: - - - meta - - name: keywords - content: 计算机基础书籍精选 + - - meta + - name: keywords + content: Selected Computer Fundamentals Books --- -考虑到很多同学比较喜欢看视频,因此,这部分内容我不光会推荐书籍,还会顺便推荐一些我觉得不错的视频教程和各大高校的 Project。 +Considering that many students prefer watching videos, in this section, I will not only recommend books but also some video tutorials and projects from major universities that I think are great. -## 操作系统 +## Operating Systems -**为什么要学习操作系统?** +**Why learn about operating systems?** -**从对个人能力方面提升来说**,操作系统中的很多思想、很多经典的算法,你都可以在我们日常开发使用的各种工具或者框架中找到它们的影子。比如说我们开发的系统使用的缓存(比如 Redis)和操作系统的高速缓存就很像。CPU 中的高速缓存有很多种,不过大部分都是为了解决 CPU 处理速度和内存处理速度不对等的问题。我们还可以把内存可以看作外存的高速缓存,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。同样地,我们使用的 Redis 缓存就是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。高速缓存一般会按照局部性原理(2-8 原则)根据相应的淘汰算法保证缓存中的数据是经常会被访问的。我们平常使用的 Redis 缓存很多时候也会按照 2-8 原则去做,很多淘汰算法都和操作系统中的类似。既说了 2-8 原则,那就不得不提命中率了,这是所有缓存概念都通用的。简单来说也就是你要访问的数据有多少能直接在缓存中直接找到。命中率高的话,一般表明你的缓存设计比较合理,系统处理速度也相对较快。 +**From the perspective of personal skill enhancement**, many concepts and classic algorithms in operating systems can be found in the various tools or frameworks we use in daily development. For example, the caching used in the systems we develop (like Redis) is very similar to the cache in operating systems. There are many types of caches in the CPU, but most are designed to address the disparity between CPU processing speed and memory processing speed. We can also think of memory as a high-speed cache for external storage; when a program runs, we copy data from external storage to memory, which significantly speeds up processing due to the much higher processing speed of memory compared to external storage. Similarly, the Redis cache we use is designed to solve the problem of the disparity between program processing speed and the speed of accessing conventional relational databases. Caches generally ensure that the data in them is frequently accessed according to the principle of locality (the 2-8 principle) and corresponding eviction algorithms. The Redis cache we commonly use often follows the 2-8 principle, and many eviction algorithms are similar to those in operating systems. Speaking of the 2-8 principle, we must mention the hit rate, which is a common concept for all caches. Simply put, it refers to how much of the data you want to access can be found directly in the cache. A high hit rate generally indicates that your cache design is reasonable, and the system's processing speed is relatively fast. -**从面试角度来说**,尤其是校招,对于操作系统方面知识的考察是非常非常多的。 +**From the interview perspective**, especially for campus recruitment, there is a significant emphasis on knowledge of operating systems. -**简单来说,学习操作系统能够提高自己思考的深度以及对技术的理解力,并且,操作系统方面的知识也是面试必备。** +**In short, learning about operating systems can deepen your thinking and understanding of technology, and knowledge in this area is essential for interviews.** -如果你要系统地学习操作系统的话,最硬核最权威的书籍是 **[《操作系统导论》](https://book.douban.com/subject/33463930/)** 。你可以再配套一个 **[《深入理解计算机系统》](https://book.douban.com/subject/1230413/)** 加深你对计算机系统本质的认识,美滋滋! +If you want to systematically learn about operating systems, the most authoritative and hardcore book is **[“Operating System Concepts”](https://book.douban.com/subject/33463930/)**. You can also pair it with **[“Computer Systems: A Programmer's Perspective”](https://book.douban.com/subject/1230413/)** to deepen your understanding of the essence of computer systems, which is delightful! ![](https://oss.javaguide.cn/github/javaguide/booksimage-20201012191645919.png) -另外,去年新出的一本国产的操作系统书籍也很不错:**[《现代操作系统:原理与实现》](https://book.douban.com/subject/35208251/)** (夏老师和陈老师团队的力作,值得推荐)。 +Additionally, a new domestic book on operating systems released last year is also quite good: **[“Modern Operating Systems: Principles and Implementation”](https://book.douban.com/subject/35208251/)** (a collaborative work by Professor Xia and Professor Chen's team, highly recommended). ![](https://oss.javaguide.cn/github/javaguide/books/20210406132050845.png) -如果你比较喜欢动手,对于理论知识比较抵触的话,我推荐你看看 **[《30 天自制操作系统》](https://book.douban.com/subject/11530329/)** ,这本书会手把手教你编写一个操作系统。 +If you prefer hands-on experience and are resistant to theoretical knowledge, I recommend you check out **[“30 Days to Create Your Own Operating System”](https://book.douban.com/subject/11530329/)**, which will guide you step by step in writing an operating system. -纸上学来终觉浅 绝知此事要躬行!强烈推荐 CS 专业的小伙伴一定要多多实践!!! +Learning from books alone is never enough; practical experience is essential! I strongly recommend that CS majors engage in more hands-on practice!!! ![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409123802972.png) -其他相关书籍推荐: +Other related book recommendations: -- **[《自己动手写操作系统》](https://book.douban.com/subject/1422377/)**:不光会带着你详细分析操作系统原理的基础,还会用丰富的实例代码,一步一步地指导你用 C 语言和汇编语言编写出一个具备操作系统基本功能的操作系统框架。 -- **[《现代操作系统》](https://book.douban.com/subject/3852290/)**:内容很不错,不过,翻译的一般。如果你是精读本书的话,建议把课后习题都做了。 -- **[《操作系统真象还原》](https://book.douban.com/subject/26745156/)**:这本书的作者毕业于北京大学,前百度运维高级工程师。因为在大学期间曾重修操作系统这一科,后对操作系统进行深入研究,著下此书。 -- **[《深度探索 Linux 操作系统》](https://book.douban.com/subject/25743846/)**:跟着这本书的内容走,可以让你对如何制作一套完善的 GNU/Linux 系统有了清晰的认识。 -- **[《操作系统设计与实现》](https://book.douban.com/subject/2044818/)**:操作系统的权威教学教材。 -- **[《Orange'S:一个操作系统的实现》](https://book.douban.com/subject/3735649/)**:从只有二十行的引导扇区代码出发,一步一步地向读者呈现一个操作系统框架的完成过程。配合《操作系统设计与实现》一起食用更佳! - -如果你比较喜欢看视频的话,推荐哈工大李治军老师主讲的慕课 [《操作系统》](https://www.icourse163.org/course/HIT-1002531008),内容质量吊打一众国家精品课程。 - -课程的大纲如下: - -![课程大纲](https://oss.javaguide.cn/github/javaguide/books/image-20220414144527747.png) - -主要讲了一个基本操作系统中的六个基本模块:CPU 管理、内存管理、外设管理、磁盘管理与文件系统、用户接口和启动模块 。 - -课程难度还是比较大的,尤其是课后的 lab。如果大家想要真正搞懂操作系统底层原理的话,对应的 lab 能做尽量做一下。正如李治军老师说的那样:“纸上得来终觉浅,绝知此事要躬行”。 - -![](https://oss.javaguide.cn/github/javaguide/books/image-20220414145210679.png) - -如果你能独立完成几个 lab 的话,我相信你对操作系统的理解绝对要上升几个台阶。当然了,如果你仅仅是为了突击面试的话,那就不需要做 lab 了。 - -说点心里话,我本人非常喜欢李治军老师讲的课,我觉得他是国内不可多得的好老师。他知道我们国内的教程和国外的差距在哪里,也知道国内的学生和国外学生的差距在哪里,他自己在努力着通过自己的方式来缩小这个差距。真心感谢,期待李治军老师的下一个课程。 - -![](https://oss.javaguide.cn/github/javaguide/books/image-20220414145249714.png) - -还有下面这个国外的课程 [《深入理解计算机系统 》](https://www.bilibili.com/video/av31289365?from=search&seid=16298868573410423104) 也很不错。 - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20201204140653318.png) - -## 计算机网络 - -计算机网络是一门系统性比较强的计算机专业课,各大名校的计算机网络课程打磨的应该都比较成熟。 - -要想学好计算机网络,首先要了解的就是 OSI 七层模型或 TCP/IP 五层模型,即应用层(应用层、表示层、会话层)、传输层、网络层、数据链路层、物理层。 - -![osi七层模型](https://oss.javaguide.cn/github/javaguide/booksosi%E4%B8%83%E5%B1%82%E6%A8%A1%E5%9E%8B2.png) - -关于这门课,首先强烈推荐参考书是**机械工业出版社的《计算机网络——自顶向下方法》**。该书目录清晰,按照 TCP/IP 五层模型逐层讲解,对每层涉及的技术都展开了详细讨论,基本上高校里开设的课程的教学大纲就是这本书的目录了。 - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409123250570.png) - -如果你觉得上面这本书看着比较枯燥的话,我强烈推荐+安利你看看下面这两本非常有趣的网络相关的书籍: - -- [《图解 HTTP》](https://book.douban.com/subject/25863515/ "《图解 HTTP》"):讲漫画一样的讲 HTTP,很有意思,不会觉得枯燥,大概也涵盖也 HTTP 常见的知识点。因为篇幅问题,内容可能不太全面。不过,如果不是专门做网络方向研究的小伙伴想研究 HTTP 相关知识的话,读这本书的话应该来说就差不多了。 -- [《网络是怎样连接的》](https://book.douban.com/subject/26941639/ "《网络是怎样连接的》"):从在浏览器中输入网址开始,一路追踪了到显示出网页内容为止的整个过程,以图配文,讲解了网络的全貌,并重点介绍了实际的网络设备和软件是如何工作的。 - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20201011215144139.png) - -除了理论知识之外,学习计算机网络非常重要的一点就是:“**动手实践**”。这点和我们编程差不多。 - -GitHub 上就有一些名校的计算机网络试验/Project: - -- [哈工大计算机网络实验](https://github.com/rccoder/HIT-Computer-Network) -- [《计算机网络-自顶向下方法(原书第 6 版)》编程作业,Wireshark 实验文档的翻译和解答。](https://github.com/moranzcw/Computer-Networking-A-Top-Down-Approach-NOTES) -- [计算机网络的期末 Project,用 Python 编写的聊天室](https://github.com/KevinWang15/network-pj-chatroom) -- [CMU 的计算机网络课程](https://computer-networks.github.io/sp19/lectures.html) - -我知道,还有很多小伙伴可能比较喜欢边看视频边学习。所以,我这里再推荐几个顶好的计算机网络视频讲解。 - -**1、[哈工大的计算机网络课程](http://www.icourse163.org/course/HIT-154005)**:国家精品课程,截止目前已经开了 10 次课了。大家对这门课的评价都非常高!所以,非常推荐大家看一下! - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20201218141241911.png) - -**2、[王道考研的计算机网络](https://www.bilibili.com/video/BV19E411D78Q?from=search&seid=17198507506906312317)**:非常适合 CS 专业考研的小朋友!这个视频目前在哔哩哔哩上已经有 1.6w+ 的点赞。 - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20201218141652837.png) - -## 算法 - -先来看三本入门书籍。 这三本入门书籍中的任何一本拿来作为入门学习都非常好。 - -1. [《我的第一本算法书》](https://book.douban.com/subject/30357170/) -2. [《算法图解》](https://book.douban.com/subject/26979890/) -3. [《啊哈!算法》](https://book.douban.com/subject/25894685/) - -![](https://oss.javaguide.cn/java-guide-blog/image-20210327104418851.png) - -我个人比较倾向于 **[《我的第一本算法书》](https://book.douban.com/subject/30357170/)** 这本书籍,虽然它相比于其他两本书集它的豆瓣评分略低一点。我觉得它的配图以及讲解是这三本书中最优秀,唯一比较明显的问题就是没有代码示例。但是,我觉得这不影响它是一本好的算法书籍。因为本身下面这三本入门书籍的目的就不是通过代码来让你的算法有多厉害,只是作为一本很好的入门书籍让你进入算法学习的大门。 - -再推荐几本比较经典的算法书籍。 - -**[《算法》](https://book.douban.com/subject/19952400/)** - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409123422140.png) - -这本书内容非常清晰易懂,适合数据结构和算法小白阅读。书中把一些常用的数据结构和算法都介绍到了! - -我在大二的时候被我们的一个老师强烈安利过!自己也在当时购买了一本放在宿舍,到离开大学的时候自己大概看了一半多一点。因为内容实在太多了!另外,这本书还提供了详细的 Java 代码,非常适合学习 Java 的朋友来看,可以说是 Java 程序员的必备书籍之一了。 - -> **下面这些书籍都是经典中的经典,但是阅读起来难度也比较大,不做太多阐述,神书就完事了!** -> -> **如果你仅仅是准备算法面试的话,不建议你阅读下面这些书籍。** - -**[《编程珠玑》](https://book.douban.com/subject/3227098/)** - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409145334093.png) - -经典名著,ACM 冠军、亚军这种算法巨佬都强烈推荐的一本书籍。这本书的作者也非常厉害,Java 之父 James Gosling 就是他的学生。 - -很多人都说这本书不是教你具体的算法,而是教你一种编程的思考方式。这种思考方式不仅仅在编程领域适用,在其他同样适用。 - -**[《算法设计手册》](https://book.douban.com/subject/4048566/)** - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409145411049.png) - -这是一本被 GitHub 上的爆火的计算机自学项目 [Teach Yourself Computer Science](https://link.zhihu.com/?target=https%3A//teachyourselfcs.com/) 强烈推荐的一本算法书籍。 - -类似的神书还有 [《算法导论》](https://book.douban.com/subject/20432061/)、[《计算机程序设计艺术(第 1 卷)》](https://book.douban.com/subject/1130500/) 。 - -**如果说你要准备面试的话,下面这几本书籍或许对你有帮助!** - -**[《剑指 Offer》](https://book.douban.com/subject/6966465/)** - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409145506482.png) - -这本面试宝典上面涵盖了很多经典的算法面试题,如果你要准备大厂面试的话一定不要错过这本书。 - -《剑指 Offer》 对应的算法编程题部分的开源项目解析:[CodingInterviews](https://link.zhihu.com/?target=https%3A//github.com/gatieme/CodingInterviews) 。 - -**[《程序员代码面试指南(第 2 版)》](https://book.douban.com/subject/30422021/)** - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409145622758.png) - -《程序员代码面试指南(第 2 版)》里的大部分题目相比于《剑指 offer》 来说要难很多,题目涵盖面相比于《剑指 offer》也更加全面。全书一共有将近 300 道真实出现过的经典代码面试题。 - -视频的话,推荐北京大学的国家精品课程—**[程序设计与算法(二)算法基础](https://www.icourse163.org/course/PKU-1001894005)**,讲的非常好! - -![](https://oss.javaguide.cn/github/javaguide/books/22ce4a17dc0c40f6a3e0d58002261b7a.png) - -这个课程把七种基本的通用算法(枚举、二分、递归、分治、动态规划、搜索、贪心)都介绍到了。各种复杂算法问题的解决,都可能用到这些基本的思想。并且,这个课程的一部分的例题和 ACM 国际大学生程序设计竞赛中的中等题相当,如果你能够解决这些问题,那你的算法能力将超过绝大部分的高校计算机专业本科毕业生。 - -## 数据结构 - -其实,上面提到的很多算法类书籍(比如 **《算法》** 和 **《算法导论》**)都详细地介绍了常用的数据结构。 - -我这里再另外补充基本和数据结构相关的书籍。 - -**[《大话数据结构》](https://book.douban.com/subject/6424904/)** - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409145803440.png) - -入门类型的书籍,读起来比较浅显易懂,适合没有数据结构基础或者说数据结构没学好的小伙伴用来入门数据结构。 - -**[《数据结构与算法分析:Java 语言描述》](https://book.douban.com/subject/3351237/)** - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409145823973.png) - -质量很高,介绍了常用的数据结构和算法。 - -类似的还有 **[《数据结构与算法分析:C 语言描述》](https://book.douban.com/subject/1139426/)**、**[《数据结构与算法分析:C++ 描述》](https://book.douban.com/subject/1971825/)** - -![](https://oss.javaguide.cn/github/javaguide/books/d9c450ccc5224a5fba77f4fa937f7b9c.png) - -视频的话推荐你看浙江大学的国家精品课程—**[《数据结构》](https://www.icourse163.org/course/ZJU-93001#/info)** 。 - -姥姥的数据结构讲的非常棒!不过,还是有一些难度的,尤其是课后练习题。 - -## 计算机专业基础课 - -数学和英语属于通用课,一般在大一和大二两学年就可以全部修完,大二大三逐渐接触专业课。通用课作为许多高中生升入大学的第一门课,算是高中阶段到本科阶段的一个过渡,从职业生涯重要性上来说,远不及专业课重要,但是在本科阶段的学习生活规划中,有着非常重要的地位。由于通用课的课程多,学分重,占据了本科阶段绩点的主要部分,影响到学生在前两年的专业排名,也影响到大三结束时的推免资格分配,也就是保研。而从升学角度来看,对于攻读研究生和博士生的小伙伴来说,数学和英语这两大基础课,还是十分有用的。 - -### 数学 - -#### 微积分(高等数学) - -微积分,即传说中的高数,成为了无数新大一心中的痛。但好在,大学的课程考核没那么严格,期末想要拿高分,也不至于像高中那样刷题刷的那么狠。微积分对于计算机专业学生的重要性,主要体现在计算机图形学中的函数变换,机器学习中的梯度算法,信号处理等领域。 - -微积分的知识体系包括微分和积分两部分,一般会先学微分,再学积分,也有的学校把高数分为两个学期。微分就是高中的导数的升级版,对于大一萌新来说还算比较友好。积分恰好是微分的逆运算,思想上对大一萌新来说比较新,一时半会可能接受不了。不过这门课所有的高校都有开设,而且大部分的名校都有配套的网课,教材也都打磨的非常出色,结合网课和教材的“啃书”学习模式,这门课一定不会落下。 - -书籍的话,推荐《普林斯顿微积分读本》。这本书详细讲解了微积分基础、极限、连续、微分、导数的应用、积分、无穷级数、泰勒级数与幂级数等内容。 - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409155056751.png) - -#### 线性代数(高等代数) - -线性代数的思维模式就更加复杂了一些,它定义了一个全新的数学世界,所有的符号、定理都是全新的,唯一能尝试的去理解的方式,大概就是用几何的方式去理解线性代数了。由于线性代数和几何学有着密不可分的关系,比如空间变换的理论支撑就是线性代数,因此,网上有着各种“可视化学习线性代数”的学习资源,帮助理解线性代数的意义,有助于公式的记忆。 - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409153940473.png) - -书籍的话,推荐中科大李尚志老师的 **[《线性代数学习指导》](https://book.douban.com/subject/26390093/)** 。 - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409155325251.png) - -#### 概率论与数理统计 - -对于计算机专业的小伙伴来说,这门课可能是概率论更有用一点,而非数理统计。可能某些学校只开设概率论课程,也可能数理统计也教,但仅仅是皮毛。概率论的学习路线和微积分相似,就是一个个公式辅以实例,不像线性代数那么抽象,比较贴近生活。在现在的就业形势下,概率论与数理统计专业的学生,应该是数学专业最好就业的了,他们通常到岗位上会做一些数据分析的工作,因此,**这门课程确实是数据分析的重要前置课程,概率论在机器学习中的重要性也就不言而喻了。** - -书籍的话,推荐 **[《概率论与数理统计教程》](https://book.douban.com/subject/34897672/)** 。这本书共八章,前四章为概率论部分,主要叙述各种概率分布及其性质,后四章为数理统计部分,主要叙述各种参数估计与假设检验。 - -![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409155738505.png) - -#### 离散数学(集合论、图论、近世代数等) - -离散数学是计算机专业的专属数学,但实际上对于本科毕业找工作的小伙伴来说,离散数学还并没有发挥它的巨大作用。离散数学的作用主要在在图研究等领域,理论性极强,需要读研深造的小伙伴尽可能地扎实掌握。 - -### 英语 - -英语算是大学里面比较灵活的一项技能了,有的人会说,“英语学的越好,对个人发展越有利”,此话说的没错,但是对于一些有着明确发展目标的小伙伴,可能英语技能并不在他们的技能清单内。接下来的这些话只针对计算机专业的小伙伴们哦。 - -英语课在大学本科一般只有前两年开设,小伙伴们可以记住,**想用英语课来提升自己的英语水平的,可以打消这个念头了。** 英语水平的提高全靠自己平时的积累和练习,以及有针对性的刷题。 - -**英语的大学四六级一定要过。** 这是必备技能,绝大部分就业岗位都要看四六级水平的,最起码要通过的。四级比高中英语稍微难一些,一般的小伙伴可能会卡在六级上,六级需要针对性的训练一下,因为大学期间能接触英语的实在太少了,每学期一门英语课是不足以保持自己的英语水平的。对于一些来自于偏远地区,高中英语基础薄弱的,考四六级会更加吃力。建议考前集中训练一下历年真题,辅以背一下高频词汇,四六级通过只需要 425 分,这个分数线还是比较容易达到的。稍微好一点的小伙伴可能冲一下 500 分,要是能考到 600 分的话,那是非常不错的水平了,算是简历上比较有亮点的一项。 - -英语的雅思托福考试只限于想要出国的小伙伴,以及应聘岗位对英语能力有特殊要求的。雅思托福考试裸考不容易通过,花钱去比较靠谱的校外补课班应该是一个比较好的选择。 - -对于计算机专业的小伙伴来说,英语能力还是比较重要的,虽然应聘的时候不会因为没有雅思托福成绩卡人,但是你起码要能够: - -- **熟练使用英文界面的软件、系统等** -- **对于外网的一些博客、bug 解决方案等,阅读无压力** -- **熟练阅读英文文献** -- **具备一定的英文论文的撰写能力** - -毕竟计算机语言就是字符语言,听说读写中最起码要满足**读写**这两项不过分吧。 - -### 编译原理 - -编译原理相比于前面介绍的专业课,地位显得不那么重要了。编译原理的重要性主要体现在: - -- 底层语言、引擎或高级语言的开发,如 MySQL,Java 等 -- 操作系统或嵌入式系统的开发 -- 词法、语法、语义的思想,以及自动机思想 - -**编译原理的重要前置课程就是形式语言与自动机,自动机的思想在词法分析当中有着重要应用,学习了这门课后,应该就会发现许多场景下,自动机算法的妙用了。** - -总的来说,这门课对于各位程序员的职业发展来说,相对不那么重要,但是从难度上来说,学习这门课可以对编程思想有一个较好的巩固。学习资源的话,除了课堂上的幻灯片课件以外,还可以把 《编译原理》 这本书作为参考书,用以辅助自己学不懂的地方(大家口中的龙书,想要啃下来还是有一定难度的)。 - -![](https://oss.javaguide.cn/github/javaguide/books/20210406152148373.png) - -其他书籍推荐: - -- **[《现代编译原理》](https://book.douban.com/subject/30191414/)**:编译原理的入门书。 -- **[《编译器设计》](https://book.douban.com/subject/20436488/)**:覆盖了编译器从前端到后端的全部主题。 - -我上面推荐的书籍的难度还是比较高的,真心很难坚持看完。这里强烈推荐[哈工大的编译原理视频课程](https://www.icourse163.org/course/HIT-1002123007),真心不错,还是国家精品课程,关键还是又漂亮有温柔的美女老师讲的! - -![](https://oss.javaguide.cn/github/javaguide/books/20210406152847824.png) +- **[“Operating Systems: Design and Implementation”](https://book.douban.com/subject/1422377/)**: This book not only provides a detailed analysis of the principles of operating systems but also guides you step by step with rich example code to write an operating system framework with basic functionalities using C and assembly language. +- **[“Modern Operating Systems”](https://book.douban.com/subject/3852290/)**: The content is quite good, but the translation is average. If you plan to read this book thoroughly, it is recommended to complete the exercises at the end of each chapter. +- **[“Operating Systems: Three Easy Pieces”](https://book.douban.com/subject/26745156/)**: The author of this book graduated from Peking University and was a senior engineer at Baidu. After retaking the operating systems course in college, he conducted in-depth research on operating systems and wrote this book. +- **[“Deep Dive into Linux Operating System”](https://book.douban.com/subject/25743846/)**: Following the content of this book will give you a clear understanding of how to create a complete GNU/Linux system. +- **[“Operating System Design and Implementation”](https://book.douban.com/subject/2044818/)**: An authoritative textbook on operating diff --git a/docs/books/database.md b/docs/books/database.md index 87f92d24184..2e07e5543dc 100644 --- a/docs/books/database.md +++ b/docs/books/database.md @@ -1,106 +1,106 @@ --- -title: 数据库必读经典书籍 -category: 计算机书籍 -icon: "database" +title: Must-Read Classic Books on Databases +category: Computer Books +icon: database head: - - - meta - - name: keywords - content: 数据库书籍精选 + - - meta + - name: keywords + content: Selected Database Books --- -## 数据库基础 +## Database Fundamentals -数据库基础这块,如果你觉得书籍比较枯燥,自己坚持不下来的话,我推荐你可以先看看一些不错的视频,北京师范大学的[《数据库系统原理》](https://www.icourse163.org/course/BNU-1002842007)、哈尔滨工业大学的[《数据库系统(下):管理与技术》](https://www.icourse163.org/course/HIT-1001578001)就很不错。 +If you find the books on database fundamentals quite dry and cannot stick with it, I recommend that you first look at some good videos, such as Beijing Normal University's [“Principles of Database Systems”](https://www.icourse163.org/course/BNU-1002842007) and Harbin Institute of Technology's [“Database Systems (Part 2): Management and Technology”](https://www.icourse163.org/course/HIT-1001578001). -[《数据库系统原理》](https://www.icourse163.org/course/BNU-1002842007)这个课程的老师讲的非常详细,而且每一小节的作业设计的也与所讲知识很贴合,后面还有很多配套实验。 +The instructor for [“Principles of Database Systems”](https://www.icourse163.org/course/BNU-1002842007) is very detailed, and the assignments for each section are closely related to the knowledge being taught, with many supplementary experiments afterwards. ![](https://oss.javaguide.cn/github/javaguide/books/up-e113c726a41874ef5fb19f7ac14e38e16ce.png) -如果你比较喜欢动手,对于理论知识比较抵触的话,推荐你看看[《如何开发一个简单的数据库》](https://cstack.github.io/db_tutorial/) ,这个 project 会手把手教你编写一个简单的数据库。 +If you prefer hands-on activities and resist theoretical knowledge, I recommend you check out [“How to Develop a Simple Database”](https://cstack.github.io/db_tutorial/); this project will guide you step by step in writing a simple database. ![](https://oss.javaguide.cn/github/javaguide/books/up-11de8cb239aa7201cc8d78fa28928b9ec7d.png) -GitHub 上也已经有大佬用 Java 实现过一个简易的数据库,介绍的挺详细的,感兴趣的朋友可以去看看。地址:[https://github.com/alchemystar/Freedom](https://github.com/alchemystar/Freedom) 。 +There is also a simplified database implemented in Java on GitHub by some experts, which is quite detailed, and those interested can take a look. Address: [https://github.com/alchemystar/Freedom](https://github.com/alchemystar/Freedom). -除了这个用 Java 写的之外,**[db_tutorial](https://github.com/cstack/db_tutorial)** 这个项目是国外的一个大佬用 C 语言写的,朋友们也可以去瞅瞅。 +Aside from the Java implementation, **[db_tutorial](https://github.com/cstack/db_tutorial)** is a project by an expert abroad that is written in C language; friends can check that out as well. -**只要利用好搜索引擎,你可以找到各种语言实现的数据库玩具。** +**As long as you make good use of search engines, you can find various language implementations of database toys.** ![](https://oss.javaguide.cn/github/javaguide/books/up-d32d853f847633ac7ed0efdecf56be1f1d2.png) -**纸上学来终觉浅 绝知此事要躬行!强烈推荐 CS 专业的小伙伴一定要多多实践!!!** +**Learning from books alone is shallow; you must practice to fully understand! I strongly recommend that friends majoring in CS should practice more!!!** -### 《数据库系统概念》 +### "Database System Concepts" -[《数据库系统概念》](https://book.douban.com/subject/10548379/)这本书涵盖了数据库系统的全套概念,知识体系清晰,是学习数据库系统非常经典的教材!不是参考书! +[“Database System Concepts”](https://book.douban.com/subject/10548379/) covers the complete set of concepts related to database systems. The knowledge system is clear and it is a very classic textbook for learning database systems! It is not a reference book! ![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409150441742.png) -### 《数据库系统实现》 +### "Database System Implementation" -如果你也想要研究 MySQL 底层原理的话,我推荐你可以先阅读一下[《数据库系统实现》](https://book.douban.com/subject/4838430/)。 +If you are interested in researching the underlying principles of MySQL, I recommend you read [“Database System Implementation”](https://book.douban.com/subject/4838430/) first. ![](https://oss.javaguide.cn/github/javaguide/books/database-system-implementation.png) -不管是 MySQL 还是 Oracle ,它们总体的架子是差不多的,不同的是其内部的实现比如数据库索引的数据结构、存储引擎的实现方式等等。 +Whether it’s MySQL or Oracle, their overall framework is quite similar; the differences are mainly in their internal implementations—such as the data structures used for database indexing and the implementation methods of the storage engines. -这本书有些地方还是翻译的比较蹩脚,有能力看英文版的还是建议上手英文版。 +This book is somewhat awkward in translation in certain places, so if you can read the English version, it is recommended to start with that. -《数据库系统实现》 这本书是斯坦福的教材,另外还有一本[《数据库系统基础教程》](https://book.douban.com/subject/3923575/)是前置课程,可以带你入门数据库。 +“Database System Implementation” is a textbook from Stanford, and there's also a precursor course book, [“Database System Fundamentals”](https://book.douban.com/subject/3923575/), which can introduce you to databases. ## MySQL -我们网站或者 APP 的数据都是需要使用数据库来存储数据的。 +The data for our website or APP needs to be stored in a database. -一般企业项目开发中,使用 MySQL 比较多。如果你要学习 MySQL 的话,可以看下面这 3 本书籍: +In general, MySQL is often used in enterprise project development. If you want to learn MySQL, you can refer to the following three books: -- **[《MySQL 必知必会》](https://book.douban.com/subject/3354490/)**:非常薄!非常适合 MySQL 新手阅读,很棒的入门教材。 -- **[《高性能 MySQL》](https://book.douban.com/subject/23008813/)**:MySQL 领域的经典之作!学习 MySQL 必看!属于进阶内容,主要教你如何更好地使用 MySQL 。既有有理论,又有实践!如果你没时间都看一遍的话,我建议第 5 章(创建高性能的索引)、第 6 章(查询性能优化) 你一定要认真看一下。 -- **[《MySQL 技术内幕》](https://book.douban.com/subject/24708143/)**:你想深入了解 MySQL 存储引擎的话,看这本书准没错! +- **[“MySQL Must-Know Must-Read”](https://book.douban.com/subject/3354490/)**: Very thin! Extremely suitable for beginners learning MySQL; it is a great introductory textbook. +- **[“High-Performance MySQL”](https://book.douban.com/subject/23008813/)**: A classic work in the MySQL field! A must-read for learning MySQL! It belongs to the advanced content and mainly teaches you how to use MySQL better. It contains both theory and practice! If you don’t have time to read it all, I suggest you carefully review Chapter 5 (Creating High-Performance Indexes) and Chapter 6 (Query Performance Optimization). +- **[“MySQL Technical Insider”](https://book.douban.com/subject/24708143/)**: If you want to delve into the MySQL storage engine, this book is definitely the right choice! ![](https://oss.javaguide.cn/github/javaguide/books/up-3d31e762933f9e50cc7170b2ebd8433917b.png) -视频的话,你可以看看动力节点的 [《MySQL 数据库教程视频》](https://www.bilibili.com/video/BV1fx411X7BD)。这个视频基本上把 MySQL 的相关一些入门知识给介绍完了。 +For videos, you can check out Donglijian's [“MySQL Database Tutorial Video”](https://www.bilibili.com/video/BV1fx411X7BD). This video basically covers all the introductory information related to MySQL. -另外,强推一波 **[《MySQL 是怎样运行的》](https://book.douban.com/subject/35231266/)** 这本书,内容很适合拿来准备面试。讲的很细节,但又不枯燥,内容非常良心! +Additionally, I strongly recommend **[“How MySQL Works”](https://book.douban.com/subject/35231266/)**; the content is very suitable for interview preparation. It discusses many details without being boring, and the material is outstanding! ![](https://oss.javaguide.cn/github/javaguide/csdn/20210703120643370.png) ## PostgreSQL -和 MySQL 一样,PostgreSQL 也是开源免费且功能强大的关系型数据库。PostgreSQL 的 Slogan 是“**世界上最先进的开源关系型数据库**” 。 +Like MySQL, PostgreSQL is also an open-source, free and powerful relational database. PostgreSQL's slogan is “**The World's Most Advanced Open Source Relational Database**”. ![](https://oss.javaguide.cn/github/javaguide/books/image-20220702144954370.png) -最近几年,由于 PostgreSQL 的各种新特性过于优秀,使用 PostgreSQL 代替 MySQL 的项目越来越多了。 +In recent years, due to various excellent new features of PostgreSQL, more and more projects are replacing MySQL with PostgreSQL. -如果你还在纠结是否尝试一下 PostgreSQL 的话,建议你看看这个知乎话题:[PostgreSQL 与 MySQL 相比,优势何在? - 知乎](https://www.zhihu.com/question/20010554) 。 +If you are still unsure whether to try PostgreSQL, I suggest you check out this Zhihu topic: [What are the advantages of PostgreSQL compared to MySQL? - Zhihu](https://www.zhihu.com/question/20010554). -### 《PostgreSQL 指南:内幕探索》 +### "PostgreSQL Guide: Inside Exploration" -[《PostgreSQL 指南:内幕探索》](https://book.douban.com/subject/33477094/)这本书主要介绍了 PostgreSQL 内部的工作原理,包括数据库对象的逻辑组织与物理实现,进程与内存的架构。 +[“PostgreSQL Guide: Inside Exploration”](https://book.douban.com/subject/33477094/) mainly introduces the internal working principles of PostgreSQL, including the logical organization and physical implementation of database objects, as well as the architecture of processes and memory. -刚工作那会需要用到 PostgreSQL ,看了大概 1/3 的内容,感觉还不错。 +When I first started working, I needed to use PostgreSQL and read about 1/3 of the content, which felt quite good. ![](https://oss.javaguide.cn/github/javaguide/books/PostgreSQL-Guide.png) -### 《PostgreSQL 技术内幕:查询优化深度探索》 +### "PostgreSQL Technical Insider: In-Depth Exploration of Query Optimization" -[《PostgreSQL 技术内幕:查询优化深度探索》](https://book.douban.com/subject/30256561/)这本书主要讲了 PostgreSQL 在查询优化上的一些技术实现细节,可以让你对 PostgreSQL 的查询优化器有深层次的了解。 +[“PostgreSQL Technical Insider: In-Depth Exploration of Query Optimization”](https://book.douban.com/subject/30256561/) mainly discusses some technical implementation details regarding query optimization in PostgreSQL, allowing you to gain a deeper understanding of the PostgreSQL query optimizer. -![《PostgreSQL 技术内幕:查询优化深度探索》](https://oss.javaguide.cn/github/javaguide/books/PostgreSQL-TechnologyInsider.png) +![“PostgreSQL Technical Insider: In-Depth Exploration of Query Optimization”](https://oss.javaguide.cn/github/javaguide/books/PostgreSQL-TechnologyInsider.png) ## Redis -**Redis 就是一个使用 C 语言开发的数据库**,不过与传统数据库不同的是 **Redis 的数据是存在内存中的** ,也就是它是内存数据库,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向。 +**Redis is a database developed in C language**, but unlike traditional databases, **the data of Redis is stored in memory**, meaning it is an in-memory database and thus has very fast read and write speeds. As a result, Redis is widely used in caching. -如果你要学习 Redis 的话,强烈推荐下面这两本书: +If you want to learn Redis, I highly recommend the following two books: -- [《Redis 设计与实现》](https://book.douban.com/subject/25900156/) :主要是 Redis 理论知识相关的内容,比较全面。我之前写过一篇文章 [《7 年前,24 岁,出版了一本 Redis 神书》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247507030&idx=1&sn=0a5fd669413991b30163ab6f5834a4ad&chksm=cea1939df9d61a8b93925fae92f4cee0838c449534e60731cfaf533369831192e296780b32a6&token=709354671&lang=zh_CN&scene=21#wechat_redirect) 来介绍这本书。 -- [《Redis 核心原理与实践》](https://book.douban.com/subject/26612779/):主要是结合源码来分析 Redis 的重要知识点比如各种数据结构和高级特性。 +- [“Redis Design and Implementation”](https://book.douban.com/subject/25900156/): This primarily covers theoretical knowledge related to Redis and is quite comprehensive. I wrote an article before [“Seven Years Ago, at 24, I Published a Redis Classic”](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247507030&idx=1&sn=0a5fd669413991b30163ab6f5834a4ad&chksm=cea1939df9d61a8b93925fae92f4cee0838c449534e60731cfaf533369831192e296780b32a6&token=709354671&lang=zh_CN&scene=21#wechat_redirect) to introduce this book. +- [“Redis Core Principles and Practices”](https://book.douban.com/subject/26612779/): Mainly analyzes important knowledge points of Redis through source code, such as various data structures and advanced features. -![《Redis 设计与实现》和《Redis 设计与实现》](https://oss.javaguide.cn/github/javaguide/books/redis-books.png) +![“Redis Design and Implementation” and “Redis Core Principles and Practices”](https://oss.javaguide.cn/github/javaguide/books/redis-books.png) -另外,[《Redis 开发与运维》](https://book.douban.com/subject/26971561/) 这本书也非常不错,既有基础介绍,又有一线开发运维经验分享。 +Additionally, [“Redis Development and Operations”](https://book.douban.com/subject/26971561/) is also quite good, offering both basic introductions and frontline development and operations experience sharing. -![《Redis 开发与运维》](https://oss.javaguide.cn/github/javaguide/books/redis-kaifa-yu-yunwei.png) +![“Redis Development and Operations”](https://oss.javaguide.cn/github/javaguide/books/redis-kaifa-yu-yunwei.png) diff --git a/docs/books/distributed-system.md b/docs/books/distributed-system.md index bb131d6dd65..8c5fff6e0aa 100644 --- a/docs/books/distributed-system.md +++ b/docs/books/distributed-system.md @@ -1,85 +1,85 @@ --- -title: 分布式必读经典书籍 -category: 计算机书籍 -icon: "distributed-network" +title: Must-Read Classic Books on Distributed Systems +category: Computer Books +icon: distributed-network --- -## 《深入理解分布式系统》 +## "In-Depth Understanding of Distributed Systems" ![](https://oss.javaguide.cn/github/javaguide/books/deep-understanding-of-distributed-system.png) -**[《深入理解分布式系统》](https://book.douban.com/subject/35794814/)** 是 2022 年出版的一本分布式中文原创书籍,主要讲的是分布式领域的基本概念、常见挑战以及共识算法。 +**["In-Depth Understanding of Distributed Systems"](https://book.douban.com/subject/35794814/)** is a Chinese original book published in 2022, focusing on the basic concepts, common challenges, and consensus algorithms in the field of distributed systems. -作者用了大量篇幅来介绍分布式领域中非常重要的共识算法,并且还会基于 Go 语言带着你从零实现了一个共识算法的鼻祖 Paxos 算法。 +The author dedicates a substantial portion of the book to discussing the very important consensus algorithms in the distributed field, and also guides you through the implementation of the ancestral Paxos algorithm from scratch using the Go language. -实话说,我还没有开始看这本书。但是!这本书的作者的博客上的分布式相关的文章我几乎每一篇都认真看过。作者从 2019 年开始构思《深入理解分布式系统》,2020 年开始动笔,花了接近两年的时间才最终交稿。 +To be honest, I haven't started reading this book yet. However! I have carefully read almost every one of the articles related to distributed systems on the author's blog. The author began conceptualizing "In-Depth Understanding of Distributed Systems" in 2019 and started writing in 2020, spending nearly two years before finally submitting the manuscript. ![](https://oss.javaguide.cn/github/javaguide/books/image-20220706121952258.png) -作者专门写了一篇文章来介绍这本书的背后的故事,感兴趣的小伙伴可以自行查阅: 。 +The author has also written an article to introduce the story behind this book; interested friends can check it out: . -最后,放上这本书的代码仓库和勘误地址: 。 +Finally, here is the repository for the book's code and errata: . -## 《数据密集型应用系统设计》 +## "Designing Data-Intensive Applications" ![](https://oss.javaguide.cn/github/javaguide/books/ddia.png) -强推一波 **[《Designing Data-Intensive Application》](https://book.douban.com/subject/30329536/)** (DDIA,数据密集型应用系统设计),值得读很多遍!豆瓣有接近 90% 的人看了这本书之后给了五星好评。 +Highly recommend **["Designing Data-Intensive Applications"](https://book.douban.com/subject/30329536/)** (DDIA), a book well worth reading multiple times! Nearly 90% of readers on Douban have given this book a five-star rating. -这本书主要讲了分布式数据库、数据分区、事务、分布式系统等内容。 +This book mainly covers topics such as distributed databases, data partitioning, transactions, and distributed systems. -书中介绍的大部分概念你可能之前都听过,但是在看了书中的内容之后,你可能会豁然开朗:“哇塞!原来是这样的啊!这不是某技术的原理么?”。 +You may have heard most of the concepts introduced in this book before, but after reading its content, you might find yourself enlightened: "Wow! So that's how it is! Isn't that the principle behind a certain technology?" -这本书我之前专门写过知乎回答介绍和推荐,没看过的朋友可以看看:[有哪些你看了以后大呼过瘾的编程书?](https://www.zhihu.com/question/50408698/answer/2278198495) 。另外,如果你在阅读这本书的时候感觉难度比较大,很多地方读不懂的话,我这里推荐一下《深入理解分布式系统》作者写的[《DDIA 逐章精读》小册](https://ddia.qtmuniao.com)。 +I have previously written a Zhihu answer to introduce and recommend this book; if you haven't read it yet, you can take a look: [What programming books have made you shout in excitement after reading?](https://www.zhihu.com/question/50408698/answer/2278198495). Additionally, if you find the reading of this book challenging and difficult to understand in many places, I recommend the [“DDIA Chapter by Chapter Study Guide”](https://ddia.qtmuniao.com) written by the author of "In-Depth Understanding of Distributed Systems". -## 《深入理解分布式事务》 +## "In-Depth Understanding of Distributed Transactions" ![](https://oss.javaguide.cn/github/javaguide/books/In-depth-understanding-of-distributed-transactions-xiaoyu.png) -**[《深入理解分布式事务》](https://book.douban.com/subject/35626925/)** 这本书的其中一位作者是 Apache ShenYu(incubating)网关创始人、Hmily、RainCat、Myth 等分布式事务框架的创始人。 +**["In-Depth Understanding of Distributed Transactions"](https://book.douban.com/subject/35626925/)** features one of the authors as the founder of Apache ShenYu (incubating) Gateway, as well as the founder of distributed transaction frameworks like Hmily, RainCat, and Myth. -学习分布式事务的时候,可以参考一下这本书。虽有一些小错误以及逻辑不通顺的地方,但对于各种分布式事务解决方案的介绍,总体来说还是不错的。 +When learning about distributed transactions, this book can serve as a useful reference. While it contains some minor errors and areas with logical inconsistencies, the overall introduction to various distributed transaction solutions is still commendable. -## 《从 Paxos 到 Zookeeper》 +## "From Paxos to Zookeeper" ![](https://oss.javaguide.cn/github/javaguide/books/image-20211216161350118.png) -**[《从 Paxos 到 Zookeeper》](https://book.douban.com/subject/26292004/)** 是一本带你入门分布式理论的好书。这本书主要介绍几种典型的分布式一致性协议,以及解决分布式一致性问题的思路,其中重点讲解了 Paxos 和 ZAB 协议。 +**["From Paxos to Zookeeper"](https://book.douban.com/subject/26292004/)** is a good book to get you started on distributed theory. It mainly introduces several typical distributed consistency protocols and strategies for solving distributed consistency problems, with a focus on the Paxos and ZAB protocols. -PS:Zookeeper 现在用的不多,可以不用重点学习,但 Paxos 和 ZAB 协议还是非常值得深入研究的。 +PS: Zookeeper is not widely used nowadays, so it may not require in-depth study; however, the Paxos and ZAB protocols are still very worthwhile for thorough research. -## 《深入理解分布式共识算法》 +## "In-Depth Understanding of Distributed Consensus Algorithms" ![](https://oss.javaguide.cn/github/javaguide/books/deep-dive-into-distributed-consensus-algorithms.png) -**[《深入理解分布式共识算法》](https://book.douban.com/subject/36335459/)** 详细剖析了 Paxos、Raft、Zab 等主流分布式共识算法的核心原理和实现细节。如果你想要了解分布式共识算法的话,不妨参考一下这本书的总结。 +**["In-Depth Understanding of Distributed Consensus Algorithms"](https://book.douban.com/subject/36335459/)** provides a detailed analysis of the core principles and implementation details of mainstream distributed consensus algorithms such as Paxos, Raft, and Zab. If you want to learn about distributed consensus algorithms, consider using this book as a reference. -## 《微服务架构设计模式》 +## "Microservices Architecture Patterns" ![](https://oss.javaguide.cn/github/javaguide/books/microservices-patterns.png) -**[《微服务架构设计模式》](https://book.douban.com/subject/33425123/)** 的作者 Chris Richardson 被评为世界十大软件架构师之一、微服务架构先驱。这本书汇集了 44 个经过实践验证的架构设计模式,这些模式用来解决诸如服务拆分、事务管理、查询和跨服务通信等难题。书中的内容不仅理论扎实,还通过丰富的 Java 代码示例,引导读者一步步掌握开发和部署生产级别的微服务架构应用。 +**["Microservices Architecture Patterns"](https://book.douban.com/subject/33425123/)** is authored by Chris Richardson, recognized as one of the top ten software architects in the world and a pioneer in microservices architecture. This book compiles 44 practical architecture design patterns that address challenges such as service decomposition, transaction management, querying, and inter-service communication. The content of the book is not only solid in theory but also includes rich Java code examples, guiding readers step by step in mastering the development and deployment of production-level microservices architecture applications. -## 《凤凰架构》 +## "Phoenix Architecture" ![](https://oss.javaguide.cn/github/javaguide/books/f5bec14d3b404ac4b041d723153658b5.png) -**[《凤凰架构》](https://book.douban.com/subject/35492898/)** 这本书是周志明老师多年架构和研发经验的总结,内容非常干货,深度与广度并存,理论结合实践! +**["Phoenix Architecture"](https://book.douban.com/subject/35492898/)** is a summary of Teacher Zhou Zhiming's many years of architectural and development experience. The content is very rich, blending depth and breadth, and combines theory with practice! -正如书名的副标题“构建可靠的大型分布式系统”所说的那样,这本书的主要内容就是讲:“如何构建一套可靠的分布式大型软件系统” ,涵盖了下面这些方面的内容: +As the subtitle of the book suggests, "Building Reliable Large-Scale Distributed Systems," the main content discusses: "How to build a reliable distributed large software system," covering aspects such as: -- 软件架构从单体到微服务再到无服务的演进之路。 -- 架构师应该在架构设计时应该注意哪些问题,有哪些比较好的实践。 -- 分布式的基石比如常见的分布式共识算法 Paxos、Multi Paxos。 -- 不可变基础设施比如虚拟化容器、服务网格。 -- 向微服务迈进的避坑指南。 +- The evolution of software architecture from monolithic to microservices to serverless. +- What architects should pay attention to during architectural design and good practices to follow. +- Foundations of distributed systems, such as common distributed consensus algorithms like Paxos and Multi Paxos. +- Immutable infrastructures such as virtualization containers and service meshes. +- A guide to avoid pitfalls when transitioning to microservices. -这本书我推荐过很多次了。详见历史文章: +I have recommended this book many times. See my historical articles for more details: -- [周志明老师的又一神书!发现宝藏!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247505254&idx=1&sn=04faf3093d6002354f06fffbfc2954e0&chksm=cea19aadf9d613bbba7ed0e02ccc4a9ef3a30f4d83530e7ad319c2cc69cd1770e43d1d470046&scene=178&cur_album_id=1646812382221926401#rd) -- [Java 领域的又一神书!周志明老师 YYDS!](https://mp.weixin.qq.com/s/9nbzfZGAWM9_qIMp1r6uUQ) +- [Teacher Zhou Zhiming's Another Great Book! Discover the Treasure!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247505254&idx=1&sn=04faf3093d6002354f06fffbfc2954e0&chksm=cea19aadf9d613bbba7ed0e02ccc4a9ef3a30f4d83530e7ad319c2cc69cd1770e43d1d470046&scene=178&cur_album_id=1646812382221926401#rd) +- [Another Great Book in the Java Field! Teacher Zhou Zhiming is Forever Young!](https://mp.weixin.qq.com/s/9nbzfZGAWM9_qIMp1r6uUQ) -## 其他 +## Others -- [《分布式系统 : 概念与设计》](https://book.douban.com/subject/21624776/):偏教材类型,内容全而无趣,可作为参考书籍; -- [《分布式架构原理与实践》](https://book.douban.com/subject/35689350/):2021 年出版的,没什么热度,我也还没看过。 +- ["Distributed Systems: Principles and Paradigms"](https://book.douban.com/subject/21624776/): More textbook-like, with comprehensive yet tedious content, can be used as a reference book; +- ["Principles and Practice of Distributed Architecture"](https://book.douban.com/subject/35689350/): Published in 2021, it lacks interest, and I haven't read it yet. diff --git a/docs/books/java.md b/docs/books/java.md index 8278ed596e1..4f1d6e3f188 100644 --- a/docs/books/java.md +++ b/docs/books/java.md @@ -1,260 +1,260 @@ --- -title: Java 必读经典书籍 -category: 计算机书籍 -icon: "java" +title: Must-Read Classic Java Books +category: Computer Books +icon: java --- -## Java 基础 +## Java Basics -**[《Head First Java》](https://book.douban.com/subject/2000732/)** +**[Head First Java](https://book.douban.com/subject/2000732/)** -![《Head First Java》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424103035793.png) +![Head First Java - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424103035793.png) -《Head First Java》这本书的内容很轻松有趣,可以说是我学习编程初期最喜欢的几本书之一了。同时,这本书也是我的 Java 启蒙书籍。我在学习 Java 的初期多亏了这本书的帮助,自己才算是跨进 Java 语言的大门。 +"Head First Java" is a light and enjoyable book, and I can say it's one of my favorite books from the early days of learning programming. This book is also my introductory book to Java. Thanks to its help in the early stages, I managed to step into the world of Java programming. -我觉得我在 Java 这块能够坚持下来,这本书有很大的功劳。我身边的的很多朋友学习 Java 初期都是看的这本书。 +I believe I could stick with Java because of this book's significant role. Many of my friends also started learning Java with this book. -有很多小伙伴就会问了:**这本书适不适合编程新手阅读呢?** +Many folks ask: **Is this book suitable for programming beginners?** -我个人觉得这本书还是挺适合编程新手阅读的,毕竟是 “Head First” 系列。 +In my opinion, this book is quite suitable for programming newcomers since it belongs to the "Head First" series. -**[《Java 核心技术卷 1 + 卷 2》](https://book.douban.com/subject/34898994/)** +**[Java Core Technology Volume 1 + Volume 2](https://book.douban.com/subject/34898994/)** -![《Java 核心技术卷 1》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424101217849.png) +![Java Core Technology Volume 1 - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424101217849.png) -这两本书也非常不错。不过,这两本书的内容很多,全看的话比较费时间。我现在是把这两本书当做工具书来用,就比如我平时写文章的时候,碰到一些 Java 基础方面的问题,经常就翻看这两本来当做参考! +These two books are also excellent. However, they contain a lot of information, and reading them in full can be quite time-consuming. I currently use these two books as reference manuals; for instance, when I encounter some fundamental Java issues while writing articles, I often refer back to these books. -我当时在大学的时候就买了两本放在寝室,没事的时候就翻翻。建议有点 Java 基础之后再读,介绍的还是比较深入和全面的,非常推荐。 +I bought these two books while in college and kept them in my dorm room for casual reading. I recommend reading them after getting a foundational understanding of Java because they are quite in-depth and comprehensive—highly recommended. -**[《Java 编程思想》](https://book.douban.com/subject/2130190/)** +**[Thinking in Java](https://book.douban.com/subject/2130190/)** -![《Java 编程思想》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424103124893.png) +![Thinking in Java - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424103124893.png) -另外,这本书的作者去年新出版了[《On Java》](https://book.douban.com/subject/35751619/),我更推荐这本,内容更新,介绍了 Java 的 3 个长期支持版(Java 8、11、17)。 +Additionally, the author of this book published a new book last year, **[On Java](https://book.douban.com/subject/35751619/)**, which I recommend more. It has updated content and covers three long-term support versions of Java (Java 8, 11, and 17). ![](https://oss.javaguide.cn/github/javaguide/books/on-java/6171657600353_.pic_hd.jpg) -毕竟,这是市面上目前唯一一本介绍了 Java 的 3 个长期支持版(Java 8、11、17)的技术书籍。 +After all, this is currently the only technical book on the market that explains three long-term support versions of Java (Java 8, 11, 17). -**[《Java 8 实战》](https://book.douban.com/subject/26772632/)** +**[Java 8 in Action](https://book.douban.com/subject/26772632/)** -![《Java 8实战》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424103202625.png) +![Java 8 in Action - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424103202625.png) -Java 8 算是一个里程碑式的版本,现在一般企业还是用 Java 8 比较多。掌握 Java 8 的一些新特性比如 Lambda、Stream API 还是挺有必要的。这块的话,我推荐 **[《Java 8 实战》](https://book.douban.com/subject/26772632/)** 这本书。 +Java 8 is a milestone version; it is still widely used in many enterprises. Mastering new features like Lambda and Stream API in Java 8 is quite necessary. For this, I recommend **[Java 8 in Action](https://book.douban.com/subject/26772632/)**. -**[《Java 编程的逻辑》](https://book.douban.com/subject/30133440/)** +**[The Logic in Java Programming](https://book.douban.com/subject/30133440/)** -![《Java编程的逻辑》](https://oss.javaguide.cn/github/javaguide/books/image-20230721153650488.png) +![The Logic in Java Programming](https://oss.javaguide.cn/github/javaguide/books/image-20230721153650488.png) -一本非常低调的好书,相比于入门书来说,内容更有深度。适合初学者,同时也适合大家拿来复习 Java 基础知识。 +A very low-key excellent book, which offers deeper content compared to beginner books. It's suitable for beginners as well as anyone reviewing Java basics. -## Java 并发 +## Java Concurrency -**[《Java 并发编程之美》](https://book.douban.com/subject/30351286/)** +**[The Beauty of Concurrency Programming in Java](https://book.douban.com/subject/30351286/)** -![《Java 并发编程之美》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424112413660.png) +![The Beauty of Concurrency Programming in Java - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424112413660.png) -这本书还是非常适合我们用来学习 Java 多线程的,讲解非常通俗易懂,作者从并发编程基础到实战都是信手拈来。 +This book is very suitable for learning Java multithreading. The explanations are straightforward, and the author skillfully covers everything from the basics of concurrency to practical applications. -另外,这本书的作者加多自身也会经常在网上发布各种技术文章。这本书也是加多大佬这么多年在多线程领域的沉淀所得的结果吧!他书中的内容基本都是结合代码讲解,非常有说服力! +Additionally, the author often publishes various technical articles online. This book is the culmination of years of work in the multithreading field! The content in the book is largely explained in conjunction with code, making it very persuasive! -**[《实战 Java 高并发程序设计》](https://book.douban.com/subject/30358019/)** +**[Practical Java High-Performance Concurrency](https://book.douban.com/subject/30358019/)** -![《实战 Java 高并发程序设计》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424112554830.png) +![Practical Java High-Performance Concurrency - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424112554830.png) -这个是我第二本要推荐的书籍,比较适合作为多线程入门/进阶书籍来看。这本书内容同样是理论结合实战,对于每个知识点的讲解也比较通俗易懂,整体结构也比较清。 +This is the second book I recommend, which is well-suited as an introductory or advanced book on multithreading. The content combines theory with practical applications, and the explanations for each knowledge point are quite understandable with a clear overall structure. -**[《深入浅出 Java 多线程》](https://github.com/RedSpider1/concurrent)** +**[In-Depth Java Multithreading](https://github.com/RedSpider1/concurrent)** -![《深入浅出 Java 多线程》在线阅读](https://oss.javaguide.cn/github/javaguide/books/image-20220424112927759.png) +![In-Depth Java Multithreading Online Reading](https://oss.javaguide.cn/github/javaguide/books/image-20220424112927759.png) -这本开源书籍是几位大厂的大佬开源的。这几位作者为了写好《深入浅出 Java 多线程》这本书阅读了大量的 Java 多线程方面的书籍和博客,然后再加上他们的经验总结、Demo 实例、源码解析,最终才形成了这本书。 +This open-source book is authored by several prominent figures from leading companies. In writing **In-Depth Java Multithreading**, these authors read a multitude of books and blogs on Java multithreading and combined their experience with examples and source code analysis to create this book. -这本书的质量也是非常过硬!给作者们点个赞!这本书有统一的排版规则和语言风格、清晰的表达方式和逻辑。并且每篇文章初稿写完后,作者们就会互相审校,合并到主分支时所有成员会再次审校,最后再通篇修订了三遍。 +The quality of this book is also very solid! Kudos to the authors! This book has standardized formatting and writing style, clear expression, and logic. Furthermore, after each draft, the authors review each other's work, and all members re-evaluate before merging into the main branch. Finally, the entire manuscript was revised three times. -在线阅读:。 +Online reading: . -**[《Java 并发实现原理:JDK 源码剖析》](https://book.douban.com/subject/35013531/)** +**[Principles of Java Concurrency: JDK Source Analysis](https://book.douban.com/subject/35013531/)** -![《Java 并发实现原理:JDK 源码剖析》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/0b1b046af81f4c94a03e292e66dd6f7d.png) +![Principles of Java Concurrency: JDK Source Analysis - Douban](https://oss.javaguide.cn/github/javaguide/books/0b1b046af81f4c94a03e292e66dd6f7d.png) -这本书主要是对 Java Concurrent 包中一些比较重要的源码进行了讲解,另外,像 JMM、happen-before、CAS 等等比较重要的并发知识这本书也都会一并介绍到。 +This book primarily explains the important source code in the Java Concurrent package and also covers essential concurrency concepts such as JMM, happen-before, CAS, etc. -不论是你想要深入研究 Java 并发,还是说要准备面试,你都可以看看这本书。 +Whether you want to deeply study Java concurrency or prepare for interviews, this book is worth checking out. ## JVM -**[《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/)** +**[In-Depth Understanding of the Java Virtual Machine](https://book.douban.com/subject/34907497/)** -![《深入理解 Java 虚拟机》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/20210710104655705.png) +![In-Depth Understanding of the Java Virtual Machine - Douban](https://oss.javaguide.cn/github/javaguide/books/20210710104655705.png) -这本书就一句话形容:**国产书籍中的战斗机,实实在在的优秀!** (真心希望国内能有更多这样的优质书籍出现!加油!💪) +This book can be described in one sentence: **A fighter among domestic books, genuinely excellent!** (I sincerely hope more quality books like this can emerge in the industry! Keep it up! 💪) -这本书的第 3 版 2019 年底已经出来了,新增了很多实在的内容比如 ZGC 等新一代 GC 的原理剖析。目前豆瓣上是 9.5 的高分,🐂 不 🐂 我就不多说了! +The third edition of this book was published at the end of 2019, adding a lot of practical content, including the principles of new-generation garbage collection like ZGC. It holds a high rating of 9.5 on Douban, which speaks for itself! -不论是你面试还是你想要在 Java 领域学习的更深,你都离不开这本书籍。这本书不光要看,你还要多看几遍,里面都是干货。这本书里面还有一些需要自己实践的东西,我建议你也跟着实践一下。 +Whether you are preparing for an interview or seeking deeper knowledge in the Java field, you cannot overlook this book. It’s necessary not just to read this book but to read it multiple times as it is packed with valuable insights. The book also includes practical components, and I recommend following along with practical exercises. -类似的书籍还有 **[《实战 Java 虚拟机》](https://book.douban.com/subject/26354292/)**、**[《虚拟机设计与实现:以 JVM 为例》](https://book.douban.com/subject/34935105/)** ,这两本都是非常不错的! +Other similar books include **[Practical Java Virtual Machine](https://book.douban.com/subject/26354292/)** and **[Design and Implementation of Virtual Machines: Taking JVM as an Example](https://book.douban.com/subject/34935105/)**, both of which are excellent! -![《实战 Java 虚拟机》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113158144.png) +![Practical Java Virtual Machine - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424113158144.png) -![《虚拟机设计与实现:以 JVM 为例》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113210153.png) +![Design and Implementation of Virtual Machines: Taking JVM as an Example - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424113210153.png) -如果你对实战比较感兴趣,想要自己动手写一个简易的 JVM 的话,可以看看 **[《自己动手写 Java 虚拟机》](https://book.douban.com/subject/26802084/)** 这本书。 +If you are particularly interested in practical operations and want to write a simple JVM yourself, you can check out **[Let's Write a Java Virtual Machine](https://book.douban.com/subject/26802084/)**. -![《自己动手写 Java 虚拟机》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113445246.png) +![Let's Write a Java Virtual Machine - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424113445246.png) -书中的代码是基于 Go 语言实现的,搞懂了原理之后,你可以使用 Java 语言模仿着写一个,也算是练练手! 如果你当前没有能力独立使用 Java 语言模仿着写一个的话,你也可以在网上找到很多基于 Java 语言版本的实现,比如[《zachaxy 的手写 JVM 系列》](https://zachaxy.github.io/tags/JVM/) 。 +The code in this book is implemented in Go. Once you understand the principles, you can try writing one in Java, which can serve as practice! If you currently lack the capacity to replicate a JVM independently in Java, you can find many implementations based on Java online, such as **[Zachaxy's Handwritten JVM Series](https://zachaxy.github.io/tags/JVM/)**. -这本书目前在豆瓣有 8.2 的评分,我个人觉得张秀宏老师写的挺好的,这本书值得更高的评分。 +This book currently holds a rating of 8.2 on Douban. I personally believe that teacher Zhang Xiuhong has done an excellent job, and this book deserves a higher rating. -另外,R 大在豆瓣发的[《从表到里学习 JVM 实现》](https://www.douban.com/doulist/2545443/)这篇文章中也推荐了很多不错的 JVM 相关的书籍,推荐小伙伴们去看看。 +Additionally, R Da's article **[Learning JVM Implementation from the Inside Out](https://www.douban.com/doulist/2545443/)** on Douban also recommends many excellent JVM-related books, which I encourage you to check out. -再推荐两个视频给喜欢看视频学习的小伙伴。 +I will also recommend two videos for those who prefer learning through video. -第 1 个是尚硅谷的宋红康老师讲的[《JVM 全套教程》](https://www.bilibili.com/video/BV1PJ411n7xZ)。这个课程的内容非常硬,一共有接近 400 小节。 +The first is **[Complete JVM Tutorial by Teacher Song Hongkang from Shang Silicon Valley](https://www.bilibili.com/video/BV1PJ411n7xZ)**. This course is very rigorous, consisting of nearly 400 sections. -课程的内容分为 3 部分: +The course content is divided into three parts: -1. 《内存与垃圾回收篇》 -2. 《字节码与类的加载篇》 -3. 《性能监控与调优篇》 +1. Memory and Garbage Collection +1. Bytecode and Class Loading +1. Performance Monitoring and Tuning -第 2 个是你假笨大佬的 **[《JVM 参数【Memory 篇】》](https://club.perfma.com/course/438755/list)** 教程,很厉害了! +The second is the tutorial **[JVM Parameters [Memory Edition]](https://club.perfma.com/course/438755/list)** by **You Jiao Ben**, which is quite impressive! ![](https://oss.javaguide.cn/java-guide-blog/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70.png) -## 常用工具 +## Common Tools -非常重要!非常重要!特别是 Git 和 Docker。 +Very important! Extremely important! Especially Git and Docker. -- **IDEA**:熟悉基本操作以及常用快捷。相关资料: [《IntelliJ IDEA 简体中文专题教程》](https://github.com/judasn/IntelliJ-IDEA-Tutorial) 。 -- **Maven**:强烈建议学习常用框架之前可以提前花几天时间学习一下**Maven**的使用。(到处找 Jar 包,下载 Jar 包是真的麻烦费事,使用 Maven 可以为你省很多事情)。相关阅读:[Maven 核心概念总结](https://javaguide.cn/tools/maven/maven-core-concepts.html)。 -- **Git**:基本的 Git 技能也是必备的,试着在学习的过程中将自己的代码托管在 Github 上。相关阅读:[Git 核心概念总结](https://javaguide.cn/tools/git/git-intro.html)。 -- **Docker**:学着用 Docker 安装学习中需要用到的软件比如 MySQL ,这样方便很多,可以为你节省不少时间。相关资料:[《Docker - 从入门到实践》](https://yeasy.gitbook.io/docker_practice/) 。 +- **IDEA**: Familiarize yourself with basic operations and commonly used shortcuts. Related materials: **[IntelliJ IDEA Simplified Chinese Tutorial](https://github.com/judasn/IntelliJ-IDEA-Tutorial)**. +- **Maven**: Strongly recommend spending a few days learning about **Maven** before diving into common frameworks. (It’s a real hassle to find and download jar files; using Maven can save you a lot of trouble.) Further reading: **[Maven Core Concepts Summary](https://javaguide.cn/tools/maven/maven-core-concepts.html)**. +- **Git**: Basic Git skills are also essential; try to host your code on GitHub as you learn. Further reading: **[Git Core Concepts Summary](https://javaguide.cn/tools/git/git-intro.html)**. +- **Docker**: Learn to use Docker to install and manage software needed during learning, like MySQL; this can save you a great deal of time. Related materials: **[Docker - From Beginner to Practice](https://yeasy.gitbook.io/docker_practice/)**. -除了这些工具之外,我强烈建议你一定要搞懂 GitHub 的使用。一些使用 GitHub 的小技巧,你可以看[Github 实用小技巧总结](https://javaguide.cn/tools/git/github-tips.html)这篇文章。 +In addition to these tools, I strongly recommend you thoroughly understand how to use GitHub. For some handy tips on using GitHub, you can check out the article **[Practical Tips for Using GitHub](https://javaguide.cn/tools/git/github-tips.html)**. -## 常用框架 +## Common Frameworks -框架部分建议找官方文档或者博客来看。 +For frameworks, I suggest looking at official documentation or blogs. ### Spring/SpringBoot -**Spring 和 SpringBoot 真的很重要!** +**Spring and SpringBoot are truly important!** -一定要搞懂 AOP 和 IOC 这两个概念。Spring 中 bean 的作用域与生命周期、SpringMVC 工作原理详解等等知识点都是非常重要的,一定要搞懂。 +Make sure to understand AOP and IOC concepts. The scope and lifecycle of beans in Spring, the workings of SpringMVC, and other such knowledge are very important and must be grasped. -企业中做 Java 后端,你一定离不开 SpringBoot ,这个是必备的技能了!一定一定一定要学好! +If you work in Java backend development, you can’t do without SpringBoot; it is an essential skill you need to master! You must learn it thoroughly! -像 SpringBoot 和一些常见技术的整合你也要知识怎么做,比如 SpringBoot 整合 MyBatis、 ElasticSearch、SpringSecurity、Redis 等等。 +You should also understand how to integrate SpringBoot with common technologies like MyBatis, ElasticSearch, SpringSecurity, Redis, etc. -下面是一些比较推荐的书籍/专栏。 +Here are some recommended books/columns. -**[《Spring 实战》](https://book.douban.com/subject/34949443/)** +**[Spring in Action](https://book.douban.com/subject/34949443/)** -![《Spring 实战》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113512453.png) +![Spring in Action - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424113512453.png) -不建议当做入门书籍读,入门的话可以找点国人的书或者视频看。这本定位就相当于是关于 Spring 的一个概览,只有一些基本概念的介绍和示例,涵盖了 Spring 的各个方面,但都不够深入。就像作者在最后一页写的那样:“学习 Spring,这才刚刚开始”。 +Not recommended as an introductory book; for starters, look for books or videos by Chinese authors. This book serves as an overview of Spring, containing introductions to some basic concepts and examples, covering various aspects of Spring but not in great depth. As the author writes on the last page: "Learning Spring is just the beginning." -**[《Spring 5 高级编程》](https://book.douban.com/subject/30452637/)** +**[Spring 5 Advanced Programming](https://book.douban.com/subject/30452637/)** ![](https://oss.javaguide.cn/github/javaguide/books/20210328171223638.png) -对于 Spring5 的新特性介绍的比较详细,也说不上好。另外,感觉全书翻译的有一点蹩脚的味道,还有一点枯燥。全书的内容比较多,我一般拿来当做工具书参考。 +This book provides a detailed introduction to the new features of Spring 5, but it's not particularly outstanding. Additionally, the translation seems a bit awkward, and the content feels somewhat dry. I usually use this book as a reference manual. -**[《Spring Boot 编程思想(核心篇)》](https://book.douban.com/subject/33390560/)** +**[Spring Boot Programming Principles (Core Edition)](https://book.douban.com/subject/33390560/)** -![《Spring Boot 编程思想(核心篇)》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113546513.png) +![Spring Boot Programming Principles (Core Edition) - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424113546513.png) -_稍微有点啰嗦,但是原理介绍的比较清楚。_ +_It’s a bit verbose, but the principle explanation is quite clear._ -SpringBoot 解析,不适合初学者。我是去年入手的,现在就看了几章,后面没看下去。书很厚,感觉很多很多知识点的讲解过于啰嗦和拖沓,不过,这本书对于 SpringBoot 内部原理讲解的还是很清楚。 +SpringBoot analysis; it’s not suitable for beginners. I picked it up last year and only got through a few chapters, couldn't continue further. The book is quite thick and seems to drag on with too many points covered, but it explains the internal principles of SpringBoot very clearly. -**[《Spring Boot 实战》](https://book.douban.com/subject/26857423/)** +**[Spring Boot in Action](https://book.douban.com/subject/26857423/)** -![《Spring Boot 实战》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113614768.png) +![Spring Boot in Action - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424113614768.png) -比较一般的一本书,可以简单拿来看一下。 +A fairly average book; you can simply take a look at it. ### MyBatis -MyBatis 国内用的挺多的,我的建议是不需要花太多时间在上面。当然了,MyBatis 的源码还是非常值得学习的,里面有很多不错的编码实践。这里推荐两本讲解 MyBatis 源码的书籍。 +MyBatis is widely used in China, and I recommend not spending too much time on it. However, the source code of MyBatis is definitely worth studying, as it contains excellent coding practices. Here are two books that explain MyBatis source code. -**[《手写 MyBatis:渐进式源码实践》](https://book.douban.com/subject/36243250/)** +**[Handwriting MyBatis: Progressive Source Code Practice](https://book.douban.com/subject/36243250/)** -![《手写MyBatis:渐进式源码实践》](https://oss.javaguide.cn/github/javaguide/books/image-20230724123402784.png) +![Handwriting MyBatis: Progressive Source Code Practice](https://oss.javaguide.cn/github/javaguide/books/image-20230724123402784.png) -我的好朋友小傅哥出版的一本书。这本书以实践为核心,摒弃 MyBatis 源码中繁杂的内容,聚焦于 MyBaits 中的核心逻辑,简化代码实现过程,以渐进式的开发方式,逐步实现 MyBaits 中的核心功能。 +A book published by my good friend, Xiao Fu Ge. This book focuses on practical aspects, avoiding the complexities found in the MyBatis source code, honing in on core logic, simplifying the coding process, and adopting a progressive development approach to gradually implement core functionalities in MyBatis. -这本书的配套项目的仓库地址: 。 +The accompanying project repository for this book can be found at: . -**[《通用源码阅读指导书――MyBatis 源码详解》](https://book.douban.com/subject/35138963/)** +**[Guidance for Reading Generic Source Code: MyBatis Source Code Explanation](https://book.douban.com/subject/35138963/)** -![《通用源码阅读指导书――MyBatis源码详解》](https://oss.javaguide.cn/github/javaguide/books/image-20230724123416741.png) +![Guidance for Reading Generic Source Code: MyBatis Source Code Explanation](https://oss.javaguide.cn/github/javaguide/books/image-20230724123416741.png) -这本书通过 MyBatis 开源代码讲解源码阅读的流程和方法!一共对 MyBatis 源码中的 300 多个类进行了详细解析,包括其背景知识、组织方式、逻辑结构、实现细节。 +This book explains the process and method of reading the MyBatis open-source code! It provides a detailed analysis of over 300 classes in the MyBatis source code, including background knowledge, organizational structure, logical frameworks, and implementation details. -这本书的配套示例仓库地址: 。 +The accompanying example repository for this book can be found at: . ### Netty -**[《Netty 实战》](https://book.douban.com/subject/27038538/)** +**[Netty in Action](https://book.douban.com/subject/27038538/)** -![《Netty 实战》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113715369.png) +![Netty in Action - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424113715369.png) -这本书可以用来入门 Netty ,内容从 BIO 聊到了 NIO、之后才详细介绍为什么有 Netty、Netty 为什么好用以及 Netty 重要的知识点讲解。 +This book can be used to get started with Netty, covering topics from BIO to NIO, then detailing why Netty exists, why it's useful, and its important knowledge points. -这本书基本把 Netty 一些重要的知识点都介绍到了,而且基本都是通过实战的形式讲解。 +This book basically covers most of the essential knowledge points of Netty and is largely explained through practical examples. -**[《Netty 进阶之路:跟着案例学 Netty》](https://book.douban.com/subject/30381214/)** +**[Advanced Netty: Learning Netty Through Case Studies](https://book.douban.com/subject/30381214/)** -![《Netty 进阶之路:跟着案例学 Netty》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113747345.png) +![Advanced Netty: Learning Netty Through Case Studies - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424113747345.png) -内容都是关于使用 Netty 的实践案例比如内存泄露这些东西。如果你觉得你的 Netty 已经完全入门了,并且你想要对 Netty 掌握的更深的话,推荐你看一下这本书。 +The content consists of practical examples using Netty, like memory leaks. If you think you've fully grasped Netty and wish to master it further, I recommend reading this book. -**[《跟闪电侠学 Netty:Netty 即时聊天实战与底层原理》](https://book.douban.com/subject/35752082/)** +**[Learn Netty from Flash Superman: Real-time Chat Practical and Underlying Principles](https://book.douban.com/subject/35752082/)** ![](https://oss.javaguide.cn/github/javaguide/open-source-project/image-20220503085034268.png) -2022 年 3 月出版的一本书。这本书分为上下两篇,上篇通过一个即时聊天系统的实战案例带你入门 Netty,下篇通过 Netty 源码分析带你搞清 Netty 比较重要的底层原理。 +A book published in March 2022. It is divided into two parts; the first part introduces you to Netty through a practical case of a real-time chat system, while the second part clarifies the essential underlying principles of Netty through source code analysis. -## 性能调优 +## Performance Tuning -**[《Java 性能权威指南》](https://book.douban.com/subject/26740520/)** +**[The Definitive Guide to Java Performance](https://book.douban.com/subject/26740520/)** -![《Java 性能权威指南》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113809644.png) +![The Definitive Guide to Java Performance - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424113809644.png) -_希望能有更多这 Java 性能优化方面的好书!_ +_I hope there will be more excellent books on Java performance optimization!_ -O'Reilly 家族书,性能调优的入门书,我个人觉得性能调优是每个 Java 从业者必备知识。 +This is an O'Reilly family book, an introductory text on performance tuning, which I believe is essential knowledge for every Java practitioner. -这本书介绍的实战内容很不错,尤其是 JVM 调优,缺点也比较明显,就是内容稍微有点老。市面上这种书很少。这本书不适合初学者,建议对 Java 语言已经比价掌握了再看。另外,阅读之前,最好先看看周志明大佬的《深入理解 Java 虚拟机》。 +This book introduces practical content very well, especially regarding JVM tuning, though it has some notable downsides—specifically that its content feels a bit outdated. There are very few books like this available on the market. It’s not suited for beginners, so it’s advisable to have a good understanding of Java before reading it. Additionally, it's best to review Zuo Zhiming's **In-Depth Understanding of the Java Virtual Machine** beforehand. -## 网站架构 +## Website Architecture -看过很多网站架构方面的书籍,比如《大型网站技术架构:核心原理与案例分析》、《亿级流量网站架构核心技术》、《架构修炼之道——亿级网关、平台开放、分布式、微服务、容错等核心技术修炼实践》等等。 +I’ve read many books about website architecture, such as "Core Principles and Case Studies of Large-Scale Website Architecture," "Core Technologies for Websites with Hundreds of Millions of Visits," and "The Path of Architectural Refinement—Practicing Core Technologies like Hundred Million Gateways, Platform Openness, Distributed Systems, Microservices, Fault Tolerance, etc." -目前我觉得能推荐的只有李运华老师的 **[《从零开始学架构》](https://book.douban.com/subject/30335935/)** 和 余春龙老师的 **[《软件架构设计:大型网站技术架构与业务架构融合之道》](https://book.douban.com/subject/30443578/ "《软件架构设计:大型网站技术架构与业务架构融合之道》")** 。 +Currently, the only books I can recommend are **[Learning Architecture from Scratch](https://book.douban.com/subject/30335935/)** by Teacher Li Yunhua and **[Software Architecture Design: The Path to Merging Large-Scale Website Technical Architecture with Business Architecture](https://book.douban.com/subject/30443578/ "Software Architecture Design: The Path to Merging Large-Scale Website Technical Architecture with Business Architecture")** by Teacher Yu Chunlong. ![](https://oss.javaguide.cn/github/javaguide/books/20210412224443177.png) -《从零开始学架构》这本书对应的有一个极客时间的专栏—《从零开始学架构》,里面的很多内容都是这个专栏里面的,两者买其一就可以了。我看了很小一部分,内容挺全面的,是一本真正在讲如何做架构的书籍。 +"Learning Architecture from Scratch" corresponds to a Geek Time column—"Learning Architecture from Scratch." Much of the content in this book overlaps with that of the column, so you can opt for one of them. I have only read a small portion, but it is quite comprehensive and genuinely discusses how to design architecture. ![](https://oss.javaguide.cn/github/javaguide/books/20210412232441459.png) -事务与锁、分布式(CAP、分布式事务……)、高并发、高可用 《软件架构设计:大型网站技术架构与业务架构融合之道》 这本书都有介绍到。 +This book covers discussions on transactions and locks, distribution (CAP, distributed transactions, etc.), high concurrency, and high availability. -## 面试 +## Interviews -**《JavaGuide 面试突击版》** +**JavaGuide Interview Attack Edition** ![](https://oss.javaguide.cn/github/javaguide-mianshituji/image-20220830103023493.png) ![](https://oss.javaguide.cn/github/javaguide-mianshituji/image-20220830102925775.png) -[JavaGuide](https://javaguide.cn/) 的面试版本,涵盖了 Java 后端方面的大部分知识点比如 集合、JVM、多线程还有数据库 MySQL 等内容。 +This is the interview version from JavaGuide, covering most of the knowledge points related to Java backend, including collections, JVM, multithreading, and the database MySQL. -公众号后台回复:“**面试突击**” 即可免费获取,无任何套路。 +You can get it for free by replying with “**Interview Attack**” on the official account without any strings attached. -![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) +![JavaGuide Official WeChat Account](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) diff --git a/docs/books/search-engine.md b/docs/books/search-engine.md index 50abbd57056..14e138dfc47 100644 --- a/docs/books/search-engine.md +++ b/docs/books/search-engine.md @@ -1,33 +1,33 @@ --- -title: 搜索引擎必读经典书籍 -category: 计算机书籍 -icon: "search" +title: Must-Read Classic Books on Search Engines +category: Computer Books +icon: search --- ## Lucene -Elasticsearch 在 Apache Lucene 的基础上开发而成,学习 ES 之前,建议简单了解一下 Lucene 的相关概念。 +Elasticsearch is developed based on Apache Lucene. Before learning ES, it is recommended to have a basic understanding of Lucene-related concepts. -**[《Lucene 实战》](https://book.douban.com/subject/6440615/)** 是国内为数不多的中文版本讲 Lucene 的书籍,适合用来学习和了解 Lucene 相关的概念和常见操作。 +**[“Lucene in Action”](https://book.douban.com/subject/6440615/)** is one of the few Chinese versions of books that explain Lucene, suitable for learning and understanding Lucene-related concepts and common operations. -![《Lucene实战》-实战](https://oss.javaguide.cn/github/javaguide/books/vAJkdYEyol4e6Nr.png) +![“Lucene in Action” - Practical](https://oss.javaguide.cn/github/javaguide/books/vAJkdYEyol4e6Nr.png) ## Elasticsearch -**[《一本书讲透 Elasticsearch:原理、进阶与工程实践》](https://book.douban.com/subject/36716996/)** +**[“A Comprehensive Guide to Elasticsearch: Principles, Advanced Topics, and Engineering Practices”](https://book.douban.com/subject/36716996/)** ![](https://oss.javaguide.cn/github/javaguide/books/one-book-guide-to-elasticsearch.png) -基于 8.x 版本编写,目前全网最新的 Elasticsearch 讲解书籍。内容覆盖 Elastic 官方认证的核心知识点,源自真实项目案例和企业级问题解答。 +Written based on version 8.x, this is currently the latest book explaining Elasticsearch available online. The content covers the core knowledge points certified by Elastic, derived from real project cases and enterprise-level problem-solving. -**[《Elasticsearch 核心技术与实战》](http://gk.link/a/10bcT "《Elasticsearch 核心技术与实战》")** +**[“Core Technologies and Practical Applications of Elasticsearch”](http://gk.link/a/10bcT "“Core Technologies and Practical Applications of Elasticsearch”")** -极客时间的这门课程基于 Elasticsearch 7.1 版本讲解,还算比较新。并且,作者是 eBay 资深技术专家,有 20 年的行业经验,课程质量有保障! +This course from Geek Time is based on Elasticsearch version 7.1 and is relatively new. Moreover, the author is a senior technical expert from eBay with 20 years of industry experience, ensuring the quality of the course! -![《Elasticsearch 核心技术与实战》-极客时间](https://oss.javaguide.cn/github/javaguide/csdn/20210420231125225.png) +![“Core Technologies and Practical Applications of Elasticsearch” - Geek Time](https://oss.javaguide.cn/github/javaguide/csdn/20210420231125225.png) -**[《Elasticsearch 源码解析与优化实战》](https://book.douban.com/subject/30386800/)** +**[“Elasticsearch Source Code Analysis and Optimization Practice”](https://book.douban.com/subject/30386800/)** -![《Elasticsearch 源码解析与优化实战》-豆瓣](https://oss.javaguide.cn/p3-juejin/f856485931a945639d5c23aaed74fb38~tplv-k3u1fbpfcp-zoom-1.png) +![“Elasticsearch Source Code Analysis and Optimization Practice” - Douban](https://oss.javaguide.cn/p3-juejin/f856485931a945639d5c23aaed74fb38~tplv-k3u1fbpfcp-zoom-1.png) -如果你想进一步深入研究 Elasticsearch 原理的话,可以看看张超老师的这本书。这是市面上唯一一本写 Elasticsearch 源码的书。 +If you want to further delve into the principles of Elasticsearch, you can check out this book by Teacher Zhang Chao. It is the only book on the market that discusses the source code of Elasticsearch. diff --git a/docs/books/software-quality.md b/docs/books/software-quality.md index 5cfce79dfaa..b65bc0206e0 100644 --- a/docs/books/software-quality.md +++ b/docs/books/software-quality.md @@ -1,131 +1,131 @@ --- -title: 软件质量必读经典书籍 -category: 计算机书籍 -icon: "highavailable" +title: Must-Read Classic Books on Software Quality +category: Computer Books +icon: highavailable head: - - - meta - - name: keywords - content: 软件质量书籍精选 + - - meta + - name: keywords + content: Selected Books on Software Quality --- -下面推荐都是我看过并且我觉得值得推荐的书籍。 +Below are the books I have read and believe are worth recommending. -不过,这些书籍都比较偏理论,只能帮助你建立一个写优秀代码的意识标准。 如果你想要编写更高质量的代码、更高质量的软件,还是应该多去看优秀的源码,多去学习优秀的代码实践。 +However, these books are more theoretical and can only help you establish a standard for writing excellent code. If you want to write higher quality code and better software, you should read more excellent source code and learn outstanding coding practices. -## 代码整洁之道 +## The Clean Code -**[《重构》](https://book.douban.com/subject/30468597/)** +**[“Refactoring”](https://book.douban.com/subject/30468597/)** ![](https://oss.javaguide.cn/github/javaguide/books/20210328174841577.png) -必看书籍!无需多言。编程书籍领域的瑰宝。 +A must-read! Need I say more? A gem in the programming book field. -世界顶级、国宝级别的 Martin Fowler 的书籍,可以说是软件开发领域最经典的几本书之一。目前已经出了第二版。 +A world-class, national treasure-level book by Martin Fowler, one of the most classic books in software development. The second edition has already been released. -这是一本值得你看很多遍的书籍。 +This is a book worth reading several times. -**[《Clean Code》](https://book.douban.com/subject/4199741/)** +**[“Clean Code”](https://book.douban.com/subject/4199741/)** ![](https://oss.javaguide.cn/github/javaguide/books/20210328174824891.png) -《Clean Code》是 Bob 大叔的一本经典著作,强烈建议小伙伴们一定要看看。 +“Clean Code” is a classic work by Uncle Bob, and I strongly recommend that everyone take a look. -Bob 大叔将自己对整洁代码的理解浓缩在了这本书中,真可谓是对后生的一大馈赠。 +Uncle Bob distilled his understanding of clean code into this book, which is a significant gift to future generations. -**[《Effective Java 》](https://book.douban.com/subject/30412517/)** +**[“Effective Java”](https://book.douban.com/subject/30412517/)** ![](https://oss.javaguide.cn/github/javaguide/books/82d510c951384383b325080428af6c0a.png) -《Effective Java 》这本书是 Java 领域国宝级别的书,非常经典。Java 程序员必看! +“Effective Java” is a national treasure-level book in the field of Java, and it is very classic. A must-read for Java programmers! -这本书主要介绍了在 Java 编程中很多极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。这篇文章能够非常实际地帮助你写出更加清晰、健壮和高效的代码。本书中的每条规则都以简短、独立的小文章形式出现,并通过例子代码加以进一步说明。 +This book primarily introduces many practical experience rules in Java programming; these rules cover solutions to most problems developers face daily. This book can practically help you write clearer, more robust, and efficient code. Each rule in this book appears in the form of a short, independent article and is further illustrated with example code. -**[《代码大全》](https://book.douban.com/subject/1477390/)** +**[“Code Complete”](https://book.douban.com/subject/1477390/)** ![](https://oss.javaguide.cn/github/javaguide/books/20210314173253221.png) -其实,《代码大全(第 2 版)》这本书我本身是不太想推荐给大家了。但是,看在它的豆瓣评分这么高的份上,还是拿出来说说吧! +In fact, I originally did not want to recommend “Code Complete (2nd Edition)” to everyone. However, given its high Douban rating, I decided to mention it! -这也是一本非常经典的书籍,第二版对第一版进行了重写。 +This is also a very classic book, with the second edition rewritten from the first. -我简单地浏览过全书的内容,感觉内容总体比较虚,对于大部分程序员的作用其实不大。如果你想要切实地提高自己的代码质量,《Clean Code》和 《编写可读代码的艺术》我觉得都要比《代码大全》这本书更好。 +I briefly skimmed through the content of the book and felt that the content is generally superficial, and its usefulness for most programmers is limited. If you want to improve your code quality, I think “Clean Code” and “The Art of Readable Code” are better options than “Code Complete.” -不过,最重要的还是要多看优秀的源码,多学习优秀的代码实践。 +However, the most important thing is still to read excellent source code and learn outstanding coding practices. -**[《编写可读代码的艺术》](https://book.douban.com/subject/10797189/)** +**[“The Art of Readable Code”](https://book.douban.com/subject/10797189/)** ![](https://oss.javaguide.cn/github/javaguide/books/20210314175536443.png) -《编写可读代码的艺术》这本书要表达的意思和《Clean Code》很像,你看它俩的目录就可以看出来了。 +“The Art of Readable Code” conveys ideas very similar to “Clean Code,” as you can see from their table of contents. ![](https://oss.javaguide.cn/github/javaguide/books/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70-20230309230739963.png) -在我看来,如果你看过 《Clean Code》 的话,就不需要再看这本书了。当然,如果你有时间和精力,也可以快速过一遍。 +In my opinion, if you have read “Clean Code,” you do not need to read this book again. Of course, if you have time and energy, you can quickly skim through it. -另外,我这里还要推荐一个叫做 **[write-readable-code](https://github.com/biezhi/write-readable-code)** 的仓库。这个仓库的作者免费分享了一系列基于《编写可读代码的艺术》这本书的视频。这一系列视频会基于 Java 语言来教你如何优化咱们的代码。 +Additionally, I want to recommend a repository called **[write-readable-code](https://github.com/biezhi/write-readable-code)**. The author of this repository shares a series of videos based on “The Art of Readable Code” for free. This series of videos will teach you how to optimize our code based on Java. -在实践中学习的效果肯定会更好!推荐小伙伴们都抓紧学起来啊! +Learning in practice will definitely be more effective! I recommend everyone to start studying right away! ![](https://oss.javaguide.cn/github/javaguide/books/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70-20230309230743258.png) -## 程序员职业素养 +## Professional Qualities of Programmers -**[《The Clean Coder》](https://book.douban.com/subject/26919457/)** +**[“The Clean Coder”](https://book.douban.com/subject/26919457/)** ![](https://oss.javaguide.cn/github/javaguide/books/20210314191210273.png) -《 The Clean Coder》是 Bob 大叔的又一经典著作。 +“The Clean Coder” is another classic by Uncle Bob. -《Clean Code》和《 The Clean Coder》这两本书在国内都翻译为 《代码整洁之道》,我觉得这个翻译还是不够优雅的。 +Both “Clean Code” and “The Clean Coder” are translated in China as “The Clean Code Way,” which I feel is not an elegant translation. -另外,两者的内容差异也很大。《Clean Code》这本书从代码层面来讲解如何提高自己的代码质量。而《The Clean Coder》这本书则是从如何成为一名更优秀的开发者的角度来写的,比如这书会教你如何在自己的领域更专业、如何说不、如何做时间管理、如何处理压力等等。 +Additionally, the content differences between the two are quite significant. “Clean Code” discusses how to improve code quality from the code perspective, while “The Clean Coder” is focused on how to become a better developer, teaching you about professionalism in your field, how to say no, time management, stress handling, and more. -## 架构整洁之道 +## The Clean Architecture -**[《架构整洁之道》](https://book.douban.com/subject/30333919/)** +**[“Clean Architecture”](https://book.douban.com/subject/30333919/)** ![](https://oss.javaguide.cn/github/javaguide/books/2021031412342771.png) -你没看错,《架构整洁之道》这本书又是 Bob 大叔的经典之作。 +You read that right, “Clean Architecture” is another classic by Uncle Bob. -这本书我强烈安利!认真读完之后,我保证你对编程本质、编程语言的本质、软件设计、架构设计可以有进一步的认识。 +I highly recommend this book! After reading it thoroughly, I assure you will gain a deeper understanding of the essence of programming, the nature of programming languages, software design, and architecture design. -国内的很多书籍和专栏都借鉴了《架构整洁之道》 这本书。毫不夸张地说,《架构整洁之道》就是架构领域最经典的书籍之一。 +Many books and columns in China have drawn inspiration from “Clean Architecture.” It is not an exaggeration to say that “Clean Architecture” is one of the most classic books in the field of architecture. -正如作者说的那样: +As the author states: -> 如果深入研究计算机编程的本质,我们就会发现这 50 年来,计算机编程基本没有什么大的变化。编程语言稍微进步了一点,工具的质量大大提升了,但是计算机程序的基本构造没有什么变化。 +> If we delve into the essence of computer programming, we will find that there hasn't been much change in computer programming in the past 50 years. Programming languages have progressed slightly, tools have greatly improved in quality, but the fundamental structure of computer programs has not changed. > -> 虽然我们有了新的编程语言、新的编程框架、新的编程范式,但是软件架构的规则仍然和 1946 年阿兰·图灵写下第一行机器代码的时候一样。 +> Although we have new programming languages, new frameworks, and new paradigms, the rules of software architecture remain the same as they were when Alan Turing wrote the first line of machine code in 1946. > -> 这本书就是为了把这些永恒不变的软件架构规则展现出来。 +> This book is intended to present these timeless rules of software architecture. -## 项目管理 +## Project Management -**[《人月神话》](https://book.douban.com/subject/1102259/)** +**[“The Mythical Man-Month”](https://book.douban.com/subject/1102259/)** ![](https://oss.javaguide.cn/2021/03/8ece325c-4491-4ffd-9d3d-77e95159ec40.png) -这本书主要描述了软件开发的基本定律:**一个需要 10 天才能干完的活,不可能让 10 个人在 1 天干完!** +This book primarily describes the fundamental laws of software development: **A task that takes 10 days to complete cannot be done by 10 people in 1 day!** -看书名的第一眼,感觉不像是技术类的书籍。但是,就是这样一个看似和编程不沾边的书名,却成了编程领域长久相传的经典。 +At first glance, the book title seems unrelated to technical topics. However, such a seemingly non-programming title has become a long-lasting classic in the field of programming. -**这本书对于现代软件尤其是复杂软件的开发的规范化有深刻的意义。** +**This book has profound significance for the standardization of modern software, especially complex software development.** -**[《领域驱动设计:软件核心复杂性应对之道》](https://book.douban.com/subject/5344973/)** +**[“Domain-Driven Design: Tackling Complexity in the Heart of Software”](https://book.douban.com/subject/5344973/)** ![](https://oss.javaguide.cn/2021/03/7e80418d-20b1-4066-b9af-cfe434b1bf1a.png) -这本领域驱动设计方面的经典之作一直被各种推荐,但是我还来及读。 +This classic work on domain-driven design has been recommended numerous times, but I haven't gotten around to reading it yet. -## 其他 +## Others -- [《代码的未来》](https://book.douban.com/subject/24536403/):这本书的作者是 Ruby 之父松本行弘,算是一本年代比较久远的书籍(13 年出版),不过,还是非常值得一读。这本书的内容主要介绍是编程/编程语言的本质。我个人还是比较喜欢松本行弘的文字风格,并且,你看他的文章也确实能够有所收获。 -- [《深入浅出设计模式》](https://book.douban.com/subject/1488876/):比较有趣的风格,适合设计模式入门。 -- [《软件架构设计:大型网站技术架构与业务架构融合之道》](https://book.douban.com/subject/30443578/):内容非常全面。适合面试前突击一些比较重要的理论知识,也适合拿来扩充/完善自己的技术广度。 -- [《微服务架构设计模式》](https://book.douban.com/subject/33425123/):这本书是世界十大软件架构师之一、微服务架构先驱 Chris Richardson 亲笔撰写,豆瓣评分 9.6。示例代码使用 Java 语言和 Spring 框架。帮助你设计、实现、测试和部署基于微服务的应用程序。 +- [“The Future of Code”](https://book.douban.com/subject/24536403/): This book is authored by Yukihiro Matsumoto, the father of Ruby. It is relatively old (published in 2013), but it is definitely worth a read. The book’s content primarily discusses the essence of programming/programming languages. I personally appreciate Matsumoto's writing style, and his articles offer valuable insights. +- [“Understanding Design Patterns”](https://book.douban.com/subject/1488876/): A rather interesting style, suitable for beginners in design patterns. +- [“Software Architecture Design: The Integration of Technical Architecture and Business Architecture for Large Websites”](https://book.douban.com/subject/30443578/): Very comprehensive content. Suitable for cramming important theoretical knowledge before interviews, and also good for expanding/perfecting your technical breadth. +- [“Microservices Patterns”](https://book.douban.com/subject/33425123/): This book is authored by Chris Richardson, one of the top ten software architects in the world and a pioneer of microservices architecture, with a Douban rating of 9.6. The example code uses Java and the Spring framework. It helps you design, implement, test, and deploy applications based on microservices. -最后再推荐两个相关的文档: +Lastly, here are two related documents: -- **阿里巴巴 Java 开发手册**: -- **Google Java 编程风格指南**: +- **Alibaba Java Development Handbook**: +- **Google Java Style Guide**: diff --git a/docs/database/basis.md b/docs/database/basis.md index 1df5d538fb8..f8d968c66d8 100644 --- a/docs/database/basis.md +++ b/docs/database/basis.md @@ -1,155 +1,155 @@ --- -title: 数据库基础知识总结 -category: 数据库 +title: Summary of Basic Knowledge of Databases +category: Database tag: - - 数据库基础 + - Database Fundamentals --- -数据库知识基础,这部分内容一定要理解记忆。虽然这部分内容只是理论知识,但是非常重要,这是后面学习 MySQL 数据库的基础。PS: 这部分内容由于涉及太多概念性内容,所以参考了维基百科和百度百科相应的介绍。 +The foundation of database knowledge is essential to understand and remember. Although this part consists only of theoretical knowledge, it is very important as it is the basis for learning MySQL databases later on. PS: Due to the involvement of many conceptual contents in this section, references have been taken from the corresponding introductions in Wikipedia and Baidu Encyclopedia. -## 什么是数据库, 数据库管理系统, 数据库系统, 数据库管理员? +## What are databases, database management systems, database systems, and database administrators? -- **数据库** : 数据库(DataBase 简称 DB)就是信息的集合或者说数据库是由数据库管理系统管理的数据的集合。 -- **数据库管理系统** : 数据库管理系统(Database Management System 简称 DBMS)是一种操纵和管理数据库的大型软件,通常用于建立、使用和维护数据库。 -- **数据库系统** : 数据库系统(Data Base System,简称 DBS)通常由软件、数据库和数据管理员(DBA)组成。 -- **数据库管理员** : 数据库管理员(Database Administrator, 简称 DBA)负责全面管理和控制数据库系统。 +- **Database**: A database (abbreviated as DB) is a collection of information, or more specifically, a database is a collection of data managed by a database management system. +- **Database Management System**: A database management system (abbreviated as DBMS) is a large software that manipulates and manages databases, commonly used to create, use, and maintain databases. +- **Database System**: A database system (abbreviated as DBS) typically consists of software, databases, and a database administrator (DBA). +- **Database Administrator**: The database administrator (abbreviated as DBA) is responsible for the overall management and control of the database system. -## 什么是元组, 码, 候选码, 主码, 外码, 主属性, 非主属性? +## What are tuples, keys, candidate keys, primary keys, foreign keys, primary attributes, and non-primary attributes? -- **元组**:元组(tuple)是关系数据库中的基本概念,关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,元组也称为行。 -- **码**:码就是能唯一标识实体的属性,对应表中的列。 -- **候选码**:若关系中的某一属性或属性组的值能唯一的标识一个元组,而其任何、子集都不能再标识,则称该属性组为候选码。例如:在学生实体中,“学号”是能唯一的区分学生实体的,同时又假设“姓名”、“班级”的属性组合足以区分学生实体,那么{学号}和{姓名,班级}都是候选码。 -- **主码** : 主码也叫主键。主码是从候选码中选出来的。 一个实体集中只能有一个主码,但可以有多个候选码。 -- **外码** : 外码也叫外键。如果一个关系中的一个属性是另外一个关系中的主码则这个属性为外码。 -- **主属性**:候选码中出现过的属性称为主属性。比如关系 工人(工号,身份证号,姓名,性别,部门). 显然工号和身份证号都能够唯一标示这个关系,所以都是候选码。工号、身份证号这两个属性就是主属性。如果主码是一个属性组,那么属性组中的属性都是主属性。 -- **非主属性:** 不包含在任何一个候选码中的属性称为非主属性。比如在关系——学生(学号,姓名,年龄,性别,班级)中,主码是“学号”,那么其他的“姓名”、“年龄”、“性别”、“班级”就都可以称为非主属性。 +- **Tuple**: A tuple is a fundamental concept in relational databases. A relation is a table, and each row (i.e., each record in the database) is a tuple, while each column is an attribute. In a two-dimensional table, tuples are also referred to as rows. +- **Key**: A key is an attribute that can uniquely identify an entity, corresponding to a column in a table. +- **Candidate Key**: If a certain attribute or group of attributes in a relation can uniquely identify a tuple, and no subset can do so, this group of attributes is called a candidate key. For example, in the student entity, the "student ID" uniquely differentiates student entities. Assuming that the combination of the attributes "name" and "class" is sufficient to distinguish student entities, both {student ID} and {name, class} are candidate keys. +- **Primary Key**: A primary key is a key selected from the candidate keys. An entity set can have only one primary key but may have multiple candidate keys. +- **Foreign Key**: A foreign key is an attribute in one relation that is a primary key in another relation. +- **Primary Attribute**: Attributes that appear in the candidate keys are called primary attributes. For instance, in the relation worker (employee ID, ID number, name, gender, department), both employee ID and ID number can uniquely identify this relation, so they are both candidate keys. Employee ID and ID number are primary attributes. If the primary key is a set of attributes, then all attributes in that set are primary attributes. +- **Non-primary Attribute**: Attributes not included in any candidate keys are called non-primary attributes. For instance, in the relation student (student ID, name, age, gender, class), if the primary key is "student ID," then "name," "age," "gender," and "class" are all considered non-primary attributes. -## 什么是 ER 图? +## What is an ER diagram? -我们做一个项目的时候一定要试着画 ER 图来捋清数据库设计,这个也是面试官问你项目的时候经常会被问到的。 +When working on a project, it's important to try drawing an ER diagram to clarify database design. This is often a question asked by interviewers when discussing your projects. -**ER 图** 全称是 Entity Relationship Diagram(实体联系图),提供了表示实体类型、属性和联系的方法。 +**ER Diagram** stands for Entity Relationship Diagram, which provides a method to represent entity types, attributes, and relationships. -ER 图由下面 3 个要素组成: +An ER diagram consists of the following three elements: -- **实体**:通常是现实世界的业务对象,当然使用一些逻辑对象也可以。比如对于一个校园管理系统,会涉及学生、教师、课程、班级等等实体。在 ER 图中,实体使用矩形框表示。 -- **属性**:即某个实体拥有的属性,属性用来描述组成实体的要素,对于产品设计来说可以理解为字段。在 ER 图中,属性使用椭圆形表示。 -- **联系**:即实体与实体之间的关系,在 ER 图中用菱形表示,这个关系不仅有业务关联关系,还能通过数字表示实体之间的数量对照关系。例如,一个班级会有多个学生就是一种实体间的联系。 +- **Entity**: Typically represents real-world business objects; logical objects can also be used. For instance, in a campus management system, entities may include students, teachers, courses, classes, etc. In an ER diagram, entities are represented by rectangular boxes. +- **Attributes**: Attributes describe the elements that make up the entity. In product design, they can be understood as fields. In an ER diagram, attributes are represented by oval shapes. +- **Relationships**: Relationships denote the connections between entities and are represented by diamonds in the ER diagram. These relationships can reflect not only business associations but also the quantity correspondence between entities using numbers. For example, a class may have multiple students, which illustrates a relationship between entities. -下图是一个学生选课的 ER 图,每个学生可以选若干门课程,同一门课程也可以被若干人选择,所以它们之间的关系是多对多(M: N)。另外,还有其他两种实体之间的关系是:1 对 1(1:1)、1 对多(1: N)。 +The following diagram is an ER diagram for student course selection, where each student can select multiple courses, and the same course can be selected by multiple students, making this a many-to-many (M:N) relationship. Additionally, there are two other types of relationships between entities: one-to-one (1:1) and one-to-many (1:N). -![学生与课程之间联系的E-R图](https://oss.javaguide.cn/github/javaguide/csdn/c745c87f6eda9a439e0eea52012c7f4a.png) +![ER Diagram of Student and Course Relationship](https://oss.javaguide.cn/github/javaguide/csdn/c745c87f6eda9a439e0eea52012c7f4a.png) -## 数据库范式了解吗? +## Are you familiar with database normalization? -数据库范式有 3 种: +There are three types of database normalization: -- 1NF(第一范式):属性不可再分。 -- 2NF(第二范式):1NF 的基础之上,消除了非主属性对于码的部分函数依赖。 -- 3NF(第三范式):3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。 +- 1NF (First Normal Form): Attributes cannot be further divided. +- 2NF (Second Normal Form): Building on 1NF, it eliminates partial functional dependencies of non-primary attributes regarding keys. +- 3NF (Third Normal Form): 3NF, based on 2NF, eliminates transitive functional dependencies of non-primary attributes regarding keys. -### 1NF(第一范式) +### 1NF (First Normal Form) -属性(对应于表中的字段)不能再被分割,也就是这个字段只能是一个值,不能再分为多个其他的字段了。**1NF 是所有关系型数据库的最基本要求** ,也就是说关系型数据库中创建的表一定满足第一范式。 +Attributes (corresponding to fields in the table) cannot be further divided, meaning this field can only contain a single value and cannot be broken down into multiple other fields. **1NF is the most basic requirement for all relational databases**, implying that tables created in a relational database must satisfy the first normal form. -### 2NF(第二范式) +### 2NF (Second Normal Form) -2NF 在 1NF 的基础之上,消除了非主属性对于码的部分函数依赖。如下图所示,展示了第一范式到第二范式的过渡。第二范式在第一范式的基础上增加了一个列,这个列称为主键,非主属性都依赖于主键。 +2NF builds on 1NF by eliminating partial functional dependencies of non-primary attributes on keys. The diagram below demonstrates the transition from the first normal form to the second normal form. The second normal form adds a column based on the first normal form, designated as the primary key, where non-primary attributes depend on the primary key. -![第二范式](https://oss.javaguide.cn/github/javaguide/csdn/bd1d31be3779342427fc9e462bf7f05c.png) +![Second Normal Form](https://oss.javaguide.cn/github/javaguide/csdn/bd1d31be3779342427fc9e462bf7f05c.png) -一些重要的概念: +Some important concepts: -- **函数依赖(functional dependency)**:若在一张表中,在属性(或属性组)X 的值确定的情况下,必定能确定属性 Y 的值,那么就可以说 Y 函数依赖于 X,写作 X → Y。 -- **部分函数依赖(partial functional dependency)**:如果 X→Y,并且存在 X 的一个真子集 X0,使得 X0→Y,则称 Y 对 X 部分函数依赖。比如学生基本信息表 R 中(学号,身份证号,姓名)当然学号属性取值是唯一的,在 R 关系中,(学号,身份证号)->(姓名),(学号)->(姓名),(身份证号)->(姓名);所以姓名部分函数依赖于(学号,身份证号); -- **完全函数依赖(Full functional dependency)**:在一个关系中,若某个非主属性数据项依赖于全部关键字称之为完全函数依赖。比如学生基本信息表 R(学号,班级,姓名)假设不同的班级学号有相同的,班级内学号不能相同,在 R 关系中,(学号,班级)->(姓名),但是(学号)->(姓名)不成立,(班级)->(姓名)不成立,所以姓名完全函数依赖与(学号,班级); -- **传递函数依赖**:在关系模式 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(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖。 +- **Functional Dependency**: In a table, if the value of attribute (or set of attributes) X determines the value of attribute Y, then Y is said to be functionally dependent on X, denoted as X → Y. +- **Partial Functional Dependency**: If X → Y, and there exists a proper subset X0 of X such that X0 → Y, then Y is partially functionally dependent on X. For instance, in the table of basic student information R (student ID, ID number, name), the student ID attribute is unique; thus, in relation R, (student ID, ID number) → (name), (student ID) → (name), and (ID number) → (name) hold; therefore, the name is partially functionally dependent on (student ID, ID number). +- **Full Functional Dependency**: If a non-primary attribute data item depends on the entire key in a relation, it is called full functional dependency. For instance, in the basic student information table R (student ID, class, name), assuming there are identical student IDs across different classes but they cannot be the same within the class, in relation R, (student ID, class) → (name) holds, but (student ID) → (name) and (class) → (name) do not hold; therefore, name is fully functionally dependent on (student ID, class). +- **Transitive Functional Dependency**: In relation schema R(U), let X, Y, Z be different subsets of U. If X determines Y, Y determines Z, and neither Y include X nor X includes Y, and (X ∪ Y) ∩ Z = empty set, Z is said to be transitively functionally dependent on X. Transitive functional dependency can lead to data redundancy and anomalies. The subsets Y and Z of transitive functional dependency often belong to the same entity, so they can be merged into a single table. For instance, in relation R (student ID, name, department name, department head), student ID → department name, and department name → department head, creating a transitive functional dependency of the non-primary attribute department head concerning student ID. -### 3NF(第三范式) +### 3NF (Third Normal Form) -3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。符合 3NF 要求的数据库设计,**基本**上解决了数据冗余过大,插入异常,修改异常,删除异常的问题。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖,所以该表的设计,不符合 3NF 的要求。 +3NF builds on 2NF by eliminating transitive functional dependencies of non-primary attributes regarding keys. A database design that meets the requirements of 3NF essentially resolves the issues of excessive data redundancy, insertion anomalies, modification anomalies, and deletion anomalies. For instance, in relation R (student ID, name, department name, department head), student ID → department name and department name → department head create a transitive functional dependency of the non-primary attribute department head regarding student ID, making this table design not comply with 3NF’s requirements. -## 主键和外键有什么区别? +## What is the difference between primary keys and foreign keys? -- **主键(主码)**:主键用于唯一标识一个元组,不能有重复,不允许为空。一个表只能有一个主键。 -- **外键(外码)**:外键用来和其他表建立联系用,外键是另一表的主键,外键是可以有重复的,可以是空值。一个表可以有多个外键。 +- **Primary Key**: A primary key uniquely identifies a tuple, cannot be duplicated, and is not allowed to be null. A table can have only one primary key. +- **Foreign Key**: A foreign key is used to establish relationships with other tables. A foreign key is a primary key from another table and can have duplicates or null values. A table can have multiple foreign keys. -## 为什么不推荐使用外键与级联? +## Why is the use of foreign keys and cascading updates not recommended? -对于外键和级联,阿里巴巴开发手册这样说到: +Regarding foreign keys and cascading updates, the Alibaba development manual states: -> 【强制】不得使用外键与级联,一切外键概念必须在应用层解决。 +> **Mandatory**: Do not use foreign keys and cascading operations; all foreign key concepts must be resolved at the application layer. > -> 说明: 以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度 +> Explanation: For example, in the relationship between students and grades, if the student_id in the student table is a primary key, then the student_id in the grades table is a foreign key. If the student_id in the student table is updated and triggers the update of student_id in the grades table, this is a cascading update. Foreign keys and cascading updates are suitable for low concurrency in standalone scenarios, but they are not suitable for distributed or high-concurrency clusters; cascading updates involve strong blocking, posing a risk of database update storms; foreign keys affect the insertion speed of the database. -为什么不要用外键呢?大部分人可能会这样回答: +Why should foreign keys not be used? Most people might answer as follows: -1. **增加了复杂性:** a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束,会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的,假如哪天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。 -2. **增加了额外工作**:数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗数据库资源。如果在应用层面去维护的话,可以减小数据库压力; -3. **对分库分表不友好**:因为分库分表下外键是无法生效的。 -4. …… +1. **Increased Complexity**: a. Every DELETE or UPDATE must consider foreign key constraints, making development painful and testing data cumbersome; b. The master-slave relationship of foreign keys is fixed. If the requirements change and the database no longer needs this field to be associated with other tables, it can cause many issues. +1. **Additional Work**: The database must maintain foreign keys, requiring additional operations when performing insertions, deletions, or updates involving foreign key fields to ensure data consistency and correctness, consuming database resources. If handled at the application level, database pressure can be alleviated. +1. **Not Friendly for Sharding**: Foreign keys cannot be effective in sharded database scenarios. +1. … -我个人觉得上面这种回答不是特别的全面,只是说了外键存在的一个常见的问题。实际上,我们知道外键也是有很多好处的,比如: +I personally feel that the above answers are not particularly comprehensive; they only address a common issue with foreign keys. In reality, we know that foreign keys also have many advantages, such as: -1. 保证了数据库数据的一致性和完整性; -2. 级联操作方便,减轻了程序代码量; -3. …… +1. Ensuring data consistency and integrity in the database. +1. Facilitating cascading operations and reducing programmatic code. +1. … -所以说,不要一股脑的就抛弃了外键这个概念,既然它存在就有它存在的道理,如果系统不涉及分库分表,并发量不是很高的情况还是可以考虑使用外键的。 +Therefore, it is essential not to dismiss the concept of foreign keys altogether. As they exist for a reason, if the system does not involve database sharding and the concurrency is not very high, using foreign keys should still be considered. -## 什么是存储过程? +## What is a stored procedure? -我们可以把存储过程看成是一些 SQL 语句的集合,中间加了点逻辑控制语句。存储过程在业务比较复杂的时候是非常实用的,比如很多时候我们完成一个操作可能需要写一大串 SQL 语句,这时候我们就可以写有一个存储过程,这样也方便了我们下一次的调用。存储过程一旦调试完成通过后就能稳定运行,另外,使用存储过程比单纯 SQL 语句执行要快,因为存储过程是预编译过的。 +A stored procedure can be viewed as a collection of SQL statements complemented by logical control statements. Stored procedures are quite useful in complex business scenarios. For example, often when we complete an operation, we might need to write a long series of SQL statements; in such cases, we can create a stored procedure, making it easier for future calls. Once a stored procedure is debugged and complete, it can run reliably, and utilizing stored procedures is generally faster than executing pure SQL statements, as stored procedures are pre-compiled. -存储过程在互联网公司应用不多,因为存储过程难以调试和扩展,而且没有移植性,还会消耗数据库资源。 +Stored procedures are not widely used in internet companies, as they can be difficult to debug and extend, lack portability, and consume database resources. -阿里巴巴 Java 开发手册里要求禁止使用存储过程。 +The Alibaba Java development manual mandates the prohibition of stored procedures. -![阿里巴巴Java开发手册: 禁止存储过程](https://oss.javaguide.cn/github/javaguide/csdn/0fa082bc4d4f919065767476a41b2156.png) +![Alibaba Java Development Manual: Prohibition of Stored Procedures](https://oss.javaguide.cn/github/javaguide/csdn/0fa082bc4d4f919065767476a41b2156.png) -## drop、delete 与 truncate 区别? +## What is the difference between drop, delete, and truncate? -### 用法不同 +### Different Usage -- `drop`(丢弃数据): `drop table 表名` ,直接将表都删除掉,在删除表的时候使用。 -- `truncate` (清空数据) : `truncate table 表名` ,只删除表中的数据,再插入数据的时候自增长 id 又从 1 开始,在清空表中数据的时候使用。 -- `delete`(删除数据) : `delete from 表名 where 列名=值`,删除某一行的数据,如果不加 `where` 子句和`truncate table 表名`作用类似。 +- `drop` (delete data): `drop table table_name`, directly deletes the table itself and is used when removing a table. +- `truncate` (empty data): `truncate table table_name`, only deletes the data within the table, and when inserting data again, the auto-incrementing ID starts from 1 again; it's used when clearing data from a table. +- `delete` (delete data): `delete from table_name where column_name=value`, deletes data from a specific row without a `where` clause, similar to the effect of `truncate table table_name`. -`truncate` 和不带 `where`子句的 `delete`、以及 `drop` 都会删除表内的数据,但是 **`truncate` 和 `delete` 只删除数据不删除表的结构(定义),执行 `drop` 语句,此表的结构也会删除,也就是执行`drop` 之后对应的表不复存在。** +Both `truncate` and `delete` (without a `where` clause), as well as `drop`, will delete data from the tables. However, **`truncate` and `delete` only remove data without deleting the table structure (definition), while executing the `drop` statement deletes the table structure as well, meaning the table does not exist after executing `drop`.** -### 属于不同的数据库语言 +### Different Database Languages -`truncate` 和 `drop` 属于 DDL(数据定义语言)语句,操作立即生效,原数据不放到 rollback segment 中,不能回滚,操作不触发 trigger。而 `delete` 语句是 DML (数据库操作语言)语句,这个操作会放到 rollback segment 中,事务提交之后才生效。 +`truncate` and `drop` are classified as DDL (Data Definition Language) statements, which take effect immediately, and the original data is not placed in the rollback segment, meaning operations cannot be rolled back, and triggers are not invoked. Conversely, the `delete` statement is a DML (Data Manipulation Language) statement, where the operation is placed in the rollback segment and only takes effect after the transaction is committed. -**DML 语句和 DDL 语句区别:** +**Differences between DML and DDL statements:** -- DML 是数据库操作语言(Data Manipulation Language)的缩写,是指对数据库中表记录的操作,主要包括表记录的插入、更新、删除和查询,是开发人员日常使用最频繁的操作。 -- DDL (Data Definition Language)是数据定义语言的缩写,简单来说,就是对数据库内部的对象进行创建、删除、修改的操作语言。它和 DML 语言的最大区别是 DML 只是对表内部数据的操作,而不涉及到表的定义、结构的修改,更不会涉及到其他对象。DDL 语句更多的被数据库管理员(DBA)所使用,一般的开发人员很少使用。 +- DML stands for Data Manipulation Language, which refers to operations on records in database tables, mainly involving inserting, updating, deleting, and querying table records. These operations are the most frequently used by developers in day-to-day tasks. +- DDL stands for Data Definition Language, which refers to operations for creating, deleting, and modifying objects within the database. The main difference from DML is that DML pertains only to operations within table data without modifying anything related to table definitions or structures, nor does it involve other objects. DDL statements are generally more utilized by database administrators (DBAs) and are infrequently used by regular developers. -另外,由于`select`不会对表进行破坏,所以有的地方也会把`select`单独区分开叫做数据库查询语言 DQL(Data Query Language)。 +Additionally, since `select` does not destroy tables, in some contexts, `select` is distinctly classified as Data Query Language (DQL). -### 执行速度不同 +### Different Execution Speeds -一般来说:`drop` > `truncate` > `delete`(这个我没有实际测试过)。 +Generally, the performance order is: `drop` > `truncate` > `delete` (although I have not verified this empirically). -- `delete`命令执行的时候会产生数据库的`binlog`日志,而日志记录是需要消耗时间的,但是也有个好处方便数据回滚恢复。 -- `truncate`命令执行的时候不会产生数据库日志,因此比`delete`要快。除此之外,还会把表的自增值重置和索引恢复到初始大小等。 -- `drop`命令会把表占用的空间全部释放掉。 +- The `delete` command generates database `binlog` logs during execution, which requires time to record logs but has the advantage of facilitating data rollback recovery. +- The `truncate` command does not generate database logs during execution, making it faster than `delete`. Moreover, it also resets the table's auto-incrementing value and restores indexes to their initial size. +- The `drop` command wholly releases the space occupied by the table. -Tips:你应该更多地关注在使用场景上,而不是执行效率。 +Tips: You should focus more on the usage scenario rather than execution efficiency. -## 数据库设计通常分为哪几步? +## What are the typical steps in database design? -1. **需求分析** : 分析用户的需求,包括数据、功能和性能需求。 -2. **概念结构设计** : 主要采用 E-R 模型进行设计,包括画 E-R 图。 -3. **逻辑结构设计** : 通过将 E-R 图转换成表,实现从 E-R 模型到关系模型的转换。 -4. **物理结构设计** : 主要是为所设计的数据库选择合适的存储结构和存取路径。 -5. **数据库实施** : 包括编程、测试和试运行 -6. **数据库的运行和维护** : 系统的运行与数据库的日常维护。 +1. **Requirement Analysis**: Analyze user requirements, including data, functionality, and performance needs. +1. **Conceptual Structure Design**: Primarily use the E-R model for design, which includes drawing the E-R diagram. +1. **Logical Structure Design**: Transform the E-R diagram into tables, implementing the conversion from the E-R model to the relational model. +1. **Physical Structure Design**: Choose the appropriate storage structures and access paths for the designed database. +1. **Database Implementation**: Involves programming, testing, and trial runs. +1. **Operation and Maintenance of the Database**: Regular operation of the system and daily maintenance of the database. -## 参考 +## References - - diff --git a/docs/database/character-set.md b/docs/database/character-set.md index e462a5c97e3..7765e0535f5 100644 --- a/docs/database/character-set.md +++ b/docs/database/character-set.md @@ -1,139 +1,139 @@ --- -title: 字符集详解 -category: 数据库 +title: Character Set Explained +category: Database tag: - - 数据库基础 + - Database Basics --- -MySQL 字符编码集中有两套 UTF-8 编码实现:**`utf8`** 和 **`utf8mb4`**。 +MySQL's character encoding includes two implementations of UTF-8 encoding: **`utf8`** and **`utf8mb4`**. -如果使用 **`utf8`** 的话,存储 emoji 符号和一些比较复杂的汉字、繁体字就会出错。 +If you use **`utf8`**, storing emoji symbols and some more complex Chinese characters or traditional characters will lead to errors. -为什么会这样呢?这篇文章可以从源头给你解答。 +Why does this happen? This article will provide you with an explanation from the source. -## 字符集是什么? +## What is a character set? -字符是各种文字和符号的统称,包括各个国家文字、标点符号、表情、数字等等。 **字符集** 就是一系列字符的集合。字符集的种类较多,每个字符集可以表示的字符范围通常不同,就比如说有些字符集是无法表示汉字的。 +Characters are the general term for various letters and symbols, including letters from different countries, punctuation marks, emojis, numbers, and so on. A **character set** is a collection of a series of characters. There are many types of character sets, and the range of characters that each set can represent is usually different; for example, some character sets cannot represent Chinese characters. -**计算机只能存储二进制的数据,那英文、汉字、表情等字符应该如何存储呢?** +**Computers can only store binary data; how should characters like English, Chinese, and emojis be stored?** -我们要将这些字符和二进制的数据一一对应起来,比如说字符“a”对应“01100001”,反之,“01100001”对应 “a”。我们将字符对应二进制数据的过程称为"**字符编码**",反之,二进制数据解析成字符的过程称为“**字符解码**”。 +We need to establish a correspondence between these characters and binary data, for example, the character "a" corresponds to "01100001," and conversely, "01100001" corresponds to "a." The process of mapping characters to binary data is called "**character encoding**," and the process of interpreting binary data back into characters is called "**character decoding**." -## 字符编码是什么? +## What is character encoding? -字符编码是一种将字符集中的字符与计算机中的二进制数据相互转换的方法,可以看作是一种映射规则。也就是说,字符编码的目的是为了让计算机能够存储和传输各种文字信息。 +Character encoding is a method that converts characters in a character set to binary data in a computer and vice versa, which can be seen as a mapping rule. In other words, the purpose of character encoding is to enable computers to store and transmit various texts. -每种字符集都有自己的字符编码规则,常用的字符集编码规则有 ASCII 编码、 GB2312 编码、GBK 编码、GB18030 编码、Big5 编码、UTF-8 编码、UTF-16 编码等。 +Each character set has its own character encoding rules. Common character set encoding rules include ASCII encoding, GB2312 encoding, GBK encoding, GB18030 encoding, Big5 encoding, UTF-8 encoding, UTF-16 encoding, and so on. -## 有哪些常见的字符集? +## What are some common character sets? -常见的字符集有:ASCII、GB2312、GB18030、GBK、Unicode……。 +Common character sets include: ASCII, GB2312, GB18030, GBK, Unicode, etc. -不同的字符集的主要区别在于: +The main differences among different character sets are: -- 可以表示的字符范围 -- 编码方式 +- The range of characters they can represent +- Encoding methods ### ASCII -**ASCII** (**A**merican **S**tandard **C**ode for **I**nformation **I**nterchange,美国信息交换标准代码) 是一套主要用于现代美国英语的字符集(这也是 ASCII 字符集的局限性所在)。 +**ASCII** (**A**merican **S**tandard **C**ode for **I**nformation **I**nterchange) is a character set primarily for modern American English (this is also the limitation of the ASCII character set). -**为什么 ASCII 字符集没有考虑到中文等其他字符呢?** 因为计算机是美国人发明的,当时,计算机的发展还处于比较雏形的时代,还未在其他国家大规模使用。因此,美国发布 ASCII 字符集的时候没有考虑兼容其他国家的语言。 +**Why wasn't the ASCII character set designed to accommodate other characters, such as Chinese?** Because computers were invented by Americans, and at the time, the development of computers was still in a relatively primitive stage and had not been used extensively in other countries. Therefore, when the ASCII character set was published in the U.S., compatibility with other countries' languages was not considered. -ASCII 字符集至今为止共定义了 128 个字符,其中有 33 个控制字符(比如回车、删除)无法显示。 +The ASCII character set currently defines 128 characters, 33 of which are control characters (such as carriage return and delete) and cannot be displayed. -一个 ASCII 码长度是一个字节也就是 8 个 bit,比如“a”对应的 ASCII 码是“01100001”。不过,最高位是 0 仅仅作为校验位,其余 7 位使用 0 和 1 进行组合,所以,ASCII 字符集可以定义 128(2^7)个字符。 +An ASCII code has a length of one byte, which is 8 bits in total; for example, the ASCII code for "a" is "01100001." However, the highest bit is 0, merely used for parity, while the remaining 7 bits use 0 and 1 combinations, so the ASCII character set can define 128 (2^7) characters. -由于,ASCII 码可以表示的字符实在是太少了。后来,人们对其进行了扩展得到了 **ASCII 扩展字符集** 。ASCII 扩展字符集使用 8 位(bits)表示一个字符,所以,ASCII 扩展字符集可以定义 256(2^8)个字符。 +However, the number of characters represented by ASCII is quite limited. Later on, people expanded it to create the **ASCII extended character set**. The ASCII extended character set uses 8 bits to represent a character, allowing it to define 256 (2^8) characters. -![ASCII字符编码](https://oss.javaguide.cn/github/javaguide/csdn/c1c6375d08ca268690cef2b13591a5b4.png) +![ASCII Character Encoding](https://oss.javaguide.cn/github/javaguide/csdn/c1c6375d08ca268690cef2b13591a5b4.png) ### GB2312 -我们上面说了,ASCII 字符集是一种现代美国英语适用的字符集。因此,很多国家都捣鼓了一个适合自己国家语言的字符集。 +As mentioned above, the ASCII character set is suitable mainly for modern American English. Consequently, many countries have developed character sets suitable for their own languages. -GB2312 字符集是一种对汉字比较友好的字符集,共收录 6700 多个汉字,基本涵盖了绝大部分常用汉字。不过,GB2312 字符集不支持绝大部分的生僻字和繁体字。 +The GB2312 character set is a character set that is friendly to Chinese characters, encompassing over 6700 Chinese characters, covering most commonly used Chinese characters. However, the GB2312 character set does not support most rare characters and traditional Chinese characters. -对于英语字符,GB2312 编码和 ASCII 码是相同的,1 字节编码即可。对于非英字符,需要 2 字节编码。 +For English characters, GB2312 encoding is the same as ASCII code and requires 1 byte of encoding. Non-English characters require 2 bytes of encoding. ### GBK -GBK 字符集可以看作是 GB2312 字符集的扩展,兼容 GB2312 字符集,共收录了 20000 多个汉字。 +The GBK character set can be seen as an extension of the GB2312 character set, and it is compatible with the GB2312 character set and includes over 20,000 Chinese characters. -GBK 中 K 是汉语拼音 Kuo Zhan(扩展)中的“Kuo”的首字母。 +The 'K' in GBK stands for "Kuo Zhan" (扩展), which means "expansion" in Chinese. ### GB18030 -GB18030 完全兼容 GB2312 和 GBK 字符集,纳入中国国内少数民族的文字,且收录了日韩汉字,是目前为止最全面的汉字字符集,共收录汉字 70000 多个。 +GB18030 is fully compatible with the GB2312 and GBK character sets, incorporates the characters of China's ethnic minorities, and includes Japanese and Korean Chinese characters, making it the most comprehensive Chinese character set to date, encompassing over 70,000 Chinese characters. ### BIG5 -BIG5 主要针对的是繁体中文,收录了 13000 多个汉字。 +BIG5 is mainly targeted at traditional Chinese, containing over 13,000 Chinese characters. ### Unicode & UTF-8 -为了更加适合本国语言,诞生了很多种字符集。 +To better suit local languages, a variety of character sets have been born. -我们上面也说了不同的字符集可以表示的字符范围以及编码规则存在差异。这就导致了一个非常严重的问题:**使用错误的编码方式查看一个包含字符的文件就会产生乱码现象。** +As we mentioned before, there are differences in the range of characters that different character sets can represent and their encoding rules. This leads to a very serious problem: **using the wrong encoding method to view a file containing characters can lead to garbled text.** -就比如说你使用 UTF-8 编码方式打开 GB2312 编码格式的文件就会出现乱码。示例:“牛”这个汉字 GB2312 编码后的十六进制数值为 “C5A3”,而 “C5A3” 用 UTF-8 解码之后得到的却是 “ţ”。 +For example, opening a file encoded in GB2312 using UTF-8 will result in garbled text. For instance, the hexadecimal value of the Chinese character "牛" in GB2312 encoding is "C5A3," but when "C5A3" is decoded using UTF-8, the result is "ţ." -你可以通过这个网站在线进行编码和解码: +You can perform encoding and decoding online using this website: -![](https://oss.javaguide.cn/github/javaguide/csdn/836c49b117ee4408871b0020b74c991d.png) +![](https://oss.javaguide.cn/javaguide/csdn/836c49b117ee4408871b0020b74c991d.png) -这样我们就搞懂了乱码的本质:**编码和解码时用了不同或者不兼容的字符集** 。 +Thus, we understand the essence of garbled text: **different or incompatible character sets are used during encoding and decoding.** ![](https://oss.javaguide.cn/javaguide/a8808cbabeea49caa3af27d314fa3c02-1.jpg) -为了解决这个问题,人们就想:“如果我们能够有一种字符集将世界上所有的字符都纳入其中就好了!”。 +To resolve this issue, people thought, "If we could have a character set that includes all characters in the world, that would be great!" -然后,**Unicode** 带着这个使命诞生了。 +Then, **Unicode** was born with this mission. -Unicode 字符集中包含了世界上几乎所有已知的字符。不过,Unicode 字符集并没有规定如何存储这些字符(也就是如何使用二进制数据表示这些字符)。 +The Unicode character set contains almost all known characters in the world. However, Unicode does not stipulate how these characters should be stored (that is, how to represent these characters using binary data). -然后,就有了 **UTF-8**(**8**-bit **U**nicode **T**ransformation **F**ormat)。类似的还有 UTF-16、 UTF-32。 +Thus, **UTF-8** (**8**-bit **U**nicode **T**ransformation **F**ormat) came into existence. Similar formats include UTF-16, UTF-32. -UTF-8 使用 1 到 4 个字节为每个字符编码, UTF-16 使用 2 或 4 个字节为每个字符编码,UTF-32 固定位 4 个字节为每个字符编码。 +UTF-8 uses 1 to 4 bytes to encode each character, UTF-16 uses 2 or 4 bytes for each character, and UTF-32 uses a fixed 4 bytes for each character. -UTF-8 可以根据不同的符号自动选择编码的长短,像英文字符只需要 1 个字节就够了,这一点 ASCII 字符集一样 。因此,对于英语字符,UTF-8 编码和 ASCII 码是相同的。 +UTF-8 can automatically choose the encoding length based on different symbols: English characters only require 1 byte, just like the ASCII character set. Therefore, for English characters, UTF-8 encoding is the same as ASCII code. -UTF-32 的规则最简单,不过缺陷也比较明显,对于英文字母这类字符消耗的空间是 UTF-8 的 4 倍之多。 +UTF-32 has the simplest rules, but also a significant downside; for characters like English letters, it consumes four times the space of UTF-8. -**UTF-8** 是目前使用最广的一种字符编码。 +**UTF-8** is currently the most widely used character encoding. ![](https://oss.javaguide.cn/javaguide/1280px-Utf8webgrowth.svg.png) -## MySQL 字符集 +## MySQL Character Set -MySQL 支持很多种字符集的方式,比如 GB2312、GBK、BIG5、多种 Unicode 字符集(UTF-8 编码、UTF-16 编码、UCS-2 编码、UTF-32 编码等等)。 +MySQL supports many character sets, such as GB2312, GBK, BIG5, and various Unicode character sets (UTF-8 encoding, UTF-16 encoding, UCS-2 encoding, UTF-32 encoding, etc.). -### 查看支持的字符集 +### Viewing Supported Character Sets -你可以通过 `SHOW CHARSET` 命令来查看,支持 like 和 where 子句。 +You can view supported character sets using the `SHOW CHARSET` command, which supports like and where clauses. ![](https://oss.javaguide.cn/javaguide/image-20211008164229671.png) -### 默认字符集 +### Default Character Set -在 MySQL5.7 中,默认字符集是 `latin1` ;在 MySQL8.0 中,默认字符集是 `utf8mb4` +In MySQL 5.7, the default character set is `latin1`; in MySQL 8.0, the default character set is `utf8mb4`. -### 字符集的层次级别 +### Hierarchical Levels of Character Sets -MySQL 中的字符集有以下的层次级别: +MySQL character sets have the following hierarchical levels: -- `server`(MySQL 实例级别) -- `database`(库级别) -- `table`(表级别) -- `column`(字段级别) +- `server` (MySQL instance level) +- `database` (database level) +- `table` (table level) +- `column` (field level) -它们的优先级可以简单的认为是从上往下依次增大,也即 `column` 的优先级会大于 `table` 等其余层次的。如指定 MySQL 实例级别字符集是`utf8mb4`,指定某个表字符集是`latin1`,那么这个表的所有字段如果不指定的话,编码就是`latin1`。 +Their priority can be simply considered increasing from top to bottom, meaning that the `column` priority is higher than that of `table` and other levels. For example, if the MySQL instance level character set is `utf8mb4` and a specific table's character set is `latin1`, then all fields in that table will default to `latin1` unless otherwise specified. #### server -不同版本的 MySQL 其 `server` 级别的字符集默认值不同,在 MySQL5.7 中,其默认值是 `latin1` ;在 MySQL8.0 中,其默认值是 `utf8mb4` 。 +The default values for the `server` level character set vary by MySQL version. In MySQL 5.7, the default value is `latin1`; in MySQL 8.0, it is `utf8mb4`. -当然也可以通过在启动 `mysqld` 时指定 `--character-set-server` 来设置 `server` 级别的字符集。 +You can also specify the `--character-set-server` option when starting `mysqld` to set the `server` level character set. ```bash mysqld @@ -142,22 +142,22 @@ mysqld --character-set-server=utf8mb4 \ --collation-server=utf8mb4_0900_ai_ci ``` -或者如果你是通过源码构建的方式启动的 MySQL,你可以在 `cmake` 命令中指定选项: +Alternatively, if you start MySQL using the source build method, you can specify options in the `cmake` command: ```sh cmake . -DDEFAULT_CHARSET=latin1 -或者 +or cmake . -DDEFAULT_CHARSET=latin1 \ -DDEFAULT_COLLATION=latin1_german1_ci ``` -此外,你也可以在运行时改变 `character_set_server` 的值,从而达到修改 `server` 级别的字符集的目的。 +In addition, you can also change the value of `character_set_server` at runtime to modify the `server` level character set. -`server` 级别的字符集是 MySQL 服务器的全局设置,它不仅会作为创建或修改数据库时的默认字符集(如果没有指定其他字符集),还会影响到客户端和服务器之间的连接字符集,具体可以查看 [MySQL Connector/J 8.0 - 6.7 Using Character Sets and Unicode](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-charsets.html)。 +The `server` level character set is a global setting for the MySQL server. It serves as the default character set when creating or modifying databases (if no other character set is specified) and will also affect the character set used for communication between client and server. For more details, see [MySQL Connector/J 8.0 - 6.7 Using Character Sets and Unicode](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-charsets.html). #### database -`database` 级别的字符集是我们在创建数据库和修改数据库时指定的: +The `database` level character set is specified when creating and modifying databases: ```sql CREATE DATABASE db_name @@ -169,9 +169,9 @@ ALTER DATABASE db_name [[DEFAULT] COLLATE collation_name] ``` -如前面所说,如果在执行上述语句时未指定字符集,那么 MySQL 将会使用 `server` 级别的字符集。 +As mentioned before, if the character set is not specified when executing the above statements, MySQL will use the `server` level character set. -可以通过下面的方式查看某个数据库的字符集: +You can check the character set of a specific database using the following commands: ```sql USE db_name; @@ -185,7 +185,7 @@ FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = 'db_name'; #### table -`table` 级别的字符集是在创建表和修改表时指定的: +The `table` level character set is specified when creating and modifying tables: ```sql CREATE TABLE tbl_name (column_list) @@ -197,11 +197,11 @@ ALTER TABLE tbl_name [COLLATE collation_name] ``` -如果在创建表和修改表时未指定字符集,那么将会使用 `database` 级别的字符集。 +If no character set is specified when creating or modifying a table, the `database` level character set will be used. #### column -`column` 级别的字符集同样是在创建表和修改表时指定的,只不过它是定义在列中。下面是个例子: +The `column` level character set is also specified during table creation and modification, but it is defined within the column definition. Here is an example: ```sql CREATE TABLE t1 @@ -212,19 +212,19 @@ CREATE TABLE t1 ); ``` -如果未指定列级别的字符集,那么将会使用表级别的字符集。 +If no character set is specified at the column level, the table-level character set will be used. -### 连接字符集 +### Connection Character Set -前面说到了字符集的层次级别,它们是和存储相关的。而连接字符集涉及的是和 MySQL 服务器的通信。 +Earlier, we discussed the hierarchical levels of character sets, which are related to storage. The connection character set involves communication with the MySQL server. -连接字符集与下面这几个变量息息相关: +The connection character set is closely related to the following variables: -- `character_set_client` :描述了客户端发送给服务器的 SQL 语句使用的是什么字符集。 -- `character_set_connection` :描述了服务器接收到 SQL 语句时使用什么字符集进行翻译。 -- `character_set_results` :描述了服务器返回给客户端的结果使用的是什么字符集。 +- `character_set_client`: Describes which character set is used for SQL statements sent from the client to the server. +- `character_set_connection`: Describes which character set the server uses to translate SQL statements upon receipt. +- `character_set_results`: Describes the character set used for results returned from the server to the client. -它们的值可以通过下面的 SQL 语句查询: +The values of these variables can be queried using the following SQL statements: ```sql SELECT * FROM performance_schema.session_variables @@ -238,61 +238,61 @@ WHERE VARIABLE_NAME IN ( SHOW SESSION VARIABLES LIKE 'character\_set\_%'; ``` -如果要想修改前面提到的几个变量的值,有以下方式: +If you want to modify the values of the previously mentioned variables, you can do so in the following ways: -1、修改配置文件 +1. Modify the configuration file ```properties [mysql] -# 只针对MySQL客户端程序 +# Only for MySQL client programs default-character-set=utf8mb4 ``` -2、使用 SQL 语句 +2. Use SQL statements ```sql set names utf8mb4 -# 或者一个个进行修改 +# Or modify each one individually # SET character_set_client = utf8mb4; # SET character_set_results = utf8mb4; # SET collation_connection = utf8mb4; ``` -### JDBC 对连接字符集的影响 +### JDBC and Connection Character Set -不知道你们有没有碰到过存储 emoji 表情正常,但是使用类似 Navicat 之类的软件的进行查询的时候,发现 emoji 表情变成了问号的情况。这个问题很有可能就是 JDBC 驱动引起的。 +Have you ever encountered a situation where emojis are stored correctly, but when querying with software like Navicat, the emojis appear as question marks? This issue is likely caused by the JDBC driver. -根据前面的内容,我们知道连接字符集也是会影响我们存储的数据的,而 JDBC 驱动会影响连接字符集。 +From the previous content, we know that the connection character set also affects the data we store, and the JDBC driver impacts the connection character set. -`mysql-connector-java` (JDBC 驱动)主要通过这几个属性影响连接字符集: +The `mysql-connector-java` (JDBC driver) mainly affects the connection character set through the following properties: - `characterEncoding` - `characterSetResults` -以 `DataGrip 2023.1.2` 来说,在它配置数据源的高级对话框中,可以看到 `characterSetResults` 的默认值是 `utf8` ,在使用 `mysql-connector-java 8.0.25` 时,连接字符集最后会被设置成 `utf8mb3` 。那么这种情况下 emoji 表情就会被显示为问号,并且当前版本驱动还不支持把 `characterSetResults` 设置为 `utf8mb4` ,不过换成 `mysql-connector-java driver 8.0.29` 却是允许的。 +For example, in `DataGrip 2023.1.2`, in the advanced dialog for configuring data sources, the default value for `characterSetResults` is `utf8`. When using `mysql-connector-java 8.0.25`, the connection character set ends up being set to `utf8mb3`. In this case, emojis will be displayed as question marks, and the current version of the driver does not support setting `characterSetResults` to `utf8mb4`. However, switching to `mysql-connector-java driver 8.0.29` allows this. -具体可以看一下 StackOverflow 的 [DataGrip MySQL stores emojis correctly but displays them as?](https://stackoverflow.com/questions/54815419/datagrip-mysql-stores-emojis-correctly-but-displays-them-as)这个回答。 +For specifics, you can read the StackOverflow answer [DataGrip MySQL stores emojis correctly but displays them as?](https://stackoverflow.com/questions/54815419/datagrip-mysql-stores-emojis-correctly-but-displays-them-as). -### UTF-8 使用 +### Using UTF-8 -通常情况下,我们建议使用 UTF-8 作为默认的字符编码方式。 +Generally, we recommend using UTF-8 as the default character encoding method. -不过,这里有一个小坑。 +However, there is a small pitfall. -MySQL 字符编码集中有两套 UTF-8 编码实现: +MySQL's character encoding includes two implementations of UTF-8 encoding: -- **`utf8`**:`utf8`编码只支持`1-3`个字节 。 在 `utf8` 编码中,中文是占 3 个字节,其他数字、英文、符号占一个字节。但 emoji 符号占 4 个字节,一些较复杂的文字、繁体字也是 4 个字节。 -- **`utf8mb4`**:UTF-8 的完整实现,正版!最多支持使用 4 个字节表示字符,因此,可以用来存储 emoji 符号。 +- **`utf8`**: The `utf8` encoding supports only `1-3` bytes. In `utf8`, Chinese characters occupy 3 bytes, while numbers, English characters, and symbols occupy 1 byte each. However, emoji characters take up 4 bytes, as do some more complex characters and traditional Chinese characters. +- **`utf8mb4`**: The complete implementation of UTF-8, the "real" deal! It supports up to 4 bytes per character, making it suitable for storing emoji symbols. -**为什么有两套 UTF-8 编码实现呢?** 原因如下: +**Why are there two implementations of UTF-8?** The reason is as follows: ![](https://oss.javaguide.cn/javaguide/image-20211008164542347.png) -因此,如果你需要存储`emoji`类型的数据或者一些比较复杂的文字、繁体字到 MySQL 数据库的话,数据库的编码一定要指定为`utf8mb4` 而不是`utf8` ,要不然存储的时候就会报错了。 +As a result, if you need to store `emoji` data or some more complex characters or traditional Chinese characters in a MySQL database, the database's encoding must be specified as `utf8mb4`, not `utf8`, otherwise, an error will occur during storage. -演示一下吧!(环境:MySQL 5.7+) +Let’s demonstrate! (Environment: MySQL 5.7+) -建表语句如下,我们指定数据库 CHARSET 为 `utf8` 。 +Here is the create table statement, where we specify the database CHARSET as `utf8`. ```sql CREATE TABLE `user` ( @@ -303,31 +303,30 @@ CREATE TABLE `user` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` -当我们执行下面的 insert 语句插入数据到数据库时,果然报错! +When we execute the following insert statement to insert data into the database, an error indeed occurs! ```sql INSERT INTO `user` (`id`, `name`, `phone`, `password`) VALUES ('A00003', 'guide哥😘😘😘', '181631312312', '123456'); - ``` -报错信息如下: +The reported error message is as follows: ```plain Incorrect string value: '\xF0\x9F\x98\x98\xF0\x9F...' for column 'name' at row 1 ``` -## 参考 - -- 字符集和字符编码(Charset & Encoding): -- 十分钟搞清字符集和字符编码: -- Unicode-维基百科: -- GB2312-维基百科: -- UTF-8-维基百科: -- GB18030-维基百科: -- MySQL8 文档: -- MySQL5.7 文档: -- MySQL Connector/J 文档: +## References + +- Character set and character encoding (Charset & Encoding): +- Clarifying character sets and character encoding in ten minutes: +- Unicode - Wikipedia: +- GB2312 - Wikipedia: +- UTF-8 - Wikipedia: +- GB18030 - Wikipedia: +- MySQL 8 Documentation: +- MySQL 5.7 Documentation: +- MySQL Connector/J Documentation: diff --git a/docs/database/elasticsearch/elasticsearch-questions-01.md b/docs/database/elasticsearch/elasticsearch-questions-01.md index fe6daa6926c..75e820b3bd5 100644 --- a/docs/database/elasticsearch/elasticsearch-questions-01.md +++ b/docs/database/elasticsearch/elasticsearch-questions-01.md @@ -1,12 +1,12 @@ --- -title: Elasticsearch常见面试题总结(付费) -category: 数据库 +title: Summary of Common Elasticsearch Interview Questions (Paid) +category: Database tag: - NoSQL - Elasticsearch --- -**Elasticsearch** 相关的面试题为我的[知识星球](../../about-the-author/zhishixingqiu-two-years.md)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](../../zhuanlan/java-mian-shi-zhi-bei.md)中。 +The interview questions related to **Elasticsearch** are exclusive content for my [Knowledge Planet](../../about-the-author/zhishixingqiu-two-years.md) (click the link to view detailed introduction and joining methods), and have been organized in the [“Java Interview Guide”](../../zhuanlan/java-mian-shi-zhi-bei.md). ![](https://oss.javaguide.cn/javamianshizhibei/elasticsearch-questions.png) diff --git a/docs/database/mongodb/mongodb-questions-01.md b/docs/database/mongodb/mongodb-questions-01.md index 81b7db98890..31324248865 100644 --- a/docs/database/mongodb/mongodb-questions-01.md +++ b/docs/database/mongodb/mongodb-questions-01.md @@ -1,343 +1,73 @@ --- -title: MongoDB常见面试题总结(上) -category: 数据库 +title: Summary of Common MongoDB Interview Questions (Part 1) +category: Database tag: - NoSQL - MongoDB --- -> 少部分内容参考了 MongoDB 官方文档的描述,在此说明一下。 +> A small portion of the content references descriptions from the official MongoDB documentation, which is noted here. -## MongoDB 基础 +## MongoDB Basics -### MongoDB 是什么? +### What is MongoDB? -MongoDB 是一个基于 **分布式文件存储** 的开源 NoSQL 数据库系统,由 **C++** 编写的。MongoDB 提供了 **面向文档** 的存储方式,操作起来比较简单和容易,支持“**无模式**”的数据建模,可以存储比较复杂的数据类型,是一款非常流行的 **文档类型数据库** 。 +MongoDB is an open-source NoSQL database system based on **distributed file storage**, written in **C++**. MongoDB provides a **document-oriented** storage method, which is relatively simple and easy to operate, supports "**schema-less**" data modeling, and can store complex data types. It is a very popular **document-type database**. -在高负载的情况下,MongoDB 天然支持水平扩展和高可用,可以很方便地添加更多的节点/实例,以保证服务性能和可用性。在许多场景下,MongoDB 可以用于代替传统的关系型数据库或键/值存储方式,皆在为 Web 应用提供可扩展的高可用高性能数据存储解决方案。 +Under high load, MongoDB natively supports horizontal scaling and high availability, allowing for easy addition of more nodes/instances to ensure service performance and availability. In many scenarios, MongoDB can replace traditional relational databases or key/value storage methods, providing scalable, high-availability, high-performance data storage solutions for web applications. -### MongoDB 的存储结构是什么? +### What is the storage structure of MongoDB? -MongoDB 的存储结构区别于传统的关系型数据库,主要由如下三个单元组成: +The storage structure of MongoDB differs from traditional relational databases and mainly consists of the following three units: -- **文档(Document)**:MongoDB 中最基本的单元,由 BSON 键值对(key-value)组成,类似于关系型数据库中的行(Row)。 -- **集合(Collection)**:一个集合可以包含多个文档,类似于关系型数据库中的表(Table)。 -- **数据库(Database)**:一个数据库中可以包含多个集合,可以在 MongoDB 中创建多个数据库,类似于关系型数据库中的数据库(Database)。 +- **Document**: The most basic unit in MongoDB, composed of BSON key-value pairs, similar to rows in relational databases. +- **Collection**: A collection can contain multiple documents, similar to tables in relational databases. +- **Database**: A database can contain multiple collections, and multiple databases can be created in MongoDB, similar to databases in relational databases. -也就是说,MongoDB 将数据记录存储为文档 (更具体来说是[BSON 文档](https://www.mongodb.com/docs/manual/core/document/#std-label-bson-document-format)),这些文档在集合中聚集在一起,数据库中存储一个或多个文档集合。 +In other words, MongoDB stores data records as documents (more specifically, [BSON documents](https://www.mongodb.com/docs/manual/core/document/#std-label-bson-document-format)), which are grouped together in collections, with one or more document collections stored in a database. -**SQL 与 MongoDB 常见术语对比**: +**Comparison of Common Terms between SQL and MongoDB**: -| SQL | MongoDB | -| ------------------------ | ------------------------------- | -| 表(Table) | 集合(Collection) | -| 行(Row) | 文档(Document) | -| 列(Col) | 字段(Field) | -| 主键(Primary Key) | 对象 ID(Objectid) | -| 索引(Index) | 索引(Index) | -| 嵌套表(Embedded Table) | 嵌入式文档(Embedded Document) | -| 数组(Array) | 数组(Array) | +| SQL | MongoDB | +| -------------- | ----------------- | +| Table | Collection | +| Row | Document | +| Column | Field | +| Primary Key | Object ID | +| Index | Index | +| Embedded Table | Embedded Document | +| Array | Array | -#### 文档 +#### Document -MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。字段的值可能包括其他文档、数组和文档数组。 +Records in MongoDB are BSON documents, which are data structures composed of key-value pairs, similar to JSON objects, and are the basic data unit in MongoDB. The values of fields may include other documents, arrays, and arrays of documents. -![MongoDB 文档](https://oss.javaguide.cn/github/javaguide/database/mongodb/crud-annotated-document..png) +![MongoDB Document](https://oss.javaguide.cn/github/javaguide/database/mongodb/crud-annotated-document..png) -文档的键是字符串。除了少数例外情况,键可以使用任意 UTF-8 字符。 +The keys of documents are strings. With few exceptions, keys can use any UTF-8 character. -- 键不能含有 `\0`(空字符)。这个字符用来表示键的结尾。 -- `.` 和 `$` 有特别的意义,只有在特定环境下才能使用。 -- 以下划线`_`开头的键是保留的(不是严格要求的)。 +- Keys cannot contain `\0` (null character). This character is used to indicate the end of the key. +- `.` and `$` have special meanings and can only be used in specific contexts. +- Keys that start with an underscore `_` are reserved (not strictly enforced). -**BSON [bee·sahn]** 是 Binary [JSON](http://json.org/)的简称,是 JSON 文档的二进制表示,支持将文档和数组嵌入到其他文档和数组中,还包含允许表示不属于 JSON 规范的数据类型的扩展。有关 BSON 规范的内容,可以参考 [bsonspec.org](http://bsonspec.org/),另见[BSON 类型](https://www.mongodb.com/docs/manual/reference/bson-types/)。 +**BSON [bee·sahn]** is short for Binary [JSON](http://json.org/), which is the binary representation of JSON documents. It supports embedding documents and arrays within other documents and arrays and includes extensions that allow for the representation of data types not included in the JSON specification. For details on the BSON specification, refer to [bsonspec.org](http://bsonspec.org/), and see also [BSON Types](https://www.mongodb.com/docs/manual/reference/bson-types/). -根据维基百科对 BJSON 的介绍,BJSON 的遍历速度优于 JSON,这也是 MongoDB 选择 BSON 的主要原因,但 BJSON 需要更多的存储空间。 +According to Wikipedia's introduction to BJSON, BJSON has better traversal speed than JSON, which is a primary reason MongoDB chose BSON, although BJSON requires more storage space. -> 与 JSON 相比,BSON 着眼于提高存储和扫描效率。BSON 文档中的大型元素以长度字段为前缀以便于扫描。在某些情况下,由于长度前缀和显式数组索引的存在,BSON 使用的空间会多于 JSON。 +> Compared to JSON, BSON focuses on improving storage and scanning efficiency. Large elements in BSON documents are prefixed with length fields for easier scanning. In some cases, due to the presence of length prefixes and explicit array indexes, BSON may use more space than JSON. -![BSON 官网首页](https://oss.javaguide.cn/github/javaguide/database/mongodb/bsonspec.org.png) +![BSON Official Homepage](https://oss.javaguide.cn/github/javaguide/database/mongodb/bsonspec.org.png) -#### 集合 +#### Collection -MongoDB 集合存在于数据库中,**没有固定的结构**,也就是 **无模式** 的,这意味着可以往集合插入不同格式和类型的数据。不过,通常情况下,插入集合中的数据都会有一定的关联性。 +MongoDB collections exist within databases and have **no fixed structure**, meaning they are **schema-less**, allowing for the insertion of different formats and types of data into a collection. However, typically, the data inserted into a collection will have some degree of correlation. -![MongoDB 集合](https://oss.javaguide.cn/github/javaguide/database/mongodb/crud-annotated-collection.png) +![MongoDB Collection](https://oss.javaguide.cn/github/javaguide/database/mongodb/crud-annotated-collection.png) -集合不需要事先创建,当第一个文档插入或者第一个索引创建时,如果该集合不存在,则会创建一个新的集合。 +Collections do not need to be created in advance; a new collection will be created if it does not exist when the first document is inserted or the first index is created. -集合名可以是满足下列条件的任意 UTF-8 字符串: +Collection names can be any UTF-8 string that meets the following conditions: -- 集合名不能是空字符串`""`。 -- 集合名不能含有 `\0` (空字符),这个字符表示集合名的结尾。 -- 集合名不能以"system."开头,这是为系统集合保留的前缀。例如 `system.users` 这个集合保存着数据库的用户信息,`system.namespaces` 集合保存着所有数据库集合的信息。 -- 集合名必须以下划线或者字母符号开始,并且不能包含 `$`。 - -#### 数据库 - -数据库用于存储所有集合,而集合又用于存储所有文档。一个 MongoDB 中可以创建多个数据库,每一个数据库都有自己的集合和权限。 - -MongoDB 预留了几个特殊的数据库。 - -- **admin** : admin 数据库主要是保存 root 用户和角色。例如,system.users 表存储用户,system.roles 表存储角色。一般不建议用户直接操作这个数据库。将一个用户添加到这个数据库,且使它拥有 admin 库上的名为 dbAdminAnyDatabase 的角色权限,这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行,比如关闭服务器。 -- **local** : local 数据库是不会被复制到其他分片的,因此可以用来存储本地单台服务器的任意 collection。一般不建议用户直接使用 local 库存储任何数据,也不建议进行 CRUD 操作,因为数据无法被正常备份与恢复。 -- **config** : 当 MongoDB 使用分片设置时,config 数据库可用来保存分片的相关信息。 -- **test** : 默认创建的测试库,连接 [mongod](https://mongoing.com/docs/reference/program/mongod.html) 服务时,如果不指定连接的具体数据库,默认就会连接到 test 数据库。 - -数据库名可以是满足以下条件的任意 UTF-8 字符串: - -- 不能是空字符串`""`。 -- 不得含有`' '`(空格)、`.`、`$`、`/`、`\`和 `\0` (空字符)。 -- 应全部小写。 -- 最多 64 字节。 - -数据库名最终会变成文件系统里的文件,这也就是有如此多限制的原因。 - -### MongoDB 有什么特点? - -- **数据记录被存储为文档**:MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。 -- **模式自由**:集合的概念类似 MySQL 里的表,但它不需要定义任何模式,能够用更少的数据对象表现复杂的领域模型对象。 -- **支持多种查询方式**:MongoDB 查询 API 支持读写操作 (CRUD)以及数据聚合、文本搜索和地理空间查询。 -- **支持 ACID 事务**:NoSQL 数据库通常不支持事务,为了可扩展和高性能进行了权衡。不过,也有例外,MongoDB 就支持事务。与关系型数据库一样,MongoDB 事务同样具有 ACID 特性。MongoDB 单文档原生支持原子性,也具备事务的特性。MongoDB 4.0 加入了对多文档事务的支持,但只支持复制集部署模式下的事务,也就是说事务的作用域限制为一个副本集内。MongoDB 4.2 引入了分布式事务,增加了对分片集群上多文档事务的支持,并合并了对副本集上多文档事务的现有支持。 -- **高效的二进制存储**:存储在集合中的文档,是以键值对的形式存在的。键用于唯一标识一个文档,一般是 ObjectId 类型,值是以 BSON 形式存在的。BSON = Binary JSON, 是在 JSON 基础上加了一些类型及元数据描述的格式。 -- **自带数据压缩功能**:存储同样的数据所需的资源更少。 -- **支持 mapreduce**:通过分治的方式完成复杂的聚合任务。不过,从 MongoDB 5.0 开始,map-reduce 已经不被官方推荐使用了,替代方案是 [聚合管道](https://www.mongodb.com/docs/manual/core/aggregation-pipeline/)。聚合管道提供比 map-reduce 更好的性能和可用性。 -- **支持多种类型的索引**:MongoDB 支持多种类型的索引,包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等,每种类型的索引有不同的使用场合。 -- **支持 failover**:提供自动故障恢复的功能,主节点发生故障时,自动从从节点中选举出一个新的主节点,确保集群的正常使用,这对于客户端来说是无感知的。 -- **支持分片集群**:MongoDB 支持集群自动切分数据,让集群存储更多的数据,具备更强的性能。在数据插入和更新时,能够自动路由和存储。 -- **支持存储大文件**:MongoDB 的单文档存储空间要求不超过 16MB。对于超过 16MB 的大文件,MongoDB 提供了 GridFS 来进行存储,通过 GridFS,可以将大型数据进行分块处理,然后将这些切分后的小文档保存在数据库中。 - -### MongoDB 适合什么应用场景? - -**MongoDB 的优势在于其数据模型和存储引擎的灵活性、架构的可扩展性以及对强大的索引支持。** - -选用 MongoDB 应该充分考虑 MongoDB 的优势,结合实际项目的需求来决定: - -- 随着项目的发展,使用类 JSON 格式(BSON)保存数据是否满足项目需求?MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。 -- 是否需要大数据量的存储?是否需要快速水平扩展?MongoDB 支持分片集群,可以很方便地添加更多的节点(实例),让集群存储更多的数据,具备更强的性能。 -- 是否需要更多类型索引来满足更多应用场景?MongoDB 支持多种类型的索引,包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等,每种类型的索引有不同的使用场合。 -- …… - -## MongoDB 存储引擎 - -### MongoDB 支持哪些存储引擎? - -存储引擎(Storage Engine)是数据库的核心组件,负责管理数据在内存和磁盘中的存储方式。 - -与 MySQL 一样,MongoDB 采用的也是 **插件式的存储引擎架构** ,支持不同类型的存储引擎,不同的存储引擎解决不同场景的问题。在创建数据库或集合时,可以指定存储引擎。 - -> 插件式的存储引擎架构可以实现 Server 层和存储引擎层的解耦,可以支持多种存储引擎,如 MySQL 既可以支持 B-Tree 结构的 InnoDB 存储引擎,还可以支持 LSM 结构的 RocksDB 存储引擎。 - -在存储引擎刚出来的时候,默认是使用 MMAPV1 存储引擎,MongoDB4.x 版本不再支持 MMAPv1 存储引擎。 - -现在主要有下面这两种存储引擎: - -- **WiredTiger 存储引擎**:自 MongoDB 3.2 以后,默认的存储引擎为 [WiredTiger 存储引擎](https://www.mongodb.com/docs/manual/core/wiredtiger/) 。非常适合大多数工作负载,建议用于新部署。WiredTiger 提供文档级并发模型、检查点和数据压缩(后文会介绍到)等功能。 -- **In-Memory 存储引擎**:[In-Memory 存储引擎](https://www.mongodb.com/docs/manual/core/inmemory/)在 MongoDB Enterprise 中可用。它不是将文档存储在磁盘上,而是将它们保留在内存中以获得更可预测的数据延迟。 - -此外,MongoDB 3.0 提供了 **可插拔的存储引擎 API** ,允许第三方为 MongoDB 开发存储引擎,这点和 MySQL 也比较类似。 - -### WiredTiger 基于 LSM Tree 还是 B+ Tree? - -目前绝大部分流行的数据库存储引擎都是基于 B/B+ Tree 或者 LSM(Log Structured Merge) Tree 来实现的。对于 NoSQL 数据库来说,绝大部分(比如 HBase、Cassandra、RocksDB)都是基于 LSM 树,MongoDB 不太一样。 - -上面也说了,自 MongoDB 3.2 以后,默认的存储引擎为 WiredTiger 存储引擎。在 WiredTiger 引擎官网上,我们发现 WiredTiger 使用的是 B+ 树作为其存储结构: - -```plain -WiredTiger maintains a table's data in memory using a data structure called a B-Tree ( B+ Tree to be specific), referring to the nodes of a B-Tree as pages. Internal pages carry only keys. The leaf pages store both keys and values. -``` - -此外,WiredTiger 还支持 [LSM(Log Structured Merge)](https://source.wiredtiger.com/3.1.0/lsm.html) 树作为存储结构,MongoDB 在使用 WiredTiger 作为存储引擎时,默认使用的是 B+ 树。 - -如果想要了解 MongoDB 使用 B+ 树的原因,可以看看这篇文章:[【驳斥八股文系列】别瞎分析了,MongoDB 使用的是 B+ 树,不是你们以为的 B 树](https://zhuanlan.zhihu.com/p/519658576)。 - -使用 B+ 树时,WiredTiger 以 **page** 为基本单位往磁盘读写数据。B+ 树的每个节点为一个 page,共有三种类型的 page: - -- **root page(根节点)**:B+ 树的根节点。 -- **internal page(内部节点)**:不实际存储数据的中间索引节点。 -- **leaf page(叶子节点)**:真正存储数据的叶子节点,包含一个页头(page header)、块头(block header)和真正的数据(key/value),其中页头定义了页的类型、页中实际载荷数据的大小、页中记录条数等信息;块头定义了此页的 checksum、块在磁盘上的寻址位置等信息。 - -其整体结构如下图所示: - -![WiredTiger B+树整体结构](https://oss.javaguide.cn/github/javaguide/database/mongodb/mongodb-b-plus-tree-integral-structure.png) - -如果想要深入研究学习 WiredTiger 存储引擎,推荐阅读 MongoDB 中文社区的 [WiredTiger 存储引擎系列](https://mongoing.com/archives/category/wiredtiger%e5%ad%98%e5%82%a8%e5%bc%95%e6%93%8e%e7%b3%bb%e5%88%97)。 - -## MongoDB 聚合 - -### MongoDB 聚合有什么用? - -实际项目中,我们经常需要将多个文档甚至是多个集合汇总到一起计算分析(比如求和、取最大值)并返回计算后的结果,这个过程被称为 **聚合操作** 。 - -根据官方文档介绍,我们可以使用聚合操作来: - -- 将来自多个文档的值组合在一起。 -- 对集合中的数据进行的一系列运算。 -- 分析数据随时间的变化。 - -### MongoDB 提供了哪几种执行聚合的方法? - -MongoDB 提供了两种执行聚合的方法: - -- **聚合管道(Aggregation Pipeline)**:执行聚合操作的首选方法。 -- **单一目的聚合方法(Single purpose aggregation methods)**:也就是单一作用的聚合函数比如 `count()`、`distinct()`、`estimatedDocumentCount()`。 - -绝大部分文章中还提到了 **map-reduce** 这种聚合方法。不过,从 MongoDB 5.0 开始,map-reduce 已经不被官方推荐使用了,替代方案是 [聚合管道](https://www.mongodb.com/docs/manual/core/aggregation-pipeline/)。聚合管道提供比 map-reduce 更好的性能和可用性。 - -MongoDB 聚合管道由多个阶段组成,每个阶段在文档通过管道时转换文档。每个阶段接收前一个阶段的输出,进一步处理数据,并将其作为输入数据发送到下一个阶段。 - -每个管道的工作流程是: - -1. 接受一系列原始数据文档 -2. 对这些文档进行一系列运算 -3. 结果文档输出给下一个阶段 - -![管道的工作流程](https://oss.javaguide.cn/github/javaguide/database/mongodb/mongodb-aggregation-stage.png) - -**常用阶段操作符**: - -| 操作符 | 简述 | -| --------- | ---------------------------------------------------------------------------------------------------- | -| \$match | 匹配操作符,用于对文档集合进行筛选 | -| \$project | 投射操作符,用于重构每一个文档的字段,可以提取字段,重命名字段,甚至可以对原有字段进行操作后新增字段 | -| \$sort | 排序操作符,用于根据一个或多个字段对文档进行排序 | -| \$limit | 限制操作符,用于限制返回文档的数量 | -| \$skip | 跳过操作符,用于跳过指定数量的文档 | -| \$count | 统计操作符,用于统计文档的数量 | -| \$group | 分组操作符,用于对文档集合进行分组 | -| \$unwind | 拆分操作符,用于将数组中的每一个值拆分为单独的文档 | -| \$lookup | 连接操作符,用于连接同一个数据库中另一个集合,并获取指定的文档,类似于 populate | - -更多操作符介绍详见官方文档: - -阶段操作符用于 `db.collection.aggregate` 方法里面,数组参数中的第一层。 - -```sql -db.collection.aggregate( [ { 阶段操作符:表述 }, { 阶段操作符:表述 }, ... ] ) -``` - -下面是 MongoDB 官方文档中的一个例子: - -```sql -db.orders.aggregate([ - # 第一阶段:$match阶段按status字段过滤文档,并将status等于"A"的文档传递到下一阶段。 - { $match: { status: "A" } }, - # 第二阶段:$group阶段按cust_id字段将文档分组,以计算每个cust_id唯一值的金额总和。 - { $group: { _id: "$cust_id", total: { $sum: "$amount" } } } -]) -``` - -## MongoDB 事务 - -> MongoDB 事务想要搞懂原理还是比较花费时间的,我自己也没有搞太明白。因此,我这里只是简单介绍一下 MongoDB 事务,想要了解原理的小伙伴,可以自行搜索查阅相关资料。 -> -> 这里推荐几篇文章,供大家参考: -> -> - [技术干货| MongoDB 事务原理](https://mongoing.com/archives/82187) -> - [MongoDB 一致性模型设计与实现](https://developer.aliyun.com/article/782494) -> - [MongoDB 官方文档对事务的介绍](https://www.mongodb.com/docs/upcoming/core/transactions/) - -我们在介绍 NoSQL 数据的时候也说过,NoSQL 数据库通常不支持事务,为了可扩展和高性能进行了权衡。不过,也有例外,MongoDB 就支持事务。 - -与关系型数据库一样,MongoDB 事务同样具有 ACID 特性: - -- **原子性**(`Atomicity`):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; -- **一致性**(`Consistency`):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的; -- **隔离性**(`Isolation`):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。WiredTiger 存储引擎支持读未提交( read-uncommitted )、读已提交( read-committed )和快照( snapshot )隔离,MongoDB 启动时默认选快照隔离。在不同隔离级别下,一个事务的生命周期内,可能出现脏读、不可重复读、幻读等现象。 -- **持久性**(`Durability`):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 - -关于事务的详细介绍这篇文章就不多说了,感兴趣的可以看看我写的[MySQL 常见面试题总结](../mysql/mysql-questions-01.md)这篇文章,里面有详细介绍到。 - -MongoDB 单文档原生支持原子性,也具备事务的特性。当谈论 MongoDB 事务的时候,通常指的是 **多文档** 。MongoDB 4.0 加入了对多文档 ACID 事务的支持,但只支持复制集部署模式下的 ACID 事务,也就是说事务的作用域限制为一个副本集内。MongoDB 4.2 引入了 **分布式事务** ,增加了对分片集群上多文档事务的支持,并合并了对副本集上多文档事务的现有支持。 - -根据官方文档介绍: - -> 从 MongoDB 4.2 开始,分布式事务和多文档事务在 MongoDB 中是一个意思。分布式事务是指分片集群和副本集上的多文档事务。从 MongoDB 4.2 开始,多文档事务(无论是在分片集群还是副本集上)也称为分布式事务。 - -在大多数情况下,多文档事务比单文档写入会产生更大的性能成本。对于大部分场景来说, [非规范化数据模型(嵌入式文档和数组)](https://www.mongodb.com/docs/upcoming/core/data-model-design/#std-label-data-modeling-embedding) 依然是最佳选择。也就是说,适当地对数据进行建模可以最大限度地减少对多文档事务的需求。 - -**注意**: - -- 从 MongoDB 4.2 开始,多文档事务支持副本集和分片集群,其中:主节点使用 WiredTiger 存储引擎,同时从节点使用 WiredTiger 存储引擎或 In-Memory 存储引擎。在 MongoDB 4.0 中,只有使用 WiredTiger 存储引擎的副本集支持事务。 -- 在 MongoDB 4.2 及更早版本中,你无法在事务中创建集合。从 MongoDB 4.4 开始,您可以在事务中创建集合和索引。有关详细信息,请参阅 [在事务中创建集合和索引](https://www.mongodb.com/docs/upcoming/core/transactions/#std-label-transactions-create-collections-indexes)。 - -## MongoDB 数据压缩 - -借助 WiredTiger 存储引擎( MongoDB 3.2 后的默认存储引擎),MongoDB 支持对所有集合和索引进行压缩。压缩以额外的 CPU 为代价最大限度地减少存储使用。 - -默认情况下,WiredTiger 使用 [Snappy](https://github.com/google/snappy) 压缩算法(谷歌开源,旨在实现非常高的速度和合理的压缩,压缩比 3 ~ 5 倍)对所有集合使用块压缩,对所有索引使用前缀压缩。 - -除了 Snappy 之外,对于集合还有下面这些压缩算法: - -- [zlib](https://github.com/madler/zlib):高度压缩算法,压缩比 5 ~ 7 倍 -- [Zstandard](https://github.com/facebook/zstd)(简称 zstd):Facebook 开源的一种快速无损压缩算法,针对 zlib 级别的实时压缩场景和更好的压缩比,提供更高的压缩率和更低的 CPU 使用率,MongoDB 4.2 开始可用。 - -WiredTiger 日志也会被压缩,默认使用的也是 Snappy 压缩算法。如果日志记录小于或等于 128 字节,WiredTiger 不会压缩该记录。 - -## Amazon Document 与 MongoDB 的差异 - -Amazon DocumentDB(与 MongoDB 兼容) 是一种快速、可靠、完全托管的数据库服务。Amazon DocumentDB 可在云中轻松设置、操作和扩展与 MongoDB 兼容的数据库。 - -### `$vectorSearch` 运算符 - -Amazon DocumentDB 不支持`$vectorSearch`作为独立运营商。相反,我们在`$search`运营商`vectorSearch`内部支持。有关更多信息,请参阅 [向量搜索 Amazon DocumentDB](https://docs.aws.amazon.com/zh_cn/documentdb/latest/developerguide/vector-search.html)。 - -### `OpCountersCommand` - -Amazon DocumentDB 的`OpCountersCommand`行为偏离于 MongoDB 的`opcounters.command` 如下: - -- MongoDB 的`opcounters.command` 计入除插入、更新和删除之外的所有命令,而 Amazon DocumentDB 的 `OpCountersCommand` 也排除 `find` 命令。 -- Amazon DocumentDB 将内部命令(例如`getCloudWatchMetricsV2`)对 `OpCountersCommand` 计入。 - -### 管理数据库和集合 - -Amazon DocumentDB 不支持管理或本地数据库,MongoDB `system.*` 或 `startup_log` 集合也不支持。 - -### `cursormaxTimeMS` - -在 Amazon DocumentDB 中,`cursor.maxTimeMS` 重置每个请求的计数器。`getMore`因此,如果指定了 3000MS `maxTimeMS`,则该查询耗时 2800MS,而每个后续`getMore`请求耗时 300MS,则游标不会超时。游标仅在单个操作(无论是查询还是单个`getMore`请求)耗时超过指定值时才将超时`maxTimeMS`。此外,检查游标执行时间的扫描器以五 (5) 分钟间隔尺寸运行。 - -### explain() - -Amazon DocumentDB 在利用分布式、容错、自修复的存储系统的专用数据库引擎上模拟 MongoDB 4.0 API。因此,查询计划和`explain()` 的输出在 Amazon DocumentDB 和 MongoDB 之间可能有所不同。希望控制其查询计划的客户可以使用 `$hint` 运算符强制选择首选索引。 - -### 字段名称限制 - -Amazon DocumentDB 不支持点“。” 例如,文档字段名称中 `db.foo.insert({‘x.1’:1})`。 - -Amazon DocumentDB 也不支持字段名称中的 $ 前缀。 - -例如,在 Amazon DocumentDB 或 MongoDB 中尝试以下命令: - -```shell -rs0:PRIMARY< db.foo.insert({"a":{"$a":1}}) -``` - -MongoDB 将返回以下内容: - -```shell -WriteResult({ "nInserted" : 1 }) -``` - -Amazon DocumentDB 将返回一个错误: - -```shell -WriteResult({ - "nInserted" : 0, - "writeError" : { - "code" : 2, - "errmsg" : "Document can't have $ prefix field names: $a" - } -}) -``` - -## 参考 - -- MongoDB 官方文档(主要参考资料,以官方文档为准): -- 《MongoDB 权威指南》 -- 技术干货| MongoDB 事务原理 - MongoDB 中文社区: -- Transactions - MongoDB 官方文档: -- WiredTiger Storage Engine - MongoDB 官方文档: -- WiredTiger 存储引擎之一:基础数据结构分析: - - +- Collection names cannot be empty strings `""`. +- Collection names cannot contain `\0` (null character), which indicates the end of the collection name. +- Collection names cannot start with "system.", as this prefix is reserved for system collections. For example, the `system.users` collection stores user information for the database, and the \`system.names diff --git a/docs/database/mongodb/mongodb-questions-02.md b/docs/database/mongodb/mongodb-questions-02.md index dcd90d72c4d..0e240d8b7e0 100644 --- a/docs/database/mongodb/mongodb-questions-02.md +++ b/docs/database/mongodb/mongodb-questions-02.md @@ -1,49 +1,49 @@ --- -title: MongoDB常见面试题总结(下) -category: 数据库 +title: Summary of Common MongoDB Interview Questions (Part 2) +category: Database tag: - NoSQL - MongoDB --- -## MongoDB 索引 +## MongoDB Indexes -### MongoDB 索引有什么用? +### What are the uses of MongoDB indexes? -和关系型数据库类似,MongoDB 中也有索引。索引的目的主要是用来提高查询效率,如果没有索引的话,MongoDB 必须执行 **集合扫描** ,即扫描集合中的每个文档,以选择与查询语句匹配的文档。如果查询存在合适的索引,MongoDB 可以使用该索引来限制它必须检查的文档数量。并且,MongoDB 可以使用索引中的排序返回排序后的结果。 +Similar to relational databases, MongoDB also has indexes. The main purpose of indexes is to improve query efficiency. Without indexes, MongoDB must perform **collection scans**, which means scanning every document in the collection to select documents that match the query statement. If a suitable index exists for the query, MongoDB can use that index to limit the number of documents it must check. Additionally, MongoDB can return sorted results based on the index order. -虽然索引可以显著缩短查询时间,但是使用索引、维护索引是有代价的。在执行写入操作时,除了要更新文档之外,还必须更新索引,这必然会影响写入的性能。因此,当有大量写操作而读操作少时,或者不考虑读操作的性能时,都不推荐建立索引。 +While indexes can significantly reduce query time, there is a cost to using and maintaining indexes. When performing write operations, in addition to updating documents, the indexes must also be updated, which inevitably affects write performance. Therefore, it is not recommended to create indexes when there are many write operations and few read operations, or when read operation performance is not a concern. -### MongoDB 支持哪些类型的索引? +### What types of indexes does MongoDB support? -**MongoDB 支持多种类型的索引,包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等,每种类型的索引有不同的使用场合。** +**MongoDB supports various types of indexes, including single-field indexes, compound indexes, multi-key indexes, hash indexes, text indexes, and geospatial indexes, each with different use cases.** -- **单字段索引:** 建立在单个字段上的索引,索引创建的排序顺序无所谓,MongoDB 可以头/尾开始遍历。 -- **复合索引:** 建立在多个字段上的索引,也可以称之为组合索引、联合索引。 -- **多键索引**:MongoDB 的一个字段可能是数组,在对这种字段创建索引时,就是多键索引。MongoDB 会为数组的每个值创建索引。就是说你可以按照数组里面的值做条件来查询,这个时候依然会走索引。 -- **哈希索引**:按数据的哈希值索引,用在哈希分片集群上。 -- **文本索引:** 支持对字符串内容的文本搜索查询。文本索引可以包含任何值为字符串或字符串元素数组的字段。一个集合只能有一个文本搜索索引,但该索引可以覆盖多个字段。MongoDB 虽然支持全文索引,但是性能低下,暂时不建议使用。 -- **地理位置索引:** 基于经纬度的索引,适合 2D 和 3D 的位置查询。 -- **唯一索引**:确保索引字段不会存储重复值。如果集合已经存在了违反索引的唯一约束的文档,则后台创建唯一索引会失败。 -- **TTL 索引**:TTL 索引提供了一个过期机制,允许为每一个文档设置一个过期时间,当一个文档达到预设的过期时间之后就会被删除。 +- **Single-field index:** An index built on a single field. The sort order of the index creation does not matter; MongoDB can traverse from the beginning or the end. +- **Compound index:** An index built on multiple fields, which can also be called a composite index or a combined index. +- **Multi-key index:** When a field in MongoDB is an array, creating an index on that field is known as a multi-key index. MongoDB will create indexes for each value in the array. This means you can query based on the values inside the array while still using the index. +- **Hash index:** An index based on the hash values of the data, used in hash-sharded clusters. +- **Text index:** Supports full-text search queries on string content. A text index can include any field whose values are strings or arrays of strings. A collection can only have one text search index, but that index can cover multiple fields. Although MongoDB supports full-text indexing, its performance is poor, and it is not recommended for now. +- **Geospatial index:** An index based on latitude and longitude, suitable for 2D and 3D location queries. +- **Unique index:** Ensures that the indexed field does not store duplicate values. If a document violating the unique constraint already exists in the collection, creating a unique index in the background will fail. +- **TTL index:** TTL indexes provide an expiration mechanism, allowing an expiration time to be set for each document. When a document reaches its preset expiration time, it will be deleted. - …… -### 复合索引中字段的顺序有影响吗? +### Does the order of fields in a compound index matter? -复合索引中字段的顺序非常重要,例如下图中的复合索引由`{userid:1, score:-1}`组成,则该复合索引首先按照`userid`升序排序;然后再每个`userid`的值内,再按照`score`降序排序。 +The order of fields in a compound index is very important. For example, if a compound index consists of `{userid:1, score:-1}`, the compound index first sorts by `userid` in ascending order; then within each value of `userid`, it sorts by `score` in descending order. -![复合索引](https://oss.javaguide.cn/github/javaguide/database/mongodb/mongodb-composite-index.png) +![Compound Index](https://oss.javaguide.cn/github/javaguide/database/mongodb/mongodb-composite-index.png) -在复合索引中,按照何种方式排序,决定了该索引在查询中是否能被应用到。 +The sorting method in a compound index determines whether that index can be applied in a query. -走复合索引的排序: +Sorting that uses the compound index: ```sql db.s2.find().sort({"userid": 1, "score": -1}) db.s2.find().sort({"userid": -1, "score": 1}) ``` -不走复合索引的排序: +Sorting that does not use the compound index: ```sql db.s2.find().sort({"userid": 1, "score": 1}) @@ -54,45 +54,45 @@ db.s2.find().sort({"score": -1, "userid": -1}) db.s2.find().sort({"score": -1, "userid": 1}) ``` -我们可以通过 explain 进行分析: +We can analyze this through explain: ```sql db.s2.find().sort({"score": -1, "userid": 1}).explain() ``` -### 复合索引遵循左前缀原则吗? +### Does a compound index follow the left-prefix principle? -**MongoDB 的复合索引遵循左前缀原则**:拥有多个键的索引,可以同时得到所有这些键的前缀组成的索引,但不包括除左前缀之外的其他子集。比如说,有一个类似 `{a: 1, b: 1, c: 1, ..., z: 1}` 这样的索引,那么实际上也等于有了 `{a: 1}`、`{a: 1, b: 1}`、`{a: 1, b: 1, c: 1}` 等一系列索引,但是不会有 `{b: 1}` 这样的非左前缀的索引。 +**MongoDB's compound index follows the left-prefix principle**: An index with multiple keys can simultaneously obtain indexes composed of all prefixes of those keys, but does not include subsets other than the left prefix. For example, an index like `{a: 1, b: 1, c: 1, ..., z: 1}` effectively implies indexes like `{a: 1}`, `{a: 1, b: 1}`, `{a: 1, b: 1, c: 1}`, etc., but there will be no index like `{b: 1}` that is not a left prefix index. -### 什么是 TTL 索引? +### What is a TTL index? -TTL 索引提供了一个过期机制,允许为每一个文档设置一个过期时间 `expireAfterSeconds` ,当一个文档达到预设的过期时间之后就会被删除。TTL 索引除了有 `expireAfterSeconds` 属性外,和普通索引一样。 +A TTL index provides an expiration mechanism, allowing an expiration time `expireAfterSeconds` to be set for each document. When a document reaches its preset expiration time, it will be deleted. Aside from having the `expireAfterSeconds` attribute, a TTL index is just like a normal index. -数据过期对于某些类型的信息很有用,比如机器生成的事件数据、日志和会话信息,这些信息只需要在数据库中保存有限的时间。 +Data expiration is useful for certain types of information, such as machine-generated event data, logs, and session information, which only need to be retained in the database for a limited time. -**TTL 索引运行原理**: +**How TTL Index Works:** -- MongoDB 会开启一个后台线程读取该 TTL 索引的值来判断文档是否过期,但不会保证已过期的数据会立马被删除,因后台线程每 60 秒触发一次删除任务,且如果删除的数据量较大,会存在上一次的删除未完成,而下一次的任务已经开启的情况,导致过期的数据也会出现超过了数据保留时间 60 秒以上的现象。 -- 对于副本集而言,TTL 索引的后台进程只会在 Primary 节点开启,在从节点会始终处于空闲状态,从节点的数据删除是由主库删除后产生的 oplog 来做同步。 +- MongoDB will start a background thread to read the TTL index values to determine if documents have expired, but it will not guarantee that expired data is deleted immediately, as the background thread triggers a deletion task every 60 seconds. If a large amount of data needs to be deleted, it is possible for the previous deletion to not complete before the next task starts, resulting in expired data being retained beyond the 60-second limit. +- For replica sets, the TTL index background process runs only on the Primary node, while it remains idle on Secondary nodes. The deletion of data on Secondary nodes is synchronized from the Primary through the oplog after it has been deleted. -**TTL 索引限制**: +**TTL Index Limitations:** -- TTL 索引是单字段索引。复合索引不支持 TTL -- `_id`字段不支持 TTL 索引。 -- 无法在上限集合(Capped Collection)上创建 TTL 索引,因为 MongoDB 无法从上限集合中删除文档。 -- 如果某个字段已经存在非 TTL 索引,那么在该字段上无法再创建 TTL 索引。 +- TTL indexes are single-field indexes. Compound indexes do not support TTL. +- The `_id` field does not support TTL indexes. +- It is not possible to create a TTL index on capped collections, as MongoDB cannot delete documents from capped collections. +- If a non-TTL index already exists on a field, a TTL index cannot be created on that field. -### 什么是覆盖索引查询? +### What is a covered index query? -根据官方文档介绍,覆盖查询是以下的查询: +According to the official documentation, a covered query is defined as follows: -- 所有的查询字段是索引的一部分。 -- 结果中返回的所有字段都在同一索引中。 -- 查询中没有字段等于`null`。 +- All query fields are part of the index. +- All fields returned in the result are contained within the same index. +- The query does not contain any fields equal to `null`. -由于所有出现在查询中的字段是索引的一部分, MongoDB 无需在整个数据文档中检索匹配查询条件和返回使用相同索引的查询结果。因为索引存在于内存中,从索引中获取数据比通过扫描文档读取数据要快得多。 +Since all fields that appear in the query are part of the index, MongoDB does not need to retrieve matching query conditions from the entire data document. Accessing data from the index, which resides in memory, is much faster than reading data via document scanning. -举个例子:我们有如下 `users` 集合: +For example, we have the following `users` collection: ```json { @@ -105,171 +105,171 @@ TTL 索引提供了一个过期机制,允许为每一个文档设置一个过 } ``` -我们在 `users` 集合中创建联合索引,字段为 `gender` 和 `user_name` : +We create a compound index in the `users` collection on the fields `gender` and `user_name`: ```sql db.users.ensureIndex({gender:1,user_name:1}) ``` -现在,该索引会覆盖以下查询: +Now, this index will cover the following query: ```sql db.users.find({gender:"M"},{user_name:1,_id:0}) ``` -为了让指定的索引覆盖查询,必须显式地指定 `_id: 0` 来从结果中排除 `_id` 字段,因为索引不包括 `_id` 字段。 +To ensure that the specified index covers the query, you must explicitly specify `_id: 0` to exclude the `_id` field from the result, as the index does not include the `_id` field. -## MongoDB 高可用 +## MongoDB High Availability -### 复制集群 +### Replica Set -#### 什么是复制集群? +#### What is a replica set? -MongoDB 的复制集群又称为副本集群,是一组维护相同数据集合的 mongod 进程。 +MongoDB's replica set is a group of mongod processes that maintain the same dataset. -客户端连接到整个 Mongodb 复制集群,主节点机负责整个复制集群的写,从节点可以进行读操作,但默认还是主节点负责整个复制集群的读。主节点发生故障时,自动从从节点中选举出一个新的主节点,确保集群的正常使用,这对于客户端来说是无感知的。 +Clients connect to the entire MongoDB replica set, where the primary node is responsible for all writes to the replica set, while secondary nodes can perform read operations. However, by default, the primary node is responsible for all reads within the replica set. If the primary node fails, a new primary node is automatically elected from the secondary nodes to ensure the cluster's ongoing availability, which is transparent to the client. -通常来说,一个复制集群包含 1 个主节点(Primary),多个从节点(Secondary)以及零个或 1 个仲裁节点(Arbiter)。 +Typically, a replica set includes one primary node (Primary), multiple secondary nodes (Secondary), and zero or one arbiter node (Arbiter). -- **主节点**:整个集群的写操作入口,接收所有的写操作,并将集合所有的变化记录到操作日志中,即 oplog。主节点挂掉之后会自动选出新的主节点。 -- **从节点**:从主节点同步数据,在主节点挂掉之后选举新节点。不过,从节点可以配置成 0 优先级,阻止它在选举中成为主节点。 -- **仲裁节点**:这个是为了节约资源或者多机房容灾用,只负责主节点选举时投票不存数据,保证能有节点获得多数赞成票。 +- **Primary Node:** The entry point for write operations in the cluster, receiving all write operations and logging all changes to the operation log, known as oplog. A new primary node is automatically elected if the current one fails. +- **Secondary Node:** Synchronizes data from the primary node and elects a new node if the primary node fails. However, a secondary node can be configured with a priority of 0 to prevent it from being elected as the primary. +- **Arbiter Node:** Used to save resources or for disaster recovery across multiple data centers; it only votes during primary node elections and does not store data, ensuring that a node can receive a majority of votes. -下图是一个典型的三成员副本集群: +The diagram below shows a typical three-member replica set: ![](https://oss.javaguide.cn/github/javaguide/database/mongodb/replica-set-read-write-operations-primary.png) -主节点与备节点之间是通过 **oplog(操作日志)** 来同步数据的。oplog 是 local 库下的一个特殊的 **上限集合(Capped Collection)** ,用来保存写操作所产生的增量日志,类似于 MySQL 中 的 Binlog。 +Data synchronization between the primary and secondary nodes occurs through the **oplog (operation log)**. The oplog is a special **capped collection** in the local database that stores incremental logs produced by write operations, similar to the Binlog in MySQL. -> 上限集合类似于定长的循环队列,数据顺序追加到集合的尾部,当集合空间达到上限时,它会覆盖集合中最旧的文档。上限集合的数据将会被顺序写入到磁盘的固定空间内,所以,I/O 速度非常快,如果不建立索引,性能更好。 +> A capped collection is similar to a fixed-length circular queue where data is appended to the end of the collection. When the collection reaches its maximum capacity, it overwrites the oldest documents. Data in a capped collection is sequentially written to disk, providing very fast I/O speeds, and performance is even better when no indexes are established. ![](https://oss.javaguide.cn/github/javaguide/database/mongodb/replica-set-primary-with-two-secondaries.png) -当主节点上的一个写操作完成后,会向 oplog 集合写入一条对应的日志,而从节点则通过这个 oplog 不断拉取到新的日志,在本地进行回放以达到数据同步的目的。 +After a write operation is completed on the primary node, a corresponding log entry is written to the oplog collection, and the secondary nodes continuously pull new logs from this oplog, replaying them locally to achieve data synchronization. -副本集最多有一个主节点。 如果当前主节点不可用,一个选举会抉择出新的主节点。MongoDB 的节点选举规则能够保证在 Primary 挂掉之后选取的新节点一定是集群中数据最全的一个。 +There can be at most one primary node in a replica set. If the current primary node becomes unavailable, an election will determine a new primary node. The rules for node election in MongoDB guarantee that the newly chosen node after a primary failure is the one with the most complete data in the cluster. -#### 为什么要用复制集群? +#### Why use a replica set? -- **实现 failover**:提供自动故障恢复的功能,主节点发生故障时,自动从从节点中选举出一个新的主节点,确保集群的正常使用,这对于客户端来说是无感知的。 -- **实现读写分离**:我们可以设置从节点上可以读取数据,主节点负责写入数据,这样的话就实现了读写分离,减轻了主节点读写压力过大的问题。MongoDB 4.0 之前版本如果主库压力不大,不建议读写分离,因为写会阻塞读,除非业务对响应时间不是非常关注以及读取历史数据接受一定时间延迟。 +- **To implement failover:** It provides automatic fault recovery functionality. When the primary node fails, a new primary is automatically elected from the secondary nodes to ensure the normal operation of the cluster, which is transparent to the client. +- **To achieve read-write separation:** We can configure secondary nodes to read data while the primary node is responsible for write operations, thus achieving read-write separation and alleviating the excessive load on the primary node. Prior to MongoDB 4.0, it was not recommended to separate reads and writes if the primary node was not under heavy load, as writes would block reads unless the business did not emphasize response time and was acceptable with a certain delay in reading historical data. -### 分片集群 +### Sharded Clusters -#### 什么是分片集群? +#### What is a sharded cluster? -分片集群是 MongoDB 的分布式版本,相较副本集,分片集群数据被均衡的分布在不同分片中, 不仅大幅提升了整个集群的数据容量上限,也将读写的压力分散到不同分片,以解决副本集性能瓶颈的难题。 +A sharded cluster is the distributed version of MongoDB. Unlike replica sets, the data in a sharded cluster is balanced across different shards, significantly increasing the overall data capacity of the cluster and distributing the read and write load across different shards to address the performance bottlenecks of replica sets. -MongoDB 的分片集群由如下三个部分组成(下图来源于[官方文档对分片集群的介绍](https://www.mongodb.com/docs/manual/sharding/)): +A MongoDB sharded cluster consists of the following three components (the diagram below is sourced from the [official documentation on sharded clusters](https://www.mongodb.com/docs/manual/sharding/)): ![](https://oss.javaguide.cn/github/javaguide/database/mongodb/sharded-cluster-production-architecture.png) -- **Config Servers**:配置服务器,本质上是一个 MongoDB 的副本集,负责存储集群的各种元数据和配置,如分片地址、Chunks 等 -- **Mongos**:路由服务,不存具体数据,从 Config 获取集群配置讲请求转发到特定的分片,并且整合分片结果返回给客户端。 -- **Shard**:每个分片是整体数据的一部分子集,从 MongoDB3.6 版本开始,每个 Shard 必须部署为副本集(replica set)架构 +- **Config Servers:** Config servers are essentially a MongoDB replica set that is responsible for storing various metadata and configurations for the cluster, such as shard addresses, Chunks, etc. +- **Mongos:** Router service that does not store actual data. It retrieves the cluster configuration from Config and forwards requests to specific shards, aggregating results and returning them to the client. +- **Shard:** Each shard is a subset of the overall data. Starting from MongoDB 3.6, each shard must be deployed as a replica set architecture. -#### 为什么要用分片集群? +#### Why use a sharded cluster? -随着系统数据量以及吞吐量的增长,常见的解决办法有两种:垂直扩展和水平扩展。 +As the amount of data and throughput in a system grows, common solutions include vertical scaling and horizontal scaling. -垂直扩展通过增加单个服务器的能力来实现,比如磁盘空间、内存容量、CPU 数量等;水平扩展则通过将数据存储到多个服务器上来实现,根据需要添加额外的服务器以增加容量。 +Vertical scaling achieves this by increasing the capability of a single server, such as disk space, memory capacity, CPU count, etc. Horizontal scaling achieves this by distributing data across multiple servers, adding additional servers as needed to increase capacity. -类似于 Redis Cluster,MongoDB 也可以通过分片实现 **水平扩展** 。水平扩展这种方式更灵活,可以满足更大数据量的存储需求,支持更高吞吐量。并且,水平扩展所需的整体成本更低,仅仅需要相对较低配置的单机服务器即可,代价是增加了部署的基础设施和维护的复杂性。 +Similar to Redis Cluster, MongoDB can also implement **horizontal scaling** through sharding. This scaling method is more flexible, can meet the storage needs of larger datasets, and supports higher throughput. Furthermore, the overall cost required for horizontal scaling is lower since it only needs relatively lower-configured single servers, albeit at the cost of increased complexity in infrastructure deployment and maintenance. -也就是说当你遇到如下问题时,可以使用分片集群解决: +This means that when you encounter the following issues, a sharded cluster can be used for resolution: -- 存储容量受单机限制,即磁盘资源遭遇瓶颈。 -- 读写能力受单机限制,可能是 CPU、内存或者网卡等资源遭遇瓶颈,导致读写能力无法扩展。 +- Storage capacity is limited by a single machine, meaning disk resources encounter bottlenecks. +- Read and write capabilities are constrained by a single machine, possibly due to CPU, memory, or network card resources encountering bottlenecks, preventing the ability to scale reads and writes. -#### 什么是分片键? +#### What is a shard key? -**分片键(Shard Key)** 是数据分区的前提, 从而实现数据分发到不同服务器上,减轻服务器的负担。也就是说,分片键决定了集合内的文档如何在集群的多个分片间的分布状况。 +**Shard Key** is the premise for data partitioning, enabling data distribution across different servers to lighten the server's load. In other words, the shard key determines how documents within a collection are distributed across multiple shards in the cluster. -分片键就是文档里面的一个字段,但是这个字段不是普通的字段,有一定的要求: +A shard key is essentially a field within the document, but it is not an ordinary field and has certain requirements: -- 它必须在所有文档中都出现。 -- 它必须是集合的一个索引,可以是单索引或复合索引的前缀索引,不能是多索引、文本索引或地理空间位置索引。 -- MongoDB 4.2 之前的版本,文档的分片键字段值不可变。MongoDB 4.2 版本开始,除非分片键字段是不可变的 `_id` 字段,否则您可以更新文档的分片键值。MongoDB 5.0 版本开始,实现了实时重新分片(live resharding),可以实现分片键的完全重新选择。 -- 它的大小不能超过 512 字节。 +- It must appear in all documents. +- It must be an index for the collection and can be a single index or the prefix index of a compound index; it cannot be a multi-index, text index, or geospatial index. +- Before MongoDB 4.2, the values of a document's shard key field were immutable. Since version 4.2, unless the shard key field is the immutable `_id` field, you can update the value of the shard key in the document. Starting from version 5.0, live resharding has been implemented, allowing for complete reshaping of the shard key. +- Its size cannot exceed 512 bytes. -#### 如何选择分片键? +#### How to choose a shard key? -选择合适的片键对 sharding 效率影响很大,主要基于如下四个因素(摘自[分片集群使用注意事项 - - 腾讯云文档](https://cloud.tencent.com/document/product/240/44611)): +Choosing an appropriate shard key significantly affects the efficiency of sharding and is primarily based on the following four factors (excerpt from [Sharded Cluster Usage Notes - Tencent Cloud Documentation](https://cloud.tencent.com/document/product/240/44611)): -- **取值基数** 取值基数建议尽可能大,如果用小基数的片键,因为备选值有限,那么块的总数量就有限,随着数据增多,块的大小会越来越大,导致水平扩展时移动块会非常困难。 例如:选择年龄做一个基数,范围最多只有 100 个,随着数据量增多,同一个值分布过多时,导致 chunck 的增长超出 chuncksize 的范围,引起 jumbo chunk,从而无法迁移,导致数据分布不均匀,性能瓶颈。 -- **取值分布** 取值分布建议尽量均匀,分布不均匀的片键会造成某些块的数据量非常大,同样有上面数据分布不均匀,性能瓶颈的问题。 -- **查询带分片** 查询时建议带上分片,使用分片键进行条件查询时,mongos 可以直接定位到具体分片,否则 mongos 需要将查询分发到所有分片,再等待响应返回。 -- **避免单调递增或递减** 单调递增的 sharding key,数据文件挪动小,但写入会集中,导致最后一篇的数据量持续增大,不断发生迁移,递减同理。 +- **Cardinality of values:** It is recommended to have as high cardinality as possible. If a shard key with low cardinality is used, then the total number of chunks will be limited, as there are limited alternative values. As data increases, the size of chunks will become larger, making it very difficult to move chunks during horizontal expansions. For example, choosing age as a cardinality will have a maximum range of only 100, and as data increases, the same value being distributed too much will cause chunk size to exceed chunk size limit, leading to jumbo chunks that cannot migrate, resulting in uneven data distribution and performance bottlenecks. +- **Distribution of values:** It is advisable to have a uniform distribution of values. An unevenly distributed shard key can cause some chunks with a significantly larger amount of data, similarly leading to the above issues of uneven data distribution and performance bottlenecks. +- **Query with shard key:** It is recommended to include the shard key in your queries. Using the shard key for conditional queries allows mongos to directly locate a specific shard; otherwise, mongos needs to distribute the queries to all shards and wait for responses to return. +- **Avoid monotonic increase or decrease:** Monotonic increasing shard keys may lead to small data file movements but concentrated writes, resulting in a continually growing data quantity in the last chunk and causing frequent migrations. The same applies for monotonic decreases. -综上,在选择片键时要考虑以上 4 个条件,尽可能满足更多的条件,才能降低 MoveChunks 对性能的影响,从而获得最优的性能体验。 +In summary, when choosing a shard key, consider the above four conditions and try to meet as many of those conditions as possible to reduce the impact of MoveChunks on performance and achieve the optimal performance experience. -#### 分片策略有哪些? +#### What are the sharding strategies? -MongoDB 支持两种分片算法来满足不同的查询需求(摘自[MongoDB 分片集群介绍 - 阿里云文档](https://help.aliyun.com/document_detail/64561.html?spm=a2c4g.11186623.0.0.3121565eQhUGGB#h2--shard-key-3)): +MongoDB supports two sharding algorithms to meet different query needs (excerpt from [Introduction to MongoDB Sharded Clusters - Alibaba Cloud Documentation](https://help.aliyun.com/document_detail/64561.html?spm=a2c4g.11186623.0.0.3121565eQhUGGB#h2--shard-key-3)): -**1、基于范围的分片**: +**1. Range-based sharding:** ![](https://oss.javaguide.cn/github/javaguide/database/mongodb/example-of-scope-based-sharding.png) -MongoDB 按照分片键(Shard Key)的值的范围将数据拆分为不同的块(Chunk),每个块包含了一段范围内的数据。当分片键的基数大、频率低且值非单调变更时,范围分片更高效。 +MongoDB splits data into different chunks based on the ranges of the shard key (Shard Key) values, with each chunk containing data within a certain range. Range sharding is more efficient when the cardinality of the shard key is high, frequency is low, and values are not monotonically changing. -- 优点:Mongos 可以快速定位请求需要的数据,并将请求转发到相应的 Shard 节点中。 -- 缺点:可能导致数据在 Shard 节点上分布不均衡,容易造成读写热点,且不具备写分散性。 -- 适用场景:分片键的值不是单调递增或单调递减、分片键的值基数大且重复的频率低、需要范围查询等业务场景。 +- Advantages: Mongos can quickly locate the required data for requests and forward the requests to the corresponding shard nodes. +- Disadvantages: May lead to data being unevenly distributed across shard nodes, easily causing read/write hotspots and lacking write dispersion. +- Applicable scenarios: When the values of the shard key are neither monotonically increasing nor decreasing, the cardinality of the shard key values is high with a low repetition frequency, and range queries are needed. -**2、基于 Hash 值的分片** +**2. Hash-based sharding:** ![](https://oss.javaguide.cn/github/javaguide/database/mongodb/example-of-hash-based-sharding.png) -MongoDB 计算单个字段的哈希值作为索引值,并以哈希值的范围将数据拆分为不同的块(Chunk)。 +MongoDB computes the hash value of a single field as an index value and splits data into different chunks based on the range of hash values. -- 优点:可以将数据更加均衡地分布在各 Shard 节点中,具备写分散性。 -- 缺点:不适合进行范围查询,进行范围查询时,需要将读请求分发到所有的 Shard 节点。 -- 适用场景:分片键的值存在单调递增或递减、片键的值基数大且重复的频率低、需要写入的数据随机分发、数据读取随机性较大等业务场景。 +- Advantages: Can distribute data more evenly across shard nodes, exhibiting write dispersion. +- Disadvantages: Not suitable for range queries; range queries require distributing read requests to all shard nodes. +- Applicable scenarios: When the values of the shard key exhibit monotonic increases or decreases, the cardinality of shard key values is high with a low repetition frequency, and data needs to be randomly distributed for writing or reading request randomness is high. -除了上述两种分片策略,您还可以配置 **复合片键** ,例如由一个低基数的键和一个单调递增的键组成。 +In addition to the above two sharding strategies, you can also configure **composite shard keys**, for example, one low cardinality key combined with a monotonically increasing key. -#### 分片数据如何存储? +#### How is sharded data stored? -**Chunk(块)** 是 MongoDB 分片集群的一个核心概念,其本质上就是由一组 Document 组成的逻辑数据单元。每个 Chunk 包含一定范围片键的数据,互不相交且并集为全部数据,即离散数学中**划分**的概念。 +**Chunk** is a core concept in MongoDB sharded clusters, essentially a logical unit of data composed of a set of documents. Each chunk contains data of certain ranges of shard keys, with non-overlapping ranges and a union that represents all data, aligning with the concept of **partitioning** in discrete mathematics. -分片集群不会记录每条数据在哪个分片上,而是记录 Chunk 在哪个分片上一级这个 Chunk 包含哪些数据。 +Sharded clusters do not record where each data point is located within which shard; instead, they keep track of which chunks are on which shards and which data each chunk encompasses. -默认情况下,一个 Chunk 的最大值默认为 64MB(可调整,取值范围为 1~1024 MB。如无特殊需求,建议保持默认值),进行数据插入、更新、删除时,如果此时 Mongos 感知到了目标 Chunk 的大小或者其中的数据量超过上限,则会触发 **Chunk 分裂**。 +By default, the maximum value of a chunk is set to 64MB (adjustable, ranging from 1 to 1024 MB. Unless a specific need arises, it is recommended to keep the default value). When inserting, updating, or deleting data, if mongos detects that the target chunk's size or its data exceeds the limit, it will trigger a **chunk split**. -![Chunk 分裂](https://oss.javaguide.cn/github/javaguide/database/mongodb/chunk-splitting-shard-a.png) +![Chunk Splitting](https://oss.javaguide.cn/github/javaguide/database/mongodb/chunk-splitting-shard-a.png) -数据的增长会让 Chunk 分裂得越来越多。这个时候,各个分片上的 Chunk 数量可能会不平衡。Mongos 中的 **均衡器(Balancer)** 组件就会执行自动平衡,尝试使各个 Shard 上 Chunk 的数量保持均衡,这个过程就是 **再平衡(Rebalance)**。默认情况下,数据库和集合的 Rebalance 是开启的。 +As data increases, chunks will become increasingly more numerous. At this point, the quantity of chunks on each shard may become unbalanced. The **balancer** component in mongos will execute automatic balancing, attempting to maintain an even quantity of chunks across each shard, a process known as **rebalance**. By default, rebalance for databases and collections is enabled. -如下图所示,随着数据插入,导致 Chunk 分裂,让 AB 两个分片有 3 个 Chunk,C 分片只有一个,这个时候就会把 B 分配的迁移一个到 C 分片实现集群数据均衡。 +As shown in the diagram below, as data is inserted leading to chunk splits, shards A and B may have 3 chunks, while shard C has only one; at this point, one chunk from B may be migrated to shard C to achieve balanced cluster data. -![Chunk 迁移](https://oss.javaguide.cn/github/javaguide/database/mongodb/mongo-reblance-three-shards.png) +![Chunk Migration](https://oss.javaguide.cn/github/javaguide/database/mongodb/mongo-reblance-three-shards.png) -> Balancer 是 MongoDB 的一个运行在 Config Server 的 Primary 节点上(自 MongoDB 3.4 版本起)的后台进程,它监控每个分片上 Chunk 数量,并在某个分片上 Chunk 数量达到阈值进行迁移。 +> The balancer is a background process running on the Primary node of the Config Server (since MongoDB version 3.4) that monitors the number of chunks on each shard and migrates chunks when the quantity on a shard exceeds a threshold. -Chunk 只会分裂,不会合并,即使 chunkSize 的值变大。 +Chunks only split, they do not merge, even if the value of chunkSize increases. -Rebalance 操作是比较耗费系统资源的,我们可以通过在业务低峰期执行、预分片或者设置 Rebalance 时间窗等方式来减少其对 MongoDB 正常使用所带来的影响。 +Rebalance operations are resource-intensive; thus, we can reduce their impact on regular MongoDB operations by executing them during off-peak business hours, pre-splitting, or setting rebalance time windows. -#### Chunk 迁移原理是什么? +#### What is the principle behind chunk migration? -关于 Chunk 迁移原理的详细介绍,推荐阅读 MongoDB 中文社区的[一文读懂 MongoDB chunk 迁移](https://mongoing.com/archives/77479)这篇文章。 +For a detailed introduction to the principles of chunk migration, it is recommended to read the article on [Understanding MongoDB chunk migration in a single article](https://mongoing.com/archives/77479) from the MongoDB Chinese community. -## 学习资料推荐 +## Recommended Learning Resources -- [MongoDB 中文手册|官方文档中文版](https://docs.mongoing.com/)(推荐):基于 4.2 版本,不断与官方最新版保持同步。 -- [MongoDB 初学者教程——7 天学习 MongoDB](https://mongoing.com/archives/docs/mongodb%e5%88%9d%e5%ad%a6%e8%80%85%e6%95%99%e7%a8%8b/mongodb%e5%a6%82%e4%bd%95%e5%88%9b%e5%bb%ba%e6%95%b0%e6%8d%ae%e5%ba%93%e5%92%8c%e9%9b%86%e5%90%88):快速入门。 -- [SpringBoot 整合 MongoDB 实战 - 2022](https://www.cnblogs.com/dxflqm/p/16643981.html):很不错的一篇 MongoDB 入门文章,主要围绕 MongoDB 的 Java 客户端使用进行基本的增删改查操作介绍。 +- [MongoDB Chinese Manual | Official Documentation in Chinese](https://docs.mongoing.com/) (Recommended): Based on version 4.2, continuously synchronized with the latest official version. +- [MongoDB Beginner's Tutorial — Learn MongoDB in 7 Days](https://mongoing.com/archives/docs/mongodb%e5%88%9d%e5%ad%a6%e8%80%85%e6%95%99%e7%a8%8b/mongodb%e5%a6%82%e4%bd%95%e5%88%9b%e5%bb%ba%e6%95%b0%e6%8d%ae%e5%ba%93%e5%92%8c%e9%9b%86%e5%90%88): A quick introduction. +- [SpringBoot Integration with MongoDB Practice - 2022](https://www.cnblogs.com/dxflqm/p/16643981.html): A great introductory article on MongoDB, mainly focusing on basic CRUD operations using the MongoDB Java client. -## 参考 +## References -- MongoDB 官方文档(主要参考资料,以官方文档为准): -- 《MongoDB 权威指南》 -- Indexes - MongoDB 官方文档: -- MongoDB - 索引知识 - 程序员翔仔 - 2022: -- MongoDB - 索引: -- Sharding - MongoDB 官方文档: -- MongoDB 分片集群介绍 - 阿里云文档: -- 分片集群使用注意事项 - - 腾讯云文档: +- MongoDB Official Documentation (main reference material, take as authoritative): +- "MongoDB: The Definitive Guide" +- Indexes - MongoDB Official Documentation: +- MongoDB - Index Knowledge - Programmer Xiangzai - 2022: +- MongoDB - Index: +- Sharding - MongoDB Official Documentation: +- Introduction to MongoDB Sharded Clusters - Alibaba Cloud Documentation: +- Usage Notes for Sharded Clusters - Tencent Cloud Documentation: diff --git a/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md b/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md index cb30376687b..1be4c654d08 100644 --- a/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md +++ b/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md @@ -1,957 +1,113 @@ --- -title: 一千行 MySQL 学习笔记 -category: 数据库 +title: One Thousand Lines of MySQL Learning Notes +category: Database tag: - MySQL --- -> 原文地址: ,JavaGuide 对本文进行了简答排版,新增了目录。 +> Original address: . JavaGuide has made a simple layout of this article and added a table of contents. -非常不错的总结,强烈建议保存下来,需要的时候看一看。 +A very good summary, strongly recommended to save it for reference when needed. -### 基本操作 +### Basic Operations ```sql -/* Windows服务 */ --- 启动 MySQL +/* Windows Service */ +-- Start MySQL net start mysql --- 创建Windows服务 - sc create mysql binPath= mysqld_bin_path(注意:等号与值之间有空格) -/* 连接与断开服务器 */ --- 连接 MySQL - mysql -h 地址 -P 端口 -u 用户名 -p 密码 --- 显示哪些线程正在运行 +-- Create Windows Service + sc create mysql binPath= mysqld_bin_path (Note: there is a space between the equal sign and the value) +/* Connect and Disconnect from Server */ +-- Connect to MySQL + mysql -h address -P port -u username -p password +-- Show which threads are running SHOW PROCESSLIST --- 显示系统变量信息 +-- Show system variable information SHOW VARIABLES ``` -### 数据库操作 +### Database Operations ```sql -/* 数据库操作 */ --- 查看当前数据库 +/* Database Operations */ +-- View current database SELECT DATABASE(); --- 显示当前时间、用户名、数据库版本 +-- Show current time, username, database version SELECT now(), user(), version(); --- 创建库 - CREATE DATABASE[ IF NOT EXISTS] 数据库名 数据库选项 - 数据库选项: +-- Create database + CREATE DATABASE [IF NOT EXISTS] database_name database_options + Database options: CHARACTER SET charset_name COLLATE collation_name --- 查看已有库 - SHOW DATABASES[ LIKE 'PATTERN'] --- 查看当前库信息 - SHOW CREATE DATABASE 数据库名 --- 修改库的选项信息 - ALTER DATABASE 库名 选项信息 --- 删除库 - DROP DATABASE[ IF EXISTS] 数据库名 - 同时删除该数据库相关的目录及其目录内容 -``` - -### 表的操作 - -```sql -/* 表的操作 */ --- 创建表 - CREATE [TEMPORARY] TABLE[ IF NOT EXISTS] [库名.]表名 ( 表的结构定义 )[ 表选项] - 每个字段必须有数据类型 - 最后一个字段后不能有逗号 - TEMPORARY 临时表,会话结束时表自动消失 - 对于字段的定义: - 字段名 数据类型 [NOT NULL | NULL] [DEFAULT default_value] [AUTO_INCREMENT] [UNIQUE [KEY] | [PRIMARY] KEY] [COMMENT 'string'] --- 表选项 - -- 字符集 +-- View existing databases + SHOW DATABASES [LIKE 'PATTERN'] +-- View current database information + SHOW CREATE DATABASE database_name +-- Modify database options + ALTER DATABASE database_name options +-- Delete database + DROP DATABASE [IF EXISTS] database_name + Also deletes the directory related to the database and its contents +``` + +### Table Operations + +```sql +/* Table Operations */ +-- Create table + CREATE [TEMPORARY] TABLE [IF NOT EXISTS] [database_name.]table_name (table structure definition) [table options] + Each field must have a data type + No comma after the last field + TEMPORARY temporary table, automatically disappears at the end of the session + For field definitions: + field_name data_type [NOT NULL | NULL] [DEFAULT default_value] [AUTO_INCREMENT] [UNIQUE [KEY] | [PRIMARY] KEY] [COMMENT 'string'] +-- Table options + -- Character set CHARSET = charset_name - 如果表没有设定,则使用数据库字符集 - -- 存储引擎 + If the table is not set, the database character set is used + -- Storage engine ENGINE = engine_name - 表在管理数据时采用的不同的数据结构,结构不同会导致处理方式、提供的特性操作等不同 - 常见的引擎:InnoDB MyISAM Memory/Heap BDB Merge Example CSV MaxDB Archive - 不同的引擎在保存表的结构和数据时采用不同的方式 - MyISAM表文件含义:.frm表定义,.MYD表数据,.MYI表索引 - InnoDB表文件含义:.frm表定义,表空间数据和日志文件 - SHOW ENGINES -- 显示存储引擎的状态信息 - SHOW ENGINE 引擎名 {LOGS|STATUS} -- 显示存储引擎的日志或状态信息 - -- 自增起始数 - AUTO_INCREMENT = 行数 - -- 数据文件目录 - DATA DIRECTORY = '目录' - -- 索引文件目录 - INDEX DIRECTORY = '目录' - -- 表注释 + Different data structures used by the table when managing data, different structures lead to different processing methods and features + Common engines: InnoDB, MyISAM, Memory/Heap, BDB, Merge, Example, CSV, MaxDB, Archive + Different engines use different methods to save table structures and data + MyISAM table file meanings: .frm table definition, .MYD table data, .MYI table index + InnoDB table file meanings: .frm table definition, table space data, and log files + SHOW ENGINES -- Show storage engine status information + SHOW ENGINE engine_name {LOGS|STATUS} -- Show storage engine logs or status information + -- Auto-increment starting number + AUTO_INCREMENT = row_number + -- Data file directory + DATA DIRECTORY = 'directory' + -- Index file directory + INDEX DIRECTORY = 'directory' + -- Table comment COMMENT = 'string' - -- 分区选项 - PARTITION BY ... (详细见手册) --- 查看所有表 - SHOW TABLES[ LIKE 'pattern'] - SHOW TABLES FROM 库名 --- 查看表结构 - SHOW CREATE TABLE 表名 (信息更详细) - DESC 表名 / DESCRIBE 表名 / EXPLAIN 表名 / SHOW COLUMNS FROM 表名 [LIKE 'PATTERN'] + -- Partition options + PARTITION BY ... (see manual for details) +-- View all tables + SHOW TABLES [LIKE 'pattern'] + SHOW TABLES FROM database_name +-- View table structure + SHOW CREATE TABLE table_name (more detailed information) + DESC table_name / DESCRIBE table_name / EXPLAIN table_name / SHOW COLUMNS FROM table_name [LIKE 'PATTERN'] SHOW TABLE STATUS [FROM db_name] [LIKE 'pattern'] --- 修改表 - -- 修改表本身的选项 - ALTER TABLE 表名 表的选项 - eg: ALTER TABLE 表名 ENGINE=MYISAM; - -- 对表进行重命名 - RENAME TABLE 原表名 TO 新表名 - RENAME TABLE 原表名 TO 库名.表名 (可将表移动到另一个数据库) - -- RENAME可以交换两个表名 - -- 修改表的字段机构(13.1.2. ALTER TABLE语法) - ALTER TABLE 表名 操作名 - -- 操作名 - ADD[ COLUMN] 字段定义 -- 增加字段 - AFTER 字段名 -- 表示增加在该字段名后面 - FIRST -- 表示增加在第一个 - ADD PRIMARY KEY(字段名) -- 创建主键 - ADD UNIQUE [索引名] (字段名)-- 创建唯一索引 - ADD INDEX [索引名] (字段名) -- 创建普通索引 - DROP[ COLUMN] 字段名 -- 删除字段 - MODIFY[ COLUMN] 字段名 字段属性 -- 支持对字段属性进行修改,不能修改字段名(所有原有属性也需写上) - CHANGE[ COLUMN] 原字段名 新字段名 字段属性 -- 支持对字段名修改 - DROP PRIMARY KEY -- 删除主键(删除主键前需删除其AUTO_INCREMENT属性) - DROP INDEX 索引名 -- 删除索引 - DROP FOREIGN KEY 外键 -- 删除外键 --- 删除表 - DROP TABLE[ IF EXISTS] 表名 ... --- 清空表数据 - TRUNCATE [TABLE] 表名 --- 复制表结构 - CREATE TABLE 表名 LIKE 要复制的表名 --- 复制表结构和数据 - CREATE TABLE 表名 [AS] SELECT * FROM 要复制的表名 --- 检查表是否有错误 - CHECK TABLE tbl_name [, tbl_name] ... [option] ... --- 优化表 - OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... --- 修复表 - REPAIR [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... [QUICK] [EXTENDED] [USE_FRM] --- 分析表 - ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... -``` - -### 数据操作 - -```sql -/* 数据操作 */ ------------------ --- 增 - INSERT [INTO] 表名 [(字段列表)] VALUES (值列表)[, (值列表), ...] - -- 如果要插入的值列表包含所有字段并且顺序一致,则可以省略字段列表。 - -- 可同时插入多条数据记录! - REPLACE与INSERT类似,唯一的区别是对于匹配的行,现有行(与主键/唯一键比较)的数据会被替换,如果没有现有行,则插入新行。 - INSERT [INTO] 表名 SET 字段名=值[, 字段名=值, ...] --- 查 - SELECT 字段列表 FROM 表名[ 其他子句] - -- 可来自多个表的多个字段 - -- 其他子句可以不使用 - -- 字段列表可以用*代替,表示所有字段 --- 删 - DELETE FROM 表名[ 删除条件子句] - 没有条件子句,则会删除全部 --- 改 - UPDATE 表名 SET 字段名=新值[, 字段名=新值] [更新条件] -``` - -### 字符集编码 - -```sql -/* 字符集编码 */ ------------------ --- MySQL、数据库、表、字段均可设置编码 --- 数据编码与客户端编码不需一致 -SHOW VARIABLES LIKE 'character_set_%' -- 查看所有字符集编码项 - character_set_client 客户端向服务器发送数据时使用的编码 - character_set_results 服务器端将结果返回给客户端所使用的编码 - character_set_connection 连接层编码 -SET 变量名 = 变量值 - SET character_set_client = gbk; - SET character_set_results = gbk; - SET character_set_connection = gbk; -SET NAMES GBK; -- 相当于完成以上三个设置 --- 校对集 - 校对集用以排序 - SHOW CHARACTER SET [LIKE 'pattern']/SHOW CHARSET [LIKE 'pattern'] 查看所有字符集 - SHOW COLLATION [LIKE 'pattern'] 查看所有校对集 - CHARSET 字符集编码 设置字符集编码 - COLLATE 校对集编码 设置校对集编码 -``` - -### 数据类型(列类型) - -```sql -/* 数据类型(列类型) */ ------------------ -1. 数值类型 --- a. 整型 ---------- - 类型 字节 范围(有符号位) - tinyint 1字节 -128 ~ 127 无符号位:0 ~ 255 - smallint 2字节 -32768 ~ 32767 - mediumint 3字节 -8388608 ~ 8388607 - int 4字节 - bigint 8字节 - int(M) M表示总位数 - - 默认存在符号位,unsigned 属性修改 - - 显示宽度,如果某个数不够定义字段时设置的位数,则前面以0补填,zerofill 属性修改 - 例:int(5) 插入一个数'123',补填后为'00123' - - 在满足要求的情况下,越小越好。 - - 1表示bool值真,0表示bool值假。MySQL没有布尔类型,通过整型0和1表示。常用tinyint(1)表示布尔型。 --- b. 浮点型 ---------- - 类型 字节 范围 - float(单精度) 4字节 - double(双精度) 8字节 - 浮点型既支持符号位 unsigned 属性,也支持显示宽度 zerofill 属性。 - 不同于整型,前后均会补填0. - 定义浮点型时,需指定总位数和小数位数。 - float(M, D) double(M, D) - M表示总位数,D表示小数位数。 - M和D的大小会决定浮点数的范围。不同于整型的固定范围。 - M既表示总位数(不包括小数点和正负号),也表示显示宽度(所有显示符号均包括)。 - 支持科学计数法表示。 - 浮点数表示近似值。 --- c. 定点数 ---------- - decimal -- 可变长度 - decimal(M, D) M也表示总位数,D表示小数位数。 - 保存一个精确的数值,不会发生数据的改变,不同于浮点数的四舍五入。 - 将浮点数转换为字符串来保存,每9位数字保存为4个字节。 -2. 字符串类型 --- a. char, varchar ---------- - char 定长字符串,速度快,但浪费空间 - varchar 变长字符串,速度慢,但节省空间 - M表示能存储的最大长度,此长度是字符数,非字节数。 - 不同的编码,所占用的空间不同。 - char,最多255个字符,与编码无关。 - varchar,最多65535字符,与编码有关。 - 一条有效记录最大不能超过65535个字节。 - utf8 最大为21844个字符,gbk 最大为32766个字符,latin1 最大为65532个字符 - varchar 是变长的,需要利用存储空间保存 varchar 的长度,如果数据小于255个字节,则采用一个字节来保存长度,反之需要两个字节来保存。 - varchar 的最大有效长度由最大行大小和使用的字符集确定。 - 最大有效长度是65532字节,因为在varchar存字符串时,第一个字节是空的,不存在任何数据,然后还需两个字节来存放字符串的长度,所以有效长度是65535-1-2=65532字节。 - 例:若一个表定义为 CREATE TABLE tb(c1 int, c2 char(30), c3 varchar(N)) charset=utf8; 问N的最大值是多少? 答:(65535-1-2-4-30*3)/3 --- b. blob, text ---------- - blob 二进制字符串(字节字符串) - tinyblob, blob, mediumblob, longblob - text 非二进制字符串(字符字符串) - tinytext, text, mediumtext, longtext - text 在定义时,不需要定义长度,也不会计算总长度。 - text 类型在定义时,不可给default值 --- c. binary, varbinary ---------- - 类似于char和varchar,用于保存二进制字符串,也就是保存字节字符串而非字符字符串。 - char, varchar, text 对应 binary, varbinary, blob. -3. 日期时间类型 - 一般用整型保存时间戳,因为PHP可以很方便的将时间戳进行格式化。 - datetime 8字节 日期及时间 1000-01-01 00:00:00 到 9999-12-31 23:59:59 - date 3字节 日期 1000-01-01 到 9999-12-31 - timestamp 4字节 时间戳 19700101000000 到 2038-01-19 03:14:07 - time 3字节 时间 -838:59:59 到 838:59:59 - year 1字节 年份 1901 - 2155 -datetime YYYY-MM-DD hh:mm:ss -timestamp YY-MM-DD hh:mm:ss - YYYYMMDDhhmmss - YYMMDDhhmmss - YYYYMMDDhhmmss - YYMMDDhhmmss -date YYYY-MM-DD - YY-MM-DD - YYYYMMDD - YYMMDD - YYYYMMDD - YYMMDD -time hh:mm:ss - hhmmss - hhmmss -year YYYY - YY - YYYY - YY -4. 枚举和集合 --- 枚举(enum) ---------- -enum(val1, val2, val3...) - 在已知的值中进行单选。最大数量为65535. - 枚举值在保存时,以2个字节的整型(smallint)保存。每个枚举值,按保存的位置顺序,从1开始逐一递增。 - 表现为字符串类型,存储却是整型。 - NULL值的索引是NULL。 - 空字符串错误值的索引值是0。 --- 集合(set) ---------- -set(val1, val2, val3...) - create table tab ( gender set('男', '女', '无') ); - insert into tab values ('男, 女'); - 最多可以有64个不同的成员。以bigint存储,共8个字节。采取位运算的形式。 - 当创建表时,SET成员值的尾部空格将自动被删除。 -``` - -### 列属性(列约束) - -```sql -/* 列属性(列约束) */ ------------------ -1. PRIMARY 主键 - - 能唯一标识记录的字段,可以作为主键。 - - 一个表只能有一个主键。 - - 主键具有唯一性。 - - 声明字段时,用 primary key 标识。 - 也可以在字段列表之后声明 - 例:create table tab ( id int, stu varchar(10), primary key (id)); - - 主键字段的值不能为null。 - - 主键可以由多个字段共同组成。此时需要在字段列表后声明的方法。 - 例:create table tab ( id int, stu varchar(10), age int, primary key (stu, age)); -2. UNIQUE 唯一索引(唯一约束) - 使得某字段的值也不能重复。 -3. NULL 约束 - null不是数据类型,是列的一个属性。 - 表示当前列是否可以为null,表示什么都没有。 - null, 允许为空。默认。 - not null, 不允许为空。 - insert into tab values (null, 'val'); - -- 此时表示将第一个字段的值设为null, 取决于该字段是否允许为null -4. DEFAULT 默认值属性 - 当前字段的默认值。 - insert into tab values (default, 'val'); -- 此时表示强制使用默认值。 - create table tab ( add_time timestamp default current_timestamp ); - -- 表示将当前时间的时间戳设为默认值。 - current_date, current_time -5. AUTO_INCREMENT 自动增长约束 - 自动增长必须为索引(主键或unique) - 只能存在一个字段为自动增长。 - 默认为1开始自动增长。可以通过表属性 auto_increment = x进行设置,或 alter table tbl auto_increment = x; -6. COMMENT 注释 - 例:create table tab ( id int ) comment '注释内容'; -7. FOREIGN KEY 外键约束 - 用于限制主表与从表数据完整性。 - alter table t1 add constraint `t1_t2_fk` foreign key (t1_id) references t2(id); - -- 将表t1的t1_id外键关联到表t2的id字段。 - -- 每个外键都有一个名字,可以通过 constraint 指定 - 存在外键的表,称之为从表(子表),外键指向的表,称之为主表(父表)。 - 作用:保持数据一致性,完整性,主要目的是控制存储在外键表(从表)中的数据。 - MySQL中,可以对InnoDB引擎使用外键约束: - 语法: - foreign key (外键字段) references 主表名 (关联字段) [主表记录删除时的动作] [主表记录更新时的动作] - 此时需要检测一个从表的外键需要约束为主表的已存在的值。外键在没有关联的情况下,可以设置为null.前提是该外键列,没有not null。 - 可以不指定主表记录更改或更新时的动作,那么此时主表的操作被拒绝。 - 如果指定了 on update 或 on delete:在删除或更新时,有如下几个操作可以选择: - 1. cascade,级联操作。主表数据被更新(主键值更新),从表也被更新(外键值更新)。主表记录被删除,从表相关记录也被删除。 - 2. set null,设置为null。主表数据被更新(主键值更新),从表的外键被设置为null。主表记录被删除,从表相关记录外键被设置成null。但注意,要求该外键列,没有not null属性约束。 - 3. restrict,拒绝父表删除和更新。 - 注意,外键只被InnoDB存储引擎所支持。其他引擎是不支持的。 - -``` - -### 建表规范 - -```sql -/* 建表规范 */ ------------------ - -- Normal Format, NF - - 每个表保存一个实体信息 - - 每个具有一个ID字段作为主键 - - ID主键 + 原子表 - -- 1NF, 第一范式 - 字段不能再分,就满足第一范式。 - -- 2NF, 第二范式 - 满足第一范式的前提下,不能出现部分依赖。 - 消除复合主键就可以避免部分依赖。增加单列关键字。 - -- 3NF, 第三范式 - 满足第二范式的前提下,不能出现传递依赖。 - 某个字段依赖于主键,而有其他字段依赖于该字段。这就是传递依赖。 - 将一个实体信息的数据放在一个表内实现。 -``` - -### SELECT - -```sql -/* SELECT */ ------------------ -SELECT [ALL|DISTINCT] select_expr FROM -> WHERE -> GROUP BY [合计函数] -> HAVING -> ORDER BY -> LIMIT -a. select_expr - -- 可以用 * 表示所有字段。 - select * from tb; - -- 可以使用表达式(计算公式、函数调用、字段也是个表达式) - select stu, 29+25, now() from tb; - -- 可以为每个列使用别名。适用于简化列标识,避免多个列标识符重复。 - - 使用 as 关键字,也可省略 as. - select stu+10 as add10 from tb; -b. FROM 子句 - 用于标识查询来源。 - -- 可以为表起别名。使用as关键字。 - SELECT * FROM tb1 AS tt, tb2 AS bb; - -- from子句后,可以同时出现多个表。 - -- 多个表会横向叠加到一起,而数据会形成一个笛卡尔积。 - SELECT * FROM tb1, tb2; - -- 向优化符提示如何选择索引 - USE INDEX、IGNORE INDEX、FORCE INDEX - SELECT * FROM table1 USE INDEX (key1,key2) WHERE key1=1 AND key2=2 AND key3=3; - SELECT * FROM table1 IGNORE INDEX (key3) WHERE key1=1 AND key2=2 AND key3=3; -c. WHERE 子句 - -- 从from获得的数据源中进行筛选。 - -- 整型1表示真,0表示假。 - -- 表达式由运算符和运算数组成。 - -- 运算数:变量(字段)、值、函数返回值 - -- 运算符: - =, <=>, <>, !=, <=, <, >=, >, !, &&, ||, - in (not) null, (not) like, (not) in, (not) between and, is (not), and, or, not, xor - is/is not 加上true/false/unknown,检验某个值的真假 - <=>与<>功能相同,<=>可用于null比较 -d. GROUP BY 子句, 分组子句 - GROUP BY 字段/别名 [排序方式] - 分组后会进行排序。升序:ASC,降序:DESC - 以下[合计函数]需配合 GROUP BY 使用: - count 返回不同的非NULL值数目 count(*)、count(字段) - sum 求和 - max 求最大值 - min 求最小值 - avg 求平均值 - group_concat 返回带有来自一个组的连接的非NULL值的字符串结果。组内字符串连接。 -e. HAVING 子句,条件子句 - 与 where 功能、用法相同,执行时机不同。 - where 在开始时执行检测数据,对原数据进行过滤。 - having 对筛选出的结果再次进行过滤。 - having 字段必须是查询出来的,where 字段必须是数据表存在的。 - where 不可以使用字段的别名,having 可以。因为执行WHERE代码时,可能尚未确定列值。 - where 不可以使用合计函数。一般需用合计函数才会用 having - SQL标准要求HAVING必须引用GROUP BY子句中的列或用于合计函数中的列。 -f. ORDER BY 子句,排序子句 - order by 排序字段/别名 排序方式 [,排序字段/别名 排序方式]... - 升序:ASC,降序:DESC - 支持多个字段的排序。 -g. LIMIT 子句,限制结果数量子句 - 仅对处理好的结果进行数量限制。将处理好的结果的看作是一个集合,按照记录出现的顺序,索引从0开始。 - limit 起始位置, 获取条数 - 省略第一个参数,表示从索引0开始。limit 获取条数 -h. DISTINCT, ALL 选项 - distinct 去除重复记录 - 默认为 all, 全部记录 -``` - -### UNION - -```sql -/* UNION */ ------------------ - 将多个select查询的结果组合成一个结果集合。 - SELECT ... UNION [ALL|DISTINCT] SELECT ... - 默认 DISTINCT 方式,即所有返回的行都是唯一的 - 建议,对每个SELECT查询加上小括号包裹。 - ORDER BY 排序时,需加上 LIMIT 进行结合。 - 需要各select查询的字段数量一样。 - 每个select查询的字段列表(数量、类型)应一致,因为结果中的字段名以第一条select语句为准。 -``` - -### 子查询 - -```sql -/* 子查询 */ ------------------ - - 子查询需用括号包裹。 --- from型 - from后要求是一个表,必须给子查询结果取个别名。 - - 简化每个查询内的条件。 - - from型需将结果生成一个临时表格,可用以原表的锁定的释放。 - - 子查询返回一个表,表型子查询。 - select * from (select * from tb where id>0) as subfrom where id>1; --- where型 - - 子查询返回一个值,标量子查询。 - - 不需要给子查询取别名。 - - where子查询内的表,不能直接用以更新。 - select * from tb where money = (select max(money) from tb); - -- 列子查询 - 如果子查询结果返回的是一列。 - 使用 in 或 not in 完成查询 - exists 和 not exists 条件 - 如果子查询返回数据,则返回1或0。常用于判断条件。 - select column1 from t1 where exists (select * from t2); - -- 行子查询 - 查询条件是一个行。 - select * from t1 where (id, gender) in (select id, gender from t2); - 行构造符:(col1, col2, ...) 或 ROW(col1, col2, ...) - 行构造符通常用于与对能返回两个或两个以上列的子查询进行比较。 - -- 特殊运算符 - != all() 相当于 not in - = some() 相当于 in。any 是 some 的别名 - != some() 不等同于 not in,不等于其中某一个。 - all, some 可以配合其他运算符一起使用。 -``` - -### 连接查询(join) - -```sql -/* 连接查询(join) */ ------------------ - 将多个表的字段进行连接,可以指定连接条件。 --- 内连接(inner join) - - 默认就是内连接,可省略inner。 - - 只有数据存在时才能发送连接。即连接结果不能出现空行。 - on 表示连接条件。其条件表达式与where类似。也可以省略条件(表示条件永远为真) - 也可用where表示连接条件。 - 还有 using, 但需字段名相同。 using(字段名) - -- 交叉连接 cross join - 即,没有条件的内连接。 - select * from tb1 cross join tb2; --- 外连接(outer join) - - 如果数据不存在,也会出现在连接结果中。 - -- 左外连接 left join - 如果数据不存在,左表记录会出现,而右表为null填充 - -- 右外连接 right join - 如果数据不存在,右表记录会出现,而左表为null填充 --- 自然连接(natural join) - 自动判断连接条件完成连接。 - 相当于省略了using,会自动查找相同字段名。 - natural join - natural left join - natural right join -select info.id, info.name, info.stu_num, extra_info.hobby, extra_info.sex from info, extra_info where info.stu_num = extra_info.stu_id; -``` - -### TRUNCATE - -```sql -/* TRUNCATE */ ------------------ -TRUNCATE [TABLE] tbl_name -清空数据 -删除重建表 -区别: -1,truncate 是删除表再创建,delete 是逐条删除 -2,truncate 重置auto_increment的值。而delete不会 -3,truncate 不知道删除了几条,而delete知道。 -4,当被用于带分区的表时,truncate 会保留分区 -``` - -### 备份与还原 - -```sql -/* 备份与还原 */ ------------------ -备份,将数据的结构与表内数据保存起来。 -利用 mysqldump 指令完成。 --- 导出 -mysqldump [options] db_name [tables] -mysqldump [options] ---database DB1 [DB2 DB3...] -mysqldump [options] --all--database -1. 导出一张表 -  mysqldump -u用户名 -p密码 库名 表名 > 文件名(D:/a.sql) -2. 导出多张表 -  mysqldump -u用户名 -p密码 库名 表1 表2 表3 > 文件名(D:/a.sql) -3. 导出所有表 -  mysqldump -u用户名 -p密码 库名 > 文件名(D:/a.sql) -4. 导出一个库 -  mysqldump -u用户名 -p密码 --lock-all-tables --database 库名 > 文件名(D:/a.sql) -可以-w携带WHERE条件 --- 导入 -1. 在登录mysql的情况下: -  source 备份文件 -2. 在不登录的情况下 -  mysql -u用户名 -p密码 库名 < 备份文件 -``` - -### 视图 - -```sql -什么是视图: - 视图是一个虚拟表,其内容由查询定义。同真实的表一样,视图包含一系列带有名称的列和行数据。但是,视图并不在数据库中以存储的数据值集形式存在。行和列数据来自由定义视图的查询所引用的表,并且在引用视图时动态生成。 - 视图具有表结构文件,但不存在数据文件。 - 对其中所引用的基础表来说,视图的作用类似于筛选。定义视图的筛选可以来自当前或其它数据库的一个或多个表,或者其它视图。通过视图进行查询没有任何限制,通过它们进行数据修改时的限制也很少。 - 视图是存储在数据库中的查询的sql语句,它主要出于两种原因:安全原因,视图可以隐藏一些数据,如:社会保险基金表,可以用视图只显示姓名,地址,而不显示社会保险号和工资数等,另一原因是可使复杂的查询易于理解和使用。 --- 创建视图 -CREATE [OR REPLACE] [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] VIEW view_name [(column_list)] AS select_statement - - 视图名必须唯一,同时不能与表重名。 - - 视图可以使用select语句查询到的列名,也可以自己指定相应的列名。 - - 可以指定视图执行的算法,通过ALGORITHM指定。 - - column_list如果存在,则数目必须等于SELECT语句检索的列数 --- 查看结构 - SHOW CREATE VIEW view_name --- 删除视图 - - 删除视图后,数据依然存在。 - - 可同时删除多个视图。 - DROP VIEW [IF EXISTS] view_name ... --- 修改视图结构 - - 一般不修改视图,因为不是所有的更新视图都会映射到表上。 - ALTER VIEW view_name [(column_list)] AS select_statement --- 视图作用 - 1. 简化业务逻辑 - 2. 对客户端隐藏真实的表结构 --- 视图算法(ALGORITHM) - MERGE 合并 - 将视图的查询语句,与外部查询需要先合并再执行! - TEMPTABLE 临时表 - 将视图执行完毕后,形成临时表,再做外层查询! - UNDEFINED 未定义(默认),指的是MySQL自主去选择相应的算法。 -``` - -### 事务(transaction) - -```sql -事务是指逻辑上的一组操作,组成这组操作的各个单元,要不全成功要不全失败。 - - 支持连续SQL的集体成功或集体撤销。 - - 事务是数据库在数据完整性方面的一个功能。 - - 需要利用 InnoDB 或 BDB 存储引擎,对自动提交的特性支持完成。 - - InnoDB被称为事务安全型引擎。 --- 事务开启 - START TRANSACTION; 或者 BEGIN; - 开启事务后,所有被执行的SQL语句均被认作当前事务内的SQL语句。 --- 事务提交 - COMMIT; --- 事务回滚 - ROLLBACK; - 如果部分操作发生问题,映射到事务开启前。 --- 事务的特性 - 1. 原子性(Atomicity) - 事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。 - 2. 一致性(Consistency) - 事务前后数据的完整性必须保持一致。 - - 事务开始和结束时,外部数据一致 - - 在整个事务过程中,操作是连续的 - 3. 隔离性(Isolation) - 多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间的数据要相互隔离。 - 4. 持久性(Durability) - 一个事务一旦被提交,它对数据库中的数据改变就是永久性的。 --- 事务的实现 - 1. 要求是事务支持的表类型 - 2. 执行一组相关的操作前开启事务 - 3. 整组操作完成后,都成功,则提交;如果存在失败,选择回滚,则会回到事务开始的备份点。 --- 事务的原理 - 利用InnoDB的自动提交(autocommit)特性完成。 - 普通的MySQL执行语句后,当前的数据提交操作均可被其他客户端可见。 - 而事务是暂时关闭“自动提交”机制,需要commit提交持久化数据操作。 --- 注意 - 1. 数据定义语言(DDL)语句不能被回滚,比如创建或取消数据库的语句,和创建、取消或更改表或存储的子程序的语句。 - 2. 事务不能被嵌套 --- 保存点 - SAVEPOINT 保存点名称 -- 设置一个事务保存点 - ROLLBACK TO SAVEPOINT 保存点名称 -- 回滚到保存点 - RELEASE SAVEPOINT 保存点名称 -- 删除保存点 --- InnoDB自动提交特性设置 - SET autocommit = 0|1; 0表示关闭自动提交,1表示开启自动提交。 - - 如果关闭了,那普通操作的结果对其他客户端也不可见,需要commit提交后才能持久化数据操作。 - - 也可以关闭自动提交来开启事务。但与START TRANSACTION不同的是, - SET autocommit是永久改变服务器的设置,直到下次再次修改该设置。(针对当前连接) - 而START TRANSACTION记录开启前的状态,而一旦事务提交或回滚后就需要再次开启事务。(针对当前事务) - -``` - -### 锁表 - -```sql -/* 锁表 */ -表锁定只用于防止其它客户端进行不正当地读取和写入 -MyISAM 支持表锁,InnoDB 支持行锁 --- 锁定 - LOCK TABLES tbl_name [AS alias] --- 解锁 - UNLOCK TABLES -``` - -### 触发器 - -```sql -/* 触发器 */ ------------------ - 触发程序是与表有关的命名数据库对象,当该表出现特定事件时,将激活该对象 - 监听:记录的增加、修改、删除。 --- 创建触发器 -CREATE TRIGGER trigger_name trigger_time trigger_event ON tbl_name FOR EACH ROW trigger_stmt - 参数: - trigger_time是触发程序的动作时间。它可以是 before 或 after,以指明触发程序是在激活它的语句之前或之后触发。 - trigger_event指明了激活触发程序的语句的类型 - INSERT:将新行插入表时激活触发程序 - UPDATE:更改某一行时激活触发程序 - DELETE:从表中删除某一行时激活触发程序 - tbl_name:监听的表,必须是永久性的表,不能将触发程序与TEMPORARY表或视图关联起来。 - trigger_stmt:当触发程序激活时执行的语句。执行多个语句,可使用BEGIN...END复合语句结构 --- 删除 -DROP TRIGGER [schema_name.]trigger_name -可以使用old和new代替旧的和新的数据 - 更新操作,更新前是old,更新后是new. - 删除操作,只有old. - 增加操作,只有new. --- 注意 - 1. 对于具有相同触发程序动作时间和事件的给定表,不能有两个触发程序。 --- 字符连接函数 -concat(str1,str2,...]) -concat_ws(separator,str1,str2,...) --- 分支语句 -if 条件 then - 执行语句 -elseif 条件 then - 执行语句 -else - 执行语句 -end if; --- 修改最外层语句结束符 -delimiter 自定义结束符号 - SQL语句 -自定义结束符号 -delimiter ; -- 修改回原来的分号 --- 语句块包裹 -begin - 语句块 -end --- 特殊的执行 -1. 只要添加记录,就会触发程序。 -2. Insert into on duplicate key update 语法会触发: - 如果没有重复记录,会触发 before insert, after insert; - 如果有重复记录并更新,会触发 before insert, before update, after update; - 如果有重复记录但是没有发生更新,则触发 before insert, before update -3. Replace 语法 如果有记录,则执行 before insert, before delete, after delete, after insert -``` - -### SQL 编程 - -```sql -/* SQL编程 */ ------------------ ---// 局部变量 ---------- --- 变量声明 - declare var_name[,...] type [default value] - 这个语句被用来声明局部变量。要给变量提供一个默认值,请包含一个default子句。值可以被指定为一个表达式,不需要为一个常数。如果没有default子句,初始值为null。 --- 赋值 - 使用 set 和 select into 语句为变量赋值。 - - 注意:在函数内是可以使用全局变量(用户自定义的变量) ---// 全局变量 ---------- --- 定义、赋值 -set 语句可以定义并为变量赋值。 -set @var = value; -也可以使用select into语句为变量初始化并赋值。这样要求select语句只能返回一行,但是可以是多个字段,就意味着同时为多个变量进行赋值,变量的数量需要与查询的列数一致。 -还可以把赋值语句看作一个表达式,通过select执行完成。此时为了避免=被当作关系运算符看待,使用:=代替。(set语句可以使用= 和 :=)。 -select @var:=20; -select @v1:=id, @v2=name from t1 limit 1; -select * from tbl_name where @var:=30; -select into 可以将表中查询获得的数据赋给变量。 - -| select max(height) into @max_height from tb; --- 自定义变量名 -为了避免select语句中,用户自定义的变量与系统标识符(通常是字段名)冲突,用户自定义变量在变量名前使用@作为开始符号。 -@var=10; - - 变量被定义后,在整个会话周期都有效(登录到退出) ---// 控制结构 ---------- --- if语句 -if search_condition then - statement_list -[elseif search_condition then - statement_list] -... -[else - statement_list] -end if; --- case语句 -CASE value WHEN [compare-value] THEN result -[WHEN [compare-value] THEN result ...] -[ELSE result] -END --- while循环 -[begin_label:] while search_condition do - statement_list -end while [end_label]; -- 如果需要在循环内提前终止 while循环,则需要使用标签;标签需要成对出现。 - -- 退出循环 - 退出整个循环 leave - 退出当前循环 iterate - 通过退出的标签决定退出哪个循环 ---// 内置函数 ---------- --- 数值函数 -abs(x) -- 绝对值 abs(-10.9) = 10 -format(x, d) -- 格式化千分位数值 format(1234567.456, 2) = 1,234,567.46 -ceil(x) -- 向上取整 ceil(10.1) = 11 -floor(x) -- 向下取整 floor (10.1) = 10 -round(x) -- 四舍五入去整 -mod(m, n) -- m%n m mod n 求余 10%3=1 -pi() -- 获得圆周率 -pow(m, n) -- m^n -sqrt(x) -- 算术平方根 -rand() -- 随机数 -truncate(x, d) -- 截取d位小数 --- 时间日期函数 -now(), current_timestamp(); -- 当前日期时间 -current_date(); -- 当前日期 -current_time(); -- 当前时间 -date('yyyy-mm-dd hh:ii:ss'); -- 获取日期部分 -time('yyyy-mm-dd hh:ii:ss'); -- 获取时间部分 -date_format('yyyy-mm-dd hh:ii:ss', '%d %y %a %d %m %b %j'); -- 格式化时间 -unix_timestamp(); -- 获得unix时间戳 -from_unixtime(); -- 从时间戳获得时间 --- 字符串函数 -length(string) -- string长度,字节 -char_length(string) -- string的字符个数 -substring(str, position [,length]) -- 从str的position开始,取length个字符 -replace(str ,search_str ,replace_str) -- 在str中用replace_str替换search_str -instr(string ,substring) -- 返回substring首次在string中出现的位置 -concat(string [,...]) -- 连接字串 -charset(str) -- 返回字串字符集 -lcase(string) -- 转换成小写 -left(string, length) -- 从string2中的左边起取length个字符 -load_file(file_name) -- 从文件读取内容 -locate(substring, string [,start_position]) -- 同instr,但可指定开始位置 -lpad(string, length, pad) -- 重复用pad加在string开头,直到字串长度为length -ltrim(string) -- 去除前端空格 -repeat(string, count) -- 重复count次 -rpad(string, length, pad) --在str后用pad补充,直到长度为length -rtrim(string) -- 去除后端空格 -strcmp(string1 ,string2) -- 逐字符比较两字串大小 --- 流程函数 -case when [condition] then result [when [condition] then result ...] [else result] end 多分支 -if(expr1,expr2,expr3) 双分支。 --- 聚合函数 -count() -sum(); -max(); -min(); -avg(); -group_concat() --- 其他常用函数 -md5(); -default(); ---// 存储函数,自定义函数 ---------- --- 新建 - CREATE FUNCTION function_name (参数列表) RETURNS 返回值类型 - 函数体 - - 函数名,应该合法的标识符,并且不应该与已有的关键字冲突。 - - 一个函数应该属于某个数据库,可以使用db_name.function_name的形式执行当前函数所属数据库,否则为当前数据库。 - - 参数部分,由"参数名"和"参数类型"组成。多个参数用逗号隔开。 - - 函数体由多条可用的mysql语句,流程控制,变量声明等语句构成。 - - 多条语句应该使用 begin...end 语句块包含。 - - 一定要有 return 返回值语句。 --- 删除 - DROP FUNCTION [IF EXISTS] function_name; --- 查看 - SHOW FUNCTION STATUS LIKE 'partten' - SHOW CREATE FUNCTION function_name; --- 修改 - ALTER FUNCTION function_name 函数选项 ---// 存储过程,自定义功能 ---------- --- 定义 -存储存储过程 是一段代码(过程),存储在数据库中的sql组成。 -一个存储过程通常用于完成一段业务逻辑,例如报名,交班费,订单入库等。 -而一个函数通常专注与某个功能,视为其他程序服务的,需要在其他语句中调用函数才可以,而存储过程不能被其他调用,是自己执行 通过call执行。 --- 创建 -CREATE PROCEDURE sp_name (参数列表) - 过程体 -参数列表:不同于函数的参数列表,需要指明参数类型 -IN,表示输入型 -OUT,表示输出型 -INOUT,表示混合型 -注意,没有返回值。 -``` - -### 存储过程 - -```sql -/* 存储过程 */ ------------------ -存储过程是一段可执行性代码的集合。相比函数,更偏向于业务逻辑。 -调用:CALL 过程名 --- 注意 -- 没有返回值。 -- 只能单独调用,不可夹杂在其他语句中 --- 参数 -IN|OUT|INOUT 参数名 数据类型 -IN 输入:在调用过程中,将数据输入到过程体内部的参数 -OUT 输出:在调用过程中,将过程体处理完的结果返回到客户端 -INOUT 输入输出:既可输入,也可输出 --- 语法 -CREATE PROCEDURE 过程名 (参数列表) -BEGIN - 过程体 -END -``` - -### 用户和权限管理 - -```sql -/* 用户和权限管理 */ ------------------ --- root密码重置 -1. 停止MySQL服务 -2. [Linux] /usr/local/mysql/bin/safe_mysqld --skip-grant-tables & - [Windows] mysqld --skip-grant-tables -3. use mysql; -4. UPDATE `user` SET PASSWORD=PASSWORD("密码") WHERE `user` = "root"; -5. FLUSH PRIVILEGES; -用户信息表:mysql.user --- 刷新权限 -FLUSH PRIVILEGES; --- 增加用户 -CREATE USER 用户名 IDENTIFIED BY [PASSWORD] 密码(字符串) - - 必须拥有mysql数据库的全局CREATE USER权限,或拥有INSERT权限。 - - 只能创建用户,不能赋予权限。 - - 用户名,注意引号:如 'user_name'@'192.168.1.1' - - 密码也需引号,纯数字密码也要加引号 - - 要在纯文本中指定密码,需忽略PASSWORD关键词。要把密码指定为由PASSWORD()函数返回的混编值,需包含关键字PASSWORD --- 重命名用户 -RENAME USER old_user TO new_user --- 设置密码 -SET PASSWORD = PASSWORD('密码') -- 为当前用户设置密码 -SET PASSWORD FOR 用户名 = PASSWORD('密码') -- 为指定用户设置密码 --- 删除用户 -DROP USER 用户名 --- 分配权限/添加用户 -GRANT 权限列表 ON 表名 TO 用户名 [IDENTIFIED BY [PASSWORD] 'password'] - - all privileges 表示所有权限 - - *.* 表示所有库的所有表 - - 库名.表名 表示某库下面的某表 - GRANT ALL PRIVILEGES ON `pms`.* TO 'pms'@'%' IDENTIFIED BY 'pms0817'; --- 查看权限 -SHOW GRANTS FOR 用户名 - -- 查看当前用户权限 - SHOW GRANTS; 或 SHOW GRANTS FOR CURRENT_USER; 或 SHOW GRANTS FOR CURRENT_USER(); --- 撤消权限 -REVOKE 权限列表 ON 表名 FROM 用户名 -REVOKE ALL PRIVILEGES, GRANT OPTION FROM 用户名 -- 撤销所有权限 --- 权限层级 --- 要使用GRANT或REVOKE,您必须拥有GRANT OPTION权限,并且您必须用于您正在授予或撤销的权限。 -全局层级:全局权限适用于一个给定服务器中的所有数据库,mysql.user - GRANT ALL ON *.*和 REVOKE ALL ON *.*只授予和撤销全局权限。 -数据库层级:数据库权限适用于一个给定数据库中的所有目标,mysql.db, mysql.host - GRANT ALL ON db_name.*和REVOKE ALL ON db_name.*只授予和撤销数据库权限。 -表层级:表权限适用于一个给定表中的所有列,mysql.talbes_priv - GRANT ALL ON db_name.tbl_name和REVOKE ALL ON db_name.tbl_name只授予和撤销表权限。 -列层级:列权限适用于一个给定表中的单一列,mysql.columns_priv - 当使用REVOKE时,您必须指定与被授权列相同的列。 --- 权限列表 -ALL [PRIVILEGES] -- 设置除GRANT OPTION之外的所有简单权限 -ALTER -- 允许使用ALTER TABLE -ALTER ROUTINE -- 更改或取消已存储的子程序 -CREATE -- 允许使用CREATE TABLE -CREATE ROUTINE -- 创建已存储的子程序 -CREATE TEMPORARY TABLES -- 允许使用CREATE TEMPORARY TABLE -CREATE USER -- 允许使用CREATE USER, DROP USER, RENAME USER和REVOKE ALL PRIVILEGES。 -CREATE VIEW -- 允许使用CREATE VIEW -DELETE -- 允许使用DELETE -DROP -- 允许使用DROP TABLE -EXECUTE -- 允许用户运行已存储的子程序 -FILE -- 允许使用SELECT...INTO OUTFILE和LOAD DATA INFILE -INDEX -- 允许使用CREATE INDEX和DROP INDEX -INSERT -- 允许使用INSERT -LOCK TABLES -- 允许对您拥有SELECT权限的表使用LOCK TABLES -PROCESS -- 允许使用SHOW FULL PROCESSLIST -REFERENCES -- 未被实施 -RELOAD -- 允许使用FLUSH -REPLICATION CLIENT -- 允许用户询问从属服务器或主服务器的地址 -REPLICATION SLAVE -- 用于复制型从属服务器(从主服务器中读取二进制日志事件) -SELECT -- 允许使用SELECT -SHOW DATABASES -- 显示所有数据库 -SHOW VIEW -- 允许使用SHOW CREATE VIEW -SHUTDOWN -- 允许使用mysqladmin shutdown -SUPER -- 允许使用CHANGE MASTER, KILL, PURGE MASTER LOGS和SET GLOBAL语句,mysqladmin debug命令;允许您连接(一次),即使已达到max_connections。 -UPDATE -- 允许使用UPDATE -USAGE -- “无权限”的同义词 -GRANT OPTION -- 允许授予权限 -``` - -### 表维护 - -```sql -/* 表维护 */ --- 分析和存储表的关键字分布 -ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE 表名 ... --- 检查一个或多个表是否有错误 -CHECK TABLE tbl_name [, tbl_name] ... [option] ... -option = {QUICK | FAST | MEDIUM | EXTENDED | CHANGED} --- 整理数据文件的碎片 -OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... -``` - -### 杂项 - -```sql -/* 杂项 */ ------------------ -1. 可用反引号(`)为标识符(库名、表名、字段名、索引、别名)包裹,以避免与关键字重名!中文也可以作为标识符! -2. 每个库目录存在一个保存当前数据库的选项文件db.opt。 -3. 注释: - 单行注释 # 注释内容 - 多行注释 /* 注释内容 */ - 单行注释 -- 注释内容 (标准SQL注释风格,要求双破折号后加一空格符(空格、TAB、换行等)) -4. 模式通配符: - _ 任意单个字符 - % 任意多个字符,甚至包括零字符 - 单引号需要进行转义 \' -5. CMD命令行内的语句结束符可以为 ";", "\G", "\g",仅影响显示结果。其他地方还是用分号结束。delimiter 可修改当前对话的语句结束符。 -6. SQL对大小写不敏感 -7. 清除已有语句:\c -``` - - +-- Modify table + -- Modify table options + ALTER TABLE table_name table options + eg: ALTER TABLE table_name ENGINE=MYISAM; + -- Rename table + RENAME TABLE old_table_name TO new_table_name + RENAME TABLE old_table_name TO database_name.table_name (can move the table to another database) + -- RENAME can swap two table names + -- Modify table field structure (13.1.2. ALTER TABLE syntax) + ALTER TABLE table_name operation_name + -- Operation names + ADD [COLUMN] field definition -- Add field + AFTER field_name -- Indicates adding after this field name + FIRST -- Indicates adding at the first position + ADD PRIMARY KEY (field_name) -- Create primary key + ADD UNIQUE [index_name](field_name) -- Create unique index + ADD INDEX [index_name](field_name) -- Create normal index + DROP [COLUMN] field_name -- Delete field + MODIFY [COLUMN] field_name field attributes -- Supports modifying field attributes, cannot modify field name (all original attributes must also be written) + CHANGE [COLUMN] old_field_name new_field_name field attributes \ No newline at end of file diff --git a/docs/database/mysql/how-sql-executed-in-mysql.md b/docs/database/mysql/how-sql-executed-in-mysql.md index 0b01d9a4da3..a0d06ea049f 100644 --- a/docs/database/mysql/how-sql-executed-in-mysql.md +++ b/docs/database/mysql/how-sql-executed-in-mysql.md @@ -1,137 +1,140 @@ --- -title: SQL语句在MySQL中的执行过程 -category: 数据库 +title: The Execution Process of SQL Statements in MySQL +category: Database tag: - MySQL --- -> 本文来自[木木匠](https://github.com/kinglaw1204)投稿。 +> This article is contributed by [木木匠](https://github.com/kinglaw1204). -本篇文章会分析下一个 SQL 语句在 MySQL 中的执行流程,包括 SQL 的查询在 MySQL 内部会怎么流转,SQL 语句的更新是怎么完成的。 +This article will analyze the execution process of an SQL statement in MySQL, including how SQL queries flow internally in MySQL and how SQL statement updates are completed. -在分析之前我会先带着你看看 MySQL 的基础架构,知道了 MySQL 由那些组件组成以及这些组件的作用是什么,可以帮助我们理解和解决这些问题。 +Before the analysis, I will first guide you through the basic architecture of MySQL. Understanding what components make up MySQL and what roles these components play can help us comprehend and solve these issues. -## 一 MySQL 基础架构分析 +## I. Analysis of MySQL Architecture -### 1.1 MySQL 基本架构概览 +### 1.1 Overview of MySQL Architecture -下图是 MySQL 的一个简要架构图,从下图你可以很清晰的看到用户的 SQL 语句在 MySQL 内部是如何执行的。 +The diagram below provides a brief overview of the architecture of MySQL, from which you can clearly see how user SQL statements are executed internally in MySQL. -先简单介绍一下下图涉及的一些组件的基本作用帮助大家理解这幅图,在 1.2 节中会详细介绍到这些组件的作用。 +Let's briefly introduce some of the components involved in the diagram to help everyone understand it. Section 1.2 will provide a detailed explanation of these components' roles. -- **连接器:** 身份认证和权限相关(登录 MySQL 的时候)。 -- **查询缓存:** 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。 -- **分析器:** 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。 -- **优化器:** 按照 MySQL 认为最优的方案去执行。 -- **执行器:** 执行语句,然后从存储引擎返回数据。 - +- **Connector:** Related to authentication and permissions (for logging into MySQL). +- **Query Cache:** When executing a query statement, it first checks the cache (removed in MySQL 8.0 as this feature was not very practical). +- **Parser:** If the cache is not hit, the SQL statement goes through the parser, which means it first checks what your SQL statement is intended to do and then checks whether the SQL syntax is correct. +- **Optimizer:** Executes according to what MySQL deems the optimal plan. +- **Executor:** Executes the statement and then fetches the data from the storage engine. ![](https://oss.javaguide.cn/javaguide/13526879-3037b144ed09eb88.png) -简单来说 MySQL 主要分为 Server 层和存储引擎层: +In simple terms, MySQL is mainly divided into two layers: Server layer and storage engine layer. -- **Server 层**:主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binlog 日志模块。 -- **存储引擎**:主要负责数据的存储和读取,采用可以替换的插件式架构,支持 InnoDB、MyISAM、Memory 等多个存储引擎,其中 InnoDB 引擎有自有的日志模块 redolog 模块。**现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5 版本开始就被当做默认存储引擎了。** +- **Server Layer:** Mainly includes the connector, query cache, parser, optimizer, executor, etc. All cross-storage-engine functionalities are implemented at this layer, such as stored procedures, triggers, views, functions, etc., along with a general logging module, the binlog logging module. +- **Storage Engine:** Mainly responsible for data storage and retrieval, using a replaceable plugin architecture that supports multiple storage engines like InnoDB, MyISAM, Memory, etc. Among these, the InnoDB engine has its own logging module, the redolog module. **The most commonly used storage engine now is InnoDB, which has been the default storage engine since MySQL 5.5.** -### 1.2 Server 层基本组件介绍 +### 1.2 Introduction of Basic Components in the Server Layer -#### 1) 连接器 +#### 1) Connector -连接器主要和身份认证和权限相关的功能相关,就好比一个级别很高的门卫一样。 +The connector is mainly related to functions associated with authentication and permissions, much like a high-level gatekeeper. -主要负责用户登录数据库,进行用户的身份认证,包括校验账户密码,权限等操作,如果用户账户密码已通过,连接器会到权限表中查询该用户的所有权限,之后在这个连接里的权限逻辑判断都是会依赖此时读取到的权限数据,也就是说,后续只要这个连接不断开,即使管理员修改了该用户的权限,该用户也是不受影响的。 +It is primarily responsible for user login to the database, performing user identity verification, including account password validation, permissions, etc. If the user's account password is validated, the connector will query the user's permissions from the permissions table. Subsequently, permission logic judgments in this connection will depend on the permissions data read at that time. This means that as long as the connection remains open, even if the administrator modifies the user's permissions, the user will not be affected. -#### 2) 查询缓存(MySQL 8.0 版本后移除) +#### 2) Query Cache (removed in MySQL 8.0) -查询缓存主要用来缓存我们所执行的 SELECT 语句以及该语句的结果集。 +The query cache is mainly used to cache the SELECT statements we execute along with their result sets. -连接建立后,执行查询语句的时候,会先查询缓存,MySQL 会先校验这个 SQL 是否执行过,以 Key-Value 的形式缓存在内存中,Key 是查询语句,Value 是结果集。如果缓存 key 被命中,就会直接返回给客户端,如果没有命中,就会执行后续的操作,完成后也会把结果缓存起来,方便下一次调用。当然在真正执行缓存查询的时候还是会校验用户的权限,是否有该表的查询条件。 +After establishing a connection, when executing a query statement, the system first checks the cache. MySQL will verify whether this SQL has been executed before and caches it in memory in a Key-Value format, where Key is the query statement and Value is the result set. If the cache key is hit, the result will be returned directly to the client; if not, it will execute the following operations and cache the results for the next call. However, during the actual execution of the cached query, user permissions will still be checked to verify whether the query conditions on the table are valid. -MySQL 查询不建议使用缓存,因为查询缓存失效在实际业务场景中可能会非常频繁,假如你对一个表更新的话,这个表上的所有的查询缓存都会被清空。对于不经常更新的数据来说,使用缓存还是可以的。 +Using caching for MySQL queries is generally not recommended because query cache invalidation can happen very frequently in real-world business scenarios. For instance, if you update a table, all cached queries on that table will be cleared. For data that does not change frequently, using caching may still be beneficial. -所以,一般在大多数情况下我们都是不推荐去使用查询缓存的。 +Therefore, in most situations, we do not recommend using query caching. -MySQL 8.0 版本后删除了缓存的功能,官方也是认为该功能在实际的应用场景比较少,所以干脆直接删掉了。 +The query cache feature was removed in MySQL 8.0 since the official opinion was that its application scenarios were quite limited. -#### 3) 分析器 +#### 3) Parser -MySQL 没有命中缓存,那么就会进入分析器,分析器主要是用来分析 SQL 语句是来干嘛的,分析器也会分为几步: +If MySQL does not hit the cache, it will proceed to the parser. The parser is mainly used to analyze what the SQL statement is intended to do and is divided into several steps: -**第一步,词法分析**,一条 SQL 语句有多个字符串组成,首先要提取关键字,比如 select,提出查询的表,提出字段名,提出查询条件等等。做完这些操作后,就会进入第二步。 +**Step 1: Lexical Analysis.** An SQL statement consists of multiple strings, and the first step is to extract keywords, such as SELECT, identify the queried table, fields, and query conditions, etc. After completing these operations, it will proceed to the second step. -**第二步,语法分析**,主要就是判断你输入的 SQL 是否正确,是否符合 MySQL 的语法。 +**Step 2: Syntax Analysis.** This mainly determines whether the entered SQL is correct and conforms to MySQL's syntax. -完成这 2 步之后,MySQL 就准备开始执行了,但是如何执行,怎么执行是最好的结果呢?这个时候就需要优化器上场了。 +After completing these two steps, MySQL prepares to execute the statement, but how to execute it optimally is where the optimizer comes in. -#### 4) 优化器 +#### 4) Optimizer -优化器的作用就是它认为的最优的执行方案去执行(有时候可能也不是最优,这篇文章涉及对这部分知识的深入讲解),比如多个索引的时候该如何选择索引,多表查询的时候如何选择关联顺序等。 +The optimizer's role is to execute according to what it considers the optimal execution plan (sometimes it may not be the best; this article delves into this knowledge). For example, choices regarding indexing when multiple indexes exist or determining the join order when multiple tables are queried. -可以说,经过了优化器之后可以说这个语句具体该如何执行就已经定下来。 +After passing through the optimizer, the specifics of how the statement will be executed are determined. -#### 5) 执行器 +#### 5) Executor -当选择了执行方案后,MySQL 就准备开始执行了,首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会去调用引擎的接口,返回接口执行的结果。 +Once the execution plan is selected, MySQL is ready to start executing. Before execution, it will check whether the user has the necessary permissions. If permission is lacking, an error message will be returned; if the permission exists, it will call the engine's interface to return the execution result. -## 二 语句分析 +## II. Statement Analysis -### 2.1 查询语句 +### 2.1 Query Statements -说了以上这么多,那么究竟一条 SQL 语句是如何执行的呢?其实我们的 SQL 可以分为两种,一种是查询,一种是更新(增加,修改,删除)。我们先分析下查询语句,语句如下: +After discussing the above, how exactly is an SQL statement executed? Our SQL can be divided into two types: queries and updates (addition, modification, deletion). Let’s analyze the execution of a query statement, as follows: ```sql -select * from tb_student A where A.age='18' and A.name=' 张三 '; +SELECT * FROM tb_student A WHERE A.age = '18' AND A.name = '张三'; ``` -结合上面的说明,我们分析下这个语句的执行流程: +Combining the previous explanation, let’s analyze the execution flow of this statement: -- 先检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在 MySQL8.0 版本以前,会先查询缓存,以这条 SQL 语句为 key 在内存中查询是否有结果,如果有直接缓存,如果没有,执行下一步。 -- 通过分析器进行词法分析,提取 SQL 语句的关键元素,比如提取上面这个语句是查询 select,提取需要查询的表名为 tb_student,需要查询所有的列,查询条件是这个表的 id='1'。然后判断这个 SQL 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。 -- 接下来就是优化器进行确定执行方案,上面的 SQL 语句,可以有两种执行方案:a.先查询学生表中姓名为“张三”的学生,然后判断是否年龄是 18。b.先找出学生中年龄 18 岁的学生,然后再查询姓名为“张三”的学生。那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。 +- First, check if the statement has permission. If not, return an error message immediately; if there is permission, in MySQL 8.0 and earlier versions, it will first check the query cache using this SQL statement as the key to see if there is a result in memory. If found, it will return the cache; if not, it will execute the next step. -- 进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎的执行结果。 +- The parser will perform lexical analysis, extracting the critical elements of the SQL statement, such as determining that this statement involves querying (SELECT), identifying the table name is tb_student, querying all columns, and establishing that the query condition is id = '1'. Then, it checks for potential syntax errors, for instance, whether the keywords are correct, etc. If everything is fine, it proceeds to the next step. -### 2.2 更新语句 +- Next, the optimizer will determine the execution plan. The above SQL statement can have two execution plans: a. First, query students with the name "张三," and then check if their age is 18. b. First, find students aged 18, then query for those named "张三." The optimizer will choose the most efficient plan based on its optimization algorithms (what it considers best may not always be the best). Once the execution plan is confirmed, it is ready to execute. -以上就是一条查询 SQL 的执行流程,那么接下来我们看看一条更新语句如何执行的呢?SQL 语句如下: +- Conduct permission checks; if not permitted, return an error message. If permission is granted, it will call the database engine interface and return the execution results from the engine. -```plain -update tb_student A set A.age='19' where A.name=' 张三 '; +### 2.2 Update Statements + +Having covered the execution process for a query SQL, let’s now see how an update statement is executed. The SQL statement is as follows: + +```sql +UPDATE tb_student A SET A.age = '19' WHERE A.name = '张三'; ``` -我们来给张三修改下年龄,在实际数据库肯定不会设置年龄这个字段的,不然要被技术负责人打的。其实这条语句也基本上会沿着上一个查询的流程走,只不过执行更新的时候肯定要记录日志啦,这就会引入日志模块了,MySQL 自带的日志模块是 **binlog(归档日志)** ,所有的存储引擎都可以使用,我们常用的 InnoDB 引擎还自带了一个日志模块 **redo log(重做日志)**,我们就以 InnoDB 模式下来探讨这个语句的执行流程。流程如下: +We are updating the age for 张三 (Zhang San). In actual databases, you definitely wouldn't set the age field like this, or the technical lead would get mad. This statement will basically follow the execution process referenced in the previous query, but while executing the update, logging is required, which introduces the logging module. MySQL's built-in logging module is **binlog (binary log)**, which can be used by all storage engines. The commonly used InnoDB engine also has its own logging module, the **redo log**. Let’s explore the execution process of this statement using the InnoDB mode. The process is as follows: + +- First, locate the data for 张三; the query cache will not be used because the update statement will invalidate any associated query caches. +- Next, retrieve the query statement, change age to 19, call the engine API interface, and write this row of data. The InnoDB engine stores the data in memory while also recording a redo log. At this point, the redo log enters the prepare state, then informs the executor that the execution is complete and can be submitted at any time. +- Upon receiving the notification, the executor records the binlog and then calls the engine interface to change the redo log to the committed state. +- The update is complete. -- 先查询到张三这一条数据,不会走查询缓存,因为更新语句会导致与该表相关的查询缓存失效。 -- 然后拿到查询的语句,把 age 改为 19,然后调用引擎 API 接口,写入这一行数据,InnoDB 引擎把数据保存在内存中,同时记录 redo log,此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交。 -- 执行器收到通知后记录 binlog,然后调用引擎接口,提交 redo log 为提交状态。 -- 更新完成。 +**Some may wonder, why use two logging modules—can't just one suffice?** -**这里肯定有同学会问,为什么要用两个日志模块,用一个日志模块不行吗?** +This is because MySQL initially did not have the InnoDB engine (which was introduced as a plugin by another company). The default engine was MyISAM. However, since we know the redo log is unique to the InnoDB engine and other storage engines do not possess it, this results in a lack of crash-safe capability (crash-safe capability ensures that even if the database has an unexpected restart, previously committed records will not be lost). The binlog is only used for archiving. -这是因为最开始 MySQL 并没有 InnoDB 引擎(InnoDB 引擎是其他公司以插件形式插入 MySQL 的),MySQL 自带的引擎是 MyISAM,但是我们知道 redo log 是 InnoDB 引擎特有的,其他存储引擎都没有,这就导致会没有 crash-safe 的能力(crash-safe 的能力即使数据库发生异常重启,之前提交的记录都不会丢失),binlog 日志只能用来归档。 +Using just one logging module is not prohibited; it's just that the InnoDB engine relies on the redo log to support transactions. Then, some may ask, while using two logging modules, wouldn’t that be overly complex? Why does the redo log introduce a prepare pre-commit state? Let's explain why this is necessary using a proof by contradiction: -并不是说只用一个日志模块不可以,只是 InnoDB 引擎就是通过 redo log 来支持事务的。那么,又会有同学问,我用两个日志模块,但是不要这么复杂行不行,为什么 redo log 要引入 prepare 预提交状态?这里我们用反证法来说明下为什么要这么做? +- **If we write the redo log first and commit, then write the binlog:** Suppose we finish writing the redo log, the machine crashes, and the binlog is not written. After rebooting, the machine will recover data through the redo log, but it won’t have a record in binlog, leading to data loss during subsequent backups and problems with master-slave synchronization. +- **If we write the binlog first and then the redo log:** Suppose we finish writing the binlog, and then the machine restarts abnormally. Since there is no redo log, this machine cannot recover that record, but the binlog has a record of it, leading to inconsistencies in data. -- **先写 redo log 直接提交,然后写 binlog**,假设写完 redo log 后,机器挂了,binlog 日志没有被写入,那么机器重启后,这台机器会通过 redo log 恢复数据,但是这个时候 binlog 并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。 -- **先写 binlog,然后写 redo log**,假设写完了 binlog,机器异常重启了,由于没有 redo log,本机是无法恢复这一条记录的,但是 binlog 又有记录,那么和上面同样的道理,就会产生数据不一致的情况。 +If we adopt a two-phase commit mechanism for the redo log, it would prevent the aforementioned issues by writing the binlog first and then committing the redo log, thus ensuring data consistency. Now the question arises: is there an extreme case? Suppose the redo log is in a prepare state, and the binlog has already been written—what happens if an unexpected restart occurs? -如果采用 redo log 两阶段提交的方式就不一样了,写完 binlog 后,然后再提交 redo log 就会防止出现上述的问题,从而保证了数据的一致性。那么问题来了,有没有一个极端的情况呢?假设 redo log 处于预提交状态,binlog 也已经写完了,这个时候发生了异常重启会怎么样呢? -这个就要依赖于 MySQL 的处理机制了,MySQL 的处理过程如下: +This depends on MySQL's handling mechanism, which is as follows: -- 判断 redo log 是否完整,如果判断是完整的,就立即提交。 -- 如果 redo log 只是预提交但不是 commit 状态,这个时候就会去判断 binlog 是否完整,如果完整就提交 redo log, 不完整就回滚事务。 +- Determine if the redo log is complete; if it is complete, it will commit immediately. +- If the redo log is only in the prepare state but not committed, it will verify the completeness of the binlog. If the binlog is complete, it commits the redo log; if not, it rolls back the transaction. -这样就解决了数据一致性的问题。 +This approach solves the issue of data consistency. -## 三 总结 +## III. Summary -- MySQL 主要分为 Server 层和引擎层,Server 层主要包括连接器、查询缓存、分析器、优化器、执行器,同时还有一个日志模块(binlog),这个日志模块所有执行引擎都可以共用,redolog 只有 InnoDB 有。 -- 引擎层是插件式的,目前主要包括,MyISAM,InnoDB,Memory 等。 -- 查询语句的执行流程如下:权限校验(如果命中缓存)--->查询缓存--->分析器--->优化器--->权限校验--->执行器--->引擎 -- 更新语句执行流程如下:分析器---->权限校验---->执行器--->引擎---redo log(prepare 状态)--->binlog--->redo log(commit 状态) +- MySQL is primarily divided into the Server layer and the engine layer. The Server layer mainly includes the connector, query cache, parser, optimizer, executor, along with a logging module (binlog), which can be shared by all execution engines, and the redolog, which is exclusive to InnoDB. +- The engine layer is plugin-based, currently including MyISAM, InnoDB, Memory, etc. +- The execution flow of a query statement is as follows: permission check (if cache hit) ---> query cache ---> parser ---> optimizer ---> permission check ---> executor ---> engine +- The execution flow of an update statement is as follows: parser ---> permission check ---> executor ---> engine ---> redo log (prepare state) ---> binlog ---> redo log (commit state) -## 四 参考 +## IV. References -- 《MySQL 实战 45 讲》 -- MySQL 5.6 参考手册: +- "MySQL Practical Experience: 45 Lessons" +- MySQL 5.6 Reference Manual: diff --git a/docs/database/mysql/index-invalidation-caused-by-implicit-conversion.md b/docs/database/mysql/index-invalidation-caused-by-implicit-conversion.md index 377460c66a6..11310caa865 100644 --- a/docs/database/mysql/index-invalidation-caused-by-implicit-conversion.md +++ b/docs/database/mysql/index-invalidation-caused-by-implicit-conversion.md @@ -1,30 +1,27 @@ --- -title: MySQL隐式转换造成索引失效 -category: 数据库 +title: MySQL Implicit Conversion Causing Index Invalidity +category: Database tag: - MySQL - - 性能优化 + - Performance Optimization --- -> 本次测试使用的 MySQL 版本是 `5.7.26`,随着 MySQL 版本的更新某些特性可能会发生改变,本文不代表所述观点和结论于 MySQL 所有版本均准确无误,版本差异请自行甄别。 +> The MySQL version used for this test is `5.7.26`. As MySQL versions are updated, certain features may change. This article does not represent that the views and conclusions stated are accurate for all versions of MySQL; please verify version differences on your own. > -> 原文: +> Original article: -## 前言 +## Introduction -数据库优化是一个任重而道远的任务,想要做优化必须深入理解数据库的各种特性。在开发过程中我们经常会遇到一些原因很简单但造成的后果却很严重的疑难杂症,这类问题往往还不容易定位,排查费时费力最后发现是一个很小的疏忽造成的,又或者是因为不了解某个技术特性产生的。 +Database optimization is a long-term and challenging task. To achieve optimization, one must deeply understand various features of the database. During development, we often encounter seemingly simple issues that lead to serious consequences. These problems are often difficult to locate, requiring time and effort to troubleshoot, only to find that they stem from a small oversight or a lack of understanding of a specific technical feature. -于数据库层面,最常见的恐怕就是索引失效了,且一开始因为数据量小还不易被发现。但随着业务的拓展数据量的提升,性能问题慢慢的就体现出来了,处理不及时还很容易造成雪球效应,最终导致数据库卡死甚至瘫痪。造成索引失效的原因可能有很多种,相关技术博客已经有太多了,今天我要记录的是**隐式转换造成的索引失效**。 +At the database level, one of the most common issues is index invalidation, which may not be easily detected at first due to the small amount of data. However, as the business expands and the data volume increases, performance issues gradually become apparent. If not addressed in a timely manner, it can easily lead to a snowball effect, ultimately causing the database to freeze or even crash. There are many potential causes for index invalidation, and numerous technical blogs have covered this topic. Today, I want to document **index invalidation caused by implicit conversion**. -## 数据准备 +## Data Preparation -首先使用存储过程生成 1000 万条测试数据, -测试表一共建立了 7 个字段(包括主键),`num1`和`num2`保存的是和`ID`一样的顺序数字,其中`num2`是字符串类型。 -`type1`和`type2`保存的都是主键对 5 的取模,目的是模拟实际应用中常用类似 type 类型的数据,但是`type2`是没有建立索引的。 -`str1`和`str2`都是保存了一个 20 位长度的随机字符串,`str1`不能为`NULL`,`str2`允许为`NULL`,相应的生成测试数据的时候我也会在`str2`字段生产少量`NULL`值(每 100 条数据产生一个`NULL`值)。 +First, we will use a stored procedure to generate 10 million test data entries. The test table consists of 7 fields (including the primary key). `num1` and `num2` store sequential numbers identical to `ID`, with `num2` being of string type. `type1` and `type2` both store the modulus of the primary key with 5, simulating commonly used type-like data in actual applications, but `type2` does not have an index. `str1` and `str2` both store a random string of 20 characters in length, with `str1` not allowed to be `NULL` and `str2` allowed to be `NULL`. Accordingly, when generating test data, I will also produce a small number of `NULL` values in the `str2` field (one `NULL` value for every 100 entries). ```sql --- 创建测试数据表 +-- Create test data table DROP TABLE IF EXISTS test1; CREATE TABLE `test1` ( `id` int(11) NOT NULL, @@ -41,7 +38,7 @@ CREATE TABLE `test1` ( KEY `str1` (`str1`), KEY `str2` (`str2`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; --- 创建存储过程 +-- Create stored procedure DROP PROCEDURE IF EXISTS pre_test1; DELIMITER // CREATE PROCEDURE `pre_test1`() @@ -51,7 +48,7 @@ BEGIN WHILE i < 10000000 DO SET i = i + 1; SET @str1 = SUBSTRING(MD5(RAND()),1,20); - -- 每100条数据str2产生一个null值 + -- Generate a null value for str2 every 100 entries IF i % 100 = 0 THEN SET @str2 = NULL; ELSE @@ -61,102 +58,19 @@ BEGIN `type1`, `type2`, `str1`, `str2`) VALUES (CONCAT('', i), CONCAT('', i), CONCAT('', i), i%5, i%5, @str1, @str2); - -- 事务优化,每一万条数据提交一次事务 + -- Transaction optimization, commit every 10,000 entries IF i % 10000 = 0 THEN COMMIT; END IF; END WHILE; END; // DELIMITER ; --- 执行存储过程 +-- Execute stored procedure CALL pre_test1(); ``` -数据量比较大,还涉及使用`MD5`生成随机字符串,所以速度有点慢,稍安勿躁,耐心等待即可。 +The data volume is quite large, and since it involves using `MD5` to generate random strings, the speed is a bit slow. Please be patient and wait. -1000 万条数据,我用了 33 分钟才跑完(实际时间跟你电脑硬件配置有关)。这里贴几条生成的数据,大致长这样。 +It took me 33 minutes to generate 10 million entries (the actual time depends on your computer's hardware configuration). Here are a few examples of the generated data, which look like this. -![](https://oss.javaguide.cn/github/javaguide/mysqlindex-invalidation-caused-by-implicit-conversion-01.png) - -## SQL 测试 - -先来看这组 SQL,一共四条,我们的测试数据表`num1`是`int`类型,`num2`是`varchar`类型,但是存储的数据都是跟主键`id`一样的顺序数字,两个字段都建立有索引。 - -```sql -1: SELECT * FROM `test1` WHERE num1 = 10000; -2: SELECT * FROM `test1` WHERE num1 = '10000'; -3: SELECT * FROM `test1` WHERE num2 = 10000; -4: SELECT * FROM `test1` WHERE num2 = '10000'; -``` - -这四条 SQL 都是有针对性写的,12 查询的字段是 int 类型,34 查询的字段是`varchar`类型。12 或 34 查询的字段虽然都相同,但是一个条件是数字,一个条件是用引号引起来的字符串。这样做有什么区别呢?先不看下边的测试结果你能猜出这四条 SQL 的效率顺序吗? - -经测试这四条 SQL 最后的执行结果却相差很大,其中 124 三条 SQL 基本都是瞬间出结果,大概在 0.001~0.005 秒,在千万级的数据量下这样的结果可以判定这三条 SQL 性能基本没差别了。但是第三条 SQL,多次测试耗时基本在 4.5~4.8 秒之间。 - -为什么 34 两条 SQL 效率相差那么大,但是同样做对比的 12 两条 SQL 却没什么差别呢?查看一下执行计划,下边分别 1234 条 SQL 的执行计划数据: - -![](https://oss.javaguide.cn/github/javaguide/mysqlindex-invalidation-caused-by-implicit-conversion-02.png) - -可以看到,124 三条 SQL 都能使用到索引,连接类型都为`ref`,扫描行数都为 1,所以效率非常高。再看看第三条 SQL,没有用上索引,所以为全表扫描,`rows`直接到达 1000 万了,所以性能差别才那么大。 - -仔细观察你会发现,34 两条 SQL 查询的字段`num2`是`varchar`类型的,查询条件等号右边加引号的第 4 条 SQL 是用到索引的,那么是查询的数据类型和字段数据类型不一致造成的吗?如果是这样那 12 两条 SQL 查询的字段`num1`是`int`类型,但是第 2 条 SQL 查询条件右边加了引号为什么还能用上索引呢。 - -查阅 MySQL 相关文档发现是隐式转换造成的,看一下官方的描述: - -> 官方文档:[12.2 Type Conversion in Expression Evaluation](https://dev.mysql.com/doc/refman/5.7/en/type-conversion.html?spm=5176.100239.blogcont47339.5.1FTben) -> -> 当操作符与不同类型的操作数一起使用时,会发生类型转换以使操作数兼容。某些转换是隐式发生的。例如,MySQL 会根据需要自动将字符串转换为数字,反之亦然。以下规则描述了比较操作的转换方式: -> -> 1. 两个参数至少有一个是`NULL`时,比较的结果也是`NULL`,特殊的情况是使用`<=>`对两个`NULL`做比较时会返回`1`,这两种情况都不需要做类型转换 -> 2. 两个参数都是字符串,会按照字符串来比较,不做类型转换 -> 3. 两个参数都是整数,按照整数来比较,不做类型转换 -> 4. 十六进制的值和非数字做比较时,会被当做二进制串 -> 5. 有一个参数是`TIMESTAMP`或`DATETIME`,并且另外一个参数是常量,常量会被转换为`timestamp` -> 6. 有一个参数是`decimal`类型,如果另外一个参数是`decimal`或者整数,会将整数转换为`decimal`后进行比较,如果另外一个参数是浮点数,则会把`decimal`转换为浮点数进行比较 -> 7. **所有其他情况下,两个参数都会被转换为浮点数再进行比较** - -根据官方文档的描述,我们的第 23 两条 SQL 都发生了隐式转换,第 2 条 SQL 的查询条件`num1 = '10000'`,左边是`int`类型右边是字符串,第 3 条 SQL 相反,那么根据官方转换规则第 7 条,左右两边都会转换为浮点数再进行比较。 - -先看第 2 条 SQL:``SELECT * FROM `test1` WHERE num1 = '10000';`` **左边为 int 类型**`10000`,转换为浮点数还是`10000`,右边字符串类型`'10000'`,转换为浮点数也是`10000`。两边的转换结果都是唯一确定的,所以不影响使用索引。 - -第 3 条 SQL:``SELECT * FROM `test1` WHERE num2 = 10000;`` **左边是字符串类型**`'10000'`,转浮点数为 10000 是唯一的,右边`int`类型`10000`转换结果也是唯一的。但是,因为左边是检索条件,`'10000'`转到`10000`虽然是唯一,但是其他字符串也可以转换为`10000`,比如`'10000a'`,`'010000'`,`'10000'`等等都能转为浮点数`10000`,这样的情况下,是不能用到索引的。 - -关于这个**隐式转换**我们可以通过查询测试验证一下,先插入几条数据,其中`num2='10000a'`、`'010000'`和`'10000'`: - -```sql -INSERT INTO `test1` (`id`, `num1`, `num2`, `type1`, `type2`, `str1`, `str2`) VALUES ('10000001', '10000', '10000a', '0', '0', '2df3d9465ty2e4hd523', '2df3d9465ty2e4hd523'); -INSERT INTO `test1` (`id`, `num1`, `num2`, `type1`, `type2`, `str1`, `str2`) VALUES ('10000002', '10000', '010000', '0', '0', '2df3d9465ty2e4hd523', '2df3d9465ty2e4hd523'); -INSERT INTO `test1` (`id`, `num1`, `num2`, `type1`, `type2`, `str1`, `str2`) VALUES ('10000003', '10000', ' 10000', '0', '0', '2df3d9465ty2e4hd523', '2df3d9465ty2e4hd523'); -``` - -然后使用第三条 SQL 语句``SELECT * FROM `test1` WHERE num2 = 10000;``进行查询: - -![](https://oss.javaguide.cn/github/javaguide/mysqlindex-invalidation-caused-by-implicit-conversion-03.png) - -从结果可以看到,后面插入的三条数据也都匹配上了。那么这个字符串隐式转换的规则是什么呢?为什么`num2='10000a'`、`'010000'`和`'10000'`这三种情形都能匹配上呢?查阅相关资料发现规则如下: - -1. **不以数字开头**的字符串都将转换为`0`。如`'abc'`、`'a123bc'`、`'abc123'`都会转化为`0`; -2. **以数字开头的**字符串转换时会进行截取,从第一个字符截取到第一个非数字内容为止。比如`'123abc'`会转换为`123`,`'012abc'`会转换为`012`也就是`12`,`'5.3a66b78c'`会转换为`5.3`,其他同理。 - -现对以上规则做如下测试验证: - -![](https://oss.javaguide.cn/github/javaguide/mysqlindex-invalidation-caused-by-implicit-conversion-04.png) - -如此也就印证了之前的查询结果了。 - -再次写一条 SQL 查询 str1 字段:``SELECT * FROM `test1` WHERE str1 = 1234;`` - -![](https://oss.javaguide.cn/github/javaguide/mysqlindex-invalidation-caused-by-implicit-conversion-05.png) - -## 分析和总结 - -通过上面的测试我们发现 MySQL 使用操作符的一些特性: - -1. 当操作符**左右两边的数据类型不一致**时,会发生**隐式转换**。 -2. 当 where 查询操作符**左边为数值类型**时发生了隐式转换,那么对效率影响不大,但还是不推荐这么做。 -3. 当 where 查询操作符**左边为字符类型**时发生了隐式转换,那么会导致索引失效,造成全表扫描效率极低。 -4. 字符串转换为数值类型时,非数字开头的字符串会转化为`0`,以数字开头的字符串会截取从第一个字符到第一个非数字内容为止的值为转化结果。 - -所以,我们在写 SQL 时一定要养成良好的习惯,查询的字段是什么类型,等号右边的条件就写成对应的类型。特别当查询的字段是字符串时,等号右边的条件一定要用引号引起来标明这是一个字符串,否则会造成索引失效触发全表扫描。 - - +!\[\](https://oss.javaguide.cn/github/javaguide diff --git a/docs/database/mysql/innodb-implementation-of-mvcc.md b/docs/database/mysql/innodb-implementation-of-mvcc.md index a2e19998d71..48ac7df06d2 100644 --- a/docs/database/mysql/innodb-implementation-of-mvcc.md +++ b/docs/database/mysql/innodb-implementation-of-mvcc.md @@ -1,259 +1,60 @@ --- -title: InnoDB存储引擎对MVCC的实现 -category: 数据库 +title: Implementation of MVCC in InnoDB Storage Engine +category: Database tag: - MySQL --- -## 多版本并发控制 (Multi-Version Concurrency Control) +## Multi-Version Concurrency Control (MVCC) -MVCC 是一种并发控制机制,用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性。它是通过在每个数据行上维护多个版本的数据来实现的。当一个事务要对数据库中的数据进行修改时,MVCC 会为该事务创建一个数据快照,而不是直接修改实际的数据行。 +MVCC is a concurrency control mechanism used to maintain data consistency and isolation when multiple concurrent transactions read and write to the database simultaneously. It achieves this by maintaining multiple versions of data for each data row. When a transaction needs to modify data in the database, MVCC creates a data snapshot for that transaction instead of directly modifying the actual data row. -1、读操作(SELECT): +1. Read Operations (SELECT): -当一个事务执行读操作时,它会使用快照读取。快照读取是基于事务开始时数据库中的状态创建的,因此事务不会读取其他事务尚未提交的修改。具体工作情况如下: +When a transaction performs a read operation, it uses snapshot reading. Snapshot reading is based on the state of the database at the time the transaction started, so the transaction does not read modifications made by other transactions that have not yet been committed. The specific workings are as follows: -- 对于读取操作,事务会查找符合条件的数据行,并选择符合其事务开始时间的数据版本进行读取。 -- 如果某个数据行有多个版本,事务会选择不晚于其开始时间的最新版本,确保事务只读取在它开始之前已经存在的数据。 -- 事务读取的是快照数据,因此其他并发事务对数据行的修改不会影响当前事务的读取操作。 +- For read operations, the transaction looks for data rows that meet the criteria and selects the version of the data that corresponds to its transaction start time for reading. +- If a data row has multiple versions, the transaction selects the latest version that is not later than its start time, ensuring that the transaction only reads data that existed before it started. +- The transaction reads snapshot data, so modifications made by other concurrent transactions to the data row do not affect the current transaction's read operation. -2、写操作(INSERT、UPDATE、DELETE): +2. Write Operations (INSERT, UPDATE, DELETE): -当一个事务执行写操作时,它会生成一个新的数据版本,并将修改后的数据写入数据库。具体工作情况如下: +When a transaction performs a write operation, it generates a new data version and writes the modified data to the database. The specific workings are as follows: -- 对于写操作,事务会为要修改的数据行创建一个新的版本,并将修改后的数据写入新版本。 -- 新版本的数据会带有当前事务的版本号,以便其他事务能够正确读取相应版本的数据。 -- 原始版本的数据仍然存在,供其他事务使用快照读取,这保证了其他事务不受当前事务的写操作影响。 +- For write operations, the transaction creates a new version for the data row to be modified and writes the modified data into the new version. +- The new version of the data carries the version number of the current transaction so that other transactions can correctly read the corresponding version of the data. +- The original version of the data still exists for other transactions to use snapshot reading, ensuring that other transactions are not affected by the write operations of the current transaction. -3、事务提交和回滚: +3. Transaction Commit and Rollback: -- 当一个事务提交时,它所做的修改将成为数据库的最新版本,并且对其他事务可见。 -- 当一个事务回滚时,它所做的修改将被撤销,对其他事务不可见。 +- When a transaction commits, the modifications it made become the latest version of the database and are visible to other transactions. +- When a transaction rolls back, the modifications it made are undone and are not visible to other transactions. -4、版本的回收: +4. Version Cleanup: -为了防止数据库中的版本无限增长,MVCC 会定期进行版本的回收。回收机制会删除已经不再需要的旧版本数据,从而释放空间。 +To prevent the versions in the database from growing indefinitely, MVCC periodically performs version cleanup. The cleanup mechanism deletes old version data that is no longer needed, thereby freeing up space. -MVCC 通过创建数据的多个版本和使用快照读取来实现并发控制。读操作使用旧版本数据的快照,写操作创建新版本,并确保原始版本仍然可用。这样,不同的事务可以在一定程度上并发执行,而不会相互干扰,从而提高了数据库的并发性能和数据一致性。 +MVCC achieves concurrency control by creating multiple versions of data and using snapshot reading. Read operations use snapshots of old version data, write operations create new versions, and ensure that the original versions remain available. This allows different transactions to execute concurrently to a certain extent without interfering with each other, thereby improving the concurrency performance and data consistency of the database. -## 一致性非锁定读和锁定读 +## Consistent Nonlocking Reads and Locking Reads -### 一致性非锁定读 +### Consistent Nonlocking Reads -对于 [**一致性非锁定读(Consistent Nonlocking Reads)**](https://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html)的实现,通常做法是加一个版本号或者时间戳字段,在更新数据的同时版本号 + 1 或者更新时间戳。查询时,将当前可见的版本号与对应记录的版本号进行比对,如果记录的版本小于可见版本,则表示该记录可见 +For [**Consistent Nonlocking Reads**](https://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html), the typical approach is to add a version number or timestamp field, incrementing the version number by 1 or updating the timestamp when data is modified. During queries, the current visible version number is compared with the corresponding record's version number; if the record's version is less than the visible version, it indicates that the record is visible. -在 `InnoDB` 存储引擎中,[多版本控制 (multi versioning)](https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html) 就是对非锁定读的实现。如果读取的行正在执行 `DELETE` 或 `UPDATE` 操作,这时读取操作不会去等待行上锁的释放。相反地,`InnoDB` 存储引擎会去读取行的一个快照数据,对于这种读取历史数据的方式,我们叫它快照读 (snapshot read) +In the `InnoDB` storage engine, [multi-versioning](https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html) is the implementation of nonlocking reads. If the row being read is undergoing a `DELETE` or `UPDATE` operation, the read operation does not wait for the row lock to be released. Instead, the `InnoDB` storage engine reads a snapshot of the row's data; this method of reading historical data is called snapshot read. -在 `Repeatable Read` 和 `Read Committed` 两个隔离级别下,如果是执行普通的 `select` 语句(不包括 `select ... lock in share mode` ,`select ... for update`)则会使用 `一致性非锁定读(MVCC)`。并且在 `Repeatable Read` 下 `MVCC` 实现了可重复读和防止部分幻读 +In the `Repeatable Read` and `Read Committed` isolation levels, if a normal `SELECT` statement is executed (excluding `SELECT ... LOCK IN SHARE MODE`, `SELECT ... FOR UPDATE`), it will use `Consistent Nonlocking Reads (MVCC)`. Additionally, in `Repeatable Read`, `MVCC` implements repeatable reads and prevents phantom reads. -### 锁定读 +### Locking Reads -如果执行的是下列语句,就是 [**锁定读(Locking Reads)**](https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html) +If the following statements are executed, it is a [**Locking Read**](https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html): -- `select ... lock in share mode` -- `select ... for update` -- `insert`、`update`、`delete` 操作 +- `SELECT ... LOCK IN SHARE MODE` +- `SELECT ... FOR UPDATE` +- `INSERT`, `UPDATE`, `DELETE` operations -在锁定读下,读取的是数据的最新版本,这种读也被称为 `当前读(current read)`。锁定读会对读取到的记录加锁: +In a locking read, the latest version of the data is read, which is also referred to as `Current Read`. Locking reads will lock the records that are read: -- `select ... lock in share mode`:对记录加 `S` 锁,其它事务也可以加`S`锁,如果加 `x` 锁则会被阻塞 - -- `select ... for update`、`insert`、`update`、`delete`:对记录加 `X` 锁,且其它事务不能加任何锁 - -在一致性非锁定读下,即使读取的记录已被其它事务加上 `X` 锁,这时记录也是可以被读取的,即读取的快照数据。上面说了,在 `Repeatable Read` 下 `MVCC` 防止了部分幻读,这边的 “部分” 是指在 `一致性非锁定读` 情况下,只能读取到第一次查询之前所插入的数据(根据 Read View 判断数据可见性,Read View 在第一次查询时生成)。但是!如果是 `当前读` ,每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。所以, **`InnoDB` 在实现`Repeatable Read` 时,如果执行的是当前读,则会对读取的记录使用 `Next-key Lock` ,来防止其它事务在间隙间插入数据** - -## InnoDB 对 MVCC 的实现 - -`MVCC` 的实现依赖于:**隐藏字段、Read View、undo log**。在内部实现中,`InnoDB` 通过数据行的 `DB_TRX_ID` 和 `Read View` 来判断数据的可见性,如不可见,则通过数据行的 `DB_ROLL_PTR` 找到 `undo log` 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 `Read View` 之前已经提交的修改和该事务本身做的修改 - -### 隐藏字段 - -在内部,`InnoDB` 存储引擎为每行数据添加了三个 [隐藏字段](https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html): - -- `DB_TRX_ID(6字节)`:表示最后一次插入或更新该行的事务 id。此外,`delete` 操作在内部被视为更新,只不过会在记录头 `Record header` 中的 `deleted_flag` 字段将其标记为已删除 -- `DB_ROLL_PTR(7字节)` 回滚指针,指向该行的 `undo log` 。如果该行未被更新,则为空 -- `DB_ROW_ID(6字节)`:如果没有设置主键且该表没有唯一非空索引时,`InnoDB` 会使用该 id 来生成聚簇索引 - -### ReadView - -```c -class ReadView { - /* ... */ -private: - trx_id_t m_low_limit_id; /* 大于等于这个 ID 的事务均不可见 */ - - trx_id_t m_up_limit_id; /* 小于这个 ID 的事务均可见 */ - - trx_id_t m_creator_trx_id; /* 创建该 Read View 的事务ID */ - - trx_id_t m_low_limit_no; /* 事务 Number, 小于该 Number 的 Undo Logs 均可以被 Purge */ - - ids_t m_ids; /* 创建 Read View 时的活跃事务列表 */ - - m_closed; /* 标记 Read View 是否 close */ -} -``` - -[`Read View`](https://github.com/facebook/mysql-8.0/blob/8.0/storage/innobase/include/read0types.h#L298) 主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务” - -主要有以下字段: - -- `m_low_limit_id`:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见 -- `m_up_limit_id`:活跃事务列表 `m_ids` 中最小的事务 ID,如果 `m_ids` 为空,则 `m_up_limit_id` 为 `m_low_limit_id`。小于这个 ID 的数据版本均可见 -- `m_ids`:`Read View` 创建时其他未提交的活跃事务 ID 列表。创建 `Read View`时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。`m_ids` 不包括当前事务自己和已提交的事务(正在内存中) -- `m_creator_trx_id`:创建该 `Read View` 的事务 ID - -**事务可见性示意图**([图源](https://leviathan.vip/2019/03/20/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-MVCC/#MVCC-1)): - -![trans_visible](./images/mvvc/trans_visible.png) - -### undo-log - -`undo log` 主要有两个作用: - -- 当事务回滚时用于将数据恢复到修改前的样子 -- 另一个作用是 `MVCC` ,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过 `undo log` 读取之前的版本数据,以此实现非锁定读 - -**在 `InnoDB` 存储引擎中 `undo log` 分为两种:`insert undo log` 和 `update undo log`:** - -1. **`insert undo log`**:指在 `insert` 操作中产生的 `undo log`。因为 `insert` 操作的记录只对事务本身可见,对其他事务不可见,故该 `undo log` 可以在事务提交后直接删除。不需要进行 `purge` 操作 - -**`insert` 时的数据初始状态:** - -![](./images/mvvc/317e91e1-1ee1-42ad-9412-9098d5c6a9ad.png) - -2. **`update undo log`**:`update` 或 `delete` 操作中产生的 `undo log`。该 `undo log`可能需要提供 `MVCC` 机制,因此不能在事务提交时就进行删除。提交时放入 `undo log` 链表,等待 `purge线程` 进行最后的删除 - -**数据第一次被修改时:** - -![](./images/mvvc/c52ff79f-10e6-46cb-b5d4-3c9cbcc1934a.png) - -**数据第二次被修改时:** - -![](./images/mvvc/6a276e7a-b0da-4c7b-bdf7-c0c7b7b3b31c.png) - -不同事务或者相同事务的对同一记录行的修改,会使该记录行的 `undo log` 成为一条链表,链首就是最新的记录,链尾就是最早的旧记录。 - -### 数据可见性算法 - -在 `InnoDB` 存储引擎中,创建一个新事务后,执行每个 `select` 语句前,都会创建一个快照(Read View),**快照中保存了当前数据库系统中正处于活跃(没有 commit)的事务的 ID 号**。其实简单的说保存的是系统中当前不应该被本事务看到的其他事务 ID 列表(即 m_ids)。当用户在这个事务中要读取某个记录行的时候,`InnoDB` 会将该记录行的 `DB_TRX_ID` 与 `Read View` 中的一些变量及当前事务 ID 进行比较,判断是否满足可见性条件 - -[具体的比较算法](https://github.com/facebook/mysql-8.0/blob/8.0/storage/innobase/include/read0types.h#L161)如下([图源](https://leviathan.vip/2019/03/20/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-MVCC/#MVCC-1)): - -![](./images/mvvc/8778836b-34a8-480b-b8c7-654fe207a8c2.png) - -1. 如果记录 DB_TRX_ID < m_up_limit_id,那么表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照之前就提交了,所以该记录行的值对当前事务是可见的 - -2. 如果 DB_TRX_ID >= m_low_limit_id,那么表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照之后才修改该行,所以该记录行的值对当前事务不可见。跳到步骤 5 - -3. m_ids 为空,则表明在当前事务创建快照之前,修改该行的事务就已经提交了,所以该记录行的值对当前事务是可见的 - -4. 如果 m_up_limit_id <= DB_TRX_ID < m_low_limit_id,表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表 m_ids 进行查找(源码中是用的二分查找,因为是有序的) - - - 如果在活跃事务列表 m_ids 中能找到 DB_TRX_ID,表明:① 在当前事务创建快照前,该记录行的值被事务 ID 为 DB_TRX_ID 的事务修改了,但没有提交;或者 ② 在当前事务创建快照后,该记录行的值被事务 ID 为 DB_TRX_ID 的事务修改了。这些情况下,这个记录行的值对当前事务都是不可见的。跳到步骤 5 - - - 在活跃事务列表中找不到,则表明“id 为 trx_id 的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见 - -5. 在该记录行的 DB_ROLL_PTR 指针所指向的 `undo log` 取出快照记录,用快照记录的 DB_TRX_ID 跳到步骤 1 重新开始判断,直到找到满足的快照版本或返回空 - -## RC 和 RR 隔离级别下 MVCC 的差异 - -在事务隔离级别 `RC` 和 `RR` (InnoDB 存储引擎的默认事务隔离级别)下,`InnoDB` 存储引擎使用 `MVCC`(非锁定一致性读),但它们生成 `Read View` 的时机却不同 - -- 在 RC 隔离级别下的 **`每次select`** 查询前都生成一个`Read View` (m_ids 列表) -- 在 RR 隔离级别下只在事务开始后 **`第一次select`** 数据前生成一个`Read View`(m_ids 列表) - -## MVCC 解决不可重复读问题 - -虽然 RC 和 RR 都通过 `MVCC` 来读取快照数据,但由于 **生成 Read View 时机不同**,从而在 RR 级别下实现可重复读 - -举个例子: - -![](./images/mvvc/6fb2b9a1-5f14-4dec-a797-e4cf388ed413.png) - -### 在 RC 下 ReadView 生成情况 - -**1. 假设时间线来到 T4 ,那么此时数据行 id = 1 的版本链为:** - -![](./images/mvvc/a3fd1ec6-8f37-42fa-b090-7446d488fd04.png) - -由于 RC 级别下每次查询都会生成`Read View` ,并且事务 101、102 并未提交,此时 `103` 事务生成的 `Read View` 中活跃的事务 **`m_ids` 为:[101,102]** ,`m_low_limit_id`为:104,`m_up_limit_id`为:101,`m_creator_trx_id` 为:103 - -- 此时最新记录的 `DB_TRX_ID` 为 101,m_up_limit_id <= 101 < m_low_limit_id,所以要在 `m_ids` 列表中查找,发现 `DB_TRX_ID` 存在列表中,那么这个记录不可见 -- 根据 `DB_ROLL_PTR` 找到 `undo log` 中的上一版本记录,上一条记录的 `DB_TRX_ID` 还是 101,不可见 -- 继续找上一条 `DB_TRX_ID`为 1,满足 1 < m_up_limit_id,可见,所以事务 103 查询到数据为 `name = 菜花` - -**2. 时间线来到 T6 ,数据的版本链为:** - -![](./images/mvvc/528559e9-dae8-4d14-b78d-a5b657c88391.png) - -因为在 RC 级别下,重新生成 `Read View`,这时事务 101 已经提交,102 并未提交,所以此时 `Read View` 中活跃的事务 **`m_ids`:[102]** ,`m_low_limit_id`为:104,`m_up_limit_id`为:102,`m_creator_trx_id`为:103 - -- 此时最新记录的 `DB_TRX_ID` 为 102,m_up_limit_id <= 102 < m_low_limit_id,所以要在 `m_ids` 列表中查找,发现 `DB_TRX_ID` 存在列表中,那么这个记录不可见 - -- 根据 `DB_ROLL_PTR` 找到 `undo log` 中的上一版本记录,上一条记录的 `DB_TRX_ID` 为 101,满足 101 < m_up_limit_id,记录可见,所以在 `T6` 时间点查询到数据为 `name = 李四`,与时间 T4 查询到的结果不一致,不可重复读! - -**3. 时间线来到 T9 ,数据的版本链为:** - -![](./images/mvvc/6f82703c-36a1-4458-90fe-d7f4edbac71a.png) - -重新生成 `Read View`, 这时事务 101 和 102 都已经提交,所以 **m_ids** 为空,则 m_up_limit_id = m_low_limit_id = 104,最新版本事务 ID 为 102,满足 102 < m_low_limit_id,可见,查询结果为 `name = 赵六` - -> **总结:** **在 RC 隔离级别下,事务在每次查询开始时都会生成并设置新的 Read View,所以导致不可重复读** - -### 在 RR 下 ReadView 生成情况 - -在可重复读级别下,只会在事务开始后第一次读取数据时生成一个 Read View(m_ids 列表) - -**1. 在 T4 情况下的版本链为:** - -![](./images/mvvc/0e906b95-c916-4f30-beda-9cb3e49746bf.png) - -在当前执行 `select` 语句时生成一个 `Read View`,此时 **`m_ids`:[101,102]** ,`m_low_limit_id`为:104,`m_up_limit_id`为:101,`m_creator_trx_id` 为:103 - -此时和 RC 级别下一样: - -- 最新记录的 `DB_TRX_ID` 为 101,m_up_limit_id <= 101 < m_low_limit_id,所以要在 `m_ids` 列表中查找,发现 `DB_TRX_ID` 存在列表中,那么这个记录不可见 -- 根据 `DB_ROLL_PTR` 找到 `undo log` 中的上一版本记录,上一条记录的 `DB_TRX_ID` 还是 101,不可见 -- 继续找上一条 `DB_TRX_ID`为 1,满足 1 < m_up_limit_id,可见,所以事务 103 查询到数据为 `name = 菜花` - -**2. 时间点 T6 情况下:** - -![](./images/mvvc/79ed6142-7664-4e0b-9023-cf546586aa39.png) - -在 RR 级别下只会生成一次`Read View`,所以此时依然沿用 **`m_ids`:[101,102]** ,`m_low_limit_id`为:104,`m_up_limit_id`为:101,`m_creator_trx_id` 为:103 - -- 最新记录的 `DB_TRX_ID` 为 102,m_up_limit_id <= 102 < m_low_limit_id,所以要在 `m_ids` 列表中查找,发现 `DB_TRX_ID` 存在列表中,那么这个记录不可见 - -- 根据 `DB_ROLL_PTR` 找到 `undo log` 中的上一版本记录,上一条记录的 `DB_TRX_ID` 为 101,不可见 - -- 继续根据 `DB_ROLL_PTR` 找到 `undo log` 中的上一版本记录,上一条记录的 `DB_TRX_ID` 还是 101,不可见 - -- 继续找上一条 `DB_TRX_ID`为 1,满足 1 < m_up_limit_id,可见,所以事务 103 查询到数据为 `name = 菜花` - -**3. 时间点 T9 情况下:** - -![](./images/mvvc/cbbedbc5-0e3c-4711-aafd-7f3d68a4ed4e.png) - -此时情况跟 T6 完全一样,由于已经生成了 `Read View`,此时依然沿用 **`m_ids`:[101,102]** ,所以查询结果依然是 `name = 菜花` - -## MVCC➕Next-key-Lock 防止幻读 - -`InnoDB`存储引擎在 RR 级别下通过 `MVCC`和 `Next-key Lock` 来解决幻读问题: - -**1、执行普通 `select`,此时会以 `MVCC` 快照读的方式读取数据** - -在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 `Read View` ,并使用至事务提交。所以在生成 `Read View` 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读” - -**2、执行 select...for update/lock in share mode、insert、update、delete 等当前读** - -在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!`InnoDB` 使用 [Next-key Lock](https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-next-key-locks) 来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读 - -## 参考 - -- **《MySQL 技术内幕 InnoDB 存储引擎第 2 版》** -- [Innodb 中的事务隔离级别和锁的关系](https://tech.meituan.com/2014/08/20/innodb-lock.html) -- [MySQL 事务与 MVCC 如何实现的隔离级别](https://blog.csdn.net/qq_35190492/article/details/109044141) -- [InnoDB 事务分析-MVCC](https://leviathan.vip/2019/03/20/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-MVCC/) - - +- `SELECT ... LOCK IN SHARE MODE`: applies an `S` lock to the record, allowing other transactions to also apply `S` locks, but if an `X` lock is applied, it will be blocked. +- `SELECT ... FOR UPDATE`, `INSERT`, `UPDATE`, \` diff --git a/docs/database/mysql/mysql-auto-increment-primary-key-continuous.md b/docs/database/mysql/mysql-auto-increment-primary-key-continuous.md index ec900188610..aaaaba20b50 100644 --- a/docs/database/mysql/mysql-auto-increment-primary-key-continuous.md +++ b/docs/database/mysql/mysql-auto-increment-primary-key-continuous.md @@ -1,221 +1,62 @@ --- -title: MySQL自增主键一定是连续的吗 -category: 数据库 +title: Is MySQL Auto-Increment Primary Key Always Continuous? +category: Database tag: - MySQL - - 大厂面试 + - Big Company Interviews --- -> 作者:飞天小牛肉 +> Author: Feitian Xiaoniurou > -> 原文: +> Original: -众所周知,自增主键可以让聚集索引尽量地保持递增顺序插入,避免了随机查询,从而提高了查询效率。 +As we all know, auto-increment primary keys allow clustered indexes to maintain an increasing order of insertion, avoiding random queries and thus improving query efficiency. -但实际上,MySQL 的自增主键并不能保证一定是连续递增的。 +However, in reality, MySQL's auto-increment primary keys do not guarantee that they will always be continuously increasing. -下面举个例子来看下,如下所示创建一张表: +Let's look at an example by creating a table as shown below: ![](https://oss.javaguide.cn/p3-juejin/3e6b80ba50cb425386b80924e3da0d23~tplv-k3u1fbpfcp-zoom-1.png) -## 自增值保存在哪里? +## Where is the Auto-Increment Value Stored? -使用 `insert into test_pk values(null, 1, 1)` 插入一行数据,再执行 `show create table` 命令来看一下表的结构定义: +Using `insert into test_pk values(null, 1, 1)` to insert a row of data, then executing the `show create table` command to check the table structure definition: ![](https://oss.javaguide.cn/p3-juejin/c17e46230bd34150966f0d86b2ad5e91~tplv-k3u1fbpfcp-zoom-1.png) -上述表的结构定义存放在后缀名为 `.frm` 的本地文件中,在 MySQL 安装目录下的 data 文件夹下可以找到这个 `.frm` 文件: +The structure definition of the above table is stored in a local file with the `.frm` suffix, which can be found in the data folder under the MySQL installation directory: ![](https://oss.javaguide.cn/p3-juejin/3ec0514dd7be423d80b9e7f2d52f5902~tplv-k3u1fbpfcp-zoom-1.png) -从上述表结构可以看到,表定义里面出现了一个 `AUTO_INCREMENT=2`,表示下一次插入数据时,如果需要自动生成自增值,会生成 id = 2。 +From the above table structure, we can see that the table definition contains `AUTO_INCREMENT=2`, indicating that the next time data is inserted, if an auto-increment value is needed, it will generate id = 2. -但需要注意的是,自增值并不会保存在这个表结构也就是 `.frm` 文件中,不同的引擎对于自增值的保存策略不同: +However, it is important to note that the auto-increment value is not stored in this table structure, i.e., the `.frm` file. Different engines have different strategies for storing auto-increment values: -1)MyISAM 引擎的自增值保存在数据文件中 +1. The MyISAM engine stores the auto-increment value in the data file. -2)InnoDB 引擎的自增值,其实是保存在了内存里,并没有持久化。第一次打开表的时候,都会去找自增值的最大值 `max(id)`,然后将 `max(id)+1` 作为这个表当前的自增值。 +1. The InnoDB engine actually stores the auto-increment value in memory and does not persist it. When the table is first opened, it looks for the maximum auto-increment value `max(id)` and then sets `max(id)+1` as the current auto-increment value for the table. -举个例子:我们现在表里当前数据行里最大的 id 是 1,AUTO_INCREMENT=2,对吧。这时候,我们删除 id=1 的行,AUTO_INCREMENT 还是 2。 +For example: if the current maximum id in the table is 1, and AUTO_INCREMENT=2, right? If we delete the row with id=1, AUTO_INCREMENT remains 2. ![](https://oss.javaguide.cn/p3-juejin/61b8dc9155624044a86d91c368b20059~tplv-k3u1fbpfcp-zoom-1.png) -但如果马上重启 MySQL 实例,重启后这个表的 AUTO_INCREMENT 就会变成 1。 也就是说,MySQL 重启可能会修改一个表的 AUTO_INCREMENT 的值。 +But if we immediately restart the MySQL instance, after the restart, the AUTO_INCREMENT for this table will change to 1. This means that restarting MySQL may modify the AUTO_INCREMENT value of a table. ![](https://oss.javaguide.cn/p3-juejin/27fdb15375664249a31f88b64e6e5e66~tplv-k3u1fbpfcp-zoom-1.png) ![](https://oss.javaguide.cn/p3-juejin/dee15f93e65d44d384345a03404f3481~tplv-k3u1fbpfcp-zoom-1.png) -以上,是在我本地 MySQL 5.x 版本的实验,实际上,**到了 MySQL 8.0 版本后,自增值的变更记录被放在了 redo log 中,提供了自增值持久化的能力** ,也就是实现了“如果发生重启,表的自增值可以根据 redo log 恢复为 MySQL 重启前的值” +The above is based on my local MySQL 5.x version experiment. In fact, **after MySQL 8.0, the change records of auto-increment values are stored in the redo log, providing the capability for auto-increment value persistence**, meaning that "if a restart occurs, the table's auto-increment value can be restored to its value before the MySQL restart" based on the redo log. -也就是说对于上面这个例子来说,重启实例后这个表的 AUTO_INCREMENT 仍然是 2。 +In other words, for the above example, after restarting the instance, the AUTO_INCREMENT for this table is still 2. -理解了 MySQL 自增值到底保存在哪里以后,我们再来看看自增值的修改机制,并以此引出第一种自增值不连续的场景。 +After understanding where MySQL auto-increment values are stored, let's look at the mechanism for modifying auto-increment values, which leads us to the first scenario of non-continuous auto-increment values. -## 自增值不连续的场景 +## Scenarios of Non-Continuous Auto-Increment Values -### 自增值不连续场景 1 +### Scenario 1: Non-Continuous Auto-Increment Values -在 MySQL 里面,如果字段 id 被定义为 AUTO_INCREMENT,在插入一行数据的时候,自增值的行为如下: +In MySQL, if the id field is defined as AUTO_INCREMENT, the behavior of the auto-increment value when inserting a row of data is as follows: -- 如果插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段; -- 如果插入数据时 id 字段指定了具体的值,就直接使用语句里指定的值。 - -根据要插入的值和当前自增值的大小关系,自增值的变更结果也会有所不同。假设某次要插入的值是 `insert_num`,当前的自增值是 `autoIncrement_num`: - -- 如果 `insert_num < autoIncrement_num`,那么这个表的自增值不变 -- 如果 `insert_num >= autoIncrement_num`,就需要把当前自增值修改为新的自增值 - -也就是说,如果插入的 id 是 100,当前的自增值是 90,`insert_num >= autoIncrement_num`,那么自增值就会被修改为新的自增值即 101 - -一定是这样吗? - -非也~ - -了解过分布式 id 的小伙伴一定知道,为了避免两个库生成的主键发生冲突,我们可以让一个库的自增 id 都是奇数,另一个库的自增 id 都是偶数 - -这个奇数偶数其实是通过 `auto_increment_offset` 和 `auto_increment_increment` 这两个参数来决定的,这俩分别用来表示自增的初始值和步长,默认值都是 1。 - -所以,上面的例子中生成新的自增值的步骤实际是这样的:从 `auto_increment_offset` 开始,以 `auto_increment_increment` 为步长,持续叠加,直到找到第一个大于 100 的值,作为新的自增值。 - -所以,这种情况下,自增值可能会是 102,103 等等之类的,就会导致不连续的主键 id。 - -更遗憾的是,即使在自增初始值和步长这两个参数都设置为 1 的时候,自增主键 id 也不一定能保证主键是连续的 - -### 自增值不连续场景 2 - -举个例子,我们现在往表里插入一条 (null,1,1) 的记录,生成的主键是 1,AUTO_INCREMENT= 2,对吧 - -![](https://oss.javaguide.cn/p3-juejin/c22c4f2cea234c7ea496025eb826c3bc~tplv-k3u1fbpfcp-zoom-1.png) - -这时我再执行一条插入 `(null,1,1)` 的命令,很显然会报错 `Duplicate entry`,因为我们设置了一个唯一索引字段 `a`: - -![](https://oss.javaguide.cn/p3-juejin/c0325e31398d4fa6bb1cbe08ef797b7f~tplv-k3u1fbpfcp-zoom-1.png) - -但是,你会惊奇的发现,虽然插入失败了,但自增值仍然从 2 增加到了 3! - -这是为啥? - -我们来分析下这个 insert 语句的执行流程: - -1. 执行器调用 InnoDB 引擎接口准备插入一行记录 (null,1,1); -2. InnoDB 发现用户没有指定自增 id 的值,则获取表 `test_pk` 当前的自增值 2; -3. 将传入的记录改成 (2,1,1); -4. 将表的自增值改成 3; -5. 继续执行插入数据操作,由于已经存在 a=1 的记录,所以报 Duplicate key error,语句返回 - -可以看到,自增值修改的这个操作,是在真正执行插入数据的操作之前。 - -这个语句真正执行的时候,因为碰到唯一键 a 冲突,所以 id = 2 这一行并没有插入成功,但也没有将自增值再改回去。所以,在这之后,再插入新的数据行时,拿到的自增 id 就是 3。也就是说,出现了自增主键不连续的情况。 - -至此,我们已经罗列了两种自增主键不连续的情况: - -1. 自增初始值和自增步长设置不为 1 -2. 唯一键冲突 - -除此之外,事务回滚也会导致这种情况 - -### 自增值不连续场景 3 - -我们现在表里有一行 `(1,1,1)` 的记录,AUTO_INCREMENT = 3: - -![](https://oss.javaguide.cn/p3-juejin/6220fcf7dac54299863e43b6fb97de3e~tplv-k3u1fbpfcp-zoom-1.png) - -我们先插入一行数据 `(null, 2, 2)`,也就是 (3, 2, 2) 嘛,并且 AUTO_INCREMENT 变为 4: - -![](https://oss.javaguide.cn/p3-juejin/3f02d46437d643c3b3d9f44a004ab269~tplv-k3u1fbpfcp-zoom-1.png) - -再去执行这样一段 SQL: - -![](https://oss.javaguide.cn/p3-juejin/faf5ce4a2920469cae697f845be717f5~tplv-k3u1fbpfcp-zoom-1.png) - -虽然我们插入了一条 (null, 3, 3) 记录,但是使用 rollback 进行回滚了,所以数据库中是没有这条记录的: - -![](https://oss.javaguide.cn/p3-juejin/6cb4c02722674dd399939d3d03a431c1~tplv-k3u1fbpfcp-zoom-1.png) - -在这种事务回滚的情况下,自增值并没有同样发生回滚!如下图所示,自增值仍然固执地从 4 增加到了 5: - -![](https://oss.javaguide.cn/p3-juejin/e6eea1c927424ac7bda34a511ca521ae~tplv-k3u1fbpfcp-zoom-1.png) - -所以这时候我们再去插入一条数据(null, 3, 3)的时候,主键 id 就会被自动赋为 `5` 了: - -![](https://oss.javaguide.cn/p3-juejin/80da69dd13b543c4a32d6ed832a3c568~tplv-k3u1fbpfcp-zoom-1.png) - -那么,为什么在出现唯一键冲突或者回滚的时候,MySQL 没有把表的自增值改回去呢?回退回去的话不就不会发生自增 id 不连续了吗? - -事实上,这么做的主要原因是为了提高性能。 - -我们直接用反证法来验证:假设 MySQL 在事务回滚的时候会把自增值改回去,会发生什么? - -现在有两个并行执行的事务 A 和 B,在申请自增值的时候,为了避免两个事务申请到相同的自增 id,肯定要加锁,然后顺序申请,对吧。 - -1. 假设事务 A 申请到了 id = 1, 事务 B 申请到 id=2,那么这时候表 t 的自增值是 3,之后继续执行。 -2. 事务 B 正确提交了,但事务 A 出现了唯一键冲突,也就是 id = 1 的那行记录插入失败了,那如果允许事务 A 把自增 id 回退,也就是把表的当前自增值改回 1,那么就会出现这样的情况:表里面已经有 id = 2 的行,而当前的自增 id 值是 1。 -3. 接下来,继续执行的其他事务就会申请到 id=2。这时,就会出现插入语句报错“主键冲突”。 - -![](https://oss.javaguide.cn/p3-juejin/5f26f02e60f643c9a7cab88a9f1bdce9~tplv-k3u1fbpfcp-zoom-1.png) - -而为了解决这个主键冲突,有两种方法: - -1. 每次申请 id 之前,先判断表里面是否已经存在这个 id,如果存在,就跳过这个 id -2. 把自增 id 的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增 id - -很显然,上述两个方法的成本都比较高,会导致性能问题。而究其原因呢,是我们假设的这个 “允许自增 id 回退”。 - -因此,InnoDB 放弃了这个设计,语句执行失败也不回退自增 id。也正是因为这样,所以才只保证了自增 id 是递增的,但不保证是连续的。 - -综上,已经分析了三种自增值不连续的场景,还有第四种场景:批量插入数据。 - -### 自增值不连续场景 4 - -对于批量插入数据的语句,MySQL 有一个批量申请自增 id 的策略: - -1. 语句执行过程中,第一次申请自增 id,会分配 1 个; -2. 1 个用完以后,这个语句第二次申请自增 id,会分配 2 个; -3. 2 个用完以后,还是这个语句,第三次申请自增 id,会分配 4 个; -4. 依此类推,同一个语句去申请自增 id,每次申请到的自增 id 个数都是上一次的两倍。 - -注意,这里说的批量插入数据,不是在普通的 insert 语句里面包含多个 value 值!!!,因为这类语句在申请自增 id 的时候,是可以精确计算出需要多少个 id 的,然后一次性申请,申请完成后锁就可以释放了。 - -而对于 `insert … select`、replace …… select 和 load data 这种类型的语句来说,MySQL 并不知道到底需要申请多少 id,所以就采用了这种批量申请的策略,毕竟一个一个申请的话实在太慢了。 - -举个例子,假设我们现在这个表有下面这些数据: - -![](https://oss.javaguide.cn/p3-juejin/6453cfc107f94e3bb86c95072d443472~tplv-k3u1fbpfcp-zoom-1.png) - -我们创建一个和当前表 `test_pk` 有相同结构定义的表 `test_pk2`: - -![](https://oss.javaguide.cn/p3-juejin/45248a6dc34f431bba14d434bee2c79e~tplv-k3u1fbpfcp-zoom-1.png) - -然后使用 `insert...select` 往 `teset_pk2` 表中批量插入数据: - -![](https://oss.javaguide.cn/p3-juejin/c1b061e86bae484694d15ceb703b10ca~tplv-k3u1fbpfcp-zoom-1.png) - -可以看到,成功导入了数据。 - -再来看下 `test_pk2` 的自增值是多少: - -![](https://oss.javaguide.cn/p3-juejin/0ff9039366154c738331d64ebaf88d3b~tplv-k3u1fbpfcp-zoom-1.png) - -如上分析,是 8 而不是 6 - -具体来说,insert……select 实际上往表中插入了 5 行数据 (1 1)(2 2)(3 3)(4 4)(5 5)。但是,这五行数据是分三次申请的自增 id,结合批量申请策略,每次申请到的自增 id 个数都是上一次的两倍,所以: - -- 第一次申请到了一个 id:id=1 -- 第二次被分配了两个 id:id=2 和 id=3 -- 第三次被分配到了 4 个 id:id=4、id = 5、id = 6、id=7 - -由于这条语句实际只用上了 5 个 id,所以 id=6 和 id=7 就被浪费掉了。之后,再执行 `insert into test_pk2 values(null,6,6)`,实际上插入的数据就是(8,6,6): - -![](https://oss.javaguide.cn/p3-juejin/51612fbac3804cff8c5157df21d6e355~tplv-k3u1fbpfcp-zoom-1.png) - -## 小结 - -本文总结下自增值不连续的 4 个场景: - -1. 自增初始值和自增步长设置不为 1 -2. 唯一键冲突 -3. 事务回滚 -4. 批量插入(如 `insert...select` 语句) - - +- If the id field is specified as 0, null, or not specified, the current AUTO_INCREMENT value of the table is filled into the auto-increment field; +- If the id field is specified with a specific value during insertion, the diff --git a/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md b/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md index 38c333b3308..acb7143114a 100644 --- a/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md +++ b/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md @@ -1,389 +1,95 @@ --- -title: MySQL高性能优化规范建议总结 -category: 数据库 +title: MySQL High Performance Optimization Specification Summary +category: Database tag: - MySQL --- -> 作者: 听风 原文地址: 。 +> Author: Listening to the Wind Original Article: 。 > -> JavaGuide 已获得作者授权,并对原文内容进行了完善补充。 +> JavaGuide has obtained authorization from the author and has made improvements and supplements to the original content. -## 数据库命名规范 +## Database Naming Conventions -- 所有数据库对象名称必须使用小写字母并用下划线分割。 -- 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)。 -- 数据库对象的命名要能做到见名识义,并且最好不要超过 32 个字符。 -- 临时库表必须以 `tmp_` 为前缀并以日期为后缀,备份表必须以 `bak_` 为前缀并以日期 (时间戳) 为后缀。 -- 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)。 +- All database object names must use lowercase letters and be separated by underscores. +- The use of MySQL reserved keywords in database object names is prohibited (if a keyword is included in the table name, it must be enclosed in single quotes during queries). +- The naming of database objects should be meaningful and should ideally not exceed 32 characters. +- Temporary tables must have a prefix of `tmp_` and a suffix of the date, while backup tables must have a prefix of `bak_` and a suffix of the date (timestamp). +- All columns that store the same data must have consistent names and data types (generally as associated columns; if the data types of associated columns are inconsistent during queries, implicit type conversion will occur, which can invalidate indexes on the columns and reduce query efficiency). -## 数据库基本设计规范 +## Basic Database Design Specifications -### 所有表必须使用 InnoDB 存储引擎 +### All tables must use the InnoDB storage engine -没有特殊要求(即 InnoDB 无法满足的功能如:列存储、存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 MyISAM,5.6 以后默认的为 InnoDB)。 +Unless there are special requirements (i.e., functionalities that InnoDB cannot meet, such as column storage, storage space data, etc.), all tables must use the InnoDB storage engine (MySQL 5.5 and earlier defaulted to MyISAM, while 5.6 and later default to InnoDB). -InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能更好。 +InnoDB supports transactions, row-level locking, better recovery, and performs better under high concurrency. -### 数据库和表的字符集统一使用 UTF8 +### The character set for databases and tables must uniformly use UTF8 -兼容性更好,统一字符集可以避免由于字符集转换产生的乱码,不同的字符集进行比较前需要进行转换会造成索引失效,如果数据库中有存储 emoji 表情的需要,字符集需要采用 utf8mb4 字符集。 +Better compatibility; a unified character set can avoid garbled characters caused by character set conversion. Comparing different character sets requires conversion, which can invalidate indexes. If the database needs to store emoji characters, the character set should use utf8mb4. -推荐阅读一下我写的这篇文章:[MySQL 字符集详解](../character-set.md) 。 +I recommend reading this article I wrote: [Detailed Explanation of MySQL Character Sets](../character-set.md). -### 所有表和字段都需要添加注释 +### All tables and fields must have comments added -使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护。 +Use the comment clause to add remarks to tables and columns, maintaining a data dictionary from the start. -### 尽量控制单表数据量的大小,建议控制在 500 万以内 +### Try to control the size of data in a single table, recommended to keep it under 5 million -500 万并不是 MySQL 数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题。 +5 million is not a limitation of MySQL; larger sizes can cause significant issues with modifying table structures, backups, and recovery. -可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小。 +You can control data size using historical data archiving (applicable to log data), sharding (applicable to business data), etc. -### 谨慎使用 MySQL 分区表 +### Use MySQL partitioned tables with caution -分区表在物理上表现为多个文件,在逻辑上表现为一个表。 +Partitioned tables physically appear as multiple files but logically appear as a single table. -谨慎选择分区键,跨分区查询效率可能更低。 +Carefully choose partition keys, as cross-partition queries may have lower efficiency. -建议采用物理分表的方式管理大数据。 +It is recommended to manage large data using physical sharding. -### 经常一起使用的列放到一个表中 +### Frequently used columns should be placed in the same table -避免更多的关联操作。 +Avoid more join operations. -### 禁止在表中建立预留字段 +### Prohibit the establishment of reserved fields in tables -- 预留字段的命名很难做到见名识义。 -- 预留字段无法确认存储的数据类型,所以无法选择合适的类型。 -- 对预留字段类型的修改,会对表进行锁定。 +- The naming of reserved fields is difficult to make meaningful. +- The data type of reserved fields cannot be confirmed, making it impossible to choose an appropriate type. +- Modifying the type of reserved fields will lock the table. -### 禁止在数据库中存储文件(比如图片)这类大的二进制数据 +### Prohibit storing large binary data (such as images) in the database -在数据库中存储文件会严重影响数据库性能,消耗过多存储空间。 +Storing files in the database can severely impact performance and consume excessive storage space. -文件(比如图片)这类大的二进制数据通常存储于文件服务器,数据库只存储文件地址信息。 +Large binary data (such as images) is typically stored on file servers, with the database only storing file address information. -### 不要被数据库范式所束缚 +### Do not be constrained by database normalization -一般来说,设计关系数据库时需要满足第三范式,但为了满足第三范式,我们可能会拆分出多张表。而在进行查询时需要对多张表进行关联查询,有时为了提高查询效率,会降低范式的要求,在表中保存一定的冗余信息,也叫做反范式。但要注意反范式一定要适度。 +Generally, when designing a relational database, it is necessary to meet the third normal form. However, to satisfy the third normal form, we may split into multiple tables. During queries, multiple tables need to be joined, and sometimes to improve query efficiency, we may relax normalization requirements and store some redundant information in tables, which is also known as denormalization. However, it is important to ensure that denormalization is done in moderation. -### 禁止在线上做数据库压力测试 +### Prohibit conducting database stress tests online -### 禁止从开发环境、测试环境直接连接生产环境数据库 +### Prohibit direct connections to the production database from development or testing environments -安全隐患极大,要对生产环境抱有敬畏之心! +This poses significant security risks; one must have a sense of reverence for the production environment! -## 数据库字段设计规范 +## Database Field Design Specifications -### 优先选择符合存储需要的最小的数据类型 +### Prioritize selecting the smallest data type that meets storage needs -存储字节越小,占用空间也就越小,性能也越好。 +The smaller the storage bytes, the less space it occupies, and the better the performance. -**a.某些字符串可以转换成数字类型存储,比如可以将 IP 地址转换成整型数据。** +**a. Some strings can be converted to numeric types for storage, such as converting IP addresses to integer data.** -数字是连续的,性能更好,占用空间也更小。 +Numbers are continuous, perform better, and occupy less space. -MySQL 提供了两个方法来处理 ip 地址: +MySQL provides two methods to handle IP addresses: -- `INET_ATON()`:把 ip 转为无符号整型 (4-8 位); -- `INET_NTOA()`:把整型的 ip 转为地址。 +- `INET_ATON()`: Converts IP to an unsigned integer (4-8 bits); +- `INET_NTOA()`: Converts an integer IP back to an address. -插入数据前,先用 `INET_ATON()` 把 ip 地址转为整型;显示数据时,使用 `INET_NTOA()` 把整型的 ip 地址转为地址显示即可。 +Before inserting data, use `INET_ATON()` to convert the IP address to an integer; when displaying data, use `INET_NTOA()` to convert the integer IP address back to an address for display. -**b.对于非负型的数据 (如自增 ID、整型 IP、年龄) 来说,要优先使用无符号整型来存储。** - -无符号相对于有符号可以多出一倍的存储空间: - -```sql -SIGNED INT -2147483648~2147483647 -UNSIGNED INT 0~4294967295 -``` - -**c.小数值类型(比如年龄、状态表示如 0/1)优先使用 TINYINT 类型。** - -### 避免使用 TEXT、BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据 - -**a. 建议把 BLOB 或是 TEXT 列分离到单独的扩展表中。** - -MySQL 内存临时表不支持 TEXT、BLOB 这样的大数据类型,如果查询中包含这样的数据,在排序等操作时,就不能使用内存临时表,必须使用磁盘临时表进行。而且对于这种数据,MySQL 还是要进行二次查询,会使 sql 性能变得很差,但是不是说一定不能使用这样的数据类型。 - -如果一定要使用,建议把 BLOB 或是 TEXT 列分离到单独的扩展表中,查询时一定不要使用 `select *`而只需要取出必要的列,不需要 TEXT 列的数据时不要对该列进行查询。 - -**2、TEXT 或 BLOB 类型只能使用前缀索引** - -因为 MySQL 对索引字段长度是有限制的,所以 TEXT 类型只能使用前缀索引,并且 TEXT 列上是不能有默认值的。 - -### 避免使用 ENUM 类型 - -- 修改 ENUM 值需要使用 ALTER 语句。 -- ENUM 类型的 ORDER BY 操作效率低,需要额外操作。 -- ENUM 数据类型存在一些限制,比如建议不要使用数值作为 ENUM 的枚举值。 - -相关阅读:[是否推荐使用 MySQL 的 enum 类型? - 架构文摘 - 知乎](https://www.zhihu.com/question/404422255/answer/1661698499) 。 - -### 尽可能把所有列定义为 NOT NULL - -除非有特别的原因使用 NULL 值,否则应该总是让字段保持 NOT NULL。 - -- 索引 NULL 列需要额外的空间来保存,所以要占用更多的空间。 -- 进行比较和计算时要对 NULL 值做特别的处理。 - -相关阅读:[技术分享 | MySQL 默认值选型(是空,还是 NULL)](https://opensource.actionsky.com/20190710-mysql/) 。 - -### 一定不要用字符串存储日期 - -对于日期类型来说,一定不要用字符串存储日期。可以考虑 DATETIME、TIMESTAMP 和数值型时间戳。 - -这三种种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家在实际开发中选择正确的存放时间的数据类型: - -| 类型 | 存储空间 | 日期格式 | 日期范围 | 是否带时区信息 | -| ------------ | -------- | ------------------------------ | ------------------------------------------------------------ | -------------- | -| DATETIME | 5~8 字节 | YYYY-MM-DD hh:mm:ss[.fraction] | 1000-01-01 00:00:00[.000000] ~ 9999-12-31 23:59:59[.999999] | 否 | -| TIMESTAMP | 4~7 字节 | YYYY-MM-DD hh:mm:ss[.fraction] | 1970-01-01 00:00:01[.000000] ~ 2038-01-19 03:14:07[.999999] | 是 | -| 数值型时间戳 | 4 字节 | 全数字如 1578707612 | 1970-01-01 00:00:01 之后的时间 | 否 | - -MySQL 时间类型选择的详细介绍请看这篇:[MySQL 时间类型数据存储建议](https://javaguide.cn/database/mysql/some-thoughts-on-database-storage-time.html)。 - -### 同财务相关的金额类数据必须使用 decimal 类型 - -- **非精准浮点**:float、double -- **精准浮点**:decimal - -decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节。并且,decimal 可用于存储比 bigint 更大的整型数据。 - -不过, 由于 decimal 需要额外的空间和计算开销,应该尽量只在需要对数据进行精确计算时才使用 decimal 。 - -### 单表不要包含过多字段 - -如果一个表包含过多字段的话,可以考虑将其分解成多个表,必要时增加中间表进行关联。 - -## 索引设计规范 - -### 限制每张表上的索引数量,建议单张表索引不超过 5 个 - -索引并不是越多越好!索引可以提高效率,同样可以降低效率。 - -索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。 - -因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划。如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。 - -### 禁止使用全文索引 - -全文索引不适用于 OLTP 场景。 - -### 禁止给表中的每一列都建立单独的索引 - -5.6 版本之前,一个 sql 只能使用到一个表中的一个索引;5.6 以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询方式好。 - -### 每个 InnoDB 表必须有个主键 - -InnoDB 是一种索引组织表:数据的存储的逻辑顺序和索引的顺序是相同的。每个表都可以有多个索引,但是表的存储顺序只能有一种。 - -InnoDB 是按照主键索引的顺序来组织表的。 - -- 不要使用更新频繁的列作为主键,不使用多列主键(相当于联合索引)。 -- 不要使用 UUID、MD5、HASH、字符串列作为主键(无法保证数据的顺序增长)。 -- 主键建议使用自增 ID 值。 - -### 常见索引列建议 - -- 出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列。 -- 包含在 ORDER BY、GROUP BY、DISTINCT 中的字段。 -- 不要将符合 1 和 2 中的字段的列都建立一个索引,通常将 1、2 中的字段建立联合索引效果更好。 -- 多表 join 的关联列。 - -### 如何选择索引列的顺序 - -建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。 - -- **区分度最高的列放在联合索引的最左侧**:这是最重要的原则。区分度越高,通过索引筛选出的数据就越少,I/O 操作也就越少。计算区分度的方法是 `count(distinct column) / count(*)`。 -- **最频繁使用的列放在联合索引的左侧**:这符合最左前缀匹配原则。将最常用的查询条件列放在最左侧,可以最大程度地利用索引。 -- **字段长度**:字段长度对联合索引非叶子节点的影响很小,因为它存储了所有联合索引字段的值。字段长度主要影响主键和包含在其他索引中的字段的存储空间,以及这些索引的叶子节点的大小。因此,在选择联合索引列的顺序时,字段长度的优先级最低。对于主键和包含在其他索引中的字段,选择较短的字段长度可以节省存储空间和提高 I/O 性能。 - -### 避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间) - -- 重复索引示例:primary key(id)、index(id)、unique index(id)。 -- 冗余索引示例:index(a,b,c)、index(a,b)、index(a)。 - -### 对于频繁的查询,优先考虑使用覆盖索引 - -> 覆盖索引:就是包含了所有查询字段 (where、select、order by、group by 包含的字段) 的索引 - -**覆盖索引的好处**: - -- **避免 InnoDB 表进行索引的二次查询,也就是回表操作**:InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询(回表),减少了 IO 操作,提升了查询效率。 -- **可以把随机 IO 变成顺序 IO 加快查询效率**:由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。 - ---- - -### 索引 SET 规范 - -**尽量避免使用外键约束** - -- 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引。 -- 外键可用于保证数据的参照完整性,但建议在业务端实现。 -- 外键会影响父表和子表的写操作从而降低性能。 - -## 数据库 SQL 开发规范 - -### 尽量不在数据库做运算,复杂运算需移到业务应用里完成 - -尽量不在数据库做运算,复杂运算需移到业务应用里完成。这样可以避免数据库的负担过重,影响数据库的性能和稳定性。数据库的主要作用是存储和管理数据,而不是处理数据。 - -### 优化对性能影响较大的 SQL 语句 - -要找到最需要优化的 SQL 语句。要么是使用最频繁的语句,要么是优化后提高最明显的语句,可以通过查询 MySQL 的慢查询日志来发现需要进行优化的 SQL 语句。 - -### 充分利用表上已经存在的索引 - -避免使用双%号的查询条件。如:`a like '%123%'`(如果无前置%,只有后置%,是可以用到列上的索引的)。 - -一个 SQL 只能利用到复合索引中的一列进行范围查询。如:有 a,b,c 列的联合索引,在查询条件中有 a 列的范围查询,则在 b,c 列上的索引将不会被用到。 - -在定义联合索引时,如果 a 列要用到范围查找的话,就要把 a 列放到联合索引的右侧,使用 left join 或 not exists 来优化 not in 操作,因为 not in 也通常会使用索引失效。 - -### 禁止使用 SELECT \* 必须使用 SELECT <字段列表> 查询 - -- `SELECT *` 会消耗更多的 CPU。 -- `SELECT *` 无用字段增加网络带宽资源消耗,增加数据传输时间,尤其是大字段(如 varchar、blob、text)。 -- `SELECT *` 无法使用 MySQL 优化器覆盖索引的优化(基于 MySQL 优化器的“覆盖索引”策略又是速度极快、效率极高、业界极为推荐的查询优化方式)。 -- `SELECT <字段列表>` 可减少表结构变更带来的影响。 - -### 禁止使用不含字段列表的 INSERT 语句 - -**不推荐**: - -```sql -insert into t values ('a','b','c'); -``` - -**推荐**: - -```sql -insert into t(c1,c2,c3) values ('a','b','c'); -``` - -### 建议使用预编译语句进行数据库操作 - -- 预编译语句可以重复使用这些计划,减少 SQL 编译所需要的时间,还可以解决动态 SQL 所带来的 SQL 注入的问题。 -- 只传参数,比传递 SQL 语句更高效。 -- 相同语句可以一次解析,多次使用,提高处理效率。 - -### 避免数据类型的隐式转换 - -隐式转换会导致索引失效,如: - -```sql -select name,phone from customer where id = '111'; -``` - -详细解读可以看:[MySQL 中的隐式转换造成的索引失效](./index-invalidation-caused-by-implicit-conversion.md) 这篇文章。 - -### 避免使用子查询,可以把子查询优化为 join 操作 - -通常子查询在 in 子句中,且子查询中为简单 SQL(不包含 union、group by、order by、limit 从句) 时,才可以把子查询转化为关联查询进行优化。 - -**子查询性能差的原因**:子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大。由于子查询会产生大量的临时表也没有索引,所以会消耗过多的 CPU 和 IO 资源,产生大量的慢查询。 - -### 避免使用 JOIN 关联太多的表 - -对于 MySQL 来说,是存在关联缓存的,缓存的大小可以由 join_buffer_size 参数进行设置。 - -在 MySQL 中,对于同一个 SQL 多关联(join)一个表,就会多分配一个关联缓存,如果在一个 SQL 中关联的表越多,所占用的内存也就越大。 - -如果程序中大量地使用了多表关联的操作,同时 join_buffer_size 设置得也不合理,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。 - -同时对于关联操作来说,会产生临时表操作,影响查询效率,MySQL 最多允许关联 61 个表,建议不超过 5 个。 - -### 减少同数据库的交互次数 - -数据库更适合处理批量操作,合并多个相同的操作到一起,可以提高处理效率。 - -### 对应同一列进行 or 判断时,使用 in 代替 or - -in 的值不要超过 500 个。in 操作可以更有效的利用索引,or 大多数情况下很少能利用到索引。 - -### 禁止使用 order by rand() 进行随机排序 - -order by rand() 会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值。如果满足条件的数据集非常大,就会消耗大量的 CPU 和 IO 及内存资源。 - -推荐在程序中获取一个随机值,然后从数据库中获取数据的方式。 - -### WHERE 从句中禁止对列进行函数转换和计算 - -对列进行函数转换或计算时会导致无法使用索引。 - -**不推荐**: - -```sql -where date(create_time)='20190101' -``` - -**推荐**: - -```sql -where create_time >= '20190101' and create_time < '20190102' -``` - -### 在明显不会有重复值时使用 UNION ALL 而不是 UNION - -- UNION 会把两个结果集的所有数据放到临时表中后再进行去重操作。 -- UNION ALL 不会再对结果集进行去重操作。 - -### 拆分复杂的大 SQL 为多个小 SQL - -- 大 SQL 逻辑上比较复杂,需要占用大量 CPU 进行计算的 SQL。 -- MySQL 中,一个 SQL 只能使用一个 CPU 进行计算。 -- SQL 拆分后可以通过并行执行来提高处理效率。 - -### 程序连接不同的数据库使用不同的账号,禁止跨库查询 - -- 为数据库迁移和分库分表留出余地。 -- 降低业务耦合度。 -- 避免权限过大而产生的安全风险。 - -## 数据库操作行为规范 - -### 超 100 万行的批量写 (UPDATE、DELETE、INSERT) 操作,要分批多次进行操作 - -**大批量操作可能会造成严重的主从延迟** - -主从环境中,大批量操作可能会造成严重的主从延迟,大批量的写操作一般都需要执行一定长的时间,而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况。 - -**binlog 日志为 row 格式时会产生大量的日志** - -大批量写操作会产生大量日志,特别是对于 row 格式二进制数据而言,由于在 row 格式中会记录每一行数据的修改,我们一次修改的数据越多,产生的日志量也就会越多,日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因。 - -**避免产生大事务操作** - -大批量修改数据,一定是在一个事务中进行的,这就会造成表中大批量数据进行锁定,从而导致大量的阻塞,阻塞会对 MySQL 的性能产生非常大的影响。 - -特别是长时间的阻塞会占满所有数据库的可用连接,这会使生产环境中的其他应用无法连接到数据库,因此一定要注意大批量写操作要进行分批。 - -### 对于大表使用 pt-online-schema-change 修改表结构 - -- 避免大表修改产生的主从延迟。 -- 避免在对表字段进行修改时进行锁表。 - -对大表数据结构的修改一定要谨慎,会造成严重的锁表操作,尤其是生产环境,是不能容忍的。 - -pt-online-schema-change 它会首先建立一个与原表结构相同的新表,并且在新表上进行表结构的修改,然后再把原表中的数据复制到新表中,并在原表中增加一些触发器。把原表中新增的数据也复制到新表中,在行所有数据复制完成之后,把新表命名成原表,并把原来的表删除掉。把原来一个 DDL 操作,分解成多个小的批次进行。 - -### 禁止为程序使用的账号赋予 super 权限 - -- 当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接。 -- super 权限只能留给 DBA 处理问题的账号使用。 - -### 对于程序连接数据库账号,遵循权限最小原则 - -- 程序使用数据库账号只能在一个 DB 下使用,不准跨库。 -- 程序使用的账号原则上不准有 drop 权限。 - -## 推荐阅读 - -- [技术同学必会的 MySQL 设计规约,都是惨痛的教训 - 阿里开发者](https://mp.weixin.qq.com/s/XC8e5iuQtfsrEOERffEZ-Q) -- [聊聊数据库建表的 15 个小技巧](https://mp.weixin.qq.com/s/NM-aHaW6TXrnO6la6Jfl5A) - - +\*\*b. For diff --git a/docs/database/mysql/mysql-index.md b/docs/database/mysql/mysql-index.md index a21d133feea..b47f742f8b8 100644 --- a/docs/database/mysql/mysql-index.md +++ b/docs/database/mysql/mysql-index.md @@ -1,47 +1,47 @@ --- -title: MySQL索引详解 -category: 数据库 +title: MySQL Index Detailed Explanation +category: Database tag: - MySQL --- -> 感谢[WT-AHA](https://github.com/WT-AHA)对本文的完善,相关 PR: 。 +> Thanks to [WT-AHA](https://github.com/WT-AHA) for improving this article, related PR: . -但凡经历过几场面试的小伙伴,应该都清楚,数据库索引这个知识点在面试中出现的频率高到离谱。 +Anyone who has gone through a few interviews should be well aware that the topic of database indexing appears with alarming frequency in interviews. -除了对于准备面试来说非常重要之外,善用索引对 SQL 的性能提升非常明显,是一个性价比较高的 SQL 优化手段。 +In addition to being very important for interview preparation, effectively using indexes can significantly improve SQL performance, making it a cost-effective SQL optimization technique. -## 索引介绍 +## Introduction to Indexes -**索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。** +**An index is a data structure used for fast querying and retrieval of data, essentially functioning as a sorted data structure.** -索引的作用就相当于书的目录。打个比方:我们在查字典的时候,如果没有目录,那我们就只能一页一页地去找我们需要查的那个字,速度很慢;如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。 +The role of an index is similar to that of a table of contents in a book. For example, when we look up a word in a dictionary, without a table of contents, we would have to search page by page for the word we need, which is very slow; with a table of contents, we can first find the location of the word in the table and then directly turn to that page. -索引底层数据结构存在很多种类型,常见的索引结构有:B 树、 B+ 树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyISAM,都使用了 B+ 树作为索引结构。 +There are many types of underlying data structures for indexes, with common index structures including B-trees, B+ trees, Hash, and Red-Black trees. In MySQL, both InnoDB and MyISAM use B+ trees as their index structure. -## 索引的优缺点 +## Advantages and Disadvantages of Indexes -**优点**: +**Advantages**: -- 使用索引可以大大加快数据的检索速度(大大减少检索的数据量),减少 IO 次数,这也是创建索引的最主要的原因。 -- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 +- Using indexes can greatly speed up data retrieval (significantly reducing the amount of data scanned) and decrease the number of I/O operations, which is the primary reason for creating indexes. +- By creating unique indexes, we can ensure the uniqueness of each row of data in the database table. -**缺点**: +**Disadvantages**: -- 创建和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态地修改,这会降低 SQL 执行效率。 -- 索引需要使用物理文件存储,也会耗费一定空间。 +- Creating and maintaining indexes can be time-consuming. When performing insert, delete, or update operations on data in a table, if the data has indexes, those indexes also need to be dynamically modified, which can reduce SQL execution efficiency. +- Indexes require physical file storage, which also consumes space. -但是,**使用索引一定能提高查询性能吗?** +However, **can using indexes always improve query performance?** -大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。 +In most cases, indexed queries are faster than full table scans. However, if the amount of data in the database is not large, using indexes may not bring significant improvements. -## 索引底层数据结构选型 +## Selection of Underlying Data Structures for Indexes -### Hash 表 +### Hash Table -哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。 +A hash table is a collection of key-value pairs, allowing for quick retrieval of the corresponding value through the key, thus enabling fast data retrieval (close to O(1)). -**为何能够通过 key 快速取出 value 呢?** 原因在于 **哈希算法**(也叫散列算法)。通过哈希算法,我们可以快速找到 key 对应的 index,找到了 index 也就找到了对应的 value。 +**Why can we quickly retrieve the value through the key?** The reason lies in the **hash algorithm**. By using a hash algorithm, we can quickly find the index corresponding to the key, and once we have the index, we can find the corresponding value. ```java hash = hashfunc(key) @@ -50,500 +50,17 @@ index = hash % array_size ![](https://oss.javaguide.cn/github/javaguide/database/mysql20210513092328171.png) -但是!哈希算法有个 **Hash 冲突** 问题,也就是说多个不同的 key 最后得到的 index 相同。通常情况下,我们常用的解决办法是 **链地址法**。链地址法就是将哈希冲突数据存放在链表中。就比如 JDK1.8 之前 `HashMap` 就是通过链地址法来解决哈希冲突的。不过,JDK1.8 以后`HashMap`为了提高链表过长时的搜索效率,引入了红黑树。 +However! The hash algorithm has a **hash collision** problem, meaning that multiple different keys can end up with the same index. Typically, the common solution is the **chaining method**, which stores the colliding data in a linked list. For example, before JDK 1.8, `HashMap` used the chaining method to resolve hash collisions. However, after JDK 1.8, `HashMap` introduced Red-Black trees to improve search efficiency when the linked list becomes too long. ![](https://oss.javaguide.cn/github/javaguide/database/mysql20210513092224836.png) -为了减少 Hash 冲突的发生,一个好的哈希函数应该“均匀地”将数据分布在整个可能的哈希值集合中。 +To reduce the occurrence of hash collisions, a good hash function should "evenly" distribute data across the entire possible set of hash values. -MySQL 的 InnoDB 存储引擎不直接支持常规的哈希索引,但是,InnoDB 存储引擎中存在一种特殊的“自适应哈希索引”(Adaptive Hash Index),自适应哈希索引并不是传统意义上的纯哈希索引,而是结合了 B+Tree 和哈希索引的特点,以便更好地适应实际应用中的数据访问模式和性能需求。自适应哈希索引的每个哈希桶实际上是一个小型的 B+Tree 结构。这个 B+Tree 结构可以存储多个键值对,而不仅仅是一个键。这有助于减少哈希冲突链的长度,提高了索引的效率。关于 Adaptive Hash Index 的详细介绍,可以查看 [MySQL 各种“Buffer”之 Adaptive Hash Index](https://mp.weixin.qq.com/s/ra4v1XR5pzSWc-qtGO-dBg) 这篇文章。 +The InnoDB storage engine in MySQL does not directly support conventional hash indexes. However, there is a special "Adaptive Hash Index" in the InnoDB storage engine. The Adaptive Hash Index is not a pure hash index in the traditional sense but combines the characteristics of B+ trees and hash indexes to better adapt to actual data access patterns and performance requirements. Each hash bucket in the Adaptive Hash Index is actually a small B+ tree structure. This B+ tree structure can store multiple key-value pairs, not just a single key. This helps reduce the length of hash collision chains and improves index efficiency. For a detailed introduction to the Adaptive Hash Index, you can refer to the article [MySQL Various "Buffer" and Adaptive Hash Index](https://mp.weixin.qq.com/s/ra4v1XR5pzSWc-qtGO-dBg). -既然哈希表这么快,**为什么 MySQL 没有使用其作为索引的数据结构呢?** 主要是因为 Hash 索引不支持顺序和范围查询。假如我们要对表中的数据进行排序或者进行范围查询,那 Hash 索引可就不行了。并且,每次 IO 只能取一个。 +Since hash tables are so fast, **why doesn't MySQL use them as the index data structure?** The main reason is that hash indexes do not support ordered and range queries. If we need to sort data in a table or perform a range query, hash indexes are not suitable. Additionally, each I/O operation can only retrieve one item. -试想一种情况: +Consider a situation: ```java -SELECT * FROM tb1 WHERE id < 500; -``` - -在这种范围查询中,优势非常大,直接遍历比 500 小的叶子节点就够了。而 Hash 索引是根据 hash 算法来定位的,难不成还要把 1 - 499 的数据,每个都进行一次 hash 计算来定位吗?这就是 Hash 最大的缺点了。 - -### 二叉查找树(BST) - -二叉查找树(Binary Search Tree)是一种基于二叉树的数据结构,它具有以下特点: - -1. 左子树所有节点的值均小于根节点的值。 -2. 右子树所有节点的值均大于根节点的值。 -3. 左右子树也分别为二叉查找树。 - -当二叉查找树是平衡的时候,也就是树的每个节点的左右子树深度相差不超过 1 的时候,查询的时间复杂度为 O(log2(N)),具有比较高的效率。然而,当二叉查找树不平衡时,例如在最坏情况下(有序插入节点),树会退化成线性链表(也被称为斜树),导致查询效率急剧下降,时间复杂退化为 O(N)。 - -![斜树](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/oblique-tree.png) - -也就是说,**二叉查找树的性能非常依赖于它的平衡程度,这就导致其不适合作为 MySQL 底层索引的数据结构。** - -为了解决这个问题,并提高查询效率,人们发明了多种在二叉查找树基础上的改进型数据结构,如平衡二叉树、B-Tree、B+Tree 等。 - -### AVL 树 - -AVL 树是计算机科学中最早被发明的自平衡二叉查找树,它的名称来自于发明者 G.M. Adelson-Velsky 和 E.M. Landis 的名字缩写。AVL 树的特点是保证任何节点的左右子树高度之差不超过 1,因此也被称为高度平衡二叉树,它的查找、插入和删除在平均和最坏情况下的时间复杂度都是 O(logn)。 - -![](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/avl-tree.png) - -AVL 树采用了旋转操作来保持平衡。主要有四种旋转操作:LL 旋转、RR 旋转、LR 旋转和 RL 旋转。其中 LL 旋转和 RR 旋转分别用于处理左左和右右失衡,而 LR 旋转和 RL 旋转则用于处理左右和右左失衡。 - -由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了数据库写操作的性能。并且, 在使用 AVL 树时,每个树节点仅存储一个数据,而每次进行磁盘 IO 时只能读取一个节点的数据,如果需要查询的数据分布在多个节点上,那么就需要进行多次磁盘 IO。**磁盘 IO 是一项耗时的操作,在设计数据库索引时,我们需要优先考虑如何最大限度地减少磁盘 IO 操作的次数。** - -实际应用中,AVL 树使用的并不多。 - -### 红黑树 - -红黑树是一种自平衡二叉查找树,通过在插入和删除节点时进行颜色变换和旋转操作,使得树始终保持平衡状态,它具有以下特点: - -1. 每个节点非红即黑; -2. 根节点总是黑色的; -3. 每个叶子节点都是黑色的空节点(NIL 节点); -4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定); -5. 从任意节点到它的叶子节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。 - -![红黑树](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/red-black-tree.png) - -和 AVL 树不同的是,红黑树并不追求严格的平衡,而是大致的平衡。正因如此,红黑树的查询效率稍有下降,因为红黑树的平衡性相对较弱,可能会导致树的高度较高,这可能会导致一些数据需要进行多次磁盘 IO 操作才能查询到,这也是 MySQL 没有选择红黑树的主要原因。也正因如此,红黑树的插入和删除操作效率大大提高了,因为红黑树在插入和删除节点时只需进行 O(1) 次数的旋转和变色操作,即可保持基本平衡状态,而不需要像 AVL 树一样进行 O(logn) 次数的旋转操作。 - -**红黑树的应用还是比较广泛的,TreeMap、TreeSet 以及 JDK1.8 的 HashMap 底层都用到了红黑树。对于数据在内存中的这种情况来说,红黑树的表现是非常优异的。** - -### B 树& B+ 树 - -B 树也称 B- 树,全称为 **多路平衡查找树**,B+ 树是 B 树的一种变体。B 树和 B+ 树中的 B 是 `Balanced`(平衡)的意思。 - -目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。 - -**B 树& B+ 树两者有何异同呢?** - -- B 树的所有节点既存放键(key)也存放数据(data),而 B+ 树只有叶子节点存放 key 和 data,其他内节点只存放 key。 -- B 树的叶子节点都是独立的;B+ 树的叶子节点有一条引用链指向与它相邻的叶子节点。 -- B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+ 树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。 -- 在 B 树中进行范围查询时,首先找到要查找的下限,然后对 B 树进行中序遍历,直到找到查找的上限;而 B+ 树的范围查询,只需要对链表进行遍历即可。 - -综上,B+ 树与 B 树相比,具备更少的 IO 次数、更稳定的查询效率和更适于范围查询这些优势。 - -在 MySQL 中,MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是,两者的实现方式不太一样。(下面的内容整理自《Java 工程师修炼之道》) - -> MyISAM 引擎中,B+Tree 叶节点的 data 域存放的是数据记录的地址。在索引检索的时候,首先按照 B+Tree 搜索算法搜索索引,如果指定的 Key 存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“**非聚簇索引(非聚集索引)**”。 -> -> InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。这被称为“**聚簇索引(聚集索引)**”,而其余的索引都作为 **辅助索引**,辅助索引的 data 域存储相应记录主键的值而不是地址,这也是和 MyISAM 不同的地方。在根据主索引搜索时,直接找到 key 所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。 - -## 索引类型总结 - -按照数据结构维度划分: - -- BTree 索引:MySQL 里默认和最常用的索引类型。只有叶子节点存储 value,非叶子节点只有指针和 key。存储引擎 MyISAM 和 InnoDB 实现 BTree 索引都是使用 B+Tree,但二者实现方式不一样(前面已经介绍了)。 -- 哈希索引:类似键值对的形式,一次即可定位。 -- RTree 索引:一般不会使用,仅支持 geometry 数据类型,优势在于范围查找,效率较低,通常使用搜索引擎如 ElasticSearch 代替。 -- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR`、`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。 - -按照底层存储方式角度划分: - -- 聚簇索引(聚集索引):索引结构和数据一起存放的索引,InnoDB 中的主键索引就属于聚簇索引。 -- 非聚簇索引(非聚集索引):索引结构和数据分开存放的索引,二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。 - -按照应用维度划分: - -- 主键索引:加速查询 + 列值唯一(不可以有 NULL)+ 表中只有一个。 -- 普通索引:仅加速查询。 -- 唯一索引:加速查询 + 列值唯一(可以有 NULL)。 -- 覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。 -- 联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。 -- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR`、`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。 -- 前缀索引:对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。 - -MySQL 8.x 中实现的索引新特性: - -- 隐藏索引:也称为不可见索引,不会被优化器使用,但是仍然需要维护,通常会软删除和灰度发布的场景中使用。主键不能设置为隐藏(包括显式设置或隐式设置)。 -- 降序索引:之前的版本就支持通过 desc 来指定索引为降序,但实际上创建的仍然是常规的升序索引。直到 MySQL 8.x 版本才开始真正支持降序索引。另外,在 MySQL 8.x 版本中,不再对 GROUP BY 语句进行隐式排序。 -- 函数索引:从 MySQL 8.0.13 版本开始支持在索引中使用函数或者表达式的值,也就是在索引中可以包含函数或者表达式。 - -## 主键索引(Primary Key) - -数据表的主键列使用的就是主键索引。 - -一张数据表有只能有一个主键,并且主键不能为 null,不能重复。 - -在 MySQL 的 InnoDB 的表中,当没有显示的指定表的主键时,InnoDB 会自动先检查表中是否有唯一索引且不允许存在 null 值的字段,如果有,则选择该字段为默认的主键,否则 InnoDB 将会自动创建一个 6Byte 的自增主键。 - -![主键索引](https://oss.javaguide.cn/github/javaguide/open-source-project/cluster-index.png) - -## 二级索引 - -二级索引(Secondary Index)的叶子节点存储的数据是主键的值,也就是说,通过二级索引可以定位主键的位置,二级索引又称为辅助索引/非主键索引。 - -唯一索引、普通索引、前缀索引等索引都属于二级索引。 - -PS:不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,也可以自行搜索。 - -1. **唯一索引(Unique Key)**:唯一索引也是一种约束。唯一索引的属性列不能出现重复的数据,但是允许数据为 NULL,一张表允许创建多个唯一索引。 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。 -2. **普通索引(Index)**:普通索引的唯一作用就是为了快速查询数据。一张表允许创建多个普通索引,并允许数据重复和 NULL。 -3. **前缀索引(Prefix)**:前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。 -4. **全文索引(Full Text)**:全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MyISAM 引擎支持全文索引,5.6 之后 InnoDB 也支持了全文索引。 - -二级索引: - -![二级索引](https://oss.javaguide.cn/github/javaguide/open-source-project/no-cluster-index.png) - -## 聚簇索引与非聚簇索引 - -### 聚簇索引(聚集索引) - -#### 聚簇索引介绍 - -聚簇索引(Clustered Index)即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。 - -在 MySQL 中,InnoDB 引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+ 树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。 - -#### 聚簇索引的优缺点 - -**优点**: - -- **查询速度非常快**:聚簇索引的查询速度非常的快,因为整个 B+ 树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。相比于非聚簇索引, 聚簇索引少了一次读取数据的 IO 操作。 -- **对排序查找和范围查找优化**:聚簇索引对于主键的排序查找和范围查找速度非常快。 - -**缺点**: - -- **依赖于有序的数据**:因为 B+ 树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。 -- **更新代价大**:如果对索引列的数据被修改时,那么对应的索引也将会被修改,而且聚簇索引的叶子节点还存放着数据,修改代价肯定是较大的,所以对于主键索引来说,主键一般都是不可被修改的。 - -### 非聚簇索引(非聚集索引) - -#### 非聚簇索引介绍 - -非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。 - -非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。 - -#### 非聚簇索引的优缺点 - -**优点**: - -更新代价比聚簇索引要小。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的。 - -**缺点**: - -- **依赖于有序的数据**:跟聚簇索引一样,非聚簇索引也依赖于有序的数据。 -- **可能会二次查询(回表)**:这应该是非聚簇索引最大的缺点了。当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。 - -这是 MySQL 的表的文件截图: - -![MySQL 表的文件](https://oss.javaguide.cn/github/javaguide/database/mysql20210420165311654.png) - -聚簇索引和非聚簇索引: - -![聚簇索引和非聚簇索引](https://oss.javaguide.cn/github/javaguide/database/mysql20210420165326946.png) - -#### 非聚簇索引一定回表查询吗(覆盖索引)? - -**非聚簇索引不一定回表查询。** - -试想一种情况,用户准备使用 SQL 查询用户名,而用户名字段正好建立了索引。 - -```sql - SELECT name FROM table WHERE name='guang19'; -``` - -那么这个索引的 key 本身就是 name,查到对应的 name 直接返回就行了,无需回表查询。 - -即使是 MyISAM 也是这样,虽然 MyISAM 的主键索引确实需要回表,因为它的主键索引的叶子节点存放的是指针。但是!**如果 SQL 查的就是主键呢?** - -```sql -SELECT id FROM table WHERE id=1; -``` - -主键索引本身的 key 就是主键,查到返回就行了。这种情况就称之为覆盖索引了。 - -## 覆盖索引和联合索引 - -### 覆盖索引 - -如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)**。 - -在 InnoDB 存储引擎中,非主键索引的叶子节点包含的是主键的值。这意味着,当使用非主键索引进行查询时,数据库会先找到对应的主键值,然后再通过主键索引来定位和检索完整的行数据。这个过程被称为“回表”。 - -**覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,而无需回表查询。** - -> 如主键索引,如果一条 SQL 需要查询主键,那么正好根据主键索引就可以查到主键。再如普通索引,如果一条 SQL 需要查询 name,name 字段正好有索引, -> 那么直接根据这个索引就可以查到数据,也无需回表。 - -![覆盖索引](https://oss.javaguide.cn/github/javaguide/database/mysql20210420165341868.png) - -我们这里简单演示一下覆盖索引的效果。 - -1、创建一个名为 `cus_order` 的表,来实际测试一下这种排序方式。为了测试方便,`cus_order` 这张表只有 `id`、`score`、`name` 这 3 个字段。 - -```sql -CREATE TABLE `cus_order` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `score` int(11) NOT NULL, - `name` varchar(11) NOT NULL DEFAULT '', - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8mb4; -``` - -2、定义一个简单的存储过程(PROCEDURE)来插入 100w 测试数据。 - -```sql -DELIMITER ;; -CREATE DEFINER=`root`@`%` PROCEDURE `BatchinsertDataToCusOder`(IN start_num INT,IN max_num INT) -BEGIN - DECLARE i INT default start_num; - WHILE i < max_num DO - insert into `cus_order`(`id`, `score`, `name`) - values (i,RAND() * 1000000,CONCAT('user', i)); - SET i = i + 1; - END WHILE; - END;; -DELIMITER ; -``` - -存储过程定义完成之后,我们执行存储过程即可! - -```sql -CALL BatchinsertDataToCusOder(1, 1000000); # 插入100w+的随机数据 -``` - -等待一会,100w 的测试数据就插入完成了! - -3、创建覆盖索引并使用 `EXPLAIN` 命令分析。 - -为了能够对这 100w 数据按照 `score` 进行排序,我们需要执行下面的 SQL 语句。 - -```sql -#降序排序 -SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC; -``` - -使用 `EXPLAIN` 命令分析这条 SQL 语句,通过 `Extra` 这一列的 `Using filesort`,我们发现是没有用到覆盖索引的。 - -![](https://oss.javaguide.cn/github/javaguide/mysql/not-using-covering-index-demo.png) - -不过这也是理所应当,毕竟我们现在还没有创建索引呢! - -我们这里以 `score` 和 `name` 两个字段建立联合索引: - -```sql -ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name); -``` - -创建完成之后,再用 `EXPLAIN` 命令分析再次分析这条 SQL 语句。 - -![](https://oss.javaguide.cn/github/javaguide/mysql/using-covering-index-demo.png) - -通过 `Extra` 这一列的 `Using index`,说明这条 SQL 语句成功使用了覆盖索引。 - -关于 `EXPLAIN` 命令的详细介绍请看:[MySQL 执行计划分析](./mysql-query-execution-plan.md)这篇文章。 - -### 联合索引 - -使用表中的多个字段创建索引,就是 **联合索引**,也叫 **组合索引** 或 **复合索引**。 - -以 `score` 和 `name` 两个字段建立联合索引: - -```sql -ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name); -``` - -### 最左前缀匹配原则 - -最左前缀匹配原则指的是在使用联合索引时,MySQL 会根据索引中的字段顺序,从左到右依次匹配查询条件中的字段。如果查询条件与索引中的最左侧字段相匹配,那么 MySQL 就会使用索引来过滤数据,这样可以提高查询效率。 - -最左匹配原则会一直向右匹配,直到遇到范围查询(如 >、<)为止。对于 >=、<=、BETWEEN 以及前缀匹配 LIKE 的范围查询,不会停止匹配(相关阅读:[联合索引的最左匹配原则全网都在说的一个错误结论](https://mp.weixin.qq.com/s/8qemhRg5MgXs1So5YCv0fQ))。 - -假设有一个联合索引 `(column1, column2, column3)`,其从左到右的所有前缀为 `(column1)`、`(column1, column2)`、`(column1, column2, column3)`(创建 1 个联合索引相当于创建了 3 个索引),包含这些列的所有查询都会走索引而不会全表扫描。 - -我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。 - -我们这里简单演示一下最左前缀匹配的效果。 - -1、创建一个名为 `student` 的表,这张表只有 `id`、`name`、`class` 这 3 个字段。 - -```sql -CREATE TABLE `student` ( - `id` int NOT NULL, - `name` varchar(100) DEFAULT NULL, - `class` varchar(100) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `name_class_idx` (`name`,`class`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -2、下面我们分别测试三条不同的 SQL 语句。 - -![](https://oss.javaguide.cn/github/javaguide/database/mysql/leftmost-prefix-matching-rule.png) - -```sql -# 可以命中索引 -SELECT * FROM student WHERE name = 'Anne Henry'; -EXPLAIN SELECT * FROM student WHERE name = 'Anne Henry' AND class = 'lIrm08RYVk'; -# 无法命中索引 -SELECT * FROM student WHERE class = 'lIrm08RYVk'; -``` - -再来看一个常见的面试题:如果有索引 `联合索引(a,b,c)`,查询 `a=1 AND c=1` 会走索引么?`c=1` 呢?`b=1 AND c=1` 呢? - -先不要往下看答案,给自己 3 分钟时间想一想。 - -1. 查询 `a=1 AND c=1`:根据最左前缀匹配原则,查询可以使用索引的前缀部分。因此,该查询仅在 `a=1` 上使用索引,然后对结果进行 `c=1` 的过滤。 -2. 查询 `c=1`:由于查询中不包含最左列 `a`,根据最左前缀匹配原则,整个索引都无法被使用。 -3. 查询 `b=1 AND c=1`:和第二种一样的情况,整个索引都不会使用。 - -MySQL 8.0.13 版本引入了索引跳跃扫描(Index Skip Scan,简称 ISS),它可以在某些索引查询场景下提高查询效率。在没有 ISS 之前,不满足最左前缀匹配原则的联合索引查询中会执行全表扫描。而 ISS 允许 MySQL 在某些情况下避免全表扫描,即使查询条件不符合最左前缀。不过,这个功能比较鸡肋, 和 Oracle 中的没法比,MySQL 8.0.31 还报告了一个 bug:[Bug #109145 Using index for skip scan cause incorrect result](https://bugs.mysql.com/bug.php?id=109145)(后续版本已经修复)。个人建议知道有这个东西就好,不需要深究,实际项目也不一定能用上。 - -## 索引下推 - -**索引下推(Index Condition Pushdown,简称 ICP)** 是 **MySQL 5.6** 版本中提供的一项索引优化功能,它允许存储引擎在索引遍历过程中,执行部分 `WHERE` 字句的判断条件,直接过滤掉不满足条件的记录,从而减少回表次数,提高查询效率。 - -假设我们有一个名为 `user` 的表,其中包含 `id`、`username`、`zipcode` 和 `birthdate` 4 个字段,创建了联合索引 `(zipcode, birthdate)`。 - -```sql -CREATE TABLE `user` ( - `id` int NOT NULL AUTO_INCREMENT, - `username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, - `zipcode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, - `birthdate` date NOT NULL, - PRIMARY KEY (`id`), - KEY `idx_username_birthdate` (`zipcode`,`birthdate`) ) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8mb4; - -# 查询 zipcode 为 431200 且生日在 3 月的用户 -# birthdate 字段使用函数索引失效 -SELECT * FROM user WHERE zipcode = '431200' AND MONTH(birthdate) = 3; -``` - -- 没有索引下推之前,即使 `zipcode` 字段利用索引可以帮助我们快速定位到 `zipcode = '431200'` 的用户,但我们仍然需要对每一个找到的用户进行回表操作,获取完整的用户数据,再去判断 `MONTH(birthdate) = 3`。 -- 有了索引下推之后,存储引擎会在使用 `zipcode` 字段索引查找 `zipcode = '431200'` 的用户时,同时判断 `MONTH(birthdate) = 3`。这样,只有同时满足条件的记录才会被返回,减少了回表次数。 - -![](https://oss.javaguide.cn/github/javaguide/database/mysql/index-condition-pushdown.png) - -![](https://oss.javaguide.cn/github/javaguide/database/mysql/index-condition-pushdown-graphic-illustration.png) - -再来讲讲索引下推的具体原理,先看下面这张 MySQL 简要架构图。 - -![](https://oss.javaguide.cn/javaguide/13526879-3037b144ed09eb88.png) - -MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处理查询解析、分析、优化、缓存以及与客户端的交互等操作,而存储引擎层负责数据的存储和读取,MySQL 支持 InnoDB、MyISAM、Memory 等多种存储引擎。 - -索引下推的 **下推** 其实就是指将部分上层(Server 层)负责的事情,交给了下层(存储引擎层)去处理。 - -我们这里结合索引下推原理再对上面提到的例子进行解释。 - -没有索引下推之前: - -- 存储引擎层先根据 `zipcode` 索引字段找到所有 `zipcode = '431200'` 的用户的主键 ID,然后二次回表查询,获取完整的用户数据; -- 存储引擎层把所有 `zipcode = '431200'` 的用户数据全部交给 Server 层,Server 层根据 `MONTH(birthdate) = 3` 这一条件再进一步做筛选。 - -有了索引下推之后: - -- 存储引擎层先根据 `zipcode` 索引字段找到所有 `zipcode = '431200'` 的用户,然后直接判断 `MONTH(birthdate) = 3`,筛选出符合条件的主键 ID; -- 二次回表查询,根据符合条件的主键 ID 去获取完整的用户数据; -- 存储引擎层把符合条件的用户数据全部交给 Server 层。 - -可以看出,**除了可以减少回表次数之外,索引下推还可以减少存储引擎层和 Server 层的数据传输量。** - -最后,总结一下索引下推应用范围: - -1. 适用于 InnoDB 引擎和 MyISAM 引擎的查询。 -2. 适用于执行计划是 range、ref、eq_ref、ref_or_null 的范围查询。 -3. 对于 InnoDB 表,仅用于非聚簇索引。索引下推的目标是减少全行读取次数,从而减少 I/O 操作。对于 InnoDB 聚集索引,完整的记录已经读入 InnoDB 缓冲区。在这种情况下使用索引下推不会减少 I/O。 -4. 子查询不能使用索引下推,因为子查询通常会创建临时表来处理结果,而这些临时表是没有索引的。 -5. 存储过程不能使用索引下推,因为存储引擎无法调用存储函数。 - -## 正确使用索引的一些建议 - -### 选择合适的字段创建索引 - -- **不为 NULL 的字段**:索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0、1、true、false 这样语义较为清晰的短值或短字符作为替代。 -- **被频繁查询的字段**:我们创建索引的字段应该是查询操作非常频繁的字段。 -- **被作为条件查询的字段**:被作为 WHERE 条件查询的字段,应该被考虑建立索引。 -- **频繁需要排序的字段**:索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。 -- **被经常频繁用于连接的字段**:经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。 - -### 被频繁更新的字段应该慎重建立索引 - -虽然索引能带来查询上的效率,但是维护索引的成本也是不小的。 如果一个字段不被经常查询,反而被经常修改,那么就更不应该在这种字段上建立索引了。 - -### 限制每张表上的索引数量 - -索引并不是越多越好,建议单张表索引不超过 5 个!索引可以提高效率,同样可以降低效率。 - -索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。 - -因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。 - -### 尽可能的考虑建立联合索引而不是单列索引 - -因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+ 树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。 - -### 注意避免冗余索引 - -冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city)和(name)这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的。在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。 - -### 字符串类型的字段使用前缀索引代替普通索引 - -前缀索引仅限于字符串类型,较普通索引会占用更小的空间,所以可以考虑使用前缀索引带替普通索引。 - -### 避免索引失效 - -索引失效也是慢查询的主要原因之一,常见的导致索引失效的情况有下面这些: - -- ~~使用 `SELECT *` 进行查询;~~ `SELECT *` 不会直接导致索引失效(如果不走索引大概率是因为 where 查询范围过大导致的),但它可能会带来一些其他的性能问题比如造成网络传输和数据处理的浪费、无法使用索引覆盖; -- 创建了组合索引,但查询条件未遵守最左匹配原则; -- 在索引列上进行计算、函数、类型转换等操作; -- 以 % 开头的 LIKE 查询比如 `LIKE '%abc';`; -- 查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到; -- IN 的取值范围较大时会导致索引失效,走全表扫描(NOT IN 和 IN 的失效场景相同); -- 发生[隐式转换](https://javaguide.cn/database/mysql/index-invalidation-caused-by-implicit-conversion.html); -- …… - -推荐阅读这篇文章:[美团暑期实习一面:MySQl 索引失效的场景有哪些?](https://mp.weixin.qq.com/s/mwME3qukHBFul57WQLkOYg)。 - -### 删除长期未使用的索引 - -删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗。 - -MySQL 5.7 可以通过查询 `sys` 库的 `schema_unused_indexes` 视图来查询哪些索引从未被使用。 - -### 知道如何分析 SQL 语句是否走索引查询 - -我们可以使用 `EXPLAIN` 命令来分析 SQL 的 **执行计划** ,这样就知道语句是否命中索引了。执行计划是指一条 SQL 语句在经过 MySQL 查询优化器的优化会后,具体的执行方式。 - -`EXPLAIN` 并不会真的去执行相关的语句,而是通过 **查询优化器** 对语句进行分析,找出最优的查询方案,并显示对应的信息。 - -`EXPLAIN` 的输出格式如下: - -```sql -mysql> EXPLAIN SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC; -+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+ -| 1 | SIMPLE | cus_order | NULL | ALL | NULL | NULL | NULL | NULL | 997572 | 100.00 | Using filesort | -+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+ -1 row in set, 1 warning (0.00 sec) -``` - -各个字段的含义如下: - -| **列名** | **含义** | -| ------------- | -------------------------------------------- | -| id | SELECT 查询的序列标识符 | -| select_type | SELECT 关键字对应的查询类型 | -| table | 用到的表名 | -| partitions | 匹配的分区,对于未分区的表,值为 NULL | -| type | 表的访问方法 | -| possible_keys | 可能用到的索引 | -| key | 实际用到的索引 | -| key_len | 所选索引的长度 | -| ref | 当使用索引等值查询时,与索引作比较的列或常量 | -| rows | 预计要读取的行数 | -| filtered | 按表条件过滤后,留存的记录数的百分比 | -| Extra | 附加信息 | - -篇幅问题,我这里只是简单介绍了一下 MySQL 执行计划,详细介绍请看:[MySQL 执行计划分析](./mysql-query-execution-plan.md)这篇文章。 - - +SELECT * FROM \ No newline at end of file diff --git a/docs/database/mysql/mysql-logs.md b/docs/database/mysql/mysql-logs.md index ac7e29db2f3..d48a84b483e 100644 --- a/docs/database/mysql/mysql-logs.md +++ b/docs/database/mysql/mysql-logs.md @@ -1,348 +1,65 @@ --- -title: MySQL三大日志(binlog、redo log和undo log)详解 -category: 数据库 +title: Detailed Explanation of MySQL's Three Major Logs (binlog, redo log, and undo log) +category: Database tag: - MySQL --- -> 本文来自公号程序猿阿星投稿,JavaGuide 对其做了补充完善。 +> This article is contributed by the public account Programmer A Xing, and JavaGuide has made supplementary improvements. -## 前言 +## Introduction -MySQL 日志 主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中,比较重要的还要属二进制日志 binlog(归档日志)和事务日志 redo log(重做日志)和 undo log(回滚日志)。 +MySQL logs mainly include error logs, query logs, slow query logs, transaction logs, and binary logs. Among these, the most important are the binary log (binlog), redo log, and undo log. ![](https://oss.javaguide.cn/github/javaguide/01.png) -今天就来聊聊 redo log(重做日志)、binlog(归档日志)、两阶段提交、undo log(回滚日志)。 +Today, let's discuss redo log, binlog, two-phase commit, and undo log. ## redo log -redo log(重做日志)是 InnoDB 存储引擎独有的,它让 MySQL 拥有了崩溃恢复能力。 +The redo log is unique to the InnoDB storage engine, giving MySQL the ability to recover from crashes. -比如 MySQL 实例挂了或宕机了,重启时,InnoDB 存储引擎会使用 redo log 恢复数据,保证数据的持久性与完整性。 +For example, if a MySQL instance crashes or goes down, upon restart, the InnoDB storage engine will use the redo log to recover data, ensuring data persistence and integrity. ![](https://oss.javaguide.cn/github/javaguide/02.png) -MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 `Buffer Pool` 中。 +In MySQL, data is organized in pages. When you query a record, a page of data is loaded from the disk, and this loaded data is called a data page, which is placed into the `Buffer Pool`. -后续的查询都是先从 `Buffer Pool` 中找,没有命中再去硬盘加载,减少硬盘 IO 开销,提升性能。 +Subsequent queries first look for data in the `Buffer Pool`, and if not found, then load from the disk, reducing disk I/O overhead and improving performance. -更新表数据的时候,也是如此,发现 `Buffer Pool` 里存在要更新的数据,就直接在 `Buffer Pool` 里更新。 +When updating table data, the same principle applies. If the data to be updated is found in the `Buffer Pool`, it is updated directly there. -然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(`redo log buffer`)里,接着刷盘到 redo log 文件里。 +Then, the modifications made to a specific data page are recorded in the redo log buffer, which is subsequently flushed to the redo log file. ![](https://oss.javaguide.cn/github/javaguide/03.png) -> 图片笔误提示:第 4 步 “清空 redo log buffe 刷盘到 redo 日志中”这句话中的 buffe 应该是 buffer。 +> Typo alert: In step 4, the phrase "clear redo log buffe and flush to redo log" should have "buffe" corrected to "buffer". -理想情况,事务一提交就会进行刷盘操作,但实际上,刷盘的时机是根据策略来进行的。 +Ideally, flushing occurs immediately upon transaction commit, but in practice, the timing of flushing is determined by policy. -> 小贴士:每条 redo 记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成 +> Tip: Each redo record consists of "tablespace ID + data page ID + offset + length of modified data + specific modified data". -### 刷盘时机 +### Flushing Timing -InnoDB 刷新重做日志的时机有几种情况: +There are several scenarios in which InnoDB flushes the redo log to disk: -InnoDB 将 redo log 刷到磁盘上有几种情况: +1. Transaction commit: When a transaction is committed, the redo log in the log buffer is flushed to disk (this can be controlled by the `innodb_flush_log_at_trx_commit` parameter, which will be discussed later). +1. Insufficient log buffer space: When the cached redo log in the log buffer occupies about half of its total capacity, it needs to be flushed to disk. +1. Transaction log buffer full: InnoDB uses a transaction log buffer to temporarily store redo log entries. When the buffer is full, it triggers a flush to write the logs to disk. +1. Checkpoint: InnoDB periodically performs checkpoint operations to flush dirty data (modified but not yet written to disk) to disk, along with the corresponding redo logs to ensure data consistency. +1. Background flush thread: InnoDB starts a background thread that periodically (every second) flushes dirty pages (modified but not yet written to disk) to disk, along with the related redo logs. +1. Normal server shutdown: When MySQL shuts down, the redo logs are flushed to disk. -1. 事务提交:当事务提交时,log buffer 里的 redo log 会被刷新到磁盘(可以通过`innodb_flush_log_at_trx_commit`参数控制,后文会提到)。 -2. log buffer 空间不足时:log buffer 中缓存的 redo log 已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。 -3. 事务日志缓冲区满:InnoDB 使用一个事务日志缓冲区(transaction log buffer)来暂时存储事务的重做日志条目。当缓冲区满时,会触发日志的刷新,将日志写入磁盘。 -4. Checkpoint(检查点):InnoDB 定期会执行检查点操作,将内存中的脏数据(已修改但尚未写入磁盘的数据)刷新到磁盘,并且会将相应的重做日志一同刷新,以确保数据的一致性。 -5. 后台刷新线程:InnoDB 启动了一个后台线程,负责周期性(每隔 1 秒)地将脏页(已修改但尚未写入磁盘的数据页)刷新到磁盘,并将相关的重做日志一同刷新。 -6. 正常关闭服务器:MySQL 关闭的时候,redo log 都会刷入到磁盘里去。 +In summary, InnoDB flushes the redo log in various situations to ensure data persistence and consistency. -总之,InnoDB 在多种情况下会刷新重做日志,以保证数据的持久性和一致性。 +We need to pay attention to setting the correct flushing policy `innodb_flush_log_at_trx_commit`. Depending on the configured flushing strategy, there may be slight data loss after a MySQL crash. -我们要注意设置正确的刷盘策略`innodb_flush_log_at_trx_commit` 。根据 MySQL 配置的刷盘策略的不同,MySQL 宕机之后可能会存在轻微的数据丢失问题。 +The value of `innodb_flush_log_at_trx_commit` has three options, representing three flushing strategies: -`innodb_flush_log_at_trx_commit` 的值有 3 种,也就是共有 3 种刷盘策略: +- **0**: When set to 0, it means no flushing occurs upon each transaction commit. This method has the highest performance but is the least safe, as if MySQL crashes, the last second's transactions may be lost. +- **1**: When set to 1, it means flushing occurs on every transaction commit. This method has the lowest performance but is the safest, as once a transaction is successfully committed, the redo log record is guaranteed to be on disk, with no data loss. +- **2**: When set to 2, it means that on each transaction commit, only the redo log content in the log buffer is written to the page cache (file system cache). The page cache is specifically for caching files, and the cached file here is the redo log file. This method's performance and safety are intermediate between the first two. -- **0**:设置为 0 的时候,表示每次事务提交时不进行刷盘操作。这种方式性能最高,但是也最不安全,因为如果 MySQL 挂了或宕机了,可能会丢失最近 1 秒内的事务。 -- **1**:设置为 1 的时候,表示每次事务提交时都将进行刷盘操作。这种方式性能最低,但是也最安全,因为只要事务提交成功,redo log 记录就一定在磁盘里,不会有任何数据丢失。 -- **2**:设置为 2 的时候,表示每次事务提交时都只把 log buffer 里的 redo log 内容写入 page cache(文件系统缓存)。page cache 是专门用来缓存文件的,这里被缓存的文件就是 redo log 文件。这种方式的性能和安全性都介于前两者中间。 +The default value of the flushing strategy `innodb_flush_log_at_trx_commit` is 1, which ensures no data loss. To guarantee transaction persistence, we must set it to 1. -刷盘策略`innodb_flush_log_at_trx_commit` 的默认值为 1,设置为 1 的时候才不会丢失任何数据。为了保证事务的持久性,我们必须将其设置为 1。 - -另外,InnoDB 存储引擎有一个后台线程,每隔`1` 秒,就会把 `redo log buffer` 中的内容写到文件系统缓存(`page cache`),然后调用 `fsync` 刷盘。 - -![](https://oss.javaguide.cn/github/javaguide/04.png) - -也就是说,一个没有提交事务的 redo log 记录,也可能会刷盘。 - -**为什么呢?** - -因为在事务执行过程 redo log 记录是会写入`redo log buffer` 中,这些 redo log 记录会被后台线程刷盘。 - -![](https://oss.javaguide.cn/github/javaguide/05.png) - -除了后台线程每秒`1`次的轮询操作,还有一种情况,当 `redo log buffer` 占用的空间即将达到 `innodb_log_buffer_size` 一半的时候,后台线程会主动刷盘。 - -下面是不同刷盘策略的流程图。 - -#### innodb_flush_log_at_trx_commit=0 - -![](https://oss.javaguide.cn/github/javaguide/06.png) - -为`0`时,如果 MySQL 挂了或宕机可能会有`1`秒数据的丢失。 - -#### innodb_flush_log_at_trx_commit=1 - -![](https://oss.javaguide.cn/github/javaguide/07.png) - -为`1`时, 只要事务提交成功,redo log 记录就一定在硬盘里,不会有任何数据丢失。 - -如果事务执行期间 MySQL 挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。 - -#### innodb_flush_log_at_trx_commit=2 - -![](https://oss.javaguide.cn/github/javaguide/09.png) - -为`2`时, 只要事务提交成功,`redo log buffer`中的内容只写入文件系统缓存(`page cache`)。 - -如果仅仅只是 MySQL 挂了不会有任何数据丢失,但是宕机可能会有`1`秒数据的丢失。 - -### 日志文件组 - -硬盘上存储的 redo log 日志文件不只一个,而是以一个**日志文件组**的形式出现的,每个的`redo`日志文件大小都是一样的。 - -比如可以配置为一组`4`个文件,每个文件的大小是 `1GB`,整个 redo log 日志文件组可以记录`4G`的内容。 - -它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示。 - -![](https://oss.javaguide.cn/github/javaguide/10.png) - -在这个**日志文件组**中还有两个重要的属性,分别是 `write pos、checkpoint` - -- **write pos** 是当前记录的位置,一边写一边后移 -- **checkpoint** 是当前要擦除的位置,也是往后推移 - -每次刷盘 redo log 记录到**日志文件组**中,`write pos` 位置就会后移更新。 - -每次 MySQL 加载**日志文件组**恢复数据时,会清空加载过的 redo log 记录,并把 `checkpoint` 后移更新。 - -`write pos` 和 `checkpoint` 之间的还空着的部分可以用来写入新的 redo log 记录。 - -![](https://oss.javaguide.cn/github/javaguide/11.png) - -如果 `write pos` 追上 `checkpoint` ,表示**日志文件组**满了,这时候不能再写入新的 redo log 记录,MySQL 得停下来,清空一些记录,把 `checkpoint` 推进一下。 - -![](https://oss.javaguide.cn/github/javaguide/12.png) - -注意从 MySQL 8.0.30 开始,日志文件组有了些许变化: - -> The innodb_redo_log_capacity variable supersedes the innodb_log_files_in_group and innodb_log_file_size variables, which are deprecated. When the innodb_redo_log_capacity setting is defined, the innodb_log_files_in_group and innodb_log_file_size settings are ignored; otherwise, these settings are used to compute the innodb_redo_log_capacity setting (innodb_log_files_in_group \* innodb_log_file_size = innodb_redo_log_capacity). If none of those variables are set, redo log capacity is set to the innodb_redo_log_capacity default value, which is 104857600 bytes (100MB). The maximum redo log capacity is 128GB. - -> Redo log files reside in the #innodb_redo directory in the data directory unless a different directory was specified by the innodb_log_group_home_dir variable. If innodb_log_group_home_dir was defined, the redo log files reside in the #innodb_redo directory in that directory. There are two types of redo log files, ordinary and spare. Ordinary redo log files are those being used. Spare redo log files are those waiting to be used. InnoDB tries to maintain 32 redo log files in total, with each file equal in size to 1/32 \* innodb_redo_log_capacity; however, file sizes may differ for a time after modifying the innodb_redo_log_capacity setting. - -意思是在 MySQL 8.0.30 之前可以通过 `innodb_log_files_in_group` 和 `innodb_log_file_size` 配置日志文件组的文件数和文件大小,但在 MySQL 8.0.30 及之后的版本中,这两个变量已被废弃,即使被指定也是用来计算 `innodb_redo_log_capacity` 的值。而日志文件组的文件数则固定为 32,文件大小则为 `innodb_redo_log_capacity / 32` 。 - -关于这一点变化,我们可以验证一下。 - -首先创建一个配置文件,里面配置一下 `innodb_log_files_in_group` 和 `innodb_log_file_size` 的值: - -```properties -[mysqld] -innodb_log_file_size = 10485760 -innodb_log_files_in_group = 64 -``` - -docker 启动一个 MySQL 8.0.32 的容器: - -```bash -docker run -d -p 3312:3309 -e MYSQL_ROOT_PASSWORD=your-password -v /path/to/your/conf:/etc/mysql/conf.d --name -MySQL830 mysql:8.0.32 -``` - -现在我们来看一下启动日志: - -```plain -2023-08-03T02:05:11.720357Z 0 [Warning] [MY-013907] [InnoDB] Deprecated configuration parameters innodb_log_file_size and/or innodb_log_files_in_group have been used to compute innodb_redo_log_capacity=671088640. Please use innodb_redo_log_capacity instead. -``` - -这里也表明了 `innodb_log_files_in_group` 和 `innodb_log_file_size` 这两个变量是用来计算 `innodb_redo_log_capacity` ,且已经被废弃。 - -我们再看下日志文件组的文件数是多少: - -![](images/redo-log.png) - -可以看到刚好是 32 个,并且每个日志文件的大小是 `671088640 / 32 = 20971520` - -所以在使用 MySQL 8.0.30 及之后的版本时,推荐使用 `innodb_redo_log_capacity` 变量配置日志文件组 - -### redo log 小结 - -相信大家都知道 redo log 的作用和它的刷盘时机、存储形式。 - -现在我们来思考一个问题:**只要每次把修改后的数据页直接刷盘不就好了,还有 redo log 什么事?** - -它们不都是刷盘么?差别在哪里? - -```java -1 Byte = 8bit -1 KB = 1024 Byte -1 MB = 1024 KB -1 GB = 1024 MB -1 TB = 1024 GB -``` - -实际上,数据页大小是`16KB`,刷盘比较耗时,可能就修改了数据页里的几 `Byte` 数据,有必要把完整的数据页刷盘吗? - -而且数据页刷盘是随机写,因为一个数据页对应的位置可能在硬盘文件的随机位置,所以性能是很差。 - -如果是写 redo log,一行记录可能就占几十 `Byte`,只包含表空间号、数据页号、磁盘文件偏移 -量、更新值,再加上是顺序写,所以刷盘速度很快。 - -所以用 redo log 形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。 - -> 其实内存的数据页在一定时机也会刷盘,我们把这称为页合并,讲 `Buffer Pool`的时候会对这块细说 - -## binlog - -redo log 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎。 - -而 binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于`MySQL Server` 层。 - -不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志。 - -那 binlog 到底是用来干嘛的? - -可以说 MySQL 数据库的**数据备份、主备、主主、主从**都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。 - -![](https://oss.javaguide.cn/github/javaguide/01-20220305234724956.png) - -binlog 会记录所有涉及更新数据的逻辑操作,并且是顺序写。 - -### 记录格式 - -binlog 日志有三种格式,可以通过`binlog_format`参数指定。 - -- **statement** -- **row** -- **mixed** - -指定`statement`,记录的内容是`SQL`语句原文,比如执行一条`update T set update_time=now() where id=1`,记录的内容如下。 - -![](https://oss.javaguide.cn/github/javaguide/02-20220305234738688.png) - -同步数据时,会执行记录的`SQL`语句,但是有个问题,`update_time=now()`这里会获取当前系统时间,直接执行会导致与原库的数据不一致。 - -为了解决这种问题,我们需要指定为`row`,记录的内容不再是简单的`SQL`语句了,还包含操作的具体数据,记录内容如下。 - -![](https://oss.javaguide.cn/github/javaguide/03-20220305234742460.png) - -`row`格式记录的内容看不到详细信息,要通过`mysqlbinlog`工具解析出来。 - -`update_time=now()`变成了具体的时间`update_time=1627112756247`,条件后面的@1、@2、@3 都是该行数据第 1 个~3 个字段的原始值(**假设这张表只有 3 个字段**)。 - -这样就能保证同步数据的一致性,通常情况下都是指定为`row`,这样可以为数据库的恢复与同步带来更好的可靠性。 - -但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗 IO 资源,影响执行速度。 - -所以就有了一种折中的方案,指定为`mixed`,记录的内容是前两者的混合。 - -MySQL 会判断这条`SQL`语句是否可能引起数据不一致,如果是,就用`row`格式,否则就用`statement`格式。 - -### 写入机制 - -binlog 的写入时机也非常简单,事务执行过程中,先把日志写到`binlog cache`,事务提交的时候,再把`binlog cache`写到 binlog 文件中。 - -因为一个事务的 binlog 不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为`binlog cache`。 - -我们可以通过`binlog_cache_size`参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘(`Swap`)。 - -binlog 日志刷盘流程如下 - -![](https://oss.javaguide.cn/github/javaguide/04-20220305234747840.png) - -- **上图的 write,是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快** -- **上图的 fsync,才是将数据持久化到磁盘的操作** - -`write`和`fsync`的时机,可以由参数`sync_binlog`控制,默认是`1`。 - -为`0`的时候,表示每次提交事务都只`write`,由系统自行判断什么时候执行`fsync`。 - -![](https://oss.javaguide.cn/github/javaguide/05-20220305234754405.png) - -虽然性能得到提升,但是机器宕机,`page cache`里面的 binlog 会丢失。 - -为了安全起见,可以设置为`1`,表示每次提交事务都会执行`fsync`,就如同 **redo log 日志刷盘流程** 一样。 - -最后还有一种折中方式,可以设置为`N(N>1)`,表示每次提交事务都`write`,但累积`N`个事务后才`fsync`。 - -![](https://oss.javaguide.cn/github/javaguide/06-20220305234801592.png) - -在出现 IO 瓶颈的场景里,将`sync_binlog`设置成一个比较大的值,可以提升性能。 - -同样的,如果机器宕机,会丢失最近`N`个事务的 binlog 日志。 - -## 两阶段提交 - -redo log(重做日志)让 InnoDB 存储引擎拥有了崩溃恢复能力。 - -binlog(归档日志)保证了 MySQL 集群架构的数据一致性。 - -虽然它们都属于持久化的保证,但是侧重点不同。 - -在执行更新语句过程,会记录 redo log 与 binlog 两块日志,以基本的事务为单位,redo log 在事务执行过程中可以不断写入,而 binlog 只有在提交事务时才写入,所以 redo log 与 binlog 的写入时机不一样。 - -![](https://oss.javaguide.cn/github/javaguide/01-20220305234816065.png) - -回到正题,redo log 与 binlog 两份日志之间的逻辑不一致,会出现什么问题? - -我们以`update`语句为例,假设`id=2`的记录,字段`c`值是`0`,把字段`c`值更新成`1`,`SQL`语句为`update T set c=1 where id=2`。 - -假设执行过程中写完 redo log 日志后,binlog 日志写期间发生了异常,会出现什么情况呢? - -![](https://oss.javaguide.cn/github/javaguide/02-20220305234828662.png) - -由于 binlog 没写完就异常,这时候 binlog 里面没有对应的修改记录。因此,之后用 binlog 日志恢复数据时,就会少这一次更新,恢复出来的这一行`c`值是`0`,而原库因为 redo log 日志恢复,这一行`c`值是`1`,最终数据不一致。 - -![](https://oss.javaguide.cn/github/javaguide/03-20220305235104445.png) - -为了解决两份日志之间的逻辑一致问题,InnoDB 存储引擎使用**两阶段提交**方案。 - -原理很简单,将 redo log 的写入拆成了两个步骤`prepare`和`commit`,这就是**两阶段提交**。 - -![](https://oss.javaguide.cn/github/javaguide/04-20220305234956774.png) - -使用**两阶段提交**后,写入 binlog 时发生异常也不会有影响,因为 MySQL 根据 redo log 日志恢复数据时,发现 redo log 还处于`prepare`阶段,并且没有对应 binlog 日志,就会回滚该事务。 - -![](https://oss.javaguide.cn/github/javaguide/05-20220305234937243.png) - -再看一个场景,redo log 设置`commit`阶段发生异常,那会不会回滚事务呢? - -![](https://oss.javaguide.cn/github/javaguide/06-20220305234907651.png) - -并不会回滚事务,它会执行上图框住的逻辑,虽然 redo log 是处于`prepare`阶段,但是能通过事务`id`找到对应的 binlog 日志,所以 MySQL 认为是完整的,就会提交事务恢复数据。 - -## undo log - -> 这部分内容为 JavaGuide 的补充: - -每一个事务对数据的修改都会被记录到 undo log ,当执行事务过程中出现错误或者需要执行回滚操作的话,MySQL 可以利用 undo log 将数据恢复到事务开始之前的状态。 - -undo log 属于逻辑日志,记录的是 SQL 语句,比如说事务执行一条 DELETE 语句,那 undo log 就会记录一条相对应的 INSERT 语句。同时,undo log 的信息也会被记录到 redo log 中,因为 undo log 也要实现持久性保护。并且,undo-log 本身是会被删除清理的,例如 INSERT 操作,在事务提交之后就可以清除掉了;UPDATE/DELETE 操作在事务提交不会立即删除,会加入 history list,由后台线程 purge 进行清理。 - -undo log 是采用 segment(段)的方式来记录的,每个 undo 操作在记录的时候占用一个 **undo log segment**(undo 日志段),undo log segment 包含在 **rollback segment**(回滚段)中。事务开始时,需要为其分配一个 rollback segment。每个 rollback segment 有 1024 个 undo log segment,这有助于管理多个并发事务的回滚需求。 - -通常情况下, **rollback segment header**(通常在回滚段的第一个页)负责管理 rollback segment。rollback segment header 是 rollback segment 的一部分,通常在回滚段的第一个页。**history list** 是 rollback segment header 的一部分,它的主要作用是记录所有已经提交但还没有被清理(purge)的事务的 undo log。这个列表使得 purge 线程能够找到并清理那些不再需要的 undo log 记录。 - -另外,`MVCC` 的实现依赖于:**隐藏字段、Read View、undo log**。在内部实现中,InnoDB 通过数据行的 `DB_TRX_ID` 和 `Read View` 来判断数据的可见性,如不可见,则通过数据行的 `DB_ROLL_PTR` 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 `Read View` 之前已经提交的修改和该事务本身做的修改 - -## 总结 - -> 这部分内容为 JavaGuide 的补充: - -MySQL InnoDB 引擎使用 **redo log(重做日志)** 保证事务的**持久性**,使用 **undo log(回滚日志)** 来保证事务的**原子性**。 - -MySQL 数据库的**数据备份、主备、主主、主从**都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。 - -## 参考 - -- 《MySQL 实战 45 讲》 -- 《从零开始带你成为 MySQL 实战优化高手》 -- 《MySQL 是怎样运行的:从根儿上理解 MySQL》 -- 《MySQL 技术 Innodb 存储引擎》 - - +Additionally, the InnoDB storage engine has a background thread that writes the contents of the `redo log buffer` diff --git a/docs/database/mysql/mysql-query-cache.md b/docs/database/mysql/mysql-query-cache.md index cdc49b2c59c..b05bbf6d0c5 100644 --- a/docs/database/mysql/mysql-query-cache.md +++ b/docs/database/mysql/mysql-query-cache.md @@ -1,50 +1,50 @@ --- -title: MySQL查询缓存详解 -category: 数据库 +title: MySQL Query Cache Explained +category: Database tag: - MySQL head: - - - meta - - name: keywords - content: MySQL查询缓存,MySQL缓存机制中的内存管理 - - - meta - - name: description - content: 为了提高完全相同的查询语句的响应速度,MySQL Server 会对查询语句进行 Hash 计算得到一个 Hash 值。MySQL Server 不会对 SQL 做任何处理,SQL 必须完全一致 Hash 值才会一样。得到 Hash 值之后,通过该 Hash 值到查询缓存中匹配该查询的结果。MySQL 中的查询缓存虽然能够提升数据库的查询性能,但是查询同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。 + - - meta + - name: keywords + content: MySQL Query Cache, MySQL Cache Mechanism Memory Management + - - meta + - name: description + content: To improve the response speed of identical query statements, MySQL Server calculates a hash value for the query statement. MySQL Server does not process the SQL; the SQL must be exactly the same for the hash value to match. After obtaining the hash value, it matches the query result in the query cache using that hash value. Although the query cache in MySQL can enhance the query performance of the database, it also brings additional overhead, as a cache operation must be performed after each query, and it must be destroyed after it becomes invalid. --- -缓存是一个有效且实用的系统性能优化的手段,不论是操作系统还是各种软件和网站或多或少都用到了缓存。 +Caching is an effective and practical means of optimizing system performance, and it is used to some extent in operating systems, various software, and websites. -然而,有经验的 DBA 都建议生产环境中把 MySQL 自带的 Query Cache(查询缓存)给关掉。而且,从 MySQL 5.7.20 开始,就已经默认弃用查询缓存了。在 MySQL 8.0 及之后,更是直接删除了查询缓存的功能。 +However, experienced DBAs recommend disabling the built-in Query Cache in MySQL in production environments. Moreover, starting from MySQL 5.7.20, the query cache has been deprecated by default. In MySQL 8.0 and later, the query cache feature has been completely removed. -这又是为什么呢?查询缓存真就这么鸡肋么? +Why is this the case? Is the query cache really that useless? -带着如下几个问题,我们正式进入本文。 +With the following questions in mind, we officially enter this article. -- MySQL 查询缓存是什么?适用范围? -- MySQL 缓存规则是什么? -- MySQL 缓存的优缺点是什么? -- MySQL 缓存对性能有什么影响? +- What is MySQL Query Cache? What is its scope of application? +- What are the caching rules in MySQL? +- What are the advantages and disadvantages of MySQL caching? +- How does MySQL caching affect performance? -## MySQL 查询缓存介绍 +## Introduction to MySQL Query Cache -MySQL 体系架构如下图所示: +The architecture of MySQL is shown in the diagram below: ![](https://oss.javaguide.cn/github/javaguide/mysql/mysql-architecture.png) -为了提高完全相同的查询语句的响应速度,MySQL Server 会对查询语句进行 Hash 计算得到一个 Hash 值。MySQL Server 不会对 SQL 做任何处理,SQL 必须完全一致 Hash 值才会一样。得到 Hash 值之后,通过该 Hash 值到查询缓存中匹配该查询的结果。 +To improve the response speed of identical query statements, MySQL Server calculates a hash value for the query statement. MySQL Server does not process the SQL; the SQL must be exactly the same for the hash value to match. After obtaining the hash value, it matches the query result in the query cache using that hash value. -- 如果匹配(命中),则将查询的结果集直接返回给客户端,不必再解析、执行查询。 -- 如果没有匹配(未命中),则将 Hash 值和结果集保存在查询缓存中,以便以后使用。 +- If there is a match (hit), the result set of the query is returned directly to the client without needing to parse or execute the query. +- If there is no match (miss), the hash value and result set are stored in the query cache for future use. -也就是说,**一个查询语句(select)到了 MySQL Server 之后,会先到查询缓存看看,如果曾经执行过的话,就直接返回结果集给客户端。** +In other words, **when a query statement (select) reaches MySQL Server, it first checks the query cache. If it has been executed before, it directly returns the result set to the client.** ![](https://oss.javaguide.cn/javaguide/13526879-3037b144ed09eb88.png) -## MySQL 查询缓存管理和配置 +## MySQL Query Cache Management and Configuration -通过 `show variables like '%query_cache%'`命令可以查看查询缓存相关的信息。 +You can view information related to the query cache using the command `show variables like '%query_cache%'`. -8.0 版本之前的话,打印的信息可能是下面这样的: +Before version 8.0, the printed information might look like this: ```bash mysql> show variables like '%query_cache%'; @@ -61,7 +61,7 @@ mysql> show variables like '%query_cache%'; 6 rows in set (0.02 sec) ``` -8.0 以及之后版本之后,打印的信息是下面这样的: +After version 8.0 and later, the printed information looks like this: ```bash mysql> show variables like '%query_cache%'; @@ -73,136 +73,9 @@ mysql> show variables like '%query_cache%'; 1 row in set (0.01 sec) ``` -我们这里对 8.0 版本之前`show variables like '%query_cache%';`命令打印出来的信息进行解释。 +Here, we explain the information printed by the command `show variables like '%query_cache%';` before version 8.0. -- **`have_query_cache`:** 该 MySQL Server 是否支持查询缓存,如果是 YES 表示支持,否则则是不支持。 -- **`query_cache_limit`:** MySQL 查询缓存的最大查询结果,查询结果大于该值时不会被缓存。 -- **`query_cache_min_res_unit`:** 查询缓存分配的最小块的大小(字节)。当查询进行的时候,MySQL 把查询结果保存在查询缓存中,但如果要保存的结果比较大,超过 `query_cache_min_res_unit` 的值 ,这时候 MySQL 将一边检索结果,一边进行保存结果,也就是说,有可能在一次查询中,MySQL 要进行多次内存分配的操作。适当的调节 `query_cache_min_res_unit` 可以优化内存。 -- **`query_cache_size`:** 为缓存查询结果分配的内存的数量,单位是字节,且数值必须是 1024 的整数倍。默认值是 0,即禁用查询缓存。 -- **`query_cache_type`:** 设置查询缓存类型,默认为 ON。设置 GLOBAL 值可以设置后面的所有客户端连接的类型。客户端可以设置 SESSION 值以影响他们自己对查询缓存的使用。 -- **`query_cache_wlock_invalidate`**:如果某个表被锁住,是否返回缓存中的数据,默认关闭,也是建议的。 - -`query_cache_type` 可能的值(修改 `query_cache_type` 需要重启 MySQL Server): - -- 0 或 OFF:关闭查询功能。 -- 1 或 ON:开启查询缓存功能,但不缓存 `Select SQL_NO_CACHE` 开头的查询。 -- 2 或 DEMAND:开启查询缓存功能,但仅缓存 `Select SQL_CACHE` 开头的查询。 - -**建议**: - -- `query_cache_size`不建议设置的过大。过大的空间不但挤占实例其他内存结构的空间,而且会增加在缓存中搜索的开销。建议根据实例规格,初始值设置为 10MB 到 100MB 之间的值,而后根据运行使用情况调整。 -- 建议通过调整 `query_cache_size` 的值来开启、关闭查询缓存,因为修改`query_cache_type` 参数需要重启 MySQL Server 生效。 - - 8.0 版本之前,`my.cnf` 加入以下配置,重启 MySQL 开启查询缓存 - -```properties -query_cache_type=1 -query_cache_size=600000 -``` - -或者,MySQL 执行以下命令也可以开启查询缓存 - -```properties -set global query_cache_type=1; -set global query_cache_size=600000; -``` - -手动清理缓存可以使用下面三个 SQL: - -- `flush query cache;`:清理查询缓存内存碎片。 -- `reset query cache;`:从查询缓存中移除所有查询。 -- `flush tables;` 关闭所有打开的表,同时该操作会清空查询缓存中的内容。 - -## MySQL 缓存机制 - -### 缓存规则 - -- 查询缓存会将查询语句和结果集保存到内存(一般是 key-value 的形式,key 是查询语句,value 是查询的结果集),下次再查直接从内存中取。 -- 缓存的结果是通过 sessions 共享的,所以一个 client 查询的缓存结果,另一个 client 也可以使用。 -- SQL 必须完全一致才会导致查询缓存命中(大小写、空格、使用的数据库、协议版本、字符集等必须一致)。检查查询缓存时,MySQL Server 不会对 SQL 做任何处理,它精确的使用客户端传来的查询。 -- 不缓存查询中的子查询结果集,仅缓存查询最终结果集。 -- 不确定的函数将永远不会被缓存, 比如 `now()`、`curdate()`、`last_insert_id()`、`rand()` 等。 -- 不缓存产生告警(Warnings)的查询。 -- 太大的结果集不会被缓存 (< query_cache_limit)。 -- 如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL 库中的系统表,其查询结果也不会被缓存。 -- 缓存建立之后,MySQL 的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。 -- MySQL 缓存在分库分表环境下是不起作用的。 -- 不缓存使用 `SQL_NO_CACHE` 的查询。 -- …… - -查询缓存 `SELECT` 选项示例: - -```sql -SELECT SQL_CACHE id, name FROM customer;# 会缓存 -SELECT SQL_NO_CACHE id, name FROM customer;# 不会缓存 -``` - -### 缓存机制中的内存管理 - -查询缓存是完全存储在内存中的,所以在配置和使用它之前,我们需要先了解它是如何使用内存的。 - -MySQL 查询缓存使用内存池技术,自己管理内存释放和分配,而不是通过操作系统。内存池使用的基本单位是变长的 block, 用来存储类型、大小、数据等信息。一个结果集的缓存通过链表把这些 block 串起来。block 最短长度为 `query_cache_min_res_unit`。 - -当服务器启动的时候,会初始化缓存需要的内存,是一个完整的空闲块。当查询结果需要缓存的时候,先从空闲块中申请一个数据块为参数 `query_cache_min_res_unit` 配置的空间,即使缓存数据很小,申请数据块也是这个,因为查询开始返回结果的时候就分配空间,此时无法预知结果多大。 - -分配内存块需要先锁住空间块,所以操作很慢,MySQL 会尽量避免这个操作,选择尽可能小的内存块,如果不够,继续申请,如果存储完时有空余则释放多余的。 - -但是如果并发的操作,余下的需要回收的空间很小,小于 `query_cache_min_res_unit`,不能再次被使用,就会产生碎片。 - -## MySQL 查询缓存的优缺点 - -**优点:** - -- 查询缓存的查询,发生在 MySQL 接收到客户端的查询请求、查询权限验证之后和查询 SQL 解析之前。也就是说,当 MySQL 接收到客户端的查询 SQL 之后,仅仅只需要对其进行相应的权限验证之后,就会通过查询缓存来查找结果,甚至都不需要经过 Optimizer 模块进行执行计划的分析优化,更不需要发生任何存储引擎的交互。 -- 由于查询缓存是基于内存的,直接从内存中返回相应的查询结果,因此减少了大量的磁盘 I/O 和 CPU 计算,导致效率非常高。 - -**缺点:** - -- MySQL 会对每条接收到的 SELECT 类型的查询进行 Hash 计算,然后查找这个查询的缓存结果是否存在。虽然 Hash 计算和查找的效率已经足够高了,一条查询语句所带来的开销可以忽略,但一旦涉及到高并发,有成千上万条查询语句时,hash 计算和查找所带来的开销就必须重视了。 -- 查询缓存的失效问题。如果表的变更比较频繁,则会造成查询缓存的失效率非常高。表的变更不仅仅指表中的数据发生变化,还包括表结构或者索引的任何变化。 -- 查询语句不同,但查询结果相同的查询都会被缓存,这样便会造成内存资源的过度消耗。查询语句的字符大小写、空格或者注释的不同,查询缓存都会认为是不同的查询(因为他们的 Hash 值会不同)。 -- 相关系统变量设置不合理会造成大量的内存碎片,这样便会导致查询缓存频繁清理内存。 - -## MySQL 查询缓存对性能的影响 - -在 MySQL Server 中打开查询缓存对数据库的读和写都会带来额外的消耗: - -- 读查询开始之前必须检查是否命中缓存。 -- 如果读查询可以缓存,那么执行完查询操作后,会查询结果和查询语句写入缓存。 -- 当向某个表写入数据的时候,必须将这个表所有的缓存设置为失效,如果缓存空间很大,则消耗也会很大,可能使系统僵死一段时间,因为这个操作是靠全局锁操作来保护的。 -- 对 InnoDB 表,当修改一个表时,设置了缓存失效,但是多版本特性会暂时将这修改对其他事务屏蔽,在这个事务提交之前,所有查询都无法使用缓存,直到这个事务被提交,所以长时间的事务,会大大降低查询缓存的命中。 - -## 总结 - -MySQL 中的查询缓存虽然能够提升数据库的查询性能,但是查询同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。 - -查询缓存是一个适用较少情况的缓存机制。如果你的应用对数据库的更新很少,那么查询缓存将会作用显著。比较典型的如博客系统,一般博客更新相对较慢,数据表相对稳定不变,这时候查询缓存的作用会比较明显。 - -简单总结一下查询缓存的适用场景: - -- 表数据修改不频繁、数据较静态。 -- 查询(Select)重复度高。 -- 查询结果集小于 1 MB。 - -对于一个更新频繁的系统来说,查询缓存缓存的作用是很微小的,在某些情况下开启查询缓存会带来性能的下降。 - -简单总结一下查询缓存不适用的场景: - -- 表中的数据、表结构或者索引变动频繁 -- 重复的查询很少 -- 查询的结果集很大 - -《高性能 MySQL》这样写到: - -> 根据我们的经验,在高并发压力环境中查询缓存会导致系统性能的下降,甚至僵死。如果你一 定要使用查询缓存,那么不要设置太大内存,而且只有在明确收益的时候才使用(数据库内容修改次数较少)。 - -**确实是这样的!实际项目中,更建议使用本地缓存(比如 Caffeine)或者分布式缓存(比如 Redis) ,性能更好,更通用一些。** - -## 参考 - -- 《高性能 MySQL》 -- MySQL 缓存机制: -- RDS MySQL 查询缓存(Query Cache)的设置和使用 - 阿里元云数据库 RDS 文档: -- 8.10.3 The MySQL Query Cache - MySQL 官方文档: - - +- **`have_query_cache`:** Indicates whether this MySQL Server supports query cache. If it is YES, it means support; otherwise, it does not support. +- **`query_cache_limit`:** The maximum query result for MySQL query cache. Query results larger than this value will not be cached. +- **`query_cache_min_res_unit`:** The minimum size (in bytes) of the allocated block for the query cache. When a query is executed, MySQL stores the query result in the query cache, but if the result to be saved is large and exceeds the value of `query_cache_min_res_unit`, MySQL will retrieve the result while saving it, meaning that it may need to perform multiple memory allocation operations in a single query. Properly adjusting `query_cache_min_res_unit` can optimize memory usage. +- **`query_cache_size`:** The amount of memory allocated for caching query results, measured in bytes, and the value must be a multiple of diff --git a/docs/database/mysql/mysql-query-execution-plan.md b/docs/database/mysql/mysql-query-execution-plan.md index 8866737b934..59413c0f6ed 100644 --- a/docs/database/mysql/mysql-query-execution-plan.md +++ b/docs/database/mysql/mysql-query-execution-plan.md @@ -1,40 +1,40 @@ --- -title: MySQL执行计划分析 -category: 数据库 +title: MySQL Execution Plan Analysis +category: Database tag: - MySQL head: - - - meta - - name: keywords - content: MySQL基础,MySQL执行计划,EXPLAIN,查询优化器 - - - meta - - name: description - content: 执行计划是指一条 SQL 语句在经过MySQL 查询优化器的优化会后,具体的执行方式。优化 SQL 的第一步应该是读懂 SQL 的执行计划。 + - - meta + - name: keywords + content: MySQL Basics, MySQL Execution Plan, EXPLAIN, Query Optimizer + - - meta + - name: description + content: The execution plan refers to the specific execution method of an SQL statement after being optimized by the MySQL query optimizer. The first step in optimizing SQL should be to understand the SQL execution plan. --- -> 本文来自公号 MySQL 技术,JavaGuide 对其做了补充完善。原文地址: +> This article is from the public account MySQL Technology, with enhancements by JavaGuide. Original article link: -优化 SQL 的第一步应该是读懂 SQL 的执行计划。本篇文章,我们一起来学习下 MySQL `EXPLAIN` 执行计划相关知识。 +The first step in optimizing SQL should be to understand the SQL execution plan. In this article, we will learn about MySQL `EXPLAIN` execution plan related knowledge together. -## 什么是执行计划? +## What is an Execution Plan? -**执行计划** 是指一条 SQL 语句在经过 **MySQL 查询优化器** 的优化后,具体的执行方式。 +**Execution Plan** refers to the specific execution method of an SQL statement after being optimized by the **MySQL Query Optimizer**. -执行计划通常用于 SQL 性能分析、优化等场景。通过 `EXPLAIN` 的结果,可以了解到如数据表的查询顺序、数据查询操作的操作类型、哪些索引可以被命中、哪些索引实际会命中、每个数据表有多少行记录被查询等信息。 +Execution plans are typically used in SQL performance analysis, optimization, and other scenarios. By examining the results of `EXPLAIN`, one can understand information such as the order of table queries, the types of data query operations, which indexes can be hit, which indexes will actually be hit, and how many rows from each table are being queried. -## 如何获取执行计划? +## How to Obtain an Execution Plan? -MySQL 为我们提供了 `EXPLAIN` 命令,来获取执行计划的相关信息。 +MySQL provides us with the `EXPLAIN` command to obtain information related to the execution plan. -需要注意的是,`EXPLAIN` 语句并不会真的去执行相关的语句,而是通过查询优化器对语句进行分析,找出最优的查询方案,并显示对应的信息。 +It is important to note that the `EXPLAIN` statement does not actually execute the related statement; instead, it analyzes the statement through the query optimizer to find the optimal query plan and displays the corresponding information. -`EXPLAIN` 执行计划支持 `SELECT`、`DELETE`、`INSERT`、`REPLACE` 以及 `UPDATE` 语句。我们一般多用于分析 `SELECT` 查询语句,使用起来非常简单,语法如下: +The `EXPLAIN` execution plan supports `SELECT`, `DELETE`, `INSERT`, `REPLACE`, and `UPDATE` statements. It is generally used more for analyzing `SELECT` queries, and it is very simple to use, with the syntax as follows: ```sql -EXPLAIN + SELECT 查询语句; +EXPLAIN + SELECT query statement; ``` -我们简单来看下一条查询语句的执行计划: +Let's take a look at the execution plan for a simple query statement: ```sql mysql> explain SELECT * FROM dept_emp WHERE emp_no IN (SELECT emp_no FROM dept_emp GROUP BY emp_no HAVING COUNT(emp_no)>1); @@ -46,101 +46,35 @@ mysql> explain SELECT * FROM dept_emp WHERE emp_no IN (SELECT emp_no FROM dept_e +----+-------------+----------+------------+-------+-----------------+---------+---------+------+--------+----------+-------------+ ``` -可以看到,执行计划结果中共有 12 列,各列代表的含义总结如下表: +As we can see, the execution plan result contains 12 columns, and the meanings of each column are summarized in the table below: -| **列名** | **含义** | -| ------------- | -------------------------------------------- | -| id | SELECT 查询的序列标识符 | -| select_type | SELECT 关键字对应的查询类型 | -| table | 用到的表名 | -| partitions | 匹配的分区,对于未分区的表,值为 NULL | -| type | 表的访问方法 | -| possible_keys | 可能用到的索引 | -| key | 实际用到的索引 | -| key_len | 所选索引的长度 | -| ref | 当使用索引等值查询时,与索引作比较的列或常量 | -| rows | 预计要读取的行数 | -| filtered | 按表条件过滤后,留存的记录数的百分比 | -| Extra | 附加信息 | +| **Column Name** | **Meaning** | +| --------------- | ---------------------------------------------------------------------- | +| id | Identifier for the sequence of the SELECT query | +| select_type | The type of query corresponding to the SELECT keyword | +| table | The name of the table used | +| partitions | Matching partitions; NULL for non-partitioned tables | +| type | The access method for the table | +| possible_keys | Possible indexes that could be used | +| key | The actual index used | +| key_len | The length of the selected index | +| ref | The column or constant compared with the index in equality queries | +| rows | The estimated number of rows to be read | +| filtered | The percentage of records retained after filtering by table conditions | +| Extra | Additional information | -## 如何分析 EXPLAIN 结果? +## How to Analyze EXPLAIN Results? -为了分析 `EXPLAIN` 语句的执行结果,我们需要搞懂执行计划中的重要字段。 +To analyze the execution results of the `EXPLAIN` statement, we need to understand the important fields in the execution plan. ### id -`SELECT` 标识符,用于标识每个 `SELECT` 语句的执行顺序。 +The `SELECT` identifier is used to identify the execution order of each `SELECT` statement. -id 如果相同,从上往下依次执行。id 不同,id 值越大,执行优先级越高,如果行引用其他行的并集结果,则该值可以为 NULL。 +If the id is the same, they are executed in order from top to bottom. If the id is different, a higher id value indicates a higher execution priority. If a row references the union result of other rows, this value can be NULL. ### select_type -查询的类型,主要用于区分普通查询、联合查询、子查询等复杂的查询,常见的值有: +The type of query, mainly used to distinguish between simple queries, union queries, subqueries, and other complex queries. Common values include: -- **SIMPLE**:简单查询,不包含 UNION 或者子查询。 -- **PRIMARY**:查询中如果包含子查询或其他部分,外层的 SELECT 将被标记为 PRIMARY。 -- **SUBQUERY**:子查询中的第一个 SELECT。 -- **UNION**:在 UNION 语句中,UNION 之后出现的 SELECT。 -- **DERIVED**:在 FROM 中出现的子查询将被标记为 DERIVED。 -- **UNION RESULT**:UNION 查询的结果。 - -### table - -查询用到的表名,每行都有对应的表名,表名除了正常的表之外,也可能是以下列出的值: - -- **``** : 本行引用了 id 为 M 和 N 的行的 UNION 结果; -- **``** : 本行引用了 id 为 N 的表所产生的的派生表结果。派生表有可能产生自 FROM 语句中的子查询。 -- **``** : 本行引用了 id 为 N 的表所产生的的物化子查询结果。 - -### type(重要) - -查询执行的类型,描述了查询是如何执行的。所有值的顺序从最优到最差排序为: - -system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL - -常见的几种类型具体含义如下: - -- **system**:如果表使用的引擎对于表行数统计是精确的(如:MyISAM),且表中只有一行记录的情况下,访问方法是 system ,是 const 的一种特例。 -- **const**:表中最多只有一行匹配的记录,一次查询就可以找到,常用于使用主键或唯一索引的所有字段作为查询条件。 -- **eq_ref**:当连表查询时,前一张表的行在当前这张表中只有一行与之对应。是除了 system 与 const 之外最好的 join 方式,常用于使用主键或唯一索引的所有字段作为连表条件。 -- **ref**:使用普通索引作为查询条件,查询结果可能找到多个符合条件的行。 -- **index_merge**:当查询条件使用了多个索引时,表示开启了 Index Merge 优化,此时执行计划中的 key 列列出了使用到的索引。 -- **range**:对索引列进行范围查询,执行计划中的 key 列表示哪个索引被使用了。 -- **index**:查询遍历了整棵索引树,与 ALL 类似,只不过扫描的是索引,而索引一般在内存中,速度更快。 -- **ALL**:全表扫描。 - -### possible_keys - -possible_keys 列表示 MySQL 执行查询时可能用到的索引。如果这一列为 NULL ,则表示没有可能用到的索引;这种情况下,需要检查 WHERE 语句中所使用的的列,看是否可以通过给这些列中某个或多个添加索引的方法来提高查询性能。 - -### key(重要) - -key 列表示 MySQL 实际使用到的索引。如果为 NULL,则表示未用到索引。 - -### key_len - -key_len 列表示 MySQL 实际使用的索引的最大长度;当使用到联合索引时,有可能是多个列的长度和。在满足需求的前提下越短越好。如果 key 列显示 NULL ,则 key_len 列也显示 NULL 。 - -### rows - -rows 列表示根据表统计信息及选用情况,大致估算出找到所需的记录或所需读取的行数,数值越小越好。 - -### Extra(重要) - -这列包含了 MySQL 解析查询的额外信息,通过这些信息,可以更准确的理解 MySQL 到底是如何执行查询的。常见的值如下: - -- **Using filesort**:在排序时使用了外部的索引排序,没有用到表内索引进行排序。 -- **Using temporary**:MySQL 需要创建临时表来存储查询的结果,常见于 ORDER BY 和 GROUP BY。 -- **Using index**:表明查询使用了覆盖索引,不用回表,查询效率非常高。 -- **Using index condition**:表示查询优化器选择使用了索引条件下推这个特性。 -- **Using where**:表明查询使用了 WHERE 子句进行条件过滤。一般在没有使用到索引的时候会出现。 -- **Using join buffer (Block Nested Loop)**:连表查询的方式,表示当被驱动表的没有使用索引的时候,MySQL 会先将驱动表读出来放到 join buffer 中,再遍历被驱动表与驱动表进行查询。 - -这里提醒下,当 Extra 列包含 Using filesort 或 Using temporary 时,MySQL 的性能可能会存在问题,需要尽可能避免。 - -## 参考 - -- -- - - +- **SIMPLE**: A simple query that diff --git a/docs/database/mysql/mysql-questions-01.md b/docs/database/mysql/mysql-questions-01.md index 4878eee56ef..043b38049ae 100644 --- a/docs/database/mysql/mysql-questions-01.md +++ b/docs/database/mysql/mysql-questions-01.md @@ -1,908 +1,87 @@ --- -title: MySQL常见面试题总结 -category: 数据库 +title: Summary of Common MySQL Interview Questions +category: Database tag: - MySQL - - 大厂面试 + - Big Company Interviews head: - - - meta - - name: keywords - content: MySQL基础,MySQL基础架构,MySQL存储引擎,MySQL查询缓存,MySQL事务,MySQL锁等内容。 - - - meta - - name: description - content: 一篇文章总结MySQL常见的知识点和面试题,涵盖MySQL基础、MySQL基础架构、MySQL存储引擎、MySQL查询缓存、MySQL事务、MySQL锁等内容。 + - - meta + - name: keywords + content: MySQL basics, MySQL architecture, MySQL storage engines, MySQL query cache, MySQL transactions, MySQL locks, etc. + - - meta + - name: description + content: An article summarizing common knowledge points and interview questions about MySQL, covering MySQL basics, MySQL architecture, MySQL storage engines, MySQL query cache, MySQL transactions, MySQL locks, etc. --- -## MySQL 基础 +## MySQL Basics -### 什么是关系型数据库? +### What is a Relational Database? -顾名思义,关系型数据库(RDB,Relational Database)就是一种建立在关系模型的基础上的数据库。关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。 +As the name suggests, a relational database (RDB) is a type of database built on the relational model. The relational model indicates the relationships between the data stored in the database (one-to-one, one-to-many, many-to-many). -关系型数据库中,我们的数据都被存放在了各种表中(比如用户表),表中的每一行就存放着一条数据(比如一个用户的信息)。 +In a relational database, our data is stored in various tables (for example, a user table), with each row in the table storing a piece of data (for example, information about a user). -![关系型数据库表关系](https://oss.javaguide.cn/java-guide-blog/5e3c1a71724a38245aa43b02_99bf70d46cc247be878de9d3a88f0c44.png) +![Relational Database Table Relationships](https://oss.javaguide.cn/java-guide-blog/5e3c1a71724a38245aa43b02_99bf70d46cc247be878de9d3a88f0c44.png) -大部分关系型数据库都使用 SQL 来操作数据库中的数据。并且,大部分关系型数据库都支持事务的四大特性(ACID)。 +Most relational databases use SQL to manipulate the data within them. Additionally, most relational databases support the four major properties of transactions (ACID). -**有哪些常见的关系型数据库呢?** +**What are some common relational databases?** -MySQL、PostgreSQL、Oracle、SQL Server、SQLite(微信本地的聊天记录的存储就是用的 SQLite) ……。 +MySQL, PostgreSQL, Oracle, SQL Server, SQLite (the local chat record storage in WeChat uses SQLite), etc. -### 什么是 SQL? +### What is SQL? -SQL 是一种结构化查询语言(Structured Query Language),专门用来与数据库打交道,目的是提供一种从数据库中读写数据的简单有效的方法。 +SQL is a Structured Query Language specifically designed for interacting with databases, providing a simple and effective way to read and write data from a database. -几乎所有的主流关系数据库都支持 SQL ,适用性非常强。并且,一些非关系型数据库也兼容 SQL 或者使用的是类似于 SQL 的查询语言。 +Almost all mainstream relational databases support SQL, making it highly applicable. Some non-relational databases also support SQL or use a query language similar to SQL. -SQL 可以帮助我们: +SQL can help us: -- 新建数据库、数据表、字段; -- 在数据库中增加,删除,修改,查询数据; -- 新建视图、函数、存储过程; -- 对数据库中的数据进行简单的数据分析; -- 搭配 Hive,Spark SQL 做大数据; -- 搭配 SQLFlow 做机器学习; +- Create databases, tables, and fields; +- Add, delete, modify, and query data in the database; +- Create views, functions, and stored procedures; +- Perform simple data analysis on the data in the database; +- Work with Hive and Spark SQL for big data; +- Work with SQLFlow for machine learning; - …… -### 什么是 MySQL? +### What is MySQL? ![](https://oss.javaguide.cn/github/javaguide/csdn/20210327143351823.png) -**MySQL 是一种关系型数据库,主要用于持久化存储我们的系统中的一些数据比如用户信息。** +**MySQL is a relational database primarily used for the persistent storage of some data in our systems, such as user information.** -由于 MySQL 是开源免费并且比较成熟的数据库,因此,MySQL 被大量使用在各种系统中。任何人都可以在 GPL(General Public License) 的许可下下载并根据个性化的需要对其进行修改。MySQL 的默认端口号是**3306**。 +Since MySQL is open-source, free, and relatively mature, it is widely used in various systems. Anyone can download it under the GPL (General Public License) and modify it according to their needs. The default port number for MySQL is **3306**. -### MySQL 有什么优点? +### What are the advantages of MySQL? -这个问题本质上是在问 MySQL 如此流行的原因。 +This question essentially asks why MySQL is so popular. -MySQL 主要具有下面这些优点: +MySQL has the following advantages: -1. 成熟稳定,功能完善。 -2. 开源免费。 -3. 文档丰富,既有详细的官方文档,又有非常多优质文章可供参考学习。 -4. 开箱即用,操作简单,维护成本低。 -5. 兼容性好,支持常见的操作系统,支持多种开发语言。 -6. 社区活跃,生态完善。 -7. 事务支持优秀, InnoDB 存储引擎默认使用 REPEATABLE-READ 并不会有任何性能损失,并且,InnoDB 实现的 REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的。 -8. 支持分库分表、读写分离、高可用。 +1. Mature, stable, and feature-rich. +1. Open-source and free. +1. Abundant documentation, including detailed official documentation and many high-quality articles for reference and learning. +1. Ready to use, easy to operate, and low maintenance costs. +1. Good compatibility, supporting common operating systems and various programming languages. +1. Active community and complete ecosystem. +1. Excellent transaction support; the InnoDB storage engine uses REPEATABLE-READ by default without any performance loss, and the REPEATABLE-READ isolation level implemented by InnoDB can actually solve the phantom read problem. +1. Supports sharding, read-write separation, and high availability. -## MySQL 字段类型 +## MySQL Field Types -MySQL 字段类型可以简单分为三大类: +MySQL field types can be simply divided into three categories: -- **数值类型**:整型(TINYINT、SMALLINT、MEDIUMINT、INT 和 BIGINT)、浮点型(FLOAT 和 DOUBLE)、定点型(DECIMAL) -- **字符串类型**:CHAR、VARCHAR、TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT、TINYBLOB、BLOB、MEDIUMBLOB 和 LONGBLOB 等,最常用的是 CHAR 和 VARCHAR。 -- **日期时间类型**:YEAR、TIME、DATE、DATETIME 和 TIMESTAMP 等。 +- **Numeric Types**: Integer (TINYINT, SMALLINT, MEDIUMINT, INT, and BIGINT), Floating-point (FLOAT and DOUBLE), Fixed-point (DECIMAL) +- **String Types**: CHAR, VARCHAR, TINYTEXT, TEXT, MEDIUMTEXT, LONGTEXT, TINYBLOB, BLOB, MEDIUMBLOB, and LONGBLOB, etc. The most commonly used are CHAR and VARCHAR. +- **Date and Time Types**: YEAR, TIME, DATE, DATETIME, and TIMESTAMP, etc. -下面这张图不是我画的,忘记是从哪里保存下来的了,总结的还蛮不错的。 +The following image is not drawn by me; I forgot where I saved it from, but it summarizes quite well. -![MySQL 常见字段类型总结](https://oss.javaguide.cn/github/javaguide/mysql/summary-of-mysql-field-types.png) +![Summary of Common MySQL Field Types](https://oss.javaguide.cn/github/javaguide/mysql/summary-of-mysql-field-types.png) -MySQL 字段类型比较多,我这里会挑选一些日常开发使用很频繁且面试常问的字段类型,以面试问题的形式来详细介绍。如无特殊说明,针对的都是 InnoDB 存储引擎。 +MySQL has many field types, and I will select some frequently used and commonly asked field types in interviews to introduce in detail in the form of interview questions. Unless otherwise specified, the focus is on the InnoDB storage engine. -另外,推荐阅读一下《高性能 MySQL(第三版)》的第四章,有详细介绍 MySQL 字段类型优化。 - -### 整数类型的 UNSIGNED 属性有什么用? - -MySQL 中的整数类型可以使用可选的 UNSIGNED 属性来表示不允许负值的无符号整数。使用 UNSIGNED 属性可以将正整数的上限提高一倍,因为它不需要存储负数值。 - -例如, TINYINT UNSIGNED 类型的取值范围是 0 ~ 255,而普通的 TINYINT 类型的值范围是 -128 ~ 127。INT UNSIGNED 类型的取值范围是 0 ~ 4,294,967,295,而普通的 INT 类型的值范围是 -2,147,483,648 ~ 2,147,483,647。 - -对于从 0 开始递增的 ID 列,使用 UNSIGNED 属性可以非常适合,因为不允许负值并且可以拥有更大的上限范围,提供了更多的 ID 值可用。 - -### CHAR 和 VARCHAR 的区别是什么? - -CHAR 和 VARCHAR 是最常用到的字符串类型,两者的主要区别在于:**CHAR 是定长字符串,VARCHAR 是变长字符串。** - -CHAR 在存储时会在右边填充空格以达到指定的长度,检索时会去掉空格;VARCHAR 在存储时需要使用 1 或 2 个额外字节记录字符串的长度,检索时不需要处理。 - -CHAR 更适合存储长度较短或者长度都差不多的字符串,例如 Bcrypt 算法、MD5 算法加密后的密码、身份证号码。VARCHAR 类型适合存储长度不确定或者差异较大的字符串,例如用户昵称、文章标题等。 - -CHAR(M) 和 VARCHAR(M) 的 M 都代表能够保存的字符数的最大值,无论是字母、数字还是中文,每个都只占用一个字符。 - -### VARCHAR(100)和 VARCHAR(10)的区别是什么? - -VARCHAR(100)和 VARCHAR(10)都是变长类型,表示能存储最多 100 个字符和 10 个字符。因此,VARCHAR (100) 可以满足更大范围的字符存储需求,有更好的业务拓展性。而 VARCHAR(10)存储超过 10 个字符时,就需要修改表结构才可以。 - -虽说 VARCHAR(100)和 VARCHAR(10)能存储的字符范围不同,但二者存储相同的字符串,所占用磁盘的存储空间其实是一样的,这也是很多人容易误解的一点。 - -不过,VARCHAR(100) 会消耗更多的内存。这是因为 VARCHAR 类型在内存中操作时,通常会分配固定大小的内存块来保存值,即使用字符类型中定义的长度。例如在进行排序的时候,VARCHAR(100)是按照 100 这个长度来进行的,也就会消耗更多内存。 - -### DECIMAL 和 FLOAT/DOUBLE 的区别是什么? - -DECIMAL 和 FLOAT 的区别是:**DECIMAL 是定点数,FLOAT/DOUBLE 是浮点数。DECIMAL 可以存储精确的小数值,FLOAT/DOUBLE 只能存储近似的小数值。** - -DECIMAL 用于存储具有精度要求的小数,例如与货币相关的数据,可以避免浮点数带来的精度损失。 - -在 Java 中,MySQL 的 DECIMAL 类型对应的是 Java 类 `java.math.BigDecimal`。 - -### 为什么不推荐使用 TEXT 和 BLOB? - -TEXT 类型类似于 CHAR(0-255 字节)和 VARCHAR(0-65,535 字节),但可以存储更长的字符串,即长文本数据,例如博客内容。 - -| 类型 | 可存储大小 | 用途 | -| ---------- | -------------------- | -------------- | -| TINYTEXT | 0-255 字节 | 一般文本字符串 | -| TEXT | 0-65,535 字节 | 长文本字符串 | -| MEDIUMTEXT | 0-16,772,150 字节 | 较大文本数据 | -| LONGTEXT | 0-4,294,967,295 字节 | 极大文本数据 | - -BLOB 类型主要用于存储二进制大对象,例如图片、音视频等文件。 - -| 类型 | 可存储大小 | 用途 | -| ---------- | ---------- | ------------------------ | -| TINYBLOB | 0-255 字节 | 短文本二进制字符串 | -| BLOB | 0-65KB | 二进制字符串 | -| MEDIUMBLOB | 0-16MB | 二进制形式的长文本数据 | -| LONGBLOB | 0-4GB | 二进制形式的极大文本数据 | - -在日常开发中,很少使用 TEXT 类型,但偶尔会用到,而 BLOB 类型则基本不常用。如果预期长度范围可以通过 VARCHAR 来满足,建议避免使用 TEXT。 - -数据库规范通常不推荐使用 BLOB 和 TEXT 类型,这两种类型具有一些缺点和限制,例如: - -- 不能有默认值。 -- 在使用临时表时无法使用内存临时表,只能在磁盘上创建临时表(《高性能 MySQL》书中有提到)。 -- 检索效率较低。 -- 不能直接创建索引,需要指定前缀长度。 -- 可能会消耗大量的网络和 IO 带宽。 -- 可能导致表上的 DML 操作变慢。 -- …… - -### DATETIME 和 TIMESTAMP 的区别是什么? - -DATETIME 类型没有时区信息,TIMESTAMP 和时区有关。 - -TIMESTAMP 只需要使用 4 个字节的存储空间,但是 DATETIME 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,Timestamp 表示的时间范围更小。 - -- DATETIME:'1000-01-01 00:00:00.000000' 到 '9999-12-31 23:59:59.999999' -- Timestamp:'1970-01-01 00:00:01.000000' UTC 到 '2038-01-19 03:14:07.999999' UTC - -关于两者的详细对比,请参考我写的 [MySQL 时间类型数据存储建议](./some-thoughts-on-database-storage-time.md)。 - -### NULL 和 '' 的区别是什么? - -`NULL` 和 `''` (空字符串) 是两个完全不同的值,它们分别表示不同的含义,并在数据库中有着不同的行为。`NULL` 代表缺失或未知的数据,而 `''` 表示一个已知存在的空字符串。它们的主要区别如下: - -1. **含义**: - - `NULL` 代表一个不确定的值,它不等于任何值,包括它自身。因此,`SELECT NULL = NULL` 的结果是 `NULL`,而不是 `true` 或 `false`。 `NULL` 意味着缺失或未知的信息。虽然 `NULL` 不等于任何值,但在某些操作中,数据库系统会将 `NULL` 值视为相同的类别进行处理,例如:`DISTINCT`,`GROUP BY`,`ORDER BY`。需要注意的是,这些操作将 `NULL` 值视为相同的类别进行处理,并不意味着 `NULL` 值之间是相等的。 它们只是在特定操作中被特殊处理,以保证结果的正确性和一致性。 这种处理方式是为了方便数据操作,而不是改变了 `NULL` 的语义。 - - `''` 表示一个空字符串,它是一个已知的值。 -2. **存储空间**: - - `NULL` 的存储空间占用取决于数据库的实现,通常需要一些空间来标记该值为空。 - - `''` 的存储空间占用通常较小,因为它只存储一个空字符串的标志,不需要存储实际的字符。 -3. **比较运算**: - - 任何值与 `NULL` 进行比较(例如 `=`, `!=`, `>`, `<` 等)的结果都是 `NULL`,表示结果不确定。要判断一个值是否为 `NULL`,必须使用 `IS NULL` 或 `IS NOT NULL`。 - - `''` 可以像其他字符串一样进行比较运算。例如,`'' = ''` 的结果是 `true`。 -4. **聚合函数**: - - 大多数聚合函数(例如 `SUM`, `AVG`, `MIN`, `MAX`)会忽略 `NULL` 值。 - - `COUNT(*)` 会统计所有行数,包括包含 `NULL` 值的行。`COUNT(列名)` 会统计指定列中非 `NULL` 值的行数。 - - 空字符串 `''` 会被聚合函数计算在内。例如,`SUM` 会将其视为 0,`MIN` 和 `MAX` 会将其视为一个空字符串。 - -看了上面的介绍之后,相信你对另外一个高频面试题:“为什么 MySQL 不建议使用 `NULL` 作为列默认值?”也有了答案。 - -### Boolean 类型如何表示? - -MySQL 中没有专门的布尔类型,而是用 TINYINT(1) 类型来表示布尔值。TINYINT(1) 类型可以存储 0 或 1,分别对应 false 或 true。 - -## MySQL 基础架构 - -> 建议配合 [SQL 语句在 MySQL 中的执行过程](./how-sql-executed-in-mysql.md) 这篇文章来理解 MySQL 基础架构。另外,“一个 SQL 语句在 MySQL 中的执行流程”也是面试中比较常问的一个问题。 - -下图是 MySQL 的一个简要架构图,从下图你可以很清晰的看到客户端的一条 SQL 语句在 MySQL 内部是如何执行的。 - -![](https://oss.javaguide.cn/javaguide/13526879-3037b144ed09eb88.png) - -从上图可以看出, MySQL 主要由下面几部分构成: - -- **连接器:** 身份认证和权限相关(登录 MySQL 的时候)。 -- **查询缓存:** 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。 -- **分析器:** 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。 -- **优化器:** 按照 MySQL 认为最优的方案去执行。 -- **执行器:** 执行语句,然后从存储引擎返回数据。 执行语句之前会先判断是否有权限,如果没有权限的话,就会报错。 -- **插件式存储引擎**:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。InnoDB 是 MySQL 的默认存储引擎,绝大部分场景使用 InnoDB 就是最好的选择。 - -## MySQL 存储引擎 - -MySQL 核心在于存储引擎,想要深入学习 MySQL,必定要深入研究 MySQL 存储引擎。 - -### MySQL 支持哪些存储引擎?默认使用哪个? - -MySQL 支持多种存储引擎,你可以通过 `SHOW ENGINES` 命令来查看 MySQL 支持的所有存储引擎。 - -![查看 MySQL 提供的所有存储引擎](https://oss.javaguide.cn/github/javaguide/mysql/image-20220510105408703.png) - -从上图我们可以查看出, MySQL 当前默认的存储引擎是 InnoDB。并且,所有的存储引擎中只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务。 - -我这里使用的 MySQL 版本是 8.x,不同的 MySQL 版本之间可能会有差别。 - -MySQL 5.5.5 之前,MyISAM 是 MySQL 的默认存储引擎。5.5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。 - -你可以通过 `SELECT VERSION()` 命令查看你的 MySQL 版本。 - -```bash -mysql> SELECT VERSION(); -+-----------+ -| VERSION() | -+-----------+ -| 8.0.27 | -+-----------+ -1 row in set (0.00 sec) -``` - -你也可以通过 `SHOW VARIABLES LIKE '%storage_engine%'` 命令直接查看 MySQL 当前默认的存储引擎。 - -```bash -mysql> SHOW VARIABLES LIKE '%storage_engine%'; -+---------------------------------+-----------+ -| Variable_name | Value | -+---------------------------------+-----------+ -| default_storage_engine | InnoDB | -| default_tmp_storage_engine | InnoDB | -| disabled_storage_engines | | -| internal_tmp_mem_storage_engine | TempTable | -+---------------------------------+-----------+ -4 rows in set (0.00 sec) -``` - -如果你想要深入了解每个存储引擎以及它们之间的区别,推荐你去阅读以下 MySQL 官方文档对应的介绍(面试不会问这么细,了解即可): - -- InnoDB 存储引擎详细介绍: 。 -- 其他存储引擎详细介绍: 。 - -![](https://oss.javaguide.cn/github/javaguide/mysql/image-20220510155143458.png) - -### MySQL 存储引擎架构了解吗? - -MySQL 存储引擎采用的是 **插件式架构** ,支持多种存储引擎,我们甚至可以为不同的数据库表设置不同的存储引擎以适应不同场景的需要。**存储引擎是基于表的,而不是数据库。** - -下图展示了具有可插拔存储引擎的 MySQL 架构(): - -![MySQL architecture diagram showing connectors, interfaces, pluggable storage engines, the file system with files and logs.](https://oss.javaguide.cn/github/javaguide/mysql/mysql-architecture.png) - -你还可以根据 MySQL 定义的存储引擎实现标准接口来编写一个属于自己的存储引擎。这些非官方提供的存储引擎可以称为第三方存储引擎,区别于官方存储引擎。像目前最常用的 InnoDB 其实刚开始就是一个第三方存储引擎,后面由于过于优秀,其被 Oracle 直接收购了。 - -MySQL 官方文档也有介绍到如何编写一个自定义存储引擎,地址: 。 - -### MyISAM 和 InnoDB 有什么区别? - -MySQL 5.5 之前,MyISAM 引擎是 MySQL 的默认存储引擎,可谓是风光一时。 - -虽然,MyISAM 的性能还行,各种特性也还不错(比如全文索引、压缩、空间函数等)。但是,MyISAM 不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。 - -MySQL 5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。 - -言归正传!咱们下面还是来简单对比一下两者: - -**1、是否支持行级锁** - -MyISAM 只有表级锁(table-level locking),而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。 - -也就说,MyISAM 一锁就是锁住了整张表,这在并发写的情况下是多么滴憨憨啊!这也是为什么 InnoDB 在并发写的时候,性能更牛皮了! - -**2、是否支持事务** - -MyISAM 不提供事务支持。 - -InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别,具有提交(commit)和回滚(rollback)事务的能力。并且,InnoDB 默认使用的 REPEATABLE-READ(可重读)隔离级别是可以解决幻读问题发生的(基于 MVCC 和 Next-Key Lock)。 - -关于 MySQL 事务的详细介绍,可以看看我写的这篇文章:[MySQL 事务隔离级别详解](./transaction-isolation-level.md)。 - -**3、是否支持外键** - -MyISAM 不支持,而 InnoDB 支持。 - -外键对于维护数据一致性非常有帮助,但是对性能有一定的损耗。因此,通常情况下,我们是不建议在实际生产项目中使用外键的,在业务代码中进行约束即可! - -阿里的《Java 开发手册》也是明确规定禁止使用外键的。 - -![](https://oss.javaguide.cn/github/javaguide/mysql/image-20220510090309427.png) - -不过,在代码中进行约束的话,对程序员的能力要求更高,具体是否要采用外键还是要根据你的项目实际情况而定。 - -总结:一般我们也是不建议在数据库层面使用外键的,应用层面可以解决。不过,这样会对数据的一致性造成威胁。具体要不要使用外键还是要根据你的项目来决定。 - -**4、是否支持数据库异常崩溃后的安全恢复** - -MyISAM 不支持,而 InnoDB 支持。 - -使用 InnoDB 的数据库在异常崩溃后,数据库重新启动的时候会保证数据库恢复到崩溃前的状态。这个恢复的过程依赖于 `redo log` 。 - -**5、是否支持 MVCC** - -MyISAM 不支持,而 InnoDB 支持。 - -讲真,这个对比有点废话,毕竟 MyISAM 连行级锁都不支持。MVCC 可以看作是行级锁的一个升级,可以有效减少加锁操作,提高性能。 - -**6、索引实现不一样。** - -虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。 - -InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。 - -详细区别,推荐你看看我写的这篇文章:[MySQL 索引详解](./mysql-index.md)。 - -**7、性能有差别。** - -InnoDB 的性能比 MyISAM 更强大,不管是在读写混合模式下还是只读模式下,随着 CPU 核数的增加,InnoDB 的读写能力呈线性增长。MyISAM 因为读写不能并发,它的处理能力跟核数没关系。 - -![InnoDB 和 MyISAM 性能对比](https://oss.javaguide.cn/github/javaguide/mysql/innodb-myisam-performance-comparison.png) - -**8、数据缓存策略和机制实现不同。** - -InnoDB 使用缓冲池(Buffer Pool)缓存数据页和索引页,MyISAM 使用键缓存(Key Cache)仅缓存索引页而不缓存数据页。 - -**总结**: - -- InnoDB 支持行级别的锁粒度,MyISAM 不支持,只支持表级别的锁粒度。 -- MyISAM 不提供事务支持。InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别。 -- MyISAM 不支持外键,而 InnoDB 支持。 -- MyISAM 不支持 MVCC,而 InnoDB 支持。 -- 虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。 -- MyISAM 不支持数据库异常崩溃后的安全恢复,而 InnoDB 支持。 -- InnoDB 的性能比 MyISAM 更强大。 - -最后,再分享一张图片给你,这张图片详细对比了常见的几种 MySQL 存储引擎。 - -![常见的几种 MySQL 存储引擎对比](https://oss.javaguide.cn/github/javaguide/mysql/comparison-of-common-mysql-storage-engines.png) - -### MyISAM 和 InnoDB 如何选择? - -大多数时候我们使用的都是 InnoDB 存储引擎,在某些读密集的情况下,使用 MyISAM 也是合适的。不过,前提是你的项目不介意 MyISAM 不支持事务、崩溃恢复等缺点(可是~我们一般都会介意啊)。 - -《MySQL 高性能》上面有一句话这样写到: - -> 不要轻易相信“MyISAM 比 InnoDB 快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB 的速度都可以让 MyISAM 望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。 - -因此,对于咱们日常开发的业务系统来说,你几乎找不到什么理由使用 MyISAM 了,老老实实用默认的 InnoDB 就可以了! - -## MySQL 索引 - -MySQL 索引相关的问题比较多,对于面试和工作都比较重要,于是,我单独抽了一篇文章专门来总结 MySQL 索引相关的知识点和问题:[MySQL 索引详解](./mysql-index.md) 。 - -## MySQL 查询缓存 - -MySQL 查询缓存是查询结果缓存。执行查询语句的时候,会先查询缓存,如果缓存中有对应的查询结果,就会直接返回。 - -`my.cnf` 加入以下配置,重启 MySQL 开启查询缓存 - -```properties -query_cache_type=1 -query_cache_size=600000 -``` - -MySQL 执行以下命令也可以开启查询缓存 - -```properties -set global query_cache_type=1; -set global query_cache_size=600000; -``` - -查询缓存会在同样的查询条件和数据情况下,直接返回缓存中的结果。但需要注意的是,查询缓存的匹配条件非常严格,任何细微的差异都会导致缓存无法命中。这里的查询条件包括查询语句本身、当前使用的数据库、以及其他可能影响结果的因素,如客户端协议版本号等。 - -**查询缓存不命中的情况:** - -1. 任何两个查询在任何字符上的不同都会导致缓存不命中。 -2. 如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL 库中的系统表,其查询结果也不会被缓存。 -3. 缓存建立之后,MySQL 的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。 - -**缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。** 因此,开启查询缓存要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十 MB 比较合适。此外,还可以通过 `sql_cache` 和 `sql_no_cache` 来控制某个查询语句是否需要缓存: - -```sql -SELECT sql_no_cache COUNT(*) FROM usr; -``` - -MySQL 5.6 开始,查询缓存已默认禁用。MySQL 8.0 开始,已经不再支持查询缓存了(具体可以参考这篇文章:[MySQL 8.0: Retiring Support for the Query Cache](https://dev.mysql.com/blog-archive/mysql-8-0-retiring-support-for-the-query-cache/))。 - -![MySQL 8.0: Retiring Support for the Query Cache](https://oss.javaguide.cn/github/javaguide/mysql/mysql8.0-retiring-support-for-the-query-cache.png) - -## MySQL 日志 - -MySQL 日志常见的面试题有: - -- MySQL 中常见的日志有哪些? -- 慢查询日志有什么用? -- binlog 主要记录了什么? -- redo log 如何保证事务的持久性? -- 页修改之后为什么不直接刷盘呢? -- binlog 和 redolog 有什么区别? -- undo log 如何保证事务的原子性? -- …… - -上诉问题的答案可以在[《Java 面试指北》(付费)](../../zhuanlan/java-mian-shi-zhi-bei.md) 的 **「技术面试题篇」** 中找到。 - -![《Java 面试指北》技术面试题篇](https://oss.javaguide.cn/javamianshizhibei/technical-interview-questions.png) - -## MySQL 事务 - -### 何谓事务? - -我们设想一个场景,这个场景中我们需要插入多条相关联的数据到数据库,不幸的是,这个过程可能会遇到下面这些问题: - -- 数据库中途突然因为某些原因挂掉了。 -- 客户端突然因为网络原因连接不上数据库了。 -- 并发访问数据库时,多个线程同时写入数据库,覆盖了彼此的更改。 -- …… - -上面的任何一个问题都可能会导致数据的不一致性。为了保证数据的一致性,系统必须能够处理这些问题。事务就是我们抽象出来简化这些问题的首选机制。事务的概念起源于数据库,目前,已经成为一个比较广泛的概念。 - -**何为事务?** 一言蔽之,**事务是逻辑上的一组操作,要么都执行,要么都不执行。** - -事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账 1000 元,这个转账会涉及到两个关键操作,这两个操作必须都成功或者都失败。 - -1. 将小明的余额减少 1000 元 -2. 将小红的余额增加 1000 元。 - -事务会把这两个操作就可以看成逻辑上的一个整体,这个整体包含的操作要么都成功,要么都要失败。这样就不会出现小明余额减少而小红的余额却并没有增加的情况。 - -![事务示意图](https://oss.javaguide.cn/github/javaguide/mysql/%E4%BA%8B%E5%8A%A1%E7%A4%BA%E6%84%8F%E5%9B%BE.png) - -### 何谓数据库事务? - -大多数情况下,我们在谈论事务的时候,如果没有特指**分布式事务**,往往指的就是**数据库事务**。 - -数据库事务在我们日常开发中接触的最多了。如果你的项目属于单体架构的话,你接触到的往往就是数据库事务了。 - -**那数据库事务有什么作用呢?** - -简单来说,数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体。构成这个逻辑上的整体的这些数据库操作遵循:**要么全部执行成功,要么全部不执行** 。 - -```sql -# 开启一个事务 -START TRANSACTION; -# 多条 SQL 语句 -SQL1,SQL2... -## 提交事务 -COMMIT; -``` - -![数据库事务示意图](https://oss.javaguide.cn/github/javaguide/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BA%8B%E5%8A%A1%E7%A4%BA%E6%84%8F%E5%9B%BE.png) - -另外,关系型数据库(例如:`MySQL`、`SQL Server`、`Oracle` 等)事务都有 **ACID** 特性: - -![ACID](https://oss.javaguide.cn/github/javaguide/mysql/ACID.png) - -1. **原子性**(`Atomicity`):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; -2. **一致性**(`Consistency`):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的; -3. **隔离性**(`Isolation`):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; -4. **持久性**(`Durability`):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 - -🌈 这里要额外补充一点:**只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!** 想必大家也和我一样,被 ACID 这个概念被误导了很久! 我也是看周志明老师的公开课[《周志明的软件架构课》](https://time.geekbang.org/opencourse/intro/100064201)才搞清楚的(多看好书!!!)。 - -![AID->C](https://oss.javaguide.cn/github/javaguide/mysql/AID-%3EC.png) - -另外,DDIA 也就是 [《Designing Data-Intensive Application(数据密集型应用系统设计)》](https://book.douban.com/subject/30329536/) 的作者在他的这本书中如是说: - -> Atomicity, isolation, and durability are properties of the database, whereas consis‐ -> tency (in the ACID sense) is a property of the application. The application may rely -> on the database’s atomicity and isolation properties in order to achieve consistency, -> but it’s not up to the database alone. -> -> 翻译过来的意思是:原子性,隔离性和持久性是数据库的属性,而一致性(在 ACID 意义上)是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。因此,字母 C 不属于 ACID 。 - -《Designing Data-Intensive Application(数据密集型应用系统设计)》这本书强推一波,值得读很多遍!豆瓣有接近 90% 的人看了这本书之后给了五星好评。另外,中文翻译版本已经在 GitHub 开源,地址:[https://github.com/Vonng/ddia](https://github.com/Vonng/ddia) 。 - -![](https://oss.javaguide.cn/github/javaguide/books/ddia.png) - -### 并发事务带来了哪些问题? - -在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。 - -#### 脏读(Dirty read) - -一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据,这也就是脏读的由来。 - -例如:事务 1 读取某表中的数据 A=20,事务 1 修改 A=A-1,事务 2 读取到 A = 19,事务 1 回滚导致对 A 的修改并未提交到数据库, A 的值还是 20。 - -![脏读](./images/concurrency-consistency-issues-dirty-reading.png) - -#### 丢失修改(Lost to modify) - -在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 - -例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 先修改 A=A-1,事务 2 后来也修改 A=A-1,最终结果 A=19,事务 1 的修改被丢失。 - -![丢失修改](./images/concurrency-consistency-issues-missing-modifications.png) - -#### 不可重复读(Unrepeatable read) - -指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。 - -例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 再次读取 A =19,此时读取的结果和第一次读取的结果不同。 - -![不可重复读](./images/concurrency-consistency-issues-unrepeatable-read.png) - -#### 幻读(Phantom read) - -幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。 - -例如:事务 2 读取某个范围的数据,事务 1 在这个范围插入了新的数据,事务 2 再次读取这个范围的数据发现相比于第一次读取的结果多了新的数据。 - -![幻读](./images/concurrency-consistency-issues-phantom-read.png) - -### 不可重复读和幻读有什么区别? - -- 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改; -- 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。 - -幻读其实可以看作是不可重复读的一种特殊情况,单独把幻读区分出来的原因主要是解决幻读和不可重复读的方案不一样。 - -举个例子:执行 `delete` 和 `update` 操作的时候,可以直接对记录加锁,保证事务安全。而执行 `insert` 操作的时候,由于记录锁(Record Lock)只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁(Gap Lock)。也就是说执行 `insert` 操作的时候需要依赖 Next-Key Lock(Record Lock+Gap Lock) 进行加锁来保证不出现幻读。 - -### 并发事务的控制方式有哪些? - -MySQL 中并发事务的控制方式无非就两种:**锁** 和 **MVCC**。锁可以看作是悲观控制的模式,多版本并发控制(MVCC,Multiversion concurrency control)可以看作是乐观控制的模式。 - -**锁** 控制方式下会通过锁来显式控制共享资源而不是通过调度手段,MySQL 中主要是通过 **读写锁** 来实现并发控制。 - -- **共享锁(S 锁)**:又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。 -- **排他锁(X 锁)**:又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。 - -读写锁可以做到读读并行,但是无法做到写读、写写并行。另外,根据根据锁粒度的不同,又被分为 **表级锁(table-level locking)** 和 **行级锁(row-level locking)** 。InnoDB 不光支持表级锁,还支持行级锁,默认为行级锁。行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说, InnoDB 的性能更高。不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类。 - -**MVCC** 是多版本并发控制方法,即对一份数据会存储多个版本,通过事务的可见性来保证事务能看到自己应该看到的版本。通常会有一个全局的版本分配器来为每一行数据设置版本号,版本号是唯一的。 - -MVCC 在 MySQL 中实现所依赖的手段主要是: **隐藏字段、read view、undo log**。 - -- undo log : undo log 用于记录某行数据的多个版本的数据。 -- read view 和 隐藏字段 : 用来判断当前版本数据的可见性。 - -关于 InnoDB 对 MVCC 的具体实现可以看这篇文章:[InnoDB 存储引擎对 MVCC 的实现](./innodb-implementation-of-mvcc.md) 。 - -### SQL 标准定义了哪些事务隔离级别? - -SQL 标准定义了四个隔离级别: - -- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。 -- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。 -- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 -- **SERIALIZABLE(可串行化)** :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。 - ---- - -| 隔离级别 | 脏读 | 不可重复读 | 幻读 | -| :--------------: | :--: | :--------: | :--: | -| READ-UNCOMMITTED | √ | √ | √ | -| READ-COMMITTED | × | √ | √ | -| REPEATABLE-READ | × | × | √ | -| SERIALIZABLE | × | × | × | - -### MySQL 的隔离级别是基于锁实现的吗? - -MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。 - -SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。 - -### MySQL 的默认隔离级别是什么? - -MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;` - -```sql -mysql> SELECT @@tx_isolation; -+-----------------+ -| @@tx_isolation | -+-----------------+ -| REPEATABLE-READ | -+-----------------+ -``` - -关于 MySQL 事务隔离级别的详细介绍,可以看看我写的这篇文章:[MySQL 事务隔离级别详解](./transaction-isolation-level.md)。 - -## MySQL 锁 - -锁是一种常见的并发事务的控制方式。 - -### 表级锁和行级锁了解吗?有什么区别? - -MyISAM 仅仅支持表级锁(table-level locking),一锁就锁整张表,这在并发写的情况下性非常差。InnoDB 不光支持表级锁(table-level locking),还支持行级锁(row-level locking),默认为行级锁。 - -行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说, InnoDB 的性能更高。 - -**表级锁和行级锁对比**: - -- **表级锁:** MySQL 中锁定粒度最大的一种锁(全局锁除外),是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。 -- **行级锁:** MySQL 中锁定粒度最小的一种锁,是 **针对索引字段加的锁** ,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。行级锁和存储引擎有关,是在存储引擎层面实现的。 - -### 行级锁的使用有什么注意事项? - -InnoDB 的行锁是针对索引字段加的锁,表级锁是针对非索引字段加的锁。当我们执行 `UPDATE`、`DELETE` 语句时,如果 `WHERE`条件中字段没有命中唯一索引或者索引失效的话,就会导致扫描全表对表中的所有行记录进行加锁。这个在我们日常工作开发中经常会遇到,一定要多多注意!!! - -不过,很多时候即使用了索引也有可能会走全表扫描,这是因为 MySQL 优化器的原因。 - -### InnoDB 有哪几类行锁? - -InnoDB 行锁是通过对索引数据页上的记录加锁实现的,MySQL InnoDB 支持三种行锁定方式: - -- **记录锁(Record Lock)**:属于单个行记录上的锁。 -- **间隙锁(Gap Lock)**:锁定一个范围,不包括记录本身。 -- **临键锁(Next-Key Lock)**:Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题(MySQL 事务部分提到过)。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。 - -**在 InnoDB 默认的隔离级别 REPEATABLE-READ 下,行锁默认使用的是 Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。** - -一些大厂面试中可能会问到 Next-Key Lock 的加锁范围,这里推荐一篇文章:[MySQL next-key lock 加锁范围是什么? - 程序员小航 - 2021](https://segmentfault.com/a/1190000040129107) 。 - -### 共享锁和排他锁呢? - -不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类: - -- **共享锁(S 锁)**:又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。 -- **排他锁(X 锁)**:又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。 - -排他锁与任何的锁都不兼容,共享锁仅和共享锁兼容。 - -| | S 锁 | X 锁 | -| :--- | :----- | :--- | -| S 锁 | 不冲突 | 冲突 | -| X 锁 | 冲突 | 冲突 | - -由于 MVCC 的存在,对于一般的 `SELECT` 语句,InnoDB 不会加任何锁。不过, 你可以通过以下语句显式加共享锁或排他锁。 - -```sql -# 共享锁 可以在 MySQL 5.7 和 MySQL 8.0 中使用 -SELECT ... LOCK IN SHARE MODE; -# 共享锁 可以在 MySQL 8.0 中使用 -SELECT ... FOR SHARE; -# 排他锁 -SELECT ... FOR UPDATE; -``` - -### 意向锁有什么作用? - -如果需要用到表锁的话,如何判断表中的记录没有行锁呢,一行一行遍历肯定是不行,性能太差。我们需要用到一个叫做意向锁的东东来快速判断是否可以对某个表使用表锁。 - -意向锁是表级锁,共有两种: - -- **意向共享锁(Intention Shared Lock,IS 锁)**:事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。 -- **意向排他锁(Intention Exclusive Lock,IX 锁)**:事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。 - -**意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InnoDB 会先获取该数据行所在在数据表的对应意向锁。** - -意向锁之间是互相兼容的。 - -| | IS 锁 | IX 锁 | -| ----- | ----- | ----- | -| IS 锁 | 兼容 | 兼容 | -| IX 锁 | 兼容 | 兼容 | - -意向锁和共享锁和排它锁互斥(这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)。 - -| | IS 锁 | IX 锁 | -| ---- | ----- | ----- | -| S 锁 | 兼容 | 互斥 | -| X 锁 | 互斥 | 互斥 | - -《MySQL 技术内幕 InnoDB 存储引擎》这本书对应的描述应该是笔误了。 - -![](https://oss.javaguide.cn/github/javaguide/mysql/image-20220511171419081.png) - -### 当前读和快照读有什么区别? - -**快照读**(一致性非锁定读)就是单纯的 `SELECT` 语句,但不包括下面这两类 `SELECT` 语句: - -```sql -SELECT ... FOR UPDATE -# 共享锁 可以在 MySQL 5.7 和 MySQL 8.0 中使用 -SELECT ... LOCK IN SHARE MODE; -# 共享锁 可以在 MySQL 8.0 中使用 -SELECT ... FOR SHARE; -``` - -快照即记录的历史版本,每行记录可能存在多个历史版本(多版本技术)。 - -快照读的情况下,如果读取的记录正在执行 UPDATE/DELETE 操作,读取操作不会因此去等待记录上 X 锁的释放,而是会去读取行的一个快照。 - -只有在事务隔离级别 RC(读取已提交) 和 RR(可重读)下,InnoDB 才会使用一致性非锁定读: - -- 在 RC 级别下,对于快照数据,一致性非锁定读总是读取被锁定行的最新一份快照数据。 -- 在 RR 级别下,对于快照数据,一致性非锁定读总是读取本事务开始时的行数据版本。 - -快照读比较适合对于数据一致性要求不是特别高且追求极致性能的业务场景。 - -**当前读** (一致性锁定读)就是给行记录加 X 锁或 S 锁。 - -当前读的一些常见 SQL 语句类型如下: - -```sql -# 对读的记录加一个X锁 -SELECT...FOR UPDATE -# 对读的记录加一个S锁 -SELECT...LOCK IN SHARE MODE -# 对读的记录加一个S锁 -SELECT...FOR SHARE -# 对修改的记录加一个X锁 -INSERT... -UPDATE... -DELETE... -``` - -### 自增锁有了解吗? - -> 不太重要的一个知识点,简单了解即可。 - -关系型数据库设计表的时候,通常会有一列作为自增主键。InnoDB 中的自增主键会涉及一种比较特殊的表级锁— **自增锁(AUTO-INC Locks)** 。 - -```sql -CREATE TABLE `sequence_id` ( - `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, - `stub` CHAR(10) NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - UNIQUE KEY `stub` (`stub`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -更准确点来说,不仅仅是自增主键,`AUTO_INCREMENT`的列都会涉及到自增锁,毕竟非主键也可以设置自增长。 - -如果一个事务正在插入数据到有自增列的表时,会先获取自增锁,拿不到就可能会被阻塞住。这里的阻塞行为只是自增锁行为的其中一种,可以理解为自增锁就是一个接口,其具体的实现有多种。具体的配置项为 `innodb_autoinc_lock_mode` (MySQL 5.1.22 引入),可以选择的值如下: - -| innodb_autoinc_lock_mode | 介绍 | -| :----------------------- | :----------------------------- | -| 0 | 传统模式 | -| 1 | 连续模式(MySQL 8.0 之前默认) | -| 2 | 交错模式(MySQL 8.0 之后默认) | - -交错模式下,所有的“INSERT-LIKE”语句(所有的插入语句,包括:`INSERT`、`REPLACE`、`INSERT…SELECT`、`REPLACE…SELECT`、`LOAD DATA`等)都不使用表级锁,使用的是轻量级互斥锁实现,多条插入语句可以并发执行,速度更快,扩展性也更好。 - -不过,如果你的 MySQL 数据库有主从同步需求并且 Binlog 存储格式为 Statement 的话,不要将 InnoDB 自增锁模式设置为交叉模式,不然会有数据不一致性问题。这是因为并发情况下插入语句的执行顺序就无法得到保障。 - -> 如果 MySQL 采用的格式为 Statement ,那么 MySQL 的主从同步实际上同步的就是一条一条的 SQL 语句。 - -最后,再推荐一篇文章:[为什么 MySQL 的自增主键不单调也不连续](https://draveness.me/whys-the-design-mysql-auto-increment/) 。 - -## MySQL 性能优化 - -关于 MySQL 性能优化的建议总结,请看这篇文章:[MySQL 高性能优化规范建议总结](./mysql-high-performance-optimization-specification-recommendations.md) 。 - -### 能用 MySQL 直接存储文件(比如图片)吗? - -可以是可以,直接存储文件对应的二进制数据即可。不过,还是建议不要在数据库中存储文件,会严重影响数据库性能,消耗过多存储空间。 - -可以选择使用云服务厂商提供的开箱即用的文件存储服务,成熟稳定,价格也比较低。 - -![](https://oss.javaguide.cn/github/javaguide/mysql/oss-search.png) - -也可以选择自建文件存储服务,实现起来也不难,基于 FastDFS、MinIO(推荐) 等开源项目就可以实现分布式文件服务。 - -**数据库只存储文件地址信息,文件由文件存储服务负责存储。** - -相关阅读:[Spring Boot 整合 MinIO 实现分布式文件服务](https://www.51cto.com/article/716978.html) 。 - -### MySQL 如何存储 IP 地址? - -可以将 IP 地址转换成整形数据存储,性能更好,占用空间也更小。 - -MySQL 提供了两个方法来处理 ip 地址 - -- `INET_ATON()`:把 ip 转为无符号整型 (4-8 位) -- `INET_NTOA()` :把整型的 ip 转为地址 - -插入数据前,先用 `INET_ATON()` 把 ip 地址转为整型,显示数据时,使用 `INET_NTOA()` 把整型的 ip 地址转为地址显示即可。 - -### 有哪些常见的 SQL 优化手段? - -[《Java 面试指北》(付费)](../../zhuanlan/java-mian-shi-zhi-bei.md) 的 **「技术面试题篇」** 有一篇文章详细介绍了常见的 SQL 优化手段,非常全面,清晰易懂! - -![常见的 SQL 优化手段](https://oss.javaguide.cn/javamianshizhibei/javamianshizhibei-sql-optimization.png) - -### 如何分析 SQL 的性能? - -我们可以使用 `EXPLAIN` 命令来分析 SQL 的 **执行计划** 。执行计划是指一条 SQL 语句在经过 MySQL 查询优化器的优化会后,具体的执行方式。 - -`EXPLAIN` 并不会真的去执行相关的语句,而是通过 **查询优化器** 对语句进行分析,找出最优的查询方案,并显示对应的信息。 - -`EXPLAIN` 适用于 `SELECT`, `DELETE`, `INSERT`, `REPLACE`, 和 `UPDATE`语句,我们一般分析 `SELECT` 查询较多。 - -我们这里简单来演示一下 `EXPLAIN` 的使用。 - -`EXPLAIN` 的输出格式如下: - -```sql -mysql> EXPLAIN SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC; -+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+ -| 1 | SIMPLE | cus_order | NULL | ALL | NULL | NULL | NULL | NULL | 997572 | 100.00 | Using filesort | -+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+ -1 row in set, 1 warning (0.00 sec) -``` - -各个字段的含义如下: - -| **列名** | **含义** | -| ------------- | -------------------------------------------- | -| id | SELECT 查询的序列标识符 | -| select_type | SELECT 关键字对应的查询类型 | -| table | 用到的表名 | -| partitions | 匹配的分区,对于未分区的表,值为 NULL | -| type | 表的访问方法 | -| possible_keys | 可能用到的索引 | -| key | 实际用到的索引 | -| key_len | 所选索引的长度 | -| ref | 当使用索引等值查询时,与索引作比较的列或常量 | -| rows | 预计要读取的行数 | -| filtered | 按表条件过滤后,留存的记录数的百分比 | -| Extra | 附加信息 | - -篇幅问题,我这里只是简单介绍了一下 MySQL 执行计划,详细介绍请看:[SQL 的执行计划](./mysql-query-execution-plan.md)这篇文章。 - -### 读写分离和分库分表了解吗? - -读写分离和分库分表相关的问题比较多,于是,我单独写了一篇文章来介绍:[读写分离和分库分表详解](../../high-performance/read-and-write-separation-and-library-subtable.md)。 - -### 深度分页如何优化? - -[深度分页介绍及优化建议](../../high-performance/deep-pagination-optimization.md) - -### 数据冷热分离如何做? - -[数据冷热分离详解](../../high-performance/data-cold-hot-separation.md) - -### MySQL 性能怎么优化? - -MySQL 性能优化是一个系统性工程,涉及多个方面,在面试中不可能面面俱到。因此,建议按照“点-线-面”的思路展开,从核心问题入手,再逐步扩展,展示出你对问题的思考深度和解决能力。 - -**1. 抓住核心:慢 SQL 定位与分析** - -性能优化的第一步永远是找到瓶颈。面试时,建议先从 **慢 SQL 定位和分析** 入手,这不仅能展示你解决问题的思路,还能体现你对数据库性能监控的熟练掌握: - -- **监控工具:** 介绍常用的慢 SQL 监控工具,如 **MySQL 慢查询日志**、**Performance Schema** 等,说明你对这些工具的熟悉程度以及如何通过它们定位问题。 -- **EXPLAIN 命令:** 详细说明 `EXPLAIN` 命令的使用,分析查询计划、索引使用情况,可以结合实际案例展示如何解读分析结果,比如执行顺序、索引使用情况、全表扫描等。 - -**2. 由点及面:索引、表结构和 SQL 优化** - -定位到慢 SQL 后,接下来就要针对具体问题进行优化。 这里可以重点介绍索引、表结构和 SQL 编写规范等方面的优化技巧: - -- **索引优化:** 这是 MySQL 性能优化的重点,可以介绍索引的创建原则、覆盖索引、最左前缀匹配原则等。如果能结合你项目的实际应用来说明如何选择合适的索引,会更加分一些。 -- **表结构优化:** 优化表结构设计,包括选择合适的字段类型、避免冗余字段、合理使用范式和反范式设计等等。 -- **SQL 优化:** 避免使用 `SELECT *`、尽量使用具体字段、使用连接查询代替子查询、合理使用分页查询、批量操作等,都是 SQL 编写过程中需要注意的细节。 - -**3. 进阶方案:架构优化** - -当面试官对基础优化知识比较满意时,可能会深入探讨一些架构层面的优化方案。以下是一些常见的架构优化策略: - -- **读写分离:** 将读操作和写操作分离到不同的数据库实例,提升数据库的并发处理能力。 -- **分库分表:** 将数据分散到多个数据库实例或数据表中,降低单表数据量,提升查询效率。但要权衡其带来的复杂性和维护成本,谨慎使用。 -- **数据冷热分离**:根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在存储在低成本、低性能的介质中,热数据高性能存储介质中。 -- **缓存机制:** 使用 Redis 等缓存中间件,将热点数据缓存到内存中,减轻数据库压力。这个非常常用,提升效果非常明显,性价比极高! - -**4. 其他优化手段** - -除了慢 SQL 定位、索引优化和架构优化,还可以提及一些其他优化手段,展示你对 MySQL 性能调优的全面理解: - -- **连接池配置:** 配置合理的数据库连接池(如 **连接池大小**、**超时时间** 等),能够有效提升数据库连接的效率,避免频繁的连接开销。 -- **硬件配置:** 提升硬件性能也是优化的重要手段之一。使用高性能服务器、增加内存、使用 **SSD** 硬盘等硬件升级,都可以有效提升数据库的整体性能。 - -**5.总结** - -在面试中,建议按优先级依次介绍慢 SQL 定位、[索引优化](./mysql-index.md)、表结构设计和 [SQL 优化](../../high-performance/sql-optimization.md)等内容。架构层面的优化,如[读写分离和分库分表](../../high-performance/read-and-write-separation-and-library-subtable.md)、[数据冷热分离](../../high-performance/data-cold-hot-separation.md) 应作为最后的手段,除非在特定场景下有明显的性能瓶颈,否则不应轻易使用,因其引入的复杂性会带来额外的维护成本。 - -## MySQL 学习资料推荐 - -[**书籍推荐**](../../books/database.md#mysql) 。 - -**文章推荐** : - -- [一树一溪的 MySQL 系列教程](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg3NTc3NjM4Nw==&action=getalbum&album_id=2372043523518300162&scene=173&from_msgid=2247484308&from_itemidx=1&count=3&nolastread=1#wechat_redirect) -- [Yes 的 MySQL 系列教程](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzkxNTE3NjQ3MA==&action=getalbum&album_id=1903249596194095112&scene=173&from_msgid=2247490365&from_itemidx=1&count=3&nolastread=1#wechat_redirect) -- [写完这篇 我的 SQL 优化能力直接进入新层次 - 变成派大星 - 2022](https://juejin.cn/post/7161964571853815822) -- [两万字详解!InnoDB 锁专题! - 捡田螺的小男孩 - 2022](https://juejin.cn/post/7094049650428084232) -- [MySQL 的自增主键一定是连续的吗? - 飞天小牛肉 - 2022](https://mp.weixin.qq.com/s/qci10h9rJx_COZbHV3aygQ) -- [深入理解 MySQL 索引底层原理 - 腾讯技术工程 - 2020](https://zhuanlan.zhihu.com/p/113917726) - -## 参考 - -- 《高性能 MySQL》第 7 章 MySQL 高级特性 -- 《MySQL 技术内幕 InnoDB 存储引擎》第 6 章 锁 -- Relational Database: -- 一篇文章看懂 mysql 中 varchar 能存多少汉字、数字,以及 varchar(100)和 varchar(10)的区别: -- 技术分享 | 隔离级别:正确理解幻读: -- MySQL Server Logs - MySQL 5.7 Reference Manual: -- Redo Log - MySQL 5.7 Reference Manual: -- Locking Reads - MySQL 5.7 Reference Manual: -- 深入理解数据库行锁与表锁 -- 详解 MySQL InnoDB 中意向锁的作用: -- 深入剖析 MySQL 自增锁: -- 在数据库中不可重复读和幻读到底应该怎么分?: - - +Additionally, I recommend reading Chapter 4 of "High-Performance MySQL (3rd diff --git a/docs/database/mysql/some-thoughts-on-database-storage-time.md b/docs/database/mysql/some-thoughts-on-database-storage-time.md index e22ce2800da..6ee7fb8cda7 100644 --- a/docs/database/mysql/some-thoughts-on-database-storage-time.md +++ b/docs/database/mysql/some-thoughts-on-database-storage-time.md @@ -1,49 +1,49 @@ --- -title: MySQL日期类型选择建议 -category: 数据库 +title: MySQL Date Type Selection Recommendations +category: Database tag: - MySQL head: - - - meta - - name: keywords - content: MySQL 日期类型选择, MySQL 时间存储最佳实践, MySQL 时间存储效率, MySQL DATETIME 和 TIMESTAMP 区别, MySQL 时间戳存储, MySQL 数据库时间存储类型, MySQL 开发日期推荐, MySQL 字符串存储日期的缺点, MySQL 时区设置方法, MySQL 日期范围对比, 高性能 MySQL 日期存储, MySQL UNIX_TIMESTAMP 用法, 数值型时间戳优缺点, MySQL 时间存储性能优化, MySQL TIMESTAMP 时区转换, MySQL 时间格式转换, MySQL 时间存储空间对比, MySQL 时间类型选择建议, MySQL 日期类型性能分析, 数据库时间存储优化 + - - meta + - name: keywords + content: MySQL date type selection, MySQL time storage best practices, MySQL time storage efficiency, MySQL DATETIME and TIMESTAMP differences, MySQL timestamp storage, MySQL database time storage types, MySQL development date recommendations, MySQL disadvantages of storing dates as strings, MySQL timezone settings, MySQL date range comparison, high-performance MySQL date storage, MySQL UNIX_TIMESTAMP usage, advantages and disadvantages of numeric timestamps, MySQL time storage performance optimization, MySQL TIMESTAMP timezone conversion, MySQL time format conversion, MySQL time storage space comparison, MySQL time type selection recommendations, MySQL date type performance analysis, database time storage optimization --- -在日常的软件开发工作中,存储时间是一项基础且常见的需求。无论是记录数据的操作时间、金融交易的发生时间,还是行程的出发时间、用户的下单时间等等,时间信息与我们的业务逻辑和系统功能紧密相关。因此,正确选择和使用 MySQL 的日期时间类型至关重要,其恰当与否甚至可能对业务的准确性和系统的稳定性产生显著影响。 +In daily software development work, storing time is a fundamental and common requirement. Whether it's recording the operation time of data, the occurrence time of financial transactions, the departure time of a trip, or the order time of a user, time information is closely related to our business logic and system functionality. Therefore, correctly choosing and using MySQL's date and time types is crucial, as the appropriateness of this choice can significantly impact the accuracy of the business and the stability of the system. -本文旨在帮助开发者重新审视并深入理解 MySQL 中不同的时间存储方式,以便做出更合适项目业务场景的选择。 +This article aims to help developers re-examine and gain a deeper understanding of the different time storage methods in MySQL, so they can make more suitable choices for their project business scenarios. -## 不要用字符串存储日期 +## Do Not Use Strings to Store Dates -和许多数据库初学者一样,笔者在早期学习阶段也曾尝试使用字符串(如 VARCHAR)类型来存储日期和时间,甚至一度认为这是一种简单直观的方法。毕竟,'YYYY-MM-DD HH:MM:SS' 这样的格式看起来清晰易懂。 +Like many beginners in databases, I also tried to use strings (such as VARCHAR) to store dates and times during my early learning phase, and I even thought this was a simple and intuitive method. After all, a format like 'YYYY-MM-DD HH:MM:SS' looks clear and understandable. -但是,这是不正确的做法,主要会有下面两个问题: +However, this is an incorrect approach, mainly due to the following two issues: -1. **空间效率**:与 MySQL 内建的日期时间类型相比,字符串通常需要占用更多的存储空间来表示相同的时间信息。 -2. **查询与计算效率低下**: - - **比较操作复杂且低效**:基于字符串的日期比较需要按照字典序逐字符进行,这不仅不直观(例如,'2024-05-01' 会小于 '2024-1-10'),而且效率远低于使用原生日期时间类型进行的数值或时间点比较。 - - **计算功能受限**:无法直接利用数据库提供的丰富日期时间函数进行运算(例如,计算两个日期之间的间隔、对日期进行加减操作等),需要先转换格式,增加了复杂性。 - - **索引性能不佳**:基于字符串的索引在处理范围查询(如查找特定时间段内的数据)时,其效率和灵活性通常不如原生日期时间类型的索引。 +1. **Space Efficiency**: Compared to MySQL's built-in date and time types, strings typically require more storage space to represent the same time information. +1. **Inefficient Query and Calculation**: + - **Complex and Inefficient Comparison Operations**: Date comparisons based on strings need to be done character by character in dictionary order, which is not intuitive (for example, '2024-05-01' is less than '2024-1-10') and is far less efficient than using native date and time types for numerical or timestamp comparisons. + - **Limited Calculation Functions**: You cannot directly utilize the rich date and time functions provided by the database for calculations (such as calculating the interval between two dates or performing addition and subtraction on dates), requiring format conversion first, which adds complexity. + - **Poor Index Performance**: String-based indexes are usually less efficient and flexible than native date and time type indexes when handling range queries (such as finding data within a specific time period). -## DATETIME 和 TIMESTAMP 选择 +## Choosing Between DATETIME and TIMESTAMP -`DATETIME` 和 `TIMESTAMP` 是 MySQL 中两种非常常用的、用于存储包含日期和时间信息的数据类型。它们都可以存储精确到秒(MySQL 5.6.4+ 支持更高精度的小数秒)的时间值。那么,在实际应用中,我们应该如何在这两者之间做出选择呢? +`DATETIME` and `TIMESTAMP` are two very commonly used data types in MySQL for storing date and time information. Both can store time values accurate to the second (MySQL 5.6.4+ supports higher precision fractional seconds). So, how should we choose between the two in practical applications? -下面我们从几个关键维度对它们进行对比: +Let's compare them across several key dimensions: -### 时区信息 +### Time Zone Information -`DATETIME` 类型存储的是**字面量的日期和时间值**,它本身**不包含任何时区信息**。当你插入一个 `DATETIME` 值时,MySQL 存储的就是你提供的那个确切的时间,不会进行任何时区转换。 +The `DATETIME` type stores **literal date and time values** and does **not contain any time zone information**. When you insert a `DATETIME` value, MySQL stores the exact time you provided without performing any time zone conversion. -**这样就会有什么问题呢?** 如果你的应用需要支持多个时区,或者服务器、客户端的时区可能发生变化,那么使用 `DATETIME` 时,应用程序需要自行处理时区的转换和解释。如果处理不当(例如,假设所有存储的时间都属于同一个时区,但实际环境变化了),可能会导致时间显示或计算上的混乱。 +**What problems can this cause?** If your application needs to support multiple time zones, or if the time zones of the server and client may change, then when using `DATETIME`, the application must handle time zone conversion and interpretation on its own. If not handled properly (for example, assuming all stored times belong to the same time zone while the actual environment changes), it may lead to confusion in time display or calculations. -**`TIMESTAMP` 和时区有关**。存储时,MySQL 会将当前会话时区下的时间值转换成 UTC(协调世界时)进行内部存储。当查询 `TIMESTAMP` 字段时,MySQL 又会将存储的 UTC 时间转换回当前会话所设置的时区来显示。 +**`TIMESTAMP` is related to time zones**. When storing, MySQL converts the time value from the current session's time zone to UTC (Coordinated Universal Time) for internal storage. When querying a `TIMESTAMP` field, MySQL converts the stored UTC time back to the time zone set for the current session for display. -这意味着,对于同一条记录的 `TIMESTAMP` 字段,在不同的会话时区设置下查询,可能会看到不同的本地时间表示,但它们都对应着同一个绝对时间点(UTC 时间)。这对于需要全球化、多时区支持的应用来说非常有用。 +This means that for the same record's `TIMESTAMP` field, querying under different session time zone settings may yield different local time representations, but they all correspond to the same absolute time point (UTC time). This is very useful for applications that require globalization and multi-time zone support. -下面实际演示一下! +Let's demonstrate this practically! -建表 SQL 语句: +Table creation SQL statement: ```sql CREATE TABLE `time_zone_test` ( @@ -54,148 +54,4 @@ CREATE TABLE `time_zone_test` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` -插入一条数据(假设当前会话时区为系统默认,例如 UTC+0):: - -```sql -INSERT INTO time_zone_test(date_time,time_stamp) VALUES(NOW(),NOW()); -``` - -查询数据(在同一时区会话下): - -```sql -SELECT date_time, time_stamp FROM time_zone_test; -``` - -结果: - -```plain -+---------------------+---------------------+ -| date_time | time_stamp | -+---------------------+---------------------+ -| 2020-01-11 09:53:32 | 2020-01-11 09:53:32 | -+---------------------+---------------------+ -``` - -现在,修改当前会话的时区为东八区 (UTC+8): - -```sql -SET time_zone = '+8:00'; -``` - -再次查询数据: - -```bash -# TIMESTAMP 的值自动转换为 UTC+8 时间 -+---------------------+---------------------+ -| date_time | time_stamp | -+---------------------+---------------------+ -| 2020-01-11 09:53:32 | 2020-01-11 17:53:32 | -+---------------------+---------------------+ -``` - -**扩展:MySQL 时区设置常用 SQL 命令** - -```sql -# 查看当前会话时区 -SELECT @@session.time_zone; -# 设置当前会话时区 -SET time_zone = 'Europe/Helsinki'; -SET time_zone = "+00:00"; -# 数据库全局时区设置 -SELECT @@global.time_zone; -# 设置全局时区 -SET GLOBAL time_zone = '+8:00'; -SET GLOBAL time_zone = 'Europe/Helsinki'; -``` - -### 占用空间 - -下图是 MySQL 日期类型所占的存储空间(官方文档传送门:): - -![](https://oss.javaguide.cn/github/javaguide/FhRGUVHFK0ujRPNA75f6CuOXQHTE.jpeg) - -在 MySQL 5.6.4 之前,DateTime 和 TIMESTAMP 的存储空间是固定的,分别为 8 字节和 4 字节。但是从 MySQL 5.6.4 开始,它们的存储空间会根据毫秒精度的不同而变化,DateTime 的范围是 5~8 字节,TIMESTAMP 的范围是 4~7 字节。 - -### 表示范围 - -`TIMESTAMP` 表示的时间范围更小,只能到 2038 年: - -- `DATETIME`:'1000-01-01 00:00:00.000000' 到 '9999-12-31 23:59:59.999999' -- `TIMESTAMP`:'1970-01-01 00:00:01.000000' UTC 到 '2038-01-19 03:14:07.999999' UTC - -### 性能 - -由于 `TIMESTAMP` 在存储和检索时需要进行 UTC 与当前会话时区的转换,这个过程可能涉及到额外的计算开销,尤其是在需要调用操作系统底层接口获取或处理时区信息时。虽然现代数据库和操作系统对此进行了优化,但在某些极端高并发或对延迟极其敏感的场景下,`DATETIME` 因其不涉及时区转换,处理逻辑相对更简单直接,可能会表现出微弱的性能优势。 - -为了获得可预测的行为并可能减少 `TIMESTAMP` 的转换开销,推荐的做法是在应用程序层面统一管理时区,或者在数据库连接/会话级别显式设置 `time_zone` 参数,而不是依赖服务器的默认或操作系统时区。 - -## 数值时间戳是更好的选择吗? - -除了上述两种类型,实践中也常用整数类型(`INT` 或 `BIGINT`)来存储所谓的“Unix 时间戳”(即从 1970 年 1 月 1 日 00:00:00 UTC 起至目标时间的总秒数,或毫秒数)。 - -这种存储方式的具有 `TIMESTAMP` 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。 - -时间戳的定义如下: - -> 时间戳的定义是从一个基准时间开始算起,这个基准时间是「1970-1-1 00:00:00 +0:00」,从这个时间开始,用整数表示,以秒计时,随着时间的流逝这个时间整数不断增加。这样一来,我只需要一个数值,就可以完美地表示时间了,而且这个数值是一个绝对数值,即无论的身处地球的任何角落,这个表示时间的时间戳,都是一样的,生成的数值都是一样的,并且没有时区的概念,所以在系统的中时间的传输中,都不需要进行额外的转换了,只有在显示给用户的时候,才转换为字符串格式的本地时间。 - -数据库中实际操作: - -```sql --- 将日期时间字符串转换为 Unix 时间戳 (秒) -mysql> SELECT UNIX_TIMESTAMP('2020-01-11 09:53:32'); -+---------------------------------------+ -| UNIX_TIMESTAMP('2020-01-11 09:53:32') | -+---------------------------------------+ -| 1578707612 | -+---------------------------------------+ -1 row in set (0.00 sec) - --- 将 Unix 时间戳 (秒) 转换为日期时间格式 -mysql> SELECT FROM_UNIXTIME(1578707612); -+---------------------------+ -| FROM_UNIXTIME(1578707612) | -+---------------------------+ -| 2020-01-11 09:53:32 | -+---------------------------+ -1 row in set (0.01 sec) -``` - -## PostgreSQL 中没有 DATETIME - -由于有读者提到 PostgreSQL(PG) 的时间类型,因此这里拓展补充一下。PG 官方文档对时间类型的描述地址:。 - -![PostgreSQL 时间类型总结](https://oss.javaguide.cn/github/javaguide/mysql/pg-datetime-types.png) - -可以看到,PG 没有名为 `DATETIME` 的类型: - -- PG 的 `TIMESTAMP WITHOUT TIME ZONE`在功能上最接近 MySQL 的 `DATETIME`。它存储日期和时间,但不包含任何时区信息,存储的是字面值。 -- PG 的`TIMESTAMP WITH TIME ZONE` (或 `TIMESTAMPTZ`) 相当于 MySQL 的 `TIMESTAMP`。它在存储时会将输入值转换为 UTC,并在检索时根据当前会话的时区进行转换显示。 - -对于绝大多数需要记录精确发生时间点的应用场景,`TIMESTAMPTZ`是 PostgreSQL 中最推荐、最健壮的选择,因为它能最好地处理时区复杂性。 - -## 总结 - -MySQL 中时间到底怎么存储才好?`DATETIME`?`TIMESTAMP`?还是数值时间戳? - -并没有一个银弹,很多程序员会觉得数值型时间戳是真的好,效率又高还各种兼容,但是很多人又觉得它表现的不够直观。 - -《高性能 MySQL 》这本神书的作者就是推荐 TIMESTAMP,原因是数值表示时间不够直观。下面是原文: - - - -每种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型: - -| 类型 | 存储空间 | 日期格式 | 日期范围 | 是否带时区信息 | -| ------------ | -------- | ------------------------------ | ------------------------------------------------------------ | -------------- | -| DATETIME | 5~8 字节 | YYYY-MM-DD hh:mm:ss[.fraction] | 1000-01-01 00:00:00[.000000] ~ 9999-12-31 23:59:59[.999999] | 否 | -| TIMESTAMP | 4~7 字节 | YYYY-MM-DD hh:mm:ss[.fraction] | 1970-01-01 00:00:01[.000000] ~ 2038-01-19 03:14:07[.999999] | 是 | -| 数值型时间戳 | 4 字节 | 全数字如 1578707612 | 1970-01-01 00:00:01 之后的时间 | 否 | - -**选择建议小结:** - -- `TIMESTAMP` 的核心优势在于其内建的时区处理能力。数据库负责 UTC 存储和基于会话时区的自动转换,简化了需要处理多时区应用的开发。如果应用需要处理多时区,或者希望数据库能自动管理时区转换,`TIMESTAMP` 是自然的选择(注意其时间范围限制,也就是 2038 年问题)。 -- 如果应用场景不涉及时区转换,或者希望应用程序完全控制时区逻辑,并且需要表示 2038 年之后的时间,`DATETIME` 是更稳妥的选择。 -- 如果极度关注比较性能,或者需要频繁跨系统传递时间数据,并且可以接受可读性的牺牲(或总是在应用层转换),数值时间戳是一个强大的选项。 - - +Insert a diff --git a/docs/database/mysql/transaction-isolation-level.md b/docs/database/mysql/transaction-isolation-level.md index 52ad40f4a47..46b60051291 100644 --- a/docs/database/mysql/transaction-isolation-level.md +++ b/docs/database/mysql/transaction-isolation-level.md @@ -1,33 +1,33 @@ --- -title: MySQL事务隔离级别详解 -category: 数据库 +title: Detailed Explanation of MySQL Transaction Isolation Levels +category: Database tag: - MySQL --- -> 本文由 [SnailClimb](https://github.com/Snailclimb) 和 [guang19](https://github.com/guang19) 共同完成。 +> This article is co-authored by [SnailClimb](https://github.com/Snailclimb) and [guang19](https://github.com/guang19). -关于事务基本概览的介绍,请看这篇文章的介绍:[MySQL 常见知识点&面试题总结](./mysql-questions-01.md#MySQL-事务) +For an overview of transactions, please refer to this article: [Summary of Common MySQL Knowledge Points & Interview Questions](./mysql-questions-01.md#MySQL-%E4%BA%8B%E5%8A%A1) -## 事务隔离级别总结 +## Summary of Transaction Isolation Levels -SQL 标准定义了四个隔离级别: +The SQL standard defines four isolation levels: -- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。 -- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。 -- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 -- **SERIALIZABLE(可串行化)** :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。 +- **READ-UNCOMMITTED**: The lowest isolation level, allowing reading of uncommitted data changes, which may lead to dirty reads, phantom reads, or non-repeatable reads. +- **READ-COMMITTED**: Allows reading of data that has been committed by concurrent transactions, preventing dirty reads, but phantom reads or non-repeatable reads may still occur. +- **REPEATABLE-READ**: Ensures that multiple reads of the same field yield consistent results unless the data is modified by the transaction itself. It prevents dirty reads and non-repeatable reads, but phantom reads may still occur. +- **SERIALIZABLE**: The highest isolation level, fully compliant with the ACID isolation level. All transactions are executed sequentially, preventing any interference between transactions, meaning this level can prevent dirty reads, non-repeatable reads, and phantom reads. ---- +______________________________________________________________________ -| 隔离级别 | 脏读 | 不可重复读 | 幻读 | -| :--------------: | :--: | :--------: | :--: | -| READ-UNCOMMITTED | √ | √ | √ | -| READ-COMMITTED | × | √ | √ | -| REPEATABLE-READ | × | × | √ | -| SERIALIZABLE | × | × | × | +| Isolation Level | Dirty Read | Non-repeatable Read | Phantom Read | +| :--------------: | :--------: | :-----------------: | :----------: | +| READ-UNCOMMITTED | √ | √ | √ | +| READ-COMMITTED | × | √ | √ | +| REPEATABLE-READ | × | × | √ | +| SERIALIZABLE | × | × | × | -MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;` +The default isolation level supported by the MySQL InnoDB storage engine is **REPEATABLE-READ**. We can check this using the command `SELECT @@tx_isolation;`, which in MySQL 8.0 has changed to `SELECT @@transaction_isolation;` ```sql MySQL> SELECT @@tx_isolation; @@ -38,78 +38,34 @@ MySQL> SELECT @@tx_isolation; +-----------------+ ``` -从上面对 SQL 标准定义了四个隔离级别的介绍可以看出,标准的 SQL 隔离级别定义里,REPEATABLE-READ(可重复读)是不可以防止幻读的。 +From the introduction above regarding the four isolation levels defined by the SQL standard, it can be seen that the standard SQL isolation level definition does not prevent phantom reads for REPEATABLE-READ. -但是!InnoDB 实现的 REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的,主要有下面两种情况: +However! The REPEATABLE-READ isolation level implemented by InnoDB can actually resolve the issue of phantom reads, mainly in the following two situations: -- **快照读**:由 MVCC 机制来保证不出现幻读。 -- **当前读**:使用 Next-Key Lock 进行加锁来保证不出现幻读,Next-Key Lock 是行锁(Record Lock)和间隙锁(Gap Lock)的结合,行锁只能锁住已经存在的行,为了避免插入新行,需要依赖间隙锁。 +- **Snapshot Read**: Ensured by the MVCC mechanism to prevent phantom reads. +- **Current Read**: Uses Next-Key Lock for locking to prevent phantom reads. Next-Key Lock is a combination of row locks (Record Lock) and gap locks (Gap Lock). Row locks can only lock existing rows, and to prevent the insertion of new rows, gap locks are relied upon. -因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 **READ-COMMITTED** ,但是你要知道的是 InnoDB 存储引擎默认使用 **REPEATABLE-READ** 并不会有任何性能损失。 +Since lower isolation levels require fewer locks for transaction requests, most database systems use **READ-COMMITTED** as the default isolation level. However, it is important to note that the InnoDB storage engine's default use of **REPEATABLE-READ** does not incur any performance loss. -InnoDB 存储引擎在分布式事务的情况下一般会用到 SERIALIZABLE 隔离级别。 +In distributed transactions, the InnoDB storage engine generally uses the SERIALIZABLE isolation level. -《MySQL 技术内幕:InnoDB 存储引擎(第 2 版)》7.7 章这样写到: +Chapter 7.7 of "MySQL Internals: InnoDB Storage Engine (2nd Edition)" states: -> InnoDB 存储引擎提供了对 XA 事务的支持,并通过 XA 事务来支持分布式事务的实现。分布式事务指的是允许多个独立的事务资源(transactional resources)参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的 ACID 要求又有了提高。另外,在使用分布式事务时,InnoDB 存储引擎的事务隔离级别必须设置为 SERIALIZABLE。 +> The InnoDB storage engine provides support for XA transactions and uses XA transactions to support the implementation of distributed transactions. Distributed transactions allow multiple independent transactional resources to participate in a global transaction. Transactional resources are typically relational database systems, but can also include other types of resources. A global transaction requires that all participating transactions either commit or roll back, which raises the original ACID requirements for transactions. Additionally, when using distributed transactions, the transaction isolation level of the InnoDB storage engine must be set to SERIALIZABLE. -## 实际情况演示 +## Practical Demonstration -在下面我会使用 2 个命令行 MySQL ,模拟多线程(多事务)对同一份数据的脏读问题。 +Below, I will use two MySQL command line instances to simulate the dirty read problem with multiple threads (multiple transactions) on the same data. -MySQL 命令行的默认配置中事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作。如果要显式地开启一个事务需要使用命令:`START TRANSACTION`。 +In the default configuration of the MySQL command line, transactions are automatically committed, meaning that after executing an SQL statement, a COMMIT operation is immediately performed. To explicitly start a transaction, the command `START TRANSACTION` is used. -我们可以通过下面的命令来设置隔离级别。 +We can set the isolation level using the following command: ```sql SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE] ``` -我们再来看一下我们在下面实际操作中使用到的一些并发控制语句: - -- `START TRANSACTION` |`BEGIN`:显式地开启一个事务。 -- `COMMIT`:提交事务,使得对数据库做的所有修改成为永久性。 -- `ROLLBACK`:回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。 - -### 脏读(读未提交) - -![]() - -### 避免脏读(读已提交) - -![](https://oss.javaguide.cn/github/javaguide/2019-31-2%E8%AF%BB%E5%B7%B2%E6%8F%90%E4%BA%A4%E5%AE%9E%E4%BE%8B.jpg) - -### 不可重复读 - -还是刚才上面的读已提交的图,虽然避免了读未提交,但是却出现了,一个事务还没有结束,就发生了 不可重复读问题。 - -![](https://oss.javaguide.cn/github/javaguide/2019-32-1%E4%B8%8D%E5%8F%AF%E9%87%8D%E5%A4%8D%E8%AF%BB%E5%AE%9E%E4%BE%8B.jpg) - -### 可重复读 - -![](https://oss.javaguide.cn/github/javaguide/2019-33-2%E5%8F%AF%E9%87%8D%E5%A4%8D%E8%AF%BB.jpg) - -### 幻读 - -#### 演示幻读出现的情况 - -![](https://oss.javaguide.cn/github/javaguide/phantom_read.png) - -SQL 脚本 1 在第一次查询工资为 500 的记录时只有一条,SQL 脚本 2 插入了一条工资为 500 的记录,提交之后;SQL 脚本 1 在同一个事务中再次使用当前读查询发现出现了两条工资为 500 的记录这种就是幻读。 - -#### 解决幻读的方法 - -解决幻读的方式有很多,但是它们的核心思想就是一个事务在操作某张表数据的时候,另外一个事务不允许新增或者删除这张表中的数据了。解决幻读的方式主要有以下几种: - -1. 将事务隔离级别调整为 `SERIALIZABLE` 。 -2. 在可重复读的事务级别下,给事务操作的这张表添加表锁。 -3. 在可重复读的事务级别下,给事务操作的这张表添加 `Next-key Lock(Record Lock+Gap Lock)`。 - -### 参考 - -- 《MySQL 技术内幕:InnoDB 存储引擎》 -- -- [Mysql 锁:灵魂七拷问](https://tech.youzan.com/seven-questions-about-the-lock-of-MySQL/) -- [Innodb 中的事务隔离级别和锁的关系](https://tech.meituan.com/2014/08/20/innodb-lock.html) +Next, let's look at some concurrency control statements we will use in the practical operations below: - +- `START TRANSACTION` | `BEGIN`: Explicitly starts a transaction. +- \`COMMIT diff --git a/docs/database/nosql.md b/docs/database/nosql.md index d5ca59698bd..17bf3fa7070 100644 --- a/docs/database/nosql.md +++ b/docs/database/nosql.md @@ -1,61 +1,61 @@ --- -title: NoSQL基础知识总结 -category: 数据库 +title: Summary of NoSQL Fundamentals +category: Database tag: - NoSQL - MongoDB - Redis --- -## NoSQL 是什么? +## What is NoSQL? -NoSQL(Not Only SQL 的缩写)泛指非关系型的数据库,主要针对的是键值、文档以及图形类型数据存储。并且,NoSQL 数据库天生支持分布式,数据冗余和数据分片等特性,旨在提供可扩展的高可用高性能数据存储解决方案。 +NoSQL (Not Only SQL) refers to non-relational databases that primarily target key-value, document, and graph data storage. Moreover, NoSQL databases naturally support distributed features, data redundancy, and data partitioning, aiming to provide scalable, high-availability, and high-performance data storage solutions. -一个常见的误解是 NoSQL 数据库或非关系型数据库不能很好地存储关系型数据。NoSQL 数据库可以存储关系型数据—它们与关系型数据库的存储方式不同。 +A common misconception is that NoSQL databases or non-relational databases cannot store relational data well. NoSQL databases can store relational data—they just have a different storage method compared to relational databases. -NoSQL 数据库代表:HBase、Cassandra、MongoDB、Redis。 +Examples of NoSQL databases include: HBase, Cassandra, MongoDB, Redis. ![](https://oss.javaguide.cn/github/javaguide/database/mongodb/sql-nosql-tushi.png) -## SQL 和 NoSQL 有什么区别? +## What are the differences between SQL and NoSQL? -| | SQL 数据库 | NoSQL 数据库 | -| :----------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| 数据存储模型 | 结构化存储,具有固定行和列的表格 | 非结构化存储。文档:JSON 文档,键值:键值对,宽列:包含行和动态列的表,图:节点和边 | -| 发展历程 | 开发于 1970 年代,重点是减少数据重复 | 开发于 2000 年代后期,重点是提升可扩展性,减少大规模数据的存储成本 | -| 例子 | Oracle、MySQL、Microsoft SQL Server、PostgreSQL | 文档:MongoDB、CouchDB,键值:Redis、DynamoDB,宽列:Cassandra、 HBase,图表:Neo4j、 Amazon Neptune、Giraph | -| ACID 属性 | 提供原子性、一致性、隔离性和持久性 (ACID) 属性 | 通常不支持 ACID 事务,为了可扩展、高性能进行了权衡,少部分支持比如 MongoDB 。不过,MongoDB 对 ACID 事务 的支持和 MySQL 还是有所区别的。 | -| 性能 | 性能通常取决于磁盘子系统。要获得最佳性能,通常需要优化查询、索引和表结构。 | 性能通常由底层硬件集群大小、网络延迟以及调用应用程序来决定。 | -| 扩展 | 垂直(使用性能更强大的服务器进行扩展)、读写分离、分库分表 | 横向(增加服务器的方式横向扩展,通常是基于分片机制) | -| 用途 | 普通企业级的项目的数据存储 | 用途广泛比如图数据库支持分析和遍历连接数据之间的关系、键值数据库可以处理大量数据扩展和极高的状态变化 | -| 查询语法 | 结构化查询语言 (SQL) | 数据访问语法可能因数据库而异 | +| | SQL Databases | NoSQL Databases | +| :------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Data Storage Model | Structured storage with tables that have fixed rows and columns | Unstructured storage. Documents: JSON documents, Key-Value: Key-Value pairs, Wide Column: Tables with rows and dynamic columns, Graph: Nodes and edges | +| Development History | Developed in the 1970s, focusing on reducing data duplication | Developed in the late 2000s, focusing on improving scalability and reducing storage costs for massive amounts of data | +| Examples | Oracle, MySQL, Microsoft SQL Server, PostgreSQL | Documents: MongoDB, CouchDB; Key-Value: Redis, DynamoDB; Wide Column: Cassandra, HBase; Graph: Neo4j, Amazon Neptune, Giraph | +| ACID Properties | Provides Atomicity, Consistency, Isolation, and Durability (ACID) properties | Generally does not support ACID transactions, making trade-offs for scalability and high performance; some, like MongoDB, support them, but differently than MySQL. | +| Performance | Performance typically depends on the disk subsystem. To achieve optimal performance, optimizing queries, indexes, and table structures is often necessary. | Performance is usually determined by the size of the underlying hardware cluster, network latency, and the application calling. | +| Scalability | Vertical (scaling up using more powerful servers), read-write separation, database sharding | Horizontal (scaling out by adding servers, usually based on sharding mechanisms) | +| Use Cases | Data storage for typical enterprise-level projects | Wide-ranging uses, for example, graph databases support analysis and traversing relationships between connected data; key-value databases can handle massive data scaling and extreme state changes. | +| Query Language | Structured Query Language (SQL) | Data access syntax may vary by database | -## NoSQL 数据库有什么优势? +## What are the advantages of NoSQL databases? -NoSQL 数据库非常适合许多现代应用程序,例如移动、Web 和游戏等应用程序,它们需要灵活、可扩展、高性能和功能强大的数据库以提供卓越的用户体验。 +NoSQL databases are particularly suitable for many modern applications, such as mobile, web, and gaming applications, which require flexible, scalable, high-performance, and powerful databases to provide excellent user experiences. -- **灵活性:** NoSQL 数据库通常提供灵活的架构,以实现更快速、更多的迭代开发。灵活的数据模型使 NoSQL 数据库成为半结构化和非结构化数据的理想之选。 -- **可扩展性:** NoSQL 数据库通常被设计为通过使用分布式硬件集群来横向扩展,而不是通过添加昂贵和强大的服务器来纵向扩展。 -- **高性能:** NoSQL 数据库针对特定的数据模型和访问模式进行了优化,这与尝试使用关系数据库完成类似功能相比可实现更高的性能。 -- **强大的功能:** NoSQL 数据库提供功能强大的 API 和数据类型,专门针对其各自的数据模型而构建。 +- **Flexibility:** NoSQL databases often provide flexible architectures for faster and more iterative development. Their flexible data models make NoSQL databases ideal for semi-structured and unstructured data. +- **Scalability:** NoSQL databases are generally designed to scale horizontally by utilizing distributed hardware clusters rather than vertically scaling by adding expensive and powerful servers. +- **High Performance:** NoSQL databases are optimized for specific data models and access patterns, achieving higher performance compared to trying to accomplish similar functions with a relational database. +- **Powerful Features:** NoSQL databases offer powerful APIs and data types designed specifically for their respective data models. -## NoSQL 数据库有哪些类型? +## What types of NoSQL databases are there? -NoSQL 数据库主要可以分为下面四种类型: +NoSQL databases can primarily be divided into the following four types: -- **键值**:键值数据库是一种较简单的数据库,其中每个项目都包含键和值。这是极为灵活的 NoSQL 数据库类型,因为应用可以完全控制 value 字段中存储的内容,没有任何限制。Redis 和 DynanoDB 是两款非常流行的键值数据库。 -- **文档**:文档数据库中的数据被存储在类似于 JSON(JavaScript 对象表示法)对象的文档中,非常清晰直观。每个文档包含成对的字段和值。这些值通常可以是各种类型,包括字符串、数字、布尔值、数组或对象等,并且它们的结构通常与开发者在代码中使用的对象保持一致。MongoDB 就是一款非常流行的文档数据库。 -- **图形**:图形数据库旨在轻松构建和运行与高度连接的数据集一起使用的应用程序。图形数据库的典型使用案例包括社交网络、推荐引擎、欺诈检测和知识图形。Neo4j 和 Giraph 是两款非常流行的图形数据库。 -- **宽列**:宽列存储数据库非常适合需要存储大量的数据。Cassandra 和 HBase 是两款非常流行的宽列存储数据库。 +- **Key-Value:** Key-value databases are a simpler type of database where each item consists of a key and a value. This is a very flexible type of NoSQL database because applications have complete control over the content stored in the value field, with no restrictions. Redis and DynamoDB are two popular key-value databases. +- **Document:** Data in document databases is stored in documents resembling JSON (JavaScript Object Notation) objects, which are very clear and intuitive. Each document contains paired fields and values. These values can typically be of various types, including strings, numbers, booleans, arrays, or objects, and their structure often aligns with the objects developers use in code. MongoDB is a very popular document database. +- **Graph:** Graph databases are designed to make it easy to build and run applications that work with highly connected datasets. Typical use cases for graph databases include social networks, recommendation engines, fraud detection, and knowledge graphs. Neo4j and Giraph are two well-known graph databases. +- **Wide Column:** Wide-column store databases are well-suited for scenarios requiring the storage of massive amounts of data. Cassandra and HBase are two popular wide-column storage databases. -下面这张图片来源于 [微软的官方文档 | 关系数据与 NoSQL 数据](https://learn.microsoft.com/en-us/dotnet/architecture/cloud-native/relational-vs-nosql-data)。 +The image below is sourced from [Microsoft's official documentation | Relational Data vs. NoSQL Data](https://learn.microsoft.com/en-us/dotnet/architecture/cloud-native/relational-vs-nosql-data). -![NoSQL 数据模型](https://oss.javaguide.cn/github/javaguide/database/mongodb/types-of-nosql-datastores.png) +![NoSQL Data Model](https://oss.javaguide.cn/github/javaguide/database/mongodb/types-of-nosql-datastores.png) -## 参考 +## References -- NoSQL 是什么?- MongoDB 官方文档: -- 什么是 NoSQL? - AWS: -- NoSQL vs. SQL Databases - MongoDB 官方文档: +- What is NoSQL? - MongoDB Official Documentation: +- What is NoSQL? - AWS: +- NoSQL vs. SQL Databases - MongoDB Official Documentation: diff --git a/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md b/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md index 7ad88958704..ce2d47640e9 100644 --- a/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md +++ b/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md @@ -1,118 +1,87 @@ --- -title: 3种常用的缓存读写策略详解 -category: 数据库 +title: Detailed Explanation of 3 Common Cache Read/Write Strategies +category: Database tag: - Redis --- -看到很多小伙伴简历上写了“**熟练使用缓存**”,但是被我问到“**缓存常用的 3 种读写策略**”的时候却一脸懵逼。 +I've seen many friends list "**proficient in using cache**" on their resumes, but when I ask them about the "**3 common read/write strategies for cache**," they often look confused. -在我看来,造成这个问题的原因是我们在学习 Redis 的时候,可能只是简单写了一些 Demo,并没有去关注缓存的读写策略,或者说压根不知道这回事。 +In my opinion, this issue arises because when we learn Redis, we might only write some simple demos without paying attention to cache read/write strategies, or we might not even be aware of them. -但是,搞懂 3 种常见的缓存读写策略对于实际工作中使用缓存以及面试中被问到缓存都是非常有帮助的! +However, understanding the 3 common cache read/write strategies is very helpful for using cache in real work and for interview questions about cache! -**下面介绍到的三种模式各有优劣,不存在最佳模式,根据具体的业务场景选择适合自己的缓存读写模式。** +**The three patterns introduced below each have their pros and cons; there is no best pattern. Choose a cache read/write pattern that suits your specific business scenario.** -### Cache Aside Pattern(旁路缓存模式) +### Cache Aside Pattern -**Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。** +**The Cache Aside Pattern is a commonly used cache read/write pattern, particularly suitable for scenarios with a high volume of read requests.** -Cache Aside Pattern 中服务端需要同时维系 db 和 cache,并且是以 db 的结果为准。 +In the Cache Aside Pattern, the server needs to maintain both the database (db) and the cache, with the db results being the authoritative source. -下面我们来看一下这个策略模式下的缓存读写步骤。 +Let's take a look at the steps for reading and writing cache under this strategy. -**写**: +**Write:** -- 先更新 db -- 然后直接删除 cache 。 +- First, update the db. +- Then, delete the cache directly. -简单画了一张图帮助大家理解写的步骤。 +Here’s a simple diagram to help you understand the writing steps. ![](https://oss.javaguide.cn/github/javaguide/database/redis/cache-aside-write.png) -**读** : +**Read:** -- 从 cache 中读取数据,读取到就直接返回 -- cache 中读取不到的话,就从 db 中读取数据返回 -- 再把数据放到 cache 中。 +- Read data from the cache; if found, return it directly. +- If not found in the cache, read the data from the db and return it. +- Then, place the data into the cache. -简单画了一张图帮助大家理解读的步骤。 +Here’s a simple diagram to help you understand the reading steps. ![](https://oss.javaguide.cn/github/javaguide/database/redis/cache-aside-read.png) -你仅仅了解了上面这些内容的话是远远不够的,我们还要搞懂其中的原理。 +If you only understand the above content, it is far from enough; we also need to grasp the underlying principles. -比如说面试官很可能会追问:“**在写数据的过程中,可以先删除 cache ,后更新 db 么?**” +For example, the interviewer might follow up with: "**During the data writing process, can we delete the cache first and then update the db?**" -**答案:** 那肯定是不行的!因为这样可能会造成 **数据库(db)和缓存(Cache)数据不一致**的问题。 +**Answer:** Absolutely not! Because this could lead to the problem of **inconsistency between the database (db) and the cache.** -举例:请求 1 先写数据 A,请求 2 随后读数据 A 的话,就很有可能产生数据不一致性的问题。 +For instance: If request 1 writes data A first, and request 2 subsequently reads data A, it is very likely to cause data inconsistency. -这个过程可以简单描述为: +This process can be simply described as: -> 请求 1 先把 cache 中的 A 数据删除 -> 请求 2 从 db 中读取数据->请求 1 再把 db 中的 A 数据更新 +> Request 1 deletes data A from the cache -> Request 2 reads data from the db -> Request 1 updates data A in the db. -当你这样回答之后,面试官可能会紧接着就追问:“**在写数据的过程中,先更新 db,后删除 cache 就没有问题了么?**” +After you answer this, the interviewer might immediately ask: "**During the data writing process, is it okay to update the db first and then delete the cache?**" -**答案:** 理论上来说还是可能会出现数据不一致性的问题,不过概率非常小,因为缓存的写入速度是比数据库的写入速度快很多。 +**Answer:** Theoretically, there could still be a problem with data inconsistency, but the probability is very low because the write speed of the cache is much faster than that of the database. -举例:请求 1 先读数据 A,请求 2 随后写数据 A,并且数据 A 在请求 1 请求之前不在缓存中的话,也有可能产生数据不一致性的问题。 +For example: If request 1 reads data A first, and request 2 subsequently writes data A, and data A is not in the cache before request 1, there could still be a problem with data inconsistency. -这个过程可以简单描述为: +This process can be simply described as: -> 请求 1 从 db 读数据 A-> 请求 2 更新 db 中的数据 A(此时缓存中无数据 A ,故不用执行删除缓存操作 ) -> 请求 1 将数据 A 写入 cache +> Request 1 reads data A from the db -> Request 2 updates data A in the db (at this point, there is no data A in the cache, so no cache deletion is needed) -> Request 1 writes data A into the cache. -现在我们再来分析一下 **Cache Aside Pattern 的缺陷**。 +Now let's analyze the **deficiencies of the Cache Aside Pattern**. -**缺陷 1:首次请求数据一定不在 cache 的问题** +**Deficiency 1: The issue that the first request for data is definitely not in the cache.** -解决办法:可以将热点数据可以提前放入 cache 中。 +Solution: Hot data can be preloaded into the cache. -**缺陷 2:写操作比较频繁的话导致 cache 中的数据会被频繁被删除,这样会影响缓存命中率 。** +**Deficiency 2: Frequent write operations can lead to data being frequently deleted from the cache, which affects the cache hit rate.** -解决办法: +Solutions: -- 数据库和缓存数据强一致场景:更新 db 的时候同样更新 cache,不过我们需要加一个锁/分布式锁来保证更新 cache 的时候不存在线程安全问题。 -- 可以短暂地允许数据库和缓存数据不一致的场景:更新 db 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。 +- For scenarios requiring strong consistency between the database and cache: Update the cache when updating the db, but we need to add a lock/distributed lock to ensure thread safety when updating the cache. +- For scenarios where temporary inconsistency between the database and cache is acceptable: Update the cache when updating the db, but set a relatively short expiration time for the cache, which can ensure that even if data is inconsistent, the impact is minimal. -### Read/Write Through Pattern(读写穿透) +### Read/Write Through Pattern -Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。 +In the Read/Write Through Pattern, the server treats the cache as the primary data store, reading data from it and writing data into it. The cache service is responsible for reading and writing this data to the db, thereby reducing the responsibilities of the application. -这种缓存读写策略小伙伴们应该也发现了在平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存 Redis 并没有提供 cache 将数据写入 db 的功能。 +This cache read/write strategy is also rarely seen in our development process. Aside from performance impacts, it is likely because the distributed cache we often use, Redis, does not provide the functionality for the cache to write data to the db. -**写(Write Through):** +**Write (Write Through):** -- 先查 cache,cache 中不存在,直接更新 db。 -- cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(**同步更新 cache 和 db**)。 - -简单画了一张图帮助大家理解写的步骤。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/write-through.png) - -**读(Read Through):** - -- 从 cache 中读取数据,读取到就直接返回 。 -- 读取不到的话,先从 db 加载,写入到 cache 后返回响应。 - -简单画了一张图帮助大家理解读的步骤。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/read-through.png) - -Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。 - -和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。 - -### Write Behind Pattern(异步缓存写入) - -Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。 - -但是,两个又有很大的不同:**Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。** - -很明显,这种方式对数据一致性带来了更大的挑战,比如 cache 数据可能还没异步更新 db 的话,cache 服务可能就就挂掉了。 - -这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制都用到了这种策略。 - -Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。 - - +- First, check the cache; if it does not exist, directly update the db. +- If it exists in the cache, first update the cache, then the diff --git a/docs/database/redis/cache-basics.md b/docs/database/redis/cache-basics.md index 391e5bec82d..b82a91c1d3d 100644 --- a/docs/database/redis/cache-basics.md +++ b/docs/database/redis/cache-basics.md @@ -1,11 +1,11 @@ --- -title: 缓存基础常见面试题总结(付费) -category: 数据库 +title: Summary of Common Interview Questions on Cache Basics (Paid) +category: Database tag: - Redis --- -**缓存基础** 相关的面试题为我的 [知识星球](../../about-the-author/zhishixingqiu-two-years.md)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](../../zhuanlan/java-mian-shi-zhi-bei.md)中。 +Interview questions related to **cache basics** are exclusive content for my [Knowledge Planet](../../about-the-author/zhishixingqiu-two-years.md) (click the link to view detailed introduction and joining method), and have been organized into [“Java Interview Guide”](../../zhuanlan/java-mian-shi-zhi-bei.md). ![](https://oss.javaguide.cn/javamianshizhibei/database-questions.png) diff --git a/docs/database/redis/redis-cluster.md b/docs/database/redis/redis-cluster.md index e3ef2efd04c..480596dd84b 100644 --- a/docs/database/redis/redis-cluster.md +++ b/docs/database/redis/redis-cluster.md @@ -1,11 +1,11 @@ --- -title: Redis集群详解(付费) -category: 数据库 +title: Detailed Explanation of Redis Clusters (Paid) +category: Database tag: - Redis --- -**Redis 集群** 相关的面试题为我的 [知识星球](../../about-the-author/zhishixingqiu-two-years.md)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](../../zhuanlan/java-mian-shi-zhi-bei.md)中。 +The interview questions related to **Redis Clusters** are exclusive content for my [Knowledge Planet](../../about-the-author/zhishixingqiu-two-years.md) (click the link for a detailed introduction and joining methods) and have been compiled in [《Java Interview Guide》](../../zhuanlan/java-mian-shi-zhi-bei.md). ![](https://oss.javaguide.cn/xingqiu/mianshizhibei-database.png) diff --git a/docs/database/redis/redis-common-blocking-problems-summary.md b/docs/database/redis/redis-common-blocking-problems-summary.md index 9aec17fc0cc..62342697a2d 100644 --- a/docs/database/redis/redis-common-blocking-problems-summary.md +++ b/docs/database/redis/redis-common-blocking-problems-summary.md @@ -1,146 +1,144 @@ --- -title: Redis常见阻塞原因总结 -category: 数据库 +title: Summary of Common Blocking Reasons in Redis +category: Database tag: - Redis --- -> 本文整理完善自: ,作者:阿 Q 说代码 +> This article is compiled and perfected from: , author: A Q Talks Code -这篇文章会详细总结一下可能导致 Redis 阻塞的情况,这些情况也是影响 Redis 性能的关键因素,使用 Redis 的时候应该格外注意! +This article will summarize in detail the situations that may cause Redis blocking, which are also key factors affecting Redis performance. Care should be taken when using Redis! -## O(n) 命令 +## O(n) Commands -Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 O(n) 时间复杂度的命令,例如: +Most commands in Redis have an O(1) time complexity, but there are also a few commands with O(n) time complexity, such as: -- `KEYS *`:会返回所有符合规则的 key。 -- `HGETALL`:会返回一个 Hash 中所有的键值对。 -- `LRANGE`:会返回 List 中指定范围内的元素。 -- `SMEMBERS`:返回 Set 中的所有元素。 -- `SINTER`/`SUNION`/`SDIFF`:计算多个 Set 的交集/并集/差集。 +- `KEYS *`: Returns all keys that match the rules. +- `HGETALL`: Returns all key-value pairs in a Hash. +- `LRANGE`: Returns elements within a specified range in a List. +- `SMEMBERS`: Returns all elements in a Set. +- `SINTER`/`SUNION`/`SDIFF`: Calculates the intersection/union/difference of multiple Sets. - …… -由于这些命令时间复杂度是 O(n),有时候也会全表扫描,随着 n 的增大,执行耗时也会越长,从而导致客户端阻塞。不过, 这些命令并不是一定不能使用,但是需要明确 N 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。 +Due to the O(n) time complexity of these commands, sometimes a full table scan occurs. As n increases, the execution time will also increase, leading to client blocking. However, these commands are not entirely unusable, but the value of N needs to be clarified. If there is a need for traversal, `HSCAN`, `SSCAN`, or `ZSCAN` can be used instead. -除了这些 O(n)时间复杂度的命令可能会导致阻塞之外, 还有一些时间复杂度可能在 O(N) 以上的命令,例如: +In addition to these O(n) time complexity commands that may cause blocking, there are also commands with time complexities potentially greater than O(N), such as: -- `ZRANGE`/`ZREVRANGE`:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 为返回的元素数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。 -- `ZREMRANGEBYRANK`/`ZREMRANGEBYSCORE`:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。 +- `ZRANGE`/`ZREVRANGE`: Returns all elements within a specified ranking range in a Sorted Set. The time complexity is O(log(n)+m), where n is the total number of elements and m is the number of elements returned. When m and n are relatively large, the time complexity O(n) is smaller. +- `ZREMRANGEBYRANK`/`ZREMRANGEBYSCORE`: Removes all elements within a specified ranking range/specified score range in a Sorted Set. The time complexity is O(log(n)+m), where n is the total number of elements and m is the number of deleted elements. When m and n are relatively large, the time complexity O(n) is smaller. - …… -## SAVE 创建 RDB 快照 +## SAVE Creating RDB Snapshots -Redis 提供了两个命令来生成 RDB 快照文件: +Redis provides two commands to generate RDB snapshot files: -- `save` : 同步保存操作,会阻塞 Redis 主线程; -- `bgsave` : fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。 +- `save`: Synchronously saves operations and blocks the Redis main thread. +- `bgsave`: Forks a child process to execute, which does not block the Redis main thread, the default option. -默认情况下,Redis 默认配置会使用 `bgsave` 命令。如果手动使用 `save` 命令生成 RDB 快照文件的话,就会阻塞主线程。 +By default, Redis will use the `bgsave` command. If you manually use the `save` command to generate RDB snapshot files, it will block the main thread. ## AOF -### AOF 日志记录阻塞 +### AOF Log Record Blocking -Redis AOF 持久化机制是在执行完命令之后再记录日志,这和关系型数据库(如 MySQL)通常都是执行命令之前记录日志(方便故障恢复)不同。 +Redis's AOF persistence mechanism records logs after executing commands, differing from relational databases (like MySQL), which usually log before executing commands for recovery purposes. -![AOF 记录日志过程](https://oss.javaguide.cn/github/javaguide/database/redis/redis-aof-write-log-disc.png) +![AOF Log Recording Process](https://oss.javaguide.cn/github/javaguide/database/redis/redis-aof-write-log-disc.png) -**为什么是在执行完命令之后记录日志呢?** +**Why record logs after executing commands?** -- 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查; -- 在命令执行完之后再记录,不会阻塞当前的命令执行。 +- To avoid extra checking overhead, AOF log recording does not perform syntax checking on commands. +- Logging after command execution does not block the current command execution. -这样也带来了风险(我在前面介绍 AOF 持久化的时候也提到过): +This also brings risks (which I mentioned when introducing AOF persistence): -- 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失; -- **可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)**。 +- If Redis crashes right after executing a command, the corresponding modifications may be lost. +- **It may block the execution of subsequent commands (AOF log recording is conducted in the Redis main thread).** -### AOF 刷盘阻塞 +### AOF Flush Blocking -开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 `server.aof_buf` 中,然后再根据 `appendfsync` 配置来决定何时将其同步到硬盘中的 AOF 文件。 +After enabling AOF persistence, every command that changes data in Redis will write that command to the AOF buffer `server.aof_buf`, and then according to the `appendfsync` configuration, it decides when to synchronize it to the AOF file on the hard disk. -在 Redis 的配置文件中存在三种不同的 AOF 持久化方式( `fsync`策略),它们分别是: +There are three different AOF persistence methods (fsync strategy) in Redis's configuration file: -1. `appendfsync always`:主线程调用 `write` 执行写操作后,后台线程( `aof_fsync` 线程)立即会调用 `fsync` 函数同步 AOF 文件(刷盘),`fsync` 完成后线程返回,这样会严重降低 Redis 的性能(`write` + `fsync`)。 -2. `appendfsync everysec`:主线程调用 `write` 执行写操作后立即返回,由后台线程( `aof_fsync` 线程)每秒钟调用 `fsync` 函数(系统调用)同步一次 AOF 文件(`write`+`fsync`,`fsync`间隔为 1 秒) -3. `appendfsync no`:主线程调用 `write` 执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(`write`但不`fsync`,`fsync` 的时机由操作系统决定)。 +1. `appendfsync always`: The main thread calls `write` to perform a write operation, after which the background thread (`aof_fsync` thread) immediately calls the `fsync` function to sync the AOF file (flush). After `fsync` completes, the thread returns; this will severely decrease Redis's performance (write + fsync). +1. `appendfsync everysec`: The main thread calls `write` to perform a write operation and immediately returns, with the background thread (`aof_fsync` thread) calling the `fsync` function once every second to sync the AOF file (write + fsync, the fsync interval is 1 second). +1. `appendfsync no`: The main thread calls `write` to perform a write operation and immediately returns, allowing the operating system to decide when to sync, which is usually once every 30 seconds on Linux (write but no fsync; the timing of fsync is determined by the operating system). -当后台线程( `aof_fsync` 线程)调用 `fsync` 函数同步 AOF 文件时,需要等待,直到写入完成。当磁盘压力太大的时候,会导致 `fsync` 操作发生阻塞,主线程调用 `write` 函数时也会被阻塞。`fsync` 完成后,主线程执行 `write` 才能成功返回。 +When the background thread (`aof_fsync` thread) calls the `fsync` function to sync the AOF file, it needs to wait until the writing is complete. When there is too much disk pressure, blocking may occur during the `fsync` operation, and the main thread calling `write` will also be blocked. The main thread can only return successfully after `fsync` completes. -关于 AOF 工作流程的详细介绍可以查看:[Redis 持久化机制详解](./redis-persistence.md),有助于理解 AOF 刷盘阻塞。 +For a detailed introduction to the AOF workflow, you can refer to: [Detailed Explanation of Redis Persistence Mechanism](./redis-persistence.md), which helps understand AOF flush blocking. -### AOF 重写阻塞 +### AOF Rewrite Blocking -1. fork 出一条子线程来将文件重写,在执行 `BGREWRITEAOF` 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子线程创建新 AOF 文件期间,记录服务器执行的所有写命令。 -2. 当子线程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。 -3. 最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。 +1. A child thread is forked to rewrite the file. When executing the `BGREWRITEAOF` command, the Redis server maintains an AOF rewrite buffer that records all write commands executed by the server during the child thread's creation of the new AOF file. +1. After the child thread completes the task of creating the new AOF file, the server appends all content from the rewrite buffer to the end of the new AOF file, ensuring that the new AOF file preserves the database state consistent with the existing database state. +1. Finally, the server replaces the old AOF file with the new AOF file to complete the AOF file rewrite operation. -阻塞就是出现在第 2 步的过程中,将缓冲区中新数据写到新文件的过程中会产生**阻塞**。 +Blocking occurs during the process of step 2 when writing new data from the buffer to the new file, causing a **block**. -相关阅读:[Redis AOF 重写阻塞问题分析](https://cloud.tencent.com/developer/article/1633077)。 +For further reading: [Analysis of Redis AOF Rewrite Blocking Issues](https://cloud.tencent.com/developer/article/1633077). -## 大 Key +## Big Key -如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准: +If the value corresponding to a key occupies a large amount of memory, that key can be considered a big key. What exactly counts as large? There is a rather imprecise reference standard: -- string 类型的 value 超过 1MB -- 复合类型(列表、哈希、集合、有序集合等)的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。 +- String-type values exceeding 1MB. +- Composite types (lists, hashes, sets, sorted sets, etc.) whose values contain more than 5000 elements (for composite types, having more elements does not necessarily mean occupying more memory). -大 key 造成的阻塞问题如下: +The blocking issues caused by big keys are as follows: -- 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。 -- 引发网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。 -- 阻塞工作线程:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。 +- Client timeout blocking: Since Redis executes commands in a single-threaded manner, operations on big keys can be time-consuming, causing Redis to block, resulting in the client not responding for an extended period. +- Causing network blocking: The network traffic generated when retrieving big keys is relatively large. If a key's size is 1MB and the access frequency is 1000 per second, it will generate 1000MB of traffic per second, which is disastrous for a typical gigabit network card server. +- Blocking worker threads: If deleting a big key using del, it will block worker threads, preventing subsequent commands from being processed. -### 查找大 key +### Finding Big Keys -当我们在使用 Redis 自带的 `--bigkeys` 参数查找大 key 时,最好选择在从节点上执行该命令,因为主节点上执行时,会**阻塞**主节点。 +When using Redis's built-in `--bigkeys` parameter to find big keys, it is best to execute the command on a slave node, as executing it on the master node will **block** the master node. -- 我们还可以使用 SCAN 命令来查找大 key; +- We can also use the SCAN command to find big keys; +- Analyzing RDB files to identify big keys requires that Redis is using RDB persistence. There are tools available online: + - redis-rdb-tools: A Python tool for analyzing Redis RDB snapshot files. + - rdb_bigkeys: A Go tool for analyzing Redis RDB snapshot files, which performs better. -- 通过分析 RDB 文件来找出 big key,这种方案的前提是 Redis 采用的是 RDB 持久化。网上有现成的工具: +### Deleting Big Keys -- - redis-rdb-tools:Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具 - - rdb_bigkeys:Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好。 +The essence of the delete operation is to free the memory space occupied by key-value pairs. -### 删除大 key +Freeing memory is just the first step. To manage memory space more efficiently, when an application releases memory, **the operating system needs to insert the released memory blocks into a linked list of free memory blocks** for subsequent management and reallocation. This process itself requires some time and can **block** the current application that is freeing memory. -删除操作的本质是要释放键值对占用的内存空间。 +Therefore, if a large amount of memory is released at once, the time required for managing the linked list of free memory blocks will increase, consequently causing blocking in the Redis main thread. If the main thread becomes blocked, all other requests may timeout, leading to increasing timeouts that can exhaust Redis connections and generate various exceptions. -释放内存只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,**操作系统需要把释放掉的内存块插入一个空闲内存块的链表**,以便后续进行管理和再分配。这个过程本身需要一定时间,而且会**阻塞**当前释放内存的应用程序。 +When deleting big keys, it is recommended to use batch deletions and asynchronous deletions. -所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞,如果主线程发生了阻塞,其他所有请求可能都会超时,超时越来越多,会造成 Redis 连接耗尽,产生各种异常。 +## Clearing the Database -删除大 key 时建议采用分批次删除和异步删除的方式进行。 +Clearing the database is similar to the deletion of big keys; `flushdb` and `flushall` involve the deletion and release of all key-value pairs, which are also blocking points in Redis. -## 清空数据库 +## Cluster Expansion -清空数据库和上面 bigkey 删除也是同样道理,`flushdb`、`flushall` 也涉及到删除和释放所有的键值对,也是 Redis 的阻塞点。 +Redis clusters can dynamically scale up and down nodes. This process is currently semi-automated and requires manual intervention. -## 集群扩容 +During the expansion or contraction, data migration is needed. Redis ensures the consistency of migration; all migration operations are synchronous. -Redis 集群可以进行节点的动态扩容缩容,这一过程目前还处于半自动状态,需要人工介入。 +During migration, both ends of Redis will enter a blocking state for varying durations. For small keys, this time can be negligible, but if a key's memory usage is too large, it can trigger failover within the cluster, causing unnecessary switching. -在扩缩容的时候,需要进行数据迁移。而 Redis 为了保证迁移的一致性,迁移所有操作都是同步操作。 +## Swap -执行迁移时,两端的 Redis 均会进入时长不等的阻塞状态,对于小 Key,该时间可以忽略不计,但如果一旦 Key 的内存使用过大,严重的时候会触发集群内的故障转移,造成不必要的切换。 +**What is Swap?** Swap refers to the exchange, and in Linux, Swap is often called memory swapping or swap partition. Similar to virtual memory in Windows, it solves memory shortages by virtualizing some hard disk space to be used as memory. Thus, the purpose of the Swap partition is to sacrifice hard disk space to increase memory and address the issue of insufficient or overloaded VPS memory. -## Swap(内存交换) +Swap can be very detrimental to Redis. One important premise for Redis to ensure high performance is that all data exists in memory. If the operating system swaps out some of the memory used by Redis to the hard disk, the read and write speeds of memory and hard disk differ by several orders of magnitude, leading to a drastic decline in Redis performance after the swap. -**什么是 Swap?** Swap 直译过来是交换的意思,Linux 中的 Swap 常被称为内存交换或者交换分区。类似于 Windows 中的虚拟内存,就是当内存不足的时候,把一部分硬盘空间虚拟成内存使用,从而解决内存容量不足的情况。因此,Swap 分区的作用就是牺牲硬盘,增加内存,解决 VPS 内存不够用或者爆满的问题。 +Here are methods to check if Redis is experiencing Swap: -Swap 对于 Redis 来说是非常致命的,Redis 保证高性能的一个重要前提是所有的数据在内存中。如果操作系统把 Redis 使用的部分内存换出硬盘,由于内存与硬盘的读写速度差几个数量级,会导致发生交换后的 Redis 性能急剧下降。 - -识别 Redis 发生 Swap 的检查方法如下: - -1、查询 Redis 进程号 +1. Query the Redis process ID ```bash redis-cli -p 6383 info server | grep process_id process_id: 4476 ``` -2、根据进程号查询内存交换信息 +2. Check memory swap information based on the process ID ```bash cat /proc/4476/smaps | grep Swap @@ -152,27 +150,27 @@ Swap: 0kB ..... ``` -如果交换量都是 0KB 或者个别的是 4KB,则正常。 +If the swap amounts are all 0KB or a few are 4KB, it is normal. -预防内存交换的方法: +Methods to prevent memory swapping: -- 保证机器充足的可用内存 -- 确保所有 Redis 实例设置最大可用内存(maxmemory),防止极端情况 Redis 内存不可控的增长 -- 降低系统使用 swap 优先级,如`echo 10 > /proc/sys/vm/swappiness` +- Ensure ample available memory on the machine. +- Ensure all Redis instances set a maximum available memory (maxmemory) to prevent uncontrolled growth of Redis memory in extreme situations. +- Lower the system's swap priority, such as `echo 10 > /proc/sys/vm/swappiness`. -## CPU 竞争 +## CPU Competition -Redis 是典型的 CPU 密集型应用,不建议和其他多核 CPU 密集型服务部署在一起。当其他进程过度消耗 CPU 时,将严重影响 Redis 的吞吐量。 +Redis is a typical CPU-intensive application and should not be deployed alongside other multi-core CPU-intensive services. When other processes excessively consume CPU, it will severely affect Redis's throughput. -可以通过`redis-cli --stat`获取当前 Redis 使用情况。通过`top`命令获取进程对 CPU 的利用率等信息 通过`info commandstats`统计信息分析出命令不合理开销时间,查看是否是因为高算法复杂度或者过度的内存优化问题。 +You can use `redis-cli --stat` to get the current Redis usage. The `top` command can be used to get information on processes' CPU utilization, and `info commandstats` can provide statistics to analyze unreasonable command overhead, checking if it is due to high algorithm complexity or excessive memory optimization issues. -## 网络问题 +## Network Issues -连接拒绝、网络延迟,网卡软中断等网络问题也可能会导致 Redis 阻塞。 +Connection refusals, network delays, software interrupts on network cards, and other network issues might also lead to Redis blocking. -## 参考 +## References -- Redis 阻塞的 6 大类场景分析与总结: -- Redis 开发与运维笔记-Redis 的噩梦-阻塞: +- Analysis and Summary of 6 Major Scenarios of Redis Blocking: +- Redis Development and Operation Notes - The Nightmare of Redis - Blocking: diff --git a/docs/database/redis/redis-data-structures-01.md b/docs/database/redis/redis-data-structures-01.md index 9dfb0c3eaa5..0e7d5a4d03f 100644 --- a/docs/database/redis/redis-data-structures-01.md +++ b/docs/database/redis/redis-data-structures-01.md @@ -1,69 +1,69 @@ --- -title: Redis 5 种基本数据类型详解 -category: 数据库 +title: Detailed Explanation of 5 Basic Data Types in Redis +category: Database tag: - Redis head: - - meta - name: keywords - content: Redis常见数据类型 + content: Common data types in Redis - - meta - name: description - content: Redis基础数据类型总结:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合) + content: Summary of Redis basic data types: String, List, Set, Hash, Zset --- -Redis 共有 5 种基本数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。 +Redis has a total of 5 basic data types: String, List, Set, Hash, and Zset. -这 5 种数据类型是直接提供给用户使用的,是数据的保存形式,其底层实现主要依赖这 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Dict(哈希表/字典)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。 +These 5 data types are directly provided for user use and represent the form of data storage. Their underlying implementation mainly relies on these 8 data structures: Simple Dynamic Strings (SDS), LinkedList, Dict (hash table/dictionary), SkipList, Intset (integer set), ZipList (compressed list), and QuickList. -Redis 5 种基本数据类型对应的底层数据结构实现如下表所示: +The underlying data structure implementations corresponding to the 5 basic data types in Redis are shown in the table below: | String | List | Hash | Set | Zset | | :----- | :--------------------------- | :------------ | :----------- | :---------------- | -| SDS | LinkedList/ZipList/QuickList | Dict、ZipList | Dict、Intset | ZipList、SkipList | +| SDS | LinkedList/ZipList/QuickList | Dict, ZipList | Dict, Intset | ZipList, SkipList | -Redis 3.2 之前,List 底层实现是 LinkedList 或者 ZipList。 Redis 3.2 之后,引入了 LinkedList 和 ZipList 的结合 QuickList,List 的底层实现变为 QuickList。从 Redis 7.0 开始, ZipList 被 ListPack 取代。 +Before Redis 3.2, the underlying implementation of List was LinkedList or ZipList. After Redis 3.2, QuickList, a combination of LinkedList and ZipList, was introduced, and the underlying implementation of List changed to QuickList. Starting from Redis 7.0, ZipList has been replaced by ListPack. -你可以在 Redis 官网上找到 Redis 数据类型/结构非常详细的介绍: +You can find a very detailed introduction to Redis data types/structures on the official Redis website: - [Redis Data Structures](https://redis.com/redis-enterprise/data-structures/) - [Redis Data types tutorial](https://redis.io/docs/manual/data-types/data-types-tutorial/) -未来随着 Redis 新版本的发布,可能会有新的数据结构出现,通过查阅 Redis 官网对应的介绍,你总能获取到最靠谱的信息。 +In the future, with the release of new versions of Redis, new data structures may appear. By consulting the corresponding introductions on the Redis official website, you can always obtain the most reliable information. ![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220720181630203.png) -## String(字符串) +## String -### 介绍 +### Introduction -String 是 Redis 中最简单同时也是最常用的一个数据类型。 +String is the simplest and most commonly used data type in Redis. -String 是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。 +String is a binary-safe data type that can be used to store any type of data, such as strings, integers, floating-point numbers, images (base64 encoding or decoding of images or image paths), and serialized objects. ![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719124403897.png) -虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C 的字符串表示,而是自己构建了一种 **简单动态字符串**(Simple Dynamic String,**SDS**)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。 +Although Redis is written in C, it does not use C's string representation but instead constructs a **Simple Dynamic String** (SDS). Compared to C's native strings, Redis's SDS can not only store text data but also binary data, and the complexity of obtaining the string length is O(1) (C strings are O(N)). In addition, Redis's SDS API is safe and does not cause buffer overflows. -### 常用命令 +### Common Commands -| 命令 | 介绍 | -| ------------------------------- | -------------------------------- | -| SET key value | 设置指定 key 的值 | -| SETNX key value | 只有在 key 不存在时设置 key 的值 | -| GET key | 获取指定 key 的值 | -| MSET key1 value1 key2 value2 …… | 设置一个或多个指定 key 的值 | -| MGET key1 key2 ... | 获取一个或多个指定 key 的值 | -| STRLEN key | 返回 key 所储存的字符串值的长度 | -| INCR key | 将 key 中储存的数字值增一 | -| DECR key | 将 key 中储存的数字值减一 | -| EXISTS key | 判断指定 key 是否存在 | -| DEL key(通用) | 删除指定的 key | -| EXPIRE key seconds(通用) | 给指定 key 设置过期时间 | +| Command | Description | +| -------------------------------- | --------------------------------------------------- | +| SET key value | Set the value of the specified key | +| SETNX key value | Set the value of the key only if it does not exist | +| GET key | Get the value of the specified key | +| MSET key1 value1 key2 value2 ... | Set the values of one or more specified keys | +| MGET key1 key2 ... | Get the values of one or more specified keys | +| STRLEN key | Return the length of the string value stored at key | +| INCR key | Increment the numeric value stored at key by one | +| DECR key | Decrement the numeric value stored at key by one | +| EXISTS key | Determine if the specified key exists | +| DEL key (general) | Delete the specified key | +| EXPIRE key seconds (general) | Set an expiration time for the specified key | -更多 Redis String 命令以及详细使用指南,请查看 Redis 官网对应的介绍: 。 +For more Redis String commands and detailed usage guides, please refer to the corresponding introduction on the Redis official website: . -**基本操作**: +**Basic Operations**: ```bash > SET key value @@ -80,424 +80,22 @@ OK (nil) ``` -**批量设置**: +**Batch Setting**: ```bash > MSET key1 value1 key2 value2 OK -> MGET key1 key2 # 批量获取多个 key 对应的 value +> MGET key1 key2 # Batch get the values corresponding to multiple keys 1) "value1" 2) "value2" ``` -**计数器(字符串的内容为整数的时候可以使用):** +**Counter (when the content of the string is an integer)**: ```bash > SET number 1 OK -> INCR number # 将 key 中储存的数字值增一 +> INCR number # Increment the numeric value stored at key by one (integer) 2 > GET number -"2" -> DECR number # 将 key 中储存的数字值减一 -(integer) 1 -> GET number -"1" -``` - -**设置过期时间(默认为永不过期)**: - -```bash -> EXPIRE key 60 -(integer) 1 -> SETEX key 60 value # 设置值并设置过期时间 -OK -> TTL key -(integer) 56 -``` - -### 应用场景 - -**需要存储常规数据的场景** - -- 举例:缓存 Session、Token、图片地址、序列化后的对象(相比较于 Hash 存储更节省内存)。 -- 相关命令:`SET`、`GET`。 - -**需要计数的场景** - -- 举例:用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数。 -- 相关命令:`SET`、`GET`、 `INCR`、`DECR` 。 - -**分布式锁** - -利用 `SETNX key value` 命令可以实现一个最简易的分布式锁(存在一些缺陷,通常不建议这样实现分布式锁)。 - -## List(列表) - -### 介绍 - -Redis 中的 List 其实就是链表数据结构的实现。我在 [线性数据结构 :数组、链表、栈、队列](https://javaguide.cn/cs-basics/data-structure/linear-data-structure.html) 这篇文章中详细介绍了链表这种数据结构,我这里就不多做介绍了。 - -许多高级编程语言都内置了链表的实现比如 Java 中的 `LinkedList`,但是 C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构。Redis 的 List 的实现为一个 **双向链表**,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719124413287.png) - -### 常用命令 - -| 命令 | 介绍 | -| --------------------------- | ------------------------------------------ | -| RPUSH key value1 value2 ... | 在指定列表的尾部(右边)添加一个或多个元素 | -| LPUSH key value1 value2 ... | 在指定列表的头部(左边)添加一个或多个元素 | -| LSET key index value | 将指定列表索引 index 位置的值设置为 value | -| LPOP key | 移除并获取指定列表的第一个元素(最左边) | -| RPOP key | 移除并获取指定列表的最后一个元素(最右边) | -| LLEN key | 获取列表元素数量 | -| LRANGE key start end | 获取列表 start 和 end 之间 的元素 | - -更多 Redis List 命令以及详细使用指南,请查看 Redis 官网对应的介绍: 。 - -**通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP`实现队列**: - -```bash -> RPUSH myList value1 -(integer) 1 -> RPUSH myList value2 value3 -(integer) 3 -> LPOP myList -"value1" -> LRANGE myList 0 1 -1) "value2" -2) "value3" -> LRANGE myList 0 -1 -1) "value2" -2) "value3" -``` - -**通过 `RPUSH/RPOP`或者`LPUSH/LPOP` 实现栈**: - -```bash -> RPUSH myList2 value1 value2 value3 -(integer) 3 -> RPOP myList2 # 将 list的最右边的元素取出 -"value3" -``` - -我专门画了一个图方便大家理解 `RPUSH` , `LPOP` , `lpush` , `RPOP` 命令: - -![](https://oss.javaguide.cn/github/javaguide/database/redis/redis-list.png) - -**通过 `LRANGE` 查看对应下标范围的列表元素**: - -```bash -> RPUSH myList value1 value2 value3 -(integer) 3 -> LRANGE myList 0 1 -1) "value1" -2) "value2" -> LRANGE myList 0 -1 -1) "value1" -2) "value2" -3) "value3" -``` - -通过 `LRANGE` 命令,你可以基于 List 实现分页查询,性能非常高! - -**通过 `LLEN` 查看链表长度**: - -```bash -> LLEN myList -(integer) 3 -``` - -### 应用场景 - -**信息流展示** - -- 举例:最新文章、最新动态。 -- 相关命令:`LPUSH`、`LRANGE`。 - -**消息队列** - -`List` 可以用来做消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。 - -相对来说,Redis 5.0 新增加的一个数据结构 `Stream` 更适合做消息队列一些,只是功能依然非常简陋。和专业的消息队列相比,还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。 - -## Hash(哈希) - -### 介绍 - -Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值。 - -Hash 类似于 JDK1.8 前的 `HashMap`,内部实现也差不多(数组 + 链表)。不过,Redis 的 Hash 做了更多优化。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719124421703.png) - -### 常用命令 - -| 命令 | 介绍 | -| ----------------------------------------- | -------------------------------------------------------- | -| HSET key field value | 设置指定哈希表中指定字段的值 | -| HSETNX key field value | 只有指定字段不存在时设置指定字段的值 | -| HMSET key field1 value1 field2 value2 ... | 同时将一个或多个 field-value (域-值)对设置到指定哈希表中 | -| HGET key field | 获取指定哈希表中指定字段的值 | -| HMGET key field1 field2 ... | 获取指定哈希表中一个或者多个指定字段的值 | -| HGETALL key | 获取指定哈希表中所有的键值对 | -| HEXISTS key field | 查看指定哈希表中指定的字段是否存在 | -| HDEL key field1 field2 ... | 删除一个或多个哈希表字段 | -| HLEN key | 获取指定哈希表中字段的数量 | -| HINCRBY key field increment | 对指定哈希中的指定字段做运算操作(正数为加,负数为减) | - -更多 Redis Hash 命令以及详细使用指南,请查看 Redis 官网对应的介绍: 。 - -**模拟对象数据存储**: - -```bash -> HMSET userInfoKey name "guide" description "dev" age 24 -OK -> HEXISTS userInfoKey name # 查看 key 对应的 value中指定的字段是否存在。 -(integer) 1 -> HGET userInfoKey name # 获取存储在哈希表中指定字段的值。 -"guide" -> HGET userInfoKey age -"24" -> HGETALL userInfoKey # 获取在哈希表中指定 key 的所有字段和值 -1) "name" -2) "guide" -3) "description" -4) "dev" -5) "age" -6) "24" -> HSET userInfoKey name "GuideGeGe" -> HGET userInfoKey name -"GuideGeGe" -> HINCRBY userInfoKey age 2 -(integer) 26 ``` - -### 应用场景 - -**对象数据存储场景** - -- 举例:用户信息、商品信息、文章信息、购物车信息。 -- 相关命令:`HSET` (设置单个字段的值)、`HMSET`(设置多个字段的值)、`HGET`(获取单个字段的值)、`HMGET`(获取多个字段的值)。 - -## Set(集合) - -### 介绍 - -Redis 中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 `HashSet` 。当你需要存储一个列表数据,又不希望出现重复数据时,Set 是一个很好的选择,并且 Set 提供了判断某个元素是否在一个 Set 集合内的重要接口,这个也是 List 所不能提供的。 - -你可以基于 Set 轻易实现交集、并集、差集的操作,比如你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。这样的话,Set 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719124430264.png) - -### 常用命令 - -| 命令 | 介绍 | -| ------------------------------------- | ----------------------------------------- | -| SADD key member1 member2 ... | 向指定集合添加一个或多个元素 | -| SMEMBERS key | 获取指定集合中的所有元素 | -| SCARD key | 获取指定集合的元素数量 | -| SISMEMBER key member | 判断指定元素是否在指定集合中 | -| SINTER key1 key2 ... | 获取给定所有集合的交集 | -| SINTERSTORE destination key1 key2 ... | 将给定所有集合的交集存储在 destination 中 | -| SUNION key1 key2 ... | 获取给定所有集合的并集 | -| SUNIONSTORE destination key1 key2 ... | 将给定所有集合的并集存储在 destination 中 | -| SDIFF key1 key2 ... | 获取给定所有集合的差集 | -| SDIFFSTORE destination key1 key2 ... | 将给定所有集合的差集存储在 destination 中 | -| SPOP key count | 随机移除并获取指定集合中一个或多个元素 | -| SRANDMEMBER key count | 随机获取指定集合中指定数量的元素 | - -更多 Redis Set 命令以及详细使用指南,请查看 Redis 官网对应的介绍: 。 - -**基本操作**: - -```bash -> SADD mySet value1 value2 -(integer) 2 -> SADD mySet value1 # 不允许有重复元素,因此添加失败 -(integer) 0 -> SMEMBERS mySet -1) "value1" -2) "value2" -> SCARD mySet -(integer) 2 -> SISMEMBER mySet value1 -(integer) 1 -> SADD mySet2 value2 value3 -(integer) 2 -``` - -- `mySet` : `value1`、`value2` 。 -- `mySet2`:`value2`、`value3` 。 - -**求交集**: - -```bash -> SINTERSTORE mySet3 mySet mySet2 -(integer) 1 -> SMEMBERS mySet3 -1) "value2" -``` - -**求并集**: - -```bash -> SUNION mySet mySet2 -1) "value3" -2) "value2" -3) "value1" -``` - -**求差集**: - -```bash -> SDIFF mySet mySet2 # 差集是由所有属于 mySet 但不属于 A 的元素组成的集合 -1) "value1" -``` - -### 应用场景 - -**需要存放的数据不能重复的场景** - -- 举例:网站 UV 统计(数据量巨大的场景还是 `HyperLogLog`更适合一些)、文章点赞、动态点赞等场景。 -- 相关命令:`SCARD`(获取集合数量) 。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719073733851.png) - -**需要获取多个数据源交集、并集和差集的场景** - -- 举例:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等场景。 -- 相关命令:`SINTER`(交集)、`SINTERSTORE` (交集)、`SUNION` (并集)、`SUNIONSTORE`(并集)、`SDIFF`(差集)、`SDIFFSTORE` (差集)。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719074543513.png) - -**需要随机获取数据源中的元素的场景** - -- 举例:抽奖系统、随机点名等场景。 -- 相关命令:`SPOP`(随机获取集合中的元素并移除,适合不允许重复中奖的场景)、`SRANDMEMBER`(随机获取集合中的元素,适合允许重复中奖的场景)。 - -## Sorted Set(有序集合) - -### 介绍 - -Sorted Set 类似于 Set,但和 Set 相比,Sorted Set 增加了一个权重参数 `score`,使得集合中的元素能够按 `score` 进行有序排列,还可以通过 `score` 的范围来获取元素的列表。有点像是 Java 中 `HashMap` 和 `TreeSet` 的结合体。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719124437791.png) - -### 常用命令 - -| 命令 | 介绍 | -| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | -| ZADD key score1 member1 score2 member2 ... | 向指定有序集合添加一个或多个元素 | -| ZCARD KEY | 获取指定有序集合的元素数量 | -| ZSCORE key member | 获取指定有序集合中指定元素的 score 值 | -| ZINTERSTORE destination numkeys key1 key2 ... | 将给定所有有序集合的交集存储在 destination 中,对相同元素对应的 score 值进行 SUM 聚合操作,numkeys 为集合数量 | -| ZUNIONSTORE destination numkeys key1 key2 ... | 求并集,其它和 ZINTERSTORE 类似 | -| ZDIFFSTORE destination numkeys key1 key2 ... | 求差集,其它和 ZINTERSTORE 类似 | -| ZRANGE key start end | 获取指定有序集合 start 和 end 之间的元素(score 从低到高) | -| ZREVRANGE key start end | 获取指定有序集合 start 和 end 之间的元素(score 从高到底) | -| ZREVRANK key member | 获取指定有序集合中指定元素的排名(score 从大到小排序) | - -更多 Redis Sorted Set 命令以及详细使用指南,请查看 Redis 官网对应的介绍: 。 - -**基本操作**: - -```bash -> ZADD myZset 2.0 value1 1.0 value2 -(integer) 2 -> ZCARD myZset -2 -> ZSCORE myZset value1 -2.0 -> ZRANGE myZset 0 1 -1) "value2" -2) "value1" -> ZREVRANGE myZset 0 1 -1) "value1" -2) "value2" -> ZADD myZset2 4.0 value2 3.0 value3 -(integer) 2 - -``` - -- `myZset` : `value1`(2.0)、`value2`(1.0) 。 -- `myZset2`:`value2` (4.0)、`value3`(3.0) 。 - -**获取指定元素的排名**: - -```bash -> ZREVRANK myZset value1 -0 -> ZREVRANK myZset value2 -1 -``` - -**求交集**: - -```bash -> ZINTERSTORE myZset3 2 myZset myZset2 -1 -> ZRANGE myZset3 0 1 WITHSCORES -value2 -5 -``` - -**求并集**: - -```bash -> ZUNIONSTORE myZset4 2 myZset myZset2 -3 -> ZRANGE myZset4 0 2 WITHSCORES -value1 -2 -value3 -3 -value2 -5 -``` - -**求差集**: - -```bash -> ZDIFF 2 myZset myZset2 WITHSCORES -value1 -2 -``` - -### 应用场景 - -**需要随机获取数据源中的元素根据某个权重进行排序的场景** - -- 举例:各种排行榜比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。 -- 相关命令:`ZRANGE` (从小到大排序)、 `ZREVRANGE` (从大到小排序)、`ZREVRANK` (指定元素排名)。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/2021060714195385.png) - -[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的「技术面试题篇」就有一篇文章详细介绍如何使用 Sorted Set 来设计制作一个排行榜。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719071115140.png) - -**需要存储的数据有优先级或者重要程度的场景** 比如优先级任务队列。 - -- 举例:优先级任务队列。 -- 相关命令:`ZRANGE` (从小到大排序)、 `ZREVRANGE` (从大到小排序)、`ZREVRANK` (指定元素排名)。 - -## 总结 - -| 数据类型 | 说明 | -| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| String | 一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。 | -| List | Redis 的 List 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。 | -| Hash | 一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值。 | -| Set | 无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 `HashSet` 。 | -| Zset | 和 Set 相比,Sorted Set 增加了一个权重参数 `score`,使得集合中的元素能够按 `score` 进行有序排列,还可以通过 `score` 的范围来获取元素的列表。有点像是 Java 中 `HashMap` 和 `TreeSet` 的结合体。 | - -## 参考 - -- Redis Data Structures: 。 -- Redis Commands: 。 -- Redis Data types tutorial: 。 -- Redis 存储对象信息是用 Hash 还是 String : - - diff --git a/docs/database/redis/redis-data-structures-02.md b/docs/database/redis/redis-data-structures-02.md index 9e5fbcee59b..835b5f8b8be 100644 --- a/docs/database/redis/redis-data-structures-02.md +++ b/docs/database/redis/redis-data-structures-02.md @@ -1,48 +1,48 @@ --- -title: Redis 3 种特殊数据类型详解 -category: 数据库 +title: Detailed Explanation of 3 Special Data Types in Redis +category: Database tag: - Redis head: - - meta - name: keywords - content: Redis常见数据类型 + content: Common data types in Redis - - meta - name: description - content: Redis特殊数据类型总结:HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。 + content: Summary of Redis special data types: HyperLogLogs (cardinality statistics), Bitmap (bit storage), Geospatial (geolocation). --- -除了 5 种基本的数据类型之外,Redis 还支持 3 种特殊的数据类型:Bitmap、HyperLogLog、GEO。 +In addition to the 5 basic data types, Redis also supports 3 special data types: Bitmap, HyperLogLog, and GEO. -## Bitmap (位图) +## Bitmap -### 介绍 +### Introduction -根据官网介绍: +According to the official website: > Bitmaps are not an actual data type, but a set of bit-oriented operations defined on the String type which is treated like a bit vector. Since strings are binary safe blobs and their maximum length is 512 MB, they are suitable to set up to 2^32 different bits. > -> Bitmap 不是 Redis 中的实际数据类型,而是在 String 类型上定义的一组面向位的操作,将其视为位向量。由于字符串是二进制安全的块,且最大长度为 512 MB,它们适合用于设置最多 2^32 个不同的位。 +> Bitmap is not an actual data type in Redis, but a set of bit-oriented operations defined on the String type, treated as a bit vector. Since strings are binary safe blobs and their maximum length is 512 MB, they are suitable for setting up to 2^32 different bits. -Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。 +Bitmaps store continuous binary numbers (0 and 1). With Bitmap, only one bit is needed to represent the value or state of a corresponding element, where the key is the element itself. We know that 8 bits can form one byte, so Bitmap can greatly save storage space. -你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。 +You can think of Bitmap as an array that stores binary numbers (0 and 1), where each element's index is called an offset. ![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220720194154133.png) -### 常用命令 +### Common Commands -| 命令 | 介绍 | -| ------------------------------------- | ---------------------------------------------------------------- | -| SETBIT key offset value | 设置指定 offset 位置的值 | -| GETBIT key offset | 获取指定 offset 位置的值 | -| BITCOUNT key start end | 获取 start 和 end 之间值为 1 的元素个数 | -| BITOP operation destkey key1 key2 ... | 对一个或多个 Bitmap 进行运算,可用运算符有 AND, OR, XOR 以及 NOT | +| Command | Description | +| ------------------------------------- | ------------------------------------------------------------------------------- | +| SETBIT key offset value | Set the value at the specified offset position | +| GETBIT key offset | Get the value at the specified offset position | +| BITCOUNT key start end | Get the number of elements with a value of 1 between start and end | +| BITOP operation destkey key1 key2 ... | Perform operations on one or more Bitmaps, with operators AND, OR, XOR, and NOT | -**Bitmap 基本操作演示**: +**Basic Bitmap Operations Demonstration**: ```bash -# SETBIT 会返回之前位的值(默认是 0)这里会生成 7 个位 +# SETBIT will return the previous value of the bit (default is 0), generating 7 bits > SETBIT mykey 7 1 (integer) 0 > SETBIT mykey 7 0 @@ -53,174 +53,35 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需 (integer) 0 > SETBIT mykey 8 1 (integer) 0 -# 通过 bitcount 统计被被设置为 1 的位的数量。 +# Count the number of bits set to 1 using bitcount. > BITCOUNT mykey (integer) 2 ``` -### 应用场景 +### Application Scenarios -**需要保存状态信息(0/1 即可表示)的场景** +**Scenarios that require saving state information (0/1 can represent)** -- 举例:用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。 -- 相关命令:`SETBIT`、`GETBIT`、`BITCOUNT`、`BITOP`。 +- Example: User sign-in status, active user status, user behavior statistics (e.g., whether a user has liked a certain video). +- Related commands: `SETBIT`, `GETBIT`, `BITCOUNT`, `BITOP`. -## HyperLogLog(基数统计) +## HyperLogLog -### 介绍 +### Introduction -HyperLogLog 是一种有名的基数计数概率算法 ,基于 LogLog Counting(LLC)优化改进得来,并不是 Redis 特有的,Redis 只是实现了这个算法并提供了一些开箱即用的 API。 +HyperLogLog is a well-known probabilistic algorithm for cardinality counting, optimized from LogLog Counting (LLC). It is not unique to Redis; Redis simply implements this algorithm and provides some out-of-the-box APIs. -Redis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储接近`2^64`个不同元素。这是真的厉害,这就是数学的魅力么!并且,Redis 对 HyperLogLog 的存储结构做了优化,采用两种方式计数: +The HyperLogLog provided by Redis occupies a very small space, requiring only 12k of space to store nearly `2^64` different elements. This is truly impressive; this is the charm of mathematics! Additionally, Redis has optimized the storage structure of HyperLogLog, using two counting methods: -- **稀疏矩阵**:计数较少的时候,占用空间很小。 -- **稠密矩阵**:计数达到某个阈值的时候,占用 12k 的空间。 +- **Sparse Matrix**: When the count is low, it occupies very little space. +- **Dense Matrix**: When the count reaches a certain threshold, it occupies 12k of space. -Redis 官方文档中有对应的详细说明: +The official Redis documentation provides detailed explanations: ![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220721091424563.png) -基数计数概率算法为了节省内存并不会直接存储元数据,而是通过一定的概率统计方法预估基数值(集合中包含元素的个数)。因此, HyperLogLog 的计数结果并不是一个精确值,存在一定的误差(标准误差为 `0.81%` )。 +The cardinality counting probabilistic algorithm does not directly store metadata to save memory; instead, it estimates the cardinality value (the number of elements in the set) through certain probabilistic statistical methods. Therefore, the counting result of HyperLogLog is not an exact value and has a certain margin of error (standard error is `0.81%`). ![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220720194154133.png) -HyperLogLog 的使用非常简单,但原理非常复杂。HyperLogLog 的原理以及在 Redis 中的实现可以看这篇文章:[HyperLogLog 算法的原理讲解以及 Redis 是如何应用它的](https://juejin.cn/post/6844903785744056333) 。 - -再推荐一个可以帮助理解 HyperLogLog 原理的工具:[Sketch of the Day: HyperLogLog — Cornerstone of a Big Data Infrastructure](http://content.research.neustar.biz/blog/hll.html) 。 - -除了 HyperLogLog 之外,Redis 还提供了其他的概率数据结构,对应的官方文档地址: 。 - -### 常用命令 - -HyperLogLog 相关的命令非常少,最常用的也就 3 个。 - -| 命令 | 介绍 | -| ----------------------------------------- | -------------------------------------------------------------------------------- | -| PFADD key element1 element2 ... | 添加一个或多个元素到 HyperLogLog 中 | -| PFCOUNT key1 key2 | 获取一个或者多个 HyperLogLog 的唯一计数。 | -| PFMERGE destkey sourcekey1 sourcekey2 ... | 将多个 HyperLogLog 合并到 destkey 中,destkey 会结合多个源,算出对应的唯一计数。 | - -**HyperLogLog 基本操作演示**: - -```bash -> PFADD hll foo bar zap -(integer) 1 -> PFADD hll zap zap zap -(integer) 0 -> PFADD hll foo bar -(integer) 0 -> PFCOUNT hll -(integer) 3 -> PFADD some-other-hll 1 2 3 -(integer) 1 -> PFCOUNT hll some-other-hll -(integer) 6 -> PFMERGE desthll hll some-other-hll -"OK" -> PFCOUNT desthll -(integer) 6 -``` - -### 应用场景 - -**数量巨大(百万、千万级别以上)的计数场景** - -- 举例:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计。 -- 相关命令:`PFADD`、`PFCOUNT` 。 - -## Geospatial (地理位置) - -### 介绍 - -Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。 - -通过 GEO 我们可以轻松实现两个位置距离的计算、获取指定位置附近的元素等功能。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220720194359494.png) - -### 常用命令 - -| 命令 | 介绍 | -| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | -| GEOADD key longitude1 latitude1 member1 ... | 添加一个或多个元素对应的经纬度信息到 GEO 中 | -| GEOPOS key member1 member2 ... | 返回给定元素的经纬度信息 | -| GEODIST key member1 member2 M/KM/FT/MI | 返回两个给定元素之间的距离 | -| GEORADIUS key longitude latitude radius distance | 获取指定位置附近 distance 范围内的其他元素,支持 ASC(由近到远)、DESC(由远到近)、Count(数量) 等参数 | -| GEORADIUSBYMEMBER key member radius distance | 类似于 GEORADIUS 命令,只是参照的中心点是 GEO 中的元素 | - -**基本操作**: - -```bash -> GEOADD personLocation 116.33 39.89 user1 116.34 39.90 user2 116.35 39.88 user3 -3 -> GEOPOS personLocation user1 -116.3299986720085144 -39.89000061669732844 -> GEODIST personLocation user1 user2 km -1.4018 -``` - -通过 Redis 可视化工具查看 `personLocation` ,果不其然,底层就是 Sorted Set。 - -GEO 中存储的地理位置信息的经纬度数据通过 GeoHash 算法转换成了一个整数,这个整数作为 Sorted Set 的 score(权重参数)使用。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220721201545147.png) - -**获取指定位置范围内的其他元素**: - -```bash -> GEORADIUS personLocation 116.33 39.87 3 km -user3 -user1 -> GEORADIUS personLocation 116.33 39.87 2 km -> GEORADIUS personLocation 116.33 39.87 5 km -user3 -user1 -user2 -> GEORADIUSBYMEMBER personLocation user1 5 km -user3 -user1 -user2 -> GEORADIUSBYMEMBER personLocation user1 2 km -user1 -user2 -``` - -`GEORADIUS` 命令的底层原理解析可以看看阿里的这篇文章:[Redis 到底是怎么实现“附近的人”这个功能的呢?](https://juejin.cn/post/6844903966061363207) 。 - -**移除元素**: - -GEO 底层是 Sorted Set ,你可以对 GEO 使用 Sorted Set 相关的命令。 - -```bash -> ZREM personLocation user1 -1 -> ZRANGE personLocation 0 -1 -user3 -user2 -> ZSCORE personLocation user2 -4069879562983946 -``` - -### 应用场景 - -**需要管理使用地理空间数据的场景** - -- 举例:附近的人。 -- 相关命令: `GEOADD`、`GEORADIUS`、`GEORADIUSBYMEMBER` 。 - -## 总结 - -| 数据类型 | 说明 | -| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Bitmap | 你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。 | -| HyperLogLog | Redis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储接近`2^64`个不同元素。不过,HyperLogLog 的计数结果并不是一个精确值,存在一定的误差(标准误差为 `0.81%` )。 | -| Geospatial index | Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。 | - -## 参考 - -- Redis Data Structures: 。 -- 《Redis 深度历险:核心原理与应用实践》1.6 四两拨千斤——HyperLogLog -- 布隆过滤器,位图,HyperLogLog: - - +Using HyperLogLog is very simple, but the principle is quite complex. You can read this article for an explanation of the principle of HyperLogLog and how it is applied in Redis: \[Explanation of the Principle of HyperLogLog Algorithm and How Redis Applies It\](https://juejin.cn/post/684490378574 diff --git a/docs/database/redis/redis-delayed-task.md b/docs/database/redis/redis-delayed-task.md index 35f9304321f..acfebfbb9d6 100644 --- a/docs/database/redis/redis-delayed-task.md +++ b/docs/database/redis/redis-delayed-task.md @@ -1,82 +1,82 @@ --- -title: 如何基于Redis实现延时任务 -category: 数据库 +title: How to Implement Delayed Tasks Based on Redis +category: Database tag: - Redis --- -基于 Redis 实现延时任务的功能无非就下面两种方案: +The functionality of implementing delayed tasks based on Redis consists of the following two solutions: -1. Redis 过期事件监听 -2. Redisson 内置的延时队列 +1. Redis Expiration Event Listening +1. Built-in Delayed Queue in Redisson -面试的时候,你可以先说自己考虑了这两种方案,但最后发现 Redis 过期事件监听这种方案存在很多问题,因此你最终选择了 Redisson 内置的 DelayedQueue 这种方案。 +During the interview, you can first mention that you considered these two solutions, but ultimately found that the Redis expiration event listening approach has many issues, which led you to choose the built-in DelayedQueue in Redisson. -这个时候面试官可能会追问你一些相关的问题,我们后面会提到,提前准备就好了。 +At this point, the interviewer may ask you some related questions, which we will cover later, so it's good to prepare in advance. -另外,除了下面介绍到的这些问题之外,Redis 相关的常见问题建议你都复习一遍,不排除面试官会顺带问你一些 Redis 的其他问题。 +Additionally, besides the issues mentioned below, it is recommended that you review all common issues related to Redis, as the interviewer may also ask some other questions about Redis. -### Redis 过期事件监听实现延时任务功能的原理? +### What is the Principle of Implementing Delayed Task Functionality through Redis Expiration Event Listening? -Redis 2.0 引入了发布订阅 (pub/sub) 功能。在 pub/sub 中,引入了一个叫做 **channel(频道)** 的概念,有点类似于消息队列中的 **topic(主题)**。 +Redis 2.0 introduced the publish/subscribe (pub/sub) feature. In pub/sub, a concept called **channel** is introduced, which is somewhat similar to **topic** in message queues. -pub/sub 涉及发布者(publisher)和订阅者(subscriber,也叫消费者)两个角色: +pub/sub involves two roles: publisher and subscriber (also known as consumers): -- 发布者通过 `PUBLISH` 投递消息给指定 channel。 -- 订阅者通过`SUBSCRIBE`订阅它关心的 channel。并且,订阅者可以订阅一个或者多个 channel。 +- The publisher delivers messages to a specified channel using `PUBLISH`. +- Subscribers subscribe to the channels of their interest using `SUBSCRIBE`, and they can subscribe to one or more channels. -![Redis 发布订阅 (pub/sub) 功能](https://oss.javaguide.cn/github/javaguide/database/redis/redis-pub-sub.png) +![Redis Publish/Subscribe (pub/sub) Functionality](https://oss.javaguide.cn/github/javaguide/database/redis/redis-pub-sub.png) -在 pub/sub 模式下,生产者需要指定消息发送到哪个 channel 中,而消费者则订阅对应的 channel 以获取消息。 +In the pub/sub model, the producer needs to specify which channel the message is sent to, while consumers subscribe to the corresponding channel to receive messages. -Redis 中有很多默认的 channel,这些 channel 是由 Redis 本身向它们发送消息的,而不是我们自己编写的代码。其中,`__keyevent@0__:expired` 就是一个默认的 channel,负责监听 key 的过期事件。也就是说,当一个 key 过期之后,Redis 会发布一个 key 过期的事件到`__keyevent@__:expired`这个 channel 中。 +Redis has many default channels, which are messages sent by Redis itself rather than our own code. One of them, `__keyevent@0__:expired`, is a default channel responsible for listening to the expiration events of keys. This means that when a key expires, Redis will publish an expiration event to the channel `__keyevent@__:expired`. -我们只需要监听这个 channel,就可以拿到过期的 key 的消息,进而实现了延时任务功能。 +We only need to listen to this channel to receive messages about expired keys, thus achieving the delayed task functionality. -这个功能被 Redis 官方称为 **keyspace notifications** ,作用是实时监控 Redis 键和值的变化。 +This feature is officially referred to as **keyspace notifications** by Redis, which serves to monitor changes in Redis keys and values in real-time. -### Redis 过期事件监听实现延时任务功能有什么缺陷? +### What Are the Defects of Implementing Delayed Task Functionality through Redis Expiration Event Listening? -**1、时效性差** +**1. Poor Timeliness** -官方文档的一段介绍解释了时效性差的原因,地址: 。 +A paragraph in the official documentation explains the reason for poor timeliness, reference: . -![Redis 过期事件](https://oss.javaguide.cn/github/javaguide/database/redis/redis-timing-of-expired-events.png) +![Redis Expiration Events](https://oss.javaguide.cn/github/javaguide/database/redis/redis-timing-of-expired-events.png) -这段话的核心是:过期事件消息是在 Redis 服务器删除 key 时发布的,而不是一个 key 过期之后就会就会直接发布。 +The core of this statement is that expiration event messages are published when the Redis server deletes the key, not immediately when a key expires. -我们知道常用的过期数据的删除策略就两个: +We know there are two common strategies for deleting expired data: -1. **惰性删除**:只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。 -2. **定期删除**:每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。 +1. **Lazy Deletion**: It only checks for expiration when the key is accessed. This is friendly to the CPU but may result in many expired keys not being deleted. +1. **Periodic Deletion**: A batch of keys is randomly selected to execute the deletion of expired keys at intervals. Moreover, Redis limits the duration and frequency of deletion operations to reduce the impact on CPU time. -定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 **定期删除+惰性/懒汉式删除** 。 +Periodic deletion is more memory-friendly, while lazy deletion is more CPU-friendly. Each has its advantages, which is why Redis adopts **periodic deletion combined with lazy/eager deletion**. -因此,就会存在我设置了 key 的过期时间,但到了指定时间 key 还未被删除,进而没有发布过期事件的情况。 +Therefore, there might be scenarios where I set an expiration time for a key, but the key has not been deleted by the specified time and thus did not publish an expiration event. -**2、丢消息** +**2. Message Loss** -Redis 的 pub/sub 模式中的消息并不支持持久化,这与消息队列不同。在 Redis 的 pub/sub 模式中,发布者将消息发送给指定的频道,订阅者监听相应的频道以接收消息。当没有订阅者时,消息会被直接丢弃,在 Redis 中不会存储该消息。 +Messages in Redis's pub/sub mode do not support persistence, which is different from message queues. In Redis's pub/sub mode, the publisher sends messages to a specified channel, and subscribers listen to the corresponding channel to receive messages. When there are no subscribers, messages are directly discarded and will not be stored in Redis. -**3、多服务实例下消息重复消费** +**3. Message Duplicate Consumption in Multiple Service Instances** -Redis 的 pub/sub 模式目前只有广播模式,这意味着当生产者向特定频道发布一条消息时,所有订阅相关频道的消费者都能够收到该消息。 +Redis's pub/sub mode currently only supports broadcasting, which means that when a producer publishes a message to a specific channel, all consumers listening to that channel can receive it. -这个时候,我们需要注意多个服务实例重复处理消息的问题,这会增加代码开发量和维护难度。 +In this case, we need to be aware of the issue of multiple service instances processing messages repeatedly, which increases both code development and maintenance difficulty. -### Redisson 延迟队列原理是什么?有什么优势? +### What Is the Principle of Redisson Delayed Queue? What Are Its Advantages? -Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,比如多种分布式锁的实现、延时队列。 +Redisson is an open-source Redis client for the Java language, providing many out-of-the-box features, such as various implementations of distributed locks and delayed queues. -我们可以借助 Redisson 内置的延时队列 RDelayedQueue 来实现延时任务功能。 +We can use Redisson's built-in delayed queue RDelayedQueue to implement the delayed task functionality. -Redisson 的延迟队列 RDelayedQueue 是基于 Redis 的 SortedSet 来实现的。SortedSet 是一个有序集合,其中的每个元素都可以设置一个分数,代表该元素的权重。Redisson 利用这一特性,将需要延迟执行的任务插入到 SortedSet 中,并给它们设置相应的过期时间作为分数。 +Redisson's delayed queue RDelayedQueue is implemented based on Redis's SortedSet. SortedSet is an ordered collection where each element can have a score set that represents the weight of that element. Redisson uses this feature to insert tasks that need to be executed with a delay into the SortedSet and sets their corresponding expiration time as the score. -Redisson 定期使用 `zrangebyscore` 命令扫描 SortedSet 中过期的元素,然后将这些过期元素从 SortedSet 中移除,并将它们加入到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被消费者监听到。这样做可以避免消费者对整个 SortedSet 进行轮询,提高了执行效率。 +Redisson periodically uses the `zrangebyscore` command to scan for expired elements in the SortedSet, then removes these expired elements from the SortedSet and adds them to the ready message list. The ready message list is a blocking queue that consumers can listen to when new messages enter. This avoids consumers polling the entire SortedSet, thereby improving execution efficiency. -相比于 Redis 过期事件监听实现延时任务功能,这种方式具备下面这些优势: +Compared to the Redis expiration event listening method for implementing delayed tasks, this approach has the following advantages: -1. **减少了丢消息的可能**:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。 -2. **消息不存在重复消费问题**:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。 +1. **Reduced Possibility of Message Loss**: Messages in the DelayedQueue are persisted, so even if Redis crashes, based on the persistence mechanism, only a small number of messages may be lost, which has a minimal impact. Of course, you can also use database scanning as a compensation mechanism. +1. **No Duplicate Consumption Issue**: Every client retrieves tasks from the same target queue, eliminating the issue of duplicate consumption. -跟 Redisson 内置的延时队列相比,消息队列可以通过保障消息消费的可靠性、控制消息生产者和消费者的数量等手段来实现更高的吞吐量和更强的可靠性,实际项目中首选使用消息队列的延时消息这种方案。 +Compared to Redisson's built-in delayed queue, message queues can achieve higher throughput and stronger reliability by ensuring message consumption reliability and controlling the number of message producers and consumers, making delayed messages in message queues the preferred solution in practical projects. diff --git a/docs/database/redis/redis-memory-fragmentation.md b/docs/database/redis/redis-memory-fragmentation.md index cb2da7476d1..97a731c3291 100644 --- a/docs/database/redis/redis-memory-fragmentation.md +++ b/docs/database/redis/redis-memory-fragmentation.md @@ -1,37 +1,37 @@ --- -title: Redis内存碎片详解 -category: 数据库 +title: Detailed Explanation of Redis Memory Fragmentation +category: Database tag: - Redis --- -## 什么是内存碎片? +## What is Memory Fragmentation? -你可以将内存碎片简单地理解为那些不可用的空闲内存。 +You can simply understand memory fragmentation as the unusable free memory. -举个例子:操作系统为你分配了 32 字节的连续内存空间,而你存储数据实际只需要使用 24 字节内存空间,那这多余出来的 8 字节内存空间如果后续没办法再被分配存储其他数据的话,就可以被称为内存碎片。 +For example, if the operating system allocates a continuous memory space of 32 bytes for you, but you only need to use 24 bytes of that space to store data, then the extra 8 bytes of memory, if it cannot be allocated for other data later, can be referred to as memory fragmentation. -![内存碎片](https://oss.javaguide.cn/github/javaguide/memory-fragmentation.png) +![Memory Fragmentation](https://oss.javaguide.cn/github/javaguide/memory-fragmentation.png) -Redis 内存碎片虽然不会影响 Redis 性能,但是会增加内存消耗。 +Although Redis memory fragmentation does not affect Redis performance, it does increase memory consumption. -## 为什么会有 Redis 内存碎片? +## Why Does Redis Memory Fragmentation Occur? -Redis 内存碎片产生比较常见的 2 个原因: +There are two common reasons for Redis memory fragmentation: -**1、Redis 存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。** +**1. The memory space requested by Redis from the operating system when storing data may be larger than the actual storage space needed.** -以下是这段 Redis 官方的原话: +Here is the original statement from the Redis official documentation: > To store user keys, Redis allocates at most as much memory as the `maxmemory` setting enables (however there are small extra allocations possible). -Redis 使用 `zmalloc` 方法(Redis 自己实现的内存分配方法)进行内存分配的时候,除了要分配 `size` 大小的内存之外,还会多分配 `PREFIX_SIZE` 大小的内存。 +When Redis allocates memory using the `zmalloc` method (a memory allocation method implemented by Redis itself), it not only allocates memory of size `size`, but also allocates additional memory of size `PREFIX_SIZE`. -`zmalloc` 方法源码如下(源码地址: +The source code for the `zmalloc` method is as follows (source link: ): ```java void *zmalloc(size_t size) { - // 分配指定大小的内存 + // Allocate memory of the specified size void *ptr = malloc(size+PREFIX_SIZE); if (!ptr) zmalloc_oom_handler(size); #ifdef HAVE_MALLOC_SIZE @@ -45,80 +45,39 @@ void *zmalloc(size_t size) { } ``` -另外,Redis 可以使用多种内存分配器来分配内存( libc、jemalloc、tcmalloc),默认使用 [jemalloc](https://github.com/jemalloc/jemalloc),而 jemalloc 按照一系列固定的大小(8 字节、16 字节、32 字节……)来分配内存的。jemalloc 划分的内存单元如下图所示: +Additionally, Redis can use various memory allocators to allocate memory (libc, jemalloc, tcmalloc), with the default being [jemalloc](https://github.com/jemalloc/jemalloc), which allocates memory based on a series of fixed sizes (8 bytes, 16 bytes, 32 bytes, etc.). The memory units allocated by jemalloc are shown in the following diagram: -![jemalloc 内存单元示意图](https://oss.javaguide.cn/github/javaguide/database/redis/6803d3929e3e46c1b1c9d0bb9ee8e717.png) +![jemalloc Memory Unit Diagram](https://oss.javaguide.cn/github/javaguide/database/redis/6803d3929e3e46c1b1c9d0bb9ee8e717.png) -当程序申请的内存最接近某个固定值时,jemalloc 会给它分配相应大小的空间,就比如说程序需要申请 17 字节的内存,jemalloc 会直接给它分配 32 字节的内存,这样会导致有 15 字节内存的浪费。不过,jemalloc 专门针对内存碎片问题做了优化,一般不会存在过度碎片化的问题。 +When the memory requested by the program is closest to a fixed value, jemalloc allocates the corresponding size of space. For example, if the program requests 17 bytes of memory, jemalloc will allocate 32 bytes, resulting in a waste of 15 bytes of memory. However, jemalloc has been optimized specifically for memory fragmentation issues, so excessive fragmentation is generally not a problem. -**2、频繁修改 Redis 中的数据也会产生内存碎片。** +**2. Frequent modifications to data in Redis can also lead to memory fragmentation.** -当 Redis 中的某个数据删除时,Redis 通常不会轻易释放内存给操作系统。 +When a piece of data in Redis is deleted, Redis typically does not readily release memory back to the operating system. -这个在 Redis 官方文档中也有对应的原话: +This is also mentioned in the official Redis documentation: ![](https://oss.javaguide.cn/github/javaguide/redis-docs-memory-optimization.png) -文档地址: 。 +Documentation link: . -## 如何查看 Redis 内存碎片的信息? +## How to Check Redis Memory Fragmentation Information? -使用 `info memory` 命令即可查看 Redis 内存相关的信息。下图中每个参数具体的含义,Redis 官方文档有详细的介绍: 。 +You can use the `info memory` command to view Redis memory-related information. The specific meanings of each parameter in the following image are detailed in the Redis official documentation: . ![](https://oss.javaguide.cn/github/javaguide/redis-info-memory.png) -Redis 内存碎片率的计算公式:`mem_fragmentation_ratio` (内存碎片率)= `used_memory_rss` (操作系统实际分配给 Redis 的物理内存空间大小)/ `used_memory`(Redis 内存分配器为了存储数据实际申请使用的内存空间大小) +The formula for calculating Redis memory fragmentation ratio: `mem_fragmentation_ratio` (memory fragmentation ratio) = `used_memory_rss` (the actual physical memory space allocated to Redis by the operating system) / `used_memory` (the actual memory space requested by the Redis memory allocator for storing data). -也就是说,`mem_fragmentation_ratio` (内存碎片率)的值越大代表内存碎片率越严重。 +In other words, a larger value of `mem_fragmentation_ratio` indicates a more serious memory fragmentation issue. -一定不要误认为`used_memory_rss` 减去 `used_memory`值就是内存碎片的大小!!!这不仅包括内存碎片,还包括其他进程开销,以及共享库、堆栈等的开销。 +Do not mistakenly think that the value of `used_memory_rss` minus `used_memory` is the size of memory fragmentation!!! This includes not only memory fragmentation but also overhead from other processes, as well as shared libraries, stacks, etc. -很多小伙伴可能要问了:“多大的内存碎片率才是需要清理呢?”。 +Many may ask, "What level of memory fragmentation ratio requires cleanup?" -通常情况下,我们认为 `mem_fragmentation_ratio > 1.5` 的话才需要清理内存碎片。 `mem_fragmentation_ratio > 1.5` 意味着你使用 Redis 存储实际大小 2G 的数据需要使用大于 3G 的内存。 +Generally, we consider that a `mem_fragmentation_ratio > 1.5` indicates the need to clean up memory fragmentation. A `mem_fragmentation_ratio > 1.5` means that you need more than 3G of memory to store an actual size of 2G of data in Redis. -如果想要快速查看内存碎片率的话,你还可以通过下面这个命令: +If you want to quickly check the memory fragmentation ratio, you can use the following command: ```bash -> redis-cli -p 6379 info | grep mem_fragmentation_ratio -``` - -另外,内存碎片率可能存在小于 1 的情况。这种情况我在日常使用中还没有遇到过,感兴趣的小伙伴可以看看这篇文章 [故障分析 | Redis 内存碎片率太低该怎么办?- 爱可生开源社区](https://mp.weixin.qq.com/s/drlDvp7bfq5jt2M5pTqJCw) 。 - -## 如何清理 Redis 内存碎片? - -Redis4.0-RC3 版本以后自带了内存整理,可以避免内存碎片率过大的问题。 - -直接通过 `config set` 命令将 `activedefrag` 配置项设置为 `yes` 即可。 - -```bash -config set activedefrag yes -``` - -具体什么时候清理需要通过下面两个参数控制: - -```bash -# 内存碎片占用空间达到 500mb 的时候开始清理 -config set active-defrag-ignore-bytes 500mb -# 内存碎片率大于 1.5 的时候开始清理 -config set active-defrag-threshold-lower 50 -``` - -通过 Redis 自动内存碎片清理机制可能会对 Redis 的性能产生影响,我们可以通过下面两个参数来减少对 Redis 性能的影响: - -```bash -# 内存碎片清理所占用 CPU 时间的比例不低于 20% -config set active-defrag-cycle-min 20 -# 内存碎片清理所占用 CPU 时间的比例不高于 50% -config set active-defrag-cycle-max 50 -``` - -另外,重启节点可以做到内存碎片重新整理。如果你采用的是高可用架构的 Redis 集群的话,你可以将碎片率过高的主节点转换为从节点,以便进行安全重启。 - -## 参考 - -- Redis 官方文档: -- Redis 核心技术与实战 - 极客时间 - 删除数据后,为什么内存占用率还是很高?: -- Redis 源码解析——内存分配:< 源码解析——内存管理> - - +> redis-cli -p 637 \ No newline at end of file diff --git a/docs/database/redis/redis-persistence.md b/docs/database/redis/redis-persistence.md index c17fe7db316..1929e74c349 100644 --- a/docs/database/redis/redis-persistence.md +++ b/docs/database/redis/redis-persistence.md @@ -1,217 +1,74 @@ --- -title: Redis持久化机制详解 -category: 数据库 +title: Detailed Explanation of Redis Persistence Mechanisms +category: Database tag: - Redis head: - - meta - name: keywords - content: Redis持久化机制详解 + content: Detailed Explanation of Redis Persistence Mechanisms - - meta - name: description - content: Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:快照(snapshotting,RDB)、只追加文件(append-only file, AOF)、RDB 和 AOF 的混合持久化(Redis 4.0 新增)。 + content: One important difference between Redis and Memcached is that Redis supports persistence, and it supports three types of persistence methods: snapshots (snapshotting, RDB), append-only files (append-only file, AOF), and hybrid persistence of RDB and AOF (new in Redis 4.0). --- -使用缓存的时候,我们经常需要对内存中的数据进行持久化也就是将内存中的数据写入到硬盘中。大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了做数据同步(比如 Redis 集群的主从节点通过 RDB 文件同步数据)。 +When using caching, we often need to persist data in memory, which means writing the data in memory to disk. The main reasons are to reuse data later (for example, to recover data after restarting the machine or after a machine failure) or to synchronize data (for example, Redis cluster master-slave nodes synchronize data through RDB files). -Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式: +One important difference between Redis and Memcached is that Redis supports persistence, and it supports three types of persistence methods: -- 快照(snapshotting,RDB) -- 只追加文件(append-only file, AOF) -- RDB 和 AOF 的混合持久化(Redis 4.0 新增) +- Snapshots (snapshotting, RDB) +- Append-only files (append-only file, AOF) +- Hybrid persistence of RDB and AOF (new in Redis 4.0) -官方文档地址: 。 +Official documentation link: . ![](https://oss.javaguide.cn/github/javaguide/database/redis/redis4.0-persitence.png) -## RDB 持久化 +## RDB Persistence -### 什么是 RDB 持久化? +### What is RDB Persistence? -Redis 可以通过创建快照来获得存储在内存里面的数据在 **某个时间点** 上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。 +Redis can create snapshots to obtain a copy of the data stored in memory at **a certain point in time**. After Redis creates a snapshot, it can back it up, copy the snapshot to other servers to create replicas with the same data (the Redis master-slave structure, mainly used to improve Redis performance), or keep the snapshot in place for use when restarting the server. -快照持久化是 Redis 默认采用的持久化方式,在 `redis.conf` 配置文件中默认有此下配置: +Snapshot persistence is the default persistence method used by Redis, and the following configuration is present by default in the `redis.conf` configuration file: ```clojure -save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发bgsave命令创建快照。 +save 900 1 # After 900 seconds (15 minutes), if at least 1 key has changed, Redis will automatically trigger the bgsave command to create a snapshot. -save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发bgsave命令创建快照。 +save 300 10 # After 300 seconds (5 minutes), if at least 10 keys have changed, Redis will automatically trigger the bgsave command to create a snapshot. -save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发bgsave命令创建快照。 +save 60 10000 # After 60 seconds (1 minute), if at least 10000 keys have changed, Redis will automatically trigger the bgsave command to create a snapshot. ``` -### RDB 创建快照时会阻塞主线程吗? +### Does RDB Block the Main Thread When Creating Snapshots? -Redis 提供了两个命令来生成 RDB 快照文件: +Redis provides two commands to generate RDB snapshot files: -- `save` : 同步保存操作,会阻塞 Redis 主线程; -- `bgsave` : fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。 +- `save`: A synchronous save operation that blocks the Redis main thread; +- `bgsave`: Forks a child process to execute, which does not block the Redis main thread, the default option. -> 这里说 Redis 主线程而不是主进程的主要是因为 Redis 启动之后主要是通过单线程的方式完成主要的工作。如果你想将其描述为 Redis 主进程,也没毛病。 +> Here, we refer to the Redis main thread rather than the main process mainly because Redis primarily completes its main work in a single-threaded manner after startup. If you want to describe it as the Redis main process, that is also acceptable. -## AOF 持久化 +## AOF Persistence -### 什么是 AOF 持久化? +### What is AOF Persistence? -与快照持久化相比,AOF 持久化的实时性更好。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化(Redis 6.0 之后已经默认是开启了),可以通过 `appendonly` 参数开启: +Compared to snapshot persistence, AOF persistence has better real-time performance. By default, Redis does not enable AOF (append-only file) persistence (it has been enabled by default since Redis 6.0), and it can be enabled through the `appendonly` parameter: ```bash appendonly yes ``` -开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 `server.aof_buf` 中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( `fsync`策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。 +After enabling AOF persistence, every command that modifies data in Redis will write that command to the AOF buffer `server.aof_buf`, and then write it to the AOF file (at this point, it is still in the system kernel cache and has not been synchronized to disk). Finally, it decides when to synchronize the data in the system kernel cache to the hard disk based on the persistence method (`fsync` strategy) configuration. -只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险,比如说:系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了。 +Only when synchronized to disk is it considered persistently saved; otherwise, there is still a risk of data loss. For example, if the data in the system kernel cache has not been synchronized and the disk machine crashes, that portion of data will be lost. -AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 `dir` 参数设置的,默认的文件名是 `appendonly.aof`。 +The AOF file's save location is the same as that of the RDB file, both set by the `dir` parameter, and the default filename is `appendonly.aof`. -### AOF 工作基本流程是怎样的? +### What is the Basic Workflow of AOF? -AOF 持久化功能的实现可以简单分为 5 步: +The implementation of AOF persistence can be simply divided into 5 steps: -1. **命令追加(append)**:所有的写命令会追加到 AOF 缓冲区中。 -2. **文件写入(write)**:将 AOF 缓冲区的数据写入到 AOF 文件中。这一步需要调用`write`函数(系统调用),`write`将数据写入到了系统内核缓冲区之后直接返回了(延迟写)。注意!!!此时并没有同步到磁盘。 -3. **文件同步(fsync)**:AOF 缓冲区根据对应的持久化方式( `fsync` 策略)向硬盘做同步操作。这一步需要调用 `fsync` 函数(系统调用), `fsync` 针对单个文件操作,对其进行强制硬盘同步,`fsync` 将阻塞直到写入磁盘完成后返回,保证了数据持久化。 -4. **文件重写(rewrite)**:随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。 -5. **重启加载(load)**:当 Redis 重启时,可以加载 AOF 文件进行数据恢复。 - -> Linux 系统直接提供了一些函数用于对文件和设备进行访问和控制,这些函数被称为 **系统调用(syscall)**。 - -这里对上面提到的一些 Linux 系统调用再做一遍解释: - -- `write`:写入系统内核缓冲区之后直接返回(仅仅是写到缓冲区),不会立即同步到硬盘。虽然提高了效率,但也带来了数据丢失的风险。同步硬盘操作通常依赖于系统调度机制,Linux 内核通常为 30s 同步一次,具体值取决于写出的数据量和 I/O 缓冲区的状态。 -- `fsync`:`fsync`用于强制刷新系统内核缓冲区(同步到到磁盘),确保写磁盘操作结束才会返回。 - -AOF 工作流程图如下: - -![AOF 工作基本流程](https://oss.javaguide.cn/github/javaguide/database/redis/aof-work-process.png) - -### AOF 持久化方式有哪些? - -在 Redis 的配置文件中存在三种不同的 AOF 持久化方式( `fsync`策略),它们分别是: - -1. `appendfsync always`:主线程调用 `write` 执行写操作后,后台线程( `aof_fsync` 线程)立即会调用 `fsync` 函数同步 AOF 文件(刷盘),`fsync` 完成后线程返回,这样会严重降低 Redis 的性能(`write` + `fsync`)。 -2. `appendfsync everysec`:主线程调用 `write` 执行写操作后立即返回,由后台线程( `aof_fsync` 线程)每秒钟调用 `fsync` 函数(系统调用)同步一次 AOF 文件(`write`+`fsync`,`fsync`间隔为 1 秒) -3. `appendfsync no`:主线程调用 `write` 执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(`write`但不`fsync`,`fsync` 的时机由操作系统决定)。 - -可以看出:**这 3 种持久化方式的主要区别在于 `fsync` 同步 AOF 文件的时机(刷盘)**。 - -为了兼顾数据和写入性能,可以考虑 `appendfsync everysec` 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能受到的影响较小。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。 - -从 Redis 7.0.0 开始,Redis 使用了 **Multi Part AOF** 机制。顾名思义,Multi Part AOF 就是将原来的单个 AOF 文件拆分成多个 AOF 文件。在 Multi Part AOF 中,AOF 文件被分为三种类型,分别为: - -- BASE:表示基础 AOF 文件,它一般由子进程通过重写产生,该文件最多只有一个。 -- INCR:表示增量 AOF 文件,它一般会在 AOFRW 开始执行时被创建,该文件可能存在多个。 -- HISTORY:表示历史 AOF 文件,它由 BASE 和 INCR AOF 变化而来,每次 AOFRW 成功完成时,本次 AOFRW 之前对应的 BASE 和 INCR AOF 都将变为 HISTORY,HISTORY 类型的 AOF 会被 Redis 自动删除。 - -Multi Part AOF 不是重点,了解即可,详细介绍可以看看阿里开发者的[Redis 7.0 Multi Part AOF 的设计和实现](https://zhuanlan.zhihu.com/p/467217082) 这篇文章。 - -**相关 issue**:[Redis 的 AOF 方式 #783](https://github.com/Snailclimb/JavaGuide/issues/783)。 - -### AOF 为什么是在执行完命令之后记录日志? - -关系型数据库(如 MySQL)通常都是执行命令之前记录日志(方便故障恢复),而 Redis AOF 持久化机制是在执行完命令之后再记录日志。 - -![AOF 记录日志过程](https://oss.javaguide.cn/github/javaguide/database/redis/redis-aof-write-log-disc.png) - -**为什么是在执行完命令之后记录日志呢?** - -- 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查; -- 在命令执行完之后再记录,不会阻塞当前的命令执行。 - -这样也带来了风险(我在前面介绍 AOF 持久化的时候也提到过): - -- 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失; -- 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)。 - -### AOF 重写了解吗? - -当 AOF 变得太大时,Redis 能够在后台自动重写 AOF 产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。 - -![AOF 重写](https://oss.javaguide.cn/github/javaguide/database/redis/aof-rewrite.png) - -> AOF 重写(rewrite) 是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。 - -由于 AOF 重写会进行大量的写入操作,为了避免对 Redis 正常处理命令请求造成影响,Redis 将 AOF 重写程序放到子进程里执行。 - -AOF 文件重写期间,Redis 还会维护一个 **AOF 重写缓冲区**,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。 - -开启 AOF 重写功能,可以调用 `BGREWRITEAOF` 命令手动执行,也可以设置下面两个配置项,让程序自动决定触发时机: - -- `auto-aof-rewrite-min-size`:如果 AOF 文件大小小于该值,则不会触发 AOF 重写。默认值为 64 MB; -- `auto-aof-rewrite-percentage`:执行 AOF 重写时,当前 AOF 大小(aof_current_size)和上一次重写时 AOF 大小(aof_base_size)的比值。如果当前 AOF 文件大小增加了这个百分比值,将触发 AOF 重写。将此值设置为 0 将禁用自动 AOF 重写。默认值为 100。 - -Redis 7.0 版本之前,如果在重写期间有写入命令,AOF 可能会使用大量内存,重写期间到达的所有写入命令都会写入磁盘两次。 - -Redis 7.0 版本之后,AOF 重写机制得到了优化改进。下面这段内容摘自阿里开发者的[从 Redis7.0 发布看 Redis 的过去与未来](https://mp.weixin.qq.com/s/RnoPPL7jiFSKkx3G4p57Pg) 这篇文章。 - -> AOF 重写期间的增量数据如何处理一直是个问题,在过去写期间的增量数据需要在内存中保留,写结束后再把这部分增量数据写入新的 AOF 文件中以保证数据完整性。可以看出来 AOF 写会额外消耗内存和磁盘 IO,这也是 Redis AOF 写的痛点,虽然之前也进行过多次改进但是资源消耗的本质问题一直没有解决。 -> -> 阿里云的 Redis 企业版在最初也遇到了这个问题,在内部经过多次迭代开发,实现了 Multi-part AOF 机制来解决,同时也贡献给了社区并随此次 7.0 发布。具体方法是采用 base(全量数据)+inc(增量数据)独立文件存储的方式,彻底解决内存和 IO 资源的浪费,同时也支持对历史 AOF 文件的保存管理,结合 AOF 文件中的时间信息还可以实现 PITR 按时间点恢复(阿里云企业版 Tair 已支持),这进一步增强了 Redis 的数据可靠性,满足用户数据回档等需求。 - -**相关 issue**:[Redis AOF 重写描述不准确 #1439](https://github.com/Snailclimb/JavaGuide/issues/1439)。 - -### AOF 校验机制了解吗? - -纯 AOF 模式下,Redis 不会对整个 AOF 文件使用校验和(如 CRC64),而是通过逐条解析文件中的命令来验证文件的有效性。如果解析过程中发现语法错误(如命令不完整、格式错误),Redis 会终止加载并报错,从而避免错误数据载入内存。 - -在 **混合持久化模式**(Redis 4.0 引入)下,AOF 文件由两部分组成: - -- **RDB 快照部分**:文件以固定的 `REDIS` 字符开头,存储某一时刻的内存数据快照,并在快照数据末尾附带一个 CRC64 校验和(位于 RDB 数据块尾部、AOF 增量部分之前)。 -- **AOF 增量部分**:紧随 RDB 快照部分之后,记录 RDB 快照生成后的增量写命令。这部分增量命令以 Redis 协议格式逐条记录,无整体或全局校验和。 - -RDB 文件结构的核心部分如下: - -| **字段** | **解释** | -| ----------------- | ---------------------------------------------- | -| `"REDIS"` | 固定以该字符串开始 | -| `RDB_VERSION` | RDB 文件的版本号 | -| `DB_NUM` | Redis 数据库编号,指明数据需要存放到哪个数据库 | -| `KEY_VALUE_PAIRS` | Redis 中具体键值对的存储 | -| `EOF` | RDB 文件结束标志 | -| `CHECK_SUM` | 8 字节确保 RDB 完整性的校验和 | - -Redis 启动并加载 AOF 文件时,首先会校验文件开头 RDB 快照部分的数据完整性,即计算该部分数据的 CRC64 校验和,并与紧随 RDB 数据之后、AOF 增量部分之前存储的 CRC64 校验和值进行比较。如果 CRC64 校验和不匹配,Redis 将拒绝启动并报告错误。 - -RDB 部分校验通过后,Redis 随后逐条解析 AOF 部分的增量命令。如果解析过程中出现错误(如不完整的命令或格式错误),Redis 会停止继续加载后续命令,并报告错误,但此时 Redis 已经成功加载了 RDB 快照部分的数据。 - -## Redis 4.0 对于持久化机制做了什么优化? - -由于 RDB 和 AOF 各有优势,于是,Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。 - -如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。 - -官方文档地址: - -![](https://oss.javaguide.cn/github/javaguide/database/redis/redis4.0-persitence.png) - -## 如何选择 RDB 和 AOF? - -关于 RDB 和 AOF 的优缺点,官网上面也给了比较详细的说明[Redis persistence](https://redis.io/docs/manual/persistence/),这里结合自己的理解简单总结一下。 - -**RDB 比 AOF 优秀的地方**: - -- RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。AOF 文件存储的是每一次写命令,类似于 MySQL 的 binlog 日志,通常会比 RDB 文件大很多。当 AOF 变得太大时,Redis 能够在后台自动重写 AOF。新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。不过, Redis 7.0 版本之前,如果在重写期间有写入命令,AOF 可能会使用大量内存,重写期间到达的所有写入命令都会写入磁盘两次。 -- 使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。而 AOF 则需要依次执行每个写命令,速度非常慢。也就是说,与 AOF 相比,恢复大数据集的时候,RDB 速度更快。 - -**AOF 比 RDB 优秀的地方**: - -- RDB 的数据安全性不如 AOF,没有办法实时或者秒级持久化数据。生成 RDB 文件的过程是比较繁重的, 虽然 BGSAVE 子进程写入 RDB 文件的工作不会阻塞主线程,但会对机器的 CPU 资源和内存资源产生影响,严重的情况下甚至会直接把 Redis 服务干宕机。AOF 支持秒级数据丢失(取决 fsync 策略,如果是 everysec,最多丢失 1 秒的数据),仅仅是追加命令到 AOF 文件,操作轻量。 -- RDB 文件是以特定的二进制格式保存的,并且在 Redis 版本演进中有多个版本的 RDB,所以存在老版本的 Redis 服务不兼容新版本的 RDB 格式的问题。 -- AOF 以一种易于理解和解析的格式包含所有操作的日志。你可以轻松地导出 AOF 文件进行分析,你也可以直接操作 AOF 文件来解决一些问题。比如,如果执行`FLUSHALL`命令意外地刷新了所有内容后,只要 AOF 文件没有被重写,删除最新命令并重启即可恢复之前的状态。 - -**综上**: - -- Redis 保存的数据丢失一些也没什么影响的话,可以选择使用 RDB。 -- 不建议单独使用 AOF,因为时不时地创建一个 RDB 快照可以进行数据库备份、更快的重启以及解决 AOF 引擎错误。 -- 如果保存的数据要求安全性比较高的话,建议同时开启 RDB 和 AOF 持久化或者开启 RDB 和 AOF 混合持久化。 - -## 参考 - -- 《Redis 设计与实现》 -- Redis persistence - Redis 官方文档: -- The difference between AOF and RDB persistence: -- Redis AOF 持久化详解 - 程序员历小冰: -- Redis RDB 与 AOF 持久化 · Analyze: - - +1. **Command Append (append)**: All write commands are appended to the AOF buffer. +1. **File Write (write)**: The data in the AOF buffer is written to the AOF file. This step requires calling the `write` function (system call), and `write` returns directly after writing the data to the system kernel buffer (delayed write). Note!!! At this point, it has not been synchronized to disk. +1. \*\*File Synchron diff --git a/docs/database/redis/redis-questions-01.md b/docs/database/redis/redis-questions-01.md index 7102985b9a5..ead2803acfb 100644 --- a/docs/database/redis/redis-questions-01.md +++ b/docs/database/redis/redis-questions-01.md @@ -1,874 +1,57 @@ --- -title: Redis常见面试题总结(上) -category: 数据库 +title: Summary of Common Redis Interview Questions (Part 1) +category: Database tag: - Redis head: - - - meta - - name: keywords - content: Redis基础,Redis常见数据结构,Redis线程模型,Redis内存管理,Redis事务,Redis性能优化 - - - meta - - name: description - content: 一篇文章总结Redis常见的知识点和面试题,涵盖Redis基础、Redis常见数据结构、Redis线程模型、Redis内存管理、Redis事务、Redis性能优化等内容。 + - - meta + - name: keywords + content: Redis Basics, Common Redis Data Structures, Redis Thread Model, Redis Memory Management, Redis Transactions, Redis Performance Optimization + - - meta + - name: description + content: An article summarizing common knowledge points and interview questions about Redis, covering Redis basics, common data structures, thread model, memory management, transactions, performance optimization, and more. --- -## Redis 基础 +## Redis Basics -### 什么是 Redis? +### What is Redis? -[Redis](https://redis.io/) (**RE**mote **DI**ctionary **S**erver)是一个基于 C 语言开发的开源 NoSQL 数据库(BSD 许可)。与传统数据库不同的是,Redis 的数据是保存在内存中的(内存数据库,支持持久化),因此读写速度非常快,被广泛应用于分布式缓存方向。并且,Redis 存储的是 KV 键值对数据。 +[Redis](https://redis.io/) (**RE**mote **DI**ctionary **S**erver) is an open-source NoSQL database developed in C language (BSD license). Unlike traditional databases, Redis stores data in memory (in-memory database, supports persistence), which allows for very fast read and write speeds, making it widely used in distributed caching. Additionally, Redis stores data as key-value pairs (KV). -为了满足不同的业务场景,Redis 内置了多种数据类型实现(比如 String、Hash、Sorted Set、Bitmap、HyperLogLog、GEO)。并且,Redis 还支持事务、持久化、Lua 脚本、发布订阅模型、多种开箱即用的集群方案(Redis Sentinel、Redis Cluster)。 +To meet different business scenarios, Redis has built-in implementations of various data types (such as String, Hash, Sorted Set, Bitmap, HyperLogLog, GEO). Furthermore, Redis supports transactions, persistence, Lua scripting, a publish-subscribe model, and various out-of-the-box clustering solutions (Redis Sentinel, Redis Cluster). -![Redis 数据类型概览](https://oss.javaguide.cn/github/javaguide/database/redis/redis-overview-of-data-types-2023-09-28.jpg) +![Overview of Redis Data Types](https://oss.javaguide.cn/github/javaguide/database/redis/redis-overview-of-data-types-2023-09-28.jpg) -Redis 没有外部依赖,Linux 和 OS X 是 Redis 开发和测试最多的两个操作系统,官方推荐生产环境使用 Linux 部署 Redis。 +Redis has no external dependencies, and Linux and OS X are the two operating systems where Redis is developed and tested the most. The official recommendation is to deploy Redis in a production environment using Linux. -个人学习的话,你可以自己本机安装 Redis 或者通过 Redis 官网提供的[在线 Redis 环境](https://try.redis.io/)(少部分命令无法使用)来实际体验 Redis。 +For personal learning, you can install Redis on your local machine or use the [online Redis environment](https://try.redis.io/) provided by the Redis official website (some commands may not be available) to experience Redis in practice. ![try-redis](https://oss.javaguide.cn/github/javaguide/database/redis/try.redis.io.png) -全世界有非常多的网站使用到了 Redis,[techstacks.io](https://techstacks.io/) 专门维护了一个[使用 Redis 的热门站点列表](https://techstacks.io/tech/redis),感兴趣的话可以看看。 +Many websites around the world use Redis, and [techstacks.io](https://techstacks.io/) maintains a [list of popular sites using Redis](https://techstacks.io/tech/redis). If you're interested, you can take a look. -### Redis 为什么这么快? +### Why is Redis so fast? -Redis 内部做了非常多的性能优化,比较重要的有下面 4 点: +Redis has implemented many performance optimizations internally, with the following four points being particularly important: -1. **纯内存操作 (Memory-Based Storage)** :这是最主要的原因。Redis 数据读写操作都发生在内存中,访问速度是纳秒级别,而传统数据库频繁读写磁盘的速度是毫秒级别,两者相差数个数量级。 -2. **高效的 I/O 模型 (I/O Multiplexing & Single-Threaded Event Loop)** :Redis 使用单线程事件循环配合 I/O 多路复用技术,让单个线程可以同时处理多个网络连接上的 I/O 事件(如读写),避免了多线程模型中的上下文切换和锁竞争问题。虽然是单线程,但结合内存操作的高效性和 I/O 多路复用,使得 Redis 能轻松处理大量并发请求(Redis 线程模型会在后文中详细介绍到)。 -3. **优化的内部数据结构 (Optimized Data Structures)** :Redis 提供多种数据类型(如 String, List, Hash, Set, Sorted Set 等),其内部实现采用高度优化的编码方式(如 ziplist, quicklist, skiplist, hashtable 等)。Redis 会根据数据大小和类型动态选择最合适的内部编码,以在性能和空间效率之间取得最佳平衡。 -4. **简洁高效的通信协议 (Simple Protocol - RESP)** :Redis 使用的是自己设计的 RESP (REdis Serialization Protocol) 协议。这个协议实现简单、解析性能好,并且是二进制安全的。客户端和服务端之间通信的序列化/反序列化开销很小,有助于提升整体的交互速度。 +1. **Memory-Based Storage**: This is the primary reason. Redis read and write operations occur in memory, with access speeds at the nanosecond level, while traditional databases frequently read and write to disk at the millisecond level, resulting in several orders of magnitude difference. +1. **Efficient I/O Model (I/O Multiplexing & Single-Threaded Event Loop)**: Redis uses a single-threaded event loop combined with I/O multiplexing technology, allowing a single thread to handle multiple I/O events (such as read and write) on multiple network connections simultaneously, avoiding context switching and lock contention issues in multi-threaded models. Although it is single-threaded, the efficiency of memory operations and I/O multiplexing allows Redis to easily handle a large number of concurrent requests (the Redis thread model will be introduced in detail later). +1. **Optimized Internal Data Structures**: Redis provides various data types (such as String, List, Hash, Set, Sorted Set, etc.), and its internal implementation uses highly optimized encoding methods (such as ziplist, quicklist, skiplist, hashtable, etc.). Redis dynamically selects the most suitable internal encoding based on data size and type to achieve the best balance between performance and space efficiency. +1. **Simple and Efficient Communication Protocol (Simple Protocol - RESP)**: Redis uses its own designed RESP (REdis Serialization Protocol) protocol. This protocol is simple to implement, has good parsing performance, and is binary safe. The serialization/deserialization overhead for communication between the client and server is minimal, which helps improve overall interaction speed. -> 下面这张图片总结的挺不错的,分享一下,出自 [Why is Redis so fast?](https://twitter.com/alexxubyte/status/1498703822528544770)。 +> The image below summarizes this well, shared from [Why is Redis so fast?](https://twitter.com/alexxubyte/status/1498703822528544770). ![why-redis-so-fast](./images/why-redis-so-fast.png) -那既然都这么快了,为什么不直接用 Redis 当主数据库呢?主要是因为内存成本太高,并且 Redis 提供的数据持久化仍然有数据丢失的风险。 +Since Redis is so fast, why not use it as the primary database? The main reason is that memory costs are too high, and the data persistence provided by Redis still carries the risk of data loss. -### 除了 Redis,你还知道其他分布式缓存方案吗? +### Besides Redis, do you know other distributed caching solutions? -如果面试中被问到这个问题的话,面试官主要想看看: +If asked this question in an interview, the interviewer mainly wants to see: -1. 你在选择 Redis 作为分布式缓存方案时,是否是经过严谨的调研和思考,还是只是因为 Redis 是当前的“热门”技术。 -2. 你在分布式缓存方向的技术广度。 +1. Whether your choice of Redis as a distributed caching solution was based on rigorous research and thought, or just because Redis is currently a "hot" technology. +1. Your breadth of knowledge in distributed caching technologies. -如果你了解其他方案,并且能解释为什么最终选择了 Redis(更进一步!),这会对你面试表现加分不少! - -下面简单聊聊常见的分布式缓存技术选型。 - -分布式缓存的话,比较老牌同时也是使用的比较多的还是 **Memcached** 和 **Redis**。不过,现在基本没有看过还有项目使用 **Memcached** 来做缓存,都是直接用 **Redis**。 - -Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。 - -有一些大厂也开源了类似于 Redis 的分布式高性能 KV 存储数据库,例如,腾讯开源的 [**Tendis**](https://github.com/Tencent/Tendis)。Tendis 基于知名开源项目 [RocksDB](https://github.com/facebook/rocksdb) 作为存储引擎 ,100% 兼容 Redis 协议和 Redis4.0 所有数据模型。关于 Redis 和 Tendis 的对比,腾讯官方曾经发过一篇文章:[Redis vs Tendis:冷热混合存储版架构揭秘](https://mp.weixin.qq.com/s/MeYkfOIdnU6LYlsGb24KjQ),可以简单参考一下。 - -不过,从 Tendis 这个项目的 Github 提交记录可以看出,Tendis 开源版几乎已经没有被维护更新了,加上其关注度并不高,使用的公司也比较少。因此,不建议你使用 Tendis 来实现分布式缓存。 - -目前,比较业界认可的 Redis 替代品还是下面这两个开源分布式缓存(都是通过碰瓷 Redis 火的): - -- [Dragonfly](https://github.com/dragonflydb/dragonfly):一种针对现代应用程序负荷需求而构建的内存数据库,完全兼容 Redis 和 Memcached 的 API,迁移时无需修改任何代码,号称全世界最快的内存数据库。 -- [KeyDB](https://github.com/Snapchat/KeyDB):Redis 的一个高性能分支,专注于多线程、内存效率和高吞吐量。 - -不过,个人还是建议分布式缓存首选 Redis,毕竟经过了这么多年的考验,生态非常优秀,资料也很全面! - -PS:篇幅问题,我这并没有对上面提到的分布式缓存选型做详细介绍和对比,感兴趣的话,可以自行研究一下。 - -### 说一下 Redis 和 Memcached 的区别和共同点 - -现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!不过,了解 Redis 和 Memcached 的区别和共同点,有助于我们在做相应的技术选型的时候,能够做到有理有据! - -**共同点**: - -1. 都是基于内存的数据库,一般都用来当做缓存使用。 -2. 都有过期策略。 -3. 两者的性能都非常高。 - -**区别**: - -1. **数据类型**:Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list、set、zset、hash 等数据结构的存储;而 Memcached 只支持最简单的 k/v 数据类型。 -2. **数据持久化**:Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用;而 Memcached 把数据全部存在内存之中。也就是说,Redis 有灾难恢复机制,而 Memcached 没有。 -3. **集群模式支持**:Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;而 Redis 自 3.0 版本起是原生支持集群模式的。 -4. **线程模型**:Memcached 是多线程、非阻塞 IO 复用的网络模型;而 Redis 使用单线程的多路 IO 复用模型(Redis 6.0 针对网络数据的读写引入了多线程)。 -5. **特性支持**:Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。 -6. **过期数据删除**:Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。 - -相信看了上面的对比之后,我们已经没有什么理由可以选择使用 Memcached 来作为自己项目的分布式缓存了。 - -### 为什么要用 Redis? - -**1、访问速度更快** - -传统数据库数据保存在磁盘,而 Redis 基于内存,内存的访问速度比磁盘快很多。引入 Redis 之后,我们可以把一些高频访问的数据放到 Redis 中,这样下次就可以直接从内存中读取,速度可以提升几十倍甚至上百倍。 - -**2、高并发** - -一般像 MySQL 这类的数据库的 QPS 大概都在 4k 左右(4 核 8g),但是使用 Redis 缓存之后很容易达到 5w+,甚至能达到 10w+(就单机 Redis 的情况,Redis 集群的话会更高)。 - -> QPS(Query Per Second):服务器每秒可以执行的查询次数; - -由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。 - -**3、功能全面** - -Redis 除了可以用作缓存之外,还可以用于分布式锁、限流、消息队列、延时队列等场景,功能强大! - -### 为什么用 Redis 而不用本地缓存呢? - -| 特性 | 本地缓存 | Redis | -| ------------ | ------------------------------------ | -------------------------------- | -| 数据一致性 | 多服务器部署时存在数据不一致问题 | 数据一致 | -| 内存限制 | 受限于单台服务器内存 | 独立部署,内存空间更大 | -| 数据丢失风险 | 服务器宕机数据丢失 | 可持久化,数据不易丢失 | -| 管理维护 | 分散,管理不便 | 集中管理,提供丰富的管理工具 | -| 功能丰富性 | 功能有限,通常只提供简单的键值对存储 | 功能丰富,支持多种数据结构和功能 | - -### 常见的缓存读写策略有哪些? - -关于常见的缓存读写策略的详细介绍,可以看我写的这篇文章:[3 种常用的缓存读写策略详解](https://javaguide.cn/database/redis/3-commonly-used-cache-read-and-write-strategies.html)。 - -### 什么是 Redis Module?有什么用? - -Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特殊的需求。这些 Module 以动态链接库(so 文件)的形式被加载到 Redis 中,这是一种非常灵活的动态扩展功能的实现方式,值得借鉴学习! - -我们每个人都可以基于 Redis 去定制化开发自己的 Module,比如实现搜索引擎功能、自定义分布式锁和分布式限流。 - -目前,被 Redis 官方推荐的 Module 有: - -- [RediSearch](https://github.com/RediSearch/RediSearch):用于实现搜索引擎的模块。 -- [RedisJSON](https://github.com/RedisJSON/RedisJSON):用于处理 JSON 数据的模块。 -- [RedisGraph](https://github.com/RedisGraph/RedisGraph):用于实现图形数据库的模块。 -- [RedisTimeSeries](https://github.com/RedisTimeSeries/RedisTimeSeries):用于处理时间序列数据的模块。 -- [RedisBloom](https://github.com/RedisBloom/RedisBloom):用于实现布隆过滤器的模块。 -- [RedisAI](https://github.com/RedisAI/RedisAI):用于执行深度学习/机器学习模型并管理其数据的模块。 -- [RedisCell](https://github.com/brandur/redis-cell):用于实现分布式限流的模块。 -- …… - -关于 Redis 模块的详细介绍,可以查看官方文档:。 - -## Redis 应用 - -### Redis 除了做缓存,还能做什么? - -- **分布式锁**:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html)。 -- **限流**:一般是通过 Redis + Lua 脚本的方式来实现限流。如果不想自己写 Lua 脚本的话,也可以直接利用 Redisson 中的 `RRateLimiter` 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。 -- **消息队列**:Redis 自带的 List 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。 -- **延时队列**:Redisson 内置了延时队列(基于 Sorted Set 实现的)。 -- **分布式 Session**:利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。 -- **复杂业务场景**:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景,比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜、通过 HyperLogLog 统计网站 UV 和 PV。 -- …… - -### 如何基于 Redis 实现分布式锁? - -关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock-implementations.html)。 - -### Redis 可以做消息队列么? - -> 实际项目中使用 Redis 来做消息队列的非常少,毕竟有更成熟的消息队列中间件可以用。 - -先说结论:**可以是可以,但不建议使用 Redis 来做消息队列。和专业的消息队列相比,还是有很多欠缺的地方。** - -**Redis 2.0 之前,如果想要使用 Redis 来做消息队列的话,只能通过 List 来实现。** - -通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP` 即可实现简易版消息队列: - -```bash -# 生产者生产消息 -> RPUSH myList msg1 msg2 -(integer) 2 -> RPUSH myList msg3 -(integer) 3 -# 消费者消费消息 -> LPOP myList -"msg1" -``` - -不过,通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP` 这样的方式存在性能问题,我们需要不断轮询去调用 `RPOP` 或 `LPOP` 来消费消息。当 List 为空时,大部分的轮询的请求都是无效请求,这种方式大量浪费了系统资源。 - -因此,Redis 还提供了 `BLPOP`、`BRPOP` 这种阻塞式读取的命令(带 B-Blocking 的都是阻塞式),并且还支持一个超时参数。如果 List 为空,Redis 服务端不会立刻返回结果,它会等待 List 中有新数据后再返回或者是等待最多一个超时时间后返回空。如果将超时时间设置为 0 时,即可无限等待,直到弹出消息 - -```bash -# 超时时间为 10s -# 如果有数据立刻返回,否则最多等待10秒 -> BRPOP myList 10 -null -``` - -**List 实现消息队列功能太简单,像消息确认机制等功能还需要我们自己实现,最要命的是没有广播机制,消息也只能被消费一次。** - -**Redis 2.0 引入了发布订阅 (pub/sub) 功能,解决了 List 实现消息队列没有广播机制的问题。** - -![Redis 发布订阅 (pub/sub) 功能](https://oss.javaguide.cn/github/javaguide/database/redis/redis-pub-sub.png) - -pub/sub 中引入了一个概念叫 **channel(频道)**,发布订阅机制的实现就是基于这个 channel 来做的。 - -pub/sub 涉及发布者(Publisher)和订阅者(Subscriber,也叫消费者)两个角色: - -- 发布者通过 `PUBLISH` 投递消息给指定 channel。 -- 订阅者通过`SUBSCRIBE`订阅它关心的 channel。并且,订阅者可以订阅一个或者多个 channel。 - -我们这里启动 3 个 Redis 客户端来简单演示一下: - -![pub/sub 实现消息队列演示](https://oss.javaguide.cn/github/javaguide/database/redis/redis-pubsub-message-queue.png) - -pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不过,消息丢失(客户端断开连接或者 Redis 宕机都会导致消息丢失)、消息堆积(发布者发布消息的时候不会管消费者的具体消费能力如何)等问题依然没有一个比较好的解决办法。 - -为此,Redis 5.0 新增加的一个数据结构 `Stream` 来做消息队列。`Stream` 支持: - -- 发布 / 订阅模式; -- 按照消费者组进行消费(借鉴了 Kafka 消费者组的概念); -- 消息持久化( RDB 和 AOF); -- ACK 机制(通过确认机制来告知已经成功处理了消息); -- 阻塞式获取消息。 - -`Stream` 的结构如下: - -![](https://oss.javaguide.cn/github/javaguide/database/redis/redis-stream-structure.png) - -这是一个有序的消息链表,每个消息都有一个唯一的 ID 和对应的内容。ID 是一个时间戳和序列号的组合,用来保证消息的唯一性和递增性。内容是一个或多个键值对(类似 Hash 基本数据类型),用来存储消息的数据。 - -这里再对图中涉及到的一些概念,进行简单解释: - -- `Consumer Group`:消费者组用于组织和管理多个消费者。消费者组本身不处理消息,而是再将消息分发给消费者,由消费者进行真正的消费。 -- `last_delivered_id`:标识消费者组当前消费位置的游标,消费者组中任意一个消费者读取了消息都会使 last_delivered_id 往前移动。 -- `pending_ids`:记录已经被客户端消费但没有 ack 的消息的 ID。 - -下面是`Stream` 用作消息队列时常用的命令: - -- `XADD`:向流中添加新的消息。 -- `XREAD`:从流中读取消息。 -- `XREADGROUP`:从消费组中读取消息。 -- `XRANGE`:根据消息 ID 范围读取流中的消息。 -- `XREVRANGE`:与 `XRANGE` 类似,但以相反顺序返回结果。 -- `XDEL`:从流中删除消息。 -- `XTRIM`:修剪流的长度,可以指定修建策略(`MAXLEN`/`MINID`)。 -- `XLEN`:获取流的长度。 -- `XGROUP CREATE`:创建消费者组。 -- `XGROUP DESTROY`:删除消费者组。 -- `XGROUP DELCONSUMER`:从消费者组中删除一个消费者。 -- `XGROUP SETID`:为消费者组设置新的最后递送消息 ID。 -- `XACK`:确认消费组中的消息已被处理。 -- `XPENDING`:查询消费组中挂起(未确认)的消息。 -- `XCLAIM`:将挂起的消息从一个消费者转移到另一个消费者。 -- `XINFO`:获取流(`XINFO STREAM`)、消费组(`XINFO GROUPS`)或消费者(`XINFO CONSUMERS`)的详细信息。 - -`Stream` 使用起来相对要麻烦一些,这里就不演示了。 - -总的来说,`Stream` 已经可以满足一个消息队列的基本要求了。不过,`Stream` 在实际使用中依然会有一些小问题不太好解决,比如在 Redis 发生故障恢复后不能保证消息至少被消费一次。 - -综上,和专业的消息队列相比,使用 Redis 来实现消息队列还是有很多欠缺的地方,比如消息丢失和堆积问题不好解决。因此,我们通常建议不要使用 Redis 来做消息队列,你完全可以选择市面上比较成熟的一些消息队列,比如 RocketMQ、Kafka。不过,如果你就是想要用 Redis 来做消息队列的话,那我建议你优先考虑 `Stream`,这是目前相对最优的 Redis 消息队列实现。 - -相关阅读:[Redis 消息队列发展历程 - 阿里开发者 - 2022](https://mp.weixin.qq.com/s/gCUT5TcCQRAxYkTJfTRjJw)。 - -### Redis 可以做搜索引擎么? - -Redis 是可以实现全文搜索引擎功能的,需要借助 **RediSearch**,这是一个基于 Redis 的搜索引擎模块。 - -RediSearch 支持中文分词、聚合统计、停用词、同义词、拼写检查、标签查询、向量相似度查询、多关键词搜索、分页搜索等功能,算是一个功能比较完善的全文搜索引擎了。 - -相比较于 Elasticsearch 来说,RediSearch 主要在下面两点上表现更优异一些: - -1. 性能更优秀:依赖 Redis 自身的高性能,基于内存操作(Elasticsearch 基于磁盘)。 -2. 较低内存占用实现快速索引:RediSearch 内部使用压缩的倒排索引,所以可以用较低的内存占用来实现索引的快速构建。 - -对于小型项目的简单搜索场景来说,使用 RediSearch 来作为搜索引擎还是没有问题的(搭配 RedisJSON 使用)。 - -对于比较复杂或者数据规模较大的搜索场景,还是不太建议使用 RediSearch 来作为搜索引擎,主要是因为下面这些限制和问题: - -1. 数据量限制:Elasticsearch 可以支持 PB 级别的数据量,可以轻松扩展到多个节点,利用分片机制提高可用性和性能。RedisSearch 是基于 Redis 实现的,其能存储的数据量受限于 Redis 的内存容量,不太适合存储大规模的数据(内存昂贵,扩展能力较差)。 -2. 分布式能力较差:Elasticsearch 是为分布式环境设计的,可以轻松扩展到多个节点。虽然 RedisSearch 支持分布式部署,但在实际应用中可能会面临一些挑战,如数据分片、节点间通信、数据一致性等问题。 -3. 聚合功能较弱:Elasticsearch 提供了丰富的聚合功能,而 RediSearch 的聚合功能相对较弱,只支持简单的聚合操作。 -4. 生态较差:Elasticsearch 可以轻松和常见的一些系统/软件集成比如 Hadoop、Spark、Kibana,而 RedisSearch 则不具备该优势。 - -Elasticsearch 适用于全文搜索、复杂查询、实时数据分析和聚合的场景,而 RediSearch 适用于快速数据存储、缓存和简单查询的场景。 - -### 如何基于 Redis 实现延时任务? - -> 类似的问题: -> -> - 订单在 10 分钟后未支付就失效,如何用 Redis 实现? -> - 红包 24 小时未被查收自动退还,如何用 Redis 实现? - -基于 Redis 实现延时任务的功能无非就下面两种方案: - -1. Redis 过期事件监听。 -2. Redisson 内置的延时队列。 - -Redis 过期事件监听存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。 - -Redisson 内置的延时队列具备下面这些优势: - -1. **减少了丢消息的可能**:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。 -2. **消息不存在重复消费问题**:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。 - -关于 Redis 实现延时任务的详细介绍,可以看我写的这篇文章:[如何基于 Redis 实现延时任务?](./redis-delayed-task.md)。 - -## Redis 数据类型 - -关于 Redis 5 种基础数据类型和 3 种特殊数据类型的详细介绍请看下面这两篇文章以及 [Redis 官方文档](https://redis.io/docs/data-types/): - -- [Redis 5 种基本数据类型详解](https://javaguide.cn/database/redis/redis-data-structures-01.html) -- [Redis 3 种特殊数据类型详解](https://javaguide.cn/database/redis/redis-data-structures-02.html) - -### Redis 常用的数据类型有哪些? - -Redis 中比较常见的数据类型有下面这些: - -- **5 种基础数据类型**:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。 -- **3 种特殊数据类型**:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。 - -除了上面提到的之外,还有一些其他的比如 [Bloom filter(布隆过滤器)](https://javaguide.cn/cs-basics/data-structure/bloom-filter.html)、Bitfield(位域)。 - -### String 的应用场景有哪些? - -String 是 Redis 中最简单同时也是最常用的一个数据类型。它是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。 - -String 的常见应用场景如下: - -- 常规数据(比如 Session、Token、序列化后的对象、图片的路径)的缓存; -- 计数比如用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数; -- 分布式锁(利用 `SETNX key value` 命令可以实现一个最简易的分布式锁); -- …… - -关于 String 的详细介绍请看这篇文章:[Redis 5 种基本数据类型详解](https://javaguide.cn/database/redis/redis-data-structures-01.html)。 - -### String 还是 Hash 存储对象数据更好呢? - -简单对比一下二者: - -- **对象存储方式**:String 存储的是序列化后的对象数据,存放的是整个对象,操作简单直接。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。 -- **内存消耗**:Hash 通常比 String 更节省内存,特别是在字段较多且字段长度较短时。Redis 对小型 Hash 进行优化(如使用 ziplist 存储),进一步降低内存占用。 -- **复杂对象存储**:String 在处理多层嵌套或复杂结构的对象时更方便,因为无需处理每个字段的独立存储和操作。 -- **性能**:String 的操作通常具有 O(1) 的时间复杂度,因为它存储的是整个对象,操作简单直接,整体读写的性能较好。Hash 由于需要处理多个字段的增删改查操作,在字段较多且经常变动的情况下,可能会带来额外的性能开销。 - -总结: - -- 在绝大多数情况下,**String** 更适合存储对象数据,尤其是当对象结构简单且整体读写是主要操作时。 -- 如果你需要频繁操作对象的部分字段或节省内存,**Hash** 可能是更好的选择。 - -### String 的底层实现是什么? - -Redis 是基于 C 语言编写的,但 Redis 的 String 类型的底层实现并不是 C 语言中的字符串(即以空字符 `\0` 结尾的字符数组),而是自己编写了 [SDS](https://github.com/antirez/sds)(Simple Dynamic String,简单动态字符串)来作为底层实现。 - -SDS 最早是 Redis 作者为日常 C 语言开发而设计的 C 字符串,后来被应用到了 Redis 上,并经过了大量的修改完善以适合高性能操作。 - -Redis7.0 的 SDS 的部分源码如下(): - -```c -/* Note: sdshdr5 is never used, we just access the flags byte directly. - * However is here to document the layout of type 5 SDS strings. */ -struct __attribute__ ((__packed__)) sdshdr5 { - unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ - char buf[]; -}; -struct __attribute__ ((__packed__)) sdshdr8 { - uint8_t len; /* used */ - uint8_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; -}; -struct __attribute__ ((__packed__)) sdshdr16 { - uint16_t len; /* used */ - uint16_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; -}; -struct __attribute__ ((__packed__)) sdshdr32 { - uint32_t len; /* used */ - uint32_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; -}; -struct __attribute__ ((__packed__)) sdshdr64 { - uint64_t len; /* used */ - uint64_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; -}; -``` - -通过源码可以看出,SDS 共有五种实现方式:SDS_TYPE_5(并未用到)、SDS_TYPE_8、SDS_TYPE_16、SDS_TYPE_32、SDS_TYPE_64,其中只有后四种实际用到。Redis 会根据初始化的长度决定使用哪种类型,从而减少内存的使用。 - -| 类型 | 字节 | 位 | -| -------- | ---- | --- | -| sdshdr5 | < 1 | <8 | -| sdshdr8 | 1 | 8 | -| sdshdr16 | 2 | 16 | -| sdshdr32 | 4 | 32 | -| sdshdr64 | 8 | 64 | - -对于后四种实现都包含了下面这 4 个属性: - -- `len`:字符串的长度也就是已经使用的字节数。 -- `alloc`:总共可用的字符空间大小,alloc-len 就是 SDS 剩余的空间大小。 -- `buf[]`:实际存储字符串的数组。 -- `flags`:低三位保存类型标志。 - -SDS 相比于 C 语言中的字符串有如下提升: - -1. **可以避免缓冲区溢出**:C 语言中的字符串被修改(比如拼接)时,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。SDS 被修改时,会先根据 len 属性检查空间大小是否满足要求,如果不满足,则先扩展至所需大小再进行修改操作。 -2. **获取字符串长度的复杂度较低**:C 语言中的字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。SDS 的长度获取直接读取 len 属性即可,时间复杂度为 O(1)。 -3. **减少内存分配次数**:为了避免修改(增加/减少)字符串时,每次都需要重新分配内存(C 语言的字符串是这样的),SDS 实现了空间预分配和惰性空间释放两种优化策略。当 SDS 需要增加字符串时,Redis 会为 SDS 分配好内存,并且根据特定的算法分配多余的内存,这样可以减少连续执行字符串增长操作所需的内存重分配次数。当 SDS 需要减少字符串时,这部分内存不会立即被回收,会被记录下来,等待后续使用(支持手动释放,有对应的 API)。 -4. **二进制安全**:C 语言中的字符串以空字符 `\0` 作为字符串结束的标识,这存在一些问题,像一些二进制文件(比如图片、视频、音频)就可能包括空字符,C 字符串无法正确保存。SDS 使用 len 属性判断字符串是否结束,不存在这个问题。 - -🤐 多提一嘴,很多文章里 SDS 的定义是下面这样的: - -```c -struct sdshdr { - unsigned int len; - unsigned int free; - char buf[]; -}; -``` - -这个也没错,Redis 3.2 之前就是这样定义的。后来,由于这种方式的定义存在问题,`len` 和 `free` 的定义用了 4 个字节,造成了浪费。Redis 3.2 之后,Redis 改进了 SDS 的定义,将其划分为了现在的 5 种类型。 - -### 购物车信息用 String 还是 Hash 存储更好呢? - -由于购物车中的商品频繁修改和变动,购物车信息建议使用 Hash 存储: - -- 用户 id 为 key -- 商品 id 为 field,商品数量为 value - -![Hash维护简单的购物车信息](https://oss.javaguide.cn/github/javaguide/database/redis/hash-shopping-cart.png) - -那用户购物车信息的维护具体应该怎么操作呢? - -- 用户添加商品就是往 Hash 里面增加新的 field 与 value; -- 查询购物车信息就是遍历对应的 Hash; -- 更改商品数量直接修改对应的 value 值(直接 set 或者做运算皆可); -- 删除商品就是删除 Hash 中对应的 field; -- 清空购物车直接删除对应的 key 即可。 - -这里只是以业务比较简单的购物车场景举例,实际电商场景下,field 只保存一个商品 id 是没办法满足需求的。 - -### 使用 Redis 实现一个排行榜怎么做? - -Redis 中有一个叫做 `Sorted Set`(有序集合)的数据类型经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。 - -相关的一些 Redis 命令:`ZRANGE`(从小到大排序)、`ZREVRANGE`(从大到小排序)、`ZREVRANK`(指定元素排名)。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/2021060714195385.png) - -[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的「技术面试题篇」就有一篇文章详细介绍如何使用 Sorted Set 来设计制作一个排行榜,感兴趣的小伙伴可以看看。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719071115140.png) - -### Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+ 树? - -这道面试题很多大厂比较喜欢问,难度还是有点大的。 - -- 平衡树 vs 跳表:平衡树的插入、删除和查询的时间复杂度和跳表一样都是 **O(log n)**。对于范围查询来说,平衡树也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。跳表诞生的初衷就是为了克服平衡树的一些缺点。跳表使用概率平衡而不是严格强制的平衡,因此,跳表中的插入和删除算法比平衡树的等效算法简单得多,速度也快得多。 -- 红黑树 vs 跳表:相比较于红黑树来说,跳表的实现也更简单一些,不需要通过旋转和染色(红黑变换)来保证黑平衡。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。 -- B+ 树 vs 跳表:B+ 树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+ 树这种方式进行维护,只需按照概率进行随机维护即可,节约内存。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+ 树那样插入时发现失衡时还需要对节点分裂与合并。 - -另外,我还单独写了一篇文章从有序集合的基本使用到跳表的源码分析和实现,让你会对 Redis 的有序集合底层实现的跳表有着更深刻的理解和掌握:[Redis 为什么用跳表实现有序集合](./redis-skiplist.md)。 - -### Set 的应用场景是什么? - -Redis 中 `Set` 是一种无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 `HashSet` 。 - -`Set` 的常见应用场景如下: - -- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是 `HyperLogLog` 更适合一些)、文章点赞、动态点赞等等。 -- 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集)等等。 -- 需要随机获取数据源中的元素的场景:抽奖系统、随机点名等等。 - -### 使用 Set 实现抽奖系统怎么做? - -如果想要使用 `Set` 实现一个简单的抽奖系统的话,直接使用下面这几个命令就可以了: - -- `SADD key member1 member2 ...`:向指定集合添加一个或多个元素。 -- `SPOP key count`:随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。 -- `SRANDMEMBER key count`:随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。 - -### 使用 Bitmap 统计活跃用户怎么做? - -Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap,只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。 - -你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。 - -![img](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220720194154133.png) - -如果想要使用 Bitmap 统计活跃用户的话,可以使用日期(精确到天)作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1。 - -初始化数据: - -```bash -> SETBIT 20210308 1 1 -(integer) 0 -> SETBIT 20210308 2 1 -(integer) 0 -> SETBIT 20210309 1 1 -(integer) 0 -``` - -统计 20210308~20210309 总活跃用户数: - -```bash -> BITOP and desk1 20210308 20210309 -(integer) 1 -> BITCOUNT desk1 -(integer) 1 -``` - -统计 20210308~20210309 在线活跃用户数: - -```bash -> BITOP or desk2 20210308 20210309 -(integer) 1 -> BITCOUNT desk2 -(integer) 2 -``` - -### 使用 HyperLogLog 统计页面 UV 怎么做? - -使用 HyperLogLog 统计页面 UV 主要需要用到下面这两个命令: - -- `PFADD key element1 element2 ...`:添加一个或多个元素到 HyperLogLog 中。 -- `PFCOUNT key1 key2`:获取一个或者多个 HyperLogLog 的唯一计数。 - -1、将访问指定页面的每个用户 ID 添加到 `HyperLogLog` 中。 - -```bash -PFADD PAGE_1:UV USER1 USER2 ...... USERn -``` - -2、统计指定页面的 UV。 - -```bash -PFCOUNT PAGE_1:UV -``` - -## Redis 持久化机制(重要) - -Redis 持久化机制(RDB 持久化、AOF 持久化、RDB 和 AOF 的混合持久化)相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 Redis 持久化机制相关的知识点和问题:[Redis 持久化机制详解](https://javaguide.cn/database/redis/redis-persistence.html)。 - -## Redis 线程模型(重要) - -对于读写命令来说,Redis 一直是单线程模型。不过,在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作,Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。 - -### Redis 单线程模型了解吗? - -**Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型**(Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。 - -《Redis 设计与实现》有一段话是这样介绍文件事件处理器的,我觉得写得挺不错。 - -> Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。 -> -> - 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。 -> - 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。 -> -> **虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字**,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。 - -**既然是单线程,那怎么监听大量的客户端连接呢?** - -Redis 通过 **IO 多路复用程序** 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。 - -这样的好处非常明显:**I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗**(和 NIO 中的 `Selector` 组件很像)。 - -文件事件处理器(file event handler)主要是包含 4 个部分: - -- 多个 socket(客户端连接) -- IO 多路复用程序(支持多个客户端连接的关键) -- 文件事件分派器(将 socket 关联到相应的事件处理器) -- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器) - -![文件事件处理器(file event handler)](https://oss.javaguide.cn/github/javaguide/database/redis/redis-event-handler.png) - -相关阅读:[Redis 事件机制详解](http://remcarpediem.net/article/1aa2da89/)。 - -### Redis6.0 之前为什么不使用多线程? - -虽然说 Redis 是单线程模型,但实际上,**Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。** - -不过,Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主线程之外的其他线程来“异步处理”,从而减少对主线程的影响。 - -为此,Redis 4.0 之后新增了几个异步命令: - -- `UNLINK`:可以看作是 `DEL` 命令的异步版本。 -- `FLUSHALL ASYNC`:用于清空所有数据库的所有键,不限于当前 `SELECT` 的数据库。 -- `FLUSHDB ASYNC`:用于清空当前 `SELECT` 数据库中的所有键。 - -![redis4.0 more thread](https://oss.javaguide.cn/github/javaguide/database/redis/redis4.0-more-thread.png) - -总的来说,直到 Redis 6.0 之前,Redis 的主要操作仍然是单线程处理的。 - -**那 Redis6.0 之前为什么不使用多线程?** 我觉得主要原因有 3 点: - -- 单线程编程容易并且更容易维护; -- Redis 的性能瓶颈不在 CPU,主要在内存和网络; -- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。 - -相关阅读:[为什么 Redis 选择单线程模型?](https://draveness.me/whys-the-design-redis-single-thread/)。 - -### Redis6.0 之后为何引入了多线程? - -**Redis6.0 引入多线程主要是为了提高网络 IO 读写性能**,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。 - -虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。 - -Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要设置 IO 线程数 > 1,需要修改 redis 配置文件 `redis.conf`: - -```bash -io-threads 4 #设置1的话只会开启主线程,官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 -``` - -另外: - -- io-threads 的个数一旦设置,不能通过 config 动态设置。 -- 当设置 ssl 后,io-threads 将不工作。 - -开启多线程后,默认只会使用多线程进行 IO 写入 writes,即发送数据给客户端,如果需要开启多线程 IO 读取 reads,同样需要修改 redis 配置文件 `redis.conf`: - -```bash -io-threads-do-reads yes -``` - -但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启。 - -相关阅读: - -- [Redis 6.0 新特性-多线程连环 13 问!](https://mp.weixin.qq.com/s/FZu3acwK6zrCBZQ_3HoUgw) -- [Redis 多线程网络模型全面揭秘](https://segmentfault.com/a/1190000039223696)(推荐) - -### Redis 后台线程了解吗? - -我们虽然经常说 Redis 是单线程模型(主要逻辑是单线程完成的),但实际还有一些后台线程用于执行一些比较耗时的操作: - -- 通过 `bio_close_file` 后台线程来释放 AOF / RDB 等过程中产生的临时文件资源。 -- 通过 `bio_aof_fsync` 后台线程调用 `fsync` 函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘(AOF 文件)。 -- 通过 `bio_lazy_free` 后台线程释放大对象(已删除)占用的内存空间. - -在`bio.h` 文件中有定义(Redis 6.0 版本,源码地址:): - -```java -#ifndef __BIO_H -#define __BIO_H - -/* Exported API */ -void bioInit(void); -void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3); -unsigned long long bioPendingJobsOfType(int type); -unsigned long long bioWaitStepOfType(int type); -time_t bioOlderJobOfType(int type); -void bioKillThreads(void); - -/* Background job opcodes */ -#define BIO_CLOSE_FILE 0 /* Deferred close(2) syscall. */ -#define BIO_AOF_FSYNC 1 /* Deferred AOF fsync. */ -#define BIO_LAZY_FREE 2 /* Deferred objects freeing. */ -#define BIO_NUM_OPS 3 - -#endif -``` - -关于 Redis 后台线程的详细介绍可以查看 [Redis 6.0 后台线程有哪些?](https://juejin.cn/post/7102780434739626014) 这篇就文章。 - -## Redis 内存管理 - -### Redis 给缓存数据设置过期时间有什么用? - -一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢? - -内存是有限且珍贵的,如果不对缓存数据设置过期时间,那内存占用就会一直增长,最终可能会导致 OOM 问题。通过设置合理的过期时间,Redis 会自动删除暂时不需要的数据,为新的缓存数据腾出空间。 - -Redis 自带了给缓存数据设置过期时间的功能,比如: - -```bash -127.0.0.1:6379> expire key 60 # 数据在 60s 后过期 -(integer) 1 -127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire) -OK -127.0.0.1:6379> ttl key # 查看数据还有多久过期 -(integer) 56 -``` - -注意 ⚠️:Redis 中除了字符串类型有自己独有设置过期时间的命令 `setex` 外,其他方法都需要依靠 `expire` 命令来设置过期时间 。另外,`persist` 命令可以移除一个键的过期时间。 - -**过期时间除了有助于缓解内存的消耗,还有什么其他用么?** - -很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 Token 可能只在 1 天内有效。 - -如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。 - -### Redis 是如何判断数据是否过期的呢? - -Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。 - -![Redis 过期字典](https://oss.javaguide.cn/github/javaguide/database/redis/redis-expired-dictionary.png) - -过期字典是存储在 redisDb 这个结构里的: - -```c -typedef struct redisDb { - ... - - dict *dict; //数据库键空间,保存着数据库中所有键值对 - dict *expires // 过期字典,保存着键的过期时间 - ... -} redisDb; -``` - -在查询一个 key 的时候,Redis 首先检查该 key 是否存在于过期字典中(时间复杂度为 O(1)),如果不在就直接返回,在的话需要判断一下这个 key 是否过期,过期直接删除 key 然后返回 null。 - -### Redis 过期 key 删除策略了解么? - -如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢? - -常用的过期数据的删除策略就下面这几种: - -1. **惰性删除**:只会在取出/查询 key 的时候才对数据进行过期检查。这种方式对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。 -2. **定期删除**:周期性地随机从设置了过期时间的 key 中抽查一批,然后逐个检查这些 key 是否过期,过期就删除 key。相比于惰性删除,定期删除对内存更友好,对 CPU 不太友好。 -3. **延迟队列**:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。 -4. **定时删除**:每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器。 - -**Redis 采用的是那种删除策略呢?** - -Redis 采用的是 **定期删除+惰性/懒汉式删除** 结合的策略,这也是大部分缓存框架的选择。定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,结合起来使用既能兼顾 CPU 友好,又能兼顾内存友好。 - -下面是我们详细介绍一下 Redis 中的定期删除具体是如何做的。 - -Redis 的定期删除过程是随机的(周期性地随机从设置了过期时间的 key 中抽查一批),所以并不保证所有过期键都会被立即删除。这也就解释了为什么有的 key 过期了,并没有被删除。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。 - -另外,定期删除还会受到执行时间和过期 key 的比例的影响: - -- 执行时间已经超过了阈值,那么就中断这一次定期删除循环,以避免使用过多的 CPU 时间。 -- 如果这一批过期的 key 比例超过一个比例,就会重复执行此删除流程,以更积极地清理过期 key。相应地,如果过期的 key 比例低于这个比例,就会中断这一次定期删除循环,避免做过多的工作而获得很少的内存回收。 - -Redis 7.2 版本的执行时间阈值是 **25ms**,过期 key 比例设定值是 **10%**。 - -```c -#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds. */ -#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */ -#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which - we do extra efforts. */ -``` - -**每次随机抽查数量是多少?** - -`expire.c` 中定义了每次随机抽查的数量,Redis 7.2 版本为 20,也就是说每次会随机选择 20 个设置了过期时间的 key 判断是否过期。 - -```c -#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */ -``` - -**如何控制定期删除的执行频率?** - -在 Redis 中,定期删除的频率是由 **hz** 参数控制的。hz 默认为 10,代表每秒执行 10 次,也就是每秒钟进行 10 次尝试来查找并删除过期的 key。 - -hz 的取值范围为 1~500。增大 hz 参数的值会提升定期删除的频率。如果你想要更频繁地执行定期删除任务,可以适当增加 hz 的值,但这会增加 CPU 的使用率。根据 Redis 官方建议,hz 的值不建议超过 100,对于大部分用户使用默认的 10 就足够了。 - -下面是 hz 参数的官方注释,我翻译了其中的重要信息(Redis 7.2 版本)。 - -![redis.conf 对于 hz 的注释](https://oss.javaguide.cn/github/javaguide/database/redis/redis.conf-hz.png) - -类似的参数还有一个 **dynamic-hz**,这个参数开启之后 Redis 就会在 hz 的基础上动态计算一个值。Redis 提供并默认启用了使用自适应 hz 值的能力, - -这两个参数都在 Redis 配置文件 `redis.conf` 中: - -```properties -# 默认为 10 -hz 10 -# 默认开启 -dynamic-hz yes -``` - -多提一嘴,除了定期删除过期 key 这个定期任务之外,还有一些其他定期任务例如关闭超时的客户端连接、更新统计信息,这些定期任务的执行频率也是通过 hz 参数决定。 - -**为什么定期删除不是把所有过期 key 都删除呢?** - -这样会对性能造成太大的影响。如果我们 key 数量非常庞大的话,挨个遍历检查是非常耗时的,会严重影响性能。Redis 设计这种策略的目的是为了平衡内存和性能。 - -**为什么 key 过期之后不立马把它删掉呢?这样不是会浪费很多内存空间吗?** - -因为不太好办到,或者说这种删除方式的成本太高了。假如我们使用延迟队列作为删除策略,这样存在下面这些问题: - -1. 队列本身的开销可能很大:key 多的情况下,一个延迟队列可能无法容纳。 -2. 维护延迟队列太麻烦:修改 key 的过期时间就需要调整期在延迟队列中的位置,并且还需要引入并发控制。 - -### 大量 key 集中过期怎么办? - -当 Redis 中存在大量 key 在同一时间点集中过期时,可能会导致以下问题: - -- **请求延迟增加**:Redis 在处理过期 key 时需要消耗 CPU 资源,如果过期 key 数量庞大,会导致 Redis 实例的 CPU 占用率升高,进而影响其他请求的处理速度,造成延迟增加。 -- **内存占用过高**:过期的 key 虽然已经失效,但在 Redis 真正删除它们之前,仍然会占用内存空间。如果过期 key 没有及时清理,可能会导致内存占用过高,甚至引发内存溢出。 - -为了避免这些问题,可以采取以下方案: - -1. **尽量避免 key 集中过期**:在设置键的过期时间时尽量随机一点。 -2. **开启 lazy free 机制**:修改 `redis.conf` 配置文件,将 `lazyfree-lazy-expire` 参数设置为 `yes`,即可开启 lazy free 机制。开启 lazy free 机制后,Redis 会在后台异步删除过期的 key,不会阻塞主线程的运行,从而降低对 Redis 性能的影响。 - -### Redis 内存淘汰策略了解么? - -> 相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据? - -Redis 的内存淘汰策略只有在运行内存达到了配置的最大内存阈值时才会触发,这个阈值是通过 `redis.conf` 的 `maxmemory` 参数来定义的。64 位操作系统下,`maxmemory` 默认为 0,表示不限制内存大小。32 位操作系统下,默认的最大内存值是 3GB。 - -你可以使用命令 `config get maxmemory` 来查看 `maxmemory` 的值。 - -```bash -> config get maxmemory -maxmemory -0 -``` - -Redis 提供了 6 种内存淘汰策略: - -1. **volatile-lru(least recently used)**:从已设置过期时间的数据集(`server.db[i].expires`)中挑选最近最少使用的数据淘汰。 -2. **volatile-ttl**:从已设置过期时间的数据集(`server.db[i].expires`)中挑选将要过期的数据淘汰。 -3. **volatile-random**:从已设置过期时间的数据集(`server.db[i].expires`)中任意选择数据淘汰。 -4. **allkeys-lru(least recently used)**:从数据集(`server.db[i].dict`)中移除最近最少使用的数据淘汰。 -5. **allkeys-random**:从数据集(`server.db[i].dict`)中任意选择数据淘汰。 -6. **no-eviction**(默认内存淘汰策略):禁止驱逐数据,当内存不足以容纳新写入数据时,新写入操作会报错。 - -4.0 版本后增加以下两种: - -7. **volatile-lfu(least frequently used)**:从已设置过期时间的数据集(`server.db[i].expires`)中挑选最不经常使用的数据淘汰。 -8. **allkeys-lfu(least frequently used)**:从数据集(`server.db[i].dict`)中移除最不经常使用的数据淘汰。 - -`allkeys-xxx` 表示从所有的键值中淘汰数据,而 `volatile-xxx` 表示从设置了过期时间的键值中淘汰数据。 - -`config.c` 中定义了内存淘汰策略的枚举数组: - -```c -configEnum maxmemory_policy_enum[] = { - {"volatile-lru", MAXMEMORY_VOLATILE_LRU}, - {"volatile-lfu", MAXMEMORY_VOLATILE_LFU}, - {"volatile-random",MAXMEMORY_VOLATILE_RANDOM}, - {"volatile-ttl",MAXMEMORY_VOLATILE_TTL}, - {"allkeys-lru",MAXMEMORY_ALLKEYS_LRU}, - {"allkeys-lfu",MAXMEMORY_ALLKEYS_LFU}, - {"allkeys-random",MAXMEMORY_ALLKEYS_RANDOM}, - {"noeviction",MAXMEMORY_NO_EVICTION}, - {NULL, 0} -}; -``` - -你可以使用 `config get maxmemory-policy` 命令来查看当前 Redis 的内存淘汰策略。 - -```bash -> config get maxmemory-policy -maxmemory-policy -noeviction -``` - -可以通过 `config set maxmemory-policy 内存淘汰策略` 命令修改内存淘汰策略,立即生效,但这种方式重启 Redis 之后就失效了。修改 `redis.conf` 中的 `maxmemory-policy` 参数不会因为重启而失效,不过,需要重启之后修改才能生效。 - -```properties -maxmemory-policy noeviction -``` - -关于淘汰策略的详细说明可以参考 Redis 官方文档:。 - -## 参考 - -- 《Redis 开发与运维》 -- 《Redis 设计与实现》 -- 《Redis 核心原理与实战》 -- Redis 命令手册: -- RedisSearch 终极使用指南,你值得拥有!: -- WHY Redis choose single thread (vs multi threads): [https://medium.com/@jychen7/sharing-redis-single-thread-vs-multi-threads-5870bd44d153](https://medium.com/@jychen7/sharing-redis-single-thread-vs-multi-threads-5870bd44d153) - - +If you are familiar with other solutions and can explain why you ultimately chose Redis (even better!), this will significantly enhance your interview performance diff --git a/docs/database/redis/redis-questions-02.md b/docs/database/redis/redis-questions-02.md index 37ef43ea72a..9a391821bf5 100644 --- a/docs/database/redis/redis-questions-02.md +++ b/docs/database/redis/redis-questions-02.md @@ -1,34 +1,34 @@ --- -title: Redis常见面试题总结(下) -category: 数据库 +title: Summary of Common Redis Interview Questions (Part 2) +category: Database tag: - Redis head: - - - meta - - name: keywords - content: Redis基础,Redis常见数据结构,Redis线程模型,Redis内存管理,Redis事务,Redis性能优化 - - - meta - - name: description - content: 一篇文章总结Redis常见的知识点和面试题,涵盖Redis基础、Redis常见数据结构、Redis线程模型、Redis内存管理、Redis事务、Redis性能优化等内容。 + - - meta + - name: keywords + content: Redis Basics, Common Redis Data Structures, Redis Thread Model, Redis Memory Management, Redis Transactions, Redis Performance Optimization + - - meta + - name: description + content: An article summarizing common knowledge points and interview questions about Redis, covering Redis basics, common data structures, thread model, memory management, transactions, and performance optimization. --- -## Redis 事务 +## Redis Transactions -### 什么是 Redis 事务? +### What is a Redis Transaction? -你可以将 Redis 中的事务理解为:**Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。** +You can understand a transaction in Redis as: **Redis transactions provide a way to package multiple command requests together. Then, all the packaged commands are executed in order without being interrupted.** -Redis 事务实际开发中使用的非常少,功能比较鸡肋,不要将其和我们平时理解的关系型数据库的事务混淆了。 +In actual development, Redis transactions are used very rarely, and their functionality is somewhat limited. Do not confuse them with transactions in relational databases as we usually understand them. -除了不满足原子性和持久性之外,事务中的每条命令都会与 Redis 服务器进行网络交互,这是比较浪费资源的行为。明明一次批量执行多个命令就可以了,这种操作实在是看不懂。 +In addition to not satisfying atomicity and durability, each command in a transaction interacts with the Redis server over the network, which is a resource-wasting behavior. Clearly, multiple commands can be executed in a batch, so this operation is hard to understand. -因此,Redis 事务是不建议在日常开发中使用的。 +Therefore, Redis transactions are not recommended for use in daily development. -### 如何使用 Redis 事务? +### How to Use Redis Transactions? -Redis 可以通过 **`MULTI`、`EXEC`、`DISCARD` 和 `WATCH`** 等命令来实现事务(Transaction)功能。 +Redis can implement transaction functionality through commands like **`MULTI`, `EXEC`, `DISCARD`, and `WATCH`**. ```bash > MULTI @@ -42,15 +42,15 @@ QUEUED 2) "JavaGuide" ``` -[`MULTI`](https://redis.io/commands/multi) 命令后可以输入多个命令,Redis 不会立即执行这些命令,而是将它们放到队列,当调用了 [`EXEC`](https://redis.io/commands/exec) 命令后,再执行所有的命令。 +After the [`MULTI`](https://redis.io/commands/multi) command, you can input multiple commands. Redis will not execute these commands immediately but will queue them. When the [`EXEC`](https://redis.io/commands/exec) command is called, all commands are executed. -这个过程是这样的: +The process is as follows: -1. 开始事务(`MULTI`); -2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行); -3. 执行事务(`EXEC`)。 +1. Start the transaction (`MULTI`); +1. Queue commands (commands for batch operations on Redis are executed in a first-in-first-out (FIFO) order); +1. Execute the transaction (`EXEC`). -你也可以通过 [`DISCARD`](https://redis.io/commands/discard) 命令取消一个事务,它会清空事务队列中保存的所有命令。 +You can also cancel a transaction using the [`DISCARD`](https://redis.io/commands/discard) command, which will clear all commands saved in the transaction queue. ```bash > MULTI @@ -63,10 +63,10 @@ QUEUED OK ``` -你可以通过[`WATCH`](https://redis.io/commands/watch) 命令监听指定的 Key,当调用 `EXEC` 命令执行事务时,如果一个被 `WATCH` 命令监视的 Key 被 **其他客户端/Session** 修改的话,整个事务都不会被执行。 +You can use the [`WATCH`](https://redis.io/commands/watch) command to monitor a specified key. When the `EXEC` command is called to execute the transaction, if a key being monitored by the `WATCH` command is modified by **another client/session**, the entire transaction will not be executed. ```bash -# 客户端 1 +# Client 1 > SET PROJECT "RustGuide" OK > WATCH PROJECT @@ -76,21 +76,21 @@ OK > SET PROJECT "JavaGuide" QUEUED -# 客户端 2 -# 在客户端 1 执行 EXEC 命令提交事务之前修改 PROJECT 的值 +# Client 2 +# Modifies the value of PROJECT before Client 1 executes the EXEC command to submit the transaction > SET PROJECT "GoGuide" -# 客户端 1 -# 修改失败,因为 PROJECT 的值被客户端2修改了 +# Client 1 +# Modification fails because the value of PROJECT was changed by Client 2 > EXEC (nil) > GET PROJECT "GoGuide" ``` -不过,如果 **WATCH** 与 **事务** 在同一个 Session 里,并且被 **WATCH** 监视的 Key 被修改的操作发生在事务内部,这个事务是可以被执行成功的(相关 issue:[WATCH 命令碰到 MULTI 命令时的不同效果](https://github.com/Snailclimb/JavaGuide/issues/1714))。 +However, if **WATCH** and **transaction** are in the same session, and the operation that modifies the monitored key occurs within the transaction, the transaction can be successfully executed (related issue: [Different Effects of WATCH Command When Encountering MULTI Command](https://github.com/Snailclimb/JavaGuide/issues/1714)). -事务内部修改 WATCH 监视的 Key: +Modifying the WATCH monitored key inside the transaction: ```bash > SET PROJECT "JavaGuide" @@ -113,7 +113,7 @@ QUEUED "JavaGuide3" ``` -事务外部修改 WATCH 监视的 Key: +Modifying the WATCH monitored key outside the transaction: ```bash > SET PROJECT "JavaGuide" @@ -130,670 +130,10 @@ QUEUED (nil) ``` -Redis 官网相关介绍 [https://redis.io/topics/transactions](https://redis.io/topics/transactions) 如下: +The official Redis website provides related information [https://redis.io/topics/transactions](https://redis.io/topics/transactions) as follows: -![Redis 事务](https://oss.javaguide.cn/github/javaguide/database/redis/redis-transactions.png) +![Redis Transactions](https://oss.javaguide.cn/github/javaguide/database/redis/redis-transactions.png) -### Redis 事务支持原子性吗? +### Does Redis Transactions Support Atomicity? -Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性:**1. 原子性**,**2. 隔离性**,**3. 持久性**,**4. 一致性**。 - -1. **原子性(Atomicity)**:事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; -2. **隔离性(Isolation)**:并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; -3. **持久性(Durability)**:一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响; -4. **一致性(Consistency)**:执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的。 - -Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的。 - -Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。 - -![Redis 为什么不支持回滚](https://oss.javaguide.cn/github/javaguide/database/redis/redis-rollback.png) - -**相关 issue**: - -- [issue#452: 关于 Redis 事务不满足原子性的问题](https://github.com/Snailclimb/JavaGuide/issues/452)。 -- [Issue#491:关于 Redis 没有事务回滚?](https://github.com/Snailclimb/JavaGuide/issues/491)。 - -### Redis 事务支持持久性吗? - -Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式: - -- 快照(snapshotting,RDB); -- 只追加文件(append-only file,AOF); -- RDB 和 AOF 的混合持久化(Redis 4.0 新增)。 - -与 RDB 持久化相比,AOF 持久化的实时性更好。在 Redis 的配置文件中存在三种不同的 AOF 持久化方式(`fsync` 策略),它们分别是: - -```bash -appendfsync always #每次有数据修改发生时,都会调用fsync函数同步AOF文件,fsync完成后线程返回,这样会严重降低Redis的速度 -appendfsync everysec #每秒钟调用fsync函数同步一次AOF文件 -appendfsync no #让操作系统决定何时进行同步,一般为30秒一次 -``` - -AOF 持久化的 `fsync` 策略为 no、everysec 时都会存在数据丢失的情况。always 下可以基本是可以满足持久性要求的,但性能太差,实际开发过程中不会使用。 - -因此,Redis 事务的持久性也是没办法保证的。 - -### 如何解决 Redis 事务的缺陷? - -Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。 - -一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。 - -不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此,**严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。** - -如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。 - -另外,Redis 7.0 新增了 [Redis functions](https://redis.io/docs/manual/programmability/functions-intro/) 特性,你可以将 Redis functions 看作是比 Lua 更强大的脚本。 - -## Redis 性能优化(重要) - -除了下面介绍的内容之外,再推荐两篇不错的文章: - -- [你的 Redis 真的变慢了吗?性能优化如何做 - 阿里开发者](https://mp.weixin.qq.com/s/nNEuYw0NlYGhuKKKKoWfcQ)。 -- [Redis 常见阻塞原因总结 - JavaGuide](https://javaguide.cn/database/redis/redis-common-blocking-problems-summary.html)。 - -### 使用批量操作减少网络传输 - -一个 Redis 命令的执行可以简化为以下 4 步: - -1. 发送命令; -2. 命令排队; -3. 命令执行; -4. 返回结果。 - -其中,第 1 步和第 4 步耗费时间之和称为 **Round Trip Time(RTT,往返时间)**,也就是数据在网络上传输的时间。 - -使用批量操作可以减少网络传输次数,进而有效减小网络开销,大幅减少 RTT。 - -另外,除了能减少 RTT 之外,发送一次命令的 socket I/O 成本也比较高(涉及上下文切换,存在 `read()` 和 `write()` 系统调用),批量操作还可以减少 socket I/O 成本。这个在官方对 pipeline 的介绍中有提到:。 - -#### 原生批量操作命令 - -Redis 中有一些原生支持批量操作的命令,比如: - -- `MGET`(获取一个或多个指定 key 的值)、`MSET`(设置一个或多个指定 key 的值)、 -- `HMGET`(获取指定哈希表中一个或者多个指定字段的值)、`HMSET`(同时将一个或多个 field-value 对设置到指定哈希表中)、 -- `SADD`(向指定集合添加一个或多个元素) -- …… - -不过,在 Redis 官方提供的分片集群解决方案 Redis Cluster 下,使用这些原生批量操作命令可能会存在一些小问题需要解决。就比如说 `MGET` 无法保证所有的 key 都在同一个 **hash slot(哈希槽)** 上,`MGET`可能还是需要多次网络传输,原子操作也无法保证了。不过,相较于非批量操作,还是可以节省不少网络传输次数。 - -整个步骤的简化版如下(通常由 Redis 客户端实现,无需我们自己再手动实现): - -1. 找到 key 对应的所有 hash slot; -2. 分别向对应的 Redis 节点发起 `MGET` 请求获取数据; -3. 等待所有请求执行结束,重新组装结果数据,保持跟入参 key 的顺序一致,然后返回结果。 - -如果想要解决这个多次网络传输的问题,比较常用的办法是自己维护 key 与 slot 的关系。不过这样不太灵活,虽然带来了性能提升,但同样让系统复杂性提升。 - -> Redis Cluster 并没有使用一致性哈希,采用的是 **哈希槽分区**,每一个键值对都属于一个 **hash slot(哈希槽)**。当客户端发送命令请求的时候,需要先根据 key 通过上面的计算公式找到的对应的哈希槽,然后再查询哈希槽和节点的映射关系,即可找到目标 Redis 节点。 -> -> 我在 [Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html) 这篇文章中详细介绍了 Redis Cluster 这部分的内容,感兴趣地可以看看。 - -#### pipeline - -对于不支持批量操作的命令,我们可以利用 **pipeline(流水线)** 将一批 Redis 命令封装成一组,这些 Redis 命令会被一次性提交到 Redis 服务器,只需要一次网络传输。不过,需要注意控制一次批量操作的 **元素个数**(例如 500 以内,实际也和元素字节数有关),避免网络传输的数据量过大。 - -与 `MGET`、`MSET` 等原生批量操作命令一样,pipeline 同样在 Redis Cluster 上使用会存在一些小问题。原因类似,无法保证所有的 key 都在同一个 **hash slot(哈希槽)** 上。如果想要使用的话,客户端需要自己维护 key 与 slot 的关系。 - -原生批量操作命令和 pipeline 的是有区别的,使用的时候需要注意: - -- 原生批量操作命令是原子操作,pipeline 是非原子操作。 -- pipeline 可以打包不同的命令,原生批量操作命令不可以。 -- 原生批量操作命令是 Redis 服务端支持实现的,而 pipeline 需要服务端和客户端的共同实现。 - -顺带补充一下 pipeline 和 Redis 事务的对比: - -- 事务是原子操作,pipeline 是非原子操作。两个不同的事务不会同时运行,而 pipeline 可以同时以交错方式执行。 -- Redis 事务中每个命令都需要发送到服务端,而 Pipeline 只需要发送一次,请求次数更少。 - -> 事务可以看作是一个原子操作,但其实并不满足原子性。当我们提到 Redis 中的原子操作时,主要指的是这个操作(比如事务、Lua 脚本)不会被其他操作(比如其他事务、Lua 脚本)打扰,并不能完全保证这个操作中的所有写命令要么都执行要么都不执行。这主要也是因为 Redis 是不支持回滚操作。 - -![](https://oss.javaguide.cn/github/javaguide/database/redis/redis-pipeline-vs-transaction.png) - -另外,pipeline 不适用于执行顺序有依赖关系的一批命令。就比如说,你需要将前一个命令的结果给后续的命令使用,pipeline 就没办法满足你的需求了。对于这种需求,我们可以使用 **Lua 脚本**。 - -#### Lua 脚本 - -Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作一条命令执行,可以看作是 **原子操作**。也就是说,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰,这是 pipeline 所不具备的。 - -并且,Lua 脚本中支持一些简单的逻辑处理比如使用命令读取值并在 Lua 脚本中进行处理,这同样是 pipeline 所不具备的。 - -不过, Lua 脚本依然存在下面这些缺陷: - -- 如果 Lua 脚本运行时出错并中途结束,之后的操作不会进行,但是之前已经发生的写操作不会撤销,所以即使使用了 Lua 脚本,也不能实现类似数据库回滚的原子性。 -- Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 **hash slot(哈希槽)** 上。 - -### 大量 key 集中过期问题 - -我在前面提到过:对于过期 key,Redis 采用的是 **定期删除+惰性/懒汉式删除** 策略。 - -定期删除执行过程中,如果突然遇到大量过期 key 的话,客户端请求必须等待定期清理过期 key 任务线程执行完成,因为这个这个定期任务线程是在 Redis 主线程中执行的。这就导致客户端请求没办法被及时处理,响应速度会比较慢。 - -**如何解决呢?** 下面是两种常见的方法: - -1. 给 key 设置随机过期时间。 -2. 开启 lazy-free(惰性删除/延迟释放)。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。 - -个人建议不管是否开启 lazy-free,我们都尽量给 key 设置随机过期时间。 - -### Redis bigkey(大 Key) - -#### 什么是 bigkey? - -简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准: - -- String 类型的 value 超过 1MB -- 复合类型(List、Hash、Set、Sorted Set 等)的 value 包含的元素超过 5000 个(不过,对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。 - -![bigkey 判定标准](https://oss.javaguide.cn/github/javaguide/database/redis/bigkey-criterion.png) - -#### bigkey 是怎么产生的?有什么危害? - -bigkey 通常是由于下面这些原因产生的: - -- 程序设计不当,比如直接使用 String 类型存储较大的文件对应的二进制数据。 -- 对于业务的数据规模考虑不周到,比如使用集合类型的时候没有考虑到数据量的快速增长。 -- 未及时清理垃圾数据,比如哈希中冗余了大量的无用键值对。 - -bigkey 除了会消耗更多的内存空间和带宽,还会对性能造成比较大的影响。 - -在 [Redis 常见阻塞原因总结](./redis-common-blocking-problems-summary.md) 这篇文章中我们提到:大 key 还会造成阻塞问题。具体来说,主要体现在下面三个方面: - -1. 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。 -2. 网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。 -3. 工作线程阻塞:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。 - -大 key 造成的阻塞问题还会进一步影响到主从同步和集群扩容。 - -综上,大 key 带来的潜在问题是非常多的,我们应该尽量避免 Redis 中存在 bigkey。 - -#### 如何发现 bigkey? - -**1、使用 Redis 自带的 `--bigkeys` 参数来查找。** - -```bash -# redis-cli -p 6379 --bigkeys - -# Scanning the entire keyspace to find biggest keys as well as -# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec -# per 100 SCAN commands (not usually needed). - -[00.00%] Biggest string found so far '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28057c20"' with 4437 bytes -[00.00%] Biggest list found so far '"my-list"' with 17 items - --------- summary ------- - -Sampled 5 keys in the keyspace! -Total key length in bytes is 264 (avg len 52.80) - -Biggest list found '"my-list"' has 17 items -Biggest string found '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28057c20"' has 4437 bytes - -1 lists with 17 items (20.00% of keys, avg size 17.00) -0 hashs with 0 fields (00.00% of keys, avg size 0.00) -4 strings with 4831 bytes (80.00% of keys, avg size 1207.75) -0 streams with 0 entries (00.00% of keys, avg size 0.00) -0 sets with 0 members (00.00% of keys, avg size 0.00) -0 zsets with 0 members (00.00% of keys, avg size 0.00 -``` - -从这个命令的运行结果,我们可以看出:这个命令会扫描(Scan)Redis 中的所有 key,会对 Redis 的性能有一点影响。并且,这种方式只能找出每种数据结构 top 1 bigkey(占用内存最大的 String 数据类型,包含元素最多的复合数据类型)。然而,一个 key 的元素多并不代表占用内存也多,需要我们根据具体的业务情况来进一步判断。 - -在线上执行该命令时,为了降低对 Redis 的影响,需要指定 `-i` 参数控制扫描的频率。`redis-cli -p 6379 --bigkeys -i 3` 表示扫描过程中每次扫描后休息的时间间隔为 3 秒。 - -**2、使用 Redis 自带的 SCAN 命令** - -`SCAN` 命令可以按照一定的模式和数量返回匹配的 key。获取了 key 之后,可以利用 `STRLEN`、`HLEN`、`LLEN` 等命令返回其长度或成员数量。 - -| 数据结构 | 命令 | 复杂度 | 结果(对应 key) | -| ---------- | ------ | ------ | ------------------ | -| String | STRLEN | O(1) | 字符串值的长度 | -| Hash | HLEN | O(1) | 哈希表中字段的数量 | -| List | LLEN | O(1) | 列表元素数量 | -| Set | SCARD | O(1) | 集合元素数量 | -| Sorted Set | ZCARD | O(1) | 有序集合的元素数量 | - -对于集合类型还可以使用 `MEMORY USAGE` 命令(Redis 4.0+),这个命令会返回键值对占用的内存空间。 - -**3、借助开源工具分析 RDB 文件。** - -通过分析 RDB 文件来找出 big key。这种方案的前提是你的 Redis 采用的是 RDB 持久化。 - -网上有现成的代码/工具可以直接拿来使用: - -- [redis-rdb-tools](https://github.com/sripathikrishnan/redis-rdb-tools):Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具。 -- [rdb_bigkeys](https://github.com/weiyanwei412/rdb_bigkeys):Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好。 - -**4、借助公有云的 Redis 分析服务。** - -如果你用的是公有云的 Redis 服务的话,可以看看其是否提供了 key 分析功能(一般都提供了)。 - -这里以阿里云 Redis 为例说明,它支持 bigkey 实时分析、发现,文档地址:。 - -![阿里云Key分析](https://oss.javaguide.cn/github/javaguide/database/redis/aliyun-key-analysis.png) - -#### 如何处理 bigkey? - -bigkey 的常见处理以及优化办法如下(这些方法可以配合起来使用): - -- **分割 bigkey**:将一个 bigkey 分割为多个小 key。例如,将一个含有上万字段数量的 Hash 按照一定策略(比如二次哈希)拆分为多个 Hash。 -- **手动清理**:Redis 4.0+ 可以使用 `UNLINK` 命令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用 `SCAN` 命令结合 `DEL` 命令来分批次删除。 -- **采用合适的数据结构**:例如,文件二进制数据不使用 String 保存、使用 HyperLogLog 统计页面 UV、Bitmap 保存状态信息(0/1)。 -- **开启 lazy-free(惰性删除/延迟释放)**:lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。 - -### Redis hotkey(热 Key) - -#### 什么是 hotkey? - -如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 **hotkey(热 Key)**。例如在 Redis 实例的每秒处理请求达到 5000 次,而其中某个 key 的每秒访问量就高达 2000 次,那这个 key 就可以看作是 hotkey。 - -hotkey 出现的原因主要是某个热点数据访问量暴增,如重大的热搜事件、参与秒杀的商品。 - -#### hotkey 有什么危害? - -处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。此外,如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。 - -因此,hotkey 很可能成为系统性能的瓶颈点,需要单独对其进行优化,以确保系统的高可用性和稳定性。 - -#### 如何发现 hotkey? - -**1、使用 Redis 自带的 `--hotkeys` 参数来查找。** - -Redis 4.0.3 版本中新增了 `hotkeys` 参数,该参数能够返回所有 key 的被访问次数。 - -使用该方案的前提条件是 Redis Server 的 `maxmemory-policy` 参数设置为 LFU 算法,不然就会出现如下所示的错误。 - -```bash -# redis-cli -p 6379 --hotkeys - -# Scanning the entire keyspace to find hot keys as well as -# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec -# per 100 SCAN commands (not usually needed). - -Error: ERR An LFU maxmemory policy is not selected, access frequency not tracked. Please note that when switching between policies at runtime LRU and LFU data will take some time to adjust. -``` - -Redis 中有两种 LFU 算法: - -1. **volatile-lfu(least frequently used)**:从已设置过期时间的数据集(`server.db[i].expires`)中挑选最不经常使用的数据淘汰。 -2. **allkeys-lfu(least frequently used)**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。 - -以下是配置文件 `redis.conf` 中的示例: - -```properties -# 使用 volatile-lfu 策略 -maxmemory-policy volatile-lfu - -# 或者使用 allkeys-lfu 策略 -maxmemory-policy allkeys-lfu -``` - -需要注意的是,`hotkeys` 参数命令也会增加 Redis 实例的 CPU 和内存消耗(全局扫描),因此需要谨慎使用。 - -**2、使用 `MONITOR` 命令。** - -`MONITOR` 命令是 Redis 提供的一种实时查看 Redis 的所有操作的方式,可以用于临时监控 Redis 实例的操作情况,包括读写、删除等操作。 - -由于该命令对 Redis 性能的影响比较大,因此禁止长时间开启 `MONITOR`(生产环境中建议谨慎使用该命令)。 - -```bash -# redis-cli -127.0.0.1:6379> MONITOR -OK -1683638260.637378 [0 172.17.0.1:61516] "ping" -1683638267.144236 [0 172.17.0.1:61518] "smembers" "mySet" -1683638268.941863 [0 172.17.0.1:61518] "smembers" "mySet" -1683638269.551671 [0 172.17.0.1:61518] "smembers" "mySet" -1683638270.646256 [0 172.17.0.1:61516] "ping" -1683638270.849551 [0 172.17.0.1:61518] "smembers" "mySet" -1683638271.926945 [0 172.17.0.1:61518] "smembers" "mySet" -1683638274.276599 [0 172.17.0.1:61518] "smembers" "mySet2" -1683638276.327234 [0 172.17.0.1:61518] "smembers" "mySet" -``` - -在发生紧急情况时,我们可以选择在合适的时机短暂执行 `MONITOR` 命令并将输出重定向至文件,在关闭 `MONITOR` 命令后通过对文件中请求进行归类分析即可找出这段时间中的 hotkey。 - -**3、借助开源项目。** - -京东零售的 [hotkey](https://gitee.com/jd-platform-opensource/hotkey) 这个项目不光支持 hotkey 的发现,还支持 hotkey 的处理。 - -![京东零售开源的 hotkey](https://oss.javaguide.cn/github/javaguide/database/redis/jd-hotkey.png) - -**4、根据业务情况提前预估。** - -可以根据业务情况来预估一些 hotkey,比如参与秒杀活动的商品数据等。不过,我们无法预估所有 hotkey 的出现,比如突发的热点新闻事件等。 - -**5、业务代码中记录分析。** - -在业务代码中添加相应的逻辑对 key 的访问情况进行记录分析。不过,这种方式会让业务代码的复杂性增加,一般也不会采用。 - -**6、借助公有云的 Redis 分析服务。** - -如果你用的是公有云的 Redis 服务的话,可以看看其是否提供了 key 分析功能(一般都提供了)。 - -这里以阿里云 Redis 为例说明,它支持 hotkey 实时分析、发现,文档地址:。 - -![阿里云Key分析](https://oss.javaguide.cn/github/javaguide/database/redis/aliyun-key-analysis.png) - -#### 如何解决 hotkey? - -hotkey 的常见处理以及优化办法如下(这些方法可以配合起来使用): - -- **读写分离**:主节点处理写请求,从节点处理读请求。 -- **使用 Redis Cluster**:将热点数据分散存储在多个 Redis 节点上。 -- **二级缓存**:hotkey 采用二级缓存的方式进行处理,将 hotkey 存放一份到 JVM 本地内存中(可以用 Caffeine)。 - -除了这些方法之外,如果你使用的公有云的 Redis 服务话,还可以留意其提供的开箱即用的解决方案。 - -这里以阿里云 Redis 为例说明,它支持通过代理查询缓存功能(Proxy Query Cache)优化热点 Key 问题。 - -![通过阿里云的Proxy Query Cache优化热点Key问题](https://oss.javaguide.cn/github/javaguide/database/redis/aliyun-hotkey-proxy-query-cache.png) - -### 慢查询命令 - -#### 为什么会有慢查询命令? - -我们知道一个 Redis 命令的执行可以简化为以下 4 步: - -1. 发送命令; -2. 命令排队; -3. 命令执行; -4. 返回结果。 - -Redis 慢查询统计的是命令执行这一步骤的耗时,慢查询命令也就是那些命令执行时间较长的命令。 - -Redis 为什么会有慢查询命令呢? - -Redis 中的大部分命令都是 O(1) 时间复杂度,但也有少部分 O(n) 时间复杂度的命令,例如: - -- `KEYS *`:会返回所有符合规则的 key。 -- `HGETALL`:会返回一个 Hash 中所有的键值对。 -- `LRANGE`:会返回 List 中指定范围内的元素。 -- `SMEMBERS`:返回 Set 中的所有元素。 -- `SINTER`/`SUNION`/`SDIFF`:计算多个 Set 的交集/并集/差集。 -- …… - -由于这些命令时间复杂度是 O(n),有时候也会全表扫描,随着 n 的增大,执行耗时也会越长。不过, 这些命令并不是一定不能使用,但是需要明确 N 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。 - -除了这些 O(n) 时间复杂度的命令可能会导致慢查询之外,还有一些时间复杂度可能在 O(N) 以上的命令,例如: - -- `ZRANGE`/`ZREVRANGE`:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量,m 为返回的元素数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。 -- `ZREMRANGEBYRANK`/`ZREMRANGEBYSCORE`:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量,m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。 -- …… - -#### 如何找到慢查询命令? - -在 `redis.conf` 文件中,我们可以使用 `slowlog-log-slower-than` 参数设置耗时命令的阈值,并使用 `slowlog-max-len` 参数设置耗时命令的最大记录条数。 - -当 Redis 服务器检测到执行时间超过 `slowlog-log-slower-than` 阈值的命令时,就会将该命令记录在慢查询日志(slow log)中,这点和 MySQL 记录慢查询语句类似。当慢查询日志超过设定的最大记录条数之后,Redis 会把最早的执行命令依次舍弃。 - -⚠️注意:由于慢查询日志会占用一定内存空间,如果设置最大记录条数过大,可能会导致内存占用过高的问题。 - -`slowlog-log-slower-than` 和 `slowlog-max-len` 的默认配置如下(可以自行修改): - -```properties -# The following time is expressed in microseconds, so 1000000 is equivalent -# to one second. Note that a negative number disables the slow log, while -# a value of zero forces the logging of every command. -slowlog-log-slower-than 10000 - -# There is no limit to this length. Just be aware that it will consume memory. -# You can reclaim memory used by the slow log with SLOWLOG RESET. -slowlog-max-len 128 -``` - -除了修改配置文件之外,你也可以直接通过 `CONFIG` 命令直接设置: - -```bash -# 命令执行耗时超过 10000 微妙(即10毫秒)就会被记录 -CONFIG SET slowlog-log-slower-than 10000 -# 只保留最近 128 条耗时命令 -CONFIG SET slowlog-max-len 128 -``` - -获取慢查询日志的内容很简单,直接使用 `SLOWLOG GET` 命令即可。 - -```bash -127.0.0.1:6379> SLOWLOG GET #慢日志查询 - 1) 1) (integer) 5 - 2) (integer) 1684326682 - 3) (integer) 12000 - 4) 1) "KEYS" - 2) "*" - 5) "172.17.0.1:61152" - 6) "" - // ... -``` - -慢查询日志中的每个条目都由以下六个值组成: - -1. 唯一渐进的日志标识符。 -2. 处理记录命令的 Unix 时间戳。 -3. 执行所需的时间量,以微秒为单位。 -4. 组成命令参数的数组。 -5. 客户端 IP 地址和端口。 -6. 客户端名称。 - -`SLOWLOG GET` 命令默认返回最近 10 条的的慢查询命令,你也自己可以指定返回的慢查询命令的数量 `SLOWLOG GET N`。 - -下面是其他比较常用的慢查询相关的命令: - -```bash -# 返回慢查询命令的数量 -127.0.0.1:6379> SLOWLOG LEN -(integer) 128 -# 清空慢查询命令 -127.0.0.1:6379> SLOWLOG RESET -OK -``` - -### Redis 内存碎片 - -**相关问题**: - -1. 什么是内存碎片?为什么会有 Redis 内存碎片? -2. 如何清理 Redis 内存碎片? - -**参考答案**:[Redis 内存碎片详解](https://javaguide.cn/database/redis/redis-memory-fragmentation.html)。 - -## Redis 生产问题(重要) - -### 缓存穿透 - -#### 什么是缓存穿透? - -缓存穿透说简单点就是大量请求的 key 是不合理的,**根本不存在于缓存中,也不存在于数据库中**。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。 - -![缓存穿透](https://oss.javaguide.cn/github/javaguide/database/redis/redis-cache-penetration.png) - -举个例子:某个黑客故意制造一些非法的 key 发起大量请求,导致大量请求落到数据库,结果数据库上也没有查到对应的数据。也就是说这些请求最终都落到了数据库上,对数据库造成了巨大的压力。 - -#### 有哪些解决办法? - -最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。 - -**1)缓存无效 key** - -如果缓存和数据库都查不到某个 key 的数据,就写一个到 Redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086`。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点,比如 1 分钟。 - -另外,这里多说一嘴,一般情况下我们是这样设计 key 的:`表名:列名:主键名:主键值`。 - -如果用 Java 代码展示的话,差不多是下面这样的: - -```java -public Object getObjectInclNullById(Integer id) { - // 从缓存中获取数据 - Object cacheValue = cache.get(id); - // 缓存为空 - if (cacheValue == null) { - // 从数据库中获取 - Object storageValue = storage.get(key); - // 缓存空对象 - cache.set(key, storageValue); - // 如果存储数据为空,需要设置一个过期时间(300秒) - if (storageValue == null) { - // 必须设置过期时间,否则有被攻击的风险 - cache.expire(key, 60 * 5); - } - return storageValue; - } - return cacheValue; -} -``` - -**2)布隆过滤器** - -布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的 List、Map、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。 - -![Bloom Filter 的简单原理示意图](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/bloom-filter-simple-schematic-diagram.png) - -Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1(代表 false 或者 true),这也是 Bloom Filter 节省内存的核心所在。这样来算的话,申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 KB ≈ 122KB 的空间。 - -![位数组](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/bloom-filter-bit-table.png) - -具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。 - -加入布隆过滤器之后的缓存处理流程图如下: - -![加入布隆过滤器之后的缓存处理流程图](https://oss.javaguide.cn/github/javaguide/database/redis/redis-cache-penetration-bloom-filter.png) - -更多关于布隆过滤器的详细介绍可以看看我的这篇原创:[不了解布隆过滤器?一文给你整的明明白白!](https://javaguide.cn/cs-basics/data-structure/bloom-filter.html),强烈推荐。 - -**3)接口限流** - -根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制,例如将异常 IP 列入黑名单。 - -后面提到的缓存击穿和雪崩都可以配合接口限流来解决,毕竟这些问题的关键都是有很多请求落到了数据库上造成数据库压力过大。 - -限流的具体方案可以参考这篇文章:[服务限流详解](https://javaguide.cn/high-availability/limit-request.html)。 - -### 缓存击穿 - -#### 什么是缓存击穿? - -缓存击穿中,请求的 key 对应的是 **热点数据**,该数据 **存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)**。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。 - -![缓存击穿](https://oss.javaguide.cn/github/javaguide/database/redis/redis-cache-breakdown.png) - -举个例子:秒杀进行过程中,缓存中的某个秒杀商品的数据突然过期,这就导致瞬时大量对该商品的请求直接落到数据库上,对数据库造成了巨大的压力。 - -#### 有哪些解决办法? - -1. **永不过期**(不推荐):设置热点数据永不过期或者过期时间比较长。 -2. **提前预热**(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。 -3. **加锁**(看情况):在缓存失效后,通过设置互斥锁确保只有一个请求去查询数据库并更新缓存。 - -#### 缓存穿透和缓存击穿有什么区别? - -缓存穿透中,请求的 key 既不存在于缓存中,也不存在于数据库中。 - -缓存击穿中,请求的 key 对应的是 **热点数据** ,该数据 **存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)** 。 - -### 缓存雪崩 - -#### 什么是缓存雪崩? - -我发现缓存雪崩这名字起的有点意思,哈哈。 - -实际上,缓存雪崩描述的就是这样一个简单的场景:**缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。** 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。 - -另外,缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。 - -![缓存雪崩](https://oss.javaguide.cn/github/javaguide/database/redis/redis-cache-avalanche.png) - -举个例子:缓存中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。 - -#### 有哪些解决办法? - -**针对 Redis 服务不可用的情况**: - -1. **Redis 集群**:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。Redis Cluster 和 Redis Sentinel 是两种最常用的 Redis 集群实现方案,详细介绍可以参考:[Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html)。 -2. **多级缓存**:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。 - -**针对大量缓存同时失效的情况**: - -1. **设置随机失效时间**(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。 -2. **提前预热**(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间,比如秒杀场景下的数据在秒杀结束之前不过期。 -3. **持久缓存策略**(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。 - -#### 缓存预热如何实现? - -常见的缓存预热方式有两种: - -1. 使用定时任务,比如 xxl-job,来定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中。 -2. 使用消息队列,比如 Kafka,来异步地进行缓存预热,将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务消费消息队列中的数据,根据主键或者 ID 查询数据库并更新缓存。 - -#### 缓存雪崩和缓存击穿有什么区别? - -缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在于缓存中(通常是因为缓存中的那份数据已经过期)。 - -### 如何保证缓存和数据库数据的一致性? - -细说的话可以扯很多,但是我觉得其实没太大必要(小声 BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。 - -下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。 - -Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直接删除缓存。 - -如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说有两个解决方案: - -1. **缓存失效时间变短**(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。 -2. **增加缓存更新重试机制**(常用):如果缓存服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。不过,这里更适合引入消息队列实现异步重试,将删除缓存重试的消息投递到消息队列,然后由专门的消费者来重试,直到成功。虽然说多引入了一个消息队列,但其整体带来的收益还是要更高一些。 - -相关文章推荐:[缓存和数据库一致性问题,看这篇就够了 - 水滴与银弹](https://mp.weixin.qq.com/s?__biz=MzIyOTYxNDI5OA==&mid=2247487312&idx=1&sn=fa19566f5729d6598155b5c676eee62d&chksm=e8beb8e5dfc931f3e35655da9da0b61c79f2843101c130cf38996446975014f958a6481aacf1&scene=178&cur_album_id=1699766580538032128#rd)。 - -### 哪些情况可能会导致 Redis 阻塞? - -单独抽了一篇文章来总结可能会导致 Redis 阻塞的情况:[Redis 常见阻塞原因总结](https://javaguide.cn/database/redis/redis-common-blocking-problems-summary.html)。 - -## Redis 集群 - -**Redis Sentinel**: - -1. 什么是 Sentinel? 有什么用? -2. Sentinel 如何检测节点是否下线?主观下线与客观下线的区别? -3. Sentinel 是如何实现故障转移的? -4. 为什么建议部署多个 sentinel 节点(哨兵集群)? -5. Sentinel 如何选择出新的 master(选举机制)? -6. 如何从 Sentinel 集群中选择出 Leader? -7. Sentinel 可以防止脑裂吗? - -**Redis Cluster**: - -1. 为什么需要 Redis Cluster?解决了什么问题?有什么优势? -2. Redis Cluster 是如何分片的? -3. 为什么 Redis Cluster 的哈希槽是 16384 个? -4. 如何确定给定 key 的应该分布到哪个哈希槽中? -5. Redis Cluster 支持重新分配哈希槽吗? -6. Redis Cluster 扩容缩容期间可以提供服务吗? -7. Redis Cluster 中的节点是怎么进行通信的? - -**参考答案**:[Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html)。 - -## Redis 使用规范 - -实际使用 Redis 的过程中,我们尽量要准守一些常见的规范,比如: - -1. 使用连接池:避免频繁创建关闭客户端连接。 -2. 尽量不使用 O(n) 指令,使用 O(n) 命令时要关注 n 的数量:像 `KEYS *`、`HGETALL`、`LRANGE`、`SMEMBERS`、`SINTER`/`SUNION`/`SDIFF` 等 O(n) 命令并非不能使用,但是需要明确 n 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。 -3. 使用批量操作减少网络传输:原生批量操作命令(比如 `MGET`、`MSET` 等等)、pipeline、Lua 脚本。 -4. 尽量不使用 Redis 事务:Redis 事务实现的功能比较鸡肋,可以使用 Lua 脚本代替。 -5. 禁止长时间开启 monitor:对性能影响比较大。 -6. 控制 key 的生命周期:避免 Redis 中存放了太多不经常被访问的数据。 -7. …… - -相关文章推荐:[阿里云 Redis 开发规范](https://developer.aliyun.com/article/531067)。 - -## 参考 - -- 《Redis 开发与运维》 -- 《Redis 设计与实现》 -- Redis Transactions: -- What is Redis Pipeline: -- 一文详解 Redis 中 BigKey、HotKey 的发现与处理: -- Bigkey 问题的解决思路与方式探索: -- Redis 延迟问题全面排障指南: - - +Redis transactions are different from what we usually understand as transactions in relational databases. We know that transactions have four major characteristics: **1. Atomicity**, **2. Isolation**, **3. Durability**, diff --git a/docs/database/redis/redis-skiplist.md b/docs/database/redis/redis-skiplist.md index 1194f736374..e62a788c1c3 100644 --- a/docs/database/redis/redis-skiplist.md +++ b/docs/database/redis/redis-skiplist.md @@ -1,28 +1,27 @@ --- -title: Redis为什么用跳表实现有序集合 -category: 数据库 +title: Why Redis Uses Skip Lists to Implement Sorted Sets +category: Database tag: - Redis --- -## 前言 +## Introduction -近几年针对 Redis 面试时会涉及常见数据结构的底层设计,其中就有这么一道比较有意思的面试题:“Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+树?”。 +In recent years, common data structure design questions have been frequently asked in Redis interviews. One interesting interview question is: "Why does Redis use skip lists for its sorted sets instead of balanced trees, red-black trees, or B+ trees?" -本文就以这道大厂常问的面试题为切入点,带大家详细了解一下跳表这个数据结构。 +This article will use this commonly asked interview question as a starting point to provide a detailed understanding of the skip list data structure. -本文整体脉络如下图所示,笔者会从有序集合的基本使用到跳表的源码分析和实现,让你会对 Redis 的有序集合底层实现的跳表有着更深刻的理解和掌握。 +The overall structure of this article is illustrated in the following diagram. I will guide you from the basic usage of sorted sets to the source code analysis and implementation of skip lists, giving you a deeper understanding and mastery of the skip list used in the underlying implementation of Redis's sorted sets. ![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005468.png) -## 跳表在 Redis 中的运用 +## The Use of Skip Lists in Redis -这里我们需要先了解一下 Redis 用到跳表的数据结构有序集合的使用,Redis 有个比较常用的数据结构叫**有序集合(sorted set,简称 zset)**,正如其名它是一个可以保证有序且元素唯一的集合,所以它经常用于排行榜等需要进行统计排列的场景。 +First, we need to understand the usage of the skip list data structure in Redis's sorted sets. Redis has a commonly used data structure called **sorted set (zset)**. As the name suggests, it is a collection that guarantees order and uniqueness of elements, making it frequently used in scenarios like leaderboards that require statistical ranking. -这里我们通过命令行的形式演示一下排行榜的实现,可以看到笔者分别输入 3 名用户:**xiaoming**、**xiaohong**、**xiaowang**,它们的**score**分别是 60、80、60,最终按照成绩升级降序排列。 +Here, we will demonstrate the implementation of a leaderboard using command line. We can see that I entered three users: **xiaoming**, **xiaohong**, and **xiaowang**, with their **scores** being 60, 80, and 60, respectively, and they are finally ranked in descending order of their scores. ```bash - 127.0.0.1:6379> zadd rankList 60 xiaoming (integer) 1 127.0.0.1:6379> zadd rankList 80 xiaohong @@ -30,7 +29,7 @@ tag: 127.0.0.1:6379> zadd rankList 60 xiaowang (integer) 1 -# 返回有序集中指定区间内的成员,通过索引,分数从高到低 +# Return members in the specified range of the sorted set, indexed, with scores from high to low 127.0.0.1:6379> ZREVRANGE rankList 0 100 WITHSCORES 1) "xiaohong" 2) "80" @@ -40,734 +39,42 @@ tag: 6) "60" ``` -此时我们通过 `object` 指令查看 zset 的数据结构,可以看到当前有序集合存储的还是**ziplist(压缩列表)**。 +At this point, we can check the data structure of the zset using the `object` command, and we can see that the current sorted set is still stored as a **ziplist**. ```bash 127.0.0.1:6379> object encoding rankList "ziplist" ``` -因为设计者考虑到 Redis 数据存放于内存,为了节约宝贵的内存空间,在有序集合元素小于 64 字节且个数小于 128 的时候,会使用 ziplist,而这个阈值的默认值的设置就来自下面这两个配置项。 +The designer considered that Redis stores data in memory, and to save precious memory space, when the number of elements in the sorted set is less than 128 and each element is less than 64 bytes, it will use a ziplist. The default threshold for this setting comes from the following two configuration items. ```bash zset-max-ziplist-value 64 zset-max-ziplist-entries 128 ``` -一旦有序集合中的某个元素超出这两个其中的一个阈值它就会转为 **skiplist**(实际是 dict+skiplist,还会借用字典来提高获取指定元素的效率)。 +Once an element in the sorted set exceeds one of these two thresholds, it will switch to a **skiplist** (actually a dict + skiplist, which also uses a dictionary to improve the efficiency of retrieving specific elements). -我们不妨在添加一个大于 64 字节的元素,可以看到有序集合的底层存储转为 skiplist。 +Let's add an element greater than 64 bytes and see that the underlying storage of the sorted set switches to a skiplist. ```bash 127.0.0.1:6379> zadd rankList 90 yigemingzihuichaoguo64zijiedeyonghumingchengyongyuceshitiaobiaodeshijiyunyong (integer) 1 -# 超过阈值,转为跳表 +# Exceeds the threshold, switches to skiplist 127.0.0.1:6379> object encoding rankList "skiplist" ``` -也就是说,ZSet 有两种不同的实现,分别是 ziplist 和 skiplist,具体使用哪种结构进行存储的规则如下: - -- 当有序集合对象同时满足以下两个条件时,使用 ziplist: - 1. ZSet 保存的键值对数量少于 128 个; - 2. 每个元素的长度小于 64 字节。 -- 如果不满足上述两个条件,那么使用 skiplist 。 - -## 手写一个跳表 - -为了更好的回答上述问题以及更好的理解和掌握跳表,这里可以通过手写一个简单的跳表的形式来帮助读者理解跳表这个数据结构。 - -我们都知道有序链表在添加、查询、删除的平均时间复杂都都是 **O(n)** 即线性增长,所以一旦节点数量达到一定体量后其性能表现就会非常差劲。而跳表我们完全可以理解为在原始链表基础上,建立多级索引,通过多级索引检索定位将增删改查的时间复杂度变为 **O(log n)** 。 - -可能这里说的有些抽象,我们举个例子,以下图跳表为例,其原始链表存储按序存储 1-10,有 2 级索引,每级索引的索引个数都是基于下层元素个数的一半。 - -![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005436.png) - -假如我们需要查询元素 6,其工作流程如下: - -1. 从 2 级索引开始,先来到节点 4。 -2. 查看 4 的后继节点,是 8 的 2 级索引,这个值大于 6,说明 2 级索引后续的索引都是大于 6 的,没有再往后搜寻的必要,我们索引向下查找。 -3. 来到 4 的 1 级索引,比对其后继节点为 6,查找结束。 - -相较于原始有序链表需要 6 次,我们的跳表通过建立多级索引,我们只需两次就直接定位到了目标元素,其查寻的复杂度被直接优化为**O(log n)**。 - -![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005524.png) - -对应的添加也是一个道理,假如我们需要在这个有序集合中添加一个元素 7,那么我们就需要通过跳表找到**小于元素 7 的最大值**,也就是下图元素 6 的位置,将其插入到元素 6 的后面,让元素 6 的索引指向新插入的节点 7,其工作流程如下: - -1. 从 2 级索引开始定位到了元素 4 的索引。 -2. 查看索引 4 的后继索引为 8,索引向下推进。 -3. 来到 1 级索引,发现索引 4 后继索引为 6,小于插入元素 7,指针推进到索引 6 位置。 -4. 继续比较 6 的后继节点为索引 8,大于元素 7,索引继续向下。 -5. 最终我们来到 6 的原始节点,发现其后继节点为 7,指针没有继续向下的空间,自此我们可知元素 6 就是小于插入元素 7 的最大值,于是便将元素 7 插入。 - -![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005480.png) - -这里我们又面临一个问题,我们是否需要为元素 7 建立索引,索引多高合适? - -我们上文提到,理想情况是每一层索引是下一层元素个数的二分之一,假设我们的总共有 16 个元素,对应各级索引元素个数应该是: - -```bash -1. 一级索引:16/2=8 -2. 二级索引:8/2 =4 -3. 三级索引:4/2=2 -``` - -由此我们用数学归纳法可知: - -```bash -1. 一级索引:16/2=16/2^1=8 -2. 二级索引:8/2 => 16/2^2 =4 -3. 三级索引:4/2=>16/2^3=2 -``` - -假设元素个数为 n,那么对应 k 层索引的元素个数 r 计算公式为: - -```bash -r=n/2^k -``` - -同理我们再来推断以下索引的最大高度,一般来说最高级索引的元素个数为 2,我们设元素总个数为 n,索引高度为 h,代入上述公式可得: - -```bash -2= n/2^h -=> 2*2^h=n -=> 2^(h+1)=n -=> h+1=log2^n -=> h=log2^n -1 -``` - -而 Redis 又是内存数据库,我们假设元素最大个数是**65536**,我们把**65536**代入上述公式可知最大高度为 16。所以我们建议添加一个元素后为其建立的索引高度不超过 16。 - -因为我们要求尽可能保证每一个上级索引都是下级索引的一半,在实现高度生成算法时,我们可以这样设计: - -1. 跳表的高度计算从原始链表开始,即默认情况下插入的元素的高度为 1,代表没有索引,只有元素节点。 -2. 设计一个为插入元素生成节点索引高度 level 的方法。 -3. 进行一次随机运算,随机数值范围为 0-1 之间。 -4. 如果随机数大于 0.5 则为当前元素添加一级索引,自此我们保证生成一级索引的概率为 **50%** ,这也就保证了 1 级索引理想情况下只有一半的元素会生成索引。 -5. 同理后续每次随机算法得到的值大于 0.5 时,我们的索引高度就加 1,这样就可以保证节点生成的 2 级索引概率为 **25%** ,3 级索引为 **12.5%** …… - -我们回过头,上述插入 7 之后,我们通过随机算法得到 2,即要为其建立 1 级索引: - -![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005505.png) - -最后我们再来说说删除,假设我们这里要删除元素 10,我们必须定位到当前跳表**各层**元素小于 10 的最大值,索引执行步骤为: - -1. 2 级索引 4 的后继节点为 8,指针推进。 -2. 索引 8 无后继节点,该层无要删除的元素,指针直接向下。 -3. 1 级索引 8 后继节点为 10,说明 1 级索引 8 在进行删除时需要将自己的指针和 1 级索引 10 断开联系,将 10 删除。 -4. 1 级索引完成定位后,指针向下,后继节点为 9,指针推进。 -5. 9 的后继节点为 10,同理需要让其指向 null,将 10 删除。 - -![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005503.png) - -### 模板定义 - -有了整体的思路之后,我们可以开始实现一个跳表了,首先定义一下跳表中的节点**Node**,从上文的演示中可以看出每一个**Node**它都包含以下几个元素: - -1. 存储的**value**值。 -2. 后继节点的地址。 -3. 多级索引。 - -为了更方便统一管理**Node**后继节点地址和多级索引指向的元素地址,笔者在**Node**中设置了一个**forwards**数组,用于记录原始链表节点的后继节点和多级索引的后继节点指向。 - -以下图为例,我们**forwards**数组长度为 5,其中**索引 0**记录的是原始链表节点的后继节点地址,而其余自底向上表示从 1 级索引到 4 级索引的后继节点指向。 - -![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005347.png) - -于是我们的就有了这样一个代码定义,可以看出笔者对于数组的长度设置为固定的 16**(上文的推算最大高度建议是 16)**,默认**data**为-1,节点最大高度**maxLevel**初始化为 1,注意这个**maxLevel**的值代表原始链表加上索引的总高度。 - -```java -/** - * 跳表索引最大高度为16 - */ -private static final int MAX_LEVEL = 16; - -class Node { - private int data = -1; - private Node[] forwards = new Node[MAX_LEVEL]; - private int maxLevel = 0; - -} -``` - -### 元素添加 - -定义好节点之后,我们先实现以下元素的添加,添加元素时首先自然是设置**data**这一步我们直接根据将传入的**value**设置到**data**上即可。 - -然后就是高度**maxLevel**的设置 ,我们在上文也已经给出了思路,默认高度为 1,即只有一个原始链表节点,通过随机算法每次大于 0.5 索引高度加 1,由此我们得出高度计算的算法`randomLevel()`: - -```java -/** - * 理论来讲,一级索引中元素个数应该占原始数据的 50%,二级索引中元素个数占 25%,三级索引12.5% ,一直到最顶层。 - * 因为这里每一层的晋升概率是 50%。对于每一个新插入的节点,都需要调用 randomLevel 生成一个合理的层数。 - * 该 randomLevel 方法会随机生成 1~MAX_LEVEL 之间的数,且 : - * 50%的概率返回 1 - * 25%的概率返回 2 - * 12.5%的概率返回 3 ... - * @return - */ -private int randomLevel() { - int level = 1; - while (Math.random() > PROB && level < MAX_LEVEL) { - ++level; - } - return level; -} -``` - -然后再设置当前要插入的**Node**和**Node**索引的后继节点地址,这一步稍微复杂一点,我们假设当前节点的高度为 4,即 1 个节点加 3 个索引,所以我们创建一个长度为 4 的数组**maxOfMinArr** ,遍历各级索引节点中小于当前**value**的最大值。 - -假设我们要插入的**value**为 5,我们的数组查找结果当前节点的前驱节点和 1 级索引、2 级索引的前驱节点都为 4,三级索引为空。 - -![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005299.png) - -然后我们基于这个数组**maxOfMinArr** 定位到各级的后继节点,让插入的元素 5 指向这些后继节点,而**maxOfMinArr**指向 5,结果如下图: - -![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005369.png) - -转化成代码就是下面这个形式,是不是很简单呢?我们继续: - -```java -/** - * 默认情况下的高度为1,即只有自己一个节点 - */ -private int levelCount = 1; - -/** - * 跳表最底层的节点,即头节点 - */ -private Node h = new Node(); - -public void add(int value) { - - //随机生成高度 - int level = randomLevel(); - - Node newNode = new Node(); - newNode.data = value; - newNode.maxLevel = level; - - //创建一个node数组,用于记录小于当前value的最大值 - Node[] maxOfMinArr = new Node[level]; - //默认情况下指向头节点 - for (int i = 0; i < level; i++) { - maxOfMinArr[i] = h; - } - - //基于上述结果拿到当前节点的后继节点 - Node p = h; - for (int i = level - 1; i >= 0; i--) { - while (p.forwards[i] != null && p.forwards[i].data < value) { - p = p.forwards[i]; - } - maxOfMinArr[i] = p; - } - - //更新前驱节点的后继节点为当前节点newNode - for (int i = 0; i < level; i++) { - newNode.forwards[i] = maxOfMinArr[i].forwards[i]; - maxOfMinArr[i].forwards[i] = newNode; - } - - //如果当前newNode高度大于跳表最高高度则更新levelCount - if (levelCount < level) { - levelCount = level; - } - -} -``` - -### 元素查询 - -查询逻辑比较简单,从跳表最高级的索引开始定位找到小于要查的 value 的最大值,以下图为例,我们希望查找到节点 8: - -1. 跳表的 3 级索引首先找找到 5 的索引,5 的 3 级索引 **forwards[3]** 指向空,索引直接向下。 -2. 来到 5 的 2 级索引,其后继 **forwards[2]** 指向 8,继续向下。 -3. 5 的 1 级索引 **forwards[1]** 指向索引 6,继续向前。 -4. 索引 6 的 **forwards[1]** 指向索引 8,继续向下。 -5. 我们在原始节点向前找到节点 7。 -6. 节点 7 后续就是节点 8,继续向前为节点 8,无法继续向下,结束搜寻。 -7. 判断 7 的前驱,等于 8,查找结束。 - -![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005323.png) - -所以我们的代码实现也很上述步骤差不多,从最高级索引开始向前查找,如果不为空且小于要查找的值,则继续向前搜寻,遇到不小于的节点则继续向下,如此往复,直到得到当前跳表中小于查找值的最大节点,查看其前驱是否等于要查找的值: - -```java -public Node get(int value) { - Node p = h; - //找到小于value的最大值 - for (int i = levelCount - 1; i >= 0; i--) { - while (p.forwards[i] != null && p.forwards[i].data < value) { - p = p.forwards[i]; - } - } - //如果p的前驱节点等于value则直接返回 - if (p.forwards[0] != null && p.forwards[0].data == value) { - return p.forwards[0]; - } - - return null; -} -``` - -### 元素删除 - -最后是删除逻辑,需要查找各层级小于要删除节点的最大值,假设我们要删除 10: - -1. 3 级索引得到小于 10 的最大值为 5,继续向下。 -2. 2 级索引从索引 5 开始查找,发现小于 10 的最大值为 8,继续向下。 -3. 同理 1 级索引得到 8,继续向下。 -4. 原始节点找到 9。 -5. 从最高级索引开始,查看每个小于 10 的节点后继节点是否为 10,如果等于 10,则让这个节点指向 10 的后继节点,将节点 10 及其索引交由 GC 回收。 - -![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005350.png) - -```java -/** - * 删除 - * - * @param value - */ -public void delete(int value) { - Node p = h; - //找到各级节点小于value的最大值 - Node[] updateArr = new Node[levelCount]; - for (int i = levelCount - 1; i >= 0; i--) { - while (p.forwards[i] != null && p.forwards[i].data < value) { - p = p.forwards[i]; - } - updateArr[i] = p; - } - //查看原始层节点前驱是否等于value,若等于则说明存在要删除的值 - if (p.forwards[0] != null && p.forwards[0].data == value) { - //从最高级索引开始查看其前驱是否等于value,若等于则将当前节点指向value节点的后继节点 - for (int i = levelCount - 1; i >= 0; i--) { - if (updateArr[i].forwards[i] != null && updateArr[i].forwards[i].data == value) { - updateArr[i].forwards[i] = updateArr[i].forwards[i].forwards[i]; - } - } - } - - //从最高级开始查看是否有一级索引为空,若为空则层级减1 - while (levelCount > 1 && h.forwards[levelCount - 1] == null) { - levelCount--; - } - -} -``` - -### 完整代码以及测试 - -完整代码如下,读者可自行参阅: - -```java -public class SkipList { - - /** - * 跳表索引最大高度为16 - */ - private static final int MAX_LEVEL = 16; - - /** - * 每个节点添加一层索引高度的概率为二分之一 - */ - private static final float PROB = 0.5 f; - - /** - * 默认情况下的高度为1,即只有自己一个节点 - */ - private int levelCount = 1; - - /** - * 跳表最底层的节点,即头节点 - */ - private Node h = new Node(); - - public SkipList() {} - - public class Node { - private int data = -1; - /** - * - */ - private Node[] forwards = new Node[MAX_LEVEL]; - private int maxLevel = 0; - - @Override - public String toString() { - return "Node{" + - "data=" + data + - ", maxLevel=" + maxLevel + - '}'; - } - } - - public void add(int value) { - - //随机生成高度 - int level = randomLevel(); - - Node newNode = new Node(); - newNode.data = value; - newNode.maxLevel = level; - - //创建一个node数组,用于记录小于当前value的最大值 - Node[] maxOfMinArr = new Node[level]; - //默认情况下指向头节点 - for (int i = 0; i < level; i++) { - maxOfMinArr[i] = h; - } - - //基于上述结果拿到当前节点的后继节点 - Node p = h; - for (int i = level - 1; i >= 0; i--) { - while (p.forwards[i] != null && p.forwards[i].data < value) { - p = p.forwards[i]; - } - maxOfMinArr[i] = p; - } - - //更新前驱节点的后继节点为当前节点newNode - for (int i = 0; i < level; i++) { - newNode.forwards[i] = maxOfMinArr[i].forwards[i]; - maxOfMinArr[i].forwards[i] = newNode; - } - - //如果当前newNode高度大于跳表最高高度则更新levelCount - if (levelCount < level) { - levelCount = level; - } - - } - - /** - * 理论来讲,一级索引中元素个数应该占原始数据的 50%,二级索引中元素个数占 25%,三级索引12.5% ,一直到最顶层。 - * 因为这里每一层的晋升概率是 50%。对于每一个新插入的节点,都需要调用 randomLevel 生成一个合理的层数。 - * 该 randomLevel 方法会随机生成 1~MAX_LEVEL 之间的数,且 : - * 50%的概率返回 1 - * 25%的概率返回 2 - * 12.5%的概率返回 3 ... - * @return - */ - private int randomLevel() { - int level = 1; - while (Math.random() > PROB && level < MAX_LEVEL) { - ++level; - } - return level; - } - - public Node get(int value) { - Node p = h; - //找到小于value的最大值 - for (int i = levelCount - 1; i >= 0; i--) { - while (p.forwards[i] != null && p.forwards[i].data < value) { - p = p.forwards[i]; - } - } - //如果p的前驱节点等于value则直接返回 - if (p.forwards[0] != null && p.forwards[0].data == value) { - return p.forwards[0]; - } - - return null; - } - - /** - * 删除 - * - * @param value - */ - public void delete(int value) { - Node p = h; - //找到各级节点小于value的最大值 - Node[] updateArr = new Node[levelCount]; - for (int i = levelCount - 1; i >= 0; i--) { - while (p.forwards[i] != null && p.forwards[i].data < value) { - p = p.forwards[i]; - } - updateArr[i] = p; - } - //查看原始层节点前驱是否等于value,若等于则说明存在要删除的值 - if (p.forwards[0] != null && p.forwards[0].data == value) { - //从最高级索引开始查看其前驱是否等于value,若等于则将当前节点指向value节点的后继节点 - for (int i = levelCount - 1; i >= 0; i--) { - if (updateArr[i].forwards[i] != null && updateArr[i].forwards[i].data == value) { - updateArr[i].forwards[i] = updateArr[i].forwards[i].forwards[i]; - } - } - } - - //从最高级开始查看是否有一级索引为空,若为空则层级减1 - while (levelCount > 1 && h.forwards[levelCount - 1] == null) { - levelCount--; - } - - } - - public void printAll() { - Node p = h; - //基于最底层的非索引层进行遍历,只要后继节点不为空,则速速出当前节点,并移动到后继节点 - while (p.forwards[0] != null) { - System.out.println(p.forwards[0]); - p = p.forwards[0]; - } - - } - -} -``` - -对应测试代码和输出结果如下: - -```java -public static void main(String[] args) { - SkipList skipList = new SkipList(); - for (int i = 0; i < 24; i++) { - skipList.add(i); - } - - System.out.println("**********输出添加结果**********"); - skipList.printAll(); - - SkipList.Node node = skipList.get(22); - System.out.println("**********查询结果:" + node+" **********"); - - skipList.delete(22); - System.out.println("**********删除结果**********"); - skipList.printAll(); - - - } -``` - -输出结果: - -```bash -**********输出添加结果********** -Node{data=0, maxLevel=2} -Node{data=1, maxLevel=3} -Node{data=2, maxLevel=1} -Node{data=3, maxLevel=1} -Node{data=4, maxLevel=2} -Node{data=5, maxLevel=2} -Node{data=6, maxLevel=2} -Node{data=7, maxLevel=2} -Node{data=8, maxLevel=4} -Node{data=9, maxLevel=1} -Node{data=10, maxLevel=1} -Node{data=11, maxLevel=1} -Node{data=12, maxLevel=1} -Node{data=13, maxLevel=1} -Node{data=14, maxLevel=1} -Node{data=15, maxLevel=3} -Node{data=16, maxLevel=4} -Node{data=17, maxLevel=2} -Node{data=18, maxLevel=1} -Node{data=19, maxLevel=1} -Node{data=20, maxLevel=1} -Node{data=21, maxLevel=3} -Node{data=22, maxLevel=1} -Node{data=23, maxLevel=1} -**********查询结果:Node{data=22, maxLevel=1} ********** -**********删除结果********** -Node{data=0, maxLevel=2} -Node{data=1, maxLevel=3} -Node{data=2, maxLevel=1} -Node{data=3, maxLevel=1} -Node{data=4, maxLevel=2} -Node{data=5, maxLevel=2} -Node{data=6, maxLevel=2} -Node{data=7, maxLevel=2} -Node{data=8, maxLevel=4} -Node{data=9, maxLevel=1} -Node{data=10, maxLevel=1} -Node{data=11, maxLevel=1} -Node{data=12, maxLevel=1} -Node{data=13, maxLevel=1} -Node{data=14, maxLevel=1} -Node{data=15, maxLevel=3} -Node{data=16, maxLevel=4} -Node{data=17, maxLevel=2} -Node{data=18, maxLevel=1} -Node{data=19, maxLevel=1} -Node{data=20, maxLevel=1} -Node{data=21, maxLevel=3} -Node{data=23, maxLevel=1} -``` - -**Redis 跳表的特点**: - -1. 采用**双向链表**,不同于上面的示例,存在一个回退指针。主要用于简化操作,例如删除某个元素时,还需要找到该元素的前驱节点,使用回退指针会非常方便。 -2. `score` 值可以重复,如果 `score` 值一样,则按照 ele(节点存储的值,为 sds)字典排序 -3. Redis 跳跃表默认允许最大的层数是 32,被源码中 `ZSKIPLIST_MAXLEVEL` 定义。 - -## 和其余三种数据结构的比较 - -最后,我们再来回答一下文章开头的那道面试题: “Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+树?”。 - -### 平衡树 vs 跳表 - -先来说说它和平衡树的比较,平衡树我们又会称之为 **AVL 树**,是一个严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过 1,即平衡因子为范围为 `[-1,1]`)。平衡树的插入、删除和查询的时间复杂度和跳表一样都是 **O(log n)** 。 - -对于范围查询来说,它也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。 - -![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005312.png) - -跳表诞生的初衷就是为了克服平衡树的一些缺点,跳表的发明者在论文[《Skip lists: a probabilistic alternative to balanced trees》](https://15721.courses.cs.cmu.edu/spring2018/papers/08-oltpindexes1/pugh-skiplists-cacm1990.pdf)中有详细提到: - -![](https://oss.javaguide.cn/github/javaguide/database/redis/skiplist-a-probabilistic-alternative-to-balanced-trees.png) - -> Skip lists are a data structure that can be used in place of balanced trees. Skip lists use probabilistic balancing rather than strictly enforced balancing and as a result the algorithms for insertion and deletion in skip lists are much simpler and significantly faster than equivalent algorithms for balanced trees. -> -> 跳表是一种可以用来代替平衡树的数据结构。跳表使用概率平衡而不是严格强制的平衡,因此,跳表中的插入和删除算法比平衡树的等效算法简单得多,速度也快得多。 - -笔者这里也贴出了 AVL 树插入操作的核心代码,可以看出每一次添加操作都需要进行一次递归定位插入位置,然后还需要根据回溯到根节点检查沿途的各层节点是否失衡,再通过旋转节点的方式进行调整。 - -```java -// 向二分搜索树中添加新的元素(key, value) -public void add(K key, V value) { - root = add(root, key, value); -} - -// 向以node为根的二分搜索树中插入元素(key, value),递归算法 -// 返回插入新节点后二分搜索树的根 -private Node add(Node node, K key, V value) { - - if (node == null) { - size++; - return new Node(key, value); - } - - if (key.compareTo(node.key) < 0) - node.left = add(node.left, key, value); - else if (key.compareTo(node.key) > 0) - node.right = add(node.right, key, value); - else // key.compareTo(node.key) == 0 - node.value = value; - - node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right)); - - int balanceFactor = getBalanceFactor(node); - - // LL型需要右旋 - if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) { - return rightRotate(node); - } - - //RR型失衡需要左旋 - if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) { - return leftRotate(node); - } - - //LR需要先左旋成LL型,然后再右旋 - if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) { - node.left = leftRotate(node.left); - return rightRotate(node); - } - - //RL - if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) { - node.right = rightRotate(node.right); - return leftRotate(node); - } - return node; -} -``` - -### 红黑树 vs 跳表 - -红黑树(Red Black Tree)也是一种自平衡二叉查找树,它的查询性能略微逊色于 AVL 树,但插入和删除效率更高。红黑树的插入、删除和查询的时间复杂度和跳表一样都是 **O(log n)** 。 - -红黑树是一个**黑平衡树**,即从任意节点到另外一个叶子叶子节点,它所经过的黑节点是一样的。当对它进行插入操作时,需要通过旋转和染色(红黑变换)来保证黑平衡。不过,相较于 AVL 树为了维持平衡的开销要小一些。关于红黑树的详细介绍,可以查看这篇文章:[红黑树](https://javaguide.cn/cs-basics/data-structure/red-black-tree.html)。 - -相比较于红黑树来说,跳表的实现也更简单一些。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。 - -![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005709.png) - -对应红黑树添加的核心代码如下,读者可自行参阅理解: - -```java -private Node < K, V > add(Node < K, V > node, K key, V val) { - - if (node == null) { - size++; - return new Node(key, val); - - } - - if (key.compareTo(node.key) < 0) { - node.left = add(node.left, key, val); - } else if (key.compareTo(node.key) > 0) { - node.right = add(node.right, key, val); - } else { - node.val = val; - } - - //左节点不为红,右节点为红,左旋 - if (isRed(node.right) && !isRed(node.left)) { - node = leftRotate(node); - } - - //左链右旋 - if (isRed(node.left) && isRed(node.left.left)) { - node = rightRotate(node); - } - - //颜色翻转 - if (isRed(node.left) && isRed(node.right)) { - flipColors(node); - } - - return node; -} -``` - -### B+树 vs 跳表 - -想必使用 MySQL 的读者都知道 B+树这个数据结构,B+树是一种常用的数据结构,具有以下特点: - -1. **多叉树结构**:它是一棵多叉树,每个节点可以包含多个子节点,减小了树的高度,查询效率高。 -2. **存储效率高**:其中非叶子节点存储多个 key,叶子节点存储 value,使得每个节点更够存储更多的键,根据索引进行范围查询时查询效率更高。- -3. **平衡性**:它是绝对的平衡,即树的各个分支高度相差不大,确保查询和插入时间复杂度为 **O(log n)** 。 -4. **顺序访问**:叶子节点间通过链表指针相连,范围查询表现出色。 -5. **数据均匀分布**:B+树插入时可能会导致数据重新分布,使得数据在整棵树分布更加均匀,保证范围查询和删除效率。 - -![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005649.png) - -所以,B+树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+树这种方式进行维护,只需按照概率进行随机维护即可,节约内存。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+树那样插入时发现失衡时还需要对节点分裂与合并。 - -### Redis 作者给出的理由 - -当然我们也可以通过 Redis 的作者自己给出的理由: - -> There are a few reasons: -> 1、They are not very memory intensive. It's up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees. -> 2、A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees. -> 3、They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code. - -翻译过来的意思就是: - -> 有几个原因: -> -> 1、它们不是很占用内存。这主要取决于你。改变节点拥有给定层数的概率的参数,会使它们比 B 树更节省内存。 -> -> 2、有序集合经常是许多 ZRANGE 或 ZREVRANGE 操作的目标,也就是说,以链表的方式遍历跳表。通过这种操作,跳表的缓存局部性至少和其他类型的平衡树一样好。 -> -> 3、它们更容易实现、调试等等。例如,由于跳表的简单性,我收到了一个补丁(已经在 Redis 主分支中),用增强的跳表实现了 O(log(N))的 ZRANK。它只需要对代码做很少的修改。 +In other words, ZSet has two different implementations: ziplist and skiplist. The specific rules for which structure to use for storage are as follows: -## 小结 +- Use ziplist when the sorted set object meets both of the following conditions: + 1. The number of key-value pairs in ZSet is less than 128. + 1. The length of each element is less than 64 bytes. +- If the above two conditions are not met, then use skiplist. -本文通过大量篇幅介绍跳表的工作原理和实现,帮助读者更进一步的熟悉跳表这一数据结构的优劣,最后再结合各个数据结构操作的特点进行比对,从而帮助读者更好的理解这道面试题,建议读者实现理解跳表时,尽可能配合执笔模拟来了解跳表的增删改查详细过程。 +## Implementing a Skip List -## 参考 +To better answer the above question and to understand and master skip lists, we can help readers understand this data structure by implementing a simple skip list. -- 为啥 redis 使用跳表(skiplist)而不是使用 red-black?: -- Skip List--跳表(全网最详细的跳表文章没有之一): -- Redis 对象与底层数据结构详解: -- Redis 有序集合(sorted set): -- 红黑树和跳表比较: -- 为什么 redis 的 zset 用跳跃表而不用 b+ tree?: +We all know that the average time complexity for adding, querying, and deleting in a sorted linked list is **O(n)**, which grows linearly. Therefore, once the number of nodes reaches a certain size, its performance will be very poor. A skip list can be understood as building multiple levels of indexes on top of the original linked list, allowing for a time complexity of **O(log n)** for search, insert diff --git a/docs/database/sql/sql-questions-01.md b/docs/database/sql/sql-questions-01.md index 4bf08f0fa0b..56510ad1b2c 100644 --- a/docs/database/sql/sql-questions-01.md +++ b/docs/database/sql/sql-questions-01.md @@ -1,20 +1,20 @@ --- -title: SQL常见面试题总结(1) -category: 数据库 +title: Summary of Common SQL Interview Questions (1) +category: Database tag: - - 数据库基础 + - Database Basics - SQL --- -> 题目来源于:[牛客题霸 - SQL 必知必会](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=298) +> Source of questions: [Niuke Question Bank - SQL Essentials](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=298) -## 检索数据 +## Data Retrieval -`SELECT` 用于从数据库中查询数据。 +`SELECT` is used to query data from the database. -### 从 Customers 表中检索所有的 ID +### Retrieve all IDs from the Customers table -现有表 `Customers` 如下: +The existing `Customers` table is as follows: | cust_id | | ------- | @@ -22,18 +22,18 @@ tag: | B | | C | -编写 SQL 语句,从 `Customers` 表中检索所有的 `cust_id`。 +Write an SQL statement to retrieve all `cust_id` from the `Customers` table. -答案: +Answer: ```sql SELECT cust_id FROM Customers ``` -### 检索并列出已订购产品的清单 +### Retrieve and list the ordered product list -表 `OrderItems` 含有非空的列 `prod_id` 代表商品 id,包含了所有已订购的商品(有些已被订购多次)。 +The `OrderItems` table contains a non-null column `prod_id` representing the product ID, which includes all ordered products (some may have been ordered multiple times). | prod_id | | ------- | @@ -45,20 +45,20 @@ FROM Customers | a6 | | a7 | -编写 SQL 语句,检索并列出所有已订购商品(`prod_id`)的去重后的清单。 +Write an SQL statement to retrieve and list all distinct ordered products (`prod_id`). -答案: +Answer: ```sql SELECT DISTINCT prod_id FROM OrderItems ``` -知识点:`DISTINCT` 用于返回列中的唯一不同值。 +Knowledge point: `DISTINCT` is used to return unique different values in a column. -### 检索所有列 +### Retrieve all columns -现在有 `Customers` 表(表中含有列 `cust_id` 代表客户 id,`cust_name` 代表客户姓名) +Now there is a `Customers` table (which contains the column `cust_id` representing customer ID and `cust_name` representing customer name) | cust_id | cust_name | | ------- | --------- | @@ -70,22 +70,22 @@ FROM OrderItems | a6 | lee | | a7 | hex | -需要编写 SQL 语句,检索所有列。 +You need to write an SQL statement to retrieve all columns. -答案: +Answer: ```sql SELECT cust_id, cust_name FROM Customers ``` -## 排序检索数据 +## Sorting Data Retrieval -`ORDER BY` 用于对结果集按照一个列或者多个列进行排序。默认按照升序对记录进行排序,如果需要按照降序对记录进行排序,可以使用 `DESC` 关键字。 +`ORDER BY` is used to sort the result set by one or more columns. By default, records are sorted in ascending order; to sort records in descending order, you can use the `DESC` keyword. -### 检索顾客名称并且排序 +### Retrieve customer names and sort -有表 `Customers`,`cust_id` 代表客户 id,`cust_name` 代表客户姓名。 +There is a `Customers` table, where `cust_id` represents customer ID and `cust_name` represents customer name. | cust_id | cust_name | | ------- | --------- | @@ -97,9 +97,9 @@ FROM Customers | a6 | lee | | a7 | hex | -从 `Customers` 中检索所有的顾客名称(`cust_name`),并按从 Z 到 A 的顺序显示结果。 +Retrieve all customer names (`cust_name`) from `Customers` and display the results in descending order from Z to A. -答案: +Answer: ```sql SELECT cust_name @@ -107,9 +107,9 @@ FROM Customers ORDER BY cust_name DESC ``` -### 对顾客 ID 和日期排序 +### Sort by customer ID and date -有 `Orders` 表: +There is an `Orders` table: | cust_id | order_num | order_date | | ------- | --------- | ------------------- | @@ -118,23 +118,23 @@ ORDER BY cust_name DESC | bob | cccc | 2021-01-10 12:00:00 | | dick | dddd | 2021-01-11 00:00:00 | -编写 SQL 语句,从 `Orders` 表中检索顾客 ID(`cust_id`)和订单号(`order_num`),并先按顾客 ID 对结果进行排序,再按订单日期倒序排列。 +Write an SQL statement to retrieve customer ID (`cust_id`) and order number (`order_num`) from the `Orders` table, first sorting the results by customer ID, then sorting by order date in descending order. -答案: +Answer: ```sql -# 根据列名排序 -# 注意:是 order_date 降序,而不是 order_num +# Sort by column name +# Note: order_date is in descending order, not order_num SELECT cust_id, order_num FROM Orders -ORDER BY cust_id,order_date DESC +ORDER BY cust_id, order_date DESC ``` -知识点:`order by` 对多列排序的时候,先排序的列放前面,后排序的列放后面。并且,不同的列可以有不同的排序规则。 +Knowledge point: When sorting multiple columns with `ORDER BY`, the first column to be sorted is placed first, and the subsequent columns follow. Different columns can have different sorting rules. -### 按照数量和价格排序 +### Sort by quantity and price -假设有一个 `OrderItems` 表: +Assuming there is an `OrderItems` table: | quantity | item_price | | -------- | ---------- | @@ -142,1687 +142,21 @@ ORDER BY cust_id,order_date DESC | 10 | 1003 | | 2 | 500 | -编写 SQL 语句,显示 `OrderItems` 表中的数量(`quantity`)和价格(`item_price`),并按数量由多到少、价格由高到低排序。 +Write an SQL statement to display the quantity (`quantity`) and price (`item_price`) from the `OrderItems` table, sorted by quantity in descending order and price in descending order. -答案: +Answer: ```sql SELECT quantity, item_price FROM OrderItems -ORDER BY quantity DESC,item_price DESC +ORDER BY quantity DESC, item_price DESC ``` -### 检查 SQL 语句 +### Check SQL statement -有 `Vendors` 表: +There is a `Vendors` table: | vend_name | | --------- | -| 海底捞 | -| 小龙坎 | -| 大龙燚 | - -下面的 SQL 语句有问题吗?尝试将它改正确,使之能够正确运行,并且返回结果根据`vend_name` 逆序排列。 - -```sql -SELECT vend_name, -FROM Vendors -ORDER vend_name DESC -``` - -改正后: - -```sql -SELECT vend_name -FROM Vendors -ORDER BY vend_name DESC -``` - -知识点: - -- 逗号作用是用来隔开列与列之间的。 -- ORDER BY 是有 BY 的,需要撰写完整,且位置正确。 - -## 过滤数据 - -`WHERE` 可以过滤返回的数据。 - -下面的运算符可以在 `WHERE` 子句中使用: - -| 运算符 | 描述 | -| :------ | :----------------------------------------------------------- | -| = | 等于 | -| <> | 不等于。 **注释:** 在 SQL 的一些版本中,该操作符可被写成 != | -| > | 大于 | -| < | 小于 | -| >= | 大于等于 | -| <= | 小于等于 | -| BETWEEN | 在某个范围内 | -| LIKE | 搜索某种模式 | -| IN | 指定针对某个列的多个可能值 | - -### 返回固定价格的产品 - -有表 `Products`: - -| prod_id | prod_name | prod_price | -| ------- | -------------- | ---------- | -| a0018 | sockets | 9.49 | -| a0019 | iphone13 | 600 | -| b0018 | gucci t-shirts | 1000 | - -【问题】从 `Products` 表中检索产品 ID(`prod_id`)和产品名称(`prod_name`),只返回价格为 9.49 美元的产品。 - -答案: - -```sql -SELECT prod_id, prod_name -FROM Products -WHERE prod_price = 9.49 -``` - -### 返回更高价格的产品 - -有表 `Products`: - -| prod_id | prod_name | prod_price | -| ------- | -------------- | ---------- | -| a0018 | sockets | 9.49 | -| a0019 | iphone13 | 600 | -| b0019 | gucci t-shirts | 1000 | - -【问题】编写 SQL 语句,从 `Products` 表中检索产品 ID(`prod_id`)和产品名称(`prod_name`),只返回价格为 9 美元或更高的产品。 - -答案: - -```sql -SELECT prod_id, prod_name -FROM Products -WHERE prod_price >= 9 -``` - -### 返回产品并且按照价格排序 - -有表 `Products`: - -| prod_id | prod_name | prod_price | -| ------- | --------- | ---------- | -| a0011 | egg | 3 | -| a0019 | sockets | 4 | -| b0019 | coffee | 15 | - -【问题】编写 SQL 语句,返回 `Products` 表中所有价格在 3 美元到 6 美元之间的产品的名称(`prod_name`)和价格(`prod_price`),然后按价格对结果进行排序。 - -答案: - -```sql -SELECT prod_name, prod_price -FROM Products -WHERE prod_price BETWEEN 3 AND 6 -ORDER BY prod_price - -# 或者 -SELECT prod_name, prod_price -FROM Products -WHERE prod_price >= 3 AND prod_price <= 6 -ORDER BY prod_price -``` - -### 返回更多的产品 - -`OrderItems` 表含有:订单号 `order_num`,`quantity`产品数量 - -| order_num | quantity | -| --------- | -------- | -| a1 | 105 | -| a2 | 1100 | -| a2 | 200 | -| a4 | 1121 | -| a5 | 10 | -| a2 | 19 | -| a7 | 5 | - -【问题】从 `OrderItems` 表中检索出所有不同且不重复的订单号(`order_num`),其中每个订单都要包含 100 个或更多的产品。 - -答案: - -```sql -SELECT order_num -FROM OrderItems -GROUP BY order_num -HAVING SUM(quantity) >= 100 -``` - -## 高级数据过滤 - -`AND` 和 `OR` 运算符用于基于一个以上的条件对记录进行过滤,两者可以结合使用。`AND` 必须 2 个条件都成立,`OR`只要 2 个条件中的一个成立即可。 - -### 检索供应商名称 - -`Vendors` 表有字段供应商名称(`vend_name`)、供应商国家(`vend_country`)、供应商州(`vend_state`) - -| vend_name | vend_country | vend_state | -| --------- | ------------ | ---------- | -| apple | USA | CA | -| vivo | CNA | shenzhen | -| huawei | CNA | xian | - -【问题】编写 SQL 语句,从 `Vendors` 表中检索供应商名称(`vend_name`),仅返回加利福尼亚州的供应商(这需要按国家[USA]和州[CA]进行过滤,没准其他国家也存在一个 CA) - -答案: - -```sql -SELECT vend_name -FROM Vendors -WHERE vend_country = 'USA' AND vend_state = 'CA' -``` - -### 检索并列出已订购产品的清单 - -`OrderItems` 表包含了所有已订购的产品(有些已被订购多次)。 - -| prod_id | order_num | quantity | -| ------- | --------- | -------- | -| BR01 | a1 | 105 | -| BR02 | a2 | 1100 | -| BR02 | a2 | 200 | -| BR03 | a4 | 1121 | -| BR017 | a5 | 10 | -| BR02 | a2 | 19 | -| BR017 | a7 | 5 | - -【问题】编写 SQL 语句,查找所有订购了数量至少 100 个的 `BR01`、`BR02` 或 `BR03` 的订单。你需要返回 `OrderItems` 表的订单号(`order_num`)、产品 ID(`prod_id`)和数量(`quantity`),并按产品 ID 和数量进行过滤。 - -答案: - -```sql -SELECT order_num, prod_id, quantity -FROM OrderItems -WHERE prod_id IN ('BR01', 'BR02', 'BR03') AND quantity >= 100 -``` - -### 返回所有价格在 3 美元到 6 美元之间的产品的名称和价格 - -有表 `Products`: - -| prod_id | prod_name | prod_price | -| ------- | --------- | ---------- | -| a0011 | egg | 3 | -| a0019 | sockets | 4 | -| b0019 | coffee | 15 | - -【问题】编写 SQL 语句,返回所有价格在 3 美元到 6 美元之间的产品的名称(`prod_name`)和价格(`prod_price`),使用 AND 操作符,然后按价格对结果进行升序排序。 - -答案: - -```sql -SELECT prod_name, prod_price -FROM Products -WHERE prod_price >= 3 and prod_price <= 6 -ORDER BY prod_price -``` - -### 检查 SQL 语句 - -供应商表 `Vendors` 有字段供应商名称 `vend_name`、供应商国家 `vend_country`、供应商省份 `vend_state` - -| vend_name | vend_country | vend_state | -| --------- | ------------ | ---------- | -| apple | USA | CA | -| vivo | CNA | shenzhen | -| huawei | CNA | xian | - -【问题】修改正确下面 sql,使之正确返回。 - -```sql -SELECT vend_name -FROM Vendors -ORDER BY vend_name -WHERE vend_country = 'USA' AND vend_state = 'CA'; -``` - -修改后: - -```sql -SELECT vend_name -FROM Vendors -WHERE vend_country = 'USA' AND vend_state = 'CA' -ORDER BY vend_name -``` - -`ORDER BY` 语句必须放在 `WHERE` 之后。 - -## 用通配符进行过滤 - -SQL 通配符必须与 `LIKE` 运算符一起使用 - -在 SQL 中,可使用以下通配符: - -| 通配符 | 描述 | -| :------------------------------- | :------------------------- | -| `%` | 代表零个或多个字符 | -| `_` | 仅替代一个字符 | -| `[charlist]` | 字符列中的任何单一字符 | -| `[^charlist]` 或者 `[!charlist]` | 不在字符列中的任何单一字符 | - -### 检索产品名称和描述(一) - -`Products` 表如下: - -| prod_name | prod_desc | -| --------- | -------------- | -| a0011 | usb | -| a0019 | iphone13 | -| b0019 | gucci t-shirts | -| c0019 | gucci toy | -| d0019 | lego toy | - -【问题】编写 SQL 语句,从 `Products` 表中检索产品名称(`prod_name`)和描述(`prod_desc`),仅返回描述中包含 `toy` 一词的产品名称。 - -答案: - -```sql -SELECT prod_name, prod_desc -FROM Products -WHERE prod_desc LIKE '%toy%' -``` - -### 检索产品名称和描述(二) - -`Products` 表如下: - -| prod_name | prod_desc | -| --------- | -------------- | -| a0011 | usb | -| a0019 | iphone13 | -| b0019 | gucci t-shirts | -| c0019 | gucci toy | -| d0019 | lego toy | - -【问题】编写 SQL 语句,从 `Products` 表中检索产品名称(`prod_name`)和描述(`prod_desc`),仅返回描述中未出现 `toy` 一词的产品,最后按”产品名称“对结果进行排序。 - -答案: - -```sql -SELECT prod_name, prod_desc -FROM Products -WHERE prod_desc NOT LIKE '%toy%' -ORDER BY prod_name -``` - -### 检索产品名称和描述(三) - -`Products` 表如下: - -| prod_name | prod_desc | -| --------- | ---------------- | -| a0011 | usb | -| a0019 | iphone13 | -| b0019 | gucci t-shirts | -| c0019 | gucci toy | -| d0019 | lego carrots toy | - -【问题】编写 SQL 语句,从 `Products` 表中检索产品名称(`prod_name`)和描述(`prod_desc`),仅返回描述中同时出现 `toy` 和 `carrots` 的产品。有好几种方法可以执行此操作,但对于这个挑战题,请使用 `AND` 和两个 `LIKE` 比较。 - -答案: - -```sql -SELECT prod_name, prod_desc -FROM Products -WHERE prod_desc LIKE '%toy%' AND prod_desc LIKE "%carrots%" -``` - -### 检索产品名称和描述(四) - -`Products` 表如下: - -| prod_name | prod_desc | -| --------- | ---------------- | -| a0011 | usb | -| a0019 | iphone13 | -| b0019 | gucci t-shirts | -| c0019 | gucci toy | -| d0019 | lego toy carrots | - -【问题】编写 SQL 语句,从 Products 表中检索产品名称(prod_name)和描述(prod_desc),仅返回在描述中以**先后顺序**同时出现 toy 和 carrots 的产品。提示:只需要用带有三个 `%` 符号的 `LIKE` 即可。 - -答案: - -```sql -SELECT prod_name, prod_desc -FROM Products -WHERE prod_desc LIKE '%toy%carrots%' -``` - -## 创建计算字段 - -### 别名 - -别名的常见用法是在检索出的结果中重命名表的列字段(为了符合特定的报表要求或客户需求)。有表 `Vendors` 代表供应商信息,`vend_id` 供应商 id、`vend_name` 供应商名称、`vend_address` 供应商地址、`vend_city` 供应商城市。 - -| vend_id | vend_name | vend_address | vend_city | -| ------- | ------------- | ------------ | --------- | -| a001 | tencent cloud | address1 | shenzhen | -| a002 | huawei cloud | address2 | dongguan | -| a003 | aliyun cloud | address3 | hangzhou | -| a003 | netease cloud | address4 | guangzhou | - -【问题】编写 SQL 语句,从 `Vendors` 表中检索 `vend_id`、`vend_name`、`vend_address` 和 `vend_city`,将 `vend_name` 重命名为 `vname`,将 `vend_city` 重命名为 `vcity`,将 `vend_address` 重命名为 `vaddress`,按供应商名称对结果进行升序排序。 - -答案: - -```sql -SELECT vend_id, vend_name AS vname, vend_address AS vaddress, vend_city AS vcity -FROM Vendors -ORDER BY vname -# as 可以省略 -SELECT vend_id, vend_name vname, vend_address vaddress, vend_city vcity -FROM Vendors -ORDER BY vname -``` - -### 打折 - -我们的示例商店正在进行打折促销,所有产品均降价 10%。`Products` 表包含 `prod_id` 产品 id、`prod_price` 产品价格。 - -【问题】编写 SQL 语句,从 `Products` 表中返回 `prod_id`、`prod_price` 和 `sale_price`。`sale_price` 是一个包含促销价格的计算字段。提示:可以乘以 0.9,得到原价的 90%(即 10%的折扣)。 - -答案: - -```sql -SELECT prod_id, prod_price, prod_price * 0.9 AS sale_price -FROM Products -``` - -注意:`sale_price` 是对计算结果的命名,而不是原有的列名。 - -## 使用函数处理数据 - -### 顾客登录名 - -我们的商店已经上线了,正在创建顾客账户。所有用户都需要登录名,默认登录名是其名称和所在城市的组合。 - -给出 `Customers` 表 如下: - -| cust_id | cust_name | cust_contact | cust_city | -| ------- | --------- | ------------ | --------- | -| a1 | Andy Li | Andy Li | Oak Park | -| a2 | Ben Liu | Ben Liu | Oak Park | -| a3 | Tony Dai | Tony Dai | Oak Park | -| a4 | Tom Chen | Tom Chen | Oak Park | -| a5 | An Li | An Li | Oak Park | -| a6 | Lee Chen | Lee Chen | Oak Park | -| a7 | Hex Liu | Hex Liu | Oak Park | - -【问题】编写 SQL 语句,返回顾客 ID(`cust_id`)、顾客名称(`cust_name`)和登录名(`user_login`),其中登录名全部为大写字母,并由顾客联系人的前两个字符(`cust_contact`)和其所在城市的前三个字符(`cust_city`)组成。提示:需要使用函数、拼接和别名。 - -答案: - -```sql -SELECT cust_id, cust_name, UPPER(CONCAT(SUBSTRING(cust_contact, 1, 2), SUBSTRING(cust_city, 1, 3))) AS user_login -FROM Customers -``` - -知识点: - -- 截取函数`SUBSTRING()`:截取字符串,`substring(str ,n ,m)`(n 表示起始截取位置,m 表示要截取的字符个数)表示返回字符串 str 从第 n 个字符开始截取 m 个字符; -- 拼接函数`CONCAT()`:将两个或多个字符串连接成一个字符串,select concat(A,B):连接字符串 A 和 B。 - -- 大写函数 `UPPER()`:将指定字符串转换为大写。 - -### 返回 2020 年 1 月的所有订单的订单号和订单日期 - -`Orders` 订单表如下: - -| order_num | order_date | -| --------- | ------------------- | -| a0001 | 2020-01-01 00:00:00 | -| a0002 | 2020-01-02 00:00:00 | -| a0003 | 2020-01-01 12:00:00 | -| a0004 | 2020-02-01 00:00:00 | -| a0005 | 2020-03-01 00:00:00 | - -【问题】编写 SQL 语句,返回 2020 年 1 月的所有订单的订单号(`order_num`)和订单日期(`order_date`),并按订单日期升序排序 - -答案: - -```sql -SELECT order_num, order_date -FROM Orders -WHERE month(order_date) = '01' AND YEAR(order_date) = '2020' -ORDER BY order_date -``` - -也可以用通配符来做: - -```sql -SELECT order_num, order_date -FROM Orders -WHERE order_date LIKE '2020-01%' -ORDER BY order_date -``` - -知识点: - -- 日期格式:`YYYY-MM-DD` -- 时间格式:`HH:MM:SS` - -日期和时间处理相关的常用函数: - -| 函 数 | 说 明 | -| --------------- | ------------------------------ | -| `ADDDATE()` | 增加一个日期(天、周等) | -| `ADDTIME()` | 增加一个时间(时、分等) | -| `CURDATE()` | 返回当前日期 | -| `CURTIME()` | 返回当前时间 | -| `DATE()` | 返回日期时间的日期部分 | -| `DATEDIFF` | 计算两个日期之差 | -| `DATE_FORMAT()` | 返回一个格式化的日期或时间串 | -| `DAY()` | 返回一个日期的天数部分 | -| `DAYOFWEEK()` | 对于一个日期,返回对应的星期几 | -| `HOUR()` | 返回一个时间的小时部分 | -| `MINUTE()` | 返回一个时间的分钟部分 | -| `MONTH()` | 返回一个日期的月份部分 | -| `NOW()` | 返回当前日期和时间 | -| `SECOND()` | 返回一个时间的秒部分 | -| `TIME()` | 返回一个日期时间的时间部分 | -| `YEAR()` | 返回一个日期的年份部分 | - -## 汇总数据 - -汇总数据相关的函数: - -| 函 数 | 说 明 | -| --------- | ---------------- | -| `AVG()` | 返回某列的平均值 | -| `COUNT()` | 返回某列的行数 | -| `MAX()` | 返回某列的最大值 | -| `MIN()` | 返回某列的最小值 | -| `SUM()` | 返回某列值之和 | - -### 确定已售出产品的总数 - -`OrderItems` 表代表售出的产品,`quantity` 代表售出商品数量。 - -| quantity | -| -------- | -| 10 | -| 100 | -| 1000 | -| 10001 | -| 2 | -| 15 | - -【问题】编写 SQL 语句,确定已售出产品的总数。 - -答案: - -```sql -SELECT Sum(quantity) AS items_ordered -FROM OrderItems -``` - -### 确定已售出产品项 BR01 的总数 - -`OrderItems` 表代表售出的产品,`quantity` 代表售出商品数量,产品项为 `prod_id`。 - -| quantity | prod_id | -| -------- | ------- | -| 10 | AR01 | -| 100 | AR10 | -| 1000 | BR01 | -| 10001 | BR010 | - -【问题】修改创建的语句,确定已售出产品项(`prod_id`)为"BR01"的总数。 - -答案: - -```sql -SELECT Sum(quantity) AS items_ordered -FROM OrderItems -WHERE prod_id = 'BR01' -``` - -### 确定 Products 表中价格不超过 10 美元的最贵产品的价格 - -`Products` 表如下,`prod_price` 代表商品的价格。 - -| prod_price | -| ---------- | -| 9.49 | -| 600 | -| 1000 | - -【问题】编写 SQL 语句,确定 `Products` 表中价格不超过 10 美元的最贵产品的价格(`prod_price`)。将计算所得的字段命名为 `max_price`。 - -答案: - -```sql -SELECT Max(prod_price) AS max_price -FROM Products -WHERE prod_price <= 10 -``` - -## 分组数据 - -`GROUP BY`: - -- `GROUP BY` 子句将记录分组到汇总行中。 -- `GROUP BY` 为每个组返回一个记录。 -- `GROUP BY` 通常还涉及聚合`COUNT`,`MAX`,`SUM`,`AVG` 等。 -- `GROUP BY` 可以按一列或多列进行分组。 -- `GROUP BY` 按分组字段进行排序后,`ORDER BY` 可以以汇总字段来进行排序。 - -`HAVING`: - -- `HAVING` 用于对汇总的 `GROUP BY` 结果进行过滤。 -- `HAVING` 必须要与 `GROUP BY` 连用。 -- `WHERE` 和 `HAVING` 可以在相同的查询中。 - -`HAVING` vs `WHERE`: - -- `WHERE`:过滤指定的行,后面不能加聚合函数(分组函数)。 -- `HAVING`:过滤分组,必须要与 `GROUP BY` 连用,不能单独使用。 - -### 返回每个订单号各有多少行数 - -`OrderItems` 表包含每个订单的每个产品 - -| order_num | -| --------- | -| a002 | -| a002 | -| a002 | -| a004 | -| a007 | - -【问题】编写 SQL 语句,返回每个订单号(`order_num`)各有多少行数(`order_lines`),并按 `order_lines` 对结果进行升序排序。 - -答案: - -```sql -SELECT order_num, Count(order_num) AS order_lines -FROM OrderItems -GROUP BY order_num -ORDER BY order_lines -``` - -知识点: - -1. `count(*)`,`count(列名)`都可以,区别在于,`count(列名)`是统计非 NULL 的行数; -2. `order by` 最后执行,所以可以使用列别名; -3. 分组聚合一定不要忘记加上 `group by` ,不然只会有一行结果。 - -### 每个供应商成本最低的产品 - -有 `Products` 表,含有字段 `prod_price` 代表产品价格,`vend_id` 代表供应商 id - -| vend_id | prod_price | -| ------- | ---------- | -| a0011 | 100 | -| a0019 | 0.1 | -| b0019 | 1000 | -| b0019 | 6980 | -| b0019 | 20 | - -【问题】编写 SQL 语句,返回名为 `cheapest_item` 的字段,该字段包含每个供应商成本最低的产品(使用 `Products` 表中的 `prod_price`),然后从最低成本到最高成本对结果进行升序排序。 - -答案: - -```sql -SELECT vend_id, Min(prod_price) AS cheapest_item -FROM Products -GROUP BY vend_id -ORDER BY cheapest_item -``` - -### 返回订单数量总和不小于 100 的所有订单的订单号 - -`OrderItems` 代表订单商品表,包括:订单号 `order_num` 和订单数量 `quantity`。 - -| order_num | quantity | -| --------- | -------- | -| a1 | 105 | -| a2 | 1100 | -| a2 | 200 | -| a4 | 1121 | -| a5 | 10 | -| a2 | 19 | -| a7 | 5 | - -【问题】请编写 SQL 语句,返回订单数量总和不小于 100 的所有订单号,最后结果按照订单号升序排序。 - -答案: - -```sql -# 直接聚合 -SELECT order_num -FROM OrderItems -GROUP BY order_num -HAVING Sum(quantity) >= 100 -ORDER BY order_num - -# 子查询 -SELECT a.order_num -FROM (SELECT order_num, Sum(quantity) AS sum_num - FROM OrderItems - GROUP BY order_num - HAVING sum_num >= 100) a -ORDER BY a.order_num -``` - -知识点: - -- `where`:过滤过滤指定的行,后面不能加聚合函数(分组函数)。 -- `having`:过滤分组,与 `group by` 连用,不能单独使用。 - -### 计算总和 - -`OrderItems` 表代表订单信息,包括字段:订单号 `order_num` 和 `item_price` 商品售出价格、`quantity` 商品数量。 - -| order_num | item_price | quantity | -| --------- | ---------- | -------- | -| a1 | 10 | 105 | -| a2 | 1 | 1100 | -| a2 | 1 | 200 | -| a4 | 2 | 1121 | -| a5 | 5 | 10 | -| a2 | 1 | 19 | -| a7 | 7 | 5 | - -【问题】编写 SQL 语句,根据订单号聚合,返回订单总价不小于 1000 的所有订单号,最后的结果按订单号进行升序排序。 - -提示:总价 = item_price 乘以 quantity - -答案: - -```sql -SELECT order_num, Sum(item_price * quantity) AS total_price -FROM OrderItems -GROUP BY order_num -HAVING total_price >= 1000 -ORDER BY order_num -``` - -### 检查 SQL 语句 - -`OrderItems` 表含有 `order_num` 订单号 - -| order_num | -| --------- | -| a002 | -| a002 | -| a002 | -| a004 | -| a007 | - -【问题】将下面代码修改正确后执行 - -```sql -SELECT order_num, COUNT(*) AS items -FROM OrderItems -GROUP BY items -HAVING COUNT(*) >= 3 -ORDER BY items, order_num; -``` - -修改后: - -```sql -SELECT order_num, COUNT(*) AS items -FROM OrderItems -GROUP BY order_num -HAVING items >= 3 -ORDER BY items, order_num; -``` - -## 使用子查询 - -子查询是嵌套在较大查询中的 SQL 查询,也称内部查询或内部选择,包含子查询的语句也称为外部查询或外部选择。简单来说,子查询就是指将一个 `SELECT` 查询(子查询)的结果作为另一个 SQL 语句(主查询)的数据来源或者判断条件。 - -子查询可以嵌入 `SELECT`、`INSERT`、`UPDATE` 和 `DELETE` 语句中,也可以和 `=`、`<`、`>`、`IN`、`BETWEEN`、`EXISTS` 等运算符一起使用。 - -子查询常用在 `WHERE` 子句和 `FROM` 子句后边: - -- 当用于 `WHERE` 子句时,根据不同的运算符,子查询可以返回单行单列、多行单列、单行多列数据。子查询就是要返回能够作为 WHERE 子句查询条件的值。 -- 当用于 `FROM` 子句时,一般返回多行多列数据,相当于返回一张临时表,这样才符合 `FROM` 后面是表的规则。这种做法能够实现多表联合查询。 - -> 注意:MySQL 数据库从 4.1 版本才开始支持子查询,早期版本是不支持的。 - -用于 `WHERE` 子句的子查询的基本语法如下: - -```sql -SELECT column_name [, column_name ] -FROM table1 [, table2 ] -WHERE column_name operator -(SELECT column_name [, column_name ] -FROM table1 [, table2 ] -[WHERE]) -``` - -- 子查询需要放在括号`( )`内。 -- `operator` 表示用于 `WHERE` 子句的运算符,可以是比较运算符(如 `=`, `<`, `>`, `<>` 等)或逻辑运算符(如 `IN`, `NOT IN`, `EXISTS`, `NOT EXISTS` 等),具体根据需求来确定。 - -用于 `FROM` 子句的子查询的基本语法如下: - -```sql -SELECT column_name [, column_name ] -FROM (SELECT column_name [, column_name ] - FROM table1 [, table2 ] - [WHERE]) AS temp_table_name [, ...] -[JOIN type JOIN table_name ON condition] -WHERE condition; -``` - -- 用于 `FROM` 的子查询返回的结果相当于一张临时表,所以需要使用 AS 关键字为该临时表起一个名字。 -- 子查询需要放在括号 `( )` 内。 -- 可以指定多个临时表名,并使用 `JOIN` 语句连接这些表。 - -### 返回购买价格为 10 美元或以上产品的顾客列表 - -`OrderItems` 表示订单商品表,含有字段订单号:`order_num`、订单价格:`item_price`;`Orders` 表代表订单信息表,含有顾客 `id:cust_id` 和订单号:`order_num` - -`OrderItems` 表: - -| order_num | item_price | -| --------- | ---------- | -| a1 | 10 | -| a2 | 1 | -| a2 | 1 | -| a4 | 2 | -| a5 | 5 | -| a2 | 1 | -| a7 | 7 | - -`Orders` 表: - -| order_num | cust_id | -| --------- | ------- | -| a1 | cust10 | -| a2 | cust1 | -| a2 | cust1 | -| a4 | cust2 | -| a5 | cust5 | -| a2 | cust1 | -| a7 | cust7 | - -【问题】使用子查询,返回购买价格为 10 美元或以上产品的顾客列表,结果无需排序。 - -答案: - -```sql -SELECT cust_id -FROM Orders -WHERE order_num IN (SELECT DISTINCT order_num - FROM OrderItems - where item_price >= 10) -``` - -### 确定哪些订单购买了 prod_id 为 BR01 的产品(一) - -表 `OrderItems` 代表订单商品信息表,`prod_id` 为产品 id;`Orders` 表代表订单表有 `cust_id` 代表顾客 id 和订单日期 `order_date` - -`OrderItems` 表: - -| prod_id | order_num | -| ------- | --------- | -| BR01 | a0001 | -| BR01 | a0002 | -| BR02 | a0003 | -| BR02 | a0013 | - -`Orders` 表: - -| order_num | cust_id | order_date | -| --------- | ------- | ------------------- | -| a0001 | cust10 | 2022-01-01 00:00:00 | -| a0002 | cust1 | 2022-01-01 00:01:00 | -| a0003 | cust1 | 2022-01-02 00:00:00 | -| a0013 | cust2 | 2022-01-01 00:20:00 | - -【问题】 - -编写 SQL 语句,使用子查询来确定哪些订单(在 `OrderItems` 中)购买了 `prod_id` 为 "BR01" 的产品,然后从 `Orders` 表中返回每个产品对应的顾客 ID(`cust_id`)和订单日期(`order_date`),按订购日期对结果进行升序排序。 - -答案: - -```sql -# 写法 1:子查询 -SELECT cust_id,order_date -FROM Orders -WHERE order_num IN - (SELECT order_num - FROM OrderItems - WHERE prod_id = 'BR01' ) -ORDER BY order_date; - -# 写法 2: 连接表 -SELECT b.cust_id, b.order_date -FROM OrderItems a,Orders b -WHERE a.order_num = b.order_num AND a.prod_id = 'BR01' -ORDER BY order_date -``` - -### 返回购买 prod_id 为 BR01 的产品的所有顾客的电子邮件(一) - -你想知道订购 BR01 产品的日期,有表 `OrderItems` 代表订单商品信息表,`prod_id` 为产品 id;`Orders` 表代表订单表有 `cust_id` 代表顾客 id 和订单日期 `order_date`;`Customers` 表含有 `cust_email` 顾客邮件和 `cust_id` 顾客 id - -`OrderItems` 表: - -| prod_id | order_num | -| ------- | --------- | -| BR01 | a0001 | -| BR01 | a0002 | -| BR02 | a0003 | -| BR02 | a0013 | - -`Orders` 表: - -| order_num | cust_id | order_date | -| --------- | ------- | ------------------- | -| a0001 | cust10 | 2022-01-01 00:00:00 | -| a0002 | cust1 | 2022-01-01 00:01:00 | -| a0003 | cust1 | 2022-01-02 00:00:00 | -| a0013 | cust2 | 2022-01-01 00:20:00 | - -`Customers` 表代表顾客信息,`cust_id` 为顾客 id,`cust_email` 为顾客 email - -| cust_id | cust_email | -| ------- | ----------------- | -| cust10 | | -| cust1 | | -| cust2 | | - -【问题】返回购买 `prod_id` 为 `BR01` 的产品的所有顾客的电子邮件(`Customers` 表中的 `cust_email`),结果无需排序。 - -提示:这涉及 `SELECT` 语句,最内层的从 `OrderItems` 表返回 `order_num`,中间的从 `Customers` 表返回 `cust_id`。 - -答案: - -```sql -# 写法 1:子查询 -SELECT cust_email -FROM Customers -WHERE cust_id IN (SELECT cust_id - FROM Orders - WHERE order_num IN (SELECT order_num - FROM OrderItems - WHERE prod_id = 'BR01')) - -# 写法 2: 连接表(inner join) -SELECT c.cust_email -FROM OrderItems a,Orders b,Customers c -WHERE a.order_num = b.order_num AND b.cust_id = c.cust_id AND a.prod_id = 'BR01' - -# 写法 3:连接表(left join) -SELECT c.cust_email -FROM Orders a LEFT JOIN - OrderItems b ON a.order_num = b.order_num LEFT JOIN - Customers c ON a.cust_id = c.cust_id -WHERE b.prod_id = 'BR01' -``` - -### 返回每个顾客不同订单的总金额 - -我们需要一个顾客 ID 列表,其中包含他们已订购的总金额。 - -`OrderItems` 表代表订单信息,`OrderItems` 表有订单号:`order_num` 和商品售出价格:`item_price`、商品数量:`quantity`。 - -| order_num | item_price | quantity | -| --------- | ---------- | -------- | -| a0001 | 10 | 105 | -| a0002 | 1 | 1100 | -| a0002 | 1 | 200 | -| a0013 | 2 | 1121 | -| a0003 | 5 | 10 | -| a0003 | 1 | 19 | -| a0003 | 7 | 5 | - -`Orders` 表订单号:`order_num`、顾客 id:`cust_id` - -| order_num | cust_id | -| --------- | ------- | -| a0001 | cust10 | -| a0002 | cust1 | -| a0003 | cust1 | -| a0013 | cust2 | - -【问题】 - -编写 SQL 语句,返回顾客 ID(`Orders` 表中的 `cust_id`),并使用子查询返回 `total_ordered` 以便返回每个顾客的订单总数,将结果按金额从大到小排序。 - -答案: - -```sql -# 写法 1:子查询 -SELECT o.cust_id, SUM(tb.total_ordered) AS `total_ordered` -FROM (SELECT order_num, SUM(item_price * quantity) AS total_ordered - FROM OrderItems - GROUP BY order_num) AS tb, - Orders o -WHERE tb.order_num = o.order_num -GROUP BY o.cust_id -ORDER BY total_ordered DESC; - -# 写法 2:连接表 -SELECT b.cust_id, Sum(a.quantity * a.item_price) AS total_ordered -FROM OrderItems a,Orders b -WHERE a.order_num = b.order_num -GROUP BY cust_id -ORDER BY total_ordered DESC -``` - -关于写法一详细介绍可以参考: [issue#2402:写法 1 存在的错误以及修改方法](https://github.com/Snailclimb/JavaGuide/issues/2402)。 - -### 从 Products 表中检索所有的产品名称以及对应的销售总数 - -`Products` 表中检索所有的产品名称:`prod_name`、产品 id:`prod_id` - -| prod_id | prod_name | -| ------- | --------- | -| a0001 | egg | -| a0002 | sockets | -| a0013 | coffee | -| a0003 | cola | - -`OrderItems` 代表订单商品表,订单产品:`prod_id`、售出数量:`quantity` - -| prod_id | quantity | -| ------- | -------- | -| a0001 | 105 | -| a0002 | 1100 | -| a0002 | 200 | -| a0013 | 1121 | -| a0003 | 10 | -| a0003 | 19 | -| a0003 | 5 | - -【问题】 - -编写 SQL 语句,从 `Products` 表中检索所有的产品名称(`prod_name`),以及名为 `quant_sold` 的计算列,其中包含所售产品的总数(在 `OrderItems` 表上使用子查询和 `SUM(quantity)` 检索)。 - -答案: - -```sql -# 写法 1:子查询 -SELECT p.prod_name, tb.quant_sold -FROM (SELECT prod_id, Sum(quantity) AS quant_sold - FROM OrderItems - GROUP BY prod_id) AS tb, - Products p -WHERE tb.prod_id = p.prod_id - -# 写法 2:连接表 -SELECT p.prod_name, Sum(o.quantity) AS quant_sold -FROM Products p, - OrderItems o -WHERE p.prod_id = o.prod_id -GROUP BY p.prod_name(这里不能用 p.prod_id,会报错) -``` - -## 连接表 - -JOIN 是“连接”的意思,顾名思义,SQL JOIN 子句用于将两个或者多个表联合起来进行查询。 - -连接表时需要在每个表中选择一个字段,并对这些字段的值进行比较,值相同的两条记录将合并为一条。**连接表的本质就是将不同表的记录合并起来,形成一张新表。当然,这张新表只是临时的,它仅存在于本次查询期间**。 - -使用 `JOIN` 连接两个表的基本语法如下: - -```sql -SELECT table1.column1, table2.column2... -FROM table1 -JOIN table2 -ON table1.common_column1 = table2.common_column2; -``` - -`table1.common_column1 = table2.common_column2` 是连接条件,只有满足此条件的记录才会合并为一行。您可以使用多个运算符来连接表,例如 =、>、<、<>、<=、>=、!=、`between`、`like` 或者 `not`,但是最常见的是使用 =。 - -当两个表中有同名的字段时,为了帮助数据库引擎区分是哪个表的字段,在书写同名字段名时需要加上表名。当然,如果书写的字段名在两个表中是唯一的,也可以不使用以上格式,只写字段名即可。 - -另外,如果两张表的关联字段名相同,也可以使用 `USING`子句来代替 `ON`,举个例子: - -```sql -# join....on -SELECT c.cust_name, o.order_num -FROM Customers c -INNER JOIN Orders o -ON c.cust_id = o.cust_id -ORDER BY c.cust_name - -# 如果两张表的关联字段名相同,也可以使用USING子句:JOIN....USING() -SELECT c.cust_name, o.order_num -FROM Customers c -INNER JOIN Orders o -USING(cust_id) -ORDER BY c.cust_name -``` - -**`ON` 和 `WHERE` 的区别**: - -- 连接表时,SQL 会根据连接条件生成一张新的临时表。`ON` 就是连接条件,它决定临时表的生成。 -- `WHERE` 是在临时表生成以后,再对临时表中的数据进行过滤,生成最终的结果集,这个时候已经没有 JOIN-ON 了。 - -所以总结来说就是:**SQL 先根据 ON 生成一张临时表,然后再根据 WHERE 对临时表进行筛选**。 - -SQL 允许在 `JOIN` 左边加上一些修饰性的关键词,从而形成不同类型的连接,如下表所示: - -| 连接类型 | 说明 | -| ---------------------------------------- | --------------------------------------------------------------------------------------------- | -| INNER JOIN 内连接 | (默认连接方式)只有当两个表都存在满足条件的记录时才会返回行。 | -| LEFT JOIN / LEFT OUTER JOIN 左(外)连接 | 返回左表中的所有行,即使右表中没有满足条件的行也是如此。 | -| RIGHT JOIN / RIGHT OUTER JOIN 右(外)连接 | 返回右表中的所有行,即使左表中没有满足条件的行也是如此。 | -| FULL JOIN / FULL OUTER JOIN 全(外)连接 | 只要其中有一个表存在满足条件的记录,就返回行。 | -| SELF JOIN | 将一个表连接到自身,就像该表是两个表一样。为了区分两个表,在 SQL 语句中需要至少重命名一个表。 | -| CROSS JOIN | 交叉连接,从两个或者多个连接表中返回记录集的笛卡尔积。 | - -下图展示了 LEFT JOIN、RIGHT JOIN、INNER JOIN、OUTER JOIN 相关的 7 种用法。 - -![](https://oss.javaguide.cn/github/javaguide/csdn/d1794312b448516831369f869814ab39.png) - -如果不加任何修饰词,只写 `JOIN`,那么默认为 `INNER JOIN` - -对于 `INNER JOIN` 来说,还有一种隐式的写法,称为 “**隐式内连接**”,也就是没有 `INNER JOIN` 关键字,使用 `WHERE` 语句实现内连接的功能 - -```sql -# 隐式内连接 -SELECT c.cust_name, o.order_num -FROM Customers c,Orders o -WHERE c.cust_id = o.cust_id -ORDER BY c.cust_name - -# 显式内连接 -SELECT c.cust_name, o.order_num -FROM Customers c -INNER JOIN Orders o -USING(cust_id) -ORDER BY c.cust_name; -``` - -### 返回顾客名称和相关订单号 - -`Customers` 表有字段顾客名称 `cust_name`、顾客 id `cust_id` - -| cust_id | cust_name | -| -------- | --------- | -| cust10 | andy | -| cust1 | ben | -| cust2 | tony | -| cust22 | tom | -| cust221 | an | -| cust2217 | hex | - -`Orders` 订单信息表,含有字段 `order_num` 订单号、`cust_id` 顾客 id - -| order_num | cust_id | -| --------- | -------- | -| a1 | cust10 | -| a2 | cust1 | -| a3 | cust2 | -| a4 | cust22 | -| a5 | cust221 | -| a7 | cust2217 | - -【问题】编写 SQL 语句,返回 `Customers` 表中的顾客名称(`cust_name`)和 `Orders` 表中的相关订单号(`order_num`),并按顾客名称再按订单号对结果进行升序排序。你可以尝试用两个不同的写法,一个使用简单的等连接语法,另外一个使用 INNER JOIN。 - -答案: - -```sql -# 隐式内连接 -SELECT c.cust_name, o.order_num -FROM Customers c,Orders o -WHERE c.cust_id = o.cust_id -ORDER BY c.cust_name,o.order_num - -# 显式内连接 -SELECT c.cust_name, o.order_num -FROM Customers c -INNER JOIN Orders o -USING(cust_id) -ORDER BY c.cust_name,o.order_num; -``` - -### 返回顾客名称和相关订单号以及每个订单的总价 - -`Customers` 表有字段,顾客名称:`cust_name`、顾客 id:`cust_id` - -| cust_id | cust_name | -| -------- | --------- | -| cust10 | andy | -| cust1 | ben | -| cust2 | tony | -| cust22 | tom | -| cust221 | an | -| cust2217 | hex | - -`Orders` 订单信息表,含有字段,订单号:`order_num`、顾客 id:`cust_id` - -| order_num | cust_id | -| --------- | -------- | -| a1 | cust10 | -| a2 | cust1 | -| a3 | cust2 | -| a4 | cust22 | -| a5 | cust221 | -| a7 | cust2217 | - -`OrderItems` 表有字段,商品订单号:`order_num`、商品数量:`quantity`、商品价格:`item_price` - -| order_num | quantity | item_price | -| --------- | -------- | ---------- | -| a1 | 1000 | 10 | -| a2 | 200 | 10 | -| a3 | 10 | 15 | -| a4 | 25 | 50 | -| a5 | 15 | 25 | -| a7 | 7 | 7 | - -【问题】除了返回顾客名称和订单号,返回 `Customers` 表中的顾客名称(`cust_name`)和 `Orders` 表中的相关订单号(`order_num`),添加第三列 `OrderTotal`,其中包含每个订单的总价,并按顾客名称再按订单号对结果进行升序排序。 - -```sql -# 简单的等连接语法 -SELECT c.cust_name, o.order_num, SUM(quantity * item_price) AS OrderTotal -FROM Customers c,Orders o,OrderItems oi -WHERE c.cust_id = o.cust_id AND o.order_num = oi.order_num -GROUP BY c.cust_name, o.order_num -ORDER BY c.cust_name, o.order_num -``` - -注意,可能有小伙伴会这样写: - -```sql -SELECT c.cust_name, o.order_num, SUM(quantity * item_price) AS OrderTotal -FROM Customers c,Orders o,OrderItems oi -WHERE c.cust_id = o.cust_id AND o.order_num = oi.order_num -GROUP BY c.cust_name -ORDER BY c.cust_name,o.order_num -``` - -这是错误的!只对 `cust_name` 进行聚类确实符合题意,但是不符合 `GROUP BY` 的语法。 - -select 语句中,如果没有 `GROUP BY` 语句,那么 `cust_name`、`order_num` 会返回若干个值,而 `sum(quantity * item_price)` 只返回一个值,通过 `group by` `cust_name` 可以让 `cust_name` 和 `sum(quantity * item_price)` 一一对应起来,或者说**聚类**,所以同样的,也要对 `order_num` 进行聚类。 - -> **一句话,select 中的字段要么都聚类,要么都不聚类** - -### 确定哪些订单购买了 prod_id 为 BR01 的产品(二) - -表 `OrderItems` 代表订单商品信息表,`prod_id` 为产品 id;`Orders` 表代表订单表有 `cust_id` 代表顾客 id 和订单日期 `order_date` - -`OrderItems` 表: - -| prod_id | order_num | -| ------- | --------- | -| BR01 | a0001 | -| BR01 | a0002 | -| BR02 | a0003 | -| BR02 | a0013 | - -`Orders` 表: - -| order_num | cust_id | order_date | -| --------- | ------- | ------------------- | -| a0001 | cust10 | 2022-01-01 00:00:00 | -| a0002 | cust1 | 2022-01-01 00:01:00 | -| a0003 | cust1 | 2022-01-02 00:00:00 | -| a0013 | cust2 | 2022-01-01 00:20:00 | - -【问题】 - -编写 SQL 语句,使用子查询来确定哪些订单(在 `OrderItems` 中)购买了 `prod_id` 为 "BR01" 的产品,然后从 `Orders` 表中返回每个产品对应的顾客 ID(`cust_id`)和订单日期(`order_date`),按订购日期对结果进行升序排序。 - -提示:这一次使用连接和简单的等连接语法。 - -```sql -# 写法 1:子查询 -SELECT cust_id, order_date -FROM Orders -WHERE order_num IN (SELECT order_num - FROM OrderItems - WHERE prod_id = 'BR01') -ORDER BY order_date - -# 写法 2:连接表 inner join -SELECT cust_id, order_date -FROM Orders o INNER JOIN - (SELECT order_num - FROM OrderItems - WHERE prod_id = 'BR01') tb ON o.order_num = tb.order_num -ORDER BY order_date - -# 写法 3:写法 2 的简化版 -SELECT cust_id, order_date -FROM Orders -INNER JOIN OrderItems USING(order_num) -WHERE OrderItems.prod_id = 'BR01' -ORDER BY order_date -``` - -### 返回购买 prod_id 为 BR01 的产品的所有顾客的电子邮件(二) - -有表 `OrderItems` 代表订单商品信息表,`prod_id` 为产品 id;`Orders` 表代表订单表有 `cust_id` 代表顾客 id 和订单日期 `order_date`;`Customers` 表含有 `cust_email` 顾客邮件和 cust_id 顾客 id - -`OrderItems` 表: - -| prod_id | order_num | -| ------- | --------- | -| BR01 | a0001 | -| BR01 | a0002 | -| BR02 | a0003 | -| BR02 | a0013 | - -`Orders` 表: - -| order_num | cust_id | order_date | -| --------- | ------- | ------------------- | -| a0001 | cust10 | 2022-01-01 00:00:00 | -| a0002 | cust1 | 2022-01-01 00:01:00 | -| a0003 | cust1 | 2022-01-02 00:00:00 | -| a0013 | cust2 | 2022-01-01 00:20:00 | - -`Customers` 表代表顾客信息,`cust_id` 为顾客 id,`cust_email` 为顾客 email - -| cust_id | cust_email | -| ------- | ----------------- | -| cust10 | | -| cust1 | | -| cust2 | | - -【问题】返回购买 `prod_id` 为 BR01 的产品的所有顾客的电子邮件(`Customers` 表中的 `cust_email`),结果无需排序。 - -提示:涉及到 `SELECT` 语句,最内层的从 `OrderItems` 表返回 `order_num`,中间的从 `Customers` 表返回 `cust_id`,但是必须使用 INNER JOIN 语法。 - -```sql -SELECT cust_email -FROM Customers -INNER JOIN Orders using(cust_id) -INNER JOIN OrderItems using(order_num) -WHERE OrderItems.prod_id = 'BR01' -``` - -### 确定最佳顾客的另一种方式(二) - -`OrderItems` 表代表订单信息,确定最佳顾客的另一种方式是看他们花了多少钱,`OrderItems` 表有订单号 `order_num` 和 `item_price` 商品售出价格、`quantity` 商品数量 - -| order_num | item_price | quantity | -| --------- | ---------- | -------- | -| a1 | 10 | 105 | -| a2 | 1 | 1100 | -| a2 | 1 | 200 | -| a4 | 2 | 1121 | -| a5 | 5 | 10 | -| a2 | 1 | 19 | -| a7 | 7 | 5 | - -`Orders` 表含有字段 `order_num` 订单号、`cust_id` 顾客 id - -| order_num | cust_id | -| --------- | -------- | -| a1 | cust10 | -| a2 | cust1 | -| a3 | cust2 | -| a4 | cust22 | -| a5 | cust221 | -| a7 | cust2217 | - -顾客表 `Customers` 有字段 `cust_id` 客户 id、`cust_name` 客户姓名 - -| cust_id | cust_name | -| -------- | --------- | -| cust10 | andy | -| cust1 | ben | -| cust2 | tony | -| cust22 | tom | -| cust221 | an | -| cust2217 | hex | - -【问题】编写 SQL 语句,返回订单总价不小于 1000 的客户名称和总额(`OrderItems` 表中的 `order_num`)。 - -提示:需要计算总和(`item_price` 乘以 `quantity`)。按总额对结果进行排序,请使用 `INNER JOIN`语法。 - -```sql -SELECT cust_name, SUM(item_price * quantity) AS total_price -FROM Customers -INNER JOIN Orders USING(cust_id) -INNER JOIN OrderItems USING(order_num) -GROUP BY cust_name -HAVING total_price >= 1000 -ORDER BY total_price -``` - -## 创建高级连接 - -### 检索每个顾客的名称和所有的订单号(一) - -`Customers` 表代表顾客信息含有顾客 id `cust_id` 和 顾客名称 `cust_name` - -| cust_id | cust_name | -| -------- | --------- | -| cust10 | andy | -| cust1 | ben | -| cust2 | tony | -| cust22 | tom | -| cust221 | an | -| cust2217 | hex | - -`Orders` 表代表订单信息含有订单号 `order_num` 和顾客 id `cust_id` - -| order_num | cust_id | -| --------- | -------- | -| a1 | cust10 | -| a2 | cust1 | -| a3 | cust2 | -| a4 | cust22 | -| a5 | cust221 | -| a7 | cust2217 | - -【问题】使用 INNER JOIN 编写 SQL 语句,检索每个顾客的名称(`Customers` 表中的 `cust_name`)和所有的订单号(`Orders` 表中的 `order_num`),最后根据顾客姓名 `cust_name` 升序返回。 - -```sql -SELECT cust_name, order_num -FROM Customers -INNER JOIN Orders -USING(cust_id) -ORDER BY cust_name -``` - -### 检索每个顾客的名称和所有的订单号(二) - -`Orders` 表代表订单信息含有订单号 `order_num` 和顾客 id `cust_id` - -| order_num | cust_id | -| --------- | -------- | -| a1 | cust10 | -| a2 | cust1 | -| a3 | cust2 | -| a4 | cust22 | -| a5 | cust221 | -| a7 | cust2217 | - -`Customers` 表代表顾客信息含有顾客 id `cust_id` 和 顾客名称 `cust_name` - -| cust_id | cust_name | -| -------- | --------- | -| cust10 | andy | -| cust1 | ben | -| cust2 | tony | -| cust22 | tom | -| cust221 | an | -| cust2217 | hex | -| cust40 | ace | - -【问题】检索每个顾客的名称(`Customers` 表中的 `cust_name`)和所有的订单号(Orders 表中的 `order_num`),列出所有的顾客,即使他们没有下过订单。最后根据顾客姓名 `cust_name` 升序返回。 - -```sql -SELECT cust_name, order_num -FROM Customers -LEFT JOIN Orders -USING(cust_id) -ORDER BY cust_name -``` - -### 返回产品名称和与之相关的订单号 - -`Products` 表为产品信息表含有字段 `prod_id` 产品 id、`prod_name` 产品名称 - -| prod_id | prod_name | -| ------- | --------- | -| a0001 | egg | -| a0002 | sockets | -| a0013 | coffee | -| a0003 | cola | -| a0023 | soda | - -`OrderItems` 表为订单信息表含有字段 `order_num` 订单号和产品 id `prod_id` - -| prod_id | order_num | -| ------- | --------- | -| a0001 | a105 | -| a0002 | a1100 | -| a0002 | a200 | -| a0013 | a1121 | -| a0003 | a10 | -| a0003 | a19 | -| a0003 | a5 | - -【问题】使用外连接(left join、 right join、full join)联结 `Products` 表和 `OrderItems` 表,返回产品名称(`prod_name`)和与之相关的订单号(`order_num`)的列表,并按照产品名称升序排序。 - -```sql -SELECT prod_name, order_num -FROM Products -LEFT JOIN OrderItems -USING(prod_id) -ORDER BY prod_name -``` - -### 返回产品名称和每一项产品的总订单数 - -`Products` 表为产品信息表含有字段 `prod_id` 产品 id、`prod_name` 产品名称 - -| prod_id | prod_name | -| ------- | --------- | -| a0001 | egg | -| a0002 | sockets | -| a0013 | coffee | -| a0003 | cola | -| a0023 | soda | - -`OrderItems` 表为订单信息表含有字段 `order_num` 订单号和产品 id `prod_id` - -| prod_id | order_num | -| ------- | --------- | -| a0001 | a105 | -| a0002 | a1100 | -| a0002 | a200 | -| a0013 | a1121 | -| a0003 | a10 | -| a0003 | a19 | -| a0003 | a5 | - -【问题】 - -使用 OUTER JOIN 联结 `Products` 表和 `OrderItems` 表,返回产品名称(`prod_name`)和每一项产品的总订单数(不是订单号),并按产品名称升序排序。 - -```sql -SELECT prod_name, COUNT(order_num) AS orders -FROM Products -LEFT JOIN OrderItems -USING(prod_id) -GROUP BY prod_name -ORDER BY prod_name -``` - -### 列出供应商及其可供产品的数量 - -有 `Vendors` 表含有 `vend_id` (供应商 id) - -| vend_id | -| ------- | -| a0002 | -| a0013 | -| a0003 | -| a0010 | - -有 `Products` 表含有 `vend_id`(供应商 id)和 prod_id(供应产品 id) - -| vend_id | prod_id | -| ------- | -------------------- | -| a0001 | egg | -| a0002 | prod_id_iphone | -| a00113 | prod_id_tea | -| a0003 | prod_id_vivo phone | -| a0010 | prod_id_huawei phone | - -【问题】列出供应商(`Vendors` 表中的 `vend_id`)及其可供产品的数量,包括没有产品的供应商。你需要使用 OUTER JOIN 和 COUNT()聚合函数来计算 `Products` 表中每种产品的数量,最后根据 vend_id 升序排序。 - -注意:`vend_id` 列会显示在多个表中,因此在每次引用它时都需要完全限定它。 - -```sql -SELECT v.vend_id, COUNT(prod_id) AS prod_id -FROM Vendors v -LEFT JOIN Products p -USING(vend_id) -GROUP BY v.vend_id -ORDER BY v.vend_id -``` - -## 组合查询 - -`UNION` 运算符将两个或更多查询的结果组合起来,并生成一个结果集,其中包含来自 `UNION` 中参与查询的提取行。 - -`UNION` 基本规则: - -- 所有查询的列数和列顺序必须相同。 -- 每个查询中涉及表的列的数据类型必须相同或兼容。 -- 通常返回的列名取自第一个查询。 - -默认地,`UNION` 操作符选取不同的值。如果允许重复的值,请使用 `UNION ALL`。 - -```sql -SELECT column_name(s) FROM table1 -UNION ALL -SELECT column_name(s) FROM table2; -``` - -`UNION` 结果集中的列名总是等于 `UNION` 中第一个 `SELECT` 语句中的列名。 - -`JOIN` vs `UNION`: - -- `JOIN` 中连接表的列可能不同,但在 `UNION` 中,所有查询的列数和列顺序必须相同。 -- `UNION` 将查询之后的行放在一起(垂直放置),但 `JOIN` 将查询之后的列放在一起(水平放置),即它构成一个笛卡尔积。 - -### 将两个 SELECT 语句结合起来(一) - -表 `OrderItems` 包含订单产品信息,字段 `prod_id` 代表产品 id、`quantity` 代表产品数量 - -| prod_id | quantity | -| ------- | -------- | -| a0001 | 105 | -| a0002 | 100 | -| a0002 | 200 | -| a0013 | 1121 | -| a0003 | 10 | -| a0003 | 19 | -| a0003 | 5 | -| BNBG | 10002 | - -【问题】将两个 `SELECT` 语句结合起来,以便从 `OrderItems` 表中检索产品 id(`prod_id`)和 `quantity`。其中,一个 `SELECT` 语句过滤数量为 100 的行,另一个 `SELECT` 语句过滤 id 以 BNBG 开头的产品,最后按产品 id 对结果进行升序排序。 - -```sql -SELECT prod_id, quantity -FROM OrderItems -WHERE quantity = 100 -UNION -SELECT prod_id, quantity -FROM OrderItems -WHERE prod_id LIKE 'BNBG%' -``` - -### 将两个 SELECT 语句结合起来(二) - -表 `OrderItems` 包含订单产品信息,字段 `prod_id` 代表产品 id、`quantity` 代表产品数量。 - -| prod_id | quantity | -| ------- | -------- | -| a0001 | 105 | -| a0002 | 100 | -| a0002 | 200 | -| a0013 | 1121 | -| a0003 | 10 | -| a0003 | 19 | -| a0003 | 5 | -| BNBG | 10002 | - -【问题】将两个 `SELECT` 语句结合起来,以便从 `OrderItems` 表中检索产品 id(`prod_id`)和 `quantity`。其中,一个 `SELECT` 语句过滤数量为 100 的行,另一个 `SELECT` 语句过滤 id 以 BNBG 开头的产品,最后按产品 id 对结果进行升序排序。 注意:**这次仅使用单个 SELECT 语句。** - -答案: - -要求只用一条 select 语句,那就用 `or` 不用 `union` 了。 - -```sql -SELECT prod_id, quantity -FROM OrderItems -WHERE quantity = 100 OR prod_id LIKE 'BNBG%' -``` - -### 组合 Products 表中的产品名称和 Customers 表中的顾客名称 - -`Products` 表含有字段 `prod_name` 代表产品名称 - -| prod_name | -| --------- | -| flower | -| rice | -| ring | -| umbrella | - -Customers 表代表顾客信息,cust_name 代表顾客名称 - -| cust_name | -| --------- | -| andy | -| ben | -| tony | -| tom | -| an | -| lee | -| hex | - -【问题】编写 SQL 语句,组合 `Products` 表中的产品名称(`prod_name`)和 `Customers` 表中的顾客名称(`cust_name`)并返回,然后按产品名称对结果进行升序排序。 - -```sql -# UNION 结果集中的列名总是等于 UNION 中第一个 SELECT 语句中的列名。 -SELECT prod_name -FROM Products -UNION -SELECT cust_name -FROM Customers -ORDER BY prod_name -``` - -### 检查 SQL 语句 - -表 `Customers` 含有字段 `cust_name` 顾客名、`cust_contact` 顾客联系方式、`cust_state` 顾客州、`cust_email` 顾客 `email` - -| cust_name | cust_contact | cust_state | cust_email | -| --------- | ------------ | ---------- | ----------------- | -| cust10 | 8695192 | MI | | -| cust1 | 8695193 | MI | | -| cust2 | 8695194 | IL | | - -【问题】修正下面错误的 SQL - -```sql -SELECT cust_name, cust_contact, cust_email -FROM Customers -WHERE cust_state = 'MI' -ORDER BY cust_name; -UNION -SELECT cust_name, cust_contact, cust_email -FROM Customers -WHERE cust_state = 'IL'ORDER BY cust_name; -``` - -修正后: - -```sql -SELECT cust_name, cust_contact, cust_email -FROM Customers -WHERE cust_state = 'MI' -UNION -SELECT cust_name, cust_contact, cust_email -FROM Customers -WHERE cust_state = 'IL' -ORDER BY cust_name; -``` - -使用 `union` 组合查询时,只能使用一条 `order by` 字句,他必须位于最后一条 `select` 语句之后 - -或者直接用 `or` 来做: - -```sql -SELECT cust_name, cust_contact, cust_email -FROM Customers -WHERE cust_state = 'MI' or cust_state = 'IL' -ORDER BY cust_name; -``` - - +| Haidilao | +| Xia | diff --git a/docs/database/sql/sql-questions-02.md b/docs/database/sql/sql-questions-02.md index 2a4a3e496c6..2f88a9c173f 100644 --- a/docs/database/sql/sql-questions-02.md +++ b/docs/database/sql/sql-questions-02.md @@ -1,354 +1,354 @@ --- -title: SQL常见面试题总结(2) -category: 数据库 +title: Summary of Common SQL Interview Questions (2) +category: Database tag: - - 数据库基础 + - Database Basics - SQL --- -> 题目来源于:[牛客题霸 - SQL 进阶挑战](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) +> Source of the question: [Niuke Problem Boss - Advanced SQL Challenge](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) -## 增删改操作 +## Insert, Delete, and Update Operations -SQL 插入记录的方式汇总: +Summary of SQL methods for inserting records: -- **普通插入(全字段)** :`INSERT INTO table_name VALUES (value1, value2, ...)` -- **普通插入(限定字段)** :`INSERT INTO table_name (column1, column2, ...) VALUES (value1, value2, ...)` -- **多条一次性插入** :`INSERT INTO table_name (column1, column2, ...) VALUES (value1_1, value1_2, ...), (value2_1, value2_2, ...), ...` -- **从另一个表导入** :`INSERT INTO table_name SELECT * FROM table_name2 [WHERE key=value]` -- **带更新的插入** :`REPLACE INTO table_name VALUES (value1, value2, ...)`(注意这种原理是检测到主键或唯一性索引键重复就删除原记录后重新插入) +- **Normal Insert (All Fields)**: `INSERT INTO table_name VALUES (value1, value2, ...)` +- **Normal Insert (Specified Fields)**: `INSERT INTO table_name (column1, column2, ...) VALUES (value1, value2, ...)` +- **Insert Multiple Records at Once**: `INSERT INTO table_name (column1, column2, ...) VALUES (value1_1, value1_2, ...), (value2_1, value2_2, ...), ...` +- **Import from Another Table**: `INSERT INTO table_name SELECT * FROM table_name2 [WHERE key=value]` +- **Insert with Update**: `REPLACE INTO table_name VALUES (value1, value2, ...)` (Note that this method deletes the original record if a primary key or unique index key duplicate is detected, then reinserts it) -### 插入记录(一) +### Inserting Records (1) -**描述**:牛客后台会记录每个用户的试卷作答记录到 `exam_record` 表,现在有两个用户的作答记录详情如下: +**Description**: The Niuke backend records each user's exam record in the `exam_record` table. Two users' exam records are as follows: -- 用户 1001 在 2021 年 9 月 1 日晚上 10 点 11 分 12 秒开始作答试卷 9001,并在 50 分钟后提交,得了 90 分; -- 用户 1002 在 2021 年 9 月 4 日上午 7 点 1 分 2 秒开始作答试卷 9002,并在 10 分钟后退出了平台。 +- User 1001 started the exam 9001 at 10:11:12 PM on September 1, 2021, and submitted it after 50 minutes, scoring 90 points; +- User 1002 started the exam 9002 at 7:01:02 AM on September 4, 2021, and left the platform after 10 minutes. -试卷作答记录表`exam_record`中,表已建好,其结构如下,请用一条语句将这两条记录插入表中。 +The structure of the `exam_record` table is already established as follows; please use a single statement to insert these two records into the table. -| Filed | Type | Null | Key | Extra | Default | Comment | -| ----------- | ---------- | ---- | --- | -------------- | ------- | -------- | -| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | -| uid | int(11) | NO | | | (NULL) | 用户 ID | -| exam_id | int(11) | NO | | | (NULL) | 试卷 ID | -| start_time | datetime | NO | | | (NULL) | 开始时间 | -| submit_time | datetime | YES | | | (NULL) | 提交时间 | -| score | tinyint(4) | YES | | | (NULL) | 得分 | +| Field | Type | Null | Key | Extra | Default | Comment | +| ----------- | ---------- | ---- | --- | -------------- | ------- | ----------------- | +| id | int(11) | NO | PRI | auto_increment | (NULL) | Auto-increment ID | +| uid | int(11) | NO | | | (NULL) | User ID | +| exam_id | int(11) | NO | | | (NULL) | Exam ID | +| start_time | datetime | NO | | | (NULL) | Start Time | +| submit_time | datetime | YES | | | (NULL) | Submit Time | +| score | tinyint(4) | YES | | | (NULL) | Score | -**答案**: +**Answer**: ```sql -// 存在自增主键,无需手动赋值 +-- Auto-incrementing primary key, no manual assignment required INSERT INTO exam_record (uid, exam_id, start_time, submit_time, score) VALUES (1001, 9001, '2021-09-01 22:11:12', '2021-09-01 23:01:12', 90), (1002, 9002, '2021-09-04 07:01:02', NULL, NULL); ``` -### 插入记录(二) +### Inserting Records (2) -**描述**:现有一张试卷作答记录表`exam_record`,结构如下表,其中包含多年来的用户作答试卷记录,由于数据越来越多,维护难度越来越大,需要对数据表内容做精简,历史数据做备份。 +**Description**: There is an `exam_record` table containing user exam records for many years. Due to the increasing amount of data, maintenance difficulty is getting higher, and the content of the data table needs to be simplified, with historical data backed up. -表`exam_record`: +Table `exam_record`: -| Filed | Type | Null | Key | Extra | Default | Comment | -| ----------- | ---------- | ---- | --- | -------------- | ------- | -------- | -| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | -| uid | int(11) | NO | | | (NULL) | 用户 ID | -| exam_id | int(11) | NO | | | (NULL) | 试卷 ID | -| start_time | datetime | NO | | | (NULL) | 开始时间 | -| submit_time | datetime | YES | | | (NULL) | 提交时间 | -| score | tinyint(4) | YES | | | (NULL) | 得分 | +| Field | Type | Null | Key | Extra | Default | Comment | +| ----------- | ---------- | ---- | --- | -------------- | ------- | ----------------- | +| id | int(11) | NO | PRI | auto_increment | (NULL) | Auto-increment ID | +| uid | int(11) | NO | | | (NULL) | User ID | +| exam_id | int(11) | NO | | | (NULL) | Exam ID | +| start_time | datetime | NO | | | (NULL) | Start Time | +| submit_time | datetime | YES | | | (NULL) | Submit Time | +| score | tinyint(4) | YES | | | (NULL) | Score | -我们已经创建了一张新表`exam_record_before_2021`用来备份 2021 年之前的试题作答记录,结构和`exam_record`表一致,请将 2021 年之前的已完成了的试题作答纪录导入到该表。 +We have created a new table `exam_record_before_2021` to back up exam records before 2021, which has the same structure as the `exam_record` table. Please import the completed exam records before 2021 into this new table. -**答案**: +**Answer**: ```sql INSERT INTO exam_record_before_2021 (uid, exam_id, start_time, submit_time, score) -SELECT uid,exam_id,start_time,submit_time,score +SELECT uid, exam_id, start_time, submit_time, score FROM exam_record WHERE YEAR(submit_time) < 2021; ``` -### 插入记录(三) +### Inserting Records (3) -**描述**:现在有一套 ID 为 9003 的高难度 SQL 试卷,时长为一个半小时,请你将 2021-01-01 00:00:00 作为发布时间插入到试题信息表`examination_info`,不管该 ID 试卷是否存在,都要插入成功,请尝试插入它。 +**Description**: There is a difficult SQL exam with ID 9003, which lasts for one and a half hours. Please insert the release time `2021-01-01 00:00:00` into the `examination_info` table, regardless of whether this ID exam exists or not. -试题信息表`examination_info`: +Table `examination_info`: -| Filed | Type | Null | Key | Extra | Default | Comment | -| ------------ | ----------- | ---- | --- | -------------- | ------- | ------------ | -| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | -| exam_id | int(11) | NO | UNI | | (NULL) | 试卷 ID | -| tag | varchar(32) | YES | | | (NULL) | 类别标签 | -| difficulty | varchar(8) | YES | | | (NULL) | 难度 | -| duration | int(11) | NO | | | (NULL) | 时长(分钟数) | -| release_time | datetime | YES | | | (NULL) | 发布时间 | +| Field | Type | Null | Key | Extra | Default | Comment | +| ------------ | ----------- | ---- | --- | -------------- | ------- | ------------------ | +| id | int(11) | NO | PRI | auto_increment | (NULL) | Auto-increment ID | +| exam_id | int(11) | NO | UNI | | (NULL) | Exam ID | +| tag | varchar(32) | YES | | | (NULL) | Category Tag | +| difficulty | varchar(8) | YES | | | (NULL) | Difficulty | +| duration | int(11) | NO | | | (NULL) | Duration (minutes) | +| release_time | datetime | YES | | | (NULL) | Release Time | -**答案**: +**Answer**: ```sql REPLACE INTO examination_info VALUES (NULL, 9003, "SQL", "hard", 90, "2021-01-01 00:00:00"); ``` -### 更新记录(一) +### Updating Records (1) -**描述**:现在有一张试卷信息表 `examination_info`, 表结构如下图所示: +**Description**: There is an `examination_info` table. The table structure is as follows: -| Filed | Type | Null | Key | Extra | Default | Comment | -| ------------ | -------- | ---- | --- | -------------- | ------- | -------- | -| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | -| exam_id | int(11) | NO | UNI | | (NULL) | 试卷 ID | -| tag | char(32) | YES | | | (NULL) | 类别标签 | -| difficulty | char(8) | YES | | | (NULL) | 难度 | -| duration | int(11) | NO | | | (NULL) | 时长 | -| release_time | datetime | YES | | | (NULL) | 发布时间 | +| Field | Type | Null | Key | Extra | Default | Comment | +| ------------ | -------- | ---- | --- | -------------- | ------- | ----------------- | +| id | int(11) | NO | PRI | auto_increment | (NULL) | Auto-increment ID | +| exam_id | int(11) | NO | UNI | | (NULL) | Exam ID | +| tag | char(32) | YES | | | (NULL) | Category Tag | +| difficulty | char(8) | YES | | | (NULL) | Difficulty | +| duration | int(11) | NO | | | (NULL) | Duration | +| release_time | datetime | YES | | | (NULL) | Release Time | -请把**examination_info**表中`tag`为`PYTHON`的`tag`字段全部修改为`Python`。 +Please change all `tag` fields of `tag` equal to `PYTHON` in the `examination_info` table to `Python`. -**思路**:这题有两种解题思路,最容易想到的是直接`update + where`来指定条件更新,第二种就是根据要修改的字段进行查找替换 +**Thought Process**: There are two ways to solve this problem. The most straightforward approach is to directly use `update + where` to specify the conditional update. The second method is to find and replace based on the field that needs to be modified. -**答案一**: +**Answer 1**: ```sql UPDATE examination_info SET tag = 'Python' WHERE tag='PYTHON' ``` -**答案二**: +**Answer 2**: ```sql UPDATE examination_info SET tag = REPLACE(tag,'PYTHON','Python') -# REPLACE (目标字段,"查找内容","替换内容") +# REPLACE (Target Field, "Search Content", "Replacement Content") ``` -### 更新记录(二) +### Updating Records (2) -**描述**:现有一张试卷作答记录表 exam_record,其中包含多年来的用户作答试卷记录,结构如下表:作答记录表 `exam_record`: **`submit_time`** 为 完成时间 (注意这句话) +**Description**: There is an `exam_record` table, which contains years of user exam records. The table structure is as follows: The **`submit_time`** is the completion time (Note this statement). -| Filed | Type | Null | Key | Extra | Default | Comment | -| ----------- | ---------- | ---- | --- | -------------- | ------- | -------- | -| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | -| uid | int(11) | NO | | | (NULL) | 用户 ID | -| exam_id | int(11) | NO | | | (NULL) | 试卷 ID | -| start_time | datetime | NO | | | (NULL) | 开始时间 | -| submit_time | datetime | YES | | | (NULL) | 提交时间 | -| score | tinyint(4) | YES | | | (NULL) | 得分 | +| Field | Type | Null | Key | Extra | Default | Comment | +| ----------- | ---------- | ---- | --- | -------------- | ------- | ----------------- | +| id | int(11) | NO | PRI | auto_increment | (NULL) | Auto-increment ID | +| uid | int(11) | NO | | | (NULL) | User ID | +| exam_id | int(11) | NO | | | (NULL) | Exam ID | +| start_time | datetime | NO | | | (NULL) | Start Time | +| submit_time | datetime | YES | | | (NULL) | Submit Time | +| score | tinyint(4) | YES | | | (NULL) | Score | -**题目要求**:请把 `exam_record` 表中 2021 年 9 月 1 日==之前==开始作答的==未完成==记录全部改为被动完成,即:将完成时间改为'2099-01-01 00:00:00',分数改为 0。 +**Requirement**: Please change all uncompleted records in the `exam_record` table that started before September 1, 2021, to be marked as completed. Specifically: Set the completion time to '2099-01-01 00:00:00' and the score to 0. -**思路**:注意题干中的关键字(已经高亮) `" xxx 时间 "`之前这个条件, 那么这里马上就要想到要进行时间的比较 可以直接 `xxx_time < "2021-09-01 00:00:00",` 也可以采用`date()`函数来进行比较;第二个条件就是 `"未完成"`, 即完成时间为 NULL,也就是题目中的提交时间 ----- `submit_time 为 NULL`。 +**Thought Process**: Pay attention to the key phrases in the problem statement (highlighted) regarding "xxx time" before this condition. This suggests that we need to perform a time comparison using either `xxx_time < "2021-09-01 00:00:00"` or using the `date()` function for comparison. The second condition is "uncompleted", meaning the submission time is NULL; hence, `submit_time IS NULL`. -**答案**: +**Answer**: ```sql UPDATE exam_record SET submit_time = '2099-01-01 00:00:00', score = 0 WHERE DATE(start_time) < "2021-09-01" AND submit_time IS null ``` -### 删除记录(一) +### Deleting Records (1) -**描述**:现有一张试卷作答记录表 `exam_record`,其中包含多年来的用户作答试卷记录,结构如下表: +**Description**: There is an `exam_record` table containing many years of user records as follows: -作答记录表`exam_record:` **`start_time`** 是试卷开始时间`submit_time` 是交卷,即结束时间。 +The exam record table `exam_record`: **`start_time`** is the start time and `submit_time` is the submission/end time. -| Filed | Type | Null | Key | Extra | Default | Comment | -| ----------- | ---------- | ---- | --- | -------------- | ------- | -------- | -| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | -| uid | int(11) | NO | | | (NULL) | 用户 ID | -| exam_id | int(11) | NO | | | (NULL) | 试卷 ID | -| start_time | datetime | NO | | | (NULL) | 开始时间 | -| submit_time | datetime | YES | | | (NULL) | 提交时间 | -| score | tinyint(4) | YES | | | (NULL) | 得分 | +| Field | Type | Null | Key | Extra | Default | Comment | +| ----------- | ---------- | ---- | --- | -------------- | ------- | ----------------- | +| id | int(11) | NO | PRI | auto_increment | (NULL) | Auto-increment ID | +| uid | int(11) | NO | | | (NULL) | User ID | +| exam_id | int(11) | NO | | | (NULL) | Exam ID | +| start_time | datetime | NO | | | (NULL) | Start Time | +| submit_time | datetime | YES | | | (NULL) | Submit Time | +| score | tinyint(4) | YES | | | (NULL) | Score | -**要求**:请删除`exam_record`表中作答时间小于 5 分钟整且分数不及格(及格线为 60 分)的记录; +**Requirement**: Delete records in the `exam_record` table where the exam duration is less than 5 minutes and the score is failing (the passing score is 60). -**思路**:这一题虽然是练习删除,仔细看确是考察对时间函数的用法,这里提及的分钟数比较,常用的函数有 **`TIMEDIFF`**和**`TIMESTAMPDIFF`** ,两者用法稍有区别,后者更为灵活,这都是看个人习惯。 +**Thought Process**: While this question practices deletion, it exactly tests the usage of time functions. Two common functions for minute comparison are **`TIMEDIFF`** and **`TIMESTAMPDIFF`**, both with slightly different usages, with the latter being more flexible. -1.  `TIMEDIFF`:两个时间之间的差值 +1. `TIMEDIFF`: The time difference between two times ```sql TIMEDIFF(time1, time2) ``` -两者参数都是必须的,都是一个时间或者日期时间表达式。如果指定的参数不合法或者是 NULL,那么函数将返回 NULL。 +Both parameters are required and must be a time or datetime expression. If either is invalid or NULL, the function returns NULL. -对于这题而言,可以用在 minute 函数里面,因为 TIMEDIFF 计算出来的是时间的差值,在外面套一个 MINUTE 函数,计算出来的就是分钟数。 +For this problem, it can be applied within the minute function because the TIMEDIFF result gives the time difference, and wrapping this in a MINUTE function gives the minutes. -2. `TIMESTAMPDIFF`:用于计算两个日期的时间差 +2. `TIMESTAMPDIFF`: Used to calculate the time difference between two dates ```sql -TIMESTAMPDIFF(unit,datetime_expr1,datetime_expr2) -# 参数说明 -#unit: 日期比较返回的时间差单位,常用可选值如下: -SECOND:秒 -MINUTE:分钟 -HOUR:小时 -DAY:天 -WEEK:星期 -MONTH:月 -QUARTER:季度 -YEAR:年 -# TIMESTAMPDIFF函数返回datetime_expr2 - datetime_expr1的结果(人话: 后面的 - 前面的 即2-1),其中datetime_expr1和datetime_expr2可以是DATE或DATETIME类型值(人话:可以是“2023-01-01”, 也可以是“2023-01-01- 00:00:00”) +TIMESTAMPDIFF(unit, datetime_expr1, datetime_expr2) +# Parameter description +# unit: Common optional values for the time difference returned: +SECOND: seconds +MINUTE: minutes +HOUR: hours +DAY: days +WEEK: weeks +MONTH: months +QUARTER: quarters +YEAR: years +# The TIMESTAMPDIFF function returns the result of datetime_expr2 - datetime_expr1 (in simple terms: the latter - the former, i.e., 2-1), where datetime_expr1 and datetime_expr2 can be DATE or DATETIME type values. ``` -这题需要进行分钟的比较,那么就是 TIMESTAMPDIFF(MINUTE, 开始时间, 结束时间) < 5 +This question requires minute comparison, hence `TIMESTAMPDIFF(MINUTE, start_time, submit_time) < 5`. -**答案**: +**Answer**: ```sql -DELETE FROM exam_record WHERE MINUTE (TIMEDIFF(submit_time , start_time)) < 5 AND score < 60 +DELETE FROM exam_record WHERE MINUTE (TIMEDIFF(submit_time, start_time)) < 5 AND score < 60 ``` ```sql DELETE FROM exam_record WHERE TIMESTAMPDIFF(MINUTE, start_time, submit_time) < 5 AND score < 60 ``` -### 删除记录(二) +### Deleting Records (2) -**描述**:现有一张试卷作答记录表`exam_record`,其中包含多年来的用户作答试卷记录,结构如下表: +**Description**: There is an `exam_record` table that includes numerous user exam records, structured as follows: -作答记录表`exam_record`:`start_time` 是试卷开始时间,`submit_time` 是交卷时间,即结束时间,如果未完成的话,则为空。 +Exam record table `exam_record`: `start_time` is the exam start time, and `submit_time` is the submission time. If incomplete, it is NULL. -| Filed | Type | Null | Key | Extra | Default | Comment | -| ----------- | ---------- | :--: | --- | -------------- | ------- | -------- | -| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | -| uid | int(11) | NO | | | (NULL) | 用户 ID | -| exam_id | int(11) | NO | | | (NULL) | 试卷 ID | -| start_time | datetime | NO | | | (NULL) | 开始时间 | -| submit_time | datetime | YES | | | (NULL) | 提交时间 | -| score | tinyint(4) | YES | | | (NULL) | 分数 | +| Field | Type | Null | Key | Extra | Default | Comment | +| ----------- | ---------- | :--: | --- | -------------- | ------- | ----------------- | +| id | int(11) | NO | PRI | auto_increment | (NULL) | Auto-increment ID | +| uid | int(11) | NO | | | (NULL) | User ID | +| exam_id | int(11) | NO | | | (NULL) | Exam ID | +| start_time | datetime | NO | | | (NULL) | Start Time | +| submit_time | datetime | YES | | | (NULL) | Submit Time | +| score | tinyint(4) | YES | | | (NULL) | Score | -**要求**:请删除`exam_record`表中未完成作答==或==作答时间小于 5 分钟整的记录中,开始作答时间最早的 3 条记录。 +**Requirement**: Delete the earliest 3 records among those that are either uncompleted or have an exam duration of less than 5 minutes. -**思路**:这题比较简单,但是要注意题干中给出的信息,结束时间,如果未完成的话,则为空,这个其实就是一个条件 +**Thought Process**: This problem is relatively simple, but be sure to note the information given in the prompt. The end time, if incomplete, is NULL, which means this is one condition. -还有一个条件就是小于 5 分钟,跟上题类似,但是这里是**或**,即两个条件满足一个就行;另外就是稍微考察到了排序和 limit 的用法。 +Another condition is being less than 5 minutes, similar to the previous question, but here it is **or**, so either condition suffices. Lastly, this tests sorting and the use of limit. -**答案**: +**Answer**: ```sql -DELETE FROM exam_record WHERE submit_time IS null OR TIMESTAMPDIFF(MINUTE, start_time, submit_time) < 5 +DELETE FROM exam_record WHERE submit_time IS NULL OR TIMESTAMPDIFF(MINUTE, start_time, submit_time) < 5 ORDER BY start_time LIMIT 3 -# 默认就是asc, desc是降序排列 +# The default is ascending, desc is descending order ``` -### 删除记录(三) +### Deleting Records (3) -**描述**:现有一张试卷作答记录表 exam_record,其中包含多年来的用户作答试卷记录,结构如下表: +**Description**: There is an `exam_record` table containing multi-year user exam records structured as follows: -| Filed | Type | Null | Key | Extra | Default | Comment | -| ----------- | ---------- | :--: | --- | -------------- | ------- | -------- | -| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | -| uid | int(11) | NO | | | (NULL) | 用户 ID | -| exam_id | int(11) | NO | | | (NULL) | 试卷 ID | -| start_time | datetime | NO | | | (NULL) | 开始时间 | -| submit_time | datetime | YES | | | (NULL) | 提交时间 | -| score | tinyint(4) | YES | | | (NULL) | 分数 | +| Field | Type | Null | Key | Extra | Default | Comment | +| ----------- | ---------- | :--: | --- | -------------- | ------- | ----------------- | +| id | int(11) | NO | PRI | auto_increment | (NULL) | Auto-increment ID | +| uid | int(11) | NO | | | (NULL) | User ID | +| exam_id | int(11) | NO | | | (NULL) | Exam ID | +| start_time | datetime | NO | | | (NULL) | Start Time | +| submit_time | datetime | YES | | | (NULL) | Submit Time | +| score | tinyint(4) | YES | | | (NULL) | Score | -**要求**:请删除`exam_record`表中所有记录,==并重置自增主键== +**Requirement**: Delete all records in the `exam_record` table and reset the auto-incrementing primary key. -**思路**:这题考察对三种删除语句的区别,注意高亮部分,要求重置主键; +**Thought Process**: This question tests the differences between three delete statements. Note the highlighted part about resetting the primary key: -- `DROP`: 清空表,删除表结构,不可逆 -- `TRUNCATE`: 格式化表,不删除表结构,不可逆 -- `DELETE`:删除数据,可逆 +- `DROP`: Deletes the table and its structure and is irreversible. +- `TRUNCATE`: Clears the table but does not delete the structure. It is irreversible. +- `DELETE`: Deletes data and is reversible. -这里选用`TRUNCATE`的原因是:TRUNCATE 只能作用于表;`TRUNCATE`会清空表中的所有行,但表结构及其约束、索引等保持不变;`TRUNCATE`会重置表的自增值;使用`TRUNCATE`后会使表和索引所占用的空间会恢复到初始大小。 +In this case, `TRUNCATE` is chosen because it only affects the table; `TRUNCATE` clears all rows from the table but keeps the structure, constraints, indexes, etc.; `TRUNCATE` resets the auto-increment value; after using `TRUNCATE`, the space occupied by the table and indexes reverts to the initial size. -这题也可以采用`DELETE`来做,但是在删除后,还需要手动`ALTER`表结构来设置主键初始值; +This problem can also be approached with `DELETE`, but would require a manual `ALTER` after deleting to set the primary key initial value. -同理也可以采用`DROP`来做,直接删除整张表,包括表结构,然后再新建表即可。 +Similarly, `DROP` could be used but involves deleting the entire table, including its structure and then recreating it. -**答案**: +**Answer**: ```sql -TRUNCATE exam_record; +TRUNCATE exam_record; ``` -## 表与索引操作 +## Table and Index Operations -### 创建一张新表 +### Creating a New Table -**描述**:现有一张用户信息表,其中包含多年来在平台注册过的用户信息,随着牛客平台的不断壮大,用户量飞速增长,为了高效地为高活跃用户提供服务,现需要将部分用户拆分出一张新表。 +**Description**: There is a user information table that contains user information registered on the platform over the years. As the Niuke platform continues to grow, the user base is rapidly increasing. To efficiently serve high-activity users, there is a need to split some users into a new table. -原来的用户信息表: +Original user information table: -| Filed | Type | Null | Key | Default | Extra | Comment | -| ------------- | ----------- | ---- | --- | ----------------- | -------------- | -------- | -| id | int(11) | NO | PRI | (NULL) | auto_increment | 自增 ID | -| uid | int(11) | NO | UNI | (NULL) | | 用户 ID | -| nick_name | varchar(64) | YES | | (NULL) | | 昵称 | -| achievement | int(11) | YES | | 0 | | 成就值 | -| level | int(11) | YES | | (NULL) | | 用户等级 | -| job | varchar(32) | YES | | (NULL) | | 职业方向 | -| register_time | datetime | YES | | CURRENT_TIMESTAMP | | 注册时间 | +| Field | Type | Null | Key | Default | Extra | Comment | +| ------------- | ----------- | ---- | --- | ----------------- | -------------- | ----------------- | +| id | int(11) | NO | PRI | (NULL) | auto_increment | Auto-increment ID | +| uid | int(11) | NO | UNI | (NULL) | | User ID | +| nick_name | varchar(64) | YES | | (NULL) | | Nickname | +| achievement | int(11) | YES | | 0 | | Achievement Value | +| level | int(11) | YES | | (NULL) | | User Level | +| job | varchar(32) | YES | | (NULL) | | Career Direction | +| register_time | datetime | YES | | CURRENT_TIMESTAMP | | Registration Time | -作为数据分析师,请**创建一张优质用户信息表 user_info_vip**,表结构和用户信息表一致。 +As a data analyst, please **create a quality user information table named user_info_vip** with the same structure as the user information table. -你应该返回的输出如下表格所示,请写出建表语句将表格中所有限制和说明记录到表里。 +The output you should return should match the table structure shown below. Please write the create table statement to include all constraints and comments. -| Filed | Type | Null | Key | Default | Extra | Comment | -| ------------- | ----------- | ---- | --- | ----------------- | -------------- | -------- | -| id | int(11) | NO | PRI | (NULL) | auto_increment | 自增 ID | -| uid | int(11) | NO | UNI | (NULL) | | 用户 ID | -| nick_name | varchar(64) | YES | | (NULL) | | 昵称 | -| achievement | int(11) | YES | | 0 | | 成就值 | -| level | int(11) | YES | | (NULL) | | 用户等级 | -| job | varchar(32) | YES | | (NULL) | | 职业方向 | -| register_time | datetime | YES | | CURRENT_TIMESTAMP | | 注册时间 | +| Field | Type | Null | Key | Default | Extra | Comment | +| ------------- | ----------- | ---- | --- | ----------------- | -------------- | ----------------- | +| id | int(11) | NO | PRI | (NULL) | auto_increment | Auto-increment ID | +| uid | int(11) | NO | UNI | (NULL) | | User ID | +| nick_name | varchar(64) | YES | | (NULL) | | Nickname | +| achievement | int(11) | YES | | 0 | | Achievement Value | +| level | int(11) | YES | | (NULL) | | User Level | +| job | varchar(32) | YES | | (NULL) | | Career Direction | +| register_time | datetime | YES | | CURRENT_TIMESTAMP | | Registration Time | -**思路**:如果这题给出了旧表的名称,可直接`create table 新表 as select * from 旧表;` 但是这题并没有给出旧表名称,所以需要自己创建,注意默认值和键的创建即可,比较简单。(注意:如果是在牛客网上面执行,请注意 comment 中要和题目中的 comment 保持一致,包括大小写,否则不通过,还有字符也要设置) +**Thought Process**: If the old table name was provided, one could simply use `create table new_table as select * from old_table;` However, since the name is not given, it needs to be created manually, paying attention to defaults and key creation. This is relatively simple (Note: if executed on Niuke.com, ensure the comment matches the requirement, including case sensitivity, otherwise it will fail). -答案: +**Answer**: ```sql CREATE TABLE IF NOT EXISTS user_info_vip( - id INT(11) PRIMARY KEY AUTO_INCREMENT COMMENT'自增ID', - uid INT(11) UNIQUE NOT NULL COMMENT '用户ID', - nick_name VARCHAR(64) COMMENT'昵称', - achievement INT(11) DEFAULT 0 COMMENT '成就值', - `level` INT(11) COMMENT '用户等级', - job VARCHAR(32) COMMENT '职业方向', - register_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间' -)CHARACTER SET UTF8 + id INT(11) PRIMARY KEY AUTO_INCREMENT COMMENT 'Auto-increment ID', + uid INT(11) UNIQUE NOT NULL COMMENT 'User ID', + nick_name VARCHAR(64) COMMENT 'Nickname', + achievement INT(11) DEFAULT 0 COMMENT 'Achievement Value', + `level` INT(11) COMMENT 'User Level', + job VARCHAR(32) COMMENT 'Career Direction', + register_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Registration Time' +) CHARACTER SET UTF8 ``` -### 修改表 +### Modifying a Table -**描述**: 现有一张用户信息表`user_info`,其中包含多年来在平台注册过的用户信息。 +**Description**: There is a user information table `user_info` which contains user information registered on the platform over the years. -**用户信息表 `user_info`:** +**User Information Table `user_info`:** -| Filed | Type | Null | Key | Default | Extra | Comment | -| ------------- | ----------- | ---- | --- | ----------------- | -------------- | -------- | -| id | int(11) | NO | PRI | (NULL) | auto_increment | 自增 ID | -| uid | int(11) | NO | UNI | (NULL) | | 用户 ID | -| nick_name | varchar(64) | YES | | (NULL) | | 昵称 | -| achievement | int(11) | YES | | 0 | | 成就值 | -| level | int(11) | YES | | (NULL) | | 用户等级 | -| job | varchar(32) | YES | | (NULL) | | 职业方向 | -| register_time | datetime | YES | | CURRENT_TIMESTAMP | | 注册时间 | +| Field | Type | Null | Key | Default | Extra | Comment | +| ------------- | ----------- | ---- | --- | ----------------- | -------------- | ----------------- | +| id | int(11) | NO | PRI | (NULL) | auto_increment | Auto-increment ID | +| uid | int(11) | NO | UNI | (NULL) | | User ID | +| nick_name | varchar(64) | YES | | (NULL) | | Nickname | +| achievement | int(11) | YES | | 0 | | Achievement Value | +| level | int(11) | YES | | (NULL) | | User Level | +| job | varchar(32) | YES | | (NULL) | | Career Direction | +| register_time | datetime | YES | | CURRENT_TIMESTAMP | | Registration Time | -**要求:**请在用户信息表,字段 `level` 的后面增加一列最多可保存 15 个汉字的字段 `school`;并将表中 `job` 列名改为 `profession`,同时 `varchar` 字段长度变为 10;`achievement` 的默认值设置为 0。 +**Requirement**: Please add a new column `school` that can store a maximum of 15 Chinese characters after the `level` column in the user information table; rename the `job` column to `profession` while also changing the `varchar` field length to 10; set the default value of `achievement` to 0. -**思路**:首先做这题之前,需要了解 ALTER 语句的基本用法: +**Thought Process**: Before answering, one should understand the basic usage of the ALTER statement: -- 添加一列:`ALTER TABLE 表名 ADD COLUMN 列名 类型 【first | after 字段名】;`(first : 在某列之前添加,after 反之) -- 修改列的类型或约束:`ALTER TABLE 表名 MODIFY COLUMN 列名 新类型 【新约束】;` -- 修改列名:`ALTER TABLE 表名 change COLUMN 旧列名 新列名 类型;` -- 删除列:`ALTER TABLE 表名 drop COLUMN 列名;` -- 修改表名:`ALTER TABLE 表名 rename 【to】 新表名;` -- 将某一列放到第一列:`ALTER TABLE 表名 MODIFY COLUMN 列名 类型 first;` +- Add a column: `ALTER TABLE table_name ADD COLUMN column_name type [first | after column_name];` (first: add before a certain column, after is the opposite) +- Modify the column type or constraints: `ALTER TABLE table_name MODIFY COLUMN column_name new_type [new_constraints];` +- Rename the column: `ALTER TABLE table_name CHANGE COLUMN old_column_name new_column_name type;` +- Delete a column: `ALTER TABLE table_name DROP COLUMN column_name;` +- Rename a table: `ALTER TABLE table_name RENAME [TO] new_table_name;` +- Move a column to the first position: `ALTER TABLE table_name MODIFY COLUMN column_name type FIRST;` -`COLUMN` 关键字其实可以省略不写,这里基于规范还是罗列出来了。 +The `COLUMN` keyword can actually be omitted, but it is listed here for clarity in format. -在修改时,如果有多个修改项,可以写到一起,但要注意格式 +When making modifications, if there are multiple changes, they can be combined, but formatting is essential. -**答案**: +**Answer**: ```sql ALTER TABLE user_info @@ -357,15 +357,15 @@ ALTER TABLE user_info MODIFY achievement INT(11) DEFAULT 0; ``` -### 删除表 +### Deleting a Table -**描述**:现有一张试卷作答记录表 `exam_record`,其中包含多年来的用户作答试卷记录。一般每年都会为 `exam_record` 表建立一张备份表 `exam_record_{YEAR},{YEAR}` 为对应年份。 +**Description**: There is an `exam_record` table containing numerous user exam records over the years. Every year, a backup table named `exam_record_{YEAR}` is created, where `{YEAR}` corresponds to the year. -现在随着数据越来越多,存储告急,请你把很久前的(2011 到 2014 年)备份表都删掉(如果存在的话)。 +Now, due to increasing data, storage is running low, so please delete the old backup tables from 2011 to 2014 (if they exist). -**思路**:这题很简单,直接删就行,如果嫌麻烦,可以将要删除的表用逗号隔开,写到一行;这里肯定会有小伙伴问:如果要删除很多张表呢?放心,如果要删除很多张表,可以写脚本来进行删除。 +**Thought Process**: This question is straightforward; just delete them. To avoid additional complexity, you can list the tables to be deleted separated by commas in one line; one might ask: What if there are many tables to delete? No worries, scripts can be written to delete multiple tables. -**答案**: +**Answer**: ```sql DROP TABLE IF EXISTS exam_record_2011; @@ -374,13 +374,13 @@ DROP TABLE IF EXISTS exam_record_2013; DROP TABLE IF EXISTS exam_record_2014; ``` -### 创建索引 +### Creating an Index -**描述**:现有一张试卷信息表 `examination_info`,其中包含各种类型试卷的信息。为了对表更方便快捷地查询,需要在 `examination_info` 表创建以下索引, +**Description**: There is an `examination_info` table containing information on various types of exams. To facilitate quicker queries on this table, the following indexes need to be created in the `examination_info` table: -规则如下:在 `duration` 列创建普通索引 `idx_duration`、在 `exam_id` 列创建唯一性索引 `uniq_idx_exam_id`、在 `tag` 列创建全文索引 `full_idx_tag`。 +The rules are as follows: create a normal index `idx_duration` on the `duration` column, create a unique index `uniq_idx_exam_id` on the `exam_id` column, and a full-text index `full_idx_tag` on the `tag` column. -根据题意,将返回如下结果: +Based on the question, the expected output is as follows: | examination_info | 0 | PRIMARY | 1 | id | A | 0 | | | | BTREE | | ---------------- | --- | ---------------- | --- | -------- | --- | --- | --- | --- | --- | -------- | @@ -388,32 +388,18 @@ DROP TABLE IF EXISTS exam_record_2014; | examination_info | 1 | idx_duration | 1 | duration | A | 0 | | | | BTREE | | examination_info | 1 | full_idx_tag | 1 | tag | | 0 | | | YES | FULLTEXT | -备注:后台会通过 `SHOW INDEX FROM examination_info` 语句来对比输出结果 +Note: The backend will compare the output using `SHOW INDEX FROM examination_info`. -**思路**:做这题首先需要了解常见的索引类型: +**Thought Process**: Before answering, one needs to understand common index types: -- B-Tree 索引:B-Tree(或称为平衡树)索引是最常见和默认的索引类型。它适用于各种查询条件,可以快速定位到符合条件的数据。B-Tree 索引适用于普通的查找操作,支持等值查询、范围查询和排序。 -- 唯一索引:唯一索引与普通的 B-Tree 索引类似,不同之处在于它要求被索引的列的值是唯一的。这意味着在插入或更新数据时,MySQL 会验证索引列的唯一性。 -- 主键索引:主键索引是一种特殊的唯一索引,它用于唯一标识表中的每一行数据。每个表只能有一个主键索引,它可以帮助提高数据的访问速度和数据完整性。 -- 全文索引:全文索引用于在文本数据中进行全文搜索。它支持在文本字段中进行关键字搜索,而不仅仅是简单的等值或范围查找。全文索引适用于需要进行全文搜索的应用场景。 +- B-Tree Index: The B-Tree (or balanced tree) index is the most common and default type. It's suitable for various query conditions and can quickly locate the data that meets the condition. It's typically used for ordinary queries supporting equality, range queries, and sorting. +- Unique Index: Similar to the ordinary B-Tree index, but it requires the indexed column’s values to be unique, which means MySQL will check for uniqueness during insertion and updates. +- Primary Key Index: This is a special type of unique index used to uniquely identify every row in a table. Each table can only have one primary key index, which helps enhance access speed and data integrity. +- Full-Text Index: A full-text index is used for full-text searches within text data. It supports keyword searches in text fields, not just simple equality or range queries. It's suitable for applications needing full-text search. -```sql --- 示例: --- 添加B-Tree索引: - CREATE INDEX idx_name(索引名) ON 表名 (字段名); -- idx_name为索引名,以下都是 --- 创建唯一索引: - CREATE UNIQUE INDEX idx_name ON 表名 (字段名); --- 创建一个主键索引: - ALTER TABLE 表名 ADD PRIMARY KEY (字段名); --- 创建一个全文索引 - ALTER TABLE 表名 ADD FULLTEXT INDEX idx_name (字段名); - --- 通过以上示例,可以看出create 和 alter 都可以添加索引 -``` - -有了以上的基础知识之后,该题答案也就浮出水面了。 +With this foundational knowledge, the answer to this question is now clear. -**答案**: +**Answer**: ```sql ALTER TABLE examination_info @@ -422,25 +408,25 @@ ALTER TABLE examination_info ADD FULLTEXT INDEX full_idx_tag(tag); ``` -### 删除索引 +### Deleting an Index -**描述**:请删除`examination_info`表上的唯一索引 uniq_idx_exam_id 和全文索引 full_idx_tag。 +**Description**: Please delete the unique index `uniq_idx_exam_id` and the full-text index `full_idx_tag` on the `examination_info` table. -**思路**:该题考察删除索引的基本语法: +**Thought Process**: This question tests the basic syntax for deleting an index: ```sql --- 使用 DROP INDEX 删除索引 -DROP INDEX idx_name ON 表名; +-- Using DROP INDEX to delete an index +DROP INDEX idx_name ON table_name; --- 使用 ALTER TABLE 删除索引 -ALTER TABLE employees DROP INDEX idx_email; +-- Using ALTER TABLE to delete an index +ALTER TABLE table_name DROP INDEX idx_name; ``` -这里需要注意的是:在 MySQL 中,一次删除多个索引的操作是不支持的。每次删除索引时,只能指定一个索引名称进行删除。 +It is important to note that in MySQL, multiple indexes cannot be deleted in a single operation. Each operation can only specify one index name for deletion. -而且 **DROP** 命令需要慎用!!! +Additionally, **DROP** commands should be used with caution! -**答案**: +**Answer**: ```sql DROP INDEX uniq_idx_exam_id ON examination_info; diff --git a/docs/database/sql/sql-questions-03.md b/docs/database/sql/sql-questions-03.md index f5acd8fc5c8..fe478879d90 100644 --- a/docs/database/sql/sql-questions-03.md +++ b/docs/database/sql/sql-questions-03.md @@ -1,31 +1,31 @@ --- -title: SQL常见面试题总结(3) -category: 数据库 +title: Summary of Common SQL Interview Questions (3) +category: Database tag: - - 数据库基础 + - Database Basics - SQL --- -> 题目来源于:[牛客题霸 - SQL 进阶挑战](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) +> Source of questions: [Niuke Question Bank - SQL Advanced Challenge](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) -较难或者困难的题目可以根据自身实际情况和面试需要来决定是否要跳过。 +You can decide whether to skip difficult or challenging questions based on your actual situation and interview needs. -## 聚合函数 +## Aggregate Functions -### SQL 类别高难度试卷得分的截断平均值(较难) +### Truncated Average Score of High-Difficulty SQL Papers (Difficult) -**描述**: 牛客的运营同学想要查看大家在 SQL 类别中高难度试卷的得分情况。 +**Description**: The operations team at Niuke wants to see the score situation of users on high-difficulty SQL papers. -请你帮她从`exam_record`数据表中计算所有用户完成 SQL 类别高难度试卷得分的截断平均值(去掉一个最大值和一个最小值后的平均值)。 +Please help her calculate the truncated average score (the average after removing one maximum and one minimum score) of all users who completed high-difficulty SQL papers from the `exam_record` data table. -示例数据:`examination_info`(`exam_id` 试卷 ID, tag 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间) +Sample data: `examination_info` (`exam_id` Paper ID, `tag` Paper Category, `difficulty` Paper Difficulty, `duration` Exam Duration, `release_time` Release Time) -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ---- | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | -| 2 | 9002 | 算法 | medium | 80 | 2020-08-02 10:00:00 | +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | --------- | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | +| 2 | 9002 | Algorithm | medium | 80 | 2020-08-02 10:00:00 | -示例数据:`exam_record`(uid 用户 ID, exam_id 试卷 ID, start_time 开始作答时间, submit_time 交卷时间, score 得分) +Sample data: `exam_record` (`uid` User ID, `exam_id` Paper ID, `start_time` Start Time, `submit_time` Submit Time, `score` Score) | id | uid | exam_id | start_time | submit_time | score | | --- | ---- | ------- | ------------------- | ------------------- | ------ | @@ -40,1262 +40,16 @@ tag: | 9 | 1003 | 9001 | 2021-09-07 12:01:01 | 2021-09-07 10:31:01 | 50 | | 10 | 1004 | 9001 | 2021-09-06 10:01:01 | (NULL) | (NULL) | -根据输入你的查询结果如下: +Based on your input, your query result is as follows: | tag | difficulty | clip_avg_score | | --- | ---------- | -------------- | | SQL | hard | 81.7 | -从`examination_info`表可知,试卷 9001 为高难度 SQL 试卷,该试卷被作答的得分有[80,81,84,90,50],去除最高分和最低分后为[80,81,84],平均分为 81.6666667,保留一位小数后为 81.7 +From the `examination_info` table, it can be seen that paper 9001 is a high-difficulty SQL paper, and the scores for this paper are [80, 81, 84, 90, 50]. After removing the highest and lowest scores, we have [80, 81, 84], and the average score is 81.6666667, which rounds to 81.7 when keeping one decimal place. -**输入描述:** +**Input Description:** -输入数据中至少有 3 个有效分数 +The input data must contain at least 3 valid scores. -**思路一:** 要找出高难度 sql 试卷,肯定需要联 examination_info 这张表,然后找出高难度的课程,由 examination_info 得知,高难度 sql 的 exam_id 为 9001,那么等下就以 exam_id = 9001 作为条件去查询; - -先找出 9001 号考试 `select * from exam_record where exam_id = 9001` - -然后,找出最高分 `select max(score) 最高分 from exam_record where exam_id = 9001` - -接着,找出最低分 `select min(score) 最低分 from exam_record where exam_id = 9001` - -在查询出来的分数结果集当中,去掉最高分和最低分,最直观能想到的就是 NOT IN 或者 用 NOT EXISTS 也行,这里以 NOT IN 来做 - -首先将主体写出来`select tag, difficulty, round(avg(score), 1) clip_avg_score from examination_info info INNER JOIN exam_record record` - -**小 tips** : MYSQL 的 `ROUND()` 函数 ,`ROUND(X)`返回参数 X 最近似的整数 `ROUND(X,D)`返回 X ,其值保留到小数点后 D 位,第 D 位的保留方式为四舍五入。 - -再将上面的 "碎片" 语句拼凑起来即可, 注意在 NOT IN 中两个子查询用 UNION ALL 来关联,用 union 把 max 和 min 的结果集中在一行当中,这样形成一列多行的效果。 - -**答案一:** - -```sql -SELECT tag, difficulty, ROUND(AVG(score), 1) clip_avg_score - FROM examination_info info INNER JOIN exam_record record - WHERE info.exam_id = record.exam_id - AND record.exam_id = 9001 - AND record.score NOT IN( - SELECT MAX(score) - FROM exam_record - WHERE exam_id = 9001 - UNION ALL - SELECT MIN(score) - FROM exam_record - WHERE exam_id = 9001 - ) -``` - -这是最直观,也是最容易想到的解法,但是还有待改进,这算是投机取巧过关,其实严格按照题目要求应该这么写: - -```sql -SELECT tag, - difficulty, - ROUND(AVG(score), 1) clip_avg_score -FROM examination_info info -INNER JOIN exam_record record -WHERE info.exam_id = record.exam_id - AND record.exam_id = - (SELECT examination_info.exam_id - FROM examination_info - WHERE tag = 'SQL' - AND difficulty = 'hard' ) - AND record.score NOT IN - (SELECT MAX(score) - FROM exam_record - WHERE exam_id = - (SELECT examination_info.exam_id - FROM examination_info - WHERE tag = 'SQL' - AND difficulty = 'hard' ) - UNION ALL SELECT MIN(score) - FROM exam_record - WHERE exam_id = - (SELECT examination_info.exam_id - FROM examination_info - WHERE tag = 'SQL' - AND difficulty = 'hard' ) ) -``` - -然而你会发现,重复的语句非常多,所以可以利用`WITH`来抽取公共部分 - -**`WITH` 子句介绍**: - -`WITH` 子句,也称为公共表表达式(Common Table Expression,CTE),是在 SQL 查询中定义临时表的方式。它可以让我们在查询中创建一个临时命名的结果集,并且可以在同一查询中引用该结果集。 - -基本用法: - -```sql -WITH cte_name (column1, column2, ..., columnN) AS ( - -- 查询体 - SELECT ... - FROM ... - WHERE ... -) --- 主查询 -SELECT ... -FROM cte_name -WHERE ... -``` - -`WITH` 子句由以下几个部分组成: - -- `cte_name`: 给临时表起一个名称,可以在主查询中引用。 -- `(column1, column2, ..., columnN)`: 可选,指定临时表的列名。 -- `AS`: 必需,表示开始定义临时表。 -- `CTE 查询体`: 实际的查询语句,用于定义临时表中的数据。 - -`WITH` 子句的主要用途之一是增强查询的可读性和可维护性,尤其在涉及多个嵌套子查询或需要重复使用相同的查询逻辑时。通过将这些逻辑放在一个命名的临时表中,我们可以更清晰地组织查询,并消除重复代码。 - -此外,`WITH` 子句还可以在复杂的查询中实现递归查询。递归查询允许我们在单个查询中执行对同一表的多次迭代,逐步构建结果集。这在处理层次结构数据、组织结构和树状结构等场景中非常有用。 - -**小细节**:MySQL 5.7 版本以及之前的版本不支持在 `WITH` 子句中直接使用别名。 - -下面是改进后的答案: - -```sql -WITH t1 AS - (SELECT record.*, - info.tag, - info.difficulty - FROM exam_record record - INNER JOIN examination_info info ON record.exam_id = info.exam_id - WHERE info.tag = "SQL" - AND info.difficulty = "hard" ) -SELECT tag, - difficulty, - ROUND(AVG(score), 1) -FROM t1 -WHERE score NOT IN - (SELECT max(score) - FROM t1 - UNION SELECT min(score) - FROM t1) -``` - -**思路二:** - -- 筛选 SQL 高难度试卷:`where tag="SQL" and difficulty="hard"` -- 计算截断平均值:`(和-最大值-最小值) / (总个数-2)`: - - `(sum(score) - max(score) - min(score)) / (count(score) - 2)` - - 有一个缺点就是,如果最大值和最小值有多个,这个方法就很难筛选出来, 但是题目中说了----->**`去掉一个最大值和一个最小值后的平均值`**, 所以这里可以用这个公式。 - -**答案二:** - -```sql -SELECT info.tag, - info.difficulty, - ROUND((SUM(record.score)- MIN(record.score)- MAX(record.score)) / (COUNT(record.score)- 2), 1) AS clip_avg_score -FROM examination_info info, - exam_record record -WHERE info.exam_id = record.exam_id - AND info.tag = "SQL" - AND info.difficulty = "hard"; -``` - -### 统计作答次数 - -有一个试卷作答记录表 `exam_record`,请从中统计出总作答次数 `total_pv`、试卷已完成作答数 `complete_pv`、已完成的试卷数 `complete_exam_cnt`。 - -示例数据 `exam_record` 表(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:01 | 80 | -| 2 | 1001 | 9001 | 2021-05-02 10:01:01 | 2021-05-02 10:30:01 | 81 | -| 3 | 1001 | 9001 | 2021-06-02 19:01:01 | 2021-06-02 19:31:01 | 84 | -| 4 | 1001 | 9002 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 | -| 5 | 1001 | 9001 | 2021-09-02 12:01:01 | (NULL) | (NULL) | -| 6 | 1001 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | -| 7 | 1002 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 | -| 8 | 1002 | 9001 | 2021-05-05 18:01:01 | 2021-05-05 18:59:02 | 90 | -| 9 | 1003 | 9001 | 2021-09-07 12:01:01 | 2021-09-07 10:31:01 | 50 | -| 10 | 1004 | 9001 | 2021-09-06 10:01:01 | (NULL) | (NULL) | - -示例输出: - -| total_pv | complete_pv | complete_exam_cnt | -| -------- | ----------- | ----------------- | -| 10 | 7 | 2 | - -解释:表示截止当前,有 10 次试卷作答记录,已完成的作答次数为 7 次(中途退出的为未完成状态,其交卷时间和份数为 NULL),已完成的试卷有 9001 和 9002 两份。 - -**思路**: 这题一看到统计次数,肯定第一时间就要想到用`COUNT`这个函数来解决,问题是要统计不同的记录,该怎么来写?使用子查询就能解决这个题目(这题用 case when 也能写出来,解法类似,逻辑不同而已);首先在做这个题之前,让我们先来了解一下`COUNT`的基本用法; - -`COUNT()` 函数的基本语法如下所示: - -```sql -COUNT(expression) -``` - -其中,`expression` 可以是列名、表达式、常量或通配符。下面是一些常见的用法示例: - -1. 计算表中所有行的数量: - -```sql -SELECT COUNT(*) FROM table_name; -``` - -2. 计算特定列非空(不为 NULL)值的数量: - -```sql -SELECT COUNT(column_name) FROM table_name; -``` - -3. 计算满足条件的行数: - -```sql -SELECT COUNT(*) FROM table_name WHERE condition; -``` - -4. 结合 `GROUP BY` 使用,计算分组后每个组的行数: - -```sql -SELECT column_name, COUNT(*) FROM table_name GROUP BY column_name; -``` - -5. 计算不同列组合的唯一组合数: - -```sql -SELECT COUNT(DISTINCT column_name1, column_name2) FROM table_name; -``` - -在使用 `COUNT()` 函数时,如果不指定任何参数或者使用 `COUNT(*)`,将会计算所有行的数量。而如果使用列名,则只会计算该列非空值的数量。 - -另外,`COUNT()` 函数的结果是一个整数值。即使结果是零,也不会返回 NULL,这点需要谨记。 - -**答案**: - -```sql -SELECT - count(*) total_pv, - ( SELECT count(*) FROM exam_record WHERE submit_time IS NOT NULL ) complete_pv, - ( SELECT COUNT( DISTINCT exam_id, score IS NOT NULL OR NULL ) FROM exam_record ) complete_exam_cnt -FROM - exam_record -``` - -这里着重说一下`COUNT( DISTINCT exam_id, score IS NOT NULL OR NULL )`这一句,判断 score 是否为 null ,如果是即为真,如果不是返回 null;注意这里如果不加 `or null` 在不是 null 的情况下只会返回 false 也就是返回 0; - -`COUNT`本身是不可以对多列求行数的,`distinct`的加入是的多列成为一个整体,可以求出现的行数了;`count distinct`在计算时只返回非 null 的行, 这个也要注意; - -另外通过本题 get 到了------>count 加条件常用句式`count( 列判断 or null)` - -### 得分不小于平均分的最低分 - -**描述**: 请从试卷作答记录表中找到 SQL 试卷得分不小于该类试卷平均得分的用户最低得分。 - -示例数据 exam_record 表(uid 用户 ID, exam_id 试卷 ID, start_time 开始作答时间, submit_time 交卷时间, score 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:01 | 80 | -| 2 | 1002 | 9001 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 | -| 3 | 1002 | 9002 | 2021-09-02 12:01:01 | (NULL) | (NULL) | -| 4 | 1002 | 9003 | 2021-09-01 12:01:01 | (NULL) | (NULL) | -| 5 | 1002 | 9001 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 | -| 6 | 1002 | 9002 | 2021-05-05 18:01:01 | 2021-05-05 18:59:02 | 90 | -| 7 | 1003 | 9002 | 2021-02-06 12:01:01 | (NULL) | (NULL) | -| 8 | 1003 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 | -| 9 | 1004 | 9003 | 2021-09-06 12:01:01 | (NULL) | (NULL) | - -`examination_info` 表(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间) - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ---- | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | -| 2 | 9002 | SQL | easy | 60 | 2020-02-01 10:00:00 | -| 3 | 9003 | 算法 | medium | 80 | 2020-08-02 10:00:00 | - -示例输出数据: - -| min_score_over_avg | -| ------------------ | -| 87 | - -**解释**:试卷 9001 和 9002 为 SQL 类别,作答这两份试卷的得分有[80,89,87,90],平均分为 86.5,不小于平均分的最小分数为 87 - -**思路**:这类题目第一眼看确实很复杂, 因为不知道从哪入手,但是当我们仔细读题审题后,要学会抓住题干中的关键信息。以本题为例:`请从试卷作答记录表中找到SQL试卷得分不小于该类试卷平均得分的用户最低得分。`你能一眼从中提取哪些有效信息来作为解题思路? - -第一条:找到==SQL==试卷得分 - -第二条:该类试卷==平均得分== - -第三条:该类试卷的==用户最低得分== - -然后中间的 “桥梁” 就是==不小于== - -将条件拆分后,先逐步完成 - -```sql --- 找出tag为‘SQL’的得分 【80, 89,87,90】 --- 再算出这一组的平均得分 -select ROUND(AVG(score), 1) from examination_info info INNER JOIN exam_record record - where info.exam_id = record.exam_id - and tag= 'SQL' -``` - -然后再找出该类试卷的最低得分,接着将结果集`【80, 89,87,90】` 去和平均分数作比较,方可得出最终答案。 - -**答案**: - -```sql -SELECT MIN(score) AS min_score_over_avg -FROM examination_info info -INNER JOIN exam_record record -WHERE info.exam_id = record.exam_id - AND tag= 'SQL' - AND score >= - (SELECT ROUND(AVG(score), 1) - FROM examination_info info - INNER JOIN exam_record record - WHERE info.exam_id = record.exam_id - AND tag= 'SQL' ) -``` - -其实这类题目给出的要求看似很 “绕”,但其实仔细梳理一遍,将大条件拆分成小条件,逐个拆分完以后,最后将所有条件拼凑起来。反正只要记住:**抓主干,理分支**,问题便迎刃而解。 - -## 分组查询 - -### 平均活跃天数和月活人数 - -**描述**:用户在牛客试卷作答区作答记录存储在表 `exam_record` 中,内容如下: - -`exam_record` 表(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分) - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2021-07-02 09:01:01 | 2021-07-02 09:21:01 | 80 | -| 2 | 1002 | 9001 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 81 | -| 3 | 1002 | 9002 | 2021-09-02 12:01:01 | (NULL) | (NULL) | -| 4 | 1002 | 9003 | 2021-09-01 12:01:01 | (NULL) | (NULL) | -| 5 | 1002 | 9001 | 2021-07-02 19:01:01 | 2021-07-02 19:30:01 | 82 | -| 6 | 1002 | 9002 | 2021-07-05 18:01:01 | 2021-07-05 18:59:02 | 90 | -| 7 | 1003 | 9002 | 2021-07-06 12:01:01 | (NULL) | (NULL) | -| 8 | 1003 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 | -| 9 | 1004 | 9003 | 2021-09-06 12:01:01 | (NULL) | (NULL) | -| 10 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 | -| 11 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 | -| 12 | 1006 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 | -| 13 | 1007 | 9002 | 2020-09-02 12:11:01 | 2020-09-02 12:31:01 | 89 | - -请计算 2021 年每个月里试卷作答区用户平均月活跃天数 `avg_active_days` 和月度活跃人数 `mau`,上面数据的示例输出如下: - -| month | avg_active_days | mau | -| ------ | --------------- | --- | -| 202107 | 1.50 | 2 | -| 202109 | 1.25 | 4 | - -**解释**:2021 年 7 月有 2 人活跃,共活跃了 3 天(1001 活跃 1 天,1002 活跃 2 天),平均活跃天数 1.5;2021 年 9 月有 4 人活跃,共活跃了 5 天,平均活跃天数 1.25,结果保留 2 位小数。 - -注:此处活跃指有==交卷==行为。 - -**思路**:读完题先注意高亮部分;一般求天数和月活跃人数马上就要想到相关的日期函数;这一题我们同样来进行拆分,把问题细化再解决;首先求活跃人数,肯定要用到`COUNT()`,那这里首先就有一个坑,不知道大家注意了没有?用户 1002 在 9 月份做了两种不同的试卷,所以这里要注意去重,不然在统计的时候,活跃人数是错的;第二个就是要知道日期的格式化,如上表,题目要求以`202107`这种日期格式展现,要用到`DATE_FORMAT`来进行格式化。 - -基本用法: - -`DATE_FORMAT(date_value, format)` - -- `date_value` 参数是待格式化的日期或时间值。 -- `format` 参数是指定的日期或时间格式(这个和 Java 里面的日期格式一样)。 - -**答案**: - -```sql -SELECT DATE_FORMAT(submit_time, '%Y%m') MONTH, - round(count(DISTINCT UID, DATE_FORMAT(submit_time, '%Y%m%d')) / count(DISTINCT UID), 2) avg_active_days, - COUNT(DISTINCT UID) mau -FROM exam_record -WHERE YEAR (submit_time) = 2021 -GROUP BY MONTH -``` - -这里多说一句, 使用`COUNT(DISTINCT uid, DATE_FORMAT(submit_time, '%Y%m%d'))` 可以统计在 `uid` 列和 `submit_time` 列按照年份、月份和日期进行格式化后的组合值的数量。 - -### 月总刷题数和日均刷题数 - -**描述**:现有一张题目练习记录表 `practice_record`,示例内容如下: - -| id | uid | question_id | submit_time | score | -| --- | ---- | ----------- | ------------------- | ----- | -| 1 | 1001 | 8001 | 2021-08-02 11:41:01 | 60 | -| 2 | 1002 | 8001 | 2021-09-02 19:30:01 | 50 | -| 3 | 1002 | 8001 | 2021-09-02 19:20:01 | 70 | -| 4 | 1002 | 8002 | 2021-09-02 19:38:01 | 70 | -| 5 | 1003 | 8002 | 2021-08-01 19:38:01 | 80 | - -请从中统计出 2021 年每个月里用户的月总刷题数 `month_q_cnt` 和日均刷题数 `avg_day_q_cnt`(按月份升序排序)以及该年的总体情况,示例数据输出如下: - -| submit_month | month_q_cnt | avg_day_q_cnt | -| ------------ | ----------- | ------------- | -| 202108 | 2 | 0.065 | -| 202109 | 3 | 0.100 | -| 2021 汇总 | 5 | 0.161 | - -**解释**:2021 年 8 月共有 2 次刷题记录,日均刷题数为 2/31=0.065(保留 3 位小数);2021 年 9 月共有 3 次刷题记录,日均刷题数为 3/30=0.100;2021 年共有 5 次刷题记录(年度汇总平均无实际意义,这里我们按照 31 天来算 5/31=0.161) - -> 牛客已经采用最新的 Mysql 版本,如果您运行结果出现错误:ONLY_FULL_GROUP_BY,意思是:对于 GROUP BY 聚合操作,如果在 SELECT 中的列,没有在 GROUP BY 中出现,那么这个 SQL 是不合法的,因为列不在 GROUP BY 从句中,也就是说查出来的列必须在 group by 后面出现否则就会报错,或者这个字段出现在聚合函数里面。 - -**思路:** - -看到实例数据就要马上联想到相关的函数,比如`submit_month`就要用到`DATE_FORMAT`来格式化日期。然后查出每月的刷题数量。 - -每月的刷题数量 - -```sql -SELECT MONTH ( submit_time ), COUNT( question_id ) -FROM - practice_record -GROUP BY - MONTH (submit_time) -``` - -接着第三列这里要用到`DAY(LAST_DAY(date_value))`函数来查找给定日期的月份中的天数。 - -示例代码如下: - -```sql -SELECT DAY(LAST_DAY('2023-07-08')) AS days_in_month; --- 输出:31 - -SELECT DAY(LAST_DAY('2023-02-01')) AS days_in_month; --- 输出:28 (闰年中的二月份) - -SELECT DAY(LAST_DAY(NOW())) AS days_in_current_month; --- 输出:31 (当前月份的天数) -``` - -使用 `LAST_DAY()` 函数获取给定日期的当月最后一天,然后使用 `DAY()` 函数提取该日期的天数。这样就能获得指定月份的天数。 - -需要注意的是,`LAST_DAY()` 函数返回的是日期值,而 `DAY()` 函数用于提取日期值中的天数部分。 - -有了上述的分析之后,即可马上写出答案,这题复杂就复杂在处理日期上,其中的逻辑并不难。 - -**答案**: - -```sql -SELECT DATE_FORMAT(submit_time, '%Y%m') submit_month, - count(question_id) month_q_cnt, - ROUND(COUNT(question_id) / DAY (LAST_DAY(submit_time)), 3) avg_day_q_cnt -FROM practice_record -WHERE DATE_FORMAT(submit_time, '%Y') = '2021' -GROUP BY submit_month -UNION ALL -SELECT '2021汇总' AS submit_month, - count(question_id) month_q_cnt, - ROUND(COUNT(question_id) / 31, 3) avg_day_q_cnt -FROM practice_record -WHERE DATE_FORMAT(submit_time, '%Y') = '2021' -ORDER BY submit_month -``` - -在实例数据输出中因为最后一行需要得出汇总数据,所以这里要 `UNION ALL`加到结果集中;别忘了最后要排序! - -### 未完成试卷数大于 1 的有效用户(较难) - -**描述**:现有试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分),示例数据如下: - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2021-07-02 09:01:01 | 2021-07-02 09:21:01 | 80 | -| 2 | 1002 | 9001 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 81 | -| 3 | 1002 | 9002 | 2021-09-02 12:01:01 | (NULL) | (NULL) | -| 4 | 1002 | 9003 | 2021-09-01 12:01:01 | (NULL) | (NULL) | -| 5 | 1002 | 9001 | 2021-07-02 19:01:01 | 2021-07-02 19:30:01 | 82 | -| 6 | 1002 | 9002 | 2021-07-05 18:01:01 | 2021-07-05 18:59:02 | 90 | -| 7 | 1003 | 9002 | 2021-07-06 12:01:01 | (NULL) | (NULL) | -| 8 | 1003 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 | -| 9 | 1004 | 9003 | 2021-09-06 12:01:01 | (NULL) | (NULL) | -| 10 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 | -| 11 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 | -| 12 | 1006 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 | -| 13 | 1007 | 9002 | 2020-09-02 12:11:01 | 2020-09-02 12:31:01 | 89 | - -还有一张试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间),示例数据如下: - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ---- | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | -| 2 | 9002 | SQL | easy | 60 | 2020-02-01 10:00:00 | -| 3 | 9003 | 算法 | medium | 80 | 2020-08-02 10:00:00 | - -请统计 2021 年每个未完成试卷作答数大于 1 的有效用户的数据(有效用户指完成试卷作答数至少为 1 且未完成数小于 5),输出用户 ID、未完成试卷作答数、完成试卷作答数、作答过的试卷 tag 集合,按未完成试卷数量由多到少排序。示例数据的输出结果如下: - -| uid | incomplete_cnt | complete_cnt | detail | -| ---- | -------------- | ------------ | --------------------------------------------------------------------------- | -| 1002 | 2 | 4 | 2021-09-01:算法;2021-07-02:SQL;2021-09-02:SQL;2021-09-05:SQL;2021-07-05:SQL | - -**解释**:2021 年的作答记录中,除了 1004,其他用户均满足有效用户定义,但只有 1002 未完成试卷数大于 1,因此只输出 1002,detail 中是 1002 作答过的试卷{日期:tag}集合,日期和 tag 间用 **:** 连接,多元素间用 **;** 连接。 - -**思路:** - -仔细读题后,分析出:首先要联表,因为后面要输出`tag`; - -筛选出 2021 年的数据 - -```sql -SELECT * -FROM exam_record er -LEFT JOIN examination_info ei ON er.exam_id = ei.exam_id -WHERE YEAR (er.start_time)= 2021 -``` - -根据 uid 进行分组,然后对每个用户进行条件进行判断,题目中要求`完成试卷数至少为1,未完成试卷数要大于1,小于5` - -那么等会儿写 sql 的时候条件应该是:`未完成 > 1 and 已完成 >=1 and 未完成 < 5` - -因为最后要用到字符串的拼接,而且还要组合拼接,这个可以用`GROUP_CONCAT`函数,下面简单介绍一下该函数的用法: - -基本格式: - -```sql -GROUP_CONCAT([DISTINCT] expr [ORDER BY {unsigned_integer | col_name | expr} [ASC | DESC] [, ...]] [SEPARATOR sep]) -``` - -- `expr`:要连接的列或表达式。 -- `DISTINCT`:可选参数,用于去重。当指定了 `DISTINCT`,相同的值只会出现一次。 -- `ORDER BY`:可选参数,用于排序连接后的值。可以选择升序 (`ASC`) 或降序 (`DESC`) 排序。 -- `SEPARATOR sep`:可选参数,用于设置连接后的值的分隔符。(本题要用这个参数设置 ; 号 ) - -`GROUP_CONCAT()` 函数常用于 `GROUP BY` 子句中,将一组行的值连接为一个字符串,并在结果集中以聚合的形式返回。 - -**答案**: - -```sql -SELECT a.uid, - SUM(CASE - WHEN a.submit_time IS NULL THEN 1 - END) AS incomplete_cnt, - SUM(CASE - WHEN a.submit_time IS NOT NULL THEN 1 - END) AS complete_cnt, - GROUP_CONCAT(DISTINCT CONCAT(DATE_FORMAT(a.start_time, '%Y-%m-%d'), ':', b.tag) - ORDER BY start_time SEPARATOR ";") AS detail -FROM exam_record a -LEFT JOIN examination_info b ON a.exam_id = b.exam_id -WHERE YEAR (a.start_time)= 2021 -GROUP BY a.uid -HAVING incomplete_cnt > 1 -AND complete_cnt >= 1 -AND incomplete_cnt < 5 -ORDER BY incomplete_cnt DESC -``` - -- `SUM(CASE WHEN a.submit_time IS NULL THEN 1 END)` 统计了每个用户未完成的记录数量。 -- `SUM(CASE WHEN a.submit_time IS NOT NULL THEN 1 END)` 统计了每个用户已完成的记录数量。 -- `GROUP_CONCAT(DISTINCT CONCAT(DATE_FORMAT(a.start_time, '%Y-%m-%d'), ':', b.tag) ORDER BY a.start_time SEPARATOR ';')` 将每个用户的考试日期和标签以逗号分隔的形式连接成一个字符串,并按考试开始时间进行排序。 - -## 嵌套子查询 - -### 月均完成试卷数不小于 3 的用户爱作答的类别(较难) - -**描述**:现有试卷作答记录表 `exam_record`(`uid`:用户 ID, `exam_id`:试卷 ID, `start_time`:开始作答时间, `submit_time`:交卷时间,没提交的话为 NULL, `score`:得分),示例数据如下: - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2021-07-02 09:01:01 | (NULL) | (NULL) | -| 2 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:21:01 | 60 | -| 3 | 1002 | 9002 | 2021-09-02 12:01:01 | 2021-09-02 12:31:01 | 70 | -| 4 | 1002 | 9001 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 81 | -| 5 | 1002 | 9002 | 2021-07-06 12:01:01 | (NULL) | (NULL) | -| 6 | 1003 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 | -| 7 | 1003 | 9003 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 | -| 8 | 1003 | 9001 | 2021-09-08 13:01:01 | (NULL) | (NULL) | -| 9 | 1003 | 9002 | 2021-09-08 14:01:01 | (NULL) | (NULL) | -| 10 | 1003 | 9003 | 2021-09-08 15:01:01 | (NULL) | (NULL) | -| 11 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 | -| 12 | 1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 | -| 13 | 1005 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 | - -试卷信息表 `examination_info`(`exam_id`:试卷 ID, `tag`:试卷类别, `difficulty`:试卷难度, `duration`:考试时长, `release_time`:发布时间),示例数据如下: - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ---- | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | -| 2 | 9002 | C++ | easy | 60 | 2020-02-01 10:00:00 | -| 3 | 9003 | 算法 | medium | 80 | 2020-08-02 10:00:00 | - -请从表中统计出 “当月均完成试卷数”不小于 3 的用户们爱作答的类别及作答次数,按次数降序输出,示例输出如下: - -| tag | tag_cnt | -| ---- | ------- | -| C++ | 4 | -| SQL | 2 | -| 算法 | 1 | - -**解释**:用户 1002 和 1005 在 2021 年 09 月的完成试卷数目均为 3,其他用户均小于 3;然后用户 1002 和 1005 作答过的试卷 tag 分布结果按作答次数降序排序依次为 C++、SQL、算法。 - -**思路**:这题考察联合子查询,重点在于`月均回答>=3`, 但是个人认为这里没有表述清楚,应该直接说查 9 月的就容易理解多了;这里不是每个月都要>=3 或者是所有答题次数/答题月份。不要理解错误了。 - -先查询出哪些用户月均答题大于三次 - -```sql -SELECT UID -FROM exam_record record -GROUP BY UID, - MONTH (start_time) -HAVING count(submit_time) >= 3 -``` - -有了这一步之后再进行深入,只要能理解上一步(我的意思是不被题目中的月均所困扰),然后再套一个子查询,查哪些用户包含其中,然后查出题目中所需的列即可。记得排序!! - -```sql -SELECT tag, - count(start_time) AS tag_cnt -FROM exam_record record -INNER JOIN examination_info info ON record.exam_id = info.exam_id -WHERE UID IN - (SELECT UID - FROM exam_record record - GROUP BY UID, - MONTH (start_time) - HAVING count(submit_time) >= 3) -GROUP BY tag -ORDER BY tag_cnt DESC -``` - -### 试卷发布当天作答人数和平均分 - -**描述**:现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间),示例数据如下: - -| id | uid | nick_name | achievement | level | job | register_time | -| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | -| 1 | 1001 | 牛客 1 号 | 3100 | 7 | 算法 | 2020-01-01 10:00:00 | -| 2 | 1002 | 牛客 2 号 | 2100 | 6 | 算法 | 2020-01-01 10:00:00 | -| 3 | 1003 | 牛客 3 号 | 1500 | 5 | 算法 | 2020-01-01 10:00:00 | -| 4 | 1004 | 牛客 4 号 | 1100 | 4 | 算法 | 2020-01-01 10:00:00 | -| 5 | 1005 | 牛客 5 号 | 1600 | 6 | C++ | 2020-01-01 10:00:00 | -| 6 | 1006 | 牛客 6 号 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | - -**释义**:用户 1001 昵称为牛客 1 号,成就值为 3100,用户等级是 7 级,职业方向为算法,注册时间 2020-01-01 10:00:00 - -试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间) 示例数据如下: - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ---- | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | -| 2 | 9002 | C++ | easy | 60 | 2020-02-01 10:00:00 | -| 3 | 9003 | 算法 | medium | 80 | 2020-08-02 10:00:00 | - -试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分) 示例数据如下: - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2021-07-02 09:01:01 | 2021-09-01 09:41:01 | 70 | -| 2 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:21:01 | 60 | -| 3 | 1002 | 9002 | 2021-09-02 12:01:01 | 2021-09-02 12:31:01 | 70 | -| 4 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 80 | -| 5 | 1002 | 9003 | 2021-08-01 12:01:01 | 2021-08-01 12:21:01 | 60 | -| 6 | 1002 | 9002 | 2021-08-02 12:01:01 | 2021-08-02 12:31:01 | 70 | -| 7 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 85 | -| 8 | 1002 | 9002 | 2021-07-06 12:01:01 | (NULL) | (NULL) | -| 9 | 1003 | 9002 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 | -| 10 | 1003 | 9003 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 | -| 11 | 1003 | 9003 | 2021-09-01 13:01:01 | 2021-09-01 13:41:01 | 70 | -| 12 | 1003 | 9001 | 2021-09-08 14:01:01 | (NULL) | (NULL) | -| 13 | 1003 | 9002 | 2021-09-08 15:01:01 | (NULL) | (NULL) | -| 14 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 90 | -| 15 | 1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 | -| 16 | 1005 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 | - -请计算每张 SQL 类别试卷发布后,当天 5 级以上的用户作答的人数 `uv` 和平均分 `avg_score`,按人数降序,相同人数的按平均分升序,示例数据结果输出如下: - -| exam_id | uv | avg_score | -| ------- | --- | --------- | -| 9001 | 3 | 81.3 | - -解释:只有一张 SQL 类别的试卷,试卷 ID 为 9001,发布当天(2021-09-01)有 1001、1002、1003、1005 作答过,但是 1003 是 5 级用户,其他 3 位为 5 级以上,他们三的得分有[70,80,85,90],平均分为 81.3(保留 1 位小数)。 - -**思路**:这题看似很复杂,但是先逐步将“外边”条件拆分,然后合拢到一起,答案就出来,多表查询反正记住:由外向里,抽丝剥茧。 - -先把三种表连起来,同时给定一些条件,比如题目中要求`等级> 5`的用户,那么可以先查出来 - -```sql -SELECT DISTINCT u_info.uid -FROM examination_info e_info -INNER JOIN exam_record record -INNER JOIN user_info u_info -WHERE e_info.exam_id = record.exam_id - AND u_info.uid = record.uid - AND u_info.LEVEL > 5 -``` - -接着注意题目中要求:`每张sql类别试卷发布后,当天作答用户`,注意其中的==当天==,那我们马上就要想到要用到时间的比较。 - -对试卷发布日期和开始考试日期进行比较:`DATE(e_info.release_time) = DATE(record.start_time)`;不用担心`submit_time` 为 null 的问题,后续在 where 中会给过滤掉。 - -**答案**: - -```sql -SELECT record.exam_id AS exam_id, - COUNT(DISTINCT u_info.uid) AS uv, - ROUND(SUM(record.score) / COUNT(u_info.uid), 1) AS avg_score -FROM examination_info e_info -INNER JOIN exam_record record -INNER JOIN user_info u_info -WHERE e_info.exam_id = record.exam_id - AND u_info.uid = record.uid - AND DATE (e_info.release_time) = DATE (record.start_time) - AND submit_time IS NOT NULL - AND tag = 'SQL' - AND u_info.LEVEL > 5 -GROUP BY record.exam_id -ORDER BY uv DESC, - avg_score ASC -``` - -注意最后的分组排序!先按人数排,若一致,按平均分排。 - -### 作答试卷得分大于过 80 的人的用户等级分布 - -**描述**: - -现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): - -| id | uid | nick_name | achievement | level | job | register_time | -| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | -| 1 | 1001 | 牛客 1 号 | 3100 | 7 | 算法 | 2020-01-01 10:00:00 | -| 2 | 1002 | 牛客 2 号 | 2100 | 6 | 算法 | 2020-01-01 10:00:00 | -| 3 | 1003 | 牛客 3 号 | 1500 | 5 | 算法 | 2020-01-01 10:00:00 | -| 4 | 1004 | 牛客 4 号 | 1100 | 4 | 算法 | 2020-01-01 10:00:00 | -| 5 | 1005 | 牛客 5 号 | 1600 | 6 | C++ | 2020-01-01 10:00:00 | -| 6 | 1006 | 牛客 6 号 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | - -试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ---- | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | -| 2 | 9002 | C++ | easy | 60 | 2021-09-01 06:00:00 | -| 3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 | - -试卷作答信息表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:41:01 | 79 | -| 2 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:21:01 | 60 | -| 3 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 70 | -| 4 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 80 | -| 5 | 1002 | 9003 | 2021-08-01 12:01:01 | 2021-08-01 12:21:01 | 60 | -| 6 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 70 | -| 7 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 85 | -| 8 | 1002 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | -| 9 | 1003 | 9002 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 | -| 10 | 1003 | 9003 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 | -| 11 | 1003 | 9003 | 2021-09-01 13:01:01 | 2021-09-01 13:41:01 | 81 | -| 12 | 1003 | 9001 | 2021-09-01 14:01:01 | (NULL) | (NULL) | -| 13 | 1003 | 9002 | 2021-09-08 15:01:01 | (NULL) | (NULL) | -| 14 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 90 | -| 15 | 1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 | -| 16 | 1005 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 | - -统计作答 SQL 类别的试卷得分大于过 80 的人的用户等级分布,按数量降序排序(保证数量都不同)。示例数据结果输出如下: - -| level | level_cnt | -| ----- | --------- | -| 6 | 2 | -| 5 | 1 | - -解释:9001 为 SQL 类试卷,作答该试卷大于 80 分的人有 1002、1003、1005 共 3 人,6 级两人,5 级一人。 - -**思路:**这题和上一题都是一样的数据,只是查询条件改变了而已,上一题理解了,这题分分钟做出来。 - -**答案**: - -```sql -SELECT u_info.LEVEL AS LEVEL, - count(u_info.uid) AS level_cnt -FROM examination_info e_info -INNER JOIN exam_record record -INNER JOIN user_info u_info -WHERE e_info.exam_id = record.exam_id - AND u_info.uid = record.uid - AND record.score > 80 - AND submit_time IS NOT NULL - AND tag = 'SQL' -GROUP BY LEVEL -ORDER BY level_cnt DESC -``` - -## 合并查询 - -### 每个题目和每份试卷被作答的人数和次数 - -**描述**: - -现有试卷作答记录表 exam_record(uid 用户 ID, exam_id 试卷 ID, start_time 开始作答时间, submit_time 交卷时间, score 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:41:01 | 81 | -| 2 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 70 | -| 3 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 80 | -| 4 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 70 | -| 5 | 1004 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 85 | -| 6 | 1002 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | - -题目练习表 practice_record(uid 用户 ID, question_id 题目 ID, submit_time 提交时间, score 得分): - -| id | uid | question_id | submit_time | score | -| --- | ---- | ----------- | ------------------- | ----- | -| 1 | 1001 | 8001 | 2021-08-02 11:41:01 | 60 | -| 2 | 1002 | 8001 | 2021-09-02 19:30:01 | 50 | -| 3 | 1002 | 8001 | 2021-09-02 19:20:01 | 70 | -| 4 | 1002 | 8002 | 2021-09-02 19:38:01 | 70 | -| 5 | 1003 | 8001 | 2021-08-02 19:38:01 | 70 | -| 6 | 1003 | 8001 | 2021-08-02 19:48:01 | 90 | -| 7 | 1003 | 8002 | 2021-08-01 19:38:01 | 80 | - -请统计每个题目和每份试卷被作答的人数和次数,分别按照"试卷"和"题目"的 uv & pv 降序显示,示例数据结果输出如下: - -| tid | uv | pv | -| ---- | --- | --- | -| 9001 | 3 | 3 | -| 9002 | 1 | 3 | -| 8001 | 3 | 5 | -| 8002 | 2 | 2 | - -**解释**:“试卷”有 3 人共练习 3 次试卷 9001,1 人作答 3 次 9002;“刷题”有 3 人刷 5 次 8001,有 2 人刷 2 次 8002 - -**思路**:这题的难点和易错点在于`UNION`和`ORDER BY` 同时使用的问题 - -有以下几种情况:使用`union`和多个`order by`不加括号,报错! - -`order by`在`union`连接的子句中不起作用; - -比如不加括号: - -```sql -SELECT exam_id AS tid, - COUNT(DISTINCT UID) AS uv, - COUNT(UID) AS pv -FROM exam_record -GROUP BY exam_id -ORDER BY uv DESC, - pv DESC -UNION -SELECT question_id AS tid, - COUNT(DISTINCT UID) AS uv, - COUNT(UID) AS pv -FROM practice_record -GROUP BY question_id -ORDER BY uv DESC, - pv DESC -``` - -直接报语法错误,如果没有括号,只能有一个`order by` - -还有一种`order by`不起作用的情况,但是能在子句的子句中起作用,这里的解决方案就是在外面再套一层查询。 - -**答案**: - -```sql -SELECT * -FROM - (SELECT exam_id AS tid, - COUNT(DISTINCT exam_record.uid) uv, - COUNT(*) pv - FROM exam_record - GROUP BY exam_id - ORDER BY uv DESC, pv DESC) t1 -UNION -SELECT * -FROM - (SELECT question_id AS tid, - COUNT(DISTINCT practice_record.uid) uv, - COUNT(*) pv - FROM practice_record - GROUP BY question_id - ORDER BY uv DESC, pv DESC) t2; -``` - -### 分别满足两个活动的人 - -**描述**: 为了促进更多用户在牛客平台学习和刷题进步,我们会经常给一些既活跃又表现不错的用户发放福利。假使以前我们有两拨运营活动,分别给每次试卷得分都能到 85 分的人(activity1)、至少有一次用了一半时间就完成高难度试卷且分数大于 80 的人(activity2)发了福利券。 - -现在,需要你一次性将这两个活动满足的人筛选出来,交给运营同学。请写出一个 SQL 实现:输出 2021 年里,所有每次试卷得分都能到 85 分的人以及至少有一次用了一半时间就完成高难度试卷且分数大于 80 的人的 id 和活动号,按用户 ID 排序输出。 - -现有试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ---- | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | -| 2 | 9002 | C++ | easy | 60 | 2021-09-01 06:00:00 | -| 3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 | - -试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 | -| 2 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 70 | -| 3 | 1003 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | **86** | -| 4 | 1003 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 89 | -| 5 | 1004 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 | - -示例数据输出结果: - -| uid | activity | -| ---- | --------- | -| 1001 | activity2 | -| 1003 | activity1 | -| 1004 | activity1 | -| 1004 | activity2 | - -**解释**:用户 1001 最小分数 81 不满足活动 1,但 29 分 59 秒完成了 60 分钟长的试卷得分 81,满足活动 2;1003 最小分数 86 满足活动 1,完成时长都大于试卷时长的一半,不满足活动 2;用户 1004 刚好用了一半时间(30 分钟整)完成了试卷得分 85,满足活动 1 和活动 2。 - -**思路**: 这一题需要涉及到时间的减法,需要用到 `TIMESTAMPDIFF()` 函数计算两个时间戳之间的分钟差值。 - -下面我们来看一下基本用法 - -示例: - -```sql -TIMESTAMPDIFF(MINUTE, start_time, end_time) -``` - -`TIMESTAMPDIFF()` 函数的第一个参数是时间单位,这里我们选择 `MINUTE` 表示返回分钟差值。第二个参数是较早的时间戳,第三个参数是较晚的时间戳。函数会返回它们之间的分钟差值 - -了解了这个函数的用法之后,我们再回过头来看`activity1`的要求,求分数大于 85 即可,那我们还是先把这个写出来,后续思路就会清晰很多 - -```sql -SELECT DISTINCT UID -FROM exam_record -WHERE score >= 85 - AND YEAR (start_time) = '2021' -``` - -根据条件 2,接着写出`在一半时间内完成高难度试卷且分数大于80的人` - -```sql -SELECT UID -FROM examination_info info -INNER JOIN exam_record record -WHERE info.exam_id = record.exam_id - AND (TIMESTAMPDIFF(MINUTE, start_time, submit_time)) < (info.duration / 2) - AND difficulty = 'hard' - AND score >= 80 -``` - -然后再把两者`UNION` 起来即可。(这里特别要注意括号问题和`order by`位置,具体用法在上一篇中已提及) - -**答案**: - -```sql -SELECT DISTINCT UID UID, - 'activity1' activity -FROM exam_record -WHERE UID not in - (SELECT UID - FROM exam_record - WHERE score<85 - AND YEAR(submit_time) = 2021 ) -UNION -SELECT DISTINCT UID UID, - 'activity2' activity -FROM exam_record e_r -LEFT JOIN examination_info e_i ON e_r.exam_id = e_i.exam_id -WHERE YEAR(submit_time) = 2021 - AND difficulty = 'hard' - AND TIMESTAMPDIFF(SECOND, start_time, submit_time) <= duration *30 - AND score>80 -ORDER BY UID -``` - -## 连接查询 - -### 满足条件的用户的试卷完成数和题目练习数(困难) - -**描述**: - -现有用户信息表 user_info(uid 用户 ID,nick_name 昵称, achievement 成就值, level 等级, job 职业方向, register_time 注册时间): - -| id | uid | nick_name | achievement | level | job | register_time | -| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | -| 1 | 1001 | 牛客 1 号 | 3100 | 7 | 算法 | 2020-01-01 10:00:00 | -| 2 | 1002 | 牛客 2 号 | 2300 | 7 | 算法 | 2020-01-01 10:00:00 | -| 3 | 1003 | 牛客 3 号 | 2500 | 7 | 算法 | 2020-01-01 10:00:00 | -| 4 | 1004 | 牛客 4 号 | 1200 | 5 | 算法 | 2020-01-01 10:00:00 | -| 5 | 1005 | 牛客 5 号 | 1600 | 6 | C++ | 2020-01-01 10:00:00 | -| 6 | 1006 | 牛客 6 号 | 2000 | 6 | C++ | 2020-01-01 10:00:00 | - -试卷信息表 examination_info(exam_id 试卷 ID, tag 试卷类别, difficulty 试卷难度, duration 考试时长, release_time 发布时间): - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ---- | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | -| 2 | 9002 | C++ | hard | 60 | 2021-09-01 06:00:00 | -| 3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 | - -试卷作答记录表 exam_record(uid 用户 ID, exam_id 试卷 ID, start_time 开始作答时间, submit_time 交卷时间, score 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ----- | -| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 | -| 2 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 | -| 3 | 1003 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 86 | -| 4 | 1003 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:51 | 89 | -| 5 | 1004 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 | -| 6 | 1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:02 | 85 | -| 7 | 1006 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:21:01 | 84 | -| 8 | 1006 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:21:01 | 80 | - -题目练习记录表 practice_record(uid 用户 ID, question_id 题目 ID, submit_time 提交时间, score 得分): - -| id | uid | question_id | submit_time | score | -| --- | ---- | ----------- | ------------------- | ----- | -| 1 | 1001 | 8001 | 2021-08-02 11:41:01 | 60 | -| 2 | 1002 | 8001 | 2021-09-02 19:30:01 | 50 | -| 3 | 1002 | 8001 | 2021-09-02 19:20:01 | 70 | -| 4 | 1002 | 8002 | 2021-09-02 19:38:01 | 70 | -| 5 | 1004 | 8001 | 2021-08-02 19:38:01 | 70 | -| 6 | 1004 | 8002 | 2021-08-02 19:48:01 | 90 | -| 7 | 1001 | 8002 | 2021-08-02 19:38:01 | 70 | -| 8 | 1004 | 8002 | 2021-08-02 19:48:01 | 90 | -| 9 | 1004 | 8002 | 2021-08-02 19:58:01 | 94 | -| 10 | 1004 | 8003 | 2021-08-02 19:38:01 | 70 | -| 11 | 1004 | 8003 | 2021-08-02 19:48:01 | 90 | -| 12 | 1004 | 8003 | 2021-08-01 19:38:01 | 80 | - -请你找到高难度 SQL 试卷得分平均值大于 80 并且是 7 级的红名大佬,统计他们的 2021 年试卷总完成次数和题目总练习次数,只保留 2021 年有试卷完成记录的用户。结果按试卷完成数升序,按题目练习数降序。 - -示例数据输出如下: - -| uid | exam_cnt | question_cnt | -| ---- | -------- | ------------ | -| 1001 | 1 | 2 | -| 1003 | 2 | 0 | - -解释:用户 1001、1003、1004、1006 满足高难度 SQL 试卷得分平均值大于 80,但只有 1001、1003 是 7 级红名大佬;1001 完成了 1 次试卷 1001,练习了 2 次题目;1003 完成了 2 次试卷 9001、9002,未练习题目(因此计数为 0) - -**思路:** - -先将条件进行初步筛选,比如先查出做过高难度 sql 试卷的用户 - -```sql -SELECT - record.uid -FROM - exam_record record - INNER JOIN examination_info e_info ON record.exam_id = e_info.exam_id - JOIN user_info u_info ON record.uid = u_info.uid -WHERE - e_info.tag = 'SQL' - AND e_info.difficulty = 'hard' -``` - -然后根据题目要求,接着再往里叠条件即可; - -但是这里又要注意: - -第一:不能`YEAR(submit_time)= 2021`这个条件放到最后,要在`ON`条件里,因为左连接存在返回左表全部行,右表为 null 的情形,放在 `JOIN`条件的 `ON` 子句中的目的是为了确保在连接两个表时,只有满足年份条件的记录会进行连接。这样可以避免其他年份的记录被包含在结果中。即 1001 做过 2021 年的试卷,但没有练习过,如果把条件放到最后,就会排除掉这种情况。 - -第二,必须是`COUNT(distinct er.exam_id) exam_cnt, COUNT(distinct pr.id) question_cnt,`要加 distinct,因为有左连接产生很多重复值。 - -**答案**: - -```sql -SELECT er.uid AS UID, - count(DISTINCT er.exam_id) AS exam_cnt, - count(DISTINCT pr.id) AS question_cnt -FROM exam_record er -LEFT JOIN practice_record pr ON er.uid = pr.uid -AND YEAR (er.submit_time)= 2021 -AND YEAR (pr.submit_time)= 2021 -WHERE er.uid IN - (SELECT er.uid - FROM exam_record er - LEFT JOIN examination_info ei ON er.exam_id = ei.exam_id - LEFT JOIN user_info ui ON er.uid = ui.uid - WHERE tag = 'SQL' - AND difficulty = 'hard' - AND LEVEL = 7 - GROUP BY er.uid - HAVING avg(score) > 80) -GROUP BY er.uid -ORDER BY exam_cnt, - question_cnt DESC -``` - -可能细心的小伙伴会发现,为什么明明将条件限制了`tag = 'SQL' AND difficulty = 'hard'`,但是用户 1003 仍然能查出两条考试记录,其中一条的考试`tag`为 `C++`; 这是由于`LEFT JOIN`的特性,即使没有与右表匹配的行,左表的所有记录仍然会被保留。 - -### 每个 6/7 级用户活跃情况(困难) - -**描述**: - -现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): - -| id | uid | nick_name | achievement | level | job | register_time | -| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | -| 1 | 1001 | 牛客 1 号 | 3100 | 7 | 算法 | 2020-01-01 10:00:00 | -| 2 | 1002 | 牛客 2 号 | 2300 | 7 | 算法 | 2020-01-01 10:00:00 | -| 3 | 1003 | 牛客 3 号 | 2500 | 7 | 算法 | 2020-01-01 10:00:00 | -| 4 | 1004 | 牛客 4 号 | 1200 | 5 | 算法 | 2020-01-01 10:00:00 | -| 5 | 1005 | 牛客 5 号 | 1600 | 6 | C++ | 2020-01-01 10:00:00 | -| 6 | 1006 | 牛客 6 号 | 2600 | 7 | C++ | 2020-01-01 10:00:00 | - -试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ---- | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | -| 2 | 9002 | C++ | easy | 60 | 2021-09-01 06:00:00 | -| 3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 | - -试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): - -| uid | exam_id | start_time | submit_time | score | -| ---- | ------- | ------------------- | ------------------- | ------ | -| 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 78 | -| 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 | -| 1005 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 | -| 1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:02 | 85 | -| 1006 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:21:59 | 84 | -| 1006 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:21:01 | 81 | -| 1002 | 9001 | 2020-09-01 13:01:01 | 2020-09-01 13:41:01 | 81 | -| 1005 | 9001 | 2021-09-01 14:01:01 | (NULL) | (NULL) | - -题目练习记录表 `practice_record`(`uid` 用户 ID, `question_id` 题目 ID, `submit_time` 提交时间, `score` 得分): - -| uid | question_id | submit_time | score | -| ---- | ----------- | ------------------- | ----- | -| 1001 | 8001 | 2021-08-02 11:41:01 | 60 | -| 1004 | 8001 | 2021-08-02 19:38:01 | 70 | -| 1004 | 8002 | 2021-08-02 19:48:01 | 90 | -| 1001 | 8002 | 2021-08-02 19:38:01 | 70 | -| 1004 | 8002 | 2021-08-02 19:48:01 | 90 | -| 1006 | 8002 | 2021-08-04 19:58:01 | 94 | -| 1006 | 8003 | 2021-08-03 19:38:01 | 70 | -| 1006 | 8003 | 2021-08-02 19:48:01 | 90 | -| 1006 | 8003 | 2020-08-01 19:38:01 | 80 | - -请统计每个 6/7 级用户总活跃月份数、2021 年活跃天数、2021 年试卷作答活跃天数、2021 年答题活跃天数,按照总活跃月份数、2021 年活跃天数降序排序。由示例数据结果输出如下: - -| uid | act_month_total | act_days_2021 | act_days_2021_exam | -| ---- | --------------- | ------------- | ------------------ | -| 1006 | 3 | 4 | 1 | -| 1001 | 2 | 2 | 1 | -| 1005 | 1 | 1 | 1 | -| 1002 | 1 | 0 | 0 | -| 1003 | 0 | 0 | 0 | - -**解释**:6/7 级用户共有 5 个,其中 1006 在 202109、202108、202008 共 3 个月活跃过,2021 年活跃的日期有 20210907、20210804、20210803、20210802 共 4 天,2021 年在试卷作答区 20210907 活跃 1 天,在题目练习区活跃了 3 天。 - -**思路:** - -这题的关键在于`CASE WHEN THEN`的使用,不然要写很多的`left join` 因为会产生很多的结果集。 - -`CASE WHEN THEN`语句是一种条件表达式,用于在 SQL 中根据条件执行不同的操作或返回不同的结果。 - -语法结构如下: - -```sql -CASE - WHEN condition1 THEN result1 - WHEN condition2 THEN result2 - ... - ELSE result -END -``` - -在这个结构中,可以根据需要添加多个`WHEN`子句,每个`WHEN`子句后面跟着一个条件(condition)和一个结果(result)。条件可以是任何逻辑表达式,如果满足条件,将返回对应的结果。 - -最后的`ELSE`子句是可选的,用于指定当所有前面的条件都不满足时的默认返回结果。如果没有提供`ELSE`子句,则默认返回`NULL`。 - -例如: - -```sql -SELECT score, - CASE - WHEN score >= 90 THEN '优秀' - WHEN score >= 80 THEN '良好' - WHEN score >= 60 THEN '及格' - ELSE '不及格' - END AS grade -FROM student_scores; -``` - -在上述示例中,根据学生成绩(score)的不同范围,使用 CASE WHEN THEN 语句返回相应的等级(grade)。如果成绩大于等于 90,则返回"优秀";如果成绩大于等于 80,则返回"良好";如果成绩大于等于 60,则返回"及格";否则返回"不及格"。 - -那了解到了上述的用法之后,回过头看看该题,要求列出不同的活跃天数。 - -```sql -count(distinct act_month) as act_month_total, -count(distinct case when year(act_time)='2021'then act_day end) as act_days_2021, -count(distinct case when year(act_time)='2021' and tag='exam' then act_day end) as act_days_2021_exam, -count(distinct case when year(act_time)='2021' and tag='question'then act_day end) as act_days_2021_question -``` - -这里的 tag 是先给标记,方便对查询进行区分,将考试和答题分开。 - -找出试卷作答区的用户 - -```sql -SELECT - uid, - exam_id AS ans_id, - start_time AS act_time, - date_format( start_time, '%Y%m' ) AS act_month, - date_format( start_time, '%Y%m%d' ) AS act_day, - 'exam' AS tag - FROM - exam_record -``` - -紧接着就是答题作答区的用户 - -```sql -SELECT - uid, - question_id AS ans_id, - submit_time AS act_time, - date_format( submit_time, '%Y%m' ) AS act_month, - date_format( submit_time, '%Y%m%d' ) AS act_day, - 'question' AS tag - FROM - practice_record -``` - -最后将两个结果进行`UNION` 最后别忘了将结果进行排序 (这题有点类似于分治法的思想) - -**答案**: - -```sql -SELECT user_info.uid, - count(DISTINCT act_month) AS act_month_total, - count(DISTINCT CASE - WHEN YEAR (act_time)= '2021' THEN act_day - END) AS act_days_2021, - count(DISTINCT CASE - WHEN YEAR (act_time)= '2021' - AND tag = 'exam' THEN act_day - END) AS act_days_2021_exam, - count(DISTINCT CASE - WHEN YEAR (act_time)= '2021' - AND tag = 'question' THEN act_day - END) AS act_days_2021_question -FROM - (SELECT UID, - exam_id AS ans_id, - start_time AS act_time, - date_format(start_time, '%Y%m') AS act_month, - date_format(start_time, '%Y%m%d') AS act_day, - 'exam' AS tag - FROM exam_record - UNION ALL SELECT UID, - question_id AS ans_id, - submit_time AS act_time, - date_format(submit_time, '%Y%m') AS act_month, - date_format(submit_time, '%Y%m%d') AS act_day, - 'question' AS tag - FROM practice_record) total -RIGHT JOIN user_info ON total.uid = user_info.uid -WHERE user_info.LEVEL IN (6, - 7) -GROUP BY user_info.uid -ORDER BY act_month_total DESC, - act_days_2021 DESC -``` - - +**Approach 1:** To find high-difficulty SQL papers, you need to join the `examination_info` table and find the high-difficulty courses. From `examination_info`, we know that the exam_id for high-difficulty SQL is 900 diff --git a/docs/database/sql/sql-questions-04.md b/docs/database/sql/sql-questions-04.md index 84f1a2b3c8c..8d60b4e8ecc 100644 --- a/docs/database/sql/sql-questions-04.md +++ b/docs/database/sql/sql-questions-04.md @@ -1,55 +1,55 @@ --- -title: SQL常见面试题总结(4) -category: 数据库 +title: Summary of Common SQL Interview Questions (4) +category: Database tag: - - 数据库基础 + - Database Basics - SQL --- -> 题目来源于:[牛客题霸 - SQL 进阶挑战](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) +> Source of the questions: [Niuke Question Bank - SQL Advanced Challenge](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) -较难或者困难的题目可以根据自身实际情况和面试需要来决定是否要跳过。 +For more difficult questions, you can decide whether to skip them based on your actual situation and interview needs. -## 专用窗口函数 +## Specialized Window Functions -MySQL 8.0 版本引入了窗口函数的支持,下面是 MySQL 中常见的窗口函数及其用法: +MySQL 8.0 introduced support for window functions. Below are common window functions in MySQL and their usage: -1. `ROW_NUMBER()`: 为查询结果集中的每一行分配一个唯一的整数值。 +1. `ROW_NUMBER()`: Assigns a unique integer value to each row in the result set. ```sql SELECT col1, col2, ROW_NUMBER() OVER (ORDER BY col1) AS row_num FROM table; ``` -2. `RANK()`: 计算每一行在排序结果中的排名。 +2. `RANK()`: Calculates the rank of each row in the sorted result. ```sql SELECT col1, col2, RANK() OVER (ORDER BY col1 DESC) AS ranking FROM table; ``` -3. `DENSE_RANK()`: 计算每一行在排序结果中的排名,保留相同的排名。 +3. `DENSE_RANK()`: Calculates the rank of each row in the sorted result, preserving the same rank for ties. ```sql SELECT col1, col2, DENSE_RANK() OVER (ORDER BY col1 DESC) AS ranking FROM table; ``` -4. `NTILE(n)`: 将结果分成 n 个基本均匀的桶,并为每个桶分配一个标识号。 +4. `NTILE(n)`: Divides the result into n evenly distributed buckets and assigns an identifier to each bucket. ```sql SELECT col1, col2, NTILE(4) OVER (ORDER BY col1) AS bucket FROM table; ``` -5. `SUM()`, `AVG()`,`COUNT()`, `MIN()`, `MAX()`: 这些聚合函数也可以与窗口函数结合使用,计算窗口内指定列的汇总、平均值、计数、最小值和最大值。 +5. `SUM()`, `AVG()`, `COUNT()`, `MIN()`, `MAX()`: These aggregate functions can also be used with window functions to calculate the sum, average, count, minimum, and maximum of specified columns within the window. ```sql SELECT col1, col2, SUM(col1) OVER () AS sum_col FROM table; ``` -6. `LEAD()` 和 `LAG()`: LEAD 函数用于获取当前行之后的某个偏移量的行的值,而 LAG 函数用于获取当前行之前的某个偏移量的行的值。 +6. `LEAD()` and `LAG()`: The LEAD function is used to get the value of a row at a specified offset after the current row, while the LAG function is used to get the value of a row at a specified offset before the current row. ```sql SELECT col1, col2, LEAD(col1, 1) OVER (ORDER BY col1) AS next_col1, @@ -57,7 +57,7 @@ SELECT col1, col2, LEAD(col1, 1) OVER (ORDER BY col1) AS next_col1, FROM table; ``` -7. `FIRST_VALUE()` 和 `LAST_VALUE()`: FIRST_VALUE 函数用于获取窗口内指定列的第一个值,LAST_VALUE 函数用于获取窗口内指定列的最后一个值。 +7. `FIRST_VALUE()` and `LAST_VALUE()`: The FIRST_VALUE function retrieves the first value of a specified column within the window, while the LAST_VALUE function retrieves the last value of a specified column within the window. ```sql SELECT col1, col2, FIRST_VALUE(col2) OVER (PARTITION BY col1 ORDER BY col2) AS first_val, @@ -65,768 +65,24 @@ SELECT col1, col2, FIRST_VALUE(col2) OVER (PARTITION BY col1 ORDER BY col2) AS f FROM table; ``` -窗口函数通常需要配合 OVER 子句一起使用,用于定义窗口的大小、排序规则和分组方式。 +Window functions typically need to be used in conjunction with the OVER clause to define the size of the window, sorting rules, and grouping methods. -### 每类试卷得分前三名 +### Top Three Scores for Each Exam Type -**描述**: +**Description**: -现有试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): +There is an exam information table `examination_info` (`exam_id` Exam ID, `tag` Exam Category, `difficulty` Exam Difficulty, `duration` Exam Duration, `release_time` Release Time): -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ---- | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | -| 2 | 9002 | SQL | hard | 60 | 2021-09-01 06:00:00 | -| 3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 | +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | --------- | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | +| 2 | 9002 | SQL | hard | 60 | 2021-09-01 06:00:00 | +| 3 | 9003 | Algorithm | medium | 80 | 2021-09-01 10:00:00 | -试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, score 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 78 | -| 2 | 1002 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 | -| 3 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 | -| 4 | 1003 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 86 | -| 5 | 1003 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:51 | 89 | -| 6 | 1004 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 | -| 7 | 1005 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:31:02 | 85 | -| 8 | 1006 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:21:01 | 84 | -| 9 | 1003 | 9003 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 | -| 10 | 1003 | 9002 | 2021-09-01 14:01:01 | (NULL) | (NULL) | - -找到每类试卷得分的前 3 名,如果两人最大分数相同,选择最小分数大者,如果还相同,选择 uid 大者。由示例数据结果输出如下: - -| tid | uid | ranking | -| ---- | ---- | ------- | -| SQL | 1003 | 1 | -| SQL | 1004 | 2 | -| SQL | 1002 | 3 | -| 算法 | 1005 | 1 | -| 算法 | 1006 | 2 | -| 算法 | 1003 | 3 | - -**解释**:有作答得分记录的试卷 tag 有 SQL 和算法,SQL 试卷用户 1001、1002、1003、1004 有作答得分,最高得分分别为 81、81、89、85,最低得分分别为 78、81、86、40,因此先按最高得分排名再按最低得分排名取前三为 1003、1004、1002。 - -**答案**: - -```sql -SELECT tag, - UID, - ranking -FROM - (SELECT b.tag AS tag, - a.uid AS UID, - ROW_NUMBER() OVER (PARTITION BY b.tag - ORDER BY b.tag, - max(a.score) DESC, - min(a.score) DESC, - a.uid DESC) AS ranking - FROM exam_record a - LEFT JOIN examination_info b ON a.exam_id = b.exam_id - GROUP BY b.tag, - a.uid) t -WHERE ranking <= 3 -``` - -### 第二快/慢用时之差大于试卷时长一半的试卷(较难) - -**描述**: - -现有试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ---- | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | -| 2 | 9002 | C++ | hard | 60 | 2021-09-01 06:00:00 | -| 3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 | - -试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| ---- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:51:01 | 78 | -| 2 | 1001 | 9002 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 | -| 3 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 | -| 4 | 1003 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:59:01 | 86 | -| 5 | 1003 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:51 | 89 | -| 6 | 1004 | 9002 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 | -| 7 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:02 | 85 | -| 8 | 1006 | 9001 | 2021-09-07 10:02:01 | 2021-09-07 10:21:01 | 84 | -| 9 | 1003 | 9001 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 | -| 10 | 1003 | 9002 | 2021-09-01 14:01:01 | (NULL) | (NULL) | -| 11 | 1005 | 9001 | 2021-09-01 14:01:01 | (NULL) | (NULL) | -| 12 | 1003 | 9003 | 2021-09-08 15:01:01 | (NULL) | (NULL) | - -找到第二快和第二慢用时之差大于试卷时长的一半的试卷信息,按试卷 ID 降序排序。由示例数据结果输出如下: - -| exam_id | duration | release_time | -| ------- | -------- | ------------------- | -| 9001 | 60 | 2021-09-01 06:00:00 | - -**解释**:试卷 9001 被作答用时有 50 分钟、58 分钟、30 分 1 秒、19 分钟、10 分钟,第二快和第二慢用时之差为 50 分钟-19 分钟=31 分钟,试卷时长为 60 分钟,因此满足大于试卷时长一半的条件,输出试卷 ID、时长、发布时间。 - -**思路:** - -第一步,找到每张试卷完成时间的顺序排名和倒序排名 也就是表 a; - -第二步,与通过试卷信息表 b 建立内连接,并根据试卷 id 分组,利用`having`筛选排名为第二个数据,将秒转化为分钟并进行比较,最后再根据试卷 id 倒序排序就行 - -**答案**: - -```sql -SELECT a.exam_id, - b.duration, - b.release_time -FROM - (SELECT exam_id, - row_number() OVER (PARTITION BY exam_id - ORDER BY timestampdiff(SECOND, start_time, submit_time) DESC) rn1, - row_number() OVER (PARTITION BY exam_id - ORDER BY timestampdiff(SECOND, start_time, submit_time) ASC) rn2, - timestampdiff(SECOND, start_time, submit_time) timex - FROM exam_record - WHERE score IS NOT NULL ) a -INNER JOIN examination_info b ON a.exam_id = b.exam_id -GROUP BY a.exam_id -HAVING (max(IF (rn1 = 2, a.timex, 0))- max(IF (rn2 = 2, a.timex, 0)))/ 60 > b.duration / 2 -ORDER BY a.exam_id DESC -``` - -### 连续两次作答试卷的最大时间窗(较难) - -**描述** - -现有试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ----- | -| 1 | 1006 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:21:02 | 84 | -| 2 | 1006 | 9001 | 2021-09-01 12:11:01 | 2021-09-01 12:31:01 | 89 | -| 3 | 1006 | 9002 | 2021-09-06 10:01:01 | 2021-09-06 10:21:01 | 81 | -| 4 | 1005 | 9002 | 2021-09-05 10:01:01 | 2021-09-05 10:21:01 | 81 | -| 5 | 1005 | 9001 | 2021-09-05 10:31:01 | 2021-09-05 10:51:01 | 81 | - -请计算在 2021 年至少有两天作答过试卷的人中,计算该年连续两次作答试卷的最大时间窗 `days_window`,那么根据该年的历史规律他在 `days_window` 天里平均会做多少套试卷,按最大时间窗和平均做答试卷套数倒序排序。由示例数据结果输出如下: - -| uid | days_window | avg_exam_cnt | -| ---- | ----------- | ------------ | -| 1006 | 6 | 2.57 | - -**解释**:用户 1006 分别在 20210901、20210906、20210907 作答过 3 次试卷,连续两次作答最大时间窗为 6 天(1 号到 6 号),他 1 号到 7 号这 7 天里共做了 3 张试卷,平均每天 3/7=0.428571 张,那么 6 天里平均会做 0.428571\*6=2.57 张试卷(保留两位小数);用户 1005 在 20210905 做了两张试卷,但是只有一天的作答记录,过滤掉。 - -**思路:** - -上面这个解释中提示要对作答记录去重,千万别被骗了,不要去重!去重就通不过测试用例。注意限制时间是 2021 年; - -而且要注意时间差要+1 天;还要注意==没交卷也算在内==!!!! (反正感觉这题描述不清,出的不是很好) - -**答案**: - -```sql -SELECT UID, - max(datediff(next_time, start_time)) + 1 AS days_window, - round(count(start_time)/(datediff(max(start_time), min(start_time))+ 1) * (max(datediff(next_time, start_time))+ 1), 2) AS avg_exam_cnt -FROM - (SELECT UID, - start_time, - lead(start_time, 1) OVER (PARTITION BY UID - ORDER BY start_time) AS next_time - FROM exam_record - WHERE YEAR (start_time) = '2021' ) a -GROUP BY UID -HAVING count(DISTINCT date(start_time)) > 1 -ORDER BY days_window DESC, - avg_exam_cnt DESC -``` - -### 近三个月未完成为 0 的用户完成情况 - -**描述**: - -现有试卷作答记录表 `exam_record`(`uid`:用户 ID, `exam_id`:试卷 ID, `start_time`:开始作答时间, `submit_time`:交卷时间,为空的话则代表未完成, `score`:得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1006 | 9003 | 2021-09-06 10:01:01 | 2021-09-06 10:21:02 | 84 | -| 2 | 1006 | 9001 | 2021-08-02 12:11:01 | 2021-08-02 12:31:01 | 89 | -| 3 | 1006 | 9002 | 2021-06-06 10:01:01 | 2021-06-06 10:21:01 | 81 | -| 4 | 1006 | 9002 | 2021-05-06 10:01:01 | 2021-05-06 10:21:01 | 81 | -| 5 | 1006 | 9001 | 2021-05-01 12:01:01 | (NULL) | (NULL) | -| 6 | 1001 | 9001 | 2021-09-05 10:31:01 | 2021-09-05 10:51:01 | 81 | -| 7 | 1001 | 9003 | 2021-08-01 09:01:01 | 2021-08-01 09:51:11 | 78 | -| 8 | 1001 | 9002 | 2021-07-01 09:01:01 | 2021-07-01 09:31:00 | 81 | -| 9 | 1001 | 9002 | 2021-07-01 12:01:01 | 2021-07-01 12:31:01 | 81 | -| 10 | 1001 | 9002 | 2021-07-01 12:01:01 | (NULL) | (NULL) | - -找到每个人近三个有试卷作答记录的月份中没有试卷是未完成状态的用户的试卷作答完成数,按试卷完成数和用户 ID 降序排名。由示例数据结果输出如下: - -| uid | exam_complete_cnt | -| ---- | ----------------- | -| 1006 | 3 | - -**解释**:用户 1006 近三个有作答试卷的月份为 202109、202108、202106,作答试卷数为 3,全部完成;用户 1001 近三个有作答试卷的月份为 202109、202108、202107,作答试卷数为 5,完成试卷数为 4,因为有未完成试卷,故过滤掉。 - -**思路:** - -1. `找到每个人近三个有试卷作答记录的月份中没有试卷是未完成状态的用户的试卷作答完成数`首先看这句话,肯定要先根据人进行分组 -2. 最近三个月,可以采用连续重复排名,倒序排列,排名<=3 -3. 统计作答数 -4. 拼装剩余条件 -5. 排序 - -**答案**: - -```sql -SELECT UID, - count(score) exam_complete_cnt -FROM - (SELECT *, DENSE_RANK() OVER (PARTITION BY UID - ORDER BY date_format(start_time, '%Y%m') DESC) dr - FROM exam_record) t1 -WHERE dr <= 3 -GROUP BY UID -HAVING count(dr)= count(score) -ORDER BY exam_complete_cnt DESC, - UID DESC -``` - -### 未完成率较高的 50%用户近三个月答卷情况(困难) - -**描述**: - -现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): - -| id | uid | nick_name | achievement | level | job | register_time | -| --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | -| 1 | 1001 | 牛客 1 号 | 3200 | 7 | 算法 | 2020-01-01 10:00:00 | -| 2 | 1002 | 牛客 2 号 | 2500 | 6 | 算法 | 2020-01-01 10:00:00 | -| 3 | 1003 | 牛客 3 号 ♂ | 2200 | 5 | 算法 | 2020-01-01 10:00:00 | - -试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ------ | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | -| 2 | 9002 | SQL | hard | 80 | 2020-01-01 10:00:00 | -| 3 | 9003 | 算法 | hard | 80 | 2020-01-01 10:00:00 | -| 4 | 9004 | PYTHON | medium | 70 | 2020-01-01 10:00:00 | - -试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ----- | -| 1 | 1001 | 9001 | 2020-01-01 09:01:01 | 2020-01-01 09:21:59 | 90 | -| 15 | 1002 | 9001 | 2020-01-01 18:01:01 | 2020-01-01 18:59:02 | 90 | -| 13 | 1001 | 9001 | 2020-01-02 10:01:01 | 2020-01-02 10:31:01 | 89 | -| 2 | 1002 | 9001 | 2020-01-20 10:01:01 | | | -| 3 | 1002 | 9001 | 2020-02-01 12:11:01 | | | -| 5 | 1001 | 9001 | 2020-03-01 12:01:01 | | | -| 6 | 1002 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:41:01 | 90 | -| 4 | 1003 | 9001 | 2020-03-01 19:01:01 | | | -| 7 | 1002 | 9001 | 2020-05-02 19:01:01 | 2020-05-02 19:32:00 | 90 | -| 14 | 1001 | 9002 | 2020-01-01 12:11:01 | | | -| 8 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:59:01 | 69 | -| 9 | 1001 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:20:01 | 99 | -| 10 | 1002 | 9002 | 2020-02-02 12:01:01 | | | -| 11 | 1002 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:43:01 | 81 | -| 12 | 1002 | 9002 | 2020-03-02 12:11:01 | | | -| 17 | 1001 | 9002 | 2020-05-05 18:01:01 | | | -| 16 | 1002 | 9003 | 2020-05-06 12:01:01 | | | - -请统计 SQL 试卷上未完成率较高的 50%用户中,6 级和 7 级用户在有试卷作答记录的近三个月中,每个月的答卷数目和完成数目。按用户 ID、月份升序排序。 - -由示例数据结果输出如下: - -| uid | start_month | total_cnt | complete_cnt | -| ---- | ----------- | --------- | ------------ | -| 1002 | 202002 | 3 | 1 | -| 1002 | 202003 | 2 | 1 | -| 1002 | 202005 | 2 | 1 | - -解释:各个用户对 SQL 试卷的未完成数、作答总数、未完成率如下: - -| uid | incomplete_cnt | total_cnt | incomplete_rate | -| ---- | -------------- | --------- | --------------- | -| 1001 | 3 | 7 | 0.4286 | -| 1002 | 4 | 8 | 0.5000 | -| 1003 | 1 | 1 | 1.0000 | - -1001、1002、1003 分别排在 1.0、0.5、0.0 的位置,因此较高的 50%用户(排位<=0.5)为 1002、1003; - -1003 不是 6 级或 7 级; - -有试卷作答记录的近三个月为 202005、202003、202002; - -这三个月里 1002 的作答题数分别为 3、2、2,完成数目分别为 1、1、1。 - -**思路:** - -注意点:这题注意求的是所有的答题次数和完成次数,而 sql 类别的试卷是限制未完成率排名,6, 7 级用户限制的是做题记录。 - -先求出未完成率的排名 - -```sql -SELECT UID, - count(submit_time IS NULL - OR NULL)/ count(start_time) AS num, - PERCENT_RANK() OVER ( - ORDER BY count(submit_time IS NULL - OR NULL)/ count(start_time)) AS ranking -FROM exam_record -LEFT JOIN examination_info USING (exam_id) -WHERE tag = 'SQL' -GROUP BY UID -``` - -再求出最近三个月的练习记录 - -```sql -SELECT UID, - date_format(start_time, '%Y%m') AS month_d, - submit_time, - exam_id, - dense_rank() OVER (PARTITION BY UID - ORDER BY date_format(start_time, '%Y%m') DESC) AS ranking -FROM exam_record -LEFT JOIN user_info USING (UID) -WHERE LEVEL IN (6,7) -``` - -**答案**: - -```sql -SELECT t1.uid, - t1.month_d, - count(*) AS total_cnt, - count(t1.submit_time) AS complete_cnt -FROM-- 先求出未完成率的排名 - - (SELECT UID, - count(submit_time IS NULL OR NULL)/ count(start_time) AS num, - PERCENT_RANK() OVER ( - ORDER BY count(submit_time IS NULL OR NULL)/ count(start_time)) AS ranking - FROM exam_record - LEFT JOIN examination_info USING (exam_id) - WHERE tag = 'SQL' - GROUP BY UID) t -INNER JOIN - (-- 再求出近三个月的练习记录 - SELECT UID, - date_format(start_time, '%Y%m') AS month_d, - submit_time, - exam_id, - dense_rank() OVER (PARTITION BY UID - ORDER BY date_format(start_time, '%Y%m') DESC) AS ranking - FROM exam_record - LEFT JOIN user_info USING (UID) - WHERE LEVEL IN (6,7) ) t1 USING (UID) -WHERE t1.ranking <= 3 AND t.ranking >= 0.5 -- 使用限制找到符合条件的记录 - -GROUP BY t1.uid, - t1.month_d -ORDER BY t1.uid, - t1.month_d -``` - -### 试卷完成数同比 2020 年的增长率及排名变化(困难) - -**描述**: - -现有试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ------ | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2021-01-01 10:00:00 | -| 2 | 9002 | C++ | hard | 80 | 2021-01-01 10:00:00 | -| 3 | 9003 | 算法 | hard | 80 | 2021-01-01 10:00:00 | -| 4 | 9004 | PYTHON | medium | 70 | 2021-01-01 10:00:00 | - -试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): +There is an exam record table `exam_record` (`uid` User ID, `exam_id` Exam ID, `start_time` Start Time, `submit_time` Submit Time, `score` Score): | id | uid | exam_id | start_time | submit_time | score | | --- | ---- | ------- | ------------------- | ------------------- | ----- | -| 1 | 1001 | 9001 | 2020-08-02 10:01:01 | 2020-08-02 10:31:01 | 89 | -| 2 | 1002 | 9001 | 2020-04-01 18:01:01 | 2020-04-01 18:59:02 | 90 | -| 3 | 1001 | 9001 | 2020-04-01 09:01:01 | 2020-04-01 09:21:59 | 80 | -| 5 | 1002 | 9001 | 2021-03-02 19:01:01 | 2021-03-02 19:32:00 | 20 | -| 8 | 1003 | 9001 | 2021-05-02 12:01:01 | 2021-05-02 12:31:01 | 98 | -| 13 | 1003 | 9001 | 2020-01-02 10:01:01 | 2020-01-02 10:31:01 | 89 | -| 9 | 1001 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:20:01 | 99 | -| 10 | 1002 | 9002 | 2021-02-02 12:01:01 | 2020-02-02 12:43:01 | 81 | -| 11 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:59:01 | 69 | -| 16 | 1002 | 9002 | 2020-02-02 12:01:01 | | | -| 17 | 1002 | 9002 | 2020-03-02 12:11:01 | | | -| 18 | 1001 | 9002 | 2021-05-05 18:01:01 | | | -| 4 | 1002 | 9003 | 2021-01-20 10:01:01 | 2021-01-20 10:10:01 | 81 | -| 6 | 1001 | 9003 | 2021-04-02 19:01:01 | 2021-04-02 19:40:01 | 89 | -| 15 | 1002 | 9003 | 2021-01-01 18:01:01 | 2021-01-01 18:59:02 | 90 | -| 7 | 1004 | 9004 | 2020-05-02 12:01:01 | 2020-05-02 12:20:01 | 99 | -| 12 | 1001 | 9004 | 2021-09-02 12:11:01 | | | -| 14 | 1002 | 9004 | 2020-01-01 12:11:01 | 2020-01-01 12:31:01 | 83 | - -请计算 2021 年上半年各类试卷的做完次数相比 2020 年上半年同期的增长率(百分比格式,保留 1 位小数),以及做完次数排名变化,按增长率和 21 年排名降序输出。 - -由示例数据结果输出如下: - -| tag | exam_cnt_20 | exam_cnt_21 | growth_rate | exam_cnt_rank_20 | exam_cnt_rank_21 | rank_delta | -| --- | ----------- | ----------- | ----------- | ---------------- | ---------------- | ---------- | -| SQL | 3 | 2 | -33.3% | 1 | 2 | 1 | - -解释:2020 年上半年有 3 个 tag 有作答完成的记录,分别是 C++、SQL、PYTHON,它们被做完的次数分别是 3、3、2,做完次数排名为 1、1(并列)、3; - -2021 年上半年有 2 个 tag 有作答完成的记录,分别是算法、SQL,它们被做完的次数分别是 3、2,做完次数排名为 1、2;具体如下: - -| tag | start_year | exam_cnt | exam_cnt_rank | -| ------ | ---------- | -------- | ------------- | -| C++ | 2020 | 3 | 1 | -| SQL | 2020 | 3 | 1 | -| PYTHON | 2020 | 2 | 3 | -| 算法 | 2021 | 3 | 1 | -| SQL | 2021 | 2 | 2 | - -因此能输出同比结果的 tag 只有 SQL,从 2020 到 2021 年,做完次数 3=>2,减少 33.3%(保留 1 位小数);排名 1=>2,后退 1 名。 - -**思路:** - -本题难点在于长整型的数据类型要求不能有负号产生,用 cast 函数转换数据类型为 signed。 - -以及用到的`增长率计算公式:(exam_cnt_21-exam_cnt_20)/exam_cnt_20` - -做完次数排名变化(2021 年和 2020 年比排名升了或者降了多少) - -计算公式:`exam_cnt_rank_21 - exam_cnt_rank_20` - -在 MySQL 中,`CAST()` 函数用于将一个表达式的数据类型转换为另一个数据类型。它的基本语法如下: - -```sql -CAST(expression AS data_type) - --- 将一个字符串转换成整数 -SELECT CAST('123' AS INT); -``` - -示例就不一一举例了,这个函数很简单 - -**答案**: - -```sql -SELECT - tag, - exam_cnt_20, - exam_cnt_21, - concat( - round( - 100 * (exam_cnt_21 - exam_cnt_20) / exam_cnt_20, - 1 - ), - '%' - ) AS growth_rate, - exam_cnt_rank_20, - exam_cnt_rank_21, - cast(exam_cnt_rank_21 AS signed) - cast(exam_cnt_rank_20 AS signed) AS rank_delta -FROM - ( - #2020年、2021年上半年各类试卷的做完次数和做完次数排名 - SELECT - tag, - count( - IF ( - date_format(start_time, '%Y%m%d') BETWEEN '20200101' - AND '20200630', - start_time, - NULL - ) - ) AS exam_cnt_20, - count( - IF ( - substring(start_time, 1, 10) BETWEEN '2021-01-01' - AND '2021-06-30', - start_time, - NULL - ) - ) AS exam_cnt_21, - rank() over ( - ORDER BY - count( - IF ( - date_format(start_time, '%Y%m%d') BETWEEN '20200101' - AND '20200630', - start_time, - NULL - ) - ) DESC - ) AS exam_cnt_rank_20, - rank() over ( - ORDER BY - count( - IF ( - substring(start_time, 1, 10) BETWEEN '2021-01-01' - AND '2021-06-30', - start_time, - NULL - ) - ) DESC - ) AS exam_cnt_rank_21 - FROM - examination_info - JOIN exam_record USING (exam_id) - WHERE - submit_time IS NOT NULL - GROUP BY - tag - ) main -WHERE - exam_cnt_21 * exam_cnt_20 <> 0 -ORDER BY - growth_rate DESC, - exam_cnt_rank_21 DESC -``` - -## 聚合窗口函数 - -### 对试卷得分做 min-max 归一化 - -**描述**: - -现有试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ------ | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | -| 2 | 9002 | C++ | hard | 80 | 2020-01-01 10:00:00 | -| 3 | 9003 | 算法 | hard | 80 | 2020-01-01 10:00:00 | -| 4 | 9004 | PYTHON | medium | 70 | 2020-01-01 10:00:00 | - -试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 6 | 1003 | 9001 | 2020-01-02 12:01:01 | 2020-01-02 12:31:01 | 68 | -| 9 | 1001 | 9001 | 2020-01-02 10:01:01 | 2020-01-02 10:31:01 | 89 | -| 1 | 1001 | 9001 | 2020-01-01 09:01:01 | 2020-01-01 09:21:59 | 90 | -| 12 | 1002 | 9002 | 2021-05-05 18:01:01 | (NULL) | (NULL) | -| 3 | 1004 | 9002 | 2020-01-01 12:01:01 | 2020-01-01 12:11:01 | 60 | -| 2 | 1003 | 9002 | 2020-01-01 19:01:01 | 2020-01-01 19:30:01 | 75 | -| 7 | 1001 | 9002 | 2020-01-02 12:01:01 | 2020-01-02 12:43:01 | 81 | -| 10 | 1002 | 9002 | 2020-01-01 12:11:01 | 2020-01-01 12:31:01 | 83 | -| 4 | 1003 | 9002 | 2020-01-01 12:01:01 | 2020-01-01 12:41:01 | 90 | -| 5 | 1002 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:32:00 | 90 | -| 11 | 1002 | 9004 | 2021-09-06 12:01:01 | (NULL) | (NULL) | -| 8 | 1001 | 9005 | 2020-01-02 12:11:01 | (NULL) | (NULL) | - -在物理学及统计学数据计算时,有个概念叫 min-max 标准化,也被称为离差标准化,是对原始数据的线性变换,使结果值映射到[0 - 1]之间。 - -转换函数为: - -![](https://oss.javaguide.cn/github/javaguide/database/sql/29A377601170AB822322431FCDF7EDFE.png) - -请你将用户作答高难度试卷的得分在每份试卷作答记录内执行 min-max 归一化后缩放到[0,100]区间,并输出用户 ID、试卷 ID、归一化后分数平均值;最后按照试卷 ID 升序、归一化分数降序输出。(注:得分区间默认为[0,100],如果某个试卷作答记录中只有一个得分,那么无需使用公式,归一化并缩放后分数仍为原分数)。 - -由示例数据结果输出如下: - -| uid | exam_id | avg_new_score | -| ---- | ------- | ------------- | -| 1001 | 9001 | 98 | -| 1003 | 9001 | 0 | -| 1002 | 9002 | 88 | -| 1003 | 9002 | 75 | -| 1001 | 9002 | 70 | -| 1004 | 9002 | 0 | - -解释:高难度试卷有 9001、9002、9003; - -作答了 9001 的记录有 3 条,分数分别为 68、89、90,按给定公式归一化后分数为:0、95、100,而后两个得分都是用户 1001 作答的,因此用户 1001 对试卷 9001 的新得分为(95+100)/2≈98(只保留整数部分),用户 1003 对于试卷 9001 的新得分为 0。最后结果按照试卷 ID 升序、归一化分数降序输出。 - -**思路:** - -注意点: - -1. 将高难度的试卷,按每类试卷的得分,利用 max/min (col) over()窗口函数求得各组内最大最小值,然后进行归一化公式计算,缩放区间为[0,100],即 min_max\*100 -2. 若某类试卷只有一个得分,则无需使用归一化公式,因只有一个分 max_score=min_score,score,公式后结果可能会变成 0。 -3. 最后结果按 uid、exam_id 分组求归一化后均值,score 为 NULL 的要过滤掉。 - -最后就是仔细看上面公式 (说实话,这题看起来就很绕) - -**答案**: - -```sql -SELECT - uid, - exam_id, - round(sum(min_max) / count(score), 0) AS avg_new_score -FROM - ( - SELECT - *, - IF ( - max_score = min_score, - score, - (score - min_score) / (max_score - min_score) * 100 - ) AS min_max - FROM - ( - SELECT - uid, - a.exam_id, - score, - max(score) over (PARTITION BY a.exam_id) AS max_score, - min(score) over (PARTITION BY a.exam_id) AS min_score - FROM - exam_record a - LEFT JOIN examination_info b USING (exam_id) - WHERE - difficulty = 'hard' - ) t - WHERE - score IS NOT NULL - ) t1 -GROUP BY - uid, - exam_id -ORDER BY - exam_id ASC, - avg_new_score DESC; -``` - -### 每份试卷每月作答数和截止当月的作答总数 - -**描述:** - -现有试卷作答记录表 exam_record(uid 用户 ID, exam_id 试卷 ID, start_time 开始作答时间, submit_time 交卷时间, score 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2020-01-01 09:01:01 | 2020-01-01 09:21:59 | 90 | -| 2 | 1002 | 9001 | 2020-01-20 10:01:01 | 2020-01-20 10:10:01 | 89 | -| 3 | 1002 | 9001 | 2020-02-01 12:11:01 | 2020-02-01 12:31:01 | 83 | -| 4 | 1003 | 9001 | 2020-03-01 19:01:01 | 2020-03-01 19:30:01 | 75 | -| 5 | 1004 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:11:01 | 60 | -| 6 | 1003 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:41:01 | 90 | -| 7 | 1002 | 9001 | 2020-05-02 19:01:01 | 2020-05-02 19:32:00 | 90 | -| 8 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:59:01 | 69 | -| 9 | 1004 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:20:01 | 99 | -| 10 | 1003 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:31:01 | 68 | -| 11 | 1001 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:43:01 | 81 | -| 12 | 1001 | 9002 | 2020-03-02 12:11:01 | (NULL) | (NULL) | - -请输出每份试卷每月作答数和截止当月的作答总数。 -由示例数据结果输出如下: - -| exam_id | start_month | month_cnt | cum_exam_cnt | -| ------- | ----------- | --------- | ------------ | -| 9001 | 202001 | 2 | 2 | -| 9001 | 202002 | 1 | 3 | -| 9001 | 202003 | 3 | 6 | -| 9001 | 202005 | 1 | 7 | -| 9002 | 202001 | 1 | 1 | -| 9002 | 202002 | 3 | 4 | -| 9002 | 202003 | 1 | 5 | - -解释:试卷 9001 在 202001、202002、202003、202005 共 4 个月有被作答记录,每个月被作答数分别为 2、1、3、1,截止当月累积作答总数为 2、3、6、7。 - -**思路:** - -这题就两个关键点:统计截止当月的作答总数、输出每份试卷每月作答数和截止当月的作答总数 - -这个是关键`**sum(count(*)) over(partition by exam_id order by date_format(start_time,'%Y%m'))**` - -**答案**: - -```sql -SELECT exam_id, - date_format(start_time, '%Y%m') AS start_month, - count(*) AS month_cnt, - sum(count(*)) OVER (PARTITION BY exam_id - ORDER BY date_format(start_time, '%Y%m')) AS cum_exam_cnt -FROM exam_record -GROUP BY exam_id, - start_month -``` - -### 每月及截止当月的答题情况(较难) - -**描述**:现有试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2020-01-01 09:01:01 | 2020-01-01 09:21:59 | 90 | -| 2 | 1002 | 9001 | 2020-01-20 10:01:01 | 2020-01-20 10:10:01 | 89 | -| 3 | 1002 | 9001 | 2020-02-01 12:11:01 | 2020-02-01 12:31:01 | 83 | -| 4 | 1003 | 9001 | 2020-03-01 19:01:01 | 2020-03-01 19:30:01 | 75 | -| 5 | 1004 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:11:01 | 60 | -| 6 | 1003 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:41:01 | 90 | -| 7 | 1002 | 9001 | 2020-05-02 19:01:01 | 2020-05-02 19:32:00 | 90 | -| 8 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:59:01 | 69 | -| 9 | 1004 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:20:01 | 99 | -| 10 | 1003 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:31:01 | 68 | -| 11 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-02-02 12:43:01 | 81 | -| 12 | 1001 | 9002 | 2020-03-02 12:11:01 | (NULL) | (NULL) | - -请输出自从有用户作答记录以来,每月的试卷作答记录中月活用户数、新增用户数、截止当月的单月最大新增用户数、截止当月的累积用户数。结果按月份升序输出。 - -由示例数据结果输出如下: - -| start_month | mau | month_add_uv | max_month_add_uv | cum_sum_uv | -| ----------- | --- | ------------ | ---------------- | ---------- | -| 202001 | 2 | 2 | 2 | 2 | -| 202002 | 4 | 2 | 2 | 4 | -| 202003 | 3 | 0 | 2 | 4 | -| 202005 | 1 | 0 | 2 | 4 | - -| month | 1001 | 1002 | 1003 | 1004 | -| ------ | ---- | ---- | ---- | ---- | -| 202001 | 1 | 1 | | | -| 202002 | 1 | 1 | 1 | 1 | -| 202003 | 1 | | 1 | 1 | -| 202005 | | 1 | | | - -由上述矩阵可以看出,2020 年 1 月有 2 个用户活跃(mau=2),当月新增用户数为 2; - -2020 年 2 月有 4 个用户活跃,当月新增用户数为 2,最大单月新增用户数为 2,当前累积用户数为 4。 - -**思路:** - -难点: - -1.如何求每月新增用户 - -2.截至当月的答题情况 - -大致流程: - -(1)统计每个人的首次登陆月份 `min()` - -(2)统计每月的月活和新增用户数:先得到每个人的首次登陆月份,再对首次登陆月份分组求和是该月份的新增人数 - -(3)统计截止当月的单月最大新增用户数、截止当月的累积用户数 ,最终按照按月份升序输出 - -**答案**: - -```sql --- 截止当月的单月最大新增用户数、截止当月的累积用户数,按月份升序输出 -SELECT - start_month, - mau, - month_add_uv, - max( month_add_uv ) over ( ORDER BY start_month ), - sum( month_add_uv ) over ( ORDER BY start_month ) -FROM - ( - -- 统计每月的月活和新增用户数 - SELECT - date_format( a.start_time, '%Y%m' ) AS start_month, - count( DISTINCT a.uid ) AS mau, - count( DISTINCT b.uid ) AS month_add_uv - FROM - exam_record a - LEFT JOIN ( - -- 统计每个人的首次登陆月份 - SELECT uid, min( date_format( start_time, '%Y%m' )) AS first_month FROM exam_record GROUP BY uid ) b ON date_format( a.start_time, '%Y%m' ) = b.first_month - GROUP BY - start_month - ) main -ORDER BY - start_month -``` - - +| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 78 | +| 2 | 1002 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 | +| 3 | 1002 | 900 | | | | diff --git a/docs/database/sql/sql-questions-05.md b/docs/database/sql/sql-questions-05.md index c20af2cad39..7475e377940 100644 --- a/docs/database/sql/sql-questions-05.md +++ b/docs/database/sql/sql-questions-05.md @@ -1,22 +1,22 @@ --- -title: SQL常见面试题总结(5) -category: 数据库 +title: Summary of Common SQL Interview Questions (5) +category: Database tag: - - 数据库基础 + - Database Basics - SQL --- -> 题目来源于:[牛客题霸 - SQL 进阶挑战](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) +> Source of questions: [Niuke Question Bank - SQL Advanced Challenge](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) -较难或者困难的题目可以根据自身实际情况和面试需要来决定是否要跳过。 +Difficult or challenging questions can be skipped based on your actual situation and interview needs. -## 空值处理 +## Handling Null Values -### 统计有未完成状态的试卷的未完成数和未完成率 +### Count the number of unfinished exams and the unfinished rate for exams with an unfinished status -**描述**: +**Description**: -现有试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分),数据如下: +There is an exam record table `exam_record` (`uid` User ID, `exam_id` Exam ID, `start_time` Start time, `submit_time` Submission time, `score` Score), with the following data: | id | uid | exam_id | start_time | submit_time | score | | --- | ---- | ------- | ------------------- | ------------------- | ------ | @@ -24,21 +24,21 @@ tag: | 2 | 1001 | 9001 | 2021-05-02 10:01:01 | 2021-05-02 10:30:01 | 81 | | 3 | 1001 | 9001 | 2021-09-02 12:01:01 | (NULL) | (NULL) | -请统计有未完成状态的试卷的未完成数 incomplete_cnt 和未完成率 incomplete_rate。由示例数据结果输出如下: +Please count the number of unfinished exams `incomplete_cnt` and the unfinished rate `incomplete_rate` for exams with an unfinished status. The output based on the example data is as follows: | exam_id | incomplete_cnt | complete_rate | | ------- | -------------- | ------------- | | 9001 | 1 | 0.333 | -解释:试卷 9001 有 3 次被作答的记录,其中两次完成,1 次未完成,因此未完成数为 1,未完成率为 0.333(保留 3 位小数) +Explanation: Exam 9001 has 3 records, of which two are completed and one is unfinished, so the number of unfinished exams is 1, and the unfinished rate is 0.333 (retaining 3 decimal places). -**思路**: +**Thought Process**: -这题只需要注意一个是有条件限制,一个是没条件限制的;要么分别查询条件,然后合并;要么直接在 select 里面进行条件判断。 +This question only requires attention to one condition with restrictions and one without; you can either query the conditions separately and then merge them, or directly perform conditional checks in the select statement. -**答案**: +**Answer**: -写法 1: +Method 1: ```sql SELECT exam_id, @@ -49,7 +49,7 @@ GROUP BY exam_id HAVING incomplete_cnt <> 0 ``` -写法 2: +Method 2: ```sql SELECT exam_id, @@ -60,954 +60,22 @@ GROUP BY exam_id HAVING incomplete_cnt <> 0 ``` -两种写法都可以,只有中间的写法不一样,一个是对符合条件的才`COUNT`,一个是直接上`IF`,后者更为直观,最后这个`having`解释一下, 无论是 `complete_rate` 还是 `incomplete_cnt`,只要不为 0 即可,不为 0 就意味着有未完成的。 +Both methods are valid; only the middle part differs, one counts only those that meet the condition, while the other uses `IF` directly, which is more intuitive. Finally, this `HAVING` clause explains that as long as either `complete_rate` or `incomplete_cnt` is not 0, it means there are unfinished exams. -### 0 级用户高难度试卷的平均用时和平均得分 +### Average time and average score for level 0 users on difficult exams -**描述**: +**Description**: -现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间),数据如下: +There is a user information table `user_info` (`uid` User ID, `nick_name` Nickname, `achievement` Achievement value, `level` Level, `job` Job direction, `register_time` Registration time), with the following data: -| id | uid | nick_name | achievement | level | job | register_time | -| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | -| 1 | 1001 | 牛客 1 号 | 10 | 0 | 算法 | 2020-01-01 10:00:00 | -| 2 | 1002 | 牛客 2 号 | 2100 | 6 | 算法 | 2020-01-01 10:00:00 | +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | --------- | ----------- | ----- | --------- | ------------------- | +| 1 | 1001 | Niuke 1 | 10 | 0 | Algorithm | 2020-01-01 10:00:00 | +| 2 | 1002 | Niuke 2 | 2100 | 6 | Algorithm | 2020-01-01 10:00:00 | -试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间),数据如下: - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ---- | ---------- | -------- | ------------------- | -| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | -| 2 | 9002 | SQL | easy | 60 | 2020-01-01 10:00:00 | -| 3 | 9004 | 算法 | medium | 80 | 2020-01-01 10:00:00 | - -试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分),数据如下: - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:59 | 80 | -| 2 | 1001 | 9001 | 2021-05-02 10:01:01 | (NULL) | (NULL) | -| 3 | 1001 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 | -| 4 | 1001 | 9001 | 2021-06-02 19:01:01 | 2021-06-02 19:32:00 | 20 | -| 5 | 1001 | 9002 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 | -| 6 | 1001 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | -| 7 | 1002 | 9002 | 2021-05-05 18:01:01 | 2021-05-05 18:59:02 | 90 | - -请输出每个 0 级用户所有的高难度试卷考试平均用时和平均得分,未完成的默认试卷最大考试时长和 0 分处理。由示例数据结果输出如下: - -| uid | avg_score | avg_time_took | -| ---- | --------- | ------------- | -| 1001 | 33 | 36.7 | - -解释:0 级用户有 1001,高难度试卷有 9001,1001 作答 9001 的记录有 3 条,分别用时 20 分钟、未完成(试卷时长 60 分钟)、30 分钟(未满 31 分钟),分别得分为 80 分、未完成(0 分处理)、20 分。因此他的平均用时为 110/3=36.7(保留一位小数),平均得分为 33 分(取整) - -**思路**:这题用`IF`是判断的最方便的,因为涉及到 NULL 值的判断。当然 `case when`也可以,大同小异。这题的难点就在于空值的处理,其他的这些查询条件什么的,我相信难不倒大家。 - -**答案**: - -```sql -SELECT UID, - round(avg(new_socre)) AS avg_score, - round(avg(time_diff), 1) AS avg_time_took -FROM - (SELECT er.uid, - IF (er.submit_time IS NOT NULL, TIMESTAMPDIFF(MINUTE, start_time, submit_time), ef.duration) AS time_diff, - IF (er.submit_time IS NOT NULL,er.score,0) AS new_socre - FROM exam_record er - LEFT JOIN user_info uf ON er.uid = uf.uid - LEFT JOIN examination_info ef ON er.exam_id = ef.exam_id - WHERE uf.LEVEL = 0 AND ef.difficulty = 'hard' ) t -GROUP BY UID -ORDER BY UID -``` - -## 高级条件语句 - -### 筛选限定昵称成就值活跃日期的用户(较难) - -**描述**: - -现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): - -| id | uid | nick_name | achievement | level | job | register_time | -| --- | ---- | ----------- | ----------- | ----- | ---- | ------------------- | -| 1 | 1001 | 牛客 1 号 | 1000 | 2 | 算法 | 2020-01-01 10:00:00 | -| 2 | 1002 | 牛客 2 号 | 1200 | 3 | 算法 | 2020-01-01 10:00:00 | -| 3 | 1003 | 进击的 3 号 | 2200 | 5 | 算法 | 2020-01-01 10:00:00 | -| 4 | 1004 | 牛客 4 号 | 2500 | 6 | 算法 | 2020-01-01 10:00:00 | -| 5 | 1005 | 牛客 5 号 | 3000 | 7 | C++ | 2020-01-01 10:00:00 | - -试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:59 | 80 | -| 3 | 1001 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 | -| 2 | 1001 | 9001 | 2021-05-02 10:01:01 | (NULL) | (NULL) | -| 4 | 1001 | 9001 | 2021-06-02 19:01:01 | 2021-06-02 19:32:00 | 20 | -| 6 | 1001 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | -| 5 | 1001 | 9002 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 | -| 11 | 1002 | 9001 | 2020-01-01 12:01:01 | 2020-01-01 12:31:01 | 81 | -| 12 | 1002 | 9002 | 2020-02-01 12:01:01 | 2020-02-01 12:31:01 | 82 | -| 13 | 1002 | 9002 | 2020-02-02 12:11:01 | 2020-02-02 12:31:01 | 83 | -| 7 | 1002 | 9002 | 2021-05-05 18:01:01 | 2021-05-05 18:59:02 | 90 | -| 16 | 1002 | 9001 | 2021-09-06 12:01:01 | 2021-09-06 12:21:01 | 80 | -| 17 | 1002 | 9001 | 2021-09-06 12:01:01 | (NULL) | (NULL) | -| 18 | 1002 | 9001 | 2021-09-07 12:01:01 | (NULL) | (NULL) | -| 8 | 1003 | 9003 | 2021-02-06 12:01:01 | (NULL) | (NULL) | -| 9 | 1003 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 89 | -| 10 | 1004 | 9002 | 2021-08-06 12:01:01 | (NULL) | (NULL) | -| 14 | 1005 | 9001 | 2021-02-01 11:01:01 | 2021-02-01 11:31:01 | 84 | -| 15 | 1006 | 9001 | 2021-02-01 11:01:01 | 2021-02-01 11:31:01 | 84 | - -题目练习记录表 `practice_record`(`uid` 用户 ID, `question_id` 题目 ID, `submit_time` 提交时间, `score` 得分): - -| id | uid | question_id | submit_time | score | -| --- | ---- | ----------- | ------------------- | ----- | -| 1 | 1001 | 8001 | 2021-08-02 11:41:01 | 60 | -| 2 | 1002 | 8001 | 2021-09-02 19:30:01 | 50 | -| 3 | 1002 | 8001 | 2021-09-02 19:20:01 | 70 | -| 4 | 1002 | 8002 | 2021-09-02 19:38:01 | 70 | -| 5 | 1003 | 8002 | 2021-09-01 19:38:01 | 80 | - -请找到昵称以『牛客』开头『号』结尾、成就值在 1200~2500 之间,且最近一次活跃(答题或作答试卷)在 2021 年 9 月的用户信息。 - -由示例数据结果输出如下: - -| uid | nick_name | achievement | -| ---- | --------- | ----------- | -| 1002 | 牛客 2 号 | 1200 | - -**解释**:昵称以『牛客』开头『号』结尾且成就值在 1200~2500 之间的有 1002、1004; - -1002 最近一次试卷区活跃为 2021 年 9 月,最近一次题目区活跃为 2021 年 9 月;1004 最近一次试卷区活跃为 2021 年 8 月,题目区未活跃。 - -因此最终满足条件的只有 1002。 - -**思路**: - -先根据条件列出主要查询语句 - -昵称以『牛客』开头『号』结尾: `nick_name LIKE "牛客%号"` - -成就值在 1200~2500 之间:`achievement BETWEEN 1200 AND 2500` - -第三个条件因为限定了为 9 月,所以直接写就行:`( date_format( record.submit_time, '%Y%m' )= 202109 OR date_format( pr.submit_time, '%Y%m' )= 202109 )` - -**答案**: - -```sql -SELECT DISTINCT u_info.uid, - u_info.nick_name, - u_info.achievement -FROM user_info u_info -LEFT JOIN exam_record record ON record.uid = u_info.uid -LEFT JOIN practice_record pr ON u_info.uid = pr.uid -WHERE u_info.nick_name LIKE "牛客%号" - AND u_info.achievement BETWEEN 1200 - AND 2500 - AND (date_format(record.submit_time, '%Y%m')= 202109 - OR date_format(pr.submit_time, '%Y%m')= 202109) -GROUP BY u_info.uid -``` - -### 筛选昵称规则和试卷规则的作答记录(较难) - -**描述**: - -现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): - -| id | uid | nick_name | achievement | level | job | register_time | -| --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | -| 1 | 1001 | 牛客 1 号 | 1900 | 2 | 算法 | 2020-01-01 10:00:00 | -| 2 | 1002 | 牛客 2 号 | 1200 | 3 | 算法 | 2020-01-01 10:00:00 | -| 3 | 1003 | 牛客 3 号 ♂ | 2200 | 5 | 算法 | 2020-01-01 10:00:00 | -| 4 | 1004 | 牛客 4 号 | 2500 | 6 | 算法 | 2020-01-01 10:00:00 | -| 5 | 1005 | 牛客 555 号 | 2000 | 7 | C++ | 2020-01-01 10:00:00 | -| 6 | 1006 | 666666 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | - -试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): +There is an exam information table `examination_info` (`exam_id` Exam ID, `tag` Exam category, `difficulty` Exam difficulty, `duration` Exam duration, `release_time` Release time), with the following data: | id | exam_id | tag | difficulty | duration | release_time | | --- | ------- | --- | ---------- | -------- | ------------------- | -| 1 | 9001 | C++ | hard | 60 | 2020-01-01 10:00:00 | -| 2 | 9002 | c# | hard | 80 | 2020-01-01 10:00:00 | -| 3 | 9003 | SQL | medium | 70 | 2020-01-01 10:00:00 | - -试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:59 | 80 | -| 2 | 1001 | 9001 | 2021-05-02 10:01:01 | (NULL) | (NULL) | -| 4 | 1001 | 9001 | 2021-06-02 19:01:01 | 2021-06-02 19:32:00 | 20 | -| 3 | 1001 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 | -| 5 | 1001 | 9002 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 | -| 6 | 1001 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | -| 11 | 1002 | 9001 | 2020-01-01 12:01:01 | 2020-01-01 12:31:01 | 81 | -| 16 | 1002 | 9001 | 2021-09-06 12:01:01 | 2021-09-06 12:21:01 | 80 | -| 17 | 1002 | 9001 | 2021-09-06 12:01:01 | (NULL) | (NULL) | -| 18 | 1002 | 9001 | 2021-09-07 12:01:01 | (NULL) | (NULL) | -| 7 | 1002 | 9002 | 2021-05-05 18:01:01 | 2021-05-05 18:59:02 | 90 | -| 12 | 1002 | 9002 | 2020-02-01 12:01:01 | 2020-02-01 12:31:01 | 82 | -| 13 | 1002 | 9002 | 2020-02-02 12:11:01 | 2020-02-02 12:31:01 | 83 | -| 9 | 1003 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 89 | -| 8 | 1003 | 9003 | 2021-02-06 12:01:01 | (NULL) | (NULL) | -| 10 | 1004 | 9002 | 2021-08-06 12:01:01 | (NULL) | (NULL) | -| 14 | 1005 | 9001 | 2021-02-01 11:01:01 | 2021-02-01 11:31:01 | 84 | -| 15 | 1006 | 9001 | 2021-02-01 11:01:01 | 2021-09-01 11:31:01 | 84 | - -找到昵称以"牛客"+纯数字+"号"或者纯数字组成的用户对于字母 c 开头的试卷类别(如 C,C++,c#等)的已完成的试卷 ID 和平均得分,按用户 ID、平均分升序排序。由示例数据结果输出如下: - -| uid | exam_id | avg_score | -| ---- | ------- | --------- | -| 1002 | 9001 | 81 | -| 1002 | 9002 | 85 | -| 1005 | 9001 | 84 | -| 1006 | 9001 | 84 | - -解释:昵称满足条件的用户有 1002、1004、1005、1006; - -c 开头的试卷有 9001、9002; - -满足上述条件的作答记录中,1002 完成 9001 的得分有 81、80,平均分为 81(80.5 取整四舍五入得 81); - -1002 完成 9002 的得分有 90、82、83,平均分为 85; - -**思路**: - -还是老样子,既然给出了条件,就先把各个条件先写出来 - -找到昵称以"牛客"+纯数字+"号"或者纯数字组成的用户: 我最开始是这么写的:`nick_name LIKE '牛客%号' OR nick_name REGEXP '^[0-9]+$'`,如果表中有个 “牛客 H 号” ,那也能通过。 - -所以这里还得用正则: `nick_name LIKE '^牛客[0-9]+号'` - -对于字母 c 开头的试卷类别: `e_info.tag LIKE 'c%'` 或者 `tag regexp '^c|^C'` 第一个也能匹配到大写 C - -**答案**: - -```sql -SELECT UID, - exam_id, - ROUND(AVG(score), 0) avg_score -FROM exam_record -WHERE UID IN - (SELECT UID - FROM user_info - WHERE nick_name RLIKE "^牛客[0-9]+号 $" - OR nick_name RLIKE "^[0-9]+$") - AND exam_id IN - (SELECT exam_id - FROM examination_info - WHERE tag RLIKE "^[cC]") - AND score IS NOT NULL -GROUP BY UID,exam_id -ORDER BY UID,avg_score; -``` - -### 根据指定记录是否存在输出不同情况(困难) - -**描述**: - -现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): - -| id | uid | nick_name | achievement | level | job | register_time | -| --- | ---- | ----------- | ----------- | ----- | ---- | ------------------- | -| 1 | 1001 | 牛客 1 号 | 19 | 0 | 算法 | 2020-01-01 10:00:00 | -| 2 | 1002 | 牛客 2 号 | 1200 | 3 | 算法 | 2020-01-01 10:00:00 | -| 3 | 1003 | 进击的 3 号 | 22 | 0 | 算法 | 2020-01-01 10:00:00 | -| 4 | 1004 | 牛客 4 号 | 25 | 0 | 算法 | 2020-01-01 10:00:00 | -| 5 | 1005 | 牛客 555 号 | 2000 | 7 | C++ | 2020-01-01 10:00:00 | -| 6 | 1006 | 666666 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | - -试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:59 | 80 | -| 2 | 1001 | 9001 | 2021-05-02 10:01:01 | (NULL) | (NULL) | -| 3 | 1001 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 | -| 4 | 1001 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | -| 5 | 1001 | 9003 | 2021-09-02 12:01:01 | (NULL) | (NULL) | -| 6 | 1001 | 9004 | 2021-09-03 12:01:01 | (NULL) | (NULL) | -| 7 | 1002 | 9001 | 2020-01-01 12:01:01 | 2020-01-01 12:31:01 | 99 | -| 8 | 1002 | 9003 | 2020-02-01 12:01:01 | 2020-02-01 12:31:01 | 82 | -| 9 | 1002 | 9003 | 2020-02-02 12:11:01 | (NULL) | (NULL) | -| 10 | 1002 | 9002 | 2021-05-05 18:01:01 | (NULL) | (NULL) | -| 11 | 1002 | 9001 | 2021-09-06 12:01:01 | (NULL) | (NULL) | -| 12 | 1003 | 9003 | 2021-02-06 12:01:01 | (NULL) | (NULL) | -| 13 | 1003 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 89 | - -请你筛选表中的数据,当有任意一个 0 级用户未完成试卷数大于 2 时,输出每个 0 级用户的试卷未完成数和未完成率(保留 3 位小数);若不存在这样的用户,则输出所有有作答记录的用户的这两个指标。结果按未完成率升序排序。 - -由示例数据结果输出如下: - -| uid | incomplete_cnt | incomplete_rate | -| ---- | -------------- | --------------- | -| 1004 | 0 | 0.000 | -| 1003 | 1 | 0.500 | -| 1001 | 4 | 0.667 | - -**解释**:0 级用户有 1001、1003、1004;他们作答试卷数和未完成数分别为:6:4、2:1、0:0; - -存在 1001 这个 0 级用户未完成试卷数大于 2,因此输出这三个用户的未完成数和未完成率(1004 未作答过试卷,未完成率默认填 0,保留 3 位小数后是 0.000); - -结果按照未完成率升序排序。 - -附:如果 1001 不满足『未完成试卷数大于 2』,则需要输出 1001、1002、1003 的这两个指标,因为试卷作答记录表里只有这三个用户的作答记录。 - -**思路**: - -先把可能满足条件**“0 级用户未完成试卷数大于 2”**的 SQL 写出来 - -```sql -SELECT ui.uid UID -FROM user_info ui -LEFT JOIN exam_record er ON ui.uid = er.uid -WHERE ui.uid IN - (SELECT ui.uid - FROM user_info ui - LEFT JOIN exam_record er ON ui.uid = er.uid - WHERE er.submit_time IS NULL - AND ui.LEVEL = 0 ) -GROUP BY ui.uid -HAVING sum(IF(er.submit_time IS NULL, 1, 0)) > 2 -``` - -然后再分别写出两种情况的 SQL 查询语句: - -情况 1. 查询存在条件要求的 0 级用户的试卷未完成率 - -```sql -SELECT - tmp1.uid uid, - sum( - IF - ( er.submit_time IS NULL AND er.start_time IS NOT NULL, 1, 0 )) incomplete_cnt, - round( - sum( - IF - ( er.submit_time IS NULL AND er.start_time IS NOT NULL, 1, 0 ))/ count( tmp1.uid ), - 3 - ) incomplete_rate -FROM - ( - SELECT DISTINCT - ui.uid - FROM - user_info ui - LEFT JOIN exam_record er ON ui.uid = er.uid - WHERE - er.submit_time IS NULL - AND ui.LEVEL = 0 - ) tmp1 - LEFT JOIN exam_record er ON tmp1.uid = er.uid -GROUP BY - tmp1.uid -ORDER BY - incomplete_rate -``` - -情况 2. 查询不存在条件要求时所有有作答记录的 yong 用户的试卷未完成率 - -```sql -SELECT - ui.uid uid, - sum( CASE WHEN er.submit_time IS NULL AND er.start_time IS NOT NULL THEN 1 ELSE 0 END ) incomplete_cnt, - round( - sum( - IF - ( er.submit_time IS NULL AND er.start_time IS NOT NULL, 1, 0 ))/ count( ui.uid ), - 3 - ) incomplete_rate -FROM - user_info ui - JOIN exam_record er ON ui.uid = er.uid -GROUP BY - ui.uid -ORDER BY - incomplete_rate -``` - -拼在一起,就是答案 - -```sql -WITH host_user AS - (SELECT ui.uid UID - FROM user_info ui - LEFT JOIN exam_record er ON ui.uid = er.uid - WHERE ui.uid IN - (SELECT ui.uid - FROM user_info ui - LEFT JOIN exam_record er ON ui.uid = er.uid - WHERE er.submit_time IS NULL - AND ui.LEVEL = 0 ) - GROUP BY ui.uid - HAVING sum(IF (er.submit_time IS NULL, 1, 0))> 2), - tt1 AS - (SELECT tmp1.uid UID, - sum(IF (er.submit_time IS NULL - AND er.start_time IS NOT NULL, 1, 0)) incomplete_cnt, - round(sum(IF (er.submit_time IS NULL - AND er.start_time IS NOT NULL, 1, 0))/ count(tmp1.uid), 3) incomplete_rate - FROM - (SELECT DISTINCT ui.uid - FROM user_info ui - LEFT JOIN exam_record er ON ui.uid = er.uid - WHERE er.submit_time IS NULL - AND ui.LEVEL = 0 ) tmp1 - LEFT JOIN exam_record er ON tmp1.uid = er.uid - GROUP BY tmp1.uid - ORDER BY incomplete_rate), - tt2 AS - (SELECT ui.uid UID, - sum(CASE - WHEN er.submit_time IS NULL - AND er.start_time IS NOT NULL THEN 1 - ELSE 0 - END) incomplete_cnt, - round(sum(IF (er.submit_time IS NULL - AND er.start_time IS NOT NULL, 1, 0))/ count(ui.uid), 3) incomplete_rate - FROM user_info ui - JOIN exam_record er ON ui.uid = er.uid - GROUP BY ui.uid - ORDER BY incomplete_rate) - (SELECT tt1.* - FROM tt1 - LEFT JOIN - (SELECT UID - FROM host_user) t1 ON 1 = 1 - WHERE t1.uid IS NOT NULL ) -UNION ALL - (SELECT tt2.* - FROM tt2 - LEFT JOIN - (SELECT UID - FROM host_user) t2 ON 1 = 1 - WHERE t2.uid IS NULL) -``` - -V2 版本(根据上面做出的改进,答案缩短了,逻辑更强): - -```sql -SELECT - ui.uid, - SUM( - IF - ( start_time IS NOT NULL AND score IS NULL, 1, 0 )) AS incomplete_cnt,#3.试卷未完成数 - ROUND( AVG( IF ( start_time IS NOT NULL AND score IS NULL, 1, 0 )), 3 ) AS incomplete_rate #4.未完成率 - -FROM - user_info ui - LEFT JOIN exam_record USING ( uid ) -WHERE -CASE - - WHEN (#1.当有任意一个0级用户未完成试卷数大于2时 - SELECT - MAX( lv0_incom_cnt ) - FROM - ( - SELECT - SUM( - IF - ( score IS NULL, 1, 0 )) AS lv0_incom_cnt - FROM - user_info - JOIN exam_record USING ( uid ) - WHERE - LEVEL = 0 - GROUP BY - uid - ) table1 - )> 2 THEN - uid IN ( #1.1找出每个0级用户 - SELECT uid FROM user_info WHERE LEVEL = 0 ) ELSE uid IN ( #2.若不存在这样的用户,找出有作答记录的用户 - SELECT DISTINCT uid FROM exam_record ) - END - GROUP BY - ui.uid - ORDER BY - incomplete_rate #5.结果按未完成率升序排序 -``` - -### 各用户等级的不同得分表现占比(较难) - -**描述**: - -现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): - -| id | uid | nick_name | achievement | level | job | register_time | -| --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | -| 1 | 1001 | 牛客 1 号 | 19 | 0 | 算法 | 2020-01-01 10:00:00 | -| 2 | 1002 | 牛客 2 号 | 1200 | 3 | 算法 | 2020-01-01 10:00:00 | -| 3 | 1003 | 牛客 3 号 ♂ | 22 | 0 | 算法 | 2020-01-01 10:00:00 | -| 4 | 1004 | 牛客 4 号 | 25 | 0 | 算法 | 2020-01-01 10:00:00 | -| 5 | 1005 | 牛客 555 号 | 2000 | 7 | C++ | 2020-01-01 10:00:00 | -| 6 | 1006 | 666666 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | - -试卷作答记录表 exam_record(uid 用户 ID, exam_id 试卷 ID, start_time 开始作答时间, submit_time 交卷时间, score 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:59 | 80 | -| 2 | 1001 | 9001 | 2021-05-02 10:01:01 | (NULL) | (NULL) | -| 3 | 1001 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 75 | -| 4 | 1001 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:11:01 | 60 | -| 5 | 1001 | 9003 | 2021-09-02 12:01:01 | 2021-09-02 12:41:01 | 90 | -| 6 | 1001 | 9001 | 2021-06-02 19:01:01 | 2021-06-02 19:32:00 | 20 | -| 7 | 1001 | 9002 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 | -| 8 | 1001 | 9004 | 2021-09-03 12:01:01 | (NULL) | (NULL) | -| 9 | 1002 | 9001 | 2020-01-01 12:01:01 | 2020-01-01 12:31:01 | 99 | -| 10 | 1002 | 9003 | 2020-02-01 12:01:01 | 2020-02-01 12:31:01 | 82 | -| 11 | 1002 | 9003 | 2020-02-02 12:11:01 | 2020-02-02 12:41:01 | 76 | - -为了得到用户试卷作答的定性表现,我们将试卷得分按分界点[90,75,60]分为优良中差四个得分等级(分界点划分到左区间),请统计不同用户等级的人在完成过的试卷中各得分等级占比(结果保留 3 位小数),未完成过试卷的用户无需输出,结果按用户等级降序、占比降序排序。 - -由示例数据结果输出如下: - -| level | score_grade | ratio | -| ----- | ----------- | ----- | -| 3 | 良 | 0.667 | -| 3 | 优 | 0.333 | -| 0 | 良 | 0.500 | -| 0 | 中 | 0.167 | -| 0 | 优 | 0.167 | -| 0 | 差 | 0.167 | - -解释:完成过试卷的用户有 1001、1002;完成了的试卷对应的用户等级和分数等级如下: - -| uid | exam_id | score | level | score_grade | -| ---- | ------- | ----- | ----- | ----------- | -| 1001 | 9001 | 80 | 0 | 良 | -| 1001 | 9002 | 75 | 0 | 良 | -| 1001 | 9002 | 60 | 0 | 中 | -| 1001 | 9003 | 90 | 0 | 优 | -| 1001 | 9001 | 20 | 0 | 差 | -| 1001 | 9002 | 89 | 0 | 良 | -| 1002 | 9001 | 99 | 3 | 优 | -| 1002 | 9003 | 82 | 3 | 良 | -| 1002 | 9003 | 76 | 3 | 良 | - -因此 0 级用户(只有 1001)的各分数等级比例为:优 1/6,良 1/6,中 1/6,差 3/6;3 级用户(只有 1002)各分数等级比例为:优 1/3,良 2/3。结果保留 3 位小数。 - -**思路**: - -先把 **“将试卷得分按分界点[90,75,60]分为优良中差四个得分等级”**这个条件写出来,这里可以用到`case when` - -```sql -CASE - WHEN a.score >= 90 THEN - '优' - WHEN a.score < 90 AND a.score >= 75 THEN - '良' - WHEN a.score < 75 AND a.score >= 60 THEN - '中' ELSE '差' -END -``` - -这题的关键点就在于这,其他剩下的就是条件拼接了 - -**答案**: - -```sql -SELECT a.LEVEL, - a.score_grade, - ROUND(a.cur_count / b.total_num, 3) AS ratio -FROM - (SELECT b.LEVEL AS LEVEL, - (CASE - WHEN a.score >= 90 THEN '优' - WHEN a.score < 90 - AND a.score >= 75 THEN '良' - WHEN a.score < 75 - AND a.score >= 60 THEN '中' - ELSE '差' - END) AS score_grade, - count(1) AS cur_count - FROM exam_record a - LEFT JOIN user_info b ON a.uid = b.uid - WHERE a.submit_time IS NOT NULL - GROUP BY b.LEVEL, - score_grade) a -LEFT JOIN - (SELECT b.LEVEL AS LEVEL, - count(b.LEVEL) AS total_num - FROM exam_record a - LEFT JOIN user_info b ON a.uid = b.uid - WHERE a.submit_time IS NOT NULL - GROUP BY b.LEVEL) b ON a.LEVEL = b.LEVEL -ORDER BY a.LEVEL DESC, - ratio DESC -``` - -## 限量查询 - -### 注册时间最早的三个人 - -**描述**: - -现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): - -| id | uid | nick_name | achievement | level | job | register_time | -| --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | -| 1 | 1001 | 牛客 1 号 | 19 | 0 | 算法 | 2020-01-01 10:00:00 | -| 2 | 1002 | 牛客 2 号 | 1200 | 3 | 算法 | 2020-02-01 10:00:00 | -| 3 | 1003 | 牛客 3 号 ♂ | 22 | 0 | 算法 | 2020-01-02 10:00:00 | -| 4 | 1004 | 牛客 4 号 | 25 | 0 | 算法 | 2020-01-02 11:00:00 | -| 5 | 1005 | 牛客 555 号 | 4000 | 7 | C++ | 2020-01-11 10:00:00 | -| 6 | 1006 | 666666 | 3000 | 6 | C++ | 2020-11-01 10:00:00 | - -请从中找到注册时间最早的 3 个人。由示例数据结果输出如下: - -| uid | nick_name | register_time | -| ---- | ------------ | ------------------- | -| 1001 | 牛客 1 | 2020-01-01 10:00:00 | -| 1003 | 牛客 3 号 ♂ | 2020-01-02 10:00:00 | -| 1004 | 牛客 4 号 | 2020-01-02 11:00:00 | - -解释:按注册时间排序后选取前三名,输出其用户 ID、昵称、注册时间。 - -**答案**: - -```sql -SELECT uid, nick_name, register_time - FROM user_info - ORDER BY register_time - LIMIT 3 -``` - -### 注册当天就完成了试卷的名单第三页(较难) - -**描述**:现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): - -| id | uid | nick_name | achievement | level | job | register_time | -| --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | -| 1 | 1001 | 牛客 1 | 19 | 0 | 算法 | 2020-01-01 10:00:00 | -| 2 | 1002 | 牛客 2 号 | 1200 | 3 | 算法 | 2020-01-01 10:00:00 | -| 3 | 1003 | 牛客 3 号 ♂ | 22 | 0 | 算法 | 2020-01-01 10:00:00 | -| 4 | 1004 | 牛客 4 号 | 25 | 0 | 算法 | 2020-01-01 10:00:00 | -| 5 | 1005 | 牛客 555 号 | 4000 | 7 | 算法 | 2020-01-11 10:00:00 | -| 6 | 1006 | 牛客 6 号 | 25 | 0 | 算法 | 2020-01-02 11:00:00 | -| 7 | 1007 | 牛客 7 号 | 25 | 0 | 算法 | 2020-01-02 11:00:00 | -| 8 | 1008 | 牛客 8 号 | 25 | 0 | 算法 | 2020-01-02 11:00:00 | -| 9 | 1009 | 牛客 9 号 | 25 | 0 | 算法 | 2020-01-02 11:00:00 | -| 10 | 1010 | 牛客 10 号 | 25 | 0 | 算法 | 2020-01-02 11:00:00 | -| 11 | 1011 | 666666 | 3000 | 6 | C++ | 2020-01-02 10:00:00 | - -试卷信息表 examination_info(exam_id 试卷 ID, tag 试卷类别, difficulty 试卷难度, duration 考试时长, release_time 发布时间): - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ---- | ---------- | -------- | ------------------- | -| 1 | 9001 | 算法 | hard | 60 | 2020-01-01 10:00:00 | -| 2 | 9002 | 算法 | hard | 80 | 2020-01-01 10:00:00 | -| 3 | 9003 | SQL | medium | 70 | 2020-01-01 10:00:00 | - -试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ----- | -| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:59 | 80 | -| 2 | 1002 | 9003 | 2020-01-20 10:01:01 | 2020-01-20 10:10:01 | 81 | -| 3 | 1002 | 9002 | 2020-01-01 12:11:01 | 2020-01-01 12:31:01 | 83 | -| 4 | 1003 | 9002 | 2020-01-01 19:01:01 | 2020-01-01 19:30:01 | 75 | -| 5 | 1004 | 9002 | 2020-01-01 12:01:01 | 2020-01-01 12:11:01 | 60 | -| 6 | 1005 | 9002 | 2020-01-01 12:01:01 | 2020-01-01 12:41:01 | 90 | -| 7 | 1006 | 9001 | 2020-01-02 19:01:01 | 2020-01-02 19:32:00 | 20 | -| 8 | 1007 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:40:01 | 89 | -| 9 | 1008 | 9003 | 2020-01-02 12:01:01 | 2020-01-02 12:20:01 | 99 | -| 10 | 1008 | 9001 | 2020-01-02 12:01:01 | 2020-01-02 12:31:01 | 98 | -| 11 | 1009 | 9002 | 2020-01-02 12:01:01 | 2020-01-02 12:31:01 | 82 | -| 12 | 1010 | 9002 | 2020-01-02 12:11:01 | 2020-01-02 12:41:01 | 76 | -| 13 | 1011 | 9001 | 2020-01-02 10:01:01 | 2020-01-02 10:31:01 | 89 | - -![](https://oss.javaguide.cn/github/javaguide/database/sql/D2B491866B85826119EE3474F10D3636.png) - -找到求职方向为算法工程师,且注册当天就完成了算法类试卷的人,按参加过的所有考试最高得分排名。排名榜很长,我们将采用分页展示,每页 3 条,现在需要你取出第 3 页(页码从 1 开始)的人的信息。 - -由示例数据结果输出如下: - -| uid | level | register_time | max_score | -| ---- | ----- | ------------------- | --------- | -| 1010 | 0 | 2020-01-02 11:00:00 | 76 | -| 1003 | 0 | 2020-01-01 10:00:00 | 75 | -| 1004 | 0 | 2020-01-01 11:00:00 | 60 | - -解释:除了 1011 其他用户的求职方向都为算法工程师;算法类试卷有 9001 和 9002,11 个用户注册当天都完成了算法类试卷;计算他们的所有考试最大分时,只有 1002 和 1008 完成了两次考试,其他人只完成了一场考试,1002 两场考试最高分为 81,1008 最高分为 99。 - -按最高分排名如下: - -| uid | level | register_time | max_score | -| ---- | ----- | ------------------- | --------- | -| 1008 | 0 | 2020-01-02 11:00:00 | 99 | -| 1005 | 7 | 2020-01-01 10:00:00 | 90 | -| 1007 | 0 | 2020-01-02 11:00:00 | 89 | -| 1002 | 3 | 2020-01-01 10:00:00 | 83 | -| 1009 | 0 | 2020-01-02 11:00:00 | 82 | -| 1001 | 0 | 2020-01-01 10:00:00 | 80 | -| 1010 | 0 | 2020-01-02 11:00:00 | 76 | -| 1003 | 0 | 2020-01-01 10:00:00 | 75 | -| 1004 | 0 | 2020-01-01 11:00:00 | 60 | -| 1006 | 0 | 2020-01-02 11:00:00 | 20 | - -每页 3 条,第三页也就是第 7~9 条,返回 1010、1003、1004 的行记录即可。 - -**思路**: - -1. 每页三条,即需要取出第三页的人的信息,要用到`limit` - -2. 统计求职方向为算法工程师且注册当天就完成了算法类试卷的人的**信息和每次记录的得分**,先求满足条件的用户,后用 left join 做连接查找信息和每次记录的得分 - -**答案**: - -```sql -SELECT t1.uid, - LEVEL, - register_time, - max(score) AS max_score -FROM exam_record t -JOIN examination_info USING (exam_id) -JOIN user_info t1 ON t.uid = t1.uid -AND date(t.submit_time) = date(t1.register_time) -WHERE job = '算法' - AND tag = '算法' -GROUP BY t1.uid, - LEVEL, - register_time -ORDER BY max_score DESC -LIMIT 6,3 -``` - -## 文本转换函数 - -### 修复串列了的记录 - -**描述**:现有试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | -------------- | ---------- | -------- | ------------------- | -| 1 | 9001 | 算法 | hard | 60 | 2021-01-01 10:00:00 | -| 2 | 9002 | 算法 | hard | 80 | 2021-01-01 10:00:00 | -| 3 | 9003 | SQL | medium | 70 | 2021-01-01 10:00:00 | -| 4 | 9004 | 算法,medium,80 | | 0 | 2021-01-01 10:00:00 | - -录题同学有一次手误将部分记录的试题类别 tag、难度、时长同时录入到了 tag 字段,请帮忙找出这些录错了的记录,并拆分后按正确的列类型输出。 - -由示例数据结果输出如下: - -| exam_id | tag | difficulty | duration | -| ------- | ---- | ---------- | -------- | -| 9004 | 算法 | medium | 80 | - -**思路**: - -先来学习下本题要用到的函数 - -`SUBSTRING_INDEX` 函数用于提取字符串中指定分隔符的部分。它接受三个参数:原始字符串、分隔符和指定要返回的部分的数量。 - -以下是 `SUBSTRING_INDEX` 函数的语法: - -```sql -SUBSTRING_INDEX(str, delimiter, count) -``` - -- `str`:要进行分割的原始字符串。 -- `delimiter`:用作分割的字符串或字符。 -- `count`:指定要返回的部分的数量。 - - 如果 `count` 大于 0,则返回从左边开始的前 `count` 个部分(以分隔符为界)。 - - 如果 `count` 小于 0,则返回从右边开始的前 `count` 个部分(以分隔符为界),即从右侧向左计数。 - -下面是一些示例,演示了 `SUBSTRING_INDEX` 函数的使用: - -1. 提取字符串中的第一个部分: - - ```sql - SELECT SUBSTRING_INDEX('apple,banana,cherry', ',', 1); - -- 输出结果:'apple' - ``` - -2. 提取字符串中的最后一个部分: - - ```sql - SELECT SUBSTRING_INDEX('apple,banana,cherry', ',', -1); - -- 输出结果:'cherry' - ``` - -3. 提取字符串中的前两个部分: - - ```sql - SELECT SUBSTRING_INDEX('apple,banana,cherry', ',', 2); - -- 输出结果:'apple,banana' - ``` - -4. 提取字符串中的最后两个部分: - - ```sql - SELECT SUBSTRING_INDEX('apple,banana,cherry', ',', -2); - -- 输出结果:'banana,cherry' - ``` - -**答案**: - -```sql -SELECT - exam_id, - substring_index( tag, ',', 1 ) tag, - substring_index( substring_index( tag, ',', 2 ), ',',- 1 ) difficulty, - substring_index( tag, ',',- 1 ) duration -FROM - examination_info -WHERE - difficulty = '' -``` - -### 对过长的昵称截取处理 - -**描述**:现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): - -| id | uid | nick_name | achievement | level | job | register_time | -| --- | ---- | ---------------------- | ----------- | ----- | ---- | ------------------- | -| 1 | 1001 | 牛客 1 | 19 | 0 | 算法 | 2020-01-01 10:00:00 | -| 2 | 1002 | 牛客 2 号 | 1200 | 3 | 算法 | 2020-01-01 10:00:00 | -| 3 | 1003 | 牛客 3 号 ♂ | 22 | 0 | 算法 | 2020-01-01 10:00:00 | -| 4 | 1004 | 牛客 4 号 | 25 | 0 | 算法 | 2020-01-01 11:00:00 | -| 5 | 1005 | 牛客 5678901234 号 | 4000 | 7 | 算法 | 2020-01-11 10:00:00 | -| 6 | 1006 | 牛客 67890123456789 号 | 25 | 0 | 算法 | 2020-01-02 11:00:00 | - -有的用户的昵称特别长,在一些展示场景会导致样式混乱,因此需要将特别长的昵称转换一下再输出,请输出字符数大于 10 的用户信息,对于字符数大于 13 的用户输出前 10 个字符然后加上三个点号:『...』。 - -由示例数据结果输出如下: - -| uid | nick_name | -| ---- | ------------------ | -| 1005 | 牛客 5678901234 号 | -| 1006 | 牛客 67890123... | - -解释:字符数大于 10 的用户有 1005 和 1006,长度分别为 13、17;因此需要对 1006 的昵称截断输出。 - -**思路**: - -这题涉及到字符的计算,要计算字符串的字符数(即字符串的长度),可以使用 `LENGTH` 函数或 `CHAR_LENGTH` 函数。这两个函数的区别在于对待多字节字符的方式。 - -1. `LENGTH` 函数:它返回给定字符串的字节数。对于包含多字节字符的字符串,每个字符都会被当作一个字节来计算。 - -示例: - -```sql -SELECT LENGTH('你好'); -- 输出结果:6,因为 '你好' 中的每个汉字每个占3个字节 -``` - -1. `CHAR_LENGTH` 函数:它返回给定字符串的字符数。对于包含多字节字符的字符串,每个字符会被当作一个字符来计算。 - -示例: - -```sql -SELECT CHAR_LENGTH('你好'); -- 输出结果:2,因为 '你好' 中有两个字符,即两个汉字 -``` - -**答案**: - -```sql -SELECT - uid, -CASE - - WHEN CHAR_LENGTH( nick_name ) > 13 THEN - CONCAT( SUBSTR( nick_name, 1, 10 ), '...' ) ELSE nick_name - END AS nick_name -FROM - user_info -WHERE - CHAR_LENGTH( nick_name ) > 10 -GROUP BY - uid; -``` - -### 大小写混乱时的筛选统计(较难) - -**描述**: - -现有试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): - -| id | exam_id | tag | difficulty | duration | release_time | -| --- | ------- | ---- | ---------- | -------- | ------------------- | -| 1 | 9001 | 算法 | hard | 60 | 2021-01-01 10:00:00 | -| 2 | 9002 | C++ | hard | 80 | 2021-01-01 10:00:00 | -| 3 | 9003 | C++ | hard | 80 | 2021-01-01 10:00:00 | -| 4 | 9004 | sql | medium | 70 | 2021-01-01 10:00:00 | -| 5 | 9005 | C++ | hard | 80 | 2021-01-01 10:00:00 | -| 6 | 9006 | C++ | hard | 80 | 2021-01-01 10:00:00 | -| 7 | 9007 | C++ | hard | 80 | 2021-01-01 10:00:00 | -| 8 | 9008 | SQL | medium | 70 | 2021-01-01 10:00:00 | -| 9 | 9009 | SQL | medium | 70 | 2021-01-01 10:00:00 | -| 10 | 9010 | SQL | medium | 70 | 2021-01-01 10:00:00 | - -试卷作答信息表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): - -| id | uid | exam_id | start_time | submit_time | score | -| --- | ---- | ------- | ------------------- | ------------------- | ------ | -| 1 | 1001 | 9001 | 2020-01-01 09:01:01 | 2020-01-01 09:21:59 | 80 | -| 2 | 1002 | 9003 | 2020-01-20 10:01:01 | 2020-01-20 10:10:01 | 81 | -| 3 | 1002 | 9002 | 2020-02-01 12:11:01 | 2020-02-01 12:31:01 | 83 | -| 4 | 1003 | 9002 | 2020-03-01 19:01:01 | 2020-03-01 19:30:01 | 75 | -| 5 | 1004 | 9002 | 2020-03-01 12:01:01 | 2020-03-01 12:11:01 | 60 | -| 6 | 1005 | 9002 | 2020-03-01 12:01:01 | 2020-03-01 12:41:01 | 90 | -| 7 | 1006 | 9001 | 2020-05-02 19:01:01 | 2020-05-02 19:32:00 | 20 | -| 8 | 1007 | 9003 | 2020-01-02 19:01:01 | 2020-01-02 19:40:01 | 89 | -| 9 | 1008 | 9004 | 2020-02-02 12:01:01 | 2020-02-02 12:20:01 | 99 | -| 10 | 1008 | 9001 | 2020-02-02 12:01:01 | 2020-02-02 12:31:01 | 98 | -| 11 | 1009 | 9002 | 2020-02-02 12:01:01 | 2020-01-02 12:43:01 | 81 | -| 12 | 1010 | 9001 | 2020-01-02 12:11:01 | (NULL) | (NULL) | -| 13 | 1010 | 9001 | 2020-02-02 12:01:01 | 2020-01-02 10:31:01 | 89 | - -试卷的类别 tag 可能出现大小写混乱的情况,请先筛选出试卷作答数小于 3 的类别 tag,统计将其转换为大写后对应的原本试卷作答数。 - -如果转换后 tag 并没有发生变化,不输出该条结果。 - -由示例数据结果输出如下: - -| tag | answer_cnt | -| --- | ---------- | -| C++ | 6 | - -解释:被作答过的试卷有 9001、9002、9003、9004,他们的 tag 和被作答次数如下: - -| exam_id | tag | answer_cnt | -| ------- | ---- | ---------- | -| 9001 | 算法 | 4 | -| 9002 | C++ | 6 | -| 9003 | c++ | 2 | -| 9004 | sql | 2 | - -作答次数小于 3 的 tag 有 c++和 sql,而转为大写后只有 C++本来就有作答数,于是输出 c++转化大写后的作答次数为 6。 - -**思路**: - -首先,这题有点混乱,9004 根据示例数据查出来只有 1 次,这里显示有 2 次。 - -先看一下大小写转换函数: - -1.`UPPER(s)`或`UCASE(s)`函数可以将字符串 s 中的字母字符全部转换成大写字母; - -2.`LOWER(s)`或者`LCASE(s)`函数可以将字符串 s 中的字母字符全部转换成小写字母。 - -难点在于相同表做连接要查询不同的值 - -**答案**: - -```sql -WITH a AS - (SELECT tag, - COUNT(start_time) AS answer_cnt - FROM exam_record er - JOIN examination_info ei ON er.exam_id = ei.exam_id - GROUP BY tag) -SELECT a.tag, - b.answer_cnt -FROM a -INNER JOIN a AS b ON UPPER(a.tag)= b.tag #a小写 b大写 -AND a.tag != b.tag -WHERE a.answer_cnt < 3; -``` - - +| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | +| 2 | 9002 | SQL | easy | 60 | 2020-01-01 | diff --git a/docs/database/sql/sql-syntax-summary.md b/docs/database/sql/sql-syntax-summary.md index cff0b931495..c11eadd7bd9 100644 --- a/docs/database/sql/sql-syntax-summary.md +++ b/docs/database/sql/sql-syntax-summary.md @@ -1,1212 +1,100 @@ --- -title: SQL语法基础知识总结 -category: 数据库 +title: Summary of Basic SQL Syntax +category: Database tag: - - 数据库基础 + - Database Basics - SQL --- -> 本文整理完善自下面这两份资料: +> This article is organized and improved based on the following two resources: > -> - [SQL 语法速成手册](https://juejin.cn/post/6844903790571700231) -> - [MySQL 超全教程](https://www.begtut.com/mysql/mysql-tutorial.html) +> - [SQL Syntax Quick Reference](https://juejin.cn/post/6844903790571700231) +> - [Comprehensive MySQL Tutorial](https://www.begtut.com/mysql/mysql-tutorial.html) -## 基本概念 +## Basic Concepts -### 数据库术语 +### Database Terminology -- `数据库(database)` - 保存有组织的数据的容器(通常是一个文件或一组文件)。 -- `数据表(table)` - 某种特定类型数据的结构化清单。 -- `模式(schema)` - 关于数据库和表的布局及特性的信息。模式定义了数据在表中如何存储,包含存储什么样的数据,数据如何分解,各部分信息如何命名等信息。数据库和表都有模式。 -- `列(column)` - 表中的一个字段。所有表都是由一个或多个列组成的。 -- `行(row)` - 表中的一个记录。 -- `主键(primary key)` - 一列(或一组列),其值能够唯一标识表中每一行。 +- `Database` - A container for storing organized data (usually a file or a set of files). +- `Table` - A structured list of a specific type of data. +- `Schema` - Information about the layout and characteristics of the database and tables. The schema defines how data is stored in tables, what types of data are stored, how data is decomposed, and how parts of the information are named. Both databases and tables have schemas. +- `Column` - A field in a table. All tables consist of one or more columns. +- `Row` - A record in a table. +- `Primary Key` - A column (or a set of columns) whose values uniquely identify each row in the table. -### SQL 语法 +### SQL Syntax -SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。 +SQL (Structured Query Language) is managed by the ANSI standards committee, hence it is referred to as ANSI SQL. Each DBMS has its own implementation, such as PL/SQL, Transact-SQL, etc. -#### SQL 语法结构 +#### SQL Syntax Structure ![](https://oss.javaguide.cn/p3-juejin/cb684d4c75fc430e92aaee226069c7da~tplv-k3u1fbpfcp-zoom-1.png) -SQL 语法结构包括: +The SQL syntax structure includes: -- **`子句`** - 是语句和查询的组成成分。(在某些情况下,这些都是可选的。) -- **`表达式`** - 可以产生任何标量值,或由列和行的数据库表 -- **`谓词`** - 给需要评估的 SQL 三值逻辑(3VL)(true/false/unknown)或布尔真值指定条件,并限制语句和查询的效果,或改变程序流程。 -- **`查询`** - 基于特定条件检索数据。这是 SQL 的一个重要组成部分。 -- **`语句`** - 可以持久地影响纲要和数据,也可以控制数据库事务、程序流程、连接、会话或诊断。 +- **`Clause`** - Components of statements and queries. (In some cases, these are optional.) +- **`Expression`** - Can produce any scalar value or be derived from columns and rows of a database table. +- **`Predicate`** - Specifies conditions for evaluating SQL three-valued logic (3VL) (true/false/unknown) or Boolean truth values, and limits the effects of statements and queries or alters program flow. +- **`Query`** - Retrieves data based on specific conditions. This is an important component of SQL. +- **`Statement`** - Can permanently affect schemas and data, and can also control database transactions, program flow, connections, sessions, or diagnostics. -#### SQL 语法要点 +#### Key Points of SQL Syntax -- **SQL 语句不区分大小写**,但是数据库表名、列名和值是否区分,依赖于具体的 DBMS 以及配置。例如:`SELECT` 与 `select`、`Select` 是相同的。 -- **多条 SQL 语句必须以分号(`;`)分隔**。 -- 处理 SQL 语句时,**所有空格都被忽略**。 +- **SQL statements are case-insensitive**, but whether database table names, column names, and values are case-sensitive depends on the specific DBMS and configuration. For example, `SELECT`, `select`, and `Select` are the same. +- **Multiple SQL statements must be separated by a semicolon (`;`).** +- When processing SQL statements, **all whitespace is ignored**. -SQL 语句可以写成一行,也可以分写为多行。 +SQL statements can be written in a single line or split into multiple lines. ```sql --- 一行 SQL 语句 +-- Single line SQL statement UPDATE user SET username='robot', password='robot' WHERE username = 'root'; --- 多行 SQL 语句 +-- Multi-line SQL statement UPDATE user SET username='robot', password='robot' WHERE username = 'root'; ``` -SQL 支持三种注释: +SQL supports three types of comments: ```sql -## 注释1 --- 注释2 -/* 注释3 */ +## Comment 1 +-- Comment 2 +/* Comment 3 */ ``` -### SQL 分类 +### SQL Classification -#### 数据定义语言(DDL) +#### Data Definition Language (DDL) -数据定义语言(Data Definition Language,DDL)是 SQL 语言集中负责数据结构定义与数据库对象定义的语言。 +Data Definition Language (DDL) is the part of SQL responsible for defining data structures and database objects. -DDL 的主要功能是**定义数据库对象**。 +The main function of DDL is to **define database objects**. -DDL 的核心指令是 `CREATE`、`ALTER`、`DROP`。 +The core commands of DDL are `CREATE`, `ALTER`, `DROP`. -#### 数据操纵语言(DML) +#### Data Manipulation Language (DML) -数据操纵语言(Data Manipulation Language, DML)是用于数据库操作,对数据库其中的对象和数据运行访问工作的编程语句。 +Data Manipulation Language (DML) is used for database operations, executing access operations on database objects and data. -DML 的主要功能是 **访问数据**,因此其语法都是以**读写数据库**为主。 +The main function of DML is to **access data**, so its syntax is primarily focused on **reading and writing databases**. -DML 的核心指令是 `INSERT`、`UPDATE`、`DELETE`、`SELECT`。这四个指令合称 CRUD(Create, Read, Update, Delete),即增删改查。 +The core commands of DML are `INSERT`, `UPDATE`, `DELETE`, `SELECT`. These four commands are collectively referred to as CRUD (Create, Read, Update, Delete). -#### 事务控制语言(TCL) +#### Transaction Control Language (TCL) -事务控制语言 (Transaction Control Language, TCL) 用于**管理数据库中的事务**。这些用于管理由 DML 语句所做的更改。它还允许将语句分组为逻辑事务。 +Transaction Control Language (TCL) is used to **manage transactions in the database**. These manage changes made by DML statements. It also allows grouping statements into logical transactions. -TCL 的核心指令是 `COMMIT`、`ROLLBACK`。 +The core commands of TCL are `COMMIT`, `ROLLBACK`. -#### 数据控制语言(DCL) +#### Data Control Language (DCL) -数据控制语言 (Data Control Language, DCL) 是一种可对数据访问权进行控制的指令,它可以控制特定用户账户对数据表、查看表、预存程序、用户自定义函数等数据库对象的控制权。 +Data Control Language (DCL) is a set of commands that control data access rights, allowing control over specific user accounts regarding database objects such as tables, views, stored procedures, and user-defined functions. -DCL 的核心指令是 `GRANT`、`REVOKE`。 +The core commands of DCL are `GRANT`, `REVOKE`. -DCL 以**控制用户的访问权限**为主,因此其指令作法并不复杂,可利用 DCL 控制的权限有:`CONNECT`、`SELECT`、`INSERT`、`UPDATE`、`DELETE`、`EXECUTE`、`USAGE`、`REFERENCES`。 +DCL primarily focuses on **controlling user access rights**, so its commands are not complex. The permissions that can be controlled by DCL include: `CONNECT`, `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `EXECUTE`, `USAGE`, `REFERENCES`. -根据不同的 DBMS 以及不同的安全性实体,其支持的权限控制也有所不同。 - -**我们先来介绍 DML 语句用法。 DML 的主要功能是读写数据库实现增删改查。** - -## 增删改查 - -增删改查,又称为 CRUD,数据库基本操作中的基本操作。 - -### 插入数据 - -`INSERT INTO` 语句用于向表中插入新记录。 - -**插入完整的行** - -```sql -# 插入一行 -INSERT INTO user -VALUES (10, 'root', 'root', 'xxxx@163.com'); -# 插入多行 -INSERT INTO user -VALUES (10, 'root', 'root', 'xxxx@163.com'), (12, 'user1', 'user1', 'xxxx@163.com'), (18, 'user2', 'user2', 'xxxx@163.com'); -``` - -**插入行的一部分** - -```sql -INSERT INTO user(username, password, email) -VALUES ('admin', 'admin', 'xxxx@163.com'); -``` - -**插入查询出来的数据** - -```sql -INSERT INTO user(username) -SELECT name -FROM account; -``` - -### 更新数据 - -`UPDATE` 语句用于更新表中的记录。 - -```sql -UPDATE user -SET username='robot', password='robot' -WHERE username = 'root'; -``` - -### 删除数据 - -- `DELETE` 语句用于删除表中的记录。 -- `TRUNCATE TABLE` 可以清空表,也就是删除所有行。说明:`TRUNCATE` 语句不属于 DML 语法而是 DDL 语法。 - -**删除表中的指定数据** - -```sql -DELETE FROM user -WHERE username = 'robot'; -``` - -**清空表中的数据** - -```sql -TRUNCATE TABLE user; -``` - -### 查询数据 - -`SELECT` 语句用于从数据库中查询数据。 - -`DISTINCT` 用于返回唯一不同的值。它作用于所有列,也就是说所有列的值都相同才算相同。 - -`LIMIT` 限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。 - -- `ASC`:升序(默认) -- `DESC`:降序 - -**查询单列** - -```sql -SELECT prod_name -FROM products; -``` - -**查询多列** - -```sql -SELECT prod_id, prod_name, prod_price -FROM products; -``` - -**查询所有列** - -```sql -SELECT * -FROM products; -``` - -**查询不同的值** - -```sql -SELECT DISTINCT -vend_id FROM products; -``` - -**限制查询结果** - -```sql --- 返回前 5 行 -SELECT * FROM mytable LIMIT 5; -SELECT * FROM mytable LIMIT 0, 5; --- 返回第 3 ~ 5 行 -SELECT * FROM mytable LIMIT 2, 3; -``` - -## 排序 - -`order by` 用于对结果集按照一个列或者多个列进行排序。默认按照升序对记录进行排序,如果需要按照降序对记录进行排序,可以使用 `desc` 关键字。 - -`order by` 对多列排序的时候,先排序的列放前面,后排序的列放后面。并且,不同的列可以有不同的排序规则。 - -```sql -SELECT * FROM products -ORDER BY prod_price DESC, prod_name ASC; -``` - -## 分组 - -**`group by`**: - -- `group by` 子句将记录分组到汇总行中。 -- `group by` 为每个组返回一个记录。 -- `group by` 通常还涉及聚合`count`,`max`,`sum`,`avg` 等。 -- `group by` 可以按一列或多列进行分组。 -- `group by` 按分组字段进行排序后,`order by` 可以以汇总字段来进行排序。 - -**分组** - -```sql -SELECT cust_name, COUNT(cust_address) AS addr_num -FROM Customers GROUP BY cust_name; -``` - -**分组后排序** - -```sql -SELECT cust_name, COUNT(cust_address) AS addr_num -FROM Customers GROUP BY cust_name -ORDER BY cust_name DESC; -``` - -**`having`**: - -- `having` 用于对汇总的 `group by` 结果进行过滤。 -- `having` 一般都是和 `group by` 连用。 -- `where` 和 `having` 可以在相同的查询中。 - -**使用 WHERE 和 HAVING 过滤数据** - -```sql -SELECT cust_name, COUNT(*) AS NumberOfOrders -FROM Customers -WHERE cust_email IS NOT NULL -GROUP BY cust_name -HAVING COUNT(*) > 1; -``` - -**`having` vs `where`**: - -- `where`:过滤过滤指定的行,后面不能加聚合函数(分组函数)。`where` 在`group by` 前。 -- `having`:过滤分组,一般都是和 `group by` 连用,不能单独使用。`having` 在 `group by` 之后。 - -## 子查询 - -子查询是嵌套在较大查询中的 SQL 查询,也称内部查询或内部选择,包含子查询的语句也称为外部查询或外部选择。简单来说,子查询就是指将一个 `select` 查询(子查询)的结果作为另一个 SQL 语句(主查询)的数据来源或者判断条件。 - -子查询可以嵌入 `SELECT`、`INSERT`、`UPDATE` 和 `DELETE` 语句中,也可以和 `=`、`<`、`>`、`IN`、`BETWEEN`、`EXISTS` 等运算符一起使用。 - -子查询常用在 `WHERE` 子句和 `FROM` 子句后边: - -- 当用于 `WHERE` 子句时,根据不同的运算符,子查询可以返回单行单列、多行单列、单行多列数据。子查询就是要返回能够作为 `WHERE` 子句查询条件的值。 -- 当用于 `FROM` 子句时,一般返回多行多列数据,相当于返回一张临时表,这样才符合 `FROM` 后面是表的规则。这种做法能够实现多表联合查询。 - -> 注意:MYSQL 数据库从 4.1 版本才开始支持子查询,早期版本是不支持的。 - -用于 `WHERE` 子句的子查询的基本语法如下: - -```sql -select column_name [, column_name ] -from table1 [, table2 ] -where column_name operator - (select column_name [, column_name ] - from table1 [, table2 ] - [where]) -``` - -- 子查询需要放在括号`( )`内。 -- `operator` 表示用于 where 子句的运算符。 - -用于 `FROM` 子句的子查询的基本语法如下: - -```sql -select column_name [, column_name ] -from (select column_name [, column_name ] - from table1 [, table2 ] - [where]) as temp_table_name -where condition -``` - -用于 `FROM` 的子查询返回的结果相当于一张临时表,所以需要使用 AS 关键字为该临时表起一个名字。 - -**子查询的子查询** - -```sql -SELECT cust_name, cust_contact -FROM customers -WHERE cust_id IN (SELECT cust_id - FROM orders - WHERE order_num IN (SELECT order_num - FROM orderitems - WHERE prod_id = 'RGAN01')); -``` - -内部查询首先在其父查询之前执行,以便可以将内部查询的结果传递给外部查询。执行过程可以参考下图: - -![](https://oss.javaguide.cn/p3-juejin/c439da1f5d4e4b00bdfa4316b933d764~tplv-k3u1fbpfcp-zoom-1.png) - -### WHERE - -- `WHERE` 子句用于过滤记录,即缩小访问数据的范围。 -- `WHERE` 后跟一个返回 `true` 或 `false` 的条件。 -- `WHERE` 可以与 `SELECT`,`UPDATE` 和 `DELETE` 一起使用。 -- 可以在 `WHERE` 子句中使用的操作符。 - -| 运算符 | 描述 | -| ------- | ------------------------------------------------------ | -| = | 等于 | -| <> | 不等于。注释:在 SQL 的一些版本中,该操作符可被写成 != | -| > | 大于 | -| < | 小于 | -| >= | 大于等于 | -| <= | 小于等于 | -| BETWEEN | 在某个范围内 | -| LIKE | 搜索某种模式 | -| IN | 指定针对某个列的多个可能值 | - -**`SELECT` 语句中的 `WHERE` 子句** - -```ini -SELECT * FROM Customers -WHERE cust_name = 'Kids Place'; -``` - -**`UPDATE` 语句中的 `WHERE` 子句** - -```ini -UPDATE Customers -SET cust_name = 'Jack Jones' -WHERE cust_name = 'Kids Place'; -``` - -**`DELETE` 语句中的 `WHERE` 子句** - -```ini -DELETE FROM Customers -WHERE cust_name = 'Kids Place'; -``` - -### IN 和 BETWEEN - -- `IN` 操作符在 `WHERE` 子句中使用,作用是在指定的几个特定值中任选一个值。 -- `BETWEEN` 操作符在 `WHERE` 子句中使用,作用是选取介于某个范围内的值。 - -**IN 示例** - -```sql -SELECT * -FROM products -WHERE vend_id IN ('DLL01', 'BRS01'); -``` - -**BETWEEN 示例** - -```sql -SELECT * -FROM products -WHERE prod_price BETWEEN 3 AND 5; -``` - -### AND、OR、NOT - -- `AND`、`OR`、`NOT` 是用于对过滤条件的逻辑处理指令。 -- `AND` 优先级高于 `OR`,为了明确处理顺序,可以使用 `()`。 -- `AND` 操作符表示左右条件都要满足。 -- `OR` 操作符表示左右条件满足任意一个即可。 -- `NOT` 操作符用于否定一个条件。 - -**AND 示例** - -```sql -SELECT prod_id, prod_name, prod_price -FROM products -WHERE vend_id = 'DLL01' AND prod_price <= 4; -``` - -**OR 示例** - -```ini -SELECT prod_id, prod_name, prod_price -FROM products -WHERE vend_id = 'DLL01' OR vend_id = 'BRS01'; -``` - -**NOT 示例** - -```sql -SELECT * -FROM products -WHERE prod_price NOT BETWEEN 3 AND 5; -``` - -### LIKE - -- `LIKE` 操作符在 `WHERE` 子句中使用,作用是确定字符串是否匹配模式。 -- 只有字段是文本值时才使用 `LIKE`。 -- `LIKE` 支持两个通配符匹配选项:`%` 和 `_`。 -- 不要滥用通配符,通配符位于开头处匹配会非常慢。 -- `%` 表示任何字符出现任意次数。 -- `_` 表示任何字符出现一次。 - -**% 示例** - -```sql -SELECT prod_id, prod_name, prod_price -FROM products -WHERE prod_name LIKE '%bean bag%'; -``` - -**\_ 示例** - -```sql -SELECT prod_id, prod_name, prod_price -FROM products -WHERE prod_name LIKE '__ inch teddy bear'; -``` - -## 连接 - -JOIN 是“连接”的意思,顾名思义,SQL JOIN 子句用于将两个或者多个表联合起来进行查询。 - -连接表时需要在每个表中选择一个字段,并对这些字段的值进行比较,值相同的两条记录将合并为一条。**连接表的本质就是将不同表的记录合并起来,形成一张新表。当然,这张新表只是临时的,它仅存在于本次查询期间**。 - -使用 `JOIN` 连接两个表的基本语法如下: - -```sql -select table1.column1, table2.column2... -from table1 -join table2 -on table1.common_column1 = table2.common_column2; -``` - -`table1.common_column1 = table2.common_column2` 是连接条件,只有满足此条件的记录才会合并为一行。您可以使用多个运算符来连接表,例如 =、>、<、<>、<=、>=、!=、`between`、`like` 或者 `not`,但是最常见的是使用 =。 - -当两个表中有同名的字段时,为了帮助数据库引擎区分是哪个表的字段,在书写同名字段名时需要加上表名。当然,如果书写的字段名在两个表中是唯一的,也可以不使用以上格式,只写字段名即可。 - -另外,如果两张表的关联字段名相同,也可以使用 `USING`子句来代替 `ON`,举个例子: - -```sql -# join....on -select c.cust_name, o.order_num -from Customers c -inner join Orders o -on c.cust_id = o.cust_id -order by c.cust_name; - -# 如果两张表的关联字段名相同,也可以使用USING子句:join....using() -select c.cust_name, o.order_num -from Customers c -inner join Orders o -using(cust_id) -order by c.cust_name; -``` - -**`ON` 和 `WHERE` 的区别**: - -- 连接表时,SQL 会根据连接条件生成一张新的临时表。`ON` 就是连接条件,它决定临时表的生成。 -- `WHERE` 是在临时表生成以后,再对临时表中的数据进行过滤,生成最终的结果集,这个时候已经没有 JOIN-ON 了。 - -所以总结来说就是:**SQL 先根据 ON 生成一张临时表,然后再根据 WHERE 对临时表进行筛选**。 - -SQL 允许在 `JOIN` 左边加上一些修饰性的关键词,从而形成不同类型的连接,如下表所示: - -| 连接类型 | 说明 | -| ---------------------------------------- | --------------------------------------------------------------------------------------------- | -| INNER JOIN 内连接 | (默认连接方式)只有当两个表都存在满足条件的记录时才会返回行。 | -| LEFT JOIN / LEFT OUTER JOIN 左(外)连接 | 返回左表中的所有行,即使右表中没有满足条件的行也是如此。 | -| RIGHT JOIN / RIGHT OUTER JOIN 右(外)连接 | 返回右表中的所有行,即使左表中没有满足条件的行也是如此。 | -| FULL JOIN / FULL OUTER JOIN 全(外)连接 | 只要其中有一个表存在满足条件的记录,就返回行。 | -| SELF JOIN | 将一个表连接到自身,就像该表是两个表一样。为了区分两个表,在 SQL 语句中需要至少重命名一个表。 | -| CROSS JOIN | 交叉连接,从两个或者多个连接表中返回记录集的笛卡尔积。 | - -下图展示了 LEFT JOIN、RIGHT JOIN、INNER JOIN、OUTER JOIN 相关的 7 种用法。 - -![](https://oss.javaguide.cn/p3-juejin/701670942f0f45d3a3a2187cd04a12ad~tplv-k3u1fbpfcp-zoom-1.png) - -如果不加任何修饰词,只写 `JOIN`,那么默认为 `INNER JOIN` - -对于 `INNER JOIN` 来说,还有一种隐式的写法,称为 “**隐式内连接**”,也就是没有 `INNER JOIN` 关键字,使用 `WHERE` 语句实现内连接的功能 - -```sql -# 隐式内连接 -select c.cust_name, o.order_num -from Customers c, Orders o -where c.cust_id = o.cust_id -order by c.cust_name; - -# 显式内连接 -select c.cust_name, o.order_num -from Customers c inner join Orders o -using(cust_id) -order by c.cust_name; -``` - -## 组合 - -`UNION` 运算符将两个或更多查询的结果组合起来,并生成一个结果集,其中包含来自 `UNION` 中参与查询的提取行。 - -`UNION` 基本规则: - -- 所有查询的列数和列顺序必须相同。 -- 每个查询中涉及表的列的数据类型必须相同或兼容。 -- 通常返回的列名取自第一个查询。 - -默认地,`UNION` 操作符选取不同的值。如果允许重复的值,请使用 `UNION ALL`。 - -```sql -SELECT column_name(s) FROM table1 -UNION ALL -SELECT column_name(s) FROM table2; -``` - -`UNION` 结果集中的列名总是等于 `UNION` 中第一个 `SELECT` 语句中的列名。 - -`JOIN` vs `UNION`: - -- `JOIN` 中连接表的列可能不同,但在 `UNION` 中,所有查询的列数和列顺序必须相同。 -- `UNION` 将查询之后的行放在一起(垂直放置),但 `JOIN` 将查询之后的列放在一起(水平放置),即它构成一个笛卡尔积。 - -## 函数 - -不同数据库的函数往往各不相同,因此不可移植。本节主要以 MySQL 的函数为例。 - -### 文本处理 - -| 函数 | 说明 | -| -------------------- | ---------------------- | -| `LEFT()`、`RIGHT()` | 左边或者右边的字符 | -| `LOWER()`、`UPPER()` | 转换为小写或者大写 | -| `LTRIM()`、`RTRIM()` | 去除左边或者右边的空格 | -| `LENGTH()` | 长度,以字节为单位 | -| `SOUNDEX()` | 转换为语音值 | - -其中, **`SOUNDEX()`** 可以将一个字符串转换为描述其语音表示的字母数字模式。 - -```sql -SELECT * -FROM mytable -WHERE SOUNDEX(col1) = SOUNDEX('apple') -``` - -### 日期和时间处理 - -- 日期格式:`YYYY-MM-DD` -- 时间格式:`HH:MM:SS` - -| 函 数 | 说 明 | -| --------------- | ------------------------------ | -| `AddDate()` | 增加一个日期(天、周等) | -| `AddTime()` | 增加一个时间(时、分等) | -| `CurDate()` | 返回当前日期 | -| `CurTime()` | 返回当前时间 | -| `Date()` | 返回日期时间的日期部分 | -| `DateDiff()` | 计算两个日期之差 | -| `Date_Add()` | 高度灵活的日期运算函数 | -| `Date_Format()` | 返回一个格式化的日期或时间串 | -| `Day()` | 返回一个日期的天数部分 | -| `DayOfWeek()` | 对于一个日期,返回对应的星期几 | -| `Hour()` | 返回一个时间的小时部分 | -| `Minute()` | 返回一个时间的分钟部分 | -| `Month()` | 返回一个日期的月份部分 | -| `Now()` | 返回当前日期和时间 | -| `Second()` | 返回一个时间的秒部分 | -| `Time()` | 返回一个日期时间的时间部分 | -| `Year()` | 返回一个日期的年份部分 | - -### 数值处理 - -| 函数 | 说明 | -| ------ | ------ | -| SIN() | 正弦 | -| COS() | 余弦 | -| TAN() | 正切 | -| ABS() | 绝对值 | -| SQRT() | 平方根 | -| MOD() | 余数 | -| EXP() | 指数 | -| PI() | 圆周率 | -| RAND() | 随机数 | - -### 汇总 - -| 函 数 | 说 明 | -| --------- | ---------------- | -| `AVG()` | 返回某列的平均值 | -| `COUNT()` | 返回某列的行数 | -| `MAX()` | 返回某列的最大值 | -| `MIN()` | 返回某列的最小值 | -| `SUM()` | 返回某列值之和 | - -`AVG()` 会忽略 NULL 行。 - -使用 `DISTINCT` 可以让汇总函数值汇总不同的值。 - -```sql -SELECT AVG(DISTINCT col1) AS avg_col -FROM mytable -``` - -**接下来,我们来介绍 DDL 语句用法。DDL 的主要功能是定义数据库对象(如:数据库、数据表、视图、索引等)** - -## 数据定义 - -### 数据库(DATABASE) - -#### 创建数据库 - -```sql -CREATE DATABASE test; -``` - -#### 删除数据库 - -```sql -DROP DATABASE test; -``` - -#### 选择数据库 - -```sql -USE test; -``` - -### 数据表(TABLE) - -#### 创建数据表 - -**普通创建** - -```sql -CREATE TABLE user ( - id int(10) unsigned NOT NULL COMMENT 'Id', - username varchar(64) NOT NULL DEFAULT 'default' COMMENT '用户名', - password varchar(64) NOT NULL DEFAULT 'default' COMMENT '密码', - email varchar(64) NOT NULL DEFAULT 'default' COMMENT '邮箱' -) COMMENT='用户表'; -``` - -**根据已有的表创建新表** - -```sql -CREATE TABLE vip_user AS -SELECT * FROM user; -``` - -#### 删除数据表 - -```sql -DROP TABLE user; -``` - -#### 修改数据表 - -**添加列** - -```sql -ALTER TABLE user -ADD age int(3); -``` - -**删除列** - -```sql -ALTER TABLE user -DROP COLUMN age; -``` - -**修改列** - -```sql -ALTER TABLE `user` -MODIFY COLUMN age tinyint; -``` - -**添加主键** - -```sql -ALTER TABLE user -ADD PRIMARY KEY (id); -``` - -**删除主键** - -```sql -ALTER TABLE user -DROP PRIMARY KEY; -``` - -### 视图(VIEW) - -定义: - -- 视图是基于 SQL 语句的结果集的可视化的表。 -- 视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。对视图的操作和对普通表的操作一样。 - -作用: - -- 简化复杂的 SQL 操作,比如复杂的联结; -- 只使用实际表的一部分数据; -- 通过只给用户访问视图的权限,保证数据的安全性; -- 更改数据格式和表示。 - -![mysql视图](https://oss.javaguide.cn/p3-juejin/ec4c975296ea4a7097879dac7c353878~tplv-k3u1fbpfcp-zoom-1.jpeg) - -#### 创建视图 - -```sql -CREATE VIEW top_10_user_view AS -SELECT id, username -FROM user -WHERE id < 10; -``` - -#### 删除视图 - -```sql -DROP VIEW top_10_user_view; -``` - -### 索引(INDEX) - -**索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。** - -索引的作用就相当于书的目录。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。 - -**优点**: - -- 使用索引可以大大加快 数据的检索速度(大大减少检索的数据量), 这也是创建索引的最主要的原因。 -- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 - -**缺点**: - -- 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。 -- 索引需要使用物理文件存储,也会耗费一定空间。 - -但是,**使用索引一定能提高查询性能吗?** - -大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。 - -关于索引的详细介绍,请看我写的 [MySQL 索引详解](https://javaguide.cn/database/mysql/mysql-index.html) 这篇文章。 - -#### 创建索引 - -```sql -CREATE INDEX user_index -ON user (id); -``` - -#### 添加索引 - -```sql -ALTER table user ADD INDEX user_index(id) -``` - -#### 创建唯一索引 - -```sql -CREATE UNIQUE INDEX user_index -ON user (id); -``` - -#### 删除索引 - -```sql -ALTER TABLE user -DROP INDEX user_index; -``` - -### 约束 - -SQL 约束用于规定表中的数据规则。 - -如果存在违反约束的数据行为,行为会被约束终止。 - -约束可以在创建表时规定(通过 CREATE TABLE 语句),或者在表创建之后规定(通过 ALTER TABLE 语句)。 - -约束类型: - -- `NOT NULL` - 指示某列不能存储 NULL 值。 -- `UNIQUE` - 保证某列的每行必须有唯一的值。 -- `PRIMARY KEY` - NOT NULL 和 UNIQUE 的结合。确保某列(或两个列多个列的结合)有唯一标识,有助于更容易更快速地找到表中的一个特定的记录。 -- `FOREIGN KEY` - 保证一个表中的数据匹配另一个表中的值的参照完整性。 -- `CHECK` - 保证列中的值符合指定的条件。 -- `DEFAULT` - 规定没有给列赋值时的默认值。 - -创建表时使用约束条件: - -```sql -CREATE TABLE Users ( - Id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增Id', - Username VARCHAR(64) NOT NULL UNIQUE DEFAULT 'default' COMMENT '用户名', - Password VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '密码', - Email VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '邮箱地址', - Enabled TINYINT(4) DEFAULT NULL COMMENT '是否有效', - PRIMARY KEY (Id) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -``` - -**接下来,我们来介绍 TCL 语句用法。TCL 的主要功能是管理数据库中的事务。** - -## 事务处理 - -不能回退 `SELECT` 语句,回退 `SELECT` 语句也没意义;也不能回退 `CREATE` 和 `DROP` 语句。 - -**MySQL 默认是隐式提交**,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 `START TRANSACTION` 语句时,会关闭隐式提交;当 `COMMIT` 或 `ROLLBACK` 语句执行后,事务会自动关闭,重新恢复隐式提交。 - -通过 `set autocommit=0` 可以取消自动提交,直到 `set autocommit=1` 才会提交;`autocommit` 标记是针对每个连接而不是针对服务器的。 - -指令: - -- `START TRANSACTION` - 指令用于标记事务的起始点。 -- `SAVEPOINT` - 指令用于创建保留点。 -- `ROLLBACK TO` - 指令用于回滚到指定的保留点;如果没有设置保留点,则回退到 `START TRANSACTION` 语句处。 -- `COMMIT` - 提交事务。 - -```sql --- 开始事务 -START TRANSACTION; - --- 插入操作 A -INSERT INTO `user` -VALUES (1, 'root1', 'root1', 'xxxx@163.com'); - --- 创建保留点 updateA -SAVEPOINT updateA; - --- 插入操作 B -INSERT INTO `user` -VALUES (2, 'root2', 'root2', 'xxxx@163.com'); - --- 回滚到保留点 updateA -ROLLBACK TO updateA; - --- 提交事务,只有操作 A 生效 -COMMIT; -``` - -**接下来,我们来介绍 DCL 语句用法。DCL 的主要功能是控制用户的访问权限。** - -## 权限控制 - -要授予用户帐户权限,可以用`GRANT`命令。要撤销用户的权限,可以用`REVOKE`命令。这里以 MySQL 为例,介绍权限控制实际应用。 - -`GRANT`授予权限语法: - -```sql -GRANT privilege,[privilege],.. ON privilege_level -TO user [IDENTIFIED BY password] -[REQUIRE tsl_option] -[WITH [GRANT_OPTION | resource_option]]; -``` - -简单解释一下: - -1. 在`GRANT`关键字后指定一个或多个权限。如果授予用户多个权限,则每个权限由逗号分隔。 -2. `ON privilege_level` 确定权限应用级别。MySQL 支持 global(`*.*`),database(`database.*`),table(`database.table`)和列级别。如果使用列权限级别,则必须在每个权限之后指定一个或逗号分隔列的列表。 -3. `user` 是要授予权限的用户。如果用户已存在,则`GRANT`语句将修改其权限。否则,`GRANT`语句将创建一个新用户。可选子句`IDENTIFIED BY`允许您为用户设置新的密码。 -4. `REQUIRE tsl_option`指定用户是否必须通过 SSL,X059 等安全连接连接到数据库服务器。 -5. 可选 `WITH GRANT OPTION` 子句允许您授予其他用户或从其他用户中删除您拥有的权限。此外,您可以使用`WITH`子句分配 MySQL 数据库服务器的资源,例如,设置用户每小时可以使用的连接数或语句数。这在 MySQL 共享托管等共享环境中非常有用。 - -`REVOKE` 撤销权限语法: - -```sql -REVOKE privilege_type [(column_list)] - [, priv_type [(column_list)]]... -ON [object_type] privilege_level -FROM user [, user]... -``` - -简单解释一下: - -1. 在 `REVOKE` 关键字后面指定要从用户撤消的权限列表。您需要用逗号分隔权限。 -2. 指定在 `ON` 子句中撤销特权的特权级别。 -3. 指定要撤消 `FROM` 子句中的权限的用户帐户。 - -`GRANT` 和 `REVOKE` 可在几个层次上控制访问权限: - -- 整个服务器,使用 `GRANT ALL` 和 `REVOKE ALL`; -- 整个数据库,使用 `ON database.*`; -- 特定的表,使用 `ON database.table`; -- 特定的列; -- 特定的存储过程。 - -新创建的账户没有任何权限。账户用 `username@host` 的形式定义,`username@%` 使用的是默认主机名。MySQL 的账户信息保存在 mysql 这个数据库中。 - -```sql -USE mysql; -SELECT user FROM user; -``` - -下表说明了可用于`GRANT`和`REVOKE`语句的所有允许权限: - -| **特权** | **说明** | **级别** | | | | | | -| ----------------------- | ------------------------------------------------------------------------------------------------------- | -------- | ------ | -------- | -------- | --- | --- | -| **全局** | 数据库 | **表** | **列** | **程序** | **代理** | | | -| ALL [PRIVILEGES] | 授予除 GRANT OPTION 之外的指定访问级别的所有权限 | | | | | | | -| ALTER | 允许用户使用 ALTER TABLE 语句 | X | X | X | | | | -| ALTER ROUTINE | 允许用户更改或删除存储的例程 | X | X | | | X | | -| CREATE | 允许用户创建数据库和表 | X | X | X | | | | -| CREATE ROUTINE | 允许用户创建存储的例程 | X | X | | | | | -| CREATE TABLESPACE | 允许用户创建,更改或删除表空间和日志文件组 | X | | | | | | -| CREATE TEMPORARY TABLES | 允许用户使用 CREATE TEMPORARY TABLE 创建临时表 | X | X | | | | | -| CREATE USER | 允许用户使用 CREATE USER,DROP USER,RENAME USER 和 REVOKE ALL PRIVILEGES 语句。 | X | | | | | | -| CREATE VIEW | 允许用户创建或修改视图。 | X | X | X | | | | -| DELETE | 允许用户使用 DELETE | X | X | X | | | | -| DROP | 允许用户删除数据库,表和视图 | X | X | X | | | | -| EVENT | 启用事件计划程序的事件使用。 | X | X | | | | | -| EXECUTE | 允许用户执行存储的例程 | X | X | X | | | | -| FILE | 允许用户读取数据库目录中的任何文件。 | X | | | | | | -| GRANT OPTION | 允许用户拥有授予或撤消其他帐户权限的权限。 | X | X | X | | X | X | -| INDEX | 允许用户创建或删除索引。 | X | X | X | | | | -| INSERT | 允许用户使用 INSERT 语句 | X | X | X | X | | | -| LOCK TABLES | 允许用户对具有 SELECT 权限的表使用 LOCK TABLES | X | X | | | | | -| PROCESS | 允许用户使用 SHOW PROCESSLIST 语句查看所有进程。 | X | | | | | | -| PROXY | 启用用户代理。 | | | | | | | -| REFERENCES | 允许用户创建外键 | X | X | X | X | | | -| RELOAD | 允许用户使用 FLUSH 操作 | X | | | | | | -| REPLICATION CLIENT | 允许用户查询以查看主服务器或从属服务器的位置 | X | | | | | | -| REPLICATION SLAVE | 允许用户使用复制从属从主服务器读取二进制日志事件。 | X | | | | | | -| SELECT | 允许用户使用 SELECT 语句 | X | X | X | X | | | -| SHOW DATABASES | 允许用户显示所有数据库 | X | | | | | | -| SHOW VIEW | 允许用户使用 SHOW CREATE VIEW 语句 | X | X | X | | | | -| SHUTDOWN | 允许用户使用 mysqladmin shutdown 命令 | X | | | | | | -| SUPER | 允许用户使用其他管理操作,例如 CHANGE MASTER TO,KILL,PURGE BINARY LOGS,SET GLOBAL 和 mysqladmin 命令 | X | | | | | | -| TRIGGER | 允许用户使用 TRIGGER 操作。 | X | X | X | | | | -| UPDATE | 允许用户使用 UPDATE 语句 | X | X | X | X | | | -| USAGE | 相当于“没有特权” | | | | | | | - -### 创建账户 - -```sql -CREATE USER myuser IDENTIFIED BY 'mypassword'; -``` - -### 修改账户名 - -```sql -UPDATE user SET user='newuser' WHERE user='myuser'; -FLUSH PRIVILEGES; -``` - -### 删除账户 - -```sql -DROP USER myuser; -``` - -### 查看权限 - -```sql -SHOW GRANTS FOR myuser; -``` - -### 授予权限 - -```sql -GRANT SELECT, INSERT ON *.* TO myuser; -``` - -### 删除权限 - -```sql -REVOKE SELECT, INSERT ON *.* FROM myuser; -``` - -### 更改密码 - -```sql -SET PASSWORD FOR myuser = 'mypass'; -``` - -## 存储过程 - -存储过程可以看成是对一系列 SQL 操作的批处理。存储过程可以由触发器,其他存储过程以及 Java, Python,PHP 等应用程序调用。 - -![mysql存储过程](https://oss.javaguide.cn/p3-juejin/60afdc9c9a594f079727ec64a2e698a3~tplv-k3u1fbpfcp-zoom-1.jpeg) - -使用存储过程的好处: - -- 代码封装,保证了一定的安全性; -- 代码复用; -- 由于是预先编译,因此具有很高的性能。 - -创建存储过程: - -- 命令行中创建存储过程需要自定义分隔符,因为命令行是以 `;` 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。 -- 包含 `in`、`out` 和 `inout` 三种参数。 -- 给变量赋值都需要用 `select into` 语句。 -- 每次只能给一个变量赋值,不支持集合的操作。 - -需要注意的是:**阿里巴巴《Java 开发手册》强制禁止使用存储过程。因为存储过程难以调试和扩展,更没有移植性。** - -![](https://oss.javaguide.cn/p3-juejin/93a5e011ade4450ebfa5d82057532a49~tplv-k3u1fbpfcp-zoom-1.png) - -至于到底要不要在项目中使用,还是要看项目实际需求,权衡好利弊即可! - -### 创建存储过程 - -```sql -DROP PROCEDURE IF EXISTS `proc_adder`; -DELIMITER ;; -CREATE DEFINER=`root`@`localhost` PROCEDURE `proc_adder`(IN a int, IN b int, OUT sum int) -BEGIN - DECLARE c int; - if a is null then set a = 0; - end if; - - if b is null then set b = 0; - end if; - - set sum = a + b; -END -;; -DELIMITER ; -``` - -### 使用存储过程 - -```less -set @b=5; -call proc_adder(2,@b,@s); -select @s as sum; -``` - -## 游标 - -游标(cursor)是一个存储在 DBMS 服务器上的数据库查询,它不是一条 `SELECT` 语句,而是被该语句检索出来的结果集。 - -在存储过程中使用游标可以对一个结果集进行移动遍历。 - -游标主要用于交互式应用,其中用户需要滚动屏幕上的数据,并对数据进行浏览或做出更改。 - -使用游标的几个明确步骤: - -- 在使用游标前,必须声明(定义)它。这个过程实际上没有检索数据, 它只是定义要使用的 `SELECT` 语句和游标选项。 - -- 一旦声明,就必须打开游标以供使用。这个过程用前面定义的 SELECT 语句把数据实际检索出来。 - -- 对于填有数据的游标,根据需要取出(检索)各行。 - -- 在结束游标使用时,必须关闭游标,可能的话,释放游标(有赖于具 - - 体的 DBMS)。 - -```sql -DELIMITER $ -CREATE PROCEDURE getTotal() -BEGIN - DECLARE total INT; - -- 创建接收游标数据的变量 - DECLARE sid INT; - DECLARE sname VARCHAR(10); - -- 创建总数变量 - DECLARE sage INT; - -- 创建结束标志变量 - DECLARE done INT DEFAULT false; - -- 创建游标 - DECLARE cur CURSOR FOR SELECT id,name,age from cursor_table where age>30; - -- 指定游标循环结束时的返回值 - DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true; - SET total = 0; - OPEN cur; - FETCH cur INTO sid, sname, sage; - WHILE(NOT done) - DO - SET total = total + 1; - FETCH cur INTO sid, sname, sage; - END WHILE; - - CLOSE cur; - SELECT total; -END $ -DELIMITER ; - --- 调用存储过程 -call getTotal(); -``` - -## 触发器 - -触发器是一种与表操作有关的数据库对象,当触发器所在表上出现指定事件时,将调用该对象,即表的操作事件触发表上的触发器的执行。 - -我们可以使用触发器来进行审计跟踪,把修改记录到另外一张表中。 - -使用触发器的优点: - -- SQL 触发器提供了另一种检查数据完整性的方法。 -- SQL 触发器可以捕获数据库层中业务逻辑中的错误。 -- SQL 触发器提供了另一种运行计划任务的方法。通过使用 SQL 触发器,您不必等待运行计划任务,因为在对表中的数据进行更改之前或之后会自动调用触发器。 -- SQL 触发器对于审计表中数据的更改非常有用。 - -使用触发器的缺点: - -- SQL 触发器只能提供扩展验证,并且不能替换所有验证。必须在应用程序层中完成一些简单的验证。例如,您可以使用 JavaScript 在客户端验证用户的输入,或者使用服务器端脚本语言(如 JSP,PHP,ASP.NET,Perl)在服务器端验证用户的输入。 -- 从客户端应用程序调用和执行 SQL 触发器是不可见的,因此很难弄清楚数据库层中发生了什么。 -- SQL 触发器可能会增加数据库服务器的开销。 - -MySQL 不允许在触发器中使用 CALL 语句 ,也就是不能调用存储过程。 - -> 注意:在 MySQL 中,分号 `;` 是语句结束的标识符,遇到分号表示该段语句已经结束,MySQL 可以开始执行了。因此,解释器遇到触发器执行动作中的分号后就开始执行,然后会报错,因为没有找到和 BEGIN 匹配的 END。 -> -> 这时就会用到 `DELIMITER` 命令(DELIMITER 是定界符,分隔符的意思)。它是一条命令,不需要语句结束标识,语法为:`DELIMITER new_delimiter`。`new_delimiter` 可以设为 1 个或多个长度的符号,默认的是分号 `;`,我们可以把它修改为其他符号,如 `$` - `DELIMITER $` 。在这之后的语句,以分号结束,解释器不会有什么反应,只有遇到了 `$`,才认为是语句结束。注意,使用完之后,我们还应该记得把它给修改回来。 - -在 MySQL 5.7.2 版之前,可以为每个表定义最多六个触发器。 - -- `BEFORE INSERT` - 在将数据插入表格之前激活。 -- `AFTER INSERT` - 将数据插入表格后激活。 -- `BEFORE UPDATE` - 在更新表中的数据之前激活。 -- `AFTER UPDATE` - 更新表中的数据后激活。 -- `BEFORE DELETE` - 在从表中删除数据之前激活。 -- `AFTER DELETE` - 从表中删除数据后激活。 - -但是,从 MySQL 版本 5.7.2+开始,可以为同一触发事件和操作时间定义多个触发器。 - -**`NEW` 和 `OLD`**: - -- MySQL 中定义了 `NEW` 和 `OLD` 关键字,用来表示触发器的所在表中,触发了触发器的那一行数据。 -- 在 `INSERT` 型触发器中,`NEW` 用来表示将要(`BEFORE`)或已经(`AFTER`)插入的新数据; -- 在 `UPDATE` 型触发器中,`OLD` 用来表示将要或已经被修改的原数据,`NEW` 用来表示将要或已经修改为的新数据; -- 在 `DELETE` 型触发器中,`OLD` 用来表示将要或已经被删除的原数据; -- 使用方法:`NEW.columnName` (columnName 为相应数据表某一列名) - -### 创建触发器 - -> 提示:为了理解触发器的要点,有必要先了解一下创建触发器的指令。 - -`CREATE TRIGGER` 指令用于创建触发器。 - -语法: - -```sql -CREATE TRIGGER trigger_name -trigger_time -trigger_event -ON table_name -FOR EACH ROW -BEGIN - trigger_statements -END; -``` - -说明: - -- `trigger_name`:触发器名 -- `trigger_time` : 触发器的触发时机。取值为 `BEFORE` 或 `AFTER`。 -- `trigger_event` : 触发器的监听事件。取值为 `INSERT`、`UPDATE` 或 `DELETE`。 -- `table_name` : 触发器的监听目标。指定在哪张表上建立触发器。 -- `FOR EACH ROW`: 行级监视,Mysql 固定写法,其他 DBMS 不同。 -- `trigger_statements`: 触发器执行动作。是一条或多条 SQL 语句的列表,列表内的每条语句都必须用分号 `;` 来结尾。 - -当触发器的触发条件满足时,将会执行 `BEGIN` 和 `END` 之间的触发器执行动作。 - -示例: - -```sql -DELIMITER $ -CREATE TRIGGER `trigger_insert_user` -AFTER INSERT ON `user` -FOR EACH ROW -BEGIN - INSERT INTO `user_history`(user_id, operate_type, operate_time) - VALUES (NEW.id, 'add a user', now()); -END $ -DELIMITER ; -``` - -### 查看触发器 - -```sql -SHOW TRIGGERS; -``` - -### 删除触发器 - -```sql -DROP TRIGGER IF EXISTS trigger_insert_user; -``` - -## 文章推荐 - -- [后端程序员必备:SQL 高性能优化指南!35+条优化建议立马 GET!](https://mp.weixin.qq.com/s/I-ZT3zGTNBZ6egS7T09jyQ) -- [后端程序员必备:书写高质量 SQL 的 30 条建议](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486461&idx=1&sn=60a22279196d084cc398936fe3b37772&chksm=cea24436f9d5cd20a4fa0e907590f3e700d7378b3f608d7b33bb52cfb96f503b7ccb65a1deed&token=1987003517&lang=zh_CN#rd) - - +The supported permission controls may vary depending on different DBMS and security entities. diff --git a/docs/distributed-system/api-gateway.md b/docs/distributed-system/api-gateway.md index e9b9f27e6dd..4ab33311d2c 100644 --- a/docs/distributed-system/api-gateway.md +++ b/docs/distributed-system/api-gateway.md @@ -1,63 +1,63 @@ --- -title: API网关基础知识总结 -category: 分布式 +title: API Gateway Basic Knowledge Summary +category: Distributed --- -## 什么是网关? +## What is a Gateway? -微服务背景下,一个系统被拆分为多个服务,但是像安全认证,流量控制,日志,监控等功能是每个服务都需要的,没有网关的话,我们就需要在每个服务中单独实现,这使得我们做了很多重复的事情并且没有一个全局的视图来统一管理这些功能。 +In the context of microservices, a system is split into multiple services, but features such as security authentication, traffic control, logging, and monitoring are required by each service. Without a gateway, we would need to implement these features individually in each service, leading to a lot of duplication and lacking a global view to manage these functions uniformly. -![网关示意图](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway-overview.png) +![Gateway Schematic](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway-overview.png) -一般情况下,网关可以为我们提供请求转发、安全认证(身份/权限认证)、流量控制、负载均衡、降级熔断、日志、监控、参数校验、协议转换等功能。 +Generally, a gateway can provide us with features such as request forwarding, security authentication (identity/authorization authentication), traffic control, load balancing, circuit breaking, logging, monitoring, parameter validation, and protocol conversion. -上面介绍了这么多功能,实际上,网关主要做了两件事情:**请求转发** + **请求过滤**。 +Despite all these features, the gateway mainly does two things: **request forwarding** + **request filtering**. -由于引入网关之后,会多一步网络转发,因此性能会有一点影响(几乎可以忽略不计,尤其是内网访问的情况下)。 另外,我们需要保障网关服务的高可用,避免单点风险。 +Since introducing a gateway adds a step of network forwarding, performance may be slightly affected (virtually negligible, especially in the case of internal network access). Additionally, we need to ensure high availability of the gateway service to avoid single points of failure. -如下图所示,网关服务外层通过 Nginx(其他负载均衡设备/软件也行) 进⾏负载转发以达到⾼可⽤。Nginx 在部署的时候,尽量也要考虑高可用,避免单点风险。 +As shown in the figure below, the outer layer of the gateway service uses Nginx (other load balancing devices/software can also be used) for load forwarding to achieve high availability. When deploying Nginx, considerations for high availability should be made to avoid single points of risk. -![基于 Nginx 的服务端负载均衡](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/server-load-balancing.png) +![Nginx-based Server Load Balancing](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/server-load-balancing.png) -## 网关能提供哪些功能? +## What Functions Can a Gateway Provide? -绝大部分网关可以提供下面这些功能(有一些功能需要借助其他框架或者中间件): +Most gateways can provide the following functions (some functions may require the assistance of other frameworks or middleware): -- **请求转发**:将请求转发到目标微服务。 -- **负载均衡**:根据各个微服务实例的负载情况或者具体的负载均衡策略配置对请求实现动态的负载均衡。 -- **安全认证**:对用户请求进行身份验证并仅允许可信客户端访问 API,并且还能够使用类似 RBAC 等方式来授权。 -- **参数校验**:支持参数映射与校验逻辑。 -- **日志记录**:记录所有请求的行为日志供后续使用。 -- **监控告警**:从业务指标、机器指标、JVM 指标等方面进行监控并提供配套的告警机制。 -- **流量控制**:对请求的流量进行控制,也就是限制某一时刻内的请求数。 -- **熔断降级**:实时监控请求的统计信息,达到配置的失败阈值后,自动熔断,返回默认值。 -- **响应缓存**:当用户请求获取的是一些静态的或更新不频繁的数据时,一段时间内多次请求获取到的数据很可能是一样的。对于这种情况可以将响应缓存起来。这样用户请求可以直接在网关层得到响应数据,无需再去访问业务服务,减轻业务服务的负担。 -- **响应聚合**:某些情况下用户请求要获取的响应内容可能会来自于多个业务服务。网关作为业务服务的调用方,可以把多个服务的响应整合起来,再一并返回给用户。 -- **灰度发布**:将请求动态分流到不同的服务版本(最基本的一种灰度发布)。 -- **异常处理**:对于业务服务返回的异常响应,可以在网关层在返回给用户之前做转换处理。这样可以把一些业务侧返回的异常细节隐藏,转换成用户友好的错误提示返回。 -- **API 文档:** 如果计划将 API 暴露给组织以外的开发人员,那么必须考虑使用 API 文档,例如 Swagger 或 OpenAPI。 -- **协议转换**:通过协议转换整合后台基于 REST、AMQP、Dubbo 等不同风格和实现技术的微服务,面向 Web Mobile、开放平台等特定客户端提供统一服务。 -- **证书管理**:将 SSL 证书部署到 API 网关,由一个统一的入口管理接口,降低了证书更换时的复杂度。 +- **Request Forwarding**: Forward requests to the target microservice. +- **Load Balancing**: Implement dynamic load balancing based on the load of each microservice instance or specific load balancing strategy configurations. +- **Security Authentication**: Verify user requests and only allow trusted clients to access the API, and authorization can be done using methods like RBAC. +- **Parameter Validation**: Support parameter mapping and validation logic. +- **Logging**: Record the behavior logs of all requests for future use. +- **Monitoring and Alerts**: Monitor from operational metrics, machine metrics, JVM metrics, etc., and provide a corresponding alert mechanism. +- **Traffic Control**: Control the traffic of requests, limiting the number of requests at a certain time. +- **Circuit Breaking**: Monitor request statistics in real-time, and when the configured failure threshold is reached, automatically break the circuit and return default values. +- **Response Caching**: When user requests are for some static or infrequently updated data, multiple requests for data within a certain time period may yield the same result. In such cases, responses can be cached. This way, user requests can directly receive response data at the gateway layer without going through the business service, reducing the load on the business service. +- **Response Aggregation**: In some cases, the response content that user requests may come from multiple business services. The gateway, as the caller of the business services, can aggregate the responses from multiple services and return them to the user together. +- **Gray Release**: Dynamically divert requests to different service versions (a basic form of gray release). +- **Exception Handling**: For exception responses returned by business services, the gateway layer can transform them before returning them to the user. This can obscure some details of the exceptions returned from the business side, converting them into user-friendly error messages. +- **API Documentation**: If planning to expose APIs to developers outside the organization, it's necessary to consider using API documentation tools like Swagger or OpenAPI. +- **Protocol Conversion**: Through protocol conversion, integrate backend microservices based on REST, AMQP, Dubbo, etc., providing unified services for specific clients like Web Mobile and open platforms. +- **Certificate Management**: Deploy SSL certificates to the API gateway, managing interfaces through a unified entry point, reducing the complexity during certificate replacement. -下图来源于[百亿规模 API 网关服务 Shepherd 的设计与实现 - 美团技术团队 - 2021](https://mp.weixin.qq.com/s/iITqdIiHi3XGKq6u6FRVdg)这篇文章。 +The following diagram is sourced from [Design and Implementation of the Billion-scale API Gateway Service Shepherd - Meituan Technical Team - 2021](https://mp.weixin.qq.com/s/iITqdIiHi3XGKq6u6FRVdg). ![](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/up-35e102c633bbe8e0dea1e075ea3fee5dcfb.png) -## 有哪些常见的网关系统? +## What Common Gateway Systems Are There? ### Netflix Zuul -Zuul 是 Netflix 开发的一款提供动态路由、监控、弹性、安全的网关服务,基于 Java 技术栈开发,可以和 Eureka、Ribbon、Hystrix 等组件配合使用。 +Zuul is a gateway service developed by Netflix that provides dynamic routing, monitoring, elasticity, and security, developed on the Java technology stack and can be used with components like Eureka, Ribbon, and Hystrix. -Zuul 核心架构如下: +The core architecture of Zuul is as follows: -![Zuul 核心架构](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/zuul-core-architecture.webp) +![Zuul Core Architecture](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/zuul-core-architecture.webp) -Zuul 主要通过过滤器(类似于 AOP)来过滤请求,从而实现网关必备的各种功能。 +Zuul mainly uses filters (similar to AOP) to filter requests, thereby implementing various necessary functions of a gateway. -![Zuul 请求声明周期](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/zuul-request-lifecycle.webp) +![Zuul Request Lifecycle](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/zuul-request-lifecycle.webp) -我们可以自定义过滤器来处理请求,并且,Zuul 生态本身就有很多现成的过滤器供我们使用。就比如限流可以直接用国外朋友写的 [spring-cloud-zuul-ratelimit](https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit) (这里只是举例说明,一般是配合 hystrix 来做限流): +We can customize filters to handle requests, and the Zuul ecosystem already has many ready-made filters for us to use. For instance, rate limiting can directly use a repository written by overseas friends called [spring-cloud-zuul-ratelimit](https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit) (this is just an example; generally, it is used in conjunction with Hystrix for rate limiting): ```xml @@ -71,60 +71,60 @@ Zuul 主要通过过滤器(类似于 AOP)来过滤请求,从而实现网 ``` -[Zuul 1.x](https://netflixtechblog.com/announcing-zuul-edge-service-in-the-cloud-ab3af5be08ee) 基于同步 IO,性能较差。[Zuul 2.x](https://netflixtechblog.com/open-sourcing-zuul-2-82ea476cb2b3) 基于 Netty 实现了异步 IO,性能得到了大幅改进。 +[Zuul 1.x](https://netflixtechblog.com/announcing-zuul-edge-service-in-the-cloud-ab3af5be08ee) is based on synchronous I/O, which performs poorly. [Zuul 2.x](https://netflixtechblog.com/open-sourcing-zuul-2-82ea476cb2b3) implements asynchronous I/O based on Netty, significantly improving performance. -![Zuul2 架构](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/zuul2-core-architecture.png) +![Zuul2 Architecture](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/zuul2-core-architecture.png) -- GitHub 地址: -- 官方 Wiki: +- GitHub Address: +- Official Wiki: ### Spring Cloud Gateway -SpringCloud Gateway 属于 Spring Cloud 生态系统中的网关,其诞生的目标是为了替代老牌网关 **Zuul**。准确点来说,应该是 Zuul 1.x。SpringCloud Gateway 起步要比 Zuul 2.x 更早。 +Spring Cloud Gateway is a gateway in the Spring Cloud ecosystem born to replace the legacy gateway **Zuul**. More precisely, it should be Zuul 1.x. Spring Cloud Gateway started earlier than Zuul 2.x. -为了提升网关的性能,SpringCloud Gateway 基于 Spring WebFlux 。Spring WebFlux 使用 Reactor 库来实现响应式编程模型,底层基于 Netty 实现同步非阻塞的 I/O。 +To enhance gateway performance, Spring Cloud Gateway is built on Spring WebFlux. Spring WebFlux uses the Reactor library to implement a reactive programming model, based on Netty for synchronous, non-blocking I/O. ![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/springcloud-gateway-%20demo.png) -Spring Cloud Gateway 不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,限流。 +Spring Cloud Gateway provides not only a unified routing method but also offers basic gateway functionalities based on a filter chain, such as security, monitoring/metrics, and rate limiting. -Spring Cloud Gateway 和 Zuul 2.x 的差别不大,也是通过过滤器来处理请求。不过,目前更加推荐使用 Spring Cloud Gateway 而非 Zuul,Spring Cloud 生态对其支持更加友好。 +There aren't many differences between Spring Cloud Gateway and Zuul 2.x, both use filters to process requests. However, Spring Cloud Gateway is currently more recommended over Zuul, as the Spring Cloud ecosystem supports it more favorably. -- Github 地址: -- 官网: +- GitHub Address: +- Official Site: ### OpenResty -根据官方介绍: +According to the official introduction: -> OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。 +> OpenResty is a high-performance web platform based on Nginx and Lua, which integrates numerous high-quality Lua libraries, third-party modules, and most dependencies. It is used to conveniently build dynamic web applications, web services, and dynamic gateways that can handle ultra-high concurrency with high scalability. -![OpenResty 和 Nginx 以及 Lua 的关系](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gatewaynginx-lua-openresty.png) +![Relationship between OpenResty, Nginx, and Lua](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gatewaynginx-lua-openresty.png) -OpenResty 基于 Nginx,主要还是看中了其优秀的高并发能力。不过,由于 Nginx 采用 C 语言开发,二次开发门槛较高。如果想在 Nginx 上实现一些自定义的逻辑或功能,就需要编写 C 语言的模块,并重新编译 Nginx。 +OpenResty is based on Nginx, primarily due to its excellent high concurrency capability. However, because Nginx is developed in C language, the barrier to secondary development is relatively high. If you want to implement some custom logic or functionality on Nginx, it requires writing C language modules and recompiling Nginx. -为了解决这个问题,OpenResty 通过实现 `ngx_lua` 和 `stream_lua` 等 Nginx 模块,把 Lua/LuaJIT 完美地整合进了 Nginx,从而让我们能够在 Nginx 内部里嵌入 Lua 脚本,使得可以通过简单的 Lua 语言来扩展网关的功能,比如实现自定义的路由规则、过滤器、缓存策略等。 +To solve this problem, OpenResty integrates Lua/LuaJIT perfectly into Nginx by implementing `ngx_lua` and `stream_lua` modules, allowing us to embed Lua scripts inside Nginx. This makes it possible to extend the gateway's functionality using simple Lua language, such as implementing custom routing rules, filters, caching strategies, etc. -> Lua 是一种非常快速的动态脚本语言,它的运行速度接近于 C 语言。LuaJIT 是 Lua 的一个即时编译器,它可以显著提高 Lua 代码的执行效率。LuaJIT 将一些常用的 Lua 函数和工具库预编译并缓存,这样在下次调用时就可以直接使用缓存的字节码,从而大大加快了执行速度。 +> Lua is a very fast dynamic scripting language, with running speeds close to those of C language. LuaJIT is a just-in-time compiler for Lua that can significantly improve the execution efficiency of Lua code. LuaJIT precompiles and caches commonly used Lua functions and tool libraries, so they can be directly reused in the next call, greatly speeding up execution. -关于 OpenResty 的入门以及网关安全实战推荐阅读这篇文章:[每个后端都应该了解的 OpenResty 入门以及网关安全实战](https://mp.weixin.qq.com/s/3HglZs06W95vF3tSa3KrXw)。 +For an introduction to OpenResty and practical gateway security, it is recommended to read this article: [OpenResty Introduction and Gateway Security Practices Every Backend Should Know](https://mp.weixin.qq.com/s/3HglZs06W95vF3tSa3KrXw). -- Github 地址: -- 官网地址: +- GitHub Address: +- Official Site: ### Kong -Kong 是一款基于 [OpenResty](https://github.com/openresty/) (Nginx + Lua)的高性能、云原生、可扩展、生态丰富的网关系统,主要由 3 个组件组成: +Kong is a high-performance, cloud-native, scalable, and rich ecosystem gateway system based on [OpenResty](https://github.com/openresty/) (Nginx + Lua), mainly consisting of three components: -- Kong Server:基于 Nginx 的服务器,用来接收 API 请求。 -- Apache Cassandra/PostgreSQL:用来存储操作数据。 -- Kong Dashboard:官方推荐 UI 管理工具,当然,也可以使用 RESTful 方式 管理 Admin api。 +- Kong Server: A server based on Nginx, used to receive API requests. +- Apache Cassandra/PostgreSQL: Used to store operational data. +- Kong Dashboard: Officially recommended UI management tool. Alternatively, RESTful methods can be used to manage the Admin API. ![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/kong-way.webp) -由于默认使用 Apache Cassandra/PostgreSQL 存储数据,Kong 的整个架构比较臃肿,并且会带来高可用的问题。 +Due to the default use of Apache Cassandra/PostgreSQL for data storage, Kong's entire architecture can be somewhat bulky and may introduce high availability issues. -Kong 提供了插件机制来扩展其功能,插件在 API 请求响应循环的生命周期中被执行。比如在服务上启用 Zipkin 插件: +Kong offers a plugin mechanism to extend its functionality, executed during the API request-response lifecycle. For example, to enable the Zipkin plugin on a service: ```shell $ curl -X POST http://kong:8001/services/{service}/plugins \ @@ -133,79 +133,79 @@ $ curl -X POST http://kong:8001/services/{service}/plugins \ --data "config.sample_ratio=0.001" ``` -Kong 本身就是一个 Lua 应用程序,并且是在 Openresty 的基础之上做了一层封装的应用。归根结底就是利用 Lua 嵌入 Nginx 的方式,赋予了 Nginx 可编程的能力,这样以插件的形式在 Nginx 这一层能够做到无限想象的事情。例如限流、安全访问策略、路由、负载均衡等等。编写一个 Kong 插件,就是按照 Kong 插件编写规范,写一个自己自定义的 Lua 脚本,然后加载到 Kong 中,最后引用即可。 +Kong itself is a Lua application, but it is also an encapsulated application built on top of OpenResty. Ultimately, it empowers Nginx with programmable capabilities using the embedded Lua approach, enabling endless possibilities through plugins for things like rate limiting, security access policies, routing, load balancing, and so on. Writing a Kong plugin involves creating a custom Lua script following the Kong plugin development standards and loading it into Kong. ![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/kong-gateway-overview.png) -除了 Lua,Kong 还可以基于 Go 、JavaScript、Python 等语言开发插件,得益于对应的 PDK(插件开发工具包)。 +In addition to Lua, Kong can also develop plugins based on Go, JavaScript, Python, and other languages, thanks to the corresponding PDK (Plugin Development Kit). -关于 Kong 插件的详细介绍,推荐阅读官方文档:,写的比较详细。 +For a detailed introduction to Kong plugins, it is recommended to read the official documentation: , which is quite comprehensive. -- Github 地址: -- 官网地址: +- GitHub Address: +- Official Site: ### APISIX -APISIX 是一款基于 OpenResty 和 etcd 的高性能、云原生、可扩展的网关系统。 +APISIX is a high-performance, cloud-native, scalable gateway system based on OpenResty and etcd. -> etcd 是使用 Go 语言开发的一个开源的、高可用的分布式 key-value 存储系统,使用 Raft 协议做分布式共识。 +> etcd is an open-source, high-availability distributed key-value store system developed using Go, utilizing the Raft protocol for distributed consensus. -与传统 API 网关相比,APISIX 具有动态路由和插件热加载,特别适合微服务系统下的 API 管理。并且,APISIX 与 SkyWalking(分布式链路追踪系统)、Zipkin(分布式链路追踪系统)、Prometheus(监控系统) 等 DevOps 生态工具对接都十分方便。 +Compared to traditional API gateways, APISIX has dynamic routing and hot plugin loading, making it especially suitable for API management in microservice systems. Additionally, APISIX integrates easily with various DevOps tools such as SkyWalking (distributed tracing system), Zipkin (distributed tracing system), and Prometheus (monitoring system). -![APISIX 架构图](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/apisix-architecture.png) +![APISIX Architecture](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/apisix-architecture.png) -作为 Nginx 和 Kong 的替代项目,APISIX 目前已经是 Apache 顶级开源项目,并且是最快毕业的国产开源项目。国内目前已经有很多知名企业(比如金山、有赞、爱奇艺、腾讯、贝壳)使用 APISIX 处理核心的业务流量。 +As an alternative project to Nginx and Kong, APISIX is currently an Apache top-level open-source project and is the fastest domestic open-source project to graduate. Many well-known domestic enterprises (such as Kingsoft, Youzan, iQIYI, Tencent, and Beike) are using APISIX to handle core business traffic. -根据官网介绍:“APISIX 已经生产可用,功能、性能、架构全面优于 Kong”。 +According to the official site: "APISIX is already production-ready and has comprehensive features, performance, and architecture surpassing Kong." -APISIX 同样支持定制化的插件开发。开发者除了能够使用 Lua 语言开发插件,还能通过下面两种方式开发来避开 Lua 语言的学习成本: +APISIX also supports customizable plugin development. Developers can create plugins using Lua, and they can also do so in the following two ways to avoid the learning cost of Lua: -- 通过 Plugin Runner 来支持更多的主流编程语言(比如 Java、Python、Go 等等)。通过这样的方式,可以让后端工程师通过本地 RPC 通信,使用熟悉的编程语言开发 APISIX 的插件。这样做的好处是减少了开发成本,提高了开发效率,但是在性能上会有一些损失。 -- 使用 Wasm(WebAssembly) 开发插件。Wasm 被嵌入到了 APISIX 中,用户可以使用 Wasm 去编译成 Wasm 的字节码在 APISIX 中运行。 +- Use Plugin Runner to support more mainstream programming languages (such as Java, Python, Go, etc.). This allows backend engineers to develop APISIX plugins using familiar programming languages via local RPC communication. This approach reduces development costs and improves efficiency, although there will be some performance losses. +- Use Wasm (WebAssembly) to develop plugins. Wasm is embedded in APISIX, allowing users to compile their code into Wasm bytecode to run in APISIX. -> Wasm 是基于堆栈的虚拟机的二进制指令格式,一种低级汇编语言,旨在非常接近已编译的机器代码,并且非常接近本机性能。Wasm 最初是为浏览器构建的,但是随着技术的成熟,在服务器端看到了越来越多的用例。 +> Wasm is a binary instruction format for a stack-based virtual machine, a low-level assembly language designed to be very close to compiled machine code and very close to native performance. Originally built for browsers, as the technology matured, more use cases for server-side applications have emerged. ![](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/up-a240d3b113cde647f5850f4c7cc55d4ff5c.png) -- Github 地址: -- 官网地址: +- GitHub Address: +- Official Site: -相关阅读: +Related articles: -- [为什么说 Apache APISIX 是最好的 API 网关?](https://mp.weixin.qq.com/s/j8ggPGEHFu3x5ekJZyeZnA) -- [有了 NGINX 和 Kong,为什么还需要 Apache APISIX](https://www.apiseven.com/zh/blog/why-we-need-Apache-APISIX) -- [APISIX 技术博客](https://www.apiseven.com/zh/blog) -- [APISIX 用户案例](https://www.apiseven.com/zh/usercases)(推荐) +- [Why Apache APISIX is the Best API Gateway?](https://mp.weixin.qq.com/s/j8ggPGEHFu3x5ekJZyeZnA) +- [With NGINX and Kong, Why Do We Still Need Apache APISIX](https://www.apiseven.com/zh/blog/why-we-need-Apache-APISIX) +- [APISIX Technical Blog](https://www.apiseven.com/zh/blog) +- [APISIX User Cases](https://www.apiseven.com/zh/usercases) (Recommended) ### Shenyu -Shenyu 是一款基于 WebFlux 的可扩展、高性能、响应式网关,Apache 顶级开源项目。 +Shenyu is an extensible, high-performance, and reactive gateway based on WebFlux, and is an Apache top-level open-source project. -![Shenyu 架构](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/shenyu-architecture.png) +![Shenyu Architecture](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/shenyu-architecture.png) -Shenyu 通过插件扩展功能,插件是 ShenYu 的灵魂,并且插件也是可扩展和热插拔的。不同的插件实现不同的功能。Shenyu 自带了诸如限流、熔断、转发、重写、重定向、和路由监控等插件。 +Shenyu extends its functionality through plugins, which are the soul of Shenyu, and the plugins are also extensible and hot-swappable. Different plugins implement different features. Shenyu comes with built-in plugins for rate limiting, circuit breaking, forwarding, rewriting, redirection, and routing monitoring. -- Github 地址: -- 官网地址: +- GitHub Address: +- Official Site: -## 如何选择? +## How to Choose? -上面介绍的几个常见的网关系统,最常用的是 Spring Cloud Gateway、Kong、APISIX 这三个。 +Among the common gateway systems introduced above, the three most commonly used are Spring Cloud Gateway, Kong, and APISIX. -对于公司业务以 Java 为主要开发语言的情况下,Spring Cloud Gateway 通常是个不错的选择,其优点有:简单易用、成熟稳定、与 Spring Cloud 生态系统兼容、Spring 社区成熟等等。不过,Spring Cloud Gateway 也有一些局限性和不足之处, 一般还需要结合其他网关一起使用比如 OpenResty。并且,其性能相比较于 Kong 和 APISIX,还是差一些。如果对性能要求比较高的话,Spring Cloud Gateway 不是一个好的选择。 +For companies where Java is the primary development language, Spring Cloud Gateway is typically a good choice, with advantages such as: simplicity, maturity and stability, compatibility with the Spring Cloud ecosystem, and a mature Spring community, etc. However, Spring Cloud Gateway also has some limitations and shortcomings, generally requiring it to be used in conjunction with other gateways like OpenResty. Moreover, its performance is still somewhat inferior compared to Kong and APISIX. If performance is a significant concern, Spring Cloud Gateway is not a good choice. -Kong 和 APISIX 功能更丰富,性能更强大,技术架构更贴合云原生。Kong 是开源 API 网关的鼻祖,生态丰富,用户群体庞大。APISIX 属于后来者,更优秀一些,根据 APISIX 官网介绍:“APISIX 已经生产可用,功能、性能、架构全面优于 Kong”。下面简单对比一下二者: +Kong and APISIX offer more robust functionality, with stronger performance and a technology architecture more aligned with cloud-native principles. Kong is the pioneer of open-source API gateways, with a rich ecosystem and a large user base. APISIX, as a later entrant, is even better; according to its official site: "APISIX is already production-ready, with functionality, performance, and architecture comprehensively superior to Kong." Below is a brief comparison between the two: -- APISIX 基于 etcd 来做配置中心,不存在单点问题,云原生友好;而 Kong 基于 Apache Cassandra/PostgreSQL ,存在单点风险,需要额外的基础设施保障做高可用。 -- APISIX 支持热更新,并且实现了毫秒级别的热更新响应;而 Kong 不支持热更新。 -- APISIX 的性能要优于 Kong 。 -- APISIX 支持的插件更多,功能更丰富。 +- APISIX uses etcd as a configuration center, avoiding single point issues and is cloud-native friendly; whereas Kong relies on Apache Cassandra/PostgreSQL, which has single point risks and requires additional infrastructure for high availability. +- APISIX supports hot updates and achieves milliseconds-level hot update response; while Kong does not support hot updates. +- APISIX exhibits superior performance compared to Kong. +- APISIX supports a greater number of plugins, with richer functionalities. -## 参考 +## References -- Kong 插件开发教程[通俗易懂]: -- API 网关 Kong 实战: -- Spring Cloud Gateway 原理介绍和应用: -- 微服务为什么要用到 API 网关?: +- Kong Plugin Development Tutorial \[Easy to Understand\]: +- API Gateway Kong Practical Application: +- Introduction to Spring Cloud Gateway Principles and Applications: +- Why Do Microservices Need an API Gateway?: diff --git a/docs/distributed-system/distributed-configuration-center.md b/docs/distributed-system/distributed-configuration-center.md index 2e00aec70a3..1759140bcd6 100644 --- a/docs/distributed-system/distributed-configuration-center.md +++ b/docs/distributed-system/distributed-configuration-center.md @@ -1,9 +1,9 @@ --- -title: 分布式配置中心常见问题总结(付费) -category: 分布式 +title: Summary of Common Issues in Distributed Configuration Centers (Paid) +category: Distributed --- -**分布式配置中心** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了《Java 面试指北》中。 +The interview questions related to **Distributed Configuration Centers** are exclusive content for my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) (click the link to see the detailed introduction and joining methods) and have been compiled into the "Java Interview Guide." ![](https://oss.javaguide.cn/javamianshizhibei/distributed-system.png) diff --git a/docs/distributed-system/distributed-id-design.md b/docs/distributed-system/distributed-id-design.md index 5b737f34593..8d545e4cfb9 100644 --- a/docs/distributed-system/distributed-id-design.md +++ b/docs/distributed-system/distributed-id-design.md @@ -1,173 +1,62 @@ --- -title: 分布式ID设计指南 -category: 分布式 +title: Distributed ID Design Guide +category: Distributed --- ::: tip -看到百度 Geek 说的一篇结合具体场景聊分布式 ID 设计的文章,感觉挺不错的。于是,我将这篇文章的部分内容整理到了这里。原文传送门:[分布式 ID 生成服务的技术原理和项目实战](https://mp.weixin.qq.com/s/bFDLb6U6EgI-DvCdLTq_QA) 。 +I came across an article by Baidu Geek discussing the design of distributed IDs in specific scenarios, and I found it quite insightful. Therefore, I have organized some of the content from this article here. You can find the original article here: [Technical Principles and Project Practice of Distributed ID Generation Service](https://mp.weixin.qq.com/s/bFDLb6U6EgI-DvCdLTq_QA). ::: -网上绝大多数的分布式 ID 生成服务,一般着重于技术原理剖析,很少见到根据具体的业务场景去选型 ID 生成服务的文章。 +Most distributed ID generation services available online generally focus on technical principles and rarely discuss how to choose ID generation services based on specific business scenarios. -本文结合一些使用场景,进一步探讨业务场景中对 ID 有哪些具体的要求。 +This article explores the specific requirements for IDs in various business scenarios. -## 场景一:订单系统 +## Scenario 1: Order System -我们在商场买东西一码付二维码,下单生成的订单号,使用到的优惠券码,联合商品兑换券码,这些是在网上购物经常使用到的单号,那么为什么有些单号那么长,有些只有几位数?有些单号一看就知道年月日的信息,有些却看不出任何意义?下面展开分析下订单系统中不同场景的 id 服务的具体实现。 +When we shop in a mall, we often use a QR code for payment, and the order number generated upon placing an order, along with the coupon codes and product exchange codes, are frequently used identifiers in online shopping. So why are some order numbers so long while others are only a few digits? Some order numbers clearly indicate the date, while others seem meaningless. Below, we analyze the specific implementations of ID services in different scenarios within the order system. -### 1、一码付 +### 1. One-Code Payment -我们常见的一码付,指的是一个二维码可以使用支付宝或者微信进行扫码支付。 +The common one-code payment refers to a QR code that can be scanned using Alipay or WeChat for payment. -二维码的本质是一个字符串。聚合码的本质就是一个链接地址。用户使用支付宝微信直接扫一个码付钱,不用担心拿支付宝扫了微信的收款码或者用微信扫了支付宝的收款码,这极大减少了用户扫码支付的时间。 +The essence of a QR code is a string. The aggregation code is essentially a link. Users can directly scan a code with Alipay or WeChat to make payments without worrying about scanning the wrong payment code, which greatly reduces the time taken for users to complete the payment. -实现原理是当客户用 APP 扫码后,网站后台就会判断客户的扫码环境。(微信、支付宝、QQ 钱包、京东支付、云闪付等)。 +The implementation principle is that when a customer scans the code with the app, the website backend determines the customer's scanning environment (WeChat, Alipay, QQ Wallet, JD Pay, UnionPay, etc.). -判断扫码环境的原理就是根据打开链接浏览器的 HTTP header。任何浏览器打开 http 链接时,请求的 header 都会有 User-Agent(UA、用户代理)信息。 +The principle for determining the scanning environment is based on the HTTP header of the browser that opens the link. Any browser that opens an HTTP link will have a User-Agent (UA) information in the request header. -UA 是一个特殊字符串头,服务器依次可以识别出客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等很多信息。 +The UA is a special string header that allows the server to identify the customer's operating system and version, CPU type, browser and version, browser rendering engine, browser language, browser plugins, and much more. -各渠道对应支付产品的名称不一样,一定要仔细看各支付产品的 API 介绍。 +The names of payment products vary across different channels, so it is essential to carefully review the API documentation for each payment product. -1. 微信支付:JSAPI 支付支付 -2. 支付宝:手机网站支付 -3. QQ 钱包:公众号支付 +1. WeChat Pay: JSAPI Payment +1. Alipay: Mobile Website Payment +1. QQ Wallet: Public Account Payment -其本质均为在 APP 内置浏览器中实现 HTML5 支付。 +Essentially, they all implement HTML5 payment within the app's built-in browser. -![文库会员支付示例](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-design-pay-one-card.png) +![Library Member Payment Example](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-design-pay-one-card.png) -文库的研发同学在这个思路上,做了优化迭代。动态生成一码付的二维码预先绑定用户所选的商品信息和价格,根据用户所选的商品动态更新。这样不仅支持一码多平台调起支付,而且不用用户选择商品输入金额,即可完成订单支付的功能,很丝滑。用户在真正扫码后,服务端才通过前端获取用户 UID,结合二维码绑定的商品信息,真正的生成订单,发送支付信息到第三方(qq、微信、支付宝),第三方生成支付订单推给用户设备,从而调起支付。 +The development team at the library has optimized this approach. They dynamically generate a one-code payment QR code that is pre-bound to the product information and price selected by the user, updating dynamically based on the user's selection. This not only supports multi-platform payment initiation but also allows users to complete order payments without selecting products or entering amounts, providing a smooth experience. Only after the user scans the code does the server obtain the user's UID from the frontend, combine it with the product information bound to the QR code, truly generate the order, and send the payment information to third parties (QQ, WeChat, Alipay), which then generate the payment order and push it to the user's device to initiate payment. -区别于固定的一码付,在文库的应用中,使用到了动态二维码,二维码本质是一个短网址,ID 服务提供短网址的唯一标志参数。唯一的短网址映射的 ID 绑定了商品的订单信息,技术和业务的深度结合,缩短了支付流程,提升用户的支付体验。 +Unlike fixed one-code payments, the library's application uses dynamic QR codes, where the QR code is essentially a short URL, and the ID service provides a unique identifier for the short URL. The unique short URL maps to the ID bound to the product's order information, deeply integrating technology and business, shortening the payment process, and enhancing the user's payment experience. -### 2、订单号 +### 2. Order Number -订单号在实际的业务过程中作为一个订单的唯一标识码存在,一般实现以下业务场景: +In actual business processes, the order number serves as a unique identifier for an order and generally fulfills the following business scenarios: -1. 用户订单遇到问题,需要找客服进行协助; -2. 对订单进行操作,如线下收款,订单核销; -3. 下单,改单,成单,退单,售后等系统内部的订单流程处理和跟进。 +1. Users encounter issues with their orders and need to contact customer service for assistance. +1. Operations on the order, such as offline payments and order verification. +1. Internal order processing and follow-up for placing, modifying, completing, canceling, and after-sales orders. -很多时候搜索订单相关信息的时候都是以订单 ID 作为唯一标识符,这是由于订单号的生成规则的唯一性决定的。从技术角度看,除了 ID 服务必要的特性之外,在订单号的设计上需要体现几个特性: +Often, when searching for order-related information, the order ID is used as a unique identifier, which is determined by the uniqueness of the order number generation rules. From a technical perspective, in addition to the necessary characteristics of the ID service, the design of the order number needs to reflect several features: -**(1)信息安全** +**(1) Information Security** -编号不能透露公司的运营情况,比如日销、公司流水号等信息,以及商业信息和用户手机号,身份证等隐私信息。并且不能有明显的整体规律(可以有局部规律),任意修改一个字符就能查询到另一个订单信息,这也是不允许的。 +The numbering should not reveal the company's operational status, such as daily sales, company serial numbers, or any commercial information and user privacy information like phone numbers and ID numbers. Additionally, there should not be any obvious overall patterns (local patterns are acceptable), and modifying a single character should not allow access to another order's information. -类比于我们高考时候的考生编号的生成规则,一定不能是连号的,否则只需要根据顺序往下查询就能搜索到别的考生的成绩,这是绝对不可允许。 +This is akin to the generation rules for student numbers during our college entrance examination, which must not be sequential; otherwise, one could easily search for another student's scores based on the order. -**(2)部分可读** - -位数要便于操作,因此要求订单号的位数适中,且局部有规律。这样可以方便在订单异常,或者退货时客服查询。 - -过长的订单号或易读性差的订单号会导致客服输入困难且易错率较高,影响用户体验的售后体验。因此在实际的业务场景中,订单号的设计通常都会适当携带一些允许公开的对使用场景有帮助的信息,如时间,星期,类型等等,这个主要根据所涉及的编号对应的使用场景来。 - -而且像时间、星期这些自增长的属于作为订单号的设计的一部分元素,有助于解决业务累积而导致的订单号重复的问题。 - -**(3)查询效率** - -常见的电商平台订单号大多是纯数字组成,兼具可读性的同时,int 类型相对 varchar 类型的查询效率更高,对在线业务更加友好。 - -### 3、优惠券和兑换券 - -优惠券、兑换券是运营推广最常用的促销工具之一,合理使用它们,可以让买家得到实惠,商家提升商品销量。常见场景有: - -1. 在文库购买【文库 VIP+QQ 音乐年卡】联合商品,支付成功后会得到 QQ 音乐年卡的兑换码,可以去 QQ 音乐 App 兑换音乐会员年卡; -2. 疫情期间,部分地方政府发放的消费券; -3. 瓶装饮料经常会出现输入优惠编码兑换奖品。 - -![优惠编码兑换奖品](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-design-coupon.png) - -从技术角度看,有些场景适合 ID 即时生成,比如电商平台购物领取的优惠券,只需要在用户领取时分配优惠券信息即可。有些线上线下结合的场景,比如疫情优惠券,瓶盖开奖,京东卡,超市卡这种,则需要预先生成,预先生成的券码具备以下特性: - -1.预先生成,在活动正式开始前提供出来进行活动预热; - -2.优惠券体量大,以万为单位,通常在 10 万级别以上; - -3.不可破解、仿制券码; - -4.支持用后核销; - -5.优惠券、兑换券属于广撒网的策略,所以利用率低,也就不适合使用数据库进行存储 **(占空间,有效的数据又少)**。 - -设计思路上,需要设计一种有效的兑换码生成策略,支持预先生成,支持校验,内容简洁,生成的兑换码都具有唯一性,那么这种策略就是一种特殊的编解码策略,按照约定的编解码规则支撑上述需求。 - -既然是一种编解码规则,那么需要约定编码空间(也就是用户看到的组成兑换码的字符),编码空间由字符 a-z,A-Z,数字 0-9 组成,为了增强兑换码的可识别度,剔除大写字母 O 以及 I,可用字符如下所示,共 60 个字符: - -abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXZY0123456789 - -之前说过,兑换码要求尽可能简洁,那么设计时就需要考虑兑换码的字符数,假设上限为 12 位,而字符空间有 60 位,那么可以表示的空间范围为 60^12=130606940160000000000000(也就是可以 12 位的兑换码可以生成天量,应该够运营同学挥霍了),转换成 2 进制: - -1001000100000000101110011001101101110011000000000000000000000(61 位) - -**兑换码组成成分分析** - -兑换码可以预先生成,并且不需要额外的存储空间保存这些信息,每一个优惠方案都有独立的一组兑换码(指运营同学组织的每一场运营活动都有不同的兑换码,不能混合使用, 例如双 11 兑换码不能使用在双 12 活动上),每个兑换码有自己的编号,防止重复,为了保证兑换码的有效性,对兑换码的数据需要进行校验,当前兑换码的数据组成如下所示: - -优惠方案 ID + 兑换码序列号 i + 校验码 - -**编码方案** - -1. 兑换码序列号 i,代表当前兑换码是当前活动中第 i 个兑换码,兑换码序列号的空间范围决定了优惠活动可以发行的兑换码数目,当前采用 30 位 bit 位表示,可表示范围:1073741824(10 亿个券码)。 -2. 优惠方案 ID, 代表当前优惠方案的 ID 号,优惠方案的空间范围决定了可以组织的优惠活动次数,当前采用 15 位表示,可以表示范围:32768(考虑到运营活动的频率,以及 ID 的初始值 10000,15 位足够,365 天每天有运营活动,可以使用 54 年)。 -3. 校验码,校验兑换码是否有效,主要为了快捷的校验兑换码信息的是否正确,其次可以起到填充数据的目的,增强数据的散列性,使用 13 位表示校验位,其中分为两部分,前 6 位和后 7 位。 - -深耕业务还会有区分通用券和单独券的情况,分别具备以下特点,技术实现需要因地制宜地思考。 - -1. 通用券:多个玩家都可以输入兑换,然后有总量限制,期限限制。 -2. 单独券:运营同学可以在后台设置兑换码的奖励物品、期限、个数,然后由后台生成兑换码的列表,兑换之后核销。 - -## 场景二:Tracing - -### 1、日志跟踪 - -在分布式服务架构下,一个 Web 请求从网关流入,有可能会调用多个服务对请求进行处理,拿到最终结果。这个过程中每个服务之间的通信又是单独的网络请求,无论请求经过的哪个服务出了故障或者处理过慢都会对前端造成影响。 - -处理一个 Web 请求要调用的多个服务,为了能更方便的查询哪个环节的服务出现了问题,现在常用的解决方案是为整个系统引入分布式链路跟踪。 - -![在分布式链路跟踪](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-design-tracing.png) - -在分布式链路跟踪中有两个重要的概念:跟踪(trace)和 跨度( span)。trace 是请求在分布式系统中的整个链路视图,span 则代表整个链路中不同服务内部的视图,span 组合在一起就是整个 trace 的视图。 - -在整个请求的调用链中,请求会一直携带 traceid 往下游服务传递,每个服务内部也会生成自己的 spanid 用于生成自己的内部调用视图,并和 traceid 一起传递给下游服务。 - -### 2、TraceId 生成规则 - -这种场景下,生成的 ID 除了要求唯一之外,还要求生成的效率高、吞吐量大。traceid 需要具备接入层的服务器实例自主生成的能力,如果每个 trace 中的 ID 都需要请求公共的 ID 服务生成,纯纯的浪费网络带宽资源。且会阻塞用户请求向下游传递,响应耗时上升,增加了没必要的风险。所以需要服务器实例最好可以自行计算 tracid,spanid,避免依赖外部服务。 - -产生规则:服务器 IP + ID 产生的时间 + 自增序列 + 当前进程号 ,比如: - -0ad1348f1403169275002100356696 - -前 8 位 0ad1348f 即产生 TraceId 的机器的 IP,这是一个十六进制的数字,每两位代表 IP 中的一段,我们把这个数字,按每两位转成 10 进制即可得到常见的 IP 地址表示方式 10.209.52.143,您也可以根据这个规律来查找到请求经过的第一个服务器。 - -后面的 13 位 1403169275002 是产生 TraceId 的时间。之后的 4 位 1003 是一个自增的序列,从 1000 涨到 9000,到达 9000 后回到 1000 再开始往上涨。最后的 5 位 56696 是当前的进程 ID,为了防止单机多进程出现 TraceId 冲突的情况,所以在 TraceId 末尾添加了当前的进程 ID。 - -### 3、SpanId 生成规则 - -span 是层的意思,比如在第一个实例算是第一层, 请求代理或者分流到下一个实例处理,就是第二层,以此类推。通过层,SpanId 代表本次调用在整个调用链路树中的位置。 - -假设一个 服务器实例 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。 - -根据上面的描述可以知道,如果把一次调用中所有的 SpanId 收集起来,可以组成一棵完整的链路树。 - -**spanid 的生成本质:在跨层传递透传的同时,控制大小版本号的自增来实现的。** - -## 场景三:短网址 - -短网址主要功能包括网址缩短与还原两大功能。相对于长网址,短网址可以更方便地在电子邮件,社交网络,微博和手机上传播,例如原来很长的网址通过短网址服务即可生成相应的短网址,避免折行或超出字符限制。 - -![短网址作用](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-design-short-url.png) - -常用的 ID 生成服务比如:MySQL ID 自增、 Redis 键自增、号段模式,生成的 ID 都是一串数字。短网址服务把客户的长网址转换成短网址, - -实际是在 dwz.cn 域名后面拼接新产生的数字类型 ID,直接用数字 ID,网址长度也有些长,服务可以通过数字 ID 转更高进制的方式压缩长度。这种算法在短网址的技术实现上越来越多了起来,它可以进一步压缩网址长度。转进制的压缩算法在生活中有广泛的应用场景,举例: - -- 客户的长网址: -- ID 映射的短网址: (演示使用,可能无法正确打开) -- 转进制后的短网址: (演示使用,可能无法正确打开) - - +\*\*(2) Partial diff --git a/docs/distributed-system/distributed-id.md b/docs/distributed-system/distributed-id.md index 9920f8f7753..e23a8336dbf 100644 --- a/docs/distributed-system/distributed-id.md +++ b/docs/distributed-system/distributed-id.md @@ -1,65 +1,65 @@ --- -title: 分布式ID介绍&实现方案总结 -category: 分布式 +title: Introduction to Distributed ID & Summary of Implementation Solutions +category: Distributed --- -## 分布式 ID 介绍 +## Introduction to Distributed ID -### 什么是 ID? +### What is an ID? -日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示,比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单。 +In daily development, we need to uniquely represent various data in the system using IDs. For example, a user ID corresponds to and only corresponds to one person, a product ID corresponds to and only corresponds to one product, and an order ID corresponds to and only corresponds to one order. -我们现实生活中也有各种 ID,比如身份证 ID 对应且仅对应一个人、地址 ID 对应且仅对应一个地址。 +In real life, we also have various IDs, such as an ID card ID that corresponds to and only corresponds to one person, and an address ID that corresponds to and only corresponds to one address. -简单来说,**ID 就是数据的唯一标识**。 +In simple terms, **an ID is a unique identifier for data**. -### 什么是分布式 ID? +### What is a Distributed ID? -分布式 ID 是分布式系统下的 ID。分布式 ID 不存在与现实生活中,属于计算机系统中的一个概念。 +A distributed ID is an ID used in a distributed system. Distributed IDs do not exist in real life; they are a concept within computer systems. -我简单举一个分库分表的例子。 +Let me give a simple example of sharding. -我司的一个项目,使用的是单机 MySQL 。但是,没想到的是,项目上线一个月之后,随着使用人数越来越多,整个系统的数据量将越来越大。单机 MySQL 已经没办法支撑了,需要进行分库分表(推荐 Sharding-JDBC)。 +In one of our projects, we used a standalone MySQL database. However, unexpectedly, after the project went live for a month, as the number of users increased, the data volume of the entire system grew larger. The standalone MySQL could no longer support it, and we needed to implement sharding (recommended Sharding-JDBC). -在分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。**我们如何为不同的数据节点生成全局唯一主键呢?** +After sharding, the data is distributed across databases on different servers, and the auto-incrementing primary key of the database can no longer guarantee the uniqueness of the generated primary key. **How do we generate a globally unique primary key for different data nodes?** -这个时候就需要生成**分布式 ID**了。 +At this point, we need to generate **distributed IDs**. ![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/id-after-the-sub-table-not-conflict.png) -### 分布式 ID 需要满足哪些要求? +### What Requirements Must Distributed IDs Meet? ![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-requirements.png) -分布式 ID 作为分布式系统中必不可少的一环,很多地方都要用到分布式 ID。 +As an essential part of a distributed system, distributed IDs are needed in many places. -一个最基本的分布式 ID 需要满足下面这些要求: +A basic distributed ID must meet the following requirements: -- **全局唯一**:ID 的全局唯一性肯定是首先要满足的! -- **高性能**:分布式 ID 的生成速度要快,对本地资源消耗要小。 -- **高可用**:生成分布式 ID 的服务要保证可用性无限接近于 100%。 -- **方便易用**:拿来即用,使用方便,快速接入! +- **Globally Unique**: The global uniqueness of the ID must be guaranteed! +- **High Performance**: The generation speed of distributed IDs must be fast, with minimal local resource consumption. +- **High Availability**: The service generating distributed IDs must ensure availability close to 100%. +- **Convenient and Easy to Use**: It should be ready to use, easy to implement, and quickly integrated! -除了这些之外,一个比较好的分布式 ID 还应保证: +In addition to these, a good distributed ID should also ensure: -- **安全**:ID 中不包含敏感信息。 -- **有序递增**:如果要把 ID 存放在数据库的话,ID 的有序性可以提升数据库写入速度。并且,很多时候 ,我们还很有可能会直接通过 ID 来进行排序。 -- **有具体的业务含义**:生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。 -- **独立部署**:也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。 +- **Security**: The ID should not contain sensitive information. +- **Ordered Increment**: If IDs are to be stored in a database, the order of IDs can improve database write speed. Moreover, many times, we may directly sort by ID. +- **Specific Business Meaning**: If the generated ID has specific business meaning, it can make problem identification and development more transparent (the ID can indicate which business it belongs to). +- **Independent Deployment**: This means that the distributed system has a separate ID generator service specifically for generating distributed IDs. This decouples the ID generation service from business-related services. However, this also increases the overhead of network calls. Overall, if there are many scenarios requiring distributed IDs, an independently deployed ID generator service is still very necessary. -## 分布式 ID 常见解决方案 +## Common Solutions for Distributed IDs -### 数据库 +### Database -#### 数据库主键自增 +#### Auto-Incrementing Database Primary Key -这种方式就比较简单直白了,就是通过关系型数据库的自增主键产生来唯一的 ID。 +This method is quite straightforward; it generates a unique ID through the auto-incrementing primary key of a relational database. -![数据库主键自增](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/the-primary-key-of-the-database-increases-automatically.png) +![Auto-Incrementing Database Primary Key](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/the-primary-key-of-the-database-increases-automatically.png) -以 MySQL 举例,我们通过下面的方式即可。 +Taking MySQL as an example, we can do it as follows. -**1.创建一个数据库表。** +**1. Create a database table.** ```sql CREATE TABLE `sequence_id` ( @@ -70,9 +70,9 @@ CREATE TABLE `sequence_id` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` -`stub` 字段无意义,只是为了占位,便于我们插入或者修改数据。并且,给 `stub` 字段创建了唯一索引,保证其唯一性。 +The `stub` field is meaningless; it is just a placeholder to facilitate data insertion or modification. Additionally, a unique index is created for the `stub` field to ensure its uniqueness. -**2.通过 `replace into` 来插入数据。** +**2. Insert data using `replace into`.** ```java BEGIN; @@ -81,315 +81,11 @@ SELECT LAST_INSERT_ID(); COMMIT; ``` -插入数据这里,我们没有使用 `insert into` 而是使用 `replace into` 来插入数据,具体步骤是这样的: +In this data insertion, we use `replace into` instead of `insert into`. The specific steps are as follows: -- 第一步:尝试把数据插入到表中。 +- Step 1: Attempt to insert data into the table. +- Step 2: If a duplicate data error occurs due to the primary key or unique index field, delete the conflicting row containing the duplicate key value from the table, and then attempt to insert the data again. -- 第二步:如果主键或唯一索引字段出现重复数据错误而插入失败时,先从表中删除含有重复关键字值的冲突行,然后再次尝试把数据插入到表中。 +The advantages and disadvantages of this method are also quite clear: -这种方式的优缺点也比较明显: - -- **优点**:实现起来比较简单、ID 有序递增、存储消耗空间小 -- **缺点**:支持的并发量不大、存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! )、每次获取 ID 都要访问一次数据库(增加了对数据库的压力,获取速度也慢) - -#### 数据库号段模式 - -数据库主键自增这种模式,每次获取 ID 都要访问一次数据库,ID 需求比较大的时候,肯定是不行的。 - -如果我们可以批量获取,然后存在在内存里面,需要用到的时候,直接从内存里面拿就舒服了!这也就是我们说的 **基于数据库的号段模式来生成分布式 ID。** - -数据库的号段模式也是目前比较主流的一种分布式 ID 生成方式。像滴滴开源的[Tinyid](https://github.com/didi/tinyid/wiki/tinyid原理介绍) 就是基于这种方式来做的。不过,TinyId 使用了双号段缓存、增加多 db 支持等方式来进一步优化。 - -以 MySQL 举例,我们通过下面的方式即可。 - -**1. 创建一个数据库表。** - -```sql -CREATE TABLE `sequence_id_generator` ( - `id` int(10) NOT NULL, - `current_max_id` bigint(20) NOT NULL COMMENT '当前最大id', - `step` int(10) NOT NULL COMMENT '号段的长度', - `version` int(20) NOT NULL COMMENT '版本号', - `biz_type` int(20) NOT NULL COMMENT '业务类型', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -`current_max_id` 字段和`step`字段主要用于获取批量 ID,获取的批量 id 为:`current_max_id ~ current_max_id+step`。 - -![数据库号段模式](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/database-number-segment-mode.png) - -`version` 字段主要用于解决并发问题(乐观锁),`biz_type` 主要用于表示业务类型。 - -**2. 先插入一行数据。** - -```sql -INSERT INTO `sequence_id_generator` (`id`, `current_max_id`, `step`, `version`, `biz_type`) -VALUES - (1, 0, 100, 0, 101); -``` - -**3. 通过 SELECT 获取指定业务下的批量唯一 ID** - -```sql -SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101 -``` - -结果: - -```plain -id current_max_id step version biz_type -1 0 100 0 101 -``` - -**4. 不够用的话,更新之后重新 SELECT 即可。** - -```sql -UPDATE sequence_id_generator SET current_max_id = 0+100, version=version+1 WHERE version = 0 AND `biz_type` = 101 -SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101 -``` - -结果: - -```plain -id current_max_id step version biz_type -1 100 100 1 101 -``` - -相比于数据库主键自增的方式,**数据库的号段模式对于数据库的访问次数更少,数据库压力更小。** - -另外,为了避免单点问题,你可以从使用主从模式来提高可用性。 - -**数据库号段模式的优缺点:** - -- **优点**:ID 有序递增、存储消耗空间小 -- **缺点**:存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! ) - -#### NoSQL - -![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/nosql-distributed-id.png) - -一般情况下,NoSQL 方案使用 Redis 多一些。我们通过 Redis 的 `incr` 命令即可实现对 id 原子顺序递增。 - -```bash -127.0.0.1:6379> set sequence_id_biz_type 1 -OK -127.0.0.1:6379> incr sequence_id_biz_type -(integer) 2 -127.0.0.1:6379> get sequence_id_biz_type -"2" -``` - -为了提高可用性和并发,我们可以使用 Redis Cluster。Redis Cluster 是 Redis 官方提供的 Redis 集群解决方案(3.0+版本)。 - -除了 Redis Cluster 之外,你也可以使用开源的 Redis 集群方案[Codis](https://github.com/CodisLabs/codis) (大规模集群比如上百个节点的时候比较推荐)。 - -除了高可用和并发之外,我们知道 Redis 基于内存,我们需要持久化数据,避免重启机器或者机器故障后数据丢失。Redis 支持两种不同的持久化方式:**快照(snapshotting,RDB)**、**只追加文件(append-only file, AOF)**。 并且,Redis 4.0 开始支持 **RDB 和 AOF 的混合持久化**(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。 - -关于 Redis 持久化,我这里就不过多介绍。不了解这部分内容的小伙伴,可以看看 [Redis 持久化机制详解](https://javaguide.cn/database/redis/redis-persistence.html)这篇文章。 - -**Redis 方案的优缺点:** - -- **优点**:性能不错并且生成的 ID 是有序递增的 -- **缺点**:和数据库主键自增方案的缺点类似 - -除了 Redis 之外,MongoDB ObjectId 经常也会被拿来当做分布式 ID 的解决方案。 - -![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/mongodb9-objectId-distributed-id.png) - -MongoDB ObjectId 一共需要 12 个字节存储: - -- 0~3:时间戳 -- 3~6:代表机器 ID -- 7~8:机器进程 ID -- 9~11:自增值 - -**MongoDB 方案的优缺点:** - -- **优点**:性能不错并且生成的 ID 是有序递增的 -- **缺点**:需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID)、有安全性问题(ID 生成有规律性) - -### 算法 - -#### UUID - -UUID 是 Universally Unique Identifier(通用唯一标识符) 的缩写。UUID 包含 32 个 16 进制数字(8-4-4-4-12)。 - -JDK 就提供了现成的生成 UUID 的方法,一行代码就行了。 - -```java -//输出示例:cb4a9ede-fa5e-4585-b9bb-d60bce986eaa -UUID.randomUUID() -``` - -[RFC 4122](https://tools.ietf.org/html/rfc4122) 中关于 UUID 的示例是这样的: - -![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/rfc-4122-uuid.png) - -我们这里重点关注一下这个 Version(版本),不同的版本对应的 UUID 的生成规则是不同的。 - -8 种不同的 Version(版本)值分别对应的含义(参考[维基百科对于 UUID 的介绍](https://zh.wikipedia.org/wiki/通用唯一识别码)): - -- **版本 1 (基于时间和节点 ID)** : 基于时间戳(通常是当前时间)和节点 ID(通常为设备的 MAC 地址)生成。当包含 MAC 地址时,可以保证全球唯一性,但也因此存在隐私泄露的风险。 -- **版本 2 (基于标识符、时间和节点 ID)** : 与版本 1 类似,也基于时间和节点 ID,但额外包含了本地标识符(例如用户 ID 或组 ID)。 -- **版本 3 (基于命名空间和名称的 MD5 哈希)**:使用 MD5 哈希算法,将命名空间标识符(一个 UUID)和名称字符串组合计算得到。相同的命名空间和名称总是生成相同的 UUID(**确定性生成**)。 -- **版本 4 (基于随机数)**:几乎完全基于随机数生成,通常使用伪随机数生成器(PRNG)或加密安全随机数生成器(CSPRNG)来生成。 虽然理论上存在碰撞的可能性,但理论上碰撞概率极低(2^122 的可能性),可以认为在实际应用中是唯一的。 -- **版本 5 (基于命名空间和名称的 SHA-1 哈希)**:类似于版本 3,但使用 SHA-1 哈希算法。 -- **版本 6 (基于时间戳、计数器和节点 ID)**:改进了版本 1,将时间戳放在最高有效位(Most Significant Bit,MSB),使得 UUID 可以直接按时间排序。 -- **版本 7 (基于时间戳和随机数据)**:基于 Unix 时间戳和随机数据生成。 由于时间戳位于最高有效位,因此支持按时间排序。并且,不依赖 MAC 地址或节点 ID,避免了隐私问题。 -- **版本 8 (自定义)**:允许用户根据自己的需求定义 UUID 的生成方式。其结构和内容由用户决定,提供更大的灵活性。 - -下面是 Version 1 版本下生成的 UUID 的示例: - -![Version 1 版本下生成的 UUID 的示例](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/version1-uuid.png) - -JDK 中通过 `UUID` 的 `randomUUID()` 方法生成的 UUID 的版本默认为 4。 - -```java -UUID uuid = UUID.randomUUID(); -int version = uuid.version();// 4 -``` - -另外,Variant(变体)也有 4 种不同的值,这种值分别对应不同的含义。这里就不介绍了,貌似平时也不怎么需要关注。 - -需要用到的时候,去看看维基百科对于 UUID 的 Variant(变体) 相关的介绍即可。 - -从上面的介绍中可以看出,UUID 可以保证唯一性,因为其生成规则包括 MAC 地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素,计算机基于这些规则生成的 UUID 是肯定不会重复的。 - -虽然,UUID 可以做到全局唯一性,但是,我们一般很少会使用它。 - -比如使用 UUID 作为 MySQL 数据库主键的时候就非常不合适: - -- 数据库主键要尽量越短越好,而 UUID 的消耗的存储空间比较大(32 个字符串,128 位)。 -- UUID 是无顺序的,InnoDB 引擎下,数据库主键的无序性会严重影响数据库性能。 - -最后,我们再简单分析一下 **UUID 的优缺点** (面试的时候可能会被问到的哦!) : - -- **优点**:生成速度通常比较快、简单易用 -- **缺点**:存储消耗空间大(32 个字符串,128 位)、 不安全(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)、无序(非自增)、没有具体业务含义、需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID) - -#### Snowflake(雪花算法) - -Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit 的二进制数字组成,这 64bit 的二进制被分成了几部分,每一部分存储的数据都有特定的含义: - -![Snowflake 组成](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/snowflake-distributed-id-schematic-diagram.png) - -- **sign(1bit)**:符号位(标识正负),始终为 0,代表生成的 ID 为正数。 -- **timestamp (41 bits)**:一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41 毫秒(约 69 年) -- **datacenter id + worker id (10 bits)**:一般来说,前 5 位表示机房 ID,后 5 位表示机器 ID(实际项目中可以根据实际情况调整)。这样就可以区分不同集群/机房的节点。 -- **sequence (12 bits)**:一共 12 位,用来表示序列号。 序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数(2^12 = 4096),也就是说单台机器每毫秒最多可以生成 4096 个 唯一 ID。 - -在实际项目中,我们一般也会对 Snowflake 算法进行改造,最常见的就是在 Snowflake 算法生成的 ID 中加入业务类型信息。 - -我们再来看看 Snowflake 算法的优缺点: - -- **优点**:生成速度比较快、生成的 ID 有序递增、比较灵活(可以对 Snowflake 算法进行简单的改造比如加入业务 ID) -- **缺点**:需要解决重复 ID 问题(ID 生成依赖时间,在获取时间的时候,可能会出现时间回拨的问题,也就是服务器上的时间突然倒退到之前的时间,进而导致会产生重复 ID)、依赖机器 ID 对分布式环境不友好(当需要自动启停或增减机器时,固定的机器 ID 可能不够灵活)。 - -如果你想要使用 Snowflake 算法的话,一般不需要你自己再造轮子。有很多基于 Snowflake 算法的开源实现比如美团 的 Leaf、百度的 UidGenerator(后面会提到),并且这些开源实现对原有的 Snowflake 算法进行了优化,性能更优秀,还解决了 Snowflake 算法的时间回拨问题和依赖机器 ID 的问题。 - -并且,Seata 还提出了“改良版雪花算法”,针对原版雪花算法进行了一定的优化改良,解决了时间回拨问题,大幅提高的 QPS。具体介绍和改进原理,可以参考下面这两篇文章: - -- [Seata 基于改良版雪花算法的分布式 UUID 生成器分析](https://seata.io/zh-cn/blog/seata-analysis-UUID-generator.html) -- [在开源项目中看到一个改良版的雪花算法,现在它是你的了。](https://www.cnblogs.com/thisiswhy/p/17611163.html) - -### 开源框架 - -#### UidGenerator(百度) - -[UidGenerator](https://github.com/baidu/uid-generator) 是百度开源的一款基于 Snowflake(雪花算法)的唯一 ID 生成器。 - -不过,UidGenerator 对 Snowflake(雪花算法)进行了改进,生成的唯一 ID 组成如下: - -![UidGenerator 生成的 ID 组成](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/uidgenerator-distributed-id-schematic-diagram.png) - -- **sign(1bit)**:符号位(标识正负),始终为 0,代表生成的 ID 为正数。 -- **delta seconds (28 bits)**:当前时间,相对于时间基点"2016-05-20"的增量值,单位:秒,最多可支持约 8.7 年 -- **worker id (22 bits)**:机器 id,最多可支持约 420w 次机器启动。内置实现为在启动时由数据库分配,默认分配策略为用后即弃,后续可提供复用策略。 -- **sequence (13 bits)**:每秒下的并发序列,13 bits 可支持每秒 8192 个并发。 - -可以看出,和原始 Snowflake(雪花算法)生成的唯一 ID 的组成不太一样。并且,上面这些参数我们都可以自定义。 - -UidGenerator 官方文档中的介绍如下: - -![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/uidgenerator-introduction-official-documents.png) - -自 18 年后,UidGenerator 就基本没有再维护了,我这里也不过多介绍。想要进一步了解的朋友,可以看看 [UidGenerator 的官方介绍](https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md)。 - -#### Leaf(美团) - -[Leaf](https://github.com/Meituan-Dianping/Leaf) 是美团开源的一个分布式 ID 解决方案 。这个项目的名字 Leaf(树叶) 起源于德国哲学家、数学家莱布尼茨的一句话:“There are no two identical leaves in the world”(世界上没有两片相同的树叶) 。这名字起得真心挺不错的,有点文艺青年那味了! - -Leaf 提供了 **号段模式** 和 **Snowflake(雪花算法)** 这两种模式来生成分布式 ID。并且,它支持双号段,还解决了雪花 ID 系统时钟回拨问题。不过,时钟问题的解决需要弱依赖于 Zookeeper(使用 Zookeeper 作为注册中心,通过在特定路径下读取和创建子节点来管理 workId) 。 - -Leaf 的诞生主要是为了解决美团各个业务线生成分布式 ID 的方法多种多样以及不可靠的问题。 - -Leaf 对原有的号段模式进行改进,比如它这里增加了双号段避免获取 DB 在获取号段的时候阻塞请求获取 ID 的线程。简单来说,就是我一个号段还没用完之前,我自己就主动提前去获取下一个号段(图片来自于美团官方文章:[《Leaf——美团点评分布式 ID 生成系统》](https://tech.meituan.com/2017/04/21/mt-leaf.html))。 - -![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-id/leaf-principle.png) - -根据项目 README 介绍,在 4C8G VM 基础上,通过公司 RPC 方式调用,QPS 压测结果近 5w/s,TP999 1ms。 - -#### Tinyid(滴滴) - -[Tinyid](https://github.com/didi/tinyid) 是滴滴开源的一款基于数据库号段模式的唯一 ID 生成器。 - -数据库号段模式的原理我们在上面已经介绍过了。**Tinyid 有哪些亮点呢?** - -为了搞清楚这个问题,我们先来看看基于数据库号段模式的简单架构方案。(图片来自于 Tinyid 的官方 wiki:[《Tinyid 原理介绍》](https://github.com/didi/tinyid/wiki/tinyid%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D)) - -![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-id/tinyid-principle.png) - -在这种架构模式下,我们通过 HTTP 请求向发号器服务申请唯一 ID。负载均衡 router 会把我们的请求送往其中的一台 tinyid-server。 - -这种方案有什么问题呢?在我看来(Tinyid 官方 wiki 也有介绍到),主要由下面这 2 个问题: - -- 获取新号段的情况下,程序获取唯一 ID 的速度比较慢。 -- 需要保证 DB 高可用,这个是比较麻烦且耗费资源的。 - -除此之外,HTTP 调用也存在网络开销。 - -Tinyid 的原理比较简单,其架构如下图所示: - -![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-id/tinyid-architecture-design.png) - -相比于基于数据库号段模式的简单架构方案,Tinyid 方案主要做了下面这些优化: - -- **双号段缓存**:为了避免在获取新号段的情况下,程序获取唯一 ID 的速度比较慢。 Tinyid 中的号段在用到一定程度的时候,就会去异步加载下一个号段,保证内存中始终有可用号段。 -- **增加多 db 支持**:支持多个 DB,并且,每个 DB 都能生成唯一 ID,提高了可用性。 -- **增加 tinyid-client**:纯本地操作,无 HTTP 请求消耗,性能和可用性都有很大提升。 - -Tinyid 的优缺点这里就不分析了,结合数据库号段模式的优缺点和 Tinyid 的原理就能知道。 - -#### IdGenerator(个人) - -和 UidGenerator、Leaf 一样,[IdGenerator](https://github.com/yitter/IdGenerator) 也是一款基于 Snowflake(雪花算法)的唯一 ID 生成器。 - -IdGenerator 有如下特点: - -- 生成的唯一 ID 更短; -- 兼容所有雪花算法(号段模式或经典模式,大厂或小厂); -- 原生支持 C#/Java/Go/C/Rust/Python/Node.js/PHP(C 扩展)/SQL/ 等语言,并提供多线程安全调用动态库(FFI); -- 解决了时间回拨问题,支持手工插入新 ID(当业务需要在历史时间生成新 ID 时,用本算法的预留位能生成 5000 个每秒); -- 不依赖外部存储系统; -- 默认配置下,ID 可用 71000 年不重复。 - -IdGenerator 生成的唯一 ID 组成如下: - -![IdGenerator 生成的 ID 组成](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/idgenerator-distributed-id-schematic-diagram.png) - -- **timestamp (位数不固定)**:时间差,是生成 ID 时的系统时间减去 BaseTime(基础时间,也称基点时间、原点时间、纪元时间,默认值为 2020 年) 的总时间差(毫秒单位)。初始为 5bits,随着运行时间而增加。如果觉得默认值太老,你可以重新设置,不过要注意,这个值以后最好不变。 -- **worker id (默认 6 bits)**:机器 id,机器码,最重要参数,是区分不同机器或不同应用的唯一 ID,最大值由 `WorkerIdBitLength`(默认 6)限定。如果一台服务器部署多个独立服务,需要为每个服务指定不同的 WorkerId。 -- **sequence (默认 6 bits)**:序列数,是每毫秒下的序列数,由参数中的 `SeqBitLength`(默认 6)限定。增加 `SeqBitLength` 会让性能更高,但生成的 ID 也会更长。 - -Java 语言使用示例:。 - -## 总结 - -通过这篇文章,我基本上已经把最常见的分布式 ID 生成方案都总结了一波。 - -除了上面介绍的方式之外,像 ZooKeeper 这类中间件也可以帮助我们生成唯一 ID。**没有银弹,一定要结合实际项目来选择最适合自己的方案。** - -不过,本文主要介绍的是分布式 ID 的理论知识。在实际的面试中,面试官可能会结合具体的业务场景来考察你对分布式 ID 的设计,你可以参考这篇文章:[分布式 ID 设计指南](./distributed-id-design)(对于实际工作中分布式 ID 的设计也非常有帮助)。 - - +- **Advantages**: Simple to implement, IDs are ordered and incrementing, diff --git a/docs/distributed-system/distributed-lock-implementations.md b/docs/distributed-system/distributed-lock-implementations.md index cb4504c4a7a..fbb695d1020 100644 --- a/docs/distributed-system/distributed-lock-implementations.md +++ b/docs/distributed-system/distributed-lock-implementations.md @@ -1,19 +1,19 @@ --- -title: 分布式锁常见实现方案总结 -category: 分布式 +title: Summary of Common Distributed Lock Implementation Solutions +category: Distributed --- -通常情况下,我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁,Redis 用的要更多一点,我这里也先以 Redis 为例介绍分布式锁的实现。 +In general, we usually choose to implement distributed locks based on Redis or ZooKeeper, with Redis being the more commonly used option. Here, I will introduce the implementation of distributed locks using Redis as an example. -## 基于 Redis 实现分布式锁 +## Implementing Distributed Locks Based on Redis -### 如何基于 Redis 实现一个最简易的分布式锁? +### How to Implement a Simple Distributed Lock Using Redis? -不论是本地锁还是分布式锁,核心都在于“互斥”。 +Whether it's a local lock or a distributed lock, the core concept is "mutual exclusion." -在 Redis 中, `SETNX` 命令是可以帮助我们实现互斥。`SETNX` 即 **SET** if **N**ot e**X**ists (对应 Java 中的 `setIfAbsent` 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, `SETNX` 啥也不做。 +In Redis, the `SETNX` command helps us achieve mutual exclusion. `SETNX` stands for **SET** if **N**ot e**X**ists (corresponding to the `setIfAbsent` method in Java). It sets the key's value only if the key does not exist. If the key already exists, `SETNX` does nothing. ```bash > SETNX lockKey uniqueValue @@ -22,19 +22,19 @@ category: 分布式 (integer) 0 ``` -释放锁的话,直接通过 `DEL` 命令删除对应的 key 即可。 +To release the lock, simply delete the corresponding key using the `DEL` command. ```bash > DEL lockKey (integer) 1 ``` -为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。 +To prevent accidentally deleting other locks, it is recommended to use a Lua script to check the value (unique value) corresponding to the key. -选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。 +Using a Lua script ensures the atomicity of the unlock operation. When Redis executes a Lua script, it does so atomically, thus guaranteeing the atomicity of the lock release operation. ```lua -// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放 +// When releasing the lock, first compare the value of the lock to avoid accidental release if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else @@ -42,340 +42,40 @@ else end ``` -![Redis 实现简易分布式锁](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-setnx.png) +![Simple Distributed Lock Implementation with Redis](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-setnx.png) -这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。 +This is a simple implementation of a Redis distributed lock, which is straightforward and efficient. However, this method has some issues. For example, if the application encounters a problem and the logic for releasing the lock suddenly crashes, it may lead to the lock not being released, causing shared resources to be inaccessible to other threads/processes. -### 为什么要给锁设置一个过期时间? +### Why Set an Expiration Time for the Lock? -为了避免锁无法被释放,我们可以想到的一个解决办法就是:**给这个 key(也就是锁) 设置一个过期时间** 。 +To avoid the lock being unreleased, one solution is to **set an expiration time for this key (the lock)**. ```bash 127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX OK ``` -- **lockKey**:加锁的锁名; -- **uniqueValue**:能够唯一标识锁的随机字符串; -- **NX**:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功; -- **EX**:过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。 +- **lockKey**: The name of the lock; +- **uniqueValue**: A random string that uniquely identifies the lock; +- **NX**: The SET operation will only succeed if the key corresponding to lockKey does not exist; +- **EX**: Sets the expiration time (in seconds). EX 3 indicates that this lock has an automatic expiration time of 3 seconds. The counterpart to EX is PX (in milliseconds), and both are used for setting expiration times. -**一定要保证设置指定 key 的值和过期时间是一个原子操作!!!** 不然的话,依然可能会出现锁无法被释放的问题。 +**It is crucial to ensure that setting the specified key's value and expiration time is an atomic operation!!!** Otherwise, the issue of the lock not being released may still occur. -这样确实可以解决问题,不过,这种解决办法同样存在漏洞:**如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。** +This indeed solves the problem, but this solution also has vulnerabilities: **If the time taken to operate on the shared resource exceeds the expiration time, the lock may expire prematurely, leading to the distributed lock becoming invalid. If the timeout is set too long, it may affect performance.** -你或许在想:**如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了!** +You might be wondering: **If the operation on the shared resource is not yet complete, it would be great if the lock's expiration time could be renewed automatically!** -### 如何实现锁的优雅续期? +### How to Implement Elegant Lock Renewal? -对于 Java 开发的小伙伴来说,已经有了现成的解决方案:**[Redisson](https://github.com/redisson/redisson)** 。其他语言的解决方案,可以在 Redis 官方文档中找到,地址: 。 +For Java developers, there is already a ready-made solution: **[Redisson](https://github.com/redisson/redisson)**. Solutions for other languages can be found in the official Redis documentation at: . ![Distributed locks with Redis](https://oss.javaguide.cn/github/javaguide/redis-distributed-lock.png) -Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。并且,Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。 +Redisson is an open-source Redis client for Java that provides many out-of-the-box features, including various implementations of distributed locks. Additionally, Redisson supports multiple deployment architectures such as Redis standalone, Redis Sentinel, and Redis Cluster. -Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 **Watch Dog( 看门狗)**,如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。 +The distributed lock in Redisson comes with an automatic renewal mechanism, making it very easy to use. The principle is quite simple: it provides a dedicated **Watch Dog** to monitor and renew the lock. If the thread operating on the shared resource has not completed its execution, the Watch Dog will continuously extend the lock's expiration time, ensuring that the lock is not released due to timeout. -![Redisson 看门狗自动续期](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-redisson-renew-expiration.png) +![Redisson Watch Dog Automatic Renewal](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-redisson-renew-expiration.png) -看门狗名字的由来于 `getLockWatchdogTimeout()` 方法,这个方法返回的是看门狗给锁续期的过期时间,默认为 30 秒([redisson-3.17.6](https://github.com/redisson/redisson/releases/tag/redisson-3.17.6))。 - -```java -//默认 30秒,支持修改 -private long lockWatchdogTimeout = 30 * 1000; - -public Config setLockWatchdogTimeout(long lockWatchdogTimeout) { - this.lockWatchdogTimeout = lockWatchdogTimeout; - return this; -} -public long getLockWatchdogTimeout() { - return lockWatchdogTimeout; -} -``` - -`renewExpiration()` 方法包含了看门狗的主要逻辑: - -```java -private void renewExpiration() { - //...... - Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { - @Override - public void run(Timeout timeout) throws Exception { - //...... - // 异步续期,基于 Lua 脚本 - CompletionStage future = renewExpirationAsync(threadId); - future.whenComplete((res, e) -> { - if (e != null) { - // 无法续期 - log.error("Can't update lock " + getRawName() + " expiration", e); - EXPIRATION_RENEWAL_MAP.remove(getEntryName()); - return; - } - - if (res) { - // 递归调用实现续期 - renewExpiration(); - } else { - // 取消续期 - cancelExpirationRenewal(null); - } - }); - } - // 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用 - }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); - - ee.setTimeout(task); - } -``` - -默认情况下,每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。看门狗续期前也会先判断是否需要执行续期操作,需要才会执行续期,否则取消续期操作。 - -Watch Dog 通过调用 `renewExpirationAsync()` 方法实现锁的异步续期: - -```java -protected CompletionStage renewExpirationAsync(long threadId) { - return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, - // 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认) - "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + - "redis.call('pexpire', KEYS[1], ARGV[1]); " + - "return 1; " + - "end; " + - "return 0;", - Collections.singletonList(getRawName()), - internalLockLeaseTime, getLockName(threadId)); -} -``` - -可以看出, `renewExpirationAsync` 方法其实是调用 Lua 脚本实现的续期,这样做主要是为了保证续期操作的原子性。 - -我这里以 Redisson 的分布式可重入锁 `RLock` 为例来说明如何使用 Redisson 实现分布式锁: - -```java -// 1.获取指定的分布式锁对象 -RLock lock = redisson.getLock("lock"); -// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制 -lock.lock(); -// 3.执行业务 -... -// 4.释放锁 -lock.unlock(); -``` - -只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。 - -```java -// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制 -lock.lock(10, TimeUnit.SECONDS); -``` - -如果使用 Redis 来实现分布式锁的话,还是比较推荐直接基于 Redisson 来做的。 - -### 如何实现可重入锁? - -所谓可重入锁指的是在一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法即可重入 ,而无需重新获得锁。像 Java 中的 `synchronized` 和 `ReentrantLock` 都属于可重入锁。 - -**不可重入的分布式锁基本可以满足绝大部分业务场景了,一些特殊的场景可能会需要使用可重入的分布式锁。** - -可重入分布式锁的实现核心思路是线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。为此,我们可以为每个锁关联一个可重入计数器和一个占有它的线程。当可重入计数器大于 0 时,则锁被占有,需要判断占有该锁的线程和请求获取锁的线程是否为同一个。 - -实际项目中,我们不需要自己手动实现,推荐使用我们上面提到的 **Redisson** ,其内置了多种类型的锁比如可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)。 - -![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/redisson-readme-locks.png) - -### Redis 如何解决集群情况下分布式锁的可靠性? - -为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。 - -Redis 集群下,上面介绍到的分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。 - -![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/redis-master-slave-distributed-lock.png) - -针对这个问题,Redis 之父 antirez 设计了 [Redlock 算法](https://redis.io/topics/distlock) 来解决。 - -![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-redis.io-realock.png) - -Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。 - -即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。 - -Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。 - -Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。《数据密集型应用系统设计》一书的作者 Martin Kleppmann 曾经专门发文([How to do distributed locking - Martin Kleppmann - 2016](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html))怼过 Redlock,他认为这是一个很差的分布式锁实现。感兴趣的朋友可以看看[Redis 锁从面试连环炮聊到神仙打架](https://mp.weixin.qq.com/s?__biz=Mzg3NjU3NTkwMQ==&mid=2247505097&idx=1&sn=5c03cb769c4458350f4d4a321ad51f5a&source=41#wechat_redirect)这篇文章,有详细介绍到 antirez 和 Martin Kleppmann 关于 Redlock 的激烈辩论。 - -实际项目中不建议使用 Redlock 算法,成本和收益不成正比,可以考虑基于 Redis 主从复制+哨兵模式实现分布式锁。 - -## 基于 ZooKeeper 实现分布式锁 - -ZooKeeper 相比于 Redis 实现分布式锁,除了提供相对更高的可靠性之外,在功能层面还有一个非常有用的特性:**Watch 机制**。这个机制可以用来实现公平的分布式锁。不过,使用 ZooKeeper 实现的分布式锁在性能方面相对较差,因此如果对性能要求比较高的话,ZooKeeper 可能就不太适合了。 - -### 如何基于 ZooKeeper 实现分布式锁? - -ZooKeeper 分布式锁是基于 **临时顺序节点** 和 **Watcher(事件监听器)** 实现的。 - -获取锁: - -1. 首先我们要有一个持久节点`/locks`,客户端获取锁就是在`locks`下创建临时顺序节点。 -2. 假设客户端 1 创建了`/locks/lock1`节点,创建成功之后,会判断 `lock1`是否是 `/locks` 下最小的子节点。 -3. 如果 `lock1`是最小的子节点,则获取锁成功。否则,获取锁失败。 -4. 如果获取锁失败,则说明有其他的客户端已经成功获取锁。客户端 1 并不会不停地循环去尝试加锁,而是在前一个节点比如`/locks/lock0`上注册一个事件监听器。这个监听器的作用是当前一个节点释放锁之后通知客户端 1(避免无效自旋),这样客户端 1 就加锁成功了。 - -释放锁: - -1. 成功获取锁的客户端在执行完业务流程之后,会将对应的子节点删除。 -2. 成功获取锁的客户端在出现故障之后,对应的子节点由于是临时顺序节点,也会被自动删除,避免了锁无法被释放。 -3. 我们前面说的事件监听器其实监听的就是这个子节点删除事件,子节点删除就意味着锁被释放。 - -![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-zookeeper.png) - -实际项目中,推荐使用 Curator 来实现 ZooKeeper 分布式锁。Curator 是 Netflix 公司开源的一套 ZooKeeper Java 客户端框架,相比于 ZooKeeper 自带的客户端 zookeeper 来说,Curator 的封装更加完善,各种 API 都可以比较方便地使用。 - -`Curator`主要实现了下面四种锁: - -- `InterProcessMutex`:分布式可重入排它锁 -- `InterProcessSemaphoreMutex`:分布式不可重入排它锁 -- `InterProcessReadWriteLock`:分布式读写锁 -- `InterProcessMultiLock`:将多个锁作为单个实体管理的容器,获取锁的时候获取所有锁,释放锁也会释放所有锁资源(忽略释放失败的锁)。 - -```java -CuratorFramework client = ZKUtils.getClient(); -client.start(); -// 分布式可重入排它锁 -InterProcessLock lock1 = new InterProcessMutex(client, lockPath1); -// 分布式不可重入排它锁 -InterProcessLock lock2 = new InterProcessSemaphoreMutex(client, lockPath2); -// 将多个锁作为一个整体 -InterProcessMultiLock lock = new InterProcessMultiLock(Arrays.asList(lock1, lock2)); - -if (!lock.acquire(10, TimeUnit.SECONDS)) { - throw new IllegalStateException("不能获取多锁"); -} -System.out.println("已获取多锁"); -System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess()); -System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess()); -try { - // 资源操作 - resource.use(); -} finally { - System.out.println("释放多个锁"); - lock.release(); -} -System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess()); -System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess()); -client.close(); -``` - -### 为什么要用临时顺序节点? - -每个数据节点在 ZooKeeper 中被称为 **znode**,它是 ZooKeeper 中数据的最小单元。 - -我们通常是将 znode 分为 4 大类: - -- **持久(PERSISTENT)节点**:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。 -- **临时(EPHEMERAL)节点**:临时节点的生命周期是与 **客户端会话(session)** 绑定的,**会话消失则节点消失** 。并且,**临时节点只能做叶子节点** ,不能创建子节点。 -- **持久顺序(PERSISTENT_SEQUENTIAL)节点**:除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 `/node1/app0000000001`、`/node1/app0000000002` 。 -- **临时顺序(EPHEMERAL_SEQUENTIAL)节点**:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。 - -可以看出,临时节点相比持久节点,最主要的是对会话失效的情况处理不一样,临时节点会话消失则对应的节点消失。这样的话,如果客户端发生异常导致没来得及释放锁也没关系,会话失效节点自动被删除,不会发生死锁的问题。 - -使用 Redis 实现分布式锁的时候,我们是通过过期时间来避免锁无法被释放导致死锁问题的,而 ZooKeeper 直接利用临时节点的特性即可。 - -假设不使用顺序节点的话,所有尝试获取锁的客户端都会对持有锁的子节点加监听器。当该锁被释放之后,势必会造成所有尝试获取锁的客户端来争夺锁,这样对性能不友好。使用顺序节点之后,只需要监听前一个节点就好了,对性能更友好。 - -### 为什么要设置对前一个节点的监听? - -> Watcher(事件监听器),是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。 - -同一时间段内,可能会有很多客户端同时获取锁,但只有一个可以获取成功。如果获取锁失败,则说明有其他的客户端已经成功获取锁。获取锁失败的客户端并不会不停地循环去尝试加锁,而是在前一个节点注册一个事件监听器。 - -这个事件监听器的作用是:**当前一个节点对应的客户端释放锁之后(也就是前一个节点被删除之后,监听的是删除事件),通知获取锁失败的客户端(唤醒等待的线程,Java 中的 `wait/notifyAll` ),让它尝试去获取锁,然后就成功获取锁了。** - -### 如何实现可重入锁? - -这里以 Curator 的 `InterProcessMutex` 对可重入锁的实现来介绍(源码地址:[InterProcessMutex.java](https://github.com/apache/curator/blob/master/curator-recipes/src/main/java/org/apache/curator/framework/recipes/locks/InterProcessMutex.java))。 - -当我们调用 `InterProcessMutex#acquire`方法获取锁的时候,会调用`InterProcessMutex#internalLock`方法。 - -```java -// 获取可重入互斥锁,直到获取成功为止 -@Override -public void acquire() throws Exception { - if (!internalLock(-1, null)) { - throw new IOException("Lost connection while trying to acquire lock: " + basePath); - } -} -``` - -`internalLock` 方法会先获取当前请求锁的线程,然后从 `threadData`( `ConcurrentMap` 类型)中获取当前线程对应的 `lockData` 。 `lockData` 包含锁的信息和加锁的次数,是实现可重入锁的关键。 - -第一次获取锁的时候,`lockData`为 `null`。获取锁成功之后,会将当前线程和对应的 `lockData` 放到 `threadData` 中 - -```java -private boolean internalLock(long time, TimeUnit unit) throws Exception { - // 获取当前请求锁的线程 - Thread currentThread = Thread.currentThread(); - // 拿对应的 lockData - LockData lockData = threadData.get(currentThread); - // 第一次获取锁的话,lockData 为 null - if (lockData != null) { - // 当前线程获取过一次锁之后 - // 因为当前线程的锁存在, lockCount 自增后返回,实现锁重入. - lockData.lockCount.incrementAndGet(); - return true; - } - // 尝试获取锁 - String lockPath = internals.attemptLock(time, unit, getLockNodeBytes()); - if (lockPath != null) { - LockData newLockData = new LockData(currentThread, lockPath); - // 获取锁成功之后,将当前线程和对应的 lockData 放到 threadData 中 - threadData.put(currentThread, newLockData); - return true; - } - - return false; -} -``` - -`LockData`是 `InterProcessMutex`中的一个静态内部类。 - -```java -private final ConcurrentMap threadData = Maps.newConcurrentMap(); - -private static class LockData -{ - // 当前持有锁的线程 - final Thread owningThread; - // 锁对应的子节点 - final String lockPath; - // 加锁的次数 - final AtomicInteger lockCount = new AtomicInteger(1); - - private LockData(Thread owningThread, String lockPath) - { - this.owningThread = owningThread; - this.lockPath = lockPath; - } -} -``` - -如果已经获取过一次锁,后面再来获取锁的话,直接就会在 `if (lockData != null)` 这里被拦下了,然后就会执行`lockData.lockCount.incrementAndGet();` 将加锁次数加 1。 - -整个可重入锁的实现逻辑非常简单,直接在客户端判断当前线程有没有获取锁,有的话直接将加锁次数加 1 就可以了。 - -## 总结 - -在这篇文章中,我介绍了实现分布式锁的两种常见方式:**Redis** 和 **ZooKeeper**。至于具体选择 Redis 还是 ZooKeeper 来实现分布式锁,还是要根据业务的具体需求来决定。 - -- 如果对性能要求比较高的话,建议使用 Redis 实现分布式锁。推荐优先选择 **Redisson** 提供的现成分布式锁,而不是自己实现。实际项目中不建议使用 Redlock 算法,成本和收益不成正比,可以考虑基于 Redis 主从复制+哨兵模式实现分布式锁。 -- 如果对可靠性要求比较高,建议使用 ZooKeeper 实现分布式锁,推荐基于 **Curator** 框架来实现。不过,现在很多项目都不会用到 ZooKeeper,如果单纯是因为分布式锁而引入 ZooKeeper 的话,那是不太可取的,不建议这样做,为了一个小小的功能增加了系统的复杂度。 - -需要注意的是,无论选择哪种方式实现分布式锁,包括 Redis、ZooKeeper 或 Etcd(本文没介绍,但也经常用来实现分布式锁),都无法保证 100% 的安全性,特别是在遇到进程垃圾回收(GC)、网络延迟等异常情况下。 - -为了进一步提高系统的可靠性,建议引入一个兜底机制。例如,可以通过 **版本号(Fencing Token)机制** 来避免并发冲突。 - -最后,再分享几篇我觉得写的还不错的文章: - -- [分布式锁实现原理与最佳实践 - 阿里云开发者](https://mp.weixin.qq.com/s/JzCHpIOiFVmBoAko58ZuGw) -- [聊聊分布式锁 - 字节跳动技术团队](https://mp.weixin.qq.com/s/-N4x6EkxwAYDGdJhwvmZLw) -- [Redis、ZooKeeper、Etcd,谁有最好用的分布式锁? - 腾讯云开发者](https://mp.weixin.qq.com/s/yZC6VJGxt1ANZkn0SljZBg) - - +The name "Watch Dog" comes from the \`getLockWatchdog diff --git a/docs/distributed-system/distributed-lock.md b/docs/distributed-system/distributed-lock.md index ba53f443d03..9d6403b1f0e 100644 --- a/docs/distributed-system/distributed-lock.md +++ b/docs/distributed-system/distributed-lock.md @@ -1,84 +1,56 @@ --- -title: 分布式锁介绍 -category: 分布式 +title: Introduction to Distributed Locks +category: Distributed --- -网上有很多分布式锁相关的文章,写了一个相对简洁易懂的版本,针对面试和工作应该够用了。 +There are many articles online about distributed locks, and I have written a relatively concise and easy-to-understand version that should be sufficient for interviews and work. -这篇文章我们先介绍一下分布式锁的基本概念。 +In this article, we will first introduce the basic concepts of distributed locks. -## 为什么需要分布式锁? +## Why Do We Need Distributed Locks? -在多线程环境中,如果多个线程同时访问共享资源(例如商品库存、外卖订单),会发生数据竞争,可能会导致出现脏数据或者系统问题,威胁到程序的正常运行。 +In a multithreaded environment, if multiple threads access shared resources (such as product inventory or food delivery orders) simultaneously, data contention can occur, potentially leading to dirty data or system issues, threatening the normal operation of the program. -举个例子,假设现在有 100 个用户参与某个限时秒杀活动,每位用户限购 1 件商品,且商品的数量只有 3 个。如果不对共享资源进行互斥访问,就可能出现以下情况: +For example, suppose there are 100 users participating in a limited-time flash sale, with each user allowed to purchase 1 item, and there are only 3 items available. If mutual exclusion is not applied to the shared resource, the following situation may occur: -- 线程 1、2、3 等多个线程同时进入抢购方法,每一个线程对应一个用户。 -- 线程 1 查询用户已经抢购的数量,发现当前用户尚未抢购且商品库存还有 1 个,因此认为可以继续执行抢购流程。 -- 线程 2 也执行查询用户已经抢购的数量,发现当前用户尚未抢购且商品库存还有 1 个,因此认为可以继续执行抢购流程。 -- 线程 1 继续执行,将库存数量减少 1 个,然后返回成功。 -- 线程 2 继续执行,将库存数量减少 1 个,然后返回成功。 -- 此时就发生了超卖问题,导致商品被多卖了一份。 +- Threads 1, 2, 3, etc., enter the purchase method simultaneously, with each thread corresponding to a user. +- Thread 1 queries the number of items the user has already purchased, finds that the current user has not yet purchased and that there is still 1 item in stock, and thus believes it can continue the purchase process. +- Thread 2 also executes the query for the number of items the user has already purchased, finds that the current user has not yet purchased and that there is still 1 item in stock, and thus believes it can continue the purchase process. +- Thread 1 continues to execute, reducing the stock quantity by 1, and then returns success. +- Thread 2 continues to execute, reducing the stock quantity by 1, and then returns success. +- At this point, an overselling issue occurs, resulting in the product being sold more than once. -![共享资源未互斥访问导致出现问题](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/oversold-without-locking.png) +![Issues caused by non-mutual access to shared resources](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/oversold-without-locking.png) -为了保证共享资源被安全地访问,我们需要使用互斥操作对共享资源进行保护,即同一时刻只允许一个线程访问共享资源,其他线程需要等待当前线程释放后才能访问。这样可以避免数据竞争和脏数据问题,保证程序的正确性和稳定性。 +To ensure that shared resources are accessed safely, we need to use mutual exclusion operations to protect shared resources, allowing only one thread to access the shared resource at any given time, while other threads must wait until the current thread releases it. This can avoid data contention and dirty data issues, ensuring the correctness and stability of the program. -**如何才能实现共享资源的互斥访问呢?** 锁是一个比较通用的解决方案,更准确点来说是悲观锁。 +**How can we achieve mutual access to shared resources?** Locks are a relatively general solution, more specifically, pessimistic locks. -悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。 +Pessimistic locks always assume the worst-case scenario, believing that issues will arise every time shared resources are accessed (for example, shared data being modified). Therefore, every time a resource operation is performed, a lock is acquired, causing other threads that want to access this resource to block until the lock is released by the previous holder. In other words, **the shared resource is used by only one thread at a time, while other threads are blocked, and after use, the resource is transferred to other threads**. -对于单机多线程来说,在 Java 中,我们通常使用 `ReentrantLock` 类、`synchronized` 关键字这类 JDK 自带的 **本地锁** 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。 +For single-machine multithreading, in Java, we typically use the `ReentrantLock` class or the `synchronized` keyword, which are built-in **local locks** provided by the JDK, to control access to local shared resources by multiple threads within a single JVM process. -下面是我对本地锁画的一张示意图。 +Here is a diagram I created to illustrate local locks. -![本地锁](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/jvm-local-lock.png) +![Local Lock](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/jvm-local-lock.png) -从图中可以看出,这些线程访问共享资源是互斥的,同一时刻只有一个线程可以获取到本地锁访问共享资源。 +From the diagram, it can be seen that these threads access shared resources mutually exclusively, with only one thread able to acquire the local lock to access the shared resource at any given time. -分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,**分布式锁** 就诞生了。 +In a distributed system, different services/clients typically run in separate JVM processes. If multiple JVM processes share the same resource, local locks cannot achieve mutual access to the resource. Thus, **distributed locks** were born. -举个例子:系统的订单服务一共部署了 3 份,都对外提供服务。用户下订单之前需要检查库存,为了防止超卖,这里需要加锁以实现对检查库存操作的同步访问。由于订单服务位于不同的 JVM 进程中,本地锁在这种情况下就没办法正常工作了。我们需要用到分布式锁,这样的话,即使多个线程不在同一个 JVM 进程中也能获取到同一把锁,进而实现共享资源的互斥访问。 +For example: The order service of the system is deployed in 3 instances, all providing services externally. Before a user places an order, they need to check the inventory. To prevent overselling, a lock is needed to synchronize access to the inventory check operation. Since the order service is located in different JVM processes, local locks cannot function properly in this case. We need to use distributed locks, which allow multiple threads, even if they are not in the same JVM process, to acquire the same lock, thereby achieving mutual access to shared resources. -下面是我对分布式锁画的一张示意图。 +Here is a diagram I created to illustrate distributed locks. -![分布式锁](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock.png) +![Distributed Lock](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock.png) -从图中可以看出,这些独立的进程中的线程访问共享资源是互斥的,同一时刻只有一个线程可以获取到分布式锁访问共享资源。 +From the diagram, it can be seen that threads in these independent processes access shared resources mutually exclusively, with only one thread able to acquire the distributed lock to access the shared resource at any given time. -## 分布式锁应该具备哪些条件? +## What Conditions Should a Distributed Lock Meet? -一个最基本的分布式锁需要满足: +A basic distributed lock needs to satisfy the following: -- **互斥**:任意一个时刻,锁只能被一个线程持有。 -- **高可用**:锁服务是高可用的,当一个锁服务出现问题,能够自动切换到另外一个锁服务。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。这一般是通过超时机制实现的。 -- **可重入**:一个节点获取了锁之后,还可以再次获取锁。 - -除了上面这三个基本条件之外,一个好的分布式锁还需要满足下面这些条件: - -- **高性能**:获取和释放锁的操作应该快速完成,并且不应该对整个系统的性能造成过大影响。 -- **非阻塞**:如果获取不到锁,不能无限期等待,避免对系统正常运行造成影响。 - -## 分布式锁的常见实现方式有哪些? - -常见分布式锁实现方案如下: - -- 基于关系型数据库比如 MySQL 实现分布式锁。 -- 基于分布式协调服务 ZooKeeper 实现分布式锁。 -- 基于分布式键值存储系统比如 Redis 、Etcd 实现分布式锁。 - -关系型数据库的方式一般是通过唯一索引或者排他锁实现。不过,一般不会使用这种方式,问题太多比如性能太差、不具备锁失效机制。 - -基于 ZooKeeper 或者 Redis 实现分布式锁这两种实现方式要用的更多一些,我专门写了一篇文章来详细介绍这两种方案:[分布式锁常见实现方案总结](./distributed-lock-implementations.md)。 - -## 总结 - -这篇文章我们主要介绍了: - -- 分布式锁的用途:分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。 -- 分布式锁的应该具备的条件:互斥、高可用、可重入、高性能、非阻塞。 -- 分布式锁的常见实现方式:关系型数据库比如 MySQL、分布式协调服务 ZooKeeper、分布式键值存储系统比如 Redis 、Etcd 。 - - +- **Mutual Exclusion**: At any given time, the lock can only be held by one thread. +- **High Availability**: The lock service is highly available. When one diff --git a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.md b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.md index af6f3de5a21..e9065b67c9f 100644 --- a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.md +++ b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.md @@ -1,76 +1,76 @@ --- -title: ZooKeeper 实战 -category: 分布式 +title: ZooKeeper Practical Guide +category: Distributed tag: - ZooKeeper --- -这篇文章简单给演示一下 ZooKeeper 常见命令的使用以及 ZooKeeper Java 客户端 Curator 的基本使用。介绍到的内容都是最基本的操作,能满足日常工作的基本需要。 +This article will briefly demonstrate the use of common ZooKeeper commands and the basic usage of the ZooKeeper Java client, Curator. The topics covered are the most basic operations and can meet the basic needs of daily work. -如果文章有任何需要改善和完善的地方,欢迎在评论区指出,共同进步! +If there are any areas in the article that need improvement or enhancement, feel free to point them out in the comments section for our mutual progress! -## ZooKeeper 安装 +## ZooKeeper Installation -### 使用 Docker 安装 zookeeper +### Installing ZooKeeper Using Docker -**a.使用 Docker 下载 ZooKeeper** +**a. Download ZooKeeper using Docker** ```shell docker pull zookeeper:3.5.8 ``` -**b.运行 ZooKeeper** +**b. Run ZooKeeper** ```shell docker run -d --name zookeeper -p 2181:2181 zookeeper:3.5.8 ``` -### 连接 ZooKeeper 服务 +### Connecting to the ZooKeeper Service -**a.进入 ZooKeeper 容器中** +**a. Enter the ZooKeeper container** -先使用 `docker ps` 查看 ZooKeeper 的 ContainerID,然后使用 `docker exec -it ContainerID /bin/bash` 命令进入容器中。 +First, use `docker ps` to check the ContainerID of ZooKeeper, and then use the command `docker exec -it ContainerID /bin/bash` to enter the container. -**b.先进入 bin 目录,然后通过 `./zkCli.sh -server 127.0.0.1:2181`命令连接 ZooKeeper 服务** +**b. Navigate to the bin directory and connect to the ZooKeeper service using the command `./zkCli.sh -server 127.0.0.1:2181`** ```bash root@eaf70fc620cb:/apache-zookeeper-3.5.8-bin# cd bin ``` -如果你看到控制台成功打印出如下信息的话,说明你已经成功连接 ZooKeeper 服务。 +If you see the following message successfully printed in the console, it indicates that you have successfully connected to the ZooKeeper service. -![连接 ZooKeeper 服务](https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/connect-zooKeeper-service.png) +![Connect to ZooKeeper Service](https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/connect-zooKeeper-service.png) -## ZooKeeper 常用命令演示 +## Common ZooKeeper Commands Demonstration -### 查看常用命令(help 命令) +### View Common Commands (help Command) -通过 `help` 命令查看 ZooKeeper 常用命令 +Use the `help` command to view common ZooKeeper commands. -### 创建节点(create 命令) +### Create Node (create Command) -通过 `create` 命令在根目录创建了 node1 节点,与它关联的字符串是"node1" +Use the `create` command to create the node1 in the root directory, with the associated string "node1". ```shell -[zk: 127.0.0.1:2181(CONNECTED) 34] create /node1 “node1” +[zk: 127.0.0.1:2181(CONNECTED) 34] create /node1 "node1" ``` -通过 `create` 命令在根目录创建了 node1 节点,与它关联的内容是数字 123 +Use the `create` command to create the node1.1 under node1, with the associated content as the number 123. ```shell [zk: 127.0.0.1:2181(CONNECTED) 1] create /node1/node1.1 123 Created /node1/node1.1 ``` -### 更新节点数据内容(set 命令) +### Update Node Data Content (set Command) ```shell [zk: 127.0.0.1:2181(CONNECTED) 11] set /node1 "set node1" ``` -### 获取节点的数据(get 命令) +### Retrieve Node Data (get Command) -`get` 命令可以获取指定节点的数据内容和节点的状态,可以看出我们通过 `set` 命令已经将节点数据内容改为 "set node1"。 +The `get` command can retrieve the data content and status of the specified node, showing that we have already changed the node data to "set node1" using the `set` command. ```shell [zk: zookeeper(CONNECTED) 12] get -s /node1 @@ -86,30 +86,29 @@ aclVersion = 0 ephemeralOwner = 0x0 dataLength = 9 numChildren = 1 - ``` -### 查看某个目录下的子节点(ls 命令) +### View Subnodes of a Directory (ls Command) -通过 `ls` 命令查看根目录下的节点 +Use the `ls` command to view the nodes in the root directory. ```shell [zk: 127.0.0.1:2181(CONNECTED) 37] ls / [dubbo, ZooKeeper, node1] ``` -通过 `ls` 命令查看 node1 目录下的节点 +Use the `ls` command to view the nodes under the node1 directory. ```shell [zk: 127.0.0.1:2181(CONNECTED) 5] ls /node1 [node1.1] ``` -ZooKeeper 中的 ls 命令和 linux 命令中的 ls 类似, 这个命令将列出绝对路径 path 下的所有子节点信息(列出 1 级,并不递归) +The `ls` command in ZooKeeper is similar to the `ls` command in Linux; this command will list all subnode information under the absolute path (only listing 1 level, not recursively). -### 查看节点状态(stat 命令) +### View Node Status (stat Command) -通过 `stat` 命令查看节点状态 +Use the `stat` command to view the node status. ```shell [zk: 127.0.0.1:2181(CONNECTED) 10] stat /node1 @@ -126,14 +125,14 @@ dataLength = 11 numChildren = 1 ``` -上面显示的一些信息比如 cversion、aclVersion、numChildren 等等,我在上面 “[ZooKeeper 相关概念总结(入门)](https://javaguide.cn/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.html)” 这篇文章中已经介绍到。 +The information displayed above, such as cversion, aclVersion, numChildren, etc., has been introduced in my previous article “[Summary of ZooKeeper Related Concepts (Beginner)](https://javaguide.cn/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.html)”. -### 查看节点信息和状态(ls2 命令) +### View Node Information and Status (ls2 Command) -`ls2` 命令更像是 `ls` 命令和 `stat` 命令的结合。 `ls2` 命令返回的信息包括 2 部分: +The `ls2` command is more like a combination of the `ls` and `stat` commands. The information returned by the `ls2` command includes 2 parts: -1. 子节点列表 -2. 当前节点的 stat 信息。 +1. List of subnodes +1. The current node's stat information. ```shell [zk: 127.0.0.1:2181(CONNECTED) 7] ls2 /node1 @@ -149,28 +148,27 @@ aclVersion = 0 ephemeralOwner = 0x0 dataLength = 11 numChildren = 1 - ``` -### 删除节点(delete 命令) +### Delete Node (delete Command) -这个命令很简单,但是需要注意的一点是如果你要删除某一个节点,那么这个节点必须无子节点才行。 +This command is straightforward, but it is crucial to note that if you want to delete a node, that node must have no children. ```shell [zk: 127.0.0.1:2181(CONNECTED) 3] delete /node1/node1.1 ``` -在后面我会介绍到 Java 客户端 API 的使用以及开源 ZooKeeper 客户端 ZkClient 和 Curator 的使用。 +Later, I will introduce the use of the Java client API and the open-source ZooKeeper clients ZkClient and Curator. -## ZooKeeper Java 客户端 Curator 简单使用 +## Simple Usage of ZooKeeper Java Client Curator -Curator 是 Netflix 公司开源的一套 ZooKeeper Java 客户端框架,相比于 Zookeeper 自带的客户端 zookeeper 来说,Curator 的封装更加完善,各种 API 都可以比较方便地使用。 +Curator is an open-source ZooKeeper Java client framework developed by Netflix. Compared to the built-in ZooKeeper client, Curator's encapsulation is more comprehensive, allowing various APIs to be used more conveniently. ![](https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/curator.png) -下面我们就来简单地演示一下 Curator 的使用吧! +Now, let's briefly demonstrate the usage of Curator! -Curator4.0+版本对 ZooKeeper 3.5.x 支持比较好。开始之前,请先将下面的依赖添加进你的项目。 +Curator version 4.0+ has good support for ZooKeeper 3.5.x. Before starting, please add the following dependencies to your project. ```xml @@ -185,9 +183,9 @@ Curator4.0+版本对 ZooKeeper 3.5.x 支持比较好。开始之前,请先将 ``` -### 连接 ZooKeeper 客户端 +### Connecting to ZooKeeper Client -通过 `CuratorFrameworkFactory` 创建 `CuratorFramework` 对象,然后再调用 `CuratorFramework` 对象的 `start()` 方法即可! +Create a `CuratorFramework` object through `CuratorFrameworkFactory`, and then call the `start()` method on the `CuratorFramework` object! ```java private static final int BASE_SLEEP_TIME = 1000; @@ -203,92 +201,92 @@ CuratorFramework zkClient = CuratorFrameworkFactory.builder() zkClient.start(); ``` -对于一些基本参数的说明: +Here are explanations of some basic parameters: -- `baseSleepTimeMs`:重试之间等待的初始时间 -- `maxRetries`:最大重试次数 -- `connectString`:要连接的服务器列表 -- `retryPolicy`:重试策略 +- `baseSleepTimeMs`: The initial time to wait between retries +- `maxRetries`: The maximum number of retries +- `connectString`: The list of servers to connect to +- `retryPolicy`: The retry policy -### 数据节点的增删改查 +### CRUD Operations on Data Nodes -#### 创建节点 +#### Create Node -我们在 [ZooKeeper 常见概念解读](./zookeeper-intro.md) 中介绍到,我们通常是将 znode 分为 4 大类: +As we discussed in [Common Concepts of ZooKeeper](./zookeeper-intro.md), we generally categorize znodes into 4 major types: -- **持久(PERSISTENT)节点**:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。 -- **临时(EPHEMERAL)节点**:临时节点的生命周期是与 **客户端会话(session)** 绑定的,**会话消失则节点消失** 。并且,临时节点 **只能做叶子节点** ,不能创建子节点。 -- **持久顺序(PERSISTENT_SEQUENTIAL)节点**:除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 `/node1/app0000000001`、`/node1/app0000000002` 。 -- **临时顺序(EPHEMERAL_SEQUENTIAL)节点**:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。 +- **Persistent (PERSISTENT) Nodes**: Once created, they always exist even if the ZooKeeper cluster crashes, until deleted. +- **Ephemeral (EPHEMERAL) Nodes**: The lifecycle of ephemeral nodes is bound to the **client session**; **the session disappears, and the node disappears**. Moreover, ephemeral nodes **can only be leaf nodes**, and cannot have child nodes. +- **Persistent Sequential (PERSISTENT_SEQUENTIAL) Nodes**: In addition to having the characteristics of persistent nodes, the names of child nodes also have order. For example, `/node1/app0000000001`, `/node1/app0000000002`. +- **Ephemeral Sequential (EPHEMERAL_SEQUENTIAL) Nodes**: In addition to having the characteristics of ephemeral nodes, the names of child nodes also have order. -你在使用的 ZooKeeper 的时候,会发现 `CreateMode` 类中实际有 7 种 znode 类型 ,但是用的最多的还是上面介绍的 4 种。 +When using ZooKeeper, you will find that the `CreateMode` class has a total of 7 types of znodes, but the most commonly used are the 4 types mentioned above. -**a.创建持久化节点** +**a. Create Persistent Node** -你可以通过下面两种方式创建持久化的节点。 +You can create persistent nodes in the following two ways. ```java -//注意:下面的代码会报错,下文说了具体原因 +// Note: The following code will cause an error; the specific reason will be explained later zkClient.create().forPath("/node1/00001"); zkClient.create().withMode(CreateMode.PERSISTENT).forPath("/node1/00002"); ``` -但是,你运行上面的代码会报错,这是因为的父节点`node1`还未创建。 +However, running the above code will raise an error because the parent node `node1` has not been created yet. -你可以先创建父节点 `node1` ,然后再执行上面的代码就不会报错了。 +You can first create the parent node `node1`, and then the above code will not cause an error. ```java zkClient.create().forPath("/node1"); ``` -更推荐的方式是通过下面这行代码, **`creatingParentsIfNeeded()` 可以保证父节点不存在的时候自动创建父节点,这是非常有用的。** +A more recommended way is to use the following line of code. **`creatingParentsIfNeeded()` ensures that if the parent node does not exist, it will be created automatically, which is very useful.** ```java zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath("/node1/00001"); ``` -**b.创建临时节点** +**b. Create Ephemeral Node** ```java zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001"); ``` -**c.创建节点并指定数据内容** +**c. Create Node and Specify Data Content** ```java zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001","java".getBytes()); -zkClient.getData().forPath("/node1/00001");//获取节点的数据内容,获取到的是 byte数组 +zkClient.getData().forPath("/node1/00001");// Retrieve the data content of the node; the result is a byte array ``` -**d.检测节点是否创建成功** +**d. Check if the Node Was Created Successfully** ```java -zkClient.checkExists().forPath("/node1/00001");//不为null的话,说明节点创建成功 +zkClient.checkExists().forPath("/node1/00001");// If not null, it indicates that the node was created successfully ``` -#### 删除节点 +#### Delete Node -**a.删除一个子节点** +**a. Delete a Child Node** ```java zkClient.delete().forPath("/node1/00001"); ``` -**b.删除一个节点以及其下的所有子节点** +**b. Delete a Node and All Its Children** ```java zkClient.delete().deletingChildrenIfNeeded().forPath("/node1"); ``` -#### 获取/更新节点数据内容 +#### Retrieve/Update Node Data Content ```java zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001","java".getBytes()); -zkClient.getData().forPath("/node1/00001");//获取节点的数据内容 -zkClient.setData().forPath("/node1/00001","c++".getBytes());//更新节点数据内容 +zkClient.getData().forPath("/node1/00001");// Retrieve the data content of the node +zkClient.setData().forPath("/node1/00001","c++".getBytes());// Update the data content of the node ``` -#### 获取某个节点的所有子节点路径 +#### Retrieve All Child Node Paths of a Specific Node ```java List childrenPaths = zkClient.getChildren().forPath("/node1"); diff --git a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md index 955c5d2813a..11bebd0d158 100644 --- a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md +++ b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md @@ -1,316 +1,52 @@ --- -title: ZooKeeper相关概念总结(入门) -category: 分布式 +title: Summary of ZooKeeper Related Concepts (Introduction) +category: Distributed tag: - ZooKeeper --- -相信大家对 ZooKeeper 应该不算陌生。但是你真的了解 ZooKeeper 到底有啥用不?如果别人/面试官让你给他讲讲对于 ZooKeeper 的认识,你能回答到什么地步呢? +I believe everyone is somewhat familiar with ZooKeeper. But do you really understand what ZooKeeper is used for? If someone, like an interviewer, asks you to explain your understanding of ZooKeeper, how far could you go in your answer? -拿我自己来说吧!我本人在大学曾经使用 Dubbo 来做分布式项目的时候,使用了 ZooKeeper 作为注册中心。为了保证分布式系统能够同步访问某个资源,我还使用 ZooKeeper 做过分布式锁。另外,我在学习 Kafka 的时候,知道 Kafka 很多功能的实现依赖了 ZooKeeper。 +Let me share my own experience! During my university days, I used Dubbo for a distributed project and employed ZooKeeper as the registration center. To ensure that the distributed system could synchronously access a certain resource, I also used ZooKeeper for distributed locking. Additionally, while learning Kafka, I realized that many of Kafka's functionalities depend on ZooKeeper. -前几天,总结项目经验的时候,我突然问自己 ZooKeeper 到底是个什么东西?想了半天,脑海中只是简单的能浮现出几句话: +A few days ago, while summarizing project experiences, I suddenly asked myself, what exactly is ZooKeeper? After thinking for a long time, I could only recall a few simple statements: -1. ZooKeeper 可以被用作注册中心、分布式锁; -2. ZooKeeper 是 Hadoop 生态系统的一员; -3. 构建 ZooKeeper 集群的时候,使用的服务器最好是奇数台。 +1. ZooKeeper can be used as a registration center and for distributed locks; +1. ZooKeeper is a part of the Hadoop ecosystem; +1. When building a ZooKeeper cluster, it is best to use an odd number of servers. -由此可见,我对于 ZooKeeper 的理解仅仅是停留在了表面。 +From this, it is clear that my understanding of ZooKeeper was merely superficial. -所以,通过本文,希望带大家稍微详细的了解一下 ZooKeeper 。如果没有学过 ZooKeeper ,那么本文将会是你进入 ZooKeeper 大门的垫脚砖。如果你已经接触过 ZooKeeper ,那么本文将带你回顾一下 ZooKeeper 的一些基础概念。 +Therefore, through this article, I hope to provide a more detailed understanding of ZooKeeper. If you have never learned about ZooKeeper, this article will serve as a stepping stone for you to enter the world of ZooKeeper. If you have already been exposed to ZooKeeper, this article will help you review some basic concepts. -另外,本文不光会涉及到 ZooKeeper 的一些概念,后面的文章会介绍到 ZooKeeper 常见命令的使用以及使用 Apache Curator 作为 ZooKeeper 的客户端。 +Moreover, this article will not only cover some concepts of ZooKeeper; subsequent articles will introduce common commands for ZooKeeper and the use of Apache Curator as a client for ZooKeeper. -_如果文章有任何需要改善和完善的地方,欢迎在评论区指出,共同进步!_ +_If there are any areas in the article that need improvement or enhancement, please feel free to point them out in the comments section for our mutual progress!_ -## ZooKeeper 介绍 +## Introduction to ZooKeeper -### ZooKeeper 由来 +### Origin of ZooKeeper -正式介绍 ZooKeeper 之前,我们先来看看 ZooKeeper 的由来,还挺有意思的。 +Before formally introducing ZooKeeper, let's take a look at its origin, which is quite interesting. -下面这段内容摘自《从 Paxos 到 ZooKeeper》第四章第一节,推荐大家阅读一下: +The following excerpt is from Chapter 4, Section 1 of "From Paxos to ZooKeeper," and I recommend everyone read it: -> ZooKeeper 最早起源于雅虎研究院的一个研究小组。在当时,研究人员发现,在雅虎内部很多大型系统基本都需要依赖一个类似的系统来进行分布式协调,但是这些系统往往都存在分布式单点问题。所以,雅虎的开发人员就试图开发一个通用的无单点问题的分布式协调框架,以便让开发人员将精力集中在处理业务逻辑上。 +> ZooKeeper originated from a research group at Yahoo! Research. At that time, researchers found that many large systems within Yahoo! relied on a similar system for distributed coordination, but these systems often had distributed single point issues. Therefore, Yahoo! developers attempted to create a general-purpose distributed coordination framework without single points of failure, allowing developers to focus on business logic. > -> 关于“ZooKeeper”这个项目的名字,其实也有一段趣闻。在立项初期,考虑到之前内部很多项目都是使用动物的名字来命名的(例如著名的 Pig 项目),雅虎的工程师希望给这个项目也取一个动物的名字。时任研究院的首席科学家 RaghuRamakrishnan 开玩笑地说:“在这样下去,我们这儿就变成动物园了!”此话一出,大家纷纷表示就叫动物园管理员吧一一一因为各个以动物命名的分布式组件放在一起,雅虎的整个分布式系统看上去就像一个大型的动物园了,而 ZooKeeper 正好要用来进行分布式环境的协调一一于是,ZooKeeper 的名字也就由此诞生了。 +> Regarding the name "ZooKeeper," there is also an interesting story. In the early stages of the project, considering that many previous internal projects were named after animals (such as the famous Pig project), Yahoo! engineers wanted to give this project an animal name as well. The then chief scientist of the research institute, Raghu Ramakrishnan, jokingly said, "At this rate, we will turn into a zoo!" Once this was said, everyone agreed to call it ZooKeeper—because various distributed components named after animals together made Yahoo!'s entire distributed system look like a large zoo, and ZooKeeper was just meant for coordinating in a distributed environment—thus, the name ZooKeeper was born. -### ZooKeeper 概览 +### Overview of ZooKeeper -ZooKeeper 是一个开源的**分布式协调服务**,它的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。 +ZooKeeper is an open-source **distributed coordination service** designed to encapsulate complex and error-prone distributed consistency services into a set of efficient and reliable primitives, providing users with a series of simple and easy-to-use interfaces. -> **原语:** 操作系统或计算机网络用语范畴。是由若干条指令组成的,用于完成一定功能的一个过程。具有不可分割性,即原语的执行必须是连续的,在执行过程中不允许被中断。 +> **Primitive:** A term used in operating systems or computer networks. It is a process composed of several instructions used to accomplish a certain function. It has indivisibility, meaning that the execution of a primitive must be continuous and cannot be interrupted during execution. -ZooKeeper 为我们提供了高可用、高性能、稳定的分布式数据一致性解决方案,通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。这些功能的实现主要依赖于 ZooKeeper 提供的 **数据存储+事件监听** 功能(后文会详细介绍到) 。 +ZooKeeper provides us with a high-availability, high-performance, and stable distributed data consistency solution, commonly used to implement functionalities such as data publishing/subscription, load balancing, naming services, distributed coordination/notification, cluster management, master election, distributed locks, and distributed queues. The implementation of these functionalities mainly relies on the **data storage + event listening** capabilities provided by ZooKeeper (which will be detailed later). -ZooKeeper 将数据保存在内存中,性能是不错的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景)。 +ZooKeeper stores data in memory, resulting in good performance, especially in applications where "reads" outnumber "writes," as "writes" cause synchronization of states among all servers. (The scenario of "reads" outnumbering "writes" is typical for coordination services). -另外,很多顶级的开源项目都用到了 ZooKeeper,比如: +Additionally, many top open-source projects utilize ZooKeeper, such as: -- **Kafka** : ZooKeeper 主要为 Kafka 提供 Broker 和 Topic 的注册以及多个 Partition 的负载均衡等功能。不过,在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构。 -- **Hbase** : ZooKeeper 为 Hbase 提供确保整个集群只有一个 Master 以及保存和提供 regionserver 状态信息(是否在线)等功能。 -- **Hadoop** : ZooKeeper 为 Namenode 提供高可用支持。 - -### ZooKeeper 特点 - -- **顺序一致性:** 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。 -- **原子性:** 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。 -- **单一系统映像:** 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。 -- **可靠性:** 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。 -- **实时性:** 一旦数据发生变更,其他节点会实时感知到。每个客户端的系统视图都是最新的。 -- **集群部署**:3~5 台(最好奇数台)机器就可以组成一个集群,每台机器都在内存保存了 ZooKeeper 的全部数据,机器之间互相通信同步数据,客户端连接任何一台机器都可以。 -- **高可用:**如果某台机器宕机,会保证数据不丢失。集群中挂掉不超过一半的机器,都能保证集群可用。比如 3 台机器可以挂 1 台,5 台机器可以挂 2 台。 - -### ZooKeeper 应用场景 - -ZooKeeper 概览中,我们介绍到使用其通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。 - -下面选 3 个典型的应用场景来专门说说: - -1. **命名服务**:可以通过 ZooKeeper 的顺序节点生成全局唯一 ID。 -2. **数据发布/订阅**:通过 **Watcher 机制** 可以很方便地实现数据发布/订阅。当你将数据发布到 ZooKeeper 被监听的节点上,其他机器可通过监听 ZooKeeper 上节点的变化来实现配置的动态更新。 -3. **分布式锁**:通过创建唯一节点获得分布式锁,当获得锁的一方执行完相关代码或者是挂掉之后就释放锁。分布式锁的实现也需要用到 **Watcher 机制** ,我在 [分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html) 这篇文章中有详细介绍到如何基于 ZooKeeper 实现分布式锁。 - -实际上,这些功能的实现基本都得益于 ZooKeeper 可以保存数据的功能,但是 ZooKeeper 不适合保存大量数据,这一点需要注意。 - -## ZooKeeper 重要概念 - -_破音:拿出小本本,下面的内容非常重要哦!_ - -### Data model(数据模型) - -ZooKeeper 数据模型采用层次化的多叉树形结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二进制序列。并且。每个节点还可以拥有 N 个子节点,最上层是根节点以“/”来代表。每个数据节点在 ZooKeeper 中被称为 **znode**,它是 ZooKeeper 中数据的最小单元。并且,每个 znode 都有一个唯一的路径标识。 - -强调一句:**ZooKeeper 主要是用来协调服务的,而不是用来存储业务数据的,所以不要放比较大的数据在 znode 上,ZooKeeper 给出的每个节点的数据大小上限是 1M 。** - -从下图可以更直观地看出:ZooKeeper 节点路径标识方式和 Unix 文件系统路径非常相似,都是由一系列使用斜杠"/"进行分割的路径表示,开发人员可以向这个节点中写入数据,也可以在节点下面创建子节点。这些操作我们后面都会介绍到。 - -![ZooKeeper 数据模型](https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/znode-structure.png) - -### znode(数据节点) - -介绍了 ZooKeeper 树形数据模型之后,我们知道每个数据节点在 ZooKeeper 中被称为 **znode**,它是 ZooKeeper 中数据的最小单元。你要存放的数据就放在上面,是你使用 ZooKeeper 过程中经常需要接触到的一个概念。 - -我们通常是将 znode 分为 4 大类: - -- **持久(PERSISTENT)节点**:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。 -- **临时(EPHEMERAL)节点**:临时节点的生命周期是与 **客户端会话(session)** 绑定的,**会话消失则节点消失**。并且,**临时节点只能做叶子节点** ,不能创建子节点。 -- **持久顺序(PERSISTENT_SEQUENTIAL)节点**:除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 `/node1/app0000000001`、`/node1/app0000000002` 。 -- **临时顺序(EPHEMERAL_SEQUENTIAL)节点**:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性 - -每个 znode 由 2 部分组成: - -- **stat**:状态信息 -- **data**:节点存放的数据的具体内容 - -如下所示,我通过 get 命令来获取 根目录下的 dubbo 节点的内容。(get 命令在下面会介绍到)。 - -```shell -[zk: 127.0.0.1:2181(CONNECTED) 6] get /dubbo -# 该数据节点关联的数据内容为空 -null -# 下面是该数据节点的一些状态信息,其实就是 Stat 对象的格式化输出 -cZxid = 0x2 -ctime = Tue Nov 27 11:05:34 CST 2018 -mZxid = 0x2 -mtime = Tue Nov 27 11:05:34 CST 2018 -pZxid = 0x3 -cversion = 1 -dataVersion = 0 -aclVersion = 0 -ephemeralOwner = 0x0 -dataLength = 0 -numChildren = 1 -``` - -Stat 类中包含了一个数据节点的所有状态信息的字段,包括事务 ID(cZxid)、节点创建时间(ctime) 和子节点个数(numChildren) 等等。 - -下面我们来看一下每个 znode 状态信息究竟代表的是什么吧!(下面的内容来源于《从 Paxos 到 ZooKeeper 分布式一致性原理与实践》,因为 Guide 确实也不是特别清楚,要学会参考资料的嘛! ): - -| znode 状态信息 | 解释 | -| -------------- | --------------------------------------------------------------------------------------------------- | -| cZxid | create ZXID,即该数据节点被创建时的事务 id | -| ctime | create time,即该节点的创建时间 | -| mZxid | modified ZXID,即该节点最终一次更新时的事务 id | -| mtime | modified time,即该节点最后一次的更新时间 | -| pZxid | 该节点的子节点列表最后一次修改时的事务 id,只有子节点列表变更才会更新 pZxid,子节点内容变更不会更新 | -| cversion | 子节点版本号,当前节点的子节点每次变化时值增加 1 | -| dataVersion | 数据节点内容版本号,节点创建时为 0,每更新一次节点内容(不管内容有无变化)该版本号的值增加 1 | -| aclVersion | 节点的 ACL 版本号,表示该节点 ACL 信息变更次数 | -| ephemeralOwner | 创建该临时节点的会话的 sessionId;如果当前节点为持久节点,则 ephemeralOwner=0 | -| dataLength | 数据节点内容长度 | -| numChildren | 当前节点的子节点个数 | - -### 版本(version) - -在前面我们已经提到,对应于每个 znode,ZooKeeper 都会为其维护一个叫作 **Stat** 的数据结构,Stat 中记录了这个 znode 的三个相关的版本: - -- **dataVersion**:当前 znode 节点的版本号 -- **cversion**:当前 znode 子节点的版本 -- **aclVersion**:当前 znode 的 ACL 的版本。 - -### ACL(权限控制) - -ZooKeeper 采用 ACL(AccessControlLists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。 - -对于 znode 操作的权限,ZooKeeper 提供了以下 5 种: - -- **CREATE** : 能创建子节点 -- **READ**:能获取节点数据和列出其子节点 -- **WRITE** : 能设置/更新节点数据 -- **DELETE** : 能删除子节点 -- **ADMIN** : 能设置节点 ACL 的权限 - -其中尤其需要注意的是,**CREATE** 和 **DELETE** 这两种权限都是针对 **子节点** 的权限控制。 - -对于身份认证,提供了以下几种方式: - -- **world**:默认方式,所有用户都可无条件访问。 -- **auth** :不使用任何 id,代表任何已认证的用户。 -- **digest** :用户名:密码认证方式:_username:password_ 。 -- **ip** : 对指定 ip 进行限制。 - -### Watcher(事件监听器) - -Watcher(事件监听器),是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。 - -![ZooKeeper Watcher 机制](https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/zookeeper-watcher.png) - -_破音:非常有用的一个特性,都拿出小本本记好了,后面用到 ZooKeeper 基本离不开 Watcher(事件监听器)机制。_ - -### 会话(Session) - -Session 可以看作是 ZooKeeper 服务器与客户端的之间的一个 TCP 长连接,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向 ZooKeeper 服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的 Watcher 事件通知。 - -Session 有一个属性叫做:`sessionTimeout` ,`sessionTimeout` 代表会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在`sessionTimeout`规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。 - -另外,在为客户端创建会话之前,服务端首先会为每个客户端都分配一个 `sessionID`。由于 `sessionID`是 ZooKeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 `sessionID` 的,因此,无论是哪台服务器为客户端分配的 `sessionID`,都务必保证全局唯一。 - -## ZooKeeper 集群 - -为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。通常 3 台服务器就可以构成一个 ZooKeeper 集群了。ZooKeeper 官方提供的架构图就是一个 ZooKeeper 集群整体对外提供服务。 - -![ZooKeeper 集群架构](https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/zookeeper-cluster.png) - -上图中每一个 Server 代表一个安装 ZooKeeper 服务的服务器。组成 ZooKeeper 服务的服务器都会在内存中维护当前的服务器状态,并且每台服务器之间都互相保持着通信。集群间通过 ZAB 协议(ZooKeeper Atomic Broadcast)来保持数据的一致性。 - -**最典型集群模式:Master/Slave 模式(主备模式)**。在这种模式中,通常 Master 服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。 - -### ZooKeeper 集群角色 - -但是,在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了 Leader、Follower 和 Observer 三种角色。如下图所示 - -![ZooKeeper 集群中角色](https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/zookeeper-cluser-roles.png) - -ZooKeeper 集群中的所有机器通过一个 **Leader 选举过程** 来选定一台称为 “**Leader**” 的机器,Leader 既可以为客户端提供写服务又能提供读服务。除了 Leader 外,**Follower** 和 **Observer** 都只能提供读服务。Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能。 - -| 角色 | 说明 | -| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Leader | 为客户端提供读和写的服务,负责投票的发起和决议,更新系统状态。 | -| Follower | 为客户端提供读服务,如果是写服务则转发给 Leader。参与选举过程中的投票。 | -| Observer | 为客户端提供读服务,如果是写服务则转发给 Leader。不参与选举过程中的投票,也不参与“过半写成功”策略。在不影响写性能的情况下提升集群的读性能。此角色于 ZooKeeper3.3 系列新增的角色。 | - -### ZooKeeper 集群 Leader 选举过程 - -当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,就会进入 Leader 选举过程,这个过程会选举产生新的 Leader 服务器。 - -这个过程大致是这样的: - -1. **Leader election(选举阶段)**:节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。 -2. **Discovery(发现阶段)**:在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。 -3. **Synchronization(同步阶段)**:同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成之后准 leader 才会成为真正的 leader。 -4. **Broadcast(广播阶段)**:到了这个阶段,ZooKeeper 集群才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。 - -ZooKeeper 集群中的服务器状态有下面几种: - -- **LOOKING**:寻找 Leader。 -- **LEADING**:Leader 状态,对应的节点为 Leader。 -- **FOLLOWING**:Follower 状态,对应的节点为 Follower。 -- **OBSERVING**:Observer 状态,对应节点为 Observer,该节点不参与 Leader 选举。 - -### ZooKeeper 集群为啥最好奇数台? - -ZooKeeper 集群在宕掉几个 ZooKeeper 服务器之后,如果剩下的 ZooKeeper 服务器个数大于宕掉的个数的话整个 ZooKeeper 才依然可用。假如我们的集群中有 n 台 ZooKeeper 服务器,那么也就是剩下的服务数必须大于 n/2。先说一下结论,2n 和 2n-1 的容忍度是一样的,都是 n-1,大家可以先自己仔细想一想,这应该是一个很简单的数学问题了。 - -比如假如我们有 3 台,那么最大允许宕掉 1 台 ZooKeeper 服务器,如果我们有 4 台的的时候也同样只允许宕掉 1 台。 -假如我们有 5 台,那么最大允许宕掉 2 台 ZooKeeper 服务器,如果我们有 6 台的的时候也同样只允许宕掉 2 台。 - -综上,何必增加那一个不必要的 ZooKeeper 呢? - -### ZooKeeper 选举的过半机制防止脑裂 - -**何为集群脑裂?** - -对于一个集群,通常多台机器会部署在不同机房,来提高这个集群的可用性。保证可用性的同时,会发生一种机房间网络线路故障,导致机房间网络不通,而集群被割裂成几个小集群。这时候子集群各自选主导致“脑裂”的情况。 - -举例说明:比如现在有一个由 6 台服务器所组成的一个集群,部署在了 2 个机房,每个机房 3 台。正常情况下只有 1 个 leader,但是当两个机房中间网络断开的时候,每个机房的 3 台服务器都会认为另一个机房的 3 台服务器下线,而选出自己的 leader 并对外提供服务。若没有过半机制,当网络恢复的时候会发现有 2 个 leader。仿佛是 1 个大脑(leader)分散成了 2 个大脑,这就发生了脑裂现象。脑裂期间 2 个大脑都可能对外提供了服务,这将会带来数据一致性等问题。 - -**过半机制是如何防止脑裂现象产生的?** - -ZooKeeper 的过半机制导致不可能产生 2 个 leader,因为少于等于一半是不可能产生 leader 的,这就使得不论机房的机器如何分配都不可能发生脑裂。 - -## ZAB 协议和 Paxos 算法 - -Paxos 算法应该可以说是 ZooKeeper 的灵魂了。但是,ZooKeeper 并没有完全采用 Paxos 算法 ,而是使用 ZAB 协议作为其保证数据一致性的核心算法。另外,在 ZooKeeper 的官方文档中也指出,ZAB 协议并不像 Paxos 算法那样,是一种通用的分布式一致性算法,它是一种特别为 Zookeeper 设计的崩溃可恢复的原子消息广播算法。 - -### ZAB 协议介绍 - -ZAB(ZooKeeper Atomic Broadcast,原子广播) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。 在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。 - -### ZAB 协议两种基本的模式:崩溃恢复和消息广播 - -ZAB 协议包括两种基本的模式,分别是 - -- **崩溃恢复**:当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 协议就会退出恢复模式。其中,**所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致**。 -- **消息广播**:**当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。** 当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。 - -### ZAB 协议&Paxos 算法文章推荐 - -关于 **ZAB 协议&Paxos 算法** 需要讲和理解的东西太多了,具体可以看下面这几篇文章: - -- [Paxos 算法详解](https://javaguide.cn/distributed-system/protocol/paxos-algorithm.html) -- [ZooKeeper 与 Zab 协议 · Analyze](https://wingsxdu.com/posts/database/zookeeper/) -- [Raft 算法详解](https://javaguide.cn/distributed-system/protocol/raft-algorithm.html) - -## ZooKeeper VS ETCD - -[ETCD](https://etcd.io/) 是一种强一致性的分布式键值存储,它提供了一种可靠的方式来存储需要由分布式系统或机器集群访问的数据。ETCD 内部采用 [Raft 算法](https://javaguide.cn/distributed-system/protocol/raft-algorithm.html)作为一致性算法,基于 Go 语言实现。 - -与 ZooKeeper 类似,ETCD 也可用于数据发布/订阅、负载均衡、命名服务、分布式协调/通知、分布式锁等场景。那二者如何选择呢? - -得物技术的[浅析如何基于 ZooKeeper 实现高可用架构](https://mp.weixin.qq.com/s/pBI3rjv5NdS1124Z7HQ-JA)这篇文章给出了如下的对比表格(我进一步做了优化),可以作为参考: - -| | ZooKeeper | ETCD | -| ---------------- | --------------------------------------------------------------------- | ------------------------------------------------------ | -| **语言** | Java | Go | -| **协议** | TCP | Grpc | -| **接口调用** | 必须要使用自己的 client 进行调用 | 可通过 HTTP 传输,即可通过 CURL 等命令实现调用 | -| **一致性算法** | Zab 协议 | Raft 算法 | -| **Watcher 机制** | 较局限,一次性触发器 | 一次 Watch 可以监听所有的事件 | -| **数据模型** | 基于目录的层次模式 | 参考了 zk 的数据模型,是个扁平的 kv 模型 | -| **存储** | kv 存储,使用的是 ConcurrentHashMap,内存存储,一般不建议存储较多数据 | kv 存储,使用 bbolt 存储引擎,可以处理几个 GB 的数据。 | -| **MVCC** | 不支持 | 支持,通过两个 B+ Tree 进行版本控制 | -| **全局 Session** | 存在缺陷 | 实现更灵活,避免了安全性问题 | -| **权限校验** | ACL | RBAC | -| **事务能力** | 提供了简易的事务能力 | 只提供了版本号的检查能力 | -| **部署维护** | 复杂 | 简单 | - -ZooKeeper 在存储性能、全局 Session、Watcher 机制等方面存在一定局限性,越来越多的开源项目在替换 ZooKeeper 为 Raft 实现或其它分布式协调服务,例如:[Kafka Needs No Keeper - Removing ZooKeeper Dependency (confluent.io)](https://www.confluent.io/blog/removing-zookeeper-dependency-in-kafka/)、[Moving Toward a ZooKeeper-Less Apache Pulsar (streamnative.io)](https://streamnative.io/blog/moving-toward-zookeeper-less-apache-pulsar)。 - -ETCD 相对来说更优秀一些,提供了更稳定的高负载读写能力,对 ZooKeeper 暴露的许多问题进行了改进优化。并且,ETCD 基本能够覆盖 ZooKeeper 的所有应用场景,实现对其的替代。 - -## 总结 - -1. ZooKeeper 本身就是一个分布式程序(只要半数以上节点存活,ZooKeeper 就能正常服务)。 -2. 为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。 -3. ZooKeeper 将数据保存在内存中,这也就保证了 高吞吐量和低延迟(但是内存限制了能够存储的容量不太大,此限制也是保持 znode 中存储的数据量较小的进一步原因)。 -4. ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地明显,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景。) -5. ZooKeeper 有临时节点的概念。 当创建临时节点的客户端会话一直保持活动,瞬时节点就一直存在。而当会话终结时,瞬时节点被删除。持久节点是指一旦这个 znode 被创建了,除非主动进行 znode 的移除操作,否则这个 znode 将一直保存在 ZooKeeper 上。 -6. ZooKeeper 底层其实只提供了两个功能:① 管理(存储、读取)用户程序提交的数据;② 为用户程序提供数据节点监听服务。 - -## 参考 - -- 《从 Paxos 到 ZooKeeper 分布式一致性原理与实践》 -- 谈谈 ZooKeeper 的局限性: - - +- **Kafka**: ZooKeeper primarily provides registration for Brokers and Topics, as well as load balancing for multiple Partitions. However, after Kafka 2.8, it introduced the KRaft mode based on the Raft protocol, eliminating the dependency on ZooKeeper and greatly simplifying Kafka's architecture. +- **HBase**: ZooKeeper ensures that there is only one Master in the entire HBase cluster and provides and saves the status information of region servers (whether they are online). +- **Hadoop**: ZooKeeper provides high availability support for the Namen diff --git a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md index 856378a0cd5..ad6193591ea 100644 --- a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md +++ b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md @@ -1,386 +1,44 @@ --- -title: ZooKeeper相关概念总结(进阶) -category: 分布式 +title: Summary of ZooKeeper Related Concepts (Advanced) +category: Distributed tag: - ZooKeeper --- -> [FrancisQ](https://juejin.im/user/5c33853851882525ea106810) 投稿。 +> [FrancisQ](https://juejin.im/user/5c33853851882525ea106810) Contribution. -## 什么是 ZooKeeper +## What is ZooKeeper -`ZooKeeper` 由 `Yahoo` 开发,后来捐赠给了 `Apache` ,现已成为 `Apache` 顶级项目。`ZooKeeper` 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 `Paxos` 算法的 `ZAB` 协议完成的。其主要功能包括:配置维护、分布式同步、集群管理等。 +`ZooKeeper` was developed by `Yahoo` and later donated to `Apache`, and it has now become an Apache top-level project. `ZooKeeper` is an open-source distributed application coordination server that provides consistency services for distributed systems. Its consistency is achieved through the `ZAB` protocol based on the `Paxos` algorithm. Its main functions include configuration maintenance, distributed synchronization, cluster management, and more. -简单来说, `ZooKeeper` 是一个 **分布式协调服务框架** 。分布式?协调服务?这啥玩意?🤔🤔 +In simple terms, `ZooKeeper` is a **distributed coordination service framework**. Distributed? Coordination service? What is that? 🤔🤔 -其实解释到分布式这个概念的时候,我发现有些同学并不是能把 **分布式和集群** 这两个概念很好的理解透。前段时间有同学和我探讨起分布式的东西,他说分布式不就是加机器吗?一台机器不够用再加一台抗压呗。当然加机器这种说法也无可厚非,你一个分布式系统必定涉及到多个机器,但是你别忘了,计算机学科中还有一个相似的概念—— `Cluster` ,集群不也是加机器吗?但是 集群 和 分布式 其实就是两个完全不同的概念。 +When explaining the concept of distribution, I found that some students do not fully understand the difference between **distributed systems and clusters**. Recently, a student discussed distributed systems with me, saying that isn't distributed just about adding machines? If one machine is not enough, just add another to handle the load. Of course, saying "add machines" is not entirely wrong; a distributed system must involve multiple machines. However, don't forget that there is a similar concept in computer science—`Cluster`. Isn't a cluster also about adding machines? But clusters and distributed systems are actually two completely different concepts. -比如,我现在有一个秒杀服务,并发量太大单机系统承受不住,那我加几台服务器也 **一样** 提供秒杀服务,这个时候就是 **`Cluster` 集群** 。 +For example, I now have a flash sale service, and the concurrency is too high for a single machine to handle. If I add a few servers, I can still provide the flash sale service; this is a **`Cluster`**. ![cluster](https://oss.javaguide.cn/p3-juejin/60263e969b9e4a0f81724b1f4d5b3d58~tplv-k3u1fbpfcp-zoom-1.jpeg) -但是,我现在换一种方式,我将一个秒杀服务 **拆分成多个子服务** ,比如创建订单服务,增加积分服务,扣优惠券服务等等,**然后我将这些子服务都部署在不同的服务器上** ,这个时候就是 **`Distributed` 分布式** 。 +However, if I take a different approach and **split a flash sale service into multiple sub-services**, such as creating an order service, a points service, a coupon deduction service, etc., **and then deploy these sub-services on different servers**, this is a **`Distributed`** system. ![distributed](https://oss.javaguide.cn/p3-juejin/0d42e7b4249144b3a77a0c519216ae3d~tplv-k3u1fbpfcp-zoom-1.jpeg) -而我为什么反驳同学所说的分布式就是加机器呢?因为我认为加机器更加适用于构建集群,因为它真是只有加机器。而对于分布式来说,你首先需要将业务进行拆分,然后再加机器(不仅仅是加机器那么简单),同时你还要去解决分布式带来的一系列问题。 +Why do I refute the student's claim that distributed means just adding machines? Because I believe that adding machines is more applicable to building a cluster, as it truly only involves adding machines. In contrast, for a distributed system, you first need to split the business, and then add machines (it's not just as simple as adding machines). At the same time, you also need to solve a series of problems brought by distribution. ![](https://oss.javaguide.cn/p3-juejin/e3662ca1a09c4444b07f15dbf85c6ba8~tplv-k3u1fbpfcp-zoom-1.jpeg) -比如各个分布式组件如何协调起来,如何减少各个系统之间的耦合度,分布式事务的处理,如何去配置整个分布式系统等等。`ZooKeeper` 主要就是解决这些问题的。 +For example, how various distributed components coordinate, how to reduce coupling between systems, handling distributed transactions, how to configure the entire distributed system, etc. `ZooKeeper` mainly addresses these issues. -## 一致性问题 +## Consistency Issues -设计一个分布式系统必定会遇到一个问题—— **因为分区容忍性(partition tolerance)的存在,就必定要求我们需要在系统可用性(availability)和数据一致性(consistency)中做出权衡** 。这就是著名的 `CAP` 定理。 +Designing a distributed system will inevitably encounter a problem—**due to the existence of partition tolerance, we must make trade-offs between system availability and data consistency**. This is the famous `CAP` theorem. -理解起来其实很简单,比如说把一个班级作为整个系统,而学生是系统中的一个个独立的子系统。这个时候班里的小红小明偷偷谈恋爱被班里的大嘴巴小花发现了,小花欣喜若狂告诉了周围的人,然后小红小明谈恋爱的消息在班级里传播起来了。当在消息的传播(散布)过程中,你抓到一个同学问他们的情况,如果回答你不知道,那么说明整个班级系统出现了数据不一致的问题(因为小花已经知道这个消息了)。而如果他直接不回答你,因为整个班级有消息在进行传播(为了保证一致性,需要所有人都知道才可提供服务),这个时候就出现了系统的可用性问题。 +Understanding it is actually quite simple. For example, consider a class as the entire system, and the students are independent subsystems within the system. At this point, if Xiaohong and Xiaoming in the class are secretly dating and are discovered by the gossiping Xiaohua, who excitedly tells those around her, the news of Xiaohong and Xiaoming's relationship spreads throughout the class. If you catch a student during the spread of the news and ask them about it, if they respond with "I don't know," it indicates that the entire class system has a data inconsistency issue (because Xiaohua already knows this news). If they simply do not answer you, it means that the entire class is in the process of spreading the news (to ensure consistency, everyone must know before services can be provided), and at this point, a system availability issue arises. ![](https://oss.javaguide.cn/p3-juejin/38b9ff4b193e4487afe32c9710c6d644~tplv-k3u1fbpfcp-zoom-1-20230717160254318-20230717160259975.jpeg) -而上述前者就是 `Eureka` 的处理方式,它保证了 AP(可用性),后者就是我们今天所要讲的 `ZooKeeper` 的处理方式,它保证了 CP(数据一致性)。 +The former is the handling method of `Eureka`, which guarantees AP (availability), while the latter is the handling method of `ZooKeeper`, which guarantees CP (data consistency). -## 一致性协议和算法 +## Consistency Protocols and Algorithms -而为了解决数据一致性问题,在科学家和程序员的不断探索中,就出现了很多的一致性协议和算法。比如 2PC(两阶段提交),3PC(三阶段提交),Paxos 算法等等。 - -这时候请你思考一个问题,同学之间如果采用传纸条的方式去传播消息,那么就会出现一个问题——我咋知道我的小纸条有没有传到我想要传递的那个人手中呢?万一被哪个小家伙给劫持篡改了呢,对吧? - -![](https://oss.javaguide.cn/p3-juejin/8c73e264d28b4a93878f4252e4e3e43c~tplv-k3u1fbpfcp-zoom-1.jpeg) - -这个时候就引申出一个概念—— **拜占庭将军问题** 。它意指 **在不可靠信道上试图通过消息传递的方式达到一致性是不可能的**, 所以所有的一致性算法的 **必要前提** 就是安全可靠的消息通道。 - -而为什么要去解决数据一致性的问题?你想想,如果一个秒杀系统将服务拆分成了下订单和加积分服务,这两个服务部署在不同的机器上了,万一在消息的传播过程中积分系统宕机了,总不能你这边下了订单却没加积分吧?你总得保证两边的数据需要一致吧? - -### 2PC(两阶段提交) - -两阶段提交是一种保证分布式系统数据一致性的协议,现在很多数据库都是采用的两阶段提交协议来完成 **分布式事务** 的处理。 - -在介绍 2PC 之前,我们先来想想分布式事务到底有什么问题呢? - -还拿秒杀系统的下订单和加积分两个系统来举例吧(我想你们可能都吐了 🤮🤮🤮),我们此时下完订单会发个消息给积分系统告诉它下面该增加积分了。如果我们仅仅是发送一个消息也不收回复,那么我们的订单系统怎么能知道积分系统的收到消息的情况呢?如果我们增加一个收回复的过程,那么当积分系统收到消息后返回给订单系统一个 `Response` ,但在中间出现了网络波动,那个回复消息没有发送成功,订单系统是不是以为积分系统消息接收失败了?它是不是会回滚事务?但此时积分系统是成功收到消息的,它就会去处理消息然后给用户增加积分,这个时候就会出现积分加了但是订单没下成功。 - -所以我们所需要解决的是在分布式系统中,整个调用链中,我们所有服务的数据处理要么都成功要么都失败,即所有服务的 **原子性问题** 。 - -在两阶段提交中,主要涉及到两个角色,分别是协调者和参与者。 - -第一阶段:当要执行一个分布式事务的时候,事务发起者首先向协调者发起事务请求,然后协调者会给所有参与者发送 `prepare` 请求(其中包括事务内容)告诉参与者你们需要执行事务了,如果能执行我发的事务内容那么就先执行但不提交,执行后请给我回复。然后参与者收到 `prepare` 消息后,他们会开始执行事务(但不提交),并将 `Undo` 和 `Redo` 信息记入事务日志中,之后参与者就向协调者反馈是否准备好了。 - -第二阶段:第二阶段主要是协调者根据参与者反馈的情况来决定接下来是否可以进行事务的提交操作,即提交事务或者回滚事务。 - -比如这个时候 **所有的参与者** 都返回了准备好了的消息,这个时候就进行事务的提交,协调者此时会给所有的参与者发送 **`Commit` 请求** ,当参与者收到 `Commit` 请求的时候会执行前面执行的事务的 **提交操作** ,提交完毕之后将给协调者发送提交成功的响应。 - -而如果在第一阶段并不是所有参与者都返回了准备好了的消息,那么此时协调者将会给所有参与者发送 **回滚事务的 `rollback` 请求**,参与者收到之后将会 **回滚它在第一阶段所做的事务处理** ,然后再将处理情况返回给协调者,最终协调者收到响应后便给事务发起者返回处理失败的结果。 - -![2PC流程](https://oss.javaguide.cn/p3-juejin/1a7210167f1d4d4fb97afcec19902a59~tplv-k3u1fbpfcp-zoom-1.jpeg) - -个人觉得 2PC 实现得还是比较鸡肋的,因为事实上它只解决了各个事务的原子性问题,随之也带来了很多的问题。 - -![](https://oss.javaguide.cn/p3-juejin/cc534022c7184770b9b82b2d0008432a~tplv-k3u1fbpfcp-zoom-1.jpeg) - -- **单点故障问题**,如果协调者挂了那么整个系统都处于不可用的状态了。 -- **阻塞问题**,即当协调者发送 `prepare` 请求,参与者收到之后如果能处理那么它将会进行事务的处理但并不提交,这个时候会一直占用着资源不释放,如果此时协调者挂了,那么这些资源都不会再释放了,这会极大影响性能。 -- **数据不一致问题**,比如当第二阶段,协调者只发送了一部分的 `commit` 请求就挂了,那么也就意味着,收到消息的参与者会进行事务的提交,而后面没收到的则不会进行事务提交,那么这时候就会产生数据不一致性问题。 - -### 3PC(三阶段提交) - -因为 2PC 存在的一系列问题,比如单点,容错机制缺陷等等,从而产生了 **3PC(三阶段提交)** 。那么这三阶段又分别是什么呢? - -> 千万不要吧 PC 理解成个人电脑了,其实他们是 phase-commit 的缩写,即阶段提交。 - -1. **CanCommit 阶段**:协调者向所有参与者发送 `CanCommit` 请求,参与者收到请求后会根据自身情况查看是否能执行事务,如果可以则返回 YES 响应并进入预备状态,否则返回 NO 。 -2. **PreCommit 阶段**:协调者根据参与者返回的响应来决定是否可以进行下面的 `PreCommit` 操作。如果上面参与者返回的都是 YES,那么协调者将向所有参与者发送 `PreCommit` 预提交请求,**参与者收到预提交请求后,会进行事务的执行操作,并将 `Undo` 和 `Redo` 信息写入事务日志中** ,最后如果参与者顺利执行了事务则给协调者返回成功的响应。如果在第一阶段协调者收到了 **任何一个 NO** 的信息,或者 **在一定时间内** 并没有收到全部的参与者的响应,那么就会中断事务,它会向所有参与者发送中断请求(abort),参与者收到中断请求之后会立即中断事务,或者在一定时间内没有收到协调者的请求,它也会中断事务。 -3. **DoCommit 阶段**:这个阶段其实和 `2PC` 的第二阶段差不多,如果协调者收到了所有参与者在 `PreCommit` 阶段的 YES 响应,那么协调者将会给所有参与者发送 `DoCommit` 请求,**参与者收到 `DoCommit` 请求后则会进行事务的提交工作**,完成后则会给协调者返回响应,协调者收到所有参与者返回的事务提交成功的响应之后则完成事务。若协调者在 `PreCommit` 阶段 **收到了任何一个 NO 或者在一定时间内没有收到所有参与者的响应** ,那么就会进行中断请求的发送,参与者收到中断请求后则会 **通过上面记录的回滚日志** 来进行事务的回滚操作,并向协调者反馈回滚状况,协调者收到参与者返回的消息后,中断事务。 - -![3PC流程](https://oss.javaguide.cn/p3-juejin/80854635d48c42d896dbaa066abf5c26~tplv-k3u1fbpfcp-zoom-1.jpeg) - -> 这里是 `3PC` 在成功的环境下的流程图,你可以看到 `3PC` 在很多地方进行了超时中断的处理,比如协调者在指定时间内未收到全部的确认消息则进行事务中断的处理,这样能 **减少同步阻塞的时间** 。还有需要注意的是,**`3PC` 在 `DoCommit` 阶段参与者如未收到协调者发送的提交事务的请求,它会在一定时间内进行事务的提交**。为什么这么做呢?是因为这个时候我们肯定**保证了在第一阶段所有的协调者全部返回了可以执行事务的响应**,这个时候我们有理由**相信其他系统都能进行事务的执行和提交**,所以**不管**协调者有没有发消息给参与者,进入第三阶段参与者都会进行事务的提交操作。 - -总之,`3PC` 通过一系列的超时机制很好的缓解了阻塞问题,但是最重要的一致性并没有得到根本的解决,比如在 `DoCommit` 阶段,当一个参与者收到了请求之后其他参与者和协调者挂了或者出现了网络分区,这个时候收到消息的参与者都会进行事务提交,这就会出现数据不一致性问题。 - -所以,要解决一致性问题还需要靠 `Paxos` 算法 ⭐️ ⭐️ ⭐️ 。 - -### `Paxos` 算法 - -`Paxos` 算法是基于**消息传递且具有高度容错特性的一致性算法**,是目前公认的解决分布式一致性问题最有效的算法之一,**其解决的问题就是在分布式系统中如何就某个值(决议)达成一致** 。 - -在 `Paxos` 中主要有三个角色,分别为 `Proposer提案者`、`Acceptor表决者`、`Learner学习者`。`Paxos` 算法和 `2PC` 一样,也有两个阶段,分别为 `Prepare` 和 `accept` 阶段。 - -#### prepare 阶段 - -- `Proposer提案者`:负责提出 `proposal`,每个提案者在提出提案时都会首先获取到一个 **具有全局唯一性的、递增的提案编号 N**,即在整个集群中是唯一的编号 N,然后将该编号赋予其要提出的提案,在**第一阶段是只将提案编号发送给所有的表决者**。 -- `Acceptor表决者`:每个表决者在 `accept` 某提案后,会将该提案编号 N 记录在本地,这样每个表决者中保存的已经被 accept 的提案中会存在一个**编号最大的提案**,其编号假设为 `maxN`。每个表决者仅会 `accept` 编号大于自己本地 `maxN` 的提案,在批准提案时表决者会将以前接受过的最大编号的提案作为响应反馈给 `Proposer` 。 - -> 下面是 `prepare` 阶段的流程图,你可以对照着参考一下。 - -![paxos第一阶段](https://oss.javaguide.cn/p3-juejin/cd1e5f78875b4ad6b54013738f570943~tplv-k3u1fbpfcp-zoom-1.jpeg) - -#### accept 阶段 - -当一个提案被 `Proposer` 提出后,如果 `Proposer` 收到了超过半数的 `Acceptor` 的批准(`Proposer` 本身同意),那么此时 `Proposer` 会给所有的 `Acceptor` 发送真正的提案(你可以理解为第一阶段为试探),这个时候 `Proposer` 就会发送提案的内容和提案编号。 - -表决者收到提案请求后会再次比较本身已经批准过的最大提案编号和该提案编号,如果该提案编号 **大于等于** 已经批准过的最大提案编号,那么就 `accept` 该提案(此时执行提案内容但不提交),随后将情况返回给 `Proposer` 。如果不满足则不回应或者返回 NO 。 - -![paxos第二阶段1](https://oss.javaguide.cn/p3-juejin/dad7f51d58b24a72b249278502ec04bd~tplv-k3u1fbpfcp-zoom-1.jpeg) - -当 `Proposer` 收到超过半数的 `accept` ,那么它这个时候会向所有的 `acceptor` 发送提案的提交请求。需要注意的是,因为上述仅仅是超过半数的 `acceptor` 批准执行了该提案内容,其他没有批准的并没有执行该提案内容,所以这个时候需要**向未批准的 `acceptor` 发送提案内容和提案编号并让它无条件执行和提交**,而对于前面已经批准过该提案的 `acceptor` 来说 **仅仅需要发送该提案的编号** ,让 `acceptor` 执行提交就行了。 - -![paxos第二阶段2](https://oss.javaguide.cn/p3-juejin/9359bbabb511472e8de04d0826967996~tplv-k3u1fbpfcp-zoom-1.jpeg) - -而如果 `Proposer` 如果没有收到超过半数的 `accept` 那么它将会将 **递增** 该 `Proposal` 的编号,然后 **重新进入 `Prepare` 阶段** 。 - -> 对于 `Learner` 来说如何去学习 `Acceptor` 批准的提案内容,这有很多方式,读者可以自己去了解一下,这里不做过多解释。 - -#### paxos 算法的死循环问题 - -其实就有点类似于两个人吵架,小明说我是对的,小红说我才是对的,两个人据理力争的谁也不让谁 🤬🤬。 - -比如说,此时提案者 P1 提出一个方案 M1,完成了 `Prepare` 阶段的工作,这个时候 `acceptor` 则批准了 M1,但是此时提案者 P2 同时也提出了一个方案 M2,它也完成了 `Prepare` 阶段的工作。然后 P1 的方案已经不能在第二阶段被批准了(因为 `acceptor` 已经批准了比 M1 更大的 M2),所以 P1 自增方案变为 M3 重新进入 `Prepare` 阶段,然后 `acceptor` ,又批准了新的 M3 方案,它又不能批准 M2 了,这个时候 M2 又自增进入 `Prepare` 阶段。。。 - -就这样无休无止的永远提案下去,这就是 `paxos` 算法的死循环问题。 - -![](https://oss.javaguide.cn/p3-juejin/bc3d45941abf4fca903f7f4b69405abf~tplv-k3u1fbpfcp-zoom-1.jpeg) - -那么如何解决呢?很简单,人多了容易吵架,我现在 **就允许一个能提案** 就行了。 - -## 引出 ZAB - -### Zookeeper 架构 - -作为一个优秀高效且可靠的分布式协调框架,`ZooKeeper` 在解决分布式数据一致性问题时并没有直接使用 `Paxos` ,而是专门定制了一致性协议叫做 `ZAB(ZooKeeper Atomic Broadcast)` 原子广播协议,该协议能够很好地支持 **崩溃恢复** 。 - -![Zookeeper架构](https://oss.javaguide.cn/p3-juejin/07bf6c1e10f84fc58a2453766ca6bd18~tplv-k3u1fbpfcp-zoom-1.png) - -### ZAB 中的三个角色 - -和介绍 `Paxos` 一样,在介绍 `ZAB` 协议之前,我们首先来了解一下在 `ZAB` 中三个主要的角色,`Leader 领导者`、`Follower跟随者`、`Observer观察者` 。 - -- `Leader`:集群中 **唯一的写请求处理者** ,能够发起投票(投票也是为了进行写请求)。 -- `Follower`:能够接收客户端的请求,如果是读请求则可以自己处理,**如果是写请求则要转发给 `Leader`** 。在选举过程中会参与投票,**有选举权和被选举权** 。 -- `Observer`:就是没有选举权和被选举权的 `Follower` 。 - -在 `ZAB` 协议中对 `zkServer`(即上面我们说的三个角色的总称) 还有两种模式的定义,分别是 **消息广播** 和 **崩溃恢复** 。 - -### 消息广播模式 - -说白了就是 `ZAB` 协议是如何处理写请求的,上面我们不是说只有 `Leader` 能处理写请求嘛?那么我们的 `Follower` 和 `Observer` 是不是也需要 **同步更新数据** 呢?总不能数据只在 `Leader` 中更新了,其他角色都没有得到更新吧? - -不就是 **在整个集群中保持数据的一致性** 嘛?如果是你,你会怎么做呢? - -废话,第一步肯定需要 `Leader` 将写请求 **广播** 出去呀,让 `Leader` 问问 `Followers` 是否同意更新,如果超过半数以上的同意那么就进行 `Follower` 和 `Observer` 的更新(和 `Paxos` 一样)。当然这么说有点虚,画张图理解一下。 - -![消息广播](https://oss.javaguide.cn/p3-juejin/b64c7f25a5d24766889da14260005e31~tplv-k3u1fbpfcp-zoom-1.jpeg) - -嗯。。。看起来很简单,貌似懂了 🤥🤥🤥。这两个 `Queue` 哪冒出来的?答案是 **`ZAB` 需要让 `Follower` 和 `Observer` 保证顺序性** 。何为顺序性,比如我现在有一个写请求 A,此时 `Leader` 将请求 A 广播出去,因为只需要半数同意就行,所以可能这个时候有一个 `Follower` F1 因为网络原因没有收到,而 `Leader` 又广播了一个请求 B,因为网络原因,F1 竟然先收到了请求 B 然后才收到了请求 A,这个时候请求处理的顺序不同就会导致数据的不同,从而 **产生数据不一致问题** 。 - -所以在 `Leader` 这端,它为每个其他的 `zkServer` 准备了一个 **队列** ,采用先进先出的方式发送消息。由于协议是 **通过 `TCP`** 来进行网络通信的,保证了消息的发送顺序性,接受顺序性也得到了保证。 - -除此之外,在 `ZAB` 中还定义了一个 **全局单调递增的事务 ID `ZXID`** ,它是一个 64 位 long 型,其中高 32 位表示 `epoch` 年代,低 32 位表示事务 id。`epoch` 是会根据 `Leader` 的变化而变化的,当一个 `Leader` 挂了,新的 `Leader` 上位的时候,年代(`epoch`)就变了。而低 32 位可以简单理解为递增的事务 id。 - -定义这个的原因也是为了顺序性,每个 `proposal` 在 `Leader` 中生成后需要 **通过其 `ZXID` 来进行排序** ,才能得到处理。 - -### 崩溃恢复模式 - -说到崩溃恢复我们首先要提到 `ZAB` 中的 `Leader` 选举算法,当系统出现崩溃影响最大应该是 `Leader` 的崩溃,因为我们只有一个 `Leader` ,所以当 `Leader` 出现问题的时候我们势必需要重新选举 `Leader` 。 - -`Leader` 选举可以分为两个不同的阶段,第一个是我们提到的 `Leader` 宕机需要重新选举,第二则是当 `Zookeeper` 启动时需要进行系统的 `Leader` 初始化选举。下面我先来介绍一下 `ZAB` 是如何进行初始化选举的。 - -假设我们集群中有 3 台机器,那也就意味着我们需要两台以上同意(超过半数)。比如这个时候我们启动了 `server1` ,它会首先 **投票给自己** ,投票内容为服务器的 `myid` 和 `ZXID` ,因为初始化所以 `ZXID` 都为 0,此时 `server1` 发出的投票为 (1,0)。但此时 `server1` 的投票仅为 1,所以不能作为 `Leader` ,此时还在选举阶段所以整个集群处于 **`Looking` 状态**。 - -接着 `server2` 启动了,它首先也会将投票选给自己(2,0),并将投票信息广播出去(`server1`也会,只是它那时没有其他的服务器了),`server1` 在收到 `server2` 的投票信息后会将投票信息与自己的作比较。**首先它会比较 `ZXID` ,`ZXID` 大的优先为 `Leader`,如果相同则比较 `myid`,`myid` 大的优先作为 `Leader`**。所以此时`server1` 发现 `server2` 更适合做 `Leader`,它就会将自己的投票信息更改为(2,0)然后再广播出去,之后`server2` 收到之后发现和自己的一样无需做更改,并且自己的 **投票已经超过半数** ,则 **确定 `server2` 为 `Leader`**,`server1` 也会将自己服务器设置为 `Following` 变为 `Follower`。整个服务器就从 `Looking` 变为了正常状态。 - -当 `server3` 启动发现集群没有处于 `Looking` 状态时,它会直接以 `Follower` 的身份加入集群。 - -还是前面三个 `server` 的例子,如果在整个集群运行的过程中 `server2` 挂了,那么整个集群会如何重新选举 `Leader` 呢?其实和初始化选举差不多。 - -首先毫无疑问的是剩下的两个 `Follower` 会将自己的状态 **从 `Following` 变为 `Looking` 状态** ,然后每个 `server` 会向初始化投票一样首先给自己投票(这不过这里的 `zxid` 可能不是 0 了,这里为了方便随便取个数字)。 - -假设 `server1` 给自己投票为(1,99),然后广播给其他 `server`,`server3` 首先也会给自己投票(3,95),然后也广播给其他 `server`。`server1` 和 `server3` 此时会收到彼此的投票信息,和一开始选举一样,他们也会比较自己的投票和收到的投票(`zxid` 大的优先,如果相同那么就 `myid` 大的优先)。这个时候 `server1` 收到了 `server3` 的投票发现没自己的合适故不变,`server3` 收到 `server1` 的投票结果后发现比自己的合适于是更改投票为(1,99)然后广播出去,最后 `server1` 收到了发现自己的投票已经超过半数就把自己设为 `Leader`,`server3` 也随之变为 `Follower`。 - -> 请注意 `ZooKeeper` 为什么要设置奇数个结点?比如这里我们是三个,挂了一个我们还能正常工作,挂了两个我们就不能正常工作了(已经没有超过半数的节点数了,所以无法进行投票等操作了)。而假设我们现在有四个,挂了一个也能工作,**但是挂了两个也不能正常工作了**,这是和三个一样的,而三个比四个还少一个,带来的效益是一样的,所以 `Zookeeper` 推荐奇数个 `server` 。 - -那么说完了 `ZAB` 中的 `Leader` 选举方式之后我们再来了解一下 **崩溃恢复** 是什么玩意? - -其实主要就是 **当集群中有机器挂了,我们整个集群如何保证数据一致性?** - -如果只是 `Follower` 挂了,而且挂的没超过半数的时候,因为我们一开始讲了在 `Leader` 中会维护队列,所以不用担心后面的数据没接收到导致数据不一致性。 - -如果 `Leader` 挂了那就麻烦了,我们肯定需要先暂停服务变为 `Looking` 状态然后进行 `Leader` 的重新选举(上面我讲过了),但这个就要分为两种情况了,分别是 **确保已经被 Leader 提交的提案最终能够被所有的 Follower 提交** 和 **跳过那些已经被丢弃的提案** 。 - -确保已经被 Leader 提交的提案最终能够被所有的 Follower 提交是什么意思呢? - -假设 `Leader (server2)` 发送 `commit` 请求(忘了请看上面的消息广播模式),他发送给了 `server3`,然后要发给 `server1` 的时候突然挂了。这个时候重新选举的时候我们如果把 `server1` 作为 `Leader` 的话,那么肯定会产生数据不一致性,因为 `server3` 肯定会提交刚刚 `server2` 发送的 `commit` 请求的提案,而 `server1` 根本没收到所以会丢弃。 - -![崩溃恢复](https://oss.javaguide.cn/p3-juejin/4b8365e80bdf441ea237847fb91236b7~tplv-k3u1fbpfcp-zoom-1.jpeg) - -那怎么解决呢? - -聪明的同学肯定会质疑,**这个时候 `server1` 已经不可能成为 `Leader` 了,因为 `server1` 和 `server3` 进行投票选举的时候会比较 `ZXID` ,而此时 `server3` 的 `ZXID` 肯定比 `server1` 的大了**。(不理解可以看前面的选举算法) - -那么跳过那些已经被丢弃的提案又是什么意思呢? - -假设 `Leader (server2)` 此时同意了提案 N1,自身提交了这个事务并且要发送给所有 `Follower` 要 `commit` 的请求,却在这个时候挂了,此时肯定要重新进行 `Leader` 的选举,比如说此时选 `server1` 为 `Leader` (这无所谓)。但是过了一会,这个 **挂掉的 `Leader` 又重新恢复了** ,此时它肯定会作为 `Follower` 的身份进入集群中,需要注意的是刚刚 `server2` 已经同意提交了提案 N1,但其他 `server` 并没有收到它的 `commit` 信息,所以其他 `server` 不可能再提交这个提案 N1 了,这样就会出现数据不一致性问题了,所以 **该提案 N1 最终需要被抛弃掉** 。 - -![崩溃恢复](https://oss.javaguide.cn/p3-juejin/99cdca39ad6340ae8b77e8befe94e36e~tplv-k3u1fbpfcp-zoom-1.jpeg) - -## Zookeeper 的几个理论知识 - -了解了 `ZAB` 协议还不够,它仅仅是 `Zookeeper` 内部实现的一种方式,而我们如何通过 `Zookeeper` 去做一些典型的应用场景呢?比如说集群管理,分布式锁,`Master` 选举等等。 - -这就涉及到如何使用 `Zookeeper` 了,但在使用之前我们还需要掌握几个概念。比如 `Zookeeper` 的 **数据模型**、**会话机制**、**ACL**、**Watcher 机制** 等等。 - -### 数据模型 - -`zookeeper` 数据存储结构与标准的 `Unix` 文件系统非常相似,都是在根节点下挂很多子节点(树型)。但是 `zookeeper` 中没有文件系统中目录与文件的概念,而是 **使用了 `znode` 作为数据节点** 。`znode` 是 `zookeeper` 中的最小数据单元,每个 `znode` 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。 - -![zk数据模型](https://oss.javaguide.cn/p3-juejin/663240470d524dd4ac6e68bde0b666eb~tplv-k3u1fbpfcp-zoom-1.jpeg) - -每个 `znode` 都有自己所属的 **节点类型** 和 **节点状态**。 - -其中节点类型可以分为 **持久节点**、**持久顺序节点**、**临时节点** 和 **临时顺序节点**。 - -- 持久节点:一旦创建就一直存在,直到将其删除。 -- 持久顺序节点:一个父节点可以为其子节点 **维护一个创建的先后顺序** ,这个顺序体现在 **节点名称** 上,是节点名称后自动添加一个由 10 位数字组成的数字串,从 0 开始计数。 -- 临时节点:临时节点的生命周期是与 **客户端会话** 绑定的,**会话消失则节点消失** 。临时节点 **只能做叶子节点** ,不能创建子节点。 -- 临时顺序节点:父节点可以创建一个维持了顺序的临时节点(和前面的持久顺序性节点一样)。 - -节点状态中包含了很多节点的属性比如 `czxid`、`mzxid` 等等,在 `zookeeper` 中是使用 `Stat` 这个类来维护的。下面我列举一些属性解释。 - -- `czxid`:`Created ZXID`,该数据节点被 **创建** 时的事务 ID。 -- `mzxid`:`Modified ZXID`,节点 **最后一次被更新时** 的事务 ID。 -- `ctime`:`Created Time`,该节点被创建的时间。 -- `mtime`:`Modified Time`,该节点最后一次被修改的时间。 -- `version`:节点的版本号。 -- `cversion`:**子节点** 的版本号。 -- `aversion`:节点的 `ACL` 版本号。 -- `ephemeralOwner`:创建该节点的会话的 `sessionID` ,如果该节点为持久节点,该值为 0。 -- `dataLength`:节点数据内容的长度。 -- `numChildre`:该节点的子节点个数,如果为临时节点为 0。 -- `pzxid`:该节点子节点列表最后一次被修改时的事务 ID,注意是子节点的 **列表** ,不是内容。 - -### 会话 - -我想这个对于后端开发的朋友肯定不陌生,不就是 `session` 吗?只不过 `zk` 客户端和服务端是通过 **`TCP` 长连接** 维持的会话机制,其实对于会话来说你可以理解为 **保持连接状态** 。 - -在 `zookeeper` 中,会话还有对应的事件,比如 `CONNECTION_LOSS 连接丢失事件`、`SESSION_MOVED 会话转移事件`、`SESSION_EXPIRED 会话超时失效事件` 。 - -### ACL - -`ACL` 为 `Access Control Lists` ,它是一种权限控制。在 `zookeeper` 中定义了 5 种权限,它们分别为: - -- `CREATE`:创建子节点的权限。 -- `READ`:获取节点数据和子节点列表的权限。 -- `WRITE`:更新节点数据的权限。 -- `DELETE`:删除子节点的权限。 -- `ADMIN`:设置节点 ACL 的权限。 - -### Watcher 机制 - -`Watcher` 为事件监听器,是 `zk` 非常重要的一个特性,很多功能都依赖于它,它有点类似于订阅的方式,即客户端向服务端 **注册** 指定的 `watcher` ,当服务端符合了 `watcher` 的某些事件或要求则会 **向客户端发送事件通知** ,客户端收到通知后找到自己定义的 `Watcher` 然后 **执行相应的回调方法** 。 - -![watcher机制](https://oss.javaguide.cn/p3-juejin/ac87b7cff7b44c63997ff0f6a7b6d2eb~tplv-k3u1fbpfcp-zoom-1.jpeg) - -## Zookeeper 的几个典型应用场景 - -前面说了这么多的理论知识,你可能听得一头雾水,这些玩意有啥用?能干啥事?别急,听我慢慢道来。 - -![](https://oss.javaguide.cn/p3-juejin/dbc1a52b0c304bb093ef08fb1d4c704c~tplv-k3u1fbpfcp-zoom-1.jpeg) - -### 选主 - -还记得上面我们的所说的临时节点吗?因为 `Zookeeper` 的强一致性,能够很好地在保证 **在高并发的情况下保证节点创建的全局唯一性** (即无法重复创建同样的节点)。 - -利用这个特性,我们可以 **让多个客户端创建一个指定的节点** ,创建成功的就是 `master`。 - -但是,如果这个 `master` 挂了怎么办??? - -你想想为什么我们要创建临时节点?还记得临时节点的生命周期吗?`master` 挂了是不是代表会话断了?会话断了是不是意味着这个节点没了?还记得 `watcher` 吗?我们是不是可以 **让其他不是 `master` 的节点监听节点的状态** ,比如说我们监听这个临时节点的父节点,如果子节点个数变了就代表 `master` 挂了,这个时候我们 **触发回调函数进行重新选举** ,或者我们直接监听节点的状态,我们可以通过节点是否已经失去连接来判断 `master` 是否挂了等等。 - -![选主](https://oss.javaguide.cn/p3-juejin/00468757fb8f4f51875f645fbb7b25a2~tplv-k3u1fbpfcp-zoom-1.jpeg) - -总的来说,我们可以完全 **利用 临时节点、节点状态 和 `watcher` 来实现选主的功能**,临时节点主要用来选举,节点状态和`watcher` 可以用来判断 `master` 的活性和进行重新选举。 - -### 数据发布/订阅 - -还记得 Zookeeper 的 `Watcher` 机制吗? Zookeeper 通过这种推拉相结合的方式实现客户端与服务端的交互:客户端向服务端注册节点,一旦相应节点的数据变更,服务端就会向“监听”该节点的客户端发送 `Watcher` 事件通知,客户端接收到通知后需要 **主动** 到服务端获取最新的数据。基于这种方式,Zookeeper 实现了 **数据发布/订阅** 功能。 - -一个典型的应用场景为 **全局配置信息的集中管理**。 客户端在启动时会主动到 Zookeeper 服务端获取配置信息,同时 **在指定节点注册一个** `Watcher` **监听**。当配置信息发生变更,服务端通知所有订阅的客户端重新获取配置信息,实现配置信息的实时更新。 - -上面所提到的全局配置信息通常包括机器列表信息、运行时的开关配置、数据库配置信息等。需要注意的是,这类全局配置信息通常具备以下特性: - -- 数据量较小 -- 数据内容在运行时动态变化 -- 集群中机器共享一致配置 - -### 负载均衡 - -可以通过 Zookeeper 的 **临时节点** 实现负载均衡。回顾一下临时节点的特性:当创建节点的客户端与服务端之间断开连接,即客户端会话(session)消失时,对应节点也会自动消失。因此,我们可以使用临时节点来维护 Server 的地址列表,从而保证请求不会被分配到已停机的服务上。 - -具体地,我们需要在集群的每一个 Server 中都使用 Zookeeper 客户端连接 Zookeeper 服务端,同时用 Server **自身的地址信息**在服务端指定目录下创建临时节点。当客户端请求调用集群服务时,首先通过 Zookeeper 获取该目录下的节点列表 (即所有可用的 Server),随后根据不同的负载均衡策略将请求转发到某一具体的 Server。 - -### 分布式锁 - -分布式锁的实现方式有很多种,比如 `Redis`、数据库、`zookeeper` 等。个人认为 `zookeeper` 在实现分布式锁这方面是非常非常简单的。 - -上面我们已经提到过了 **zk 在高并发的情况下保证节点创建的全局唯一性**,这玩意一看就知道能干啥了。实现互斥锁呗,又因为能在分布式的情况下,所以能实现分布式锁呗。 - -如何实现呢?这玩意其实跟选主基本一样,我们也可以利用临时节点的创建来实现。 - -首先肯定是如何获取锁,因为创建节点的唯一性,我们可以让多个客户端同时创建一个临时节点,**创建成功的就说明获取到了锁** 。然后没有获取到锁的客户端也像上面选主的非主节点创建一个 `watcher` 进行节点状态的监听,如果这个互斥锁被释放了(可能获取锁的客户端宕机了,或者那个客户端主动释放了锁)可以调用回调函数重新获得锁。 - -> `zk` 中不需要向 `redis` 那样考虑锁得不到释放的问题了,因为当客户端挂了,节点也挂了,锁也释放了。是不是很简单? - -那能不能使用 `zookeeper` 同时实现 **共享锁和独占锁** 呢?答案是可以的,不过稍微有点复杂而已。 - -还记得 **有序的节点** 吗? - -这个时候我规定所有创建节点必须有序,当你是读请求(要获取共享锁)的话,如果 **没有比自己更小的节点,或比自己小的节点都是读请求** ,则可以获取到读锁,然后就可以开始读了。**若比自己小的节点中有写请求** ,则当前客户端无法获取到读锁,只能等待前面的写请求完成。 - -如果你是写请求(获取独占锁),若 **没有比自己更小的节点** ,则表示当前客户端可以直接获取到写锁,对数据进行修改。若发现 **有比自己更小的节点,无论是读操作还是写操作,当前客户端都无法获取到写锁** ,等待所有前面的操作完成。 - -这就很好地同时实现了共享锁和独占锁,当然还有优化的地方,比如当一个锁得到释放它会通知所有等待的客户端从而造成 **羊群效应** 。此时你可以通过让等待的节点只监听他们前面的节点。 - -具体怎么做呢?其实也很简单,你可以让 **读请求监听比自己小的最后一个写请求节点,写请求只监听比自己小的最后一个节点** ,感兴趣的小伙伴可以自己去研究一下。 - -### 命名服务 - -如何给一个对象设置 ID,大家可能都会想到 `UUID`,但是 `UUID` 最大的问题就在于它太长了。。。(太长不一定是好事,嘿嘿嘿)。那么在条件允许的情况下,我们能不能使用 `zookeeper` 来实现呢? - -我们之前提到过 `zookeeper` 是通过 **树形结构** 来存储数据节点的,那也就是说,对于每个节点的 **全路径**,它必定是唯一的,我们可以使用节点的全路径作为命名方式了。而且更重要的是,路径是我们可以自己定义的,这对于我们对有些有语意的对象的 ID 设置可以更加便于理解。 - -### 集群管理和注册中心 - -看到这里是不是觉得 `zookeeper` 实在是太强大了,它怎么能这么能干! - -别急,它能干的事情还很多呢。可能我们会有这样的需求,我们需要了解整个集群中有多少机器在工作,我们想对集群中的每台机器的运行时状态进行数据采集,对集群中机器进行上下线操作等等。 - -而 `zookeeper` 天然支持的 `watcher` 和 临时节点能很好的实现这些需求。我们可以为每条机器创建临时节点,并监控其父节点,如果子节点列表有变动(我们可能创建删除了临时节点),那么我们可以使用在其父节点绑定的 `watcher` 进行状态监控和回调。 - -![集群管理](https://oss.javaguide.cn/p3-juejin/f3d70709f10f4fa6b09125a56a976fda~tplv-k3u1fbpfcp-zoom-1.jpeg) - -至于注册中心也很简单,我们同样也是让 **服务提供者** 在 `zookeeper` 中创建一个临时节点并且将自己的 `ip、port、调用方式` 写入节点,当 **服务消费者** 需要进行调用的时候会 **通过注册中心找到相应的服务的地址列表(IP 端口什么的)** ,并缓存到本地(方便以后调用),当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。 - -当服务提供者的某台服务器宕机或下线时,相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(当然你可以让消费者进行节点监听,我记得 `Eureka` 会先试错,然后再更新)。 - -![注册中心](https://oss.javaguide.cn/p3-juejin/469cebf9670740d1a6711fe54db70e05~tplv-k3u1fbpfcp-zoom-1.jpeg) - -## 总结 - -看到这里的同学实在是太有耐心了 👍👍👍 不知道大家是否还记得我讲了什么 😒。 - -![](https://oss.javaguide.cn/p3-juejin/912c1aa6b7794d4aac8ebe6a14832cae~tplv-k3u1fbpfcp-zoom-1.jpeg) - -这篇文章中我带大家入门了 `zookeeper` 这个强大的分布式协调框架。现在我们来简单梳理一下整篇文章的内容。 - -- 分布式与集群的区别 - -- `2PC`、`3PC` 以及 `paxos` 算法这些一致性框架的原理和实现。 - -- `zookeeper` 专门的一致性算法 `ZAB` 原子广播协议的内容(`Leader` 选举、崩溃恢复、消息广播)。 - -- `zookeeper` 中的一些基本概念,比如 `ACL`,数据节点,会话,`watcher`机制等等。 - -- `zookeeper` 的典型应用场景,比如选主,注册中心等等。 - - 如果忘了可以回去看看再次理解一下,如果有疑问和建议欢迎提出 🤝🤝🤝。 - - +To solve the data consistency problem, many consistency protocols diff --git a/docs/distributed-system/distributed-transaction.md b/docs/distributed-system/distributed-transaction.md index fa4c83c743c..decdd90f1fe 100644 --- a/docs/distributed-system/distributed-transaction.md +++ b/docs/distributed-system/distributed-transaction.md @@ -1,9 +1,9 @@ --- -title: 分布式事务常见解决方案总结(付费) -category: 分布式 +title: Summary of Common Solutions for Distributed Transactions (Paid) +category: Distributed --- -**分布式事务** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了《Java 面试指北》中。 +**Distributed Transactions** interview questions are exclusive content for my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) (click the link to view detailed information and joining methods) and have been organized into the "Java Interview Guide." ![](https://oss.javaguide.cn/javamianshizhibei/distributed-system.png) diff --git a/docs/distributed-system/protocol/cap-and-base-theorem.md b/docs/distributed-system/protocol/cap-and-base-theorem.md index 36a2fa54d4a..eeb5fbb9193 100644 --- a/docs/distributed-system/protocol/cap-and-base-theorem.md +++ b/docs/distributed-system/protocol/cap-and-base-theorem.md @@ -1,161 +1,161 @@ --- -title: CAP & BASE理论详解 -category: 分布式 +title: Explanation of CAP & BASE Theories +category: Distributed Systems tag: - - 分布式理论 + - Distributed Theory --- -经历过技术面试的小伙伴想必对 CAP & BASE 这个两个理论已经再熟悉不过了! +Those who have experienced technical interviews are likely very familiar with the CAP & BASE theories! -我当年参加面试的时候,不夸张地说,只要问到分布式相关的内容,面试官几乎是必定会问这两个分布式相关的理论。一是因为这两个分布式基础理论是学习分布式知识的必备前置基础,二是因为很多面试官自己比较熟悉这两个理论(方便提问)。 +When I participated in interviews back then, I can say without exaggeration that as soon as distributed-related topics came up, interviewers almost invariably asked about these two distributed theories. This is partly because these two foundational theories are essential prerequisites for learning about distributed systems, and partly because many interviewers themselves are familiar with these theories (making it easier to ask questions). -我们非常有必要将这两个理论搞懂,并且能够用自己的理解给别人讲出来。 +It is very necessary for us to understand these two theories and be able to explain them to others in our own words. -## CAP 理论 +## CAP Theory -[CAP 理论/定理](https://zh.wikipedia.org/wiki/CAP%E5%AE%9A%E7%90%86)起源于 2000 年,由加州大学伯克利分校的 Eric Brewer 教授在分布式计算原理研讨会(PODC)上提出,因此 CAP 定理又被称作 **布鲁尔定理(Brewer’s theorem)** +[CAP Theorem](https://zh.wikipedia.org/wiki/CAP%E5%AE%9A%E7%90%86) originated in 2000, proposed by Professor Eric Brewer from the University of California, Berkeley at the Principles of Distributed Computing (PODC) symposium. Therefore, the CAP theorem is also known as **Brewer's theorem**. -2 年后,麻省理工学院的 Seth Gilbert 和 Nancy Lynch 发表了布鲁尔猜想的证明,CAP 理论正式成为分布式领域的定理。 +Two years later, Seth Gilbert and Nancy Lynch from MIT published a proof of Brewer's conjecture, and the CAP theory officially became a theorem in the distributed field. -### 简介 +### Introduction -**CAP** 也就是 **Consistency(一致性)**、**Availability(可用性)**、**Partition Tolerance(分区容错性)** 这三个单词首字母组合。 +**CAP** is a combination of the first letters of **Consistency**, **Availability**, and **Partition Tolerance**. ![](https://oss.javaguide.cn/2020-11/cap.png) -CAP 理论的提出者布鲁尔在提出 CAP 猜想的时候,并没有详细定义 **Consistency**、**Availability**、**Partition Tolerance** 三个单词的明确定义。 +When Brewer proposed the CAP conjecture, he did not provide a detailed definition of **Consistency**, **Availability**, and **Partition Tolerance**. -因此,对于 CAP 的民间解读有很多,一般比较被大家推荐的是下面 👇 这种版本的解读。 +As a result, there are many informal interpretations of CAP, with the most commonly recommended interpretation being the one below 👇. -在理论计算机科学中,CAP 定理(CAP theorem)指出对于一个分布式系统来说,当设计读写操作时,只能同时满足以下三点中的两个: +In theoretical computer science, the CAP theorem states that for a distributed system, when designing read and write operations, it can only simultaneously satisfy two of the following three points: -- **一致性(Consistency)** : 所有节点访问同一份最新的数据副本 -- **可用性(Availability)**: 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。 -- **分区容错性(Partition Tolerance)** : 分布式系统出现网络分区的时候,仍然能够对外提供服务。 +- **Consistency**: All nodes access the same up-to-date data copy. +- **Availability**: Non-faulty nodes return reasonable responses (not errors or timeouts) within a reasonable time. +- **Partition Tolerance**: The system continues to provide services despite network partitions. -**什么是网络分区?** +**What is a network partition?** -分布式系统中,多个节点之间的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫 **网络分区**。 +In a distributed system, multiple nodes are originally connected, but due to some faults (like issues with part of the node's network), some nodes become unreachable, and the entire network splits into several regions. This is called a **network partition**. ![partition-tolerance](https://oss.javaguide.cn/2020-11/partition-tolerance.png) -### 不是所谓的“3 选 2” +### Not a "choose 2 of 3" -大部分人解释这一定律时,常常简单的表述为:“一致性、可用性、分区容忍性三者你只能同时达到其中两个,不可能同时达到”。实际上这是一个非常具有误导性质的说法,而且在 CAP 理论诞生 12 年之后,CAP 之父也在 2012 年重写了之前的论文。 +Most people simplistically explain this theorem by saying: "You can only achieve two of consistency, availability, and partition tolerance at the same time, and cannot achieve all three." This is actually a very misleading statement, and twelve years after the CAP theory was born, its father also rewrote his earlier paper in 2012. -> **当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。** +> **When a network partition occurs, if we are to continue providing services, then strong consistency and availability can only be chosen between 2 out of 1. In other words, when a network partition happens, P is the prerequisite, determining the choice of C or A thereafter. This means partition tolerance (P) is something we must implement.** > -> 简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。 +> In short: In the CAP theory, partition tolerance P must be satisfied, and on this basis, we can only satisfy either availability A or consistency C. -因此,**分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。** 比如 ZooKeeper、HBase 就是 CP 架构,Cassandra、Eureka 就是 AP 架构,Nacos 不仅支持 CP 架构也支持 AP 架构。 +Therefore, **theoretically, a distributed system cannot choose a CA architecture but can only choose a CP or AP architecture.** For example, ZooKeeper and HBase are CP architectures, while Cassandra and Eureka are AP architectures. Nacos supports both CP and AP architectures. -**为啥不可能选择 CA 架构呢?** 举个例子:若系统出现“分区”,系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他节点的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。 +**Why is it impossible to choose a CA architecture?** For example: If a "partition" occurs in the system and a certain node is performing a write operation. To ensure C, we must prohibit other nodes’ read and write operations, which conflicts with A. Conversely, if we ensure A and allow other nodes to perform read and write operations, that would conflict with C. -**选择 CP 还是 AP 的关键在于当前的业务场景,没有定论,比如对于需要确保强一致性的场景如银行一般会选择保证 CP 。** +**The choice between CP and AP depends on the current business scenario, with no conclusive answer. For scenarios requiring strong consistency, such as banking, CP is generally chosen.** -另外,需要补充说明的一点是:**如果网络分区正常的话(系统在绝大部分时候所处的状态),也就说不需要保证 P 的时候,C 和 A 能够同时保证。** +Additionally, it should be noted that: **If the network partition is functioning normally (which is the state the system is in most of the time), that is, when P does not need to be ensured, C and A can be guaranteed simultaneously.** -### CAP 实际应用案例 +### Practical Application of CAP -我这里以注册中心来探讨一下 CAP 的实际应用。考虑到很多小伙伴不知道注册中心是干嘛的,这里简单以 Dubbo 为例说一说。 +I will explore the practical application of CAP using a registration center. Considering many may not know what a registration center is for, I will briefly explain it using Dubbo as an example. -下图是 Dubbo 的架构图。**注册中心 Registry 在其中扮演了什么角色呢?提供了什么服务呢?** +The following diagram is the architecture of Dubbo. **What role does the registration center play? What services does it provide?** -注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。 +The registration center is responsible for the registration and discovery of service addresses, analogous to a directory service. Service providers and consumers only interact with the registration center upon starting, and the registration center does not forward requests, resulting in low pressure. ![](https://oss.javaguide.cn/2020-11/dubbo-architecture.png) -常见的可以作为注册中心的组件有:ZooKeeper、Eureka、Nacos...。 +Common components that can serve as registration centers include: ZooKeeper, Eureka, Nacos... -1. **ZooKeeper 保证的是 CP。** 任何时刻对 ZooKeeper 的读请求都能得到一致性的结果,但是, ZooKeeper 不保证每次请求的可用性比如在 Leader 选举过程中或者半数以上的机器不可用的时候服务就是不可用的。 -2. **Eureka 保证的则是 AP。** Eureka 在设计的时候就是优先保证 A (可用性)。在 Eureka 中不存在什么 Leader 节点,每个节点都是一样的、平等的。因此 Eureka 不会像 ZooKeeper 那样出现选举过程中或者半数以上的机器不可用的时候服务就是不可用的情况。 Eureka 保证即使大部分节点挂掉也不会影响正常提供服务,只要有一个节点是可用的就行了。只不过这个节点上的数据可能并不是最新的。 -3. **Nacos 不仅支持 CP 也支持 AP。** +1. **ZooKeeper guarantees CP.** Any read request to ZooKeeper will provide a consistent result; however, ZooKeeper does not guarantee availability during events like Leader election or when more than half of the machines are unavailable—services would then be unavailable. +1. **Eureka guarantees AP.** Eureka was designed with priority on A (availability). There is no Leader node in Eureka; each node is the same and equal. Thus, unlike ZooKeeper, Eureka does not face unavailability during election processes or when more than half of the machines are unavailable. Eureka ensures that as long as one node is available, normal services can continue, even if that node's data may not be the latest. +1. **Nacos supports both CP and AP.** -**🐛 修正(参见:[issue#1906](https://github.com/Snailclimb/JavaGuide/issues/1906))**: +**🐛 Correction (see: [issue#1906](https://github.com/Snailclimb/JavaGuide/issues/1906))**: -ZooKeeper 通过可线性化(Linearizable)写入、全局 FIFO 顺序访问等机制来保障数据一致性。多节点部署的情况下, ZooKeeper 集群处于 Quorum 模式。Quorum 模式下的 ZooKeeper 集群, 是一组 ZooKeeper 服务器节点组成的集合,其中大多数节点必须同意任何变更才能被视为有效。 +ZooKeeper ensures data consistency through linearizable writes, global FIFO access, and other mechanisms. In a multi-node deployment, the ZooKeeper cluster operates in Quorum mode. In this mode, a group of ZooKeeper server nodes is formed, where the majority must agree to any changes for them to be considered valid. -由于 Quorum 模式下的读请求不会触发各个 ZooKeeper 节点之间的数据同步,因此在某些情况下还是可能会存在读取到旧数据的情况,导致不同的客户端视图上看到的结果不同,这可能是由于网络延迟、丢包、重传等原因造成的。ZooKeeper 为了解决这个问题,提供了 Watcher 机制和版本号机制来帮助客户端检测数据的变化和版本号的变更,以保证数据的一致性。 +Due to the nature of Quorum mode, read requests do not trigger data synchronization between different ZooKeeper nodes, resulting in situations where outdated data may still be read, leading to inconsistent views on different clients due to reasons like network latency, packet loss, or retransmission. To address this, ZooKeeper provides a Watcher mechanism and versioning to help clients detect changes in data and version changes to ensure data consistency. -### 总结 +### Conclusion -在进行分布式系统设计和开发时,我们不应该仅仅局限在 CAP 问题上,还要关注系统的扩展性、可用性等等 +In designing and developing distributed systems, we should not be limited to the CAP issue but also focus on aspects such as system scalability and availability. -在系统发生“分区”的情况下,CAP 理论只能满足 CP 或者 AP。要注意的是,这里的前提是系统发生了“分区” +In the event of a network "partition", the CAP theorem can only satisfy CP or AP. It is important to note that this assumes a "partition" has occurred. -如果系统没有发生“分区”的话,节点间的网络连接通信正常的话,也就不存在 P 了。这个时候,我们就可以同时保证 C 和 A 了。 +If there is no "partition" in the system and the network communication among nodes is normal, P no longer exists. At that point, we can simultaneously ensure C and A. -总结:**如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。** +In summary: **If a partition occurs in the system, we must consider whether to choose CP or AP. If there is no partition, we should think about how to ensure CA.** -### 推荐阅读 +### Recommended Reading -1. [CAP 定理简化](https://medium.com/@ravindraprasad/cap-theorem-simplified-28499a67eab4) (英文,有趣的案例) -2. [神一样的 CAP 理论被应用在何方](https://juejin.im/post/6844903936718012430) (中文,列举了很多实际的例子) -3. [请停止呼叫数据库 CP 或 AP](https://martin.kleppmann.com/2015/05/11/please-stop-calling-databases-cp-or-ap.html) (英文,带给你不一样的思考) +1. [Simplifying the CAP Theorem](https://medium.com/@ravindraprasad/cap-theorem-simplified-28499a67eab4) (English, with interesting examples) +1. [Where is the Amazing CAP Theory Applied](https://juejin.im/post/6844903936718012430) (Chinese, listing many practical examples) +1. [Please Stop Calling Databases CP or AP](https://martin.kleppmann.com/2015/05/11/please-stop-calling-databases-cp-or-ap.html) (English, offering a different perspective) -## BASE 理论 +## BASE Theory -[BASE 理论](https://dl.acm.org/doi/10.1145/1394127.1394128)起源于 2008 年, 由 eBay 的架构师 Dan Pritchett 在 ACM 上发表。 +[BASE Theory](https://dl.acm.org/doi/10.1145/1394127.1394128) originated in 2008 and was published by eBay architect Dan Pritchett in ACM. -### 简介 +### Introduction -**BASE** 是 **Basically Available(基本可用)**、**Soft-state(软状态)** 和 **Eventually Consistent(最终一致性)** 三个短语的缩写。BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。 +**BASE** is an acronym for **Basically Available**, **Soft-state**, and **Eventually Consistent**. The BASE theory is the result of the trade-offs between consistency (C) and availability (A) in CAP, arising from a summary of distributed practices in large-scale Internet systems. It has gradually evolved from the CAP theorem, significantly relaxing our requirements of the system. -### BASE 理论的核心思想 +### Core Idea of BASE Theory -即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 +Even if strong consistency cannot be achieved, every application can adopt appropriate methods to achieve eventual consistency according to its business characteristics. -> 也就是牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体“主要可用”。 +> This means sacrificing data consistency to ensure high system availability, wherein part of the system's data may be unavailable or inconsistent, yet the system overall still remains "mostly available". -**BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。** +**The BASE theory is essentially an extension and supplement to CAP, specifically, it supplements the AP solution in CAP.** -**为什么这样说呢?** +**Why do I say this?** -CAP 理论这节我们也说过了: +As mentioned in the section on the CAP theory: -> 如果系统没有发生“分区”的话,节点间的网络连接通信正常的话,也就不存在 P 了。这个时候,我们就可以同时保证 C 和 A 了。因此,**如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。** +> If the system has not undergone a "partition," and the network connections between nodes are functioning normally, then P does not exist. At that point, we can guarantee both C and A. Thus, **if a partition occurs in the system, we must consider choosing between CP or AP. If there is no "partition," we should think about how to ensure CA.** -因此,AP 方案只是在系统发生分区的时候放弃一致性,而不是永远放弃一致性。在分区故障恢复后,系统应该达到最终一致性。这一点其实就是 BASE 理论延伸的地方。 +Therefore, the AP solution only sacrifices consistency during system partitions but does not permanently relinquish consistency. After recovering from partition faults, the system should achieve eventual consistency. This is, in fact, the essence of the extension provided by the BASE theory. -### BASE 理论三要素 +### Three Elements of BASE Theory -![BASE理论三要素](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC81LzI0LzE2MzkxNDgwNmQ5ZTE1YzY?x-oss-process=image/format,png) +![Three Elements of BASE Theory](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC81LzI0LzE2MzkxNDgwNmQ5ZTE1YzY?x-oss-process=image/format,png) -#### 基本可用 +#### Basically Available -基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。 +Basically available means that when unexpected failures occur in a distributed system, it allows some loss of availability. However, this does not equate to the system being unavailable. -**什么叫允许损失部分可用性呢?** +**What does it mean to allow loss of availability?** -- **响应时间上的损失**: 正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3 s。 -- **系统功能上的损失**:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。 +- **Loss in response time**: Normally, it takes 0.5s to handle a user request, but due to system failures, the response time extends to 3s. +- **Loss in system functionality**: Normally, users can access all features of the system, but due to a sudden surge in traffic, some non-core functionalities may become unavailable. -#### 软状态 +#### Soft State -软状态指允许系统中的数据存在中间状态(**CAP 理论中的数据不一致**),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。 +Soft state refers to allowing data in the system to exist in intermediate states (**data inconsistency as described in the CAP theory**) while recognizing that the existence of this intermediate state will not affect the overall availability of the system. This means allowing delays in the synchronization process of data replicas between different nodes. -#### 最终一致性 +#### Eventually Consistent -最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。 +Eventually consistent emphasizes that all data replicas in the system can achieve a consistent state after a period of synchronization. Therefore, the essence of eventual consistency is that the system needs to ensure that the final data can achieve consistency, rather than requiring real-time strong consistency of the data. -> 分布式一致性的 3 种级别: +> The three levels of distributed consistency: > -> 1. **强一致性**:系统写入了什么,读出来的就是什么。 -> 2. **弱一致性**:不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。 -> 3. **最终一致性**:弱一致性的升级版,系统会保证在一定时间内达到数据一致的状态。 +> 1. **Strong consistency**: What is written in the system is exactly what is read. +> 1. **Weak consistency**: It is not certain that the most recently written value can be read, nor is there a guarantee that the data read after a certain time will be the latest; it only aims to achieve data consistency at a given moment. +> 1. **Eventual consistency**: This is an upgraded version of weak consistency, wherein the system guarantees that data will achieve consistency within a certain period. > -> **业界比较推崇是最终一致性级别,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。** +> **The industry tends to commend the eventual consistency level, but in certain scenarios where data consistency is critically required, such as banking transactions, strong consistency must still be guaranteed.** -那实现最终一致性的具体方式是什么呢? [《分布式协议与算法实战》](http://gk.link/a/10rZM) 中是这样介绍: +What are the specific ways to achieve eventual consistency? In [《Distributed Protocols and Algorithms in Practice》](http://gk.link/a/10rZM), it is introduced as follows: -> - **读时修复** : 在读取数据时,检测数据的不一致,进行修复。比如 Cassandra 的 Read Repair 实现,具体来说,在向 Cassandra 系统查询数据的时候,如果检测到不同节点的副本数据不一致,系统就自动修复数据。 -> - **写时修复** : 在写入数据,检测数据的不一致时,进行修复。比如 Cassandra 的 Hinted Handoff 实现。具体来说,Cassandra 集群的节点之间远程写数据的时候,如果写失败 就将数据缓存下来,然后定时重传,修复数据的不一致性。 -> - **异步修复** : 这个是最常用的方式,通过定时对账检测副本数据的一致性,并修复。 +> - **Read Repair**: During data reads, inconsistencies are detected and repaired. For example, Cassandra's Read Repair implementation automatically fixes data when inconsistencies are found among the replicas during a read query. +> - **Write Repair**: During data writes, inconsistencies are detected and repaired. For example, Cassandra's Hinted Handoff implementation caches data that fails to write remotely between cluster nodes and retransmits it periodically to fix the inconsistencies. +> - **Asynchronous Repair**: This is the most commonly used method, conducted through regular reconciliation checks to verify the consistency of replica data and repair it. -比较推荐 **写时修复**,这种方式对性能消耗比较低。 +**Write Repair** is highly recommended as this method is less demanding on performance. -### 总结 +### Conclusion -**ACID 是数据库事务完整性的理论,CAP 是分布式系统设计理论,BASE 是 CAP 理论中 AP 方案的延伸。** +**ACID is a theory of transaction integrity in databases, CAP is a theory of distributed system design, and BASE is an extension of the AP solution in CAP theory.** diff --git a/docs/distributed-system/protocol/gossip-protocl.md b/docs/distributed-system/protocol/gossip-protocl.md index 5590401a9b6..c6db7b9630f 100644 --- a/docs/distributed-system/protocol/gossip-protocl.md +++ b/docs/distributed-system/protocol/gossip-protocl.md @@ -1,145 +1,62 @@ --- -title: Gossip 协议详解 -category: 分布式 +title: Detailed Explanation of the Gossip Protocol +category: Distributed tag: - - 分布式协议&算法 - - 共识算法 + - Distributed Protocols & Algorithms + - Consensus Algorithms --- -## 背景 +## Background -在分布式系统中,不同的节点进行数据/信息共享是一个基本的需求。 +In distributed systems, data/information sharing among different nodes is a fundamental requirement. -一种比较简单粗暴的方法就是 **集中式发散消息**,简单来说就是一个主节点同时共享最新信息给其他所有节点,比较适合中心化系统。这种方法的缺陷也很明显,节点多的时候不光同步消息的效率低,还太依赖与中心节点,存在单点风险问题。 +A relatively simple and straightforward method is **centralized message dissemination**, which essentially involves a primary node sharing the latest information with all other nodes simultaneously, making it more suitable for centralized systems. The drawbacks of this method are also quite evident; when there are many nodes, not only is the efficiency of message synchronization low, but it also heavily relies on the central node, posing a single point of failure risk. -于是,**分散式发散消息** 的 **Gossip 协议** 就诞生了。 +Thus, the **Gossip Protocol** for **decentralized message dissemination** was born. -## Gossip 协议介绍 +## Introduction to the Gossip Protocol -Gossip 直译过来就是闲话、流言蜚语的意思。流言蜚语有什么特点呢?容易被传播且传播速度还快,你传我我传他,然后大家都知道了。 +Gossip literally translates to idle talk or rumors. What are the characteristics of rumors? They are easily spread and propagate quickly; you tell me, I tell him, and then everyone knows. ![](./images/gossip/gossip.png) -**Gossip 协议** 也叫 Epidemic 协议(流行病协议)或者 Epidemic propagation 算法(疫情传播算法),别名很多。不过,这些名字的特点都具有 **随机传播特性** (联想一下病毒传播、癌细胞扩散等生活中常见的情景),这也正是 Gossip 协议最主要的特点。 +The **Gossip Protocol** is also known as the Epidemic Protocol or Epidemic Propagation Algorithm, and it has many aliases. However, the common feature of these names is the **random propagation characteristic** (think of scenarios like virus transmission or cancer cell spread in everyday life), which is also the main feature of the Gossip Protocol. -Gossip 协议最早是在 ACM 上的一篇 1987 年发表的论文 [《Epidemic Algorithms for Replicated Database Maintenance》](https://dl.acm.org/doi/10.1145/41840.41841)中被提出的。根据论文标题,我们大概就能知道 Gossip 协议当时提出的主要应用是在分布式数据库系统中各个副本节点同步数据。 +The Gossip Protocol was first proposed in a paper published in 1987 in ACM titled [“Epidemic Algorithms for Replicated Database Maintenance”](https://dl.acm.org/doi/10.1145/41840.41841). From the title of the paper, we can infer that the main application of the Gossip Protocol at that time was to synchronize data among various replica nodes in distributed database systems. -正如 Gossip 协议其名一样,这是一种随机且带有传染性的方式将信息传播到整个网络中,并在一定时间内,使得系统内的所有节点数据一致。 +As the name suggests, the Gossip Protocol is a random and contagious way to disseminate information throughout the network, ensuring that all nodes in the system achieve data consistency within a certain time frame. -在 Gossip 协议下,没有所谓的中心节点,每个节点周期性地随机找一个节点互相同步彼此的信息,理论上来说,各个节点的状态最终会保持一致。 +Under the Gossip Protocol, there is no so-called central node; each node periodically and randomly selects another node to synchronize information with each other. Theoretically, the states of all nodes will eventually remain consistent. -下面我们来对 Gossip 协议的定义做一个总结:**Gossip 协议是一种允许在分布式系统中共享状态的去中心化通信协议,通过这种通信协议,我们可以将信息传播给网络或集群中的所有成员。** +To summarize the definition of the Gossip Protocol: **The Gossip Protocol is a decentralized communication protocol that allows state sharing in distributed systems, enabling us to disseminate information to all members in a network or cluster.** -## Gossip 协议应用 +## Applications of the Gossip Protocol -NoSQL 数据库 Redis 和 Apache Cassandra、服务网格解决方案 Consul 等知名项目都用到了 Gossip 协议,学习 Gossip 协议有助于我们搞清很多技术的底层原理。 +Notable projects such as the NoSQL databases Redis and Apache Cassandra, as well as service mesh solutions like Consul, utilize the Gossip Protocol. Learning about the Gossip Protocol helps us understand the underlying principles of many technologies. -我们这里以 Redis Cluster 为例说明 Gossip 协议的实际应用。 +Here, we will illustrate the practical application of the Gossip Protocol using Redis Cluster as an example. -我们经常使用的分布式缓存 Redis 的官方集群解决方案(3.0 版本引入) Redis Cluster 就是基于 Gossip 协议来实现集群中各个节点数据的最终一致性。 +The official clustering solution for the distributed cache Redis (introduced in version 3.0), Redis Cluster, is based on the Gossip Protocol to achieve eventual consistency of data among various nodes in the cluster. -![Redis 的官方集群解决方案](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/up-fcacc1eefca6e51354a5f1fc9f2919f51ec.png) +![Official Clustering Solution of Redis](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/up-fcacc1eefca6e51354a5f1fc9f2919f51ec.png) -Redis Cluster 是一个典型的分布式系统,分布式系统中的各个节点需要互相通信。既然要相互通信就要遵循一致的通信协议,Redis Cluster 中的各个节点基于 **Gossip 协议** 来进行通信共享信息,每个 Redis 节点都维护了一份集群的状态信息。 +Redis Cluster is a typical distributed system where various nodes need to communicate with each other. Since communication is required, a consistent communication protocol must be followed. The nodes in Redis Cluster communicate and share information based on the **Gossip Protocol**, with each Redis node maintaining a copy of the cluster's state information. -Redis Cluster 的节点之间会相互发送多种 Gossip 消息: +Nodes in Redis Cluster send various Gossip messages to each other: -- **MEET**:在 Redis Cluster 中的某个 Redis 节点上执行 `CLUSTER MEET ip port` 命令,可以向指定的 Redis 节点发送一条 MEET 信息,用于将其添加进 Redis Cluster 成为新的 Redis 节点。 -- **PING/PONG**:Redis Cluster 中的节点都会定时地向其他节点发送 PING 消息,来交换各个节点状态信息,检查各个节点状态,包括在线状态、疑似下线状态 PFAIL 和已下线状态 FAIL。 -- **FAIL**:Redis Cluster 中的节点 A 发现 B 节点 PFAIL ,并且在下线报告的有效期限内集群中半数以上的节点将 B 节点标记为 PFAIL,节点 A 就会向集群广播一条 FAIL 消息,通知其他节点将故障节点 B 标记为 FAIL 。 +- **MEET**: Executing the `CLUSTER MEET ip port` command on a Redis node in Redis Cluster sends a MEET message to a specified Redis node, adding it as a new Redis node in the cluster. +- **PING/PONG**: Nodes in Redis Cluster periodically send PING messages to other nodes to exchange status information, checking the status of each node, including online status, suspected offline status (PFAIL), and offline status (FAIL). +- **FAIL**: If node A in Redis Cluster detects that node B is PFAIL, and more than half of the nodes in the cluster mark B as PFAIL within the validity period of the offline report, node A will broadcast a FAIL message to the cluster, notifying other nodes to mark the faulty node B as FAIL. - …… -下图就是主从架构的 Redis Cluster 的示意图,图中的虚线代表的就是各个节点之间使用 Gossip 进行通信 ,实线表示主从复制。 +The diagram below illustrates the master-slave architecture of Redis Cluster, where the dashed lines represent communication between nodes using Gossip, and the solid lines indicate master-slave replication. ![](./images/gossip/redis-cluster-gossip.png) -有了 Redis Cluster 之后,不需要专门部署 Sentinel 集群服务了。Redis Cluster 相当于是内置了 Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议共享集群内信息。 +With Redis Cluster, there is no need to deploy a separate Sentinel cluster service. Redis Cluster effectively has a built-in Sentinel mechanism, with the various Redis nodes internally sharing cluster information through the Gossip Protocol. -关于 Redis Cluster 的详细介绍,可以查看这篇文章 [Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html) 。 +For a detailed introduction to Redis Cluster, you can refer to this article [Detailed Explanation of Redis Cluster (Paid)](https://javaguide.cn/database/redis/redis-cluster.html). -## Gossip 协议消息传播模式 +## Gossip Protocol Message Propagation Modes -Gossip 设计了两种可能的消息传播模式:**反熵(Anti-Entropy)** 和 **传谣(Rumor-Mongering)**。 - -### 反熵(Anti-entropy) - -根据维基百科: - -> 熵的概念最早起源于[物理学](https://zh.wikipedia.org/wiki/物理学),用于度量一个热力学系统的混乱程度。熵最好理解为不确定性的量度而不是确定性的量度,因为越随机的信源的熵越大。 - -在这里,你可以把反熵中的熵理解为节点之间数据的混乱程度/差异性,反熵就是指消除不同节点中数据的差异,提升节点间数据的相似度,从而降低熵值。 - -具体是如何反熵的呢?集群中的节点,每隔段时间就随机选择某个其他节点,然后通过互相交换自己的所有数据来消除两者之间的差异,实现数据的最终一致性。 - -在实现反熵的时候,主要有推、拉和推拉三种方式: - -- 推方式,就是将自己的所有副本数据,推给对方,修复对方副本中的熵。 -- 拉方式,就是拉取对方的所有副本数据,修复自己副本中的熵。 -- 推拉就是同时修复自己副本和对方副本中的熵。 - -伪代码如下: - -![反熵伪代码](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/up-df16e98bf71e872a7e1f01ca31cee93d77b.png) - -在我们实际应用场景中,一般不会采用随机的节点进行反熵,而是可以设计成一个闭环。这样的话,我们能够在一个确定的时间范围内实现各个节点数据的最终一致性,而不是基于随机的概率。像 InfluxDB 就是这样来实现反熵的。 - -![](./images/gossip/反熵-闭环.png) - -1. 节点 A 推送数据给节点 B,节点 B 获取到节点 A 中的最新数据。 -2. 节点 B 推送数据给 C,节点 C 获取到节点 A,B 中的最新数据。 -3. 节点 C 推送数据给 A,节点 A 获取到节点 B,C 中的最新数据。 -4. 节点 A 再推送数据给 B 形成闭环,这样节点 B 就获取到节点 C 中的最新数据。 - -虽然反熵很简单实用,但是,节点过多或者节点动态变化的话,反熵就不太适用了。这个时候,我们想要实现最终一致性就要靠 **谣言传播(Rumor mongering)** 。 - -### 谣言传播(Rumor mongering) - -谣言传播指的是分布式系统中的一个节点一旦有了新数据之后,就会变为活跃节点,活跃节点会周期性地联系其他节点向其发送新数据,直到所有的节点都存储了该新数据。 - -如下图所示(下图来自于[INTRODUCTION TO GOSSIP](https://managementfromscratch.wordpress.com/2016/04/01/introduction-to-gossip/) 这篇文章): - -![Gossip 传播示意图](./images/gossip/gossip-rumor-mongering.gif) - -伪代码如下: - -![](https://oss.javaguide.cn/github/javaguide/csdn/20210605170707933.png) - -谣言传播比较适合节点数量比较多的情况,不过,这种模式下要尽量避免传播的信息包不能太大,避免网络消耗太大。 - -### 总结 - -- 反熵(Anti-Entropy)会传播节点的所有数据,而谣言传播(Rumor-Mongering)只会传播节点新增的数据。 -- 我们一般会给反熵设计一个闭环。 -- 谣言传播(Rumor-Mongering)比较适合节点数量比较多或者节点动态变化的场景。 - -## Gossip 协议优势和缺陷 - -**优势:** - -1、相比于其他分布式协议/算法来说,Gossip 协议理解起来非常简单。 - -2、能够容忍网络上节点的随意地增加或者减少,宕机或者重启,因为 Gossip 协议下这些节点都是平等的,去中心化的。新增加或者重启的节点在理想情况下最终是一定会和其他节点的状态达到一致。 - -3、速度相对较快。节点数量比较多的情况下,扩散速度比一个主节点向其他节点传播信息要更快(多播)。 - -**缺陷** : - -1、消息需要通过多个传播的轮次才能传播到整个网络中,因此,必然会出现各节点状态不一致的情况。毕竟,Gossip 协议强调的是最终一致,至于达到各个节点的状态一致需要多长时间,谁也无从得知。 - -2、由于拜占庭将军问题,不允许存在恶意节点。 - -3、可能会出现消息冗余的问题。由于消息传播的随机性,同一个节点可能会重复收到相同的消息。 - -## 总结 - -- Gossip 协议是一种允许在分布式系统中共享状态的通信协议,通过这种通信协议,我们可以将信息传播给网络或集群中的所有成员。 -- Gossip 协议被 Redis、Apache Cassandra、Consul 等项目应用。 -- 谣言传播(Rumor-Mongering)比较适合节点数量比较多或者节点动态变化的场景。 - -## 参考 - -- 一万字详解 Redis Cluster Gossip 协议: -- 《分布式协议与算法实战》 -- 《Redis 设计与实现》 - - +Gossip has designed two possible message propagation modes: \*\*Anti-Entropy diff --git a/docs/distributed-system/protocol/paxos-algorithm.md b/docs/distributed-system/protocol/paxos-algorithm.md index c820209f4a8..d1e5d437f71 100644 --- a/docs/distributed-system/protocol/paxos-algorithm.md +++ b/docs/distributed-system/protocol/paxos-algorithm.md @@ -1,83 +1,83 @@ --- -title: Paxos 算法详解 -category: 分布式 +title: Detailed Explanation of the Paxos Algorithm +category: Distributed tag: - - 分布式协议&算法 - - 共识算法 + - Distributed Protocols & Algorithms + - Consensus Algorithms --- -## 背景 +## Background -Paxos 算法是 Leslie Lamport([莱斯利·兰伯特](https://zh.wikipedia.org/wiki/莱斯利·兰伯特))在 **1990** 年提出了一种分布式系统 **共识** 算法。这也是第一个被证明完备的共识算法(前提是不存在拜占庭将军问题,也就是没有恶意节点)。 +The Paxos algorithm is a consensus algorithm for distributed systems proposed by Leslie Lamport in **1990**. It is also the first consensus algorithm to be proven to be complete (under the assumption that the Byzantine Generals problem does not exist, meaning there are no malicious nodes). -为了介绍 Paxos 算法,兰伯特专门写了一篇幽默风趣的论文。在这篇论文中,他虚拟了一个叫做 Paxos 的希腊城邦来更形象化地介绍 Paxos 算法。 +To introduce the Paxos algorithm, Lamport wrote a humorous and witty paper. In this paper, he created a fictional Greek city-state called Paxos to more vividly illustrate the Paxos algorithm. -不过,审稿人并不认可这篇论文的幽默。于是,他们就给兰伯特说:“如果你想要成功发表这篇论文的话,必须删除所有 Paxos 相关的故事背景”。兰伯特一听就不开心了:“我凭什么修改啊,你们这些审稿人就是缺乏幽默细胞,发不了就不发了呗!”。 +However, the reviewers did not appreciate the humor in the paper. They told Lamport, "If you want to successfully publish this paper, you must remove all the background stories related to Paxos." Lamport was not pleased and replied, "Why should I make changes? You reviewers are simply lacking a sense of humor. If you can't publish it, then do not publish it!" -于是乎,提出 Paxos 算法的那篇论文在当时并没有被成功发表。 +As a result, the paper proposing the Paxos algorithm was not successfully published at that time. -直到 1998 年,系统研究中心 (Systems Research Center,SRC)的两个技术研究员需要找一些合适的分布式算法来服务他们正在构建的分布式系统,Paxos 算法刚好可以解决他们的部分需求。因此,兰伯特就把论文发给了他们。在看了论文之后,这俩大佬觉得论文还是挺不错的。于是,兰伯特在 **1998** 年重新发表论文 [《The Part-Time Parliament》](http://lamport.azurewebsites.net/pubs/lamport-paxos.pdf)。 +It wasn't until **1998** that two technical researchers from the Systems Research Center (SRC) needed to find suitable distributed algorithms for the distributed system they were building, and the Paxos algorithm happened to fulfill some of their requirements. Lamport then sent the paper to them. After reviewing the paper, these two researchers found it to be quite impressive. Thus, Lamport republished his paper titled [“The Part-Time Parliament”](http://lamport.azurewebsites.net/pubs/lamport-paxos.pdf) in **1998**. -论文发表之后,各路学者直呼看不懂,言语中还略显调侃之意。这谁忍得了,在 **2001** 年的时候,兰伯特专门又写了一篇 [《Paxos Made Simple》](http://lamport.azurewebsites.net/pubs/paxos-simple.pdf) 的论文来简化对 Paxos 的介绍,主要讲述两阶段共识协议部分,顺便还不忘嘲讽一下这群学者。 +After the paper was published, scholars around the world exclaimed that it was hard to understand, with a hint of mocking tone in their words. Who could stand that? In **2001**, Lamport specifically wrote another paper [“Paxos Made Simple”](http://lamport.azurewebsites.net/pubs/paxos-simple.pdf) to simplify the introduction of Paxos, mainly discussing the two-phase consensus protocol, while also taking a dig at those scholars. -《Paxos Made Simple》这篇论文就 14 页,相比于 《The Part-Time Parliament》的 33 页精简了不少。最关键的是这篇论文的摘要就一句话: +The paper "Paxos Made Simple" is only 14 pages long, considerably shorter than the 33 pages of "The Part-Time Parliament." The key point is that the abstract of this paper is just one sentence: ![](./images/paxos/paxos-made-simple.png) > The Paxos algorithm, when presented in plain English, is very simple. -翻译过来的意思大概就是:当我用无修饰的英文来描述时,Paxos 算法真心简单! +This roughly translates to: "When I describe it in straightforward English, the Paxos algorithm is genuinely simple!" -有没有感觉到来自兰伯特大佬满满地嘲讽的味道? +Do you sense the strong flavor of sarcasm from Lamport? -## 介绍 +## Introduction -Paxos 算法是第一个被证明完备的分布式系统共识算法。共识算法的作用是让分布式系统中的多个节点之间对某个提案(Proposal)达成一致的看法。提案的含义在分布式系统中十分宽泛,像哪一个节点是 Leader 节点、多个事件发生的顺序等等都可以是一个提案。 +The Paxos algorithm is the first proven comprehensive consensus algorithm for distributed systems. The role of a consensus algorithm is to reach a unanimous opinion among multiple nodes in a distributed system about a certain proposal (Proposal). The meaning of a proposal in a distributed system is quite broad, encompassing which node is the Leader, the order of multiple events, etc. -兰伯特当时提出的 Paxos 算法主要包含 2 个部分: +The Paxos algorithm proposed by Lamport actually comprises 2 parts: -- **Basic Paxos 算法**:描述的是多节点之间如何就某个值(提案 Value)达成共识。 -- **Multi-Paxos 思想**:描述的是执行多个 Basic Paxos 实例,就一系列值达成共识。Multi-Paxos 说白了就是执行多次 Basic Paxos ,核心还是 Basic Paxos 。 +- **Basic Paxos Algorithm**: This describes how multiple nodes can reach a consensus on a certain value (proposed Value). +- **Multi-Paxos Concept**: This describes executing multiple Basic Paxos instances to achieve consensus on a series of values. Multi-Paxos, simply put, entails executing Basic Paxos multiple times, with the core still being Basic Paxos. -由于 Paxos 算法在国际上被公认的非常难以理解和实现,因此不断有人尝试简化这一算法。到了 2013 年才诞生了一个比 Paxos 算法更易理解和实现的共识算法—[Raft 算法](https://javaguide.cn/distributed-system/theorem&algorithm&protocol/raft-algorithm.html) 。更具体点来说,Raft 是 Multi-Paxos 的一个变种,其简化了 Multi-Paxos 的思想,变得更容易被理解以及工程实现。 +Due to the international consensus that the Paxos algorithm is particularly difficult to understand and implement, continuous attempts have been made to simplify this algorithm. It wasn't until 2013 that a consensus algorithm more understandable and implementable than Paxos—the [Raft algorithm](https://javaguide.cn/distributed-system/theorem&algorithm&protocol/raft-algorithm.html)—was born. More specifically, Raft is a variant of Multi-Paxos that simplifies the ideas behind Multi-Paxos, making it easier to understand and engineer. -针对没有恶意节点的情况,除了 Raft 算法之外,当前最常用的一些共识算法比如 **ZAB 协议**、 **Fast Paxos** 算法都是基于 Paxos 算法改进的。 +For scenarios without malicious nodes, some commonly used consensus algorithms, like the **ZAB protocol**, **Fast Paxos**, are all improvements based on the Paxos algorithm. -针对存在恶意节点的情况,一般使用的是 **工作量证明(POW,Proof-of-Work)**、 **权益证明(PoS,Proof-of-Stake )** 等共识算法。这类共识算法最典型的应用就是区块链,就比如说前段时间以太坊官方宣布其共识机制正在从工作量证明(PoW)转变为权益证明(PoS)。 +In cases with malicious nodes, consensus algorithms like **Proof-of-Work (PoW)** and **Proof-of-Stake (PoS)** are generally used. A typical application of such consensus algorithms is in blockchain technology. For example, recently, Ethereum's official announcement indicated that its consensus mechanism is transitioning from Proof-of-Work (PoW) to Proof-of-Stake (PoS). -区块链系统使用的共识算法需要解决的核心问题是 **拜占庭将军问题** ,这和我们日常接触到的 ZooKeeper、Etcd、Consul 等分布式中间件不太一样。 +The core problem that consensus algorithms used in blockchain systems need to solve is the **Byzantine Generals problem**, which is quite different from the distributed middleware we encounter in daily life, such as ZooKeeper, Etcd, and Consul. -下面我们来对 Paxos 算法的定义做一个总结: +Now, let's summarize the definition of the Paxos algorithm: -- Paxos 算法是兰伯特在 **1990** 年提出了一种分布式系统共识算法。 -- 兰伯特当时提出的 Paxos 算法主要包含 2 个部分: Basic Paxos 算法和 Multi-Paxos 思想。 -- Raft 算法、ZAB 协议、 Fast Paxos 算法都是基于 Paxos 算法改进而来。 +- The Paxos algorithm is a consensus algorithm for distributed systems proposed by Lamport in **1990**. +- The Paxos algorithm proposed by Lamport primarily consists of 2 parts: the Basic Paxos algorithm and the Multi-Paxos concept. +- The Raft algorithm, ZAB protocol, and Fast Paxos algorithm are all derived from improvements over the Paxos algorithm. -## Basic Paxos 算法 +## Basic Paxos Algorithm -Basic Paxos 中存在 3 个重要的角色: +The Basic Paxos algorithm involves 3 important roles: -1. **提议者(Proposer)**:也可以叫做协调者(coordinator),提议者负责接受客户端的请求并发起提案。提案信息通常包括提案编号 (Proposal ID) 和提议的值 (Value)。 -2. **接受者(Acceptor)**:也可以叫做投票员(voter),负责对提议者的提案进行投票,同时需要记住自己的投票历史; -3. **学习者(Learner)**:如果有超过半数接受者就某个提议达成了共识,那么学习者就需要接受这个提议,并就该提议作出运算,然后将运算结果返回给客户端。 +1. **Proposer**: Also known as the Coordinator, the proposer is responsible for accepting client requests and initiating proposals. Proposal information typically includes a proposal number (Proposal ID) and the proposed value (Value). +1. **Acceptor**: Also referred to as a voter, the acceptor is responsible for voting on proposals from the proposer and must remember its voting history. +1. **Learner**: If more than half of the acceptors reach a consensus on a certain proposal, the learner must accept that proposal, perform computations based on it, and then return the computation results to the client. ![](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/up-890fa3212e8bf72886a595a34654918486c.png) -为了减少实现该算法所需的节点数,一个节点可以身兼多个角色。并且,一个提案被选定需要被半数以上的 Acceptor 接受。这样的话,Basic Paxos 算法还具备容错性,在少于一半的节点出现故障时,集群仍能正常工作。 +To reduce the number of nodes required to implement this algorithm, a single node can take on multiple roles. Additionally, a proposal must be accepted by more than half of the acceptors to be chosen. This way, the Basic Paxos algorithm also possesses fault tolerance, allowing the cluster to operate normally even when less than half of the nodes fail. -## Multi Paxos 思想 +## Multi-Paxos Concept -Basic Paxos 算法的仅能就单个值达成共识,为了能够对一系列的值达成共识,我们需要用到 Multi Paxos 思想。 +The Basic Paxos algorithm can only reach consensus on a single value. To achieve consensus on a series of values, we need to employ the Multi-Paxos concept. -⚠️**注意**:Multi-Paxos 只是一种思想,这种思想的核心就是通过多个 Basic Paxos 实例就一系列值达成共识。也就是说,Basic Paxos 是 Multi-Paxos 思想的核心,Multi-Paxos 就是多执行几次 Basic Paxos。 +⚠️**Note**: Multi-Paxos is merely a concept; the core of this concept is to reach consensus on a series of values through multiple Basic Paxos instances. In other words, Basic Paxos is at the heart of the Multi-Paxos concept, while Multi-Paxos simply entails executing Basic Paxos several times. -由于兰伯特提到的 Multi-Paxos 思想缺少代码实现的必要细节(比如怎么选举领导者),所以在理解和实现上比较困难。 +Since the Multi-Paxos concept mentioned by Lamport lacks the essential details for code implementation (such as how to elect a leader), it can be somewhat challenging to understand and implement. -不过,也不需要担心,我们并不需要自己实现基于 Multi-Paxos 思想的共识算法,业界已经有了比较出名的实现。像 Raft 算法就是 Multi-Paxos 的一个变种,其简化了 Multi-Paxos 的思想,变得更容易被理解以及工程实现,实际项目中可以优先考虑 Raft 算法。 +However, there's no need to worry; we do not need to implement a consensus algorithm based on the Multi-Paxos concept ourselves. The industry has already produced some notable implementations. For instance, the Raft algorithm is a variant of Multi-Paxos that simplifies its ideas, making it easier to understand and implement. In actual projects, Raft should be prioritized. -## 参考 +## References - -- 分布式系统中的一致性与共识算法: +- Consistency and Consensus Algorithms in Distributed Systems: diff --git a/docs/distributed-system/protocol/raft-algorithm.md b/docs/distributed-system/protocol/raft-algorithm.md index 18d2c2eb0cb..aad8a9af1cf 100644 --- a/docs/distributed-system/protocol/raft-algorithm.md +++ b/docs/distributed-system/protocol/raft-algorithm.md @@ -1,167 +1,166 @@ --- -title: Raft 算法详解 -category: 分布式 +title: Detailed Explanation of the Raft Algorithm +category: Distributed Systems tag: - - 分布式协议&算法 - - 共识算法 + - Distributed Protocols & Algorithms + - Consensus Algorithm --- -> 本文由 [SnailClimb](https://github.com/Snailclimb) 和 [Xieqijun](https://github.com/jun0315) 共同完成。 +> This article is jointly completed by [SnailClimb](https://github.com/Snailclimb) and [Xieqijun](https://github.com/jun0315). -## 1 背景 +## 1 Background -当今的数据中心和应用程序在高度动态的环境中运行,为了应对高度动态的环境,它们通过额外的服务器进行横向扩展,并且根据需求进行扩展和收缩。同时,服务器和网络故障也很常见。 +Today’s data centers and applications operate in highly dynamic environments. To cope with such environments, they horizontally scale with additional servers and adjust resources based on demand. At the same time, server and network failures are quite common. -因此,系统必须在正常操作期间处理服务器的上下线。它们必须对变故做出反应并在几秒钟内自动适应;对客户来说的话,明显的中断通常是不可接受的。 +Therefore, systems must handle server ups and downs during normal operations. They must react to incidents and adapt automatically within seconds; significant interruptions are usually unacceptable for customers. -幸运的是,分布式共识可以帮助应对这些挑战。 +Fortunately, distributed consensus can help address these challenges. -### 1.1 拜占庭将军 +### 1.1 Byzantine Generals -在介绍共识算法之前,先介绍一个简化版拜占庭将军的例子来帮助理解共识算法。 +Before introducing consensus algorithms, let's first present a simplified example of Byzantine generals to help understand the consensus algorithm. -> 假设多位拜占庭将军中没有叛军,信使的信息可靠但有可能被暗杀的情况下,将军们如何达成是否要进攻的一致性决定? +> Suppose there are multiple Byzantine generals with no traitors, and the messenger's information is reliable but may get assassinated. How can the generals reach a consensus on whether to attack? -解决方案大致可以理解成:先在所有的将军中选出一个大将军,用来做出所有的决定。 +The solution can be roughly understood as: first, select a general to be the Grand General among all the generals, who will make all the decisions. -举例如下:假如现在一共有 3 个将军 A,B 和 C,每个将军都有一个随机时间的倒计时器,倒计时一结束,这个将军就把自己当成大将军候选人,然后派信使传递选举投票的信息给将军 B 和 C,如果将军 B 和 C 还没有把自己当作候选人(自己的倒计时还没有结束),并且没有把选举票投给其他人,它们就会把票投给将军 A,信使回到将军 A 时,将军 A 知道自己收到了足够的票数,成为大将军。在有了大将军之后,是否需要进攻就由大将军 A 决定,然后再去派信使通知另外两个将军,自己已经成为了大将军。如果一段时间还没收到将军 B 和 C 的回复(信使可能会被暗杀),那就再重派一个信使,直到收到回复。 +For example, suppose there are 3 generals A, B, and C. Each general has a countdown timer that runs for a random time. Once the timer reaches zero, that general declares themselves a candidate for Grand General and sends messages to generals B and C with details of the election vote. If generals B and C have not declared themselves candidates (their countdowners have not expired) and have not voted for anyone else, they will vote for general A. When messenger returns to general A, they will know they have received enough votes to become the Grand General. With the Grand General in place, whether to attack will be determined by Grand General A, who will then inform the other two generals about it. If there is still no response from generals B and C after a period of time (the messenger may have been assassinated), a new messenger will be dispatched until a response is received. -### 1.2 共识算法 +### 1.2 Consensus Algorithm -共识是可容错系统中的一个基本问题:即使面对故障,服务器也可以在共享状态上达成一致。 +Consensus is a fundamental problem in fault-tolerant systems: even in the presence of failures, servers must agree on a shared state. -共识算法允许一组节点像一个整体一样一起工作,即使其中的一些节点出现故障也能够继续工作下去,其正确性主要是源于复制状态机的性质:一组`Server`的状态机计算相同状态的副本,即使有一部分的`Server`宕机了它们仍然能够继续运行。 +Consensus algorithms allow a group of nodes to work together as a whole, ensuring that the system can continue to function even if some nodes fail. Its correctness primarily stems from the properties of replicated state machines: a set of `Servers` computes replicas of the same state machine, which can continue to operate even if part of the `Servers` goes down. ![rsm-architecture.png](https://oss.javaguide.cn/github/javaguide/paxos-rsm-architecture.png) -`图-1 复制状态机架构` +`Figure 1: Replicated State Machine Architecture` -一般通过使用复制日志来实现复制状态机。每个`Server`存储着一份包括命令序列的日志文件,状态机会按顺序执行这些命令。因为每个日志包含相同的命令,并且顺序也相同,所以每个状态机处理相同的命令序列。由于状态机是确定性的,所以处理相同的状态,得到相同的输出。 +Typically, replicated state machines are implemented using replicated logs. Each `Server` stores a log file that includes a sequence of commands; state machines execute these commands in order. Since each log contains the same commands executed in the same order, every state machine processes the same command sequence. As the state machine is deterministic, it produces the same output when processing the same state. -因此共识算法的工作就是保持复制日志的一致性。服务器上的共识模块从客户端接收命令并将它们添加到日志中。它与其他服务器上的共识模块通信,以确保即使某些服务器发生故障。每个日志最终包含相同顺序的请求。一旦命令被正确地复制,它们就被称为已提交。每个服务器的状态机按照日志顺序处理已提交的命令,并将输出返回给客户端,因此,这些服务器形成了一个单一的、高度可靠的状态机。 +Thus, the job of the consensus algorithm is to maintain the consistency of the replicated logs. The consensus module on each server receives commands from clients and adds them to the log. It communicates with the consensus modules on other servers to ensure that all logs contain requests in the same order, even if some servers fail. Once commands are correctly replicated, they are said to be committed. The state machine of each server processes committed commands in log order and returns the output to the client; hence, these servers form a single, highly reliable state machine. -适用于实际系统的共识算法通常具有以下特性: +Consensus algorithms suitable for real systems typically have the following characteristics: -- 安全。确保在非拜占庭条件(也就是上文中提到的简易版拜占庭)下的安全性,包括网络延迟、分区、包丢失、复制和重新排序。 -- 高可用。只要大多数服务器都是可操作的,并且可以相互通信,也可以与客户端进行通信,那么这些服务器就可以看作完全功能可用的。因此,一个典型的由五台服务器组成的集群可以容忍任何两台服务器端故障。假设服务器因停止而发生故障;它们稍后可能会从稳定存储上的状态中恢复并重新加入集群。 -- 一致性不依赖时序。错误的时钟和极端的消息延迟,在最坏的情况下也只会造成可用性问题,而不会产生一致性问题。 +- Safety. Ensures safety under non-Byzantine conditions (i.e., the simplified version of Byzantine mentioned above), including network delays, partitions, packet losses, and message reordering. +- High availability. As long as a majority of servers are operational and can communicate with each other and with clients, these servers can be considered fully functional. Thus, a typical cluster of five servers can tolerate the failure of any two servers. If servers fail due to stoppage, they can later recover from stable storage and rejoin the cluster. +- Consistency does not depend on timing. Erroneous clocks and extreme message delays will only cause availability issues in the worst case and will not result in consistency problems. +- As long as the majority of servers respond in the cluster, commands can be completed without being affected by a few slow servers. -- 在集群中大多数服务器响应,命令就可以完成,不会被少数运行缓慢的服务器来影响整体系统性能。 +## 2 Basics -## 2 基础 +### 2.1 Node Types -### 2.1 节点类型 +A Raft cluster consists of several servers. Taking a typical cluster of 5 servers as an example, at any given time, each server will be in one of the following three states: -一个 Raft 集群包括若干服务器,以典型的 5 服务器集群举例。在任意的时间,每个服务器一定会处于以下三个状态中的一个: +- `Leader`: Responsible for initiating heartbeats, responding to clients, creating logs, and synchronizing logs. +- `Candidate`: A temporary role during the leader election process, transformed from Follower, initiating votes to participate in the election. +- `Follower`: Accepts heartbeats and log synchronization data from the Leader, voting for the Candidate. -- `Leader`:负责发起心跳,响应客户端,创建日志,同步日志。 -- `Candidate`:Leader 选举过程中的临时角色,由 Follower 转化而来,发起投票参与竞选。 -- `Follower`:接受 Leader 的心跳和日志同步数据,投票给 Candidate。 - -在正常的情况下,只有一个服务器是 Leader,剩下的服务器是 Follower。Follower 是被动的,它们不会发送任何请求,只是响应来自 Leader 和 Candidate 的请求。 +Under normal conditions, only one server is the Leader, while the remaining servers are Followers. Followers are passive; they do not send requests but only respond to requests from the Leader and Candidates. ![](https://oss.javaguide.cn/github/javaguide/paxos-server-state.png) -`图-2:服务器的状态` +`Figure 2: Server States` -### 2.2 任期 +### 2.2 Terms ![](https://oss.javaguide.cn/github/javaguide/paxos-term.png) -`图-3:任期` +`Figure 3: Terms` -如图 3 所示,raft 算法将时间划分为任意长度的任期(term),任期用连续的数字表示,看作当前 term 号。每一个任期的开始都是一次选举,在选举开始时,一个或多个 Candidate 会尝试成为 Leader。如果一个 Candidate 赢得了选举,它就会在该任期内担任 Leader。如果没有选出 Leader,将会开启另一个任期,并立刻开始下一次选举。raft 算法保证在给定的一个任期最少要有一个 Leader。 +As shown in Figure 3, the Raft algorithm divides time into terms of arbitrary length, represented by consecutive numbers considered the current term number. The beginning of each term is an election; at the start of an election, one or more Candidates will attempt to become the Leader. If a Candidate wins the election, they will serve as the Leader for that term. If no Leader is elected, a new term will be initiated, and the next election will begin immediately. The Raft algorithm guarantees that there is at least one Leader in a given term. -每个节点都会存储当前的 term 号,当服务器之间进行通信时会交换当前的 term 号;如果有服务器发现自己的 term 号比其他人小,那么他会更新到较大的 term 值。如果一个 Candidate 或者 Leader 发现自己的 term 过期了,他会立即退回成 Follower。如果一台服务器收到的请求的 term 号是过期的,那么它会拒绝此次请求。 +Each node stores the current term number, and during communication between servers, they exchange their current term numbers. If a server finds its term number is smaller than others, it will update to the larger term value. If a Candidate or Leader finds its term has expired, it will revert to Follower immediately. If a server receives a request with an expired term number, it will reject the request. -### 2.3 日志 +### 2.3 Logs -- `entry`:每一个事件成为 entry,只有 Leader 可以创建 entry。entry 的内容为``其中 cmd 是可以应用到状态机的操作。 -- `log`:由 entry 构成的数组,每一个 entry 都有一个表明自己在 log 中的 index。只有 Leader 才可以改变其他节点的 log。entry 总是先被 Leader 添加到自己的 log 数组中,然后再发起共识请求,获得同意后才会被 Leader 提交给状态机。Follower 只能从 Leader 获取新日志和当前的 commitIndex,然后把对应的 entry 应用到自己的状态机中。 +- `entry`: Each event becomes an entry, and only the Leader can create entries. The content of an entry is `` where cmd is an operation that can be applied to the state machine. +- `log`: An array composed of entries, where each entry has an index indicating its position in the log. Only the Leader can change the logs of other nodes. Entries are always first added to the Leader's log array, and only after initiating a consensus request and obtaining agreement can they be committed to the state machine. Followers can only obtain new logs and the current commitIndex from the Leader and then apply the corresponding entries to their state machines. -## 3 领导人选举 +## 3 Leader Election -raft 使用心跳机制来触发 Leader 的选举。 +Raft uses a heartbeat mechanism to trigger Leader elections. -如果一台服务器能够收到来自 Leader 或者 Candidate 的有效信息,那么它会一直保持为 Follower 状态,并且刷新自己的 electionElapsed,重新计时。 +If a server receives valid information from a Leader or Candidate, it will remain in the Follower state and refresh its electionElapsed timer. -Leader 会向所有的 Follower 周期性发送心跳来保证自己的 Leader 地位。如果一个 Follower 在一个周期内没有收到心跳信息,就叫做选举超时,然后它就会认为此时没有可用的 Leader,并且开始进行一次选举以选出一个新的 Leader。 +The Leader periodically sends heartbeats to all Followers to ensure its leadership status. If a Follower does not receive heartbeat information within a cycle, it triggers an election timeout and assumes there is currently no available Leader, initiating an election to elect a new Leader. -为了开始新的选举,Follower 会自增自己的 term 号并且转换状态为 Candidate。然后他会向所有节点发起 RequestVoteRPC 请求, Candidate 的状态会持续到以下情况发生: +To start a new election, the Follower increments its term number and changes its state to Candidate. It will then send a RequestVoteRPC to all nodes, and the Candidate's state will persist until one of the following occurs: -- 赢得选举 -- 其他节点赢得选举 -- 一轮选举结束,无人胜出 +- Wins the election +- Other nodes win the election +- One round of the election ends with no winner -赢得选举的条件是:一个 Candidate 在一个任期内收到了来自集群内的多数选票`(N/2+1)`,就可以成为 Leader。 +The condition for winning the election is: a Candidate must receive votes from a majority of the cluster `(N/2+1)` in one term to become the Leader. -在 Candidate 等待选票的时候,它可能收到其他节点声明自己是 Leader 的心跳,此时有两种情况: +While waiting for votes, a Candidate may receive heartbeat messages from other nodes claiming to be the Leader, resulting in two situations: -- 该 Leader 的 term 号大于等于自己的 term 号,说明对方已经成为 Leader,则自己回退为 Follower。 -- 该 Leader 的 term 号小于自己的 term 号,那么会拒绝该请求并让该节点更新 term。 +- The term number of that Leader is greater than or equal to its own; indicating that the other has become the Leader, it will revert to Follower. +- The term number of that Leader is less than its own. In this case, the request is rejected, prompting that node to update its term. -由于可能同一时刻出现多个 Candidate,导致没有 Candidate 获得大多数选票,如果没有其他手段来重新分配选票的话,那么可能会无限重复下去。 +As multiple Candidates may appear simultaneously without any means to redistribute votes, it could lead to an infinite loop of elections. -raft 使用了随机的选举超时时间来避免上述情况。每一个 Candidate 在发起选举后,都会随机化一个新的选举超时时间,这种机制使得各个服务器能够分散开来,在大多数情况下只有一个服务器会率先超时;它会在其他服务器超时之前赢得选举。 +Raft uses a random election timeout to avoid the above situation. Each Candidate randomizes a new election timeout after initiating an election, allowing servers to spread apart; in most cases, only one server will timeout first, winning the election before others do. -## 4 日志复制 +## 4 Log Replication -一旦选出了 Leader,它就开始接受客户端的请求。每一个客户端的请求都包含一条需要被复制状态机(`Replicated State Machine`)执行的命令。 +Once a Leader is elected, it starts receiving client requests. Each client's request includes a command that needs to be executed by the replicated state machine (`Replicated State Machine`). -Leader 收到客户端请求后,会生成一个 entry,包含``,再将这个 entry 添加到自己的日志末尾后,向所有的节点广播该 entry,要求其他服务器复制这条 entry。 +After the Leader receives the client request, it generates an entry containing ``, adds this entry to its log's end, and broadcasts it to all nodes, asking the other servers to replicate the entry. -如果 Follower 接受该 entry,则会将 entry 添加到自己的日志后面,同时返回给 Leader 同意。 +If a Follower accepts the entry, it will append the entry to its log and return a consent response to the Leader. -如果 Leader 收到了多数的成功响应,Leader 会将这个 entry 应用到自己的状态机中,之后可以称这个 entry 是 committed 的,并且向客户端返回执行结果。 +If the Leader receives a majority of successful responses, it will apply this entry to its state machine, and this entry can then be termed as committed, eventually returning the execution result to the client. -raft 保证以下两个性质: +Raft guarantees the following two properties: -- 在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们一定有相同的 cmd -- 在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们前面的 entry 也一定相同 +- Among two logs, if two entries have the same index and term, their cmds must also be the same. +- Among two logs, if two entries have the same index and term, their preceding entries must also be the same. -通过“仅有 Leader 可以生成 entry”来保证第一个性质,第二个性质需要一致性检查来进行保证。 +The first property is guaranteed by "only the Leader can generate entries," while the second property requires consistency checks to ensure. -一般情况下,Leader 和 Follower 的日志保持一致,然后,Leader 的崩溃会导致日志不一样,这样一致性检查会产生失败。Leader 通过强制 Follower 复制自己的日志来处理日志的不一致。这就意味着,在 Follower 上的冲突日志会被领导者的日志覆盖。 +Generally, the logs of Leaders and Followers remain consistent; however, a Leader's crash may result in inconsistent logs, causing consistency checks to fail. The Leader handles log inconsistency by forcing Followers to replicate its logs, meaning any conflicting logs on Followers will be overwritten by the Leader's logs. -为了使得 Follower 的日志和自己的日志一致,Leader 需要找到 Follower 与它日志一致的地方,然后删除 Follower 在该位置之后的日志,接着把这之后的日志发送给 Follower。 +To ensure Followers' logs align with its own, the Leader must find the point of consistency in the Follower's log and delete any log entries after that point, then send the missing logs from that position onward to the Follower. -`Leader` 给每一个`Follower` 维护了一个 `nextIndex`,它表示 `Leader` 将要发送给该追随者的下一条日志条目的索引。当一个 `Leader` 开始掌权时,它会将 `nextIndex` 初始化为它的最新的日志条目索引数+1。如果一个 `Follower` 的日志和 `Leader` 的不一致,`AppendEntries` 一致性检查会在下一次 `AppendEntries RPC` 时返回失败。在失败之后,`Leader` 会将 `nextIndex` 递减然后重试 `AppendEntries RPC`。最终 `nextIndex` 会达到一个 `Leader` 和 `Follower` 日志一致的地方。这时,`AppendEntries` 会返回成功,`Follower` 中冲突的日志条目都被移除了,并且添加所缺少的上了 `Leader` 的日志条目。一旦 `AppendEntries` 返回成功,`Follower` 和 `Leader` 的日志就一致了,这样的状态会保持到该任期结束。 +The `Leader` maintains a `nextIndex` for each `Follower`, representing the index of the next log entry that the `Leader` will send to that Follower. When a `Leader` begins authority, it initializes `nextIndex` to the number of its latest log entry index + 1. If a `Follower`'s log is inconsistent with the `Leader`'s, the consistency check in `AppendEntries` will return a failure in the next `AppendEntries RPC`. After a failure, the `Leader` decrements `nextIndex` and retries `AppendEntries RPC`. Eventually, `nextIndex` reaches a point of consistency between the `Leader` and `Follower` logs. At that point, `AppendEntries` succeeds, and any conflicting log entries on the `Follower` are removed, with the missing log entries appended from the `Leader`. Once `AppendEntries` succeeds, the logs of `Follower` and `Leader` are consistent, maintaining this state until the end of the term. -## 5 安全性 +## 5 Safety -### 5.1 选举限制 +### 5.1 Election Constraints -Leader 需要保证自己存储全部已经提交的日志条目。这样才可以使日志条目只有一个流向:从 Leader 流向 Follower,Leader 永远不会覆盖已经存在的日志条目。 +The Leader must ensure it stores all committed log entries. This assures that log entries have only one direction: from Leader to Follower; the Leader will never overwrite already existing log entries. -每个 Candidate 发送 RequestVoteRPC 时,都会带上最后一个 entry 的信息。所有节点收到投票信息时,会对该 entry 进行比较,如果发现自己的更新,则拒绝投票给该 Candidate。 +Every Candidate sends a RequestVoteRPC with information on the last entry. When all nodes receive the voting information, they compare this entry against their own updates; if they find their logs more up-to-date, they refuse to vote for that Candidate. -判断日志新旧的方式:如果两个日志的 term 不同,term 大的更新;如果 term 相同,更长的 index 更新。 +The way to determine the recency of logs: if the terms of the two logs differ, the log with the larger term updates; if the terms are the same, the longer index updates. -### 5.2 节点崩溃 +### 5.2 Node Crashes -如果 Leader 崩溃,集群中的节点在 electionTimeout 时间内没有收到 Leader 的心跳信息就会触发新一轮的选主,在选主期间整个集群对外是不可用的。 +If the Leader crashes, nodes in the cluster that do not receive heartbeat information from the Leader within the electionTimeout period will trigger a new round of leader election, during which the entire cluster becomes unavailable to outside requests. -如果 Follower 和 Candidate 崩溃,处理方式会简单很多。之后发送给它的 RequestVoteRPC 和 AppendEntriesRPC 会失败。由于 raft 的所有请求都是幂等的,所以失败的话会无限的重试。如果崩溃恢复后,就可以收到新的请求,然后选择追加或者拒绝 entry。 +If Followers or Candidates crash, the process is much simpler. Subsequent RequestVoteRPCs and AppendEntriesRPCs sent to them will fail. Since all Raft requests are idempotent, failures will result in infinite retries. After recovery from a crash, they can receive new requests, choosing to append or reject entries. -### 5.3 时间与可用性 +### 5.3 Time and Availability -raft 的要求之一就是安全性不依赖于时间:系统不能仅仅因为一些事件发生的比预想的快一些或者慢一些就产生错误。为了保证上述要求,最好能满足以下的时间条件: +One of Raft's requirements is that safety does not depend on time: the system cannot generate errors simply because some events occur faster or slower than expected. To ensure the above requirement, it is best to satisfy the following time conditions: `broadcastTime << electionTimeout << MTBF` -- `broadcastTime`:向其他节点并发发送消息的平均响应时间; -- `electionTimeout`:选举超时时间; -- `MTBF(mean time between failures)`:单台机器的平均健康时间; +- `broadcastTime`: The average response time for concurrently sending messages to other nodes. +- `electionTimeout`: The timeout for elections. +- `MTBF(mean time between failures)`: The average healthy uptime of a single machine. -`broadcastTime`应该比`electionTimeout`小一个数量级,为的是使`Leader`能够持续发送心跳信息(heartbeat)来阻止`Follower`开始选举; +`broadcastTime` should be an order of magnitude smaller than `electionTimeout` to allow the `Leader` to continually send heartbeat messages to prevent `Follower` from initiating elections. -`electionTimeout`也要比`MTBF`小几个数量级,为的是使得系统稳定运行。当`Leader`崩溃时,大约会在整个`electionTimeout`的时间内不可用;我们希望这种情况仅占全部时间的很小一部分。 +`electionTimeout` should also be several orders smaller than `MTBF` to keep the system running stably. When `Leader` crashes, the system will be unavailable for approximately the entire duration of `electionTimeout`; we wish for this situation to occupy only a small part of the total time. -由于`broadcastTime`和`MTBF`是由系统决定的属性,因此需要决定`electionTimeout`的时间。 +Since `broadcastTime` and `MTBF` are system-determined properties, it is necessary to set the duration of `electionTimeout`. -一般来说,broadcastTime 一般为 `0.5~20ms`,electionTimeout 可以设置为 `10~500ms`,MTBF 一般为一两个月。 +In general, `broadcastTime` is usually between `0.5-20ms`, while `electionTimeout` can be set to `10-500ms`, and `MTBF` is typically one to two months. -## 6 参考 +## 6 References - - diff --git a/docs/distributed-system/rpc/dubbo.md b/docs/distributed-system/rpc/dubbo.md index 3eaee38b50c..703d0f1f7d0 100644 --- a/docs/distributed-system/rpc/dubbo.md +++ b/docs/distributed-system/rpc/dubbo.md @@ -1,461 +1,71 @@ --- -title: Dubbo常见问题总结 -category: 分布式 +title: Summary of Common Dubbo Issues +category: Distributed tag: - rpc --- ::: tip -- Dubbo3 已经发布,这篇文章是基于 Dubbo2 写的。Dubbo3 基于 Dubbo2 演进而来,在保持原有核心功能特性的同时, Dubbo3 在易用性、超大规模微服务实践、云原生基础设施适配、安全设计等几大方向上进行了全面升级。 -- 本文中的很多链接已经失效,主要原因是因为 Dubbo 官方文档进行了修改导致 URL 失效。 +- Dubbo3 has been released, and this article is based on Dubbo2. Dubbo3 is evolved from Dubbo2, maintaining the original core functional characteristics while undergoing comprehensive upgrades in usability, large-scale microservice practices, cloud-native infrastructure adaptation, and security design. +- Many links in this article are outdated, mainly due to changes in the official Dubbo documentation that have caused URL failures. ::: -这篇文章是我根据官方文档以及自己平时的使用情况,对 Dubbo 所做的一个总结。欢迎补充! +This article is a summary of Dubbo based on the official documentation and my own usage experience. Contributions are welcome! -## Dubbo 基础 +## Dubbo Basics -### 什么是 Dubbo? +### What is Dubbo? -![Dubbo 官网](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/rpc/dubbo.org-overview.png) +![Dubbo Official Website](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/rpc/dubbo.org-overview.png) -[Apache Dubbo](https://github.com/apache/dubbo) |ˈdʌbəʊ| 是一款高性能、轻量级的开源 WEB 和 RPC 框架。 +[Apache Dubbo](https://github.com/apache/dubbo) |ˈdʌbəʊ| is a high-performance, lightweight open-source WEB and RPC framework. -根据 [Dubbo 官方文档](https://dubbo.apache.org/zh/)的介绍,Dubbo 提供了六大核心能力 +According to the [official Dubbo documentation](https://dubbo.apache.org/zh/), Dubbo provides six core capabilities: -1. 面向接口代理的高性能 RPC 调用。 -2. 智能容错和负载均衡。 -3. 服务自动注册和发现。 -4. 高度可扩展能力。 -5. 运行期流量调度。 -6. 可视化的服务治理与运维。 +1. High-performance RPC calls oriented to interface proxies. +1. Intelligent fault tolerance and load balancing. +1. Automatic service registration and discovery. +1. Highly extensible capabilities. +1. Runtime traffic scheduling. +1. Visual service governance and operation. -![Dubbo提供的六大核心能力](https://oss.javaguide.cn/%E6%BA%90%E7%A0%81/dubbo/dubbo%E6%8F%90%E4%BE%9B%E7%9A%84%E5%85%AD%E5%A4%A7%E6%A0%B8%E5%BF%83%E8%83%BD%E5%8A%9B.png) +![Six Core Capabilities Provided by Dubbo](https://oss.javaguide.cn/%E6%BA%90%E7%A0%81/dubbo/dubbo%E6%8F%90%E4%BE%9B%E7%9A%84%E5%85%AD%E5%A4%A7%E6%A0%B8%E5%BF%83%E8%83%BD%E5%8A%9B.png) -简单来说就是:**Dubbo 不光可以帮助我们调用远程服务,还提供了一些其他开箱即用的功能比如智能负载均衡。** +In simple terms: **Dubbo not only helps us call remote services but also provides other out-of-the-box features like intelligent load balancing.** -Dubbo 目前已经有接近 34.4 k 的 Star 。 +Dubbo currently has nearly 34.4k stars. -在 **2020 年度 OSC 中国开源项目** 评选活动中,Dubbo 位列开发框架和基础组件类项目的第 7 名。相比几年前来说,热度和排名有所下降。 +In the **2020 OSC China Open Source Project** selection event, Dubbo ranked 7th among development frameworks and basic component projects. Compared to a few years ago, its popularity and ranking have declined. ![](https://oss.javaguide.cn/%E6%BA%90%E7%A0%81/dubbo/image-20210107153159545.png) -Dubbo 是由阿里开源,后来加入了 Apache 。正是由于 Dubbo 的出现,才使得越来越多的公司开始使用以及接受分布式架构。 +Dubbo was open-sourced by Alibaba and later joined Apache. It is the emergence of Dubbo that has led to more and more companies beginning to use and accept distributed architectures. -### 为什么要用 Dubbo? +### Why Use Dubbo? -随着互联网的发展,网站的规模越来越大,用户数量越来越多。单一应用架构、垂直应用架构无法满足我们的需求,这个时候分布式服务架构就诞生了。 +With the development of the internet, websites are becoming larger, and the number of users is increasing. Single application architectures and vertical application architectures can no longer meet our needs, which is when distributed service architectures were born. -分布式服务架构下,系统被拆分成不同的服务比如短信服务、安全服务,每个服务独立提供系统的某个核心服务。 +In a distributed service architecture, the system is split into different services, such as SMS service and security service, with each service independently providing a core service of the system. -我们可以使用 Java RMI(Java Remote Method Invocation)、Hessian 这种支持远程调用的框架来简单地暴露和引用远程服务。但是!当服务越来越多之后,服务调用关系越来越复杂。当应用访问压力越来越大后,负载均衡以及服务监控的需求也迫在眉睫。我们可以用 F5 这类硬件来做负载均衡,但这样增加了成本,并且存在单点故障的风险。 +We can use frameworks that support remote calls, such as Java RMI (Java Remote Method Invocation) and Hessian, to simply expose and reference remote services. However! As the number of services increases, the relationships between service calls become increasingly complex. When application access pressure increases, the need for load balancing and service monitoring becomes urgent. We can use hardware like F5 for load balancing, but this increases costs and poses a risk of single points of failure. -不过,Dubbo 的出现让上述问题得到了解决。**Dubbo 帮助我们解决了什么问题呢?** +However, the emergence of Dubbo has solved the above problems. **What problems does Dubbo help us solve?** -1. **负载均衡**:同一个服务部署在不同的机器时该调用哪一台机器上的服务。 -2. **服务调用链路生成**:随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如何调用的。 -3. **服务访问压力以及时长统计、资源调度和治理**:基于访问压力实时管理集群容量,提高集群利用率。 -4. …… +1. **Load Balancing**: When the same service is deployed on different machines, which machine's service should be called? +1. **Service Call Chain Generation**: As the system evolves, the number of services increases, and the dependencies between services become complex, making it difficult to determine which application should start before which. Even architects cannot fully describe the architectural relationships of applications. Dubbo can help us understand how services call each other. +1. **Service Access Pressure and Duration Statistics, Resource Scheduling, and Governance**: Real-time management of cluster capacity based on access pressure to improve cluster utilization. +1. …… -![Dubbo 能力概览](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/rpc/dubbo-features-overview.jpg) +![Overview of Dubbo Capabilities](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/rpc/dubbo-features-overview.jpg) -另外,Dubbo 除了能够应用在分布式系统中,也可以应用在现在比较火的微服务系统中。不过,由于 Spring Cloud 在微服务中应用更加广泛,所以,我觉得一般我们提 Dubbo 的话,大部分是分布式系统的情况。 +Additionally, Dubbo can be applied not only in distributed systems but also in the currently popular microservice systems. However, since Spring Cloud is more widely used in microservices, I believe that when we mention Dubbo, it is mostly in the context of distributed systems. -**我们刚刚提到了分布式这个概念,下面再给大家介绍一下什么是分布式?为什么要分布式?** +**We just mentioned the concept of distribution, so let’s introduce what distribution is and why it is necessary.** -## 分布式基础 +## Basics of Distribution -### 什么是分布式? +### What is Distribution? -分布式或者说 SOA 分布式重要的就是面向服务,说简单的分布式就是我们把整个系统拆分成不同的服务然后将这些服务放在不同的服务器上减轻单体服务的压力提高并发量和性能。比如电商系统可以简单地拆分成订单系统、商品系统、登录系统等等,拆分之后的每个服务可以部署在不同的机器上,如果某一个服务的访问量比较大的话也可以将这个服务同时部署在多台机器上。 - -![分布式事务示意图](https://oss.javaguide.cn/java-guide-blog/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E7%A4%BA%E6%84%8F%E5%9B%BE.png) - -### 为什么要分布式? - -从开发角度来讲单体应用的代码都集中在一起,而分布式系统的代码根据业务被拆分。所以,每个团队可以负责一个服务的开发,这样提升了开发效率。另外,代码根据业务拆分之后更加便于维护和扩展。 - -另外,我觉得将系统拆分成分布式之后不光便于系统扩展和维护,更能提高整个系统的性能。你想一想嘛?把整个系统拆分成不同的服务/系统,然后每个服务/系统 单独部署在一台服务器上,是不是很大程度上提高了系统性能呢? - -## Dubbo 架构 - -### Dubbo 架构中的核心角色有哪些? - -[官方文档中的框架设计章节](https://dubbo.apache.org/zh/docs/v2.7/dev/design/) 已经介绍的非常详细了,我这里把一些比较重要的点再提一下。 - -![dubbo-relation](https://oss.javaguide.cn/%E6%BA%90%E7%A0%81/dubbo/dubbo-relation.jpg) - -上述节点简单介绍以及他们之间的关系: - -- **Container:** 服务运行容器,负责加载、运行服务提供者。必须。 -- **Provider:** 暴露服务的服务提供方,会向注册中心注册自己提供的服务。必须。 -- **Consumer:** 调用远程服务的服务消费方,会向注册中心订阅自己所需的服务。必须。 -- **Registry:** 服务注册与发现的注册中心。注册中心会返回服务提供者地址列表给消费者。非必须。 -- **Monitor:** 统计服务的调用次数和调用时间的监控中心。服务消费者和提供者会定时发送统计数据到监控中心。 非必须。 - -### Dubbo 中的 Invoker 概念了解么? - -`Invoker` 是 Dubbo 领域模型中非常重要的一个概念,你如果阅读过 Dubbo 源码的话,你会无数次看到这玩意。就比如下面我要说的负载均衡这块的源码中就有大量 `Invoker` 的身影。 - -简单来说,`Invoker` 就是 Dubbo 对远程调用的抽象。 - -![dubbo_rpc_invoke.jpg](https://oss.javaguide.cn/java-guide-blog/dubbo_rpc_invoke.jpg) - -按照 Dubbo 官方的话来说,`Invoker` 分为 - -- 服务提供 `Invoker` -- 服务消费 `Invoker` - -假如我们需要调用一个远程方法,我们需要动态代理来屏蔽远程调用的细节吧!我们屏蔽掉的这些细节就依赖对应的 `Invoker` 实现, `Invoker` 实现了真正的远程服务调用。 - -### Dubbo 的工作原理了解么? - -下图是 Dubbo 的整体设计,从下至上分为十层,各层均为单向依赖。 - -> 左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。 - -![dubbo-framework](https://oss.javaguide.cn/source-code/dubbo/dubbo-framework.jpg) - -- **config 配置层**:Dubbo 相关的配置。支持代码配置,同时也支持基于 Spring 来做配置,以 `ServiceConfig`, `ReferenceConfig` 为中心 -- **proxy 服务代理层**:调用远程方法像调用本地的方法一样简单的一个关键,真实调用过程依赖代理类,以 `ServiceProxy` 为中心。 -- **registry 注册中心层**:封装服务地址的注册与发现。 -- **cluster 路由层**:封装多个提供者的路由及负载均衡,并桥接注册中心,以 `Invoker` 为中心。 -- **monitor 监控层**:RPC 调用次数和调用时间监控,以 `Statistics` 为中心。 -- **protocol 远程调用层**:封装 RPC 调用,以 `Invocation`, `Result` 为中心。 -- **exchange 信息交换层**:封装请求响应模式,同步转异步,以 `Request`, `Response` 为中心。 -- **transport 网络传输层**:抽象 mina 和 netty 为统一接口,以 `Message` 为中心。 -- **serialize 数据序列化层**:对需要在网络传输的数据进行序列化。 - -### Dubbo 的 SPI 机制了解么? 如何扩展 Dubbo 中的默认实现? - -SPI(Service Provider Interface) 机制被大量用在开源项目中,它可以帮助我们动态寻找服务/功能(比如负载均衡策略)的实现。 - -SPI 的具体原理是这样的:我们将接口的实现类放在配置文件中,我们在程序运行过程中读取配置文件,通过反射加载实现类。这样,我们可以在运行的时候,动态替换接口的实现类。和 IoC 的解耦思想是类似的。 - -Java 本身就提供了 SPI 机制的实现。不过,Dubbo 没有直接用,而是对 Java 原生的 SPI 机制进行了增强,以便更好满足自己的需求。 - -**那我们如何扩展 Dubbo 中的默认实现呢?** - -比如说我们想要实现自己的负载均衡策略,我们创建对应的实现类 `XxxLoadBalance` 实现 `LoadBalance` 接口或者 `AbstractLoadBalance` 类。 - -```java -package com.xxx; - -import org.apache.dubbo.rpc.cluster.LoadBalance; -import org.apache.dubbo.rpc.Invoker; -import org.apache.dubbo.rpc.Invocation; -import org.apache.dubbo.rpc.RpcException; - -public class XxxLoadBalance implements LoadBalance { - public Invoker select(List> invokers, Invocation invocation) throws RpcException { - // ... - } -} -``` - -我们将这个实现类的路径写入到`resources` 目录下的 `META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance`文件中即可。 - -```java -src - |-main - |-java - |-com - |-xxx - |-XxxLoadBalance.java (实现LoadBalance接口) - |-resources - |-META-INF - |-dubbo - |-org.apache.dubbo.rpc.cluster.LoadBalance (纯文本文件,内容为:xxx=com.xxx.XxxLoadBalance) -``` - -`org.apache.dubbo.rpc.cluster.LoadBalance` - -```plain -xxx=com.xxx.XxxLoadBalance -``` - -其他还有很多可供扩展的选择,你可以在[官方文档](https://cn.dubbo.apache.org/zh-cn/overview/home/)中找到。 - -### Dubbo 的微内核架构了解吗? - -Dubbo 采用 微内核(Microkernel) + 插件(Plugin) 模式,简单来说就是微内核架构。微内核只负责组装插件。 - -**何为微内核架构呢?** 《软件架构模式》 这本书是这样介绍的: - -> 微内核架构模式(有时被称为插件架构模式)是实现基于产品应用程序的一种自然模式。基于产品的应用程序是已经打包好并且拥有不同版本,可作为第三方插件下载的。然后,很多公司也在开发、发布自己内部商业应用像有版本号、说明及可加载插件式的应用软件(这也是这种模式的特征)。微内核系统可让用户添加额外的应用如插件,到核心应用,继而提供了可扩展性和功能分离的用法。 - -微内核架构包含两类组件:**核心系统(core system)** 和 **插件模块(plug-in modules)**。 - -![](https://oss.javaguide.cn/source-code/dubbo/%E5%BE%AE%E5%86%85%E6%A0%B8%E6%9E%B6%E6%9E%84%E7%A4%BA%E6%84%8F%E5%9B%BE.png) - -核心系统提供系统所需核心能力,插件模块可以扩展系统的功能。因此, 基于微内核架构的系统,非常易于扩展功能。 - -我们常见的一些 IDE,都可以看作是基于微内核架构设计的。绝大多数 IDE 比如 IDEA、VSCode 都提供了插件来丰富自己的功能。 - -正是因为 Dubbo 基于微内核架构,才使得我们可以随心所欲替换 Dubbo 的功能点。比如你觉得 Dubbo 的序列化模块实现的不满足自己要求,没关系啊!你自己实现一个序列化模块就好了啊! - -通常情况下,微核心都会采用 Factory、IoC、OSGi 等方式管理插件生命周期。Dubbo 不想依赖 Spring 等 IoC 容器,也不想自己造一个小的 IoC 容器(过度设计),因此采用了一种最简单的 Factory 方式管理插件:**JDK 标准的 SPI 扩展机制** (`java.util.ServiceLoader`)。 - -### 关于 Dubbo 架构的一些自测小问题 - -#### 注册中心的作用了解么? - -注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互。 - -#### 服务提供者宕机后,注册中心会做什么? - -注册中心会立即推送事件通知消费者。 - -#### 监控中心的作用呢? - -监控中心负责统计各服务调用次数,调用时间等。 - -#### 注册中心和监控中心都宕机的话,服务都会挂掉吗? - -不会。两者都宕机也不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表。注册中心和监控中心都是可选的,服务消费者可以直连服务提供者。 - -## Dubbo 的负载均衡策略 - -### 什么是负载均衡? - -先来看一下稍微官方点的解释。下面这段话摘自维基百科对负载均衡的定义: - -> 负载均衡改善了跨多个计算资源(例如计算机,计算机集群,网络链接,中央处理单元或磁盘驱动)的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间,并避免任何单个资源的过载。使用具有负载平衡而不是单个组件的多个组件可以通过冗余提高可靠性和可用性。负载平衡通常涉及专用软件或硬件。 - -**上面讲的大家可能不太好理解,再用通俗的话给大家说一下。** - -我们的系统中的某个服务的访问量特别大,我们将这个服务部署在了多台服务器上,当客户端发起请求的时候,多台服务器都可以处理这个请求。那么,如何正确选择处理该请求的服务器就很关键。假如,你就要一台服务器来处理该服务的请求,那该服务部署在多台服务器的意义就不复存在了。负载均衡就是为了避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题,我们从负载均衡的这四个字就能明显感受到它的意义。 - -### Dubbo 提供的负载均衡策略有哪些? - -在集群负载均衡时,Dubbo 提供了多种均衡策略,默认为 `random` 随机调用。我们还可以自行扩展负载均衡策略(参考 Dubbo SPI 机制)。 - -在 Dubbo 中,所有负载均衡实现类均继承自 `AbstractLoadBalance`,该类实现了 `LoadBalance` 接口,并封装了一些公共的逻辑。 - -```java -public abstract class AbstractLoadBalance implements LoadBalance { - - static int calculateWarmupWeight(int uptime, int warmup, int weight) { - } - - @Override - public Invoker select(List> invokers, URL url, Invocation invocation) { - } - - protected abstract Invoker doSelect(List> invokers, URL url, Invocation invocation); - - - int getWeight(Invoker invoker, Invocation invocation) { - - } -} -``` - -`AbstractLoadBalance` 的实现类有下面这些: - -![](https://oss.javaguide.cn/java-guide-blog/image-20210326105257812.png) - -官方文档对负载均衡这部分的介绍非常详细,推荐小伙伴们看看,地址:[https://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/#m-zhdocsv27devsourceloadbalance](https://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/#m-zhdocsv27devsourceloadbalance) 。 - -#### RandomLoadBalance - -根据权重随机选择(对加权随机算法的实现)。这是 Dubbo 默认采用的一种负载均衡策略。 - -`RandomLoadBalance` 具体的实现原理非常简单,假如有两个提供相同服务的服务器 S1,S2,S1 的权重为 7,S2 的权重为 3。 - -我们把这些权重值分布在坐标区间会得到:S1->[0, 7) ,S2->[7, 10)。我们生成[0, 10) 之间的随机数,随机数落到对应的区间,我们就选择对应的服务器来处理请求。 - -![RandomLoadBalance](https://oss.javaguide.cn/java-guide-blog/%20RandomLoadBalance.png) - -`RandomLoadBalance` 的源码非常简单,简单花几分钟时间看一下。 - -> 以下源码来自 Dubbo master 分支上的最新的版本 2.7.9。 - -```java -public class RandomLoadBalance extends AbstractLoadBalance { - - public static final String NAME = "random"; - - @Override - protected Invoker doSelect(List> invokers, URL url, Invocation invocation) { - - int length = invokers.size(); - boolean sameWeight = true; - int[] weights = new int[length]; - int totalWeight = 0; - // 下面这个for循环的主要作用就是计算所有该服务的提供者的权重之和 totalWeight(), - // 除此之外,还会检测每个服务提供者的权重是否相同 - for (int i = 0; i < length; i++) { - int weight = getWeight(invokers.get(i), invocation); - totalWeight += weight; - weights[i] = totalWeight; - if (sameWeight && totalWeight != weight * (i + 1)) { - sameWeight = false; - } - } - if (totalWeight > 0 && !sameWeight) { - // 随机生成一个 [0, totalWeight) 区间内的数字 - int offset = ThreadLocalRandom.current().nextInt(totalWeight); - // 判断会落在哪个服务提供者的区间 - for (int i = 0; i < length; i++) { - if (offset < weights[i]) { - return invokers.get(i); - } - } - - return invokers.get(ThreadLocalRandom.current().nextInt(length)); - } - -} - -``` - -#### LeastActiveLoadBalance - -`LeastActiveLoadBalance` 直译过来就是**最小活跃数负载均衡**。 - -这个名字起得有点不直观,不仔细看官方对活跃数的定义,你压根不知道这玩意是干嘛的。 - -我这么说吧!初始状态下所有服务提供者的活跃数均为 0(每个服务提供者的中特定方法都对应一个活跃数,我在后面的源码中会提到),每收到一个请求后,对应的服务提供者的活跃数 +1,当这个请求处理完之后,活跃数 -1。 - -因此,**Dubbo 就认为谁的活跃数越少,谁的处理速度就越快,性能也越好,这样的话,我就优先把请求给活跃数少的服务提供者处理。** - -**如果有多个服务提供者的活跃数相等怎么办?** - -很简单,那就再走一遍 `RandomLoadBalance` 。 - -```java -public class LeastActiveLoadBalance extends AbstractLoadBalance { - - public static final String NAME = "leastactive"; - - @Override - protected Invoker doSelect(List> invokers, URL url, Invocation invocation) { - int length = invokers.size(); - int leastActive = -1; - int leastCount = 0; - int[] leastIndexes = new int[length]; - int[] weights = new int[length]; - int totalWeight = 0; - int firstWeight = 0; - boolean sameWeight = true; - // 这个 for 循环的主要作用是遍历 invokers 列表,找出活跃数最小的 Invoker - // 如果有多个 Invoker 具有相同的最小活跃数,还会记录下这些 Invoker 在 invokers 集合中的下标,并累加它们的权重,比较它们的权重值是否相等 - for (int i = 0; i < length; i++) { - Invoker invoker = invokers.get(i); - // 获取 invoker 对应的活跃(active)数 - int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); - int afterWarmup = getWeight(invoker, invocation); - weights[i] = afterWarmup; - if (leastActive == -1 || active < leastActive) { - leastActive = active; - leastCount = 1; - leastIndexes[0] = i; - totalWeight = afterWarmup; - firstWeight = afterWarmup; - sameWeight = true; - } else if (active == leastActive) { - leastIndexes[leastCount++] = i; - totalWeight += afterWarmup; - if (sameWeight && afterWarmup != firstWeight) { - sameWeight = false; - } - } - } - // 如果只有一个 Invoker 具有最小的活跃数,此时直接返回该 Invoker 即可 - if (leastCount == 1) { - return invokers.get(leastIndexes[0]); - } - // 如果有多个 Invoker 具有相同的最小活跃数,但它们之间的权重不同 - // 这里的处理方式就和 RandomLoadBalance 一致了 - if (!sameWeight && totalWeight > 0) { - int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight); - for (int i = 0; i < leastCount; i++) { - int leastIndex = leastIndexes[i]; - offsetWeight -= weights[leastIndex]; - if (offsetWeight < 0) { - return invokers.get(leastIndex); - } - } - } - return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]); - } -} - -``` - -活跃数是通过 `RpcStatus` 中的一个 `ConcurrentMap` 保存的,根据 URL 以及服务提供者被调用的方法的名称,我们便可以获取到对应的活跃数。也就是说服务提供者中的每一个方法的活跃数都是互相独立的。 - -```java -public class RpcStatus { - - private static final ConcurrentMap> METHOD_STATISTICS = - new ConcurrentHashMap>(); - - public static RpcStatus getStatus(URL url, String methodName) { - String uri = url.toIdentityString(); - ConcurrentMap map = METHOD_STATISTICS.computeIfAbsent(uri, k -> new ConcurrentHashMap<>()); - return map.computeIfAbsent(methodName, k -> new RpcStatus()); - } - public int getActive() { - return active.get(); - } - -} -``` - -#### ConsistentHashLoadBalance - -`ConsistentHashLoadBalance` 小伙伴们应该也不会陌生,在分库分表、各种集群中就经常使用这个负载均衡策略。 - -`ConsistentHashLoadBalance` 即**一致性 Hash 负载均衡策略**。 `ConsistentHashLoadBalance` 中没有权重的概念,具体是哪个服务提供者处理请求是由你的请求的参数决定的,也就是说相同参数的请求总是发到同一个服务提供者。 - -![](https://oss.javaguide.cn/java-guide-blog/consistent-hash-data-incline.jpg) - -另外,Dubbo 为了避免数据倾斜问题(节点不够分散,大量请求落到同一节点),还引入了虚拟节点的概念。通过虚拟节点可以让节点更加分散,有效均衡各个节点的请求量。 - -![](https://oss.javaguide.cn/java-guide-blog/consistent-hash-invoker.jpg) - -官方有详细的源码分析:[https://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/#23-consistenthashloadbalance](https://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/#23-consistenthashloadbalance) 。这里还有一个相关的 [PR#5440](https://github.com/apache/dubbo/pull/5440) 来修复老版本中 ConsistentHashLoadBalance 存在的一些 Bug。感兴趣的小伙伴,可以多花点时间研究一下。我这里不多分析了,这个作业留给你们! - -#### RoundRobinLoadBalance - -加权轮询负载均衡。 - -轮询就是把请求依次分配给每个服务提供者。加权轮询就是在轮询的基础上,让更多的请求落到权重更大的服务提供者上。比如假如有两个提供相同服务的服务器 S1,S2,S1 的权重为 7,S2 的权重为 3。 - -如果我们有 10 次请求,那么 7 次会被 S1 处理,3 次被 S2 处理。 - -但是,如果是 `RandomLoadBalance` 的话,很可能存在 10 次请求有 9 次都被 S1 处理的情况(概率性问题)。 - -Dubbo 中的 `RoundRobinLoadBalance` 的代码实现被修改重建了好几次,Dubbo-2.6.5 版本的 `RoundRobinLoadBalance` 为平滑加权轮询算法。 - -## Dubbo 序列化协议 - -### Dubbo 支持哪些序列化方式呢? - -![Dubbo 支持的序列化协议](https://oss.javaguide.cn/github/javaguide/csdn/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70-20230309234143460.png) - -Dubbo 支持多种序列化方式:JDK 自带的序列化、hessian2、JSON、Kryo、FST、Protostuff,ProtoBuf 等等。 - -Dubbo 默认使用的序列化方式是 hessian2。 - -### 谈谈你对这些序列化协议了解? - -一般我们不会直接使用 JDK 自带的序列化方式。主要原因有两个: - -1. **不支持跨语言调用** : 如果调用的是其他语言开发的服务的时候就不支持了。 -2. **性能差**:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。 - -JSON 序列化由于性能问题,我们一般也不会考虑使用。 - -像 Protostuff,ProtoBuf、hessian2 这些都是跨语言的序列化方式,如果有跨语言需求的话可以考虑使用。 - -Kryo 和 FST 这两种序列化方式是 Dubbo 后来才引入的,性能非常好。不过,这两者都是专门针对 Java 语言的。Dubbo 官网的一篇文章中提到说推荐使用 Kryo 作为生产环境的序列化方式。 - -Dubbo 官方文档中还有一个关于这些[序列化协议的性能对比图](https://dubbo.apache.org/zh/docs/v2.7/user/serialization/#m-zhdocsv27userserialization)可供参考。 - -![序列化协议的性能对比](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/dubbo-serialization-protocol-performance-comparison.png) - - +Distribution, or SOA, is fundamentally service-oriented. In simple terms, distribution means splitting the entire system into different services and placing these services on different servers to reduce the pressure on monolithic services and diff --git a/docs/distributed-system/rpc/http&rpc.md b/docs/distributed-system/rpc/http&rpc.md index 35301d0bceb..ed51c28908d 100644 --- a/docs/distributed-system/rpc/http&rpc.md +++ b/docs/distributed-system/rpc/http&rpc.md @@ -1,196 +1,196 @@ --- -title: 有了 HTTP 协议,为什么还要有 RPC ? -category: 分布式 +title: With HTTP Protocol, Why is RPC Still Needed? +category: Distributed tag: - rpc --- -> 本文来自[小白 debug](https://juejin.cn/user/4001878057422087)投稿,原文: 。 +> This article is contributed by [Xiao Bai debug](https://juejin.cn/user/4001878057422087), original text: . -我想起了我刚工作的时候,第一次接触 RPC 协议,当时就很懵,我 HTTP 协议用的好好的,为什么还要用 RPC 协议? +I remember when I first started working and encountered RPC protocol for the first time. I was confused; I was using HTTP protocol just fine, so why do we need RPC protocol? -于是就到网上去搜。 +So, I searched online. -不少解释显得非常官方,我相信大家在各种平台上也都看到过,解释了又好像没解释,都在**用一个我们不认识的概念去解释另外一个我们不认识的概念**,懂的人不需要看,不懂的人看了还是不懂。 +Many explanations seemed very official. I'm sure everyone has seen them on various platforms, explaining but seemingly not explaining; they all used **one concept we don't understand to explain another concept we don't understand**. Those who understood didn't need to read it, and those who didn't understand remained confused. -这种看了,又好像没看的感觉,云里雾里的很难受,**我懂**。 +That feeling of having read but not having grasped it is quite uncomfortable, **I understand**. -为了避免大家有强烈的**审丑疲劳**,今天我们来尝试重新换个方式讲一讲。 +To avoid eliciting strong **aesthetic fatigue**, let's try to explain this in a different way today. -## 从 TCP 聊起 +## Let's Start with TCP -作为一个程序员,假设我们需要在 A 电脑的进程发一段数据到 B 电脑的进程,我们一般会在代码里使用 socket 进行编程。 +As a programmer, suppose we need to send some data from a process on computer A to a process on computer B, we typically use sockets in our code for programming. -这时候,我们可选项一般也就**TCP 和 UDP 二选一。TCP 可靠,UDP 不可靠。** 除非是马总这种神级程序员(早期 QQ 大量使用 UDP),否则,只要稍微对可靠性有些要求,普通人一般无脑选 TCP 就对了。 +At this point, our options generally come down to **TCP or UDP**. TCP is reliable, while UDP is not. Unless someone like Ma Huateng, a legendary programmer (who heavily used UDP in early QQ), opts for UDP, most ordinary people tend to choose TCP without thinking if there's any requirement for reliability. -类似下面这样。 +It looks something like this. ```ini fd = socket(AF_INET,SOCK_STREAM,0); ``` -其中`SOCK_STREAM`,是指使用**字节流**传输数据,说白了就是**TCP 协议**。 +Where `SOCK_STREAM` indicates the use of **byte stream** to transmit data, which simply means **TCP protocol**. -在定义了 socket 之后,我们就可以愉快的对这个 socket 进行操作,比如用`bind()`绑定 IP 端口,用`connect()`发起建连。 +After defining the socket, we can happily perform operations on it, such as using `bind()` to bind the IP port and `connect()` to initiate a connection. -![握手建立连接流程](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/f410977cda814d32b0eff3645c385a8a~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) +![Handshake Connection Process](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/f410977cda814d32b0eff3645c385a8a~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) -在连接建立之后,我们就可以使用`send()`发送数据,`recv()`接收数据。 +Once the connection is established, we can use `send()` to send data and `recv()` to receive data. -光这样一个纯裸的 TCP 连接,就可以做到收发数据了,那是不是就够了? +A pure naked TCP connection can achieve data transmission; isn't that enough? -不行,这么用会有问题。 +No, there are problems with using it this way. -## 使用纯裸 TCP 会有什么问题 +## What are the Problems with Using Pure Naked TCP? -八股文常背,TCP 是有三个特点,**面向连接**、**可靠**、基于**字节流**。 +It's often repeated that TCP has three characteristics: **connection-oriented**, **reliable**, and based on **byte stream**. -![TCP是什么](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/acb4508111cb47d8a3df6734d04818bc~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) +![What is TCP](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/acb4508111cb47d8a3df6734d04818bc~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) -这三个特点真的概括的 **非常精辟** ,这个八股文我们没白背。 +These three characteristics are indeed **very concise**; we didn’t memorize them for nothing. -每个特点展开都能聊一篇文章,而今天我们需要关注的是 **基于字节流** 这一点。 +Each characteristic can be discussed in its own article, but today we need to focus on **byte stream**. -字节流可以理解为一个双向的通道里流淌的二进制数据,也就是 **01 串** 。纯裸 TCP 收发的这些 01 串之间是 **没有任何边界** 的,你根本不知道到哪个地方才算一条完整消息。 +A byte stream can be understood as flowing binary data in a bidirectional channel, which is essentially a string of **01**. The 01 strings transmitted by pure naked TCP have **no boundaries** between them; you have no idea where a complete message starts and ends. -![01二进制字节流](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/b82d4fcdd0c4491e979856c93c1750d7~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) +![Binary Byte Stream](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/b82d4fcdd0c4491e979856c93c1750d7~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) -正因为这个没有任何边界的特点,所以当我们选择使用 TCP 发送 **"夏洛"和"特烦恼"** 的时候,接收端收到的就是 **"夏洛特烦恼"** ,这时候接收端没发区分你是想要表达 **"夏洛"+"特烦恼"** 还是 **"夏洛特"+"烦恼"** 。 +Because of this lack of boundaries, when we choose to send **"Xia Luo" and "Te Fan Nao"** over TCP, the receiving end gets **"Xia Luo Te Fan Nao."** At this point, the receiver cannot distinguish whether you meant to express **"Xia Luo" + "Te Fan Nao"** or **"Xia Luo Te" + "Fan Nao."** -![消息对比](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/4e120d0f1152419585565f693e744a3a~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) +![Message Comparison](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/4e120d0f1152419585565f693e744a3a~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) -这就是所谓的 **粘包问题**,之前也写过一篇专门的[文章](https://mp.weixin.qq.com/s/0-YBxU1cSbDdzcZEZjmQYA)聊过这个问题。 +This is known as the **sticky packet problem**, and I have previously written a dedicated [article](https://mp.weixin.qq.com/s/0-YBxU1cSbDdzcZEZjmQYA) discussing this issue. -说这个的目的是为了告诉大家,纯裸 TCP 是不能直接拿来用的,你需要在这个基础上加入一些 **自定义的规则** ,用于区分 **消息边界** 。 +The point of mentioning this is to let everyone know that pure naked TCP cannot be used directly; you need to add some **custom rules** to distinguish **message boundaries.** -于是我们会把每条要发送的数据都包装一下,比如加入 **消息头** ,消息头里写清楚一个完整的包长度是多少,根据这个长度可以继续接收数据,截取出来后它们就是我们真正要传输的 **消息体** 。 +So, we typically wrap each piece of data to be sent, such as adding a **message header** that clearly states the length of a complete packet. Based on this length, we can continue receiving data and extract it, which forms our actual **message body**. -![消息边界长度标志](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/cb29659d4907446e9f70551c44c6369f~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) +![Message Boundary Length Indicator](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/cb29659d4907446e9f70551c44c6369f~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) -而这里头提到的 **消息头** ,还可以放各种东西,比如消息体是否被压缩过和消息体格式之类的,只要上下游都约定好了,互相都认就可以了,这就是所谓的 **协议。** +The mentioned **message header** can also contain various pieces of information, such as whether the message body is compressed and the format of the message body. As long as both ends agree on the protocol, it's fine. -每个使用 TCP 的项目都可能会定义一套类似这样的协议解析标准,他们可能 **有区别,但原理都类似**。 +Every project using TCP may define a set of parsing standards similar to this; they may **differ but the principles are similar.** -**于是基于 TCP,就衍生了非常多的协议,比如 HTTP 和 RPC。** +**Thus, many protocols have emerged based on TCP, such as HTTP and RPC.** -## HTTP 和 RPC +## HTTP and RPC -### RPC 其实是一种调用方式 +### RPC is Essentially a Calling Method -我们回过头来看网络的分层图。 +Let’s turn back to the network layering diagram. -![四层网络协议](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/04b603b5bd2443209233deea87816161~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) +![Four-Layer Network Protocol](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/04b603b5bd2443209233deea87816161~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) -**TCP 是传输层的协议** ,而基于 TCP 造出来的 HTTP 和各类 RPC 协议,它们都只是定义了不同消息格式的 **应用层协议** 而已。 +**TCP is a transport layer protocol**, while HTTP and various RPC protocols built on TCP are merely **application layer protocols** that define different message formats. -**HTTP**(**H**yper **T**ext **T**ransfer **P**rotocol)协议又叫做 **超文本传输协议** 。我们用的比较多,平时上网在浏览器上敲个网址就能访问网页,这里用到的就是 HTTP 协议。 +**HTTP** (Hyper Text Transfer Protocol) is commonly known as the **hypertext transfer protocol**. We use it quite often; when browsing the internet, we can access a webpage by typing a URL in the browser, which utilizes the HTTP protocol. -![HTTP调用](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/8f07a5d1c72a4c4fa811c6c3b5aadd3d~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) +![HTTP Call](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/8f07a5d1c72a4c4fa811c6c3b5aadd3d~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) -而 **RPC**(**R**emote **P**rocedure **C**all)又叫做 **远程过程调用**,它本身并不是一个具体的协议,而是一种 **调用方式** 。 +On the other hand, **RPC** (Remote Procedure Call) is also known as **remote procedure invocation**. It is not a specific protocol; it represents a **calling method**. -举个例子,我们平时调用一个 **本地方法** 就像下面这样。 +For example, when we usually call a **local method**, it looks something like this. ```ini - res = localFunc(req) +res = localFunc(req) ``` -如果现在这不是个本地方法,而是个**远端服务器**暴露出来的一个方法`remoteFunc`,如果我们还能像调用本地方法那样去调用它,这样就可以**屏蔽掉一些网络细节**,用起来更方便,岂不美哉? +If now this isn't a local method, but a method `remoteFunc` exposed by a **remote server**, and we can still call it just like a local method, it can **mask some network details**, making it more convenient, wouldn't that be great? ```ini res = remoteFunc(req) ``` -![RPC可以像调用本地方法那样调用远端方法](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/761da6c30af244e19b1c44075d8b4254~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) +![RPC Can Call Remote Methods Like Local Methods](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/761da6c30af244e19b1c44075d8b4254~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) -基于这个思路,大佬们造出了非常多款式的 RPC 协议,比如比较有名的`gRPC`,`thrift`。 +Based on this idea, many different types of RPC protocols have been created, such as the well-known `gRPC` and `thrift`. -值得注意的是,虽然大部分 RPC 协议底层使用 TCP,但实际上 **它们不一定非得使用 TCP,改用 UDP 或者 HTTP,其实也可以做到类似的功能。** +It's important to note that although most RPC protocols use TCP underneath, in reality, **they don't have to use TCP; they could also use UDP or HTTP, achieving similar functionalities.** -到这里,我们回到文章标题的问题。 +Now, let's return to the question posed in the article title. -### 那既然有 RPC 了,为什么还要有 HTTP 呢? +### If RPC Exists, Why is HTTP Still Needed? -其实,TCP 是 **70 年** 代出来的协议,而 HTTP 是 **90 年代** 才开始流行的。而直接使用裸 TCP 会有问题,可想而知,这中间这么多年有多少自定义的协议,而这里面就有 **80 年代** 出来的`RPC`。 +In fact, TCP was a protocol developed in the **1970s**, while HTTP didn't begin to gain popularity until the **1990s**. Given that directly using naked TCP has its problems, it’s easy to see how many custom protocols have emerged over the years; and RPC was developed in the **1980s**. -所以我们该问的不是 **既然有 HTTP 协议为什么要有 RPC** ,而是 **为什么有 RPC 还要有 HTTP 协议?** +Therefore, we should not ask **why RPC exists alongside HTTP** but rather **why does HTTP still exist alongside RPC?** -现在电脑上装的各种联网软件,比如 xx 管家,xx 卫士,它们都作为客户端(Client) 需要跟服务端(Server) 建立连接收发消息,此时都会用到应用层协议,在这种 Client/Server (C/S) 架构下,它们可以使用自家造的 RPC 协议,因为它只管连自己公司的服务器就 ok 了。 +Various networked applications installed on computers, such as xx Manager and xx Guardian, act as clients requiring connection to servers to send and receive messages; they utilize application layer protocols. In this Client/Server (C/S) architecture, they can use their own RPC protocols since they only need to connect to their company’s server. -但有个软件不同,浏览器(Browser) ,不管是 Chrome 还是 IE,它们不仅要能访问自家公司的**服务器(Server)** ,还需要访问其他公司的网站服务器,因此它们需要有个统一的标准,不然大家没法交流。于是,HTTP 就是那个时代用于统一 **Browser/Server (B/S)** 的协议。 +However, one type of software is different: browsers (Browser). Whether it’s Chrome or IE, they need not only to access their own company’s **server** but also to reach websites hosted by other companies; therefore, they need a unified standard, otherwise, communication would be impossible. Consequently, HTTP serves as that standard for unifying **Browser/Server (B/S)** communication. -也就是说在多年以前,**HTTP 主要用于 B/S 架构,而 RPC 更多用于 C/S 架构。但现在其实已经没分那么清了,B/S 和 C/S 在慢慢融合。** 很多软件同时支持多端,比如某度云盘,既要支持**网页版**,还要支持**手机端和 PC 端**,如果通信协议都用 HTTP 的话,那服务器只用同一套就够了。而 RPC 就开始退居幕后,一般用于公司内部集群里,各个微服务之间的通讯。 +In other words, many years ago, **HTTP was primarily used for B/S architecture, while RPC was more for C/S architecture. However, such distinctions have blurred over time; B/S and C/S are gradually merging.** Many applications now support multiple platforms, such as a certain cloud storage service needing both **web** and **mobile/PC** support. If communication protocols only used HTTP, the server would only need to deal with one set of protocols. Consequently, RPC has begun to take a backseat and is generally used for communication between various microservices within company internal clusters. -那这么说的话,**都用 HTTP 得了,还用什么 RPC?** +So that raises the question: **Why not just use HTTP; why use RPC at all?** -仿佛又回到了文章开头的样子,那这就要从它们之间的区别开始说起。 +It feels like we're back at the beginning of the article, so we’ll need to discuss the differences between them. -### HTTP 和 RPC 有什么区别 +### What Are the Differences Between HTTP and RPC? -我们来看看 RPC 和 HTTP 区别比较明显的几个点。 +Let's look at a few points that clearly distinguish RPC from HTTP. -#### 服务发现 +#### Service Discovery -首先要向某个服务器发起请求,你得先建立连接,而建立连接的前提是,你得知道 **IP 地址和端口** 。这个找到服务对应的 IP 端口的过程,其实就是 **服务发现**。 +To initiate a request to a server, you must first establish a connection, and the prerequisite for establishing that connection is knowing the **IP address and port**. The process of finding the service's corresponding IP and port is known as **service discovery**. -在 **HTTP** 中,你知道服务的域名,就可以通过 **DNS 服务** 去解析得到它背后的 IP 地址,默认 **80 端口**。 +In **HTTP**, if you know the domain name of the service, you can resolve the IP address behind it through **DNS services**, typically on the **default port 80**. -而 **RPC** 的话,就有些区别,一般会有专门的中间服务去保存服务名和 IP 信息,比如 **Consul、Etcd、Nacos、ZooKeeper,甚至是 Redis**。想要访问某个服务,就去这些中间服务去获得 IP 和端口信息。由于 DNS 也是服务发现的一种,所以也有基于 DNS 去做服务发现的组件,比如 **CoreDNS**。 +In the case of **RPC**, there are some differences. Typically, there will be dedicated intermediary services responsible for storing service names and IP information, such as **Consul, Etcd, Nacos, ZooKeeper, or even Redis**. To access a certain service, you'd query these intermediary services to obtain the IP and port information. Since DNS also functions as a form of service discovery, there exist components that achieve service discovery based on DNS, such as **CoreDNS**. -可以看出服务发现这一块,两者是有些区别,但不太能分高低。 +It's evident that the service discovery aspect does show some differences between the two, but they are not significantly superior or inferior to one another. -#### 底层连接形式 +#### Underlying Connection Method -以主流的 **HTTP1.1** 协议为例,其默认在建立底层 TCP 连接之后会一直保持这个连接(**keep alive**),之后的请求和响应都会复用这条连接。 +Taking the mainstream **HTTP/1.1** protocol as an example, its default behavior is to maintain a TCP connection once established (the **keep-alive** feature), allowing subsequent requests and responses to reuse this connection. -而 **RPC** 协议,也跟 HTTP 类似,也是通过建立 TCP 长链接进行数据交互,但不同的地方在于,RPC 协议一般还会再建个 **连接池**,在请求量大的时候,建立多条连接放在池内,要发数据的时候就从池里取一条连接出来,用完放回去,下次再复用,可以说非常环保。 +On the other hand, **RPC protocols** also establish TCP long connections for data interactions, but unlike HTTP, RPC protocols typically create a **connection pool**. During times of high request volume, multiple connections are established and kept in the pool; when data needs to be sent, a connection is retrieved from the pool, used, and then returned for future reuse, which is quite efficient. -![connection_pool](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/72fcad064c9e4103a11f1a2d579f79b2~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) +![Connection Pool](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/72fcad064c9e4103a11f1a2d579f79b2~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) -由于连接池有利于提升网络请求性能,所以不少编程语言的网络库里都会给 HTTP 加个连接池,比如 Go 就是这么干的。 +Connection pools help improve network request performance, which is why many programming languages' network libraries introduce a connection pool for HTTP, as seen in Go. -可以看出这一块两者也没太大区别,所以也不是关键。 +From this perspective, there isn’t a significant distinction in practices between them, so it’s not a key difference either. -#### 传输的内容 +#### Content Being Transmitted -基于 TCP 传输的消息,说到底,无非都是 **消息头 Header 和消息体 Body。** +Messages transmitted using TCP ultimately consist of **header and body**. -**Header** 是用于标记一些特殊信息,其中最重要的是 **消息体长度**。 +**Header** is used to denote certain special information, with the most important aspect being the **length of the message body**. -**Body** 则是放我们真正需要传输的内容,而这些内容只能是二进制 01 串,毕竟计算机只认识这玩意。所以 TCP 传字符串和数字都问题不大,因为字符串可以转成编码再变成 01 串,而数字本身也能直接转为二进制。但结构体呢,我们得想个办法将它也转为二进制 01 串,这样的方案现在也有很多现成的,比如 **JSON,Protocol Buffers (Protobuf)** 。 +**Body** contains the actual content we need to transmit, which must be represented in binary as 01 strings since computers only recognize this format. Thus, TCP can handle strings and numbers without issue, because strings can be encoded as 01 strings, and numbers can be directly converted into binary. For structures, however, we need a method to convert them into binary 01 strings, and there are many existing solutions for this, such as **JSON** and **Protocol Buffers (Protobuf)**. -这个将结构体转为二进制数组的过程就叫 **序列化** ,反过来将二进制数组复原成结构体的过程叫 **反序列化**。 +The process of converting a structure into a binary array is called **serialization**, while reverting a binary array back into a structure is called **deserialization**. -![序列化和反序列化](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/d501dfc6f764430188ce61fda0f3e5d9~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) +![Serialization and Deserialization](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/d501dfc6f764430188ce61fda0f3e5d9~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) -对于主流的 HTTP1.1,虽然它现在叫超文本协议,支持音频视频,但 HTTP 设计 初是用于做网页文本展示的,所以它传的内容以字符串为主。Header 和 Body 都是如此。在 Body 这块,它使用 **JSON** 来 **序列化** 结构体数据。 +For mainstream HTTP/1.1, although it is referred to as a hypertext protocol—supporting audio and video—HTTP was originally designed primarily for displaying web text, so the content it transmits is predominantly string-based. Both the header and body follow this pattern. In the body section, it employs **JSON** to **serialize** structured data. -我们可以随便截个图直观看下。 +We can capture a snapshot for a more intuitive view. -![HTTP报文](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/04e8a79ddb7247759df23f1132c01655~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) +![HTTP Message](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/04e8a79ddb7247759df23f1132c01655~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) -可以看到这里面的内容非常多的冗余,显得非常啰嗦。最明显的,像 Header 里的那些信息,其实如果我们约定好头部的第几位是 `Content-Type`,就不需要每次都真的把 `Content-Type` 这个字段都传过来,类似的情况其实在 Body 的 JSON 结构里也特别明显。 +The content here exhibits significant redundancy and appears verbose. Most noticeably, information in the header (like `Content-Type`) could be effectively conveyed by agreeing on a specific bit's position in the header instead of transmitting the `Content-Type` field literally each time. Similar situations can also be observed in the body's JSON structure. -而 RPC,因为它定制化程度更高,可以采用体积更小的 Protobuf 或其他序列化协议去保存结构体数据,同时也不需要像 HTTP 那样考虑各种浏览器行为,比如 302 重定向跳转啥的。**因此性能也会更好一些,这也是在公司内部微服务中抛弃 HTTP,选择使用 RPC 的最主要原因。** +In contrast, RPC, due to its higher level of customization, can utilize smaller serialization formats like Protobuf to retain structured data, without needing to accommodate various browser behaviors (like 302 redirection). **Thus, its performance can be better, which is a major reason for opting for RPC instead of HTTP in internal microservice communications.** -![HTTP原理](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/284c26bb7f2848889d1d9b95cf49decb~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) +![HTTP Principle](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/284c26bb7f2848889d1d9b95cf49decb~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) -![RPC原理](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/edb050d383c644e895e505253f1c4d90~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) +![RPC Principle](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/edb050d383c644e895e505253f1c4d90~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) -当然上面说的 HTTP,其实 **特指的是现在主流使用的 HTTP1.1**,`HTTP2`在前者的基础上做了很多改进,所以 **性能可能比很多 RPC 协议还要好**,甚至连`gRPC`底层都直接用的`HTTP2`。 +It’s worth noting that the HTTP referred to here specifically denotes the currently mainstream HTTP/1.1. `HTTP/2` has made many improvements on top of this, so **its performance may exceed many RPC protocols**, with `gRPC` directly utilizing `HTTP/2` at its core. -那么问题又来了。 +This prompts yet another question. -### 为什么既然有了 HTTP2,还要有 RPC 协议? +### Why, Since We've Emerged with HTTP/2, Do We Still Need RPC Protocols? -这个是由于 HTTP2 是 2015 年出来的。那时候很多公司内部的 RPC 协议都已经跑了好些年了,基于历史原因,一般也没必要去换了。 +The reason is that HTTP/2 was introduced in 2015, by which time many internal RPC protocols had already been running for several years; thus, due to historical reasons, there typically wasn’t a pressing need to shift to alternatives. -## 总结 +## Conclusion -- 纯裸 TCP 是能收发数据,但它是个无边界的数据流,上层需要定义消息格式用于定义 **消息边界** 。于是就有了各种协议,HTTP 和各类 RPC 协议就是在 TCP 之上定义的应用层协议。 -- **RPC 本质上不算是协议,而是一种调用方式**,而像 gRPC 和 Thrift 这样的具体实现,才是协议,它们是实现了 RPC 调用的协议。目的是希望程序员能像调用本地方法那样去调用远端的服务方法。同时 RPC 有很多种实现方式,**不一定非得基于 TCP 协议**。 -- 从发展历史来说,**HTTP 主要用于 B/S 架构,而 RPC 更多用于 C/S 架构。但现在其实已经没分那么清了,B/S 和 C/S 在慢慢融合。** 很多软件同时支持多端,所以对外一般用 HTTP 协议,而内部集群的微服务之间则采用 RPC 协议进行通讯。 -- RPC 其实比 HTTP 出现的要早,且比目前主流的 HTTP1.1 性能要更好,所以大部分公司内部都还在使用 RPC。 -- **HTTP2.0** 在 **HTTP1.1** 的基础上做了优化,性能可能比很多 RPC 协议都要好,但由于是这几年才出来的,所以也不太可能取代掉 RPC。 +- Pure naked TCP can send and receive data, but it is a borderless data stream, requiring the upper layers to define a message format to establish **message boundaries**. Thus, various application layer protocols, including HTTP and various RPC protocols, are defined on top of TCP. +- **RPC is not essentially a protocol but a method of invocation**, while implementations like gRPC and Thrift are the protocols that realize RPC calls. The goal is to allow programmers to invoke remote service methods like local methods. Moreover, there are various ways to implement RPC, **and it does not have to be based on the TCP protocol**. +- From a historical development perspective, **HTTP was primarily used for B/S architecture, while RPC was more oriented towards C/S architecture. However, now the distinction has become less clear as B/S and C/S are gradually merging.** Many applications support multiple platforms, making HTTP generally suitable for external communications, while RPC protocols are used for communication within internal clusters. +- RPC actually predates HTTP and tends to perform better than the currently mainstream HTTP/1.1, which is why most companies still utilize RPC internally. +- **HTTP/2** has been optimized upon **HTTP/1.1**, potentially outperforming many RPC protocols; however, it was introduced only in recent years, making it more challenging to replace RPC quickly. diff --git a/docs/distributed-system/rpc/rpc-intro.md b/docs/distributed-system/rpc/rpc-intro.md index d2c5fb5e9c7..8d5e2801222 100644 --- a/docs/distributed-system/rpc/rpc-intro.md +++ b/docs/distributed-system/rpc/rpc-intro.md @@ -1,141 +1,138 @@ --- -title: RPC基础知识总结 -category: 分布式 +title: Summary of Basic RPC Knowledge +category: Distributed tag: - rpc --- -这篇文章会简单介绍一下 RPC 相关的基础概念。 +This article will briefly introduce the basic concepts related to RPC. -## RPC 是什么? +## What is RPC? -**RPC(Remote Procedure Call)** 即远程过程调用,通过名字我们就能看出 RPC 关注的是远程调用而非本地调用。 +**RPC (Remote Procedure Call)** refers to remote procedure calls, and from its name, we can see that RPC focuses on remote calls rather than local calls. -**为什么要 RPC ?** 因为,两个不同的服务器上的服务提供的方法不在一个内存空间,所以,需要通过网络编程才能传递方法调用所需要的参数。并且,方法调用的结果也需要通过网络编程来接收。但是,如果我们自己手动网络编程来实现这个调用过程的话工作量是非常大的,因为,我们需要考虑底层传输方式(TCP 还是 UDP)、序列化方式等等方面。 +**Why use RPC?** Because the methods provided by services on two different servers do not share the same memory space, it is necessary to use network programming to pass the parameters required for the method call. Furthermore, the results of the method call also need to be received through network programming. However, if we were to manually implement this calling process through network programming, the workload would be extensive, as we need to consider various factors such as the underlying transmission method (TCP or UDP), serialization methods, and more. -**RPC 能帮助我们做什么呢?** 简单来说,通过 RPC 可以帮助我们调用远程计算机上某个服务的方法,这个过程就像调用本地方法一样简单。并且!我们不需要了解底层网络编程的具体细节。 +**What can RPC help us with?** Simply put, RPC allows us to call a method from a service on a remote computer as easily as calling a local method. Moreover, we do not need to understand the specific details of underlying network programming. -举个例子:两个不同的服务 A、B 部署在两台不同的机器上,服务 A 如果想要调用服务 B 中的某个方法的话就可以通过 RPC 来做。 +For example, if service A, deployed on one machine, wants to call a method from service B, deployed on another machine, it can do so via RPC. -一言蔽之:**RPC 的出现就是为了让你调用远程方法像调用本地方法一样简单。** +In short: **The purpose of RPC is to make calling remote methods as simple as calling local methods.** -## RPC 的原理是什么? +## What is the principle of RPC? -为了能够帮助小伙伴们理解 RPC 原理,我们可以将整个 RPC 的 核心功能看作是下面 👇 5 个部分实现的: +To help understand the principle of RPC, we can view the entire core functionality of RPC as being implemented in the following five parts: -1. **客户端(服务消费端)**:调用远程方法的一端。 -1. **客户端 Stub(桩)**:这其实就是一代理类。代理类主要做的事情很简单,就是把你调用方法、类、方法参数等信息传递到服务端。 -1. **网络传输**:网络传输就是你要把你调用的方法的信息比如说参数啊这些东西传输到服务端,然后服务端执行完之后再把返回结果通过网络传输给你传输回来。网络传输的实现方式有很多种比如最基本的 Socket 或者性能以及封装更加优秀的 Netty(推荐)。 -1. **服务端 Stub(桩)**:这个桩就不是代理类了。我觉得理解为桩实际不太好,大家注意一下就好。这里的服务端 Stub 实际指的就是接收到客户端执行方法的请求后,去执行对应的方法然后返回结果给客户端的类。 -1. **服务端(服务提供端)**:提供远程方法的一端。 +1. **Client (Service Consumer)**: The end that calls the remote method. +1. **Client Stub**: This is essentially a proxy class. Its main responsibility is to pass information about the method call, class, and method parameters to the server. +1. **Network Transmission**: This involves sending the information regarding the method call, such as parameters, to the server. After the server completes execution, it sends the return result back to you via network transmission. There are many ways to implement network transmission; for example, the basic Socket or the more efficient and encapsulated Netty (recommended). +1. **Server Stub**: This stub is not a proxy class. It's important to note here that the server stub refers to the class that receives the method execution request from the client, executes the corresponding method, and then returns the result to the client. +1. **Server (Service Provider)**: The end that provides the remote method. -具体原理图如下,后面我会串起来将整个 RPC 的过程给大家说一下。 +The detailed principle diagram is as follows, and later I will explain the entire RPC process to everyone. -![RPC原理图](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/37345851.jpg) +![RPC Principle Diagram](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/37345851.jpg) -1. 服务消费端(client)以本地调用的方式调用远程服务; -1. 客户端 Stub(client stub) 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体(序列化):`RpcRequest`; -1. 客户端 Stub(client stub) 找到远程服务的地址,并将消息发送到服务提供端; -1. 服务端 Stub(桩)收到消息将消息反序列化为 Java 对象: `RpcRequest`; -1. 服务端 Stub(桩)根据`RpcRequest`中的类、方法、方法参数等信息调用本地的方法; -1. 服务端 Stub(桩)得到方法执行结果并将组装成能够进行网络传输的消息体:`RpcResponse`(序列化)发送至消费方; -1. 客户端 Stub(client stub)接收到消息并将消息反序列化为 Java 对象:`RpcResponse` ,这样也就得到了最终结果。over! +1. The service consumer (client) calls the remote service as if it were a local call; +1. The client stub receives the call and is responsible for assembling the method, parameters, etc., into a message body for network transmission (serialization): `RpcRequest`; +1. The client stub finds the address of the remote service and sends the message to the service provider; +1. The server stub receives the message and deserializes it into a Java object: `RpcRequest`; +1. The server stub invokes the local method based on the class, method, and method parameters in the `RpcRequest`; +1. The server stub obtains the execution result and assembles it into a message body for network transmission: `RpcResponse` (serialization) to send back to the consumer; +1. The client stub receives the message and deserializes it into a Java object: `RpcResponse`, thus obtaining the final result. Over! -相信小伙伴们看完上面的讲解之后,已经了解了 RPC 的原理。 +I believe that after reading the explanation above, everyone understands the principle of RPC. -虽然篇幅不多,但是基本把 RPC 框架的核心原理讲清楚了!另外,对于上面的技术细节,我会在后面的章节介绍到。 +Although the content is not extensive, it effectively clarifies the core principles of RPC frameworks! Additionally, I will introduce the technical details mentioned above in subsequent sections. -**最后,对于 RPC 的原理,希望小伙伴不单单要理解,还要能够自己画出来并且能够给别人讲出来。因为,在面试中这个问题在面试官问到 RPC 相关内容的时候基本都会碰到。** +**Finally, when it comes to the principles of RPC, I hope everyone not only understands but can also reproduce it and explain it to others. This is a common question you will encounter in interviews when the interviewer asks about RPC-related content.** -## 有哪些常见的 RPC 框架? +## What are some common RPC frameworks? -我们这里说的 RPC 框架指的是可以让客户端直接调用服务端方法,就像调用本地方法一样简单的框架,比如我下面介绍的 Dubbo、Motan、gRPC 这些。 如果需要和 HTTP 协议打交道,解析和封装 HTTP 请求和响应。这类框架并不能算是“RPC 框架”,比如 Feign。 +The RPC frameworks we refer to here are those that allow clients to directly call server methods as easily as calling local methods, such as Dubbo, Motan, and gRPC that I will introduce below. If you need to interact with the HTTP protocol, parse, and encapsulate HTTP requests and responses, those frameworks cannot be classified as "RPC frameworks," such as Feign. ### Dubbo ![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/image-20220716111053081.png) -Apache Dubbo 是一款微服务框架,为大规模微服务实践提供高性能 RPC 通信、流量治理、可观测性等解决方案, -涵盖 Java、Golang 等多种语言 SDK 实现。 +Apache Dubbo is a microservice framework that provides high-performance RPC communication, traffic governance, observability, and other solutions for large-scale microservices practices, covering various language SDK implementations including Java and Golang. -Dubbo 提供了从服务定义、服务发现、服务通信到流量管控等几乎所有的服务治理能力,支持 Triple 协议(基于 HTTP/2 之上定义的下一代 RPC 通信协议)、应用级服务发现、Dubbo Mesh (Dubbo3 赋予了很多云原生友好的新特性)等特性。 +Dubbo offers almost all service governance capabilities, from service definition, service discovery, service communication to traffic control, supporting the Triple protocol (the next generation RPC communication protocol defined over HTTP/2), application-level service discovery, and Dubbo Mesh (Dubbo3 has introduced many cloud-native friendly new features). ![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/image-20220716111545343.png) -Dubbo 是由阿里开源,后来加入了 Apache 。正是由于 Dubbo 的出现,才使得越来越多的公司开始使用以及接受分布式架构。 +Dubbo was open-sourced by Alibaba and later joined Apache. It is due to the emergence of Dubbo that more and more companies have begun to adopt and accept distributed architectures. -Dubbo 算的是比较优秀的国产开源项目了,它的源码也是非常值得学习和阅读的! +Dubbo is considered one of the more excellent domestic open-source projects, and its source code is worthy of study and reading! -- GitHub:[https://github.com/apache/incubator-dubbo](https://github.com/apache/incubator-dubbo "https://github.com/apache/incubator-dubbo") -- 官网: +- GitHub: [https://github.com/apache/incubator-dubbo](https://github.com/apache/incubator-dubbo "https://github.com/apache/incubator-dubbo") +- Official Website: ### Motan -Motan 是新浪微博开源的一款 RPC 框架,据说在新浪微博正支撑着千亿次调用。不过笔者倒是很少看到有公司使用,而且网上的资料也比较少。 +Motan is an RPC framework open-sourced by Sina Weibo, reportedly supporting billions of calls at Sina Weibo. However, the author has rarely seen any companies using it, and there are also relatively few resources available online. -很多人喜欢拿 Motan 和 Dubbo 作比较,毕竟都是国内大公司开源的。笔者在查阅了很多资料,以及简单查看了其源码之后发现:**Motan 更像是一个精简版的 Dubbo,可能是借鉴了 Dubbo 的思想,Motan 的设计更加精简,功能更加纯粹。** +Many people like to compare Motan with Dubbo, as both are open-sourced by large domestic companies. After reviewing many materials and briefly checking the source code, I found that **Motan is more like a simplified version of Dubbo. It may have borrowed ideas from Dubbo, but its design is simpler and its functionality more refined.** -不过,我不推荐你在实际项目中使用 Motan。如果你要是公司实际使用的话,还是推荐 Dubbo ,其社区活跃度以及生态都要好很多。 +However, I do not recommend using Motan in actual projects. If your company intends to use it, I still recommend Dubbo, as its community activity and ecosystem are much better. -- 从 Motan 看 RPC 框架设计:[http://kriszhang.com/motan-rpc-impl/](http://kriszhang.com/motan-rpc-impl/ "http://kriszhang.com/motan-rpc-impl/") -- Motan 中文文档:[https://github.com/weibocom/motan/wiki/zh_overview](https://github.com/weibocom/motan/wiki/zh_overview "https://github.com/weibocom/motan/wiki/zh_overview") +- Looking at RPC framework design from Motan: [http://kriszhang.com/motan-rpc-impl/](http://kriszhang.com/motan-rpc-impl/ "http://kriszhang.com/motan-rpc-impl/") +- Motan Chinese Documentation: [https://github.com/weibocom/motan/wiki/zh_overview](https://github.com/weibocom/motan/wiki/zh_overview "https://github.com/weibocom/motan/wiki/zh_overview") ### gRPC ![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/2843b10d-0c2f-4b7e-9c3e-ea4466792a8b.png) -gRPC 是 Google 开源的一个高性能、通用的开源 RPC 框架。其由主要面向移动应用开发并基于 HTTP/2 协议标准而设计(支持双向流、消息头压缩等功能,更加节省带宽),基于 ProtoBuf 序列化协议开发,并且支持众多开发语言。 +gRPC is a high-performance, open-source RPC framework developed by Google. It is primarily designed for mobile application development and is based on the HTTP/2 protocol standard (supporting bidirectional streaming, message header compression, etc., which saves bandwidth), and is developed based on the ProtoBuf serialization protocol, supporting many programming languages. -**何谓 ProtoBuf?** [ProtoBuf( Protocol Buffer)](https://github.com/protocolbuffers/protobuf) 是一种更加灵活、高效的数据格式,可用于通讯协议、数据存储等领域,基本支持所有主流编程语言且与平台无关。不过,通过 ProtoBuf 定义接口和数据类型还挺繁琐的,这是一个小问题。 +**What is ProtoBuf?** [ProtoBuf (Protocol Buffer)](https://github.com/protocolbuffers/protobuf) is a more flexible and efficient data format that can be used in communication protocols, data storage, and other fields. It supports almost all mainstream programming languages and is platform-independent. However, defining interfaces and data types using ProtoBuf can be a bit cumbersome; that's a minor drawback. ![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/image-20220716104304033.png) -不得不说,gRPC 的通信层的设计还是非常优秀的,[Dubbo-go 3.0](https://dubbogo.github.io/) 的通信层改进主要借鉴了 gRPC。 +It must be said that the design of gRPC's communication layer is exceptionally well done, and the communication layer improvement of [Dubbo-go 3.0](https://dubbogo.github.io/) has heavily borrowed from gRPC. -不过,gRPC 的设计导致其几乎没有服务治理能力。如果你想要解决这个问题的话,就需要依赖其他组件比如腾讯的 PolarisMesh(北极星)了。 +However, gRPC's design leads to almost no service governance capabilities. If you want to solve this problem, you will need to rely on other components, such as Tencent's PolarisMesh (North Star). -- GitHub:[https://github.com/grpc/grpc](https://github.com/grpc/grpc "https://github.com/grpc/grpc") -- 官网:[https://grpc.io/](https://grpc.io/ "https://grpc.io/") +- GitHub: [https://github.com/grpc/grpc](https://github.com/grpc/grpc "https://github.com/grpc/grpc") +- Official Website: [https://grpc.io/](https://grpc.io/ "https://grpc.io/") ### Thrift -Apache Thrift 是 Facebook 开源的跨语言的 RPC 通信框架,目前已经捐献给 Apache 基金会管理,由于其跨语言特性和出色的性能,在很多互联网公司得到应用,有能力的公司甚至会基于 thrift 研发一套分布式服务框架,增加诸如服务注册、服务发现等功能。 +Apache Thrift is a cross-language RPC communication framework open-sourced by Facebook and is now managed by the Apache Foundation. Due to its cross-language features and excellent performance, it is applied in many internet companies. Capable companies may even develop a distributed service framework based on Thrift, adding features such as service registration and service discovery. -`Thrift`支持多种不同的**编程语言**,包括`C++`、`Java`、`Python`、`PHP`、`Ruby`等(相比于 gRPC 支持的语言更多 )。 +`Thrift` supports various **programming languages**, including `C++`, `Java`, `Python`, `PHP`, `Ruby`, etc., and it supports more languages compared to gRPC. -- 官网:[https://thrift.apache.org/](https://thrift.apache.org/ "https://thrift.apache.org/") -- Thrift 简单介绍:[https://www.jianshu.com/p/8f25d057a5a9](https://www.jianshu.com/p/8f25d057a5a9 "https://www.jianshu.com/p/8f25d057a5a9") +- Official Website: [https://thrift.apache.org/](https://thrift.apache.org/ "https://thrift.apache.org/") +- Simple Introduction to Thrift: [https://www.jianshu.com/p/8f25d057a5a9](https://www.jianshu.com/p/8f25d057a5a9 "https://www.jianshu.com/p/8f25d057a5a9") -### 总结 +### Conclusion -gRPC 和 Thrift 虽然支持跨语言的 RPC 调用,但是它们只提供了最基本的 RPC 框架功能,缺乏一系列配套的服务化组件和服务治理功能的支撑。 +Although gRPC and Thrift support cross-language RPC calls, they only provide the most basic RPC framework functionalities, lacking a series of supporting services and governance features. -Dubbo 不论是从功能完善程度、生态系统还是社区活跃度来说都是最优秀的。而且,Dubbo 在国内有很多成功的案例比如当当网、滴滴等等,是一款经得起生产考验的成熟稳定的 RPC 框架。最重要的是你还能找到非常多的 Dubbo 参考资料,学习成本相对也较低。 +Dubbo is the most outstanding framework in terms of functionality completeness, ecosystem, and community activity. Moreover, there are many successful domestic cases of Dubbo, such as Dangdang, Didi, and others; it is a mature and stable RPC framework that has withstood practical tests. Most importantly, you can find a wealth of reference materials on Dubbo, making the learning curve relatively low. -下图展示了 Dubbo 的生态系统。 +The following diagram shows Dubbo's ecosystem. ![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/eee98ff2-8e06-4628-a42b-d30ffcd2831e.png) -Dubbo 也是 Spring Cloud Alibaba 里面的一个组件。 +Dubbo is also a component of Spring Cloud Alibaba. ![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/0d195dae-72bc-4956-8451-3eaf6dd11cbd.png) -但是,Dubbo 和 Motan 主要是给 Java 语言使用。虽然,Dubbo 和 Motan 目前也能兼容部分语言,但是不太推荐。如果需要跨多种语言调用的话,可以考虑使用 gRPC。 +However, both Dubbo and Motan are mainly used for the Java language. Although they can currently also support some other languages, it's not highly recommended. If you need to call across multiple languages, consider using gRPC. -综上,如果是 Java 后端技术栈,并且你在纠结选择哪一种 RPC 框架的话,我推荐你考虑一下 Dubbo。 +In summary, if you are in a Java backend tech stack and are conflicted about which RPC framework to choose, I recommend considering Dubbo. -## 如何设计并实现一个 RPC 框架? +## How to design and implement an RPC framework? -**《手写 RPC 框架》** 是我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)的一个内部小册,我写了 12 篇文章来讲解如何从零开始基于 Netty+Kyro+Zookeeper 实现一个简易的 RPC 框架。 +**"Writing an RPC Framework"** is an internal booklet of my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html), where I wrote 12 articles explaining how to start from scratch and implement a simple RPC framework based on Netty+Kyro+Zookeeper. -麻雀虽小五脏俱全,项目代码注释详细,结构清晰,并且集成了 Check Style 规范代码结构,非常适合阅读和学习。 +Although the project is compact, it covers all essential aspects, with detailed comments in the code, a clear structure, and integrated Check Style norms for code structure, making it very suitable for reading and learning. -**内容概览**: +**Content Overview**: ![](https://oss.javaguide.cn/github/javaguide/image-20220308100605485.png) -## 既然有了 HTTP 协议,为什么还要有 RPC ? +## Since there is an HTTP protocol, why is there still a need for RPC? -关于这个问题的详细答案,请看这篇文章:[有了 HTTP 协议,为什么还要有 RPC ?](http&rpc.md) 。 - - +For a detailed answer to this question, please refer to this article: [Since there is an HTTP protocol, why is there still a need for RPC?](http&rpc.md). diff --git a/docs/distributed-system/spring-cloud-gateway-questions.md b/docs/distributed-system/spring-cloud-gateway-questions.md index 1e6e86845af..e9f0103c590 100644 --- a/docs/distributed-system/spring-cloud-gateway-questions.md +++ b/docs/distributed-system/spring-cloud-gateway-questions.md @@ -1,138 +1,138 @@ --- -title: Spring Cloud Gateway常见问题总结 -category: 分布式 +title: Summary of Common Issues with Spring Cloud Gateway +category: Distributed --- -> 本文重构完善自[6000 字 | 16 图 | 深入理解 Spring Cloud Gateway 的原理 - 悟空聊架构](https://mp.weixin.qq.com/s/XjFYsP1IUqNzWqXZdJn-Aw)这篇文章。 +> This article is a refined version of [6000 words | 16 images | In-depth Understanding of the Principles of Spring Cloud Gateway - Wukong Talks Architecture](https://mp.weixin.qq.com/s/XjFYsP1IUqNzWqXZdJn-Aw). -## 什么是 Spring Cloud Gateway? +## What is Spring Cloud Gateway? -Spring Cloud Gateway 属于 Spring Cloud 生态系统中的网关,其诞生的目标是为了替代老牌网关 **Zuul**。准确点来说,应该是 Zuul 1.x。Spring Cloud Gateway 起步要比 Zuul 2.x 更早。 +Spring Cloud Gateway is a gateway in the Spring Cloud ecosystem, designed to replace the legacy gateway **Zuul**. More precisely, it aims to replace Zuul 1.x, as Spring Cloud Gateway was developed earlier than Zuul 2.x. -为了提升网关的性能,Spring Cloud Gateway 基于 Spring WebFlux 。Spring WebFlux 使用 Reactor 库来实现响应式编程模型,底层基于 Netty 实现同步非阻塞的 I/O。 +To enhance gateway performance, Spring Cloud Gateway is built on Spring WebFlux. Spring WebFlux utilizes the Reactor library to implement a reactive programming model, and is based on Netty for synchronous non-blocking I/O at the core. ![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/springcloud-gateway-%20demo.png) -Spring Cloud Gateway 不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,限流。 +Spring Cloud Gateway not only provides a unified routing method but also offers basic gateway functions based on a chain of filters, such as security, monitoring/metrics, and rate limiting. -Spring Cloud Gateway 和 Zuul 2.x 的差别不大,也是通过过滤器来处理请求。不过,目前更加推荐使用 Spring Cloud Gateway 而非 Zuul,Spring Cloud 生态对其支持更加友好。 +The differences between Spring Cloud Gateway and Zuul 2.x are minimal, as both handle requests through filters. However, Spring Cloud Gateway is currently more recommended than Zuul, as the Spring Cloud ecosystem offers better support for it. -- GitHub 地址: -- 官网: +- GitHub address: +- Official website: -## Spring Cloud Gateway 的工作流程? +## What is the workflow of Spring Cloud Gateway? -Spring Cloud Gateway 的工作流程如下图所示: +The workflow of Spring Cloud Gateway is illustrated in the following diagram: -![Spring Cloud Gateway 的工作流程](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-workflow.png) +![Workflow of Spring Cloud Gateway](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-workflow.png) -这是 Spring 官方博客中的一张图,原文地址:。 +This is an image from the official Spring blog, original text address: . -具体的流程分析: +Specific process analysis: -1. **路由判断**:客户端的请求到达网关后,先经过 Gateway Handler Mapping 处理,这里面会做断言(Predicate)判断,看下符合哪个路由规则,这个路由映射后端的某个服务。 -2. **请求过滤**:然后请求到达 Gateway Web Handler,这里面有很多过滤器,组成过滤器链(Filter Chain),这些过滤器可以对请求进行拦截和修改,比如添加请求头、参数校验等等,有点像净化污水。然后将请求转发到实际的后端服务。这些过滤器逻辑上可以称作 Pre-Filters,Pre 可以理解为“在...之前”。 -3. **服务处理**:后端服务会对请求进行处理。 -4. **响应过滤**:后端处理完结果后,返回给 Gateway 的过滤器再次做处理,逻辑上可以称作 Post-Filters,Post 可以理解为“在...之后”。 -5. **响应返回**:响应经过过滤处理后,返回给客户端。 +1. **Route Determination**: Once the client's request reaches the gateway, it first passes through the Gateway Handler Mapping, where predicate assertions are evaluated to see which route rule applies. This route maps to a certain backend service. +1. **Request Filtering**: The request then reaches the Gateway Web Handler, which has many filters that form a filter chain. These filters can intercept and modify requests, such as adding request headers, validating parameters, etc., akin to purifying wastewater. The request is then forwarded to the actual backend service. These filters are logically termed Pre-Filters, where Pre can be understood as "before...". +1. **Service Processing**: The backend service processes the request. +1. **Response Filtering**: After the backend has finished processing and returned the result, it is processed again by the Gateway's filters, logically termed Post-Filters, where Post can be understood as "after...". +1. **Response Return**: After filtering, the response is returned to the client. -总结:客户端的请求先通过匹配规则找到合适的路由,就能映射到具体的服务。然后请求经过过滤器处理后转发给具体的服务,服务处理后,再次经过过滤器处理,最后返回给客户端。 +In summary: the client's request first finds the appropriate route by matching rules, mapping it to a specific service. After passing through filter processing, the request is forwarded to the specific service, which is processed, filtered again, and ultimately returned to the client. -## Spring Cloud Gateway 的断言是什么? +## What are predicates in Spring Cloud Gateway? -断言(Predicate)这个词听起来极其深奥,它是一种编程术语,我们生活中根本就不会用它。说白了它就是对一个表达式进行 if 判断,结果为真或假,如果为真则做这件事,否则做那件事。 +The term predicate might sound complex; it is a programming term that we do not commonly use in our daily life. Simply put, it refers to performing an if-judgment on an expression, yielding a true or false result. If true, an action is taken; if false, another action is executed. -在 Gateway 中,如果客户端发送的请求满足了断言的条件,则映射到指定的路由器,就能转发到指定的服务上进行处理。 +In the Gateway, if the client's request meets the predicate's conditions, it maps to the specified router, enabling forwarding to the designated service for processing. -断言配置的示例如下,配置了两个路由规则,有一个 predicates 断言配置,当请求 url 中包含 `api/thirdparty`,就匹配到了第一个路由 `route_thirdparty`。 +An example of predicate configuration is shown below, which configures two routing rules, with a predicates assertion that matches when the request URL contains `api/thirdparty`, corresponding to the first route `route_thirdparty`. -![断言配置示例](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-predicate-example.png) +![Predicate Configuration Example](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-predicate-example.png) -常见的路由断言规则如下图所示: +Common routing predicate rules are shown in the following diagram: -![Spring Cloud GateWay 路由断言规则](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-predicate-rules.png) +![Routing Predicate Rules of Spring Cloud Gateway](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-predicate-rules.png) -## Spring Cloud Gateway 的路由和断言是什么关系? +## What is the relationship between routes and predicates in Spring Cloud Gateway? -Route 路由和 Predicate 断言的对应关系如下:: +The correspondence between Route and Predicate is as follows: -![路由和断言的对应关系](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-predicate-route.png) +![Correspondence between Routes and Predicates](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-predicate-route.png) -- **一对多**:一个路由规则可以包含多个断言。如上图中路由 Route1 配置了三个断言 Predicate。 -- **同时满足**:如果一个路由规则中有多个断言,则需要同时满足才能匹配。如上图中路由 Route2 配置了两个断言,客户端发送的请求必须同时满足这两个断言,才能匹配路由 Route2。 -- **第一个匹配成功**:如果一个请求可以匹配多个路由,则映射第一个匹配成功的路由。如上图所示,客户端发送的请求满足 Route3 和 Route4 的断言,但是 Route3 的配置在配置文件中靠前,所以只会匹配 Route3。 +- **One-to-Many**: One routing rule can contain multiple predicates. As shown in the image, Route1 is configured with three predicates. +- **Simultaneous Satisfaction**: If a routing rule has multiple predicates, all must be satisfied for a match. In the image, for Route2, the client's request must satisfy both predicates to match Route2. +- **First Match Success**: If a request can match multiple routes, the first successfully matching route is chosen. In the image, the client's request satisfies both Route3 and Route4's predicates, but since Route3's configuration appears earlier in the configuration file, it will only match Route3. -## Spring Cloud Gateway 如何实现动态路由? +## How does Spring Cloud Gateway achieve dynamic routing? -在使用 Spring Cloud Gateway 的时候,官方文档提供的方案总是基于配置文件或代码配置的方式。 +When using Spring Cloud Gateway, official documentation often provides solutions based on configuration files or code configurations. -Spring Cloud Gateway 作为微服务的入口,需要尽量避免重启,而现在配置更改需要重启服务不能满足实际生产过程中的动态刷新、实时变更的业务需求,所以我们需要在 Spring Cloud Gateway 运行时动态配置网关。 +As the entry point for microservices, Spring Cloud Gateway should aim to avoid restarts. However, current configuration changes require server restarts, which cannot meet the dynamic refresh and real-time change demands in actual production. Thus, we need to dynamically configure the gateway during its runtime. -实现动态路由的方式有很多种,其中一种推荐的方式是基于 Nacos 注册中心来做。 Spring Cloud Gateway 可以从注册中心获取服务的元数据(例如服务名称、路径等),然后根据这些信息自动生成路由规则。这样,当你添加、移除或更新服务实例时,网关会自动感知并相应地调整路由规则,无需手动维护路由配置。 +There are many ways to achieve dynamic routing, and one recommended method is based on the Nacos registry. Spring Cloud Gateway can retrieve service metadata (such as service name, path, etc.) from the registry and automatically generate routing rules based on this information. Thus, when you add, remove, or update service instances, the gateway will automatically perceive these changes and adjust the routing rules accordingly, eliminating the need for manual maintenance of routing configurations. -其实这些复杂的步骤并不需要我们手动实现,通过 Nacos Server 和 Spring Cloud Alibaba Nacos Config 即可实现配置的动态变更,官方文档地址: 。 +In fact, these complex steps do not require manual implementation since dynamic configuration changes can be achieved through Nacos Server and Spring Cloud Alibaba Nacos Config. For the official documentation, visit: . -## Spring Cloud Gateway 的过滤器有哪些? +## What are the filters in Spring Cloud Gateway? -过滤器 Filter 按照请求和响应可以分为两种: +Filters can be categorized into two types based on requests and responses: -- **Pre 类型**:在请求被转发到微服务之前,对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。 -- **Post 类型**:微服务处理完请求后,返回响应给网关,网关可以再次进行处理,例如修改响应内容或响应头、日志输出、流量监控等。 +- **Pre Type**: Intercepts and modifies requests before they are forwarded to microservices, including parameter validation, permission checks, traffic monitoring, logging, and protocol conversion. +- **Post Type**: After the microservice processes the request and returns a response to the gateway, the gateway can perform further processing, such as modifying the response content or headers, logging, and traffic monitoring. -另外一种分类是按照过滤器 Filter 作用的范围进行划分: +Another classification divides filters based on their scope of action: -- **GatewayFilter**:局部过滤器,应用在单个路由或一组路由上的过滤器。标红色表示比较常用的过滤器。 -- **GlobalFilter**:全局过滤器,应用在所有路由上的过滤器。 +- **GatewayFilter**: Local filters applied to a single route or a group of routes. The red-colored ones are commonly used filters. +- **GlobalFilter**: Global filters applied across all routes. -### 局部过滤器 +### Local Filters -常见的局部过滤器如下图所示: +Common local filters are illustrated in the following diagram: ![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-gatewayfilters.png) -具体怎么用呢?这里有个示例,如果 URL 匹配成功,则去掉 URL 中的 “api”。 +How is it used? Here’s an example: if the URL matches successfully, it removes "api" from the URL. ```yaml -filters: #过滤器 - - RewritePath=/api/(?.*),/$\{segment} # 将跳转路径中包含的 “api” 替换成空 +filters: # Filters + - RewritePath=/api/(?.*),/$\{segment} # Replace "api" in the redirection path with empty ``` -当然我们也可以自定义过滤器,本篇不做展开。 +Of course, we can also define custom filters, but this article does not elaborate on that. -### 全局过滤器 +### Global Filters -常见的全局过滤器如下图所示: +Common global filters are illustrated in the following diagram: ![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-globalfilters.png) -全局过滤器最常见的用法是进行负载均衡。配置如下所示: +The most common use of global filters is for load balancing. The configuration is as follows: ```yaml spring: cloud: gateway: routes: - - id: route_member # 第三方微服务路由规则 - uri: lb://passjava-member # 负载均衡,将请求转发到注册中心注册的 passjava-member 服务 - predicates: # 断言 - - Path=/api/member/** # 如果前端请求路径包含 api/member,则应用这条路由规则 - filters: #过滤器 - - RewritePath=/api/(?.*),/$\{segment} # 将跳转路径中包含的api替换成空 + - id: route_member # Third-party microservice routing rule + uri: lb://passjava-member # Load balancing, forward requests to the registered passjava-member service in the registry + predicates: # Predicates + - Path=/api/member/** # If the frontend request path includes api/member, this routing rule applies + filters: # Filters + - RewritePath=/api/(?.*),/$\{segment} # Replace "api" in the redirection path with empty ``` -这里有个关键字 `lb`,用到了全局过滤器 `LoadBalancerClientFilter`,当匹配到这个路由后,会将请求转发到 passjava-member 服务,且支持负载均衡转发,也就是先将 passjava-member 解析成实际的微服务的 host 和 port,然后再转发给实际的微服务。 +Here we have a keyword `lb`, which utilizes the global filter `LoadBalancerClientFilter`. When this route matches, the request will be forwarded to the passjava-member service, supporting load-balanced forwarding, meaning it first resolves passjava-member to the actual microservice's host and port before forwarding it to the actual microservice. -## Spring Cloud Gateway 支持限流吗? +## Does Spring Cloud Gateway support rate limiting? -Spring Cloud Gateway 自带了限流过滤器,对应的接口是 `RateLimiter`,`RateLimiter` 接口只有一个实现类 `RedisRateLimiter` (基于 Redis + Lua 实现的限流),提供的限流功能比较简易且不易使用。 +Spring Cloud Gateway comes with a built-in rate limiting filter, corresponding to the interface `RateLimiter`. The `RateLimiter` interface has only one implementation class `RedisRateLimiter` (a rate limiter based on Redis + Lua), providing relatively simple and not very user-friendly rate limiting functionality. -从 Sentinel 1.6.0 版本开始,Sentinel 引入了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:route 维度和自定义 API 维度。也就是说,Spring Cloud Gateway 可以结合 Sentinel 实现更强大的网关流量控制。 +Starting from Sentinel version 1.6.0, Sentinel introduced an adaptation module for Spring Cloud Gateway that provides two dimensions for rate limiting: route dimension and custom API dimension. This means Spring Cloud Gateway can combine with Sentinel to achieve more powerful gateway traffic control. -## Spring Cloud Gateway 如何自定义全局异常处理? +## How do we customize global exception handling in Spring Cloud Gateway? -在 SpringBoot 项目中,我们捕获全局异常只需要在项目中配置 `@RestControllerAdvice`和 `@ExceptionHandler`就可以了。不过,这种方式在 Spring Cloud Gateway 下不适用。 +In SpringBoot projects, we can capture global exceptions by simply configuring `@RestControllerAdvice` and `@ExceptionHandler`. However, this method is not applicable under Spring Cloud Gateway. -Spring Cloud Gateway 提供了多种全局处理的方式,比较常用的一种是实现`ErrorWebExceptionHandler`并重写其中的`handle`方法。 +Spring Cloud Gateway provides various global handling methods, with a commonly used one being to implement `ErrorWebExceptionHandler` and override its `handle` method. ```java @Order(-1) @@ -148,10 +148,10 @@ public class GlobalErrorWebExceptionHandler implements ErrorWebExceptionHandler } ``` -## 参考 +## References -- Spring Cloud Gateway 官方文档: -- Creating a custom Spring Cloud Gateway Filter: -- 全局异常处理: +- Official Spring Cloud Gateway documentation: +- Creating a custom Spring Cloud Gateway Filter: +- Global exception handling: diff --git a/docs/high-availability/fallback-and-circuit-breaker.md b/docs/high-availability/fallback-and-circuit-breaker.md index 59725fa0521..34607fc1964 100644 --- a/docs/high-availability/fallback-and-circuit-breaker.md +++ b/docs/high-availability/fallback-and-circuit-breaker.md @@ -1,10 +1,10 @@ --- -title: 降级&熔断详解(付费) -category: 高可用 +title: Detailed Explanation of Downgrading & Circuit Breaking (Paid) +category: High Availability icon: circuit --- -**降级&熔断** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。 +The interview questions related to **Downgrading & Circuit Breaking** are exclusive content for my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) (click the link to view detailed introduction and joining methods) and have been compiled in [“Java Interview Guide”](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html). ![](https://oss.javaguide.cn/xingqiu/mianshizhibei-gaobingfa.png) diff --git a/docs/high-availability/high-availability-system-design.md b/docs/high-availability/high-availability-system-design.md index f461f93e99b..44f49e5dc56 100644 --- a/docs/high-availability/high-availability-system-design.md +++ b/docs/high-availability/high-availability-system-design.md @@ -1,71 +1,56 @@ --- -title: 高可用系统设计指南 -category: 高可用 +title: High Availability System Design Guide +category: High Availability icon: design --- -## 什么是高可用?可用性的判断标准是啥? +## What is High Availability? What are the criteria for determining availability? -高可用描述的是一个系统在大部分时间都是可用的,可以为我们提供服务的。高可用代表系统即使在发生硬件故障或者系统升级的时候,服务仍然是可用的。 +High availability describes a system that is available most of the time and can provide services to us. High availability means that the system remains available even during hardware failures or system upgrades. -一般情况下,我们使用多少个 9 来评判一个系统的可用性,比如 99.9999% 就是代表该系统在所有的运行时间中只有 0.0001% 的时间是不可用的,这样的系统就是非常非常高可用的了!当然,也会有系统如果可用性不太好的话,可能连 9 都上不了。 +Generally, we use the number of nines to evaluate a system's availability. For example, 99.9999% means that the system is unavailable only 0.0001% of the time during all operational hours, making it extremely high availability! Of course, there are systems with poor availability that may not even reach a single nine. -除此之外,系统的可用性还可以用某功能的失败次数与总的请求次数之比来衡量,比如对网站请求 1000 次,其中有 10 次请求失败,那么可用性就是 99%。 +In addition, the availability of a system can also be measured by the ratio of the number of failures of a certain function to the total number of requests. For example, if a website receives 1000 requests and 10 of them fail, the availability would be 99%. -## 哪些情况会导致系统不可用? +## What situations can lead to system unavailability? -1. 黑客攻击; -2. 硬件故障,比如服务器坏掉。 -3. 并发量/用户请求量激增导致整个服务宕掉或者部分服务不可用。 -4. 代码中的坏味道导致内存泄漏或者其他问题导致程序挂掉。 -5. 网站架构某个重要的角色比如 Nginx 或者数据库突然不可用。 -6. 自然灾害或者人为破坏。 -7. …… +1. Hacker attacks; +1. Hardware failures, such as server breakdowns. +1. A surge in concurrent users/request volume causing the entire service to crash or parts of the service to become unavailable. +1. Code smells leading to memory leaks or other issues causing the program to crash. +1. An important component of the website architecture, such as Nginx or the database, suddenly becomes unavailable. +1. Natural disasters or human destruction. +1. …… -## 有哪些提高系统可用性的方法? +## What methods are there to improve system availability? -### 注重代码质量,测试严格把关 +### Focus on Code Quality, Strict Testing -我觉得这个是最最最重要的,代码质量有问题比如比较常见的内存泄漏、循环依赖都是对系统可用性极大的损害。大家都喜欢谈限流、降级、熔断,但是我觉得从代码质量这个源头把关是首先要做好的一件很重要的事情。如何提高代码质量?比较实际可用的就是 CodeReview,不要在乎每天多花的那 1 个小时左右的时间,作用可大着呢! +I believe this is the most important aspect. Issues with code quality, such as common memory leaks and circular dependencies, can greatly harm system availability. Everyone likes to talk about rate limiting, degradation, and circuit breaking, but I think ensuring code quality from the source is a crucial first step. How can we improve code quality? A practical approach is Code Review; don't worry about spending an extra hour each day, as the benefits can be significant! -另外,安利几个对提高代码质量有实际效果的神器: +Additionally, here are a few tools that can effectively improve code quality: - [Sonarqube](https://www.sonarqube.org/); -- Alibaba 开源的 Java 诊断工具 [Arthas](https://arthas.aliyun.com/doc/); -- [阿里巴巴 Java 代码规范](https://github.com/alibaba/p3c)(Alibaba Java Code Guidelines); -- IDEA 自带的代码分析等工具。 +- Alibaba's open-source Java diagnostic tool [Arthas](https://arthas.aliyun.com/doc/); +- [Alibaba Java Code Guidelines](https://github.com/alibaba/p3c); +- Built-in code analysis tools in IDEA. -### 使用集群,减少单点故障 +### Use Clusters to Reduce Single Points of Failure -先拿常用的 Redis 举个例子!我们如何保证我们的 Redis 缓存高可用呢?答案就是使用集群,避免单点故障。当我们使用一个 Redis 实例作为缓存的时候,这个 Redis 实例挂了之后,整个缓存服务可能就挂了。使用了集群之后,即使一台 Redis 实例挂了,不到一秒就会有另外一台 Redis 实例顶上。 +Let's take the commonly used Redis as an example! How can we ensure our Redis cache is highly available? The answer is to use a cluster to avoid single points of failure. When we use a single Redis instance as a cache, if that instance goes down, the entire cache service may fail. With a cluster, even if one Redis instance fails, another instance will take over in less than a second. -### 限流 +### Rate Limiting -流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。——来自 [alibaba-Sentinel](https://github.com/alibaba/Sentinel "Sentinel") 的 wiki。 +Flow control involves monitoring application traffic metrics such as QPS or concurrent thread counts. When a specified threshold is reached, traffic is controlled to avoid being overwhelmed by sudden traffic spikes, thereby ensuring high application availability. — From the [alibaba-Sentinel](https://github.com/alibaba/Sentinel "Sentinel") wiki. -### 超时和重试机制设置 +### Timeout and Retry Mechanism Settings -一旦用户请求超过某个时间的得不到响应,就抛出异常。这个是非常重要的,很多线上系统故障都是因为没有进行超时设置或者超时设置的方式不对导致的。我们在读取第三方服务的时候,尤其适合设置超时和重试机制。一般我们使用一些 RPC 框架的时候,这些框架都自带的超时重试的配置。如果不进行超时设置可能会导致请求响应速度慢,甚至导致请求堆积进而让系统无法再处理请求。重试的次数一般设为 3 次,再多次的重试没有好处,反而会加重服务器压力(部分场景使用失败重试机制会不太适合)。 +If a user request does not receive a response within a certain time, an exception should be thrown. This is very important; many online system failures occur due to the lack of timeout settings or incorrect timeout configurations. When reading from third-party services, it is especially suitable to set timeout and retry mechanisms. Generally, when using some RPC frameworks, these frameworks come with built-in timeout and retry configurations. Not setting a timeout can lead to slow response times and even request accumulation, making the system unable to process requests. The retry count is usually set to 3 times; retrying more than that is not beneficial and may increase server pressure (in some scenarios, using a failure retry mechanism may not be suitable). -### 熔断机制 +### Circuit Breaker Mechanism -超时和重试机制设置之外,熔断机制也是很重要的。 熔断机制说的是系统自动收集所依赖服务的资源使用情况和性能指标,当所依赖的服务恶化或者调用失败次数达到某个阈值的时候就迅速失败,让当前系统立即切换依赖其他备用服务。 比较常用的流量控制和熔断降级框架是 Netflix 的 Hystrix 和 alibaba 的 Sentinel。 +In addition to timeout and retry mechanism settings, the circuit breaker mechanism is also very important. The circuit breaker mechanism automatically collects resource usage and performance metrics of the dependent services. When the performance of the dependent service deteriorates or the number of failed calls reaches a certain threshold, it quickly fails, allowing the current system to switch to other backup services immediately. Commonly used flow control and circuit breaker frameworks include Netflix's Hystrix and Alibaba's Sentinel. -### 异步调用 +### Asynchronous Calls -异步调用的话我们不需要关心最后的结果,这样我们就可以用户请求完成之后就立即返回结果,具体处理我们可以后续再做,秒杀场景用这个还是蛮多的。但是,使用异步之后我们可能需要 **适当修改业务流程进行配合**,比如**用户在提交订单之后,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**。除了可以在程序中实现异步之外,我们常常还使用消息队列,消息队列可以通过异步处理提高系统性能(削峰、减少响应所需时间)并且可以降低系统耦合性。 - -### 使用缓存 - -如果我们的系统属于并发量比较高的话,如果我们单纯使用数据库的话,当大量请求直接落到数据库可能数据库就会直接挂掉。使用缓存缓存热点数据,因为缓存存储在内存中,所以速度相当地快! - -### 其他 - -- **核心应用和服务优先使用更好的硬件** -- **监控系统资源使用情况增加报警设置。** -- **注意备份,必要时候回滚。** -- **灰度发布:** 将服务器集群分成若干部分,每天只发布一部分机器,观察运行稳定没有故障,第二天继续发布一部分机器,持续几天才把整个集群全部发布完毕,期间如果发现问题,只需要回滚已发布的一部分服务器即可 -- **定期检查/更换硬件:** 如果不是购买的云服务的话,定期还是需要对硬件进行一波检查的,对于一些需要更换或者升级的硬件,要及时更换或者升级。 -- …… - - +With asynchronous calls, we do not need to worry about the final result, allowing us to return results immediately after the user request is completed. The specific processing can be done later, which is quite common in flash sale scenarios. However, after using asynchronous calls, we may need to **appropriately modify the business process to accommodate** this, such as **not immediately returning a success message to the user after submitting an order. Instead, we should notify the user of the order success via email or SMS only after the order has been processed by the message queue's order consumer process, or even after it has been shipped**. In addition to implementing diff --git a/docs/high-availability/idempotency.md b/docs/high-availability/idempotency.md index 41384457ccb..8d122a9b38f 100644 --- a/docs/high-availability/idempotency.md +++ b/docs/high-availability/idempotency.md @@ -1,10 +1,10 @@ --- -title: 接口幂等方案总结(付费) -category: 高可用 +title: Summary of Idempotency Solutions for APIs (Paid) +category: High Availability icon: security-fill --- -**接口幂等** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。 +**Idempotency of APIs** related interview questions are exclusive content for my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) (click the link for detailed introduction and joining methods), and have been compiled in [“Java Interview Guide”](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html). ![](https://oss.javaguide.cn/xingqiu/mianshizhibei-gaobingfa.png) diff --git a/docs/high-availability/limit-request.md b/docs/high-availability/limit-request.md index 22db662eedf..c45eafd8b55 100644 --- a/docs/high-availability/limit-request.md +++ b/docs/high-availability/limit-request.md @@ -1,298 +1,63 @@ --- -title: 服务限流详解 -category: 高可用 +title: Detailed Explanation of Rate Limiting +category: High Availability icon: limit_rate --- -针对软件系统来说,限流就是对请求的速率进行限制,避免瞬时的大量请求击垮软件系统。毕竟,软件系统的处理能力是有限的。如果说超过了其处理能力的范围,软件系统可能直接就挂掉了。 +For software systems, rate limiting refers to the restriction of request rates to prevent a sudden influx of requests from overwhelming the system. After all, the processing capacity of a software system is limited. If the number of requests exceeds its processing capacity, the software system may crash. -限流可能会导致用户的请求无法被正确处理或者无法立即被处理,不过,这往往也是权衡了软件系统的稳定性之后得到的最优解。 +Rate limiting may result in user requests not being processed correctly or not being processed immediately; however, this is often the optimal solution after weighing the stability of the software system. -现实生活中,处处都有限流的实际应用,就比如排队买票是为了避免大量用户涌入购票而导致售票员无法处理。 +In real life, there are many practical applications of rate limiting. For example, queuing to buy tickets is intended to prevent a large number of users from flooding in and overwhelming the ticket seller. -## 常见限流算法有哪些? +## What are the Common Rate Limiting Algorithms? -简单介绍 4 种非常好理解并且容易实现的限流算法! +Here’s a brief introduction to four very understandable and easy-to-implement rate limiting algorithms! -> 图片来源于 InfoQ 的一篇文章[《分布式服务限流实战,已经为你排好坑了》](https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673)。 +> Image source: An article from InfoQ [“Practical Distributed Service Rate Limiting, Already Prepared for You”](https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673). -### 固定窗口计数器算法 +### Fixed Window Counter Algorithm -固定窗口其实就是时间窗口,其原理是将时间划分为固定大小的窗口,在每个窗口内限制请求的数量或速率,即固定窗口计数器算法规定了系统单位时间处理的请求数量。 +A fixed window is essentially a time window. Its principle is to divide time into fixed-size windows, limiting the number of requests or the rate within each window. The fixed window counter algorithm specifies the number of requests the system can handle in a unit of time. -假如我们规定系统中某个接口 1 分钟只能被访问 33 次的话,使用固定窗口计数器算法的实现思路如下: +For example, if we specify that a certain interface in the system can only be accessed 33 times in one minute, the implementation idea of the fixed window counter algorithm is as follows: -- 将时间划分固定大小窗口,这里是 1 分钟一个窗口。 -- 给定一个变量 `counter` 来记录当前接口处理的请求数量,初始值为 0(代表接口当前 1 分钟内还未处理请求)。 -- 1 分钟之内每处理一个请求之后就将 `counter+1` ,当 `counter=33` 之后(也就是说在这 1 分钟内接口已经被访问 33 次的话),后续的请求就会被全部拒绝。 -- 等到 1 分钟结束后,将 `counter` 重置 0,重新开始计数。 +- Divide time into fixed-size windows, here it is one window per minute. +- Set a variable `counter` to record the number of requests processed by the current interface, with an initial value of 0 (indicating that no requests have been processed in the current minute). +- For each request processed within one minute, increment `counter` by 1. Once `counter=33` (meaning the interface has been accessed 33 times in this minute), all subsequent requests will be rejected. +- At the end of one minute, reset `counter` to 0 and start counting again. -![固定窗口计数器算法](https://static001.infoq.cn/resource/image/8d/15/8ded7a2b90e1482093f92fff555b3615.png) +![Fixed Window Counter Algorithm](https://static001.infoq.cn/resource/image/8d/15/8ded7a2b90e1482093f92fff555b3615.png) -优点:实现简单,易于理解。 +Advantages: Simple to implement and easy to understand. -缺点: +Disadvantages: -- 限流不够平滑。例如,我们限制某个接口每分钟只能访问 30 次,假设前 30 秒就有 30 个请求到达的话,那后续 30 秒将无法处理请求,这是不可取的,用户体验极差! -- 无法保证限流速率,因而无法应对突然激增的流量。例如,我们限制某个接口 1 分钟只能访问 1000 次,该接口的 QPS 为 500,前 55s 这个接口 1 个请求没有接收,后 1s 突然接收了 1000 个请求。然后,在当前场景下,这 1000 个请求在 1s 内是没办法被处理的,系统直接就被瞬时的大量请求给击垮了。 +- Rate limiting is not smooth enough. For example, if we limit a certain interface to only 30 accesses per minute, and 30 requests arrive in the first 30 seconds, then no requests can be processed in the following 30 seconds, which is undesirable and results in a poor user experience! +- It cannot guarantee the rate limiting speed, thus unable to cope with sudden surges in traffic. For instance, if we limit a certain interface to only 1000 accesses per minute, and the QPS of that interface is 500, if no requests are received in the first 55 seconds, but suddenly 1000 requests arrive in the last second, these 1000 requests cannot be processed in that second, and the system will be overwhelmed by the sudden influx of requests. -### 滑动窗口计数器算法 +### Sliding Window Counter Algorithm -**滑动窗口计数器算法** 算的上是固定窗口计数器算法的升级版,限流的颗粒度更小。 +**The sliding window counter algorithm** can be considered an upgraded version of the fixed window counter algorithm, with finer granularity for rate limiting. -滑动窗口计数器算法相比于固定窗口计数器算法的优化在于:**它把时间以一定比例分片** 。 +The optimization of the sliding window counter algorithm compared to the fixed window counter algorithm is that **it divides time into segments at a certain ratio**. -例如我们的接口限流每分钟处理 60 个请求,我们可以把 1 分钟分为 60 个窗口。每隔 1 秒移动一次,每个窗口一秒只能处理不大于 `60(请求数)/60(窗口数)` 的请求, 如果当前窗口的请求计数总和超过了限制的数量的话就不再处理其他请求。 +For example, if our interface limits processing to 60 requests per minute, we can divide one minute into 60 windows. Each window can only handle no more than `60(requests)/60(window)` requests per second. If the total request count in the current window exceeds the limit, no further requests will be processed. -很显然, **当滑动窗口的格子划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。** +Clearly, **the more segments the sliding window is divided into, the smoother the sliding window's movement will be, and the more accurate the rate limiting statistics will be.** -![滑动窗口计数器算法](https://static001.infoq.cn/resource/image/ae/15/ae4d3cd14efb8dc7046d691c90264715.png) +![Sliding Window Counter Algorithm](https://static001.infoq.cn/resource/image/ae/15/ae4d3cd14efb8dc7046d691c90264715.png) -优点: +Advantages: -- 相比于固定窗口算法,滑动窗口计数器算法可以应对突然激增的流量。 -- 相比于固定窗口算法,滑动窗口计数器算法的颗粒度更小,可以提供更精确的限流控制。 +- Compared to the fixed window algorithm, the sliding window counter algorithm can handle sudden surges in traffic. +- Compared to the fixed window algorithm, the sliding window counter algorithm has finer granularity, allowing for more precise rate limiting control. -缺点: +Disadvantages: -- 与固定窗口计数器算法类似,滑动窗口计数器算法依然存在限流不够平滑的问题。 -- 相比较于固定窗口计数器算法,滑动窗口计数器算法实现和理解起来更复杂一些。 +- Similar to the fixed window counter algorithm, the sliding window counter algorithm still suffers from the issue of insufficiently smooth rate limiting. +- Compared to the fixed window counter algorithm, the sliding window counter algorithm is more complex to implement and understand. -### 漏桶算法 +### Leaky Bucket Algorithm -我们可以把发请求的动作比作成注水到桶中,我们处理请求的过程可以比喻为漏桶漏水。我们往桶中以任意速率流入水,以一定速率流出水。当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。 - -如果想要实现这个算法的话也很简单,准备一个队列用来保存请求,然后我们定期从队列中拿请求来执行就好了(和消息队列削峰/限流的思想是一样的)。 - -![漏桶算法](https://static001.infoq.cn/resource/image/75/03/75938d1010138ce66e38c6ed0392f103.png) - -优点: - -- 实现简单,易于理解。 -- 可以控制限流速率,避免网络拥塞和系统过载。 - -缺点: - -- 无法应对突然激增的流量,因为只能以固定的速率处理请求,对系统资源利用不够友好。 -- 桶流入水(发请求)的速率如果一直大于桶流出水(处理请求)的速率的话,那么桶会一直是满的,一部分新的请求会被丢弃,导致服务质量下降。 - -实际业务场景中,基本不会使用漏桶算法。 - -### 令牌桶算法 - -令牌桶算法也比较简单。和漏桶算法算法一样,我们的主角还是桶(这限流算法和桶过不去啊)。不过现在桶里装的是令牌了,请求在被处理之前需要拿到一个令牌,请求处理完毕之后将这个令牌丢弃(删除)。我们根据限流大小,按照一定的速率往桶里添加令牌。如果桶装满了,就不能继续往里面继续添加令牌了。 - -![令牌桶算法](https://static001.infoq.cn/resource/image/ec/93/eca0e5eaa35dac938c673fecf2ec9a93.png) - -优点: - -- 可以限制平均速率和应对突然激增的流量。 -- 可以动态调整生成令牌的速率。 - -缺点: - -- 如果令牌产生速率和桶的容量设置不合理,可能会出现问题比如大量的请求被丢弃、系统过载。 -- 相比于其他限流算法,实现和理解起来更复杂一些。 - -## 针对什么来进行限流? - -实际项目中,还需要确定限流对象,也就是针对什么来进行限流。常见的限流对象如下: - -- IP :针对 IP 进行限流,适用面较广,简单粗暴。 -- 业务 ID:挑选唯一的业务 ID 以实现更针对性地限流。例如,基于用户 ID 进行限流。 -- 个性化:根据用户的属性或行为,进行不同的限流策略。例如, VIP 用户不限流,而普通用户限流。根据系统的运行指标(如 QPS、并发调用数、系统负载等),动态调整限流策略。例如,当系统负载较高的时候,控制每秒通过的请求减少。 - -针对 IP 进行限流是目前比较常用的一个方案。不过,实际应用中需要注意用户真实 IP 地址的正确获取。常用的真实 IP 获取方法有 X-Forwarded-For 和 TCP Options 字段承载真实源 IP 信息。虽然 X-Forwarded-For 字段可能会被伪造,但因为其实现简单方便,很多项目还是直接用的这种方法。 - -除了我上面介绍到的限流对象之外,还有一些其他较为复杂的限流对象策略,比如阿里的 Sentinel 还支持 [基于调用关系的限流](https://github.com/alibaba/Sentinel/wiki/流量控制#基于调用关系的流量控制)(包括基于调用方限流、基于调用链入口限流、关联流量限流等)以及更细维度的 [热点参数限流](https://github.com/alibaba/Sentinel/wiki/热点参数限流)(实时的统计热点参数并针对热点参数的资源调用进行流量控制)。 - -另外,一个项目可以根据具体的业务需求选择多种不同的限流对象搭配使用。 - -## 单机限流怎么做? - -单机限流针对的是单体架构应用。 - -单机限流可以直接使用 Google Guava 自带的限流工具类 `RateLimiter` 。 `RateLimiter` 基于令牌桶算法,可以应对突发流量。 - -> Guava 地址: - -除了最基本的令牌桶算法(平滑突发限流)实现之外,Guava 的`RateLimiter`还提供了 **平滑预热限流** 的算法实现。 - -平滑突发限流就是按照指定的速率放令牌到桶里,而平滑预热限流会有一段预热时间,预热时间之内,速率会逐渐提升到配置的速率。 - -我们下面通过两个简单的小例子来详细了解吧! - -我们直接在项目中引入 Guava 相关的依赖即可使用。 - -```xml - - com.google.guava - guava - 31.0.1-jre - -``` - -下面是一个简单的 Guava 平滑突发限流的 Demo。 - -```java -import com.google.common.util.concurrent.RateLimiter; - -/** - * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 - * - * @author Guide哥 - * @date 2021/10/08 19:12 - **/ -public class RateLimiterDemo { - - public static void main(String[] args) { - // 1s 放 5 个令牌到桶里也就是 0.2s 放 1个令牌到桶里 - RateLimiter rateLimiter = RateLimiter.create(5); - for (int i = 0; i < 10; i++) { - double sleepingTime = rateLimiter.acquire(1); - System.out.printf("get 1 tokens: %ss%n", sleepingTime); - } - } -} - -``` - -输出: - -```bash -get 1 tokens: 0.0s -get 1 tokens: 0.188413s -get 1 tokens: 0.197811s -get 1 tokens: 0.198316s -get 1 tokens: 0.19864s -get 1 tokens: 0.199363s -get 1 tokens: 0.193997s -get 1 tokens: 0.199623s -get 1 tokens: 0.199357s -get 1 tokens: 0.195676s -``` - -下面是一个简单的 Guava 平滑预热限流的 Demo。 - -```java -import com.google.common.util.concurrent.RateLimiter; -import java.util.concurrent.TimeUnit; - -/** - * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 - * - * @author Guide哥 - * @date 2021/10/08 19:12 - **/ -public class RateLimiterDemo { - - public static void main(String[] args) { - // 1s 放 5 个令牌到桶里也就是 0.2s 放 1个令牌到桶里 - // 预热时间为3s,也就说刚开始的 3s 内发牌速率会逐渐提升到 0.2s 放 1 个令牌到桶里 - RateLimiter rateLimiter = RateLimiter.create(5, 3, TimeUnit.SECONDS); - for (int i = 0; i < 20; i++) { - double sleepingTime = rateLimiter.acquire(1); - System.out.printf("get 1 tokens: %sds%n", sleepingTime); - } - } -} -``` - -输出: - -```bash -get 1 tokens: 0.0s -get 1 tokens: 0.561919s -get 1 tokens: 0.516931s -get 1 tokens: 0.463798s -get 1 tokens: 0.41286s -get 1 tokens: 0.356172s -get 1 tokens: 0.300489s -get 1 tokens: 0.252545s -get 1 tokens: 0.203996s -get 1 tokens: 0.198359s -``` - -另外,**Bucket4j** 是一个非常不错的基于令牌/漏桶算法的限流库。 - -> Bucket4j 地址: - -相对于,Guava 的限流工具类来说,Bucket4j 提供的限流功能更加全面。不仅支持单机限流和分布式限流,还可以集成监控,搭配 Prometheus 和 Grafana 使用。 - -不过,毕竟 Guava 也只是一个功能全面的工具类库,其提供的开箱即用的限流功能在很多单机场景下还是比较实用的。 - -Spring Cloud Gateway 中自带的单机限流的早期版本就是基于 Bucket4j 实现的。后来,替换成了 **Resilience4j**。 - -Resilience4j 是一个轻量级的容错组件,其灵感来自于 Hystrix。自[Netflix 宣布不再积极开发 Hystrix](https://github.com/Netflix/Hystrix/commit/a7df971cbaddd8c5e976b3cc5f14013fe6ad00e6) 之后,Spring 官方和 Netflix 都更推荐使用 Resilience4j 来做限流熔断。 - -> Resilience4j 地址: - -一般情况下,为了保证系统的高可用,项目的限流和熔断都是要一起做的。 - -Resilience4j 不仅提供限流,还提供了熔断、负载保护、自动重试等保障系统高可用开箱即用的功能。并且,Resilience4j 的生态也更好,很多网关都使用 Resilience4j 来做限流熔断的。 - -因此,在绝大部分场景下 Resilience4j 或许会是更好的选择。如果是一些比较简单的限流场景的话,Guava 或者 Bucket4j 也是不错的选择。 - -## 分布式限流怎么做? - -分布式限流针对的分布式/微服务应用架构应用,在这种架构下,单机限流就不适用了,因为会存在多种服务,并且一种服务也可能会被部署多份。 - -分布式限流常见的方案: - -- **借助中间件限流**:可以借助 Sentinel 或者使用 Redis 来自己实现对应的限流逻辑。 -- **网关层限流**:比较常用的一种方案,直接在网关层把限流给安排上了。不过,通常网关层限流通常也需要借助到中间件/框架。就比如 Spring Cloud Gateway 的分布式限流实现`RedisRateLimiter`就是基于 Redis+Lua 来实现的,再比如 Spring Cloud Gateway 还可以整合 Sentinel 来做限流。 - -如果你要基于 Redis 来手动实现限流逻辑的话,建议配合 Lua 脚本来做。 - -**为什么建议 Redis+Lua 的方式?** 主要有两点原因: - -- **减少了网络开销**:我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。 -- **原子性**:一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。 - -我这里就不放具体的限流脚本代码了,网上也有很多现成的优秀的限流脚本供你参考,就比如 Apache 网关项目 ShenYu 的 RateLimiter 限流插件就基于 Redis + Lua 实现了令牌桶算法/并发令牌桶算法、漏桶算法、滑动窗口算法。 - -> ShenYu 地址: - -![ShenYu 限流脚本](https://oss.javaguide.cn/github/javaguide/high-availability/limit-request/shenyu-ratelimit-lua-scripts.png) - -另外,如果不想自己写 Lua 脚本的话,也可以直接利用 Redisson 中的 `RRateLimiter` 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。 - -Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,比如 Java 中常用的数据结构实现、分布式锁、延迟队列等等。并且,Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。 - -`RRateLimiter` 的使用方式非常简单。我们首先需要获取一个`RRateLimiter`对象,直接通过 Redisson 客户端获取即可。然后,设置限流规则就好。 - -```java -// 创建一个 Redisson 客户端实例 -RedissonClient redissonClient = Redisson.create(); -// 获取一个名为 "javaguide.limiter" 的限流器对象 -RRateLimiter rateLimiter = redissonClient.getRateLimiter("javaguide.limiter"); -// 尝试设置限流器的速率为每小时 100 次 -// RateType 有两种,OVERALL是全局限流,ER_CLIENT是单Client限流(可以认为就是单机限流) -rateLimiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.HOURS); -``` - -接下来我们调用`acquire()`方法或`tryAcquire()`方法即可获取许可。 - -```java -// 获取一个许可,如果超过限流器的速率则会等待 -// acquire()是同步方法,对应的异步方法:acquireAsync() -rateLimiter.acquire(1); -// 尝试在 5 秒内获取一个许可,如果成功则返回 true,否则返回 false -// tryAcquire()是同步方法,对应的异步方法:tryAcquireAsync() -boolean res = rateLimiter.tryAcquire(1, 5, TimeUnit.SECONDS); -``` - -## 总结 - -这篇文章主要介绍了常见的限流算法、限流对象的选择以及单机限流和分布式限流分别应该怎么做。 - -## 参考 - -- 服务治理之轻量级熔断框架 Resilience4j: -- 超详细的 Guava RateLimiter 限流原理解析: -- 实战 Spring Cloud Gateway 之限流篇 👍: -- 详解 Redisson 分布式限流的实现原理: -- 一文详解 Java 限流接口实现 - 阿里云开发者: -- 分布式限流方案的探索与实践 - 腾讯云开发者: - - +We can compare the action of sending requests to pouring water into a bucket, while the process of handling requests can be likened to water leaking from the bucket. We can pour water into the bucket at any rate, and it leaks out at a certain rate. If the water exceeds the bucket's capacity, it will diff --git a/docs/high-availability/performance-test.md b/docs/high-availability/performance-test.md index 47201441d7e..0cb09454396 100644 --- a/docs/high-availability/performance-test.md +++ b/docs/high-availability/performance-test.md @@ -1,178 +1,178 @@ --- -title: 性能测试入门 -category: 高可用 +title: Introduction to Performance Testing +category: High Availability icon: et-performance --- -性能测试一般情况下都是由测试这个职位去做的,那还需要我们开发学这个干嘛呢?了解性能测试的指标、分类以及工具等知识有助于我们更好地去写出性能更好的程序,另外作为开发这个角色,如果你会性能测试的话,相信也会为你的履历加分不少。 +Performance testing is generally conducted by testers, so why should developers learn about it? Understanding performance testing metrics, classifications, and tools helps us write better-performing programs. Furthermore, if you as a developer possess performance testing skills, it will undoubtedly enhance your resume. -这篇文章是我会结合自己的实际经历以及在测试这里取的经所得,除此之外,我还借鉴了一些优秀书籍,希望对你有帮助。 +This article combines my personal experiences and insights from testing, along with references to some excellent books, hoping to be of help to you. -## 不同角色看网站性能 +## Perspectives on Website Performance from Different Roles -### 用户 +### Users -当用户打开一个网站的时候,最关注的是什么?当然是网站响应速度的快慢。比如我们点击了淘宝的主页,淘宝需要多久将首页的内容呈现在我的面前,我点击了提交订单按钮需要多久返回结果等等。 +When a user opens a website, what do they pay the most attention to? Certainly, it is the website's response speed. For instance, when we click on the homepage of Taobao, how long does it take for Taobao to display the content? How long does it take to return a result after clicking the "submit order" button, etc. -所以,用户在体验我们系统的时候往往根据你的响应速度的快慢来评判你的网站的性能。 +Therefore, when users experience our system, they often judge the performance of your website based on its response speed. -### 开发人员 +### Developers -用户与开发人员都关注速度,这个速度实际上就是我们的系统**处理用户请求的速度**。 +Both users and developers focus on speed, which truly refers to our system's **processing speed of user requests**. -开发人员一般情况下很难直观的去评判自己网站的性能,我们往往会根据网站当前的架构以及基础设施情况给一个大概的值,比如: +Developers generally find it challenging to intuitively assess the performance of their website. We often provide an approximate value based on the current architecture and infrastructure of the website, for instance: -1. 项目架构是分布式的吗? -2. 用到了缓存和消息队列没有? -3. 高并发的业务有没有特殊处理? -4. 数据库设计是否合理? -5. 系统用到的算法是否还需要优化? -6. 系统是否存在内存泄露的问题? -7. 项目使用的 Redis 缓存多大?服务器性能如何?用的是机械硬盘还是固态硬盘? -8. …… +1. Is the project architecture distributed? +1. Are caching and messaging queues utilized? +1. Are there any special handling for high-concurrency business? +1. Is the database design reasonable? +1. Do the algorithms used in the system need optimization? +1. Does the system have memory leak issues? +1. How large is the Redis cache used in the project? What is the server performance like? Is it using a mechanical hard disk or a solid-state drive? +1. …… -### 测试人员 +### Testers -测试人员一般会根据性能测试工具来测试,然后一般会做出一个表格。这个表格可能会涵盖下面这些重要的内容: +Testers typically conduct tests according to performance testing tools and usually create a table. This table might cover important contents like: -1. 响应时间; -2. 请求成功率; -3. 吞吐量; -4. …… +1. Response time; +1. Request success rate; +1. Throughput; +1. …… -### 运维人员 +### Operations Personnel -运维人员会倾向于根据基础设施和资源的利用率来判断网站的性能,比如我们的服务器资源使用是否合理、数据库资源是否存在滥用的情况、当然,这是传统的运维人员,现在 Devops 火起来后,单纯干运维的很少了。我们这里暂且还保留有这个角色。 +Operations personnel tend to evaluate the website's performance based on infrastructure and resource utilization rates. For instance, they might analyze whether resource usage on our servers is reasonable or whether there is abuse of database resources. Of course, this is the traditional role of operations personnel. With the rise of DevOps, pure operations roles are less common now, although we still acknowledge this role for the time being. -## 性能测试需要注意的点 +## Key Points to Note in Performance Testing -几乎没有文章在讲性能测试的时候提到这个问题,大家都会讲如何去性能测试,有哪些性能测试指标这些东西。 +Few articles address this issue when discussing performance testing. Most articles focus on how to conduct performance tests and what performance testing metrics exist. -### 了解系统的业务场景 +### Understanding the Business Context of the System -**性能测试之前更需要你了解当前的系统的业务场景。** 对系统业务了解的不够深刻,我们很容易犯测试方向偏执的错误,从而导致我们忽略了对系统某些更需要性能测试的地方进行测试。比如我们的系统可以为用户提供发送邮件的功能,用户配置成功邮箱后只需输入相应的邮箱之后就能发送,系统每天大概能处理上万次发邮件的请求。很多人看到这个可能就直接开始使用相关工具测试邮箱发送接口,但是,发送邮件这个场景可能不是当前系统的性能瓶颈,这么多人用我们的系统发邮件, 还可能有很多人一起发邮件,单单这个场景就这么人用,那用户管理可能才是性能瓶颈吧! +**Before performance testing, it is crucial to comprehend the business context of the current system.** If our understanding of the system's business is not deep enough, we easily make biased testing direction mistakes, leading us to overlook certain areas of the system that need performance testing. For example, if our system provides users with the ability to send emails, after setting up a successful email address, users can simply input the corresponding email to send, and the system can handle thousands of email-sending requests daily. Many would immediately begin using relevant tools to test the email-sending interface, but sending emails may not currently be the performance bottleneck of the system. With so many users sending emails, there might be many others sending emails simultaneously, so user management could be the actual performance bottleneck! -### 历史数据非常有用 +### Historical Data is Very Useful -当前系统所留下的历史数据非常重要,一般情况下,我们可以通过相应的些历史数据初步判定这个系统哪些接口调用的比较多、哪些服务承受的压力最大,这样的话,我们就可以针对这些地方进行更细致的性能测试与分析。 +The historical data left by the current system is very important. Generally, we can use relevant historical data to initially determine which interfaces are being called frequently and which services are under the most pressure. This allows us to conduct more detailed performance testing and analysis targeted at these areas. -另外,这些地方也就像这个系统的一个短板一样,优化好了这些地方会为我们的系统带来质的提升。 +Moreover, these areas are like weak points in the system; optimizing them can result in a significant qualitative improvement for our system. -## 常见性能指标 +## Common Performance Metrics -### 响应时间 +### Response Time -**响应时间 RT(Response-time)就是用户发出请求到用户收到系统处理结果所需要的时间。** +**Response Time RT (Response-time) is the time taken from when a user issues a request to when the user receives the system's processed result.** -RT 是一个非常重要且直观的指标,RT 数值大小直接反应了系统处理用户请求速度的快慢。 +RT is a very important and intuitive metric. The size of the RT value directly reflects how quickly the system processes user requests. -### 并发数 +### Concurrent Users -**并发数可以简单理解为系统能够同时供多少人访问使用也就是说系统同时能处理的请求数量。** +**Concurrent users can be simply understood as the number of people the system can support to access and use simultaneously, meaning the number of requests the system can handle at the same time.** -并发数反应了系统的负载能力。 +The number of concurrent users reflects the system's load capacity. -### QPS 和 TPS +### QPS and TPS -- **QPS(Query Per Second)** :服务器每秒可以执行的查询次数; -- **TPS(Transaction Per Second)** :服务器每秒处理的事务数(这里的一个事务可以理解为客户发出请求到收到服务器的过程); +- **QPS (Queries Per Second)**: The number of queries the server can execute per second; +- **TPS (Transactions Per Second)**: The number of transactions processed by the server per second (a transaction can be understood as the process from when a customer issues a request to when they receive a response from the server). -书中是这样描述 QPS 和 TPS 的区别的。 +The book describes the difference between QPS and TPS as follows: -> QPS vs TPS:QPS 基本类似于 TPS,但是不同的是,对于一个页面的一次访问,形成一个 TPS;但一次页面请求,可能产生多次对服务器的请求,服务器对这些请求,就可计入“QPS”之中。如,访问一个页面会请求服务器 2 次,一次访问,产生一个“T”,产生 2 个“Q”。 +> QPS vs TPS: QPS is quite similar to TPS, but the difference lies in that one access to a page counts as one TPS; however, a single page request may generate multiple requests to the server, which can be counted as "QPS." For example, if accessing a page generates 2 requests to the server, one access would result in one "T" and two "Q". -### 吞吐量 +### Throughput -**吞吐量指的是系统单位时间内系统处理的请求数量。** +**Throughput refers to the number of requests processed by the system within a unit of time.** -一个系统的吞吐量与请求对系统的资源消耗等紧密关联。请求对系统资源消耗越多,系统吞吐能力越低,反之则越高。 +The throughput of a system is closely related to the resource consumption of requests. The more resources a request consumes from the system, the lower the system's throughput capability, and vice versa. -TPS、QPS 都是吞吐量的常用量化指标。 +TPS and QPS are commonly used quantitative metrics for throughput. -- **QPS(TPS)** = 并发数/平均响应时间(RT) -- **并发数** = QPS \* 平均响应时间(RT) +- **QPS (TPS)** = Concurrent Users / Average Response Time (RT) +- **Concurrent Users** = QPS * Average Response Time (RT) -## 系统活跃度指标 +## System Activity Indicators -### PV(Page View) +### PV (Page View) -访问量, 即页面浏览量或点击量,衡量网站用户访问的网页数量;在一定统计周期内用户每打开或刷新一个页面就记录 1 次,多次打开或刷新同一页面则浏览量累计。UV 从网页打开的数量/刷新的次数的角度来统计的。 +Page Views refer to website traffic or click counts, measuring the number of pages visited by users; each time a user opens or refreshes a page within a specific statistical period, it records one instance; multiple openings or refreshes of the same page accumulate the view count. UV is counted from the perspective of the number of pages opened/refreshed. -### UV(Unique Visitor) +### UV (Unique Visitor) -独立访客,统计 1 天内访问某站点的用户数。1 天内相同访客多次访问网站,只计算为 1 个独立访客。UV 是从用户个体的角度来统计的。 +Unique Visitors refer to the count of users visiting a site within one day. If the same visitor accesses the site multiple times in one day, they are only counted as one unique visitor. UV is counted from the perspective of individual users. -### DAU(Daily Active User) +### DAU (Daily Active Users) -日活跃用户数量。 +The number of daily active users. -### MAU(monthly active users) +### MAU (Monthly Active Users) -月活跃用户人数。 +The number of monthly active users. -举例:某网站 DAU 为 1200w, 用户日均使用时长 1 小时,RT 为 0.5s,求并发量和 QPS。 +For example, if a website has a DAU of 12 million, with an average usage time of 1 hour per day, and an RT of 0.5s, we can calculate the concurrent user load and QPS. -平均并发量 = DAU(1200w)\* 日均使用时长(1 小时,3600 秒) /一天的秒数(86400)=1200w/24 = 50w +Average concurrent users = DAU (12 million) * Average usage time (1 hour, 3600 seconds) / Seconds in a day (86400) = 12 million / 24 = 500,000 -真实并发量(考虑到某些时间段使用人数比较少) = DAU(1200w)\* 日均使用时长(1 小时,3600 秒) /一天的秒数-访问量比较小的时间段假设为 8 小时(57600)=1200w/16 = 75w +Real concurrent users (considering lower usage during certain periods) = DAU (12 million) * Average usage time (1 hour, 3600 seconds) / Seconds in a day - assume 8 hours (57600) for low access time = 12 million / 16 = 750,000 -峰值并发量 = 平均并发量 \* 6 = 300w +Peak concurrent users = Average concurrent users * 6 = 3 million -QPS = 真实并发量/RT = 75W/0.5=150w/s +QPS = Real concurrent users / RT = 750,000 / 0.5 = 1.5 million/s -## 性能测试分类 +## Classification of Performance Testing -### 性能测试 +### Performance Testing -性能测试方法是通过测试工具模拟用户请求系统,目的主要是为了测试系统的性能是否满足要求。通俗地说,这种方法就是要在特定的运行条件下验证系统的能力状态。 +Performance testing methods simulate user requests to the system using testing tools, primarily to verify whether the system's performance meets the requirements. Simply put, this method aims to validate the system's capacity under specific operating conditions. -性能测试是你在对系统性能已经有了解的前提之后进行的,并且有明确的性能指标。 +Performance testing is carried out after you have some understanding of system performance, with clear performance metrics. -### 负载测试 +### Load Testing -对被测试的系统继续加大请求压力,直到服务器的某个资源已经达到饱和了,比如系统的缓存已经不够用了或者系统的响应时间已经不满足要求了。 +Continuously increasing request pressure on the tested system until some resource on the server reaches saturation, such as when system caching is insufficient or the response time is no longer acceptable. -负载测试说白点就是测试系统的上限。 +In simpler terms, load testing tests the limits of the system. -### 压力测试 +### Stress Testing -不去管系统资源的使用情况,对系统继续加大请求压力,直到服务器崩溃无法再继续提供服务。 +Pressuring the system with requests regardless of resource utilization until the server crashes and can no longer provide services. -### 稳定性测试 +### Stability Testing -模拟真实场景,给系统一定压力,看看业务是否能稳定运行。 +Simulating real scenarios by applying a certain amount of pressure on the system to see if the business can operate stably. -## 常用性能测试工具 +## Commonly Used Performance Testing Tools -### 后端常用 +### Common Backend Tools -既然系统设计涉及到系统性能方面的问题,那在面试的时候,面试官就很可能会问:**你是如何进行性能测试的?** +Since system design involves performance-related issues, interviewers are likely to ask, **How do you conduct performance testing?** -推荐 4 个比较常用的性能测试工具: +Here are four commonly used performance testing tools: -1. **Jmeter** :Apache JMeter 是 JAVA 开发的性能测试工具。 -2. **LoadRunner**:一款商业的性能测试工具。 -3. **Galtling** :一款基于 Scala 开发的高性能服务器性能测试工具。 -4. **ab** :全称为 Apache Bench 。Apache 旗下的一款测试工具,非常实用。 +1. **JMeter**: Apache JMeter is a performance testing tool developed in Java. +1. **LoadRunner**: A commercial performance testing tool. +1. **Gatling**: A high-performance server performance testing tool developed in Scala. +1. **ab**: Full name is Apache Bench. It is a testing tool under the Apache umbrella and is very practical. -没记错的话,除了 **LoadRunner** 其他几款性能测试工具都是开源免费的。 +If I remember correctly, all the performance testing tools mentioned, except for **LoadRunner**, are open-source and free. -### 前端常用 +### Common Frontend Tools -1. **Fiddler**:抓包工具,它可以修改请求的数据,甚至可以修改服务器返回的数据,功能非常强大,是 Web 调试的利器。 -2. **HttpWatch**: 可用于录制 HTTP 请求信息的工具。 +1. **Fiddler**: A packet capturing tool that can modify request data, and even server response data, with powerful features; it's an essential tool for web debugging. +1. **HttpWatch**: A tool for recording HTTP request information. -## 常见的性能优化策略 +## Common Performance Optimization Strategies -性能优化之前我们需要对请求经历的各个环节进行分析,排查出可能出现性能瓶颈的地方,定位问题。 +Before performance optimization, we need to analyze each stage of the requests to identify potential performance bottlenecks and locate issues. -下面是一些性能优化时,我经常拿来自问的一些问题: +Here are some questions I often ask myself during performance optimization: -1. 系统是否需要缓存? -2. 系统架构本身是不是就有问题? -3. 系统是否存在死锁的地方? -4. 系统是否存在内存泄漏?(Java 的自动回收内存虽然很方便,但是,有时候代码写的不好真的会造成内存泄漏) -5. 数据库索引使用是否合理? -6. …… +1. Does the system require caching? +1. Does the system architecture itself have problems? +1. Are there deadlock situations in the system? +1. Does the system have memory leaks? (While Java’s automatic memory recovery is very convenient, poorly written code can sometimes lead to memory leaks.) +1. Is the database indexing reasonable? +1. …… diff --git a/docs/high-availability/redundancy.md b/docs/high-availability/redundancy.md index 9d14d726675..1f40115ee83 100644 --- a/docs/high-availability/redundancy.md +++ b/docs/high-availability/redundancy.md @@ -1,47 +1,45 @@ --- -title: 冗余设计详解 -category: 高可用 +title: Detailed Explanation of Redundant Design +category: High Availability icon: cluster --- -冗余设计是保证系统和数据高可用的最常的手段。 +Redundant design is the most common means to ensure high availability of systems and data. -对于服务来说,冗余的思想就是相同的服务部署多份,如果正在使用的服务突然挂掉的话,系统可以很快切换到备份服务上,大大减少系统的不可用时间,提高系统的可用性。 +For services, the idea of redundancy is to deploy multiple instances of the same service. If the currently used service suddenly fails, the system can quickly switch to a backup service, significantly reducing downtime and improving system availability. -对于数据来说,冗余的思想就是相同的数据备份多份,这样就可以很简单地提高数据的安全性。 +For data, the idea of redundancy is to have multiple backups of the same data, which can easily enhance data security. -实际上,日常生活中就有非常多的冗余思想的应用。 +In fact, there are many applications of redundancy in daily life. -拿我自己来说,我对于重要文件的保存方法就是冗余思想的应用。我日常所使用的重要文件都会同步一份在 GitHub 以及个人云盘上,这样就可以保证即使电脑硬盘损坏,我也可以通过 GitHub 或者个人云盘找回自己的重要文件。 +For example, my method of saving important files is an application of redundancy. I synchronize important files I use daily to both GitHub and my personal cloud storage, ensuring that even if my computer's hard drive fails, I can retrieve my important files from GitHub or my personal cloud. -高可用集群(High Availability Cluster,简称 HA Cluster)、同城灾备、异地灾备、同城多活和异地多活是冗余思想在高可用系统设计中最典型的应用。 +High Availability Cluster (HA Cluster), local disaster recovery, remote disaster recovery, local active-active, and remote active-active are the most typical applications of redundancy in high availability system design. -- **高可用集群** : 同一份服务部署两份或者多份,当正在使用的服务突然挂掉的话,可以切换到另外一台服务,从而保证服务的高可用。 -- **同城灾备**:一整个集群可以部署在同一个机房,而同城灾备中相同服务部署在同一个城市的不同机房中。并且,备用服务不处理请求。这样可以避免机房出现意外情况比如停电、火灾。 -- **异地灾备**:类似于同城灾备,不同的是,相同服务部署在异地(通常距离较远,甚至是在不同的城市或者国家)的不同机房中 -- **同城多活**:类似于同城灾备,但备用服务可以处理请求,这样可以充分利用系统资源,提高系统的并发。 -- **异地多活** : 将服务部署在异地的不同机房中,并且,它们可以同时对外提供服务。 +- **High Availability Cluster**: The same service is deployed in two or more instances. If the currently used service suddenly fails, it can switch to another service, ensuring high availability. +- **Local Disaster Recovery**: An entire cluster can be deployed in the same data center, while in local disaster recovery, the same service is deployed in different data centers within the same city. Additionally, the backup service does not handle requests. This helps avoid unexpected situations in the data center, such as power outages or fires. +- **Remote Disaster Recovery**: Similar to local disaster recovery, but the same service is deployed in different data centers in remote locations (usually far apart, even in different cities or countries). +- **Local Active-Active**: Similar to local disaster recovery, but the backup service can handle requests, allowing for better utilization of system resources and increased concurrency. +- **Remote Active-Active**: Services are deployed in different data centers in remote locations, and they can provide services simultaneously. -高可用集群单纯是服务的冗余,并没有强调地域。同城灾备、异地灾备、同城多活和异地多活实现了地域上的冗余。 +High Availability Clusters focus solely on service redundancy without emphasizing geographic location. Local disaster recovery, remote disaster recovery, local active-active, and remote active-active achieve geographic redundancy. -同城和异地的主要区别在于机房之间的距离。异地通常距离较远,甚至是在不同的城市或者国家。 +The main difference between local and remote is the distance between data centers. Remote locations are usually farther apart, even in different cities or countries. -和传统的灾备设计相比,同城多活和异地多活最明显的改变在于“多活”,即所有站点都是同时在对外提供服务的。异地多活是为了应对突发状况比如火灾、地震等自然或者人为灾害。 +Compared to traditional disaster recovery design, the most significant change in local active-active and remote active-active is the "active-active" aspect, meaning all sites are simultaneously providing services. Remote active-active is designed to respond to emergencies such as fires, earthquakes, or other natural or man-made disasters. -光做好冗余还不够,必须要配合上 **故障转移** 才可以! 所谓故障转移,简单来说就是实现不可用服务快速且自动地切换到可用服务,整个过程不需要人为干涉。 +Simply having redundancy is not enough; it must be paired with **failover**! Failover, in simple terms, is the ability to quickly and automatically switch from an unavailable service to an available one without human intervention. -举个例子:哨兵模式的 Redis 集群中,如果 Sentinel(哨兵) 检测到 master 节点出现故障的话, 它就会帮助我们实现故障转移,自动将某一台 slave 升级为 master,确保整个 Redis 系统的可用性。整个过程完全自动,不需要人工介入。我在[《Java 面试指北》](https://www.yuque.com/docs/share/f37fc804-bfe6-4b0d-b373-9c462188fec7)的「技术面试题篇」中的数据库部分详细介绍了 Redis 集群相关的知识点&面试题,感兴趣的小伙伴可以看看。 +For example, in a Redis cluster using Sentinel mode, if the Sentinel detects a failure in the master node, it will help us achieve failover by automatically promoting one of the slaves to master, ensuring the availability of the entire Redis system. The entire process is fully automated and requires no manual intervention. I have detailed the relevant knowledge and interview questions about Redis clusters in the database section of [“Java Interview Guide”](https://www.yuque.com/docs/share/f37fc804-bfe6-4b0d-b373-9c462188fec7), which interested readers can check out. -再举个例子:Nginx 可以结合 Keepalived 来实现高可用。如果 Nginx 主服务器宕机的话,Keepalived 可以自动进行故障转移,备用 Nginx 主服务器升级为主服务。并且,这个切换对外是透明的,因为使用的虚拟 IP,虚拟 IP 不会改变。我在[《Java 面试指北》](https://www.yuque.com/docs/share/f37fc804-bfe6-4b0d-b373-9c462188fec7)的「技术面试题篇」中的「服务器」部分详细介绍了 Nginx 相关的知识点&面试题,感兴趣的小伙伴可以看看。 +Another example: Nginx can be combined with Keepalived to achieve high availability. If the primary Nginx server goes down, Keepalived can automatically perform failover, promoting the backup Nginx server to primary. Moreover, this switch is transparent to external users because a virtual IP is used, which does not change. I have detailed the relevant knowledge and interview questions about Nginx in the "Servers" section of [“Java Interview Guide”](https://www.yuque.com/docs/share/f37fc804-bfe6-4b0d-b373-9c462188fec7), which interested readers can check out. -异地多活架构实施起来非常难,需要考虑的因素非常多。本人不才,实际项目中并没有实践过异地多活架构,我对其了解还停留在书本知识。 +Implementing a remote active-active architecture is very challenging, with many factors to consider. I have not personally practiced remote active-active architecture in actual projects, and my understanding remains at the theoretical level. -如果你想要深入学习异地多活相关的知识,我这里推荐几篇我觉得还不错的文章: +If you want to delve deeper into knowledge related to remote active-active, I recommend a few articles that I find quite good: -- [搞懂异地多活,看这篇就够了- 水滴与银弹 - 2021](https://mp.weixin.qq.com/s/T6mMDdtTfBuIiEowCpqu6Q) -- [四步构建异地多活](https://mp.weixin.qq.com/s/hMD-IS__4JE5_nQhYPYSTg) -- [《从零开始学架构》— 28 | 业务高可用的保障:异地多活架构](http://gk.link/a/10pKZ) +- [Understanding Remote Active-Active, This Article is Enough - Water Droplets and Silver Bullets - 2021](https://mp.weixin.qq.com/s/T6mMDdtTfBuIiEowCpqu6Q) +- [Building Remote Active-Active in Four Steps](https://mp.weixin.qq.com/s/hMD-IS__4JE5_nQhYPYSTg) +- [“Learning Architecture from Scratch” — 28 | Ensuring Business High Availability: Remote Active-Active Architecture](http://gk.link/a/10pKZ) -不过,这些文章大多也都是在介绍概念知识。目前,网上还缺少真正介绍具体要如何去实践落地异地多活架构的资料。 - - +However, most of these articles mainly introduce conceptual knowledge. Currently, there is diff --git a/docs/high-availability/timeout-and-retry.md b/docs/high-availability/timeout-and-retry.md index 3c7ba1ac9cd..8b5f3a4a161 100644 --- a/docs/high-availability/timeout-and-retry.md +++ b/docs/high-availability/timeout-and-retry.md @@ -1,86 +1,58 @@ --- -title: 超时&重试详解 -category: 高可用 +title: Timeout & Retry Explained +category: High Availability icon: retry --- -由于网络问题、系统或者服务内部的 Bug、服务器宕机、操作系统崩溃等问题的不确定性,我们的系统或者服务永远不可能保证时刻都是可用的状态。 +Due to uncertainties such as network issues, internal bugs in systems or services, server crashes, and operating system failures, our systems or services can never guarantee constant availability. -为了最大限度的减小系统或者服务出现故障之后带来的影响,我们需要用到的 **超时(Timeout)** 和 **重试(Retry)** 机制。 +To minimize the impact of failures in systems or services, we need to utilize **Timeout** and **Retry** mechanisms. -想要把超时和重试机制讲清楚其实很简单,因为它俩本身就不是什么高深的概念。 +Explaining the timeout and retry mechanisms is actually quite simple, as they are not complex concepts. -虽然超时和重试机制的思想很简单,但是它俩是真的非常实用。你平时接触到的绝大部分涉及到远程调用的系统或者服务都会应用超时和重试机制。尤其是对于微服务系统来说,正确设置超时和重试非常重要。单体服务通常只涉及数据库、缓存、第三方 API、中间件等的网络调用,而微服务系统内部各个服务之间还存在着网络调用。 +Although the ideas behind timeout and retry mechanisms are straightforward, they are indeed very practical. Most systems or services you encounter that involve remote calls will apply timeout and retry mechanisms. This is especially important for microservices systems, where correctly setting timeouts and retries is crucial. Monolithic services typically only involve network calls to databases, caches, third-party APIs, and middleware, while microservices systems also involve network calls between various services. -## 超时机制 +## Timeout Mechanism -### 什么是超时机制? +### What is the Timeout Mechanism? -超时机制说的是当一个请求超过指定的时间(比如 1s)还没有被处理的话,这个请求就会直接被取消并抛出指定的异常或者错误(比如 `504 Gateway Timeout`)。 +The timeout mechanism refers to the cancellation of a request if it exceeds a specified time (e.g., 1 second) without being processed, resulting in a specified exception or error being thrown (e.g., `504 Gateway Timeout`). -我们平时接触到的超时可以简单分为下面 2 种: +Timeouts can generally be categorized into the following two types: -- **连接超时(ConnectTimeout)**:客户端与服务端建立连接的最长等待时间。 -- **读取超时(ReadTimeout)**:客户端和服务端已经建立连接,客户端等待服务端处理完请求的最长时间。实际项目中,我们关注比较多的还是读取超时。 +- **Connect Timeout**: The maximum wait time for the client to establish a connection with the server. +- **Read Timeout**: The maximum time the client waits for the server to complete processing the request after a connection has been established. In practical projects, we often focus more on read timeouts. -一些连接池客户端框架中可能还会有获取连接超时和空闲连接清理超时。 +Some connection pool client frameworks may also have connection acquisition timeouts and idle connection cleanup timeouts. -如果没有设置超时的话,就可能会导致服务端连接数爆炸和大量请求堆积的问题。 +If timeouts are not set, it can lead to an explosion of server connections and a backlog of requests. -这些堆积的连接和请求会消耗系统资源,影响新收到的请求的处理。严重的情况下,甚至会拖垮整个系统或者服务。 +These accumulated connections and requests consume system resources, affecting the processing of new incoming requests. In severe cases, it can even bring down the entire system or service. -我之前在实际项目就遇到过类似的问题,整个网站无法正常处理请求,服务器负载直接快被拉满。后面发现原因是项目超时设置错误加上客户端请求处理异常,导致服务端连接数直接接近 40w+,这么多堆积的连接直接把系统干趴了。 +I encountered a similar issue in a previous project where the entire website could not process requests normally, and the server load was nearly maxed out. The cause was found to be incorrect timeout settings combined with exceptions in client request handling, leading to server connections approaching 400,000+. Such a backlog of connections directly caused the system to crash. -### 超时时间应该如何设置? +### How Should Timeout Values Be Set? -超时到底设置多长时间是一个难题!超时值设置太高或者太低都有风险。如果设置太高的话,会降低超时机制的有效性,比如你设置超时为 10s 的话,那设置超时就没啥意义了,系统依然可能会出现大量慢请求堆积的问题。如果设置太低的话,就可能会导致在系统或者服务在某些处理请求速度变慢的情况下(比如请求突然增多),大量请求重试(超时通常会结合重试)继续加重系统或者服务的压力,进而导致整个系统或者服务被拖垮的问题。 +Determining the appropriate timeout duration is a challenge! Setting the timeout value too high or too low both carry risks. If set too high, it reduces the effectiveness of the timeout mechanism; for example, if you set the timeout to 10 seconds, then the timeout becomes meaningless, and the system may still experience a backlog of slow requests. If set too low, it may lead to a situation where, under certain conditions (e.g., a sudden increase in requests), a large number of requests are retried (timeouts are usually combined with retries), further increasing the pressure on the system or service, potentially leading to a complete system failure. -通常情况下,我们建议读取超时设置为 **1500ms** ,这是一个比较普适的值。如果你的系统或者服务对于延迟比较敏感的话,那读取超时值可以适当在 **1500ms** 的基础上进行缩短。反之,读取超时值也可以在 **1500ms** 的基础上进行加长,不过,尽量还是不要超过 **1500ms** 。连接超时可以适当设置长一些,建议在 **1000ms ~ 5000ms** 之内。 +In general, we recommend setting the read timeout to **1500ms**, which is a relatively universal value. If your system or service is sensitive to latency, the read timeout can be shortened from the **1500ms** baseline. Conversely, it can also be extended, but it is best not to exceed **1500ms**. The connect timeout can be set a bit longer, ideally within **1000ms to 5000ms**. -没有银弹!超时值具体该设置多大,还是要根据实际项目的需求和情况慢慢调整优化得到。 +There is no silver bullet! The specific timeout value should be adjusted and optimized based on the actual project requirements and conditions. -更上一层,参考[美团的 Java 线程池参数动态配置](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)思想,我们也可以将超时弄成可配置化的参数而不是固定的,比较简单的一种办法就是将超时的值放在配置中心中。这样的话,我们就可以根据系统或者服务的状态动态调整超时值了。 +Furthermore, referring to [Meituan's dynamic configuration of Java thread pool parameters](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html), we can also make timeouts configurable parameters rather than fixed values. A simple approach is to place the timeout values in a configuration center. This way, we can dynamically adjust the timeout values based on the state of the system or service. -## 重试机制 +## Retry Mechanism -### 什么是重试机制? +### What is the Retry Mechanism? -重试机制一般配合超时机制一起使用,指的是多次发送相同的请求来避免瞬态故障和偶然性故障。 +The retry mechanism is generally used in conjunction with the timeout mechanism, referring to the repeated sending of the same request to avoid transient and occasional failures. -瞬态故障可以简单理解为某一瞬间系统偶然出现的故障,并不会持久。偶然性故障可以理解为哪些在某些情况下偶尔出现的故障,频率通常较低。 +Transient failures can be simply understood as faults that occur momentarily in the system and are not persistent. Occasional failures can be understood as faults that occur infrequently under certain conditions. -重试的核心思想是通过消耗服务器的资源来尽可能获得请求更大概率被成功处理。由于瞬态故障和偶然性故障是很少发生的,因此,重试对于服务器的资源消耗几乎是可以被忽略的。 +The core idea of retrying is to consume server resources to maximize the probability of successfully processing a request. Since transient and occasional failures are rare, the resource consumption from retries is almost negligible. -### 常见的重试策略有哪些? +### What Are Common Retry Strategies? -常见的重试策略有两种: +There are two common retry strategies: -1. **固定间隔时间重试**:每次重试之间都使用相同的时间间隔,比如每隔 1.5 秒进行一次重试。这种重试策略的优点是实现起来比较简单,不需要考虑重试次数和时间的关系,也不需要维护额外的状态信息。但是这种重试策略的缺点是可能会导致重试过于频繁或过于稀疏,从而影响系统的性能和效率。如果重试间隔太短,可能会对目标系统造成过大的压力,导致雪崩效应;如果重试间隔太长,可能会导致用户等待时间过长,影响用户体验。 -2. **梯度间隔重试**:根据重试次数的增加去延长下次重试时间,比如第一次重试间隔为 1 秒,第二次为 2 秒,第三次为 4 秒,以此类推。这种重试策略的优点是能够有效提高重试成功的几率(随着重试次数增加,但是重试依然不成功,说明目标系统恢复时间比较长,因此可以根据重试次数延长下次重试时间),也能通过柔性化的重试避免对下游系统造成更大压力。但是这种重试策略的缺点是实现起来比较复杂,需要考虑重试次数和时间的关系,以及设置合理的上限和下限值。另外,这种重试策略也可能会导致用户等待时间过长,影响用户体验。 - -这两种适合的场景各不相同。固定间隔时间重试适用于目标系统恢复时间比较稳定和可预测的场景,比如网络波动或服务重启。梯度间隔重试适用于目标系统恢复时间比较长或不可预测的场景,比如网络故障和服务故障。 - -### 重试的次数如何设置? - -重试的次数不宜过多,否则依然会对系统负载造成比较大的压力。 - -重试的次数通常建议设为 3 次。大部分情况下,我们还是更建议使用梯度间隔重试策略,比如说我们要重试 3 次的话,第 1 次请求失败后,等待 1 秒再进行重试,第 2 次请求失败后,等待 2 秒再进行重试,第 3 次请求失败后,等待 3 秒再进行重试。 - -### 什么是重试幂等? - -超时和重试机制在实际项目中使用的话,需要注意保证同一个请求没有被多次执行。 - -什么情况下会出现一个请求被多次执行呢?客户端等待服务端完成请求完成超时但此时服务端已经执行了请求,只是由于短暂的网络波动导致响应在发送给客户端的过程中延迟了。 - -举个例子:用户支付购买某个课程,结果用户支付的请求由于重试的问题导致用户购买同一门课程支付了两次。对于这种情况,我们在执行用户购买课程的请求的时候需要判断一下用户是否已经购买过。这样的话,就不会因为重试的问题导致重复购买了。 - -### Java 中如何实现重试? - -如果要手动编写代码实现重试逻辑的话,可以通过循环(例如 while 或 for 循环)或者递归实现。不过,一般不建议自己动手实现,有很多第三方开源库提供了更完善的重试机制实现,例如 Spring Retry、Resilience4j、Guava Retrying。 - -## 参考 - -- 微服务之间调用超时的设置治理: -- 超时、重试和抖动回退: - - +1. **Fixed Interval Retry**: Each retry uses the same time interval, such as retrying every 1.5 seconds. The advantage of this strategy is its simplicity; there is no need to consider the relationship between retry count and time, nor maintain additional state information. However, the downside is that it may lead to retries being too frequent or too sparse, affecting system performance and efficiency. If the retry interval is too short, it may put excessive pressure on the target system, leading to diff --git a/docs/high-performance/cdn.md b/docs/high-performance/cdn.md index f4ca0eab5f2..417a6a70b4f 100644 --- a/docs/high-performance/cdn.md +++ b/docs/high-performance/cdn.md @@ -1,135 +1,135 @@ --- -title: CDN工作原理详解 -category: 高性能 +title: Detailed Explanation of How CDN Works +category: High Performance head: - - - meta - - name: keywords - content: CDN,内容分发网络 - - - meta - - name: description - content: CDN 就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。 + - - meta + - name: keywords + content: CDN, Content Delivery Network + - - meta + - name: description + content: CDN is the distribution of static resources to multiple different places to achieve nearby access, thereby speeding up the access speed of static resources and reducing the burden on servers and bandwidth. --- -## 什么是 CDN ? +## What is CDN? -**CDN** 全称是 Content Delivery Network/Content Distribution Network,翻译过的意思是 **内容分发网络** 。 +**CDN** stands for Content Delivery Network, which translates to **Content Distribution Network**. -我们可以将内容分发网络拆开来看: +We can break down the concept of a content distribution network: -- 内容:指的是静态资源比如图片、视频、文档、JS、CSS、HTML。 -- 分发网络:指的是将这些静态资源分发到位于多个不同的地理位置机房中的服务器上,这样,就可以实现静态资源的就近访问比如北京的用户直接访问北京机房的数据。 +- Content: Refers to static resources such as images, videos, documents, JS, CSS, and HTML. +- Distribution Network: Refers to distributing these static resources to servers located in different geographic locations, allowing for nearby access to static resources, such as a user in Beijing directly accessing data from the Beijing data center. -所以,简单来说,**CDN 就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。** +So, simply put, **CDN is the distribution of static resources to multiple different places to achieve nearby access, thereby speeding up the access speed of static resources and reducing the burden on servers and bandwidth.** -类似于京东建立的庞大的仓储运输体系,京东物流在全国拥有非常多的仓库,仓储网络几乎覆盖全国所有区县。这样的话,用户下单的第一时间,商品就从距离用户最近的仓库,直接发往对应的配送站,再由京东小哥送到你家。 +Similar to the massive warehousing and transportation system established by JD.com, JD logistics has many warehouses across the country, with a storage network covering almost all districts and counties in the country. This way, when a user places an order, the product is sent directly from the nearest warehouse to the corresponding distribution station, and then delivered to your home by JD couriers. -![京东仓配系统](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/jingdong-wuliu-cangpei.png) +![JD Warehouse and Distribution System](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/jingdong-wuliu-cangpei.png) -你可以将 CDN 看作是服务上一层的特殊缓存服务,分布在全国各地,主要用来处理静态资源的请求。 +You can think of CDN as a special caching service on top of the server, distributed across the country, primarily used to handle requests for static resources. -![CDN 简易示意图](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-101.png) +![CDN Simplified Diagram](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-101.png) -我们经常拿全站加速和内容分发网络做对比,不要把两者搞混了!全站加速(不同云服务商叫法不同,腾讯云叫 ECDN、阿里云叫 DCDN)既可以加速静态资源又可以加速动态资源,内容分发网络(CDN)主要针对的是 **静态资源** 。 +We often compare full-site acceleration with content delivery networks; do not confuse the two! Full-site acceleration (known as ECDN by Tencent Cloud and DCDN by Alibaba Cloud) can accelerate both static and dynamic resources, whereas content delivery network (CDN) mainly targets **static resources**. -![阿里云文档:https://help.aliyun.com/document_detail/64836.html](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-aliyun-dcdn.png) +![Alibaba Cloud Documentation: https://help.aliyun.com/document_detail/64836.html](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-aliyun-dcdn.png) -绝大部分公司都会在项目开发中使用 CDN 服务,但很少会有自建 CDN 服务的公司。基于成本、稳定性和易用性考虑,建议直接选择专业的云厂商(比如阿里云、腾讯云、华为云、青云)或者 CDN 厂商(比如网宿、蓝汛)提供的开箱即用的 CDN 服务。 +Most companies will use CDN services in their project development, but very few will build their own CDN services. Considering cost, stability, and usability, it is advisable to choose ready-to-use CDN services offered by professional cloud vendors (such as Alibaba Cloud, Tencent Cloud, Huawei Cloud, QingCloud) or CDN providers (such as Wangsu, Blue boolean). -很多朋友可能要问了:**既然是就近访问,为什么不直接将服务部署在多个不同的地方呢?** +Many friends may ask: **Since it's about nearby access, why not deploy services directly in multiple different places?** -- 成本太高,需要部署多份相同的服务。 -- 静态资源通常占用空间比较大且经常会被访问到,如果直接使用服务器或者缓存来处理静态资源请求的话,对系统资源消耗非常大,可能会影响到系统其他服务的正常运行。 +- The cost is too high, as multiple identical services need to be deployed. +- Static resources usually take up a lot of space and are frequently accessed. If we use servers or caches to handle static resource requests directly, it consumes a lot of system resources, potentially affecting the normal operation of other system services. -同一个服务在在多个不同的地方部署多份(比如同城灾备、异地灾备、同城多活、异地多活)是为了实现系统的高可用而不是就近访问。 +Deploying the same service in multiple different locations (such as local disaster recovery, remote disaster recovery, local active-active, remote active-active) is for achieving high availability of the system, not for nearby access. -## CDN 工作原理是什么? +## What is the Working Principle of CDN? -搞懂下面 3 个问题也就搞懂了 CDN 的工作原理: +Understanding the following three questions will help you grasp the working principle of CDN: -1. 静态资源是如何被缓存到 CDN 节点中的? -2. 如何找到最合适的 CDN 节点? -3. 如何防止静态资源被盗用? +1. How are static resources cached to CDN nodes? +1. How to find the most suitable CDN node? +1. How to prevent static resources from being misused? -### 静态资源是如何被缓存到 CDN 节点中的? +### How are static resources cached to CDN nodes? -你可以通过 **预热** 的方式将源站的资源同步到 CDN 的节点中。这样的话,用户首次请求资源可以直接从 CDN 节点中取,无需回源。这样可以降低源站压力,提升用户体验。 +You can synchronize the origin server's resources to the CDN nodes through **warming**. This way, users requesting resources for the first time can directly retrieve them from the CDN node without needing to go back to the origin, reducing the pressure on the origin server and improving user experience. -如果不预热的话,你访问的资源可能不在 CDN 节点中,这个时候 CDN 节点将请求源站获取资源,这个过程是大家经常说的 **回源**。 +If resources are not warmed, the requested resources may not be present at the CDN node. In this case, the CDN node will fetch the resources from the origin server, which is often referred to as **back to source**. -> - 回源:当 CDN 节点上没有用户请求的资源或该资源的缓存已经过期时,CDN 节点需要从原始服务器获取最新的资源内容,这个过程就是回源。当用户请求发生回源的话,会导致该请求的响应速度比未使用 CDN 还慢,因为相比于未使用 CDN 还多了一层 CDN 的调用流程。 -> - 预热:预热是指在 CDN 上提前将内容缓存到 CDN 节点上。这样当用户在请求这些资源时,能够快速地从最近的 CDN 节点获取到而不需要回源,进而减少了对源站的访问压力,提高了访问速度。 +> - Back to Source: When the CDN node does not have the requested resource or the resource's cache has expired, the CDN node needs to retrieve the latest resource content from the original server. This process is known as back to source. When a user request undergoes back to source, it results in a response speed slower than without using CDN, as it adds an extra layer of CDN call process. +> - Warming: Warming refers to caching content in advance at the CDN nodes. This allows users to quickly retrieve resources from the nearest CDN node without going back to the origin, thereby reducing the access pressure on the origin server and improving access speed. -![CDN 回源](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-back-to-source.png) +![CDN Back to Source](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-back-to-source.png) -如果资源有更新的话,你也可以对其 **刷新** ,删除 CDN 节点上缓存的旧资源,并强制 CDN 节点回源站获取最新资源。 +If there are updates to resources, you can also **refresh** them by deleting the old cached resources on the CDN node and forcing the CDN node to go back to the origin server to fetch the latest resources. -几乎所有云厂商提供的 CDN 服务都具备缓存的刷新和预热功能(下图是阿里云 CDN 服务提供的相应功能): +Almost all cloud providers' CDN services offer cache refresh and warming functions (the following image shows the corresponding functions provided by Alibaba Cloud CDN): -![CDN 缓存的刷新和预热](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-refresh-warm-up.png) +![CDN Cache Refresh and Warming](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-refresh-warm-up.png) -**命中率** 和 **回源率** 是衡量 CDN 服务质量两个重要指标。命中率越高越好,回源率越低越好。 +**Hit Rate** and **Back to Source Rate** are two important indicators for measuring the quality of CDN services. The higher the hit rate, the better, and the lower the back to source rate, the better. -### 如何找到最合适的 CDN 节点? +### How to find the most suitable CDN node? -GSLB (Global Server Load Balance,全局负载均衡)是 CDN 的大脑,负责多个 CDN 节点之间相互协作,最常用的是基于 DNS 的 GSLB。 +GSLB (Global Server Load Balance) is the brain of the CDN, responsible for the cooperation among multiple CDN nodes. The most commonly used is DNS-based GSLB. -CDN 会通过 GSLB 找到最合适的 CDN 节点,更具体点来说是下面这样的: +CDN uses GSLB to find the most suitable CDN node, specifically as follows: -1. 浏览器向 DNS 服务器发送域名请求; -2. DNS 服务器向根据 CNAME( Canonical Name ) 别名记录向 GSLB 发送请求; -3. GSLB 返回性能最好(通常距离请求地址最近)的 CDN 节点(边缘服务器,真正缓存内容的地方)的地址给浏览器; -4. 浏览器直接访问指定的 CDN 节点。 +1. The browser sends a domain name request to the DNS server; +1. The DNS server sends a request to GSLB based on the CNAME (Canonical Name) alias record; +1. GSLB returns the address of the best-performing (usually the nearest to the request address) CDN node (edge server, the actual caching location) to the browser; +1. The browser directly accesses the specified CDN node. -![CDN 原理示意图](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-overview.png) +![CDN Principle Diagram](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-overview.png) -为了方便理解,上图其实做了一点简化。GSLB 内部可以看作是 CDN 专用 DNS 服务器和负载均衡系统组合。CDN 专用 DNS 服务器会返回负载均衡系统 IP 地址给浏览器,浏览器使用 IP 地址请求负载均衡系统进而找到对应的 CDN 节点。 +To facilitate understanding, the above diagram simplifies certain aspects. Internally, GSLB can be viewed as a combination of a CDN-specific DNS server and a load-balancing system. The CDN-specific DNS server returns the IP address of the load-balancing system to the browser, which then uses this IP address to request the load-balancing system to find the corresponding CDN node. -**GSLB 是如何选择出最合适的 CDN 节点呢?** GSLB 会根据请求的 IP 地址、CDN 节点状态(比如负载情况、性能、响应时间、带宽)等指标来综合判断具体返回哪一个 CDN 节点的地址。 +**How does GSLB choose the most suitable CDN node?** GSLB comprehensively judges which CDN node’s address to return based on metrics such as the request's IP address, the status of the CDN node (such as load status, performance, response time, bandwidth), etc. -### 如何防止资源被盗刷? +### How to prevent misuse of resources? -如果我们的资源被其他用户或者网站非法盗刷的话,将会是一笔不小的开支。 +If our resources are illegally accessed or scraped by other users or websites, it could result in significant expenses. -解决这个问题最常用最简单的办法设置 **Referer 防盗链**,具体来说就是根据 HTTP 请求的头信息里面的 Referer 字段对请求进行限制。我们可以通过 Referer 字段获取到当前请求页面的来源页面的网站地址,这样我们就能确定请求是否来自合法的网站。 +The most common and straightforward solution to this problem is to set up **Referer anti-hotlinking**. Specifically, this limits requests based on the Referer field in the HTTP request headers. By utilizing the Referer field, we can determine the originating website address of the current request and ascertain whether it comes from a legitimate website. -CDN 服务提供商几乎都提供了这种比较基础的防盗链机制。 +Almost all CDN service providers offer this basic anti-hotlinking mechanism. -![腾讯云 CDN Referer 防盗链配置](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cnd-tencent-cloud-anti-theft.png) +![Tencent Cloud CDN Referer Anti-Hotlinking Configuration](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cnd-tencent-cloud-anti-theft.png) -不过,如果站点的防盗链配置允许 Referer 为空的话,通过隐藏 Referer,可以直接绕开防盗链。 +However, if the site's anti-hotlinking configuration allows a blank Referer, it is possible to bypass the anti-hotlinking by hiding the Referer. -通常情况下,我们会配合其他机制来确保静态资源被盗用,一种常用的机制是 **时间戳防盗链** 。相比之下,**时间戳防盗链** 的安全性更强一些。时间戳防盗链加密的 URL 具有时效性,过期之后就无法再被允许访问。 +Usually, we combine other mechanisms to ensure the static resources are not misused. A commonly used mechanism is **timestamp anti-hotlinking**. Compared to the simple Referer-based approach, **timestamp anti-hotlinking** offers stronger security. The encrypted URL in timestamp anti-hotlinking is time-sensitive, and once it expires, access is no longer permitted. -时间戳防盗链的 URL 通常会有两个参数一个是签名字符串,一个是过期时间。签名字符串一般是通过对用户设定的加密字符串、请求路径、过期时间通过 MD5 哈希算法取哈希的方式获得。 +The URL for timestamp anti-hotlinking typically includes two parameters: a signature string and an expiration time. The signature string is generally obtained by executing an MD5 hash on a user-defined encrypted string, request path, and expiration time. -时间戳防盗链 URL 示例: +Example URL for timestamp anti-hotlinking: ```plain -http://cdn.wangsu.com/4/123.mp3? wsSecret=79aead3bd7b5db4adeffb93a010298b5&wsTime=1601026312 +http://cdn.wangsu.com/4/123.mp3?wsSecret=79aead3bd7b5db4adeffb93a010298b5&wsTime=1601026312 ``` -- `wsSecret`:签名字符串。 -- `wsTime`: 过期时间。 +- `wsSecret`: Signature string. +- `wsTime`: Expiration time. ![](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/timestamp-anti-theft.png) -时间戳防盗链的实现也比较简单,并且可靠性较高,推荐使用。并且,绝大部分 CDN 服务提供商都提供了开箱即用的时间戳防盗链机制。 +The implementation of timestamp anti-hotlinking is relatively simple and highly reliable, making it a recommended option. Additionally, most CDN service providers offer plug-and-play timestamp anti-hotlinking mechanisms. -![七牛云时间戳防盗链配置](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/qiniuyun-timestamp-anti-theft.png) +![Qiniu Cloud Timestamp Anti-Hotlinking Configuration](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/qiniuyun-timestamp-anti-theft.png) -除了 Referer 防盗链和时间戳防盗链之外,你还可以 IP 黑白名单配置、IP 访问限频配置等机制来防盗刷。 +In addition to Referer anti-hotlinking and timestamp anti-hotlinking, you can also utilize IP black and white list configurations, IP access rate limiting configurations, and other mechanisms to prevent misuse. -## 总结 +## Summary -- CDN 就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。 -- 基于成本、稳定性和易用性考虑,建议直接选择专业的云厂商(比如阿里云、腾讯云、华为云、青云)或者 CDN 厂商(比如网宿、蓝汛)提供的开箱即用的 CDN 服务。 -- GSLB (Global Server Load Balance,全局负载均衡)是 CDN 的大脑,负责多个 CDN 节点之间相互协作,最常用的是基于 DNS 的 GSLB。CDN 会通过 GSLB 找到最合适的 CDN 节点。 -- 为了防止静态资源被盗用,我们可以利用 **Referer 防盗链** + **时间戳防盗链** 。 +- CDN is the distribution of static resources to multiple different places to achieve nearby access, thereby speeding up the access speed of static resources and reducing the burden on servers and bandwidth. +- Considering cost, stability, and usability, it is advisable to choose ready-to-use CDN services offered by professional cloud vendors (such as Alibaba Cloud, Tencent Cloud, Huawei Cloud, QingCloud) or CDN providers (such as Wangsu, Blue boolean). +- GSLB (Global Server Load Balance) is the brain of the CDN, responsible for the cooperation among multiple CDN nodes. The most common implementation is DNS-based GSLB. CDN utilizes GSLB to find the most suitable CDN node. +- To prevent the misuse of static resources, we can use **Referer anti-hotlinking** combined with **timestamp anti-hotlinking**. -## 参考 +## References -- 时间戳防盗链 - 七牛云 CDN: -- CDN 是个啥玩意?一文说个明白: -- 《透视 HTTP 协议》- 37 | CDN:加速我们的网络服务: +- Timestamp Anti-Hotlinking - Qiniu Cloud CDN: +- What on Earth is CDN? A Clear Explanation: +- "Understanding HTTP Protocol" - 37 | CDN: Accelerating Our Network Services: diff --git a/docs/high-performance/data-cold-hot-separation.md b/docs/high-performance/data-cold-hot-separation.md index d7ae70c2bfd..436b68af621 100644 --- a/docs/high-performance/data-cold-hot-separation.md +++ b/docs/high-performance/data-cold-hot-separation.md @@ -1,68 +1,56 @@ --- -title: 数据冷热分离详解 -category: 高性能 +title: Detailed Explanation of Data Hot and Cold Separation +category: High Performance head: - - - meta - - name: keywords - content: 数据冷热分离,冷数据迁移,冷数据存储 - - - meta - - name: description - content: 数据冷热分离是指根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在存储在低成本、低性能的介质中,热数据高性能存储介质中。 + - - meta + - name: keywords + content: Data hot and cold separation, cold data migration, cold data storage + - - meta + - name: description + content: Data hot and cold separation refers to dividing data into cold and hot data based on access frequency and business importance. Cold data is generally stored in low-cost, low-performance media, while hot data is stored in high-performance media. --- -## 什么是数据冷热分离? +## What is Data Hot and Cold Separation? -数据冷热分离是指根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在存储在低成本、低性能的介质中,热数据高性能存储介质中。 +Data hot and cold separation refers to dividing data into cold and hot data based on access frequency and business importance. Cold data is generally stored in low-cost, low-performance media, while hot data is stored in high-performance media. -### 冷数据和热数据 +### Cold Data and Hot Data -热数据是指经常被访问和修改且需要快速访问的数据,冷数据是指不经常访问,对当前项目价值较低,但需要长期保存的数据。 +Hot data refers to data that is frequently accessed and modified and requires quick access, while cold data refers to data that is infrequently accessed, has lower current project value, but needs to be stored for the long term. -冷热数据到底如何区分呢?有两个常见的区分方法: +How do we distinguish between hot and cold data? There are two common methods of distinction: -1. **时间维度区分**:按照数据的创建时间、更新时间、过期时间等,将一定时间段内的数据视为热数据,超过该时间段的数据视为冷数据。例如,订单系统可以将 1 年前的订单数据作为冷数据,1 年内的订单数据作为热数据。这种方法适用于数据的访问频率和时间有较强的相关性的场景。 -2. **访问频率区分**:将高频访问的数据视为热数据,低频访问的数据视为冷数据。例如,内容系统可以将浏览量非常低的文章作为冷数据,浏览量较高的文章作为热数据。这种方法需要记录数据的访问频率,成本较高,适合访问频率和数据本身有较强的相关性的场景。 +1. **Time Dimension Distinction**: Data is classified as hot or cold based on its creation time, update time, expiration time, etc. Data within a certain time frame is considered hot, while data beyond that time frame is considered cold. For example, an order system might classify order data from one year ago as cold data and order data within the last year as hot data. This method is suitable for scenarios where access frequency is strongly correlated with time. +1. **Access Frequency Distinction**: Data that is accessed frequently is considered hot, while data that is accessed infrequently is considered cold. For example, a content system might classify articles with very low views as cold data and articles with higher views as hot data. This method requires tracking data access frequency, which can be costly, and is suitable for scenarios where access frequency is strongly correlated with the data itself. -几年前的数据并不一定都是冷数据,例如一些优质文章发表几年后依然有很多人访问,大部分普通用户新发表的文章却基本没什么人访问。 +Data from a few years ago is not necessarily all cold data; for instance, some high-quality articles may still receive many visits years after publication, while most newly published articles by ordinary users may hardly be accessed. -这两种区分冷热数据的方法各有优劣,实际项目中,可以将两者结合使用。 +Both methods of distinguishing hot and cold data have their pros and cons, and in actual projects, they can be used in combination. -### 冷热分离的思想 +### The Concept of Hot and Cold Separation -冷热分离的思想非常简单,就是对数据进行分类,然后分开存储。冷热分离的思想可以应用到很多领域和场景中,而不仅仅是数据存储,例如: +The concept of hot and cold separation is quite simple: classify data and then store it separately. This concept can be applied in many fields and scenarios, not just data storage, such as: -- 邮件系统中,可以将近期的比较重要的邮件放在收件箱,将比较久远的不太重要的邮件存入归档。 -- 日常生活中,可以将常用的物品放在显眼的位置,不常用的物品放入储藏室或者阁楼。 -- 图书馆中,可以将最受欢迎和最常借阅的图书单独放在一个显眼的区域,将较少借阅的书籍放在不起眼的位置。 +- In an email system, important recent emails can be placed in the inbox, while less important older emails can be archived. +- In daily life, frequently used items can be placed in prominent locations, while less frequently used items can be stored in a storage room or attic. +- In a library, the most popular and frequently borrowed books can be placed in a prominent area, while less borrowed books can be placed in less noticeable locations. - …… -### 数据冷热分离的优缺点 +### Advantages and Disadvantages of Data Hot and Cold Separation -- 优点:热数据的查询性能得到优化(用户的绝大部分操作体验会更好)、节约成本(可以冷热数据的不同存储需求,选择对应的数据库类型和硬件配置,比如将热数据放在 SSD 上,将冷数据放在 HDD 上) -- 缺点:系统复杂性和风险增加(需要分离冷热数据,数据错误的风险增加)、统计效率低(统计的时候可能需要用到冷库的数据)。 +- Advantages: The query performance of hot data is optimized (the user experience for most operations will be better), and costs are saved (different storage requirements for hot and cold data allow for the selection of appropriate database types and hardware configurations, such as placing hot data on SSDs and cold data on HDDs). +- Disadvantages: Increased system complexity and risk (the need to separate hot and cold data increases the risk of data errors), and lower statistical efficiency (cold storage data may be needed for statistics). -## 冷数据如何迁移? +## How to Migrate Cold Data? -冷数据迁移方案: +Cold data migration solutions: -1. 业务层代码实现:当有对数据进行写操作时,触发冷热分离的逻辑,判断数据是冷数据还是热数据,冷数据就入冷库,热数据就入热库。这种方案会影响性能且冷热数据的判断逻辑不太好确定,还需要修改业务层代码,因此一般不会使用。 -2. 任务调度:可以利用 xxl-job 或者其他分布式任务调度平台定时去扫描数据库,找出满足冷数据条件的数据,然后批量地将其复制到冷库中,并从热库中删除。这种方法修改的代码非常少,非常适合按照时间区分冷热数据的场景。 -3. 监听数据库的变更日志 binlog :将满足冷数据条件的数据从 binlog 中提取出来,然后复制到冷库中,并从热库中删除。这种方法可以不用修改代码,但不适合按照时间维度区分冷热数据的场景。 +1. **Business Layer Code Implementation**: When there is a write operation on the data, trigger the logic for hot and cold separation to determine whether the data is cold or hot. Cold data goes to cold storage, while hot data goes to hot storage. This solution can impact performance, and the logic for determining hot and cold data can be difficult to establish, plus it requires modifying business layer code, so it is generally not used. +1. **Task Scheduling**: You can use xxl-job or other distributed task scheduling platforms to periodically scan the database, identify data that meets the cold data criteria, and then batch copy it to cold storage while deleting it from hot storage. This method requires very little code modification and is very suitable for scenarios where cold and hot data are distinguished by time. +1. **Listening to Database Change Logs (binlog)**: Extract data that meets the cold data criteria from the binlog, then copy it to cold storage and delete it from hot storage. This method does not require code modification but is not suitable for scenarios where hot and cold data are distinguished by time. -如果你的公司有 DBA 的话,也可以让 DBA 进行冷数据的人工迁移,一次迁移完成冷数据到冷库。然后,再搭配上面介绍的方案实现后续冷数据的迁移工作。 +If your company has a DBA, you can also have the DBA perform manual migration of cold data to cold storage in one go. Then, you can use the solutions mentioned above to implement subsequent cold data migration work. -## 冷数据如何存储? +## How to Store Cold Data? -冷数据的存储要求主要是容量大,成本低,可靠性高,访问速度可以适当牺牲。 - -冷数据存储方案: - -- 中小厂:直接使用 MySQL/PostgreSQL 即可(不改变数据库选型和项目当前使用的数据库保持一致),比如新增一张表来存储某个业务的冷数据或者使用单独的冷库来存放冷数据(涉及跨库查询,增加了系统复杂性和维护难度) -- 大厂:Hbase(常用)、RocksDB、Doris、Cassandra - -如果公司成本预算足的话,也可以直接上 TiDB 这种分布式关系型数据库,直接一步到位。TiDB 6.0 正式支持数据冷热存储分离,可以降低 SSD 使用成本。使用 TiDB 6.0 的数据放置功能,可以在同一个集群实现海量数据的冷热存储,将新的热数据存入 SSD,历史冷数据存入 HDD。 - -## 案例分享 - -- [如何快速优化几千万数据量的订单表 - 程序员济癫 - 2023](https://www.cnblogs.com/fulongyuanjushi/p/17910420.html) -- [海量数据冷热分离方案与实践 - 字节跳动技术团队 - 2022](https://mp.weixin.qq.com/s/ZKRkZP6rLHuTE1wvnqmAPQ) +The storage requirements for cold data mainly include large capacity, low cost, high diff --git a/docs/high-performance/deep-pagination-optimization.md b/docs/high-performance/deep-pagination-optimization.md index 0d39e627cef..4aaf6a61f15 100644 --- a/docs/high-performance/deep-pagination-optimization.md +++ b/docs/high-performance/deep-pagination-optimization.md @@ -1,140 +1,140 @@ --- -title: 深度分页介绍及优化建议 -category: 高性能 +title: Introduction to Deep Pagination and Optimization Suggestions +category: High Performance head: - - - meta - - name: keywords - content: 深度分页 - - - meta - - name: description - content: 查询偏移量过大的场景我们称为深度分页,这会导致查询性能较低。深度分页可以采用范围查询、子查询、INNER JOIN 延迟关联、覆盖索引等方法进行优化。 + - - meta + - name: keywords + content: Deep Pagination + - - meta + - name: description + content: The scenario where the query offset is excessively large is referred to as deep pagination, which leads to lower query performance. Deep pagination can be optimized using techniques such as range queries, subqueries, INNER JOIN lazy association, and covering indexes. --- -## 深度分页介绍 +## Introduction to Deep Pagination -查询偏移量过大的场景我们称为深度分页,这会导致查询性能较低,例如: +The scenario where the query offset is excessively large is referred to as deep pagination, which leads to lower query performance. For example: ```sql -# MySQL 在无法利用索引的情况下跳过1000000条记录后,再获取10条记录 +# MySQL skips 1,000,000 records without utilizing the index, then retrieves 10 records. SELECT * FROM t_order ORDER BY id LIMIT 1000000, 10 ``` -## 深度分页问题的原因 +## Reasons for Deep Pagination Issues -当查询偏移量过大时,MySQL 的查询优化器可能会选择全表扫描而不是利用索引来优化查询。这是因为扫描索引和跳过大量记录可能比直接全表扫描更耗费资源。 +When the query offset is too large, the MySQL query optimizer may choose a full table scan instead of using the index to optimize the query. This is because scanning the index and skipping a large number of records may be more resource-intensive than performing a direct full table scan. -![深度分页问题](https://oss.javaguide.cn/github/javaguide/mysql/deep-pagination-phenomenon.png) +![Deep Pagination Issues](https://oss.javaguide.cn/github/javaguide/mysql/deep-pagination-phenomenon.png) -不同机器上这个查询偏移量过大的临界点可能不同,取决于多个因素,包括硬件配置(如 CPU 性能、磁盘速度)、表的大小、索引的类型和统计信息等。 +The critical point for this excessively large query offset may vary between different machines, depending on multiple factors, including hardware configuration (such as CPU performance, disk speed), table size, index types, and statistical information. -![转全表扫描的临界点](https://oss.javaguide.cn/github/javaguide/mysql/deep-pagination-phenomenon-critical-point.png) +![Critical Point for Full Table Scan](https://oss.javaguide.cn/github/javaguide/mysql/deep-pagination-phenomenon-critical-point.png) -MySQL 的查询优化器采用基于成本的策略来选择最优的查询执行计划。它会根据 CPU 和 I/O 的成本来决定是否使用索引扫描或全表扫描。如果优化器认为全表扫描的成本更低,它就会放弃使用索引。不过,即使偏移量很大,如果查询中使用了覆盖索引(covering index),MySQL 仍然可能会使用索引,避免回表操作。 +MySQL’s query optimizer employs a cost-based strategy to select the optimal query execution plan. It determines whether to use index scanning or full table scanning based on the costs of CPU and I/O. If the optimizer decides that the cost of a full table scan is lower, it will forgo using the index. However, even with a large offset, if a covering index is used in the query, MySQL might still utilize the index, avoiding the need to access the data rows again. -## 深度分页优化建议 +## Optimization Suggestions for Deep Pagination -这里以 MySQL 数据库为例介绍一下如何优化深度分页。 +Taking the MySQL database as an example, here are some ways to optimize deep pagination. -### 范围查询 +### Range Query -当可以保证 ID 的连续性时,根据 ID 范围进行分页是比较好的解决方案: +When continuity of IDs can be ensured, using ID ranges for pagination is a good solution: ```sql -# 查询指定 ID 范围的数据 +# Query data within a specified ID range SELECT * FROM t_order WHERE id > 100000 AND id <= 100010 ORDER BY id -# 也可以通过记录上次查询结果的最后一条记录的ID进行下一页的查询: +# You can also query the next page using the ID of the last record from the previous query: SELECT * FROM t_order WHERE id > 100000 LIMIT 10 ``` -这种基于 ID 范围的深度分页优化方式存在很大限制: +This ID range-based deep pagination optimization method has significant limitations: -1. **ID 连续性要求高**: 实际项目中,数据库自增 ID 往往因为各种原因(例如删除数据、事务回滚等)导致 ID 不连续,难以保证连续性。 -2. **排序问题**: 如果查询需要按照其他字段(例如创建时间、更新时间等)排序,而不是按照 ID 排序,那么这种方法就不再适用。 -3. **并发场景**: 在高并发场景下,单纯依赖记录上次查询的最后一条记录的 ID 进行分页,容易出现数据重复或遗漏的问题。 +1. **High Requirement for ID Continuity**: In actual projects, database auto-increment IDs often become non-continuous for various reasons (such as data deletion, transaction rollbacks), making it difficult to ensure continuity. +1. **Sorting Issues**: If the query needs to be sorted by other fields (such as creation time, update time, etc.) instead of sorting by ID, then this method is no longer applicable. +1. **Concurrent Scenarios**: In high concurrency scenarios, relying solely on the ID of the last record from the previous query for pagination can easily lead to data duplication or omission issues. -### 子查询 +### Subquery -我们先查询出 limit 第一个参数对应的主键值,再根据这个主键值再去过滤并 limit,这样效率会更快一些。 +We can first retrieve the primary key values corresponding to the first parameter of limit, and then filter and limit based on this primary key value, which will be more efficient. -阿里巴巴《Java 开发手册》中也有对应的描述: +The Alibaba "Java Development Handbook" also describes this: -> 利用延迟关联或者子查询优化超多分页场景。 +> Use lazy association or subqueries to optimize excessive pagination scenarios. > > ![](https://oss.javaguide.cn/github/javaguide/mysql/alibaba-java-development-handbook-paging.png) ```sql -# 通过子查询来获取 id 的起始值,把 limit 1000000 的条件转移到子查询 +# Use a subquery to get the starting value of id, transferring the LIMIT 1000000 condition to the subquery SELECT * FROM t_order WHERE id >= (SELECT id FROM t_order where id > 1000000 limit 1) LIMIT 10; ``` -**工作原理**: +**How It Works**: -1. 子查询 `(SELECT id FROM t_order where id > 1000000 limit 1)` 会利用主键索引快速定位到第 1000001 条记录,并返回其 ID 值。 -2. 主查询 `SELECT * FROM t_order WHERE id >= ... LIMIT 10` 将子查询返回的起始 ID 作为过滤条件,使用 `id >=` 获取从该 ID 开始的后续 10 条记录。 +1. The subquery `(SELECT id FROM t_order where id > 1000000 limit 1)` quickly locates the 1,000,001st record using the primary key index and returns its ID value. +1. The main query `SELECT * FROM t_order WHERE id >= ... LIMIT 10` uses the starting ID returned from the subquery as a filter condition, using `id >=` to retrieve the subsequent 10 records starting from that ID. -不过,子查询的结果会产生一张新表,会影响性能,应该尽量避免大量使用子查询。并且,这种方法只适用于 ID 是正序的。在复杂分页场景,往往需要通过过滤条件,筛选到符合条件的 ID,此时的 ID 是离散且不连续的。 +However, the result of the subquery generates a temporary table, which can impact performance. Therefore, large-scale use of subqueries should be avoided. Moreover, this method is only suitable when IDs are in ascending order. In complex pagination scenarios, filtering conditions may be needed to identify matching IDs, resulting in discrete and non-continuous IDs. -当然,我们也可以利用子查询先去获取目标分页的 ID 集合,然后再根据 ID 集合获取内容,但这种写法非常繁琐,不如使用 INNER JOIN 延迟关联。 +Of course, we can also use subqueries to retrieve the target page's set of IDs and then fetch content based on this ID set, but this approach is very cumbersome compared to using INNER JOIN for lazy association. -### 延迟关联 +### Lazy Association -延迟关联与子查询的优化思路类似,都是通过将 `LIMIT` 操作转移到主键索引树上,减少回表次数。相比直接使用子查询,延迟关联通过 `INNER JOIN` 将子查询结果集成到主查询中,避免了子查询可能产生的临时表。在执行 `INNER JOIN` 时,MySQL 优化器能够利用索引进行高效的连接操作(如索引扫描或其他优化策略),因此在深度分页场景下,性能通常优于直接使用子查询。 +Lazy association is similar to the optimization idea of subqueries, where `LIMIT` operations are moved to the primary key index tree to reduce the number of back-table operations. Compared to directly using subqueries, lazy association integrates the subquery results into the main query through `INNER JOIN`, avoiding the potential creation of temporary tables by subqueries. When executing `INNER JOIN`, the MySQL optimizer can utilize indexes for efficient join operations (like index scanning or other optimization strategies), thus usually achieving better performance in deep pagination scenarios than directly using subqueries. ```sql --- 使用 INNER JOIN 进行延迟关联 +-- Use INNER JOIN for lazy association SELECT t1.* FROM t_order t1 INNER JOIN (SELECT id FROM t_order where id > 1000000 LIMIT 10) t2 ON t1.id = t2.id; ``` -**工作原理**: +**How It Works**: -1. 子查询 `(SELECT id FROM t_order where id > 1000000 LIMIT 10)` 利用主键索引快速定位目标分页的 10 条记录的 ID。 -2. 通过 `INNER JOIN` 将子查询结果与主表 `t_order` 关联,获取完整的记录数据。 +1. The subquery `(SELECT id FROM t_order where id > 1000000 LIMIT 10)` quickly locates the IDs of the 10 target records using the primary key index. +1. Using `INNER JOIN`, the subquery results are associated with the main table `t_order`, retrieving complete record data. -除了使用 INNER JOIN 之外,还可以使用逗号连接子查询。 +In addition to using INNER JOIN, you can also connect subqueries with commas. ```sql --- 使用逗号进行延迟关联 +-- Use a comma for lazy association SELECT t1.* FROM t_order t1, (SELECT id FROM t_order where id > 1000000 LIMIT 10) t2 WHERE t1.id = t2.id; ``` -**注意**: 虽然逗号连接子查询也能实现类似的效果,但为了代码可读性和可维护性,建议使用更规范的 `INNER JOIN` 语法。 +[!NOTE] +Although connecting subqueries with commas can achieve similar effects, it is recommended to use the more standardized `INNER JOIN` syntax for code readability and maintainability. +### Covering Index -### 覆盖索引 +A query method where all required fields are included in the index is known as a covering index. -索引中已经包含了所有需要获取的字段的查询方式称为覆盖索引。 +**Benefits of Covering Index**: -**覆盖索引的好处:** - -- **避免 InnoDB 表进行索引的二次查询,也就是回表操作:** InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询(回表),减少了 IO 操作,提升了查询效率。 -- **可以把随机 IO 变成顺序 IO 加快查询效率:** 由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。 +- **Avoids InnoDB tables from performing second index queries, which is a back-table operation**: InnoDB stores data in the order of the clustered index. For InnoDB, the secondary index stores the primary key information in its leaf nodes. If data is queried using the secondary index, after finding the corresponding key value, a secondary query via the primary key is still required to retrieve the actual data we need. However, in covering indexes, all data can be accessed from the secondary index's key values, avoiding the need for a secondary query (back-table access), thereby reducing I/O operations and improving query efficiency. +- **Transforms random I/O into sequential I/O, speeding up query efficiency**: Since covering indexes are stored in key value order, for I/O intensive range queries, it significantly reduces the I/O involved in randomly reading each row of data from disk, thus making disk random reads into index lookups in sequential order. ```sql -# 如果只需要查询 id, code, type 这三列,可建立 code 和 type 的覆盖索引 +# If only the id, code, and type columns are needed, a covering index can be established for code and type SELECT id, code, type FROM t_order ORDER BY code LIMIT 1000000, 10; ``` -**⚠️注意**: +**⚠️Note**: -- 当查询的结果集占表的总行数的很大一部分时,MySQL 查询优化器可能选择放弃使用索引,自动转换为全表扫描。 -- 虽然可以使用 `FORCE INDEX` 强制查询优化器走索引,但这种方式可能会导致查询优化器无法选择更优的执行计划,效果并不总是理想。 +- When the result set of a query accounts for a large proportion of the total rows in the table, MySQL’s query optimizer may opt to forgo using the index, automatically switching to a full table scan. +- While you can use `FORCE INDEX` to compel the query optimizer to use the index, this might lead to the optimizer being unable to select a better execution plan, and the outcome may not always be ideal. -## 总结 +## Conclusion -本文总结了几种常见的深度分页优化方案: +This article summarizes several common optimization schemes for deep pagination: -1. **范围查询**: 基于 ID 连续性进行分页,通过记录上一页最后一条记录的 ID 来获取下一页数据。适合 ID 连续且按 ID 查询的场景,但在 ID 不连续或需要按其他字段排序时存在局限。 -2. **子查询**: 先通过子查询获取分页的起始主键值,再根据主键进行筛选分页。利用主键索引提高效率,但子查询会生成临时表,复杂场景下性能不佳。 -3. **延迟关联 (INNER JOIN)**: 使用 `INNER JOIN` 将分页操作转移到主键索引上,减少回表次数。相比子查询,延迟关联的性能更优,适合大数据量的分页查询。 -4. **覆盖索引**: 通过索引直接获取所需字段,避免回表操作,减少 IO 开销,适合查询特定字段的场景。但当结果集较大时,MySQL 可能会选择全表扫描。 +1. **Range Query**: Pagination based on ID continuity, using the ID of the last record from the previous page to fetch data for the next page. Suitable for scenarios where IDs are continuous and queries are sorted by ID, but limited in cases where IDs are non-continuous or sorted by other fields. +1. **Subquery**: First, the starting primary key value for pagination is retrieved through a subquery, then filtered based on the primary key for pagination. It utilizes primary key indexing to improve efficiency, but subqueries create temporary tables, which may lead to poor performance in complex scenarios. +1. **Lazy Association (INNER JOIN)**: Utilizing `INNER JOIN` to shift pagination operations to the primary key index, reducing the incidence of back-table operations. Lazy association typically performs better than subqueries, making it suitable for pagination queries involving large data volumes. +1. **Covering Index**: Directly retrieving required fields through an index, avoiding back-table operations and minimizing I/O overhead, suitable for queries that target specific fields. However, when the result set is large, MySQL may choose a full table scan. -## 参考 +## References -- 聊聊如何解决 MySQL 深分页问题 - 捡田螺的小男孩: -- 数据库深分页介绍及优化方案 - 京东零售技术: -- MySQL 深分页优化 - 得物技术: +- Discussing how to solve MySQL deep pagination issues - A Boy Who Picks Snails: +- Introduction to deep pagination in databases and optimization schemes - JD Retail Technology: +- MySQL deep pagination optimization - Dewu Technology: diff --git a/docs/high-performance/load-balancing.md b/docs/high-performance/load-balancing.md index 619df980574..50b4db93623 100644 --- a/docs/high-performance/load-balancing.md +++ b/docs/high-performance/load-balancing.md @@ -1,208 +1,208 @@ --- -title: 负载均衡原理及算法详解 -category: 高性能 +title: Detailed Explanation of Load Balancing Principles and Algorithms +category: High Performance head: - - meta - name: keywords - content: 客户端负载均衡,服务负载均衡,Nginx,负载均衡算法,七层负载均衡,DNS解析 + content: Client-side Load Balancing, Server-side Load Balancing, Nginx, Load Balancing Algorithms, Layer 7 Load Balancing, DNS Resolution - - meta - name: description - content: 负载均衡指的是将用户请求分摊到不同的服务器上处理,以提高系统整体的并发处理能力。负载均衡可以简单分为服务端负载均衡和客户端负载均衡 这两种。服务端负载均衡涉及到的知识点更多,工作中遇到的也比较多,因为,我会花更多时间来介绍。 + content: Load balancing refers to distributing user requests across different servers to improve the overall concurrency and reliability of the system. Load balancing can be simply divided into two types: server-side load balancing and client-side load balancing. Server-side load balancing involves more knowledge points and is encountered more frequently in work, thus, I will spend more time introducing it. --- -## 什么是负载均衡? +## What is Load Balancing? -**负载均衡** 指的是将用户请求分摊到不同的服务器上处理,以提高系统整体的并发处理能力以及可靠性。负载均衡服务可以有由专门的软件或者硬件来完成,一般情况下,硬件的性能更好,软件的价格更便宜(后文会详细介绍到)。 +**Load balancing** refers to distributing user requests across different servers to enhance the overall concurrency capacity and reliability of a system. Load balancing services can be implemented by specialized software or hardware. Generally, hardware has better performance, while software is cheaper (which will be discussed in detail later). -下图是[《Java 面试指北》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247519384&idx=1&sn=bc7e71af75350b755f04ca4178395b1a&chksm=cea1c353f9d64a458f797696d4144b4d6e58639371a4612b8e4d106d83a66d2289e7b2cd7431&token=660789642&lang=zh_CN&scene=21#wechat_redirect) 「高并发篇」中的一篇文章的配图,从图中可以看出,系统的商品服务部署了多份在不同的服务器上,为了实现访问商品服务请求的分流,我们用到了负载均衡。 +The following image is from an article in the "High Concurrency" section of [“Java Interview Guide”](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247519384&idx=1&sn=bc7e71af75350b755f04ca4178395b1a&chksm=cea1c353f9d64a458f797696d4144b4d6e58639371a4612b8e4d106d83a66d2289e7b2cd7431&token=660789642&lang=zh_CN&scene=21#wechat_redirect). From the image, we can see that the system's product service is deployed across multiple servers, and to achieve request sharding for accessing the product service, we utilize load balancing. -![多服务实例-负载均衡](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/multi-service-load-balancing.drawio.png) +![Multiple Service Instances - Load Balancing](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/multi-service-load-balancing.drawio.png) -负载均衡是一种比较常用且实施起来较为简单的提高系统并发能力和可靠性的手段,不论是单体架构的系统还是微服务架构的系统几乎都会用到。 +Load balancing is a commonly used and relatively simple means of enhancing system concurrency and reliability. It is utilized in nearly all systems, whether they are monolithic architectures or microservice architectures. -## 负载均衡分为哪几种? +## What Types of Load Balancing Are There? -负载均衡可以简单分为 **服务端负载均衡** 和 **客户端负载均衡** 这两种。 +Load balancing can be simply divided into **server-side load balancing** and **client-side load balancing**. -服务端负载均衡涉及到的知识点更多,工作中遇到的也比较多,因此,我会花更多时间来介绍。 +Server-side load balancing involves more knowledge points and is encountered more frequently in work, hence I will spend more time introducing it. -### 服务端负载均衡 +### Server-side Load Balancing -**服务端负载均衡** 主要应用在 **系统外部请求** 和 **网关层** 之间,可以使用 **软件** 或者 **硬件** 实现。 +**Server-side load balancing** is mainly applied between **external system requests** and the **gateway layer** and can be implemented using **software** or **hardware**. -下图是我画的一个简单的基于 Nginx 的服务端负载均衡示意图: +The following diagram illustrates a simple server-side load balancing setup based on Nginx: -![基于 Nginx 的服务端负载均衡](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/server-load-balancing.png) +![Server-side Load Balancing Based on Nginx](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/server-load-balancing.png) -**硬件负载均衡** 通过专门的硬件设备(比如 **F5、A10、Array** )实现负载均衡功能。 +**Hardware load balancing** is achieved via specialized hardware devices (such as **F5, A10, Array**). -硬件负载均衡的优势是性能很强且稳定,缺点就是实在是太贵了。像基础款的 F5 最低也要 20 多万,绝大部分公司是根本负担不起的,业务量不大的话,真没必要非要去弄个硬件来做负载均衡,用软件负载均衡就足够了! +The advantage of hardware load balancing is its strong and stable performance; however, the downside is that it is quite expensive. For instance, a basic model of F5 starts from over 200,000 yuan, which most companies cannot afford. If the business volume is not high, it is unnecessary to invest in hardware for load balancing; software load balancing suffices! -在我们日常开发中,一般很难接触到硬件负载均衡,接触的比较多的还是 **软件负载均衡** 。软件负载均衡通过软件(比如 **LVS、Nginx、HAproxy** )实现负载均衡功能,性能虽然差一些,但价格便宜啊!像基础款的 Linux 服务器也就几千,性能好一点的 2~3 万的就很不错了。 +In our daily development, we generally have limited exposure to hardware load balancing, as we encounter **software load balancing** more frequently. Software load balancing uses software (such as **LVS, Nginx, HAProxy**) to implement load balancing functionality; while its performance is slightly lower, it is more cost-effective! For instance, a basic Linux server can be just a few thousand yuan, and a slightly better one might cost around 20,000 to 30,000 yuan. -根据 OSI 模型,服务端负载均衡还可以分为: +According to the OSI model, server-side load balancing can also be categorized into: -- 二层负载均衡 -- 三层负载均衡 -- 四层负载均衡 -- 七层负载均衡 +- Layer 2 Load Balancing +- Layer 3 Load Balancing +- Layer 4 Load Balancing +- Layer 7 Load Balancing -最常见的是四层和七层负载均衡,因此,本文也是重点介绍这两种负载均衡。 +The most common types are Layer 4 and Layer 7 load balancing, thus this article will primarily focus on these two types. -> Nginx 官网对四层负载和七层负载均衡均衡做了详细介绍,感兴趣的可以看看。 +> The Nginx official website provides a detailed introduction to Layer 4 and Layer 7 load balancing. If you're interested, you can check it out. > > - [What Is Layer 4 Load Balancing?](https://www.nginx.com/resources/glossary/layer-4-load-balancing/) > - [What Is Layer 7 Load Balancing?](https://www.nginx.com/resources/glossary/layer-7-load-balancing/) -![OSI 七层模型](https://oss.javaguide.cn/github/javaguide/cs-basics/network/osi-7-model.png) +![OSI Seven Layer Model](https://oss.javaguide.cn/github/javaguide/cs-basics/network/osi-7-model.png) -- **四层负载均衡** 工作在 OSI 模型第四层,也就是传输层,这一层的主要协议是 TCP/UDP,负载均衡器在这一层能够看到数据包里的源端口地址以及目的端口地址,会基于这些信息通过一定的负载均衡算法将数据包转发到后端真实服务器。也就是说,四层负载均衡的核心就是 IP+端口层面的负载均衡,不涉及具体的报文内容。 -- **七层负载均衡** 工作在 OSI 模型第七层,也就是应用层,这一层的主要协议是 HTTP 。这一层的负载均衡比四层负载均衡路由网络请求的方式更加复杂,它会读取报文的数据部分(比如说我们的 HTTP 部分的报文),然后根据读取到的数据内容(如 URL、Cookie)做出负载均衡决策。也就是说,七层负载均衡器的核心是报文内容(如 URL、Cookie)层面的负载均衡,执行第七层负载均衡的设备通常被称为 **反向代理服务器** 。 +- **Layer 4 load balancing** operates at the fourth layer of the OSI model, which is the transport layer. The main protocols at this layer are TCP/UDP. The load balancer at this layer can see the source and destination port addresses in the data packets and will forward the packets to the backend real servers based on this information using specific load balancing algorithms. In other words, the core of Layer 4 load balancing is IP + port level load balancing without involving the actual content of the messages. +- **Layer 7 load balancing** functions at the seventh layer of the OSI model, which is the application layer. The main protocol at this layer is HTTP. The load balancing at this layer is more complex than at Layer 4, as it reads the message data section (for instance, the HTTP portion of messages), and makes load balancing decisions based on the content read (such as URL, Cookie). In other words, the core of Layer 7 load balancers is load balancing at the message content level (such as URL, Cookie), and devices performing Layer 7 load balancing are usually referred to as **reverse proxy servers**. -七层负载均衡比四层负载均衡会消耗更多的性能,不过,也相对更加灵活,能够更加智能地路由网络请求,比如说你可以根据请求的内容进行优化如缓存、压缩、加密。 +Layer 7 load balancing consumes more performance than Layer 4 load balancing, but it is also relatively more flexible and capable of routing network requests more intelligently. For example, you can optimize based on request content, such as caching, compression, and encryption. -简单来说,**四层负载均衡性能很强,七层负载均衡功能更强!** 不过,对于绝大部分业务场景来说,四层负载均衡和七层负载均衡的性能差异基本可以忽略不计的。 +In short, **Layer 4 load balancing has strong performance, while Layer 7 load balancing has more powerful functionalities!** However, for most business scenarios, the performance difference between Layer 4 and Layer 7 load balancing can generally be negligible. -下面这段话摘自 Nginx 官网的 [What Is Layer 4 Load Balancing?](https://www.nginx.com/resources/glossary/layer-4-load-balancing/) 这篇文章。 +The following excerpt is from the Nginx official article [What Is Layer 4 Load Balancing?](https://www.nginx.com/resources/glossary/layer-4-load-balancing/). > 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. > > 第 4 层负载平衡是一种流行的流量处理体系结构方法,当时商用硬件没有现在这么强大,客户端和应用程序服务器之间的交互也不那么复杂。它比更复杂的负载平衡方法(如第 7 层)需要更少的计算量,但是 CPU 和内存现在足够快和便宜,在大多数情况下,第 4 层负载平衡的性能优势已经变得微不足道或无关紧要。 -在工作中,我们通常会使用 **Nginx** 来做七层负载均衡,LVS(Linux Virtual Server 虚拟服务器, Linux 内核的 4 层负载均衡)来做四层负载均衡。 +In practice, we usually use **Nginx** for Layer 7 load balancing and LVS (Linux Virtual Server, a Layer 4 load balancer in the Linux kernel) for Layer 4 load balancing. -关于 Nginx 的常见知识点总结,[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 中「技术面试题篇」中已经有对应的内容了,感兴趣的小伙伴可以去看看。 +For a summary of common knowledge about Nginx, corresponding content is already available in the "Technical Interview Questions" section of [“Java Interview Guide”](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html). Interested readers can take a look. ![](https://oss.javaguide.cn/github/javaguide/image-20220328105759300.png) -不过,LVS 这个绝大部分公司真用不上,像阿里、百度、腾讯、eBay 等大厂才会使用到,用的最多的还是 Nginx。 +However, most companies do not need to use LVS; it is typically used by large companies like Alibaba, Baidu, Tencent, eBay, etc. The most commonly used solution remains Nginx. -### 客户端负载均衡 +### Client-side Load Balancing -**客户端负载均衡** 主要应用于系统内部的不同的服务之间,可以使用现成的负载均衡组件来实现。 +**Client-side load balancing** is mainly applied between different services within the system and can be implemented using existing load balancing components. -在客户端负载均衡中,客户端会自己维护一份服务器的地址列表,发送请求之前,客户端会根据对应的负载均衡算法来选择具体某一台服务器处理请求。 +In client-side load balancing, the client maintains its own list of server addresses. Before sending a request, the client selects a specific server to handle the request based on the corresponding load balancing algorithm. -客户端负载均衡器和服务运行在同一个进程或者说 Java 程序里,不存在额外的网络开销。不过,客户端负载均衡的实现会受到编程语言的限制,比如说 Spring Cloud Load Balancer 就只能用于 Java 语言。 +The client load balancer and the services run within the same process or Java program, eliminating additional network overhead. However, the implementation of client-side load balancing may be constrained by the programming language; for instance, Spring Cloud Load Balancer can only be used with Java. -Java 领域主流的微服务框架 Dubbo、Spring Cloud 等都内置了开箱即用的客户端负载均衡实现。Dubbo 属于是默认自带了负载均衡功能,Spring Cloud 是通过组件的形式实现的负载均衡,属于可选项,比较常用的是 Spring Cloud Load Balancer(官方,推荐) 和 Ribbon(Netflix,已被弃用)。 +Mainstream microservice frameworks in the Java field, such as Dubbo and Spring Cloud, come with out-of-the-box client-side load balancing implementations. Dubbo has load balancing functionality built in by default, while Spring Cloud implements load balancing through components; it is optional, with Spring Cloud Load Balancer (official, recommended) and Ribbon (Netflix, deprecated) being the more commonly used options. -下图是我画的一个简单的基于 Spring Cloud Load Balancer(Ribbon 也类似) 的客户端负载均衡示意图: +The following diagram illustrates a simple client-side load balancing setup based on Spring Cloud Load Balancer (Ribbon is similar): ![](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/spring-cloud-lb-gateway.png) -## 负载均衡常见的算法有哪些? +## What Are the Common Algorithms for Load Balancing? -### 随机法 +### Random Method -**随机法** 是最简单粗暴的负载均衡算法。 +**Random method** is the simplest and most brute-force load balancing algorithm. -如果没有配置权重的话,所有的服务器被访问到的概率都是相同的。如果配置权重的话,权重越高的服务器被访问的概率就越大。 +If weights are not configured, all servers have the same probability of being accessed. If weights are configured, servers with higher weights are more likely to be accessed. -未加权重的随机算法适合于服务器性能相近的集群,其中每个服务器承载相同的负载。加权随机算法适合于服务器性能不等的集群,权重的存在可以使请求分配更加合理化。 +The unweighted random algorithm is suitable for clusters with similar server performance, where each server bears the same load. The weighted random algorithm is suitable for clusters with unequal server performance, where the presence of weights allows for a more rational request distribution. -不过,随机算法有一个比较明显的缺陷:部分机器在一段时间之内无法被随机到,毕竟是概率算法,就算是大家权重一样, 也可能会出现这种情况。 +However, the random algorithm has a notable flaw: some machines may not be randomly selected for a period of time due to the nature of probability algorithms. Even if everyone has the same weight, such situations can still occur. -于是,**轮询法** 来了! +Thus, the **Round Robin method** comes into play! -### 轮询法 +### Round Robin Method -轮询法是挨个轮询服务器处理,也可以设置权重。 +The Round Robin method involves polling each server for processing, and weights can also be set. -如果没有配置权重的话,每个请求按时间顺序逐一分配到不同的服务器处理。如果配置权重的话,权重越高的服务器被访问的次数就越多。 +If weights are not configured, each request is distributed sequentially to different servers based on time. If weights are configured, the server with the higher weight will be accessed more frequently. -未加权重的轮询算法适合于服务器性能相近的集群,其中每个服务器承载相同的负载。加权轮询算法适合于服务器性能不等的集群,权重的存在可以使请求分配更加合理化。 +The unweighted round robin algorithm is suitable for clusters with similar server performance, where each server bears the same load. The weighted round robin algorithm is suitable for clusters with unequal server performance, where the presence of weights allows for a more rational request distribution. -在加权轮询的基础上,还有进一步改进得到的负载均衡算法,比如平滑的加权轮训算法。 +On the basis of weighted round robin, further improved load balancing algorithms may arise, such as the smooth weighted round robin algorithm. -平滑的加权轮训算法最早是在 Nginx 中被实现,可以参考这个 commit:。如果你认真学习过 Dubbo 负载均衡策略的话,就会发现 Dubbo 的加权轮询就借鉴了该算法实现并进一步做了优化。 +The smooth weighted round robin algorithm was first implemented in Nginx; you can refer to this commit: . If you have studied Dubbo's load balancing strategies seriously, you will find that Dubbo's weighted round robin borrowed from this algorithm and further optimized it. -![Dubbo 加权轮询负载均衡算法](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/dubbo-round-robin-load-balance.png) +![Dubbo Weighted Round Robin Load Balancing Algorithm](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/dubbo-round-robin-load-balance.png) -### 两次随机法 +### Two-time Random Method -两次随机法在随机法的基础上多增加了一次随机,多选出一个服务器。随后再根据两台服务器的负载等情况,从其中选择出一个最合适的服务器。 +The two-time random method adds an additional round of randomness on the basis of the random method by selecting an extra server. Then, based on the load conditions of these two servers, one is chosen as the most suitable for processing. -两次随机法的好处是可以动态地调节后端节点的负载,使其更加均衡。如果只使用一次随机法,可能会导致某些服务器过载,而某些服务器空闲。 +The benefit of the two-time random method is that it can dynamically adjust the load of backend nodes to achieve better balance. Relying solely on the single random method may lead to certain servers being overloaded while others remain idle. -### 哈希法 +### Hash Method -将请求的参数信息通过哈希函数转换成一个哈希值,然后根据哈希值来决定请求被哪一台服务器处理。 +The request parameters are transformed into a hash value using a hash function, and the request is then directed to the server based on this hash value. -在服务器数量不变的情况下,相同参数的请求总是发到同一台服务器处理,比如同个 IP 的请求、同一个用户的请求。 +When the number of servers remains the same, requests with the same parameters will always be sent to the same server, such as requests from the same IP address or requests from the same user. -### 一致性 Hash 法 +### Consistent Hash Method -和哈希法类似,一致性 Hash 法也可以让相同参数的请求总是发到同一台服务器处理。不过,它解决了哈希法存在的一些问题。 +Similar to the hash method, the consistent hash method also allows requests with the same parameters to be consistently sent to the same server. However, it addresses some issues present in the hash method. -常规哈希法在服务器数量变化时,哈希值会重新落在不同的服务器上,这明显违背了使用哈希法的本意。而一致性哈希法的核心思想是将数据和节点都映射到一个哈希环上,然后根据哈希值的顺序来确定数据属于哪个节点。当服务器增加或删除时,只影响该服务器的哈希,而不会导致整个服务集群的哈希键值重新分布。 +Conventional hash methods reassign hash values to different servers when the number of servers changes, which clearly defeats the purpose of using the hash method. The core idea of consistent hashing is to map both data and nodes onto a hash ring and determine the respective nodes based on the order of hash values. When servers are added or removed, only the hash of the related servers is affected, without causing a complete redistribution of the hash keys across the service cluster. -### 最小连接法 +### Least Connections Method -当有新的请求出现时,遍历服务器节点列表并选取其中连接数最小的一台服务器来响应当前请求。相同连接的情况下,可以进行加权随机。 +When a new request arrives, the load balancer traverses the server node list and selects the one with the least number of connections to handle the current request. In the case of equal connections, weighted random methods may be employed. -最少连接数基于一个服务器连接数越多,负载就越高这一理想假设。然而, 实际情况是连接数并不能代表服务器的实际负载,有些连接耗费系统资源更多,有些连接不怎么耗费系统资源。 +The least connected method is based on the ideal assumption that a higher number of connections indicates a higher load on the server. In practice, however, the number of connections does not accurately represent the server's actual load, as some connections may consume more system resources than others. -### 最少活跃法 +### Least Active Method -最少活跃法和最小连接法类似,但要更科学一些。最少活跃法以活动连接数为标准,活动连接数可以理解为当前正在处理的请求数。活跃数越低,说明处理能力越强,这样就可以使处理能力强的服务器处理更多请求。相同活跃数的情况下,可以进行加权随机。 +The least active method is similar to the least connections method but is more scientific. The least active method uses the number of active connections as the standard; active connections could be understood as the current number of requests being processed. The lower the active connection count, the stronger the processing capacity, allowing more requests to be handled by stronger servers. In the case of the same number of active connections, weighted random methods can be employed. -### 最快响应时间法 +### Fastest Response Time Method -不同于最小连接法和最少活跃法,最快响应时间法以响应时间为标准来选择具体是哪一台服务器处理。客户端会维持每个服务器的响应时间,每次请求挑选响应时间最短的。相同响应时间的情况下,可以进行加权随机。 +Differing from the least connections and least active methods, the fastest response time method selects the appropriate server based on the response time. The client maintains the response times for each server and chooses the one with the shortest response time for each request. In the case of equal response times, weighted random methods may be employed. -这种算法可以使得请求被更快处理,但可能会造成流量过于集中于高性能服务器的问题。 +This algorithm ensures that requests are processed more quickly, but it may lead to an excessive concentration of traffic on high-performance servers. -## 七层负载均衡可以怎么做? +## How Can Layer 7 Load Balancing Be Achieved? -简单介绍两种项目中常用的七层负载均衡解决方案:DNS 解析和反向代理。 +Let’s briefly introduce two commonly used Layer 7 load balancing solutions: DNS resolution and reverse proxy. -除了我介绍的这两种解决方案之外,HTTP 重定向等手段也可以用来实现负载均衡,不过,相对来说,还是 DNS 解析和反向代理用的更多一些,也更推荐一些。 +Apart from the two solutions introduced, other methods such as HTTP redirection can also achieve load balancing; however, DNS resolution and reverse proxy are generally used more and are recommended. -### DNS 解析 +### DNS Resolution -DNS 解析是比较早期的七层负载均衡实现方式,非常简单。 +DNS resolution is an early implementation of Layer 7 load balancing; it is very simple. -DNS 解析实现负载均衡的原理是这样的:在 DNS 服务器中为同一个主机记录配置多个 IP 地址,这些 IP 地址对应不同的服务器。当用户请求域名的时候,DNS 服务器采用轮询算法返回 IP 地址,这样就实现了轮询版负载均衡。 +The principle of DNS resolution for achieving load balancing is as follows: Multiple IP addresses corresponding to different servers are configured in the DNS server for the same host record. When a user requests a domain name, the DNS server uses a round-robin algorithm to return an IP address, thereby achieving round-robin load balancing. ![](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/6997605302452f07e8b28d257d349bf0.png) -现在的 DNS 解析几乎都支持 IP 地址的权重配置,这样的话,在服务器性能不等的集群中请求分配会更加合理化。像我自己目前正在用的阿里云 DNS 就支持权重配置。 +Most modern DNS resolutions support the configuration of weight for IP addresses, making request distribution in clusters with unequal server performance more rational. For instance, Alibaba Cloud DNS, which I am currently using, supports weight configuration. ![](https://oss.javaguide.cn/github/javaguide/aliyun-dns-weight-setting.png) -### 反向代理 +### Reverse Proxy -客户端将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器,获取数据后再返回给客户端。对外暴露的是反向代理服务器地址,隐藏了真实服务器 IP 地址。反向代理“代理”的是目标服务器,这一个过程对于客户端而言是透明的。 +Clients send requests to the reverse proxy server, which then selects the target server. After obtaining the data, it is returned to the client. The external address exposed is the reverse proxy server's address, concealing the real server IP addresses. The reverse proxy "proxies" the target server, and this process is transparent to the client. -Nginx 就是最常用的反向代理服务器,它可以将接收到的客户端请求以一定的规则(负载均衡策略)均匀地分配到这个服务器集群中所有的服务器上。 +Nginx is the most commonly used reverse proxy server, capable of evenly distributing incoming client requests across all servers in the server cluster based on certain rules (load balancing strategies). -反向代理负载均衡同样属于七层负载均衡。 +Reverse proxy load balancing also falls under Layer 7 load balancing. ![](https://oss.javaguide.cn/github/javaguide/nginx-load-balance.png) -## 客户端负载均衡通常是怎么做的? +## How Is Client-side Load Balancing Usually Implemented? -我们上面也说了,客户端负载均衡可以使用现成的负载均衡组件来实现。 +As mentioned earlier, client-side load balancing can be implemented using existing load balancing components. -**Netflix Ribbon** 和 **Spring Cloud Load Balancer** 就是目前 Java 生态最流行的两个负载均衡组件。 +**Netflix Ribbon** and **Spring Cloud Load Balancer** are currently the two most popular load balancing components in the Java ecosystem. -Ribbon 是老牌负载均衡组件,由 Netflix 开发,功能比较全面,支持的负载均衡策略也比较多。 Spring Cloud Load Balancer 是 Spring 官方为了取代 Ribbon 而推出的,功能相对更简单一些,支持的负载均衡也少一些。 +Ribbon is an established load balancing component developed by Netflix, offering comprehensive features and supporting various load balancing strategies. Spring Cloud Load Balancer is launched by Spring as a replacement for Ribbon, featuring a relatively simpler function set and fewer supported load balancing strategies. -Ribbon 支持的 7 种负载均衡策略: +The 7 load balancing strategies supported by Ribbon include: -- `RandomRule`:随机策略。 -- `RoundRobinRule`(默认):轮询策略 -- `WeightedResponseTimeRule`:权重(根据响应时间决定权重)策略 -- `BestAvailableRule`:最小连接数策略 -- `RetryRule`:重试策略(按照轮询策略来获取服务,如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试来获取服务,如果超过指定时间依然没获取到服务实例则返回 null) -- `AvailabilityFilteringRule`:可用敏感性策略(先过滤掉非健康的服务实例,然后再选择连接数较小的服务实例) -- `ZoneAvoidanceRule`:区域敏感性策略(根据服务所在区域的性能和服务的可用性来选择服务实例) +- `RandomRule`: Random strategy. +- `RoundRobinRule` (default): Round-robin strategy +- `WeightedResponseTimeRule`: Weighted strategy (determining weights based on response time) +- `BestAvailableRule`: Least connections strategy +- `RetryRule`: Retry strategy (fetching services based on round-robin strategy; if the obtained service instance is null or already invalid, it keeps retrying within the specified time; if it still cannot fetch a service instance beyond the specified time, it returns null) +- `AvailabilityFilteringRule`: Availability sensitivity strategy (first filtering out unhealthy service instances, then choosing the one with a lesser number of connections) +- `ZoneAvoidanceRule`: Zone sensitivity strategy (selecting service instances based on performance and availability of the service's operating zone) -Spring Cloud Load Balancer 支持的 2 种负载均衡策略: +The 2 load balancing strategies supported by Spring Cloud Load Balancer include: -- `RandomLoadBalancer`:随机策略 -- `RoundRobinLoadBalancer`(默认):轮询策略 +- `RandomLoadBalancer`: Random strategy +- `RoundRobinLoadBalancer` (default): Round-robin strategy ```java public class CustomLoadBalancerConfiguration { @@ -218,16 +218,16 @@ public class CustomLoadBalancerConfiguration { } ``` -不过,Spring Cloud Load Balancer 支持的负载均衡策略其实不止这两种,`ServiceInstanceListSupplier` 的实现类同样可以让其支持类似于 Ribbon 的负载均衡策略。这个应该是后续慢慢完善引入的,不看官方文档还真发现不了,所以说阅读官方文档真的很重要! +However, the load balancing strategies supported by Spring Cloud Load Balancer extend beyond just these two; the implementation class of `ServiceInstanceListSupplier` can also be configured to support loading balancing strategies similar to those of Ribbon. This should be gradually improved and introduced in future updates. Without looking at the official documentation, one might not realize this, which underscores the importance of reading official documentation! -这里举两个官方的例子: +Here are two official examples: -- `ZonePreferenceServiceInstanceListSupplier`:实现基于区域的负载平衡 -- `HintBasedServiceInstanceListSupplier`:实现基于 hint 提示的负载均衡 +- `ZonePreferenceServiceInstanceListSupplier`: Implements region-based load balancing +- `HintBasedServiceInstanceListSupplier`: Implements hint-based load balancing ```java public class CustomLoadBalancerConfiguration { - // 使用基于区域的负载平衡方法 + // Using region-based loading balancing method @Bean public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( ConfigurableApplicationContext context) { @@ -240,28 +240,28 @@ public class CustomLoadBalancerConfiguration { } ``` -关于 Spring Cloud Load Balancer 更详细更新的介绍,推荐大家看看官方文档: ,一切以官方文档为主。 +For a more detailed and updated introduction to Spring Cloud Load Balancer, I recommend checking the official documentation: , everything is based on the official documentation. -轮询策略基本可以满足绝大部分项目的需求,我们的实际项目中如果没有特殊需求的话,通常使用的就是默认的轮询策略。并且,Ribbon 和 Spring Cloud Load Balancer 都支持自定义负载均衡策略。 +The round-robin strategy can meet the needs of the vast majority of projects. In our actual projects, if there are no special requirements, we usually just use the default round-robin strategy. Moreover, both Ribbon and Spring Cloud Load Balancer support custom load balancing strategies. -个人建议如非必需 Ribbon 某个特有的功能或者负载均衡策略的话,就优先选择 Spring 官方提供的 Spring Cloud Load Balancer。 +Personally, I suggest opting for Spring Cloud Load Balancer first unless you specifically need a unique feature or load balancing strategy from Ribbon. -最后再说说为什么我不太推荐使用 Ribbon 。 +Lastly, let's discuss why I do not recommend using Ribbon much. -Spring Cloud 2020.0.0 版本移除了 Netflix 除 Eureka 外的所有组件。Spring Cloud Hoxton.M2 是第一个支持 Spring Cloud Load Balancer 来替代 Netfix Ribbon 的版本。 +Spring Cloud 2020.0.0 version removed all Netflix components except for Eureka. Spring Cloud Hoxton.M2 was the first version to support Spring Cloud Load Balancer in place of Netflix Ribbon. -我们早期学习微服务,肯定接触过 Netflix 公司开源的 Feign、Ribbon、Zuul、Hystrix、Eureka 等知名的微服务系统构建所必须的组件,直到现在依然有非常非常多的公司在使用这些组件。不夸张地说,Netflix 公司引领了 Java 技术栈下的微服务发展。 +When we were learning about microservices, we certainly came across well-known components necessary for building microservices systems, such as Feign, Ribbon, Zuul, Hystrix, and Eureka from Netflix. Even now, many companies are still using these components. It is not an exaggeration to say that Netflix has led the development of microservices in the Java technology stack. ![](https://oss.javaguide.cn/github/javaguide/SpringCloudNetflix.png) -**那为什么 Spring Cloud 这么急着移除 Netflix 的组件呢?** 主要是因为在 2018 年的时候,Netflix 宣布其开源的核心组件 Hystrix、Ribbon、Zuul、Eureka 等进入维护状态,不再进行新特性开发,只修 BUG。于是,Spring 官方不得不考虑移除 Netflix 的组件。 +**So why is Spring Cloud so eager to remove Netflix components?** The main reason is that in 2018, Netflix announced that its core open-source components such as Hystrix, Ribbon, Zuul, and Eureka would enter maintenance mode, ceasing new feature developments and focusing solely on bug fixes. Therefore, Spring had to consider removing Netflix components. -**Spring Cloud Alibaba** 是一个不错的选择,尤其是对于国内的公司和个人开发者来说。 +**Spring Cloud Alibaba** is a good choice, especially for domestic companies and individual developers. -## 参考 +## References -- 干货 | eBay 的 4 层软件负载均衡实现: -- HTTP Load Balancing(Nginx 官方文档): -- 深入浅出负载均衡 - vivo 互联网技术: +- Practical Guide | eBay's Layer 4 Software Load Balancing Implementation: +- HTTP Load Balancing (Nginx Official Documentation): +- In-depth Understanding of Load Balancing - Vivo Internet Technology: diff --git a/docs/high-performance/message-queue/disruptor-questions.md b/docs/high-performance/message-queue/disruptor-questions.md index 1881f6c2c79..ca3185ec15f 100644 --- a/docs/high-performance/message-queue/disruptor-questions.md +++ b/docs/high-performance/message-queue/disruptor-questions.md @@ -1,140 +1,140 @@ --- -title: Disruptor常见问题总结 -category: 高性能 +title: Summary of Common Questions About Disruptor +category: High Performance tag: - - 消息队列 + - Message Queue --- -Disruptor 是一个相对冷门一些的知识点,不过,如果你的项目经历中用到了 Disruptor 的话,那面试中就很可能会被问到。 +Disruptor is a relatively niche topic, but if you've used Disruptor in your project experience, it's likely you'll be asked about it in an interview. -一位球友之前投稿的面经(社招)中就涉及一些 Disruptor 的问题,文章传送门:[圆梦!顺利拿到字节、淘宝、拼多多等大厂 offer!](https://mp.weixin.qq.com/s/C5QMjwEb6pzXACqZsyqC4A) 。 +A fellow player previously submitted an interview experience (for external recruitment) that included some Disruptor-related questions. You can find the article here: [Achieved my dream! Successfully received offers from major companies like ByteDance, Taobao, and Pinduoduo!](https://mp.weixin.qq.com/s/C5QMjwEb6pzXACqZsyqC4A). ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/disruptor-interview-questions.png) -这篇文章可以看作是对 Disruptor 做的一个简单总结,每个问题都不会扯太深入,主要针对面试或者速览 Disruptor。 +This article can be seen as a simple summary of Disruptor. Each question does not delve too deeply and is mainly focused on interviews or a quick overview of Disruptor. -## Disruptor 是什么? +## What is Disruptor? -Disruptor 是一个开源的高性能内存队列,诞生初衷是为了解决内存队列的性能和内存安全问题,由英国外汇交易公司 LMAX 开发。 +Disruptor is an open-source high-performance memory queue, originally developed to solve performance and memory safety issues associated with memory queues by LMAX, a foreign exchange trading company in the UK. -根据 Disruptor 官方介绍,基于 Disruptor 开发的系统 LMAX(新的零售金融交易平台),单线程就能支撑每秒 600 万订单。Martin Fowler 在 2011 年写的一篇文章 [The LMAX Architecture](https://martinfowler.com/articles/lmax.html) 中专门介绍过这个 LMAX 系统的架构,感兴趣的可以看看这篇文章。。 +According to the official introduction of Disruptor, the LMAX system developed based on Disruptor (a new retail financial trading platform) can support 6 million orders per second with a single thread. Martin Fowler wrote an article in 2011 [The LMAX Architecture](https://martinfowler.com/articles/lmax.html) specifically introducing the architecture of this LMAX system, which may be of interest. -LMAX 公司 2010 年在 QCon 演讲后,Disruptor 获得了业界关注,并获得了 2011 年的 Oracle 官方的 Duke's Choice Awards(Duke 选择大奖)。 +After LMAX’s presentation at QCon in 2010, Disruptor received attention from the industry and won the Oracle official Duke's Choice Award in 2011. ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/640.png) -> “Duke 选择大奖”旨在表彰过去一年里全球个人或公司开发的、最具影响力的 Java 技术应用,由甲骨文公司主办。含金量非常高! +> The "Duke's Choice Award" aims to recognize the most influential Java technology applications developed by individuals or companies worldwide in the past year, and is hosted by Oracle. It holds significant value! -我专门找到了 Oracle 官方当年颁布获得 Duke's Choice Awards 项目的那篇文章(文章地址: 。从文中可以看出,同年获得此大奖荣誉的还有大名鼎鼎的 Netty、JRebel 等项目。 +I specifically found the Oracle official article announcing the Duke's Choice Award winners that year (article link: ). From the text, it can be seen that other well-known projects such as Netty and JRebel also received this award that year. -![2011 年的 Oracle 官方的 Duke's Choice Awards](https://oss.javaguide.cn/javaguide/image-20211015152323898.png) +![2011 Oracle Official Duke's Choice Awards](https://oss.javaguide.cn/javaguide/image-20211015152323898.png) -Disruptor 提供的功能优点类似于 Kafka、RocketMQ 这类分布式队列,不过,其作为范围是 JVM(内存)。 +The capabilities provided by Disruptor are similar to those of distributed queues like Kafka and RocketMQ, but its scope is JVM (memory). -- Github 地址: -- 官方教程: +- GitHub link: +- Official tutorial: -关于如何在 Spring Boot 项目中使用 Disruptor,可以看这篇文章:[Spring Boot + Disruptor 实战入门](https://mp.weixin.qq.com/s/0iG5brK3bYF0BgSjX4jRiA) 。 +To learn how to use Disruptor in a Spring Boot project, you can refer to this article: [Spring Boot + Disruptor Practical Introduction](https://mp.weixin.qq.com/s/0iG5brK3bYF0BgSjX4jRiA). -## 为什么要用 Disruptor? +## Why Use Disruptor? -Disruptor 主要解决了 JDK 内置线程安全队列的性能和内存安全问题。 +Disruptor mainly addresses the performance and memory safety issues of the JDK's built-in thread-safe queues. -**JDK 中常见的线程安全的队列如下**: +**The commonly used thread-safe queues in the JDK are as follows**: -| 队列名字 | 锁 | 是否有界 | -| ----------------------- | ----------------------- | -------- | -| `ArrayBlockingQueue` | 加锁(`ReentrantLock`) | 有界 | -| `LinkedBlockingQueue` | 加锁(`ReentrantLock`) | 有界 | -| `LinkedTransferQueue` | 无锁(`CAS`) | 无界 | -| `ConcurrentLinkedQueue` | 无锁(`CAS`) | 无界 | +| Queue Name | Lock | Bounded/Unbounded | +| ----------------------- | ------------------------ | ----------------- | +| `ArrayBlockingQueue` | Locked (`ReentrantLock`) | Bounded | +| `LinkedBlockingQueue` | Locked (`ReentrantLock`) | Bounded | +| `LinkedTransferQueue` | Lock-Free (`CAS`) | Unbounded | +| `ConcurrentLinkedQueue` | Lock-Free (`CAS`) | Unbounded | -从上表中可以看出:这些队列要不就是加锁有界,要不就是无锁无界。而加锁的的队列势必会影响性能,无界的队列又存在内存溢出的风险。 +As can be seen from the table above, these queues are either bounded and locked or unbounded and lock-free. The locked queues inevitably affect performance, while unbounded queues face the risk of memory overflow. -因此,一般情况下,我们都是不建议使用 JDK 内置线程安全队列。 +Therefore, in general, we do not recommend using the JDK's built-in thread-safe queues. -**Disruptor 就不一样了!它在无锁的情况下还能保证队列有界,并且还是线程安全的。** +**Disruptor is different! It guarantees that the queue is bounded while being lock-free, and it is still thread-safe.** -下面这张图是 Disruptor 官网提供的 Disruptor 和 ArrayBlockingQueue 的延迟直方图对比。 +The following image is a comparison of latency histograms between Disruptor and ArrayBlockingQueue provided by the Disruptor official website. ![disruptor-latency-histogram](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/disruptor-latency-histogram.png) -Disruptor 真的很快,关于它为什么这么快这个问题,会在后文介绍到。 +Disruptor is indeed very fast, and we will introduce why it is so fast later. -此外,Disruptor 还提供了丰富的扩展功能比如支持批量操作、支持多种等待策略。 +Additionally, Disruptor also provides a rich set of extension capabilities, such as supporting batch operations and multiple wait strategies. -## Kafka 和 Disruptor 什么区别? +## What Are the Differences Between Kafka and Disruptor? -- **Kafka**:分布式消息队列,一般用在系统或者服务之间的消息传递,还可以被用作流式处理平台。 -- **Disruptor**:内存级别的消息队列,一般用在系统内部中线程间的消息传递。 +- **Kafka**: A distributed message queue typically used for message transmission between systems or services and can also be used as a stream processing platform. +- **Disruptor**: A memory-level message queue commonly used for message transmission between threads within a system. -## 哪些组件用到了 Disruptor? +## Which Components Use Disruptor? -用到 Disruptor 的开源项目还是挺多的,这里简单举几个例子: +There are quite a few open-source projects that utilize Disruptor. Here are a few examples: -- **Log4j2**:Log4j2 是一款常用的日志框架,它基于 Disruptor 来实现异步日志。 -- **SOFATracer**:SOFATracer 是蚂蚁金服开源的分布式应用链路追踪工具,它基于 Disruptor 来实现异步日志。 -- **Storm** : Storm 是一个开源的分布式实时计算系统,它基于 Disruptor 来实现工作进程内发生的消息传递(同一 Storm 节点上的线程间,无需网络通信)。 -- **HBase**:HBase 是一个分布式列存储数据库系统,它基于 Disruptor 来提高写并发性能。 +- **Log4j2**: Log4j2 is a commonly used logging framework that implements asynchronous logging based on Disruptor. +- **SOFATracer**: SOFATracer is an open-source distributed application tracing tool developed by Ant Financial, which uses Disruptor for asynchronous logging. +- **Storm**: Storm is an open-source distributed real-time computation system that uses Disruptor for message passing within the same worker process (between threads on the same Storm node, without the need for network communication). +- **HBase**: HBase is a distributed column storage database system that uses Disruptor to improve write concurrency performance. - …… -## Disruptor 核心概念有哪些? +## What Are the Core Concepts of Disruptor? -- **Event**:你可以把 Event 理解为存放在队列中等待消费的消息对象。 -- **EventFactory**:事件工厂用于生产事件,我们在初始化 `Disruptor` 类的时候需要用到。 -- **EventHandler**:Event 在对应的 Handler 中被处理,你可以将其理解为生产消费者模型中的消费者。 -- **EventProcessor**:EventProcessor 持有特定消费者(Consumer)的 Sequence,并提供用于调用事件处理实现的事件循环(Event Loop)。 -- **Disruptor**:事件的生产和消费需要用到 `Disruptor` 对象。 -- **RingBuffer**:RingBuffer(环形数组)用于保存事件。 -- **WaitStrategy**:等待策略。决定了没有事件可以消费的时候,事件消费者如何等待新事件的到来。 -- **Producer**:生产者,只是泛指调用 `Disruptor` 对象发布事件的用户代码,Disruptor 没有定义特定接口或类型。 -- **ProducerType**:指定是单个事件发布者模式还是多个事件发布者模式(发布者和生产者的意思类似,我个人比较喜欢用发布者)。 -- **Sequencer**:Sequencer 是 Disruptor 的真正核心。此接口有两个实现类 `SingleProducerSequencer`、`MultiProducerSequencer` ,它们定义在生产者和消费者之间快速、正确地传递数据的并发算法。 +- **Event**: You can think of an Event as a message object that is stored in the queue waiting to be consumed. +- **EventFactory**: The event factory is used to produce events that we need when initializing the `Disruptor` class. +- **EventHandler**: Events are processed in the corresponding Handler, which can be understood as the consumer in the producer-consumer model. +- **EventProcessor**: EventProcessor holds the sequence of a specific consumer and provides an event loop for invoking the implementation of event processing. +- **Disruptor**: The production and consumption of events require the `Disruptor` object. +- **RingBuffer**: RingBuffer (circular array) is used to store events. +- **WaitStrategy**: The wait strategy determines how the event consumers wait for new events to arrive when there are no events to consume. +- **Producer**: The producer refers generically to user code that calls the `Disruptor` object to publish events; Disruptor does not define a specific interface or type. +- **ProducerType**: Specifies whether it is a single event publisher mode or multiple event publisher mode (the term publisher is similar to producer, and I personally prefer to use publisher). +- **Sequencer**: The Sequencer is the true core of Disruptor. This interface has two implementation classes: `SingleProducerSequencer` and `MultiProducerSequencer`, which define a concurrent algorithm for fast and correct data transmission between producers and consumers. -下面这张图摘自 Disruptor 官网,展示了 LMAX 系统使用 Disruptor 的示例。 +The following image is extracted from the Disruptor official website and shows an example of using Disruptor in the LMAX system. -![LMAX 系统使用 Disruptor 的示例](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/disruptor-models.png) +![Example of Using Disruptor in LMAX System](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/disruptor-models.png) -## Disruptor 等待策略有哪些? +## What Are the Wait Strategies of Disruptor? -**等待策略(WaitStrategy)** 决定了没有事件可以消费的时候,事件消费者如何等待新事件的到来。 +**WaitStrategy** determines how event consumers wait for new events to arrive when there are no events to consume. -常见的等待策略有下面这些: +Common wait strategies include the following: -![Disruptor 等待策略](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/DisruptorWaitStrategy.png) +![Disruptor Wait Strategy](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/DisruptorWaitStrategy.png) -- `BlockingWaitStrategy`:基于 `ReentrantLock`+`Condition` 来实现等待和唤醒操作,实现代码非常简单,是 Disruptor 默认的等待策略。虽然最慢,但也是 CPU 使用率最低和最稳定的选项生产环境推荐使用; -- `BusySpinWaitStrategy`:性能很好,存在持续自旋的风险,使用不当会造成 CPU 负载 100%,慎用; -- `LiteBlockingWaitStrategy`:基于 `BlockingWaitStrategy` 的轻量级等待策略,在没有锁竞争的时候会省去唤醒操作,但是作者说测试不充分,因此不建议使用; -- `TimeoutBlockingWaitStrategy`:带超时的等待策略,超时后会执行业务指定的处理逻辑; -- `LiteTimeoutBlockingWaitStrategy`:基于`TimeoutBlockingWaitStrategy`的策略,当没有锁竞争的时候会省去唤醒操作; -- `SleepingWaitStrategy`:三段式策略,第一阶段自旋,第二阶段执行 Thread.yield 让出 CPU,第三阶段睡眠执行时间,反复的睡眠; -- `YieldingWaitStrategy`:二段式策略,第一阶段自旋,第二阶段执行 Thread.yield 交出 CPU; -- `PhasedBackoffWaitStrategy`:四段式策略,第一阶段自旋指定次数,第二阶段自旋指定时间,第三阶段执行 `Thread.yield` 交出 CPU,第四阶段调用成员变量的`waitFor`方法,该成员变量可以被设置为`BlockingWaitStrategy`、`LiteBlockingWaitStrategy`、`SleepingWaitStrategy`三个中的一个。 +- `BlockingWaitStrategy`: Implemented using `ReentrantLock` + `Condition` for waiting and waking operations. The implementation code is quite simple and is the default wait strategy of Disruptor. Although it is the slowest, it is also the option with the lowest and most stable CPU usage and is recommended for production environments. +- `BusySpinWaitStrategy`: Provides good performance but has the risk of continuous spinning, which can cause the CPU load to reach 100% if used improperly, so caution is advised. +- `LiteBlockingWaitStrategy`: A lightweight wait strategy based on `BlockingWaitStrategy` that skips the waking operation when there is no lock contention, but the author mentions that testing is insufficient and therefore it's not recommended for use. +- `TimeoutBlockingWaitStrategy`: A wait strategy with a timeout that executes specified processing logic after the timeout. +- `LiteTimeoutBlockingWaitStrategy`: A strategy based on `TimeoutBlockingWaitStrategy` that skips wakening operations when there is no lock contention. +- `SleepingWaitStrategy`: A three-phase strategy — in the first phase, it spins; in the second phase, it executes Thread.yield to give up the CPU; in the third phase, it sleeps for a specific time, repeating the sleep. +- `YieldingWaitStrategy`: A two-phase strategy — in the first phase, it spins; in the second phase, it executes Thread.yield to yield the CPU. +- `PhasedBackoffWaitStrategy`: A four-phase strategy — in the first phase, it spins a specified number of times; in the second phase, it spins for a specified duration; in the third phase, it executes `Thread.yield` to yield the CPU; in the fourth phase, it calls the member variable's `waitFor` method, which can be set to one of `BlockingWaitStrategy`, `LiteBlockingWaitStrategy`, or `SleepingWaitStrategy`. -## Disruptor 为什么这么快? +## Why is Disruptor So Fast? -- **RingBuffer(环形数组)** : Disruptor 内部的 RingBuffer 是通过数组实现的。由于这个数组中的所有元素在初始化时一次性全部创建,因此这些元素的内存地址一般来说是连续的。这样做的好处是,当生产者不断往 RingBuffer 中插入新的事件对象时,这些事件对象的内存地址就能够保持连续,从而利用 CPU 缓存的局部性原理,将相邻的事件对象一起加载到缓存中,提高程序的性能。这类似于 MySQL 的预读机制,将连续的几个页预读到内存里。除此之外,RingBuffer 基于数组还支持批量操作(一次处理多个元素)、还可以避免频繁的内存分配和垃圾回收(RingBuffer 是一个固定大小的数组,当向数组中添加新元素时,如果数组已满,则新元素将覆盖掉最旧的元素)。 -- **避免了伪共享问题**:CPU 缓存内部是按照 Cache Line(缓存行)管理的,一般的 Cache Line 大小在 64 字节左右。Disruptor 为了确保目标字段独占一个 Cache Line,会在目标字段前后增加字节填充(前 56 个字节和后 56 个字节),这样可以避免 Cache Line 的伪共享(False Sharing)问题。同时,为了让 RingBuffer 存放数据的数组独占缓存行,数组的设计为 无效填充(128 字节)+ 有效数据。 -- **无锁设计**:Disruptor 采用无锁设计,避免了传统锁机制带来的竞争和延迟。Disruptor 的无锁实现起来比较复杂,主要是基于 CAS、内存屏障(Memory Barrier)、RingBuffer 等技术实现的。 +- **RingBuffer (Circular Array)**: The internal RingBuffer of Disruptor is implemented via an array. Since all elements of this array are created at once during initialization, these element memory addresses are generally continuous. The benefit of this is that as the producer continuously inserts new event objects into the RingBuffer, the memory addresses of these event objects can remain contiguous, thus utilizing the locality principle of CPU caches and loading adjacent event objects into the cache together to improve program performance. This is similar to MySQL's pre-read mechanism, which pre-loads several continuous pages into memory. Additionally, the RingBuffer, based on the array, supports batch operations (processing multiple elements at once) and avoids frequent memory allocation and garbage collection (the RingBuffer is a fixed-size array, so when a new element is added to the array, if the array is full, the new element will overwrite the oldest element). +- **Avoided False Sharing Issues**: CPU caches are managed according to Cache Lines, with a typical Cache Line size around 64 bytes. Disruptor ensures that the target field occupies a single Cache Line by adding byte padding (56 bytes before and after the target field), thereby avoiding the false sharing problem. Additionally, to ensure that the array storing the data in the RingBuffer occupies a cache line, the design of the array includes invalid padding (128 bytes) plus valid data. +- **Lock-Free Design**: Disruptor employs a lock-free design, eliminating the competition and delays associated with traditional locking mechanisms. The lock-free implementation of Disruptor is relatively complex and is primarily based on technologies such as CAS, memory barriers, and RingBuffer. -综上所述,Disruptor 之所以能够如此快,是基于一系列优化策略的综合作用,既充分利用了现代 CPU 缓存结构的特点,又避免了常见的并发问题和性能瓶颈。 +In summary, the rapid performance of Disruptor stems from a comprehensive series of optimization strategies, effectively utilizing the characteristics of modern CPU cache structures while avoiding common concurrency issues and performance bottlenecks. -关于 Disruptor 高性能队列原理的详细介绍,可以查看这篇文章:[Disruptor 高性能队列原理浅析](https://qin.news/disruptor/) (参考了美团技术团队的[高性能队列——Disruptor](https://tech.meituan.com/2016/11/18/disruptor.html)这篇文章)。 +For a detailed introduction to the principles of Disruptor's high-performance queue, refer to this article: [Analysis of Disruptor High-Performance Queue Principles](https://qin.news/disruptor/) (which references the Meituan tech team's article on [High-Performance Queue - Disruptor](https://tech.meituan.com/2016/11/18/disruptor.html)). -🌈 这里额外补充一点:**数组中对象元素地址连续为什么可以提高性能?** +🌈 As an additional note: **Why does having contiguous memory addresses for object elements in an array improve performance?** -CPU 缓存是通过将最近使用的数据存储在高速缓存中来实现更快的读取速度,并使用预取机制提前加载相邻内存的数据以利用局部性原理。 +CPU caches enable faster reading speeds by storing recently used data in high-speed caches and using prefetching mechanisms to load adjacent memory data ahead of time, leveraging the principle of locality. -在计算机系统中,CPU 主要访问高速缓存和内存。高速缓存是一种速度非常快、容量相对较小的内存,通常被分为多级缓存,其中 L1、L2、L3 分别表示一级缓存、二级缓存、三级缓存。越靠近 CPU 的缓存,速度越快,容量也越小。相比之下,内存容量相对较大,但速度较慢。 +In computer systems, the CPU primarily accesses high-speed caches and memory. Caches are very fast and relatively small in capacity, usually organized into multiple levels, denoted as L1, L2, and L3 (representing levels of cache). The closer the cache is to the CPU, the faster and smaller its capacity tends to be. In contrast, memory has a relatively large capacity but slower speeds. -![CPU 缓存模型示意图](https://oss.javaguide.cn/github/javaguide/java/concurrent/cpu-cache.png) +![CPU Cache Model Diagram](https://oss.javaguide.cn/github/javaguide/java/concurrent/cpu-cache.png) -为了加速数据的读取过程,CPU 会先将数据从内存中加载到高速缓存中,如果下一次需要访问相同的数据,就可以直接从高速缓存中读取,而不需要再次访问内存。这就是所谓的 **缓存命中** 。另外,为了利用 **局部性原理** ,CPU 还会根据之前访问的内存地址预取相邻的内存数据,因为在程序中,连续的内存地址通常会被频繁访问到,这样做可以提高数据的缓存命中率,进而提高程序的性能。 +To accelerate data reading, the CPU first loads data from memory into the cache; if the same data needs to be accessed again, it can be read directly from the cache without needing to access memory again. This is known as a **cache hit**. Additionally, to utilize **locality**, the CPU will prefetch adjacent memory data based on previously accessed memory addresses, as consecutive memory addresses are usually accessed frequently in programs; this increases the cache hit rate and, consequently, improves program performance. -## 参考 +## References -- Disruptor 高性能之道-等待策略:< 高性能之道-等待策略/> -- 《Java 并发编程实战》- 40 | 案例分析(三):高性能队列 Disruptor: +- Disruptor Performance Strategy - Wait Strategies: \< Performance Strategy - Wait Strategies/> +- "Java Concurrency in Practice" - 40 | Case Analysis (3): High-Performance Queue Disruptor: diff --git a/docs/high-performance/message-queue/kafka-questions-01.md b/docs/high-performance/message-queue/kafka-questions-01.md index 070858cc1f8..84883a97136 100644 --- a/docs/high-performance/message-queue/kafka-questions-01.md +++ b/docs/high-performance/message-queue/kafka-questions-01.md @@ -1,250 +1,249 @@ --- -title: Kafka常见问题总结 -category: 高性能 +title: Summary of Common Kafka Issues +category: High Performance tag: - - 消息队列 + - Message Queue --- -## Kafka 基础 +## Kafka Basics -### Kafka 是什么?主要应用场景有哪些? +### What is Kafka? What are its main application scenarios? -Kafka 是一个分布式流式处理平台。这到底是什么意思呢? +Kafka is a distributed streaming processing platform. What does that mean exactly? -流平台具有三个关键功能: +A streaming platform has three key functions: -1. **消息队列**:发布和订阅消息流,这个功能类似于消息队列,这也是 Kafka 也被归类为消息队列的原因。 -2. **容错的持久方式存储记录消息流**:Kafka 会把消息持久化到磁盘,有效避免了消息丢失的风险。 -3. **流式处理平台:** 在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。 +1. **Message Queue**: It publishes and subscribes to message streams, which is similar to a message queue, and this is also the reason Kafka is classified as a message queue. +1. **Fault-tolerant Persistent Storage for Recorded Message Streams**: Kafka persists messages to disk, effectively avoiding the risk of message loss. +1. **Streaming Processing Platform**: It processes messages at the time of publishing, and Kafka provides a complete library for streaming processing. -Kafka 主要有两大应用场景: +Kafka primarily has two major application scenarios: -1. **消息队列**:建立实时流数据管道,以可靠地在系统或应用程序之间获取数据。 -2. **数据处理:** 构建实时的流数据处理程序来转换或处理数据流。 +1. **Message Queue**: It establishes real-time streaming data pipelines to reliably acquire data between systems or applications. +1. **Data Processing**: It builds real-time streaming data processing programs to transform or process data streams. -### 和其他消息队列相比,Kafka 的优势在哪里? +### What are the advantages of Kafka compared to other message queues? -我们现在经常提到 Kafka 的时候就已经默认它是一个非常优秀的消息队列了,我们也会经常拿它跟 RocketMQ、RabbitMQ 对比。我觉得 Kafka 相比其他消息队列主要的优势如下: +When we talk about Kafka now, we already assume it is a very excellent message queue. We often compare it with RocketMQ and RabbitMQ. I think Kafka's main advantages over other message queues are as follows: -1. **极致的性能**:基于 Scala 和 Java 语言开发,设计中大量使用了批量处理和异步的思想,最高可以每秒处理千万级别的消息。 -2. **生态系统兼容性无可匹敌**:Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域。 +1. **Extreme Performance**: Developed in Scala and Java, heavily utilizing batch processing and asynchronous principles in its design, it can process tens of millions of messages per second at its peak. +1. **Unmatched Ecosystem Compatibility**: The compatibility of Kafka with surrounding ecosystems is the best without rival, especially in the fields of big data and stream computing. -实际上在早期的时候 Kafka 并不是一个合格的消息队列,早期的 Kafka 在消息队列领域就像是一个衣衫褴褛的孩子一样,功能不完备并且有一些小问题比如丢失消息、不保证消息可靠性等等。当然,这也和 LinkedIn 最早开发 Kafka 用于处理海量的日志有很大关系,哈哈哈,人家本来最开始就不是为了作为消息队列滴,谁知道后面误打误撞在消息队列领域占据了一席之地。 +In fact, in the early days, Kafka was not a qualified message queue; early Kafka was like a ragged child in the message queue domain, with incomplete functions and some minor issues such as message loss and lack of message reliability guarantees. Of course, this also relates to LinkedIn's initial development of Kafka for processing massive logs. Haha, it wasn't intended to serve as a message queue initially; who knew it would inadvertently occupy a place in the message queue domain later. -随着后续的发展,这些短板都被 Kafka 逐步修复完善。所以,**Kafka 作为消息队列不可靠这个说法已经过时!** +With subsequent development, these shortcomings were gradually fixed and improved in Kafka. Thus, the statement that **Kafka is unreliable as a message queue is outdated!** -### 队列模型了解吗?Kafka 的消息模型知道吗? +### Do you understand the queue model? Do you know Kafka's message model? -> 题外话:早期的 JMS 和 AMQP 属于消息服务领域权威组织所做的相关的标准,我在 [JavaGuide](https://github.com/Snailclimb/JavaGuide)的 [《消息队列其实很简单》](https://github.com/Snailclimb/JavaGuide#%E6%95%B0%E6%8D%AE%E9%80%9A%E4%BF%A1%E4%B8%AD%E9%97%B4%E4%BB%B6)这篇文章中介绍过。但是,这些标准的进化跟不上消息队列的演进速度,这些标准实际上已经属于废弃状态。所以,可能存在的情况是:不同的消息队列都有自己的一套消息模型。 +> Sidebar: Early JMS and AMQP are standards developed by authoritative organizations in the message service field. I introduced this in my article [“Message Queue is Actually Simple”](https://github.com/Snailclimb/JavaGuide#%E6%95%B0%E6%8D%AE%E9%80%9A%E4%BF%A1%E4%B8%AD%E9%97%B4%E4%BB%B6) on [JavaGuide](https://github.com/Snailclimb/JavaGuide). However, these standards have not kept up with the evolution speed of message queues, and they are now effectively obsolete. Therefore, it's possible that different message queues have their own unique message models. -#### 队列模型:早期的消息模型 +#### Queue Model: Early Message Model -![队列模型](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/%E9%98%9F%E5%88%97%E6%A8%A1%E5%9E%8B23.png) +![Queue Model](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/%E9%98%9F%E5%88%97%E6%A8%A1%E5%9E%8B23.png) -**使用队列(Queue)作为消息通信载体,满足生产者与消费者模式,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。** 比如:我们生产者发送 100 条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。) +**Using a Queue as the medium for message communication satisfies the producer-consumer model; each message can only be used by one consumer, and unconsumed messages are retained in the queue until consumed or timed out.** For example: If we have a producer sending 100 messages, typically two consumers will consume in order, each taking half (one for you and one for me). -**队列模型存在的问题:** +**Issues with the Queue Model:** -假如我们存在这样一种情况:我们需要将生产者产生的消息分发给多个消费者,并且每个消费者都能接收到完整的消息内容。 +Suppose we have a situation where we need to distribute messages generated by producers to multiple consumers, and each consumer should receive the complete message content. -这种情况,队列模型就不好解决了。很多比较杠精的人就说:我们可以为每个消费者创建一个单独的队列,让生产者发送多份。这是一种非常愚蠢的做法,浪费资源不说,还违背了使用消息队列的目的。 +In this case, the queue model is not well-suited to solve the issue. Many nitpickers might say: we can create a separate queue for each consumer and let the producer send multiple copies. This is a very foolish approach; not only is it a waste of resources, but it also defeats the purpose of using a message queue. -#### 发布-订阅模型:Kafka 消息模型 +#### Publish-Subscribe Model: Kafka's Message Model -发布-订阅模型主要是为了解决队列模型存在的问题。 +The publish-subscribe model mainly addresses the issues present in the queue model. -![发布订阅模型](https://oss.javaguide.cn/java-guide-blog/%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85%E6%A8%A1%E5%9E%8B.png) +![Publish-Subscribe Model](https://oss.javaguide.cn/java-guide-blog/%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85%E6%A8%A1%E5%9E%8B.png) -发布订阅模型(Pub-Sub) 使用**主题(Topic)** 作为消息通信载体,类似于**广播模式**;发布者发布一条消息,该消息通过主题传递给所有的订阅者,**在一条消息广播之后才订阅的用户则是收不到该条消息的**。 +The publish-subscribe model (Pub-Sub) uses **topics** as the medium for message communication, similar to a **broadcast mode**; a publisher publishes a message, and that message is transmitted to all subscribers via the topic. **Users who subscribe after the message is broadcast will not receive the message.** -**在发布 - 订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样的了。所以说,发布 - 订阅模型在功能层面上是可以兼容队列模型的。** +**In the publish-subscribe model, if there is only one subscriber, it is essentially the same as the queue model. Thus, the publish-subscribe model can functionally accommodate the queue model.** -**Kafka 采用的就是发布 - 订阅模型。** +**Kafka adopts the publish-subscribe model.** -> **RocketMQ 的消息模型和 Kafka 基本是完全一样的。唯一的区别是 Kafka 中没有队列这个概念,与之对应的是 Partition(分区)。** +> **RocketMQ's message model is fundamentally the same as Kafka's. The only difference is that Kafka does not have the concept of a queue, corresponding instead to Partition.** -## Kafka 核心概念 +## Core Concepts of Kafka -### 什么是 Producer、Consumer、Broker、Topic、Partition? +### What are Producer, Consumer, Broker, Topic, and Partition? -Kafka 将生产者发布的消息发送到 **Topic(主题)** 中,需要这些消息的消费者可以订阅这些 **Topic(主题)**,如下图所示: +Kafka sends messages produced by producers to **Topics**, and consumers that need these messages can subscribe to these **Topics**, as shown in the diagram below: ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue20210507200944439.png) -上面这张图也为我们引出了,Kafka 比较重要的几个概念: +The diagram above introduces several important concepts in Kafka: -1. **Producer(生产者)** : 产生消息的一方。 -2. **Consumer(消费者)** : 消费消息的一方。 -3. **Broker(代理)** : 可以看作是一个独立的 Kafka 实例。多个 Kafka Broker 组成一个 Kafka Cluster。 +1. **Producer**: The party generating messages. +1. **Consumer**: The party consuming messages. +1. **Broker**: Can be viewed as an independent Kafka instance. Multiple Kafka Brokers form a Kafka Cluster. -同时,你一定也注意到每个 Broker 中又包含了 Topic 以及 Partition 这两个重要的概念: +At the same time, you should also notice that each Broker contains the important concepts of Topic and Partition: -- **Topic(主题)** : Producer 将消息发送到特定的主题,Consumer 通过订阅特定的 Topic(主题) 来消费消息。 -- **Partition(分区)** : Partition 属于 Topic 的一部分。一个 Topic 可以有多个 Partition ,并且同一 Topic 下的 Partition 可以分布在不同的 Broker 上,这也就表明一个 Topic 可以横跨多个 Broker 。这正如我上面所画的图一样。 +- **Topic**: Producers send messages to specific topics; Consumers consume messages by subscribing to specific Topics. +- **Partition**: A Partition is part of a Topic. A Topic can have multiple Partitions, and Partitions under the same Topic can be distributed across different Brokers, indicating that a Topic can span multiple Brokers, just like the diagram above shows. -> 划重点:**Kafka 中的 Partition(分区) 实际上可以对应成为消息队列中的队列。这样是不是更好理解一点?** +> Key Point: **Partitions in Kafka can actually correspond to queues in message queues. This should make it easier to understand, right?** -### Kafka 的多副本机制了解吗?带来了什么好处? +### Do you understand Kafka's replica mechanism? What benefits does it bring? -还有一点我觉得比较重要的是 Kafka 为分区(Partition)引入了多副本(Replica)机制。分区(Partition)中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。 +Another important aspect is Kafka's introduction of a replica mechanism for partitions. Within a partition, there are multiple replicas, among which one is designated as the leader, while the others are called followers. Messages we send are sent to the leader replica, and only then can the follower replicas pull messages from the leader to sync. -> 生产者和消费者只与 leader 副本交互。你可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。当 leader 副本发生故障时会从 follower 中选举出一个 leader,但是 follower 中如果有和 leader 同步程度达不到要求的参加不了 leader 的竞选。 +> Producers and consumers only interact with the leader replica. You can think of the other replicas as mere copies of the leader. Their existence is only to ensure the security of message storage. When a leader replica fails, one follower is elected as the new leader, but followers that do not meet the sync criteria with the leader will not participate in the election. -**Kafka 的多分区(Partition)以及多副本(Replica)机制有什么好处呢?** +**What are the benefits of Kafka's partition and replica mechanism?** -1. Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力(负载均衡)。 -2. Partition 可以指定对应的 Replica 数, 这也极大地提高了消息存储的安全性, 提高了容灾能力,不过也相应的增加了所需要的存储空间。 +1. Kafka improves concurrency capability (load balancing) by specifying multiple Partitions for a particular Topic, each distributed across different Brokers. +1. Partitions can specify corresponding replica numbers, which greatly enhances message storage security and disaster recovery capabilities, although it also increases the required storage space. -## Zookeeper 和 Kafka +## Zookeeper and Kafka -### Zookeeper 在 Kafka 中的作用是什么? +### What is Zookeeper's role in Kafka? -> 要想搞懂 zookeeper 在 Kafka 中的作用 一定要自己搭建一个 Kafka 环境然后自己进 zookeeper 去看一下有哪些文件夹和 Kafka 有关,每个节点又保存了什么信息。 一定不要光看不实践,这样学来的也终会忘记!这部分内容参考和借鉴了这篇文章: 。 +> To understand Zookeeper's role in Kafka, you must set up a Kafka environment and then explore Zookeeper to see what folders are related to Kafka and what information each node saves. Don't just read without practice, as you'll eventually forget what you've learned! This part of the content references and draws from this article: . -下图就是我的本地 Zookeeper ,它成功和我本地的 Kafka 关联上(以下文件夹结构借助 idea 插件 Zookeeper tool 实现)。 +The image below shows my local Zookeeper successfully linked to my local Kafka (the folder structure below was implemented using the idea plugin Zookeeper tool). -ZooKeeper 主要为 Kafka 提供元数据的管理的功能。 +ZooKeeper mainly provides metadata management functions for Kafka. -从图中我们可以看出,Zookeeper 主要为 Kafka 做了下面这些事情: +From the image, we can see that Zookeeper primarily does the following for Kafka: -1. **Broker 注册**:在 Zookeeper 上会有一个专门**用来进行 Broker 服务器列表记录**的节点。每个 Broker 在启动时,都会到 Zookeeper 上进行注册,即到 `/brokers/ids` 下创建属于自己的节点。每个 Broker 就会将自己的 IP 地址和端口等信息记录到该节点中去 -2. **Topic 注册**:在 Kafka 中,同一个**Topic 的消息会被分成多个分区**并将其分布在多个 Broker 上,**这些分区信息及与 Broker 的对应关系**也都是由 Zookeeper 在维护。比如我创建了一个名字为 my-topic 的主题并且它有两个分区,对应到 zookeeper 中会创建这些文件夹:`/brokers/topics/my-topic/Partitions/0`、`/brokers/topics/my-topic/Partitions/1` -3. **负载均衡**:上面也说过了 Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力。 对于同一个 Topic 的不同 Partition,Kafka 会尽力将这些 Partition 分布到不同的 Broker 服务器上。当生产者产生消息后也会尽量投递到不同 Broker 的 Partition 里面。当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。 -4. …… +1. **Broker Registration**: There is a dedicated node in Zookeeper for recording the Broker server list. Each Broker will register itself upon starting, creating its own node under `/brokers/ids`. Each Broker will record its IP address, port, and other information in that node. +1. **Topic Registration**: In Kafka, messages from the same Topic are divided into multiple partitions and distributed across multiple Brokers. **The information about these partitions and their corresponding Brokers is also maintained by Zookeeper.** For example, if I create a topic named my-topic with two partitions, corresponding folders would be created in Zookeeper: `/brokers/topics/my-topic/Partitions/0`, `/brokers/topics/my-topic/Partitions/1`. +1. **Load Balancing**: As mentioned earlier, Kafka can provide better concurrency capability by specifying multiple Partitions for a particular Topic, with each Partition distributed across different Brokers. For different Partitions of the same Topic, Kafka will strive to distribute these Partitions across different Broker servers. When a producer generates a message, it will also attempt to deliver it to different Brokers’ Partitions. When Consumers consume, Zookeeper can achieve dynamic load balancing according to the current number of Partitions and Consumers. +1. …… -### 使用 Kafka 能否不引入 Zookeeper? +### Can Kafka be used without introducing Zookeeper? -在 Kafka 2.8 之前,Kafka 最被大家诟病的就是其重度依赖于 Zookeeper。在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构,让你可以以一种轻量级的方式来使用 Kafka。 +Before Kafka 2.8, Kafka was heavily criticized for its heavy dependency on Zookeeper. After Kafka 2.8, KRaft mode based on the Raft protocol was introduced, eliminating the need for Zookeeper, significantly simplifying Kafka's architecture and allowing you to use Kafka in a lightweight manner. -不过,要提示一下:**如果要使用 KRaft 模式的话,建议选择较高版本的 Kafka,因为这个功能还在持续完善优化中。Kafka 3.3.1 版本是第一个将 KRaft(Kafka Raft)共识协议标记为生产就绪的版本。** +However, a reminder: **If you want to use KRaft mode, it is suggested to choose a higher version of Kafka, as this feature is still under continuous improvement and optimization. Kafka version 3.3.1 is the first version to mark the KRaft (Kafka Raft) consensus protocol as production-ready.** ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/kafka3.3.1-kraft-production-ready.png) -## Kafka 消费顺序、消息丢失和重复消费 +## Kafka Consumption Order, Message Loss and Duplicate Consumption -### Kafka 如何保证消息的消费顺序? +### How does Kafka guarantee the order of message consumption? -我们在使用消息队列的过程中经常有业务场景需要严格保证消息的消费顺序,比如我们同时发了 2 个消息,这 2 个消息对应的操作分别对应的数据库操作是: +In the process of using message queues, we often have business scenarios that require strict guarantees on the order of message consumption. For example, if we send two messages simultaneously, the corresponding operations in the database are: -1. 更改用户会员等级。 -2. 根据会员等级计算订单价格。 +1. Change the user's membership level. +1. Calculate the order price based on the membership level. -假如这两条消息的消费顺序不一样造成的最终结果就会截然不同。 +If the order in which these two messages are consumed is different, the final result could be completely different. -我们知道 Kafka 中 Partition(分区)是真正保存消息的地方,我们发送的消息都被放在了这里。而我们的 Partition(分区) 又存在于 Topic(主题) 这个概念中,并且我们可以给特定 Topic 指定多个 Partition。 +We know that Kafka's Partitions are the actual storage locations for messages, where the messages we send are placed. Our Partitions exist within the concept of Topics, and we can specify multiple Partitions for a specific Topic. ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/KafkaTopicPartionsLayout.png) -每次添加消息到 Partition(分区) 的时候都会采用尾加法,如上图所示。 **Kafka 只能为我们保证 Partition(分区) 中的消息有序。** +Each time a message is added to a Partition, it is appended sequentially, as shown in the image above. **Kafka can only guarantee the order of messages within a Partition.** -> 消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。 +> Messages are assigned a specific offset when added to a Partition. Kafka uses offsets to ensure the order of messages within the partition. -所以,我们就有一种很简单的保证消息消费顺序的方法:**1 个 Topic 只对应一个 Partition**。这样当然可以解决问题,但是破坏了 Kafka 的设计初衷。 +Thus, there is a simple way to ensure the order of message consumption: **1 Topic corresponds to 1 Partition.** Of course, this solves the problem but violates Kafka's original design intent. -Kafka 中发送 1 条消息的时候,可以指定 topic, partition, key,data(数据) 4 个参数。如果你发送消息的时候指定了 Partition 的话,所有消息都会被发送到指定的 Partition。并且,同一个 key 的消息可以保证只发送到同一个 partition,这个我们可以采用表/对象的 id 来作为 key 。 +When sending a message in Kafka, you can specify four parameters: topic, partition, key, and data. If you specify the Partition when sending the message, all messages will be sent to that specified Partition. Moreover, messages with the same key can only be sent to the same Partition, which we can achieve by using table/object IDs as keys. -总结一下,对于如何保证 Kafka 中消息消费的顺序,有了下面两种方法: +In summary, there are two methods to ensure the order of message consumption in Kafka: -1. 1 个 Topic 只对应一个 Partition。 -2. (推荐)发送消息的时候指定 key/Partition。 +1. One Topic corresponds to one Partition. +1. (Recommended) Specify key/Partition when sending messages. -当然不仅仅只有上面两种方法,上面两种方法是我觉得比较好理解的, +Of course, there are additional methods, but the two mentioned above are quite straightforward. -### Kafka 如何保证消息不丢失? +### How does Kafka guarantee messages are not lost? -#### 生产者丢失消息的情况 +#### Producer Missing Messages -生产者(Producer) 调用`send`方法发送消息之后,消息可能因为网络问题并没有发送过去。 +When a Producer calls the `send` method to send a message, the message may not be delivered due to network issues. -所以,我们不能默认在调用`send`方法发送消息之后消息发送成功了。为了确定消息是发送成功,我们要判断消息发送的结果。但是要注意的是 Kafka 生产者(Producer) 使用 `send` 方法发送消息实际上是异步的操作,我们可以通过 `get()`方法获取调用结果,但是这样也让它变为了同步操作,示例代码如下: +Thus, we cannot assume that a message was successfully sent just because we called the `send` method. To confirm that the message was sent successfully, we have to determine the result of the message sending. It's worth noting that the Kafka producer uses the `send` method to send messages, which is actually an asynchronous operation. We can obtain the result of the call through the `get()` method, but this turns it into a synchronous operation, as shown in the sample code below: -> **详细代码见我的这篇文章:[Kafka 系列第三篇!10 分钟学会如何在 Spring Boot 程序中使用 Kafka 作为消息队列?](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486269&idx=2&sn=ec00417ad641dd8c3d145d74cafa09ce&chksm=cea244f6f9d5cde0c8eb233fcc4cf82e11acd06446719a7af55230649863a3ddd95f78d111de&token=1633957262&lang=zh_CN#rd)** +> **For detailed code, please refer to my article: [Kafka Series Part 3! Learn how to use Kafka as a message queue in a Spring Boot program in 10 minutes?](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486269&idx=2&sn=ec00417ad641dd8c3d145d74cafa09ce&chksm=cea244f6f9d5cde0c8eb233fcc4cf82e11acd06446719a7af55230649863a3ddd95f78d111de&token=1633957262&lang=zh_CN#rd)** ```java SendResult sendResult = kafkaTemplate.send(topic, o).get(); if (sendResult.getRecordMetadata() != null) { - logger.info("生产者成功发送消息到" + sendResult.getProducerRecord().topic() + "-> " + sendRe - sult.getProducerRecord().value().toString()); + logger.info("Producer successfully sent message to " + sendResult.getProducerRecord().topic() + "-> " + sendResult.getProducerRecord().value().toString()); } ``` -但是一般不推荐这么做!可以采用为其添加回调函数的形式,示例代码如下: +However, this is generally not recommended! A better approach would be to use a callback function, as shown in the sample code below: ```java - ListenableFuture> future = kafkaTemplate.send(topic, o); - future.addCallback(result -> logger.info("生产者成功发送消息到topic:{} partition:{}的消息", result.getRecordMetadata().topic(), result.getRecordMetadata().partition()), - ex -> logger.error("生产者发送消失败,原因:{}", ex.getMessage())); +ListenableFuture> future = kafkaTemplate.send(topic, o); +future.addCallback(result -> logger.info("Producer successfully sent message to topic:{} partition:{} message", result.getRecordMetadata().topic(), result.getRecordMetadata().partition()), + ex -> logger.error("Producer failed to send message, reason: {}", ex.getMessage())); ``` -如果消息发送失败的话,我们检查失败的原因之后重新发送即可! +If message sending fails, we can check the reasons for failure and resend the message! -另外,这里推荐为 Producer 的`retries`(重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网络波动一次你 3 次一下子就重试完了。 +Additionally, it's recommended to set a reasonable value for the Producer's 'retries' (retry count), generally set to 3. However, to ensure no message loss, a somewhat larger value is usually set. After this is configured, if network issues occur, the message sending can be automatically retried to avoid loss. It is also advisable to set a retry interval; if the interval is too short, the effect of retrying will be diminished since multiple retries could happen within a short duration due to a single network fluctuation. -#### 消费者丢失消息的情况 +#### Consumer Missing Messages -我们知道消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。偏移量(offset)表示 Consumer 当前消费到的 Partition(分区)的所在的位置。Kafka 通过偏移量(offset)可以保证消息在分区内的顺序性。 +When we add messages to a Partition, each message receives a specific offset. The offset indicates the current position in the Partition that the Consumer has reached. Kafka uses offsets to ensure the order of messages within the partition. ![kafka offset](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/kafka-offset.jpg) -当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。 +Once a Consumer retrieves a message from a Partition, the offset is automatically committed. However, automatic committing introduces a problem: suppose when the Consumer just grabs this message to consume, it suddenly crashes. The message has not actually been consumed, but the offset has been automatically committed. -**解决办法也比较粗暴,我们手动关闭自动提交 offset,每次在真正消费完消息之后再自己手动提交 offset 。** 但是,细心的朋友一定会发现,这样会带来消息被重新消费的问题。比如你刚刚消费完消息之后,还没提交 offset,结果自己挂掉了,那么这个消息理论上就会被消费两次。 +**A more straightforward solution is to manually turn off automatic offset committing and manually commit the offset only after truly consuming the message.** However, careful observers will note that this could lead to the problem of re-consuming the message. For instance, if you consume a message successfully but crash before committing the offset, that message could theoretically be consumed twice. -#### Kafka 弄丢了消息 +#### Kafka Losing Messages -我们知道 Kafka 为分区(Partition)引入了多副本(Replica)机制。分区(Partition)中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。生产者和消费者只与 leader 副本交互。你可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。 +We know that Kafka introduces a replica mechanism for partitions. Within a partition, there are multiple replicas, one designated as the leader and the others as followers. Messages sent to Kafka are delivered to the leader replica first, then the follower replicas pull messages from the leader to sync. Producers and consumers only interact with the leader. You can think of other replicas as mere copies of the leader, existing solely to guarantee message storage security. -**试想一种情况:假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选出一个 leader ,但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消息丢失。** +**Consider a scenario where the broker hosting the leader replica suddenly crashes. In this case, a new leader has to be elected from among the follower replicas. However, if the leader has data that the follower has not yet synced, it could result in message loss.** -**设置 acks = all** +**Setting acks = all** -解决办法就是我们设置 **acks = all**。acks 是 Kafka 生产者(Producer) 很重要的一个参数。 +The remedy for this is to set **acks = all**. The acks parameter is very important for Kafka producers. -acks 的默认值即为 1,代表我们的消息被 leader 副本接收之后就算被成功发送。当我们配置 **acks = all** 表示只有所有 ISR 列表的副本全部收到消息时,生产者才会接收到来自服务器的响应. 这种模式是最高级别的,也是最安全的,可以确保不止一个 Broker 接收到了消息. 该模式的延迟会很高. +The default value of acks is 1, which means our message is deemed successfully sent once it is received by the leader replica. By configuring **acks = all**, it indicates that the producer will only receive a response from the server when all replicas in the ISR (in-sync replica) list have received the message. This mode is the highest level of reliability and ensures that more than one Broker has received the message, although it incurs higher latency. -**设置 replication.factor >= 3** +**Setting replication.factor >= 3** -为了保证 leader 副本能有 follower 副本能同步消息,我们一般会为 topic 设置 **replication.factor >= 3**。这样就可以保证每个 分区(partition) 至少有 3 个副本。虽然造成了数据冗余,但是带来了数据的安全性。 +To ensure that the leader replica has follower replicas that can sync messages, we generally set **replication.factor >= 3** for topics. This ensures that each partition has at least three replicas. Although this introduces data redundancy, it provides increased data security. -**设置 min.insync.replicas > 1** +**Setting min.insync.replicas > 1** -一般情况下我们还需要设置 **min.insync.replicas> 1** ,这样配置代表消息至少要被写入到 2 个副本才算是被成功发送。**min.insync.replicas** 的默认值为 1 ,在实际生产中应尽量避免默认值 1。 +In typical cases, we also need to set **min.insync.replicas > 1**. This configuration means that a message must be written to at least two replicas to be considered successfully sent. The default value of **min.insync.replicas** is 1, and in practice, this default value should be avoided. -但是,为了保证整个 Kafka 服务的高可用性,你需要确保 **replication.factor > min.insync.replicas** 。为什么呢?设想一下假如两者相等的话,只要是有一个副本挂掉,整个分区就无法正常工作了。这明显违反高可用性!一般推荐设置成 **replication.factor = min.insync.replicas + 1**。 +However, to ensure high availability of the entire Kafka service, you must ensure that **replication.factor > min.insync.replicas**. Why? If the two values are equal, as soon as one replica fails, the entire partition becomes non-functional, which clearly violates the principle of high availability! It is generally recommended to set **replication.factor = min.insync.replicas + 1**. -**设置 unclean.leader.election.enable = false** +**Setting unclean.leader.election.enable = false** -> **Kafka 0.11.0.0 版本开始 unclean.leader.election.enable 参数的默认值由原来的 true 改为 false** +> **Starting from Kafka version 0.11.0.0, the default value of the unclean.leader.election.enable parameter changed from true to false.** -我们最开始也说了我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。多个 follower 副本之间的消息同步情况不一样,当我们配置了 **unclean.leader.election.enable = false** 的话,当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。 +As mentioned, messages are sent to the leader replica first, which then allows the followers to sync messages. The synchronization state among various follower replicas can differ. When we configure **unclean.leader.election.enable = false**, if the leader replica fails, it will not select a leader from replicas that do not meet the synchronization criteria with the leader, thus reducing the likelihood of message loss. -### Kafka 如何保证消息不重复消费? +### How does Kafka ensure messages are not consumed multiple times? -**kafka 出现消息重复消费的原因:** +**Causes of duplicate message consumption in Kafka:** -- 服务端侧已经消费的数据没有成功提交 offset(根本原因)。 -- Kafka 侧 由于服务端处理业务时间长或者网络链接等等原因让 Kafka 认为服务假死,触发了分区 rebalance。 +- The data that has already been consumed on the server side was not successfully committed (the root cause). +- On the Kafka side, service processing time is too long or network connectivity issues can cause Kafka to think the service is in a pseudo-dead state, triggering a partition rebalance. -**解决方案:** +**Solutions:** -- 消费消息服务做幂等校验,比如 Redis 的 set、MySQL 的主键等天然的幂等功能。这种方法最有效。 -- 将 **`enable.auto.commit`** 参数设置为 false,关闭自动提交,开发者在代码中手动提交 offset。那么这里会有个问题:**什么时候提交 offset 合适?** - - 处理完消息再提交:依旧有消息重复消费的风险,和自动提交一样 - - 拉取到消息即提交:会有消息丢失的风险。允许消息延时的场景,一般会采用这种方式。然后,通过定时任务在业务不繁忙(比如凌晨)的时候做数据兜底。 +- Implement idempotent checks in the message consumption service, such as using Redis's set or MySQL's primary key, which inherently provides idempotence. This method is the most effective. +- Configure the **`enable.auto.commit`** parameter to false to disable automatic commits; developers manually commit the offset in their code. However, this raises the question: **When is it appropriate to commit the offset?** + - Commit after processing the message: This still carries the risk of duplicate consumption, similar to automatic committing. + - Commit immediately after pulling the message: This incurs a risk of message loss. Scenarios allowing message delays often adopt this approach, followed by timed tasks performed during off-peak hours (e.g., midnight) to ensure data integrity. -## Kafka 重试机制 +## Kafka Retry Mechanism -在 Kafka 如何保证消息不丢失这里,我们提到了 Kafka 的重试机制。由于这部分内容较为重要,我们这里再来详细介绍一下。 +In the discussion about how Kafka ensures messages are not lost, we touched on Kafka's retry mechanism. Since this part is quite important, we'll elaborate on it here. -网上关于 Spring Kafka 的默认重试机制文章很多,但大多都是过时的,和实际运行结果完全不一样。以下是根据 [spring-kafka-2.9.3](https://mvnrepository.com/artifact/org.springframework.kafka/spring-kafka/2.9.3) 源码重新梳理一下。 +There are many articles online about Spring Kafka's default retry mechanism, but most are outdated and do not reflect actual running results. Below is a reorganization based on the [spring-kafka-2.9.3](https://mvnrepository.com/artifact/org.springframework.kafka/spring-kafka/2.9.3) source code. -### 消费失败会怎么样? +### What happens if consumption fails? -在消费过程中,当其中一个消息消费异常时,会不会卡住后续队列消息的消费?这样业务岂不是卡住了? +During the consumption process, if one message fails to process, will it block the consumption of subsequent queued messages? Wouldn't that stall the business? -生产者代码: +Producer code: ```Java for (int i = 0; i < 10; i++) { @@ -252,90 +251,88 @@ acks 的默认值即为 1,代表我们的消息被 leader 副本接收之后 } ``` -消费者消代码: +Consumer code: ```Java - @KafkaListener(topics = {KafkaConst.TEST_TOPIC},groupId = "apple") + @KafkaListener(topics = {KafkaConst.TEST_TOPIC}, groupId = "apple") private void customer(String message) throws InterruptedException { - log.info("kafka customer:{}",message); + log.info("kafka customer:{}", message); Integer n = Integer.parseInt(message); - if (n%5==0){ - throw new RuntimeException(); + if (n % 5 == 0) { + throw new RuntimeException(); } } ``` -在默认配置下,当消费异常会进行重试,重试多次后会跳过当前消息,继续进行后续消息的消费,不会一直卡在当前消息。下面是一段消费的日志,可以看出当 `test-0@95` 重试多次后会被跳过。 +Under the default configuration, if a consumption exception occurs, it will attempt retries. After multiple retries, it will skip the current message and continue processing the subsequent messages, thus not blocking on the current message. The log below shows that `test-0@95` is skipped after multiple retries. ```Java 2023-08-10 12:03:32.918 DEBUG 9700 --- [ntainer#0-0-C-1] o.s.kafka.listener.DefaultErrorHandler : Skipping seek of: test-0@95 2023-08-10 12:03:32.918 TRACE 9700 --- [ntainer#0-0-C-1] o.s.kafka.listener.DefaultErrorHandler : Seeking: test-0 to: 96 -2023-08-10 12:03:32.918 INFO 9700 --- [ntainer#0-0-C-1] o.a.k.clients.consumer.KafkaConsumer : [Consumer clientId=consumer-apple-1, groupId=apple] Seeking to offset 96 for partition test-0 - +2023-08-10 12:03:32.918 INFO 9700 --- [ntainer#0-0-C-1] o.a.k.clients.consumer.KafkaConsumer : [Consumer clientId=consumer-apple-1, groupId=apple] Seeking to offset 96 for partition test-0 ``` -因此,即使某个消息消费异常,Kafka 消费者仍然能够继续消费后续的消息,不会一直卡在当前消息,保证了业务的正常进行。 +Therefore, even if one message fails to process, the Kafka consumer can still continue consuming subsequent messages, ensuring normal business operation. -### 默认会重试多少次? +### How many retries are there by default? -默认配置下,消费异常会进行重试,重试次数是多少, 重试是否有时间间隔? +Under the default configuration, when an exception occurs during consumption, retries will take place. So how many retries are there, and is there a time interval between retries? -看源码 `FailedRecordTracker` 类有个 `recovered` 函数,返回 Boolean 值判断是否要进行重试,下面是这个函数中判断是否重试的逻辑: +Examining the `FailedRecordTracker` class, there is a `recovered` function that returns a Boolean value to determine if a retry should occur. Here’s the logic for determining whether to attempt a retry within this function: ```java - @Override - public boolean recovered(ConsumerRecord << ? , ? > record, Exception exception, - @Nullable MessageListenerContainer container, - @Nullable Consumer << ? , ? > consumer) throws InterruptedException { - - if (this.noRetries) { - // 不支持重试 - attemptRecovery(record, exception, null, consumer); - return true; - } - // 取已经失败的消费记录集合 - Map < TopicPartition, FailedRecord > map = this.failures.get(); - if (map == null) { - this.failures.set(new HashMap < > ()); - map = this.failures.get(); - } - // 获取消费记录所在的Topic和Partition - TopicPartition topicPartition = new TopicPartition(record.topic(), record.partition()); - FailedRecord failedRecord = getFailedRecordInstance(record, exception, map, topicPartition); - // 通知注册的重试监听器,消息投递失败 - this.retryListeners.forEach(rl - > - rl.failedDelivery(record, exception, failedRecord.getDeliveryAttempts().get())); - // 获取下一次重试的时间间隔 +@Override +public boolean recovered(ConsumerRecord << ? , ? > record, Exception exception, + @Nullable MessageListenerContainer container, + @Nullable Consumer << ? , ? > consumer) throws InterruptedException { + + if (this.noRetries) { + // Not supporting retries + attemptRecovery(record, exception, null, consumer); + return true; + } + // Retrieve the collection of already failed consumption records + Map < TopicPartition, FailedRecord > map = this.failures.get(); + if (map == null) { + this.failures.set(new HashMap < > ()); + map = this.failures.get(); + } + // Get the Topic and Partition where the consumption record is located + TopicPartition topicPartition = new TopicPartition(record.topic(), record.partition()); + FailedRecord failedRecord = getFailedRecordInstance(record, exception, map, topicPartition); + // Notify registered retry listeners of message delivery failure + this.retryListeners.forEach(rl -> rl.failedDelivery(record, exception, failedRecord.getDeliveryAttempts().get())); + // Get the next retry interval long nextBackOff = failedRecord.getBackOffExecution().nextBackOff(); - if (nextBackOff != BackOffExecution.STOP) { - this.backOffHandler.onNextBackOff(container, exception, nextBackOff); - return false; - } else { - attemptRecovery(record, exception, topicPartition, consumer); - map.remove(topicPartition); - if (map.isEmpty()) { - this.failures.remove(); - } - return true; - } - } + if (nextBackOff != BackOffExecution.STOP) { + this.backOffHandler.onNextBackOff(container, exception, nextBackOff); + return false; + } else { + attemptRecovery(record, exception, topicPartition, consumer); + map.remove(topicPartition); + if (map.isEmpty()) { + this.failures.remove(); + } + return true; + } +} ``` -其中, `BackOffExecution.STOP` 的值为 -1。 +Within this code, `BackOffExecution.STOP` is defined as -1. ```java @FunctionalInterface public interface BackOffExecution { - long STOP = -1; - long nextBackOff(); + long STOP = -1; + long nextBackOff(); } ``` -`nextBackOff` 的值调用 `BackOff` 类的 `nextBackOff()` 函数。如果当前执行次数大于最大执行次数则返回 `STOP`,既超过这个最大执行次数后才会停止重试。 +The value of `nextBackOff` is determined by calling the `nextBackOff()` function of the `BackOff` class. If the number of executions exceeds the maximum attempts, it returns `STOP`, meaning it will only stop retries after reaching the maximum number of attempts. -```Java +```java public long nextBackOff() { this.currentAttempts++; if (this.currentAttempts <= getMaxAttempts()) { @@ -347,7 +344,7 @@ public long nextBackOff() { } ``` -那么这个 `getMaxAttempts` 的值又是多少呢?回到最开始,当执行出错会进入 `DefaultErrorHandler` 。`DefaultErrorHandler` 默认的构造函数是: +So, what is the value of `getMaxAttempts`? Returning to the beginning, when an error occurs, it goes into `DefaultErrorHandler`. The default constructor of `DefaultErrorHandler` is: ```Java public DefaultErrorHandler() { @@ -355,7 +352,7 @@ public DefaultErrorHandler() { } ``` -`SeekUtils.DEFAULT_BACK_OFF` 定义的是: +`SeekUtils.DEFAULT_BACK_OFF` is defined as: ```Java public static final int DEFAULT_MAX_FAILURES = 10; @@ -363,19 +360,19 @@ public static final int DEFAULT_MAX_FAILURES = 10; public static final FixedBackOff DEFAULT_BACK_OFF = new FixedBackOff(0, DEFAULT_MAX_FAILURES - 1); ``` -`DEFAULT_MAX_FAILURES` 的值是 10,`currentAttempts` 从 0 到 9,所以总共会执行 10 次,每次重试的时间间隔为 0。 +The value of `DEFAULT_MAX_FAILURES` is 10; therefore, `currentAttempts` ranges from 0 to 9, making a total of 10 attempts, each with an interval of 0. -最后,简单总结一下:Kafka 消费者在默认配置下会进行最多 10 次 的重试,每次重试的时间间隔为 0,即立即进行重试。如果在 10 次重试后仍然无法成功消费消息,则不再进行重试,消息将被视为消费失败。 +In summary, Kafka consumers, under the default configuration, will attempt a maximum of 10 retries, with each retry having an interval of 0, meaning they will retry immediately. If, after 10 attempts, the message still cannot be consumed successfully, it will not be retried further, and the message will be regarded as failed consumption. -### 如何自定义重试次数以及时间间隔? +### How to customize retry counts and time intervals? -从上面的代码可以知道,默认错误处理器的重试次数以及时间间隔是由 `FixedBackOff` 控制的,`FixedBackOff` 是 `DefaultErrorHandler` 初始化时默认的。所以自定义重试次数以及时间间隔,只需要在 `DefaultErrorHandler` 初始化的时候传入自定义的 `FixedBackOff` 即可。重新实现一个 `KafkaListenerContainerFactory` ,调用 `setCommonErrorHandler` 设置新的自定义的错误处理器就可以实现。 +As indicated in the above code, the default error handler's retry count and time interval are controlled by the `FixedBackOff`, which is initialized by the `DefaultErrorHandler` by default. Thus, we can customize the retry count and time interval simply by passing a custom `FixedBackOff` when initializing the `DefaultErrorHandler`. To implement this, you can create a new `KafkaListenerContainerFactory`, invoking `setCommonErrorHandler` to set up a new custom error handler. ```Java @Bean public KafkaListenerContainerFactory kafkaListenerContainerFactory(ConsumerFactory consumerFactory) { ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory(); - // 自定义重试时间间隔以及次数 + // Custom retry time interval and counts FixedBackOff fixedBackOff = new FixedBackOff(1000, 5); factory.setCommonErrorHandler(new DefaultErrorHandler(fixedBackOff)); factory.setConsumerFactory(consumerFactory); @@ -383,39 +380,39 @@ public KafkaListenerContainerFactory kafkaListenerContainerFactory(ConsumerFacto } ``` -### 如何在重试失败后进行告警? +### How to alert when retries fail? -自定义重试失败后逻辑,需要手动实现,以下是一个简单的例子,重写 `DefaultErrorHandler` 的 `handleRemaining` 函数,加上自定义的告警等操作。 +To handle custom logic for when retries fail, you need to manually implement it. Below is a simple example where the `handleRemaining` function of the `DefaultErrorHandler` is overridden to include custom alerting operations. ```Java @Slf4j public class DelErrorHandler extends DefaultErrorHandler { public DelErrorHandler(FixedBackOff backOff) { - super(null,backOff); + super(null, backOff); } @Override public void handleRemaining(Exception thrownException, List> records, Consumer consumer, MessageListenerContainer container) { super.handleRemaining(thrownException, records, consumer, container); - log.info("重试多次失败"); - // 自定义操作 + log.info("Retry failed multiple times"); + // Custom operations } } ``` -`DefaultErrorHandler` 只是默认的一个错误处理器,Spring Kafka 还提供了 `CommonErrorHandler` 接口。手动实现 `CommonErrorHandler` 就可以实现更多的自定义操作,有很高的灵活性。例如根据不同的错误类型,实现不同的重试逻辑以及业务逻辑等。 +The `DefaultErrorHandler` is merely a default error handler; Spring Kafka also provides a `CommonErrorHandler` interface. You can implement `CommonErrorHandler` manually to achieve more custom operations, allowing for a high level of flexibility; for instance, to implement different retry logic and business logic based on various error types. -### 重试失败后的数据如何再次处理? +### How to process data again after retry failures? -当达到最大重试次数后,数据会直接被跳过,继续向后进行。当代码修复后,如何重新消费这些重试失败的数据呢? +Once the maximum retry count has been reached, data will simply be skipped, and processing will continue. After the code issues are fixed, how can we re-consume the data that failed to process? -**死信队列(Dead Letter Queue,简称 DLQ)** 是消息中间件中的一种特殊队列。它主要用于处理无法被消费者正确处理的消息,通常是因为消息格式错误、处理失败、消费超时等情况导致的消息被"丢弃"或"死亡"的情况。当消息进入队列后,消费者会尝试处理它。如果处理失败,或者超过一定的重试次数仍无法被成功处理,消息可以发送到死信队列中,而不是被永久性地丢弃。在死信队列中,可以进一步分析、处理这些无法正常消费的消息,以便定位问题、修复错误,并采取适当的措施。 +**Dead Letter Queue (DLQ)** is a special queue in message middleware for handling messages that cannot be correctly processed by consumers. This is usually due to issues like incorrect message formats, processing failures, or consumption timeouts leading to messages being "discarded" or "dead." When a message enters the queue, consumers will attempt to process it. If processing fails, or it cannot be successfully processed after exceeding the retry count, the message can be sent to a dead letter queue instead of being permanently discarded. In the dead letter queue, these problematic messages can be further analyzed and processed to identify issues, correct errors, and take appropriate action. -`@RetryableTopic` 是 Spring Kafka 中的一个注解,它用于配置某个 Topic 支持消息重试,更推荐使用这个注解来完成重试。 +`@RetryableTopic` is an annotation in Spring Kafka used to configure a specific Topic to support message retries. It is recommended to leverage this annotation for retries. ```Java -// 重试 5 次,重试间隔 100 毫秒,最大间隔 1 秒 +// Retry 5 times, with a 100-millisecond interval and a maximum of 1 second @RetryableTopic( attempts = "5", backoff = @Backoff(delay = 100, maxDelay = 1000) @@ -431,11 +428,9 @@ private void customer(String message) { } ``` -当达到最大重试次数后,如果仍然无法成功处理消息,消息会被发送到对应的死信队列中。对于死信队列的处理,既可以用 `@DltHandler` 处理,也可以使用 `@KafkaListener` 重新消费。 - -## 参考 +When the maximum retry count is reached, if the message is still not processed successfully, it'll be sent to the corresponding dead letter queue. Handling of dead letter queues can be done through `@DltHandler` or re-consuming with `@KafkaListener`. -- Kafka 官方文档: -- 极客时间—《Kafka 核心技术与实战》第 11 节:无消息丢失配置怎么实现? +## References - +- Kafka Official Documentation: +- Geek Time - Section 11 of “Kafka Core Technologies and Practice”: How to configure for no message loss? diff --git a/docs/high-performance/message-queue/message-queue.md b/docs/high-performance/message-queue/message-queue.md index b366836ec2c..cb1dbb9183f 100644 --- a/docs/high-performance/message-queue/message-queue.md +++ b/docs/high-performance/message-queue/message-queue.md @@ -1,314 +1,314 @@ --- -title: 消息队列基础知识总结 -category: 高性能 +title: Summary of Basic Knowledge of Message Queue +category: High Performance tag: - - 消息队列 + - Message Queue --- ::: tip -这篇文章中的消息队列主要指的是分布式消息队列。 +The message queue discussed in this article primarily refers to distributed message queues. ::: -“RabbitMQ?”“Kafka?”“RocketMQ?”...在日常学习与开发过程中,我们常常听到消息队列这个关键词。我也在我的多篇文章中提到了这个概念。可能你是熟练使用消息队列的老手,又或者你是不懂消息队列的新手,不论你了不了解消息队列,本文都将带你搞懂消息队列的一些基本理论。 +"RabbitMQ?" "Kafka?" "RocketMQ?"... In our daily study and development, we often hear the keyword message queue. I have mentioned this concept in my multiple articles. You may be an experienced user of message queues, or perhaps you are a novice unfamiliar with message queues. Regardless of your understanding, this article will help you grasp some fundamental theories of message queues. -如果你是老手,你可能从本文学到你之前不曾注意的一些关于消息队列的重要概念,如果你是新手,相信本文将是你打开消息队列大门的一板砖。 +If you are an expert, you may learn some important concepts about message queues that you have not previously noticed from this article. If you are a novice, I believe this article will serve as a stepping stone for you to open the door to message queues. -## 什么是消息队列? +## What is a Message Queue? -我们可以把消息队列看作是一个存放消息的容器,当我们需要使用消息的时候,直接从容器中取出消息供自己使用即可。由于队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。 +We can think of a message queue as a container for storing messages. When we need to use a message, we can directly take it out from the container. Since a queue is a first-in, first-out data structure, the consumption of messages is also done in order. ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-small.png) -参与消息传递的双方称为 **生产者** 和 **消费者** ,生产者负责发送消息,消费者负责处理消息。 +The two parties involved in message transmission are called **producers** and **consumers**. The producer is responsible for sending messages, while the consumer is responsible for processing messages. -![发布/订阅(Pub/Sub)模型](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-pub-sub-model.png) +![Publish/Subscribe (Pub/Sub) Model](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-pub-sub-model.png) -操作系统中的进程通信的一种很重要的方式就是消息队列。我们这里提到的消息队列稍微有点区别,更多指的是各个服务以及系统内部各个组件/模块之前的通信,属于一种 **中间件** 。 +One important way for process communication in operating systems is through message queues. The message queues we are referring to here are slightly different; they more often indicate communication between various services and components/modules within systems, functioning as a type of **middleware**. -维基百科是这样介绍中间件的: +Wikipedia describes middleware as follows: -> 中间件(英语:Middleware),又译中间件、中介层,是一类提供系统软件和应用软件之间连接、便于软件各部件之间的沟通的软件,应用软件可以借助中间件在不同的技术架构之间共享信息与资源。中间件位于客户机服务器的操作系统之上,管理着计算资源和网络通信。 +> Middleware is software that provides common services and capabilities to applications outside of what's offered by the operating system. It can be used to connect different applications, making communication between software components easier. Middleware sits on top of the operating system of client-server and manages computing resources and network communication. -简单来说:**中间件就是一类为应用软件服务的软件,应用软件是为用户服务的,用户不会接触或者使用到中间件。** +In simple terms: **Middleware is a type of software that serves application software, which in turn serves users who do not directly interact with or use middleware.** -除了消息队列之外,常见的中间件还有 RPC 框架、分布式组件、HTTP 服务器、任务调度框架、配置中心、数据库层的分库分表工具和数据迁移工具等等。 +In addition to message queues, common middleware includes RPC frameworks, distributed components, HTTP servers, task scheduling frameworks, configuration centers, database partitioning and sharding tools, and data migration tools, among others. -关于中间件比较详细的介绍可以参考阿里巴巴淘系技术的一篇回答: 。 +A more detailed introduction to middleware can be found in a response from Alibaba's Taobao technology: . -随着分布式和微服务系统的发展,消息队列在系统设计中有了更大的发挥空间,使用消息队列可以降低系统耦合性、实现任务异步、有效地进行流量削峰,是分布式和微服务系统中重要的组件之一。 +With the development of distributed and microservice systems, message queues have gained greater utility in system design. Using message queues can reduce system coupling, enable asynchronous tasks, and effectively manage peak traffic. They are one of the key components in distributed and microservice systems. -## 消息队列有什么用? +## What Are the Uses of Message Queues? -通常来说,使用消息队列主要能为我们的系统带来下面三点好处: +Generally speaking, using message queues can bring the following three benefits to our systems: -1. 异步处理 -2. 削峰/限流 -3. 降低系统耦合性 +1. Asynchronous processing +1. Traffic shaping/throttling +1. Reduced system coupling -除了这三点之外,消息队列还有其他的一些应用场景,例如实现分布式事务、顺序保证和数据流处理。 +In addition to these three points, message queues also have other application scenarios, such as implementing distributed transactions, ensuring order guarantees, and performing data stream processing. -如果在面试的时候你被面试官问到这个问题的话,一般情况是你在你的简历上涉及到消息队列这方面的内容,这个时候推荐你结合你自己的项目来回答。 +If you are asked this question in an interview, it is usually because you have mentioned message queues in your resume. At this point, it is advisable to relate your answer to your own project experience. -### 异步处理 +### Asynchronous Processing -![通过异步处理提高系统性能](https://oss.javaguide.cn/github/javaguide/Asynchronous-message-queue.png) +![Improving System Performance Through Asynchronous Processing](https://oss.javaguide.cn/github/javaguide/Asynchronous-message-queue.png) -将用户请求中包含的耗时操作,通过消息队列实现异步处理,将对应的消息发送到消息队列之后就立即返回结果,减少响应时间,提高用户体验。随后,系统再对消息进行消费。 +Handling time-consuming operations included in user requests through message queues allows the corresponding messages to be sent to the queue, enabling immediate results to be returned and reducing response times, thus improving user experience. Subsequently, the system consumes the messages. -因为用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败。因此,**使用消息队列进行异步处理之后,需要适当修改业务流程进行配合**,比如用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功,以免交易纠纷。这就类似我们平时手机订火车票和电影票。 +Since user request data is written into the message queue and immediately returned to the user, it is possible that the request fails during subsequent business verification or database writing operations. Therefore, **after using a message queue for asynchronous processing, it is necessary to appropriately modify business processes to accommodate this**, for instance, informing users of successful order submissions via email or SMS only after the order is truly processed by the message queue's consumer process or shipped, to avoid transaction disputes. This is similar to the process of booking train tickets or movie tickets on our smartphones. -### 削峰/限流 +### Traffic Shaping/Throttling -**先将短时间高并发产生的事务消息存储在消息队列中,然后后端服务再慢慢根据自己的能力去消费这些消息,这样就避免直接把后端服务打垮掉。** +**Store the transactional messages generated by high concurrency in a short period in the message queue, and then the backend services can gradually consume these messages based on their capacity, thereby avoiding overwhelming the backend services.** -举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示: +For example: In some e-commerce flash sales or promotional activities, properly using message queues can effectively mitigate the impact of a surge of orders at the start of a promotional event. As shown in the figure below: -![削峰](https://oss.javaguide.cn/github/javaguide/%E5%89%8A%E5%B3%B0-%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97.png) +![Traffic Shaping](https://oss.javaguide.cn/github/javaguide/%E5%89%8A%E5%B3%B0-%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97.png) -### 降低系统耦合性 +### Reducing System Coupling -使用消息队列还可以降低系统耦合性。如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。 +Using message queues can also reduce system coupling. If there are no direct calls between modules, then adding or modifying a module impacts other modules less, enhancing system scalability. -生产者(客户端)发送消息到消息队列中去,消费者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合,这显然也提高了系统的扩展性。 +The producer (client) sends messages to the message queue, and the consumer (server) processes the messages. The systems that need to consume messages can directly retrieve them from the message queue without coupling with other systems, clearly improving system extensibility. -![发布/订阅(Pub/Sub)模型](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-pub-sub-model.png) +![Publish/Subscribe (Pub/Sub) Model](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-pub-sub-model.png) -**消息队列使用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。 +**Message queues work using the publish-subscribe model, where the message sender (producer) publishes messages, and one or more message receivers (consumers) subscribe to the messages.** As seen in the above diagram, **there is no direct coupling between message senders (producers) and message receivers (consumers)**. The message sender sends the message to the distributed message queue and completes its handling of that message. The message receiver retrieves the message from the distributed message queue for further processing without needing to know where the message originated. **For new business, as long as the interested parties subscribe to the relevant messages, there is no impact on existing systems and business, thereby achieving extensibility of the website's business design**. -例如,我们商城系统分为用户、订单、财务、仓储、消息通知、物流、风控等多个服务。用户在完成下单后,需要调用财务(扣款)、仓储(库存管理)、物流(发货)、消息通知(通知用户发货)、风控(风险评估)等服务。使用消息队列后,下单操作和后续的扣款、发货、通知等操作就解耦了,下单完成发送一个消息到消息队列,需要用到的地方去订阅这个消息进行消息即可。 +For instance, our e-commerce system comprises various services like user, order, finance, storage, message notification, logistics, and risk control. After a user completes an order, it needs to call services for finance (payment), storage (inventory management), logistics (delivery), message notification (notifying users of shipment), and risk control (risk assessment). With message queues, the order placement and subsequent payment, delivery, notification, etc., become decoupled; after placing an order, a message is sent to the message queue, and the necessary parties can subscribe to that message for consumption. ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-decouple-mall-example.png) -另外,为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。 +Additionally, to prevent message loss due to message queue server downtime, successfully sent messages are stored on the message producer's server and are deleted only once the message has been genuinely processed by the consumer server. When the message queue server is down, the producer server will choose another server from the distributed message queue server cluster to publish the message. -**备注:** 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的。除了发布-订阅模式,还有点对点订阅模式(一个消息只有一个消费者),我们比较常用的是发布-订阅模式。另外,这两种消息模型是 JMS 提供的,AMQP 协议还提供了另外 5 种消息模型。 +**Note:** Do not think that message queues can only work using the publish-subscribe model; it is just that in the specific business context of decoupling, the publish-subscribe model is utilized. Besides the publish-subscribe model, there is also the point-to-point subscription model (where a message is consumed by only one consumer). The most commonly used model is the publish-subscribe model. Moreover, these two messaging models are provided by JMS, and the AMQP protocol offers five additional messaging models. -### 实现分布式事务 +### Implementing Distributed Transactions -分布式事务的解决方案之一就是 MQ 事务。 +One solution for distributed transactions is MG transactions. -RocketMQ、 Kafka、Pulsar、QMQ 都提供了事务相关的功能。事务允许事件流应用将消费,处理,生产消息整个过程定义为一个原子操作。 +RocketMQ, Kafka, Pulsar, and QMQ all provide features related to transactions. Transactions allow event-stream applications to define the entire process of consuming, processing, and producing messages as an atomic operation. -详细介绍可以查看 [分布式事务详解(付费)](https://javaguide.cn/distributed-system/distributed-transaction.html) 这篇文章。 +For a detailed introduction, you can refer to the article [Detailed Explanation of Distributed Transactions (Paid)](https://javaguide.cn/distributed-system/distributed-transaction.html). -![分布式事务详解 - MQ事务](https://oss.javaguide.cn/github/javaguide/csdn/07b338324a7d8894b8aef4b659b76d92.png) +![Detailed Explanation of Distributed Transactions - MQ Transactions](https://oss.javaguide.cn/github/javaguide/csdn/07b338324a7d8894b8aef4b659b76d92.png) -### 顺序保证 +### Order Guarantee -在很多应用场景中,处理数据的顺序至关重要。消息队列保证数据按照特定的顺序被处理,适用于那些对数据顺序有严格要求的场景。大部分消息队列,例如 RocketMQ、RabbitMQ、Pulsar、Kafka,都支持顺序消息。 +In many application scenarios, the order of data processing is crucial. Message queues ensure that data is processed in a specific order, making them suitable for scenarios with strict requirements for data order. Most message queues, such as RocketMQ, RabbitMQ, Pulsar, and Kafka, support ordered messages. -### 延时/定时处理 +### Delayed/Timed Processing -消息发送后不会立即被消费,而是指定一个时间,到时间后再消费。大部分消息队列,例如 RocketMQ、RabbitMQ、Pulsar、Kafka,都支持定时/延时消息。 +Messages are not immediately consumed after being sent but are instead scheduled for processing after a specified time. Most message queues, like RocketMQ, RabbitMQ, Pulsar, and Kafka, support scheduled/delayed messages. ![](https://oss.javaguide.cn/github/javaguide/tools/docker/rocketmq-schedule-message.png) -### 即时通讯 +### Instant Messaging -MQTT(消息队列遥测传输协议)是一种轻量级的通讯协议,采用发布/订阅模式,非常适合于物联网(IoT)等需要在低带宽、高延迟或不可靠网络环境下工作的应用。它支持即时消息传递,即使在网络条件较差的情况下也能保持通信的稳定性。 +MQTT (Message Queuing Telemetry Transport) is a lightweight messaging protocol that adopts the publish/subscribe model and is very suitable for applications in IoT (Internet of Things) that need to operate in low bandwidth, high latency, or unreliable network environments. It supports instant messaging, maintaining stable communication even under adverse network conditions. -RabbitMQ 内置了 MQTT 插件用于实现 MQTT 功能(默认不启用,需要手动开启)。 +RabbitMQ has an MQTT plugin built-in to implement MQTT functionality (it is disabled by default and needs to be manually enabled). -### 数据流处理 +### Data Stream Processing -针对分布式系统产生的海量数据流,如业务日志、监控数据、用户行为等,消息队列可以实时或批量收集这些数据,并将其导入到大数据处理引擎中,实现高效的数据流管理和处理。 +For the massive data streams generated by distributed systems, such as business logs, monitoring data, and user behavior, message queues can collect this data in real-time or in batches and import it into big data processing engines, enabling efficient data stream management and processing. -## 使用消息队列会带来哪些问题? +## What Problems Can Using Message Queues Bring? -- **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了! -- **系统复杂性提高:** 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题! -- **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! +- **Decreased System Availability:** System availability is reduced to some extent. Why is that? Before introducing MQ, you didn't have to consider issues like message loss or MQ crashes, but after introducing MQ, you do! +- **Increased System Complexity:** After adding MQ, you need to ensure messages are not consumed multiple times, handle message processing failures, guarantee message delivery order, and other related issues! +- **Consistency Problems:** I mentioned above that message queues can achieve asynchronous processing, which indeed improves system response speed. However, what if the true consumers of the messages do not consume them correctly? This can lead to inconsistent data! -## JMS 和 AMQP +## JMS and AMQP -### JMS 是什么? +### What is JMS? -JMS(JAVA Message Service,java 消息服务)是 Java 的消息服务,JMS 的客户端之间可以通过 JMS 服务进行异步的消息传输。**JMS(JAVA Message Service,Java 消息服务)API 是一个消息服务的标准或者说是规范**,允许应用程序组件基于 JavaEE 平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。 +JMS (Java Message Service) is Java's messaging service, allowing JMS clients to perform asynchronous message transmission through JMS services. **The JMS (Java Message Service) API is a standard or specification for message services**, allowing application components to create, send, receive, and read messages based on the JavaEE platform. It reduces the coupling of distributed communication, making message services more reliable and asynchronous. -JMS 定义了五种不同的消息正文格式以及调用的消息类型,允许你发送并接收以一些不同形式的数据: +JMS defines five different message body formats and calling message types that allow you to send and receive data in various forms: -- `StreamMessage:Java` 原始值的数据流 -- `MapMessage`:一套名称-值对 -- `TextMessage`:一个字符串对象 -- `ObjectMessage`:一个序列化的 Java 对象 -- `BytesMessage`:一个字节的数据流 +- `StreamMessage`: Java raw value data stream +- `MapMessage`: A set of name-value pairs +- `TextMessage`: A string object +- `ObjectMessage`: A serialized Java object +- `BytesMessage`: A byte data stream -**ActiveMQ(已被淘汰) 就是基于 JMS 规范实现的。** +**ActiveMQ (now deprecated) is based on the JMS specification.** -### JMS 两种消息模型 +### Two Message Models in JMS -#### 点到点(P2P)模型 +#### Point-to-Point (P2P) Model -![队列模型](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-queue-model.png) +![Queue Model](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-queue-model.png) -使用**队列(Queue)**作为消息通信载体;满足**生产者与消费者模式**,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。比如:我们生产者发送 100 条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。) +Uses **Queues** as the message communication carrier; satisfies the **Producer-Consumer model** where a message can only be used by one consumer. Unconsumed messages remain in the queue until they are consumed or time out. For example, if the producer sends 100 messages, typically, two consumers will consume half of them each, following the order in which the messages were sent. -#### 发布/订阅(Pub/Sub)模型 +#### Publish/Subscribe (Pub/Sub) Model -![发布/订阅(Pub/Sub)模型](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-pub-sub-model.png) +![Publish/Subscribe (Pub/Sub) Model](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-pub-sub-model.png) -发布订阅模型(Pub/Sub) 使用**主题(Topic)**作为消息通信载体,类似于**广播模式**;发布者发布一条消息,该消息通过主题传递给所有的订阅者。 +The Publish-Subscribe model (Pub/Sub) uses **Topics** as the message communication carrier, similar to **broadcast mode**; when a publisher publishes a message, that message is delivered to all subscribers through the topic. -### AMQP 是什么? +### What is AMQP? -AMQP,即 Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准 **高级消息队列协议**(二进制应用层协议),是应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。 +AMQP, or Advanced Message Queuing Protocol, is a binary application layer protocol providing a unified messaging service. It is an open standard designed for message-oriented middleware and is compatible with JMS. AMQP-based clients can pass messages to message middleware without being limited by client/middleware products or different programming languages. -**RabbitMQ 就是基于 AMQP 协议实现的。** +**RabbitMQ is based on the AMQP protocol.** -### JMS vs AMQP +### Comparison of JMS and AMQP -| 对比方向 | JMS | AMQP | -| :----------: | :-------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 定义 | Java API | 协议 | -| 跨语言 | 否 | 是 | -| 跨平台 | 否 | 是 | -| 支持消息类型 | 提供两种消息模型:①Peer-2-Peer;②Pub/sub | 提供了五种消息模型:①direct exchange;②fanout exchange;③topic change;④headers exchange;⑤system exchange。本质来讲,后四种和 JMS 的 pub/sub 模型没有太大差别,仅是在路由机制上做了更详细的划分; | -| 支持消息类型 | 支持多种消息类型 ,我们在上面提到过 | byte[](二进制) | +| Comparison Aspect | JMS | AMQP | +| :------------------: | :---------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Definition | Java API | Protocol | +| Cross-Language | No | Yes | +| Cross-Platform | No | Yes | +| Message Type Support | Provides two message models: ① Peer-2-Peer; ② Pub/Sub | Provides five message models: ① direct exchange; ② fanout exchange; ③ topic change; ④ headers exchange; ⑤ system exchange. Essentially, the latter four are not significantly different from the JMS pub/sub model but have a more detailed routing mechanism. | +| Message Type Support | Supports various message types mentioned above | byte[](binary) | -**总结:** +**Summary:** -- AMQP 为消息定义了线路层(wire-level protocol)的协议,而 JMS 所定义的是 API 规范。在 Java 体系中,多个 client 均可以通过 JMS 进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而 AMQP 天然具有跨平台、跨语言特性。 -- JMS 支持 `TextMessage`、`MapMessage` 等复杂的消息类型;而 AMQP 仅支持 `byte[]` 消息类型(复杂的类型可序列化后发送)。 -- 由于 Exchange 提供的路由算法,AMQP 可以提供多样化的路由方式来传递消息到消息队列,而 JMS 仅支持 队列 和 主题/订阅 方式两种。 +- AMQP defines a wire-level protocol for messaging, while JMS defines the API specification. Within the Java ecosystem, multiple clients can interact through JMS without needing to modify application code, but its support for cross-platform capability is weaker. AMQP, by nature, supports cross-platform and cross-language features. +- JMS supports complex message types like `TextMessage` and `MapMessage`, whereas AMQP only supports `byte[]` message types (complex types can be serialized and sent). +- Due to the routing algorithms provided by Exchange, AMQP can offer a diverse range of routing methods for delivering messages to message queues, while JMS only supports two methods: queues and topics/subscriptions. -## RPC 和消息队列的区别 +## Differences Between RPC and Message Queues -RPC 和消息队列都是分布式微服务系统中重要的组件之一,下面我们来简单对比一下两者: +Both RPC and message queues are important components in distributed microservice systems. Let's briefly compare the two: -- **从用途来看**:RPC 主要用来解决两个服务的远程通信问题,不需要了解底层网络的通信机制。通过 RPC 可以帮助我们调用远程计算机上某个服务的方法,这个过程就像调用本地方法一样简单。消息队列主要用来降低系统耦合性、实现任务异步、有效地进行流量削峰。 -- **从通信方式来看**:RPC 是双向直接网络通讯,消息队列是单向引入中间载体的网络通讯。 -- **从架构上来看**:消息队列需要把消息存储起来,RPC 则没有这个要求,因为前面也说了 RPC 是双向直接网络通讯。 -- **从请求处理的时效性来看**:通过 RPC 发出的调用一般会立即被处理,存放在消息队列中的消息并不一定会立即被处理。 +- **Use Case:** RPC primarily addresses the remote communication issue between two services, without needing to know the underlying network communication mechanism. Through RPC, we can easily invoke methods from services on remote computers as if calling local methods. Message queues are mainly used to reduce system coupling, achieve asynchronous tasks, and efficiently manage traffic shaping. +- **Communication Method:** RPC is bidirectional direct network communication, while message queues are unidirectional with an intermediary. +- **Architecture Requirements:** Message queues need to store messages, whereas RPC does not require this, as it is bidirectional direct network communication. +- **Timeliness of Request Processing:** Calls made through RPC are typically processed immediately, while messages stored in a message queue may not be processed right away. -RPC 和消息队列本质上是网络通讯的两种不同的实现机制,两者的用途不同,万不可将两者混为一谈。 +RPC and message queues are essentially two different implementation mechanisms for network communication, serving different purposes and should not be conflated. -## 分布式消息队列技术选型 +## Distributed Message Queue Technology Selection -### 常见的消息队列有哪些? +### What Are the Common Message Queues? #### Kafka ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/kafka-logo.png) -Kafka 是 LinkedIn 开源的一个分布式流式处理平台,已经成为 Apache 顶级项目,早期被用来用于处理海量的日志,后面才慢慢发展成了一款功能全面的高性能消息队列。 +Kafka is a distributed streaming processing platform open-sourced by LinkedIn and has become an Apache top-level project. It was initially used for handling massive logs, later evolving into a comprehensive high-performance message queue. -流式处理平台具有三个关键功能: +Streaming processing platforms have three key functions: -1. **消息队列**:发布和订阅消息流,这个功能类似于消息队列,这也是 Kafka 也被归类为消息队列的原因。 -2. **容错的持久方式存储记录消息流**:Kafka 会把消息持久化到磁盘,有效避免了消息丢失的风险。 -3. **流式处理平台:** 在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。 +1. **Message Queues:** Publishing and subscribing to message streams, similar to message queues, which is why Kafka is categorized as a message queue. +1. **Fault-Tolerant Persistent Storage of Recorded Message Streams:** Kafka persists messages to disk, effectively avoiding the risk of message loss. +1. **Streaming Processing Platform:** Processes messages during publishing, with Kafka providing a complete streaming processing library. -Kafka 是一个分布式系统,由通过高性能 TCP 网络协议进行通信的服务器和客户端组成,可以部署在在本地和云环境中的裸机硬件、虚拟机和容器上。 +Kafka is a distributed system composed of servers and clients communicating through a high-performance TCP network protocol and can be deployed on bare metal hardware, virtual machines, and containers in local and cloud environments. -在 Kafka 2.8 之前,Kafka 最被大家诟病的就是其重度依赖于 Zookeeper 做元数据管理和集群的高可用。在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构,让你可以以一种轻量级的方式来使用 Kafka。 +Before Kafka 2.8, Kafka was heavily criticized for its reliance on Zookeeper for metadata management and cluster high availability. After Kafka 2.8, KRaft mode was introduced based on the Raft protocol, eliminating the dependency on Zookeeper and greatly simplifying Kafka's architecture to allow for a more lightweight usage. -不过,要提示一下:**如果要使用 KRaft 模式的话,建议选择较高版本的 Kafka,因为这个功能还在持续完善优化中。Kafka 3.3.1 版本是第一个将 KRaft(Kafka Raft)共识协议标记为生产就绪的版本。** +However, a note of caution: **If you want to use KRaft mode, it is recommended that you choose a higher version of Kafka, as this feature is still under continuous refinement and optimization. Kafka version 3.3.1 is the first release where KRaft (Kafka Raft) consensus protocol is marked as production-ready.** ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/kafka3.3.1-kraft-production-ready.png) -Kafka 官网: +Kafka official website: -Kafka 更新记录(可以直观看到项目是否还在维护): +Kafka update records (to visually check if the project is still maintained): #### RocketMQ ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/rocketmq-logo.png) -RocketMQ 是阿里开源的一款云原生“消息、事件、流”实时数据处理平台,借鉴了 Kafka,已经成为 Apache 顶级项目。 +RocketMQ is an open-source "message, event, and stream" real-time data processing platform from Alibaba, which has drawn inspiration from Kafka and has become an Apache top-level project. -RocketMQ 的核心特性(摘自 RocketMQ 官网): +Core features of RocketMQ (from the RocketMQ official site): -- 云原生:生与云,长与云,无限弹性扩缩,K8s 友好 -- 高吞吐:万亿级吞吐保证,同时满足微服务与大数据场景。 -- 流处理:提供轻量、高扩展、高性能和丰富功能的流计算引擎。 -- 金融级:金融级的稳定性,广泛用于交易核心链路。 -- 架构极简:零外部依赖,Shared-nothing 架构。 -- 生态友好:无缝对接微服务、实时计算、数据湖等周边生态。 +- Cloud-Native: Designed for cloud environments with infinite scalability, Kubernetes-friendly. +- High Throughput: Guarantees trillions of throughput while meeting microservices and big data scenarios. +- Stream Processing: Provides lightweight, highly scalable, high-performance, and feature-rich stream computing engines. +- Financial Grade: Stable enough for enterprise-level core trading links. +- Simplified Architecture: No external dependencies, shared-nothing architecture. +- Ecosystem Friendly: Seamless integration with microservices, real-time computing, data lakes, and other surrounding ecosystems. -根据官网介绍: +According to the official site, -> Apache RocketMQ 自诞生以来,因其架构简单、业务功能丰富、具备极强可扩展性等特点被众多企业开发者以及云厂商广泛采用。历经十余年的大规模场景打磨,RocketMQ 已经成为业内共识的金融级可靠业务消息首选方案,被广泛应用于互联网、大数据、移动互联网、物联网等领域的业务场景。 +> Since its inception, Apache RocketMQ has been widely adopted by numerous enterprise developers and cloud vendors due to its simple architecture, rich business functionality, and strong scalability. After ten years of large-scale scene refinement, RocketMQ has become a consensus choice for financial-grade reliable business messaging, widely used in e-commerce, big data, mobile internet, and IoT applications. -RocketMQ 官网: (文档很详细,推荐阅读) +RocketMQ official site: (the documentation is very detailed and highly recommended) -RocketMQ 更新记录(可以直观看到项目是否还在维护): +RocketMQ update records (to visually check if the project is still maintained): #### RabbitMQ ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/rabbitmq-logo.png) -RabbitMQ 是采用 Erlang 语言实现 AMQP(Advanced Message Queuing Protocol,高级消息队列协议)的消息中间件,它最初起源于金融系统,用于在分布式系统中存储转发消息。 +RabbitMQ is a messaging middleware implemented in Erlang that uses AMQP (Advanced Message Queuing Protocol). It originally stemmed from financial systems for message storage and forwarding in distributed systems. -RabbitMQ 发展到今天,被越来越多的人认可,这和它在易用性、扩展性、可靠性和高可用性等方面的卓著表现是分不开的。RabbitMQ 的具体特点可以概括为以下几点: +Today, RabbitMQ has gained recognition among many users due to its outstanding performance in usability, scalability, reliability, and high availability. The specific characteristics of RabbitMQ can be summarized as follows: -- **可靠性:** RabbitMQ 使用一些机制来保证消息的可靠性,如持久化、传输确认及发布确认等。 -- **灵活的路由:** 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。这个后面会在我们讲 RabbitMQ 核心概念的时候详细介绍到。 -- **扩展性:** 多个 RabbitMQ 节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。 -- **高可用性:** 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队列仍然可用。 -- **支持多种协议:** RabbitMQ 除了原生支持 AMQP 协议,还支持 STOMP、MQTT 等多种消息中间件协议。 -- **多语言客户端:** RabbitMQ 几乎支持所有常用语言,比如 Java、Python、Ruby、PHP、C#、JavaScript 等。 -- **易用的管理界面:** RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。在安装 RabbitMQ 的时候会介绍到,安装好 RabbitMQ 就自带管理界面。 -- **插件机制:** RabbitMQ 提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。感觉这个有点类似 Dubbo 的 SPI 机制 +- **Reliability:** RabbitMQ employs mechanisms to ensure message reliability, such as persistence, delivery confirmations, and publishing confirmations. +- **Flexible Routing:** Messages are routed through exchangeers before entering queues. RabbitMQ has provided built-in exchangeers for typical routing functionalities. For more complex routing functionalities, multiple exchangeers can be bound together or custom exchangeers can be implemented via plugins, which will be detailed later in the core concepts of RabbitMQ. +- **Scalability:** Multiple RabbitMQ nodes can form a cluster and dynamically extend nodes based on actual business demands. +- **High Availability:** Queues can be mirrored across machines in the cluster, ensuring availability even if some nodes fail. +- **Support for Multiple Protocols:** In addition to natively supporting AMQP, RabbitMQ also supports STOMP, MQTT, and various other messaging middleware protocols. +- **Multi-language Clients:** RabbitMQ supports almost all commonly used languages such as Java, Python, Ruby, PHP, C#, and JavaScript. +- **User-Friendly Management Interface:** RabbitMQ provides an easy-to-use graphical interface for users to monitor and manage messages and nodes in the cluster. We will discuss this during the RabbitMQ installation. +- **Plugin Mechanism:** RabbitMQ offers numerous plugins for diverse functionalities and there is also the capability to develop custom plugins, somewhat akin to Dubbo's SPI mechanism. -RabbitMQ 官网: 。 +RabbitMQ official site: . -RabbitMQ 更新记录(可以直观看到项目是否还在维护): +RabbitMQ update records (to visually check if the project is still maintained): #### Pulsar ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/pulsar-logo.png) -Pulsar 是下一代云原生分布式消息流平台,最初由 Yahoo 开发 ,已经成为 Apache 顶级项目。 +Pulsar is a next-generation cloud-native distributed messaging streaming platform, initially developed by Yahoo, and has become an Apache top-level project. -Pulsar 集消息、存储、轻量化函数式计算为一体,采用计算与存储分离架构设计,支持多租户、持久化存储、多机房跨区域数据复制,具有强一致性、高吞吐、低延时及高可扩展性等流数据存储特性,被看作是云原生时代实时消息流传输、存储和计算最佳解决方案。 +Pulsar integrates messaging, storage, and lightweight function-based computing, adopting a compute-storage separation architecture design, and supports multi-tenancy, persistent storage, and cross-region data replication, offering strong consistency, high throughput, low latency, and high scalability characteristics for streaming data storage. It is considered one of the best solutions for real-time message transmission, storage, and computation in the cloud-native era. -Pulsar 的关键特性如下(摘自官网): +Key features of Pulsar include (from the official site): -- 是下一代云原生分布式消息流平台。 -- Pulsar 的单个实例原生支持多个集群,可跨机房在集群间无缝地完成消息复制。 -- 极低的发布延迟和端到端延迟。 -- 可无缝扩展到超过一百万个 topic。 -- 简单的客户端 API,支持 Java、Go、Python 和 C++。 -- 主题的多种订阅模式(独占、共享和故障转移)。 -- 通过 Apache BookKeeper 提供的持久化消息存储机制保证消息传递 。 -- 由轻量级的 serverless 计算框架 Pulsar Functions 实现流原生的数据处理。 -- 基于 Pulsar Functions 的 serverless connector 框架 Pulsar IO 使得数据更易移入、移出 Apache Pulsar。 -- 分层式存储可在数据陈旧时,将数据从热存储卸载到冷/长期存储(如 S3、GCS)中。 +- A next-generation cloud-native distributed messaging streaming platform. +- A single instance of Pulsar natively supports multiple clusters and can seamlessly replicate messages across regions. +- Extremely low publish latency and end-to-end latency. +- Seamless scalability to over one million topics. +- A simple client API that supports Java, Go, Python, and C++. +- Various subscription modes for topics (exclusive, shared, and failover). +- A persistent message storage mechanism guaranteed by Apache BookKeeper. +- Stream native data processing realized by the lightweight serverless computing framework Pulsar Functions. +- A serverless connector framework based on Pulsar Functions called Pulsar IO makes it easier to move data into and out of Apache Pulsar. +- Layered storage allows data to be offloaded from hot storage to cold/long-term storage (such as S3, GCS) as it becomes stale. -Pulsar 官网: +Pulsar official site: -Pulsar 更新记录(可以直观看到项目是否还在维护): +Pulsar update records (to visually check if the project is still maintained): #### ActiveMQ -目前已经被淘汰,不推荐使用,不建议学习。 +Currently deprecated; not recommended for use or learning. -### 如何选择? +### How to Choose? -> 参考《Java 工程师面试突击第 1 季-中华石杉老师》 +> Refer to "Java Engineer Interview Quick Strike Season 1 - Teacher Zhonghua Shishan" -| 对比方向 | 概要 | -| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 吞吐量 | 万级的 ActiveMQ 和 RabbitMQ 的吞吐量(ActiveMQ 的性能最差)要比十万级甚至是百万级的 RocketMQ 和 Kafka 低一个数量级。 | -| 可用性 | 都可以实现高可用。ActiveMQ 和 RabbitMQ 都是基于主从架构实现高可用性。RocketMQ 基于分布式架构。 Kafka 也是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | -| 时效性 | RabbitMQ 基于 Erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级,其他几个都是 ms 级。 | -| 功能支持 | Pulsar 的功能更全面,支持多租户、多种消费模式和持久性模式等功能,是下一代云原生分布式消息流平台。 | -| 消息丢失 | ActiveMQ 和 RabbitMQ 丢失的可能性非常低, Kafka、RocketMQ 和 Pulsar 理论上可以做到 0 丢失。 | +| Comparison Aspect | Overview | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Throughput | The throughput of ActiveMQ and RabbitMQ (with ActiveMQ being the worst performer) at tens of thousands is an order of magnitude lower than that of RocketMQ and Kafka at hundreds of thousands or even millions. | +| Availability | All can achieve high availability. ActiveMQ and RabbitMQ implement high availability based on a master-slave architecture, while RocketMQ is based on a distributed architecture. Kafka is also distributed with multiple data replicas, meaning that data loss is unlikely even if a small number of machines fail. | +| Timeliness | RabbitMQ, being developed on Erlang, has strong concurrency capabilities and performs exceptionally well, achieving microsecond latency, while the others range in millisecond latency. | +| Feature Support | Pulsar offers more comprehensive features, supporting multi-tenancy, various consumption modes, persistent modes, etc., and is the next-generation cloud-native distributed messaging stream platform. | +| Message Loss | The likelihood of message loss in ActiveMQ and RabbitMQ is very low; Kafka, RocketMQ, and Pulsar can theoretically achieve zero loss. | -**总结:** +**Summary:** -- ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使用,已经被淘汰了。 -- RabbitMQ 在吞吐量方面虽然稍逊于 Kafka、RocketMQ 和 Pulsar,但是由于它基于 Erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 Erlang 开发,所以国内很少有公司有实力做 Erlang 源码级别的研究和定制。如果业务场景对并发量要求不是太高(十万级、百万级),那这几种消息队列中,RabbitMQ 或许是你的首选。 -- RocketMQ 和 Pulsar 支持强一致性,对消息一致性要求比较高的场景可以使用。 -- RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的 MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。 -- Kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 Kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。Kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 +- ActiveMQ has a mature community but is currently underperforming compared to others and experiences slow version iterations, leading to its deprecation. +- Although RabbitMQ's throughput lags behind Kafka, RocketMQ, and Pulsar, its concurrent capabilities are robustly supported by Erlang, leading to exceptional performance and low latency. However, few companies in China possess the capability to conduct Erlang-level code research and customizations. If your business scenario does not demand extremely high levels of concurrency (like hundreds of thousands or millions), RabbitMQ could be your first choice among these message queues. +- RocketMQ and Pulsar support strong consistency, making them suitable for scenarios demanding high message consistency. +- RocketMQ is produced by Alibaba, is an open-source project within the Java ecosystem, allowing one to read its source code directly and tailor it to company needs. RocketMQ has also undergone practical tests in Alibaba's business scenarios. +- Kafka's characteristics are notable: it focuses on providing fewer core functionalities but achieves extremely high throughput, ms-level latency, and exceptional availability and reliability, with flexible scalability. Kafka operates best with fewer topic counts to maintain its super high throughput. The only downside is the potential for message duplicates, which may have a minimal impact on data accuracy. However, this slight impact is negligible in the domain of big data and log collection. Kafka is indeed the industry standard for real-time computing and log collection in big data scenarios, with a highly active community that is unlikely to stagnate, making it the de facto norm worldwide in this field. -## 参考 +## References -- 《大型网站技术架构 》 -- KRaft: Apache Kafka Without ZooKeeper: -- 消息队列的使用场景是什么样的?: +- "Technical Architecture of Large-Scale Websites" +- KRaft: Apache Kafka Without ZooKeeper: +- What Are the Usage Scenarios for Message Queues?: diff --git a/docs/high-performance/message-queue/rabbitmq-questions.md b/docs/high-performance/message-queue/rabbitmq-questions.md index e971eaf3604..ed33a2a4d80 100644 --- a/docs/high-performance/message-queue/rabbitmq-questions.md +++ b/docs/high-performance/message-queue/rabbitmq-questions.md @@ -1,247 +1,57 @@ --- -title: RabbitMQ常见问题总结 -category: 高性能 +title: Summary of Common RabbitMQ Issues +category: High Performance tag: - - 消息队列 + - Message Queue head: - - - meta - - name: keywords - content: RabbitMQ,AMQP,Broker,Exchange,优先级队列,延迟队列 - - - meta - - name: description - content: RabbitMQ 是一个在 AMQP(Advanced Message Queuing Protocol )基础上实现的,可复用的企业消息系统。它可以用于大型软件系统各个模块之间的高效通信,支持高并发,支持可扩展。它支持多种客户端如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX,持久化,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。 + - - meta + - name: keywords + content: RabbitMQ, AMQP, Broker, Exchange, Priority Queue, Delay Queue + - - meta + - name: description + content: RabbitMQ is a reusable enterprise messaging system built on the Advanced Message Queuing Protocol (AMQP). It can be used for efficient communication between various modules of large software systems, supports high concurrency, and is scalable. It supports multiple clients such as Python, Ruby, .NET, Java, JMS, C, PHP, ActionScript, XMPP, STOMP, etc., supports AJAX, persistence, and is used for storing and forwarding messages in distributed systems, performing well in usability, scalability, and high availability. --- -> 本篇文章由 JavaGuide 收集自网络,原出处不明。 +> This article is collected from the internet by JavaGuide, original source unknown. -## RabbitMQ 是什么? +## What is RabbitMQ? -RabbitMQ 是一个在 AMQP(Advanced Message Queuing Protocol )基础上实现的,可复用的企业消息系统。它可以用于大型软件系统各个模块之间的高效通信,支持高并发,支持可扩展。它支持多种客户端如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP 等,支持 AJAX,持久化,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。 +RabbitMQ is a reusable enterprise messaging system built on the Advanced Message Queuing Protocol (AMQP). It can be used for efficient communication between various modules of large software systems, supports high concurrency, and is scalable. It supports multiple clients such as Python, Ruby, .NET, Java, JMS, C, PHP, ActionScript, XMPP, STOMP, etc., supports AJAX, persistence, and is used for storing and forwarding messages in distributed systems, performing well in usability, scalability, and high availability. -RabbitMQ 是使用 Erlang 编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。它同时实现了一个 Broker 构架,这意味着消息在发送给客户端时先在中心队列排队,对路由(Routing)、负载均衡(Load balance)或者数据持久化都有很好的支持。 +RabbitMQ is an open-source message queue written in Erlang, supporting many protocols: AMQP, XMPP, SMTP, STOMP. This makes it very heavyweight and more suitable for enterprise-level development. It also implements a Broker architecture, which means that messages are queued in a central queue before being sent to clients, providing good support for routing, load balancing, and data persistence. -PS:也可能直接问什么是消息队列?消息队列就是一个使用队列来通信的组件。 +PS: You may also directly ask what a message queue is? A message queue is a component that uses a queue for communication. -## RabbitMQ 特点? +## What are the features of RabbitMQ? -- **可靠性**: RabbitMQ 使用一些机制来保证可靠性, 如持久化、传输确认及发布确认等。 -- **灵活的路由** : 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能, RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起, 也可以通过插件机制来实现自己的交换器。 -- **扩展性**: 多个 RabbitMQ 节点可以组成一个集群,也可以根据实际业务情况动态地扩展 集群中节点。 -- **高可用性** : 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队 列仍然可用。 -- **多种协议**: RabbitMQ 除了原生支持 AMQP 协议,还支持 STOMP, MQTT 等多种消息 中间件协议。 -- **多语言客户端** :RabbitMQ 几乎支持所有常用语言,比如 Java、 Python、 Ruby、 PHP、 C#、 JavaScript 等。 -- **管理界面** : RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集 群中的节点等。 -- **插件机制** : RabbitMQ 提供了许多插件 , 以实现从多方面进行扩展,当然也可以编写自 己的插件。 +- **Reliability**: RabbitMQ uses mechanisms to ensure reliability, such as persistence, delivery confirmations, and publish confirmations. +- **Flexible Routing**: Messages are routed through an exchange before entering the queue. RabbitMQ provides several built-in exchanges for typical routing functions. For more complex routing, multiple exchanges can be bound together, or custom exchanges can be implemented through a plugin mechanism. +- **Scalability**: Multiple RabbitMQ nodes can form a cluster and can dynamically expand nodes in the cluster based on actual business needs. +- **High Availability**: Queues can be mirrored across machines in the cluster, ensuring that queues remain available even if some nodes encounter issues. +- **Multiple Protocols**: In addition to natively supporting the AMQP protocol, RabbitMQ also supports various middleware protocols such as STOMP and MQTT. +- **Multi-language Clients**: RabbitMQ supports almost all commonly used languages, such as Java, Python, Ruby, PHP, C#, JavaScript, etc. +- **Management Interface**: RabbitMQ provides an easy-to-use user interface for monitoring and managing messages, nodes in the cluster, etc. +- **Plugin Mechanism**: RabbitMQ offers many plugins for various extensions, and users can also write their own plugins. -## RabbitMQ 核心概念? +## What are the core concepts of RabbitMQ? -RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。可以把消息传递的过程想象成:当你将一个包裹送到邮局,邮局会暂存并最终将邮件通过邮递员送到收件人的手上,RabbitMQ 就好比由邮局、邮箱和邮递员组成的一个系统。从计算机术语层面来说,RabbitMQ 模型更像是一种交换机模型。 +Overall, RabbitMQ is a producer-consumer model responsible for receiving, storing, and forwarding messages. You can imagine the message delivery process as follows: when you send a package to the post office, the post office temporarily stores it and eventually delivers it to the recipient through a mail carrier. RabbitMQ is like a system composed of a post office, mailboxes, and mail carriers. From a computer terminology perspective, the RabbitMQ model resembles a switch model. -RabbitMQ 的整体模型架构如下: +The overall model architecture of RabbitMQ is as follows: -![图1-RabbitMQ 的整体模型架构](https://oss.javaguide.cn/github/javaguide/rabbitmq/96388546.jpg) +![Figure 1 - Overall Model Architecture of RabbitMQ](https://oss.javaguide.cn/github/javaguide/rabbitmq/96388546.jpg) -下面我会一一介绍上图中的一些概念。 +Below, I will introduce some concepts from the above diagram one by one. -### Producer(生产者) 和 Consumer(消费者) +### Producer and Consumer -- **Producer(生产者)** :生产消息的一方(邮件投递者) -- **Consumer(消费者)** :消费消息的一方(邮件收件人) +- **Producer**: The party that produces messages (mail sender). +- **Consumer**: The party that consumes messages (mail recipient). -消息一般由 2 部分组成:**消息头**(或者说是标签 Label)和 **消息体**。消息体也可以称为 payLoad ,消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括 routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。生产者把消息交由 RabbitMQ 后,RabbitMQ 会根据消息头把消息发送给感兴趣的 Consumer(消费者)。 +Messages generally consist of two parts: **message header** (or label) and **message body**. The message body can also be called payload; it is opaque, while the message header consists of a series of optional attributes, including routing-key, priority (relative to other messages), delivery-mode (indicating whether the message may require persistent storage), etc. After the producer hands the message to RabbitMQ, RabbitMQ sends the message to interested consumers based on the message header. -### Exchange(交换器) +### Exchange -在 RabbitMQ 中,消息并不是直接被投递到 **Queue(消息队列)** 中的,中间还必须经过 **Exchange(交换器)** 这一层,**Exchange(交换器)** 会把我们的消息分配到对应的 **Queue(消息队列)** 中。 +In RabbitMQ, messages are not delivered directly to the **Queue**; they must first pass through the **Exchange**, which assigns our messages to the corresponding **Queue**. -**Exchange(交换器)** 用来接收生产者发送的消息并将这些消息路由给服务器中的队列中,如果路由不到,或许会返回给 **Producer(生产者)** ,或许会被直接丢弃掉 。这里可以将 RabbitMQ 中的交换器看作一个简单的实体。 - -**RabbitMQ 的 Exchange(交换器) 有 4 种类型,不同的类型对应着不同的路由策略**:**direct(默认)**,**fanout**, **topic**, 和 **headers**,不同类型的 Exchange 转发消息的策略有所区别。这个会在介绍 **Exchange Types(交换器类型)** 的时候介绍到。 - -Exchange(交换器) 示意图如下: - -![Exchange(交换器) 示意图](https://oss.javaguide.cn/github/javaguide/rabbitmq/24007899.jpg) - -生产者将消息发给交换器的时候,一般会指定一个 **RoutingKey(路由键)**,用来指定这个消息的路由规则,而这个 **RoutingKey 需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效**。 - -RabbitMQ 中通过 **Binding(绑定)** 将 **Exchange(交换器)** 与 **Queue(消息队列)** 关联起来,在绑定的时候一般会指定一个 **BindingKey(绑定建)** ,这样 RabbitMQ 就知道如何正确将消息路由到队列了,如下图所示。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和 Queue 的绑定可以是多对多的关系。 - -Binding(绑定) 示意图: - -![Binding(绑定) 示意图](https://oss.javaguide.cn/github/javaguide/rabbitmq/70553134.jpg) - -生产者将消息发送给交换器时,需要一个 RoutingKey,当 BindingKey 和 RoutingKey 相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的 BindingKey。BindingKey 并不是在所有的情况下都生效,它依赖于交换器类型,比如 fanout 类型的交换器就会无视,而是将消息路由到所有绑定到该交换器的队列中。 - -### Queue(消息队列) - -**Queue(消息队列)** 用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。 - -**RabbitMQ** 中消息只能存储在 **队列** 中,这一点和 **Kafka** 这种消息中间件相反。Kafka 将消息存储在 **topic(主题)** 这个逻辑层面,而相对应的队列逻辑只是 topic 实际存储文件中的位移标识。 RabbitMQ 的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。 - -**多个消费者可以订阅同一个队列**,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,这样避免消息被重复消费。 - -**RabbitMQ** 不支持队列层面的广播消费,如果有广播消费的需求,需要在其上进行二次开发,这样会很麻烦,不建议这样做。 - -### Broker(消息中间件的服务节点) - -对于 RabbitMQ 来说,一个 RabbitMQ Broker 可以简单地看作一个 RabbitMQ 服务节点,或者 RabbitMQ 服务实例。大多数情况下也可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。 - -下图展示了生产者将消息存入 RabbitMQ Broker,以及消费者从 Broker 中消费数据的整个流程。 - -![消息队列的运转过程](https://oss.javaguide.cn/github/javaguide/rabbitmq/67952922.jpg) - -这样图 1 中的一些关于 RabbitMQ 的基本概念我们就介绍完毕了,下面再来介绍一下 **Exchange Types(交换器类型)** 。 - -### Exchange Types(交换器类型) - -RabbitMQ 常用的 Exchange Type 有 **fanout**、**direct**、**topic**、**headers** 这四种(AMQP 规范里还提到两种 Exchange Type,分别为 system 与 自定义,这里不予以描述)。 - -**1、fanout** - -fanout 类型的 Exchange 路由规则非常简单,它会把所有发送到该 Exchange 的消息路由到所有与它绑定的 Queue 中,不需要做任何判断操作,所以 fanout 类型是所有的交换机类型里面速度最快的。fanout 类型常用来广播消息。 - -**2、direct** - -direct 类型的 Exchange 路由规则也很简单,它会把消息路由到那些 Bindingkey 与 RoutingKey 完全匹配的 Queue 中。 - -![direct 类型交换器](https://oss.javaguide.cn/github/javaguide/rabbitmq/37008021.jpg) - -以上图为例,如果发送消息的时候设置路由键为“warning”,那么消息会路由到 Queue1 和 Queue2。如果在发送消息的时候设置路由键为"Info”或者"debug”,消息只会路由到 Queue2。如果以其他的路由键发送消息,则消息不会路由到这两个队列中。 - -direct 类型常用在处理有优先级的任务,根据任务的优先级把消息发送到对应的队列,这样可以指派更多的资源去处理高优先级的队列。 - -**3、topic** - -前面讲到 direct 类型的交换器路由规则是完全匹配 BindingKey 和 RoutingKey ,但是这种严格的匹配方式在很多情况下不能满足实际业务的需求。topic 类型的交换器在匹配规则上进行了扩展,它与 direct 类型的交换器相似,也是将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中,但这里的匹配规则有些不同,它约定: - -- RoutingKey 为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词),如 “com.rabbitmq.client”、“java.util.concurrent”、“com.hidden.client”; -- BindingKey 和 RoutingKey 一样也是点号“.”分隔的字符串; -- BindingKey 中可以存在两种特殊字符串“\*”和“#”,用于做模糊匹配,其中“\*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。 - -![topic 类型交换器](https://oss.javaguide.cn/github/javaguide/rabbitmq/73843.jpg) - -以上图为例: - -- 路由键为 “com.rabbitmq.client” 的消息会同时路由到 Queue1 和 Queue2; -- 路由键为 “com.hidden.client” 的消息只会路由到 Queue2 中; -- 路由键为 “com.hidden.demo” 的消息只会路由到 Queue2 中; -- 路由键为 “java.rabbitmq.demo” 的消息只会路由到 Queue1 中; -- 路由键为 “java.util.concurrent” 的消息将会被丢弃或者返回给生产者(需要设置 mandatory 参数),因为它没有匹配任何路由键。 - -**4、headers(不推荐)** - -headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。在绑定队列和交换器时指定一组键值对,当发送消息到交换器时,RabbitMQ 会获取到该消息的 headers(也是一个键值对的形式),对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。 - -## AMQP 是什么? - -RabbitMQ 就是 AMQP 协议的 `Erlang` 的实现(当然 RabbitMQ 还支持 `STOMP2`、 `MQTT3` 等协议 ) AMQP 的模型架构 和 RabbitMQ 的模型架构是一样的,生产者将消息发送给交换器,交换器和队列绑定 。 - -RabbitMQ 中的交换器、交换器类型、队列、绑定、路由键等都是遵循的 AMQP 协议中相 应的概念。目前 RabbitMQ 最新版本默认支持的是 AMQP 0-9-1。 - -**AMQP 协议的三层**: - -- **Module Layer**:协议最高层,主要定义了一些客户端调用的命令,客户端可以用这些命令实现自己的业务逻辑。 -- **Session Layer**:中间层,主要负责客户端命令发送给服务器,再将服务端应答返回客户端,提供可靠性同步机制和错误处理。 -- **TransportLayer**:最底层,主要传输二进制数据流,提供帧的处理、信道复用、错误检测和数据表示等。 - -**AMQP 模型的三大组件**: - -- **交换器 (Exchange)**:消息代理服务器中用于把消息路由到队列的组件。 -- **队列 (Queue)**:用来存储消息的数据结构,位于硬盘或内存中。 -- **绑定 (Binding)**:一套规则,告知交换器消息应该将消息投递给哪个队列。 - -## **说说生产者 Producer 和消费者 Consumer?** - -**生产者** : - -- 消息生产者,就是投递消息的一方。 -- 消息一般包含两个部分:消息体(`payload`)和标签(`Label`)。 - -**消费者**: - -- 消费消息,也就是接收消息的一方。 -- 消费者连接到 RabbitMQ 服务器,并订阅到队列上。消费消息时只消费消息体,丢弃标签。 - -## 说说 Broker 服务节点、Queue 队列、Exchange 交换器? - -- **Broker**:可以看做 RabbitMQ 的服务节点。一般情况下一个 Broker 可以看做一个 RabbitMQ 服务器。 -- **Queue**:RabbitMQ 的内部对象,用于存储消息。多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。 -- **Exchange**:生产者将消息发送到交换器,由交换器将消息路由到一个或者多个队列中。当路由不到时,或返回给生产者或直接丢弃。 - -## 什么是死信队列?如何导致的? - -DLX,全称为 `Dead-Letter-Exchange`,死信交换器,死信邮箱。当消息在一个队列中变成死信 (`dead message`) 之后,它能被重新发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。 - -**导致的死信的几种原因**: - -- 消息被拒(`Basic.Reject /Basic.Nack`) 且 `requeue = false`。 -- 消息 TTL 过期。 -- 队列满了,无法再添加。 - -## 什么是延迟队列?RabbitMQ 怎么实现延迟队列? - -延迟队列指的是存储对应的延迟消息,消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。 - -RabbitMQ 本身是没有延迟队列的,要实现延迟消息,一般有两种方式: - -1. 通过 RabbitMQ 本身队列的特性来实现,需要使用 RabbitMQ 的死信交换机(Exchange)和消息的存活时间 TTL(Time To Live)。 -2. 在 RabbitMQ 3.5.7 及以上的版本提供了一个插件(rabbitmq-delayed-message-exchange)来实现延迟队列功能。同时,插件依赖 Erlang/OPT 18.0 及以上。 - -也就是说,AMQP 协议以及 RabbitMQ 本身没有直接支持延迟队列的功能,但是可以通过 TTL 和 DLX 模拟出延迟队列的功能。 - -## 什么是优先级队列? - -RabbitMQ 自 V3.5.0 有优先级队列实现,优先级高的队列会先被消费。 - -可以通过`x-max-priority`参数来实现优先级队列。不过,当消费速度大于生产速度且 Broker 没有堆积的情况下,优先级显得没有意义。 - -## RabbitMQ 有哪些工作模式? - -- 简单模式 -- work 工作模式 -- pub/sub 发布订阅模式 -- Routing 路由模式 -- Topic 主题模式 - -## RabbitMQ 消息怎么传输? - -由于 TCP 链接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈,所以 RabbitMQ 使用信道的方式来传输数据。信道(Channel)是生产者、消费者与 RabbitMQ 通信的渠道,信道是建立在 TCP 链接上的虚拟链接,且每条 TCP 链接上的信道数量没有限制。就是说 RabbitMQ 在一条 TCP 链接上建立成百上千个信道来达到多个线程处理,这个 TCP 被多个线程共享,每个信道在 RabbitMQ 都有唯一的 ID,保证了信道私有性,每个信道对应一个线程使用。 - -## **如何保证消息的可靠性?** - -消息到 MQ 的过程中搞丢,MQ 自己搞丢,MQ 到消费过程中搞丢。 - -- 生产者到 RabbitMQ:事务机制和 Confirm 机制,注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错。 -- RabbitMQ 自身:持久化、集群、普通模式、镜像模式。 -- RabbitMQ 到消费者:basicAck 机制、死信队列、消息补偿机制。 - -## 如何保证 RabbitMQ 消息的顺序性? - -- 拆分多个 queue(消息队列),每个 queue(消息队列) 一个 consumer(消费者),就是多一些 queue (消息队列)而已,确实是麻烦点; -- 或者就一个 queue (消息队列)但是对应一个 consumer(消费者),然后这个 consumer(消费者)内部用内存队列做排队,然后分发给底层不同的 worker 来处理。 - -## 如何保证 RabbitMQ 高可用的? - -RabbitMQ 是比较有代表性的,因为是基于主从(非分布式)做高可用性的,我们就以 RabbitMQ 为例子讲解第一种 MQ 的高可用性怎么实现。RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。 - -**单机模式** - -Demo 级别的,一般就是你本地启动了玩玩儿的?,没人生产用单机模式。 - -**普通集群模式** - -意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。 - -你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个 queue 的读写操作。 - -**镜像集群模式** - -这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。 - -这样的好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。 - -## 如何解决消息队列的延时以及过期失效问题? - -RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上 12 点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。 - - +The **Exchange** receives messages sent by producers and routes them to queues in the server. If routing fails, the message may be returned to the **Producer** or discarded diff --git a/docs/high-performance/message-queue/rocketmq-questions.md b/docs/high-performance/message-queue/rocketmq-questions.md index 9591e5d2612..ce22d44f350 100644 --- a/docs/high-performance/message-queue/rocketmq-questions.md +++ b/docs/high-performance/message-queue/rocketmq-questions.md @@ -1,934 +1,54 @@ --- -title: RocketMQ常见问题总结 -category: 高性能 +title: Summary of Common Issues with RocketMQ +category: High Performance tag: - RocketMQ - - 消息队列 + - Message Queue --- -> [本文由 FrancisQ 投稿!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485969&idx=1&sn=6bd53abde30d42a778d5a35ec104428c&chksm=cea245daf9d5cccce631f93115f0c2c4a7634e55f5bef9009fd03f5a0ffa55b745b5ef4f0530&token=294077121&lang=zh_CN#rd) 相比原文主要进行了下面这些完善: +> [This article is contributed by FrancisQ!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485969&idx=1&sn=6bd53abde30d42a778d5a35ec104428c&chksm=cea245daf9d5cccce631f93115f0c2c4a7634e55f5bef9009fd03f5a0ffa55b745b5ef4f0530&token=294077121&lang=zh_CN#rd) Compared to the original text, the following improvements have been made: > -> - [分析了 RocketMQ 高性能读写的原因和顺序消费的具体实现](https://github.com/Snailclimb/JavaGuide/pull/2133) -> - [增加了消息类型、消费者类型、消费者组和生产者组的介绍](https://github.com/Snailclimb/JavaGuide/pull/2134) +> - [Analyzed the reasons for RocketMQ's high-performance read and write and the specific implementation of sequential consumption](https://github.com/Snailclimb/JavaGuide/pull/2133) +> - [Added introductions to message types, consumer types, consumer groups, and producer groups](https://github.com/Snailclimb/JavaGuide/pull/2134) -## 消息队列扫盲 +## Introduction to Message Queues -消息队列顾名思义就是存放消息的队列,队列我就不解释了,别告诉我你连队列都不知道是啥吧? +A message queue, as the name suggests, is a queue that stores messages. I won't explain what a queue is; please don't tell me you don't even know what a queue is! -所以问题并不是消息队列是什么,而是 **消息队列为什么会出现?消息队列能用来干什么?用它来干这些事会带来什么好处?消息队列会带来副作用吗?** +So the question is not what a message queue is, but rather **why do message queues exist? What can message queues be used for? What benefits do they bring? Do message queues have side effects?** -### 消息队列为什么会出现? +### Why Do Message Queues Exist? -消息队列算是作为后端程序员的一个必备技能吧,因为**分布式应用必定涉及到各个系统之间的通信问题**,这个时候消息队列也应运而生了。可以说分布式的产生是消息队列的基础,而分布式怕是一个很古老的概念了吧,所以消息队列也是一个很古老的中间件了。 +Message queues are considered an essential skill for backend developers because **distributed applications inevitably involve communication between various systems**, and this is where message queues come into play. It can be said that the emergence of distributed systems is the foundation of message queues, and distributed systems are quite an old concept, making message queues also a very old middleware. -### 消息队列能用来干什么? +### What Can Message Queues Be Used For? -#### 异步 +#### Asynchronous Communication -你可能会反驳我,应用之间的通信又不是只能由消息队列解决,好好的通信为什么中间非要插一个消息队列呢?我不能直接进行通信吗? +You might argue that communication between applications can be solved without message queues. Why insert a message queue into the middle of a good communication process? Can't I communicate directly? -很好 👍,你又提出了一个概念,**同步通信**。就比如现在业界使用比较多的 `Dubbo` 就是一个适用于各个系统之间同步通信的 `RPC` 框架。 +Good point 👍, you've introduced the concept of **synchronous communication**. For example, `Dubbo`, which is widely used in the industry, is an `RPC` framework suitable for synchronous communication between various systems. -我来举个 🌰 吧,比如我们有一个购票系统,需求是用户在购买完之后能接收到购买完成的短信。 +Let me give you an example 🌰. Suppose we have a ticketing system where the requirement is that users receive a text message after completing their purchase. ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef37fee7e09230.jpg) -我们省略中间的网络通信时间消耗,假如购票系统处理需要 150ms ,短信系统处理需要 200ms ,那么整个处理流程的时间消耗就是 150ms + 200ms = 350ms。 +If we omit the time consumed by network communication, let's say the ticketing system takes 150ms to process, and the SMS system takes 200ms, then the total processing time is 150ms + 200ms = 350ms. -当然,乍看没什么问题。可是仔细一想你就感觉有点问题,我用户购票在购票系统的时候其实就已经完成了购买,而我现在通过同步调用非要让整个请求拉长时间,而短信系统这玩意又不是很有必要,它仅仅是一个辅助功能增强用户体验感而已。我现在整个调用流程就有点 **头重脚轻** 的感觉了,购票是一个不太耗时的流程,而我现在因为同步调用,非要等待发送短信这个比较耗时的操作才返回结果。那我如果再加一个发送邮件呢? +At first glance, there seems to be no problem. However, upon closer inspection, you might feel something is off. When the user completes the purchase in the ticketing system, they have already finished the purchase. Yet, through synchronous calls, the entire request is unnecessarily prolonged because we have to wait for the SMS system, which is not essential; it merely enhances user experience. The entire call process feels a bit **top-heavy**; purchasing is not a time-consuming process, but now, due to synchronous calls, we have to wait for the more time-consuming operation of sending the SMS to return a result. What if I add an email notification? ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef380429cf373e.jpg) -这样整个系统的调用链又变长了,整个时间就变成了 550ms。 +Now the entire system's call chain has lengthened, and the total time has become 550ms. -当我们在学生时代需要在食堂排队的时候,我们和食堂大妈就是一个同步的模型。 +When we were students and had to queue in the cafeteria, we were in a synchronous model with the cafeteria lady. -我们需要告诉食堂大妈:“姐姐,给我加个鸡腿,再加个酸辣土豆丝,帮我浇点汁上去,多打点饭哦 😋😋😋” 咦~~~ 为了多吃点,真恶心。 +We need to tell the cafeteria lady: "Sister, please add a chicken leg, and some spicy potato shreds, and pour some sauce on top, and give me extra rice 😋😋😋." Oh\~~~ To eat more, how disgusting. -然后大妈帮我们打饭配菜,我们看着大妈那颤抖的手和掉落的土豆丝不禁咽了咽口水。 +Then the lady helps us with the rice and dishes, and we can't help but swallow as we watch her trembling hands and the falling potato shreds. -最终我们从大妈手中接过饭菜然后去寻找座位了... +Finally, we take the food from her and go look for a seat... -回想一下,我们在给大妈发送需要的信息之后我们是 **同步等待大妈给我配好饭菜** 的,上面我们只是加了鸡腿和土豆丝,万一我再加一个番茄牛腩,韭菜鸡蛋,这样是不是大妈打饭配菜的流程就会变长,我们等待的时间也会相应的变长。 +Reflecting on it, after we send the necessary information to the lady, we are **synchronously waiting for her to prepare our food**. We only added chicken legs and potato shreds; what if I add a tomato beef brisket and chives with eggs? Wouldn't the process of the lady preparing the food take longer, and our waiting time would also increase accordingly? -那后来,我们工作赚钱了有钱去饭店吃饭了,我们告诉服务员来一碗牛肉面加个荷包蛋 **(传达一个消息)** ,然后我们就可以在饭桌上安心的玩手机了 **(干自己其他事情)** ,等到我们的牛肉面上了我们就可以吃了。这其中我们也就传达了一个消息,然后我们又转过头干其他事情了。这其中虽然做面的时间没有变短,但是我们只需要传达一个消息就可以干其他事情了,这是一个 **异步** 的概念。 - -所以,为了解决这一个问题,聪明的程序员在中间也加了个类似于服务员的中间件——消息队列。这个时候我们就可以把模型给改造了。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef38124f55eaea.jpg) - -这样,我们在将消息存入消息队列之后我们就可以直接返回了(我们告诉服务员我们要吃什么然后玩手机),所以整个耗时只是 150ms + 10ms = 160ms。 - -> 但是你需要注意的是,整个流程的时长是没变的,就像你仅仅告诉服务员要吃什么是不会影响到做面的速度的。 - -#### 解耦 - -回到最初同步调用的过程,我们写个伪代码简单概括一下。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef381a505d3e1f.jpg) - -那么第二步,我们又添加了一个发送邮件,我们就得重新去修改代码,如果我们又加一个需求:用户购买完还需要给他加积分,这个时候我们是不是又得改代码? - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef381c4e1b1ac7.jpg) - -如果你觉得还行,那么我这个时候不要发邮件这个服务了呢,我是不是又得改代码,又得重启应用? - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef381f273a66bd.jpg) - -这样改来改去是不是很麻烦,那么 **此时我们就用一个消息队列在中间进行解耦** 。你需要注意的是,我们后面的发送短信、发送邮件、添加积分等一些操作都依赖于上面的 `result` ,这东西抽象出来就是购票的处理结果呀,比如订单号,用户账号等等,也就是说我们后面的一系列服务都是需要同样的消息来进行处理。既然这样,我们是不是可以通过 **“广播消息”** 来实现。 - -我上面所讲的“广播”并不是真正的广播,而是接下来的系统作为消费者去 **订阅** 特定的主题。比如我们这里的主题就可以叫做 `订票` ,我们购买系统作为一个生产者去生产这条消息放入消息队列,然后消费者订阅了这个主题,会从消息队列中拉取消息并消费。就比如我们刚刚画的那张图,你会发现,在生产者这边我们只需要关注 **生产消息到指定主题中** ,而 **消费者只需要关注从指定主题中拉取消息** 就行了。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef382674b66892.jpg) - -> 如果没有消息队列,每当一个新的业务接入,我们都要在主系统调用新接口、或者当我们取消某些业务,我们也得在主系统删除某些接口调用。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,接下来收到消息如何处理,是下游的事情,无疑极大地减少了开发和联调的工作量。 - -#### 削峰 - -我们再次回到一开始我们使用同步调用系统的情况,并且思考一下,如果此时有大量用户请求购票整个系统会变成什么样? - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef382a9756bb1c.jpg) - -如果,此时有一万的请求进入购票系统,我们知道运行我们主业务的服务器配置一般会比较好,所以这里我们假设购票系统能承受这一万的用户请求,那么也就意味着我们同时也会出现一万调用发短信服务的请求。而对于短信系统来说并不是我们的主要业务,所以我们配备的硬件资源并不会太高,那么你觉得现在这个短信系统能承受这一万的峰值么,且不说能不能承受,系统会不会 **直接崩溃** 了? - -短信业务又不是我们的主业务,我们能不能 **折中处理** 呢?如果我们把购买完成的信息发送到消息队列中,而短信系统 **尽自己所能地去消息队列中取消息和消费消息** ,即使处理速度慢一点也无所谓,只要我们的系统没有崩溃就行了。 - -留得江山在,还怕没柴烧?你敢说每次发送验证码的时候是一发你就收到了的么? - -#### 消息队列能带来什么好处? - -其实上面我已经说了。**异步、解耦、削峰。** 哪怕你上面的都没看懂也千万要记住这六个字,因为他不仅是消息队列的精华,更是编程和架构的精华。 - -#### 消息队列会带来副作用吗? - -没有哪一门技术是“银弹”,消息队列也有它的副作用。 - -比如,本来好好的两个系统之间的调用,我中间加了个消息队列,如果消息队列挂了怎么办呢?是不是 **降低了系统的可用性** ? - -那这样是不是要保证 HA(高可用)?是不是要搞集群?那么我 **整个系统的复杂度是不是上升了** ? - -抛开上面的问题不讲,万一我发送方发送失败了,然后执行重试,这样就可能产生重复的消息。 - -或者我消费端处理失败了,请求重发,这样也会产生重复的消息。 - -对于一些微服务来说,消费重复消息会带来更大的麻烦,比如增加积分,这个时候我加了多次是不是对其他用户不公平? - -那么,又 **如何解决重复消费消息的问题** 呢? - -如果我们此时的消息需要保证严格的顺序性怎么办呢?比如生产者生产了一系列的有序消息(对一个 id 为 1 的记录进行删除增加修改),但是我们知道在发布订阅模型中,对于主题是无顺序的,那么这个时候就会导致对于消费者消费消息的时候没有按照生产者的发送顺序消费,比如这个时候我们消费的顺序为修改删除增加,如果该记录涉及到金额的话是不是会出大事情? - -那么,又 **如何解决消息的顺序消费问题** 呢? - -就拿我们上面所讲的分布式系统来说,用户购票完成之后是不是需要增加账户积分?在同一个系统中我们一般会使用事务来进行解决,如果用 `Spring` 的话我们在上面伪代码中加入 `@Transactional` 注解就好了。但是在不同系统中如何保证事务呢?总不能这个系统我扣钱成功了你那积分系统积分没加吧?或者说我这扣钱明明失败了,你那积分系统给我加了积分。 - -那么,又如何 **解决分布式事务问题** 呢? - -我们刚刚说了,消息队列可以进行削峰操作,那如果我的消费者如果消费很慢或者生产者生产消息很快,这样是不是会将消息堆积在消息队列中? - -那么,又如何 **解决消息堆积的问题** 呢? - -可用性降低,复杂度上升,又带来一系列的重复消费,顺序消费,分布式事务,消息堆积的问题,这消息队列还怎么用啊 😵? - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef382d709abc9d.png) - -别急,办法总是有的。 - -## RocketMQ 是什么? - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef383014430799.jpg) - -哇,你个混蛋!上面给我抛出那么多问题,你现在又讲 `RocketMQ` ,还让不让人活了?!🤬 - -别急别急,话说你现在清楚 `MQ` 的构造吗,我还没讲呢,我们先搞明白 `MQ` 的内部构造,再来看看如何解决上面的一系列问题吧,不过你最好带着问题去阅读和了解喔。 - -`RocketMQ` 是一个 **队列模型** 的消息中间件,具有**高性能、高可靠、高实时、分布式** 的特点。它是一个采用 `Java` 语言开发的分布式的消息系统,由阿里巴巴团队开发,在 2016 年底贡献给 `Apache`,成为了 `Apache` 的一个顶级项目。 在阿里内部,`RocketMQ` 很好地服务了集团大大小小上千个应用,在每年的双十一当天,更有不可思议的万亿级消息通过 `RocketMQ` 流转。 - -废话不多说,想要了解 `RocketMQ` 历史的同学可以自己去搜寻资料。听完上面的介绍,你只要知道 `RocketMQ` 很快、很牛、而且经历过双十一的实践就行了! - -## 队列模型和主题模型是什么? - -在谈 `RocketMQ` 的技术架构之前,我们先来了解一下两个名词概念——**队列模型** 和 **主题模型** 。 - -首先我问一个问题,消息队列为什么要叫消息队列? - -你可能觉得很弱智,这玩意不就是存放消息的队列嘛?不叫消息队列叫什么? - -的确,早期的消息中间件是通过 **队列** 这一模型来实现的,可能是历史原因,我们都习惯把消息中间件成为消息队列。 - -但是,如今例如 `RocketMQ`、`Kafka` 这些优秀的消息中间件不仅仅是通过一个 **队列** 来实现消息存储的。 - -### 队列模型 - -就像我们理解队列一样,消息中间件的队列模型就真的只是一个队列。。。我画一张图给大家理解。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3834ae653469.jpg) - -在一开始我跟你提到了一个 **“广播”** 的概念,也就是说如果我们此时我们需要将一个消息发送给多个消费者(比如此时我需要将信息发送给短信系统和邮件系统),这个时候单个队列即不能满足需求了。 - -当然你可以让 `Producer` 生产消息放入多个队列中,然后每个队列去对应每一个消费者。问题是可以解决,创建多个队列并且复制多份消息是会很影响资源和性能的。而且,这样子就会导致生产者需要知道具体消费者个数然后去复制对应数量的消息队列,这就违背我们消息中间件的 **解耦** 这一原则。 - -### 主题模型 - -那么有没有好的方法去解决这一个问题呢?有,那就是 **主题模型** 或者可以称为 **发布订阅模型** 。 - -> 感兴趣的同学可以去了解一下设计模式里面的观察者模式并且手动实现一下,我相信你会有所收获的。 - -在主题模型中,消息的生产者称为 **发布者(Publisher)** ,消息的消费者称为 **订阅者(Subscriber)** ,存放消息的容器称为 **主题(Topic)** 。 - -其中,发布者将消息发送到指定主题中,订阅者需要 **提前订阅主题** 才能接受特定主题的消息。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3837887d9a54sds.jpg) - -### RocketMQ 中的消息模型 - -`RocketMQ` 中的消息模型就是按照 **主题模型** 所实现的。你可能会好奇这个 **主题** 到底是怎么实现的呢?你上面也没有讲到呀! - -其实对于主题模型的实现来说每个消息中间件的底层设计都是不一样的,就比如 `Kafka` 中的 **分区** ,`RocketMQ` 中的 **队列** ,`RabbitMQ` 中的 `Exchange` 。我们可以理解为 **主题模型/发布订阅模型** 就是一个标准,那些中间件只不过照着这个标准去实现而已。 - -所以,`RocketMQ` 中的 **主题模型** 到底是如何实现的呢?首先我画一张图,大家尝试着去理解一下。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef383d3e8c9788.jpg) - -我们可以看到在整个图中有 `Producer Group`、`Topic`、`Consumer Group` 三个角色,我来分别介绍一下他们。 - -- `Producer Group` 生产者组:代表某一类的生产者,比如我们有多个秒杀系统作为生产者,这多个合在一起就是一个 `Producer Group` 生产者组,它们一般生产相同的消息。 -- `Consumer Group` 消费者组:代表某一类的消费者,比如我们有多个短信系统作为消费者,这多个合在一起就是一个 `Consumer Group` 消费者组,它们一般消费相同的消息。 -- `Topic` 主题:代表一类消息,比如订单消息,物流消息等等。 - -你可以看到图中生产者组中的生产者会向主题发送消息,而 **主题中存在多个队列**,生产者每次生产消息之后是指定主题中的某个队列发送消息的。 - -每个主题中都有多个队列(分布在不同的 `Broker`中,如果是集群的话,`Broker`又分布在不同的服务器中),集群消费模式下,一个消费者集群多台机器共同消费一个 `topic` 的多个队列,**一个队列只会被一个消费者消费**。如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。就像上图中 `Consumer1` 和 `Consumer2` 分别对应着两个队列,而 `Consumer3` 是没有队列对应的,所以一般来讲要控制 **消费者组中的消费者个数和主题中队列个数相同** 。 - -当然也可以消费者个数小于队列个数,只不过不太建议。如下图。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3850c808d707.jpg) - -**每个消费组在每个队列上维护一个消费位置** ,为什么呢? - -因为我们刚刚画的仅仅是一个消费者组,我们知道在发布订阅模式中一般会涉及到多个消费者组,而每个消费者组在每个队列中的消费位置都是不同的。如果此时有多个消费者组,那么消息被一个消费者组消费完之后是不会删除的(因为其它消费者组也需要呀),它仅仅是为每个消费者组维护一个 **消费位移(offset)** ,每次消费者组消费完会返回一个成功的响应,然后队列再把维护的消费位移加一,这样就不会出现刚刚消费过的消息再一次被消费了。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3857fefaa079.jpg) - -可能你还有一个问题,**为什么一个主题中需要维护多个队列** ? - -答案是 **提高并发能力** 。的确,每个主题中只存在一个队列也是可行的。你想一下,如果每个主题中只存在一个队列,这个队列中也维护着每个消费者组的消费位置,这样也可以做到 **发布订阅模式** 。如下图。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef38600cdb6d4b.jpg) - -但是,这样我生产者是不是只能向一个队列发送消息?又因为需要维护消费位置所以一个队列只能对应一个消费者组中的消费者,这样是不是其他的 `Consumer` 就没有用武之地了?从这两个角度来讲,并发度一下子就小了很多。 - -所以总结来说,`RocketMQ` 通过**使用在一个 `Topic` 中配置多个队列并且每个队列维护每个消费者组的消费位置** 实现了 **主题模式/发布订阅模式** 。 - -## RocketMQ 的架构图 - -讲完了消息模型,我们理解起 `RocketMQ` 的技术架构起来就容易多了。 - -`RocketMQ` 技术架构中有四大角色 `NameServer`、`Broker`、`Producer`、`Consumer` 。我来向大家分别解释一下这四个角色是干啥的。 - -- `Broker`:主要负责消息的存储、投递和查询以及服务高可用保证。说白了就是消息队列服务器嘛,生产者生产消息到 `Broker` ,消费者从 `Broker` 拉取消息并消费。 - - 这里,我还得普及一下关于 `Broker`、`Topic` 和 队列的关系。上面我讲解了 `Topic` 和队列的关系——一个 `Topic` 中存在多个队列,那么这个 `Topic` 和队列存放在哪呢? - - **一个 `Topic` 分布在多个 `Broker`上,一个 `Broker` 可以配置多个 `Topic` ,它们是多对多的关系**。 - - 如果某个 `Topic` 消息量很大,应该给它多配置几个队列(上文中提到了提高并发能力),并且 **尽量多分布在不同 `Broker` 上,以减轻某个 `Broker` 的压力** 。 - - `Topic` 消息量都比较均匀的情况下,如果某个 `broker` 上的队列越多,则该 `broker` 压力越大。 - - ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef38687488a5a4.jpg) - - > 所以说我们需要配置多个 Broker。 - -- `NameServer`:不知道你们有没有接触过 `ZooKeeper` 和 `Spring Cloud` 中的 `Eureka` ,它其实也是一个 **注册中心** ,主要提供两个功能:**Broker 管理** 和 **路由信息管理** 。说白了就是 `Broker` 会将自己的信息注册到 `NameServer` 中,此时 `NameServer` 就存放了很多 `Broker` 的信息(Broker 的路由表),消费者和生产者就从 `NameServer` 中获取路由表然后照着路由表的信息和对应的 `Broker` 进行通信(生产者和消费者定期会向 `NameServer` 去查询相关的 `Broker` 的信息)。 - -- `Producer`:消息发布的角色,支持分布式集群方式部署。说白了就是生产者。 - -- `Consumer`:消息消费的角色,支持分布式集群方式部署。支持以 push 推,pull 拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制。说白了就是消费者。 - -听完了上面的解释你可能会觉得,这玩意好简单。不就是这样的么? - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef386c6d1e8bdb.jpg) - -嗯?你可能会发现一个问题,这老家伙 `NameServer` 干啥用的,这不多余吗?直接 `Producer`、`Consumer` 和 `Broker` 直接进行生产消息,消费消息不就好了么? - -但是,我们上文提到过 `Broker` 是需要保证高可用的,如果整个系统仅仅靠着一个 `Broker` 来维持的话,那么这个 `Broker` 的压力会不会很大?所以我们需要使用多个 `Broker` 来保证 **负载均衡** 。 - -如果说,我们的消费者和生产者直接和多个 `Broker` 相连,那么当 `Broker` 修改的时候必定会牵连着每个生产者和消费者,这样就会产生耦合问题,而 `NameServer` 注册中心就是用来解决这个问题的。 - -> 如果还不是很理解的话,可以去看我介绍 `Spring Cloud` 的那篇文章,其中介绍了 `Eureka` 注册中心。 - -当然,`RocketMQ` 中的技术架构肯定不止前面那么简单,因为上面图中的四个角色都是需要做集群的。我给出一张官网的架构图,大家尝试理解一下。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef386fa3be1e53.jpg) - -其实和我们最开始画的那张乞丐版的架构图也没什么区别,主要是一些细节上的差别。听我细细道来 🤨。 - -第一、我们的 `Broker` **做了集群并且还进行了主从部署** ,由于消息分布在各个 `Broker` 上,一旦某个 `Broker` 宕机,则该`Broker` 上的消息读写都会受到影响。所以 `Rocketmq` 提供了 `master/slave` 的结构,`salve` 定时从 `master` 同步数据(同步刷盘或者异步刷盘),如果 `master` 宕机,**则 `slave` 提供消费服务,但是不能写入消息** (后面我还会提到哦)。 - -第二、为了保证 `HA` ,我们的 `NameServer` 也做了集群部署,但是请注意它是 **去中心化** 的。也就意味着它没有主节点,你可以很明显地看出 `NameServer` 的所有节点是没有进行 `Info Replicate` 的,在 `RocketMQ` 中是通过 **单个 Broker 和所有 NameServer 保持长连接** ,并且在每隔 30 秒 `Broker` 会向所有 `Nameserver` 发送心跳,心跳包含了自身的 `Topic` 配置信息,这个步骤就对应这上面的 `Routing Info` 。 - -第三、在生产者需要向 `Broker` 发送消息的时候,**需要先从 `NameServer` 获取关于 `Broker` 的路由信息**,然后通过 **轮询** 的方法去向每个队列中生产数据以达到 **负载均衡** 的效果。 - -第四、消费者通过 `NameServer` 获取所有 `Broker` 的路由信息后,向 `Broker` 发送 `Pull` 请求来获取消息数据。`Consumer` 可以以两种模式启动—— **广播(Broadcast)和集群(Cluster)**。广播模式下,一条消息会发送给 **同一个消费组中的所有消费者** ,集群模式下消息只会发送给一个消费者。 - -## RocketMQ 功能特性 - -### 消息 - -#### 普通消息 - -普通消息一般应用于微服务解耦、事件驱动、数据集成等场景,这些场景大多数要求数据传输通道具有可靠传输的能力,且对消息的处理时机、处理顺序没有特别要求。以在线的电商交易场景为例,上游订单系统将用户下单支付这一业务事件封装成独立的普通消息并发送至 RocketMQ 服务端,下游按需从服务端订阅消息并按照本地消费逻辑处理下游任务。每个消息之间都是相互独立的,且不需要产生关联。另外还有日志系统,以离线的日志收集场景为例,通过埋点组件收集前端应用的相关操作日志,并转发到 RocketMQ 。 - -![](https://rocketmq.apache.org/zh/assets/images/lifecyclefornormal-e8a2a7e42a0722f681eb129b51e1bd66.png) - -**普通消息生命周期** - -- 初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。 -- 待消费:消息被发送到服务端,对消费者可见,等待消费者消费的状态。 -- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,RocketMQ 会对消息进行重试处理。 -- 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。RocketMQ 默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。 -- 消息删除:RocketMQ 按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。 - -#### 定时消息 - -在分布式定时调度触发、任务超时处理等场景,需要实现精准、可靠的定时事件触发。使用 RocketMQ 的定时消息可以简化定时调度任务的开发逻辑,实现高性能、可扩展、高可靠的定时触发能力。定时消息仅支持在 MessageType 为 Delay 的主题内使用,即定时消息只能发送至类型为定时消息的主题中,发送的消息的类型必须和主题的类型一致。在 4.x 版本中,只支持延时消息,默认分为 18 个等级分别为:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,也可以在配置文件中增加自定义的延时等级和时长。在 5.x 版本中,开始支持定时消息,在构造消息时提供了 3 个 API 来指定延迟时间或定时时间。 - -基于定时消息的超时任务处理具备如下优势: - -- **精度高、开发门槛低**:基于消息通知方式不存在定时阶梯间隔。可以轻松实现任意精度事件触发,无需业务去重。 -- **高性能可扩展**:传统的数据库扫描方式较为复杂,需要频繁调用接口扫描,容易产生性能瓶颈。RocketMQ 的定时消息具有高并发和水平扩展的能力。 - -![](https://rocketmq.apache.org/zh/assets/images/lifecyclefordelay-2ce8278df69cd026dd11ffd27ab09a17.png) - -**定时消息生命周期** - -- 初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。 -- 定时中:消息被发送到服务端,和普通消息不同的是,服务端不会直接构建消息索引,而是会将定时消息**单独存储在定时存储系统中**,等待定时时刻到达。 -- 待消费:定时时刻到达后,服务端将消息重新写入普通存储引擎,对下游消费者可见,等待消费者消费的状态。 -- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,RocketMQ 会对消息进行重试处理。 -- 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。RocketMQ 默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。 -- 消息删除:Apache RocketMQ 按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。 - -定时消息的实现逻辑需要先经过定时存储等待触发,定时时间到达后才会被投递给消费者。因此,如果将大量定时消息的定时时间设置为同一时刻,则到达该时刻后会有大量消息同时需要被处理,会造成系统压力过大,导致消息分发延迟,影响定时精度。 - -#### 顺序消息 - -顺序消息仅支持使用 MessageType 为 FIFO 的主题,即顺序消息只能发送至类型为顺序消息的主题中,发送的消息的类型必须和主题的类型一致。和普通消息发送相比,顺序消息发送必须要设置消息组。(推荐实现 MessageQueueSelector 的方式,见下文)。要保证消息的顺序性需要单一生产者串行发送。 - -单线程使用 MessageListenerConcurrently 可以顺序消费,多线程环境下使用 MessageListenerOrderly 才能顺序消费。 - -#### 事务消息 - -事务消息是 Apache RocketMQ 提供的一种高级消息类型,支持在分布式场景下保障消息生产和本地事务的最终一致性。简单来讲,就是将本地事务(数据库的 DML 操作)与发送消息合并在同一个事务中。例如,新增一个订单。在事务未提交之前,不发送订阅的消息。发送消息的动作随着事务的成功提交而发送,随着事务的回滚而取消。当然真正地处理过程不止这么简单,包含了半消息、事务监听和事务回查等概念,下面有更详细的说明。 - -## 关于发送消息 - -### **不建议单一进程创建大量生产者** - -Apache RocketMQ 的生产者和主题是多对多的关系,支持同一个生产者向多个主题发送消息。对于生产者的创建和初始化,建议遵循够用即可、最大化复用原则,如果有需要发送消息到多个主题的场景,无需为每个主题都创建一个生产者。 - -### **不建议频繁创建和销毁生产者** - -Apache RocketMQ 的生产者是可以重复利用的底层资源,类似数据库的连接池。因此不需要在每次发送消息时动态创建生产者,且在发送结束后销毁生产者。这样频繁的创建销毁会在服务端产生大量短连接请求,严重影响系统性能。 - -正确示例: - -```java -Producer p = ProducerBuilder.build(); -for (int i =0;i messageViewList = simpleConsumer.receive(10, Duration.ofSeconds(30)); - messageViewList.forEach(messageView -> { - System.out.println(messageView); - // 消费处理完成后,需要主动调用 ACK 提交消费结果。 - try { - simpleConsumer.ack(messageView); - } catch (ClientException e) { - logger.error("Failed to ack message, messageId={}", messageView.getMessageId(), e); - } - }); -} catch (ClientException e) { - // 如果遇到系统流控等原因造成拉取失败,需要重新发起获取消息请求。 - logger.error("Failed to receive message", e); -} -``` - -SimpleConsumer 适用于以下场景: - -- 消息处理时长不可控:如果消息处理时长无法预估,经常有长时间耗时的消息处理情况。建议使用 SimpleConsumer 消费类型,可以在消费时自定义消息的预估处理时长,若实际业务中预估的消息处理时长不符合预期,也可以通过接口提前修改。 -- 需要异步化、批量消费等高级定制场景:SimpleConsumer 在 SDK 内部没有复杂的线程封装,完全由业务逻辑自由定制,可以实现异步分发、批量消费等高级定制场景。 -- 需要自定义消费速率:SimpleConsumer 是由业务逻辑主动调用接口获取消息,因此可以自由调整获取消息的频率,自定义控制消费速率。 - -### PullConsumer - -施工中。。。 - -## 消费者分组和生产者分组 - -### 生产者分组 - -RocketMQ 服务端 5.x 版本开始,**生产者是匿名的**,无需管理生产者分组(ProducerGroup);对于历史版本服务端 3.x 和 4.x 版本,已经使用的生产者分组可以废弃无需再设置,且不会对当前业务产生影响。 - -### 消费者分组 - -消费者分组是多个消费行为一致的消费者的负载均衡分组。消费者分组不是具体实体而是一个逻辑资源。通过消费者分组实现消费性能的水平扩展以及高可用容灾。 - -消费者分组中的订阅关系、投递顺序性、消费重试策略是一致的。 - -- 订阅关系:Apache RocketMQ 以消费者分组的粒度管理订阅关系,实现订阅关系的管理和追溯。 -- 投递顺序性:Apache RocketMQ 的服务端将消息投递给消费者消费时,支持顺序投递和并发投递,投递方式在消费者分组中统一配置。 -- 消费重试策略: 消费者消费消息失败时的重试策略,包括重试次数、死信队列设置等。 - -RocketMQ 服务端 5.x 版本:上述消费者的消费行为从关联的消费者分组中统一获取,因此,同一分组内所有消费者的消费行为必然是一致的,客户端无需关注。 - -RocketMQ 服务端 3.x/4.x 历史版本:上述消费逻辑由消费者客户端接口定义,因此,您需要自己在消费者客户端设置时保证同一分组下的消费者的消费行为一致。(来自官方网站) - -## 如何解决顺序消费和重复消费? - -其实,这些东西都是我在介绍消息队列带来的一些副作用的时候提到的,也就是说,这些问题不仅仅挂钩于 `RocketMQ` ,而是应该每个消息中间件都需要去解决的。 - -在上面我介绍 `RocketMQ` 的技术架构的时候我已经向你展示了 **它是如何保证高可用的** ,这里不涉及运维方面的搭建,如果你感兴趣可以自己去官网上照着例子搭建属于你自己的 `RocketMQ` 集群。 - -> 其实 `Kafka` 的架构基本和 `RocketMQ` 类似,只是它注册中心使用了 `Zookeeper`、它的 **分区** 就相当于 `RocketMQ` 中的 **队列** 。还有一些小细节不同会在后面提到。 - -### 顺序消费 - -在上面的技术架构介绍中,我们已经知道了 **`RocketMQ` 在主题上是无序的、它只有在队列层面才是保证有序** 的。 - -这又扯到两个概念——**普通顺序** 和 **严格顺序** 。 - -所谓普通顺序是指 消费者通过 **同一个消费队列收到的消息是有顺序的** ,不同消息队列收到的消息则可能是无顺序的。普通顺序消息在 `Broker` **重启情况下不会保证消息顺序性** (短暂时间) 。 - -所谓严格顺序是指 消费者收到的 **所有消息** 均是有顺序的。严格顺序消息 **即使在异常情况下也会保证消息的顺序性** 。 - -但是,严格顺序看起来虽好,实现它可会付出巨大的代价。如果你使用严格顺序模式,`Broker` 集群中只要有一台机器不可用,则整个集群都不可用。你还用啥?现在主要场景也就在 `binlog` 同步。 - -一般而言,我们的 `MQ` 都是能容忍短暂的乱序,所以推荐使用普通顺序模式。 - -那么,我们现在使用了 **普通顺序模式** ,我们从上面学习知道了在 `Producer` 生产消息的时候会进行轮询(取决你的负载均衡策略)来向同一主题的不同消息队列发送消息。那么如果此时我有几个消息分别是同一个订单的创建、支付、发货,在轮询的策略下这 **三个消息会被发送到不同队列** ,因为在不同的队列此时就无法使用 `RocketMQ` 带来的队列有序特性来保证消息有序性了。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3874585e096e.jpg) - -那么,怎么解决呢? - -其实很简单,我们需要处理的仅仅是将同一语义下的消息放入同一个队列(比如这里是同一个订单),那我们就可以使用 **Hash 取模法** 来保证同一个订单在同一个队列中就行了。 - -RocketMQ 实现了两种队列选择算法,也可以自己实现 - -- 轮询算法 - - - 轮询算法就是向消息指定的 topic 所在队列中依次发送消息,保证消息均匀分布 - - 是 RocketMQ 默认队列选择算法 - -- 最小投递延迟算法 - - - 每次消息投递的时候统计消息投递的延迟,选择队列时优先选择消息延时小的队列,导致消息分布不均匀,按照如下设置即可。 - - - ```java - producer.setSendLatencyFaultEnable(true); - ``` - -- 继承 MessageQueueSelector 实现 - - - ```java - SendResult sendResult = producer.send(msg, new MessageQueueSelector() { - @Override - public MessageQueue select(List mqs, Message msg, Object arg) { - //从mqs中选择一个队列,可以根据msg特点选择 - return null; - } - }, new Object()); - ``` - -### 特殊情况处理 - -#### 发送异常 - -选择队列后会与 Broker 建立连接,通过网络请求将消息发送到 Broker 上,如果 Broker 挂了或者网络波动发送消息超时此时 RocketMQ 会进行重试。 - -重新选择其他 Broker 中的消息队列进行发送,默认重试两次,可以手动设置。 - -```java -producer.setRetryTimesWhenSendFailed(5); -``` - -#### 消息过大 - -消息超过 4k 时 RocketMQ 会将消息压缩后在发送到 Broker 上,减少网络资源的占用。 - -### 重复消费 - -emmm,就两个字—— **幂等** 。在编程中一个*幂等* 操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。比如说,这个时候我们有一个订单的处理积分的系统,每当来一个消息的时候它就负责为创建这个订单的用户的积分加上相应的数值。可是有一次,消息队列发送给订单系统 FrancisQ 的订单信息,其要求是给 FrancisQ 的积分加上 500。但是积分系统在收到 FrancisQ 的订单信息处理完成之后返回给消息队列处理成功的信息的时候出现了网络波动(当然还有很多种情况,比如 Broker 意外重启等等),这条回应没有发送成功。 - -那么,消息队列没收到积分系统的回应会不会尝试重发这个消息?问题就来了,我再发这个消息,万一它又给 FrancisQ 的账户加上 500 积分怎么办呢? - -所以我们需要给我们的消费者实现 **幂等** ,也就是对同一个消息的处理结果,执行多少次都不变。 - -那么如何给业务实现幂等呢?这个还是需要结合具体的业务的。你可以使用 **写入 `Redis`** 来保证,因为 `Redis` 的 `key` 和 `value` 就是天然支持幂等的。当然还有使用 **数据库插入法** ,基于数据库的唯一键来保证重复数据不会被插入多条。 - -不过最主要的还是需要 **根据特定场景使用特定的解决方案** ,你要知道你的消息消费是否是完全不可重复消费还是可以忍受重复消费的,然后再选择强校验和弱校验的方式。毕竟在 CS 领域还是很少有技术银弹的说法。 - -而在整个互联网领域,幂等不仅仅适用于消息队列的重复消费问题,这些实现幂等的方法,也同样适用于,**在其他场景中来解决重复请求或者重复调用的问题** 。比如将 HTTP 服务设计成幂等的,**解决前端或者 APP 重复提交表单数据的问题** ,也可以将一个微服务设计成幂等的,解决 `RPC` 框架自动重试导致的 **重复调用问题** 。 - -## RocketMQ 如何实现分布式事务? - -如何解释分布式事务呢?事务大家都知道吧?**要么都执行要么都不执行** 。在同一个系统中我们可以轻松地实现事务,但是在分布式架构中,我们有很多服务是部署在不同系统之间的,而不同服务之间又需要进行调用。比如此时我下订单然后增加积分,如果保证不了分布式事务的话,就会出现 A 系统下了订单,但是 B 系统增加积分失败或者 A 系统没有下订单,B 系统却增加了积分。前者对用户不友好,后者对运营商不利,这是我们都不愿意见到的。 - -那么,如何去解决这个问题呢? - -如今比较常见的分布式事务实现有 2PC、TCC 和事务消息(half 半消息机制)。每一种实现都有其特定的使用场景,但是也有各自的问题,**都不是完美的解决方案**。 - -在 `RocketMQ` 中使用的是 **事务消息加上事务反查机制** 来解决分布式事务问题的。我画了张图,大家可以对照着图进行理解。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef38798d7a987f.png) - -在第一步发送的 half 消息 ,它的意思是 **在事务提交之前,对于消费者来说,这个消息是不可见的** 。 - -> 那么,如何做到写入消息但是对用户不可见呢?RocketMQ 事务消息的做法是:如果消息是 half 消息,将备份原消息的主题与消息消费队列,然后 **改变主题** 为 RMQ_SYS_TRANS_HALF_TOPIC。由于消费组未订阅该主题,故消费端无法消费 half 类型的消息,**然后 RocketMQ 会开启一个定时任务,从 Topic 为 RMQ_SYS_TRANS_HALF_TOPIC 中拉取消息进行消费**,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。 - -你可以试想一下,如果没有从第 5 步开始的 **事务反查机制** ,如果出现网路波动第 4 步没有发送成功,这样就会产生 MQ 不知道是不是需要给消费者消费的问题,他就像一个无头苍蝇一样。在 `RocketMQ` 中就是使用的上述的事务反查来解决的,而在 `Kafka` 中通常是直接抛出一个异常让用户来自行解决。 - -你还需要注意的是,在 `MQ Server` 指向系统 B 的操作已经和系统 A 不相关了,也就是说在消息队列中的分布式事务是——**本地事务和存储消息到消息队列才是同一个事务**。这样也就产生了事务的**最终一致性**,因为整个过程是异步的,**每个系统只要保证它自己那一部分的事务就行了**。 - -实践中会遇到的问题:事务消息需要一个事务监听器来监听本地事务是否成功,并且事务监听器接口只允许被实现一次。那就意味着需要把各种事务消息的本地事务都写在一个接口方法里面,必将会产生大量的耦合和类型判断。采用函数 Function 接口来包装整个业务过程,作为一个参数传递到监听器的接口方法中。再调用 Function 的 apply() 方法来执行业务,事务也会在 apply() 方法中执行。让监听器与业务之间实现解耦,使之具备了真实生产环境中的可行性。 - -1.模拟一个添加用户浏览记录的需求 - -```java -@PostMapping("/add") -@ApiOperation("添加用户浏览记录") -public Result add(Long userId, Long forecastLogId) { - - // 函数式编程:浏览记录入库 - Function function = transactionId -> viewHistoryHandler.addViewHistory(transactionId, userId, forecastLogId); - - Map hashMap = new HashMap<>(); - hashMap.put("userId", userId); - hashMap.put("forecastLogId", forecastLogId); - String jsonString = JSON.toJSONString(hashMap); - - // 发送事务消息;将本地的事务操作,用函数Function接口接收,作为一个参数传入到方法中 - TransactionSendResult transactionSendResult = mqProducerService.sendTransactionMessage(jsonString, MQDestination.TAG_ADD_VIEW_HISTORY, function); - return Result.success(transactionSendResult); -} -``` - -2.发送事务消息的方法 - -```java -/** - * 发送事务消息 - * - * @param msgBody - * @param tag - * @param function - * @return - */ -public TransactionSendResult sendTransactionMessage(String msgBody, String tag, Function function) { - // 构建消息体 - Message message = buildMessage(msgBody); - - // 构建消息投递信息 - String destination = buildDestination(tag); - - TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(destination, message, function); - return result; -} -``` - -3.生产者消息监听器,只允许一个类去实现该监听器 - -```java -@Slf4j -@RocketMQTransactionListener -public class TransactionMsgListener implements RocketMQLocalTransactionListener { - - @Autowired - private RedisService redisService; - - /** - * 执行本地事务(在发送消息成功时执行) - * - * @param message - * @param o - * @return commit or rollback or unknown - */ - @Override - public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) { - - // 1、获取事务ID - String transactionId = null; - try { - transactionId = message.getHeaders().get("rocketmq_TRANSACTION_ID").toString(); - // 2、判断传入函数对象是否为空,如果为空代表没有要执行的业务直接抛弃消息 - if (o == null) { - //返回ROLLBACK状态的消息会被丢弃 - log.info("事务消息回滚,没有需要处理的业务 transactionId={}", transactionId); - return RocketMQLocalTransactionState.ROLLBACK; - } - // 将Object o转换成Function对象 - Function function = (Function) o; - // 执行业务 事务也会在function.apply中执行 - Boolean apply = function.apply(transactionId); - if (apply) { - log.info("事务提交,消息正常处理 transactionId={}", transactionId); - //返回COMMIT状态的消息会立即被消费者消费到 - return RocketMQLocalTransactionState.COMMIT; - } - } catch (Exception e) { - log.info("出现异常 返回ROLLBACK transactionId={}", transactionId); - return RocketMQLocalTransactionState.ROLLBACK; - } - return RocketMQLocalTransactionState.ROLLBACK; - } - - /** - * 事务回查机制,检查本地事务的状态 - * - * @param message - * @return - */ - @Override - public RocketMQLocalTransactionState checkLocalTransaction(Message message) { - - String transactionId = message.getHeaders().get("rocketmq_TRANSACTION_ID").toString(); - - // 查redis - MqTransaction mqTransaction = redisService.getCacheObject("mqTransaction:" + transactionId); - if (Objects.isNull(mqTransaction)) { - return RocketMQLocalTransactionState.ROLLBACK; - } - return RocketMQLocalTransactionState.COMMIT; - } -} -``` - -4.模拟的业务场景,这里的方法必须提取出来,放在别的类里面.如果调用方与被调用方在同一个类中,会发生事务失效的问题. - -```java -@Component -public class ViewHistoryHandler { - - @Autowired - private IViewHistoryService viewHistoryService; - - @Autowired - private IMqTransactionService mqTransactionService; - - @Autowired - private RedisService redisService; - - /** - * 浏览记录入库 - * - * @param transactionId - * @param userId - * @param forecastLogId - * @return - */ - @Transactional - public Boolean addViewHistory(String transactionId, Long userId, Long forecastLogId) { - // 构建浏览记录 - ViewHistory viewHistory = new ViewHistory(); - viewHistory.setUserId(userId); - viewHistory.setForecastLogId(forecastLogId); - viewHistory.setCreateTime(LocalDateTime.now()); - boolean save = viewHistoryService.save(viewHistory); - - // 本地事务信息 - MqTransaction mqTransaction = new MqTransaction(); - mqTransaction.setTransactionId(transactionId); - mqTransaction.setCreateTime(new Date()); - mqTransaction.setStatus(MqTransaction.StatusEnum.VALID.getStatus()); - - // 1.可以把事务信息存数据库 - mqTransactionService.save(mqTransaction); - - // 2.也可以选择存redis,4个小时有效期,'4个小时'是RocketMQ内置的最大回查超时时长,过期未确认将强制回滚 - redisService.setCacheObject("mqTransaction:" + transactionId, mqTransaction, 4L, TimeUnit.HOURS); - - // 放开注释,模拟异常,事务回滚 - // int i = 10 / 0; - - return save; - } -} -``` - -5.消费消息,以及幂等处理 - -```java -@Service -@RocketMQMessageListener(topic = MQDestination.TOPIC, selectorExpression = MQDestination.TAG_ADD_VIEW_HISTORY, consumerGroup = MQDestination.TAG_ADD_VIEW_HISTORY) -public class ConsumerAddViewHistory implements RocketMQListener { - // 监听到消息就会执行此方法 - @Override - public void onMessage(Message message) { - // 幂等校验 - String transactionId = message.getTransactionId(); - - // 查redis - MqTransaction mqTransaction = redisService.getCacheObject("mqTransaction:" + transactionId); - - // 不存在事务记录 - if (Objects.isNull(mqTransaction)) { - return; - } - - // 已消费 - if (Objects.equals(mqTransaction.getStatus(), MqTransaction.StatusEnum.CONSUMED.getStatus())) { - return; - } - - String msg = new String(message.getBody()); - Map map = JSON.parseObject(msg, new TypeReference>() { - }); - Long userId = map.get("userId"); - Long forecastLogId = map.get("forecastLogId"); - - // 下游的业务处理 - // TODO 记录用户喜好,更新用户画像 - - // TODO 更新'证券预测文章'的浏览量,重新计算文章的曝光排序 - - // 更新状态为已消费 - mqTransaction.setUpdateTime(new Date()); - mqTransaction.setStatus(MqTransaction.StatusEnum.CONSUMED.getStatus()); - redisService.setCacheObject("mqTransaction:" + transactionId, mqTransaction, 4L, TimeUnit.HOURS); - log.info("监听到消息:msg={}", JSON.toJSONString(map)); - } -} -``` - -## 如何解决消息堆积问题? - -在上面我们提到了消息队列一个很重要的功能——**削峰** 。那么如果这个峰值太大了导致消息堆积在队列中怎么办呢? - -其实这个问题可以将它广义化,因为产生消息堆积的根源其实就只有两个——生产者生产太快或者消费者消费太慢。 - -我们可以从多个角度去思考解决这个问题,当流量到峰值的时候是因为生产者生产太快,我们可以使用一些 **限流降级** 的方法,当然你也可以增加多个消费者实例去水平扩展增加消费能力来匹配生产的激增。如果消费者消费过慢的话,我们可以先检查 **是否是消费者出现了大量的消费错误** ,或者打印一下日志查看是否是哪一个线程卡死,出现了锁资源不释放等等的问题。 - -> 当然,最快速解决消息堆积问题的方法还是增加消费者实例,不过 **同时你还需要增加每个主题的队列数量** 。 -> -> 别忘了在 `RocketMQ` 中,**一个队列只会被一个消费者消费** ,如果你仅仅是增加消费者实例就会出现我一开始给你画架构图的那种情况。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef387d939ab66d.jpg) - -## 什么是回溯消费? - -回溯消费是指 `Consumer` 已经消费成功的消息,由于业务上需求需要重新消费,在`RocketMQ` 中, `Broker` 在向`Consumer` 投递成功消息后,**消息仍然需要保留** 。并且重新消费一般是按照时间维度,例如由于 `Consumer` 系统故障,恢复后需要重新消费 1 小时前的数据,那么 `Broker` 要提供一种机制,可以按照时间维度来回退消费进度。`RocketMQ` 支持按照时间回溯消费,时间维度精确到毫秒。 - -这是官方文档的解释,我直接照搬过来就当科普了 😁😁😁。 - -## RocketMQ 如何保证高性能读写 - -### 传统 IO 方式 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/31699457085_.pic.jpg) - -传统的 IO 读写其实就是 read + write 的操作,整个过程会分为如下几步 - -- 用户调用 read()方法,开始读取数据,此时发生一次上下文从用户态到内核态的切换,也就是图示的切换 1 -- 将磁盘数据通过 DMA 拷贝到内核缓存区 -- 将内核缓存区的数据拷贝到用户缓冲区,这样用户,也就是我们写的代码就能拿到文件的数据 -- read()方法返回,此时就会从内核态切换到用户态,也就是图示的切换 2 -- 当我们拿到数据之后,就可以调用 write()方法,此时上下文会从用户态切换到内核态,即图示切换 3 -- CPU 将用户缓冲区的数据拷贝到 Socket 缓冲区 -- 将 Socket 缓冲区数据拷贝至网卡 -- write()方法返回,上下文重新从内核态切换到用户态,即图示切换 4 - -整个过程发生了 4 次上下文切换和 4 次数据的拷贝,这在高并发场景下肯定会严重影响读写性能故引入了零拷贝技术 - -### 零拷贝技术 - -#### mmap - -mmap(memory map)是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。 - -简单地说就是内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次 CPU 拷贝。基于此上述架构图可变为: - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/41699457086_.pic.jpg) - -基于 mmap IO 读写其实就变成 mmap + write 的操作,也就是用 mmap 替代传统 IO 中的 read 操作。 - -当用户发起 mmap 调用的时候会发生上下文切换 1,进行内存映射,然后数据被拷贝到内核缓冲区,mmap 返回,发生上下文切换 2;随后用户调用 write,发生上下文切换 3,将内核缓冲区的数据拷贝到 Socket 缓冲区,write 返回,发生上下文切换 4。 - -发生 4 次上下文切换和 3 次 IO 拷贝操作,在 Java 中的实现: - -```java -FileChannel fileChannel = new RandomAccessFile("test.txt", "rw").getChannel(); -MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size()); -``` - -#### sendfile - -sendfile()跟 mmap()一样,也会减少一次 CPU 拷贝,但是它同时也会减少两次上下文切换。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/51699457087_.pic.jpg) - -如图,用户在发起 sendfile()调用时会发生切换 1,之后数据通过 DMA 拷贝到内核缓冲区,之后再将内核缓冲区的数据 CPU 拷贝到 Socket 缓冲区,最后拷贝到网卡,sendfile()返回,发生切换 2。发生了 3 次拷贝和两次切换。Java 也提供了相应 api: - -```java -FileChannel channel = FileChannel.open(Paths.get("./test.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); -//调用transferTo方法向目标数据传输 -channel.transferTo(position, len, target); -``` - -在如上代码中,并没有文件的读写操作,而是直接将文件的数据传输到 target 目标缓冲区,也就是说,sendfile 是无法知道文件的具体的数据的;但是 mmap 不一样,他是可以修改内核缓冲区的数据的。假设如果需要对文件的内容进行修改之后再传输,只有 mmap 可以满足。 - -通过上面的一些介绍,结论是基于零拷贝技术,可以减少 CPU 的拷贝次数和上下文切换次数,从而可以实现文件高效的读写操作。 - -RocketMQ 内部主要是使用基于 mmap 实现的零拷贝(其实就是调用上述提到的 api),用来读写文件,这也是 RocketMQ 为什么快的一个很重要原因。 - -## RocketMQ 的刷盘机制 - -上面我讲了那么多的 `RocketMQ` 的架构和设计原理,你有没有好奇 - -在 `Topic` 中的 **队列是以什么样的形式存在的?** - -**队列中的消息又是如何进行存储持久化的呢?** - -我在上文中提到的 **同步刷盘** 和 **异步刷盘** 又是什么呢?它们会给持久化带来什么样的影响呢? - -下面我将给你们一一解释。 - -### 同步刷盘和异步刷盘 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef387fba311cda-20230814005009889.jpg) - -如上图所示,在同步刷盘中需要等待一个刷盘成功的 `ACK` ,同步刷盘对 `MQ` 消息可靠性来说是一种不错的保障,但是 **性能上会有较大影响** ,一般地适用于金融等特定业务场景。 - -而异步刷盘往往是开启一个线程去异步地执行刷盘操作。消息刷盘采用后台异步线程提交的方式进行, **降低了读写延迟** ,提高了 `MQ` 的性能和吞吐量,一般适用于如发验证码等对于消息保证要求不太高的业务场景。 - -一般地,**异步刷盘只有在 `Broker` 意外宕机的时候会丢失部分数据**,你可以设置 `Broker` 的参数 `FlushDiskType` 来调整你的刷盘策略(ASYNC_FLUSH 或者 SYNC_FLUSH)。 - -### 同步复制和异步复制 - -上面的同步刷盘和异步刷盘是在单个结点层面的,而同步复制和异步复制主要是指的 `Borker` 主从模式下,主节点返回消息给客户端的时候是否需要同步从节点。 - -- 同步复制:也叫 “同步双写”,也就是说,**只有消息同步双写到主从节点上时才返回写入成功** 。 -- 异步复制:**消息写入主节点之后就直接返回写入成功** 。 - -然而,很多事情是没有完美的方案的,就比如我们进行消息写入的节点越多就更能保证消息的可靠性,但是随之的性能也会下降,所以需要程序员根据特定业务场景去选择适应的主从复制方案。 - -那么,**异步复制会不会也像异步刷盘那样影响消息的可靠性呢?** - -答案是不会的,因为两者就是不同的概念,对于消息可靠性是通过不同的刷盘策略保证的,而像异步同步复制策略仅仅是影响到了 **可用性** 。为什么呢?其主要原因**是 `RocketMQ` 是不支持自动主从切换的,当主节点挂掉之后,生产者就不能再给这个主节点生产消息了**。 - -比如这个时候采用异步复制的方式,在主节点还未发送完需要同步的消息的时候主节点挂掉了,这个时候从节点就少了一部分消息。但是此时生产者无法再给主节点生产消息了,**消费者可以自动切换到从节点进行消费**(仅仅是消费),所以在主节点挂掉的时间只会产生主从结点短暂的消息不一致的情况,降低了可用性,而当主节点重启之后,从节点那部分未来得及复制的消息还会继续复制。 - -在单主从架构中,如果一个主节点挂掉了,那么也就意味着整个系统不能再生产了。那么这个可用性的问题能否解决呢?**一个主从不行那就多个主从的呗**,别忘了在我们最初的架构图中,每个 `Topic` 是分布在不同 `Broker` 中的。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef38687488a5asadasfg4.jpg) - -但是这种复制方式同样也会带来一个问题,那就是无法保证 **严格顺序** 。在上文中我们提到了如何保证的消息顺序性是通过将一个语义的消息发送在同一个队列中,使用 `Topic` 下的队列来保证顺序性的。如果此时我们主节点 A 负责的是订单 A 的一系列语义消息,然后它挂了,这样其他节点是无法代替主节点 A 的,如果我们任意节点都可以存入任何消息,那就没有顺序性可言了。 - -而在 `RocketMQ` 中采用了 `Dledger` 解决这个问题。他要求在写入消息的时候,要求**至少消息复制到半数以上的节点之后**,才给客⼾端返回写⼊成功,并且它是⽀持通过选举来动态切换主节点的。这里我就不展开说明了,读者可以自己去了解。 - -> 也不是说 `Dledger` 是个完美的方案,至少在 `Dledger` 选举过程中是无法提供服务的,而且他必须要使用三个节点或以上,如果多数节点同时挂掉他也是无法保证可用性的,而且要求消息复制半数以上节点的效率和直接异步复制还是有一定的差距的。 - -### 存储机制 - -还记得上面我们一开始的三个问题吗?到这里第三个问题已经解决了。 - -但是,在 `Topic` 中的 **队列是以什么样的形式存在的?队列中的消息又是如何进行存储持久化的呢?** 还未解决,其实这里涉及到了 `RocketMQ` 是如何设计它的存储结构了。我首先想大家介绍 `RocketMQ` 消息存储架构中的三大角色——`CommitLog`、`ConsumeQueue` 和 `IndexFile` 。 - -- `CommitLog`:**消息主体以及元数据的存储主体**,存储 `Producer` 端写入的消息主体内容,消息内容不是定长的。单个文件大小默认 1G ,文件名长度为 20 位,左边补零,剩余为起始偏移量,比如 00000000000000000000 代表了第一个文件,起始偏移量为 0,文件大小为 1G=1073741824;当第一个文件写满了,第二个文件为 00000000001073741824,起始偏移量为 1073741824,以此类推。消息主要是**顺序写入日志文件**,当文件满了,写入下一个文件。 -- `ConsumeQueue`:消息消费队列,**引入的目的主要是提高消息消费的性能**(我们再前面也讲了),由于`RocketMQ` 是基于主题 `Topic` 的订阅模式,消息消费是针对主题进行的,如果要遍历 `commitlog` 文件中根据 `Topic` 检索消息是非常低效的。`Consumer` 即可根据 `ConsumeQueue` 来查找待消费的消息。其中,`ConsumeQueue`(逻辑消费队列)**作为消费消息的索引**,保存了指定 `Topic` 下的队列消息在 `CommitLog` 中的**起始物理偏移量 `offset` **,消息大小 `size` 和消息 `Tag` 的 `HashCode` 值。**`consumequeue` 文件可以看成是基于 `topic` 的 `commitlog` 索引文件**,故 `consumequeue` 文件夹的组织方式如下:topic/queue/file 三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样 `consumequeue` 文件采取定长设计,每一个条目共 20 个字节,分别为 8 字节的 `commitlog` 物理偏移量、4 字节的消息长度、8 字节 tag `hashcode`,单个文件由 30W 个条目组成,可以像数组一样随机访问每一个条目,每个 `ConsumeQueue`文件大小约 5.72M; -- `IndexFile`:`IndexFile`(索引文件)提供了一种可以通过 key 或时间区间来查询消息的方法。这里只做科普不做详细介绍。 - -总结来说,整个消息存储的结构,最主要的就是 `CommitLoq` 和 `ConsumeQueue` 。而 `ConsumeQueue` 你可以大概理解为 `Topic` 中的队列。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3884c02acc72.png) - -`RocketMQ` 采用的是 **混合型的存储结构** ,即为 `Broker` 单个实例下所有的队列共用一个日志数据文件来存储消息。有意思的是在同样高并发的 `Kafka` 中会为每个 `Topic` 分配一个存储文件。这就有点类似于我们有一大堆书需要装上书架,`RocketMQ` 是不分书的种类直接成批的塞上去的,而 `Kafka` 是将书本放入指定的分类区域的。 - -而 `RocketMQ` 为什么要这么做呢?原因是 **提高数据的写入效率** ,不分 `Topic` 意味着我们有更大的几率获取 **成批** 的消息进行数据写入,但也会带来一个麻烦就是读取消息的时候需要遍历整个大文件,这是非常耗时的。 - -所以,在 `RocketMQ` 中又使用了 `ConsumeQueue` 作为每个队列的索引文件来 **提升读取消息的效率**。我们可以直接根据队列的消息序号,计算出索引的全局位置(索引序号\*索引固定⻓度 20),然后直接读取这条索引,再根据索引中记录的消息的全局位置,找到消息。 - -讲到这里,你可能对 `RocketMQ` 的存储架构还有些模糊,没事,我们结合着图来理解一下。 - -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef388763c25c62.jpg) - -emmm,是不是有一点复杂 🤣,看英文图片和英文文档的时候就不要怂,硬着头皮往下看就行。 - -> 如果上面没看懂的读者一定要认真看下面的流程分析! - -首先,在最上面的那一块就是我刚刚讲的你现在可以直接 **把 `ConsumerQueue` 理解为 `Queue`**。 - -在图中最左边说明了红色方块代表被写入的消息,虚线方块代表等待被写入的。左边的生产者发送消息会指定 `Topic`、`QueueId` 和具体消息内容,而在 `Broker` 中管你是哪门子消息,他直接 **全部顺序存储到了 CommitLog**。而根据生产者指定的 `Topic` 和 `QueueId` 将这条消息本身在 `CommitLog` 的偏移(offset),消息本身大小,和 tag 的 hash 值存入对应的 `ConsumeQueue` 索引文件中。而在每个队列中都保存了 `ConsumeOffset` 即每个消费者组的消费位置(我在架构那里提到了,忘了的同学可以回去看一下),而消费者拉取消息进行消费的时候只需要根据 `ConsumeOffset` 获取下一个未被消费的消息就行了。 - -上述就是我对于整个消息存储架构的大概理解(这里不涉及到一些细节讨论,比如稀疏索引等等问题),希望对你有帮助。 - -因为有一个知识点因为写嗨了忘讲了,想想在哪里加也不好,所以我留给大家去思考 🤔🤔 一下吧。 - -为什么 `CommitLog` 文件要设计成固定大小的长度呢?提醒:**内存映射机制**。 - -## 总结 - -总算把这篇博客写完了。我讲的你们还记得吗 😅? - -这篇文章中我主要想大家介绍了 - -1. 消息队列出现的原因 -2. 消息队列的作用(异步,解耦,削峰) -3. 消息队列带来的一系列问题(消息堆积、重复消费、顺序消费、分布式事务等等) -4. 消息队列的两种消息模型——队列和主题模式 -5. 分析了 `RocketMQ` 的技术架构(`NameServer`、`Broker`、`Producer`、`Consumer`) -6. 结合 `RocketMQ` 回答了消息队列副作用的解决方案 -7. 介绍了 `RocketMQ` 的存储机制和刷盘策略。 - -等等。。。 - - +Later, when we started working and had money to eat at restaurants, we would tell the waiter to bring a bowl of beef noodles with a poached egg \*\*(sending diff --git a/docs/high-performance/read-and-write-separation-and-library-subtable.md b/docs/high-performance/read-and-write-separation-and-library-subtable.md index da25f066e9e..dc0accc826a 100644 --- a/docs/high-performance/read-and-write-separation-and-library-subtable.md +++ b/docs/high-performance/read-and-write-separation-and-library-subtable.md @@ -1,291 +1,291 @@ --- -title: 读写分离和分库分表详解 -category: 高性能 +title: Explanation of Read-Write Separation and Database Sharding +category: High Performance head: - - - meta - - name: keywords - content: 读写分离,分库分表,主从复制 - - - meta - - name: description - content: 读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。 这样的话,就能够小幅提升写性能,大幅提升读性能。 读写分离基于主从复制,MySQL 主从复制是依赖于 binlog 。分库就是将数据库中的数据分散到不同的数据库上。分表就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。引入分库分表之后,需要系统解决事务、分布式 id、无法 join 操作问题。 + - - meta + - name: keywords + content: read-write separation, database sharding, master-slave replication + - - meta + - name: description + content: The main purpose of read-write separation is to distribute read and write operations on the database across different database nodes. This can slightly enhance write performance and significantly improve read performance. Read-write separation is based on master-slave replication, and MySQL master-slave replication relies on binlog. Database sharding involves distributing data from a database across different databases. Table sharding involves splitting data from a single table, which can be vertical or horizontal. After introducing database sharding, the system needs to resolve transactional issues, distributed IDs, and the inability to perform join operations. --- -## 读写分离 +## Read-Write Separation -### 什么是读写分离? +### What is Read-Write Separation? -见名思意,根据读写分离的名字,我们就可以知道:**读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。** 这样的话,就能够小幅提升写性能,大幅提升读性能。 +As the name implies, **read-write separation mainly aims to distribute read and write operations on the database across different database nodes.** This allows for a slight improvement in write performance and a significant enhancement in read performance. -我简单画了一张图来帮助不太清楚读写分离的小伙伴理解。 +I created a simple diagram to help those who are not very clear about read-write separation understand better. -![读写分离示意图](https://oss.javaguide.cn/github/javaguide/high-performance/read-and-write-separation-and-library-subtable/read-and-write-separation.png) +![Read-Write Separation Diagram](https://oss.javaguide.cn/github/javaguide/high-performance/read-and-write-separation-and-library-subtable/read-and-write-separation.png) -一般情况下,我们都会选择一主多从,也就是一台主数据库负责写,其他的从数据库负责读。主库和从库之间会进行数据同步,以保证从库中数据的准确性。这样的架构实现起来比较简单,并且也符合系统的写少读多的特点。 +Generally, we choose one master and multiple slaves, meaning one master database is responsible for writing while the other slave databases handle reading. There will be data synchronization between the master and slave databases to ensure data accuracy in the slave databases. This architecture is relatively simple to implement and aligns with the system's characteristic of having more reads than writes. -### 如何实现读写分离? +### How to Implement Read-Write Separation? -不论是使用哪一种读写分离具体的实现方案,想要实现读写分离一般包含如下几步: +Regardless of the specific implementation方案 for read-write separation, it generally involves the following steps: -1. 部署多台数据库,选择其中的一台作为主数据库,其他的一台或者多台作为从数据库。 -2. 保证主数据库和从数据库之间的数据是实时同步的,这个过程也就是我们常说的**主从复制**。 -3. 系统将写请求交给主数据库处理,读请求交给从数据库处理。 +1. Deploy multiple databases, selecting one as the master database and one or more as slave databases. +1. Ensure real-time data synchronization between the master and slave databases, which we commonly refer to as **master-slave replication**. +1. The system delegates write requests to the master database and read requests to the slave databases. -落实到项目本身的话,常用的方式有两种: +For project implementation, there are two commonly used methods: -**1. 代理方式** +**1. Proxy Approach** -![代理方式实现读写分离](https://oss.javaguide.cn/github/javaguide/high-performance/read-and-write-separation-and-library-subtable/read-and-write-separation-proxy.png) +![Proxy Approach for Read-Write Separation](https://oss.javaguide.cn/github/javaguide/high-performance/read-and-write-separation-and-library-subtable/read-and-write-separation-proxy.png) -我们可以在应用和数据中间加了一个代理层。应用程序所有的数据请求都交给代理层处理,代理层负责分离读写请求,将它们路由到对应的数据库中。 +We can add a proxy layer between the application and the database. All data requests from the application are handled by the proxy layer, which is responsible for separating read and write requests and routing them to the corresponding databases. -提供类似功能的中间件有 **MySQL Router**(官方, MySQL Proxy 的替代方案)、**Atlas**(基于 MySQL Proxy)、**MaxScale**、**MyCat**。 +Middleware that provides similar functionality includes **MySQL Router** (official, an alternative to MySQL Proxy), **Atlas** (based on MySQL Proxy), **MaxScale**, and **MyCat**. -关于 MySQL Router 多提一点:在 MySQL 8.2 的版本中,MySQL Router 能自动分辨对数据库读写/操作并把这些操作路由到正确的实例上。这是一项有价值的功能,可以优化数据库性能和可扩展性,而无需在应用程序中进行任何更改。具体介绍可以参考官方博客:[MySQL 8.2 – transparent read/write splitting](https://blogs.oracle.com/mysql/post/mysql-82-transparent-readwrite-splitting)。 +One point to highlight about MySQL Router: In the MySQL 8.2 version, MySQL Router can automatically distinguish between read and write operations on the database and route these operations to the correct instances. This is a valuable feature that can optimize database performance and scalability without requiring any changes to the application. For more details, you can refer to the official blog: [MySQL 8.2 – transparent read/write splitting](https://blogs.oracle.com/mysql/post/mysql-82-transparent-readwrite-splitting). -**2. 组件方式** +**2. Component Approach** -在这种方式中,我们可以通过引入第三方组件来帮助我们读写请求。 +In this method, we can introduce third-party components to assist with read and write requests. -这也是我比较推荐的一种方式。这种方式目前在各种互联网公司中用的最多的,相关的实际的案例也非常多。如果你要采用这种方式的话,推荐使用 `sharding-jdbc` ,直接引入 jar 包即可使用,非常方便。同时,也节省了很多运维的成本。 +This is also a method I highly recommend. It is widely used in various internet companies, and there are many practical cases related to it. If you choose to adopt this method, I recommend using `sharding-jdbc`, which can be easily implemented by directly adding the jar package, making it very convenient. Additionally, it saves a lot of operational costs. -你可以在 shardingsphere 官方找到 [sharding-jdbc 关于读写分离的操作](https://shardingsphere.apache.org/document/legacy/3.x/document/cn/manual/sharding-jdbc/usage/read-write-splitting/)。 +You can find the [operations regarding read-write separation for sharding-jdbc](https://shardingsphere.apache.org/document/legacy/3.x/document/cn/manual/sharding-jdbc/usage/read-write-splitting/) on the official shardingsphere documentation. -### 主从复制原理是什么? +### What is the Principle of Master-Slave Replication? -MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据库中数据的所有变化(数据库执行的所有 DDL 和 DML 语句)。因此,我们根据主库的 MySQL binlog 日志就能够将主库的数据同步到从库中。 +MySQL binlog (binary log) mainly records all changes in data within the MySQL database (all DDL and DML statements executed by the database). Therefore, we can synchronize data from the master database to the slave database based on the MySQL binlog logs from the master database. -更具体和详细的过程是这个样子的(图片来自于:[《MySQL Master-Slave Replication on the Same Machine》](https://www.toptal.com/mysql/mysql-master-slave-replication-tutorial)): +The specific and detailed process is as follows (image from: [“MySQL Master-Slave Replication on the Same Machine”](https://www.toptal.com/mysql/mysql-master-slave-replication-tutorial)): -![MySQL主从复制](https://oss.javaguide.cn/java-guide-blog/78816271d3ab52424bfd5ad3086c1a0f.png) +![MySQL Master-Slave Replication](https://oss.javaguide.cn/java-guide-blog/78816271d3ab52424bfd5ad3086c1a0f.png) -1. 主库将数据库中数据的变化写入到 binlog -2. 从库连接主库 -3. 从库会创建一个 I/O 线程向主库请求更新的 binlog -4. 主库会创建一个 binlog dump 线程来发送 binlog ,从库中的 I/O 线程负责接收 -5. 从库的 I/O 线程将接收的 binlog 写入到 relay log 中。 -6. 从库的 SQL 线程读取 relay log 同步数据到本地(也就是再执行一遍 SQL )。 +1. The master database writes changes in the database to the binlog +1. The slave database connects to the master database +1. The slave database creates an I/O thread to request the updated binlog from the master database +1. The master database creates a binlog dump thread to send the binlog, and the I/O thread in the slave is responsible for receiving it +1. The I/O thread in the slave writes the received binlog to the relay log +1. The SQL thread in the slave reads the relay log and synchronizes the data locally (essentially executing the SQL again). -怎么样?看了我对主从复制这个过程的讲解,你应该搞明白了吧! +So, do you understand the process of master-slave replication after my explanation? -你一般看到 binlog 就要想到主从复制。当然,除了主从复制之外,binlog 还能帮助我们实现数据恢复。 +Whenever you see binlog, you should think of master-slave replication. Of course, aside from master-slave replication, binlog can also help us achieve data recovery. -🌈 拓展一下: +🌈 Here's an extension: -不知道大家有没有使用过阿里开源的一个叫做 canal 的工具。这个工具可以帮助我们实现 MySQL 和其他数据源比如 Elasticsearch 或者另外一台 MySQL 数据库之间的数据同步。很显然,这个工具的底层原理肯定也是依赖 binlog。canal 的原理就是模拟 MySQL 主从复制的过程,解析 binlog 将数据同步到其他的数据源。 +I wonder if any of you have used an open-source tool from Alibaba called canal. This tool can help us synchronize data between MySQL and other data sources, such as Elasticsearch or another MySQL database. Obviously, the underlying principle of this tool also relies on binlog. The principle of canal is to simulate the master-slave replication process of MySQL, parsing the binlog to synchronize data to other data sources. -另外,像咱们常用的分布式缓存组件 Redis 也是通过主从复制实现的读写分离。 +Additionally, commonly used distributed caching components like Redis also achieve read-write separation through master-slave replication. -🌕 简单总结一下: +🌕 In summary: -**MySQL 主从复制是依赖于 binlog 。另外,常见的一些同步 MySQL 数据到其他数据源的工具(比如 canal)的底层一般也是依赖 binlog 。** +**MySQL master-slave replication relies on binlog. Furthermore, some common tools for synchronizing MySQL data to other data sources (like canal) typically also depend on binlog.** -### 如何避免主从延迟? +### How to Avoid Master-Slave Lag? -读写分离对于提升数据库的并发非常有效,但是,同时也会引来一个问题:主库和从库的数据存在延迟,比如你写完主库之后,主库的数据同步到从库是需要时间的,这个时间差就导致了主库和从库的数据不一致性问题。这也就是我们经常说的 **主从同步延迟** 。 +Read-write separation is very effective for improving database concurrency. However, it also brings a problem: there is lag between the master and slave databases, meaning after writing to the master database, it takes time to synchronize data to the slave database. This time difference leads to inconsistency between the data in the master and slave databases, which we often refer to as **master-slave synchronization lag**. -如果我们的业务场景无法容忍主从同步延迟的话,应该如何避免呢(注意:我这里说的是避免而不是减少延迟)? +If our business scenario cannot tolerate master-slave synchronization lag, how can we avoid it? (Note: I'm talking about avoiding rather than just reducing lag.) -这里提供两种我知道的方案(能力有限,欢迎补充),你可以根据自己的业务场景参考一下。 +Here are two solutions I know of (limited capabilities, welcome to add more), which you can refer to based on your business scenarios. -#### 强制将读请求路由到主库处理 +#### Force Read Requests to Route to the Master Database -既然你从库的数据过期了,那我就直接从主库读取嘛!这种方案虽然会增加主库的压力,但是,实现起来比较简单,也是我了解到的使用最多的一种方式。 +Since the data in the slave database is stale, I will just read directly from the master database! This solution may increase the load on the master database, but it is relatively simple to implement and is one of the most commonly used methods I know of. -比如 `Sharding-JDBC` 就是采用的这种方案。通过使用 Sharding-JDBC 的 `HintManager` 分片键值管理器,我们可以强制使用主库。 +For example, `Sharding-JDBC` employs this solution. By using the `HintManager` shard key manager of Sharding-JDBC, we can force routes to the master database. ```java HintManager hintManager = HintManager.getInstance(); hintManager.setMasterRouteOnly(); -// 继续JDBC操作 +// Continue JDBC operations ``` -对于这种方案,你可以将那些必须获取最新数据的读请求都交给主库处理。 +For this solution, you can pass all read requests that must obtain the latest data to the master database. -#### 延迟读取 +#### Delayed Reading -还有一些朋友肯定会想既然主从同步存在延迟,那我就在延迟之后读取啊,比如主从同步延迟 0.5s,那我就 1s 之后再读取数据。这样多方便啊!方便是方便,但是也很扯淡。 +Some friends might think, if there is lag in master-slave synchronization, why not read after a delay? For instance, if the master-slave synchronization lag is 0.5s, then I will read the data after 1s. This seems convenient! It is convenient, but also quite nonsensical. -不过,如果你是这样设计业务流程就会好很多:对于一些对数据比较敏感的场景,你可以在完成写请求之后,避免立即进行请求操作。比如你支付成功之后,跳转到一个支付成功的页面,当你点击返回之后才返回自己的账户。 +However, if you design your business process this way, it will be much better: for scenarios where data is very sensitive, you can avoid performing request operations immediately after completing write requests. For example, after a successful payment, you could redirect to a payment success page, and only return to your accounts when you click back. -#### 总结 +#### Summary -关于如何避免主从延迟,我们这里介绍了两种方案。实际上,延迟读取这种方案没办法完全避免主从延迟,只能说可以减少出现延迟的概率而已,实际项目中一般不会使用。 +Regarding how to avoid master-slave lag, we discussed two solutions here. In fact, the delayed reading solution cannot completely avoid master-slave lag; it can only reduce the probability of lag occurring, and in actual projects, it is generally not used. -总的来说,要想不出现延迟问题,一般还是要强制将那些必须获取最新数据的读请求都交给主库处理。如果你的项目的大部分业务场景对数据准确性要求不是那么高的话,这种方案还是可以选择的。 +Overall, to avoid lag issues, you usually need to force those read requests that must obtain the latest data to be handled by the master database. If the majority of your project's business scenarios do not require high data accuracy, this solution is still a valid option. -### 什么情况下会出现主从延迟?如何尽量减少延迟? +### In What Situations Does Master-Slave Lag Occur? How to Minimize Lag? -我们在上面的内容中也提到了主从延迟以及避免主从延迟的方法,这里我们再来详细分析一下主从延迟出现的原因以及应该如何尽量减少主从延迟。 +We have mentioned master-slave lag and methods to avoid it previously; here we will analyze the reasons for master-slave lag and how to minimize it. -要搞懂什么情况下会出现主从延迟,我们需要先搞懂什么是主从延迟。 +To understand in what situations master-slave lag occurs, we first need to understand what master-slave lag is. -MySQL 主从同步延时是指从库的数据落后于主库的数据,这种情况可能由以下两个原因造成: +MySQL master-slave synchronization lag refers to the situation where the data in the slave database is behind that in the master database. This situation might be caused by the following two reasons: -1. 从库 I/O 线程接收 binlog 的速度跟不上主库写入 binlog 的速度,导致从库 relay log 的数据滞后于主库 binlog 的数据; -2. 从库 SQL 线程执行 relay log 的速度跟不上从库 I/O 线程接收 binlog 的速度,导致从库的数据滞后于从库 relay log 的数据。 +1. The speed of the slave I/O thread receiving the binlog is slower than that of the master database writing the binlog, leading to the relay log data in the slave being lagged behind the binlog data in the master. +1. The speed at which the slave SQL thread executes the relay log is slower than that at which the slave I/O thread receives the binlog, causing the slave’s data to lag behind the relay log. -与主从同步有关的时间点主要有 3 个: +Three main time points related to master-slave synchronization are: -1. 主库执行完一个事务,写入 binlog,将这个时刻记为 T1; -2. 从库 I/O 线程接收到 binlog 并写入 relay log 的时刻记为 T2; -3. 从库 SQL 线程读取 relay log 同步数据本地的时刻记为 T3。 +1. The master database finishes a transaction, writes to the binlog, and we mark this moment as T1. +1. The slave's I/O thread receives the binlog and writes it to the relay log, we mark this moment as T2. +1. The slave's SQL thread reads from the relay log and synchronizes data locally at the moment marked T3. -结合我们上面讲到的主从复制原理,可以得出: +Combining what we discussed about master-slave replication principles, we can conclude: -- T2 和 T1 的差值反映了从库 I/O 线程的性能和网络传输的效率,这个差值越小说明从库 I/O 线程的性能和网络传输效率越高。 -- T3 和 T2 的差值反映了从库 SQL 线程执行的速度,这个差值越小,说明从库 SQL 线程执行速度越快。 +- The difference between T2 and T1 reflects the performance of the slave's I/O thread and the efficiency of network transmission—the smaller this difference, the better the performance and network efficiency of the slave's I/O thread. +- The difference between T3 and T2 reflects the speed of the slave's SQL thread executing—the smaller this difference, the faster the execution speed of the slave's SQL thread. -那什么情况下会出现出从延迟呢?这里列举几种常见的情况: +So in what situations can there be slave lag? Here are a few common situations: -1. **从库机器性能比主库差**:从库接收 binlog 并写入 relay log 以及执行 SQL 语句的速度会比较慢(也就是 T2-T1 和 T3-T2 的值会较大),进而导致延迟。解决方法是选择与主库一样规格或更高规格的机器作为从库,或者对从库进行性能优化,比如调整参数、增加缓存、使用 SSD 等。 -2. **从库处理的读请求过多**:从库需要执行主库的所有写操作,同时还要响应读请求,如果读请求过多,会占用从库的 CPU、内存、网络等资源,影响从库的复制效率(也就是 T2-T1 和 T3-T2 的值会较大,和前一种情况类似)。解决方法是引入缓存(推荐)、使用一主多从的架构,将读请求分散到不同的从库,或者使用其他系统来提供查询的能力,比如将 binlog 接入到 Hadoop、Elasticsearch 等系统中。 -3. **大事务**:运行时间比较长,长时间未提交的事务就可以称为大事务。由于大事务执行时间长,并且从库上的大事务会比主库上的大事务花费更多的时间和资源,因此非常容易造成主从延迟。解决办法是避免大批量修改数据,尽量分批进行。类似的情况还有执行时间较长的慢 SQL ,实际项目遇到慢 SQL 应该进行优化。 -4. **从库太多**:主库需要将 binlog 同步到所有的从库,如果从库数量太多,会增加同步的时间和开销(也就是 T2-T1 的值会比较大,但这里是因为主库同步压力大导致的)。解决方案是减少从库的数量,或者将从库分为不同的层级,让上层的从库再同步给下层的从库,减少主库的压力。 -5. **网络延迟**:如果主从之间的网络传输速度慢,或者出现丢包、抖动等问题,那么就会影响 binlog 的传输效率,导致从库延迟。解决方法是优化网络环境,比如提升带宽、降低延迟、增加稳定性等。 -6. **单线程复制**:MySQL5.5 及之前,只支持单线程复制。为了优化复制性能,MySQL 5.6 引入了 **多线程复制**,MySQL 5.7 还进一步完善了多线程复制。 -7. **复制模式**:MySQL 默认的复制是异步的,必然会存在延迟问题。全同步复制不存在延迟问题,但性能太差了。半同步复制是一种折中方案,相对于异步复制,半同步复制提高了数据的安全性,减少了主从延迟(还是有一定程度的延迟)。MySQL 5.5 开始,MySQL 以插件的形式支持 **semi-sync 半同步复制**。并且,MySQL 5.7 引入了 **增强半同步复制** 。 -8. …… +1. **Slave Machine Has Inferior Performance Compared to Master**: The slave's speed in receiving the binlog and writing it to the relay log, as well as executing SQL statements, will be slower (meaning the values of T2-T1 and T3-T2 will be large), causing lag. The solution is to select a slave machine with specifications equivalent to or greater than that of the master or optimize the performance of the slave, such as adjusting parameters, increasing cache, using SSDs, etc. +1. **Excessive Read Requests to the Slave**: The slave must execute all write operations from the master while also responding to read requests; if there are too many read requests, they will consume the slave's CPU, memory, and network resources, affecting the slave's replication efficiency (again, T2-T1 and T3-T2 values will be large, similar to the previous situation). Solutions include introducing caching (recommended), using a one-master multi-slave architecture to distribute read requests across different slaves, or employing other systems to provide querying capabilities, such as integrating the binlog with Hadoop, Elasticsearch, etc. +1. **Large Transactions**: Long-running transactions that have not been committed for a long time can be termed large transactions. Because large transactions take longer to execute, and those on the slave will take more time and resources than those on the master, they are likely to cause master-slave lag. The solution is to avoid making batch modifications to data and try to do so in smaller increments. Similar issues may arise with long-running slow SQL queries; optimization should be done for slow SQL in actual projects. +1. **Too Many Slaves**: The master needs to synchronize the binlog to all slaves; if there are too many slaves, it will increase synchronization time and overhead (where T2-T1 will be comparatively larger, caused by heavier synchronization pressure on the master). The solution is to minimize the number of slaves or to organize slaves into different tiers, with upper-level slaves synchronizing data to lower-level slaves, thus reducing the master’s pressure. +1. **Network Delay**: If the transmission speed between the master and slaves is slow, or issues like packet loss and jitter occur, it can affect the efficiency of binlog transmission, leading to slave delay. Solutions include optimizing the network environment, such as increasing bandwidth, reducing latency, and improving stability. +1. **Single-threaded Replication**: Up until MySQL 5.5, only single-threaded replication was supported. To optimize replication performance, MySQL 5.6 introduced **multi-threaded replication**, with further enhancements in MySQL 5.7. +1. **Replication Mode**: MySQL's default replication is asynchronous, which inherently leads to lag issues. Fully synchronous replication has no lag but offers poor performance. Semi-synchronous replication is a compromise that enhances data safety compared to asynchronous replication while reducing master-slave lag (yet some lag remains). Since MySQL 5.5, MySQL has supported **semi-synchronous replication** in the form of plugins, and MySQL 5.7 introduced **enhanced semi-synchronous replication**. +1. …… -[《MySQL 实战 45 讲》](https://time.geekbang.org/column/intro/100020801?code=ieY8HeRSlDsFbuRtggbBQGxdTh-1jMASqEIeqzHAKrI%3D)这个专栏中的[读写分离有哪些坑?](https://time.geekbang.org/column/article/77636)这篇文章也有对主从延迟解决方案这一话题进行探讨,感兴趣的可以阅读学习一下。 +The article [“What Pitfalls Are There in Read-Write Separation?”](https://time.geekbang.org/column/article/77636) in the series [“45 Talks on MySQL Practice”](https://time.geekbang.org/column/intro/100020801?code=ieY8HeRSlDsFbuRtggbBQGxdTh-1jMASqEIeqzHAKrI%3D) also discusses solutions to master-slave lag; interested readers can refer to it for further learning. -## 分库分表 +## Database Sharding -读写分离主要应对的是数据库读并发,没有解决数据库存储问题。试想一下:**如果 MySQL 一张表的数据量过大怎么办?** +Read-write separation mainly addresses read concurrency in databases without solving storage issues. Just imagine: **What if the data volume of a single table in MySQL is too large?** -换言之,**我们该如何解决 MySQL 的存储压力呢?** +In other words, **how do we alleviate storage pressure on MySQL?** -答案之一就是 **分库分表**。 +One answer is **database sharding.** -### 什么是分库? +### What is Database Sharding? -**分库** 就是将数据库中的数据分散到不同的数据库上,可以垂直分库,也可以水平分库。 +**Database sharding** means distributing data from the database across different databases, which can occur through vertical or horizontal sharding. -**垂直分库** 就是把单一数据库按照业务进行划分,不同的业务使用不同的数据库,进而将一个数据库的压力分担到多个数据库。 +**Vertical sharding** divides a single database according to business needs, with different businesses utilizing separate databases to distribute the pressure of one database across multiple databases. -举个例子:说你将数据库中的用户表、订单表和商品表分别单独拆分为用户数据库、订单数据库和商品数据库。 +For instance, you can separate user tables, order tables, and product tables into individual user databases, order databases, and product databases. -![垂直分库](./images/read-and-write-separation-and-library-subtable/vertical-slicing-database.png) +![Vertical Sharding](./images/read-and-write-separation-and-library-subtable/vertical-slicing-database.png) -**水平分库** 是把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,这样就实现了水平扩展,解决了单表的存储和性能瓶颈的问题。 +**Horizontal sharding** distributes the same table across different databases based on specific rules. Each database may reside on different servers, achieving horizontal scalability and resolving storage and performance bottlenecks of a single table. -举个例子:订单表数据量太大,你对订单表进行了水平切分(水平分表),然后将切分后的 2 张订单表分别放在两个不同的数据库。 +For example, if the data volume of the order table is excessively large, you can horizontally segment the order table (horizontal partitioning) and place the resulting two order tables in two different databases. -![水平分库](./images/read-and-write-separation-and-library-subtable/horizontal-slicing-database.png) +![Horizontal Sharding](./images/read-and-write-separation-and-library-subtable/horizontal-slicing-database.png) -### 什么是分表? +### What is Table Sharding? -**分表** 就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。 +**Table sharding** means splitting the data of a single table, which can also be done through vertical or horizontal methods. -**垂直分表** 是对数据表列的拆分,把一张列比较多的表拆分为多张表。 +**Vertical table sharding** involves dividing the columns of a data table, breaking a table with many columns into multiple tables. -举个例子:我们可以将用户信息表中的一些列单独抽出来作为一个表。 +For example, some columns from the user information table can be separated into their own table. -**水平分表** 是对数据表行的拆分,把一张行比较多的表拆分为多张表,可以解决单一表数据量过大的问题。 +**Horizontal table sharding** divides the rows of a data table, breaking a table with a large number of rows into multiple tables to alleviate the impact of excessive data volume on performance. -举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。 +For example, user information tables can be partitioned into multiple tables, which can avoid performance impacts caused by excessive data in a single table. -水平拆分只能解决单表数据量大的问题,为了提升性能,我们通常会选择将拆分后的多张表放在不同的数据库中。也就是说,水平分表通常和水平分库同时出现。 +Horizontal partitioning can only address the issue of a single table having a large volume of data. To enhance performance, we generally opt to distribute the resulting multiple tables across different databases; that is, horizontal table sharding often coexists with horizontal database sharding. -![分表](./images/read-and-write-separation-and-library-subtable/two-forms-of-sub-table.png) +![Table Sharding](./images/read-and-write-separation-and-library-subtable/two-forms-of-sub-table.png) -### 什么情况下需要分库分表? +### When Should We Consider Database Sharding? -遇到下面几种场景可以考虑分库分表: +Consider database sharding in the following scenarios: -- 单表的数据达到千万级别以上,数据库读写速度比较缓慢。 -- 数据库中的数据占用的空间越来越大,备份时间越来越长。 -- 应用的并发量太大(应该优先考虑其他性能优化方法,而非分库分表)。 +- If the data in a single table exceeds tens of millions, and the database read/write speed is sluggish. +- If the data in the database takes up an increasingly large amount of space, resulting in longer backup times. +- If the application's concurrency is excessively high (alternative performance optimization methods should be prioritized over sharding). -不过,分库分表的成本太高,如非必要尽量不要采用。而且,并不一定是单表千万级数据量就要分表,毕竟每张表包含的字段不同,它们在不错的性能下能够存放的数据量也不同,还是要具体情况具体分析。 +However, the cost of database sharding is quite high, so it should be avoided unless necessary. Moreover, it’s not always the case that a table with tens of millions of records must be sharded; since every table contains different fields, the volume of data each can hold under acceptable performance varies, necessitating analysis of the specific situation. -之前看过一篇文章分析 “[InnoDB 中高度为 3 的 B+ 树最多可以存多少数据](https://juejin.cn/post/7165689453124517896)”,写的挺不错,感兴趣的可以看看。 +I previously read an article analyzing “[How much data can a B+ tree with a height of 3 in InnoDB store](https://juejin.cn/post/7165689453124517896)”, which is quite well written, and I recommend taking a look if interested. -### 常见的分片算法有哪些? +### What are Common Sharding Algorithms? -分片算法主要解决了数据被水平分片之后,数据究竟该存放在哪个表的问题。 +Sharding algorithms mainly address the question of where data should be stored after being horizontally partitioned. -常见的分片算法有: +Common sharding algorithms include: -- **哈希分片**:求指定分片键的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,不太适合经常需要范围查询的场景。哈希分片可以使每个表的数据分布相对均匀,但对动态伸缩(例如新增一个表或者库)不友好。 -- **范围分片**:按照特定的范围区间(比如时间区间、ID 区间)来分配数据,比如 将 `id` 为 `1~299999` 的记录分到第一个表, `300000~599999` 的分到第二个表。范围分片适合需要经常进行范围查找且数据分布均匀的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。 -- **映射表分片**:使用一个单独的表(称为映射表)来存储分片键和分片位置的对应关系。映射表分片策略可以支持任何类型的分片算法,如哈希分片、范围分片等。映射表分片策略是可以灵活地调整分片规则,不需要修改应用程序代码或重新分布数据。不过,这种方式需要维护额外的表,还增加了查询的开销和复杂度。 -- **一致性哈希分片**:将哈希空间组织成一个环形结构,将分片键和节点(数据库或表)都映射到这个环上,然后根据顺时针的规则确定数据或请求应该分配到哪个节点上,解决了传统哈希对动态伸缩不友好的问题。 -- **地理位置分片**:很多 NewSQL 数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。 -- **融合算法分片**:灵活组合多种分片算法,比如将哈希分片和范围分片组合。 +- **Hash-based Sharding**: Calculate the hash of a specified sharding key and determine which table the data should be placed in according to the hash value. Hash sharding is more suitable for scenarios requiring random read/write and less suitable for frequent range queries. Hash sharding can help ensure even distribution of data across tables but is unfriendly to dynamic scaling (e.g., adding a new table or database). +- **Range-based Sharding**: Allocate data according to specific range intervals (e.g., a time interval, ID interval). For instance, records with `id` values from `1 to 299999` go to the first table, while `300000 to 599999` go to the second table. Range sharding is suitable for scenarios requiring frequent range searches and where data distribution is even, but it’s less suitable for random read/write (data isn’t dispersed leading to hot data issues). +- **Mapping Table Sharding**: Use a single separate table (referred to as a mapping table) to store the correspondence between the sharding key and the sharding location. Mapping table sharding can support any type of sharding algorithm, such as hash sharding, range sharding, etc. This strategy allows flexible adjustment of sharding rules without requiring changes to application code or redistributing data. However, this method necessitates maintaining an additional table and increases query overhead and complexity. +- **Consistent Hash Sharding**: Organizes the hash space into a circular structure, mapping both sharding keys and nodes (databases or tables) onto this circle, then determining to which node data or requests should be assigned based on a clockwise rule, thus solving the traditional hash’s unfriendly nature towards dynamic scaling. +- **Geographic Location Sharding**: Many NewSQL databases support geographic location sharding algorithms, which distribute data based on geographical locations (e.g., city, region). +- **Hybrid Algorithm Sharding**: Flexibly combine multiple sharding algorithms, such as combining hash and range sharding. - …… -### 分片键如何选择? +### How to Choose a Sharding Key? -分片键(Sharding Key)是数据分片的关键字段。分片键的选择非常重要,它关系着数据的分布和查询效率。一般来说,分片键应该具备以下特点: +The sharding key is the crucial field for data partitioning. The selection of an appropriate sharding key is highly important, as it directly affects data distribution and query efficiency. Generally speaking, a sharding key should have the following characteristics: -- 具有共性,即能够覆盖绝大多数的查询场景,尽量减少单次查询所涉及的分片数量,降低数据库压力; -- 具有离散性,即能够将数据均匀地分散到各个分片上,避免数据倾斜和热点问题; -- 具有稳定性,即分片键的值不会发生变化,避免数据迁移和一致性问题; -- 具有扩展性,即能够支持分片的动态增加和减少,避免数据重新分片的开销。 +- **Universality**: It should cover the vast majority of query scenarios, minimizing the number of shards involved in a single query, thus reducing database pressure. +- **Discreteness**: It should evenly distribute data across all shards to avoid data skew and hot spot issues. +- **Stability**: The value of the sharding key should remain unchanged to prevent data migration and consistency issues. +- **Scalability**: It should allow for dynamic addition and removal of shards to avoid the overhead of data re-sharding. -实际项目中,分片键很难满足上面提到的所有特点,需要权衡一下。并且,分片键可以是表中多个字段的组合,例如取用户 ID 后四位作为订单 ID 后缀。 +In actual projects, it's challenging for a sharding key to fulfill all the aforementioned characteristics, requiring some trade-offs. Additionally, the sharding key can be a combination of multiple fields in a table, such as taking the last four digits of a user ID as a suffix for an order ID. -### 分库分表会带来什么问题呢? +### What Issues Can Database Sharding Bring? -记住,你在公司做的任何技术决策,不光是要考虑这个技术能不能满足我们的要求,是否适合当前业务场景,还要重点考虑其带来的成本。 +Remember, any technical decision you make at your company should consider not just whether this technology can fulfill our requirements and is suited to the current business scenario, but also consider the costs it incurs. -引入分库分表之后,会给系统带来什么挑战呢? +What challenges can arise in the system after introducing database sharding? -- **join 操作**:同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。不过,很多大厂的资深 DBA 都是建议尽量不要使用 join 操作。因为 join 的效率低,并且会对分库分表造成影响。对于需要用到 join 操作的地方,可以采用多次查询业务层进行数据组装的方法。不过,这种方法需要考虑业务上多次查询的事务性的容忍度。 -- **事务问题**:同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。这个时候,我们就需要引入分布式事务了。关于分布式事务常见解决方案总结,网站上也有对应的总结: 。 -- **分布式 ID**:分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 ID 了。关于分布式 ID 的详细介绍&实现方案总结,可以看我写的这篇文章:[分布式 ID 介绍&实现方案总结](https://javaguide.cn/distributed-system/distributed-id.html)。 -- **跨库聚合查询问题**:分库分表会导致常规聚合查询操作,如 group by,order by 等变得异常复杂。这是因为这些操作需要在多个分片上进行数据汇总和排序,而不是在单个数据库上进行。为了实现这些操作,需要编写复杂的业务代码,或者使用中间件来协调分片间的通信和数据传输。这样会增加开发和维护的成本,以及影响查询的性能和可扩展性。 +- **Join Operations**: When tables from the same database are distributed across different databases, it becomes impossible to use join operations. This leads to a need for manual data encapsulation; for instance, you might query a piece of data in one database and then look for corresponding data in another database using that data. However, many experienced DBAs in large companies suggest avoiding join operations whenever possible since join operations are inefficient and can negatively impact database sharding. For places where join operations are needed, you could resort to multiple queries at the business layer to assemble the data. However, this method must consider the transactional tolerance of multiple queries from a business standpoint. +- **Transactional Issues**: When tables from the same database are distributed across different databases, standard transactions won't suffice if an individual operation involves multiple databases. In such cases, we need to introduce distributed transactions. There are several common solutions for distributed transaction issues summarized on various websites: . +- **Distributed IDs**: Once sharding is implemented, data is spread across different servers, making it impossible for the database's auto-increment primary key to ensure unique primary key generation. How do we generate global unique primary keys for different data nodes? At this point, we need to introduce distributed IDs into our system. For a detailed introduction and implementation summary of distributed IDs, check out this article I wrote: [Introduction to Distributed IDs & Implementation Summary](https://javaguide.cn/distributed-system/distributed-id.html). +- **Cross-Database Aggregation Query Issues**: Database sharding can complicate conventional aggregation query operations, such as group by and order by. This is because these operations require data aggregation and sorting across multiple shards rather than within a single database. Implementing these operations necessitates writing complex business code or using middleware to coordinate communication and data transmission between shards, leading to increased development and maintenance costs, as well as affecting query performance and scalability. - …… -另外,引入分库分表之后,一般需要 DBA 的参与,同时还需要更多的数据库服务器,这些都属于成本。 +Moreover, introducing database sharding typically requires DBA involvement and a greater number of database servers, all of which contribute to cost. -### 分库分表有没有什么比较推荐的方案? +### Are There Any Recommended Solutions for Database Sharding? -Apache ShardingSphere 是一款分布式的数据库生态系统, 可以将任意数据库转换为分布式数据库,并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。 +Apache ShardingSphere is a distributed database ecosystem that can transform any database into a distributed database and enhance the original database's functionality through data partitioning, elastic scaling, encryption, and other capabilities. -ShardingSphere 项目(包括 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar)是当当捐入 Apache 的,目前主要由京东数科的一些巨佬维护。 +The ShardingSphere project (including Sharding-JDBC, Sharding-Proxy, and Sharding-Sidecar) was donated to Apache by Dangdang and is currently maintained by several prominent figures from JD Digits. -ShardingSphere 绝对可以说是当前分库分表的首选!ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理、影子库、数据加密和脱敏等功能。 +ShardingSphere is undoubtedly the top choice for database sharding! ShardingSphere's functionality is comprehensive; it supports read-write separation and database sharding while also providing distributed transactions, database governance, shadow libraries, data encryption, and desensitization functionalities. -ShardingSphere 提供的功能如下: +The capabilities offered by ShardingSphere include: -![ShardingSphere 提供的功能](https://oss.javaguide.cn/github/javaguide/high-performance/shardingsphere-features.png) +![ShardingSphere Features](https://oss.javaguide.cn/github/javaguide/high-performance/shardingsphere-features.png) -ShardingSphere 的优势如下(摘自 ShardingSphere 官方文档:): +ShardingSphere's advantages include (extracted from the ShardingSphere official documentation: ): -- 极致性能:驱动程序端历经长年打磨,效率接近原生 JDBC,性能极致。 -- 生态兼容:代理端支持任何通过 MySQL/PostgreSQL 协议的应用访问,驱动程序端可对接任意实现 JDBC 规范的数据库。 -- 业务零侵入:面对数据库替换场景,ShardingSphere 可满足业务无需改造,实现平滑业务迁移。 -- 运维低成本:在保留原技术栈不变前提下,对 DBA 学习、管理成本低,交互友好。 -- 安全稳定:基于成熟数据库底座之上提供增量能力,兼顾安全性及稳定性。 -- 弹性扩展:具备计算、存储平滑在线扩展能力,可满足业务多变的需求。 -- 开放生态:通过多层次(内核、功能、生态)插件化能力,为用户提供可定制满足自身特殊需求的独有系统。 +- **Ultimate performance**: The driver has undergone extensive refinement over years, achieving efficiency close to native JDBC performance. +- **Ecosystem compatibility**: The proxy supports access from any application through MySQL/PostgreSQL protocol, while the driver can interface with any database implementing JDBC standards. +- **Zero-invasion for business**: For scenarios requiring database replacement, ShardingSphere allows for smooth business migration without requiring any alterations to existing applications. +- **Low operational costs**: It maintains low learning and management costs for DBAs while retaining the original technology stack, ensuring user-friendly interaction. +- **Safe and stable**: It provides incremental capabilities based on mature database foundations while balancing security and stability. +- **Elastic scalability**: It has the capacity for smooth online expansion of computing and storage, meeting diverse business demands. +- **Open ecosystem**: It offers customizable solutions for users’ unique needs through multi-layered (core, functionalities, ecosystem) plugin capabilities. -另外,ShardingSphere 的生态体系完善,社区活跃,文档完善,更新和发布比较频繁。 +Additionally, ShardingSphere has a complete ecosystem, an active community, comprehensive documentation, with frequent updates and releases. -不过,还是要多提一句:**现在很多公司都是用的类似于 TiDB 这种分布式关系型数据库,不需要我们手动进行分库分表(数据库层面已经帮我们做了),也不需要解决手动分库分表引入的各种问题,直接一步到位,内置很多实用的功能(如无感扩容和缩容、冷热存储分离)!如果公司条件允许的话,个人也是比较推荐这种方式!** +However, it’s worth reiterating: **Many companies are currently using distributed relational databases like TiDB that eliminate the need for manual sharding (the database layer takes care of this), and they do not have to deal with the various issues that arise from manual sharding. This provides a seamless solution with many built-in practical features (such as seamless expansion and reduction, and cold/hot storage separation)! If conditions allow within the company, I personally recommend this approach!** -### 分库分表后,数据怎么迁移呢? +### How to Migrate Data After Database Sharding? -分库分表之后,我们如何将老库(单库单表)的数据迁移到新库(分库分表后的数据库系统)呢? +After implementing database sharding, how can we migrate data from the old database (single database, single table) to the new database (the database system after sharding)? -比较简单同时也是非常常用的方案就是**停机迁移**,写个脚本老库的数据写到新库中。比如你在凌晨 2 点,系统使用的人数非常少的时候,挂一个公告说系统要维护升级预计 1 小时。然后,你写一个脚本将老库的数据都同步到新库中。 +A simple and commonly used method is **downtime migration**, where you write a script to transfer data from the old database to the new one. For instance, at 2 AM when system usage is minimal, you can put up a notice stating that the system will undergo maintenance and upgrade for about an hour. Then, you can write a script that synchronizes all data from the old database to the new database. -如果你不想停机迁移数据的话,也可以考虑**双写方案**。双写方案是针对那种不能停机迁移的场景,实现起来要稍微麻烦一些。具体原理是这样的: +If you prefer not to have downtime during the migration process, you could consider a **dual-write approach**. The dual-write approach is designed for scenarios where downtime migration is not possible and is somewhat more complex. The specific principle is as follows: -- 我们对老库的更新操作(增删改),同时也要写入新库(双写)。如果操作的数据不存在于新库的话,需要插入到新库中。 这样就能保证,咱们新库里的数据是最新的。 -- 在迁移过程,双写只会让被更新操作过的老库中的数据同步到新库,我们还需要自己写脚本将老库中的数据和新库的数据做比对。如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。 -- 重复上一步的操作,直到老库和新库的数据一致为止。 +- For updates (insertions, deletions, modifications) in the old database, you also write to the new database (dual writing). If the data being operated on does not exist in the new database, it needs to be inserted. This ensures that the new database contains the latest data. +- During the migration, dual writing will only synchronize data that has been updated from the old database to the new database, you’ll also need to write a script to compare the data between the old and new databases. If any data is missing in the new database, insert it. If the new database has data that isn’t in the old database, delete the corresponding redundant entries in the new database. +- Repeat the above operation until the data in the old and new databases are consistent. -想要在项目中实施双写还是比较麻烦的,很容易会出现问题。我们可以借助上面提到的数据库同步工具 Canal 做增量数据迁移(还是依赖 binlog,开发和维护成本较低)。 +Implementing dual writing can be quite tricky in a project, and problems can easily arise. We could use the aforementioned database synchronization tool Canal for incremental data migration (which still relies on binlog, minimizing development and maintenance costs). -## 总结 +## Summary -- 读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。 这样的话,就能够小幅提升写性能,大幅提升读性能。 -- 读写分离基于主从复制,MySQL 主从复制是依赖于 binlog 。 -- **分库** 就是将数据库中的数据分散到不同的数据库上。**分表** 就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。 -- 引入分库分表之后,需要系统解决事务、分布式 id、无法 join 操作问题。 -- 现在很多公司都是用的类似于 TiDB 这种分布式关系型数据库,不需要我们手动进行分库分表(数据库层面已经帮我们做了),也不需要解决手动分库分表引入的各种问题,直接一步到位,内置很多实用的功能(如无感扩容和缩容、冷热存储分离)!如果公司条件允许的话,个人也是比较推荐这种方式! -- 如果必须要手动分库分表的话,ShardingSphere 是首选!ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理等功能。另外,ShardingSphere 的生态体系完善,社区活跃,文档完善,更新和发布比较频繁。 +- Read-write separation mainly aims to distribute read and write operations on the database across different database nodes. This can slightly improve write performance and significantly enhance read performance. +- Read-write separation is based on master-slave replication, with MySQL master-slave replication relying on binlog. +- **Database sharding** involves distributing data across multiple databases. **Table sharding** refers to splitting data from a single table, which can be vertical or horizontal. +- After introducing database sharding, the system needs to address transactional issues, distributed IDs, and the inability to perform join operations. +- Many companies currently utilize distributed relational databases like TiDB, where manual sharding is unnecessary (the database layer handles it), and companies are spared from resolving the various issues introduced by manual sharding, enjoying many built-in practical features (like seamless scaling and cold/hot storage separation)! If company conditions allow, I personally recommend this option! +- If manual sharding is necessary, ShardingSphere is the first choice! ShardingSphere's functionality is comprehensive; in addition to supporting read-write separation and database sharding, it also provides distributed transactions, database governance, among other features. Additionally, ShardingSphere has a robust ecosystem, an active community, comprehensive documentation, with frequent updates and releases. diff --git a/docs/high-performance/sql-optimization.md b/docs/high-performance/sql-optimization.md index 9aa94dfd528..a586bf7c129 100644 --- a/docs/high-performance/sql-optimization.md +++ b/docs/high-performance/sql-optimization.md @@ -1,16 +1,16 @@ --- -title: 常见SQL优化手段总结(付费) -category: 高性能 +title: Summary of Common SQL Optimization Techniques (Paid) +category: High Performance head: - - - meta - - name: keywords - content: 分页优化,索引,Show Profile,慢 SQL - - - meta - - name: description - content: SQL 优化是一个大家都比较关注的热门话题,无论你在面试,还是工作中,都很有可能会遇到。如果某天你负责的某个线上接口,出现了性能问题,需要做优化。那么你首先想到的很有可能是优化 SQL 优化,因为它的改造成本相对于代码来说也要小得多。 + - - meta + - name: keywords + content: Pagination Optimization, Indexing, Show Profile, Slow SQL + - - meta + - name: description + content: SQL optimization is a hot topic that everyone pays attention to. Whether in interviews or in work, you are likely to encounter it. If one day the online interface you are responsible for experiences performance issues and needs optimization, the first thing you might think of is optimizing SQL, as the cost of modification is much lower compared to code. --- -**常见 SQL 优化手段总结** 相关的内容为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。 +**Summary of Common SQL Optimization Techniques** is exclusive content related to my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) (click the link to view detailed introduction and joining methods), and has been organized in [“Java Interview Guide”](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html). ![](https://oss.javaguide.cn/javamianshizhibei/sql-optimization.png) diff --git a/docs/high-quality-technical-articles/README.md b/docs/high-quality-technical-articles/README.md index 149ddba7a4f..c5533759831 100644 --- a/docs/high-quality-technical-articles/README.md +++ b/docs/high-quality-technical-articles/README.md @@ -1,47 +1,47 @@ -# 程序人生 +# Programmer's Life -这里主要会收录一些我看到的或者我自己写的和程序员密切相关的非技术类的优质文章,每一篇都值得你阅读 3 遍以上!常看常新! +This section mainly collects high-quality non-technical articles closely related to programmers that I have seen or written myself. Each one is worth reading more than three times! Read them often to keep them fresh! -## 练级攻略 +## Leveling Up Strategies -- [程序员如何快速学习新技术](./advanced-programmer/programmer-quickly-learn-new-technology.md) -- [程序员的技术成长战略](./advanced-programmer/the-growth-strategy-of-the-technological-giant.md) -- [十年大厂成长之路](./advanced-programmer/ten-years-of-dachang-growth-road.md) -- [美团三年,总结的 10 条血泪教训](./advanced-programmer/meituan-three-year-summary-lesson-10.md) -- [给想成长为高级别开发同学的七条建议](./advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md) -- [糟糕程序员的 20 个坏习惯](./advanced-programmer/20-bad-habits-of-bad-programmers.md) -- [工作五年之后,对技术和业务的思考](./advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md) +- [How Programmers Can Quickly Learn New Technologies](./advanced-programmer/programmer-quickly-learn-new-technology.md) +- [The Growth Strategy of Technological Giants for Programmers](./advanced-programmer/the-growth-strategy-of-the-technological-giant.md) +- [The Growth Journey of a Decade in a Big Company](./advanced-programmer/ten-years-of-dachang-growth-road.md) +- [10 Painful Lessons Learned from Three Years at Meituan](./advanced-programmer/meituan-three-year-summary-lesson-10.md) +- [Seven Tips for Those Who Want to Grow into Senior Developers](./advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md) +- [20 Bad Habits of Poor Programmers](./advanced-programmer/20-bad-habits-of-bad-programmers.md) +- [Reflections on Technology and Business After Five Years of Work](./advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md) -## 个人经历 +## Personal Experiences -- [从校招入职腾讯的四年工作总结](./personal-experience/four-year-work-in-tencent-summary.md) -- [我在滴滴和头条的两年后端研发工作经验分享](./personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md) -- [一个中科大差生的 8 年程序员工作总结](./personal-experience/8-years-programmer-work-summary.md) -- [华为 OD 275 天后,我进了腾讯!](./personal-experience/huawei-od-275-days.md) +- [Summary of Four Years of Work at Tencent After Campus Recruitment](./personal-experience/four-year-work-in-tencent-summary.md) +- [Sharing My Two Years of Back-End Development Experience at Didi and Toutiao](./personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md) +- [An 8-Year Summary of a Poor Student from USTC as a Programmer](./personal-experience/8-years-programmer-work-summary.md) +- [After 275 Days at Huawei OD, I Joined Tencent!](./personal-experience/huawei-od-275-days.md) -## 程序员 +## Programmers -- [程序员最该拿的几种高含金量证书](./programmer/high-value-certifications-for-programmers.md) -- [程序员怎样出版一本技术书](./programmer/how-do-programmers-publish-a-technical-book.md) -- [程序员高效出书避坑和实践指南](./programmer/efficient-book-publishing-and-practice-guide.md) +- [High-Value Certifications Programmers Should Obtain](./programmer/high-value-certifications-for-programmers.md) +- [How Programmers Can Publish a Technical Book](./programmer/how-do-programmers-publish-a-technical-book.md) +- [Guide to Efficient Book Publishing and Avoiding Pitfalls for Programmers](./programmer/efficient-book-publishing-and-practice-guide.md) -## 面试 +## Interviews -- [斩获 20+ 大厂 offer 的面试经验分享](./interview/the-experience-of-get-offer-from-over-20-big-companies.md) -- [一位大龄程序员所经历的面试的历炼和思考](./interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md) -- [从面试官和候选者的角度谈如何准备技术初试](./interview/technical-preliminary-preparation.md) -- [包装严重的 IT 行业,作为面试官,我是如何甄别应聘者的包装程度](./interview/screen-candidates-for-packaging.md) -- [普通人的春招总结(阿里、腾讯 offer)](./interview/summary-of-spring-recruitment.md) -- [2021 校招我的个人经历和经验](./interview/my-personal-experience-in-2021.md) -- [如何在技术初试中考察程序员的技术能力](./interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md) -- [阿里技术面试的一些秘密](./interview/some-secrets-about-alibaba-interview.md) +- [Sharing Interview Experiences That Led to 20+ Offers from Big Companies](./interview/the-experience-of-get-offer-from-over-20-big-companies.md) +- [Reflections and Experiences of an Older Programmer During Interviews](./interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md) +- [How to Prepare for Technical Preliminary Interviews from the Perspectives of Interviewers and Candidates](./interview/technical-preliminary-preparation.md) +- [The Highly Packaged IT Industry: How I Screen Candidates as an Interviewer](./interview/screen-candidates-for-packaging.md) +- [Summary of Spring Recruitment for Ordinary People (Alibaba, Tencent Offers)](./interview/summary-of-spring-recruitment.md) +- [My Personal Experience and Insights from Campus Recruitment in 2021](./interview/my-personal-experience-in-2021.md) +- [How to Assess Programmers' Technical Abilities in the First Technical Interview](./interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md) +- [Some Secrets About Alibaba's Technical Interviews](./interview/some-secrets-about-alibaba-interview.md) -## 工作 +## Work -- [新入职一家公司如何快速进入工作状态](./work/get-into-work-mode-quickly-when-you-join-a-company.md) -- [32 条总结教你提升职场经验](./work/32-tips-improving-career.md) -- [聊聊大厂的绩效考核](./work/employee-performance.md) +- [How to Quickly Get into Work Mode When Joining a New Company](./work/get-into-work-mode-quickly-when-you-join-a-company.md) +- [32 Tips to Improve Your Career Experience](./work/32-tips-improving-career.md) +- [Discussing Performance Evaluations in Big Companies](./work/employee-performance.md) diff --git a/docs/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.md b/docs/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.md index 59191347757..463fc00789f 100644 --- a/docs/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.md +++ b/docs/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.md @@ -1,146 +1,144 @@ --- -title: 糟糕程序员的 20 个坏习惯 -category: 技术文章精选集 +title: 20 Bad Habits of Terrible Programmers +category: Selected Technical Articles author: Kaito tag: - - 练级攻略 + - Leveling Guide --- -> **推荐语**:Kaito 大佬的一篇文章,很实用的建议! +> **Recommendation**: A very practical article by Kaito! > -> **原文地址:** +> **Original link:** -我想你肯定遇到过这样一类程序员:**他们无论是写代码,还是写文档,又或是和别人沟通,都显得特别专业**。每次遇到这类人,我都在想,他们到底是怎么做到的? +I believe you have encountered a type of programmer: **they appear particularly professional in writing code, documentation, or communicating with others**. Every time I meet such individuals, I wonder how they manage it. -随着工作时间的增长,渐渐地我也总结出一些经验,他们身上都保持着一些看似很微小的优秀习惯,但正是因为这些习惯,体现出了一个优秀程序员的基本素养。 +As my work experience has grown, I have gradually summarized some insights: they maintain some seemingly minor but excellent habits, which reflect the basic qualities of an outstanding programmer. -但今天我们来换个角度,来看看一个糟糕程序员有哪些坏习惯?只要我们都能避开这些问题,就可以逐渐向一个优秀程序员靠近。 +Today, however, let's take a different perspective and examine what bad habits a terrible programmer has. As long as we can avoid these issues, we can gradually move toward becoming an excellent programmer. -## 1、技术名词拼写不规范 +## 1. Non-standard Spelling of Technical Terms -无论是个人简历,还是技术文档,我经常看到拼写不规范的技术名词,例如 JAVA、javascript、python、MySql、Hbase、restful。 +In personal resumes or technical documentation, I often see irregular spellings of technical terms, such as JAVA, javascript, python, MySql, Hbase, restful. -正确的拼写应该是 Java、JavaScript、Python、MySQL、HBase、RESTful,不要小看这个问题,很多面试官很有可能因为这一点刷掉你的简历。 +The correct spellings should be Java, JavaScript, Python, MySQL, HBase, RESTful. Don’t underestimate this issue; many interviewers might dismiss your resume due to this. -## 2、写文档,中英文混排不规范 +## 2. Inconsistent Mixing of Chinese and English in Documents -中文描述使用英文标点符号,英文和数字使用了全角字符,中文与英文、数字之间没有空格等等。 +Using English punctuation in Chinese descriptions, full-width characters for English and numbers, and not adding spaces between Chinese and English or numbers, etc. -其中很多人会忽视中文和英文、数字之间加一个「空格」,这样排版阅读起来会更舒服。之前我的文章排版,都是遵循了这些细节。 +Many overlook the importance of adding a "space" between Chinese and English or numbers to make reading more comfortable. My previous articles adhered to these details in formatting. -## 3、重要逻辑不写注释,或写得很拖沓 +## 3. Important Logic Without Comments or Overly Verbose Comments -复杂且重要的逻辑代码,很多程序员不写注释,除了自己能看懂代码逻辑,其他人根本看不懂。或者是注释虽然写了,但写得很拖沓,没有逻辑可言。 +For complex and critical logic, many programmers neglect to write comments. While they can understand their code, others cannot. Alternatively, they might write comments that are overly verbose and lack logical structure. -重要的逻辑不止要写注释,还要写得简洁、清晰。如果是一眼就能读懂的简单代码,可以不加注释。 +Important logic requires concise and clear comments. If the code is simple enough to understand at a glance, comments may not be necessary. -## 4、写复杂冗长的函数 +## 4. Writing Complex and Lengthy Functions -一个函数几百行,一个文件上千行代码,复杂函数不做拆分,导致代码变得越来越难维护,最后谁也不敢动。 +A function spans hundreds of lines, a file contains thousands of lines of code, and complex functions are not divided, making the code increasingly difficult to maintain, leading to a situation where no one dares to modify it. -基本的设计模式还是要遵守的,例如单一职责,一个函数只做一件事,开闭原则,对扩展开放,对修改关闭。 +Basic design patterns should still be followed, such as the Single Responsibility Principle (SRP) where a function should only perform one task, and the Open/Closed Principle (OCP) where it should be open for extension but closed for modification. -如果函数逻辑确实复杂,也至少要保证主干逻辑足够清晰。 +If the function logic is indeed complex, at least ensure the main logic is clear enough. -## 5、不看官方文档,只看垃圾博客 +## 5. Not Reading Official Documentation, Only Junk Blogs -很多人遇到问题不先去看官方文档,而是热衷于去看垃圾博客,这些博客的内容都是互相抄袭,错误百出。 +Many people do not check the official documentation first when encountering a problem but instead are keen to look at junk blogs with plagiarized content full of errors. -其实很多软件官方文档写得已经非常好了,常见问题都能找到答案,认真读一读官方文档,比看垃圾博客强一百倍,要养成看官方文档的好习惯。 +In fact, many software official documents are well-written, with answers to common questions readily available. Taking the time to read the official documentation is a hundred times better than looking at junk blogs; it’s essential to cultivate the habit of consulting official documentation. -## 6、宣扬内功无用论 +## 6. Advocating the Futility of Fundamental Knowledge -有些人天天追求日新月异的开源项目和框架,却不肯花时间去啃一啃底层原理,常见问题虽然可以解决,但遇到稍微深一点的问题就束手无策。 +Some people pursue ever-changing open-source projects and frameworks every day but refuse to spend time understanding underlying principles. They can resolve common issues but are helpless in the face of slightly more complex problems. -很多高大上的架构设计,思路其实都源于底层。想一想,像计算机体系结构、操作系统、网络协议这些东西,经过多少年演进才变为现在的样子,演进过程中遇到的复杂问题比比皆是,理解了解决这些问题的思路,再看上层技术会变得很简单。 +Many sophisticated architectural designs originate from the fundamentals. Consider how much evolution has occurred over the years in computer architecture, operating systems, and network protocols, encountering countless complex problems along the way. Understanding these problem-solving approaches makes it easier to grasp higher-level technologies. -## 7、乐于炫技 +## 7. Eagerness to Show Off Technical Skills -有些人天天把「高大上」的技术名词挂在嘴边,生怕别人不知道自己学了什么高深技术,嘴上乐于炫技,但别人一问他细节就会哑口无言。 +Some people constantly flaunt "high-end" technical terms, fearing others don't realize the advanced technologies they’ve learned. However, when asked for details, they become speechless. -## 8、不接受质疑 +## 8. Inability to Accept Criticism -自己设计的方案,别人提出疑问时只会回怼,而不是理性分析利弊,抱着学习的心态交流。 +When others question their designed solutions, they only retort instead of rationally analyzing the pros and cons, failing to engage in discussions with a learning mindset. -这些人学了点东西就觉得自己很有本事,殊不知只是自己见识太少。 +These individuals often feel capable after learning a bit, unaware that it’s simply due to their lack of exposure. -## 9、接口协议不规范 +## 9. Non-standard API Protocols -和别人定 API 协议全靠口头沟通,不给规范的文档说明,甚至到了测试联调时会发现,竟然和协商的还不一样,或者改协议了却不通知对接方,合作体验极差。 +Setting API protocols based solely on verbal communication without providing a formal documentation, then discovering during testing that the output differs from what was agreed upon, or altering the protocol without informing the counterpart results in a poor collaborative experience. -## 10、遇到问题自己死磕 +## 10. Struggling Alone with Problems -很初级程序员容易犯的问题,遇到问题只会自己死磕,拖到 deadline 也没有产出,领导来问才知道有问题解决不了。 +A common mistake among novice programmers is struggling with issues on their own, resulting in no output until the deadline, only to reveal unresolved problems when asked by a supervisor. -有问题及时反馈才是对自己负责,对团队负责。 +Timely feedback on issues is crucial for personal accountability and team responsibility. -## 11、一说就会,一写就废 +## 11. All Talk, No Action -平时技术方案吹得天花乱坠,一让他写代码就废,典型的眼高手低选手。 +They can speak eloquently about technical solutions but falter when it comes to writing code, showcasing a classic case of having great aspirations but lacking practical capabilities. -## 12、表达没有逻辑,不站在对方角度看问题 +## 12. Disorganized Expression and Lack of Empathy -讨论问题不交代背景,上来就说自己的方案,别人听得云里雾里,让你从头描述你又讲不明白。 +During discussions, they fail to provide background information and dive straight into solutions, leaving others confused; when asked to elaborate, they struggle to clarify. -学会沟通和表达,是合作的基础。 +Learning to communicate and express oneself is foundational to collaboration. -## 13、不主动思考,伸手党 +## 13. Passive and Reluctant to Think -遇到问题不去 google,不做思考就向别人提问,喜欢做伸手党。 +When facing problems, they do not Google for solutions and instead ask others without considering the issue, showcasing a tendency to be reliant on others. -每个人的时间都很宝贵,大家都更喜欢你带着自己的思考来提问,一来可以规避很多低级问题,二来可以提高交流质量。 +Everyone's time is precious; people prefer queries accompanied by one’s own thought process. This avoids many basic issues and enhances the quality of communication. -## 14、经常犯重复的错误 +## 14. Frequently Repeating Mistakes -出问题后说下次会注意,但下次问题依旧,对自己不负责任,说到底是态度问题。 +After an issue arises, they promise to be more careful next time, yet the same problems recur, indicating a lack of personal accountability and ultimately reflecting an attitude problem. -## 15、加功能不考虑扩展性 +## 15. Ignoring Scalability When Adding Features -加新功能只关注某一小块业务,不考虑系统整体的扩展性,堆代码行为严重。 +When adding new features, they only focus on a specific area of business without considering the overall scalability of the system, leading to excessive code bloat. -要学会分析需求和未来可能发生的变化,设计更通用的解决方案,降低后期开发成本。 +It's essential to analyze requirements and potential future changes to design more universal solutions, thereby reducing later development costs. -## 16、接口不自测,出问题不打日志 +## 16. Not Self-testing APIs and Failing to Log Errors -自己开发的接口不自测就和别人联调,出了问题又说没打日志,协作效率极低。 +Collaborating without self-testing their developed APIs and then claiming there's no logging when issues arise results in extremely low collaboration efficiency. -## 17、提交代码不规范 +## 17. Non-standard Code Submission Practices -很多人提交代码不写描述,或者写的是无意义的描述,尤其是修改很少代码时,这种情况会导致回溯问题成本变高。 +Many people submit code without writing a description or writing meaningless ones, especially when they’ve made minimal changes. This can lead to increased costs when tracing back problems. -制定代码提交规范,能让你在每一次提交代码时,不会做太随意的代码修改。 +Establishing code submission standards ensures that you do not make casual code modifications with every submission. -## 18、手动修改生产环境数据库 +## 18. Directly Modifying Production Environment Databases -直连生产环境数据库修改数据,更有 UPDATE / DELETE SQL 忘写 WHERE 条件的情况,产生数据事故。 +Directly connecting to and modifying production databases creates risks, with cases of SQL UPDATE/DELETE missing WHERE conditions, leading to data accidents. -修改生产环境数据库一定要谨慎再谨慎,建议操作前先找同事 review 代码再操作。 +Always exercise extreme caution when modifying production databases; it is advisable to get a colleague to review the code before proceeding. -## 19、没理清需求就直接写代码 +## 19. Writing Code Without Clarifying Requirements -很多程序员接到需求后,不怎么思考就开始写代码,需求和自己理解的有偏差,造成无意义返工。 +Many programmers start coding immediately upon receiving a requirement without much thought, which can lead to misunderstandings and unnecessary rework. -多花些时间梳理需求,能规避很多不合理的问题。 +Taking extra time to clarify requirements can avoid many unreasonable problems. -## 20、重要设计不写文档 +## 20. Lack of Documentation for Important Designs -重要的设计没有文档输出,和别人交接系统时只做口头描述,丢失关键信息。 +Failing to document significant designs and only providing verbal descriptions during handovers leads to the loss of key information. -有时候理解一个设计方案,一个好的文档要比看几百行代码更高效。 +Sometimes understanding a design plan is more efficient through a good document than sifting through hundreds of lines of code. -## 总结 +## Conclusion -以上这些不良习惯,你命中几个呢?或者你身边有没有碰到这样的人? +How many of these bad habits have you encountered? Or do you know anyone like this? -我认为提早规避这些问题,是成为一个优秀程序员必须要做的。这些习惯总结起来大致分为这 4 个方面: +I believe proactively avoiding these issues is essential for becoming an excellent programmer. These habits can be summarized into four key aspects: -- 良好的编程修养 -- 谦虚的学习心态 -- 良好的沟通和表达 -- 注重团队协作 +- Good programming discipline +- A humble attitude toward learning +- Effective communication and expression +- Emphasis on teamwork -优秀程序员的专业技能,我们可能很难在短时间内学会,但这些基本的职业素养,是可以在短期内做到的。 +While it might be challenging to quickly acquire the professional skills of an excellent programmer, cultivating these basic professional qualities can be achieved in a short period. -希望你我可以有则改之,无则加勉。 - - +I hope both you and I can improve ourselves where necessary and strive for excellence. diff --git a/docs/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.md b/docs/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.md index 3ae2a5eac42..e1cb91b811f 100644 --- a/docs/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.md +++ b/docs/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.md @@ -1,172 +1,54 @@ --- -title: 美团三年,总结的10条血泪教训 -category: 技术文章精选集 -author: CityDreamer部落 +title: Meituan in Three Years: 10 Painful Lessons Learned +category: Selected Technical Articles +author: CityDreamer Tribe tag: - - 练级攻略 + - Leveling Guide --- -> **推荐语**:作者用了很多生动的例子和故事展示了自己在美团的成长和感悟,看了之后受益颇多! +> **Recommendation**: The author uses many vivid examples and stories to showcase their growth and insights at Meituan, which are quite beneficial to read! > -> **内容概览**: +> **Content Overview**: > -> 本文的作者提出了以下十条建议,希望能对其他职场人有所启发和帮助: +> The author of this article presents the following ten suggestions, hoping to inspire and help other professionals: > -> 1. 结构化思考与表达,提高个人影响力 -> 2. 忘掉职级,该怼就怼,推动事情往前走 -> 3. 用好平台资源,结识优秀的人,学习通识课 -> 4. 一切都是争取来的,不要等待机会,要主动寻求 -> 5. 关注商业,升维到老板思维,看清趋势,及时止损 -> 6. 培养数据思维,利用数据了解世界,指导决策 -> 7. 做一个好"销售",无论是自己还是产品,都要学会展示和说服 -> 8. 少加班多运动,保持身心健康,提高工作效率 -> 9. 有随时可以离开的底气,不要被职场所困,借假修真,提升自己 -> 10. 只是一份工作,不要过分纠结,相信自己,走出去看看 +> 1. Structured thinking and expression to enhance personal influence +> 1. Forget about job titles; confront issues head-on to push things forward +> 1. Utilize platform resources effectively, meet outstanding people, and learn general knowledge +> 1. Everything is earned; don’t wait for opportunities, actively seek them out +> 1. Focus on business, elevate to a boss's mindset, see trends clearly, and cut losses in time +> 1. Cultivate data thinking, use data to understand the world and guide decisions +> 1. Be a good "salesperson"; whether it's yourself or a product, learn to present and persuade +> 1. Work less overtime and exercise more to maintain physical and mental health, improving work efficiency +> 1. Have the confidence to leave at any time; don’t be trapped by the workplace, use breaks to improve yourself +> 1. It’s just a job; don’t get overly entangled, believe in yourself, and go out to see the world > -> **原文地址**: +> **Original Article Link**: -在美团的三年多时光,如同一部悠长的交响曲,高高低低,而今离开已有一段时间。闲暇之余,梳理了三年多的收获与感慨,总结成 10 条,既是对过去一段时光的的一个深情回眸,也是对未来之路的一份期许。 +My time at Meituan, spanning over three years, feels like a long symphony with its highs and lows, and it has been a while since I left. In my spare time, I have organized my gains and reflections from these years into ten points, which serve as both a heartfelt look back at the past and a hopeful outlook for the future. -倘若一些感悟能为刚步入职场的年轻人,或是刚在职业生涯中崭露头角的后起之秀,带来一点点启示与帮助,也是莫大的荣幸。 +If some insights can provide a bit of inspiration and help to young people just entering the workforce or to rising stars in their careers, it would be a great honor. -## 01 结构化思考与表达 +## 01 Structured Thinking and Expression -美团是一家特别讲究方法论的公司,人人都要熟读四大名著《高效能人士的七个习惯》、《金字塔原理》、《用图表说话》和《学会提问》。 +Meituan is a company that places great emphasis on methodology; everyone is expected to be well-versed in the four classics: "The 7 Habits of Highly Effective People," "The Pyramid Principle," "Talk Like TED," and "A More Beautiful Question." -与结构化思考和表达相关的,是《金字塔原理》,作者是麦肯锡公司第一位女性咨询顾问。这本书告诉我们,思考和表达的过程,就像构建金字塔(或者构建一棵树),先有整体结论,再寻找证据,证据之间要讲究相互独立、而且能穷尽(MECE 原则),论证的过程也要按特定的顺序进行,比如时间顺序、空间顺序、重要性顺序…… +Related to structured thinking and expression is "The Pyramid Principle," authored by the first female consultant at McKinsey. This book teaches us that the process of thinking and expression is like building a pyramid (or a tree); you start with an overall conclusion, then seek evidence, ensuring that the evidence is mutually exclusive and collectively exhaustive (MECE principle), and the argumentation process should follow a specific order, such as chronological, spatial, or by importance... -作为大厂社畜,日常很大一部分工作就是写文档、看别人文档。大家做的事,但最后呈现的结果却有很大差异。一篇逻辑清晰、详略得当的文档,给人一种如沐春风的感受,能提炼出重要信息,是好的参考指南。 +As a corporate worker, a significant part of daily work involves writing documents and reviewing others' documents. While everyone does similar tasks, the final results can vary greatly. A logically clear and appropriately detailed document provides a refreshing experience, distilling important information and serving as a good reference guide. -结构化思考与表达算是职场最通用的能力,也是打造个人影响力最重要的途径之一。 +Structured thinking and expression are among the most universal skills in the workplace and are one of the most important ways to build personal influence. -## 02 忘掉职级,该怼就怼 +## 02 Forget About Job Titles; Confront Issues Head-On -在阿里工作时,能看到每个人的 Title,看到江湖地位高(职级高+入职时间早)的同学,即便跟自己没有汇报关系,不自然的会多一层敬畏。推进工作时,会多一层压力,对方未读或已读未回时,不知如何应对。 +When I worked at Alibaba, I could see everyone’s titles and would naturally feel a layer of respect for those with higher status (higher title + longer tenure), even if they were not in my reporting line. This added pressure when pushing work forward, especially when the other party had read but not responded. -美团只能看到每个人的坑位信息,还有 Ta 的上级。工作相关的问题,可以向任何人提问,如果协同方没有及时响应,隔段时间@一次,甚至"怼一怼",都没啥问题,事情一直往前推进才最重要。除了大象消息直接提问外,还有个大杀器--TT(公司级问题流转系统),在上面提问时,加上对方主管,如果对方未及时回应,问题会自动升级,每天定时 Push,直到解决为止。 +At Meituan, you can only see each person's position and their superior. You can ask anyone about work-related issues, and if the collaborator does not respond promptly, it’s perfectly fine to @ them again after a while or even "confront" them; what matters most is pushing things forward. Besides directly asking in the group chat, there’s a powerful tool—TT (the company-wide issue circulation system). When you ask a question there and tag the other party's supervisor, if they do not respond in time, the issue will automatically escalate and be pushed daily until resolved. -我见到一些很年轻的同事,他们在推动 OKR、要资源的事上,很有一套,只要能达到自己的目标,不会考虑别人的感受,最终,他们还真能把事办成。 +I’ve seen some very young colleagues who are quite adept at pushing OKRs and requesting resources; as long as they can achieve their goals, they don’t consider others' feelings, and they often get things done. -当然了,段位越高的人,越能用自己的人格魅力、影响力、资源等,去影响和推动事情的进程,而不是靠对他人的 Push。只是在拿结果的事上,不要把自己太当回事,把别人太当回事,大家在一起,也只是为了完成各自的任务,忘掉职级,该怼时还得怼。 +Of course, those at higher levels can use their charisma, influence, and resources to affect and drive the process rather than relying solely on pushing others. However, when it comes to achieving results, don’t take yourself too seriously or others too seriously; everyone is just here to complete their tasks. Forget about job titles and confront issues when necessary. -## 03 用好平台资源 +## 03 Make Good Use of Platform Resources -没有人能在一家公司待一辈子,公司再牛,跟自己关系不大,重要的是,在有限的时间内,最大化用好平台资源。 - -在美团除了认识自己节点的同事外,有幸认识一群特别棒的协作方,还有其他 BU 的同学。 - -这些优秀的人身上,有很多共同的特质:谦虚、利他、乐于分享、双赢思维。 - -有两位做运营的同学。 - -一位是无意中关注他公众号结识上的。他公众号记录了很多职场成长、家庭建造上的思考和收获,还有定期个人复盘。他和太太都是大厂中层管理者,从文章中看到的不是他多厉害,而是非常接地气的故事。我们约饭了两次,有很多共同话题,现在还时不时有一些互动。 - -一位职级更高的同学,他在内网发起了一个"请我喝一杯咖啡,和我一起聊聊个人困惑"的活动,我报名参与了一期。和他聊天的过程,特别像是一场教练对话(最近学习教练课程时才感受到的),帮我排除干扰、聚焦目标的同时,也从他分享个人成长蜕变的过程,收获很多动力。(刚好自己最近也学习了教练技术,后面也准备采用类似的方式,去帮助曾经像我一样迷茫的人) - -还有一些协作方同学。他们工作做得超级到位,能感受到,他们在乎他人时间;稍微有点出彩的事儿,不忘记拉上更多人。利他和双赢思维,在他们身上是最好的阐释。 - -除了结识优秀的人,向他们学习外,还可以关注各个通道/工种的课程资源。 - -在大厂,多数人的角色都是螺丝钉,但千万不要局限于做一颗螺丝钉。多去学习一些通识课,了解商业交付的各个环节,看清商业世界,明白自己的定位,超越自己的定位。 - -## 04 一切都是争取来的 - -工作很多年了,很晚才明白这个道理。 - -之前一直认为,只要做好自己该做的,一定会被看见,被赏识,也会得到更多机会。但很多时候,这只是个人的一厢情愿。除了自己,不会有人关心你的权益。 - -社会主义初级阶段,我国国内的主要矛盾是人民日益增长的物质文化需要同落后的社会生产之间的矛盾。无论在哪里,资源都是稀缺的,自己在乎的,就得去争取。 - -想成长某个技能、想参与哪个模块、想做哪个项目,升职加薪……自己不提,不去争取,不会有人主动给你。 - -争不争取是一回事,能不能得到是一回事,只有争取,才有可能得到。争取了,即便没有得到,最终也没失去什么。 - -## 05 关注商业 - -大公司,极度关注效率,大部分岗位,拆解的粒度越细,效率会越高,这些对组织是有利的。但对个人来说,则很容易螺丝钉化。 - -做技术的同学,更是这样。 - -做前端的同学,不会关注数据是如何落库的;做后端的同学,不会思考页面是否存在兼容性问题;做业务开发的,不用考虑微服务诸多中间件是如何搭建起来的…… - -大部分人都想着怎么把自己这摊子事搞好,不会去思考上下游同学在做些什么,更少有人真正关注商业,关心公司的盈利模式,关心每一次产品迭代到底带来哪些业务价值。 - -把手头的事做好是应该的,但绝不能停留在此。所有的产品,只有在商业社会产生交付,让客户真正获益,才是有价值的。 - -关注商业,能帮我们升维到老板思维,明白投入产出比,抓大放小;也帮助我们,在碰到不好的业务时,及时止损;更重要的是,它帮助我们真正看清趋势,提前做好准备。 - -《五分钟商学院》系列,是很好的商业入门级书籍。尽管作者刘润最近存在争议,但不可否认,他比我们大多数人段位还是高很多,他的书值得一读。 - -## 06 培养数据思维 - -当今数字化时代,数据思维显得尤为重要。数据不仅可以帮助我们更好地了解世界,还可以指导我们的决策和行动。 - -非常幸运的是,在阿里和美团的两份经历,都是做商业化广告业务,在离钱💰最近的地方,也培养了数据的敏感性。见过商业数据指标的定义、加工、生产和应用全流程,也在不断熏陶下,能看懂大部分指标背后的价值。 - -除了直接面向业务的数据,还有研发协作全流程产生的数据。数据被记录和汇总统计后,能直观地看到每个环节的效率和质量。螺丝钉们的工作,也彻彻底底被数字量化,除了积极面对虚拟化、线上化、数字化外,我们别无他法。 - -受工作数据化的影响,生活中,我也渐渐变成了一个数据记录狂,日常运动(骑行、跑步、健走等)必须通过智能手表记录下来,没带 Apple Watch,感觉这次白运动了。每天也在很努力地完成三个圆环。 - -数据时代,我们沦为了透明人。也得益于数据被记录和分析,我们做任何事,都能快速得到反馈,这也是自我提升的一个重要环节。 - -## 07 做一个好"销售" - -就某种程度来说,所有的工作,本质都是销售。 - -这是很多大咖的观点,我也是很晚才明白这个道理。 - -我们去一家公司应聘,本质上是在讲一个「我很牛」的故事,销售的是自己;日常工作汇报、季度/年度述职、晋升答辩,是在销售自己;在任何一个场合曝光,也是在销售自己。 - -如果我们所服务的组织,对外提供的是一件产品或一项服务,所有上下游协作的同学,唯一在做的事就是,齐心协力把产品/服务卖出去, 我们本质做的还是销售。 - -所以, 千万不要看不起任何销售,也不要认为认为销售是一件很丢面子的事。 - -真正的大佬,随时随地都在销售。 - -## 08 少加班多运动 - -在职场,大家都认同一个观点,工作是做不完的。 - -我们要做的是,用好时间管理四象限法,识别重要程度和优先级,有限时间,聚焦在固定几件事上。 - -这要求我们不断提高自己的问题识别能力、拆解能力,还有专注力。 - -我们会因为部分项目的需要而加班,但不会长期加班。 - -加班时间短一点,就能腾出更多时间运动。 - -最近一次线下培训课,认识一位老师 Hubert,Hubert 是一位超级有魅力的中年大叔(可以通过「有意思教练」的课程链接到他),从外企高管的位置离开后,和太太一起创办了一家培训机构。作为公司高层,日常工作非常忙,头发也有些花白了,但一身腱子肉胜过很多健身教练,给人的状态也是很年轻。聊天得知,Hubert 经常 5 点多起来泡健身房~ - -我身边还有一些同事,跟我年龄差不多,因为长期加班,发福严重,比实际年龄看起来苍老 10+岁; - -还有同事曾经加班进 ICU,幸好后面身体慢慢恢复过来。 - -某某厂员工长期加班猝死的例子,更是屡见不鲜。 - -减少加班,增加运动,绝对是一件性价比极高的事。 - -## 09 有随时可以离开的底气 - -当今职场,跟父辈时候完全不一样,职业的多样性和变化性越来越快,很少有人能够在同一份工作或同一个公司待一辈子。除了某些特定的岗位,如公务员、事业单位等,大多数人都会在职业生涯中经历多次的职业变化和调整。 - -在商业组织里,个体是弱势群体,但不要做弱者。每一段职场,每一项工作,都是上天给我们的修炼。 - -我很喜欢"借假修真"这个词。我们参与的大大小小的项目, 重要吗?对公司来说可能重要,对个人来说,则未必。我们去做,一方面是迫于生计; - -另外一方面,参与每个项目的感悟、心得、体会,是真实存在的,很多的能力,都是在这个过程得到提升。 - -明白这一点,就不会被职场所困,会刻意在各样事上提升自己,积累的越多,对事务的本质理解的越深、越广,也越发相信很多底层知识是通用的,内心越平静,也会建立起随时都可以离开的底气。 - -## 10 只是一份工作 - -工作中,我们时常会遇到各种挑战和困难,如发展瓶颈、难以处理的人和事,甚至职场 PUA 等。这些经历可能会让我们感到疲惫、沮丧,甚至怀疑自己的能力和价值。然而,重要的是要明白,困难只是成长道路上的暂时阻碍,而不是我们的定义。 - -写总结和复盘是很好的方式,可以帮我们理清思路,找到问题的根源,并学习如何应对类似的情况。但也要注意不要陷入自我怀疑和内耗的陷阱。遇到困难时,应该学会相信自己,积极寻找解决问题的方法,而不是过分纠结于自己的不足和错误。 - -内网常有同学匿名分享工作压力过大,常常失眠甚至中度抑郁,每次看到这些话题,非常难过。大环境不好,是不争的事实,但并不代表个体就没有出路。 - -我们容易预设困难,容易加很多"可是",当窗户布满灰尘时,不要试图努力把窗户擦干净,走出去吧,你将看到一片蔚蓝的天空。 - -## 最后 - -写到最后,特别感恩美团三年多的经历。感谢我的 Leader 们,感谢曾经并肩作战过的小伙伴,感谢遇到的每一位和我一样在平凡的岗位,努力想带给身边一片微光的同学。所有的相遇,都是缘分。 +No one can stay at a company for a lifetime; no matter how great the company is, it’s not diff --git a/docs/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.md b/docs/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.md index 3cd553a182e..878f30c4e68 100644 --- a/docs/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.md +++ b/docs/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.md @@ -1,50 +1,50 @@ --- -title: 程序员如何快速学习新技术 -category: 技术文章精选集 +title: How Programmers Can Quickly Learn New Technologies +category: Selected Technical Articles tag: - - 练级攻略 + - Leveling Up Strategy --- -> **推荐语**:这是[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)练级攻略篇中的一篇文章,分享了我对于如何快速学习一门新技术的看法。 +> **Recommendation**: This is an article from the leveling up strategy section of [《Java Interview Guide》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html), sharing my views on how to quickly learn a new technology. > -> ![《Java 面试指北》练级攻略篇](https://oss.javaguide.cn/javamianshizhibei/training-strategy-articles.png) +> ![《Java Interview Guide》 leveling up strategy section](https://oss.javaguide.cn/javamianshizhibei/training-strategy-articles.png) -很多时候,我们因为工作原因需要快速学习某项技术,进而在项目中应用。或者说,我们想要去面试的公司要求的某项技术我们之前没有接触过,为了应对面试需要,我们需要快速掌握这项技术。 +Often, we need to quickly learn a technology due to work reasons and then apply it in projects. Alternatively, we might need to learn a technology that the company we are interviewing with requires, which we have not encountered before, in order to prepare for the interview. -作为一个人纯自学出生的程序员,这篇文章简单聊聊自己对于如何快速学习某项技术的看法。 +As a self-taught programmer, this article briefly discusses my thoughts on how to quickly learn a technology. -学习任何一门技术的时候,一定要先搞清楚这个技术是为了解决什么问题的。深入学习这个技术的之前,一定先从全局的角度来了解这个技术,思考一下它是由哪些模块构成的,提供了哪些功能,和同类的技术想必它有什么优势。 +When learning any technology, it's essential to first understand what problem this technology aims to solve. Before diving deeper into the technology, start by gaining a global perspective of it, considering what modules it consists of, what features it provides, and what advantages it has compared to similar technologies. -比如说我们在学习 Spring 的时候,通过 Spring 官方文档你就可以知道 Spring 最新的技术动态,Spring 包含哪些模块 以及 Spring 可以帮你解决什么问题。 +For instance, when we learn Spring, you can know about the latest technological developments of Spring and what modules it includes through the official Spring documentation, as well as what problems Spring can help you solve. ![](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/20210506110341207.png) -再比如说我在学习消息队列的时候,我会先去了解这个消息队列一般在系统中有什么作用,帮助我们解决了什么问题。消息队列的种类很多,具体学习研究某个消息队列的时候,我会将其和自己已经学习过的消息队列作比较。像我自己在学习 RocketMQ 的时候,就会先将其和自己曾经学习过的第 1 个消息队列 ActiveMQ 进行比较,思考 RocketMQ 相对于 ActiveMQ 有了哪些提升,解决了 ActiveMQ 的哪些痛点,两者有哪些相似的地方,又有哪些不同的地方。 +Similarly, when I'm learning message queues, I would first understand the general role of this message queue in systems and what problems it helps us solve. There are many types of message queues, and when specifically studying a certain message queue, I would compare it with the ones I've learned before. For example, when learning RocketMQ, I would compare it with the first message queue I learned, ActiveMQ, considering what improvements RocketMQ has made over ActiveMQ, what pain points it addresses, what similarities exist between the two, and what differences there are. -**学习一个技术最有效最快的办法就是将这个技术和自己之前学到的技术建立连接,形成一个网络。** +**The most effective and quickest way to learn a technology is to establish connections between this technology and what you have learned before, forming a network.** -然后,我建议你先去看看官方文档的教程,运行一下相关的 Demo ,做一些小项目。 +Then, I recommend you check out the official documentation tutorials, run related demos, and work on some small projects. -不过,官方文档通常是英文的,通常只有国产项目以及少部分国外的项目提供了中文文档。并且,官方文档介绍的往往也比较粗糙,不太适合初学者作为学习资料。 +However, official documentation is often in English, and typically only domestic projects and a few foreign projects offer Chinese documentation. Moreover, the official documents are usually somewhat superficial and may not be well-suited for beginners as learning material. -如果你看不太懂官网的文档,你也可以搜索相关的关键词找一些高质量的博客或者视频来看。 **一定不要一上来就想着要搞懂这个技术的原理。** +If you find it hard to understand the official documents, you can search for related keywords to find some high-quality blogs or videos. **Do not start by trying to understand the principles of the technology right away.** -就比如说我们在学习 Spring 框架的时候,我建议你在搞懂 Spring 框架所解决的问题之后,不是直接去开始研究 Spring 框架的原理或者源码,而是先实际去体验一下 Spring 框架提供的核心功能 IoC(Inverse of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程),使用 Spring 框架写一些 Demo,甚至是使用 Spring 框架做一些小项目。 +For instance, when learning the Spring framework, after understanding the problems Spring framework addresses, instead of diving directly into the principles or source code of Spring, it's better to first experience the core functionalities provided by the Spring framework, such as IoC (Inversion of Control) and AOP (Aspect-Oriented Programming). You can write some demos using the Spring framework or even undertake small projects using it. -一言以蔽之, **在研究这个技术的原理之前,先要搞懂这个技术是怎么使用的。** +In summary, **before studying the principles of a technology, you should first understand how to use that technology.** -这样的循序渐进的学习过程,可以逐渐帮你建立学习的快感,获得即时的成就感,避免直接研究原理性的知识而被劝退。 +This step-by-step learning process can gradually help you build a sense of enjoyment in learning and achieve immediate feelings of accomplishment, avoiding the discouragement that can come from directly tackling theoretical knowledge. -**研究某个技术原理的时候,为了避免内容过于抽象,我们同样可以动手实践。** +**When studying the principles of a technology, to avoid the content being too abstract, we can also practice hands-on.** -比如说我们学习 Tomcat 原理的时候,我们发现 Tomcat 的自定义线程池挺有意思,那我们自己也可以手写一个定制版的线程池。再比如我们学习 Dubbo 原理的时候,可以自己动手造一个简易版的 RPC 框架。 +For example, when studying the principle of Tomcat, we might find that Tomcat's custom thread pool is quite interesting, prompting us to write our own customized thread pool. Similarly, when studying the principle of Dubbo, we could create a simplified version of an RPC framework. -另外,学习项目中需要用到的技术和面试中需要用到的技术其实还是有一些差别的。 +Additionally, learning technologies needed for projects and those required for interviews does have some differences. -如果你学习某一项技术是为了在实际项目中使用的话,那你的侧重点就是学习这项技术的使用以及最佳实践,了解这项技术在使用过程中可能会遇到的问题。你的最终目标就是这项技术为项目带来了实际的效果,并且,这个效果是正面的。 +If you're learning a technology for actual project use, your focus should be on understanding how to utilize it and best practices, as well as being aware of potential issues that may arise during usage. Your ultimate goal is to achieve a tangible, positive effect from this technology in the project. -如果你学习某一项技术仅仅是为了面试的话,那你的侧重点就应该放在这项技术在面试中最常见的一些问题上,也就是我们常说的八股文。 +If you are only learning a technology for the sake of an interview, your focus should be on the most common questions regarding that technology in interviews, often referred to as "eight-legged essays." -很多人一提到八股文,就是一脸不屑。在我看来,如果你不是死记硬背八股文,而是去所思考这些面试题的本质。那你在准备八股文的过程中,同样也能让你加深对这项技术的了解。 +Many people disdain the term "eight-legged essays." However, I believe that if you don't rote learn them but think about the essence of these interview questions, then preparing for them can also deepen your understanding of the technology. -最后,最重要同时也是最难的还是 **知行合一!知行合一!知行合一!** 不论是编程还是其他领域,最重要不是你知道的有多少,而是要尽量做到知行合一。 +Finally, the most important and challenging aspect is **unity of knowledge and action! Unity of knowledge and action! Unity of knowledge and action!** Whether in programming or other fields, what matters most is not how much you know but how well you can apply that knowledge in practice. diff --git a/docs/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md b/docs/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md index ef273f47b5c..3dd6f453d9a 100644 --- a/docs/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md +++ b/docs/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md @@ -1,107 +1,65 @@ --- -title: 给想成长为高级别开发同学的七条建议 -category: 技术文章精选集 +title: Seven Suggestions for Aspiring Senior Developers +category: Selected Technical Articles author: Kaito tag: - - 练级攻略 + - Leveling Guide --- -> **推荐语**:普通程序员要想成长为高级程序员甚至是专家等更高级别,应该注意在哪些方面注意加强?开发内功修炼号主飞哥在这篇文章中就给出了七条实用的建议。 +> **Recommendation**: For ordinary programmers who want to grow into senior programmers or even experts, what aspects should they focus on strengthening? In this article, Feige, the author of "Development Internal Skills Training," provides seven practical suggestions. > -> **内容概览**: +> **Content Overview**: > -> 1. 刻意加强需求评审能力 -> 2. 主动思考效率 -> 3. 加强内功能力 -> 4. 思考性能 -> 5. 重视线上 -> 6. 关注全局 -> 7. 归纳总结能力 +> 1. Deliberately strengthen requirement review skills +> 1. Proactively think about efficiency +> 1. Enhance internal capabilities +> 1. Consider performance +> 1. Value online operations +> 1. Pay attention to the big picture +> 1. Summarization and induction skills > -> **原文地址**: +> **Original Article Link**: -### 建议 1:刻意加强需求评审能力 +### Suggestion 1: Deliberately Strengthen Requirement Review Skills -先从需求评审开始说。在互联网公司,需求评审是开发工作的主要入口。 +Let's start with requirement reviews. In internet companies, requirement reviews are the main entry point for development work. -对于普通程序员来说,一般就是根据产品经理提的需求细节,开始设想这个功能要怎么实现,开发成本大概需要多长时间。把自己当成了需求到代码之间的翻译官。很少去思考需求的合理性,对于自己做的事情有多大价值,不管也不问。 +For ordinary programmers, it usually means imagining how to implement a feature based on the details provided by the product manager, estimating how long the development will take. They see themselves as translators between requirements and code, rarely considering the rationality of the requirements or the value of what they are doing. -而对于高级别的程序员来说,并不会一开始就陷入细节,而是会更多地会从产品本身出发,询问产品经理为啥要做这个细节,目的是啥。换个说法,就是会先考虑这个需求是不是合理。 +In contrast, senior programmers do not get bogged down in details from the start; instead, they focus more on the product itself, asking the product manager why this detail is necessary and what its purpose is. In other words, they first consider whether the requirement is reasonable. -如果需求高级不合理就进行 PK ,要么对需求进行调整,要么就砍掉。不过要注意的是 PK 和调整需求不仅仅砍需求,还有另外一个方向,那就是对需求进行加强。 +If the requirement is unreasonable, they will engage in a discussion, either adjusting the requirement or discarding it. However, it's important to note that this discussion and adjustment can also mean enhancing the requirement rather than just cutting it. -产品同学由于缺乏技术背景,很可能想的并不够充分,这个时候如果你有更好的想法,也完全可以提出来,加到需求里,让这个需求变得更有价值。 +Product colleagues, lacking a technical background, may not think things through sufficiently. If you have a better idea, you can propose it to be included in the requirement, making it more valuable. -总之,高级程序员并不会一五一十地按产品经理的需求文档来进行后面的开发,而是**一切从有利于业务的角度出发思考,对产品经理的需求进行删、改、增。** +In summary, senior programmers do not simply follow the product manager's requirement document for subsequent development; instead, they **think from the perspective of what benefits the business, modifying, deleting, or adding to the product manager's requirements.** -这样的工作表面看似和开发无关,但是只有这样才能保证后续所有开发同学都是有价值的,而不是做一堆无用功。无用功做的多了会极大的挫伤开发的成就感。 +This work may seem unrelated to development on the surface, but it is the only way to ensure that all subsequent developers are creating value rather than doing a lot of useless work. Excessive useless work can greatly diminish a developer's sense of achievement. -所以,**普通程序员要想成长为更高级别的开发,一定要加强需求评审能力的培养**。 +Therefore, **if ordinary programmers want to grow into more senior developers, they must strengthen their requirement review skills.** -### 建议 2:主动思考效率 +### Suggestion 2: Proactively Think About Efficiency -普通的程序员,按部就班的去写代码,有活儿来我就干,没活儿的时候我就呆着。很少去深度思考现有的这些代码为什么要这么写,这么写的好处是啥,有哪些地方存在瓶颈,我是否可以把它优化一些。 +Ordinary programmers tend to write code in a step-by-step manner, doing tasks as they come and idling when there is no work. They rarely deeply consider why the existing code is written this way, what the benefits are, where the bottlenecks exist, and whether they can optimize it. -而高级一点程序员,并不会局限于把手头的活儿开发就算完事。他们会主动去琢磨,现在这种开发模式是不是不够的好。那么我是否能做一个什么东西能把这个效率给提升起来。 +In contrast, more advanced programmers do not limit themselves to just completing the tasks at hand. They actively ponder whether the current development model is sufficient. They think about what they can create to improve efficiency. -举一个小例子,我 6 年前接手一个项目的时候,我发现运营一个月会找我四次,就是找我给她发送一个推送。她说以前的开发都是这么帮他弄的。虽然这个需求处理起来很简单,改两行发布一下就完事。但是烦啊,你想象一下你正专心写代码呢,她又双叒来找你了,思路全被她中断了。而且频繁地操作线上本来就会引入不确定的风险,万一那天手一抽抽搞错了,线上就完蛋了。 +For example, when I took over a project six years ago, I found that the operations team would come to me four times a month just to ask me to send a push notification. They said previous developers had helped them this way. Although this request was simple—just changing a couple of lines and releasing it—it was annoying. Imagine being focused on writing code and then being interrupted repeatedly. Moreover, frequently operating online introduces uncertain risks; if a mistake happens, it could lead to significant issues. -我的做法就是,我专门抽了一周的时间,给她做了一套运营后台。这样以后所有的运营推送她就直接在后台上操作就完事了。我倒出精力去做其它更有价值的事情去了。 +My solution was to dedicate a week to create an operations backend for them. This way, they could handle all push notifications directly through the backend, allowing me to focus on more valuable tasks. -所以,**第二个建议就是要主动思考一下现有工作中哪些地方效率有改进的空间,想到了就主动去改进它!** +Thus, **the second suggestion is to proactively think about where there is room for improvement in your current work efficiency and take the initiative to improve it!** -### 建议 3:加强内功能力 +### Suggestion 3: Enhance Internal Capabilities -哪些算是内功呢,我想内功修炼的读者们肯定也都很熟悉的了,指的就是大家学校里都学过的操作系统、网络等这些基础。 +What constitutes internal capabilities? Readers familiar with internal skills training will know that it refers to the fundamentals we all learned in school, such as operating systems and networks. -普通的程序员会觉得,这些基础知识我都会好么,我大学可是足足学了四年的。工作了以后并不会刻意来回头再来加强自己在这些基础上的深层次的提升。 +Ordinary programmers might think, "I know these foundational concepts; I studied them for four years in college." However, after starting work, they often do not intentionally revisit and deepen their understanding of these fundamentals. -高级的程序员,非常清楚自己当年学的那点知识太皮毛了。工作之余也会深入地去研究 Linux、研究网络等方向的底层实现。 +Senior programmers are acutely aware that the knowledge they learned back then was superficial. In their spare time, they delve into the underlying implementations of Linux, networking, and other areas. -事实上,互联网业界的技术大牛们很大程度是因为对这些基础的理解相当是深厚,具备了深厚的内功以后才促使他们成长为了技术大牛。 +In fact, many technical experts in the internet industry have a profound understanding of these fundamentals, which has contributed to their growth as technical leaders. -我很难相信一个不理解底层,只会 CURD,只会用别人框架的开发将来能在技术方向成长为大牛。 +I find it hard to believe that someone who does not understand the underlying principles and only knows how to perform CRUD operations or use others' frameworks can grow into a technical expert. -所以,**还建议多多锻炼底层技术内功能力**。如果你不知道怎么练,那就坚持看「开发内功修炼」公众号。 - -### 建议 4:思考性能 - -普通程序员往往就是把需求开发完了就不管了,只要需求实现了,测试通过了就可以交付了。将来流量会有多大,没想过。自己的服务 QPS 能支撑多少,不清楚。 - -而高级的程序员往往会关注自己写出来的代码的性能。 - -在需求评审的时候,他们一般就会估算大概的请求流量有多大。进而设计阶段就会根据这个量设计符合性能要求的方案。 - -在上线之前也会进行性能压测,检验一下在性能上是否符合预期。如果性能存在问题,瓶颈在哪儿,怎么样能进行优化一下。 - -所以,**第四个建议就是一定要多多主动你所负责业务的性能,并多多进行优化和改进**。我想这个建议的重要程度非常之高。但这是需要你具备深厚的内功才可以办的到的,否则如果你连网络是怎么工作的都不清楚,谈何优化! - -### 建议 5:重视线上 - -普通程序员往往对线上的事情很少去关注,手里记录的服务器就是自己的开发机和发布机,线上机器有几台,流量多大,最近有没有波动这些可能都不清楚。 - -而高级的程序员深深的明白,有条件的话,会尽量多多观察自己的线上服务,观察一下代码跑的咋样,有没有啥 error log。请求峰值的时候 CPU、内存的消耗咋样。网络端口消耗的情况咋样,是否需要调节一些参数配置。 - -当性能不尽如人意的时候,可能会回头再来思考出性能的改进方案,重新开发和上线。 - -你会发现在线上出问题的时候,能紧急扑上前线救火的都是高级一点的程序员。 - -所以,**飞哥给的第五个建议就是要多多观察线上运行情况**。只有多多关注线上,当线上出故障的时候,你才能承担的起快速排出线上问题的重任。 - -### 建议 6:关注全局 - -普通程序员是你分配给我哪个模块,我就干哪个模块,给自己的工作设定了非常小的一个边界,自己所有的眼光都聚集在这个小框框内。 - -高级程序员是团队内所有项目模块,哪怕不是他负责的,他也会去熟悉,去了解。具备这种思维的同学无论在技术上,无论是在业务上,成长的也都是最快的。在职级上得到晋升,或者是职位上得到提拔的往往都是这类同学。 - -甚至有更高级别的同学,还不止于把目光放在团队内,甚至还会关注公司内其它团队,甚至是业界的业务和技术栈。写到这里我想起了张一鸣说过的,不给自己的工作设边界。 - -所以,**建议要有大局观,不仅仅是你负责的模块,整个项目其实你都应该去关注**。而不是连自己组内同学做的是啥都不知道。 - -### 建议 7:归纳总结能力 - -普通程序员往往是工作的事情做完就拉到,很少回头去对自己的技术,对业务进行归纳和总结。 - -而高级的程序员往往都会在一件比较大的事情做完之后总结一下,做个 ppt,写个博客啥的记录下来。这样既对自己的工作是一个归纳,也可以分享给其它同学,促进团队的共同成长。 - - +Therefore, **I suggest you work on enhancing your foundational technical skills**. If you don't know how to practice diff --git a/docs/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.md b/docs/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.md index 045c2bfed66..e8788ef4849 100644 --- a/docs/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.md +++ b/docs/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.md @@ -1,136 +1,58 @@ --- -title: 十年大厂成长之路 -category: 技术文章精选集 +title: The Growth Journey in a Big Company Over Ten Years +category: Selected Technical Articles author: CodingBetterLife tag: - - 练级攻略 + - Leveling Guide --- -> **推荐语**:这篇文章的作者有着丰富的工作经验,曾在大厂工作了 12 年。结合自己走过的弯路和接触过的优秀技术人,他总结出了一些对于个人成长具有普遍指导意义的经验和特质。 +> **Recommendation**: The author of this article has extensive work experience, having worked in a large company for 12 years. Combining the detours he has taken and the excellent technical individuals he has encountered, he summarizes some universally guiding experiences and qualities for personal growth. > -> **原文地址:** +> **Original Article Link:** -最近这段时间,有好几个年轻的同学和我聊到自己的迷茫。其中有关于技术成长的、有关于晋升的、有关于择业的。我很高兴他们愿意听我这个“过来人”分享自己的经验。 +Recently, several young colleagues have talked to me about their confusion. Some discussions were about technical growth, some about promotions, and some about career choices. I am glad they are willing to listen to the experiences of someone who has been through it. -我自己毕业后进入大厂,在大厂工作 12 年,我说的内容都来自于我自己或者身边人的真实情况。尤其,我会把 **【我自己走过的弯路】** 和 **【我看到过的优秀技术人的特质】** 相结合来给出建议。 +After graduating, I entered a large company and worked there for 12 years. The content I share comes from my own experiences or those of people around me. In particular, I will combine **[the detours I have taken]** and **[the qualities of excellent technical individuals I have observed]** to provide advice. -这些内容我觉得具有普遍的指导意义,所以决定做个整理分享出来。我相信,无论你在大厂还是小厂,如果你相信这些建议,或早或晚他们会帮助到你。 +I believe these insights have universal guiding significance, so I decided to organize and share them. I believe that whether you are in a large or small company, if you believe in this advice, it will help you sooner or later. -我自己工作 12 年,走了些弯路,所以我就来讲讲,“在一个技术人 10 年的发展过程中,应该注意些什么”。我们把内容分为两块: +Having worked for 12 years and taken some detours myself, I want to discuss "what to pay attention to in the development of a technical person over ten years." We will divide the content into two parts: -1. **十年技术路怎么走** -2. **一些重要选择** +1. **How to Navigate the Technical Path Over Ten Years** +1. **Some Important Choices** -## 01 十年技术路怎么走 +## 01 How to Navigate the Technical Path Over Ten Years -### 【1-2 年】=> 从“菜鸟”到“职业” +### [1-2 Years] => From "Novice" to "Professional" -应届生刚进入到工作时,会有各种不适应。比如写好的代码会被反复打回、和团队老司机讨论技术问题会有一堆问号、不敢提问和质疑、碰到问题一个人使劲死磕等等。 +When fresh graduates first enter the workforce, they often face various adjustments. For example, well-written code may be repeatedly rejected, discussions with experienced team members may leave them with many questions, they may hesitate to ask questions or challenge ideas, and they may struggle alone when encountering problems. -**简单来说就是,即使日以继夜地埋头苦干,最后也无法顺利的开展工作。** +**In simple terms, even if you work day and night, you may still find it difficult to carry out your work smoothly.** -这个阶段最重要的几个点: +The most important points during this stage are: -**【多看多模仿】**:比如写代码的时候,不要就像在学校完成书本作业那样只关心功能是否正确,还要关心模块的设计、异常的处理、代码的可读性等等。在你还没有了解这些内容的精髓之前,也要照猫画虎地模仿起来,慢慢地你就会越来越明白真实世界的代码是怎么写的,以及为什么要这么写。 +**[Observe and Imitate]**: For instance, when writing code, don’t just focus on whether the functionality is correct like you did in school; also pay attention to module design, exception handling, code readability, etc. Even before you fully understand these concepts, start imitating what you see, and gradually you will understand how real-world code is written and why it is written that way. -做技术方案的时候也是同理,技术文档的要求你也许并不理解,但你可以先参考已有文档写起来。 +The same applies when creating technical solutions; you may not fully understand the requirements of technical documentation, but you can start by referencing existing documents. -**【脸皮厚一点】**:不懂就问,你是新人大家都是理解的。你做的各种方案也可以多找老司机们 review,不要怕被看笑话。 +**[Be a Little Thick-Skinned]**: Don’t hesitate to ask questions; everyone understands you are a newcomer. You can also seek reviews from experienced colleagues for your various proposals; don’t be afraid of being laughed at. -**【关注工作方式】**:比如发现需求在计划时间完不成就要尽快报风险、及时做好工作内容的汇报(例如周报)、开会后确定会议结论和 todo 项、承诺时间就要尽力完成、严格遵循公司的要求(例如发布规范、权限规范等) +**[Focus on Work Methods]**: For example, if you find that a requirement cannot be completed within the planned time, report the risk as soon as possible, and ensure timely reporting of work content (such as weekly reports), confirm meeting conclusions and to-do items after meetings, and strive to meet deadlines while strictly adhering to company requirements (such as release specifications, permission specifications, etc.). -一般来说,工作 2 年后,你就应该成为一个职业人。老板可以相信任何工作交到你的手里,不会出现“意外”(例如一个重要需求明天要上线了,突然被告知上不了)。 +Generally speaking, after working for two years, you should become a professional. Your boss should be able to trust you with any task without unexpected issues (for example, being told that an important requirement cannot go live tomorrow). -### 【3-4 年】=> 从“职业”到“尖兵” +### [3-4 Years] => From "Professional" to "Elite" -工作两年后,对业务以及现有系统的了解已经到达了一定的程度,技术同学会开始承担更有难度的技术挑战。 +After two years of work, your understanding of the business and existing systems reaches a certain level, and technical colleagues will begin to take on more challenging technical tasks. -例如需要将性能提升到某一个水位、例如需要对某一个重要模块进行重构、例如有个重要的项目需要协同 N 个团队一起完成。 +For example, you may need to improve performance to a certain level, refactor an important module, or collaborate with multiple teams to complete a significant project. -可见,上述的这些技术问题,难度都已经远远超过一个普通的需求。解决这些问题需要有一定的技术能力,同时也需要具备更高的协同能力。 +Clearly, these technical issues are far more complex than ordinary requirements. Solving these problems requires a certain level of technical ability, as well as higher collaboration skills. -这个阶段最重要的几个点: +The most important points during this stage are: -**【技术能力提升】**:无论是公司内还是公司外的技术内容,都要多做主动的学习。基本上这个阶段的技术难题都集中在【性能】【稳定性】和【扩展性】上,而这些内容在业界都是有成型的方法论的。 +**[Enhance Technical Skills]**: Actively learn from both internal and external technical content. Most technical challenges at this stage focus on [performance], [stability], and [scalability], and there are established methodologies in the industry for these areas. -**【主人翁精神】**:技术难题除了技术方案设计及落地外,背后还有一系列的其他工作。例如上线后对效果的观测、重点项目对于上下游改造和风险的了解程度、对于整个技改后续的计划(二期、三期的优化思路)等。 +**[Sense of Ownership]**: Technical challenges involve not only the design and implementation of technical solutions but also a series of other tasks. For example, observing the effects after going live, understanding the degree of transformation and risks for key projects, and planning for subsequent technical improvements (such as phase two and phase three optimization ideas). -在工作四年后,基本上你成为了团队的一、二号技术位。很多技术难题即使不是你来落地,也是由你来决定方案。你会做调研、会做方案对比、会考虑整个技改的生命周期。 - -### 【5-7 年】=> 从“尖兵”到“专家” - -技术尖兵重点在于解决某一个具体的技术难题或者重点项目。而下一步的发展方向,就是能够承担起来一整个“业务板块”,也就是“领域技术专家”。 - -想要承担一整个“业务板块”需要 **【对业务领域有深刻的理解,同时基于这些理解来规划技术的发展方向】** 。 - -拿支付做个例子。简单的支付功能其实很容易完成,只要处理好和双联(网联和银联)的接口调用(成功、失败、异常)即可。但在很多背景下,支付没有那么简单。 - -例如,支付是一个用户敏感型操作,非常强调用户体验,如何能兼顾体验和接口的不稳定?支付接口还需要承担费用,同步和异步的接口费用不同,如何能够降本?支付接口往往还有限额等。这一系列问题的背后涉及到很多技术的设计,包括异步化、补偿设计、资金流设计、最终一致性设计等等。 - -这个阶段最重要的几个点: - -**【深入理解行业及趋势】**:密切关注行业的各种变化(新鲜的玩法、政策的变动、竞对的策略、科技等外在因素的影响等等),和业务同学加强沟通。 - -**【深入了解行业解决方案】**:充分对标已有的国内外技术方案,做深入学习和尝试,评估建设及运维成本,结合业务趋势制定计划。 - -### 【8-10 年】=> 从“专家”到“TL” - -其实很多时候,如果能做到专家,基本也是一个 TL 的角色了,但这并不代表正在执行 TL 的职责。 - -专家虽然已经可以做到“为业务发展而规划好技术发展”,但问题是要怎么落地呢?显然,靠一个人的力量是不可能完成建设的。所以,这里的 TL 更多强调的不是“领导”这个职位,而是 **【通过聚合一个团队的力量来实施技术规划】** 。 - -所以,这里的 TL 需要具备【团队技术培养】【合理分配资源】【确认工作优先级】【激励与奖惩】等各种能力。 - -这个阶段最重要的几个点: - -**【学习管理学】**:这里的管理学当然不是指 PUA,而是指如何在每个同学都有各自诉求的现实背景下,让个人目标和团队目标相结合,产生向前发展的动力。 - -**【始终扎根技术】**:很多时候,工作重心偏向管理以后,就会荒废技术。但事实是,一个优秀的领导永远是一个优秀的技术人。参与一起讨论技术方案并给予指导、不断扩展自己的技术宽度、保持对技术的好奇心,这些是让一个技术领导持续拥有向心力的关键。 - -## 02 一些重要选择 - -下面来聊聊在十年间我们可能会碰到的一些重要选择。这些都是真实的血与泪的教训。 - -### 我该不该转岗? - -大厂都有转岗的机制。转岗可以帮助员工寻找自己感兴趣的方向,也可以帮助新型团队招募有即战力的同学。 - -转岗看似只是在公司内部变动,但你需要谨慎决定。 - -本人转岗过多次。虽然还在同一家公司,但转岗等同于换工作。无论是领域沉淀、工作内容、信任关系、协作关系都是从零开始。 - -针对转岗我的建议是:**如果你是想要拓宽自己的技术广度,也就是抱着提升技术能力的想法,我觉得可以转岗。但如果你想要晋升,不建议你转岗。**晋升需要在一个领域的持续积淀和在一个团队信任感的持续建立。 - -当然,转岗可能还有其他原因,例如家庭原因、身体原因等,这个不展开讨论了。 - -### 我该不该跳槽? - -跳槽和转岗一样,往往有很多因素造成,不能一概而论,我仅以几个场景来说: - -**【晋升失败】**:扪心自问,如果你觉得自己确实还不够格,那你就踏踏实实继续努力。如果你觉得评委有失偏颇,你可以尝试去外面面试一下,让市场来给你答案。 - -**【成长局限】**:觉得自己做的事情没有挑战,无法成长。你可以和老板聊一下,有可能是因为你没有看到其中的挑战,也有可能老板没有意识到你的“野心”。 - -**【氛围不适】**:一般来自于新入职或者领导更换,这种情况下不适是正常的。我的建议是,**如果一个环境是“对事不对人”的,那就可以留下来**,努力去适应,这种不适应只是做事方式不同导致的。但如果这个环境是“对人不对事”的话,走吧。 - -### 跳槽该找怎样的工作? - -我们跳槽的时候往往会同时面试好几家公司。行情好的时候,往往可以收到多家 offer,那么我们要如何选择呢? - -考虑一个 offer 往往有这几点:【公司品牌】【薪资待遇】【职级职称】【技术背景】。每个同学其实都有自己的诉求,所以无论做什么选择都没有对错之分。 - -我的一个建议是:**你要关注新岗位的空间,这个空间是有希望满足你的期待的**。 - -比如,你想成为架构师,那新岗位是否有足够的技术挑战来帮助你提升技术能力,而不仅仅是疲于奔命地应付需求? - -比如,你想往技术管理发展,那新岗位是否有带人的机会?是否有足够的问题需要搭建团队来解决? - -比如,你想扎根在某个领域持续发展(例如电商、游戏),那新岗位是不是延续这个领域,并且可以碰到更多这个领域的问题? - -当然,如果薪资实在高到无法拒绝,以上参考可以忽略! - -## 结语 - -以上就是我对互联网从业技术人员十年成长之路的心得,希望在你困惑和关键选择的时候可以帮助到你。如果我的只言片语能够在未来的某个时间帮助到你哪怕一点,那将是我莫大的荣幸。 - - +After four years of work, you will generally become one of the top technical positions in the team. Many technical challenges may not be implemented by you, but you will be responsible for deciding diff --git a/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md b/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md index 20aed477d3a..1744dd6095b 100644 --- a/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md +++ b/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md @@ -1,205 +1,203 @@ --- -title: 程序员的技术成长战略 -category: 技术文章精选集 -author: 波波微课 +title: Programmer's Technical Growth Strategy +category: Selected Technical Articles +author: Bobo Weike tag: - - 练级攻略 + - Leveling Strategy --- -> **推荐语**:波波老师的一篇文章,写的非常好,不光是对技术成长有帮助,其他领域也是同样适用的!建议反复阅读,形成一套自己的技术成长策略。 +> **Recommendation**: An excellent article by Teacher Bobo, which is not only helpful for technical growth but also applicable in other fields! It is recommended to read it repeatedly to form your own technical growth strategy. > -> **原文地址:** +> **Original Article URL:** -## 1. 前言 +## 1. Introduction -在波波的微信技术交流群里头,经常有学员问关于技术人该如何学习成长的问题,虽然是微信交流,但我依然可以感受到小伙伴们焦虑的心情。 +In Bobo's WeChat technical exchange group, students often ask questions about how technical individuals can learn and grow. Although this is a WeChat exchange, I can still sense the anxiety among my peers. -**技术人为啥焦虑?** 恕我直言,说白了是胆识不足格局太小。胆就是胆量,焦虑的人一般对未来的不确定性怀有恐惧。识就是见识,焦虑的人一般看不清楚周围世界,也看不清自己和适合自己的道路。格局也称志向,容易焦虑的人通常视野窄志向小。如果从战略和管理的视角来看,就是对自己和周围世界的认知不足,没有一个清晰和长期的学习成长战略,也没有可执行的阶段性目标计划+严格的执行。 +**Why are technical professionals anxious?** To put it bluntly, it is a lack of courage and a narrow perspective. "Courage" refers to bravery; anxious individuals generally fear the uncertainty of the future. "Perspective" refers to insight; anxious individuals typically fail to see the surrounding world clearly and don't understand themselves and the path suitable for them. "Perspective" can also refer to ambition; those who are easily anxious often have narrow vision and small ambitions. From a strategic and managerial perspective, this reflects insufficient awareness of oneself and the surrounding world, a lack of a clear and long-term learning and growth strategy, and no executable phased goal plans combined with strict execution. -因为问此类问题的学员很多,让我感觉有点烦了,为了避免重复回答,所以我专门总结梳理了这篇长文,试图统一来回答这类问题。如果后面还有学员问类似问题,我会引导他们来读这篇文章,然后让他们用三个月、一年甚至更长的时间,去思考和回答这样一个问题:**你的技术成长战略究竟是什么?** 如果你想清楚了这个问题,有清晰和可落地的答案,那么恭喜你,你只需按部就班执行就好,根本无需焦虑,你实现自己的战略目标并做出成就只是一个时间问题;否则,你仍然需要通过不断磨炼+思考,务必去搞清楚这个人生的大问题!!! +Since there are many students asking such questions, it has become somewhat tedious for me. To avoid repeated responses, I have specially summarized this long article, attempting to address these questions uniformly. If any students ask similar questions in the future, I will guide them to read this article and then encourage them to take three months, a year, or even longer to think through and answer a question: **What is your technical growth strategy?** If you can clearly answer this question, you can simply follow through with execution, and there is no need for anxiety; achieving your strategic goals and accomplishments is just a matter of time. Otherwise, you still need to refine and reflect continuously, making sure to understand this significant life question!!! -下面我们来看一些行业技术大牛是怎么做的。 +Now, let’s look at how some industry technical experts approach their growth strategies. -## 二. 跟技术大牛学成长战略 +## 2. Learn Growth Strategies from Technical Experts -我们知道软件设计是有设计模式(Design Pattern)的,其实技术人的成长也是有成长模式(Growth Pattern)的。波波经常在 Linkedin 上看一些技术大牛的成长履历,探究其中的成长模式,从而启发制定自己的技术成长战略。 +We know that software design has design patterns; in fact, there are also growth patterns (Growth Patterns) for technical professionals. Bobo often reviews the growth histories of technical elites on LinkedIn to explore their growth patterns, inspiring the formulation of his own technical growth strategy. -当然,很少有技术大牛会清晰地告诉你他们的技术成长战略,以及每一年的细分落地计划。但是,这并不妨碍我们通过他们的过往履历和产出成果,去溯源他们的技术成长战略。实际上, **越是牛逼的技术人,他们的技术成长战略和路径越是清晰,我们越容易从中探究出一些成功的模式。** +Of course, very few technical elites will explicitly tell you about their growth strategies and the segmented implementation plans for each year. However, this does not prevent us from tracing their growth strategies through their past experiences and results. In reality, **the more exceptional the technical person, the clearer their growth strategy and path become, making it easier for us to identify successful patterns.** -### 2.1 系统性能专家案例 +### 2.1 Case Study of a Systems Performance Expert -国内的开发者大都热衷于系统性能优化,有些人甚至三句话离不开高性能/高并发,但真正能深入这个领域,做到专家级水平的却寥寥无几。 +Most domestic developers are enthusiastic about system performance optimization, with some individuals constantly discussing high performance and high concurrency, yet only a few truly penetrate this field and reach an expert level. -我这边要特别介绍的这个技术大牛叫 **Brendan Gregg** ,他是系统性能领域经典书《System Performance: Enterprise and the Cloud》(中文版[《性能之巅:洞悉系统、企业和云计算》](https://www.amazon.cn/dp/B08GC261P9))的作者,也是著名的[性能分析利器火焰图(Flame Graph)](https://github.com/brendangregg/FlameGraph)的作者。 +The technical elite I want to introduce here is **Brendan Gregg**, the author of the classic book in the systems performance domain, **System Performance: Enterprise and the Cloud** (Chinese version: [性能之巅:洞悉系统、企业和云计算](https://www.amazon.cn/dp/B08GC261P9)), and also the creator of the famous [Flame Graph](https://github.com/brendangregg/FlameGraph) performance analysis tool. -Brendan Gregg 之前是 Netflix 公司的高级性能架构师,在 Netflix 工作近 7 年。2022 年 4 月,他离开了 Netflix 去了 Intel,担任院士职位。 +Brendan Gregg was previously a senior performance architect at Netflix, working there for nearly seven years. In April 2022, he left Netflix to join Intel in an academic position. ![](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/cdb11ce2f1c3a69fd19e922a7f5f59bf.png) -总体上,他已经在系统性能领域深耕超过 10 年,[Brendan Gregg 的过往履历](https://www.linkedin.com/in/brendangregg/)可以在 linkedin 上看到。在这 10 年间,除了书籍以外,Brendan Gregg 还产出了超过上百份和系统性能相关的技术文档,演讲视频/ppt,还有各种工具软件,相关内容都整整齐齐地分享在[他的技术博客](http://www.brendangregg.com/)上,可以说他是一个非常高产的技术大牛。 +Overall, he has been deeply immersed in the realm of system performance for over a decade. You can view [Brendan Gregg's past experiences](https://www.linkedin.com/in/brendangregg/) on LinkedIn. During these ten years, Brendan Gregg has produced more than a hundred technical documents, presentations/videos, and various tool software related to system performance, all neatly shared on [his technical blog](http://www.brendangregg.com/). He can be considered a highly productive technical expert. -![性能工具](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231802218.png) +![Performance Tools](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231802218.png) -上图来自 Brendan Gregg 的新书《BPF Performance Tools: Linux System and Application Observability》。从这个图可以看出,Brendan Gregg 对系统性能领域的掌握程度,已经深挖到了硬件、操作系统和应用的每一个角落,可以说是 360 度无死角,整个计算机系统对他来说几乎都是透明的。波波认为,Brendan Gregg 是名副其实的,世界级的,系统性能领域的大神级人物。 +The above image comes from Brendan Gregg's new book, **BPF Performance Tools: Linux System and Application Observability**. From this image, it's evident that Brendan Gregg's mastery of the system performance field has delved into every corner of hardware, operating systems, and applications. One could say he has a 360-degree unobstructed view; the entire computer system is almost transparent to him. Bobo believes Brendan Gregg is undoubtedly a world-class figure in the field of systems performance. -### 2.2 从开源到企业案例 +### 2.2 From Open Source to Enterprise Case Study -我要分享的第二个技术大牛是 **Jay Kreps**,他是知名的开源消息中间件 Kafka 的创始人/架构师,也是 Confluent 公司的联合创始人和 CEO,Confluent 公司是围绕 Kafka 开发企业级产品和服务的技术公司。 +The second technical elite I want to share is **Jay Kreps**, the founder/architect of the well-known open-source message broker Kafka, as well as the co-founder and CEO of Confluent, a technology company that develops enterprise-level products and services around Kafka. -从[Jay Kreps 的 Linkedin 的履历](https://www.linkedin.com/in/jaykreps/)上我们可以看出,Jay Kreps 之前在 Linkedin 工作了 7 年多(2007.6 ~ 2014. 9),从高级工程师、工程主管,一直做到首席资深工程师。Kafka 大致是在 2010 年,Jay Kreps 在 Linkedin 发起的一个项目,解决 Linkedin 内部的大数据采集、存储和消费问题。之后,他和他的团队一直专注 Kafka 的打磨,开源(2011 年初)和社区生态的建设。 +From [Jay Kreps' LinkedIn profile](https://www.linkedin.com/in/jaykreps/), we can see that he worked at LinkedIn for over seven years (June 2007 - September 2014), rising from senior engineer to engineering manager, ultimately serving as a principal engineer. Kafka was a project initiated by Jay Kreps at LinkedIn around 2010 to solve internal issues with data collection, storage, and consumption. He and his team then focused on refining Kafka, open-sourcing it (early 2011), and building its community ecosystem. -到 2014 年底,Kafka 在社区已经非常成功,有了一个比较大的用户群,于是 Jay Kreps 就和几个早期作者一起离开了 Linkedin,成立了[Confluent 公司](https://tech.163.com/14/1107/18/AAFG92LD00094ODU.html),开始了 Kafka 和周边产品的企业化服务道路。今年(2020.4 月),Confluent 公司已经获得 E 轮 2.5 亿美金融资,公司估值达到 45 亿美金。从 Kafka 诞生到现在,Jay Kreps 差不多在这个产品和公司上投入了整整 10 年。 +By the end of 2014, Kafka had become very successful in the community, with a significant user base. Consequently, Jay Kreps, along with several early authors, left LinkedIn to establish [Confluent](https://tech.163.com/14/1107/18/AAFG92LD00094ODU.html) and began the journey of providing enterprise-level services for Kafka and related products. As of April 2020, Confluent had secured $250 million in Series E funding, with a company valuation reaching $4.5 billion. From the inception of Kafka until now, Jay Kreps has invested almost a full decade into this product and company. -![Confluent创始人三人组](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231805796.png) +![Confluent Founders Trio](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231805796.png) -上图是 Confluent 创始人三人组,一个非常有意思的组合,一个中国人(左),一个印度人(右),中间的 Jay Kreps 是美国人。 +The above image shows the three founders of Confluent, a very interesting combination, with one Chinese individual (left), one Indian individual (right), and Jay Kreps as the American in the middle. -我之所以对 Kafka 和 Jay Kreps 的印象特别深刻,是因为在 2012 年下半年,我在携程框架部也是专门搞大数据采集的,我还开发过一套功能类似 Kafka 的 Log Collector + Agent 产品。我记得同时期有不止 4 个同类型的开源产品:Facebook Scribe、Apache Chukwa、Apache Flume 和 Apache Kafka。现在回头看,只有 Kafka 走到现在发展得最好,这个和创始人的专注和持续投入是分不开的,当然背后和几个创始人的技术大格局也是分不开的。 +The reason I have a particularly deep impression of Kafka and Jay Kreps is that in the second half of 2012, I was also engaged in big data collection at Ctrip's architecture department, where I developed a product similar to Kafka called Log Collector + Agent. I recall during that period, there were more than four similar open-source projects: Facebook Scribe, Apache Chukwa, Apache Flume, and Apache Kafka. Looking back now, only Kafka has thrived and developed successfully, which is inseparable from the dedication and sustained input from its founder. Of course, the high-level technical perspective of several founders also plays a crucial role. -当年我对战略性思维几乎没有概念,还处在**什么技术都想学、认为各种项目做得越多越牛的阶段**。搞了半年的数据采集以后,我就掉头搞其它“更有趣的”项目去了(从这个事情的侧面,也可以看出我当年的技术格局是很小的)。中间我陆续关注过 Jay 的一些创业动向,但是没想到他能把 Confluent 公司发展到目前这个规模。现在回想,其实在十年前,Jay Kreps 对自己的技术成长就有比较明确的战略性思考,也具有大的技术格局和成事的一些必要特质。Jay Kreps 和 Kafka 给我上了一堂生动的技术战略和实践课。 +At that time, I had almost no concept of strategic thinking and was at the stage of **wanting to learn everything, believing that doing more projects would make me a better engineer**. After half a year of working on data collection, I shifted to other "more interesting" projects (this also reflects the limited technical perspective I had back then). Throughout this time, I paid attention to Jay's entrepreneurial movements, yet I was surprised by the scale Confluent has reached today. In retrospect, ten years ago, Jay Kreps had fairly clear strategic thinking regarding his technical growth, coupled with significant technical perspective and necessary traits for success. Jay Kreps and Kafka provided me with a vivid lesson in technical strategy and practice. -### 2.3 技术媒体大 V 案例 +### 2.3 Technical Media Influencer Case Study -介绍到这里,有些同学可能会反驳说:波波你讲的这些大牛都是学历背景好,功底扎实起点高,所以他们才更能成功。其实不然,这里我再要介绍一位技术媒体界的大 V 叫 Brad Traversy,大家可以看[他的 Linkedin 简历](https://www.linkedin.com/in/bradtraversy/),背景很一般,学历差不多是一个非正规的社区大学(相当于大专),没有正规大厂工作经历,有限几份工作一直是在做网站外包。 +At this point, some students might rebut, “Bobo, the elites you're mentioning all have great academic backgrounds and solid foundations, which is why they can achieve greater success.” This is not entirely the case; I want to introduce another technical media influencer, **Brad Traversy**, whose [LinkedIn resume](https://www.linkedin.com/in/bradtraversy/) reveals an average background, having attended a non-formal community college (equivalent to a diploma), and lacking formal work experience in major companies, with limited job roles mostly in website outsourcing. ![](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/30d6d67dc6dd5f9251f2f01af4de53fc.png) -但是!!!Brad Traversy 目前是技术媒体领域的一个大 V,当前[他在 Youtube 上的频道](https://www.youtube.com/c/TraversyMedia)有 138 万多的订阅量,10 年累计输出 Web 开发和编程相关教学视频超过 800 个。Brad Traversy 也是 [Udemy](https://www.udemy.com/user/brad-traversy/) 上的一个成功讲师,目前已经在 Udemy 上累计输出课程 19 门,购课学生数量近 42 万。 +But!!! Brad Traversy is currently a big name in the technical media field; his [YouTube channel](https://www.youtube.com/c/TraversyMedia) has over 1.38 million subscribers, and over ten years he has produced more than 800 teaching videos on web development and programming. Brad Traversy is also a successful instructor on [Udemy](https://www.udemy.com/user/brad-traversy/), having released 19 courses with close to 420,000 students enrolled. ![](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/160b0bc4f689413757b9b5e2448f940b.png) -Brad Traversy 目前是自由职业者,他的 Youtube 广告+Udemy 课程的收入相当不错。 +Currently, Brad Traversy works as a freelancer, and his income from YouTube advertisements and Udemy courses is quite substantial. -就是这样一位技术媒体大 V,你很难想象,在年轻的时候,贴在他身上的标签是:不良少年,酗酒,抽烟,吸毒,纹身,进监狱。。。直 - -到结婚后的第一个孩子诞生,他才开始担起责任做出改变,然后凭借对技术的一腔热情,开始在 Youtube 平台上持续输出免费课程。从此他找到了适合自己的战略目标,然后人生开始发生各种积极的变化。。。如果大家对 Brad Traversy 的过往经历感兴趣,推荐观看他在 Youtube 上的自述视频[《My Struggles & Success》](https://www.youtube.com/watch?v=zA9krklwADI)。 +It is hard to imagine that this major figure in technical media once bore labels of a troubled youth—drinking, smoking, using drugs, getting tattoos, and even going to jail... It wasn't until the birth of his first child after marriage that he began to take responsibility and change his ways. Driven by his passion for technology, he started to consistently provide free courses on YouTube. From there, he found a strategic goal that suited him, leading to various positive changes in his life. If you're interested in Brad Traversy's past experiences, I recommend watching his self-telling video on YouTube, [My Struggles & Success](https://www.youtube.com/watch?v=zA9krklwADI). ![My Struggles & Success](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231830686.png) -我粗略浏览了[Brad Traversy 在 Youtube 上的所有视频](https://www.youtube.com/c/TraversyMedia/videos),10 年总计输出 800+视频,平均每年 80+。第一个视频提交于 2010 年 8 月,刚开始几年几乎没有订阅量,2017 年 1 月订阅量才到 50k,这中间差不多隔了 6 年。2017.10 月订阅量猛增到 200k,2018 年 3 月订阅量到 300k。当前 2021.1 月,订阅量达到 138 万。可以认为从 2017 开始,也就是在积累了 6 ~ 7 年后,他的订阅量开始出现拐点。**如果把这些数据画出来,将会是一条非常漂亮的复利曲线**。 +I briefly browsed through [Brad Traversy's entire collection of videos on YouTube](https://www.youtube.com/c/TraversyMedia/videos). Over the span of ten years, he has produced more than 800 videos, averaging over 80 per year. His first video was submitted in August 2010, and in the early years, he hardly gained subscribers; it was not until January 2017 that his subscriber count reached 50k, almost six years in. In October 2017, the subscriber count surged to 200k, and by March 2018, it reached 300k. As of January 2021, his subscriber count has reached 1.38 million. From 2017 onward, we can consider that after six to seven years of accumulation, his subscriber growth began to show an inflection point. **If we were to visualize this data, it would form a beautiful compounding growth curve.** -### 2.4 案例小结 +### 2.4 Case Summary -Brendan Gregg,Jay Kreps 和 Brad Traversy 三个人走的技术路线各不相同,但是他们的成功具有共性或者说模式: +Brendan Gregg, Jay Kreps, and Brad Traversy each took different technical paths, yet their successes share commonalities or patterns: -**1、找到了适合自己的长期战略目标。** +**1. They have found their long-term strategic goals that suit them.** -- Brendan Gregg: 成为系统性能领域顶级专家 -- Jay Kreps:开创基于 Kafka 开源消息队列的企业服务公司,并将公司做到上市 -- Brad Traversy: 成为技术媒体领域大 V 和课程讲师,并以此作为自己的职业 +- Brendan Gregg: Become a top expert in the field of system performance. +- Jay Kreps: Establish an enterprise service company based on the open-source message queue Kafka and take the company public. +- Brad Traversy: Become a major player and course instructor in the technical media field, making this his profession. -**2、专注深耕一个(或有限几个相关的)细分领域(Niche),保持定力,不随便切换领域。** +**2. They focused on and delved deeply into one (or a few related) niche areas, maintaining their resolve without casually switching fields.** -- Brendan Gregg:系统性能领域 -- Jay Kreps: 消息中间件/实时计算领域+创业 -- Brad Traversy: 技术媒体/教学领域,方向 Web 开发 + 编程语言 +- Brendan Gregg: Systems performance field. +- Jay Kreps: Message middleware/real-time computing field + entrepreneurship. +- Brad Traversy: Technical media/teaching field, focusing on web development + programming languages. -**3、长期投入,三人都持续投入了 10 年。** +**3. Long-term investment; each has consistently invested for 10 years.** -**4、年度细分计划+持续可量化的价值产出(Persistent & Measurable Value Output)。** +**4. Annual segmented plans + continuous measurable value output.** -- Brendan Gregg:除公司日常工作产出以外,每年有超过 10 份以上的技术文档和演讲视频产出,平均每年有 2.5 个开源工具产出。十年共产出书籍 2 本,其中《System Performance》已经更新到第二版。 -- Jay Kreps:总体有开源产品+公司产出,1 本书产出,每年有 Kafka 和周边产品发版若干。 -- Brad Traversy: 每年有 Youtube 免费视频产出(平均每年 80+)+Udemy 收费视频课产出(平均每年 1.5 门)。 +- Brendan Gregg: Besides his regular company work output, he produces over ten technical documents and presentation videos each year, averaging 2.5 open-source tools released annually. Over ten years, he has published two books, with **System Performance** now in its second edition. +- Jay Kreps: Overall outputs include open-source products and company deliverables, plus one published book and numerous releases of Kafka and related products every year. +- Brad Traversy: He releases free YouTube videos each year (averaging over 80) and offers courses on Udemy (averaging 1.5 courses annually). -**5、以终为始是牛人和普通人的一大区别。** +**5. Starting with the end in mind is a significant difference between elite individuals and ordinary ones.** -普通人通常走一步算一步,很少长远规划。牛人通 常是先有远大目标,然后采用倒推法,将大目标细化到每年/月/周的详细落地计划。Brendan Gregg,Jay Kreps 和 Brad Traversy 三人都是以终为始的典型。 +Ordinary individuals usually think step by step, rarely planning for the long term. Elite individuals often start with grand goals, employing a reverse-engineering approach to refine those goals into detailed implementation plans for each year/month/week. Brendan Gregg, Jay Kreps, and Brad Traversy exemplify the practice of starting with the end in mind. -![以终为始](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231833871.png) +![Start with the end in mind](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231833871.png) -上面总结了几位技术大牛的成长模式,其中一个重点就是:这些大牛的成长都是通过 **持续有价值产出(Persistent Valuable Output)** 来驱动的。持续产出为啥如此重要,这个还要从下面的学习金字塔说起。 +The growth models of these technical elites summarized above highlight a key point: their growth was driven by **persistent valuable output**. Understanding why sustained output is crucial leads us to the following concept of the learning pyramid. -## 三、学习金字塔和刻意训练 +## 3. Learning Pyramid and Deliberate Practice -![学习金字塔](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231836811.png) +![Learning Pyramid](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231836811.png) -学习金字塔是美国缅因州国家训练实验室的研究成果,它认为: +The learning pyramid is the result of research from the National Training Laboratory in Maine, USA, which suggests that: -> 1. 我们平时上课听讲之后,学习内容平均留存率大致只有 5%左右; -> 2. 书本阅读的平均留存率大致只有 10%左右; -> 3. 学习配上视听效果的课程,平均留存率大致在 20%左右; -> 4. 老师实际动手做实验演示后的平均留存率大致在 30%左右; -> 5. 小组讨论(尤其是辩论后)的平均留存率可以达到 50%左右; -> 6. 在实践中实际应用所学之后,平均留存率可以达到 75%左右; -> 7. 在实践的基础上,再把所学梳理出来,转而再传授给他人后,平均留存率可以达到 90%左右。 +> 1. After attending lectures, the average retention rate of the content learned is approximately 5%. +> 1. The average retention rate from reading books is about 10%. +> 1. Courses coupled with audiovisual effects yield an average retention rate of around 20%. +> 1. When a teacher conducts hands-on experiments/demonstrations, the average retention rate rises to about 30%. +> 1. Group discussions (especially after debates) can achieve an average retention rate of 50%. +> 1. Applying what is learned in practice leads to an average retention rate of about 75%. +> 1. Analyzing and teaching others based on what has been learned can yield an average retention rate of 90%. -上面列出的 7 种学习方法,前四种称为 **被动学习** ,后三种称为 **主动学习**。 +The first four learning methods are termed **passive learning**, while the last three are considered **active learning**. -拿学游泳做个类比,被动学习相当于你看别人游泳,而主动学习则是你自己要下水去游。我们知道游泳或者跑步之类的运动是要燃烧身体卡路里的,这样才能达到锻炼身体和长肌肉的效果(肌肉是卡路里燃烧的结果)。如果你只是看别人游泳,自己不实际去游,是不会长肌肉的。同样的,主动学习也是要燃烧脑部卡路里的,这样才能达到训练大脑和长脑部“肌肉”的效果。 +To draw an analogy with swimming, passive learning is akin to watching others swim, whereas active learning involves getting in the water and swimming yourself. We know that activities like swimming or running burn calories in the body, achieving physical fitness and muscle growth (muscle is the result of calorie burning). If you only watch others swim without ever trying it, you won’t build muscle. Likewise, active learning also requires "burning calories" in the brain to achieve effective training of the brain and expand its "muscle." -我们也知道,燃烧身体的卡路里,通常会让人感觉不舒适,如果燃烧身体卡路里会让人感觉舒适的话,估计这个世界上应该不会有胖子这类人。同样,燃烧脑部卡路里也会让人感觉不适、紧张、出汗或语无伦次,如果燃烧脑部卡路里会让人感觉舒适的话,估计这个世界上人人都很聪明,人人都能发挥最大潜能。当然,这些不舒适是短期的,长期会使你更健康和聪明。波波一直认为, **人与人之间的先天身体其实都差不多,但是后天身体素质和能力有差异,这些差异,很大程度是由后天对身体和大脑的训练质量、频度和强度所造成的。** +It's understood that burning body calories typically induces discomfort; if burning calories felt comfortable, there would probably not be overweight individuals in the world. Similarly, burning calories in the brain can lead to discomfort, tension, sweating, or incoherence; if burning brain calories were comfortable, it’s likely everyone would be smart and fully realize their potential. Of course, this discomfort is temporary; in the long run, it makes you healthier and smarter. Bobo has always believed that **the innate physical condition among individuals is relatively similar, but the differences in postnatal physical quality and capabilities primarily stem from the quality, frequency, and intensity of subsequent training for both body and brain.** -明白这个道理之后,心智成熟和自律的人就会对自己进行持续地 **刻意训练** 。这个刻意训练包括对身体的训练,比如波波现在每天坚持跑步 3km,走 3km,每天做 60 个仰卧起坐,5 分钟平板撑等等,每天保持让身体燃烧一定量的卡路里。刻意训练也包括对大脑的训练,比如波波现在每天做项目写代码 coding(训练脑+手),平均每天在 B 站上输出十分钟免费视频(训练脑+口头表达),另外有定期总结输出公众号文章(训练脑+文字表达),还有每天打半小时左右的平衡球(下图)或古墓丽影游戏(训练小脑+手),每天保持让大脑燃烧一定量的卡路里,并保持一定强度(适度不适感)。 +Once you understand this principle, mature-minded and self-disciplined individuals will engage in ongoing **deliberate practice**. This deliberate practice includes physical training; for instance, Bobo currently runs 3km and walks 3km daily, does 60 sit-ups daily, and holds a plank for five minutes each day, maintaining a certain level of calorie burning for the body. Deliberate practice also encompasses mental training; for instance, Bobo currently writes code (training the brain and hands) daily, produces ten minutes of free video content on Bilibili each day (training the brain and verbal expression), regularly sums up and publishes articles on his public account (training the brain and written expression), and plays balance ball games (as shown below) or Tomb Raider games for about half an hour daily (training the cerebellum and hands). Every day, he keeps his brain burning a specific amount of calories while maintaining a certain intensity (a moderate level of discomfort). -![平衡球游戏](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231839985.png) +![Balance Ball Game](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231839985.png) -关于刻意训练的专业原理和方法论,推荐看书籍《刻意练习》。 +For specialized principles and methodologies regarding deliberate practice, I recommend reading the book **Deliberate Practice**. -![刻意练习](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231842735.png) +![Deliberate Practice](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231842735.png) -注意,如果你平时从来不做举重锻炼的,那么某天突然做举重会很不适应甚至受伤。脑部训练也是一样的,如果你从来没有做过视频输出,那么刚开始做会很不适应,做出来的视频质量会很差。不过没有关系,任何训练都是一个循序渐进,不断强化的过程。等大脑相关区域的"肌肉"长出来以后,会逐步进入正循环,后面会越来越顺畅,相关"肌肉"会越来越发达。所以,和健身一样,健脑也不能遇到困难就放弃,需要循序渐进(Incremental)+持续地(Persistent)刻意训练。 +It’s important to note that if you have never engaged in weightlifting, suddenly doing weightlifting one day will be quite uncomfortable, or even lead to injury. The same goes for brain training; if you have never produced video content, it will feel very awkward when you first start, and the quality of initial videos may not be great. Nevertheless, any form of training is a progressive and strengthening process. Once the "muscles" in the brain develop, you'll gradually enter a positive feedback loop, and it will get easier, leading to the development of related "muscles." Therefore, like physical fitness, mental training must not be given up due to difficulties; instead, it should be approached gradually (Incremental) and continuously (Persistent) through deliberate practice. -理解了学习金字塔和刻意训练以后,现在再来看 Brendan Gregg,Jay Kreps 和 Brad Traversy 这些大牛的做法,他们的学习成长都是建立在持续有价值产出的基础上的,这些产出都是刻意训练+燃烧脑部卡路里的成果。他们的产出要么是建立在实践基础上的产出,例如 Jay Kreps 的 Kafka 开源项目和 Confluent 公司;要么是在实践的基础上,再整理传授给其他人的产出,例如,Brendan Greeg 的技术演讲 ppt/视频,书籍,还有 Brad Traversy 的教学视频等等。换句话说,他们一直在学习金字塔的 5 ~ 7 层主动和高效地学习。并且,他们的学习产出还可以获得用户使用,有客户价值(Customer Value),有用户就有反馈和度量。记住,有反馈和度量的学习,也称闭环学习,它是能够不断改进提升的;反之,没有反馈和度量的学习,无法改进提升。 +After understanding the learning pyramid and the concept of deliberate practice, we can now observe how elite figures like Brendan Gregg, Jay Kreps, and Brad Traversy operate their learning and growth, all based on the foundation of persistent, valuable output. These outputs are derived from deliberate practice and the effort of burning mental calories. Their outputs are either based on practical application, such as Jay Kreps' open-source project Kafka and Confluent; or are generated after consolidating and sharing their learnings with others, such as Brendan Gregg's technical presentations, documents/videos, books, and Brad Traversy's teaching videos, etc. In other words, they have consistently engaged in active and effective learning at the 5-7 levels of the learning pyramid. Furthermore, their learning outputs can also provide value to users, aligning with customer value (Customer Value). With users, there comes feedback and metrics. Remember, learning that includes feedback and metrics is called closed-loop learning, which allows for continuous improvement and enhancement; conversely, learning without feedback and metrics cannot be improved. -现在,你也应该明白,晒个书单秀个技能图谱很简单,读个书上个课也不难。但是要你给出 5 ~ 10 年的总体技术成长战略,再基于这个战略给出每年的细分落地计划(尤其是产出计划),然后再严格按计划执行,这的确是很难的事情。这需要大量的实践训练+深度思考,要燃烧大量的脑部卡路里!但这是上天设置的进化法则,成长为真正的技术大牛如同成长为一流的运动员,是需要通过燃烧与之相匹配量的卡路里来交换的。成长为真正的技术大牛,也是需要通过产出与之匹配的社会价值来交换的,只有这样社会才能正常进化。你推进了社会进化,社会才会回馈你。如果不是这样,社会就无法正常进化。 +By now, you should also realize that it's quite simple to showcase a reading list or present a skills map. Reading books or attending classes also isn’t challenging. However, formulating a comprehensive technical growth strategy for the next 5 to 10 years, and then deriving annual segmented implementations based on that strategy (especially output plans), followed by strict adherence—this is indeed a challenging endeavor. It requires extensive practical training and deep reflection, burning significant mental calories! But this is a law of evolution set by nature; growing to become a true technical elite, just like becoming a top athlete, requires burning a matching amount of calories. To become a true technical expert necessitates generating social value in line with one's outputs; only then can society evolve normally. When you champion societal evolution, society will reciprocate. If not, society will stagnate. -## 四、战略思维的诞生 +## 4. The Birth of Strategic Thinking -![思考周期和机会点](https://oss.javaguide.cn/p3-juejin/dc87167f53b243d49f9f4e8c7fe530a1~tplv-k3u1fbpfcp-zoom-1.png) +![Thinking Cycle and Opportunity Points](https://oss.javaguide.cn/p3-juejin/dc87167f53b243d49f9f4e8c7fe530a1~tplv-k3u1fbpfcp-zoom-1.png) -一般毕业生刚进入企业工作的时候,思考大都是以天/星期/月为单位的,基本上都是今天学个什么技术,明天学个什么语言,很少会去思考一年甚至更长的目标。这是个眼前漆黑看不到的懵懂时期,捕捉到机会点的能力和概率都非常小。 +Typically, when fresh graduates first enter a corporate job, their thinking is often unit-based—days, weeks, or months. They usually focus on learning a particular technology today and another language tomorrow, rarely contemplating year-long or longer goals. This phase consists of an unclear and ignorant mindset where capturing opportunities is a significant challenge. -工作了三年以后,悟性好的人通常会以一年为思考周期,制定和实施一些年度计划。这个时期是相信天赋和比拼能力的阶段,可以捕捉到一些小机会。 +After working for three years, perceptive individuals usually adapt their thinking cycles to the one-year frame, formulating and implementing annual plans. This stage focuses on talent and skill competition to capture small opportunities. -工作了五年以后,一些悟性好的人会产生出一定的胆识和眼光,他们会以 3 ~ 5 年为周期来制定和实施计划,开始主动布局去捕捉一些中型机会点。 +By the five-year mark, some astute individuals develop a certain level of courage and insight. They begin to implement plans on a 3-5 year cycle, actively positioning themselves to seize medium-scale opportunities. -工作了十年以后,悟性高的人会看到模式和规则变化,例如看出行业发展模式,还有人才的成长模式等,于是开始诞生出战略性思维。然后他们会以 5 ~ 10 年为周期来制定和实施自己的战略计划,开始主动布局去捕捉一些中大机会点。Brendan Gregg,Jay Kreps 和 Brad Traversy 都是属于这个阶段的人。 +By the ten-year point, highly perceptive individuals can start to see patterns and changes in rules, such as industry development models, and talent growth patterns, leading to the birth of strategic thinking. They will then devise and implement their strategic plans on 5-10 year cycles, actively positioning themselves to seize medium to large opportunities. Brendan Gregg, Jay Kreps, and Brad Traversy all belong to this stage. -当然还有很少一些更牛的时代精英,他们能够看透时代和人性,他们的思考是以一生甚至更长时间为单位的,这些超人不在本文讨论范围内。 +There are, of course, quite a few extraordinary talents who can perceive the essence of the times and human nature. Their thinking spans over their lifetime or even longer. However, these transcendents are beyond the scope of this article. -## 五、建议 +## 5. Recommendations -**1、以 5 ~ 10 年为周期去布局谋划你的战略。** +**1. Plan your strategy over a period of 5-10 years.** -现在大学生毕业的年龄一般在 22 ~ 23 岁,那么在工作了十年后,也就是在你 32 ~ 33 岁的时候,你也差不多看了十年了,应该对自己和周围的世界(你的行业和领域)有一个比较深刻的领悟了。**如果你到这个年纪还懵懵懂懂,今天抓东明天抓西,那么只能说你的胆识格局是相当的低**。在当前 IT 行业竞争这么激烈的情况下,到 35 岁被下岗可能就在眼前了。 +Currently, university graduates are generally around 22-23 years old. After about ten years in the workplace, when you reach about 32-33 years old, you should have gained enough experience over a decade to develop a rather profound insight into yourself and the world around you (your industry and field). **If by this age you are still bewildered, chasing after this and that, it can only be said that your courage and perspective are quite limited.** In today's fiercely competitive IT industry, the possibility of being laid off by 35 years old may very well loom. -有了战略性思考,你应该以 5 ~ 10 年为周期去布局谋划你的战略。以 Brendan Gregg,Jay Kreps 和 Brad Traversy 这些大牛为例,**人生若真的要干点成就出来,投入周期一般都要十年的**。从 33 岁开始,你大致有 3 个十年,因为到 60 岁以后,一般人都老眼昏花干不了大事了。如果你悟性差一点,到 40 岁才开始规划,那么你大致还有 2 个十年。如果你规划好了,这 2 ~ 3 个十年可以成就不小的事业。否则,你很可能一生都成就不了什么事业,或者一直在帮助别人成就别人的事业。 +With strategic thinking in place, you should plan your strategy over a 5-10 year span. Taking Brendan Gregg, Jay Kreps, and Brad Traversy as examples, **if one truly wants to accomplish something significant in life, the commitment period generally spans a decade.** Starting from age 33, you essentially have three decades to work with, as most people become less capable of achieving significant endeavors beyond the age of 60. If your perception is not as keen and you only begin planning at 40, you would be left with approximately two decades. If planned effectively, these 20-30 years could lead to considerable accomplishments. Otherwise, it is likely that you may accomplish little or spend your life helping others succeed. -**2、专注自己的精力。** +**2. Concentrate your energy.** -考虑到人生能干事业的时间也就是 2 ~ 3 个十年,你会发现人生其实很短暂,这时候你会把精力都投入到实现你的十年战略上去,没有时间再浪费在比如网上的闲聊和扯皮争论上去。 +Considering that one's career span generally covers about two to three decades, you'll realize that life is quite short. At this juncture, you should focus all your energy on realizing your ten-year strategy, leaving no time to squander on idle online chatter or fruitless debates. -**3、细分落地计划尤其是产出计划。** +**3. Segment your yearly implementation plans, especially your output plans.** -有了十年战略方向,下一步是每年的细分落地计划,尤其是产出计划。这些计划主要应该工作在学习金字塔的 5/6/7 层。**产出应该是刻意训练+燃烧卡路里的结果,每天让身体和大脑都保持燃烧一定量的卡路里**。 +Having established a ten-year strategic direction, the next step is to formulate annual segmented implementation plans, focusing particularly on output plans. These plans should primarily operate at levels 5/6/7 of the learning pyramid. **Output should result from deliberate practice plus burning calories, ensuring both your body and brain maintain a consistent caloric expenditure.** -**4、产出有价值的东西形成正反馈。** +**4. Generate valuable output to form a positive feedback loop.** -产出应该有客户价值,自己能学习(自己成长进化),对别人还有用(推动社会成长进化),这样可以得到**用户回馈和度量**,形成一个闭环,可以持续改进和提升你的学习。 +Output should have customer value, allowing for personal growth (your own evolution) and benefit others (propelling societal progress). This creates a **feedback loop with users and metrics**, enabling continuous improvement and enhancement of your learning. -**5、少即是多。** +**5. Less is more.** -深耕一个(或有限几个相关的)领域。所有细分计划应该紧密围绕你的战略展开。克制内心欲望,不要贪多和分心,不要被喧嚣的世界所迷惑。 +Delve deeply into one (or a few related) fields. All segmented plans should closely align with your strategy. Restrain internal desires to avoid spreading yourself thin and be misled by the clamorous world. -**6、战略方向+细分计划都要写下来,定期 review 优化。** +**6. Document both your strategic direction and segmented plans, regularly reviewing and optimizing them.** -**7、要有定力,持续努力。** +**7. Maintain resolve and consistent effort.** -曲则全、枉则直,战略实现是不可能直线的。战略方向和细分计划通常要按需调整,尤其在早期,但是最终要收敛。如果老是变不收敛,就是缺乏战略定力,是个必须思考和解决的大问题。 +All paths may be winding, and success will not come in a straight line. Strategic directions and segmented plans often need to be adjusted as necessary, especially in the beginning; however, they should ultimately converge. If changes are constant without settling, it signifies a lack of strategic resolve, which is a significant issue that needs to be thoughtfully addressed. -别人的成长战略可以参考,但是不要刻意去模仿,你有你自己的颜色,**你应该成为独一无二的你**。 +You can reference others' growth strategies, but avoid trying to mimic them overly; you have your own unique qualities; **you should become a one-of-a-kind version of yourself.** -战略方向和细分计划明确了,接下来就是按部就班执行,十年如一日铁打不动。 +Once your strategic direction and segmented plans are clarified, the next step is to execute step by step, adhering steadfastly over the long haul. -**8、慢就是快。** +**8. Slow is fast.** -战略目标的实现也和种树一样是生长出来的,需要时间耐心栽培,记住**慢就是快。**焦虑纠结的时候,像念经一样默念王阳明《传习录》中的教诲: +The achievement of strategic goals requires time and patient cultivation, much like growing trees; remember, **slow is fast.** During anxious periods, silently affirm teachings from Wang Yangming's **Instructions for Cultivating One's Mind**: -> 立志用功,如种树然。方其根芽,犹未有干;及其有干,尚未有枝;枝而后叶,叶而后花实。初种根时,只管栽培灌溉。勿作枝想,勿作花想,勿作实想。悬想何益?但不忘栽培之功,怕没有枝叶花实? +> Setting high aspirations and working hard is like growing a tree. At first, there are only small roots; the trunk has not emerged; only once the trunk develops can branches begin to grow; once branches grow, leaves will follow, and then flowers and fruits will emerge. When first planting, just focus on nurturing and watering. Do not dwell on when branches will grow or when flowers will bloom or fruits will be borne. What good does worrying do? Just persist in the nurturing efforts, and you need not fear without producing branches, leaves, or fruit. > -> 译文: +> Translation: > -> 实现战略目标,就像种树一样。刚开始只是一个小根芽,树干还没有长出来;树干长出来了,枝叶才能慢慢长出来;树枝长出来,然后才能开花和结果。刚开始种树的时候,只管栽培灌溉,别老是纠结枝什么时候长出来,花什么时候开,果实什么时候结出来。纠结有什么好处呢?只要你坚持投入栽培,还怕没有枝叶花实吗? +> Achieving strategic goals is akin to planting trees. Initially, just a small root grows, and the trunk hasn't yet formed; once the trunk is established, branches begin to sprout slowly; only after branches sprout will flowers and fruits follow. At the beginning of planting, just focus on nurturing and watering, without constantly fretting over when branches will appear, when flowers will bloom, or when fruits will form. What benefit is there in worrying? As long as you persist in your nurturing efforts, need you fear the absence of branches, leaves, or fruit? diff --git a/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md b/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md index d32f586b449..282dfda825e 100644 --- a/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md +++ b/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md @@ -1,109 +1,63 @@ --- -title: 工作五年之后,对技术和业务的思考 -category: 技术文章精选集 -author: 知了一笑 +title: Reflections on Technology and Business After Five Years of Work +category: Selected Technical Articles +author: Zhi Liao Yi Xiao tag: - - 练级攻略 + - Leveling Guide --- -> **推荐语**:这是我在两年前看到的一篇对我触动比较深的文章。确实要学会适应变化,并积累能力。积累解决问题的能力,优化思考方式,拓宽自己的认知。 +> **Recommendation**: This is an article I came across two years ago that deeply resonated with me. It is indeed important to learn to adapt to change and accumulate skills. Accumulating problem-solving abilities, optimizing thinking methods, and broadening one's understanding are essential. > -> **原文地址:** +> **Original Article Link:** -苦海无边,回头无岸。 +The sea of bitterness is boundless; turning back is no shore. -## 01 前言 +## 01 Introduction -晃晃悠悠的,在互联网行业工作了五年,默然回首,你看哪里像灯火阑珊处? +After five years in the internet industry, I find myself reflecting on my journey. Where does it feel like a place of dim lights? -初入职场,大部分程序员会觉得苦学技术,以后会顺风顺水升职加薪,这样的想法没有错,但是不算全面,五年后你会不会继续做技术写代码这是核心问题。 +When entering the workforce, most programmers believe that by diligently learning technology, they will smoothly advance in their careers and receive salary increases. This thought is not wrong, but it is not comprehensive. The core question five years later is whether you will continue to work in technology and write code. -初入职场,会觉得努力加班可以不断提升能力,可以学到技术的公司就算薪水低点也可以接受,但是五年之后会认为加班都是在不断挤压自己的上升空间,薪水低是人生的天花板。 +At the beginning of your career, you might think that working hard and putting in overtime can continuously enhance your abilities. You might accept a lower salary if the company offers good learning opportunities. However, after five years, you may feel that overtime is merely squeezing your upward mobility, and a low salary becomes a ceiling in life. -这里想说的关键问题就是:初入职场的认知和想法大部分不会再适用于五年后的认知。 +The key issue here is that the perceptions and thoughts of newcomers to the workforce will largely not apply to the understanding five years later. -工作五年之后面临的最大压力就是选择:职场天花板,技术能力天花板,薪水天花板,三十岁天花板。 +The greatest pressure faced after five years of work is the choice: the career ceiling, the technical ability ceiling, the salary ceiling, and the thirty-year-old ceiling. -如何面对这些问题,是大部分程序员都在思考和纠结的。做选择的唯一参考点就是:利益最大化,这里可以理解为职场更好的升职加薪,顺风顺水。 +How to face these issues is something most programmers are contemplating and struggling with. The only reference point for making choices is: maximizing benefits, which can be understood as better promotions and salary increases in the workplace. -五年,变化最大不是工作经验,能力积累,而是心态,清楚的知道现实和理想之间是存在巨大的差距。 +In five years, the most significant change is not work experience or skill accumulation, but mindset—clearly understanding that there is a huge gap between reality and ideals. -## 02 学会适应变化,并积累能力 +## 02 Learn to Adapt to Change and Accumulate Skills -回首自己的职场五年,最认可的一句话就是:学会适应变化,并积累能力。 +Looking back on my five years in the workplace, the most recognized statement is: learn to adapt to change and accumulate skills. -变化的就是,五年的时间技术框架更新迭代,开发工具的变迁,公司环境队友的更换,甚至是不同城市的流浪,想着能把肉体和灵魂安放在一处,有句很经典的话就是:唯一不变的就是变化本身。 +What has changed is that in five years, technology frameworks have been updated and iterated, development tools have evolved, company environments and teammates have changed, and even the wandering between different cities has occurred. The classic saying goes: the only constant is change itself. -要积累的是:解决问题的能力,思考方式,拓宽认知。 +What needs to be accumulated is: problem-solving abilities, ways of thinking, and broadening understanding. -这种很难直白的描述,属于个人认知的范畴,不同的人有不一样的看法,所以只能站在大众化的角度去思考。 +This is difficult to describe directly and belongs to the realm of personal cognition. Different people have different views, so we can only think from a generalized perspective. -首先聊聊技术,大部分小白级别的,都希望自己的技术能力不断提高,争取做到架构师级别,但是站在当前的互联网环境中,这种想法实现难度还是偏高,这里既不是打击也不是为了抬杠。 +First, let's talk about technology. Most beginners hope to continuously improve their technical skills and strive to reach the architect level. However, in the current internet environment, the difficulty of achieving this is relatively high. This is neither to discourage nor to argue. -可以观察一下现状,技术团队大的 20-30 人,小的 10-15 人,能有一个架构师去专门管理底层框架都是少有现象。 +We can observe the current situation: large technical teams have 20-30 people, while smaller ones have 10-15. It is rare to have an architect specifically managing the underlying framework. -这个问题的原因很多,首先架构师的成本过高,环境架构也不是需要经常升级,说的难听点可能框架比项目生命周期更高。 +There are many reasons for this issue. First, the cost of an architect is too high, and the environmental architecture does not need frequent upgrades. To put it bluntly, the framework may outlast the project lifecycle. -所以大部分公司的大部分业务,基于现有大部分成熟的开源框架都可以解决,这也就导致架构师这个角色通常由项目主管代替或者级别较高的开发直接负责,这就是现实情况。 +Thus, most companies can solve the majority of their business needs based on existing mature open-source frameworks. This leads to the role of architect often being replaced by project leads or higher-level developers, which is the reality. -这就导致技术框架的选择思路就是:只选对的。即这方面的人才多,开源解决方案多,以此降低技术方面对公司业务发展的影响。 +This results in the thought process for selecting technology frameworks being: only choose what is right. That is, there are many talents in this area, and many open-source solutions, which reduces the impact of technology on the company's business development. -那为什么还要不断学习和积累技术能力?如果没有这个能力,程序员岗位可能根本走不了五年之久,需要用技术深度积累不断解决工作中的各种问题,用技术的广度提升自己实现业务需求的认知边界,这是安放肉体的根本保障。 +So why continue to learn and accumulate technical skills? Without this ability, a programmer may not last five years in the role. It is necessary to deeply accumulate technical knowledge to continuously solve various problems at work and to broaden one's understanding of business needs through the breadth of technology. This is the fundamental guarantee for placing one's body and soul in a stable position. -这就是导致很多五年以后的程序员压力陡然升高的原因,走向管理岗的另一个壁垒就是业务思维和认知。 +This is why many programmers experience a sudden increase in pressure after five years, and another barrier to moving into management roles is the need for business thinking and understanding. -## 03 提高业务能力的积累 +## 03 Accumulating Business Skills -程序员该不该用心研究业务,这个问题真的没有纠结的必要,只要不是纯技术型的公司,都需要面对业务。 +Should programmers diligently study business? This question really does not require much debate. As long as it is not a purely technical company, everyone needs to face business. -不管技术、运营、产品、管理层,都是在面向业务工作。 +Whether in technology, operations, product management, or management, all are working towards business objectives. -从自己职场轨迹来看,五年变化最大就是解决业务问题的能力,职场之初面对很多业务场景都不知道如何下手,到几年之后设计业务的解决方案。 +From my career trajectory, the most significant change over five years has been the ability to solve business problems. At the beginning of my career, I often did not know how to approach many business scenarios, but after a few years, I was able to design solutions for business challenges. -这是大部分程序员在职场前五年跳槽就能涨薪的根本原因,面对业务场景,基于积累的经验和现有的开源工具,能快速给出合理的解决思路和实现过程。 - -工作五年可能对技术底层的清晰程度都没有初入职场的小白清楚,但是写的程序却可以避开很多坑坑洼洼,对于业务的审视也是很细节全面。 - -解决业务能力的积累,对于技术视野的宽度需求更甚,比如职场初期对于海量数据的处理束手无策,但是在工作几年之后见识数据行业的技术栈,真的就是技术选型的视野问题。 - -什么是衡量技术能力的标准?站在一个共识的角度上看:系统的架构与代码设计能适应业务的不断变化和各种需求。 - -相对比与技术,业务的变化更加快速频繁,高级工程师或者架构师之所以薪资高,这些角色一方面能适应业务的迭代,并且在工作中具有一定前瞻性,会考虑业务变化的情况下代码复用逻辑,这样的能力是需要一定的技术视野和业务思维的沉淀。 - -所以职场中:业务能说的井井有条,代码能写的明明白白,得到机会的可能性更大。 - -## 04 不同的阶段技术和业务的平衡和选择 - -从理性的角度看技术和业务两个方面,能让大部分人职场走的平稳顺利,但是不同的阶段对两者的平衡和选择是不一样的。 - -在思考如何选择的时候,可以参考二八原则的逻辑,即在任何一组东西中,最重要的只占其中一小部分,约 20%,其余 80%尽管是多数,却是次要的,因此又称二八定律。 - -个人真的非常喜欢这个原则,大部分人都不是天才,所以很难三心二意同时做好几件事情,在同一时间段内应该集中精力做好一件事件。 - -但是单纯的二八原则模式可能不适应大部分职场初期的人,因为初期要学习很多内容,如何在职场生存:专业能力,职场关系,为人处世,产品设计等等。 - -当然这些东西不是都要用心刻意学习,但是合理安排二二六原则或其他组合是更明智的,首先是专业能力要重点练习,其次可以根据自己的兴趣合理选择一到两个方面去慢慢了解,例如产品,运营,运维,数据等,毕竟三五年以后会不会继续写代码很难说,多给自己留个机会总是有备无患。 - -在职场初期,基本都是从技术角度去思考问题,如何快速提升自己的编码能力,在公司能稳定是首要目标,因此大部分时间都是在做基础编码和学习规范,这时可能 90%的心思都是放在基础编码上,另外 10%会学习环境架构。 - -最多一到两年,就会开始独立负责模块需求开发,需要自己设计整个代码思路,这里业务就会进入视野,要懂得业务上下游关联关系,学会思考如何设计代码结构,才能在需求变动的情况下代码改动较少,这个时候可能就会放 20%的心思在业务方面,30%学习架构方式。 - -三到五年这个时间段,是解决问题能力提升最快的时候,因为这个阶段的程序员基本都是在开发核心业务链路,例如交易、支付、结算、智能商业等模块,需要对业务整体有较清晰的把握能力,不然就是给自己挖坑,这个阶段要对业务流付出大量心血思考。 - -越是核心的业务线,越是容易爆发各种问题,如果在日常工作中不花心思处理各种细节问题,半夜异常自动的消息和邮件总是容易让人憔悴。 - -所以努力学习技术是提升自己,培养自己的业务认知也同样重要,个人认为这二者的分量平分秋色,只是需要在合适的阶段做出合理的权重划分。 - -## 05 学会在职场做选择和生存 - -基于技术能力和业务思维,学会在职场做选择和生存,这些是职场前五年一路走来的最大体会。 - -不管是技术还是业务,这两个概念依旧是个很大的命题,不容易把握,所以学会理清这两个方面能力中的公共模块是关键。 - -不管技术还是业务,都不可能从一家公司完全复制到另一家公司,但是可以把一家公司的技术框架,业务解决方案学会,并且带到另一家公司,例如技术领域内的架构、设计、流程、数据管理,业务领域内的思考方式、产品逻辑、分析等,这些是核心能力并且是大部分公司人才招聘的要求,所以这些才是工作中需要重点积累的。 - -人的精力是有限的,而且面对三十这个天花板,各种事件也会接连而至,在职场中学会合理安排时间并不断提升核心能力,这样才能保证自己的竞争力。 - -职场就像苦海无边,回首望去可能也没有岸边停泊,但是要具有换船的能力或者有个小木筏也就大差不差了。 - - +This is the fundamental reason why most programmers can see salary increases when changing jobs within diff --git a/docs/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md b/docs/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md index f96a20fec16..3fe1de902af 100644 --- a/docs/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md +++ b/docs/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md @@ -1,342 +1,57 @@ --- -title: 如何在技术初试中考察程序员的技术能力 -category: 技术文章精选集 -author: 琴水玉 +title: How to Assess a Programmer's Technical Ability in Technical Interviews +category: Selected Technical Articles +author: Qinshi Yuyu tag: - - 面试 + - Interview --- -> **推荐语**:从面试官和面试者两个角度探讨了技术面试!非常不错! +> **Recommendation**: This article explores technical interviews from both the interviewer and interviewee perspectives! Very insightful! > -> **内容概览:** +> **Content Overview:** > -> - 实战与理论结合。比如,候选人叙述 JVM 内存模型布局之后,可以接着问:有哪些原因可能会导致 OOM , 有哪些预防措施? 你是否遇到过内存泄露的问题? 如何排查和解决这类问题? -> - 项目经历考察不宜超过两个。因为要深入考察一个项目的详情,所占用的时间还是比较大的。一般来说,会让候选人挑选一个他或她觉得最有收获的/最有挑战的/印象最深刻的/自己觉得特有意思的项目。然后围绕这个项目进行发问。通常是从项目背景出发,考察项目的技术栈、项目模块及交互的整体理解、项目中遇到的有挑战性的技术问题及解决方案、排查和解决问题、代码可维护性问题、工程质量保障等。 -> - 多问少说,让候选者多表现。根据候选者的回答适当地引导或递进或横向移动。 +> - Combine practical experience with theory. For example, after a candidate describes the JVM memory model layout, you can follow up with questions like: What could cause an OOM? What preventive measures can be taken? Have you encountered memory leak issues? How do you troubleshoot and resolve such problems? +> - Limit the assessment of project experience to no more than two. This is because delving into the details of a project takes considerable time. Generally, candidates are asked to choose a project they found most rewarding, challenging, memorable, or particularly interesting. Questions will then revolve around this project, typically starting from the project background, assessing the technology stack, overall understanding of project modules and interactions, challenging technical issues encountered and their solutions, troubleshooting and problem-solving, code maintainability, and engineering quality assurance. +> - Ask more and say less, allowing candidates to showcase themselves. Appropriately guide or progress based on the candidate's responses. > -> **原文地址**: +> **Original Article Link**: -## 灵魂三连问 +## Soulful Three Questions -1. 你觉得人怎么样? 【表达能力、沟通能力、学习能力、总结能力、自省改进能力、抗压能力、情绪管理能力、影响力、团队管理能力】 -2. 如果让他独立完成项目的设计和实现,你觉得他能胜任吗? 【系统设计能力、项目管理能力】 -3. 他的分析和解决问题的能力,你的评价是啥?【原理理解能力、实战应用能力】 +1. What do you think of the person? [Expression ability, communication ability, learning ability, summarization ability, self-reflection and improvement ability, stress resistance, emotional management ability, influence, team management ability] +1. If asked to independently complete the design and implementation of a project, do you think they can handle it? [System design ability, project management ability] +1. What is your evaluation of their analysis and problem-solving abilities? [Understanding of principles, practical application ability] -## 考察目标和思路 +## Assessment Goals and Ideas -首先明确,技术初试的考察目标: +First, clarify the assessment goals of the technical interview: -- 候选人的技术基础; -- 候选人解决问题的思路和能力。 +- The candidate's technical foundation; +- The candidate's problem-solving thought process and abilities. -技术基础是基石(冰山之下的东西),占七分, 解决问题的思路和能力是落地(冰山之上露出的部分),占三分。 业务和技术基础考察,三七开。 +The technical foundation is the cornerstone (the part below the iceberg), accounting for seventy percent, while the problem-solving thought process and abilities are the visible part (the part above the iceberg), accounting for thirty percent. The assessment of business and technical foundations should be in a 70-30 ratio. -核心考察目标:分析和解决问题的能力。 +Core assessment goal: the ability to analyze and solve problems. -技术层面:深度 + 应用能力 + 广度。 对于校招或社招 P6 级别以下,要多注重 深度 + 应用能力,广度是加分项; 在 P6 之上,可增加 广度。 +From a technical perspective: depth + application ability + breadth. For campus recruitment or social recruitment below P6 level, more emphasis should be placed on depth + application ability, with breadth as a bonus; above P6, breadth can be increased. -- 校招:基础扎实,思维敏捷。 主要考察内容:基础数据结构与算法、进程与并发、内存管理、系统调用与 IO 机制、网络协议、数据库范式与设计、设计模式、设计原则、编程习惯; -- 社招:经验丰富,里外兼修。 主要考察内容:有一定深度的基础技术机制,比如 Java 内存模型及内存泄露、 JVM 机制、类加载机制、数据库索引及查询优化、缓存、消息中间件、项目、架构设计、工程规范等。 +- Campus recruitment: solid foundation, agile thinking. Main assessment content: basic data structures and algorithms, processes and concurrency, memory management, system calls and IO mechanisms, network protocols, database paradigms and design, design patterns, design principles, programming habits; +- Social recruitment: rich experience, well-rounded. Main assessment content: a certain depth of foundational technical mechanisms, such as Java memory model and memory leaks, JVM mechanisms, class loading mechanisms, database indexing and query optimization, caching, message middleware, project, architecture design, engineering specifications, etc. -### 技术基础是什么? +### What is Technical Foundation? -作为技术初试官,怎么去考察技术基础?究竟什么是技术基础?是知道什么,还是知道如何思考?知识作为现有的成熟原理体系,构成了基础的重要组成部分,而知道如何思考亦尤为重要。俗话说,知其然而知其所以然。知其然,是指熟悉现有知识体系,知其所以然,则是自底向上推导,真正理解知识的来龙去脉,理解为何是这样而不是那样。毕竟,对于本质是逻辑的程序世界而言,并无定法。知道如何思考,并能缜密地设计和开发,深入到细节,这就是技术基础吧。 +As a technical interviewer, how do you assess the technical foundation? What exactly is the technical foundation? Is it knowing what to do, or knowing how to think? Knowledge, as an existing mature principle system, constitutes an important part of the foundation, while knowing how to think is equally important. As the saying goes, "to know the why behind the what." Knowing the what means being familiar with the existing knowledge system, while knowing the why means deriving from the bottom up, truly understanding the origins of knowledge, and comprehending why it is this way and not that way. After all, in the logical world of programming, there are no fixed rules. Knowing how to think, being able to design and develop meticulously, and delving into details—that is the technical foundation. -### 为什么要考察技术基础? +### Why Assess Technical Foundation? -程序员最重要的两种技术思维能力,是逻辑思维能力和抽象设计能力。逻辑思维能力是基础,抽象设计能力是高阶。 考察技术基础,正好可以同时考察这两种思维能力。能不能理解基础技术概念及关联,是考察逻辑思维能力;能不能把业务问题抽象成技术问题并合理的组织映射,是考察抽象设计能力。 +The two most important technical thinking abilities for programmers are logical thinking ability and abstract design ability. Logical thinking ability is foundational, while abstract design ability is advanced. Assessing the technical foundation allows for the simultaneous assessment of these two thinking abilities. The ability to understand foundational technical concepts and their relationships assesses logical thinking ability; the ability to abstract business problems into technical problems and organize the mapping reasonably assesses abstract design ability. -绝大部分业务问题,都可以抽象成技术问题。在某种意义上,业务问题只是技术问题的领域化表述。 +The vast majority of business problems can be abstracted into technical problems. In a sense, business problems are merely domain-specific expressions of technical problems. -因此,通过技术基础考察候选者,才能考察到候选者的真实技术实力:技术深度和广度。 +Therefore, assessing candidates through their technical foundation allows for a true evaluation of their technical strength: depth and breadth. -### 为什么不能单考察业务维度? +### Why Not Only Assess Business Dimensions? -因为业务方面通常比较熟悉,可能就直接按照现有方案说出来了,很难考察到候选人的深入理解、横向拓展和归纳总结能力。 +Because candidates are usually quite familiar with business aspects, they may simply regurgitate existing solutions, making it difficult to assess their deep understanding, lateral expansion, and summarization abilities. -这一点,建议有针对性地考察下候选人的归纳总结能力:比如, 微服务搭建或开发或维护/保证系统稳定性或性能方面的过程中,你收获了哪些可以分享的经验? - -### 为什么要考察业务维度? - -技术基础考察,容易错过的地方是,候选人的非技术能力特质,比如沟通组织能力、带项目能力、抗压能力、解决实际问题的能力、团队影响力、其它性格特质等。 - -## 考察方法 - -### 技术基础考察 - -技术基础怎么考察?通过有效的多角度的发问模式来考察。 - -**是什么-为什么** - -是什么考察对概念的基本理解,为什么考察对概念的实现原理。 - -比如索引是什么? 索引是如何实现的? - -**引导-横向发问-深入发问** - -引导性,比如 “你对 java 同步工具熟悉吗?” 作个试探,得到肯定答复后,可以进一步问:“你熟悉哪些同步工具类?” 了解候选者的广度; - -获取候选者的回答后,可以进一步问:“ 谈谈 ConcurrentHashMap 或 AQS 的实现原理?” - -一个人在多大程度上把技术原理能讲得清晰,包括思路和细节,说明他对技术的掌握能力有多强。 - -**深度有梯度和层次的发问** - -设置三个深度层次的发问。每个深度层次可以对应到某个技术深度。 - -- 第一个发问是基本概念层次,考察候选人对概念的理解能力和深度; -- 第二个发问是原理机制层次,考察候选人对概念的内涵和外延的理解深度; -- 第三个发问是应用层次,考察候选人的应用能力和思维敏捷程度。 - -**跳跃式/交叉式发问** - -比如,讲到哈希高效查找,可以谈谈哈希一致性算法 。 两者既有关联又有很多不同点。也是一种技术广度的考察方法。 - -**总结性发问** - -比如,你在做 XXX 中,获得了哪些可以分享的经验? 考察候选人的归纳总结能力。 - -**实战与理论结合** - -- 比如,候选人叙述 JVM 内存模型布局之后,可以接着问:有哪些原因可能会导致 OOM , 有哪些预防措施? 你是否遇到过内存泄露的问题? 如何排查和解决这类问题? -- 比如,候选人有谈到 SQL 优化和索引优化,那就正好谈谈索引的实现原理,如何建立最佳索引? -- 比如,候选人有谈到事务,那就正好谈谈事务实现原理,隔离级别,快照实现等; - -**熟悉与不熟悉结合** - -针对候选人简历上写的熟悉的部分,和没有写出的都问下。比如候选人简历上写着:熟悉 JVM 内存模型, 那我就考察下内存管理相关(熟悉部分),再考察下 Java 并发工具类(不确定是否熟悉部分)。 - -**死知识与活知识结合** - -比如,查找算法有哪些?顺序查找、二分查找、哈希查找。这些大家通常能说出来,也是“死知识”。 - -这些查找算法各适用于什么场景?在你工作中,有哪些场景用到了哪些查找算法?为什么? 这些是“活知识”。 - -**学习或工作中遇到的** - -有时,在学习和工作中遇到的问题,也可以作为面试题。 - -比如,最近在学习《操作系统导论》并发部分,有一章节是如何使数据结构成为线程安全的。这里就有一些可以提问的地方:如何实现一个锁?如何实现一个线程安全的计数器?如何实现一个线程安全的链表?如何实现一个线程安全的 Map ?如何提升并发的性能? - -工作中遇到的问题,也可以抽象提炼出来,作为技术基础面试题。 - -**技术栈适配度发问** - -如果候选人(简历上所写的)使用的某些技术与本公司的技术栈比较契合,则可以针对这些技术点进行深入提问,考察候选人在这些技术点的掌握程度。如果掌握程度比较好,则技术适配度相对更高一些。 - -当然,这一点并不能作为筛掉那些没有使用该技术栈的候选人的依据。比如本公司使用 MongoDB 和 MySQL, 而一个候选人没有用过 Mongodb, 但使用过 MySQL, Redis, ES, HBase 等多种存储系统,那么适配度并不比仅使用过 MySQL 和 Mongodb 的候选人逊色,因为他所涉及的技术广度更大,可以推断出他有足够能力掌握 Mongodb。 - -**应对背题式面试** - -首先,背题式面试,说明候选人至少是有做准备的。当然,对于招聘的一方来说,更希望找到有能力而不是仅记忆了知识的候选人。 - -应对背题式面试,可以通过 “引导-横向发问-深入发问” 的方式,先对候选人关于某个知识点的深度和广度做一个了解,然后出一道实际应用题来考察他是否能灵活使用知识。 - -比如 Java 线程同步机制,可以出一道题:线程 A 执行了一段代码,然后创建了一个异步任务在线程 B 中执行,线程 A 需要等待线程 B 执行完成后才能继续执行,请问怎么实现? - -”理论 + 应用题“的模式。敌知我之变,而不知我变之形。变之形,不计其数。 - -**实用不生僻** - -考察工作中频繁用到的知识、技能和能力,不考察冷僻的知识。 - -比如我偏向考察数据结构与算法、并发、设计 这三类。因为这三类非常基础非常核心。 - -**综合串联式发问** - -知识之间总是相互联系着的,不要单独考察一个知识点。 - -设计一个初始问题,比如说查找算法,然后从这个初始问题出发,串联起各个知识点。比如: - -![](https://oss.javaguide.cn/github/javaguide/open-source-project/502996-20220211115505399-72788909.png) - -在每一个技术点上,都可以应用以上发问技巧,导向不同的问题分支。同时考察面试者的深度、广度和应用能力。 - -**创造有个性的面试题库** - -每个技术面试官都会有一个面试题库。持续积累面试题库,日常中突然想到的问题,就随手记录下来。 - -### 解决问题能力考察 - -仅仅只是技术基础还不够,通常最好结合实际业务,针对他项目里的业务,抽象出技术问题进行考察。 - -解决思路重在层层递进。这一点对于面试官的要求也比较高,兼具良好的倾听能力、技术深度和业务经验。首先要仔细倾听候选人的阐述,找到适当的技术切入点,然后进行发问。如果进不去,那就容易考察失败。 -常见问题: - -- 性能方面,qps, tps 多少?采用了什么优化措施,达成了什么效果? -- 如果有大数据量,如何处理?如何保证稳定性? -- 你觉得这个功能/模块/系统的关键点在哪里?有什么解决方案? -- 为什么使用 XXX 而不是 YYY ? -- 长字段如何做索引? -- 还有哪些方案或思路?各自的利弊? -- 第三方对接,如何应对外部接口的不稳定性? -- 第三方对接,对接大量外部系统,代码可维护性? -- 资损场景?严重故障场景? -- 线上出现了 CPU 飙高,如何处理? OOM 如何处理? IO 读写尖刺,如何排查? -- 线上运行过程中,出现过哪些问题?如何解决的? -- 多个子系统之间的数据一致性问题? -- 如果需要新增一个 XXX 需求,如何扩展? -- 重来一遍,你觉得可以在哪些方面改进? - -系统可问的关联问题: - -- 绝大多数系统都有性能相关问题。如果没有性能问题,则说明是小系统,小系统就不值得考察了; -- 中大型系统通常有技术选型问题; -- 绝大多数系统都有改进空间; -- 大多数业务系统都涉及可扩展性问题和可维护性问题; -- 大多数重要业务系统都经历过比较惨重的线上教训; -- 大数据量系统必定有稳定性问题; -- 消费系统必定有时延和堆积问题; -- 第三方系统对接必定涉及可靠性问题; -- 分布式系统必定涉及可用性问题; -- 多个子系统协同必定涉及数据一致性问题; -- 交易系统有资损和故障场景; - -**设计问题** - -- 比如多个机器间共享大量业务对象,这些业务对象之间有些联合字段是重复的,如何去重? 如果字段比较长,怎么处理? -- 如果瞬时有大量请求涌入,如何保证服务器的稳定性? -- 组件级别:设计一个本地缓存? 设计一个分布式缓存? -- 模块级别:设计一个任务调度模块?需要考虑什么因素? -- 系统级别:设计一个内部系统,从各个部门获取销售数据然后统计出报表。复杂性体现在哪里?关键质量属性是哪些?模块划分,模块之间的关联关系?技术选型? - -**项目经历** - -项目经历考察不宜超过两个。因为要深入考察一个项目的详情,所占用的时间还是比较大的。 - -一般来说,会让候选人挑选一个他或她觉得最有收获的/最有挑战的/印象最深刻的/自己觉得特有意思/感受到挫折的项目。然后围绕这个项目进行发问。通常是从项目背景出发,考察项目的技术栈、项目模块及交互的整体理解、项目中遇到的有挑战性的技术问题及解决方案、排查和解决问题、代码可维护性问题、工程质量保障、重来一遍可以改进哪些等。 - -## 面试过程 - -### 预先准备 - -面试官也需要做一些准备。比如熟悉候选者的技能优势、工作经历等,做一个面试设计。 - -在面试将要开始时,做好面试准备。此外,面试官也需要对公司的一些基本情况有所了解,尤其是公司所使用技术栈、业务全景及方向、工作内容、晋升制度等,这一点技术型候选人问得比较多。 - -### 面试启动 - -一般以候选人自我介绍启动,不过候选人往往会谈得比较散,因此,我会直接提问:谈谈你有哪些优势以及自己觉得可以改进的地方? - -然后以一个相对简单的基础题作为技术提问的开始:你熟悉哪些查找算法?大多数人是能答上顺序查找、二分查找、哈希查找的。 - -### 问题设计 - -提前阅读候选人简历,从简历中筛选出关键词,根据这些关键词进行有针对性地问题设计。 - -比如候选人简历里提到 MVVM ,可以问 MVVM 与 MVC 的区别; 提到了观察者模式,可以谈谈观察者模式,顺便问问他还熟悉哪些设计模式。 - -可遵循“优势-标准-随机”原则: - -- 首先,问他对哪方面技术感兴趣、投入较多(优势部分),根据其优势部分,阐述原理及实战应用; -- 其次,问若干标准化的问题,看看他的原理理解、实战应用如何; -- 最后,随机选一个问题,看看他的原理理解、实战应用如何; - -对于项目同样可以如此: - -- 首先,问他最有成就感的项目,技术栈、模块及关联、技术选型、设计关键问题、解决方案、实现细节、改进空间; -- 其次,问他有挫折感的项目,问题在哪里、做过什么努力、如何改进; - -### 宽松氛围 - -即使问的问题比较多比较难,也要注意保持宽松氛围。 - -在面试前,根据候选人基本信息适当调侃一下,比如一位候选人叫汪奎,那我就说:之前我们团队有位叫袁奎,我们都喊他奎爷。 - -在面试过程中,适当提示,或者给出少量自己的看法,也能缓解候选人的紧张情绪。 - -### 学会倾听 - -多问少说,让候选者多表现。根据候选者的回答适当地引导或递进或横向移动。 - -引导候选人表现他最优势的一面,让他或她感觉好一些:毕竟一场面试双方都付出了时间和精力,不应该是面试官 Diss 候选人的场合,而应该让彼此有更好的交流。很大可能,你也能从候选人那里学到不少东西。 - -面试这件事,只不过双方的角色和立场有所不同,但并不代表面试官的水平就一定高于候选人。 - -### 记录重点 - -认真客观地记录候选人的回答,尽可能避免任何主观评价,亦不作任何加工(比如自己给总结一下,总结能力也是候选人的一个特质)。 - -### 多练习 - -模拟面试。 - -### 作出判断 - -面试过程是一种铺垫,关键的是作出判断。 - -作出判断最容易陷入误区的是:贪深求全。总希望候选人技术又深入又全面。实际上,这是一种奢望。如果候选人的技术能力又深入又全面,很可能也会面临两种情况:1. 候选人有更好的选择; 2. 候选人在其它方面可能存在不足,比如团队协作方面。 - -一个比较合适的尺度是:1. 他或她的技术水平能否胜任当前工作; 2. 他或她的技术水平与同组团队成员水平如何; 3. 他或她的技术水平是否与年限相对匹配,是否有潜力胜任更复杂的任务。 - -### 不同年龄看重的东西不一样 - -对于三年以下的工程师,应当更看重其技术基础,因为这代表着他的未来潜能;同时也考察下他在实际开发中的体现,比如团队协作、业务经验、抗压能力、主动学习的热情和能力等。 - -对于三年以上的工程师,应当更看重其业务经验、解决问题能力,看看他或她是如何分析具体问题,在业务范畴内考察其技术基础的深度和广度。 - -如何判断一个候选人的真实技术水平及是否适合所需,这方面,我也在学习中。 - -## 面试初上路 - -- 提前准备好摄像头和音频,可以用耳机测试下。 -- 提前阅读候选人简历,从中筛选关键字,准备几个基本问题。 -- 多问技术基础题,培养下面试感觉。 -- 适当深入问下原理和实现。 -- 如果候选人简历有突出的地方,就先问那个部分;如果没有,就让候选人介绍项目背景,根据项目背景及经验来提问。 -- 小量练习“连问”技巧,直到能够熟悉使用。 -- 着重考察分析和解决问题的能力,必要的话,可以出个编程题。 -- 留出时间给对方问:你有什么想问的?并告知对方三个工作日内回复面试结果。 - -## 高效考察 - -当作为技术面试官有一定熟悉度时,就需要提升面试效率。即:在更少的时间内有效考察候选人的技术深度和技术广度。可以准备一些常见的问题,作为标准化测试。 - -比如我喜欢考察内存管理及算法、数据库索引、缓存、并发、系统设计、问题分析和思考能力等子主题。 - -- 熟悉哪些用于查找的数据结构和算法? 请任选一种阐述其思想以及你认为有意思的地方。 -- 如果运行到一个 Java 方法,里面创建了一个对象列表,内存是如何分配的?什么时候可能导致栈溢出?什么时候可能导致 OOM ? 导致 OOM 的原因有哪些?如何避免? 线上是否有遇到过 OOM ,怎么解决的? -- Java 分代垃圾回收算法是怎样的? 项目里选用的垃圾回收器是怎样的?为什么选择这个回收器而不是那个? -- Java 并发工具有哪些?不同工具适合于什么场景? -- `Atomic` 原子类的实现原理 ? `ConcurrentHashMap` 的实现原理? -- 如何实现一个可重入锁? -- 举个项目中的例子,哪些字段使用了索引?为什么是这些字段?你觉得还有什么优化空间?如何建一个好的索引? -- 缓存的可设置的参数有哪些?分别的影响是什么? -- Redis 过期策略有哪些? 如何选择 redis 过期策略? -- 如何实现病毒文件检测任务去重? -- 熟悉哪些设计模式和设计原则? -- 从 0 到 1 搭建一个模块/完整系统?你如何着手? - -如果候选人答不上,可以问:如果你来设计这样一个 XXX, 你会怎么做? - -时间占比大概为:技术基础(25-30 分钟) + 项目(20-25 分钟) + 候选人提问(5-10 分钟) - -## 给候选人的话 - -**为什么候选人需要关注技术基础** - -一个常见的疑惑是:开发业务系统的大多数时候,基本不涉及数据结构与算法的设计与实现,为什么要考察 `HashMap` 的实现原理?为什么要学好数据结构与算法、操作系统、网络通信这些基础课程? - -现在我可以给出一个答案了: - -- 正如上面所述,绝大多数的业务问题,实际上最终都会映射到基础技术问题上:数据结构与算法的实现、内存管理、并发控制、网络通信等;这些是理解现代互联网大规模程序以及解决程序疑难问题的基石,—— 除非能祝福自己永远都不会遇到疑难问题,永远都只满足于编写 CRUD; -- 这些技术基础正是程序世界里最有趣最激动人心的地方。如果对这些不感兴趣,就很难在这个领域里深入进去,不如及早转行从事其它职业,非技术的世界一直都很精彩广阔(有时我也想多出去走走,不想局限于技术世界); -- 技术基础是程序员的内功,而具体技术则是招式。徒有招式而内功不深,遇到高手(优秀同行从业者的竞争及疑难杂症)容易不堪一击; -- 具备扎实的专业技术基础,能达到的上限更高,未来更有可能胜任复杂的技术问题求解,或者在同样的问题上能够做到更好的方案; -- 人们喜欢跟与自己相似的人合作,牛人倾向于与牛人合作能得到更好的效果;如果一个团队大部分人技术基础比较好,那么进来一个技术基础比较薄弱的人,协作成本会变高;如果你想和牛人一起合作拿到更好的结果,那就要让自己至少在技术基础上能够与牛人搭配的上; -- 在 CRUD 的基础上拓展其它才能也不失为一种好的选择,但这不会是一个真正的程序员的姿态,顶多是有技术基础的产品经理、项目经理、HR、运营、客满等其它岗位人才。这是职业选择的问题,已经超出了考察程序员的范畴。 - -**不要在意某个问题回答不上来** - -如果面试官问你很多问题,而有些没有回答上来,不要在意。面试官很可能只是在测试你的技术深度和广度,然后判断你是否达到某个水位线。 - -重点是:有些问题你答得很有深度,也体现了你的深度思考能力。 - -这一点是我当了技术面试官才领会到的。当然,并不是每位技术面试官都是这么想的,但我觉得这应该是个更合适的方式。 - -## 参考资料 - -- [技术面试官的 9 大误区](https://zhuanlan.zhihu.com/p/51404304) -- [如何当一个好的面试官?](https://www.zhihu.com/question/26240321) - - +In this regard, it is advisable to specifically assess the candidate's summarization ability: for example, during the process of building, developing, or maintaining microservices diff --git a/docs/high-quality-technical-articles/interview/my-personal-experience-in-2021.md b/docs/high-quality-technical-articles/interview/my-personal-experience-in-2021.md index f7d62085553..18a04b3848e 100644 --- a/docs/high-quality-technical-articles/interview/my-personal-experience-in-2021.md +++ b/docs/high-quality-technical-articles/interview/my-personal-experience-in-2021.md @@ -1,201 +1,77 @@ --- -title: 校招进入飞书的个人经验 -category: 技术文章精选集 -author: 月色真美 +title: Personal Experience of Joining Feishu through Campus Recruitment +category: Selected Technical Articles +author: The Moonlight is Beautiful tag: - - 面试 + - Interview --- -> **推荐语**:这篇文章的作者校招最终去了飞书做开发。在这篇文章中,他分享了自己的校招经历以及个人经验。 +> **Recommendation**: The author of this article ultimately joined Feishu as a developer through campus recruitment. In this article, he shares his campus recruitment experience and personal insights. > -> **原文地址**: +> **Original Article Link**: -## 基本情况 +## Basic Information -我是 C++主要是后台开发的方向。 +I am primarily focused on C++ backend development. -2021 春招入职字节飞书客户端,入职字节之前拿到了百度 offer(音视频直播部分) 以及腾讯 PCG (微视、后台开发)的 HR 面试通过(还没有收到录用意向书)。 +I joined ByteDance's Feishu client during the spring recruitment of 2021. Before joining ByteDance, I received offers from Baidu (audio and video live streaming) and passed the HR interview at Tencent PCG (Weishi, backend development) (but had not yet received an offer letter). -## 不顺利的春招过程 +## Unsuccessful Spring Recruitment Process -### 春招实习对我来说不太顺利 +### Spring Recruitment Internship Was Not Smooth for Me -实验室在 1 月份元旦的那天正式可以放假回家,但回家仍然继续“远程工作”,工作并没有减少,每天日复一日的测试,调试我们开发的“流媒体会议系统”。 +The lab officially went on holiday on New Year's Day in January, but I continued to "work remotely" at home. The workload did not decrease, and I spent day after day testing and debugging our developed "streaming conference system." -在 1 月的倒数第三天,我们开了“年终总结”线上会议。至此,作为研二基本上与实验室的工作开始告别。也正式开始了春招复习的阶段。 +On the third-to-last day of January, we held an online "year-end summary" meeting. At this point, I was basically saying goodbye to the lab work as a second-year graduate student and officially entered the review phase for spring recruitment. -2 月前已经间歇性的开始准备,无非就是在 LeetCode 上面刷刷题目,一天刷不了几道,后面甚至象征性的刷一下每日一题。对我的算法刷题帮助很少。 +I had already started preparing intermittently before February, mainly brushing questions on LeetCode. I could only solve a few questions a day, and later I even just symbolically brushed one question daily. This did not help much with my algorithm practice. -2 月份开始,2 月初的时候,LeetCode 才刷了大概 40 多道题目,挤出了几周时间更新了 handsome 主题的 8.x 版本,这又是一个繁忙的几周。直到春节的当天正式发布,春节过后又开始陆陆续续用一些时间修复 bug,发布修复版本。2 月份这样悄悄溜走。 +Starting in February, at the beginning of the month, I had only brushed about 40 questions on LeetCode. I squeezed out a few weeks to update the 8.x version of the handsome theme, which was another busy few weeks. It was officially released on New Year's Day, and after the Spring Festival, I gradually spent some time fixing bugs and releasing patches. February quietly slipped away. -### 找实习的过程 +### The Process of Finding an Internship -**2021-3 月初** +**Early March 2021** -3 月 初的时候,投了阿里提前批,没想到阿里 3 月 4 号提前批就结束了,那一天约的一面的电话面也被取消了。紧接了开学实验室开会同步进度的时候,发现大家都一面/二面/三面的进度,而我还没有投递的进度。 +At the beginning of March, I applied for Alibaba's early recruitment, but unexpectedly, Alibaba's early recruitment ended on March 4. The phone interview I had scheduled for that day was also canceled. During a lab meeting to synchronize progress after the semester started, I found that everyone else had progressed to the first, second, or third interviews, while I had not even submitted my applications. -**2021-3-8** +**March 8, 2021** -投递了字节飞书 +I applied to ByteDance's Feishu. -**2021-4 月初** +**Early April 2021** -字节第一次一面,腾讯第一次一面 +First interview with ByteDance, first interview with Tencent. -**2021-4 中旬** +**Mid-April 2021** -美团一、二面,腾讯第二次一面和二面,百度三轮面试,通过了。 +First and second interviews with Meituan, second and third interviews with Tencent, and the third round of interviews with Baidu, which I passed. -**2021-4 底** +**End of April 2021** -腾讯第三次一面和字节第二次一面 +Third interview with Tencent and second interview with ByteDance. -**2021-5 月初** +**Early May 2021** -腾讯第三次二面和字节第二次二面,后面这两个都通过了 +Second interview with Tencent and second interview with ByteDance, both of which I passed. -#### 阿里 +#### Alibaba -第一次投了钉钉,没想到因为行测做的不好,在简历筛选给拒绝了。 +I first applied for DingTalk but was rejected during the resume screening due to poor performance on the aptitude test. -第二次阿里妈妈的后端面试,一面电话面试,我感觉面的还可以,最后题目也做出来了。最后反问阶段问对我的面试有什么建议,面试官说投阿里最好还是 Java 的…… 然后电话结束后就给我拒了…… +The second time was for Alibaba Mama's backend interview, a phone interview. I felt it went well, and I managed to solve the questions. During the final Q&A, I asked for suggestions on my interview performance, and the interviewer said it was best to know Java for Alibaba... After the call ended, I received a rejection... -当时真的心态有点崩,问了这个晚上 7 点半的面试,一直看书晚上都没吃…… +At that time, I was really feeling a bit overwhelmed. I had the interview at 7:30 PM and had been studying all evening without eating... -所以春招和阿里就无缘了。 +So, I was destined to have no connection with Alibaba during the spring recruitment. -#### 美团 +#### Meituan -美团一面的面试官真的人很好。也很轻松,因为他们是 Java 岗位,也没问 c++知识,聊了一些基础知识,后面半个小时就是聊非技术问题,比如最喜欢网络上的某位程序员是谁,如何写出优雅的代码,推荐的技术类的书籍之类的。当时回答王垠是比较喜欢的程序员,面试官笑了说他也很喜欢。面试的氛围感觉很好。 +The interviewer for the first interview at Meituan was really nice. It was quite relaxed because they were hiring for a Java position and did not ask about C++ knowledge. We chatted about some basic knowledge, and the latter half hour was about non-technical questions, such as who my favorite programmer online was, how to write elegant code, and recommended technical books. I mentioned that Wang Yin was a programmer I liked, and the interviewer smiled and said he liked him too. The atmosphere of the interview felt great. -二面的时候全程就问简历上的一个项目,问了大概 90 分钟,感觉他从一开始就有点不太想要我的感觉,很大原因我觉的是我是 c++,转 Java 可能成本还是有一些的。最后问 HR 说结果待定,几天后通知被拒了。 +During the second interview, the entire time was spent discussing one project on my resume, which lasted about 90 minutes. I felt that the interviewer seemed a bit hesitant about wanting to hire me, likely because I was a C++ developer transitioning to Java, which might have some associated costs. In the end, HR told me the result was pending, and a few days later, I was notified of my rejection. -#### 百度 +#### Baidu -百度一共三轮面试,在一个下午一起进行,真的很刺激。一面就是很基础的一些 c++问题,写了一个题目说一下思路没让运行(真的要运行还不一定能运行起来:)) +Baidu had a total of three rounds of interviews conducted in one afternoon, which was really intense. The first interview consisted of some basic C++ questions, where I wrote a problem and explained my thought process without running the code (if it had to run, it might not have worked :) ). -二面也是基础,第一个题目合并两个有序数组,第二个题目写归并排序,写的结果不对,又给我换了一个题目,树的 BFS。二面面试官最后问我对今天面试觉得怎么样,我说虽然中间有一个道题目结果不对,但是思路是对的,可能某个小地方写的有问题,但总体的应该还是可以的。二面就给我通过了。 - -三面问的技术问题比较少,30 多分钟,也没写题目,问了一些基本情况和基础知识。最后问部门做的什么内容。面试官说后面 hr 会联系我告诉我内容。 - -#### 字节飞书 - -第一次一面就凉了,原因应该是笔试题目结果不对…… - -第二次一面在 4 月底了,很顺利。二面在五一劳动节后,面试官还让学姐告诉我让我多看看智能指针,面试的时候让我手写 shared_ptr,我之前看了一些实现,但是没有自己写过,导致代码考虑的不够完善,leader 就一直提醒我要怎么改怎么改。 - -本来我以为凉了,在 5 月中旬的时候都准备去百度入职了,给我通知说过了,就这样决定去了字节。 - -#### 感悟 - -这么多次面试中,让我感悟最深的是面试中的考察题目真的很重要,因为我在基础知识上面也不突出,再加上如果算法题(一般 1 道或者 2 道)如果没做出来,基本就凉了。而面试之前的笔试考试反而没那么重要,也没那么难。基本 4 题写出来 1~2 道题目就有发起面试的机会了。难度也基本就是 LeetCode top 100 上面的那些算法。 - -面试中做题,我很容易紧张,头脑就容易一片空白,稍不注意,写错个符号,或者链表赋值错了,很难看出来问题,导出最终结果不对。 - -## 入职字节实习 - -入职字节之前我本来觉得这个岗位可能是我面试的最适合我的了,因为我主 c++,而且飞书用 c++应该挺深的。来之后就觉得我可能不太喜欢做客户端相关,感觉好复杂……也许服务端好一些,现在我仍然不能确定。 - -字节的实习福利在这些公司中应该算是比较好的,小问题是工位比较窄,还是工作强度比其他的互联网公司大一些。字节食堂免费而且挺不错的。字节办公大厦很多,我所在的办公地点比较小。 - -目前,需要放轻松,仓库代码慢慢看呗,mentor 也让我不急,准备有问题就多问问,不能憋着,浪费时间。拿到转正 offer 后,秋招还是想多试试外企或者国企。强度太大的工作目前很难适应。 - -希望过段时间可以分享一下我的感受,以及能够更加适应目前的工作内容。 - -## 求职经验分享 - -### 一些概念 - -#### 日常实习与正式(暑期)实习有什么区别 - -- **日常实习如果一个组比较缺人,就很可能一年四季都招实习生,就会有日常实习的机会**,只要是在校学生都可以去面试。而正式实习开始时间有一个范围比较固定,比如每年的 3-6 月,也就是暑期实习。 -- 日常实习相对要好进一些,但是有的日常实习没有转正名额,这个要先确认一下。 -- **字节的日常实习和正式实习在转正没什么区别,都是一起申请转正的。** - -#### 正式实习拿到 offer 之后什么时候可以去实习 - -暑期实习拿到 offer 后就**可以立即实习**(一般需要走个流程 1 周左右的样子),**也可以选择晚一点去实习**,时间可以自己去把握,有的公司可以在系统上选择去实习的时间,有的是直接和 hr 沟通一下就可以。 - -#### 提前批和正式批的区别 - -以找实习为例: - -- 先提前批,再正式批,提前批一般是小组直接招人**不进系统**,**没有笔试**,**流程相对走的快**,一般一面过了,很快就是二面。 -- 正式批面试都会有面评,如果上一次失败的面试评价会影响下一次面试,所以还是谨慎一点好 - -#### 实习 offer 和正式 offer 区别 - -简单来说,实习 offer 只是给你一个实习的机会,如果在实习期间干的不错就可以转正,获得正式 offer。 - -签署正式 offer 之后并不是意味着马上去上班,因为我们是校招生,拿到正式 offer 之后,可以继续实习(工资会是正式工资的百分比),也可以请假一段时间等真正毕业的时候再去正式工作。 - -### 时间节点 - -> 尽早把简历弄出来,最好就是最近一段时间,因为大家对实验室项目现在还很熟悉,现在写起来不是很难,再过几个月写简历就比较痛苦了。 - -以去年为例: - -- 2 月份中旬的时候阿里提前批开始(基本上只有阿里这个时候开了提前批),3 月 8 号阿里提前批结束。腾讯提前批是 3 月多开始的,4 月 15 号结束 -- 3-5 月拿到实习 offer,最好在 4 月份可以拿到比较想去的实习 offer。 -- 4-8 月份实习,7 月初秋招提前批,7 月底或者 8 月初就是秋招正式批,9 月底秋招就少了挺多,但是只是相对来说,还是有机会, -- 10 月底秋招基本结束,后面还会有秋招补录 - ---- - -- **怎么找实习机会**,个人觉得可以找认识的人内推比较好,内推好处除了可以帮看进度,一般可以直推到组,这样可以排除一些坑的组。提前知道这个组干嘛的。 -- **实习挺重要,最好是实习的时候就找到一个想去的公司,秋招会轻松很多**,因为实习转正基本没什么问题,其次实习转正的 offer 一般要比秋招的好(当然如果秋招表现好也是可以拿到很好的 offer)身边不少人正式 offer 都是实习转正的。 -- **控制好实习的时间**,因为边实习边准备秋招挺累的,一般实习的时候工作压力也挺大,没什么时间刷题。 - -### 面试准备 - -#### 项目经历 - -我觉得我们实验室项目是没问题的,重要是要讲好。 - -- **项目介绍** - -首先可能让你介绍一下这个项目是什么东西,以及**为什么要去做这个项目**。 - -- **项目的结果** - -然后可能会问这个项目的一些数据上最终结果,比如会议系统能够同时多少人使用,或者量化的体验,比如流畅度,或者是一些其他的一些优势。 - -- **项目中的困难** - -最后都会问过程中有没有遇到什么困难、挑战的,以及怎么解决的。这个过程中主要考察这个项目的技术点是什么。 - -> 困难是指什么,个人觉得主要是花了好几天才解决的问题就是困难。 - -举两个例子: - -**第一个例子是排查 bug 方面**,比如有一个内存泄露的问题花了一周才排查出来,那就算一个困难,那么解决这个困难的过程就是**如何去定位这个问题过程**,比如我们先根据错误搜索相关资料,肯定没那么容易就直接找到原因,而是我们会在这些资料中找到一些**关键词**,比如一些工具,那么我们对这个工具的使用就是解决问题的一个过程。 - -**第二个例子是需求方案的设计**,比如某个需求完成,我们实现这个需求可能有多个可行的设计方案。解决这个困难的过程就是**我们对最终选择这个方法的原因,以及其他的设计方案的优缺点的思考**。 - -[面试中被问到:你在工作中碰到的最困难的问题是什么?*发现问题,解决问题.-CSDN 博客*面试中问到工作中遇到困难是怎么解决的](https://blog.csdn.net/u012423865/article/details/79452713) - -有人说我解决方法就是通过百度搜索,但实际上细节也是先搜索某个错误或者问题,但是肯定不可能一下子就搜到了代码答案,而是找到一个答案中有某个关键词,接着我们继续找关键词获取其他的信息。 - -#### 笔试 - -找实习的笔试我觉得不会太难,一般如果是 4 道题目,做出来 1-2 道题目差不多就有面试的机会了。 - -刷题老生常谈的问题,LeetCode Top100。一开始刷题很痛苦,等刷了 40 道题目的时候就有点感觉的,建议从链表、二叉树开始刷,数组类型题目有很多不能通用的技巧。 - -- ::一定要用白版进行训练::,一定要用白板,不仅仅是为了面试记住 API,更重要的是用白板熟练后,写代码会更熟练而且思路更独立和没有依赖。 -- 算法题重中之重,终点不是困难题目,而是简单,中等,常见,高频的题目要熟能生巧,滚瓜烂熟。 -- 面试的笔试过程中,如果出现了问题,**一定要第一时间申请使用本地 IDE 进行调试**,否则可能很长时间找不到问题,浪费了机会。 - -#### 面试 - -面试一般 1 场 1 个小时候分为两个部分,前半部分会问一些基础知识或者项目经历,后半部分做题。 - -**基础知识复习一开始没必要系统的去复习,首先是确保高频问题必会**,比如计算机网络、操作系统那几个必问的问题,可以多看看面经就能找到常问题的问题,对于比较偏问题就算没答上来也不是决定性的影响。 - -- **多看面经!!!!!!** 不要一直埋头自己学,要看别人问过了哪些常问的问题。 -- 对于实习工作,**看的知识点常见的问题一定要全!!!!!**,不是那么精问题不大,一定要全,一定要全!!!! -- **对于自己不会的,尽量多的说!!!!** 实在不行,就往别的地方说!!!总之是引导面试官往自己会的地方上说。 -- 面试中的笔试和前面的笔试风格不同,面试笔试题目不太难,但是考察是冷静思考,代码优雅,没有 bug,先思考清楚!!!在写!!! -- 在描述项目的难点的时候,不要去聊文档调研是难点,回答这部分问题更应该是技术上的难点,最后通过了什么技术解决了这个问题,这部分技术可以让面试官来更多提问以便知道自己的技术能力。 - - +The second interview was also basic. The first question was diff --git a/docs/high-quality-technical-articles/interview/screen-candidates-for-packaging.md b/docs/high-quality-technical-articles/interview/screen-candidates-for-packaging.md index 5b0ff739b34..2039e8ffaa2 100644 --- a/docs/high-quality-technical-articles/interview/screen-candidates-for-packaging.md +++ b/docs/high-quality-technical-articles/interview/screen-candidates-for-packaging.md @@ -1,118 +1,114 @@ --- -title: 如何甄别应聘者的包装程度 -category: 技术文章精选集 +title: How to Identify the Degree of Candidate Packaging +category: Selected Technical Articles author: Coody tag: - - 面试 + - Interview --- -> **推荐语**:经常听到培训班待过的朋友给我说他们的老师是怎么教他们“包装”自己的,不光是培训班,我认识的很多朋友也都会在面试之前“包装”一下自己,所以这个现象是普遍存在的。但是面试官也不都是傻子,通过下面这篇文章来看看面试官是如何甄别应聘者的包装程度。 +> **Recommendation**: I often hear from friends who have attended training classes about how their teachers taught them to "package" themselves. It's not just in training classes; many friends I know also "package" themselves before interviews. This phenomenon is widespread. However, interviewers are not fools. Let’s take a look at how interviewers identify the degree of candidate packaging through the following article. > -> **原文地址**: +> **Original article link**: -## 前言 +## Introduction -上到职场干将下到职场萌新,都会接触到包装简历这个词语。当你简历投到心仪的公司,公司内负责求职的工作人员是如何甄别简历的包装程度的?我根据自己的经验写下了这篇文章,谁都不是天才,包装无可厚非,切勿对号入座! +From experienced professionals to newcomers in the workplace, everyone will encounter the term packaging resumes. When your resume lands in your ideal company, how do the staff in charge of recruitment discern the degree of your resume packaging? I have written this article based on my own experiences. No one is a genius; packaging is understandable, so don't take it personally! -## 正文 +## Main Text -在互联网极速膨胀的社会背景下,各行各业涌入互联网的 IT 民工日益增大。 +In the context of the rapid expansion of the internet, the number of IT workers entering various industries has been steadily increasing. -早在 2016 年,我司发布了 Java、Ios 工程师的招聘信息,就 Java 工程师单个岗位而言,日收简历近 200 份,Ios 日收简历近一千份。 +Back in 2016, my company published job postings for Java and iOS engineers. Just for the Java engineer position alone, we received nearly 200 resumes per day, and for iOS, almost a thousand. -没错,这就是当年培训机构对 Ios 工程师这个岗位发起的市场讨伐。而随着近几年的发展,市场供大于求现象日益严重。人员摸底成为用人单位对人才考核的重大难题。 +Indeed, this is the result of market campaigns initiated by training institutions targeting the iOS engineer position. However, in recent years, the oversupply in the market has become increasingly severe. Evaluating personnel has become a significant difficulty for employers in talent assessment. -笔者初次与求职者以面试的形式进行沟通是 2015 年 6 月。由于当时笔者从业时间短,经验不够丰富,错过了一些优秀的求职者。 +My first communication with a job seeker in the form of an interview occurred in June 2015. At that time, due to my short industry experience, I missed some excellent candidates. -三年后的,今天,笔者再次因公司规模扩大而深入与求职者进行沟通。 +Three years later, today, my company has expanded, which has led me to engage more deeply with job seekers. -### 1.初选如何鉴别劣质简历 +### 1. How to Identify Poor Quality Resumes During Initial Screening -培训机构除了提供技术培训,往往还提供**简历编写指导**、**面试指导**。很多潜移默化的东西,我们很难甄别。但培训机构包装的简历,存在千遍一律的特征。 +Besides providing technical training, training institutions often offer **resume writing guidance** and **interview coaching**. Many subtle factors are tough to discern. However, resumes packaged by training institutions typically exhibit uniform characteristics. -**年龄较小却具备高级文凭** +**Young but possess advanced degrees** -年龄较小却具备高级文凭,这个或许不能作为一项标准,但是大部分的应聘者,均符合传统文凭的市场情况。个别技术爱好者可能通过自考获得文凭,这种情况需提供独有的技术亮点。 +While being young and possessing advanced degrees may not be a definitive standard, most candidates appear to conform to traditional educational backgrounds. Individual tech enthusiasts might obtain degrees through self-study, in which case they need to present unique technical highlights. -**年龄较大却几乎不具备技术经验** +**Old but with virtually no technical experience** -年龄较大却几乎不具备技术经验,相对前一点,这个问题就比较严重了。大家都知道,一个正常的人,对新事物的接受能力会随着年龄的增长而降低,互联网技术也包括其内。如果一个人年龄较大不具备技术经验,那么只有两种情况: +This issue is more severe than the previous one. It's well-known that an individual's ability to accept new things generally declines with age, including internet technologies. If an older person lacks technical experience, only two scenarios could explain this: -1. 中途转行(通过培训、自学等方式强行入行)。 -2. 由于能力问题,已有的经验不敢写入简历中(能力与经验/薪资不符)。 +1. They changed careers midway (forcing their way in through training or self-learning). +1. Due to capability issues, they do not dare to include their existing experience on their resume (their skills do not align with their experience/salary). -**项目经验多为管理系统** +**Project experience mostly comprises management systems** -项目经验,这一项用来评估应聘者的水平太合适不过了。随着互联网的发展迭代,每一年都会出来很多创新型的互联网公司和新兴行业。笔者最近发布的招聘需求里面。CRM 系统、商城、XX 管理系统、问卷系统、课堂系统占了 90%的份额。试问现在 2019 年,内部管理系统这么火爆么。言归正传,我们对于简历的评估,应当多考虑“确有其事”的项目。比如说该人员当时就职于 XX 公司,该公司当时的背景下确实研发了该项目(外包除外)。 +Project experience is an essential criterion for evaluating a candidate's level. With the rapid iteration of the internet, numerous innovative internet companies and emerging industries arise each year. In my recent recruitment requests, 90% of responses focused on CRM systems, e-commerce, management systems, survey systems, and classroom systems. Is internal management so popular now in 2019? To evaluate resumes, we should consider projects that are "real." For instance, if an individual was employed by Company XX at the time and that company indeed developed the project (excluding outsourcing). -**项目的背景不符合互联网发展背景** +**Project backgrounds not aligning with internet development trends** -项目背景,每年的市场走向不同,从早些年的电商、彩票风波,到后来的 O2O、夺宝、直播、新零售。每个系列的产品的出现,都符合市场的定义。如果简历中出现 18 年、19 年才刚立项做彩票(15 年政府禁止互联网彩票)、O2O、商城、夺宝(17 年初禁止夺宝类产品)、直播等产品。显然是非常不符合市场需求的。这种情况下需考虑具体情况是否存在理解空间。 +Project backgrounds vary with market directions each year, from early years of e-commerce and lottery issues to later O2O, treasure-hunting, live streaming, and new retail trends. Each series of products arises to meet market definitions. If resumes mention projects initiated in 2018 or 2019 related to lotteries (which were banned in 2015), O2O, e-commerce, treasure-hunting (which was prohibited in early 2017), live streaming, etc., it clearly does not match market demands. In this case, we must assess whether any misunderstanding could exist. -**缺乏新意** +**Lack of innovation** -不同工作经验下多个项目技术架构或项目结构一致,缺乏新意。一般情况而言,不同的公司技术栈不同,甚至产品的走向和模式完全不同。故此,当一个应聘者多家公司的多个项目中写到的技术千遍一律,业务流程异曲同工。看似整洁,实则更加缺乏说服力。 +If a candidate describes multiple projects with the same technical architecture or project structure, this suggests a lack of innovation. Generally, different companies have different tech stacks, and even product direction and models differ entirely. Therefore, when a candidate cites the same technology across multiple projects at various companies, the appearance of tidiness masks a lack of persuasiveness. -**技术过于新颖,对旧技术却只字不提** +**Too focused on new technologies, with no mention of old technologies** -技术过于新颖,根据互联网技术发展的走向来看,我们在不断向新型技术靠拢。但是任何企业作为资历深厚的 CTO、架构师来说。往往会选择更稳定、更成熟、学习成本更低的已有技术。对新技术的追求不会过于明显。而培训机构则是“哪项技术火我们就教哪项”。故此,出现了很多走入互联网行业的新人对旧技术一窍不通。甚至很多技术都没听过。 +When observing the evolution of internet technology, we continuously align with new technologies. However, any veteran CTO or architect in a company typically opts for more stable, mature, and lower-cost learning technologies. The pursuit of new technologies will not be overly evident. Conversely, training institutions tend to teach whatever technology is trending. Hence, many newcomers to the internet industry lack even basic knowledge of established technologies. -**工作经验较丰富,但从事的工作较低级。** +**Rich work experience but engaged in low-level work** -工作经验比较丰富,单从事的工作比较低级,这里存在很大的问题,要么就是原公司没法提供合理的舞台给该人员更好的发展空间,要么就是该人员能力不够,没法完成更高级的工作。当然,还有一种情况就是该人员包装过多的经验导致简历中不和谐。这种情况需要评估公司规模和背景。 +Having considerable work experience but engaged in low-level roles poses significant issues. It may indicate either that the prior company lacked the opportunity to provide better developmental pathways for the individual, or that the individual's skills were insufficient for more advanced work. There is also a scenario wherein excessive packaging of experience renders a resume discordant. Assessing the company’s scale and background becomes necessary in this situation. -**公司背景跨省跨市** +**Company background crossing provinces and cities** -可能很多用人单位和鄙人一样,最近接受到的简历,90%为跨市跳槽的人员。其中武汉占了 60%以上。均为武汉 XX 网络科技有限公司。公司规模均小于 50 人。也有厦门、宁波、南京等等。这个问题笔者就不提了,大家都懂的。跨地区跳槽不好查证。 +Many hiring entities, including myself, have recently noticed that 90% of resumes received involve individuals relocating across cities. Over 60% come from Wuhan XX Network Technology Co., Ltd., with all companies having fewer than 50 employees. Others hail from Xiamen, Ningbo, Nanjing, etc. This issue is self-explanatory; we all understand it. Cross-regional job-hopping is difficult to verify. -**缺少业余热情于技术的证明** +**Lack of passion for technology in personal projects** -有些眼高手低的技术员,做了几个管理系统。用到的技术确是各种分布式、集群、高并发、大数据、消息队列、搜索引擎、镜像容器、多数据库、数据中心等等。期望的薪资也高于行业标准。一个对技术很热情的人,业余时间肯定在技术方面花费过不少时间。那么可以从该人员的博客、git 地址入手。甚至可以通过手机号、邮箱、昵称、马甲。去搜索引擎进行搜集,核实该人员是否在论坛、贴吧、开源组织有过技术背景。 +Some overambitious technicians have completed several management systems yet have utilized a variety of distributed systems, clusters, high concurrency techniques, big data, message queues, search engines, image containers, multiple databases, and data centers—all while expecting salaries above industry standards. A genuinely passionate individual in technology would undoubtedly invest a considerable amount of personal time into it. Consequently, we can explore the candidate's blog, Git address, or even trace back their phone number, email, nickname, or pseudo name through search engines to verify whether the individual has a technical background in forums, discussion boards, or open-source organizations. -### 2. 进入面试阶段,如何甄别对方的水分 +### 2. Entering the Interview Phase: How to Identify Their Fluff -在甄别对方水分这一块,并没有明确的标准,但是笔者可以提几个点。这也是笔者在实际面试中惯用的做法。 +Identifying fluff in a candidate's statements does not follow defined standards, but I can propose a few points based on my usual interview practices. -**通过公司规模、团队规模、人员分配是否合理、人员合作方式来判断对方是否具备工作经验** +**Assessing the company’s size, team structure, reasonable personnel allocation, and collaboration methods to determine if the candidate possesses relevant work experience** -当招聘初级、初中级 IT 人员的时候,可以询问一些问题,比如公司有多少人、产品团队多少人、产品、技术、后端、前端、客户端、UI、测试各多少人。工作中如何合作的、产品做了多少时间、何时上线的、上线后多长时间迭代一个版本、多长时间迭代一个活动、发展至今多少用户(后端)、多大并发等等(后端)。根据笔者的经验,如果一个人没有任何从业周期,面对这些问题的时候,或多或少答非所问或者给出的答案非常不合理。 +When hiring junior or mid-level IT personnel, it can be beneficial to pose inquiries regarding the company’s size, the size of the product team, and distribution across product, technical, backend, frontend, UI, and testing roles. Understand how collaboration occurs, how long the product has been under development, when it was launched, the frequency of version iterations, the duration of activity iterations, the number of users (backend), and peak concurrency (backend). According to my experience, if a person lacks any practical experience, they will typically provide off-the-mark answers or give irrational responses when faced with these questions. -**背景公司入职时间、项目立项实现、完工时间、产品技术栈、迭代流程的核实** +**Verifying background company entry time, project initiation and completion times, product tech stack, and iteration processes** -很多应聘者对于简历过于包装,只为了追求更高的薪资。当我们问起:你是 xx 年 xx 月入职的该公司?你们项目是 xx 年 xx 月上线的?你们项目使用到 xx 技术?你们每次上线前夕是如何评审的。面对这些问题,应聘者给出的答案经常与简历不符合。这样问题就来了。关于项目使用到的技术,很多项目我们可以通过搜索该项目的地址、APP。通过 HTTP 协议、技术特征、抛出异常特征来大致判别对方使用到的技术。如果应聘者给出的答案明显与之不匹配,嘿嘿。 +Many candidates excessively embellish their resumes to pursue higher salaries. When we inquire: "You joined the company in xx year and xx month?" "Your project went live in xx year and xx month?" "What technology did your project use?" "How do you typically review your releases?" Candidates often provide answers that conflict with the information on their resumes. This prompts questions regarding the technology used in projects. We can generally assess utilized technologies through project addresses or apps, leveraging HTTP protocol, technical characteristics, and exceptional traits. If candidates provide responses that obviously do not match, that is telling. -**通过技术深度,甄别对方的技术水平** +**Assessing technical depth to gauge the candidate's technical level** -1. 确定对方的技术栈,如:你做过最满意的项目是哪个,为什么?你最喜欢使用的技术是哪些,为什么? +1. Determine the candidate's tech stack: For example, "What project are you most satisfied with, and why?" "What technologies do you prefer to use, and why?" +1. Assess the product's developmental stage: "How long has your product been in development, how many iterations have occurred, how many versions released, what is the current user count, and what peak concurrency are you handling?" +1. Identify the candidate's technical networking habits: "What channels do you use to interact with other technical individuals, and what technologies have you primarily communicated about?" -2. 确定对方项目的发展程度,如:你们产品做了多久,迭代了多久,发布了多少版本,发展到了多少用户,带来多大并发,多少流水? +Recently, many candidates presented resumes filled with an array of technologies. To avoid stepping outside the candidate’s expertise, I specifically focus on the technologies mentioned in their resumes when forming my inquiries. Here are a few examples. -3. 确定对方的技术属性,如:平时你会通过什么渠道跟其他技术人形成技术沟通与交流,主要交流过哪些技术? +**1) A candidate claimed proficiency in Redis.** -笔者最近接待的面试者,很多面试者的简历上,写着层出不穷的各种技术,为了不跨越求职者的技术栈,笔者专门挑应聘者简历写到或用到的技术来进行询问。笔者举几个例子。 +1. Describe the Redis data structures you have used and their business scenarios; +1. Explain the plugins you used to manipulate Redis; +1. Clarify the serialization methods employed; +1. Discuss any significant issues you encountered while using Redis; -**1)某求职者简历上写着熟练使用 Redis。** +**2) A candidate asserted familiarity with HTTP protocols and claimed to have written web crawlers.** -1. 介绍一下你使用过 Redis 的哪些数据结构,并描述一下使用的业务场景; -2. 介绍一下你操作 Redis 用到的是什么插件; -3. 介绍一下你们使用的序列化方式; -4. 介绍一下你们使用 Redis 遇到过给你印象较深的问题; +1. Describe the HTTP headers you are familiar with and their purposes; +1. If a frontend submission succeeds but the backend cannot receive the data, how would you troubleshoot this issue?; +1. Explain the basic structure of an HTTP message; +1. If the server returns a Cookie, what header field in the response contains it?; +1. What does it signify if the server returns `Transfer-Encoding: chunked`?; +1. Can you describe segmented loading and its technical process? -**2)某求职者声称熟练 HTTP 协议并编写过爬虫。** +Of course, the technical depth will vary based on the specific technologies involved. -1. 介绍一下你所了解的几个 HTTP head 头并描述其用途; -2. 如果前端提交成功,后端无法接受数据,这时候你将如何排查问题; -3. 描述一下 HTTP 基本报文结构; -4. 如果服务器返回 Cookie,存储在响应内容里面 head 头的字段叫做什么; -5. 当服务端返回 Transfer-Encoding:chunked 代表什么含义 -6. 是否了解分段加载并描述下其技术流程。 +The general approach is as follows: If you claim to have slaughtered pigs, I’ll surely ask how many pigs, at what times, and the sizes and colors of them. The responses might be: "I’ve slaughtered dozens, some weighing fifty pounds, and they’ve been green, yellow, red, and blue." Yet, issues arise when a candidate claiming to have used Git for two years is unaware of GitHub, or another has a year’s experience using Redis without knowledge of data structures or serialization methods, or a specialist in web scraping fails to understand the meaning of `content-type`, or an individual using search engine technology cannot name two segmentation plugins while working with read-write separation in databases but knows nothing of synchronization delays, etc. -当然,面向不同的技术,对应的技术深度自然也不一样。 - -大体上的套路便是如此:你说你杀过猪。那么你杀过几头猪,分别是啥时候,杀过多大的猪,有啥毛色。事实上对方可能给你的回答是:杀过、十几头、杀过五十斤的、杀过绿色、黄色、红色、蓝色的猪。那么问题就来了。 - -然而笔者碰到的问题是:使用 Git 两年却不知道 GitHub、使用 Redis 一年却不知道数据结构也不知道序列化、专业做爬虫却不懂 `content-type` 含义、使用搜索引擎技术却说不出两个分词插件、使用数据库读写分离却不知道同步延时等等。 - -写在最后,笔者认为在招聘途中,并不是不允许求职者包装,但是尽可能满足能筹平衡。虽然这篇文章没有完美的结尾,但是笔者提供了面试失败的各种经验。笔者最终招到了如意的小伙伴。也希望所有技术面试官早日找到符合自己产品发展的 IT 伙伴。 +In conclusion, I believe that in the recruitment process, it is not forbidden for candidates to package themselves, but it is crucial to strike a balance. Although this article does not have a perfect ending, I have provided various experiences regarding interview failures. Ultimately, I found suitable partners. I also hope that all technical interviewers find their ideal IT partners that align with their product development needs soon. diff --git a/docs/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.md b/docs/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.md index 175efc3da14..32b61508b51 100644 --- a/docs/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.md +++ b/docs/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.md @@ -1,119 +1,119 @@ --- -title: 阿里技术面试的一些秘密 -category: 技术文章精选集 -author: 龙叔 +title: Some Secrets of Alibaba's Technical Interviews +category: Selected Technical Articles +author: Uncle Long tag: - - 面试 + - Interview --- -> **推荐语**:详细介绍了求职者在面试中应该具备哪些能力才会有更大概率脱颖而出。 +> **Recommendation**: It details the abilities that job seekers should possess to have a higher probability of standing out in interviews. > -> **原文地址:** +> **Original Article Address:** -最近我的工作稍微轻松些,就被安排去校招面试了 +Recently, my work has become slightly easier, so I was assigned to participate in campus recruitment interviews. -当时还是有些**激动**的,以前都是被面试的,现在我自己也成为一个面试别人的面试官 +At that time, I was a bit **excited**; previously, I was the one being interviewed, and now I have become an interviewer. -接下来就谈谈我的面试心得(谈谈阿里面试的秘籍) +Next, I will share my interview insights (talk about Alibaba's interview secrets). -## 我是怎么筛选简历的? +## How Do I Screen Resumes? -面试之前都是要筛选简历,这个大家应该知道 +Before the interviews, resumes need to be screened, which everyone should know. -阿里对待招聘非常负责任,面试官必须对每位同学的简历进行查看和筛选,如果不合适还需要写清楚理由 +Alibaba takes recruitment very seriously; interviewers must review and filter every candidate's resume, and if it's not suitable, they need to clearly state the reasons. -对于校招生来说,第一份工作非常重要,而且校招的面试机会也只有一次,一旦收到大家的简历意味着大家非常认可和喜爱阿里这家公司 +For campus recruits, the first job is very important, and there is only one chance for the campus recruitment interview. Receiving everyone's resumes means they highly recognize and appreciate Alibaba as a company. -所以我们对每份简历都会认真看,大家可以非常放心,不会无缘无故挂掉大家的简历 +Therefore, we look carefully at each resume; you can rest assured that we won't discard resumes without reason. -尽管我们报以非常负责任的态度,但有些同学们的简历实在是难以下看 +Although we approach this with a very responsible attitude, some candidates' resumes are indeed difficult to read. -关于如何写简历,我之前写过类似的文章,这里就把之前的文章放这里让大家看看 [一份好的简历应该有哪些内容](https://mp.weixin.qq.com/s?__biz=MzI4MDYzNDc1Mg==&mid=2247484010&idx=1&sn=afbe90c8446f5f21631cae750431d3ee&scene=21#wechat_redirect) +I have previously written similar articles on how to write a resume, so I will link to it here for everyone to check out: [What Should a Good Resume Include](https://mp.weixin.qq.com/s?__biz=MzI4MDYzNDc1Mg==&mid=2247484010&idx=1&sn=afbe90c8446f5f21631cae750431d3ee&scene=21#wechat_redirect). -在筛选简历的时候会有以下信息非常重要,大家一定要认真写 +The following information is very important when screening resumes, and candidates must write them carefully: -- **项目经历**,具体写法可以看上面提到的文章 -- **个人含金量比较高的奖项**,比如 ACM 奖牌、计算机竞赛等 -- **个人技能** 这块会看,但是大多数简历写法都差不多,尽量写得**言简意赅** -- **重要期刊论文发表、开源项目** 加分项 +- **Project Experience**; specifics can be found in the aforementioned article. +- **High-Value Personal Awards**; such as ACM medals, computer competitions, etc. +- **Personal Skills**; this section will be reviewed, but most resumes are similar, so try to write **concise** descriptions. +- **Publications in Important Journals, Open Source Projects**; these are considered pluses. -这些信息非常重要,我筛选简历的时候这些信息占整份简历的比重 4/5 左右 +This information is highly important, and during my resume screening, these aspects account for about 4/5 of the entire resume. -## 面试的时候我会注重哪些方面? +## What Aspects Do I Focus on During the Interview? -### **表达要清楚** +### **Clear Expression** -这点是硬伤,在面试的时候有些同学半天说不清楚自己做的项目,我都在替你着急 +This is a major flaw; during interviews, some candidates struggle to clearly explain their projects, and I feel anxious for them. -描述项目有个简单的方法论,我自己总结的 大家看看适不适合自己 +There is a simple methodology for describing projects that I have summarized, and I hope it suits you: -- 最好言简意赅的描述一下你的项目背景,让面试官很快知道项目干了啥(让面试官很快对项目感兴趣) -- 说下项目用了哪些技术,做技术的用了哪些技术得说清楚,面试官会对你的技术比较感兴趣 -- 解决了什么问题,做项目肯定是为了解决问题,总不能为了做项目而做项目吧(解决问题的能力非常重要) -- 遇到哪些难题,如何突破这些难题,项目遇到困难问题很正常,突破困难才是一次好的成长 -- 项目还有哪些完善的地方,不可能设计出完美的执行方案,有待改进说明你对项目认识深刻,思考深入 +- It's best to concisely describe your project's background so the interviewer quickly understands what the project is about (this engages their interest quickly). +- Talk about which technologies were used in the project. Be clear on the technologies you worked with since interviewers will be very interested in your technical skills. +- What problems were solved; projects are generally undertaken to solve problems, as projects shouldn't just exist for their own sake (the ability to solve problems is very important). +- What difficulties were encountered, and how were they overcome? It's normal to face difficulties in a project; overcoming them is a sign of good growth. +- What areas still require improvement? It's impossible to design a perfect execution plan; suggesting improvements shows your deep understanding of the project and thorough thinking. -一场面试时间一般 60—80 分钟,好的表达有助于彼此之间了解更多的问题 +The general interview lasts about 60-80 minutes, and good expression helps us understand each other better. -### **基础知识要扎实** +### **Solid Foundation Knowledge** -校招非常注重基础知识,所以这块问的问题比较多,我一般会结合你项目去问,看看同学对技术是停留在用的阶段还是有自己的深入思考 +Campus recruitment places a strong emphasis on foundational knowledge, so the questions in this area are numerous. I typically ask questions related to your projects to see if you have merely utilized the technology or if you have deeper insights. -每个方向对基础知识要求不同,但有些基础知识是通用的 +Different fields have different requirements for foundational knowledge, but some basics are applicable across the board. -比如**数据结构与算法**、**操作系统**、**计算机网络** 等 +For instance, **Data Structures and Algorithms**, **Operating Systems**, **Computer Networks**, etc. -这些基础技术知识一定要掌握扎实,技术岗位都会或多或少去问这些基础 +These technical fundamentals must be mastered thoroughly, as technical positions will generally inquire about these basics. -### **动手能力很重要** +### **Practical Skills Are Important** -action,action,action ,重要的事情说三遍,做技术的不可能光靠一张嘴,能落地才是最重要的 +Action, action, action – I can't emphasize this enough. People in technical roles cannot rely solely on words; being able to implement is most crucial. -面试官除了问你基础知识和项目还会去考考你的动手能力,面试时间一般不会太长,根据岗位的不同一般会让同学们写一些算法题目 +Besides asking you about foundational knowledge and projects, interviewers will also assess your practical abilities. The interview time typically isn't too long, and will often involve writing some algorithm questions depending on the position. -阿里面试,不会给你出非常变态的算法题目 +In Alibaba interviews, you're not given exceedingly difficult algorithm problems. -主要还是考察大家的动手能力、思考问题的能力、数据结构的应用能力 +The goal is primarily to evaluate your practical skills, problem-solving abilities, and understanding of data structures. -在写代码的过程中,我也总结了自己的方法论: +Through my coding experience, I have summarized my own methodology: -- 上来不要先写,审题、问清楚题目意图,不要自以为是的去理解思路,工作中 沟通需求、明确需求、提出质疑和建议是非常好的习惯 -- 接下来说思路 思路错了写到一半再去改会非常浪费时间 -- 描述清楚之后,先写代码思路的步骤注释,一边写注释,脑子里迭代一遍自己的思路是否正确,是否是最优解 -- 最后,代码规范 +- Don't start by writing immediately; first analyze the problem and ask for clarification on the question's intent. Avoid making assumptions without understanding; it's a very good habit in the workplace to communicate needs, clarify requirements, and raise questions and suggestions. +- Then, articulate your thoughts. If your approach is incorrect, rewriting halfway through will waste a lot of time. +- After clearly describing your thought process, first write the steps of your coding thoughts in comments. While writing comments, iterate over whether your thinking is correct and if it's the optimal solution. +- Lastly, adhere to coding standards. -## 除了上面这些常规的方面 +## Aside from These Conventional Aspects -其实,现在面试已经非常**卷**了,上面说的这些很多都是 **八股文** +In fact, interviews have become very **intense**; much of what’s been discussed is just **formulaic**. -有些学生会拿到很多面试题目和答案,反复的去记忆,面试官问问题他就开始在脑子里面检索答案 +Some students acquire numerous interview questions and answers and memorize them repeatedly. When an interviewer asks a question, they search for an answer in their head. -我一般问几个问题就知道该学生是不是在背八股文了。 +I typically know if a student is just reciting formulaic responses after asking a few questions. -对于背八股文的同学,我真的非常难过。 +I genuinely feel saddened for students who rely on rote memorization. -尽管你背的很好,但不能给你过啊,得对得起自己职责,得对公司负责啊! +Even if you remember everything well, it doesn't justify passing; one must be responsible to oneself and the company! -背的在好,不如理解一个知识点,理解一个知识点会有助于你去理解很多其他的知识点,很多知识点连起来就是一个知识体系。 +Understanding a knowledge point is far more valuable than memorizing it. Grasping one concept can help you understand many others, and many knowledge points together form a knowledge system. -当面试官问你体系中的任何一个问题,都可以把这个体系讲给他听,不是**背诵** 。 +When interviewers ask about any topic within that system, you should be able to articulate the entire framework rather than just **reciting**. -深入理解问题,我会比较关注。 +I pay close attention to a deep understanding of issues. -我在面试过程中,会通过一个问题去问一串问题,慢慢就把整体体系串起来。 +During my interviews, I ask a series of questions rooted in a single theme, gradually connecting the entire framework. -你的**比赛**和**论文**是你的亮点,这些东西是非常重要的加分项。 +Your **competitions** and **papers** are your highlights; these elements are significant bonus points. -我也会在面试中穿插一些**开放性题目**,都是思考题 考验一个同学思考问题的方式。 +I also weave some **open-ended questions** into the interview—they are thought exercises that test how a candidate thinks through problems. -## 最后 +## Finally -作为一个面试官,我很想对大家说,每个企业都非常渴望人才,都希望找到最适合企业发展的人 +As an interviewer, I want to say that every company is eager for talent and hopes to find the most suitable people for its development. -面试的时候面试官会尽量去挖掘你的价值。 +During the interview, interviewers will try their best to uncover your value. -但是,面试时间有限,同学们一定要在有限的时间里展现出自己的**能力**和**无限的潜力** 。 +However, time is limited during interviews, and candidates must demonstrate their **capabilities** and **infinite potential** within that time. -最后,祝愿优秀的你能找到自己理想的工作! +In conclusion, I wish you all the best in finding your ideal job! diff --git a/docs/high-quality-technical-articles/interview/summary-of-spring-recruitment.md b/docs/high-quality-technical-articles/interview/summary-of-spring-recruitment.md index 474434645a9..e1096911de8 100644 --- a/docs/high-quality-technical-articles/interview/summary-of-spring-recruitment.md +++ b/docs/high-quality-technical-articles/interview/summary-of-spring-recruitment.md @@ -1,161 +1,37 @@ --- -title: 普通人的春招总结(阿里、腾讯offer) -category: 技术文章精选集 -author: 钟期既遇 +title: Summary of Spring Recruitment for Ordinary People (Alibaba, Tencent Offers) +category: Selected Technical Articles +author: Zhong Qi Ji Yu tag: - - 面试 + - Interview --- -> **推荐语**:牛客网热帖,写的很全面!暑期实习,投了阿里、腾讯、字节,拿到了阿里和腾讯的 offer。 +> **Recommendation**: A popular post on Niuke.com, very comprehensive! For summer internships, I applied to Alibaba, Tencent, and ByteDance, and received offers from Alibaba and Tencent. > -> **原文地址:** +> **Original Article Link:** > -> **下篇**:[十年饮冰,难凉热血——秋招总结](https://www.nowcoder.com/discuss/804679) +> **Next Article**: [Ten Years of Drinking Ice, Hard to Cool Hot Blood—Autumn Recruitment Summary](https://www.nowcoder.com/discuss/804679) -## 背景 +## Background -写这篇文章的时候,腾讯 offer 已经下来了,春招也算结束了,这次找暑期实习没有像去年找日常实习一样海投,只投了 BAT 三家,阿里和腾讯收获了 offer,字节没有给面试机会,可能是笔试太拉垮了。 +When writing this article, I had already received the Tencent offer, and the spring recruitment was considered over. This time, I didn't apply widely for summer internships like I did last year for regular internships; I only applied to the three BAT companies. I received offers from Alibaba and Tencent, but ByteDance did not give me an interview opportunity, probably because my written test performance was poor. -楼主大三,双非本科,我的春招的起始时间应该是 2 月 20 日到 3 月 23 日收到阿里意向书为止,但是从 3 月 7 日蚂蚁技术终面面完之后就没有面过技术面了,只面过两个 HR 面,剩下的时间都在等 offer。最开始是找朋友内推了字节财经的日常实习,但是到现在还在简历评估,后面又投了财经的暑期实习,笔试之后就一直卡在流程里了。腾讯是一开始被天美捞了,一面挂了之后被 PCG 捞了,最后走完了流程。阿里提前批投了好多部门,蚂蚁最先走完了终面,就录入了系统,最后拿了 offer。这一路走过来真的是酸甜苦辣都经历过,因为学历自卑过,以至于想去考研。总而言之,一定要找一个搭档和你一起复习,比如说 @你怕是个憨批哦,这是我实验室的同学,也是我们实验室的队长,这个人是真的强,阿里核心部门都拿遍了,他在我复习的过程中给了我很多帮助。 +I am a junior in college, from a non-prestigious university. My spring recruitment timeline was from February 20 to March 23, when I received the intention letter from Alibaba. However, after finishing the final technical interview with Ant Group on March 7, I did not have any more technical interviews, only two HR interviews, and spent the rest of the time waiting for offers. Initially, I sought a referral from a friend for a regular internship at ByteDance, but it is still under resume evaluation. Later, I applied for a summer internship in finance, but after the written test, I got stuck in the process. I was initially contacted by TiMi Studios at Tencent, but after failing the first interview, I was picked up by PCG and completed the process. I applied to many departments in Alibaba's early batch, and Ant Group completed the final interview first, which was recorded in the system, and I finally received the offer. This journey has truly been filled with ups and downs; I experienced feelings of inferiority due to my academic background, which made me consider pursuing a master's degree. In summary, it is essential to find a partner to study with, for example, @You Might Be a Fool, who is a classmate from my lab and also the team leader. This person is genuinely strong and has received offers from core departments at Alibaba. He provided me with a lot of help during my review. -## 写这个帖子的目的 +## Purpose of Writing This Post -1. 写给自己:总结反思一下大学前三年以及找工作的一些经历与感悟。 -2. 写给还在找实习的朋友:希望自己的经历以及面经]能给你们一些启发和帮助。 -3. 写给和我一样有着大厂梦的学弟学妹们:你们还有很长的准备时间,无论你之前在干什么,没有目标也好,碌碌无为也好,没找对方向也好,只要从现在开始,找对学习的方向,并且坚持不懈的学上一年两年,一定可以实现你的梦想的。 +1. To write for myself: to summarize and reflect on my experiences and insights from the first three years of college and job hunting. +1. To write for friends still looking for internships: I hope my experiences and interview insights can provide you with some inspiration and help. +1. To write for juniors who, like me, dream of working at big companies: you still have a long preparation time. No matter what you have been doing, whether you have no goals, feel aimless, or haven't found the right direction, as long as you start now, find the right direction for your studies, and persistently study for a year or two, you can definitely achieve your dreams. -## 我的大学经历 +## My College Experience -先简单聊聊一下自己大学的经历。 +Let me briefly talk about my college experience. -本人无论文、无比赛、无 ACM,要啥奖没啥奖,绩点还行,不是很拉垮,也不亮眼。保研肯定保不了,考研估计也考不上。 +I have no papers, no competitions, no ACM, and I have no awards. My GPA is decent, not too poor, but not outstanding. I definitely cannot secure a recommendation for graduate school, and I probably won't pass the entrance exam either. -大一时候加入了工作室,上学期自学了 C 语言和数据结构,从寒假开始学 Java,当时还不知道 Java 那么卷,我得到的消息是 Java 好找工作,这里就不由得感叹信息差的重要性了,我当时只知道前端、后端和安卓开发,而我确实对后端开发感兴趣,但是因为信息差,我只知道 Java 可以做后端开发,并不知道后端开发其实是一个很局限的概念,后面才慢慢了解到后台开发、服务端开发这些名词,也不知道 C++、Golang 等语言也可以做后台开发,所以就学了 Java。但其实 Java 更适合做业务,C++ 更适合做底层开发、服务端开发,我虽然对业务不反感,但是对 OS、Network 这些更感兴趣一些,当然这些会作为我的一些兴趣,业余时间会自己去研究下。 +In my freshman year, I joined a studio. In the first semester, I self-studied C language and data structures, and started learning Java during the winter break. At that time, I didn't know how competitive Java was; I only heard that Java was good for finding jobs. This highlights the importance of information disparity. I only knew about front-end, back-end, and Android development, and I was indeed interested in back-end development. However, due to the information gap, I only knew that Java could be used for back-end development, not realizing that back-end development is actually a very limited concept. I later gradually learned about terms like back-end development and server-side development, and I didn't know that languages like C++ and Golang could also be used for back-end development, so I learned Java. But in fact, Java is more suitable for business applications, while C++ is more suitable for low-level and server-side development. Although I don't dislike business applications, I am more interested in OS and Network topics, which I will explore in my spare time. -### 学习路线 +### Learning Path -大概学习的路线就是:Java SE 基础 -> MySQL -> Java Web(主要包括 JDBC、Servlet、JSP 等)-> SSM(其实当时 Spring Boot 已经兴起,但是我觉得没有 SSM 基础很难学会 Spring Boot,就先学了 SSM)-> Spring Boot -> Spring Cloud(当时虽然学了 Spring Cloud,但是缺少项目的锤炼,完全不会用,只是了解了分布式的一些概念)-> Redis -> Nginx -> 计算机网络(本来是计算机专业的必修课,可是我们专业要到大三下才学,所以就提前自学了)-> Dubbo -> Zookeeper -> JVM -> JUC -> Netty -> Rabbit MQ -> 操作系统(同计算机网络)-> 计算机组成原理(直接不开这门课)。 - -这就是我的一个具体的学习路线,大概是在大二的下学期学完的这些东西,都是通过看视频学的,只会用,并不了解底层原理,达不到面试八股文的水准,把这些东西学完之后,搭建起了知识体系,就开始准备面试了,大概的开始时间是去年的六月份,开始在牛客网上看一些面经,然后会自己总结。准备面试的阶段我觉得最重要的是啃书 + 刷题,八股文只是辅助,我们只是自嘲说面试就背背八股文,但其实像阿里这样的公司,背八股文是完全不能蒙混过关的,除非你有非常亮眼的项目或者实习经历。 - -### 书籍推荐 - -- 《Thinking in Java》:不多说了,好书,但太厚了,买了没看。 -- 《深入理解 Java 虚拟机》:JVM 的圣经,看了两遍,每一遍都有不同的收获。 -- 《Java 并发编程的艺术》:阿里人写的,基本涵盖了面试会问的并发编程的问题。 -- 《MySQL 技术内幕》:写的很深入,但是对初学者可能不太友好,第一感觉写的比较深而杂,后面单独去看每一章节,觉得收获很大。 -- 《Redis 设计与实现》:书如其名,结合源码深入讲解了 Redis 的实现原理,必看。 -- 《深入理解计算机系统》:大名鼎鼎的 CSAPP,对你面 Java 可能帮助不是很大,但是不得不说这是一本经典,涵盖了计算机系统、体系结构、组成原理、操作系统等知识,我蚂蚁二面的时候就被问了遇到的最大的困难,我就和面试官交流了读这本书中遇到的一些问题,淘系二面的时候也和面试官交流了这本书,我们都觉得这本书还需要二刷。 -- 《TCP/IP 详解卷 1》:我只看了 TCP 相关的章节,但是是有必要通读一遍的,面天美时候和面试官交流了这本书。 -- 《操作系统导论》:颇具盛名的 OSTEP,南大操作系统的课本,看的时候可以结合在 B 站蒋炎岩老师的视频,我会在下面放链接。 - -这几本书理解透彻了,我相信面试的时候可以面试官面试官聊的很深入了,面试官也会对你印象非常好。但是对于普通人来说,看一遍是肯定记不住的,遗忘是非常正常的现象,我很多也只看了一遍,很多细节也记不清了,最近准备二刷。 - -更多书籍推荐建议大家看 [JavaGuide](https://javaguide.cn/books/) 这个网站上的书籍推荐,比较全面。 - -![](https://oss.javaguide.cn/p3-juejin/62099c9b2fd24d3cb6511e49756f486b~tplv-k3u1fbpfcp-zoom-1.png) - -### 教程推荐 - -我上面谈到的学习路线,我建议是跟着视频学,尚硅谷和黑马的教程都可以,一定要手敲一遍。 - -- [2021 南京大学 “操作系统:设计与实现” (蒋炎岩)](https://www.bilibili.com/video/BV1HN41197Ko):我不多说了,看评论就知道了。 -- [SpringSecurity-Social-OAuth2 社交登录接口授权鉴权系列课程](https://www.bilibili.com/video/BV16J41127jq):字母哥讲的 Spring Security 也很好,Spring Security 或者 Shiro 是做项目必备的,会一个就好,根据实际场景以及个人喜好(笑)来选型。 -- [清华大学邓俊辉数据结构与算法](https://www.bilibili.com/video/BV1jt4y117KR):清华不解释了。 -- [MySQL 实战 45 讲](https://time.geekbang.org/column/intro/100020801):前 27 讲多看几遍基本可以秒杀面试中遇到的 MySQL 问题了。 -- [Redis 核心技术与实战](https://time.geekbang.org/column/intro/100056701):讲解了大量的 Redis 在生产上的使用场景,和《Redis 设计与实现》配合着看,也可以秒杀面试中遇到的 Redis 问题了。 -- [JavaGuide](https://javaguide.cn/books/):「Java 学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。 -- [《Java 面试指北》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247519384&idx=1&sn=bc7e71af75350b755f04ca4178395b1a&chksm=cea1c353f9d64a458f797696d4144b4d6e58639371a4612b8e4d106d83a66d2289e7b2cd7431&token=660789642&lang=zh_CN&scene=21#wechat_redirect):这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 - -## 找工作 - -大概是去年 11 月的时候,牛客上日常实习的面经开始多了起来,我也有了找实习的意识,然后就开始一边复习一边海投,投了很多公司,给面试机会的就那几家,腾讯二面挂了两次,当时心态完全崩了,甚至有了看空春招的想法。很幸运最后收获了一个实习机会,在实习的时候,除了完成日常的工作以外,其余时间也没有松懈,晚上下班后、周末的时间都用来复习,心里也暗暗下定决心,春招一定要卷土重来! - -从二月下旬开始海投阿里提前批,基本都有了面试,开系统那天收到了 16 封内推邮件,具体的面经可以看我以前发的文章。 - -从 3.1 到 3.7 那一个周平均每天三场面试,真的非常崩溃,一度想考研,也焦虑过、哭过、笑过,还好结果是好的,最后也去了一直想去的支付宝。 - -我主要是想通过自己对面试过程的总结给大家提一些建议,大佬不喜勿喷。 - -### 面试准备 - -要去面试首先要准备一份简历,我个人认为一份好的简历应该有一下三个部分: - -1. 完整的个人信息,这个不多说了吧,个人信息不完整面试官或 HR 都联系不上你,就算学校不好也要写上去,因为听说有些公司没有学校无法进行简历评估,非科班或者说学校不太出名可以将教育信息写在最下面。 -2. 项目/实习经历,项目真的很重要,面试大部分时间会围绕着项目来,你项目准备好了可以把控面试的节奏,引导面试官问你擅长的方向,我就是在这方面吃了亏。如果没有项目怎么办,可以去 GitHub 上找一些开源的项目,自己跟着做一遍,加入一些自己的思考和理解。还有做项目不能简单实现功能,还要考虑性能和优化,面试官并不关注你这个功能是怎么实现的,他想知道的是你是如何一步步思考的,一开始的方案是什么,后面选了什么方案,对性能有哪些提升,还能再改进吗? -3. 具备的专业技能,这个可以简单的写一下你学过的专业知识,这样可以让面试官有针对的问一些基础知识,切忌长篇罗列,最擅长的一定要写在上面,依次往下。 - -简历写好了之后就进入了投递环节,最好找一个靠谱的内推人,因为内推人可以帮你跟进面试的进度,必要时候和 HR 沟通,哪怕挂了也可以告诉你原因,哪些方面表现的不好。现在内推已经不再是门槛,而是最低的入场券,没有认识的人内推也可以在牛客上找一些师兄内推,他们往往也很热情。 - -在面试过程中一定不要紧张,因为一面面试官可能比我们大不了几岁,也工作没几年,所以 duck 不必紧张的不会说话,不会就说不会,然后笑一下,会就流利的表达出来,面试并不是一问一答,面试是沟通,是交流,你可以大胆的说出自己的思考,表达沟通能力也是面试的一个衡量指标。 - -我个人认为面试和追妹子是差不多的,都是尽快的让对方了解自己,发现你身上的闪光点,只不过面试是让面试官了解你在技术上的造诣。所以,自我介绍环节就变得非常重要,你可以简单介绍完自己的个人信息之后,介绍一下你做过的项目,自我介绍最好长一些,因为在面试前,面试官可能没看过你的简历(逃),你最好留给面试官充足的时间去看你的简历。自我介绍包括项目的介绍可以写成一遍文档,多读几遍,在面试的时候能够背下来,实在不行也可以照着读。 - -### 项目 - -我还是要重点讲一下项目,我以前认为项目是一个不确定性非常大的地方,后来经过面试才知道项目是最容易带面试官节奏的地方。问项目的意义是通过项目来问基础知识,所以就要求你对自己的项目非常熟悉,考虑各种极端情况以及优化方案,熟悉用到的中间件原理,以及这些中间件是如何处理这些情况的,比如说,MQ 的宕机恢复,Redis 集群、哨兵,缓存雪崩、缓存击穿、缓存穿透等。 - -优化主要可以从缓存、MQ 解耦、加索引、多线程、异步任务、用 ElasticSearch 做检索等方面考虑,我认为项目优化主要的着手点就是减少数据库的访问量,减少同步调用的次数,比如说加缓存、用 ElasticSearch 做检索就是通过减少数据库的访问来实现的优化,MQ 解耦、异步任务等就是通过减少同步调用的次数来实现的优化。 - -项目中还可以学到很多东西,比如下面的这些就是通过项目来学习的: - -1. 权限控制(ABAC、RBAC) -2. JWT -3. 单点登录 -4. 分库分表 -5. 分片上传/导出 -6. 分布式锁 -7. 负载均衡 - -当然还有很多东西,每个人的项目不一样,能学到的东西也天差地别,但是你要相信的是,你接触到的东西,面试官应该是都会的,所以一定要好好准备,不然容易被怼。 - -本质上来讲,项目也可以拆解成八股文,可以用准备基础知识的方式来准备项目。 - -### 算法 - -项目的八股文化,会进一步导致无法准确的甄选候选人,所以就到了面试的第三个衡量标准,那就是算法,我曾经在反问阶段问过面试官刷算法对哪些方面有帮助,面试官直截了当的对我说,刷题对你以后找工作有帮助。我的观点是算法其实也是可以通过记忆来提高的,LeetCode 前 200 道题能刷上 3 遍,我不信面试时候还能手撕不了,所以在复习的过程中一定要保持算法的训练。 - -### 面试建议 - -1. 自我介绍尽量丰富一下,项目提前准备好如何介绍。 -2. 在面试的时候,遇到不会的问题最好不要直接说不会,然后愣着,等面试官问下一个问题,你可以说自己对这方面不太了解,但是对 XX 有一些了解,然后讲一下,如果面试官感兴趣,你就可以继续说,不感兴趣他就会问下一个问题,面试官一般是不会打断的,这也是让面试官快速了解你的一个小技巧。 -3. 尽量向面试官展示你的技术热情,比如说你可以和面试官聊 Java 每个版本的新特性,最近技术圈的一些新闻等等,因为就我所知,技术热情也是阿里面试考察的一方面。 -4. 面试是一个双向选择的过程,不要表现的太过去谄媚。 -5. 好好把握好反问阶段,问一些有价值的内容,比如说新人培养机制、转正机制等。 - -## 经验 - -1. 如果你现在大一,OK,我希望你能多了解一下互联网就业的方向,看看自己的兴趣在哪,先把基础打好,比如说数据结构、操作性、计算机网络、计算机组成原理,因为这四门课既是大部分学校考研的专业课,也是面试中常常会被问到的问题。 -2. 如果已经大二了,那就要明确自己的方向,要有自驱力,知道你学习的这个方向都要学哪些知识,学到什么程度能够就业,合理安排好时间,知道自己在什么阶段要达到什么样的水准。 -3. 如果你学历比较吃亏,亦或是非科班出身,那么我建议你一定要付出超过常人的努力,因为在我混迹牛客这么多年,我看到的面经一般是学校好一些的问的简单一些,相对差一些的问的难一些,其实也可以理解,毕竟普遍上来说名校出身的综合实力要强一些。 -4. 尽量早点实习,如果你现在大二,已经有了能够实习的水平,我建议你早点投简历,尽量找暑期实习,你相信我,如果你这个暑假去实习了,明年一定是乱杀。 -5. 接上条,如果找不到实习,尽量要做几个有挑战的项目,并且找到这个项目的抓手。 -6. 多刷刷牛客,我在牛客上就认识了很多志同道合的人,他们在我找工作过程中给了我很多帮助。 - -## 建议 - -1. 一定要抱团取暖,一起找工作的同学可以拉一个群,无论是自己学校的还是网上认识的,平常多交流复习心得,n 个 1 相加的和一定是大于 n 的。 -2. 知识的深度和广度都很重要,平常一定要多了解新技术,而且每学一门技术一定要争取了解它的原理,不然你学的不算是计算机,而是英语系,工作职位也不是研发工程师,而是 API 调用工程师。 -3. 运营好自己的 CSDN、掘金等博客平台,我有个学弟大二是 CSDN 博客专家,已经有猎头联系他了,平常写的代码尽量都提交到 GitHub 上,无论是项目也好,实验也好,如果有能力的话最好能录制一些视频发到哔哩哔哩上,因为这是面试官在面试你之前了解你表达能力的一个重要途径。 -4. 心态一定要好,面试不顺利,不一定是你的能力问题,也可能是因为他们招人很少,或者说某一些客观条件与他们不匹配,一定要多尝试不同的选择。 -5. 多和人沟通交流,不要自己埋头苦干,因为你以后进公司里也需要和别人合作,所以表达和沟通能力是一项基本的技能,要提前培养。 - -## 闲聊 - -### 谈谈信息差 - -我觉得学校的差距并不只是体现在教学水平上,诚然名校的老师讲课水平、实验水平都是高于弱校的,但是信息差才是主要的差距。在 985 学校里面读书,不仅能接触到更多优质企业的校招宣讲、讲座,还能接触到更好的就业氛围,因为名校里面去大厂、去外企的人、甚至出国的人更多,学长学姐的内推只是一方面,另一方面是你可以从他们身上学到技术以外的东西,而双非学校去大厂的人少,他们能影响的只是很少一部分人,这就是信息差。信息差的劣势主要体现在哪些方面呢?比如人家大二已经开始找日常实习了,而你认为找工作是大四的事情,人家大三已经找到暑期实习了,你暑假还需要去参加学校组织的培训,一步步的就这样拉下了。 - -好在,互联网的出现让信息更加透明,你可以在网上检索各种各样你想要的信息,比如我就在牛客]上认识了一些志同道合的朋友,他们在找工作的过程中给了我很多帮助。平常可以多刷刷牛客,能够有效的减小信息差。 - -### 谈谈 Java 的内卷 - -Java 卷吗?毫无疑问,很卷,我个人认为开发属于没有什么门槛的工作,本科生来干正合适,但是因为算法岗更是神仙打架,导致很多的研究生也转了开发,而且基本都转了 Java 开发。Java 的内卷只是这个原因造成的吗?当然不是,我认为还有一个原因就是培训机构的兴起,让这个行业的门槛进一步降低,你要学什么东西,怎么学,都有人给你安排好了,这是造成内卷的第二个原因。第三个原因就是非科班转码,其它行业的凋落和互联网行业的繁荣形成了鲜明对比,导致很多其它专业的人也自学计算机,找互联网的工作,导致这个行业的人越来越多,蛋糕就那么大,分蛋糕的人却越来越多。 - -其实内卷也不一定是个坏现象,这说明阶级上升的通道还没有完全关闭,还是有不少人愿意通过努力来改变现状,这也一定程度上会加快行业的发展,社会的发展。选择权在你自己手上,你可以选择回老家躺平或者进互联网公司内卷,如果选择后者的话,我的建议还是尽早占下坑位,因为唯一不变的是变化,你永远不知道三年后是什么样子。 - -## 祝福 - -惟愿诸君,前程似锦! - - +My learning path roughly went as follows: Java SE Basics -> MySQL -> Java Web (mainly including JDBC, Servlet, JSP, etc.) -> SSM (at that time, Spring Boot had already emerged, but I felt it was difficult to learn Spring Boot without a foundation in SSM, so I learned SSM first) -> Spring Boot -> Spring Cloud (although I learned Spring Cloud, I lacked practical project experience and didn't know how to use it; I diff --git a/docs/high-quality-technical-articles/interview/technical-preliminary-preparation.md b/docs/high-quality-technical-articles/interview/technical-preliminary-preparation.md index ae4e95b1818..412eb05eb65 100644 --- a/docs/high-quality-technical-articles/interview/technical-preliminary-preparation.md +++ b/docs/high-quality-technical-articles/interview/technical-preliminary-preparation.md @@ -1,216 +1,81 @@ --- -title: 从面试官和候选者的角度谈如何准备技术初试 -category: 技术文章精选集 -author: 琴水玉 +title: How to Prepare for Technical Interviews from the Perspectives of Interviewers and Candidates +category: Selected Technical Articles +author: Qinshi Yu tag: - - 面试 + - Interview --- -> **推荐语**:从面试官和面试者两个角度探讨了技术面试!非常不错! +> **Recommendation**: This article explores technical interviews from both the interviewer and interviewee perspectives! Very insightful! > -> **内容概览:** +> **Content Overview:** > -> - 通过技术基础考察候选者,才能考察到候选者的真实技术实力:技术深度和广度。 -> - 实战与理论结合。比如,候选人叙述 JVM 内存模型布局之后,可以接着问:有哪些原因可能会导致 OOM , 有哪些预防措施? 你是否遇到过内存泄露的问题? 如何排查和解决这类问题? -> - 项目经历考察不宜超过两个。因为要深入考察一个项目的详情,所占用的时间还是比较大的。一般来说,会让候选人挑选一个他或她觉得最有收获的/最有挑战的/印象最深刻的/自己觉得特有意思的项目。然后围绕这个项目进行发问。通常是从项目背景出发,考察项目的技术栈、项目模块及交互的整体理解、项目中遇到的有挑战性的技术问题及解决方案、排查和解决问题、代码可维护性问题、工程质量保障等。 -> - 多问少说,让候选者多表现。根据候选者的回答适当地引导或递进或横向移动。 -> -> **原文地址:** - -## 考察目标和思路 - -首先明确,技术初试的考察目标: - -- 候选人的技术基础; -- 候选人解决问题的思路和能力。 - -技术基础是基石(冰山之下的东西),占七分, 解决问题的思路和能力是落地(冰山之上露出的部分),占三分。 业务和技术基础考察,三七开。 - -## 技术基础考察 - -### 为什么要考察技术基础? - -程序员最重要的两种技术思维能力,是逻辑思维能力和抽象设计能力。逻辑思维能力是基础,抽象设计能力是高阶。 考察技术基础,正好可以同时考察这两种思维能力。能不能理解基础技术概念及关联,是考察逻辑思维能力;能不能把业务问题抽象成技术问题并合理的组织映射,是考察抽象设计能力。 - -绝大部分业务问题,都可以抽象成技术问题。在某种意义上,业务问题只是技术问题的领域化表述。 - -因此,**通过技术基础考察候选者,才能考察到候选者的真实技术实力:技术深度和广度。** - -### 技术基础怎么考察? - -技术基础怎么考察?通过有效的多角度的发问模式来考察。 - -#### 是什么-为什么 - -是什么考察对概念的基本理解,为什么考察对概念的实现原理。 - -比如:索引是什么? 索引是如何实现的? - -#### 引导-横向发问-深入发问 - -引导性,比如 “你对 Java 同步工具熟悉吗?” 作个试探,得到肯定答复后,可以进一步问:“你熟悉哪些同步工具类?” 了解候选者的广度; - -获取候选者的回答后,可以进一步问:“ 谈谈 `ConcurrentHashMap` 或 `AQS` 的实现原理?” - -一个人在多大程度上把技术原理能讲得清晰,包括思路和细节,说明他对技术的掌握能力有多强。 - -#### 跳跃式/交叉式发问 - -比如:讲到哈希高效查找,可以谈谈哈希一致性算法 。 两者既有关联又有很多不同点。也是一种技术广度的考察方法。 - -#### 总结性发问 - -比如:你在做 XXX 中,获得了哪些可以分享的经验? 考察候选人的归纳总结能力。 - -#### 实战与理论结合 - -比如,候选人叙述 JVM 内存模型布局之后,可以接着问:有哪些原因可能会导致 OOM , 有哪些预防措施? 你是否遇到过内存泄露的问题? 如何排查和解决这类问题? - -比如,候选人有谈到 SQL 优化和索引优化,那就正好谈谈索引的实现原理,如何建立最佳索引? - -再比如,候选人有谈到事务,那就正好谈谈事务实现原理,隔离级别,快照实现等; - -#### 熟悉与不熟悉结合 - -针对候选人简历上写的熟悉的部分,和没有写出的都问下。比如候选人简历上写着:熟悉 JVM 内存模型, 那我就考察下内存管理相关(熟悉部分),再考察下 Java 并发工具类(不确定是否熟悉部分)。 - -#### 死知识与活知识结合 - -比如,查找算法有哪些?顺序查找、二分查找、哈希查找。这些大家通常能说出来,也是“死知识”。 - -这些查找算法各适用于什么场景?在你工作中,有哪些场景用到了哪些查找算法?为什么? 这些是“活知识”。 - -#### 学习或工作中遇到的 - -有时,在学习和工作中遇到的问题,也可以作为面试题。 - -比如,最近在学习《操作系统导论》并发部分,有一章节是如何使数据结构成为线程安全的。这里就有一些可以提问的地方:如何实现一个锁?如何实现一个线程安全的计数器?如何实现一个线程安全的链表?如何实现一个线程安全的 `Map` ?如何提升并发的性能? - -工作中遇到的问题,也可以抽象提炼出来,作为技术基础面试题。 - -#### 技术栈适配度发问 - -如果候选人(简历上所写的)使用的某些技术与本公司的技术栈比较契合,则可以针对这些技术点进行深入提问,考察候选人在这些技术点的掌握程度。如果掌握程度比较好,则技术适配度相对更高一些。 - -当然,这一点并不能作为筛掉那些没有使用该技术栈的候选人的依据。比如本公司使用 `MongoDB` 和 `MySQL`, 而一个候选人没有用过 `Mongodb,` 但使用过 `MySQL`, `Redis`, `ES`, `HBase` 等多种存储系统,那么适配度并不比仅使用过 `MySQL` 和 `MongoDB` 的候选人逊色,因为他所涉及的技术广度更大,可以推断出他有足够能力掌握 `Mongodb`。 - -#### 创造有个性的面试题库 - -每个技术面试官都会有一个面试题库。持续积累面试题库,日常中突然想到的问题,就随手记录下来。 - -## 业务维度考察 - -### 为什么要考察业务维度? - -技术基础考察,容易错过的地方是,候选人的非技术能力特质,比如沟通组织能力、带项目能力、抗压能力、解决实际问题的能力、团队影响力、其它性格特质等。 - -### 为什么不能单考察业务维度? - -因为业务方面通常比较熟悉,可能就直接按照现有方案说出来了,很难考察到候选人的深入理解、横向拓展和归纳总结能力。 - -这一点,建议有针对性地考察下候选人的归纳总结能力:比如, 微服务搭建或开发或维护/保证系统稳定性或性能方面的过程中,你收获了哪些可以分享的经验? - -## 解决问题能力考察 - -仅仅只是技术基础还不够,通常最好结合实际业务,针对他项目里的业务,抽象出技术问题进行考察。 - -解决思路重在层层递进。这一点对于面试官的要求也比较高,兼具良好的倾听能力、技术深度和业务经验。首先要仔细倾听候选人的阐述,找到适当的技术切入点,然后进行发问。如果进不去,那就容易考察失败。 - -### 设计问题 - -- 比如多个机器间共享大量业务对象,这些业务对象之间有些联合字段是重复的,如何去重? -- 如果瞬时有大量请求涌入,如何保证服务器的稳定性? - -### 项目经历 - -项目经历考察不宜超过两个。因为要深入考察一个项目的详情,所占用的时间还是比较大的。 - -一般来说,会让候选人挑选一个他或她觉得最有收获的/最有挑战的/印象最深刻的/自己觉得特有意思的项目。然后围绕这个项目进行发问。通常是从项目背景出发,考察项目的技术栈、项目模块及交互的整体理解、项目中遇到的有挑战性的技术问题及解决方案、排查和解决问题、代码可维护性问题、工程质量保障等。 - -## 面试官如何做好一场面试? - -### 预先准备 - -面试官也需要做一些准备。比如熟悉候选者的技能优势、工作经历等,做一个面试设计。 - -在面试将要开始时,做好面试准备。此外,面试官也需要对公司的一些基本情况有所了解,尤其是公司所使用技术栈、业务全景及方向、工作内容、晋升制度等,这一点技术型候选人问得比较多。 - -### 面试启动 - -一般以候选人自我介绍启动,不过候选人往往会谈得比较散,因此,我会直接提问:谈谈你有哪些优势以及自己觉得可以改进的地方? - -然后以一个相对简单的基础题作为技术提问的开始:你熟悉哪些查找算法?大多数人是能答上顺序查找、二分查找、哈希查找的。 - -### 问题设计 +> - Assessing candidates through technical fundamentals allows for a true evaluation of their technical strength: depth and breadth. +> - Combining practical experience with theory. For example, after a candidate describes the JVM memory model layout, you can follow up with: What could cause an OOM? What preventive measures are there? Have you encountered memory leak issues? How did you troubleshoot and resolve such problems? +> - Limit project experience assessments to no more than two. This is because delving into the details of a project can be time-consuming. Generally, candidates are asked to choose a project they found most rewarding/challenging/memorable/interesting, and questions are centered around that project. Typically, this starts with the project background, assessing the technology stack, overall understanding of project modules and interactions, challenging technical issues encountered and their solutions, troubleshooting and resolving issues, code maintainability, and engineering quality assurance. +> - Ask more and say less, allowing candidates to showcase their abilities. Appropriately guide or progress based on the candidate's responses. -提前阅读候选人简历,从简历中筛选出关键词,根据这些关键词进行有针对性地问题设计。 +**Original Article Link:** -比如候选人简历里提到 `MVVM` ,可以问 `MVVM` 与 `MVC` 的区别; 提到了观察者模式,可以谈谈观察者模式,顺便问问他还熟悉哪些设计模式。 +## Assessment Goals and Approach -### 宽松氛围 +First, clarify the assessment goals of the technical interview: -即使问的问题比较多比较难,也要注意保持宽松氛围。 +- The candidate's technical foundation; +- The candidate's problem-solving thought process and abilities. -在面试前,根据候选人基本信息适当调侃一下,比如一位候选人叫汪奎,那我就说:之前我们团队有位叫袁奎,我们都喊他奎爷。 +Technical foundation is the cornerstone (the part below the iceberg), accounting for seventy percent, while problem-solving thought process and abilities are the visible part (the part above the iceberg), accounting for thirty percent. The assessment of business and technical foundations is a 70-30 split. -在面试过程中,适当提示,或者给出少量自己的看法,也能缓解候选人的紧张情绪。 +## Technical Foundation Assessment -### 学会倾听 +### Why Assess Technical Foundation? -多问少说,让候选者多表现。根据候选者的回答适当地引导或递进或横向移动。 +The two most important technical thinking abilities for programmers are logical thinking and abstract design abilities. Logical thinking is foundational, while abstract design is advanced. Assessing technical foundation allows for the evaluation of both thinking abilities. Understanding basic technical concepts and their relationships assesses logical thinking; the ability to abstract business problems into technical issues and organize mappings reasonably assesses abstract design ability. -引导候选人表现他最优势的一面,让他或她感觉好一些:毕竟一场面试双方都付出了时间和精力,不应该是面试官 Diss 候选人的场合,而应该让彼此有更好的交流。很大可能,你也能从候选人那里学到不少东西。 +Most business problems can be abstracted into technical problems. In a sense, business problems are merely domain-specific expressions of technical issues. -面试这件事,只不过双方的角色和立场有所不同,但并不代表面试官的水平就一定高于候选人。 +Therefore, **assessing candidates through technical fundamentals allows for a true evaluation of their technical strength: depth and breadth.** -### 记录重点 +### How to Assess Technical Foundation? -认真客观地记录候选人的回答,尽可能避免任何主观评价,亦不作任何加工(比如自己给总结一下,总结能力也是候选人的一个特质)。 +How to assess technical foundation? Through effective, multi-faceted questioning patterns. -## 作出判断 +#### What is it - Why -面试过程是一种铺垫,关键的是作出判断。 +"What is it?" assesses basic understanding of concepts, while "Why?" assesses the principles behind the implementation of those concepts. -作出判断最容易陷入误区的是:贪深求全。总希望候选人技术又深入又全面。实际上,这是一种奢望。如果候选人的技术能力又深入又全面,很可能也会面临两种情况: +For example: What is an index? How is an index implemented? -1. 候选人有更好的选择; -2. 候选人在其它方面可能存在不足,比如团队协作方面。 +#### Guiding - Lateral Questioning - In-depth Questioning -一个比较合适的尺度是: +Guiding questions, such as "Are you familiar with Java synchronization tools?" serve as a probe. After receiving a positive response, you can further ask: "Which synchronization tool classes are you familiar with?" to understand the candidate's breadth. -1. 他或她的技术水平能否胜任当前工作; -2. 他或她的技术水平与同组团队成员水平如何; -3. 他或她的技术水平是否与年限相对匹配,是否有潜力胜任更复杂的任务。 +After obtaining the candidate's response, you can further ask: "Can you discuss the implementation principles of `ConcurrentHashMap` or `AQS`?" -**不同年龄看重的东西不一样。** +The extent to which a person can clearly articulate technical principles, including thoughts and details, indicates their mastery of the technology. -对于三年以下的工程师,应当更看重其技术基础,因为这代表着他的未来潜能;同时也考察下他在实际开发中的体现,比如团队协作、业务经验、抗压能力、主动学习的热情和能力等。 +#### Jumping/Cross-Questioning -对于三年以上的工程师,应当更看重其业务经验、解决问题能力,看看他或她是如何分析具体问题,在业务范畴内考察其技术基础的深度和广度。 +For example, when discussing efficient hash lookups, you can talk about hash consistency algorithms. The two are related yet have many differences, serving as a method to assess technical breadth. -如何判断一个候选人的真实技术水平及是否适合所需,这方面,我也在学习中。 +#### Summary Questions -## 给候选人的话 +For example: What experiences have you gained from doing XXX that you can share? This assesses the candidate's ability to summarize and synthesize. -### 关注技术基础 +#### Combining Practical and Theoretical Knowledge -一个常见的疑惑是:开发业务系统的大多数时候,基本不涉及数据结构与算法的设计与实现,为什么要考察 `HashMap` 的实现原理?为什么要学好数据结构与算法、操作系统、网络通信这些基础课程? +For example, after a candidate describes the JVM memory model layout, you can follow up with: What could cause an OOM? What preventive measures are there? Have you encountered memory leak issues? How did you troubleshoot and resolve such problems? -现在我可以给出一个答案了: +If a candidate discusses SQL optimization and index optimization, you can then discuss the implementation principles of indexes and how to establish the best indexes. -- 正如上面所述,绝大多数的业务问题,实际上最终都会映射到基础技术问题上:数据结构与算法的实现、内存管理、并发控制、网络通信等;这些是理解现代互联网大规模程序以及解决程序疑难问题的基石,—— 除非能祝福自己永远都不会遇到疑难问题,永远都只满足于编写 CRUD; -- 这些技术基础正是程序世界里最有趣最激动人心的地方。如果对这些不感兴趣,就很难在这个领域里深入进去,不如及早转行从事其它职业,非技术的世界一直都很精彩广阔(有时我也想多出去走走,不想局限于技术世界); -- 技术基础是程序员的内功,而具体技术则是招式。徒有招式而内功不深,遇到高手(优秀同行从业者的竞争及疑难杂症)容易不堪一击; -- 具备扎实的专业技术基础,能达到的上限更高,未来更有可能胜任复杂的技术问题求解,或者在同样的问题上能够做到更好的方案; -- 人们喜欢跟与自己相似的人合作,牛人倾向于与牛人合作能得到更好的效果;如果一个团队大部分人技术基础比较好,那么进来一个技术基础比较薄弱的人,协作成本会变高;如果你想和牛人一起合作拿到更好的结果,那就要让自己至少在技术基础上能够与牛人搭配的上; -- 在 CRUD 的基础上拓展其它才能也不失为一种好的选择,但这不会是一个真正的程序员的姿态,顶多是有技术基础的产品经理、项目经理、HR、运营、客满等其它岗位人才。这是职业选择的问题,已经超出了考察程序员的范畴。 +If a candidate mentions transactions, you can then discuss the principles of transaction implementation, isolation levels, snapshot implementations, etc. -### 不要在意某个问题回答不上来 +#### Combining Familiar and Unfamiliar Topics -如果面试官问你很多问题,而有些没有回答上来,不要在意。面试官很可能只是在测试你的技术深度和广度,然后判断你是否达到某个水位线。 +Ask about both the familiar parts listed on the candidate's resume and those not mentioned. For example, if the candidate's resume states they are familiar with the JVM memory model, you can assess memory management (the familiar part) and then probe into Java concurrency tool classes (the uncertain part). -重点是:有些问题你答得很有深度,也体现了你的深度思考能力。 +#### Combining Dead Knowledge and Living Knowledge -这一点是我当了技术面试官才领会到的。当然,并不是每位技术面试官都是这么想的,但我觉得这应该是个更合适的方式。 +For example, what search algorithms are there? Sequential search, binary search, hash search. These are typically easy to name and are considered "dead knowledge." - +What scenarios are each of these search algorithms suitable for? In your work, what scenarios have you used which search algorithms? Why? These are "living knowledge diff --git a/docs/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md b/docs/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md index 8aaa1d65aca..3f2aae43f36 100644 --- a/docs/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md +++ b/docs/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md @@ -1,362 +1,362 @@ --- -title: 一位大龄程序员所经历的面试的历炼和思考 -category: 技术文章精选集 -author: 琴水玉 +title: The Trials and Reflections of a Middle-aged Programmer in Interviews +category: Selected Technical Articles +author: Qin Shuiyu tag: - - 面试 + - Interview --- -> **推荐语**:本文的作者,今年 36 岁,已有 8 年 JAVA 开发经验。在阿里云三年半,有赞四年半,已是标准的大龄程序员了。在这篇文章中,作者给出了一些关于面试和个人能力提升的一些小建议,非常实用! +> **Recommendation**: The author of this article is 36 years old this year and has 8 years of experience in JAVA development. With three and a half years at Alibaba Cloud and four and a half years at Youzan, he is a typical middle-aged programmer. In this article, the author provides some practical advice regarding interviews and personal skills enhancement! > -> **内容概览**: +> **Content Overview**: > -> 1. 个人介绍,是对自己的一个更为清晰、深入和全面的认识契机。 -> 2. 简历是充分展示自己的浓缩精华,也是重新审视自己和过往经历的契机。不仅仅是简要介绍技能和经验,更要最大程度凸显自己的优势领域(差异化)。 -> 3. 我个人是不赞成海投的,而倾向于定向投。找准方向投,虽然目标更少,但更有效率。 -> 4. 技术探索,一定要先理解原理。原理不懂,就会浮于表层,不能真正掌握它。技术原理探究要掌握到什么程度?数据结构与算法设计、考量因素、技术机制、优化思路。要在脑中回放,直到一切细节而清晰可见。如果能够清晰有条理地表述出来,就更好了。技术原理探究,一定要看源码。看了源码与没看源码是有区别的。没看源码,虽然说得出来,但终是隔了一层纸;看了源码,才捅破了那层纸,有了自己的理解,也就能说得更加有底气了。当然,也可能是我缺乏演戏的本领。 -> 5. 要善于从失败中学习。正是在杭州四个月空档期的持续学习、思考、积累和提炼,以及面试失败的反思、不断调整对策、完善准备、改善原有的短板,采取更为合理的方式,才在回武汉的短短两个周内拿到比较满意的 offer 。 -> 6. 面试是通过沟通来理解双方的过程。面试中的问题,千变万化,但有一些问题是需要提前准备好的。 +> 1. Personal introduction is an opportunity for a clearer, deeper, and comprehensive understanding of oneself. +> 1. A resume is a condensed essence of showcasing oneself and a chance to re-evaluate oneself and past experiences. It's not just about a brief introduction of skills and experiences, but also about highlighting one’s areas of advantage (differentiation) to the maximum extent. +> 1. Personally, I do not support mass job applications but prefer targeted applications. Finding the right direction to apply may result in fewer targets, but it is more efficient. +> 1. Technical exploration must start with understanding the principles. If you do not understand the principles, you will remain superficial and unable to truly grasp it. To what extent should one understand the technical principles? Data structure and algorithm design, considerations, technical mechanisms, optimization ideas. One must replay it in their mind until all details are clear. The ability to present it clearly and coherently is even better. Exploring technical principles must include reading source code. There is a difference between having read the source code and not having done so. If you haven't read the source code, you may articulate it, but there's still a layer of separation; having read the source code breaks through that layer, leads to personal understanding, and allows one to express it more confidently. Of course, it might be that I lack the ability to act. +> 1. Learn to be good at learning from failures. It was during a four-month learning, thinking, accumulating, and refining period in Hangzhou, along with reflections on interview failures, constant adjustments of strategies, improving preparations, and enhancing previous shortcomings that I managed to receive a satisfactory offer within just two weeks of returning to Wuhan. +> 1. An interview is a process of understanding both parties through communication. The questions during an interview can vary widely, but there are certain questions that need to be prepared in advance. > -> **原文地址**: +> **Original Article Link**: -从每一段经历中学习,在每一件事情中修行。善于从挫折中学习。 +Learn from every experience, and practice in everything. Be good at learning from setbacks. -## 引子 +## Prologue -我今年 36 岁,已有 8 年 JAVA 开发经验。在阿里云三年半,有赞四年半,已是标准的大龄程序员了。 +I am 36 years old this year with 8 years of JAVA development experience. I spent three and a half years at Alibaba Cloud and four and a half years at Youzan, making me a typical middle-aged programmer. -在多年的读书、学习和思考中,我的价值观、人生观和世界观也逐步塑造成型。我意识到自己的志趣在于做教育文化方面,因此在半冲动之下,8 月份下旬,裸辞去找工作了。有限理性难以阻挡冲动的个性。不建议裸辞,做事应该有规划、科学合理。 +Through many years of reading, learning, and thinking, my values, outlook on life, and worldview have gradually taken shape. I realized that my interest lies in education and culture, so impulsively, in late August, I quit my job to find new opportunities. Limited rationality could not restrain my impulsive nature. I do not recommend quitting without a plan; there should be a scientific and rational approach to work. -尽管我最初认为自己“有理想有目标有意愿有能力”,找一份教育开发的工作应该不难,但事实上我还是过于乐观了。现实很快给我泼了一瓢瓢冷水。我屡战屡败,又屡败屡战。惊讶地发现自己还有这个韧性。面试是一项历炼,如果没有被失败击倒,那么从中会生长出一份韧性,这种韧性能让人走得更远。谁没有经历过失败的历练呢?失败是最伟大的导师了,如果你愿意跟他学一学的话。 +Although I initially believed that I “had ideals, goals, willingness, and capability,” finding a job in educational development should not be too difficult, but in reality, I was overly optimistic. The reality quickly showered me with cold water. I faced repeated defeats but continued to fight. I was surprised to find that I had this resilience. The interview process is a trial; if you are not defeated by failure, a resilience will grow from it, allowing one to go further. Who hasn’t gone through the trials of failure? Failure is the greatest teacher if you are willing to learn from it. -在面试的过程中,我很快发现自己的劣势: +During the interview process, I quickly discovered my disadvantages: -- 投入精力做业务,技术深度不够,对原理的理解局限于较浅的层次; -- 视野不够开阔,局限于自己所做的订单业务线,对其它关联业务线(比如商品、营销、支付等)了解不够; -- 思维不够开阔,大部分时间投入在开发和测试上,对运维、产品、业务、商业层面思考都思考不多; -- 缺乏管理经验,年龄偏大;这两项劣势我一度低估,但逐渐凸显出来,甚至让我一度不自信,但最终我还是走出来了。 +- I invested energy into business, but my technical depth was insufficient, and my understanding of principles was limited to a shallow level; +- My perspective was not broad enough, limited to the order business line I worked on, and I did not understand other associated business lines (like products, marketing, payment, etc.) well; +- My thinking was not expansive; I spent most of my time on development and testing, with little thought on operations, products, business, or commercial aspects; +- I lacked management experience and was older; I once underestimated these two disadvantages, but they gradually became evident, leading to a loss of confidence, yet I ultimately pulled through. -但我也有自己的优势。职业竞争的基本法则是稀缺性和差异化。能够解决大型项目的架构设计和攻克技术难题,精通某个高端技术领域是稀缺性体现;而能够做事能做到缜密周全精细化,有高并发大流量系统开发经验,则是差异性体现。稀缺性是上策,差异化是中策,而降格以求就是下策了。 +But I also had my advantages. The fundamental law of professional competition is scarcity and differentiation. The ability to solve large project architecture designs and tackle technical challenges reflects scarcity; being able to do meticulous, thorough work, along with experience in high-concurrency, high-traffic system development reflects differentiation. Scarcity is the best strategy, differentiation is a middle-ground strategy, while compromising is the worst strategy. -我缺乏稀缺性优势,但还有一点差异化优势: +I may lack the advantage of scarcity, but I do have some points of differentiation: -- 对每一份工作都很踏实,时间均在 3 年 - 5 年之间,有一点大厂光环,能获得更多面试机会(虽然不一定能面上); -- 坚持写博客,孜孜不倦地追求软件开发的“道”,时常思考记录开发中遇到的问题及解决方案; -- 做事认真严谨,能够从整体分析和思考问题,也很注重基础提升; -- 对工程质量、性能优化、稳定性建设、业务配置化设计有实践经验; -- 大流量微服务系统的长期开发维护经验。 +- I approach every job diligently, typically spending 3 to 5 years in each position, which gives me some prestige from large companies, leading to more interview opportunities (though it doesn't guarantee I will pass); +- I persist in writing blogs, tirelessly pursuing the "way" of software development, often reflecting on and recording problems and solutions encountered during development; +- I work seriously and rigorously, able to analyze and think about problems from a holistic perspective, while also paying attention to foundational improvements; +- I have practical experience in engineering quality, performance optimization, stability construction, and business configuration design; +- I have long-term development and maintenance experience in high-traffic microservice systems. -我投出简历的公司并不多。在不多的面试中,我逐渐意识到网上的“斩获几十家大厂 offer”的说法并不可信。理由如下: +I didn’t send out many resumes. In the few interviews I had, I gradually realized that the claim of “landing offers from dozens of major companies” online is not credible. Here are the reasons: -- 如果能真斩获大量大厂 offer ,面试的级别很大概率是初级工程师。要知道面试 4 年以上的工程师,面试的深度和广度令人发指,从基础的算法、到各种中间件的原理机制到实际运维架构,无所不包,真个是沉浸在“技术的海洋”,除非一个人的背景和实力非常强大,平时也做了非常深且广的沉淀; -- 一个背景和实力非常强大的人,是不会有兴趣去投入这么多精力去面各种公司,仅仅是为了吹嘘自己有多能耐;实力越强的人,他会有自己的选择逻辑,投的简历会更定向精准。话说,他为什么不花更多精力投入在那些能够让他有最大化收益的优秀企业呢? -- 培训机构做的广告。因为他们最清楚新手需要的是信心,哪怕是伪装出来的信心。 +- If one truly landed a large number of offers, the interview level is likely that of a junior engineer. To interview for a position as a senior engineer after over four years of experience involves extreme depth and breadth, encompassing everything from basic algorithms to various middleware principles and mechanisms, as well as practical operation architecture—it's an immersion in the “ocean of technology.” Unless a person's background and strength are very strong, and they have also done very deep and broad accumulation; +- A person with a strong background and strength wouldn't be interested in spending so much energy interviewing various companies just to boast about their capabilities; stronger individuals will have their own logic for choosing, and their resumes will be more targeted. Why wouldn’t they invest more energy in excellent companies that maximize their benefits? +- Advertisements by training institutions. They know very well that what new people need is confidence, even if it’s a fabricated confidence. -好了,闲话不多说了。我讲讲自己在面试中所经受的历练和思考吧。 +Alright, enough of the small talk. Let me talk about what I have undergone and reflected upon in interviews. -## 准备工作 +## Preparation -人生或许很长,但面试的时间很短,最长不过一小时或一个半小时。别人如何在短短一小时内能够更清晰地认识长达三十多年的你呢?这就需要你做大量细致的准备工作了。在某种程度上,面试与舞蹈有异曲同工之妙:台上五分钟,台下十年功。 +Life may be long, but the interview time is short, at most one hour or an hour and a half. How can others gain a clearer understanding of you, someone with over thirty years of life experience, in just one hour? This requires you to do a lot of detailed preparatory work. To some extent, interviews share similarities with dancing: five minutes on stage requires ten years of practice offstage. -准备工作主要包括简历准备、个人介绍、公司了解、技术探索、表述能力、常见问题、中高端职位、好的心态。准备工作是对自身和对外部世界的一次全面深入的重新认知。 +The preparation work mainly includes resume preparation, personal introduction, understanding of the company, technical exploration, communication skills, common questions, mid-senior level positions, and maintaining a positive mindset. This preparation is a comprehensive and in-depth re-evaluation of oneself and the external world. -初期,我以为自己准备很充分,简历改改就完事了。随着一次次受挫,才发现自己的准备很不充分。在现在的我看来,准备七分,应变三分。准备,就是要知己知彼,知道对方会问哪些问题(通常是系统/项目/技术的深度和广度)、自己应当如何作答;应变,就是当自己遇到不会、不懂、不知道的问题时,如何合理地展示自己的解决思路,以及根据面试中答不上来的问题查漏补缺,夯实基础。 +Initially, I thought I was well-prepared, simply tweaking my old resume. But with repeated setbacks, I discovered my preparation was insufficient. Now I believe that preparation is 70%, adaptability is 30%. Preparation means knowing oneself and the opponent, being aware of what questions will be asked (usually regarding system/project/technology depth and breadth), and understanding how to respond; adaptability involves reasonably demonstrating one’s problem-solving thoughts when faced with questions that are unknown or unclear, as well as identifying gaps based on unanswered questions during the interview and reinforcing the fundamentals. -这个过程,实际上也是学习的过程。持续的反思和提炼、学习新的内容、重新认识自己和过往经历等。 +This process is essentially a learning journey—continuous reflection and refinement, learning new content, and reevaluating oneself and past experiences. -### 简历准备 +### Resume Preparation -最开始,我做得比较简单。把以前的简历拿出来,添加上新的工作经历,略作修改,但整体上模板基本不变。 +At first, I kept it rather simple. I took out my previous resume, added new work experiences, made slight modifications, yet the overall template remained largely unchanged. -在基本面上,我做的是较为细致的,诚实地写上了自己擅长和熟悉的技能和经验经历,排版也尽力做得整洁美观(学过一些 UI 设计)。不浮夸也不故作谦虚。 +On a basic level, I did it relatively carefully, honestly listing my familiar and proficient skills and experiences, and I tried my best to make it neat and visually appealing (having studied some UI design). Not overly extravagant or pretentious. -在扩展面上,我做的还是不够的。有一天,一位猎头打电话给我,问:“你最大的优势是什么?”。我顿时说不上来。当时也未多加思考。在后续面试屡遭失败之后,一度有些不自信之后,我开始仔细思考自己的优势来。然后将“对工程质量、性能优化、稳定性建设、业务配置化设计有深入思考和实践经验”写在了“技能素养”栏的第一行,因为这确实是我所做过的、最实在且脚踏实地的且具备概括性的。 +However, on the expansion level, I wasn't doing enough. One day, a headhunter called and asked me, “What is your greatest advantage?” I was momentarily at a loss for words. At that time, I didn't think much about it. After experiencing failures in subsequent interviews and feeling a bit insecure, I began to think carefully about my advantages. I then wrote “in-depth thought and practical experience in engineering quality, performance optimization, stability construction, and business configuration design” at the top of the "Skills and Qualifications" section, as this truly reflected what I had done, which was practical, grounded, and summarizable. -有时,简历内容的编排顺序也很重要。之前,我把掌握的语言及技术写在前面,而“项目管理能力和团队影响力”之类的写在后面。但投年糕妈妈之后,未有面试直接被拉到不合适里面,受到了刺激,我意识到或许是对方觉得我管理经验不足。因此,刻意将“项目管理能力和团队影响力”提到了前面,表示自己是重视管理方面的,不过,投过新的简历之后,没有回应。我意识到,这样的编排顺序可能会让人误解我是管理能力偏重的(事实上有一位 HR 问我是不是还在写代码),但实际上管理方面我是欠缺的,最后,我还是调回了原来的顺序,凸出自己“工程师的本色”。后面,我又做了一些语句的编排上的修改。 +Sometimes, the order of the content in the resume is also very important. Previously, I listed the languages and technologies I mastered at the beginning, while placing “project management skills and team influence” at the end. However, after applying to Nian Gao Mama, I didn’t receive an interview and was triggered; I realized that the company might have considered my management experience insufficient. Therefore, I deliberately moved “project management skills and team influence” to the forefront, indicating that I valued management aspects. However, upon sending out the new resume, I received no response. I realized that such a rearrangement might mislead people into thinking I leaned towards management skills (in fact, one HR even asked me if I was still writing code). But in reality, I lacked in management, so eventually, I reverted to the original order, highlighting my “essence as an engineer.” Subsequently, I made further modifications to the phrasing. -随着面试的进展,有时,也会发现自己的简历上写得不够或者以前做得不够的地方。比如,在订单导出这段经历里,我只是写了大幅提升性能和稳定性,显得定性描述化,因此,我添加了一些量化的东西(2w 阻塞 => 300w+,1w/1min)作为证实;比如,8 月份离职,到 12 月份面试的时候,有一段空档期,有些企业会问到这个。因此,我索性加了一句话,说明这段时间我在干些啥;比如,代表性系统和项目,每一个系统和项目的价值和意义(不一定写在上面,但是心里要有数)。功夫要下足。 +As the interviews progressed, I sometimes found my resume lacking or that I hadn’t previously elaborated enough. For example, in the experience section regarding order exports, I only wrote about a significant improvement in performance and stability, making it seem qualitative. Therefore, I added some quantitative elements (2w blocks => 300w+, 1w/1min) for validation; for instance, after leaving my job in August and interviewing in December, there was a gap period, and some companies would inquire about it. Consequently, I added a statement explaining what I had been doing during this time; for example, representative systems and projects, knowing the value and significance of each system/project (though not necessarily needing to write it down, it’s important to have a clear understanding internally). One must put in the effort. -再比如,我很详细地写了有赞的工作经历及经验,但阿里云的那段基本没动。而有些企业对这段经历更感兴趣,我却觉得没太多可说的,留在脑海里的只有少量印象深刻的东西,以及一些博客文章的记录,相比这段工作经历来说显得太单薄。这里实质上不是简历的问题,而是过往经历复盘的问题。建议,在每个项目结束后,都要写个自我复盘。避免时间将这些可贵的经历冲淡。 +For instance, I detailed my work experiences and knowledge at Youzan, but the section for Alibaba Cloud remained largely untouched. However, some companies were more interested in that experience, while I felt there wasn’t much to say, and only a few memories remained vividly, along with some records of blog articles which seemed too scant compared to that work experience. Here, it wasn’t actually a resume issue, but a matter of retrospection regarding past experiences. It is advisable to write a self-review after each project ends, to avoid allowing time to dilute these valuable experiences. -每个人其实都有很多可说的东西,但记录下来的又有多少呢?值得谈道的有多少呢?过往不努力,面试徒伤悲。 +Everyone actually has a lot to talk about, but how much is genuinely recorded? How much is worth discussing? Failing to put in effort in the past will lead to suffering in the interview. -**简历更新的心得**: +**Insights on Updating the Resume**: -- 简历是充分展示自己的浓缩精华,也是重新审视自己和过往经历的契机; -- 不仅仅是简要介绍技能和经验,更要最大程度凸显自己的优势领域(差异化); -- 增强工作经历的表述,凸显贡献,赢得别人的认可; -- 复盘并记录每一个项目中的收获,为跳槽和面试打下好的铺垫。 +- A resume is a concentrated essence for showcasing oneself and an opportunity to re-evaluate oneself and past experiences; +- Not only should it briefly introduce skills and experiences, but it should also highlight one’s advantageous areas (differentiation) to the maximum extent; +- Enhance the description of work experience to emphasize contributions and gain recognition from others; +- Reflect and document the gains from every project to lay a solid foundation for job-hopping and interviews. -### 个人介绍 +### Personal Introduction -面试前通常会要求做个简要的个人介绍。个人介绍通常作为进入面试的前奏曲和缓冲阶段,缓和下紧张气氛。 +Interviews usually require a brief personal introduction. The personal introduction often serves as a prelude and buffer stage for entering the interview, easing the tension. -我最开始的个人介绍,个性啊业余生活啊工作经历啊志趣啊等等,似乎不知道该说些什么。实际上,个人介绍是一个充分展示自己的主页。主页应当让自己最最核心的优势一目了然(需要挖掘自己的经历并仔细提炼)。我现在的个人介绍一般会包括:个性(比如偏安静)、做事风格(工作认真严谨、注重质量、善于整体思考)、最大优势(owner 意识、执行力、工程把控能力)、工作经历简述(在每个公司的工作负责什么、贡献了什么、收获了什么)。个人介绍简明扼要,无需赘言。 +Initially, my personal introductions covered aspects like personality, hobbies, work experience, aspirations, etc., but I seemed unsure of what to say. In reality, the personal introduction is a chance to fully showcase oneself. The introduction should clearly highlight one's core advantages (it requires excavating one’s experiences and refining them). My current personal introduction generally includes: personality (e.g., relatively quiet), working style (diligent and meticulous, with an emphasis on quality and holistic thinking), greatest advantage (owner mentality, execution capability, engineering control capability), and a brief summary of work experience (responsibilities at each company, contributions, and gains). The personal introduction should be concise and to the point. -个人介绍,是对自己的一个更为清晰、深入和全面的认识契机。 +The personal introduction is an opportunity for a clearer, deeper, and comprehensive understanding of oneself. -### 公司了解 +### Understanding the Company -很多人可能跟我一样,对公司业务了解甚少,就直接投出去了。这样其实是不合理的。首先,我个人是不赞成海投的,而倾向于定向投。找准方向投,虽然目标更少,但更有效率。这跟租房一样,我一般在豆瓣上租房,虽然目标源少,但逮着一个就是好运。 +Many people, like me, may know little about the company’s business yet directly submit applications. This is actually unreasonable. I personally do not support mass applications but prefer targeted ones. Finding the right direction is more efficient, even if it results in fewer targets. It’s like renting a house—I usually look for places on Douban. Although there are fewer sources, finding the right one is a stroke of luck. -投一家公司,是因为这家公司符合意向,值得争取,而不是因为这是一家公司。就像找对象,不是为了找一个女人。要确定这家公司是否符合意向,就应当多去了解这家公司:主营业务、未来发展及规划、所在行业及地位、财务状况、业界及网络评价等。 +Choosing a company to apply to should be because it aligns with your intentions and is worth pursuing, not just because it is a company. Just like seeking a partner, it’s not just about finding a woman. To determine whether a company meets your intentions, one should understand more about the company: its main business, future development and plans, its industry and position, financial status, and industry reputation and evaluations. -在面试的过程中适当谈到公司的业务及思考,是可加分项。亦可用于“你有什么想问的?”的提问。 +Bringing in discussions about the company’s business and thoughts during the interview can be a plus point. This can also be used in the “Do you have any questions?” area. -### 技术探索 +### Technical Exploration -技术能力是一个技术人的基本素养。因此,我觉得,无论未来做什么工作,技术能力过硬,总归是最不可或缺的不可忽视的。 +Technical capability is a fundamental quality for a technical person. Therefore, I believe that regardless of the future job, strong technical competency is ultimately indispensable and cannot be overlooked. -原理和设计思想是软件技术中最为精髓的东西。一般软件技术可以分为两个方面: +Principles and design ideas are the most essential aspects of software technology. Generally, software technology can be divided into two aspects: -- 原理:事物如何工作的基本规律和流程; -- 架构:如何组织大规模逻辑的艺术。 +- Principles: the basic laws and processes of how things work; +- Architecture: the art of organizing large-scale logic. -**技术探索,一定要先理解原理。原理不懂,就会浮于表层,不能真正掌握它。技术原理探究要掌握到什么程度?数据结构与算法设计、考量因素、技术机制、优化思路。要在脑中回放,直到一切细节而清晰可见。如果能够清晰有条理地表述出来,就更好了。** +**Technical exploration must start with understanding principles. If you do not understand the principles, you will remain superficial and cannot truly grasp it. To what extent should one master technical principles? Data structure and algorithm design, factors to consider, technical mechanisms, optimization ideas. One must replay them in their mind until all details are clear. Being able to articulate it clearly and coherently is even better.** -**技术原理探究,一定要看源码。看了源码与没看源码是有区别的。没看源码,虽然说得出来,但终是隔了一层纸;看了源码,才捅破了那层纸,有了自己的理解,也就能说得更加有底气了。当然,也可能是我缺乏演戏的本领。** +**Exploring technical principles includes examining source code. There is a distinct difference between having read the source code and not having done so. If you haven’t read the source code, you might be able to articulate, but there’s still a layer of separation; having read the source code breaks through that layer, allowing for personal understanding, and one can articulate with greater confidence. Of course, this may also reflect a lack of acting ability on my part.** -我个人不太赞成刷题式面试。虽然刷题确实是进厂的捷径,但也有缺点: +I personally do not strongly support question-centric interviews. While practicing questions can be a shortcut for entering the industry, it comes with disadvantages: -- 它依然是别人的知识体系,而不是自己总结的知识体系; -- 技术探究是为了未来的工作准备,而不是为了应对一时之需,否则即使进去了还是会处于麻痹状态。 +- It remains someone else's knowledge system rather than one's own summarized knowledge system; +- Technical exploration is to prepare for future work instead of responding to immediate needs; otherwise, even if you get in, you might still remain in a state of numbness. -经过系统的整理,我逐步形成了适合自己的技术体系结构:[“互联网应用服务端的常用技术思想与机制纲要”](https://www.cnblogs.com/lovesqcc/p/13633409.html) 。在这个基础上,再博采众长,看看面试题进行自测和查漏补缺,是更恰当的方式。我会在这个体系上深耕细作。 +Through systematic organization, I gradually formed a technical system structure suitable for myself: [“Common Technical Thoughts and Mechanisms in Internet Application Server”](https://www.cnblogs.com/lovesqcc/p/13633409.html). On this basis, reviewing interview questions for self-testing and addressing gaps is a more appropriate approach. I will delve deeply into this system. -### 表述能力 +### Communication Skills -目前,绝大多数企业的主要面试形式是通过口头沟通进行的,少部分企业可能有笔试或机试。口头沟通的形式是有其局限性的。对表述能力的要求比较高,而对专业能力的凸显并不明显。一个人掌握的专业和经验的深度和广度,很难通过几分钟的表述呈现出来。往往深度和广度越大,反而越难表述。而技术人员往往疏于表达。 +Currently, the main form of interviews in most companies is conducted via oral communication, with only a small fraction having written or online tests. The limitations of oral communication are evident. The demand for communication skills is quite high while the emphasis on showcasing professional capabilities is not as prominent. It is challenging to present the depth and breadth of a person’s expertise and experiences through a few minutes of expression. Often, the greater the depth and breadth, the more difficult it is to articulate. Technical staff often neglect expression. -我平时写得多说得少,说起来不利索。有时没讲清楚背景,就直接展开,兼之啰嗦、跳跃和回旋往复(这种方式可能更适合写小说),让面试官有时摸不着头脑。表述的条理性和清晰性也是很重要的。不妨自己测试一下:Dubbo 的架构设计是怎样的? Redis 的持久化机制是怎样的?然后自己回答试试看。 +I usually write more and speak less, and when I speak, I am not fluent. Sometimes I would dive in without clarifying the background and instead become verbose, jumping around in my exposition (this approach might be more suitable for writing novels), which leaves interviewers confused at times. The coherence and clarity of expression are crucial. You might as well test yourself: What is the architectural design of Dubbo? What is the persistence mechanism of Redis? Then try answering it. -表述能力的基本法则: +Basic rules for communication skills: -- 先总后分,先整体后局部; -- 先说基本思路,然后说优化; -- 体现互动。先综述,然后向面试官询问要听哪方面,再分述。避免自己一脑瓜子倾倒出来,让面试官猝不及防;系统设计的场景题,多问一些要求,比如时间要求、空间要求、要支持多大数据量或并发量、是否要考虑某些情况等。 +- Start with the main point and then detail; first the overall view, then the specifics; +- Present the basic idea before discussing optimizations; +- Reflect interaction. Begin with an overview, then ask the interviewer what they'd like to hear about, followed by detailed exposition. Avoid dumping all your thoughts at once, catching the interviewer off guard; for scenario-based system design questions, ask clarifying requirements, such as time constraints, space requirements, data volume or concurrency considerations, and whether to consider specific situations. -### 常见问题 +### Common Questions -面试是通过沟通来理解双方的过程。面试中的问题,千变万化,但有一些问题是需要提前准备好的。 +The interview is a process of understanding both parties through communication. The questions during interviews can vary widely, but there are several questions that need to be prepared in advance. -比如“灵魂 N 问”: +For example, the “soul questions”: -- 你为什么从 XXX 离职? -- 你的期望薪资是多少? -- 你有一段空档期,能解释下怎么回事么? -- 你的职业规划是怎样的? +- Why did you leave XXX? +- What is your expected salary? +- You have a gap period; can you explain what happened? +- What are your career plans? -高频技术问题: +High-frequency technical questions: -- 基础:数据结构与算法、网络; -- 微服务:技术体系、组件、基础设施等; -- Dubbo:Dubbo 整体架构、扩展机制、服务暴露、引用、调用、优雅停机等; -- MySQL:索引与事务的实现原理、SQL 优化、分库分表; -- Redis : 数据结构、缓存、分布式锁、持久化机制、复制机制; -- 分布式:分布式事务、一致性问题; -- 消息中间件:原理、对比; -- 架构:架构设计方法、架构经验、设计模式; -- 性能优化:JVM、GC、应用层面的性能优化; -- 并发基础:ConcurrentHashMap, AQS, CAS,线程池等; -- 高并发:IO 多路复用;缓存问题及方案; -- 稳定性:稳定性的思想及经验; -- 生产问题:工具及排查方法。 +- Basics: Data structures and algorithms, networks; +- Microservices: Technical systems, components, infrastructure, etc.; +- Dubbo: Overall architecture, extension mechanisms, service exposure, referencing, calling, graceful shutdown, etc.; +- MySQL: Indexing and transaction implementation principles, SQL optimization, partitioning; +- Redis: Data structures, caching, distributed locks, persistence mechanisms, replication mechanisms; +- Distributed: Distributed transactions, consistency issues; +- Message middleware: Principles, comparisons; +- Architecture: Architectural design methodologies, architecture experience, design patterns; +- Performance optimization: JVM, GC, application-level performance optimization; +- Concurrency basics: ConcurrentHashMap, AQS, CAS, thread pools, etc.; +- High concurrency: IO multiplexing; cache issues and solutions; +- Stability: Thoughts and experiences on stability; +- Production issues: Tools and troubleshooting methods. -### 中高端职位 +### Mid-Senior Level Positions -说起来,我这人可能有点不太自信。我是怀着“踏实做一个工程师”的思想投简历的。 +It might seem that I am not particularly confident. I apply with the mindset of “being a solid engineer.” -对于大龄程序员,企业的期望更高。我的每一份“高级工程师”投递,自动被转换为“技术专家”或“架构师”。无力反驳,倍感压力。面试中高端职位,需要更多准备: +For middle-aged programmers, companies have higher expectations. Each of my applications for “Senior Engineer” positions automatically translates to “Technical Expert” or “Architect.” I feel the pressure with no way to refute it. Interviews for mid-senior level positions require more preparation: -- 你有带团队经历吗? -- 在你 X 年的工作经历中,有多少时间用于架构设计? -- 架构过程是怎样的?你有哪些架构设计思想或方法论? +- Do you have experience leading a team? +- How much of your X years of working experience has been spent on architecture design? +- What does the architecture process look like? What architectural design ideas or methodologies do you have? -如果不作准备,就被一下子问懵,乱了阵脚。实际上,我或许还是存着侥幸心理把“技术专家”和“架构师”岗位当做“高工”来面试的,也就无一不遭遇失败了。显然,我把次序弄反了:应当以“技术专家”和“架构师”的规格来面试高级工程师。 +If you are unprepared and suddenly thrown such questions, it would be easy to get flustered. In reality, I may have harbored a bit of luck, treating “Technical Expert” and “Architect” positions as “Senior Engineer” interviews, which led to a series of failures. Clearly, I reversed the order: I should be interviewing for senior engineer positions with the parameters of a “Technical Expert” or “Architect.” -好吧,那就迎难而上吧!我不是惧怕挑战的人。 +Well then, let’s embrace the challenge! I am not one to shy away from challenges. -此外,“技术专家”和“架构师”职位应当至少留一天的时间来准备。已经有丰富经验的技术专家和架构师可以忽略。 +Additionally, when preparing for “Technical Expert” and “Architect” roles, one should allocate at least a day for preparation. Those already possessing rich experiences may skip this. -### 好的心态 +### Good Mindset -保持好的心态也尤为重要。我经历了“乐观-不自信-重拾信心”的心态变化过程。 +Maintaining a good state of mind is also particularly important. I have gone through a process of mental changes: “optimism - insecurity - regaining confidence.” -很长一段时间,由于“求成心切”,生怕某个技术问题回答不上来搞砸,因此小心谨慎,略显紧张,结果已经梳理好的往往说不清楚或者说得不够有条理。冲着“拿 offer ”的心态去面试,真的很难受,会觉得每场面试都很被动那么难过,甚至有点想要“降格以求”。 +For a long time, due to “desire for results,” I was cautious and somewhat nervous, fearing that I would fail to answer a technical question. As a result, I often struggled to articulate clearly or present structured thoughts. Approaching interviews with the mindset of “securing an offer” was indeed uncomfortable, making each interview feel quite passive and leading me to consider lowering my standards. -有时,我在想:咋就混成这个样子了呢?按理来说,这个时候我应该有能力去追求自己喜爱的事业了啊!还是平时有点松懈了,视野狭窄,积累不够,导致今天的不利处境。 +At times, I wondered: How did I end up like this? By rights, at this time, I should have been capable of pursuing my beloved career! Perhaps I had been a bit lax previously, with a narrow vision and insufficient accumulation, leading to my current unfavorable situation. -我是一个守时的人,也希望对方尽可能守时。杭州的面试官中,基本是守时的,即使迟到也在心理接受范围内,回武汉面试后,节奏就有点被少量企业带偏了。有一两次,我甚至不确定面试官什么时候进入会议。我想,难道这是人才应该受到的“礼待”吗?我有点被轻微冒犯的感觉了。不过我还是“很有涵养地”表示没事。但我始终觉得:面试官迟到,是对人才的不尊重。进入不尊重人才的公司,我是怀有疑虑的。良禽择木而栖,良臣择主而事。难道我能因为此刻的不利处境,而放弃一些基本的原则底线,而屈从于一份不尊重人才的 offer 吗? +I am a punctual person and hope that others will be as well. Most interviewers in Hangzhou were punctual, and even if someone was late, it was within an acceptable psychological range. However, after returning to Wuhan for interviews, the pace seemed to be slightly thrown off by a few companies. There were one or two instances where I wasn't even sure when the interviewer would join the meeting. I wondered, is this the sort of “hospitality” talent should expect? I felt slightly offended. Nevertheless, I politely expressed that it was fine. Yet, I firmly believe: an interviewer being late is a disrespect to talent. I have reservations about entering companies that do not respect talent. A good bird chooses its tree to rest on, a good servant chooses their master to serve. Can I abandon some basic principles and bottom lines and succumb to an offer that disrespects talent simply because of my current unfavorable circumstance? -我意识到:一个人应当用其实力去赢得对方的尊重和赏识,以后的合作才会更顺畅。不若,哪怕惜其无缘,亦不可强留。无论别人怎么存疑,心无旁骛地打磨实力,挖掘自己的才干和优势,终会发出自己的光芒。因此,我的心态顿时转变了:应当专注去沟通,与对方充分认识了解,赢得对方心服的认可,而不是拿到一张入门券,成为干活的工具。 +I realized: One should earn the respect and appreciation of others through their abilities so that future collaborations can proceed more smoothly. Rather than forcing someone to stay, it is better to let them go if things are not meant to be. Regardless of others’ doubts, one should focus on honing their capabilities, uncovering their talents and advantages, and they will ultimately shine. Therefore, my mindset shifted dramatically: I should focus on communicating, fully understanding and knowing the other party, to earn their genuine acknowledgment instead of merely obtaining a ticket to entry, becoming a tool for work. -有一个“石头和玉”的小故事,把自己当做人才,并努力去提升自己,才能获得“人才的礼遇”;把自己当石头贱卖,放松努力,也就只能得到“石头的礼遇”。尽管一个人不一定马上就具备人才的能力,但在自己的内心里,就应当从人才的视角去观察待入职的企业,而不仅仅是为了找一份“赚更多钱”的工作。 +There is a little story about “stones and jade”: Treat yourself as a talent, and strive to improve yourself to gain the “treatment of talents”; Treat yourself as a stone and sell yourself cheap, relax in your efforts, and you will only receive the “treatment of stones.” Although one might not immediately possess the abilities of a talent, in one’s heart, one should start observing potential employers from the perspective of a talent, rather than solely looking for a job that pays “more money.” -此外,焦虑也是不必要的。焦虑的实质是现实与目标的差距。一个人总可以评估目标的合理性及如何达成目标。如果目标过高,则适当调整目标级别;目标可行,则作出合理的决策,并通过持续的努力和恰当的出击来实现目标。决策、努力和出击能力都是可以持续修炼的。 +Moreover, anxiety is unnecessary. Anxiety essentially arises from the gap between reality and goals. One can always assess the rationality of their goals and how to achieve them. If the goals are too high, then appropriately adjust the goal level; if the goals are achievable, then make reasonable decisions and realize the goals through continuous effort and well-timed actions. Decision-making, effort, and action capabilities can all be continually cultivated. -## 面试历炼 +## Interview Experiences -技术人的面试还是更偏重于技术,因此,技术的深度和广度还是要好好准备的。面试官和候选人的处境是不一样的,一个面试官问的只是少量点,但是多个面试官合起来就是一个面。明白这一点,作为面试官的你就不要忘乎所以,以为自己就比候选人厉害。 +Technical interviews focus more on technical depth and breadth, so thorough preparation is still essential. The circumstances for interviewers and candidates differ; an interviewer only poses a few points, but collectively, many interviewers create a full picture. Once you understand this, do not become complacent and think you are superior to the candidates. -我面的企业不多,因为我已经打算从事教育事业,用“志趣和驱动力”这项就直接过滤了很多企业的面试邀请。在杭州面试的基本是教育企业,连阿里华为等抛来的橄榄枝都婉拒了(尽管我也不一定能面上)。虽然做法有点“直男”,但投入最多精力于自己期望从事的行业和事业,才是值得的。 +I did not go through many companies for interviews, as I had already decided to pursue a career in education, directly filtering out numerous interview invitations based on my “interest and motivation.” Most of the interviews I had in Hangzhou were with educational companies, and I even declined offers from companies like Alibaba and Huawei (though I might not have made it). -我所认为的教育事业,并不局限于现在常谈起的在线教育或 K12 教育,而是一个教育体系,任何可以更好滴起到教育效果的事业,包括而不限于教学、阅读、音乐、设计等。 +While my approach may seem “straightforward,” it is indeed worthwhile to invest most effort in the industry and career I wish to pursue. -### 接力棒科技-高工 +What I consider education does not only include the commonly discussed online education or K12 education, but rather an educational system encompassing any effort that can better achieve educational outcomes, including but not limited to teaching, reading, music, and design. -面的第一家。畅谈一番后,没音讯了。但我也没有太在意。面试官问的比较偏交易业务性的东西,较深的就是如何保证应用的数据一致性了。 +### Relays Technology - Senior Engineer -此时的我,就像在路上扔了一颗探路的小石子,尚未意识到自己的处境。 +This was the first company I interviewed with. After an enjoyable discussion, I heard nothing further. However, I didn't pay too much attention to it. The interviewer asked mostly about transaction-related business aspects, with the deeper question being how to ensure data consistency across applications. -### 网易云音乐-高工 +At this point, I feel like I threw down a small stone to explore the path ahead, still unaware of my circumstances. -接着是网易云音乐。大厂就是大厂。一面问的尽是缓存、分布式锁、Dubbo、ZK, MQ 中间件相关的机制。很遗憾,由于我平时关于技术原理的沉淀还是很少,基本是“一问两不知”,挂得很出彩。 +### NetEase Cloud Music - Senior Engineer -此时,我初步意识到自己的技术底子还很薄弱,也就开始了广阔的技术学习和夯实,自底向上地梳理原理和逻辑,系统地进行整理总结,最终初步形成了自己的互联网服务端技术知识体系结构。 +Next was NetEase Cloud Music. A large company is indeed a large company. The first interview was filled with questions on caching, distributed locks, Dubbo, ZK, and related mechanisms. Unfortunately, due to my limited depth of understanding of technical principles, I essentially hung up spectacularly with a “I only know bits and pieces” kind of performance. -### 铭师堂-技术专家 +At this moment, I became acutely aware of the shortcomings in my technical foundation, prompting me to embark on broad technical learning and deepening my understanding of principles and logic, systematically organizing and summarizing my knowledge, ultimately forming my own internet server technology knowledge system. -架构师面试的。问的相对多了一些,DB, Redis 等。反馈是技术还行,但缺乏管理经验。这是我第一次意识到大龄程序员缺乏管理经验的不利。中小企业的技术专家线招聘中,往往附加了管理经验的需求。应聘时要注意。 +### Ming Shitang - Technical Expert -缺乏管理经验,该怎么办呢?思考过一段时间后,我的想法是: +This interview was for an architect role. They asked a relatively broader range of questions, covering DB, Redis, etc. The feedback was that my technical skills were decent but I lacked management experience. This was the first time I realized that middle-aged programmers lacking management experience could be disadvantageous. When applying for technical expert roles in small and medium enterprises, the requirements often include management experience. This is something to be aware of during applications. -- 改变能改变的,不能改变的,学习它。比如技术原理的学习是我能够改变的,但管理经验属于难以一时改变的,那就多了解点管理的基本理论吧。 -- 从经历中挖掘相关经验。虽然我没有正式带团队的实际经验,但是有带项目和带工程师,管控某个业务线的基本管理经验。多多挖掘自己的经历。 +What should I do if I lack management experience? After some reflection, my thoughts are: -### 字节教育-高工 +- Change what can be changed, and learn from what cannot be changed. For instance, studying technical principles is something I can change, while management experience is harder to change instantly; thus, I should learn more about fundamental management theories. +- Uncover relevant experiences from my history. Although I may not have formal experience leading a team, I do have experience managing projects and engineers, as well as basic management experience over a business line. I should unearth these experiences more. -字节教育面试,我给自己挖了不少坑往里跳。 +### Byte Education - Senior Engineer -比如面试官问,讲一个你比较成就感的项目经历。我选择的是近 4 年前的周期购项目。虽然这是我入职有赞的第一个有代表性的项目,但时间太久,又没有详细记录,很多技术细节遗忘不清晰了。我讲到当时印象比较深的“一体化”设计思想,却忘记了当时为什么会有这种思想(未做仔细记录)。 +During the Byte Education interview, I dug quite a few holes for myself. -再比如,一个上课的场景题,我问是用 CS 架构还是 BS 架构?面试官说用 CS 架构吧。这不是给自己挖坑吗?明明自己不熟悉 CS 架构,何必问这个选择呢,不如直接按照 BS 架构来讲解。哎! +For instance, when the interviewer asked me to talk about a project that I felt proud of, I chose a project from nearly four years ago: the cyclical purchasing project. Although this was my first representative project upon joining Youzan, the time lapse was too long, and I had not documented it thoroughly, forgetting many technical details. I highlighted the “integrated” design concept that left a lasting impression on me, but I failed to recall why this concept emerged (not having documented it carefully). -字节教育给我的反馈是:业务 Sense 不错,系统设计能力有待提高。我觉得还是比较中肯的。因此,也开始注重系统设计实战方面的文章阅读和思考训练。 +Furthermore, during a scenario-based question regarding a class, I asked whether to use a CS or BS architecture. The interviewer said CS. Wasn’t that digging a hole for myself? Clearly, I wasn’t familiar with CS architecture; why not ask about BS architecture from the start? Sigh! -经验是: +The feedback from Byte Education stated I had good business sense but needed to improve my system design skills. I found it quite candid. Thus, I began to place more focus on reading articles and practicing thoughts on real-world system design. -- 做项目时,要详细记录每个项目的技术栈、技术决策及原因、技术细节,为面试做好铺垫; -- 提前准备好印象最深刻的最代表性的系统和项目,避免选择距离当前时间较久的缺乏详细记录的项目; -- 选择熟悉的项目和架构,至少有好的第一印象,不然给面试官的印象就是你啥都不会。 +Lessons learned: -### 咪咕数媒-架构师 +- When working on projects, diligently document the technology stack, design decisions and their rationale, and technical details to lay a foundation for interviews; +- Prepare prominent and representative systems and projects in advance to avoid choosing ones that are too distant in time without detailed records; +- Select familiar projects and architectures to create a good first impression; otherwise, the interviewer will think you don't possess relevant knowledge. -好家伙,一下子 3 位面试官群面。可能我以前经历的太少了吧。似乎国企面试较高端职位,喜欢采取这种形式。兼听则明偏听则暗嘛。问的问题也很广泛,从 ES 的基本原理,到机房的数据迁移。有些技术机制虽然学习过,但不牢固,不清晰,答的也不好。比如 ES 的搜索原理优化,讲过倒排索引后,我对 Term Index 和 Trie 树 讲不清楚。这说明,知道并不代表真正理解了。只有能够清晰有条理地把思路和细节都讲清楚,才算是真正理解了。 +### Migu Digital Media - Architect -印象深刻的是,有一个问题:你有哪些架构思想?这是第一次被问到架构设计方面的东西,我顿时有点慌乱。虽然平时多有思考,也有写过文章,却没有形成系统精炼的方法论,结果就是答的比较凌乱。 +This interview involved three interviewers simultaneously. Perhaps I had experienced too few interviews before. It seems that state-owned enterprises appreciate such formats for higher-level positions. Inquiry fosters insight while partiality leads to ignorance. The questions were quite broad, ranging from the basic principles of ES to data migration in server rooms. Some technical mechanisms, while I had studied them, were unclear in my mind, leading to poor responses. For example, when discussing ES's search principles optimizations, after mentioning inverted indices, I was unable to articulate Term Index and Trie trees. This indicated that knowing something does not equate to truly understanding it. Genuine understanding requires the ability to articulate thoughts and details clearly. -### 涂鸦智能-高工 +What left a deep impression was one particular question: What architectural thoughts do you hold? This was my first experience being asked about architectural design concepts, and I suddenly felt a bit flustered. Although I frequently contemplate such issues and have written articles on them, I had not formed a systematic, refined methodology, resulting in a somewhat disorganized response. -应聘涂鸦智能,是因为我觉得这家企业不错。优秀的企业至少应该多沟通一下,说不准以后有合作机会呢!看问题的思维要开阔一些,不能死守在自己想到的那一个事情上。 +### Tuya Smart - Senior Engineer -涂鸦智能给我的整体观感还是不错的。面试官也很有礼貌有耐心,整体架构、技术和项目都问了很多,问到了我熟悉的地方,答得也还可以。也许我的经验正好是切中他们的需求吧。 +I applied to Tuya Smart because I found this company to be promising. An excellent company deserves at least a conversation; who knows? There might be opportunities for collaboration in the future! One's perspective on issues should be broad; one should not cling solely to their own ideas. -若不是当时想做教育的执念特别强,我很大概率会入职涂鸦智能。物联网在我看来应该是很有趣的领域。 +Overall, my experience with Tuya Smart was positive. The interviewers were courteous and patient, covering the overall architecture, technologies, and projects extensively with questions that fell into my realm of expertise, to which I responded fairly well. Perhaps my experiences matched their needs perfectly. -### 跟谁学-技术专家 +If it weren’t for my particular strong inclination towards education, it’s quite likely I would have joined Tuya Smart. In my view, the Internet of Things is an intriguing field. -“跟谁学”基本能答上来。不过反馈是:对于提问抓重点的能力有所欠缺,对于技术的归纳整理也不够。我当时还有点不服气,认为自己写了那么多文章,也算是有不少思考,怎能算是总结不够呢?顶多是有技术盲点。技术犹如海洋,谁能没有盲点? +### GoGo Learning - Technical Expert -不过现在反观,确实距离自己应该有的程度不够。对技术原理机制和生产问题排查的总结不够,不够清晰细致;对设计实践的经验总结也不够,不够系统扎实。这个事情还要持续深入地去做。 +I was able to answer questions at GoGo Learning for the most part. However, the feedback pointed out that I lacked focus on what was being asked and that my technical summarization was insufficient. At that time, I felt a bit indignant, considering how many articles I had written—I thought I had done quite a lot of thinking; how could it be considered insufficient? It was merely a matter of technical blind spots. The sea of technology is vast; who can lack blind spots completely? -此外,面得越多,越发现自己的表述能力确实有所欠缺。啰嗦、容易就一点展开说个没完、脱离背景直接说方案、跳跃、回旋往复,然后面试官很可能没耐心了。应该遵循“先总后分”、“基本思路-实现-优化”的一些基本逻辑来作答会更好一些。表述能力真的很重要,不可只顾着敲代码。还有每次面教育企业就不免紧张,生怕错过这个机会。 +However, upon reflection, I recognize that I indeed fell short of the requisite level of understanding. My summaries regarding technical principles and mechanisms, as well as troubleshooting in production situations, lack clarity and detail. This is something I need to work on continuously. -这是第二家直接告诉我年龄与经验不匹配的企业,加深了我对年龄偏大的忧虑,以致于开始有点不自信了。 +Moreover, through more interviews, I noticed an evident deficiency in my communication skills. I tend to be verbose, tend to elaborate indefinitely on a minor point, often lose context while diving straight into solutions, and retreat into a pattern of circular reasoning, which can cause interviewers to lose patience. I should adhere to core logic like “start with the main point, then break it down,” as well as “basic idea—implementation—optimization” when responding. Communication skills are critical and cannot be neglected just because one focuses on coding. Each time I interview with an educational institution, I inevitably feel nervous, fearing I might miss the opportunity. -那么我又是怎么重拾信心的呢?有一句老话:“留得青山在,不怕没柴烧”。就算我年龄比较大,如果我的技术能力打磨得足够硬朗,就不信找不到一家能够认可我的企业。大不了我去做开源项目好了。具备好的技术能力,并不一定就局限在企业的范围内去发挥作用,也没必要局限于那些被年龄偏见所蒙蔽的人的认知里。外界的认可固然重要,内在的可贵性却远胜于外在。 +This was the second company to directly point out that my age and experience did not match, heightening my concerns about being over-aged and causing me to lose a bit of confidence. -### 亿童文教-架构师 +So, how did I regain confidence? There’s an old saying: “As long as the green hills remain, one need not worry about firewood.” Even if I am older, if I can polish my technical abilities to a high degree, I refuse to believe that I won’t find a company that recognizes my worth. At worst, I could work on open-source projects. Having strong technical abilities does not necessitate functioning exclusively within a corporate framework; neither should I confine myself to the views of those blinded by age prejudice. External validation is important, but internal value far outweighs anything superficial. -也是采用的 3 人同时面试。主要问的是项目经历,技术方面问得倒不是深入。个人觉得答得还行。面试官也问了架构设计相关的问题,我答得一般。此时,我仍然没有意识到自己在以面“高级工程师”的规格来面试“架构师”岗位。 +### Yi Tong Wenchuang - Architect -面试官比较温和,HR 也在积极联系和沟通,感觉还不错。只是,我没有主动去问反馈意见,也就没有下文了。 +This interview also involved three interviewers, mainly focusing on project experiences while the technical questions were not too deep. I felt I answered reasonably well. The interviewers also asked questions related to architectural design, and my responses were average. At that moment, I still had not realized that I was interviewing for an “Architect” position while presenting myself at the level of “Senior Engineer.” -### 新东方-高工 +The interviewers were quite mild, and HR was actively in touch and communicating; the atmosphere felt positive. However, I did not proactively inquire about the feedback, so I did not hear back after that. -面试新东方,主要是因为切中我做教育的期望,虽然职位需求是做信息管理系统,距离我理想中的业务还有一定距离。经过沟通了解,他们更需要的是对运维方面更熟悉的工程师,不过我正好对运维方面不太熟悉,平时关注不多,因此不太符合他们的真实招聘要求。面试官也是很温和的人,老家在宜昌,是我本科上大学的地方,面试体验不错。 +### New Oriental - Senior Engineer -以后要花些时间学习一些运维相关的东西。作为一名优秀的工程师和合格的架构师,是要广泛学习和熟悉系统所采用的各种组件、中间件、运维部署等的。要有综观能力,不过我醒悟的可能有点迟。Better later than never. +I interviewed with New Oriental mainly because it aligned with my ambition in education, even though the position was for an information management system, which differed somewhat from my ideal business focus. After discussing further, it turned out they were seeking engineers more familiar with operations, a domain in which I lack familiarity and attention. Thus, I didn’t quite meet their genuine recruitment expectations. The interviewer, who was warm, was originally from Yichang—my university town; the interview experience was great. -### ZOOM-高工 +In the future, I need to dedicate time to learn about operational aspects. As an excellent engineer and a qualified architect, one should broadly learn and become familiar with various components, middleware, and operational deployments used in systems. One should possess an overarching view, although I might have realized this a bit late. Better late than never. -ZOOM 的一位面试官或许是我见过的所有面试官中最差劲的。共有两位面试官,一位显得很有耐心,另一位则挺着胖胖的肚子,还打着哈欠,一副不怎么关心面试和候选人的样子。我心想,你要不想面,为啥还要来面呢?你以为候选人就低你一等么?换个位置我可以暴打你。不过我还是很有礼貌的,当做什么事也没发生。公司在挑人,候选人也在挑选公司。 +### ZOOM - Senior Engineer -想想,ZOOM 还是疫情期间我们公司用过的远程通信会议软件。印象还不错,有这样的工程师和面试官藏于其中,我也是服了。难倒他是传说中的大大神?据我所知,国外对国内的互联网软件技术设施基本呈碾压态势,中国大部分企业所用的框架、中间件、基础设施等基本是拿国外的来用或者做定制化,真正有自研的很少,有什么好自满的呢? +The interview with ZOOM left a rather sour impression. There were two interviewers present; one seemed very patient, while the other, portly and yawning, appeared somewhat uninterested in both the interview and the candidate. I thought to myself, if you don't feel like interviewing, why come at all? Do you think candidates are beneath you? I could easily dash you a smart retort. Nevertheless, I remained polite, acting as though nothing happened. The company is selecting talent, and candidates are also selecting companies. -### 阿优文化-高工 +Reflecting back, ZOOM was the remote communication tool our company used during the pandemic, which left a good impression; discovering such engineers and interviewers within was quite surprising. Is he really that legendary? As far as I know, foreign technology in the realm of internet software basically crushes domestic options. Most Chinese companies use or customize foreign frameworks, middleware, and infrastructures; what boast is there to be had? -阿优文化有四轮技术面。其中第一个技术面给我印象比较深刻。看上去,面试官对操作系统的原理机制特别擅长和熟悉。很多问题我都没答上来。本以为挂了,不过又给了扳回一局的机会。第二位面试问的项目经历和技术问题是我很熟悉的。第三位面试官问的比较广泛,有答的上来的,有答不上来的。不过面试官很耐心。第四位是技术总监,也问得很广泛细致。 +### Ayou Culture - Senior Engineer -整体来说,面试氛围还是很宽松的。不过,阿优当时的招聘需求并不强烈,估计是希望后续有机会时再联系我。可惜我那时准备回武汉了。主要是考虑父母年事已高,希望能多陪陪父母。 +Ayou Culture had four rounds of technical interviews. The first technical interview left a strong impression on me. The interviewer seemed well-versed in the principles and mechanisms of operating systems, and I struggled to answer many questions. I initially thought I might fail the interview, but surprisingly, I was given another chance to prove myself. The second interviewer covered project experiences and technical questions, which I was quite familiar with. The third interviewer asked a range of questions, some of which I answered well, and some where I struggled. Fortunately, they were very patient. -想想,我想问题做决策还是过于简单的,不会做很复杂的计算和权衡。 +Overall, the atmosphere during the interview was quite relaxed. However, at that time, Ayou’s hiring needs were not urgent; they likely hoped to reach out again when opportunities arose. Unfortunately, I was preparing to return to Wuhan at that time, mainly considering my elderly parents, and I wished to spend more time with them. -### 小米-专家/架构 +Considering this, I realized my approach to problem-solving and decision-making was rather simplistic, lacking the ability to make more intricate calculations and balances. -应聘小米,主要是因为职位与之前在有赞做的很相似,都是做交易中台相关。浏览小米官网之后,觉得他们做的事情很棒,可是与我想做教育文化事业的初衷不太贴合。 +### Xiaomi - Expert/Architect -加入小米的意愿不太强烈,面试也就失去了大半动力。我这个性子还是要改一改。 +I applied to Xiaomi mainly because the position was very similar to my previous role at Youzan, both involving transaction platforms. After browsing their website, I found their work impressive, yet it didn’t align closely with my aspiration in educational culture. -### 视觉中国-高工 +I lacked a strong desire to join Xiaomi, resulting in my dwindling motivation for the interview. -围绕技术、项目和经历来问。总体来说,技术深度并不是太难,项目方面也涉及到了。人力面前辈很温和,我以为会针对自己的经历进行一番“轰炸”,结果是为前辈讲了讲有赞的产品服务和生意模式,然后略略带了下自己的一些经历。 +### Visual China - Senior Engineer -### 科大讯飞-架构师 +They asked questions centered around technology, projects, and experiences. Overall, the depth of technical questions wasn’t too difficult, and they did involve project elements. The HR representative was pleasant, and instead of “bombarding” me regarding my experiences, I explained Youzan's product services and business model while slightly introducing my background. -一二面,感觉面试官对安排的面试不太感兴趣。架构师,至少是一个对技术和设计能力非常高要求的职位。一面的技术和架构都问了些,二面总围绕我的背景和非技术相关的东西问,似乎对我的外在更关注,而对我自身的技术和设计能力不感兴趣。交流偏浅。 +### iFlytek - Architect -能力固然有高下之分,但尊重人才的基本礼节却是不变的。尊重人才,是指聚焦人才的能力和才学,而不是一些与才学不甚相关的东西。 +In the first two interviews, I felt the interviewers were disinterested in the arrangements made for the interviews. An architect's role is one that demands very high technical and design capabilities. In the first interview, they asked several questions on technology and architecture; however, in the second round, most questions revolved around my background and non-technical aspects, seemingly focusing more on my outward attributes rather than my technical and design capabilities. The discussions felt superficial. -### 青藤云-高工 +While abilities can differ in level, the fundamental etiquette of respecting talent remains unchanged. Respecting talent implies focusing on their abilities and skills, not on aspects unrelated to talent. -青藤云的技术面试风格是温和的。感受到坦率交流的味道,被认可的感觉。感受到 HR 求才若渴的心情。和我之前认为的“应当用其实力去赢得对方的尊重和赏识”不谋而合。 +### Qingteng Cloud - Senior Engineer -### 腾讯会议-高工 +The technical interview process at Qingteng Cloud was gentle. There was a sense of open and candid communication, and I felt valued. Their enthusiasm for hiring talent coincided with my previous belief in “earning respect and appreciation through abilities.” -和腾讯面试官是用腾讯会议软件面试腾讯会议的职位。哈哈。由于网络不太稳定,面试过程充满了磕磕碰碰,一句话没说完整就听不清楚了。可想情况如何。但是我们都很有很有很有耐心,最终一起完成了一面。面试是双方智慧与力量的较量,更是双方一起去完成一件事情、发现彼此的合作。这样想来,传统的“单方考验筛选式”的面试观念需要革新。 +### Tencent Conference - Senior Engineer -由于我已经拿到 offer , 且腾讯会议的事情并不太贴合自己的初衷,因此,我与腾讯方面沟通,停止了二面。 +I interviewed for a position at Tencent using Tencent's own meeting software. Due to unstable internet connections, the interview was riddled with interruptions, and I struggled to get through sentences clearly. Nevertheless, we remained patient and ultimately completed the interview. Interviews should be seen as a confrontation of wisdom and strength between both parties, and more importantly, as a cooperative endeavor to achieve something while discovering mutual collaboration potential. Hence, the traditional view of interviews as a singular testing mechanism requires reevaluation. -### 最终选择 +Since I had already received an offer and this particular role did not align closely with my original intentions, I communicated with Tencent and decided to withdraw from further interviews. -当拿到多个 offer 时,如何选择呢?我个人主要看重: +### Final Choices -1. 志趣与驱动力; -2. 薪资待遇; -3. 公司发展前景和个人发展空间; -4. 工作氛围; -5. 小而有战斗力的企业。 +When receiving multiple offers, how do I choose? Personally, I focus on: -在视觉中国与青藤云之间如何选择?作个对比: +1. Interests and motivations; +1. Salary and benefits; +1. Company development prospects and personal growth opportunities; +1. Work environment; +1. Small but capable enterprises. -- 薪资待遇:两者的薪资待遇不相上下,也都是认可我的;视觉中国给出的是 Leader 的职位,而青藤云给出的是核心业务的承诺; -- 工作氛围:青藤云应该更偏工程师文化氛围,而视觉中国更偏业务化; -- 挑战性:青藤云的技术挑战更强,而视觉中国的业务挑战性更强; -- 志趣与驱动力:视觉中国更符合我想做文化的事情,而青藤云安全并不贴合我想做教育文化事业的初衷,而且比较偏技术和底层(我更希望做一些人文性的事情)。但青藤云做的是关于安全的事情,安全是一件很有价值很有意义的事情。而且,以后安全也可以服务于教育行业。有点曲线救国的味道。尤其是创始人张福的理想主义信念“让安全之光照亮互联网的每个角落”及自己的身体力行,让人更有一些触动。最终,我觉得做安全比做图片版权保护稍胜出一小筹。 +How do I select between Visual China and Qingteng Cloud? Here's a comparison: -此外,我觉得做教育,更适合自己的是编程教育,或者是工程师教育。我还想成为一名系统设计师。还需要积累更多生产实践经验。可以多与初中级工程师打交道,在企业内部做培训指导。或者工作之余录制视频,上传到 B 站,服务广大吃瓜群众。将来,我或许还会写一本关于编程设计的书,汇聚毕生所学。 +- Salary and benefits: Both offer competitive packages and recognize my potential; Visual China’s offer is for a Lead position, while Qingteng Cloud promises roles in core business; +- Work environment: Qingteng Cloud should lean towards an engineering culture, while Visual China focuses more on business; +- Challenge: Qingteng Cloud presents a more robust technical challenge, while Visual China poses a stronger business challenge; +- Interests and motivations: Visual China aligns better with my desire to engage in cultural endeavors, while Qingteng Cloud does not closely fit my educational aspirations and leans more towards fundamental technical work (while I prefer more humanistic endeavors). However, Qingteng Cloud’s focus on safety is of great value and meaningful. Furthermore, safety in the future can serve the education sector, providing a sort of circumstantial rescue. Particularly, founder Zhang Fu’s idealistic belief “to let the light of security illuminate every corner of the internet” and his active pursuit inspire me. In the end, I feel that opting for safety slightly outweighs the notion of engaging in image copyright protection. -因此,经过一天慎重的考虑,我决定,加入青藤云安全。当然,做这个选择的同时,也意味着我选择了一个更大的挑战:在安全方面我基本一穷二白,需要学习很多很多的知识和经验,对于我这个大龄程序员来说,是一项不小的挑战。 +Additionally, I think that education, as it suits me best, should focus on coding education or engineer education. I want to become a systems designer, requiring more practical production experience. I may interact more with junior to mid-level engineers, providing training and guidance within the company. Alternatively, during my spare time, I may record videos for Bilibili to serve a broader audience. In the future, I might even write a book on coding design, consolidating what I have learned throughout my life. -## 小结 +Thus, after careful consideration over a day, I decided to join Qingteng Cloud Security. Of course, making this choice also means embracing a greater challenge: I am starting from nearly zero in security knowledge and must learn a vast array of knowledge and experience, which constitutes a significant challenge for me as a middle-aged programmer. -很多事情都有解决的方法,即使“头疼的”大龄程序员找工作也不例外。确立明确清晰的目标、制定科学合理的决策、持续的努力、掌握基本面、恰当的出击,终能斩获胜利的果实。但要强调一下:功夫在平时。平时要是不累积好,面试的时候就要花更多时间去学习,会受挫、磕磕碰碰、过得也不太舒坦。还是平摊到平时比较好。此外,平时视野也要保持开阔,切忌在面试的时候才“幡然醒悟”。 +## Summary -一个重要经验是,要善于从失败中学习。正是在杭州四个月空档期的持续学习、思考、积累和提炼,以及面试失败的反思、不断调整对策、完善准备、改善原有的短板,采取更为合理的方式,才在回武汉的短短两个周内拿到比较满意的 offer 。 +Many issues have solutions, and the plight of a “middle-aged programmer” seeking a job is no exception. By establishing clear and defined goals, making scientifically sound decisions, sustaining efforts, mastering the basics, and striking appropriately, one can ultimately reap the rewards of victory. However, it's crucial to emphasize: The effort must be made during normal times. If one neglects accumulation during regular moments, it leads to needing to spend even more time studying during interviews, facing setbacks, and experiencing a bumpy path, making it quite uncomfortable. It's always better to distribute effort over time. Additionally, maintaining an open perspective is essential—do not wait until interviews to have an epiphany. -此外,值得提及的是,对于技术人员,写博客是一件很有价值的事情。面试通过沟通去了解对方,有其局限性所在。面试未能筛选出符合的人才其实是有比较大概率的: +An important lesson is to learn from failures. The continuous learning, thinking, accumulating, and refining periods during a four-month gap in Hangzhou, alongside reflections on interview failures, constantly adjusting strategies, perfecting preparations, and enhancing previous shortcomings allowed me to receive a satisfactory offer within just two weeks upon returning to Wuhan. -1. 面试的时间很短,即使是很有经验的面试官,也会看走眼(根本局限性); -2. 面试官问到的正好是自己不会的(运气问题); -3. 面试官情绪不好,没兴趣(运气问题); -4. 面试官自身的水平。 +Moreover, it’s worth mentioning that for technical personnel, writing blogs is an incredibly valuable endeavor. Interviews are limited by the communication format and cannot filter out candidates who may be suited for the role: -因此,具备真才实学而被 PASS 掉,并不值得伤心。写博客的意义在于,有更多展示自己思考和平时工作的维度。 +1. The interview time is short—even experienced interviewers can overlook potential (a fundamental limitation); +1. The interviewer may ask something for which the candidate is unprepared (an issue of luck); +1. The interviewer could be in a bad mood and uninterested (another issue of luck); +1. The interviewer’s own level of understanding. -尊重人才的企业,一定是希望从多方面去认识候选人(在优点和缺点之间选择确认是否符合期望),包括博客;不尊重人才的企业,则会倾向于用偷懒的方法,对候选人真实的本领不在意,用一些外在的标准去快速过滤,固然高效,最终对人才的识别能力并不会有多大进步。 +Thus, possessing true skills yet being passed over should not lead to heartbreak. The value of writing blogs lies in providing additional dimensions through which to exhibit one’s thinking and day-to-day work. -经过这一段面试的历炼,我觉得现在相比离职时的自己,又有了不少进步的。不说脱胎换骨,至少也是蜕了一层皮吧。差距,差距还是有的。起码面试那些知名大厂企业的技术专家和架构师还有差距。这与我平时工作的挑战性、认知视野的局限性及总结不足有关。下一次,我希望积蓄足够实力做到更好,和内心热爱的有价值有意义的事情再近一些些。 +Quality organizations looking to respect talent will seek to understand candidates from multiple perspectives (choosing between strengths and weaknesses to confirm fit), including blogs; while companies that do not respect talents will lean towards lazy methods that disregard the genuine abilities of candidates, using superficial standards for quick filtering—efficient, yes, but ultimately it doesn’t enhance their talent recognition capabilities. -面试,其实也是一段工作经历。 +Having undergone this series of interview trials, I feel I have made quite a bit of progress compared to the person I was when I left. While not a complete transformation, at the very least, I have shed a layer of skin. There are still differences, particularly in interviewing for technical expert and architect positions in well-known large companies. This gap relates to the challenges faced in my daily work, the limitations of my cognitive horizons, and insufficient summarization. Next time, I hope to accumulate enough strength to perform even better, drawing closer to valuable endeavors that resonate with my heart. - +Interviews can also be regarded as an experience in one’s career. diff --git a/docs/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.md b/docs/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.md index 4cc38409fb7..5c6fd85a41c 100644 --- a/docs/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.md +++ b/docs/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.md @@ -1,197 +1,197 @@ --- -title: 斩获 20+ 大厂 offer 的面试经验分享 -category: 技术文章精选集 -author: 业余码农 +title: Interview Experience Sharing for Securing 20+ Offers from Major Companies +category: Selected Technical Articles +author: Amateur Coder tag: - - 面试 + - Interview --- -> **推荐语**:很实用的面试经验分享! +> **Recommendation**: This is a very practical interview experience sharing! > -> **原文地址**: +> **Original Article Link**: -突然回想起当年,我也在秋招时也斩获了 20+的互联网各大厂 offer。现在想起来也是有点唏嘘,毕竟拿得再多也只能选择一家。不过许多朋友想让我分享下互联网面试方法,今天就来给大家仔细讲讲打法! +Suddenly reminiscing, I also received offers from more than 20 major internet companies during my autumn recruitment back then. Looking back now, it feels a bit bittersweet, after all, no matter how many offers I got, I could only choose one. However, many friends have asked me to share my interview methods for the internet sector, so today I will detail the strategies with you all! -如今金九银十已经过去,满是硝烟的求职战场上也只留下一处处炮灰。在现在这段日子,又是重新锻炼,时刻准备着明年金三银四的时候。 +Now that the golden September and silver October have passed, the job hunt battlefield is filled only with fallen candidates. During this period, it’s time for retraining, always prepared for the upcoming golden March and silver April next year. -对于还没毕业的学生来说,明年三四月是春招补招或者实习招聘的机会;对于职场老油条来说,明年三四月也是拿完年终奖准备提桶跑路的时候。 +For students who have not yet graduated, March and April next year will be opportunities for spring recruitment and internships; for seasoned professionals, these months will also be the time to grab their year-end bonuses and prepare to leave. -所以这段日子,就需要好好准备积累面试方法以及面试经验,明年的冲锋陷阵打下基础。这篇文章将为大家讲讲,程序员应该如何准备好技术面试。 +Therefore, during this time, it’s essential to prepare and accumulate interview strategies and experiences as a foundation for next year's battles. This article will discuss how programmers should prepare for technical interviews. -一般而言,互联网公司技术岗的招聘都会根据需要设置为 3 ~ 4 轮面试,一些 HC 较少的岗位可能还会经历 5 ~ 8 轮面试不等。除此之外,视公司情况,面试之前还可能也会设定相应的笔试环节。 +Generally, the recruitment for technical positions in internet companies is set to 3 to 4 rounds of interviews, and some positions with fewer headcounts may undergo 5 to 8 rounds. Additionally, depending on the company, there may also be corresponding written tests before the interviews. -多轮的面试中包括技术面和 HR 面。相对来说,在整体的招聘流程中,技术面的决定性比较重要,HR 面更多的是确认候选人的基本情况和职业素养。 +The multiple rounds of interviews include technical interviews and HR interviews. Relatively speaking, the technical interview holds more decisive importance in the overall recruitment process, whereas the HR interview mainly confirms the candidate's basic situation and professional qualities. -不过在某些大厂,HR 也具有一票否决权,所以每一轮面试都该好好准备和应对。技术面试一般可分为五个部分: +However, in some major companies, HR has a veto power, so each round of interviews should be well-prepared and handled. Technical interviews can generally be divided into five parts: -1. 双方自我介绍 -2. 项目经历 -3. 专业知识考查 -4. 编码能力考察 -5. 候选人 Q&A +1. Self-introduction by both parties +1. Project experience +1. Professional knowledge assessment +1. Coding ability assessment +1. Candidate Q&A -## 双方自我介绍 +## Self-introduction by both parties -面试往往是以自我介绍作为开场,很多时候一段条理清晰逻辑明确的开场会决定整场面试的氛围和节奏。 +Interviews often begin with a self-introduction. Many times, a clear and logically structured opening can set the atmosphere and rhythm for the entire interview. -**作为候选人,我们可以在自我介绍中适当的为本次面试提供指向性的信息,以辅助面试官去发掘自己身上的亮点和长处**。 +**As candidates, we can provide directional information in our self-introduction to assist the interviewer in discovering our highlights and strengths**. -其实自我介绍并不是简单的个人基本情况的条条过目,而是对自己简历的有效性概括。 +In fact, a self-introduction is not merely a recitation of basic personal information; it is an effective summary of the resume. -什么是有效性概括呢,就是意味着需要对简历中的信息进行核心关键词的提取整合。一段话下来,就能够让面试官对你整体的情况有了了解,从而能够引导面试官的联系提问。 +What does effective summarization mean? It implies extracting and integrating core keywords from the information in the resume. By summarizing in one paragraph, the interviewer can gain an overall understanding of your situation, thus guiding their follow-up questions. -## 项目经历 +## Project Experience -项目经历是面试过程中非常重要的一环,特别是在社招的面试中。一般社招的职级越高,往往越看重项目经历。 +Project experience is a crucial aspect of the interview process, especially in social recruitment interviews. Generally, the higher the level of the position in social recruitment, the more emphasis is placed on project experience. -而对于一般的校招生而言,几份岗位度匹配度以及项目完整性高的项目经历可以成为面试的亮点,也是决定于拿`SP` or `SSP`的关键。 +For typical campus hires, having a few high-fit and complete project experiences can become the highlight of the interview and is key to securing an `SP` or `SSP`. -但是准备好项目经历,并不是一件容易的事情。很多人并不清楚应该怎样去描述自己的项目,更不知道应该在经历中如何去体现自己的优势和亮点。 +However, preparing project experiences is not an easy task. Many people do not know how to describe their projects or how to demonstrate their advantages and highlights within their experiences. -这里针对项目经历给大家提几点建议: +Here are some suggestions for project experience: -**1、高效有条理的描述** +**1. Efficient and Organized Description** -项目经历的一般是简历里篇幅最大的部分,所以在面试时这部分同样重要。在表述时,语言的逻辑和条理一定要清晰,以保证面试官能够在最快的时间抓到你的项目的整体思路。 +Project experience is usually the largest section in a resume, making it equally important during the interview. In your expression, ensure that the logic and clarity are apparent, allowing the interviewer to quickly grasp the overall idea of your project. -相信很多人都听说过写简历的各种原则,比如`STAR`、`SMART`等。但实际上这些原则都可以用来规范自己的表达逻辑。 +Many have heard of various principles for writing resumes, such as `STAR` and `SMART`. In reality, these principles can help standardize your expression logic. -`STAR`原则相对简单,用来在面试过程中规范自己的条理非常有效。所谓`STAR`,即`Situation`、`Target`、`Action`、`Result`。这跟写论文写文档的逻辑划分大体一致。 +The `STAR` principle is relatively straightforward and very effective for structuring your thoughts during the interview. The acronym `STAR` stands for `Situation`, `Target`, `Action`, and `Result`, which is broadly consistent with the logical structure used in writing papers or documents. -- `Situation`: 即项目背景,需要将项目提出的原因、现状以及出发点表述清楚。简单来说,就是要将项目提出的来龙去脉描述清晰。比如某某平台建设的原因,是切入用户怎样的痛点之类的。 -- `Target`: 即项目目标,这点描述的是项目预期达到或完成的程度。**最好是有可量化的指标和预期结果。**比如性能优化的指标、架构优化所带来的业务收益等等。 -- `Action`: 即方法方案,意味着完成项目具体实施的行为。这点在技术面试中最为重要,也是表现候选人能力的基础。**项目的方法或方案可以从技术栈出发,根据采用的不同技术点来具体写明解决了哪些问题。**比如用了什么框架/技术实现了什么架构/优化/设计,解决了项目中什么样的问题。 -- `Result`: 即项目获得结果,这点可以在面试中讲讲自己经历过项目后的思考和反思。这样会让面试官感受到你的成长和沉淀,会比直接的结果并动人。 +- `Situation`: The background of the project, where you need to clarify the reason, current situation, and starting point of the project. In simple terms, you should clearly describe the context of the project. For example, explain the reasons behind the establishment of a particular platform and the user pain points it addresses. +- `Target`: The project objectives, which describe the expected degree of achievement or completion of the project. **It’s best if there are quantifiable indicators and expected results.** For instance, performance optimization metrics, business benefits from architectural improvements, etc. +- `Action`: The methods or plans, which represent the specific actions taken to implement the project. This part is the most important during technical interviews and serves as the foundation for demonstrating the candidate's capabilities. **The methods or plans can be detailed based on the technology stack used, specifying which problems were solved by employing different technologies.** For example, specifying which frameworks or technologies were used for implementations, optimizations, or designs and what problems were addressed in the project. +- `Result`: The outcomes of the project. This part can showcase your reflections and insights after experiencing the project during the interview. This approach allows the interviewer to perceive your growth and introspection, which can be more impactful than simply stating the results. -**2、充分准备项目亮点** +**2. Sufficiently Prepare Project Highlights** -说实话,大部分人其实都没有十分亮眼的项目,但是并不意味着没有项目经历的亮点。特别是在面试中。 +To be honest, most people may not have particularly outstanding projects, but that does not mean there are no highlights in their project experiences, especially in an interview. -在面试中,你可以通过充分的准备以及深入的思考来突出你的项目亮点。比如可以从以下几个方向入手: +During the interview, you can emphasize your project highlights through thorough preparation and deep reflection. Here are several directions to consider: -- 充分了解项目的业务逻辑和技术架构 -- 熟悉项目的整体架构和关键设计 -- 明确的知道业务架构或技术方案选型以及决策逻辑 -- 深入掌握项目中涉及的组件以及框架 -- 熟悉项目中的疑难杂症或长期遗留 bug 的解决方案 -- …… +- Fully understand the business logic and technical architecture of the project +- Be familiar with the overall architecture and key designs of the project +- Clearly understand the architectural decisions made and the rationale behind them +- Have an in-depth knowledge of the components and frameworks involved in the project +- Be familiar with the pain points or longstanding bugs in the project and their solutions +- … -## 专业知识考查 +## Professional Knowledge Assessment -有经验的面试官往往会在对项目经历刨根问底的同时,从中考察你的专业知识。 +Experienced interviewers often dig deep into project experiences while also assessing your professional knowledge. -所谓专业知识,对于程序员而言就是意向岗位的计算机知识图谱。对于校招生来说,大部分都是计算机基础;而对于社招而言,很大部分可能是对应岗位的技能树。 +Professional knowledge, for programmers, pertains to the computer knowledge framework relevant to the intended position. For campus recruits, it largely consists of foundational computer knowledge; for experienced recruits, it pertains more to the skill tree related to the specific position. -计算机基础主要就是计算机网络、操作系统、编程语言之类的,也就是所谓的八股文。虽然这些东西在实际的工作中可能用处并不多,但是却是面试官评估候选人潜力的标准。 +Foundational computer knowledge primarily includes computer networks, operating systems, programming languages, and the like, which can be considered the basics. Although such knowledge may not be frequently used in actual work, it serves as a standard for interviewers to evaluate candidates’ potential. -而对应岗位的技能树就需要根据具体的岗位来划分,**比如说客户端岗位可能会问移动操作系统理解、端性能优化、客户端架构以及跨端框架之类的。跟直播视频相关的岗位,还会问音视频处理、通信等相关的知识。** +The skill tree relevant to the position should be classified according to specific roles. **For example, for client-side positions, one might be questioned on their understanding of mobile operating systems, end performance optimization, client architecture, and cross-end frameworks. For positions related to live video, questions may cover audio and video processing and communications.** -而后端岗位可能就更偏向于**高可用架构、事务理论、分布式中间件以及一些服务化、异步、高可用可扩展的架构设计思想**。 +Backend positions may lean more towards **high availability architecture, transaction theory, distributed middleware, and the architectural design principles of service-oriented, asynchronous, and scalable systems**. -总而言之,工作经验越丰富,岗位技术能的问题也就越深入。 +In summary, the richer one’s work experience, the more in-depth their technical understanding of the position. -怎么在面试前去准备这些技术点,在这里我就不过多说了, 因为很多学习路线以及说的很清楚了。 +I won’t go into too much detail about how to prepare these technical points before the interview, as numerous study paths have already been clearly outlined. -这里我就讲讲在应对面试的时候,该怎样去更好的表达描述清楚。 +I’ll focus on how to better express and clearly describe these points during the interview. -这里针对专业知识考察给大家提几点建议: +Here are some suggestions for professional knowledge assessment: -**1、提前建立一份技术知识图谱** +**1. Establish a Technical Knowledge Framework in Advance** -在面试之前,可以先将自己比较熟悉的知识点做一个简单的归纳总结,根据不同方向和领域画个简单的草图。这是为了辅助自己在面试时能够进行合理的扩展和延伸。 +Before the interview, you can summarize the knowledge points you are familiar with and create a simple diagram categorized by different directions and fields. This serves to assist yourself in expanding and extending your knowledge during the interview. -面试官一问一答形式的面试总是会给人不太好的面试体验,所以在回答技术要点的过程中,要善于利用自己已有的知识图谱来进行技术广度的扩展和技术深度的钻研。这样一来能够引导面试官往你擅长的方向去提问,二来能够尽可能多的展现自己的亮点。 +The question-and-answer format of interviews often leaves a less favorable impression, so during the response process, be adept at using your established knowledge framework to broaden the technical breadth and delve into technical depth. This will not only guide the interviewer toward asking questions in areas where you excel but also help showcase as many highlights of yours as possible. -**2、结合具体经验来总结理解** +**2. Summarize Understanding by Combining Specific Experiences** -技术点本身都是非常死板和冰冷的,但是如果能够将生硬的技术点与具体的案例结合起来描述,会让人眼前一亮。同时也能够表明自己是的的确确理解了该知识点。 +Technical points themselves can be quite rigid and cold; however, if you can describe them by combining hard technical points with specific cases, it will be much more engaging. It also demonstrates that you truly understand that knowledge point. -现在网上各种面试素材应有尽有,可能你背背题就能够应付面试官的提问。但是面试官也同样知道这点,所以他能够很清楚的判别出你是否在背题。 +There is a vast array of interview resources available online, and sometimes you might just need to memorize questions to handle the interviewer's inquiries. However, interviewers are also aware of this and can easily discern whether you are merely regurgitating answers. -因此,结合具体的经验来解释表达问题是能够防止被误认为背题的有效方法。可能有人会问了,那具体的经验哪里去找呢。 +Therefore, combining specific experiences to explain and express your answers is an effective way to avoid being perceived as someone who merely memorizes answers. Some may wonder where to find such specific experiences. -这就得靠平时的积累了,平时需要多积累沉淀,多看大厂的各类技术输出。经验不一定是自己的,也可以是从别的地方总结而来的。 +This relies on daily accumulation; it is crucial to continually gather insights and observe various technical outputs from major companies. Experience doesn't have to be solely your own but can also be derived from summarizing other sources. -此外,也可以结合自己在做项目的过程中的一些技术选型经验以及技术方案更新迭代的过程进行融会贯通,相互结合的来进行表述。 +Furthermore, you can amalgamate experiences from your projects involving decision-making on technical selections and the iterative processes of technological solutions to articulate your points cohesively. -## 编码能力考察 +## Coding Ability Assessment -编码能力考察就是咱们俗称的手撕代码,也是许多同学最害怕的一关。很多人会觉得面试结果就是看手撕代码的表现,但其实并不一定。 +The coding ability assessment refers to what we commonly call "whiteboard coding," which is one of the aspects many students dread the most. Many believe that interview outcomes hinge solely on their coding performance, but that is not always the case. -**首先得明确的一点是,编码能力不完全等于算法能力。**很多同学面试时候算法题明明写出来了,但是最终的面试评价却是编码能力一般。还有很多同学面试时算法题死活没通过,但是面试官却觉得他的编码能力还可以。 +**First and foremost, it’s essential to clarify that coding ability does not completely equal algorithm proficiency.** Many students may successfully solve algorithm questions during interviews, yet the overall assessment is that their coding ability is average. Conversely, other students may fail to pass the algorithm question, yet the interviewer still finds their coding ability acceptable. -所以一定要注意区分这点,编码能力不完全等于算法能力。从公司出发,如果纯粹为了出难度高的算法题来筛选候选人,是没有意义的。因为大家都知道,进了公司可能工作几年都写不了几个算法。 +Thus, it's crucial to distinguish between the two; coding ability is not solely synonymous with algorithm ability. From the company's perspective, solely using difficult algorithm questions to filter candidates holds no real value. After all, everyone understands that they may not solve many algorithms in their jobs years down the line. -要记住,做算法题只是一个用来验证编码能力和逻辑思维的手段和方式。 +Remember, solving algorithm questions is merely a means to validate coding ability and logical thinking. -当然说到底,在准备这一块的面试时,算法题肯定得刷,但是不该盲目追求难度,甚至是死记硬背。 +Ultimately, while preparing for this aspect of the interview, practicing algorithm questions is indeed necessary, but one should not blindly pursue difficulty or rote memorization. -几点面试时的建议: +Here are some suggestions for interviews: -**1、数据结构和算法思想是基础** +**1. Data Structures and Algorithm Concepts are Fundamental** -算法本身实际上是逻辑思考的产物,所以掌握算法思想比会做某一道题要更有意义。数据结构是帮助实现算法的工具,这也很编程的基本能力。所以这二者的熟悉程度是手撕代码的基础。 +Algorithms are essentially products of logical thinking, so grasping algorithm concepts holds more significance than merely solving a specific question. Data structures serve as tools to implement algorithms, representing core programming abilities. Therefore, proficiency in both is foundational for coding assessments. -**2、不要忽视编码规范** +**2. Do Not Overlook Coding Standards** -这点就是提醒大家要记住,就算是一段很简单的算法题也能够从中看出你的编码能力。这往往就体现在一些基本的编码规范上。你说你编程经验有 3 年,但是发现连基本的函数封装类型保护都不会,让人怎么相信呢。 +This point is a reminder that even a very simple algorithm question can reveal your coding ability. This often reflects in adherence to basic coding standards. If you claim to have 3 years of programming experience but cannot even implement essential function encapsulation type protections, how can one trust you? -**3、沟通很重要** +**3. Communication is Key** -手撕代码绝对不是一个闭卷考试的过程,而是一个相互沟通的过程。上面也说过,考察算法也是为了考察逻辑思维能力。所以让面试官知道你思考问题的思路以及逻辑比你直接写出答案更重要。 +Whiteboard coding is definitely not a closed-book exam; rather, it is a process of mutual communication. As mentioned earlier, assessing algorithms is also aimed at evaluating logical thinking capabilities. Therefore, sharing your thought processes and reasoning with the interviewer is far more important than simply writing down an answer. -不仅如此,提前沟通清楚思路,遇到题意不明确的地方及时询问,也是节省大家时间,给面试官留下好印象的机会。 +Moreover, clearly communicating your thoughts beforehand and promptly questioning any ambiguities in the problem statement not only saves time for everyone but also leaves a favorable impression on the interviewer. -此外,自己写的代码一定要经得住推敲和质疑,自己能够讲的明白。这也是能够区分「背题」和「真正会做」的地方。 +Additionally, the code you write must withstand scrutiny and questioning, and you should be able to explain it clearly. This also serves as a distinguishing factor between "reciting answers" and "truly understanding." -最后,如果代码实在写不出来,但是也可以适当的表达自己的思路并与面试官交流探讨。毕竟面试也是一个学习的过程。 +Lastly, if you find yourself unable to produce code, you can still express your thought process and engage in discussion with the interviewer. After all, the interview process is also a learning experience. -## 候选人 Q&A +## Candidate Q&A -一般正常的话,都会有候选人反问环节。倘若没有,可能是想让你回家等消息。 +Typically, there will be a candidate questioning segment. If this is absent, it may mean they want you to go home and await news. -反问环节其实也可以是面试中重要的环节,因为这个时候你能够从面试官口中获得关于公司关于岗位更具体真实的信息。 +The questioning segment can actually be a crucial part of the interview since you can gain more concrete and authentic information about the company and the role from the interviewer. -这些信息可以帮助我们做出更全面更理性的决策,毕竟求职也是一个双向选择的过程。 +This information can assist us in making more comprehensive and rational decisions, as job seeking is a two-way selection process. -## 加分项 +## Bonus Points -最后,给能够坚持看到最后的同学一个福利。我们来谈谈面试中的加分项。 +Finally, here’s a reward for those who managed to read to the end. Let's talk about bonus points in interviews. -很多同学会觉得明明面试时候的问题都答上来了,但是最终却没有通过面试,或者面试评价并不高。这很有可能就是面试过程中缺少了亮点,可能你并不差,但是没有打动面试官的地方。 +Many students might feel they have answered all questions during the interview yet still fail to secure a position or receive a low interview evaluation. This could very well be due to a lack of highlights throughout the interview; perhaps you are not lacking in skills, but there was nothing that moved the interviewer. -一般面试官会从下面几个方面去考察候选人的亮点: +Interviewers typically evaluate candidates based on the following aspects: -**1、沟通** +**1. Communication** -面试毕竟是问答与表达的艺术,所以你流利的表达,清晰有条理的思路自然能够增加面试官对你的高感度。同时如果还具有举一反三的思维,那也能够从侧面证明你的潜力。 +Interviews, after all, are an art of questioning and expression, so your fluent communication and clear, organized thought processes can naturally enhance the interviewer's perception of you. Additionally, if you demonstrate the ability to apply one concept to another, it can indirectly prove your potential. -**2、匹配度** +**2. Match Quality** -这一点毋庸置疑,但是却很容易被忽视。因为往往大家都会认为,匹配度不高的都在简历筛选阶段被刷掉了。但其实在面试过程中,面试官同样也会评估面试人与岗位的匹配度。 +This point is unquestionable but easily overlooked. Many believe that candidates with low match quality are filtered out during the resume screening stage. However, interviewers also assess the candidate's alignment with the role during the interview. -这个匹配度与工作经历强相关,与之前做过的业务和技术联系很大。特别是某些垂直领域的技术岗位,比如财经、资金、音视频等。 +This match quality is strongly correlated with work experience and closely linked to previous business and technology contexts. Particularly for certain specialized technical roles such as finance, investments, and audio-video related fields. -所以在面试中,如若有跟目标岗位匹配度很高的经历和项目,可以着重详细介绍。 +Thus, in an interview, if you have experiences and projects that demonstrate a high degree of alignment with the target role, be sure to elaborate on them. -**3、高业绩,有超出岗位的思考** +**3. High Performance and Thinking Beyond the Role** -这点就是可遇不可及,毕竟不是所有人都能够拿着好业绩然后跳槽。但是上一份工作所带来的好业绩,以及在重要项目中的骨干身份会为自己的经历加分。 +This aspect is somewhat incidental; not everyone can bring in strong performance and then switch jobs. However, strong performance from a previous job and a core role in key projects can enhance one's background. -同时,如果能在面试中表现出超出岗位本身的能力,更能引起面试官注意。比如具备一定的技术视野,具备良好的规划能力,或者对业务方向有比较深入的见解。这些都能够成为亮点。 +Furthermore, if you can showcase abilities that exceed the basic requirements of the position in an interview, it can significantly attract the interviewer’s attention. For instance, possessing a certain level of technical vision, excellent planning skills, or in-depth insights into business directions can all serve as highlights. -**4、技术深度或广度** +**4. Depth or Breadth of Technical Knowledge** -相信很多人都听过,职场中最受欢迎的是`T`型人才。也就是在拥有一定技术广度的基础上,在自己擅长的领域十分拔尖。这样的人才的确很难得,既要求能够胜任自己的在职工作,又能够不设边界的学习和输出其它领域的知识。 +Many have heard that the most sought-after professionals in the workplace are `T`-shaped talents. This refers to individuals who possess a certain breadth of knowledge while excelling in their area of expertise. Such talents are indeed rare, as they are able to manage their current work while unrestrictedly learning and contributing knowledge from other fields. -除此之外,**比 T 型人才更为难得是所谓 π 型人才,相比于 T 型人才,有了不止一项拔尖的领域。这类人才更是公司会抢占的资源。** +Moreover, **even rarer than `T`-shaped talents are the so-called π-shaped talents, who possess expertise in more than one area of proficiency. Such talents are highly coveted resources by companies.** -## 总结 +## Conclusion -面试虽说是考察和筛选优秀人才的过程,但说到底还是人与人沟通并展现自我的方式。所以掌握有效面试的技巧也是帮助自己收获更多的工具。 +Although interviews are processes for evaluating and selecting outstanding talents, they fundamentally remain a way for people to communicate and present themselves. Therefore, mastering effective interview techniques helps you gain more advantages. -这篇文章其实算讲的是方法论,很多我们一看就明白的「道理」实施起来可能会很难。可能会遇到一个不按常理出牌的面试官,也可能也会遇到一个沟通困难的面试官,当然也可能会撞上一个不怎么匹配的岗位。 +This article primarily discusses methodologies; many "principles" we recognize as clear may be challenging to implement. You might encounter an interviewer who behaves unconventionally, or face difficulties in communication with them, not to mention potentially landing interviews for roles that aren’t a good fit. -总而言之,为了自己想要争取的东西,做好足够的准备总是没有坏处的。祝愿大家能成为`π`型人才,获得想要的`offer`! +In summary, in pursuit of what you desire, being well-prepared is never a disadvantage. I wish everyone can become `π`-shaped talents and secure the desired `offers`! diff --git a/docs/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.md b/docs/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.md index 0af5480b58e..e1a76bd8789 100644 --- a/docs/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.md +++ b/docs/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.md @@ -1,234 +1,48 @@ --- -title: 一个中科大差生的 8 年程序员工作总结 -category: 技术文章精选集 -author: 陈小房 +title: An 8-Year Work Summary of a Poor Student from USTC +category: Selected Technical Articles +author: Chen Xiaofang tag: - - 个人经历 + - Personal Experience --- -> **推荐语**:这篇文章讲述了一位中科大的朋友 8 年的经历:从 2013 年毕业之后加入上海航天 x 院某卫星研究所,再到入职华为,从华为离职。除了丰富的经历之外,作者在文章还给出了很多自己对于工作/生活的思考。我觉得非常受用!我在这里,向这位作者表达一下衷心的感谢。 +> **Recommendation**: This article tells the story of a friend from USTC over the past 8 years: from graduating in 2013 and joining a satellite research institute at Shanghai Aerospace X Academy, to working at Huawei and then leaving. Besides the rich experiences, the author also shares many thoughts on work and life. I find it very useful! Here, I would like to express my heartfelt thanks to the author. > -> **原文地址**: +> **Original Article Link**: ---- - -## 前言 - -今年终于从大菊花厂离职了,离职前收入大概 60w 不到吧!在某乎属于比较差的,今天终于有空写一下自己的职场故事,也算是给自己近 8 年的程序员工作做个总结复盘。 - -近 8 年有些事情做对了,也有更多事情做错了,在这里记录一下,希望能够给后人一些帮助吧,也欢迎私信交流。文笔不好,见谅,有些细节记不清了,如果有出入,就当是我编的这个故事吧。 - -_PS:有几个问题先在这里解释一下,评论就不一一回复了_ - -1. 关于差生,我本人在科大时确实成绩偏下,差生主要讲这一点,没其他意思。 -2. 因为买房是我人生中的大事,我认为需要记录和总结一下,本文中会有买房,房价之类的信息出现,您如果对房价,炒房等反感的话,请您停止阅读,并且我再这里为浪费您的时间先道个歉。 - -## 2013 年 - -### 加入上海航天 x 院某卫星研究所 - -本人 86 年生人,13 年从中科大软件相关专业毕业,由于父母均是老师,从小接受的教育就是努力学习,找个稳定的“好工作”,报效国家。 - -于是乎,毕业时候头脑一热加入了上海航天 x 院某卫星研究所,没有经过自己认真思考,仅仅听从父母意见,就草率的决定了自己的第一份工作,这也为我 5 年后离职埋下了隐患。这里总结第一条经验: - -**如果你的亲人是普通阶层,那对于人生中一些大事来说,他们给的建议往往就是普通阶层的思维,他们的阶层就是他们一生思维决策的结果,如果你的目标是跳出本阶层,那最好只把他们的建议当成参考。** - -13 年 4 月份,我坐上火车来到上海,在一路换乘地铁来到了大闵行,出了地铁走路到单位,一路上建筑都比较老旧,我心里想这跟老家也没什么区别嘛,还大上海呢。 - -到达单位报道,负责报道的老师很亲切,填写完资料,分配了一间宿舍,还给了大概 3k 左右安家费,当时我心里那个激动啊(乡下孩子没有见过钱啊,见谅),拿了安家费,在附近小超市买好生活用品,这样我就开始了自己航天生涯。 - -经过 1 个月集中培训后,我分配到部门,主要负责卫星上嵌入式软件开发。不过说是高大上的卫星软件开发,其实刚开始就是打杂,给实验室、厂房推箱子搬设备,呵呵,说航天是个体力活相信很多航天人都有同感吧。不过当时年轻,心思很单纯,每天搬完设备,晚上主动加班,看文档材料,画软件流程图,编程练习,日子过得很充实。 - -记得第一个月到手大概 5k 左右(好少呀),当时很多一起入职的同事抱怨,我没有,我甚至不太愿意和他们比较工资,这里总结第二条经验: - -**不要和你的同事比工资,没有意义,比工资总会有人受伤,更多的是负面影响,并且很多时候受伤的会是你。** - -### 工作中暂露头角 - -工作大概一个月的时候,我遇到了一件事情,让我从新员工里面开始暂露头角。事情是这样的当时国家要对军工单位进行 GJB5000A 软件开发等级认证(搞过这个认证的同学应该都知道,过这个认证那是要多酸爽有多酸爽),但是当时一个负责配置管理的同事却提出离职,原因是他考上了公务员,当时我们用的那个软件平台后台的版本控制是 SVN 实现的,恰好我在学校写程序时用过,呵呵,话说回来现在学生自己写软件很少有人会在本地搭版本控制器吧!我记得当时还被同学嘲笑过,这让我想起了乔布斯学习美术字的故事,这里总结一下: - -**不要说一项技能没有用,任何你掌握的技能都有价值,但是你要学会找到发挥它的场景。如果有一天你落水了,你可能会很庆幸,自己以前学会了游泳。** - -**工作中如果要上升,你要勇于承担麻烦的、有挑战的任务,当你推掉麻烦的时候,你也推掉了机遇。** - -好了,扯远了,回到前面,当时我主动跟单位认证负责人提出,我可以帮忙负责这方面的工作,我有一定经验。这里要提一下这个负责人,是位女士,她是我非常敬佩的一个前辈,认真,负责,无私,整个人为国家的航天事业奉献了几十年,其实航天领域有非常多这样的老前辈,他们默默奋斗,拿着不高的薪水,为祖国的国防建设做出了巨大的贡献。当时这位负责人,看我平时工作认真积极,思维反应也比较灵活(因为过认证需要和认证专家现场答辩的)就同意了我的请求,接受到这个任务之后,我迅速投入,学习认证流程、体系文件、迅速掌握认证工作要点,一点一点把相关的工作做好,同时周期性对业务进行复盘,总结复盘可能是我自己的一个优点: - -**很多人喜欢不停的做事,但不会停下来思考,缺乏总结复盘的能力,其实阶段性总结复盘,不仅能够固化前面的经验,也能梳理后面的方向;把事情做对很重要,但是更重要的是做对的事;另外不要贪快,方向正确慢就是快**(后半句是我后来才想明白的,如果早想明白,就不会混成目前这样了) - -1 个月后,当时有惊无险通过了当年的认证,当时负责人主动向单位申请了 2k 特别奖,当时我真的非常高兴,主要是自己的工作产生了价值,得到了认可。后来几个月的日子平淡无奇,有印象的好像只有两件事情。 - -一件事情是当年端午,当时我们在单位的宿舍休息,突然楼道上一阵骚动,我打开宿舍门一看,原来是书记来慰问,还给每个人送了一箱消暑饮料,这件事印象比较深刻,是我觉得国企虽然有各种各样的问题,但是论人文关怀,还是国企要好得多。 - -### 错失一次暴富的机会 - -另一件事是当年室友刚买房,然后天天研究生财&之道,一会劝我买房,一会劝我买比&特&币,我当时没有鸟他,为什么呢,因为当时的室友生活习惯不太好,会躺在床上抽烟,还在宿舍内做饭(我们宿舍是那种很老的单位房,通风不好),我有鼻炎,所以不是很喜欢他(嗯,这里要向室友道歉,当年真是太幼稚了)。现在 B&T&C4 万美元了,我当时要是听了室友也能小发一笔了(其实我后来 18 年买了,但是没有拿住这是后话),这里要总结一下: - -**不要因为某人的外在,如外貌、习惯、学历等对人贴上标签,去盲目否定别人,对于别人的建议,应该从客观出发,综合分析,从善如流是一项非常难得的品质。** - -**人很难挣到他认知之外的财富,就算偶然拿到了,也可能很快失去。所以不要羡慕别人投机获得的财富,努力提升你的思维,财商才是正道。** - -### 航天生涯的第一个正式项目 - -转眼到了 9 月份(我 4 月份入职的),我迎来了我航天生涯第一个正式的型号项目(型号,是军工的术语,就相当于某个产品系列比如华为的 mate),当时分配给我的型号正式启动,我终于可以开始写卫星上的代码了。 - -当时真的是心无旁骛,一心投身军工码农事业,每天实验室,测试厂房,评审会,日子虽然忙碌,但是也算充实。并且由于我的努力工作,加上还算可以的技术水平,我很快就能独立胜任一些型号基础性的工作了,并且我的努力也受到了型号(产品)线的领导的认可,他们开始计划让我担任型号主管设计师,这是一般工作 1-2 年的员工的岗位,当时还是有的激动的。 - -## 2014 年 - -### 升任主管设计师后的一次波折 - -转眼间到 2014 年了,大概上半年吧,我正式升任主管设计师,研发工作上也开时独挡一面了,但是没多久产品研发就给了我当头一棒。 - -事情是这样的,当时有一个版本软件编写完毕,加载到整星上进行测试,有一天大领导来检查,当时非常巧,领导来时测试主岗按某个岗位的人员要求,发送了一串平时整星没有使用的命令(我在实验室是验证过的),结果我的软件立刻崩溃,无法运行。由于正好领导视察,这个问题立马被上报到质量处,于是我开始了苦逼的技术归零攻关(搞航天的都懂,我就不解释了)。 - -期间每天都有 3 个以上领导,询问进度,当时作为新人的我压力可想而知,可是我无论如何都查不出来问题,在实验室我的软件完全正常!后来,某天中午我突然想到,整星上可能有不同的环境,具体过程就不说了。后人查出来是一个负责加载我软件的第三方软件没有受控,非法篡改了我程序的 4 个字节,而这 4 字节正好是那天发送命令才会执行的代码,结果导致我的软件崩溃。最后我花了进一个月完成了所有质量归零报告,技术分析报告,当然负责技术的领导没有责怪,相反的还更加看重我了,后来我每遇到一个质量问题,无论多忙最后定要写一份总结分析报告,这成了我一个技术习惯,也为后来我升任软件开发组长奠定了技术影响基础。 - -**强烈建议技术团队定期开展质量回溯,需要文档化,还要当面讲解,深入的技术回溯有助于增加团队技术交流活跃度,同时提升团队技术积淀,是提升产品质量,打造优秀团队的有效方法。** - -**个人的话建议养成写技术总结文章的习惯,这不仅能提升个人技术,同时分享也可以增加你的影响力** - -### 职场软技能的重新认识 - -上半年就在忙碌中度过了,到了年底,发生了一件对我们组影响很大的事情,年底单位开展优秀小组评比, 其实这个很多公司都有,那为什么说对我们组影响很大内,这里我先卖关子。这里先不得不提一个人,是个女孩子,南京大学的,比我晚来一年,她做事积极,反应灵敏,还做得一手不错的 PPT,非常优秀,就是黑了点(希望她看到了不要来找我,呵呵)。 - -当时单位开展优秀小组评比,我们当时是新员工,什么都很新鲜,就想参加一下,当时领导说我们每年都参加的,我们问,我们每年做不少东西,怎么没有看到过评比的奖状,领导有点不好意思,说我们没有进过决赛。我们又问,多少名可以进入决赛圈,答曰前 27 名即可(总共好像 50+个组)我们当时心里真是一万个羊驼跑过。。。。 - -其实当时我们组每年是做不少事情的,我们觉得我们不应该排名如此之低,于是我们几个年轻人开始策划,先是对我们的办公室彻底改造(因为要现场先打分,然后进决赛),然后好好梳理了我们当年取得的成绩,现场评比时我自告奋勇进行答辩(我沟通表达能力还不错,这也算我一个优势吧),后面在加上前文提到的女孩子做的漂亮 PPT,最后我们组拿到了铜牌班组的好成绩,我也因为这次答辩的优秀表现在领导那里又得到了认可,写了大半段了,再总结一下: - -**职场软技能如自我展示很重要,特别是程序员,往往在这方面是个弱项,如果可以的话,可以通过练习,培训强化一下这些软技能,对职场的中后期非常有帮助。** - -## 2015 年 - -时间总是过得很快,一下就到 2015 年了,这一年发生了一件对我影响很大的事情。 - -### 升任小组副组长 - -当时我们小组有 18 个人了,有一天部门开会,主任要求大家匿名投票选副组长(当时部门领导还是很民主的),因为日常事务逐渐增多,老组长精力有限,想把一些事物分担出来,当天选举结果就出来了,我由于前面的技术积累和沟通表达能力的展现,居然升任副组长,当时即有些意外,因为总是有传言说国企没背景一辈子就是最最底层,后来我仔细思考过,下面是我不成熟的想法: - -**不要总觉得国企事业单位的人都是拼背景,拼关系,我承认存在关系户,但是不要把关系户和低能力挂钩,背景只是一个放大器,当关系户做出了成绩时它会正面放大影响,当关系户做了不光彩的事情是,它也会让影响更坏。没有背景,你可以作出更大的贡献来达到自己的目标,你奋斗的过程是更大的财富。另外,我遇到的关系户能力都很强,也可能是巧合,也可能是他们的父辈给给他们在经验层次上比我们更优秀的教育。** - -### 学习团队管理技巧 - -升任副组长后,我的工作更加忙碌了,不仅要做自己项目的事情,还要横向管理和协调组内其他项目的事情,有人说要多体谅军工单位的基层班组长,这话真是没错啊。这个时候我开始学习一些管理技巧,如何凝聚团队,如何统一协调资源等等,这段时间我还是在不断成长。不过记得当年还是犯了一个很大的方向性错误,虽然更多的原因可能归结为体制吧,但是当时其实可以在力所能及的范围内做一些事情的。 - -具体是这样的,当时管理上有项目线,有行政线,就是很常见的矩阵式管理体系,不过在这个特殊的体制下面出现了一些问题。当时部门一把手被上级强制要求不得挂名某个型号,因为他要负责部门资源调配,而下面的我们每个人都归属 1-2 个型号(项目),在更高层的管理上又有横向的行政线(不归属型号),又有纵向的型号管理线。 - -而型号的任务往往是第一线的,因为产品还是第一位的,但是个人的绩效、升迁又归属行政线管理,这种形式在能够高效沟通的民企或者外企一般来说不是问题,但是在沟通效率缓慢,还有其他掣肘因素的国企最终导致组内每个人忙于自身的型号任务,各自单打独斗,无法聚焦,一年忙到头最终却得不到部门认可,我也因为要两面管理疲于应付,后来曾经反思过,其实可以聚焦精力打造通用平台(虽然这在我们行业很难)部分解决这个问题: - -**无论个人还是团队,做事情要聚焦,因为个人和团队资源永远都是有限的,如果集中一个事情都做不好,那分散就更难以成功,但是在聚焦之前要深入思考,往什么方向聚焦才是正确的,只有持续做正确的事情才是最重要的。** - -## 2016 年 - -这一年是我人生的关键一年,发生了很多事情。 - -### 升任小组副组长 - -第一件事情是我正式升任组长,由于副组长的工作经验,在组长的岗位上也做得比较顺利,在保证研发工作的同时,继续带领团队连续获得铜牌以上班组奖励,另外各种认证检查都稳稳当通过,但是就在这个时候,因为年轻,我犯下了一个至今非常后悔的错误。 - -大概是这样的,我们部门当时有两个大组,一个是我们的软件研发组,一个是负责系统设计的系统分析组。 - -当时两个组的工作界面是系统组下发软件任务书给软件组,软件组依照任务书开发,当时由于历史原因,软件组有不少 10 年以上的老员工,而系统组由于新成立由很多员工工作时间不到 2 年,不知道从什么时候起,也不知道是从哪位人员开始,软件组的不少同事认为自己是给系统组打工的。并且,由于系统组同事工作年限较短,实际设计经验不足,任务书中难免出现遗漏,从而导致实际产品出错,两组同事矛盾不断加深。 - -最后,出现了一个爆发:当时系统组主推一项新的平台,虽然这个平台得到了行政线的支持,但是由于军工产品迭代严谨,这个新平台当时没有型号愿意使用,同时平台的部分负责人,居然没有完整的型号经验!由于这个新平台的软件需要软件组实现,但是因为已经形成的偏见,软件同事认为这项工作中自己是为利益既得者打工。 - -我当时也因为即负责实际软件开发,又负责部分行政事务,并且年轻思想不成熟,也持有类似的思想。过程中的摩擦、冲突就不说了,最后的结果是系统组、软件组多人辞职,系统组组长离职,部门主任离职创业(当然他们辞职不全是这个原因,包括我离职也不全是这个原因,但是我相信这件事情有一定的影响),这件事情我非常后悔,后来反思过其实当时自己应该站出来,协调两组矛盾,全力支持部门技术升级,可能最终就不会有那么多优秀的同事离开了。 - -**公司战略的转型,技术的升级迭代,一定会伴随着阵痛,作为基层组织者,应该摒弃个人偏见,带领团队配合部门、公司主战略,主战略的成功才是团队成功的前提。** - -### 买房 - -16 年我第二件大事情就是买房,关注过近几年房价的人都可能还记得,16 年一线城市猛涨的情景。其实当时 15 年底,上海市中心和学区房已经开始上涨,我 15 年底听同事开始讨论上涨的房价,我心里开始有了买房的打算,大约 16 春节(2 月份吧,具体记不得了),我回老家探望父母,同时跟他们提出了买房的打算。 - -我的父亲是一个“央视新闻爱好者”,爱好看狼咸平,XX 刀,XX 檀的节目,大家懂了吧,父亲说上海房价太高了,都是泡沫,不要买。这个时候我已经不是菜鸟了,我想起我总结的第一条经验(见上文),我开始收集往年的房价数据,中央历年的房价政策,在复盘 15 年的经济政策时我发现,当年有 5 次降息降准,提升公积金贷款额度,放松贷款要求于是我判定房价一定会继续涨,涨到一个幅度各地才会出台各种限购政策,并且房价在城市中是按内环往外涨的于是我开始第一次在人生大事上反对父母,我坚决表态要买房。父亲还是不太同意,他说年底吧,先看看情况(实际是年底母亲的退休公积金可以拿出来大概十几万吧,另外未来丈母娘的公积金也能拿出来了大概比这多些)。我还是不同意,父亲最终拗不过我,终于松口,于是我们拿着双方家庭凑的 50w 现金开始买房,后来上海的房价大家都看到了。这件事也是我做的不多的正确的事情之一。 - -但是最可笑的是,我研究房价的同时居然犯下了一个匪夷所思的错误,我居然没有研究买房子最重要的因素是什么,我们当时一心想买一手房(现在想想真是脑子进水),最后买了一套松江区交通不便的房子,这第一套房子的地理位置也为我后来第二次离职埋下了隐患,这个后面会说。 - -**一线或者准一线城市能买尽量买,不要听信房产崩溃论,如果买不起,那可以在有潜力的城市群里用父母的名义先买一套,毕竟大多数人的财富其实是涨不过通货膨胀的。另外买房最重要的三个要素是,地段,地段,地段。** - -买房的那天上午和女朋友领的证,话说当时居然把身份证写错了三次 。。。 - -这下我终于算是有个家了,交完首付那个时候身上真的是身无分文了。航天的基层员工的收入真的是不高,我记得我当时作为组长,每月到手大概也就 7k-8k 的样子,另外有少量的奖金,但是总数仍然不高,好在公积金比较多,我日常也没什么消费欲望,房贷到是压力不大。 - -买完房子之后,我心里想,这下真的是把双方家庭都掏空了(我们双方家庭都比较普通,我的收入也在知乎垫底,没办法)万一有个意外怎么办,我思来想去,于是在我下一个月发工资之后,做了一个我至今也不知道是对是错的举动,我利用当月的工资,给全家人家人买了保险保险,各种重疾,意外都配好了。但是为什么我至今也不知道对错呢,因为后来老丈人,我母亲都遭遇病魔,但是两次保险公司都拒赔,找出的理由我真是哑口无言,谁叫我近视呢。另外真的是要感谢国家,亲人重病之后,最终还是走了医保,赔偿了部分,不然真的是一笔不小的负担。 - -## 2017 年 - -对我人生重大影响的 2016 年,在历史的长河中终究连浪花都激不起来。历史长河静静流淌到了 2017 年,这一年我参加了中国深空探测项目,当然后面我没有等到天问一号发射就离开了航天,但是有时候仰望星空的时候,想想我的代码正在遥远的星空发挥作用,心里也挺感慨的,我也算是重大历史的参与者了,呵呵。好了不说工作了,平淡无奇的 2017 年,对我来说也发生了两件大事。 - -### 买了第二套房子 - -第一件事是我买了第二套房子,说来可笑,当年第一套房子都是掏空家里,这第二年就买了第二套房子,生活真的是难以捉摸。到 2017 年时,前文说道,我母亲和丈母娘先后退休,公积金提取出来了,然后在双方家里各自办了酒席,酒席之后,双方父母都把所有礼金给了我们,父母对自己的孩子真的是无私之至。当时我们除了月光之外,其实没有什么外债,就是生活简单点。拿到这笔钱后,我们就在想如何使用,一天我在菜市场买菜,有人给我一张 xuanchuan 页,本来对于这样的 xuanchuan 页我一般是直接扔掉的,但是当天鬼死神差我看了一眼,只见上面写着“嘉善高铁房,紧邻上海 1.5w”我当时就石化了,我记得去年我研究上海房价的时候,曾经在网站上看到过嘉善的房价,我清楚的记得是 5-6k,我突然意识到我是不是错过了什么机会,反思一下: - -**工作生活中尽量保持好奇心,不要对什么的持怀疑态度,很多机会就隐藏在不起眼的细节中,比如二十年前有人告诉你未来可以在网上购物,有人告诉你未来可以用手机支付,你先别把他直接归为骗子,静下来想一想,凡事要有好奇心,但是要有自己的判断。** - -于是我立马飞奔回家,开始分析,大城市周边的房价。我分析了昆山,燕郊,东莞,我发现燕郊极其特殊,几乎没有产业,纯粹是承接大城市人口溢出,因此房价成高度波动。而昆山和东莞,由于自身有产业支撑,又紧邻大城市,因此房价稳定上涨。我和妻子一商量,开始了外地看房之旅,后来我们去了嘉善,觉得没有产业支撑,昆山限购,我们又到嘉兴看房,我发现嘉兴房价也涨了很多,但是这里购房的大多数新房,都是上海购房者,入住率比较低,很多都是打算买给父母住的,但是实际情况是父母几乎不在里面住,我觉得这里买房不妥,存在一个变现的问题。于是我开始继续寻找,一天我看着杭州湾的地图,突然想到,杭州湾北侧不行,那南侧呢?南侧绍兴,宁波经济不是更达吗。于是我们目光投向绍兴,看了一个月后,最后在绍兴紧贴杭州的一个区,购买了一套小房子,后来 17 年房价果然如我预料的那样完成中心城市的上涨之后开始带动三四线城市上涨。后来国家出台了大湾区政策,我对我的小房子更有信心了。这里稍微总结一下我个人不成熟的看法: - -**在稳定通胀的时代,负债其实是一种财富。长三角城市群会未来强于珠港澳,因为香港和澳门和深圳存在竞争关系,而长三角城市间更多的是互补,未来我们看澳门可能就跟看一个中等省会城市一样了。** - -### 准备要孩子 - -2017 年的第二件事是,我们终于准备要孩子了,但是妻子怎么也备孕不成功,我们开始频繁的去医院,从 10 元挂号费的普通门诊,看到 200 元,300 元挂号费的专家门诊,看到 600 元的特需门诊,从综合医院看到妇幼医院,从西医看到中医,每个周末不是在医院排队,就是在去医院的路上。最后的诊疗结果是有一定的希望,但是有困难,得到消息时我真的感觉眼前一片黑暗,这种从来在新闻上才能看到了事情居然落到了我们头上,我们甚至开始接触地下 XX 市场。同时越来越高的医疗开销(专家门诊以上就不能报销了)也开始成为了我的负担,前文说了,我收入一直不高,又还贷款,又支付医疗开支渐渐的开始捉襟见肘,我甚至动了卖小房子的打算。 - -## 2018 年 - -前面说到,2017 年开始频繁出入医院,同时项目也越来越忙,我渐渐的开始喘不过气起来,最后医生也给了结论,需要做手术,手术有不小的失败的几率。我和妻子商量后一咬牙做吧,如果失败就走地下的路子,但是可能需要准备一笔钱(手术如果成功倒是花销不会太大),哎,古人说一分钱难倒英雄汉,真是诚不欺我啊,这个时候我已经开始萌生离职的想法了。怎么办呢,生活还是要继续,我想起了经常来单位办理贷款的银行人员,贷款吧,这种事情保险公司肯定不赔的嘛,于是我办理了一笔贷款,准备应急。 - -### 项目结束,离职 - -时间慢慢的时间走到了 8 月份,我的项目已经告一定段落,一颗卫星圆满发射成功,深空项目也通过了初样阶段我的第一份工作也算有始有终了。我开始在网上投递简历,我技术还算可以,沟通交流也不错,面试很顺利,一个月就拿到了 6 个 offer,其中就有大菊花厂的 offer,定级 16A,25k 月薪后来政策改革加了绩效工资 6k(其实我定级和总薪水还是有些偏低了和我是国企,本来总薪水就低有很大关系,话说菊花厂级别后面真的是注水严重,博士入职轻松 17 级)菊花厂的 offer 审批流程是我见过最长,我当时的接口人天天催于流程都走了近 2 个月。我向领导提出了离职,离职的过程很痛苦,有过经历的人估计都知道,这里就不说了。话说我为什么会选择华为呢,一是当时急需钱,二是总觉得搞嵌入式的不到华为看看真的是人生遗憾。现在想想没有认真去理解公司的企业文化就进入一家公司还是太草率了: - -**如果你不认同一个公司的企业文化,你大概率干不长,干不到中高层,IT 人你不及时突破到中高层很快你就会面临非常多问题;公司招人主要有两种人,一种是合格的人,一种是合适的人,合格的人是指技能合格,合适的人是指认同文化。企业招人就是先把合格的人找进来,然后通过日日宣讲,潜移默化把不合适的人淘汰掉。** - -### 入职华为 - -经过一阵折腾终于离职成功,开始入职华为。离职我做了一件比较疯狂的事情,当时因为手上有一笔现金了,一直在支付利息,心里就像拿它干点啥。那时由于看病,接触了地下 XX 市场,听说了 B&TC,走之前我心一横买 B&T&C,后来不断波动,最终我还是卖了,挣了一些钱,但是最终没有拿到现在,果然是考验人性啊。 - -## 2019 年 - -### 成功转正 - -华为的试用期真长,整整 6 个月,每个月还有流程跟踪,交流访谈,终于我转正了,转正答辩我不出意料拿到了 Excellent 评价,涨了点薪水,呵呵还不错。华为的事情我不太想说太多,总之我觉得自己没有资格评判这个公司,从公司看公司的角度华为真正是个伟大的公司,任老爷子也是一个值得敬佩的企业家。 - -在华为干了半年后,我发现我终究还是入职的时候太草率了,我当时没有具体的了解这个岗位,这个部门。入职之后我发现,**我所在的是硬件部门,我在一个硬件部门的软件组,我真是脑子秀逗了**。 - -**在一个部门,你需要尽力进入到部门主航道里,尽力不要在边缘的航道工作,特别是那些节奏快,考核严格的部门。** - -更严峻的是我所在的大组,居然是一个分布在全国 4 地的组,大组长(华为叫 LM)在上海,4 地各有一个本地业务负责人。我立刻意识到,到年终考评时,所有的成果一定会是 4 地分配,并且 4 地的负责人会占去一大部分,这是组织结构形成的优势。我所在的小组到时候会难以突破,资源分配会非常激烈。 - -### 备孕成功 +______________________________________________________________________ -先不说这些,在 18 年时妻子做完了手术,手术居然很成功。休息完之后我们 19 年初开始备孕了,这次真的是上天保佑,运气不错,很快就怀上了。这段时间,我虽然每天做地铁 1.5 小时到公司上班,经受高强度的工作,我心里每天还是乐滋滋的。但是,突然有一天,PL(华为小组长)根我说,LM 需要派人去杭研所支持工作,我是最合适人选,让我有个心里准备。当时我是不想去的,这个时候妻子是最需要关怀的时候,我想 LM 表达了我的意愿,并且我也知道如果去了杭州年底绩效考评肯定不高。过程不多说了,反正结果是我去了杭州。 +## Preface -于是我开始了两头奔波的日子,每个月回上海一趟。这过程中还有个插曲,家里老家城中村改造,分了一点钱,父母执意卖掉了老家学校周边的房子,丈母娘也处理老家的一些房子,然后把钱都给了我们,然后我用这笔家里最后的资产,同时利用华为的现金流在绍、甬不限购地区购买一些房子,我没有炒房的想法,只是防止被通货膨胀侵蚀而已,不过后来结果证明我貌似又蒙对了啊,我自己的看法是: +This year, I finally left the big chrysanthemum factory, with a salary of about 600,000 before leaving! In some circles, this is considered quite poor. Today, I finally have time to write about my career story, which serves as a summary and reflection on my nearly 8 years as a programmer. -**杭绍甬在经济层面会连成紧密的一片,在行政区上杭州兼并绍 部分区域的概率其实不大,行政区的扩展应该是先兼并自身的下级代管城市。** +In these 8 years, I have done some things right, but even more things wrong. I hope to record them here to help future generations, and I welcome private messages for communication. My writing may not be great, so please forgive me if some details are unclear; if there are discrepancies, just consider it part of the story I’m telling. -### 宝宝出生 +_PS: I would like to clarify a few questions here, so I won’t respond to comments one by one._ -不说房子了,继续工作吧。10 月份干了快一年时候,我华为的师傅(华为有师徒培养体系)偷偷告诉我被定为备选 PL 了,虽然不知道真假,但是我心里还是有点小高兴。不过我心里也慢慢意识到这个公司可能不是我真正想要的公司,这么多年了,愚钝如我慢慢也开始知道自己想干什么了。因为我的宝宝出生了,看着这只四脚吞金兽,我意识到自己已经是一个父亲了。 +1. Regarding being a poor student, I did indeed have lower grades at USTC. This is mainly what I mean by "poor student," with no other implications. +1. Buying a house is a significant event in my life, and I believe it needs to be recorded and summarized. There will be information about buying a house and housing prices in this article. If you are averse to housing prices or speculation, please stop reading, and I apologize for wasting your time. -2019 年随着美国不断升级的制裁消息,我在华为的日子也走到年底,马上将迎来神奇的 2020 年。 +## 2013 -## 2020 +### Joining a Satellite Research Institute at Shanghai Aerospace X Academy -> 2020 就少写一些了,有些东西真的可能忘却更好。 +I was born in 1986 and graduated from USTC with a software-related major in 2013. Since both of my parents are teachers, I was educated from a young age to study hard, find a stable "good job," and serve the country. -### 在家办公 +So, at graduation, I impulsively joined a satellite research institute at Shanghai Aerospace X Academy without serious consideration, merely following my parents' advice, which laid the groundwork for my departure five years later. Here’s my first piece of advice: -年初就给大家来了一个重击,新冠疫情改变了太多的东西。这个时候真的是看出华为的执行力,居家办公也效率不减多少,并且迅速实现了复工。到了 3-4 月份,华为开始正式评议去年绩效等级,我心里开始有预感,以前的分析大概率会兑现,并且绩效和收入挂钩,华为是个风险意识极强的公司,去年的制裁会导致公司开始风险预备,虽然我日常工作还是受到多数人好评,但是我知道这其实在评议人员那里,没有任何意义。果然绩效评议结果出来了,呵呵,我很不满意。绩效沟通时 LM 破例跟我沟通了很长时间,我直接表达了我的想法。LM 承诺钱不会少,呵呵,我不评价吧。后来一天开始组织调整,成立一个新的小组,LM 给我电话让我当组长,我拒绝了,这件事情我不知道对错,我当时是这样考虑的 +**If your family members are from an ordinary background, their advice on significant life events often reflects the mindset of the ordinary class. Their class is the result of their life decisions. If your goal is to rise above this class, it’s best to treat their advice as a reference only.** -1. **升任新的职位,未必是好事,更高的职位意味着更高的要求,因此对备选人员要么在原岗位已经能力有余,要么时间精力有余;我认为当时我这两个都不满足,呵呵离家有点远,LM 很可以只是因为绩效事情做些补偿。** -2. **华为不会垮,这点大家有信心,但未来一定会出现战略收缩,最后这艘大船上还剩下哪些人不清楚,底层士兵有可能是牺牲品。** -3. **我 34 岁了** +In April 2013, I took a train to Shanghai, transferred to the subway, and arrived at the old Minhang area. Walking from the subway to the workplace, I noticed the buildings were quite old, and I thought to myself, "This is no different from my hometown, even though it’s Shanghai." -### 提出离职 +Upon arriving at the workplace, the teacher in charge of onboarding was very friendly. After filling out the paperwork, I was assigned a dormitory and given about 3,000 yuan as a relocation allowance. I was so excited at the time (a country kid who had never seen much money, please forgive me). With the relocation allowance, I bought daily necessities at a nearby supermarket, and thus began my aerospace career. -另外我一直思考未来想做什么,已经有了一丝眉目,就这样,我拿了年终奖约 7 月就提出了离职,后来部门还让我做了最后一次贡献,把我硬留到 10 月份,这样就可以参加上半年考核了,让帮忙背了一个 C 呵呵,这是工作多年,最差绩效吧。 +After a month of intensive training, I was assigned to a department, mainly responsible for embedded software development on satellites. Although it was called high-end satellite software development, I initially did a lot of menial tasks, like moving equipment in the lab and factory. Many people in the aerospace field can relate to the physical nature of the work. However, being young and naive, I worked hard every day, voluntarily putting in overtime, reading documents, drawing software flowcharts, and practicing programming. Life felt very fulfilling. -这里还有一个小插曲,最后这三个月我负责什么工作呢,因为 20 年 3 月开始我就接手了部分部门招聘工作(在华为干过的都知道为什么非 HR 也要帮忙招聘,呵呵大坑啊,就不多解释了),结果最后三个月我这个待离职员工居然继续负责招聘,真的是很搞笑,不过由于我在上一份工作中其实一直也有招聘的工作,所以也算做的轻车熟路,每天看 50 份左右简历(我看得都非常仔细,我害怕自己的疏忽会导致一个优秀的人才错失机会,所以比较慢)其实也蛮有收货,最后好歹对程序员如何写简历有了一些心得。 +I remember my first month's salary was about 5,000 yuan (so little!). Many of my colleagues who joined at the same time complained, but I didn’t. I was even reluctant to compare salaries with them. Here’s my second piece of advice: -## 总结 +**Don’t compare salaries with your colleagues; it’s meaningless. There will always be someone hurt by salary comparisons, and more often than not, it will be you who gets hurt.** -好了 7 年多,近 8 年的职场讲完了,不管过去如何,未来还是要继续努力,希望看到这篇文章觉得有帮助的朋友,可以帮忙点个推荐,这样可能更多的人看到,也许可以避免更多的人犯我犯的错误。另外欢迎私信或者其他方式交流(某 Xin 号,jingyewandeng),可以讨论职场经验,方向,我也可以帮忙改简历(免费啊),不用怕打扰,能帮助别人是一项很有成绩感的事,并且过程中也会有收获,程序员也不要太腼腆呵呵 +### Starting to Stand Out at Work - +About a month into my job, I encountered an event that allowed me to start standing out among the new employees. The situation was that the country was going to conduct GJB5000A software development level certification for military enterprises (those who have gone through this certification know how rewarding it is). However, a colleague responsible for configuration management decided to leave because he passed the civil service exam. At that time, we were using a software diff --git a/docs/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.md b/docs/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.md index 4630bff560e..0710e7bc1ec 100644 --- a/docs/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.md +++ b/docs/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.md @@ -1,109 +1,111 @@ --- -title: 从校招入职腾讯的四年工作总结 -category: 技术文章精选集 +title: A Four-Year Work Summary from Campus Recruitment to Tencent +category: Selected Technical Articles author: pioneeryi tag: - - 个人经历 + - Personal Experience --- -程序员是一个流动性很大的职业,经常会有新面孔的到来,也经常会有老面孔的离开,有主动离开的,也有被动离职的。 +The profession of a programmer has a high turnover rate, with new faces often arriving and familiar ones leaving, some departing voluntarily while others are laid off. -再加上这几年卷得厉害,做的事更多了,拿到的却更少了,互联网好像也没有那么香了。 +Moreover, the competition has intensified in recent years, leading to a greater workload but less reward, making the internet industry seem less appealing than before. -人来人往,变动无常的状态,其实也早已习惯。 +The constant ebb and flow of people and the unpredictability of circumstances have become something I am accustomed to. -打工人的唯一出路,无外乎精进自己的专业技能,提升自己的核心竞争力,这样无论有什么变动,走到哪里,都能有口饭吃。 +The only way for employees to succeed is to refine their professional skills and enhance their core competitiveness, so that no matter what changes occur, they will always be able to make a living. -今天分享一位博主,校招入职腾讯,工作四年后,离开的故事。 +Today, I want to share the story of a blogger who joined Tencent through campus recruitment and left after four years of work. -至于为什么离开,我也不清楚,可能是有其他更好的选择,或者是觉得当前的工作对自己的提升有限。 +As for why he left, I am unclear; it may be due to better opportunities or a perception that his current job offered limited personal growth. -**下文中的“我”,指这位作者本人。** +**In the following text, "I" refers to this author.** -> 原文地址: +> Original link: -研究生毕业后, 一直在腾讯工作,不知不觉就过了四年。个人本身没有刻意总结的习惯,以前只顾着往前奔跑了,忘了停下来思考总结。记得看过一个职业规划文档,说的三年一个阶段,五年一个阶段的说法,现在恰巧是四年,同时又从腾讯离开,该做一个总结了。 +After graduating from graduate school, I have been working at Tencent, and unconsciously, four years have passed. I personally do not have the habit of intentionally summarizing, as I have only focused on moving forward and forgot to pause and reflect. I remember reading a career planning document that mentioned the idea of three-year phases and five-year phases; right now, it just so happens to be four years, and since I'm leaving Tencent, it's time to summarize. -先对自己这四年做一个简单的评价吧:个人认为,没有完全的浪费和辜负这四年的光阴。为何要这么说了?因为我发现和别人对比,好像意义不大,比我混的好的人很多;比我混的差的人也不少。说到底,我只是一个普普通通的人,才不惊人,技不压众,接受自己的平凡,然后看自己做的,是否让自己满意就好。 +First, let's evaluate my four years simply: I believe that this time has not been completely wasted or squandered. Why do I say this? Because I have realized that comparing myself to others seems meaningless; there are many who are doing better than I am, and there are also quite a few who are not doing as well as I am. Ultimately, I am just an ordinary person, not remarkable, and my skills do not stand out. Accepting my ordinariness and checking whether I am satisfied with what I have done is what matters. -下面具体谈几点吧,我主要想聊下工作,绩效,EPC,嫡系看法,最后再谈下收获。 +Now, let's talk about a few specific points; I mainly want to discuss work, performance, EPC, core culture, and finally my gains. -## 工作情况 +## Work Situation -我在腾讯内部没有转过岗,但是做过的项目也还是比较丰富的,包括:BUGLY、分布式调用链(Huskie)、众包系统(SOHO),EPC 度量系统。其中一些是对外的,一些是内部系统,可能有些大家不知道。还是比较感谢这些项目经历,既有纯业务的系统,也有偏框架的系统,让我学到了不少知识。 +I have not changed roles within Tencent, but I have worked on a variety of projects, including: BUGLY, Distributed Call Chain (Huskie), Crowdsourcing System (SOHO), and EPC Measurement System. Some are external projects, while others are internal systems; perhaps some of you do not know about them. I am quite grateful for these project experiences, as they encompass both pure business systems and framework-oriented systems, allowing me to learn a lot. -接下来,简单介绍一下每个项目吧,毕竟每一个项目都付出了很多心血的: +Next, let me briefly introduce each project, as each one has absorbed a lot of effort: -BUGLY,这是一个终端 Crash 联网上报的系统,很多 APP 都接入了。Huskie,这是一个基于 zipkin 搭建的分布式调用链跟踪项目。SOHO,这是一个众包系统,主要是将数据标准和语音采集任务众包出去,让人家做。EPC 度量系统,这是研发效能度量系统,主要是度量研发效能情况的。这里我谈一下对于业务开发的理解和认识,很多人可能都跟我最开始一样,有一个疑惑,整天做业务开发如何成长?换句话说,就是说整天做 CRUD,如何成长?我开始也有这样的疑惑,后来我转变了观念。 +BUGLY is a terminal crash reporting system that many apps have integrated. Huskie is a distributed call chain tracing project built on Zipkin. SOHO is a crowdsourcing system primarily for outsourcing data standards and voice collection tasks. The EPC Measurement System is a system for measuring development efficiency. Here, I’d like to share my understanding and insights on business development. Many people may have doubts like I initially did—how does one grow while doing business development? In other words, how can one grow while constantly performing CRUD operations? I also had such doubts at first, but later I changed my perspective. -我觉得对于系统的复杂度,可以粗略的分为技术复杂度和业务复杂度,对于业务系统,就是业务复杂度高一些,对于框架系统就是技术复杂度偏高一些。解决这两种复杂度,都具有很大的挑战。 +I believe that the complexity of a system can be roughly divided into technical complexity and business complexity. For business systems, business complexity is higher, while for framework systems, technical complexity is higher. Tackling both types of complexity presents significant challenges. -此前做过的众包系统,就是各种业务逻辑,搞过去,搞过来,其实这就是业务复杂度高。为了解决这个问题,我们开始探索和实践领域驱动(DDD),确实带来了一些帮助,不至于系统那么混乱了。同时,我觉得这个过程中,自己对于 DDD 的感悟,对于我后来的项目系统划分和设计以及开发都带来了帮助。 +The crowdsourcing system I previously worked on involved various business logics, which is indicative of high business complexity. To address this problem, we began to explore and practice Domain-Driven Design (DDD), which indeed provided some assistance and prevented the system from becoming overly chaotic. During this process, my insights into DDD have influenced my later project organization, design, and development. -当然 DDD 不是银弹,我也不是吹嘘它有多好,只是了解了它后,有时候设计和开发时,能换一种思路。 +Of course, DDD is not a silver bullet, and I am not claiming it to be exceptional; it’s just that after understanding it, I can occasionally approach design and development from a different perspective. -可以发现,其实平时咱们做业务,想做好,其实也没那么容易,如果可以多探索多实践,将一些好的方法或思想或架构引入进来,与个人和业务都会有有帮助。 +I have realized that excelling in business is not as easy as one might think. If we can explore more and practice while introducing good methods, ideas, or architectures, it will benefit both the individual and the business. -## 绩效情况 +## Performance Situation -我在腾讯工作四年,腾讯半年考核一次,一共考核八次,回想了下,四年来的绩效情况为:三星,三星,五星,三星,五星,四星,四星,三星。统计一下, 四五星占比刚好一半。 +During my four years at Tencent, assessments are conducted every six months, totaling eight assessments. Reflecting back, my performance ratings over four years were: three stars, three stars, five stars, three stars, five stars, four stars, four stars, and three stars. Statistics show that four and five-star ratings made up exactly half. ![](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/640.png) -PS:还好以前有奖杯,不然一点念想都没了。(现在腾讯似乎不发了) +PS: Luckily, I had trophies in the past; otherwise, there would have been no motivation at all. (It seems Tencent doesn't hand them out anymore). -印象比较深的是两次五星获得经历。第一次五星是工作的第二年,那一年是在做众包项目,因为项目本身难度不大,因此我把一些精力投入到了团队的基础建设中,帮团队搭建了 java 以及 golang 的项目脚手架,又做了几次中心技术分享,最终 Leader 觉得我表现比较突出,因此给了我五星。看来,主动一些,与个人与团队都是有好处的,最终也能获得一些回报。 +What sticks in my mind are two experiences of receiving five-star ratings. The first time was in my second year, while working on the crowdsourcing project. Because the project itself wasn't too challenging, I dedicated some energy to the fundamental construction of the team, helping to build Java and Golang project scaffolding and delivering several technical sharing sessions. Ultimately, my leader felt that I performed quite outstandingly and awarded me five stars. It appears that being proactive can benefit both oneself and the team, leading to rewards in return. -第二次五星,就是与 EPC 有关了。说一个搞笑的事,我也是后来才知道的,项目初期,总监去汇报时,给老板演示系统,加载了很久指标才刷出来,总监很不好意思的说正在优化;过了一段时间,又去汇报演示,结果又很尴尬的刷了很久才出来,总监无赖表示还是在优化。没想到,自己曾经让总监这么丢脸,哈哈。好吧,说一下结果,最终,我自己写了一个查询引擎替换了 Mondrian,之后再也没有出现那种尴尬的情况了。随之而来,也给了好绩效鼓励。做 EPC 度量项目,我觉得自己成长很大,比如抗压能力,当你从零到一搭建一个系统时,会有一个先扛住再优化的过程,此外如果你的项目很重要,尤其是数据相关,那么任何一点问题,都可能让你神经紧绷,得想尽办法降低风险和故障。此外,另一个不同的感受就是,以前得项目,我大多是开发者,而这个系统,我是 Owner 负责人,当你 Owner 一个系统时,你得时刻负责,同时还需要思考系统的规划和方向,此外还需要分配好需求和把控进度,角色体验跟以前完全不一样。 +The second five-star experience was related to EPC. Here's a amusing anecdote: I learned later that during the project's early stages, when the director presented the system to the boss and it took a long time to load the indicators, the director awkwardly noted that they were still optimizing. After a while, during another presentation, it again took a long time to load, and the director helplessly claimed they were still optimizing. I didn’t expect that I had caused the director such embarrassment, haha. Well, let me explain the outcome: ultimately, I developed a query engine to replace Mondrian, and after that, such awkward situations never occurred again. This led to a good performance evaluation. Working on the EPC measurement project, I felt significant growth, particularly in stress resistance; when you build a system from scratch, there is a process of enduring before optimizing. Additionally, if your project is crucial, particularly related to data, every minor issue can keep you on edge, requiring you to think of ways to mitigate risks and failures. Another different feeling was that in previous projects, I was mostly a developer, while in this system, I was the owner and responsible person. Being the owner of a system demands continuous responsibility and requires constant thought about the system's planning and direction, as well as proper task allocation and progress control. This role experience was entirely different from before. -## 谈谈 EPC +## Discussing EPC -很多人都骂 EPC,或者笑 EPC,作为度量平台核心开发者之一,我来谈谈客观的看法。 +Many people criticize EPC or laugh at it. As one of the core developers of the measurement platform, I would like to share my objective perspective. -其实 EPC 初衷是好的,希望通过全方位多维度的研效指标,来度量研发效能各环节的质量,进而反推业务,提升研发效能。然而,最终在实践的过程中,才发现,客观条件并不支持(工具还没建设好);此外,一味的追求指标数据,使得下面的人想方设法让指标好看,最终违背了初衷。 +In fact, the original intention of EPC is good. It aims to measure the quality of various aspects of development efficiency through comprehensive and multidimensional indicators, thereby enhancing business effectiveness. However, in the end, it became evident during implementation that the objective conditions do not support this (the tools are not yet well established). Moreover, the blind pursuit of indicator data led those underneath to find ways to make the indicators look good, ultimately contradicting the initial intention. -为什么,说 EPC 好了,其实如果你仔细了解下 EPC,你就会发现,他是一套相当完善且比较先进的指标度量体系。覆盖了需求,代码,缺陷,测试,持续集成,运营部署各个环节。 +Why, when discussing EPC positively, the truth is that if you closely examine EPC, you will find that it is actually a rather complete and advanced set of measurement indicators. It covers requirements, code, defects, testing, continuous integration, and operational deployment across all aspects. -此外,这个过程中,虽然一些人和一些业务做弊,但绝大多数业务还是做出了改变的,比如微视那边的人反馈是,以前的代码写的跟屎一样,当有了 EPC 后,代码质量好了很多。虽然最后微视还是亡了,但是大厦将倾,EPC 是救不了的,亡了也更不能怪 EPC。 +Additionally, while a few individuals and businesses have manipulated the system, the majority of businesses have made changes, such as feedback from the team at Weishi, where people noted that their previous code quality was poor, but after EPC was introduced, code quality improved significantly. Although Weishi ultimately failed, at that point, the situation was beyond saving, and we cannot blame EPC for its demise. -## 谈谈嫡系 +## Discussing Core Culture -大家都说腾讯,嫡系文化盛行。但其实我觉得在那个公司都一样吧。这也符合事物的基本规律,人们只相信自己信任并熟悉的人。作为领导,你难道会把把重要的事情交给自己不熟悉的人吗? +Many people say that Tencent has a prevalent core culture. However, I think this is similar in any company. This aligns with the basic principle that people only trust those they know and feel familiar with. As a leader, would you hand over important tasks to someone you are not familiar with? -其实我也不知道我算不算嫡系,脉脉上有人问过”怎么知道自己算不算嫡系”,下面有一个回答,我觉得很妙:如果你不知道你是不是嫡系,那你就不是。哈哈,这么说来,我可能不是。 +I actually don't know if I can be considered part of the core culture. Someone once asked on an online platform, "How do you know if you are part of the core culture?" One answer struck me as insightful: "If you don't know whether you are part of the core culture, then you probably aren't." Haha, by this logic, I may not be. -但另一方面,后来我负责了团队内很重要的事情,应该是中心内都算很重要的事,我独自负责一个方向,直接向总监汇报,似乎又有点像。 +On the other hand, later on, I took on some very important responsibilities within the team, likely some of the most important within our center, where I independently managed a direction and reported directly to the director, which seems somewhat similar. -网上也有其他说法,一针见血,是不是嫡系,就看钱到不到位,这么说也有道理。我在 7 级时,就发了股票,自我感觉,还是不错的。我当时以为不出意外的话,我以后的钱途和发展是不是就会一帆风顺。不出意外就出了意外,第二年,EPC 不达预期,部门总经理和总监都被换了,中心来了一个新的的总监。 +There are other viewpoints online as well, bluntly stating that whether you are part of the core culture hinges on monetary gains, which makes sense. When I was at level 7, I received stock options, and I felt pretty good about it. At the time, I thought that, barring any accidents, my future financial prospects and career development would be smooth sailing. But then surprises happened; the following year, EPC did not meet expectations, and the department's general manager and director were replaced, leading to a new director in our center. -好吧,又要重新建立信任了。再到后来,是不是嫡系已经不重要了,因为大环境不好,又加上裁员,大家主动的被动的差不多都走了。 +Well, I had to rebuild trust again. Later on, whether one is part of the core culture became less significant, as with the economic downturn and layoffs, most people left, either voluntarily or involuntarily. -总结一下,嫡系的存在,其实情有可原。怎么样成为嫡系了?其实我也不知道。不过,我觉得,与其思考怎么成为嫡系,不如思考怎么展现自己的价值和能力,当别人发现你的价值和能力了,那自然更多的机会就会给予你,有了机会,只要把握住了,那就有更多的福利了。 +In summary, the existence of core culture is understandable. How does one become part of it? I honestly don’t know. However, I believe that rather than pondering how to join the core culture, it would be more productive to focus on how to demonstrate one’s value and capability. Once others recognize your value and capabilities, more opportunities will naturally come your way. When opportunities arise, just seize them, and there will be more benefits. -## 再谈收获 +## Reflecting on Gains -收获,什么叫做收获了?个人觉得无论是外在的物质,技能,职级;还是内在的感悟,认识,都算收获。 +What does it mean to gain? I believe that both external factors such as material rewards, skills, and levels, as well as internal insights and recognitions, count as gains. -先说一些可量化的吧,我觉得有: +First, I'll mention some quantifiable aspects that I believe I have achieved: -- 级别上,升上了九级,高级工程师。虽然大家都在说腾讯职级缩水,但是有没有高工的能力自己其实是知道的,我个人感觉,通过我这几年的努力,我算是达到了我当时认为的我需要在高工时达到的状态; -- 绩效上,自我评价,个人不是一个特别卷的人,或者说不会为了卷而卷。但是,如果我认定我应该把它做好得,我的 Owner 意识,以及负责态度,我觉得还是可以的。最终在腾讯四年的绩效也还算过的去。再谈一些其他软技能方面: +- In terms of level, I have risen to level nine, becoming a senior engineer. Although people say that Tencent has reduced level allocations, I actually know whether I possess the skills of a senior engineer. Personally, I feel I have reached the state that I needed to be in during my years of effort. +- As for performance, self-assessment shows that I am not someone particularly bent on competition, or rather, I won’t compete just for the sake of competition. However, if I believe I should do something well, my ownership mindset and sense of responsibility are still commendable. Ultimately, my performance at Tencent over the four years can be considered satisfactory. -**1、文档能力** +Now, let's discuss some other soft skills: -作为程序员,文档能力其实是一项很重要的能力。其实我也没觉得自己文档能力有多好,但是前后两任总监,都说我的文档不错,那看来,我可能在平均水准之上。 +**1. Documentation Skills** -**2、明确方向** +As a programmer, documentation skills are actually quite important. Honestly, I don’t think my documentation skills are exceptional, but both of my previous directors have said my documentation was good, so it seems I might be above average. -最后,说一个更虚的,但是我觉得最有价值的收获: 我逐渐明确了,或者确定了以后的方向和路,那就是走数据开发。 +**2. Clear Direction** -其实,找到并确定一个目标很难,身边有清晰目标和方向的人很少,大多数是迷茫的。 +Lastly, I’d like to mention something less tangible but still valuable: I have gradually clarified or determined my future direction, which is to engage in data development. -前一段时间,跟人聊天,谈到职业规划,说是可以从两个角度思考: +In fact, finding and identifying a target is challenging, and few people around have clear goals and directions; most are in a state of confusion. -- 选一个业务方向,比如电商,广告,不断地积累业务领域知识和业务相关技能,随着经验的不断积累,最终你就是这个领域的专家。 -- 深入一个技术方向,不断钻研底层技术知识,这样就有希望成为此技术专家。坦白来说,虽然我深入研究并实践过领域驱动设计,也用来建模和解决了一些复杂业务问题,但是发自内心的,我其实更喜欢钻研技术,同时,我又对大数据很感兴趣。因此,我决定了,以后的方向,就做数据相关的工作。 +Not long ago, I was chatting with someone about career planning, and we discussed two perspectives to consider: -腾讯的四年,是我的第一份工作经历,认识了很多厉害的人,学到了很多。最后自己主动离开,也算走的体面(即使损失了大礼包),还是感谢腾讯。 +- Choose a business direction, such as e-commerce or advertising, and continuously accumulate knowledge and skills related to that business area. As experience accumulates, you will become an expert in that field. +- Delve into a technical direction and continuously research foundational technical knowledge, increasing your chances of becoming a specialist in that technology. - +To be honest, while I have deeply researched and practiced Domain-Driven Design, employing it to model and solve complex business problems, my heartfelt preference leans towards delving into technology, and I am very interested in big data. Therefore, I have decided that my future direction will center around data-related work. + +My four years at Tencent mark my first work experience, where I have met many outstanding individuals and learned a lot. In the end, my voluntary departure can also be seen as a dignified exit (even though I sacrificed a huge gift), and I am still grateful for Tencent. diff --git a/docs/high-quality-technical-articles/personal-experience/huawei-od-275-days.md b/docs/high-quality-technical-articles/personal-experience/huawei-od-275-days.md index 419f364adcf..1756d7392cb 100644 --- a/docs/high-quality-technical-articles/personal-experience/huawei-od-275-days.md +++ b/docs/high-quality-technical-articles/personal-experience/huawei-od-275-days.md @@ -1,337 +1,69 @@ --- -title: 华为 OD 275 天后,我进了腾讯! -category: 技术文章精选集 +title: After 275 Days at Huawei OD, I Joined Tencent! +category: Selected Technical Articles tag: - - 个人经历 + - Personal Experience --- -> **推荐语**:一位朋友的华为 OD 工作经历以及腾讯面试经历分享,内容很不错。 +> **Recommendation**: A friend's experience at Huawei OD and interview experience at Tencent, the content is quite good. > -> **原文地址**: +> **Original Article Link**: -## 时间线 +## Timeline -- 18 年 7 月,毕业于某不知名 985 计科专业; -- 毕业前,在某马的 JavaEE(后台开发)培训了 6 个月; -- 第一份工作(18-07 ~ 19-12)接触了大数据,感觉大数据更有前景; -- 19 年 12 月,入职中国平安产险(去到才发现是做后台开发 😢); -- 20 年 3 月,从平安辞职,跳去华为 OD 做大数据基础平台; -- 2021 年 1 月,入职鹅厂 +- July 2018, graduated from an unknown 985 computer science program; +- Before graduation, trained for 6 months in JavaEE (backend development) at a certain company; +- First job (July 2018 - December 2019) involved big data, felt that big data had more prospects; +- December 2019, joined Ping An Property & Casualty Insurance (only to find out it was backend development 😢); +- March 2020, resigned from Ping An and jumped to Huawei OD to work on the big data infrastructure platform; +- January 2021, joined Tencent. -## 华为 OD 工作经历总结 +## Summary of Huawei OD Work Experience -### 为什么会去华为 OD +### Why I Went to Huawei OD -在平安产险(正式员工)只待了 3 个月,就跳去华为 OD,朋友们都是很不理解的 —— 好好的正编不做,去什么外包啊 😂 +I only stayed at Ping An Property & Casualty Insurance (as a full-time employee) for 3 months before jumping to Huawei OD, which my friends found hard to understand — why leave a stable job for an outsourcing position? 😂 -但那个时候,我铁了心要去做大数据,不想和没完没了的 CRUD 打交道。刚好面试通过的岗位是华为 Cloud BU 的大数据部门,做的是国内政企中使用率绝对领先的大数据平台…… -平台和工作内容都不错,这么好的机会,说啥也要去啊 💪 +But at that time, I was determined to work in big data and didn’t want to deal with endless CRUD operations. Coincidentally, the position I interviewed for was in the big data department of Huawei Cloud BU, working on a big data platform that is absolutely leading in usage among domestic government and enterprise sectors... +The platform and work content were great, such a good opportunity, I had to go! 💪 -> 其实有想过在平安内部转岗到大数据的,但是不满足“入职一年以上”这个要求; -> 「等待就是浪费生命」,在转正流程还没批下来的时候,赶紧溜了 😂 +> I actually considered transferring to big data internally at Ping An, but I didn’t meet the requirement of "more than one year of employment"; +> "Waiting is wasting life," so I quickly left before my probation process was approved. 😂 -### 华为 OD 的工作内容 +### Work Content at Huawei OD -**带着无限的期待,火急火燎地去华为报到了。** +**With great expectations, I rushed to report to Huawei.** -和招聘的 HR 说的一样,和华为自有员工一起办公,工作内容和他们完全一样: +As the HR who recruited me said, I worked alongside Huawei's own employees, and the work content was exactly the same as theirs: -> 主管根据你的能力水平分配工作,逐渐增加难度,能者多劳; -> 试用期 6 个月,有导师带你,一般都是高你 2 个 Level 的华为自有员工,基本都是部门大牛。 +> The supervisor assigns work based on your ability level, gradually increasing the difficulty, with more work for those who can handle it; +> The probation period is 6 months, with a mentor who is usually a Huawei employee two levels above you, and they are generally experts in the department. -所以,**不存在外包做的都是基础的、流程性的、没有技术含量的工作** —— 顾虑这个的完全不用担心,你只需要打听清楚要去的部门/小组具体做什么,能接受就再考虑其他的。 +So, **there is no concern that outsourcing work is all basic, procedural, and lacks technical content** — you don’t need to worry about this at all; you just need to find out what the specific department/group you are going to work in does, and if you can accept it, then consider the rest. -感触很深的一点是:华为是有着近 20 万员工的巨头,内部有很多流程和制度。好处是:能接触到大公司的产品从开发、测试,到发布、运维等一系列的流程,比如提交代码的时候,会由经验资深、经过内部认证的大牛给你 Review,在拉会检视的时候,可以学习他们考虑问题的角度,还有对整个产品全局的把控。 +One thing that struck me deeply is: Huawei is a giant with nearly 200,000 employees, and there are many processes and systems internally. The benefit is that you can access the entire process of a large company's products from development, testing, to release and operation, for example, when submitting code, it will be reviewed by experienced, internally certified experts, and during review meetings, you can learn their perspectives on problem-solving and their overall grasp of the product. -但同时,个人觉得这也有不好的地方:流程繁琐会导致工作效率变低,比如改动几行代码,就需要跑完整个 CI(有些耗时比较久),还要提供自验和 VT 的报告。 +However, I also feel that there are downsides: cumbersome processes can lead to lower work efficiency, for example, if you change a few lines of code, you need to run the entire CI (which can take a long time), and you also need to provide self-verification and VT reports. -### OD 与华为自有员工的对比 +### Comparison Between OD and Huawei Employees -什么是 OD?Outstanding Dispatcher,人员派遣,官方强调说,OD 和常说的“外包”是不一样的。 +What is OD? Outstanding Dispatcher, personnel dispatch, the official emphasis is that OD is different from the commonly referred to "outsourcing." -说说我了解的 OD: +Here’s what I know about OD: -- 参考华为的薪酬框架,OD 人员的薪酬体系有一定的市场竞争力 —— 的确是这样,貌似会稍微倒挂同级别的自有员工; -- 可以参与华为主力产品的研发 —— 是的,这也是和某软等“供应商”的兄弟们不一样的地方; -- 外网权限也可以申请打开(对,就是梯子),部门内部的大多数文档都是可以看的; -- 工号是单独的 300 号段,其他供应商员工的工号是 8 开头,或着 WX 开头; -- 工卡带是红色的,和自有员工一样,但是工卡内容不同,OD 的明确标注:办公区通行证,并有德科公司的备注: +- Referring to Huawei's compensation framework, the salary system for OD personnel has a certain market competitiveness — indeed, it seems to be slightly lower than that of equivalent full-time employees; +- You can participate in the development of Huawei's main products — yes, this is also different from the "suppliers" like certain software companies; +- External network permissions can also be applied for (yes, that’s a VPN), and most internal documents in the department can be accessed; +- Employee numbers are in a separate 300 range, while other supplier employees have numbers starting with 8 or WX; +- The employee card is red, the same as full-time employees, but the content is different, clearly marked as: office area access pass, with a note from the staffing company: ![](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/personal-experience/1438655-20210124231550508-1315720640.jpg) -还听到一些内部的说法: +I also heard some internal comments: -- 没股票,没 TUP,年终奖少,只有工资可能比我司高一点点而已; -- 不能借针对 HW 的消费贷,也不能买公司提供的优惠保险…… +- No stock options, no TUP, fewer year-end bonuses, only salary might be slightly higher than my company; +- Cannot borrow consumer loans targeted at HW, nor can buy the discounted insurance provided by the company... -### 那,到底要不要去华为 OD? +### So, Should You Go to Huawei OD? -我想,搜到我这篇文字的你,心里其实是有偏向的,只是缺最后一片雪花 ❄️,让自己下决心。 - -作为过来人之一,我再提供一些参考吧 😃 - -1)除了华为 OD,**还有没有更好的选择?** 综合考虑加班(996、有些是 9106 甚至更多)、薪资、工作内容,以及这份工作经历对你整个职业的加成等等因素; - -2)有看到一些内部的说法,比如:“奇怪 OD 这么棒,为啥大家不自愿转去 OD 啊?”;再比如:“OD 等同华为?这话都说的出口,既然都等同,为啥还要 OD?就是降成本嘛……” - -3)内心够强大吗?虽然没有人会说你是 OD,但总有一些事情会提醒你:**你不是华为员工**。比如: - -a) 内部发文啥的,还有心声平台的大部分内容,都是无权限看的: - -![](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/personal-experience/1438655-20210124225007848-1701355006.png) - -b) 你的考勤是在租赁人员管理系统里考核,绩效管理也是; - -c) 自有员工的工卡具有消费功能(包括刷夜宵),OD 的工卡不能消费,需要办个消费卡,而且夜宵只能通过手机软件领取(自有员工是用工卡领的); - -d) 你的加班一定要提加班申请电子流换 Double 薪资,不然只能换调休,离职时没时间调休也换不来 Double —— 而华为员工即使自己主动离职,也是有 N+1,以及加班时间换成 Double 薪资的; - -### 网传的 OD 转华为正编,真的假的? - -这个放到单独的一节,是因为它很重要,有很多纠结的同学在关注这个问题。 - -**答案是:真的。** - -据各类非官方渠道(比如知乎上的一些分享),转华为自有是有条件的(): - -1)入职时间:一年以上 -2)绩效要求:连续两次绩效 A -3)认证要求:通过可信专业级认证 -4)其他条件:根据业务部门的人员需求及指标要求确定 - -说说这些条件吧 😃 - -**条件 2 连续两次绩效 A** - -上面链接里的说法: - -> 绩效 A 大约占整个部门的前 10%,连续两次 A 的意思就是一年里两次考评都排在部门前 10%,能做到这样的在华为属于火车头,这种难得的绩效会舍得分给一个租赁人员吗? - -OD 同学能拿到 A 吗?不知道,我入职晚,都没有经历一个完整的绩效考评。 - -(20210605 更新下)一年多了,还留着的 OD 同学告知我:OD 是单独评绩效的,能拿到 A 的比例,大概是 1/5,对应的年终奖就是 4 个月;绩效是 B,年终奖就是 2 个月。 - -在我看来,在试用期答辩时,能拿 A,接下来半年的绩效大概率也是拿 A 的。 - -但总的来说,这种事既看实力,又看劳动态度(能不能拼命三郎疯狂加班),还要看运气(主管对你是不是认可)…… - -**条件 3 通过可信专业级认证** - -可信专业级认证考试是啥?华为在推动技术人员的可信认证,算是一项安全合规的工作。 -专业级有哪些考试呢?共有四门: - -- 科目一:上级编程,对比力扣 2 道中等、1 道困难; -- 科目二:编程知识与应用,考察基础的编程语言知识等; -- 科目三:安全编程、质量、隐私,还有开发者测试等; -- 科目四:重构知识,包括设计模式、代码重构等。 - -上面这些,每一门单季度只能考一次(好像有些一年只能考 3 次),每个都要准备,少则 3 天,多则 1 星期,不准备,基本都过不了。 -我在 4 个月左右、还没转正的时候,就考过了专业级的科目二、三、四,只剩科目一大半年都没过(算法确实太菜了 😂 -但也有同事没准备,连着好几次都没通过。 - -**条件 4 部门人员需求指标?** - -这个听起来都感觉很玄学。还是那句话,实力和运气到了,应该可以的!成功转正员工图镇楼: - -![](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/personal-experience/1438655-20210124231943817-1976130336.jpg) - -### 真的感谢 OD,也感谢华为 - -运气很好,在我换工作还不到 3 个月的时候,华为还收我。 - -我遇到了很好的主管,起码在工作时间,感觉跟兄长一样指导、帮助我; - -分配给我的导师,是我工作以来认识到技术实力最厉害的人,定位问题思路清晰,编码实力强悍,全局思考问题、制定方案…… - -小组、部门的同学都很 nice,9 个多月里,我基本每天都跟打了鸡血一样,现在想想,也不知道当时为什么会那么积极有干劲 😂 - -从个人能力上来讲,我是进不去华为的(心里还是有点数的 😂)。正是有了 OD 这个渠道,才有机会切身感受华为的工作氛围,也学到了很多软技能: - -- 积极主动,勇于承担尝试,好工作要抢过来自己做; -- 及时同步工作进展,包括已完成、待完成,存在的风险困难等内容,要让领导知道你的工作情况; -- 勤于总结提炼输出,形成个人 DNA,利人利己; -- 有不懂的可以随时找人问,脸皮要厚,虚心求教; -- 不管多忙,所有的会议,不论大小,都要有会议纪要,邮件发给相关人…… - -再次感谢,大家都加油,向很牛掰很牛掰前进 💪 - -## 投简历,找面试官求虐 - -20 年 11 月初的一天,在同事们讨论“某某被其他公司高薪挖去了,钱景无限”的消息。 - -我忽然惊觉,自己来到华为半年多,除了熟悉内部的系统和流程,好像没有什么成长和进步? - -不禁反思:只有厉害的人才会被挖,现在这个状态的我,在市场上值几个钱? - -刚好想起了之前的一个同事在离职聚会上分享的经验: - -> 技术人不能闭门造车,要多交流,多看看外面的动态。 -> -> 如果感觉自己太安逸了,那就把简历挂出去,去了解其他公司用的是什么技术,他们更关注哪些痛点?面几次你就有方向了。 - -这时候起了个念头:找面试官求虐,以此来鞭策自己,进而更好地制定学习方向。 - -于是我重新下载了某聘软件,在首页推荐里投了几家公司。 - -## 开始面试 - -11 月 10 号投的简历,当天就有 2 家预约了 11 号下午的线上面试,其中就有鹅厂 🐧 - -好巧不巧,10 号晚上要双十一业务保障,一直到第二天凌晨 2 点半才下班。 - -熬夜太伤身,还好能申请调休一天,也省去了找借口请假 🙊 - -这段时间集中面了 3 家: - -> 第 1 个是广州的公司,11 号当晚就完成了 2 轮线上面试,开得有点低,就婉拒了; -> 第 2 个就是本文的重点——鹅厂; -> 第 3 个是做跨境电商的公司,一面就跪(恭喜它荣升为“在我有限的工作经历中,面试体验最差的 2 家公司之一”🙂️) - -## 鹅厂,去还是不去? - -一直有一个大厂梦,奈何菜鸟一枚,之前试过好几次,都跪在技术面了。 - -所以想了个曲线救国的方法:先在其他单位积累着,有机会了再争取大厂的机会 💪 - -很幸运,也很猝不及防,这次竟然通过了鹅厂的所有面试。 - -虽然已到年底,但是要是错过这么难得的机会,下次就不知道什么时候才能再通关了。 - -所以,**年后拿到年终再跳槽 vs 已到手的鹅厂 Offer,我选择了后者 😄** - -## 我的鹅厂面试 - -如本文标题所说,16 天通关五轮面试,第 17 天,我终于收到了期盼已久的鹅厂 Offer。 - -做技术的同学,可能会对鹅厂的面试很好奇,他们都会问哪些问题呢? - -我应聘的是大数据开发(Java)岗位,接下来对我的面试做个梳理,也给想来鹅厂的同学们一个参考 😊 - -> 几乎所有问题都能在网络上找到很详细的答案。 -> 篇幅有限,这里只写题目和一些引申的问题。 - -### 技术一面 - -#### Java 语言相关 - -1、对 Java 的类加载器有没有了解?如何自定义类加载器? - -> 引申:一个类能被加载多次吗?`java/javax` 包下的类会被加载多次吗? - -2、Java 中要怎么创建一个对象 🐘? - -3、对多线程有了解吗?在什么场景下需要使用多线程? - -> 引申:对 **线程安全** 的认识;对线程池的了解,以及各个线程池的适用场景。 - -4、对垃圾回收的了解? - -5、对 JVM 分代的了解? - -6、NIO 的了解?用过 RandomAccessFile 吗? - -> 引申:对 **同步、异步,阻塞、非阻塞** 的理解? -> -> 多路复用 IO 的优势? - -7、ArrayList 和 LinkedList 的区别?各自的适用场景? - -8、实现一个 Hash 集合,需要考虑哪些因素? - -> 引申:JDK 对 HashMap 的设计关键点,比如初识容量,扩所容,链表转红黑树,以及 JDK 7 和 JDK 8 的区别等等。 - -#### 通用学科相关 - -1、TCP 的三次握手; - -2、Linux 的常用命令,比如: - -> ```shell -> ps aux / ps -ef、top C -> df -h、du -sh *、free -g -> vmstat、mpstat、iostat、netstat -> ``` - -#### 项目框架相关 - -1、Kafka 和其他 MQ 的区别?它的吞吐量为什么高? - -> 消费者主动 pull 数据,目的是:控制消费节奏,还可以重复消费; -> -> 吞吐量高:各 partition 顺序写 IO,批量刷新到磁盘(OS 的 pageCache 负责刷盘,Kafka 不用管),比随机 IO 快;读取数据基于 sendfile 的 Zero Copy;批量数据压缩…… - -2、Hive 和 SparkSQL 的区别? - -3、Ranger 的权限模型、权限对象,鉴权过程,策略如何刷新…… - -#### 问题定位方法 - -1、ssh 连接失败,如何定位? - -> 是否能 ping 通(DNS 是否正确)、对端端口是否开了防火墙、对端服务是否正常…… - -2、运行 Java 程序的服务器,CPU 使用率达到 100%,如何定位? - -> `ps aux | grep xxx` 或 `jps` 命令找到 Java 的进程号 `pid`, -> -> 然后用 `top -Hp pid` 命令查看其阻塞的线程序号,**将其转换为 16 进制**; -> -> 再通过 `jstack pid` 命令跟踪此 Java 进程的堆栈,搜索上述转换来的 16 进制线程号,即可找到对应的线程名及其堆栈信息…… - -3、Java 程序发生了内存溢出,如何定位? - -> `jmap` 工具查看堆栈信息,看 Eden、Old 区的变化…… - -### 技术二面 - -二面主要是过往项目相关的问题: - -1、Solr 和 Elasticsearch 的区别 / 优劣? - -2、对 Elasticsearch 的优化,它的索引过程,选主过程等问题…… - -3、项目中遇到的难题,如何解决的? - -blabla 有少量的基础问题和一面有重复,还有几个和大数据相关的问题,记不太清了 😅 - -### 技术三面 - -这一面是总监面,更多是个人关于职业发展的一些想法,以及在之前公司的成长和收获、对下一份工作的期望等问题。 - -但也问了几个技术问题。印象比较深的是这个: - -> 1 个 1TB 的大文件,每行都只是 1 个数字,无重复,8GB 内存,要怎么对这个文件进行排序? - -首先想到的是 MapReduce 的思路,拆分小文件,分批排序,最后合并。 - -**此时连环追问来了:** - -> Q:如何尽可能多的利用内存呢? -> -> A:用位图法的思路,对数字按顺序映射。(对映射方法要有基本的了解) -> -> Q:如果在排好序之后,还需要快速查找呢? -> -> A:可以做索引,类似 Redis 的跳表,通过多级索引提高查找速度。 -> -> Q:索引查找的还是文件。要如何才能更多地利用内存呢? -> -> A:那就要添加缓存了,把读取过的数字缓存到内存中。 -> -> Q:缓存应该满足什么特点呢? -> -> A:应该使用 LRU 型的缓存。 - -呼。。。总算是追问完了这道题 😂 - ---- - -还有 GM 面和 HR 面,问题都和个人经历相关,这里就略去不表。 - -## 文末的絮叨 - -**入职鹅厂已经 1 月有余。不同的岗位,不同的工作内容,也是不同的挑战。** - -感受比较深的是,作为程序员,还是要自我驱动,努力提升个人技术能力,横向纵向都要扩充,这样才能走得长远。 - - +I diff --git a/docs/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md b/docs/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md index 5b6c47be7c7..33a86bd0803 100644 --- a/docs/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md +++ b/docs/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md @@ -1,152 +1,35 @@ --- -title: 滴滴和头条两年后端工作经验分享 -category: 技术文章精选集 +title: Experience Sharing on Backend Work at Didi and Toutiao After Two Years +category: Selected Technical Articles tag: - - 个人经历 + - Personal Experience --- -> **推荐语**:很实用的工作经验分享,看完之后十分受用! +> **Recommendation**: A very practical sharing of work experience, very useful after reading! > -> **内容概览**: +> **Content Overview**: > -> - 要学会深入思考,总结沉淀,这是我觉得最重要也是最有意义的一件事。 -> - 积极学习,保持技术热情。如果我们积极学习,保持技术能力、知识储备与工作年限成正比,这到了 35 岁哪还有什么焦虑呢,这样的大牛我觉得应该也是各大公司抢着要吧? -> - 在能为公司办成事,创造价值这一点上,我觉得最重要的两个字就是主动,主动承担任务,主动沟通交流,主动推动项目进展,主动协调资源,主动向上反馈,主动创造影响力等等。 -> - 脸皮要厚一点,多找人聊,快速融入,最忌讳有问题也不说,自己把自己孤立起来。 -> - 想舔就舔,不想舔也没必要酸别人,Respect Greatness。 -> - 时刻准备着,技术在手就没什么可怕的,哪天干得不爽了直接跳槽。 -> - 平时积极总结沉淀,多跟别人交流,形成方法论。 +> - Learn to think deeply, summarize and consolidate; this is the most important and meaningful thing in my opinion. +> - Actively learn and maintain technical enthusiasm. If we actively learn and keep our technical skills and knowledge in proportion to our years of work, what anxiety would there be at 35? Such talents should be in high demand by major companies, right? +> - In terms of being able to accomplish things for the company and create value, I think the most important two words are "proactive": proactively take on tasks, proactively communicate, proactively push project progress, proactively coordinate resources, proactively provide feedback upwards, proactively create influence, etc. +> - Be a bit thick-skinned, talk to more people, integrate quickly; the worst thing is to have problems and not speak up, isolating yourself. +> - If you want to flatter, then do it; if you don't want to, there's no need to criticize others. Respect Greatness. +> - Always be prepared; with technology in hand, there's nothing to fear. If one day you’re not happy, just switch jobs. +> - Regularly summarize and consolidate, communicate with others, and form methodologies. > - …… > -> **原文地址**: +> **Original Article Link**: -先简单交代一下背景吧,某不知名 985 的本硕,17 年毕业加入滴滴,当时找工作时候也是在牛客这里跟大家一起奋战的。今年下半年跳槽到了头条,一直从事后端研发相关的工作。之前没有实习经历,算是两年半的工作经验吧。这两年半之间完成了一次晋升,换了一家公司,有过开心满足的时光,也有过迷茫挣扎的日子,不过还算顺利地从一只职场小菜鸟转变为了一名资深划水员。在这个过程中,总结出了一些还算实用的划水经验,有些是自己领悟到的,有些是跟别人交流学到的,在这里跟大家分享一下。 +Let me briefly explain the background. I graduated from an unknown 985 university with a master's degree in 2017 and joined Didi. At that time, I was also fighting for jobs with everyone on Niuke. In the second half of this year, I switched to Toutiao, continuing to work in backend development. I had no internship experience, so I consider it about two and a half years of work experience. During these two and a half years, I completed a promotion and changed companies. I have had happy and fulfilling times, as well as days of confusion and struggle, but I have smoothly transitioned from a workplace rookie to a senior slacker. Throughout this process, I have summarized some practical slacking experiences, some of which I realized myself, and some I learned from communicating with others. I would like to share them here. -## 学会深入思考,总结沉淀 +## Learn to Think Deeply, Summarize and Consolidate -**我想说的第一条就是要学会深入思考,总结沉淀,这是我觉得最重要也是最有意义的一件事。** +**The first point I want to make is to learn to think deeply, summarize and consolidate; this is the most important and meaningful thing in my opinion.** -**先来说深入思考。** 在程序员这个圈子里,常能听到一些言论:_“我这个工作一点技术含量都没有,每天就 CRUD,再写写 if-else,这 TM 能让我学到什么东西?”_ +**First, let's talk about deep thinking.** In the programmer community, you often hear comments like: _“My job has no technical content at all; every day it's just CRUD and writing if-else statements. What can I learn from this?”_ -抛开一部分调侃和戏谑的论调不谈,这可能确实是一部分同学的真实想法,至少曾经的我,就这么认为过。后来随着工作经验的积累,加上和一些高 level 的同学交流探讨之后,我发现这个想法其实是非常错误的。之所以出现没什么可学的这样的看法,基本上是思维懒惰的结果。**任何一件看起来很不起眼的小事,只要进行深入思考,稍微纵向挖深或者横向拓宽一下,都是足以让人沉溺的知识海洋。** +Setting aside some teasing and joking remarks, this may indeed be the genuine thought of some colleagues; at least I used to think this way. However, as I accumulated work experience and engaged in discussions with some high-level colleagues, I realized that this idea is actually very wrong. The perception that there’s nothing to learn is basically a result of lazy thinking. **Any seemingly insignificant task, as long as you think deeply about it, can lead to a vast ocean of knowledge if you dig a little deeper or broaden your perspective.** -举一个例子。某次有个同学跟我说,这周有个服务 OOM 了,查了一周发现有个地方 defer 写的有问题,改了几行代码上线修复了,周报都没法写。可能大家也遇到过这样的场景,还算是有一定的代表性。其实就查 bug 这件事来说,是一个发现问题,排查问题,解决问题的过程,包含了触发、定位、复现、根因、修复、复盘等诸多步骤,花了一周来做这件事,一定有不断尝试与纠错的过程,这里面其实就有很多思考的空间。比如说定位,如何缩小范围的?走了哪些弯路?用了哪些分析工具?比如说根因,可以研究的点起码有 linux 的 OOM,k8s 的 OOM,go 的内存管理,defer 机制,函数闭包的原理等等。如果这些真的都不涉及,仍然花了一周时间做这件事,那复盘应该会有很多思考,提出来几十个 WHY 没问题吧... +For example, once a colleague told me that a service OOMed this week, and after a week of investigation, they found an issue with a defer statement, fixed a few lines of code, and went live, making it impossible to write a weekly report. Many of you may have encountered similar scenarios, which are quite representative. In fact, debugging is a process of discovering problems, troubleshooting, and solving issues, involving many steps such as triggering, locating, reproducing, root cause analysis, fixing, and reviewing. Spending a week on this task certainly involves continuous attempts and corrections, which provides ample room for thought. For instance, how did you narrow down the scope? What detours did you take? What analysis tools did you use? Regarding root causes, there are at least points to study such as Linux OOM, Kubernetes OOM, Go memory management, defer mechanisms, and the principles of function closures, etc. If none of these were involved and it still took a week to resolve the issue, then the review should have plenty of thoughts, and it wouldn’t be a problem to raise dozens of WHYs... -**再来说下总结沉淀。** 这个我觉得也是大多数程序员比较欠缺的地方,只顾埋头干活,可以把一件事做的很好。但是几乎从来不做抽象总结,以至于工作好几年了,所掌握的知识还是零星的几点,不成体系,不仅容易遗忘,而且造成自己视野比较窄,看问题比较局限。适时地做一些总结沉淀是很重要的,这是一个从术到道的过程,会让自己看问题的角度更广,层次更高。遇到同类型的问题,可以按照总结好的方法论,系统化、层次化地推进和解决。 - -还是举一个例子。做后台服务,今天优化了 1G 内存,明天优化了 50%的读写耗时,是不是可以做一下性能优化的总结?比如说在应用层,可以管理服务对接的应用方,梳理他们访问的合理性;在架构层,可以做缓存、预处理、读写分离、异步、并行等等;在代码层,可以做的事情更多了,资源池化、对象复用、无锁化设计、大 key 拆分、延迟处理、编码压缩、gc 调优还有各种语言相关的高性能实践...等下次再遇到需要性能优化的场景,一整套思路立马就能套用过来了,剩下的就是工具和实操的事儿了。 - -还有的同学说了,我就每天跟 PM 撕撕逼,做做需求,也不做性能优化啊。先不讨论是否可以搞性能优化,单就做业务需求来讲,也有可以总结的地方。比如说,如何做系统建设?系统核心能力,系统边界,系统瓶颈,服务分层拆分,服务治理这些问题有思考过吗?每天跟 PM 讨论需求,那作为技术同学该如何培养产品思维,引导产品走向,如何做到架构先行于业务,这些问题也是可以思考和总结的吧。就想一下,连接手维护别人烂代码这种蛋疼的事情,都能让 Martin Fowler 整出来一套重构理论,还显得那么高大上,我们确实也没啥必要对自己的工作妄自菲薄... - -所以说:**学习和成长是一个自驱的过程,如果觉得没什么可学的,大概率并不是真的没什么可学的,而是因为自己太懒了,不仅是行动上太懒了,思维上也太懒了。可以多写技术文章,多分享,强迫自己去思考和总结,毕竟如果文章深度不够,大家也不好意思公开分享。** - -## 积极学习,保持技术热情 - -最近两年在互联网圈里广泛传播的一种焦虑论叫做 35 岁程序员现象,大意是说程序员这个行业干到 35 岁就基本等着被裁员了。不可否认,互联网行业在这一点上确实不如公务员等体制内职业。但是,这个问题里 35 岁程序员并不是绝对生理意义上的 35 岁,应该是指那些工作十几年和工作两三年没什么太大区别的程序员。后面的工作基本是在吃老本,没有主动学习与充电,35 岁和 25 岁差不多,而且没有了 25 岁时对学习成长的渴望,反而添了家庭生活的诸多琐事,薪资要求往往也较高,在企业看来这确实是没什么竞争力。 - -**如果我们积极学习,保持技术能力、知识储备与工作年限成正比,这到了 35 岁哪还有什么焦虑呢,这样的大牛我觉得应该也是各大公司抢着要吧?** 但是,**学习这件事,其实是一个反人类的过程,这就需要我们强迫自己跳出自己的安逸区,主动学习,保持技术热情。** 在滴滴时有一句话大概是,**主动跳出自己的舒适区,感到挣扎与压力的时候,往往是黎明前的黑暗,那才是成长最快的时候。相反如果感觉自己每天都过得很安逸,工作只是在混时长,那可能真的是温水煮青蛙了。** - -刚毕业的这段时间,往往空闲时间还比较多,正是努力学习技术的好时候。借助这段时间夯实基础,培养出良好的学习习惯,保持积极的学习态度,应该是受益终身的。至于如何高效率学习,网上有很多大牛写这样的帖子,到了公司后内网也能找到很多这样的分享,我就不多谈了。 - -**_可以加入学习小组和技术社区,公司内和公司外的都可以,关注前沿技术。_** - -## 主动承担,及时交流反馈 - -前两条还是从个人的角度出发来说的,希望大家可以提升个人能力,保持核心竞争力,但从公司角度来讲,公司招聘员工入职,最重要的是让员工创造出业务价值,为公司服务。虽然对于校招生一般都会有一定的培养体系,但实际上公司确实没有帮助我们成长的义务。 - -**在能为公司办成事,创造价值这一点上,我觉得最重要的两个字就是主动,主动承担任务,主动沟通交流,主动推动项目进展,主动协调资源,主动向上反馈,主动创造影响力等等。** - -我当初刚入职的时候,基本就是 leader 给分配什么任务就把本职工作做好,然后就干自己的事了,几乎从来不主动去跟别人交流或者主动去思考些能帮助项目发展的点子。自以为把本职工作保质保量完成就行了,后来发现这么做其实是非常不够的,这只是最基本的要求。而有些同学的做法则是 leader 只需要同步一下最近要做什么方向,下面的一系列事情基本不需要 leader 操心了 ,这样的同学我是 leader 我也喜欢啊。入职后经常会听到的一个词叫 owner 意识,大概就是这个意思吧。 - -在这个过程中,另外很重要的一点就是及时向上沟通反馈。项目进展不顺利,遇到什么问题,及时跟 leader 同步,技术方案拿捏不准可以跟 leader 探讨,一些资源协调不了可以找 leader 帮忙,不要有太多顾忌,认为这些会太麻烦,leader 其实就是干这个事的。。如果项目进展比较顺利,确实也不需要 leader 介入,那也需要及时把项目的进度,取得的收益及时反馈,自己有什么想法也提出来探讨,问问 leader 对当前进展的建议,还有哪些地方需要改进,消除信息误差。做这些事一方面是合理利用 leader 的各种资源,另一方面也可以让 leader 了解到自己的工作量,对项目整体有所把控,毕竟 leader 也有 leader,也是要汇报的。可能算是大家比较反感的向上管理吧,有内味了,这个其实我也做得不好。但是最基本的一点,不要接了一个任务闷着头干活甚至与世隔绝了,一个月了也没跟 leader 同步过,想着憋个大招之类的,那基本凉凉。 - -**一定要主动,可以先从强迫自己在各种公开场合发言开始,有问题或想法及时 one-one。** - -除了以上几点,还有一些小点我觉得也是比较重要的,列在下面: - -## 第一件事建立信任 - -无论是校招还是社招,刚入职的第一件事是非常重要的,直接决定了 leader 和同事对自己的第一印象。入职后要做的第一件事一定要做好,最起码的要顺利完成而且不能出线上事故。这件事的目的就是为了建立信任,让团队觉得自己起码是靠谱的。如果这件事做得比较好,后面一路都会比较顺利。如果这件事就搞杂了,可能有的 leader 还会给第二次机会,再搞不好,后面就很难了,这一条对于社招来说更为重要。 - -而刚入职,公司技术栈不熟练,业务繁杂很难理清什么头绪,压力确实比较大。这时候一方面需要自己投入更多的精力,另一方面要多跟组内的同学交流,不懂就问。**最有效率的学习方式,我觉得不是什么看书啊学习视频啊,而是直接去找对应的人聊,让别人讲一遍自己基本就全懂了,这效率比看文档看代码快多了,不仅省去了过滤无用信息的过程,还了解到了业务的演变历史。当然,这需要一定的沟通技巧,毕竟同事们也都很忙。** - -**脸皮要厚一点,多找人聊,快速融入,最忌讳有问题也不说,自己把自己孤立起来。** - -## 超出预期 - -超出预期这个词的外延范围很广,比如 leader 让去做个值周,解答用户群里大家的问题,结果不仅解答了大家的问题,还收集了这些问题进行分类,进而做了一个智能问答机器人解放了值周的人力,这可以算超出预期。比如 leader 让给运营做一个小工具,结果建设了一系列的工具甚至发展成了一个平台,成为了一个完整的项目,这也算超出预期。超出预期要求我们有把事情做大的能力,也就是想到了 leader 没想到的地方,并且创造了实际价值,拿到了业务收益。这个能力其实也比较重要,在工作中发现,有的人能把一个小盘子越做越大,而有的人恰好反之,那么那些有创新能力,经常超出预期的同学发展空间显然就更大一点。 - -**这块其实比较看个人能力,暂时没想到什么太好的捷径,多想一步吧。** - -## 体系化思考,系统化建设 - -这句话是晋升时候总结出来的,大意就是做系统建设要有全局视野,不要局限于某一个小点,应该有良好的规划能力和清晰的演进蓝图。比如,今天加了一个监控,明天加一个报警,这些事不应该成为一个个孤岛,而是属于稳定性建设一期其中的一小步。这一期稳定性建设要做的工作是报警配置和监控梳理,包括机器监控、系统监控、业务监控、数据监控等,预期能拿到 XXX 的收益。这个工作还有后续的 roadmap,稳定性建设二期要做容量规划,接入压测,三期要做降级演练,多活容灾,四期要做...给人的感觉就是这个人思考非常全面,办事有体系有规划。 - -**平时积极总结沉淀,多跟别人交流,形成方法论。** - -## 提升自己的软素质能力 - -这里的软素质能力其实想说的就是 PPT、沟通、表达、时间管理、设计、文档等方面的能力。说实话,我觉得我当时能晋升就是因为 PPT 做的好了一点...可能大家平时对这些能力都不怎么关注,以前我也不重视,觉得比较简单,用时候直接上就行了,但事实可能并不像想象得那样简单。比如晋升时候 PPT+演讲+答辩这个工作,其实有很多细节的思考在里面,内容如何选取,排版怎么设计,怎样引导听众的情绪,如何回答评委的问题等等。晋升时候我见过很多同学 PPT 内容编排杂乱无章,演讲过程也不流畅自然,虽然确实做了很多实际工作,但在表达上欠缺了很多,属于会做不会说,如果再遇到不了解实际情况的外部门评委,吃亏是可以预见的。 - -**_公司内网一般都会有一些软素质培训课程,可以找一些场合刻意训练。_** - -以上都是这些分享还都算比较伟光正,但是社会吧也不全是那么美好的。。下面这些内容有负能量倾向,三观特别正的同学以及观感不适者建议跳过。 - -## 拍马屁是真的香 - -拍马屁这东西入职前我是很反感的,我最初想加入互联网公司的原因就是觉得互联网公司的人情世故没那么多,事实证明,我错了...入职前几天,部门群里大 leader 发了一条消息,后面几十条带着大拇指的消息立马跟上,学习了,点赞,真不错,优秀,那场面,说是红旗招展锣鼓喧天鞭炮齐鸣一点也不过分。除了惊叹大家超强的信息接收能力和处理速度外,更进一步我还发现,连拍马屁都是有队形的,一级部门 leader 发消息,几个二级部门 leader 跟上,后面各组长跟上,最后是大家的狂欢,让我一度怀疑拍马屁的速度就决定了职业生涯的发展前景(没错,现在我已经不怀疑了)。 - -坦诚地说,我到现在也没习惯在群里拍马屁,但也不反感了,可以说把这个事当成一乐了。倒不是说我没有那个口才和能力(事实上也不需要什么口才,大家都简单直接),在某些场合,为活跃气氛的需要,我也能小嘴儿抹了蜜,甚至能把古诗文彩虹屁给 leader 安排上。而是我发现我的直属 leader 也不怎么在群里拍马屁,所以我表面上不公开拍马屁其实属于暗地里事实上迎合了 leader 的喜好... - -但是拍马屁这个事只要掌握好度,整体来说还是香的,最多是没用,至少不会有什么坏处嘛。大家能力都差不多,每一次在群里拍马屁的机会就是一次露脸的机会,按某个同事的说法,这就叫打造个人技术影响力... - -**想舔就舔,不想舔也没必要酸别人,Respect Greatness。** - -## 永不缺席的撕逼甩锅实战 - -有人的地方,就有江湖。虽然搞技术的大多城府也不深,但撕逼甩锅邀功抢活这些闹心的事儿基本也不会缺席,甚至我还见到过公开群发邮件撕逼的...这部分话题涉及到一些敏感信息就不多说了,而且我们低职级的遇到这些事儿的机会也不会太多。只是给大家提个醒,在工作的时候迟早都会吃到这方面的瓜,到时候留个心眼。 - -**稍微注意一下,咱不会去欺负别人,但也不能轻易让别人给欺负了。** - -## 不要被画饼蒙蔽了双眼 - -说实话,我个人是比较反感灌鸡汤、打鸡血、谈梦想、讲奋斗这一类行为的,9102 年都快过完了,这一套\*\*\*治还在大行其道,真不知道是该可笑还是可悲。当然,这些词本身并没有什么问题,但是这些东西应该是自驱的,而不应该成为外界的一种强 push。『我必须努力奋斗』这个句式我觉得是正常的,但是『你必须努力奋斗』这种话多少感觉有点诡异,努力奋斗所以让公司的股东们发家致富?尤其在钱没给够的情况下,这些行为无异于耍流氓。我们需要对 leader 的这些画饼操作保持清醒的认知,理性分析,作出决策。比如感觉钱没给够(或者职级太低,同理)的时候,可能有以下几种情况: - -1. leader 并没有注意到你薪资较低这一事实 -2. leader 知道这个事实,但是不知道你有多强烈的涨薪需求 -3. leader 知道你有涨薪的需求,但他觉得你能力还不够 -4. leader 知道你有涨薪的需求,能力也够,但是他不想给你涨 -5. leader 想给你涨,也向上反馈和争取了,但是没有资源 - -这时候我们需要做的是向上反馈,跟 leader 沟通确认。如果是 1 和 2,那么通过沟通可以消除信息误差。如果是 3,需要分情况讨论。如果是 4 和 5,已经可以考虑撤退了。对于这些事儿,也没必要抱怨,抱怨解决不了任何问题。我们要做的就是努力提升好个人能力,保持个人竞争力,等一个合适的时机,跳槽就完事了。 - -**时刻准备着,技术在手就没什么可怕的,哪天干得不爽了直接跳槽。** - -## 学会包装 - -这一条说白了就是,要会吹。忘了从哪儿看到的了,能说、会写、善做是对职场人的三大要求。能说是很重要的,能说才能要来项目,拉来资源,招来人。同样一件事,不同的人能说出来完全不一样的效果。比如我做了个小工具上线了,我就只能说出来基本事实,而让 leader 描述一下,这就成了,打造了 XXX 的工具抓手,改进了 XXX 的完整生态,形成了 XXX 的业务闭环。老哥,我服了,硬币全给你还不行嘛。据我的观察,每个互联网公司都有这么几个词,抓手、生态、闭环、拉齐、梳理、迭代、owner 意识等等等等,我们需要做的就是熟读并背诵全文,啊不,是牢记并熟练使用。 - -这是对事情的包装,对人的包装也是一样的,尤其是在晋升和面试这样的应试型场合,特点是流程短一锤子买卖,包装显得尤为重要。晋升和面试这里就不展开说了,这里面的道和术太多了。。下面的场景提炼自面试过程中和某公司面试官的谈话,大家可以感受一下: - -1. 我们背后是一个四五百亿美金的市场... -2. 我负责过每天千亿级别访问量的系统... -3. 工作两年能达到这个程度挺不错的... -4. 贵司技术氛围挺好的,业务发展前景也很广阔... -5. 啊,彼此彼此... -6. 嗯,久仰久仰... - -人生如戏,全靠演技 - -**可以多看 leader 的 PPT,多听老板的向上汇报和宣讲会。** - -## 选择和努力哪个更重要? - -这还用问么,当然是选择。在完美的选择面前,努力显得一文不值,我有个多年没联系的高中同学今年已经在时代广场敲钟了...但是这样的案例太少了,做出完美选择的随机成本太高,不确定性太大。对于大多数刚毕业的同学,对行业的判断力还不够成熟,对自身能力和创业难度把握得也不够精准,此时拉几个人去创业,显得风险太高。我觉得更为稳妥的一条路是,先加入规模稍大一点的公司,找一个好 leader,抱好大腿,提升自己的个人能力。好平台加上大腿,再加上个人努力,这个起飞速度已经可以了。等后面积累了一定人脉和资金,深刻理解了市场和需求,对自己有信心了,可以再去考虑创业的事。 - -## 后记 - -本来还想分享一些生活方面的故事,发现已经这么长了,那就先这样叭。上面写的一些总结和建议我自己做的也不是很好,还需要继续加油,和大家共勉。另外,其中某些观点,由于个人视角的局限性也不保证是普适和正确的,可能再工作几年这些观点也会发生改变,欢迎大家跟我交流~(甩锅成功) - -最后祝大家都能找到心仪的工作,快乐工作,幸福生活,广阔天地,大有作为。 - - +**Next, let’s talk about summarizing and consolidating.** I think this is also a common shortcoming among most programmers; they focus solely on getting the work done well but rarely abstract and summarize. As a result, after working for several years, the knowledge they possess is still scattered and lacks a system, making it easy to forget and leading to a narrow perspective and limited problem-solving abilities. It is very important to periodically summarize and consolidate; this is a process from technique to principle, which broadens diff --git a/docs/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.md b/docs/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.md index fad7bede853..93242c34a4b 100644 --- a/docs/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.md +++ b/docs/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.md @@ -1,140 +1,140 @@ --- -title: 程序员高效出书避坑和实践指南 -category: 技术文章精选集 +title: A Guide for Programmers to Efficiently Publish Books: Avoiding Pitfalls and Practical Tips +category: Selected Technical Articles author: hsm_computer tag: - - 程序员 + - Programmer --- -> **推荐语**:详细介绍了程序员出书的一些常见问题,强烈建议有出书想法的朋友看看这篇文章。 +> **Recommendation**: This article provides a detailed introduction to common issues faced by programmers when publishing books. It is highly recommended for friends who are considering writing a book. > -> **原文地址**: +> **Original Article Link**: -古有三不朽, 所谓立德、立功、立言。程序员出一本属于自己的书,如果说是立言,可能过于高大上,但终究也算一件雅事。 +In ancient times, there were three pillars of immortality: establishing virtue, achieving merit, and making noteworthy statements. For a programmer to publish a book of their own may seem overly grand to call it "making noteworthy statements," yet it is undoubtedly an elegant endeavor. -出书其实不挣钱,而且从写作到最终拿钱的周期也不短。但程序员如果有一本属于自己的技术书,那至少在面试中能很好地证明自己,也能渐渐地在业内积累自己的名气,面试和做其它事情时也能有不少底气。在本文里,本人就将结合自己的经验和自己踩过的坑,和大家聊聊程序员出书的那些事。 +Publishing a book does not actually bring in much money, and the timeline from writing to receiving payment is also lengthy. However, for a programmer to have their own technical book can certainly serve as a strong proof of their skills during interviews and gradually build their reputation within the industry, providing significant confidence in interviews and other endeavors. In this article, I will share my experiences and the pitfalls I have encountered while discussing matters related to programmers publishing books. -## 1.出书的稿酬收益和所需要的时间 +## 1. Royalties and Required Time for Publishing -先说下出书的收益和需要付出的代价,这里姑且先不谈“出书带来的无形资产”,先谈下真金白银的稿酬。 +Let’s start with the earnings from publishing and the investments required; here, I will not discuss the "intangible assets" brought by publishing, but rather the tangible royalties. -如果直接和出版社联系,一般稿酬是版税,是书价格的 8%乘以印刷数(或者实际销售数),如果你是大牛的话,还可以往上加,不过一般版税估计也就 10%到 12%。请注意这里的价格是书的全价,不是打折后的价格。 +If you contact a publisher directly, royalties are usually calculated as a percentage of the book price—typically 8% multiplied by the printed quantity (or actual sales quantity). If you are a well-known figure in the field, you might negotiate a higher percentage, but generally, royalties are estimated at around 10% to 12%. Please note that this percentage is based on the full price of the book, not the discounted price. -比如一本书全价是 70 块,在京东等地打 7 折销售,那么版税是 70 块的 8%,也就是说卖出一本作者能有 5.6 的收益,当然真实拿到手以后还再要扣税。 +For example, if the full price of a book is 70 yuan, and it is sold at a 30% discount on platforms like JD.com, the royalty from each sale would be calculated as 8% of 70 yuan. In other words, the author would earn 5.6 yuan from the sale of each book, but taxes will still need to be deducted upon payment. -同时也请注意合同的约定是支付稿酬的方式是印刷数还是实际销售数,我和出版社谈的,一般是印刷数量,这有什么差别呢?现在计算机类的图书一般是首印 2500 册,那么实际拿到手的钱数是 70*8%*2500,当然还要扣税。但如果是按实际销售数量算的话,如果首印才销了 1800 本的话,那么就得按这个数量算钱了。 +Moreover, please note that the contract stipulates whether the payment of royalties is based on printed quantities or actual sales numbers. In my discussions with publishers, it typically pertains to the printed quantity. What’s the difference? Nowadays, the first print run for computer-related books is usually around 2,500 copies, meaning the pre-tax earnings would be calculated as 70\*8%\*2500. Of course, taxes would also apply. However, if royalties are based on actual sales and the first print run sells only 1,800 copies, the calculation will be adjusted accordingly. -现在一本 300 页的书,定价一般在 70 左右,按版税 8%和 2500 册算的话,税前收益是 14000,税后估计是 12000 左右,对新手作者的话,300 的书至少要写 8 个月,由此大家可以算下平均每个月的收益,算下来其实每月也就 1500 的收益,真不多。 +Currently, a typical 300-page book is priced at around 70 yuan. Assuming an 8% royalty and a print run of 2,500 copies, the pre-tax earnings would be approximately 14,000 yuan, leading to an estimated post-tax income of around 12,000 yuan. For a novice author, writing a 300-page book usually requires at least 8 months, allowing you to calculate an average monthly income, which amounts to only about 1,500 yuan—a rather modest figure. -别人的情况我不敢说,但我出书以后,除了稿酬,还有哪些其它的收益呢? +While I cannot speak for others, I can share that aside from royalties, there are several other benefits I gained from publishing my book: -- 在当下和之前的公司面试时,告诉面试官我在相关方面出过书以后,面试官就直接会认为我很资深,帮我省了不少事情。 -- 我还在做线下的培训,我就直接拿我最近出的 Python 书做教材了,省得我再备课了。 -- 和别人谈项目,能用我的书证明自己的技术实力,如果是第一次和别人打交道,那么这种证明能立杆见效。 +- During interviews with my current and previous companies, when I informed the interviewer that I had published a book on relevant topics, they immediately regarded me as very seasoned, which saved me a lot of effort. +- I am also conducting offline training sessions and have used my recently published Python book as teaching material, which saved me preparation time. +- When discussing projects with others, my book serves as proof of my technical capabilities. If it’s my first time interacting with someone, having this proof is quite effective. -尤其是第一点,其实对一些小公司或者是一些外派开发岗而言,如果候选人在这个方面出过书,甚至都有可能免面试直接录取,本人之前面试过一个大公司的外派岗,就得到过这种待遇。 +Especially the first point holds true; for some small companies or remote positions, if a candidate has published a book in this domain, they may even be offered direct employment without an interview. I once interviewed for a remote position with a large company and experienced such treatment. -## 2.支付稿酬的时间点和加印后的收益 +## 2. Timing of Royalty Payments and Income from Reprints -我是和出版社直接联系出书,支付稿酬的时间点一般是在首印后的 3 个月内拿到首印部分稿酬的一部分(具体是 50%到 90%),然后在图书出版后的一年后再拿到其它部分的稿酬。当下有不少书,能销掉首印的册数就不错了,不过也有不少书能加印,甚至出第二和第三版,一般加印册数的版税会在加印后的半年到一年内结清。 +I contacted the publisher directly to publish my book, and the timing of royalty payments generally involves receiving a portion of the royalties (typically 50% to 90%) within three months after the first print run, with the remaining royalties paid out a year after publication. Nowadays, many books are fortunate if they sell out their first print run, but some do get reprints and even second and third editions. Generally, the royalties from reprints are settled within six months to a year. -从支付稿酬的时间点上来,对作者确实会有延迟,外加上稿酬也不算高,相对于作者的辛勤劳动,所以出书真不是挣钱的事,而且拿钱的周期还长。如果个别图书公司工作人员一方面在出书阶段对作者没什么帮助, 另一方面还要在中间再挣个差价,那么真有些作践作者的辛勤劳动了。 +Considering the timing of royalty payments, authors often face delays, and coupled with the relatively low royalties compared to the author’s labor, publishing really isn’t a profitable venture, especially since the waiting period for payment is long. If individual staff at publishing companies provide little support during the publication process and also attempt to profit off the author’s hard work, it indeed undermines the efforts of authors. -## 3.同图书公司打交道的所见所闻 +## 3. Experiences with Various Publishing Companies -在和出版社编辑沟通前,我也和图书公司的工作人员交流过,不少工作人员对我也是比较尊重,交流虽然不算深入,但也算客气。不过最终对比出版社给出的稿酬等条件,我还是没有通过图书公司出书,这也是比较可惜的事情。下面我给出些具体的经历。 +Before communicating with the editors at publishing houses, I also interacted with staff from publishing companies. Many of them were respectful towards me during our exchanges; while the conversations weren’t particularly deep, they were courteous. However, after comparing the royalties and conditions offered by the publishers, I ultimately did not go through a publishing company to publish my book, which I consider somewhat unfortunate. Below are some specific experiences I had. -- 我经常在博客园等地收到一些图书公司工作人员的留言,问要不要出书,一般我不问,他们不会说自己是出版社编辑还是图书公司的工作人员。有个别图书公司的工作人员,会向作者,尤其是新手作者,说些“出版社编辑一般不会直接和作者联系”,以及“出书一般是通过图书公司”等的话。其实这些话不能算错,比如你不联系出版社编辑,那么对方自然不会直接联系你,但相反如果作者直接和出版社编辑联系,第一没难度,第二可能更直接。 -- 我和出版社编辑交流大纲时,即使大纲有不足,他们也能直接给出具体的修改意见,比如某个章节该写什么,某个小节的大纲该怎么写。而我和个别图书公司的工作人员交流过大纲时,得到的反馈大多是“要重写”,怎么个重写法?这些工作人员可能只能给出抽象的意见,什么都要我自己琢磨。在我之前的博文[程序员怎样出版一本技术书](./how-do-programmers-publish-a-technical-book)里,我就给出过具体的经历。 -- 由于交流不深,所以我没有和图书公司签订过出书协议,但我知道,只有出版社能出书。由于没有经历过,所以我也不知道图书公司在合同里是否有避规风险等条款,但我见过一位图书公司人员人员给出的一些退稿案例,并隐约流露出对作者的责备之意。细思感觉不妥,对接的工作人员第一不能在出问题的第一时间及时发现并向作者反馈,第二在出问题之后不能对应协调最终导致退稿,第三在退稿之后,作者在付出劳动的情况下图书公司不仅不用承担任何风险,并还能指摘作者。对此,退稿固然有作者的因素,但同是作者的我未免有兔死狐悲之谈。而我在出版社出书时,编辑有时候甚至会主动关心,主动给素材,哪怕有问题也会第一时间修改,所以甚至大范围修改稿件的情况都基本没有出现。 -- 再说下图书公司给作者的稿酬。我见过按页给钱,比如一页 30 到 50 块,并卖断版权,即书重印后作者也无法再得到稿酬,如果是按版税给钱,我也见过给 6%,至于图书公司能否给到 8 个点甚至更高,我没见到过,所以不知道,也不敢擅拟。 +- I frequently receive messages from staff at publishing companies on platforms like CSDN, inquiring if I want to publish a book. Typically, unless I ask, they do not specify whether they are publishers' editors or publishing company staff. Some staff may tell authors, especially novice authors, that "publishers’ editors generally do not contact authors directly" and "books are usually published through publishing companies." While there is some truth to this, if authors directly contact publishers’ editors, it’s neither difficult nor is there any barrier. +- When discussing outlines with publishers’ editors, even if there are shortcomings in the outline, they can provide specific suggestions for modifications, such as what to write in a particular chapter or how to organize a section’s outline. However, in discussions with some staff at publishing companies regarding outlines, the feedback tends to be "needs to be rewritten," but how to rewrite it? These staff members often provide only abstract advice, leaving it for me to figure things out independently. In my previous blog post [How Do Programmers Publish Technical Books](./how-do-programmers-publish-a-technical-book), I had detailed experiences on this. +- Due to the lack of deep communication, I have never signed a publishing agreement with a publishing company, but I know that only publishers can publish books. Since I have no personal experience in this area, I cannot confirm whether publishing companies include clauses for risk avoidance in their contracts. Nonetheless, I have seen cases of rejected manuscripts presented by some publishing company staff, which subtly implied a blame towards the authors. Upon reflection, this seems inappropriate. The connecting staff should first be able to identify issues promptly and provide feedback to the authors. Secondly, they should coordinate effectively in the event of problems to prevent rejection. Lastly, once a manuscript is rejected, the publishing company does not bear any risk despite the author’s effort but ends up criticizing the author. Sure, rejections can stem from the author's actions, but as a fellow author, I find it hard not to sympathize. In contrast, when I published with a traditional publisher, the editors sometimes reached out proactively to help me, providing materials, and even addressing issues quickly, so large-scale revisions were very rare. +- Regarding royalties from publishing companies, I have seen offers that pay per page, for instance, 30 to 50 yuan per page, with a buyout of rights, meaning that once the book is reprinted, the author does not receive additional royalties. As for royalties offered based on percentages, I have seen offers around 6%. However, I have not seen publishing companies providing 8% or higher, so I cannot speculate further. -我交流过的图书公司工作人员不多,交流也不深,因为我现在主要是和出版社的编辑交流。所以以上只是我对个别图书公司编辑的感受,我无意以偏概全,而和我交流的一些图书公司工作人员至少态度上对我很尊重。所以大家也可以对比尝试下和图书公司以及出版社合作的不同方式。不管怎样,你在写书甚至在签出书协议前,你需要问清楚如下的事项,并且对方有义务让你了解如下的事实。 +I have communicated with relatively few staff from publishing companies, and those conversations were not particularly deep, as my main focus has been on discussions with editors from traditional publishers. Therefore, the above points are just my impressions based on interactions with certain publishing company staff, and I do not intend to generalize. At least those publishing company staff I dealt with were respectful in their approach. It’s worth experimenting between collaborating with publishing companies and traditional publishers. Regardless, before writing or signing a publishing agreement, you need to clarify the following matters, and the other party has an obligation to inform you of the following facts. -- 你得问清楚,对方的身份是出版社编辑还是图书公司工作人员,这其实应当是对方主动告之。 -- 你的书在哪个出版社出版?这点需要在出书协议里明确给出,不能是先完稿再定出版社。而且,最终能出版书的,一定是出版社,而不是图书公司。 -- 稿酬的支付方式,哪怕图书公司中间可能挣差价,但至少你得了解出版社能给到的稿酬。如果你是通过图书公司出的书,不管图书公司怎么和你谈的,但出版社给图书公司的钱一分不会少,中间部分应该就是图书公司的盈利。 -- 最终和你签订出书合同的,是图书公司还是出版社,这一定得在你签字前搞明白,哪怕你最终是和图书公司签协议,但至少得知道你还能直接和出版社签协议。 -- 你不能存有“在图书公司出书要求低”的想法,更不应该存有“我能力一般,所以只能在图书公司出书”的想法。图书公司自己是没有资格出书的,所以他们也是会把稿件交给出版社,所以该有的要求一点也不会低。你的大纲在出版社编辑那边通不过,那么在图书公司的工作人员那边同样通不过,哪怕你索要的稿酬少,图书公司方面对应的要求一定也不会降低。 +- You need to clarify whether the other party is a publisher’s editor or publishing company staff, and this should ideally be mentioned proactively. +- Which publisher is your book going to be published by? This should be clearly stated in the publishing agreement and cannot be determined after the manuscript is completed. Ultimately, it is the publisher that can publish the book, not the publishing company. +- The method of royalty payments; even if the publishing company may profit off the difference, you should at least understand the royalties offered by the publisher. If you are publishing through a publishing company, regardless of how they negotiate with you, the publisher will not pay them any less. The difference is where the publishing company profits should lie. +- Finally, you must clarify before signing the agreement whether it is the publishing company or the publisher that you are signing with. Even if you eventually sign with the publishing company, you should at least be aware that you can sign directly with the publisher. +- You should not assume that "publishing with a publishing company requires lower standards," nor should you think "my abilities are average, so I can only publish with a publishing company." Publishing companies are not qualified to publish books themselves, meaning they will also submit manuscripts to publishers, thus their conditions should not be lower. If your outline is not accepted by the publisher's editor, it will similarly not be accepted by the publishing company's staff, regardless of whether your requested royalty is lower. The corresponding requirements from the publishing company will certainly not diminish. -如果你明知“图书公司和出版社的差别”,并还是和图书公司合作,这个是两厢情愿的事情。但如果对方“不主动告知”,而你在不了解两者差异的基础上同图书公司合作,那么对方也无可指摘。不过兼听则明,大家如果要出书,不妨和出版社和图书公司都去打打交道对比下。 +If you are aware of the differences between "publishing companies and publishers," yet choose to collaborate with a publishing company, this is a mutually agreed decision. However, if the other party does not proactively provide this information and you engage with a publishing company without understanding the differences, then they cannot be blamed. That said, it’s wise to seek multiple opinions; if you are looking to publish a book, try to interact with both publishers and publishing companies for comparison. -## 4.如何直接同国内计算机图书的知名出版社编辑联系 +## 4. How to Directly Contact Renowned Domestic Computer Book Publishers' Editors -我在清华大学出版社、机械工业出版社、北京大学出版社和电子工业出版社出过书,出书流程也比较顺畅,和编辑打交道也比较愉快。我个人无意把国内出版社划分成三六九等,但计算机行业,比较知名的出版社有清华、机工、电子工业和人邮这四家,当然其它出版社在计算机方面也出版过精品书。 +I have published books with Tsinghua University Press, Mechanical Industry Press, Peking University Press, and Electronics Industry Press. The publishing process has been relatively smooth, and my interactions with the editors have been quite pleasant. I have no intention of categorizing domestic publishers into hierarchies, but some well-known publishers in the computer science field include Tsinghua, MEUP, Electronics Industry, and People’s Postal. Of course, other publishers have also released excellent books in the computer domain. -如何同这些知名出版社的编辑直接打交道? +How can one directly communicate with editors from these well-known publishers? -- 直接到官网,一般官网上都直接有联系方式。 -- 你在博客园等地发表文章,会有人找你出书,其中除了图书公司的工作人员外,也有出版社编辑,一般出版社的编辑会直接说明身份,比如我是 xx 出版社的编辑 xx。 -- 本人也和些出版社的编辑联系过,大家如果要,我可以给。 +- Visit their official websites, as they usually provide direct contact information. +- If you publish articles on CSDN or similar platforms, someone may reach out to you for book publishing. Aside from publishing company staff, there may also be publisher’s editors, who typically identify themselves directly (e.g., "I am xx from xx publisher"). +- I have also contacted some editors from various publishers, so if anyone needs, I can provide those contacts. -那怎么去找图书公司的工作人员?一般不用主动找,你发表若干博文后,他们会主动找你。如果你细问,“您是出版社编辑还是图书公司的编辑”,他们会表明身份,如果你再细问,那么他们可能会站在图书公司的立场上解释出版社和图书公司的差异。 +So how can you find staff from publishing companies? Generally, there is no need to actively seek them out; after publishing several articles, they will reach out to you. If you inquire, "Are you a publisher's editor or a publishing company's editor?" they will clarify their identity. If you press further, they may explain the differences between publishers and publishing companies from the publishing company’s perspective. -从中大家可以看到,不管你最终是否写成书,但去找知名出版社的编辑,并不难。并且,你找到后,他们还会进一步和你交流选题。 +From this, it is clear that whether or not you end up writing a book, it is not difficult to seek out editors from well-known publishers. Moreover, once you establish contact, they will further discuss the selections with you. -## 5.定选题和出书的流程 +## 5. Determining Topics and the Publishing Process -这里给出我和出版社编辑交流合作,最终出书的流程。 +Here I outline the process of discussing collaboration with a publisher's editor, ultimately leading to publication: -第一,联系上出版社编辑后,先讨论选题,你可以选择一个你比较熟悉的方向,或者你愿意专攻的方向,这个方向可以是 java 分布式组件,Spring cloud 全家桶,微服务,或者是 Python 数据分析,机器学习或深度学习等。这方面你如果有扎实的项目经验那最好,如果你当下虽然不熟悉,但你有毅力经过短时间的系统学习确保你写的内容能成系统或者能帮到别人,那么你也可以在这方面出书。 +First, after making contact with the publisher's editor, discuss the topic. You can choose a direction you are familiar with or one you wish to specialize in—aspects may include Java distributed components, the Spring Cloud suite, microservices, or Python data analysis, machine learning, or deep learning. Having solid project experience in this area is preferable; however, if you are not familiar at the time but possess the determination to engage in systematic learning to ensure that your content is coherent and beneficial to others, you can still publish in these areas. -第二,定好选题方向后,你可以先列出大纲,比如以 Python 数据分析为例,你可以定 12 个章节,第一章讲语法,第二章讲 numpy 类等等,以此类推,你定大纲的时候,可以参考别人书的目录,从而制定你的写作内容。定好大纲以后,你可以和编辑交流,当编辑也认可这个大纲以后,就可以定出版协议。 +Second, once you have determined your topic, create an outline. Taking Python data analysis as an example, you might draft 12 chapters: the first chapter on syntax, the second on NumPy, and so forth. When formulating your outline, you can refer to the contents of others' books to help organize your writing. After establishing your outline, you can discuss it with the editor. If the editor also approves of the outline, you can proceed to draft a publishing agreement. -对一般作者而言,出版协议其实差不多,稿酬一般是 8 个点,写作周期是和出版社协商,支付周期可能也大同小异,然后出版社会买断这本书的电子以及各种文字的版权。但如果作者是大牛,那么这些细节都可以和出版社协商。 +For most authors, the publishing agreements tend to be similar, generally offering royalties around 8%. The writing period is usually negotiated with the publisher, and the payment timeline may also vary, with publishers typically purchasing the electronic and textual rights to the book. However, if the author is an esteemed figure, these details can usually be negotiated with the publisher. -然后是写书,这是很枯燥的,尤其是写最后几章的时候。我一般是工作日每天用半小时,两天周末周末用 4,5 个小时写,这样一般半年能写完一本 300 页的书,关于高效写书的技巧,后文会详细提及。 +Next comes the writing process, which can be quite tedious, especially when nearing the final chapters. I typically write for half an hour each workday, with four to five hours dedicated on weekends, allowing me to complete a 300-page book in about six months. Tips for efficient writing will be discussed later on. -在写书时,一般建议每写好一个章节就交给编辑审阅,这样就不会导致太大问题的出现,而且如果是新手作者,刚开始的措辞和写作技巧都需要积累,这样出版社的编辑在开始阶段也能及时帮到作者。 +While writing, it is advisable to submit each chapter for editorial review as it is completed, which helps mitigate significant issues. As a novice author, refining your terminology and writing techniques is paramount and can be supported by editors from the publisher during the initial stages. -当你写完把稿件交到编辑以后,可能会有三校三审的事情,在其中同我合作的编辑会帮助我修改语法和错别字等问题,然后会形成一个修改意见让我确认和修改。我了解下来,如果在图书公司出书,退稿的风险一般就发生在这个阶段,因为图书公司可能是会一次性地把稿件提交给出版社。但由于我会把每个章节都直接提交给出版社编辑审阅,所以即使有大问题,那么在写开始几个章节时都已经暴露并修改,所以最后的修改意见一般不会太长。也就是说,如果是直接和出版社沟通,在三校三审阶段,工作量可能未必大,我一般是在提交一本书以后,由编辑做这个事情,然后我就继续策划并开始写后一本书。 +After submitting the manuscript to the editor, there may be a process of revisions and reviews. The cooperating editor typically assists in correcting grammar and typos and would then provide me with a feedback document to review and revise. From what I understand, in the context of publishing companies, the risk of manuscript rejection often occurs at this stage since the publishing company may opt to send the entire manuscript in one go. However, because I directly submit each chapter to the publisher's editor for review, any major issues are often identified and corrected early on, leading to a shorter list of revision notes at the end. This means that if communication is maintained directly with the publisher, the workload during the review stage may not be overly burdensome. Usually, after I submit a complete manuscript, the editor handles the revisions while I continue to plan and begin writing my next book. -最后就是拿稿酬,之前已经说了,作者其实不应该对稿酬有太大的期望,也就是聊胜于无。但如果一不小心写了本销量在 5000 乃至 10000 本左右的畅销书,那么可能在一年内也能有 5 万左右的额外收益,并能在业内积累些名气。 +The last step is receiving royalties. As mentioned earlier, authors should not expect too much from royalties; they may simply be meager. Nonetheless, if you inadvertently write a bestseller that sells between 5,000 and 10,000 copies, you could potentially gain an additional income of around 50,000 yuan within a year and gradually build your reputation in the industry. -## 6.出案例书比出经验书要快 +## 6. Case Books Are Quicker to Write than Experience Books -对一些作者而言,尤其是新手作者,出书不容易,往往是开始几个章节干劲十足,后面发现问题越积越多,外加工作一忙,就不了了之了,或者用 1 年以上的时间才能完成一本书。对此,我的感受是,一本 300 到 400 书的写作周期最长是 8 个月。为了能在这个时间段里完成一本书,我对应给出的建议是,新手作者可以写案例书,别先写介绍经验类的书。 +For some authors, especially novices, writing a book can be challenging, often leading to initial enthusiasm for the first few chapters, only to find that the issues pile up, and with work commitments, the book is left unfinished, or it may take over a year to complete. In my experience, the longest writing period for a 300 to 400-page book is 8 months. To accomplish this, I suggest that novice authors start by writing case books instead of introductory experience books. -什么叫案例书?比如一本书里用一个大案例贯穿,系统介绍一个知识点,比如小程序开发,或者全栈开发等。或者一本书一个章节放一个案例,在一本书里给出 10 个左右 Python 深度学习方面的案例。什么叫经验类书呢?比如介绍面试经验的书就属于这这种,或者一些技术大牛写的介绍分布式高并发开发经验的书也算经验类书。 +What constitutes a case book? For instance, a book might traverse a significant case that systematically introduces a knowledge point, such as mini-program development or full-stack development. Alternatively, each chapter could feature a case, with a total of about 10 cases focusing on Python deep learning. As for experience-oriented books, these include titles discussing interview experiences or writings by tech experts illustrating their experiences in distributed high-concurrency development. -请注意这里并没有区分两类书的差异,只是对新手作者而言,案例书好写。因为在其中,更多的是看图说话,先给出案例(比如 Python 深度学习里的图像识别案例),然后通过案例介绍 API 的用法(比如 Python 对应库的用法),以及技术的综合要点(比如如何用 Python 库综合实现图像识别功能)。并且案例书里需要作者主观发挥的点比较少,作者无需用自己的话整理相关的经验。对新手作者而言,在组织文字介绍经验时,可能会有自己明白但说不上来的感觉,这样一方面就无法达到预期的效果,另一方面还有可能因为无法有效表述而导致进度的延迟。 +Please note that I am not distinguishing between the two types of books; it simply tends to be easier for novice authors to write case books. This is primarily because they rely on visual explanations; first presenting a case (like an image recognition case in Python deep learning), then explaining the API usage (how to use the corresponding Python library), along with an overview of the key points of the technology. Furthermore, case books require less subjective interpretation on the author's part, allowing them to avoid rephrasing established experiences. For novice authors, while organizing text to convey their experiences, they may have clear understanding but face difficulties articulating effectively, which can hinder progress. -但相反对于案例书,第一案例一般可以借鉴别人的,第二介绍现存的技术总比介绍自己的经验要容易,第三一般还有同类的书可以供作者参考,所以作者不大需要斟酌措辞,新手作者用半年到八个月的时间也有可能写完一本。当作者通过写几本书积累一定经验后,再去挑战经验类书,在这种情况下,写出来的经验类书就有可能畅销了。 +In contrast, case books usually derive their cases from existing samples. Moreover, explaining established technologies is often easier than presenting personal experiences. Thirdly, there are similar books available for reference, decreasing the need for authors to be overly meticulous about wording. Therefore, it is indeed possible for novice authors to complete a book within six to eight months. After accumulating experience through writing a few books, they may then challenge themselves with experience-based works, which could lead to potential bestsellers. -那么具体而言,怎么高效出一本案例书呢? +So, to efficiently publish a case book, here are some specific tips: -- 对整本书而言,先用少量章节介绍搭建环境和通用基本语法的内容。 -- 在写每个章节案例时,用到总分总的结构,先总体介绍下你这个案例的需求功能,以及要用的技术点,再分开介绍每个功能点的代码实现,最后再总结下这些功能点的使用要点。 -- 在介绍案例中具体代码时,也可以用到总分总的结构,即先总体介绍下这段代码的结构,再分别给出关键代码的说明,最后再给出运行效果并综述其中技术的实现要点。 +- For the entire book, start with a small number of chapters introducing the environment setup and basic syntax. +- While writing each chapter with case studies, use a structure of introduction, body, and conclusion. First, broadly introduce the functionalities and technologies involved in the case, then break down the code implementation for each functionality, and finally summarize the key takeaways. +- As you walk through specific code examples in the case studies, also employ the same structure: introduce the overall design of the code first, then detail the key code snippets, and conclude by demonstrating the output while summarizing the technical highlights. -这样的话,刚开始可以是 1 个月一个章节,写到后面熟练以后估计一个月能写两个章节,这样 8 个月完成一本书,也就不是不可能了。 +With this approach, you can aim to complete one chapter in a month at the beginning, and later, as you become more proficient, you could aim for two chapters a month. Completing a book within eight months becomes quite feasible. -## 7.如何在参考现有内容的基础上避免版权问题 +## 7. How to Reference Existing Content While Avoiding Copyright Issues -写书时,一般多少都需要参考现有的代码和现有的书,但这绝不是重复劳动。比如某位作者整合了不同网站上多个案例,然后系统地讲述了 Python 数据分析,这样虽然现成资料都有,但对读者来说,就能一站式学习。同样地,比如在 Python 神经网络方面,现有 2,3 本书分别给出了若干人脸识别等若干案例,但如果你有效整合到一起,并加他人的基础上加上你的功能,那对读者来说也是有价值的。 +When writing a book, it is often necessary to reference existing code and books, but this should not be repetitive labor. For instance, if an author aggregates multiple cases from different websites and systematically presents Python data analysis, even if the resources are readily available, it provides a one-stop learning experience for readers. Likewise, if existing books provide several cases in Python neural networks—like facial recognition—effectively synthesizing those along with adding your functionalities will also offer value to readers. -这里就涉及到版权问题,先要说明,作者不能抱有任何幻想,如果出了版权问题,书没出版还好,如果已经出版了,作者不仅要赔钱,而且在业内就会有不好的名声,可谓身败名裂。但其实要避免版权问题一点也不难。 +This raises questions of copyright. The author should have no illusions; if copyright issues arise, they could potentially damage their reputation severely. If a book has not yet been published, that’s one thing; if it is already published and problems surface, not only might the author face financial consequences, but they may also earn a negative reputation in the industry. -- 不能抄袭网上现有的内容,哪怕一句也不行。对此,作者可以在理解人家语句含义的基础上改写。不能抄袭人家书上现有的目录,更不能抄袭人家书上的话,同样一句也不行,对应的解决方法同样是在理解的基础上改写。 -- 不能抄袭 GitHub 上或者任何地方别人的代码,哪怕这个代码是开源的。对此,你可以在理解对方代码的基础上,先运行通,然后一定得自己新建一个项目,在你的项目里参考别人的代码实现你的功能,在这个过程中不能有大段的复制粘贴操作。也就是说,你的代码和别人的代码,在注释,变量命名,类名和方法名上不能有雷同的地方,当然你还可以额外加上你自己的功能。 -- 至于在写技术和案例介绍时,你就可以用你自己的话来说,这样也不会出现版权问题。 +However, avoiding copyright issues is not particularly difficult. -用了上述办法以后,作者就可以在参考现有资料的基础上,充分加上属于你的功能,写上你独到的理解,从而高效地出版属于你自己的书。 +- Do not plagiarize existing content from the internet, not even a single sentence. Authors should summarize while understanding the meanings of sentences written by others. Never copy another author's table of contents or sections verbatim; similarly, each sentence must be rephrased when appropriate. +- Do not plagiarize code from GitHub or anywhere else, even if it’s open-source. Instead, authors should understand the code, run it, and then create a new project to incorporate features based on the existing code while avoiding lengthy copying and pasting. Your code must be distinguishable from the original in terms of comments, variable names, class names, and method names; you can, of course, add your unique features. +- When writing technical descriptions and case studies, you should express these in your own words to avoid copyright issues. -## 8.新手作者需要着着重避免的问题 +By following these guidelines, authors can reference existing materials while infusing their unique functionalities, articulating their insights thoroughly, and thus efficiently publish their books. -在上文里详细给出了出书的流程,并通过案例书,给出了具体的习作方法,这里就特别针对新手作者,给出些需要注意的实践要点。 +## 8. Key Issues New Authors Should Avoid -- 技术书不同于文艺书,在其中首先要确保把技能知识点讲清楚,然后再此基础上可以适当加上些风趣生动的措辞。所以对新手作者而言,甚至可以直接用朴素的文字介绍案例技术,而无需过多考虑文字上的生动性。 -- 内容需要针对初学者,在介绍技术时,从最基本的零基础讲起,别讲太深的。这里以 Python 机器学习为例,可以从什么是机器学习以及 Python 如何实现机器学习讲起,但如果首先就讲机器学习里的实践经验,就未必能确保初学者能学会。 -- 新手作者恨不得把自己知道的都写出来。这种态度非常好,但需要考虑读者的客观接受水平所以需要在写书前设置个预期效果,比如零基础的 Python 开发人员读了我的书以后至少能干活。这个预期效果别不可行,比如不能是“零基础的 Python 开发人员读了我书以后能达到 3 年开发的水准”。这样就可以根据预先制定的效果,制定写作内容,从在你的书就能更着重讲基础知识,这样读者就能有真正有收获。 +Having outlined the publishing process in detail and the writing of case books with specific methods, I would like to emphasize some practical points for new authors. -不过话说回来,如果新手作者直接和出版社编辑联系,找个热门点的方向,并根据案例仔细讲解技术,甚至都有可能写出销量过万的畅销书。 +- Technical books differ from literary works in that the primary goal is ensuring the clarity of skills and knowledge points. A lighthearted tone can be added afterwards. So for novice authors, it might be better to use straightforward language to explain case studies without overemphasizing creativity in wording. +- The content should be geared towards beginners. When introducing technology, start from the most basic aspects; do not dive too deep too quickly. Taking Python machine learning as an example, you can start by explaining what machine learning is and how to implement it in Python, but beginning with practical experiences might not guarantee that absolute beginners could keep up. +- New authors may feel inclined to write everything they know. This eagerness is admirable; however, they must consider readers’ capabilities and set expectations before writing. For example, when a beginner Python developer reads my book, they should at least be able to perform tasks. The expected outcome should be realistic; for instance, it would be impractical to claim, "A beginner Python developer will achieve three years of development experience after reading my book." By establishing predetermined outcomes, authors can tailor their writing material to emphasize foundational knowledge, ensuring that readers derive genuine benefits. -## 9.总结:在国内知名出版社出书,其实是个体力活 +That being said, if a novice author directly contacts a publisher's editor about a trending topic and elaborates on the technology through case studies, they may very well write a bestseller with sales exceeding 10,000 copies. -可能当下,写公众号和录视频等的方式,挣钱收益要高于出书,不过话可以这样说,经营公众号和录制视频也是个长期的事情,在短时间里可能未必有收益,如果不是系统地发表内容的话,可能甚至不会有收益。所以出书可能是个非常好的前期准备工作,你靠出书系统积累了素材,靠出书整合了你的知识体系,那么在此基础上,靠公众号或者录视频挣钱可能就会事半功倍。 +## 9. Conclusion: Publishing with Renowned Domestic Publishers is Truly Labor-Intensive -从上文里大家可以看到,在出书前期,联系出版社编辑和定选题并不难,如果要写案例书,那么在参考别人内容的基础上,要写完一般书可能也不是高不可攀的事情。甚至可以这样说,出书是个体力活,只要坚持,要出本书并不难,只是你愿不愿意坚持下去的问题。但一旦你有了属于自己的技术书,那么在找工作时,你就能自信地和面试官说你是这方面的专家,在你的视频、公众号和文字里,你也能正大光明地说,你是计算机图书的作者。更为重要的是,和名校、大厂经历一样,属于你的技术书同样是证明程序员能力的重要证据,当你通过出书有效整合了相关方面的知识体系后,那么在这方面,不管是找工作,或者是干私活,或者是接项目做,你都能理直气壮地和别人说:我能行! +Currently, earning through platforms like public accounts and video recordings may yield higher returns than publishing books. Nonetheless, it should be noted that running a public account or creating video content requires long-term commitment, and initial gains may not be immediate, especially if you do not consistently publish content, potentially leading to no returns at all. Therefore, publishing a book can be an excellent preparatory step. By systematically accumulating materials through writing a book and consolidating your knowledge base, you could eventually find it much easier to earn money via public accounts or videos. - +From the above discussion, we see that contacting publishers and determining topics are not particularly daunting tasks. If you aim to write a case book, finishing one is likely to be well within reach, provided you reference existing content as guidance. Really, publishing a book is indeed a labor-intensive task; as long as you are persistent, achieving the goal of publishing a book is not unattainable. It ultimately depends on your willingness to persevere. Once you have your own technical book, you can confidently tell interviewers that you are an expert in that field, and you can assertively state in your videos, public accounts, or writings that you are the author of a computer science book. More importantly, much like having experience from prestigious universities or large companies, having your own technical book serves as a solid evidence of a programmer's competence. Once you effectively consolidate your knowledge through writing a book, you can confidently assert your capabilities in job searching, freelance work, and project engagement: **I can do it!** diff --git a/docs/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.md b/docs/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.md index 7ea9f7932e0..70e0c87d9b8 100644 --- a/docs/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.md +++ b/docs/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.md @@ -1,114 +1,114 @@ --- -title: 程序员最该拿的几种高含金量证书 -category: 技术文章精选集 +title: The Most Valuable Certificates for Programmers +category: Selected Technical Articles tag: - - 程序员 + - Programmer --- -证书是能有效证明自己能力的好东西,它就是你实力的象征。在短短的面试时间内,证书可以为你加不少分。通过考证来提升自己,是一种性价比很高的办法。不过,相比金融、建筑、医疗等行业,IT 行业的职业资格证书并没有那么多。 +Certificates are a great way to effectively prove one's abilities; they symbolize your strength. In a short interview time, certificates can score you extra points. Improving yourself through certification is a high cost-performance method. However, compared to industries like finance, construction, and healthcare, the IT industry does not have as many professional qualification certificates. -下面我总结了一下程序员可以考的一些常见证书。 +Below, I've summarized some common certificates that programmers can pursue. -## 软考 +## Soft Examination -全国计算机技术与软件专业技术资格(水平)考试,简称“软考”,是国内认可度较高的一项计算机技术资格认证。尽管一些人吐槽其实际价值,但在特定领域和情况下,它还是非常有用的,例如软考证书在国企和事业单位中具有较高的认可度、在某些城市软考证书可以用于积分落户、可用于个税补贴。 +The National Computer Technology and Software Professional Technical Qualification (Level) Examination, abbreviated as "Soft Examination," is a highly recognized computer technology certification in China. Although some people criticize its actual value, it is very useful in specific fields and situations. For example, the Soft Examination certificate is highly recognized in state-owned enterprises and public institutions, can be used for points-based residence permits in certain cities, and is eligible for individual tax subsidies. -软考有初、中、高三个级别,建议直接考高级。相比于 PMP(项目管理专业人士认证),软考高项的难度更大,特别是论文部分,绝大部分人都挂在了论文部分。过了软考高项,在一些单位可以内部挂证,每个月多拿几百。 +The Soft Examination has three levels: junior, intermediate, and senior. It is recommended to directly take the senior exam. Compared to PMP (Project Management Professional certification), the difficulty of the senior exam is higher, especially in the paper part, where most people fail. Those who pass the senior exam can often receive internal certifications at some companies, earning several hundred more each month. -![软考高级证书](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/ruankao-advanced-certification%20.jpg) +![Soft Examination Senior Certificate](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/ruankao-advanced-certification%20.jpg) -官网地址:。 +Official website: . -备考建议:[2024 年上半年,一次通过软考高级架构师考试的备考秘诀 - 阿里云开发者](https://mp.weixin.qq.com/s/9aUXHJ7dXgrHuT19jRhCnw) +Preparation advice: [Secrets to Passing the Soft Examination Senior Architect Exam in One Go - Alibaba Cloud Developer](https://mp.weixin.qq.com/s/9aUXHJ7dXgrHuT19jRhCnw) ## PAT -攀拓计算机能力测评(PAT)是一个专注于考察算法能力的测评体系,由浙江大学主办。该测评分为四个级别:基础级、乙级、甲级和顶级。 +The PAT (Pangu Computer Ability Test) focuses on assessing algorithm capabilities and is organized by Zhejiang University. The test is divided into four levels: Basic, Grade B, Grade A, and Top Grade. -通过 PAT 测评并达到联盟企业规定的相应评级和分数,可以跳过学历门槛,免除筛选简历和笔试环节,直接获得面试机会。具体有哪些公司可以去官网看看: 。 +By passing the PAT and achieving corresponding ratings and scores set by partner companies, candidates can bypass the degree threshold, skip resume screening and written tests, and directly obtain interview opportunities. For the list of participating companies, visit the official website: . -对于考研浙江大学的同学来说,PAT(甲级)成绩在一年内可以作为硕士研究生招生考试上机复试成绩。 +For students preparing for graduate studies at Zhejiang University, the PAT (Grade A) score can be used as part of the oral exam for master’s degree admissions within one year. -![PAT(甲级)成绩作用](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/pat-enterprise-alliance.png) +![PAT (Grade A) Score Usage](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/pat-enterprise-alliance.png) ## PMP -PMP(Project Management Professional)认证由美国项目管理协会(PMI)提供,是全球范围内认可度最高的项目管理专业人士资格认证。PMP 认证旨在提升项目管理专业人士的知识和技能,确保项目顺利完成。 +PMP (Project Management Professional) certification is provided by the Project Management Institute (PMI) in the United States and is the most recognized project management professional qualification globally. The PMP certification aims to enhance the knowledge and skills of project management professionals, ensuring successful project completion. -![PMP 证书](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/pmp-certification.png) +![PMP Certificate](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/pmp-certification.png) -PMP 是“一证在手,全球通用”的资格认证,对项目管理人士来说,PMP 证书含金量还是比较高的。放眼全球,很多成功企业都会将 PMP 认证作为项目经理的入职标准。 +PMP is a globally applicable qualification certification. For project management professionals, the PMP certificate is still quite valuable. Worldwide, many successful companies consider PMP certification as an entry standard for project managers. -但是!真正有价值的不是 PMP 证书,而是《PMBOK》 那套项目管理体系,在《PMBOK》(PMP 考试指定用书)中也包含了非常多商业活动、实业项目、组织规划、建筑行业等各个领域的项目案例。 +However! The truly valuable aspect is not the PMP certificate itself but the PMBOK project management framework, which includes many real-world cases from various sectors such as business activities, industrial projects, organizational planning, and construction. -另外,PMP 证书不是一个高大上的证书,而是一个基础的证书。 +Furthermore, the PMP certificate is not a high-end certificate but rather a foundational one. ## ACP -ACP(Agile Certified Practitioner)认证同样由美国项目管理协会(PMI)提供,是项目管理领域的另一个重要认证。与 PMP(Project Management Professional)注重传统的瀑布方法论不同,ACP 专注于敏捷项目管理方法论,如 Scrum、Kanban、Lean、Extreme Programming(XP)等。 +The ACP (Agile Certified Practitioner) certification is also provided by PMI and is another important certification in the field of project management. Unlike PMP, which emphasizes traditional waterfall methodologies, ACP focuses on agile project management methodologies such as Scrum, Kanban, Lean, and Extreme Programming (XP). ## OCP -Oracle Certified Professional(OCP)是 Oracle 公司提供的一项专业认证,专注于 Oracle 数据库及相关技术。这个认证旨在验证和认证个人在使用和管理 Oracle 数据库方面的专业知识和技能。 +Oracle Certified Professional (OCP) is a professional certification offered by Oracle Corporation, focusing on Oracle databases and related technologies. This certification is designed to validate and certify an individual's professional knowledge and skills in using and managing Oracle databases. -下图展示了 Oracle 认证的不同路径和相应的认证级别,分别是核心路径(Core Track)和专业路径(Speciality Track)。 +The following illustration shows the different paths and corresponding certification levels for Oracle certification, which include the Core Track and Speciality Track. -![OCP 认证路径](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/oracle-certified-professional.jpg) +![OCP Certification Path](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/oracle-certified-professional.jpg) -## 阿里云认证 +## Alibaba Cloud Certification -阿里云(Alibaba Cloud)提供的专业认证,认证方向包括云计算、大数据、人工智能、Devops 等。职业认证分为 ACA、ACP、ACE 三个等级,除了职业认证之外,还有一个开发者 Clouder 认证,这是专门为开发者设立的专项技能认证。 +Alibaba Cloud offers professional certifications across various directions, including cloud computing, big data, artificial intelligence, and DevOps. The certifications are divided into three levels: ACA, ACP, and ACE. In addition to these, there is also a developer Clouder certification specifically designed for developers. ![](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/aliyun-professional-certification.png) -官网地址:。 +Official website: . -## 华为认证 +## Huawei Certification -华为认证是由华为技术有限公司提供的面向 ICT(信息与通信技术)领域的专业认证,认证方向包括网络、存储、云计算、大数据、人工智能等,非常庞大的认证体系。 +Huawei certifications are a series of professional certifications provided by Huawei Technologies Co., Ltd., aimed at the ICT (Information and Communication Technology) field, covering areas such as networking, storage, cloud computing, big data, and artificial intelligence, comprising a very extensive certification system. ![](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/huawei-professional-certification.png) -## AWS 认证 +## AWS Certification -AWS 云认证考试是 AWS 云计算服务的官方认证考试,旨在验证 IT 专业人士在设计、部署和管理 AWS 基础架构方面的技能。 +AWS cloud certification exams are the official certification exams for AWS cloud services, aimed at validating IT professionals' skills in designing, deploying, and managing AWS infrastructure. -AWS 认证分为多个级别,包括基础级、从业者级、助理级、专业级和专家级(Specialty),涵盖多个角色和技能: +AWS certifications are divided into several levels, including foundational, associate, professional, and specialty, covering various roles and skills: -- **基础级别**:AWS Certified Cloud Practitioner,适合初学者,验证对 AWS 基础知识的理解,是最简单的入门认证。 -- **助理级别**:包括 AWS Certified Solutions Architect – Associate、AWS Certified Developer – Associate 和 AWS Certified SysOps Administrator – Associate,适合中级专业人士,验证其设计、开发和管理 AWS 应用的能力。 -- **专业级别**:包括 AWS Certified Solutions Architect – Professional 和 AWS Certified DevOps Engineer – Professional,适合高级专业人士,验证其在复杂和大规模 AWS 环境中的能力。 -- **专家级别**:包括 AWS Certified Advanced Networking – Specialty、AWS Certified Big Data – Specialty 等,专注于特定技术领域的深度知识和技能。 +- **Foundational Level**: AWS Certified Cloud Practitioner, suitable for beginners, verifying understanding of basic AWS knowledge and is the simplest entry-level certification. +- **Associate Level**: Includes AWS Certified Solutions Architect – Associate, AWS Certified Developer – Associate, and AWS Certified SysOps Administrator – Associate, suitable for mid-level professionals, verifying their ability to design, develop, and manage AWS applications. +- **Professional Level**: Includes AWS Certified Solutions Architect – Professional and AWS Certified DevOps Engineer – Professional, suitable for senior professionals, validating their skills in complex and large-scale AWS environments. +- **Specialty Level**: Includes AWS Certified Advanced Networking – Specialty, AWS Certified Big Data – Specialty, etc., focusing on in-depth knowledge and skills in specific technical areas. -备考建议:[小白入门云计算的最佳方式,是去考一张 AWS 的证书(附备考经验)](https://mp.weixin.qq.com/s/xAqNOnfZ05GDRuUbAiMHIA) +Preparation advice: [The Best Way for Beginners to Start Cloud Computing is to Get an AWS Certification (with Study Experiences)](https://mp.weixin.qq.com/s/xAqNOnfZ05GDRuUbAiMHIA) -## Google Cloud 认证 +## Google Cloud Certification -与 AWS 认证不同,Google Cloud 认证只有一门助理级认证(Associate Cloud Engineer),其他大部分为专业级(专家级)认证。 +Unlike AWS certification, Google Cloud certification offers only one associate-level certification (Associate Cloud Engineer), while most others are professional-level certifications. -备考建议:[如何备考谷歌云认证](https://mp.weixin.qq.com/s/Vw5LGPI_akA7TQl1FMygWw) +Preparation advice: [How to Prepare for Google Cloud Certification](https://mp.weixin.qq.com/s/Vw5LGPI_akA7TQl1FMygWw) -官网地址: +Official website: -## 微软认证 +## Microsoft Certification -微软的认证体系主要针对其 Azure 云平台,分为基础级别、助理级别和专家级别,认证方向包括云计算、数据管理、开发、生产力工具等。 +Microsoft's certification system mainly targets its Azure cloud platform, categorized into foundational, associate, and expert levels, covering areas like cloud computing, data management, development, and productivity tools. ![](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/microsoft-certification.png) -## Elastic 认证 +## Elastic Certification -Elastic 认证是由 Elastic 公司提供的一系列专业认证,旨在验证个人在使用 Elastic Stack(包括 Elasticsearch、Logstash、Kibana 、Beats 等)方面的技能和知识。 +Elastic certification consists of a series of professional certifications provided by Elastic, aimed at validating individuals' skills and knowledge in using the Elastic Stack (including Elasticsearch, Logstash, Kibana, Beats, etc.). -如果你在日常开发核心工作是负责 ElasticSearch 相关业务的话,还是比较建议考的,含金量挺高。 +If your core development work involves Elasticsearch, it is highly recommended to consider taking this certification, as it holds substantial value. -目前 Elastic 认证证书分为四类:Elastic Certified Engineer、Elastic Certified Analyst、Elastic Certified Observability Engineer、Elastic Certified SIEM Specialist。 +Currently, Elastic certification is categorized into four types: Elastic Certified Engineer, Elastic Certified Analyst, Elastic Certified Observability Engineer, and Elastic Certified SIEM Specialist. -比较建议考 **Elastic Certified Engineer**,这个是 Elastic Stack 的基础认证,考察安装、配置、管理和维护 Elasticsearch 集群等核心技能。 +It is especially recommended to take the **Elastic Certified Engineer** exam, which serves as the foundational certification for the Elastic Stack, assessing skills essential for installing, configuring, managing, and maintaining Elasticsearch clusters. ![](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/elastic-certified-engineer-certification.png) -## 其他 +## Others -- PostgreSQL 认证:国内的 PostgreSQL 认证分为专员级(PCA)、专家级(PCP)和大师级(PCM),主要考查 PostgreSQL 数据库管理和优化,价格略贵,不是很推荐。 -- Kubernetes 认证:Cloud Native Computing Foundation (CNCF) 提供了几个官方认证,例如 Certified Kubernetes Administrator (CKA)、Certified Kubernetes Application Developer (CKAD),主要考察 Kubernetes 方面的技能和知识。 +- PostgreSQL Certification: The PostgreSQL certification in China is divided into Specialist Level (PCA), Expert Level (PCP), and Master Level (PCM), primarily examining PostgreSQL database management and optimization. Its cost is relatively high, and it is not highly recommended. +- Kubernetes Certification: The Cloud Native Computing Foundation (CNCF) offers several official certifications, such as Certified Kubernetes Administrator (CKA) and Certified Kubernetes Application Developer (CKAD), focusing on Kubernetes skills and knowledge. diff --git a/docs/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.md b/docs/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.md index 99dae8f9d72..5c967c9ff06 100644 --- a/docs/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.md +++ b/docs/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.md @@ -1,93 +1,33 @@ --- -title: 程序员怎样出版一本技术书 -category: 技术文章精选集 +title: How Programmers Can Publish a Technical Book +category: Selected Technical Articles author: hsm_computer tag: - - 程序员 + - Programmer --- -> **推荐语**:详细介绍了程序员应该如何从头开始出一本自己的书籍。 +> **Recommendation**: This article provides a detailed introduction on how programmers can start from scratch to publish their own book. > -> **原文地址**: +> **Original Article Link**: -在面试或联系副业的时候,如果能令人信服地证明自己的实力,那么很有可能事半功倍。如何证明自己的实力?最有信服力的是大公司职位背景背书,没有之一,比如在 BAT 担任资深架构,那么其它话甚至都不用讲了。 +When interviewing or pursuing side jobs, being able to convincingly demonstrate your abilities can lead to significant advantages. How can you prove your capabilities? The most convincing endorsement comes from having a background in a major company, such as being a senior architect at BAT; in that case, you might not need to say much more. -不过,不是每个人入职后马上就是大公司架构师,在上进的路上,还可以通过公众号,专栏博文,GitHub 代码量和出书出视频等方式来证明自己。和其它方式相比,属于自己的技术图书由于经过了国家级出版社的加持,相对更能让别人认可自己的实力,而对于一些小公司而言,一本属于自己的书甚至可以说是免面试的通行证。所以在本文里,就将和广大程序员朋友聊聊出版技术书的那些事。 +However, not everyone becomes a major company architect immediately after starting a job. On the path to advancement, you can prove yourself through public accounts, column articles, GitHub contributions, and publishing books or videos. Compared to other methods, having your own technical book, backed by a national-level publisher, can significantly enhance others' recognition of your abilities. For some smaller companies, having your own book can even serve as a pass that allows you to skip interviews. Therefore, in this article, I will discuss the various aspects of publishing a technical book with fellow programmers. -## 1.不是有能力了再出书,而是在出书过程中升能力 +## 1. It's not about having the ability to publish a book, but about improving your skills during the writing process -我知道的不少朋友,是在工作 3 年内出了第一本书,有些优秀的,甚至在校阶段就出书了。 +I know several friends who published their first book within three years of working, and some outstanding individuals even published books while still in school. -与之相比还有另外一种态度,不少同学可能想,要等到技术积累到一定程度再写。其实这或许就不怎么积极了,边写书,边升技术,而且写出的书对人还有帮助,这绝对可以做到的。 +In contrast, there is another attitude where many students might think they should wait until they have accumulated enough technical knowledge before writing. This mindset may not be very proactive. You can write a book while improving your skills, and the book you produce can help others; this is absolutely achievable. -比如有同学向深入了解最近比较热门的 Python 数据分析和机器学习,那么就可以在系统性的学习之后,整理之前学习到的爬虫,数据分析和机器学习的案例,根据自己的理解,用适合于初学者的方式整理一下,然后就能出书了。这种书,对资深的人帮助未必大,但由于包含案例,对入门级的读者绝对有帮助,因为这属于现身说法。而且话说回来,如果没有出书这个动力,或者学习过程也就是浅尝辄止,或者未必能全身心地投入,有了出书这个目标,更能保证学习的效果。 +For example, if someone wants to delve into the currently popular topics of Python data analysis and machine learning, they can systematically study these subjects, organize their previous learning on web scraping, data analysis, and machine learning cases, and then compile their understanding in a way that is suitable for beginners. This way, they can publish a book. Such a book may not be very helpful for advanced readers, but it will definitely assist entry-level readers because it provides real-world examples. Moreover, if there is no motivation to publish a book, the learning process may be superficial, and one might not fully engage. Having the goal of publishing a book can ensure better learning outcomes. -## 2.适合初级开发,高级开发和架构师写的书 +## 2. Books Suitable for Junior Developers, Senior Developers, and Architects -之前也提到了,初级开发适合写案例书,就拿 Python 爬虫数据分析机器学习题材为例,可以先找几本这方面现成的书,这些书里,或者章节内容不同,但一起集成看的话,应该可以包含这方面的内容。然后就参考别人书的思路,比如一章写爬虫,一章写 pandas,一章写 matplotlib 等等,整合起来,就可以用 若干个章节构成一本书了。总之,别人书里包含什么内容,你别照抄,但可以参考别人写哪些技术点。 +As mentioned earlier, junior developers are suitable for writing case study books. Taking Python web scraping, data analysis, and machine learning as an example, one can start by finding a few existing books on these topics. While the content or chapters may differ, integrating them can cover the necessary material. Then, you can follow the structure of others' books, such as dedicating one chapter to web scraping, another to pandas, and another to matplotlib, and so on, to create a book composed of several chapters. In summary, while you shouldn't copy the content of others' books, you can reference the technical points they cover. -定好章节后,再定下每个章节的小节,比如第三章讲爬虫案例,那么可以定 3.1 讲爬虫概念,3.2 讲如何搭建 Scrapy 库,3.3 讲如何开发 Scrapy 爬虫案例,通过先章再节的次序,就可以定好一本书的框架。由于是案例书,所以是先给运行通的代码,再用这些代码案例教别人入门,所以案例未必很深,但需要让初学者看了就能懂,而且按照你给出的知识体系逐步学习之后,能理解这个主题的内容。并且,能在看完你这本书以后,能通过调通你给出的爬虫,机器学习等的案例,掌握这一领域的知识,并能从事这方面的基本开发。这个目标,对初级开发而言,稍微用点心,费点时间,应该不难达到。 +Once the chapters are determined, you can outline the subsections for each chapter. For instance, if Chapter 3 discusses web scraping cases, you might define 3.1 as the concept of web scraping, 3.2 as how to set up the Scrapy library, and 3.3 as how to develop a Scrapy web scraping case. By establishing a chapter and subsection order, you can create a framework for the book. Since this is a case study book, you should first provide runnable code and then use these code examples to teach others the basics. The cases may not be very deep, but they need to be understandable for beginners, allowing them to learn progressively according to the knowledge system you provide. After reading your book, they should be able to run the web scraping and machine learning examples you provide, mastering the knowledge in this field and being able to engage in basic development. This goal should be achievable for junior developers with a bit of effort and time. -而对于高级开发和架构师而言,除了写存粹案例书以外,还可以在书里给出你在大公司里总结出来的开发经验,也就是所谓踩过的坑,比如 Python 在用 matplotlib 会图例时,在设置坐标轴方面有哪些技巧,设置时会遇到哪些常见问题,如果在书里大量包含这种经验,你的书含金量更高。 +For senior developers and architects, in addition to writing pure case study books, you can also share the development experiences you have summarized from working in large companies, such as the pitfalls you have encountered. For example, when using matplotlib in Python, what tips are there for setting axes, and what common problems might arise? Including such experiences in your book will increase its value. -此外,高级开发和架构师还可以写一些技术含量更高的书,比如就讲高并发场景下的实践经验,或者 k8s+docker 应对高并发的经验,这种书里,可以给出代码,更可以给出实施方案和架构实施技巧,比如就讲高并发场景里,缓存该如何选型,如何避免击穿,雪崩等场景,如何排查线上 redis 问题,如何设计故障应对预案。除了这条路之外,还可以深入细节,比如通过讲 dubbo 底层代码,告诉大家如何高效配置 dubbo,出了问题该如何排查。如果架构师或高级开发有这类书作为背书,外带大厂工作经验,那么就更可以打出自己的知名度。 - -## 3.可以直接找出版社,也可以找出版公司 - -在我的这篇博文里,[程序员副业那些事:聊聊出书和录视频](https://www.cnblogs.com/JavaArchitect/p/11616906.html),给出了通过出版社出书和图书公司出书的差别,供大家参考,大家看了以后可以自行决定出书方式。 - -不过不管怎么选,在出书前你得搞明白一些事,或许个别图书出版公司的工作人员不会主动说,这需要你自己问清楚。 - -- 你的合作方是谁?图书出版公司还是出版社? -- 你的书将在哪个出版社出版?国内比较有名的是清华,人邮,电子和机械,同时其它出版社不能说不好,但业内比较认这四个。 -- 和你沟通的人,是最终有决定权的图书编辑吗?还是图书公司里的工作人员?再啰嗦下,最后能决定书能否出版,以及确定修改意见的,是出版社的编辑。 - -通过对比出版社和图书出版公司,在搞清楚诸多细节后,大家可以自己斟酌考虑合作的方式。而且,出版社和图书公司的联系方式,在官网上都有,大家可以自行通过邮件等方式联系。 - -## 4.如果别人拿你做试错对象,或有不尊重,赶紧止损 - -我之前看到有图书出版公司招募面向 Java 初学者图书的作者,并且也主动联系过相关人员,得到的反馈大多是:“要重写”。 - -比如我列了大纲发过去,反馈是“要重写”,原因是对方没学过 Java,但作为零基础的人看了我的大纲,发现学不会。至于要重写成什么样子 ,对方也说不上来,总之让我再给个大纲,再给一版后,同样没过,这次好些,给了我几本其它类似书的大纲,让我自行看别人有什么好的点。总之不提(或者说提不出)具体的改进点,要我自行尝试各种改进点,试到对方感觉可以为止。 - -相比我和几位出版社专业的编辑沟通时,哪怕大纲或稿件有问题,对方会指明到点,并给出具体的修改意见。我不知道图书出版公司里的组织结构,但出版社里,计算机图书有专门的部门,专门的编辑,对方提出的意见都是比较专业,且修改起来很有操作性。 - -另外,我在各种渠道,时不时看到有图书出版公司的人员,晒出别人交付的稿件,在众目睽睽之下,说其中有什么问题,意思让大家引以为戒。姑且不论这样做的动机,并且这位工作人员也涂掉了能表面作者身份的信息。但作者出于信任把稿件交到你手上,在不征得作者同意就公开稿件,说“不把作者当回事”,这并不为过。不然,完全可以用私信的方式和作者交流,而不是把作者无心之过公示于众。 - -我在和出版社合作时,这类事绝没发生过,而且我认识的出版社编辑,都对各位作者保持着足够的尊重。而且我和我的朋友和多位图书出版公司的朋友交流时,也能得到尊重和礼遇。所以,如果大家在写书时,尤其在写第一本书时,如果遇到被试错,或者从言辞等方面感觉对方不把你当会事,那么可以当即止损。其实也没有什么“损失”,你把当前的大纲和稿件再和出版社编辑交流时,或许你的收益还能提升。 - -## 5.如何写好 30 页篇幅的章节? - -在和出版社定好写作合同后,就可以创作了。书是由章节构成,这里讲下如何构思并创作一个章节。 - -比如写爬虫章节,大概 30 页,先定节和目,比如 3.1 搭建爬虫环境是小节,3.1.1 下载 Python Scrapy 包,则是目。先定要写的内容,具体到爬虫小节,可以写 3.1 搭建环境,3.2 Scrapy 的重要模块,3.3 如何开发 Scrapy 爬虫,3.4 开发好以后如何运行,3.5 如何把爬到的信息放入数据库,这些都是小节。 - -再具体到目,比如 3.5 里,3.5.1 里写如何搭建数据库环境 3.5.2 里写如何在 Scrapy 里连接数据库 3.5.3 里给出实际案例 3.5.4 里给出运行步骤和示例效果。 - -这样可以搭建好一个章的框架,在每个小节里,先给出可以运行通的,而且能说明问题的代码,再给出对代码的说明,再写下代码如何配置,开发时该注意哪些问题,必要时用表格和图来说明,用这样的条理,最多 3 个星期可以完成一个章节,快的话一周半就一个章节。 - -以此类推,一本书大概有 12 个章节,第一章可以讲如何安装环境,以及基础语法,后面就可以由浅入深,一个章节一个主题,比如讲 Python 爬虫,第二章可以是讲基础语法,第三章讲 http 协议以及爬虫知识点,以此深入,讲全爬虫,数据分析,数据展示和机器学习等技能。 - -按这样算,如果出第一本书,平均下来一个月 2 个章节,大概半年到八个月可以完成一本书,思路就是先搭建书的知识体系,写每个章节时再搭建某个知识点的框架,在小节和目里,用代码结合说明的方式,这样从简到难,大家就可以完成第一本属于自己的书了。 - -## 6.如何写出一本销量过 5 千的书 - -目前纸质书一般一次印刷在 2500 册,大多数书一般就一次印刷,买完为止。如果能销调 5000 本,就属于受欢迎了,如果销量过万,就可以说是大神级书的。这里先不论大神级书,就说下如何写一本过 5000 的畅销书。 - -1 最好贴近热点,比如当前热点是全栈开发和机器学习等,如何找热点,就到京东等处去看热销书的关键字。具体操作起来,多和出版社编辑沟通,或许作者更多是从技术角度分析,但出版社的编辑是从市场角度来考虑问题。 - -2 如果你的书能被培训机构用作教材,那想不热都不行。培训机构一般用哪些教材呢?第一面向初学者,第二代码全面,第三在这个领域里涵盖知识点全。如果要达成这点,大家可以和出版社的编辑直接沟通,问下相关细节。 - -3 可以文字生动,但不能用过于花哨的文字来掩盖书的内涵不足,也就是说畅销书一定要有干货,能解决初学者实际问题,比如 Python 机器学习方向,就写一本用案例涵盖目前常用的机器学习算法,一个章节一种算法,并且案例中有可视化,数据分析,爬虫等要素,可视化的效果如果再吸引人,这本书畅销的可能性也很大。 - -4 一定不能心存敷衍,代码调通不算,更力求简洁,说明文字多面向读者,内容上,确保读者一看就会,而且看了有收获,或许这点说起来很抽象,但我写了几本书以后切身体会,要做到这很难,同时做到了,书哪怕不畅想,但至少不误人子弟。 - -## 7.总结,出书仅是一个里程碑,程序员在上进路上应永不停息 - -出书不简单,因为不是每个人都愿意在半年到八个月里,每个晚上每个周末都费时费力写书。但出书也不难,毕竟时间用上去了,出书也只是调试代码加写文字的活,最多再外加些和人沟通的成本。 - -其实出书收益并不高,算下来月入大概能在 3k 左右,如果是和图书出版公司合作,估计更少,但这好歹能证明自己的实力。不过在出书后不能止步于此,因为在大厂里有太多的牛人,甚至不用靠出书来证明自己的实力。 - -那么如何让出书带来的利益最大化呢?第一可以靠这进大厂,面试时有自己的书绝对是加分项。第二可以用这个去各大网站开专栏,录视频,或者开公众号,毕竟有出版社的背书,能更让别人信服你的能力。第三更得用写书时积累的学习方法和上进的态势继续专研更高深技术,技术有了,不仅能到大厂挣更多的钱,还能通过企业培训等方式更高效地挣钱。 - - +Additionally, senior developers and architects can write books with higher technical content, such as sharing practical experiences in high-concurrency scenarios or discussing k8s and Docker for handling high concurrency. In such books, you can provide code, implementation plans, and architectural implementation techniques, such as how to choose caching strategies in high-concurrency scenarios, how to avoid issues like cache breakdown and avalanche, how to troubleshoot online Redis problems, and how to design contingency plans. Beyond this, you can delve into details, such as explaining how to efficiently configure Dubbo through its underlying code diff --git a/docs/high-quality-technical-articles/work/32-tips-improving-career.md b/docs/high-quality-technical-articles/work/32-tips-improving-career.md index 78b0e66b122..ae622a0ab84 100644 --- a/docs/high-quality-technical-articles/work/32-tips-improving-career.md +++ b/docs/high-quality-technical-articles/work/32-tips-improving-career.md @@ -1,72 +1,71 @@ --- -title: 32条总结教你提升职场经验 -category: 技术文章精选集 +title: 32 Insights to Help You Enhance Your Workplace Experience +category: Selected Technical Articles tag: - - 工作 + - Work --- -> **推荐语**:阿里开发者的一篇职场经验的分享。 +> **Recommendation**: A shared experience from an Alibaba developer regarding workplace growth. > -> **原文地址:** +> **Original Article Link:** -## 成长的捷径 +## Shortcuts to Growth -- 入职伊始谦逊的态度是好的,但不要把“我是新人”作为心理安全线; -- 写一篇技术博客大概需要两周左右,但可能是最快的成长方式; -- 一定要读两本书:金字塔原理、高效能人士的七个习惯(这本书名字像成功学,实际讲的是如何塑造性格); -- 多问是什么、为什么,追本溯源把问题解决掉,试图绕过的问题永远会在下个路口等着你; -- 不要沉迷于忙碌带来的虚假安全感中,目标的确定和追逐才是最真实的安全; -- 不用过于计较一时的得失,在公平的环境中,吃亏是福不是鸡汤; -- 思维和技能不要受限于前端、后端、测试等角色,把自己定位成业务域问题的终结者; -- 好奇和热爱是成长最大的捷径,长期主义者会认同自己的工作价值,甚至要高于组织当下给的认同(KPI)。 +- A humble attitude at the beginning of your job is good, but don’t treat “I am a newcomer” as a psychological safety net. +- It takes about two weeks to write a technical blog, but it might be the fastest way to grow. +- Make sure to read two books: The Pyramid Principle and The 7 Habits of Highly Effective People (though it sounds like a book on success, it actually discusses how to shape character). +- Ask “what” and “why” frequently; trace issues back to their roots to solve problems. The issues you try to bypass will always be waiting for you at the next turn. +- Don't get trapped in a false sense of security from busyness; having clear goals and pursuing them is the only real safety. +- Don’t get overly hung up on short-term gains and losses. In a fair environment, taking a loss can be a blessing, not a cliché. +- Don't limit your thinking and skills to front-end, back-end, testing roles; position yourself as the problem-solver in the business domain. +- Curiosity and passion are the biggest shortcuts to growth; long-term thinkers will recognize their work's value, even exceeding the acknowledgment (KPI) given by the organization. -## 功夫在日常 +## Efforts in Daily Routine -- 每行代码要代表自己当下的最高水平,你觉得无所谓的小细节,有可能就是在晋升场上伤害你的暗箭; -- 双周报不是工作日志流水账,不要被时间推着走,最起码要知道下次双周报里会有什么(小目标驱动); -- 觉得日常都是琐碎工作、不技术、给师兄打杂等,可以尝试对手头事情做一下分类,想象成每个分类都是个小格子,这些格子连起来的终点就是自己的目标,这样每天不再是机械的做需求,而是有规划的填格子、为目标努力,甚至会给自己加需求,因为自己看清楚了要去哪里; -- 日常的言行举止是能力的显微镜,大部分人可能意识不到,自己的强大和虚弱是那么的明显,不要无谓的试图掩盖,更不存在蒙混过关。 +- Every line of code should represent your highest current standard. The small details that you think are unimportant can potentially harm your promotion chances. +- Bi-weekly reports should not be a mere log of tasks. Don’t let time push you along; at the very least, know what will be in your next bi-weekly report (driven by small goals). +- If you feel that daily work is trivial, untechnical, or just assisting others, try categorizing your tasks. Imagine each category as a small box; the connection of these boxes leads to your ultimate goal. This makes your daily activities less mechanical and more planned, pushing you toward your goals. You may even find yourself adding tasks as you clarify where you want to go. +- Daily behaviors serve as a microscope for abilities. Most people may not realize how obvious their strengths and weaknesses are. Don't attempt to cover them up unnecessarily, nor try to cheat your way through. -> 最后一条大概意思就是有时候我们会在意自己在聚光灯下(述职、晋升、周报、汇报等)的表现,以为大家会根据这个评价自己。实际上日常是怎么完成业务需求、帮助身边同学、创造价值的,才是大家评价自己的依据,而且每个人是什么样的特质,合作过三次的伙伴就可以精准评价,在聚光灯下的表演只能骗自己。 +> The last point roughly means that sometimes we care too much about our performance under the spotlight (reviews, promotions, reports, etc.), thinking others will evaluate us based on this. In reality, how we fulfill business requirements, help our peers, and create value is what others use to assess us. Moreover, what qualities each person possesses can be accurately evaluated by partners you have worked with multiple times; performances under the spotlight only deceive oneself. -## 学会被管理 +## Learning to be Managed -> 上级、主管是泛指,开发对口的 PD 主管等也在范围内。 +> Superior, supervisor is a general reference, including the relevant PD managers in development. -- 不要传播负面情绪,不要总是抱怨; -- 对上级不卑不亢更容易获得尊重,但不要当众反驳对方观点,分歧私下沟通; -- 好好做向上管理,尤其是对齐预期,沟通绩效出现 Surprise 双方其实都有责任,但倒霉的是自己; -- 尽量站在主管角度想问题: +- Don’t spread negative emotions; avoid constant complaining. +- Being neither servile nor arrogant towards your superiors makes it easier to gain respect, but don’t publicly refute their views; resolve disagreements privately. +- Manage upwards effectively, especially in aligning expectations. If surprises arise in performance discussions, both parties share the responsibility, but often it’s the individual who suffers. +- Try to think from the supervisor's perspective: + - This understanding can clarify many decisions that previously seemed unreasonable. + - Don’t care about who executes or where credit is due; alleviating team worries and earning your supervisor’s trust is far more important. + - Do not interpret this principle as being excessively subservient; this is the most despicable. -- - 这样能理解很多过去感觉匪夷所思的决策; - - 不要在意谁执行、功劳是谁的等,为团队分忧赢得主管信任的重要性远远高于这些; - - 不要把这个原则理解为唯上,这种最让人不齿。 +## Shifting Mindset -## 思维转换 +- Defining problems is a high-level ability; establish the mental loop of discovering a problem -> defining the problem -> solving the problem -> eliminating the problem as early as possible. +- Focus on value when defining tasks, results when executing tasks, and problems when discussing tasks. +- If you can’t explain something clearly, it’s probably not because you are action-oriented, but rather because you haven’t thought it through, which becomes more obvious in promotional contexts. +- When someone excels at solving a particular problem, over time, they may find it difficult to detach from that scenario (once a label is pinned on someone, it’s challenging to remove it). -- 定义问题是个高阶能力,尽早形成 发现问题->定义问题->解决问题->消灭问题 的思维闭环; -- 定事情价值导向,做事情结果导向,讲事情问题导向; -- 讲不清楚,大概率不是因为自己是实干型,而是没想清楚,在晋升场更加明显; -- 当一个人擅长解决某一场景的问题的时候,时间越久也许越离不开这个场景(被人贴上一个标签很难,撕掉一个标签更难)。 +## Managing Emotions -## 要栓住情绪 +- Learn to control your emotions; no one takes an angry person seriously. +- Stay rational, no matter how wronged or angry you feel; avoid becoming someone who needs to be coddled. +- Only those who are sufficiently confident can openly admit their own issues. Often, we get angered simply because someone pointed out our deep-seated insecurities. +- What hurts us most is not others' actions or our mistakes, but our responses to those mistakes. -- 学会控制情绪,没人会认真听一个愤怒的人在说什么; -- 再委屈、再愤怒也要保持理智,不要让自己成为需要被哄着的那种人; -- 足够自信的人才会坦率的承认自己的问题,很多时候我们被激怒了,只是因为对方指出了自己藏在深处的自卑; -- 伤害我们最深的既不是别人的所作所为,也不是自己犯的错误,而是我们对错误的回应。 +## Becoming a Leader -## 成为 Leader +> Managers have subordinates, while leaders have followers. You don’t need many managers, but anyone can be a leader. -> Manager 有下属,Leader 有追随者,管理者不需要很多,但人人都可以是 Leader。 - -- 让你信服、愿意追随的人不是职务上的 Manager,而是在帮助自己的那个人,自己想服众的话道理一样; -- 不要轻易对人做负面评价,片面认知下的评价可能不准确,不经意的传播更是会给对方带来极大的困扰; -- Leader 如果不认同公司的使命、愿景、价值观,会过的特别痛苦; -- 困难时候不要否定自己的队友,多给及时、正向的反馈; -- 船长最重要的事情不是造船,而是激发水手对大海的向往; -- Leader 的天然职责是让团队活下去,唯一的途径是实现上级、老板、公司经营者的目标,越是艰难的时候越明显; -- Leader 的重要职责是识别团队需要被做的事情,并坚定信念,使众人行,越是艰难的时候越要坚定; -- Leader 应该让自己遇到的每个人都感觉自己很重要、被需要。 +- The person you trust and want to follow is not necessarily a positional manager but someone who helps you in your growth; a good leader shares the same rationale. +- Avoid hastily making negative judgments about others; misinformed opinions can be inaccurate, and carelessly sharing them can upset others significantly. +- If a leader does not resonate with the company’s mission, vision, and values, they will have a particularly difficult time. +- In challenging times, do not deny your teammates; provide timely and positive feedback. +- The most important thing for a captain is not to build the ship but to inspire sailors' longing for the sea. +- A leader’s inherent duty is to ensure the team thrives, and the only way to achieve this is to meet the objectives set by superiors, bosses, and company owners, with this becoming even more evident during tough times. +- A key responsibility of a leader is to identify what needs to be done within the team and steadfastly guide everyone to action, especially in challenging times. +- A leader should make everyone they encounter feel important and needed. diff --git a/docs/high-quality-technical-articles/work/employee-performance.md b/docs/high-quality-technical-articles/work/employee-performance.md index af22114fb04..c489530c0e3 100644 --- a/docs/high-quality-technical-articles/work/employee-performance.md +++ b/docs/high-quality-technical-articles/work/employee-performance.md @@ -1,132 +1,68 @@ --- -title: 聊聊大厂的绩效考核 -category: 技术文章精选集 +title: Let's Talk About Performance Evaluation in Big Companies +category: Selected Technical Articles tag: - - 工作 + - Work --- -> **内容概览**: +> **Content Overview**: > -> - 在大部分公司,绩效跟你的年终奖、职级晋升、薪水涨幅等等福利是直接相关的。 -> - 你的上级、上上级对你的绩效拥有绝对的话语权,这是潜规则,放到任何公司都是。成年人的世界,没有绝对的公平,绩效考核尤为明显。 -> - 提升绩效的打法: -> - 短期打法:找出 1-2 件事,体现出你的独特价值(抓关键事件)。 -> - 长期打法:通过一步步信任的建立,成为团队的核心人员或者是老板的心腹,具备不可替代性。 +> - In most companies, performance is directly related to your year-end bonus, promotion, salary increase, and other benefits. +> - Your immediate supervisor and their supervisor have absolute authority over your performance evaluation; this is an unspoken rule that applies to any company. In the adult world, there is no absolute fairness, and performance evaluation is particularly evident in this regard. +> - Strategies to improve performance: +> - Short-term strategy: Identify 1-2 key tasks that showcase your unique value (focus on critical events). +> - Long-term strategy: Build trust step by step to become a core member of the team or a trusted confidant of the boss, making yourself irreplaceable. > -> **原文地址**: +> **Original Article Link**: -在新公司度过了一个完整的 Q3 季度,被打了绩效,也给下属打了绩效,感慨颇深。 +After spending a complete Q3 in a new company, I have been evaluated on my performance and have also evaluated my subordinates, which has left me with a lot of thoughts. -今天就好好聊聊**大厂打工人最最关心的「绩效考核」**,谈谈它背后的逻辑以及潜规则,摸清楚了它,你在大厂这片丛林里才能更好的生存下去。 +Today, let's discuss **the performance evaluation that workers in big companies care about the most**, exploring the logic and unspoken rules behind it. Understanding this will help you survive better in the jungle of big companies. -## 大厂的绩效到底有多重要? +## How Important is Performance Evaluation in Big Companies? -先从公司角度,谈谈为什么需要绩效考核? +First, let's talk about why performance evaluation is necessary from the company's perspective. -有一个著名的管理者言论,即:企业战略的上三路和下三路。 +There is a famous saying by a management expert: the upper three levels and the lower three levels of corporate strategy. -> 上三路是使命、愿景、价值观,下三路是组织、人才、KPI。下三路需要确保上三路能执行下去,否则便是空谈。那怎么才能达成呢? +> The upper three levels are mission, vision, and values; the lower three levels are organization, talent, and KPI. The lower levels need to ensure that the upper levels can be executed; otherwise, it is just empty talk. So how can this be achieved? -马老板在湖畔大学的课堂上,对底下众多 CEO 学员说,“只能靠 KPI。没有 KPI,一切都是空话,组织和公司是不会进步的”。 +At a class at Lakeview University, Boss Ma told a group of CEOs, "It can only rely on KPI. Without KPI, everything is just empty talk, and the organization and company will not progress." -所以,KPI 一般是用来承接企业战略的。身处大厂的打工者们,也能深深感受到:每个季度的 KPI 是如何从大 Boss、到 Boss、再到基层,一层层拆解下来的,最终让所有人朝着一个方向行动,这便是 KPI 对于公司的意义。 +Therefore, KPI is generally used to implement corporate strategy. Workers in big companies can deeply feel how each quarter's KPI is broken down from the big boss to the boss and then to the grassroots level, ultimately guiding everyone to act in the same direction. This is the significance of KPI for the company. -然鹅,并非每个员工都会站在 CEO 的高度去理解 KPI 的价值,大家更关注的是 KPI 对于我个人来说到底有什么意义? +However, not every employee will understand the value of KPI from the CEO's perspective; they are more concerned about what KPI means for them personally. -在互联网大厂,每家公司都会设定一套绩效考核体系,字节用的是 OKR,阿里用的是 KPI,通常都是「271」 制度,即: +In internet giants, every company sets up a performance evaluation system. ByteDance uses OKR, while Alibaba uses KPI, typically following the "271" system, which means: -> 20% 的比例是 A+ 和 A,对应明星员工。 +> 20% are A+ and A, corresponding to star employees. > -> 70% 的比例是 B,对应普通员工。 +> 70% are B, corresponding to average employees. > -> 10% 的比例是 C 和 C-,对应需要绩效改进或者淘汰的员工。 +> 10% are C and C-, corresponding to employees who need performance improvement or are to be eliminated. -有了三六九等,然后才有了利益分配。 +With this ranking system, benefits can then be distributed. -**在大厂,绩效结果跟奖金、晋升、薪水涨幅、股票授予是直接相关的。在内卷的今天,甚至可以直接划上等号。** +**In big companies, performance results are directly related to bonuses, promotions, salary increases, and stock grants. In today's competitive environment, they can even be equated.** -绩效好的员工,奖金必然多,一年可能调薪两次,晋升答辩时能 PK 掉绩效一般的人,职级低的人甚至可以晋升免试。 +Employees with good performance will naturally receive more bonuses, may have salary adjustments twice a year, and can outperform average performers during promotion discussions. Those with lower performance may end up working a whole year for nothing or even be let go (the elimination of the bottom performers is an unwritten rule in big companies). -而绩效差的人,有可能一年白干,甚至走人(大厂的末尾淘汰是不成文的规定)。 +In summary, all the direct benefits you can think of are closely related to "performance." Therefore, in the jungle of big companies filled with skilled individuals, understanding the logic behind performance is not only a survival strategy but also a valuable skill. -总之,你能想到的直接利益都和「绩效」息息相关。所以,在大厂这片高手众多的丛林里,多琢磨下绩效背后的逻辑,既是生存之道,更是一技之长。 +## How Do You View Performance Evaluation? -## 你是怎么看待绩效的? +Most people subconsciously want to break the rules used to evaluate them rather than be constrained by them. -凡是用来考核人的规则,大部分人在潜意识里都想去突破它,而不是被束缚。 +At least in my first few years of work, when I saw some colleagues leave quietly due to receiving a C rating, I thought performance evaluation was a cold-blooded management tool. -至少在我刚工作的前几年,看着身边有些同事因为背个 C 黯然离开的时候,觉得绩效考核就是一个冷血的管理工具。 +Especially when encountering a leader I didn't respect, I was quite dismissive of the performance rating they gave me. -尤其遇到自己看不上的领导时,对于他给我打的绩效,其实也是很不屑的。 +Today, having seen too many negative examples and having stumbled into some pitfalls myself, I gradually realized that my initial thoughts, aside from providing a momentary sense of satisfaction, seemed to have no real effect and even distorted my work approach. -到今天,实在见过太多的反面案例了,自己也踩过一些坑,逐渐认识到:当初的想法除了让自己心里爽一点,好像起不到任何作用,甚至会让我的工作方式变形。 +As my mindset changed, so did my attitude toward performance evaluation. At least two points need to be clear for workers. -当思维方式变了,也就改变了我对绩效的态度,至少有两点我认为是打工人需要看清的。 +**First, your immediate supervisor and their supervisor have absolute authority over your performance evaluation; this is an unspoken rule that applies to any company.** -**第一,你的上级、上上级对你的绩效拥有绝对的话语权,这是潜规则,放到任何公司都是。** +You can observe that those who develop particularly well around you, besides having strong personal abilities, are almost always adept at utilizing the rules rather than challenging them. -大家可以去看看身边发展特别好的人,除了有很强的个人能力以外,几乎都是善于利用规则,而不是去挑战规则的人。 - -当然,我并不是说你要一味地去跪舔你的领导,而是表达:工作中不要站在领导的对立面去做对抗,如果领导做法很过分,要么直接沟通去影响他,要么选择离开。 - -**第二,成年人的世界,没有绝对的公平,绩效考核尤为明显。** - -我所待过的团队,绩效考核还是相对公平的,虽然也存在受照顾的情况,但都是个例。 - -另外就是,技术岗的绩效考核不同于销售或者运营岗,很容易指标化。 - -需求吞吐量、BUG 数、线上事故... 的确有一大堆研发效能指标,但这些指标在绩效考核时是否会被参考?具体又该如何分配比重?本身就是一个扯不清楚的难题。 - -最终决定你绩效结果的还是你领导的主观判断。你所见到的 360 环评,以及弄一些指标排序,这些都只是将绩效结果合理化的一种方式,并非关键所在。 - -因此,多琢磨如何去影响你的领导?站在他的视角去审视他在绩效考核时到底关注哪些核心点?这才是至关重要的。 - -上面讲了一堆潜规则,是不是意味着绩效考核是可以投机取巧,完全不看工作业绩呢,当然不是。 - -“你的努力不一定会被看见”、“你的努力应该有的放矢”,大家先记住这两条。 - -下面我再展开聊聊,大家最最关心的 A 和 C,它们背后的逻辑。 - -## 绩效被打 A 和 C 的逻辑是什么? - -“铆足了劲拿不到 A,一不留神居然拿了个 C”,这是绝大多数打工人最真实的职场现状。 - -A 和 C 属于绩效的两个极端,背后的逻辑类似,反着理解即可,下面我详细分析下 C。 - -先从我身边人的情况说起,我所看到的案例绝大多数都属于:绩效被打了 C,完全没有任何预感,主管跟他沟通结果时,还是一脸懵逼,“为什么会给我打 C?一定是黑我呀!”。 - -前阵子听公司一位大佬分享,用他的话说,这种人就是没有「角色认知」,他不知道他所处的角色和职级该做好哪些事?做成什么样才算「做好了」?被打 C 后自然觉得是在背锅。 - -所以,务必确保你对于当前角色是认知到位的,这样才称得上进入了「工作状态」,否则你的一次松懈,一段不太好的表现,很可能导致 C 落在你的头上,岗位越高,摔得越重。 - -有了角色认知,再说下对绩效的认知。 - -第一,团队很优秀,是不是不用背 C?不是!大厂的 C 都是强制分配的,再优秀的团队也会有 C。所以团队越厉害,竞争越惨烈。 - -第二,完成了 KPI,没有工作失误,是不是就万事大吉,不用背 C?不是,绩效是相对的,你必须清楚你在团队所处的位置,你在老板眼中的排序,慢慢练出这种嗅觉。 - -懂了上面这些道理,很自然就能知道打 C 的逻辑,C 会集中在两类人上: - -> 1、工作表现称不上角色要求的人。 -> -> 2、在老板眼里排序靠后,就算离开,对团队影响也很小的人。 - -要规避 C,有两种打法。 - -第 1 种是短期打法:抓关键事件,能不能找出 1-2 件事,体现出你的独特价值(比如本身影响力很大的项目,或者是领导最重视的事),相当于让你的排序有了最基本的保障。 - -这种打法,你不能等到评价时再去改变,一定是在前期就抓住机会,承担起最有挑战的任务,然后全力以赴,做好了拿 A,不弄砸也不至于背 C,就怕静水潜流,躺平了去工作。 - -第 2 种是长期打法:通过一步步信任的建立,成为团队的核心人员或者是老板的心腹,具备不可替代性。 - -上面两种打法都是大的思路,还有很多锦上添花的技巧,比如:加强主动汇报(抹平领导的信息差)、让关键干系人给你点赞(能影响到你领导做出绩效决策的人)。 - -## 写在最后 - -有人的地方就有江湖,有江湖就一定有规则,大厂平面看似平静,其实在绩效考核、晋升等利益点面前,都是一场厮杀。 - -当大家攻山头的能力都很强时,**到底做成什么样才算做好了?**当你弄清楚了这个玄机,职场也就看透了。 - -如果这篇文章让你有一点启发,来个点赞和在看呀!我是武哥,我们下期见! - - +Of course, I am not saying you should blindly flatter your leaders; rather, I mean that you should not position yourself in opposition to your leaders at work. If your leader's actions are excessive, either communicate directly to influence diff --git a/docs/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.md b/docs/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.md index 72c672a5f92..c0e75573824 100644 --- a/docs/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.md +++ b/docs/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.md @@ -1,94 +1,94 @@ --- -title: 新入职一家公司如何快速进入工作状态 -category: 技术文章精选集 +title: How to Quickly Enter Work Mode After Joining a New Company +category: Selected Technical Articles tag: - - 工作 + - Work --- -> **推荐语**:强烈建议每一位即将入职/在职的小伙伴看看这篇文章,看完之后可以帮助你少踩很多坑。整篇文章逻辑清晰,内容全面! +> **Recommendation**: It is highly recommended that every newcomer or existing employee read this article. It can help you avoid many pitfalls. The entire article is logically clear and comprehensive! > -> **原文地址**: +> **Original link**: -![新入职一家公司如何快速进入状态](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/work/%E6%96%B0%E5%85%A5%E8%81%8C%E4%B8%80%E5%AE%B6%E5%85%AC%E5%8F%B8%E5%A6%82%E4%BD%95%E5%BF%AB%E9%80%9F%E8%BF%9B%E5%85%A5%E7%8A%B6%E6%80%81.png) +![How to Quickly Enter Work Mode After Joining a New Company](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/work/%E6%96%B0%E5%85%A5%E8%81%8C%E4%B8%80%E5%AE%B6%E5%85%AC%E5%8F%B8%E5%A6%82%E4%BD%95%E5%BF%AB%E9%80%9F%E8%BF%9B%E5%85%A5%E7%8A%B6%E6%80%81.png) -一年一度的金三银四跳槽大戏即将落幕,相信很多跳槽的小伙伴们已经找到了心仪的工作,即将或已经有了新的开始。 +The annual job-hopping season of "March Gold and April Silver" is about to come to an end. I believe many job-hoppers have already found their ideal positions and are about to or have already started anew. -相信有过跳槽经验的小伙伴们都知道,每到一个新的公司面临的可能都是新的业务、新的技术、新的团队……这些可能会打破你原来工作思维、编码习惯、合作方式…… +For those with job-hopping experience, you know that each new company comes with potentially new business, new technologies, and new teams... These may disrupt your original working thought processes, coding habits, and collaboration methods... -而于公司而言,又不能给你几个月的时间去慢慢的熟悉。这个时候,如何快速进入工作状态,尽快发挥自己的价值是非常重要的。 +For companies, they cannot give you months to get acclimatized. At this time, how to quickly enter work mode and quickly showcase your value is extremely important. -有些人可能会很幸运,入职的公司会有完善的流程与机制,通过一带一、各种培训等方式可以在短时间内快速的让新人进入工作状态。有些人可能就没有那么幸运了,就比如我在几年前跳槽进入某厂的时候,当时还没有像我们现在这么完善的带新人融入的机制,又赶上团队最忙的一段时间,刚一入职的当天下午就让给了我几个线上问题去排查,也没有任何的文档和培训。遇到情况,很多人可能会因为难以快速适应,最终承受不起压力而萌生退意。 +Some may be fortunate enough to join companies with well-established processes and mechanisms, allowing new employees to quickly integrate into work through mentoring, various training, etc. Others may not be so lucky; for example, when I switched jobs to a certain company a few years ago, there was no such complete mechanism for onboarding. I also happened to join during one of the busiest periods for the team, and on my first day, I was immediately given several issues to troubleshoot online without any documentation or training. Many people may find it hard to adapt quickly, leading them to feel overwhelmed by the pressure and consider leaving. ![bad175e3a380bea.](https://hunter-picgos.oss-cn-shanghai.aliyuncs.com/picgo/bad175e3a380bea..jpg) -那么,**我们应该如何去快速的让自己进入工作状态,适应新的工作节奏呢?** +So, **how can we quickly adapt ourselves to a new work mode and work rhythm?** -新的工作面对着一堆的代码仓库,很多人常常感觉无从下手。但回顾一下自己过往的工作与项目的经验,我们可以发现它们有着异曲同工之处。当开始一个新的项目,一般会经历几个步骤:需求->设计->开发->测试->发布,就这么循环往复,我们完成了一个又一个的项目。 +When starting a new job that involves a pile of code repositories, many people often feel lost. However, if we reflect on our past work and project experiences, we can find similarities. When starting a new project, we generally go through several steps: requirements -> design -> development -> testing -> release, and this cycle repeats as we complete project after project. -![项目流程](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/work/image-20220704191430466.png) +![Project Process](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/work/image-20220704191430466.png) -而在这个过程中主要有四个方面的知识那就是业务、技术、项目与团队贯穿始终。新入职一家公司,我们第一阶段的目标就是要具备能够跟着团队做项目的能力,因此我们所应尽快掌握的知识点也要从这四个方面入手。 +In this process, there are four main aspects of knowledge that run through everything: business, technology, projects, and team. Our first stage goal when starting a new job is to have the ability to follow the team in project execution, so the knowledge we should quickly master should begin with these four aspects. -## 业务 +## Business -很多人可能会认为作为一个技术人,最应该了解的不应该是技术吗?于是他们在进入一家公司后,就迫不及待的研究起来了一些技术文档,系统架构,甚至抱起来源代码就开始“啃”,如果你也是这么做的,那就大错特错了!在几乎所有的公司里,技术都是作为一个工具存在的,虽然它很重要,但是它也是为了承载业务所存在的,技术解决了如何做的问题,而业务却告诉我们,做什么,为什么做。一旦脱离了业务,那么技术的存在将毫无意义。 +Many might think that as a tech person, the most important thing to understand is technology, right? And so, after joining a company, they eagerly dive into technical documentation, system architecture, or even start "gnawing" through the source code. If you’re doing this, you're making a big mistake! In almost all companies, technology serves as a tool. Although it’s important, it exists to support the business. Technology addresses the "how-to" aspect, while business tells us "what to do" and "why to do it." Once you detach from business, the existence of technology becomes meaningless. -想要了解业务,有两个非常重要的方式 +To understand business, there are two very important methods: -**一是靠问** +**First is to ask.** -如果你加入的团队,有着完善的业务培训机制,详尽的需求文档,也许你不需要过多的询问就可以了解业务,但这只是理想中的情况,大多数公司是没有这个条件的。因此我们只能靠问。 +If you're in a team with a robust business training mechanism and detailed requirement documents, you might not need to ask too many questions to understand the business. However, this ideal situation is rare; most companies lack such conditions. Therefore, we can only rely on asking questions. -这里不得不提的是,作为一个新人一定要有一定的脸皮厚度,不懂就要问。我见过很多新人会因为内向、腼腆,遇到疑问总是不好意思去问,这导致他们很长一段时间都难以融入团队、承担更重要的责任。不怕要怕挨训、怕被怼,而且我相信绝对多数的程序员还是很好沟通的! +It's crucial for newcomers to be willing to ask questions without feeling embarrassed. I've seen many newcomers hesitate to ask due to being shy or introverted, which causes them to struggle to integrate into the team and take on responsibilities for a long time. Don’t fear asking questions — it's the fear of being scolded or rebuffed that is more detrimental. I believe the vast majority of programmers are quite approachable! -**二是靠测试** +**Second is through testing.** -我认为测试绝对是一个人快速了解团队业务的方式。通过测试我们可以走一走自己团队所负责项目的整体流程,如果遇到自己走不下去或想不通的地方及时去问,在这个过程中我们自然而然的就可以快速的了解到核心的业务流程。 +I believe that testing is definitely a great way for someone to quickly understand the team’s business. Through testing, we can walk through the entire process of the project our team is responsible for. If we encounter something we can’t figure out, we should timely ask questions. In this process, we naturally grasp the core business workflow quickly. -在了解业务的过程中,我们应该注意的是不要让自己过多的去追求细节,我们的目的是先能够整体了解业务流程,我们面向哪些用户,提供了哪些服务…… +While understanding the business, we should focus on not getting too caught up in details. Our goal is to get a holistic understanding of the business process, the users we are targeting, and the services we provide… -## 技术 +## Technology -在我们初步了解完业务之后,就该到技术了,也许你已经按捺不住翻开源代码的准备了,但还是要先提醒你一句先不要着急。 +Once we have a preliminary understanding of the business, it’s time to delve into technology. You might be eager to start flipping through the source code, but hold on a minute. -这个时候我们应该先按照自己了解到的业务,结合自己过往的工作经验去思考一下如果是自己去实现这个系统,应该如何去做?这一步很重要,它可以在后面我们具体去了解系统的技术实现的时候去对比一下与自己的实现思路有哪些差异,为什么会有这些差异,哪些更好,哪些不好,对于不好我们可以提出自己的意见,对于更好的我们可以吸收学习为己用! +At this point, we should consider how we would implement the system based on our understanding of the business and our past experiences. This step is essential as it allows us to compare the implementation approach against actual system technical implementations later. We can identify any differences, analyze why those differences exist, understand what works better, and what doesn’t. For the aspects that aren’t as effective, we can suggest improvements, and for those that are better, we can learn and integrate them into our own practice! -接下来,我们就是要了解技术了,但也不是一上来就去翻源代码。 **应该按照从宏观到细节,由外而内逐步地对系统进行分析。** +Next, we should explore the technology, but not by jumping straight into the source code. **We should analyze the system gradually from a macro to a micro perspective.** -首先,我们应该简单的了解一下 **自己团队/项目的所用到的技术栈** ,Java 还是.NET、亦或是多种语言并存,项目是前后端分离还是服务端全包,使用的数据库是 MySQL 还是 PostgreSQL……,这样我们可能会对所用到的技术和框架,以及自己所负责的内容有一定的预期,这一点有的人可能在面试的时候就会简单了解过。 +First, we should get a simple overview of **the technology stack used by our team/project**, whether it’s Java, .NET, or a combination of languages. Is it a front-end and back-end separation, or does the server cover everything? What kind of database is used — MySQL, PostgreSQL? This way, we can set certain expectations regarding the technologies, frameworks, and our responsibilities, which some might have gathered even during the interview process. -下一步,我们应该了解的是 **系统的宏观业务架构** 。自己的团队主要负责哪些系统,每个系统又主要包含哪些模块,又与哪些外部系统进行交互……对于这些,最好可以通过流程图或者思维导图等方式整理出来。 +Next, we should understand **the macro business architecture of the system**. What systems does our team primarily manage? What modules are each of these systems composed of? How do they interact with external systems? It’s ideal to document these in the form of flowcharts or mind maps. -然后,我们要做的是看一下 **自己的团队提供了哪些对外的接口或者服务** 。每个接口和服务所提供功能是什么。这一点我们可以继续去测试自己的系统,这个时候我们要看一看主要流程中主要包含了哪些页面,每个页面又调用了后端的哪些接口,每个后端接口又对应着哪个代码仓库。(如果是单纯做后端服务的,可以看一下我们提供了哪些服务,又有哪些上游服务,每个上游服务调用自己团队的哪些服务……),同样我们应该用画图的形式整理出来。 +Then, we need to look at **what external interfaces or services our team provides**. What functions does each interface and service offer? We can continue testing our system; at this stage, we should examine which pages are included in the main processes, what backend APIs are called by each page, and which code repositories correspond to each backend API. (If we are only providing backend services, we can look at what services we offer and which upstream services call them…). Again, this should be organized visually. -接着,我们要了解一下 **自己的系统或服务又依赖了哪些外部服务** ,也就是说需要哪些外部系统的支持,这些服务也许是团队之外、公司之外,也可能是其他公司提供的。这个时候我们可以简单的进入代码看一下与外部系统的交互是怎么做的,包括通讯框架(REST、RPC)、通讯协议…… +Next, we should understand **what external services our system or services rely on** — i.e., what external systems support this. These services might originate from outside the team or company, or they could come from other companies. At this point, we can look at the code and examine how interactions with external systems are set up, including communication frameworks (REST, RPC) and protocols. -到了代码层面,我们首先应该了解每个模块代码的层次结构,一个模块分了多少层,每个层次的职责是什么,了解了这个就对系统的整个设计有了初步的概念,紧接着就是代码的目录结构、配置文件的位置。 +When we reach the code layer, the first thing we should understand is the hierarchical structure of the code for each module — how many layers each module has, what the responsibility of each layer is. Understanding this gives us a preliminary concept of the system's entire design, followed by locating the code directory structure and configuration files. -最后,我们可以寻找一个示例,可以是一个接口,一个页面,让我们的思路跟随者代码的运行的路线,从入参到出参,完整的走一遍来验证一下我们之前的了解。 +Finally, we should look for an example — it could be an interface or a page — to follow the code's execution path through the inputs to outputs for a complete walkthrough to verify our previous understanding. -到了这里我们对于技术层面的了解就可以先告一段落了,我们的目的知识对系统有一个初步的认知,更细节的东西,后面我们会有大把的时间去了解 +At this point, we can temporarily conclude our understanding of the technical aspects. Our aim is to gain a preliminary comprehension of the system; we’ll have plenty of time later to delve deeper into the details. -## 项目与团队 +## Projects and Teams -上面我们提到,新入职一家公司,第一阶段的目标是有跟着团队做项目的能力,接下来我们要了解的就是项目是如何运作的。 +As mentioned earlier, when starting at a new company, the first phase goal is to be capable of working on projects alongside the team. Next, we need to understand how projects operate. -我们应该把握从需求设计到代码编写入库最终到发布上线的整个过程中的一些关键点。例如项目采用敏捷还是瀑布的模式,一个迭代周期是多长,需求的来源以及展现形式,有没有需求评审,代码的编写规范是什么,编写完成后如何构建,如何入库,有没有提交规范,如何交付测试,发布前的准备是什么,发布工具如何使用…… +We should capture some key points in the process from requirement design to code writing and ultimately to release and online deployment. For instance, does the project adopt an Agile or Waterfall model? What is the duration of an iteration cycle? Where do requirements come from, and how are they presented? Is there a requirement review process? What are the coding standards? How is the code structured? What regulations govern submission? How is testing delivered? What preparations are needed before release, and how are release tools utilized? -关于项目我们只需要观察同事,或者自己亲身经历一个迭代的开发,就能够大概了解清楚。 +For understanding projects, simply observing colleagues or personally experiencing an iteration of development will provide a rough understanding. -在了解项目运作的同时,我们还应该去了解团队,同样我们应该先从外部开始,我们对接了哪些外部团队,比如需求从哪里来,是否对接公司外部的团队,提供服务的上游团队有哪些,依赖的下游团队有哪些,团队之间如何沟通,常用的沟通方式是什么…… +While understanding project operations, we should also familiarize ourselves with the team. Again, we start from the outside. Which external teams are we interfacing with? For instance, where do requirements come from? Are there connections to external teams? What upstream teams provide our services, and what downstream teams rely on us? How do teams communicate, and what are the common communication methods? -接下来则是团队内部,团队中有哪些角色,每个人的职责是什么,这样遇到问题我们也可以清楚的找到对应的同事寻求帮助。是否有一些定期的活动与会议,例如每日站会、周例会,是否有一些约定俗成的规矩,是否有一些内部评审,分享机制…… +Next, we focus on internal team dynamics. What roles exist within the team, and what are each person's responsibilities? This helps us identify whom to approach when issues arise. Are there any regular activities or meetings, such as daily stand-ups or weekly review meetings? Are there any conventional rules, internal reviews, or sharing mechanisms? -## 总结 +## Conclusion -新入职一家公司,面临新的工作挑战,能够尽快进入工作状态,实现自己的价值,将会给你带来一个好的开始。 +When joining a new company and facing new work challenges, being able to quickly enter work mode and demonstrate your value will lead to a strong start. -作为一个程序员,能够尽快进入工作状态,意味着我们首先应该具备跟着团队做项目的能力,这里我站在了一个后端开发的角度上从业务、技术、项目与团队四个方面总结了一些方法和经验。 +As a programmer, quickly entering work mode means we should first have the ability to collaborate with the team on projects. From the perspective of backend development, I summarized some methods and experiences across four areas: business, technology, projects, and teams. -关于如何快速进入工作状态,如果你有好的方法与建议,欢迎在评论区留言。 +If you have good methods or suggestions on how to quickly enter work mode, feel free to share them in the comments. -最后我们用一张思维导图来回顾一下这篇文章的内容。如果你觉得这篇文章对你有所帮助,可以关注文末公众号,我会经常分享一些自己成长过程中的经验与心得,与大家一起学习与进步。 +Finally, let’s review the contents of this article with a mind map. If you find this article helpful, you can follow the WeChat account at the end of the article, where I will frequently share my experiences and insights gained throughout my growth journey for everyone to learn and improve together. diff --git a/docs/home.md b/docs/home.md index 015a9105da3..4bfcb9b13cb 100644 --- a/docs/home.md +++ b/docs/home.md @@ -1,430 +1,62 @@ --- icon: creative -title: JavaGuide(Java学习&面试指南) +title: JavaGuide (Java Learning & Interview Guide) --- -::: tip 友情提示 +::: tip Friendly Reminder -- **面试专版**:准备 Java 面试的小伙伴可以考虑面试专版:**[《Java 面试指北 》](./zhuanlan/java-mian-shi-zhi-bei.md)** (质量很高,专为面试打造,配合 JavaGuide 食用)。 -- **知识星球**:专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 **[JavaGuide 知识星球](./about-the-author/zhishixingqiu-two-years.md)**(点击链接即可查看星球的详细介绍,一定确定自己真的需要再加入)。 -- **使用建议** :有水平的面试官都是顺着项目经历挖掘技术问题。一定不要死记硬背技术八股文!详细的学习建议请参考:[JavaGuide 使用建议](./javaguide/use-suggestion.md)。 -- **求个 Star**:如果觉得 JavaGuide 的内容对你有帮助的话,还请点个免费的 Star,这是对我最大的鼓励,感谢各位一起同行,共勉!传送门:[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)。 -- **转载须知**:以下所有文章如非文首说明为转载皆为 JavaGuide 原创,转载请在文首注明出处。如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! +- **Interview Special Edition**: Friends preparing for Java interviews may consider the Interview Special Edition: **[“Java Interview Guide”](./zhuanlan/java-mian-shi-zhi-bei.md)** (high quality, specifically designed for interviews, best used in conjunction with JavaGuide). +- **Knowledge Planet**: Exclusive interview booklet/one-on-one communication/resume modification/exclusive job-seeking guide, welcome to join **[JavaGuide Knowledge Planet](./about-the-author/zhishixingqiu-two-years.md)** (click the link to view the detailed introduction of the planet, make sure you really need to join). +- **Usage Suggestions**: Competent interviewers often dig into technical questions based on project experience. Do not memorize technical jargon! For detailed learning suggestions, please refer to: [JavaGuide Usage Suggestions](./javaguide/use-suggestion.md). +- **Request a Star**: If you find the content of JavaGuide helpful, please give it a free Star, which is the greatest encouragement for me. Thank you all for walking together, let’s encourage each other! Portal: [GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide). +- **Reprint Notice**: All articles below, unless stated otherwise at the beginning, are original works of JavaGuide. Please indicate the source at the beginning when reprinting. If malicious plagiarism or copying is found, legal action will be taken to protect my rights. Let’s work together to maintain a good technical creation environment! ::: ## Java -### 基础 +### Basics -**知识点/面试题总结** : (必看:+1: ): +**Knowledge Points/Interview Questions Summary**: (Must-read: +1): -- [Java 基础常见知识点&面试题总结(上)](./java/basis/java-basic-questions-01.md) -- [Java 基础常见知识点&面试题总结(中)](./java/basis/java-basic-questions-02.md) -- [Java 基础常见知识点&面试题总结(下)](./java/basis/java-basic-questions-03.md) +- [Common Java Basic Knowledge Points & Interview Questions Summary (Part 1)](./java/basis/java-basic-questions-01.md) +- [Common Java Basic Knowledge Points & Interview Questions Summary (Part 2)](./java/basis/java-basic-questions-02.md) +- [Common Java Basic Knowledge Points & Interview Questions Summary (Part 3)](./java/basis/java-basic-questions-03.md) -**重要知识点详解**: +**Important Knowledge Points Explained**: -- [为什么 Java 中只有值传递?](./java/basis/why-there-only-value-passing-in-java.md) -- [Java 序列化详解](./java/basis/serialization.md) -- [泛型&通配符详解](./java/basis/generics-and-wildcards.md) -- [Java 反射机制详解](./java/basis/reflection.md) -- [Java 代理模式详解](./java/basis/proxy.md) -- [BigDecimal 详解](./java/basis/bigdecimal.md) -- [Java 魔法类 Unsafe 详解](./java/basis/unsafe.md) -- [Java SPI 机制详解](./java/basis/spi.md) -- [Java 语法糖详解](./java/basis/syntactic-sugar.md) +- [Why is there only value passing in Java?](./java/basis/why-there-only-value-passing-in-java.md) +- [Detailed Explanation of Java Serialization](./java/basis/serialization.md) +- [Detailed Explanation of Generics & Wildcards](./java/basis/generics-and-wildcards.md) +- [Detailed Explanation of Java Reflection Mechanism](./java/basis/reflection.md) +- [Detailed Explanation of Java Proxy Pattern](./java/basis/proxy.md) +- [Detailed Explanation of BigDecimal](./java/basis/bigdecimal.md) +- [Detailed Explanation of Java Magic Class Unsafe](./java/basis/unsafe.md) +- [Detailed Explanation of Java SPI Mechanism](./java/basis/spi.md) +- [Detailed Explanation of Java Syntactic Sugar](./java/basis/syntactic-sugar.md) -### 集合 +### Collections -**知识点/面试题总结**: +**Knowledge Points/Interview Questions Summary**: -- [Java 集合常见知识点&面试题总结(上)](./java/collection/java-collection-questions-01.md) (必看 :+1:) -- [Java 集合常见知识点&面试题总结(下)](./java/collection/java-collection-questions-02.md) (必看 :+1:) -- [Java 集合使用注意事项总结](./java/collection/java-collection-precautions-for-use.md) +- [Common Java Collection Knowledge Points & Interview Questions Summary (Part 1)](./java/collection/java-collection-questions-01.md) (Must-read: +1) +- [Common Java Collection Knowledge Points & Interview Questions Summary (Part 2)](./java/collection/java-collection-questions-02.md) (Must-read: +1) +- [Summary of Precautions for Using Java Collections](./java/collection/java-collection-precautions-for-use.md) -**源码分析**: +**Source Code Analysis**: -- [ArrayList 核心源码+扩容机制分析](./java/collection/arraylist-source-code.md) -- [LinkedList 核心源码分析](./java/collection/linkedlist-source-code.md) -- [HashMap 核心源码+底层数据结构分析](./java/collection/hashmap-source-code.md) -- [ConcurrentHashMap 核心源码+底层数据结构分析](./java/collection/concurrent-hash-map-source-code.md) -- [LinkedHashMap 核心源码分析](./java/collection/linkedhashmap-source-code.md) -- [CopyOnWriteArrayList 核心源码分析](./java/collection/copyonwritearraylist-source-code.md) -- [ArrayBlockingQueue 核心源码分析](./java/collection/arrayblockingqueue-source-code.md) -- [PriorityQueue 核心源码分析](./java/collection/priorityqueue-source-code.md) -- [DelayQueue 核心源码分析](./java/collection/priorityqueue-source-code.md) +- [Core Source Code + Expansion Mechanism Analysis of ArrayList](./java/collection/arraylist-source-code.md) +- [Core Source Code Analysis of LinkedList](./java/collection/linkedlist-source-code.md) +- [Core Source Code + Underlying Data Structure Analysis of HashMap](./java/collection/hashmap-source-code.md) +- [Core Source Code + Underlying Data Structure Analysis of ConcurrentHashMap](./java/collection/concurrent-hash-map-source-code.md) +- [Core Source Code Analysis of LinkedHashMap](./java/collection/linkedhashmap-source-code.md) +- [Core Source Code Analysis of CopyOnWriteArrayList](./java/collection/copyonwritearraylist-source-code.md) +- [Core Source Code Analysis of ArrayBlockingQueue](./java/collection/arrayblockingqueue-source-code.md) +- [Core Source Code Analysis of PriorityQueue](./java/collection/priorityqueue-source-code.md) +- [Core Source Code Analysis of DelayQueue](./java/collection/priorityqueue-source-code.md) ### IO -- [IO 基础知识总结](./java/io/io-basis.md) -- [IO 设计模式总结](./java/io/io-design-patterns.md) -- [IO 模型详解](./java/io/io-model.md) -- [NIO 核心知识总结](./java/io/nio-basis.md) - -### 并发 - -**知识点/面试题总结** : (必看 :+1:) - -- [Java 并发常见知识点&面试题总结(上)](./java/concurrent/java-concurrent-questions-01.md) -- [Java 并发常见知识点&面试题总结(中)](./java/concurrent/java-concurrent-questions-02.md) -- [Java 并发常见知识点&面试题总结(下)](./java/concurrent/java-concurrent-questions-03.md) - -**重要知识点详解**: - -- [乐观锁和悲观锁详解](./java/concurrent/optimistic-lock-and-pessimistic-lock.md) -- [CAS 详解](./java/concurrent/cas.md) -- [JMM(Java 内存模型)详解](./java/concurrent/jmm.md) -- **线程池**:[Java 线程池详解](./java/concurrent/java-thread-pool-summary.md)、[Java 线程池最佳实践](./java/concurrent/java-thread-pool-best-practices.md) -- [ThreadLocal 详解](./java/concurrent/threadlocal.md) -- [Java 并发容器总结](./java/concurrent/java-concurrent-collections.md) -- [Atomic 原子类总结](./java/concurrent/atomic-classes.md) -- [AQS 详解](./java/concurrent/aqs.md) -- [CompletableFuture 详解](./java/concurrent/completablefuture-intro.md) - -### JVM (必看 :+1:) - -JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.com/javase/specs/jvms/se8/html/index.html) 和周志明老师的[《深入理解 Java 虚拟机(第 3 版)》](https://book.douban.com/subject/34907497/) (强烈建议阅读多遍!)。 - -- **[Java 内存区域](./java/jvm/memory-area.md)** -- **[JVM 垃圾回收](./java/jvm/jvm-garbage-collection.md)** -- [类文件结构](./java/jvm/class-file-structure.md) -- **[类加载过程](./java/jvm/class-loading-process.md)** -- [类加载器](./java/jvm/classloader.md) -- [【待完成】最重要的 JVM 参数总结(翻译完善了一半)](./java/jvm/jvm-parameters-intro.md) -- [【加餐】大白话带你认识 JVM](./java/jvm/jvm-intro.md) -- [JDK 监控和故障处理工具](./java/jvm/jdk-monitoring-and-troubleshooting-tools.md) - -### 新特性 - -- **Java 8**:[Java 8 新特性总结(翻译)](./java/new-features/java8-tutorial-translate.md)、[Java8 常用新特性总结](./java/new-features/java8-common-new-features.md) -- [Java 9 新特性概览](./java/new-features/java9.md) -- [Java 10 新特性概览](./java/new-features/java10.md) -- [Java 11 新特性概览](./java/new-features/java11.md) -- [Java 12 & 13 新特性概览](./java/new-features/java12-13.md) -- [Java 14 & 15 新特性概览](./java/new-features/java14-15.md) -- [Java 16 新特性概览](./java/new-features/java16.md) -- [Java 17 新特性概览](./java/new-features/java17.md) -- [Java 18 新特性概览](./java/new-features/java18.md) -- [Java 19 新特性概览](./java/new-features/java19.md) -- [Java 20 新特性概览](./java/new-features/java20.md) -- [Java 21 新特性概览](./java/new-features/java21.md) -- [Java 22 & 23 新特性概览](./java/new-features/java22-23.md) -- [Java 24 新特性概览](./java/new-features/java24.md) - -## 计算机基础 - -### 操作系统 - -- [操作系统常见知识点&面试题总结(上)](./cs-basics/operating-system/operating-system-basic-questions-01.md) -- [操作系统常见知识点&面试题总结(下)](./cs-basics/operating-system/operating-system-basic-questions-02.md) -- **Linux**: - - [后端程序员必备的 Linux 基础知识总结](./cs-basics/operating-system/linux-intro.md) - - [Shell 编程基础知识总结](./cs-basics/operating-system/shell-intro.md) - -### 网络 - -**知识点/面试题总结**: - -- [计算机网络常见知识点&面试题总结(上)](./cs-basics/network/other-network-questions.md) -- [计算机网络常见知识点&面试题总结(下)](./cs-basics/network/other-network-questions2.md) -- [谢希仁老师的《计算机网络》内容总结(补充)](./cs-basics/network/computer-network-xiexiren-summary.md) - -**重要知识点详解**: - -- [OSI 和 TCP/IP 网络分层模型详解(基础)](./cs-basics/network/osi-and-tcp-ip-model.md) -- [应用层常见协议总结(应用层)](./cs-basics/network/application-layer-protocol.md) -- [HTTP vs HTTPS(应用层)](./cs-basics/network/http-vs-https.md) -- [HTTP 1.0 vs HTTP 1.1(应用层)](./cs-basics/network/http1.0-vs-http1.1.md) -- [HTTP 常见状态码(应用层)](./cs-basics/network/http-status-codes.md) -- [DNS 域名系统详解(应用层)](./cs-basics/network/dns.md) -- [TCP 三次握手和四次挥手(传输层)](./cs-basics/network/tcp-connection-and-disconnection.md) -- [TCP 传输可靠性保障(传输层)](./cs-basics/network/tcp-reliability-guarantee.md) -- [ARP 协议详解(网络层)](./cs-basics/network/arp.md) -- [NAT 协议详解(网络层)](./cs-basics/network/nat.md) -- [网络攻击常见手段总结(安全)](./cs-basics/network/network-attack-means.md) - -### 数据结构 - -**图解数据结构:** - -- [线性数据结构 :数组、链表、栈、队列](./cs-basics/data-structure/linear-data-structure.md) -- [图](./cs-basics/data-structure/graph.md) -- [堆](./cs-basics/data-structure/heap.md) -- [树](./cs-basics/data-structure/tree.md):重点关注[红黑树](./cs-basics/data-structure/red-black-tree.md)、B-,B+,B\*树、LSM 树 - -其他常用数据结构: - -- [布隆过滤器](./cs-basics/data-structure/bloom-filter.md) - -### 算法 - -算法这部分内容非常重要,如果你不知道如何学习算法的话,可以看下我写的: - -- [算法学习书籍+资源推荐](https://www.zhihu.com/question/323359308/answer/1545320858) 。 -- [如何刷 Leetcode?](https://www.zhihu.com/question/31092580/answer/1534887374) - -**常见算法问题总结**: - -- [几道常见的字符串算法题总结](./cs-basics/algorithms/string-algorithm-problems.md) -- [几道常见的链表算法题总结](./cs-basics/algorithms/linkedlist-algorithm-problems.md) -- [剑指 offer 部分编程题](./cs-basics/algorithms/the-sword-refers-to-offer.md) -- [十大经典排序算法](./cs-basics/algorithms/10-classical-sorting-algorithms.md) - -另外,[GeeksforGeeks](https://www.geeksforgeeks.org/fundamentals-of-algorithms/) 这个网站总结了常见的算法 ,比较全面系统。 - -[![Banner](https://oss.javaguide.cn/xingqiu/xingqiu.png)](./about-the-author/zhishixingqiu-two-years.md) - -## 数据库 - -### 基础 - -- [数据库基础知识总结](./database/basis.md) -- [NoSQL 基础知识总结](./database/nosql.md) -- [字符集详解](./database/character-set.md) -- SQL : - - [SQL 语法基础知识总结](./database/sql/sql-syntax-summary.md) - - [SQL 常见面试题总结](./database/sql/sql-questions-01.md) - -### MySQL - -**知识点/面试题总结:** - -- **[MySQL 常见知识点&面试题总结](./database/mysql/mysql-questions-01.md)** (必看 :+1:) -- [MySQL 高性能优化规范建议总结](./database/mysql/mysql-high-performance-optimization-specification-recommendations.md) - -**重要知识点:** - -- [MySQL 索引详解](./database/mysql/mysql-index.md) -- [MySQL 事务隔离级别图文详解)](./database/mysql/transaction-isolation-level.md) -- [MySQL 三大日志(binlog、redo log 和 undo log)详解](./database/mysql/mysql-logs.md) -- [InnoDB 存储引擎对 MVCC 的实现](./database/mysql/innodb-implementation-of-mvcc.md) -- [SQL 语句在 MySQL 中的执行过程](./database/mysql/how-sql-executed-in-mysql.md) -- [MySQL 查询缓存详解](./database/mysql/mysql-query-cache.md) -- [MySQL 执行计划分析](./database/mysql/mysql-query-execution-plan.md) -- [MySQL 自增主键一定是连续的吗](./database/mysql/mysql-auto-increment-primary-key-continuous.md) -- [MySQL 时间类型数据存储建议](./database/mysql/some-thoughts-on-database-storage-time.md) -- [MySQL 隐式转换造成索引失效](./database/mysql/index-invalidation-caused-by-implicit-conversion.md) - -### Redis - -**知识点/面试题总结** : (必看:+1: ): - -- [Redis 常见知识点&面试题总结(上)](./database/redis/redis-questions-01.md) -- [Redis 常见知识点&面试题总结(下)](./database/redis/redis-questions-02.md) - -**重要知识点:** - -- [3 种常用的缓存读写策略详解](./database/redis/3-commonly-used-cache-read-and-write-strategies.md) -- [Redis 5 种基本数据结构详解](./database/redis/redis-data-structures-01.md) -- [Redis 3 种特殊数据结构详解](./database/redis/redis-data-structures-02.md) -- [Redis 持久化机制详解](./database/redis/redis-persistence.md) -- [Redis 内存碎片详解](./database/redis/redis-memory-fragmentation.md) -- [Redis 常见阻塞原因总结](./database/redis/redis-common-blocking-problems-summary.md) -- [Redis 集群详解](./database/redis/redis-cluster.md) - -### MongoDB - -- [MongoDB 常见知识点&面试题总结(上)](./database/mongodb/mongodb-questions-01.md) -- [MongoDB 常见知识点&面试题总结(下)](./database/mongodb/mongodb-questions-02.md) - -## 搜索引擎 - -[Elasticsearch 常见面试题总结(付费)](./database/elasticsearch/elasticsearch-questions-01.md) - -![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) - -## 开发工具 - -### Maven - -- [Maven 核心概念总结](./tools/maven/maven-core-concepts.md) -- [Maven 最佳实践](./tools/maven/maven-best-practices.md) - -### Gradle - -[Gradle 核心概念总结](./tools/gradle/gradle-core-concepts.md)(可选,目前国内还是使用 Maven 普遍一些) - -### Docker - -- [Docker 核心概念总结](./tools/docker/docker-intro.md) -- [Docker 实战](./tools/docker/docker-in-action.md) - -### Git - -- [Git 核心概念总结](./tools/git/git-intro.md) -- [GitHub 实用小技巧总结](./tools/git/github-tips.md) - -## 系统设计 - -- [系统设计常见面试题总结](./system-design/system-design-questions.md) -- [设计模式常见面试题总结](./system-design/design-pattern.md) - -### 基础 - -- [RestFul API 简明教程](./system-design/basis/RESTfulAPI.md) -- [软件工程简明教程简明教程](./system-design/basis/software-engineering.md) -- [代码命名指南](./system-design/basis/naming.md) -- [代码重构指南](./system-design/basis/refactoring.md) -- [单元测试指南](./system-design/basis/unit-test.md) - -### 常用框架 - -#### Spring/SpringBoot (必看 :+1:) - -**知识点/面试题总结** : - -- [Spring 常见知识点&面试题总结](./system-design/framework/spring/spring-knowledge-and-questions-summary.md) -- [SpringBoot 常见知识点&面试题总结](./system-design/framework/spring/springboot-knowledge-and-questions-summary.md) -- [Spring/Spring Boot 常用注解总结](./system-design/framework/spring/spring-common-annotations.md) -- [SpringBoot 入门指南](https://github.com/Snailclimb/springboot-guide) - -**重要知识点详解**: - -- [IoC & AOP 详解(快速搞懂)](./system-design/framework/spring/ioc-and-aop.md) -- [Spring 事务详解](./system-design/framework/spring/spring-transaction.md) -- [Spring 中的设计模式详解](./system-design/framework/spring/spring-design-patterns-summary.md) -- [SpringBoot 自动装配原理详解](./system-design/framework/spring/spring-boot-auto-assembly-principles.md) - -#### MyBatis - -[MyBatis 常见面试题总结](./system-design/framework/mybatis/mybatis-interview.md) - -### 安全 - -#### 认证授权 - -- [认证授权基础概念详解](./system-design/security/basis-of-authority-certification.md) -- [JWT 基础概念详解](./system-design/security/jwt-intro.md) -- [JWT 优缺点分析以及常见问题解决方案](./system-design/security/advantages-and-disadvantages-of-jwt.md) -- [SSO 单点登录详解](./system-design/security/sso-intro.md) -- [权限系统设计详解](./system-design/security/design-of-authority-system.md) -- [常见加密算法总结](./system-design/security/encryption-algorithms.md) - -#### 数据脱敏 - -数据脱敏说的就是我们根据特定的规则对敏感信息数据进行变形,比如我们把手机号、身份证号某些位数使用 \* 来代替。 - -#### 敏感词过滤 - -[敏感词过滤方案总结](./system-design/security/sentive-words-filter.md) - -### 定时任务 - -[Java 定时任务详解](./system-design/schedule-task.md) - -### Web 实时消息推送 - -[Web 实时消息推送详解](./system-design/web-real-time-message-push.md) - -## 分布式 - -### 理论&算法&协议 - -- [CAP 理论和 BASE 理论解读](./distributed-system/protocol/cap-and-base-theorem.md) -- [Paxos 算法解读](./distributed-system/protocol/paxos-algorithm.md) -- [Raft 算法解读](./distributed-system/protocol/raft-algorithm.md) -- [Gossip 协议详解](./distributed-system/protocol/gossip-protocl.md) - -### RPC - -- [RPC 基础知识总结](./distributed-system/rpc/rpc-intro.md) -- [Dubbo 常见知识点&面试题总结](./distributed-system/rpc/dubbo.md) - -### ZooKeeper - -> 这两篇文章可能有内容重合部分,推荐都看一遍。 - -- [ZooKeeper 相关概念总结(入门)](./distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md) -- [ZooKeeper 相关概念总结(进阶)](./distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md) - -### API 网关 - -- [API 网关基础知识总结](./distributed-system/api-gateway.md) -- [Spring Cloud Gateway 常见知识点&面试题总结](./distributed-system/spring-cloud-gateway-questions.md) - -### 分布式 ID - -- [分布式 ID 常见知识点&面试题总结](./distributed-system/distributed-id.md) -- [分布式 ID 设计指南](./distributed-system/distributed-id-design.md) - -### 分布式锁 - -- [分布式锁介绍](https://javaguide.cn/distributed-system/distributed-lock.html) -- [分布式锁常见实现方案总结](https://javaguide.cn/distributed-system/distributed-lock-implementations.html) - -### 分布式事务 - -[分布式事务常见知识点&面试题总结](./distributed-system/distributed-transaction.md) - -### 分布式配置中心 - -[分布式配置中心常见知识点&面试题总结](./distributed-system/distributed-configuration-center.md) - -## 高性能 - -### 数据库优化 - -- [数据库读写分离和分库分表](./high-performance/read-and-write-separation-and-library-subtable.md) -- [数据冷热分离](./high-performance/data-cold-hot-separation.md) -- [常见 SQL 优化手段总结](./high-performance/sql-optimization.md) -- [深度分页介绍及优化建议](./high-performance/deep-pagination-optimization.md) - -### 负载均衡 - -[负载均衡常见知识点&面试题总结](./high-performance/load-balancing.md) - -### CDN - -[CDN(内容分发网络)常见知识点&面试题总结](./high-performance/cdn.md) - -### 消息队列 - -- [消息队列基础知识总结](./high-performance/message-queue/message-queue.md) -- [Disruptor 常见知识点&面试题总结](./high-performance/message-queue/disruptor-questions.md) -- [RabbitMQ 常见知识点&面试题总结](./high-performance/message-queue/rabbitmq-questions.md) -- [RocketMQ 常见知识点&面试题总结](./high-performance/message-queue/rocketmq-questions.md) -- [Kafka 常常见知识点&面试题总结](./high-performance/message-queue/kafka-questions-01.md) - -## 高可用 - -[高可用系统设计指南](./high-availability/high-availability-system-design.md) - -### 冗余设计 - -[冗余设计详解](./high-availability/redundancy.md) - -### 限流 - -[服务限流详解](./high-availability/limit-request.md) - -### 降级&熔断 - -[降级&熔断详解](./high-availability/fallback-and-circuit-breaker.md) - -### 超时&重试 - -[超时&重试详解](./high-availability/timeout-and-retry.md) - -### 集群 - -相同的服务部署多份,避免单点故障。 - -### 灾备设计和异地多活 - -**灾备** = 容灾 + 备份。 - -- **备份**:将系统所产生的的所有重要数据多备份几份。 -- **容灾**:在异地建立两个完全相同的系统。当某个地方的系统突然挂掉,整个应用系统可以切换到另一个,这样系统就可以正常提供服务了。 - -**异地多活** 描述的是将服务部署在异地并且服务同时对外提供服务。和传统的灾备设计的最主要区别在于“多活”,即所有站点都是同时在对外提供服务的。异地多活是为了应对突发状况比如火灾、地震等自然或者人为灾害。 - -## Star 趋势 - -![Stars](https://api.star-history.com/svg?repos=Snailclimb/JavaGuide&type=Date) - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号“**JavaGuide**”。 - -![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) +- [Summary of Basic IO Knowledge](./java/io/io-basis.md) +- [Summary of IO Design Patterns](./java/io/io-design-patterns.md) +- \[Detailed Explanation of IO Models\](./java diff --git a/docs/interview-preparation/how-to-handle-interview-nerves.md b/docs/interview-preparation/how-to-handle-interview-nerves.md index 1a1a79409f9..3fd9d779500 100644 --- a/docs/interview-preparation/how-to-handle-interview-nerves.md +++ b/docs/interview-preparation/how-to-handle-interview-nerves.md @@ -1,67 +1,67 @@ --- -title: 面试太紧张怎么办? -category: 面试准备 +title: What to Do If You're Too Nervous for an Interview? +category: Interview Preparation icon: security-fill --- -很多小伙伴在第一次技术面试时都会感到紧张甚至害怕,面试结束后还会有种“懵懵的”感觉。我也经历过类似的状况,可以说是深有体会。其实,**紧张是很正常的**——它代表你对面试的重视,也来自于对未知结果的担忧。但如果过度紧张,反而会影响你的临场发挥。 +Many friends feel nervous or even scared during their first technical interview, and afterward, they often feel a bit "dazed." I have experienced similar situations, and I can relate. In fact, **feeling nervous is very normal**—it shows that you care about the interview and stems from worries about uncertain outcomes. However, if you become overly nervous, it can negatively impact your performance on the spot. -下面,我就分享一些自己的心得,帮大家更好地应对面试中的紧张情绪。 +Here, I will share some of my insights to help everyone better cope with nervous emotions during interviews. -## 试着接受紧张情绪,调整心态 +## Try to Accept Nervous Feelings and Adjust Your Mindset -首先要明白,紧张是正常情绪,特别是初次或前几次面试时,多少都会有点忐忑。不要过分排斥这种情绪,可以适当地“拥抱”它: +First, it’s important to understand that feeling nervous is a normal emotion, especially during your first or few interviews. It’s natural to feel a bit anxious. Don’t overly reject this emotion; you can appropriately "embrace" it: -- **搞清楚面试的本质**:面试本质上是一场与面试官的深入交流,是一个双向选择的过程。面试失败并不意味着你的价值和努力被否定,而可能只是因为你与目标岗位暂时不匹配,或者仅仅是一次 KPI 面试,这家公司可能压根就没有真正的招聘需求。失败的原因也可能是某些知识点、项目经验或表达方式未能充分展现出你的能力。即便这次面试未通过,也不妨碍你继续尝试其他公司,完全不慌! -- **不要害怕面试官**:很多求职者平时和同学朋友交流沟通的蛮好,一到面试就害怕了。面试官和求职者双方是平等的,以后说不定就是同事关系。也不要觉得面试官就很厉害,实际上,面试官的水平也参差不齐。他们提出的问题,可能自己也没有完全理解。 -- **给自己积极的心理暗示**:告诉自己“有点紧张没关系,这只能让我更专注,心跳加快是我在给自己打气,我一定可以回答的很好!”。 +- **Understand the essence of the interview**: Essentially, an interview is an in-depth conversation with the interviewer and a two-way selection process. Failing an interview does not mean your value and efforts are denied; it may just be that you are not temporarily aligned with the desired position, or it could simply be a KPI interview where the company might not have an actual recruitment need. The reason for failure might also be that certain knowledge points, project experiences, or expressive methods did not adequately showcase your abilities. Even if this interview doesn’t go well, it doesn’t prevent you from applying to other companies—don’t panic at all! +- **Don’t be afraid of the interviewer**: Many job seekers communicate well with classmates and friends, but they feel apprehensive during the interview. The relationship between the interviewer and the job seeker is equal, and they may even become colleagues in the future. Also, don’t think that the interviewer is exceptionally skilled; in reality, their levels can vary widely. They may not fully comprehend the questions they are asking. +- **Give yourself positive psychological hints**: Tell yourself, "It's okay to be a bit nervous; this will only help me focus more. My quickened heartbeat is just my way of motivating myself, and I am definitely going to answer well!". -## 提前准备,减少不确定性 +## Prepare in Advance to Reduce Uncertainty -**不确定性越多,越容易紧张。** 如果你能够在面试前做充分的准备,很多“未知”就会消失,紧张情绪自然会减轻很多。 +**The more uncertainty there is, the easier it is to feel nervous.** If you can prepare thoroughly before the interview, many "unknowns" will disappear, and your nervousness will naturally diminish. -### 认真准备技术面试 +### Prepare for the Technical Interview -- **优先梳理核心知识点**:比如计算基础、数据库、Java 基础、Java 集合、并发编程、SpringBoot(这里以 Java 后端方向为例)等。如果时间不够,可以分轻重缓急,有重点地复习。强烈推荐阅读一下 [Java 面试重点总结(重要)](https://javaguide.cn/interview-preparation/key-points-of-interview.html)这篇文章。 -- **精心准备项目经历**:认真思考你简历上最重要的项目(面试以前两个项目为主,尤其是第一个),它们的技术难点、业务逻辑、架构设计,以及可能被面试官深挖的点。把你的思考总结成可能出现的面试问题,并尝试回答。 +- **Prioritize core knowledge points**: For example, fundamentals of computing, databases, Java basics, Java collections, concurrent programming, SpringBoot (using Java backend as an example). If time is limited, prioritize your review based on importance. I highly recommend reading the article [Key Points of Java Interviews (Important)](https://javaguide.cn/interview-preparation/key-points-of-interview.html). +- **Carefully prepare your project experience**: Think deeply about the most important projects on your resume (focus on the two most recent projects before the interview, especially the first one), including their technical challenges, business logic, architecture designs, and potential points the interviewer may delve into. Summarize your thinking into possible interview questions and try to answer them. -### 模拟面试和自测 +### Simulate Interviews and Self-Test -- **约朋友或同学互相提问**:以真实的面试场景来进行演练,并及时对回答进行诊断和反馈。 -- **线上练习**:很多平台都提供 AI 模拟面试,能比较真实地模拟面试官提问情境。 -- **面经**:平时可以多看一些前辈整理的面经,尤其是目标岗位或目标公司的面经,总结高频考点和常见问题。 -- **技术面试题自测**:在 [《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的 「技术面试题自测篇」 ,我总结了 Java 面试中最重要的知识点的最常见的面试题并按照面试提问的方式展现出来。其中,每一个问题都有提示和重要程度说明,非常适合用来自测。 +- **Ask friends or classmates to interview each other**: Perform exercises in a real interview setting and diagnose and provide feedback on your answers promptly. +- **Online practice**: Many platforms offer AI simulation interviews that can realistically simulate the questioning scenario of an interviewer. +- **Interview experiences**: Frequently read interview experiences compiled by predecessors, especially for your target position or company, summarizing high-frequency examination points and common questions. +- **Self-test on technical interview questions**: In the [“Java Interview Guide”](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) under the "Technical Interview Questions Self-Test" section, I summarized the most commonly asked interview questions related to the most important knowledge points in Java interviews and presented them in interview questioning format. Each question includes hints and importance levels, making it very suitable for self-testing. -[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的 「技术面试题自测篇」概览: +Overview of the "Technical Interview Questions Self-Test" in the [“Java Interview Guide”](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html): -![技术面试题自测篇](https://oss.javaguide.cn/javamianshizhibei/technical-interview-questions-self-test.png) +![Technical Interview Questions Self-Test](https://oss.javaguide.cn/javamianshizhibei/technical-interview-questions-self-test.png) -### 多表达 +### Express Yourself More -平时要多说,多表达出来,不要只是在心里面想,不然真正面试的时候会发现想的和说的不太一样。 +Make it a habit to talk more and express your thoughts, rather than just thinking about them. Otherwise, you may find that what you think and what you say are different during the actual interview. -我前面推荐的模拟面试和自测,有一部分原因就是为了能够多多表达。 +The simulations and self-tests I recommended earlier are partly aimed at helping you express yourself more. -### 多面试 +### Attend More Interviews -- **先小厂后大厂**:可以先去一些规模较小或者对你来说压力没那么大的公司试试手,积累一些实战经验,增加一些信心;等熟悉了面试流程、能够更从容地回答问题后,再去挑战自己心仪的大厂或热门公司。 -- **积累“失败经验”**:不要怕被拒,有些时候被拒绝却能从中学到更多。多复盘,多思考到底是哪个环节出了问题,再用更好的状态迎接下一次面试。 +- **Start with smaller companies and then move to larger ones**: You can first try companies that are smaller or less stressful for you to gain practical experience and build confidence. After becoming familiar with the interview process and feeling more at ease answering questions, you can challenge yourself with your desired larger companies or popular firms. +- **Gather "failure experiences"**: Don’t fear rejection; sometimes, being turned down can teach you a lot more. Reflect and analyze what went wrong in which stage, and then approach the next interview with an improved mindset. -### 保证休息 +### Ensure Rest -- **留出充裕时间**:面试前尽量不要排太多事情,保证自己能有个好状态去参加面试。 -- **保证休息**:充足睡眠有助于情绪稳定,也能让你在面试时更清晰地思考问题。 +- **Allow enough time**: Try not to schedule too many things before the interview to ensure you are in a good state for the interview. +- **Ensure adequate rest**: Sufficient sleep helps stabilize emotions and allows you to think more clearly during the interview. -## 遇到不会的问题不要慌 +## Don’t Panic When Encountering Unknown Questions -一场面试,不太可能面试官提的每一个问题你都能轻松应对,除非这场面试非常简单。 +In an interview, it’s unlikely that you can handle every question posed by the interviewer with ease, unless the interview is very simple. -在面试过程中,遇到不会的问题,首先要做的是快速回顾自己过往的知识,看是否能找到突破口。如果实在没有思路的话,可以真诚地向面试要一些提示比如谈谈你对这个问题的理解以及困惑点。一定不要觉得向面试官要提示很可耻,只要沟通没问题,这其实是很正常的。最怕的就是自己不会,还乱回答一通,这样会让面试官觉得你技术态度有问题。 +During the interview process, if you encounter a question you don't know, the first thing to do is quickly review your past knowledge to see if you can find a breakthrough. If you truly have no idea, don’t hesitate to ask the interviewer for some hints, such as discussing your understanding of the question and what confuses you. Do not feel embarrassed to ask for hints; as long as communication is smooth, it’s quite normal. The worst thing is to not know the answer and respond randomly, which can make the interviewer feel you have a problem with your technical attitude. -## 面试结束后的复盘 +## Review After the Interview -很多人关注面试前的准备,却忽略了面试后的复盘,这一步真的非常非常非常重要: +Many people focus on preparation before the interview but overlook the importance of reviewing afterward; this step is truly very, very, very important: -1. **记录面试中的问题**:无论回答得好坏,都把它们写下来。如果问到了一些没想过的问题,可以认真思考并在面试后补上答案。 -2. **反思自己的表现**:有没有遇到卡壳的地方?是知识没准备到还是过于紧张导致表达混乱?下次如何改进? -3. **持续完善自己的“面试题库”**:把新的问题补充进去,不断拓展自己的知识面,也逐步降低对未知问题的恐惧感。 +1. **Record the questions during the interview**: Regardless of how well you answered, write them down. If you were asked questions you hadn’t considered, think carefully about them and supplement your answers after the interview. +1. **Reflect on your performance**: Did you encounter any blocks? Was it due to insufficient knowledge preparation or was it too much nervousness that led to confused expression? How can you improve next time? +1. **Continuously improve your "interview question bank"**: Add new questions, progressively expand your knowledge base, and gradually reduce your fear of unknown questions. diff --git a/docs/interview-preparation/internship-experience.md b/docs/interview-preparation/internship-experience.md index 4e16fc7e0b5..a1455aef24a 100644 --- a/docs/interview-preparation/internship-experience.md +++ b/docs/interview-preparation/internship-experience.md @@ -1,56 +1,52 @@ --- -title: 校招没有实习经历怎么办? -category: 面试准备 +title: What to Do If You Don't Have Internship Experience for Campus Recruitment? +category: Interview Preparation icon: experience --- -由于目前的面试太卷,对于犹豫是否要找实习的同学来说,个人建议不论是本科生还是研究生都应该在参加校招面试之前,争取一下不错的实习机会,尤其是大厂的实习机会,日常实习或者暑期实习都可以。当然,如果大厂实习面不上,中小厂实习也是可以接受的。 +Due to the highly competitive nature of current interviews, I personally suggest that whether you are an undergraduate or a graduate student, you should strive for good internship opportunities before participating in campus recruitment interviews, especially internships at large companies. Both regular internships and summer internships are acceptable. Of course, if you can't secure an internship at a large company, internships at small and medium-sized companies are also acceptable. -不过,现在的实习是真难找,今年有非常多的同学没有找到实习,有一部分甚至是 211/985 名校的同学。 +However, finding internships is quite challenging now. Many students this year, including some from 211/985 universities, have not found internships. -如果实在是找不到合适的实习的话,那也没办法,我们应该多花时间去把下面这三件事情给做好: +If you really can't find a suitable internship, then there's nothing we can do. We should spend more time focusing on the following three things: -1. 补强项目经历 -2. 持续完善简历 -3. 准备技术面试 +1. Strengthen project experience +1. Continuously improve your resume +1. Prepare for technical interviews -## 补强项目经历 +## Strengthen Project Experience -校招没有实习经历的话,找工作比较吃亏(没办法,太卷了),需要在项目经历部分多发力弥补一下。 +If you don't have internship experience for campus recruitment, it can be a disadvantage (it's just too competitive), so you need to put more effort into the project experience section to make up for it. -建议你尽全力地去补强自己的项目经历,完善现有的项目或者去做更有亮点的项目,尽可能地通过项目经历去弥补一些。 +I recommend that you do your best to strengthen your project experience, improve existing projects, or work on more impressive projects to compensate as much as possible. -你面试中的重点就是你的项目经历涉及到的知识点,如果你的项目经历比较简单的话,面试官直接不知道问啥了。另外,你的项目经历中不涉及的知识点,但在技能介绍中提到的知识点也很大概率会被问到。像 Redis 这种基本是面试 Java 后端岗位必备的技能,我觉得大部分面试官应该都会问。 +The focus of your interview will be on the knowledge points related to your project experience. If your project experience is relatively simple, the interviewer may not know what to ask. Additionally, knowledge points not covered in your project experience but mentioned in your skills introduction are also likely to be questioned. For example, skills like Redis are essential for Java backend positions, and I believe most interviewers will ask about them. -推荐阅读一下网站的这篇文章:[项目经验指南](https://javaguide.cn/interview-preparation/project-experience-guide.html)。 +I recommend reading this article on the website: [Project Experience Guide](https://javaguide.cn/interview-preparation/project-experience-guide.html). -## **完善简历** +## **Improve Your Resume** -一定一定一定要重视简历啊!建议至少花 2~3 天时间来专门完善自己的简历。并且,后续还要持续完善。 +You must, must, must pay attention to your resume! I suggest spending at least 2-3 days specifically to improve your resume. Moreover, you should continue to refine it afterward. -对于面试官来说,筛选简历的时候会比较看重下面这些维度: +When screening resumes, interviewers tend to focus on the following dimensions: -1. **实习/工作经历**:看你是否有不错的实习经历,大厂且与面试岗位相关的实习/工作经历最佳。 -2. **获奖经历**:如果有含金量比较高(知名度较高的赛事比如 ACM、阿里云天池)的获奖经历的话,也是加分点,尤其是对于校招来说,这类求职者属于是很多大厂争抢的对象(但不是说获奖了就能进大厂,还是要面试表现还可以)。对于社招来说,获奖经历作用相对较小,通常会更看重过往的工作经历和项目经验。 -3. **项目经验**:项目经验对于面试来说非常重要,面试官会重点关注,同时也是有水平的面试提问的重点。 -4. **技能匹配度**:看你的技能是否满足岗位的需求。在投递简历之前,一定要确认一下自己的技能介绍中是否缺少一些你要投递的对应岗位的技能要求。 -5. **学历**:相对其他行业来说,程序员求职面试对于学历的包容度还是比较高的,只要你在其他方面有过人之出的话,也是可以弥补一下学历的缺陷的。你要知道,很多行业比如律师、金融,学历就是敲门砖,学历没达到要求,直接面试机会都没有。不过,由于现在面试越来越卷,一些大厂、国企和研究所也开始卡学历了,很多岗位都要求 211/985,甚至必须需要硕士学历。总之,学历很难改变,学校较差的话,就投递那些对学历没有明确要求的公司即可,努力提升自己的其他方面的硬实力。 +1. **Internship/Work Experience**: They look for good internship experiences, preferably at large companies and relevant to the position you are applying for. +1. **Awards**: If you have high-value awards (from well-known competitions like ACM or Alibaba Cloud Tianchi), it can be a plus, especially for campus recruitment, as such candidates are often sought after by large companies (but winning an award doesn't guarantee entry into a large company; interview performance still matters). For social recruitment, the impact of awards is relatively small, and past work experience and project experience are usually more important. +1. **Project Experience**: Project experience is very important for interviews, and interviewers will pay close attention to it. It is also a key area for advanced interview questions. +1. **Skill Match**: They will check if your skills meet the job requirements. Before submitting your resume, make sure that your skills introduction does not lack any skills required for the position you are applying for. +1. **Education**: Compared to other industries, the tolerance for educational background in programming job interviews is relatively high. As long as you excel in other areas, you can compensate for any shortcomings in your education. You should know that in many industries, such as law and finance, education is a key factor; if you don't meet the educational requirements, you won't even get an interview opportunity. However, as interviews become more competitive, some large companies, state-owned enterprises, and research institutes have started to set educational requirements, with many positions requiring 211/985 degrees or even a master's degree. In summary, education is hard to change; if you come from a less prestigious school, apply to companies that do not have strict educational requirements and work on improving your other hard skills. -对于大部分求职者来说,实习/工作经历、项目经验、技能匹配度更重要一些。不过,不排除一些公司会因为学历卡人。 +For most job seekers, internship/work experience, project experience, and skill match are more important. However, some companies may still filter candidates based on education. -详细的程序员简历编写指南可以参考这篇文章:[程序员简历编写指南(重要)](https://javaguide.cn/interview-preparation/resume-guide.html)。 +For a detailed guide on writing a programmer's resume, you can refer to this article: [Programmer Resume Writing Guide (Important)](https://javaguide.cn/interview-preparation/resume-guide.html). -## **准备技术面试** +## **Prepare for Technical Interviews** -面试之前一定要提前准备一下常见的面试题也就是八股文: +Before the interview, make sure to prepare for common interview questions, also known as the "eight-legged essay": -- 自己面试中可能涉及哪些知识点、那些知识点是重点。 -- 面试中哪些问题会被经常问到、面试中自己该如何回答。(强烈不推荐死记硬背,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!) +- What knowledge points might be involved in your interview, and which ones are key? +- What questions are frequently asked in interviews, and how should you answer them? (I strongly do not recommend rote memorization. First: How much can you remember through this method? How long can you remember it? Second: It's hard to maintain learning through rote memorization!) -Java 后端面试复习的重点请看这篇文章:[Java 后端的面试重点是什么?](https://javaguide.cn/interview-preparation/key-points-of-interview.html)。 +For key points in Java backend interview preparation, please refer to this article: [What Are the Key Points for Java Backend Interviews?](https://javaguide.cn/interview-preparation/key-points-of-interview.html). -不同类型的公司对于技能的要求侧重点是不同的比如腾讯、字节可能更重视计算机基础比如网络、操作系统这方面的内容。阿里、美团这种可能更重视你的项目经历、实战能力。 - -一定不要抱着一种思想,觉得八股文或者基础问题的考查意义不大。如果你抱着这种思想复习的话,那效果可能不会太好。实际上,个人认为还是很有意义的,八股文或者基础性的知识在日常开发中也会需要经常用到。例如,线程池这块的拒绝策略、核心参数配置什么的,如果你不了解,实际项目中使用线程池可能就用的不是很明白,容易出现问题。而且,其实这种基础性的问题是最容易准备的,像各种底层原理、系统设计、场景题以及深挖你的项目这类才是最难的! - -八股文资料首推我的 [《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 和 [JavaGuide](https://javaguide.cn/home.html) 。里面不仅仅是原创八股文,还有很多对实际开发有帮助的干货。除了我的资料之外,你还可以去网上找一些其他的优质的文章、视频来看。 +Different types of companies have different focuses on skill requirements. For example, Tencent and ByteDance may place more emphasis diff --git a/docs/interview-preparation/interview-experience.md b/docs/interview-preparation/interview-experience.md index 2b58e95df10..42fbab289e5 100644 --- a/docs/interview-preparation/interview-experience.md +++ b/docs/interview-preparation/interview-experience.md @@ -1,18 +1,18 @@ --- -title: 优质面经汇总(付费) -category: 知识星球 +title: Summary of Quality Interview Experiences (Paid) +category: Knowledge Planet icon: experience --- -古人云:“**他山之石,可以攻玉**” 。善于学习借鉴别人的面试的成功经验或者失败的教训,可以让自己少走许多弯路。 +As the ancients said: "**The stone from another mountain can be used to polish jade**." Being good at learning from others' successful interview experiences or lessons from failures can help you avoid many detours. -在 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)** 的 **「面经篇」** ,我分享了 15+ 篇高质量的 Java 后端面经,有校招的,也有社招的,有大厂的,也有中小厂的。 +In **[“Java Interview Guide”](../zhuanlan/java-mian-shi-zhi-bei.md)** under the **“Interview Experience”** section, I share over 15 high-quality Java backend interview experiences, including those from campus recruitment, social recruitment, large companies, and small to medium-sized enterprises. -如果你是非科班的同学,也能在这些文章中找到对应的非科班的同学写的面经。 +If you are a non-computer science major, you can also find interview experiences written by fellow non-CS students in these articles. ![](https://oss.javaguide.cn/githubjuejinjihua/thinkimage-20220612185810480.png) -并且,[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)还有专门分享面经和面试题的专题,里面会分享很多优质的面经和面试题。 +Additionally, [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) has a dedicated section for sharing interview experiences and questions, where many quality interview experiences and questions will be shared. ![](https://oss.javaguide.cn/xingqiu/image-20220304120018731.png) @@ -20,11 +20,11 @@ icon: experience ![](https://oss.javaguide.cn/xingqiu/image-20220628101805897.png) -相比于牛客网或者其他网站的面经,《Java 面试指北》中整理的面经质量更高,并且,我会提供优质的参考资料。 +Compared to the interview experiences on Niuke.com or other websites, the interview experiences compiled in **“Java Interview Guide”** are of higher quality, and I will provide quality reference materials. -有很多同学要说了:“为什么不直接给出具体答案呢?”。主要原因有如下两点: +Many students might ask: “Why not just provide the specific answers directly?” The main reasons are as follows: -1. 参考资料解释的要更详细一些,还可以顺便让你把相关的知识点复习一下。 -2. 给出的参考资料基本都是我的原创,假如后续我想对面试问题的答案进行完善,就不需要挨个把之前的面经写的答案给修改了(面试中的很多问题都是比较类似的)。当然了,我的原创文章也不太可能覆盖到面试的每个点,部分面试问题的答案,我是精选的其他技术博主写的优质文章,文章质量都很高。 +1. The reference materials provide more detailed explanations and also allow you to review related knowledge points. +1. The reference materials I provide are mostly my original work. If I want to improve the answers to interview questions later, I won’t need to modify each answer from previous interview experiences (as many interview questions are quite similar). Of course, my original articles may not cover every point in interviews, and for some interview questions, I have selected high-quality articles written by other tech bloggers, all of which are of high quality. diff --git a/docs/interview-preparation/java-roadmap.md b/docs/interview-preparation/java-roadmap.md index 60dd0fa7fc0..0b62953e0d5 100644 --- a/docs/interview-preparation/java-roadmap.md +++ b/docs/interview-preparation/java-roadmap.md @@ -1,29 +1,29 @@ --- -title: Java 学习路线(最新版,4w+字) -category: 面试准备 +title: Java Learning Path (Latest Version, 40k+ Words) +category: Interview Preparation icon: path --- -::: tip 重要说明 +::: tip Important Note -本学习路线保持**年度系统性修订**,严格同步 Java 技术生态与招聘市场的最新动态,**确保内容时效性与前瞻性**。 +This learning path undergoes **annual systematic revisions**, rigorously synchronizing with the latest developments in the Java technology ecosystem and the job market, **ensuring content timeliness and foresight**. ::: -历时一个月精心打磨,笔者基于当下 Java 后端开发岗位招聘的最新要求,对既有学习路线进行了全面升级。本次升级涵盖技术栈增删、学习路径优化、配套学习资源更新等维度,力争构建出更符合 Java 开发者成长曲线的知识体系。 +After a month of meticulous refinement, the author has comprehensively upgraded the existing learning path based on the latest requirements for Java back-end developer positions. This upgrade includes adjustments to the tech stack, optimization of the learning path, and updates to accompanying learning resources, aiming to create a knowledge system that better aligns with the growth trajectory of Java developers. -![Java 学习路线 PDF 概览](https://oss.javaguide.cn/github/javaguide/interview-preparation/java-road-map-pdf.png) +![Java Learning Path PDF Overview](https://oss.javaguide.cn/github/javaguide/interview-preparation/java-road-map-pdf.png) -这可能是你见过的最用心、最全面的 Java 后端学习路线。这份学习路线共包含 **4w+** 字,但你完全不用担心内容过多而学不完。我会根据学习难度,划分出适合找小厂工作必学的内容,以及适合逐步提升 Java 后端开发能力的学习路径。 +This might be the most thoughtful and comprehensive Java back-end learning path you have ever seen. This learning path contains a total of **40k+** words, but you don't have to worry about the overwhelming amount of content. I will categorize it based on learning difficulty, highlighting essential content for securing small company positions as well as learning paths for gradually improving Java back-end development skills. -![Java 学习路线图](https://oss.javaguide.cn/github/javaguide/interview-preparation/java-road-map.png) +![Java Learning Path Diagram](https://oss.javaguide.cn/github/javaguide/interview-preparation/java-road-map.png) -对于初学者,你可以按照这篇文章推荐的学习路线和资料进行系统性的学习;对于有经验的开发者,你可以根据这篇文章更一步地深入学习 Java 后端开发,提升个人竞争力。 +For beginners, you can follow the recommended learning path and resources in this article for systematic learning; for experienced developers, you can take a deeper dive into Java back-end development based on this article to enhance your personal competitiveness. -在看这份学习路线的过程中,建议搭配 [Java 面试重点总结(重要)](https://javaguide.cn/interview-preparation/key-points-of-interview.html),可以让你在学习过程中更有目的性。 +While reviewing this learning path, it is advisable to pair it with [Java Interview Key Points Summary (Important)](https://javaguide.cn/interview-preparation/key-points-of-interview.html) to make your learning process more purposeful. -由于这份学习路线内容太多,因此我将其整理成了 PDF 版本(共 **61** 页),方便大家阅读。这份 PDF 有黑夜和白天两种阅读版本,满足大家的不同需求。 +Due to the extensive content of this learning path, I have organized it into a PDF version (a total of **61** pages) for your convenience. This PDF is available in both dark and light reading versions to meet different needs. -这份学习路线的获取方法很简单:直接在公众号「**JavaGuide**」后台回复“**学习路线**”即可获取。 +Getting this learning path is simple: just reply "**Learning Path**" in the background of the WeChat public account "**JavaGuide**" to receive it. -![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) +![JavaGuide Official WeChat Public Account](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) diff --git a/docs/interview-preparation/key-points-of-interview.md b/docs/interview-preparation/key-points-of-interview.md index c2101dc307a..4678550004f 100644 --- a/docs/interview-preparation/key-points-of-interview.md +++ b/docs/interview-preparation/key-points-of-interview.md @@ -1,43 +1,43 @@ --- -title: Java后端面试重点总结 -category: 面试准备 +title: Key Summary for Java Backend Interviews +category: Interview Preparation icon: star --- -::: tip 友情提示 -本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 +::: tip Friendly Reminder +This article is excerpted from **[“Java Interview Guide”](../zhuanlan/java-mian-shi-zhi-bei.md)**. It is a column that teaches you how to prepare for interviews more efficiently, complementing the content in JavaGuide by covering common topics (system design, common frameworks, distributed systems, high concurrency, etc.) and high-quality interview experiences. ::: -## Java 后端面试哪些知识点是重点? +## What Key Knowledge Points Should Be Focused on for Java Backend Interviews? -**准备面试的时候,具体哪些知识点是重点呢?如何把握重点?** +**When preparing for interviews, which specific knowledge points should be the focus? How can you grasp these key points?** -给你几点靠谱的建议: +Here are some reliable suggestions: -1. Java 基础、集合、并发、MySQL、Redis 、Spring、Spring Boot 这些 Java 后端开发必备的知识点(MySQL + Redis >= Java > Spring + Spring Boot)。大厂以及中小厂的面试问的比较多的就是这些知识点。Spring 和 Spring Boot 这俩框架类的知识点相对前面的知识点来说重要性要稍低一些,但一般面试也会问一些,尤其是中小厂。并发知识一般中大厂提问更多也更难,尤其是大厂喜欢深挖底层,很容易把人问倒。计算机基础相关的内容会在下面提到。 -2. 你的项目经历涉及到的知识点是重中之重,有水平的面试官都是会根据你的项目经历来问的。举个例子,你的项目经历使用了 Redis 来做限流,那 Redis 相关的八股文(比如 Redis 常见数据结构)以及限流相关的八股文(比如常见的限流算法)你就应该多花更多心思来搞懂吃透!你把项目经历上的知识点吃透之后,再把你简历上哪些写熟练掌握的技术给吃透,最后再去花时间准备其他知识点。 -3. 针对自身找工作的需求,你又可以适当地调整复习的重点。像中小厂一般问计算机基础比较少一些,有些大厂比如字节比较重视计算机基础尤其是算法。这样的话,如果你的目标是中小厂的话,计算机基础就准备面试来说不是那么重要了。如果复习时间不够的话,可以暂时先放放,腾出时间给其他重要的知识点。 -4. 一般校招的面试不会强制要求你会分布式/微服务、高并发的知识(不排除个别岗位有这方面的硬性要求),所以到底要不要掌握还是要看你个人当前的实际情况。如果你会这方面的知识的话,对面试相对来说还是会更有利一些(想要让项目经历有亮点,还是得会一些性能优化的知识。性能优化的知识这也算是高并发知识的一个小分支了)。如果你的技能介绍或者项目经历涉及到分布式/微服务、高并发的知识,那建议你尽量也要抽时间去认真准备一下,面试中很可能会被问到,尤其是项目经历用到的时候。不过,也还是主要准备写在简历上的那些知识点就好。 -5. JVM 相关的知识点,一般是大厂(例如美团、阿里)和一些不错的中厂(例如携程、顺丰、招银网络)才会问到,面试国企、差一点的中厂和小厂就没必要准备了。JVM 面试中比较常问的是 [Java 内存区域](https://javaguide.cn/java/jvm/memory-area.html)、[JVM 垃圾回收](https://javaguide.cn/java/jvm/jvm-garbage-collection.html)、[类加载器和双亲委派模型](https://javaguide.cn/java/jvm/classloader.html) 以及 JVM 调优和问题排查(我之前分享过一些[常见的线上问题案例](https://t.zsxq.com/0bsAac47U),里面就有 JVM 相关的)。 -6. 不同的大厂面试侧重点也会不同。比如说你要去阿里这种公司的话,项目和八股文就是重点,阿里笔试一般会有代码题,进入面试后就很少问代码题了,但是对原理性的问题问的比较深,经常会问一些你对技术的思考。再比如说你要面试字节这种公司,那计算机基础,尤其是算法是重点,字节的面试十分注重代码功底,有时候开始面试就会直接甩给你一道代码题,写出来再谈别的。也会问面试八股文,以及项目,不过,相对来说要少很多。建议你看一下这篇文章 [为了解开互联网大厂秋招内幕,我把他们全面了一遍](https://mp.weixin.qq.com/s/pBsGQNxvRupZeWt4qZReIA),了解一下常见大厂的面试题侧重点。 -7. 多去找一些面经看看,尤其你目标公司或者类似公司对应岗位的面经。这样可以实现针对性的复习,还能顺便自测一波,检查一下自己的掌握情况。 +1. The essential knowledge points for Java backend development include Java basics, collections, concurrency, MySQL, Redis, Spring, and Spring Boot (MySQL + Redis >= Java > Spring + Spring Boot). These are the areas that are frequently asked about in interviews at both large and small companies. The knowledge points for Spring and Spring Boot are relatively less important than the aforementioned points, but they are still commonly asked, especially at smaller firms. Questions about concurrency are generally more challenging and frequent at medium to large companies, particularly as larger firms like to delve into the underlying principles, which can easily stump candidates. Topics related to computer fundamentals will be mentioned below. +1. Knowledge points related to your project experience are extremely important; competent interviewers will base their questions on your project background. For example, if your project experience includes using Redis for rate limiting, you should pay extra attention to Redis-related knowledge (such as common data structures in Redis) and rate-limiting-related topics (like common algorithms for rate limiting)! After mastering the knowledge points from your project experience, focus on thoroughly understanding the technologies you’ve listed as proficient on your resume, and then spend time preparing other knowledge points. +1. You can adjust the focus of your review according to your job-seeking needs. Generally, small to medium firms ask fewer questions about computer fundamentals, while some large companies, like ByteDance, place a heavier emphasis on computer fundamentals, especially algorithms. Therefore, if you are targeting small to medium firms, preparing for computer fundamentals might not be as crucial for the interview. If your review time is limited, you can temporarily set those aside and allocate time to other important knowledge points. +1. Typically, companies hiring fresh graduates do not strictly require knowledge of distributed systems/microservices or high concurrency (with some specific positions possibly having this hard requirement). Whether or not to master these topics depends on your current situation. If you are knowledgeable in these areas, it can certainly benefit your interview (to make your project experience stand out, you need some performance optimization knowledge, which can be considered a small branch of high concurrency expertise). If your skills or project experience involve distributed systems/microservices or high concurrency, it's advisable to take time to prepare this knowledge seriously, as you are likely to be questioned about it during the interview, particularly when discussing your project experience. However, focus mainly on the knowledge points listed on your resume. +1. Knowledge related to the JVM is typically asked about only by large companies (like Meituan, Alibaba) and some decent medium-sized firms (like Ctrip, SF Express, and China Merchants Bank's Network). If you're interviewing at state-owned enterprises, less competitive medium-sized firms, or small companies, there’s generally no need to prepare for this. Commonly asked topics related to the JVM include [Java Memory Areas](https://javaguide.cn/java/jvm/memory-area.html), [JVM Garbage Collection](https://javaguide.cn/java/jvm/jvm-garbage-collection.html), [Class Loaders and the Parent Delegation Model](https://javaguide.cn/java/jvm/classloader.html), and JVM tuning and troubleshooting (I've previously shared some [common online issue cases](https://t.zsxq.com/0bsAac47U) that include JVM-related content). +1. Different large companies will have different interview focuses. For instance, if you are interviewing at a company like Alibaba, the focus will be on projects and common interview topics. Alibaba’s written test generally includes coding questions, and during the interview, there’s seldom coding-related questioning, but they tend to ask deep theoretical questions and often inquire about your thoughts on technology. In contrast, if you are interviewing at ByteDance, computer fundamentals, especially algorithms, will be the focus; their interviews are quite stringent about coding skills, and they might start right off with a coding question before discussing anything else. They also ask about common interview topics and your projects, though significantly less. I recommend reading this article [To Uncover the Inside Story of Autumn Recruitment in Internet Giants, I Reviewed Them All](https://mp.weixin.qq.com/s/pBsGQNxvRupZeWt4qZReIA) to understand the common interview question focuses of major companies. +1. Look for interview experiences, especially those related to the target company or similar companies for your desired position. This can help you review specific topics and also serve as a self-test to check your grasp of the material. -看似 Java 后端八股文很多,实际把复习范围一缩小,重要的东西就是那些。考虑到时间问题,你不可能连一些比较冷门的知识点也给准备了。这没必要,主要精力先放在那些重要的知识点即可。 +While it seems there is a lot of common knowledge for Java backend interviews, narrowing your focus reveals that the important material is just that. Given time constraints, there's no need to prepare for some less common knowledge points. Concentrate your energy on the key areas. -## 如何更高效地准备八股文? +## How to Prepare for Common Topics More Efficiently? -对于技术八股文来说,尽量不要死记硬背,这种方式非常枯燥且对自身能力提升有限!但是!想要一点不背是不太现实的,只是说要结合实际应用场景和实战来理解记忆。 +For technical common topics, try to avoid rote memorization, as this approach is very tedious and offers limited enhancement to your skills! However, expecting to remember nothing at all is unrealistic; rather, aim to understand and memorize through practical application contexts. -我一直觉得面试八股文最好是和实际应用场景和实战相结合。很多同学现在的方向都错了,上来就是直接背八股文,硬生生学成了文科,那当然无趣了。 +I believe the best way to approach interview common topics is to integrate them with real-world application scenarios and practice. Many students are currently heading in the wrong direction, directly memorizing topics instead of mixing in practical applications, making it feel very dull. -举个例子:你的项目中需要用到 Redis 来做缓存,你对照着官网简单了解并实践了简单使用 Redis 之后,你去看了 Redis 对应的八股文。你发现 Redis 可以用来做限流、分布式锁,于是你去在项目中实践了一下并掌握了对应的八股文。紧接着,你又发现 Redis 内存不够用的情况下,还能使用 Redis Cluster 来解决,于是你就又去实践了一下并掌握了对应的八股文。 +For example: Your project requires using Redis for caching. After simply consulting the official website to understand and practice basic Redis usage, you proceed to review the related common topics. You discover that Redis can be used for rate limiting and distributed locks; hence, you practice in your project and grasp the relevant common topics. Subsequently, you find that when Redis memory usage is insufficient, Redis Cluster can be utilized to solve the problem, leading you to practice that and understand its common topics as well. -**一定要记住你的主要目标是理解和记关键词,而不是像背课文一样一字一句地记下来,这样毫无意义!效率最低,对自身帮助也最小!** +**Always remember that your main goal is to understand and remember key terms, not to memorize word for word. That approach is pointless! It's the least efficient and provides minimal benefit!** -还要注意适当“投机取巧”,不要单纯死记八股,有些技术方案的实现有很多种,例如分布式 ID、分布式锁、幂等设计,想要完全记住所有方案不太现实,你就重点记忆你项目的实现方案以及选择该种实现方案的原因就好了。当然,其他方案还是建议你简单了解一下,不然也没办法和你选择的方案进行对比。 +Also, be sure to engage in some "intelligent shortcuts." Don’t simply memorize common topics; many technological solutions have multiple implementations, such as distributed IDs, distributed locks, and idempotent design. Remembering every possible solution isn't realistic; instead, focus on recalling the implementation used in your project and the reasons behind choosing that approach. Of course, it's advisable to have a basic understanding of other solutions so you can compare them with your chosen one. -想要检测自己是否搞懂或者加深印象,记录博客或者用自己的理解把对应的知识点讲给别人听也是一个不错的选择。 +To test your understanding or reinforce your memory, writing a blog or explaining the concepts to someone else can be a good strategy. -另外,准备八股文的过程中,强烈建议你花个几个小时去根据你的简历(主要是项目经历部分)思考一下哪些地方可能被深挖,然后把你自己的思考以面试问题的形式体现出来。面试之后,你还要根据当下的面试情况复盘一波,对之前自己整理的面试问题进行完善补充。这个过程对于个人进一步熟悉自己的简历(尤其是项目经历)部分,非常非常有用。这些问题你也一定要多花一些时间搞懂吃透,能够流畅地表达出来。面试问题可以参考 [Java 面试常见问题总结(2024 最新版)](https://t.zsxq.com/0eRq7EJPy),记得根据自己项目经历去深入拓展即可! +Moreover, during your preparation of common topics, I strongly recommend spending a few hours reflecting on your resume (particularly the project experience section) to consider areas that may be probed deeply and framing your thoughts as potential interview questions. After interviews, revisit the interview experience to refine and supplement your previously organized questions. This process is incredibly useful for familiarizing yourself with your resume (especially the project experience) in greater depth. It’s essential to dedicate time to thoroughly understand and articulate these questions. You can refer to [Common Java Interview Questions Summary (2024 Edition)](https://t.zsxq.com/0eRq7EJPy) to expand based on your project experience! -最后,准备技术面试的同学一定要定期复习(自测的方式非常好),不然确实会遗忘的。 +Finally, students preparing for technical interviews should regularly review (self-testing is highly effective), otherwise, you will indeed forget the material. diff --git a/docs/interview-preparation/project-experience-guide.md b/docs/interview-preparation/project-experience-guide.md index 0546626f889..1aa705c22f5 100644 --- a/docs/interview-preparation/project-experience-guide.md +++ b/docs/interview-preparation/project-experience-guide.md @@ -1,114 +1,114 @@ --- -title: 项目经验指南 -category: 面试准备 +title: Project Experience Guide +category: Interview Preparation icon: project --- -::: tip 友情提示 -本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 +::: tip Friendly Reminder +This article is excerpted from **[《Java Interview Guide》](../zhuanlan/java-mian-shi-zhi-bei.md)**. It is a column that teaches you how to prepare for interviews more efficiently, complementing the content of JavaGuide, covering common interview topics (system design, common frameworks, distributed systems, high concurrency, etc.), and high-quality interview experiences. ::: -## 没有项目经验怎么办? +## What if I don't have project experience? -没有项目经验是大部分应届生会碰到的一个问题。甚至说,有很多有工作经验的程序员,对自己在公司做的项目不满意,也想找一个比较有技术含量的项目来做。 +Not having project experience is a common issue faced by most fresh graduates. In fact, many experienced programmers are also dissatisfied with the projects they have worked on in their companies and wish to find more technically challenging projects to work on. -说几种我觉得比较靠谱的获取项目经验的方式,希望能够对你有启发。 +Here are several reliable ways to gain project experience that I hope will inspire you. -### 实战项目视频/专栏 +### Practical Project Videos/Columns -在网上找一个符合自己能力与找工作需求的实战项目视频或者专栏,跟着老师一起做。 +Find a practical project video or column online that matches your abilities and job search requirements, and work on it alongside an instructor. -你可以通过慕课网、哔哩哔哩、拉勾、极客时间、培训机构(比如黑马、尚硅谷)等渠道获取到适合自己的实战项目视频/专栏。 +You can find suitable practical project videos/columns through platforms such as Mooc, Bilibili, Lagou, Geek Time, training institutions (like Black Horse, Silicon Valley), etc. -![慕课网实战课](https://oss.javaguide.cn/javamianshizhibei/mukewangzhiazhanke.png) +![Mooc Practical Course](https://oss.javaguide.cn/javamianshizhibei/mukewangzhiazhanke.png) -尽量选择一个适合自己的项目,没必要必须做分布式/微服务项目,对于绝大部分同学来说,能把一个单机项目做好就已经很不错了。 +Try to choose a project that suits you; there is no need to strictly work on distributed/microservices projects. For the vast majority of students, it is already commendable to do well on a standalone project. -我面试过很多求职者,简历上看着有微服务的项目经验,结果随便问两个问题就知道根本不是自己做的或者说做的时候压根没认真思考。这种情况会给我留下非常不好的印象。 +I have interviewed many candidates who claimed to have microservices project experience based on their resumes, but after asking a couple of questions, it was clear they didn't actually work on it or didn't think about it seriously during that time. This leaves a very negative impression on me. -我在 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)** 的「面试准备篇」中也说过: +In the "Interview Preparation" section of **[《Java Interview Guide》](../zhuanlan/java-mian-shi-zhi-bei.md)**, I also mentioned: -> 个人认为也没必要非要去做微服务或者分布式项目,不一定对你面试有利。微服务或者分布式项目涉及的知识点太多,一般人很难吃透。并且,这类项目其实对于校招生来说稍微有一点超标了。即使你做出来,很多面试官也会认为不是你独立完成的。 +> I personally believe there is no need to focus on microservices or distributed projects, as they may not necessarily benefit your interview. The knowledge involved in microservices or distributed projects is too extensive, making it difficult for an average person to fully grasp. Moreover, such projects may be slightly beyond the scope for recent graduates. Even if you complete them, many interviewers may not consider them as entirely your independent work. > -> 其实,你能把一个单体项目做到极致也很好,对于个人能力提升不比做微服务或者分布式项目差。如何做到极致?代码质量这里就不提了,更重要的是你要尽量让自己的项目有一些亮点(比如你是如何提升项目性能的、如何解决项目中存在的一个痛点的),项目经历取得的成果尽量要量化一下比如我使用 xxx 技术解决了 xxx 问题,系统 qps 从 xxx 提高到了 xxx。 +> In fact, doing an excellent job on a monolithic project is also very good, and can improve your personal skills just as well as tackling microservices or distributed projects. How to excel at it? Without even mentioning code quality, it’s more important to ensure your project has some highlights (like how you improved project performance or solved a specific pain point in the project). Try to quantify the achievements gained from your project experience, for example, "I used xxx technology to solve xxx problem, and the system's QPS increased from xxx to xxx." -跟着老师做的过程中,你一定要有自己的思考,不要浅尝辄止。对于很多知识点,别人的讲解可能只是满足项目就够了,你自己想多点知识的话,对于重要的知识点就要自己学会去深入学习。 +While following the instructor's guidance, you must have your own thoughts and not just skim the surface. For many knowledge points, others' explanations may only be sufficient to complete the project, but if you want to deepen your understanding of important knowledge, you need to learn more independently. -### 实战类开源项目 +### Practical Open Source Projects -GitHub 或者码云上面有很多实战类别项目,你可以选择一个来研究,为了让自己对这个项目更加理解,在理解原有代码的基础上,你可以对原有项目进行改进或者增加功能。 +There are many practical category projects on GitHub or Gitee; you can choose one to study. To gain a better understanding of the project, you can improve or add features to the original project based on your comprehension of its code. -你可以参考 [Java 优质开源实战项目](https://javaguide.cn/open-source-project/practical-project.html "Java 优质开源实战项目") 上面推荐的实战类开源项目,质量都很高,项目类型也比较全面,涵盖博客/论坛系统、考试/刷题系统、商城系统、权限管理系统、快速开发脚手架以及各种轮子。 +You can refer to the [High-Quality Open Source Practical Projects](https://javaguide.cn/open-source-project/practical-project.html "High-Quality Open Source Practical Projects") for recommended practical open source projects, which are of high quality, covering a wide range of project types such as blog/forum systems, exam/question practice systems, e-commerce systems, permission management systems, rapid development scaffolds, and various libraries. -![Java 优质开源实战项目](https://oss.javaguide.cn/javamianshizhibei/javaguide-practical-project.png) +![High-Quality Open Source Practical Projects](https://oss.javaguide.cn/javamianshizhibei/javaguide-practical-project.png) -一定要记住:**不光要做,还要改进,改善。不论是实战项目视频或者专栏还是实战类开源项目,都一定会有很多可以完善改进的地方。** +Always remember: **It’s not only about doing but also about improving and enhancing. Whether it’s practical project videos/columns or practical open source projects, there will always be many areas to完善和改进**. -### 从头开始做 +### Start from Scratch -自己动手去做一个自己想完成的东西,遇到不会的东西就临时去学,现学现卖。 +Take the initiative to create something you want to accomplish, and learn what you don’t know as you go along. -这个要求比较高,我建议你已经有了一个项目经验之后,再采用这个方法。如果你没有做过项目的话,还是老老实实采用上面两个方法比较好。 +This approach has higher requirements, and I suggest you adopt it after you have some project experience. If you have never worked on a project before, it’s better to stick to the two methods mentioned above. -### 参加各种大公司组织的各种大赛 +### Participate in Various Competitions Organized by Big Companies -如果参加这种赛事能获奖的话,项目含金量非常高。即使没获奖也没啥,也可以写简历上。 +If you can win awards in such competitions, the project will carry significant weight. Even if you don’t win, you can still include it on your resume. -![阿里云天池大赛](https://oss.javaguide.cn/xingqiu/up-673f598477242691900a1e72c5d8b26df2c.png) +![Aliyun Tianchi Competition](https://oss.javaguide.cn/xingqiu/up-673f598477242691900a1e72c5d8b26df2c.png) -### 参与实际项目 +### Participate in Actual Projects -通常情况下,你有如下途径接触到企业实际项目的开发: +Typically, you can access actual project development in enterprises through the following means: -1. 老师接的项目; -2. 自己接的私活; -3. 实习/工作接触到的项目; +1. Projects contracted by teachers; +1. Side projects you take on; +1. Projects encountered during internships/jobs; -老师接的项目和自己接的私活通常都是一些偏业务的项目,很少会涉及到性能优化。这种情况下,你可以考虑对项目进行改进,别怕花时间,某个时间用心做好一件事情就好比如你对项目的数据模型进行改进、引入缓存提高访问速度等等。 +Projects contracted by teachers and side projects often focus more on business aspects and rarely involve performance optimization. In such cases, consider improving the project; don’t hesitate to spend time on it, as it's important to dedicate yourself to doing one thing well over a certain period, like improving the project's data model or introducing caching to enhance access speed. -实习/工作接触到的项目类似,如果遇到一些偏业务的项目,也是要自己私下对项目进行改进优化。 +The projects you encounter during internships/jobs are similar; if you face more business-oriented projects, you should also privately work on improving and optimizing them. -尽量是真的对项目进行了优化,这本身也是对个人能力的提升。如果你实在是没时间去实践的话,也没关系,吃透这个项目优化手段就好,把一些面试可能会遇到的问题提前准备一下。 +Strive to genuinely optimize the project, as this itself enhances your personal skill set. If you really don’t have time for practice, that’s fine; just understand the optimization techniques for this project and prepare for common interview questions in advance. -## 有没有还不错的项目推荐? +## Are there any recommended projects? -**[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)** 的「面试准备篇」中有一篇文章专门整理了一些比较高质量的实战项目,非常适合用来学习或者作为项目经验。 +In the "Interview Preparation" section of **[《Java Interview Guide》](../zhuanlan/java-mian-shi-zhi-bei.md)**, there is an article specifically compiling a list of high-quality practical projects that are very suitable for learning or as project experience. ![](https://oss.javaguide.cn/javamianshizhibei/project-experience-guide.png) -这篇文章一共推荐了 15+ 个实战项目,有业务类的,也有轮子类的,有开源项目、也有视频教程。对于参加校招的小伙伴,我更建议做一个业务类项目加上一个轮子类的项目。 +This article recommends a total of over 15 practical projects, including business-related projects and library projects, open source projects as well as video tutorials. For those participating in campus recruitment, I would particularly recommend working on one business project and one library project. -## 我跟着视频做的项目会被面试官嫌弃不? +## Will the interviewer look down on projects I did by following videos? -很多应届生都是跟着视频做的项目,这个大部分面试官都心知肚明。 +Many fresh graduates create projects by following videos, and most interviewers are well aware of this. -不排除确实有些面试官不吃这一套,这个也看人。不过我相信大多数面试官都是能理解的,毕竟你在学校的时候实际上是没有什么获得实际项目经验的途径的。 +It's possible that some interviewers don't appreciate this approach, but that typically depends on the person. However, I believe that most interviewers can understand this, given that students often lack avenues to gain practical project experience during their studies. -大部分应届生的项目经验都是自己在网上找的或者像你一样买的付费课程跟着做的,极少部分是比较真实的项目。 从你能想着做一个实战项目来说,我觉得初衷是好的,确实也能真正学到东西。 但是,究竟有多少是自己掌握了很重要。看视频最忌讳的是被动接受,自己多改进一下,多思考一下!就算是你跟着视频做的项目,也是可以优化的! +Most fresh graduates' project experiences are either self-found online or similar to yours, where they buy paid courses and follow along; only a small portion is from genuine projects. That you are willing to create a practical project speaks to a good intention, and you can indeed learn invaluable things from it. However, the crucial point is how much you master. The worst mistake when watching videos is to passively accept the content; instead, try to make your modifications and think critically! Even if you worked on a project by following a video, it can still be optimized! -**如果你想真正学到东西的话,建议不光要把项目单纯完成跑起来,还要去自己尝试着优化!** +**If you truly want to learn, I suggest that you not only focus on simply completing the project to run but also attempt to optimize it yourself!** -简单说几个比较容易的优化点: +Here are a few relatively easy optimization points: -1. **全局异常处理**:很多项目这方面都做的不是很好,可以参考我的这篇文章:[《使用枚举简单封装一个优雅的 Spring Boot 全局异常处理!》](https://mp.weixin.qq.com/s/Y4Q4yWRqKG_lw0GLUsY2qw) 来做优化。 -2. **项目的技术选型优化**:比如使用 Guava 做本地缓存的地方可以换成 **Caffeine** 。Caffeine 的各方面的表现要更加好!再比如 Controller 层是否放了太多的业务逻辑。 -3. **数据库方面**:数据库设计可否优化?索引是否使用使用正确?SQL 语句是否可以优化?是否需要进行读写分离? -4. **缓存**:项目有没有哪些数据是经常被访问的?是否引入缓存来提高响应速度? -5. **安全**:项目是否存在安全问题? -6. …… +1. **Global Exception Handling**: Many projects don't handle this aspect very well; you can refer to my article: [《Using Enums to Simply Encapsulate an Elegant Spring Boot Global Exception Handling!》](https://mp.weixin.qq.com/s/Y4Q4yWRqKG_lw0GLUsY2qw) for optimization ideas. +1. **Project Technology Stack Optimization**: For instance, replace the local cache used with Guava with **Caffeine**. Caffeine offers better performance overall! Also, consider whether too much business logic is put in the Controller layer. +1. **Database Considerations**: Can the database design be optimized? Are indexes used correctly? Can SQL statements be optimized? Is it necessary to perform read-write separation? +1. **Caching**: Are there data points in the project that are frequently accessed? Has caching been introduced to improve response speed? +1. **Security**: Are there any security issues in the project? +1. …… -另外,我在星球分享过常见的性能优化方向实践案例,涉及到多线程、异步、索引、缓存等方向,强烈推荐你看看: 。 +Additionally, I have shared common performance optimization practices in my community, covering areas such as multi-threading, asynchronous operations, indexing, and caching. I strongly recommend you take a look: . -最后,**再给大家推荐一个 IDEA 优化代码的小技巧,超级实用!** +Finally, **here’s a useful tip for optimizing code in IDEA—super handy!** -分析你的代码:右键项目-> Analyze->Inspect Code +Analyze your code: Right-click the project -> Analyze -> Inspect Code ![](https://oss.javaguide.cn/xingqiu/up-651672bce128025a135c1536cd5dc00532e.png) -扫描完成之后,IDEA 会给出一些可能存在的代码坏味道比如命名问题。 +After the scan is complete, IDEA will suggest some potential code smells like naming issues. ![](https://oss.javaguide.cn/xingqiu/up-05c83b319941995b07c8020fddc57f26037.png) -并且,你还可以自定义检查规则。 +Moreover, you can customize the inspection rules. ![](https://oss.javaguide.cn/xingqiu/up-6b618ad3bad0bc3f76e6066d90c8cd2f255.png) diff --git a/docs/interview-preparation/resume-guide.md b/docs/interview-preparation/resume-guide.md index 396ef4b47e4..cfebd62de7b 100644 --- a/docs/interview-preparation/resume-guide.md +++ b/docs/interview-preparation/resume-guide.md @@ -1,295 +1,69 @@ --- -title: 程序员简历编写指南 -category: 面试准备 -icon: jianli +title: Programmer Resume Writing Guide +category: Interview Preparation +icon: resume --- -::: tip 友情提示 -本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 +::: tip Friendly Reminder +This article is excerpted from **[“Java Interview Guide”](../zhuanlan/java-mian-shi-zhi-bei.md)**. It is a booklet that teaches you how to prepare for interviews more efficiently, covering common topics (system design, common frameworks, distributed systems, high concurrency, etc.), and high-quality interview experiences. ::: -## 前言 +## Introduction -一份好的简历可以在整个申请面试以及面试过程中起到非常重要的作用。 +A good resume plays a very important role throughout the application and interview process. -**为什么说简历很重要呢?** 我们可以从下面几点来说: +**Why is the resume so important?** We can discuss it from the following points: -**1、简历就像是我们的一个门面一样,它在很大程度上决定了是否能够获得面试机会。** +**1. The resume is like our facade; it largely determines whether we can get an interview opportunity.** -- 假如你是网申,你的简历必然会经过 HR 的筛选,一张简历 HR 可能也就花费 10 秒钟左右看一下,然后决定你能否进入面试。 -- 假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。 +- If you are applying online, your resume will inevitably go through HR's screening. An HR person might spend only about 10 seconds looking at a resume before deciding whether you can proceed to the interview. +- If you are referred internally, if your resume does not have any advantages, even if the person referring you is very diligent, it will be of no help. -另外,就算你通过了第一轮的筛选获得面试机会,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。 +Moreover, even if you pass the first round of screening and get an interview opportunity, the interviewer will also judge whether you are worth their time based on your resume. -**2、简历上的内容很大程度上决定了面试官提问的侧重点。** +**2. The content on the resume largely determines the focus of the interviewer's questions.** -- 一般情况下你的简历上注明你会的东西才会被问到(Java 基础、集合、并发、MySQL、Redis 、Spring、Spring Boot 这些算是每个人必问的),比如写了你熟练使用 Redis,那面试官就很大概率会问你 Redis 的一些问题,再比如你写了你在项目中使用了消息队列,那面试官大概率问很多消息队列相关的问题。 -- 技能熟练度在很大程度上也决定了面试官提问的深度。 +- Generally, the things you list on your resume are what will be asked about (Java basics, collections, concurrency, MySQL, Redis, Spring, Spring Boot are all must-ask topics). For example, if you state that you are proficient in Redis, the interviewer is likely to ask you some questions about Redis. Similarly, if you mention that you used message queues in your projects, the interviewer will likely ask many questions related to message queues. +- The level of proficiency in skills also largely determines the depth of the interviewer's questions. -在不夸大自己能力的情况下,写出一份好的简历也是一项很棒的能力。一般情况下,技术能力和学习能力比较厉害的,写出来的简历也比较棒! +Writing a good resume without exaggerating your abilities is also a great skill. Generally, those with strong technical and learning abilities tend to write better resumes! -## 简历模板 +## Resume Template -简历的样式真的非常非常重要!!!如果你的简历样式丑到没朋友的话,面试官真的没有看下去的欲望。一天处理上百份的简历的痛苦,你不懂! +The style of the resume is extremely important!!! If your resume is unattractive, the interviewer will really have no desire to continue reading. You wouldn't understand the pain of processing hundreds of resumes in a day! -我这里的话,推荐大家使用 Markdown 语法写简历,然后再将 Markdown 格式转换为 PDF 格式后进行简历投递。如果你对 Markdown 语法不太了解的话,可以花半个小时简单看一下 Markdown 语法说明: 。 +I recommend everyone use Markdown syntax to write their resumes and then convert the Markdown format to PDF for submission. If you are not familiar with Markdown syntax, you can spend half an hour to briefly look at the Markdown syntax guide: . -下面是我收集的一些还不错的简历模板: +Here are some decent resume templates I have collected: -- 适合中文的简历模板收集(推荐,开源免费): -- 木及简历(推荐,部分免费) : -- 简单简历(推荐,部分免费): -- 极简简历(免费): -- Markdown 简历排版工具(开源免费): -- 站长简历(收费,支持 AI 生成): -- typora+markdown+css 自定义简历模板 : -- 超级简历(部分收费) : +- Collection of resume templates suitable for Chinese (recommended, open source and free): +- Mujicv (recommended, partially free): +- Easy Resume (recommended, partially free): +- Minimalist Resume (free): +- Markdown Resume Formatting Tool (open source and free): +- Webmaster Resume (paid, supports AI generation): +- Typora + Markdown + CSS custom resume template: +- Super Resume (partially paid): -上面这些简历模板大多是只有 1 页内容,很难展现足够的信息量。如果你不是顶级大牛(比如 ACM 大赛获奖)的话,我建议还是尽可能多写一点可以突出你自己能力的内容(校招生 2 页之内,社招生 3 页之内,记得精炼语言,不要过多废话)。 +Most of these resume templates are only one page long, making it difficult to showcase enough information. If you are not a top-tier talent (like an ACM competition winner), I suggest writing a bit more to highlight your abilities (within 2 pages for campus recruits, within 3 pages for social recruits, remember to refine your language and avoid excessive fluff). -再总结几点 **简历排版的注意事项**: +Here are a few **layout tips for resumes**: -- 尽量简洁,不要太花里胡哨。 -- 技术名词最好规范大小写比较好,比如 java->Java ,spring boot -> Spring Boot 。这个虽然有些面试官不会介意,但是很多面试官都会在意这个细节的。 -- 中文和数字英文之间加上空格的话看起来会舒服一点。 +- Keep it simple; avoid overly flashy designs. +- It's best to standardize the capitalization of technical terms, for example, java -> Java, spring boot -> Spring Boot. While some interviewers may not mind, many will pay attention to these details. +- Adding spaces between Chinese characters and English numbers makes it look more comfortable. -另外,知识星球里还有真实的简历模板可供参考,地址: (需加入[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)获取)。 +Additionally, there are real resume templates available for reference in the Knowledge Planet, address: (you need to join [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) to access). ![](https://oss.javaguide.cn/javamianshizhibei/image-20230918073550606.png) -## 简历内容 +## Resume Content -### 个人信息 +### Personal Information -- 最基本的 :姓名(身份证上的那个)、年龄、电话、籍贯、联系方式、邮箱地址 -- 潜在加分项 : Github 地址、博客地址(如果技术博客和 Github 上没有什么内容的话,就不要写了) +- The basics: Name (the one on your ID), age, phone number, place of origin, contact information, email address. +- Potential bonus items: GitHub address, blog address (if your technical blog and GitHub have no content, don’t include them). -示例: +Example: -![](https://oss.javaguide.cn/zhishixingqiu/20210428212337599.png) - -**简历要不要放照片呢?** 很多人写简历的时候都有这个问题。 - -其实放不放都行,影响不大,完全不用在意这个问题。除非,你投递的岗位明确要求要放照片。 不过,如果要放的话,不要放生活照,还是应该放正规一些的照片比如证件照。 - -### 求职意向 - -你想要应聘什么岗位,希望在什么城市。另外,你也可以将求职意向放到个人信息这块写。 - -示例: - -![](https://oss.javaguide.cn/zhishixingqiu/20210428212410288.png) - -### 教育经历 - -教育经历也不可或缺。通过教育经历的介绍,你要确保能让面试官就可以知道你的学历、专业、毕业学校以及毕业的日期。 - -示例: - -> 北京理工大学 硕士,软件工程 2019.09 - 2022.01 -> 湖南大学 学士,应用化学 2015.09 ~ 2019.06 - -### 专业技能 - -先问一下你自己会什么,然后看看你意向的公司需要什么。一般 HR 可能并不太懂技术,所以他在筛选简历的时候可能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可以写上自己了解这个技能。 - -下面是一份最新的 Java 后端开发技能清单,你可以根据自身情况以及岗位招聘要求做动态调整,核心思想就是尽可能满足岗位招聘的所有技能要求。 - -![Java 后端技能模板](https://oss.javaguide.cn/zhishixingqiu/jinengmuban.png) - -我这里再单独放一个我看过的某位同学的技能介绍,我们来找找问题。 - -![](https://oss.javaguide.cn/zhishixingqiu/up-a58d644340f8ce5cd32f9963f003abe4233.png) - -上图中的技能介绍存在的问题: - -- 技术名词最好规范大小写比较好,比如 java->Java ,spring boot -> Spring Boot 。这个虽然有些面试官不会介意,但是很多面试官都会在意这个细节的。 -- 技能介绍太杂,没有亮点。不需要全才,某个领域做得好就行了! -- 对 Java 后台开发的部分技能比如 Spring Boot 的熟悉度仅仅为了解,无法满足企业的要求。 - -### 实习经历/工作经历(重要) - -工作经历针对社招,实习经历针对校招。 - -工作经历建议采用时间倒序的方式来介绍。实习经历和工作经历都需要简单突出介绍自己在职期间主要做了什么。 - -示例: - -> **XXX 公司 (201X 年 X 月 ~ 201X 年 X 月 )** -> -> - **职位**:Java 后端开发工程师 -> - **工作内容**:主要负责 XXX - -### 项目经历(重要) - -简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。 - -很多求职者的项目经历介绍都会面临过于啰嗦、过于简单、没突出亮点等问题。 - -项目经历介绍模板如下: - -> 项目名称(字号要大一些) -> -> 2017-05~2018-06 淘宝 Java 后端开发工程师 -> -> - **项目描述** : 简单描述项目是做什么的。 -> - **技术栈** :用了什么技术(如 Spring Boot + MySQL + Redis + Mybatis-plus + Spring Security + Oauth2) -> - **工作内容/个人职责** : 简单描述自己做了什么,解决了什么问题,带来了什么实质性的改善。突出自己的能力,不要过于平淡的叙述。 -> - **个人收获(可选)** : 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用。通常是可以不用写个人收获的,因为你在个人职责介绍中写的东西已经表明了自己的主要收获。 -> - **项目成果(可选)** :简单描述这个项目取得了什么成绩。 - -**1、项目经历应该突出自己做了什么,简单概括项目基本情况。** - -项目介绍尽量压缩在两行之内,不需要介绍太多,但也不要随便几个字就介绍完了。 - -另外,个人收获和项目成果都是可选的,如果选择写的话,也不要花费太多篇幅,记住你的重点是介绍工作内容/个人职责。 - -**2、技术架构直接写技术名词就行,不要再介绍技术是干嘛的了,没意义,属于无效介绍。** - -![](https://oss.javaguide.cn/github/javaguide/interview-preparation/46c92fbc5160e65dd85c451143177144.png) - -**3、尽量减少纯业务的个人职责介绍,对于面试不太友好。尽量再多挖掘一些亮点(6~8 条个人职责介绍差不多了,做好筛选),最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目优化了某个模块的性能。** - -即使不是你做的功能模块或者解决的问题,你只要搞懂吃透了就能拿来自己用,适当润色即可! - -像性能优化方向上的亮点面试之前也比较容易准备,但也不要都是性能优化相关的,这种也算是一个极端。 - -另外,技术优化取得的成果尽量要量化一下: - -- 使用 xxx 技术解决了 xxx 问题,系统 QPS 从 xxx 提高到了 xxx。 -- 使用 xxx 技术了优化了 xxx 接口,系统 QPS 从 xxx 提高到了 xxx。 -- 使用 xxx 技术解决了 xxx 问题,查询速度优化了 xxx,系统 QPS 达到 10w+。 -- 使用 xxx 技术优化了 xxx 模块,响应时间从 2s 降低到 0.2s。 -- …… - -个人职责介绍示例(这里只是举例,不要照搬,结合自己项目经历自己去写,不然面试的时候容易被问倒) : - -- 基于 Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权,使用 RBAC 权限模型实现动态权限控制。 -- 参与项目订单模块的开发,负责订单创建、删除、查询等功能,基于 Spring 状态机实现订单状态流转。 -- 商品和订单搜索场景引入 Elasticsearch,并且实现了相关商品推荐以及搜索提示功能。 -- 整合 Canal + RabbitMQ 将 MySQL 增量数据(如商品、订单数据)同步到 Elasticsearch。 -- 利用 RabbitMQ 官方提供的延迟队列插件实现延时任务场景比如订单超时自动取消、优惠券过期提醒、退款处理。 -- 消息推送系统引入 RabbitMQ 实现异步处理、削峰填谷和服务解耦,最高推送速度 10w/s,单日最大消息量 2000 万。 -- 使用 MAT 工具分析 dump 文件解决了广告服务新版本上线后导致大量的服务超时告警的问题。 -- 排查并解决扣费模块由于扣费父任务和反作弊子任务使用同一个线程池导致的死锁问题。 -- 基于 EasyExcel 实现广告投放数据的导入导出,通过 MyBatis 批处理插入数据,基于任务表实现异步。 -- 负责用户统计模块的开发,使用 CompletableFuture 并行加载后台用户统计模块的数据信息,平均相应时间从 3.5s 降低到 1s。 -- 基于 Sentinel 对核心场景(如用户登入注册、收货地址查询等)进行限流、降级,保护系统,提升用户体验。 -- 热门数据(如首页、热门博客)使用 Redis+Caffeine 两级缓存,解决了缓存击穿和穿透问题,查询速度毫秒级,QPS 30w+。 -- 使用 CompletableFuture 优化购物车查询模块,对获取用户信息、商品详情、优惠券信息等异步 RPC 调用进行编排,响应时间从 2s 降低为 0.2s。 -- 搭建 EasyMock 服务,用于模拟第三方平台接口,方便了在网络隔离情况下的接口对接工作。 -- 基于 SkyWalking + Elasticsearch 搭建分布式链路追踪系统实现全链路监控。 - -**4、如果你觉得你的项目技术比较落后的话,可以自己私下进行改进。重要的是让项目比较有亮点,通过什么方式就无所谓了。** - -项目经历这部分对于简历来说非常重要,[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)的面试准备篇有好几篇关于优化项目经历的文章,建议你仔细阅读一下,应该会对你有帮助。 - -![](https://oss.javaguide.cn/zhishixingqiu/4e11dbc842054e53ad6c5f0445023eb5~tplv-k3u1fbpfcp-zoom-1.png) - -**5、避免个人职责介绍都是围绕一个技术点来写,非常不可取。** - -![](https://oss.javaguide.cn/zhishixingqiu/image-20230424222513028.png) - -**6、避免模糊性描述,介绍要具体(技术+场景+效果),也要注意精简语言(避免堆砌技术词,省略不必要的描述)。** - -![](https://oss.javaguide.cn/github/javaguide/interview-preparation/project-experience-avoiding-ambiguity-descriptio.png) - -### 荣誉奖项(可选) - -如果你有含金量比较高的竞赛(比如 ACM、阿里的天池大赛)的获奖经历的话,荣誉奖项这块内容一定要写一下!并且,你还可以将荣誉奖项这块内容适当往前放,放在一个更加显眼的位置。 - -### 校园经历(可选) - -如果有比较亮眼的校园经历的话就简单写一下,没有就不写! - -### 个人评价 - -**个人评价就是对自己的解读,一定要用简洁的语言突出自己的特点和优势,避免废话!** 像勤奋、吃苦这些比较虚的东西就不要扯了,面试官看着这种个人评价就烦。 - -我们可以从下面几个角度来写个人评价: - -- 文档编写能力、学习能力、沟通能力、团队协作能力 -- 对待工作的态度以及个人的责任心 -- 能承受的工作压力以及对待困难的态度 -- 对技术的追求、对代码质量的追求 -- 分布式、高并发系统开发或维护经验 - -列举 3 个实际的例子: - -- 学习能力较强,大三参加国家软件设计大赛的时候快速上手 Python 写了一个可配置化的爬虫系统。 -- 具有团队协作精神,大三参加国家软件设计大赛的时候协调项目组内 5 名开发同学,并对编码遇到困难的同学提供帮助,最终顺利在 1 个月的时间完成项目的核心功能。 -- 项目经验丰富,在校期间主导过多个企业级项目的开发。 - -## STAR 法则和 FAB 法则 - -### STAR 法则(Situation Task Action Result) - -相信大家一定听说过 STAR 法则。对于面试,你可以将这个法则用在自己的简历以及和面试官沟通交流的过程中。 - -STAR 法则由下面 4 个单词组成(STAR 法则的名字就是由它们的首字母组成): - -- **Situation:** 情景。 事情是在什么情况下发生的? -- **Task:** 任务。你的任务是什么? -- **Action:** 行动。你做了什么? -- **Result:** 结果。最终的结果怎样? - -### FAB 法则(Feature Advantage Benefit) - -除了 STAR 法则,你还需要了解在销售行业经常用到的一个叫做 FAB 的法则。 - -FAB 法则由下面 3 个单词组成(FAB 法则的名字就是由它们的首字母组成): - -- **Feature:** 你的特征/优势是什么? -- **Advantage:** 比别人好在哪些地方; -- **Benefit:** 如果雇佣你,招聘方会得到什么好处。 - -简单来说,**FAB 法则主要是让你的面试官知道你的优势和你能为公司带来的价值。** - -## 建议 - -### 避免页数过多 - -精简表述,突出亮点。校招简历建议不要超过 2 页,社招简历建议不要超过 3 页。如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。 - -看了几千份简历,有少部分同学的简历页数都接近 10 页了,让我头皮发麻。 - -![简历页数过多](https://oss.javaguide.cn/zhishixingqiu/image-20230508223646164.png) - -### 避免语义模糊 - -尽量避免主观表述,少一点语义模糊的形容词。表述要简洁明了,简历结构要清晰。 - -举例: - -- 不好的表述:我在团队中扮演了很重要的角色。 -- 好的表述:我作为后端技术负责人,领导团队完成后端项目的设计与开发。 - -### 注意简历样式 - -简历样式同样很重要,一定要注意!不必追求花里胡哨,但要尽量保证结构清晰且易于阅读。 - -### 其他 - -- 一定要使用 PDF 格式投递,不要使用 Word 或者其他格式投递。这是最基本的! -- 不会的东西就不要写在简历上了。注意简历真实性,适当润色没有问题。 -- 工作经历建议采用时间倒序的方式来介绍,实习经历建议将最有价值的放在最前面。 -- 将自己的项目经历完美的展示出来非常重要,重点是突出自己做了什么(挖掘亮点),而不是介绍项目是做什么的。 -- 项目经历建议以时间倒序排序,另外项目经历不在于多(精选 2~3 即可),而在于有亮点。 -- 准备面试的过程中应该将你写在简历上的东西作为重点,尤其是项目经历上和技能介绍上的。 -- 面试和工作是两回事,聪明的人会把面试官往自己擅长的领域领,其他人则被面试官牵着鼻子走。虽说面试和工作是两回事,但是你要想要获得自己满意的 offer ,你自身的实力必须要强。 - -## 简历修改 - -到目前为止,我至少帮助 **6000+** 位球友提供了免费的简历修改服务。由于个人精力有限,修改简历仅限加入星球的读者,需要帮看简历的话,可以加入 [**JavaGuide 官方知识星球**](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html#%E7%AE%80%E5%8E%86%E4%BF%AE%E6%94%B9)(点击链接查看详细介绍)。 - -![img](https://oss.javaguide.cn/xingqiu/%E7%AE%80%E5%8E%86%E4%BF%AE%E6%94%B92.jpg) - -虽然收费只有培训班/训练营的百分之一,但是知识星球里的内容质量更高,提供的服务也更全面,非常适合准备 Java 面试和学习 Java 的同学。 - -下面是星球提供的部分服务(点击下方图片即可获取知识星球的详细介绍): - -[![星球服务](https://oss.javaguide.cn/xingqiu/xingqiufuwu.png)](../about-the-author/zhishixingqiu-two-years.md) - -这里再提供一份限时专属优惠卷: - -![知识星球30元优惠卷](https://oss.javaguide.cn/xingqiu/xingqiuyouhuijuan-30.jpg) +!\[\](https://oss.javaguide.cn/zhishixingqiu/20210428212337599.png diff --git a/docs/interview-preparation/self-test-of-common-interview-questions.md b/docs/interview-preparation/self-test-of-common-interview-questions.md index 85b0e236c01..7945f6add04 100644 --- a/docs/interview-preparation/self-test-of-common-interview-questions.md +++ b/docs/interview-preparation/self-test-of-common-interview-questions.md @@ -1,18 +1,18 @@ --- -title: 常见面试题自测(付费) -category: 知识星球 +title: Common Interview Questions Self-Assessment (Paid) +category: Knowledge Planet icon: security-fill --- -面试之前,强烈建议大家多拿常见的面试题来进行自测,检查一下自己的掌握情况,这是一种非常实用的备战技术面试的小技巧。 +Before the interview, it is highly recommended that everyone take common interview questions for self-assessment to check their grasp of the subject. This is a very practical technique for preparing for technical interviews. -在 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)** 的 **「技术面试题自测篇」** ,我总结了 Java 面试中最重要的知识点的最常见的面试题并按照面试提问的方式展现出来。 +In **[“Java Interview Guide”](../zhuanlan/java-mian-shi-zhi-bei.md)**, under the section **“Technical Interview Questions Self-Assessment”**, I have summarized the most common interview questions related to the most important knowledge points in Java interviews and presented them in the format of interview questions. ![](https://oss.javaguide.cn/xingqiu/image-20220628102643202.png) -每一道用于自测的面试题我都会给出重要程度,方便大家在时间比较紧张的时候根据自身情况来选择性自测。并且,我还会给出提示,方便你回忆起对应的知识点。 +For each self-assessment interview question, I provide an importance level, making it easier for everyone to choose selectively based on their circumstances when time is tight. Additionally, I will provide hints to help you recall the corresponding knowledge points. -在面试中如果你实在没有头绪的话,一个好的面试官也是会给你提示的。 +If you really have no clue during the interview, a good interviewer will also give you hints. ![](https://oss.javaguide.cn/xingqiu/image-20220628102848236.png) diff --git a/docs/interview-preparation/teach-you-how-to-prepare-for-the-interview-hand-in-hand.md b/docs/interview-preparation/teach-you-how-to-prepare-for-the-interview-hand-in-hand.md index ae8a4bf6f18..b034c1c7f21 100644 --- a/docs/interview-preparation/teach-you-how-to-prepare-for-the-interview-hand-in-hand.md +++ b/docs/interview-preparation/teach-you-how-to-prepare-for-the-interview-hand-in-hand.md @@ -1,209 +1,57 @@ --- -title: 如何高效准备Java面试? -category: 知识星球 +title: How to Efficiently Prepare for Java Interviews? +category: Knowledge Planet icon: path --- -::: tip 友情提示 -本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 +::: tip Friendly Reminder +This article is excerpted from **[《Java Interview Guide》](../zhuanlan/java-mian-shi-zhi-bei.md)**. It is a column that teaches you how to prepare for interviews more efficiently, complementing the content of JavaGuide, covering common interview topics (system design, common frameworks, distributed systems, high concurrency, etc.), and high-quality interview experiences. ::: -你的身边一定有很多编程比你厉害但是找的工作并没有你好的朋友!**技术面试不同于编程,编程厉害不代表技术面试就一定能过。** +You must have many friends around you who are better at programming but haven't found a job as good as yours! **Technical interviews are different from programming; being good at programming does not guarantee success in technical interviews.** -现在你去面个试,不认真准备一下,那简直就是往枪口上撞。我们大部分都只是普通人,没有发过顶级周刊或者获得过顶级大赛奖项。在这样一个技术面试氛围下,我们需要花费很多精力来准备面试,来提高自己的技术能力。“[面试造火箭,工作拧螺丝钉](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247491596&idx=1&sn=36fbf80922f71c200990de11514955f7&chksm=cea1afc7f9d626d1c70d5e54505495ac499ce6eb5e05ba4f4bb079a8563a84e27f17ceff38af&token=353590436&lang=zh_CN&scene=21#wechat_redirect)” 就是目前的一个常态,预计未来很长很长一段时间也还是会是这样。 +If you go for an interview without serious preparation, it's like walking into the line of fire. Most of us are just ordinary people who haven't published in top journals or won top competition awards. In this technical interview atmosphere, we need to spend a lot of energy preparing for interviews to improve our technical abilities. “**Interviewing is like building rockets, while work is like tightening screws**” is currently a norm, and it is expected to remain so for a long time. -准备面试不等于耍小聪明或者死记硬背面试题。 **一定不要对面试抱有侥幸心理。打铁还需自身硬!** 千万不要觉得自己看几篇面经,看几篇面试题解析就能通过面试了。一定要静下心来深入学习! +Preparing for an interview does not mean being clever or memorizing interview questions. **Do not have a fluke mentality about interviews. You must be strong yourself!** Never think that reading a few interview experiences or question analyses will guarantee you pass the interview. You must calm down and study deeply! -这篇我会从宏观面出发简单聊聊如何准备 Java 面试,让你少走弯路! +In this article, I will briefly discuss how to prepare for Java interviews from a macro perspective, helping you avoid detours! -## 尽早以求职为导向来学习 +## Start Learning with Job Orientation Early -我是比较建议还在学校的同学尽可能早一点以求职为导向来学习的。 +I recommend that students still in school try to learn with job orientation as early as possible. -**这样更有针对性,并且可以大概率减少自己处在迷茫的时间,很大程度上还可以让自己少走很多弯路。** +**This approach is more targeted and can significantly reduce the time spent in confusion, allowing you to avoid many detours.** -但是!不要把“以求职为导向学习”理解为“我就不用学课堂上那些计算机基础课程了”! +However! Do not interpret "learning with job orientation" as "I don't need to learn those basic computer courses in class"! -我在之前的很多次分享中都强调过:**一定要用心学习计算机基础知识!操作系统、计算机组成原理、计算机网络真的不是没有实际用处的学科!!!** +I have emphasized in many previous shares: **You must earnestly learn computer fundamentals! Operating systems, computer organization principles, and computer networks are indeed useful subjects!!!** -你会发现大厂面试你会用到,以后工作之后你也会用到。我分别列举 2 个例子吧! +You will find that these topics will be relevant in interviews with large companies and in your future work. Here are two examples: -- **面试中**:像字节、腾讯这些大厂的技术面试以及几乎所有公司的笔试都会考操作系统相关的问题。 -- **工作中**:在实际使用缓存的时候,软件层次而言的缓存思想,则是源自数据库速度、Redis(内存中间件)速度、本地内存速度之间的不匹配;而在计算机存储层次结构设计中,我们也能发现同样的问题及缓存思想的使用:内存用于解决磁盘访问速度过慢的问题,CPU 用三级缓存缓解寄存器和内存之间的速度差异。它们面临的都是同一个问题(速度不匹配)和同一个思想,那么计算机先驱者在存储层次结构设计上对缓存性能的优化措施,同样也适用于软件层次缓存的性能优化。 +- **In interviews**: Technical interviews at major companies like ByteDance and Tencent, as well as almost all companies' written tests, will cover questions related to operating systems. +- **In work**: When using caching in practice, the concept of caching at the software level originates from the mismatch between database speed, Redis (in-memory middleware) speed, and local memory speed; in the design of computer storage hierarchy, we can also find similar issues and the use of caching concepts: memory is used to solve the problem of slow disk access speed, and CPUs use three levels of cache to alleviate the speed difference between registers and memory. They all face the same problem (speed mismatch) and the same concept, so the optimization measures for cache performance in storage hierarchy design by computer pioneers are also applicable to performance optimization of software-level caches. -**如何求职为导向学习呢?** 简答来说就是:根据招聘要求整理一份目标岗位的技能清单,然后按照技能清单去学习和提升。 +**How to learn with job orientation?** In simple terms: organize a skill checklist for your target position based on job requirements, and then learn and improve according to that checklist. -1. 你首先搞清楚自己要找什么工作 -2. 然后根据招聘岗位的要求梳理一份技能清单 -3. 根据技能清单写好最终的简历 -4. 最后再按照简历的要求去学习和提升。 +1. First, clarify what job you are looking for. +1. Then, organize a skill checklist based on the job requirements. +1. Write a final resume based on the skill checklist. +1. Finally, learn and improve according to the requirements of the resume. -这其实也是 **以终为始** 思想的运用。 +This is also an application of the **begin with the end in mind** philosophy. -**何为以终为始?** 简单来说,以终为始就是我们可以站在结果来考虑问题,从结果出发,根据结果来确定自己要做的事情。 +**What does begin with the end in mind mean?** In simple terms, it means we can consider problems from the perspective of the result, starting from the result to determine what we need to do. -你会发现,其实几乎任何领域都可以用到 **以终为始** 的思想。 +You will find that the **begin with the end in mind** philosophy can be applied in almost any field. -## 了解投递简历的黄金时间 +## Understand the Golden Time for Submitting Resumes -面试之前,你肯定是先要搞清楚春招和秋招的具体时间的。 +Before the interview, you must first clarify the specific times for spring recruitment and autumn recruitment. -正所谓金三银四,金九银十,错过了这个时间,很多公司都没有 HC 了。 +As the saying goes, "golden March and silver April, golden September and silver October," if you miss this time, many companies will no longer have hiring quotas. -**秋招一般 7 月份就开始了,大概一直持续到 9 月底。** +**Autumn recruitment generally starts in July and lasts until the end of September.** -**春招一般 3 月份就开始了,大概一直持续到 4 月底。** +**Spring recruitment generally starts in March and lasts until the end of April.** -很多公司(尤其大厂)到了 9 月中旬(秋招)/3 月中旬(春招),很可能就会没有 HC 了。面试的话一般都是至少是 3 轮起步,一些大厂比如阿里、字节可能会有 5 轮面试。**面试失败话的不要紧,某一面表现差的话也不要紧,调整好心态。又不是单一选择对吧?你能投这么多企业呢! 调整心态。** 今年面试的话,因为疫情原因,有些公司还是可能会还是集中在线上进行面试。然后,还是因为疫情的影响,可能会比往年更难找工作(对大厂影响较小)。 - -## 知道如何获取招聘信息 - -下面是常见的获取招聘信息的渠道: - -- **目标企业的官网/公众号**:最及时最权威的获取招聘信息的途径。 -- **招聘网站**:[BOSS 直聘](https://www.zhipin.com/)、[智联招聘](https://www.zhaopin.com/)、[拉勾招聘](https://www.lagou.com/)……。 -- **牛客网**:每年秋招/春招,都会有大批量的公司会到牛客网发布招聘信息,并且还会有大量的公司员工来到这里发内推的帖子。地址: 。 -- **超级简历**:超级简历目前整合了各大企业的校园招聘入口,地址: -- **认识的朋友**:如果你有认识的朋友在目标企业工作的话,你也可以找他们了解招聘信息,并且可以让他们帮你内推。 -- **宣讲会**:宣讲会也是一个不错的途径,不过,好的企业通常只会去比较好的学校,可以留意一下意向公司的宣讲会安排或者直接去到一所比较好的学校参加宣讲会。像我当时校招就去参加了几场宣讲会。不过,我是在荆州上学,那边没什么比较好的学校,一般没有公司去开宣讲会。所以,我当时是直接跑到武汉来了,参加了武汉理工大学以及华中科技大学的几场宣讲会。总体感觉还是很不错的! -- **其他**:校园就业信息网、学校论坛、班级 or 年级 QQ 群。 - -校招的话,建议以官网为准,有宣讲会的话更好。社招的话,可以多留意一下各大招聘网站比如 BOSS 直聘、拉勾上的职位信息。 - -不论校招和社招,如果能找到比较靠谱的内推机会的话,获得面试的机会的概率还是非常大的。而且,你可以让内推你的人定向地给你一些建议。找内推的方式有很多,首选比较熟悉的朋友、同学,还可以留意技术交流社区和公众号上的内推信息。 - -一般是只能投递一个岗位,不过,也有极少数投递不同部门两个岗位的情况,这个应该不会有影响,但你的前一次面试情况可能会被记录,也就是说就算你投递成功两个岗位,第一个岗位面试失败的话,对第二个岗位也会有影响,很可能直接就被 pass。 - -## 多花点时间完善简历 - -一定一定一定要重视简历啊!朋友们!至少要花 2~3 天时间来专门完善自己的简历。 - -最近看了很多份简历,满意的很少,我简单拿出一份来说分析一下(欢迎在评论区补充)。 - -**1.个人介绍没太多实用的信息。** - -![](https://oss.javaguide.cn/github/javaguide/interview-preparation/format,png.png) - -技术博客、GitHub 以及在校获奖经历的话,能写就尽量写在这里。 你可以参考下面 👇 的模板进行修改: - -![](https://oss.javaguide.cn/github/javaguide/interview-preparation/format,png-20230309224235808.png) - -**2.项目经历过于简单,完全没有质量可言** - -![](https://oss.javaguide.cn/github/javaguide/interview-preparation/format,png-20230309224240305.png) - -每一个项目经历真的就一两句话可以描述了么?还是自己不想写?还是说不是自己做的,不敢多写。 - -如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑: - -1. 你对项目整体设计的一个感受(面试官可能会让你画系统的架构图) -2. 你在这个项目中你负责了什么、做了什么、担任了什么角色。 -3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用。 -4. 你在这个项目中是否解决过什么问题?怎么解决的?收获了什么? -5. 你的项目用到了哪些技术?这些技术你吃透了没有?举个例子,你的项目经历使用了 Seata 来做分布式事务,那 Seata 相关的问题你要提前准备一下吧,比如说 Seata 支持哪些配置中心、Seata 的事务分组是怎么做的、Seata 支持哪些事务模式,怎么选择? -6. 你在这个项目中犯过的错误,最后是怎么弥补的? - -**3.计算机二级这个证书对于计算机专业完全不用写了,没有含金量的。** - -![](https://oss.javaguide.cn/github/javaguide/interview-preparation/format,png-20230309224247261.png) - -**4.技能介绍问题太大。** - -![](https://oss.javaguide.cn/github/javaguide/interview-preparation/93da1096fb02e19071ba13b4f6a7471c.png) - -- 技术名词最好规范大小写比较好,比如 java->Java ,spring boot -> Spring Boot 。这个虽然有些面试官不会介意,但是很多面试官都会在意这个细节的。 -- 技能介绍太杂,没有亮点。不需要全才,某个领域做得好就行了! -- 对 Java 后台开发的部分技能比如 Spring Boot 的熟悉度仅仅为了解,无法满足企业的要求。 - -详细的程序员简历编写指南请参考:[程序员简历到底该怎么写?](https://javaguide.cn/interview-preparation/resume-guide.html)。 - -## 岗位匹配度很重要 - -校招通常会对你的项目经历的研究方向比较宽容,即使你的项目经历和对应公司的具体业务没有关系,影响其实也并不大。 - -社招的话就不一样了,毕竟公司是要招聘可以直接来干活的人,你有相关的经验,公司会比较省事。社招通常会比较重视你的过往工作经历以及项目经历,HR 在筛选简历的时候会根据这两方面信息来判断你是否满足他们的招聘要求。就比如说你投递电商公司,而你之前的并没有和电商相关的工作经历以及项目经历,那 HR 在筛简历的时候很可能会直接把你 Pass 掉。 - -不过,这个也并不绝对,也有一些公司在招聘的时候更看重的是你的过往经历,较少地关注岗位匹配度,优秀公司的工作经历以及有亮点的项目经验都是加分项。这类公司相信你既然在某个领域(比如电商、支付)已经做的不错了,那应该也可以在另外一个领域(比如流媒体平台、社交软件)很快成为专家。这个领域指的不是技术领域,更多的是业务方向。横跨技术领域(比如后端转算法、后端转大数据)找工作,你又没有相关的经验,几乎是没办法找到的。即使找到了,也大概率会面临 HR 压薪资的问题。 - -## 提前准备技术面试 - -面试之前一定要提前准备一下常见的面试题也就是八股文: - -- 自己面试中可能涉及哪些知识点、那些知识点是重点。 -- 面试中哪些问题会被经常问到、面试中自己该如何回答。(强烈不推荐死记硬背,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!) - -Java 后端面试复习的重点请看这篇文章:[Java 面试重点总结(重要)](https://javaguide.cn/interview-preparation/key-points-of-interview.html)。 - -不同类型的公司对于技能的要求侧重点是不同的比如腾讯、字节可能更重视计算机基础比如网络、操作系统这方面的内容。阿里、美团这种可能更重视你的项目经历、实战能力。 - -一定不要抱着一种思想,觉得八股文或者基础问题的考查意义不大。如果你抱着这种思想复习的话,那效果可能不会太好。实际上,个人认为还是很有意义的,八股文或者基础性的知识在日常开发中也会需要经常用到。例如,线程池这块的拒绝策略、核心参数配置什么的,如果你不了解,实际项目中使用线程池可能就用的不是很明白,容易出现问题。而且,其实这种基础性的问题是最容易准备的,像各种底层原理、系统设计、场景题以及深挖你的项目这类才是最难的! - -八股文资料首推我的 [《Java 面试指北》](https://t.zsxq.com/11rZ6D7Wk) (配合 JavaGuide 使用,会根据每一年的面试情况对内容进行更新完善)和 [JavaGuide](https://javaguide.cn/) 。里面不仅仅是原创八股文,还有很多对实际开发有帮助的干货。除了我的资料之外,你还可以去网上找一些其他的优质的文章、视频来看。 - -![《Java 面试指北》内容概览](https://oss.javaguide.cn/javamianshizhibei/javamianshizhibei-content-overview.png) - -## 提前准备手撕算法 - -很明显,国内现在的校招面试开始越来越重视算法了,尤其是像字节跳动、腾讯这类大公司。绝大部分公司的校招笔试是有算法题的,如果 AC 率比较低的话,基本就挂掉了。 - -社招的话,算法面试同样会有。不过,面试官可能会更看重你的工程能力,你的项目经历。如果你的其他方面都很优秀,但是算法很菜的话,不一定会挂掉。不过,还是建议刷下算法题,避免让其成为自己在面试中的短板。 - -社招往往是在技术面试的最后,面试官给你一个算法题目让你做。 - -关于如何准备算法面试[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的面试准备篇有详细介绍到。 - -![《Java 面试指北》面试准备篇](https://oss.javaguide.cn/javamianshizhibei/preparation-for-interview.png) - -## 提前准备自我介绍 - -自我介绍一般是你和面试官的第一次面对面正式交流,换位思考一下,假如你是面试官的话,你想听到被你面试的人如何介绍自己呢?一定不是客套地说说自己喜欢编程、平时花了很多时间来学习、自己的兴趣爱好是打球吧? - -我觉得一个好的自我介绍至少应该包含这几点要素: - -- 用简洁的话说清楚自己主要的技术栈于擅长的领域; -- 把重点放在自己在行的地方以及自己的优势之处; -- 重点突出自己的能力比如自己的定位的 bug 的能力特别厉害; - -简单来说就是用简洁的语言突出自己的亮点,也就是推销自己嘛! - -- 如果你去过大公司实习,那对应的实习经历就是你的亮点。 -- 如果你参加过技术竞赛,那竞赛经历就是你的亮点。 -- 如果你大学就接触过企业级项目的开发,实战经验比较多,那这些项目经历就是你的亮点。 -- …… - -从社招和校招两个角度来举例子吧!我下面的两个例子仅供参考,自我介绍并不需要死记硬背,记住要说的要点,面试的时候根据公司的情况临场发挥也是没问题的。另外,网上一般建议的是准备好两份自我介绍:一份对 hr 说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节和项目经验。 - -**社招:** - -> 面试官,您好!我叫独秀儿。我目前有 1 年半的工作经验,熟练使用 Spring、MyBatis 等框架、了解 Java 底层原理比如 JVM 调优并且有着丰富的分布式开发经验。离开上一家公司是因为我想在技术上得到更多的锻炼。在上一个公司我参与了一个分布式电子交易系统的开发,负责搭建了整个项目的基础架构并且通过分库分表解决了原始数据库以及一些相关表过于庞大的问题,目前这个网站最高支持 10 万人同时访问。工作之余,我利用自己的业余时间写了一个简单的 RPC 框架,这个框架用到了 Netty 进行网络通信, 目前我已经将这个项目开源,在 GitHub 上收获了 2k 的 Star! 说到业余爱好的话,我比较喜欢通过博客整理分享自己所学知识,现在已经是多个博客平台的认证作者。 生活中我是一个比较积极乐观的人,一般会通过运动打球的方式来放松。我一直都非常想加入贵公司,我觉得贵公司的文化和技术氛围我都非常喜欢,期待能与你共事! - -**校招:** - -> 面试官,您好!我叫秀儿。大学时间我主要利用课外时间学习了 Java 以及 Spring、MyBatis 等框架 。在校期间参与过一个考试系统的开发,这个系统的主要用了 Spring、MyBatis 和 shiro 这三种框架。我在其中主要担任后端开发,主要负责了权限管理功能模块的搭建。另外,我在大学的时候参加过一次软件编程大赛,我和我的团队做的在线订餐系统成功获得了第二名的成绩。我还利用自己的业余时间写了一个简单的 RPC 框架,这个框架用到了 Netty 进行网络通信, 目前我已经将这个项目开源,在 GitHub 上收获了 2k 的 Star! 说到业余爱好的话,我比较喜欢通过博客整理分享自己所学知识,现在已经是多个博客平台的认证作者。 生活中我是一个比较积极乐观的人,一般会通过运动打球的方式来放松。我一直都非常想加入贵公司,我觉得贵公司的文化和技术氛围我都非常喜欢,期待能与你共事! - -## 减少抱怨 - -就像现在的技术面试一样,大家都说内卷了,抱怨现在的面试真特么难。然而,单纯抱怨有用么?你对其他求职者说:“大家都不要刷 Leetcode 了啊!都不要再准备高并发、高可用的面试题了啊!现在都这么卷了!” - -会有人听你的么?**你不准备面试,但是其他人会准备面试啊!那你是不是傻啊?还是真的厉害到不需要准备面试呢?** - -因此,准备 Java 面试的第一步,我们一定要尽量减少抱怨。抱怨的声音多了之后,会十分影响自己,会让自己变得十分焦虑。 - -## 面试之后及时复盘 - -如果失败,不要灰心;如果通过,切勿狂喜。面试和工作实际上是两回事,可能很多面试未通过的人,工作能力比你强的多,反之亦然。 - -面试就像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油! - -## 总结 - -这篇文章内容有点多,如果这篇文章只能让你记住 7 句话,那请记住下面这 7 句: - -1. 一定要提前准备面试!技术面试不同于编程,编程厉害不代表技术面试就一定能过。 -2. 一定不要对面试抱有侥幸心理。打铁还需自身硬!千万不要觉得自己看几篇面经,看几篇面试题解析就能通过面试了。一定要静下心来深入学习!尤其是目标是大厂的同学,那更要深挖原理! -3. 建议大学生尽可能早一点以求职为导向来学习的。这样更有针对性,并且可以大概率减少自己处在迷茫的时间,很大程度上还可以让自己少走很多弯路。 但是,不要把“以求职为导向学习”理解为“我就不用学课堂上那些计算机基础课程了”! -4. 一定不要抱着一种思想,觉得八股文或者基础问题的考查意义不大。如果你抱着这种思想复习的话,那效果可能不会太好。实际上,个人认为还是很有意义的,八股文或者基础性的知识在日常开发中也会需要经常用到。例如,线程池这块的拒绝策略、核心参数配置什么的,如果你不了解,实际项目中使用线程池可能就用的不是很明白,容易出现问题。 -5. 手撕算法是当下技术面试的标配,尽早准备! -6. 岗位匹配度很重要。校招通常会对你的项目经历的研究方向比较宽容,即使你的项目经历和对应公司的具体业务没有关系,影响其实也并不大。社招的话就不一样了,毕竟公司是要招聘可以直接来干活的人,你有相关的经验,公司会比较省事。 - -7. 面试之后及时复盘。面试就像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油! +Many companies (especially large ones) may not have hiring quotas by mid-September (autumn recruitment) or mid-March (spring recruitment). Interviews usually start with at least three rounds, and some large companies like Alibaba and ByteDance may have five rounds of interviews. **If you fail an interview, don't worry; if you perform poorly in one round, it's also okay. Adjust your mindset. It's not a single choice, right? You can apply to so many companies! Adjust your mindset.** This year, due to the pandemic, some companies may still conduct interviews online. Additionally, due to the pandemic's impact, it diff --git a/docs/java/basis/bigdecimal.md b/docs/java/basis/bigdecimal.md index a1e966c9905..c1f2041856d 100644 --- a/docs/java/basis/bigdecimal.md +++ b/docs/java/basis/bigdecimal.md @@ -1,15 +1,15 @@ --- -title: BigDecimal 详解 +title: BigDecimal Explained category: Java tag: - - Java基础 + - Java Basics --- -《阿里巴巴 Java 开发手册》中提到:“为了避免精度丢失,可以使用 `BigDecimal` 来进行浮点数的运算”。 +The "Alibaba Java Development Manual" mentions: "To avoid precision loss, you can use `BigDecimal` for floating-point arithmetic." -浮点数的运算竟然还会有精度丢失的风险吗?确实会! +Is there really a risk of precision loss when performing floating-point arithmetic? Indeed, there is! -示例代码: +Example code: ```java float a = 2.0f - 1.9f; @@ -19,38 +19,39 @@ System.out.println(b);// 0.099999905 System.out.println(a == b);// false ``` -**为什么浮点数 `float` 或 `double` 运算的时候会有精度丢失的风险呢?** +**Why is there a risk of precision loss when performing operations with floating-point numbers `float` or `double`?** -这个和计算机保存小数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么十进制小数没有办法用二进制精确表示。 +This is largely related to the way computers store decimal numbers. We know that computers use binary representation, and they have a limited width when representing a number. Infinite repeating decimals can only be truncated when stored, which leads to loss of precision. This explains why decimal fractions cannot be accurately represented in binary. -就比如说十进制下的 0.2 就没办法精确转换成二进制小数: +For example, the decimal 0.2 cannot be precisely converted to its binary representation: ```java -// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止, -// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。 +// The process of converting 0.2 to binary involves continuously multiplying by 2 +// until there's no decimal part left. The integer parts obtained during this +// calculation form the binary result. 0.2 * 2 = 0.4 -> 0 0.4 * 2 = 0.8 -> 0 0.8 * 2 = 1.6 -> 1 0.6 * 2 = 1.2 -> 1 -0.2 * 2 = 0.4 -> 0(发生循环) +0.2 * 2 = 0.4 -> 0 (looping occurs) ... ``` -关于浮点数的更多内容,建议看一下[计算机系统基础(四)浮点数](http://kaito-kidd.com/2018/08/08/computer-system-float-point/)这篇文章。 +For more information about floating-point numbers, it is recommended to read the article [Computer System Basics (Four) Floating-point Numbers](http://kaito-kidd.com/2018/08/08/computer-system-float-point/). -## BigDecimal 介绍 +## Introduction to BigDecimal -`BigDecimal` 可以实现对小数的运算,不会造成精度丢失。 +`BigDecimal` can be used to perform arithmetic on decimal numbers without causing precision loss. -通常情况下,大部分需要小数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 `BigDecimal` 来做的。 +Typically, most business scenarios that require precise decimal calculations (such as those involving money) utilize `BigDecimal`. -《阿里巴巴 Java 开发手册》中提到:**浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals 来判断。** +The "Alibaba Java Development Manual" states: **When comparing equality between floating-point numbers, primitive data types should not use `==`, and wrapper data types should not use `equals()`.** ![](https://oss.javaguide.cn/javaguide/image-20211213101646884.png) -具体原因我们在上面已经详细介绍了,这里就不多提了。 +We have already discussed the specific reasons in detail above, so we won't elaborate further. -想要解决浮点数运算精度丢失这个问题,可以直接使用 `BigDecimal` 来定义小数的值,然后再进行小数的运算操作即可。 +To solve the problem of precision loss during floating-point arithmetic, you can define decimal values using `BigDecimal` and then perform arithmetic operations. ```java BigDecimal a = new BigDecimal("1.0"); @@ -63,19 +64,19 @@ BigDecimal y = b.subtract(c); System.out.println(x.compareTo(y));// 0 ``` -## BigDecimal 常见方法 +## Common Methods of BigDecimal -### 创建 +### Creation -我们在使用 `BigDecimal` 时,为了防止精度丢失,推荐使用它的`BigDecimal(String val)`构造方法或者 `BigDecimal.valueOf(double val)` 静态方法来创建对象。 +When using `BigDecimal`, to prevent precision loss, it is recommended to use its `BigDecimal(String val)` constructor or `BigDecimal.valueOf(double val)` static method to create instances. -《阿里巴巴 Java 开发手册》对这部分内容也有提到,如下图所示。 +The "Alibaba Java Development Manual" mentions this part as shown in the image below. ![](https://oss.javaguide.cn/javaguide/image-20211213102222601.png) -### 加减乘除 +### Addition, Subtraction, Multiplication, Division -`add` 方法用于将两个 `BigDecimal` 对象相加,`subtract` 方法用于将两个 `BigDecimal` 对象相减。`multiply` 方法用于将两个 `BigDecimal` 对象相乘,`divide` 方法用于将两个 `BigDecimal` 对象相除。 +The `add` method is used to add two `BigDecimal` objects, and the `subtract` method is used to subtract them. The `multiply` method is for multiplying two `BigDecimal` objects, while `divide` is for dividing them. ```java BigDecimal a = new BigDecimal("1.0"); @@ -83,11 +84,11 @@ BigDecimal b = new BigDecimal("0.9"); System.out.println(a.add(b));// 1.9 System.out.println(a.subtract(b));// 0.1 System.out.println(a.multiply(b));// 0.90 -System.out.println(a.divide(b));// 无法除尽,抛出 ArithmeticException 异常 +System.out.println(a.divide(b));// Throws ArithmeticException due to non-integer result System.out.println(a.divide(b, 2, RoundingMode.HALF_UP));// 1.11 ``` -这里需要注意的是,在我们使用 `divide` 方法的时候尽量使用 3 个参数版本,并且`RoundingMode` 不要选择 `UNNECESSARY`,否则很可能会遇到 `ArithmeticException`(无法除尽出现无限循环小数的时候),其中 `scale` 表示要保留几位小数,`roundingMode` 代表保留规则。 +It's important to note that when using the `divide` method, you should preferably use the version with three parameters, and do not choose `RoundingMode.UNNECESSARY`, otherwise you may encounter `ArithmeticException` (in cases where division results in infinite repeating decimals). The `scale` represents the number of decimal places to retain, and `roundingMode` indicates the rounding rule. ```java public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) { @@ -95,7 +96,7 @@ public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMod } ``` -保留规则非常多,这里列举几种: +There are many rounding rules; here are a few: ```java public enum RoundingMode { @@ -118,9 +119,9 @@ public enum RoundingMode { } ``` -### 大小比较 +### Size Comparison -`a.compareTo(b)` : 返回 -1 表示 `a` 小于 `b`,0 表示 `a` 等于 `b` , 1 表示 `a` 大于 `b`。 +`a.compareTo(b)`: returns -1 if `a` is less than `b`, 0 if `a` equals `b`, and 1 if `a` is greater than `b`. ```java BigDecimal a = new BigDecimal("1.0"); @@ -128,61 +129,61 @@ BigDecimal b = new BigDecimal("0.9"); System.out.println(a.compareTo(b));// 1 ``` -### 保留几位小数 +### Setting Decimal Places -通过 `setScale`方法设置保留几位小数以及保留规则。保留规则有挺多种,不需要记,IDEA 会提示。 +Use the `setScale` method to specify how many decimal places to retain and the rounding rule. There are many rounding rules, and you don't need to memorize them as the IDE will offer hints. ```java BigDecimal m = new BigDecimal("1.255433"); -BigDecimal n = m.setScale(3,RoundingMode.HALF_DOWN); +BigDecimal n = m.setScale(3, RoundingMode.HALF_DOWN); System.out.println(n);// 1.255 ``` -## BigDecimal 等值比较问题 +## Equality Comparison Issues with BigDecimal -《阿里巴巴 Java 开发手册》中提到: +The "Alibaba Java Development Manual" points out: ![](https://oss.javaguide.cn/github/javaguide/java/basis/image-20220714161315993.png) -`BigDecimal` 使用 `equals()` 方法进行等值比较出现问题的代码示例: +Example of code where using `equals()` for equality comparison with `BigDecimal` causes issues: ```java BigDecimal a = new BigDecimal("1"); BigDecimal b = new BigDecimal("1.0"); -System.out.println(a.equals(b));//false +System.out.println(a.equals(b));// false ``` -这是因为 `equals()` 方法不仅仅会比较值的大小(value)还会比较精度(scale),而 `compareTo()` 方法比较的时候会忽略精度。 +This is because the `equals()` method compares both the value and the scale, whereas `compareTo()` ignores the scale. -1.0 的 scale 是 1,1 的 scale 是 0,因此 `a.equals(b)` 的结果是 false。 +The scale of 1.0 is 1, and the scale of 1 is 0. Therefore, `a.equals(b)` results in false. ![](https://oss.javaguide.cn/github/javaguide/java/basis/image-20220714164706390.png) -`compareTo()` 方法可以比较两个 `BigDecimal` 的值,如果相等就返回 0,如果第 1 个数比第 2 个数大则返回 1,反之返回-1。 +The `compareTo()` method can be used to compare the values of two `BigDecimal` instances; if they are equal, it returns 0. If the first number is greater than the second, it returns 1, otherwise it returns -1. ```java BigDecimal a = new BigDecimal("1"); BigDecimal b = new BigDecimal("1.0"); -System.out.println(a.compareTo(b));//0 +System.out.println(a.compareTo(b));// 0 ``` -## BigDecimal 工具类分享 +## BigDecimal Utility Class Sharing -网上有一个使用人数比较多的 `BigDecimal` 工具类,提供了多个静态方法来简化 `BigDecimal` 的操作。 +There is a widely used `BigDecimal` utility class available online, which provides several static methods to simplify operations with `BigDecimal`. -我对其进行了简单改进,分享一下源码: +I have made some simple improvements to it, sharing the source code below: ```java import java.math.BigDecimal; import java.math.RoundingMode; /** - * 简化BigDecimal计算的小工具类 + * A utility class to simplify BigDecimal calculations */ public class BigDecimalUtil { /** - * 默认除法运算精度 + * Default precision for division operations */ private static final int DEF_DIV_SCALE = 10; @@ -190,11 +191,11 @@ public class BigDecimalUtil { } /** - * 提供精确的加法运算。 + * Provides precise addition. * - * @param v1 被加数 - * @param v2 加数 - * @return 两个参数的和 + * @param v1 The first addend + * @param v2 The second addend + * @return The sum of the two parameters */ public static double add(double v1, double v2) { BigDecimal b1 = BigDecimal.valueOf(v1); @@ -203,11 +204,11 @@ public class BigDecimalUtil { } /** - * 提供精确的减法运算。 + * Provides precise subtraction. * - * @param v1 被减数 - * @param v2 减数 - * @return 两个参数的差 + * @param v1 The minuend + * @param v2 The subtrahend + * @return The difference of the two parameters */ public static double subtract(double v1, double v2) { BigDecimal b1 = BigDecimal.valueOf(v1); @@ -216,11 +217,11 @@ public class BigDecimalUtil { } /** - * 提供精确的乘法运算。 + * Provides precise multiplication. * - * @param v1 被乘数 - * @param v2 乘数 - * @return 两个参数的积 + * @param v1 The multiplicand + * @param v2 The multiplier + * @return The product of the two parameters */ public static double multiply(double v1, double v2) { BigDecimal b1 = BigDecimal.valueOf(v1); @@ -229,25 +230,25 @@ public class BigDecimalUtil { } /** - * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 - * 小数点以后10位,以后的数字四舍五入。 + * Provides (relatively) precise division. When non-integer results occur, the result + * is kept to 10 decimal places, with rounding. * - * @param v1 被除数 - * @param v2 除数 - * @return 两个参数的商 + * @param v1 The dividend + * @param v2 The divisor + * @return The quotient of the two parameters */ public static double divide(double v1, double v2) { return divide(v1, v2, DEF_DIV_SCALE); } /** - * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 - * 定精度,以后的数字四舍五入。 + * Provides (relatively) precise division. When non-integer results occur, the scale + * parameter specifies the precision, with rounding. * - * @param v1 被除数 - * @param v2 除数 - * @param scale 表示表示需要精确到小数点以后几位。 - * @return 两个参数的商 + * @param v1 The dividend + * @param v2 The divisor + * @param scale Specifies how many decimal places to keep. + * @return The quotient of the two parameters */ public static double divide(double v1, double v2, int scale) { if (scale < 0) { @@ -260,11 +261,11 @@ public class BigDecimalUtil { } /** - * 提供精确的小数位四舍五入处理。 + * Provides precise rounding of decimal places. * - * @param v 需要四舍五入的数字 - * @param scale 小数点后保留几位 - * @return 四舍五入后的结果 + * @param v The number to be rounded + * @param scale The number of decimal places to keep + * @return The rounded result */ public static double round(double v, int scale) { if (scale < 0) { @@ -277,10 +278,10 @@ public class BigDecimalUtil { } /** - * 提供精确的类型转换(Float) + * Provides precise type conversion to Float. * - * @param v 需要被转换的数字 - * @return 返回转换结果 + * @param v The number to be converted + * @return The conversion result */ public static float convertToFloat(double v) { BigDecimal b = new BigDecimal(v); @@ -288,10 +289,10 @@ public class BigDecimalUtil { } /** - * 提供精确的类型转换(Int)不进行四舍五入 + * Provides precise type conversion to Int without rounding. * - * @param v 需要被转换的数字 - * @return 返回转换结果 + * @param v The number to be converted + * @return The conversion result */ public static int convertsToInt(double v) { BigDecimal b = new BigDecimal(v); @@ -299,10 +300,10 @@ public class BigDecimalUtil { } /** - * 提供精确的类型转换(Long) + * Provides precise type conversion to Long. * - * @param v 需要被转换的数字 - * @return 返回转换结果 + * @param v The number to be converted + * @return The conversion result */ public static long convertsToLong(double v) { BigDecimal b = new BigDecimal(v); @@ -310,11 +311,11 @@ public class BigDecimalUtil { } /** - * 返回两个数中大的一个值 + * Returns the larger of two values. * - * @param v1 需要被对比的第一个数 - * @param v2 需要被对比的第二个数 - * @return 返回两个数中大的一个值 + * @param v1 The first number to compare + * @param v2 The second number to compare + * @return The larger of the two values */ public static double returnMax(double v1, double v2) { BigDecimal b1 = new BigDecimal(v1); @@ -323,11 +324,11 @@ public class BigDecimalUtil { } /** - * 返回两个数中小的一个值 + * Returns the smaller of two values. * - * @param v1 需要被对比的第一个数 - * @param v2 需要被对比的第二个数 - * @return 返回两个数中小的一个值 + * @param v1 The first number to compare + * @param v2 The second number to compare + * @return The smaller of the two values */ public static double returnMin(double v1, double v2) { BigDecimal b1 = new BigDecimal(v1); @@ -336,11 +337,12 @@ public class BigDecimalUtil { } /** - * 精确对比两个数字 + * Precisely compares two numbers. * - * @param v1 需要被对比的第一个数 - * @param v2 需要被对比的第二个数 - * @return 如果两个数一样则返回0,如果第一个数比第二个数大则返回1,反之返回-1 + * @param v1 The first number to compare + * @param v2 The second number to compare + * @return If the two numbers are equal, returns 0; if the first number is greater, + * returns 1; otherwise returns -1 */ public static int compareTo(double v1, double v2) { BigDecimal b1 = BigDecimal.valueOf(v1); @@ -351,14 +353,12 @@ public class BigDecimalUtil { } ``` -相关 issue:[建议对保留规则设置为 RoundingMode.HALF_EVEN,即四舍六入五成双,#2129](https://github.com/Snailclimb/JavaGuide/issues/2129) 。 +Related issue: [Propose to set the rounding rule to RoundingMode.HALF_EVEN, that is, round half away from zero, #2129](https://github.com/Snailclimb/JavaGuide/issues/2129). ![RoundingMode.HALF_EVEN](https://oss.javaguide.cn/github/javaguide/java/basis/RoundingMode.HALF_EVEN.png) -## 总结 +## Conclusion -浮点数没有办法用二进制精确表示,因此存在精度丢失的风险。 +Floating-point numbers cannot be accurately represented in binary, hence there is a risk of precision loss. -不过,Java 提供了`BigDecimal` 来操作浮点数。`BigDecimal` 的实现利用到了 `BigInteger` (用来操作大整数), 所不同的是 `BigDecimal` 加入了小数位的概念。 - - +However, Java offers `BigDecimal` for working with floating-point numbers. The implementation of `BigDecimal` utilizes `BigInteger` (for handling large integers), and the key difference is that `BigDecimal` incorporates the concept of decimal places. diff --git a/docs/java/basis/generics-and-wildcards.md b/docs/java/basis/generics-and-wildcards.md index bd9cd4b00bf..7b8630b0fe4 100644 --- a/docs/java/basis/generics-and-wildcards.md +++ b/docs/java/basis/generics-and-wildcards.md @@ -1,17 +1,17 @@ --- -title: 泛型&通配符详解 +title: Generics & Wildcards Explained category: Java tag: - - Java基础 + - Java Basics --- -**泛型&通配符** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)(点击链接即可查看详细介绍以及获取方法)中。 +**Generics & Wildcards** related interview questions are exclusive content for my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) (click the link for detailed introduction and joining methods), and have been organized into [《Java Interview Guide》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) (click the link for detailed introduction and access methods). -[《Java 面试指北》](hhttps://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的部分内容展示如下,你可以将其看作是 [JavaGuide](https://javaguide.cn/#/) 的补充完善,两者可以配合使用。 +Some content from [《Java Interview Guide》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) is displayed below. You can consider it a supplement to [JavaGuide](https://javaguide.cn/#/), and the two can be used together. ![](https://oss.javaguide.cn/xingqiu/image-20220304102536445.png) -[《Java 面试指北》](hhttps://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)只是星球内部众多资料中的一个,星球还有很多其他优质资料比如[专属专栏](https://javaguide.cn/zhuanlan/)、Java 编程视频、PDF 资料。 +[《Java Interview Guide》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) is just one of the many resources available within the planet. There are many other high-quality materials, such as [exclusive columns](https://javaguide.cn/zhuanlan/), Java programming videos, and PDF resources. ![](https://oss.javaguide.cn/xingqiu/image-20220211231206733.png) diff --git a/docs/java/basis/java-basic-questions-01.md b/docs/java/basis/java-basic-questions-01.md index 47ef386848c..a19fd77a961 100644 --- a/docs/java/basis/java-basic-questions-01.md +++ b/docs/java/basis/java-basic-questions-01.md @@ -1,1119 +1,53 @@ --- -title: Java基础常见面试题总结(上) +title: Summary of Common Java Interview Questions (Part 1) category: Java tag: - - Java基础 + - Java Basics head: - - - meta - - name: keywords - content: Java特点,Java SE,Java EE,Java ME,Java虚拟机,JVM,JDK,JRE,字节码,Java编译与解释,AOT编译,云原生,AOT与JIT对比,GraalVM,Oracle JDK与OpenJDK区别,OpenJDK,LTS支持,多线程支持,静态变量,成员变量与局部变量区别,包装类型缓存机制,自动装箱与拆箱,浮点数精度丢失,BigDecimal,Java基本数据类型,Java标识符与关键字,移位运算符,Java注释,静态方法与实例方法,方法重载与重写,可变长参数,Java性能优化 - - - meta - - name: description - content: 全网质量最高的Java基础常见知识点和面试题总结,希望对你有帮助! + - - meta + - name: keywords + content: Java features, Java SE, Java EE, Java ME, Java Virtual Machine, JVM, JDK, JRE, bytecode, Java compilation and interpretation, AOT compilation, cloud-native, AOT vs JIT comparison, GraalVM, differences between Oracle JDK and OpenJDK, OpenJDK, LTS support, multithreading support, static variables, differences between member variables and local variables, wrapper type caching mechanism, autoboxing and unboxing, floating-point precision loss, BigDecimal, Java basic data types, Java identifiers and keywords, shift operators, Java comments, static methods and instance methods, method overloading and overriding, variable-length parameters, Java performance optimization + - - meta + - name: description + content: The highest quality summary of common Java knowledge points and interview questions on the internet, hoping to be helpful to you! --- -## 基础概念与常识 +## Basic Concepts and Common Knowledge -### Java 语言有哪些特点? +### What are the features of the Java language? -1. 简单易学(语法简单,上手容易); -2. 面向对象(封装,继承,多态); -3. 平台无关性( Java 虚拟机实现平台无关性); -4. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持); -5. 可靠性(具备异常处理和自动内存管理机制); -6. 安全性(Java 语言本身的设计就提供了多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源); -7. 高效性(通过 Just In Time 编译器等技术的优化,Java 语言的运行效率还是非常不错的); -8. 支持网络编程并且很方便; -9. 编译与解释并存; -10. …… +1. Simple and easy to learn (simple syntax, easy to get started); +1. Object-oriented (encapsulation, inheritance, polymorphism); +1. Platform independence (Java Virtual Machine achieves platform independence); +1. Supports multithreading (C++ does not have built-in multithreading mechanisms, so it must call the operating system's multithreading features for multithreaded program design, while Java provides multithreading support); +1. Reliability (has exception handling and automatic memory management mechanisms); +1. Security (the design of the Java language itself provides multiple security protection mechanisms such as access modifiers and restrictions on direct access to operating system resources); +1. Efficiency (through optimizations such as Just In Time compiler, the running efficiency of Java is quite good); +1. Supports network programming and is very convenient; +1. Compilation and interpretation coexist; +1. …… -> **🐛 修正(参见:[issue#544](https://github.com/Snailclimb/JavaGuide/issues/544))**:C++11 开始(2011 年的时候),C++就引入了多线程库,在 windows、linux、macos 都可以使用`std::thread`和`std::async`来创建线程。参考链接: +> **🐛 Correction (see: [issue#544](https://github.com/Snailclimb/JavaGuide/issues/544))**: Since C++11 (in 2011), C++ introduced a multithreading library, and `std::thread` and `std::async` can be used to create threads on Windows, Linux, and macOS. Reference link: -🌈 拓展一下: +🌈 A little expansion: -“Write Once, Run Anywhere(一次编写,随处运行)”这句宣传口号,真心经典,流传了好多年!以至于,直到今天,依然有很多人觉得跨平台是 Java 语言最大的优势。实际上,跨平台已经不是 Java 最大的卖点了,各种 JDK 新特性也不是。目前市面上虚拟化技术已经非常成熟,比如你通过 Docker 就很容易实现跨平台了。在我看来,Java 强大的生态才是! +The slogan "Write Once, Run Anywhere" is truly classic and has been passed down for many years! So much so that even today, many people still believe that cross-platform capability is the biggest advantage of the Java language. In fact, cross-platform capability is no longer the biggest selling point of Java, nor are the various new features of JDK. Currently, virtualization technology is very mature; for example, you can easily achieve cross-platform with Docker. In my opinion, the powerful ecosystem of Java is what truly stands out! ### Java SE vs Java EE -- Java SE(Java Platform,Standard Edition): Java 平台标准版,Java 编程语言的基础,它包含了支持 Java 应用程序开发和运行的核心类库以及虚拟机等核心组件。Java SE 可以用于构建桌面应用程序或简单的服务器应用程序。 -- Java EE(Java Platform, Enterprise Edition ):Java 平台企业版,建立在 Java SE 的基础上,包含了支持企业级应用程序开发和部署的标准和规范(比如 Servlet、JSP、EJB、JDBC、JPA、JTA、JavaMail、JMS)。 Java EE 可以用于构建分布式、可移植、健壮、可伸缩和安全的服务端 Java 应用程序,例如 Web 应用程序。 +- Java SE (Java Platform, Standard Edition): The standard edition of the Java platform, which is the foundation of the Java programming language. It includes core class libraries and core components such as the virtual machine that support Java application development and execution. Java SE can be used to build desktop applications or simple server applications. +- Java EE (Java Platform, Enterprise Edition): The enterprise edition of the Java platform, built on the foundation of Java SE, includes standards and specifications that support the development and deployment of enterprise-level applications (such as Servlet, JSP, EJB, JDBC, JPA, JTA, JavaMail, JMS). Java EE can be used to build distributed, portable, robust, scalable, and secure server-side Java applications, such as web applications. -简单来说,Java SE 是 Java 的基础版本,Java EE 是 Java 的高级版本。Java SE 更适合开发桌面应用程序或简单的服务器应用程序,Java EE 更适合开发复杂的企业级应用程序或 Web 应用程序。 +In simple terms, Java SE is the basic version of Java, while Java EE is the advanced version of Java. Java SE is more suitable for developing desktop applications or simple server applications, while Java EE is more suitable for developing complex enterprise-level applications or web applications. -除了 Java SE 和 Java EE,还有一个 Java ME(Java Platform,Micro Edition)。Java ME 是 Java 的微型版本,主要用于开发嵌入式消费电子设备的应用程序,例如手机、PDA、机顶盒、冰箱、空调等。Java ME 无需重点关注,知道有这个东西就好了,现在已经用不上了。 +In addition to Java SE and Java EE, there is also Java ME (Java Platform, Micro Edition). Java ME is a micro version of Java, mainly used for developing applications for embedded consumer electronic devices, such as mobile phones, PDAs, set-top boxes, refrigerators, air conditioners, etc. Java ME does not need to be focused on; just knowing it exists is enough, as it is no longer in use. ### JVM vs JDK vs JRE #### JVM -Java 虚拟机(Java Virtual Machine, JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。 +The Java Virtual Machine (JVM) is a virtual machine that runs Java bytecode. JVM has specific implementations for different systems (Windows, Linux, macOS), with the goal of producing the same result using the same bytecode. The bytecode and the different system implementations of JVM are the key to Java's "compile once, run anywhere" capability. -如下图所示,不同编程语言(Java、Groovy、Kotlin、JRuby、Clojure ...)通过各自的编译器编译成 `.class` 文件,并最终通过 JVM 在不同平台(Windows、Mac、Linux)上运行。 - -![运行在 Java 虚拟机之上的编程语言](https://oss.javaguide.cn/github/javaguide/java/basis/java-virtual-machine-program-language-os.png) - -**JVM 并不是只有一种!只要满足 JVM 规范,每个公司、组织或者个人都可以开发自己的专属 JVM。** 也就是说我们平时接触到的 HotSpot VM 仅仅是是 JVM 规范的一种实现而已。 - -除了我们平时最常用的 HotSpot VM 外,还有 J9 VM、Zing VM、JRockit VM 等 JVM 。维基百科上就有常见 JVM 的对比:[Comparison of Java virtual machines](https://en.wikipedia.org/wiki/Comparison_of_Java_virtual_machines) ,感兴趣的可以去看看。并且,你可以在 [Java SE Specifications](https://docs.oracle.com/javase/specs/index.html) 上找到各个版本的 JDK 对应的 JVM 规范。 - -![](https://oss.javaguide.cn/github/javaguide/java/basis/JavaSeSpecifications.jpg) - -#### JDK 和 JRE - -JDK(Java Development Kit)是一个功能齐全的 Java 开发工具包,供开发者使用,用于创建和编译 Java 程序。它包含了 JRE(Java Runtime Environment),以及编译器 javac 和其他工具,如 javadoc(文档生成器)、jdb(调试器)、jconsole(监控工具)、javap(反编译工具)等。 - -JRE 是运行已编译 Java 程序所需的环境,主要包含以下两个部分: - -1. **JVM** : 也就是我们上面提到的 Java 虚拟机。 -2. **Java 基础类库(Class Library)**:一组标准的类库,提供常用的功能和 API(如 I/O 操作、网络通信、数据结构等)。 - -简单来说,JRE 只包含运行 Java 程序所需的环境和类库,而 JDK 不仅包含 JRE,还包括用于开发和调试 Java 程序的工具。 - -如果需要编写、编译 Java 程序或使用 Java API 文档,就需要安装 JDK。某些需要 Java 特性的应用程序(如 JSP 转换为 Servlet 或使用反射)也可能需要 JDK 来编译和运行 Java 代码。因此,即使不进行 Java 开发工作,有时也可能需要安装 JDK。 - -下图清晰展示了 JDK、JRE 和 JVM 的关系。 - -![jdk-include-jre](https://oss.javaguide.cn/github/javaguide/java/basis/jdk-include-jre.png) - -不过,从 JDK 9 开始,就不需要区分 JDK 和 JRE 的关系了,取而代之的是模块系统(JDK 被重新组织成 94 个模块)+ [jlink](http://openjdk.java.net/jeps/282) 工具 (随 Java 9 一起发布的新命令行工具,用于生成自定义 Java 运行时映像,该映像仅包含给定应用程序所需的模块) 。并且,从 JDK 11 开始,Oracle 不再提供单独的 JRE 下载。 - -在 [Java 9 新特性概览](https://javaguide.cn/java/new-features/java9.html)这篇文章中,我在介绍模块化系统的时候提到: - -> 在引入了模块系统之后,JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlink 工具,创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小。 - -也就是说,可以用 jlink 根据自己的需求,创建一个更小的 runtime(运行时),而不是不管什么应用,都是同样的 JRE。 - -定制的、模块化的 Java 运行时映像有助于简化 Java 应用的部署和节省内存并增强安全性和可维护性。这对于满足现代应用程序架构的需求,如虚拟化、容器化、微服务和云原生开发,是非常重要的。 - -### 什么是字节码?采用字节码的好处是什么? - -在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的(不过,和 C、 C++,Rust,Go 等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 - -**Java 程序从源代码到运行的过程如下图所示**: - -![Java程序转变为机器代码的过程](https://oss.javaguide.cn/github/javaguide/java/basis/java-code-to-machine-code.png) - -我们需要格外注意的是 `.class->机器码` 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 **JIT(Just in Time Compilation)** 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 **Java 是编译与解释共存的语言** 。 - -> 🌈 拓展阅读: -> -> - [基本功 | Java 即时编译器原理解析及实践 - 美团技术团队](https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html) -> - [基于静态编译构建微服务应用 - 阿里巴巴中间件](https://mp.weixin.qq.com/s/4haTyXUmh8m-dBQaEzwDJw) - -![Java程序转变为机器代码的过程](https://oss.javaguide.cn/github/javaguide/java/basis/java-code-to-machine-code-with-jit.png) - -> HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。 - -JDK、JRE、JVM、JIT 这四者的关系如下图所示。 - -![JDK、JRE、JVM、JIT 这四者的关系](https://oss.javaguide.cn/github/javaguide/java/basis/jdk-jre-jvm-jit.png) - -下面这张图是 JVM 的大致结构模型。 - -![JVM 的大致结构模型](https://oss.javaguide.cn/github/javaguide/java/basis/jvm-rough-structure-model.png) - -### 为什么说 Java 语言“编译与解释并存”? - -其实这个问题我们讲字节码的时候已经提到过,因为比较重要,所以我们这里再提一下。 - -我们可以将高级编程语言按照程序的执行方式分为两种: - -- **编译型**:[编译型语言](https://zh.wikipedia.org/wiki/%E7%B7%A8%E8%AD%AF%E8%AA%9E%E8%A8%80) 会通过[编译器](https://zh.wikipedia.org/wiki/%E7%B7%A8%E8%AD%AF%E5%99%A8)将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。 -- **解释型**:[解释型语言](https://zh.wikipedia.org/wiki/%E7%9B%B4%E8%AD%AF%E8%AA%9E%E8%A8%80)会通过[解释器](https://zh.wikipedia.org/wiki/直譯器)一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。 - -![编译型语言和解释型语言](https://oss.javaguide.cn/github/javaguide/java/basis/compiled-and-interpreted-languages.png) - -根据维基百科介绍: - -> 为了改善解释语言的效率而发展出的[即时编译](https://zh.wikipedia.org/wiki/即時編譯)技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成[字节码](https://zh.wikipedia.org/wiki/字节码)。到执行期时,再将字节码直译,之后执行。[Java](https://zh.wikipedia.org/wiki/Java)与[LLVM](https://zh.wikipedia.org/wiki/LLVM)是这种技术的代表产物。 -> -> 相关阅读:[基本功 | Java 即时编译器原理解析及实践](https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html) - -**为什么说 Java 语言“编译与解释并存”?** - -这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(`.class` 文件),这种字节码必须由 Java 解释器来解释执行。 - -### AOT 有什么优点?为什么不全部使用 AOT 呢? - -JDK 9 引入了一种新的编译模式 **AOT(Ahead of Time Compilation)** 。和 JIT 不同的是,这种编译模式会在程序被执行前就将其编译成机器码,属于静态编译(C、 C++,Rust,Go 等语言就是静态编译)。AOT 避免了 JIT 预热等各方面的开销,可以提高 Java 程序的启动速度,避免预热时间长。并且,AOT 还能减少内存占用和增强 Java 程序的安全性(AOT 编译后的代码不容易被反编译和修改),特别适合云原生场景。 - -**JIT 与 AOT 两者的关键指标对比**: - -JIT vs AOT - -可以看出,AOT 的主要优势在于启动时间、内存占用和打包体积。JIT 的主要优势在于具备更高的极限处理能力,可以降低请求的最大延迟。 - -提到 AOT 就不得不提 [GraalVM](https://www.graalvm.org/) 了!GraalVM 是一种高性能的 JDK(完整的 JDK 发行版本),它可以运行 Java 和其他 JVM 语言,以及 JavaScript、Python 等非 JVM 语言。 GraalVM 不仅能提供 AOT 编译,还能提供 JIT 编译。感兴趣的同学,可以去看看 GraalVM 的官方文档:。如果觉得官方文档看着比较难理解的话,也可以找一些文章来看看,比如: - -- [基于静态编译构建微服务应用](https://mp.weixin.qq.com/s/4haTyXUmh8m-dBQaEzwDJw) -- [走向 Native 化:Spring&Dubbo AOT 技术示例与原理讲解](https://cn.dubbo.apache.org/zh-cn/blog/2023/06/28/%e8%b5%b0%e5%90%91-native-%e5%8c%96springdubbo-aot-%e6%8a%80%e6%9c%af%e7%a4%ba%e4%be%8b%e4%b8%8e%e5%8e%9f%e7%90%86%e8%ae%b2%e8%a7%a3/) - -**既然 AOT 这么多优点,那为什么不全部使用这种编译方式呢?** - -我们前面也对比过 JIT 与 AOT,两者各有优点,只能说 AOT 更适合当下的云原生场景,对微服务架构的支持也比较友好。除此之外,AOT 编译无法支持 Java 的一些动态特性,如反射、动态代理、动态加载、JNI(Java Native Interface)等。然而,很多框架和库(如 Spring、CGLIB)都用到了这些特性。如果只使用 AOT 编译,那就没办法使用这些框架和库了,或者说需要针对性地去做适配和优化。举个例子,CGLIB 动态代理使用的是 ASM 技术,而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码文件也就是 `.class` 文件,如果全部使用 AOT 提前编译,也就不能使用 ASM 技术了。为了支持类似的动态特性,所以选择使用 JIT 即时编译器。 - -### Oracle JDK vs OpenJDK - -可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么 Oracle JDK 和 OpenJDK 之间是否存在重大差异?下面我通过收集到的一些资料,为你解答这个被很多人忽视的问题。 - -首先,2006 年 SUN 公司将 Java 开源,也就有了 OpenJDK。2009 年 Oracle 收购了 Sun 公司,于是自己在 OpenJDK 的基础上搞了一个 Oracle JDK。Oracle JDK 是不开源的,并且刚开始的几个版本(Java8 ~ Java11)还会相比于 OpenJDK 添加一些特有的功能和工具。 - -其次,对于 Java 7 而言,OpenJDK 和 Oracle JDK 是十分接近的。 Oracle JDK 是基于 OpenJDK 7 构建的,只添加了一些小功能,由 Oracle 工程师参与维护。 - -下面这段话摘自 Oracle 官方在 2012 年发表的一个博客: - -> 问:OpenJDK 存储库中的源代码与用于构建 Oracle JDK 的代码之间有什么区别? -> -> 答:非常接近 - 我们的 Oracle JDK 版本构建过程基于 OpenJDK 7 构建,只添加了几个部分,例如部署代码,其中包括 Oracle 的 Java 插件和 Java WebStart 的实现,以及一些闭源的第三方组件,如图形光栅化器,一些开源的第三方组件,如 Rhino,以及一些零碎的东西,如附加文档或第三方字体。展望未来,我们的目的是开源 Oracle JDK 的所有部分,除了我们考虑商业功能的部分。 - -最后,简单总结一下 Oracle JDK 和 OpenJDK 的区别: - -1. **是否开源**:OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是基于 OpenJDK 实现的,并不是完全开源的(个人观点:众所周知,JDK 原来是 SUN 公司开发的,后来 SUN 公司又卖给了 Oracle 公司,Oracle 公司以 Oracle 数据库而著名,而 Oracle 数据库又是闭源的,这个时候 Oracle 公司就不想完全开源了,但是原来的 SUN 公司又把 JDK 给开源了,如果这个时候 Oracle 收购回来之后就把他给闭源,必然会引起很多 Java 开发者的不满,导致大家对 Java 失去信心,那 Oracle 公司收购回来不就把 Java 烂在手里了吗!然后,Oracle 公司就想了个骚操作,这样吧,我把一部分核心代码开源出来给你们玩,并且我要和你们自己搞的 JDK 区分下,你们叫 OpenJDK,我叫 Oracle JDK,我发布我的,你们继续玩你们的,要是你们搞出来什么好玩的东西,我后续发布 Oracle JDK 也会拿来用一下,一举两得!)OpenJDK 开源项目:[https://github.com/openjdk/jdk](https://github.com/openjdk/jdk) 。 -2. **是否免费**:Oracle JDK 会提供免费版本,但一般有时间限制。JDK17 之后的版本可以免费分发和商用,但是仅有 3 年时间,3 年后无法免费商用。不过,JDK8u221 之前只要不升级可以无限期免费。OpenJDK 是完全免费的。 -3. **功能性**:Oracle JDK 在 OpenJDK 的基础上添加了一些特有的功能和工具,比如 Java Flight Recorder(JFR,一种监控工具)、Java Mission Control(JMC,一种监控工具)等工具。不过,在 Java 11 之后,OracleJDK 和 OpenJDK 的功能基本一致,之前 OracleJDK 中的私有组件大多数也已经被捐赠给开源组织。 -4. **稳定性**:OpenJDK 不提供 LTS 服务,而 OracleJDK 大概每三年都会推出一个 LTS 版进行长期支持。不过,很多公司都基于 OpenJDK 提供了对应的和 OracleJDK 周期相同的 LTS 版。因此,两者稳定性其实也是差不多的。 -5. **协议**:Oracle JDK 使用 BCL/OTN 协议获得许可,而 OpenJDK 根据 GPL v2 许可获得许可。 - -> 既然 Oracle JDK 这么好,那为什么还要有 OpenJDK? -> -> 答: -> -> 1. OpenJDK 是开源的,开源意味着你可以对它根据你自己的需要进行修改、优化,比如 Alibaba 基于 OpenJDK 开发了 Dragonwell8:[https://github.com/alibaba/dragonwell8](https://github.com/alibaba/dragonwell8) -> 2. OpenJDK 是商业免费的(这也是为什么通过 yum 包管理器上默认安装的 JDK 是 OpenJDK 而不是 Oracle JDK)。虽然 Oracle JDK 也是商业免费(比如 JDK 8),但并不是所有版本都是免费的。 -> 3. OpenJDK 更新频率更快。Oracle JDK 一般是每 6 个月发布一个新版本,而 OpenJDK 一般是每 3 个月发布一个新版本。(现在你知道为啥 Oracle JDK 更稳定了吧,先在 OpenJDK 试试水,把大部分问题都解决掉了才在 Oracle JDK 上发布) -> -> 基于以上这些原因,OpenJDK 还是有存在的必要的! - -![oracle jdk release cadence](https://oss.javaguide.cn/github/javaguide/java/basis/oracle-jdk-release-cadence.jpg) - -**Oracle JDK 和 OpenJDK 如何选择?** - -建议选择 OpenJDK 或者基于 OpenJDK 的发行版,比如 AWS 的 Amazon Corretto,阿里巴巴的 Alibaba Dragonwell。 - -🌈 拓展一下: - -- BCL 协议(Oracle Binary Code License Agreement):可以使用 JDK(支持商用),但是不能进行修改。 -- OTN 协议(Oracle Technology Network License Agreement):11 及之后新发布的 JDK 用的都是这个协议,可以自己私下用,但是商用需要付费。 - -### Java 和 C++ 的区别? - -我知道很多人没学过 C++,但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀!没办法!!!就算没学过 C++,也要记下来。 - -虽然,Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方: - -- Java 不提供指针来直接访问内存,程序内存更加安全 -- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。 -- Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。 -- C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。 -- …… - -## 基本语法 - -### 注释有哪几种形式? - -Java 中的注释有三种: - -1. **单行注释**:通常用于解释方法内某单行代码的作用。 - -2. **多行注释**:通常用于解释一段代码的作用。 - -3. **文档注释**:通常用于生成 Java 开发文档。 - -用的比较多的还是单行注释和文档注释,多行注释在实际开发中使用的相对较少。 - -![](https://oss.javaguide.cn/github/javaguide/java/basis/image-20220714112336911.png) - -在我们编写代码的时候,如果代码量比较少,我们自己或者团队其他成员还可以很轻易地看懂代码,但是当项目结构一旦复杂起来,我们就需要用到注释了。注释并不会执行(编译器在编译代码之前会把代码中的所有注释抹掉,字节码中不保留注释),是我们程序员写给自己看的,注释是你的代码说明书,能够帮助看代码的人快速地理清代码之间的逻辑关系。因此,在写程序的时候随手加上注释是一个非常好的习惯。 - -《Clean Code》这本书明确指出: - -> **代码的注释不是越详细越好。实际上好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。** -> -> **若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。** -> -> 举个例子: -> -> 去掉下面复杂的注释,只需要创建一个与注释所言同一事物的函数即可 -> -> ```java -> // check to see if the employee is eligible for full benefits -> if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)) -> ``` -> -> 应替换为 -> -> ```java -> if (employee.isEligibleForFullBenefits()) -> ``` - -### 标识符和关键字的区别是什么? - -在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了 **标识符** 。简单来说, **标识符就是一个名字** 。 - -有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这些特殊的标识符就是 **关键字** 。简单来说,**关键字是被赋予特殊含义的标识符** 。比如,在我们的日常生活中,如果我们想要开一家店,则要给这个店起一个名字,起的这个“名字”就叫标识符。但是我们店的名字不能叫“警察局”,因为“警察局”这个名字已经被赋予了特殊的含义,而“警察局”就是我们日常生活中的关键字。 - -### Java 语言关键字有哪些? - -| 分类 | 关键字 | | | | | | | -| :------------------- | -------- | ---------- | -------- | ------------ | ---------- | --------- | ------ | -| 访问控制 | private | protected | public | | | | | -| 类,方法和变量修饰符 | abstract | class | extends | final | implements | interface | native | -| | new | static | strictfp | synchronized | transient | volatile | enum | -| 程序控制 | break | continue | return | do | while | if | else | -| | for | instanceof | switch | case | default | assert | | -| 错误处理 | try | catch | throw | throws | finally | | | -| 包相关 | import | package | | | | | | -| 基本类型 | boolean | byte | char | double | float | int | long | -| | short | | | | | | | -| 变量引用 | super | this | void | | | | | -| 保留字 | goto | const | | | | | | - -> Tips:所有的关键字都是小写的,在 IDE 中会以特殊颜色显示。 -> -> `default` 这个关键字很特殊,既属于程序控制,也属于类,方法和变量修饰符,还属于访问控制。 -> -> - 在程序控制中,当在 `switch` 中匹配不到任何情况时,可以使用 `default` 来编写默认匹配的情况。 -> - 在类,方法和变量修饰符中,从 JDK8 开始引入了默认方法,可以使用 `default` 关键字来定义一个方法的默认实现。 -> - 在访问控制中,如果一个方法前没有任何修饰符,则默认会有一个修饰符 `default`,但是这个修饰符加上了就会报错。 - -⚠️ 注意:虽然 `true`, `false`, 和 `null` 看起来像关键字但实际上他们是字面值,同时你也不可以作为标识符来使用。 - -官方文档:[https://docs.oracle.com/javase/tutorial/java/nutsandbolts/\_keywords.html](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html) - -### 自增自减运算符 - -在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1。Java 提供了自增运算符 (`++`) 和自减运算符 (`--`) 来简化这种操作。 - -`++` 和 `--` 运算符可以放在变量之前,也可以放在变量之后: - -- **前缀形式**(例如 `++a` 或 `--a`):先自增/自减变量的值,然后再使用该变量,例如,`b = ++a` 先将 `a` 增加 1,然后把增加后的值赋给 `b`。 -- **后缀形式**(例如 `a++` 或 `a--`):先使用变量的当前值,然后再自增/自减变量的值。例如,`b = a++` 先将 `a` 的当前值赋给 `b`,然后再将 `a` 增加 1。 - -为了方便记忆,可以使用下面的口诀:**符号在前就先加/减,符号在后就后加/减**。 - -下面来看一个考察自增自减运算符的高频笔试题:执行下面的代码后,`a` 、`b` 、 `c` 、`d`和`e`的值是? - -```java -int a = 9; -int b = a++; -int c = ++a; -int d = c--; -int e = --d; -``` - -答案:`a = 11` 、`b = 9` 、 `c = 10` 、 `d = 10` 、 `e = 10`。 - -### 移位运算符 - -移位运算符是最基本的运算符之一,几乎每种编程语言都包含这一运算符。移位操作中,被操作的数据被视为二进制数,移位就是将其向左或向右移动若干位的运算。 - -移位运算符在各种框架以及 JDK 自身的源码中使用还是挺广泛的,`HashMap`(JDK1.8) 中的 `hash` 方法的源码就用到了移位运算符: - -```java -static final int hash(Object key) { - int h; - // key.hashCode():返回散列值也就是hashcode - // ^:按位异或 - // >>>:无符号右移,忽略符号位,空位都以0补齐 - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - } - -``` - -**使用移位运算符的主要原因**: - -1. **高效**:移位运算符直接对应于处理器的移位指令。现代处理器具有专门的硬件指令来执行这些移位操作,这些指令通常在一个时钟周期内完成。相比之下,乘法和除法等算术运算在硬件层面上需要更多的时钟周期来完成。 -2. **节省内存**:通过移位操作,可以使用一个整数(如 `int` 或 `long`)来存储多个布尔值或标志位,从而节省内存。 - -移位运算符最常用于快速乘以或除以 2 的幂次方。除此之外,它还在以下方面发挥着重要作用: - -- **位字段管理**:例如存储和操作多个布尔值。 -- **哈希算法和加密解密**:通过移位和与、或等操作来混淆数据。 -- **数据压缩**:例如霍夫曼编码通过移位运算符可以快速处理和操作二进制数据,以生成紧凑的压缩格式。 -- **数据校验**:例如 CRC(循环冗余校验)通过移位和多项式除法生成和校验数据完整性。。 -- **内存对齐**:通过移位操作,可以轻松计算和调整数据的对齐地址。 - -掌握最基本的移位运算符知识还是很有必要的,这不光可以帮助我们在代码中使用,还可以帮助我们理解源码中涉及到移位运算符的代码。 - -Java 中有三种移位运算符: - -- `<<` :左移运算符,向左移若干位,高位丢弃,低位补零。`x << n`,相当于 x 乘以 2 的 n 次方(不溢出的情况下)。 -- `>>` :带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。`x >> n`,相当于 x 除以 2 的 n 次方。 -- `>>>` :无符号右移,忽略符号位,空位都以 0 补齐。 - -虽然移位运算本质上可以分为左移和右移,但在实际应用中,右移操作需要考虑符号位的处理方式。 - -由于 `double`,`float` 在二进制中的表现比较特殊,因此不能来进行移位操作。 - -移位操作符实际上支持的类型只有`int`和`long`,编译器在对`short`、`byte`、`char`类型进行移位前,都会将其转换为`int`类型再操作。 - -**如果移位的位数超过数值所占有的位数会怎样?** - -当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)后再进行左移/右移操作。也就是说左移/右移 32 位相当于不进行移位操作(32%32=0),左移/右移 42 位相当于左移/右移 10 位(42%32=10)。当 long 类型进行左移/右移操作时,由于 long 对应的二进制是 64 位,因此求余操作的基数也变成了 64。 - -也就是说:`x<<42`等同于`x<<10`,`x>>42`等同于`x>>10`,`x >>>42`等同于`x >>> 10`。 - -**左移运算符代码示例**: - -```java -int i = -1; -System.out.println("初始数据:" + i); -System.out.println("初始数据对应的二进制字符串:" + Integer.toBinaryString(i)); -i <<= 10; -System.out.println("左移 10 位后的数据 " + i); -System.out.println("左移 10 位后的数据对应的二进制字符 " + Integer.toBinaryString(i)); -``` - -输出: - -```plain -初始数据:-1 -初始数据对应的二进制字符串:11111111111111111111111111111111 -左移 10 位后的数据 -1024 -左移 10 位后的数据对应的二进制字符 11111111111111111111110000000000 -``` - -由于左移位数大于等于 32 位操作时,会先求余(%)后再进行左移操作,所以下面的代码左移 42 位相当于左移 10 位(42%32=10),输出结果和前面的代码一样。 - -```java -int i = -1; -System.out.println("初始数据:" + i); -System.out.println("初始数据对应的二进制字符串:" + Integer.toBinaryString(i)); -i <<= 42; -System.out.println("左移 10 位后的数据 " + i); -System.out.println("左移 10 位后的数据对应的二进制字符 " + Integer.toBinaryString(i)); -``` - -右移运算符使用类似,篇幅问题,这里就不做演示了。 - -### continue、break 和 return 的区别是什么? - -在循环结构中,当循环条件不满足或者循环次数达到要求时,循环会正常结束。但是,有时候可能需要在循环的过程中,当发生了某种条件之后 ,提前终止循环,这就需要用到下面几个关键词: - -1. `continue`:指跳出当前的这一次循环,继续下一次循环。 -2. `break`:指跳出整个循环体,继续执行循环下面的语句。 - -`return` 用于跳出所在方法,结束该方法的运行。return 一般有两种用法: - -1. `return;`:直接使用 return 结束方法执行,用于没有返回值函数的方法 -2. `return value;`:return 一个特定值,用于有返回值函数的方法 - -思考一下:下列语句的运行结果是什么? - -```java -public static void main(String[] args) { - boolean flag = false; - for (int i = 0; i <= 3; i++) { - if (i == 0) { - System.out.println("0"); - } else if (i == 1) { - System.out.println("1"); - continue; - } else if (i == 2) { - System.out.println("2"); - flag = true; - } else if (i == 3) { - System.out.println("3"); - break; - } else if (i == 4) { - System.out.println("4"); - } - System.out.println("xixi"); - } - if (flag) { - System.out.println("haha"); - return; - } - System.out.println("heihei"); -} -``` - -运行结果: - -```plain -0 -xixi -1 -2 -xixi -3 -haha -``` - -## 基本数据类型 - -### Java 中的几种基本数据类型了解么? - -Java 中有 8 种基本数据类型,分别为: - -- 6 种数字类型: - - 4 种整数型:`byte`、`short`、`int`、`long` - - 2 种浮点型:`float`、`double` -- 1 种字符类型:`char` -- 1 种布尔型:`boolean`。 - -这 8 种基本数据类型的默认值以及所占空间的大小如下: - -| 基本类型 | 位数 | 字节 | 默认值 | 取值范围 | -| :-------- | :--- | :--- | :------ | -------------------------------------------------------------- | -| `byte` | 8 | 1 | 0 | -128 ~ 127 | -| `short` | 16 | 2 | 0 | -32768(-2^15) ~ 32767(2^15 - 1) | -| `int` | 32 | 4 | 0 | -2147483648 ~ 2147483647 | -| `long` | 64 | 8 | 0L | -9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1) | -| `char` | 16 | 2 | 'u0000' | 0 ~ 65535(2^16 - 1) | -| `float` | 32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 | -| `double` | 64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 | -| `boolean` | 1 | | false | true、false | - -可以看到,像 `byte`、`short`、`int`、`long`能表示的最大正数都减 1 了。这是为什么呢?这是因为在二进制补码表示法中,最高位是用来表示符号的(0 表示正数,1 表示负数),其余位表示数值部分。所以,如果我们要表示最大的正数,我们需要把除了最高位之外的所有位都设为 1。如果我们再加 1,就会导致溢出,变成一个负数。 - -对于 `boolean`,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。 - -另外,Java 的每种基本类型所占存储空间的大小不会像其他大多数语言那样随机器硬件架构的变化而变化。这种所占存储空间大小的不变性是 Java 程序比用其他大多数语言编写的程序更具可移植性的原因之一(《Java 编程思想》2.2 节有提到)。 - -**注意:** - -1. Java 里使用 `long` 类型的数据一定要在数值后面加上 **L**,否则将作为整型解析。 -2. Java 里使用 `float` 类型的数据一定要在数值后面加上 **f 或 F**,否则将无法通过编译。 -3. `char a = 'h'`char :单引号,`String a = "hello"` :双引号。 - -这八种基本类型都有对应的包装类分别为:`Byte`、`Short`、`Integer`、`Long`、`Float`、`Double`、`Character`、`Boolean` 。 - -### 基本类型和包装类型的区别? - -- **用途**:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。 -- **存储方式**:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 `static` 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。 -- **占用空间**:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。 -- **默认值**:成员变量包装类型不赋值就是 `null` ,而基本类型有默认值且不是 `null`。 -- **比较方式**:对于基本数据类型来说,`==` 比较的是值。对于包装数据类型来说,`==` 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 `equals()` 方法。 - -**为什么说是几乎所有对象实例都存在于堆中呢?** 这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存 - -⚠️ 注意:**基本数据类型存放在栈中是一个常见的误区!** 基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆/方法区/元空间中。 - -```java -public class Test { - // 成员变量,存放在堆中 - int a = 10; - // 被 static 修饰的成员变量,JDK 1.7 及之前位于方法区,1.8 后存放于元空间,均不存放于堆中。 - // 变量属于类,不属于对象。 - static int b = 20; - - public void method() { - // 局部变量,存放在栈中 - int c = 30; - static int d = 40; // 编译错误,不能在方法中使用 static 修饰局部变量 - } -} -``` - -### 包装类型的缓存机制了解么? - -Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。 - -`Byte`,`Short`,`Integer`,`Long` 这 4 种包装类默认创建了数值 **[-128,127]** 的相应类型的缓存数据,`Character` 创建了数值在 **[0,127]** 范围的缓存数据,`Boolean` 直接返回 `TRUE` or `FALSE`。 - -对于 `Integer`,可以通过 JVM 参数 `-XX:AutoBoxCacheMax=` 修改缓存上限,但不能修改下限 -128。实际使用时,并不建议设置过大的值,避免浪费内存,甚至是 OOM。 - -对于`Byte`,`Short`,`Long` ,`Character` 没有类似 `-XX:AutoBoxCacheMax` 参数可以修改,因此缓存范围是固定的,无法通过 JVM 参数调整。`Boolean` 则直接返回预定义的 `TRUE` 和 `FALSE` 实例,没有缓存范围的概念。 - -**Integer 缓存源码:** - -```java -public static Integer valueOf(int i) { - if (i >= IntegerCache.low && i <= IntegerCache.high) - return IntegerCache.cache[i + (-IntegerCache.low)]; - return new Integer(i); -} -private static class IntegerCache { - static final int low = -128; - static final int high; - static { - // high value may be configured by property - int h = 127; - } -} -``` - -**`Character` 缓存源码:** - -```java -public static Character valueOf(char c) { - if (c <= 127) { // must cache - return CharacterCache.cache[(int)c]; - } - return new Character(c); -} - -private static class CharacterCache { - private CharacterCache(){} - static final Character cache[] = new Character[127 + 1]; - static { - for (int i = 0; i < cache.length; i++) - cache[i] = new Character((char)i); - } - -} -``` - -**`Boolean` 缓存源码:** - -```java -public static Boolean valueOf(boolean b) { - return (b ? TRUE : FALSE); -} -``` - -如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。 - -两种浮点数类型的包装类 `Float`,`Double` 并没有实现缓存机制。 - -```java -Integer i1 = 33; -Integer i2 = 33; -System.out.println(i1 == i2);// 输出 true - -Float i11 = 333f; -Float i22 = 333f; -System.out.println(i11 == i22);// 输出 false - -Double i3 = 1.2; -Double i4 = 1.2; -System.out.println(i3 == i4);// 输出 false -``` - -下面我们来看一个问题:下面的代码的输出结果是 `true` 还是 `false` 呢? - -```java -Integer i1 = 40; -Integer i2 = new Integer(40); -System.out.println(i1==i2); -``` - -`Integer i1=40` 这一行代码会发生装箱,也就是说这行代码等价于 `Integer i1=Integer.valueOf(40)` 。因此,`i1` 直接使用的是缓存中的对象。而`Integer i2 = new Integer(40)` 会直接创建新的对象。 - -因此,答案是 `false` 。你答对了吗? - -记住:**所有整型包装类对象之间值的比较,全部使用 equals 方法比较**。 - -![](https://oss.javaguide.cn/github/javaguide/up-1ae0425ce8646adfb768b5374951eeb820d.png) - -### 自动装箱与拆箱了解吗?原理是什么? - -**什么是自动拆装箱?** - -- **装箱**:将基本类型用它们对应的引用类型包装起来; -- **拆箱**:将包装类型转换为基本数据类型; - -举例: - -```java -Integer i = 10; //装箱 -int n = i; //拆箱 -``` - -上面这两行代码对应的字节码为: - -```java - L1 - - LINENUMBER 8 L1 - - ALOAD 0 - - BIPUSH 10 - - INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; - - PUTFIELD AutoBoxTest.i : Ljava/lang/Integer; - - L2 - - LINENUMBER 9 L2 - - ALOAD 0 - - ALOAD 0 - - GETFIELD AutoBoxTest.i : Ljava/lang/Integer; - - INVOKEVIRTUAL java/lang/Integer.intValue ()I - - PUTFIELD AutoBoxTest.n : I - - RETURN -``` - -从字节码中,我们发现装箱其实就是调用了 包装类的`valueOf()`方法,拆箱其实就是调用了 `xxxValue()`方法。 - -因此, - -- `Integer i = 10` 等价于 `Integer i = Integer.valueOf(10)` -- `int n = i` 等价于 `int n = i.intValue()`; - -注意:**如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。** - -```java -private static long sum() { - // 应该使用 long 而不是 Long - Long sum = 0L; - for (long i = 0; i <= Integer.MAX_VALUE; i++) - sum += i; - return sum; -} -``` - -### 为什么浮点数运算的时候会有精度丢失的风险? - -浮点数运算精度丢失代码演示: - -```java -float a = 2.0f - 1.9f; -float b = 1.8f - 1.7f; -System.out.printf("%.9f",a);// 0.100000024 -System.out.println(b);// 0.099999905 -System.out.println(a == b);// false -``` - -为什么会出现这个问题呢? - -这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。 - -就比如说十进制下的 0.2 就没办法精确转换成二进制小数: - -```java -// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止, -// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。 -0.2 * 2 = 0.4 -> 0 -0.4 * 2 = 0.8 -> 0 -0.8 * 2 = 1.6 -> 1 -0.6 * 2 = 1.2 -> 1 -0.2 * 2 = 0.4 -> 0(发生循环) -... -``` - -关于浮点数的更多内容,建议看一下[计算机系统基础(四)浮点数](http://kaito-kidd.com/2018/08/08/computer-system-float-point/)这篇文章。 - -### 如何解决浮点数运算的精度丢失问题? - -`BigDecimal` 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 `BigDecimal` 来做的。 - -```java -BigDecimal a = new BigDecimal("1.0"); -BigDecimal b = new BigDecimal("1.00"); -BigDecimal c = new BigDecimal("0.8"); - -BigDecimal x = a.subtract(c); -BigDecimal y = b.subtract(c); - -System.out.println(x); /* 0.2 */ -System.out.println(y); /* 0.20 */ -// 比较内容,不是比较值 -System.out.println(Objects.equals(x, y)); /* false */ -// 比较值相等用相等compareTo,相等返回0 -System.out.println(0 == x.compareTo(y)); /* true */ -``` - -关于 `BigDecimal` 的详细介绍,可以看看我写的这篇文章:[BigDecimal 详解](https://javaguide.cn/java/basis/bigdecimal.html)。 - -### 超过 long 整型的数据应该如何表示? - -基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险。 - -在 Java 中,64 位 long 整型是最大的整数类型。 - -```java -long l = Long.MAX_VALUE; -System.out.println(l + 1); // -9223372036854775808 -System.out.println(l + 1 == Long.MIN_VALUE); // true -``` - -`BigInteger` 内部使用 `int[]` 数组来存储任意大小的整形数据。 - -相对于常规整数类型的运算来说,`BigInteger` 运算的效率会相对较低。 - -## 变量 - -### 成员变量与局部变量的区别? - -- **语法形式**:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 `public`,`private`,`static` 等修饰符所修饰,而局部变量不能被访问控制修饰符及 `static` 所修饰;但是,成员变量和局部变量都能被 `final` 所修饰。 -- **存储方式**:从变量在内存中的存储方式来看,如果成员变量是使用 `static` 修饰的,那么这个成员变量是属于类的,如果没有使用 `static` 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 -- **生存时间**:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。 -- **默认值**:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 `final` 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 - -**为什么成员变量有默认值?** - -1. 先不考虑变量类型,如果没有默认值会怎样?变量存储的是内存地址对应的任意随机值,程序读取该值运行会出现意外。 - -2. 默认值有两种设置方式:手动和自动,根据第一点,没有手动赋值一定要自动赋值。成员变量在运行时可借助反射等方法手动赋值,而局部变量不行。 - -3. 对于编译器(javac)来说,局部变量没赋值很好判断,可以直接报错。而成员变量可能是运行时赋值,无法判断,误报“没默认值”又会影响用户体验,所以采用自动赋默认值。 - -成员变量与局部变量代码示例: - -```java -public class VariableExample { - - // 成员变量 - private String name; - private int age; - - // 方法中的局部变量 - public void method() { - int num1 = 10; // 栈中分配的局部变量 - String str = "Hello, world!"; // 栈中分配的局部变量 - System.out.println(num1); - System.out.println(str); - } - - // 带参数的方法中的局部变量 - public void method2(int num2) { - int sum = num2 + 10; // 栈中分配的局部变量 - System.out.println(sum); - } - - // 构造方法中的局部变量 - public VariableExample(String name, int age) { - this.name = name; // 对成员变量进行赋值 - this.age = age; // 对成员变量进行赋值 - int num3 = 20; // 栈中分配的局部变量 - String str2 = "Hello, " + this.name + "!"; // 栈中分配的局部变量 - System.out.println(num3); - System.out.println(str2); - } -} - -``` - -### 静态变量有什么作用? - -静态变量也就是被 `static` 关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。 - -静态变量是通过类名来访问的,例如`StaticVariableExample.staticVar`(如果被 `private`关键字修饰就无法这样访问了)。 - -```java -public class StaticVariableExample { - // 静态变量 - public static int staticVar = 0; -} -``` - -通常情况下,静态变量会被 `final` 关键字修饰成为常量。 - -```java -public class ConstantVariableExample { - // 常量 - public static final int constantVar = 0; -} -``` - -### 字符型常量和字符串常量的区别? - -- **形式** : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。 -- **含义** : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。 -- **占内存大小**:字符常量只占 2 个字节; 字符串常量占若干个字节。 - -⚠️ 注意 `char` 在 Java 中占两个字节。 - -字符型常量和字符串常量代码示例: - -```java -public class StringExample { - // 字符型常量 - public static final char LETTER_A = 'A'; - - // 字符串常量 - public static final String GREETING_MESSAGE = "Hello, world!"; - public static void main(String[] args) { - System.out.println("字符型常量占用的字节数为:"+Character.BYTES); - System.out.println("字符串常量占用的字节数为:"+GREETING_MESSAGE.getBytes().length); - } -} -``` - -输出: - -```plain -字符型常量占用的字节数为:2 -字符串常量占用的字节数为:13 -``` - -## 方法 - -### 什么是方法的返回值?方法有哪几种类型? - -**方法的返回值** 是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用是接收出结果,使得它可以用于其他的操作! - -我们可以按照方法的返回值和参数类型将方法分为下面这几种: - -**1、无参数无返回值的方法** - -```java -public void f1() { - //...... -} -// 下面这个方法也没有返回值,虽然用到了 return -public void f(int a) { - if (...) { - // 表示结束方法的执行,下方的输出语句不会执行 - return; - } - System.out.println(a); -} -``` - -**2、有参数无返回值的方法** - -```java -public void f2(Parameter 1, ..., Parameter n) { - //...... -} -``` - -**3、有返回值无参数的方法** - -```java -public int f3() { - //...... - return x; -} -``` - -**4、有返回值有参数的方法** - -```java -public int f4(int a, int b) { - return a * b; -} -``` - -### 静态方法为什么不能调用非静态成员? - -这个需要结合 JVM 的相关知识,主要原因如下: - -1. 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。 -2. 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。 - -```java -public class Example { - // 定义一个字符型常量 - public static final char LETTER_A = 'A'; - - // 定义一个字符串常量 - public static final String GREETING_MESSAGE = "Hello, world!"; - - public static void main(String[] args) { - // 输出字符型常量的值 - System.out.println("字符型常量的值为:" + LETTER_A); - - // 输出字符串常量的值 - System.out.println("字符串常量的值为:" + GREETING_MESSAGE); - } -} -``` - -### 静态方法和实例方法有何不同? - -**1、调用方式** - -在外部调用静态方法时,可以使用 `类名.方法名` 的方式,也可以使用 `对象.方法名` 的方式,而实例方法只有后面这种方式。也就是说,**调用静态方法可以无需创建对象** 。 - -不过,需要注意的是一般不建议使用 `对象.方法名` 的方式来调用静态方法。这种方式非常容易造成混淆,静态方法不属于类的某个对象而是属于这个类。 - -因此,一般建议使用 `类名.方法名` 的方式来调用静态方法。 - -```java -public class Person { - public void method() { - //...... - } - - public static void staicMethod(){ - //...... - } - public static void main(String[] args) { - Person person = new Person(); - // 调用实例方法 - person.method(); - // 调用静态方法 - Person.staicMethod() - } -} -``` - -**2、访问类成员是否存在限制** - -静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。 - -### 重载和重写有什么区别? - -> 重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理 -> -> 重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法 - -#### 重载 - -发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 - -《Java 核心技术》这本书是这样介绍重载的: - -> 如果多个方法(比如 `StringBuilder` 的构造方法)有相同的名字、不同的参数, 便产生了重载。 -> -> ```java -> StringBuilder sb = new StringBuilder(); -> StringBuilder sb2 = new StringBuilder("HelloWorld"); -> ``` -> -> 编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。 如果编译器找不到匹配的参数, 就会产生编译时错误, 因为根本不存在匹配, 或者没有一个比其他的更好(这个过程被称为重载解析(overloading resolution))。 -> -> Java 允许重载任何方法, 而不只是构造器方法。 - -综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。 - -#### 重写 - -重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。 - -1. 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。 -2. 如果父类方法访问修饰符为 `private/final/static` 则子类就不能重写该方法,但是被 `static` 修饰的方法能够被再次声明。 -3. 构造方法无法被重写 - -#### 总结 - -综上:**重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。** - -| 区别点 | 重载方法 | 重写方法 | -| :--------- | :------- | :--------------------------------------------------------------- | -| 发生范围 | 同一个类 | 子类 | -| 参数列表 | 必须修改 | 一定不能修改 | -| 返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 | -| 异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; | -| 访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) | -| 发生阶段 | 编译期 | 运行期 | - -**方法的重写要遵循“两同两小一大”**(以下内容摘录自《疯狂 Java 讲义》,[issue#892](https://github.com/Snailclimb/JavaGuide/issues/892) ): - -- “两同”即方法名相同、形参列表相同; -- “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; -- “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。 - -⭐️ 关于 **重写的返回值类型** 这里需要额外多说明一下,上面的表述不太清晰准确:如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。 - -```java -public class Hero { - public String name() { - return "超级英雄"; - } -} -public class SuperMan extends Hero{ - @Override - public String name() { - return "超人"; - } - public Hero hero() { - return new Hero(); - } -} - -public class SuperSuperMan extends SuperMan { - @Override - public String name() { - return "超级超级英雄"; - } - - @Override - public SuperMan hero() { - return new SuperMan(); - } -} -``` - -### 什么是可变长参数? - -从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面这个方法就可以接受 0 个或者多个参数。 - -```java -public static void method1(String... args) { - //...... -} -``` - -另外,可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。 - -```java -public static void method2(String arg1, String... args) { - //...... -} -``` - -**遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?** - -答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。 - -我们通过下面这个例子来证明一下。 - -```java -/** - * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 - * - * @author Guide哥 - * @date 2021/12/13 16:52 - **/ -public class VariableLengthArgument { - - public static void printVariable(String... args) { - for (String s : args) { - System.out.println(s); - } - } - - public static void printVariable(String arg1, String arg2) { - System.out.println(arg1 + arg2); - } - - public static void main(String[] args) { - printVariable("a", "b"); - printVariable("a", "b", "c", "d"); - } -} -``` - -输出: - -```plain -ab -a -b -c -d -``` - -另外,Java 的可变参数编译后实际会被转换成一个数组,我们看编译后生成的 `class`文件就可以看出来了。 - -```java -public class VariableLengthArgument { - - public static void printVariable(String... args) { - String[] var1 = args; - int var2 = args.length; - - for(int var3 = 0; var3 < var2; ++var3) { - String s = var1[var3]; - System.out.println(s); - } - - } - // ...... -} -``` - -## 参考 - -- What is the difference between JDK and JRE?: -- Oracle vs OpenJDK: -- Differences between Oracle JDK and OpenJDK: -- 彻底弄懂 Java 的移位操作符: - - +As shown in the figure below, different programming languages ( diff --git a/docs/java/basis/java-basic-questions-02.md b/docs/java/basis/java-basic-questions-02.md index 9f8739f291d..8ec3fd38c6a 100644 --- a/docs/java/basis/java-basic-questions-02.md +++ b/docs/java/basis/java-basic-questions-02.md @@ -1,892 +1,105 @@ --- -title: Java基础常见面试题总结(中) +title: Summary of Common Java Interview Questions (Part 2) category: Java tag: - - Java基础 + - Java Basics head: - - - meta - - name: keywords - content: 面向对象, 面向过程, OOP, POP, Java对象, 构造方法, 封装, 继承, 多态, 接口, 抽象类, 默认方法, 静态方法, 私有方法, 深拷贝, 浅拷贝, 引用拷贝, Object类, equals, hashCode, ==, 字符串, String, StringBuffer, StringBuilder, 不可变性, 字符串常量池, intern, 字符串拼接, Java基础, 面试题 - - - meta - - name: description - content: 全网质量最高的Java基础常见知识点和面试题总结,希望对你有帮助! + - - meta + - name: keywords + content: Object-Oriented, Procedural, OOP, POP, Java Objects, Constructor, Encapsulation, Inheritance, Polymorphism, Interface, Abstract Class, Default Method, Static Method, Private Method, Deep Copy, Shallow Copy, Reference Copy, Object Class, equals, hashCode, ==, String, String, StringBuffer, StringBuilder, Immutability, String Constant Pool, intern, String Concatenation, Java Basics, Interview Questions + - - meta + - name: description + content: A summary of the highest quality common knowledge points and interview questions about Java basics on the internet, hoping to be helpful to you! --- -## 面向对象基础 +## Basics of Object-Oriented Programming -### 面向对象和面向过程的区别 +### Differences Between Object-Oriented and Procedural Programming -面向过程编程(Procedural-Oriented Programming,POP)和面向对象编程(Object-Oriented Programming,OOP)是两种常见的编程范式,两者的主要区别在于解决问题的方式不同: +Procedural-Oriented Programming (POP) and Object-Oriented Programming (OOP) are two common programming paradigms, and their main difference lies in the way they solve problems: -- **面向过程编程(POP)**:面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。 -- **面向对象编程(OOP)**:面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。 +- **Procedural-Oriented Programming (POP)**: POP breaks down the process of solving a problem into individual methods, solving the problem through the execution of these methods. +- **Object-Oriented Programming (OOP)**: OOP first abstracts objects and then solves problems by executing methods on these objects. -相比较于 POP,OOP 开发的程序一般具有下面这些优点: +Compared to POP, programs developed using OOP generally have the following advantages: -- **易维护**:由于良好的结构和封装性,OOP 程序通常更容易维护。 -- **易复用**:通过继承和多态,OOP 设计使得代码更具复用性,方便扩展功能。 -- **易扩展**:模块化设计使得系统扩展变得更加容易和灵活。 +- **Ease of Maintenance**: Due to good structure and encapsulation, OOP programs are usually easier to maintain. +- **Ease of Reuse**: Through inheritance and polymorphism, OOP design makes code more reusable and facilitates functional expansion. +- **Ease of Extension**: Modular design makes system expansion easier and more flexible. -POP 的编程方式通常更为简单和直接,适合处理一些较简单的任务。 +The programming style of POP is usually simpler and more direct, suitable for handling simpler tasks. -POP 和 OOP 的性能差异主要取决于它们的运行机制,而不仅仅是编程范式本身。因此,简单地比较两者的性能是一个常见的误区(相关 issue : [面向过程:面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431) )。 +The performance difference between POP and OOP mainly depends on their execution mechanisms, not just the programming paradigms themselves. Therefore, simply comparing the performance of the two is a common misconception (related issue: [Is procedural performance higher than object-oriented?](https://github.com/Snailclimb/JavaGuide/issues/431)). -![ POP 和 OOP 性能比较不合适](https://oss.javaguide.cn/github/javaguide/java/basis/pop-vs-oop-performance.png) +![Inappropriate performance comparison between POP and OOP](https://oss.javaguide.cn/github/javaguide/java/basis/pop-vs-oop-performance.png) -在选择编程范式时,性能并不是唯一的考虑因素。代码的可维护性、可扩展性和开发效率同样重要。 +When choosing a programming paradigm, performance is not the only consideration. Code maintainability, scalability, and development efficiency are equally important. -现代编程语言基本都支持多种编程范式,既可以用来进行面向过程编程,也可以进行面向对象编程。 +Modern programming languages generally support multiple programming paradigms, allowing for both procedural and object-oriented programming. -下面是一个求圆的面积和周长的示例,简单分别展示了面向对象和面向过程两种不同的解决方案。 +Here is an example of calculating the area and circumference of a circle, demonstrating both object-oriented and procedural solutions. -**面向对象**: +**Object-Oriented**: ```java public class Circle { - // 定义圆的半径 + // Define the radius of the circle private double radius; - // 构造函数 + // Constructor public Circle(double radius) { this.radius = radius; } - // 计算圆的面积 + // Calculate the area of the circle public double getArea() { return Math.PI * radius * radius; } - // 计算圆的周长 + // Calculate the circumference of the circle public double getPerimeter() { return 2 * Math.PI * radius; } public static void main(String[] args) { - // 创建一个半径为3的圆 + // Create a circle with a radius of 3 Circle circle = new Circle(3.0); - // 输出圆的面积和周长 - System.out.println("圆的面积为:" + circle.getArea()); - System.out.println("圆的周长为:" + circle.getPerimeter()); + // Output the area and circumference of the circle + System.out.println("The area of the circle is: " + circle.getArea()); + System.out.println("The circumference of the circle is: " + circle.getPerimeter()); } } ``` -我们定义了一个 `Circle` 类来表示圆,该类包含了圆的半径属性和计算面积、周长的方法。 +We defined a `Circle` class to represent a circle, which includes the radius property and methods to calculate the area and circumference. -**面向过程**: +**Procedural**: ```java public class Main { public static void main(String[] args) { - // 定义圆的半径 + // Define the radius of the circle double radius = 3.0; - // 计算圆的面积和周长 + // Calculate the area and circumference of the circle double area = Math.PI * radius * radius; double perimeter = 2 * Math.PI * radius; - // 输出圆的面积和周长 - System.out.println("圆的面积为:" + area); - System.out.println("圆的周长为:" + perimeter); + // Output the area and circumference of the circle + System.out.println("The area of the circle is: " + area); + System.out.println("The circumference of the circle is: " + perimeter); } } ``` -我们直接定义了圆的半径,并使用该半径直接计算出圆的面积和周长。 +We directly defined the radius of the circle and used it to calculate the area and circumference. -### 创建一个对象用什么运算符?对象实体与对象引用有何不同? +### What operator is used to create an object? What is the difference between an object entity and an object reference? -new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。 +The `new` operator is used to create an object instance (the object instance is in heap memory), and the object reference points to the object instance (the object reference is stored in stack memory). -- 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球); -- 一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。 - -### 对象的相等和引用相等的区别 - -- 对象的相等一般比较的是内存中存放的内容是否相等。 -- 引用相等一般比较的是他们指向的内存地址是否相等。 - -这里举一个例子: - -```java -String str1 = "hello"; -String str2 = new String("hello"); -String str3 = "hello"; -// 使用 == 比较字符串的引用相等 -System.out.println(str1 == str2); -System.out.println(str1 == str3); -// 使用 equals 方法比较字符串的相等 -System.out.println(str1.equals(str2)); -System.out.println(str1.equals(str3)); - -``` - -输出结果: - -```plain -false -true -true -true -``` - -从上面的代码输出结果可以看出: - -- `str1` 和 `str2` 不相等,而 `str1` 和 `str3` 相等。这是因为 `==` 运算符比较的是字符串的引用是否相等。 -- `str1`、 `str2`、`str3` 三者的内容都相等。这是因为`equals` 方法比较的是字符串的内容,即使这些字符串的对象引用不同,只要它们的内容相等,就认为它们是相等的。 - -### 如果一个类没有声明构造方法,该程序能正确执行吗? - -构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。 - -如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参数的构造方法了。 - -我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。 - -### 构造方法有哪些特点?是否可被 override? - -构造方法具有以下特点: - -- **名称与类名相同**:构造方法的名称必须与类名完全一致。 -- **没有返回值**:构造方法没有返回类型,且不能使用 `void` 声明。 -- **自动执行**:在生成类的对象时,构造方法会自动执行,无需显式调用。 - -构造方法**不能被重写(override)**,但**可以被重载(overload)**。因此,一个类中可以有多个构造方法,这些构造方法可以具有不同的参数列表,以提供不同的对象初始化方式。 - -### 面向对象三大特征 - -#### 封装 - -封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。就好像如果没有空调遥控器,那么我们就无法操控空凋制冷,空调本身就没有意义了(当然现在还有很多其他方法 ,这里只是为了举例子)。 - -```java -public class Student { - private int id;//id属性私有化 - private String name;//name属性私有化 - - //获取id的方法 - public int getId() { - return id; - } - - //设置id的方法 - public void setId(int id) { - this.id = id; - } - - //获取name的方法 - public String getName() { - return name; - } - - //设置name的方法 - public void setName(String name) { - this.name = name; - } -} -``` - -#### 继承 - -不同类型的对象,相互之间经常有一定数量的共同点。例如,小明同学、小红同学、小李同学,都共享学生的特性(班级、学号等)。同时,每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好,小红的性格惹人喜爱;小李的力气比较大。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。 - -**关于继承如下 3 点请记住:** - -1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。 -2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 -3. 子类可以用自己的方式实现父类的方法。(以后介绍)。 - -#### 多态 - -多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。 - -**多态的特点:** - -- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系; -- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定; -- 多态不能调用“只在子类存在但在父类不存在”的方法; -- 如果子类重写了父类的方法,真正执行的是子类重写的方法,如果子类没有重写父类的方法,执行的是父类的方法。 - -### 接口和抽象类有什么共同点和区别? - -#### 接口和抽象类的共同点 - -- **实例化**:接口和抽象类都不能直接实例化,只能被实现(接口)或继承(抽象类)后才能创建具体的对象。 -- **抽象方法**:接口和抽象类都可以包含抽象方法。抽象方法没有方法体,必须在子类或实现类中实现。 - -#### 接口和抽象类的区别 - -- **设计目的**:接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。 -- **继承和实现**:一个类只能继承一个类(包括抽象类),因为 Java 不支持多继承。但一个类可以实现多个接口,一个接口也可以继承多个其他接口。 -- **成员变量**:接口中的成员变量只能是 `public static final` 类型的,不能被修改且必须有初始值。抽象类的成员变量可以有任何修饰符(`private`, `protected`, `public`),可以在子类中被重新定义或赋值。 -- **方法**: - - Java 8 之前,接口中的方法默认是 `public abstract` ,也就是只能有方法声明。自 Java 8 起,可以在接口中定义 `default`(默认) 方法和 `static` (静态)方法。 自 Java 9 起,接口可以包含 `private` 方法。 - - 抽象类可以包含抽象方法和非抽象方法。抽象方法没有方法体,必须在子类中实现。非抽象方法有具体实现,可以直接在抽象类中使用或在子类中重写。 - -在 Java 8 及以上版本中,接口引入了新的方法类型:`default` 方法、`static` 方法和 `private` 方法。这些方法让接口的使用更加灵活。 - -Java 8 引入的`default` 方法用于提供接口方法的默认实现,可以在实现类中被覆盖。这样就可以在不修改实现类的情况下向现有接口添加新功能,从而增强接口的扩展性和向后兼容性。 - -```java -public interface MyInterface { - default void defaultMethod() { - System.out.println("This is a default method."); - } -} -``` - -Java 8 引入的`static` 方法无法在实现类中被覆盖,只能通过接口名直接调用( `MyInterface.staticMethod()`),类似于类中的静态方法。`static` 方法通常用于定义一些通用的、与接口相关的工具方法,一般很少用。 - -```java -public interface MyInterface { - static void staticMethod() { - System.out.println("This is a static method in the interface."); - } -} -``` - -Java 9 允许在接口中使用 `private` 方法。`private`方法可以用于在接口内部共享代码,不对外暴露。 - -```java -public interface MyInterface { - // default 方法 - default void defaultMethod() { - commonMethod(); - } - - // static 方法 - static void staticMethod() { - commonMethod(); - } - - // 私有静态方法,可以被 static 和 default 方法调用 - private static void commonMethod() { - System.out.println("This is a private method used internally."); - } - - // 实例私有方法,只能被 default 方法调用。 - private void instanceCommonMethod() { - System.out.println("This is a private instance method used internally."); - } -} -``` - -### 深拷贝和浅拷贝区别了解吗?什么是引用拷贝? - -关于深拷贝和浅拷贝区别,我这里先给结论: - -- **浅拷贝**:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。 -- **深拷贝**:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。 - -上面的结论没有完全理解的话也没关系,我们来看一个具体的案例! - -#### 浅拷贝 - -浅拷贝的示例代码如下,我们这里实现了 `Cloneable` 接口,并重写了 `clone()` 方法。 - -`clone()` 方法的实现很简单,直接调用的是父类 `Object` 的 `clone()` 方法。 - -```java -public class Address implements Cloneable{ - private String name; - // 省略构造函数、Getter&Setter方法 - @Override - public Address clone() { - try { - return (Address) super.clone(); - } catch (CloneNotSupportedException e) { - throw new AssertionError(); - } - } -} - -public class Person implements Cloneable { - private Address address; - // 省略构造函数、Getter&Setter方法 - @Override - public Person clone() { - try { - Person person = (Person) super.clone(); - return person; - } catch (CloneNotSupportedException e) { - throw new AssertionError(); - } - } -} -``` - -测试: - -```java -Person person1 = new Person(new Address("武汉")); -Person person1Copy = person1.clone(); -// true -System.out.println(person1.getAddress() == person1Copy.getAddress()); -``` - -从输出结构就可以看出, `person1` 的克隆对象和 `person1` 使用的仍然是同一个 `Address` 对象。 - -#### 深拷贝 - -这里我们简单对 `Person` 类的 `clone()` 方法进行修改,连带着要把 `Person` 对象内部的 `Address` 对象一起复制。 - -```java -@Override -public Person clone() { - try { - Person person = (Person) super.clone(); - person.setAddress(person.getAddress().clone()); - return person; - } catch (CloneNotSupportedException e) { - throw new AssertionError(); - } -} -``` - -测试: - -```java -Person person1 = new Person(new Address("武汉")); -Person person1Copy = person1.clone(); -// false -System.out.println(person1.getAddress() == person1Copy.getAddress()); -``` - -从输出结构就可以看出,显然 `person1` 的克隆对象和 `person1` 包含的 `Address` 对象已经是不同的了。 - -**那什么是引用拷贝呢?** 简单来说,引用拷贝就是两个不同的引用指向同一个对象。 - -我专门画了一张图来描述浅拷贝、深拷贝、引用拷贝: - -![shallow&deep-copy](https://oss.javaguide.cn/github/javaguide/java/basis/shallow&deep-copy.png) - -## Object - -### Object 类的常见方法有哪些? - -Object 类是一个特殊的类,是所有类的父类,主要提供了以下 11 个方法: - -```java -/** - * native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。 - */ -public final native Class getClass() -/** - * native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。 - */ -public native int hashCode() -/** - * 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。 - */ -public boolean equals(Object obj) -/** - * native 方法,用于创建并返回当前对象的一份拷贝。 - */ -protected native Object clone() throws CloneNotSupportedException -/** - * 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。 - */ -public String toString() -/** - * native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。 - */ -public final native void notify() -/** - * native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。 - */ -public final native void notifyAll() -/** - * native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。 - */ -public final native void wait(long timeout) throws InterruptedException -/** - * 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。。 - */ -public final void wait(long timeout, int nanos) throws InterruptedException -/** - * 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念 - */ -public final void wait() throws InterruptedException -/** - * 实例被垃圾回收器回收的时候触发的操作 - */ -protected void finalize() throws Throwable { } -``` - -### == 和 equals() 的区别 - -**`==`** 对于基本类型和引用类型的作用效果是不同的: - -- 对于基本数据类型来说,`==` 比较的是值。 -- 对于引用数据类型来说,`==` 比较的是对象的内存地址。 - -> 因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。 - -**`equals()`** 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。`equals()`方法存在于`Object`类中,而`Object`类是所有类的直接或间接父类,因此所有的类都有`equals()`方法。 - -`Object` 类 `equals()` 方法: - -```java -public boolean equals(Object obj) { - return (this == obj); -} -``` - -`equals()` 方法存在两种使用情况: - -- **类没有重写 `equals()`方法**:通过`equals()`比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 `Object`类`equals()`方法。 -- **类重写了 `equals()`方法**:一般我们都重写 `equals()`方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。 - -举个例子(这里只是为了举例。实际上,你按照下面这种写法的话,像 IDEA 这种比较智能的 IDE 都会提示你将 `==` 换成 `equals()` ): - -```java -String a = new String("ab"); // a 为一个引用 -String b = new String("ab"); // b为另一个引用,对象的内容一样 -String aa = "ab"; // 放在常量池中 -String bb = "ab"; // 从常量池中查找 -System.out.println(aa == bb);// true -System.out.println(a == b);// false -System.out.println(a.equals(b));// true -System.out.println(42 == 42.0);// true -``` - -`String` 中的 `equals` 方法是被重写过的,因为 `Object` 的 `equals` 方法是比较的对象的内存地址,而 `String` 的 `equals` 方法比较的是对象的值。 - -当创建 `String` 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 `String` 对象。 - -`String`类`equals()`方法: - -```java -public boolean equals(Object anObject) { - if (this == anObject) { - return true; - } - if (anObject instanceof String) { - String anotherString = (String)anObject; - int n = value.length; - if (n == anotherString.value.length) { - char v1[] = value; - char v2[] = anotherString.value; - int i = 0; - while (n-- != 0) { - if (v1[i] != v2[i]) - return false; - i++; - } - return true; - } - } - return false; -} -``` - -### hashCode() 有什么用? - -`hashCode()` 的作用是获取哈希码(`int` 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。 - -![hashCode() 方法](https://oss.javaguide.cn/github/javaguide/java/basis/java-hashcode-method.png) - -`hashCode()` 定义在 JDK 的 `Object` 类中,这就意味着 Java 中的任何类都包含有 `hashCode()` 函数。另外需要注意的是:`Object` 的 `hashCode()` 方法是本地方法,也就是用 C 语言或 C++ 实现的。 - -> ⚠️ 注意:该方法在 **Oracle OpenJDK8** 中默认是 "使用线程局部状态来实现 Marsaglia's xor-shift 随机数生成", 并不是 "地址" 或者 "地址转换而来", 不同 JDK/VM 可能不同。在 **Oracle OpenJDK8** 中有六种生成方式 (其中第五种是返回地址), 通过添加 VM 参数: -XX:hashCode=4 启用第五种。参考源码: -> -> - (1127 行) -> - (537 行开始) - -```java -public native int hashCode(); -``` - -散列表存储的是键值对(key-value),它的特点是:**能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)** - -### 为什么要有 hashCode? - -我们以“`HashSet` 如何检查重复”为例子来说明为什么要有 `hashCode`? - -下面这段内容摘自我的 Java 启蒙书《Head First Java》: - -> 当你把对象加入 `HashSet` 时,`HashSet` 会先计算对象的 `hashCode` 值来判断对象加入的位置,同时也会与其他已经加入的对象的 `hashCode` 值作比较,如果没有相符的 `hashCode`,`HashSet` 会假设对象没有重复出现。但是如果发现有相同 `hashCode` 值的对象,这时会调用 `equals()` 方法来检查 `hashCode` 相等的对象是否真的相同。如果两者相同,`HashSet` 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 `equals` 的次数,相应就大大提高了执行速度。 - -其实, `hashCode()` 和 `equals()`都是用于比较两个对象是否相等。 - -**那为什么 JDK 还要同时提供这两个方法呢?** - -这是因为在一些容器(比如 `HashMap`、`HashSet`)中,有了 `hashCode()` 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进`HashSet`的过程)! - -我们在前面也提到了添加元素进`HashSet`的过程,如果 `HashSet` 在对比的时候,同样的 `hashCode` 有多个对象,它会继续使用 `equals()` 来判断是否真的相同。也就是说 `hashCode` 帮助我们大大缩小了查找成本。 - -**那为什么不只提供 `hashCode()` 方法呢?** - -这是因为两个对象的`hashCode` 值相等并不代表两个对象就相等。 - -**那为什么两个对象有相同的 `hashCode` 值,它们也不一定是相等的?** - -因为 `hashCode()` 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 `hashCode` )。 - -总结下来就是: - -- 如果两个对象的`hashCode` 值相等,那这两个对象不一定相等(哈希碰撞)。 -- 如果两个对象的`hashCode` 值相等并且`equals()`方法也返回 `true`,我们才认为这两个对象相等。 -- 如果两个对象的`hashCode` 值不相等,我们就可以直接认为这两个对象不相等。 - -相信大家看了我前面对 `hashCode()` 和 `equals()` 的介绍之后,下面这个问题已经难不倒你们了。 - -### 为什么重写 equals() 时必须重写 hashCode() 方法? - -因为两个相等的对象的 `hashCode` 值必须是相等。也就是说如果 `equals` 方法判断两个对象是相等的,那这两个对象的 `hashCode` 值也要相等。 - -如果重写 `equals()` 时没有重写 `hashCode()` 方法的话就可能会导致 `equals` 方法判断是相等的两个对象,`hashCode` 值却不相等。 - -**思考**:重写 `equals()` 时没有重写 `hashCode()` 方法的话,使用 `HashMap` 可能会出现什么问题。 - -**总结**: - -- `equals` 方法判断两个对象是相等的,那这两个对象的 `hashCode` 值也要相等。 -- 两个对象有相同的 `hashCode` 值,他们也不一定是相等的(哈希碰撞)。 - -更多关于 `hashCode()` 和 `equals()` 的内容可以查看:[Java hashCode() 和 equals()的若干问题解答](https://www.cnblogs.com/skywang12345/p/3324958.html) - -## String - -### String、StringBuffer、StringBuilder 的区别? - -**可变性** - -`String` 是不可变的(后面会详细分析原因)。 - -`StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中也是使用字符数组保存字符串,不过没有使用 `final` 和 `private` 关键字修饰,最关键的是这个 `AbstractStringBuilder` 类还提供了很多修改字符串的方法比如 `append` 方法。 - -```java -abstract class AbstractStringBuilder implements Appendable, CharSequence { - char[] value; - public AbstractStringBuilder append(String str) { - if (str == null) - return appendNull(); - int len = str.length(); - ensureCapacityInternal(count + len); - str.getChars(0, len, value, count); - count += len; - return this; - } - //... -} -``` - -**线程安全性** - -`String` 中的对象是不可变的,也就可以理解为常量,线程安全。`AbstractStringBuilder` 是 `StringBuilder` 与 `StringBuffer` 的公共父类,定义了一些字符串的基本操作,如 `expandCapacity`、`append`、`insert`、`indexOf` 等公共方法。`StringBuffer` 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。`StringBuilder` 并没有对方法进行加同步锁,所以是非线程安全的。 - -**性能** - -每次对 `String` 类型进行改变的时候,都会生成一个新的 `String` 对象,然后将指针指向新的 `String` 对象。`StringBuffer` 每次都会对 `StringBuffer` 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 `StringBuilder` 相比使用 `StringBuffer` 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 - -**对于三者使用的总结:** - -- 操作少量的数据: 适用 `String` -- 单线程操作字符串缓冲区下操作大量数据: 适用 `StringBuilder` -- 多线程操作字符串缓冲区下操作大量数据: 适用 `StringBuffer` - -### String 为什么是不可变的? - -`String` 类中使用 `final` 关键字修饰字符数组来保存字符串,~~所以`String` 对象是不可变的。~~ - -```java -public final class String implements java.io.Serializable, Comparable, CharSequence { - private final char value[]; - //... -} -``` - -> 🐛 修正:我们知道被 `final` 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,`final` 关键字修饰的数组保存字符串并不是 `String` 不可变的根本原因,因为这个数组保存的字符串是可变的(`final` 修饰引用类型变量的情况)。 -> -> `String` 真正不可变有下面几点原因: -> -> 1. 保存字符串的数组被 `final` 修饰且为私有的,并且`String` 类没有提供/暴露修改这个字符串的方法。 -> 2. `String` 类被 `final` 修饰导致其不能被继承,进而避免了子类破坏 `String` 不可变。 -> -> 相关阅读:[如何理解 String 类型值的不可变? - 知乎提问](https://www.zhihu.com/question/20618891/answer/114125846) -> -> 补充(来自[issue 675](https://github.com/Snailclimb/JavaGuide/issues/675)):在 Java 9 之后,`String`、`StringBuilder` 与 `StringBuffer` 的实现改用 `byte` 数组存储字符串。 -> -> ```java -> public final class String implements java.io.Serializable,Comparable, CharSequence { -> // @Stable 注解表示变量最多被修改一次,称为“稳定的”。 -> @Stable -> private final byte[] value; -> } -> -> abstract class AbstractStringBuilder implements Appendable, CharSequence { -> byte[] value; -> -> } -> ``` -> -> **Java 9 为何要将 `String` 的底层实现由 `char[]` 改成了 `byte[]` ?** -> -> 新版的 String 其实支持两个编码方案:Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,`byte` 占一个字节(8 位),`char` 占用 2 个字节(16),`byte` 相较 `char` 节省一半的内存空间。 -> -> JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符。 -> -> ![](https://oss.javaguide.cn/github/javaguide/jdk9-string-latin1.png) -> -> 如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符,`byte` 和 `char` 所占用的空间是一样的。 -> -> 这是官方的介绍: 。 - -### 字符串拼接用“+” 还是 StringBuilder? - -Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。 - -```java -String str1 = "he"; -String str2 = "llo"; -String str3 = "world"; -String str4 = str1 + str2 + str3; -``` - -上面的代码对应的字节码如下: - -![](https://oss.javaguide.cn/github/javaguide/java/image-20220422161637929.png) - -可以看出,字符串对象通过“+”的字符串拼接方式,实际上是通过 `StringBuilder` 调用 `append()` 方法实现的,拼接完成之后调用 `toString()` 得到一个 `String` 对象 。 - -不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:**编译器不会创建单个 `StringBuilder` 以复用,会导致创建过多的 `StringBuilder` 对象**。 - -```java -String[] arr = {"he", "llo", "world"}; -String s = ""; -for (int i = 0; i < arr.length; i++) { - s += arr[i]; -} -System.out.println(s); -``` - -`StringBuilder` 对象是在循环内部被创建的,这意味着每循环一次就会创建一个 `StringBuilder` 对象。 - -![](https://oss.javaguide.cn/github/javaguide/java/image-20220422161320823.png) - -如果直接使用 `StringBuilder` 对象进行字符串拼接的话,就不会存在这个问题了。 - -```java -String[] arr = {"he", "llo", "world"}; -StringBuilder s = new StringBuilder(); -for (String value : arr) { - s.append(value); -} -System.out.println(s); -``` - -![](https://oss.javaguide.cn/github/javaguide/java/image-20220422162327415.png) - -如果你使用 IDEA 的话,IDEA 自带的代码检查机制也会提示你修改代码。 - -在 JDK 9 中,字符串相加“+”改为用动态方法 `makeConcatWithConstants()` 来实现,通过提前分配空间从而减少了部分临时对象的创建。然而这种优化主要针对简单的字符串拼接,如: `a+b+c` 。对于循环中的大量拼接操作,仍然会逐个动态分配内存(类似于两个两个 append 的概念),并不如手动使用 StringBuilder 来进行拼接效率高。这个改进是 JDK9 的 [JEP 280](https://openjdk.org/jeps/280) 提出的,关于这部分改进的详细介绍,推荐阅读这篇文章:还在无脑用 [StringBuilder?来重温一下字符串拼接吧](https://juejin.cn/post/7182872058743750715) 以及参考 [issue#2442](https://github.com/Snailclimb/JavaGuide/issues/2442)。 - -### String#equals() 和 Object#equals() 有何区别? - -`String` 中的 `equals` 方法是被重写过的,比较的是 String 字符串的值是否相等。 `Object` 的 `equals` 方法是比较的对象的内存地址。 - -### 字符串常量池的作用了解吗? - -**字符串常量池** 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。 - -```java -// 在字符串常量池中创建字符串对象 ”ab“ -// 将字符串对象 ”ab“ 的引用赋值给 aa -String aa = "ab"; -// 直接返回字符串常量池中字符串对象 ”ab“,赋值给引用 bb -String bb = "ab"; -System.out.println(aa==bb); // true -``` - -更多关于字符串常量池的介绍可以看一下 [Java 内存区域详解](https://javaguide.cn/java/jvm/memory-area.html) 这篇文章。 - -### String s1 = new String("abc");这句话创建了几个字符串对象? - -先说答案:会创建 1 或 2 个字符串对象。 - -1. 字符串常量池中不存在 "abc":会创建 2 个 字符串对象。一个在字符串常量池中,由 `ldc` 指令触发创建。一个在堆中,由 `new String()` 创建,并使用常量池中的 "abc" 进行初始化。 -2. 字符串常量池中已存在 "abc":会创建 1 个 字符串对象。该对象在堆中,由 `new String()` 创建,并使用常量池中的 "abc" 进行初始化。 - -下面开始详细分析。 - -1、如果字符串常量池中不存在字符串对象 “abc”,那么它首先会在字符串常量池中创建字符串对象 "abc",然后在堆内存中再创建其中一个字符串对象 "abc"。 - -示例代码(JDK 1.8): - -```java -String s1 = new String("abc"); -``` - -对应的字节码: - -```java -// 在堆内存中分配一个尚未初始化的 String 对象。 -// #2 是常量池中的一个符号引用,指向 java/lang/String 类。 -// 在类加载的解析阶段,这个符号引用会被解析成直接引用,即指向实际的 java/lang/String 类。 -0 new #2 -// 复制栈顶的 String 对象引用,为后续的构造函数调用做准备。 -// 此时操作数栈中有两个相同的对象引用:一个用于传递给构造函数,另一个用于保持对新对象的引用,后续将其存储到局部变量表。 -3 dup -// JVM 先检查字符串常量池中是否存在 "abc"。 -// 如果常量池中已存在 "abc",则直接返回该字符串的引用; -// 如果常量池中不存在 "abc",则 JVM 会在常量池中创建该字符串字面量并返回它的引用。 -// 这个引用被压入操作数栈,用作构造函数的参数。 -4 ldc #3 -// 调用构造方法,使用从常量池中加载的 "abc" 初始化堆中的 String 对象 -// 新的 String 对象将包含与常量池中的 "abc" 相同的内容,但它是一个独立的对象,存储于堆中。 -6 invokespecial #4 : (Ljava/lang/String;)V> -// 将堆中的 String 对象引用存储到局部变量表 -9 astore_1 -// 返回,结束方法 -10 return -``` - -`ldc (load constant)` 指令的确是从常量池中加载各种类型的常量,包括字符串常量、整数常量、浮点数常量,甚至类引用等。对于字符串常量,`ldc` 指令的行为如下: - -1. **从常量池加载字符串**:`ldc` 首先检查字符串常量池中是否已经有内容相同的字符串对象。 -2. **复用已有字符串对象**:如果字符串常量池中已经存在内容相同的字符串对象,`ldc` 会将该对象的引用加载到操作数栈上。 -3. **没有则创建新对象并加入常量池**:如果字符串常量池中没有相同内容的字符串对象,JVM 会在常量池中创建一个新的字符串对象,并将其引用加载到操作数栈中。 - -2、如果字符串常量池中已存在字符串对象“abc”,则只会在堆中创建 1 个字符串对象“abc”。 - -示例代码(JDK 1.8): - -```java -// 字符串常量池中已存在字符串对象“abc” -String s1 = "abc"; -// 下面这段代码只会在堆中创建 1 个字符串对象“abc” -String s2 = new String("abc"); -``` - -对应的字节码: - -```java -0 ldc #2 -2 astore_1 -3 new #3 -6 dup -7 ldc #2 -9 invokespecial #4 : (Ljava/lang/String;)V> -12 astore_2 -13 return -``` - -这里就不对上面的字节码进行详细注释了,7 这个位置的 `ldc` 命令不会在堆中创建新的字符串对象“abc”,这是因为 0 这个位置已经执行了一次 `ldc` 命令,已经在堆中创建过一次字符串对象“abc”了。7 这个位置执行 `ldc` 命令会直接返回字符串常量池中字符串对象“abc”对应的引用。 - -### String#intern 方法有什么作用? - -`String.intern()` 是一个 `native` (本地) 方法,用来处理字符串常量池中的字符串对象引用。它的工作流程可以概括为以下两种情况: - -1. **常量池中已有相同内容的字符串对象**:如果字符串常量池中已经有一个与调用 `intern()` 方法的字符串内容相同的 `String` 对象,`intern()` 方法会直接返回常量池中该对象的引用。 -2. **常量池中没有相同内容的字符串对象**:如果字符串常量池中还没有一个与调用 `intern()` 方法的字符串内容相同的对象,`intern()` 方法会将当前字符串对象的引用添加到字符串常量池中,并返回该引用。 - -总结: - -- `intern()` 方法的主要作用是确保字符串引用在常量池中的唯一性。 -- 当调用 `intern()` 时,如果常量池中已经存在相同内容的字符串,则返回常量池中已有对象的引用;否则,将该字符串添加到常量池并返回其引用。 - -示例代码(JDK 1.8) : - -```java -// s1 指向字符串常量池中的 "Java" 对象 -String s1 = "Java"; -// s2 也指向字符串常量池中的 "Java" 对象,和 s1 是同一个对象 -String s2 = s1.intern(); -// 在堆中创建一个新的 "Java" 对象,s3 指向它 -String s3 = new String("Java"); -// s4 指向字符串常量池中的 "Java" 对象,和 s1 是同一个对象 -String s4 = s3.intern(); -// s1 和 s2 指向的是同一个常量池中的对象 -System.out.println(s1 == s2); // true -// s3 指向堆中的对象,s4 指向常量池中的对象,所以不同 -System.out.println(s3 == s4); // false -// s1 和 s4 都指向常量池中的同一个对象 -System.out.println(s1 == s4); // true -``` - -### String 类型的变量和常量做“+”运算时发生了什么? - -先来看字符串不加 `final` 关键字拼接的情况(JDK1.8): - -```java -String str1 = "str"; -String str2 = "ing"; -String str3 = "str" + "ing"; -String str4 = str1 + str2; -String str5 = "string"; -System.out.println(str3 == str4);//false -System.out.println(str3 == str5);//true -System.out.println(str4 == str5);//false -``` - -> **注意**:比较 String 字符串的值是否相等,可以使用 `equals()` 方法。 `String` 中的 `equals` 方法是被重写过的。 `Object` 的 `equals` 方法是比较的对象的内存地址,而 `String` 的 `equals` 方法比较的是字符串的值是否相等。如果你使用 `==` 比较两个字符串是否相等的话,IDEA 还是提示你使用 `equals()` 方法替换。 - -![](https://oss.javaguide.cn/java-guide-blog/image-20210817123252441.png) - -**对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。** - -在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 **常量折叠(Constant Folding)** 的代码优化。《深入理解 Java 虚拟机》中是也有介绍到: - -![](https://oss.javaguide.cn/javaguide/image-20210817142715396.png) - -常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。 - -对于 `String str3 = "str" + "ing";` 编译器会给你优化成 `String str3 = "string";` 。 - -并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以: - -- 基本数据类型( `byte`、`boolean`、`short`、`char`、`int`、`float`、`long`、`double`)以及字符串常量。 -- `final` 修饰的基本数据类型和字符串变量 -- 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、\>>、\>>> ) - -**引用的值在程序编译期是无法确定的,编译器无法对其进行优化。** - -对象引用和“+”的字符串拼接方式,实际上是通过 `StringBuilder` 调用 `append()` 方法实现的,拼接完成之后调用 `toString()` 得到一个 `String` 对象 。 - -```java -String str4 = new StringBuilder().append(str1).append(str2).toString(); -``` - -我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 `StringBuilder` 或者 `StringBuffer`。 - -不过,字符串使用 `final` 关键字声明之后,可以让编译器当做常量来处理。 - -示例代码: - -```java -final String str1 = "str"; -final String str2 = "ing"; -// 下面两个表达式其实是等价的 -String c = "str" + "ing";// 常量池中的对象 -String d = str1 + str2; // 常量池中的对象 -System.out.println(c == d);// true -``` - -被 `final` 关键字修饰之后的 `String` 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。 - -如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。 - -示例代码(`str2` 在运行时才能确定其值): - -```java -final String str1 = "str"; -final String str2 = getStr(); -String c = "str" + "ing";// 常量池中的对象 -String d = str1 + str2; // 在堆上创建的新的对象 -System.out.println(c == d);// false -public static String getStr() { - return "ing"; -} -``` - -## 参考 - -- 深入解析 String#intern: -- Java String 源码解读: -- R 大(RednaxelaFX)关于常量折叠的回答: - - +- An object reference can point to 0 or 1 object (like a string can either not tie a balloon or tie one balloon); +- An object can have n references pointing to it diff --git a/docs/java/basis/java-basic-questions-03.md b/docs/java/basis/java-basic-questions-03.md index d98297398a4..df442d5b003 100644 --- a/docs/java/basis/java-basic-questions-03.md +++ b/docs/java/basis/java-basic-questions-03.md @@ -1,72 +1,72 @@ --- -title: Java基础常见面试题总结(下) +title: Summary of Common Java Interview Questions (Part 2) category: Java tag: - - Java基础 + - Java Basics head: - - - meta - - name: keywords - content: Java异常处理, Java泛型, Java反射, Java注解, Java SPI机制, Java序列化, Java反序列化, Java IO流, Java语法糖, Java基础面试题, Checked Exception, Unchecked Exception, try-with-resources, 反射应用场景, 序列化协议, BIO, NIO, AIO, IO模型 - - - meta - - name: description - content: 全网质量最高的Java基础常见知识点和面试题总结,希望对你有帮助! + - - meta + - name: keywords + content: Java Exception Handling, Java Generics, Java Reflection, Java Annotations, Java SPI Mechanism, Java Serialization, Java Deserialization, Java IO Streams, Java Syntax Sugar, Common Java Interview Questions, Checked Exception, Unchecked Exception, try-with-resources, Reflection Use Cases, Serialization Protocols, BIO, NIO, AIO, IO Models + - - meta + - name: description + content: The highest quality summary of common Java basics and interview questions available online, hoping to be helpful to you! --- -## 异常 +## Exceptions -**Java 异常类层次结构图概览**: +**Overview of Java Exception Class Hierarchy**: -![Java 异常类层次结构图](https://oss.javaguide.cn/github/javaguide/java/basis/types-of-exceptions-in-java.png) +![Java Exception Class Hierarchy](https://oss.javaguide.cn/github/javaguide/java/basis/types-of-exceptions-in-java.png) -### Exception 和 Error 有什么区别? +### What is the difference between Exception and Error? -在 Java 中,所有的异常都有一个共同的祖先 `java.lang` 包中的 `Throwable` 类。`Throwable` 类有两个重要的子类: +In Java, all exceptions have a common ancestor, the `Throwable` class in the `java.lang` package. The `Throwable` class has two important subclasses: -- **`Exception`** :程序本身可以处理的异常,可以通过 `catch` 来进行捕获。`Exception` 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。 -- **`Error`**:`Error` 属于程序无法处理的错误 ,~~我们没办法通过 `catch` 来进行捕获~~不建议通过`catch`捕获 。例如 Java 虚拟机运行错误(`Virtual MachineError`)、虚拟机内存不够错误(`OutOfMemoryError`)、类定义错误(`NoClassDefFoundError`)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。 +- **`Exception`**: Exceptions that can be handled by the program itself and can be caught using `catch`. `Exception` can be further divided into Checked Exceptions (which must be handled) and Unchecked Exceptions (which can be ignored). +- **`Error`**: Errors that are not usually handled by the program. It is not recommended to catch `Error`s. Examples include Java Virtual Machine errors (`VirtualMachineError`), out of memory errors (`OutOfMemoryError`), and class definition errors (`NoClassDefFoundError`). When these exceptions occur, the Java Virtual Machine (JVM) generally terminates the thread. -### Checked Exception 和 Unchecked Exception 有什么区别? +### What is the difference between Checked Exception and Unchecked Exception? -**Checked Exception** 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 `catch`或者`throws` 关键字处理的话,就没办法通过编译。 +**Checked Exception** refers to exceptions that are checked at compile-time. If a checked exception is not handled by a `catch` block or the `throws` keyword, the code will not compile. -比如下面这段 IO 操作的代码: +For example, in the following IO operations code: ![](https://oss.javaguide.cn/github/javaguide/java/basis/checked-exception.png) -除了`RuntimeException`及其子类以外,其他的`Exception`类及其子类都属于受检查异常 。常见的受检查异常有:IO 相关的异常、`ClassNotFoundException`、`SQLException`...。 +All `Exception` classes and their subclasses, except for `RuntimeException`, are considered checked exceptions. Common checked exceptions include IO-related exceptions, `ClassNotFoundException`, `SQLException`, etc. -**Unchecked Exception** 即 **不受检查异常** ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。 +**Unchecked Exception** refers to exceptions that are not checked at compile-time. Java code can compile normally even if unchecked exceptions are not handled. -`RuntimeException` 及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到): +`RuntimeException` and its subclasses are referred to as unchecked exceptions. Common ones (recommended to remember as they will be frequently used in daily development): -- `NullPointerException`(空指针错误) -- `IllegalArgumentException`(参数错误比如方法入参类型错误) -- `NumberFormatException`(字符串转换为数字格式错误,`IllegalArgumentException`的子类) -- `ArrayIndexOutOfBoundsException`(数组越界错误) -- `ClassCastException`(类型转换错误) -- `ArithmeticException`(算术错误) -- `SecurityException` (安全错误比如权限不够) -- `UnsupportedOperationException`(不支持的操作错误比如重复创建同一用户) +- `NullPointerException` (null pointer exception) +- `IllegalArgumentException` (argument errors, such as incorrect method parameter types) +- `NumberFormatException` (string conversion to number format error, a subclass of `IllegalArgumentException`) +- `ArrayIndexOutOfBoundsException` (array index out of bounds error) +- `ClassCastException` (class cast error) +- `ArithmeticException` (arithmetic error) +- `SecurityException` (security error, such as insufficient permission) +- `UnsupportedOperationException` (unsupported operation error, such as trying to create the same user twice) - …… ![](https://oss.javaguide.cn/github/javaguide/java/basis/unchecked-exception.png) -### Throwable 类常用方法有哪些? +### What are the common methods of the Throwable class? -- `String getMessage()`: 返回异常发生时的详细信息 -- `String toString()`: 返回异常发生时的简要描述 -- `String getLocalizedMessage()`: 返回异常对象的本地化信息。使用 `Throwable` 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 `getMessage()`返回的结果相同 -- `void printStackTrace()`: 在控制台上打印 `Throwable` 对象封装的异常信息 +- `String getMessage()`: Returns detailed information about the exception. +- `String toString()`: Returns a brief description of the exception. +- `String getLocalizedMessage()`: Returns localized information about the exception object. If this method is overridden by a subclass of `Throwable`, it can generate localized information. If not overridden, it returns the same information as `getMessage()`. +- `void printStackTrace()`: Prints the exception information encapsulated by the `Throwable` object to the console. -### try-catch-finally 如何使用? +### How to use try-catch-finally? -- `try`块:用于捕获异常。其后可接零个或多个 `catch` 块,如果没有 `catch` 块,则必须跟一个 `finally` 块。 -- `catch`块:用于处理 try 捕获到的异常。 -- `finally` 块:无论是否捕获或处理异常,`finally` 块里的语句都会被执行。当在 `try` 块或 `catch` 块中遇到 `return` 语句时,`finally` 语句块将在方法返回之前被执行。 +- `try` block: Used to catch exceptions. It can be followed by zero or more `catch` blocks, and if there are no catch blocks, it must be followed by a `finally` block. +- `catch` block: Used to handle the exception caught by the try block. +- `finally` block: The statements in the `finally` block are executed regardless of whether an exception was caught or handled. If a `return` statement is encountered in the `try` or `catch` blocks, the `finally` statements will be executed before the method returns. -代码示例: +Code example: ```java try { @@ -79,7 +79,7 @@ try { } ``` -输出: +Output: ```plain Try to do something @@ -87,9 +87,9 @@ Catch Exception -> RuntimeException Finally ``` -**注意:不要在 finally 语句块中使用 return!** 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。 +**Note: Do not use return in the finally block!** If both the try block and the finally block contain return statements, the one in the try block will be ignored. This is because the return value from the try block is temporarily stored in a local variable, and when the return statement in the finally block is executed, this local variable’s value is replaced by the return value in the finally block. -代码示例: +Code example: ```java public static void main(String[] args) { @@ -107,17 +107,17 @@ public static int f(int value) { } ``` -输出: +Output: ```plain 0 ``` -### finally 中的代码一定会执行吗? +### Will the code in the finally block always execute? -不一定的!在某些情况下,finally 中的代码不会被执行。 +Not necessarily! In some cases, the code in the finally block may not be executed. -就比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。 +For example, if the JVM terminates before reaching the finally, the code in the finally block will not be executed. ```java try { @@ -125,42 +125,42 @@ try { throw new RuntimeException("RuntimeException"); } catch (Exception e) { System.out.println("Catch Exception -> " + e.getMessage()); - // 终止当前正在运行的Java虚拟机 + // Terminate the currently running Java virtual machine System.exit(1); } finally { System.out.println("Finally"); } ``` -输出: +Output: ```plain Try to do something Catch Exception -> RuntimeException ``` -另外,在以下 2 种特殊情况下,`finally` 块的代码也不会被执行: +Additionally, in the following two special cases, the code in the `finally` block will also not be executed: -1. 程序所在的线程死亡。 -2. 关闭 CPU。 +1. The thread running the program dies. +1. The CPU is shut down. -相关 issue:。 +Related issue: . -🧗🏻 进阶一下:从字节码角度分析`try catch finally`这个语法糖背后的实现原理。 +🧗🏻 Advanced: Analyze the underlying implementation principles of `try-catch-finally` from the bytecode perspective. -### 如何使用 `try-with-resources` 代替`try-catch-finally`? +### How to use `try-with-resources` instead of `try-catch-finally`? -1. **适用范围(资源的定义):** 任何实现 `java.lang.AutoCloseable`或者 `java.io.Closeable` 的对象 -2. **关闭资源和 finally 块的执行顺序:** 在 `try-with-resources` 语句中,任何 catch 或 finally 块在声明的资源关闭后运行 +1. **Applicable scope (definition of resources):** Any object that implements `java.lang.AutoCloseable` or `java.io.Closeable`. +1. **Closing resources and execution order of finally block:** In `try-with-resources` statements, any catch or finally blocks run after the declared resources have been closed. -《Effective Java》中明确指出: +"Effective Java" clearly states: -> 面对必须要关闭的资源,我们总是应该优先使用 `try-with-resources` 而不是`try-finally`。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。`try-with-resources`语句让我们更容易编写必须要关闭的资源的代码,若采用`try-finally`则几乎做不到这点。 +> When facing resources that must be closed, we should always prefer to use `try-with-resources` instead of `try-finally`. The resulting code is neater, clearer, and the exceptions generated are more useful. `try-with-resources` allows us to write code dealing with resources that must be closed more conveniently, something that is virtually impossible with `try-finally`. -Java 中类似于`InputStream`、`OutputStream`、`Scanner`、`PrintWriter`等的资源都需要我们调用`close()`方法来手动关闭,一般情况下我们都是通过`try-catch-finally`语句来实现这个需求,如下: +Resources like `InputStream`, `OutputStream`, `Scanner`, and `PrintWriter` in Java require us to invoke their `close()` method to close them manually. Generally, we implement this requirement with `try-catch-finally` statements as follows: ```java -//读取文本文件的内容 +// Read content from a text file Scanner scanner = null; try { scanner = new Scanner(new File("D://read.txt")); @@ -176,7 +176,7 @@ try { } ``` -使用 Java 7 之后的 `try-with-resources` 语句改造上面的代码: +Using the `try-with-resources` statement introduced in Java 7, we can refactor the above code as follows: ```java try (Scanner scanner = new Scanner(new File("test.txt"))) { @@ -188,9 +188,9 @@ try (Scanner scanner = new Scanner(new File("test.txt"))) { } ``` -当然多个资源需要关闭的时候,使用 `try-with-resources` 实现起来也非常简单,如果你还是用`try-catch-finally`可能会带来很多问题。 +When multiple resources need to be closed, using `try-with-resources` is also very simple. If you still use `try-catch-finally`, it may lead to many issues. -通过使用分号分隔,可以在`try-with-resources`块中声明多个资源。 +By using a semicolon to separate, we can declare multiple resources in the `try-with-resources` block. ```java try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt"))); @@ -205,37 +205,37 @@ catch (IOException e) { } ``` -### 异常使用有哪些需要注意的地方? +### What should be taken into account when using exceptions? -- 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。 -- 抛出的异常信息一定要有意义。 -- 建议抛出更加具体的异常,比如字符串转换为数字格式错误的时候应该抛出`NumberFormatException`而不是其父类`IllegalArgumentException`。 -- 避免重复记录日志:如果在捕获异常的地方已经记录了足够的信息(包括异常类型、错误信息和堆栈跟踪等),那么在业务代码中再次抛出这个异常时,就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀,并且可能会掩盖问题的实际原因,使得问题更难以追踪和解决。 +- Do not define exceptions as static variables because this can cause the exception stack information to be messed up. Every time we manually throw an exception, we need to create a new exception object to throw. +- The thrown exception messages must be meaningful. +- It is advisable to throw more specific exceptions. For example, when there is an error converting a string to a number format, the appropriate exception to throw would be `NumberFormatException` instead of its parent class `IllegalArgumentException`. +- Avoid duplicate logging: If sufficient information (including exception type, error message, and stack trace) has already been logged in the place where the exception was caught, then the same error information should not be logged again when rethrowing this exception in business code. Duplicative logging inflates log files and may obscure the actual cause of problems, making tracing and resolving issues harder. - …… -## 泛型 +## Generics -### 什么是泛型?有什么作用? +### What are generics? What are their functions? -**Java 泛型(Generics)** 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。 +**Java Generics** is a new feature introduced in JDK 5. Using generic parameters enhances the readability and stability of code. -编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 `ArrayList persons = new ArrayList()` 这行代码就指明了该 `ArrayList` 对象只能传入 `Person` 对象,如果传入其他类型的对象就会报错。 +The compiler conducts checks on generic parameters, allowing you to specify the type of objects being passed. For example, the line `ArrayList persons = new ArrayList()` indicates that this `ArrayList` object can only contain `Person` objects, and passing in other types of objects will result in an error. ```java ArrayList extends AbstractList ``` -并且,原生 `List` 返回类型是 `Object` ,需要手动转换类型才能使用,使用泛型后编译器自动转换。 +Moreover, without generics, the return type of the raw `List` is `Object`, which necessitates manual type casting to use the elements, while generics enable automatic type conversion by the compiler. -### 泛型的使用方式有哪几种? +### What are the ways to use generics? -泛型一般有三种使用方式:**泛型类**、**泛型接口**、**泛型方法**。 +Generics can generally be used in three ways: **Generic Classes**, **Generic Interfaces**, and **Generic Methods**. -**1.泛型类**: +**1. Generic Class**: ```java -//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 -//在实例化泛型类时,必须指定T的具体类型 +// The T here can be any identifier, commonly used forms are T, E, K, V etc. to represent generics. +// When instantiating a generic class, the specific type of T must be specified. public class Generic{ private T key; @@ -250,13 +250,13 @@ public class Generic{ } ``` -如何实例化泛型类: +How to instantiate a generic class: ```java Generic genericInteger = new Generic(123456); ``` -**2.泛型接口**: +**2. Generic Interface**: ```java public interface Generator { @@ -264,7 +264,7 @@ public interface Generator { } ``` -实现泛型接口,不指定类型: +Implementing a generic interface without specifying a type: ```java class GeneratorImpl implements Generator{ @@ -275,7 +275,7 @@ class GeneratorImpl implements Generator{ } ``` -实现泛型接口,指定类型: +Implementing a generic interface with specified type: ```java class GeneratorImpl implements Generator { @@ -286,65 +286,65 @@ class GeneratorImpl implements Generator { } ``` -**3.泛型方法**: +**3. Generic Method**: ```java - public static < E > void printArray( E[] inputArray ) + public static void printArray(E[] inputArray) { - for ( E element : inputArray ){ - System.out.printf( "%s ", element ); + for (E element : inputArray) { + System.out.printf("%s ", element); } System.out.println(); } ``` -使用: +Usage: ```java -// 创建不同类型数组:Integer, Double 和 Character +// Create arrays of different types: Integer, Double, and Character Integer[] intArray = { 1, 2, 3 }; String[] stringArray = { "Hello", "World" }; -printArray( intArray ); -printArray( stringArray ); +printArray(intArray); +printArray(stringArray); ``` -> 注意: `public static < E > void printArray( E[] inputArray )` 一般被称为静态泛型方法;在 java 中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的 `` +> Note: `public static void printArray(E[] inputArray)` is often called a static generic method; in Java, generics are merely placeholders, and must be used after passing the type. The actual type parameters are truly passed when the class is instantiated. Since static methods load before class instantiation, it means that generics in the class have not yet passed the true type parameters. Thus, static generic methods cannot use generics declared on the class; they can only use their own declared ``. -### 项目中哪里用到了泛型? +### Where are generics used in projects? -- 自定义接口通用返回结果 `CommonResult` 通过参数 `T` 可根据具体的返回类型动态指定结果的数据类型 -- 定义 `Excel` 处理类 `ExcelUtil` 用于动态指定 `Excel` 导出的数据类型 -- 构建集合工具类(参考 `Collections` 中的 `sort`, `binarySearch` 方法)。 +- Custom interface common return results `CommonResult` allows specifying the result data type dynamically based on the parameter `T`. +- Defining an `Excel` processing class `ExcelUtil` to dynamically specify the data type for `Excel` exports. +- Building collection utility classes (referencing the `sort` and `binarySearch` methods in `Collections`). - …… -## 反射 +## Reflection -关于反射的详细解读,请看这篇文章 [Java 反射机制详解](./reflection.md) 。 +For a detailed discussion on Reflection, please check this article [Detailed Explanation of Java Reflection Mechanism](./reflection.md). -### 何谓反射? +### What is Reflection? -如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。 +If you have studied the underlying principles of frameworks or written your own frameworks, you must be familiar with the concept of reflection. Reflection is considered the soul of frameworks mainly because it grants us the ability to analyze classes and invoke their methods at runtime. With reflection, you can obtain all properties and methods of any class and invoke these methods and properties. -### 反射的优缺点? +### What are the pros and cons of Reflection? -反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。 +Reflection allows our code to be more flexible and facilitates the functionality provided by various frameworks. -不过,反射让我们在运行时有了分析操作类的能力的同时,也增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。 +However, while reflection gives us the ability to analyze operation classes at runtime, it also increases security concerns, such as bypassing the safety checks of generic parameters (which happen at compile-time). Additionally, reflection is generally slower in performance, though this is not a significant issue for frameworks in practice. -相关阅读:[Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) 。 +Related reading: [Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow). -### 反射的应用场景? +### What are the application scenarios of Reflection? -像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。但是!这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。 +Most of the time, we write business logic code and rarely directly encounter scenarios that utilize reflection. But! This does not imply that reflection is useless. On the contrary, it is precisely because of reflection that you can easily use various frameworks. Frameworks like Spring/Spring Boot, MyBatis, etc., heavily utilize reflection. -**这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。** +**These frameworks also extensively use dynamic proxies, and the implementation of dynamic proxies relies on reflection.** -比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 `Method` 来调用指定的方法。 +For example, the following is an example of JDK dynamic proxy implementation, which uses the reflection class `Method` to call the specified method. ```java public class DebugInvocationHandler implements InvocationHandler { /** - * 代理类中的真实对象 + * The real object in the proxy class. */ private final Object target; @@ -359,22 +359,21 @@ public class DebugInvocationHandler implements InvocationHandler { return result; } } - ``` -另外,像 Java 中的一大利器 **注解** 的实现也用到了反射。 +Additionally, one powerful feature in Java, **annotations**, is also implemented using reflection. -为什么你使用 Spring 的时候 ,一个`@Component`注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 `@Value`注解就读取到配置文件中的值呢?究竟是怎么起作用的呢? +Why does a class declared as a Spring Bean just require an `@Component` annotation when using Spring? Why does using an `@Value` annotation allow you to read values from configuration files? How does this work? -这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。 +This is all because you can analyze classes based on reflection and obtain class/property/method/parameter annotations. After obtaining annotations, you can perform further processing. -## 注解 +## Annotations -### 何谓注解? +### What are annotations? -`Annotation` (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。 +`Annotation` is a new feature introduced in Java 5, which can be seen as a special type of comment primarily used to modify classes, methods, or variables, providing some information for programs to use at compile time or runtime. -注解本质是一个继承了`Annotation` 的特殊接口: +An annotation is essentially a special interface that extends `Annotation`: ```java @Target(ElementType.METHOD) @@ -388,179 +387,179 @@ public interface Override extends Annotation{ } ``` -JDK 提供了很多内置的注解(比如 `@Override`、`@Deprecated`),同时,我们还可以自定义注解。 +The JDK provides many built-in annotations (such as `@Override`, `@Deprecated`), and we can also define custom annotations. -### 注解的解析方法有哪几种? +### What are the methods to interpret annotations? -注解只有被解析之后才会生效,常见的解析方法有两种: +Annotations only take effect once they are interpreted; common interpretation methods include two types: -- **编译期直接扫描**:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用`@Override` 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 -- **运行期通过反射处理**:像框架中自带的注解(比如 Spring 框架的 `@Value`、`@Component`)都是通过反射来进行处理的。 +- **Compile-time direct scanning**: The compiler scans and processes the corresponding annotations when compiling Java code. For example, if a method uses the `@Override` annotation, the compiler checks whether the current method overrides the corresponding method in the parent class during compilation. +- **Runtime processing via reflection**: Annotations provided in frameworks (like Spring's `@Value`, `@Component`) are processed using reflection. ## SPI -关于 SPI 的详细解读,请看这篇文章 [Java SPI 机制详解](./spi.md) 。 +For a detailed discussion on SPI, please check this article [Detailed Explanation of Java SPI Mechanism](./spi.md). -### 何谓 SPI? +### What is SPI? -SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。 +SPI stands for Service Provider Interface. Literally, it means "an interface for service providers". My understanding is that it is an interface specifically for use by service providers or developers expanding framework functionality. -SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。 +SPI separates service interfaces from specific service implementations, decoupling service callers from service implementers, thereby enhancing program extensibility and maintainability. Modifying or replacing service implementations does not require changes to callers. -很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。 +Many frameworks utilize Java's SPI mechanism, such as the Spring framework, database drivers, logging interfaces, and the extension implementations of Dubbo, etc. -### SPI 和 API 有什么区别? +### What is the difference between SPI and API? -**那 SPI 和 API 有啥区别?** +**What is the difference between SPI and API?** -说到 SPI 就不得不说一下 API(Application Programming Interface) 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下: +When discussing SPI, one cannot help but mention API (Application Programming Interface). Broadly speaking, both belong to interfaces, and it's easy to confuse the two. The following diagram illustrates this: ![SPI VS API](https://oss.javaguide.cn/github/javaguide/java/basis/spi-vs-api.png) -一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。 +In typical systems, modules communicate through interfaces, so we introduce an "interface" between service callers and service implementers (also known as service providers). -- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 **API**。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。 -- 当接口存在于调用方这边时,这就是 **SPI** 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。 +- When the implementer provides an interface and implementation, we can call the implementer's interface to use the capabilities offered by the implementer, which is **API**. In this case, the interface and implementation are both located in the implementer's package. The caller utilizes the implementer's functions through the interface without needing to care about the specific implementation details. +- When the interface exists on the caller's side, this is **SPI**. The interface caller determines the interface rules while different vendors implement this interface based on the rules to provide services. -举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。 +For example, let’s say company H is a tech company that has designed a new chip, and now it requires mass production. Market has several chip manufacturing companies. In this case, as long as company H sets the production standards for this chip (defining the interface standards), these cooperating chip companies (service providers) can deliver their unique chips according to the standards (providing different implementation options, but yielding the same result). -### SPI 的优缺点? +### What are the pros and cons of SPI? -通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如: +The SPI mechanism greatly enhances the flexibility of interface design, but it does have some disadvantages, such as: -- 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。 -- 当多个 `ServiceLoader` 同时 `load` 时,会有并发问题。 +- It requires loading all implementation classes, which prevents on-demand loading and results in lower efficiency. +- When multiple `ServiceLoader` instances simultaneously `load`, there could be concurrency issues. -## 序列化和反序列化 +## Serialization and Deserialization -关于序列化和反序列化的详细解读,请看这篇文章 [Java 序列化详解](./serialization.md) ,里面涉及到的知识点和面试题更全面。 +For a detailed explanation of serialization and deserialization, please check this article [Detailed Explanation of Java Serialization](./serialization.md), which includes more comprehensive knowledge points and interview questions. -### 什么是序列化?什么是反序列化? +### What is serialization? What is deserialization? -如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。 +If we need to persist Java objects, such as saving Java objects to a file or transmitting them over a network, serialization is required. -简单来说: +In simple terms: -- **序列化**:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式 -- **反序列化**:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程 +- **Serialization**: The process of converting a data structure or object into a format that can be stored or transmitted, typically a binary byte stream, but can also include formats like JSON and XML. +- **Deserialization**: The process of converting the data generated during serialization back into the original data structure or object. -对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。 +In a language like Java, which is object-oriented, we serialize objects (instances of classes). However, in a semi-object-oriented language like C++, `struct` defines data structure types, whereas `class` defines object types. -下面是序列化和反序列化常见应用场景: +Common scenarios for serialization and deserialization include: -- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化; -- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化; -- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化; -- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。 +- Objects need to be serialized before being transmitted over the network (e.g., during remote method calls RPC), and deserialized after receiving. +- Objects must be serialized before being stored in files and deserialized when read back from files. +- Objects stored in databases (such as Redis) need serialization before storage and deserialization when retrieved from the caching database. +- Objects need to be serialized before being retained in memory and deserialized when retrieved. -维基百科是如是介绍序列化的: +According to Wikipedia, serialization is described as follows: -> **序列化**(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。 +> Serialization in computer science data processing refers to the process of converting a data structure or object state into a format that can be readily stored (for example, in a file, buffer, or sent over a network) to allow for its restoration in the same or different computing environments. The resulting bytes can then be used to produce a replica with the same semantics as the original object. For many complex objects, especially those withmany references, the serialization reconstruction can be challenging. In OOP, object serialization does not encompass the functions associated with the original object. This process is also referred to as object marshaling. The reverse operation of extracting data structures from a series of bytes is called deserialization (also known as unmarshalling, or unmarshal). -综上:**序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。** +In summary: **The main purpose of serialization is to transmit objects over a network or to store them in file systems, databases, or memory.** ![](https://oss.javaguide.cn/github/javaguide/a478c74d-2c48-40ae-9374-87aacf05188c.png)

https://www.corejavaguru.com/java/serialization/interview-questions-1

-**序列化协议对应于 TCP/IP 4 层模型的哪一层?** +**What layer of the TCP/IP model does the serialization protocol correspond to?** -我们知道网络通信的双方必须要采用和遵守相同的协议。TCP/IP 四层模型是下面这样的,序列化协议属于哪一层呢? +We know that both parties in network communication must adopt and comply with the same protocol. The TCP/IP four-layer model looks as follows; which layer does the serialization protocol belong to? -1. 应用层 -2. 传输层 -3. 网络层 -4. 网络接口层 +1. Application Layer +1. Transport Layer +1. Network Layer +1. Network Interface Layer -![TCP/IP 四层模型](https://oss.javaguide.cn/github/javaguide/cs-basics/network/tcp-ip-4-model.png) +![TCP/IP Four-Layer Model](https://oss.javaguide.cn/github/javaguide/cs-basics/network/tcp-ip-4-model.png) -如上图所示,OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么? +As shown above, in the OSI seven-layer protocol model, the presentation layer processes user data from the application layer, converting it to binary streams. Conversely, it transforms binary streams back into user data at the application layer. Doesn’t this directly correspond to serialization and deserialization? -因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。 +Since the application, presentation, and session layers in the OSI model correspond with the application layer in the TCP/IP model, the serialization protocol is a part of the application layer of the TCP/IP protocol. -### 如果有些字段不想进行序列化怎么办? +### What to do if certain fields should not be serialized? -对于不想进行序列化的变量,使用 `transient` 关键字修饰。 +For fields that should not be serialized, use the `transient` keyword. -`transient` 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 `transient` 修饰的变量值不会被持久化和恢复。 +The `transient` keyword serves the purpose of preventing the serialization of variables marked with it; when the object is deserialized, the values of the transient variables will not be persisted or restored. -关于 `transient` 还有几点注意: +A few additional points about `transient`: -- `transient` 只能修饰变量,不能修饰类和方法。 -- `transient` 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 `int` 类型,那么反序列后结果就是 `0`。 -- `static` 变量因为不属于任何对象(Object),所以无论有没有 `transient` 关键字修饰,均不会被序列化。 +- `transient` can only modify variables, not classes or methods. +- Variables marked as `transient` will have their values set to their types’ default values upon deserialization. For instance, if it is an `int`, the result will be `0`. +- `static` variables, because they do not belong to any instance (object), will not be serialized regardless of whether or not they are marked as `transient`. -### 常见序列化协议有哪些? +### What are some common serialization protocols? -JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。 +The serialization method provided by JDK is generally not used due to its low efficiency and security issues. More commonly used serialization protocols include Hessian, Kryo, Protobuf, ProtoStuff, which are all binary-based serialization protocols. -像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。 +JSON and XML belong to text-based serialization methods. While they are more readable, they generally offer lower performance and are usually not selected. -### 为什么不推荐使用 JDK 自带的序列化? +### Why is using JDK's built-in serialization not recommended? -我们很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因: +We rarely or nearly never directly use the serialization method built into JDK, primarily for the following reasons: -- **不支持跨语言调用** : 如果调用的是其他语言开发的服务的时候就不支持了。 -- **性能差**:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。 -- **存在安全问题**:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。相关阅读:[应用安全:JAVA 反序列化漏洞之殇](https://cryin.github.io/blog/secure-development-java-deserialization-vulnerability/) 。 +- **Does not support cross-language calls**: It does not support other language-developed services. +- **Poor performance**: Compared to other serialization frameworks, its performance is lower, primarily because the serialized byte array is large, which increases transmission costs. +- **Security issues**: Serialization and deserialization themselves do not inherently pose problems. But when the input deserialization data can be controlled by users, an attacker might construct malicious input causing deserialization to produce unintended objects, which may execute arbitrary code during this process. Related reading: [Application Security: JAVA Deserialization Vulnerability](https://cryin.github.io/blog/secure-development-java-deserialization-vulnerability/). ## I/O -关于 I/O 的详细解读,请看下面这几篇文章,里面涉及到的知识点和面试题更全面。 +For detailed insights into I/O, please refer to the following articles, which cover more comprehensive knowledge points and interview topics. -- [Java IO 基础知识总结](../io/io-basis.md) -- [Java IO 设计模式总结](../io/io-design-patterns.md) -- [Java IO 模型详解](../io/io-model.md) +- [Java IO Basics Summary](../io/io-basis.md) +- [Java IO Design Patterns Summary](../io/io-design-patterns.md) +- [Detailed Explanation of Java IO Models](../io/io-model.md) -### Java IO 流了解吗? +### Are you familiar with Java IO streams? -IO 即 `Input/Output`,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。 +IO stands for `Input/Output`, which refers to the process of inputting data into the computer's memory and outputting it to external storage (like databases, files, or remote hosts). The process of data transmission is akin to water flow, thus termed IO streams. Java's IO streams are divided into input streams and output streams, further classified into byte streams and character streams based on data processing approaches. -Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。 +Over 40 classes in Java's IO streams are derived from these four abstract class bases: -- `InputStream`/`Reader`: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 -- `OutputStream`/`Writer`: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 +- `InputStream`/`Reader`: The base class for all input streams, with the former being byte input streams and the latter being character input streams. +- `OutputStream`/`Writer`: The base class for all output streams, with the former being byte output streams and the latter being character output streams. -### I/O 流为什么要分为字节流和字符流呢? +### Why are I/O streams divided into byte streams and character streams? -问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?** +The essence of the question is: **Regardless of file reading/writing or network sending/receiving, the minimal storage unit of information is byte. Why is the I/O operation divided into byte stream operations and character stream operations?** -个人认为主要有两点原因: +I believe the reasons are twofold: -- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时; -- 如果我们不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。 +- Character streams are produced from byte conversions by the Java Virtual Machine, a process that is rather time-consuming. +- If we are unaware of the encoding type, using byte streams could easily result in garbled text. -### Java IO 中的设计模式有哪些? +### What design patterns exist in Java IO? -参考答案:[Java IO 设计模式总结](../io/io-design-patterns.md) +Reference answer: [Summary of Java IO Design Patterns](../io/io-design-patterns.md) -### BIO、NIO 和 AIO 的区别? +### What are the differences between BIO, NIO, and AIO? -参考答案:[Java IO 模型详解](../io/io-model.md) +Reference answer: [Detailed Explanation of Java IO Models](../io/io-model.md) -## 语法糖 +## Syntax Sugar -### 什么是语法糖? +### What is syntax sugar? -**语法糖(Syntactic sugar)** 代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法对编程语言的功能并没有影响。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。 +**Syntax sugar** refers to a special syntax designed in programming languages for the convenience of developers, which does not affect the language’s functionality. The code written using syntax sugar often performs the same function but is simpler, cleaner, and more readable. -举个例子,Java 中的 `for-each` 就是一个常用的语法糖,其原理其实就是基于普通的 for 循环和迭代器。 +For example, the `for-each` construct in Java is a common syntax sugar that is fundamentally based on standard for loops and iterators. ```java -String[] strs = {"JavaGuide", "公众号:JavaGuide", "博客:https://javaguide.cn/"}; +String[] strs = {"JavaGuide", "Official Account: JavaGuide", "Blog: https://javaguide.cn/"}; for (String s : strs) { System.out.println(s); } ``` -不过,JVM 其实并不能识别语法糖,Java 语法糖要想被正确执行,需要先通过编译器进行解糖,也就是在程序编译阶段将其转换成 JVM 认识的基本语法。这也侧面说明,Java 中真正支持语法糖的是 Java 编译器而不是 JVM。如果你去看`com.sun.tools.javac.main.JavaCompiler`的源码,你会发现在`compile()`中有一个步骤就是调用`desugar()`,这个方法就是负责解语法糖的实现的。 +However, the JVM does not recognize syntax sugar directly; Java syntax sugar must be desugared correctly by the compiler. This means that during the compilation phase, it gets transformed into the basic syntax recognizable by the JVM. This also suggests that the Java compiler, not the JVM, supports syntax sugar. If you look at the source code of `com.sun.tools.javac.main.JavaCompiler`, you will find that in `compile()`, there is a step called `desugar()`, which is responsible for the implementation of desugaring. -### Java 中有哪些常见的语法糖? +### What are some common syntax sugars in Java? -Java 中最常用的语法糖主要有泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等。 +The most commonly used syntax sugars in Java include generics, automatic boxing/unboxing, varargs, enums, inner classes, enhanced for loops, try-with-resources syntax, lambda expressions, etc. -关于这些语法糖的详细解读,请看这篇文章 [Java 语法糖详解](./syntactic-sugar.md) 。 +For a detailed interpretation of these syntax sugars, please refer to this article [Detailed Explanation of Java Syntax Sugar](./syntactic-sugar.md). diff --git a/docs/java/basis/java-keyword-summary.md b/docs/java/basis/java-keyword-summary.md index daf13c9ec14..f114e14efef 100644 --- a/docs/java/basis/java-keyword-summary.md +++ b/docs/java/basis/java-keyword-summary.md @@ -1,32 +1,30 @@ -# final,static,this,super 关键字总结 +# Summary of final, static, this, super Keywords -## final 关键字 +## final Keyword -**final 关键字,意思是最终的、不可修改的,最见不得变化 ,用来修饰类、方法和变量,具有以下特点:** +**The final keyword means final and unmodifiable, it represents the least amount of change and is used to modify classes, methods, and variables with the following characteristics:** -1. final 修饰的类不能被继承,final 类中的所有成员方法都会被隐式的指定为 final 方法; +1. A class modified with final cannot be inherited, and all member methods in a final class will implicitly be designated as final methods; +1. A method modified with final cannot be overridden; +1. A variable modified with final is a constant. If it is a primitive data type, its value cannot be changed after initialization; if it is a reference type, it cannot point to another object after initialization. -2. final 修饰的方法不能被重写; +Note: There are two reasons to use final methods: -3. final 修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。 +1. To lock the method to prevent any inherited class from modifying its meaning; +1. Efficiency. In earlier versions of Java, final methods would be converted into inline calls. However, if the method is too large, no performance improvement from inlining may be seen (current Java versions no longer require using final methods for such optimizations). -说明:使用 final 方法的原因有两个: +## static Keyword -1. 把方法锁定,以防任何继承类修改它的含义; -2. 效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。 +**The static keyword mainly has the following four usage scenarios:** -## static 关键字 +1. **Modifying member variables and member methods:** Members modified with static belong to the class and not to a specific instance of this class, shared by all objects in the class, and it is recommended to call them through the class name. Member variables declared as static are static member variables, which are stored in the method area of the Java memory region. Invocation format: `ClassName.staticVariableName` `ClassName.staticMethodName()` +1. **Static code blocks:** Static code blocks are defined outside of class methods and are executed before non-static code blocks (static code blocks → non-static code blocks → constructors). Regardless of how many objects are created, a static code block is executed only once. +1. **Static inner classes (only classes that are static can modify inner classes):** There is one significant difference between static inner classes and non-static inner classes: non-static inner classes implicitly hold a reference to the enclosing class after compilation, while static inner classes do not. The absence of this reference means: 1. Its creation does not depend on the enclosing class's creation. 2. It cannot use any non-static member variables and methods of the enclosing class. +1. **Static import (used to import static resources from a class, new feature after 1.5):** The format is: `import static` these two keywords can be used together to specify importing certain static resources from a class, and there is no need to use the class name to call static members; static member variables and methods can be accessed directly. -**static 关键字主要有以下四种使用场景:** +## this Keyword -1. **修饰成员变量和成员方法:** 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被 static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:`类名.静态变量名` `类名.静态方法名()` -2. **静态代码块:** 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次. -3. **静态内部类(static 修饰类的话只能修饰内部类):** 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非 static 成员变量和方法。 -4. **静态导包(用来导入类中的静态资源,1.5 之后的新特性):** 格式为:`import static` 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。 - -## this 关键字 - -this 关键字用于引用类的当前实例。 例如: +The this keyword is used to reference the current instance of the class. For example: ```java class Manager { @@ -40,21 +38,21 @@ class Manager { } ``` -在上面的示例中,this 关键字用于两个地方: +In the example above, the this keyword is used in two places: -- this.employees.length:访问类 Manager 的当前实例的变量。 -- this.report():调用类 Manager 的当前实例的方法。 +- this.employees.length: Accessing the variable of the current instance of the Manager class. +- this.report(): Calling the method of the current instance of the Manager class. -此关键字是可选的,这意味着如果上面的示例在不使用此关键字的情况下表现相同。 但是,使用此关键字可能会使代码更易读或易懂。 +This keyword is optional, meaning that the example above would behave the same without using this keyword. However, using this keyword may make the code more readable or understandable. -## super 关键字 +## super Keyword -super 关键字用于从子类访问父类的变量和方法。 例如: +The super keyword is used to access variables and methods of the parent class from a subclass. For example: ```java public class Super { protected int number; - protected showNumber() { + protected void showNumber() { System.out.println("number = " + number); } } @@ -66,64 +64,64 @@ public class Sub extends Super { } ``` -在上面的例子中,Sub 类访问父类成员变量 number 并调用其父类 Super 的 `showNumber()` 方法。 +In the example above, the Sub class accesses the superclass member variable number and calls its parent class Super's `showNumber()` method. -**使用 this 和 super 要注意的问题:** +**Issues to note when using this and super:** -- 在构造器中使用 `super()` 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。 -- this、super 不能用在 static 方法中。 +- When using `super()` to call another constructor in the parent class, this statement must be the first line of the constructor; otherwise, the compiler will throw an error. Similarly, when calling another constructor within the same class using this, it should also be placed at the first line. +- this and super cannot be used in static methods. -**简单解释一下:** +**In brief:** -被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, **this 和 super 是属于对象范畴的东西,而静态方法是属于类范畴的东西**。 +Members modified with static belong to the class and are shared by all objects within the class, while this represents a reference to the current class object and points to that instance; super represents a reference to the parent class object and points to that parent; thus, **this and super pertain to the instance realm, whereas static methods pertain to the class realm.** -## 参考 +## References - - -# static 关键字详解 +# Detailed Explanation of the static Keyword -## static 关键字主要有以下四种使用场景 +## The static keyword mainly has the following four usage scenarios -1. 修饰成员变量和成员方法 -2. 静态代码块 -3. 修饰类(只能修饰内部类) -4. 静态导包(用来导入类中的静态资源,1.5 之后的新特性) +1. Modifying member variables and methods +1. Static code blocks +1. Modifying classes (only inner classes) +1. Static import (used to import static resources from a class, new feature after 1.5) -### 修饰成员变量和成员方法(常用) +### Modifying member variables and methods (Commonly used) -被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被 static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。 +Members modified with static belong to the class and not to a specific instance of this class, shared by all objects in the class, and it is recommended to call them through the class name. Member variables declared as static are static member variables, which are stored in the method area of the Java memory region. -方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。 +The method area, like the Java heap, is a memory area shared among threads and is used to store information about classes that have been loaded by the virtual machine, constants, static variables, and compiled code from the just-in-time compiler. Although the Java Virtual Machine specification describes the method area as a logical part of the heap, it is also known as Non-Heap; this is to distinguish it from the Java heap area. -HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。 +In the HotSpot virtual machine, the method area is often referred to as "Permanent Generation," but the two are not equivalent. It is merely because the team that designed the HotSpot virtual machine implemented the method area using Permanent Generation so that the garbage collector of the HotSpot virtual machine can manage this memory as it does with the Java heap. However, this is not a good idea as it can lead to memory overflow issues. -调用格式: +Invocation format: -- `类名.静态变量名` -- `类名.静态方法名()` +- `ClassName.staticVariableName` +- `ClassName.staticMethodName()` -如果变量或者方法被 private 则代表该属性或者该方法只能在类的内部被访问而不能在类的外部被访问。 +If a variable or method is marked as private, it means that this attribute or method can only be accessed inside the class and not from outside. -测试方法: +Test method: ```java public class StaticBean { String name; - //静态变量 + // Static variable static int age; public StaticBean(String name) { this.name = name; } - //静态方法 + // Static method static void sayHello() { - System.out.println("Hello i am java"); + System.out.println("Hello, I am Java"); } @Override public String toString() { return "StaticBean{"+ - "name=" + name + ",age=" + age + + "name=" + name + ", age=" + age + "}"; } } @@ -132,51 +130,51 @@ public class StaticBean { ```java public class StaticDemo { public static void main(String[] args) { - StaticBean staticBean = new StaticBean("1"); + StaticBean staticBean1 = new StaticBean("1"); StaticBean staticBean2 = new StaticBean("2"); StaticBean staticBean3 = new StaticBean("3"); StaticBean staticBean4 = new StaticBean("4"); StaticBean.age = 33; - System.out.println(staticBean + " " + staticBean2 + " " + staticBean3 + " " + staticBean4); - //StaticBean{name=1,age=33} StaticBean{name=2,age=33} StaticBean{name=3,age=33} StaticBean{name=4,age=33} - StaticBean.sayHello();//Hello i am java + System.out.println(staticBean1 + " " + staticBean2 + " " + staticBean3 + " " + staticBean4); + // StaticBean{name=1, age=33} StaticBean{name=2, age=33} StaticBean{name=3, age=33} StaticBean{name=4, age=33} + StaticBean.sayHello(); // Hello, I am Java } } ``` -### 静态代码块 +### Static Code Blocks -静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块 —> 非静态代码块 —> 构造方法)。 该类不管创建多少对象,静态代码块只执行一次. +Static code blocks are defined outside of class methods. Static code blocks are executed before non-static code blocks (static code blocks → non-static code blocks → constructors). Regardless of how many objects are created, a static code block is executed only once. -静态代码块的格式是 +The format of a static code block is: ```plain static { -语句体; + statement; } ``` -一个类中的静态代码块可以有多个,位置可以随便放,它不在任何的方法体内,JVM 加载类时会执行这些静态的代码块,如果静态代码块有多个,JVM 将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。 +A class can have multiple static code blocks, which can be placed anywhere within the class. The JVM executes these static code blocks when loading the class. If there are multiple static code blocks, the JVM will execute them in the order they appear in the class. Each block is executed only once. ![](https://oss.javaguide.cn/github/javaguide/88531075.jpg) -静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问. +Static code blocks can assign values to static variables defined after them but cannot access them. -### 静态内部类 +### Static Inner Class -静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着: +The biggest difference between static inner classes and non-static inner classes is that non-static inner classes implicitly save a reference to the enclosing class after compilation, while static inner classes do not. The absence of this reference implies: -1. 它的创建是不需要依赖外围类的创建。 -2. 它不能使用任何外围类的非 static 成员变量和方法。 +1. Its creation does not depend on the creation of the enclosing class. +1. It cannot use any non-static member variables and methods of the enclosing class. -Example(静态内部类实现单例模式) +Example (Static inner class implementing singleton pattern): ```java public class Singleton { - //声明为 private 避免调用默认构造方法创建对象 + // Declared as private to prevent calling the default constructor to create an object private Singleton() { } - // 声明为 private 表明静态内部该类只能在该 Singleton 类中被访问 + // Declared as private to indicate that the static inner class can only be accessed within the Singleton class private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } @@ -186,35 +184,35 @@ public class Singleton { } ``` -当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 `getUniqueInstance()`方法从而触发 `SingletonHolder.INSTANCE` 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。 +When the Singleton class is loaded, the static inner class SingletonHolder is not loaded into memory. Only when calling the `getUniqueInstance()` method, triggering `SingletonHolder.INSTANCE`, will SingletonHolder be loaded, initializing the INSTANCE. The JVM ensures that INSTANCE is instantiated only once. -这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。 +This method not only provides the advantage of lazy initialization but also offers thread safety support from the JVM. -### 静态导包 +### Static Import -格式为:import static +The format is: import static -这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法 +Using these two keywords together allows specifying importing specific static resources from a class without using the class name to call static members; thus, static member variables and methods can be accessed directly. ```java - //将Math中的所有静态资源导入,这时候可以直接使用里面的静态方法,而不用通过类名进行调用 - //如果只想导入单一某个静态方法,只需要将*换成对应的方法名即可 -import static java.lang.Math.*;//换成import static java.lang.Math.max;具有一样的效果 +// Importing all static resources from Math, allowing direct usage of its static methods without class name +// If you only want to import a specific static method, just replace * with the method name accordingly +import static java.lang.Math.*; // Alternatively, you may use import static java.lang.Math.max; for the same effect public class Demo { public static void main(String[] args) { - int max = max(1,2); + int max = max(1, 2); System.out.println(max); } } ``` -## 补充内容 +## Supplementary Content -### 静态方法与非静态方法 +### Static Methods vs Non-Static Methods -静态方法属于类本身,非静态方法属于从该类生成的每个对象。 如果您的方法执行的操作不依赖于其类的各个变量和方法,请将其设置为静态(这将使程序的占用空间更小)。 否则,它应该是非静态的。 +Static methods belong to the class itself, while non-static methods belong to each object instantiated from the class. If your method performs operations independent of its instance variables and methods, consider making it static (this will reduce space consumption of the program). Otherwise, it should be non-static. -Example +Example: ```java class Foo { @@ -226,81 +224,81 @@ class Foo { return "An example string that doesn't depend on i (an instance variable)"; } public int method2() { - return this.i + 1; //Depends on i + return this.i + 1; // Depends on i } } ``` -你可以像这样调用静态方法:`Foo.method1()`。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行 +You can call static methods like this: `Foo.method1()`. If you try calling method2 this way, it will fail. However, the following is acceptable: ```java Foo bar = new Foo(1); bar.method2(); ``` -总结: +Summary: -- 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 -- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制 +- When calling static methods externally, you can use either "ClassName.MethodName" or "ObjectName.MethodName" syntax. However, instance methods only support the latter. In other words, static methods can be accessed without creating objects. +- In accessing members of the class, static methods are only allowed to access static members (i.e., static member variables and static methods) and cannot access instance member variables and instance methods; instance methods do not have this restriction. -### `static{}`静态代码块与`{}`非静态代码块(构造代码块) +### Differences Between `static{}` Static Code Blocks and `{}` Non-Static Code Blocks (Constructor Code Blocks) -相同点:都是在 JVM 加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些 static 变量进行赋值。 +Similarities: Both are executed when the JVM loads the class and before the constructor executes; multiple instances can be defined, executed in the order defined, and are generally used for initializing some static variables. -不同点:静态代码块在非静态代码块之前执行(静态代码块 -> 非静态代码块 -> 构造方法)。静态代码块只在第一次 new 执行一次,之后不再执行,而非静态代码块在每 new 一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。 +Differences: Static code blocks execute before non-static code blocks (static code block → non-static code block → constructor). A static code block only executes once during the first instantiation, while non-static code blocks execute every time a new instance is created. Non-static blocks can be defined within ordinary methods (though their utility is limited), while static code blocks cannot. -> **🐛 修正(参见:[issue #677](https://github.com/Snailclimb/JavaGuide/issues/677))**:静态代码块可能在第一次 new 对象的时候执行,但不一定只在第一次 new 的时候执行。比如通过 `Class.forName("ClassDemo")`创建 Class 对象的时候也会执行,即 new 或者 `Class.forName("ClassDemo")` 都会执行静态代码块。 -> 一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的。如果我们想要设计不需要创建对象就可以调用类中的方法,例如:`Arrays` 类,`Character` 类,`String` 类等,就需要使用静态方法, 两者的区别是 静态代码块是自动执行的而静态方法是被调用的时候才执行的. +> **🐛 Correction (refer to: [issue #677](https://github.com/Snailclimb/JavaGuide/issues/677))**: Static code blocks may be executed during the first instantiation of an object but are not guaranteed to execute only once upon instantiation. For example, executing `Class.forName("ClassDemo")` also triggers their execution; thus, both instantiation and `Class.forName("ClassDemo")` will lead to the static block execution. +> Generally, if certain code, such as commonly used project variables or objects, must execute when the project starts, static blocks should be used. Such code is executed automatically. Conversely, to design methods that can be called without creating an object, such as the `Arrays`, `Character`, or `String` classes, static methods must be employed. The distinction is that static blocks execute automatically, while static methods only execute when called. -Example: +Example: ```java public class Test { public Test() { - System.out.print("默认构造方法!--"); + System.out.print("Default constructor! --"); } - //非静态代码块 + // Non-static code block { - System.out.print("非静态代码块!--"); + System.out.print("Non-static code block! --"); } - //静态代码块 + // Static code block static { - System.out.print("静态代码块!--"); + System.out.print("Static code block! --"); } private static void test() { - System.out.print("静态方法中的内容! --"); + System.out.print("Content in static method! --"); { - System.out.print("静态方法中的代码块!--"); + System.out.print("Code block in static method! --"); } } public static void main(String[] args) { Test test = new Test(); - Test.test();//静态代码块!--静态方法中的内容! --静态方法中的代码块!-- + Test.test(); // Static code block! -- Content in static method! -- Code block in static method! -- } } ``` -上述代码输出: +The output of the above code is: ```plain -静态代码块!--非静态代码块!--默认构造方法!--静态方法中的内容! --静态方法中的代码块!-- +Static code block! -- Non-static code block! -- Default constructor! -- Content in static method! -- Code block in static method! -- ``` -当只执行 `Test.test();` 时输出: +When only executing `Test.test();`: ```plain -静态代码块!--静态方法中的内容! --静态方法中的代码块!-- +Static code block! -- Content in static method! -- Code block in static method! -- ``` -当只执行 `Test test = new Test();` 时输出: +When only executing `Test test = new Test();`: ```plain -静态代码块!--非静态代码块!--默认构造方法!-- +Static code block! -- Non-static code block! -- Default constructor! -- ``` -非静态代码块与构造函数的区别是:非静态代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。 +The difference between non-static blocks and constructors is that non-static blocks serve to initialize all objects uniformly, whereas constructors initialize corresponding objects. Given that there can be multiple constructors, the executed constructor determines the created object, but regardless of which object is established, the same constructor code block will always execute, meaning the initialization contents defined in the constructor block are common for different objects. -### 参考 +### References - - diff --git a/docs/java/basis/proxy.md b/docs/java/basis/proxy.md index 615b0f00e42..4998e938b11 100644 --- a/docs/java/basis/proxy.md +++ b/docs/java/basis/proxy.md @@ -1,39 +1,39 @@ --- -title: Java 代理模式详解 +title: Detailed Explanation of Java Proxy Pattern category: Java tag: - - Java基础 + - Java Basics --- -## 1. 代理模式 +## 1. Proxy Pattern -代理模式是一种比较好理解的设计模式。简单来说就是 **我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。** +The proxy pattern is a design pattern that is relatively easy to understand. Simply put, **we use a proxy object to replace the access to the real object, which allows us to provide additional functionality and extend the real object's capabilities without modifying it.** -**代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。** +**The main purpose of the proxy pattern is to extend the functionality of the target object. For example, you can add some custom operations before and after a certain method of the target object is executed.** -举个例子:新娘找来了自己的姨妈来代替自己处理新郎的提问,新娘收到的提问都是经过姨妈处理过滤之后的。姨妈在这里就可以看作是代理你的代理对象,代理的行为(方法)是接收和回复新郎的提问。 +For instance: a bride asks her aunt to handle the groom's inquiries on her behalf. The inquiries that the bride receives have all been filtered by the aunt. The aunt can be viewed as the proxy object, whose role (method) is to receive and respond to the groom's questions. ![Understanding the Proxy Design Pattern | by Mithun Sasidharan | Medium](https://oss.javaguide.cn/2020-8/1*DjWCgTFm-xqbhbNQVsaWQw.png)

https://medium.com/@mithunsasidharan/understanding-the-proxy-design-pattern-5e63fe38052a

-代理模式有静态代理和动态代理两种实现方式,我们 先来看一下静态代理模式的实现。 +There are two implementations of the proxy pattern: static proxy and dynamic proxy. Let's first look at the implementation of static proxy. -## 2. 静态代理 +## 2. Static Proxy -**静态代理中,我们对目标对象的每个方法的增强都是手动完成的(_后面会具体演示代码_),非常不灵活(_比如接口一旦新增加方法,目标对象和代理对象都要进行修改_)且麻烦(_需要对每个目标类都单独写一个代理类_)。** 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。 +**In static proxy, the enhancement for each method of the target object is done manually (_code demonstration will follow_), which is very inflexible (_for example, if the interface adds a new method, both the target object and the proxy object need to be modified_) and troublesome (_a separate proxy class needs to be written for each target class_).** The actual application of static proxies is very rare; you hardly see static proxies used in daily development. -上面我们是从实现和应用角度来说的静态代理,从 JVM 层面来说, **静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。** +The above perspective is from the implementation and application angle of static proxy. From the JVM perspective, **the static proxy generates the interface, implementation class, and proxy class into actual class files at compile time.** -静态代理实现步骤: +Steps to implement static proxy: -1. 定义一个接口及其实现类; -2. 创建一个代理类同样实现这个接口 -3. 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。 +1. Define an interface and its implementation class. +1. Create a proxy class that also implements this interface. +1. Inject the target object into the proxy class, and then call the corresponding method of the target class within the proxy class's corresponding method. This way, we can shield the access to the target object through the proxy class and perform some desired operations before and after the execution of the target method. -下面通过代码展示! +Let's demonstrate this with code! -**1.定义发送短信的接口** +**1. Define the interface for sending messages** ```java public interface SmsService { @@ -41,7 +41,7 @@ public interface SmsService { } ``` -**2.实现发送短信的接口** +**2. Implement the interface for sending messages** ```java public class SmsServiceImpl implements SmsService { @@ -52,7 +52,7 @@ public class SmsServiceImpl implements SmsService { } ``` -**3.创建代理类并同样实现发送短信的接口** +**3. Create a proxy class that also implements the interface for sending messages** ```java public class SmsProxy implements SmsService { @@ -65,17 +65,17 @@ public class SmsProxy implements SmsService { @Override public String send(String message) { - //调用方法之前,我们可以添加自己的操作 + // Before calling the method, we can add our own operations System.out.println("before method send()"); smsService.send(message); - //调用方法之后,我们同样可以添加自己的操作 + // After calling the method, we can also add our own operations System.out.println("after method send()"); return null; } } ``` -**4.实际使用** +**4. Actual usage** ```java public class Main { @@ -87,7 +87,7 @@ public class Main { } ``` -运行上述代码之后,控制台打印出: +After running the above code, the console prints: ```bash before method send() @@ -95,31 +95,31 @@ send message:java after method send() ``` -可以输出结果看出,我们已经增加了 `SmsServiceImpl` 的`send()`方法。 +From the output, we can see that we have added functionality to the `send()` method of `SmsServiceImpl`. -## 3. 动态代理 +## 3. Dynamic Proxy -相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( _CGLIB 动态代理机制_)。 +Compared to static proxy, dynamic proxy is more flexible. We do not need to create a separate proxy class for each target class, nor do we need to implement an interface; we can directly proxy the implementation class (_CGLIB dynamic proxy mechanism_). -**从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。** +**From the JVM perspective, dynamic proxy dynamically generates bytecode for classes at runtime and loads it into the JVM.** -说到动态代理,Spring AOP、RPC 框架应该是两个不得不提的,它们的实现都依赖了动态代理。 +When it comes to dynamic proxy, Spring AOP and RPC frameworks are two notable examples that rely on dynamic proxy. -**动态代理在我们日常开发中使用的相对较少,但是在框架中的几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助。** +**Dynamic proxy is used relatively less in our daily development, but it is an essential technology in frameworks. Understanding dynamic proxies is very helpful for us to comprehend and learn the principles of various frameworks.** -就 Java 来说,动态代理的实现方式有很多种,比如 **JDK 动态代理**、**CGLIB 动态代理**等等。 +In Java, there are many ways to implement dynamic proxies, such as **JDK Dynamic Proxy** and **CGLIB Dynamic Proxy**. -[guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) 使用的是 JDK 动态代理,我们先来看看 JDK 动态代理的使用。 +[guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) uses JDK dynamic proxy; let's first take a look at how to use JDK dynamic proxy. -另外,虽然 [guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) 没有用到 **CGLIB 动态代理** ,我们这里还是简单介绍一下其使用以及和**JDK 动态代理**的对比。 +Additionally, although [guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) does not utilize **CGLIB Dynamic Proxy**, we will briefly introduce its usage and compare it with **JDK Dynamic Proxy**. -### 3.1. JDK 动态代理机制 +### 3.1. JDK Dynamic Proxy Mechanism -#### 3.1.1. 介绍 +#### 3.1.1. Introduction -**在 Java 动态代理机制中 `InvocationHandler` 接口和 `Proxy` 类是核心。** +**In Java's dynamic proxy mechanism, the `InvocationHandler` interface and the `Proxy` class are the core components.** -`Proxy` 类中使用频率最高的方法是:`newProxyInstance()` ,这个方法主要用来生成一个代理对象。 +The most frequently used method in the `Proxy` class is: `newProxyInstance()`, which is primarily used to generate a proxy object. ```java public static Object newProxyInstance(ClassLoader loader, @@ -131,44 +131,44 @@ after method send() } ``` -这个方法一共有 3 个参数: +This method has three parameters: -1. **loader** :类加载器,用于加载代理对象。 -2. **interfaces** : 被代理类实现的一些接口; -3. **h** : 实现了 `InvocationHandler` 接口的对象; +1. **loader**: The class loader used to load the proxy object. +1. **interfaces**: The interfaces implemented by the target class. +1. **h**: An object that implements the `InvocationHandler` interface. -要实现动态代理的话,还必须需要实现`InvocationHandler` 来自定义处理逻辑。 当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现`InvocationHandler` 接口类的 `invoke` 方法来调用。 +To implement dynamic proxy, you must also implement `InvocationHandler` to customize the handling logic. When our dynamic proxy object calls a method, the call will be forwarded to the `invoke` method of the class implementing the `InvocationHandler` interface. ```java public interface InvocationHandler { /** - * 当你使用代理对象调用方法的时候实际会调用到这个方法 + * This method is invoked when you call a method on the proxy object. */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; } ``` -`invoke()` 方法有下面三个参数: +The `invoke()` method has three parameters: -1. **proxy** :动态生成的代理类 -2. **method** : 与代理类对象调用的方法相对应 -3. **args** : 当前 method 方法的参数 +1. **proxy**: The dynamically generated proxy class. +1. **method**: The method corresponding to the method called on the proxy class object. +1. **args**: The parameters for the current method. -也就是说:**你通过`Proxy` 类的 `newProxyInstance()` 创建的代理对象在调用方法的时候,实际会调用到实现`InvocationHandler` 接口的类的 `invoke()`方法。** 你可以在 `invoke()` 方法中自定义处理逻辑,比如在方法执行前后做什么事情。 +In other words, **the proxy object created by the `Proxy` class's `newProxyInstance()` will actually invoke the `invoke()` method of the class implementing the `InvocationHandler` interface when calling a method.** You can customize the handling logic in the `invoke()` method, such as performing tasks before and after method execution. -#### 3.1.2. JDK 动态代理类使用步骤 +#### 3.1.2. Steps to Use JDK Dynamic Proxy -1. 定义一个接口及其实现类; -2. 自定义 `InvocationHandler` 并重写`invoke`方法,在 `invoke` 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑; -3. 通过 `Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)` 方法创建代理对象; +1. Define an interface and its implementation class. +1. Customize the `InvocationHandler` and override the `invoke` method, where we will call the actual method (the method of the proxied class) and customize some handling logic. +1. Create a proxy object using the `Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)` method. -#### 3.1.3. 代码示例 +#### 3.1.3. Code Example -这样说可能会有点空洞和难以理解,我上个例子,大家感受一下吧! +This might seem a bit abstract and hard to understand, so let's use an example to illustrate! -**1.定义发送短信的接口** +**1. Define the interface for sending messages** ```java public interface SmsService { @@ -176,7 +176,7 @@ public interface SmsService { } ``` -**2.实现发送短信的接口** +**2. Implement the interface for sending messages** ```java public class SmsServiceImpl implements SmsService { @@ -187,7 +187,7 @@ public class SmsServiceImpl implements SmsService { } ``` -**3.定义一个 JDK 动态代理类** +**3. Define a JDK dynamic proxy class** ```java import java.lang.reflect.InvocationHandler; @@ -200,7 +200,7 @@ import java.lang.reflect.Method; */ public class DebugInvocationHandler implements InvocationHandler { /** - * 代理类中的真实对象 + * The real object in the proxy class. */ private final Object target; @@ -210,43 +210,42 @@ public class DebugInvocationHandler implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException { - //调用方法之前,我们可以添加自己的操作 + // Before calling the method, we can add our own operations System.out.println("before method " + method.getName()); Object result = method.invoke(target, args); - //调用方法之后,我们同样可以添加自己的操作 + // After calling the method, we can also add our own operations System.out.println("after method " + method.getName()); return result; } } - ``` -`invoke()` 方法: 当我们的动态代理对象调用原生方法的时候,最终实际上调用到的是 `invoke()` 方法,然后 `invoke()` 方法代替我们去调用了被代理对象的原生方法。 +The `invoke()` method: When our dynamic proxy object calls the actual method, it ultimately calls the `invoke()` method, which then calls the original method of the proxied object. -**4.获取代理对象的工厂类** +**4. Factory class to get the proxy object** ```java public class JdkProxyFactory { public static Object getProxy(Object target) { return Proxy.newProxyInstance( - target.getClass().getClassLoader(), // 目标类的类加载器 - target.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个 - new DebugInvocationHandler(target) // 代理对象对应的自定义 InvocationHandler + target.getClass().getClassLoader(), // The class loader of the target class + target.getClass().getInterfaces(), // Interfaces that the proxy needs to implement, can specify multiple + new DebugInvocationHandler(target) // Custom InvocationHandler corresponding to the proxy object ); } } ``` -`getProxy()`:主要通过`Proxy.newProxyInstance()`方法获取某个类的代理对象 +`getProxy()`: Primarily obtains the proxy object of a certain class through the `Proxy.newProxyInstance()` method. -**5.实际使用** +**5. Actual usage** ```java SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl()); smsService.send("java"); ``` -运行上述代码之后,控制台打印出: +After running the above code, the console prints: ```plain before method send @@ -254,45 +253,43 @@ send message:java after method send ``` -### 3.2. CGLIB 动态代理机制 +### 3.2. CGLIB Dynamic Proxy Mechanism -#### 3.2.1. 介绍 +#### 3.2.1. Introduction -**JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。** +**The most critical issue with JDK dynamic proxy is that it can only proxy classes that implement interfaces.** -**为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。** +**To resolve this issue, we can use the CGLIB dynamic proxy mechanism.** -[CGLIB](https://github.com/cglib/cglib)(_Code Generation Library_)是一个基于[ASM](http://www.baeldung.com/java-asm)的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。很多知名的开源框架都使用到了[CGLIB](https://github.com/cglib/cglib), 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。 +[CGLIB](https://github.com/cglib/cglib) (_Code Generation Library_) is a bytecode generation library based on [ASM](http://www.baeldung.com/java-asm). It allows us to modify and dynamically generate bytecode at runtime. CGLIB implements proxying through inheritance. Many well-known open-source frameworks utilize [CGLIB](https://github.com/cglib/cglib), such as Spring's AOP module: if the target object implements an interface, JDK dynamic proxy is used by default; otherwise, CGLIB dynamic proxy is used. -**在 CGLIB 动态代理机制中 `MethodInterceptor` 接口和 `Enhancer` 类是核心。** +**In the CGLIB dynamic proxy mechanism, the `MethodInterceptor` interface and the `Enhancer` class are the core components.** -你需要自定义 `MethodInterceptor` 并重写 `intercept` 方法,`intercept` 用于拦截增强被代理类的方法。 +You need to customize `MethodInterceptor` and override the `intercept` method, which is used to intercept and enhance the proxied class's method. ```java -public interface MethodInterceptor -extends Callback{ - // 拦截被代理类中的方法 - public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,MethodProxy proxy) throws Throwable; +public interface MethodInterceptor extends Callback { + // Intercept methods in the proxied class + public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable; } - ``` -1. **obj** : 被代理的对象(需要增强的对象) -2. **method** : 被拦截的方法(需要增强的方法) -3. **args** : 方法入参 -4. **proxy** : 用于调用原始方法 +1. **obj**: The object being proxied (the object to be enhanced). +1. **method**: The intercepted method (the method to be enhanced). +1. **args**: Method parameters. +1. **proxy**: Used to call the original method. -你可以通过 `Enhancer`类来动态获取被代理类,当代理类调用方法的时候,实际调用的是 `MethodInterceptor` 中的 `intercept` 方法。 +You can dynamically obtain the proxied class through the `Enhancer` class. When the proxy class calls a method, the actual call is made to the `intercept` method in `MethodInterceptor`. -#### 3.2.2. CGLIB 动态代理类使用步骤 +#### 3.2.2. Steps to Use CGLIB Dynamic Proxy -1. 定义一个类; -2. 自定义 `MethodInterceptor` 并重写 `intercept` 方法,`intercept` 用于拦截增强被代理类的方法,和 JDK 动态代理中的 `invoke` 方法类似; -3. 通过 `Enhancer` 类的 `create()`创建代理类; +1. Define a class. +1. Customize `MethodInterceptor` and override the `intercept` method, similar to the `invoke` method in JDK dynamic proxy. +1. Create a proxy class using the `Enhancer` class's `create()` method. -#### 3.2.3. 代码示例 +#### 3.2.3. Code Example -不同于 JDK 动态代理不需要额外的依赖。[CGLIB](https://github.com/cglib/cglib)(_Code Generation Library_) 实际是属于一个开源项目,如果你要使用它的话,需要手动添加相关依赖。 +Unlike JDK dynamic proxy, CGLIB dynamic proxy doesn't require additional dependencies. [CGLIB](https://github.com/cglib/cglib) (_Code Generation Library_) is an open-source project; if you wish to use it, you need to manually add the relevant dependencies. ```xml @@ -302,7 +299,7 @@ extends Callback{ ``` -**1.实现一个使用阿里云发送短信的类** +**1. Implement a class for sending messages using Aliyun** ```java package github.javaguide.dynamicProxy.cglibDynamicProxy; @@ -315,7 +312,7 @@ public class AliSmsService { } ``` -**2.自定义 `MethodInterceptor`(方法拦截器)** +**2. Customize `MethodInterceptor` (method interceptor)** ```java import net.sf.cglib.proxy.MethodInterceptor; @@ -324,31 +321,29 @@ import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** - * 自定义MethodInterceptor + * Custom MethodInterceptor */ public class DebugMethodInterceptor implements MethodInterceptor { - /** - * @param o 被代理的对象(需要增强的对象) - * @param method 被拦截的方法(需要增强的方法) - * @param args 方法入参 - * @param methodProxy 用于调用原始方法 + * @param o The object being proxied (the object to be enhanced). + * @param method The intercepted method (the method to be enhanced). + * @param args Method parameters. + * @param methodProxy Used to call the original method. */ @Override public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { - //调用方法之前,我们可以添加自己的操作 + // Before calling the method, we can add our own operations System.out.println("before method " + method.getName()); Object object = methodProxy.invokeSuper(o, args); - //调用方法之后,我们同样可以添加自己的操作 + // After calling the method, we can also add our own operations System.out.println("after method " + method.getName()); return object; } - } ``` -**3.获取代理类** +**3. Obtain the proxy class** ```java import net.sf.cglib.proxy.Enhancer; @@ -356,28 +351,28 @@ import net.sf.cglib.proxy.Enhancer; public class CglibProxyFactory { public static Object getProxy(Class clazz) { - // 创建动态代理增强类 + // Create dynamic proxy enhancement class Enhancer enhancer = new Enhancer(); - // 设置类加载器 + // Set the class loader enhancer.setClassLoader(clazz.getClassLoader()); - // 设置被代理类 + // Set the proxied class enhancer.setSuperclass(clazz); - // 设置方法拦截器 + // Set the method interceptor enhancer.setCallback(new DebugMethodInterceptor()); - // 创建代理类 + // Create the proxy class return enhancer.create(); } } ``` -**4.实际使用** +**4. Actual usage** ```java AliSmsService aliSmsService = (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class); aliSmsService.send("java"); ``` -运行上述代码之后,控制台打印出: +After running the above code, the console prints: ```bash before method send @@ -385,20 +380,20 @@ send message:java after method send ``` -### 3.3. JDK 动态代理和 CGLIB 动态代理对比 +### 3.3. Comparison Between JDK Dynamic Proxy and CGLIB Dynamic Proxy -1. **JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。** 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。 -2. 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。 +1. **JDK dynamic proxy can only proxy classes that implement interfaces or directly proxy interfaces, while CGLIB can proxy classes that do not implement any interfaces.** Additionally, CGLIB dynamic proxy intercepts method calls by generating a subclass of the proxied class, so it cannot proxy methods or classes declared as final. +1. In terms of efficiency, JDK dynamic proxy is generally more efficient. As JDK versions improve, this advantage becomes even more pronounced. -## 4. 静态代理和动态代理的对比 +## 4. Comparison Between Static Proxy and Dynamic Proxy -1. **灵活性**:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的! -2. **JVM 层面**:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。 +1. **Flexibility**: Dynamic proxy is more flexible; there is no need to implement an interface, and it can directly proxy the implementation class without creating a separate proxy class for each target class. Additionally, in static proxy, if a new method is added to the interface, modifications need to be made to both the target object and the proxy object, which is cumbersome. +1. **JVM Perspective**: Static proxy generates the interface, implementation class, and proxy class into actual class files at compile time, while dynamic proxy dynamically generates class bytecode at runtime and loads it into the JVM. -## 5. 总结 +## 5. Summary -这篇文章中主要介绍了代理模式的两种实现:静态代理以及动态代理。涵盖了静态代理和动态代理实战、静态代理和动态代理的区别、JDK 动态代理和 Cglib 动态代理区别等内容。 +This article mainly introduces two implementations of the proxy pattern: static proxy and dynamic proxy. It covers the practical applications of static and dynamic proxies, the differences between them, and the distinctions between JDK and CGLIB dynamic proxies. -文中涉及到的所有源码,你可以在这里找到:[https://github.com/Snailclimb/guide-rpc-framework-learning/tree/master/src/main/java/github/javaguide/proxy](https://github.com/Snailclimb/guide-rpc-framework-learning/tree/master/src/main/java/github/javaguide/proxy) 。 +All the source code involved in this article can be found here: [https://github.com/Snailclimb/guide-rpc-framework-learning/tree/master/src/main/java/github/javaguide/proxy](https://github.com/Snailclimb/guide-rpc-framework-learning/tree/master/src/main/java/github/javaguide/proxy). diff --git a/docs/java/basis/reflection.md b/docs/java/basis/reflection.md index 3ce8ccab9a9..2fe38149659 100644 --- a/docs/java/basis/reflection.md +++ b/docs/java/basis/reflection.md @@ -1,32 +1,32 @@ --- -title: Java 反射机制详解 +title: Java Reflection Mechanism Detailed Explanation category: Java tag: - - Java基础 + - Java Basics --- -## 何为反射? +## What is Reflection? -如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。 +If you have studied the underlying principles of frameworks or have written your own frameworks, you must be familiar with the concept of reflection. -反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。 +Reflection is called the soul of frameworks mainly because it gives us the ability to analyze classes and execute methods in those classes at runtime. -通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。 +With reflection, you can obtain all properties and methods of any class, and you can also invoke these methods and access these properties. -## 反射的应用场景了解么? +## Do You Know the Application Scenarios of Reflection? -像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。 +Most of the time, we are writing business code and rarely encounter scenarios where we directly use reflection. -但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。 +However, this does not mean that reflection is useless. On the contrary, it is precisely because of reflection that you can easily use various frameworks. Frameworks like Spring/Spring Boot, MyBatis, etc., make extensive use of reflection. -**这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。** +**These frameworks also use dynamic proxies extensively, and the implementation of dynamic proxies relies on reflection.** -比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 `Method` 来调用指定的方法。 +For example, below is sample code that implements dynamic proxies using JDK, where the reflection class `Method` is used to invoke the specified method. ```java public class DebugInvocationHandler implements InvocationHandler { /** - * 代理类中的真实对象 + * The real object in the proxy class */ private final Object target; @@ -34,7 +34,6 @@ public class DebugInvocationHandler implements InvocationHandler { this.target = target; } - public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException { System.out.println("before method " + method.getName()); Object result = method.invoke(target, args); @@ -42,59 +41,58 @@ public class DebugInvocationHandler implements InvocationHandler { return result; } } - ``` -另外,像 Java 中的一大利器 **注解** 的实现也用到了反射。 +Additionally, one of Java's powerful features, **annotations**, also relies on reflection for its implementation. -为什么你使用 Spring 的时候 ,一个`@Component`注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 `@Value`注解就读取到配置文件中的值呢?究竟是怎么起作用的呢? +Why is it that when you use Spring, a `@Component` annotation declares a class as a Spring Bean? Why can you read values from a configuration file using a `@Value` annotation? How does this work? -这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。 +All of this is because you can analyze classes based on reflection and retrieve annotations from classes, properties, methods, and their parameters. Once you retrieve the annotations, you can perform further processing. -## 谈谈反射机制的优缺点 +## Discussing the Advantages and Disadvantages of Reflection Mechanism -**优点**:可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利 +**Advantages**: Allows our code to be more flexible and provides convenience for various frameworks to offer out-of-the-box functionality. -**缺点**:让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。相关阅读:[Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) +**Disadvantages**: Gives us the ability to analyze operational classes at runtime, which also increases security issues. For example, it can bypass safety checks for generic parameters (safety checks for generic parameters occur at compile time). Additionally, the performance of reflection is somewhat lower, but it does not significantly affect the frameworks in practice. Related reading: [Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) -## 反射实战 +## Reflection Practice -### 获取 Class 对象的四种方式 +### Four Ways to Obtain Class Objects -如果我们动态获取到这些信息,我们需要依靠 Class 对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。Java 提供了四种方式获取 Class 对象: +If we want to dynamically obtain this information, we need to rely on the Class object. The Class object provides information about a class's methods, variables, and other details to the running program. Java provides four ways to obtain a Class object: -**1. 知道具体类的情况下可以使用:** +**1. When you know the specific class:** ```java Class alunbarClass = TargetObject.class; ``` -但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化 +However, we usually do not know the specific class and typically obtain Class objects by traversing through the classes in a package. Obtaining a Class object in this way does not involve initialization. -**2. 通过 `Class.forName()`传入类的全路径获取:** +**2. Use `Class.forName()` to get the full path of the class:** ```java Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject"); ``` -**3. 通过对象实例`instance.getClass()`获取:** +**3. Obtain via an object instance using `instance.getClass()`:** ```java TargetObject o = new TargetObject(); Class alunbarClass2 = o.getClass(); ``` -**4. 通过类加载器`xxxClassLoader.loadClass()`传入类路径获取:** +**4. Obtain via class loader using `xxxClassLoader.loadClass()`:** ```java ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject"); ``` -通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一系列步骤,静态代码块和静态对象不会得到执行 +Obtaining a Class object through the class loader does not involve initialization, meaning it does not perform a series of steps including initialization, and static code blocks and static objects will not execute. -### 反射的一些基本操作 +### Some Basic Operations of Reflection -1. 创建一个我们要使用反射操作的类 `TargetObject`。 +1. Create a class we want to use reflection with called `TargetObject`. ```java package cn.javaguide; @@ -116,7 +114,7 @@ public class TargetObject { } ``` -2. 使用反射操作这个类的方法以及属性 +2. Use reflection to operate on the methods and properties of this class. ```java package cn.javaguide; @@ -128,12 +126,12 @@ import java.lang.reflect.Method; public class Main { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException { /** - * 获取 TargetObject 类的 Class 对象并且创建 TargetObject 类实例 + * Obtain the Class object of TargetObject and create an instance of TargetObject */ Class targetClass = Class.forName("cn.javaguide.TargetObject"); TargetObject targetObject = (TargetObject) targetClass.newInstance(); /** - * 获取 TargetObject 类中定义的所有方法 + * Get all methods defined in the TargetObject class */ Method[] methods = targetClass.getDeclaredMethods(); for (Method method : methods) { @@ -141,7 +139,7 @@ public class Main { } /** - * 获取指定方法并调用 + * Obtain a specific method and invoke it */ Method publicMethod = targetClass.getDeclaredMethod("publicMethod", String.class); @@ -149,26 +147,25 @@ public class Main { publicMethod.invoke(targetObject, "JavaGuide"); /** - * 获取指定参数并对参数进行修改 + * Obtain a specific parameter and modify it */ Field field = targetClass.getDeclaredField("value"); - //为了对类中的参数进行修改我们取消安全检查 + // To modify the parameter in the class, we bypass the security check field.setAccessible(true); field.set(targetObject, "JavaGuide"); /** - * 调用 private 方法 + * Invoke the private method */ Method privateMethod = targetClass.getDeclaredMethod("privateMethod"); - //为了调用private方法我们取消安全检查 + // To invoke the private method, we bypass the security check privateMethod.setAccessible(true); privateMethod.invoke(targetObject); } } - ``` -输出内容: +Output: ```plain publicMethod @@ -177,8 +174,8 @@ I love JavaGuide value is JavaGuide ``` -**注意** : 有读者提到上面代码运行会抛出 `ClassNotFoundException` 异常,具体原因是你没有下面把这段代码的包名替换成自己创建的 `TargetObject` 所在的包 。 -可以参考: 这篇文章。 +**Note**: Some readers have mentioned that the above code will throw a `ClassNotFoundException`. The reason is that you have not replaced the package name in the following line of code with the package where your created `TargetObject` resides. +You can refer to: for this article. ```java Class targetClass = Class.forName("cn.javaguide.TargetObject"); diff --git a/docs/java/basis/serialization.md b/docs/java/basis/serialization.md index fb7d3b69e7f..b5d1ba4a49c 100644 --- a/docs/java/basis/serialization.md +++ b/docs/java/basis/serialization.md @@ -1,62 +1,62 @@ --- -title: Java 序列化详解 +title: Detailed Explanation of Java Serialization category: Java tag: - - Java基础 + - Java Basics --- -## 什么是序列化和反序列化? +## What are Serialization and Deserialization? -如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。 +If we need to persist Java objects, such as saving Java objects to files or transmitting Java objects over a network, these scenarios require serialization. -简单来说: +In simple terms: -- **序列化**:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式 -- **反序列化**:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程 +- **Serialization**: The process of converting a data structure or object into a format that can be stored or transmitted, typically a binary byte stream, but it can also be in text formats like JSON or XML. +- **Deserialization**: The process of converting the data generated during serialization back into its original data structure or object. -对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。 +In Java, an object (Object) refers to an instance of a class (Class) in an object-oriented programming language. However, in C++, a struct defines a data structure type, while a class corresponds to an object type. -下面是序列化和反序列化常见应用场景: +Here are some common application scenarios for serialization and deserialization: -- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化; -- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化; -- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化; -- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。 +- Objects need to be serialized before being transmitted over a network (for example, during remote procedure calls, RPC). After receiving the serialized object, it needs to be deserialized. +- Objects must be serialized before being stored in files, and deserialization is required to read objects from files. +- Serialization is needed before storing objects in databases (like Redis), and deserialization is required to read objects from cache databases. +- Objects must be serialized before being stored in memory, and deserialization is needed to read them back from memory. -维基百科是如是介绍序列化的: +Wikipedia describes serialization as follows: -> **序列化**(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。 +> **Serialization** in data processing in computer science refers to the process of converting a data structure or object state into a format that can be stored (for example, saved to a file, buffered, or sent over a network) so that it can be restored later in the same or another computer environment. When retrieving bytes according to the serialization format, it can be used to produce a copy with the same semantics as the original object. For many objects, such as complex objects with many references, this serialization reconstruction process is not easy. Object serialization in object-oriented programming does not encompass the functions related to the original object. This process is also known as object marshalling. The reverse operation of extracting a data structure from a series of bytes is deserialization (also known as unmarshalling). -综上:**序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。** +In summary: **The main purpose of serialization is to transmit objects over a network or to store objects in a file system, database, or memory.** ![](https://oss.javaguide.cn/github/javaguide/a478c74d-2c48-40ae-9374-87aacf05188c.png)

https://www.corejavaguru.com/java/serialization/interview-questions-1

-**序列化协议对应于 TCP/IP 4 层模型的哪一层?** +**Which layer of the TCP/IP model does the serialization protocol correspond to?** -我们知道网络通信的双方必须要采用和遵守相同的协议。TCP/IP 四层模型是下面这样的,序列化协议属于哪一层呢? +We know that both parties in network communication must adopt and adhere to the same protocol. The TCP/IP four-layer model is structured as follows; which layer does the serialization protocol belong to? -1. 应用层 -2. 传输层 -3. 网络层 -4. 网络接口层 +1. Application Layer +1. Transport Layer +1. Network Layer +1. Network Interface Layer -![TCP/IP 四层模型](https://oss.javaguide.cn/github/javaguide/cs-basics/network/tcp-ip-4-model.png) +![TCP/IP Four-Layer Model](https://oss.javaguide.cn/github/javaguide/cs-basics/network/tcp-ip-4-model.png) -如上图所示,OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么? +As shown in the diagram above, the presentation layer in the OSI seven-layer protocol model mainly processes user data from the application layer into binary streams. Conversely, it converts binary streams back into user data for the application layer. Doesn't this correspond to serialization and deserialization? -因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。 +Because the application layer, presentation layer, and session layer in the OSI seven-layer protocol model correspond to the application layer in the TCP/IP four-layer model, the serialization protocol is part of the application layer of the TCP/IP protocol. -## 常见序列化协议有哪些? +## What are the common serialization protocols? -JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。 +The serialization method provided by the JDK is generally not used because of its low efficiency and security issues. Commonly used serialization protocols include Hessian, Kryo, Protobuf, and ProtoStuff, all of which are based on binary serialization. -像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。 +Text-based serialization methods like JSON and XML are more readable but have poorer performance and are generally not chosen. -### JDK 自带的序列化方式 +### JDK Built-in Serialization Method -JDK 自带的序列化,只需实现 `java.io.Serializable`接口即可。 +To use the built-in serialization of the JDK, simply implement the `java.io.Serializable` interface. ```java @AllArgsConstructor @@ -75,151 +75,6 @@ public class RpcRequest implements Serializable { } ``` -**serialVersionUID 有什么作用?** +**What is the purpose of serialVersionUID?** -序列化号 `serialVersionUID` 属于版本控制的作用。反序列化时,会检查 `serialVersionUID` 是否和当前类的 `serialVersionUID` 一致。如果 `serialVersionUID` 不一致则会抛出 `InvalidClassException` 异常。强烈推荐每个序列化类都手动指定其 `serialVersionUID`,如果不手动指定,那么编译器会动态生成默认的 `serialVersionUID`。 - -**serialVersionUID 不是被 static 变量修饰了吗?为什么还会被“序列化”?** - -~~`static` 修饰的变量是静态变量,位于方法区,本身是不会被序列化的。 `static` 变量是属于类的而不是对象。你反序列之后,`static` 变量的值就像是默认赋予给了对象一样,看着就像是 `static` 变量被序列化,实际只是假象罢了。~~ - -**🐛 修正(参见:[issue#2174](https://github.com/Snailclimb/JavaGuide/issues/2174))**:`static` 修饰的变量是静态变量,属于类而非类的实例,本身是不会被序列化的。然而,`serialVersionUID` 是一个特例,`serialVersionUID` 的序列化做了特殊处理。当一个对象被序列化时,`serialVersionUID` 会被写入到序列化的二进制流中;在反序列化时,也会解析它并做一致性判断,以此来验证序列化对象的版本一致性。如果两者不匹配,反序列化过程将抛出 `InvalidClassException`,因为这通常意味着序列化的类的定义已经发生了更改,可能不再兼容。 - -官方说明如下: - -> A serializable class can declare its own serialVersionUID explicitly by declaring a field named `"serialVersionUID"` that must be `static`, `final`, and of type `long`; -> -> 如果想显式指定 `serialVersionUID` ,则需要在类中使用 `static` 和 `final` 关键字来修饰一个 `long` 类型的变量,变量名字必须为 `"serialVersionUID"` 。 - -也就是说,`serialVersionUID` 只是用来被 JVM 识别,实际并没有被序列化。 - -**如果有些字段不想进行序列化怎么办?** - -对于不想进行序列化的变量,可以使用 `transient` 关键字修饰。 - -`transient` 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 `transient` 修饰的变量值不会被持久化和恢复。 - -关于 `transient` 还有几点注意: - -- `transient` 只能修饰变量,不能修饰类和方法。 -- `transient` 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 `int` 类型,那么反序列后结果就是 `0`。 -- `static` 变量因为不属于任何对象(Object),所以无论有没有 `transient` 关键字修饰,均不会被序列化。 - -**为什么不推荐使用 JDK 自带的序列化?** - -我们很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因: - -- **不支持跨语言调用** : 如果调用的是其他语言开发的服务的时候就不支持了。 -- **性能差**:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。 -- **存在安全问题**:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。相关阅读:[应用安全:JAVA 反序列化漏洞之殇 - Cryin](https://cryin.github.io/blog/secure-development-java-deserialization-vulnerability/)、[Java 反序列化安全漏洞怎么回事? - Monica](https://www.zhihu.com/question/37562657/answer/1916596031)。 - -### Kryo - -Kryo 是一个高性能的序列化/反序列化工具,由于其变长存储特性并使用了字节码生成机制,拥有较高的运行速度和较小的字节码体积。 - -另外,Kryo 已经是一种非常成熟的序列化实现了,已经在 Twitter、Groupon、Yahoo 以及多个著名开源项目(如 Hive、Storm)中广泛的使用。 - -[guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) 就是使用的 kryo 进行序列化,序列化和反序列化相关的代码如下: - -```java -/** - * Kryo serialization class, Kryo serialization efficiency is very high, but only compatible with Java language - * - * @author shuang.kou - * @createTime 2020年05月13日 19:29:00 - */ -@Slf4j -public class KryoSerializer implements Serializer { - - /** - * Because Kryo is not thread safe. So, use ThreadLocal to store Kryo objects - */ - private final ThreadLocal kryoThreadLocal = ThreadLocal.withInitial(() -> { - Kryo kryo = new Kryo(); - kryo.register(RpcResponse.class); - kryo.register(RpcRequest.class); - return kryo; - }); - - @Override - public byte[] serialize(Object obj) { - try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - Output output = new Output(byteArrayOutputStream)) { - Kryo kryo = kryoThreadLocal.get(); - // Object->byte:将对象序列化为byte数组 - kryo.writeObject(output, obj); - kryoThreadLocal.remove(); - return output.toBytes(); - } catch (Exception e) { - throw new SerializeException("Serialization failed"); - } - } - - @Override - public T deserialize(byte[] bytes, Class clazz) { - try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); - Input input = new Input(byteArrayInputStream)) { - Kryo kryo = kryoThreadLocal.get(); - // byte->Object:从byte数组中反序列化出对象 - Object o = kryo.readObject(input, clazz); - kryoThreadLocal.remove(); - return clazz.cast(o); - } catch (Exception e) { - throw new SerializeException("Deserialization failed"); - } - } - -} -``` - -GitHub 地址:[https://github.com/EsotericSoftware/kryo](https://github.com/EsotericSoftware/kryo) 。 - -### Protobuf - -Protobuf 出自于 Google,性能还比较优秀,也支持多种语言,同时还是跨平台的。就是在使用中过于繁琐,因为你需要自己定义 IDL 文件和生成对应的序列化代码。这样虽然不灵活,但是,另一方面导致 protobuf 没有序列化漏洞的风险。 - -> Protobuf 包含序列化格式的定义、各种语言的库以及一个 IDL 编译器。正常情况下你需要定义 proto 文件,然后使用 IDL 编译器编译成你需要的语言 - -一个简单的 proto 文件如下: - -```protobuf -// protobuf的版本 -syntax = "proto3"; -// SearchRequest会被编译成不同的编程语言的相应对象,比如Java中的class、Go中的struct -message Person { - //string类型字段 - string name = 1; - // int 类型字段 - int32 age = 2; -} -``` - -GitHub 地址:[https://github.com/protocolbuffers/protobuf](https://github.com/protocolbuffers/protobuf)。 - -### ProtoStuff - -由于 Protobuf 的易用性较差,它的哥哥 Protostuff 诞生了。 - -protostuff 基于 Google protobuf,但是提供了更多的功能和更简易的用法。虽然更加易用,但是不代表 ProtoStuff 性能更差。 - -GitHub 地址:[https://github.com/protostuff/protostuff](https://github.com/protostuff/protostuff)。 - -### Hessian - -Hessian 是一个轻量级的,自定义描述的二进制 RPC 协议。Hessian 是一个比较老的序列化实现了,并且同样也是跨语言的。 - -![](https://oss.javaguide.cn/github/javaguide/8613ec4c-bde5-47bf-897e-99e0f90b9fa3.png) - -Dubbo2.x 默认启用的序列化方式是 Hessian2 ,但是,Dubbo 对 Hessian2 进行了修改,不过大体结构还是差不多。 - -### 总结 - -Kryo 是专门针对 Java 语言序列化方式并且性能非常好,如果你的应用是专门针对 Java 语言的话可以考虑使用,并且 Dubbo 官网的一篇文章中提到说推荐使用 Kryo 作为生产环境的序列化方式。(文章地址:)。 - -![](https://oss.javaguide.cn/github/javaguide/java/569e541a-22b2-4846-aa07-0ad479f07440-20230814090158124.png) - -像 Protobuf、 ProtoStuff、hessian 这类都是跨语言的序列化方式,如果有跨语言需求的话可以考虑使用。 - -除了我上面介绍到的序列化方式的话,还有像 Thrift,Avro 这些。 - - +The serialization identifier `serialVersionUID` serves a version control purpose. During deserialization, it checks whether the `serialVersionUID` matches the current class's `serialVersionUID`. If they do not match, an \`InvalidClass diff --git a/docs/java/basis/spi.md b/docs/java/basis/spi.md index a2a7bccb7d3..96996bec553 100644 --- a/docs/java/basis/spi.md +++ b/docs/java/basis/spi.md @@ -1,565 +1,50 @@ --- -title: Java SPI 机制详解 +title: Detailed Explanation of Java SPI Mechanism category: Java tag: - - Java基础 + - Java Basics head: - - - meta - - name: keywords - content: Java SPI机制 - - - meta - - name: description - content: SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。 + - - meta + - name: keywords + content: Java SPI Mechanism + - - meta + - name: description + content: SPI stands for Service Provider Interface, which literally means "the interface for service providers." My understanding is that it is an interface specifically provided for service providers or developers who extend the framework's functionality. SPI separates the service interface from the specific service implementation, decoupling the service caller from the service implementer, which can enhance the program's extensibility and maintainability. Modifying or replacing the service implementation does not require changes to the caller. --- -> 本文来自 [Kingshion](https://github.com/jjx0708) 投稿。欢迎更多朋友参与到 JavaGuide 的维护工作,这是一件非常有意义的事情。详细信息请看:[JavaGuide 贡献指南](https://javaguide.cn/javaguide/contribution-guideline.html) 。 +> This article is contributed by [Kingshion](https://github.com/jjx0708). More friends are welcome to participate in the maintenance of JavaGuide, which is a very meaningful endeavor. For more details, please see: [JavaGuide Contribution Guidelines](https://javaguide.cn/javaguide/contribution-guideline.html). -面向对象设计鼓励模块间基于接口而非具体实现编程,以降低模块间的耦合,遵循依赖倒置原则,并支持开闭原则(对扩展开放,对修改封闭)。然而,直接依赖具体实现会导致在替换实现时需要修改代码,违背了开闭原则。为了解决这个问题,SPI 应运而生,它提供了一种服务发现机制,允许在程序外部动态指定具体实现。这与控制反转(IoC)的思想相似,将组件装配的控制权移交给了程序之外。 +Object-oriented design encourages programming between modules based on interfaces rather than concrete implementations to reduce coupling between modules, adhere to the dependency inversion principle, and support the open-closed principle (open for extension, closed for modification). However, directly depending on concrete implementations can lead to code modifications when replacing implementations, which violates the open-closed principle. To solve this problem, SPI was born, providing a service discovery mechanism that allows for dynamically specifying concrete implementations outside of the program. This is similar to the concept of Inversion of Control (IoC), which hands over the control of component assembly to outside the program. -SPI 机制也解决了 Java 类加载体系中双亲委派模型带来的限制。[双亲委派模型](https://javaguide.cn/java/jvm/classloader.html)虽然保证了核心库的安全性和一致性,但也限制了核心库或扩展库加载应用程序类路径上的类(通常由第三方实现)。SPI 允许核心或扩展库定义服务接口,第三方开发者提供并部署实现,SPI 服务加载机制则在运行时动态发现并加载这些实现。例如,JDBC 4.0 及之后版本利用 SPI 自动发现和加载数据库驱动,开发者只需将驱动 JAR 包放置在类路径下即可,无需使用`Class.forName()`显式加载驱动类。 +The SPI mechanism also addresses the limitations brought by the parent delegation model in the Java class loading system. The [parent delegation model](https://javaguide.cn/java/jvm/classloader.html) ensures the security and consistency of core libraries but also restricts core or extension libraries from loading classes on the application classpath (usually implemented by third parties). SPI allows core or extension libraries to define service interfaces, while third-party developers provide and deploy implementations. The SPI service loading mechanism dynamically discovers and loads these implementations at runtime. For example, JDBC 4.0 and later versions utilize SPI to automatically discover and load database drivers; developers only need to place the driver JAR file in the classpath without explicitly loading the driver class using `Class.forName()`. -## SPI 介绍 +## Introduction to SPI -### 何谓 SPI? +### What is SPI? -SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。 +SPI stands for Service Provider Interface, which literally means "the interface for service providers." My understanding is that it is an interface specifically provided for service providers or developers who extend the framework's functionality. -SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。 +SPI separates the service interface from the specific service implementation, decoupling the service caller from the service implementer, which can enhance the program's extensibility and maintainability. Modifying or replacing the service implementation does not require changes to the caller. -很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。 +Many frameworks use Java's SPI mechanism, such as the Spring framework, database driver loading, logging interfaces, and the extension implementations of Dubbo, among others. -### SPI 和 API 有什么区别? +### What is the difference between SPI and API? -**那 SPI 和 API 有啥区别?** +**So what is the difference between SPI and API?** -说到 SPI 就不得不说一下 API(Application Programming Interface) 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下: +When talking about SPI, we must mention API (Application Programming Interface). Broadly speaking, they both belong to interfaces and can easily be confused. Below is a diagram to illustrate: ![SPI VS API](https://oss.javaguide.cn/github/javaguide/java/basis/spi-vs-api.png) -一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。 +Generally, modules communicate through interfaces, so we introduce an "interface" between the service caller and the service implementer (also known as the service provider). -- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 **API**。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。 -- 当接口存在于调用方这边时,这就是 **SPI** 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。 +- When the implementer provides the interface and implementation, we can call the implementer's interface to gain the capabilities provided by the implementer, which is **API**. In this case, both the interface and implementation are located in the implementer's package. The caller invokes the implementer's functionality without needing to care about the specific implementation details. +- When the interface exists on the caller's side, this is **SPI**. The interface caller determines the interface rules, and then different vendors implement this interface according to the rules to provide services. -举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。 +For a simple and easy-to-understand example: Company H is a technology company that has designed a new chip and is now ready for mass production. There are several chip manufacturing companies on the market. At this point, as long as Company H specifies the production standards for the chip (defines the interface standards), these cooperating chip companies (service providers) will deliver their unique chips according to the standards (providing different implementation solutions, but yielding the same results). -## 实战演示 +## Practical Demonstration -SLF4J (Simple Logging Facade for Java)是 Java 的一个日志门面(接口),其具体实现有几种,比如:Logback、Log4j、Log4j2 等等,而且还可以切换,在切换日志具体实现的时候我们是不需要更改项目代码的,只需要在 Maven 依赖里面修改一些 pom 依赖就好了。 - -![](https://oss.javaguide.cn/github/javaguide/java/basis/spi/image-20220723213306039-165858318917813.png) - -这就是依赖 SPI 机制实现的,那我们接下来就实现一个简易版本的日志框架。 - -### Service Provider Interface - -新建一个 Java 项目 `service-provider-interface` 目录结构如下:(注意直接新建 Java 项目就好了,不用新建 Maven 项目,Maven 项目会涉及到一些编译配置,如果有私服的话,直接 deploy 会比较方便,但是没有的话,在过程中可能会遇到一些奇怪的问题。) - -```plain -│ service-provider-interface.iml -│ -├─.idea -│ │ .gitignore -│ │ misc.xml -│ │ modules.xml -│ └─ workspace.xml -│ -└─src - └─edu - └─jiangxuan - └─up - └─spi - Logger.java - LoggerService.java - Main.class -``` - -新建 `Logger` 接口,这个就是 SPI , 服务提供者接口,后面的服务提供者就要针对这个接口进行实现。 - -```java -package edu.jiangxuan.up.spi; - -public interface Logger { - void info(String msg); - void debug(String msg); -} -``` - -接下来就是 `LoggerService` 类,这个主要是为服务使用者(调用方)提供特定功能的。这个类也是实现 Java SPI 机制的关键所在,如果存在疑惑的话可以先往后面继续看。 - -```java -package edu.jiangxuan.up.spi; - -import java.util.ArrayList; -import java.util.List; -import java.util.ServiceLoader; - -public class LoggerService { - private static final LoggerService SERVICE = new LoggerService(); - - private final Logger logger; - - private final List loggerList; - - private LoggerService() { - ServiceLoader loader = ServiceLoader.load(Logger.class); - List list = new ArrayList<>(); - for (Logger log : loader) { - list.add(log); - } - // LoggerList 是所有 ServiceProvider - loggerList = list; - if (!list.isEmpty()) { - // Logger 只取一个 - logger = list.get(0); - } else { - logger = null; - } - } - - public static LoggerService getService() { - return SERVICE; - } - - public void info(String msg) { - if (logger == null) { - System.out.println("info 中没有发现 Logger 服务提供者"); - } else { - logger.info(msg); - } - } - - public void debug(String msg) { - if (loggerList.isEmpty()) { - System.out.println("debug 中没有发现 Logger 服务提供者"); - } - loggerList.forEach(log -> log.debug(msg)); - } -} -``` - -新建 `Main` 类(服务使用者,调用方),启动程序查看结果。 - -```java -package org.spi.service; - -public class Main { - public static void main(String[] args) { - LoggerService service = LoggerService.getService(); - - service.info("Hello SPI"); - service.debug("Hello SPI"); - } -} -``` - -程序结果: - -> info 中没有发现 Logger 服务提供者 -> debug 中没有发现 Logger 服务提供者 - -此时我们只是空有接口,并没有为 `Logger` 接口提供任何的实现,所以输出结果中没有按照预期打印相应的结果。 - -你可以使用命令或者直接使用 IDEA 将整个程序直接打包成 jar 包。 - -### Service Provider - -接下来新建一个项目用来实现 `Logger` 接口 - -新建项目 `service-provider` 目录结构如下: - -```plain -│ service-provider.iml -│ -├─.idea -│ │ .gitignore -│ │ misc.xml -│ │ modules.xml -│ └─ workspace.xml -│ -├─lib -│ service-provider-interface.jar -| -└─src - ├─edu - │ └─jiangxuan - │ └─up - │ └─spi - │ └─service - │ Logback.java - │ - └─META-INF - └─services - edu.jiangxuan.up.spi.Logger - -``` - -新建 `Logback` 类 - -```java -package edu.jiangxuan.up.spi.service; - -import edu.jiangxuan.up.spi.Logger; - -public class Logback implements Logger { - @Override - public void info(String s) { - System.out.println("Logback info 打印日志:" + s); - } - - @Override - public void debug(String s) { - System.out.println("Logback debug 打印日志:" + s); - } -} - -``` - -将 `service-provider-interface` 的 jar 导入项目中。 - -新建 lib 目录,然后将 jar 包拷贝过来,再添加到项目中。 - -![](https://oss.javaguide.cn/github/javaguide/java/basis/spi/523d5e25198444d3b112baf68ce49daetplv-k3u1fbpfcp-watermark.png) - -再点击 OK 。 - -![](https://oss.javaguide.cn/github/javaguide/java/basis/spi/f4ba0aa71e9b4d509b9159892a220850tplv-k3u1fbpfcp-watermark.png) - -接下来就可以在项目中导入 jar 包里面的一些类和方法了,就像 JDK 工具类导包一样的。 - -实现 `Logger` 接口,在 `src` 目录下新建 `META-INF/services` 文件夹,然后新建文件 `edu.jiangxuan.up.spi.Logger` (SPI 的全类名),文件里面的内容是:`edu.jiangxuan.up.spi.service.Logback` (Logback 的全类名,即 SPI 的实现类的包名 + 类名)。 - -**这是 JDK SPI 机制 ServiceLoader 约定好的标准。** - -这里先大概解释一下:Java 中的 SPI 机制就是在每次类加载的时候会先去找到 class 相对目录下的 `META-INF` 文件夹下的 services 文件夹下的文件,将这个文件夹下面的所有文件先加载到内存中,然后根据这些文件的文件名和里面的文件内容找到相应接口的具体实现类,找到实现类后就可以通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式拿到对应的实例对象,生成不同的实现。 - -所以会提出一些规范要求:文件名一定要是接口的全类名,然后里面的内容一定要是实现类的全类名,实现类可以有多个,直接换行就好了,多个实现类的时候,会一个一个的迭代加载。 - -接下来同样将 `service-provider` 项目打包成 jar 包,这个 jar 包就是服务提供方的实现。通常我们导入 maven 的 pom 依赖就有点类似这种,只不过我们现在没有将这个 jar 包发布到 maven 公共仓库中,所以在需要使用的地方只能手动的添加到项目中。 - -### 效果展示 - -为了更直观的展示效果,我这里再新建一个专门用来测试的工程项目:`java-spi-test` - -然后先导入 `Logger` 的接口 jar 包,再导入具体的实现类的 jar 包。 - -![](https://oss.javaguide.cn/github/javaguide/java/basis/spi/image-20220723215812708-165858469599214.png) - -新建 Main 方法测试: - -```java -package edu.jiangxuan.up.service; - -import edu.jiangxuan.up.spi.LoggerService; - -public class TestJavaSPI { - public static void main(String[] args) { - LoggerService loggerService = LoggerService.getService(); - loggerService.info("你好"); - loggerService.debug("测试Java SPI 机制"); - } -} -``` - -运行结果如下: - -> Logback info 打印日志:你好 -> Logback debug 打印日志:测试 Java SPI 机制 - -说明导入 jar 包中的实现类生效了。 - -如果我们不导入具体的实现类的 jar 包,那么此时程序运行的结果就会是: - -> info 中没有发现 Logger 服务提供者 -> debug 中没有发现 Logger 服务提供者 - -通过使用 SPI 机制,可以看出服务(`LoggerService`)和 服务提供者两者之间的耦合度非常低,如果说我们想要换一种实现,那么其实只需要修改 `service-provider` 项目中针对 `Logger` 接口的具体实现就可以了,只需要换一个 jar 包即可,也可以有在一个项目里面有多个实现,这不就是 SLF4J 原理吗? - -如果某一天需求变更了,此时需要将日志输出到消息队列,或者做一些别的操作,这个时候完全不需要更改 Logback 的实现,只需要新增一个服务实现(service-provider)可以通过在本项目里面新增实现也可以从外部引入新的服务实现 jar 包。我们可以在服务(LoggerService)中选择一个具体的 服务实现(service-provider) 来完成我们需要的操作。 - -那么接下来我们具体来说说 Java SPI 工作的重点原理—— **ServiceLoader** 。 - -## ServiceLoader - -### ServiceLoader 具体实现 - -想要使用 Java 的 SPI 机制是需要依赖 `ServiceLoader` 来实现的,那么我们接下来看看 `ServiceLoader` 具体是怎么做的: - -`ServiceLoader` 是 JDK 提供的一个工具类, 位于`package java.util;`包下。 - -```plain -A facility to load implementations of a service. -``` - -这是 JDK 官方给的注释:**一种加载服务实现的工具。** - -再往下看,我们发现这个类是一个 `final` 类型的,所以是不可被继承修改,同时它实现了 `Iterable` 接口。之所以实现了迭代器,是为了方便后续我们能够通过迭代的方式得到对应的服务实现。 - -```java -public final class ServiceLoader implements Iterable{ xxx...} -``` - -可以看到一个熟悉的常量定义: - -`private static final String PREFIX = "META-INF/services/";` - -下面是 `load` 方法:可以发现 `load` 方法支持两种重载后的入参; - -```java -public static ServiceLoader load(Class service) { - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - return ServiceLoader.load(service, cl); -} - -public static ServiceLoader load(Class service, - ClassLoader loader) { - return new ServiceLoader<>(service, loader); -} - -private ServiceLoader(Class svc, ClassLoader cl) { - service = Objects.requireNonNull(svc, "Service interface cannot be null"); - loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; - acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; - reload(); -} - -public void reload() { - providers.clear(); - lookupIterator = new LazyIterator(service, loader); -} -``` - -其解决第三方类加载的机制其实就蕴含在 `ClassLoader cl = Thread.currentThread().getContextClassLoader();` 中,`cl` 就是**线程上下文类加载器**(Thread Context ClassLoader)。这是每个线程持有的类加载器,JDK 的设计允许应用程序或容器(如 Web 应用服务器)设置这个类加载器,以便核心类库能够通过它来加载应用程序类。 - -线程上下文类加载器默认情况下是应用程序类加载器(Application ClassLoader),它负责加载 classpath 上的类。当核心库需要加载应用程序提供的类时,它可以使用线程上下文类加载器来完成。这样,即使是由引导类加载器加载的核心库代码,也能够加载并使用由应用程序类加载器加载的类。 - -根据代码的调用顺序,在 `reload()` 方法中是通过一个内部类 `LazyIterator` 实现的。先继续往下面看。 - -`ServiceLoader` 实现了 `Iterable` 接口的方法后,具有了迭代的能力,在这个 `iterator` 方法被调用时,首先会在 `ServiceLoader` 的 `Provider` 缓存中进行查找,如果缓存中没有命中那么则在 `LazyIterator` 中进行查找。 - -```java - -public Iterator iterator() { - return new Iterator() { - - Iterator> knownProviders - = providers.entrySet().iterator(); - - public boolean hasNext() { - if (knownProviders.hasNext()) - return true; - return lookupIterator.hasNext(); // 调用 LazyIterator - } - - public S next() { - if (knownProviders.hasNext()) - return knownProviders.next().getValue(); - return lookupIterator.next(); // 调用 LazyIterator - } - - public void remove() { - throw new UnsupportedOperationException(); - } - - }; -} -``` - -在调用 `LazyIterator` 时,具体实现如下: - -```java - -public boolean hasNext() { - if (acc == null) { - return hasNextService(); - } else { - PrivilegedAction action = new PrivilegedAction() { - public Boolean run() { - return hasNextService(); - } - }; - return AccessController.doPrivileged(action, acc); - } -} - -private boolean hasNextService() { - if (nextName != null) { - return true; - } - if (configs == null) { - try { - //通过PREFIX(META-INF/services/)和类名 获取对应的配置文件,得到具体的实现类 - String fullName = PREFIX + service.getName(); - if (loader == null) - configs = ClassLoader.getSystemResources(fullName); - else - configs = loader.getResources(fullName); - } catch (IOException x) { - fail(service, "Error locating configuration files", x); - } - } - while ((pending == null) || !pending.hasNext()) { - if (!configs.hasMoreElements()) { - return false; - } - pending = parse(service, configs.nextElement()); - } - nextName = pending.next(); - return true; -} - - -public S next() { - if (acc == null) { - return nextService(); - } else { - PrivilegedAction action = new PrivilegedAction() { - public S run() { - return nextService(); - } - }; - return AccessController.doPrivileged(action, acc); - } -} - -private S nextService() { - if (!hasNextService()) - throw new NoSuchElementException(); - String cn = nextName; - nextName = null; - Class c = null; - try { - c = Class.forName(cn, false, loader); - } catch (ClassNotFoundException x) { - fail(service, - "Provider " + cn + " not found"); - } - if (!service.isAssignableFrom(c)) { - fail(service, - "Provider " + cn + " not a subtype"); - } - try { - S p = service.cast(c.newInstance()); - providers.put(cn, p); - return p; - } catch (Throwable x) { - fail(service, - "Provider " + cn + " could not be instantiated", - x); - } - throw new Error(); // This cannot happen -} -``` - -可能很多人看这个会觉得有点复杂,没关系,我这边实现了一个简单的 `ServiceLoader` 的小模型,流程和原理都是保持一致的,可以先从自己实现一个简易版本的开始学: - -### 自己实现一个 ServiceLoader - -我先把代码贴出来: - -```java -package edu.jiangxuan.up.service; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.lang.reflect.Constructor; -import java.net.URL; -import java.net.URLConnection; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; - -public class MyServiceLoader { - - // 对应的接口 Class 模板 - private final Class service; - - // 对应实现类的 可以有多个,用 List 进行封装 - private final List providers = new ArrayList<>(); - - // 类加载器 - private final ClassLoader classLoader; - - // 暴露给外部使用的方法,通过调用这个方法可以开始加载自己定制的实现流程。 - public static MyServiceLoader load(Class service) { - return new MyServiceLoader<>(service); - } - - // 构造方法私有化 - private MyServiceLoader(Class service) { - this.service = service; - this.classLoader = Thread.currentThread().getContextClassLoader(); - doLoad(); - } - - // 关键方法,加载具体实现类的逻辑 - private void doLoad() { - try { - // 读取所有 jar 包里面 META-INF/services 包下面的文件,这个文件名就是接口名,然后文件里面的内容就是具体的实现类的路径加全类名 - Enumeration urls = classLoader.getResources("META-INF/services/" + service.getName()); - // 挨个遍历取到的文件 - while (urls.hasMoreElements()) { - // 取出当前的文件 - URL url = urls.nextElement(); - System.out.println("File = " + url.getPath()); - // 建立链接 - URLConnection urlConnection = url.openConnection(); - urlConnection.setUseCaches(false); - // 获取文件输入流 - InputStream inputStream = urlConnection.getInputStream(); - // 从文件输入流获取缓存 - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); - // 从文件内容里面得到实现类的全类名 - String className = bufferedReader.readLine(); - - while (className != null) { - // 通过反射拿到实现类的实例 - Class clazz = Class.forName(className, false, classLoader); - // 如果声明的接口跟这个具体的实现类是属于同一类型,(可以理解为Java的一种多态,接口跟实现类、父类和子类等等这种关系。)则构造实例 - if (service.isAssignableFrom(clazz)) { - Constructor constructor = (Constructor) clazz.getConstructor(); - S instance = constructor.newInstance(); - // 把当前构造的实例对象添加到 Provider的列表里面 - providers.add(instance); - } - // 继续读取下一行的实现类,可以有多个实现类,只需要换行就可以了。 - className = bufferedReader.readLine(); - } - } - } catch (Exception e) { - System.out.println("读取文件异常。。。"); - } - } - - // 返回spi接口对应的具体实现类列表 - public List getProviders() { - return providers; - } -} -``` - -关键信息基本已经通过代码注释描述出来了, - -主要的流程就是: - -1. 通过 URL 工具类从 jar 包的 `/META-INF/services` 目录下面找到对应的文件, -2. 读取这个文件的名称找到对应的 spi 接口, -3. 通过 `InputStream` 流将文件里面的具体实现类的全类名读取出来, -4. 根据获取到的全类名,先判断跟 spi 接口是否为同一类型,如果是的,那么就通过反射的机制构造对应的实例对象, -5. 将构造出来的实例对象添加到 `Providers` 的列表中。 - -## 总结 - -其实不难发现,SPI 机制的具体实现本质上还是通过反射完成的。即:**我们按照规定将要暴露对外使用的具体实现类在 `META-INF/services/` 文件下声明。** - -另外,SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的方式。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟咱们今天学得这个有些细微的区别,不过整体的原理都是一致的,相信大家通过对 JDK 中 SPI 机制的学习,能够一通百通,加深对其他高深框架的理解。 - -通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如: - -1. 遍历加载所有的实现类,这样效率还是相对较低的; -2. 当多个 `ServiceLoader` 同时 `load` 时,会有并发问题。 - - +SLF4J (Simple Logging Facade for Java) is a logging facade (interface) for Java diff --git a/docs/java/basis/syntactic-sugar.md b/docs/java/basis/syntactic-sugar.md index 3ce9bfc1099..2f03410bbaa 100644 --- a/docs/java/basis/syntactic-sugar.md +++ b/docs/java/basis/syntactic-sugar.md @@ -1,52 +1,52 @@ --- -title: Java 语法糖详解 +title: Detailed Explanation of Java Syntactic Sugar category: Java tag: - - Java基础 + - Java Basics head: - - - meta - - name: keywords - content: Java 语法糖 - - - meta - - name: description - content: 这篇文章介绍了 12 种 Java 中常用的语法糖。所谓语法糖就是提供给开发人员便于开发的一种语法而已。但是这种语法只有开发人员认识。要想被执行,需要进行解糖,即转成 JVM 认识的语法。当我们把语法糖解糖之后,你就会发现其实我们日常使用的这些方便的语法,其实都是一些其他更简单的语法构成的。有了这些语法糖,我们在日常开发的时候可以大大提升效率,但是同时也要避免过渡使用。使用之前最好了解下原理,避免掉坑。 + - - meta + - name: keywords + content: Java Syntactic Sugar + - - meta + - name: description + content: This article introduces 12 commonly used syntactic sugars in Java. Syntactic sugar refers to syntax that makes it easier for developers to code. However, this syntax is only recognized by developers. To be executed, it needs to be desugared, meaning it is converted to a syntax understood by the JVM. Once we desugar these syntactic sugars, we realize that many of the convenient syntaxes we use daily are actually composed of simpler syntactic structures. With these syntactic sugars, we can greatly improve efficiency in daily development, but we must also avoid overusing them. It’s best to understand the principles before using them, to avoid pitfalls. --- -> 作者:Hollis +> Author: Hollis > -> 原文: +> Original: -语法糖是大厂 Java 面试常问的一个知识点。 +Syntactic sugar is a common topic in Java interviews at major companies. -本文从 Java 编译原理角度,深入字节码及 class 文件,抽丝剥茧,了解 Java 中的语法糖原理及用法,帮助大家在学会如何使用 Java 语法糖的同时,了解这些语法糖背后的原理。 +This article delves into the principles and usages of syntactic sugar in Java from the perspective of Java compilation, exploring bytecode and class files to help everyone learn how to use Java syntactic sugar while understanding the principles behind it. -## 什么是语法糖? +## What is Syntactic Sugar? -**语法糖(Syntactic Sugar)** 也称糖衣语法,是英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。 +**Syntactic Sugar** refers to a term invented by British computer scientist Peter J. Landin, indicating certain syntax added to a programming language which does not affect the functionality of the language but makes it more convenient for programmers to use. In short, syntactic sugar makes programs more concise and readable. ![](https://oss.javaguide.cn/github/javaguide/java/basis/syntactic-sugar/image-20220818175953954.png) -> 有意思的是,在编程领域,除了语法糖,还有语法盐和语法糖精的说法,篇幅有限这里不做扩展了。 +> Interestingly, in the programming field, in addition to syntactic sugar, there are terms like "syntactic salt" and "syntactic sugar spirit," but we will not expand on that here due to space constraints. -我们所熟知的编程语言中几乎都有语法糖。作者认为,语法糖的多少是评判一个语言够不够牛逼的标准之一。很多人说 Java 是一个“低糖语言”,其实从 Java 7 开始 Java 语言层面上一直在添加各种糖,主要是在“Project Coin”项目下研发。尽管现在 Java 有人还是认为现在的 Java 是低糖,未来还会持续向着“高糖”的方向发展。 +Almost all programming languages we are familiar with contain some form of syntactic sugar. The author believes that the amount of syntactic sugar is one of the criteria for judging how impressive a language is. Many people say that Java is a "low-sugar language," but since Java 7, various sugars have been added at the language level, primarily developed under the "Project Coin" initiative. Despite some considering current Java to be low-sugar, it is expected to continue evolving towards "high-sugar" in the future. -## Java 中有哪些常见的语法糖? +## What Common Syntactic Sugars Exist in Java? -前面提到过,语法糖的存在主要是方便开发人员使用。但其实, **Java 虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖。** +As mentioned earlier, the existence of syntactic sugar primarily serves to facilitate developers. However, **the Java Virtual Machine does not support these syntactic sugars. These syntactic sugars will be restored to simple basic syntactic structures during the compilation phase, a process known as desugaring.** -说到编译,大家肯定都知道,Java 语言中,`javac`命令可以将后缀名为`.java`的源文件编译为后缀名为`.class`的可以运行于 Java 虚拟机的字节码。如果你去看`com.sun.tools.javac.main.JavaCompiler`的源码,你会发现在`compile()`中有一个步骤就是调用`desugar()`,这个方法就是负责解语法糖的实现的。 +Speaking of compilation, we all know that in Java, the `javac` command can compile source files with the `.java` suffix into bytecode with the `.class` suffix that can run on the Java Virtual Machine. If you look at the source code of `com.sun.tools.javac.main.JavaCompiler`, you will find that there is a step in `compile()` where it calls `desugar()`, which is responsible for implementing the desugaring of syntactic sugar. -Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。本文主要来分析下这些语法糖背后的原理。一步一步剥去糖衣,看看其本质。 +The most commonly used syntactic sugars in Java include generics, varargs, conditional compilation, automatic boxing and unboxing, inner classes, etc. This article will primarily analyze the principles behind these syntactic sugars, peeling off the layers of sugar step by step to reveal their essence. -我们这里会用到[反编译](https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650120609&idx=1&sn=5659f96310963ad57d55b48cee63c788&chksm=f36bbc80c41c3596a1e4bf9501c6280481f1b9e06d07af354474e6f3ed366fef016df673a7ba&scene=21#wechat_redirect),你可以通过 [Decompilers online](http://www.javadecompilers.com/) 对 Class 文件进行在线反编译。 +We will utilize [decompilation](https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650120609&idx=1&sn=5659f96310963ad57d55b48cee63c788&chksm=f36bbc80c41c3596a1e4bf9501c6280481f1b9e06d07af354474e6f3ed366fef016df673a7ba&scene=21#wechat_redirect), where you can perform online decompilation of class files through [Decompilers online](http://www.javadecompilers.com/). -### switch 支持 String 与枚举 +### Switch Supporting String and Enum -前面提到过,从 Java 7 开始,Java 语言中的语法糖在逐渐丰富,其中一个比较重要的就是 Java 7 中`switch`开始支持`String`。 +As mentioned earlier, starting from Java 7, the syntax sugar in Java has become increasingly rich, one notable addition being that `switch` now supports `String`. -在开始之前先科普下,Java 中的`switch`自身原本就支持基本类型。比如`int`、`char`等。对于`int`类型,直接进行数值的比较。对于`char`类型则是比较其 ascii 码。所以,对于编译器来说,`switch`中其实只能使用整型,任何类型的比较都要转换成整型。比如`byte`。`short`,`char`(ascii 码是整型)以及`int`。 +Before we begin, let's clarify that the `switch` statement in Java originally only supports primitive types such as `int`, `char`, etc. For the `int` type, values are compared directly. For the `char` type, the comparison is made using ASCII values. Thus, for the compiler, `switch` can only effectively use integer types; any type being compared must be converted to integer, such as `byte`, `short`, and `char` (ASCII codes are integers), along with `int`. -那么接下来看下`switch`对`String`的支持,有以下代码: +Now let's look at the support for `String` in `switch` with the following code: ```java public class switchDemoString { @@ -66,7 +66,7 @@ public class switchDemoString { } ``` -反编译后内容如下: +The decompiled content is as follows: ```java public class switchDemoString @@ -95,21 +95,21 @@ public class switchDemoString } ``` -看到这个代码,你知道原来 **字符串的 switch 是通过`equals()`和`hashCode()`方法来实现的。** 还好`hashCode()`方法返回的是`int`,而不是`long`。 +From this code, we can see that **the string `switch` is implemented using `equals()` and `hashCode()` methods.** Luckily, the `hashCode()` method returns an `int`, not `long`. -仔细看下可以发现,进行`switch`的实际是哈希值,然后通过使用`equals`方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行 `switch` 或者使用纯整数常量,但这也不是很差。 +Looking closely, it can be seen that the actual `switch` operation is on the hash value, then safe checks are performed using the `equals` method. This check is necessary because hash collisions can occur. Therefore, its performance is not as efficient as using enums in `switch` or using pure integer constants, but it's not overly inefficient either. -### 泛型 +### Generics -我们都知道,很多语言都是支持泛型的,但是很多人不知道的是,不同的编译器对于泛型的处理方式是不同的,通常情况下,一个编译器处理泛型有两种方式:`Code specialization`和`Code sharing`。C++和 C#是使用`Code specialization`的处理机制,而 Java 使用的是`Code sharing`的机制。 +We know that many languages support generics, but not everyone knows that different compilers handle generics in distinct ways. Typically, a compiler can use two strategies for generics: `Code specialization` and `Code sharing`. C++ and C# utilize `Code specialization`, while Java utilizes `Code sharing`. -> Code sharing 方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(`type erasue`)实现的。 +> The `Code sharing` method creates a unique bytecode representation for each generic type, mapping instances of that generic type to this unique representation. Mapping multiple generic class instances to a single bytecode representation is achieved through type erasure. -也就是说,**对于 Java 虚拟机来说,他根本不认识`Map map`这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖。** +In other words, **the Java Virtual Machine does not recognize syntax such as `Map map`. It needs to desugar the syntax during the compilation phase through type erasure.** -类型擦除的主要过程如下:1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。 2.移除所有的类型参数。 +The main process of type erasure is as follows: 1. Replace all generic parameters with their leftmost bounds (the top-level parent type). 2. Remove all type parameters. -以下代码: +The following code: ```java Map map = new HashMap(); @@ -118,7 +118,7 @@ map.put("wechat", "Hollis"); map.put("blog", "www.hollischuang.com"); ``` -解语法糖之后会变成: +After type erasure will be transformed into: ```java Map map = new HashMap(); @@ -127,7 +127,7 @@ map.put("wechat", "Hollis"); map.put("blog", "www.hollischuang.com"); ``` -以下代码: +The following code: ```java public static > A max(Collection xs) { @@ -142,7 +142,7 @@ public static > A max(Collection xs) { } ``` -类型擦除后会变成: +After type erasure will become: ```java public static Comparable max(Collection xs){ @@ -158,13 +158,13 @@ public static > A max(Collection xs) { } ``` -**虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的`Class`类对象。比如并不存在`List.class`或是`List.class`,而只有`List.class`。** +**There are no generics within the virtual machine, only plain classes and methods. All generic class type parameters are eradicated at compile time, and generic classes do not have their unique `Class` objects. For instance, there is no `List.class` or `List.class`, but only `List.class`.** -### 自动装箱与拆箱 +### Auto-boxing and Unboxing -自动装箱就是 Java 自动将原始类型值转换成对应的对象,比如将 int 的变量转换成 Integer 对象,这个过程叫做装箱,反之将 Integer 对象转换成 int 类型值,这个过程叫做拆箱。因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。原始类型 byte, short, char, int, long, float, double 和 boolean 对应的封装类为 Byte, Short, Character, Integer, Long, Float, Double, Boolean。 +Auto-boxing is when Java automatically converts a primitive type value into the corresponding object, such as converting an `int` variable into an `Integer` object. This process is known as boxing, while the opposite, converting an `Integer` object back into an `int` type value, is known as unboxing. Because both boxing and unboxing are automatically performed (not manually), they are referred to as auto-boxing and unboxing. The primitive types `byte`, `short`, `char`, `int`, `long`, `float`, `double`, and `boolean` correspond to the wrapper classes `Byte`, `Short`, `Character`, `Integer`, `Long`, `Float`, `Double`, `Boolean`. -先来看个自动装箱的代码: +First, let’s look at some code for auto-boxing: ```java public static void main(String[] args) { @@ -173,7 +173,7 @@ public static > A max(Collection xs) { } ``` -反编译后代码如下: +The decompiled code looks like this: ```java public static void main(String args[]) @@ -183,7 +183,7 @@ public static void main(String args[]) } ``` -再来看个自动拆箱的代码: +Now, let’s look at some code for auto-unboxing: ```java public static void main(String[] args) { @@ -193,7 +193,7 @@ public static void main(String[] args) { } ``` -反编译后代码如下: +The decompiled code is as follows: ```java public static void main(String args[]) @@ -203,20 +203,20 @@ public static void main(String args[]) } ``` -从反编译得到内容可以看出,在装箱的时候自动调用的是`Integer`的`valueOf(int)`方法。而在拆箱的时候自动调用的是`Integer`的`intValue`方法。 +From the decompiled content, we can observe that during boxing, the `Integer`'s `valueOf(int)` method is auto-invoked. In the unboxing case, the `Integer`'s `intValue` method is called automatically. -所以,**装箱过程是通过调用包装器的 valueOf 方法实现的,而拆箱过程是通过调用包装器的 xxxValue 方法实现的。** +Thus, **the boxing process is carried out by invoking the wrapper's `valueOf` method, while the unboxing process is implemented by calling the wrapper's `xxxValue` method.** -### 可变长参数 +### Varargs (Variable-Length Arguments) -可变参数(`variable arguments`)是在 Java 1.5 中引入的一个特性。它允许一个方法把任意数量的值作为参数。 +Varargs were introduced in Java 1.5 and allow a method to accept an arbitrary number of values as parameters. -看下以下可变参数代码,其中 `print` 方法接收可变参数: +Look at the following code with varargs, where the `print` method accepts variable-length arguments: ```java public static void main(String[] args) { - print("Holis", "公众号:Hollis", "博客:www.hollischuang.com", "QQ:907607222"); + print("Holis", "Public account: Hollis", "Blog: www.hollischuang.com", "QQ: 907607222"); } public static void print(String... strs) @@ -228,7 +228,7 @@ public static void print(String... strs) } ``` -反编译后代码: +The decompiled code is: ```java public static void main(String args[]) @@ -246,13 +246,13 @@ public static transient void print(String strs[]) } ``` -从反编译后代码可以看出,可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。(注:`trasient` 仅在修饰成员变量时有意义,此处 “修饰方法” 是由于在 javassist 中使用相同数值分别表示 `trasient` 以及 `vararg`,见 [此处](https://github.com/jboss-javassist/javassist/blob/7302b8b0a09f04d344a26ebe57f29f3db43f2a3e/src/main/javassist/bytecode/AccessFlag.java#L32)。) +From the decompiled code, we can see that when varargs are used, an array is first created with a length equal to the number of actual parameters passed to the method, then the parameter values are placed into this array, and this array is subsequently passed as a parameter to the called method. (Note: `transient` is meaningful only when modifying member variables; here, "method" is due to using the same value to represent `transient` and `vararg` in `javassist`; see [here](https://github.com/jboss-javassist/javassist/blob/7302b8b0a09f04d344a26ebe57f29f3db43f2a3e/src/main/javassist/bytecode/AccessFlag.java#L32).) -### 枚举 +### Enum -Java SE5 提供了一种新的类型-Java 的枚举类型,关键字`enum`可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。 +Java SE5 introduced a new type - Java’s enum type. The `enum` keyword allows creating a new type with a finite set of named values, which can be used as regular programming components, a very useful feature. -要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是`enum`吗?答案很明显不是,`enum`就和`class`一样,只是一个关键字,他并不是一个类,那么枚举是由什么类维护的呢,我们简单的写一个枚举: +To see the source code, we first need to have a class, so what kind of class is the enum type? Is it `enum`? The obvious answer is no; `enum` is just a keyword like `class`, it is not a class. So, which class maintains the enum? Let’s simply write an enum: ```java public enum t { @@ -260,7 +260,7 @@ public enum t { } ``` -然后我们使用反编译,看看这段代码到底是怎么实现的,反编译后代码内容如下: +Now, let’s decompile it to see how it is implemented. The decompiled code looks like this: ```java public final class T extends Enum @@ -297,15 +297,15 @@ public final class T extends Enum } ``` -通过反编译后代码我们可以看到,`public final class T extends Enum`,说明,该类是继承了`Enum`类的,同时`final`关键字告诉我们,这个类也是不能被继承的。 +From the decompiled code, we can see that `public final class T extends Enum` indicates this class extends `Enum`, and the `final` keyword tells us that this class cannot be further extended. -**当我们使用`enum`来定义一个枚举类型的时候,编译器会自动帮我们创建一个`final`类型的类继承`Enum`类,所以枚举类型不能被继承。** +**When we use `enum` to define an enumeration type, the compiler automatically creates a `final` type class that extends `Enum`, which is why enumeration types cannot be inherited.** -### 内部类 +### Inner Class -内部类又称为嵌套类,可以把内部类理解为外部类的一个普通成员。 +An inner class, also known as a nested class, can be understood as a regular member of an outer class. -**内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念,`outer.java`里面定义了一个内部类`inner`,一旦编译成功,就会生成两个完全不同的`.class`文件了,分别是`outer.class`和`outer$inner.class`。所以内部类的名字完全可以和它的外部类名字相同。** +**The reason inner classes are also syntactic sugar is that they are merely a compile-time concept. The `outer.java` file defining an inner class `inner`, once compiled, generates two completely different `.class` files: `outer.class` and `outer$inner.class`. Thus, the inner class name can coincide with that of its outer class.** ```java public class OutterClass { @@ -337,7 +337,7 @@ public class OutterClass { } ``` -以上代码编译后会生成两个 class 文件:`OutterClass$InnerClass.class`、`OutterClass.class` 。当我们尝试对`OutterClass.class`文件进行反编译的时候,命令行会打印以下内容:`Parsing OutterClass.class...Parsing inner class OutterClass$InnerClass.class... Generating OutterClass.jad` 。他会把两个文件全部进行反编译,然后一起生成一个`OutterClass.jad`文件。文件内容如下: +After compiling, two class files will be generated: `OutterClass$InnerClass.class` and `OutterClass.class`. When we try to decompile the `OutterClass.class` file, the command line will print the following: `Parsing OutterClass.class...Parsing inner class OutterClass$InnerClass.class... Generating OutterClass.jad`. It will decompile both files and generate an `OutterClass.jad` file with the following content: ```java public class OutterClass @@ -379,30 +379,30 @@ public class OutterClass } ``` -**为什么内部类可以使用外部类的 private 属性**: +**Why can inner classes access the private attributes of the outer class?** -我们在 InnerClass 中增加一个方法,打印外部类的 userName 属性 +We can add a method in `InnerClass` to print the `userName` property of the outer class: ```java -//省略其他属性 +// Omitted other properties public class OutterClass { private String userName; ...... class InnerClass{ ...... public void printOut(){ - System.out.println("Username from OutterClass:"+userName); + System.out.println("Username from OutterClass:" + userName); } } } -// 此时,使用javap -p命令对OutterClass反编译结果: +// At this time, using the javap -p command to decompile the result of OutterClass: public classOutterClass { private String userName; ...... static String access$000(OutterClass); } -// 此时,InnerClass的反编译结果: +// At this time, the decompiled result of InnerClass: class OutterClass$InnerClass { final OutterClass this$0; ...... @@ -411,7 +411,7 @@ class OutterClass$InnerClass { ``` -实际上,在编译完成之后,inner 实例内部会有指向 outer 实例的引用`this$0`,但是简单的`outer.name`是无法访问 private 属性的。从反编译的结果可以看到,outer 中会有一个桥方法`static String access$000(OutterClass)`,恰好返回 String 类型,即 userName 属性。正是通过这个方法实现内部类访问外部类私有属性。所以反编译后的`printOut()`方法大致如下: +In reality, after compilation, the `inner` instance internally holds a reference to the `outer` instance named `this$0`, but simply accessing `outer.name` cannot reach private attributes. From the decompiled result, we can see that the outer class has a bridge method `static String access$000(OutterClass)`, which happens to return the `String` type that is the `userName` property. This method is what allows the inner class to access the private attributes of the outer class. Hence, the decompiled `printOut()` method roughly looks like this: ```java public void printOut() { @@ -419,19 +419,19 @@ public void printOut() { } ``` -补充: +Supplementary Information: -1. 匿名内部类、局部内部类、静态内部类也是通过桥方法来获取 private 属性。 -2. 静态内部类没有`this$0`的引用 -3. 匿名内部类、局部内部类通过复制使用局部变量,该变量初始化之后就不能被修改。以下是一个案例: +1. Anonymous inner classes, local inner classes, and static inner classes also utilize bridge methods to access private attributes. +1. Static inner classes do not have the reference `this$0`. +1. Anonymous inner classes and local inner classes copy local variables, which cannot be changed after initialization. Here is a case: ```java public class OutterClass { private String userName; public void test(){ - //这里i初始化为1后就不能再被修改 - int i=1; + // Here i is initialized to 1 and cannot be modified further + int i = 1; class Inner{ public void printName(){ System.out.println(userName); @@ -442,25 +442,24 @@ public class OutterClass { } ``` -反编译后: +After decompilation: ```java -//javap命令反编译Inner的结果 -//i被复制进内部类,且为final +// The result of decompiling Inner using the javap command +// i is copied into the inner class and is final class OutterClass$1Inner { final int val$i; final OutterClass this$0; OutterClass$1Inner(); public void printName(); } - ``` -### 条件编译 +### Conditional Compilation -—般情况下,程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。 +Typically, every line of code in a program is subject to compilation. However, sometimes for optimization purposes, we may want only a part of the code to be compiled. At this point, we need to add conditions in the program to instruct the compiler to compile only the code that meets specific conditions, discarding the others. This is known as conditional compilation. -如在 C 或 CPP 中,可以通过预处理语句来实现条件编译。其实在 Java 中也可实现条件编译。我们先来看一段代码: +In C or C++, preprocessor directives can achieve conditional compilation. In fact, Java can also implement conditional compilation. Let's look at a code snippet: ```java public class ConditionalCompilation { @@ -479,7 +478,7 @@ public class ConditionalCompilation { } ``` -反编译后代码如下: +The decompiled code is as follows: ```java public class ConditionalCompilation @@ -498,15 +497,15 @@ public class ConditionalCompilation } ``` -首先,我们发现,在反编译后的代码中没有`System.out.println("Hello, ONLINE!");`,这其实就是条件编译。当`if(ONLINE)`为 false 的时候,编译器就没有对其内的代码进行编译。 +Firstly, we notice that the decompiled code does not contain `System.out.println("Hello, ONLINE!");`. This is an example of conditional compilation; when `if(ONLINE)` is false, the compiler does not compile the code within it. -所以,**Java 语法的条件编译,是通过判断条件为常量的 if 语句实现的。其原理也是 Java 语言的语法糖。根据 if 判断条件的真假,编译器直接把分支为 false 的代码块消除。通过该方式实现的条件编译,必须在方法体内实现,而无法在整个 Java 类的结构或者类的属性上进行条件编译,这与 C/C++的条件编译相比,确实更有局限性。在 Java 语言设计之初并没有引入条件编译的功能,虽有局限,但是总比没有更强。** +Thus, **the conditional compilation in Java is achieved by implementing an if statement whose condition is a constant. The principle is also syntactic sugar for Java syntax. Depending on the truth of the if condition, the compiler directly removes the code block associated with false branches. Conditionally compiled code must be placed within the method body and cannot be applied at the structural level of the entire Java class or class attributes, which is indeed more limited compared to conditionally compiling in C/C++. The Java language did not incorporate conditional compilation at its inception, yet this limitation is better than having no such feature.** -### 断言 +### Assertions -在 Java 中,`assert`关键字是从 JAVA SE 1.4 引入的,为了避免和老版本的 Java 代码中使用了`assert`关键字导致错误,Java 在执行的时候默认是不启动断言检查的(这个时候,所有的断言语句都将忽略!),如果要开启断言检查,则需要用开关`-enableassertions`或`-ea`来开启。 +In Java, the `assert` keyword was introduced in JAVA SE 1.4. To prevent errors caused by legacy Java code that used the `assert` keyword, assertions are not checked by default during execution (at this time, all assertion statements will be ignored!). To enable assertion checking, the `-enableassertions` or `-ea` switch is required. -看一段包含断言的代码: +Here’s a code snippet containing assertions: ```java public class AssertTest { @@ -514,14 +513,14 @@ public class AssertTest { int a = 1; int b = 1; assert a == b; - System.out.println("公众号:Hollis"); + System.out.println("Public account: Hollis"); assert a != b : "Hollis"; - System.out.println("博客:www.hollischuang.com"); + System.out.println("Blog: www.hollischuang.com"); } } ``` -反编译后代码如下: +The decompiled code looks like this: ```java public class AssertTest { @@ -550,13 +549,13 @@ static final boolean $assertionsDisabled = !com/hollis/suguar/AssertTest.desired } ``` -很明显,反编译之后的代码要比我们自己的代码复杂的多。所以,使用了 assert 这个语法糖我们节省了很多代码。**其实断言的底层实现就是 if 语言,如果断言结果为 true,则什么都不做,程序继续执行,如果断言结果为 false,则程序抛出 AssertError 来打断程序的执行。**`-enableassertions`会设置\$assertionsDisabled 字段的值。 +It is evident that the decompiled code is considerably more complex than our own code. Thus, using the assert syntactic sugar saves us a lot of code. **The underlying implementation of assertions is really just an if statement. If the assertion evaluates to true, nothing happens, and the program continues executing. If the assertion results in false, an AssertError is thrown, halting the program. The `-enableassertions` switch sets the value for `$assertionsDisabled`.** -### 数值字面量 +### Numeric Literals -在 java 7 中,数值字面量,不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。 +In Java 7, numeric literals, both integers and floating-point numbers, allow the insertion of any number of underscores between digits. These underscores do not affect the value of the literal; their purpose is to enhance readability. -比如: +For example: ```java public class Test { @@ -567,7 +566,7 @@ public class Test { } ``` -反编译后: +After decompilation: ```java public class Test @@ -580,26 +579,26 @@ public class Test } ``` -反编译后就是把`_`删除了。也就是说 **编译器并不认识在数字字面量中的`_`,需要在编译阶段把他去掉。** +After decompilation, the underscores are removed. This means that **the compiler does not recognize the underscores in numeric literals and removes them during compilation.** -### for-each +### For-each -增强 for 循环(`for-each`)相信大家都不陌生,日常开发经常会用到的,他会比 for 循环要少写很多代码,那么这个语法糖背后是如何实现的呢? +The enhanced for loop (`for-each`) should be quite familiar to everyone; it is commonly used in daily development and requires much less code than a traditional for loop. What is the underlying implementation of this syntactic sugar? ```java public static void main(String... args) { - String[] strs = {"Hollis", "公众号:Hollis", "博客:www.hollischuang.com"}; + String[] strs = {"Hollis", "Public account: Hollis", "Blog: www.hollischuang.com"}; for (String s : strs) { System.out.println(s); } - List strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com"); + List strList = ImmutableList.of("Hollis", "Public account: Hollis", "Blog: www.hollischuang.com"); for (String s : strList) { System.out.println(s); } } ``` -反编译后代码如下: +The decompiled code is as follows: ```java public static transient void main(String args[]) @@ -623,13 +622,13 @@ public static transient void main(String args[]) } ``` -代码很简单,**for-each 的实现原理其实就是使用了普通的 for 循环和迭代器。** +The code is straightforward; **the implementation of `for-each` is essentially a standard for loop and an iterator.** -### try-with-resource +### Try-with-Resources -Java 里,对于文件操作 IO 流、数据库连接等开销非常昂贵的资源,用完之后必须及时通过 close 方法将其关闭,否则资源会一直处于打开状态,可能会导致内存泄露等问题。 +In Java, for costly resources such as file operations, IO streams, and database connections, it is crucial to close them promptly using the close method, to avoid memory leaks and other issues. -关闭资源的常用方式就是在`finally`块里是释放,即调用`close`方法。比如,我们经常会写这样的代码: +A common way to close resources is within a `finally` block, such as: ```java public static void main(String[] args) { @@ -654,11 +653,11 @@ public static void main(String[] args) { } ``` -从 Java 7 开始,jdk 提供了一种更好的方式关闭资源,使用`try-with-resources`语句,改写一下上面的代码,效果如下: +Starting from Java 7, a better way to close resources was introduced using the `try-with-resources` statement, rewriting the previous code as follows: ```java public static void main(String... args) { - try (BufferedReader br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"))) { + try (BufferedReader br = new BufferedReader(new FileReader("d:\\hollischuang.xml"))) { String line; while ((line = br.readLine()) != null) { System.out.println(line); @@ -669,14 +668,14 @@ public static void main(String... args) { } ``` -看,这简直是一大福音啊,虽然我之前一般使用`IOUtils`去关闭流,并不会使用在`finally`中写很多代码的方式,但是这种新的语法糖看上去好像优雅很多呢。看下他的背后: +This is a significant improvement; while I typically used `IOUtils` for closing streams and avoided writing extensive code in `finally` blocks, this new form of syntactic sugar appears to be much more elegant. Let’s take a look at its underlying implementation: ```java public static transient void main(String args[]) { BufferedReader br; Throwable throwable; - br = new BufferedReader(new FileReader("d:\\ hollischuang.xml")); + br = new BufferedReader(new FileReader("d:\\hollischuang.xml")); throwable = null; String line; try @@ -723,25 +722,25 @@ public static transient void main(String args[]) } ``` -**其实背后的原理也很简单,那些我们没有做的关闭资源的操作,编译器都帮我们做了。所以,再次印证了,语法糖的作用就是方便程序员的使用,但最终还是要转成编译器认识的语言。** +**The underlying principle is quite simple; the resource closing operations that we omitted are automatically handled by the compiler. This reinforces the idea that syntactic sugar serves to facilitate programmers, yet it ultimately converts to a language understood by the compiler.** -### Lambda 表达式 +### Lambda Expressions -关于 lambda 表达式,有人可能会有质疑,因为网上有人说他并不是语法糖。其实我想纠正下这个说法。**Lambda 表达式不是匿名内部类的语法糖,但是他也是一个语法糖。实现方式其实是依赖了几个 JVM 底层提供的 lambda 相关 api。** +Regarding lambda expressions, some may question their classification as syntactic sugar, with some online claiming they are not. I would like to clarify this assertion: **Lambda expressions are not simply syntactic sugar for anonymous inner classes; however, they are a form of syntactic sugar. Their implementation heavily relies on several JVM-level lambda-related APIs.** -先来看一个简单的 lambda 表达式。遍历一个 list: +Let’s first examine a simple lambda expression that iterates over a list: ```java public static void main(String... args) { - List strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com"); + List strList = ImmutableList.of("Hollis", "Public account: Hollis", "Blog: www.hollischuang.com"); strList.forEach( s -> { System.out.println(s); } ); } ``` -为啥说他并不是内部类的语法糖呢,前面讲内部类我们说过,内部类在编译之后会有两个 class 文件,但是,包含 lambda 表达式的类编译后只有一个文件。 +Why is it said that this is not just syntactic sugar over inner classes? As discussed earlier, an inner class compiling results in two class files, whereas a class containing a lambda expression compiles into only a single file. -反编译后代码如下: +The decompiled code is as follows: ```java public static /* varargs */ void main(String ... args) { @@ -754,13 +753,13 @@ private static /* synthetic */ void lambda$main$0(String s) { } ``` -可以看到,在`forEach`方法中,其实是调用了`java.lang.invoke.LambdaMetafactory#metafactory`方法,该方法的第四个参数 `implMethod` 指定了方法实现。可以看到这里其实是调用了一个`lambda$main$0`方法进行了输出。 +Here, within the `forEach` method, we notice that it calls `java.lang.invoke.LambdaMetafactory#metafactory`, where the fourth parameter `implMethod` specifies the method implementation. It is clear that `lambda$main$0` is invoked for the output. -再来看一个稍微复杂一点的,先对 List 进行过滤,然后再输出: +Now, let’s look at a slightly more complicated example, where we filter the List first and then output the results: ```java public static void main(String... args) { - List strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com"); + List strList = ImmutableList.of("Hollis", "Public account: Hollis", "Blog: www.hollischuang.com"); List HollisList = strList.stream().filter(string -> string.contains("Hollis")).collect(Collectors.toList()); @@ -768,11 +767,11 @@ public static void main(String... args) { } ``` -反编译后代码如下: +The decompiled code looks like this: ```java public static /* varargs */ void main(String ... args) { - ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com"); + ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53F7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com"); List HollisList = strList.stream().filter((Predicate)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)Z, lambda$main$0(java.lang.String ), (Ljava/lang/String;)Z)()).collect(Collectors.toList()); HollisList.forEach((Consumer)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$1(java.lang.Object ), (Ljava/lang/Object;)V)()); } @@ -786,15 +785,15 @@ private static /* synthetic */ boolean lambda$main$0(String string) { } ``` -两个 lambda 表达式分别调用了`lambda$main$1`和`lambda$main$0`两个方法。 +The two lambda expressions call two different methods, `lambda$main$1` and `lambda$main$0`. -**所以,lambda 表达式的实现其实是依赖了一些底层的 api,在编译阶段,编译器会把 lambda 表达式进行解糖,转换成调用内部 api 的方式。** +**The implementation of lambda expressions relies on some low-level APIs, and during the compilation phase, the compiler will desugar the lambda expressions, converting them into calls to these internal APIs.** -## 可能遇到的坑 +## Potential Pitfalls -### 泛型 +### Generics -**一、当泛型遇到重载** +**1. When Generics Encounter Overloads** ```java public class GenericTypes { @@ -809,38 +808,37 @@ public class GenericTypes { } ``` -上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是`List`另一个是`List` ,但是,这段代码是编译通不过的。因为我们前面讲过,参数`List`和`List`编译之后都被擦除了,变成了一样的原生类型 List,擦除动作导致这两个方法的特征签名变得一模一样。 +The code above has two overloaded functions, differentiated by their parameter types, one is `List` and the other is `List`, but this code will not compile. This is because, as we discussed earlier, the types `List` and `List` are both erased to the same raw type `List`. This erasure results in the method signatures becoming identical. -**二、当泛型遇到 catch** +**2. When Generics Encounter Catch Statements** -泛型的类型参数不能用在 Java 异常处理的 catch 语句中。因为异常处理是由 JVM 在运行时刻来进行的。由于类型信息被擦除,JVM 是无法区分两个异常类型`MyException`和`MyException`的 +Generic type parameters cannot be used in catch statements in Java exception handling. This is because exception handling is performed by the JVM at runtime. Since type information is erased, the JVM cannot distinguish between two exception types, `MyException` and `MyException`. -**三、当泛型内包含静态变量** +**3. When Generics Include Static Variables** ```java -public class StaticTest{ - public static void main(String[] args){ +public class StaticTest { + public static void main(String[] args) { GT gti = new GT(); - gti.var=1; + gti.var = 1; GT gts = new GT(); - gts.var=2; + gts.var = 2; System.out.println(gti.var); } } -class GT{ - public static int var=0; - public void nothing(T x){} +class GT { + public static int var = 0; + public void nothing(T x) {} } ``` -以上代码输出结果为:2! +The output of the above code will be: 2! -有些同学可能会误认为泛型类是不同的类,对应不同的字节码,其实 -由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的静态变量是共享的。上面例子里的`GT.var`和`GT.var`其实是一个变量。 +Some might mistakenly think that generic classes are different classes with separate bytecodes, but actually, due to type erasure, all instances of the generic class share the same bytecode. The static variable for this generic class is therefore shared. The `GT.var` and `GT.var` refer to the same variable. -### 自动装箱与拆箱 +### Auto-Boxing and Unboxing -**对象相等比较** +**Object Equality Comparison** ```java public static void main(String[] args) { @@ -849,24 +847,24 @@ public static void main(String[] args) { Integer c = 100; Integer d = 100; System.out.println("a == b is " + (a == b)); - System.out.println(("c == d is " + (c == d))); + System.out.println("c == d is " + (c == d)); } ``` -输出结果: +The output will be: ```plain a == b is false c == d is true ``` -在 Java 5 中,在 Integer 的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。 +In Java 5, a new feature was introduced for `Integer` operations to save memory and enhance performance. Integer objects leverage the same reference for caching and reuse. -> 适用于整数值区间-128 至 +127。 +> This applies to integer values in the range of -128 to +127. > -> 只适用于自动装箱。使用构造函数创建对象不适用。 +> It is only applicable for auto-boxing. Using constructors to create objects does not apply. -### 增强 for 循环 +### Enhanced For Loop ```java for (Student stu : students) { @@ -875,16 +873,16 @@ for (Student stu : students) { } ``` -会抛出`ConcurrentModificationException`异常。 +This will throw a `ConcurrentModificationException`. -Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出`java.util.ConcurrentModificationException`异常。 +The Iterator works in an independent thread and possesses a mutex lock. Once an iterator is created, it builds a singly-linked list index pointing to the original object. If the original object’s quantity changes, the contents of this index will not be synchronized, therefore when the index pointer moves forward, it fails to find the object to iterate, causing the `java.util.ConcurrentModificationException` to be immediately thrown following the fail-fast principle. -所以 `Iterator` 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 `Iterator` 本身的方法`remove()`来删除对象,`Iterator.remove()` 方法会在删除当前迭代对象的同时维护索引的一致性。 +Thus, `Iterator` is not allowed to modify objects during its operation. However, you can use the `remove()` method of `Iterator` itself to delete objects. The `Iterator.remove()` method will maintain the index's consistency while removing the current iteration object. -## 总结 +## Conclusion -前面介绍了 12 种 Java 中常用的语法糖。所谓语法糖就是提供给开发人员便于开发的一种语法而已。但是这种语法只有开发人员认识。要想被执行,需要进行解糖,即转成 JVM 认识的语法。当我们把语法糖解糖之后,你就会发现其实我们日常使用的这些方便的语法,其实都是一些其他更简单的语法构成的。 +This article introduced 12 commonly used syntactic sugars in Java. Syntactic sugar is simply a syntax designed to facilitate development for programmers. However, this syntax is only recognized by developers. To be executed, it needs to be desugared, meaning it is converted to syntax that the JVM understands. After we desugar these syntactic sugars, we realize that many of the convenient syntaxes we use daily are fundamentally composed of simpler syntactic structures. -有了这些语法糖,我们在日常开发的时候可以大大提升效率,但是同时也要避过度使用。使用之前最好了解下原理,避免掉坑。 +With these syntactic sugars, we can significantly enhance efficiency in our daily development, but we must also be careful to avoid their overuse. It’s best to understand the principles before usage to prevent pitfalls. diff --git a/docs/java/basis/unsafe.md b/docs/java/basis/unsafe.md index efd1337d39c..1f454bd7332 100644 --- a/docs/java/basis/unsafe.md +++ b/docs/java/basis/unsafe.md @@ -1,44 +1,44 @@ --- -title: Java 魔法类 Unsafe 详解 +title: A Detailed Explanation of Java's Magic Class Unsafe category: Java tag: - - Java基础 + - Java Basics --- -> 本文整理完善自下面这两篇优秀的文章: +> This article is organized and improved based on the following two excellent articles: > -> - [Java 魔法类:Unsafe 应用解析 - 美团技术团队 -2019](https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html) -> - [Java 双刃剑之 Unsafe 类详解 - 码农参上 - 2021](https://xie.infoq.cn/article/8b6ed4195e475bfb32dacc5cb) +> - [Java Magic Class: Unsafe Application Analysis - Meituan Technology Team - 2019](https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html) +> - [Java Double-Edged Sword: Detailed Explanation of Unsafe Class - Code Farmer - 2021](https://xie.infoq.cn/article/8b6ed4195e475bfb32dacc5cb) -阅读过 JUC 源码的同学,一定会发现很多并发工具类都调用了一个叫做 `Unsafe` 的类。 +Students who have read the JUC source code will find that many concurrent utility classes call a class called `Unsafe`. -那这个类主要是用来干什么的呢?有什么使用场景呢?这篇文章就带你搞清楚! +So what is this class mainly used for? What are its usage scenarios? This article will help you clarify! -## Unsafe 介绍 +## Introduction to Unsafe -`Unsafe` 是位于 `sun.misc` 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 `Unsafe` 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 `Unsafe` 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 `Unsafe` 的使用一定要慎重。 +`Unsafe` is a class located in the `sun.misc` package, mainly providing methods for performing low-level, unsafe operations, such as directly accessing system memory resources and managing memory resources independently. These methods play a significant role in improving Java's runtime efficiency and enhancing the underlying resource operation capabilities of the Java language. However, since the `Unsafe` class gives Java the ability to manipulate memory space similarly to C language pointers, it undoubtedly increases the risk of related pointer issues in programs. Overuse or incorrect use of the `Unsafe` class can increase the likelihood of program errors, making Java, a language known for its safety, less "safe." Therefore, the use of `Unsafe` must be approached with caution. -另外,`Unsafe` 提供的这些功能的实现需要依赖本地方法(Native Method)。你可以将本地方法看作是 Java 中使用其他编程语言编写的方法。本地方法使用 **`native`** 关键字修饰,Java 代码中只是声明方法头,具体的实现则交给 **本地代码**。 +Additionally, the implementation of these functionalities provided by `Unsafe` relies on native methods. You can think of native methods as methods written in other programming languages that are used in Java. Native methods are marked with the **`native`** keyword, and in Java code, only the method header is declared, while the specific implementation is left to **native code**. ![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717115231125.png) -**为什么要使用本地方法呢?** +**Why use native methods?** -1. 需要用到 Java 中不具备的依赖于操作系统的特性,Java 在实现跨平台的同时要实现对底层的控制,需要借助其他语言发挥作用。 -2. 对于其他语言已经完成的一些现成功能,可以使用 Java 直接调用。 -3. 程序对时间敏感或对性能要求非常高时,有必要使用更加底层的语言,例如 C/C++甚至是汇编。 +1. To utilize operating system-dependent features that are not available in Java, as Java needs to control the underlying system while achieving cross-platform compatibility, which requires the assistance of other languages. +2. To directly call some successful functionalities already completed in other languages. +3. When the program is time-sensitive or has very high performance requirements, it is necessary to use lower-level languages, such as C/C++ or even assembly. -在 JUC 包的很多并发工具类在实现并发机制时,都调用了本地方法,通过它们打破了 Java 运行时的界限,能够接触到操作系统底层的某些功能。对于同一本地方法,不同的操作系统可能会通过不同的方式来实现,但是对于使用者来说是透明的,最终都会得到相同的结果。 +Many concurrent utility classes in the JUC package call native methods when implementing concurrency mechanisms, breaking the boundaries of Java runtime and allowing access to certain functionalities at the operating system level. For the same native method, different operating systems may implement it in different ways, but for the user, it is transparent, and the final results will be the same. -## Unsafe 创建 +## Creating Unsafe -`sun.misc.Unsafe` 部分源码如下: +The following is part of the source code for `sun.misc.Unsafe`: ```java public final class Unsafe { - // 单例对象 + // Singleton object private static final Unsafe theUnsafe; ...... private Unsafe() { @@ -46,7 +46,7 @@ public final class Unsafe { @CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); - // 仅在引导类加载器`BootstrapClassLoader`加载时才合法 + // Only legal when loaded by the BootstrapClassLoader if(!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { @@ -56,7 +56,7 @@ public final class Unsafe { } ``` -`Unsafe` 类为一单例实现,提供静态方法 `getUnsafe` 获取 `Unsafe`实例。这个看上去貌似可以用来获取 `Unsafe` 实例。但是,当我们直接调用这个静态方法的时候,会抛出 `SecurityException` 异常: +The `Unsafe` class is implemented as a singleton, providing a static method `getUnsafe` to obtain an instance of `Unsafe`. This may seem like it can be used to get an `Unsafe` instance. However, when we directly call this static method, a `SecurityException` will be thrown: ```bash Exception in thread "main" java.lang.SecurityException: Unsafe @@ -64,671 +64,19 @@ Exception in thread "main" java.lang.SecurityException: Unsafe at com.cn.test.GetUnsafeTest.main(GetUnsafeTest.java:12) ``` -**为什么 `public static` 方法无法被直接调用呢?** +**Why can't the `public static` method be called directly?** -这是因为在`getUnsafe`方法中,会对调用者的`classLoader`进行检查,判断当前类是否由`Bootstrap classLoader`加载,如果不是的话那么就会抛出一个`SecurityException`异常。也就是说,只有启动类加载器加载的类才能够调用 Unsafe 类中的方法,来防止这些方法在不可信的代码中被调用。 +This is because the `getUnsafe` method checks the caller's `classLoader` to determine whether the current class is loaded by the `Bootstrap classLoader`. If not, a `SecurityException` will be thrown. In other words, only classes loaded by the bootstrap class loader can call methods in the Unsafe class to prevent these methods from being called in untrusted code. -**为什么要对 Unsafe 类进行这么谨慎的使用限制呢?** +**Why is such cautious usage restriction placed on the Unsafe class?** -`Unsafe` 提供的功能过于底层(如直接访问系统内存资源、自主管理内存资源等),安全隐患也比较大,使用不当的话,很容易出现很严重的问题。 +The functionalities provided by `Unsafe` are too low-level (such as directly accessing system memory resources and managing memory resources independently), and the security risks are significant. Improper use can easily lead to serious issues. -**如若想使用 `Unsafe` 这个类的话,应该如何获取其实例呢?** +**If one wants to use the Unsafe class, how should they obtain its instance?** -这里介绍两个可行的方案。 +Here are two feasible solutions. -1、利用反射获得 Unsafe 类中已经实例化完成的单例对象 `theUnsafe` 。 +1. Use reflection to obtain the already instantiated singleton object `theUnsafe` in the Unsafe class. ```java -private static Unsafe reflectGetUnsafe() { - try { - Field field = Unsafe.class.getDeclaredField("theUnsafe"); - field.setAccessible(true); - return (Unsafe) field.get(null); - } catch (Exception e) { - log.error(e.getMessage(), e); - return null; - } -} -``` - -2、从`getUnsafe`方法的使用限制条件出发,通过 Java 命令行命令`-Xbootclasspath/a`把调用 Unsafe 相关方法的类 A 所在 jar 包路径追加到默认的 bootstrap 路径中,使得 A 被引导类加载器加载,从而通过`Unsafe.getUnsafe`方法安全的获取 Unsafe 实例。 - -```bash -java -Xbootclasspath/a: ${path} // 其中path为调用Unsafe相关方法的类所在jar包路径 -``` - -## Unsafe 功能 - -概括的来说,`Unsafe` 类实现功能可以被分为下面 8 类: - -1. 内存操作 -2. 内存屏障 -3. 对象操作 -4. 数据操作 -5. CAS 操作 -6. 线程调度 -7. Class 操作 -8. 系统信息 - -### 内存操作 - -#### 介绍 - -如果你是一个写过 C 或者 C++ 的程序员,一定对内存操作不会陌生,而在 Java 中是不允许直接对内存进行操作的,对象内存的分配和回收都是由 JVM 自己实现的。但是在 `Unsafe` 中,提供的下列接口可以直接进行内存操作: - -```java -//分配新的本地空间 -public native long allocateMemory(long bytes); -//重新调整内存空间的大小 -public native long reallocateMemory(long address, long bytes); -//将内存设置为指定值 -public native void setMemory(Object o, long offset, long bytes, byte value); -//内存拷贝 -public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes); -//清除内存 -public native void freeMemory(long address); -``` - -使用下面的代码进行测试: - -```java -private void memoryTest() { - int size = 4; - long addr = unsafe.allocateMemory(size); - long addr3 = unsafe.reallocateMemory(addr, size * 2); - System.out.println("addr: "+addr); - System.out.println("addr3: "+addr3); - try { - unsafe.setMemory(null,addr ,size,(byte)1); - for (int i = 0; i < 2; i++) { - unsafe.copyMemory(null,addr,null,addr3+size*i,4); - } - System.out.println(unsafe.getInt(addr)); - System.out.println(unsafe.getLong(addr3)); - }finally { - unsafe.freeMemory(addr); - unsafe.freeMemory(addr3); - } -} -``` - -先看结果输出: - -```plain -addr: 2433733895744 -addr3: 2433733894944 -16843009 -72340172838076673 -``` - -分析一下运行结果,首先使用`allocateMemory`方法申请 4 字节长度的内存空间,调用`setMemory`方法向每个字节写入内容为`byte`类型的 1,当使用 Unsafe 调用`getInt`方法时,因为一个`int`型变量占 4 个字节,会一次性读取 4 个字节,组成一个`int`的值,对应的十进制结果为 16843009。 - -你可以通过下图理解这个过程: - -![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144344005.png) - -在代码中调用`reallocateMemory`方法重新分配了一块 8 字节长度的内存空间,通过比较`addr`和`addr3`可以看到和之前申请的内存地址是不同的。在代码中的第二个 for 循环里,调用`copyMemory`方法进行了两次内存的拷贝,每次拷贝内存地址`addr`开始的 4 个字节,分别拷贝到以`addr3`和`addr3+4`开始的内存空间上: - -![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144354582.png) - -拷贝完成后,使用`getLong`方法一次性读取 8 个字节,得到`long`类型的值为 72340172838076673。 - -需要注意,通过这种方式分配的内存属于 堆外内存 ,是无法进行垃圾回收的,需要我们把这些内存当做一种资源去手动调用`freeMemory`方法进行释放,否则会产生内存泄漏。通用的操作内存方式是在`try`中执行对内存的操作,最终在`finally`块中进行内存的释放。 - -**为什么要使用堆外内存?** - -- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。 -- 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。 - -#### 典型应用 - -`DirectByteBuffer` 是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在 Netty、MINA 等 NIO 框架中应用广泛。`DirectByteBuffer` 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现。 - -下图为 `DirectByteBuffer` 构造函数,创建 `DirectByteBuffer` 的时候,通过 `Unsafe.allocateMemory` 分配内存、`Unsafe.setMemory` 进行内存初始化,而后构建 `Cleaner` 对象用于跟踪 `DirectByteBuffer` 对象的垃圾回收,以实现当 `DirectByteBuffer` 被垃圾回收时,分配的堆外内存一起被释放。 - -```java -DirectByteBuffer(int cap) { // package-private - - super(-1, 0, cap, cap); - boolean pa = VM.isDirectMemoryPageAligned(); - int ps = Bits.pageSize(); - long size = Math.max(1L, (long)cap + (pa ? ps : 0)); - Bits.reserveMemory(size, cap); - - long base = 0; - try { - // 分配内存并返回基地址 - base = unsafe.allocateMemory(size); - } catch (OutOfMemoryError x) { - Bits.unreserveMemory(size, cap); - throw x; - } - // 内存初始化 - unsafe.setMemory(base, size, (byte) 0); - if (pa && (base % ps != 0)) { - // Round up to page boundary - address = base + ps - (base & (ps - 1)); - } else { - address = base; - } - // 跟踪 DirectByteBuffer 对象的垃圾回收,以实现堆外内存释放 - cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); - att = null; -} -``` - -### 内存屏障 - -#### 介绍 - -在介绍内存屏障前,需要知道编译器和 CPU 会在保证程序输出结果一致的情况下,会对代码进行重排序,从指令优化角度提升性能。而指令重排序可能会带来一个不好的结果,导致 CPU 的高速缓存和内存中数据的不一致,而内存屏障(`Memory Barrier`)就是通过阻止屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况。 - -在硬件层面上,内存屏障是 CPU 为了防止代码进行重排序而提供的指令,不同的硬件平台上实现内存屏障的方法可能并不相同。在 Java8 中,引入了 3 个内存屏障的函数,它屏蔽了操作系统底层的差异,允许在代码中定义、并统一由 JVM 来生成内存屏障指令,来实现内存屏障的功能。 - -`Unsafe` 中提供了下面三个内存屏障相关方法: - -```java -//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前 -public native void loadFence(); -//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前 -public native void storeFence(); -//内存屏障,禁止load、store操作重排序 -public native void fullFence(); -``` - -内存屏障可以看做对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。以`loadFence`方法为例,它会禁止读操作重排序,保证在这个屏障之前的所有读操作都已经完成,并且将缓存数据设为无效,重新从主存中进行加载。 - -看到这估计很多小伙伴们会想到`volatile`关键字了,如果在字段上添加了`volatile`关键字,就能够实现字段在多线程下的可见性。基于读内存屏障,我们也能实现相同的功能。下面定义一个线程方法,在线程中去修改`flag`标志位,注意这里的`flag`是没有被`volatile`修饰的: - -```java -@Getter -class ChangeThread implements Runnable{ - /**volatile**/ boolean flag=false; - @Override - public void run() { - try { - Thread.sleep(3000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println("subThread change flag to:" + flag); - flag = true; - } -} -``` - -在主线程的`while`循环中,加入内存屏障,测试是否能够感知到`flag`的修改变化: - -```java -public static void main(String[] args){ - ChangeThread changeThread = new ChangeThread(); - new Thread(changeThread).start(); - while (true) { - boolean flag = changeThread.isFlag(); - unsafe.loadFence(); //加入读内存屏障 - if (flag){ - System.out.println("detected flag changed"); - break; - } - } - System.out.println("main thread end"); -} -``` - -运行结果: - -```plain -subThread change flag to:false -detected flag changed -main thread end -``` - -而如果删掉上面代码中的`loadFence`方法,那么主线程将无法感知到`flag`发生的变化,会一直在`while`中循环。可以用图来表示上面的过程: - -![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144703446.png) - -了解 Java 内存模型(`JMM`)的小伙伴们应该清楚,运行中的线程不是直接读取主内存中的变量的,只能操作自己工作内存中的变量,然后同步到主内存中,并且线程的工作内存是不能共享的。上面的图中的流程就是子线程借助于主内存,将修改后的结果同步给了主线程,进而修改主线程中的工作空间,跳出循环。 - -#### 典型应用 - -在 Java 8 中引入了一种锁的新机制——`StampedLock`,它可以看成是读写锁的一个改进版本。`StampedLock` 提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。由于 `StampedLock` 提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存 load 到线程工作内存时,会存在数据不一致问题。 - -为了解决这个问题,`StampedLock` 的 `validate` 方法会通过 `Unsafe` 的 `loadFence` 方法加入一个 `load` 内存屏障。 - -```java -public boolean validate(long stamp) { - U.loadFence(); - return (stamp & SBITS) == (state & SBITS); -} -``` - -### 对象操作 - -#### 介绍 - -**例子** - -```java -import sun.misc.Unsafe; -import java.lang.reflect.Field; - -public class Main { - - private int value; - - public static void main(String[] args) throws Exception{ - Unsafe unsafe = reflectGetUnsafe(); - assert unsafe != null; - long offset = unsafe.objectFieldOffset(Main.class.getDeclaredField("value")); - Main main = new Main(); - System.out.println("value before putInt: " + main.value); - unsafe.putInt(main, offset, 42); - System.out.println("value after putInt: " + main.value); - System.out.println("value after putInt: " + unsafe.getInt(main, offset)); - } - - private static Unsafe reflectGetUnsafe() { - try { - Field field = Unsafe.class.getDeclaredField("theUnsafe"); - field.setAccessible(true); - return (Unsafe) field.get(null); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - -} -``` - -输出结果: - -```plain -value before putInt: 0 -value after putInt: 42 -value after putInt: 42 -``` - -**对象属性** - -对象成员属性的内存偏移量获取,以及字段属性值的修改,在上面的例子中我们已经测试过了。除了前面的`putInt`、`getInt`方法外,Unsafe 提供了全部 8 种基础数据类型以及`Object`的`put`和`get`方法,并且所有的`put`方法都可以越过访问权限,直接修改内存中的数据。阅读 openJDK 源码中的注释发现,基础数据类型和`Object`的读写稍有不同,基础数据类型是直接操作的属性值(`value`),而`Object`的操作则是基于引用值(`reference value`)。下面是`Object`的读写方法: - -```java -//在对象的指定偏移地址获取一个对象引用 -public native Object getObject(Object o, long offset); -//在对象指定偏移地址写入一个对象引用 -public native void putObject(Object o, long offset, Object x); -``` - -除了对象属性的普通读写外,`Unsafe` 还提供了 **volatile 读写**和**有序写入**方法。`volatile`读写方法的覆盖范围与普通读写相同,包含了全部基础数据类型和`Object`类型,以`int`类型为例: - -```java -//在对象的指定偏移地址处读取一个int值,支持volatile load语义 -public native int getIntVolatile(Object o, long offset); -//在对象指定偏移地址处写入一个int,支持volatile store语义 -public native void putIntVolatile(Object o, long offset, int x); -``` - -相对于普通读写来说,`volatile`读写具有更高的成本,因为它需要保证可见性和有序性。在执行`get`操作时,会强制从主存中获取属性值,在使用`put`方法设置属性值时,会强制将值更新到主存中,从而保证这些变更对其他线程是可见的。 - -有序写入的方法有以下三个: - -```java -public native void putOrderedObject(Object o, long offset, Object x); -public native void putOrderedInt(Object o, long offset, int x); -public native void putOrderedLong(Object o, long offset, long x); -``` - -有序写入的成本相对`volatile`较低,因为它只保证写入时的有序性,而不保证可见性,也就是一个线程写入的值不能保证其他线程立即可见。为了解决这里的差异性,需要对内存屏障的知识点再进一步进行补充,首先需要了解两个指令的概念: - -- `Load`:将主内存中的数据拷贝到处理器的缓存中 -- `Store`:将处理器缓存的数据刷新到主内存中 - -顺序写入与`volatile`写入的差别在于,在顺序写时加入的内存屏障类型为`StoreStore`类型,而在`volatile`写入时加入的内存屏障是`StoreLoad`类型,如下图所示: - -![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144834132.png) - -在有序写入方法中,使用的是`StoreStore`屏障,该屏障确保`Store1`立刻刷新数据到内存,这一操作先于`Store2`以及后续的存储指令操作。而在`volatile`写入中,使用的是`StoreLoad`屏障,该屏障确保`Store1`立刻刷新数据到内存,这一操作先于`Load2`及后续的装载指令,并且,`StoreLoad`屏障会使该屏障之前的所有内存访问指令,包括存储指令和访问指令全部完成之后,才执行该屏障之后的内存访问指令。 - -综上所述,在上面的三类写入方法中,在写入效率方面,按照`put`、`putOrder`、`putVolatile`的顺序效率逐渐降低。 - -**对象实例化** - -使用 `Unsafe` 的 `allocateInstance` 方法,允许我们使用非常规的方式进行对象的实例化,首先定义一个实体类,并且在构造函数中对其成员变量进行赋值操作: - -```java -@Data -public class A { - private int b; - public A(){ - this.b =1; - } -} -``` - -分别基于构造函数、反射以及 `Unsafe` 方法的不同方式创建对象进行比较: - -```java -public void objTest() throws Exception{ - A a1=new A(); - System.out.println(a1.getB()); - A a2 = A.class.newInstance(); - System.out.println(a2.getB()); - A a3= (A) unsafe.allocateInstance(A.class); - System.out.println(a3.getB()); -} -``` - -打印结果分别为 1、1、0,说明通过`allocateInstance`方法创建对象过程中,不会调用类的构造方法。使用这种方式创建对象时,只用到了`Class`对象,所以说如果想要跳过对象的初始化阶段或者跳过构造器的安全检查,就可以使用这种方法。在上面的例子中,如果将 A 类的构造函数改为`private`类型,将无法通过构造函数和反射创建对象(可以通过构造函数对象 setAccessible 后创建对象),但`allocateInstance`方法仍然有效。 - -#### 典型应用 - -- **常规对象实例化方式**:我们通常所用到的创建对象的方式,从本质上来讲,都是通过 new 机制来实现对象的创建。但是,new 机制有个特点就是当类只提供有参的构造函数且无显式声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。 -- **非常规的实例化方式**:而 Unsafe 中提供 allocateInstance 方法,仅通过 Class 对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM 安全检查等。它抑制修饰符检测,也就是即使构造器是 private 修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance 在 java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。 - -### 数组操作 - -#### 介绍 - -`arrayBaseOffset` 与 `arrayIndexScale` 这两个方法配合起来使用,即可定位数组中每个元素在内存中的位置。 - -```java -//返回数组中第一个元素的偏移地址 -public native int arrayBaseOffset(Class arrayClass); -//返回数组中一个元素占用的大小 -public native int arrayIndexScale(Class arrayClass); -``` - -#### 典型应用 - -这两个与数据操作相关的方法,在 `java.util.concurrent.atomic` 包下的 `AtomicIntegerArray`(可以实现对 `Integer` 数组中每个元素的原子性操作)中有典型的应用,如下图 `AtomicIntegerArray` 源码所示,通过 `Unsafe` 的 `arrayBaseOffset`、`arrayIndexScale` 分别获取数组首元素的偏移地址 `base` 及单个元素大小因子 `scale` 。后续相关原子性操作,均依赖于这两个值进行数组中元素的定位,如下图二所示的 `getAndAdd` 方法即通过 `checkedByteOffset` 方法获取某数组元素的偏移地址,而后通过 CAS 实现原子性操作。 - -![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144927257.png) - -### CAS 操作 - -#### 介绍 - -这部分主要为 CAS 相关操作的方法。 - -```java -/** - * CAS - * @param o 包含要修改field的对象 - * @param offset 对象中某field的偏移量 - * @param expected 期望值 - * @param update 更新值 - * @return true | false - */ -public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update); - -public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update); - -public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update); -``` - -**什么是 CAS?** CAS 即比较并替换(Compare And Swap),是实现并发算法时常用到的一种技术。CAS 操作包含三个操作数——内存位置、预期原值及新值。执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS 是一条 CPU 的原子指令(cmpxchg 指令),不会造成所谓的数据不一致问题,`Unsafe` 提供的 CAS 方法(如 `compareAndSwapXXX`)底层实现即为 CPU 指令 `cmpxchg` 。 - -#### 典型应用 - -在 JUC 包的并发工具类中大量地使用了 CAS 操作,像在前面介绍`synchronized`和`AQS`的文章中也多次提到了 CAS,其作为乐观锁在并发工具类中广泛发挥了作用。在 `Unsafe` 类中,提供了`compareAndSwapObject`、`compareAndSwapInt`、`compareAndSwapLong`方法来实现的对`Object`、`int`、`long`类型的 CAS 操作。以`compareAndSwapInt`方法为例: - -```java -public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x); -``` - -参数中`o`为需要更新的对象,`offset`是对象`o`中整形字段的偏移量,如果这个字段的值与`expected`相同,则将字段的值设为`x`这个新值,并且此更新是不可被中断的,也就是一个原子操作。下面是一个使用`compareAndSwapInt`的例子: - -```java -private volatile int a; -public static void main(String[] args){ - CasTest casTest=new CasTest(); - new Thread(()->{ - for (int i = 1; i < 5; i++) { - casTest.increment(i); - System.out.print(casTest.a+" "); - } - }).start(); - new Thread(()->{ - for (int i = 5 ; i <10 ; i++) { - casTest.increment(i); - System.out.print(casTest.a+" "); - } - }).start(); -} - -private void increment(int x){ - while (true){ - try { - long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a")); - if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x)) - break; - } catch (NoSuchFieldException e) { - e.printStackTrace(); - } - } -} -``` - -运行代码会依次输出: - -```plain -1 2 3 4 5 6 7 8 9 -``` - -在上面的例子中,使用两个线程去修改`int`型属性`a`的值,并且只有在`a`的值等于传入的参数`x`减一时,才会将`a`的值变为`x`,也就是实现对`a`的加一的操作。流程如下所示: - -![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144939826.png) - -需要注意的是,在调用`compareAndSwapInt`方法后,会直接返回`true`或`false`的修改结果,因此需要我们在代码中手动添加自旋的逻辑。在`AtomicInteger`类的设计中,也是采用了将`compareAndSwapInt`的结果作为循环条件,直至修改成功才退出死循环的方式来实现的原子性的自增操作。 - -### 线程调度 - -#### 介绍 - -`Unsafe` 类中提供了`park`、`unpark`、`monitorEnter`、`monitorExit`、`tryMonitorEnter`方法进行线程调度。 - -```java -//取消阻塞线程 -public native void unpark(Object thread); -//阻塞线程 -public native void park(boolean isAbsolute, long time); -//获得对象锁(可重入锁) -@Deprecated -public native void monitorEnter(Object o); -//释放对象锁 -@Deprecated -public native void monitorExit(Object o); -//尝试获取对象锁 -@Deprecated -public native boolean tryMonitorEnter(Object o); -``` - -方法 `park`、`unpark` 即可实现线程的挂起与恢复,将一个线程进行挂起是通过 `park` 方法实现的,调用 `park` 方法后,线程将一直阻塞直到超时或者中断等条件出现;`unpark` 可以终止一个挂起的线程,使其恢复正常。 - -此外,`Unsafe` 源码中`monitor`相关的三个方法已经被标记为`deprecated`,不建议被使用: - -```java -//获得对象锁 -@Deprecated -public native void monitorEnter(Object var1); -//释放对象锁 -@Deprecated -public native void monitorExit(Object var1); -//尝试获得对象锁 -@Deprecated -public native boolean tryMonitorEnter(Object var1); -``` - -`monitorEnter`方法用于获得对象锁,`monitorExit`用于释放对象锁,如果对一个没有被`monitorEnter`加锁的对象执行此方法,会抛出`IllegalMonitorStateException`异常。`tryMonitorEnter`方法尝试获取对象锁,如果成功则返回`true`,反之返回`false`。 - -#### 典型应用 - -Java 锁和同步器框架的核心类 `AbstractQueuedSynchronizer` (AQS),就是通过调用`LockSupport.park()`和`LockSupport.unpark()`实现线程的阻塞和唤醒的,而 `LockSupport` 的 `park`、`unpark` 方法实际是调用 `Unsafe` 的 `park`、`unpark` 方式实现的。 - -```java -public static void park(Object blocker) { - Thread t = Thread.currentThread(); - setBlocker(t, blocker); - UNSAFE.park(false, 0L); - setBlocker(t, null); -} -public static void unpark(Thread thread) { - if (thread != null) - UNSAFE.unpark(thread); -} -``` - -`LockSupport` 的`park`方法调用了 `Unsafe` 的`park`方法来阻塞当前线程,此方法将线程阻塞后就不会继续往后执行,直到有其他线程调用`unpark`方法唤醒当前线程。下面的例子对 `Unsafe` 的这两个方法进行测试: - -```java -public static void main(String[] args) { - Thread mainThread = Thread.currentThread(); - new Thread(()->{ - try { - TimeUnit.SECONDS.sleep(5); - System.out.println("subThread try to unpark mainThread"); - unsafe.unpark(mainThread); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }).start(); - - System.out.println("park main mainThread"); - unsafe.park(false,0L); - System.out.println("unpark mainThread success"); -} -``` - -程序输出为: - -```plain -park main mainThread -subThread try to unpark mainThread -unpark mainThread success -``` - -程序运行的流程也比较容易看懂,子线程开始运行后先进行睡眠,确保主线程能够调用`park`方法阻塞自己,子线程在睡眠 5 秒后,调用`unpark`方法唤醒主线程,使主线程能继续向下执行。整个流程如下图所示: - -![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144950116.png) - -### Class 操作 - -#### 介绍 - -`Unsafe` 对`Class`的相关操作主要包括类加载和静态变量的操作方法。 - -**静态属性读取相关的方法** - -```java -//获取静态属性的偏移量 -public native long staticFieldOffset(Field f); -//获取静态属性的对象指针 -public native Object staticFieldBase(Field f); -//判断类是否需要初始化(用于获取类的静态属性前进行检测) -public native boolean shouldBeInitialized(Class c); -``` - -创建一个包含静态属性的类,进行测试: - -```java -@Data -public class User { - public static String name="Hydra"; - int age; -} -private void staticTest() throws Exception { - User user=new User(); - // 也可以用下面的语句触发类初始化 - // 1. - // unsafe.ensureClassInitialized(User.class); - // 2. - // System.out.println(User.name); - System.out.println(unsafe.shouldBeInitialized(User.class)); - Field sexField = User.class.getDeclaredField("name"); - long fieldOffset = unsafe.staticFieldOffset(sexField); - Object fieldBase = unsafe.staticFieldBase(sexField); - Object object = unsafe.getObject(fieldBase, fieldOffset); - System.out.println(object); -} -``` - -运行结果: - -```plain -false -Hydra -``` - -在 `Unsafe` 的对象操作中,我们学习了通过`objectFieldOffset`方法获取对象属性偏移量并基于它对变量的值进行存取,但是它不适用于类中的静态属性,这时候就需要使用`staticFieldOffset`方法。在上面的代码中,只有在获取`Field`对象的过程中依赖到了`Class`,而获取静态变量的属性时不再依赖于`Class`。 - -在上面的代码中首先创建一个`User`对象,这是因为如果一个类没有被初始化,那么它的静态属性也不会被初始化,最后获取的字段属性将是`null`。所以在获取静态属性前,需要调用`shouldBeInitialized`方法,判断在获取前是否需要初始化这个类。如果删除创建 User 对象的语句,运行结果会变为: - -```plain -true -null -``` - -**使用`defineClass`方法允许程序在运行时动态地创建一个类** - -```java -public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader,ProtectionDomain protectionDomain); -``` - -在实际使用过程中,可以只传入字节数组、起始字节的下标以及读取的字节长度,默认情况下,类加载器(`ClassLoader`)和保护域(`ProtectionDomain`)来源于调用此方法的实例。下面的例子中实现了反编译生成后的 class 文件的功能: - -```java -private static void defineTest() { - String fileName="F:\\workspace\\unsafe-test\\target\\classes\\com\\cn\\model\\User.class"; - File file = new File(fileName); - try(FileInputStream fis = new FileInputStream(file)) { - byte[] content=new byte[(int)file.length()]; - fis.read(content); - Class clazz = unsafe.defineClass(null, content, 0, content.length, null, null); - Object o = clazz.newInstance(); - Object age = clazz.getMethod("getAge").invoke(o, null); - System.out.println(age); - } catch (Exception e) { - e.printStackTrace(); - } -} -``` - -在上面的代码中,首先读取了一个`class`文件并通过文件流将它转化为字节数组,之后使用`defineClass`方法动态的创建了一个类,并在后续完成了它的实例化工作,流程如下图所示,并且通过这种方式创建的类,会跳过 JVM 的所有安全检查。 - -![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717145000710.png) - -除了`defineClass`方法外,Unsafe 还提供了一个`defineAnonymousClass`方法: - -```java -public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches); -``` - -使用该方法可以用来动态的创建一个匿名类,在`Lambda`表达式中就是使用 ASM 动态生成字节码,然后利用该方法定义实现相应的函数式接口的匿名类。在 JDK 15 发布的新特性中,在隐藏类(`Hidden classes`)一条中,指出将在未来的版本中弃用 `Unsafe` 的`defineAnonymousClass`方法。 - -#### 典型应用 - -Lambda 表达式实现需要依赖 `Unsafe` 的 `defineAnonymousClass` 方法定义实现相应的函数式接口的匿名类。 - -### 系统信息 - -#### 介绍 - -这部分包含两个获取系统相关信息的方法。 - -```java -//返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。 -public native int addressSize(); -//内存页的大小,此值为2的幂次方。 -public native int pageSize(); -``` - -#### 典型应用 - -这两个方法的应用场景比较少,在`java.nio.Bits`类中,在使用`pageCount`计算所需的内存页的数量时,调用了`pageSize`方法获取内存页的大小。另外,在使用`copySwapMemory`方法拷贝内存时,调用了`addressSize`方法,检测 32 位系统的情况。 - -## 总结 - -在本文中,我们首先介绍了 `Unsafe` 的基本概念、工作原理,并在此基础上,对它的 API 进行了说明与实践。相信大家通过这一过程,能够发现 `Unsafe` 在某些场景下,确实能够为我们提供编程中的便利。但是回到开头的话题,在使用这些便利时,确实存在着一些安全上的隐患,在我看来,一项技术具有不安全因素并不可怕,可怕的是它在使用过程中被滥用。尽管之前有传言说会在 Java9 中移除 `Unsafe` 类,不过它还是照样已经存活到了 Java16。按照存在即合理的逻辑,只要使用得当,它还是能给我们带来不少的帮助,因此最后还是建议大家,在使用 `Unsafe` 的过程中一定要做到使用谨慎使用、避免滥用。 - - +private \ No newline at end of file diff --git a/docs/java/basis/why-there-only-value-passing-in-java.md b/docs/java/basis/why-there-only-value-passing-in-java.md index dc329cd32b6..5d5ac813dc3 100644 --- a/docs/java/basis/why-there-only-value-passing-in-java.md +++ b/docs/java/basis/why-there-only-value-passing-in-java.md @@ -1,48 +1,48 @@ --- -title: Java 值传递详解 +title: Detailed Explanation of Java Value Passing category: Java tag: - - Java基础 + - Java Basics --- -开始之前,我们先来搞懂下面这两个概念: +Before we begin, let's clarify the following two concepts: -- 形参&实参 -- 值传递&引用传递 +- Formal Parameters & Actual Parameters +- Value Passing & Reference Passing -## 形参&实参 +## Formal Parameters & Actual Parameters -方法的定义可能会用到 **参数**(有参的方法),参数在程序语言中分为: +The definition of a method may involve **parameters** (methods with parameters), which are categorized in programming languages as: -- **实参(实际参数,Arguments)**:用于传递给函数/方法的参数,必须有确定的值。 -- **形参(形式参数,Parameters)**:用于定义函数/方法,接收实参,不需要有确定的值。 +- **Actual Parameters (Arguments)**: Parameters passed to a function/method that must have a definite value. +- **Formal Parameters (Parameters)**: Used to define a function/method, receiving actual parameters, and do not need to have a definite value. ```java String hello = "Hello!"; -// hello 为实参 +// hello is the actual parameter sayHello(hello); -// str 为形参 +// str is the formal parameter void sayHello(String str) { System.out.println(str); } ``` -## 值传递&引用传递 +## Value Passing & Reference Passing -程序设计语言将实参传递给方法(或函数)的方式分为两种: +Programming languages categorize the way actual parameters are passed to methods (or functions) into two types: -- **值传递**:方法接收的是实参值的拷贝,会创建副本。 -- **引用传递**:方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。 +- **Value Passing**: The method receives a copy of the actual parameter's value, creating a duplicate. +- **Reference Passing**: The method directly receives the address of the object referenced by the actual parameter in the heap, without creating a duplicate. Modifications to the formal parameter will affect the actual parameter. -很多程序设计语言(比如 C++、 Pascal)提供了两种参数传递的方式,不过,在 Java 中只有值传递。 +Many programming languages (like C++, Pascal) provide both ways of parameter passing; however, in Java, only value passing is available. -## 为什么 Java 只有值传递? +## Why Does Java Only Have Value Passing? -**为什么说 Java 只有值传递呢?** 不需要太多废话,我通过 3 个例子来给大家证明。 +**Why do we say Java only has value passing?** Without further ado, I will demonstrate this with three examples. -### 案例 1:传递基本类型参数 +### Case 1: Passing Primitive Type Parameters -代码: +Code: ```java public static void main(String[] args) { @@ -62,7 +62,7 @@ public static void swap(int a, int b) { } ``` -输出: +Output: ```plain a = 20 @@ -71,57 +71,57 @@ num1 = 10 num2 = 20 ``` -解析: +Analysis: -在 `swap()` 方法中,`a`、`b` 的值进行交换,并不会影响到 `num1`、`num2`。因为,`a`、`b` 的值,只是从 `num1`、`num2` 的复制过来的。也就是说,a、b 相当于 `num1`、`num2` 的副本,副本的内容无论怎么修改,都不会影响到原件本身。 +In the `swap()` method, the values of `a` and `b` are swapped, but this does not affect `num1` and `num2`. This is because the values of `a` and `b` are merely copies of `num1` and `num2`. In other words, `a` and `b` are copies of `num1` and `num2`, and any modifications to the copies do not affect the original values. ![](https://oss.javaguide.cn/github/javaguide/java/basis/java-value-passing-01.png) -通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看案例 2。 +From the above example, we understand that a method cannot modify a parameter of a primitive data type. However, passing object references as parameters is different; let's look at Case 2. -### 案例 2:传递引用类型参数 1 +### Case 2: Passing Reference Type Parameters 1 -代码: +Code: ```java - public static void main(String[] args) { - int[] arr = { 1, 2, 3, 4, 5 }; - System.out.println(arr[0]); - change(arr); - System.out.println(arr[0]); - } - - public static void change(int[] array) { - // 将数组的第一个元素变为0 - array[0] = 0; - } +public static void main(String[] args) { + int[] arr = { 1, 2, 3, 4, 5 }; + System.out.println(arr[0]); + change(arr); + System.out.println(arr[0]); +} + +public static void change(int[] array) { + // Change the first element of the array to 0 + array[0] = 0; +} ``` -输出: +Output: ```plain 1 0 ``` -解析: +Analysis: ![](https://oss.javaguide.cn/github/javaguide/java/basis/java-value-passing-02.png) -看了这个案例很多人肯定觉得 Java 对引用类型的参数采用的是引用传递。 +After seeing this case, many people might think that Java uses reference passing for reference type parameters. -实际上,并不是的,这里传递的还是值,不过,这个值是实参的地址罢了! +In fact, that is not the case; what is passed is still a value, but this value is the address of the actual parameter! -也就是说 `change` 方法的参数拷贝的是 `arr` (实参)的地址,因此,它和 `arr` 指向的是同一个数组对象。这也就说明了为什么方法内部对形参的修改会影响到实参。 +This means that the parameter of the `change` method is a copy of the address of `arr` (the actual parameter), so it points to the same array object as `arr`. This explains why modifications to the formal parameter affect the actual parameter. -为了更强有力地反驳 Java 对引用类型的参数采用的不是引用传递,我们再来看下面这个案例! +To further refute the idea that Java uses reference passing for reference type parameters, let's look at the next case! -### 案例 3:传递引用类型参数 2 +### Case 3: Passing Reference Type Parameters 2 ```java public class Person { private String name; - // 省略构造函数、Getter&Setter方法 + // Omitted constructor, Getter & Setter methods } public static void main(String[] args) { @@ -132,88 +132,4 @@ public static void main(String[] args) { System.out.println("xiaoLi:" + xiaoLi.getName()); } -public static void swap(Person person1, Person person2) { - Person temp = person1; - person1 = person2; - person2 = temp; - System.out.println("person1:" + person1.getName()); - System.out.println("person2:" + person2.getName()); -} -``` - -输出: - -```plain -person1:小李 -person2:小张 -xiaoZhang:小张 -xiaoLi:小李 -``` - -解析: - -怎么回事???两个引用类型的形参互换并没有影响实参啊! - -`swap` 方法的参数 `person1` 和 `person2` 只是拷贝的实参 `xiaoZhang` 和 `xiaoLi` 的地址。因此, `person1` 和 `person2` 的互换只是拷贝的两个地址的互换罢了,并不会影响到实参 `xiaoZhang` 和 `xiaoLi` 。 - -![](https://oss.javaguide.cn/github/javaguide/java/basis/java-value-passing-03.png) - -## 引用传递是怎么样的? - -看到这里,相信你已经知道了 Java 中只有值传递,是没有引用传递的。 -但是,引用传递到底长什么样呢?下面以 `C++` 的代码为例,让你看一下引用传递的庐山真面目。 - -```C++ -#include - -void incr(int& num) -{ - std::cout << "incr before: " << num << "\n"; - num++; - std::cout << "incr after: " << num << "\n"; -} - -int main() -{ - int age = 10; - std::cout << "invoke before: " << age << "\n"; - incr(age); - std::cout << "invoke after: " << age << "\n"; -} -``` - -输出结果: - -```plain -invoke before: 10 -incr before: 10 -incr after: 11 -invoke after: 11 -``` - -分析:可以看到,在 `incr` 函数中对形参的修改,可以影响到实参的值。要注意:这里的 `incr` 形参的数据类型用的是 `int&` 才为引用传递,如果是用 `int` 的话还是值传递哦! - -## 为什么 Java 不引入引用传递呢? - -引用传递看似很好,能在方法内就直接把实参的值修改了,但是,为什么 Java 不引入引用传递呢? - -**注意:以下为个人观点看法,并非来自于 Java 官方:** - -1. 出于安全考虑,方法内部对值进行的操作,对于调用者都是未知的(把方法定义为接口,调用方不关心具体实现)。你也想象一下,如果拿着银行卡去取钱,取的是 100,扣的是 200,是不是很可怕。 -2. Java 之父 James Gosling 在设计之初就看到了 C、C++ 的许多弊端,所以才想着去设计一门新的语言 Java。在他设计 Java 的时候就遵循了简单易用的原则,摒弃了许多开发者一不留意就会造成问题的“特性”,语言本身的东西少了,开发者要学习的东西也少了。 - -## 总结 - -Java 中将实参传递给方法(或函数)的方式是 **值传递**: - -- 如果参数是基本类型的话,很简单,传递的就是基本类型的字面量值的拷贝,会创建副本。 -- 如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本。 - -## 参考 - -- 《Java 核心技术卷 Ⅰ》基础知识第十版第四章 4.5 小节 -- [Java 到底是值传递还是引用传递? - Hollis 的回答 - 知乎](https://www.zhihu.com/question/31203609/answer/576030121) -- [Oracle Java Tutorials - Passing Information to a Method or a Constructor](https://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html) -- [Interview with James Gosling, Father of Java](https://mappingthejourney.com/single-post/2017/06/29/episode-3-interview-with-james-gosling-father-of-java/) - - +public static void swap(Person person1, Person person \ No newline at end of file diff --git a/docs/java/collection/arrayblockingqueue-source-code.md b/docs/java/collection/arrayblockingqueue-source-code.md index 4c923ef0d29..110c51130de 100644 --- a/docs/java/collection/arrayblockingqueue-source-code.md +++ b/docs/java/collection/arrayblockingqueue-source-code.md @@ -1,36 +1,36 @@ --- -title: ArrayBlockingQueue 源码分析 +title: ArrayBlockingQueue Source Code Analysis category: Java tag: - - Java集合 + - Java Collections --- -## 阻塞队列简介 +## Introduction to Blocking Queues -### 阻塞队列的历史 +### History of Blocking Queues -Java 阻塞队列的历史可以追溯到 JDK1.5 版本,当时 Java 平台增加了 `java.util.concurrent`,即我们常说的 JUC 包,其中包含了各种并发流程控制工具、并发容器、原子类等。这其中自然也包含了我们这篇文章所讨论的阻塞队列。 +The history of Java's blocking queues can be traced back to JDK 1.5, when the Java platform introduced `java.util.concurrent`, commonly referred to as the JUC package, which includes various concurrency control tools, concurrent containers, atomic classes, and more. Naturally, this also includes the blocking queues discussed in this article. -为了解决高并发场景下多线程之间数据共享的问题,JDK1.5 版本中出现了 `ArrayBlockingQueue` 和 `LinkedBlockingQueue`,它们是带有生产者-消费者模式实现的并发容器。其中,`ArrayBlockingQueue` 是有界队列,即添加的元素达到上限之后,再次添加就会被阻塞或者抛出异常。而 `LinkedBlockingQueue` 则由链表构成的队列,正是因为链表的特性,所以 `LinkedBlockingQueue` 在添加元素上并不会向 `ArrayBlockingQueue` 那样有着较多的约束,所以 `LinkedBlockingQueue` 设置队列是否有界是可选的(注意这里的无界并不是指可以添加任意数量的元素,而是说队列的大小默认为 `Integer.MAX_VALUE`,近乎于无限大)。 +To address the issue of data sharing among multiple threads in high-concurrency scenarios, JDK 1.5 introduced `ArrayBlockingQueue` and `LinkedBlockingQueue`, which are concurrent containers implementing the producer-consumer pattern. Among them, `ArrayBlockingQueue` is a bounded queue, meaning that once the number of added elements reaches its limit, any further additions will be blocked or throw an exception. In contrast, `LinkedBlockingQueue` is a queue constructed from a linked list, and due to the nature of linked lists, `LinkedBlockingQueue` does not impose as many constraints on adding elements as `ArrayBlockingQueue`, allowing it to optionally be bounded (note that "unbounded" here does not mean it can accept an arbitrary number of elements, but rather that the default size of the queue is `Integer.MAX_VALUE`, which is nearly infinite). -随着 Java 的不断发展,JDK 后续的几个版本又对阻塞队列进行了不少的更新和完善: +As Java has continued to evolve, subsequent versions of JDK have made numerous updates and improvements to blocking queues: -1. JDK1.6 版本:增加 `SynchronousQueue`,一个不存储元素的阻塞队列。 -2. JDK1.7 版本:增加 `TransferQueue`,一个支持更多操作的阻塞队列。 -3. JDK1.8 版本:增加 `DelayQueue`,一个支持延迟获取元素的阻塞队列。 +1. JDK 1.6: Introduced `SynchronousQueue`, a blocking queue that does not store elements. +2. JDK 1.7: Introduced `TransferQueue`, a blocking queue that supports more operations. +3. JDK 1.8: Introduced `DelayQueue`, a blocking queue that supports delayed retrieval of elements. -### 阻塞队列的思想 +### The Concept of Blocking Queues -阻塞队列就是典型的生产者-消费者模型,它可以做到以下几点: +A blocking queue is a typical producer-consumer model that can achieve the following: -1. 当阻塞队列数据为空时,所有的消费者线程都会被阻塞,等待队列非空。 -2. 当生产者往队列里填充数据后,队列就会通知消费者队列非空,消费者此时就可以进来消费。 -3. 当阻塞队列因为消费者消费过慢或者生产者存放元素过快导致队列填满时无法容纳新元素时,生产者就会被阻塞,等待队列非满时继续存放元素。 -4. 当消费者从队列中消费一个元素之后,队列就会通知生产者队列非满,生产者可以继续填充数据了。 +1. When the blocking queue is empty, all consumer threads will be blocked, waiting for the queue to become non-empty. +2. When the producer fills the queue with data, the queue will notify consumers that it is non-empty, allowing them to consume. +3. When the blocking queue is full due to slow consumption or fast production, the producer will be blocked, waiting until the queue is not full to continue adding elements. +4. After a consumer consumes an element from the queue, the queue will notify the producer that it is not full, allowing the producer to continue adding data. -总结一下:阻塞队列就说基于非空和非满两个条件实现生产者和消费者之间的交互,尽管这些交互流程和等待通知的机制实现非常复杂,好在 Doug Lea 的操刀之下已将阻塞队列的细节屏蔽,我们只需调用 `put`、`take`、`offer`、`poll` 等 API 即可实现多线程之间的生产和消费。 +In summary, the blocking queue facilitates interaction between producers and consumers based on the conditions of being non-empty and non-full. Although the implementation of these interaction processes and the waiting-notification mechanism is quite complex, thanks to Doug Lea's work, the details of blocking queues are abstracted away, and we only need to call APIs like `put`, `take`, `offer`, and `poll` to achieve production and consumption between multiple threads. -这也使得阻塞队列在多线程开发中有着广泛的运用,最常见的例子无非是我们的线程池,从源码中我们就能看出当核心线程无法及时处理任务时,这些任务都会扔到 `workQueue` 中。 +This has led to the widespread use of blocking queues in multithreaded development, with the most common example being our thread pools. From the source code, we can see that when core threads cannot process tasks in time, these tasks are thrown into the `workQueue`. ```java public ThreadPoolExecutor(int corePoolSize, @@ -42,732 +42,23 @@ public ThreadPoolExecutor(int corePoolSize, RejectedExecutionHandler handler) {// ...} ``` -## ArrayBlockingQueue 常见方法及测试 +## Common Methods and Testing of ArrayBlockingQueue -简单了解了阻塞队列的历史之后,我们就开始重点讨论本篇文章所要介绍的并发容器——`ArrayBlockingQueue`。为了后续更加深入的了解 `ArrayBlockingQueue`,我们不妨基于下面几个实例了解以下 `ArrayBlockingQueue` 的使用。 +After briefly understanding the history of blocking queues, we will focus on the concurrent container that this article aims to introduce—`ArrayBlockingQueue`. To gain a deeper understanding of `ArrayBlockingQueue`, let's explore its usage through the following examples. -先看看第一个例子,我们这里会用两个线程分别模拟生产者和消费者,生产者生产完会使用 `put` 方法生产 10 个元素给消费者进行消费,当队列元素达到我们设置的上限 5 时,`put` 方法就会阻塞。 -同理消费者也会通过 `take` 方法消费元素,当队列为空时,`take` 方法就会阻塞消费者线程。这里笔者为了保证消费者能够在消费完 10 个元素后及时退出。便通过倒计时门闩,来控制消费者结束,生产者在这里只会生产 10 个元素。当消费者将 10 个元素消费完成之后,按下倒计时门闩,所有线程都会停止。 +First, let's look at the first example, where we will use two threads to simulate a producer and a consumer. The producer will produce 10 elements using the `put` method for the consumer to consume. When the number of elements in the queue reaches our set limit of 5, the `put` method will block. +Similarly, the consumer will consume elements using the `take` method, and when the queue is empty, the `take` method will block the consumer thread. To ensure that the consumer can exit promptly after consuming 10 elements, I used a countdown latch to control the consumer's termination, while the producer will only produce 10 elements. After the consumer has consumed all 10 elements, it will press the countdown latch, causing all threads to stop. ```java public class ProducerConsumerExample { public static void main(String[] args) throws InterruptedException { - // 创建一个大小为 5 的 ArrayBlockingQueue + // Create an ArrayBlockingQueue with a size of 5 ArrayBlockingQueue queue = new ArrayBlockingQueue<>(5); - // 创建生产者线程 + // Create producer thread Thread producer = new Thread(() -> { try { for (int i = 1; i <= 10; i++) { - // 向队列中添加元素,如果队列已满则阻塞等待 - queue.put(i); - System.out.println("生产者添加元素:" + i); - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - - }); - - CountDownLatch countDownLatch = new CountDownLatch(1); - - // 创建消费者线程 - Thread consumer = new Thread(() -> { - try { - int count = 0; - while (true) { - - // 从队列中取出元素,如果队列为空则阻塞等待 - int element = queue.take(); - System.out.println("消费者取出元素:" + element); - ++count; - if (count == 10) { - break; - } - } - - countDownLatch.countDown(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - }); - - // 启动线程 - producer.start(); - consumer.start(); - - // 等待线程结束 - producer.join(); - consumer.join(); - - countDownLatch.await(); - - producer.interrupt(); - consumer.interrupt(); - } - -} -``` - -代码输出结果如下,可以看到只有生产者往队列中投放元素之后消费者才能消费,这也就意味着当队列中没有数据的时消费者就会阻塞,等待队列非空再继续消费。 - -```cpp -生产者添加元素:1 -生产者添加元素:2 -消费者取出元素:1 -消费者取出元素:2 -生产者添加元素:3 -消费者取出元素:3 -生产者添加元素:4 -生产者添加元素:5 -消费者取出元素:4 -生产者添加元素:6 -消费者取出元素:5 -生产者添加元素:7 -生产者添加元素:8 -生产者添加元素:9 -生产者添加元素:10 -消费者取出元素:6 -消费者取出元素:7 -消费者取出元素:8 -消费者取出元素:9 -消费者取出元素:10 -``` - -了解了 `put`、`take` 这两个会阻塞的存和取方法之后,我我们再来看看阻塞队列中非阻塞的入队和出队方法 `offer` 和 `poll`。 - -如下所示,我们设置了一个大小为 3 的阻塞队列,我们会尝试在队列用 offer 方法存放 4 个元素,然后再从队列中用 `poll` 尝试取 4 次。 - -```cpp -public class OfferPollExample { - - public static void main(String[] args) { - // 创建一个大小为 3 的 ArrayBlockingQueue - ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3); - - // 向队列中添加元素 - System.out.println(queue.offer("A")); - System.out.println(queue.offer("B")); - System.out.println(queue.offer("C")); - - // 尝试向队列中添加元素,但队列已满,返回 false - System.out.println(queue.offer("D")); - - // 从队列中取出元素 - System.out.println(queue.poll()); - System.out.println(queue.poll()); - System.out.println(queue.poll()); - - // 尝试从队列中取出元素,但队列已空,返回 null - System.out.println(queue.poll()); - } - -} -``` - -最终代码的输出结果如下,可以看到因为队列的大小为 3 的缘故,我们前 3 次存放到队列的结果为 true,第 4 次存放时,由于队列已满,所以存放结果返回 false。这也是为什么我们后续的 `poll` 方法只得到了 3 个元素的值。 - -```cpp -true -true -true -false -A -B -C -null -``` - -了解了阻塞存取和非阻塞存取,我们再来看看阻塞队列的一个比较特殊的操作,某些场景下,我们希望能够一次性将阻塞队列的结果存到列表中再进行批量操作,我们就可以使用阻塞队列的 `drainTo` 方法,这个方法会一次性将队列中所有元素存放到列表,如果队列中有元素,且成功存到 list 中则 `drainTo` 会返回本次转移到 list 中的元素数,反之若队列为空,`drainTo` 则直接返回 0。 - -```java -public class DrainToExample { - - public static void main(String[] args) { - // 创建一个大小为 5 的 ArrayBlockingQueue - ArrayBlockingQueue queue = new ArrayBlockingQueue<>(5); - - // 向队列中添加元素 - queue.add(1); - queue.add(2); - queue.add(3); - queue.add(4); - queue.add(5); - - // 创建一个 List,用于存储从队列中取出的元素 - List list = new ArrayList<>(); - - // 从队列中取出所有元素,并添加到 List 中 - queue.drainTo(list); - - // 输出 List 中的元素 - System.out.println(list); - } - -} -``` - -代码输出结果如下 - -```cpp -[1, 2, 3, 4, 5] -``` - -## ArrayBlockingQueue 源码分析 - -自此我们对阻塞队列的使用有了基本的印象,接下来我们就可以进一步了解一下 `ArrayBlockingQueue` 的工作机制了。 - -### 整体设计 - -在了解 `ArrayBlockingQueue` 的具体细节之前,我们先来看看 `ArrayBlockingQueue` 的类图。 - -![ArrayBlockingQueue 类图](https://oss.javaguide.cn/github/javaguide/java/collection/arrayblockingqueue-class-diagram.png) - -从图中我们可以看出,`ArrayBlockingQueue` 继承了阻塞队列 `BlockingQueue` 这个接口,不难猜出通过继承 `BlockingQueue` 这个接口之后,`ArrayBlockingQueue` 就拥有了阻塞队列那些常见的操作行为。 - -同时, `ArrayBlockingQueue` 还继承了 `AbstractQueue` 这个抽象类,这个继承了 `AbstractCollection` 和 `Queue` 的抽象类,从抽象类的特定和语义我们也可以猜出,这个继承关系使得 `ArrayBlockingQueue` 拥有了队列的常见操作。 - -所以我们是否可以得出这样一个结论,通过继承 `AbstractQueue` 获得队列所有的操作模板,其实现的入队和出队操作的整体框架。然后 `ArrayBlockingQueue` 通过继承 `BlockingQueue` 获取到阻塞队列的常见操作并将这些操作实现,填充到 `AbstractQueue` 模板方法的细节中,由此 `ArrayBlockingQueue` 成为一个完整的阻塞队列。 - -为了印证这一点,我们到源码中一探究竟。首先我们先来看看 `AbstractQueue`,从类的继承关系我们可以大致得出,它通过 `AbstractCollection` 获得了集合的常见操作方法,然后通过 `Queue` 接口获得了队列的特性。 - -```java -public abstract class AbstractQueue - extends AbstractCollection - implements Queue { - //... -} -``` - -对于集合的操作无非是增删改查,所以我们不妨从添加方法入手,从源码中我们可以看到,它实现了 `AbstractCollection` 的 `add` 方法,其内部逻辑如下: - -1. 调用继承 `Queue` 接口的来的 `offer` 方法,如果 `offer` 成功则返回 `true`。 -2. 如果 `offer` 失败,即代表当前元素入队失败直接抛异常。 - -```java -public boolean add(E e) { - if (offer(e)) - return true; - else - throw new IllegalStateException("Queue full"); -} -``` - -而 `AbstractQueue` 中并没有对 `Queue` 的 `offer` 的实现,很明显这样做的目的是定义好了 `add` 的核心逻辑,将 `offer` 的细节交由其子类即我们的 `ArrayBlockingQueue` 实现。 - -到此,我们对于抽象类 `AbstractQueue` 的分析就结束了,我们继续看看 `ArrayBlockingQueue` 中另一个重要的继承接口 `BlockingQueue`。 - -点开 `BlockingQueue` 之后,我们可以看到这个接口同样继承了 `Queue` 接口,这就意味着它也具备了队列所拥有的所有行为。同时,它还定义了自己所需要实现的方法。 - -```java -public interface BlockingQueue extends Queue { - - //元素入队成功返回true,反之则会抛出异常IllegalStateException - boolean add(E e); - - //元素入队成功返回true,反之返回false - boolean offer(E e); - - //元素入队成功则直接返回,如果队列已满元素不可入队则将线程阻塞,因为阻塞期间可能会被打断,所以这里方法签名抛出了InterruptedException - void put(E e) throws InterruptedException; - - //和上一个方法一样,只不过队列满时只会阻塞单位为unit,时间为timeout的时长,如果在等待时长内没有入队成功则直接返回false。 - boolean offer(E e, long timeout, TimeUnit unit) - throws InterruptedException; - - //从队头取出一个元素,如果队列为空则阻塞等待,因为会阻塞线程的缘故,所以该方法可能会被打断,所以签名定义了InterruptedException - E take() throws InterruptedException; - - //取出队头的元素并返回,如果当前队列为空则阻塞等待timeout且单位为unit的时长,如果这个时间段没有元素则直接返回null。 - E poll(long timeout, TimeUnit unit) - throws InterruptedException; - - //获取队列剩余元素个数 - int remainingCapacity(); - - //删除我们指定的对象,如果成功返回true,反之返回false。 - boolean remove(Object o); - - //判断队列中是否包含指定元素 - public boolean contains(Object o); - - //将队列中的元素全部存到指定的集合中 - int drainTo(Collection c); - - //转移maxElements个元素到集合中 - int drainTo(Collection c, int maxElements); -} -``` - -了解了 `BlockingQueue` 的常见操作后,我们就知道了 `ArrayBlockingQueue` 通过继承 `BlockingQueue` 的方法并实现后,填充到 `AbstractQueue` 的方法上,由此我们便知道了上文中 `AbstractQueue` 的 `add` 方法的 `offer` 方法是哪里是实现的了。 - -```java -public boolean add(E e) { - //AbstractQueue的offer来自下层的ArrayBlockingQueue从BlockingQueue继承并实现的offer方法 - if (offer(e)) - return true; - else - throw new IllegalStateException("Queue full"); -} -``` - -### 初始化 - -了解 `ArrayBlockingQueue` 的细节前,我们不妨先看看其构造函数,了解一下其初始化过程。从源码中我们可以看出 `ArrayBlockingQueue` 有 3 个构造方法,而最核心的构造方法就是下方这一个。 - -```java -// capacity 表示队列初始容量,fair 表示 锁的公平性 -public ArrayBlockingQueue(int capacity, boolean fair) { - //如果设置的队列大小小于0,则直接抛出IllegalArgumentException - if (capacity <= 0) - throw new IllegalArgumentException(); - //初始化一个数组用于存放队列的元素 - this.items = new Object[capacity]; - //创建阻塞队列流程控制的锁 - lock = new ReentrantLock(fair); - //用lock锁创建两个条件控制队列生产和消费 - notEmpty = lock.newCondition(); - notFull = lock.newCondition(); -} -``` - -这个构造方法里面有两个比较核心的成员变量 `notEmpty`(非空) 和 `notFull` (非满) ,需要我们格外留意,它们是实现生产者和消费者有序工作的关键所在,这一点笔者会在后续的源码解析中详细说明,这里我们只需初步了解一下阻塞队列的构造即可。 - -另外两个构造方法都是基于上述的构造方法,默认情况下,我们会使用下面这个构造方法,该构造方法就意味着 `ArrayBlockingQueue` 用的是非公平锁,即各个生产者或者消费者线程收到通知后,对于锁的争抢是随机的。 - -```java - public ArrayBlockingQueue(int capacity) { - this(capacity, false); - } -``` - -还有一个不怎么常用的构造方法,在初始化容量和锁的非公平性之后,它还提供了一个 `Collection` 参数,从源码中不难看出这个构造方法是将外部传入的集合的元素在初始化时直接存放到阻塞队列中。 - -```java -public ArrayBlockingQueue(int capacity, boolean fair, - Collection c) { - //初始化容量和锁的公平性 - this(capacity, fair); - - final ReentrantLock lock = this.lock; - //上锁并将c中的元素存放到ArrayBlockingQueue底层的数组中 - lock.lock(); - try { - int i = 0; - try { - //遍历并添加元素到数组中 - for (E e : c) { - checkNotNull(e); - items[i++] = e; - } - } catch (ArrayIndexOutOfBoundsException ex) { - throw new IllegalArgumentException(); - } - //记录当前队列容量 - count = i; - //更新下一次put或者offer或用add方法添加到队列底层数组的位置 - putIndex = (i == capacity) ? 0 : i; - } finally { - //完成遍历后释放锁 - lock.unlock(); - } -} -``` - -### 阻塞式获取和新增元素 - -`ArrayBlockingQueue` 阻塞式获取和新增元素对应的就是生产者-消费者模型,虽然它也支持非阻塞式获取和新增元素(例如 `poll()` 和 `offer(E e)` 方法,后文会介绍到),但一般不会使用。 - -`ArrayBlockingQueue` 阻塞式获取和新增元素的方法为: - -- `put(E e)`:将元素插入队列中,如果队列已满,则该方法会一直阻塞,直到队列有空间可用或者线程被中断。 -- `take()` :获取并移除队列头部的元素,如果队列为空,则该方法会一直阻塞,直到队列非空或者线程被中断。 - -这两个方法实现的关键就是在于两个条件对象 `notEmpty`(非空) 和 `notFull` (非满),这个我们在上文的构造方法中有提到。 - -接下来笔者就通过两张图让大家了解一下这两个条件是如何在阻塞队列中运用的。 - -![ArrayBlockingQueue 非空条件](https://oss.javaguide.cn/github/javaguide/java/collection/ArrayBlockingQueue-notEmpty-take.png) - -假设我们的代码消费者先启动,当它发现队列中没有数据,那么非空条件就会将这个线程挂起,即等待条件非空时挂起。然后 CPU 执行权到达生产者,生产者发现队列中可以存放数据,于是将数据存放进去,通知此时条件非空,此时消费者就会被唤醒到队列中使用 `take` 等方法获取值了。 - -![ArrayBlockingQueue 非满条件](https://oss.javaguide.cn/github/javaguide/java/collection/ArrayBlockingQueue-notFull-put.png) - -随后的执行中,生产者生产速度远远大于消费者消费速度,于是生产者将队列塞满后再次尝试将数据存入队列,发现队列已满,于是阻塞队列就将当前线程挂起,等待非满。然后消费者拿着 CPU 执行权进行消费,于是队列可以存放新数据了,发出一个非满的通知,此时挂起的生产者就会等待 CPU 执行权到来时再次尝试将数据存到队列中。 - -简单了解阻塞队列的基于两个条件的交互流程之后,我们不妨看看 `put` 和 `take` 方法的源码。 - -```java -public void put(E e) throws InterruptedException { - //确保插入的元素不为null - checkNotNull(e); - //加锁 - final ReentrantLock lock = this.lock; - //这里使用lockInterruptibly()方法而不是lock()方法是为了能够响应中断操作,如果在等待获取锁的过程中被打断则该方法会抛出InterruptedException异常。 - lock.lockInterruptibly(); - try { - //如果count等数组长度则说明队列已满,当前线程将被挂起放到AQS队列中,等待队列非满时插入(非满条件)。 - //在等待期间,锁会被释放,其他线程可以继续对队列进行操作。 - while (count == items.length) - notFull.await(); - //如果队列可以存放元素,则调用enqueue将元素入队 - enqueue(e); - } finally { - //释放锁 - lock.unlock(); - } -} -``` - -`put`方法内部调用了 `enqueue` 方法来实现元素入队,我们继续深入查看一下 `enqueue` 方法的实现细节: - -```java -private void enqueue(E x) { - //获取队列底层的数组 - final Object[] items = this.items; - //将putindex位置的值设置为我们传入的x - items[putIndex] = x; - //更新putindex,如果putindex等于数组长度,则更新为0 - if (++putIndex == items.length) - putIndex = 0; - //队列长度+1 - count++; - //通知队列非空,那些因为获取元素而阻塞的线程可以继续工作了 - notEmpty.signal(); -} -``` - -从源码中可以看到入队操作的逻辑就是在数组中追加一个新元素,整体执行步骤为: - -1. 获取 `ArrayBlockingQueue` 底层的数组 `items`。 -2. 将元素存到 `putIndex` 位置。 -3. 更新 `putIndex` 到下一个位置,如果 `putIndex` 等于队列长度,则说明 `putIndex` 已经到达数组末尾了,下一次插入则需要 0 开始。(`ArrayBlockingQueue` 用到了循环队列的思想,即从头到尾循环复用一个数组) -4. 更新 `count` 的值,表示当前队列长度+1。 -5. 调用 `notEmpty.signal()` 通知队列非空,消费者可以从队列中获取值了。 - -自此我们了解了 `put` 方法的流程,为了更加完整的了解 `ArrayBlockingQueue` 关于生产者-消费者模型的设计,我们继续看看阻塞获取队列元素的 `take` 方法。 - -```java -public E take() throws InterruptedException { - //获取锁 - final ReentrantLock lock = this.lock; - lock.lockInterruptibly(); - try { - //如果队列中元素个数为0,则将当前线程打断并存入AQS队列中,等待队列非空时获取并移除元素(非空条件) - while (count == 0) - notEmpty.await(); - //如果队列不为空则调用dequeue获取元素 - return dequeue(); - } finally { - //释放锁 - lock.unlock(); - } -} -``` - -理解了 `put` 方法再看`take` 方法就很简单了,其核心逻辑和`put` 方法正好是相反的,比如`put` 方法在队列满的时候等待队列非满时插入元素(非满条件),而`take` 方法等待队列非空时获取并移除元素(非空条件)。 - -`take`方法内部调用了 `dequeue` 方法来实现元素出队,其核心逻辑和 `enqueue` 方法也是相反的。 - -```java -private E dequeue() { - //获取阻塞队列底层的数组 - final Object[] items = this.items; - @SuppressWarnings("unchecked") - //从队列中获取takeIndex位置的元素 - E x = (E) items[takeIndex]; - //将takeIndex置空 - items[takeIndex] = null; - //takeIndex向后挪动,如果等于数组长度则更新为0 - if (++takeIndex == items.length) - takeIndex = 0; - //队列长度减1 - count--; - if (itrs != null) - itrs.elementDequeued(); - //通知那些被打断的线程当前队列状态非满,可以继续存放元素 - notFull.signal(); - return x; -} -``` - -由于`dequeue` 方法(出队)和上面介绍的 `enqueue` 方法(入队)的步骤大致类似,这里就不重复介绍了。 - -为了帮助理解,我专门画了一张图来展示 `notEmpty`(非空) 和 `notFull` (非满)这两个条件对象是如何控制 `ArrayBlockingQueue` 的存和取的。 - -![ArrayBlockingQueue 非空非满](https://oss.javaguide.cn/github/javaguide/java/collection/ArrayBlockingQueue-notEmpty-notFull.png) - -- **消费者**:当消费者从队列中 `take` 或者 `poll` 等操作取出一个元素之后,就会通知队列非满,此时那些等待非满的生产者就会被唤醒等待获取 CPU 时间片进行入队操作。 -- **生产者**:当生产者将元素存到队列中后,就会触发通知队列非空,此时消费者就会被唤醒等待 CPU 时间片尝试获取元素。如此往复,两个条件对象就构成一个环路,控制着多线程之间的存和取。 - -### 非阻塞式获取和新增元素 - -`ArrayBlockingQueue` 非阻塞式获取和新增元素的方法为: - -- `offer(E e)`:将元素插入队列尾部。如果队列已满,则该方法会直接返回 false,不会等待并阻塞线程。 -- `poll()`:获取并移除队列头部的元素,如果队列为空,则该方法会直接返回 null,不会等待并阻塞线程。 -- `add(E e)`:将元素插入队列尾部。如果队列已满则会抛出 `IllegalStateException` 异常,底层基于 `offer(E e)` 方法。 -- `remove()`:移除队列头部的元素,如果队列为空则会抛出 `NoSuchElementException` 异常,底层基于 `poll()`。 -- `peek()`:获取但不移除队列头部的元素,如果队列为空,则该方法会直接返回 null,不会等待并阻塞线程。 - -先来看看 `offer` 方法,逻辑和 `put` 差不多,唯一的区别就是入队失败时不会阻塞当前线程,而是直接返回 `false`。 - -```java -public boolean offer(E e) { - //确保插入的元素不为null - checkNotNull(e); - //获取锁 - final ReentrantLock lock = this.lock; - lock.lock(); - try { - //队列已满直接返回false - if (count == items.length) - return false; - else { - //反之将元素入队并直接返回true - enqueue(e); - return true; - } - } finally { - //释放锁 - lock.unlock(); - } - } -``` - -`poll` 方法同理,获取元素失败也是直接返回空,并不会阻塞获取元素的线程。 - -```java -public E poll() { - final ReentrantLock lock = this.lock; - //上锁 - lock.lock(); - try { - //如果队列为空直接返回null,反之出队返回元素值 - return (count == 0) ? null : dequeue(); - } finally { - lock.unlock(); - } - } -``` - -`add` 方法其实就是对于 `offer` 做了一层封装,如下代码所示,可以看到 `add` 会调用没有规定时间的 `offer`,如果入队失败则直接抛异常。 - -```java -public boolean add(E e) { - return super.add(e); - } - - -public boolean add(E e) { - //调用offer方法如果失败直接抛出异常 - if (offer(e)) - return true; - else - throw new IllegalStateException("Queue full"); - } -``` - -`remove` 方法同理,调用 `poll`,如果返回 `null` 则说明队列没有元素,直接抛出异常。 - -```java -public E remove() { - E x = poll(); - if (x != null) - return x; - else - throw new NoSuchElementException(); - } -``` - -`peek()` 方法的逻辑也很简单,内部调用了 `itemAt` 方法。 - -```java -public E peek() { - //加锁 - final ReentrantLock lock = this.lock; - lock.lock(); - try { - //当队列为空时返回 null - return itemAt(takeIndex); - } finally { - //释放锁 - lock.unlock(); - } - } - -//返回队列中指定位置的元素 -@SuppressWarnings("unchecked") -final E itemAt(int i) { - return (E) items[i]; -} -``` - -### 指定超时时间内阻塞式获取和新增元素 - -在 `offer(E e)` 和 `poll()` 非阻塞获取和新增元素的基础上,设计者提供了带有等待时间的 `offer(E e, long timeout, TimeUnit unit)` 和 `poll(long timeout, TimeUnit unit)` ,用于在指定的超时时间内阻塞式地添加和获取元素。 - -```java - public boolean offer(E e, long timeout, TimeUnit unit) - throws InterruptedException { - - checkNotNull(e); - long nanos = unit.toNanos(timeout); - final ReentrantLock lock = this.lock; - lock.lockInterruptibly(); - try { - //队列已满,进入循环 - while (count == items.length) { - //时间到了队列还是满的,则直接返回false - if (nanos <= 0) - return false; - //阻塞nanos时间,等待非满 - nanos = notFull.awaitNanos(nanos); - } - enqueue(e); - return true; - } finally { - lock.unlock(); - } - } -``` - -可以看到,带有超时时间的 `offer` 方法在队列已满的情况下,会等待用户所传的时间段,如果规定时间内还不能存放元素则直接返回 `false`。 - -```java -public E poll(long timeout, TimeUnit unit) throws InterruptedException { - long nanos = unit.toNanos(timeout); - final ReentrantLock lock = this.lock; - lock.lockInterruptibly(); - try { - //队列为空,循环等待,若时间到还是空的,则直接返回null - while (count == 0) { - if (nanos <= 0) - return null; - nanos = notEmpty.awaitNanos(nanos); - } - return dequeue(); - } finally { - lock.unlock(); - } - } -``` - -同理,带有超时时间的 `poll` 也一样,队列为空则在规定时间内等待,若时间到了还是空的,则直接返回 null。 - -### 判断元素是否存在 - -`ArrayBlockingQueue` 提供了 `contains(Object o)` 来判断指定元素是否存在于队列中。 - -```java -public boolean contains(Object o) { - //若目标元素为空,则直接返回 false - if (o == null) return false; - //获取当前队列的元素数组 - final Object[] items = this.items; - //加锁 - final ReentrantLock lock = this.lock; - lock.lock(); - try { - // 如果队列非空 - if (count > 0) { - final int putIndex = this.putIndex; - //从队列头部开始遍历 - int i = takeIndex; - do { - if (o.equals(items[i])) - return true; - if (++i == items.length) - i = 0; - } while (i != putIndex); - } - return false; - } finally { - //释放锁 - lock.unlock(); - } -} -``` - -## ArrayBlockingQueue 获取和新增元素的方法对比 - -为了帮助理解 `ArrayBlockingQueue` ,我们再来对比一下上面提到的这些获取和新增元素的方法。 - -新增元素: - -| 方法 | 队列满时处理方式 | 方法返回值 | -| ----------------------------------------- | -------------------------------------------------------- | ---------- | -| `put(E e)` | 线程阻塞,直到中断或被唤醒 | void | -| `offer(E e)` | 直接返回 false | boolean | -| `offer(E e, long timeout, TimeUnit unit)` | 指定超时时间内阻塞,超过规定时间还未添加成功则返回 false | boolean | -| `add(E e)` | 直接抛出 `IllegalStateException` 异常 | boolean | - -获取/移除元素: - -| 方法 | 队列空时处理方式 | 方法返回值 | -| ----------------------------------- | --------------------------------------------------- | ---------- | -| `take()` | 线程阻塞,直到中断或被唤醒 | E | -| `poll()` | 返回 null | E | -| `poll(long timeout, TimeUnit unit)` | 指定超时时间内阻塞,超过规定时间还是空的则返回 null | E | -| `peek()` | 返回 null | E | -| `remove()` | 直接抛出 `NoSuchElementException` 异常 | boolean | - -![](https://oss.javaguide.cn/github/javaguide/java/collection/ArrayBlockingQueue-get-add-element-methods.png) - -## ArrayBlockingQueue 相关面试题 - -### ArrayBlockingQueue 是什么?它的特点是什么? - -`ArrayBlockingQueue` 是 `BlockingQueue` 接口的有界队列实现类,常用于多线程之间的数据共享,底层采用数组实现,从其名字就能看出来了。 - -`ArrayBlockingQueue` 的容量有限,一旦创建,容量不能改变。 - -为了保证线程安全,`ArrayBlockingQueue` 的并发控制采用可重入锁 `ReentrantLock` ,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。并且,它还支持公平和非公平两种方式的锁访问机制,默认是非公平锁。 - -`ArrayBlockingQueue` 虽名为阻塞队列,但也支持非阻塞获取和新增元素(例如 `poll()` 和 `offer(E e)` 方法),只是队列满时添加元素会抛出异常,队列为空时获取的元素为 null,一般不会使用。 - -### ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别? - -`ArrayBlockingQueue` 和 `LinkedBlockingQueue` 是 Java 并发包中常用的两种阻塞队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别: - -- 底层实现:`ArrayBlockingQueue` 基于数组实现,而 `LinkedBlockingQueue` 基于链表实现。 -- 是否有界:`ArrayBlockingQueue` 是有界队列,必须在创建时指定容量大小。`LinkedBlockingQueue` 创建时可以不指定容量大小,默认是`Integer.MAX_VALUE`,也就是无界的。但也可以指定队列大小,从而成为有界的。 -- 锁是否分离: `ArrayBlockingQueue`中的锁是没有分离的,即生产和消费用的是同一个锁;`LinkedBlockingQueue`中的锁是分离的,即生产用的是`putLock`,消费是`takeLock`,这样可以防止生产者和消费者线程之间的锁争夺。 -- 内存占用:`ArrayBlockingQueue` 需要提前分配数组内存,而 `LinkedBlockingQueue` 则是动态分配链表节点内存。这意味着,`ArrayBlockingQueue` 在创建时就会占用一定的内存空间,且往往申请的内存比实际所用的内存更大,而`LinkedBlockingQueue` 则是根据元素的增加而逐渐占用内存空间。 - -### ArrayBlockingQueue 和 ConcurrentLinkedQueue 有什么区别? - -`ArrayBlockingQueue` 和 `ConcurrentLinkedQueue` 是 Java 并发包中常用的两种队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别: - -- 底层实现:`ArrayBlockingQueue` 基于数组实现,而 `ConcurrentLinkedQueue` 基于链表实现。 -- 是否有界:`ArrayBlockingQueue` 是有界队列,必须在创建时指定容量大小,而 `ConcurrentLinkedQueue` 是无界队列,可以动态地增加容量。 -- 是否阻塞:`ArrayBlockingQueue` 支持阻塞和非阻塞两种获取和新增元素的方式(一般只会使用前者), `ConcurrentLinkedQueue` 是无界的,仅支持非阻塞式获取和新增元素。 - -### ArrayBlockingQueue 的实现原理是什么? - -`ArrayBlockingQueue` 的实现原理主要分为以下几点(这里以阻塞式获取和新增元素为例介绍): - -- `ArrayBlockingQueue` 内部维护一个定长的数组用于存储元素。 -- 通过使用 `ReentrantLock` 锁对象对读写操作进行同步,即通过锁机制来实现线程安全。 -- 通过 `Condition` 实现线程间的等待和唤醒操作。 - -这里再详细介绍一下线程间的等待和唤醒具体的实现(不需要记具体的方法,面试中回答要点即可): - -- 当队列已满时,生产者线程会调用 `notFull.await()` 方法让生产者进行等待,等待队列非满时插入(非满条件)。 -- 当队列为空时,消费者线程会调用 `notEmpty.await()`方法让消费者进行等待,等待队列非空时消费(非空条件)。 -- 当有新的元素被添加时,生产者线程会调用 `notEmpty.signal()`方法唤醒正在等待消费的消费者线程。 -- 当队列中有元素被取出时,消费者线程会调用 `notFull.signal()`方法唤醒正在等待插入元素的生产者线程。 - -关于 `Condition`接口的补充: - -> `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 `Condition` 接口默认提供的。而`synchronized`关键字就相当于整个 `Lock` 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而`Condition`实例的`signalAll()`方法,只会唤醒注册在该`Condition`实例中的所有等待线程。 - -## 参考文献 - -- 深入理解 Java 系列 | BlockingQueue 用法详解: -- 深入浅出阻塞队列 BlockingQueue 及其典型实现 ArrayBlockingQueue: -- 并发编程大扫盲:ArrayBlockingQueue 底层原理和实战: - + // Add elements to the queue; if the queue \ No newline at end of file diff --git a/docs/java/collection/arraylist-source-code.md b/docs/java/collection/arraylist-source-code.md index 5c71801b699..ecac0119580 100644 --- a/docs/java/collection/arraylist-source-code.md +++ b/docs/java/collection/arraylist-source-code.md @@ -1,43 +1,41 @@ --- -title: ArrayList 源码分析 +title: ArrayList Source Code Analysis category: Java tag: - - Java集合 + - Java Collections --- -## ArrayList 简介 +## Introduction to ArrayList -`ArrayList` 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用`ensureCapacity`操作来增加 `ArrayList` 实例的容量。这可以减少递增式再分配的数量。 +The underlying structure of `ArrayList` is an array queue, which is equivalent to a dynamic array. Compared to arrays in Java, its capacity can grow dynamically. Before adding a large number of elements, applications can use the `ensureCapacity` operation to increase the capacity of the `ArrayList` instance. This can reduce the number of incremental reallocations. -`ArrayList` 继承于 `AbstractList` ,实现了 `List`, `RandomAccess`, `Cloneable`, `java.io.Serializable` 这些接口。 +`ArrayList` inherits from `AbstractList` and implements the interfaces `List`, `RandomAccess`, `Cloneable`, and `java.io.Serializable`. ```java - public class ArrayList extends AbstractList - implements List, RandomAccess, Cloneable, java.io.Serializable{ - - } + implements List, RandomAccess, Cloneable, java.io.Serializable { +} ``` -- `List` : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。 -- `RandomAccess` :这是一个标志接口,表明实现这个接口的 `List` 集合是支持 **快速随机访问** 的。在 `ArrayList` 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 -- `Cloneable` :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。 -- `Serializable` : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。 +- `List`: Indicates that it is a list that supports operations such as adding, deleting, and searching, and can be accessed by index. +- `RandomAccess`: This is a marker interface indicating that the `List` collection implementing this interface supports **fast random access**. In `ArrayList`, we can quickly retrieve an element object by its index, which is fast random access. +- `Cloneable`: Indicates that it has the ability to be copied, allowing for deep or shallow copy operations. +- `Serializable`: Indicates that it can be serialized, meaning it can convert objects into byte streams for persistent storage or network transmission, which is very convenient. -![ArrayList 类图](https://oss.javaguide.cn/github/javaguide/java/collection/arraylist-class-diagram.png) +![ArrayList Class Diagram](https://oss.javaguide.cn/github/javaguide/java/collection/arraylist-class-diagram.png) -### ArrayList 和 Vector 的区别?(了解即可) +### What is the difference between ArrayList and Vector? (For understanding only) -- `ArrayList` 是 `List` 的主要实现类,底层使用 `Object[]`存储,适用于频繁的查找工作,线程不安全 。 -- `Vector` 是 `List` 的古老实现类,底层使用`Object[]` 存储,线程安全。 +- `ArrayList` is the main implementation class of `List`, using `Object[]` for storage, suitable for frequent lookup operations, and is not thread-safe. +- `Vector` is an older implementation class of `List`, also using `Object[]` for storage, and is thread-safe. -### ArrayList 可以添加 null 值吗? +### Can ArrayList add null values? -`ArrayList` 中可以存储任何类型的对象,包括 `null` 值。不过,不建议向`ArrayList` 中添加 `null` 值, `null` 值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常。 +`ArrayList` can store any type of object, including `null` values. However, it is not recommended to add `null` values to `ArrayList`, as `null` values are meaningless and can make the code difficult to maintain. For example, forgetting to handle null checks can lead to a NullPointerException. -示例代码: +Example code: ```java ArrayList listOfStrings = new ArrayList<>(); @@ -46,924 +44,18 @@ listOfStrings.add("java"); System.out.println(listOfStrings); ``` -输出: +Output: ```plain [null, java] ``` -### Arraylist 与 LinkedList 区别? - -- **是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全; -- **底层数据结构:** `ArrayList` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) -- **插入和删除是否受元素位置的影响:** - - `ArrayList` 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行`add(E e)`方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element)`),时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 - - `LinkedList` 采用链表存储,所以在头尾插入或者删除元素不受元素位置的影响(`add(E e)`、`addFirst(E e)`、`addLast(E e)`、`removeFirst()`、 `removeLast()`),时间复杂度为 O(1),如果是要在指定位置 `i` 插入和删除元素的话(`add(int index, E element)`,`remove(Object o)`,`remove(int index)`), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入和删除。 -- **是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList`(实现了 `RandomAccess` 接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 -- **内存空间占用:** `ArrayList` 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。 - -## ArrayList 核心源码解读 - -这里以 JDK1.8 为例,分析一下 `ArrayList` 的底层源码。 - -```java -public class ArrayList extends AbstractList - implements List, RandomAccess, Cloneable, java.io.Serializable { - private static final long serialVersionUID = 8683452581122892189L; - - /** - * 默认初始容量大小 - */ - private static final int DEFAULT_CAPACITY = 10; - - /** - * 空数组(用于空实例)。 - */ - private static final Object[] EMPTY_ELEMENTDATA = {}; - - //用于默认大小空实例的共享空数组实例。 - //我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。 - private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; - - /** - * 保存ArrayList数据的数组 - */ - transient Object[] elementData; // non-private to simplify nested class access - - /** - * ArrayList 所包含的元素个数 - */ - private int size; - - /** - * 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小) - */ - public ArrayList(int initialCapacity) { - if (initialCapacity > 0) { - //如果传入的参数大于0,创建initialCapacity大小的数组 - this.elementData = new Object[initialCapacity]; - } else if (initialCapacity == 0) { - //如果传入的参数等于0,创建空数组 - this.elementData = EMPTY_ELEMENTDATA; - } else { - //其他情况,抛出异常 - throw new IllegalArgumentException("Illegal Capacity: " + - initialCapacity); - } - } - - /** - * 默认无参构造函数 - * DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10 - */ - public ArrayList() { - this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; - } - - /** - * 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。 - */ - public ArrayList(Collection c) { - //将指定集合转换为数组 - elementData = c.toArray(); - //如果elementData数组的长度不为0 - if ((size = elementData.length) != 0) { - // 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断) - if (elementData.getClass() != Object[].class) - //将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组 - elementData = Arrays.copyOf(elementData, size, Object[].class); - } else { - // 其他情况,用空数组代替 - this.elementData = EMPTY_ELEMENTDATA; - } - } - - /** - * 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。 - */ - public void trimToSize() { - modCount++; - if (size < elementData.length) { - elementData = (size == 0) - ? EMPTY_ELEMENTDATA - : Arrays.copyOf(elementData, size); - } - } -//下面是ArrayList的扩容机制 -//ArrayList的扩容机制提高了性能,如果每次只扩充一个, -//那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。 - - /** - * 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量 - * - * @param minCapacity 所需的最小容量 - */ - public void ensureCapacity(int minCapacity) { - // 如果不是默认空数组,则minExpand的值为0; - // 如果是默认空数组,则minExpand的值为10 - int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // 如果不是默认元素表,则可以使用任意大小 - ? 0 - // 如果是默认空数组,它应该已经是默认大小 - : DEFAULT_CAPACITY; - - // 如果最小容量大于已有的最大容量 - if (minCapacity > minExpand) { - // 根据需要的最小容量,确保容量足够 - ensureExplicitCapacity(minCapacity); - } - } - - - // 根据给定的最小容量和当前数组元素来计算所需容量。 - private static int calculateCapacity(Object[] elementData, int minCapacity) { - // 如果当前数组元素为空数组(初始情况),返回默认容量和最小容量中的较大值作为所需容量 - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - return Math.max(DEFAULT_CAPACITY, minCapacity); - } - // 否则直接返回最小容量 - return minCapacity; - } - - // 确保内部容量达到指定的最小容量。 - private void ensureCapacityInternal(int minCapacity) { - ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); - } - - //判断是否需要扩容 - private void ensureExplicitCapacity(int minCapacity) { - modCount++; - // overflow-conscious code - if (minCapacity - elementData.length > 0) - //调用grow方法进行扩容,调用此方法代表已经开始扩容了 - grow(minCapacity); - } - - /** - * 要分配的最大数组大小 - */ - private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - - /** - * ArrayList扩容的核心方法。 - */ - private void grow(int minCapacity) { - // oldCapacity为旧容量,newCapacity为新容量 - int oldCapacity = elementData.length; - //将oldCapacity 右移一位,其效果相当于oldCapacity /2, - //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, - int newCapacity = oldCapacity + (oldCapacity >> 1); - //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - //再检查新容量是否超出了ArrayList所定义的最大容量, - //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE, - //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。 - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); - } - - //比较minCapacity和 MAX_ARRAY_SIZE - private static int hugeCapacity(int minCapacity) { - if (minCapacity < 0) // overflow - throw new OutOfMemoryError(); - return (minCapacity > MAX_ARRAY_SIZE) ? - Integer.MAX_VALUE : - MAX_ARRAY_SIZE; - } - - /** - * 返回此列表中的元素数。 - */ - public int size() { - return size; - } - - /** - * 如果此列表不包含元素,则返回 true 。 - */ - public boolean isEmpty() { - //注意=和==的区别 - return size == 0; - } - - /** - * 如果此列表包含指定的元素,则返回true 。 - */ - public boolean contains(Object o) { - //indexOf()方法:返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 - return indexOf(o) >= 0; - } - - /** - * 返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 - */ - public int indexOf(Object o) { - if (o == null) { - for (int i = 0; i < size; i++) - if (elementData[i] == null) - return i; - } else { - for (int i = 0; i < size; i++) - //equals()方法比较 - if (o.equals(elementData[i])) - return i; - } - return -1; - } - - /** - * 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。. - */ - public int lastIndexOf(Object o) { - if (o == null) { - for (int i = size - 1; i >= 0; i--) - if (elementData[i] == null) - return i; - } else { - for (int i = size - 1; i >= 0; i--) - if (o.equals(elementData[i])) - return i; - } - return -1; - } - - /** - * 返回此ArrayList实例的浅拷贝。 (元素本身不被复制。) - */ - public Object clone() { - try { - ArrayList v = (ArrayList) super.clone(); - //Arrays.copyOf功能是实现数组的复制,返回复制后的数组。参数是被复制的数组和复制的长度 - v.elementData = Arrays.copyOf(elementData, size); - v.modCount = 0; - return v; - } catch (CloneNotSupportedException e) { - // 这不应该发生,因为我们是可以克隆的 - throw new InternalError(e); - } - } - - /** - * 以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 - * 返回的数组将是“安全的”,因为该列表不保留对它的引用。 - * (换句话说,这个方法必须分配一个新的数组)。 - * 因此,调用者可以自由地修改返回的数组结构。 - * 注意:如果元素是引用类型,修改元素的内容会影响到原列表中的对象。 - * 此方法充当基于数组和基于集合的API之间的桥梁。 - */ - public Object[] toArray() { - return Arrays.copyOf(elementData, size); - } - - /** - * 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); - * 返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。 - * 否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。 - * 如果列表适用于指定的数组,其余空间(即数组的列表数量多于此元素),则紧跟在集合结束后的数组中的元素设置为null 。 - * (这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。) - */ - @SuppressWarnings("unchecked") - public T[] toArray(T[] a) { - if (a.length < size) - // 新建一个运行时类型的数组,但是ArrayList数组的内容 - return (T[]) Arrays.copyOf(elementData, size, a.getClass()); - //调用System提供的arraycopy()方法实现数组之间的复制 - System.arraycopy(elementData, 0, a, 0, size); - if (a.length > size) - a[size] = null; - return a; - } - - // Positional Access Operations - - @SuppressWarnings("unchecked") - E elementData(int index) { - return (E) elementData[index]; - } - - /** - * 返回此列表中指定位置的元素。 - */ - public E get(int index) { - rangeCheck(index); - - return elementData(index); - } - - /** - * 用指定的元素替换此列表中指定位置的元素。 - */ - public E set(int index, E element) { - //对index进行界限检查 - rangeCheck(index); - - E oldValue = elementData(index); - elementData[index] = element; - //返回原来在这个位置的元素 - return oldValue; - } - - /** - * 将指定的元素追加到此列表的末尾。 - */ - public boolean add(E e) { - ensureCapacityInternal(size + 1); // Increments modCount!! - //这里看到ArrayList添加元素的实质就相当于为数组赋值 - elementData[size++] = e; - return true; - } - - /** - * 在此列表中的指定位置插入指定的元素。 - * 先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; - * 再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 - */ - public void add(int index, E element) { - rangeCheckForAdd(index); - - ensureCapacityInternal(size + 1); // Increments modCount!! - //arraycopy()这个实现数组之间复制的方法一定要看一下,下面就用到了arraycopy()方法实现数组自己复制自己 - System.arraycopy(elementData, index, elementData, index + 1, - size - index); - elementData[index] = element; - size++; - } - - /** - * 删除该列表中指定位置的元素。 将任何后续元素移动到左侧(从其索引中减去一个元素)。 - */ - public E remove(int index) { - rangeCheck(index); - - modCount++; - E oldValue = elementData(index); - - int numMoved = size - index - 1; - if (numMoved > 0) - System.arraycopy(elementData, index + 1, elementData, index, - numMoved); - elementData[--size] = null; // clear to let GC do its work - //从列表中删除的元素 - return oldValue; - } - - /** - * 从列表中删除指定元素的第一个出现(如果存在)。 如果列表不包含该元素,则它不会更改。 - * 返回true,如果此列表包含指定的元素 - */ - public boolean remove(Object o) { - if (o == null) { - for (int index = 0; index < size; index++) - if (elementData[index] == null) { - fastRemove(index); - return true; - } - } else { - for (int index = 0; index < size; index++) - if (o.equals(elementData[index])) { - fastRemove(index); - return true; - } - } - return false; - } - - /* - * 该方法为私有的移除方法,跳过了边界检查,并且不返回被移除的值。 - */ - private void fastRemove(int index) { - modCount++; - int numMoved = size - index - 1; - if (numMoved > 0) - System.arraycopy(elementData, index + 1, elementData, index, - numMoved); - elementData[--size] = null; // 在移除元素后,将该位置的元素设为 null,以便垃圾回收器(GC)能够回收该元素。 - } - - /** - * 从列表中删除所有元素。 - */ - public void clear() { - modCount++; - - // 把数组中所有的元素的值设为null - for (int i = 0; i < size; i++) - elementData[i] = null; - - size = 0; - } - - /** - * 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。 - */ - public boolean addAll(Collection c) { - Object[] a = c.toArray(); - int numNew = a.length; - ensureCapacityInternal(size + numNew); // Increments modCount - System.arraycopy(a, 0, elementData, size, numNew); - size += numNew; - return numNew != 0; - } - - /** - * 将指定集合中的所有元素插入到此列表中,从指定的位置开始。 - */ - public boolean addAll(int index, Collection c) { - rangeCheckForAdd(index); - - Object[] a = c.toArray(); - int numNew = a.length; - ensureCapacityInternal(size + numNew); // Increments modCount - - int numMoved = size - index; - if (numMoved > 0) - System.arraycopy(elementData, index, elementData, index + numNew, - numMoved); - - System.arraycopy(a, 0, elementData, index, numNew); - size += numNew; - return numNew != 0; - } - - /** - * 从此列表中删除所有索引为fromIndex (含)和toIndex之间的元素。 - * 将任何后续元素移动到左侧(减少其索引)。 - */ - protected void removeRange(int fromIndex, int toIndex) { - modCount++; - int numMoved = size - toIndex; - System.arraycopy(elementData, toIndex, elementData, fromIndex, - numMoved); - - // clear to let GC do its work - int newSize = size - (toIndex - fromIndex); - for (int i = newSize; i < size; i++) { - elementData[i] = null; - } - size = newSize; - } - - /** - * 检查给定的索引是否在范围内。 - */ - private void rangeCheck(int index) { - if (index >= size) - throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); - } - - /** - * add和addAll使用的rangeCheck的一个版本 - */ - private void rangeCheckForAdd(int index) { - if (index > size || index < 0) - throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); - } - - /** - * 返回IndexOutOfBoundsException细节信息 - */ - private String outOfBoundsMsg(int index) { - return "Index: " + index + ", Size: " + size; - } - - /** - * 从此列表中删除指定集合中包含的所有元素。 - */ - public boolean removeAll(Collection c) { - Objects.requireNonNull(c); - //如果此列表被修改则返回true - return batchRemove(c, false); - } - - /** - * 仅保留此列表中包含在指定集合中的元素。 - * 换句话说,从此列表中删除其中不包含在指定集合中的所有元素。 - */ - public boolean retainAll(Collection c) { - Objects.requireNonNull(c); - return batchRemove(c, true); - } - - - /** - * 从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。 - * 指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。 - * 返回的列表迭代器是fail-fast 。 - */ - public ListIterator listIterator(int index) { - if (index < 0 || index > size) - throw new IndexOutOfBoundsException("Index: " + index); - return new ListItr(index); - } - - /** - * 返回列表中的列表迭代器(按适当的顺序)。 - * 返回的列表迭代器是fail-fast 。 - */ - public ListIterator listIterator() { - return new ListItr(0); - } - - /** - * 以正确的顺序返回该列表中的元素的迭代器。 - * 返回的迭代器是fail-fast 。 - */ - public Iterator iterator() { - return new Itr(); - } -``` - -## ArrayList 扩容机制分析 - -### 先从 ArrayList 的构造函数说起 - -ArrayList 有三种方式来初始化,构造方法源码如下(JDK8): - -```java -/** - * 默认初始容量大小 - */ -private static final int DEFAULT_CAPACITY = 10; - -private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; - -/** - * 默认构造函数,使用初始容量10构造一个空列表(无参数构造) - */ -public ArrayList() { - this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; -} - -/** - * 带初始容量参数的构造函数。(用户自己指定容量) - */ -public ArrayList(int initialCapacity) { - if (initialCapacity > 0) {//初始容量大于0 - //创建initialCapacity大小的数组 - this.elementData = new Object[initialCapacity]; - } else if (initialCapacity == 0) {//初始容量等于0 - //创建空数组 - this.elementData = EMPTY_ELEMENTDATA; - } else {//初始容量小于0,抛出异常 - throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); - } -} - - -/** - *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回 - *如果指定的集合为null,throws NullPointerException。 - */ -public ArrayList(Collection c) { - elementData = c.toArray(); - if ((size = elementData.length) != 0) { - // c.toArray might (incorrectly) not return Object[] (see 6260652) - if (elementData.getClass() != Object[].class) - elementData = Arrays.copyOf(elementData, size, Object[].class); - } else { - // replace with empty array. - this.elementData = EMPTY_ELEMENTDATA; - } -} -``` - -细心的同学一定会发现:**以无参数构造方法创建 `ArrayList` 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。** 下面在我们分析 `ArrayList` 扩容时会讲到这一点内容! - -> 补充:JDK6 new 无参构造的 `ArrayList` 对象时,直接创建了长度是 10 的 `Object[]` 数组 `elementData` 。 - -### 一步一步分析 ArrayList 扩容机制 - -这里以无参构造函数创建的 `ArrayList` 为例分析。 - -#### add 方法 - -```java -/** -* 将指定的元素追加到此列表的末尾。 -*/ -public boolean add(E e) { - // 加元素之前,先调用ensureCapacityInternal方法 - ensureCapacityInternal(size + 1); // Increments modCount!! - // 这里看到ArrayList添加元素的实质就相当于为数组赋值 - elementData[size++] = e; - return true; -} -``` - -**注意**:JDK11 移除了 `ensureCapacityInternal()` 和 `ensureExplicitCapacity()` 方法 - -`ensureCapacityInternal` 方法的源码如下: - -```java -// 根据给定的最小容量和当前数组元素来计算所需容量。 -private static int calculateCapacity(Object[] elementData, int minCapacity) { - // 如果当前数组元素为空数组(初始情况),返回默认容量和最小容量中的较大值作为所需容量 - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - return Math.max(DEFAULT_CAPACITY, minCapacity); - } - // 否则直接返回最小容量 - return minCapacity; -} - -// 确保内部容量达到指定的最小容量。 -private void ensureCapacityInternal(int minCapacity) { - ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); -} -``` - -`ensureCapacityInternal` 方法非常简单,内部直接调用了 `ensureExplicitCapacity` 方法: - -```java -//判断是否需要扩容 -private void ensureExplicitCapacity(int minCapacity) { - modCount++; - //判断当前数组容量是否足以存储minCapacity个元素 - if (minCapacity - elementData.length > 0) - //调用grow方法进行扩容 - grow(minCapacity); -} -``` - -我们来仔细分析一下: - -- 当我们要 `add` 进第 1 个元素到 `ArrayList` 时,`elementData.length` 为 0 (因为还是一个空的 list),因为执行了 `ensureCapacityInternal()` 方法 ,所以 `minCapacity` 此时为 10。此时,`minCapacity - elementData.length > 0`成立,所以会进入 `grow(minCapacity)` 方法。 -- 当 `add` 第 2 个元素时,`minCapacity` 为 2,此时 `elementData.length`(容量)在添加第一个元素后扩容成 `10` 了。此时,`minCapacity - elementData.length > 0` 不成立,所以不会进入 (执行)`grow(minCapacity)` 方法。 -- 添加第 3、4···到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10。 - -直到添加第 11 个元素,`minCapacity`(为 11)比 `elementData.length`(为 10)要大。进入 `grow` 方法进行扩容。 - -#### grow 方法 - -```java -/** - * 要分配的最大数组大小 - */ -private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - -/** - * ArrayList扩容的核心方法。 - */ -private void grow(int minCapacity) { - // oldCapacity为旧容量,newCapacity为新容量 - int oldCapacity = elementData.length; - // 将oldCapacity 右移一位,其效果相当于oldCapacity /2, - // 我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, - int newCapacity = oldCapacity + (oldCapacity >> 1); - - // 然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - - // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE, - // 如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); -} -``` - -**`int newCapacity = oldCapacity + (oldCapacity >> 1)`,所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)!** 奇偶不同,比如:10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数. - -> ">>"(移位运算符):>>1 右移一位相当于除 2,右移 n 位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了 1 位所以相当于 oldCapacity /2。对于大数据的 2 进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源 - -**我们再来通过例子探究一下`grow()` 方法:** - -- 当 `add` 第 1 个元素时,`oldCapacity` 为 0,经比较后第一个 if 判断成立,`newCapacity = minCapacity`(为 10)。但是第二个 if 判断不会成立,即 `newCapacity` 不比 `MAX_ARRAY_SIZE` 大,则不会进入 `hugeCapacity` 方法。数组容量为 10,`add` 方法中 return true,size 增为 1。 -- 当 `add` 第 11 个元素进入 `grow` 方法时,`newCapacity` 为 15,比 `minCapacity`(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 `hugeCapacity` 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。 -- 以此类推······ - -**这里补充一点比较重要,但是容易被忽视掉的知识点:** - -- Java 中的 `length`属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. -- Java 中的 `length()` 方法是针对字符串说的,如果想看这个字符串的长度则用到 `length()` 这个方法. -- Java 中的 `size()` 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看! - -#### hugeCapacity() 方法 - -从上面 `grow()` 方法源码我们知道:如果新容量大于 `MAX_ARRAY_SIZE`,进入(执行) `hugeCapacity()` 方法来比较 `minCapacity` 和 `MAX_ARRAY_SIZE`,如果 `minCapacity` 大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 `MAX_ARRAY_SIZE` 即为 `Integer.MAX_VALUE - 8`。 - -```java -private static int hugeCapacity(int minCapacity) { - if (minCapacity < 0) // overflow - throw new OutOfMemoryError(); - // 对minCapacity和MAX_ARRAY_SIZE进行比较 - // 若minCapacity大,将Integer.MAX_VALUE作为新数组的大小 - // 若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小 - // MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - return (minCapacity > MAX_ARRAY_SIZE) ? - Integer.MAX_VALUE : - MAX_ARRAY_SIZE; -} -``` - -### `System.arraycopy()` 和 `Arrays.copyOf()`方法 - -阅读源码的话,我们就会发现 `ArrayList` 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及`add(int index, E element)`、`toArray()` 等方法中都用到了该方法! - -#### `System.arraycopy()` 方法 - -源码: - -```java - // 我们发现 arraycopy 是一个 native 方法,接下来我们解释一下各个参数的具体意义 - /** - * 复制数组 - * @param src 源数组 - * @param srcPos 源数组中的起始位置 - * @param dest 目标数组 - * @param destPos 目标数组中的起始位置 - * @param length 要复制的数组元素的数量 - */ - public static native void arraycopy(Object src, int srcPos, - Object dest, int destPos, - int length); -``` - -场景: - -```java - /** - * 在此列表中的指定位置插入指定的元素。 - *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; - *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 - */ - public void add(int index, E element) { - rangeCheckForAdd(index); - - ensureCapacityInternal(size + 1); // Increments modCount!! - //arraycopy()方法实现数组自己复制自己 - //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量; - System.arraycopy(elementData, index, elementData, index + 1, size - index); - elementData[index] = element; - size++; - } -``` - -我们写一个简单的方法测试以下: - -```java -public class ArraycopyTest { - - public static void main(String[] args) { - // TODO Auto-generated method stub - int[] a = new int[10]; - a[0] = 0; - a[1] = 1; - a[2] = 2; - a[3] = 3; - System.arraycopy(a, 2, a, 3, 3); - a[2]=99; - for (int i = 0; i < a.length; i++) { - System.out.print(a[i] + " "); - } - } - -} -``` - -结果: - -```plain -0 1 99 2 3 0 0 0 0 0 -``` - -#### `Arrays.copyOf()`方法 - -源码: - -```java - public static int[] copyOf(int[] original, int newLength) { - // 申请一个新的数组 - int[] copy = new int[newLength]; - // 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组 - System.arraycopy(original, 0, copy, 0, - Math.min(original.length, newLength)); - return copy; - } -``` - -场景: - -```java - /** - 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。 - */ - public Object[] toArray() { - //elementData:要复制的数组;size:要复制的长度 - return Arrays.copyOf(elementData, size); - } -``` - -个人觉得使用 `Arrays.copyOf()`方法主要是为了给原有数组扩容,测试代码如下: - -```java -public class ArrayscopyOfTest { - - public static void main(String[] args) { - int[] a = new int[3]; - a[0] = 0; - a[1] = 1; - a[2] = 2; - int[] b = Arrays.copyOf(a, 10); - System.out.println("b.length"+b.length); - } -} -``` - -结果: - -```plain -10 -``` - -#### 两者联系和区别 - -**联系:** - -看两者源代码可以发现 `copyOf()`内部实际调用了 `System.arraycopy()` 方法 - -**区别:** - -`arraycopy()` 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 `copyOf()` 是系统自动在内部新建一个数组,并返回该数组。 - -### `ensureCapacity`方法 - -`ArrayList` 源码中有一个 `ensureCapacity` 方法不知道大家注意到没有,这个方法 `ArrayList` 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢? - -```java - /** - 如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。 - * - * @param minCapacity 所需的最小容量 - */ - public void ensureCapacity(int minCapacity) { - int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // any size if not default element table - ? 0 - // larger than default for default empty table. It's already - // supposed to be at default size. - : DEFAULT_CAPACITY; - - if (minCapacity > minExpand) { - ensureExplicitCapacity(minCapacity); - } - } - -``` - -理论上来说,最好在向 `ArrayList` 添加大量元素之前用 `ensureCapacity` 方法,以减少增量重新分配的次数 - -我们通过下面的代码实际测试以下这个方法的效果: - -```java -public class EnsureCapacityTest { - public static void main(String[] args) { - ArrayList list = new ArrayList(); - final int N = 10000000; - long startTime = System.currentTimeMillis(); - for (int i = 0; i < N; i++) { - list.add(i); - } - long endTime = System.currentTimeMillis(); - System.out.println("使用ensureCapacity方法前:"+(endTime - startTime)); - - } -} -``` - -运行结果: - -```plain -使用ensureCapacity方法前:2158 -``` - -```java -public class EnsureCapacityTest { - public static void main(String[] args) { - ArrayList list = new ArrayList(); - final int N = 10000000; - long startTime1 = System.currentTimeMillis(); - list.ensureCapacity(N); - for (int i = 0; i < N; i++) { - list.add(i); - } - long endTime1 = System.currentTimeMillis(); - System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1)); - } -} -``` - -运行结果: - -```plain -使用ensureCapacity方法后:1773 -``` - -通过运行结果,我们可以看出向 `ArrayList` 添加大量元素之前使用`ensureCapacity` 方法可以提升性能。不过,这个性能差距几乎可以忽略不计。而且,实际项目根本也不可能往 `ArrayList` 里面添加这么多元素。 +### What is the difference between ArrayList and LinkedList? - +- **Thread Safety:** Both `ArrayList` and `LinkedList` are unsynchronized, meaning they do not guarantee thread safety. +- **Underlying Data Structure:** `ArrayList` uses an **`Object` array** for storage; `LinkedList` uses a **doubly linked list** data structure (before JDK1.6 it was a circular linked list, which was removed in JDK1.7. Note the difference between a doubly linked list and a doubly circular linked list, which is introduced below!). +- **Impact of Element Position on Insertion and Deletion:** + - `ArrayList` uses an array for storage, so the time complexity for inserting and deleting elements is affected by the position of the elements. For example, when executing the `add(E e)` method, `ArrayList` defaults to appending the specified element to the end of the list, which has a time complexity of O(1). However, if you want to insert or delete an element at a specified position `i` (`add(int index, E element)`), the time complexity becomes O(n) because the elements from index `i` and the subsequent (n-i) elements need to be shifted. + - `LinkedList` uses a linked list for storage, so inserting or deleting elements at the head or tail is not affected by the position of the elements (`add(E e)`, `addFirst(E e)`, `addLast(E e)`, `removeFirst()`, `removeLast()`), with a time complexity of O(1). However, if you want to insert or delete elements at a specified position `i` (`add(int index, E element)`, `remove(Object o)`, `remove(int index)`), the time complexity is O(n) because you need to move to the specified position first before inserting or deleting. +- **Support for Fast Random Access:** `LinkedList` does not support efficient random access to elements, while `ArrayList` (which implements the `RandomAccess` interface) does. Fast random access means quickly retrieving an element object by its index (corresponding to the `get(int index)` method). +- **Memory Space Usage:** The space waste of `ArrayList` mainly lies in reserving a certain capacity at the end of the list, diff --git a/docs/java/collection/concurrent-hash-map-source-code.md b/docs/java/collection/concurrent-hash-map-source-code.md index d0d210aacdf..fd77bc07164 100644 --- a/docs/java/collection/concurrent-hash-map-source-code.md +++ b/docs/java/collection/concurrent-hash-map-source-code.md @@ -1,25 +1,25 @@ --- -title: ConcurrentHashMap 源码分析 +title: Analysis of ConcurrentHashMap Source Code category: Java tag: - - Java集合 + - Java Collections --- -> 本文来自公众号:末读代码的投稿,原文地址: 。 +> This article is from the WeChat public account: Submission from Mo Du Code, original address: . -上一篇文章介绍了 HashMap 源码,反响不错,也有很多同学发表了自己的观点,这次又来了,这次是 `ConcurrentHashMap` 了,作为线程安全的 HashMap ,它的使用频率也是很高。那么它的存储结构和实现原理是怎么样的呢? +The previous article introduced the source code of HashMap, which received a good response, and many students shared their views. This time, we are here again, this time it's `ConcurrentHashMap`. As a thread-safe HashMap, it is also used frequently. So what is its storage structure and implementation principle? ## 1. ConcurrentHashMap 1.7 -### 1. 存储结构 +### 1. Storage Structure -![Java 7 ConcurrentHashMap 存储结构](https://oss.javaguide.cn/github/javaguide/java/collection/java7_concurrenthashmap.png) +![Java 7 ConcurrentHashMap Storage Structure](https://oss.javaguide.cn/github/javaguide/java/collection/java7_concurrenthashmap.png) -Java 7 中 `ConcurrentHashMap` 的存储结构如上图,`ConcurrnetHashMap` 由很多个 `Segment` 组合,而每一个 `Segment` 是一个类似于 `HashMap` 的结构,所以每一个 `HashMap` 的内部可以进行扩容。但是 `Segment` 的个数一旦**初始化就不能改变**,默认 `Segment` 的个数是 16 个,你也可以认为 `ConcurrentHashMap` 默认支持最多 16 个线程并发。 +The storage structure of `ConcurrentHashMap` in Java 7 is shown in the figure above. `ConcurrentHashMap` is composed of many `Segments`, and each `Segment` is a structure similar to `HashMap`, so each `HashMap` can be expanded internally. However, the number of `Segments` cannot be changed once it is **initialized**. The default number of `Segments` is 16, which means you can think of `ConcurrentHashMap` as supporting up to 16 threads concurrently by default. -### 2. 初始化 +### 2. Initialization -通过 `ConcurrentHashMap` 的无参构造探寻 `ConcurrentHashMap` 的初始化流程。 +Let's explore the initialization process of `ConcurrentHashMap` through its no-argument constructor. ```java /** @@ -31,62 +31,62 @@ Java 7 中 `ConcurrentHashMap` 的存储结构如上图,`ConcurrnetHashMap` } ``` -无参构造中调用了有参构造,传入了三个参数的默认值,他们的值是。 +The no-argument constructor calls the parameterized constructor, passing in the default values for the three parameters, which are: ```java /** - * 默认初始化容量 + * Default initial capacity */ static final int DEFAULT_INITIAL_CAPACITY = 16; /** - * 默认负载因子 + * Default load factor */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** - * 默认并发级别 + * Default concurrency level */ static final int DEFAULT_CONCURRENCY_LEVEL = 16; ``` -接着看下这个有参构造函数的内部实现逻辑。 +Next, let's look at the internal implementation logic of this parameterized constructor. ```java @SuppressWarnings("unchecked") public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) { - // 参数校验 + // Parameter validation if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); - // 校验并发级别大小,大于 1<<16,重置为 65536 + // Validate concurrency level size, reset to 65536 if greater than 1<<16 if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; // Find power-of-two sizes best matching arguments - // 2的多少次方 + // Power of 2 int sshift = 0; int ssize = 1; - // 这个循环可以找到 concurrencyLevel 之上最近的 2的次方值 + // This loop finds the nearest power of 2 above concurrencyLevel while (ssize < concurrencyLevel) { ++sshift; ssize <<= 1; } - // 记录段偏移量 + // Record segment shift this.segmentShift = 32 - sshift; - // 记录段掩码 + // Record segment mask this.segmentMask = ssize - 1; - // 设置容量 + // Set capacity if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; - // c = 容量 / ssize ,默认 16 / 16 = 1,这里是计算每个 Segment 中的类似于 HashMap 的容量 + // c = capacity / ssize, default 16 / 16 = 1, this calculates the capacity similar to HashMap in each Segment int c = initialCapacity / ssize; if (c * ssize < initialCapacity) ++c; int cap = MIN_SEGMENT_TABLE_CAPACITY; - //Segment 中的类似于 HashMap 的容量至少是2或者2的倍数 + // The capacity in Segment similar to HashMap must be at least 2 or a multiple of 2 while (cap < c) cap <<= 1; // create segments and segments[0] - // 创建 Segment 数组,设置 segments[0] + // Create Segment array, set segments[0] Segment s0 = new Segment(loadFactor, (int)(cap * loadFactor), (HashEntry[])new HashEntry[cap]); Segment[] ss = (Segment[])new Segment[ssize]; @@ -95,511 +95,9 @@ public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLe } ``` -总结一下在 Java 7 中 ConcurrentHashMap 的初始化逻辑。 +To summarize the initialization logic of `ConcurrentHashMap` in Java 7: -1. 必要参数校验。 -2. 校验并发级别 `concurrencyLevel` 大小,如果大于最大值,重置为最大值。无参构造**默认值是 16.** -3. 寻找并发级别 `concurrencyLevel` 之上最近的 **2 的幂次方**值,作为初始化容量大小,**默认是 16**。 -4. 记录 `segmentShift` 偏移量,这个值为【容量 = 2 的 N 次方】中的 N,在后面 Put 时计算位置时会用到。**默认是 32 - sshift = 28**. -5. 记录 `segmentMask`,默认是 ssize - 1 = 16 -1 = 15. -6. **初始化 `segments[0]`**,**默认大小为 2**,**负载因子 0.75**,**扩容阀值是 2\*0.75=1.5**,插入第二个值时才会进行扩容。 - -### 3. put - -接着上面的初始化参数继续查看 put 方法源码。 - -```java -/** - * Maps the specified key to the specified value in this table. - * Neither the key nor the value can be null. - * - *

The value can be retrieved by calling the get method - * with a key that is equal to the original key. - * - * @param key key with which the specified value is to be associated - * @param value value to be associated with the specified key - * @return the previous value associated with key, or - * null if there was no mapping for key - * @throws NullPointerException if the specified key or value is null - */ -public V put(K key, V value) { - Segment s; - if (value == null) - throw new NullPointerException(); - int hash = hash(key); - // hash 值无符号右移 28位(初始化时获得),然后与 segmentMask=15 做与运算 - // 其实也就是把高4位与segmentMask(1111)做与运算 - int j = (hash >>> segmentShift) & segmentMask; - if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck - (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment - // 如果查找到的 Segment 为空,初始化 - s = ensureSegment(j); - return s.put(key, hash, value, false); -} - -/** - * Returns the segment for the given index, creating it and - * recording in segment table (via CAS) if not already present. - * - * @param k the index - * @return the segment - */ -@SuppressWarnings("unchecked") -private Segment ensureSegment(int k) { - final Segment[] ss = this.segments; - long u = (k << SSHIFT) + SBASE; // raw offset - Segment seg; - // 判断 u 位置的 Segment 是否为null - if ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u)) == null) { - Segment proto = ss[0]; // use segment 0 as prototype - // 获取0号 segment 里的 HashEntry 初始化长度 - int cap = proto.table.length; - // 获取0号 segment 里的 hash 表里的扩容负载因子,所有的 segment 的 loadFactor 是相同的 - float lf = proto.loadFactor; - // 计算扩容阀值 - int threshold = (int)(cap * lf); - // 创建一个 cap 容量的 HashEntry 数组 - HashEntry[] tab = (HashEntry[])new HashEntry[cap]; - if ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck - // 再次检查 u 位置的 Segment 是否为null,因为这时可能有其他线程进行了操作 - Segment s = new Segment(lf, threshold, tab); - // 自旋检查 u 位置的 Segment 是否为null - while ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u)) - == null) { - // 使用CAS 赋值,只会成功一次 - if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s)) - break; - } - } - } - return seg; -} -``` - -上面的源码分析了 `ConcurrentHashMap` 在 put 一个数据时的处理流程,下面梳理下具体流程。 - -1. 计算要 put 的 key 的位置,获取指定位置的 `Segment`。 - -2. 如果指定位置的 `Segment` 为空,则初始化这个 `Segment`. - - **初始化 Segment 流程:** - - 1. 检查计算得到的位置的 `Segment` 是否为 null. - 2. 为 null 继续初始化,使用 `Segment[0]` 的容量和负载因子创建一个 `HashEntry` 数组。 - 3. 再次检查计算得到的指定位置的 `Segment` 是否为 null. - 4. 使用创建的 `HashEntry` 数组初始化这个 Segment. - 5. 自旋判断计算得到的指定位置的 `Segment` 是否为 null,使用 CAS 在这个位置赋值为 `Segment`. - -3. `Segment.put` 插入 key,value 值。 - -上面探究了获取 `Segment` 段和初始化 `Segment` 段的操作。最后一行的 `Segment` 的 put 方法还没有查看,继续分析。 - -```java -final V put(K key, int hash, V value, boolean onlyIfAbsent) { - // 获取 ReentrantLock 独占锁,获取不到,scanAndLockForPut 获取。 - HashEntry node = tryLock() ? null : scanAndLockForPut(key, hash, value); - V oldValue; - try { - HashEntry[] tab = table; - // 计算要put的数据位置 - int index = (tab.length - 1) & hash; - // CAS 获取 index 坐标的值 - HashEntry first = entryAt(tab, index); - for (HashEntry e = first;;) { - if (e != null) { - // 检查是否 key 已经存在,如果存在,则遍历链表寻找位置,找到后替换 value - K k; - if ((k = e.key) == key || - (e.hash == hash && key.equals(k))) { - oldValue = e.value; - if (!onlyIfAbsent) { - e.value = value; - ++modCount; - } - break; - } - e = e.next; - } - else { - // first 有值没说明 index 位置已经有值了,有冲突,链表头插法。 - if (node != null) - node.setNext(first); - else - node = new HashEntry(hash, key, value, first); - int c = count + 1; - // 容量大于扩容阀值,小于最大容量,进行扩容 - if (c > threshold && tab.length < MAXIMUM_CAPACITY) - rehash(node); - else - // index 位置赋值 node,node 可能是一个元素,也可能是一个链表的表头 - setEntryAt(tab, index, node); - ++modCount; - count = c; - oldValue = null; - break; - } - } - } finally { - unlock(); - } - return oldValue; -} -``` - -由于 `Segment` 继承了 `ReentrantLock`,所以 `Segment` 内部可以很方便的获取锁,put 流程就用到了这个功能。 - -1. `tryLock()` 获取锁,获取不到使用 **`scanAndLockForPut`** 方法继续获取。 - -2. 计算 put 的数据要放入的 index 位置,然后获取这个位置上的 `HashEntry` 。 - -3. 遍历 put 新元素,为什么要遍历?因为这里获取的 `HashEntry` 可能是一个空元素,也可能是链表已存在,所以要区别对待。 - - 如果这个位置上的 **`HashEntry` 不存在**: - - 1. 如果当前容量大于扩容阀值,小于最大容量,**进行扩容**。 - 2. 直接头插法插入。 - - 如果这个位置上的 **`HashEntry` 存在**: - - 1. 判断链表当前元素 key 和 hash 值是否和要 put 的 key 和 hash 值一致。一致则替换值 - 2. 不一致,获取链表下一个节点,直到发现相同进行值替换,或者链表表里完毕没有相同的。 - 1. 如果当前容量大于扩容阀值,小于最大容量,**进行扩容**。 - 2. 直接链表头插法插入。 - -4. 如果要插入的位置之前已经存在,替换后返回旧值,否则返回 null. - -这里面的第一步中的 `scanAndLockForPut` 操作这里没有介绍,这个方法做的操作就是不断的自旋 `tryLock()` 获取锁。当自旋次数大于指定次数时,使用 `lock()` 阻塞获取锁。在自旋时顺表获取下 hash 位置的 `HashEntry`。 - -```java -private HashEntry scanAndLockForPut(K key, int hash, V value) { - HashEntry first = entryForHash(this, hash); - HashEntry e = first; - HashEntry node = null; - int retries = -1; // negative while locating node - // 自旋获取锁 - while (!tryLock()) { - HashEntry f; // to recheck first below - if (retries < 0) { - if (e == null) { - if (node == null) // speculatively create node - node = new HashEntry(hash, key, value, null); - retries = 0; - } - else if (key.equals(e.key)) - retries = 0; - else - e = e.next; - } - else if (++retries > MAX_SCAN_RETRIES) { - // 自旋达到指定次数后,阻塞等到只到获取到锁 - lock(); - break; - } - else if ((retries & 1) == 0 && - (f = entryForHash(this, hash)) != first) { - e = first = f; // re-traverse if entry changed - retries = -1; - } - } - return node; -} - -``` - -### 4. 扩容 rehash - -`ConcurrentHashMap` 的扩容只会扩容到原来的两倍。老数组里的数据移动到新的数组时,位置要么不变,要么变为 `index+ oldSize`,参数里的 node 会在扩容之后使用链表**头插法**插入到指定位置。 - -```java -private void rehash(HashEntry node) { - HashEntry[] oldTable = table; - // 老容量 - int oldCapacity = oldTable.length; - // 新容量,扩大两倍 - int newCapacity = oldCapacity << 1; - // 新的扩容阀值 - threshold = (int)(newCapacity * loadFactor); - // 创建新的数组 - HashEntry[] newTable = (HashEntry[]) new HashEntry[newCapacity]; - // 新的掩码,默认2扩容后是4,-1是3,二进制就是11。 - int sizeMask = newCapacity - 1; - for (int i = 0; i < oldCapacity ; i++) { - // 遍历老数组 - HashEntry e = oldTable[i]; - if (e != null) { - HashEntry next = e.next; - // 计算新的位置,新的位置只可能是不变或者是老的位置+老的容量。 - int idx = e.hash & sizeMask; - if (next == null) // Single node on list - // 如果当前位置还不是链表,只是一个元素,直接赋值 - newTable[idx] = e; - else { // Reuse consecutive sequence at same slot - // 如果是链表了 - HashEntry lastRun = e; - int lastIdx = idx; - // 新的位置只可能是不变或者是老的位置+老的容量。 - // 遍历结束后,lastRun 后面的元素位置都是相同的 - for (HashEntry last = next; last != null; last = last.next) { - int k = last.hash & sizeMask; - if (k != lastIdx) { - lastIdx = k; - lastRun = last; - } - } - // ,lastRun 后面的元素位置都是相同的,直接作为链表赋值到新位置。 - newTable[lastIdx] = lastRun; - // Clone remaining nodes - for (HashEntry p = e; p != lastRun; p = p.next) { - // 遍历剩余元素,头插法到指定 k 位置。 - V v = p.value; - int h = p.hash; - int k = h & sizeMask; - HashEntry n = newTable[k]; - newTable[k] = new HashEntry(h, p.key, v, n); - } - } - } - } - // 头插法插入新的节点 - int nodeIndex = node.hash & sizeMask; // add the new node - node.setNext(newTable[nodeIndex]); - newTable[nodeIndex] = node; - table = newTable; -} -``` - -有些同学可能会对最后的两个 for 循环有疑惑,这里第一个 for 是为了寻找这样一个节点,这个节点后面的所有 next 节点的新位置都是相同的。然后把这个作为一个链表赋值到新位置。第二个 for 循环是为了把剩余的元素通过头插法插入到指定位置链表。~~这样实现的原因可能是基于概率统计,有深入研究的同学可以发表下意见。~~ - -内部第二个 `for` 循环中使用了 `new HashEntry(h, p.key, v, n)` 创建了一个新的 `HashEntry`,而不是复用之前的,是因为如果复用之前的,那么会导致正在遍历(如正在执行 `get` 方法)的线程由于指针的修改无法遍历下去。正如注释中所说的: - -> 当它们不再被可能正在并发遍历表的任何读取线程引用时,被替换的节点将被垃圾回收。 -> -> The nodes they replace will be garbage collectable as soon as they are no longer referenced by any reader thread that may be in the midst of concurrently traversing table - -为什么需要再使用一个 `for` 循环找到 `lastRun` ,其实是为了减少对象创建的次数,正如注解中所说的: - -> 从统计上看,在默认的阈值下,当表容量加倍时,只有大约六分之一的节点需要被克隆。 -> -> Statistically, at the default threshold, only about one-sixth of them need cloning when a table doubles. - -### 5. get - -到这里就很简单了,get 方法只需要两步即可。 - -1. 计算得到 key 的存放位置。 -2. 遍历指定位置查找相同 key 的 value 值。 - -```java -public V get(Object key) { - Segment s; // manually integrate access methods to reduce overhead - HashEntry[] tab; - int h = hash(key); - long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; - // 计算得到 key 的存放位置 - if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && - (tab = s.table) != null) { - for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile - (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); - e != null; e = e.next) { - // 如果是链表,遍历查找到相同 key 的 value。 - K k; - if ((k = e.key) == key || (e.hash == h && key.equals(k))) - return e.value; - } - } - return null; -} -``` - -## 2. ConcurrentHashMap 1.8 - -### 1. 存储结构 - -![Java8 ConcurrentHashMap 存储结构(图片来自 javadoop)](https://oss.javaguide.cn/github/javaguide/java/collection/java8_concurrenthashmap.png) - -可以发现 Java8 的 ConcurrentHashMap 相对于 Java7 来说变化比较大,不再是之前的 **Segment 数组 + HashEntry 数组 + 链表**,而是 **Node 数组 + 链表 / 红黑树**。当冲突链表达到一定长度时,链表会转换成红黑树。 - -### 2. 初始化 initTable - -```java -/** - * Initializes table, using the size recorded in sizeCtl. - */ -private final Node[] initTable() { - Node[] tab; int sc; - while ((tab = table) == null || tab.length == 0) { - // 如果 sizeCtl < 0 ,说明另外的线程执行CAS 成功,正在进行初始化。 - if ((sc = sizeCtl) < 0) - // 让出 CPU 使用权 - Thread.yield(); // lost initialization race; just spin - else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { - try { - if ((tab = table) == null || tab.length == 0) { - int n = (sc > 0) ? sc : DEFAULT_CAPACITY; - @SuppressWarnings("unchecked") - Node[] nt = (Node[])new Node[n]; - table = tab = nt; - sc = n - (n >>> 2); - } - } finally { - sizeCtl = sc; - } - break; - } - } - return tab; -} -``` - -从源码中可以发现 `ConcurrentHashMap` 的初始化是通过**自旋和 CAS** 操作完成的。里面需要注意的是变量 `sizeCtl` (sizeControl 的缩写),它的值决定着当前的初始化状态。 - -1. -1 说明正在初始化,其他线程需要自旋等待 -2. -N 说明 table 正在进行扩容,高 16 位表示扩容的标识戳,低 16 位减 1 为正在进行扩容的线程数 -3. 0 表示 table 初始化大小,如果 table 没有初始化 -4. \>0 表示 table 扩容的阈值,如果 table 已经初始化。 - -### 3. put - -直接过一遍 put 源码。 - -```java -public V put(K key, V value) { - return putVal(key, value, false); -} - -/** Implementation for put and putIfAbsent */ -final V putVal(K key, V value, boolean onlyIfAbsent) { - // key 和 value 不能为空 - if (key == null || value == null) throw new NullPointerException(); - int hash = spread(key.hashCode()); - int binCount = 0; - for (Node[] tab = table;;) { - // f = 目标位置元素 - Node f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值 - if (tab == null || (n = tab.length) == 0) - // 数组桶为空,初始化数组桶(自旋+CAS) - tab = initTable(); - else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { - // 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出 - if (casTabAt(tab, i, null,new Node(hash, key, value, null))) - break; // no lock when adding to empty bin - } - else if ((fh = f.hash) == MOVED) - tab = helpTransfer(tab, f); - else { - V oldVal = null; - // 使用 synchronized 加锁加入节点 - synchronized (f) { - if (tabAt(tab, i) == f) { - // 说明是链表 - if (fh >= 0) { - binCount = 1; - // 循环加入新的或者覆盖节点 - for (Node e = f;; ++binCount) { - K ek; - if (e.hash == hash && - ((ek = e.key) == key || - (ek != null && key.equals(ek)))) { - oldVal = e.val; - if (!onlyIfAbsent) - e.val = value; - break; - } - Node pred = e; - if ((e = e.next) == null) { - pred.next = new Node(hash, key, - value, null); - break; - } - } - } - else if (f instanceof TreeBin) { - // 红黑树 - Node p; - binCount = 2; - if ((p = ((TreeBin)f).putTreeVal(hash, key, - value)) != null) { - oldVal = p.val; - if (!onlyIfAbsent) - p.val = value; - } - } - } - } - if (binCount != 0) { - if (binCount >= TREEIFY_THRESHOLD) - treeifyBin(tab, i); - if (oldVal != null) - return oldVal; - break; - } - } - } - addCount(1L, binCount); - return null; -} -``` - -1. 根据 key 计算出 hashcode 。 - -2. 判断是否需要进行初始化。 - -3. 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。 - -4. 如果当前位置的 `hashcode == MOVED == -1`,则需要进行扩容。 - -5. 如果都不满足,则利用 synchronized 锁写入数据。 - -6. 如果数量大于 `TREEIFY_THRESHOLD` 则要执行树化方法,在 `treeifyBin` 中会首先判断当前数组长度 ≥64 时才会将链表转换为红黑树。 - -### 4. get - -get 流程比较简单,直接过一遍源码。 - -```java -public V get(Object key) { - Node[] tab; Node e, p; int n, eh; K ek; - // key 所在的 hash 位置 - int h = spread(key.hashCode()); - if ((tab = table) != null && (n = tab.length) > 0 && - (e = tabAt(tab, (n - 1) & h)) != null) { - // 如果指定位置元素存在,头结点hash值相同 - if ((eh = e.hash) == h) { - if ((ek = e.key) == key || (ek != null && key.equals(ek))) - // key hash 值相等,key值相同,直接返回元素 value - return e.val; - } - else if (eh < 0) - // 头结点hash值小于0,说明正在扩容或者是红黑树,find查找 - return (p = e.find(h, key)) != null ? p.val : null; - while ((e = e.next) != null) { - // 是链表,遍历查找 - if (e.hash == h && - ((ek = e.key) == key || (ek != null && key.equals(ek)))) - return e.val; - } - } - return null; -} -``` - -总结一下 get 过程: - -1. 根据 hash 值计算位置。 -2. 查找到指定位置,如果头节点就是要找的,直接返回它的 value. -3. 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。 -4. 如果是链表,遍历查找之。 - -总结: - -总的来说 `ConcurrentHashMap` 在 Java8 中相对于 Java7 来说变化还是挺大的, - -## 3. 总结 - -Java7 中 `ConcurrentHashMap` 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 `Segment` 都是一个类似 `HashMap` 数组的结构,它可以扩容,它的冲突会转化为链表。但是 `Segment` 的个数一但初始化就不能改变。 - -Java8 中的 `ConcurrentHashMap` 使用的 `Synchronized` 锁加 CAS 的机制。结构也由 Java7 中的 **`Segment` 数组 + `HashEntry` 数组 + 链表** 进化成了 **Node 数组 + 链表 / 红黑树**,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。 - -有些同学可能对 `Synchronized` 的性能存在疑问,其实 `Synchronized` 锁自从引入锁升级策略后,性能不再是问题,有兴趣的同学可以自己了解下 `Synchronized` 的**锁升级**。 - - +1. Necessary parameter validation. +1. Validate the size of the concurrency level `concurrencyLevel`, resetting to the maximum value if it exceeds the maximum. The default value for the no-argument constructor is **16**. +1. Find the nearest **power of 2** above the concurrency level `concurrencyLevel` as the initial capacity, which is **16** by default. + 4 diff --git a/docs/java/collection/copyonwritearraylist-source-code.md b/docs/java/collection/copyonwritearraylist-source-code.md index 9aceb83bc4e..e2b1c2ed2e6 100644 --- a/docs/java/collection/copyonwritearraylist-source-code.md +++ b/docs/java/collection/copyonwritearraylist-source-code.md @@ -1,48 +1,48 @@ --- -title: CopyOnWriteArrayList 源码分析 +title: CopyOnWriteArrayList Source Code Analysis category: Java tag: - - Java集合 + - Java Collections --- -## CopyOnWriteArrayList 简介 +## Introduction to CopyOnWriteArrayList -在 JDK1.5 之前,如果想要使用并发安全的 `List` 只能选择 `Vector`。而 `Vector` 是一种老旧的集合,已经被淘汰。`Vector` 对于增删改查等方法基本都加了 `synchronized`,这种方式虽然能够保证同步,但这相当于对整个 `Vector` 加上了一把大锁,使得每个方法执行的时候都要去获得锁,导致性能非常低下。 +Before JDK 1.5, if you wanted to use a concurrent-safe `List`, the only option was `Vector`. However, `Vector` is an outdated collection that has been deprecated. `Vector` adds `synchronized` to almost all methods such as add, remove, update, and query. While this approach ensures synchronization, it effectively applies a large lock to the entire `Vector`, causing every method execution to acquire the lock, resulting in very low performance. -JDK1.5 引入了 `Java.util.concurrent`(JUC)包,其中提供了很多线程安全且并发性能良好的容器,其中唯一的线程安全 `List` 实现就是 `CopyOnWriteArrayList` 。关于`java.util.concurrent` 包下常见并发容器的总结,可以看我写的这篇文章:[Java 常见并发容器总结](https://javaguide.cn/java/concurrent/java-concurrent-collections.html) 。 +JDK 1.5 introduced the `Java.util.concurrent` (JUC) package, which provides many thread-safe containers with good concurrent performance. The only thread-safe `List` implementation among them is `CopyOnWriteArrayList`. For a summary of common concurrent containers in the `java.util.concurrent` package, you can refer to my article: [Summary of Common Java Concurrent Containers](https://javaguide.cn/java/concurrent/java-concurrent-collections.html). -### CopyOnWriteArrayList 到底有什么厉害之处? +### What Makes CopyOnWriteArrayList So Powerful? -对于大部分业务场景来说,读取操作往往是远大于写入操作的。由于读取操作不会对原有数据进行修改,因此,对于每次读取都进行加锁其实是一种资源浪费。相比之下,我们应该允许多个线程同时访问 `List` 的内部数据,毕竟对于读取操作来说是安全的。 +In most business scenarios, read operations are often far greater than write operations. Since read operations do not modify the original data, locking for every read is actually a waste of resources. In contrast, we should allow multiple threads to access the internal data of the `List` simultaneously, as it is safe for read operations. -这种思路与 `ReentrantReadWriteLock` 读写锁的设计思想非常类似,即读读不互斥、读写互斥、写写互斥(只有读读不互斥)。`CopyOnWriteArrayList` 更进一步地实现了这一思想。为了将读操作性能发挥到极致,`CopyOnWriteArrayList` 中的读取操作是完全无需加锁的。更加厉害的是,写入操作也不会阻塞读取操作,只有写写才会互斥。这样一来,读操作的性能就可以大幅度提升。 +This idea is very similar to the design philosophy of `ReentrantReadWriteLock`, which allows for read-read non-exclusivity, read-write exclusivity, and write-write exclusivity (only read-read is non-exclusive). `CopyOnWriteArrayList` takes this idea a step further. To maximize the performance of read operations, `CopyOnWriteArrayList` does not require locking for reading. Even more impressively, write operations do not block read operations; only write-write operations are mutually exclusive. This significantly enhances the performance of read operations. -`CopyOnWriteArrayList` 线程安全的核心在于其采用了 **写时复制(Copy-On-Write)** 的策略,从 `CopyOnWriteArrayList` 的名字就能看出了。 +The thread safety of `CopyOnWriteArrayList` is fundamentally based on its use of the **Copy-On-Write** strategy, as indicated by its name. -### Copy-On-Write 的思想是什么? +### What is the Copy-On-Write Concept? -`CopyOnWriteArrayList`名字中的“Copy-On-Write”即写时复制,简称 COW。 +The "Copy-On-Write" in `CopyOnWriteArrayList` refers to the strategy of writing by copying, abbreviated as COW. -下面是维基百科对 Copy-On-Write 的介绍,介绍的挺不错: +Here is a good introduction to Copy-On-Write from Wikipedia: -> 写入时复制(英语:Copy-on-write,简称 COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。 +> Copy-on-write (COW) is an optimization strategy in computer programming. The core idea is that if multiple callers request the same resource (such as memory or data storage on disk) simultaneously, they will share the same pointer to the same resource until one caller attempts to modify the content of the resource. At that point, the system will create a private copy for that caller, while other callers continue to see the original resource unchanged. This process is transparent to other callers. The main advantage of this approach is that if a caller does not modify the resource, no private copy is created, allowing multiple callers to share the same resource during read operations. -这里再以 `CopyOnWriteArrayList`为例介绍:当需要修改( `add`,`set`、`remove` 等操作) `CopyOnWriteArrayList` 的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。 +Using `CopyOnWriteArrayList` as an example: when modifying the contents of `CopyOnWriteArrayList` (such as `add`, `set`, `remove`, etc.), it does not directly modify the original array. Instead, it first creates a copy of the underlying array, modifies the copy, and then assigns the modified array back. This ensures that write operations do not affect read operations. -可以看出,写时复制机制非常适合读多写少的并发场景,能够极大地提高系统的并发性能。 +It is clear that the copy-on-write mechanism is very suitable for concurrent scenarios with many reads and few writes, greatly improving the system's concurrent performance. -不过,写时复制机制并不是银弹,其依然存在一些缺点,下面列举几点: +However, the copy-on-write mechanism is not a silver bullet; it still has some drawbacks. Here are a few: -1. 内存占用:每次写操作都需要复制一份原始数据,会占用额外的内存空间,在数据量比较大的情况下,可能会导致内存资源不足。 -2. 写操作开销:每一次写操作都需要复制一份原始数据,然后再进行修改和替换,所以写操作的开销相对较大,在写入比较频繁的场景下,性能可能会受到影响。 -3. 数据一致性问题:修改操作不会立即反映到最终结果中,还需要等待复制完成,这可能会导致一定的数据一致性问题。 -4. …… +1. Memory Usage: Each write operation requires a copy of the original data, which consumes additional memory. In cases with large data volumes, this may lead to insufficient memory resources. +1. Write Operation Overhead: Each write operation requires copying the original data, followed by modification and replacement, resulting in relatively high overhead for write operations. In scenarios with frequent writes, performance may be affected. +1. Data Consistency Issues: Modification operations do not immediately reflect in the final result and must wait for the copy to complete, which may lead to certain data consistency issues. +1. …… -## CopyOnWriteArrayList 源码分析 +## Source Code Analysis of CopyOnWriteArrayList -这里以 JDK1.8 为例,分析一下 `CopyOnWriteArrayList` 的底层核心源码。 +Here, we will analyze the core source code of `CopyOnWriteArrayList` using JDK 1.8 as an example. -`CopyOnWriteArrayList` 的类定义如下: +The class definition of `CopyOnWriteArrayList` is as follows: ```java public class CopyOnWriteArrayList @@ -53,265 +53,7 @@ implements List, RandomAccess, Cloneable, Serializable } ``` -`CopyOnWriteArrayList` 实现了以下接口: +`CopyOnWriteArrayList` implements the following interfaces: -- `List` : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。 -- `RandomAccess` :这是一个标志接口,表明实现这个接口的 `List` 集合是支持 **快速随机访问** 的。 -- `Cloneable` :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。 -- `Serializable` : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。 - -![CopyOnWriteArrayList 类图](https://oss.javaguide.cn/github/javaguide/java/collection/copyonwritearraylist-class-diagram.png) - -### 初始化 - -`CopyOnWriteArrayList` 中有一个无参构造函数和两个有参构造函数。 - -```java -// 创建一个空的 CopyOnWriteArrayList -public CopyOnWriteArrayList() { - setArray(new Object[0]); -} - -// 按照集合的迭代器返回的顺序创建一个包含指定集合元素的 CopyOnWriteArrayList -public CopyOnWriteArrayList(Collection c) { - Object[] elements; - if (c.getClass() == CopyOnWriteArrayList.class) - elements = ((CopyOnWriteArrayList)c).getArray(); - else { - elements = c.toArray(); - // c.toArray might (incorrectly) not return Object[] (see 6260652) - if (elements.getClass() != Object[].class) - elements = Arrays.copyOf(elements, elements.length, Object[].class); - } - setArray(elements); -} - -// 创建一个包含指定数组的副本的列表 -public CopyOnWriteArrayList(E[] toCopyIn) { - setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); -} -``` - -### 插入元素 - -`CopyOnWriteArrayList` 的 `add()`方法有三个版本: - -- `add(E e)`:在 `CopyOnWriteArrayList` 的尾部插入元素。 -- `add(int index, E element)`:在 `CopyOnWriteArrayList` 的指定位置插入元素。 -- `addIfAbsent(E e)`:如果指定元素不存在,那么添加该元素。如果成功添加元素则返回 true。 - -这里以`add(E e)`为例进行介绍: - -```java -// 插入元素到 CopyOnWriteArrayList 的尾部 -public boolean add(E e) { - final ReentrantLock lock = this.lock; - // 加锁 - lock.lock(); - try { - // 获取原来的数组 - Object[] elements = getArray(); - // 原来数组的长度 - int len = elements.length; - // 创建一个长度+1的新数组,并将原来数组的元素复制给新数组 - Object[] newElements = Arrays.copyOf(elements, len + 1); - // 元素放在新数组末尾 - newElements[len] = e; - // array指向新数组 - setArray(newElements); - return true; - } finally { - // 解锁 - lock.unlock(); - } -} -``` - -从上面的源码可以看出: - -- `add`方法内部用到了 `ReentrantLock` 加锁,保证了同步,避免了多线程写的时候会复制出多个副本出来。锁被修饰保证了锁的内存地址肯定不会被修改,并且,释放锁的逻辑放在 `finally` 中,可以保证锁能被释放。 -- `CopyOnWriteArrayList` 通过复制底层数组的方式实现写操作,即先创建一个新的数组来容纳新添加的元素,然后在新数组中进行写操作,最后将新数组赋值给底层数组的引用,替换掉旧的数组。这也就证明了我们前面说的:`CopyOnWriteArrayList` 线程安全的核心在于其采用了 **写时复制(Copy-On-Write)** 的策略。 -- 每次写操作都需要通过 `Arrays.copyOf` 复制底层数组,时间复杂度是 O(n) 的,且会占用额外的内存空间。因此,`CopyOnWriteArrayList` 适用于读多写少的场景,在写操作不频繁且内存资源充足的情况下,可以提升系统的性能表现。 -- `CopyOnWriteArrayList` 中并没有类似于 `ArrayList` 的 `grow()` 方法扩容的操作。 - -> `Arrays.copyOf` 方法的时间复杂度是 O(n),其中 n 表示需要复制的数组长度。因为这个方法的实现原理是先创建一个新的数组,然后将源数组中的数据复制到新数组中,最后返回新数组。这个方法会复制整个数组,因此其时间复杂度与数组长度成正比,即 O(n)。值得注意的是,由于底层调用了系统级别的拷贝指令,因此在实际应用中这个方法的性能表现比较优秀,但是也需要注意控制复制的数据量,避免出现内存占用过高的情况。 - -### 读取元素 - -`CopyOnWriteArrayList` 的读取操作是基于内部数组 `array` 并没有发生实际的修改,因此在读取操作时不需要进行同步控制和锁操作,可以保证数据的安全性。这种机制下,多个线程可以同时读取列表中的元素。 - -```java -// 底层数组,只能通过getArray和setArray方法访问 -private transient volatile Object[] array; - -public E get(int index) { - return get(getArray(), index); -} - -final Object[] getArray() { - return array; -} - -private E get(Object[] a, int index) { - return (E) a[index]; -} -``` - -不过,`get`方法是弱一致性的,在某些情况下可能读到旧的元素值。 - -`get(int index)`方法是分两步进行的: - -1. 通过`getArray()`获取当前数组的引用; -2. 直接从数组中获取下标为 index 的元素。 - -这个过程并没有加锁,所以在并发环境下可能出现如下情况: - -1. 线程 1 调用`get(int index)`方法获取值,内部通过`getArray()`方法获取到了 array 属性值; -2. 线程 2 调用`CopyOnWriteArrayList`的`add`、`set`、`remove` 等修改方法时,内部通过`setArray`方法修改了`array`属性的值; -3. 线程 1 还是从旧的 `array` 数组中取值。 - -### 获取列表中元素的个数 - -```java -public int size() { - return getArray().length; -} -``` - -`CopyOnWriteArrayList`中的`array`数组每次复制都刚好能够容纳下所有元素,并不像`ArrayList`那样会预留一定的空间。因此,`CopyOnWriteArrayList`中并没有`size`属性`CopyOnWriteArrayList`的底层数组的长度就是元素个数,因此`size()`方法只要返回数组长度就可以了。 - -### 删除元素 - -`CopyOnWriteArrayList`删除元素相关的方法一共有 4 个: - -1. `remove(int index)`:移除此列表中指定位置上的元素。将任何后续元素向左移动(从它们的索引中减去 1)。 -2. `boolean remove(Object o)`:删除此列表中首次出现的指定元素,如果不存在该元素则返回 false。 -3. `boolean removeAll(Collection c)`:从此列表中删除指定集合中包含的所有元素。 -4. `void clear()`:移除此列表中的所有元素。 - -这里以`remove(int index)`为例进行介绍: - -```java -public E remove(int index) { - // 获取可重入锁 - final ReentrantLock lock = this.lock; - // 加锁 - lock.lock(); - try { - //获取当前array数组 - Object[] elements = getArray(); - // 获取当前array长度 - int len = elements.length; - //获取指定索引的元素(旧值) - E oldValue = get(elements, index); - int numMoved = len - index - 1; - // 判断删除的是否是最后一个元素 - if (numMoved == 0) - // 如果删除的是最后一个元素,直接复制该元素前的所有元素到新的数组 - setArray(Arrays.copyOf(elements, len - 1)); - else { - // 分段复制,将index前的元素和index+1后的元素复制到新数组 - // 新数组长度为旧数组长度-1 - Object[] newElements = new Object[len - 1]; - System.arraycopy(elements, 0, newElements, 0, index); - System.arraycopy(elements, index + 1, newElements, index, - numMoved); - //将新数组赋值给array引用 - setArray(newElements); - } - return oldValue; - } finally { - // 解锁 - lock.unlock(); - } -} -``` - -### 判断元素是否存在 - -`CopyOnWriteArrayList`提供了两个用于判断指定元素是否在列表中的方法: - -- `contains(Object o)`:判断是否包含指定元素。 -- `containsAll(Collection c)`:判断是否保证指定集合的全部元素。 - -```java -// 判断是否包含指定元素 -public boolean contains(Object o) { - //获取当前array数组 - Object[] elements = getArray(); - //调用index尝试查找指定元素,如果返回值大于等于0,则返回true,否则返回false - return indexOf(o, elements, 0, elements.length) >= 0; -} - -// 判断是否保证指定集合的全部元素 -public boolean containsAll(Collection c) { - //获取当前array数组 - Object[] elements = getArray(); - //获取数组长度 - int len = elements.length; - //遍历指定集合 - for (Object e : c) { - //循环调用indexOf方法判断,只要有一个没有包含就直接返回false - if (indexOf(e, elements, 0, len) < 0) - return false; - } - //最后表示全部包含或者制定集合为空集合,那么返回true - return true; -} -``` - -## CopyOnWriteArrayList 常用方法测试 - -代码: - -```java -// 创建一个 CopyOnWriteArrayList 对象 -CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); - -// 向列表中添加元素 -list.add("Java"); -list.add("Python"); -list.add("C++"); -System.out.println("初始列表:" + list); - -// 使用 get 方法获取指定位置的元素 -System.out.println("列表第二个元素为:" + list.get(1)); - -// 使用 remove 方法删除指定元素 -boolean result = list.remove("C++"); -System.out.println("删除结果:" + result); -System.out.println("列表删除元素后为:" + list); - -// 使用 set 方法更新指定位置的元素 -list.set(1, "Golang"); -System.out.println("列表更新后为:" + list); - -// 使用 add 方法在指定位置插入元素 -list.add(0, "PHP"); -System.out.println("列表插入元素后为:" + list); - -// 使用 size 方法获取列表大小 -System.out.println("列表大小为:" + list.size()); - -// 使用 removeAll 方法删除指定集合中所有出现的元素 -result = list.removeAll(List.of("Java", "Golang")); -System.out.println("批量删除结果:" + result); -System.out.println("列表批量删除元素后为:" + list); - -// 使用 clear 方法清空列表中所有元素 -list.clear(); -System.out.println("列表清空后为:" + list); -``` - -输出: - -```plain -列表更新后为:[Java, Golang] -列表插入元素后为:[PHP, Java, Golang] -列表大小为:3 -批量删除结果:true -列表批量删除元素后为:[PHP] -列表清空后为:[] -``` - - +- `List`: Indicates that it is a list that supports operations such as adding, removing, and searching, and can be accessed by index. +- `RandomAccess`: This is a marker interface indicating that the `List` diff --git a/docs/java/collection/delayqueue-source-code.md b/docs/java/collection/delayqueue-source-code.md index 5fb6f4affad..b268dc21e2a 100644 --- a/docs/java/collection/delayqueue-source-code.md +++ b/docs/java/collection/delayqueue-source-code.md @@ -1,17 +1,17 @@ --- -title: DelayQueue 源码分析 +title: DelayQueue Source Code Analysis category: Java tag: - - Java集合 + - Java Collections --- -## DelayQueue 简介 +## Introduction to DelayQueue -`DelayQueue` 是 JUC 包(`java.util.concurrent)`为我们提供的延迟队列,用于实现延时任务比如订单下单 15 分钟未支付直接取消。它是 `BlockingQueue` 的一种,底层是一个基于 `PriorityQueue` 实现的一个无界队列,是线程安全的。关于`PriorityQueue`可以参考笔者编写的这篇文章:[PriorityQueue 源码分析](./priorityqueue-source-code.md) 。 +`DelayQueue` is a delay queue provided by the JUC package (`java.util.concurrent`) for implementing delayed tasks, such as automatically canceling an order if it has not been paid within 15 minutes. It is a type of `BlockingQueue`, and its underlying implementation is an unbounded queue based on `PriorityQueue`, which is thread-safe. For more information on `PriorityQueue`, you can refer to the article I wrote: [PriorityQueue Source Code Analysis](./priorityqueue-source-code.md). -![BlockingQueue 的实现类](https://oss.javaguide.cn/github/javaguide/java/collection/blocking-queue-hierarchy.png) +![Implementation Classes of BlockingQueue](https://oss.javaguide.cn/github/javaguide/java/collection/blocking-queue-hierarchy.png) -`DelayQueue` 中存放的元素必须实现 `Delayed` 接口,并且需要重写 `getDelay()`方法(计算是否到期)。 +The elements stored in `DelayQueue` must implement the `Delayed` interface and override the `getDelay()` method (to calculate whether they have expired). ```java public interface Delayed extends Comparable { @@ -19,37 +19,37 @@ public interface Delayed extends Comparable { } ``` -默认情况下, `DelayQueue` 会按照到期时间升序编排任务。只有当元素过期时(`getDelay()`方法返回值小于等于 0),才能从队列中取出。 +By default, `DelayQueue` arranges tasks in ascending order of their expiration time. An element can only be removed from the queue when it has expired (i.e., when the return value of `getDelay()` is less than or equal to 0). -## DelayQueue 发展史 +## History of DelayQueue -- `DelayQueue` 最早是在 Java 5 中引入的,作为 `java.util.concurrent` 包中的一部分,用于支持基于时间的任务调度和缓存过期删除等场景,该版本仅仅支持延迟功能的实现,还未解决线程安全问题。 -- 在 Java 6 中,`DelayQueue` 的实现进行了优化,通过使用 `ReentrantLock` 和 `Condition` 解决线程安全及线程间交互的效率,提高了其性能和可靠性。 -- 在 Java 7 中,`DelayQueue` 的实现进行了进一步的优化,通过使用 CAS 操作实现元素的添加和移除操作,提高了其并发操作性能。 -- 在 Java 8 中,`DelayQueue` 的实现没有进行重大变化,但是在 `java.time` 包中引入了新的时间类,如 `Duration` 和 `Instant`,使得使用 `DelayQueue` 进行基于时间的调度更加方便和灵活。 -- 在 Java 9 中,`DelayQueue` 的实现进行了一些微小的改进,主要是对代码进行了一些优化和精简。 +- `DelayQueue` was first introduced in Java 5 as part of the `java.util.concurrent` package to support time-based task scheduling and cache expiration scenarios. This version only supported the implementation of delay functionality and did not address thread safety issues. +- In Java 6, the implementation of `DelayQueue` was optimized by using `ReentrantLock` and `Condition` to solve thread safety and inter-thread interaction efficiency, improving its performance and reliability. +- In Java 7, the implementation of `DelayQueue` was further optimized by using CAS operations for adding and removing elements, enhancing its concurrent operation performance. +- In Java 8, there were no major changes to the implementation of `DelayQueue`, but new time classes such as `Duration` and `Instant` were introduced in the `java.time` package, making it more convenient and flexible to use `DelayQueue` for time-based scheduling. +- In Java 9, there were some minor improvements to the implementation of `DelayQueue`, mainly optimizing and streamlining the code. -总的来说,`DelayQueue` 的发展史主要是通过优化其实现方式和提高其性能和可靠性,使其更加适用于基于时间的调度和缓存过期删除等场景。 +Overall, the development history of `DelayQueue` mainly involves optimizing its implementation and improving its performance and reliability, making it more suitable for time-based scheduling and cache expiration scenarios. -## DelayQueue 常见使用场景示例 +## Common Usage Scenarios of DelayQueue -我们这里希望任务可以按照我们预期的时间执行,例如提交 3 个任务,分别要求 1s、2s、3s 后执行,即使是乱序添加,1s 后要求 1s 执行的任务会准时执行。 +We hope that tasks can be executed at our expected times. For example, if we submit three tasks that require execution after 1s, 2s, and 3s respectively, even if they are added in a random order, the task scheduled for 1s will execute on time. -![延迟任务](https://oss.javaguide.cn/github/javaguide/java/collection/delayed-task.png) +![Delayed Tasks](https://oss.javaguide.cn/github/javaguide/java/collection/delayed-task.png) -对此我们可以使用 `DelayQueue` 来实现,所以我们首先需要继承 `Delayed` 实现 `DelayedTask`,实现 `getDelay` 方法以及优先级比较 `compareTo`。 +To achieve this, we can use `DelayQueue`. First, we need to implement `Delayed` by creating a `DelayedTask` class, implementing the `getDelay` method and the priority comparison `compareTo`. ```java /** - * 延迟任务 + * Delayed Task */ public class DelayedTask implements Delayed { /** - * 任务到期时间 + * Task expiration time */ private long executeTime; /** - * 任务 + * Task */ private Runnable task; @@ -59,7 +59,7 @@ public class DelayedTask implements Delayed { } /** - * 查看当前任务还有多久到期 + * Check how long until the current task expires * @param unit * @return */ @@ -69,7 +69,7 @@ public class DelayedTask implements Delayed { } /** - * 延迟队列需要到期时间升序入队,所以我们需要实现compareTo进行到期时间比较 + * The delay queue requires tasks to be enqueued in ascending order of expiration time, so we need to implement compareTo for expiration time comparison * @param o * @return */ @@ -84,276 +84,15 @@ public class DelayedTask implements Delayed { } ``` -完成任务的封装之后,使用就很简单了,设置好多久到期然后将任务提交到延迟队列中即可。 +After encapsulating the task, using it is straightforward: set how long until expiration and submit the task to the delay queue. ```java -// 创建延迟队列,并添加任务 -DelayQueue < DelayedTask > delayQueue = new DelayQueue < > (); +// Create a delay queue and add tasks +DelayQueue delayQueue = new DelayQueue<>(); -//分别添加1s、2s、3s到期的任务 +// Add tasks with expiration times of 1s, 2s, and 3s respectively delayQueue.add(new DelayedTask(2000, () -> System.out.println("Task 2"))); delayQueue.add(new DelayedTask(1000, () -> System.out.println("Task 1"))); delayQueue.add(new DelayedTask(3000, () -> System.out.println("Task 3"))); -// 取出任务并执行 -while (!delayQueue.isEmpty()) { - //阻塞获取最先到期的任务 - DelayedTask task = delayQueue.take(); - if (task != null) { - task.execute(); - } -} -``` - -从输出结果可以看出,即使笔者先提到 2s 到期的任务,1s 到期的任务 Task1 还是优先执行的。 - -```java -Task 1 -Task 2 -Task 3 -``` - -## DelayQueue 源码解析 - -这里以 JDK1.8 为例,分析一下 `DelayQueue` 的底层核心源码。 - -`DelayQueue` 的类定义如下: - -```java -public class DelayQueue extends AbstractQueue implements BlockingQueue -{ - //... -} -``` - -`DelayQueue` 继承了 `AbstractQueue` 类,实现了 `BlockingQueue` 接口。 - -![DelayQueue类图](https://oss.javaguide.cn/github/javaguide/java/collection/delayqueue-class-diagram.png) - -### 核心成员变量 - -`DelayQueue` 的 4 个核心成员变量如下: - -```java -//可重入锁,实现线程安全的关键 -private final transient ReentrantLock lock = new ReentrantLock(); -//延迟队列底层存储数据的集合,确保元素按照到期时间升序排列 -private final PriorityQueue q = new PriorityQueue(); - -//指向准备执行优先级最高的线程 -private Thread leader = null; -//实现多线程之间等待唤醒的交互 -private final Condition available = lock.newCondition(); -``` - -- `lock` : 我们都知道 `DelayQueue` 存取是线程安全的,所以为了保证存取元素时线程安全,我们就需要在存取时上锁,而 `DelayQueue` 就是基于 `ReentrantLock` 独占锁确保存取操作的线程安全。 -- `q` : 延迟队列要求元素按照到期时间进行升序排列,所以元素添加时势必需要进行优先级排序,所以 `DelayQueue` 底层元素的存取都是通过这个优先队列 `PriorityQueue` 的成员变量 `q` 来管理的。 -- `leader` : 延迟队列的任务只有到期之后才会执行,对于没有到期的任务只有等待,为了确保优先级最高的任务到期后可以即刻被执行,设计者就用 `leader` 来管理延迟任务,只有 `leader` 所指向的线程才具备定时等待任务到期执行的权限,而其他那些优先级低的任务只能无限期等待,直到 `leader` 线程执行完手头的延迟任务后唤醒它。 -- `available` : 上文讲述 `leader` 线程时提到的等待唤醒操作的交互就是通过 `available` 实现的,假如线程 1 尝试在空的 `DelayQueue` 获取任务时,`available` 就会将其放入等待队列中。直到有一个线程添加一个延迟任务后通过 `available` 的 `signal` 方法将其唤醒。 - -### 构造方法 - -相较于其他的并发容器,延迟队列的构造方法比较简单,它只有两个构造方法,因为所有成员变量在类加载时都已经初始完成了,所以默认构造方法什么也没做。还有一个传入 `Collection` 对象的构造方法,它会将调用 `addAll()`方法将集合元素存到优先队列 `q` 中。 - -```java -public DelayQueue() {} - -public DelayQueue(Collection c) { - this.addAll(c); -} -``` - -### 添加元素 - -`DelayQueue` 添加元素的方法无论是 `add`、`put` 还是 `offer`,本质上就是调用一下 `offer` ,所以了解延迟队列的添加逻辑我们只需阅读 offer 方法即可。 - -`offer` 方法的整体逻辑为: - -1. 尝试获取 `lock` 。 -2. 如果上锁成功,则调 `q` 的 `offer` 方法将元素存放到优先队列中。 -3. 调用 `peek` 方法看看当前队首元素是否就是本次入队的元素,如果是则说明当前这个元素是即将到期的任务(即优先级最高的元素),于是将 `leader` 设置为空,通知因为队列为空时调用 `take` 等方法导致阻塞的线程来争抢元素。 -4. 上述步骤执行完成,释放 `lock`。 -5. 返回 true。 - -源码如下,笔者已详细注释,读者可自行参阅: - -```java -public boolean offer(E e) { - //尝试获取lock - final ReentrantLock lock = this.lock; - lock.lock(); - try { - //如果上锁成功,则调q的offer方法将元素存放到优先队列中 - q.offer(e); - //调用peek方法看看当前队首元素是否就是本次入队的元素,如果是则说明当前这个元素是即将到期的任务(即优先级最高的元素) - if (q.peek() == e) { - //将leader设置为空,通知调用取元素方法而阻塞的线程来争抢这个任务 - leader = null; - available.signal(); - } - return true; - } finally { - //上述步骤执行完成,释放lock - lock.unlock(); - } -} -``` - -### 获取元素 - -`DelayQueue` 中获取元素的方式分为阻塞式和非阻塞式,先来看看逻辑比较复杂的阻塞式获取元素方法 `take`,为了让读者可以更直观的了解阻塞式获取元素的全流程,笔者将以 3 个线程并发获取元素为例讲述 `take` 的工作流程。 - -> 想要理解下面的内容,需要用到 AQS 相关的知识,推荐阅读下面这两篇文章: -> -> - [图文讲解 AQS ,一起看看 AQS 的源码……(图文较长)](https://xie.infoq.cn/article/5a3cc0b709012d40cb9f41986) -> - [AQS 都看完了,Condition 原理可不能少!](https://xie.infoq.cn/article/0223d5e5f19726b36b084b10d) - -1、首先, 3 个线程会尝试获取可重入锁 `lock`,假设我们现在有 3 个线程分别是 t1、t2、t3,随后 t1 得到了锁,而 t2、t3 没有抢到锁,故将这两个线程存入等待队列中。 - -![](https://oss.javaguide.cn/github/javaguide/java/collection/delayqueue-take-0.png) - -2、紧接着 t1 开始进行元素获取的逻辑。 - -3、线程 t1 首先会查看 `DelayQueue` 队列首元素是否为空。 - -4、如果元素为空,则说明当前队列没有任何元素,故 t1 就会被阻塞存到 `conditionWaiter` 这个队列中。 - -![](https://oss.javaguide.cn/github/javaguide/java/collection/delayqueue-take-1.png) - -注意,调用 `await` 之后 t1 就会释放 `lcok` 锁,假如 `DelayQueue` 持续为空,那么 t2、t3 也会像 t1 一样执行相同的逻辑并进入 `conditionWaiter` 队列中。 - -![](https://oss.javaguide.cn/github/javaguide/java/collection/delayqueue-take-2.png) - -如果元素不为空,则判断当前任务是否到期,如果元素到期,则直接返回出去。如果元素未到期,则判断当前 `leader` 线程(`DelayQueue` 中唯一一个可以等待并获取元素的线程引用)是否为空,若不为空,则说明当前 `leader` 正在等待执行一个优先级比当前元素还高的元素到期,故当前线程 t1 只能调用 `await` 进入无限期等待,等到 `leader` 取得元素后唤醒。反之,若 `leader` 线程为空,则将当前线程设置为 leader 并进入有限期等待,到期后取出元素并返回。 - -自此我们阻塞式获取元素的逻辑都已完成后,源码如下,读者可自行参阅: - -```java -public E take() throws InterruptedException { - // 尝试获取可重入锁,将底层AQS的state设置为1,并设置为独占锁 - final ReentrantLock lock = this.lock; - lock.lockInterruptibly(); - try { - for (;;) { - //查看队列第一个元素 - E first = q.peek(); - //若为空,则将当前线程放入ConditionObject的等待队列中,并将底层AQS的state设置为0,表示释放锁并进入无限期等待 - if (first == null) - available.await(); - else { - //若元素不为空,则查看当前元素多久到期 - long delay = first.getDelay(NANOSECONDS); - //如果小于0则说明已到期直接返回出去 - if (delay <= 0) - return q.poll(); - //如果大于0则说明任务还没到期,首先需要释放对这个元素的引用 - first = null; // don't retain ref while waiting - //判断leader是否为空,如果不为空,则说明正有线程作为leader并等待一个任务到期,则当前线程进入无限期等待 - if (leader != null) - available.await(); - else { - //反之将我们的线程成为leader - Thread thisThread = Thread.currentThread(); - leader = thisThread; - try { - //并进入有限期等待 - available.awaitNanos(delay); - } finally { - //等待任务到期时,释放leader引用,进入下一次循环将任务return出去 - if (leader == thisThread) - leader = null; - } - } - } - } - } finally { - // 收尾逻辑:当leader为null,并且队列中有任务时,唤醒等待的获取元素的线程。 - if (leader == null && q.peek() != null) - available.signal(); - //释放锁 - lock.unlock(); - } -} -``` - -我们再来看看非阻塞的获取元素方法 `poll` ,逻辑比较简单,整体步骤如下: - -1. 尝试获取可重入锁。 -2. 查看队列第一个元素,判断元素是否为空。 -3. 若元素为空,或者元素未到期,则直接返回空。 -4. 若元素不为空且到期了,直接调用 `poll` 返回出去。 -5. 释放可重入锁 `lock` 。 - -源码如下,读者可自行参阅源码及注释: - -```java -public E poll() { - //尝试获取可重入锁 - final ReentrantLock lock = this.lock; - lock.lock(); - try { - //查看队列第一个元素,判断元素是否为空 - E first = q.peek(); - - //若元素为空,或者元素未到期,则直接返回空 - if (first == null || first.getDelay(NANOSECONDS) > 0) - return null; - else - //若元素不为空且到期了,直接调用poll返回出去 - return q.poll(); - } finally { - //释放可重入锁lock - lock.unlock(); - } -} -``` - -### 查看元素 - -上文获取元素时都会调用到 `peek` 方法,peek 顾名思义仅仅窥探一下队列中的元素,它的步骤就 4 步: - -1. 上锁。 -2. 调用优先队列 q 的 peek 方法查看索引 0 位置的元素。 -3. 释放锁。 -4. 将元素返回出去。 - -```java -public E peek() { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - return q.peek(); - } finally { - lock.unlock(); - } -} -``` - -## DelayQueue 常见面试题 - -### DelayQueue 的实现原理是什么? - -`DelayQueue` 底层是使用优先队列 `PriorityQueue` 来存储元素,而 `PriorityQueue` 采用二叉小顶堆的思想确保值小的元素排在最前面,这就使得 `DelayQueue` 对于延迟任务优先级的管理就变得十分方便了。同时 `DelayQueue` 为了保证线程安全还用到了可重入锁 `ReentrantLock`,确保单位时间内只有一个线程可以操作延迟队列。最后,为了实现多线程之间等待和唤醒的交互效率,`DelayQueue` 还用到了 `Condition`,通过 `Condition` 的 `await` 和 `signal` 方法完成多线程之间的等待唤醒。 - -### DelayQueue 的实现是否线程安全? - -`DelayQueue` 的实现是线程安全的,它通过 `ReentrantLock` 实现了互斥访问和 `Condition` 实现了线程间的等待和唤醒操作,可以保证多线程环境下的安全性和可靠性。 - -### DelayQueue 的使用场景有哪些? - -`DelayQueue` 通常用于实现定时任务调度和缓存过期删除等场景。在定时任务调度中,需要将需要执行的任务封装成延迟任务对象,并将其添加到 `DelayQueue` 中,`DelayQueue` 会自动按照剩余延迟时间进行升序排序(默认情况),以保证任务能够按照时间先后顺序执行。对于缓存过期这个场景而言,在数据被缓存到内存之后,我们可以将缓存的 key 封装成一个延迟的删除任务,并将其添加到 `DelayQueue` 中,当数据过期时,拿到这个任务的 key,将这个 key 从内存中移除。 - -### DelayQueue 中 Delayed 接口的作用是什么? - -`Delayed` 接口定义了元素的剩余延迟时间(`getDelay`)和元素之间的比较规则(该接口继承了 `Comparable` 接口)。若希望元素能够存放到 `DelayQueue` 中,就必须实现 `Delayed` 接口的 `getDelay()` 方法和 `compareTo()` 方法,否则 `DelayQueue` 无法得知当前任务剩余时长和任务优先级的比较。 - -### DelayQueue 和 Timer/TimerTask 的区别是什么? - -`DelayQueue` 和 `Timer/TimerTask` 都可以用于实现定时任务调度,但是它们的实现方式不同。`DelayQueue` 是基于优先级队列和堆排序算法实现的,可以实现多个任务按照时间先后顺序执行;而 `Timer/TimerTask` 是基于单线程实现的,只能按照任务的执行顺序依次执行,如果某个任务执行时间过长,会影响其他任务的执行。另外,`DelayQueue` 还支持动态添加和移除任务,而 `Timer/TimerTask` 只能在创建时指定任务。 - -## 参考文献 - -- 《深入理解高并发编程:JDK 核心技术》: -- 一口气说出 Java 6 种延时队列的实现方法(面试官也得服): -- 图解 DelayQueue 源码(java 8)——延时队列的小九九: - +// \ No newline at end of file diff --git a/docs/java/collection/hashmap-source-code.md b/docs/java/collection/hashmap-source-code.md index 0e9342f0edf..5ad55aa888d 100644 --- a/docs/java/collection/hashmap-source-code.md +++ b/docs/java/collection/hashmap-source-code.md @@ -1,129 +1,129 @@ --- -title: HashMap 源码分析 +title: HashMap Source Code Analysis category: Java tag: - - Java集合 + - Java Collections --- -> 感谢 [changfubai](https://github.com/changfubai) 对本文的改进做出的贡献! +> Thanks to [changfubai](https://github.com/changfubai) for the contributions to the improvement of this article! -## HashMap 简介 +## Introduction to HashMap -HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的。 +HashMap is primarily used to store key-value pairs. It implements the Map interface based on a hash table and is one of the commonly used Java collections. It is not thread-safe. -`HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个 +`HashMap` can store null keys and values, but there can only be one null key, while multiple null values are allowed. -JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于等于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 +Before JDK 1.8, HashMap was composed of an array and a linked list, where the array was the main structure and the linked list existed primarily to resolve hash collisions (using the "chaining method"). Since JDK 1.8, significant changes have been made in How `HashMap` resolves hash collisions. When the length of the linked list exceeds the threshold (default is 8), it converts the linked list into a red-black tree before determining what to do next. If the current array length is less than 64, it will first expand the array rather than convert it to a red-black tree, in order to reduce search time. -`HashMap` 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, `HashMap` 总是使用 2 的幂作为哈希表的大小。 +The default initial size of `HashMap` is 16. Each time it expands, the capacity is doubled. Additionally, `HashMap` always uses powers of 2 as the size of its hash table. -## 底层数据结构分析 +## Analysis of the Underlying Data Structure -### JDK1.8 之前 +### Before JDK 1.8 -JDK1.8 之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。 +Before JDK 1.8, the underlying structure of HashMap was a combination of **array and linked list**, also referred to as **chaining**. -HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。 +HashMap obtains a hash value through the key's hashCode processed by a perturbation function, and then determines the storage position of the current element using the formula `(n - 1) & hash` (where n refers to the length of the array). If there is an existing element at that position, it checks if that element’s hash value and key are the same as the one being inserted. If they are the same, it directly replaces it. If they are different, it resolves the collision using the chaining method. -所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。 +The perturbation function refers to the hash method of HashMap. The purpose of using the hash method, or the perturbation function, is to avoid collisions caused by poorly implemented hashCode() methods. In other words, using the perturbation function can reduce collisions. -**JDK 1.8 HashMap 的 hash 方法源码:** +**Source code of the hash method in JDK 1.8 HashMap:** -JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 +The hash method in JDK 1.8 is simplified compared to the hash method in JDK 1.7, but the principle remains unchanged. ```java - static final int hash(Object key) { - int h; - // key.hashCode():返回散列值也就是hashcode - // ^:按位异或 - // >>>:无符号右移,忽略符号位,空位都以0补齐 - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - } +static final int hash(Object key) { + int h; + // key.hashCode(): returns hash value also known as hashcode + // ^: bitwise XOR + // >>>: unsigned right shift, ignoring the sign bit, filling the empty bits with 0 + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} ``` -对比一下 JDK1.7 的 HashMap 的 hash 方法源码. +Let’s compare this with the hash method source code in JDK 1.7. ```java static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). - + h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } ``` -相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。 +Compared to the hash method in JDK 1.8, the hash method in JDK 1.7 is somewhat less efficient because it performs perturbation 4 times. -所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 +The so-called **"chaining method"** combines linked lists and arrays, creating an array of linked lists. When a hash collision occurs, the colliding value is simply added to the linked list. -![jdk1.8 之前的内部结构-HashMap](https://oss.javaguide.cn/github/javaguide/java/collection/jdk1.7_hashmap.png) +![Internal Structure of HashMap Before JDK 1.8](https://oss.javaguide.cn/github/javaguide/java/collection/jdk1.7_hashmap.png) -### JDK1.8 之后 +### After JDK 1.8 -相比于之前的版本,JDK1.8 以后在解决哈希冲突时有了较大的变化。 +After JDK 1.8, there were significant changes in how hash collisions are resolved compared to previous versions. -当链表长度大于阈值(默认为 8)时,会首先调用 `treeifyBin()`方法。这个方法会根据 HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是执行 `resize()` 方法对数组扩容。相关源码这里就不贴了,重点关注 `treeifyBin()`方法即可! +When the length of the linked list exceeds the threshold (default is 8), the method `treeifyBin()` will be called first. This method decides whether to convert the structure into a red-black tree based on the HashMap array. The conversion to a red-black tree will only occur when the array length is greater than or equal to 64, in order to reduce search time. Otherwise, it will simply call the `resize()` method to expand the array. The relevant source code will not be included here; the focus should be on the `treeifyBin()` method! -![jdk1.8之后的内部结构-HashMap](https://oss.javaguide.cn/github/javaguide/java/collection/jdk1.8_hashmap.png) +![Internal Structure of HashMap After JDK 1.8](https://oss.javaguide.cn/github/javaguide/java/collection/jdk1.8_hashmap.png) -**类的属性:** +**Class Properties:** ```java public class HashMap extends AbstractMap implements Map, Cloneable, Serializable { - // 序列号 + // Serial number private static final long serialVersionUID = 362498820763181265L; - // 默认的初始容量是16 + // Default initial capacity is 16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; - // 最大容量 + // Maximum capacity static final int MAXIMUM_CAPACITY = 1 << 30; - // 默认的负载因子 + // Default load factor static final float DEFAULT_LOAD_FACTOR = 0.75f; - // 当桶(bucket)上的结点数大于等于这个值时会转成红黑树 + // Threshold for converting to red-black tree static final int TREEIFY_THRESHOLD = 8; - // 当桶(bucket)上的结点数小于等于这个值时树转链表 + // Threshold for converting tree back to linked list static final int UNTREEIFY_THRESHOLD = 6; - // 桶中结构转化为红黑树对应的table的最小容量 + // Minimum capacity for converting bucket structure to red-black tree static final int MIN_TREEIFY_CAPACITY = 64; - // 存储元素的数组,总是2的幂次倍 - transient Node[] table; - // 一个包含了映射中所有键值对的集合视图 - transient Set> entrySet; - // 存放元素的个数,注意这个不等于数组的长度。 + // Array for storing elements, always a power of 2 + transient Node[] table; + // A view of the collection containing all key-value pairs in the map + transient Set> entrySet; + // Count of elements stored; note this is not equal to the length of the array. transient int size; - // 每次扩容和更改map结构的计数器 + // Counter for each expansion and structural change to the map transient int modCount; - // 阈值(容量*负载因子) 当实际大小超过阈值时,会进行扩容 + // Threshold (capacity * load factor); when actual size exceeds this threshold, resizing occurs int threshold; - // 负载因子 + // Load factor final float loadFactor; } ``` -- **loadFactor 负载因子** +- **Load Factor** - loadFactor 负载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。 + The load factor controls the density of the data stored in the array. The closer the load factor is to 1, the more items (entries) the array can hold, and the denser the storage becomes, which increases the length of lists. Conversely, a smaller load factor, nearing 0, results in sparser data storage with fewer items. - **loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值**。 + **A load factor that is too large can reduce search efficiency, while a load factor that is too small can lead to low array utilization and widespread storage. The default value of 0.75f provides a balanced threshold recommended by the official documentation.** - 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量超过了 16 \* 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。 + Given the default capacity of 16 and the load factor of 0.75, as data is stored and exceeds the limit of 16 * 0.75 = 12, an expansion of the current 16 capacity is needed, which involves operations like rehashing and data copying, potentially consuming a lot of performance. -- **threshold** +- **Threshold** - **threshold = capacity \* loadFactor**,**当 Size>threshold**的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 **衡量数组是否需要扩增的一个标准**。 + **Threshold = capacity * load factor**. When **size > threshold**, it indicates that the array requires an increase in size, thus serving as **a criterion for measuring the need for an array expansion**. -**Node 节点类源码:** +**Node Class Source Code:** ```java -// 继承自 Map.Entry +// Inherits from Map.Entry static class Node implements Map.Entry { - final int hash;// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较 - final K key;//键 - V value;//值 - // 指向下一个节点 + final int hash; // Hash value to compare with other elements when storing in hashmap + final K key; // Key + V value; // Value + // Points to the next node Node next; Node(int hash, K key, V value, Node next) { this.hash = hash; @@ -134,7 +134,7 @@ static class Node implements Map.Entry { public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } - // 重写hashCode()方法 + // Override hashCode() method public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } @@ -144,7 +144,7 @@ static class Node implements Map.Entry { value = newValue; return oldValue; } - // 重写 equals() 方法 + // Override equals() method public final boolean equals(Object o) { if (o == this) return true; @@ -159,93 +159,98 @@ static class Node implements Map.Entry { } ``` -**树节点类源码:** +**Tree Node Class Source Code:** ```java static final class TreeNode extends LinkedHashMap.Entry { - TreeNode parent; // 父 - TreeNode left; // 左 - TreeNode right; // 右 - TreeNode prev; // needed to unlink next upon deletion - boolean red; // 判断颜色 + TreeNode parent; // Parent + TreeNode left; // Left + TreeNode right; // Right + TreeNode prev; // Needed to unlink next upon deletion + boolean red; // Indicates color TreeNode(int hash, K key, V val, Node next) { super(hash, key, val, next); } - // 返回根节点 + // Returns the root node final TreeNode root() { for (TreeNode r = this, p;;) { if ((p = r.parent) == null) return r; r = p; - } + } + } +} ``` -## HashMap 源码分析 +## Analysis of HashMap Source Code -### 构造方法 +### Constructor -HashMap 中有四个构造方法,它们分别如下: +HashMap has four constructors, which are as follows: ```java - // 默认构造函数。 + // Default constructor. public HashMap() { - this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted - } - - // 包含另一个“Map”的构造函数 - public HashMap(Map m) { - this.loadFactor = DEFAULT_LOAD_FACTOR; - putMapEntries(m, false);//下面会分析到这个方法 - } - - // 指定“容量大小”的构造函数 - public HashMap(int initialCapacity) { - this(initialCapacity, DEFAULT_LOAD_FACTOR); - } - - // 指定“容量大小”和“负载因子”的构造函数 - public HashMap(int initialCapacity, float loadFactor) { - if (initialCapacity < 0) - throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); - if (initialCapacity > MAXIMUM_CAPACITY) - initialCapacity = MAXIMUM_CAPACITY; - if (loadFactor <= 0 || Float.isNaN(loadFactor)) - throw new IllegalArgumentException("Illegal load factor: " + loadFactor); - this.loadFactor = loadFactor; - // 初始容量暂时存放到 threshold ,在resize中再赋值给 newCap 进行table初始化 - this.threshold = tableSizeFor(initialCapacity); - } + this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted + } + + // Constructor with another "Map" + public HashMap(Map m) { + this.loadFactor = DEFAULT_LOAD_FACTOR; + putMapEntries(m, false); // this method will be analyzed later + } + + // Constructor specifying "initial capacity" + public HashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + // Constructor specifying "initial capacity" and "load factor" + public HashMap(int initialCapacity, float loadFactor) { + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); + if (initialCapacity > MAXIMUM_CAPACITY) + initialCapacity = MAXIMUM_CAPACITY; + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new IllegalArgumentException("Illegal load factor: " + loadFactor); + this.loadFactor = loadFactor; + // Store initial capacity temporarily in threshold; assign to newCap for table initialization during resize + this.threshold = tableSizeFor(initialCapacity); + } ``` -> 值得注意的是上述四个构造方法中,都初始化了负载因子 loadFactor,由于 HashMap 中没有 capacity 这样的字段,即使指定了初始化容量 initialCapacity ,也只是通过 tableSizeFor 将其扩容到与 initialCapacity 最接近的 2 的幂次方大小,然后暂时赋值给 threshold ,后续通过 resize 方法将 threshold 赋值给 newCap 进行 table 的初始化。 +> It is worth noting that all four constructors initialize the load factor. Since HashMap does not have a field like capacity, even if an initial capacity is specified, it is rounded to the nearest power of 2 in `tableSizeFor` and temporarily assigned to threshold, which is later assigned to newCap for table initialization in the resize method. -**putMapEntries 方法:** +**putMapEntries Method:** ```java final void putMapEntries(Map m, boolean evict) { int s = m.size(); if (s > 0) { - // 判断table是否已经初始化 + // Check if table has already been initialized if (table == null) { // pre-size /* - * 未初始化,s为m的实际元素个数,ft=s/loadFactor => s=ft*loadFactor, 跟我们前面提到的 - * 阈值=容量*负载因子 是不是很像,是的,ft指的是要添加s个元素所需的最小的容量 + * Not initialized; s is the actual number of elements in m. + * ft=s/loadFactor => s=ft*loadFactor, which resembles the concept + * threshold=capacity*loadFactor. + * ft represents the minimum capacity required to add s elements. */ float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); /* - * 根据构造函数可知,table未初始化,threshold实际上是存放的初始化容量,如果添加s个元素所 - * 需的最小容量大于初始化容量,则将最小容量扩容为最接近的2的幂次方大小作为初始化。 - * 注意这里不是初始化阈值 + * According to the constructor, the table has not been initialized. + * If the minimum capacity required to add s elements exceeds the initialized capacity, + * then the minimum capacity is expanded to the nearest power of 2 as the initialization. + * Note this is not the initialization threshold. */ if (t > threshold) threshold = tableSizeFor(t); } - // 已初始化,并且m元素个数大于阈值,进行扩容处理 + // Initialized, and element count in m exceeds threshold; proceed with resize else if (s > threshold) resize(); - // 将m中的所有元素添加至HashMap中,如果table未初始化,putVal中会调用resize初始化或扩容 + // Add all elements from m to HashMap; if table is uninitialized, putVal will call resize to initialize or expand for (Map.Entry e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); @@ -255,14 +260,14 @@ final void putMapEntries(Map m, boolean evict) { } ``` -### put 方法 +### put Method -HashMap 只提供了 put 用于添加元素,putVal 方法只是给 put 方法调用的一个方法,并没有提供给用户使用。 +HashMap only provides the put method for adding elements; the putVal method is called by put and is not exposed to users. -**对 putVal 方法添加元素的分析如下:** +**Analysis of adding elements through the putVal method:** -1. 如果定位到的数组位置没有元素 就直接插入。 -2. 如果定位到的数组位置有元素就和要插入的 key 比较,如果 key 相同就直接覆盖,如果 key 不相同,就判断 p 是否是一个树节点,如果是就调用`e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value)`将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。 +1. If the located array position is empty, it is inserted directly. +1. If there is already an element positioned at the array location, it will compare with the key to be inserted. If the keys are the same, it will directly overwrite; if the keys are different, it checks if p is a tree node, and if so, calls `e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value)` to add the element. If it is not, it iterates through the linked list to insert (at the end of the linked list). ![ ](https://oss.javaguide.cn/github/javaguide/database/sql/put.png) @@ -274,90 +279,91 @@ public V put(K key, V value) { final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; - // table未初始化或者长度为0,进行扩容 + // If table is uninitialized or length is 0, expand if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; - // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) + // (n - 1) & hash determines which bucket to store the element in. + // If that bucket is empty, create a new node and store it there (initially, this will add the node to the array) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); - // 桶中已经存在元素(处理hash冲突) + // If the bucket already has an element (handle hash collisions) else { Node e; K k; - //快速判断第一个节点table[i]的key是否与插入的key一样,若相同就直接使用插入的值p替换掉旧的值e。 + // Quickly check if the first node table[i] has a key the same as the key being inserted; if so, replace the old value with the new value p directly. if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; - // 判断插入的是否是红黑树节点 + // Check if the inserted node is a red-black tree node else if (p instanceof TreeNode) - // 放入树中 + // Insert into the tree e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); - // 不是红黑树节点则说明为链表结点 + // If not a red-black tree node, it is a linked list node else { - // 在链表最末插入结点 + // Insert the node at the tail of the linked list for (int binCount = 0; ; ++binCount) { - // 到达链表的尾部 + // Reached the end of the linked list if ((e = p.next) == null) { - // 在尾部插入新结点 + // Insert new node at the end p.next = newNode(hash, key, value, null); - // 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法 - // 这个方法会根据 HashMap 数组来决定是否转换为红黑树。 - // 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。 + // If the count of nodes reaches the threshold (default is 8), execute treeifyBin method + // This method will determine whether to convert to a red-black tree based on the HashMap array. + // A conversion to a red-black tree will only occur if the array length is greater than or equal to 64 to reduce search time; otherwise, just expand the array. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); - // 跳出循环 + // Break the loop break; } - // 判断链表中结点的key值与插入的元素的key值是否相等 + // Compare the key value of the node in the linked list with the key value of the incoming element if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) - // 相等,跳出循环 + // If equal, break the loop break; - // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 + // This is used to traverse the linked list; combined with the previous e = p.next, it can traverse the linked list p = e; } } - // 表示在桶中找到key值、hash值与插入元素相等的结点 + // Indicates that a node with the key value and hash value equal to the inserted element was found in the bucket if (e != null) { - // 记录e的value + // Store the value of e V oldValue = e.value; - // onlyIfAbsent为false或者旧值为null + // onlyIfAbsent is false or old value is null if (!onlyIfAbsent || oldValue == null) - //用新值替换旧值 + // Replace the old value with the new value e.value = value; - // 访问后回调 + // Callback after access afterNodeAccess(e); - // 返回旧值 + // Return the old value return oldValue; } } - // 结构性修改 + // Structural modification ++modCount; - // 实际大小大于阈值则扩容 + // If actual size exceeds threshold, resize if (++size > threshold) resize(); - // 插入后回调 + // Callback after insertion afterNodeInsertion(evict); return null; } ``` -**我们再来对比一下 JDK1.7 put 方法的代码** +**Let’s compare the put method code in JDK1.7.** -**对于 put 方法的分析如下:** +**Analysis of the put method is as follows:** -- ① 如果定位到的数组位置没有元素 就直接插入。 -- ② 如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的 key 比较,如果 key 相同就直接覆盖,不同就采用头插法插入元素。 +- ① If the located array position is empty, it is inserted directly. +- ② If there is already an element located at the array position, traverse the linked list with that element as the head node, comparing it sequentially with the key being inserted; if the keys are the same, it directly replaces it; if not, the insertion is done using a head insert method. ```java -public V put(K key, V value) +public V put(K key, V value) { if (table == EMPTY_TABLE) { - inflateTable(threshold); -} + inflateTable(threshold); + } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); - for (Entry e = table[i]; e != null; e = e.next) { // 先遍历 + for (Entry e = table[i]; e != null; e = e.next) { // first traversal Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; @@ -368,12 +374,12 @@ public V put(K key, V value) } modCount++; - addEntry(hash, key, value, i); // 再插入 + addEntry(hash, key, value, i); // Insert afterwards return null; } ``` -### get 方法 +### get Method ```java public V get(Object key) { @@ -385,16 +391,16 @@ final Node getNode(int hash, Object key) { Node[] tab; Node first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { - // 数组元素相等 + // Array elements are equal if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; - // 桶中不止一个节点 + // More than one node in the bucket if ((e = first.next) != null) { - // 在树中get + // Get in the tree if (first instanceof TreeNode) return ((TreeNode)first).getTreeNode(hash, key); - // 在链表中get + // Get in the linked list do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) @@ -406,9 +412,9 @@ final Node getNode(int hash, Object key) { } ``` -### resize 方法 +### resize Method -进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。resize 方法实际上是将 table 初始化和 table 扩容 进行了整合,底层的行为都是给 table 赋值一个新的数组。 +Expansion will be accompanied by a rehash allocation and a traversal of all elements in the hash table, which is very time-consuming. In programming, it is advisable to avoid using resize. The resize method actually integrates table initialization with table expansion, where the underlying behavior is to assign a new array to the table. ```java final Node[] resize() { @@ -417,26 +423,26 @@ final Node[] resize() { int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { - // 超过最大值就不再扩充了,就只好随你碰撞去吧 + // If exceeding the maximum size, do not expand further, let collisions occur if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } - // 没超过最大值,就扩充为原来的2倍 + // Not exceeding the maximum size, expand to double the original size else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold - // 创建对象时初始化容量大小放在threshold中,此时只需要将其作为新的数组容量 + // When creating an object, initialize capacity; at this point just take it as the new array capacity newCap = oldThr; else { - // signifies using defaults 无参构造函数创建的对象在这里计算容量和阈值 + // signifies using defaults No-arg constructor creates an object with calculated capacity and threshold here newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { - // 创建时指定了初始化容量或者负载因子,在这里进行阈值初始化, - // 或者扩容前的旧容量小于16,在这里计算新的resize上限 + // Presumably specified initialization capacity or load factor, initializing threshold here, + // or the old capacity was less than 16, calculating the new resize upper limit float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } @@ -445,17 +451,17 @@ final Node[] resize() { Node[] newTab = (Node[])new Node[newCap]; table = newTab; if (oldTab != null) { - // 把每个bucket都移动到新的buckets中 + // Move each bucket to the new buckets for (int j = 0; j < oldCap; ++j) { Node e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) - // 只有一个节点,直接计算元素新的位置即可 + // Only one node; directly calculate the new position for the element newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) - // 将红黑树拆分成2棵子树,如果子树节点数小于等于 UNTREEIFY_THRESHOLD(默认为 6),则将子树转换为链表。 - // 如果子树节点数大于 UNTREEIFY_THRESHOLD,则保持子树的树结构。 + // Split the red-black tree into two subtrees, if the node count of the subtree is less than or equal to UNTREEIFY_THRESHOLD (default 6), convert the subtree into a linked list. + // If the node count of the subtree exceeds UNTREEIFY_THRESHOLD, maintain the tree structure of the subtree. ((TreeNode)e).split(this, newTab, j, oldCap); else { Node loHead = null, loTail = null; @@ -463,7 +469,7 @@ final Node[] resize() { Node next; do { next = e.next; - // 原索引 + // Original index if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; @@ -471,7 +477,7 @@ final Node[] resize() { loTail.next = e; loTail = e; } - // 原索引+oldCap + // Original index + oldCap else { if (hiTail == null) hiHead = e; @@ -480,12 +486,12 @@ final Node[] resize() { hiTail = e; } } while ((e = next) != null); - // 原索引放到bucket里 + // Place original index in bucket if (loTail != null) { loTail.next = null; newTab[j] = loHead; } - // 原索引+oldCap放到bucket里 + // Place original index + oldCap in the bucket if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; @@ -498,7 +504,7 @@ final Node[] resize() { } ``` -## HashMap 常用方法测试 +## Testing Common Methods of HashMap ```java package map; @@ -511,64 +517,66 @@ public class HashMapDemo { public static void main(String[] args) { HashMap map = new HashMap(); - // 键不能重复,值可以重复 - map.put("san", "张三"); - map.put("si", "李四"); - map.put("wu", "王五"); - map.put("wang", "老王"); - map.put("wang", "老王2");// 老王被覆盖 - map.put("lao", "老王"); - System.out.println("-------直接输出hashmap:-------"); + // Keys cannot be repeated, values can be repeated + map.put("san", "Zhang San"); + map.put("si", "Li Si"); + map.put("wu", "Wang Wu"); + map.put("wang", "Old Wang"); + map.put("wang", "Old Wang 2");// Old Wang is overwritten + map.put("lao", "Old Wang"); + System.out.println("-------Direct Output of HashMap:-------"); System.out.println(map); /** - * 遍历HashMap + * Traverse HashMap */ - // 1.获取Map中的所有键 - System.out.println("-------foreach获取Map中所有的键:------"); + // 1. Get all keys in the Map + System.out.println("-------foreach to get all keys in the Map:------"); Set keys = map.keySet(); for (String key : keys) { - System.out.print(key+" "); + System.out.print(key + " "); } - System.out.println();//换行 - // 2.获取Map中所有值 - System.out.println("-------foreach获取Map中所有的值:------"); + System.out.println();// New line + // 2. Get all values in the Map + System.out.println("-------foreach to get all values in the Map:------"); Collection values = map.values(); for (String value : values) { - System.out.print(value+" "); + System.out.print(value + " "); } - System.out.println();//换行 - // 3.得到key的值的同时得到key所对应的值 - System.out.println("-------得到key的值的同时得到key所对应的值:-------"); + System.out.println();// New line + // 3. Get the value corresponding to the key along with the key + System.out.println("-------Get the value corresponding to the key along with the key:-------"); Set keys2 = map.keySet(); for (String key : keys2) { - System.out.print(key + ":" + map.get(key)+" "); - + System.out.print(key + ":" + map.get(key) + " "); } /** - * 如果既要遍历key又要value,那么建议这种方式,因为如果先获取keySet然后再执行map.get(key),map内部会执行两次遍历。 - * 一次是在获取keySet的时候,一次是在遍历所有key的时候。 + * If you need to traverse both keys and values, this method is recommended, + * because if you first get the keySet and then execute map.get(key), + * there will be two traversals internally by the map. + * One to get the keySet, and one while traversing all keys. */ - // 当我调用put(key,value)方法的时候,首先会把key和value封装到 - // Entry这个静态内部类对象中,把Entry对象再添加到数组中,所以我们想获取 - // map中的所有键值对,我们只要获取数组中的所有Entry对象,接下来 - // 调用Entry对象中的getKey()和getValue()方法就能获取键值对了 + // When I call put(key,value), the key and value are first encapsulated into + // an Entry static inner class object, which is then added to the array, + // so to get all key-value pairs in the map, + // we need to get all Entry objects in the array, + // then call Entry object's getKey() and getValue() methods to retrieve the key-value pairs. Set> entrys = map.entrySet(); for (java.util.Map.Entry entry : entrys) { System.out.println(entry.getKey() + "--" + entry.getValue()); } /** - * HashMap其他常用方法 + * Other commonly used methods of HashMap */ - System.out.println("after map.size():"+map.size()); - System.out.println("after map.isEmpty():"+map.isEmpty()); + System.out.println("after map.size():" + map.size()); + System.out.println("after map.isEmpty():" + map.isEmpty()); System.out.println(map.remove("san")); - System.out.println("after map.remove():"+map); - System.out.println("after map.get(si):"+map.get("si")); - System.out.println("after map.containsKey(si):"+map.containsKey("si")); - System.out.println("after containsValue(李四):"+map.containsValue("李四")); - System.out.println(map.replace("si", "李四2")); - System.out.println("after map.replace(si, 李四2):"+map); + System.out.println("after map.remove():" + map); + System.out.println("after map.get(si):" + map.get("si")); + System.out.println("after map.containsKey(si):" + map.containsKey("si")); + System.out.println("after containsValue(李四):" + map.containsValue("李四")); + System.out.println(map.replace("si", "Li Si 2")); + System.out.println("after map.replace(si, Li Si 2):" + map); } } diff --git a/docs/java/collection/java-collection-precautions-for-use.md b/docs/java/collection/java-collection-precautions-for-use.md index cb68403f57c..bc14238b1b9 100644 --- a/docs/java/collection/java-collection-precautions-for-use.md +++ b/docs/java/collection/java-collection-precautions-for-use.md @@ -1,23 +1,23 @@ --- -title: Java集合使用注意事项总结 +title: Summary of Java Collection Usage Precautions category: Java tag: - - Java集合 + - Java Collections --- -这篇文章我根据《阿里巴巴 Java 开发手册》总结了关于集合使用常见的注意事项以及其具体原理。 +In this article, I summarize common precautions and specific principles regarding the use of collections based on the "Alibaba Java Development Manual." -强烈建议小伙伴们多多阅读几遍,避免自己写代码的时候出现这些低级的问题。 +I strongly recommend everyone to read this multiple times to avoid these basic issues when writing code. -## 集合判空 +## Checking for Empty Collections -《阿里巴巴 Java 开发手册》的描述如下: +The description in the "Alibaba Java Development Manual" is as follows: -> **判断所有集合内部的元素是否为空,使用 `isEmpty()` 方法,而不是 `size()==0` 的方式。** +> **When checking if all elements within a collection are empty, use the `isEmpty()` method instead of `size() == 0`.** -这是因为 `isEmpty()` 方法的可读性更好,并且时间复杂度为 `O(1)`。 +This is because the `isEmpty()` method is more readable and has a time complexity of `O(1)`. -绝大部分我们使用的集合的 `size()` 方法的时间复杂度也是 `O(1)`,不过,也有很多复杂度不是 `O(1)` 的,比如 `java.util.concurrent` 包下的 `ConcurrentLinkedQueue`。`ConcurrentLinkedQueue` 的 `isEmpty()` 方法通过 `first()` 方法进行判断,其中 `first()` 方法返回的是队列中第一个值不为 `null` 的节点(节点值为`null`的原因是在迭代器中使用的逻辑删除) +For most collections we use, the time complexity of the `size()` method is also `O(1)`. However, there are many that are not `O(1)`, such as `ConcurrentLinkedQueue` in the `java.util.concurrent` package. The `isEmpty()` method of `ConcurrentLinkedQueue` checks using the `first()` method, which returns the first node in the queue whose value is not `null` (the reason for node values being `null` is due to logical deletions used in iterators). ```java public boolean isEmpty() { return first() == null; } @@ -27,8 +27,8 @@ Node first() { for (;;) { for (Node h = head, p = h, q;;) { boolean hasItem = (p.item != null); - if (hasItem || (q = p.next) == null) { // 当前节点值不为空 或 到达队尾 - updateHead(h, p); // 将head设置为p + if (hasItem || (q = p.next) == null) { // Current node's value is not null or reached the end of the queue + updateHead(h, p); // Set head to p return hasItem ? p : null; } else if (p == q) continue restartFromHead; @@ -38,7 +38,7 @@ Node first() { } ``` -由于在插入与删除元素时,都会执行`updateHead(h, p)`方法,所以该方法的执行的时间复杂度可以近似为`O(1)`。而 `size()` 方法需要遍历整个链表,时间复杂度为`O(n)` +Since the `updateHead(h, p)` method is executed during the insertion and deletion of elements, its time complexity can be approximated to `O(1)`. However, the `size()` method requires traversing the entire linked list, with a time complexity of `O(n)`. ```java public int size() { @@ -51,7 +51,7 @@ public int size() { } ``` -此外,在`ConcurrentHashMap` 1.7 中 `size()` 方法和 `isEmpty()` 方法的时间复杂度也不太一样。`ConcurrentHashMap` 1.7 将元素数量存储在每个`Segment` 中,`size()` 方法需要统计每个 `Segment` 的数量,而 `isEmpty()` 只需要找到第一个不为空的 `Segment` 即可。但是在`ConcurrentHashMap` 1.8 中的 `size()` 方法和 `isEmpty()` 都需要调用 `sumCount()` 方法,其时间复杂度与 `Node` 数组的大小有关。下面是 `sumCount()` 方法的源码: +Additionally, in `ConcurrentHashMap` version 1.7, the time complexities of the `size()` and `isEmpty()` methods are different. `ConcurrentHashMap` version 1.7 stores the element count in each `Segment`, and the `size()` method needs to count each `Segment`, while `isEmpty()` just needs to find the first non-empty `Segment`. However, in `ConcurrentHashMap` version 1.8, both the `size()` and `isEmpty()` methods need to call the `sumCount()` method, which has a time complexity that relates to the size of the `Node` array. Below is the source code for the `sumCount()` method: ```java final long sumCount() { @@ -65,13 +65,13 @@ final long sumCount() { } ``` -这是因为在并发的环境下,`ConcurrentHashMap` 将每个 `Node` 中节点的数量存储在 `CounterCell[]` 数组中。在 `ConcurrentHashMap` 1.7 中,将元素数量存储在每个`Segment` 中,`size()` 方法需要统计每个 `Segment` 的数量,而 `isEmpty()` 只需要找到第一个不为空的 `Segment` 即可。 +This is because in a concurrent environment, `ConcurrentHashMap` stores the count of nodes in each `Node` in a `CounterCell[]` array. In `ConcurrentHashMap` version 1.7, the element count is stored in each `Segment`, and the `size()` method needs to count each `Segment`, while `isEmpty()` only needs to find the first non-empty `Segment`. -## 集合转 Map +## Converting Collection to Map -《阿里巴巴 Java 开发手册》的描述如下: +The description in the "Alibaba Java Development Manual" is as follows: -> **在使用 `java.util.stream.Collectors` 类的 `toMap()` 方法转为 `Map` 集合时,一定要注意当 value 为 null 时会抛 NPE 异常。** +> **When using the `toMap()` method of the `java.util.stream.Collectors` class to convert to a Map collection, be aware that a NPE exception will be thrown if the value is null.** ```java class Person { @@ -83,13 +83,13 @@ class Person { List bookList = new ArrayList<>(); bookList.add(new Person("jack","18163138123")); bookList.add(new Person("martin",null)); -// 空指针异常 +// Null pointer exception bookList.stream().collect(Collectors.toMap(Person::getName, Person::getPhoneNumber)); ``` -下面我们来解释一下原因。 +Let's explain the reason. -首先,我们来看 `java.util.stream.Collectors` 类的 `toMap()` 方法 ,可以看到其内部调用了 `Map` 接口的 `merge()` 方法。 +First, we look at the `toMap()` method of the `java.util.stream.Collectors` class. It internally calls the `merge()` method of the `Map` interface. ```java public static > @@ -104,9 +104,9 @@ Collector toMap(Function keyMapper, } ``` -`Map` 接口的 `merge()` 方法如下,这个方法是接口中的默认实现。 +The `merge()` method of the `Map` interface is as follows. This method is the default implementation in the interface. -> 如果你还不了解 Java 8 新特性的话,请看这篇文章:[《Java8 新特性总结》](https://mp.weixin.qq.com/s/ojyl7B6PiHaTWADqmUq2rw) 。 +> If you are not familiar with the new features of Java 8, please read this article: [Summary of Java 8 New Features](https://mp.weixin.qq.com/s/ojyl7B6PiHaTWADqmUq2rw). ```java default V merge(K key, V value, @@ -125,7 +125,7 @@ default V merge(K key, V value, } ``` -`merge()` 方法会先调用 `Objects.requireNonNull()` 方法判断 value 是否为空。 +The `merge()` method first calls the `Objects.requireNonNull()` method to check if the value is null. ```java public static T requireNonNull(T obj) { @@ -135,47 +135,47 @@ public static T requireNonNull(T obj) { } ``` -## 集合遍历 +## Iterating Through Collections -《阿里巴巴 Java 开发手册》的描述如下: +The description in the "Alibaba Java Development Manual" is as follows: -> **不要在 foreach 循环里进行元素的 `remove/add` 操作。remove 元素请使用 `Iterator` 方式,如果并发操作,需要对 `Iterator` 对象加锁。** +> **Do not perform `remove/add` operations on elements within a foreach loop. To remove elements, use the `Iterator` approach. If concurrent operations are involved, you need to lock the `Iterator` object.** -通过反编译你会发现 foreach 语法底层其实还是依赖 `Iterator` 。不过, `remove/add` 操作直接调用的是集合自己的方法,而不是 `Iterator` 的 `remove/add`方法 +By decompiling, you will find that the foreach syntax fundamentally relies on `Iterator`. However, the `remove/add` operations directly call the collection's own methods instead of the `Iterator`'s `remove/add` methods. -这就导致 `Iterator` 莫名其妙地发现自己有元素被 `remove/add` ,然后,它就会抛出一个 `ConcurrentModificationException` 来提示用户发生了并发修改异常。这就是单线程状态下产生的 **fail-fast 机制**。 +This leads to the `Iterator` confusingly detecting that some elements have been `remove/add`, which will result in it throwing a `ConcurrentModificationException` to indicate that a concurrent modification exception has occurred. This is the **fail-fast mechanism** that arises in single-threaded contexts. -> **fail-fast 机制**:多个线程对 fail-fast 集合进行修改的时候,可能会抛出`ConcurrentModificationException`。 即使是单线程下也有可能会出现这种情况,上面已经提到过。 +> **Fail-fast mechanism**: When multiple threads modify a fail-fast collection, a `ConcurrentModificationException` may be thrown. Even in a single-threaded context, such situations can occur, as previously mentioned. > -> 相关阅读:[什么是 fail-fast](https://www.cnblogs.com/54chensongxia/p/12470446.html) 。 +> Related reading: [What is fail-fast](https://www.cnblogs.com/54chensongxia/p/12470446.html). -Java8 开始,可以使用 `Collection#removeIf()`方法删除满足特定条件的元素,如 +Starting from Java 8, you can use the `Collection#removeIf()` method to remove elements that meet specific conditions, such as: ```java List list = new ArrayList<>(); for (int i = 1; i <= 10; ++i) { list.add(i); } -list.removeIf(filter -> filter % 2 == 0); /* 删除list中的所有偶数 */ +list.removeIf(filter -> filter % 2 == 0); /* Removes all even numbers from list */ System.out.println(list); /* [1, 3, 5, 7, 9] */ ``` -除了上面介绍的直接使用 `Iterator` 进行遍历操作之外,你还可以: +In addition to using `Iterator` for traversing, you can also: -- 使用普通的 for 循环 -- 使用 fail-safe 的集合类。`java.util`包下面的所有的集合类都是 fail-fast 的,而`java.util.concurrent`包下面的所有的类都是 fail-safe 的。 -- …… +- Use a standard for loop +- Use fail-safe collection classes. All collection classes in the `java.util` package are fail-fast, whereas all classes in the `java.util.concurrent` package are fail-safe. +- … -## 集合去重 +## Removing Duplicates from Collections -《阿里巴巴 Java 开发手册》的描述如下: +The description in the "Alibaba Java Development Manual" is as follows: -> **可以利用 `Set` 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 `List` 的 `contains()` 进行遍历去重或者判断包含操作。** +> **You can utilize the unique feature of `Set` to quickly remove duplicates from a collection, avoiding the use of `List`'s `contains()` for traversal removal or inclusion checks.** -这里我们以 `HashSet` 和 `ArrayList` 为例说明。 +Here we illustrate with `HashSet` and `ArrayList`. ```java -// Set 去重代码示例 +// Set deduplication code example public static Set removeDuplicateBySet(List data) { if (CollectionUtils.isEmpty(data)) { @@ -184,7 +184,7 @@ public static Set removeDuplicateBySet(List data) { return new HashSet<>(data); } -// List 去重代码示例 +// List deduplication code example public static List removeDuplicateByList(List data) { if (CollectionUtils.isEmpty(data)) { @@ -199,12 +199,11 @@ public static List removeDuplicateByList(List data) { } return result; } - ``` -两者的核心差别在于 `contains()` 方法的实现。 +The core difference between the two lies in the implementation of the `contains()` method. -`HashSet` 的 `contains()` 方法底部依赖的 `HashMap` 的 `containsKey()` 方法,时间复杂度接近于 O(1)(没有出现哈希冲突的时候为 O(1))。 +The `contains()` method of `HashSet` depends on the `containsKey()` method of `HashMap`, with a time complexity close to O(1) (when there are no hash collisions, it is O(1)). ```java private transient HashMap map; @@ -213,9 +212,9 @@ public boolean contains(Object o) { } ``` -我们有 N 个元素插入进 Set 中,那时间复杂度就接近是 O (n)。 +If we insert N elements into the Set, the time complexity approaches O(n). -`ArrayList` 的 `contains()` 方法是通过遍历所有元素的方法来做的,时间复杂度接近是 O(n)。 +The `contains()` method of `ArrayList` traverses all elements, with a time complexity close to O(n). ```java public boolean contains(Object o) { @@ -233,16 +232,15 @@ public int indexOf(Object o) { } return -1; } - ``` -## 集合转数组 +## Converting Collection to Array -《阿里巴巴 Java 开发手册》的描述如下: +The description in the "Alibaba Java Development Manual" is as follows: -> **使用集合转数组的方法,必须使用集合的 `toArray(T[] array)`,传入的是类型完全一致、长度为 0 的空数组。** +> **When converting a collection to an array, you must use the collection's `toArray(T[] array)`, passing a completely matching, length-zero empty array.** -`toArray(T[] array)` 方法的参数是一个泛型数组,如果 `toArray` 方法中没有传递任何参数的话返回的是 `Object`类 型数组。 +The parameter for `toArray(T[] array)` is a generic array. If no parameters are passed to the `toArray` method, it returns an array of type `Object`. ```java String [] s= new String[]{ @@ -250,80 +248,81 @@ String [] s= new String[]{ }; List list = Arrays.asList(s); Collections.reverse(list); -//没有指定类型的话会报错 +// Will throw an error if type is not specified s=list.toArray(new String[0]); ``` -由于 JVM 优化,`new String[0]`作为`Collection.toArray()`方法的参数现在使用更好,`new String[0]`就是起一个模板的作用,指定了返回数组的类型,0 是为了节省空间,因为它只是为了说明返回的类型。详见: +Due to JVM optimizations, using `new String[0]` as the parameter for `Collection.toArray()` is better now; `new String[0]` acts as a template, specifying the return type of the array, and 0 is for saving space as it is only to indicate the return type. See details: -## 数组转集合 +## Converting Array to Collection -《阿里巴巴 Java 开发手册》的描述如下: +The description in the "Alibaba Java Development Manual" is as follows: -> **使用工具类 `Arrays.asList()` 把数组转换成集合时,不能使用其修改集合相关的方法, 它的 `add/remove/clear` 方法会抛出 `UnsupportedOperationException` 异常。** +> **When using the utility class `Arrays.asList()` to convert an array into a collection, you should not use its modifying methods; its `add/remove/clear` methods will throw an `UnsupportedOperationException`.** -我在之前的一个项目中就遇到一个类似的坑。 +I encountered a similar pitfall in a previous project. -`Arrays.asList()`在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个 `List` 集合。 +`Arrays.asList()` is quite common in everyday development, allowing us to convert an array into a `List` collection. ```java String[] myArray = {"Apple", "Banana", "Orange"}; List myList = Arrays.asList(myArray); -//上面两个语句等价于下面一条语句 +// The above two statements are equivalent to the following one List myList = Arrays.asList("Apple","Banana", "Orange"); ``` -JDK 源码对于这个方法的说明: +The JDK source code describes this method as: ```java /** - *返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁, - * 与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。 + * Returns a fixed-size list backed by the specified array. This method serves as a bridge between array-based and + * collection-based APIs, to be used together with Collection.toArray(). The returned List is serializable and implements + * RandomAccess interface. */ public static List asList(T... a) { return new ArrayList<>(a); } ``` -下面我们来总结一下使用注意事项。 +Let's summarize the usage precautions. -**1、`Arrays.asList()`是泛型方法,传递的数组必须是对象数组,而不是基本类型。** +**1. `Arrays.asList()` is a generic method, and the passed array must be an object array, not a primitive type.** ```java int[] myArray = {1, 2, 3}; List myList = Arrays.asList(myArray); System.out.println(myList.size());//1 -System.out.println(myList.get(0));//数组地址值 -System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException +System.out.println(myList.get(0));//Array address value +System.out.println(myList.get(1));//Error: ArrayIndexOutOfBoundsException int[] array = (int[]) myList.get(0); System.out.println(array[0]);//1 ``` -当传入一个原生数据类型数组时,`Arrays.asList()` 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时 `List` 的唯一元素就是这个数组,这也就解释了上面的代码。 +When a primitive type array is passed, the real parameter obtained by `Arrays.asList()` is the array object itself rather than the elements in the array! At this point, the only element in the `List` is this array, which explains the above code. -我们使用包装类型数组就可以解决这个问题。 +Using a wrapper type array can solve this issue. ```java Integer[] myArray = {1, 2, 3}; ``` -**2、使用集合的修改方法: `add()`、`remove()`、`clear()`会抛出异常。** +**2. Using collection modification methods: `add()`, `remove()`, `clear()` will throw exceptions.** ```java List myList = Arrays.asList(1, 2, 3); -myList.add(4);//运行时报错:UnsupportedOperationException -myList.remove(1);//运行时报错:UnsupportedOperationException -myList.clear();//运行时报错:UnsupportedOperationException +myList.add(4);//Runtime error: UnsupportedOperationException +myList.remove(1);//Runtime error: UnsupportedOperationException +myList.clear();//Runtime error: UnsupportedOperationException ``` -`Arrays.asList()` 方法返回的并不是 `java.util.ArrayList` ,而是 `java.util.Arrays` 的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法。 +The `Arrays.asList()` method does not return a `java.util.ArrayList`, but rather an internal class of `java.util.Arrays`, which does not implement or override the collection modification methods. ```java List myList = Arrays.asList(1, 2, 3); System.out.println(myList.getClass());//class java.util.Arrays$ArrayList ``` -下图是 `java.util.Arrays$ArrayList` 的简易源码,我们可以看到这个类重写的方法有哪些。 +The following diagram shows a simplified source code of `java.util.Arrays$ArrayList`, and we can see which methods this class overrides. ```java private static class ArrayList extends AbstractList @@ -368,7 +367,7 @@ System.out.println(myList.getClass());//class java.util.Arrays$ArrayList } ``` -我们再看一下`java.util.AbstractList`的 `add/remove/clear` 方法就知道为什么会抛出 `UnsupportedOperationException` 了。 +Now looking at the `add/remove/clear` methods of `java.util.AbstractList` will explain why `UnsupportedOperationException` is thrown. ```java public E remove(int index) { @@ -394,12 +393,12 @@ protected void removeRange(int fromIndex, int toIndex) { } ``` -**那我们如何正确的将数组转换为 `ArrayList` ?** +**So how can we correctly convert an array to an `ArrayList`?** -1、手动实现工具类 +1. Manually implement a utility class ```java -//JDK1.5+ +// JDK1.5+ static List arrayToList(final T[] array) { final List l = new ArrayList(array.length); @@ -410,36 +409,36 @@ static List arrayToList(final T[] array) { } -Integer [] myArray = { 1, 2, 3 }; +Integer[] myArray = { 1, 2, 3 }; System.out.println(arrayToList(myArray).getClass());//class java.util.ArrayList ``` -2、最简便的方法 +2. The simplest method ```java List list = new ArrayList<>(Arrays.asList("a", "b", "c")) ``` -3、使用 Java8 的 `Stream`(推荐) +3. Using Java 8's `Stream` (recommended) ```java -Integer [] myArray = { 1, 2, 3 }; +Integer[] myArray = { 1, 2, 3 }; List myList = Arrays.stream(myArray).collect(Collectors.toList()); -//基本类型也可以实现转换(依赖boxed的装箱操作) -int [] myArray2 = { 1, 2, 3 }; +// Primitive types can also be converted (depending on boxing operations) +int[] myArray2 = { 1, 2, 3 }; List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList()); ``` -4、使用 Guava +4. Using Guava -对于不可变集合,你可以使用[`ImmutableList`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java)类及其[`of()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L101)与[`copyOf()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L225)工厂方法:(参数不能为空) +For immutable collections, you can use the [`ImmutableList`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java) class and its [`of()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L101) and [`copyOf()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L225) factory methods (parameters cannot be null): ```java List il = ImmutableList.of("string", "elements"); // from varargs List il = ImmutableList.copyOf(aStringArray); // from array ``` -对于可变集合,你可以使用[`Lists`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java)类及其[`newArrayList()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java#L87)工厂方法: +For mutable collections, you can use the [`Lists`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java) class and its [`newArrayList()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java#L87) factory method: ```java List l1 = Lists.newArrayList(anotherListOrCollection); // from collection @@ -447,14 +446,14 @@ List l2 = Lists.newArrayList(aStringArray); // from array List l3 = Lists.newArrayList("or", "string", "elements"); // from varargs ``` -5、使用 Apache Commons Collections +5. Using Apache Commons Collections ```java List list = new ArrayList(); CollectionUtils.addAll(list, str); ``` -6、 使用 Java9 的 `List.of()`方法 +6. Using Java 9's `List.of()` method ```java Integer[] array = {1, 2, 3}; diff --git a/docs/java/collection/java-collection-questions-01.md b/docs/java/collection/java-collection-questions-01.md index 417a2d10f53..237ac1f6ca6 100644 --- a/docs/java/collection/java-collection-questions-01.md +++ b/docs/java/collection/java-collection-questions-01.md @@ -1,591 +1,71 @@ --- -title: Java集合常见面试题总结(上) +title: Summary of Common Java Collection Interview Questions (Part 1) category: Java tag: - - Java集合 + - Java Collections head: - - - meta - - name: keywords - content: Collection,List,Set,Queue,Deque,PriorityQueue - - - meta - - name: description - content: Java集合常见知识点和面试题总结,希望对你有帮助! + - - meta + - name: keywords + content: Collection,List,Set,Queue,Deque,PriorityQueue + - - meta + - name: description + content: A summary of common knowledge points and interview questions about Java collections, hoping to be helpful to you! --- -## 集合概述 +## Overview of Collections -### Java 集合概览 +### Overview of Java Collections -Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 `Collection`接口,主要用于存放单一元素;另一个是 `Map` 接口,主要用于存放键值对。对于`Collection` 接口,下面又有三个主要的子接口:`List`、`Set` 、 `Queue`。 +Java collections, also known as containers, are primarily derived from two major interfaces: one is the `Collection` interface, which is mainly used to store single elements; the other is the `Map` interface, which is mainly used to store key-value pairs. For the `Collection` interface, there are three main sub-interfaces: `List`, `Set`, and `Queue`. -Java 集合框架如下图所示: +The Java collection framework is shown in the diagram below: -![Java 集合框架概览](https://oss.javaguide.cn/github/javaguide/java/collection/java-collection-hierarchy.png) +![Overview of Java Collection Framework](https://oss.javaguide.cn/github/javaguide/java/collection/java-collection-hierarchy.png) -注:图中只列举了主要的继承派生关系,并没有列举所有关系。比方省略了`AbstractList`, `NavigableSet`等抽象类以及其他的一些辅助类,如想深入了解,可自行查看源码。 +Note: The diagram only lists the main inheritance relationships and does not enumerate all relationships. For example, it omits abstract classes like `AbstractList`, `NavigableSet`, and other auxiliary classes. For a deeper understanding, you can refer to the source code. -### 说说 List, Set, Queue, Map 四者的区别? +### What are the differences between List, Set, Queue, and Map? -- `List`(对付顺序的好帮手): 存储的元素是有序的、可重复的。 -- `Set`(注重独一无二的性质): 存储的元素不可重复的。 -- `Queue`(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。 -- `Map`(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。 +- `List` (a good helper for order): The stored elements are ordered and can be duplicated. +- `Set` (emphasizing uniqueness): The stored elements cannot be duplicated. +- `Queue` (a call machine that implements queuing functionality): Determines the order based on specific queuing rules, and the stored elements are ordered and can be duplicated. +- `Map` (an expert in searching using keys): Stores key-value pairs (key-value), similar to the mathematical function y=f(x), where "x" represents the key and "y" represents the value. Keys are unordered and cannot be duplicated, while values are unordered and can be duplicated, with each key mapping to at most one value. -### 集合框架底层数据结构总结 +### Summary of Underlying Data Structures in the Collection Framework -先来看一下 `Collection` 接口下面的集合。 +First, let's look at the collections under the `Collection` interface. #### List -- `ArrayList`:`Object[]` 数组。详细可以查看:[ArrayList 源码分析](./arraylist-source-code.md)。 -- `Vector`:`Object[]` 数组。 -- `LinkedList`:双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)。详细可以查看:[LinkedList 源码分析](./linkedlist-source-code.md)。 +- `ArrayList`: An `Object[]` array. For details, see: [ArrayList Source Code Analysis](./arraylist-source-code.md). +- `Vector`: An `Object[]` array. +- `LinkedList`: A doubly linked list (previously a circular linked list before JDK1.6, removed in JDK1.7). For details, see: [LinkedList Source Code Analysis](./linkedlist-source-code.md). #### Set -- `HashSet`(无序,唯一): 基于 `HashMap` 实现的,底层采用 `HashMap` 来保存元素。 -- `LinkedHashSet`: `LinkedHashSet` 是 `HashSet` 的子类,并且其内部是通过 `LinkedHashMap` 来实现的。 -- `TreeSet`(有序,唯一): 红黑树(自平衡的排序二叉树)。 +- `HashSet` (unordered, unique): Implemented based on `HashMap`, using `HashMap` to store elements. +- `LinkedHashSet`: A subclass of `HashSet`, implemented internally using `LinkedHashMap`. +- `TreeSet` (ordered, unique): A red-black tree (self-balancing sorted binary tree). #### Queue -- `PriorityQueue`: `Object[]` 数组来实现小顶堆。详细可以查看:[PriorityQueue 源码分析](./priorityqueue-source-code.md)。 -- `DelayQueue`:`PriorityQueue`。详细可以查看:[DelayQueue 源码分析](./delayqueue-source-code.md)。 -- `ArrayDeque`: 可扩容动态双向数组。 +- `PriorityQueue`: Implemented using a min-heap with an `Object[]` array. For details, see: [PriorityQueue Source Code Analysis](./priorityqueue-source-code.md). +- `DelayQueue`: A `PriorityQueue`. For details, see: [DelayQueue Source Code Analysis](./delayqueue-source-code.md). +- `ArrayDeque`: A resizable dynamic array. -再来看看 `Map` 接口下面的集合。 +Now let's look at the collections under the `Map` interface. #### Map -- `HashMap`:JDK1.8 之前 `HashMap` 由数组+链表组成的,数组是 `HashMap` 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。详细可以查看:[HashMap 源码分析](./hashmap-source-code.md)。 -- `LinkedHashMap`:`LinkedHashMap` 继承自 `HashMap`,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,`LinkedHashMap` 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[LinkedHashMap 源码分析](./linkedhashmap-source-code.md) -- `Hashtable`:数组+链表组成的,数组是 `Hashtable` 的主体,链表则是主要为了解决哈希冲突而存在的。 -- `TreeMap`:红黑树(自平衡的排序二叉树)。 +- `HashMap`: Before JDK1.8, `HashMap` was composed of an array and a linked list, where the array was the main body and the linked list existed mainly to resolve hash collisions (using the "chaining method" to resolve conflicts). After JDK1.8, there were significant changes in resolving hash collisions. When the length of the linked list exceeds a threshold (default is 8), it converts the linked list to a red-black tree to reduce search time. For details, see: [HashMap Source Code Analysis](./hashmap-source-code.md). +- `LinkedHashMap`: Inherits from `HashMap`, so its underlying structure is still based on a chained hash structure composed of an array and a linked list or red-black tree. Additionally, `LinkedHashMap` adds a doubly linked list to maintain the insertion order of key-value pairs. It also implements access order logic through appropriate operations on the linked list. For details, see: [LinkedHashMap Source Code Analysis](./linkedhashmap-source-code.md). +- `Hashtable`: Composed of an array and a linked list, where the array is the main body and the linked list exists mainly to resolve hash collisions. +- `TreeMap`: A red-black tree (self-balancing sorted binary tree). -### 如何选用集合? +### How to Choose a Collection? -我们主要根据集合的特点来选择合适的集合。比如: - -- 我们需要根据键值获取到元素值时就选用 `Map` 接口下的集合,需要排序时选择 `TreeMap`,不需要排序时就选择 `HashMap`,需要保证线程安全就选用 `ConcurrentHashMap`。 -- 我们只需要存放元素值时,就选择实现`Collection` 接口的集合,需要保证元素唯一时选择实现 `Set` 接口的集合比如 `TreeSet` 或 `HashSet`,不需要就选择实现 `List` 接口的比如 `ArrayList` 或 `LinkedList`,然后再根据实现这些接口的集合的特点来选用。 - -### 为什么要使用集合? - -当我们需要存储一组类型相同的数据时,数组是最常用且最基本的容器之一。但是,使用数组存储对象存在一些不足之处,因为在实际开发中,存储的数据类型多种多样且数量不确定。这时,Java 集合就派上用场了。与数组相比,Java 集合提供了更灵活、更有效的方法来存储多个数据对象。Java 集合框架中的各种集合类和接口可以存储不同类型和数量的对象,同时还具有多样化的操作方式。相较于数组,Java 集合的优势在于它们的大小可变、支持泛型、具有内建算法等。总的来说,Java 集合提高了数据的存储和处理灵活性,可以更好地适应现代软件开发中多样化的数据需求,并支持高质量的代码编写。 - -## List - -### ArrayList 和 Array(数组)的区别? - -`ArrayList` 内部基于动态数组实现,比 `Array`(静态数组) 使用起来更加灵活: - -- `ArrayList`会根据实际存储的元素动态地扩容或缩容,而 `Array` 被创建之后就不能改变它的长度了。 -- `ArrayList` 允许你使用泛型来确保类型安全,`Array` 则不可以。 -- `ArrayList` 中只能存储对象。对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)。`Array` 可以直接存储基本类型数据,也可以存储对象。 -- `ArrayList` 支持插入、删除、遍历等常见操作,并且提供了丰富的 API 操作方法,比如 `add()`、`remove()`等。`Array` 只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力。 -- `ArrayList`创建时不需要指定大小,而`Array`创建时必须指定大小。 - -下面是二者使用的简单对比: - -`Array`: - -```java - // 初始化一个 String 类型的数组 - String[] stringArr = new String[]{"hello", "world", "!"}; - // 修改数组元素的值 - stringArr[0] = "goodbye"; - System.out.println(Arrays.toString(stringArr));// [goodbye, world, !] - // 删除数组中的元素,需要手动移动后面的元素 - for (int i = 0; i < stringArr.length - 1; i++) { - stringArr[i] = stringArr[i + 1]; - } - stringArr[stringArr.length - 1] = null; - System.out.println(Arrays.toString(stringArr));// [world, !, null] -``` - -`ArrayList` : - -```java -// 初始化一个 String 类型的 ArrayList - ArrayList stringList = new ArrayList<>(Arrays.asList("hello", "world", "!")); -// 添加元素到 ArrayList 中 - stringList.add("goodbye"); - System.out.println(stringList);// [hello, world, !, goodbye] - // 修改 ArrayList 中的元素 - stringList.set(0, "hi"); - System.out.println(stringList);// [hi, world, !, goodbye] - // 删除 ArrayList 中的元素 - stringList.remove(0); - System.out.println(stringList); // [world, !, goodbye] -``` - -### ArrayList 和 Vector 的区别?(了解即可) - -- `ArrayList` 是 `List` 的主要实现类,底层使用 `Object[]`存储,适用于频繁的查找工作,线程不安全 。 -- `Vector` 是 `List` 的古老实现类,底层使用`Object[]` 存储,线程安全。 - -### Vector 和 Stack 的区别?(了解即可) - -- `Vector` 和 `Stack` 两者都是线程安全的,都是使用 `synchronized` 关键字进行同步处理。 -- `Stack` 继承自 `Vector`,是一个后进先出的栈,而 `Vector` 是一个列表。 - -随着 Java 并发编程的发展,`Vector` 和 `Stack` 已经被淘汰,推荐使用并发集合类(例如 `ConcurrentHashMap`、`CopyOnWriteArrayList` 等)或者手动实现线程安全的方法来提供安全的多线程操作支持。 - -### ArrayList 可以添加 null 值吗? - -`ArrayList` 中可以存储任何类型的对象,包括 `null` 值。不过,不建议向`ArrayList` 中添加 `null` 值, `null` 值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常。 - -示例代码: - -```java -ArrayList listOfStrings = new ArrayList<>(); -listOfStrings.add(null); -listOfStrings.add("java"); -System.out.println(listOfStrings); -``` - -输出: - -```plain -[null, java] -``` - -### ArrayList 插入和删除元素的时间复杂度? - -对于插入: - -- 头部插入:由于需要将所有元素都依次向后移动一个位置,因此时间复杂度是 O(n)。 -- 尾部插入:当 `ArrayList` 的容量未达到极限时,往列表末尾插入元素的时间复杂度是 O(1),因为它只需要在数组末尾添加一个元素即可;当容量已达到极限并且需要扩容时,则需要执行一次 O(n) 的操作将原数组复制到新的更大的数组中,然后再执行 O(1) 的操作添加元素。 -- 指定位置插入:需要将目标位置之后的所有元素都向后移动一个位置,然后再把新元素放入指定位置。这个过程需要移动平均 n/2 个元素,因此时间复杂度为 O(n)。 - -对于删除: - -- 头部删除:由于需要将所有元素依次向前移动一个位置,因此时间复杂度是 O(n)。 -- 尾部删除:当删除的元素位于列表末尾时,时间复杂度为 O(1)。 -- 指定位置删除:需要将目标元素之后的所有元素向前移动一个位置以填补被删除的空白位置,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。 - -这里简单列举一个例子: - -```java -// ArrayList的底层数组大小为10,此时存储了7个元素 -+---+---+---+---+---+---+---+---+---+---+ -| 1 | 2 | 3 | 4 | 5 | 6 | 7 | | | | -+---+---+---+---+---+---+---+---+---+---+ - 0 1 2 3 4 5 6 7 8 9 -// 在索引为1的位置插入一个元素8,该元素后面的所有元素都要向右移动一位 -+---+---+---+---+---+---+---+---+---+---+ -| 1 | 8 | 2 | 3 | 4 | 5 | 6 | 7 | | | -+---+---+---+---+---+---+---+---+---+---+ - 0 1 2 3 4 5 6 7 8 9 -// 删除索引为1的位置的元素,该元素后面的所有元素都要向左移动一位 -+---+---+---+---+---+---+---+---+---+---+ -| 1 | 2 | 3 | 4 | 5 | 6 | 7 | | | | -+---+---+---+---+---+---+---+---+---+---+ - 0 1 2 3 4 5 6 7 8 9 -``` - -### LinkedList 插入和删除元素的时间复杂度? - -- 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。 -- 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。 -- 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,不过由于有头尾指针,可以从较近的指针出发,因此需要遍历平均 n/4 个元素,时间复杂度为 O(n)。 - -这里简单列举一个例子:假如我们要删除节点 9 的话,需要先遍历链表找到该节点。然后,再执行相应节点指针指向的更改,具体的源码可以参考:[LinkedList 源码分析](./linkedlist-source-code.md) 。 - -![unlink 方法逻辑](https://oss.javaguide.cn/github/javaguide/java/collection/linkedlist-unlink.jpg) - -### LinkedList 为什么不能实现 RandomAccess 接口? - -`RandomAccess` 是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。由于 `LinkedList` 底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 `RandomAccess` 接口。 - -### ArrayList 与 LinkedList 区别? - -- **是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全; -- **底层数据结构:** `ArrayList` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) -- **插入和删除是否受元素位置的影响:** - - `ArrayList` 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行`add(E e)`方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element)`),时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 - - `LinkedList` 采用链表存储,所以在头尾插入或者删除元素不受元素位置的影响(`add(E e)`、`addFirst(E e)`、`addLast(E e)`、`removeFirst()`、 `removeLast()`),时间复杂度为 O(1),如果是要在指定位置 `i` 插入和删除元素的话(`add(int index, E element)`,`remove(Object o)`,`remove(int index)`), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入和删除。 -- **是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList`(实现了 `RandomAccess` 接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 -- **内存空间占用:** `ArrayList` 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。 - -我们在项目中一般是不会使用到 `LinkedList` 的,需要用到 `LinkedList` 的场景几乎都可以使用 `ArrayList` 来代替,并且,性能通常会更好!就连 `LinkedList` 的作者约书亚 · 布洛克(Josh Bloch)自己都说从来不会使用 `LinkedList` 。 - -![](https://oss.javaguide.cn/github/javaguide/redisimage-20220412110853807.png) - -另外,不要下意识地认为 `LinkedList` 作为链表就最适合元素增删的场景。我在上面也说了,`LinkedList` 仅仅在头尾插入或者删除元素的时候时间复杂度近似 O(1),其他情况增删元素的平均时间复杂度都是 O(n) 。 - -#### 补充内容: 双向链表和双向循环链表 - -**双向链表:** 包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。 - -![双向链表](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/bidirectional-linkedlist.png) - -**双向循环链表:** 最后一个节点的 next 指向 head,而 head 的 prev 指向最后一个节点,构成一个环。 - -![双向循环链表](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/bidirectional-circular-linkedlist.png) - -#### 补充内容:RandomAccess 接口 - -```java -public interface RandomAccess { -} -``` - -查看源码我们发现实际上 `RandomAccess` 接口中什么都没有定义。所以,在我看来 `RandomAccess` 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。 - -在 `binarySearch()` 方法中,它要判断传入的 list 是否 `RandomAccess` 的实例,如果是,调用`indexedBinarySearch()`方法,如果不是,那么调用`iteratorBinarySearch()`方法 - -```java - public static - int binarySearch(List> list, T key) { - if (list instanceof RandomAccess || list.size() Fail-fast systems are designed to immediately stop functioning upon encountering an unexpected condition. This immediate failure helps to catch errors early, making debugging more straightforward. - -快速失败的思想即针对可能发生的异常进行提前表明故障并停止运行,通过尽早的发现和停止错误,降低故障系统级联的风险。 - -在`java.util`包下的大部分集合是不支持线程安全的,为了能够提前发现并发操作导致线程安全风险,提出通过维护一个`modCount`记录修改的次数,迭代期间通过比对预期修改次数`expectedModCount`和`modCount`是否一致来判断是否存在并发操作,从而实现快速失败,由此保证在避免在异常时执行非必要的复杂代码。 - -对应的我们给出下面这样一段在示例,我们首先插入`100`个操作元素,一个线程迭代元素,一个线程删除元素,最终输出结果如愿抛出`ConcurrentModificationException`: - -```java -// 使用线程安全的 CopyOnWriteArrayList 避免 ConcurrentModificationException -List list = new CopyOnWriteArrayList<>(); -CountDownLatch countDownLatch = new CountDownLatch(2); - -// 添加元素 -for (int i = 0; i < 100; i++) { - list.add(i); -} - -Thread t1 = new Thread(() -> { - // 迭代元素 (注意:Integer 是不可变的,这里的 i++ 不会修改 list 中的值) - for (Integer i : list) { - i++; // 这行代码实际上没有修改list中的元素 - } - countDownLatch.countDown(); -}); - -Thread t2 = new Thread(() -> { - System.out.println("删除元素1"); - list.remove(Integer.valueOf(1)); // 使用 Integer.valueOf(1) 删除指定值的对象 - countDownLatch.countDown(); -}); - -t1.start(); -t2.start(); -countDownLatch.await(); -``` - -我们在初始化时插入了`100`个元素,此时对应的修改`modCount`次数为`100`,随后线程 2 在线程 1 迭代期间进行元素删除操作,此时对应的`modCount`就变为`101`。 -线程 1 在随后`foreach`第 2 轮循环发现`modCount` 为`101`,与预期的`expectedModCount(值为100因为初始化插入了元素100个)`不等,判定为并发操作异常,于是便快速失败,抛出`ConcurrentModificationException`: - -![](https://oss.javaguide.cn/github/javaguide/java/collection/fail-fast-and-fail-safe-insert-100-values.png) - -对此我们也给出`for`循环底层迭代器获取下一个元素时的`next`方法,可以看到其内部的`checkForComodification`具有针对修改次数比对的逻辑: - -```java - public E next() { - //检查是否存在并发修改 - checkForComodification(); - //...... - //返回下一个元素 - return (E) elementData[lastRet = i]; - } - -final void checkForComodification() { - //当前循环遍历次数和预期修改次数不一致时,就会抛出ConcurrentModificationException - if (modCount != expectedModCount) - throw new ConcurrentModificationException(); - } - -``` - -而`fail-safe`也就是安全失败的含义,它旨在即使面对意外情况也能恢复并继续运行,这使得它特别适用于不确定或者不稳定的环境: - -> Fail-safe systems take a different approach, aiming to recover and continue even in the face of unexpected conditions. This makes them particularly suited for uncertain or volatile environments. - -该思想常运用于并发容器,最经典的实现就是`CopyOnWriteArrayList`的实现,通过写时复制的思想保证在进行修改操作时复制出一份快照,基于这份快照完成添加或者删除操作后,将`CopyOnWriteArrayList`底层的数组引用指向这个新的数组空间,由此避免迭代时被并发修改所干扰所导致并发操作安全问题,当然这种做法也存缺点即进行遍历操作时无法获得实时结果: - -![](https://oss.javaguide.cn/github/javaguide/java/collection/fail-fast-and-fail-safe-copyonwritearraylist.png) - -对应我们也给出`CopyOnWriteArrayList`实现`fail-safe`的核心代码,可以看到它的实现就是通过`getArray`获取数组引用然后通过`Arrays.copyOf`得到一个数组的快照,基于这个快照完成添加操作后,修改底层`array`变量指向的引用地址由此完成写时复制: - -```java -public boolean add(E e) { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - //获取原有数组 - Object[] elements = getArray(); - int len = elements.length; - //基于原有数组复制出一份内存快照 - Object[] newElements = Arrays.copyOf(elements, len + 1); - //进行添加操作 - newElements[len] = e; - //array指向新的数组 - setArray(newElements); - return true; - } finally { - lock.unlock(); - } - } -``` - -## Set - -### Comparable 和 Comparator 的区别 - -`Comparable` 接口和 `Comparator` 接口都是 Java 中用于排序的接口,它们在实现类对象之间比较大小、排序等方面发挥了重要作用: - -- `Comparable` 接口实际上是出自`java.lang`包 它有一个 `compareTo(Object obj)`方法用来排序 -- `Comparator`接口实际上是出自 `java.util` 包它有一个`compare(Object obj1, Object obj2)`方法用来排序 - -一般我们需要对一个集合使用自定义排序时,我们就要重写`compareTo()`方法或`compare()`方法,当我们需要对某一个集合实现两种排序方式,比如一个 `song` 对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写`compareTo()`方法和使用自制的`Comparator`方法或者以两个 `Comparator` 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 `Collections.sort()`. - -#### Comparator 定制排序 - -```java -ArrayList arrayList = new ArrayList(); -arrayList.add(-1); -arrayList.add(3); -arrayList.add(3); -arrayList.add(-5); -arrayList.add(7); -arrayList.add(4); -arrayList.add(-9); -arrayList.add(-7); -System.out.println("原始数组:"); -System.out.println(arrayList); -// void reverse(List list):反转 -Collections.reverse(arrayList); -System.out.println("Collections.reverse(arrayList):"); -System.out.println(arrayList); - -// void sort(List list),按自然排序的升序排序 -Collections.sort(arrayList); -System.out.println("Collections.sort(arrayList):"); -System.out.println(arrayList); -// 定制排序的用法 -Collections.sort(arrayList, new Comparator() { - @Override - public int compare(Integer o1, Integer o2) { - return o2.compareTo(o1); - } -}); -System.out.println("定制排序后:"); -System.out.println(arrayList); -``` - -Output: - -```plain -原始数组: -[-1, 3, 3, -5, 7, 4, -9, -7] -Collections.reverse(arrayList): -[-7, -9, 4, 7, -5, 3, 3, -1] -Collections.sort(arrayList): -[-9, -7, -5, -1, 3, 3, 4, 7] -定制排序后: -[7, 4, 3, 3, -1, -5, -7, -9] -``` - -#### 重写 compareTo 方法实现按年龄来排序 - -```java -// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列 -// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他 -// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了 -public class Person implements Comparable { - private String name; - private int age; - - public Person(String name, int age) { - super(); - this.name = name; - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - /** - * T重写compareTo方法实现按年龄来排序 - */ - @Override - public int compareTo(Person o) { - if (this.age > o.getAge()) { - return 1; - } - if (this.age < o.getAge()) { - return -1; - } - return 0; - } -} - -``` - -```java - public static void main(String[] args) { - TreeMap pdata = new TreeMap(); - pdata.put(new Person("张三", 30), "zhangsan"); - pdata.put(new Person("李四", 20), "lisi"); - pdata.put(new Person("王五", 10), "wangwu"); - pdata.put(new Person("小红", 5), "xiaohong"); - // 得到key的值的同时得到key所对应的值 - Set keys = pdata.keySet(); - for (Person key : keys) { - System.out.println(key.getAge() + "-" + key.getName()); - - } - } -``` - -Output: - -```plain -5-小红 -10-王五 -20-李四 -30-张三 -``` - -### 无序性和不可重复性的含义是什么 - -- 无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。 -- 不可重复性是指添加的元素按照 `equals()` 判断时 ,返回 false,需要同时重写 `equals()` 方法和 `hashCode()` 方法。 - -### 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同 - -- `HashSet`、`LinkedHashSet` 和 `TreeSet` 都是 `Set` 接口的实现类,都能保证元素唯一,并且都不是线程安全的。 -- `HashSet`、`LinkedHashSet` 和 `TreeSet` 的主要区别在于底层数据结构不同。`HashSet` 的底层数据结构是哈希表(基于 `HashMap` 实现)。`LinkedHashSet` 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。`TreeSet` 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。 -- 底层数据结构不同又导致这三者的应用场景不同。`HashSet` 用于不需要保证元素插入和取出顺序的场景,`LinkedHashSet` 用于保证元素的插入和取出顺序满足 FIFO 的场景,`TreeSet` 用于支持对元素自定义排序规则的场景。 - -## Queue - -### Queue 与 Deque 的区别 - -`Queue` 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 **先进先出(FIFO)** 规则。 - -`Queue` 扩展了 `Collection` 的接口,根据 **因为容量问题而导致操作失败后处理方式的不同** 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。 - -| `Queue` 接口 | 抛出异常 | 返回特殊值 | -| ------------ | --------- | ---------- | -| 插入队尾 | add(E e) | offer(E e) | -| 删除队首 | remove() | poll() | -| 查询队首元素 | element() | peek() | - -`Deque` 是双端队列,在队列的两端均可以插入或删除元素。 - -`Deque` 扩展了 `Queue` 的接口, 增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类: - -| `Deque` 接口 | 抛出异常 | 返回特殊值 | -| ------------ | ------------- | --------------- | -| 插入队首 | addFirst(E e) | offerFirst(E e) | -| 插入队尾 | addLast(E e) | offerLast(E e) | -| 删除队首 | removeFirst() | pollFirst() | -| 删除队尾 | removeLast() | pollLast() | -| 查询队首元素 | getFirst() | peekFirst() | -| 查询队尾元素 | getLast() | peekLast() | - -事实上,`Deque` 还提供有 `push()` 和 `pop()` 等其他方法,可用于模拟栈。 - -### ArrayDeque 与 LinkedList 的区别 - -`ArrayDeque` 和 `LinkedList` 都实现了 `Deque` 接口,两者都具有队列的功能,但两者有什么区别呢? - -- `ArrayDeque` 是基于可变长的数组和双指针来实现,而 `LinkedList` 则通过链表来实现。 - -- `ArrayDeque` 不支持存储 `NULL` 数据,但 `LinkedList` 支持。 - -- `ArrayDeque` 是在 JDK1.6 才被引入的,而`LinkedList` 早在 JDK1.2 时就已经存在。 - -- `ArrayDeque` 插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 `LinkedList` 不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。 - -从性能的角度上,选用 `ArrayDeque` 来实现队列要比 `LinkedList` 更好。此外,`ArrayDeque` 也可以用于实现栈。 - -### 说一说 PriorityQueue - -`PriorityQueue` 是在 JDK1.5 中被引入的, 其与 `Queue` 的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。 - -这里列举其相关的一些要点: - -- `PriorityQueue` 利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据 -- `PriorityQueue` 通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。 -- `PriorityQueue` 是非线程安全的,且不支持存储 `NULL` 和 `non-comparable` 的对象。 -- `PriorityQueue` 默认是小顶堆,但可以接收一个 `Comparator` 作为构造参数,从而来自定义元素优先级的先后。 - -`PriorityQueue` 在面试中可能更多的会出现在手撕算法的时候,典型例题包括堆排序、求第 K 大的数、带权图的遍历等,所以需要会熟练使用才行。 - -### 什么是 BlockingQueue? - -`BlockingQueue` (阻塞队列)是一个接口,继承自 `Queue`。`BlockingQueue`阻塞的原因是其支持当队列没有元素时一直阻塞,直到有元素;还支持如果队列已满,一直等到队列可以放入新元素时再放入。 - -```java -public interface BlockingQueue extends Queue { - // ... -} -``` - -`BlockingQueue` 常用于生产者-消费者模型中,生产者线程会向队列中添加数据,而消费者线程会从队列中取出数据进行处理。 - -![BlockingQueue](https://oss.javaguide.cn/github/javaguide/java/collection/blocking-queue.png) - -### BlockingQueue 的实现类有哪些? - -![BlockingQueue 的实现类](https://oss.javaguide.cn/github/javaguide/java/collection/blocking-queue-hierarchy.png) - -Java 中常用的阻塞队列实现类有以下几种: - -1. `ArrayBlockingQueue`:使用数组实现的有界阻塞队列。在创建时需要指定容量大小,并支持公平和非公平两种方式的锁访问机制。 -2. `LinkedBlockingQueue`:使用单向链表实现的可选有界阻塞队列。在创建时可以指定容量大小,如果不指定则默认为`Integer.MAX_VALUE`。和`ArrayBlockingQueue`不同的是, 它仅支持非公平的锁访问机制。 -3. `PriorityBlockingQueue`:支持优先级排序的无界阻塞队列。元素必须实现`Comparable`接口或者在构造函数中传入`Comparator`对象,并且不能插入 null 元素。 -4. `SynchronousQueue`:同步队列,是一种不存储元素的阻塞队列。每个插入操作都必须等待对应的删除操作,反之删除操作也必须等待插入操作。因此,`SynchronousQueue`通常用于线程之间的直接传递数据。 -5. `DelayQueue`:延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队。 -6. …… - -日常开发中,这些队列使用的其实都不多,了解即可。 - -### ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别? - -`ArrayBlockingQueue` 和 `LinkedBlockingQueue` 是 Java 并发包中常用的两种阻塞队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别: - -- 底层实现:`ArrayBlockingQueue` 基于数组实现,而 `LinkedBlockingQueue` 基于链表实现。 -- 是否有界:`ArrayBlockingQueue` 是有界队列,必须在创建时指定容量大小。`LinkedBlockingQueue` 创建时可以不指定容量大小,默认是`Integer.MAX_VALUE`,也就是无界的。但也可以指定队列大小,从而成为有界的。 -- 锁是否分离: `ArrayBlockingQueue`中的锁是没有分离的,即生产和消费用的是同一个锁;`LinkedBlockingQueue`中的锁是分离的,即生产用的是`putLock`,消费是`takeLock`,这样可以防止生产者和消费者线程之间的锁争夺。 -- 内存占用:`ArrayBlockingQueue` 需要提前分配数组内存,而 `LinkedBlockingQueue` 则是动态分配链表节点内存。这意味着,`ArrayBlockingQueue` 在创建时就会占用一定的内存空间,且往往申请的内存比实际所用的内存更大,而`LinkedBlockingQueue` 则是根据元素的增加而逐渐占用内存空间。 - - +We mainly choose the appropriate collection based on the diff --git a/docs/java/collection/java-collection-questions-02.md b/docs/java/collection/java-collection-questions-02.md index 94eafcf9825..0da4f46032b 100644 --- a/docs/java/collection/java-collection-questions-02.md +++ b/docs/java/collection/java-collection-questions-02.md @@ -1,31 +1,31 @@ --- -title: Java集合常见面试题总结(下) +title: Summary of Common Java Collection Interview Questions (Part 2) category: Java tag: - - Java集合 + - Java Collections head: - - - meta - - name: keywords - content: HashMap,ConcurrentHashMap,Hashtable,List,Set - - - meta - - name: description - content: Java集合常见知识点和面试题总结,希望对你有帮助! + - - meta + - name: keywords + content: HashMap, ConcurrentHashMap, Hashtable, List, Set + - - meta + - name: description + content: A summary of common knowledge points and interview questions about Java collections, hoping to be helpful to you! --- -## Map(重要) +## Map (Important) -### HashMap 和 Hashtable 的区别 +### Differences Between HashMap and Hashtable -- **线程是否安全:** `HashMap` 是非线程安全的,`Hashtable` 是线程安全的,因为 `Hashtable` 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 `ConcurrentHashMap` 吧!); -- **效率:** 因为线程安全的问题,`HashMap` 要比 `Hashtable` 效率高一点。另外,`Hashtable` 基本被淘汰,不要在代码中使用它; -- **对 Null key 和 Null value 的支持:** `HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 `NullPointerException`。 -- **初始容量大小和每次扩充容量大小的不同:** ① 创建时如果不指定容量初始值,`Hashtable` 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。`HashMap` 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 `Hashtable` 会直接使用你给定的大小,而 `HashMap` 会将其扩充为 2 的幂次方大小(`HashMap` 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 `HashMap` 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 -- **底层数据结构:** JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。`Hashtable` 没有这样的机制。 -- **哈希函数的实现**:`HashMap` 对哈希值进行了高位和低位的混合扰动处理以减少冲突,而 `Hashtable` 直接使用键的 `hashCode()` 值。 +- **Thread Safety:** `HashMap` is not thread-safe, while `Hashtable` is thread-safe because most of its internal methods are synchronized. (If you need to ensure thread safety, use `ConcurrentHashMap`!) +- **Efficiency:** Due to thread safety issues, `HashMap` is generally more efficient than `Hashtable`. Additionally, `Hashtable` is largely obsolete and should not be used in code. +- **Support for Null Keys and Values:** `HashMap` can store null keys and values, but only one null key is allowed, while multiple null values can be stored. `Hashtable` does not allow null keys or values, and will throw a `NullPointerException` if attempted. +- **Initial Capacity and Growth Size Differences:** ① If no initial capacity is specified during creation, `Hashtable` defaults to an initial size of 11, and each subsequent growth increases the capacity to 2n+1. `HashMap` defaults to an initial size of 16, and each subsequent growth doubles the capacity. ② If an initial capacity is specified, `Hashtable` uses that size directly, while `HashMap` expands it to the next power of 2 (the `tableSizeFor()` method in `HashMap` ensures this, and the source code is provided below). This means `HashMap` always uses powers of 2 for the hash table size, which will be explained later. +- **Underlying Data Structure:** After JDK 1.8, `HashMap` has undergone significant changes in handling hash collisions. When the length of a linked list exceeds a threshold (default is 8), it converts the list into a red-black tree (before converting to a red-black tree, it checks if the current array length is less than 64; if so, it opts for array expansion instead of conversion to a red-black tree) to reduce search time (this process will be analyzed in conjunction with the source code later). `Hashtable` does not have such a mechanism. +- **Hash Function Implementation:** `HashMap` applies a high and low bit mixing disturbance to the hash value to reduce collisions, while `Hashtable` directly uses the key's `hashCode()` value. -**`HashMap` 中带有初始容量的构造函数:** +**Constructor of `HashMap` with Initial Capacity:** ```java public HashMap(int initialCapacity, float loadFactor) { @@ -45,7 +45,7 @@ head: } ``` -下面这个方法保证了 `HashMap` 总是使用 2 的幂作为哈希表的大小。 +The following method ensures that `HashMap` always uses powers of 2 for the hash table size. ```java /** @@ -62,599 +62,13 @@ static final int tableSizeFor(int cap) { } ``` -### HashMap 和 HashSet 区别 +### Differences Between HashMap and HashSet -如果你看过 `HashSet` 源码的话就应该知道:`HashSet` 底层就是基于 `HashMap` 实现的。(`HashSet` 的源码非常非常少,因为除了 `clone()`、`writeObject()`、`readObject()`是 `HashSet` 自己不得不实现之外,其他方法都是直接调用 `HashMap` 中的方法。 +If you have looked at the source code of `HashSet`, you should know that `HashSet` is implemented based on `HashMap`. (The source code of `HashSet` is very minimal because, apart from `clone()`, `writeObject()`, and `readObject()`, which `HashSet` must implement, all other methods directly call methods from `HashMap`.) -| `HashMap` | `HashSet` | -| :------------------------------------: | :----------------------------------------------------------------------------------------------------------------------: | -| 实现了 `Map` 接口 | 实现 `Set` 接口 | -| 存储键值对 | 仅存储对象 | -| 调用 `put()`向 map 中添加元素 | 调用 `add()`方法向 `Set` 中添加元素 | -| `HashMap` 使用键(Key)计算 `hashcode` | `HashSet` 使用成员对象来计算 `hashcode` 值,对于两个对象来说 `hashcode` 可能相同,所以`equals()`方法用来判断对象的相等性 | - -### HashMap 和 TreeMap 区别 - -`TreeMap` 和`HashMap` 都继承自`AbstractMap` ,但是需要注意的是`TreeMap`它还实现了`NavigableMap`接口和`SortedMap` 接口。 - -![TreeMap 继承关系图](https://oss.javaguide.cn/github/javaguide/java/collection/treemap_hierarchy.png) - -实现 `NavigableMap` 接口让 `TreeMap` 有了对集合内元素的搜索的能力。 - -`NavigableMap` 接口提供了丰富的方法来探索和操作键值对: - -1. **定向搜索**: `ceilingEntry()`, `floorEntry()`, `higherEntry()`和 `lowerEntry()` 等方法可以用于定位大于等于、小于等于、严格大于、严格小于给定键的最接近的键值对。 -2. **子集操作**: `subMap()`, `headMap()`和 `tailMap()` 方法可以高效地创建原集合的子集视图,而无需复制整个集合。 -3. **逆序视图**:`descendingMap()` 方法返回一个逆序的 `NavigableMap` 视图,使得可以反向迭代整个 `TreeMap`。 -4. **边界操作**: `firstEntry()`, `lastEntry()`, `pollFirstEntry()`和 `pollLastEntry()` 等方法可以方便地访问和移除元素。 - -这些方法都是基于红黑树数据结构的属性实现的,红黑树保持平衡状态,从而保证了搜索操作的时间复杂度为 O(log n),这让 `TreeMap` 成为了处理有序集合搜索问题的强大工具。 - -实现`SortedMap`接口让 `TreeMap` 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。示例代码如下: - -```java -/** - * @author shuang.kou - * @createTime 2020年06月15日 17:02:00 - */ -public class Person { - private Integer age; - - public Person(Integer age) { - this.age = age; - } - - public Integer getAge() { - return age; - } - - - public static void main(String[] args) { - TreeMap treeMap = new TreeMap<>(new Comparator() { - @Override - public int compare(Person person1, Person person2) { - int num = person1.getAge() - person2.getAge(); - return Integer.compare(num, 0); - } - }); - treeMap.put(new Person(3), "person1"); - treeMap.put(new Person(18), "person2"); - treeMap.put(new Person(35), "person3"); - treeMap.put(new Person(16), "person4"); - treeMap.entrySet().stream().forEach(personStringEntry -> { - System.out.println(personStringEntry.getValue()); - }); - } -} -``` - -输出: - -```plain -person1 -person4 -person2 -person3 -``` - -可以看出,`TreeMap` 中的元素已经是按照 `Person` 的 age 字段的升序来排列了。 - -上面,我们是通过传入匿名内部类的方式实现的,你可以将代码替换成 Lambda 表达式实现的方式: - -```java -TreeMap treeMap = new TreeMap<>((person1, person2) -> { - int num = person1.getAge() - person2.getAge(); - return Integer.compare(num, 0); -}); -``` - -**综上,相比于`HashMap`来说, `TreeMap` 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。** - -### HashSet 如何检查重复? - -以下内容摘自我的 Java 启蒙书《Head first java》第二版: - -> 当你把对象加入`HashSet`时,`HashSet` 会先计算对象的`hashcode`值来判断对象加入的位置,同时也会与其他加入的对象的 `hashcode` 值作比较,如果没有相符的 `hashcode`,`HashSet` 会假设对象没有重复出现。但是如果发现有相同 `hashcode` 值的对象,这时会调用`equals()`方法来检查 `hashcode` 相等的对象是否真的相同。如果两者相同,`HashSet` 就不会让加入操作成功。 - -在 JDK1.8 中,`HashSet`的`add()`方法只是简单的调用了`HashMap`的`put()`方法,并且判断了一下返回值以确保是否有重复元素。直接看一下`HashSet`中的源码: - -```java -// Returns: true if this set did not already contain the specified element -// 返回值:当 set 中没有包含 add 的元素时返回真 -public boolean add(E e) { - return map.put(e, PRESENT)==null; -} -``` - -而在`HashMap`的`putVal()`方法中也能看到如下说明: - -```java -// Returns : previous value, or null if none -// 返回值:如果插入位置没有元素返回null,否则返回上一个元素 -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, - boolean evict) { -... -} -``` - -也就是说,在 JDK1.8 中,实际上无论`HashSet`中是否已经存在了某元素,`HashSet`都会直接插入,只是会在`add()`方法的返回值处告诉我们插入前是否存在相同元素。 - -### HashMap 的底层实现 - -#### JDK1.8 之前 - -JDK1.8 之前 `HashMap` 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。HashMap 通过 key 的 `hashcode` 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。 - -`HashMap` 中的扰动函数(`hash` 方法)是用来优化哈希值的分布。通过对原始的 `hashCode()` 进行额外处理,扰动函数可以减小由于糟糕的 `hashCode()` 实现导致的碰撞,从而提高数据的分布均匀性。 - -**JDK 1.8 HashMap 的 hash 方法源码:** - -JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 - -```java - static final int hash(Object key) { - int h; - // key.hashCode():返回散列值也就是hashcode - // ^:按位异或 - // >>>:无符号右移,忽略符号位,空位都以0补齐 - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - } -``` - -对比一下 JDK1.7 的 HashMap 的 hash 方法源码. - -```java -static int hash(int h) { - // This function ensures that hashCodes that differ only by - // constant multiples at each bit position have a bounded - // number of collisions (approximately 8 at default load factor). - - h ^= (h >>> 20) ^ (h >>> 12); - return h ^ (h >>> 7) ^ (h >>> 4); -} -``` - -相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。 - -所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 - -![jdk1.8 之前的内部结构-HashMap](https://oss.javaguide.cn/github/javaguide/java/collection/jdk1.7_hashmap.png) - -#### JDK1.8 之后 - -相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树。 - -这样做的目的是减少搜索时间:链表的查询效率为 O(n)(n 是链表的长度),红黑树是一种自平衡二叉搜索树,其查询效率为 O(log n)。当链表较短时,O(n) 和 O(log n) 的性能差异不明显。但当链表变长时,查询性能会显著下降。 - -![jdk1.8之后的内部结构-HashMap](https://oss.javaguide.cn/github/javaguide/java/collection/jdk1.8_hashmap.png) - -**为什么优先扩容而非直接转为红黑树?** - -数组扩容能减少哈希冲突的发生概率(即将元素重新分散到新的、更大的数组中),这在多数情况下比直接转换为红黑树更高效。 - -红黑树需要保持自平衡,维护成本较高。并且,过早引入红黑树反而会增加复杂度。 - -**为什么选择阈值 8 和 64?** - -1. 泊松分布表明,链表长度达到 8 的概率极低(小于千万分之一)。在绝大多数情况下,链表长度都不会超过 8。阈值设置为 8,可以保证性能和空间效率的平衡。 -2. 数组长度阈值 64 同样是经过实践验证的经验值。在小数组中扩容成本低,优先扩容可以避免过早引入红黑树。数组大小达到 64 时,冲突概率较高,此时红黑树的性能优势开始显现。 - -> TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 - -我们来结合源码分析一下 `HashMap` 链表到红黑树的转换。 - -**1、 `putVal` 方法中执行链表转红黑树的判断逻辑。** - -链表的长度大于 8 的时候,就执行 `treeifyBin` (转换红黑树)的逻辑。 - -```java -// 遍历链表 -for (int binCount = 0; ; ++binCount) { - // 遍历到链表最后一个节点 - if ((e = p.next) == null) { - p.next = newNode(hash, key, value, null); - // 如果链表元素个数大于TREEIFY_THRESHOLD(8) - if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st - // 红黑树转换(并不会直接转换成红黑树) - treeifyBin(tab, hash); - break; - } - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - break; - p = e; -} -``` - -**2、`treeifyBin` 方法中判断是否真的转换为红黑树。** - -```java -final void treeifyBin(Node[] tab, int hash) { - int n, index; Node e; - // 判断当前数组的长度是否小于 64 - if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) - // 如果当前数组的长度小于 64,那么会选择先进行数组扩容 - resize(); - else if ((e = tab[index = (n - 1) & hash]) != null) { - // 否则才将列表转换为红黑树 - - TreeNode hd = null, tl = null; - do { - TreeNode p = replacementTreeNode(e, null); - if (tl == null) - hd = p; - else { - p.prev = tl; - tl.next = p; - } - tl = p; - } while ((e = e.next) != null); - if ((tab[index] = hd) != null) - hd.treeify(tab); - } -} -``` - -将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树。 - -### HashMap 的长度为什么是 2 的幂次方 - -为了让 `HashMap` 存取高效并减少碰撞,我们需要确保数据尽量均匀分布。哈希值在 Java 中通常使用 `int` 表示,其范围是 `-2147483648 ~ 2147483647`前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但是,问题是一个 40 亿长度的数组,内存是放不下的。所以,这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。 - -**这个算法应该如何设计呢?** - -我们首先可能会想到采用 % 取余的操作来实现。但是,重点来了:“**取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作**(也就是说 `hash%length==hash&(length-1)` 的前提是 length 是 2 的 n 次方)。” 并且,**采用二进制位操作 & 相对于 % 能够提高运算效率**。 - -除了上面所说的位运算比取余效率高之外,我觉得更重要的一个原因是:**长度是 2 的幂次方,可以让 `HashMap` 在扩容的时候更均匀**。例如: - -- length = 8 时,length - 1 = 7 的二进制位`0111` -- length = 16 时,length - 1 = 15 的二进制位`1111` - -这时候原本存在 `HashMap` 中的元素计算新的数组位置时 `hash&(length-1)`,取决 hash 的第四个二进制位(从右数),会出现两种情况: - -1. 第四个二进制位为 0,数组位置不变,也就是说当前元素在新数组和旧数组的位置相同。 -2. 第四个二进制位为 1,数组位置在新数组扩容之后的那一部分。 - -这里列举一个例子: - -```plain -假设有一个元素的哈希值为 10101100 - -旧数组元素位置计算: -hash = 10101100 -length - 1 = 00000111 -& ----------------- -index = 00000100 (4) - -新数组元素位置计算: -hash = 10101100 -length - 1 = 00001111 -& ----------------- -index = 00001100 (12) - -看第四位(从右数): -1.高位为 0:位置不变。 -2.高位为 1:移动到新位置(原索引位置+原容量)。 -``` - -⚠️注意:这里列举的场景看的是第四个二进制位,更准确点来说看的是高位(从右数),例如 `length = 32` 时,`length - 1 = 31`,二进制为 `11111`,这里看的就是第五个二进制位。 - -也就是说扩容之后,在旧数组元素 hash 值比较均匀(至于 hash 值均不均匀,取决于前面讲的对象的 `hashcode()` 方法和扰动函数)的情况下,新数组元素也会被分配的比较均匀,最好的情况是会有一半在新数组的前半部分,一半在新数组后半部分。 - -这样也使得扩容机制变得简单和高效,扩容后只需检查哈希值高位的变化来决定元素的新位置,要么位置不变(高位为 0),要么就是移动到新位置(高位为 1,原索引位置+原容量)。 - -最后,简单总结一下 `HashMap` 的长度是 2 的幂次方的原因: - -1. 位运算效率更高:位运算(&)比取余运算(%)更高效。当长度为 2 的幂次方时,`hash % length` 等价于 `hash & (length - 1)`。 -2. 可以更好地保证哈希值的均匀分布:扩容之后,在旧数组元素 hash 值比较均匀的情况下,新数组元素也会被分配的比较均匀,最好的情况是会有一半在新数组的前半部分,一半在新数组后半部分。 -3. 扩容机制变得简单和高效:扩容后只需检查哈希值高位的变化来决定元素的新位置,要么位置不变(高位为 0),要么就是移动到新位置(高位为 1,原索引位置+原容量)。 - -### HashMap 多线程操作导致死循环问题 - -JDK1.7 及之前版本的 `HashMap` 在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。 - -为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用 `HashMap`,因为多线程下使用 `HashMap` 还是会存在数据覆盖的问题。并发环境下,推荐使用 `ConcurrentHashMap` 。 - -一般面试中这样介绍就差不多,不需要记各种细节,个人觉得也没必要记。如果想要详细了解 `HashMap` 扩容导致死循环问题,可以看看耗子叔的这篇文章:[Java HashMap 的死循环](https://coolshell.cn/articles/9606.html)。 - -### HashMap 为什么线程不安全? - -JDK1.7 及之前版本,在多线程环境下,`HashMap` 扩容时会造成死循环和数据丢失的问题。 - -数据丢失这个在 JDK1.7 和 JDK 1.8 中都存在,这里以 JDK 1.8 为例进行介绍。 - -JDK 1.8 后,在 `HashMap` 中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对 `HashMap` 的 `put` 操作会导致线程不安全,具体来说会有数据覆盖的风险。 - -举个例子: - -- 两个线程 1,2 同时进行 put 操作,并且发生了哈希冲突(hash 函数计算出的插入下标是相同的)。 -- 不同的线程可能在不同的时间片获得 CPU 执行的机会,当前线程 1 执行完哈希冲突判断后,由于时间片耗尽挂起。线程 2 先完成了插入操作。 -- 随后,线程 1 获得时间片,由于之前已经进行过 hash 碰撞的判断,所有此时会直接进行插入,这就导致线程 2 插入的数据被线程 1 覆盖了。 - -```java -public V put(K key, V value) { - return putVal(hash(key), key, value, false, true); -} - -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, - boolean evict) { - // ... - // 判断是否出现 hash 碰撞 - // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) - if ((p = tab[i = (n - 1) & hash]) == null) - tab[i] = newNode(hash, key, value, null); - // 桶中已经存在元素(处理hash冲突) - else { - // ... -} -``` - -还有一种情况是这两个线程同时 `put` 操作导致 `size` 的值不正确,进而导致数据覆盖的问题: - -1. 线程 1 执行 `if(++size > threshold)` 判断时,假设获得 `size` 的值为 10,由于时间片耗尽挂起。 -2. 线程 2 也执行 `if(++size > threshold)` 判断,获得 `size` 的值也为 10,并将元素插入到该桶位中,并将 `size` 的值更新为 11。 -3. 随后,线程 1 获得时间片,它也将元素放入桶位中,并将 size 的值更新为 11。 -4. 线程 1、2 都执行了一次 `put` 操作,但是 `size` 的值只增加了 1,也就导致实际上只有一个元素被添加到了 `HashMap` 中。 - -```java -public V put(K key, V value) { - return putVal(hash(key), key, value, false, true); -} - -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, - boolean evict) { - // ... - // 实际大小大于阈值则扩容 - if (++size > threshold) - resize(); - // 插入后回调 - afterNodeInsertion(evict); - return null; -} -``` - -### HashMap 常见的遍历方式? - -[HashMap 的 7 种遍历方式与性能分析!](https://mp.weixin.qq.com/s/zQBN3UvJDhRTKP6SzcZFKw) - -**🐛 修正(参见:[issue#1411](https://github.com/Snailclimb/JavaGuide/issues/1411))**: - -这篇文章对于 parallelStream 遍历方式的性能分析有误,先说结论:**存在阻塞时 parallelStream 性能最高, 非阻塞时 parallelStream 性能最低** 。 - -当遍历不存在阻塞时, parallelStream 的性能是最低的: - -```plain -Benchmark Mode Cnt Score Error Units -Test.entrySet avgt 5 288.651 ± 10.536 ns/op -Test.keySet avgt 5 584.594 ± 21.431 ns/op -Test.lambda avgt 5 221.791 ± 10.198 ns/op -Test.parallelStream avgt 5 6919.163 ± 1116.139 ns/op -``` - -加入阻塞代码`Thread.sleep(10)`后, parallelStream 的性能才是最高的: - -```plain -Benchmark Mode Cnt Score Error Units -Test.entrySet avgt 5 1554828440.000 ± 23657748.653 ns/op -Test.keySet avgt 5 1550612500.000 ± 6474562.858 ns/op -Test.lambda avgt 5 1551065180.000 ± 19164407.426 ns/op -Test.parallelStream avgt 5 186345456.667 ± 3210435.590 ns/op -``` - -### ConcurrentHashMap 和 Hashtable 的区别 - -`ConcurrentHashMap` 和 `Hashtable` 的区别主要体现在实现线程安全的方式上不同。 - -- **底层数据结构:** JDK1.7 的 `ConcurrentHashMap` 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟 `HashMap1.8` 的结构一样,数组+链表/红黑二叉树。`Hashtable` 和 JDK1.8 之前的 `HashMap` 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; -- **实现线程安全的方式(重要):** - - 在 JDK1.7 的时候,`ConcurrentHashMap` 对整个桶数组进行了分割分段(`Segment`,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 - - 到了 JDK1.8 的时候,`ConcurrentHashMap` 已经摒弃了 `Segment` 的概念,而是直接用 `Node` 数组+链表+红黑树的数据结构来实现,并发控制使用 `synchronized` 和 CAS 来操作。(JDK1.6 以后 `synchronized` 锁做了很多优化) 整个看起来就像是优化过且线程安全的 `HashMap`,虽然在 JDK1.8 中还能看到 `Segment` 的数据结构,但是已经简化了属性,只是为了兼容旧版本; - - **`Hashtable`(同一把锁)** :使用 `synchronized` 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 - -下面,我们再来看看两者底层数据结构的对比图。 - -**Hashtable** : - -![Hashtable 的内部结构](https://oss.javaguide.cn/github/javaguide/java/collection/jdk1.7_hashmap.png) - -

https://www.cnblogs.com/chengxiao/p/6842045.html>

- -**JDK1.7 的 ConcurrentHashMap**: - -![Java7 ConcurrentHashMap 存储结构](https://oss.javaguide.cn/github/javaguide/java/collection/java7_concurrenthashmap.png) - -`ConcurrentHashMap` 是由 `Segment` 数组结构和 `HashEntry` 数组结构组成。 - -`Segment` 数组中的每个元素包含一个 `HashEntry` 数组,每个 `HashEntry` 数组属于链表结构。 - -**JDK1.8 的 ConcurrentHashMap**: - -![Java8 ConcurrentHashMap 存储结构](https://oss.javaguide.cn/github/javaguide/java/collection/java8_concurrenthashmap.png) - -JDK1.8 的 `ConcurrentHashMap` 不再是 **Segment 数组 + HashEntry 数组 + 链表**,而是 **Node 数组 + 链表 / 红黑树**。不过,Node 只能用于链表的情况,红黑树的情况需要使用 **`TreeNode`**。当冲突链表达到一定长度时,链表会转换成红黑树。 - -`TreeNode`是存储红黑树节点,被`TreeBin`包装。`TreeBin`通过`root`属性维护红黑树的根结点,因为红黑树在旋转的时候,根结点可能会被它原来的子节点替换掉,在这个时间点,如果有其他线程要写这棵红黑树就会发生线程不安全问题,所以在 `ConcurrentHashMap` 中`TreeBin`通过`waiter`属性维护当前使用这棵红黑树的线程,来防止其他线程的进入。 - -```java -static final class TreeBin extends Node { - TreeNode root; - volatile TreeNode first; - volatile Thread waiter; - volatile int lockState; - // values for lockState - static final int WRITER = 1; // set while holding write lock - static final int WAITER = 2; // set when waiting for write lock - static final int READER = 4; // increment value for setting read lock -... -} -``` - -### ConcurrentHashMap 线程安全的具体实现方式/底层具体实现 - -#### JDK1.8 之前 - -![Java7 ConcurrentHashMap 存储结构](https://oss.javaguide.cn/github/javaguide/java/collection/java7_concurrenthashmap.png) - -首先将数据分为一段一段(这个“段”就是 `Segment`)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 - -**`ConcurrentHashMap` 是由 `Segment` 数组结构和 `HashEntry` 数组结构组成**。 - -`Segment` 继承了 `ReentrantLock`,所以 `Segment` 是一种可重入锁,扮演锁的角色。`HashEntry` 用于存储键值对数据。 - -```java -static class Segment extends ReentrantLock implements Serializable { -} -``` - -一个 `ConcurrentHashMap` 里包含一个 `Segment` 数组,`Segment` 的个数一旦**初始化就不能改变**。 `Segment` 数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。 - -`Segment` 的结构和 `HashMap` 类似,是一种数组和链表结构,一个 `Segment` 包含一个 `HashEntry` 数组,每个 `HashEntry` 是一个链表结构的元素,每个 `Segment` 守护着一个 `HashEntry` 数组里的元素,当对 `HashEntry` 数组的数据进行修改时,必须首先获得对应的 `Segment` 的锁。也就是说,对同一 `Segment` 的并发写入会被阻塞,不同 `Segment` 的写入是可以并发执行的。 - -#### JDK1.8 之后 - -![Java8 ConcurrentHashMap 存储结构](https://oss.javaguide.cn/github/javaguide/java/collection/java8_concurrenthashmap.png) - -Java 8 几乎完全重写了 `ConcurrentHashMap`,代码量从原来 Java 7 中的 1000 多行,变成了现在的 6000 多行。 - -`ConcurrentHashMap` 取消了 `Segment` 分段锁,采用 `Node + CAS + synchronized` 来保证并发安全。数据结构跟 `HashMap` 1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。 - -Java 8 中,锁粒度更细,`synchronized` 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。 - -### JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同? - -- **线程安全实现方式**:JDK 1.7 采用 `Segment` 分段锁来保证安全, `Segment` 是继承自 `ReentrantLock`。JDK1.8 放弃了 `Segment` 分段锁的设计,采用 `Node + CAS + synchronized` 保证线程安全,锁粒度更细,`synchronized` 只锁定当前链表或红黑二叉树的首节点。 -- **Hash 碰撞解决方法** : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。 -- **并发度**:JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。 - -### ConcurrentHashMap 为什么 key 和 value 不能为 null? - -`ConcurrentHashMap` 的 key 和 value 不能为 null 主要是为了避免二义性。null 是一个特殊的值,表示没有对象或没有引用。如果你用 null 作为键,那么你就无法区分这个键是否存在于 `ConcurrentHashMap` 中,还是根本没有这个键。同样,如果你用 null 作为值,那么你就无法区分这个值是否是真正存储在 `ConcurrentHashMap` 中的,还是因为找不到对应的键而返回的。 - -拿 get 方法取值来说,返回的结果为 null 存在两种情况: - -- 值没有在集合中 ; -- 值本身就是 null。 - -这也就是二义性的由来。 - -具体可以参考 [ConcurrentHashMap 源码分析](https://javaguide.cn/java/collection/concurrent-hash-map-source-code.html) 。 - -多线程环境下,存在一个线程操作该 `ConcurrentHashMap` 时,其他的线程将该 `ConcurrentHashMap` 修改的情况,所以无法通过 `containsKey(key)` 来判断否存在这个键值对,也就没办法解决二义性问题了。 - -与此形成对比的是,`HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个。如果传入 null 作为参数,就会返回 hash 值为 0 的位置的值。单线程环境下,不存在一个线程操作该 HashMap 时,其他的线程将该 `HashMap` 修改的情况,所以可以通过 `contains(key)`来做判断是否存在这个键值对,从而做相应的处理,也就不存在二义性问题。 - -也就是说,多线程下无法正确判定键值对是否存在(存在其他线程修改的情况),单线程是可以的(不存在其他线程修改的情况)。 - -如果你确实需要在 ConcurrentHashMap 中使用 null 的话,可以使用一个特殊的静态空对象来代替 null。 - -```java -public static final Object NULL = new Object(); -``` - -最后,再分享一下 `ConcurrentHashMap` 作者本人 (Doug Lea)对于这个问题的回答: - -> The main reason that nulls aren't allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can't be accommodated. The main one is that if `map.get(key)` returns `null`, you can't detect whether the key explicitly maps to `null` vs the key isn't mapped. In a non-concurrent map, you can check this via `map.contains(key)`, but in a concurrent one, the map might have changed between calls. - -翻译过来之后的,大致意思还是单线程下可以容忍歧义,而多线程下无法容忍。 - -### ConcurrentHashMap 能保证复合操作的原子性吗? - -`ConcurrentHashMap` 是线程安全的,意味着它可以保证多个线程同时对它进行读写操作时,不会出现数据不一致的情况,也不会导致 JDK1.7 及之前版本的 `HashMap` 多线程操作导致死循环问题。但是,这并不意味着它可以保证所有的复合操作都是原子性的,一定不要搞混了! - -复合操作是指由多个基本操作(如`put`、`get`、`remove`、`containsKey`等)组成的操作,例如先判断某个键是否存在`containsKey(key)`,然后根据结果进行插入或更新`put(key, value)`。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。 - -例如,有两个线程 A 和 B 同时对 `ConcurrentHashMap` 进行复合操作,如下: - -```java -// 线程 A -if (!map.containsKey(key)) { -map.put(key, value); -} -// 线程 B -if (!map.containsKey(key)) { -map.put(key, anotherValue); -} -``` - -如果线程 A 和 B 的执行顺序是这样: - -1. 线程 A 判断 map 中不存在 key -2. 线程 B 判断 map 中不存在 key -3. 线程 B 将 (key, anotherValue) 插入 map -4. 线程 A 将 (key, value) 插入 map - -那么最终的结果是 (key, value),而不是预期的 (key, anotherValue)。这就是复合操作的非原子性导致的问题。 - -**那如何保证 `ConcurrentHashMap` 复合操作的原子性呢?** - -`ConcurrentHashMap` 提供了一些原子性的复合操作,如 `putIfAbsent`、`compute`、`computeIfAbsent` 、`computeIfPresent`、`merge`等。这些方法都可以接受一个函数作为参数,根据给定的 key 和 value 来计算一个新的 value,并且将其更新到 map 中。 - -上面的代码可以改写为: - -```java -// 线程 A -map.putIfAbsent(key, value); -// 线程 B -map.putIfAbsent(key, anotherValue); -``` - -或者: - -```java -// 线程 A -map.computeIfAbsent(key, k -> value); -// 线程 B -map.computeIfAbsent(key, k -> anotherValue); -``` - -很多同学可能会说了,这种情况也能加锁同步呀!确实可以,但不建议使用加锁的同步机制,违背了使用 `ConcurrentHashMap` 的初衷。在使用 `ConcurrentHashMap` 的时候,尽量使用这些原子性的复合操作方法来保证原子性。 - -## Collections 工具类(不重要) - -**`Collections` 工具类常用方法**: - -- 排序 -- 查找,替换操作 -- 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合) - -### 排序操作 - -```java -void reverse(List list)//反转 -void shuffle(List list)//随机排序 -void sort(List list)//按自然排序的升序排序 -void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑 -void swap(List list, int i , int j)//交换两个索引位置的元素 -void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面 -``` - -### 查找,替换操作 - -```java -int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的 -int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll) -int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c) -void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素 -int frequency(Collection c, Object o)//统计元素出现次数 -int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target) -boolean replaceAll(List list, Object oldVal, Object newVal)//用新元素替换旧元素 -``` - -### 同步控制 - -`Collections` 提供了多个`synchronizedXxx()`方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。 - -我们知道 `HashSet`,`TreeSet`,`ArrayList`,`LinkedList`,`HashMap`,`TreeMap` 都是线程不安全的。`Collections` 提供了多个静态方法可以把他们包装成线程同步的集合。 - -**最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。** - -方法如下: - -```java -synchronizedCollection(Collection c) //返回指定 collection 支持的同步(线程安全的)collection。 -synchronizedList(List list)//返回指定列表支持的同步(线程安全的)List。 -synchronizedMap(Map m) //返回由指定映射支持的同步(线程安全的)Map。 -synchronizedSet(Set s) //返回指定 set 支持的同步(线程安全的)set。 -``` - - +| `HashMap` | `HashSet` | +| :--------------------------------------: | :-----------------------------------------------: | +| Implements `Map` interface | Implements `Set` interface | +| Stores key-value pairs | Stores only objects | +| Calls `put()` to add elements to the map | Calls `add()` method to add elements to the `Set` | +| \`HashMap | | diff --git a/docs/java/collection/linkedhashmap-source-code.md b/docs/java/collection/linkedhashmap-source-code.md index 08c9a2bcb28..68f0ff27872 100644 --- a/docs/java/collection/linkedhashmap-source-code.md +++ b/docs/java/collection/linkedhashmap-source-code.md @@ -1,27 +1,27 @@ --- -title: LinkedHashMap 源码分析 +title: LinkedHashMap Source Code Analysis category: Java tag: - - Java集合 + - Java Collections --- -## LinkedHashMap 简介 +## Introduction to LinkedHashMap -`LinkedHashMap` 是 Java 提供的一个集合类,它继承自 `HashMap`,并在 `HashMap` 基础上维护一条双向链表,使得具备如下特性: +`LinkedHashMap` is a collection class provided by Java that extends `HashMap` and maintains a doubly linked list on top of `HashMap`, giving it the following characteristics: -1. 支持遍历时会按照插入顺序有序进行迭代。 -2. 支持按照元素访问顺序排序,适用于封装 LRU 缓存工具。 -3. 因为内部使用双向链表维护各个节点,所以遍历时的效率和元素个数成正比,相较于和容量成正比的 HashMap 来说,迭代效率会高很多。 +1. Supports ordered iteration according to the insertion order. +2. Supports ordering by access order, suitable for encapsulating LRU cache tools. +3. Because it uses a doubly linked list to maintain nodes, the efficiency of traversal is proportional to the number of elements, making iteration much more efficient compared to `HashMap`, which is proportional to capacity. -`LinkedHashMap` 逻辑结构如下图所示,它是在 `HashMap` 基础上在各个节点之间维护一条双向链表,使得原本散列在不同 bucket 上的节点、链表、红黑树有序关联起来。 +The logical structure of `LinkedHashMap` is shown in the diagram below. It maintains a doubly linked list between each node on top of `HashMap`, allowing nodes, linked lists, and red-black trees that are originally hashed into different buckets to be associated in an ordered manner. -![LinkedHashMap 逻辑结构](https://oss.javaguide.cn/github/javaguide/java/collection/linkhashmap-structure-overview.png) +![LinkedHashMap Logical Structure](https://oss.javaguide.cn/github/javaguide/java/collection/linkhashmap-structure-overview.png) -## LinkedHashMap 使用示例 +## Example of Using LinkedHashMap -### 插入顺序遍历 +### Iteration in Insertion Order -如下所示,我们按照顺序往 `LinkedHashMap` 添加元素然后进行遍历。 +As shown below, we add elements to `LinkedHashMap` in order and then iterate through them. ```java HashMap < String, String > map = new LinkedHashMap < > (); @@ -35,7 +35,7 @@ for (Map.Entry < String, String > entry: map.entrySet()) { } ``` -输出: +Output: ```java a:2 @@ -44,13 +44,13 @@ r:1 e:23 ``` -可以看出,`LinkedHashMap` 的迭代顺序是和插入顺序一致的,这一点是 `HashMap` 所不具备的。 +It can be seen that the iteration order of `LinkedHashMap` is consistent with the insertion order, which is not the case with `HashMap`. -### 访问顺序遍历 +### Iteration in Access Order -`LinkedHashMap` 定义了排序模式 `accessOrder`(boolean 类型,默认为 false),访问顺序则为 true,插入顺序则为 false。 +`LinkedHashMap` defines a sorting mode `accessOrder` (boolean type, default is false). When `accessOrder` is true, the order is based on access; when false, it is based on insertion. -为了实现访问顺序遍历,我们可以使用传入 `accessOrder` 属性的 `LinkedHashMap` 构造方法,并将 `accessOrder` 设置为 true,表示其具备访问有序性。 +To achieve iteration in access order, we can use the `LinkedHashMap` constructor that takes the `accessOrder` parameter and set `accessOrder` to true, indicating that it has access order. ```java LinkedHashMap map = new LinkedHashMap<>(16, 0.75f, true); @@ -59,16 +59,16 @@ map.put(2, "two"); map.put(3, "three"); map.put(4, "four"); map.put(5, "five"); -//访问元素2,该元素会被移动至链表末端 +// Access element 2, this element will be moved to the end of the list map.get(2); -//访问元素3,该元素会被移动至链表末端 +// Access element 3, this element will be moved to the end of the list map.get(3); for (Map.Entry entry : map.entrySet()) { System.out.println(entry.getKey() + " : " + entry.getValue()); } ``` -输出: +Output: ```java 1 : one @@ -78,19 +78,19 @@ for (Map.Entry entry : map.entrySet()) { 3 : three ``` -可以看出,`LinkedHashMap` 的迭代顺序是和访问顺序一致的。 +It can be seen that the iteration order of `LinkedHashMap` is consistent with the access order. -### LRU 缓存 +### LRU Cache -从上一个我们可以了解到通过 `LinkedHashMap` 我们可以封装一个简易版的 LRU(**L**east **R**ecently **U**sed,最近最少使用) 缓存,确保当存放的元素超过容器容量时,将最近最少访问的元素移除。 +From the previous example, we can see that we can encapsulate a simple LRU (Least Recently Used) cache using `LinkedHashMap`, ensuring that when the stored elements exceed the container capacity, the least recently accessed elements are removed. ![](https://oss.javaguide.cn/github/javaguide/java/collection/lru-cache.png) -具体实现思路如下: +The specific implementation idea is as follows: -- 继承 `LinkedHashMap`; -- 构造方法中指定 `accessOrder` 为 true ,这样在访问元素时就会把该元素移动到链表尾部,链表首元素就是最近最少被访问的元素; -- 重写`removeEldestEntry` 方法,该方法会返回一个 boolean 值,告知 `LinkedHashMap` 是否需要移除链表首元素(缓存容量有限)。 +- Inherit from `LinkedHashMap`; +- In the constructor, set `accessOrder` to true, so that when accessing elements, the element will be moved to the end of the list, and the first element of the list will be the least recently accessed; +- Override the `removeEldestEntry` method, which returns a boolean value to inform `LinkedHashMap` whether to remove the first element of the list (the cache capacity is limited). ```java public class LRUCache extends LinkedHashMap { @@ -102,7 +102,7 @@ public class LRUCache extends LinkedHashMap { } /** - * 判断size超过容量时返回true,告知LinkedHashMap移除最老的缓存项(即链表的第一个元素) + * Returns true if size exceeds capacity, informing LinkedHashMap to remove the oldest cache entry (i.e., the first element of the list) */ @Override protected boolean removeEldestEntry(Map.Entry eldest) { @@ -111,480 +111,7 @@ public class LRUCache extends LinkedHashMap { } ``` -测试代码如下,笔者初始化缓存容量为 3,然后按照次序先后添加 4 个元素。 +The test code is as follows. The author initializes the cache capacity to 3 and then adds 4 elements in order. ```java -LRUCache cache = new LRUCache<>(3); -cache.put(1, "one"); -cache.put(2, "two"); -cache.put(3, "three"); -cache.put(4, "four"); -cache.put(5, "five"); -for (int i = 1; i <= 5; i++) { - System.out.println(cache.get(i)); -} -``` - -输出: - -```java -null -null -three -four -five -``` - -从输出结果来看,由于缓存容量为 3 ,因此,添加第 4 个元素时,第 1 个元素会被删除。添加第 5 个元素时,第 2 个元素会被删除。 - -## LinkedHashMap 源码解析 - -### Node 的设计 - -在正式讨论 `LinkedHashMap` 前,我们先来聊聊 `LinkedHashMap` 节点 `Entry` 的设计,我们都知道 `HashMap` 的 bucket 上的因为冲突转为链表的节点会在符合以下两个条件时会将链表转为红黑树: - -1. ~~链表上的节点个数达到树化的阈值 7,即`TREEIFY_THRESHOLD - 1`。~~ -2. bucket 的容量达到最小的树化容量即`MIN_TREEIFY_CAPACITY`。 - -> **🐛 修正(参见:[issue#2147](https://github.com/Snailclimb/JavaGuide/issues/2147))**: -> -> 链表上的节点个数达到树化的阈值是 8 而非 7。因为源码的判断是从链表初始元素开始遍历,下标是从 0 开始的,所以判断条件设置为 8-1=7,其实是迭代到尾部元素时再判断整个链表长度大于等于 8 才进行树化操作。 -> -> ![](https://oss.javaguide.cn/github/javaguide/java/jvm/LinkedHashMap-putval-TREEIFY.png) - -而 `LinkedHashMap` 是在 `HashMap` 的基础上为 bucket 上的每一个节点建立一条双向链表,这就使得转为红黑树的树节点也需要具备双向链表节点的特性,即每一个树节点都需要拥有两个引用存储前驱节点和后继节点的地址,所以对于树节点类 `TreeNode` 的设计就是一个比较棘手的问题。 - -对此我们不妨来看看两者之间节点类的类图,可以看到: - -1. `LinkedHashMap` 的节点内部类 `Entry` 基于 `HashMap` 的基础上,增加 `before` 和 `after` 指针使节点具备双向链表的特性。 -2. `HashMap` 的树节点 `TreeNode` 继承了具备双向链表特性的 `LinkedHashMap` 的 `Entry`。 - -![LinkedHashMap 和 HashMap 之间的关系](https://oss.javaguide.cn/github/javaguide/java/collection/map-hashmap-linkedhashmap.png) - -很多读者此时就会有这样一个疑问,为什么 `HashMap` 的树节点 `TreeNode` 要通过 `LinkedHashMap` 获取双向链表的特性呢?为什么不直接在 `Node` 上实现前驱和后继指针呢? - -先来回答第一个问题,我们都知道 `LinkedHashMap` 是在 `HashMap` 基础上对节点增加双向指针实现双向链表的特性,所以 `LinkedHashMap` 内部链表转红黑树时,对应的节点会转为树节点 `TreeNode`,为了保证使用 `LinkedHashMap` 时树节点具备双向链表的特性,所以树节点 `TreeNode` 需要继承 `LinkedHashMap` 的 `Entry`。 - -再来说说第二个问题,我们直接在 `HashMap` 的节点 `Node` 上直接实现前驱和后继指针,然后 `TreeNode` 直接继承 `Node` 获取双向链表的特性为什么不行呢?其实这样做也是可以的。只不过这种做法会使得使用 `HashMap` 时存储键值对的节点类 `Node` 多了两个没有必要的引用,占用没必要的内存空间。 - -所以,为了保证 `HashMap` 底层的节点类 `Node` 没有多余的引用,又要保证 `LinkedHashMap` 的节点类 `Entry` 拥有存储链表的引用,设计者就让 `LinkedHashMap` 的节点 `Entry` 去继承 Node 并增加存储前驱后继节点的引用 `before`、`after`,让需要用到链表特性的节点去实现需要的逻辑。然后树节点 `TreeNode` 再通过继承 `Entry` 获取 `before`、`after` 两个指针。 - -```java -static class Entry extends HashMap.Node { - Entry before, after; - Entry(int hash, K key, V value, Node next) { - super(hash, key, value, next); - } - } -``` - -但是这样做,不也使得使用 `HashMap` 时的 `TreeNode` 多了两个没有必要的引用吗?这不也是一种空间的浪费吗? - -```java -static final class TreeNode extends LinkedHashMap.Entry { - //略 - -} -``` - -对于这个问题,引用作者的一段注释,作者们认为在良好的 `hashCode` 算法时,`HashMap` 转红黑树的概率不大。就算转为红黑树变为树节点,也可能会因为移除或者扩容将 `TreeNode` 变为 `Node`,所以 `TreeNode` 的使用概率不算很大,对于这一点资源空间的浪费是可以接受的。 - -```bash -Because TreeNodes are about twice the size of regular nodes, we -use them only when bins contain enough nodes to warrant use -(see TREEIFY_THRESHOLD). And when they become too small (due to -removal or resizing) they are converted back to plain bins. In -usages with well-distributed user hashCodes, tree bins are -rarely used. Ideally, under random hashCodes, the frequency of -nodes in bins follows a Poisson distribution -``` - -### 构造方法 - -`LinkedHashMap` 构造方法有 4 个实现也比较简单,直接调用父类即 `HashMap` 的构造方法完成初始化。 - -```java -public LinkedHashMap() { - super(); - accessOrder = false; -} - -public LinkedHashMap(int initialCapacity) { - super(initialCapacity); - accessOrder = false; -} - -public LinkedHashMap(int initialCapacity, float loadFactor) { - super(initialCapacity, loadFactor); - accessOrder = false; -} - -public LinkedHashMap(int initialCapacity, - float loadFactor, - boolean accessOrder) { - super(initialCapacity, loadFactor); - this.accessOrder = accessOrder; -} -``` - -我们上面也提到了,默认情况下 `accessOrder` 为 false,如果我们要让 `LinkedHashMap` 实现键值对按照访问顺序排序(即将最近未访问的元素排在链表首部、最近访问的元素移动到链表尾部),需要调用第 4 个构造方法将 `accessOrder` 设置为 true。 - -### get 方法 - -`get` 方法是 `LinkedHashMap` 增删改查操作中唯一一个重写的方法, `accessOrder` 为 true 的情况下, 它会在元素查询完成之后,将当前访问的元素移到链表的末尾。 - -```java -public V get(Object key) { - Node < K, V > e; - //获取key的键值对,若为空直接返回 - if ((e = getNode(hash(key), key)) == null) - return null; - //若accessOrder为true,则调用afterNodeAccess将当前元素移到链表末尾 - if (accessOrder) - afterNodeAccess(e); - //返回键值对的值 - return e.value; - } -``` - -从源码可以看出,`get` 的执行步骤非常简单: - -1. 调用父类即 `HashMap` 的 `getNode` 获取键值对,若为空则直接返回。 -2. 判断 `accessOrder` 是否为 true,若为 true 则说明需要保证 `LinkedHashMap` 的链表访问有序性,执行步骤 3。 -3. 调用 `LinkedHashMap` 重写的 `afterNodeAccess` 将当前元素添加到链表末尾。 - -关键点在于 `afterNodeAccess` 方法的实现,这个方法负责将元素移动到链表末尾。 - -```java -void afterNodeAccess(Node < K, V > e) { // move node to last - LinkedHashMap.Entry < K, V > last; - //如果accessOrder 且当前节点不为链表尾节点 - if (accessOrder && (last = tail) != e) { - - //获取当前节点、以及前驱节点和后继节点 - LinkedHashMap.Entry < K, V > p = - (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after; - - //将当前节点的后继节点指针指向空,使其和后继节点断开联系 - p.after = null; - - //如果前驱节点为空,则说明当前节点是链表的首节点,故将后继节点设置为首节点 - if (b == null) - head = a; - else - //如果前驱节点不为空,则让前驱节点指向后继节点 - b.after = a; - - //如果后继节点不为空,则让后继节点指向前驱节点 - if (a != null) - a.before = b; - else - //如果后继节点为空,则说明当前节点在链表最末尾,直接让last 指向前驱节点,这个 else其实 没有意义,因为最开头if已经确保了p不是尾结点了,自然after不会是null - last = b; - - //如果last为空,则说明当前链表只有一个节点p,则将head指向p - if (last == null) - head = p; - else { - //反之让p的前驱指针指向尾节点,再让尾节点的前驱指针指向p - p.before = last; - last.after = p; - } - //tail指向p,自此将节点p移动到链表末尾 - tail = p; - - ++modCount; - } -} -``` - -从源码可以看出, `afterNodeAccess` 方法完成了下面这些操作: - -1. 如果 `accessOrder` 为 true 且链表尾部不为当前节点 p,我们则需要将当前节点移到链表尾部。 -2. 获取当前节点 p、以及它的前驱节点 b 和后继节点 a。 -3. 将当前节点 p 的后继指针设置为 null,使其和后继节点 p 断开联系。 -4. 尝试将前驱节点指向后继节点,若前驱节点为空,则说明当前节点 p 就是链表首节点,故直接将后继节点 a 设置为首节点,随后我们再将 p 追加到 a 的末尾。 -5. 再尝试让后继节点 a 指向前驱节点 b。 -6. 上述操作让前驱节点和后继节点完成关联,并将当前节点 p 独立出来,这一步则是将当前节点 p 追加到链表末端,如果链表末端为空,则说明当前链表只有一个节点 p,所以直接让 head 指向 p 即可。 -7. 上述操作已经将 p 成功到达链表末端,最后我们将 tail 指针即指向链表末端的指针指向 p 即可。 - -可以结合这张图理解,展示了 key 为 13 的元素被移动到了链表尾部。 - -![LinkedHashMap 移动元素 13 到链表尾部](https://oss.javaguide.cn/github/javaguide/java/collection/linkedhashmap-get.png) - -看不太懂也没关系,知道这个方法的作用就够了,后续有时间再慢慢消化。 - -### remove 方法后置操作——afterNodeRemoval - -`LinkedHashMap` 并没有对 `remove` 方法进行重写,而是直接继承 `HashMap` 的 `remove` 方法,为了保证键值对移除后双向链表中的节点也会同步被移除,`LinkedHashMap` 重写了 `HashMap` 的空实现方法 `afterNodeRemoval`。 - -```java -final Node removeNode(int hash, Object key, Object value, - boolean matchValue, boolean movable) { - //略 - if (node != null && (!matchValue || (v = node.value) == value || - (value != null && value.equals(v)))) { - if (node instanceof TreeNode) - ((TreeNode)node).removeTreeNode(this, tab, movable); - else if (node == p) - tab[index] = node.next; - else - p.next = node.next; - ++modCount; - --size; - //HashMap的removeNode完成元素移除后会调用afterNodeRemoval进行移除后置操作 - afterNodeRemoval(node); - return node; - } - } - return null; - } -//空实现 -void afterNodeRemoval(Node p) { } -``` - -我们可以看到从 `HashMap` 继承来的 `remove` 方法内部调用的 `removeNode` 方法将节点从 bucket 删除后,调用了 `afterNodeRemoval`。 - -```java -void afterNodeRemoval(Node e) { // unlink - - //获取当前节点p、以及e的前驱节点b和后继节点a - LinkedHashMap.Entry p = - (LinkedHashMap.Entry)e, b = p.before, a = p.after; - //将p的前驱和后继指针都设置为null,使其和前驱、后继节点断开联系 - p.before = p.after = null; - - //如果前驱节点为空,则说明当前节点p是链表首节点,让head指针指向后继节点a即可 - if (b == null) - head = a; - else - //如果前驱节点b不为空,则让b直接指向后继节点a - b.after = a; - - //如果后继节点为空,则说明当前节点p在链表末端,所以直接让tail指针指向前驱节点a即可 - if (a == null) - tail = b; - else - //反之后继节点的前驱指针直接指向前驱节点 - a.before = b; - } -``` - -从源码可以看出, `afterNodeRemoval` 方法的整体操作就是让当前节点 p 和前驱节点、后继节点断开联系,等待 gc 回收,整体步骤为: - -1. 获取当前节点 p、以及 p 的前驱节点 b 和后继节点 a。 -2. 让当前节点 p 和其前驱、后继节点断开联系。 -3. 尝试让前驱节点 b 指向后继节点 a,若 b 为空则说明当前节点 p 在链表首部,我们直接将 head 指向后继节点 a 即可。 -4. 尝试让后继节点 a 指向前驱节点 b,若 a 为空则说明当前节点 p 在链表末端,所以直接让 tail 指针指向前驱节点 b 即可。 - -可以结合这张图理解,展示了 key 为 13 的元素被删除,也就是从链表中移除了这个元素。 - -![LinkedHashMap 删除元素 13](https://oss.javaguide.cn/github/javaguide/java/collection/linkedhashmap-remove.png) - -看不太懂也没关系,知道这个方法的作用就够了,后续有时间再慢慢消化。 - -### put 方法后置操作——afterNodeInsertion - -同样的 `LinkedHashMap` 并没有实现插入方法,而是直接继承 `HashMap` 的所有插入方法交由用户使用,但为了维护双向链表访问的有序性,它做了这样两件事: - -1. 重写 `afterNodeAccess`(上文提到过),如果当前被插入的 key 已存在与 `map` 中,因为 `LinkedHashMap` 的插入操作会将新节点追加至链表末尾,所以对于存在的 key 则调用 `afterNodeAccess` 将其放到链表末端。 -2. 重写了 `HashMap` 的 `afterNodeInsertion` 方法,当 `removeEldestEntry` 返回 true 时,会将链表首节点移除。 - -这一点我们可以在 `HashMap` 的插入操作核心方法 `putVal` 中看到。 - -```java -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, - boolean evict) { - //略 - if (e != null) { // existing mapping for key - V oldValue = e.value; - if (!onlyIfAbsent || oldValue == null) - e.value = value; - //如果当前的key在map中存在,则调用afterNodeAccess - afterNodeAccess(e); - return oldValue; - } - } - ++modCount; - if (++size > threshold) - resize(); - //调用插入后置方法,该方法被LinkedHashMap重写 - afterNodeInsertion(evict); - return null; - } -``` - -上述步骤的源码上文已经解释过了,所以这里我们着重了解一下 `afterNodeInsertion` 的工作流程,假设我们的重写了 `removeEldestEntry`,当链表 `size` 超过 `capacity` 时,就返回 true。 - -```java -/** - * 判断size超过容量时返回true,告知LinkedHashMap移除最老的缓存项(即链表的第一个元素) - */ -protected boolean removeEldestEntry(Map.Entry < K, V > eldest) { - return size() > capacity; -} -``` - -以下图为例,假设笔者最后新插入了一个不存在的节点 19,假设 `capacity` 为 4,所以 `removeEldestEntry` 返回 true,我们要将链表首节点移除。 - -![LinkedHashMap 中插入新元素 19](https://oss.javaguide.cn/github/javaguide/java/collection/linkedhashmap-after-insert-1.png) - -移除的步骤很简单,查看链表首节点是否存在,若存在则断开首节点和后继节点的关系,并让首节点指针指向下一节点,所以 head 指针指向了 12,节点 10 成为没有任何引用指向的空对象,等待 GC。 - -![LinkedHashMap 中插入新元素 19](https://oss.javaguide.cn/github/javaguide/java/collection/linkedhashmap-after-insert-2.png) - -```java -void afterNodeInsertion(boolean evict) { // possibly remove eldest - LinkedHashMap.Entry first; - //如果evict为true且队首元素不为空以及removeEldestEntry返回true,则说明我们需要最老的元素(即在链表首部的元素)移除。 - if (evict && (first = head) != null && removeEldestEntry(first)) { - //获取链表首部的键值对的key - K key = first.key; - //调用removeNode将元素从HashMap的bucket中移除,并和LinkedHashMap的双向链表断开,等待gc回收 - removeNode(hash(key), key, null, false, true); - } - } -``` - -从源码可以看出, `afterNodeInsertion` 方法完成了下面这些操作: - -1. 判断 `eldest` 是否为 true,只有为 true 才能说明可能需要将最年长的键值对(即链表首部的元素)进行移除,具体是否具体要进行移除,还得确定链表是否为空`((first = head) != null)`,以及 `removeEldestEntry` 方法是否返回 true,只有这两个方法返回 true 才能确定当前链表不为空,且链表需要进行移除操作了。 -2. 获取链表第一个元素的 key。 -3. 调用 `HashMap` 的 `removeNode` 方法,该方法我们上文提到过,它会将节点从 `HashMap` 的 bucket 中移除,并且 `LinkedHashMap` 还重写了 `removeNode` 中的 `afterNodeRemoval` 方法,所以这一步将通过调用 `removeNode` 将元素从 `HashMap` 的 bucket 中移除,并和 `LinkedHashMap` 的双向链表断开,等待 gc 回收。 - -## LinkedHashMap 和 HashMap 遍历性能比较 - -`LinkedHashMap` 维护了一个双向链表来记录数据插入的顺序,因此在迭代遍历生成的迭代器的时候,是按照双向链表的路径进行遍历的。这一点相比于 `HashMap` 那种遍历整个 bucket 的方式来说,高效许多。 - -这一点我们可以从两者的迭代器中得以印证,先来看看 `HashMap` 的迭代器,可以看到 `HashMap` 迭代键值对时会用到一个 `nextNode` 方法,该方法会返回 next 指向的下一个元素,并会从 next 开始遍历 bucket 找到下一个 bucket 中不为空的元素 Node。 - -```java - final class EntryIterator extends HashIterator - implements Iterator < Map.Entry < K, V >> { - public final Map.Entry < K, - V > next() { - return nextNode(); - } - } - - //获取下一个Node - final Node < K, V > nextNode() { - Node < K, V > [] t; - //获取下一个元素next - Node < K, V > e = next; - if (modCount != expectedModCount) - throw new ConcurrentModificationException(); - if (e == null) - throw new NoSuchElementException(); - //将next指向bucket中下一个不为空的Node - if ((next = (current = e).next) == null && (t = table) != null) { - do {} while (index < t.length && (next = t[index++]) == null); - } - return e; - } -``` - -相比之下 `LinkedHashMap` 的迭代器则是直接使用通过 `after` 指针快速定位到当前节点的后继节点,简洁高效许多。 - -```java - final class LinkedEntryIterator extends LinkedHashIterator - implements Iterator < Map.Entry < K, V >> { - public final Map.Entry < K, - V > next() { - return nextNode(); - } - } - //获取下一个Node - final LinkedHashMap.Entry < K, V > nextNode() { - //获取下一个节点next - LinkedHashMap.Entry < K, V > e = next; - if (modCount != expectedModCount) - throw new ConcurrentModificationException(); - if (e == null) - throw new NoSuchElementException(); - //current 指针指向当前节点 - current = e; - //next直接当前节点的after指针快速定位到下一个节点 - next = e.after; - return e; - } -``` - -为了验证笔者所说的观点,笔者对这两个容器进行了压测,测试插入 1000w 和迭代 1000w 条数据的耗时,代码如下: - -```java -int count = 1000_0000; -Map hashMap = new HashMap<>(); -Map linkedHashMap = new LinkedHashMap<>(); - -long start, end; - -start = System.currentTimeMillis(); -for (int i = 0; i < count; i++) { - hashMap.put(ThreadLocalRandom.current().nextInt(1, count), ThreadLocalRandom.current().nextInt(0, count)); -} -end = System.currentTimeMillis(); -System.out.println("map time putVal: " + (end - start)); - -start = System.currentTimeMillis(); -for (int i = 0; i < count; i++) { - linkedHashMap.put(ThreadLocalRandom.current().nextInt(1, count), ThreadLocalRandom.current().nextInt(0, count)); -} -end = System.currentTimeMillis(); -System.out.println("linkedHashMap putVal time: " + (end - start)); - -start = System.currentTimeMillis(); -long num = 0; -for (Integer v : hashMap.values()) { - num = num + v; -} -end = System.currentTimeMillis(); -System.out.println("map get time: " + (end - start)); - -start = System.currentTimeMillis(); -for (Integer v : linkedHashMap.values()) { - num = num + v; -} -end = System.currentTimeMillis(); -System.out.println("linkedHashMap get time: " + (end - start)); -System.out.println(num); -``` - -从输出结果来看,因为 `LinkedHashMap` 需要维护双向链表的缘故,插入元素相较于 `HashMap` 会更耗时,但是有了双向链表明确的前后节点关系,迭代效率相对于前者高效了许多。不过,总体来说却别不大,毕竟数据量这么庞大。 - -```bash -map time putVal: 5880 -linkedHashMap putVal time: 7567 -map get time: 143 -linkedHashMap get time: 67 -63208969074998 -``` - -## LinkedHashMap 常见面试题 - -### 什么是 LinkedHashMap? - -`LinkedHashMap` 是 Java 集合框架中 `HashMap` 的一个子类,它继承了 `HashMap` 的所有属性和方法,并且在 `HashMap` 的基础重写了 `afterNodeRemoval`、`afterNodeInsertion`、`afterNodeAccess` 方法。使之拥有顺序插入和访问有序的特性。 - -### LinkedHashMap 如何按照插入顺序迭代元素? - -`LinkedHashMap` 按照插入顺序迭代元素是它的默认行为。`LinkedHashMap` 内部维护了一个双向链表,用于记录元素的插入顺序。因此,当使用迭代器迭代元素时,元素的顺序与它们最初插入的顺序相同。 - -### LinkedHashMap 如何按照访问顺序迭代元素? - -`LinkedHashMap` 可以通过构造函数中的 `accessOrder` 参数指定按照访问顺序迭代元素。当 `accessOrder` 为 true 时,每次访问一个元素时,该元素会被移动到链表的末尾,因此下次访问该元素时,它就会成为链表中的最后一个元素,从而实现按照访问顺序迭代元素。 - -### LinkedHashMap 如何实现 LRU 缓存? - -将 `accessOrder` 设置为 true 并重写 `removeEldestEntry` 方法当链表大小超过容量时返回 true,使得每次访问一个元素时,该元素会被移动到链表的末尾。一旦插入操作让 `removeEldestEntry` 返回 true 时,视为缓存已满,`LinkedHashMap` 就会将链表首元素移除,由此我们就能实现一个 LRU 缓存。 - -### LinkedHashMap 和 HashMap 有什么区别? - -`LinkedHashMap` 和 `HashMap` 都是 Java 集合框架中的 Map 接口的实现类。它们的最大区别在于迭代元素的顺序。`HashMap` 迭代元素的顺序是不确定的,而 `LinkedHashMap` 提供了按照插入顺序或访问顺序迭代元素的功能。此外,`LinkedHashMap` 内部维护了一个双向链表,用于记录元素的插入顺序或访问顺序,而 `HashMap` 则没有这个链表。因此,`LinkedHashMap` 的插入性能可能会比 `HashMap` 略低,但它提供了更多的功能并且迭代效率相较于 `HashMap` 更加高效。 - -## 参考文献 - -- LinkedHashMap 源码详细分析(JDK1.8): -- HashMap 与 LinkedHashMap: -- 源于 LinkedHashMap 源码: - +LRUCache -## LinkedList 简介 +## Introduction to LinkedList -`LinkedList` 是一个基于双向链表实现的集合类,经常被拿来和 `ArrayList` 做比较。关于 `LinkedList` 和`ArrayList`的详细对比,我们 [Java 集合常见面试题总结(上)](./java-collection-questions-01.md)有详细介绍到。 +`LinkedList` is a collection class implemented based on a doubly linked list, often compared with `ArrayList`. A detailed comparison between `LinkedList` and `ArrayList` can be found in our [Summary of Common Java Collection Interview Questions (Part 1)](./java-collection-questions-01.md). -![双向链表](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/bidirectional-linkedlist.png) +![Doubly Linked List](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/bidirectional-linkedlist.png) -不过,我们在项目中一般是不会使用到 `LinkedList` 的,需要用到 `LinkedList` 的场景几乎都可以使用 `ArrayList` 来代替,并且,性能通常会更好!就连 `LinkedList` 的作者约书亚 · 布洛克(Josh Bloch)自己都说从来不会使用 `LinkedList` 。 +However, in projects, we generally do not use `LinkedList`. Any scenario that requires `LinkedList` can almost always be replaced with `ArrayList`, and performance is usually better! Even the author of `LinkedList`, Joshua Bloch, has stated that he never uses `LinkedList`. ![](https://oss.javaguide.cn/github/javaguide/redisimage-20220412110853807.png) -另外,不要下意识地认为 `LinkedList` 作为链表就最适合元素增删的场景。我在上面也说了,`LinkedList` 仅仅在头尾插入或者删除元素的时候时间复杂度近似 O(1),其他情况增删元素的平均时间复杂度都是 O(n) 。 +Additionally, do not instinctively think that `LinkedList`, as a linked list, is the most suitable for scenarios involving element insertion and deletion. As mentioned above, `LinkedList` only has a time complexity close to O(1) for inserting or deleting elements at the head or tail; in other cases, the average time complexity for insertion and deletion is O(n). -### LinkedList 插入和删除元素的时间复杂度? +### What is the Time Complexity for Inserting and Deleting Elements in LinkedList? -- 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。 -- 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。 -- 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,不过由于有头尾指针,可以从较近的指针出发,因此需要遍历平均 n/4 个元素,时间复杂度为 O(n)。 +- Insertion/Deletion at the Head: Only the pointer of the head node needs to be modified to complete the insertion/deletion operation, so the time complexity is O(1). +- Insertion/Deletion at the Tail: Only the pointer of the tail node needs to be modified to complete the insertion/deletion operation, so the time complexity is O(1). +- Insertion/Deletion at a Specified Position: It is necessary to first move to the specified position and then modify the pointer of the specified node to complete the insertion/deletion. However, since there are head and tail pointers, we can start from the closer pointer, so on average, we need to traverse n/4 elements, resulting in a time complexity of O(n). -### LinkedList 为什么不能实现 RandomAccess 接口? +### Why Can't LinkedList Implement the RandomAccess Interface? -`RandomAccess` 是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。由于 `LinkedList` 底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 `RandomAccess` 接口。 +`RandomAccess` is a marker interface that indicates that the classes implementing this interface support random access (i.e., can quickly access elements by index). Since the underlying data structure of `LinkedList` is a linked list, with non-contiguous memory addresses, it can only locate elements through pointers and does not support random quick access, so it cannot implement the `RandomAccess` interface. -## LinkedList 源码分析 +## LinkedList Source Code Analysis -这里以 JDK1.8 为例,分析一下 `LinkedList` 的底层核心源码。 +Here, we will analyze the core source code of `LinkedList` using JDK 1.8 as an example. -`LinkedList` 的类定义如下: +The class definition of `LinkedList` is as follows: ```java public class LinkedList @@ -44,28 +44,28 @@ public class LinkedList } ``` -`LinkedList` 继承了 `AbstractSequentialList` ,而 `AbstractSequentialList` 又继承于 `AbstractList` 。 +`LinkedList` inherits from `AbstractSequentialList`, which in turn inherits from `AbstractList`. -阅读过 `ArrayList` 的源码我们就知道,`ArrayList` 同样继承了 `AbstractList` , 所以 `LinkedList` 会有大部分方法和 `ArrayList` 相似。 +Having read the source code of `ArrayList`, we know that `ArrayList` also inherits from `AbstractList`, so `LinkedList` will have many similar methods to `ArrayList`. -`LinkedList` 实现了以下接口: +`LinkedList` implements the following interfaces: -- `List` : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。 -- `Deque` :继承自 `Queue` 接口,具有双端队列的特性,支持从两端插入和删除元素,方便实现栈和队列等数据结构。需要注意,`Deque` 的发音为 "deck" [dɛk],这个大部分人都会读错。 -- `Cloneable` :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。 -- `Serializable` : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。 +- `List`: Indicates that it is a list, supporting operations such as adding, deleting, and searching, and can be accessed by index. +- `Deque`: Inherited from the `Queue` interface, it has the characteristics of a double-ended queue, supporting insertion and deletion of elements from both ends, making it convenient for implementing data structures like stacks and queues. Note that `Deque` is pronounced "deck" [dɛk], which most people mispronounce. +- `Cloneable`: Indicates that it has copy capabilities, allowing for deep or shallow copy operations. +- `Serializable`: Indicates that it can perform serialization operations, meaning it can convert objects into byte streams for persistent storage or network transmission, which is very convenient. -![LinkedList 类图](https://oss.javaguide.cn/github/javaguide/java/collection/linkedlist--class-diagram.png) +![LinkedList Class Diagram](https://oss.javaguide.cn/github/javaguide/java/collection/linkedlist--class-diagram.png) -`LinkedList` 中的元素是通过 `Node` 定义的: +The elements in `LinkedList` are defined through `Node`: ```java private static class Node { - E item;// 节点值 - Node next; // 指向的下一个节点(后继节点) - Node prev; // 指向的前一个节点(前驱结点) + E item; // Node value + Node next; // Pointer to the next node (successor node) + Node prev; // Pointer to the previous node (predecessor node) - // 初始化参数顺序分别是:前驱结点、本身节点值、后继节点 + // The order of initialization parameters is: predecessor node, itself node value, successor node Node(Node prev, E element, Node next) { this.item = element; this.next = next; @@ -74,445 +74,6 @@ private static class Node { } ``` -### 初始化 +### Initialization -`LinkedList` 中有一个无参构造函数和一个有参构造函数。 - -```java -// 创建一个空的链表对象 -public LinkedList() { -} - -// 接收一个集合类型作为参数,会创建一个与传入集合相同元素的链表对象 -public LinkedList(Collection c) { - this(); - addAll(c); -} -``` - -### 插入元素 - -`LinkedList` 除了实现了 `List` 接口相关方法,还实现了 `Deque` 接口的很多方法,所以我们有很多种方式插入元素。 - -我们这里以 `List` 接口中相关的插入方法为例进行源码讲解,对应的是`add()` 方法。 - -`add()` 方法有两个版本: - -- `add(E e)`:用于在 `LinkedList` 的尾部插入元素,即将新元素作为链表的最后一个元素,时间复杂度为 O(1)。 -- `add(int index, E element)`:用于在指定位置插入元素。这种插入方式需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/4 个元素,时间复杂度为 O(n)。 - -```java -// 在链表尾部插入元素 -public boolean add(E e) { - linkLast(e); - return true; -} - -// 在链表指定位置插入元素 -public void add(int index, E element) { - // 下标越界检查 - checkPositionIndex(index); - - // 判断 index 是不是链表尾部位置 - if (index == size) - // 如果是就直接调用 linkLast 方法将元素节点插入链表尾部即可 - linkLast(element); - else - // 如果不是则调用 linkBefore 方法将其插入指定元素之前 - linkBefore(element, node(index)); -} - -// 将元素节点插入到链表尾部 -void linkLast(E e) { - // 将最后一个元素赋值(引用传递)给节点 l - final Node l = last; - // 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空 - final Node newNode = new Node<>(l, e, null); - // 将 last 引用指向新节点 - last = newNode; - // 判断尾节点是否为空 - // 如果 l 是null 意味着这是第一次添加元素 - if (l == null) - // 如果是第一次添加,将first赋值为新节点,此时链表只有一个元素 - first = newNode; - else - // 如果不是第一次添加,将新节点赋值给l(添加前的最后一个元素)的next - l.next = newNode; - size++; - modCount++; -} - -// 在指定元素之前插入元素 -void linkBefore(E e, Node succ) { - // assert succ != null;断言 succ不为 null - // 定义一个节点元素保存 succ 的 prev 引用,也就是它的前一节点信息 - final Node pred = succ.prev; - // 初始化节点,并指明前驱和后继节点 - final Node newNode = new Node<>(pred, e, succ); - // 将 succ 节点前驱引用 prev 指向新节点 - succ.prev = newNode; - // 判断前驱节点是否为空,为空表示 succ 是第一个节点 - if (pred == null) - // 新节点成为第一个节点 - first = newNode; - else - // succ 节点前驱的后继引用指向新节点 - pred.next = newNode; - size++; - modCount++; -} -``` - -### 获取元素 - -`LinkedList`获取元素相关的方法一共有 3 个: - -1. `getFirst()`:获取链表的第一个元素。 -2. `getLast()`:获取链表的最后一个元素。 -3. `get(int index)`:获取链表指定位置的元素。 - -```java -// 获取链表的第一个元素 -public E getFirst() { - final Node f = first; - if (f == null) - throw new NoSuchElementException(); - return f.item; -} - -// 获取链表的最后一个元素 -public E getLast() { - final Node l = last; - if (l == null) - throw new NoSuchElementException(); - return l.item; -} - -// 获取链表指定位置的元素 -public E get(int index) { - // 下标越界检查,如果越界就抛异常 - checkElementIndex(index); - // 返回链表中对应下标的元素 - return node(index).item; -} -``` - -这里的核心在于 `node(int index)` 这个方法: - -```java -// 返回指定下标的非空节点 -Node node(int index) { - // 断言下标未越界 - // assert isElementIndex(index); - // 如果index小于size的二分之一 从前开始查找(向后查找) 反之向前查找 - if (index < (size >> 1)) { - Node x = first; - // 遍历,循环向后查找,直至 i == index - for (int i = 0; i < index; i++) - x = x.next; - return x; - } else { - Node x = last; - for (int i = size - 1; i > index; i--) - x = x.prev; - return x; - } -} -``` - -`get(int index)` 或 `remove(int index)` 等方法内部都调用了该方法来获取对应的节点。 - -从这个方法的源码可以看出,该方法通过比较索引值与链表 size 的一半大小来确定从链表头还是尾开始遍历。如果索引值小于 size 的一半,就从链表头开始遍历,反之从链表尾开始遍历。这样可以在较短的时间内找到目标节点,充分利用了双向链表的特性来提高效率。 - -### 删除元素 - -`LinkedList`删除元素相关的方法一共有 5 个: - -1. `removeFirst()`:删除并返回链表的第一个元素。 -2. `removeLast()`:删除并返回链表的最后一个元素。 -3. `remove(E e)`:删除链表中首次出现的指定元素,如果不存在该元素则返回 false。 -4. `remove(int index)`:删除指定索引处的元素,并返回该元素的值。 -5. `void clear()`:移除此链表中的所有元素。 - -```java -// 删除并返回链表的第一个元素 -public E removeFirst() { - final Node f = first; - if (f == null) - throw new NoSuchElementException(); - return unlinkFirst(f); -} - -// 删除并返回链表的最后一个元素 -public E removeLast() { - final Node l = last; - if (l == null) - throw new NoSuchElementException(); - return unlinkLast(l); -} - -// 删除链表中首次出现的指定元素,如果不存在该元素则返回 false -public boolean remove(Object o) { - // 如果指定元素为 null,遍历链表找到第一个为 null 的元素进行删除 - if (o == null) { - for (Node x = first; x != null; x = x.next) { - if (x.item == null) { - unlink(x); - return true; - } - } - } else { - // 如果不为 null ,遍历链表找到要删除的节点 - for (Node x = first; x != null; x = x.next) { - if (o.equals(x.item)) { - unlink(x); - return true; - } - } - } - return false; -} - -// 删除链表指定位置的元素 -public E remove(int index) { - // 下标越界检查,如果越界就抛异常 - checkElementIndex(index); - return unlink(node(index)); -} -``` - -这里的核心在于 `unlink(Node x)` 这个方法: - -```java -E unlink(Node x) { - // 断言 x 不为 null - // assert x != null; - // 获取当前节点(也就是待删除节点)的元素 - final E element = x.item; - // 获取当前节点的下一个节点 - final Node next = x.next; - // 获取当前节点的前一个节点 - final Node prev = x.prev; - - // 如果前一个节点为空,则说明当前节点是头节点 - if (prev == null) { - // 直接让链表头指向当前节点的下一个节点 - first = next; - } else { // 如果前一个节点不为空 - // 将前一个节点的 next 指针指向当前节点的下一个节点 - prev.next = next; - // 将当前节点的 prev 指针置为 null,,方便 GC 回收 - x.prev = null; - } - - // 如果下一个节点为空,则说明当前节点是尾节点 - if (next == null) { - // 直接让链表尾指向当前节点的前一个节点 - last = prev; - } else { // 如果下一个节点不为空 - // 将下一个节点的 prev 指针指向当前节点的前一个节点 - next.prev = prev; - // 将当前节点的 next 指针置为 null,方便 GC 回收 - x.next = null; - } - - // 将当前节点元素置为 null,方便 GC 回收 - x.item = null; - size--; - modCount++; - return element; -} -``` - -`unlink()` 方法的逻辑如下: - -1. 首先获取待删除节点 x 的前驱和后继节点; -2. 判断待删除节点是否为头节点或尾节点: - - 如果 x 是头节点,则将 first 指向 x 的后继节点 next - - 如果 x 是尾节点,则将 last 指向 x 的前驱节点 prev - - 如果 x 不是头节点也不是尾节点,执行下一步操作 -3. 将待删除节点 x 的前驱的后继指向待删除节点的后继 next,断开 x 和 x.prev 之间的链接; -4. 将待删除节点 x 的后继的前驱指向待删除节点的前驱 prev,断开 x 和 x.next 之间的链接; -5. 将待删除节点 x 的元素置空,修改链表长度。 - -可以参考下图理解(图源:[LinkedList 源码分析(JDK 1.8)](https://www.tianxiaobo.com/2018/01/31/LinkedList-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-JDK-1-8/)): - -![unlink 方法逻辑](https://oss.javaguide.cn/github/javaguide/java/collection/linkedlist-unlink.jpg) - -### 遍历链表 - -推荐使用`for-each` 循环来遍历 `LinkedList` 中的元素, `for-each` 循环最终会转换成迭代器形式。 - -```java -LinkedList list = new LinkedList<>(); -list.add("apple"); -list.add("banana"); -list.add("pear"); - -for (String fruit : list) { - System.out.println(fruit); -} -``` - -`LinkedList` 的遍历的核心就是它的迭代器的实现。 - -```java -// 双向迭代器 -private class ListItr implements ListIterator { - // 表示上一次调用 next() 或 previous() 方法时经过的节点; - private Node lastReturned; - // 表示下一个要遍历的节点; - private Node next; - // 表示下一个要遍历的节点的下标,也就是当前节点的后继节点的下标; - private int nextIndex; - // 表示当前遍历期望的修改计数值,用于和 LinkedList 的 modCount 比较,判断链表是否被其他线程修改过。 - private int expectedModCount = modCount; - ………… -} -``` - -下面我们对迭代器 `ListItr` 中的核心方法进行详细介绍。 - -我们先来看下从头到尾方向的迭代: - -```java -// 判断还有没有下一个节点 -public boolean hasNext() { - // 判断下一个节点的下标是否小于链表的大小,如果是则表示还有下一个元素可以遍历 - return nextIndex < size; -} -// 获取下一个节点 -public E next() { - // 检查在迭代过程中链表是否被修改过 - checkForComodification(); - // 判断是否还有下一个节点可以遍历,如果没有则抛出 NoSuchElementException 异常 - if (!hasNext()) - throw new NoSuchElementException(); - // 将 lastReturned 指向当前节点 - lastReturned = next; - // 将 next 指向下一个节点 - next = next.next; - nextIndex++; - return lastReturned.item; -} -``` - -再来看一下从尾到头方向的迭代: - -```java -// 判断是否还有前一个节点 -public boolean hasPrevious() { - return nextIndex > 0; -} - -// 获取前一个节点 -public E previous() { - // 检查是否在迭代过程中链表被修改 - checkForComodification(); - // 如果没有前一个节点,则抛出异常 - if (!hasPrevious()) - throw new NoSuchElementException(); - // 将 lastReturned 和 next 指针指向上一个节点 - lastReturned = next = (next == null) ? last : next.prev; - nextIndex--; - return lastReturned.item; -} -``` - -如果需要删除或插入元素,也可以使用迭代器进行操作。 - -```java -LinkedList list = new LinkedList<>(); -list.add("apple"); -list.add(null); -list.add("banana"); - -// Collection 接口的 removeIf 方法底层依然是基于迭代器 -list.removeIf(Objects::isNull); - -for (String fruit : list) { - System.out.println(fruit); -} -``` - -迭代器对应的移除元素的方法如下: - -```java -// 从列表中删除上次被返回的元素 -public void remove() { - // 检查是否在迭代过程中链表被修改 - checkForComodification(); - // 如果上次返回的节点为空,则抛出异常 - if (lastReturned == null) - throw new IllegalStateException(); - - // 获取当前节点的下一个节点 - Node lastNext = lastReturned.next; - // 从链表中删除上次返回的节点 - unlink(lastReturned); - // 修改指针 - if (next == lastReturned) - next = lastNext; - else - nextIndex--; - // 将上次返回的节点引用置为 null,方便 GC 回收 - lastReturned = null; - expectedModCount++; -} -``` - -## LinkedList 常用方法测试 - -代码: - -```java -// 创建 LinkedList 对象 -LinkedList list = new LinkedList<>(); - -// 添加元素到链表末尾 -list.add("apple"); -list.add("banana"); -list.add("pear"); -System.out.println("链表内容:" + list); - -// 在指定位置插入元素 -list.add(1, "orange"); -System.out.println("链表内容:" + list); - -// 获取指定位置的元素 -String fruit = list.get(2); -System.out.println("索引为 2 的元素:" + fruit); - -// 修改指定位置的元素 -list.set(3, "grape"); -System.out.println("链表内容:" + list); - -// 删除指定位置的元素 -list.remove(0); -System.out.println("链表内容:" + list); - -// 删除第一个出现的指定元素 -list.remove("banana"); -System.out.println("链表内容:" + list); - -// 获取链表的长度 -int size = list.size(); -System.out.println("链表长度:" + size); - -// 清空链表 -list.clear(); -System.out.println("清空后的链表:" + list); -``` - -输出: - -```plain -索引为 2 的元素:banana -链表内容:[apple, orange, banana, grape] -链表内容:[orange, banana, grape] -链表内容:[orange, grape] -链表长度:2 -清空后的链表:[] -``` - - +`LinkedList` has a no-argument constructor and a diff --git a/docs/java/collection/priorityqueue-source-code.md b/docs/java/collection/priorityqueue-source-code.md index b38cae9bcb9..db72bc3a37c 100644 --- a/docs/java/collection/priorityqueue-source-code.md +++ b/docs/java/collection/priorityqueue-source-code.md @@ -1,13 +1,13 @@ --- -title: PriorityQueue 源码分析(付费) +title: PriorityQueue Source Code Analysis (Paid) category: Java tag: - - Java集合 + - Java Collections --- -**PriorityQueue 源码分析** 为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 必读源码系列》](https://javaguide.cn/zhuanlan/source-code-reading.html)中。 +**PriorityQueue Source Code Analysis** is exclusive content for my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) (click the link for detailed introduction and joining method), and has been organized into the [“Must-Read Java Source Code Series”](https://javaguide.cn/zhuanlan/source-code-reading.html). -![PriorityQueue 源码分析](https://oss.javaguide.cn/xingqiu/image-20230727084055593.png) +![PriorityQueue Source Code Analysis](https://oss.javaguide.cn/xingqiu/image-20230727084055593.png) diff --git a/docs/java/concurrent/aqs.md b/docs/java/concurrent/aqs.md index c8e079d1a51..d04d524040a 100644 --- a/docs/java/concurrent/aqs.md +++ b/docs/java/concurrent/aqs.md @@ -1,1607 +1,61 @@ --- -title: AQS 详解 +title: AQS Explained category: Java tag: - - Java并发 + - Java Concurrency --- - +## Introduction to AQS -## AQS 介绍 - -AQS 的全称为 `AbstractQueuedSynchronizer` ,翻译过来的意思就是抽象队列同步器。这个类在 `java.util.concurrent.locks` 包下面。 +AQS stands for `AbstractQueuedSynchronizer`, which translates to "Abstract Queue Synchronizer." This class is located in the `java.util.concurrent.locks` package. ![](https://oss.javaguide.cn/github/javaguide/AQS.png) -AQS 就是一个抽象类,主要用来构建锁和同步器。 +AQS is an abstract class primarily used to build locks and synchronizers. ```java public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { } ``` -AQS 为构建锁和同步器提供了一些通用功能的实现。因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 `ReentrantLock`,`Semaphore`,其他的诸如 `ReentrantReadWriteLock`,`SynchronousQueue`等等皆是基于 AQS 的。 - -## AQS 原理 - -在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参考,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。 - -### AQS 快速了解 - -在真正讲解 AQS 源码之前,需要对 AQS 有一个整体层面的认识。这里会先通过几个问题,从整体层面上认识 AQS,了解 AQS 在整个 Java 并发中所位于的层面,之后在学习 AQS 源码的过程中,才能更加了解同步器和 AQS 之间的关系。 - -#### AQS 的作用是什么? - -AQS 解决了开发者在实现同步器时的复杂性问题。它提供了一个通用框架,用于实现各种同步器,例如 **可重入锁**(`ReentrantLock`)、**信号量**(`Semaphore`)和 **倒计时器**(`CountDownLatch`)。通过封装底层的线程同步机制,AQS 将复杂的线程管理逻辑隐藏起来,使开发者只需专注于具体的同步逻辑。 - -简单来说,AQS 是一个抽象类,为同步器提供了通用的 **执行框架**。它定义了 **资源获取和释放的通用流程**,而具体的资源获取逻辑则由具体同步器通过重写模板方法来实现。 因此,可以将 AQS 看作是同步器的 **基础“底座”**,而同步器则是基于 AQS 实现的 **具体“应用”**。 - -#### AQS 为什么使用 CLH 锁队列的变体? - -CLH 锁是一种基于 **自旋锁** 的优化实现。 - -先说一下自旋锁存在的问题:自旋锁通过线程不断对一个原子变量执行 `compareAndSet`(简称 `CAS`)操作来尝试获取锁。在高并发场景下,多个线程会同时竞争同一个原子变量,容易造成某个线程的 `CAS` 操作长时间失败,从而导致 **“饥饿”问题**(某些线程可能永远无法获取锁)。 - -CLH 锁通过引入一个队列来组织并发竞争的线程,对自旋锁进行了改进: - -- 每个线程会作为一个节点加入到队列中,并通过自旋监控前一个线程节点的状态,而不是直接竞争共享变量。 -- 线程按顺序排队,确保公平性,从而避免了 “饥饿” 问题。 - -AQS(AbstractQueuedSynchronizer)在 CLH 锁的基础上进一步优化,形成了其内部的 **CLH 队列变体**。主要改进点有以下两方面: - -1. **自旋 + 阻塞**: CLH 锁使用纯自旋方式等待锁的释放,但大量的自旋操作会占用过多的 CPU 资源。AQS 引入了 **自旋 + 阻塞** 的混合机制: - - 如果线程获取锁失败,会先短暂自旋尝试获取锁; - - 如果仍然失败,则线程会进入阻塞状态,等待被唤醒,从而减少 CPU 的浪费。 -2. **单向队列改为双向队列**:CLH 锁使用单向队列,节点只知道前驱节点的状态,而当某个节点释放锁时,需要通过队列唤醒后续节点。AQS 将队列改为 **双向队列**,新增了 `next` 指针,使得节点不仅知道前驱节点,也可以直接唤醒后继节点,从而简化了队列操作,提高了唤醒效率。 - -#### AQS 的性能比较好,原因是什么? - -因为 AQS 内部大量使用了 `CAS` 操作。 - -AQS 内部通过队列来存储等待的线程节点。由于队列是共享资源,在多线程场景下,需要保证队列的同步访问。 - -AQS 内部通过 `CAS` 操作来控制队列的同步访问,`CAS` 操作主要用于控制 `队列初始化` 、 `线程节点入队` 两个操作的并发安全。虽然利用 `CAS` 控制并发安全可以保证比较好的性能,但同时会带来比较高的 **编码复杂度** 。 - -#### AQS 中为什么 Node 节点需要不同的状态? - -AQS 中的 `waitStatus` 状态类似于 **状态机** ,通过不同状态来表明 Node 节点的不同含义,并且根据不同操作,来控制状态之间的流转。 - -- 状态 `0` :新节点加入队列之后,初始状态为 `0` 。 - -- 状态 `SIGNAL` :当有新的节点加入队列,此时新节点的前继节点状态就会由 `0` 更新为 `SIGNAL` ,表示前继节点释放锁之后,需要对新节点进行唤醒操作。如果唤醒 `SIGNAL` 状态节点的后续节点,就会将 `SIGNAL` 状态更新为 `0` 。即通过清除 `SIGNAL` 状态,表示已经执行了唤醒操作。 - -- 状态 `CANCELLED` :如果一个节点在队列中等待获取锁锁时,因为某种原因失败了,该节点的状态就会变为 `CANCELLED` ,表明取消获取锁,这种状态的节点是异常的,无法被唤醒,也无法唤醒后继节点。 - -### AQS 核心思想 - -AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 **CLH 锁** (Craig, Landin, and Hagersten locks) 进一步优化实现的。 - -**CLH 锁** 对自旋锁进行了改进,是基于单链表的自旋锁。在多线程场景下,会将请求获取锁的线程组织成一个单向队列,每个等待的线程会通过自旋访问前一个线程节点的状态,前一个节点释放锁之后,当前节点才可以获取锁。**CLH 锁** 的队列结构如下图所示。 - -![CLH 锁的队列结构](https://oss.javaguide.cn/github/javaguide/open-source-project/clh-lock-queue-structure.png) - -AQS 中使用的 **等待队列** 是 CLH 锁队列的变体(接下来简称为 CLH 变体队列)。 - -AQS 的 CLH 变体队列是一个双向队列,会暂时获取不到锁的线程将被加入到该队列中,CLH 变体队列和原本的 CLH 锁队列的区别主要有两点: - -- 由 **自旋** 优化为 **自旋 + 阻塞** :自旋操作的性能很高,但大量的自旋操作比较占用 CPU 资源,因此在 CLH 变体队列中会先通过自旋尝试获取锁,如果失败再进行阻塞等待。 -- 由 **单向队列** 优化为 **双向队列** :在 CLH 变体队列中,会对等待的线程进行阻塞操作,当队列前边的线程释放锁之后,需要对后边的线程进行唤醒,因此增加了 `next` 指针,成为了双向队列。 - -AQS 将每条请求共享资源的线程封装成一个 CLH 变体队列的一个结点(Node)来实现锁的分配。在 CLH 变体队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。 - -AQS 中的 CLH 变体队列结构如下图所示: - -![CLH 变体队列结构](https://oss.javaguide.cn/github/javaguide/java/concurrent/clh-queue-structure-bianti.png) - -关于 AQS 核心数据结构-CLH 锁的详细解读,强烈推荐阅读 [Java AQS 核心数据结构-CLH 锁 - Qunar 技术沙龙](https://mp.weixin.qq.com/s/jEx-4XhNGOFdCo4Nou5tqg) 这篇文章。 - -AQS(`AbstractQueuedSynchronizer`)的核心原理图: - -![CLH 变体队列](https://oss.javaguide.cn/github/javaguide/java/concurrent/clh-queue-state.png) - -AQS 使用 **int 成员变量 `state` 表示同步状态**,通过内置的 **FIFO 线程等待/等待队列** 来完成获取资源线程的排队工作。 - -`state` 变量由 `volatile` 修饰,用于展示当前临界资源的获取情况。 - -```java -// 共享变量,使用volatile修饰保证线程可见性 -private volatile int state; -``` - -另外,状态信息 `state` 可以通过 `protected` 类型的`getState()`、`setState()`和`compareAndSetState()` 进行操作。并且,这几个方法都是 `final` 修饰的,在子类中无法被重写。 - -```java -//返回同步状态的当前值 -protected final int getState() { - return state; -} - // 设置同步状态的值 -protected final void setState(int newState) { - state = newState; -} -//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) -protected final boolean compareAndSetState(int expect, int update) { - return unsafe.compareAndSwapInt(this, stateOffset, expect, update); -} -``` - -以可重入的互斥锁 `ReentrantLock` 为例,它的内部维护了一个 `state` 变量,用来表示锁的占用状态。`state` 的初始值为 0,表示锁处于未锁定状态。当线程 A 调用 `lock()` 方法时,会尝试通过 `tryAcquire()` 方法独占该锁,并让 `state` 的值加 1。如果成功了,那么线程 A 就获取到了锁。如果失败了,那么线程 A 就会被加入到一个等待队列(CLH 变体队列)中,直到其他线程释放该锁。假设线程 A 获取锁成功了,释放锁之前,A 线程自己是可以重复获取此锁的(`state` 会累加)。这就是可重入性的体现:一个线程可以多次获取同一个锁而不会被阻塞。但是,这也意味着,一个线程必须释放与获取的次数相同的锁,才能让 `state` 的值回到 0,也就是让锁恢复到未锁定状态。只有这样,其他等待的线程才能有机会获取该锁。 - -线程 A 尝试获取锁的过程如下图所示(图源[从 ReentrantLock 的实现看 AQS 的原理及应用 - 美团技术团队](./reentrantlock.md)): - -![AQS 独占模式获取锁](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-exclusive-mode-acquire-lock.png) - -再以倒计时器 `CountDownLatch` 以例,任务分为 N 个子线程去执行,`state` 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程开始执行任务,每执行完一个子线程,就调用一次 `countDown()` 方法。该方法会尝试使用 CAS(Compare and Swap) 操作,让 `state` 的值减少 1。当所有的子线程都执行完毕后(即 `state` 的值变为 0),`CountDownLatch` 会调用 `unpark()` 方法,唤醒主线程。这时,主线程就可以从 `await()` 方法(`CountDownLatch` 中的`await()` 方法而非 AQS 中的)返回,继续执行后续的操作。 - -### Node 节点 waitStatus 状态含义 - -AQS 中的 `waitStatus` 状态类似于 **状态机** ,通过不同状态来表明 Node 节点的不同含义,并且根据不同操作,来控制状态之间的流转。 - -| Node 节点状态 | 值 | 含义 | -| ------------- | --- | ------------------------------------------------------------------------------------------------------------------------- | -| `CANCELLED` | 1 | 表示线程已经取消获取锁。线程在等待获取资源时被中断、等待资源超时会更新为该状态。 | -| `SIGNAL` | -1 | 表示后继节点需要当前节点唤醒。在当前线程节点释放锁之后,需要对后继节点进行唤醒。 | -| `CONDITION` | -2 | 表示节点在等待 Condition。当其他线程调用了 Condition 的 `signal()` 方法后,节点会从等待队列转移到同步队列中等待获取资源。 | -| `PROPAGATE` | -3 | 用于共享模式。在共享模式下,可能会出现线程在队列中无法被唤醒的情况,因此引入了 `PROPAGATE` 状态来解决这个问题。 | -| | 0 | 加入队列的新节点的初始状态。 | - -在 AQS 的源码中,经常使用 `> 0` 、 `< 0` 来对 `waitStatus` 进行判断。 - -如果 `waitStatus > 0` ,表明节点的状态已经取消等待获取资源。 - -如果 `waitStatus < 0` ,表明节点的状态处于正常的状态,即没有取消等待。 - -其中 `SIGNAL` 状态是最重要的,节点状态流转以及对应操作如下: - -| 状态流转 | 对应操作 | -| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `0` | 新节点入队时,初始状态为 `0` 。 | -| `0 -> SIGNAL` | 新节点入队时,它的前继节点状态会由 `0` 更新为 `SIGNAL` 。`SIGNAL` 状态表明该节点的后续节点需要被唤醒。 | -| `SIGNAL -> 0` | 在唤醒后继节点时,需要清除当前节点的状态。通常发生在 `head` 节点,比如 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,表示已经对 `head` 节点的后继节点唤醒了。 | -| `0 -> PROPAGATE` | AQS 内部引入了 `PROPAGATE` 状态,为了解决并发场景下,可能造成的线程节点无法唤醒的情况。(在 AQS 共享模式获取资源的源码分析会讲到) | - -### 自定义同步器 - -基于 AQS 可以实现自定义的同步器, AQS 提供了 5 个模板方法(模板方法模式)。如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): - -1. 自定义的同步器继承 `AbstractQueuedSynchronizer` 。 -2. 重写 AQS 暴露的模板方法。 - -**AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法:** - -```java -//独占方式。尝试获取资源,成功则返回true,失败则返回false。 -protected boolean tryAcquire(int) -//独占方式。尝试释放资源,成功则返回true,失败则返回false。 -protected boolean tryRelease(int) -//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 -protected int tryAcquireShared(int) -//共享方式。尝试释放资源,成功则返回true,失败则返回false。 -protected boolean tryReleaseShared(int) -//该线程是否正在独占资源。只有用到condition才需要去实现它。 -protected boolean isHeldExclusively() -``` - -**什么是钩子方法呢?** 钩子方法是一种被声明在抽象类中的方法,一般使用 `protected` 关键字修饰,它可以是空方法(由子类实现),也可以是默认实现的方法。模板设计模式通过钩子方法控制固定步骤的实现。 - -篇幅问题,这里就不详细介绍模板方法模式了,不太了解的小伙伴可以看看这篇文章:[用 Java8 改造后的模板方法模式真的是 yyds!](https://mp.weixin.qq.com/s/zpScSCktFpnSWHWIQem2jg)。 - -除了上面提到的钩子方法之外,AQS 类中的其他方法都是 `final` ,所以无法被其他类重写。 - -### AQS 资源共享方式 - -AQS 定义两种资源共享方式:`Exclusive`(独占,只有一个线程能执行,如`ReentrantLock`)和`Share`(共享,多个线程可同时执行,如`Semaphore`/`CountDownLatch`)。 - -一般来说,自定义同步器的共享方式要么是独占,要么是共享,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。 - -### AQS 资源获取源码分析(独占模式) - -AQS 中以独占模式获取资源的入口方法是 `acquire()` ,如下: - -```JAVA -// AQS -public final void acquire(int arg) { - if (!tryAcquire(arg) && - acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) - selfInterrupt(); -} -``` - -在 `acquire()` 中,线程会先尝试获取共享资源;如果获取失败,会将线程封装为 Node 节点加入到 AQS 的等待队列中;加入队列之后,会让等待队列中的线程尝试获取资源,并且会对线程进行阻塞操作。分别对应以下三个方法: - -- `tryAcquire()` :尝试获取锁(模板方法),`AQS` 不提供具体实现,由子类实现。 -- `addWaiter()` :如果获取锁失败,会将当前线程封装为 Node 节点加入到 AQS 的 CLH 变体队列中等待获取锁。 -- `acquireQueued()` :对线程进行阻塞,并调用 `tryAcquire()` 方法让队列中的线程尝试获取锁。 - -#### `tryAcquire()` 分析 - -AQS 中对应的 `tryAcquire()` 模板方法如下: - -```JAVA -// AQS -protected boolean tryAcquire(int arg) { - throw new UnsupportedOperationException(); -} -``` - -`tryAcquire()` 方法是 AQS 提供的模板方法,不提供默认实现。 - -因此,这里分析 `tryAcquire()` 方法时,以 `ReentrantLock` 的非公平锁(独占锁)为例进行分析,`ReentrantLock` 内部实现的 `tryAcquire()` 会调用到下边的 `nonfairTryAcquire()` : - -```JAVA -// ReentrantLock -final boolean nonfairTryAcquire(int acquires) { - final Thread current = Thread.currentThread(); - // 1、获取 AQS 中的 state 状态 - int c = getState(); - // 2、如果 state 为 0,证明锁没有被其他线程占用 - if (c == 0) { - // 2.1、通过 CAS 对 state 进行更新 - if (compareAndSetState(0, acquires)) { - // 2.2、如果 CAS 更新成功,就将锁的持有者设置为当前线程 - setExclusiveOwnerThread(current); - return true; - } - } - // 3、如果当前线程和锁的持有线程相同,说明发生了「锁的重入」 - else if (current == getExclusiveOwnerThread()) { - int nextc = c + acquires; - if (nextc < 0) // overflow - throw new Error("Maximum lock count exceeded"); - // 3.1、将锁的重入次数加 1 - setState(nextc); - return true; - } - // 4、如果锁被其他线程占用,就返回 false,表示获取锁失败 - return false; -} -``` - -在 `nonfairTryAcquire()` 方法内部,主要通过两个核心操作去完成资源的获取: - -- 通过 `CAS` 更新 `state` 变量。`state == 0` 表示资源没有被占用。`state > 0` 表示资源被占用,此时 `state` 表示重入次数。 -- 通过 `setExclusiveOwnerThread()` 设置持有资源的线程。 - -如果线程更新 `state` 变量成功,就表明获取到了资源, 因此将持有资源的线程设置为当前线程即可。 - -#### `addWaiter()` 分析 - -在通过 `tryAcquire()` 方法尝试获取资源失败之后,会调用 `addWaiter()` 方法将当前线程封装为 Node 节点加入 `AQS` 内部的队列中。`addWaite()` 代码如下: - -```JAVA -// AQS -private Node addWaiter(Node mode) { - // 1、将当前线程封装为 Node 节点。 - Node node = new Node(Thread.currentThread(), mode); - Node pred = tail; - // 2、如果 pred != null,则证明 tail 节点已经被初始化,直接将 Node 节点加入队列即可。 - if (pred != null) { - node.prev = pred; - // 2.1、通过 CAS 控制并发安全。 - if (compareAndSetTail(pred, node)) { - pred.next = node; - return node; - } - } - // 3、初始化队列,并将新创建的 Node 节点加入队列。 - enq(node); - return node; -} -``` - -**节点入队的并发安全:** - -在 `addWaiter()` 方法中,需要执行 Node 节点 **入队** 的操作。由于是在多线程环境下,因此需要通过 `CAS` 操作保证并发安全。 - -通过 `CAS` 操作去更新 `tail` 指针指向新入队的 Node 节点,`CAS` 可以保证只有一个线程会成功修改 `tail` 指针,以此来保证 Node 节点入队时的并发安全。 - -**AQS 内部队列的初始化:** - -在执行 `addWaiter()` 时,如果发现 `pred == null` ,即 `tail` 指针为 null,则证明队列没有初始化,需要调用 `enq()` 方法初始化队列,并将 `Node` 节点加入到初始化后的队列中,代码如下: - -```JAVA -// AQS -private Node enq(final Node node) { - for (;;) { - Node t = tail; - if (t == null) { - // 1、通过 CAS 操作保证队列初始化的并发安全 - if (compareAndSetHead(new Node())) - tail = head; - } else { - // 2、与 addWaiter() 方法中节点入队的操作相同 - node.prev = t; - if (compareAndSetTail(t, node)) { - t.next = node; - return t; - } - } - } -} -``` - -在 `enq()` 方法中初始化队列,在初始化过程中,也需要通过 `CAS` 来保证并发安全。 - -初始化队列总共包含两个步骤:初始化 `head` 节点、`tail` 指向 `head` 节点。 - -**初始化后的队列如下图所示:** - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/clh-queue-structure-init.png) - -#### `acquireQueued()` 分析 - -为了方便阅读,这里再贴一下 `AQS` 中 `acquire()` 获取资源的代码: - -```JAVA -// AQS -public final void acquire(int arg) { - if (!tryAcquire(arg) && - acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) - selfInterrupt(); -} -``` - -在 `acquire()` 方法中,通过 `addWaiter()` 方法将 `Node` 节点加入队列之后,就会调用 `acquireQueued()` 方法。代码如下: - -```JAVA -// AQS:令队列中的节点尝试获取锁,并且对线程进行阻塞。 -final boolean acquireQueued(final Node node, int arg) { - boolean failed = true; - try { - boolean interrupted = false; - for (;;) { - // 1、尝试获取锁。 - final Node p = node.predecessor(); - if (p == head && tryAcquire(arg)) { - setHead(node); - p.next = null; // help GC - failed = false; - return interrupted; - } - // 2、判断线程是否可以阻塞,如果可以,则阻塞当前线程。 - if (shouldParkAfterFailedAcquire(p, node) && - parkAndCheckInterrupt()) - interrupted = true; - } - } finally { - // 3、如果获取锁失败,就会取消获取锁,将节点状态更新为 CANCELLED。 - if (failed) - cancelAcquire(node); - } -} -``` - -在 `acquireQueued()` 方法中,主要做两件事情: - -- **尝试获取资源:** 当前线程加入队列之后,如果发现前继节点是 `head` 节点,说明当前线程是队列中第一个等待的节点,于是调用 `tryAcquire()` 尝试获取资源。 - -- **阻塞当前线程** :如果尝试获取资源失败,就需要阻塞当前线程,等待被唤醒之后获取资源。 - -**1、尝试获取资源** - -在 `acquireQueued()` 方法中,尝试获取资源总共有 2 个步骤: +AQS provides some common functionalities for building locks and synchronizers. Therefore, using AQS allows for the simple and efficient construction of a wide range of synchronizers, such as `ReentrantLock`, `Semaphore`, and others like `ReentrantReadWriteLock`, `SynchronousQueue`, etc., all of which are based on AQS. -- `p == head` :表明当前节点的前继节点为 `head` 节点。此时当前节点为 AQS 队列中的第一个等待节点。 -- `tryAcquire(arg) == true` :表明当前线程尝试获取资源成功。 +## Principles of AQS -在成功获取资源之后,就需要将当前线程的节点 **从等待队列中移除** 。移除操作为:将当前等待的线程节点设置为 `head` 节点(`head` 节点是虚拟节点,并不参与排队获取资源)。 +In interviews, when asked about concurrency knowledge, most candidates will be asked, "Can you explain your understanding of the principles of AQS?" Below is an example for reference. Interviews are not about rote memorization; you should incorporate your own thoughts. Even if you can't add your own ideas, ensure you can explain it in simple terms rather than just reciting it. -**2、阻塞当前线程** +### Quick Overview of AQS -在 `AQS` 中,当前节点的唤醒需要依赖于上一个节点。如果上一个节点取消获取锁,它的状态就会变为 `CANCELLED` ,`CANCELLED` 状态的节点没有获取到锁,也就无法执行解锁操作对当前节点进行唤醒。因此在阻塞当前线程之前,需要跳过 `CANCELLED` 状态的节点。 - -通过 `shouldParkAfterFailedAcquire()` 方法来判断当前线程节点是否可以阻塞,如下: - -```JAVA -// AQS:判断当前线程节点是否可以阻塞。 -private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { - int ws = pred.waitStatus; - // 1、前继节点状态正常,直接返回 true 即可。 - if (ws == Node.SIGNAL) - return true; - // 2、ws > 0 表示前继节点的状态异常,即为 CANCELLED 状态,需要跳过异常状态的节点。 - if (ws > 0) { - do { - node.prev = pred = pred.prev; - } while (pred.waitStatus > 0); - pred.next = node; - } else { - // 3、如果前继节点的状态不是 SIGNAL,也不是 CANCELLED,就将状态设置为 SIGNAL。 - compareAndSetWaitStatus(pred, ws, Node.SIGNAL); - } - return false; -} -``` - -`shouldParkAfterFailedAcquire()` 方法中的判断逻辑: - -- 如果发现前继节点的状态是 `SIGNAL` ,则可以阻塞当前线程。 -- 如果发现前继节点的状态是 `CANCELLED` ,则需要跳过 `CANCELLED` 状态的节点。 -- 如果发现前继节点的状态不是 `SIGNAL` 和 `CANCELLED` ,表明前继节点的状态处于正常等待资源的状态,因此将前继节点的状态设置为 `SIGNAL` ,表明该前继节点需要对后续节点进行唤醒。 - -当判断当前线程可以阻塞之后,通过调用 `parkAndCheckInterrupt()` 方法来阻塞当前线程。内部使用了 `LockSupport` 来实现阻塞。`LockSupoprt` 底层是基于 `Unsafe` 类来阻塞线程,代码如下: - -```JAVA -// AQS -private final boolean parkAndCheckInterrupt() { - // 1、线程阻塞到这里 - LockSupport.park(this); - // 2、线程被唤醒之后,返回线程中断状态 - return Thread.interrupted(); -} -``` - -**为什么在线程被唤醒之后,要返回线程的中断状态呢?** - -在 `parkAndCheckInterrupt()` 方法中,当执行完 `LockSupport.park(this)` ,线程会被阻塞,代码如下: - -```JAVA -// AQS -private final boolean parkAndCheckInterrupt() { - LockSupport.park(this); - // 线程被唤醒之后,需要返回线程中断状态 - return Thread.interrupted(); -} -``` - -当线程被唤醒之后,需要执行 `Thread.interrupted()` 来返回线程的中断状态,这是为什么呢? - -这个和线程的中断协作机制有关系,线程被唤醒之后,并不确定是被中断唤醒,还是被 `LockSupport.unpark()` 唤醒,因此需要通过线程的中断状态来判断。 - -**在 `acquire()` 方法中,为什么需要调用 `selfInterrupt()` ?** - -`acquire()` 方法代码如下: - -```JAVA -// AQS -public final void acquire(int arg) { - if (!tryAcquire(arg) && - acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) - selfInterrupt(); -} -``` - -在 `acquire()` 方法中,当 `if` 语句的条件返回 `true` 后,就会调用 `selfInterrupt()` ,该方法会中断当前线程,为什么需要中断当前线程呢? - -当 `if` 判断为 `true` 时,需要 `tryAcquire()` 返回 `false` ,并且 `acquireQueued()` 返回 `true` 。 - -其中 `acquireQueued()` 方法返回的是线程被唤醒之后的 **中断状态** ,通过执行 `Thread.interrupted()` 来返回。该方法在返回中断状态的同时,会清除线程的中断状态。 - -因此如果 `if` 判断为 `true` ,表明线程的中断状态为 `true` ,但是调用 `Thread.interrupted()` 之后,线程的中断状态被清除为 `false` ,因此需要重新执行 `selfInterrupt()` 来重新设置线程的中断状态。 - -### AQS 资源释放源码分析(独占模式) - -AQS 中以独占模式释放资源的入口方法是 `release()` ,代码如下: - -```JAVA -// AQS -public final boolean release(int arg) { - // 1、尝试释放锁 - if (tryRelease(arg)) { - Node h = head; - // 2、唤醒后继节点 - if (h != null && h.waitStatus != 0) - unparkSuccessor(h); - return true; - } - return false; -} -``` - -在 `release()` 方法中,主要做两件事:尝试释放锁和唤醒后继节点。对应方法如下: - -**1、尝试释放锁** - -通过 `tryRelease()` 方法尝试释放锁,该方法为模板方法,由自定义同步器实现,因此这里仍然以 `ReentrantLock` 为例来讲解。 - -`ReentrantLock` 中实现的 `tryRelease()` 方法如下: - -```JAVA -// ReentrantLock -protected final boolean tryRelease(int releases) { - int c = getState() - releases; - // 1、判断持有锁的线程是否为当前线程 - if (Thread.currentThread() != getExclusiveOwnerThread()) - throw new IllegalMonitorStateException(); - boolean free = false; - // 2、如果 state 为 0,则表明当前线程已经没有重入次数。因此将 free 更新为 true,表明该线程会释放锁。 - if (c == 0) { - free = true; - // 3、更新持有资源的线程为 null - setExclusiveOwnerThread(null); - } - // 4、更新 state 值 - setState(c); - return free; -} -``` - -在 `tryRelease()` 方法中,会先计算释放锁之后的 `state` 值,判断 `state` 值是否为 0。 - -- 如果 `state == 0` ,表明该线程没有重入次数了,更新 `free = true` ,并修改持有资源的线程为 null,表明该线程完全释放这把锁。 -- 如果 `state != 0` ,表明该线程还存在重入次数,因此不更新 `free` 值,`free` 值为 `false` 表明该线程没有完全释放这把锁。 - -之后更新 `state` 值,并返回 `free` 值,`free` 值表明线程是否完全释放锁。 - -**2、唤醒后继节点** - -如果 `tryRelease()` 返回 `true` ,表明线程已经没有重入次数了,锁已经被完全释放,因此需要唤醒后继节点。 - -在唤醒后继节点之前,需要判断是否可以唤醒后继节点,判断条件为: `h != null && h.waitStatus != 0` 。这里解释一下为什么要这样判断: - -- `h == null` :表明 `head` 节点还没有被初始化,也就是 AQS 中的队列没有被初始化,因此无法唤醒队列中的线程节点。 -- `h != null && h.waitStatus == 0` :表明头节点刚刚初始化完毕(节点的初始化状态为 0),后继节点线程还没有成功入队,因此不需要对后续节点进行唤醒。(当后继节点入队之后,会将前继节点的状态修改为 `SIGNAL` ,表明需要对后继节点进行唤醒) -- `h != null && h.waitStatus != 0` :其中 `waitStatus` 有可能大于 0,也有可能小于 0。其中 `> 0` 表明节点已经取消等待获取资源,`< 0` 表明节点处于正常等待状态。 - -接下来进入 `unparkSuccessor()` 方法查看如何唤醒后继节点: - -```JAVA -// AQS:这里的入参 node 为队列的头节点(虚拟头节点) -private void unparkSuccessor(Node node) { - int ws = node.waitStatus; - // 1、将头节点的状态进行清除,为后续的唤醒做准备。 - if (ws < 0) - compareAndSetWaitStatus(node, ws, 0); - - Node s = node.next; - // 2、如果后继节点异常,则需要从 tail 向前遍历,找到正常状态的节点进行唤醒。 - if (s == null || s.waitStatus > 0) { - s = null; - for (Node t = tail; t != null && t != node; t = t.prev) - if (t.waitStatus <= 0) - s = t; - } - if (s != null) - // 3、唤醒后继节点 - LockSupport.unpark(s.thread); -} -``` - -在 `unparkSuccessor()` 中,如果头节点的状态 `< 0` (在正常情况下,只要有后继节点,头节点的状态应该为 `SIGNAL` ,即 -1),表示需要对后继节点进行唤醒,因此这里提前清除头节点的状态标识,将状态修改为 0,表示已经执行了对后续节点唤醒的操作。 - -如果 `s == null` 或者 `s.waitStatus > 0` ,表明后继节点异常,此时不能唤醒异常节点,而是要找到正常状态的节点进行唤醒。 - -因此需要从 `tail` 指针向前遍历,来找到第一个状态正常(`waitStatus <= 0`)的节点进行唤醒。 - -**为什么要从 `tail` 指针向前遍历,而不是从 `head` 指针向后遍历,寻找正常状态的节点呢?** - -遍历的方向和 **节点的入队操作** 有关。入队方法如下: - -```JAVA -// AQS:节点入队方法 -private Node addWaiter(Node mode) { - Node node = new Node(Thread.currentThread(), mode); - Node pred = tail; - if (pred != null) { - // 1、先修改 prev 指针。 - node.prev = pred; - if (compareAndSetTail(pred, node)) { - // 2、再修改 next 指针。 - pred.next = node; - return node; - } - } - enq(node); - return node; -} -``` - -在 `addWaiter()` 方法中,`node` 节点入队需要修改 `node.prev` 和 `pred.next` 两个指针,但是这两个操作并不是 **原子操作** ,先修改了 `node.prev` 指针,之后才修改 `pred.next` 指针。 - -在极端情况下,可能会出现 `head` 节点的下一个节点状态为 `CANCELLED` ,此时新入队的节点仅更新了 `node.prev` 指针,还未更新 `pred.next` 指针,如下图: - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-addWaiter.png) - -这样如果从 `head` 指针向后遍历,无法找到新入队的节点,因此需要从 `tail` 指针向前遍历找到新入队的节点。 - -### 图解 AQS 工作原理(独占模式) - -至此,AQS 中以独占模式获取资源、释放资源的源码就讲完了。为了对 AQS 的工作原理、节点状态变化有一个更加清晰的认识,接下来会通过画图的方式来了解整个 AQS 的工作原理。 - -由于 AQS 是底层同步工具,获取和释放资源的方法并没有提供具体实现,因此这里基于 `ReentrantLock` 来画图进行讲解。 - -假设总共有 3 个线程尝试获取锁,线程分别为 `T1` 、 `T2` 和 `T3` 。 - -此时,假设线程 `T1` 先获取到锁,线程 `T2` 排队等待获取锁。在线程 `T2` 进入队列之前,需要对 AQS 内部队列进行初始化。`head` 节点在初始化后状态为 `0` 。AQS 内部初始化后的队列如下图: - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process.png) - -此时,线程 `T2` 尝试获取锁。由于线程 `T1` 持有锁,因此线程 `T2` 会进入队列中等待获取锁。同时会将前继节点( `head` 节点)的状态由 `0` 更新为 `SIGNAL` ,表示需要对 `head` 节点的后继节点进行唤醒。此时,AQS 内部队列如下图所示: - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process-2.png) - -此时,线程 `T3` 尝试获取锁。由于线程 `T1` 持有锁,因此线程 `T3` 会进入队列中等待获取锁。同时会将前继节点(线程 `T2` 节点)的状态由 `0` 更新为 `SIGNAL` ,表示线程 `T2` 节点需要对后继节点进行唤醒。此时,AQS 内部队列如下图所示: - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process-3.png) - -此时,假设线程 `T1` 释放锁,会唤醒后继节点 `T2` 。线程 `T2` 被唤醒后获取到锁,并且会从等待队列中退出。 - -这里线程 `T2` 节点退出等待队列并不是直接从队列移除,而是令线程 `T2` 节点成为新的 `head` 节点,以此来退出资源获取的等待。此时 AQS 内部队列如下所示: - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process-4.png) - -此时,假设线程 `T2` 释放锁,会唤醒后继节点 `T3` 。线程 `T3` 获取到锁之后,同样也退出等待队列,即将线程 `T3` 节点变为 `head` 节点来退出资源获取的等待。此时 AQS 内部队列如下所示: - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process-5.png) - -### AQS 资源获取源码分析(共享模式) - -AQS 中以独占模式获取资源的入口方法是 `acquireShared()` ,如下: - -```JAVA -// AQS -public final void acquireShared(int arg) { - if (tryAcquireShared(arg) < 0) - doAcquireShared(arg); -} -``` - -在 `acquireShared()` 方法中,会先尝试获取共享锁,如果获取失败,则将当前线程加入到队列中阻塞,等待唤醒后尝试获取共享锁,分别对应一下两个方法:`tryAcquireShared()` 和 `doAcquireShared()` 。 - -其中 `tryAcquireShared()` 方法是 AQS 提供的模板方法,由同步器来实现具体逻辑。因此这里以 `Semaphore` 为例,来分析共享模式下,如何获取资源。 - -#### `tryAcquireShared()` 分析 - -`Semaphore` 中实现了公平锁和非公平锁,接下来以非公平锁为例来分析 `tryAcquireShared()` 源码。 - -`Semaphore` 中重写的 `tryAcquireShared()` 方法会调用下边的 `nonfairTryAcquireShared()` 方法: - -```JAVA -// Semaphore 重写 AQS 的模板方法 -protected int tryAcquireShared(int acquires) { - return nonfairTryAcquireShared(acquires); -} - -// Semaphore -final int nonfairTryAcquireShared(int acquires) { - for (;;) { - // 1、获取可用资源数量。 - int available = getState(); - // 2、计算剩余资源数量。 - int remaining = available - acquires; - // 3、如果剩余资源数量 < 0,则说明资源不足,直接返回;如果 CAS 更新 state 成功,则说明当前线程获取到了共享资源,直接返回。 - if (remaining < 0 || - compareAndSetState(available, remaining)) - return remaining; - } -} -``` - -在共享模式下,AQS 中的 `state` 值表示共享资源的数量。 - -在 `nonfairTryAcquireShared()` 方法中,会在死循环中不断尝试获取资源,如果 「剩余资源数不足」 或者 「当前线程成功获取资源」 ,就退出死循环。方法返回 **剩余的资源数量** ,根据返回值的不同,分为 3 种情况: - -- **剩余资源数量 > 0** :表示成功获取资源,并且后续的线程也可以成功获取资源。 -- **剩余资源数量 = 0** :表示成功获取资源,但是后续的线程无法成功获取资源。 -- **剩余资源数量 < 0** :表示获取资源失败。 - -#### `doAcquireShared()` 分析 - -为了方便阅读,这里再贴一下获取资源的入口方法 `acquireShared()` : - -```JAVA -// AQS -public final void acquireShared(int arg) { - if (tryAcquireShared(arg) < 0) - doAcquireShared(arg); -} -``` - -在 `acquireShared()` 方法中,会先通过 `tryAcquireShared()` 尝试获取资源。 - -如果发现方法的返回值 `< 0` ,即剩余的资源数小于 0,则表明当前线程获取资源失败。因此会进入 `doAcquireShared()` 方法,将当前线程加入到 AQS 队列进行等待。如下: - -```JAVA -// AQS -private void doAcquireShared(int arg) { - // 1、将当前线程加入到队列中等待。 - final Node node = addWaiter(Node.SHARED); - boolean failed = true; - try { - boolean interrupted = false; - for (;;) { - final Node p = node.predecessor(); - if (p == head) { - // 2、如果当前线程是等待队列的第一个节点,则尝试获取资源。 - int r = tryAcquireShared(arg); - if (r >= 0) { - // 3、将当前线程节点移出等待队列,并唤醒后续线程节点。 - setHeadAndPropagate(node, r); - p.next = null; // help GC - if (interrupted) - selfInterrupt(); - failed = false; - return; - } - } - if (shouldParkAfterFailedAcquire(p, node) && - parkAndCheckInterrupt()) - interrupted = true; - } - } finally { - // 3、如果获取资源失败,就会取消获取资源,将节点状态更新为 CANCELLED。 - if (failed) - cancelAcquire(node); - } -} -``` - -由于当前线程已经尝试获取资源失败了,因此在 `doAcquireShared()` 方法中,需要将当前线程封装为 Node 节点,加入到队列中进行等待。 - -以 **共享模式** 获取资源和 **独占模式** 获取资源最大的不同之处在于:共享模式下,资源的数量可能会大于 1,即可以多个线程同时持有资源。 - -因此在共享模式下,当线程线程被唤醒之后,获取到了资源,如果发现还存在剩余资源,就会尝试唤醒后边的线程去尝试获取资源。对应的 `setHeadAndPropagate()` 方法如下: - -```JAVA -// AQS -private void setHeadAndPropagate(Node node, int propagate) { - Node h = head; - // 1、将当前线程节点移出等待队列。 - setHead(node); - // 2、唤醒后续等待节点。 - if (propagate > 0 || h == null || h.waitStatus < 0 || - (h = head) == null || h.waitStatus < 0) { - Node s = node.next; - if (s == null || s.isShared()) - doReleaseShared(); - } -} -``` - -在 `setHeadAndPropagate()` 方法中,唤醒后续节点需要满足一定的条件,主要需要满足 2 个条件: - -- `propagate > 0` :`propagate` 代表获取资源之后剩余的资源数量,如果 `> 0` ,则可以唤醒后续线程去获取资源。 -- `h.waitStatus < 0` :这里的 `h` 节点是执行 `setHead()` 之前的 `head` 节点。判断 `head.waitStatus` 时使用 `< 0` ,主要为了确定 `head` 节点的状态为 `SIGNAL` 或 `PROPAGATE` 。如果 `head` 节点为 `SIGNAL` ,则可以唤醒后续节点;如果 `head` 节点状态为 `PROPAGATE` ,也可以唤醒后续节点(这是为了解决并发场景下出现的问题,后续会细讲)。 - -代码中关于 **唤醒后续等待节点** 的 `if` 判断稍微复杂一些,这里来讲一下为什么这样写: - -```JAVA -if (propagate > 0 || h == null || h.waitStatus < 0 || - (h = head) == null || h.waitStatus < 0) -``` - -- `h == null || h.waitStatus < 0` : `h == null` 用于防止空指针异常。正常情况下 h 不会为 `null` ,因为执行到这里之前,当前节点已经加入到队列中了,队列不可能还没有初始化。 - - `h.waitStatus < 0` 主要判断 `head` 节点的状态是否为 `SIGNAL` 或者 `PROPAGATE` ,直接使用 `< 0` 来判断比较方便。 - -- `(h = head) == null || h.waitStatus < 0` :如果到这里说明之前判断的 `h.waitStatus < 0` ,说明存在并发。 - - 同时存在其他线程在唤醒后续节点,已经将 `head` 节点的值由 `SIGNAL` 修改为 `0` 了。因此,这里重新获取新的 `head` 节点,这次获取的 `head` 节点为通过 `setHead()` 设置的当前线程节点,之后再次判断 `waitStatus` 状态。 - -如果 `if` 条件判断通过,就会走到 `doReleaseShared()` 方法唤醒后续等待节点,如下: - -```JAVA -private void doReleaseShared() { - for (;;) { - Node h = head; - // 1、队列中至少需要一个等待的线程节点。 - if (h != null && h != tail) { - int ws = h.waitStatus; - // 2、如果 head 节点的状态为 SIGNAL,则可以唤醒后继节点。 - if (ws == Node.SIGNAL) { - // 2.1 清除 head 节点的 SIGNAL 状态,更新为 0。表示已经唤醒该节点的后继节点了。 - if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) - continue; - // 2.2 唤醒后继节点 - unparkSuccessor(h); - } - // 3、如果 head 节点的状态为 0,则更新为 PROPAGATE。这是为了解决并发场景下存在的问题,接下来会细讲。 - else if (ws == 0 && - !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) - continue; - } - if (h == head) - break; - } -} -``` +Before diving into the AQS source code, it's essential to have an overall understanding of AQS. Here, we will first recognize AQS from a broader perspective through several questions, understanding its position in the entire Java concurrency framework. This will help in better understanding the relationship between synchronizers and AQS during the study of AQS source code. -在 `doReleaseShared()` 方法中,会判断 `head` 节点的 `waitStatus` 状态来决定接下来的操作,有两种情况: +#### What is the role of AQS? -- `head` 节点的状态为 `SIGNAL` :表明 `head` 节点存在后继节点需要唤醒,因此通过 `CAS` 操作将 `head` 节点的 `SIGNAL` 状态更新为 `0` 。通过清除 `SIGNAL` 状态来表示已经对 `head` 节点的后继节点进行唤醒操作了。 -- `head` 节点的状态为 `0` :表明存在并发情况,需要将 `0` 修改为 `PROPAGATE` 来保证在并发场景下可以正常唤醒线程。 +AQS addresses the complexity problem developers face when implementing synchronizers. It provides a general framework for implementing various synchronizers, such as **Reentrant Lock** (`ReentrantLock`), **Semaphore** (`Semaphore`), and **CountDownLatch** (`CountDownLatch`). By encapsulating the underlying thread synchronization mechanisms, AQS hides the complex thread management logic, allowing developers to focus solely on the specific synchronization logic. -#### 为什么需要 `PROPAGATE` 状态? +In simple terms, AQS is an abstract class that provides a general **execution framework** for synchronizers. It defines a **common process for resource acquisition and release**, while the specific resource acquisition logic is implemented by specific synchronizers through overriding template methods. Therefore, AQS can be seen as the **basic "foundation"** of synchronizers, while synchronizers are the **specific "applications"** built on top of AQS. -在 `doReleaseShared()` 释放资源时,第 3 步不太容易理解,即如果发现 `head` 节点的状态是 `0` ,就将 `head` 节点的状态由 `0` 更新为 `PROPAGATE` 。 +#### Why does AQS use a variant of the CLH lock queue? -AQS 中,Node 节点的 `PROPAGATE` 就是为了处理并发场景下可能出现的无法唤醒线程节点的问题。`PROPAGATE` 只在 `doReleaseShared()` 方法中用到一次。 +The CLH lock is an optimized implementation based on **spin locks**. -**接下来通过案例分析,为什么需要 `PROPAGATE` 状态?** +First, let's discuss the problems with spin locks: Spin locks attempt to acquire a lock by continuously executing `compareAndSet` (abbreviated as `CAS`) operations on an atomic variable. In high-concurrency scenarios, multiple threads may compete for the same atomic variable simultaneously, leading to a situation where a particular thread's `CAS` operation fails for an extended period, resulting in the **"starvation problem"** (some threads may never acquire the lock). -在共享模式下,线程获取和释放资源的方法调用链如下: +The CLH lock improves upon spin locks by introducing a queue to organize competing threads: -- 线程获取资源的方法调用链为: `acquireShared() -> tryAcquireShared() -> 线程阻塞等待唤醒 -> tryAcquireShared() -> setHeadAndPropagate() -> if (剩余资源数 > 0) || (head.waitStatus < 0) 则唤醒后续节点` 。 +- Each thread joins the queue as a node and monitors the state of the previous thread node through spinning, rather than directly competing for a shared variable. +- Threads are queued in order, ensuring fairness and avoiding the "starvation" problem. -- 线程释放资源的方法调用链为: `releaseShared() -> tryReleaseShared() -> doReleaseShared()` 。 +AQS (AbstractQueuedSynchronizer) further optimizes the CLH lock, forming its internal **CLH queue variant**. The main improvements are as follows: -**如果在释放资源时,没有将 `head` 节点的状态由 `0` 改为 `PROPAGATE` :** +1. **Spin + Block**: The CLH lock uses pure spinning to wait for the lock to be released, but excessive spinning can consume too many CPU resources. AQS introduces a **spin + block** hybrid mechanism: + - If a thread fails to acquire the lock, it will first attempt to spin briefly to acquire the lock; + - If it still fails, the thread will enter a blocked state, waiting to be awakened, thus reducing CPU waste. +1. **Unidirectional Queue to Bidirectional Queue**: The CLH lock uses a unidirectional queue, where nodes only know the state of their predecessor. When a node releases the lock, it must wake up subsequent nodes through the queue. AQS changes the queue to a **bidirectional queue**, adding a `next` pointer, allowing nodes to know not only their predecessor but also directly wake up their successor, simplifying queue operations and improving wake-up efficiency. -假设总共有 4 个线程尝试以共享模式获取资源,总共有 2 个资源。初始 `T3` 和 `T4` 线程获取到了资源,`T1` 和 `T2` 线程没有获取到,因此在队列中排队等候。 - -- 在时刻 1 时,线程 `T1` 和 `T2` 在等待队列中,`T3` 和 `T4` 持有资源。此时等待队列内节点以及对应状态为(括号内为节点的 `waitStatus` 状态): - - `head(-1) -> T1(-1) -> T2(0)` 。 - -- 在时刻 2 时,线程 `T3` 释放资源,通过 `doReleaseShared()` 方法将 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,并唤醒线程 `T1` ,之后线程 `T3` 退出。 - - 线程 `T1` 被唤醒之后,通过 `tryAcquireShared()` 获取到资源,但是此时还未来得及执行 `setHeadAndPropagate()` 将自己设置为 `head` 节点。此时等待队列内节点状态为: - - `head(0) -> T1(-1) -> T2(0)` 。 - -- 在时刻 3 时,线程 `T4` 释放资源, 由于此时 `head` 节点的状态为 `0` ,因此在 `doReleaseShared()` 方法中无法唤醒 `head` 的后继节点, 之后线程 `T4` 退出。 - -- 在时刻 4 时,线程 `T1` 继续执行 `setHeadAndPropagate()` 方法将自己设置为 `head` 节点。 - - 但是此时由于线程 `T1` 执行 `tryAcquireShared()` 方法返回的剩余资源数为 `0` ,并且 `head` 节点的状态为 `0` ,因此线程 `T1` 并不会在 `setHeadAndPropagate()` 方法中唤醒后续节点。此时等待队列内节点状态为: - - `head(-1,线程 T1 节点) -> T2(0)` 。 - -此时,就导致线程 `T2` 节点在等待队列中,无法被唤醒。对应时刻表如下: - -| 时刻 | 线程 T1 | 线程 T2 | 线程 T3 | 线程 T4 | 等待队列 | -| ------ | -------------------------------------------------------------- | -------- | ---------------- | ------------------------------------------------------------- | --------------------------------- | -| 时刻 1 | 等待队列 | 等待队列 | 持有资源 | 持有资源 | `head(-1) -> T1(-1) -> T2(0)` | -| 时刻 2 | (执行)被唤醒后,获取资源,但未来得及将自己设置为 `head` 节点 | 等待队列 | (执行)释放资源 | 持有资源 | `head(0) -> T1(-1) -> T2(0)` | -| 时刻 3 | | 等待队列 | 已退出 | (执行)释放资源。但 `head` 节点状态为 `0` ,无法唤醒后继节点 | `head(0) -> T1(-1) -> T2(0)` | -| 时刻 4 | (执行)将自己设置为 `head` 节点 | 等待队列 | 已退出 | 已退出 | `head(-1,线程 T1 节点) -> T2(0)` | - -**如果在线程释放资源时,将 `head` 节点的状态由 `0` 改为 `PROPAGATE` ,则可以解决上边出现的并发问题,如下:** - -- 在时刻 1 时,线程 `T1` 和 `T2` 在等待队列中,`T3` 和 `T4` 持有资源。此时等待队列内节点以及对应状态为: - - `head(-1) -> T1(-1) -> T2(0)` 。 - -- 在时刻 2 时,线程 `T3` 释放资源,通过 `doReleaseShared()` 方法将 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,并唤醒线程 `T1` ,之后线程 `T3` 退出。 - - 线程 `T1` 被唤醒之后,通过 `tryAcquireShared()` 获取到资源,但是此时还未来得及执行 `setHeadAndPropagate()` 将自己设置为 `head` 节点。此时等待队列内节点状态为: - - `head(0) -> T1(-1) -> T2(0)` 。 - -- 在时刻 3 时,线程 `T4` 释放资源, 由于此时 `head` 节点的状态为 `0` ,因此在 `doReleaseShared()` 方法中会将 `head` 节点的状态由 `0` 更新为 `PROPAGATE` , 之后线程 `T4` 退出。此时等待队列内节点状态为: - - `head(PROPAGATE) -> T1(-1) -> T2(0)` 。 - -- 在时刻 4 时,线程 `T1` 继续执行 `setHeadAndPropagate()` 方法将自己设置为 `head` 节点。此时等待队列内节点状态为: - - `head(-1,线程 T1 节点) -> T2(0)` 。 - -- 在时刻 5 时,虽然此时由于线程 `T1` 执行 `tryAcquireShared()` 方法返回的剩余资源数为 `0` ,但是 `head` 节点状态为 `PROPAGATE < 0` (这里的 `head` 节点是老的 `head` 节点,而不是刚成为 `head` 节点的线程 `T1` 节点)。 - - 因此线程 `T1` 会在 `setHeadAndPropagate()` 方法中唤醒后续 `T2` 节点,并将 `head` 节点的状态由 `SIGNAL` 更新为 `0`。此时等待队列内节点状态为: - - `head(0,线程 T1 节点) -> T2(0)` 。 - -- 在时刻 6 时,线程 `T2` 被唤醒后,获取到资源,并将自己设置为 `head` 节点。此时等待队列内节点状态为: - - `head(0,线程 T2 节点)` 。 - -有了 `PROPAGATE` 状态,就可以避免线程 `T2` 无法被唤醒的情况。对应时刻表如下: - -| 时刻 | 线程 T1 | 线程 T2 | 线程 T3 | 线程 T4 | 等待队列 | -| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------------- | ------------------------------------ | -| 时刻 1 | 等待队列 | 等待队列 | 持有资源 | 持有资源 | `head(-1) -> T1(-1) -> T2(0)` | -| 时刻 2 | (执行)被唤醒后,获取资源,但未来得及将自己设置为 `head` 节点 | 等待队列 | (执行)释放资源 | 持有资源 | `head(0) -> T1(-1) -> T2(0)` | -| 时刻 3 | 未继续向下执行 | 等待队列 | 已退出 | (执行)释放资源。此时会将 `head` 节点状态由 `0` 更新为 `PROPAGATE` | `head(PROPAGATE) -> T1(-1) -> T2(0)` | -| 时刻 4 | (执行)将自己设置为 `head` 节点 | 等待队列 | 已退出 | 已退出 | `head(-1,线程 T1 节点) -> T2(0)` | -| 时刻 5 | (执行)由于 `head` 节点状态为 `PROPAGATE < 0` ,因此会在 `setHeadAndPropagate()` 方法中唤醒后续节点,此时将新的 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,并唤醒线程 `T2` | 等待队列 | 已退出 | 已退出 | `head(0,线程 T1 节点) -> T2(0)` | -| 时刻 6 | 已退出 | (执行)线程 `T2` 被唤醒后,获取到资源,并将自己设置为 `head` 节点 | 已退出 | 已退出 | `head(0,线程 T2 节点)` | - -### AQS 资源释放源码分析(共享模式) - -AQS 中以共享模式释放资源的入口方法是 `releaseShared()` ,代码如下: - -```JAVA -// AQS -public final boolean releaseShared(int arg) { - if (tryReleaseShared(arg)) { - doReleaseShared(); - return true; - } - return false; -} -``` - -其中 `tryReleaseShared()` 方法是 AQS 提供的模板方法,这里同样以 `Semaphore` 来讲解,如下: - -```JAVA -// Semaphore -protected final boolean tryReleaseShared(int releases) { - for (;;) { - int current = getState(); - int next = current + releases; - if (next < current) // overflow - throw new Error("Maximum permit count exceeded"); - if (compareAndSetState(current, next)) - return true; - } -} -``` - -在 `Semaphore` 实现的 `tryReleaseShared()` 方法中,会在死循环内不断尝试释放资源,即通过 `CAS` 操作来更新 `state` 值。 - -如果更新成功,则证明资源释放成功,会进入到 `doReleaseShared()` 方法。 - -`doReleaseShared()` 方法在前文获取资源(共享模式)的部分已进行了详细的源码分析,此处不再重复。 - -## 常见同步工具类 - -下面介绍几个基于 AQS 的常见同步工具类。 - -### Semaphore(信号量) - -#### 介绍 - -`synchronized` 和 `ReentrantLock` 都是一次只允许一个线程访问某个资源,而`Semaphore`(信号量)可以用来控制同时访问特定资源的线程数量。 - -`Semaphore` 的使用简单,我们这里假设有 `N(N>5)` 个线程来获取 `Semaphore` 中的共享资源,下面的代码表示同一时刻 N 个线程中只有 5 个线程能获取到共享资源,其他线程都会阻塞,只有获取到共享资源的线程才能执行。等到有线程释放了共享资源,其他阻塞的线程才能获取到。 - -```java -// 初始共享资源数量 -final Semaphore semaphore = new Semaphore(5); -// 获取1个许可 -semaphore.acquire(); -// 释放1个许可 -semaphore.release(); -``` - -当初始的资源个数为 1 的时候,`Semaphore` 退化为排他锁。 - -`Semaphore` 有两种模式:。 - -- **公平模式:** 调用 `acquire()` 方法的顺序就是获取许可证的顺序,遵循 FIFO; -- **非公平模式:** 抢占式的。 - -`Semaphore` 对应的两个构造方法如下: - -```java -public Semaphore(int permits) { - sync = new NonfairSync(permits); -} - -public Semaphore(int permits, boolean fair) { - sync = fair ? new FairSync(permits) : new NonfairSync(permits); -} -``` - -**这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。** - -`Semaphore` 通常用于那些资源有明确访问数量限制的场景比如限流(仅限于单机模式,实际项目中推荐使用 Redis +Lua 来做限流)。 - -#### 原理 - -`Semaphore` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `permits`,你可以将 `permits` 的值理解为许可证的数量,只有拿到许可证的线程才能执行。 - -以无参 `acquire` 方法为例,调用`semaphore.acquire()` ,线程尝试获取许可证,如果 `state > 0` 的话,则表示可以获取成功,如果 `state <= 0` 的话,则表示许可证数量不足,获取失败。 - -如果可以获取成功的话(`state > 0` ),会尝试使用 CAS 操作去修改 `state` 的值 `state=state-1`。如果获取失败则会创建一个 Node 节点加入等待队列,挂起当前线程。 - -```java -// 获取1个许可证 -public void acquire() throws InterruptedException { - sync.acquireSharedInterruptibly(1); -} - -// 获取一个或者多个许可证 -public void acquire(int permits) throws InterruptedException { - if (permits < 0) throw new IllegalArgumentException(); - sync.acquireSharedInterruptibly(permits); -} -``` - -`acquireSharedInterruptibly`方法是 `AbstractQueuedSynchronizer` 中的默认实现。 - -```java -// 共享模式下获取许可证,获取成功则返回,失败则加入等待队列,挂起线程 -public final void acquireSharedInterruptibly(int arg) - throws InterruptedException { - if (Thread.interrupted()) - throw new InterruptedException(); - // 尝试获取许可证,arg为获取许可证个数,当获取失败时,则创建一个节点加入等待队列,挂起当前线程。 - if (tryAcquireShared(arg) < 0) - doAcquireSharedInterruptibly(arg); -} -``` - -这里再以非公平模式(`NonfairSync`)的为例,看看 `tryAcquireShared` 方法的实现。 - -```java -// 共享模式下尝试获取资源(在Semaphore中的资源即许可证): -protected int tryAcquireShared(int acquires) { - return nonfairTryAcquireShared(acquires); -} - -// 非公平的共享模式获取许可证 -final int nonfairTryAcquireShared(int acquires) { - for (;;) { - // 当前可用许可证数量 - int available = getState(); - /* - * 尝试获取许可证,当前可用许可证数量小于等于0时,返回负值,表示获取失败, - * 当前可用许可证大于0时才可能获取成功,CAS失败了会循环重新获取最新的值尝试获取 - */ - int remaining = available - acquires; - if (remaining < 0 || - compareAndSetState(available, remaining)) - return remaining; - } -} -``` - -以无参 `release` 方法为例,调用`semaphore.release();` ,线程尝试释放许可证,并使用 CAS 操作去修改 `state` 的值 `state=state+1`。释放许可证成功之后,同时会唤醒等待队列中的一个线程。被唤醒的线程会重新尝试去修改 `state` 的值 `state=state-1` ,如果 `state > 0` 则获取令牌成功,否则重新进入等待队列,挂起线程。 - -```java -// 释放一个许可证 -public void release() { - sync.releaseShared(1); -} - -// 释放一个或者多个许可证 -public void release(int permits) { - if (permits < 0) throw new IllegalArgumentException(); - sync.releaseShared(permits); -} -``` - -`releaseShared`方法是 `AbstractQueuedSynchronizer` 中的默认实现。 - -```java -// 释放共享锁 -// 如果 tryReleaseShared 返回 true,就唤醒等待队列中的一个或多个线程。 -public final boolean releaseShared(int arg) { - //释放共享锁 - if (tryReleaseShared(arg)) { - //释放当前节点的后置等待节点 - doReleaseShared(); - return true; - } - return false; -} -``` - -`tryReleaseShared` 方法是`Semaphore` 的内部类 `Sync` 重写的一个方法, `AbstractQueuedSynchronizer`中的默认实现仅仅抛出 `UnsupportedOperationException` 异常。 - -```java -// 内部类 Sync 中重写的一个方法 -// 尝试释放资源 -protected final boolean tryReleaseShared(int releases) { - for (;;) { - int current = getState(); - // 可用许可证+1 - int next = current + releases; - if (next < current) // overflow - throw new Error("Maximum permit count exceeded"); - // CAS修改state的值 - if (compareAndSetState(current, next)) - return true; - } -} -``` - -可以看到,上面提到的几个方法底层基本都是通过同步器 `sync` 实现的。`Sync` 是 `CountDownLatch` 的内部类 , 继承了 `AbstractQueuedSynchronizer` ,重写了其中的某些方法。并且,Sync 对应的还有两个子类 `NonfairSync`(对应非公平模式) 和 `FairSync`(对应公平模式)。 - -```java -private static final class Sync extends AbstractQueuedSynchronizer { - // ... -} -static final class NonfairSync extends Sync { - // ... -} -static final class FairSync extends Sync { - // ... -} -``` - -#### 实战 - -```java -public class SemaphoreExample { - // 请求的数量 - private static final int threadCount = 550; - - public static void main(String[] args) throws InterruptedException { - // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢) - ExecutorService threadPool = Executors.newFixedThreadPool(300); - // 初始许可证数量 - final Semaphore semaphore = new Semaphore(20); - - for (int i = 0; i < threadCount; i++) { - final int threadnum = i; - threadPool.execute(() -> {// Lambda 表达式的运用 - try { - semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20 - test(threadnum); - semaphore.release();// 释放一个许可 - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - }); - } - threadPool.shutdown(); - System.out.println("finish"); - } - - public static void test(int threadnum) throws InterruptedException { - Thread.sleep(1000);// 模拟请求的耗时操作 - System.out.println("threadnum:" + threadnum); - Thread.sleep(1000);// 模拟请求的耗时操作 - } -} -``` - -执行 `acquire()` 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 `release` 方法增加一个许可证,这可能会释放一个阻塞的 `acquire()` 方法。然而,其实并没有实际的许可证这个对象,`Semaphore` 只是维持了一个可获得许可证的数量。 `Semaphore` 经常用于限制获取某种资源的线程数量。 - -当然一次也可以一次拿取和释放多个许可,不过一般没有必要这样做: - -```java -semaphore.acquire(5);// 获取5个许可,所以可运行线程数量为20/5=4 -test(threadnum); -semaphore.release(5);// 释放5个许可 -``` - -除了 `acquire()` 方法之外,另一个比较常用的与之对应的方法是 `tryAcquire()` 方法,该方法如果获取不到许可就立即返回 false。 - -[issue645 补充内容](https://github.com/Snailclimb/JavaGuide/issues/645): - -> `Semaphore` 基于 AQS 实现,用于控制并发访问的线程数量,但它与共享锁的概念有所不同。`Semaphore` 的构造函数使用 `permits` 参数初始化 AQS 的 `state` 变量,该变量表示可用的许可数量。当线程调用 `acquire()` 方法尝试获取许可时,`state` 会原子性地减 1。如果 `state` 减 1 后大于等于 0,则 `acquire()` 成功返回,线程可以继续执行。如果 `state` 减 1 后小于 0,表示当前并发访问的线程数量已达到 `permits` 的限制,该线程会被放入 AQS 的等待队列并阻塞,**而不是自旋等待**。当其他线程完成任务并调用 `release()` 方法时,`state` 会原子性地加 1。`release()` 操作会唤醒 AQS 等待队列中的一个或多个阻塞线程。这些被唤醒的线程将再次尝试 `acquire()` 操作,竞争获取可用的许可。因此,`Semaphore` 通过控制许可数量来限制并发访问的线程数量,而不是通过自旋和共享锁机制。 - -### CountDownLatch (倒计时器) - -#### 介绍 - -`CountDownLatch` 允许 `count` 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。 - -`CountDownLatch` 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 `CountDownLatch` 使用完毕后,它不能再次被使用。 - -#### 原理 - -`CountDownLatch` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `count`。这个我们通过 `CountDownLatch` 的构造方法即可看出。 - -```java -public CountDownLatch(int count) { - if (count < 0) throw new IllegalArgumentException("count < 0"); - this.sync = new Sync(count); -} - -private static final class Sync extends AbstractQueuedSynchronizer { - Sync(int count) { - setState(count); - } - //... -} -``` - -当线程调用 `countDown()` 时,其实使用了`tryReleaseShared`方法以 CAS 的操作来减少 `state`,直至 `state` 为 0 。当 `state` 为 0 时,表示所有的线程都调用了 `countDown` 方法,那么在 `CountDownLatch` 上等待的线程就会被唤醒并继续执行。 - -```java -public void countDown() { - // Sync 是 CountDownLatch 的内部类 , 继承了 AbstractQueuedSynchronizer - sync.releaseShared(1); -} -``` - -`releaseShared`方法是 `AbstractQueuedSynchronizer` 中的默认实现。 - -```java -// 释放共享锁 -// 如果 tryReleaseShared 返回 true,就唤醒等待队列中的一个或多个线程。 -public final boolean releaseShared(int arg) { - //释放共享锁 - if (tryReleaseShared(arg)) { - //释放当前节点的后置等待节点 - doReleaseShared(); - return true; - } - return false; -} -``` - -`tryReleaseShared` 方法是`CountDownLatch` 的内部类 `Sync` 重写的一个方法, `AbstractQueuedSynchronizer`中的默认实现仅仅抛出 `UnsupportedOperationException` 异常。 - -```java -// 对 state 进行递减,直到 state 变成 0; -// 只有 count 递减到 0 时,countDown 才会返回 true -protected boolean tryReleaseShared(int releases) { - // 自选检查 state 是否为 0 - for (;;) { - int c = getState(); - // 如果 state 已经是 0 了,直接返回 false - if (c == 0) - return false; - // 对 state 进行递减 - int nextc = c-1; - // CAS 操作更新 state 的值 - if (compareAndSetState(c, nextc)) - return nextc == 0; - } -} -``` - -以无参 `await`方法为例,当调用 `await()` 的时候,如果 `state` 不为 0,那就证明任务还没有执行完毕,`await()` 就会一直阻塞,也就是说 `await()` 之后的语句不会被执行(`main` 线程被加入到等待队列也就是 变体 CLH 队列中了)。然后,`CountDownLatch` 会自旋 CAS 判断 `state == 0`,如果 `state == 0` 的话,就会释放所有等待的线程,`await()` 方法之后的语句得到执行。 - -```java -// 等待(也可以叫做加锁) -public void await() throws InterruptedException { - sync.acquireSharedInterruptibly(1); -} -// 带有超时时间的等待 -public boolean await(long timeout, TimeUnit unit) - throws InterruptedException { - return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); -} -``` - -`acquireSharedInterruptibly`方法是 `AbstractQueuedSynchronizer` 中的默认实现。 - -```java -// 尝试获取锁,获取成功则返回,失败则加入等待队列,挂起线程 -public final void acquireSharedInterruptibly(int arg) - throws InterruptedException { - if (Thread.interrupted()) - throw new InterruptedException(); - // 尝试获得锁,获取成功则返回 - if (tryAcquireShared(arg) < 0) - // 获取失败加入等待队列,挂起线程 - doAcquireSharedInterruptibly(arg); -} -``` - -`tryAcquireShared` 方法是`CountDownLatch` 的内部类 `Sync` 重写的一个方法,其作用就是判断 `state` 的值是否为 0,是的话就返回 1,否则返回 -1。 - -```java -protected int tryAcquireShared(int acquires) { - return (getState() == 0) ? 1 : -1; -} -``` - -#### 实战 - -**CountDownLatch 的两种典型用法**: - -1. 某一线程在开始运行前等待 n 个线程执行完毕 : 将 `CountDownLatch` 的计数器初始化为 n (`new CountDownLatch(n)`),每当一个任务线程执行完毕,就将计数器减 1 (`countdownlatch.countDown()`),当计数器的值变为 0 时,在 `CountDownLatch 上 await()` 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。 -2. 实现多个线程开始执行任务的最大并行性:注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 `CountDownLatch` 对象,将其计数器初始化为 1 (`new CountDownLatch(1)`),多个线程在开始执行任务前首先 `coundownlatch.await()`,当主线程调用 `countDown()` 时,计数器变为 0,多个线程同时被唤醒。 - -**CountDownLatch 代码示例**: - -```java -public class CountDownLatchExample { - // 请求的数量 - private static final int THREAD_COUNT = 550; - - public static void main(String[] args) throws InterruptedException { - // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢) - // 只是测试使用,实际场景请手动赋值线程池参数 - ExecutorService threadPool = Executors.newFixedThreadPool(300); - final CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT); - for (int i = 0; i < THREAD_COUNT; i++) { - final int threadNum = i; - threadPool.execute(() -> { - try { - test(threadNum); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - // 表示一个请求已经被完成 - countDownLatch.countDown(); - } - - }); - } - countDownLatch.await(); - threadPool.shutdown(); - System.out.println("finish"); - } - - public static void test(int threadnum) throws InterruptedException { - Thread.sleep(1000); - System.out.println("threadNum:" + threadnum); - Thread.sleep(1000); - } -} -``` - -上面的代码中,我们定义了请求的数量为 550,当这 550 个请求被处理完成之后,才会执行`System.out.println("finish");`。 - -与 `CountDownLatch` 的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用 `CountDownLatch.await()` 方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。 - -其他 N 个线程必须引用闭锁对象,因为他们需要通知 `CountDownLatch` 对象,他们已经完成了各自的任务。这种通知机制是通过 `CountDownLatch.countDown()`方法来完成的;每调用一次这个方法,在构造函数中初始化的 count 值就减 1。所以当 N 个线程都调 用了这个方法,count 的值等于 0,然后主线程就能通过 `await()`方法,恢复执行自己的任务。 - -再插一嘴:`CountDownLatch` 的 `await()` 方法使用不当很容易产生死锁,比如我们上面代码中的 for 循环改为: - -```java -for (int i = 0; i < threadCount-1; i++) { -....... -} -``` - -这样就导致 `count` 的值没办法等于 0,然后就会导致一直等待。 - -### CyclicBarrier(循环栅栏) - -#### 介绍 - -`CyclicBarrier` 和 `CountDownLatch` 非常类似,它也可以实现线程间的技术等待,但是它的功能比 `CountDownLatch` 更加复杂和强大。主要应用场景和 `CountDownLatch` 类似。 - -> `CountDownLatch` 的实现是基于 AQS 的,而 `CyclicBarrier` 是基于 `ReentrantLock`(`ReentrantLock` 也属于 AQS 同步器)和 `Condition` 的。 - -`CyclicBarrier` 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。 - -#### 原理 - -`CyclicBarrier` 内部通过一个 `count` 变量作为计数器,`count` 的初始值为 `parties` 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减 1。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。 - -```java -//每次拦截的线程数 -private final int parties; -//计数器 -private int count; -``` - -下面我们结合源码来简单看看。 - -1、`CyclicBarrier` 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用 `await()` 方法告诉 `CyclicBarrier` 我已经到达了屏障,然后当前线程被阻塞。 - -```java -public CyclicBarrier(int parties) { - this(parties, null); -} - -public CyclicBarrier(int parties, Runnable barrierAction) { - if (parties <= 0) throw new IllegalArgumentException(); - this.parties = parties; - this.count = parties; - this.barrierCommand = barrierAction; -} -``` - -其中,`parties` 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过。 - -2、当调用 `CyclicBarrier` 对象调用 `await()` 方法时,实际上调用的是 `dowait(false, 0L)`方法。 `await()` 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 `parties` 的值时,栅栏才会打开,线程才得以通过执行。 - -```java -public int await() throws InterruptedException, BrokenBarrierException { - try { - return dowait(false, 0L); - } catch (TimeoutException toe) { - throw new Error(toe); // cannot happen - } -} -``` - -`dowait(false, 0L)`方法源码分析如下: - -```java - // 当线程数量或者请求数量达到 count 时 await 之后的方法才会被执行。上面的示例中 count 的值就为 5。 - private int count; - /** - * Main barrier code, covering the various policies. - */ - private int dowait(boolean timed, long nanos) - throws InterruptedException, BrokenBarrierException, - TimeoutException { - final ReentrantLock lock = this.lock; - // 锁住 - lock.lock(); - try { - final Generation g = generation; - - if (g.broken) - throw new BrokenBarrierException(); - - // 如果线程中断了,抛出异常 - if (Thread.interrupted()) { - breakBarrier(); - throw new InterruptedException(); - } - // count 减1 - int index = --count; - // 当 count 数量减为 0 之后说明最后一个线程已经到达栅栏了,也就是达到了可以执行await 方法之后的条件 - if (index == 0) { // tripped - boolean ranAction = false; - try { - final Runnable command = barrierCommand; - if (command != null) - command.run(); - ranAction = true; - // 将 count 重置为 parties 属性的初始化值 - // 唤醒之前等待的线程 - // 下一波执行开始 - nextGeneration(); - return 0; - } finally { - if (!ranAction) - breakBarrier(); - } - } - - // loop until tripped, broken, interrupted, or timed out - for (;;) { - try { - if (!timed) - trip.await(); - else if (nanos > 0L) - nanos = trip.awaitNanos(nanos); - } catch (InterruptedException ie) { - if (g == generation && ! g.broken) { - breakBarrier(); - throw ie; - } else { - // We're about to finish waiting even if we had not - // been interrupted, so this interrupt is deemed to - // "belong" to subsequent execution. - Thread.currentThread().interrupt(); - } - } - - if (g.broken) - throw new BrokenBarrierException(); - - if (g != generation) - return index; - - if (timed && nanos <= 0L) { - breakBarrier(); - throw new TimeoutException(); - } - } - } finally { - lock.unlock(); - } - } -``` - -#### 实战 - -示例 1: - -```java -public class CyclicBarrierExample1 { - // 请求的数量 - private static final int threadCount = 550; - // 需要同步的线程数量 - private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5); - - public static void main(String[] args) throws InterruptedException { - // 创建线程池 - ExecutorService threadPool = Executors.newFixedThreadPool(10); - - for (int i = 0; i < threadCount; i++) { - final int threadNum = i; - Thread.sleep(1000); - threadPool.execute(() -> { - try { - test(threadNum); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (BrokenBarrierException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - }); - } - threadPool.shutdown(); - } - - public static void test(int threadnum) throws InterruptedException, BrokenBarrierException { - System.out.println("threadnum:" + threadnum + "is ready"); - try { - /**等待60秒,保证子线程完全执行结束*/ - cyclicBarrier.await(60, TimeUnit.SECONDS); - } catch (Exception e) { - System.out.println("-----CyclicBarrierException------"); - } - System.out.println("threadnum:" + threadnum + "is finish"); - } - -} -``` - -运行结果,如下: - -```plain -threadnum:0is ready -threadnum:1is ready -threadnum:2is ready -threadnum:3is ready -threadnum:4is ready -threadnum:4is finish -threadnum:0is finish -threadnum:1is finish -threadnum:2is finish -threadnum:3is finish -threadnum:5is ready -threadnum:6is ready -threadnum:7is ready -threadnum:8is ready -threadnum:9is ready -threadnum:9is finish -threadnum:5is finish -threadnum:8is finish -threadnum:7is finish -threadnum:6is finish -...... -``` - -可以看到当线程数量也就是请求数量达到我们定义的 5 个的时候, `await()` 方法之后的方法才被执行。 - -另外,`CyclicBarrier` 还提供一个更高级的构造函数 `CyclicBarrier(int parties, Runnable barrierAction)`,用于在线程到达屏障时,优先执行 `barrierAction`,方便处理更复杂的业务场景。 - -示例 2: - -```java -public class CyclicBarrierExample2 { - // 请求的数量 - private static final int threadCount = 550; - // 需要同步的线程数量 - private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> { - System.out.println("------当线程数达到之后,优先执行------"); - }); - - public static void main(String[] args) throws InterruptedException { - // 创建线程池 - ExecutorService threadPool = Executors.newFixedThreadPool(10); - - for (int i = 0; i < threadCount; i++) { - final int threadNum = i; - Thread.sleep(1000); - threadPool.execute(() -> { - try { - test(threadNum); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (BrokenBarrierException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - }); - } - threadPool.shutdown(); - } - - public static void test(int threadnum) throws InterruptedException, BrokenBarrierException { - System.out.println("threadnum:" + threadnum + "is ready"); - cyclicBarrier.await(); - System.out.println("threadnum:" + threadnum + "is finish"); - } - -} -``` - -运行结果,如下: - -```plain -threadnum:0is ready -threadnum:1is ready -threadnum:2is ready -threadnum:3is ready -threadnum:4is ready -------当线程数达到之后,优先执行------ -threadnum:4is finish -threadnum:0is finish -threadnum:2is finish -threadnum:1is finish -threadnum:3is finish -threadnum:5is ready -threadnum:6is ready -threadnum:7is ready -threadnum:8is ready -threadnum:9is ready -------当线程数达到之后,优先执行------ -threadnum:9is finish -threadnum:5is finish -threadnum:6is finish -threadnum:8is finish -threadnum:7is finish -...... -``` +#### Why does AQS perform well? -## 参考 +AQS performs well because it heavily utilizes `CAS` operations. -- Java 并发之 AQS 详解: -- 从 ReentrantLock 的实现看 AQS 的原理及应用: +Internally, AQS uses a queue to store waiting thread nodes. Since the queue is a shared resource, it needs to ensure synchronized access in a multithreaded environment. - +AQS controls the synchronized access to the queue through `CAS` operations, which are primarily used to ensure the concurrent safety of two operations: `queue initialization` and diff --git a/docs/java/concurrent/atomic-classes.md b/docs/java/concurrent/atomic-classes.md index ec47ba6f66f..b0a74418106 100644 --- a/docs/java/concurrent/atomic-classes.md +++ b/docs/java/concurrent/atomic-classes.md @@ -1,399 +1,91 @@ --- -title: Atomic 原子类总结 +title: Summary of Atomic Classes category: Java tag: - - Java并发 + - Java Concurrency --- -## Atomic 原子类介绍 +## Introduction to Atomic Classes -`Atomic` 翻译成中文是“原子”的意思。在化学上,原子是构成物质的最小单位,在化学反应中不可分割。在编程中,`Atomic` 指的是一个操作具有原子性,即该操作不可分割、不可中断。即使在多个线程同时执行时,该操作要么全部执行完成,要么不执行,不会被其他线程看到部分完成的状态。 +`Atomic` translates to "atomic" in Chinese. In chemistry, an atom is the smallest unit of matter and is indivisible in chemical reactions. In programming, `Atomic` refers to an operation that has atomicity, meaning the operation is indivisible and uninterruptible. Even when multiple threads are executing simultaneously, the operation either completes entirely or does not execute at all, and other threads will not see a partially completed state. -原子类简单来说就是具有原子性操作特征的类。 +In simple terms, atomic classes are classes that have the characteristic of atomic operations. -`java.util.concurrent.atomic` 包中的 `Atomic` 原子类提供了一种线程安全的方式来操作单个变量。 +The `Atomic` classes in the `java.util.concurrent.atomic` package provide a thread-safe way to operate on a single variable. -`Atomic` 类依赖于 CAS(Compare-And-Swap,比较并交换)乐观锁来保证其方法的原子性,而不需要使用传统的锁机制(如 `synchronized` 块或 `ReentrantLock`)。 +The `Atomic` classes rely on CAS (Compare-And-Swap) optimistic locking to ensure the atomicity of their methods without needing traditional locking mechanisms (such as `synchronized` blocks or `ReentrantLock`). -这篇文章我们只介绍 Atomic 原子类的概念,具体实现原理可以阅读笔者写的这篇文章:[CAS 详解](./cas.md)。 +This article will only introduce the concept of Atomic classes; for specific implementation principles, you can read the article written by the author: [Detailed Explanation of CAS](./cas.md). -![JUC原子类概览](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88.png) +![Overview of JUC Atomic Classes](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88.png) -根据操作的数据类型,可以将 JUC 包中的原子类分为 4 类: +Based on the data types of the operations, the atomic classes in the JUC package can be divided into four categories: -**1、基本类型** +**1. Basic Types** -使用原子的方式更新基本类型 +Using atomic methods to update basic types -- `AtomicInteger`:整型原子类 -- `AtomicLong`:长整型原子类 -- `AtomicBoolean`:布尔型原子类 +- `AtomicInteger`: Integer atomic class +- `AtomicLong`: Long atomic class +- `AtomicBoolean`: Boolean atomic class -**2、数组类型** +**2. Array Types** -使用原子的方式更新数组里的某个元素 +Using atomic methods to update an element in an array -- `AtomicIntegerArray`:整型数组原子类 -- `AtomicLongArray`:长整型数组原子类 -- `AtomicReferenceArray`:引用类型数组原子类 +- `AtomicIntegerArray`: Integer array atomic class +- `AtomicLongArray`: Long array atomic class +- `AtomicReferenceArray`: Reference type array atomic class -**3、引用类型** +**3. Reference Types** -- `AtomicReference`:引用类型原子类 -- `AtomicMarkableReference`:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,~~也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题~~。 -- `AtomicStampedReference`:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 +- `AtomicReference`: Reference type atomic class +- `AtomicMarkableReference`: Atomic update of a reference type with a mark. This class associates a boolean mark with a reference, ~~and can also solve the ABA problem that may occur when using CAS for atomic updates~~. +- `AtomicStampedReference`: Atomic update of a reference type with a version number. This class associates an integer value with a reference, which can be used to solve the atomic update of data and the version number of the data, and can address the ABA problem that may occur when using CAS for atomic updates. -**🐛 修正(参见:[issue#626](https://github.com/Snailclimb/JavaGuide/issues/626))** : `AtomicMarkableReference` 不能解决 ABA 问题。 +**🐛 Correction (see: [issue#626](https://github.com/Snailclimb/JavaGuide/issues/626))**: `AtomicMarkableReference` cannot solve the ABA problem. -**4、对象的属性修改类型** +**4. Object Property Modification Types** -- `AtomicIntegerFieldUpdater`:原子更新整型字段的更新器 -- `AtomicLongFieldUpdater`:原子更新长整型字段的更新器 -- `AtomicReferenceFieldUpdater`:原子更新引用类型里的字段 +- `AtomicIntegerFieldUpdater`: Atomic updater for integer fields +- `AtomicLongFieldUpdater`: Atomic updater for long fields +- `AtomicReferenceFieldUpdater`: Atomic updater for fields in reference types -## 基本类型原子类 +## Basic Type Atomic Classes -使用原子的方式更新基本类型 +Using atomic methods to update basic types -- `AtomicInteger`:整型原子类 -- `AtomicLong`:长整型原子类 -- `AtomicBoolean`:布尔型原子类 +- `AtomicInteger`: Integer atomic class +- `AtomicLong`: Long atomic class +- `AtomicBoolean`: Boolean atomic class -上面三个类提供的方法几乎相同,所以我们这里以 `AtomicInteger` 为例子来介绍。 +The methods provided by the above three classes are almost identical, so we will use `AtomicInteger` as an example for introduction. -**`AtomicInteger` 类常用方法** : +**Common Methods of `AtomicInteger`**: ```java -public final int get() //获取当前的值 -public final int getAndSet(int newValue)//获取当前的值,并设置新的值 -public final int getAndIncrement()//获取当前的值,并自增 -public final int getAndDecrement() //获取当前的值,并自减 -public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 -boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) -public final void lazySet(int newValue)//最终设置为newValue, lazySet 提供了一种比 set 方法更弱的语义,可能导致其他线程在之后的一小段时间内还是可以读到旧的值,但可能更高效。 +public final int get() // Get the current value +public final int getAndSet(int newValue) // Get the current value and set a new value +public final int getAndIncrement() // Get the current value and increment it +public final int getAndDecrement() // Get the current value and decrement it +public final int getAndAdd(int delta) // Get the current value and add the expected value +boolean compareAndSet(int expect, int update) // If the input value equals the expected value, atomically set that value to the input value (update) +public final void lazySet(int newValue) // Eventually set to newValue; lazySet provides a weaker semantic than set, which may allow other threads to read the old value for a short period afterward, but may be more efficient. ``` -**`AtomicInteger` 类使用示例** : +**Example of Using `AtomicInteger`**: ```java -// 初始化 AtomicInteger 对象,初始值为 0 +// Initialize AtomicInteger object with an initial value of 0 AtomicInteger atomicInt = new AtomicInteger(0); -// 使用 getAndSet 方法获取当前值,并设置新值为 3 +// Use getAndSet method to get the current value and set the new value to 3 int tempValue = atomicInt.getAndSet(3); System.out.println("tempValue: " + tempValue + "; atomicInt: " + atomicInt); -// 使用 getAndIncrement 方法获取当前值,并自增 1 +// Use getAndIncrement method to get the current value and increment by 1 tempValue = atomicInt.getAndIncrement(); System.out.println("tempValue: " + tempValue + "; atomicInt: " + atomicInt); -// 使用 getAndAdd 方法获取当前值,并增加指定值 5 -tempValue = atomicInt.getAndAdd(5); -System.out.println("tempValue: " + tempValue + "; atomicInt: " + atomicInt); - -// 使用 compareAndSet 方法进行原子性条件更新,期望值为 9,更新值为 10 -boolean updateSuccess = atomicInt.compareAndSet(9, 10); -System.out.println("Update Success: " + updateSuccess + "; atomicInt: " + atomicInt); - -// 获取当前值 -int currentValue = atomicInt.get(); -System.out.println("Current value: " + currentValue); - -// 使用 lazySet 方法设置新值为 15 -atomicInt.lazySet(15); -System.out.println("After lazySet, atomicInt: " + atomicInt); -``` - -输出: - -```java -tempValue: 0; atomicInt: 3 -tempValue: 3; atomicInt: 4 -tempValue: 4; atomicInt: 9 -Update Success: true; atomicInt: 10 -Current value: 10 -After lazySet, atomicInt: 15 -``` - -## 数组类型原子类 - -使用原子的方式更新数组里的某个元素 - -- `AtomicIntegerArray`:整形数组原子类 -- `AtomicLongArray`:长整形数组原子类 -- `AtomicReferenceArray`:引用类型数组原子类 - -上面三个类提供的方法几乎相同,所以我们这里以 `AtomicIntegerArray` 为例子来介绍。 - -**`AtomicIntegerArray` 类常用方法**: - -```java -public final int get(int i) //获取 index=i 位置元素的值 -public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue -public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增 -public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减 -public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值 -boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update) -public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 -``` - -**`AtomicIntegerArray` 类使用示例** : - -```java -int[] nums = {1, 2, 3, 4, 5, 6}; -// 创建 AtomicIntegerArray -AtomicIntegerArray atomicArray = new AtomicIntegerArray(nums); - -// 打印 AtomicIntegerArray 中的初始值 -System.out.println("Initial values in AtomicIntegerArray:"); -for (int j = 0; j < nums.length; j++) { - System.out.print("Index " + j + ": " + atomicArray.get(j) + " "); -} - -// 使用 getAndSet 方法将索引 0 处的值设置为 2,并返回旧值 -int tempValue = atomicArray.getAndSet(0, 2); -System.out.println("\nAfter getAndSet(0, 2):"); -System.out.println("Returned value: " + tempValue); -for (int j = 0; j < atomicArray.length(); j++) { - System.out.print("Index " + j + ": " + atomicArray.get(j) + " "); -} - -// 使用 getAndIncrement 方法将索引 0 处的值加 1,并返回旧值 -tempValue = atomicArray.getAndIncrement(0); -System.out.println("\nAfter getAndIncrement(0):"); -System.out.println("Returned value: " + tempValue); -for (int j = 0; j < atomicArray.length(); j++) { - System.out.print("Index " + j + ": " + atomicArray.get(j) + " "); -} - -// 使用 getAndAdd 方法将索引 0 处的值增加 5,并返回旧值 -tempValue = atomicArray.getAndAdd(0, 5); -System.out.println("\nAfter getAndAdd(0, 5):"); -System.out.println("Returned value: " + tempValue); -for (int j = 0; j < atomicArray.length(); j++) { - System.out.print("Index " + j + ": " + atomicArray.get(j) + " "); -} -``` - -输出: - -```plain -Initial values in AtomicIntegerArray: -Index 0: 1 Index 1: 2 Index 2: 3 Index 3: 4 Index 4: 5 Index 5: 6 -After getAndSet(0, 2): -Returned value: 1 -Index 0: 2 Index 1: 2 Index 2: 3 Index 3: 4 Index 4: 5 Index 5: 6 -After getAndIncrement(0): -Returned value: 2 -Index 0: 3 Index 1: 2 Index 2: 3 Index 3: 4 Index 4: 5 Index 5: 6 -After getAndAdd(0, 5): -Returned value: 3 -Index 0: 8 Index 1: 2 Index 2: 3 Index 3: 4 Index 4: 5 Index 5: 6 -``` - -## 引用类型原子类 - -基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。 - -- `AtomicReference`:引用类型原子类 -- `AtomicStampedReference`:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 -- `AtomicMarkableReference`:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,~~也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。~~ - -上面三个类提供的方法几乎相同,所以我们这里以 `AtomicReference` 为例子来介绍。 - -**`AtomicReference` 类使用示例** : - -```java -// Person 类 -class Person { - private String name; - private int age; - //省略getter/setter和toString -} - - -// 创建 AtomicReference 对象并设置初始值 -AtomicReference ar = new AtomicReference<>(new Person("SnailClimb", 22)); - -// 打印初始值 -System.out.println("Initial Person: " + ar.get().toString()); - -// 更新值 -Person updatePerson = new Person("Daisy", 20); -ar.compareAndSet(ar.get(), updatePerson); - -// 打印更新后的值 -System.out.println("Updated Person: " + ar.get().toString()); - -// 尝试再次更新 -Person anotherUpdatePerson = new Person("John", 30); -boolean isUpdated = ar.compareAndSet(updatePerson, anotherUpdatePerson); - -// 打印是否更新成功及最终值 -System.out.println("Second Update Success: " + isUpdated); -System.out.println("Final Person: " + ar.get().toString()); -``` - -输出: - -```plain -Initial Person: Person{name='SnailClimb', age=22} -Updated Person: Person{name='Daisy', age=20} -Second Update Success: true -Final Person: Person{name='John', age=30} -``` - -**`AtomicStampedReference` 类使用示例** : - -```java -// 创建一个 AtomicStampedReference 对象,初始值为 "SnailClimb",初始版本号为 1 -AtomicStampedReference asr = new AtomicStampedReference<>("SnailClimb", 1); - -// 打印初始值和版本号 -int[] initialStamp = new int[1]; -String initialRef = asr.get(initialStamp); -System.out.println("Initial Reference: " + initialRef + ", Initial Stamp: " + initialStamp[0]); - -// 更新值和版本号 -int oldStamp = initialStamp[0]; -String oldRef = initialRef; -String newRef = "Daisy"; -int newStamp = oldStamp + 1; - -boolean isUpdated = asr.compareAndSet(oldRef, newRef, oldStamp, newStamp); -System.out.println("Update Success: " + isUpdated); - -// 打印更新后的值和版本号 -int[] updatedStamp = new int[1]; -String updatedRef = asr.get(updatedStamp); -System.out.println("Updated Reference: " + updatedRef + ", Updated Stamp: " + updatedStamp[0]); - -// 尝试用错误的版本号更新 -boolean isUpdatedWithWrongStamp = asr.compareAndSet(newRef, "John", oldStamp, newStamp + 1); -System.out.println("Update with Wrong Stamp Success: " + isUpdatedWithWrongStamp); - -// 打印最终的值和版本号 -int[] finalStamp = new int[1]; -String finalRef = asr.get(finalStamp); -System.out.println("Final Reference: " + finalRef + ", Final Stamp: " + finalStamp[0]); -``` - -输出结果如下: - -```plain -Initial Reference: SnailClimb, Initial Stamp: 1 -Update Success: true -Updated Reference: Daisy, Updated Stamp: 2 -Update with Wrong Stamp Success: false -Final Reference: Daisy, Final Stamp: 2 -``` - -**`AtomicMarkableReference` 类使用示例** : - -```java -// 创建一个 AtomicMarkableReference 对象,初始值为 "SnailClimb",初始标记为 false -AtomicMarkableReference amr = new AtomicMarkableReference<>("SnailClimb", false); - -// 打印初始值和标记 -boolean[] initialMark = new boolean[1]; -String initialRef = amr.get(initialMark); -System.out.println("Initial Reference: " + initialRef + ", Initial Mark: " + initialMark[0]); - -// 更新值和标记 -String oldRef = initialRef; -String newRef = "Daisy"; -boolean oldMark = initialMark[0]; -boolean newMark = true; - -boolean isUpdated = amr.compareAndSet(oldRef, newRef, oldMark, newMark); -System.out.println("Update Success: " + isUpdated); - -// 打印更新后的值和标记 -boolean[] updatedMark = new boolean[1]; -String updatedRef = amr.get(updatedMark); -System.out.println("Updated Reference: " + updatedRef + ", Updated Mark: " + updatedMark[0]); - -// 尝试用错误的标记更新 -boolean isUpdatedWithWrongMark = amr.compareAndSet(newRef, "John", oldMark, !newMark); -System.out.println("Update with Wrong Mark Success: " + isUpdatedWithWrongMark); - -// 打印最终的值和标记 -boolean[] finalMark = new boolean[1]; -String finalRef = amr.get(finalMark); -System.out.println("Final Reference: " + finalRef + ", Final Mark: " + finalMark[0]); -``` - -输出结果如下: - -```plain -Initial Reference: SnailClimb, Initial Mark: false -Update Success: true -Updated Reference: Daisy, Updated Mark: true -Update with Wrong Mark Success: false -Final Reference: Daisy, Final Mark: true -``` - -## 对象的属性修改类型原子类 - -如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类。 - -- `AtomicIntegerFieldUpdater`:原子更新整形字段的更新器 -- `AtomicLongFieldUpdater`:原子更新长整形字段的更新器 -- `AtomicReferenceFieldUpdater`:原子更新引用类型里的字段的更新器 - -要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 volatile int 修饰符。 - -上面三个类提供的方法几乎相同,所以我们这里以 `AtomicIntegerFieldUpdater`为例子来介绍。 - -**`AtomicIntegerFieldUpdater` 类使用示例** : - -```java -// Person 类 -class Person { - private String name; - // 要使用 AtomicIntegerFieldUpdater,字段必须是 volatile int - volatile int age; - //省略getter/setter和toString -} - -// 创建 AtomicIntegerFieldUpdater 对象 -AtomicIntegerFieldUpdater ageUpdater = AtomicIntegerFieldUpdater.newUpdater(Person.class, "age"); - -// 创建 Person 对象 -Person person = new Person("SnailClimb", 22); - -// 打印初始值 -System.out.println("Initial Person: " + person); - -// 更新 age 字段 -ageUpdater.incrementAndGet(person); // 自增 -System.out.println("After Increment: " + person); - -ageUpdater.addAndGet(person, 5); // 增加 5 -System.out.println("After Adding 5: " + person); - -ageUpdater.compareAndSet(person, 28, 30); // 如果当前值是 28,则设置为 30 -System.out.println("After Compare and Set (28 to 30): " + person); - -// 尝试使用错误的比较值进行更新 -boolean isUpdated = ageUpdater.compareAndSet(person, 28, 35); // 这次应该失败 -System.out.println("Compare and Set (28 to 35) Success: " + isUpdated); -System.out.println("Final Person: " + person); -``` - -输出结果: - -```plain -Initial Person: Name: SnailClimb, Age: 22 -After Increment: Name: SnailClimb, Age: 23 -After Adding 5: Name: SnailClimb, Age: 28 -After Compare and Set (28 to 30): Name: SnailClimb, Age: 30 -Compare and Set (28 to 35) Success: false -Final Person: Name: SnailClimb, Age: 30 -``` - -## 参考 - -- 《Java 并发编程的艺术》 - - +// Use getAndAdd method to get the current value and add the specified value 5 +temp \ No newline at end of file diff --git a/docs/java/concurrent/cas.md b/docs/java/concurrent/cas.md index af97f28d0c8..fa9f5bd9d6d 100644 --- a/docs/java/concurrent/cas.md +++ b/docs/java/concurrent/cas.md @@ -1,133 +1,133 @@ --- -title: CAS 详解 +title: Detailed Explanation of CAS category: Java tag: - - Java并发 + - Java Concurrent --- -乐观锁和悲观锁的介绍以及乐观锁常见实现方式可以阅读笔者写的这篇文章:[乐观锁和悲观锁详解](https://javaguide.cn/java/concurrent/optimistic-lock-and-pessimistic-lock.html)。 +An introduction to optimistic and pessimistic locks, as well as common implementation methods of optimistic locks, can be found in this article written by the author: [Detailed Explanation of Optimistic and Pessimistic Locks](https://javaguide.cn/java/concurrent/optimistic-lock-and-pessimistic-lock.html). -这篇文章主要介绍 :Java 中 CAS 的实现以及 CAS 存在的一些问题。 +This article mainly introduces the implementation of CAS (Compare-And-Swap) in Java and some issues associated with CAS. -## Java 中 CAS 是如何实现的? +## How is CAS Implemented in Java? -在 Java 中,实现 CAS(Compare-And-Swap, 比较并交换)操作的一个关键类是`Unsafe`。 +In Java, a key class for implementing CAS is `Unsafe`. -`Unsafe`类位于`sun.misc`包下,是一个提供低级别、不安全操作的类。由于其强大的功能和潜在的危险性,它通常用于 JVM 内部或一些需要极高性能和底层访问的库中,而不推荐普通开发者在应用程序中使用。关于 `Unsafe`类的详细介绍,可以阅读这篇文章:📌[Java 魔法类 Unsafe 详解](https://javaguide.cn/java/basis/unsafe.html)。 +The `Unsafe` class is located in the `sun.misc` package and provides low-level, unsafe operations. Due to its powerful functionalities and potential dangers, it is typically used internally in the JVM or in libraries that require extremely high performance and low-level access, and is not recommended for use by average developers in applications. For a detailed introduction to the `Unsafe` class, you can read this article: 📌[Detailed Explanation of Java Magic Class Unsafe](https://javaguide.cn/java/basis/unsafe.html). -`sun.misc`包下的`Unsafe`类提供了`compareAndSwapObject`、`compareAndSwapInt`、`compareAndSwapLong`方法来实现的对`Object`、`int`、`long`类型的 CAS 操作: +The `Unsafe` class under the `sun.misc` package provides methods like `compareAndSwapObject`, `compareAndSwapInt`, and `compareAndSwapLong` for implementing CAS operations on `Object`, `int`, and `long` types: ```java /** - * 以原子方式更新对象字段的值。 + * Atomically updates the value of an object field. * - * @param o 要操作的对象 - * @param offset 对象字段的内存偏移量 - * @param expected 期望的旧值 - * @param x 要设置的新值 - * @return 如果值被成功更新,则返回 true;否则返回 false + * @param o The object to operate on + * @param offset The memory offset of the object field + * @param expected The expected old value + * @param x The new value to set + * @return true if the value was successfully updated; otherwise, false */ boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); /** - * 以原子方式更新 int 类型的对象字段的值。 + * Atomically updates the value of an int type object field. */ boolean compareAndSwapInt(Object o, long offset, int expected, int x); /** - * 以原子方式更新 long 类型的对象字段的值。 + * Atomically updates the value of a long type object field. */ boolean compareAndSwapLong(Object o, long offset, long expected, long x); ``` -`Unsafe`类中的 CAS 方法是`native`方法。`native`关键字表明这些方法是用本地代码(通常是 C 或 C++)实现的,而不是用 Java 实现的。这些方法直接调用底层的硬件指令来实现原子操作。也就是说,Java 语言并没有直接用 Java 实现 CAS。 +The CAS methods in the `Unsafe` class are `native` methods. The `native` keyword indicates that these methods are implemented in native code (usually C or C++) rather than in Java. These methods directly call low-level hardware instructions to achieve atomic operations. In other words, the CAS operations are not directly implemented in Java. -更准确点来说,Java 中 CAS 是 C++ 内联汇编的形式实现的,通过 JNI(Java Native Interface) 调用。因此,CAS 的具体实现与操作系统以及 CPU 密切相关。 +More precisely, CAS in Java is implemented in the form of C++ inline assembly and is called via JNI (Java Native Interface). Therefore, the specific implementation of CAS is closely related to the operating system and the CPU. -`java.util.concurrent.atomic` 包提供了一些用于原子操作的类。 +The `java.util.concurrent.atomic` package provides several classes for atomic operations. -![JUC原子类概览](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88.png) +![Overview of JUC Atomic Classes](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88.png) -关于这些 Atomic 原子类的介绍和使用,可以阅读这篇文章:[Atomic 原子类总结](https://javaguide.cn/java/concurrent/atomic-classes.html)。 +For an introduction to these Atomic classes and their usage, you can read this article: [Summary of Atomic Classes](https://javaguide.cn/java/concurrent/atomic-classes.html). -Atomic 类依赖于 CAS 乐观锁来保证其方法的原子性,而不需要使用传统的锁机制(如 `synchronized` 块或 `ReentrantLock`)。 +Atomic classes rely on CAS optimistic locks to ensure the atomicity of their methods without the need for traditional locking mechanisms such as `synchronized` blocks or `ReentrantLock`. -`AtomicInteger`是 Java 的原子类之一,主要用于对 `int` 类型的变量进行原子操作,它利用`Unsafe`类提供的低级别原子操作方法实现无锁的线程安全性。 +`AtomicInteger` is one of Java's atomic classes, mainly used for atomic operations on `int` type variables. It leverages low-level atomic operation methods provided by the `Unsafe` class to achieve lock-free thread safety. -下面,我们通过解读`AtomicInteger`的核心源码(JDK1.8),来说明 Java 如何使用`Unsafe`类的方法来实现原子操作。 +Next, we will explain how Java uses methods from the `Unsafe` class to implement atomic operations by interpreting the core source code of `AtomicInteger` (JDK1.8). -`AtomicInteger`核心源码如下: +The core source code of `AtomicInteger` is as follows: ```java -// 获取 Unsafe 实例 +// Get Unsafe instance private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { - // 获取“value”字段在AtomicInteger类中的内存偏移量 + // Get the memory offset of the "value" field in the AtomicInteger class valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } -// 确保“value”字段的可见性 +// Ensure the visibility of the "value" field private volatile int value; -// 如果当前值等于预期值,则原子地将值设置为newValue -// 使用 Unsafe#compareAndSwapInt 方法进行CAS操作 +// Atomically sets the value to newValue if the current value equals expected +// Uses Unsafe#compareAndSwapInt method for CAS operation public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } -// 原子地将当前值加 delta 并返回旧值 +// Atomically adds delta to the current value and returns the old value public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } -// 原子地将当前值加 1 并返回加之前的值(旧值) -// 使用 Unsafe#getAndAddInt 方法进行CAS操作。 +// Atomically adds 1 to the current value and returns the value before addition (old value) +// Uses Unsafe#getAndAddInt method for CAS operation. public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } -// 原子地将当前值减 1 并返回减之前的值(旧值) +// Atomically subtracts 1 from the current value and returns the value before subtraction (old value) public final int getAndDecrement() { return unsafe.getAndAddInt(this, valueOffset, -1); } ``` -`Unsafe#getAndAddInt`源码: +Source code of `Unsafe#getAndAddInt`: ```java -// 原子地获取并增加整数值 +// Atomically gets and adds an integer value public final int getAndAddInt(Object o, long offset, int delta) { int v; do { - // 以 volatile 方式获取对象 o 在内存偏移量 offset 处的整数值 + // Get the integer value at memory offset of o in a volatile manner v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); - // 返回旧值 + // Return the old value return v; } ``` -可以看到,`getAndAddInt` 使用了 `do-while` 循环:在`compareAndSwapInt`操作失败时,会不断重试直到成功。也就是说,`getAndAddInt`方法会通过 `compareAndSwapInt` 方法来尝试更新 `value` 的值,如果更新失败(当前值在此期间被其他线程修改),它会重新获取当前值并再次尝试更新,直到操作成功。 +It can be seen that `getAndAddInt` uses a `do-while` loop: it continually retries until the `compareAndSwapInt` operation succeeds. In other words, the `getAndAddInt` method attempts to update the value using the `compareAndSwapInt` method. If the update fails (because the value has been modified by another thread in the meantime), it retrieves the current value again and retries the update until the operation succeeds. -由于 CAS 操作可能会因为并发冲突而失败,因此通常会与`while`循环搭配使用,在失败后不断重试,直到操作成功。这就是 **自旋锁机制** 。 +Since CAS operations may fail due to concurrent conflicts, they are often used in conjunction with a `while` loop to keep retrying until the operation succeeds. This is known as **spin-lock mechanism**. -## CAS 算法存在哪些问题? +## What Issues Exist with CAS Algorithms? -ABA 问题是 CAS 算法最常见的问题。 +The ABA problem is the most common issue with CAS algorithms. -### ABA 问题 +### ABA Problem -如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 **"ABA"问题。** +If a variable V is initially read as value A, and when preparing to assign a new value, it checks that it is still A, can we conclude that its value has not been modified by other threads? Clearly, this is not the case, because during that period, its value may have been changed to another value and then back to A, causing the CAS operation to mistakenly assume it was never modified. This issue is referred to as the **"ABA" problem** of CAS operations. -ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 +The solution to the ABA problem is to prepend a **version number or timestamp** to the variable. The `AtomicStampedReference` class introduced in JDK 1.5 is designed to solve the ABA problem, where the `compareAndSet()` method first checks if the current reference equals the expected reference and if the current stamp equals the expected stamp. If both are equal, it atomically sets the reference and stamp to the provided update values. ```java -public boolean compareAndSet(V expectedReference, - V newReference, +public boolean compareAndSet(V expectedReference, + V newReference, int expectedStamp, int newStamp) { Pair current = pair; @@ -140,23 +140,23 @@ public boolean compareAndSet(V expectedReference, } ``` -### 循环时间长开销大 +### Long Spin Time and High Overhead -CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。 +CAS often uses spinning to retry, meaning it keeps looping until it succeeds. If it fails for a long time, it can cause significant execution overhead for the CPU. -如果 JVM 能够支持处理器提供的`pause`指令,那么自旋操作的效率将有所提升。`pause`指令有两个重要作用: +If the JVM can support the `pause` instruction provided by the processor, the efficiency of spin operations will be improved. The `pause` instruction has two key benefits: -1. **延迟流水线执行指令**:`pause`指令可以延迟指令的执行,从而减少 CPU 的资源消耗。具体的延迟时间取决于处理器的实现版本,在某些处理器上,延迟时间可能为零。 -2. **避免内存顺序冲突**:在退出循环时,`pause`指令可以避免由于内存顺序冲突而导致的 CPU 流水线被清空,从而提高 CPU 的执行效率。 +1. **Delaying Pipeline Execution Instructions**: The `pause` instruction can delay the execution of instructions, thereby reducing CPU resource consumption. The specific delay time depends on the implementation version of the processor; on some processors, the delay may be zero. +1. **Avoiding Memory Ordering Conflicts**: When exiting the loop, the `pause` instruction can prevent the CPU pipeline from being flushed due to memory ordering conflicts, thus improving CPU execution efficiency. -### 只能保证一个共享变量的原子操作 +### Can Only Ensure Atomic Operations for One Shared Variable -CAS 操作仅能对单个共享变量有效。当需要操作多个共享变量时,CAS 就显得无能为力。不过,从 JDK 1.5 开始,Java 提供了`AtomicReference`类,这使得我们能够保证引用对象之间的原子性。通过将多个变量封装在一个对象中,我们可以使用`AtomicReference`来执行 CAS 操作。 +CAS operations can only effectively operate on a single shared variable. When multiple shared variables need to be manipulated, CAS becomes ineffective. However, starting from JDK 1.5, Java provides the `AtomicReference` class, which allows us to ensure the atomicity of references between objects. By encapsulating multiple variables within a single object, we can use `AtomicReference` to perform CAS operations. -除了 `AtomicReference` 这种方式之外,还可以利用加锁来保证。 +In addition to this method via `AtomicReference`, locking can also be used to ensure atomicity. -## 总结 +## Summary -在 Java 中,CAS 通过 `Unsafe` 类中的 `native` 方法实现,这些方法调用底层的硬件指令来完成原子操作。由于其实现依赖于 C++ 内联汇编和 JNI 调用,因此 CAS 的具体实现与操作系统以及 CPU 密切相关。 +In Java, CAS is implemented through `native` methods in the `Unsafe` class, which directly call low-level hardware instructions to complete atomic operations. Because its implementation relies on C++ inline assembly and JNI calls, the specific implementation of CAS is closely related to the operating system and CPU. -CAS 虽然具有高效的无锁特性,但也需要注意 ABA 、循环时间长开销大等问题。 +While CAS has high-efficiency lock-free characteristics, it is also essential to be aware of issues such as the ABA problem and the high overhead caused by long spin times. diff --git a/docs/java/concurrent/completablefuture-intro.md b/docs/java/concurrent/completablefuture-intro.md index be21c70e1c7..25e78d1aef5 100644 --- a/docs/java/concurrent/completablefuture-intro.md +++ b/docs/java/concurrent/completablefuture-intro.md @@ -1,732 +1,73 @@ --- -title: CompletableFuture 详解 +title: CompletableFuture Explained category: Java tag: - - Java并发 + - Java Concurrency --- -实际项目中,一个接口可能需要同时获取多种不同的数据,然后再汇总返回,这种场景还是挺常见的。举个例子:用户请求获取订单信息,可能需要同时获取用户信息、商品详情、物流信息、商品推荐等数据。 +In actual projects, an interface may need to simultaneously retrieve various types of data and then summarize and return them. This scenario is quite common. For example, when a user requests order information, it may be necessary to simultaneously obtain user information, product details, logistics information, product recommendations, and other data. -如果是串行(按顺序依次执行每个任务)执行的话,接口的响应速度会非常慢。考虑到这些任务之间有大部分都是 **无前后顺序关联** 的,可以 **并行执行** ,就比如说调用获取商品详情的时候,可以同时调用获取物流信息。通过并行执行多个任务的方式,接口的响应速度会得到大幅优化。 +If executed serially (executing each task in order), the response speed of the interface will be very slow. Considering that most of these tasks are **not sequentially dependent**, they can be **executed in parallel**. For instance, when calling to get product details, logistics information can be retrieved simultaneously. By executing multiple tasks in parallel, the response speed of the interface can be significantly optimized. ![](https://oss.javaguide.cn/github/javaguide/high-performance/serial-to-parallel.png) -对于存在前后调用顺序关系的任务,可以进行任务编排。 +For tasks that have a sequential calling relationship, task orchestration can be performed. ![](https://oss.javaguide.cn/github/javaguide/high-performance/serial-to-parallel2.png) -1. 获取用户信息之后,才能调用商品详情和物流信息接口。 -2. 成功获取商品详情和物流信息之后,才能调用商品推荐接口。 +1. User information must be obtained before calling the product details and logistics information interfaces. +2. The product recommendation interface can only be called after successfully obtaining product details and logistics information. -可能会用到多线程异步任务编排的场景(这里只是举例,数据不一定是一次返回,可能会对接口进行拆分): +There may be scenarios that require multi-threaded asynchronous task orchestration (this is just an example; the data may not be returned all at once and may involve splitting the interface): -1. 首页:例如技术社区的首页可能需要同时获取文章推荐列表、广告栏、文章排行榜、热门话题等信息。 -2. 详情页:例如技术社区的文章详情页可能需要同时获取作者信息、文章详情、文章评论等信息。 -3. 统计模块:例如技术社区的后台统计模块可能需要同时获取粉丝数汇总、文章数据(阅读量、评论量、收藏量)汇总等信息。 +1. Homepage: For example, the homepage of a technology community may need to simultaneously retrieve article recommendation lists, advertisements, article rankings, hot topics, and other information. +2. Detail page: For example, the article detail page of a technology community may need to simultaneously retrieve author information, article details, article comments, and other information. +3. Statistics module: For example, the backend statistics module of a technology community may need to simultaneously retrieve fan count summaries, article data (views, comments, favorites) summaries, and other information. -对于 Java 程序来说,Java 8 才被引入的 `CompletableFuture` 可以帮助我们来做多个任务的编排,功能非常强大。 +For Java programs, the `CompletableFuture` introduced in Java 8 can help us orchestrate multiple tasks and is very powerful. -这篇文章是 `CompletableFuture` 的简单入门,带大家看看 `CompletableFuture` 常用的 API。 +This article is a simple introduction to `CompletableFuture`, showcasing commonly used APIs. -## Future 介绍 +## Introduction to Future -`Future` 类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。具体来说是这样的:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 `Future` 类获取到耗时任务的执行结果。这样一来,程序的执行效率就明显提高了。 +The `Future` class is a typical application of asynchronous thinking, mainly used in scenarios that require executing time-consuming tasks, avoiding the program waiting idly for the completion of these tasks, which is inefficient. Specifically, when we execute a time-consuming task, we can hand this task over to a child thread for asynchronous execution while we can do other things without waiting for the time-consuming task to complete. Once we finish our tasks, we can retrieve the execution result of the time-consuming task through the `Future` class. This way, the execution efficiency of the program is significantly improved. -这其实就是多线程中经典的 **Future 模式**,你可以将其看作是一种设计模式,核心思想是异步调用,主要用在多线程领域,并非 Java 语言独有。 +This is essentially the classic **Future pattern** in multithreading, which can be seen as a design pattern. The core idea is asynchronous calling, mainly used in the multithreading domain, and is not unique to the Java language. -在 Java 中,`Future` 类只是一个泛型接口,位于 `java.util.concurrent` 包下,其中定义了 5 个方法,主要包括下面这 4 个功能: +In Java, the `Future` class is just a generic interface located in the `java.util.concurrent` package, which defines five methods, mainly including the following four functionalities: -- 取消任务; -- 判断任务是否被取消; -- 判断任务是否已经执行完成; -- 获取任务执行结果。 +- Cancel the task; +- Determine if the task has been canceled; +- Determine if the task has completed execution; +- Retrieve the task execution result. ```java -// V 代表了Future执行的任务返回值的类型 +// V represents the type of the return value of the Future task public interface Future { - // 取消任务执行 - // 成功取消返回 true,否则返回 false + // Cancel task execution + // Returns true if successfully canceled, otherwise returns false boolean cancel(boolean mayInterruptIfRunning); - // 判断任务是否被取消 + // Determine if the task has been canceled boolean isCancelled(); - // 判断任务是否已经执行完成 + // Determine if the task has completed execution boolean isDone(); - // 获取任务执行结果 + // Retrieve the task execution result V get() throws InterruptedException, ExecutionException; - // 指定时间内没有返回计算结果就抛出 TimeOutException 异常 + // Throws TimeoutException if the result is not returned within the specified time V get(long timeout, TimeUnit unit) - - throws InterruptedException, ExecutionException, TimeoutExceptio - + throws InterruptedException, ExecutionException, TimeoutException; } ``` -简单理解就是:我有一个任务,提交给了 `Future` 来处理。任务执行期间我自己可以去做任何想做的事情。并且,在这期间我还可以取消任务以及获取任务的执行状态。一段时间之后,我就可以 `Future` 那里直接取出任务执行结果。 - -## CompletableFuture 介绍 - -`Future` 在实际使用过程中存在一些局限性,比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。 - -Java 8 才被引入`CompletableFuture` 类可以解决`Future` 的这些缺陷。`CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。 - -下面我们来简单看看 `CompletableFuture` 类的定义。 - -```java -public class CompletableFuture implements Future, CompletionStage { -} -``` - -可以看到,`CompletableFuture` 同时实现了 `Future` 和 `CompletionStage` 接口。 - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/completablefuture-class-diagram.jpg) - -`CompletionStage` 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。 - -`CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程的能力。 - -![](https://oss.javaguide.cn/javaguide/image-20210902092441434.png) - -`Future` 接口有 5 个方法: - -- `boolean cancel(boolean mayInterruptIfRunning)`:尝试取消执行任务。 -- `boolean isCancelled()`:判断任务是否被取消。 -- `boolean isDone()`:判断任务是否已经被执行完成。 -- `get()`:等待任务执行完成并获取运算结果。 -- `get(long timeout, TimeUnit unit)`:多了一个超时时间。 +In simple terms: I have a task that I submitted to `Future` for processing. During the task execution, I can do anything I want. Additionally, during this time, I can also cancel the task and check the task's execution status. After a while, I can directly retrieve the task execution result from `Future`. -`CompletionStage` 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。 +## Introduction to CompletableFuture -`CompletionStage` 接口中的方法比较多,`CompletableFuture` 的函数式能力就是这个接口赋予的。从这个接口的方法参数你就可以发现其大量使用了 Java8 引入的函数式编程。 +`Future` has some limitations in practical use, such as not supporting orchestration and combination of asynchronous tasks, and the `get()` method for obtaining computation results is a blocking call. -![](https://oss.javaguide.cn/javaguide/image-20210902093026059.png) - -由于方法众多,所以这里不能一一讲解,下文中我会介绍大部分常见方法的使用。 - -## CompletableFuture 常见操作 - -### 创建 CompletableFuture - -常见的创建 `CompletableFuture` 对象的方法如下: - -1. 通过 new 关键字。 -2. 基于 `CompletableFuture` 自带的静态工厂方法:`runAsync()`、`supplyAsync()` 。 - -#### new 关键字 - -通过 new 关键字创建 `CompletableFuture` 对象这种使用方式可以看作是将 `CompletableFuture` 当做 `Future` 来使用。 - -我在我的开源项目 [guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) 中就是这种方式创建的 `CompletableFuture` 对象。 - -下面咱们来看一个简单的案例。 - -我们通过创建了一个结果值类型为 `RpcResponse` 的 `CompletableFuture`,你可以把 `resultFuture` 看作是异步运算结果的载体。 - -```java -CompletableFuture> resultFuture = new CompletableFuture<>(); -``` - -假设在未来的某个时刻,我们得到了最终的结果。这时,我们可以调用 `complete()` 方法为其传入结果,这表示 `resultFuture` 已经被完成了。 - -```java -// complete() 方法只能调用一次,后续调用将被忽略。 -resultFuture.complete(rpcResponse); -``` +The `CompletableFuture` class introduced in Java 8 can solve these defects of `Future`. In addition to providing more user-friendly and powerful `Future` features, `CompletableFuture` also offers functional programming capabilities and asynchronous task orchestration and combination (allowing multiple asynchronous tasks to be chained together to form a complete chain call). -你可以通过 `isDone()` 方法来检查是否已经完成。 +Let's take a brief look at the definition of the `CompletableFuture` class. ```java -public boolean isDone() { - return result != null; -} -``` - -获取异步计算的结果也非常简单,直接调用 `get()` 方法即可。调用 `get()` 方法的线程会阻塞直到 `CompletableFuture` 完成运算。 - -```java -rpcResponse = completableFuture.get(); -``` - -如果你已经知道计算的结果的话,可以使用静态方法 `completedFuture()` 来创建 `CompletableFuture` 。 - -```java -CompletableFuture future = CompletableFuture.completedFuture("hello!"); -assertEquals("hello!", future.get()); -``` - -`completedFuture()` 方法底层调用的是带参数的 new 方法,只不过,这个方法不对外暴露。 - -```java -public static CompletableFuture completedFuture(U value) { - return new CompletableFuture((value == null) ? NIL : value); -} -``` - -#### 静态工厂方法 - -这两个方法可以帮助我们封装计算逻辑。 - -```java -static CompletableFuture supplyAsync(Supplier supplier); -// 使用自定义线程池(推荐) -static CompletableFuture supplyAsync(Supplier supplier, Executor executor); -static CompletableFuture runAsync(Runnable runnable); -// 使用自定义线程池(推荐) -static CompletableFuture runAsync(Runnable runnable, Executor executor); -``` - -`runAsync()` 方法接受的参数是 `Runnable` ,这是一个函数式接口,不允许返回值。当你需要异步操作且不关心返回结果的时候可以使用 `runAsync()` 方法。 - -```java -@FunctionalInterface -public interface Runnable { - public abstract void run(); -} -``` - -`supplyAsync()` 方法接受的参数是 `Supplier` ,这也是一个函数式接口,`U` 是返回结果值的类型。 - -```java -@FunctionalInterface -public interface Supplier { - - /** - * Gets a result. - * - * @return a result - */ - T get(); -} -``` - -当你需要异步操作且关心返回结果的时候,可以使用 `supplyAsync()` 方法。 - -```java -CompletableFuture future = CompletableFuture.runAsync(() -> System.out.println("hello!")); -future.get();// 输出 "hello!" -CompletableFuture future2 = CompletableFuture.supplyAsync(() -> "hello!"); -assertEquals("hello!", future2.get()); -``` - -### 处理异步结算的结果 - -当我们获取到异步计算的结果之后,还可以对其进行进一步的处理,比较常用的方法有下面几个: - -- `thenApply()` -- `thenAccept()` -- `thenRun()` -- `whenComplete()` - -`thenApply()` 方法接受一个 `Function` 实例,用它来处理结果。 - -```java -// 沿用上一个任务的线程池 -public CompletableFuture thenApply( - Function fn) { - return uniApplyStage(null, fn); -} - -//使用默认的 ForkJoinPool 线程池(不推荐) -public CompletableFuture thenApplyAsync( - Function fn) { - return uniApplyStage(defaultExecutor(), fn); -} -// 使用自定义线程池(推荐) -public CompletableFuture thenApplyAsync( - Function fn, Executor executor) { - return uniApplyStage(screenExecutor(executor), fn); -} -``` - -`thenApply()` 方法使用示例如下: - -```java -CompletableFuture future = CompletableFuture.completedFuture("hello!") - .thenApply(s -> s + "world!"); -assertEquals("hello!world!", future.get()); -// 这次调用将被忽略。 -future.thenApply(s -> s + "nice!"); -assertEquals("hello!world!", future.get()); -``` - -你还可以进行 **流式调用**: - -```java -CompletableFuture future = CompletableFuture.completedFuture("hello!") - .thenApply(s -> s + "world!").thenApply(s -> s + "nice!"); -assertEquals("hello!world!nice!", future.get()); -``` - -**如果你不需要从回调函数中获取返回结果,可以使用 `thenAccept()` 或者 `thenRun()`。这两个方法的区别在于 `thenRun()` 不能访问异步计算的结果。** - -`thenAccept()` 方法的参数是 `Consumer` 。 - -```java -public CompletableFuture thenAccept(Consumer action) { - return uniAcceptStage(null, action); -} - -public CompletableFuture thenAcceptAsync(Consumer action) { - return uniAcceptStage(defaultExecutor(), action); -} - -public CompletableFuture thenAcceptAsync(Consumer action, - Executor executor) { - return uniAcceptStage(screenExecutor(executor), action); -} -``` - -顾名思义,`Consumer` 属于消费型接口,它可以接收 1 个输入对象然后进行“消费”。 - -```java -@FunctionalInterface -public interface Consumer { - - void accept(T t); - - default Consumer andThen(Consumer after) { - Objects.requireNonNull(after); - return (T t) -> { accept(t); after.accept(t); }; - } -} -``` - -`thenRun()` 的方法是的参数是 `Runnable` 。 - -```java -public CompletableFuture thenRun(Runnable action) { - return uniRunStage(null, action); -} - -public CompletableFuture thenRunAsync(Runnable action) { - return uniRunStage(defaultExecutor(), action); -} - -public CompletableFuture thenRunAsync(Runnable action, - Executor executor) { - return uniRunStage(screenExecutor(executor), action); -} -``` - -`thenAccept()` 和 `thenRun()` 使用示例如下: - -```java -CompletableFuture.completedFuture("hello!") - .thenApply(s -> s + "world!").thenApply(s -> s + "nice!").thenAccept(System.out::println);//hello!world!nice! - -CompletableFuture.completedFuture("hello!") - .thenApply(s -> s + "world!").thenApply(s -> s + "nice!").thenRun(() -> System.out.println("hello!"));//hello! -``` - -`whenComplete()` 的方法的参数是 `BiConsumer` 。 - -```java -public CompletableFuture whenComplete( - BiConsumer action) { - return uniWhenCompleteStage(null, action); -} - - -public CompletableFuture whenCompleteAsync( - BiConsumer action) { - return uniWhenCompleteStage(defaultExecutor(), action); -} -// 使用自定义线程池(推荐) -public CompletableFuture whenCompleteAsync( - BiConsumer action, Executor executor) { - return uniWhenCompleteStage(screenExecutor(executor), action); -} -``` - -相对于 `Consumer` , `BiConsumer` 可以接收 2 个输入对象然后进行“消费”。 - -```java -@FunctionalInterface -public interface BiConsumer { - void accept(T t, U u); - - default BiConsumer andThen(BiConsumer after) { - Objects.requireNonNull(after); - - return (l, r) -> { - accept(l, r); - after.accept(l, r); - }; - } -} -``` - -`whenComplete()` 使用示例如下: - -```java -CompletableFuture future = CompletableFuture.supplyAsync(() -> "hello!") - .whenComplete((res, ex) -> { - // res 代表返回的结果 - // ex 的类型为 Throwable ,代表抛出的异常 - System.out.println(res); - // 这里没有抛出异常所有为 null - assertNull(ex); - }); -assertEquals("hello!", future.get()); -``` - -### 异常处理 - -你可以通过 `handle()` 方法来处理任务执行过程中可能出现的抛出异常的情况。 - -```java -public CompletableFuture handle( - BiFunction fn) { - return uniHandleStage(null, fn); -} - -public CompletableFuture handleAsync( - BiFunction fn) { - return uniHandleStage(defaultExecutor(), fn); -} - -public CompletableFuture handleAsync( - BiFunction fn, Executor executor) { - return uniHandleStage(screenExecutor(executor), fn); -} -``` - -示例代码如下: - -```java -CompletableFuture future - = CompletableFuture.supplyAsync(() -> { - if (true) { - throw new RuntimeException("Computation error!"); - } - return "hello!"; -}).handle((res, ex) -> { - // res 代表返回的结果 - // ex 的类型为 Throwable ,代表抛出的异常 - return res != null ? res : "world!"; -}); -assertEquals("world!", future.get()); -``` - -你还可以通过 `exceptionally()` 方法来处理异常情况。 - -```java -CompletableFuture future - = CompletableFuture.supplyAsync(() -> { - if (true) { - throw new RuntimeException("Computation error!"); - } - return "hello!"; -}).exceptionally(ex -> { - System.out.println(ex.toString());// CompletionException - return "world!"; -}); -assertEquals("world!", future.get()); -``` - -如果你想让 `CompletableFuture` 的结果就是异常的话,可以使用 `completeExceptionally()` 方法为其赋值。 - -```java -CompletableFuture completableFuture = new CompletableFuture<>(); -// ... -completableFuture.completeExceptionally( - new RuntimeException("Calculation failed!")); -// ... -completableFuture.get(); // ExecutionException -``` - -### 组合 CompletableFuture - -你可以使用 `thenCompose()` 按顺序链接两个 `CompletableFuture` 对象,实现异步的任务链。它的作用是将前一个任务的返回结果作为下一个任务的输入参数,从而形成一个依赖关系。 - -```java -public CompletableFuture thenCompose( - Function> fn) { - return uniComposeStage(null, fn); -} - -public CompletableFuture thenComposeAsync( - Function> fn) { - return uniComposeStage(defaultExecutor(), fn); -} - -public CompletableFuture thenComposeAsync( - Function> fn, - Executor executor) { - return uniComposeStage(screenExecutor(executor), fn); -} -``` - -`thenCompose()` 方法会使用示例如下: - -```java -CompletableFuture future - = CompletableFuture.supplyAsync(() -> "hello!") - .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "world!")); -assertEquals("hello!world!", future.get()); -``` - -在实际开发中,这个方法还是非常有用的。比如说,task1 和 task2 都是异步执行的,但 task1 必须执行完成后才能开始执行 task2(task2 依赖 task1 的执行结果)。 - -和 `thenCompose()` 方法类似的还有 `thenCombine()` 方法, 它同样可以组合两个 `CompletableFuture` 对象。 - -```java -CompletableFuture completableFuture - = CompletableFuture.supplyAsync(() -> "hello!") - .thenCombine(CompletableFuture.supplyAsync( - () -> "world!"), (s1, s2) -> s1 + s2) - .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "nice!")); -assertEquals("hello!world!nice!", completableFuture.get()); -``` - -**那 `thenCompose()` 和 `thenCombine()` 有什么区别呢?** - -- `thenCompose()` 可以链接两个 `CompletableFuture` 对象,并将前一个任务的返回结果作为下一个任务的参数,它们之间存在着先后顺序。 -- `thenCombine()` 会在两个任务都执行完成后,把两个任务的结果合并。两个任务是并行执行的,它们之间并没有先后依赖顺序。 - -除了 `thenCompose()` 和 `thenCombine()` 之外, 还有一些其他的组合 `CompletableFuture` 的方法用于实现不同的效果,满足不同的业务需求。 - -例如,如果我们想要实现 task1 和 task2 中的任意一个任务执行完后就执行 task3 的话,可以使用 `acceptEither()`。 - -```java -public CompletableFuture acceptEither( - CompletionStage other, Consumer action) { - return orAcceptStage(null, other, action); -} - -public CompletableFuture acceptEitherAsync( - CompletionStage other, Consumer action) { - return orAcceptStage(asyncPool, other, action); -} -``` - -简单举一个例子: - -```java -CompletableFuture task = CompletableFuture.supplyAsync(() -> { - System.out.println("任务1开始执行,当前时间:" + System.currentTimeMillis()); - try { - Thread.sleep(500); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println("任务1执行完毕,当前时间:" + System.currentTimeMillis()); - return "task1"; -}); - -CompletableFuture task2 = CompletableFuture.supplyAsync(() -> { - System.out.println("任务2开始执行,当前时间:" + System.currentTimeMillis()); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println("任务2执行完毕,当前时间:" + System.currentTimeMillis()); - return "task2"; -}); - -task.acceptEitherAsync(task2, (res) -> { - System.out.println("任务3开始执行,当前时间:" + System.currentTimeMillis()); - System.out.println("上一个任务的结果为:" + res); -}); - -// 增加一些延迟时间,确保异步任务有足够的时间完成 -try { - Thread.sleep(2000); -} catch (InterruptedException e) { - e.printStackTrace(); -} -``` - -输出: - -```plain -任务1开始执行,当前时间:1695088058520 -任务2开始执行,当前时间:1695088058521 -任务1执行完毕,当前时间:1695088059023 -任务3开始执行,当前时间:1695088059023 -上一个任务的结果为:task1 -任务2执行完毕,当前时间:1695088059523 -``` - -任务组合操作`acceptEitherAsync()`会在异步任务 1 和异步任务 2 中的任意一个完成时触发执行任务 3,但是需要注意,这个触发时机是不确定的。如果任务 1 和任务 2 都还未完成,那么任务 3 就不能被执行。 - -### 并行运行多个 CompletableFuture - -你可以通过 `CompletableFuture` 的 `allOf()`这个静态方法来并行运行多个 `CompletableFuture` 。 - -实际项目中,我们经常需要并行运行多个互不相关的任务,这些任务之间没有依赖关系,可以互相独立地运行。 - -比说我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。像这种情况我们就可以使用并行运行多个 `CompletableFuture` 来处理。 - -示例代码如下: - -```java -CompletableFuture task1 = - CompletableFuture.supplyAsync(()->{ - //自定义业务操作 - }); -...... -CompletableFuture task6 = - CompletableFuture.supplyAsync(()->{ - //自定义业务操作 - }); -...... - CompletableFuture headerFuture=CompletableFuture.allOf(task1,.....,task6); - - try { - headerFuture.join(); - } catch (Exception ex) { - ...... - } -System.out.println("all done. "); -``` - -经常和 `allOf()` 方法拿来对比的是 `anyOf()` 方法。 - -**`allOf()` 方法会等到所有的 `CompletableFuture` 都运行完成之后再返回** - -```java -Random rand = new Random(); -CompletableFuture future1 = CompletableFuture.supplyAsync(() -> { - try { - Thread.sleep(1000 + rand.nextInt(1000)); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - System.out.println("future1 done..."); - } - return "abc"; -}); -CompletableFuture future2 = CompletableFuture.supplyAsync(() -> { - try { - Thread.sleep(1000 + rand.nextInt(1000)); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - System.out.println("future2 done..."); - } - return "efg"; -}); -``` - -调用 `join()` 可以让程序等`future1` 和 `future2` 都运行完了之后再继续执行。 - -```java -CompletableFuture completableFuture = CompletableFuture.allOf(future1, future2); -completableFuture.join(); -assertTrue(completableFuture.isDone()); -System.out.println("all futures done..."); -``` - -输出: - -```plain -future1 done... -future2 done... -all futures done... -``` - -**`anyOf()` 方法不会等待所有的 `CompletableFuture` 都运行完成之后再返回,只要有一个执行完成即可!** - -```java -CompletableFuture f = CompletableFuture.anyOf(future1, future2); -System.out.println(f.get()); -``` - -输出结果可能是: - -```plain -future2 done... -efg -``` - -也可能是: - -```plain -future1 done... -abc -``` - -## CompletableFuture 使用建议 - -### 使用自定义线程池 - -我们上面的代码示例中,为了方便,都没有选择自定义线程池。实际项目中,这是不可取的。 - -`CompletableFuture` 默认使用全局共享的 `ForkJoinPool.commonPool()` 作为执行器,所有未指定执行器的异步任务都会使用该线程池。这意味着应用程序、多个库或框架(如 Spring、第三方库)若都依赖 `CompletableFuture`,默认情况下它们都会共享同一个线程池。 - -虽然 `ForkJoinPool` 效率很高,但当同时提交大量任务时,可能会导致资源竞争和线程饥饿,进而影响系统性能。 - -为避免这些问题,建议为 `CompletableFuture` 提供自定义线程池,带来以下优势: - -- **隔离性**:为不同任务分配独立的线程池,避免全局线程池资源争夺。 -- **资源控制**:根据任务特性调整线程池大小和队列类型,优化性能表现。 -- **异常处理**:通过自定义 `ThreadFactory` 更好地处理线程中的异常情况。 - -```java -private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue()); - -CompletableFuture.runAsync(() -> { - //... -}, executor); -``` - -### 尽量避免使用 get() - -`CompletableFuture`的`get()`方法是阻塞的,尽量避免使用。如果必须要使用的话,需要添加超时时间,否则可能会导致主线程一直等待,无法执行其他任务。 - -```java - CompletableFuture future = CompletableFuture.supplyAsync(() -> { - try { - Thread.sleep(10_000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - return "Hello, world!"; - }); - - // 获取异步任务的返回值,设置超时时间为 5 秒 - try { - String result = future.get(5, TimeUnit.SECONDS); - System.out.println(result); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - // 处理异常 - e.printStackTrace(); - } -} -``` - -上面这段代码在调用 `get()` 时抛出了 `TimeoutException` 异常。这样我们就可以在异常处理中进行相应的操作,比如取消任务、重试任务、记录日志等。 - -### 正确进行异常处理 - -使用 `CompletableFuture`的时候一定要以正确的方式进行异常处理,避免异常丢失或者出现不可控问题。 - -下面是一些建议: - -- 使用 `whenComplete` 方法可以在任务完成时触发回调函数,并正确地处理异常,而不是让异常被吞噬或丢失。 -- 使用 `exceptionally` 方法可以处理异常并重新抛出,以便异常能够传播到后续阶段,而不是让异常被忽略或终止。 -- 使用 `handle` 方法可以处理正常的返回结果和异常,并返回一个新的结果,而不是让异常影响正常的业务逻辑。 -- 使用 `CompletableFuture.allOf` 方法可以组合多个 `CompletableFuture`,并统一处理所有任务的异常,而不是让异常处理过于冗长或重复。 -- …… - -### 合理组合多个异步任务 - -正确使用 `thenCompose()` 、 `thenCombine()` 、`acceptEither()`、`allOf()`、`anyOf()`等方法来组合多个异步任务,以满足实际业务的需求,提高程序执行效率。 - -实际使用中,我们还可以利用或者参考现成的异步任务编排框架,比如京东的 [asyncTool](https://gitee.com/jd-platform-opensource/asyncTool) 。 - -![asyncTool README 文档](https://oss.javaguide.cn/github/javaguide/java/concurrent/asyncTool-readme.png) - -## 后记 - -这篇文章只是简单介绍了 `CompletableFuture` 的核心概念和比较常用的一些 API 。如果想要深入学习的话,还可以多找一些书籍和博客看,比如下面几篇文章就挺不错: - -- [CompletableFuture 原理与实践-外卖商家端 API 的异步化 - 美团技术团队](https://tech.meituan.com/2022/05/12/principles-and-practices-of-completablefuture.html):这篇文章详细介绍了 `CompletableFuture` 在实际项目中的运用。参考这篇文章,可以对项目中类似的场景进行优化,也算是一个小亮点了。这种性能优化方式比较简单且效果还不错! -- [读 RocketMQ 源码,学习并发编程三大神器 - 勇哥 java 实战分享](https://mp.weixin.qq.com/s/32Ak-WFLynQfpn0Cg0N-0A):这篇文章介绍了 RocketMQ 对`CompletableFuture`的应用。具体来说,从 RocketMQ 4.7 开始,RocketMQ 引入了 `CompletableFuture`来实现异步消息处理 。 - -另外,建议 G 友们可以看看京东的 [asyncTool](https://gitee.com/jd-platform-opensource/asyncTool) 这个并发框架,里面大量使用到了 `CompletableFuture` 。 - - +public class CompletableFuture \ No newline at end of file diff --git a/docs/java/concurrent/java-concurrent-collections.md b/docs/java/concurrent/java-concurrent-collections.md index 45aa258818a..20ffa348d38 100644 --- a/docs/java/concurrent/java-concurrent-collections.md +++ b/docs/java/concurrent/java-concurrent-collections.md @@ -1,77 +1,77 @@ --- -title: Java 常见并发容器总结 +title: Summary of Common Concurrent Containers in Java category: Java tag: - - Java并发 + - Java Concurrency --- -JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。 +Most of the containers provided by the JDK are in the `java.util.concurrent` package. -- **`ConcurrentHashMap`** : 线程安全的 `HashMap` -- **`CopyOnWriteArrayList`** : 线程安全的 `List`,在读多写少的场合性能非常好,远远好于 `Vector`。 -- **`ConcurrentLinkedQueue`** : 高效的并发队列,使用链表实现。可以看做一个线程安全的 `LinkedList`,这是一个非阻塞队列。 -- **`BlockingQueue`** : 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。 -- **`ConcurrentSkipListMap`** : 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。 +- **`ConcurrentHashMap`**: Thread-safe `HashMap` +- **`CopyOnWriteArrayList`**: Thread-safe `List`, performs very well in read-heavy scenarios, far better than `Vector`. +- **`ConcurrentLinkedQueue`**: Efficient concurrent queue implemented with a linked list. Can be seen as a thread-safe `LinkedList`, which is a non-blocking queue. +- **`BlockingQueue`**: This is an interface, implemented internally by the JDK using linked lists, arrays, etc. It represents a blocking queue, making it very suitable for use as a data-sharing channel. +- **`ConcurrentSkipListMap`**: An implementation of a skip list. This is a Map that uses skip list data structure for fast lookups. ## ConcurrentHashMap -我们知道,`HashMap` 是线程不安全的,如果在并发场景下使用,一种常见的解决方式是通过 `Collections.synchronizedMap()` 方法对 `HashMap` 进行包装,使其变为线程安全。不过,这种方式是通过一个全局锁来同步不同线程间的并发访问,会导致严重的性能瓶颈,尤其是在高并发场景下。 +We know that `HashMap` is thread-unsafe. In concurrent scenarios, a common solution is to wrap `HashMap` using `Collections.synchronizedMap()` to make it thread-safe. However, this approach synchronizes concurrent access between threads using a global lock, which can lead to severe performance bottlenecks, especially in high-concurrency situations. -为了解决这一问题,`ConcurrentHashMap` 应运而生,作为 `HashMap` 的线程安全版本,它提供了更高效的并发处理能力。 +To address this issue, `ConcurrentHashMap` was created as a thread-safe version of `HashMap`, providing more efficient concurrent processing capabilities. -在 JDK1.7 的时候,`ConcurrentHashMap` 对整个桶数组进行了分割分段(`Segment`,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 +In JDK 1.7, `ConcurrentHashMap` split the entire bucket array into segments (Segment, segmented locks), where each lock only locks a portion of the data in the container (illustration below). Access to different data segments by multiple threads avoids lock contention, increasing the concurrent access rate. -![Java7 ConcurrentHashMap 存储结构](https://oss.javaguide.cn/github/javaguide/java/collection/java7_concurrenthashmap.png) +![Java7 ConcurrentHashMap Storage Structure](https://oss.javaguide.cn/github/javaguide/java/collection/java7_concurrenthashmap.png) -到了 JDK1.8 的时候,`ConcurrentHashMap` 取消了 `Segment` 分段锁,采用 `Node + CAS + synchronized` 来保证并发安全。数据结构跟 `HashMap` 1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。 +By JDK 1.8, `ConcurrentHashMap` abandoned the `Segment` locks and adopted `Node + CAS + synchronized` to ensure concurrent safety. The data structure is similar to that of `HashMap` 1.8, using an array combined with linked lists/red-black trees. In Java 8, when the length of a linked list exceeds a certain threshold (8), it converts the linked list (with a lookup time complexity of O(N)) into a red-black tree (with a lookup time complexity of O(log(N))). -Java 8 中,锁粒度更细,`synchronized` 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。 +In Java 8, the locking granularity is finer; `synchronized` only locks the head node of the current linked list or red-black tree. Therefore, as long as hashes do not collide, there will be no concurrency issues, and it will not affect the read and write of other nodes, significantly enhancing efficiency. -![Java8 ConcurrentHashMap 存储结构](https://oss.javaguide.cn/github/javaguide/java/collection/java8_concurrenthashmap.png) +![Java8 ConcurrentHashMap Storage Structure](https://oss.javaguide.cn/github/javaguide/java/collection/java8_concurrenthashmap.png) -关于 `ConcurrentHashMap` 的详细介绍,请看我写的这篇文章:[`ConcurrentHashMap` 源码分析](./../collection/concurrent-hash-map-source-code.md)。 +For a detailed introduction to `ConcurrentHashMap`, please refer to my article: [`ConcurrentHashMap` Source Code Analysis](./../collection/concurrent-hash-map-source-code.md). ## CopyOnWriteArrayList -在 JDK1.5 之前,如果想要使用并发安全的 `List` 只能选择 `Vector`。而 `Vector` 是一种老旧的集合,已经被淘汰。`Vector` 对于增删改查等方法基本都加了 `synchronized`,这种方式虽然能够保证同步,但这相当于对整个 `Vector` 加上了一把大锁,使得每个方法执行的时候都要去获得锁,导致性能非常低下。 +Before JDK 1.5, the only option for a thread-safe `List` was `Vector`. However, `Vector` is an outdated collection and has been phased out. `Vector` essentially synchronized all methods like add, remove, etc. While this ensures synchronization, it effectively added a large lock to the entire `Vector`, requiring each method to acquire a lock to execute, which leads to significantly low performance. -JDK1.5 引入了 `Java.util.concurrent`(JUC)包,其中提供了很多线程安全且并发性能良好的容器,其中唯一的线程安全 `List` 实现就是 `CopyOnWriteArrayList` 。 +JDK 1.5 introduced the `Java.util.concurrent` (JUC) package, which provides many thread-safe containers with good concurrent performance. The only thread-safe `List` implementation is `CopyOnWriteArrayList`. -对于大部分业务场景来说,读取操作往往是远大于写入操作的。由于读取操作不会对原有数据进行修改,因此,对于每次读取都进行加锁其实是一种资源浪费。相比之下,我们应该允许多个线程同时访问 `List` 的内部数据,毕竟对于读取操作来说是安全的。 +In most business scenarios, read operations often far exceed write operations. Since read operations do not modify the original data, locking for each read is actually a waste of resources. Instead, we should allow multiple threads to access the internal data of the `List` simultaneously, as it is safe for read operations. -这种思路与 `ReentrantReadWriteLock` 读写锁的设计思想非常类似,即读读不互斥、读写互斥、写写互斥(只有读读不互斥)。`CopyOnWriteArrayList` 更进一步地实现了这一思想。为了将读操作性能发挥到极致,`CopyOnWriteArrayList` 中的读取操作是完全无需加锁的。更加厉害的是,写入操作也不会阻塞读取操作,只有写写才会互斥。这样一来,读操作的性能就可以大幅度提升。 +This concept is very similar to the design ideas of `ReentrantReadWriteLock`, where read-read operations do not interfere, read-write operations do, and write-write operations do (only read-read is non-blocking). `CopyOnWriteArrayList` takes this idea further. To maximize the performance of read operations, reading from `CopyOnWriteArrayList` does not require any locking. Even more impressively, write operations do not block read operations; only write operations will mutually exclude. This significantly enhances the performance of read operations. -`CopyOnWriteArrayList` 线程安全的核心在于其采用了 **写时复制(Copy-On-Write)** 的策略,从 `CopyOnWriteArrayList` 的名字就能看出了。 +The thread safety core of `CopyOnWriteArrayList` lies in its use of **Copy-On-Write** strategy, as indicated by its name. -当需要修改( `add`,`set`、`remove` 等操作) `CopyOnWriteArrayList` 的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。 +When modifying the contents of `CopyOnWriteArrayList` (operations like `add`, `set`, `remove`), the original array is not directly modified. Instead, it first creates a copy of the underlying array, modifies the copied array, and then assigns the modified array back. This ensures that write operations do not affect read operations. -关于 `CopyOnWriteArrayList` 的详细介绍,请看我写的这篇文章:[`CopyOnWriteArrayList` 源码分析](./../collection/copyonwritearraylist-source-code.md)。 +For a detailed introduction to `CopyOnWriteArrayList`, please refer to my article: [`CopyOnWriteArrayList` Source Code Analysis](./../collection/copyonwritearraylist-source-code.md). ## ConcurrentLinkedQueue -Java 提供的线程安全的 `Queue` 可以分为**阻塞队列**和**非阻塞队列**,其中阻塞队列的典型例子是 `BlockingQueue`,非阻塞队列的典型例子是 `ConcurrentLinkedQueue`,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 **阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。** +Java provides thread-safe `Queue`, which can be divided into **blocking queues** and **non-blocking queues**, with `BlockingQueue` being a typical example of a blocking queue and `ConcurrentLinkedQueue` a typical example of a non-blocking queue. In practical applications, one must choose between blocking or non-blocking queues based on actual requirements. **Blocking queues can be implemented with locks, while non-blocking queues can be implemented using CAS operations.** -从名字可以看出,`ConcurrentLinkedQueue`这个队列使用链表作为其数据结构.`ConcurrentLinkedQueue` 应该算是在高并发环境中性能最好的队列了。它之所有能有很好的性能,是因为其内部复杂的实现。 +As the name suggests, `ConcurrentLinkedQueue` uses a linked list as its data structure. `ConcurrentLinkedQueue` is arguably the best-performing queue in high-concurrency environments. Its excellent performance results from its complex internal implementation. -`ConcurrentLinkedQueue` 内部代码我们就不分析了,大家知道 `ConcurrentLinkedQueue` 主要使用 CAS 非阻塞算法来实现线程安全就好了。 +We will not analyze the internal code of `ConcurrentLinkedQueue`; it suffices to know that it mainly uses CAS non-blocking algorithms to achieve thread safety. -`ConcurrentLinkedQueue` 适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 `ConcurrentLinkedQueue` 来替代。 +`ConcurrentLinkedQueue` is suitable for scenarios requiring relatively high performance, where multiple threads simultaneously read from and write to the queue. If the cost of locking the queue is high, it is advisable to use the lock-free `ConcurrentLinkedQueue` as a replacement. ## BlockingQueue -### BlockingQueue 简介 +### Introduction to BlockingQueue -上面我们己经提到了 `ConcurrentLinkedQueue` 作为高性能的非阻塞队列。下面我们要讲到的是阻塞队列——`BlockingQueue`。阻塞队列(`BlockingQueue`)被广泛使用在“生产者-消费者”问题中,其原因是 `BlockingQueue` 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。 +As mentioned above, `ConcurrentLinkedQueue` serves as a high-performance non-blocking queue. Now, we will discuss the blocking queue—`BlockingQueue`. The blocking queue (`BlockingQueue`) is widely used in the “producer-consumer” problem because it provides methods for inserting and removing elements that can block. When the queue container is full, the producer thread will be blocked until the queue is not full; when the queue is empty, the consumer thread will be blocked until the queue is non-empty. -`BlockingQueue` 是一个接口,继承自 `Queue`,所以其实现类也可以作为 `Queue` 的实现来使用,而 `Queue` 又继承自 `Collection` 接口。下面是 `BlockingQueue` 的相关实现类: +`BlockingQueue` is an interface that extends `Queue`, so its implementation classes can also be used as implementations of `Queue`, and `Queue` extends the `Collection` interface. Here are some related implementation classes of `BlockingQueue`: -![BlockingQueue 的实现类](https://oss.javaguide.cn/github/javaguide/java/51622268.jpg) +![Implementation Classes of BlockingQueue](https://oss.javaguide.cn/github/javaguide/java/51622268.jpg) -下面主要介绍一下 3 个常见的 `BlockingQueue` 的实现类:`ArrayBlockingQueue`、`LinkedBlockingQueue`、`PriorityBlockingQueue` 。 +The following will primarily introduce three common implementations of `BlockingQueue`: `ArrayBlockingQueue`, `LinkedBlockingQueue`, and `PriorityBlockingQueue`. ### ArrayBlockingQueue -`ArrayBlockingQueue` 是 `BlockingQueue` 接口的有界队列实现类,底层采用数组来实现。 +`ArrayBlockingQueue` is a bounded queue implementation of the `BlockingQueue` interface, implemented using an array. ```java public class ArrayBlockingQueue @@ -79,9 +79,9 @@ extends AbstractQueue implements BlockingQueue, Serializable{} ``` -`ArrayBlockingQueue` 一旦创建,容量不能改变。其并发控制采用可重入锁 `ReentrantLock` ,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。 +Once `ArrayBlockingQueue` is created, its capacity cannot change. Concurrency control uses a reentrant lock (`ReentrantLock`), and both insert and read operations require acquiring the lock. When the queue is full, any attempt to add elements will block; similarly, attempting to remove an element from an empty queue will also block. -`ArrayBlockingQueue` 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 `ArrayBlockingQueue`。而非公平性则是指访问 `ArrayBlockingQueue` 的顺序不是遵守严格的时间顺序,有可能存在,当 `ArrayBlockingQueue` 可以被访问时,长时间阻塞的线程依然无法访问到 `ArrayBlockingQueue`。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 `ArrayBlockingQueue`,可采用如下代码: +`ArrayBlockingQueue` does not guarantee the fairness of thread access to the queue by default. Fairness means that threads can access `ArrayBlockingQueue` in strict order of waiting time—meaning the first to wait can access `ArrayBlockingQueue` first. Non-fairness means that the order of access may not adhere to strict time order, potentially causing long-blocked threads to remain unable to access `ArrayBlockingQueue` when it is available. Ensuring fairness often reduces throughput. If fairness is required for `ArrayBlockingQueue`, it can be obtained with the following code: ```java private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(10,true); @@ -89,13 +89,13 @@ private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueu ### LinkedBlockingQueue -`LinkedBlockingQueue` 底层基于**单向链表**实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 `ArrayBlockingQueue` 相比起来具有更高的吞吐量,为了防止 `LinkedBlockingQueue` 容量迅速增,损耗大量内存。通常在创建 `LinkedBlockingQueue` 对象时,会指定其大小,如果未指定,容量等于 `Integer.MAX_VALUE` 。 +`LinkedBlockingQueue` is a blocking queue implemented based on a **single linked list**. It can be used as either an unbounded or a bounded queue while maintaining FIFO characteristics. Compared to `ArrayBlockingQueue`, it has higher throughput. To prevent `LinkedBlockingQueue` from rapidly increasing its capacity and consuming large amounts of memory, it typically specifies its size at the time of creation. If not specified, the capacity defaults to `Integer.MAX_VALUE`. -**相关构造方法:** +**Related constructors:** ```java /** - *某种意义上的无界队列 + * Virtually an unbounded queue * Creates a {@code LinkedBlockingQueue} with a capacity of * {@link Integer#MAX_VALUE}. */ @@ -104,7 +104,7 @@ private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueu } /** - *有界队列 + * Bounded queue * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity. * * @param capacity the capacity of this queue @@ -120,41 +120,41 @@ private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueu ### PriorityBlockingQueue -`PriorityBlockingQueue` 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 `compareTo()` 方法来指定元素排序规则,或者初始化时通过构造器参数 `Comparator` 来指定排序规则。 +`PriorityBlockingQueue` is an unbounded blocking queue that supports priorities. By default, the elements are sorted in their natural order; custom rules for sorting can be specified either by implementing the `compareTo()` method in a custom class or by providing a `Comparator` as a constructor argument. -`PriorityBlockingQueue` 并发控制采用的是可重入锁 `ReentrantLock`,队列为无界队列(`ArrayBlockingQueue` 是有界队列,`LinkedBlockingQueue` 也可以通过在构造函数中传入 `capacity` 指定队列最大的容量,但是 `PriorityBlockingQueue` 只能指定初始的队列大小,后面插入元素的时候,**如果空间不够的话会自动扩容**)。 +`PriorityBlockingQueue` uses a reentrant lock (`ReentrantLock`) for concurrency control and is an unbounded queue (whereas `ArrayBlockingQueue` is a bounded queue, `LinkedBlockingQueue` can specify its maximum capacity using a `capacity` parameter in the constructor, but `PriorityBlockingQueue` can only set its initial queue size and will **automatically resize** when space is insufficient). -简单地说,它就是 `PriorityQueue` 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 `ClassCastException` 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。 +In simple terms, it is the thread-safe version of `PriorityQueue`. It does not allow inserting null values, and the objects inserted into the queue must be comparable; otherwise, a `ClassCastException` will be thrown. Its insertion operation (`put` method) will not block, as it is an unbounded queue (the `take` method will block when the queue is empty). -**推荐文章:** [《解读 Java 并发队列 BlockingQueue》](https://javadoop.com/post/java-concurrent-queue) +**Recommended article:** [“Understanding Java Concurrent Queue BlockingQueue”](https://javadoop.com/post/java-concurrent-queue) ## ConcurrentSkipListMap -> 下面这部分内容参考了极客时间专栏[《数据结构与算法之美》](https://time.geekbang.org/column/intro/126?code=zl3GYeAsRI4rEJIBNu5B/km7LSZsPDlGWQEpAYw5Vu0=&utm_term=SPoster "《数据结构与算法之美》")以及《实战 Java 高并发程序设计》。 +> The following content references Geek Time column [“The Beauty of Data Structures and Algorithms”](https://time.geekbang.org/column/intro/126?code=zl3GYeAsRI4rEJIBNu5B/km7LSZsPDlGWQEpAYw5Vu0=&utm_term=SPoster "The Beauty of Data Structures and Algorithms") and “Practical Java Concurrency Programming”. -为了引出 `ConcurrentSkipListMap`,先带着大家简单理解一下跳表。 +To introduce `ConcurrentSkipListMap`, let's start with a brief understanding of skip lists. -对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 **O(logn)** 所以在并发数据结构中,JDK 使用跳表来实现一个 Map。 +For a singly linked list, even if it is ordered, finding a certain data point can only be done by traversing the list from beginning to end, which naturally is inefficient. Skip lists are different. They are a data structure designed for fast lookup, somewhat like balanced trees. Both allow for quick lookup of elements. However, a significant difference is that inserting and deleting in a balanced tree often requires a global adjustment of the tree, while operations on skip lists only require local modifications to the data structure. This provides the advantage that, in high concurrency, global locks are needed to ensure the thread safety of the entire balanced tree. For skip lists, only partial locks are necessary. Thus, in high concurrency environments, you can achieve better performance. Furthermore, with respect to lookup performance, the time complexity of skip lists is also **O(logn)**, which is why the JDK uses skip lists to implement a Map. -跳表的本质是同时维护了多个链表,并且链表是分层的, +The essence of skip lists is that they maintain multiple linked lists simultaneously, and these lists are hierarchical. -![2级索引跳表](https://oss.javaguide.cn/github/javaguide/java/93666217.jpg) +![2-level index skip list](https://oss.javaguide.cn/github/javaguide/java/93666217.jpg) -最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。 +The lowest layer of the list maintains all elements within the skip list; each higher layer represents a subset of the layer below it. -跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素 18。 +All elements in the linked lists within a skip list are sorted. When searching, you can start from the top-level list. If the searched element is greater than the current list's value, you will move to the next lower level list to continue searching. This means that during the search process, it jumps through levels. For example, in searching for the element 18: -![在跳表中查找元素18](https://oss.javaguide.cn/github/javaguide/java/32005738.jpg) +![Searching for element 18 in a skip list](https://oss.javaguide.cn/github/javaguide/java/32005738.jpg) -查找 18 的时候原来需要遍历 18 次,现在只需要 7 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。 +When searching for 18, it originally required traversing 18 times; now it only takes 7 times. When dealing with longer linked lists, the efficiency of constructing an index for lookups becomes significantly apparent. -从上面很容易看出,**跳表是一种利用空间换时间的算法。** +It is easy to see that **the skip list is an algorithm that trades space for time.** -使用跳表实现 `Map` 和使用哈希算法实现 `Map` 的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择。JDK 中实现这一数据结构的类是 `ConcurrentSkipListMap`。 +Using a skip list to implement `Map` differs from using hashing in that hashing does not preserve the order of elements, while all elements in a skip list are sorted. Therefore, traversing a skip list will yield an ordered result. If your application requires order, then a skip list is your best choice. The class that implements this data structure in the JDK is `ConcurrentSkipListMap`. -## 参考 +## References -- 《实战 Java 高并发程序设计》 +- “Practical Java Concurrency Programming” - - diff --git a/docs/java/concurrent/java-concurrent-questions-01.md b/docs/java/concurrent/java-concurrent-questions-01.md index 50b3922baec..465615831ea 100644 --- a/docs/java/concurrent/java-concurrent-questions-01.md +++ b/docs/java/concurrent/java-concurrent-questions-01.md @@ -1,47 +1,47 @@ --- -title: Java并发常见面试题总结(上) +title: Summary of Common Java Concurrency Interview Questions (Part 1) category: Java tag: - - Java并发 + - Java Concurrency head: - - - meta - - name: keywords - content: 线程和进程,并发和并行,多线程,死锁,线程的生命周期 - - - meta - - name: description - content: Java并发常见知识点和面试题总结(含详细解答),希望对你有帮助! + - - meta + - name: keywords + content: threads and processes, concurrency and parallelism, multithreading, deadlock, thread lifecycle + - - meta + - name: description + content: A summary of common knowledge points and interview questions in Java concurrency (including detailed answers), hope it helps you! --- -## 线程 +## Threads -### ⭐️什么是线程和进程? +### ⭐️What are threads and processes? -#### 何为进程? +#### What is a process? -进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 +A process is the execution of a program and is the basic unit for the system to run programs; hence, a process is dynamic. When the system runs a program, it is a process from creation, execution, to termination. -在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。 +In Java, when we start the main function, we are actually starting a JVM process, and the thread where the main function resides is a thread within this process, also known as the main thread. -如下图所示,在 Windows 中通过查看任务管理器的方式,我们就可以清楚看到 Windows 当前运行的进程(`.exe` 文件的运行)。 +As shown in the image below, we can clearly see the current processes running on Windows by checking the task manager (the execution of `.exe` files). -![进程示例图片-Windows](https://oss.javaguide.cn/github/javaguide/java/%E8%BF%9B%E7%A8%8B%E7%A4%BA%E4%BE%8B%E5%9B%BE%E7%89%87-Windows.png) +![Process Example Image - Windows](https://oss.javaguide.cn/github/javaguide/java/%E8%BF%9B%E7%A8%8B%E7%A4%BA%E4%BE%8B%E5%9B%BE%E7%89%87-Windows.png) -#### 何为线程? +#### What is a thread? -线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 +A thread is similar to a process but is a smaller unit of execution than a process. A process can generate multiple threads during its execution. Unlike processes, multiple threads of the same type share the **heap** and **method area** resources of the process, but each thread has its own **program counter**, **Java virtual machine stack**, and **native method stack**. Thus, creating a thread or switching between threads imposes a much lower burden on the system than processes, which is why threads are also referred to as lightweight processes. -Java 程序天生就是多线程程序,我们可以通过 JMX 来看看一个普通的 Java 程序有哪些线程,代码如下。 +Java programs are inherently multithreaded; we can use JMX to see what threads a typical Java program has, as illustrated in the code below. ```java public class MultiThread { public static void main(String[] args) { - // 获取 Java 线程管理 MXBean - ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); - // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息 + // Get the Java thread management MXBean + ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); + // No need to get synchronized monitor and synchronizer information, just get thread and thread stack information ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); - // 遍历线程信息,仅打印线程 ID 和线程名称信息 + // Traverse thread information and only print thread ID and thread name information for (ThreadInfo threadInfo : threadInfos) { System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); } @@ -49,267 +49,267 @@ public class MultiThread { } ``` -上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可): +The output of the above program is as follows (the content may vary; do not worry too much about the function of each thread below; just know that the main thread executes the main method): ```plain -[5] Attach Listener //添加事件 -[4] Signal Dispatcher // 分发处理给 JVM 信号的线程 -[3] Finalizer //调用对象 finalize 方法的线程 -[2] Reference Handler //清除 reference 线程 -[1] main //main 线程,程序入口 +[5] Attach Listener // Add Listener +[4] Signal Dispatcher // Thread that dispatches signals to the JVM +[3] Finalizer // Thread that calls the finalize method of an object +[2] Reference Handler // Thread that cleans up references +[1] main // Main thread, program entry point ``` -从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。 +From the output, we can see that: **The execution of a Java program involves the simultaneous operation of the main thread and several other threads**. -### Java 线程和操作系统的线程有啥区别? +### What are the differences between Java threads and operating system threads? -JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。由于绿色线程和原生线程比起来在使用时有一些限制(比如绿色线程不能直接使用操作系统提供的功能如异步 I/O、只能在一个内核线程上运行无法利用多核),在 JDK 1.2 及以后,Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。 +Before JDK 1.2, Java threads were implemented using green threads, which are a type of user-level thread. This means that the JVM simulated multithreading without relying on the operating system. Due to some limitations of green threads compared to native threads (e.g., green threads cannot directly use operating system features like asynchronous I/O, and can only run on one kernel thread without utilizing multiple cores), from JDK 1.2 onwards, Java threads were changed to be implemented based on native threads, meaning that the JVM directly uses the operating system's native kernel-level threads to implement Java threads, with the operating system kernel managing thread scheduling. -我们上面提到了用户线程和内核线程,考虑到很多读者不太了解二者的区别,这里简单介绍一下: +We mentioned user threads and kernel threads earlier, and considering that many readers might not be familiar with the differences between them, here is a brief introduction: -- 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。 -- 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。 +- User Thread: A thread managed and scheduled by user-space programs, running in user space (specifically meant for application use). +- Kernel Thread: A thread managed and scheduled by the operating system kernel, running in kernel space (which only kernel programs can access). -顺便简单总结一下用户线程和内核线程的区别和特点:用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。 +To summarize briefly, here are the differences and characteristics of user threads and kernel threads: User threads have low creation and switching costs but cannot utilize multiple cores. Kernel-level threads have high creation and switching costs but can utilize multiple cores. -一句话概括 Java 线程和操作系统线程的关系:**现在的 Java 线程的本质其实就是操作系统的线程**。 +In short, the relationship between Java threads and operating system threads is: **The essence of current Java threads is essentially operating system threads**. -线程模型是用户线程和内核线程之间的关联方式,常见的线程模型有这三种: +Thread models are the relationships between user threads and kernel threads. The three common thread models are: -1. 一对一(一个用户线程对应一个内核线程) -2. 多对一(多个用户线程映射到一个内核线程) -3. 多对多(多个用户线程映射到多个内核线程) +1. One-to-One (one user thread corresponds to one kernel thread) +1. Many-to-One (multiple user threads map to one kernel thread) +1. Many-to-Many (multiple user threads map to multiple kernel threads) -![常见的三种线程模型](https://oss.javaguide.cn/github/javaguide/java/concurrent/three-types-of-thread-models.png) +![Common Three Thread Models](https://oss.javaguide.cn/github/javaguide/java/concurrent/three-types-of-thread-models.png) -在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个 Java 线程对应一个系统内核线程。Solaris 系统是一个特例(Solaris 系统本身就支持多对多的线程模型),HotSpot VM 在 Solaris 上支持多对多和一对一。具体可以参考 R 大的回答: [JVM 中的线程模型是用户级的么?](https://www.zhihu.com/question/23096638/answer/29617153)。 +In mainstream operating systems like Windows and Linux, Java threads employ a one-to-one thread model, meaning one Java thread corresponds to one system kernel thread. The Solaris system is an exception (as it natively supports a many-to-many threading model), with HotSpot VM supporting both many-to-many and one-to-one threading on Solaris. For specific details, refer to R's answer: [Is the thread model in the JVM user-level?](https://www.zhihu.com/question/23096638/answer/29617153). -### ⭐️请简要描述线程与进程的关系,区别及优缺点? +### ⭐️Please briefly describe the relationship, differences, and advantages/disadvantages of threads and processes. -下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。 +The image below shows the Java memory area, and from this image, we can discuss the relationship between threads and processes from the perspective of the JVM. -![Java 运行时数据区域(JDK1.8 之后)](https://oss.javaguide.cn/github/javaguide/java/jvm/java-runtime-data-areas-jdk1.8.png) +![Java Runtime Data Areas (Post JDK 1.8)](https://oss.javaguide.cn/github/javaguide/java/jvm/java-runtime-data-areas-jdk1.8.png) -从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 +From the image, we can see that a process can have multiple threads, and those threads share the **heap** and **method area (meta space after JDK 1.8)** resources of the process. However, each thread has its own **program counter**, **Java virtual machine stack**, and **native method stack**. -**总结:** **线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。** +**Summary:** **Threads are smaller units of execution created from a process. The biggest difference between threads and processes is that processes are generally independent, whereas threads may not be, because threads within the same process can significantly affect each other. Thread execution has a low overhead but is not conducive to resource management and protection; the opposite is true for processes.** -下面是该知识点的扩展内容! +Now let's explore an extended content of this knowledge! -下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢? +Consider this question: why are the **program counter**, **Java virtual machine stack**, and **native method stack** thread-private? Why are the heap and method area thread-shared? -#### 程序计数器为什么是私有的? +#### Why is the program counter private? -程序计数器主要有下面两个作用: +The program counter mainly has two functions: -1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 -2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 +1. The bytecode interpreter uses the program counter to read instructions sequentially, thereby controlling the flow of code: sequential execution, selection, loop, and exception handling. +1. In a multithreaded scenario, the program counter is used to record the position of the currently executing thread, so when the thread is switched back, it knows where the thread last executed. -需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。 +It's important to note that if a native method is executed, the program counter records an undefined address; only when executing Java code does the program counter record the address of the next instruction. -所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。 +Therefore, the program counter is private primarily to **restore to the correct execution position after a thread switch**. -#### 虚拟机栈和本地方法栈为什么是私有的? +#### Why are the Java virtual machine stack and the native method stack private? -- **虚拟机栈:** 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 -- **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是:**虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 +- **Java Virtual Machine Stack:** Each Java method creates a frame before execution to store local variable tables, operand stacks, references from constant pools, etc. The process from method call to execution completion corresponds to the entry and exit of a stack frame in the Java virtual machine stack. +- **Native Method Stack:** Functions similarly to the virtual machine stack, but the difference is: **The virtual machine stack serves Java method execution (i.e., bytecode), while the native method stack serves native methods used by the virtual machine.** In the HotSpot virtual machine, it merges with the Java virtual machine stack. -所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。 +Thus, to **ensure that local variables within a thread are not accessed by other threads**, both the virtual machine stack and native method stack are thread-private. -#### 一句话简单了解堆和方法区 +#### A simple phrase to understand the heap and method area -堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 +The heap and method area are resources shared by all threads, with the heap being the largest contiguous memory block in the process, mainly used for storing newly created objects (almost all objects are allocated memory here), while the method area is primarily used to store information about loaded classes, constants, static variables, and just-in-time compiled code. -### 如何创建线程? +### How to create threads? -一般来说,创建线程有很多种方式,例如继承`Thread`类、实现`Runnable`接口、实现`Callable`接口、使用线程池、使用`CompletableFuture`类等等。 +Generally speaking, there are many ways to create threads, such as inheriting from the `Thread` class, implementing the `Runnable` interface, implementing the `Callable` interface, using thread pools, and using the `CompletableFuture` class, among others. -不过,这些方式其实并没有真正创建出线程。准确点来说,这些都属于是在 Java 代码中使用多线程的方法。 +However, none of these methods truly create a thread. More accurately, they are all ways of using multithreading in Java. -严格来说,Java 就只有一种方式可以创建线程,那就是通过`new Thread().start()`创建。不管是哪种方式,最终还是依赖于`new Thread().start()`。 +Strictly speaking, there is only one way in Java to create a thread, which is to call `new Thread().start()`. Regardless of the method, it ultimately depends on `new Thread().start()`. -关于这个问题的详细分析可以查看这篇文章:[大家都说 Java 有三种创建线程的方式!并发编程中的惊天骗局!](https://mp.weixin.qq.com/s/NspUsyhEmKnJ-4OprRFp9g)。 +For a detailed analysis of this issue, you can refer to this article: [Everyone says that Java has three ways to create threads! The shocking deception in concurrent programming!](https://mp.weixin.qq.com/s/NspUsyhEmKnJ-4OprRFp9g). -### ⭐️说说线程的生命周期和状态? +### ⭐️Describe the lifecycle and state of threads? -Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态: +Java threads can only be in one of the following six different states at a designated moment in their execution lifecycle: -- NEW: 初始状态,线程被创建出来但没有被调用 `start()` 。 -- RUNNABLE: 运行状态,线程被调用了 `start()`等待运行的状态。 -- BLOCKED:阻塞状态,需要等待锁释放。 -- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。 -- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。 -- TERMINATED:终止状态,表示该线程已经运行完毕。 +- NEW: Initial state, the thread has been created but has not been called `start()`. +- RUNNABLE: Running state, the thread has been called `start()` and is waiting to run. +- BLOCKED: Blocked state, needs to wait for the lock to be released. +- WAITING: Waiting state, indicating that this thread needs to wait for another thread to perform a specific action (notification or interruption). +- TIME_WAITING: Timeout waiting state, can return after a specified time instead of waiting indefinitely like WAITING. +- TERMINATED: Termination state, indicating that the thread has finished running. -线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。 +Thread states are not fixed at a single state during their lifecycle but switch between different states as the code executes. -Java 线程状态变迁图(图源:[挑错 |《Java 并发编程的艺术》中关于线程状态的三处错误](https://mp.weixin.qq.com/s/UOrXql_LhOD8dhTq_EPI0w)): +Java thread state transition diagram (Image source: [Corrections | Three Errors about Thread States in "The Art of Concurrent Programming"](https://mp.weixin.qq.com/s/UOrXql_LhOD8dhTq_EPI0w)): -![Java 线程状态变迁图](https://oss.javaguide.cn/github/javaguide/java/concurrent/640.png) +![Java Thread State Transition Diagram](https://oss.javaguide.cn/github/javaguide/java/concurrent/640.png) -由上图可以看出:线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。 +From the image, we see that after a thread is created, it enters the **NEW** state. After calling the `start()` method, it begins running and is in the **READY** state. Once a runnable thread gets a CPU time slice, it enters the **RUNNING** state. -> 在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinJava.com/ "HowToDoInJava"):[Java Thread Life Cycle and Thread States](https://howtodoinJava.com/Java/multi-threading/Java-thread-life-cycle-and-thread-states/ "Java Thread Life Cycle and Thread States")),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。 +> At the operating system level, threads have READY and RUNNING states; at the JVM level, only the RUNNABLE state can be seen (Image source: [HowToDoInJava](https://howtodoinJava.com/ "HowToDoInJava"):[Java Thread Life Cycle and Thread States](https://howtodoinJava.com/Java/multi-threading/Java-thread-life-cycle-and-thread-states/ "Java Thread Life Cycle and Thread States")), so the Java system generally refers to these two states collectively as **RUNNABLE** state. > -> **为什么 JVM 没有区分这两种状态呢?** (摘自:[Java 线程运行怎么有第六种状态? - Dawell 的回答](https://www.zhihu.com/question/56494969/answer/154053599) ) 现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin 式)。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。 +> **Why doesn’t the JVM distinguish these two states?** (Excerpt from: [Why does Java Threading have a sixth state? - Dawell's Answer](https://www.zhihu.com/question/56494969/answer/154053599)) Current time-sharing multi-tasking OS architectures typically employ time slice preemptive round-robin scheduling, allowing a thread to run on the CPU for a maximum of a short duration (e.g., 10-20ms, meaning about 0.01 seconds). After this time slice, the thread must return to the scheduling queue's end for re-scheduling (back to READY state). Because context switching occurs so rapidly, distinguishing these two states becomes meaningless. ![RUNNABLE-VS-RUNNING](https://oss.javaguide.cn/github/javaguide/java/RUNNABLE-VS-RUNNING.png) -- 当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)** 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。 -- **TIMED_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。 -- 当线程进入 `synchronized` 方法/块或者调用 `wait` 后(被 `notify`)重新进入 `synchronized` 方法/块,但是锁被其它线程占有,这个时候线程就会进入 **BLOCKED(阻塞)** 状态。 -- 线程在执行完了 `run()`方法之后将会进入到 **TERMINATED(终止)** 状态。 +- When a thread executes the `wait()` method, it enters the **WAITING** state. A thread in the waiting state requires notification from another thread to return to the RUNNABLE state. +- The **TIMED_WAITING** state adds a timeout limit to the waiting state; for example, calls to `sleep(long millis)` or `wait(long millis)` can put the thread into the TIMED_WAITING state. After the timeout, the thread will return to RUNNABLE state. +- When a thread enters `synchronized` methods/blocks or calls `wait` and then re-enters `synchronized` methods/blocks but the lock is occupied by other threads, it will enter the **BLOCKED** state. +- After executing the `run()` method, the thread will enter the **TERMINATED** state. -相关阅读:[线程的几种状态你真的了解么?](https://mp.weixin.qq.com/s/R5MrTsWvk9McFSQ7bS0W2w) 。 +Related reading: [Do you really understand the various states of a thread?](https://mp.weixin.qq.com/s/R5MrTsWvk9McFSQ7bS0W2w). -### 什么是线程上下文切换? +### What is thread context switching? -线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。 +During execution, threads will have their own execution conditions and states (also known as contexts), such as the program counter and stack information mentioned earlier. The thread will exit the CPU-occupied state under the following circumstances: -- 主动让出 CPU,比如调用了 `sleep()`, `wait()` 等。 -- 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。 -- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。 -- 被终止或结束运行 +- Proactively relinquishing CPU, for example, by calling `sleep()`, `wait()`, etc. +- Time slice expiration, as the operating system must prevent a thread or process from monopolizing the CPU for too long, starving other threads or processes. +- Triggering a blocking type of system interrupt, such as requesting IO, causing the thread to be blocked. +- Termination or exit from execution. -这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 **上下文切换**。 +The first three cases lead to a thread switch; context switching means saving the current thread's context so that it can be restored the next time the thread occupies the CPU while loading the context of the next thread to occupy the CPU. This is referred to as **context switching**. -上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。 +Context switching is a basic function of modern operating systems, and since it requires saving and recovering information each time, it occupies CPU, memory, and other system resources, leading to some efficiency loss. Frequent switches can result in overall low efficiency. -### Thread#sleep() 方法和 Object#wait() 方法对比 +### Comparison of Thread#sleep() method and Object#wait() method -**共同点**:两者都可以暂停线程的执行。 +**Similarities:** Both can pause thread execution. -**区别**: +**Differences:** -- **`sleep()` 方法没有释放锁,而 `wait()` 方法释放了锁** 。 -- `wait()` 通常被用于线程间交互/通信,`sleep()`通常被用于暂停执行。 -- `wait()` 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 `notify()`或者 `notifyAll()` 方法。`sleep()`方法执行完成后,线程会自动苏醒,或者也可以使用 `wait(long timeout)` 超时后线程会自动苏醒。 -- `sleep()` 是 `Thread` 类的静态本地方法,`wait()` 则是 `Object` 类的本地方法。为什么这样设计呢?下一个问题就会聊到。 +- **The `sleep()` method doesn't release locks, whereas the `wait()` method does.** +- `wait()` is generally used for inter-thread communication, while `sleep()` is typically used to pause execution. +- After calling the `wait()` method, the thread doesn't wake up automatically; another thread must call `notify()` or `notifyAll()` on the same object. The `sleep()` method, after its execution ends, will make the thread wake up automatically or it can also timeout using `wait(long timeout)`. +- `sleep()` is a static native method of the `Thread` class, while `wait()` is a native method of the `Object` class. Why is this designed that way? The next question will address this. -### 为什么 wait() 方法不定义在 Thread 中? +### Why isn’t the wait() method defined in Thread? -`wait()` 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(`Object`)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(`Object`)而非当前的线程(`Thread`)。 +`wait()` allows a thread that has acquired an object lock to wait automatically, releasing the lock it currently holds. Each object (`Object`) possesses an object lock, meaning to release an object lock and enter the WAITING state, manipulation on the corresponding object (`Object`) rather than the current thread (`Thread`) is necessary. -类似的问题:**为什么 `sleep()` 方法定义在 `Thread` 中?** +Similar question: **Why is the `sleep()` method defined in `Thread`?** -因为 `sleep()` 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。 +Because `sleep()` allows the current thread to pause execution without involving the object class and does not require acquiring an object lock. -### 可以直接调用 Thread 类的 run 方法吗? +### Can the run method of the Thread class be called directly? -这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! +This is another classic Java multithreading interview question that is often posed in interviews. It seems simple, but many people struggle to answer! -new 一个 `Thread`,线程进入了新建状态。调用 `start()`方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 `start()` 会执行线程的相应准备工作,然后自动执行 `run()` 方法的内容,这是真正的多线程工作。 但是,直接执行 `run()` 方法,会把 `run()` 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 +When you instantiate a `Thread`, the thread enters a new state. Calling `start()` will initiate a thread and place it into the ready state, where it can begin running once time is allocated. `start()` executes the necessary preparations and then automatically executes the content of the `run()` method; this constitutes real multithreaded work. However, calling `run()` directly treats it as a normal method executing in the main thread, not running in a separate thread, meaning this does not constitute multithreading work. -**总结:调用 `start()` 方法方可启动线程并使线程进入就绪状态,直接执行 `run()` 方法的话不会以多线程的方式执行。** +**Summary: It is only by calling the `start()` method that a thread is initiated and enters the ready state; directly executing `run()` will not execute it in a multithreaded manner.** -## 多线程 +## Multithreading -### 并发与并行的区别 +### The difference between concurrency and parallelism -- **并发**:两个及两个以上的作业在同一 **时间段** 内执行。 -- **并行**:两个及两个以上的作业在同一 **时刻** 执行。 +- **Concurrency:** Two or more jobs are executed within the same **time period**. +- **Parallelism:** Two or more jobs are executed at the same **instant**. -最关键的点是:是否是 **同时** 执行。 +The key point is: whether the execution is **simultaneous**. -### 同步和异步的区别 +### The difference between synchronous and asynchronous -- **同步**:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。 -- **异步**:调用在发出之后,不用等待返回结果,该调用直接返回。 +- **Synchronous:** After a call is made, it cannot return until the result is obtained, waiting continuously. +- **Asynchronous:** After the call is made, it does not wait for the result to return, directly returning the call. -### ⭐️为什么要使用多线程? +### ⭐️Why use multithreading? -先从总体上来说: +Describing it overall: -- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 -- **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。 +- **From a computer's low-level perspective:** Threads can be compared to lightweight processes, being the smallest unit of program execution; the cost for switching and scheduling between threads is significantly lower than that for processes. Additionally, in the era of multi-core CPUs, multiple threads can run simultaneously, which reduces the overhead of thread context switching. +- **From the perspective of contemporary internet development trends:** Current systems often require millions or even tens of millions of concurrent volumes, and multithreaded concurrent programming is fundamental to developing high-concurrency systems, leveraging multithreading mechanisms can significantly improve the overall concurrency capability and performance of the system. -再深入到计算机底层来探讨: +Digging deeper into the computer's low-level architecture: -- **单核时代**:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。 -- **多核时代**: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 核心上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。 +- **Single-core era:** In the single-core era, multithreading primarily improves the efficiency of CPU and IO system usage within a single process. Suppose only one Java process runs; if there is only one thread in the process requesting IO, this thread will be blocked by IO, thereby blocking the entire process. CPU and IO resources run one at a time, leading to an overall system efficiency of around 50%. With multithreading, if one thread blocks due to IO, other threads can continue to utilize the CPU, enhancing overall efficiency. +- **Multi-core era:** In the multi-core era, multithreading is mainly used to leverage the capabilities of multi-core CPUs. For instance, if we need to compute a complex task with a single thread, it will only utilize one CPU core, regardless of how many cores the system has. Creating multiple threads allows these threads to be mapped to multiple CPU cores effectively, resulting in a significant increase in task execution efficiency provided there is no resource competition among threads, roughly equating to (execution time in single core/number of CPU cores). -### ⭐️单核 CPU 支持 Java 多线程吗? +### ⭐️Does a single-core CPU support Java multithreading? -单核 CPU 是支持 Java 多线程的。操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。 +A single-core CPU does support Java multithreading. The operating system allocates CPU time to different threads via time slice rotation. Although a single-core CPU can only execute one task at a time, rapid switching between multiple threads gives the impression to the user that several tasks are progressing simultaneously. -这里顺带提一下 Java 使用的线程调度方式。 +Here’s a brief mention of the thread scheduling methods used in Java. -操作系统主要通过两种线程调度方式来管理多线程的执行: +Operating systems primarily manage multithreaded execution through two types of thread scheduling: -- **抢占式调度(Preemptive Scheduling)**:操作系统决定何时暂停当前正在运行的线程,并切换到另一个线程执行。这种切换通常是由系统时钟中断(时间片轮转)或其他高优先级事件(如 I/O 操作完成)触发的。这种方式存在上下文切换开销,但公平性和 CPU 资源利用率较好,不易阻塞。 -- **协同式调度(Cooperative Scheduling)**:线程执行完毕后,主动通知系统切换到另一个线程。这种方式可以减少上下文切换带来的性能开销,但公平性较差,容易阻塞。 +- **Preemptive Scheduling:** The operating system decides when to pause the currently executing thread and switch to another thread. This switch is typically triggered by system clock interrupts (time-slice rotation) or other high-priority events (like the completion of I/O operations). This method incurs context-switching costs but offers better fairness and CPU resource utilization, avoiding blockage. +- **Cooperative Scheduling:** The thread actively notifies the system to switch to another thread after execution completion. This method can reduce context-switching performance overhead but has poorer fairness and is prone to blockage. -Java 使用的线程调度是抢占式的。也就是说,JVM 本身不负责线程的调度,而是将线程的调度委托给操作系统。操作系统通常会基于线程优先级和时间片来调度线程的执行,高优先级的线程通常获得 CPU 时间片的机会更多。 +Java employs preemptive scheduling. That is, the JVM doesn't manage thread scheduling itself, but delegates it to the operating system. The operating system typically schedules thread execution based on thread priority and time slice, with high-priority threads usually getting a greater opportunity for CPU time. -### ⭐️单核 CPU 上运行多个线程效率一定会高吗? +### ⭐️Will running multiple threads on a single-core CPU always be more efficient? -单核 CPU 同时运行多个线程的效率是否会高,取决于线程的类型和任务的性质。一般来说,有两种类型的线程: +The efficiency of simultaneously running multiple threads on a single-core CPU depends on the type of threads and the nature of the tasks. Generally, there are two types of threads: -1. **CPU 密集型**:CPU 密集型的线程主要进行计算和逻辑处理,需要占用大量的 CPU 资源。 -2. **IO 密集型**:IO 密集型的线程主要进行输入输出操作,如读写文件、网络通信等,需要等待 IO 设备的响应,而不占用太多的 CPU 资源。 +1. **CPU-intensive:** These threads primarily engage in computation and logical processing, requiring a substantial amount of CPU resources. +1. **I/O-intensive:** These threads primarily perform input/output operations, such as file reading/writing and network communication, requiring waiting for the I/O device's response without consuming much CPU resources. -在单核 CPU 上,同一时刻只能有一个线程在运行,其他线程需要等待 CPU 的时间片分配。如果线程是 CPU 密集型的,那么多个线程同时运行会导致频繁的线程切换,增加了系统的开销,降低了效率。如果线程是 IO 密集型的,那么多个线程同时运行可以利用 CPU 在等待 IO 时的空闲时间,提高了效率。 +On a single-core CPU, only one thread can run at any given moment, and other threads must wait for the CPU time slice allocation. If the thread is CPU-intensive, running multiple threads will lead to frequent context switching, increasing the system overhead and reducing efficiency. However, if the thread is I/O-intensive, running multiple threads can utilize CPU during I/O waits, thereby improving efficiency. -因此,对于单核 CPU 来说,如果任务是 CPU 密集型的,那么开很多线程会影响效率;如果任务是 IO 密集型的,那么开很多线程会提高效率。当然,这里的“很多”也要适度,不能超过系统能够承受的上限。 +Thus, for single-core CPUs, if tasks are CPU-intensive, then creating many threads may hinder efficiency; conversely, if tasks are I/O-intensive, then creating more threads may enhance efficiency. Of course, "many" should be appropriate and not exceed the system's tolerable limit. -### 使用多线程可能带来什么问题? +### What issues might arise from using multithreading? -并发编程的目的就是为了能提高程序的执行效率进而提高程序的运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。 +The goal of concurrent programming is to enhance program execution efficiency and hence speed up the program, but concurrency may not always lead to improved speed. There are many potential issues arising from concurrent programming, such as: memory leaks, deadlocks, thread unsafeness, etc. -### 如何理解线程安全和不安全? +### How to understand thread safety and unsafety? -线程安全和不安全是在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。 +Thread safety and unsafety refer to whether accessing the same data in a multithreaded environment can ensure its correctness and consistency. -- 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。 -- 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。 +- Thread safety means that in a multithreaded environment, no matter how many threads access the same data simultaneously, the correctness and consistency of this data can still be maintained. +- Thread unsafety indicates that in a multithreaded environment, simultaneous access by multiple threads may lead to data chaos, errors, or loss. -## ⭐️死锁 +## ⭐️Deadlock -### 什么是线程死锁? +### What is thread deadlock? -线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 +Thread deadlock describes a situation where multiple threads are simultaneously blocked, each waiting for a resource to be freed. Since threads remain indefinitely blocked, the program cannot terminate normally. -如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 +As shown in the image below, thread A holds resource 2, while thread B holds resource 1; they both simultaneously seek to acquire each other's resources, leading to mutual waiting and the creation of a deadlock state. -![线程死锁示意图 ](https://oss.javaguide.cn/github/javaguide/java/2019-4%E6%AD%BB%E9%94%811.png) +![Thread Deadlock Illustration](https://oss.javaguide.cn/github/javaguide/java/2019-4%E6%AD%BB%E9%94%811.png) -下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》): +Here’s an example to illustrate thread deadlock, where the code simulates the deadlock scenario depicted above (code sourced from "Concurrent Programming in Practice"): ```java public class DeadLockDemo { - private static Object resource1 = new Object();//资源 1 - private static Object resource2 = new Object();//资源 2 + private static Object resource1 = new Object(); // Resource 1 + private static Object resource2 = new Object(); // Resource 2 public static void main(String[] args) { new Thread(() -> { synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); + System.out.println(Thread.currentThread() + " get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } - System.out.println(Thread.currentThread() + "waiting get resource2"); + System.out.println(Thread.currentThread() + " waiting get resource2"); synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); + System.out.println(Thread.currentThread() + " get resource2"); } } - }, "线程 1").start(); + }, "Thread 1").start(); new Thread(() -> { synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); + System.out.println(Thread.currentThread() + " get resource2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } - System.out.println(Thread.currentThread() + "waiting get resource1"); + System.out.println(Thread.currentThread() + " waiting get resource1"); synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); + System.out.println(Thread.currentThread() + " get resource1"); } } - }, "线程 2").start(); + }, "Thread 2").start(); } } ``` @@ -317,102 +317,102 @@ public class DeadLockDemo { Output ```plain -Thread[线程 1,5,main]get resource1 -Thread[线程 2,5,main]get resource2 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 2,5,main]waiting get resource1 +Thread[Thread 1,5,main] get resource1 +Thread[Thread 2,5,main] get resource2 +Thread[Thread 1,5,main] waiting get resource2 +Thread[Thread 2,5,main] waiting get resource1 ``` -线程 A 通过 `synchronized (resource1)` 获得 `resource1` 的监视器锁,然后通过 `Thread.sleep(1000);` 让线程 A 休眠 1s,为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。 +Thread A acquires the monitor lock on `resource1` via `synchronized (resource1)`, then sleeps for 1 second using `Thread.sleep(1000);` to allow thread B to execute and acquire the monitor lock on `resource2`. Upon waking, both threads attempt to acquire each other's resources, and they fall into a mutual wait state, resulting in a deadlock. -上面的例子符合产生死锁的四个必要条件: +The above example meets the four essential conditions for deadlock: -1. **互斥条件**:该资源任意一个时刻只由一个线程占用。 -2. **请求与保持条件**:一个线程因请求资源而阻塞时,对已获得的资源保持不放。 -3. **不剥夺条件**:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 -4. **循环等待条件**:若干线程之间形成一种头尾相接的循环等待资源关系。 +1. **Mutual Exclusion Condition:** Each resource may only be occupied by one thread at any time. +1. **Hold and Wait Condition:** A thread that is blocked waiting for a resource holds on to resources it has already acquired. +1. **No Preemption Condition:** Resources acquired by a thread cannot be forcibly taken away; they can only be released by the thread holding them after completing their task. +1. **Circular Wait Condition:** A circular wait condition exists where a set of threads forms a circular chain, each waiting for a resource held by another thread in the chain. -### 如何检测死锁? +### How to detect deadlock? -- 使用`jmap`、`jstack`等命令查看 JVM 线程栈和堆内存的情况。如果有死锁,`jstack` 的输出中通常会有 `Found one Java-level deadlock:`的字样,后面会跟着死锁相关的线程信息。另外,实际项目中还可以搭配使用`top`、`df`、`free`等命令查看操作系统的基本情况,出现死锁可能会导致 CPU、内存等资源消耗过高。 -- 采用 VisualVM、JConsole 等工具进行排查。 +- Use commands like `jmap`, `jstack`, etc., to view the JVM’s thread stack and heap memory status. If deadlock exists, the output of `jstack` typically contains the phrase `Found one Java-level deadlock:`, followed by information about the threads involved in the deadlock. Additionally, tools like `top`, `df`, `free`, etc., can be used in practical projects to observe the basic situation of the operating system; a deadlock may lead to excessive consumption of CPU, memory, and other resources. +- Utilize tools such as VisualVM and JConsole for troubleshooting. -这里以 JConsole 工具为例进行演示。 +Here’s an example demonstration using JConsole. -首先,我们要找到 JDK 的 bin 目录,找到 jconsole 并双击打开。 +First, navigate to the JDK's bin directory, find `jconsole`, and double-click to open it. ![jconsole](https://oss.javaguide.cn/github/javaguide/java/concurrent/jdk-home-bin-jconsole.png) -对于 MAC 用户来说,可以通过 `/usr/libexec/java_home -V`查看 JDK 安装目录,找到后通过 `open . + 文件夹地址`打开即可。例如,我本地的某个 JDK 的路径是: +For macOS users, you can check the JDK installation directory using `/usr/libexec/java_home -V`, then use `open . + folder address` to access it. For example, my local JDK path is: ```bash - open . /Users/guide/Library/Java/JavaVirtualMachines/corretto-1.8.0_252/Contents/Home +open . /Users/guide/Library/Java/JavaVirtualMachines/corretto-1.8.0_252/Contents/Home ``` -打开 jconsole 后,连接对应的程序,然后进入线程界面选择检测死锁即可! +After opening `jconsole`, connect to the corresponding program, go to the thread interface, and select to detect deadlocks! -![jconsole 检测死锁](https://oss.javaguide.cn/github/javaguide/java/concurrent/jconsole-check-deadlock.png) +![jconsole Deadlock Detection](https://oss.javaguide.cn/github/javaguide/java/concurrent/jconsole-check-deadlock.png) -![jconsole 检测到死锁](https://oss.javaguide.cn/github/javaguide/java/concurrent/jconsole-check-deadlock-done.png) +![jconsole Deadlock Detected](https://oss.javaguide.cn/github/javaguide/java/concurrent/jconsole-check-deadlock-done.png) -### 如何预防和避免线程死锁? +### How to prevent and avoid thread deadlock? -**如何预防死锁?** 破坏死锁的产生的必要条件即可: +**How to prevent deadlock?** By breaking the necessary conditions for deadlock: -1. **破坏请求与保持条件**:一次性申请所有的资源。 -2. **破坏不剥夺条件**:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 -3. **破坏循环等待条件**:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 +1. **Break the Hold and Wait Condition:** Request all resources at once. +1. **Break the No Preemption Condition:** A thread holding some resources should release them when it cannot request additional resources. +1. **Break the Circular Wait Condition:** Prevent deadlock by acquiring resources in a specific order; request resources in a predetermined order and release in reverse order. -**如何避免死锁?** +**How to avoid deadlock?** -避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。 +Avoiding deadlock involves calculating and evaluating resource allocation using algorithms (like the banker’s algorithm) to ensure a safe state. -> **安全状态** 指的是系统能够按照某种线程推进顺序(P1、P2、P3……Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称 `` 序列为安全序列。 +> **Safe State** means that the system can allocate resources according to a certain sequence of thread advancement (P1, P2, P3……Pn) to satisfy each thread's maximum resource demand, ensuring every thread can complete smoothly. The sequence `` is termed a safe sequence. -我们对线程 2 的代码修改成下面这样就不会产生死锁了。 +We can modify the code for thread 2 as follows to avoid deadlock. ```java new Thread(() -> { synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); + System.out.println(Thread.currentThread() + " get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } - System.out.println(Thread.currentThread() + "waiting get resource2"); + System.out.println(Thread.currentThread() + " waiting get resource2"); synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); + System.out.println(Thread.currentThread() + " get resource2"); } } - }, "线程 2").start(); + }, "Thread 2").start(); ``` -输出: +Output: ```plain -Thread[线程 1,5,main]get resource1 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 1,5,main]get resource2 -Thread[线程 2,5,main]get resource1 -Thread[线程 2,5,main]waiting get resource2 -Thread[线程 2,5,main]get resource2 +Thread[Thread 1,5,main] get resource1 +Thread[Thread 1,5,main] waiting get resource2 +Thread[Thread 1,5,main] get resource2 +Thread[Thread 2,5,main] get resource1 +Thread[Thread 2,5,main] waiting get resource2 +Thread[Thread 2,5,main] get resource2 Process finished with exit code 0 ``` -我们分析一下上面的代码为什么避免了死锁的发生? +We analyze why the code above avoids deadlock? -线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。 +Thread 1 first acquires the monitor lock on resource1; at this point, thread 2 cannot acquire it. Thread 1 then attempts to acquire the monitor lock on resource2, which it successfully obtains, after which thread 1 releases its locks on resource1 and resource2. Thread 2 can now proceed to execute. Thus, the cycle of waiting is broken, and deadlock is avoided. -## 虚拟线程 +## Virtual Threads -虚拟线程在 Java 21 正式发布,这是一项重量级的更新。我写了一篇文章来总结虚拟线程常见的问题:[虚拟线程常见问题总结](./virtual-thread.md),包含下面这些问题: +Virtual threads were formally released in Java 21, representing a significant update. I wrote an article summarizing common questions about virtual threads: [Common Questions About Virtual Threads](./virtual-thread.md), which includes the following questions: -1. 什么是虚拟线程? -2. 虚拟线程和平台线程有什么关系? -3. 虚拟线程有什么优点和缺点? -4. 如何创建虚拟线程? -5. 虚拟线程的底层原理是什么? +1. What are virtual threads? +1. How are virtual threads related to platform threads? +1. What are the pros and cons of virtual threads? +1. How to create virtual threads? +1. What are the underlying principles of virtual threads? diff --git a/docs/java/concurrent/java-concurrent-questions-02.md b/docs/java/concurrent/java-concurrent-questions-02.md index f3bb411a682..503c4e6bbdc 100644 --- a/docs/java/concurrent/java-concurrent-questions-02.md +++ b/docs/java/concurrent/java-concurrent-questions-02.md @@ -1,42 +1,42 @@ --- -title: Java并发常见面试题总结(中) +title: Summary of Common Java Concurrency Interview Questions (Part 2) category: Java tag: - - Java并发 + - Java Concurrency head: - - - meta - - name: keywords - content: 多线程,死锁,synchronized,ReentrantLock,volatile,ThreadLocal,线程池,CAS,AQS - - - meta - - name: description - content: Java并发常见知识点和面试题总结(含详细解答)。 + - - meta + - name: keywords + content: multithreading, deadlock, synchronized, ReentrantLock, volatile, ThreadLocal, thread pool, CAS, AQS + - - meta + - name: description + content: Summary of common knowledge points and interview questions in Java concurrency (with detailed answers). --- -## ⭐️JMM(Java 内存模型) +## ⭐️ JMM (Java Memory Model) -JMM(Java 内存模型)相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 JMM 相关的知识点和问题:[JMM(Java 内存模型)详解](./jmm.md) 。 +There are many important questions related to JMM (Java Memory Model), so I have dedicated a separate article to summarize the knowledge points and questions related to JMM: [Detailed Explanation of JMM (Java Memory Model)](./jmm.md). -## ⭐️volatile 关键字 +## ⭐️ volatile Keyword -### 如何保证变量的可见性? +### How to Ensure Variable Visibility? -在 Java 中,`volatile` 关键字可以保证变量的可见性,如果我们将变量声明为 **`volatile`** ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。 +In Java, the `volatile` keyword can ensure the visibility of a variable. If we declare a variable as **`volatile`**, it indicates to the JVM that this variable is shared and unstable, and every time it is used, it will be read from the main memory. -![JMM(Java 内存模型)](https://oss.javaguide.cn/github/javaguide/java/concurrent/jmm.png) +![JMM (Java Memory Model)](https://oss.javaguide.cn/github/javaguide/java/concurrent/jmm.png) -![JMM(Java 内存模型)强制在主存中进行读取](https://oss.javaguide.cn/github/javaguide/java/concurrent/jmm2.png) +![JMM (Java Memory Model) Forces Reading from Main Memory](https://oss.javaguide.cn/github/javaguide/java/concurrent/jmm2.png) -`volatile` 关键字其实并非是 Java 语言特有的,在 C 语言里也有,它最原始的意义就是禁用 CPU 缓存。如果我们将一个变量使用 `volatile` 修饰,这就指示 编译器,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。 +The `volatile` keyword is not unique to the Java language; it also exists in C. Its original meaning is to disable CPU caching. If we modify a variable with `volatile`, it indicates to the compiler that this variable is shared and unstable, and every time it is used, it will be read from the main memory. -`volatile` 关键字能保证数据的可见性,但不能保证数据的原子性。`synchronized` 关键字两者都能保证。 +The `volatile` keyword can ensure data visibility but cannot guarantee data atomicity. The `synchronized` keyword can guarantee both. -### 如何禁止指令重排序? +### How to Prevent Instruction Reordering? -**在 Java 中,`volatile` 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序。** 如果我们将变量声明为 **`volatile`** ,在对这个变量进行读写操作的时候,会通过插入特定的 **内存屏障** 的方式来禁止指令重排序。 +**In Java, the `volatile` keyword not only ensures variable visibility but also plays an important role in preventing JVM instruction reordering.** If we declare a variable as **`volatile`**, when performing read and write operations on this variable, specific **memory barriers** will be inserted to prevent instruction reordering. -在 Java 中,`Unsafe` 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异: +In Java, the `Unsafe` class provides three out-of-the-box methods related to memory barriers, which shield the differences at the operating system level: ```java public native void loadFence(); @@ -44,13 +44,13 @@ public native void storeFence(); public native void fullFence(); ``` -理论上来说,你通过这个三个方法也可以实现和`volatile`禁止重排序一样的效果,只是会麻烦一些。 +Theoretically, you can achieve the same effect as `volatile` in preventing reordering using these three methods, but it would be more complicated. -下面我以一个常见的面试题为例讲解一下 `volatile` 关键字禁止指令重排序的效果。 +Below, I will explain the effect of the `volatile` keyword in preventing instruction reordering using a common interview question. -面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” +Interviewers often ask: "Are you familiar with the singleton pattern? Please write it out for me! Explain the principle of implementing the singleton pattern using double-checked locking!" -**双重校验锁实现对象单例(线程安全)**: +**Double-Checked Locking Implementation of Singleton (Thread-Safe)**: ```java public class Singleton { @@ -60,10 +60,10 @@ public class Singleton { private Singleton() { } - public static Singleton getUniqueInstance() { - //先判断对象是否已经实例过,没有实例化过才进入加锁代码 + public static Singleton getUniqueInstance() { + // First check if the object has already been instantiated; only enter the locking code if it hasn't been instantiated if (uniqueInstance == null) { - //类对象加锁 + // Lock the class object synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); @@ -75,845 +75,29 @@ public class Singleton { } ``` -`uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要的, `uniqueInstance = new Singleton();` 这段代码其实是分为三步执行: +It is necessary to use the `volatile` keyword to modify `uniqueInstance`. The line `uniqueInstance = new Singleton();` is actually executed in three steps: -1. 为 `uniqueInstance` 分配内存空间 -2. 初始化 `uniqueInstance` -3. 将 `uniqueInstance` 指向分配的内存地址 +1. Allocate memory space for `uniqueInstance`. +1. Initialize `uniqueInstance`. +1. Point `uniqueInstance` to the allocated memory address. -但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 `getUniqueInstance`() 后发现 `uniqueInstance` 不为空,因此返回 `uniqueInstance`,但此时 `uniqueInstance` 还未被初始化。 +However, due to the JVM's instruction reordering characteristics, the execution order may become 1->3->2. Instruction reordering does not pose a problem in a single-threaded environment, but in a multi-threaded environment, it can lead to one thread obtaining an instance that has not yet been initialized. For example, if thread T1 executes steps 1 and 3, and then thread T2 calls `getUniqueInstance()` and finds `uniqueInstance` is not null, it will return `uniqueInstance`, but at this point, `uniqueInstance` has not yet been initialized. -### volatile 可以保证原子性么? +### Can volatile Ensure Atomicity? -**`volatile` 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。** +**The `volatile` keyword can ensure variable visibility but cannot guarantee that operations on the variable are atomic.** -我们通过下面的代码即可证明: +We can prove this with the following code: ```java /** - * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 + * Search for "JavaGuide" on WeChat to receive a free personal original Java interview manual * - * @author Guide哥 + * @author Guide * @date 2022/08/03 13:40 **/ public class VolatileAtomicityDemo { public volatile static int inc = 0; public void increase() { - inc++; - } - - public static void main(String[] args) throws InterruptedException { - ExecutorService threadPool = Executors.newFixedThreadPool(5); - VolatileAtomicityDemo volatileAtomicityDemo = new VolatileAtomicityDemo(); - for (int i = 0; i < 5; i++) { - threadPool.execute(() -> { - for (int j = 0; j < 500; j++) { - volatileAtomicityDemo.increase(); - } - }); - } - // 等待1.5秒,保证上面程序执行完成 - Thread.sleep(1500); - System.out.println(inc); - threadPool.shutdown(); - } -} -``` - -正常情况下,运行上面的代码理应输出 `2500`。但你真正运行了上面的代码之后,你会发现每次输出结果都小于 `2500`。 - -为什么会出现这种情况呢?不是说好了,`volatile` 可以保证变量的可见性嘛! - -也就是说,如果 `volatile` 能保证 `inc++` 操作的原子性的话。每个线程中对 `inc` 变量自增完之后,其他线程可以立即看到修改后的值。5 个线程分别进行了 500 次操作,那么最终 inc 的值应该是 5\*500=2500。 - -很多人会误认为自增操作 `inc++` 是原子性的,实际上,`inc++` 其实是一个复合操作,包括三步: - -1. 读取 inc 的值。 -2. 对 inc 加 1。 -3. 将 inc 的值写回内存。 - -`volatile` 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现: - -1. 线程 1 对 `inc` 进行读取操作之后,还未对其进行修改。线程 2 又读取了 `inc`的值并对其进行修改(+1),再将`inc` 的值写回内存。 -2. 线程 2 操作完毕后,线程 1 对 `inc`的值进行修改(+1),再将`inc` 的值写回内存。 - -这也就导致两个线程分别对 `inc` 进行了一次自增操作后,`inc` 实际上只增加了 1。 - -其实,如果想要保证上面的代码运行正确也非常简单,利用 `synchronized`、`Lock`或者`AtomicInteger`都可以。 - -使用 `synchronized` 改进: - -```java -public synchronized void increase() { - inc++; -} -``` - -使用 `AtomicInteger` 改进: - -```java -public AtomicInteger inc = new AtomicInteger(); - -public void increase() { - inc.getAndIncrement(); -} -``` - -使用 `ReentrantLock` 改进: - -```java -Lock lock = new ReentrantLock(); -public void increase() { - lock.lock(); - try { - inc++; - } finally { - lock.unlock(); - } -} -``` - -## ⭐️乐观锁和悲观锁 - -### 什么是悲观锁? - -悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。 - -像 Java 中`synchronized`和`ReentrantLock`等独占锁就是悲观锁思想的实现。 - -```java -public void performSynchronisedTask() { - synchronized (this) { - // 需要同步的操作 - } -} - -private Lock lock = new ReentrantLock(); -lock.lock(); -try { - // 需要同步的操作 -} finally { - lock.unlock(); -} -``` - -高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。 - -### 什么是乐观锁? - -乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。 - -在 Java 中`java.util.concurrent.atomic`包下面的原子变量类(比如`AtomicInteger`、`LongAdder`)就是使用了乐观锁的一种实现方式 **CAS** 实现的。 -![JUC原子类概览](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88-20230814005211968.png) - -```java -// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好 -// 代价就是会消耗更多的内存空间(空间换时间) -LongAdder sum = new LongAdder(); -sum.increment(); -``` - -高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。 - -不过,大量失败重试的问题也是可以解决的,像我们前面提到的 `LongAdder`以空间换时间的方式就解决了这个问题。 - -理论上来说: - -- 悲观锁通常多用于写比较多的情况(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如`LongAdder`),也是可以考虑使用乐观锁的,要视实际情况而定。 -- 乐观锁通常多用于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考`java.util.concurrent.atomic`包下面的原子变量类)。 - -### 如何实现乐观锁? - -乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。 - -#### 版本号机制 - -一般是在数据表中加上一个数据版本号 `version` 字段,表示数据被修改的次数。当数据被修改时,`version` 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 `version` 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 `version` 值相等时才更新,否则重试更新操作,直到更新成功。 - -**举一个简单的例子**:假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( `balance` )为 \$100 。 - -1. 操作员 A 此时将其读出( `version`=1 ),并从其帐户余额中扣除 $50( $100-\$50 )。 -2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( `version`=1 ),并从其帐户余额中扣除 $20 ( $100-\$20 )。 -3. 操作员 A 完成了修改工作,将数据版本号( `version`=1 ),连同帐户扣除后余额( `balance`=\$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 `version` 更新为 2 。 -4. 操作员 B 完成了操作,也将版本号( `version`=1 )试图向数据库提交数据( `balance`=\$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 - -这样就避免了操作员 B 用基于 `version`=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。 - -#### CAS 算法 - -CAS 的全称是 **Compare And Swap(比较与交换)** ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。 - -CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。 - -> **原子操作** 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。 - -CAS 涉及到三个操作数: - -- **V**:要更新的变量值(Var) -- **E**:预期值(Expected) -- **N**:拟写入的新值(New) - -当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。 - -**举一个简单的例子**:线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。 - -1. i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。 -2. i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。 - -当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。 - -Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及 CPU 都有关系。 - -`sun.misc`包下的`Unsafe`类提供了`compareAndSwapObject`、`compareAndSwapInt`、`compareAndSwapLong`方法来实现的对`Object`、`int`、`long`类型的 CAS 操作 - -```java -/** - * CAS - * @param o 包含要修改field的对象 - * @param offset 对象中某field的偏移量 - * @param expected 期望值 - * @param update 更新值 - * @return true | false - */ -public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update); - -public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update); - -public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update); -``` - -关于 `Unsafe` 类的详细介绍可以看这篇文章:[Java 魔法类 Unsafe 详解 - JavaGuide - 2022](https://javaguide.cn/java/basis/unsafe.html) 。 - -### Java 中 CAS 是如何实现的? - -在 Java 中,实现 CAS(Compare-And-Swap, 比较并交换)操作的一个关键类是`Unsafe`。 - -`Unsafe`类位于`sun.misc`包下,是一个提供低级别、不安全操作的类。由于其强大的功能和潜在的危险性,它通常用于 JVM 内部或一些需要极高性能和底层访问的库中,而不推荐普通开发者在应用程序中使用。关于 `Unsafe`类的详细介绍,可以阅读这篇文章:📌[Java 魔法类 Unsafe 详解](https://javaguide.cn/java/basis/unsafe.html)。 - -`sun.misc`包下的`Unsafe`类提供了`compareAndSwapObject`、`compareAndSwapInt`、`compareAndSwapLong`方法来实现的对`Object`、`int`、`long`类型的 CAS 操作: - -```java -/** - * 以原子方式更新对象字段的值。 - * - * @param o 要操作的对象 - * @param offset 对象字段的内存偏移量 - * @param expected 期望的旧值 - * @param x 要设置的新值 - * @return 如果值被成功更新,则返回 true;否则返回 false - */ -boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); - -/** - * 以原子方式更新 int 类型的对象字段的值。 - */ -boolean compareAndSwapInt(Object o, long offset, int expected, int x); - -/** - * 以原子方式更新 long 类型的对象字段的值。 - */ -boolean compareAndSwapLong(Object o, long offset, long expected, long x); -``` - -`Unsafe`类中的 CAS 方法是`native`方法。`native`关键字表明这些方法是用本地代码(通常是 C 或 C++)实现的,而不是用 Java 实现的。这些方法直接调用底层的硬件指令来实现原子操作。也就是说,Java 语言并没有直接用 Java 实现 CAS,而是通过 C++ 内联汇编的形式实现的(通过 JNI 调用)。因此,CAS 的具体实现与操作系统以及 CPU 密切相关。 - -`java.util.concurrent.atomic` 包提供了一些用于原子操作的类。这些类利用底层的原子指令,确保在多线程环境下的操作是线程安全的。 - -![JUC原子类概览](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88.png) - -关于这些 Atomic 原子类的介绍和使用,可以阅读这篇文章:[Atomic 原子类总结](https://javaguide.cn/java/concurrent/atomic-classes.html)。 - -`AtomicInteger`是 Java 的原子类之一,主要用于对 `int` 类型的变量进行原子操作,它利用`Unsafe`类提供的低级别原子操作方法实现无锁的线程安全性。 - -下面,我们通过解读`AtomicInteger`的核心源码(JDK1.8),来说明 Java 如何使用`Unsafe`类的方法来实现原子操作。 - -`AtomicInteger`核心源码如下: - -```java -// 获取 Unsafe 实例 -private static final Unsafe unsafe = Unsafe.getUnsafe(); -private static final long valueOffset; - -static { - try { - // 获取“value”字段在AtomicInteger类中的内存偏移量 - valueOffset = unsafe.objectFieldOffset - (AtomicInteger.class.getDeclaredField("value")); - } catch (Exception ex) { throw new Error(ex); } -} -// 确保“value”字段的可见性 -private volatile int value; - -// 如果当前值等于预期值,则原子地将值设置为newValue -// 使用 Unsafe#compareAndSwapInt 方法进行CAS操作 -public final boolean compareAndSet(int expect, int update) { - return unsafe.compareAndSwapInt(this, valueOffset, expect, update); -} - -// 原子地将当前值加 delta 并返回旧值 -public final int getAndAdd(int delta) { - return unsafe.getAndAddInt(this, valueOffset, delta); -} - -// 原子地将当前值加 1 并返回加之前的值(旧值) -// 使用 Unsafe#getAndAddInt 方法进行CAS操作。 -public final int getAndIncrement() { - return unsafe.getAndAddInt(this, valueOffset, 1); -} - -// 原子地将当前值减 1 并返回减之前的值(旧值) -public final int getAndDecrement() { - return unsafe.getAndAddInt(this, valueOffset, -1); -} -``` - -`Unsafe#getAndAddInt`源码: - -```java -// 原子地获取并增加整数值 -public final int getAndAddInt(Object o, long offset, int delta) { - int v; - do { - // 以 volatile 方式获取对象 o 在内存偏移量 offset 处的整数值 - v = getIntVolatile(o, offset); - } while (!compareAndSwapInt(o, offset, v, v + delta)); - // 返回旧值 - return v; -} -``` - -可以看到,`getAndAddInt` 使用了 `do-while` 循环:在`compareAndSwapInt`操作失败时,会不断重试直到成功。也就是说,`getAndAddInt`方法会通过 `compareAndSwapInt` 方法来尝试更新 `value` 的值,如果更新失败(当前值在此期间被其他线程修改),它会重新获取当前值并再次尝试更新,直到操作成功。 - -由于 CAS 操作可能会因为并发冲突而失败,因此通常会与`while`循环搭配使用,在失败后不断重试,直到操作成功。这就是 **自旋锁机制** 。 - -### CAS 算法存在哪些问题? - -ABA 问题是 CAS 算法最常见的问题。 - -#### ABA 问题 - -如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 **"ABA"问题。** - -ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 - -```java -public boolean compareAndSet(V expectedReference, - V newReference, - int expectedStamp, - int newStamp) { - Pair current = pair; - return - expectedReference == current.reference && - expectedStamp == current.stamp && - ((newReference == current.reference && - newStamp == current.stamp) || - casPair(current, Pair.of(newReference, newStamp))); -} -``` - -#### 循环时间长开销大 - -CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。 - -如果 JVM 能够支持处理器提供的`pause`指令,那么自旋操作的效率将有所提升。`pause`指令有两个重要作用: - -1. **延迟流水线执行指令**:`pause`指令可以延迟指令的执行,从而减少 CPU 的资源消耗。具体的延迟时间取决于处理器的实现版本,在某些处理器上,延迟时间可能为零。 -2. **避免内存顺序冲突**:在退出循环时,`pause`指令可以避免由于内存顺序冲突而导致的 CPU 流水线被清空,从而提高 CPU 的执行效率。 - -#### 只能保证一个共享变量的原子操作 - -CAS 操作仅能对单个共享变量有效。当需要操作多个共享变量时,CAS 就显得无能为力。不过,从 JDK 1.5 开始,Java 提供了`AtomicReference`类,这使得我们能够保证引用对象之间的原子性。通过将多个变量封装在一个对象中,我们可以使用`AtomicReference`来执行 CAS 操作。 - -除了 `AtomicReference` 这种方式之外,还可以利用加锁来保证。 - -## synchronized 关键字 - -### synchronized 是什么?有什么用? - -`synchronized` 是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 - -在 Java 早期版本中,`synchronized` 属于 **重量级锁**,效率低下。这是因为监视器锁(monitor)是依赖于底层的操作系统的 `Mutex Lock` 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。 - -不过,在 Java 6 之后, `synchronized` 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 `synchronized` 锁的效率提升了很多。因此, `synchronized` 还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了 `synchronized` 。 - -关于偏向锁多补充一点:由于偏向锁增加了 JVM 的复杂性,同时也并没有为所有应用都带来性能提升。因此,在 JDK15 中,偏向锁被默认关闭(仍然可以使用 `-XX:+UseBiasedLocking` 启用偏向锁),在 JDK18 中,偏向锁已经被彻底废弃(无法通过命令行打开)。 - -### 如何使用 synchronized? - -`synchronized` 关键字的使用方式主要有下面 3 种: - -1. 修饰实例方法 -2. 修饰静态方法 -3. 修饰代码块 - -**1、修饰实例方法** (锁当前对象实例) - -给当前对象实例加锁,进入同步代码前要获得 **当前对象实例的锁** 。 - -```java -synchronized void method() { - //业务代码 -} -``` - -**2、修饰静态方法** (锁当前类) - -给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 **当前 class 的锁**。 - -这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。 - -```java -synchronized static void method() { - //业务代码 -} -``` - -静态 `synchronized` 方法和非静态 `synchronized` 方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态 `synchronized` 方法,而线程 B 需要调用这个实例对象所属类的静态 `synchronized` 方法,是允许的,不会发生互斥现象,因为访问静态 `synchronized` 方法占用的锁是当前类的锁,而访问非静态 `synchronized` 方法占用的锁是当前实例对象锁。 - -**3、修饰代码块** (锁指定对象/类) - -对括号里指定的对象/类加锁: - -- `synchronized(object)` 表示进入同步代码块前要获得 **给定对象的锁**。 -- `synchronized(类.class)` 表示进入同步代码块前要获得 **给定 Class 的锁** - -```java -synchronized(this) { - //业务代码 -} -``` - -**总结:** - -- `synchronized` 关键字加到 `static` 静态方法和 `synchronized(class)` 代码块上都是是给 Class 类上锁; -- `synchronized` 关键字加到实例方法上是给对象实例上锁; -- 尽量不要使用 `synchronized(String a)` 因为 JVM 中,字符串常量池具有缓存功能。 - -### 构造方法可以用 synchronized 修饰么? - -构造方法不能使用 synchronized 关键字修饰。不过,可以在构造方法内部使用 synchronized 代码块。 - -另外,构造方法本身是线程安全的,但如果在构造方法中涉及到共享资源的操作,就需要采取适当的同步措施来保证整个构造过程的线程安全。 - -### ⭐️synchronized 底层原理了解吗? - -synchronized 关键字底层原理属于 JVM 层面的东西。 - -#### synchronized 同步语句块的情况 - -```java -public class SynchronizedDemo { - public void method() { - synchronized (this) { - System.out.println("synchronized 代码块"); - } - } -} -``` - -通过 JDK 自带的 `javap` 命令查看 `SynchronizedDemo` 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。 - -![synchronized关键字原理](https://oss.javaguide.cn/github/javaguide/java/concurrent/synchronized-principle.png) - -从上面我们可以看出:**`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。** - -上面的字节码中包含一个 `monitorenter` 指令以及两个 `monitorexit` 指令,这是为了保证锁在同步代码块代码正常执行以及出现异常的这两种情况下都能被正确释放。 - -当执行 `monitorenter` 指令时,线程试图获取锁也就是获取 **对象监视器 `monitor`** 的持有权。 - -> 在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由[ObjectMonitor](https://github.com/openjdk-mirror/jdk7u-hotspot/blob/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/runtime/objectMonitor.cpp)实现的。每个对象中都内置了一个 `ObjectMonitor`对象。 -> -> 另外,`wait/notify`等方法也依赖于`monitor`对象,这就是为什么只有在同步的块或者方法中才能调用`wait/notify`等方法,否则会抛出`java.lang.IllegalMonitorStateException`的异常的原因。 - -在执行`monitorenter`时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。 - -![执行 monitorenter 获取锁](https://oss.javaguide.cn/github/javaguide/java/concurrent/synchronized-get-lock-code-block.png) - -对象锁的拥有者线程才可以执行 `monitorexit` 指令来释放锁。在执行 `monitorexit` 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。 - -![执行 monitorexit 释放锁](https://oss.javaguide.cn/github/javaguide/java/concurrent/synchronized-release-lock-block.png) - -如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 - -#### synchronized 修饰方法的情况 - -```java -public class SynchronizedDemo2 { - public synchronized void method() { - System.out.println("synchronized 方法"); - } -} - -``` - -![synchronized关键字原理](https://oss.javaguide.cn/github/javaguide/synchronized%E5%85%B3%E9%94%AE%E5%AD%97%E5%8E%9F%E7%90%862.png) - -`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取而代之的是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 - -如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。 - -#### 总结 - -`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。 - -`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取而代之的是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。 - -**不过,两者的本质都是对对象监视器 monitor 的获取。** - -相关推荐:[Java 锁与线程的那些事 - 有赞技术团队](https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/) 。 - -🧗🏻 进阶一下:学有余力的小伙伴可以抽时间详细研究一下对象监视器 `monitor`。 - -### JDK1.6 之后的 synchronized 底层做了哪些优化?锁升级原理了解吗? - -在 Java 6 之后, `synchronized` 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 `synchronized` 锁的效率提升了很多(JDK18 中,偏向锁已经被彻底废弃,前面已经提到过了)。 - -锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 - -`synchronized` 锁升级是一个比较复杂的过程,面试也很少问到,如果你想要详细了解的话,可以看看这篇文章:[浅析 synchronized 锁升级的原理与实现](https://www.cnblogs.com/star95/p/17542850.html)。 - -### synchronized 的偏向锁为什么被废弃了? - -Open JDK 官方声明:[JEP 374: Deprecate and Disable Biased Locking](https://openjdk.org/jeps/374) - -在 JDK15 中,偏向锁被默认关闭(仍然可以使用 `-XX:+UseBiasedLocking` 启用偏向锁),在 JDK18 中,偏向锁已经被彻底废弃(无法通过命令行打开)。 - -在官方声明中,主要原因有两个方面: - -- **性能收益不明显:** - -偏向锁是 HotSpot 虚拟机的一项优化技术,可以提升单线程对同步代码块的访问性能。 - -受益于偏向锁的应用程序通常使用了早期的 Java 集合 API,例如 HashTable、Vector,在这些集合类中通过 synchronized 来控制同步,这样在单线程频繁访问时,通过偏向锁会减少同步开销。 - -随着 JDK 的发展,出现了 ConcurrentHashMap 高性能的集合类,在集合类内部进行了许多性能优化,此时偏向锁带来的性能收益就不明显了。 - -偏向锁仅仅在单线程访问同步代码块的场景中可以获得性能收益。 - -如果存在多线程竞争,就需要 **撤销偏向锁** ,这个操作的性能开销是比较昂贵的。偏向锁的撤销需要等待进入到全局安全点(safe point),该状态下所有线程都是暂停的,此时去检查线程状态并进行偏向锁的撤销。 - -- **JVM 内部代码维护成本太高:** - -偏向锁将许多复杂代码引入到同步子系统,并且对其他的 HotSpot 组件也具有侵入性。这种复杂性为理解代码、系统重构带来了困难,因此, OpenJDK 官方希望禁用、废弃并删除偏向锁。 - -### ⭐️synchronized 和 volatile 有什么区别? - -`synchronized` 关键字和 `volatile` 关键字是两个互补的存在,而不是对立的存在! - -- `volatile` 关键字是线程同步的轻量级实现,所以 `volatile`性能肯定比`synchronized`关键字要好 。但是 `volatile` 关键字只能用于变量而 `synchronized` 关键字可以修饰方法以及代码块 。 -- `volatile` 关键字能保证数据的可见性,但不能保证数据的原子性。`synchronized` 关键字两者都能保证。 -- `volatile`关键字主要用于解决变量在多个线程之间的可见性,而 `synchronized` 关键字解决的是多个线程之间访问资源的同步性。 - -## ReentrantLock - -### ReentrantLock 是什么? - -`ReentrantLock` 实现了 `Lock` 接口,是一个可重入且独占式的锁,和 `synchronized` 关键字类似。不过,`ReentrantLock` 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。 - -```java -public class ReentrantLock implements Lock, java.io.Serializable {} -``` - -`ReentrantLock` 里面有一个内部类 `Sync`,`Sync` 继承 AQS(`AbstractQueuedSynchronizer`),添加锁和释放锁的大部分操作实际上都是在 `Sync` 中实现的。`Sync` 有公平锁 `FairSync` 和非公平锁 `NonfairSync` 两个子类。 - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/reentrantlock-class-diagram.png) - -`ReentrantLock` 默认使用非公平锁,也可以通过构造器来显式的指定使用公平锁。 - -```java -// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁 -public ReentrantLock(boolean fair) { - sync = fair ? new FairSync() : new NonfairSync(); -} -``` - -从上面的内容可以看出, `ReentrantLock` 的底层就是由 AQS 来实现的。关于 AQS 的相关内容推荐阅读 [AQS 详解](https://javaguide.cn/java/concurrent/aqs.html) 这篇文章。 - -### 公平锁和非公平锁有什么区别? - -- **公平锁** : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。 -- **非公平锁**:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。 - -### ⭐️synchronized 和 ReentrantLock 有什么区别? - -#### 两者都是可重入锁 - -**可重入锁** 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。 - -JDK 提供的所有现成的 `Lock` 实现类,包括 `synchronized` 关键字锁都是可重入的。 - -在下面的代码中,`method1()` 和 `method2()`都被 `synchronized` 关键字修饰,`method1()`调用了`method2()`。 - -```java -public class SynchronizedDemo { - public synchronized void method1() { - System.out.println("方法1"); - method2(); - } - - public synchronized void method2() { - System.out.println("方法2"); - } -} -``` - -由于 `synchronized`锁是可重入的,同一个线程在调用`method1()` 时可以直接获得当前对象的锁,执行 `method2()` 的时候可以再次获取这个对象的锁,不会产生死锁问题。假如`synchronized`是不可重入锁的话,由于该对象的锁已被当前线程所持有且无法释放,这就导致线程在执行 `method2()`时获取锁失败,会出现死锁问题。 - -#### synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API - -`synchronized` 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 `synchronized` 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。 - -`ReentrantLock` 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 - -#### ReentrantLock 比 synchronized 增加了一些高级功能 - -相比`synchronized`,`ReentrantLock`增加了一些高级功能。主要来说主要有三点: - -- **等待可中断** : `ReentrantLock`提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说当前线程在等待获取锁的过程中,如果其他线程中断当前线程「 `interrupt()` 」,当前线程就会抛出 `InterruptedException` 异常,可以捕捉该异常进行相应处理。 -- **可实现公平锁** : `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。`ReentrantLock`默认情况是非公平的,可以通过 `ReentrantLock`类的`ReentrantLock(boolean fair)`构造方法来指定是否是公平的。 -- **可实现选择性通知(锁可以绑定多个条件)**: `synchronized`关键字与`wait()`和`notify()`/`notifyAll()`方法相结合可以实现等待/通知机制。`ReentrantLock`类当然也可以实现,但是需要借助于`Condition`接口与`newCondition()`方法。 -- **支持超时** :`ReentrantLock` 提供了 `tryLock(timeout)` 的方法,可以指定等待获取锁的最长等待时间,如果超过了等待时间,就会获取锁失败,不会一直等待。 - -如果你想使用上述功能,那么选择 `ReentrantLock` 是一个不错的选择。 - -关于 `Condition`接口的补充: - -> `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 `Condition` 接口默认提供的。而`synchronized`关键字就相当于整个 `Lock` 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而`Condition`实例的`signalAll()`方法,只会唤醒注册在该`Condition`实例中的所有等待线程。 - -关于 **等待可中断** 的补充: - -> `lockInterruptibly()` 会让获取锁的线程在阻塞等待的过程中可以响应中断,即当前线程在获取锁的时候,发现锁被其他线程持有,就会阻塞等待。 -> -> 在阻塞等待的过程中,如果其他线程中断当前线程 `interrupt()` ,就会抛出 `InterruptedException` 异常,可以捕获该异常,做一些处理操作。 -> -> 为了更好理解这个方法,借用 Stack Overflow 上的一个案例,可以更好地理解 `lockInterruptibly()` 可以响应中断: -> -> ```JAVA -> public class MyRentrantlock { -> Thread t = new Thread() { -> @Override -> public void run() { -> ReentrantLock r = new ReentrantLock(); -> // 1.1、第一次尝试获取锁,可以获取成功 -> r.lock(); -> -> // 1.2、此时锁的重入次数为 1 -> System.out.println("lock() : lock count :" + r.getHoldCount()); -> -> // 2、中断当前线程,通过 Thread.currentThread().isInterrupted() 可以看到当前线程的中断状态为 true -> interrupt(); -> System.out.println("Current thread is intrupted"); -> -> // 3.1、尝试获取锁,可以成功获取 -> r.tryLock(); -> // 3.2、此时锁的重入次数为 2 -> System.out.println("tryLock() on intrupted thread lock count :" + r.getHoldCount()); -> try { -> // 4、打印线程的中断状态为 true,那么调用 lockInterruptibly() 方法就会抛出 InterruptedException 异常 -> System.out.println("Current Thread isInterrupted:" + Thread.currentThread().isInterrupted()); -> r.lockInterruptibly(); -> System.out.println("lockInterruptibly() --NOt executable statement" + r.getHoldCount()); -> } catch (InterruptedException e) { -> r.lock(); -> System.out.println("Error"); -> } finally { -> r.unlock(); -> } -> -> // 5、打印锁的重入次数,可以发现 lockInterruptibly() 方法并没有成功获取到锁 -> System.out.println("lockInterruptibly() not able to Acqurie lock: lock count :" + r.getHoldCount()); -> -> r.unlock(); -> System.out.println("lock count :" + r.getHoldCount()); -> r.unlock(); -> System.out.println("lock count :" + r.getHoldCount()); -> } -> }; -> public static void main(String str[]) { -> MyRentrantlock m = new MyRentrantlock(); -> m.t.start(); -> } -> } -> ``` -> -> 输出: -> -> ```BASH -> lock() : lock count :1 -> Current thread is intrupted -> tryLock() on intrupted thread lock count :2 -> Current Thread isInterrupted:true -> Error -> lockInterruptibly() not able to Acqurie lock: lock count :2 -> lock count :1 -> lock count :0 -> ``` - -关于 **支持超时** 的补充: - -> **为什么需要 `tryLock(timeout)` 这个功能呢?** -> -> `tryLock(timeout)` 方法尝试在指定的超时时间内获取锁。如果成功获取锁,则返回 `true`;如果在锁可用之前超时,则返回 `false`。此功能在以下几种场景中非常有用: -> -> - **防止死锁:** 在复杂的锁场景中,`tryLock(timeout)` 可以通过允许线程在合理的时间内放弃并重试来帮助防止死锁。 -> - **提高响应速度:** 防止线程无限期阻塞。 -> - **处理时间敏感的操作:** 对于具有严格时间限制的操作,`tryLock(timeout)` 允许线程在无法及时获取锁时继续执行替代操作。 - -### 可中断锁和不可中断锁有什么区别? - -- **可中断锁**:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。`ReentrantLock` 就属于是可中断锁。 -- **不可中断锁**:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 `synchronized` 就属于是不可中断锁。 - -## ReentrantReadWriteLock - -`ReentrantReadWriteLock` 在实际项目中使用的并不多,面试中也问的比较少,简单了解即可。JDK 1.8 引入了性能更好的读写锁 `StampedLock` 。 - -### ReentrantReadWriteLock 是什么? - -`ReentrantReadWriteLock` 实现了 `ReadWriteLock` ,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。 - -```java -public class ReentrantReadWriteLock - implements ReadWriteLock, java.io.Serializable{ -} -public interface ReadWriteLock { - Lock readLock(); - Lock writeLock(); -} -``` - -- 一般锁进行并发控制的规则:读读互斥、读写互斥、写写互斥。 -- 读写锁进行并发控制的规则:读读不互斥、读写互斥、写写互斥(只有读读不互斥)。 - -`ReentrantReadWriteLock` 其实是两把锁,一把是 `WriteLock` (写锁),一把是 `ReadLock`(读锁) 。读锁是共享锁,写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。 - -和 `ReentrantLock` 一样,`ReentrantReadWriteLock` 底层也是基于 AQS 实现的。 - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/reentrantreadwritelock-class-diagram.png) - -`ReentrantReadWriteLock` 也支持公平锁和非公平锁,默认使用非公平锁,可以通过构造器来显示的指定。 - -```java -// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁 -public ReentrantReadWriteLock(boolean fair) { - sync = fair ? new FairSync() : new NonfairSync(); - readerLock = new ReadLock(this); - writerLock = new WriteLock(this); -} -``` - -### ReentrantReadWriteLock 适合什么场景? - -由于 `ReentrantReadWriteLock` 既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。因此,在读多写少的情况下,使用 `ReentrantReadWriteLock` 能够明显提升系统性能。 - -### 共享锁和独占锁有什么区别? - -- **共享锁**:一把锁可以被多个线程同时获得。 -- **独占锁**:一把锁只能被一个线程获得。 - -### 线程持有读锁还能获取写锁吗? - -- 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。 -- 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。 - -读写锁的源码分析,推荐阅读 [聊聊 Java 的几把 JVM 级锁 - 阿里巴巴中间件](https://mp.weixin.qq.com/s/h3VIUyH9L0v14MrQJiiDbw) 这篇文章,写的很不错。 - -### 读锁为什么不能升级为写锁? - -写锁可以降级为读锁,但是读锁却不能升级为写锁。这是因为读锁升级为写锁会引起线程的争夺,毕竟写锁属于是独占锁,这样的话,会影响性能。 - -另外,还可能会有死锁问题发生。举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。 - -## StampedLock - -`StampedLock` 面试中问的比较少,不是很重要,简单了解即可。 - -### StampedLock 是什么? - -`StampedLock` 是 JDK 1.8 引入的性能更好的读写锁,不可重入且不支持条件变量 `Condition`。 - -不同于一般的 `Lock` 类,`StampedLock` 并不是直接实现 `Lock`或 `ReadWriteLock`接口,而是基于 **CLH 锁** 独立实现的(AQS 也是基于这玩意)。 - -```java -public class StampedLock implements java.io.Serializable { -} -``` - -`StampedLock` 提供了三种模式的读写控制模式:读锁、写锁和乐观读。 - -- **写锁**:独占锁,一把锁只能被一个线程获得。当一个线程获取写锁后,其他请求读锁和写锁的线程必须等待。类似于 `ReentrantReadWriteLock` 的写锁,不过这里的写锁是不可重入的。 -- **读锁** (悲观读):共享锁,没有线程获取写锁的情况下,多个线程可以同时持有读锁。如果己经有线程持有写锁,则其他线程请求获取该读锁会被阻塞。类似于 `ReentrantReadWriteLock` 的读锁,不过这里的读锁是不可重入的。 -- **乐观读**:允许多个线程获取乐观读以及读锁。同时允许一个写线程获取写锁。 - -另外,`StampedLock` 还支持这三种锁在一定条件下进行相互转换 。 - -```java -long tryConvertToWriteLock(long stamp){} -long tryConvertToReadLock(long stamp){} -long tryConvertToOptimisticRead(long stamp){} -``` - -`StampedLock` 在获取锁的时候会返回一个 long 型的数据戳,该数据戳用于稍后的锁释放参数,如果返回的数据戳为 0 则表示锁获取失败。当前线程持有了锁再次获取锁还是会返回一个新的数据戳,这也是`StampedLock`不可重入的原因。 - -```java -// 写锁 -public long writeLock() { - long s, next; // bypass acquireWrite in fully unlocked case only - return ((((s = state) & ABITS) == 0L && - U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ? - next : acquireWrite(false, 0L)); -} -// 读锁 -public long readLock() { - long s = state, next; // bypass acquireRead on common uncontended case - return ((whead == wtail && (s & ABITS) < RFULL && - U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ? - next : acquireRead(false, 0L)); -} -// 乐观读 -public long tryOptimisticRead() { - long s; - return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L; -} ``` - -### StampedLock 的性能为什么更好? - -相比于传统读写锁多出来的乐观读是`StampedLock`比 `ReadWriteLock` 性能更好的关键原因。`StampedLock` 的乐观读允许一个写线程获取写锁,所以不会导致所有写线程阻塞,也就是当读多写少的时候,写线程有机会获取写锁,减少了线程饥饿的问题,吞吐量大大提高。 - -### StampedLock 适合什么场景? - -和 `ReentrantReadWriteLock` 一样,`StampedLock` 同样适合读多写少的业务场景,可以作为 `ReentrantReadWriteLock`的替代品,性能更好。 - -不过,需要注意的是`StampedLock`不可重入,不支持条件变量 `Condition`,对中断操作支持也不友好(使用不当容易导致 CPU 飙升)。如果你需要用到 `ReentrantLock` 的一些高级性能,就不太建议使用 `StampedLock` 了。 - -另外,`StampedLock` 性能虽好,但使用起来相对比较麻烦,一旦使用不当,就会出现生产问题。强烈建议你在使用`StampedLock` 之前,看看 [StampedLock 官方文档中的案例](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/StampedLock.html)。 - -### StampedLock 的底层原理了解吗? - -`StampedLock` 不是直接实现 `Lock`或 `ReadWriteLock`接口,而是基于 **CLH 锁** 实现的(AQS 也是基于这玩意),CLH 锁是对自旋锁的一种改良,是一种隐式的链表队列。`StampedLock` 通过 CLH 队列进行线程的管理,通过同步状态值 `state` 来表示锁的状态和类型。 - -`StampedLock` 的原理和 AQS 原理比较类似,这里就不详细介绍了,感兴趣的可以看看下面这两篇文章: - -- [AQS 详解](https://javaguide.cn/java/concurrent/aqs.html) -- [StampedLock 底层原理分析](https://segmentfault.com/a/1190000015808032) - -如果你只是准备面试的话,建议多花点精力搞懂 AQS 原理即可,`StampedLock` 底层原理在面试中遇到的概率非常小。 - -## Atomic 原子类 - -Atomic 原子类部分的内容我单独写了一篇文章来总结:[Atomic 原子类总结](./atomic-classes.md) 。 - -## 参考 - -- 《深入理解 Java 虚拟机》 -- 《实战 Java 高并发程序设计》 -- Guide to the Volatile Keyword in Java - Baeldung: -- 不可不说的 Java“锁”事 - 美团技术团队: -- 在 ReadWriteLock 类中读锁为什么不能升级为写锁?: -- 高性能解决线程饥饿的利器 StampedLock: -- 理解 Java 中的 ThreadLocal - 技术小黑屋: -- ThreadLocal (Java Platform SE 8 ) - Oracle Help Center: - - diff --git a/docs/java/concurrent/java-concurrent-questions-03.md b/docs/java/concurrent/java-concurrent-questions-03.md index 84d58459d09..64c9e70426a 100644 --- a/docs/java/concurrent/java-concurrent-questions-03.md +++ b/docs/java/concurrent/java-concurrent-questions-03.md @@ -1,30 +1,30 @@ --- -title: Java并发常见面试题总结(下) +title: Summary of Common Java Concurrency Interview Questions (Part 2) category: Java tag: - - Java并发 + - Java Concurrency head: - - - meta - - name: keywords - content: 多线程,死锁,线程池,CAS,AQS - - - meta - - name: description - content: Java并发常见知识点和面试题总结(含详细解答),希望对你有帮助! + - - meta + - name: keywords + content: multithreading, deadlock, thread pool, CAS, AQS + - - meta + - name: description + content: A summary of common knowledge points and interview questions in Java concurrency (with detailed answers), hope it helps you! --- ## ThreadLocal -### ThreadLocal 有什么用? +### What is the use of ThreadLocal? -通常情况下,我们创建的变量可以被任何一个线程访问和修改。这在多线程环境中可能导致数据竞争和线程安全问题。那么,**如果想让每个线程都有自己的专属本地变量,该如何实现呢?** +In general, the variables we create can be accessed and modified by any thread. This can lead to data races and thread safety issues in a multithreaded environment. So, **how can we ensure that each thread has its own dedicated local variable?** -JDK 中提供的 `ThreadLocal` 类正是为了解决这个问题。**`ThreadLocal` 类允许每个线程绑定自己的值**,可以将其形象地比喻为一个“存放数据的盒子”。每个线程都有自己独立的盒子,用于存储私有数据,确保不同线程之间的数据互不干扰。 +The `ThreadLocal` class provided in the JDK is designed to solve this problem. **The `ThreadLocal` class allows each thread to bind its own value**, which can be metaphorically described as a "box for storing data." Each thread has its own independent box for storing private data, ensuring that data does not interfere between different threads. -当你创建一个 `ThreadLocal` 变量时,每个访问该变量的线程都会拥有一个独立的副本。这也是 `ThreadLocal` 名称的由来。线程可以通过 `get()` 方法获取自己线程的本地副本,或通过 `set()` 方法修改该副本的值,从而避免了线程安全问题。 +When you create a `ThreadLocal` variable, each thread accessing that variable will have its own independent copy. This is also the origin of the name `ThreadLocal`. Threads can retrieve their local copy using the `get()` method or modify the value of that copy using the `set()` method, thus avoiding thread safety issues. -举个简单的例子:假设有两个人去宝屋收集宝物。如果他们共用一个袋子,必然会产生争执;但如果每个人都有一个独立的袋子,就不会有这个问题。如果将这两个人比作线程,那么 `ThreadLocal` 就是用来避免这两个线程竞争同一个资源的方法。 +For example, suppose there are two people collecting treasures from a treasure house. If they share a bag, disputes are inevitable; but if each person has their own independent bag, this problem will not occur. If we compare these two people to threads, then `ThreadLocal` is the method used to avoid competition for the same resource between these two threads. ```java public class ThreadLocalExample { @@ -41,40 +41,40 @@ public class ThreadLocalExample { Thread thread1 = new Thread(task, "Thread-1"); Thread thread2 = new Thread(task, "Thread-2"); - thread1.start(); // 输出: Thread-1 Value: 1 - thread2.start(); // 输出: Thread-2 Value: 1 + thread1.start(); // Output: Thread-1 Value: 1 + thread2.start(); // Output: Thread-2 Value: 1 } } ``` -### ⭐️ThreadLocal 原理了解吗? +### ⭐️ Do you understand the principles of ThreadLocal? -从 `Thread`类源代码入手。 +Starting from the source code of the `Thread` class. ```java public class Thread implements Runnable { //...... - //与此线程有关的ThreadLocal值。由ThreadLocal类维护 + // ThreadLocal values related to this thread. Maintained by the ThreadLocal class ThreadLocal.ThreadLocalMap threadLocals = null; - //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 + // InheritableThreadLocal values related to this thread. Maintained by the InheritableThreadLocal class ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; //...... } ``` -从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是 null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set()`方法。 +From the above source code of the `Thread` class, we can see that there is a `threadLocals` and an `inheritableThreadLocals` variable, both of which are of type `ThreadLocalMap`. We can understand `ThreadLocalMap` as a customized `HashMap` implemented by the `ThreadLocal` class. By default, both variables are null, and they are only created when the current thread calls the `set` or `get` methods of the `ThreadLocal` class. In fact, when we call these two methods, we are calling the corresponding `get()` and `set()` methods of the `ThreadLocalMap` class. -`ThreadLocal`类的`set()`方法 +The `set()` method of the `ThreadLocal` class ```java public void set(T value) { - //获取当前请求的线程 + // Get the current requesting thread Thread t = Thread.currentThread(); - //取出 Thread 类内部的 threadLocals 变量(哈希表结构) + // Retrieve the threadLocals variable (hash table structure) inside the Thread class ThreadLocalMap map = getMap(t); if (map != null) - // 将需要存储的值放入到这个哈希表中 + // Store the value to be saved in this hash table map.set(this, value); else createMap(t, value); @@ -84,1284 +84,6 @@ ThreadLocalMap getMap(Thread t) { } ``` -通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。 +From the above content, we can conclude: **The final variable is stored in the current thread's `ThreadLocalMap`, not on the `ThreadLocal`. `ThreadLocal` can be understood as just a wrapper for `ThreadLocalMap`, passing the variable value.** The `ThreadLocal` class can access the current thread's `ThreadLocalMap` object directly through `Thread.currentThread()` and then call `getMap(Thread t)`. -**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为 key ,Object 对象为 value 的键值对。** - -```java -ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { - //...... -} -``` - -比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话, `Thread`内部都是使用仅有的那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。 - -`ThreadLocal` 数据结构如下图所示: - -![ThreadLocal 数据结构](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadlocal-data-structure.png) - -`ThreadLocalMap`是`ThreadLocal`的静态内部类。 - -![ThreadLocal内部类](https://oss.javaguide.cn/github/javaguide/java/concurrent/thread-local-inner-class.png) - -### ⭐️ThreadLocal 内存泄露问题是怎么导致的? - -`ThreadLocal` 内存泄漏的根本原因在于其内部实现机制。 - -通过上面的内容我们已经知道:每个线程维护一个名为 `ThreadLocalMap` 的 map。 当你使用 `ThreadLocal` 存储值时,实际上是将值存储在当前线程的 `ThreadLocalMap` 中,其中 `ThreadLocal` 实例本身作为 key,而你要存储的值作为 value。 - -`ThreadLocal` 的 `set()` 方法源码如下: - -```java -public void set(T value) { - Thread t = Thread.currentThread(); // 获取当前线程 - ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap - if (map != null) { - map.set(this, value); // 设置值 - } else { - createMap(t, value); // 创建新的 ThreadLocalMap - } -} -``` - -`ThreadLocalMap` 的 `set()` 和 `createMap()` 方法中,并没有直接存储 `ThreadLocal` 对象本身,而是使用 `ThreadLocal` 的哈希值计算数组索引,最终存储于类型为`static class Entry extends WeakReference>`的数组中。 - -```java -int i = key.threadLocalHashCode & (len-1); -``` - -`ThreadLocalMap` 的 `Entry` 定义如下: - -```java -static class Entry extends WeakReference> { - Object value; - - Entry(ThreadLocal k, Object v) { - super(k); - value = v; - } -} -``` - -`ThreadLocalMap` 的 `key` 和 `value` 引用机制: - -- **key 是弱引用**:`ThreadLocalMap` 中的 key 是 `ThreadLocal` 的弱引用 (`WeakReference>`)。 这意味着,如果 `ThreadLocal` 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 `ThreadLocalMap` 中对应的 key 变为 `null`。 -- **value 是强引用**:即使 `key` 被 GC 回收,`value` 仍然被 `ThreadLocalMap.Entry` 强引用存在,无法被 GC 回收。 - -当 `ThreadLocal` 实例失去强引用后,其对应的 value 仍然存在于 `ThreadLocalMap` 中,因为 `Entry` 对象强引用了它。如果线程持续存活(例如线程池中的线程),`ThreadLocalMap` 也会一直存在,导致 key 为 `null` 的 entry 无法被垃圾回收,即会造成内存泄漏。 - -也就是说,内存泄漏的发生需要同时满足两个条件: - -1. `ThreadLocal` 实例不再被强引用; -2. 线程持续存活,导致 `ThreadLocalMap` 长期存在。 - -虽然 `ThreadLocalMap` 在 `get()`, `set()` 和 `remove()` 操作时会尝试清理 key 为 null 的 entry,但这种清理机制是被动的,并不完全可靠。 - -**如何避免内存泄漏的发生?** - -1. 在使用完 `ThreadLocal` 后,务必调用 `remove()` 方法。 这是最安全和最推荐的做法。 `remove()` 方法会从 `ThreadLocalMap` 中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将 `ThreadLocal` 定义为 `static final`,也强烈建议在每次使用后调用 `remove()`。 -2. 在线程池等线程复用的场景下,使用 `try-finally` 块可以确保即使发生异常,`remove()` 方法也一定会被执行。 - -### ⭐️如何跨线程传递 ThreadLocal 的值? - -由于 `ThreadLocal` 的变量值存放在 `Thread` 里,而父子线程属于不同的 `Thread` 的。因此在异步场景下,父子线程的 `ThreadLocal` 值无法进行传递。 - -如果想要在异步场景下传递 `ThreadLocal` 值,有两种解决方案: - -- `InheritableThreadLocal` :`InheritableThreadLocal` 是 JDK1.2 提供的工具,继承自 `ThreadLocal` 。使用 `InheritableThreadLocal` 时,会在创建子线程时,令子线程继承父线程中的 `ThreadLocal` 值,但是无法支持线程池场景下的 `ThreadLocal` 值传递。 -- `TransmittableThreadLocal` : `TransmittableThreadLocal` (简称 TTL) 是阿里巴巴开源的工具类,继承并加强了`InheritableThreadLocal`类,可以在线程池的场景下支持 `ThreadLocal` 值传递。项目地址:。 - -#### InheritableThreadLocal 原理 - -`InheritableThreadLocal` 实现了创建异步线程时,继承父线程 `ThreadLocal` 值的功能。该类是 JDK 团队提供的,通过改造 JDK 源码包中的 `Thread` 类来实现创建线程时,`ThreadLocal` 值的传递。 - -**`InheritableThreadLocal` 的值存储在哪里?** - -在 `Thread` 类中添加了一个新的 `ThreadLocalMap` ,命名为 `inheritableThreadLocals` ,该变量用于存储需要跨线程传递的 `ThreadLocal` 值。如下: - -```JAVA -class Thread implements Runnable { - ThreadLocal.ThreadLocalMap threadLocals = null; - ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; -} -``` - -**如何完成 `ThreadLocal` 值的传递?** - -通过改造 `Thread` 类的构造方法来实现,在创建 `Thread` 线程时,拿到父线程的 `inheritableThreadLocals` 变量赋值给子线程即可。相关代码如下: - -```JAVA -// Thread 的构造方法会调用 init() 方法 -private void init(/* ... */) { - // 1、获取父线程 - Thread parent = currentThread(); - // 2、将父线程的 inheritableThreadLocals 赋值给子线程 - if (inheritThreadLocals && parent.inheritableThreadLocals != null) - this.inheritableThreadLocals = - ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); -} -``` - -#### TransmittableThreadLocal 原理 - -JDK 默认没有支持线程池场景下 `ThreadLocal` 值传递的功能,因此阿里巴巴开源了一套工具 `TransmittableThreadLocal` 来实现该功能。 - -阿里巴巴无法改动 JDK 的源码,因此他内部通过 **装饰器模式** 在原有的功能上做增强,以此来实现线程池场景下的 `ThreadLocal` 值传递。 - -TTL 改造的地方有两处: - -- 实现自定义的 `Thread` ,在 `run()` 方法内部做 `ThreadLocal` 变量的赋值操作。 - -- 基于 **线程池** 进行装饰,在 `execute()` 方法中,不提交 JDK 内部的 `Thread` ,而是提交自定义的 `Thread` 。 - -如果想要查看相关源码,可以引入 Maven 依赖进行下载。 - -```XML - - com.alibaba - transmittable-thread-local - 2.12.0 - -``` - -#### 应用场景 - -1. **压测流量标记**: 在压测场景中,使用 `ThreadLocal` 存储压测标记,用于区分压测流量和真实流量。如果标记丢失,可能导致压测流量被错误地当成线上流量处理。 -2. **上下文传递**:在分布式系统中,传递链路追踪信息(如 Trace ID)或用户上下文信息。 - -## 线程池 - -### 什么是线程池? - -顾名思义,线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。 - -### ⭐️为什么要用线程池? - -池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。 - -线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。使用线程池主要带来以下几个好处: - -1. **降低资源消耗**:线程池里的线程是可以重复利用的。一旦线程完成了某个任务,它不会立即销毁,而是回到池子里等待下一个任务。这就避免了频繁创建和销毁线程带来的开销。 -2. **提高响应速度**:因为线程池里通常会维护一定数量的核心线程(或者说“常驻工人”),任务来了之后,可以直接交给这些已经存在的、空闲的线程去执行,省去了创建线程的时间,任务能够更快地得到处理。 -3. **提高线程的可管理性**:线程池允许我们统一管理池中的线程。我们可以配置线程池的大小(核心线程数、最大线程数)、任务队列的类型和大小、拒绝策略等。这样就能控制并发线程的总量,防止资源耗尽,保证系统的稳定性。同时,线程池通常也提供了监控接口,方便我们了解线程池的运行状态(比如有多少活跃线程、多少任务在排队等),便于调优。 - -### 如何创建线程池? - -在 Java 中,创建线程池主要有两种方式: - -**方式一:通过 `ThreadPoolExecutor` 构造函数直接创建 (推荐)** - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpoolexecutor-construtors.png) - -这是最推荐的方式,因为它允许开发者明确指定线程池的核心参数,对线程池的运行行为有更精细的控制,从而避免资源耗尽的风险。 - -**方式二:通过 `Executors` 工具类创建 (不推荐用于生产环境)** - -`Executors`工具类提供的创建线程池的方法如下图所示: - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/executors-new-thread-pool-methods.png) - -可以看出,通过`Executors`工具类可以创建多种类型的线程池,包括: - -- `FixedThreadPool`:固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 -- `SingleThreadExecutor`: 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 -- `CachedThreadPool`: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 -- `ScheduledThreadPool`:给定的延迟后运行任务或者定期执行任务的线程池。 - -### ⭐️为什么不推荐使用内置线程池? - -在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 - -**为什么呢?** - -> 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 - -另外,《阿里巴巴 Java 开发手册》中强制线程池不允许使用 `Executors` 去创建,而是通过 `ThreadPoolExecutor` 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 - -`Executors` 返回线程池对象的弊端如下(后文会详细介绍到): - -- `FixedThreadPool` 和 `SingleThreadExecutor`:使用的是阻塞队列 `LinkedBlockingQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可以看作是无界的,可能堆积大量的请求,从而导致 OOM。 -- `CachedThreadPool`:使用的是同步队列 `SynchronousQueue`, 允许创建的线程数量为 `Integer.MAX_VALUE` ,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM。 -- `ScheduledThreadPool` 和 `SingleThreadScheduledExecutor`:使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。 - -```java -public static ExecutorService newFixedThreadPool(int nThreads) { - // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的 - return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue()); - -} - -public static ExecutorService newSingleThreadExecutor() { - // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的 - return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue())); - -} - -// 同步队列 SynchronousQueue,没有容量,最大线程数是 Integer.MAX_VALUE` -public static ExecutorService newCachedThreadPool() { - - return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue()); - -} - -// DelayedWorkQueue(延迟阻塞队列) -public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { - return new ScheduledThreadPoolExecutor(corePoolSize); -} -public ScheduledThreadPoolExecutor(int corePoolSize) { - super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, - new DelayedWorkQueue()); -} -``` - -### ⭐️线程池常见参数有哪些?如何解释? - -```java - /** - * 用给定的初始参数创建一个新的ThreadPoolExecutor。 - */ - public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量 - int maximumPoolSize,//线程池的最大线程数 - long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间 - TimeUnit unit,//时间单位 - BlockingQueue workQueue,//任务队列,用来储存等待执行任务的队列 - ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可 - RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务 - ) { - if (corePoolSize < 0 || - maximumPoolSize <= 0 || - maximumPoolSize < corePoolSize || - keepAliveTime < 0) - throw new IllegalArgumentException(); - if (workQueue == null || threadFactory == null || handler == null) - throw new NullPointerException(); - this.corePoolSize = corePoolSize; - this.maximumPoolSize = maximumPoolSize; - this.workQueue = workQueue; - this.keepAliveTime = unit.toNanos(keepAliveTime); - this.threadFactory = threadFactory; - this.handler = handler; - } -``` - -`ThreadPoolExecutor` 3 个最重要的参数: - -- `corePoolSize` : 任务队列未达到队列容量时,最大可以同时运行的线程数量。 -- `maximumPoolSize` : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- `workQueue`: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 - -`ThreadPoolExecutor`其他常见参数 : - -- `keepAliveTime`:当线程池中的线程数量大于 `corePoolSize` ,即有非核心线程(线程池中核心线程以外的线程)时,这些非核心线程空闲后不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁。 -- `unit` : `keepAliveTime` 参数的时间单位。 -- `threadFactory` :executor 创建新线程的时候会用到。 -- `handler` :拒绝策略(后面会单独详细介绍一下)。 - -下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》): - -![线程池各个参数的关系](https://oss.javaguide.cn/github/javaguide/java/concurrent/relationship-between-thread-pool-parameters.png) - -### 线程池的核心线程会被回收吗? - -`ThreadPoolExecutor` 默认不会回收核心线程,即使它们已经空闲了。这是为了减少创建线程的开销,因为核心线程通常是要长期保持活跃的。但是,如果线程池是被用于周期性使用的场景,且频率不高(周期之间有明显的空闲时间),可以考虑将 `allowCoreThreadTimeOut(boolean value)` 方法的参数设置为 `true`,这样就会回收空闲(时间间隔由 `keepAliveTime` 指定)的核心线程了。 - -```java -public void allowCoreThreadTimeOut(boolean value) { - // 核心线程的 keepAliveTime 必须大于 0 才能启用超时机制 - if (value && keepAliveTime <= 0) { - throw new IllegalArgumentException("Core threads must have nonzero keep alive times"); - } - // 设置 allowCoreThreadTimeOut 的值 - if (value != allowCoreThreadTimeOut) { - allowCoreThreadTimeOut = value; - // 如果启用了超时机制,清理所有空闲的线程,包括核心线程 - if (value) { - interruptIdleWorkers(); - } - } -} -``` - -### 核心线程空闲时处于什么状态? - -核心线程空闲时,其状态分为以下两种情况: - -- **设置了核心线程的存活时间** :核心线程在空闲时,会处于 `WAITING` 状态,等待获取任务。如果阻塞等待的时间超过了核心线程存活时间,则该线程会退出工作,将该线程从线程池的工作线程集合中移除,线程状态变为 `TERMINATED` 状态。 -- **没有设置核心线程的存活时间** :核心线程在空闲时,会一直处于 `WAITING` 状态,等待获取任务,核心线程会一直存活在线程池中。 - -当队列中有可用任务时,会唤醒被阻塞的线程,线程的状态会由 `WAITING` 状态变为 `RUNNABLE` 状态,之后去执行对应任务。 - -接下来通过相关源码,了解一下线程池内部是如何做的。 - -线程在线程池内部被抽象为了 `Worker` ,当 `Worker` 被启动之后,会不断去任务队列中获取任务。 - -在获取任务的时候,会根据 `timed` 值来决定从任务队列( `BlockingQueue` )获取任务的行为。 - -如果「设置了核心线程的存活时间」或者「线程数量超过了核心线程数量」,则将 `timed` 标记为 `true` ,表明获取任务时需要使用 `poll()` 指定超时时间。 - -- `timed == true` :使用 `poll()` 来获取任务。使用 `poll()` 方法获取任务超时的话,则当前线程会退出执行( `TERMINATED` ),该线程从线程池中被移除。 -- `timed == false` :使用 `take()` 来获取任务。使用 `take()` 方法获取任务会让当前线程一直阻塞等待(`WAITING`)。 - -源码如下: - -```JAVA -// ThreadPoolExecutor -private Runnable getTask() { - boolean timedOut = false; - for (;;) { - // ... - - // 1、如果「设置了核心线程的存活时间」或者是「线程数量超过了核心线程数量」,则 timed 为 true。 - boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; - // 2、扣减线程数量。 - // wc > maximuimPoolSize:线程池中的线程数量超过最大线程数量。其中 wc 为线程池中的线程数量。 - // timed && timeOut:timeOut 表示获取任务超时。 - // 分为两种情况:核心线程设置了存活时间 && 获取任务超时,则扣减线程数量;线程数量超过了核心线程数量 && 获取任务超时,则扣减线程数量。 - if ((wc > maximumPoolSize || (timed && timedOut)) - && (wc > 1 || workQueue.isEmpty())) { - if (compareAndDecrementWorkerCount(c)) - return null; - continue; - } - try { - // 3、如果 timed 为 true,则使用 poll() 获取任务;否则,使用 take() 获取任务。 - Runnable r = timed ? - workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : - workQueue.take(); - // 4、获取任务之后返回。 - if (r != null) - return r; - timedOut = true; - } catch (InterruptedException retry) { - timedOut = false; - } - } -} -``` - -### ⭐️线程池的拒绝策略有哪些? - -如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,`ThreadPoolExecutor` 定义一些策略: - -- `ThreadPoolExecutor.AbortPolicy`:抛出 `RejectedExecutionException`来拒绝新任务的处理。 -- `ThreadPoolExecutor.CallerRunsPolicy`:调用执行者自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 -- `ThreadPoolExecutor.DiscardPolicy`:不处理新任务,直接丢弃掉。 -- `ThreadPoolExecutor.DiscardOldestPolicy`:此策略将丢弃最早的未处理的任务请求。 - -举个例子:Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 拒绝策略来配置线程池的时候,默认使用的是 `AbortPolicy`。在这种拒绝策略下,如果队列满了,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 异常来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。如果不想丢弃任务的话,可以使用`CallerRunsPolicy`。`CallerRunsPolicy` 和其他的几个策略不同,它既不会抛弃任务,也不会抛出异常,而是将任务回退给调用者,使用调用者的线程来执行任务。 - -```java -public static class CallerRunsPolicy implements RejectedExecutionHandler { - - public CallerRunsPolicy() { } - - public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { - if (!e.isShutdown()) { - // 直接主线程执行,而不是线程池中的线程执行 - r.run(); - } - } - } -``` - -### 如果不允许丢弃任务,应该选择哪个拒绝策略? - -根据上面对线程池拒绝策略的介绍,相信大家很容易能够得出答案是:`CallerRunsPolicy` 。 - -这里我们再来结合`CallerRunsPolicy` 的源码来看看: - -```java -public static class CallerRunsPolicy implements RejectedExecutionHandler { - - public CallerRunsPolicy() { } - - - public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { - //只要当前程序没有关闭,就用执行execute方法的线程执行该任务 - if (!e.isShutdown()) { - - r.run(); - } - } - } -``` - -从源码可以看出,只要当前程序不关闭就会使用执行`execute`方法的线程执行该任务。 - -### CallerRunsPolicy 拒绝策略有什么风险?如何解决? - -我们上面也提到了:如果想要保证任何一个任务请求都要被执行的话,那选择 `CallerRunsPolicy` 拒绝策略更合适一些。 - -不过,如果走到`CallerRunsPolicy`的任务是个非常耗时的任务,且处理提交任务的线程是主线程,可能会导致主线程阻塞,影响程序的正常运行。 - -这里简单举一个例子,该线程池限定了最大线程数为 2,阻塞队列大小为 1(这意味着第 4 个任务就会走到拒绝策略),`ThreadUtil`为 Hutool 提供的工具类: - -```java -public class ThreadPoolTest { - - private static final Logger log = LoggerFactory.getLogger(ThreadPoolTest.class); - - public static void main(String[] args) { - // 创建一个线程池,核心线程数为1,最大线程数为2 - // 当线程数大于核心线程数时,多余的空闲线程存活的最长时间为60秒, - // 任务队列为容量为1的ArrayBlockingQueue,饱和策略为CallerRunsPolicy。 - ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, - 2, - 60, - TimeUnit.SECONDS, - new ArrayBlockingQueue<>(1), - new ThreadPoolExecutor.CallerRunsPolicy()); - - // 提交第一个任务,由核心线程执行 - threadPoolExecutor.execute(() -> { - log.info("核心线程执行第一个任务"); - ThreadUtil.sleep(1, TimeUnit.MINUTES); - }); - - // 提交第二个任务,由于核心线程被占用,任务将进入队列等待 - threadPoolExecutor.execute(() -> { - log.info("非核心线程处理入队的第二个任务"); - ThreadUtil.sleep(1, TimeUnit.MINUTES); - }); - - // 提交第三个任务,由于核心线程被占用且队列已满,创建非核心线程处理 - threadPoolExecutor.execute(() -> { - log.info("非核心线程处理第三个任务"); - ThreadUtil.sleep(1, TimeUnit.MINUTES); - }); - - // 提交第四个任务,由于核心线程和非核心线程都被占用,队列也满了,根据CallerRunsPolicy策略,任务将由提交任务的线程(即主线程)来执行 - threadPoolExecutor.execute(() -> { - log.info("主线程处理第四个任务"); - ThreadUtil.sleep(2, TimeUnit.MINUTES); - }); - - // 提交第五个任务,主线程被第四个任务卡住,该任务必须等到主线程执行完才能提交 - threadPoolExecutor.execute(() -> { - log.info("核心线程执行第五个任务"); - }); - - // 关闭线程池 - threadPoolExecutor.shutdown(); - } -} - -``` - -输出: - -```bash -18:19:48.203 INFO [pool-1-thread-1] c.j.concurrent.ThreadPoolTest - 核心线程执行第一个任务 -18:19:48.203 INFO [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 非核心线程处理第三个任务 -18:19:48.203 INFO [main] c.j.concurrent.ThreadPoolTest - 主线程处理第四个任务 -18:20:48.212 INFO [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 非核心线程处理入队的第二个任务 -18:21:48.219 INFO [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 核心线程执行第五个任务 -``` - -从输出结果可以看出,因为`CallerRunsPolicy`这个拒绝策略,导致耗时的任务用了主线程执行,导致线程池阻塞,进而导致后续任务无法及时执行,严重的情况下很可能导致 OOM。 - -我们从问题的本质入手,调用者采用`CallerRunsPolicy`是希望所有的任务都能够被执行,暂时无法处理的任务又被保存在阻塞队列`BlockingQueue`中。这样的话,在内存允许的情况下,我们可以增加阻塞队列`BlockingQueue`的大小并调整堆内存以容纳更多的任务,确保任务能够被准确执行。 - -为了充分利用 CPU,我们还可以调整线程池的`maximumPoolSize` (最大线程数)参数,这样可以提高任务处理速度,避免累计在 `BlockingQueue`的任务过多导致内存用完。 - -![调整阻塞队列大小和最大线程数](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpool-reject-2-threadpool-reject-01.png) - -如果服务器资源以达到可利用的极限,这就意味我们要在设计策略上改变线程池的调度了,我们都知道,导致主线程卡死的本质就是因为我们不希望任何一个任务被丢弃。换个思路,有没有办法既能保证任务不被丢弃且在服务器有余力时及时处理呢? - -这里提供的一种**任务持久化**的思路,这里所谓的任务持久化,包括但不限于: - -1. 设计一张任务表将任务存储到 MySQL 数据库中。 -2. Redis 缓存任务。 -3. 将任务提交到消息队列中。 - -这里以方案一为例,简单介绍一下实现逻辑: - -1. 实现`RejectedExecutionHandler`接口自定义拒绝策略,自定义拒绝策略负责将线程池暂时无法处理(此时阻塞队列已满)的任务入库(保存到 MySQL 中)。注意:线程池暂时无法处理的任务会先被放在阻塞队列中,阻塞队列满了才会触发拒绝策略。 -2. 继承`BlockingQueue`实现一个混合式阻塞队列,该队列包含 JDK 自带的`ArrayBlockingQueue`。另外,该混合式阻塞队列需要修改取任务处理的逻辑,也就是重写`take()`方法,取任务时优先从数据库中读取最早的任务,数据库中无任务时再从 `ArrayBlockingQueue`中去取任务。 - -![将一部分任务保存到MySQL中](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpool-reject-2-threadpool-reject-02.png) - -整个实现逻辑还是比较简单的,核心在于自定义拒绝策略和阻塞队列。如此一来,一旦我们的线程池中线程达到满载时,我们就可以通过拒绝策略将最新任务持久化到 MySQL 数据库中,等到线程池有了有余力处理所有任务时,让其优先处理数据库中的任务以避免"饥饿"问题。 - -当然,对于这个问题,我们也可以参考其他主流框架的做法,以 Netty 为例,它的拒绝策略则是直接创建一个线程池以外的线程处理这些任务,为了保证任务的实时处理,这种做法可能需要良好的硬件设备且临时创建的线程无法做到准确的监控: - -```java -private static final class NewThreadRunsPolicy implements RejectedExecutionHandler { - NewThreadRunsPolicy() { - super(); - } - public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { - try { - //创建一个临时线程处理任务 - final Thread t = new Thread(r, "Temporary task executor"); - t.start(); - } catch (Throwable e) { - throw new RejectedExecutionException( - "Failed to start a new thread", e); - } - } -} -``` - -ActiveMQ 则是尝试在指定的时效内尽可能的争取将任务入队,以保证最大交付: - -```java -new RejectedExecutionHandler() { - @Override - public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) { - try { - //限时阻塞等待,实现尽可能交付 - executor.getQueue().offer(r, 60, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker"); - } - throw new RejectedExecutionException("Timed Out while attempting to enqueue Task."); - } - }); -``` - -### 线程池常用的阻塞队列有哪些? - -新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 - -不同的线程池会选用不同的阻塞队列,我们可以结合内置线程池来分析。 - -- 容量为 `Integer.MAX_VALUE` 的 `LinkedBlockingQueue`(有界阻塞队列):`FixedThreadPool` 和 `SingleThreadExecutor` 。`FixedThreadPool`最多只能创建核心线程数的线程(核心线程数和最大线程数相等),`SingleThreadExecutor`只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满。 -- `SynchronousQueue`(同步队列):`CachedThreadPool` 。`SynchronousQueue` 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,`CachedThreadPool` 的最大线程数是 `Integer.MAX_VALUE` ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。 -- `DelayedWorkQueue`(延迟队列):`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor` 。`DelayedWorkQueue` 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。`DelayedWorkQueue` 添加元素满了之后会自动扩容,增加原来容量的 50%,即永远不会阻塞,最大扩容可达 `Integer.MAX_VALUE`,所以最多只能创建核心线程数的线程。 -- `ArrayBlockingQueue`(有界阻塞队列):底层由数组实现,容量一旦创建,就不能修改。 - -### ⭐️线程池处理任务的流程了解吗? - -![图解线程池实现原理](https://oss.javaguide.cn/github/javaguide/java/concurrent/thread-pool-principle.png) - -1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。 -2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。 -3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。 -4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用`RejectedExecutionHandler.rejectedExecution()`方法。 - -再提一个有意思的小问题:**线程池在提交任务前,可以提前创建线程吗?** - -答案是可以的!`ThreadPoolExecutor` 提供了两个方法帮助我们在提交任务之前,完成核心线程的创建,从而实现线程池预热的效果: - -- `prestartCoreThread()`:启动一个线程,等待任务,如果已达到核心线程数,这个方法返回 false,否则返回 true; -- `prestartAllCoreThreads()`:启动所有的核心线程,并返回启动成功的核心线程数。 - -### ⭐️线程池中线程异常后,销毁还是复用? - -直接说结论,需要分两种情况: - -- **使用`execute()`提交任务**:当任务通过`execute()`提交到线程池并在执行过程中抛出异常时,如果这个异常没有在任务内被捕获,那么该异常会导致当前线程终止,并且异常会被打印到控制台或日志文件中。线程池会检测到这种线程终止,并创建一个新线程来替换它,从而保持配置的线程数不变。 -- **使用`submit()`提交任务**:对于通过`submit()`提交的任务,如果在任务执行中发生异常,这个异常不会直接打印出来。相反,异常会被封装在由`submit()`返回的`Future`对象中。当调用`Future.get()`方法时,可以捕获到一个`ExecutionException`。在这种情况下,线程不会因为异常而终止,它会继续存在于线程池中,准备执行后续的任务。 - -简单来说:使用`execute()`时,未捕获异常导致线程终止,线程池创建新线程替代;使用`submit()`时,异常被封装在`Future`中,线程继续复用。 - -这种设计允许`submit()`提供更灵活的错误处理机制,因为它允许调用者决定如何处理异常,而`execute()`则适用于那些不需要关注执行结果的场景。 - -具体的源码分析可以参考这篇:[线程池中线程异常后:销毁还是复用? - 京东技术](https://mp.weixin.qq.com/s/9ODjdUU-EwQFF5PrnzOGfw)。 - -### ⭐️如何给线程池命名? - -初始化线程池的时候需要显示命名(设置线程池名称前缀),有利于定位问题。 - -默认情况下创建的线程名字类似 `pool-1-thread-n` 这样的,没有业务含义,不利于我们定位问题。 - -给线程池里的线程命名通常有下面两种方式: - -**1、利用 guava 的 `ThreadFactoryBuilder`** - -```java -ThreadFactory threadFactory = new ThreadFactoryBuilder() - .setNameFormat(threadNamePrefix + "-%d") - .setDaemon(true).build(); -ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory); -``` - -**2、自己实现 `ThreadFactory`。** - -```java -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * 线程工厂,它设置线程名称,有利于我们定位问题。 - */ -public final class NamingThreadFactory implements ThreadFactory { - - private final AtomicInteger threadNum = new AtomicInteger(); - private final String name; - - /** - * 创建一个带名字的线程池生产工厂 - */ - public NamingThreadFactory(String name) { - this.name = name; - } - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setName(name + " [#" + threadNum.incrementAndGet() + "]"); - return t; - } -} -``` - -### 如何设定线程池的大小? - -很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说:**并不是人多就能把事情做好,增加了沟通交流成本。你本来一件事情只需要 3 个人做,你硬是拉来了 6 个人,会提升做事效率嘛?我想并不会。** 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了**上下文切换**成本。不清楚什么是上下文切换的话,可以看我下面的介绍。 - -> 上下文切换: -> -> 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 -> -> 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 -> -> Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 - -类比于现实世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。 - -- 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的,CPU 根本没有得到充分利用。 -- 如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。 - -有一个简单并且适用面比较广的公式: - -- **CPU 密集型任务(N+1):** 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 -- **I/O 密集型任务(2N):** 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。 - -**如何判断是 CPU 密集任务还是 IO 密集任务?** - -CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。 - -> 🌈 拓展一下(参见:[issue#1737](https://github.com/Snailclimb/JavaGuide/issues/1737)): -> -> 线程数更严谨的计算的方法应该是:`最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间))`,其中 `WT(线程等待时间)=线程运行总时间 - ST(线程计算时间)`。 -> -> 线程等待时间所占比例越高,需要越多线程。线程计算时间所占比例越高,需要越少线程。 -> -> 我们可以通过 JDK 自带的工具 VisualVM 来查看 `WT/ST` 比例。 -> -> CPU 密集型任务的 `WT/ST` 接近或者等于 0,因此, 线程数可以设置为 N(CPU 核心数)∗(1+0)= N,和我们上面说的 N(CPU 核心数)+1 差不多。 -> -> IO 密集型任务下,几乎全是线程等待时间,从理论上来说,你就可以将线程数设置为 2N(按道理来说,WT/ST 的结果应该比较大,这里选择 2N 的原因应该是为了避免创建过多线程吧)。 - -公式也只是参考,具体还是要根据项目实际线上运行情况来动态调整。我在后面介绍的美团的线程池参数动态配置这种方案就非常不错,很实用! - -### ⭐️如何动态修改线程池的参数? - -美团技术团队在[《Java 线程池实现原理及其在美团业务中的实践》](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)这篇文章中介绍到对线程池参数实现可自定义配置的思路和方法。 - -美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是: - -- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 -- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 - -**为什么是这三个参数?** - -我在[Java 线程池详解](https://javaguide.cn/java/concurrent/java-thread-pool-summary.html) 这篇文章中就说过这三个参数是 `ThreadPoolExecutor` 最重要的参数,它们基本决定了线程池对于任务的处理策略。 - -**如何支持参数动态配置?** 且看 `ThreadPoolExecutor` 提供的下面这些方法。 - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpoolexecutor-methods.png) - -格外需要注意的是`corePoolSize`, 程序运行期间的时候,我们调用 `setCorePoolSize()`这个方法的话,线程池会首先判断当前工作线程数是否大于`corePoolSize`,如果大于的话就会回收工作线程。 - -另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 `ResizableCapacityLinkedBlockIngQueue` 的队列(主要就是把`LinkedBlockingQueue`的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。 - -最终实现的可动态修改线程池参数效果如下。👏👏👏 - -![动态配置线程池参数最终效果](https://oss.javaguide.cn/github/javaguide/java/concurrent/meituan-dynamically-configuring-thread-pool-parameters.png) - -还没看够?我在[《后端面试高频系统设计&场景题》](https://javaguide.cn/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.html#%E4%BB%8B%E7%BB%8D)中详细介绍了如何设计一个动态线程池,这也是面试中常问的一道系统设计题。 - -![《后端面试高频系统设计&场景题》](https://oss.javaguide.cn/xingqiu/back-end-interview-high-frequency-system-design-and-scenario-questions-fengmian.png) - -如果我们的项目也想要实现这种效果的话,可以借助现成的开源项目: - -- **[Hippo4j](https://github.com/opengoofy/hippo4j)**:异步线程池框架,支持线程池动态变更&监控&报警,无需修改代码轻松引入。支持多种使用模式,轻松引入,致力于提高系统运行保障能力。 -- **[Dynamic TP](https://github.com/dromara/dynamic-tp)**:轻量级动态线程池,内置监控告警功能,集成三方中间件线程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通过 SPI 自定义实现)。 - -### ⭐️如何设计一个能够根据任务的优先级来执行的线程池? - -这是一个常见的面试问题,本质其实还是在考察求职者对于线程池以及阻塞队列的掌握。 - -我们上面也提到了,不同的线程池会选用不同的阻塞队列作为任务队列,比如`FixedThreadPool` 使用的是`LinkedBlockingQueue`(有界队列),默认构造器初始的队列长度为 `Integer.MAX_VALUE` ,由于队列永远不会被放满,因此`FixedThreadPool`最多只能创建核心线程数的线程。 - -假如我们需要实现一个优先级任务线程池的话,那可以考虑使用 `PriorityBlockingQueue` (优先级阻塞队列)作为任务队列(`ThreadPoolExecutor` 的构造函数有一个 `workQueue` 参数可以传入任务队列)。 - -![ThreadPoolExecutor构造函数](https://oss.javaguide.cn/github/javaguide/java/concurrent/common-parameters-of-threadpool-workqueue.jpg) - -`PriorityBlockingQueue` 是一个支持优先级的无界阻塞队列,可以看作是线程安全的 `PriorityQueue`,两者底层都是使用小顶堆形式的二叉堆,即值最小的元素优先出队。不过,`PriorityQueue` 不支持阻塞操作。 - -要想让 `PriorityBlockingQueue` 实现对任务的排序,传入其中的任务必须是具备排序能力的,方式有两种: - -1. 提交到线程池的任务实现 `Comparable` 接口,并重写 `compareTo` 方法来指定任务之间的优先级比较规则。 -2. 创建 `PriorityBlockingQueue` 时传入一个 `Comparator` 对象来指定任务之间的排序规则(推荐)。 - -不过,这存在一些风险和问题,比如: - -- `PriorityBlockingQueue` 是无界的,可能堆积大量的请求,从而导致 OOM。 -- 可能会导致饥饿问题,即低优先级的任务长时间得不到执行。 -- 由于需要对队列中的元素进行排序操作以及保证线程安全(并发控制采用的是可重入锁 `ReentrantLock`),因此会降低性能。 - -对于 OOM 这个问题的解决比较简单粗暴,就是继承`PriorityBlockingQueue` 并重写一下 `offer` 方法(入队)的逻辑,当插入的元素数量超过指定值就返回 false 。 - -饥饿问题这个可以通过优化设计来解决(比较麻烦),比如等待时间过长的任务会被移除并重新添加到队列中,但是优先级会被提升。 - -对于性能方面的影响,是没办法避免的,毕竟需要对任务进行排序操作。并且,对于大部分业务场景来说,这点性能影响是可以接受的。 - -## Future - -重点是要掌握 `CompletableFuture` 的使用以及常见面试题。 - -除了下面的面试题之外,还推荐你看看我写的这篇文章: [CompletableFuture 详解](./completablefuture-intro.md)。 - -### Future 类有什么用? - -`Future` 类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。具体来说是这样的:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 `Future` 类获取到耗时任务的执行结果。这样一来,程序的执行效率就明显提高了。 - -这其实就是多线程中经典的 **Future 模式**,你可以将其看作是一种设计模式,核心思想是异步调用,主要用在多线程领域,并非 Java 语言独有。 - -在 Java 中,`Future` 类只是一个泛型接口,位于 `java.util.concurrent` 包下,其中定义了 5 个方法,主要包括下面这 4 个功能: - -- 取消任务; -- 判断任务是否被取消; -- 判断任务是否已经执行完成; -- 获取任务执行结果。 - -```java -// V 代表了Future执行的任务返回值的类型 -public interface Future { - // 取消任务执行 - // 成功取消返回 true,否则返回 false - boolean cancel(boolean mayInterruptIfRunning); - // 判断任务是否被取消 - boolean isCancelled(); - // 判断任务是否已经执行完成 - boolean isDone(); - // 获取任务执行结果 - V get() throws InterruptedException, ExecutionException; - // 指定时间内没有返回计算结果就抛出 TimeOutException 异常 - V get(long timeout, TimeUnit unit) - - throws InterruptedException, ExecutionException, TimeoutExceptio - -} -``` - -简单理解就是:我有一个任务,提交给了 `Future` 来处理。任务执行期间我自己可以去做任何想做的事情。并且,在这期间我还可以取消任务以及获取任务的执行状态。一段时间之后,我就可以 `Future` 那里直接取出任务执行结果。 - -### Callable 和 Future 有什么关系? - -我们可以通过 `FutureTask` 来理解 `Callable` 和 `Future` 之间的关系。 - -`FutureTask` 提供了 `Future` 接口的基本实现,常用来封装 `Callable` 和 `Runnable`,具有取消任务、查看任务是否执行完成以及获取任务执行结果的方法。`ExecutorService.submit()` 方法返回的其实就是 `Future` 的实现类 `FutureTask` 。 - -```java - Future submit(Callable task); -Future submit(Runnable task); -``` - -`FutureTask` 不光实现了 `Future`接口,还实现了`Runnable` 接口,因此可以作为任务直接被线程执行。 - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/completablefuture-class-diagram.jpg) - -`FutureTask` 有两个构造函数,可传入 `Callable` 或者 `Runnable` 对象。实际上,传入 `Runnable` 对象也会在方法内部转换为`Callable` 对象。 - -```java -public FutureTask(Callable callable) { - if (callable == null) - throw new NullPointerException(); - this.callable = callable; - this.state = NEW; -} -public FutureTask(Runnable runnable, V result) { - // 通过适配器RunnableAdapter来将Runnable对象runnable转换成Callable对象 - this.callable = Executors.callable(runnable, result); - this.state = NEW; -} -``` - -`FutureTask`相当于对`Callable` 进行了封装,管理着任务执行的情况,存储了 `Callable` 的 `call` 方法的任务执行结果。 - -关于更多 `Future` 的源码细节,可以肝这篇万字解析,写的很清楚:[Java 是如何实现 Future 模式的?万字详解!](https://juejin.cn/post/6844904199625375757)。 - -### CompletableFuture 类有什么用? - -`Future` 在实际使用过程中存在一些局限性,比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。 - -Java 8 才被引入`CompletableFuture` 类可以解决`Future` 的这些缺陷。`CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。 - -下面我们来简单看看 `CompletableFuture` 类的定义。 - -```java -public class CompletableFuture implements Future, CompletionStage { -} -``` - -可以看到,`CompletableFuture` 同时实现了 `Future` 和 `CompletionStage` 接口。 - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/completablefuture-class-diagram.jpg) - -`CompletionStage` 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。 - -`CompletionStage` 接口中的方法比较多,`CompletableFuture` 的函数式能力就是这个接口赋予的。从这个接口的方法参数你就可以发现其大量使用了 Java8 引入的函数式编程。 - -![](https://oss.javaguide.cn/javaguide/image-20210902093026059.png) - -### ⭐️一个任务需要依赖另外两个任务执行完之后再执行,怎么设计? - -这种任务编排场景非常适合通过`CompletableFuture`实现。这里假设要实现 T3 在 T2 和 T1 执行完后执行。 - -代码如下(这里为了简化代码,用到了 Hutool 的线程工具类 `ThreadUtil` 和日期时间工具类 `DateUtil`): - -```java -// T1 -CompletableFuture futureT1 = CompletableFuture.runAsync(() -> { - System.out.println("T1 is executing. Current time:" + DateUtil.now()); - // 模拟耗时操作 - ThreadUtil.sleep(1000); -}); -// T2 -CompletableFuture futureT2 = CompletableFuture.runAsync(() -> { - System.out.println("T2 is executing. Current time:" + DateUtil.now()); - ThreadUtil.sleep(1000); -}); - -// 使用allOf()方法合并T1和T2的CompletableFuture,等待它们都完成 -CompletableFuture bothCompleted = CompletableFuture.allOf(futureT1, futureT2); -// 当T1和T2都完成后,执行T3 -bothCompleted.thenRunAsync(() -> System.out.println("T3 is executing after T1 and T2 have completed.Current time:" + DateUtil.now())); -// 等待所有任务完成,验证效果 -ThreadUtil.sleep(3000); -``` - -通过 `CompletableFuture` 的 `allOf()` 这个静态方法来并行运行 T1 和 T2,当 T1 和 T2 都完成后,再执行 T3。 - -### ⭐️使用 CompletableFuture,有一个任务失败,如何处理异常? - -使用 `CompletableFuture`的时候一定要以正确的方式进行异常处理,避免异常丢失或者出现不可控问题。 - -下面是一些建议: - -- 使用 `whenComplete` 方法可以在任务完成时触发回调函数,并正确地处理异常,而不是让异常被吞噬或丢失。 -- 使用 `exceptionally` 方法可以处理异常并重新抛出,以便异常能够传播到后续阶段,而不是让异常被忽略或终止。 -- 使用 `handle` 方法可以处理正常的返回结果和异常,并返回一个新的结果,而不是让异常影响正常的业务逻辑。 -- 使用 `CompletableFuture.allOf` 方法可以组合多个 `CompletableFuture`,并统一处理所有任务的异常,而不是让异常处理过于冗长或重复。 -- …… - -### ⭐️在使用 CompletableFuture 的时候为什么要自定义线程池? - -`CompletableFuture` 默认使用全局共享的 `ForkJoinPool.commonPool()` 作为执行器,所有未指定执行器的异步任务都会使用该线程池。这意味着应用程序、多个库或框架(如 Spring、第三方库)若都依赖 `CompletableFuture`,默认情况下它们都会共享同一个线程池。 - -虽然 `ForkJoinPool` 效率很高,但当同时提交大量任务时,可能会导致资源竞争和线程饥饿,进而影响系统性能。 - -为避免这些问题,建议为 `CompletableFuture` 提供自定义线程池,带来以下优势: - -- 隔离性:为不同任务分配独立的线程池,避免全局线程池资源争夺。 -- 资源控制:根据任务特性调整线程池大小和队列类型,优化性能表现。 -- 异常处理:通过自定义 `ThreadFactory` 更好地处理线程中的异常情况。 - -```java -private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue()); - -CompletableFuture.runAsync(() -> { - //... -}, executor); -``` - -## AQS - -关于 AQS 源码的详细分析,可以看看这一篇文章:[AQS 详解](./aqs.md)。 - -### AQS 是什么? - -AQS (`AbstractQueuedSynchronizer` ,抽象队列同步器)是从 JDK1.5 开始提供的 Java 并发核心组件。 - -AQS 解决了开发者在实现同步器时的复杂性问题。它提供了一个通用框架,用于实现各种同步器,例如 **可重入锁**(`ReentrantLock`)、**信号量**(`Semaphore`)和 **倒计时器**(`CountDownLatch`)。通过封装底层的线程同步机制,AQS 将复杂的线程管理逻辑隐藏起来,使开发者只需专注于具体的同步逻辑。 - -简单来说,AQS 是一个抽象类,为同步器提供了通用的 **执行框架**。它定义了 **资源获取和释放的通用流程**,而具体的资源获取逻辑则由具体同步器通过重写模板方法来实现。 因此,可以将 AQS 看作是同步器的 **基础“底座”**,而同步器则是基于 AQS 实现的 **具体“应用”**。 - -### ⭐️AQS 的原理是什么? - -AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 **CLH 锁** (Craig, Landin, and Hagersten locks) 进一步优化实现的。 - -**CLH 锁** 对自旋锁进行了改进,是基于单链表的自旋锁。在多线程场景下,会将请求获取锁的线程组织成一个单向队列,每个等待的线程会通过自旋访问前一个线程节点的状态,前一个节点释放锁之后,当前节点才可以获取锁。**CLH 锁** 的队列结构如下图所示。 - -![CLH 锁的队列结构](https://oss.javaguide.cn/github/javaguide/open-source-project/clh-lock-queue-structure.png) - -AQS 中使用的 **等待队列** 是 CLH 锁队列的变体(接下来简称为 CLH 变体队列)。 - -AQS 的 CLH 变体队列是一个双向队列,会暂时获取不到锁的线程将被加入到该队列中,CLH 变体队列和原本的 CLH 锁队列的区别主要有两点: - -- 由 **自旋** 优化为 **自旋 + 阻塞** :自旋操作的性能很高,但大量的自旋操作比较占用 CPU 资源,因此在 CLH 变体队列中会先通过自旋尝试获取锁,如果失败再进行阻塞等待。 -- 由 **单向队列** 优化为 **双向队列** :在 CLH 变体队列中,会对等待的线程进行阻塞操作,当队列前边的线程释放锁之后,需要对后边的线程进行唤醒,因此增加了 `next` 指针,成为了双向队列。 - -AQS 将每条请求共享资源的线程封装成一个 CLH 变体队列的一个结点(Node)来实现锁的分配。在 CLH 变体队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。 - -AQS 中的 CLH 变体队列结构如下图所示: - -![CLH 变体队列结构](https://oss.javaguide.cn/github/javaguide/java/concurrent/clh-queue-structure-bianti.png) - -AQS(`AbstractQueuedSynchronizer`)的核心原理图: - -![CLH 变体队列](https://oss.javaguide.cn/github/javaguide/java/concurrent/clh-queue-state.png) - -AQS 使用 **int 成员变量 `state` 表示同步状态**,通过内置的 **线程等待队列** 来完成获取资源线程的排队工作。 - -`state` 变量由 `volatile` 修饰,用于展示当前临界资源的获锁情况。 - -```java -// 共享变量,使用volatile修饰保证线程可见性 -private volatile int state; -``` - -另外,状态信息 `state` 可以通过 `protected` 类型的`getState()`、`setState()`和`compareAndSetState()` 进行操作。并且,这几个方法都是 `final` 修饰的,在子类中无法被重写。 - -```java -//返回同步状态的当前值 -protected final int getState() { - return state; -} - // 设置同步状态的值 -protected final void setState(int newState) { - state = newState; -} -//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) -protected final boolean compareAndSetState(int expect, int update) { - return unsafe.compareAndSwapInt(this, stateOffset, expect, update); -} -``` - -以 `ReentrantLock` 为例,`state` 初始值为 0,表示未锁定状态。A 线程 `lock()` 时,会调用 `tryAcquire()` 独占该锁并将 `state+1` 。此后,其他线程再 `tryAcquire()` 时就会失败,直到 A 线程 `unlock()` 到 `state=`0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(`state` 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。 - -再以 `CountDownLatch` 以例,任务分为 N 个子线程去执行,`state` 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后`countDown()` 一次,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 `state=0` ),会 `unpark()` 主调用线程,然后主调用线程就会从 `await()` 函数返回,继续后续动作。 - -### Semaphore 有什么用? - -`synchronized` 和 `ReentrantLock` 都是一次只允许一个线程访问某个资源,而`Semaphore`(信号量)可以用来控制同时访问特定资源的线程数量。 - -Semaphore 的使用简单,我们这里假设有 N(N>5) 个线程来获取 `Semaphore` 中的共享资源,下面的代码表示同一时刻 N 个线程中只有 5 个线程能获取到共享资源,其他线程都会阻塞,只有获取到共享资源的线程才能执行。等到有线程释放了共享资源,其他阻塞的线程才能获取到。 - -```java -// 初始共享资源数量 -final Semaphore semaphore = new Semaphore(5); -// 获取1个许可 -semaphore.acquire(); -// 释放1个许可 -semaphore.release(); -``` - -当初始的资源个数为 1 的时候,`Semaphore` 退化为排他锁。 - -`Semaphore` 有两种模式:。 - -- **公平模式:** 调用 `acquire()` 方法的顺序就是获取许可证的顺序,遵循 FIFO; -- **非公平模式:** 抢占式的。 - -`Semaphore` 对应的两个构造方法如下: - -```java -public Semaphore(int permits) { - sync = new NonfairSync(permits); -} - -public Semaphore(int permits, boolean fair) { - sync = fair ? new FairSync(permits) : new NonfairSync(permits); -} -``` - -**这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。** - -`Semaphore` 通常用于那些资源有明确访问数量限制的场景比如限流(仅限于单机模式,实际项目中推荐使用 Redis +Lua 来做限流)。 - -### Semaphore 的原理是什么? - -`Semaphore` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `permits`,你可以将 `permits` 的值理解为许可证的数量,只有拿到许可证的线程才能执行。 - -调用`semaphore.acquire()` ,线程尝试获取许可证,如果 `state >= 0` 的话,则表示可以获取成功。如果获取成功的话,使用 CAS 操作去修改 `state` 的值 `state=state-1`。如果 `state<0` 的话,则表示许可证数量不足。此时会创建一个 Node 节点加入阻塞队列,挂起当前线程。 - -```java -/** - * 获取1个许可证 - */ -public void acquire() throws InterruptedException { - sync.acquireSharedInterruptibly(1); -} -/** - * 共享模式下获取许可证,获取成功则返回,失败则加入阻塞队列,挂起线程 - */ -public final void acquireSharedInterruptibly(int arg) - throws InterruptedException { - if (Thread.interrupted()) - throw new InterruptedException(); - // 尝试获取许可证,arg为获取许可证个数,当可用许可证数减当前获取的许可证数结果小于0,则创建一个节点加入阻塞队列,挂起当前线程。 - if (tryAcquireShared(arg) < 0) - doAcquireSharedInterruptibly(arg); -} -``` - -调用`semaphore.release();` ,线程尝试释放许可证,并使用 CAS 操作去修改 `state` 的值 `state=state+1`。释放许可证成功之后,同时会唤醒同步队列中的一个线程。被唤醒的线程会重新尝试去修改 `state` 的值 `state=state-1` ,如果 `state>=0` 则获取令牌成功,否则重新进入阻塞队列,挂起线程。 - -```java -// 释放一个许可证 -public void release() { - sync.releaseShared(1); -} - -// 释放共享锁,同时会唤醒同步队列中的一个线程。 -public final boolean releaseShared(int arg) { - //释放共享锁 - if (tryReleaseShared(arg)) { - //唤醒同步队列中的一个线程 - doReleaseShared(); - return true; - } - return false; -} -``` - -### CountDownLatch 有什么用? - -`CountDownLatch` 允许 `count` 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。 - -`CountDownLatch` 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 `CountDownLatch` 使用完毕后,它不能再次被使用。 - -### CountDownLatch 的原理是什么? - -`CountDownLatch` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `count`。当线程使用 `countDown()` 方法时,其实使用了`tryReleaseShared`方法以 CAS 的操作来减少 `state`,直至 `state` 为 0 。当调用 `await()` 方法的时候,如果 `state` 不为 0,那就证明任务还没有执行完毕,`await()` 方法就会一直阻塞,也就是说 `await()` 方法之后的语句不会被执行。直到`count` 个线程调用了`countDown()`使 state 值被减为 0,或者调用`await()`的线程被中断,该线程才会从阻塞中被唤醒,`await()` 方法之后的语句得到执行。 - -### 用过 CountDownLatch 么?什么场景下用的? - -`CountDownLatch` 的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。之前在项目中,有一个使用多线程读取多个文件处理的场景,我用到了 `CountDownLatch` 。具体场景是下面这样的: - -我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。 - -为此我们定义了一个线程池和 count 为 6 的`CountDownLatch`对象 。使用线程池处理读取任务,每一个线程处理完之后就将 count-1,调用`CountDownLatch`对象的 `await()`方法,直到所有文件读取完之后,才会接着执行后面的逻辑。 - -伪代码是下面这样的: - -```java -public class CountDownLatchExample1 { - // 处理文件的数量 - private static final int threadCount = 6; - - public static void main(String[] args) throws InterruptedException { - // 创建一个具有固定线程数量的线程池对象(推荐使用构造方法创建) - ExecutorService threadPool = Executors.newFixedThreadPool(10); - final CountDownLatch countDownLatch = new CountDownLatch(threadCount); - for (int i = 0; i < threadCount; i++) { - final int threadnum = i; - threadPool.execute(() -> { - try { - //处理文件的业务操作 - //...... - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - //表示一个文件已经被完成 - countDownLatch.countDown(); - } - - }); - } - countDownLatch.await(); - threadPool.shutdown(); - System.out.println("finish"); - } -} -``` - -**有没有可以改进的地方呢?** - -可以使用 `CompletableFuture` 类来改进!Java8 的 `CompletableFuture` 提供了很多对多线程友好的方法,使用它可以很方便地为我们编写多线程程序,什么异步、串行、并行或者等待所有线程执行完任务什么的都非常方便。 - -```java -CompletableFuture task1 = - CompletableFuture.supplyAsync(()->{ - //自定义业务操作 - }); -...... -CompletableFuture task6 = - CompletableFuture.supplyAsync(()->{ - //自定义业务操作 - }); -...... -CompletableFuture headerFuture=CompletableFuture.allOf(task1,.....,task6); - -try { - headerFuture.join(); -} catch (Exception ex) { - //...... -} -System.out.println("all done. "); -``` - -上面的代码还可以继续优化,当任务过多的时候,把每一个 task 都列出来不太现实,可以考虑通过循环来添加任务。 - -```java -//文件夹位置 -List filePaths = Arrays.asList(...) -// 异步处理所有文件 -List> fileFutures = filePaths.stream() - .map(filePath -> doSomeThing(filePath)) - .collect(Collectors.toList()); -// 将他们合并起来 -CompletableFuture allFutures = CompletableFuture.allOf( - fileFutures.toArray(new CompletableFuture[fileFutures.size()]) -); -``` - -### CyclicBarrier 有什么用? - -`CyclicBarrier` 和 `CountDownLatch` 非常类似,它也可以实现线程间的技术等待,但是它的功能比 `CountDownLatch` 更加复杂和强大。主要应用场景和 `CountDownLatch` 类似。 - -> `CountDownLatch` 的实现是基于 AQS 的,而 `CyclicBarrier` 是基于 `ReentrantLock`(`ReentrantLock` 也属于 AQS 同步器)和 `Condition` 的。 - -`CyclicBarrier` 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。 - -### CyclicBarrier 的原理是什么? - -`CyclicBarrier` 内部通过一个 `count` 变量作为计数器,`count` 的初始值为 `parties` 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减 1。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。 - -```java -//每次拦截的线程数 -private final int parties; -//计数器 -private int count; -``` - -下面我们结合源码来简单看看。 - -1、`CyclicBarrier` 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用 `await()` 方法告诉 `CyclicBarrier` 我已经到达了屏障,然后当前线程被阻塞。 - -```java -public CyclicBarrier(int parties) { - this(parties, null); -} - -public CyclicBarrier(int parties, Runnable barrierAction) { - if (parties <= 0) throw new IllegalArgumentException(); - this.parties = parties; - this.count = parties; - this.barrierCommand = barrierAction; -} -``` - -其中,`parties` 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过。 - -2、当调用 `CyclicBarrier` 对象调用 `await()` 方法时,实际上调用的是 `dowait(false, 0L)`方法。 `await()` 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 `parties` 的值时,栅栏才会打开,线程才得以通过执行。 - -```java -public int await() throws InterruptedException, BrokenBarrierException { - try { - return dowait(false, 0L); - } catch (TimeoutException toe) { - throw new Error(toe); // cannot happen - } -} -``` - -`dowait(false, 0L)`方法源码分析如下: - -```java - // 当线程数量或者请求数量达到 count 时 await 之后的方法才会被执行。上面的示例中 count 的值就为 5。 - private int count; - /** - * Main barrier code, covering the various policies. - */ - private int dowait(boolean timed, long nanos) - throws InterruptedException, BrokenBarrierException, - TimeoutException { - final ReentrantLock lock = this.lock; - // 锁住 - lock.lock(); - try { - final Generation g = generation; - - if (g.broken) - throw new BrokenBarrierException(); - - // 如果线程中断了,抛出异常 - if (Thread.interrupted()) { - breakBarrier(); - throw new InterruptedException(); - } - // cout减1 - int index = --count; - // 当 count 数量减为 0 之后说明最后一个线程已经到达栅栏了,也就是达到了可以执行await 方法之后的条件 - if (index == 0) { // tripped - boolean ranAction = false; - try { - final Runnable command = barrierCommand; - if (command != null) - command.run(); - ranAction = true; - // 将 count 重置为 parties 属性的初始化值 - // 唤醒之前等待的线程 - // 下一波执行开始 - nextGeneration(); - return 0; - } finally { - if (!ranAction) - breakBarrier(); - } - } - - // loop until tripped, broken, interrupted, or timed out - for (;;) { - try { - if (!timed) - trip.await(); - else if (nanos > 0L) - nanos = trip.awaitNanos(nanos); - } catch (InterruptedException ie) { - if (g == generation && ! g.broken) { - breakBarrier(); - throw ie; - } else { - // We're about to finish waiting even if we had not - // been interrupted, so this interrupt is deemed to - // "belong" to subsequent execution. - Thread.currentThread().interrupt(); - } - } - - if (g.broken) - throw new BrokenBarrierException(); - - if (g != generation) - return index; - - if (timed && nanos <= 0L) { - breakBarrier(); - throw new TimeoutException(); - } - } - } finally { - lock.unlock(); - } - } -``` - -## 虚拟线程 - -虚拟线程在 Java 21 正式发布,这是一项重量级的更新。 - -虽然目前面试中问的不多,但还是建议大家去简单了解一下,具体可以阅读这篇文章:[虚拟线程极简入门](./virtual-thread.md) 。重点搞清楚虚拟线程和平台线程的关系以及虚拟线程的优势即可。 - -## 参考 - -- 《深入理解 Java 虚拟机》 -- 《实战 Java 高并发程序设计》 -- Java 线程池的实现原理及其在业务中的最佳实践:阿里云开发者: -- 带你了解下 SynchronousQueue(并发队列专题): -- 阻塞队列 — DelayedWorkQueue 源码分析: -- Java 多线程(三)——FutureTask/CompletableFuture: -- Java 并发之 AQS 详解: -- Java 并发包基石-AQS 详解: - - +\*\*Each `Thread` has a `ThreadLocalMap`, and `ThreadLocalMap` can store key-value pairs with `ThreadLocal` as the diff --git a/docs/java/concurrent/java-thread-pool-best-practices.md b/docs/java/concurrent/java-thread-pool-best-practices.md index 04154bfa378..bd40b87cf7e 100644 --- a/docs/java/concurrent/java-thread-pool-best-practices.md +++ b/docs/java/concurrent/java-thread-pool-best-practices.md @@ -1,44 +1,44 @@ --- -title: Java 线程池最佳实践 +title: Best Practices for Java Thread Pools category: Java tag: - - Java并发 + - Java Concurrency --- -简单总结一下我了解的使用线程池的时候应该注意的东西,网上似乎还没有专门写这方面的文章。 +Let me summarize what I know about things to pay attention to when using thread pools; it seems there are no dedicated articles on this topic online. -## 1、正确声明线程池 +## 1. Correctly Declare Thread Pools -**线程池必须手动通过 `ThreadPoolExecutor` 的构造函数来声明,避免使用`Executors` 类创建线程池,会有 OOM 风险。** +**Thread pools must be explicitly declared using the `ThreadPoolExecutor` constructor. Avoid using the `Executors` class to create thread pools to mitigate OOM risk.** -`Executors` 返回线程池对象的弊端如下(后文会详细介绍到): +The drawbacks of the thread pool objects returned by `Executors` are as follows (which will be detailed later): -- **`FixedThreadPool` 和 `SingleThreadExecutor`**:使用的是阻塞队列 `LinkedBlockingQueue`,任务队列的默认长度和最大长度为 `Integer.MAX_VALUE`,可以看作是无界队列,可能堆积大量的请求,从而导致 OOM。 -- **`CachedThreadPool`**:使用的是同步队列 `SynchronousQueue`,允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。 -- **`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor`** : 使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。 +- **`FixedThreadPool` and `SingleThreadExecutor`**: These use the blocking queue `LinkedBlockingQueue`, which has a default and maximum length of `Integer.MAX_VALUE`, effectively making it an unbounded queue that can accumulate a large number of requests, leading to OOM. +- **`CachedThreadPool`**: This uses the synchronous queue `SynchronousQueue`, which allows a maximum number of threads up to `Integer.MAX_VALUE`, potentially creating a large number of threads and causing OOM. +- **`ScheduledThreadPool` and `SingleThreadScheduledExecutor`**: These employ an unbounded delayed blocking queue `DelayedWorkQueue`, with a maximum length of `Integer.MAX_VALUE`, which can also lead to a build-up of many requests and result in OOM. -说白了就是:**使用有界队列,控制线程创建数量。** +In simple terms: **Use bounded queues to control the number of threads created.** -除了避免 OOM 的原因之外,不推荐使用 `Executors`提供的两种快捷的线程池的原因还有: +In addition to avoiding OOM, the reasons not to use the two convenient thread pools provided by `Executors` include: -- 实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。 -- 我们应该显示地给我们的线程池命名,这样有助于我们定位问题。 +- In practical use, it’s necessary to manually configure the thread pool parameters according to the machine's performance and the business context, such as core thread count, task queues, and saturation strategies. +- We should explicitly name our thread pools, which helps in identifying issues. -## 2、监测线程池运行状态 +## 2. Monitor Thread Pool Running Status -你可以通过一些手段来检测线程池的运行状态比如 SpringBoot 中的 Actuator 组件。 +You can use certain tools, like the Actuator component in Spring Boot, to check the status of the thread pool. -除此之外,我们还可以利用 `ThreadPoolExecutor` 的相关 API 做一个简陋的监控。从下图可以看出, `ThreadPoolExecutor`提供了获取线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等。 +Furthermore, we can utilize the relevant APIs of `ThreadPoolExecutor` for simple monitoring. As shown in the image below, `ThreadPoolExecutor` offers methods to obtain the current thread count, active thread count, completed task count, and the number of tasks in the queue. ![](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpool-methods-information.png) -下面是一个简单的 Demo。`printThreadPoolStatus()`会每隔一秒打印出线程池的线程数、活跃线程数、完成的任务数、以及队列中的任务数。 +Here is a simple demo. `printThreadPoolStatus()` will print the thread count, active thread count, completed task count, and the task count in the queue every second. ```java /** - * 打印线程池的状态 + * Print the status of the thread pool * - * @param threadPool 线程池对象 + * @param threadPool Thread pool object */ public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) { ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, createThreadFactory("print-images/thread-pool-status", false)); @@ -53,49 +53,49 @@ public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) { } ``` -## 3、建议不同类别的业务用不同的线程池 +## 3. Different Types of Business Should Use Different Thread Pools -很多人在实际项目中都会有类似这样的问题:**我的项目中多个业务需要用到线程池,是为每个线程池都定义一个还是说定义一个公共的线程池呢?** +Many people encounter a question in actual projects: **Should I define a separate thread pool for each business need, or define a common thread pool?** -一般建议是不同的业务使用不同的线程池,配置线程池的时候根据当前业务的情况对当前线程池进行配置,因为不同的业务的并发以及对资源的使用情况都不同,重心优化系统性能瓶颈相关的业务。 +Generally, it is advisable for different businesses to use different thread pools. When configuring the thread pool, parameters should be tailored to the current business context, as different businesses have varying concurrency levels and resource usage, with a focus on optimizing performance bottlenecks relevant to each business. -**我们再来看一个真实的事故案例!** (本案例来源自:[《线程池运用不当的一次线上事故》](https://heapdump.cn/article/646639) ,很精彩的一个案例) +**Let’s look at a real incident case!** (This case is sourced from: [“An Online Incident Due to Improper Use of Thread Pool”](https://heapdump.cn/article/646639), an intriguing case) -![案例代码概览](https://oss.javaguide.cn/github/javaguide/java/concurrent/production-accident-threadpool-sharing-example.png) +![Case Code Overview](https://oss.javaguide.cn/github/javaguide/java/concurrent/production-accident-threadpool-sharing-example.png) -上面的代码可能会存在死锁的情况,为什么呢?画个图给大家捋一捋。 +The code above might have a deadlock situation. Why? Let's illustrate it with a diagram. -试想这样一种极端情况:假如我们线程池的核心线程数为 **n**,父任务(扣费任务)数量为 **n**,父任务下面有两个子任务(扣费任务下的子任务),其中一个已经执行完成,另外一个被放在了任务队列中。由于父任务把线程池核心线程资源用完,所以子任务因为无法获取到线程资源无法正常执行,一直被阻塞在队列中。父任务等待子任务执行完成,而子任务等待父任务释放线程池资源,这也就造成了 **"死锁"** 。 +Imagine this extreme scenario: suppose our thread pool's core thread count is **n**, and there are **n** parent tasks (billing tasks), each with two child tasks (sub-tasks under the billing tasks), one of which is complete while the other is queued. Since the parent task has maxed out the thread pool's core threads, the sub-task can’t execute normally because it lacks thread resources, getting blocked in the queue. The parent task waits for the sub-task to finish, and the sub-task, in turn, waits for the parent task to free up thread pool resources, resulting in **"deadlock."** -![线程池使用不当导致死锁](https://oss.javaguide.cn/github/javaguide/java/concurrent/production-accident-threadpool-sharing-deadlock.png) +![Improper Use of Thread Pool Leading to Deadlock](https://oss.javaguide.cn/github/javaguide/java/concurrent/production-accident-threadpool-sharing-deadlock.png) -解决方法也很简单,就是新增加一个用于执行子任务的线程池专门为其服务。 +The solution is simple: just add a dedicated thread pool for executing sub-tasks. -## 4、别忘记给线程池命名 +## 4. Don’t Forget to Name Your Thread Pool -初始化线程池的时候需要显示命名(设置线程池名称前缀),有利于定位问题。 +When initializing a thread pool, it’s important to explicitly name it (set the thread pool name prefix), which helps in pinpointing issues. -默认情况下创建的线程名字类似 `pool-1-thread-n` 这样的,没有业务含义,不利于我们定位问题。 +By default, the threads created are named something like `pool-1-thread-n`, which lacks business meaning and does not aid in troubleshooting. -给线程池里的线程命名通常有下面两种方式: +There are usually two ways to name threads in a thread pool: -**1、利用 guava 的 `ThreadFactoryBuilder`** +**1. Use Guava's `ThreadFactoryBuilder`** ```java ThreadFactory threadFactory = new ThreadFactoryBuilder() .setNameFormat(threadNamePrefix + "-%d") .setDaemon(true).build(); -ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory) +ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory); ``` -**2、自己实现 `ThreadFactory`。** +**2. Implement `ThreadFactory` yourself.** ```java import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; /** - * 线程工厂,它设置线程名称,有利于我们定位问题。 + * Thread factory that sets the thread name, which helps us locate issues. */ public final class NamingThreadFactory implements ThreadFactory { @@ -103,7 +103,7 @@ public final class NamingThreadFactory implements ThreadFactory { private final String name; /** - * 创建一个带名字的线程池生产工厂 + * Creates a thread pool production factory with a name */ public NamingThreadFactory(String name) { this.name = name; @@ -118,147 +118,147 @@ public final class NamingThreadFactory implements ThreadFactory { } ``` -## 5、正确配置线程池参数 +## 5. Correctly Configure Thread Pool Parameters -说到如何给线程池配置参数,美团的骚操作至今让我难忘(后面会提到)! +When it comes to configuring thread pool parameters, Meituan's clever methods leave a lasting impression on me (which will be discussed later)! -我们先来看一下各种书籍和博客上一般推荐的配置线程池参数的方式,可以作为参考。 +Let’s first look at the commonly recommended practices for configuring thread pool parameters in various books and blogs. -### 常规操作 +### General Operations -很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说:**并不是人多就能把事情做好,增加了沟通交流成本。你本来一件事情只需要 3 个人做,你硬是拉来了 6 个人,会提升做事效率嘛?我想并不会。** 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了**上下文切换** 成本。不清楚什么是上下文切换的话,可以看我下面的介绍。 +Many people might think it’s better to over-configure the thread pool! I think this is clearly a problem. Just like we often see in life: **having more people doesn’t always mean tasks will be completed more efficiently; it introduces greater communication overhead. If a task only requires 3 people, bringing in 6 won’t increase efficiency!** The impact of having too many threads is analogous to allocating too many people; in a multithreading context, it primarily increases the **context switch** cost. If you're unclear about context switching, please refer to my explanation below. -> 上下文切换: +> Context Switching: > -> 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 +> In multithreading programming, the number of threads usually exceeds the number of CPU cores, and each CPU core can only be used by one thread at a time. In order to ensure all threads effectively get executed, the CPU adopts a strategy that allocates time slots for each thread in a round-robin manner. When a thread's time slice runs out, it goes back to the ready state to let another thread use the core. This process constitutes a context switch. In summary: before the current task switches to another task, it first saves its state so that it can resume correctly when it switches back. **The process of saving to reloading an old task is a context switch.** > -> 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 +> Context switching typically has high computational costs, requiring significant CPU time. With dozens to hundreds of switches a second, each of them consumes time at the nanosecond level. Thus, context switching can consume enormous CPU time in a system and is often one of the most time-consuming operations for an operating system. > -> Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 +> Compared to other operating systems, Linux has many advantages, including low time costs for context switches and mode switches. -类比于现实世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。 +Just like humans working together in the real world, we can be sure that an oversized or undersized thread pool presents its own problems; an appropriate size is best. -- 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的,CPU 根本没有得到充分利用。 -- 如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。 +- If the thread pool number is too small, when there are many tasks/requests needing processing at the same time, many requests/tasks may queue up waiting to execute, potentially leading to full queues where tasks/requests cannot be handled, or jamming up tasks leading to OOM. Clearly, this is problematic, as the CPU is not utilized adequately. +- If the thread count is too large, many threads may be contending for CPU resources simultaneously, leading to extensive context switching, which increases task execution time, affecting overall execution efficiency. -有一个简单并且适用面比较广的公式: +A simple and widely applicable formula is: -- **CPU 密集型任务 (N):** 这种任务消耗的主要是 CPU 资源,线程数应设置为 N(CPU 核心数)。由于任务主要瓶颈在于 CPU 计算能力,与核心数相等的线程数能够最大化 CPU 利用率,过多线程反而会导致竞争和上下文切换开销。 -- **I/O 密集型任务(M \* N):** 这类任务大部分时间处理 I/O 交互,线程在等待 I/O 时不占用 CPU。 为了充分利用 CPU 资源,线程数可以设置为 M \* N,其中 N 是 CPU 核心数,M 是一个大于 1 的倍数,建议默认设置为 2 ,具体取值取决于 I/O 等待时间和任务特点,需要通过测试和监控找到最佳平衡点。 +- **CPU Intensive Tasks (N):** These tasks primarily consume CPU resources, and the thread count should be set to N (the number of CPU cores). Since these tasks are bottlenecked by CPU computing power, a thread count equal to the core count maximizes CPU utilization. Having too many threads can lead to contention and switching overhead. +- **I/O Intensive Tasks (M * N):** These tasks spend most of their time processing I/O interactions and do not occupy the CPU while waiting for I/O. To fully utilize CPU resources, the thread count should be set to M * N, where N is the number of CPU cores, and M is a multiplier greater than 1, typically suggested as 2 depending on I/O wait times and task characteristics; testing and monitoring is crucial to find the optimal balance. -CPU 密集型任务不再推荐 N+1,原因如下: +The "N+1" recommendation for CPU-intensive tasks is no longer advised for the following reasons: -- "N+1" 的初衷是希望预留线程处理突发暂停,但实际上,处理缺页中断等情况仍然需要占用 CPU 核心。 -- CPU 密集场景下,CPU 始终是瓶颈,预留线程并不能凭空增加 CPU 处理能力,反而可能加剧竞争。 +- "N+1" was initially meant to reserve threads for handling sudden pauses, but in practice, it still occupies CPU cores during operations like page faults. +- In CPU-intensive scenarios, the CPU remains the bottleneck, and reserving threads doesn't increase CPU capacity; it might aggravate contention. -**如何判断是 CPU 密集任务还是 IO 密集任务?** +**How to determine if a task is CPU or I/O intensive?** -CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。 +CPU-intensive tasks are those that utilize CPU computing power, like sorting large amounts of data in memory. Any task involving network reading or file reading is I/O intensive; in these cases, the CPU computation time is minimal compared to the time spent waiting for I/O to complete, with most time spent waiting for I/O operations to finish. -🌈 拓展一下(参见:[issue#1737](https://github.com/Snailclimb/JavaGuide/issues/1737)): +🌈 To expand (see: [issue#1737](https://github.com/Snailclimb/JavaGuide/issues/1737)): -线程数更严谨的计算的方法应该是:`最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间))`,其中 `WT(线程等待时间)=线程运行总时间 - ST(线程计算时间)`。 +A more precise method to calculate the thread count should be: `Optimal Thread Count = N (CPU Cores) * (1 + WT (Thread Wait Time) / ST (Thread Compute Time))`, where `WT (Thread Wait Time) = Total Thread Runtime - ST (Thread Compute Time)`. -线程等待时间所占比例越高,需要越多线程。线程计算时间所占比例越高,需要越少线程。 +The larger the proportion of thread wait time, the more threads are needed. The larger the proportion of thread compute time, the fewer threads are needed. -我们可以通过 JDK 自带的工具 VisualVM 来查看 `WT/ST` 比例。 +We can use JDK's built-in tool VisualVM to find the ratio of `WT/ST`. -CPU 密集型任务的 `WT/ST` 接近或者等于 0,因此, 线程数可以设置为 N(CPU 核心数)∗(1+0)= N,和我们上面说的 N(CPU 核心数)+1 差不多。 +For CPU-intensive tasks, the `WT/ST` ratio is close to or equals 0, therefore, the thread count can be set to N (CPU Cores) * (1 + 0) = N, which is similar to the earlier suggestion of N (CPU Cores) + 1. -IO 密集型任务下,几乎全是线程等待时间,从理论上来说,你就可以将线程数设置为 2N(按道理来说,WT/ST 的结果应该比较大,这里选择 2N 的原因应该是为了避免创建过多线程吧)。 +Under I/O-intensive tasks, almost all time is spent on thread waiting, theoretically allowing the thread count to be set to 2N (the ratio of `WT/ST` should be substantial, which is why selecting 2N aims to avoid creating too many threads). -**注意**:上面提到的公示也只是参考,实际项目不太可能直接按照公式来设置线程池参数,毕竟不同的业务场景对应的需求不同,具体还是要根据项目实际线上运行情况来动态调整。接下来介绍的美团的线程池参数动态配置这种方案就非常不错,很实用! +**Note**: The formulas mentioned above are for reference; practical projects may not directly set thread pool parameters based on formulas, as different business contexts have varying requirements, and actual online operations should be dynamically adjusted based on the project's live running conditions. The dynamic configuration of thread pool parameters introduced by Meituan is an excellent and practical solution! -### 美团的骚操作 +### Meituan's Clever Methods -美团技术团队在[《Java 线程池实现原理及其在美团业务中的实践》](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)这篇文章中介绍到对线程池参数实现可自定义配置的思路和方法。 +The technical team at Meituan describes the approach and methods for customizable thread pool parameter configuration in their article [“Java Thread Pool Implementation Principles and Practices in Meituan's Business”](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html). -美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是: +The Meituan team focuses primarily on customizing core parameters of the thread pool. These three core parameters are: -- **`corePoolSize` :** 核心线程数定义了最小可以同时运行的线程数量。 -- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 +- **`corePoolSize`**: The core thread count defines the minimum number of threads that can run simultaneously. +- **`maximumPoolSize`**: When the number of tasks in the queue reaches the queue capacity, the currently running threads can go up to the maximum thread count. +- **`workQueue`**: When a new task arrives, it first checks if the current running thread count has reached the core thread count; if it has, the new task will be stored in the queue. -**为什么是这三个参数?** +**Why these three parameters?** -我在这篇[《新手也能看懂的线程池学习总结》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485808&idx=1&sn=1013253533d73450cef673aee13267ab&chksm=cea246bbf9d5cfad1c21316340a0ef1609a7457fea4113a1f8d69e8c91e7d9cd6285f5ee1490&token=510053261&lang=zh_CN&scene=21#wechat_redirect) 中就说过这三个参数是 `ThreadPoolExecutor` 最重要的参数,它们基本决定了线程池对于任务的处理策略。 +In my article [“A Beginner's Guide to Understanding Thread Pool”](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485808&idx=1&sn=1013253533d73450cef673aee13267ab&chksm=cea246bbf9d5cfad1c21316340a0ef1609a7457fea4113a1f8d69e8c91e7d9cd6285f5ee1490&token=510053261&lang=zh_CN&scene=21#wechat_redirect), I stated that these three parameters are the most important ones for `ThreadPoolExecutor` and essentially determine how the thread pool processes tasks. -**如何支持参数动态配置?** 且看 `ThreadPoolExecutor` 提供的下面这些方法。 +**How to support dynamic parameter configuration?** Look at the methods provided by `ThreadPoolExecutor` below. ![](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpoolexecutor-methods.png) -格外需要注意的是`corePoolSize`, 程序运行期间的时候,我们调用 `setCorePoolSize()`这个方法的话,线程池会首先判断当前工作线程数是否大于`corePoolSize`,如果大于的话就会回收工作线程。 +It’s particularly important to note `corePoolSize`. While the program is running, if we call `setCorePoolSize()` method, the thread pool first checks if the current working thread count exceeds `corePoolSize`. If it does, it will reclaim working threads. -另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 `ResizableCapacityLinkedBlockIngQueue` 的队列(主要就是把`LinkedBlockingQueue`的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。 +Also, as can be seen above, there is no method for dynamically specifying the queue length; Meituan's approach is to create a custom queue called `ResizableCapacityLinkedBlockingQueue` (which effectively removes the final modifier from the `capacity` field of `LinkedBlockingQueue` to make it variable). -最终实现的可动态修改线程池参数效果如下。👏👏👏 +The resulting dynamic modification of thread pool parameters is shown below.👏👏👏 -![动态配置线程池参数最终效果](https://oss.javaguide.cn/github/javaguide/java/concurrent/meituan-dynamically-configuring-thread-pool-parameters.png) +![Final Effect of Dynamically Configuring Thread Pool Parameters](https://oss.javaguide.cn/github/javaguide/java/concurrent/meituan-dynamically-configuring-thread-pool-parameters.png) -如果我们的项目也想要实现这种效果的话,可以借助现成的开源项目: +If you would like to achieve this effect in your project as well, you can leverage existing open-source projects: -- **[Hippo4j](https://github.com/opengoofy/hippo4j)**:异步线程池框架,支持线程池动态变更&监控&报警,无需修改代码轻松引入。支持多种使用模式,轻松引入,致力于提高系统运行保障能力。 -- **[Dynamic TP](https://github.com/dromara/dynamic-tp)**:轻量级动态线程池,内置监控告警功能,集成三方中间件线程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通过 SPI 自定义实现)。 +- **[Hippo4j](https://github.com/opengoofy/hippo4j)**: An asynchronous thread pool framework that supports dynamic changes, monitoring, and alerting for thread pools without modifying code, it supports multiple usage modes and aims to enhance system operational reliability. +- **[Dynamic TP](https://github.com/dromara/dynamic-tp)**: A lightweight dynamic thread pool with built-in monitoring and alert features, integrating third-party middleware thread pool management, compatible with mainstream configuration centers (already supports Nacos, Apollo, Zookeeper, Consul, Etcd, and can be customized through SPI). -## 6、别忘记关闭线程池 +## 6. Don't Forget to Shut Down the Thread Pool -当线程池不再需要使用时,应该显式地关闭线程池,释放线程资源。 +When the thread pool is no longer needed, it should be explicitly shut down to release thread resources. -线程池提供了两个关闭方法: +The thread pool provides two shutdown methods: -- **`shutdown()`** :关闭线程池,线程池的状态变为 `SHUTDOWN`。线程池不再接受新任务了,但是队列里的任务得执行完毕。 -- **`shutdownNow()`** :关闭线程池,线程池的状态变为 `STOP`。线程池会终止当前正在运行的任务,停止处理排队的任务并返回正在等待执行的 List。 +- **`shutdown()`**: Shuts down the thread pool, changing its state to `SHUTDOWN`. The thread pool will no longer accept new tasks, but existing tasks in the queue will be completed. +- **`shutdownNow()`**: Shuts down the thread pool, changing its state to `STOP`. The thread pool will stop currently running tasks, cease handling queued tasks, and return a list of tasks waiting to execute. -调用完 `shutdownNow` 和 `shuwdown` 方法后,并不代表线程池已经完成关闭操作,它只是异步的通知线程池进行关闭处理。如果要同步等待线程池彻底关闭后才继续往下执行,需要调用`awaitTermination`方法进行同步等待。 +After calling `shutdownNow` and `shutdown`, it does not mean that the thread pool has completed its shutdown operations; it only asynchronously notifies the thread pool to handle the shutdown. To wait synchronously until the thread pool has completely shut down before proceeding, the `awaitTermination` method must be invoked to wait. -在调用 `awaitTermination()` 方法时,应该设置合理的超时时间,以避免程序长时间阻塞而导致性能问题。另外。由于线程池中的任务可能会被取消或抛出异常,因此在使用 `awaitTermination()` 方法时还需要进行异常处理。`awaitTermination()` 方法会抛出 `InterruptedException` 异常,需要捕获并处理该异常,以避免程序崩溃或者无法正常退出。 +When calling `awaitTermination()`, a reasonable timeout should be set to avoid potential performance issues from the program blocking for long durations. Additionally, as tasks in the thread pool may be canceled or thrown exceptions, exception handling must be implemented while using `awaitTermination()`. The `awaitTermination()` method may throw an `InterruptedException` that should be caught and handled to prevent program crashes or failure to exit normally. ```java // ... -// 关闭线程池 +// Shut down the thread pool executor.shutdown(); try { - // 等待线程池关闭,最多等待5分钟 + // Wait for the thread pool to shut down, timeout of 5 minutes if (!executor.awaitTermination(5, TimeUnit.MINUTES)) { - // 如果等待超时,则打印日志 - System.err.println("线程池未能在5分钟内完全关闭"); + // Log if waiting times out + System.err.println("The thread pool did not shut down completely within 5 minutes"); } } catch (InterruptedException e) { - // 异常处理 + // Exception handling } ``` -## 7、线程池尽量不要放耗时任务 +## 7. Avoid Long-Running Tasks in Thread Pools -线程池本身的目的是为了提高任务执行效率,避免因频繁创建和销毁线程而带来的性能开销。如果将耗时任务提交到线程池中执行,可能会导致线程池中的线程被长时间占用,无法及时响应其他任务,甚至会导致线程池崩溃或者程序假死。 +The primary goal of thread pools is to enhance task execution efficiency, eliminating the performance overhead caused by frequently creating and destroying threads. If long-running tasks are submitted to the thread pool for execution, it may result in threads in the pool being occupied for extended periods, making it unable to respond promptly to other tasks, potentially causing the thread pool to crash or the program to hang. -因此,在使用线程池时,我们应该尽量避免将耗时任务提交到线程池中执行。对于一些比较耗时的操作,如网络请求、文件读写等,可以采用 `CompletableFuture` 等其他异步操作的方式来处理,以避免阻塞线程池中的线程。 +Therefore, when using thread pools, we should avoid submitting long-running tasks to them whenever possible. For relatively time-consuming operations, such as network requests or file I/O, using other asynchronous mechanisms, like `CompletableFuture`, is advisable to avoid blocking threads in the pool. -## 8、线程池使用的一些小坑 +## 8. Some Pitfalls of Using Thread Pools -### 重复创建线程池的坑 +### The Pitfall of Repeatedly Creating Thread Pools -线程池是可以复用的,一定不要频繁创建线程池比如一个用户请求到了就单独创建一个线程池。 +Thread pools can be reused, so avoid frequently creating new thread pools, such as creating a dedicated thread pool for each user request. ```java @GetMapping("wrong") public String wrong() throws InterruptedException { - // 自定义线程池 - ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,1L,TimeUnit.SECONDS,new ArrayBlockingQueue<>(100),new ThreadPoolExecutor.CallerRunsPolicy()); + // Custom thread pool + ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 1L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy()); - // 处理任务 + // Handle task executor.execute(() -> { // ...... - } + }); return "OK"; } ``` -出现这种问题的原因还是对于线程池认识不够,需要加强线程池的基础知识。 +This issue arises from a lack of understanding of thread pools. There is a need to strengthen foundational knowledge about thread pools. -### Spring 内部线程池的坑 +### The Pitfall of Spring's Internal Thread Pool -使用 Spring 内部线程池时,一定要手动自定义线程池,配置合理的参数,不然会出现生产问题(一个请求创建一个线程)。 +When using Spring's internal thread pool, it’s essential to manually define it with appropriate settings; otherwise, it can lead to production issues (like a thread being created per request). ```java @Configuration @@ -268,35 +268,35 @@ public class ThreadPoolExecutorConfig { @Bean(name="threadPoolExecutor") public Executor threadPoolExecutor(){ ThreadPoolTaskExecutor threadPoolExecutor = new ThreadPoolTaskExecutor(); - int processNum = Runtime.getRuntime().availableProcessors(); // 返回可用处理器的Java虚拟机的数量 + int processNum = Runtime.getRuntime().availableProcessors(); // Returns the number of available processors int corePoolSize = (int) (processNum / (1 - 0.2)); int maxPoolSize = (int) (processNum / (1 - 0.5)); - threadPoolExecutor.setCorePoolSize(corePoolSize); // 核心池大小 - threadPoolExecutor.setMaxPoolSize(maxPoolSize); // 最大线程数 - threadPoolExecutor.setQueueCapacity(maxPoolSize * 1000); // 队列程度 + threadPoolExecutor.setCorePoolSize(corePoolSize); // Core pool size + threadPoolExecutor.setMaxPoolSize(maxPoolSize); // Max thread count + threadPoolExecutor.setQueueCapacity(maxPoolSize * 1000); // Queue length threadPoolExecutor.setThreadPriority(Thread.MAX_PRIORITY); threadPoolExecutor.setDaemon(false); - threadPoolExecutor.setKeepAliveSeconds(300);// 线程空闲时间 - threadPoolExecutor.setThreadNamePrefix("test-Executor-"); // 线程名字前缀 + threadPoolExecutor.setKeepAliveSeconds(300); // Thread idle time + threadPoolExecutor.setThreadNamePrefix("test-Executor-"); // Thread name prefix return threadPoolExecutor; } } ``` -### 线程池和 ThreadLocal 共用的坑 +### The Pitfall of Using Thread Pools with ThreadLocal -线程池和 `ThreadLocal`共用,可能会导致线程从`ThreadLocal`获取到的是旧值/脏数据。这是因为线程池会复用线程对象,与线程对象绑定的类的静态属性 `ThreadLocal` 变量也会被重用,这就导致一个线程可能获取到其他线程的`ThreadLocal` 值。 +Using thread pools together with `ThreadLocal` can lead to threads obtaining old values or dirty data. This happens because thread pools reuse thread objects; static attributes of classes bound to thread objects, which include `ThreadLocal` variables, are also reused, causing a thread to potentially access `ThreadLocal` values from other threads. -不要以为代码中没有显示使用线程池就不存在线程池了,像常用的 Web 服务器 Tomcat 处理任务为了提高并发量,就使用到了线程池,并且使用的是基于原生 Java 线程池改进完善得到的自定义线程池。 +Don't assume that not explicitly using thread pools in your code means there are none; common web servers like Tomcat also utilize thread pools to enhance concurrency and have improved their native Java thread pooling. -当然了,你可以将 Tomcat 设置为单线程处理任务。不过,这并不合适,会严重影响其处理任务的速度。 +Of course, you could configure Tomcat to process tasks using a single thread. However, this would not be appropriate, resulting in a serious impact on task processing speed. ```properties server.tomcat.max-threads=1 ``` -解决上述问题比较建议的办法是使用阿里巴巴开源的 `TransmittableThreadLocal`(`TTL`)。`TransmittableThreadLocal`类继承并加强了 JDK 内置的`InheritableThreadLocal`类,在使用线程池等会池化复用线程的执行组件情况下,提供`ThreadLocal`值的传递功能,解决异步执行时上下文传递的问题。 +A recommended solution for addressing the aforementioned issues is to use Alibaba's open-source `TransmittableThreadLocal` (`TTL`). The `TransmittableThreadLocal` class extends and enhances JDK's built-in `InheritableThreadLocal`, providing value transfer for `ThreadLocal` when using thread pools or other thread-recycling components, effectively solving the context propagation issue during asynchronous executions. -`TransmittableThreadLocal` 项目地址: 。 +`TransmittableThreadLocal` project address: . - + \ No newline at end of file diff --git a/docs/java/concurrent/java-thread-pool-summary.md b/docs/java/concurrent/java-thread-pool-summary.md index 47a1f916de2..6ec6a94c8b8 100644 --- a/docs/java/concurrent/java-thread-pool-summary.md +++ b/docs/java/concurrent/java-thread-pool-summary.md @@ -1,102 +1,102 @@ --- -title: Java 线程池详解 +title: Detailed Explanation of Java Thread Pool category: Java tag: - - Java并发 + - Java Concurrency --- -池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。 +The pool technology is something everyone is familiar with, such as thread pools, database connection pools, HTTP connection pools, etc., all of which are applications of this concept. The main idea of pooling technology is to reduce the overhead of acquiring resources each time and to improve the utilization of resources. -这篇文章我会详细介绍一下线程池的基本概念以及核心原理。 +In this article, I will elaborate on the basic concepts and core principles of thread pools. -## 线程池介绍 +## Introduction to Thread Pools -顾名思义,线程池就是管理一系列线程的资源池,其提供了一种限制和管理线程资源的方式。每个线程池还维护一些基本统计信息,例如已完成任务的数量。 +As the name suggests, a thread pool is a resource pool that manages a series of threads, providing a way to limit and manage thread resources. Each thread pool also maintains some basic statistical information, such as the number of completed tasks. -这里借用《Java 并发编程的艺术》书中的部分内容来总结一下使用线程池的好处: +Here is a summary of the benefits of using thread pools derived from the book "Java Concurrency in Practice": -- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度**。当任务到达时,任务可以不需要等到线程创建就能立即执行。 -- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 +- **Reduced resource consumption**. Reducing the overhead caused by thread creation and destruction by reusing already created threads. +- **Improved response speed**. When tasks arrive, they can be executed immediately without waiting for thread creation. +- **Improved manageability of threads**. Threads are a scarce resource; if created unlimitedly, they will not only consume system resources but also decrease system stability. Using a thread pool allows for unified allocation, tuning, and monitoring. -**线程池一般用于执行多个不相关联的耗时任务,没有多线程的情况下,任务顺序执行,使用了线程池的话可让多个不相关联的任务同时执行。** +**Thread pools are generally used to execute multiple unrelated time-consuming tasks. Without multithreading, tasks are executed sequentially; using a thread pool allows multiple unrelated tasks to be executed simultaneously.** -## Executor 框架介绍 +## Introduction to the Executor Framework -`Executor` 框架是 Java5 之后引进的,在 Java 5 之后,通过 `Executor` 来启动线程比使用 `Thread` 的 `start` 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。 +The `Executor` framework was introduced in Java 5. Using `Executor` to start threads is better than using the `start` method of `Thread`, as it is more manageable and efficient (using thread pools saves overhead). A key point is that it helps avoid the "this escape" problem. -> this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用,调用尚未构造完全的对象的方法可能引发令人疑惑的错误。 +> "This escape" refers to the scenario where other threads hold references to the object before the constructor returns, which may cause confusing errors when methods of the not fully constructed object are called. -`Executor` 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,`Executor` 框架让并发编程变得更加简单。 +The `Executor` framework not only includes management of thread pools, but also provides thread factories, queues, and rejection policies, making concurrent programming simpler. -`Executor` 框架结构主要由三大部分组成: +The structure of the `Executor` framework mainly consists of three parts: -**1、任务(`Runnable` /`Callable`)** +**1. Task (`Runnable` / `Callable`)** -执行任务需要实现的 **`Runnable` 接口** 或 **`Callable`接口**。**`Runnable` 接口**或 **`Callable` 接口** 实现类都可以被 **`ThreadPoolExecutor`** 或 **`ScheduledThreadPoolExecutor`** 执行。 +The tasks to be executed need to implement the **`Runnable` interface** or the **`Callable` interface**. Implementations of **`Runnable`** or **`Callable`** can be executed by **`ThreadPoolExecutor`** or **`ScheduledThreadPoolExecutor`**. -**2、任务的执行(`Executor`)** +**2. Task execution (`Executor`)** -如下图所示,包括任务执行机制的核心接口 **`Executor`** ,以及继承自 `Executor` 接口的 **`ExecutorService` 接口。`ThreadPoolExecutor`** 和 **`ScheduledThreadPoolExecutor`** 这两个关键类实现了 **`ExecutorService`** 接口。 +As shown in the diagram below, it includes the core interface of the task execution mechanism, **`Executor`**, and the **`ExecutorService`** interface that inherits from the `Executor` interface. The two key classes, **`ThreadPoolExecutor`** and **`ScheduledThreadPoolExecutor`**, implement the **`ExecutorService`** interface. ![](https://oss.javaguide.cn/github/javaguide/java/concurrent/executor-class-diagram.png) -这里提了很多底层的类关系,但是,实际上我们需要更多关注的是 `ThreadPoolExecutor` 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。 +While many underlying class relationships are mentioned here, in practice, we need to pay more attention to the `ThreadPoolExecutor` class, which is frequently used when working with thread pools. -**注意:** 通过查看 `ScheduledThreadPoolExecutor` 源代码我们发现 `ScheduledThreadPoolExecutor` 实际上是继承了 `ThreadPoolExecutor` 并实现了 `ScheduledExecutorService` ,而 `ScheduledExecutorService` 又实现了 `ExecutorService`,正如我们上面给出的类关系图显示的一样。 +**Note:** By examining the source code of `ScheduledThreadPoolExecutor`, we find that it actually inherits from `ThreadPoolExecutor` and implements `ScheduledExecutorService`, while `ScheduledExecutorService` implements `ExecutorService`, as shown in the class relationship diagram provided above. -`ThreadPoolExecutor` 类描述: +Description of the `ThreadPoolExecutor` class: ```java -//AbstractExecutorService实现了ExecutorService接口 +//AbstractExecutorService implements ExecutorService interface public class ThreadPoolExecutor extends AbstractExecutorService ``` -`ScheduledThreadPoolExecutor` 类描述: +Description of the `ScheduledThreadPoolExecutor` class: ```java -//ScheduledExecutorService继承ExecutorService接口 +//ScheduledExecutorService inherits ExecutorService interface public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService ``` -**3、异步计算的结果(`Future`)** +**3. Asynchronous calculation results (`Future`)** -**`Future`** 接口以及 `Future` 接口的实现类 **`FutureTask`** 类都可以代表异步计算的结果。 +Both the **`Future`** interface and its implementation class **`FutureTask`** represent the results of asynchronous calculations. -当我们把 **`Runnable`接口** 或 **`Callable` 接口** 的实现类提交给 **`ThreadPoolExecutor`** 或 **`ScheduledThreadPoolExecutor`** 执行。(调用 `submit()` 方法时会返回一个 **`FutureTask`** 对象) +When we submit implementations of the **`Runnable` interface** or **`Callable` interface** to **`ThreadPoolExecutor`** or **`ScheduledThreadPoolExecutor`** for execution, (calling the `submit()` method will return a **`FutureTask`** object). -**`Executor` 框架的使用示意图**: +**Illustration of using the `Executor` framework**: -![Executor 框架的使用示意图](./images/java-thread-pool-summary/Executor框架的使用示意图.png) +![Illustration of Using the Executor Framework](./images/java-thread-pool-summary/Executor%E6%A1%86%E6%9E%B6%E7%9A%84%E4%BD%BF%E7%94%A8%E7%A4%BA%E6%84%8F%E5%9B%BE.png) -1. 主线程首先要创建实现 `Runnable` 或者 `Callable` 接口的任务对象。 -2. 把创建完成的实现 `Runnable`/`Callable`接口的 对象直接交给 `ExecutorService` 执行: `ExecutorService.execute(Runnable command)`)或者也可以把 `Runnable` 对象或`Callable` 对象提交给 `ExecutorService` 执行(`ExecutorService.submit(Runnable task)`或 `ExecutorService.submit(Callable task)`)。 -3. 如果执行 `ExecutorService.submit(…)`,`ExecutorService` 将返回一个实现`Future`接口的对象(我们刚刚也提到过了执行 `execute()`方法和 `submit()`方法的区别,`submit()`会返回一个 `FutureTask 对象)。由于 FutureTask` 实现了 `Runnable`,我们也可以创建 `FutureTask`,然后直接交给 `ExecutorService` 执行。 -4. 最后,主线程可以执行 `FutureTask.get()`方法来等待任务执行完成。主线程也可以执行 `FutureTask.cancel(boolean mayInterruptIfRunning)`来取消此任务的执行。 +1. The main thread first needs to create a task object that implements `Runnable` or `Callable`. +1. The completed object that implements `Runnable` / `Callable` is submitted directly to the `ExecutorService` for execution: `ExecutorService.execute(Runnable command)`; alternatively, you can submit the `Runnable` object or the `Callable` object to `ExecutorService` for execution (`ExecutorService.submit(Runnable task)` or `ExecutorService.submit(Callable task)`). +1. If executing `ExecutorService.submit(...)`, `ExecutorService` will return an object that implements the `Future` interface (as mentioned earlier, there is a difference between `execute()` and `submit()`: `submit()` will return a `FutureTask` object). Since `FutureTask` implements `Runnable`, we can also create `FutureTask` and submit it directly to `ExecutorService`. +1. Finally, the main thread can execute the `FutureTask.get()` method to wait for the task execution to complete. The main thread can also execute `FutureTask.cancel(boolean mayInterruptIfRunning)` to cancel the execution of this task. -## ThreadPoolExecutor 类介绍(重要) +## Introduction to ThreadPoolExecutor Class (Important) -线程池实现类 `ThreadPoolExecutor` 是 `Executor` 框架最核心的类。 +The thread pool implementation class `ThreadPoolExecutor` is the core class of the `Executor` framework. -### 线程池参数分析 +### Analysis of Thread Pool Parameters -`ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么)。 +The `ThreadPoolExecutor` class provides four constructors. Let's look at the longest one; the other three are based on this constructor (the other constructors essentially provide default parameters, such as the default rejection strategy). ```java /** - * 用给定的初始参数创建一个新的ThreadPoolExecutor。 + * Creates a new ThreadPoolExecutor with the given initial parameters. */ - public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量 - int maximumPoolSize,//线程池的最大线程数 - long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间 - TimeUnit unit,//时间单位 - BlockingQueue workQueue,//任务队列,用来储存等待执行任务的队列 - ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可 - RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务 + public ThreadPoolExecutor(int corePoolSize,//the core number of threads in the pool + int maximumPoolSize,//the maximum number of threads in the pool + long keepAliveTime,//the maximum time a surplus idle thread can survive when the number of threads exceeds the core number + TimeUnit unit,//time unit + BlockingQueue workQueue,//task queue used to store waiting tasks + ThreadFactory threadFactory,//thread factory used to create threads, generally the default is fine + RejectedExecutionHandler handler//rejection policy, we can customize the strategy to handle tasks when submitted tasks are too many and cannot be processed in time ) { if (corePoolSize < 0 || maximumPoolSize <= 0 || @@ -114,37 +114,37 @@ public class ScheduledThreadPoolExecutor } ``` -下面这些参数非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。 +The following parameters are very important and you will definitely use them during your use of the thread pool, so be sure to remember them. -`ThreadPoolExecutor` 3 个最重要的参数: +The three most important parameters of `ThreadPoolExecutor`: -- `corePoolSize` : 任务队列未达到队列容量时,最大可以同时运行的线程数量。 -- `maximumPoolSize` : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- `workQueue`: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 +- `corePoolSize`: The maximum number of threads that can run simultaneously when the task queue does not reach capacity. +- `maximumPoolSize`: When the number of tasks in the queue reaches its capacity, the current number of threads that can run simultaneously increases to the maximum number of threads. +- `workQueue`: When a new task arrives, it first checks whether the current running thread count has reached the core thread count. If it has, the new task will be stored in the queue. -`ThreadPoolExecutor`其他常见参数 : +Other common parameters of `ThreadPoolExecutor`: -- `keepAliveTime`:线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁。 -- `unit` : `keepAliveTime` 参数的时间单位。 -- `threadFactory` :executor 创建新线程的时候会用到。 -- `handler` :拒绝策略(后面会单独详细介绍一下)。 +- `keepAliveTime`: When the number of threads in the pool exceeds `corePoolSize`, if no new tasks are submitted, the surplus idle threads will not be destroyed immediately, but will wait until the wait time exceeds `keepAliveTime` before being reclaimed and destroyed. +- `unit`: The time unit for the `keepAliveTime` parameter. +- `threadFactory`: Will be used when the executor creates new threads. +- `handler`: Rejection policy (which will be introduced in detail later). -下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》): +The following diagram can enhance your understanding of the relationship between various parameters in the thread pool (image source: "Java Performance Optimization in Practice"): -![线程池各个参数的关系](https://oss.javaguide.cn/github/javaguide/java/concurrent/relationship-between-thread-pool-parameters.png) +![Relationship Between Various Parameters of Thread Pool](https://oss.javaguide.cn/github/javaguide/java/concurrent/relationship-between-thread-pool-parameters.png) -**`ThreadPoolExecutor` 拒绝策略定义:** +**Definition of `ThreadPoolExecutor` Rejection Policies:** -如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,`ThreadPoolExecutor` 定义一些策略: +When the number of currently running threads reaches the maximum thread quantity and the queue is also full of tasks, `ThreadPoolExecutor` defines several strategies: -- `ThreadPoolExecutor.AbortPolicy`:抛出 `RejectedExecutionException`来拒绝新任务的处理。 -- `ThreadPoolExecutor.CallerRunsPolicy`:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 -- `ThreadPoolExecutor.DiscardPolicy`:不处理新任务,直接丢弃掉。 -- `ThreadPoolExecutor.DiscardOldestPolicy`:此策略将丢弃最早的未处理的任务请求。 +- `ThreadPoolExecutor.AbortPolicy`: Throws `RejectedExecutionException` to refuse processing new tasks. +- `ThreadPoolExecutor.CallerRunsPolicy`: The caller's thread runs the rejected task, meaning the task is run directly in the thread that called `execute()`. If the executor is shut down, this task will be discarded. Therefore, this strategy will slow down the speed of submitting new tasks, affecting the overall performance of the program. If your application can withstand this delay and you require every task request to be executed, you can choose this strategy. +- `ThreadPoolExecutor.DiscardPolicy`: Does not process new tasks, directly discarding them. +- `ThreadPoolExecutor.DiscardOldestPolicy`: This strategy will discard the oldest unprocessed task request. -举个例子: +For example: -举个例子:Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 拒绝策略来配置线程池的时候,默认使用的是 `AbortPolicy`。在这种拒绝策略下,如果队列满了,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 异常来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。如果不想丢弃任务的话,可以使用`CallerRunsPolicy`。`CallerRunsPolicy` 和其他的几个策略不同,它既不会抛弃任务,也不会抛出异常,而是将任务回退给调用者,使用调用者的线程来执行任务 +Spring, through `ThreadPoolTaskExecutor`, or when we directly create a thread pool using the `ThreadPoolExecutor` constructor, if we do not specify the `RejectedExecutionHandler` rejection policy to configure the thread pool, the default will use `AbortPolicy`. Under this rejection policy, if the queue is full, `ThreadPoolExecutor` will throw a `RejectedExecutionException` exception to refuse new tasks, meaning you will lose the processing of this task. If you do not want to lose tasks, you can use `CallerRunsPolicy`. Unlike other strategies, `CallerRunsPolicy` does not discard tasks or throw exceptions; instead, it falls back the tasks to the caller and executes them using the caller's thread. ```java public static class CallerRunsPolicy implements RejectedExecutionHandler { @@ -153,91 +153,87 @@ public static class CallerRunsPolicy implements RejectedExecutionHandler { public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { - // 直接主线程执行,而不是线程池中的线程执行 + // Execute in the main thread instead of in the thread pool's thread r.run(); } } } ``` -### 线程池创建的两种方式 +### Two Ways to Create a Thread Pool -在 Java 中,创建线程池主要有两种方式: +In Java, there are mainly two ways to create thread pools: -**方式一:通过 `ThreadPoolExecutor` 构造函数直接创建 (推荐)** +**Method 1: Directly creating through the `ThreadPoolExecutor` constructor (Recommended)** ![](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpoolexecutor-construtors.png) -这是最推荐的方式,因为它允许开发者明确指定线程池的核心参数,对线程池的运行行为有更精细的控制,从而避免资源耗尽的风险。 +This is the most recommended method because it allows developers to explicitly specify the core parameters of the thread pool, enabling finer control over the behavior of the thread pool, thus avoiding the risk of resource exhaustion. -**方式二:通过 `Executors` 工具类创建 (不推荐用于生产环境)** +**Method 2: Creating through the `Executors` utility class (Not recommended for production environments)** -`Executors`工具类提供的创建线程池的方法如下图所示: +The methods provided by the `Executors` utility class to create thread pools are shown in the diagram below: ![](https://oss.javaguide.cn/github/javaguide/java/concurrent/executors-new-thread-pool-methods.png) -可以看出,通过`Executors`工具类可以创建多种类型的线程池,包括: +As can be seen, the `Executors` utility class can create various types of thread pools, including: -- `FixedThreadPool`:固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 -- `SingleThreadExecutor`: 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 -- `CachedThreadPool`: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 -- `ScheduledThreadPool`:给定的延迟后运行任务或者定期执行任务的线程池。 +- `FixedThreadPool`: A thread pool with a fixed number of threads. The number of threads in this thread pool always remains the same. When a new task is submitted and there are idle threads, it executes immediately. If not, new tasks will be temporarily held in a task queue until threads become available to process tasks from the queue. +- `SingleThreadExecutor`: A thread pool with only one thread. If more than one task is submitted to this thread pool, the tasks will be saved in a task queue and executed in the order they were submitted as threads become idle. +- `CachedThreadPool`: A thread pool that can dynamically adjust the number of threads based on actual conditions. The number of threads in this thread pool is uncertain, but if there are idle threads available for reuse, it will prioritize using them. If all threads are busy and new tasks are submitted, new threads will be created to handle the tasks. All threads will return to the pool for reuse after completing their current tasks. +- `ScheduledThreadPool`: A thread pool that runs tasks after a given delay or executes tasks periodically. -《阿里巴巴 Java 开发手册》强制线程池不允许使用 `Executors` 去创建,而是通过 `ThreadPoolExecutor` 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 +The "Alibaba Java Development Manual" mandates that thread pools should not be created using the `Executors` utility but rather through the `ThreadPoolExecutor` constructor. This practice makes it clearer for developers regarding the operational rules of the thread pool and mitigates the risk of resource exhaustion. -`Executors` 返回线程池对象的弊端如下(后文会详细介绍到): +The drawbacks of using `Executors` to return thread pool objects are as follows (which will be detailed later): -- `FixedThreadPool` 和 `SingleThreadExecutor`:使用的是阻塞队列 `LinkedBlockingQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可以看作是无界的,可能堆积大量的请求,从而导致 OOM。 -- `CachedThreadPool`:使用的是同步队列 `SynchronousQueue`, 允许创建的线程数量为 `Integer.MAX_VALUE` ,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM。 -- `ScheduledThreadPool` 和 `SingleThreadScheduledExecutor`:使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。 +- `FixedThreadPool` and `SingleThreadExecutor`: Use the blocking queue `LinkedBlockingQueue`, with a maximum length of `Integer.MAX_VALUE`, considered unbounded; tasks may pile up significantly, leading to OOM. +- `CachedThreadPool`: Uses the synchronous queue `SynchronousQueue`, with the allowed number of threads being `Integer.MAX_VALUE`, which may create too many threads if task numbers rise rapidly, leading to OOM. +- `ScheduledThreadPool` and `SingleThreadScheduledExecutor`: Use an unbounded delay blocking queue `DelayedWorkQueue`, with a maximum length of `Integer.MAX_VALUE`, which may lead to excessive task pile-up and cause OOM. ```java public static ExecutorService newFixedThreadPool(int nThreads) { - // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的 - return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue()); - + // LinkedBlockingQueue's default length is Integer.MAX_VALUE, considered unbounded + return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); } public static ExecutorService newSingleThreadExecutor() { - // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的 - return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue())); - + // LinkedBlockingQueue's default length is Integer.MAX_VALUE, considered unbounded + return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())); } -// 同步队列 SynchronousQueue,没有容量,最大线程数是 Integer.MAX_VALUE` +// SynchronousQueue with no capacity, max threads is Integer.MAX_VALUE public static ExecutorService newCachedThreadPool() { - - return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue()); - + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); } -// DelayedWorkQueue(延迟阻塞队列) +// DelayedWorkQueue (Delay blocking queue) public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } + public ScheduledThreadPoolExecutor(int corePoolSize) { - super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, - new DelayedWorkQueue()); + super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); } ``` -### 线程池常用的阻塞队列总结 +### Summary of Common Blocking Queues Used in Thread Pools -新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 +When new tasks arrive, it first checks whether the current running thread count has reached the core thread count. If it has, the new task will be stored in the queue. -不同的线程池会选用不同的阻塞队列,我们可以结合内置线程池来分析。 +Different thread pools use different blocking queues, and we can analyze this in conjunction with built-in thread pools. -- 容量为 `Integer.MAX_VALUE` 的 `LinkedBlockingQueue`(无界队列):`FixedThreadPool` 和 `SingleThreadExector` 。`FixedThreadPool`最多只能创建核心线程数的线程(核心线程数和最大线程数相等),`SingleThreadExector`只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满。 -- `SynchronousQueue`(同步队列):`CachedThreadPool` 。`SynchronousQueue` 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,`CachedThreadPool` 的最大线程数是 `Integer.MAX_VALUE` ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。 -- `DelayedWorkQueue`(延迟阻塞队列):`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor` 。`DelayedWorkQueue` 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。`DelayedWorkQueue` 添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 `Integer.MAX_VALUE`,所以最多只能创建核心线程数的线程。 +- The `LinkedBlockingQueue` with a capacity of `Integer.MAX_VALUE` (an unbounded queue): used by `FixedThreadPool` and `SingleThreadExecutor`. `FixedThreadPool` can create a maximum of core thread number threads (core and max number of threads are equal), while `SingleThreadExecutor` can only create one thread (both core and maximum thread numbers are 1), so their task queue will never fill up. +- `SynchronousQueue` (synchronous queue): used by `CachedThreadPool`. `SynchronousQueue` has no capacity and does not store elements, ensuring that if there are available threads, they will handle submitted tasks; otherwise, a new thread will be created to process the task. Thus, the maximum thread count for `CachedThreadPool` is `Integer.MAX_VALUE`, meaning the thread count can potentially expand indefinitely, which may lead to OOM. +- `DelayedWorkQueue` (delay blocking queue): used by `ScheduledThreadPool` and `SingleThreadScheduledExecutor`. The internal elements of `DelayedWorkQueue` are sorted not by the insertion time but by the length of the delay, utilizing a heap data structure to guarantee that each dequeued task is the one with the earliest execution time in the queue. When full, `DelayedWorkQueue` automatically expands its original capacity by 1/2, meaning it will never block, with a maximum expansion of `Integer.MAX_VALUE`, so it can create at maximum the number of core threads. -## 线程池原理分析(重要) +## Analysis of Thread Pool Principles (Important) -我们上面讲解了 `Executor`框架以及 `ThreadPoolExecutor` 类,下面让我们实战一下,来通过写一个 `ThreadPoolExecutor` 的小 Demo 来回顾上面的内容。 +We previously explained the `Executor` framework and the `ThreadPoolExecutor` class; let's now practically review the above content by writing a small demo to illustrate `ThreadPoolExecutor`. -### 线程池示例代码 +### Thread Pool Example Code -首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们后面会介绍两者的区别。) +First, we create an implementation of the `Runnable` interface (it can also be a `Callable` interface, and we will later introduce the differences between the two). `MyRunnable.java` @@ -245,7 +241,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { import java.util.Date; /** - * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。 + * This is a simple Runnable class that takes approximately 5 seconds to execute its task. * @author shuang.kou */ public class MyRunnable implements Runnable { @@ -279,7 +275,7 @@ public class MyRunnable implements Runnable { ``` -编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。 +Next, we write a test program, where we use the method recommended by Alibaba to create the thread pool with custom parameters through `ThreadPoolExecutor`. `ThreadPoolExecutorDemo.java` @@ -294,10 +290,11 @@ public class ThreadPoolExecutorDemo { private static final int MAX_POOL_SIZE = 10; private static final int QUEUE_CAPACITY = 100; private static final Long KEEP_ALIVE_TIME = 1L; + public static void main(String[] args) { - //使用阿里巴巴推荐的创建线程池的方式 - //通过ThreadPoolExecutor构造函数自定义参数创建 + // Creating thread pool using the method recommended by Alibaba + // Customizing by ThreadPoolExecutor constructor ThreadPoolExecutor executor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, @@ -307,12 +304,12 @@ public class ThreadPoolExecutorDemo { new ThreadPoolExecutor.CallerRunsPolicy()); for (int i = 0; i < 10; i++) { - //创建WorkerThread对象(WorkerThread类实现了Runnable 接口) + // Create a WorkerThread object (WorkerThread class implements Runnable interface) Runnable worker = new MyRunnable("" + i); - //执行Runnable + // Execute Runnable executor.execute(worker); } - //终止线程池 + // Terminate the thread pool executor.shutdown(); while (!executor.isTerminated()) { } @@ -322,16 +319,16 @@ public class ThreadPoolExecutorDemo { ``` -可以看到我们上面的代码指定了: +We can see that in the code above, we specified: -- `corePoolSize`: 核心线程数为 5。 -- `maximumPoolSize`:最大线程数 10 -- `keepAliveTime` : 等待时间为 1L。 -- `unit`: 等待时间的单位为 TimeUnit.SECONDS。 -- `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100; -- `handler`:拒绝策略为 `CallerRunsPolicy`。 +- `corePoolSize`: Core thread count is 5. +- `maximumPoolSize`: Maximum thread count is 10. +- `keepAliveTime`: Wait time is 1L. +- `unit`: Waiting time unit is TimeUnit.SECONDS. +- `workQueue`: Task queue is `ArrayBlockingQueue` with a capacity of 100. +- `handler`: Rejection policy is `CallerRunsPolicy`. -**输出结构**: +**Output Structure**: ```plain pool-1-thread-3 Start. Time = Sun Apr 12 11:14:37 CST 2020 @@ -354,104 +351,102 @@ pool-1-thread-4 End. Time = Sun Apr 12 11:14:47 CST 2020 pool-1-thread-5 End. Time = Sun Apr 12 11:14:47 CST 2020 pool-1-thread-3 End. Time = Sun Apr 12 11:14:47 CST 2020 pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020 -Finished all threads // 任务全部执行完了才会跳出来,因为executor.isTerminated()判断为true了才会跳出while循环,当且仅当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true - +Finished all threads // This message will only be displayed after all tasks are executed since the while loop will exit when executor.isTerminated() returns true, which happens only after shutdown() is called and all submitted tasks finish. ``` -### 线程池原理分析 +### Analysis of Thread Pool Principles -我们通过前面的代码输出结果可以看出:**线程池首先会先执行 5 个任务,然后这些任务有任务被执行完的话,就会去拿新的任务执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会) +Based on the output of our code, we can see that **the thread pool will first execute 5 tasks, and whenever a task is completed, it will go for new tasks to execute.** Please take a moment to analyze how this works based on the explanations above (independent reflection for a while). -现在,我们就分析上面的输出内容来简单分析一下线程池原理。 +Now, let's analyze the output to understand the thread pool principle. -为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。 在示例代码中,我们使用 `executor.execute(worker)`来提交一个任务到线程池中去。 +To understand the principle of the thread pool, we first need to analyze the `execute` method. In the example code, we use `executor.execute(worker)` to submit a task to the thread pool. -这个方法非常重要,下面我们来看看它的源码: +This method is very important; let's take a look at its source code: ```java - // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount) + // Holds the running state of the thread pool (runState) and the count of active threads in the pool (workerCount) private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); private static int workerCountOf(int c) { return c & CAPACITY; } - //任务队列 + // Task queue private final BlockingQueue workQueue; public void execute(Runnable command) { - // 如果任务为null,则抛出异常。 + // If task is null, throw exception. if (command == null) throw new NullPointerException(); - // ctl 中保存的线程池当前的一些状态信息 + // ctl holds the current state of the thread pool int c = ctl.get(); - // 下面会涉及到 3 步 操作 - // 1.首先判断当前线程池中执行的任务数量是否小于 corePoolSize - // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 + // The following involves 3 steps of operation + // 1. First check if the count of executing tasks is less than corePoolSize + // If it is, it will create a new thread through addWorker(command, true) and add the task to that thread; then, start the thread to execute the task. if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } - // 2.如果当前执行的任务数量大于等于 corePoolSize 的时候就会走到这里,表明创建新的线程失败。 - // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态并且队列可以加入任务,该任务才会被加入进去 + // 2. If the count of executing tasks is greater than or equal to corePoolSize, it indicates failure to create new threads. + // By checking isRunning method on thread pool status, and if the pool is in RUNNING state and the queue can accept tasks, the task will be added to the queue. if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); - // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。 + // Recheck the thread pool state; if it's not RUNNING, remove the task from the queue and try checking if all threads have completed execution, handling rejection strategy as well. if (!isRunning(recheck) && remove(command)) reject(command); - // 如果当前工作线程数量为0,新创建一个线程并执行。 + // If the current count of working threads is 0, create a new thread to execute. else if (workerCountOf(recheck) == 0) addWorker(null, false); } - //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 - // 传入 false 代表增加线程时判断当前线程数是否少于 maxPoolSize - //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。 + // 3. Call addWorker(command, false) to create a new thread, add the task to that thread, and start the thread to execute. + // Passing false means to check whether the current thread count is less than maxPoolSize when adding threads. + // If addWorker(command, false) fails, invoke reject() to execute the corresponding rejection policy. else if (!addWorker(command, false)) reject(command); } ``` -这里简单分析一下整个流程(对整个逻辑进行了简化,方便理解): +Let's simply analyze the entire process (the logic is simplified for ease of understanding): -1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。 -2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。 -3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。 -4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用`RejectedExecutionHandler.rejectedExecution()`方法。 +1. If the currently running thread count is less than the core thread count, it creates a new thread to execute the task. +1. If the current running thread count is equal to or greater than the core thread count but less than the maximum thread count, the task is added to the task queue for waiting execution. +1. If it fails to add the task to the queue (the queue is full), but the current running thread count is less than the maximum thread count, a new thread will be created to execute the task. +1. If the current running thread count has reached the maximum thread count, creating a new thread will exceed the maximum thread count for running threads, leading to the current task being rejected, causing the rejection strategy to call `RejectedExecutionHandler.rejectedExecution()`. -![图解线程池实现原理](https://oss.javaguide.cn/github/javaguide/java/concurrent/thread-pool-principle.png) +![Illustration of Thread Pool Implementation Principle](https://oss.javaguide.cn/github/javaguide/java/concurrent/thread-pool-principle.png) -在 `execute` 方法中,多次调用 `addWorker` 方法。`addWorker` 这个方法主要用来创建新的工作线程,如果返回 true 说明创建和启动工作线程成功,否则的话返回的就是 false。 +In the `execute` method, the `addWorker` method is called multiple times. The `addWorker` method is primarily used to create new working threads, returning true if successful; otherwise, it returns false. ```java - // 全局锁,并发操作必备 + // Global lock, essential for concurrent operations private final ReentrantLock mainLock = new ReentrantLock(); - // 跟踪线程池的最大大小,只有在持有全局锁mainLock的前提下才能访问此集合 + // Tracks the largest size of the thread pool, only accessible under the global lock mainLock private int largestPoolSize; - // 工作线程集合,存放线程池中所有的(活跃的)工作线程,只有在持有全局锁mainLock的前提下才能访问此集合 + // Set of working threads, stores all working threads (active) in the thread pool, only accessible under the global lock mainLock private final HashSet workers = new HashSet<>(); - //获取线程池状态 + // To get the thread pool state private static int runStateOf(int c) { return c & ~CAPACITY; } - //判断线程池的状态是否为 Running + // Checks if thread pool is in Running state private static boolean isRunning(int c) { return c < SHUTDOWN; } - /** - * 添加新的工作线程到线程池 - * @param firstTask 要执行 - * @param core参数为true的话表示使用线程池的基本大小,为false使用线程池最大大小 - * @return 添加成功就返回true否则返回false + * Adds a new worker thread to the thread pool + * @param firstTask The task to be executed + * @param core If true, use the pool's core size; if false, use the maximum size + * @return True if added successfully; false otherwise */ - private boolean addWorker(Runnable firstTask, boolean core) { + private boolean addWorker(Runnable firstTask, boolean core) { retry: for (;;) { - //这两句用来获取线程池的状态 + // These two lines are to get the thread pool status int c = ctl.get(); int rs = runStateOf(c); - // Check if queue empty only if necessary. + // Check if it is needed to check if the queue is empty. if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && @@ -459,66 +454,65 @@ Finished all threads // 任务全部执行完了才会跳出来,因为executo return false; for (;;) { - //获取线程池中工作的线程的数量 + // Get the number of working threads in the pool int wc = workerCountOf(c); - // core参数为false的话表明队列也满了,线程池大小变为 maximumPoolSize + // If core is false, the queue is also full, making the pool size become maximumPoolSize if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; - //原子操作将workcount的数量加1 + // Atomic operation will increment the work count by 1 if (compareAndIncrementWorkerCount(c)) break retry; - // 如果线程的状态改变了就再次执行上述操作 + // If the thread status changes, rerun the above operations c = ctl.get(); if (runStateOf(c) != rs) continue retry; // else CAS failed due to workerCount change; retry inner loop } } - // 标记工作线程是否启动成功 + // Mark the success of the worker thread's startup boolean workerStarted = false; - // 标记工作线程是否创建成功 + // Mark the success of the worker thread creation boolean workerAdded = false; Worker w = null; try { - w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { - // 加锁 + // Lock final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { - //获取线程池状态 + // Get the thread pool state int rs = runStateOf(ctl.get()); - //rs < SHUTDOWN 如果线程池状态依然为RUNNING,并且线程的状态是存活的话,就会将工作线程添加到工作线程集合中 - //(rs=SHUTDOWN && firstTask == null)如果线程池状态小于STOP,也就是RUNNING或者SHUTDOWN状态下,同时传入的任务实例firstTask为null,则需要添加到工作线程集合和启动新的Worker - // firstTask == null证明只新建线程而不执行任务 + // If the pool state is still RUNNING and the thread is alive, add the working thread to the workers collection + //(rs=SHUTDOWN && firstTask == null) Needs to be added to the workers collection and start a new Worker if the pool status is less than STOP and the incoming task instance firstTask is null + // firstTask == null indicates a new thread creation without executing a task if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); workers.add(w); - //更新当前工作线程的最大容量 + // Update the maximum number of working threads currently int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; - // 工作线程是否启动成功 + // Whether the worker thread started successfully workerAdded = true; } } finally { - // 释放锁 + // Unlock mainLock.unlock(); } - //// 如果成功添加工作线程,则调用Worker内部的线程实例t的Thread#start()方法启动真实的线程实例 + // If added successfully, invoke thread t's Thread#start() to start the actual thread instance if (workerAdded) { t.start(); - /// 标记线程启动成功 + // Mark the worker as started workerStarted = true; } } } finally { - // 线程启动失败,需要从工作线程中移除对应的Worker + // If it fails to start, remove the corresponding Worker from the working threads if (! workerStarted) addWorkerFailed(w); } @@ -526,21 +520,21 @@ Finished all threads // 任务全部执行完了才会跳出来,因为executo } ``` -更多关于线程池源码分析的内容推荐这篇文章:硬核干货:[4W 字从源码上分析 JUC 线程池 ThreadPoolExecutor 的实现原理](https://www.throwx.cn/2020/08/23/java-concurrency-thread-pool-executor/) +For more in-depth analysis of thread pool source code, it is recommended to check this article: Hardcore: [Analysis of JUC Thread Pool ThreadPoolExecutor Implementation Principle from Source Code](https://www.throwx.cn/2020/08/23/java-concurrency-thread-pool-executor/). -现在,让我们在回到示例代码, 现在应该是不是很容易就可以搞懂它的原理了呢? +Now, back to the example code—it should be straightforward to understand its principles now, right? -没搞懂的话,也没关系,可以看看我的分析: +If you still don't understand, that's alright, you can refer to my analysis: -> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务中如果有任务被执行完了,线程池就会去拿新的任务执行。 +> In the code, we simulated 10 tasks, where the configured core thread count is 5 and the waiting queue capacity is 100. Thus, only a maximum of 5 tasks can be executed simultaneously, while the remaining 5 will be placed in the waiting queue. If any of the current 5 tasks are completed, the thread pool will pick up new tasks to execute. -### 几个常见的对比 +### Common Comparisons #### `Runnable` vs `Callable` -`Runnable`自 Java 1.0 以来一直存在,但`Callable`仅在 Java 1.5 中引入,目的就是为了来处理`Runnable`不支持的用例。`Runnable` 接口不会返回结果或抛出检查异常,但是 `Callable` 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 `Runnable` 接口,这样代码看起来会更加简洁。 +`Runnable` has existed since Java 1.0, but `Callable` was introduced in Java 1.5 to handle use cases that `Runnable` does not support. The `Runnable` interface does not return results or throw checked exceptions, whereas the `Callable` interface can. Therefore, if a task does not need to return results or throw exceptions, it is recommended to use the `Runnable` interface, as it makes the code cleaner. -工具类 `Executors` 可以实现将 `Runnable` 对象转换成 `Callable` 对象。(`Executors.callable(Runnable task)` 或 `Executors.callable(Runnable task, Object result)`)。 +The `Executors` utility class can convert `Runnable` objects into `Callable` objects (`Executors.callable(Runnable task)` or `Executors.callable(Runnable task, Object result)`). `Runnable.java` @@ -548,7 +542,7 @@ Finished all threads // 任务全部执行完了才会跳出来,因为executo @FunctionalInterface public interface Runnable { /** - * 被线程执行,没有返回值也无法抛出异常 + * Executed by the thread, with no return value and no exceptions allowed */ public abstract void run(); } @@ -560,26 +554,25 @@ public interface Runnable { @FunctionalInterface public interface Callable { /** - * 计算结果,或在无法这样做时抛出异常。 - * @return 计算得出的结果 - * @throws 如果无法计算结果,则抛出异常 + * Computes a result, or throws an exception if unable to do so. + * @return The computed result + * @throws Exception if unable to compute the result */ V call() throws Exception; } - ``` #### `execute()` vs `submit()` -`execute()` 和 `submit()`是两种提交任务到线程池的方法,有一些区别: +`execute()` and `submit()` are two methods for submitting tasks to a thread pool, and they have some differences: -- **返回值**:`execute()` 方法用于提交不需要返回值的任务。通常用于执行 `Runnable` 任务,无法判断任务是否被线程池成功执行。`submit()` 方法用于提交需要返回值的任务。可以提交 `Runnable` 或 `Callable` 任务。`submit()` 方法返回一个 `Future` 对象,通过这个 `Future` 对象可以判断任务是否执行成功,并获取任务的返回值(`get()`方法会阻塞当前线程直到任务完成, `get(long timeout,TimeUnit unit)`多了一个超时时间,如果在 `timeout` 时间内任务还没有执行完,就会抛出 `java.util.concurrent.TimeoutException`)。 -- **异常处理**:在使用 `submit()` 方法时,可以通过 `Future` 对象处理任务执行过程中抛出的异常;而在使用 `execute()` 方法时,异常处理需要通过自定义的 `ThreadFactory` (在线程工厂创建线程的时候设置`UncaughtExceptionHandler`对象来 处理异常)或 `ThreadPoolExecutor` 的 `afterExecute()` 方法来处理 +- **Return values**: `execute()` is used for submitting tasks that do not require return values and is typically used for executing `Runnable` tasks, with no way to determine whether a task was successfully executed by the thread pool. The `submit()` method is used for submitting tasks requiring return values. It can submit `Runnable` or `Callable` tasks. The `submit()` method returns a `Future` object, which allows you to determine whether the task executed successfully and to retrieve the task's return value (`get()` method will block the current thread until the task is complete, and `get(long timeout, TimeUnit unit)` adds a timeout—if the task has not completed within the `timeout` period, it throws a `java.util.concurrent.TimeoutException`). +- **Exception handling**: With the `submit()` method, you can handle exceptions thrown during the task execution through the `Future` object, whereas when using the `execute()` method, exception handling needs to be performed through a custom `ThreadFactory` (setting an `UncaughtExceptionHandler` object when creating the thread in the thread factory to handle exceptions) or in the `afterExecute()` method of `ThreadPoolExecutor`. -示例 1:使用 `get()`方法获取返回值。 +Example 1: Using the `get()` method to retrieve a return value. ```java -// 这里只是为了演示使用,推荐使用 `ThreadPoolExecutor` 构造方法来创建线程池。 +// This is just to demonstrate usage; it's recommended to use the ThreadPoolExecutor constructor to create thread pools. ExecutorService executorService = Executors.newFixedThreadPool(3); Future submit = executorService.submit(() -> { @@ -596,13 +589,13 @@ System.out.println(s); executorService.shutdown(); ``` -输出: +Output: ```plain abc ``` -示例 2:使用 `get(long timeout,TimeUnit unit)`方法获取返回值。 +Example 2: Using the `get(long timeout, TimeUnit unit)` method to retrieve a return value. ```java ExecutorService executorService = Executors.newFixedThreadPool(3); @@ -621,34 +614,34 @@ System.out.println(s); executorService.shutdown(); ``` -输出: +Output: ```plain Exception in thread "main" java.util.concurrent.TimeoutException at java.util.concurrent.FutureTask.get(FutureTask.java:205) ``` -#### `shutdown()`VS`shutdownNow()` +#### `shutdown()` vs `shutdownNow()` -- **`shutdown()`** :关闭线程池,线程池的状态变为 `SHUTDOWN`。线程池不再接受新任务了,但是队列里的任务得执行完毕。 -- **`shutdownNow()`** :关闭线程池,线程池的状态变为 `STOP`。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。 +- **`shutdown()`**: Shuts down the thread pool, changing the pool status to `SHUTDOWN`. The thread pool will no longer accept new tasks, but the tasks in the queue will finish execution. +- **`shutdownNow()`**: Shuts down the thread pool, changing the pool status to `STOP`. The thread pool will terminate currently running tasks and stop processing queued tasks, returning a list of tasks that were waiting to be executed. -#### `isTerminated()` VS `isShutdown()` +#### `isTerminated()` vs `isShutdown()` -- **`isShutDown`** 当调用 `shutdown()` 方法后返回为 true。 -- **`isTerminated`** 当调用 `shutdown()` 方法后,并且所有提交的任务完成后返回为 true +- **`isShutdown`**: Returns true after `shutdown()` method is called. +- **`isTerminated`**: Returns true after `shutdown()` method is called, and all submitted tasks have completed. -## 几种常见的内置线程池 +## Several Common Built-in Thread Pools ### FixedThreadPool -#### 介绍 +#### Introduction -`FixedThreadPool` 被称为可重用固定线程数的线程池。通过 `Executors` 类中的相关源代码来看一下相关实现: +`FixedThreadPool` is known as a reusable fixed thread number thread pool. Let's look at the relevant implementation through the source code in the `Executors` class: ```java /** - * 创建一个可重用固定数量线程的线程池 + * Creates a thread pool with a fixed number of reusable threads */ public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, @@ -658,7 +651,7 @@ Exception in thread "main" java.util.concurrent.TimeoutException } ``` -另外还有一个 `FixedThreadPool` 的实现方法,和上面的类似,所以这里不多做阐述: +There is another implementation method of `FixedThreadPool`, similar to the above, which will not be elaborated here: ```java public static ExecutorService newFixedThreadPool(int nThreads) { @@ -668,40 +661,40 @@ Exception in thread "main" java.util.concurrent.TimeoutException } ``` -从上面源代码可以看出新创建的 `FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 都被设置为 `nThreads`,这个 `nThreads` 参数是我们使用的时候自己传递的。 +From the above source code, we can see that the newly created `FixedThreadPool` has both `corePoolSize` and `maximumPoolSize` set to `nThreads`, which is the value we provide when using it. -即使 `maximumPoolSize` 的值比 `corePoolSize` 大,也至多只会创建 `corePoolSize` 个线程。这是因为`FixedThreadPool` 使用的是容量为 `Integer.MAX_VALUE` 的 `LinkedBlockingQueue`(无界队列),队列永远不会被放满。 +Even if the `maximumPoolSize` is larger than `corePoolSize`, at most only `corePoolSize` threads will be created. This is because `FixedThreadPool` uses a `LinkedBlockingQueue` with a capacity of `Integer.MAX_VALUE` (an unbounded queue), and the queue will never fill up. -#### 执行任务过程介绍 +#### Execution Task Process Introduction -`FixedThreadPool` 的 `execute()` 方法运行示意图(该图片来源:《Java 并发编程的艺术》): +The `execute()` method running illustration of `FixedThreadPool` (the image source: "Java Concurrency in Practice"): -![FixedThreadPool的execute()方法运行示意图](./images/java-thread-pool-summary/FixedThreadPool.png) +![Running Illustration of FixedThreadPool's execute() Method](./images/java-thread-pool-summary/FixedThreadPool.png) -**上图说明:** +**Description of the above image:** -1. 如果当前运行的线程数小于 `corePoolSize`, 如果再来新任务的话,就创建新的线程来执行任务; -2. 当前运行的线程数等于 `corePoolSize` 后, 如果再来新任务的话,会将任务加入 `LinkedBlockingQueue`; -3. 线程池中的线程执行完 手头的任务后,会在循环中反复从 `LinkedBlockingQueue` 中获取任务来执行; +1. If the number of currently running threads is less than `corePoolSize`, create new threads to execute the tasks if a new task arrives; +1. Once the current number of running threads reaches `corePoolSize`, additional tasks will be added to the `LinkedBlockingQueue`; +1. After the threads in the thread pool finish their tasks, they will continuously fetch tasks from the `LinkedBlockingQueue` to execute. -#### 为什么不推荐使用`FixedThreadPool`? +#### Why is `FixedThreadPool` Not Recommended? -`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`(队列的容量为 Integer.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响: +`FixedThreadPool` uses an unbounded queue `LinkedBlockingQueue` (with a queue capacity of Integer.MAX_VALUE) as the working queue of the thread pool; this brings the following impacts to the thread pool: -1. 当线程池中的线程数达到 `corePoolSize` 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 `corePoolSize`; -2. 由于使用无界队列时 `maximumPoolSize` 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 `FixedThreadPool`的源码可以看出创建的 `FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 被设置为同一个值。 -3. 由于 1 和 2,使用无界队列时 `keepAliveTime` 将是一个无效参数; -4. 运行中的 `FixedThreadPool`(未执行 `shutdown()`或 `shutdownNow()`)不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。 +1. After the thread count in the thread pool reaches `corePoolSize`, new tasks will wait in the unbounded queue; hence, the number of threads in the thread pool will not exceed `corePoolSize`. +1. Since an unbounded queue is used, `maximumPoolSize` becomes an ineffective parameter since the queue will never be full. Thus, it can be seen from the source code of creating `FixedThreadPool` that its `corePoolSize` and `maximumPoolSize` are set to the same value. +1. Due to points 1 and 2, using an unbounded queue makes `keepAliveTime` an ineffective parameter. +1. A running `FixedThreadPool` (without calling `shutdown()` or `shutdownNow()`) will not reject tasks, potentially leading to OOM (OutOfMemoryError) when there are many tasks. ### SingleThreadExecutor -#### 介绍 +#### Introduction -`SingleThreadExecutor` 是只有一个线程的线程池。下面看看**SingleThreadExecutor 的实现:** +`SingleThreadExecutor` is a thread pool with only one thread. Let's look at the implementation of **SingleThreadExecutor**: ```java /** - *返回只有一个线程的线程池 + * Returns a thread pool with only one thread */ public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService @@ -721,33 +714,33 @@ Exception in thread "main" java.util.concurrent.TimeoutException } ``` -从上面源代码可以看出新创建的 `SingleThreadExecutor` 的 `corePoolSize` 和 `maximumPoolSize` 都被设置为 1,其他参数和 `FixedThreadPool` 相同。 +From the above source code, we can see that the newly created `SingleThreadExecutor` has both `corePoolSize` and `maximumPoolSize` set to 1, whereas other parameters are the same as `FixedThreadPool`. -#### 执行任务过程介绍 +#### Execution Task Process Introduction -`SingleThreadExecutor` 的运行示意图(该图片来源:《Java 并发编程的艺术》): +The running illustration of `SingleThreadExecutor` (the image source: "Java Concurrency in Practice"): -![SingleThreadExecutor的运行示意图](./images/java-thread-pool-summary/SingleThreadExecutor.png) +![Running Illustration of SingleThreadExecutor](./images/java-thread-pool-summary/SingleThreadExecutor.png) -**上图说明** : +**Description of the above image**: -1. 如果当前运行的线程数少于 `corePoolSize`,则创建一个新的线程执行任务; -2. 当前线程池中有一个运行的线程后,将任务加入 `LinkedBlockingQueue` -3. 线程执行完当前的任务后,会在循环中反复从`LinkedBlockingQueue` 中获取任务来执行; +1. If the number of currently running threads is less than `corePoolSize`, a new thread will be created to execute the task; +1. Once there is a running thread in the current thread pool, tasks will be added to the `LinkedBlockingQueue`; +1. Once the thread finishes the current task, it will continuously fetch tasks from the `LinkedBlockingQueue` to execute. -#### 为什么不推荐使用`SingleThreadExecutor`? +#### Why is `SingleThreadExecutor` Not Recommended? -`SingleThreadExecutor` 和 `FixedThreadPool` 一样,使用的都是容量为 `Integer.MAX_VALUE` 的 `LinkedBlockingQueue`(无界队列)作为线程池的工作队列。`SingleThreadExecutor` 使用无界队列作为线程池的工作队列会对线程池带来的影响与 `FixedThreadPool` 相同。说简单点,就是可能会导致 OOM。 +`SingleThreadExecutor`, like `FixedThreadPool`, also uses an unbounded queue `LinkedBlockingQueue` (with a queue capacity of Integer.MAX_VALUE) as its working queue. Using an unbounded queue has impacts on the thread pool, similar to those of `FixedThreadPool`, meaning it could lead to OOM. ### CachedThreadPool -#### 介绍 +#### Introduction -`CachedThreadPool` 是一个会根据需要创建新线程的线程池。下面通过源码来看看 `CachedThreadPool` 的实现: +`CachedThreadPool` is a thread pool that creates new threads as needed. Let’s investigate the implementation of `CachedThreadPool`: ```java /** - * 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。 + * Creates a thread pool where new threads are created as needed, but will reuse previously constructed threads if they are available. */ public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, @@ -755,7 +748,6 @@ Exception in thread "main" java.util.concurrent.TimeoutException new SynchronousQueue(), threadFactory); } - ``` ```java @@ -766,28 +758,28 @@ Exception in thread "main" java.util.concurrent.TimeoutException } ``` -`CachedThreadPool` 的`corePoolSize` 被设置为空(0),`maximumPoolSize`被设置为 `Integer.MAX.VALUE`,即它是无界的,这也就意味着如果主线程提交任务的速度高于 `maximumPool` 中线程处理任务的速度时,`CachedThreadPool` 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。 +The `corePoolSize` of `CachedThreadPool` is set to zero (0), and the `maximumPoolSize` is set to `Integer.MAX_VALUE`, which is unbounded. This means that if the submission speed of tasks from the main thread exceeds the processing speed of threads in the `maximumPool`, `CachedThreadPool` will constantly create new threads, potentially exhausting CPU and memory resources. -#### 执行任务过程介绍 +#### Execution Task Process Introduction -`CachedThreadPool` 的 `execute()` 方法的执行示意图(该图片来源:《Java 并发编程的艺术》): +The execution illustration of the `CachedThreadPool`'s `execute()` method (the image source: "Java Concurrency in Practice"): -![CachedThreadPool的execute()方法的执行示意图](./images/java-thread-pool-summary/CachedThreadPool-execute.png) +![Execution Illustration of CachedThreadPool's execute() Method](./images/java-thread-pool-summary/CachedThreadPool-execute.png) -**上图说明:** +**Description of the above image**: -1. 首先执行 `SynchronousQueue.offer(Runnable task)` 提交任务到任务队列。如果当前 `maximumPool` 中有闲线程正在执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`,那么主线程执行 offer 操作与空闲线程执行的 `poll` 操作配对成功,主线程把任务交给空闲线程执行,`execute()`方法执行完成,否则执行下面的步骤 2; -2. 当初始 `maximumPool` 为空,或者 `maximumPool` 中没有空闲线程时,将没有线程执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`。这种情况下,步骤 1 将失败,此时 `CachedThreadPool` 会创建新线程执行任务,execute 方法执行完成; +1. First, execute `SynchronousQueue.offer(Runnable task)` to submit the task to the task queue. If there are idle threads in the `maximumPool` executing `SynchronousQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)`, the main thread’s offer operation will successfully pair with the poll operation of the idle thread, transferring the task to the idle thread for execution, thus completing the `execute()` method. If not, it proceeds to the next step. +1. If the initial `maximumPool` is empty or if there are no idle threads in it, the offer operation will fail. In this case, `CachedThreadPool` will create a new thread to execute the task, and the `execute` method will complete. -#### 为什么不推荐使用`CachedThreadPool`? +#### Why is `CachedThreadPool` Not Recommended? -`CachedThreadPool` 使用的是同步队列 `SynchronousQueue`, 允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。 +`CachedThreadPool` uses `SynchronousQueue`, allowing for up to `Integer.MAX_VALUE` threads, which may lead to an excessive number of created threads, resulting in OOM. ### ScheduledThreadPool -#### 介绍 +#### Introduction -`ScheduledThreadPool` 用来在给定的延迟后运行任务或者定期执行任务。这个在实际项目中基本不会被用到,也不推荐使用,大家只需要简单了解一下即可。 +`ScheduledThreadPool` is designed to run tasks after a specified delay or to execute tasks periodically. This will not generally be used in practical projects, and it is not recommended; it's sufficient to have a basic understanding. ```java public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { @@ -799,11 +791,11 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { } ``` -`ScheduledThreadPool` 是通过 `ScheduledThreadPoolExecutor` 创建的,使用的`DelayedWorkQueue`(延迟阻塞队列)作为线程池的任务队列。 +`ScheduledThreadPool` is created through `ScheduledThreadPoolExecutor` and utilizes a `DelayedWorkQueue` (delay blocking queue) as its task queue. -`DelayedWorkQueue` 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。`DelayedWorkQueue` 添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 `Integer.MAX_VALUE`,所以最多只能创建核心线程数的线程。 +The internal elements of `DelayedWorkQueue` are not sorted by the insertion time but by the length of the delay, employing a heap data structure to ensure every dequeued task is the one currently able to execute sooner in the queue. When full, `DelayedWorkQueue` will automatically enlarge its original capacity by half, meaning it will never block, with a maximum expansion of `Integer.MAX_VALUE`, thus it can create at most the core thread count. -`ScheduledThreadPoolExecutor` 继承了 `ThreadPoolExecutor`,所以创建 `ScheduledThreadExecutor` 本质也是创建一个 `ThreadPoolExecutor` 线程池,只是传入的参数不相同。 +`ScheduledThreadPoolExecutor` inherits from `ThreadPoolExecutor`, so creating `ScheduledThreadExecutor` fundamentally creates a `ThreadPoolExecutor` thread pool, though with differing parameters. ```java public class ScheduledThreadPoolExecutor @@ -811,23 +803,21 @@ public class ScheduledThreadPoolExecutor implements ScheduledExecutorService ``` -#### ScheduledThreadPoolExecutor 和 Timer 对比 +#### Comparison Between ScheduledThreadPoolExecutor and Timer -- `Timer` 对系统时钟的变化敏感,`ScheduledThreadPoolExecutor`不是; -- `Timer` 只有一个执行线程,因此长时间运行的任务可以延迟其他任务。 `ScheduledThreadPoolExecutor` 可以配置任意数量的线程。 此外,如果你想(通过提供 `ThreadFactory`),你可以完全控制创建的线程; -- 在`TimerTask` 中抛出的运行时异常会杀死一个线程,从而导致 `Timer` 死机即计划任务将不再运行。`ScheduledThreadExecutor` 不仅捕获运行时异常,还允许您在需要时处理它们(通过重写 `afterExecute` 方法`ThreadPoolExecutor`)。抛出异常的任务将被取消,但其他任务将继续运行。 +- **Timer** is sensitive to changes in system clock, while `ScheduledThreadPoolExecutor` is not. +- **Timer** has only one executing thread, therefore long-running tasks may delay others. `ScheduledThreadPoolExecutor` can configure any number of threads. Additionally, if you wish (via providing a `ThreadFactory`), you can fully control the threads being created. +- In `TimerTask`, runtime exceptions terminate a thread, causing the `Timer` to hang, thus halting scheduled tasks from executing. `ScheduledThreadExecutor` catches runtime exceptions and allows you to handle them if needed (by overriding `afterExecute` method of `ThreadPoolExecutor`). Exception-throwing tasks will be canceled, but others will continue running. -关于定时任务的详细介绍,可以看这篇文章:[Java 定时任务详解](https://javaguide.cn/system-design/schedule-task.html) 。 +For more detailed information on scheduled tasks, you can refer to the article: [Java Scheduled Tasks Explained](https://javaguide.cn/system-design/schedule-task.html). -## 线程池最佳实践 +## Best Practices for Thread Pools -[Java 线程池最佳实践](https://javaguide.cn/java/concurrent/java-thread-pool-best-practices.html)这篇文章总结了一些使用线程池的时候应该注意的东西,实际项目使用线程池之前可以看看。 +The article [Best Practices for Java Thread Pools](https://javaguide.cn/java/concurrent/java-thread-pool-best-practices.html) summarizes important considerations when using thread pools, which can be useful before implementing thread pools in practice. -## 参考 +## References -- 《Java 并发编程的艺术》 +- "Java Concurrency in Practice" - [Java Scheduler ScheduledExecutorService ScheduledThreadPoolExecutor Example](https://www.journaldev.com/2340/java-scheduler-scheduledexecutorservice-scheduledthreadpoolexecutor-example "Java Scheduler ScheduledExecutorService ScheduledThreadPoolExecutor Example") - [java.util.concurrent.ScheduledThreadPoolExecutor Example](https://examples.javacodegeeks.com/core-java/util/concurrent/scheduledthreadpoolexecutor/java-util-concurrent-scheduledthreadpoolexecutor-example/ "java.util.concurrent.ScheduledThreadPoolExecutor Example") - [ThreadPoolExecutor – Java Thread Pool Example](https://www.journaldev.com/1069/threadpoolexecutor-java-thread-pool-example-executorservice "ThreadPoolExecutor – Java Thread Pool Example") - - diff --git a/docs/java/concurrent/jmm.md b/docs/java/concurrent/jmm.md index dbc36a351b9..d155fb904e7 100644 --- a/docs/java/concurrent/jmm.md +++ b/docs/java/concurrent/jmm.md @@ -1,244 +1,58 @@ --- -title: JMM(Java 内存模型)详解 +title: Detailed Explanation of JMM (Java Memory Model) category: Java tag: - - Java并发 + - Java Concurrency head: - - - meta - - name: keywords - content: CPU 缓存模型,指令重排序,Java 内存模型(JMM),happens-before - - - meta - - name: description - content: 对于 Java 来说,你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。 + - - meta + - name: keywords + content: CPU Cache Model, Instruction Reordering, Java Memory Model (JMM), happens-before + - - meta + - name: description + content: For Java, you can think of JMM as a set of specifications related to concurrent programming defined by Java. In addition to abstracting the relationship between threads and main memory, it also stipulates the principles and norms related to concurrency that must be followed during the transformation process from Java source code to CPU executable instructions, with the main goal of simplifying multithreaded programming and enhancing program portability. --- -JMM(Java 内存模型)主要定义了对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。 +JMM (Java Memory Model) mainly defines the visibility of a shared variable after another thread performs a write operation on that shared variable. -要想理解透彻 JMM(Java 内存模型),我们先要从 **CPU 缓存模型和指令重排序** 说起! +To thoroughly understand JMM (Java Memory Model), we first need to discuss **CPU Cache Model and Instruction Reordering**! -## 从 CPU 缓存模型说起 +## Starting with CPU Cache Model -**为什么要弄一个 CPU 高速缓存呢?** 类比我们开发网站后台系统使用的缓存(比如 Redis)是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。 **CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。** +**Why do we need a CPU cache?** It is analogous to the cache we use in developing backend systems for websites (like Redis) to solve the disparity between the speed of program processing and the speed of accessing conventional relational databases. **The CPU cache is designed to address the disparity between CPU processing speed and memory processing speed.** -我们甚至可以把 **内存看作外存的高速缓存**,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。 +We can even think of **memory as a high-speed cache for external storage**. When a program runs, we copy data from external storage to memory, as the processing speed of memory is significantly higher than that of external storage, thus improving processing speed. -总结:**CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。** +In summary: **CPU Cache caches memory data to solve the mismatch between CPU processing speed and memory, while memory caches disk data to solve the problem of slow disk access speed.** -为了更好地理解,我画了一个简单的 CPU Cache 示意图如下所示。 +To better understand, I have drawn a simple diagram of the CPU Cache as shown below. -> **🐛 修正(参见:[issue#1848](https://github.com/Snailclimb/JavaGuide/issues/1848))**:对 CPU 缓存模型绘图不严谨的地方进行完善。 +> **🐛 Correction (see: [issue#1848](https://github.com/Snailclimb/JavaGuide/issues/1848))**: Improvements have been made to the drawing of the CPU cache model. -![CPU 缓存模型示意图](https://oss.javaguide.cn/github/javaguide/java/concurrent/cpu-cache.png) +![CPU Cache Model Diagram](https://oss.javaguide.cn/github/javaguide/java/concurrent/cpu-cache.png) -现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache。有些 CPU 可能还有 L4 Cache,这里不做讨论,并不常见 +Modern CPU caches are typically divided into three levels: L1, L2, and L3 Cache. Some CPUs may also have L4 Cache, but this is not commonly discussed. -**CPU Cache 的工作方式:** 先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 **内存缓存不一致性的问题** !比如我执行一个 i++ 操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 i++ 运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。 +**How CPU Cache Works:** It first copies a piece of data into the CPU Cache. When the CPU needs to use it, it can read the data directly from the CPU Cache. After the computation is complete, the computed data is written back to Main Memory. However, this can lead to **memory cache inconsistency issues**! For example, if I perform an i++ operation and two threads execute it simultaneously, assuming both threads read i=1 from the CPU Cache, after both threads perform the i++ operation and write back to Main Memory, i=2, while the correct result should be i=3. -**CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议(比如 [MESI 协议](https://zh.wikipedia.org/wiki/MESI%E5%8D%8F%E8%AE%AE))或者其他手段来解决。** 这个缓存一致性协议指的是在 CPU 高速缓存与主内存交互的时候需要遵守的原则和规范。不同的 CPU 中,使用的缓存一致性协议通常也会有所不同。 +**To solve the memory cache inconsistency problem, the CPU can establish cache coherence protocols (like the [MESI protocol](https://zh.wikipedia.org/wiki/MESI%E5%8D%8F%E8%AE%AE)) or other means.** This cache coherence protocol refers to the principles and norms that must be followed when interacting between the CPU cache and main memory. Different CPUs may use different cache coherence protocols. -![缓存一致性协议](https://oss.javaguide.cn/github/javaguide/java/concurrent/cpu-cache-protocol.png) +![Cache Coherence Protocol](https://oss.javaguide.cn/github/javaguide/java/concurrent/cpu-cache-protocol.png) -我们的程序运行在操作系统之上,操作系统屏蔽了底层硬件的操作细节,将各种硬件资源虚拟化。于是,操作系统也就同样需要解决内存缓存不一致性问题。 +Our programs run on top of the operating system, which abstracts the operational details of the underlying hardware and virtualizes various hardware resources. Therefore, the operating system also needs to address the memory cache inconsistency problem. -操作系统通过 **内存模型(Memory Model)** 定义一系列规范来解决这个问题。无论是 Windows 系统,还是 Linux 系统,它们都有特定的内存模型。 +The operating system defines a series of specifications through the **Memory Model** to solve this problem. Whether it is Windows or Linux, they both have specific memory models. -## 指令重排序 +## Instruction Reordering -说完了 CPU 缓存模型,我们再来看看另外一个比较重要的概念 **指令重排序** 。 +After discussing the CPU cache model, let's look at another important concept: **Instruction Reordering**. -为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。 +To improve execution speed/performance, computers may reorder instructions when executing program code. -**什么是指令重排序?** 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。 +**What is instruction reordering?** Simply put, it means that the system does not necessarily execute the code in the order you wrote it. -常见的指令重排序有下面 2 种情况: +Common types of instruction reordering include the following two situations: -- **编译器优化重排**:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。 -- **指令并行重排**:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 +- **Compiler Optimization Reordering**: The compiler (including JVM, JIT compiler, etc.) rearranges the execution order of statements without changing the semantics of single-threaded programs. +- **Instruction-Level Parallelism Reordering**: Modern processors use instruction-level parallelism (ILP) to overlap the execution of multiple instructions. If there are no data dependencies, the processor can change the execution order of the corresponding machine instructions. -另外,内存系统也会有“重排序”,但又不是真正意义上的重排序。在 JMM 里表现为主存和本地内存的内容可能不一致,进而导致程序在多线程下执行可能出现问题。 - -Java 源代码会经历 **编译器优化重排 —> 指令并行重排 —> 内存系统重排** 的过程,最终才变成操作系统可执行的指令序列。 - -**指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致** ,所以在多线程下,指令重排序可能会导致一些问题。 - -对于编译器优化重排和处理器的指令重排序(指令并行重排和内存系统重排都属于是处理器级别的指令重排序),处理该问题的方式不一样。 - -- 对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。 - -- 对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。 - -> 内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它会在处理器写入值时,强制将写缓冲区中的数据刷新到主内存;在读取值之前,使处理器本地缓存中的相关数据失效,强制从主内存中加载最新值,从而保障变量的可见性。 - -## JMM(Java Memory Model) - -### 什么是 JMM?为什么需要 JMM? - -Java 是最早尝试提供内存模型的编程语言。由于早期内存模型存在一些缺陷(比如非常容易削弱编译器的优化能力),从 Java5 开始,Java 开始使用新的内存模型 [《JSR-133:Java Memory Model and Thread Specification》](http://www.cs.umd.edu/~pugh/java/memoryModel/CommunityReview.pdf) 。 - -一般来说,编程语言也可以直接复用操作系统层面的内存模型。不过,不同的操作系统内存模型不同。如果直接复用操作系统层面的内存模型,就可能会导致同样一套代码换了一个操作系统就无法执行了。Java 语言是跨平台的,它需要自己提供一套内存模型以屏蔽系统差异。 - -这只是 JMM 存在的其中一个原因。实际上,对于 Java 来说,你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。 - -**为什么要遵守这些并发相关的原则和规范呢?** 这是因为并发编程下,像 CPU 多级缓存和指令重排这类设计可能会导致程序运行出现一些问题。就比如说我们上面提到的指令重排序就可能会让多线程程序的执行出现问题,为此,JMM 抽象了 happens-before 原则(后文会详细介绍到)来解决这个指令重排序问题。 - -JMM 说白了就是定义了一些规范来解决这些问题,开发者可以利用这些规范更方便地开发多线程程序。对于 Java 开发者说,你不需要了解底层原理,直接使用并发相关的一些关键字和类(比如 `volatile`、`synchronized`、各种 `Lock`)即可开发出并发安全的程序。 - -### JMM 是如何抽象线程和主内存之间的关系? - -**Java 内存模型(JMM)** 抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储在主内存中。 - -在 JDK1.2 之前,Java 的内存模型实现总是从 **主存** (即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存 **本地内存** (比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。 - -这和我们上面讲到的 CPU 缓存模型非常相似。 - -**什么是主内存?什么是本地内存?** - -- **主内存**:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量,还是局部变量,类信息、常量、静态变量都是放在主内存中。为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。 -- **本地内存**:每个线程都有一个私有的本地内存,本地内存存储了该线程以读 / 写共享变量的副本。每个线程只能操作自己本地内存中的变量,无法直接访问其他线程的本地内存。如果线程间需要通信,必须通过主内存来进行。本地内存是 JMM 抽象出来的一个概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。 - -Java 内存模型的抽象示意图如下: - -![JMM(Java 内存模型)](https://oss.javaguide.cn/github/javaguide/java/concurrent/jmm.png) - -从上图来看,线程 1 与线程 2 之间如果要进行通信的话,必须要经历下面 2 个步骤: - -1. 线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去。 -2. 线程 2 到主存中读取对应的共享变量的值。 - -也就是说,JMM 为共享变量提供了可见性的保障。 - -不过,多线程下,对主内存中的一个共享变量进行操作有可能诱发线程安全问题。举个例子: - -1. 线程 1 和线程 2 分别对同一个共享变量进行操作,一个执行修改,一个执行读取。 -2. 线程 2 读取到的是线程 1 修改之前的值还是修改后的值并不确定,都有可能,因为线程 1 和线程 2 都是先将共享变量从主内存拷贝到对应线程的工作内存中。 - -关于主内存与工作内存直接的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节,Java 内存模型定义来以下八种同步操作(了解即可,无需死记硬背): - -- **锁定(lock)**: 作用于主内存中的变量,将他标记为一个线程独享变量。 -- **解锁(unlock)**: 作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。 -- **read(读取)**:作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。 -- **load(载入)**:把 read 操作从主内存中得到的变量值放入工作内存的变量的副本中。 -- **use(使用)**:把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令。 -- **assign(赋值)**:作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 -- **store(存储)**:作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。 -- **write(写入)**:作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。 - -除了这 8 种同步操作之外,还规定了下面这些同步规则来保证这些同步操作的正确执行(了解即可,无需死记硬背): - -- 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中。 -- 一个新的变量只能在主内存中 “诞生”,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量,换句话说就是对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作。 -- 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。 -- 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。 -- 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定住的变量。 -- …… - -### Java 内存区域和 JMM 有何区别? - -这是一个比较常见的问题,很多初学者非常容易搞混。 **Java 内存区域和内存模型是完全不一样的两个东西**: - -- JVM 内存结构和 Java 虚拟机的运行时区域相关,定义了 JVM 在运行时如何分区存储程序数据,就比如说堆主要用于存放对象实例。 -- Java 内存模型和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。 - -### happens-before 原则是什么? - -happens-before 这个概念最早诞生于 Leslie Lamport 于 1978 年发表的论文[《Time,Clocks and the Ordering of Events in a Distributed System》](https://lamport.azurewebsites.net/pubs/time-clocks.pdf)。在这篇论文中,Leslie Lamport 提出了[逻辑时钟](https://writings.sh/post/logical-clocks)的概念,这也成了第一个逻辑时钟算法 。在分布式环境中,通过一系列规则来定义逻辑时钟的变化,从而能通过逻辑时钟来对分布式系统中的事件的先后顺序进行判断。**逻辑时钟并不度量时间本身,仅区分事件发生的前后顺序,其本质就是定义了一种 happens-before 关系。** - -上面提到的 happens-before 这个概念诞生的背景并不是重点,简单了解即可。 - -JSR 133 引入了 happens-before 这个概念来描述两个操作之间的内存可见性。 - -**为什么需要 happens-before 原则?** happens-before 原则的诞生是为了程序员和编译器、处理器之间的平衡。程序员追求的是易于理解和编程的强内存模型,遵守既定规则编码即可。编译器和处理器追求的是较少约束的弱内存模型,让它们尽己所能地去优化性能,让性能最大化。happens-before 原则的设计思想其实非常简单: - -- 为了对编译器和处理器的约束尽可能少,只要不改变程序的执行结果(单线程程序和正确执行的多线程程序),编译器和处理器怎么进行重排序优化都行。 -- 对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序。 - -下面这张是 《Java 并发编程的艺术》这本书中的一张 JMM 设计思想的示意图,非常清晰。 - -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/image-20220731155332375.png) - -了解了 happens-before 原则的设计思想,我们再来看看 JSR-133 对 happens-before 原则的定义: - -- 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,并且第一个操作的执行顺序排在第二个操作之前。 -- 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么 JMM 也允许这样的重排序。 - -我们看下面这段代码: - -```java -int userNum = getUserNum(); // 1 -int teacherNum = getTeacherNum(); // 2 -int totalNum = userNum + teacherNum; // 3 -``` - -- 1 happens-before 2 -- 2 happens-before 3 -- 1 happens-before 3 - -虽然 1 happens-before 2,但对 1 和 2 进行重排序不会影响代码的执行结果,所以 JMM 是允许编译器和处理器执行这种重排序的。但 1 和 2 必须是在 3 执行之前,也就是说 1,2 happens-before 3 。 - -**happens-before 原则表达的意义其实并不是一个操作发生在另外一个操作的前面,虽然这从程序员的角度上来说也并无大碍。更准确地来说,它更想表达的意义是前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里。** - -举个例子:操作 1 happens-before 操作 2,即使操作 1 和操作 2 不在同一个线程内,JMM 也会保证操作 1 的结果对操作 2 是可见的。 - -### happens-before 常见规则有哪些?谈谈你的理解? - -happens-before 的规则就 8 条,说多不多,重点了解下面列举的 5 条即可。全记是不可能的,很快就忘记了,意义不大,随时查阅即可。 - -1. **程序顺序规则**:一个线程内,按照代码顺序,书写在前面的操作 happens-before 于书写在后面的操作; -2. **解锁规则**:解锁 happens-before 于加锁; -3. **volatile 变量规则**:对一个 volatile 变量的写操作 happens-before 于后面对这个 volatile 变量的读操作。说白了就是对 volatile 变量的写操作的结果对于发生于其后的任何操作都是可见的。 -4. **传递规则**:如果 A happens-before B,且 B happens-before C,那么 A happens-before C; -5. **线程启动规则**:Thread 对象的 `start()`方法 happens-before 于此线程的每一个动作。 - -如果两个操作不满足上述任意一个 happens-before 规则,那么这两个操作就没有顺序的保障,JVM 可以对这两个操作进行重排序。 - -### happens-before 和 JMM 什么关系? - -happens-before 与 JMM 的关系用《Java 并发编程的艺术》这本书中的一张图就可以非常好的解释清楚。 - -![happens-before 与 JMM 的关系](https://oss.javaguide.cn/github/javaguide/java/concurrent/image-20220731084604667.png) - -## 再看并发编程三个重要特性 - -### 原子性 - -一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。 - -在 Java 中,可以借助`synchronized`、各种 `Lock` 以及各种原子类实现原子性。 - -`synchronized` 和各种 `Lock` 可以保证任一时刻只有一个线程访问该代码块,因此可以保障原子性。各种原子类是利用 CAS (compare and swap) 操作(可能也会用到 `volatile`或者`final`关键字)来保证原子操作。 - -### 可见性 - -当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。 - -在 Java 中,可以借助`synchronized`、`volatile` 以及各种 `Lock` 实现可见性。 - -如果我们将变量声明为 `volatile` ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。 - -### 有序性 - -由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。 - -我们上面讲重排序的时候也提到过: - -> **指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致** ,所以在多线程下,指令重排序可能会导致一些问题。 - -在 Java 中,`volatile` 关键字可以禁止指令进行重排序优化。 - -## 总结 - -- Java 是最早尝试提供内存模型的语言,其主要目的是为了简化多线程编程,增强程序可移植性的。 -- CPU 可以通过制定缓存一致协议(比如 [MESI 协议](https://zh.wikipedia.org/wiki/MESI%E5%8D%8F%E8%AE%AE))来解决内存缓存不一致性问题。 -- 为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。**指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致** ,所以在多线程下,指令重排序可能会导致一些问题。 -- 你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。 -- JSR 133 引入了 happens-before 这个概念来描述两个操作之间的内存可见性。 - -## 参考 - -- 《Java 并发编程的艺术》第三章 Java 内存模型 -- 《深入浅出 Java 多线程》: -- Java 内存访问重排序的研究: -- 嘿,同学,你要的 Java 内存模型 (JMM) 来了: -- JSR 133 (Java Memory Model) FAQ: - - +Additionally, the memory system may also have "reordering," but it is not true reordering in the strict sense. In JMM, this manifests as inconsistencies between main memory and local memory, which can lead to issues when programs are diff --git a/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md b/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md index ba370690a11..0b7e2727b8c 100644 --- a/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md +++ b/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md @@ -1,114 +1,68 @@ --- -title: 乐观锁和悲观锁详解 +title: Detailed Explanation of Optimistic Lock and Pessimistic Lock category: Java tag: - - Java并发 + - Java Concurrency --- -如果将悲观锁(Pessimistic Lock)和乐观锁(Optimistic Lock)对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。 +If we relate pessimistic locks and optimistic locks to real life, a pessimistic lock is like a rather pessimistic (or cautious) person who always assumes the worst-case scenario to avoid problems. An optimistic lock is like a more optimistic person who always assumes the best-case scenario and quickly resolves issues before they arise. -## 什么是悲观锁? +## What is a Pessimistic Lock? -悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。 +A pessimistic lock always assumes the worst-case scenario, believing that problems (such as shared data being modified) will occur every time shared resources are accessed. Therefore, it locks the resource every time it is accessed, causing other threads that want to access this resource to block until the lock is released by the current holder. In other words, **shared resources are only available to one thread at a time, while other threads are blocked until the resource is released**. -像 Java 中`synchronized`和`ReentrantLock`等独占锁就是悲观锁思想的实现。 +In Java, exclusive locks like `synchronized` and `ReentrantLock` are implementations of the pessimistic lock concept. ```java public void performSynchronisedTask() { synchronized (this) { - // 需要同步的操作 + // Synchronized operations } } private Lock lock = new ReentrantLock(); lock.lock(); try { - // 需要同步的操作 + // Synchronized operations } finally { lock.unlock(); } ``` -高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题(线程获得锁的顺序不当时),影响代码的正常运行。 +In high-concurrency scenarios, intense lock contention can cause thread blocking, and a large number of blocked threads can lead to context switching, increasing the performance overhead of the system. Additionally, pessimistic locks may also encounter deadlock issues (when threads acquire locks in an improper order), affecting the normal operation of the code. -## 什么是乐观锁? +## What is an Optimistic Lock? -乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。 +An optimistic lock always assumes the best-case scenario, believing that problems will not occur every time shared resources are accessed. Threads can execute continuously without locking or waiting, only verifying whether the corresponding resource (i.e., data) has been modified by other threads when submitting changes (specific methods can use version number mechanisms or CAS algorithms). -在 Java 中`java.util.concurrent.atomic`包下面的原子变量类(比如`AtomicInteger`、`LongAdder`)就是使用了乐观锁的一种实现方式 **CAS** 实现的。 -![JUC原子类概览](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88-20230814005211968.png) +In Java, atomic variable classes under the `java.util.concurrent.atomic` package (such as `AtomicInteger` and `LongAdder`) are implementations of optimistic locks using the **CAS** mechanism. +![Overview of JUC Atomic Classes](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88-20230814005211968.png) ```java -// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好 -// 代价就是会消耗更多的内存空间(空间换时间) +// LongAdder performs better than AtomicInteger and AtomicLong in high-concurrency scenarios +// The trade-off is that it consumes more memory space (space for time) LongAdder sum = new LongAdder(); sum.increment(); ``` -高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败并重试,这样同样会非常影响性能,导致 CPU 飙升。 +In high-concurrency scenarios, optimistic locks do not have the thread blocking caused by lock contention, nor do they have deadlock issues, often resulting in better performance. However, if conflicts occur frequently (in cases with a high write ratio), it may lead to frequent failures and retries, which can also significantly impact performance and cause CPU spikes. -不过,大量失败重试的问题也是可以解决的,像我们前面提到的 `LongAdder`以空间换时间的方式就解决了这个问题。 +However, the issue of frequent failures and retries can also be addressed. For example, as mentioned earlier, `LongAdder` solves this problem by trading space for time. -理论上来说: +Theoretically: -- 悲观锁通常多用于写比较多的情况(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如`LongAdder`),也是可以考虑使用乐观锁的,要视实际情况而定。 -- 乐观锁通常多用于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考`java.util.concurrent.atomic`包下面的原子变量类)。 +- Pessimistic locks are usually used in scenarios with a high number of writes (intense write scenarios) to avoid the performance impact of frequent failures and retries, and the overhead of pessimistic locks is fixed. However, if optimistic locks can solve the frequent failure and retry issue (like `LongAdder`), they can also be considered based on the actual situation. +- Optimistic locks are typically used in scenarios with fewer writes (more read scenarios, less contention) to avoid the performance impact of frequent locking. However, optimistic locks mainly target single shared variables (refer to atomic variable classes under the `java.util.concurrent.atomic` package). -## 如何实现乐观锁? +## How to Implement an Optimistic Lock? -乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。 +Optimistic locks are generally implemented using version number mechanisms or CAS algorithms, with CAS algorithms being more common, which requires special attention. -### 版本号机制 +### Version Number Mechanism -一般是在数据表中加上一个数据版本号 `version` 字段,表示数据被修改的次数。当数据被修改时,`version` 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 `version` 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 `version` 值相等时才更新,否则重试更新操作,直到更新成功。 +Typically, a `version` field is added to the data table to indicate the number of times the data has been modified. When the data is modified, the `version` value is incremented. When thread A wants to update the data value, it reads the `version` value at the same time. When submitting the update, it only updates if the `version` value read earlier matches the current `version` value in the database; otherwise, it retries the update operation until successful. -**举一个简单的例子**:假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( `balance` )为 \$100 。 +**A simple example**: Suppose there is a `version` field in the account information table in the database, with the current value being 1, and the current account balance field (`balance`) being $100. -1. 操作员 A 此时将其读出( `version`=1 ),并从其帐户余额中扣除 $50( $100-\$50 )。 -2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( `version`=1 ),并从其帐户余额中扣除 $20 ( $100-\$20 )。 -3. 操作员 A 完成了修改工作,将数据版本号( `version`=1 ),连同帐户扣除后余额( `balance`=\$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 `version` 更新为 2 。 -4. 操作员 B 完成了操作,也将版本号( `version`=1 )试图向数据库提交数据( `balance`=\$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 - -这样就避免了操作员 B 用基于 `version`=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。 - -### CAS 算法 - -CAS 的全称是 **Compare And Swap(比较与交换)** ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。 - -CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。 - -> **原子操作** 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。 - -CAS 涉及到三个操作数: - -- **V**:要更新的变量值(Var) -- **E**:预期值(Expected) -- **N**:拟写入的新值(New) - -当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。 - -**举一个简单的例子**:线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。 - -1. i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。 -2. i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。 - -当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。 - -关于 CAS 的进一步介绍,可以阅读读者写的这篇文章:[CAS 详解](./cas.md),其中详细提到了 Java 中 CAS 的实现以及 CAS 存在的一些问题。 - -## 总结 - -本文详细介绍了乐观锁和悲观锁的概念以及乐观锁常见实现方式: - -- 悲观锁基于悲观的假设,认为共享资源在每次访问时都会发生冲突,因此在每次操作时都会加锁。这种锁机制会导致其他线程阻塞,直到锁被释放。Java 中的 `synchronized` 和 `ReentrantLock` 是悲观锁的典型实现方式。虽然悲观锁能有效避免数据竞争,但在高并发场景下会导致线程阻塞、上下文切换频繁,从而影响系统性能,并且还可能引发死锁问题。 -- 乐观锁基于乐观的假设,认为共享资源在每次访问时不会发生冲突,因此无须加锁,只需在提交修改时验证数据是否被其他线程修改。Java 中的 `AtomicInteger` 和 `LongAdder` 等类通过 CAS(Compare-And-Swap)算法实现了乐观锁。乐观锁避免了线程阻塞和死锁问题,在读多写少的场景中性能优越。但在写操作频繁的情况下,可能会导致大量重试和失败,从而影响性能。 -- 乐观锁主要通过版本号机制或 CAS 算法实现。版本号机制通过比较版本号确保数据一致性,而 CAS 通过硬件指令实现原子操作,直接比较和交换变量值。 - -悲观锁和乐观锁各有优缺点,适用于不同的应用场景。在实际开发中,选择合适的锁机制能够有效提升系统的并发性能和稳定性。 - -## 参考 - -- 《Java 并发编程核心 78 讲》 -- 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其 Java 实现!: - - +1. Operator A reads it ( `version`=1) and deducts $50 from their account balance ($100-$50). +1. During Operator A's operation, Operator B also reads this diff --git a/docs/java/concurrent/reentrantlock.md b/docs/java/concurrent/reentrantlock.md index ef1cd38625c..d406957eee9 100644 --- a/docs/java/concurrent/reentrantlock.md +++ b/docs/java/concurrent/reentrantlock.md @@ -1,53 +1,53 @@ --- -title: 从ReentrantLock的实现看AQS的原理及应用 +title: Understanding AQS Principles and Applications through the Implementation of ReentrantLock category: Java tag: - - Java并发 + - Java Concurrency --- -> 本文转载自: +> This article is reproduced from: > -> 作者:美团技术团队 +> Author: Meituan Technical Team -Java 中的大部分同步类(Semaphore、ReentrantLock 等)都是基于 AbstractQueuedSynchronizer(简称为 AQS)实现的。AQS 是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。 +Most synchronization classes in Java (such as Semaphore, ReentrantLock, etc.) are implemented based on AbstractQueuedSynchronizer (AQS). AQS is a simple framework that provides atomic management of synchronization state, blocking and waking threads, and a queue model. -本文会从应用层逐渐深入到原理层,并通过 ReentrantLock 的基本特性和 ReentrantLock 与 AQS 的关联,来深入解读 AQS 相关独占锁的知识点,同时采取问答的模式来帮助大家理解 AQS。由于篇幅原因,本篇文章主要阐述 AQS 中独占锁的逻辑和 Sync Queue,不讲述包含共享锁和 Condition Queue 的部分(本篇文章核心为 AQS 原理剖析,只是简单介绍了 ReentrantLock,感兴趣同学可以阅读一下 ReentrantLock 的源码)。 +This article will gradually delve from the application layer to the principle layer, and through the basic characteristics of ReentrantLock and its association with AQS, we will deeply interpret the knowledge points related to exclusive locks in AQS, while adopting a Q&A format to help everyone understand AQS. Due to space constraints, this article mainly elaborates on the logic of exclusive locks in AQS and the Sync Queue, without discussing the parts that include shared locks and Condition Queues (the core of this article is the analysis of AQS principles, and only a brief introduction to ReentrantLock is provided; interested readers can refer to the source code of ReentrantLock). ## 1 ReentrantLock -### 1.1 ReentrantLock 特性概览 +### 1.1 Overview of ReentrantLock Features -ReentrantLock 意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。为了帮助大家更好地理解 ReentrantLock 的特性,我们先将 ReentrantLock 跟常用的 Synchronized 进行比较,其特性如下(蓝色部分为本篇文章主要剖析的点): +ReentrantLock means a reentrant lock, which refers to a thread being able to lock a critical resource repeatedly. To help everyone better understand the features of ReentrantLock, we will first compare ReentrantLock with the commonly used Synchronized, with the following features (the blue parts are the main points analyzed in this article): ![](https://p0.meituan.net/travelcube/412d294ff5535bbcddc0d979b2a339e6102264.png) -下面通过伪代码,进行更加直观的比较: +Below is a more intuitive comparison through pseudocode: ```java -// **************************Synchronized的使用方式************************** -// 1.用于代码块 +// **************************Usage of Synchronized************************** +// 1. For code blocks synchronized (this) {} -// 2.用于对象 +// 2. For objects synchronized (object) {} -// 3.用于方法 +// 3. For methods public synchronized void test () {} -// 4.可重入 +// 4. Reentrant for (int i = 0; i < 100; i++) { synchronized (this) {} } -// **************************ReentrantLock的使用方式************************** -public void test () throw Exception { - // 1.初始化选择公平锁、非公平锁 +// **************************Usage of ReentrantLock************************** +public void test () throws Exception { + // 1. Initialize to choose fair or unfair lock ReentrantLock lock = new ReentrantLock(true); - // 2.可用于代码块 + // 2. Can be used for code blocks lock.lock(); try { try { - // 3.支持多种加锁方式,比较灵活; 具有可重入特性 + // 3. Supports various locking methods, more flexible; has reentrant characteristics if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ } } finally { - // 4.手动释放锁 - lock.unlock() + // 4. Manually release the lock + lock.unlock(); } } finally { lock.unlock(); @@ -55,16 +55,16 @@ public void test () throw Exception { } ``` -### 1.2 ReentrantLock 与 AQS 的关联 +### 1.2 Association between ReentrantLock and AQS -通过上文我们已经了解,ReentrantLock 支持公平锁和非公平锁(关于公平锁和非公平锁的原理分析,可参考《[不可不说的 Java“锁”事](https://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651749434&idx=3&sn=5ffa63ad47fe166f2f1a9f604ed10091&chksm=bd12a5778a652c61509d9e718ab086ff27ad8768586ea9b38c3dcf9e017a8e49bcae3df9bcc8&scene=38#wechat_redirect)》),并且 ReentrantLock 的底层就是由 AQS 来实现的。那么 ReentrantLock 是如何通过公平锁和非公平锁与 AQS 关联起来呢? 我们着重从这两者的加锁过程来理解一下它们与 AQS 之间的关系(加锁过程中与 AQS 的关联比较明显,解锁流程后续会介绍)。 +From the above, we have learned that ReentrantLock supports both fair and unfair locks (for an analysis of the principles of fair and unfair locks, refer to “[Java Lock Matters You Must Know](https://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651749434&idx=3&sn=5ffa63ad47fe166f2f1a9f604ed10091&chksm=bd12a5778a652c61509d9e718ab086ff27ad8768586ea9b38c3dcf9e017a8e49bcae3df9bcc8&scene=38#wechat_redirect)”), and the underlying implementation of ReentrantLock is based on AQS. So how does ReentrantLock associate with AQS through fair and unfair locks? We will focus on understanding their relationship with AQS from the locking process (the association with AQS during the locking process is quite obvious, and the unlocking process will be introduced later). -非公平锁源码中的加锁流程如下: +The locking process in the unfair lock source code is as follows: ```java // java.util.concurrent.locks.ReentrantLock#NonfairSync -// 非公平锁 +// Unfair lock static final class NonfairSync extends Sync { ... final void lock() { @@ -77,946 +77,9 @@ static final class NonfairSync extends Sync { } ``` -这块代码的含义为: +The meaning of this code is: -- 若通过 CAS 设置变量 State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。 -- 若通过 CAS 设置变量 State(同步状态)失败,也就是获取锁失败,则进入 Acquire 方法进行后续处理。 +- If the CAS operation to set the State variable (synchronization state) is successful, meaning the lock is successfully acquired, then set the current thread as the exclusive owner thread. +- If the CAS operation to set the State variable (synchronization state) fails, meaning the lock acquisition failed, then enter the Acquire method for subsequent processing. -第一步很好理解,但第二步获取锁失败后,后续的处理策略是怎么样的呢?这块可能会有以下思考: - -- 某个线程获取锁失败的后续流程是什么呢?有以下两种可能: - -(1) 将当前线程获锁结果设置为失败,获取锁流程结束。这种设计会极大降低系统的并发度,并不满足我们实际的需求。所以就需要下面这种流程,也就是 AQS 框架的处理流程。 - -(2) 存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续。 - -- 对于问题 1 的第二种情况,既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢? -- 处于排队等候机制中的线程,什么时候可以有机会获取锁呢? -- 如果处于排队等候机制中的线程一直无法获取锁,还是需要一直等待吗,还是有别的策略来解决这一问题? - -带着非公平锁的这些问题,再看下公平锁源码中获锁的方式: - -```java -// java.util.concurrent.locks.ReentrantLock#FairSync - -static final class FairSync extends Sync { - ... - final void lock() { - acquire(1); - } - ... -} -``` - -看到这块代码,我们可能会存在这种疑问:Lock 函数通过 Acquire 方法进行加锁,但是具体是如何加锁的呢? - -结合公平锁和非公平锁的加锁流程,虽然流程上有一定的不同,但是都调用了 Acquire 方法,而 Acquire 方法是 FairSync 和 UnfairSync 的父类 AQS 中的核心方法。 - -对于上边提到的问题,其实在 ReentrantLock 类源码中都无法解答,而这些问题的答案,都是位于 Acquire 方法所在的类 AbstractQueuedSynchronizer 中,也就是本文的核心——AQS。下面我们会对 AQS 以及 ReentrantLock 和 AQS 的关联做详细介绍(相关问题答案会在 2.3.5 小节中解答)。 - -## 2 AQS - -首先,我们通过下面的架构图来整体了解一下 AQS 框架: - -![](https://p1.meituan.net/travelcube/82077ccf14127a87b77cefd1ccf562d3253591.png) - -- 上图中有颜色的为 Method,无颜色的为 Attribution。 -- 总的来说,AQS 框架共分为五层,自上而下由浅入深,从 AQS 对外暴露的 API 到底层基础数据。 -- 当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时,先经过第一层的 API 进入 AQS 内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层。 - -下面我们会从整体到细节,从流程到方法逐一剖析 AQS 框架,主要分析过程如下: - -![](https://p1.meituan.net/travelcube/d2f7f7fffdc30d85d17b44266c3ab05323338.png) - -### 2.1 原理概览 - -AQS 核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是 CLH 队列的变体实现的,将暂时获取不到锁的线程加入到队列中。 - -CLH:Craig、Landin and Hagersten 队列,是单向链表,AQS 中的队列是 CLH 变体的虚拟双向队列(FIFO),AQS 是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。 - -主要原理图如下: - -![](https://p0.meituan.net/travelcube/7132e4cef44c26f62835b197b239147b18062.png) - -AQS 使用一个 Volatile 的 int 类型的成员变量来表示同步状态,通过内置的 FIFO 队列来完成资源获取的排队工作,通过 CAS 完成对 State 值的修改。 - -#### 2.1.1 AQS 数据结构 - -先来看下 AQS 中最基本的数据结构——Node,Node 即为上面 CLH 变体队列中的节点。 - -![](https://p1.meituan.net/travelcube/960271cf2b5c8a185eed23e98b72c75538637.png) - -解释一下几个方法和属性值的含义: - -| 方法和属性值 | 含义 | -| :----------- | :----------------------------------------------------------------------------------------------- | -| waitStatus | 当前节点在队列中的状态 | -| thread | 表示处于该节点的线程 | -| prev | 前驱指针 | -| predecessor | 返回前驱节点,没有的话抛出 npe | -| nextWaiter | 指向下一个处于 CONDITION 状态的节点(由于本篇文章不讲述 Condition Queue 队列,这个指针不多介绍) | -| next | 后继指针 | - -线程两种锁的模式: - -| 模式 | 含义 | -| :-------- | :----------------------------- | -| SHARED | 表示线程以共享的模式等待锁 | -| EXCLUSIVE | 表示线程正在以独占的方式等待锁 | - -waitStatus 有下面几个枚举值: - -| 枚举 | 含义 | -| :-------- | :----------------------------------------------- | -| 0 | 当一个 Node 被初始化的时候的默认值 | -| CANCELLED | 为 1,表示线程获取锁的请求已经取消了 | -| CONDITION | 为-2,表示节点在等待队列中,节点线程等待唤醒 | -| PROPAGATE | 为-3,当前线程处在 SHARED 情况下,该字段才会使用 | -| SIGNAL | 为-1,表示线程已经准备好了,就等资源释放了 | - -#### 2.1.2 同步状态 State - -在了解数据结构后,接下来了解一下 AQS 的同步状态——State。AQS 中维护了一个名为 state 的字段,意为同步状态,是由 Volatile 修饰的,用于展示当前临界资源的获锁情况。 - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -private volatile int state; -``` - -下面提供了几个访问这个字段的方法: - -| 方法名 | 描述 | -| :----------------------------------------------------------------- | :---------------------- | -| protected final int getState() | 获取 State 的值 | -| protected final void setState(int newState) | 设置 State 的值 | -| protected final boolean compareAndSetState(int expect, int update) | 使用 CAS 方式更新 State | - -这几个方法都是 Final 修饰的,说明子类中无法重写它们。我们可以通过修改 State 字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程)。 - -![](https://p0.meituan.net/travelcube/27605d483e8935da683a93be015713f331378.png) - -![](https://p0.meituan.net/travelcube/3f1e1a44f5b7d77000ba4f9476189b2e32806.png) - -对于我们自定义的同步工具,需要自定义获取同步状态和释放状态的方式,也就是 AQS 架构图中的第一层:API 层。 - -### 2.2 AQS 重要方法与 ReentrantLock 的关联 - -从架构图中可以得知,AQS 提供了大量用于自定义同步器实现的 Protected 方法。自定义同步器实现的相关方法也只是为了通过修改 State 字段来实现多线程的独占模式或者共享模式。自定义同步器需要实现以下方法(ReentrantLock 需要实现的方法如下,并不是全部): - -| 方法名 | 描述 | -| :------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------- | -| protected boolean isHeldExclusively() | 该线程是否正在独占资源。只有用到 Condition 才需要去实现它。 | -| protected boolean tryAcquire(int arg) | 独占方式。arg 为获取锁的次数,尝试获取资源,成功则返回 True,失败则返回 False。 | -| protected boolean tryRelease(int arg) | 独占方式。arg 为释放锁的次数,尝试释放资源,成功则返回 True,失败则返回 False。 | -| protected int tryAcquireShared(int arg) | 共享方式。arg 为获取锁的次数,尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 | -| protected boolean tryReleaseShared(int arg) | 共享方式。arg 为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回 True,否则返回 False。 | - -一般来说,自定义同步器要么是独占方式,要么是共享方式,它们也只需实现 tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。AQS 也支持自定义同步器同时实现独占和共享两种方式,如 ReentrantReadWriteLock。ReentrantLock 是独占锁,所以实现了 tryAcquire-tryRelease。 - -以非公平锁为例,这里主要阐述一下非公平锁与 AQS 之间方法的关联之处,具体每一处核心方法的作用会在文章后面详细进行阐述。 - -![](https://p1.meituan.net/travelcube/b8b53a70984668bc68653efe9531573e78636.png) - -> 🐛 修正(参见:[issue#1761](https://github.com/Snailclimb/JavaGuide/issues/1761)): 图中的一处小错误,(AQS)CAS 修改共享资源 State 成功之后应该是获取锁成功(非公平锁)。 -> -> 对应的源码如下: -> -> ```java -> final boolean nonfairTryAcquire(int acquires) { -> final Thread current = Thread.currentThread();//获取当前线程 -> int c = getState(); -> if (c == 0) { -> if (compareAndSetState(0, acquires)) {//CAS抢锁 -> setExclusiveOwnerThread(current);//设置当前线程为独占线程 -> return true;//抢锁成功 -> } -> } -> else if (current == getExclusiveOwnerThread()) { -> int nextc = c + acquires; -> if (nextc < 0) // overflow -> throw new Error("Maximum lock count exceeded"); -> setState(nextc); -> return true; -> } -> return false; -> } -> ``` - -为了帮助大家理解 ReentrantLock 和 AQS 之间方法的交互过程,以非公平锁为例,我们将加锁和解锁的交互流程单独拎出来强调一下,以便于对后续内容的理解。 - -![](https://p1.meituan.net/travelcube/7aadb272069d871bdee8bf3a218eed8136919.png) - -加锁: - -- 通过 ReentrantLock 的加锁方法 Lock 进行加锁操作。 -- 会调用到内部类 Sync 的 Lock 方法,由于 Sync#lock 是抽象方法,根据 ReentrantLock 初始化选择的公平锁和非公平锁,执行相关内部类的 Lock 方法,本质上都会执行 AQS 的 Acquire 方法。 -- AQS 的 Acquire 方法会执行 tryAcquire 方法,但是由于 tryAcquire 需要自定义同步器实现,因此执行了 ReentrantLock 中的 tryAcquire 方法,由于 ReentrantLock 是通过公平锁和非公平锁内部类实现的 tryAcquire 方法,因此会根据锁类型不同,执行不同的 tryAcquire。 -- tryAcquire 是获取锁逻辑,获取失败后,会执行框架 AQS 的后续逻辑,跟 ReentrantLock 自定义同步器无关。 - -解锁: - -- 通过 ReentrantLock 的解锁方法 Unlock 进行解锁。 -- Unlock 会调用内部类 Sync 的 Release 方法,该方法继承于 AQS。 -- Release 中会调用 tryRelease 方法,tryRelease 需要自定义同步器实现,tryRelease 只在 ReentrantLock 中的 Sync 实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。 -- 释放成功后,所有处理由 AQS 框架完成,与自定义同步器无关。 - -通过上面的描述,大概可以总结出 ReentrantLock 加锁解锁时 API 层核心方法的映射关系。 - -![](https://p0.meituan.net/travelcube/f30c631c8ebbf820d3e8fcb6eee3c0ef18748.png) - -## 3 通过 ReentrantLock 理解 AQS - -ReentrantLock 中公平锁和非公平锁在底层是相同的,这里以非公平锁为例进行分析。 - -在非公平锁中,有一段这样的代码: - -```java -// java.util.concurrent.locks.ReentrantLock - -static final class NonfairSync extends Sync { - ... - final void lock() { - if (compareAndSetState(0, 1)) - setExclusiveOwnerThread(Thread.currentThread()); - else - acquire(1); - } - ... -} -``` - -看一下这个 Acquire 是怎么写的: - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -public final void acquire(int arg) { - if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) - selfInterrupt(); -} -``` - -再看一下 tryAcquire 方法: - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -protected boolean tryAcquire(int arg) { - throw new UnsupportedOperationException(); -} -``` - -可以看出,这里只是 AQS 的简单实现,具体获取锁的实现方法是由各自的公平锁和非公平锁单独实现的(以 ReentrantLock 为例)。如果该方法返回了 True,则说明当前线程获取锁成功,就不用往后执行了;如果获取失败,就需要加入到等待队列中。下面会详细解释线程是何时以及怎样被加入进等待队列中的。 - -### 3.1 线程加入等待队列 - -#### 3.1.1 加入队列的时机 - -当执行 Acquire(1)时,会通过 tryAcquire 获取锁。在这种情况下,如果获取锁失败,就会调用 addWaiter 加入到等待队列中去。 - -#### 3.1.2 如何加入队列 - -获取锁失败后,会执行 addWaiter(Node.EXCLUSIVE)加入等待队列,具体实现方法如下: - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -private Node addWaiter(Node mode) { - Node node = new Node(Thread.currentThread(), mode); - // Try the fast path of enq; backup to full enq on failure - Node pred = tail; - if (pred != null) { - node.prev = pred; - if (compareAndSetTail(pred, node)) { - pred.next = node; - return node; - } - } - enq(node); - return node; -} -private final boolean compareAndSetTail(Node expect, Node update) { - return unsafe.compareAndSwapObject(this, tailOffset, expect, update); -} -``` - -主要的流程如下: - -- 通过当前的线程和锁模式新建一个节点。 -- Pred 指针指向尾节点 Tail。 -- 将 New 中 Node 的 Prev 指针指向 Pred。 -- 通过 compareAndSetTail 方法,完成尾节点的设置。这个方法主要是对 tailOffset 和 Expect 进行比较,如果 tailOffset 的 Node 和 Expect 的 Node 地址是相同的,那么设置 Tail 的值为 Update 的值。 - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -static { - try { - stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state")); - headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head")); - tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail")); - waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus")); - nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("next")); - } catch (Exception ex) { - throw new Error(ex); - } -} -``` - -从 AQS 的静态代码块可以看出,都是获取一个对象的属性相对于该对象在内存当中的偏移量,这样我们就可以根据这个偏移量在对象内存当中找到这个属性。tailOffset 指的是 tail 对应的偏移量,所以这个时候会将 new 出来的 Node 置为当前队列的尾节点。同时,由于是双向链表,也需要将前一个节点指向尾节点。 - -- 如果 Pred 指针是 Null(说明等待队列中没有元素),或者当前 Pred 指针和 Tail 指向的位置不同(说明被别的线程已经修改),就需要看一下 Enq 的方法。 - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -private Node enq(final Node node) { - for (;;) { - Node t = tail; - if (t == null) { // Must initialize - if (compareAndSetHead(new Node())) - tail = head; - } else { - node.prev = t; - if (compareAndSetTail(t, node)) { - t.next = node; - return t; - } - } - } -} -``` - -如果没有被初始化,需要进行初始化一个头结点出来。但请注意,初始化的头结点并不是当前线程节点,而是调用了无参构造函数的节点。如果经历了初始化或者并发导致队列中有元素,则与之前的方法相同。其实,addWaiter 就是一个在双端链表添加尾节点的操作,需要注意的是,双端链表的头结点是一个无参构造函数的头结点。 - -总结一下,线程获取锁的时候,过程大体如下: - -1、当没有线程获取到锁时,线程 1 获取锁成功。 - -2、线程 2 申请锁,但是锁被线程 1 占有。 - -![img](https://p0.meituan.net/travelcube/e9e385c3c68f62c67c8d62ab0adb613921117.png) - -3、如果再有线程要获取锁,依次在队列中往后排队即可。 - -回到上边的代码,hasQueuedPredecessors 是公平锁加锁时判断等待队列中是否存在有效节点的方法。如果返回 False,说明当前线程可以争取共享资源;如果返回 True,说明队列中存在有效节点,当前线程必须加入到等待队列中。 - -```java -// java.util.concurrent.locks.ReentrantLock - -public final boolean hasQueuedPredecessors() { - // The correctness of this depends on head being initialized - // before tail and on head.next being accurate if the current - // thread is first in queue. - Node t = tail; // Read fields in reverse initialization order - Node h = head; - Node s; - return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); -} -``` - -看到这里,我们理解一下 h != t && ((s = h.next) == null || s.thread != Thread.currentThread());为什么要判断的头结点的下一个节点?第一个节点储存的数据是什么? - -> 双向链表中,第一个节点为虚节点,其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是在第二个节点开始的。当 h != t 时:如果(s = h.next) == null,等待队列正在有线程进行初始化,但只是进行到了 Tail 指向 Head,没有将 Head 指向 Tail,此时队列中有元素,需要返回 True(这块具体见下边代码分析)。 如果(s = h.next) != null,说明此时队列中至少有一个有效节点。如果此时 s.thread == Thread.currentThread(),说明等待队列的第一个有效节点中的线程与当前线程相同,那么当前线程是可以获取资源的;如果 s.thread != Thread.currentThread(),说明等待队列的第一个有效节点线程与当前线程不同,当前线程必须加入进等待队列。 - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer#enq - -if (t == null) { // Must initialize - if (compareAndSetHead(new Node())) - tail = head; -} else { - node.prev = t; - if (compareAndSetTail(t, node)) { - t.next = node; - return t; - } -} -``` - -节点入队不是原子操作,所以会出现短暂的 head != tail,此时 Tail 指向最后一个节点,而且 Tail 指向 Head。如果 Head 没有指向 Tail(可见 5、6、7 行),这种情况下也需要将相关线程加入队列中。所以这块代码是为了解决极端情况下的并发问题。 - -#### 3.1.3 等待队列中线程出队列时机 - -回到最初的源码: - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -public final void acquire(int arg) { - if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) - selfInterrupt(); -} -``` - -上文解释了 addWaiter 方法,这个方法其实就是把对应的线程以 Node 的数据结构形式加入到双端队列里,返回的是一个包含该线程的 Node。而这个 Node 会作为参数,进入到 acquireQueued 方法中。acquireQueued 方法可以对排队中的线程进行“获锁”操作。 - -总的来说,一个线程获取锁失败了,被放入等待队列,acquireQueued 会把放入队列中的线程不断去获取锁,直到获取成功或者不再需要获取(中断)。 - -下面我们从“何时出队列?”和“如何出队列?”两个方向来分析一下 acquireQueued 源码: - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -final boolean acquireQueued(final Node node, int arg) { - // 标记是否成功拿到资源 - boolean failed = true; - try { - // 标记等待过程中是否中断过 - boolean interrupted = false; - // 开始自旋,要么获取锁,要么中断 - for (;;) { - // 获取当前节点的前驱节点 - final Node p = node.predecessor(); - // 如果p是头结点,说明当前节点在真实数据队列的首部,就尝试获取锁(别忘了头结点是虚节点) - if (p == head && tryAcquire(arg)) { - // 获取锁成功,头指针移动到当前node - setHead(node); - p.next = null; // help GC - failed = false; - return interrupted; - } - // 说明p为头节点且当前没有获取到锁(可能是非公平锁被抢占了)或者是p不为头结点,这个时候就要判断当前node是否要被阻塞(被阻塞条件:前驱节点的waitStatus为-1),防止无限循环浪费资源。具体两个方法下面细细分析 - if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) - interrupted = true; - } - } finally { - if (failed) - cancelAcquire(node); - } -} -``` - -注:setHead 方法是把当前节点置为虚节点,但并没有修改 waitStatus,因为它是一直需要用的数据。 - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -private void setHead(Node node) { - head = node; - node.thread = null; - node.prev = null; -} - -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -// 靠前驱节点判断当前线程是否应该被阻塞 -private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { - // 获取头结点的节点状态 - int ws = pred.waitStatus; - // 说明头结点处于唤醒状态 - if (ws == Node.SIGNAL) - return true; - // 通过枚举值我们知道waitStatus>0是取消状态 - if (ws > 0) { - do { - // 循环向前查找取消节点,把取消节点从队列中剔除 - node.prev = pred = pred.prev; - } while (pred.waitStatus > 0); - pred.next = node; - } else { - // 设置前任节点等待状态为SIGNAL - compareAndSetWaitStatus(pred, ws, Node.SIGNAL); - } - return false; -} -``` - -parkAndCheckInterrupt 主要用于挂起当前线程,阻塞调用栈,返回当前线程的中断状态。 - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -private final boolean parkAndCheckInterrupt() { - LockSupport.park(this); - return Thread.interrupted(); -} -``` - -上述方法的流程图如下: - -![](https://p0.meituan.net/travelcube/c124b76dcbefb9bdc778458064703d1135485.png) - -从上图可以看出,跳出当前循环的条件是当“前置节点是头结点,且当前线程获取锁成功”。为了防止因死循环导致 CPU 资源被浪费,我们会判断前置节点的状态来决定是否要将当前线程挂起,具体挂起流程用流程图表示如下(shouldParkAfterFailedAcquire 流程): - -![](https://p0.meituan.net/travelcube/9af16e2481ad85f38ca322a225ae737535740.png) - -从队列中释放节点的疑虑打消了,那么又有新问题了: - -- shouldParkAfterFailedAcquire 中取消节点是怎么生成的呢?什么时候会把一个节点的 waitStatus 设置为-1? -- 是在什么时间释放节点通知到被挂起的线程呢? - -### 3.2 CANCELLED 状态节点生成 - -acquireQueued 方法中的 Finally 代码: - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -final boolean acquireQueued(final Node node, int arg) { - boolean failed = true; - try { - ... - for (;;) { - final Node p = node.predecessor(); - if (p == head && tryAcquire(arg)) { - ... - failed = false; - ... - } - ... - } finally { - if (failed) - cancelAcquire(node); - } -} -``` - -通过 cancelAcquire 方法,将 Node 的状态标记为 CANCELLED。接下来,我们逐行来分析这个方法的原理: - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -private void cancelAcquire(Node node) { - // 将无效节点过滤 - if (node == null) - return; - // 设置该节点不关联任何线程,也就是虚节点 - node.thread = null; - Node pred = node.prev; - // 通过前驱节点,跳过取消状态的node - while (pred.waitStatus > 0) - node.prev = pred = pred.prev; - // 获取过滤后的前驱节点的后继节点 - Node predNext = pred.next; - // 把当前node的状态设置为CANCELLED - node.waitStatus = Node.CANCELLED; - // 如果当前节点是尾节点,将从后往前的第一个非取消状态的节点设置为尾节点 - // 更新失败的话,则进入else,如果更新成功,将tail的后继节点设置为null - if (node == tail && compareAndSetTail(node, pred)) { - compareAndSetNext(pred, predNext, null); - } else { - int ws; - // 如果当前节点不是head的后继节点,1:判断当前节点前驱节点的是否为SIGNAL,2:如果不是,则把前驱节点设置为SIGNAL看是否成功 - // 如果1和2中有一个为true,再判断当前节点的线程是否为null - // 如果上述条件都满足,把当前节点的前驱节点的后继指针指向当前节点的后继节点 - if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { - Node next = node.next; - if (next != null && next.waitStatus <= 0) - compareAndSetNext(pred, predNext, next); - } else { - // 如果当前节点是head的后继节点,或者上述条件不满足,那就唤醒当前节点的后继节点 - unparkSuccessor(node); - } - node.next = node; // help GC - } -} -``` - -当前的流程: - -- 获取当前节点的前驱节点,如果前驱节点的状态是 CANCELLED,那就一直往前遍历,找到第一个 waitStatus <= 0 的节点,将找到的 Pred 节点和当前 Node 关联,将当前 Node 设置为 CANCELLED。 -- 根据当前节点的位置,考虑以下三种情况: - -(1) 当前节点是尾节点。 - -(2) 当前节点是 Head 的后继节点。 - -(3) 当前节点不是 Head 的后继节点,也不是尾节点。 - -根据上述第二条,我们来分析每一种情况的流程。 - -当前节点是尾节点。 - -![](https://p1.meituan.net/travelcube/b845211ced57561c24f79d56194949e822049.png) - -当前节点是 Head 的后继节点。 - -![](https://p1.meituan.net/travelcube/ab89bfec875846e5028a4f8fead32b7117975.png) - -当前节点不是 Head 的后继节点,也不是尾节点。 - -![](https://p0.meituan.net/travelcube/45d0d9e4a6897eddadc4397cf53d6cd522452.png) - -通过上面的流程,我们对于 CANCELLED 节点状态的产生和变化已经有了大致的了解,但是为什么所有的变化都是对 Next 指针进行了操作,而没有对 Prev 指针进行操作呢?什么情况下会对 Prev 指针进行操作? - -> 执行 cancelAcquire 的时候,当前节点的前置节点可能已经从队列中出去了(已经执行过 Try 代码块中的 shouldParkAfterFailedAcquire 方法了),如果此时修改 Prev 指针,有可能会导致 Prev 指向另一个已经移除队列的 Node,因此这块变化 Prev 指针不安全。 shouldParkAfterFailedAcquire 方法中,会执行下面的代码,其实就是在处理 Prev 指针。shouldParkAfterFailedAcquire 是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化,因此这个时候变更 Prev 指针比较安全。 -> -> ```java -> do { -> node.prev = pred = pred.prev; -> } while (pred.waitStatus > 0); -> ``` - -### 3.3 如何解锁 - -我们已经剖析了加锁过程中的基本流程,接下来再对解锁的基本流程进行分析。由于 ReentrantLock 在解锁的时候,并不区分公平锁和非公平锁,所以我们直接看解锁的源码: - -```java -// java.util.concurrent.locks.ReentrantLock - -public void unlock() { - sync.release(1); -} -``` - -可以看到,本质释放锁的地方,是通过框架来完成的。 - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -public final boolean release(int arg) { - if (tryRelease(arg)) { - Node h = head; - if (h != null && h.waitStatus != 0) - unparkSuccessor(h); - return true; - } - return false; -} -``` - -在 ReentrantLock 里面的公平锁和非公平锁的父类 Sync 定义了可重入锁的释放锁机制。 - -```java -// java.util.concurrent.locks.ReentrantLock.Sync - -// 方法返回当前锁是不是没有被线程持有 -protected final boolean tryRelease(int releases) { - // 减少可重入次数 - int c = getState() - releases; - // 当前线程不是持有锁的线程,抛出异常 - if (Thread.currentThread() != getExclusiveOwnerThread()) - throw new IllegalMonitorStateException(); - boolean free = false; - // 如果持有线程全部释放,将当前独占锁所有线程设置为null,并更新state - if (c == 0) { - free = true; - setExclusiveOwnerThread(null); - } - setState(c); - return free; -} -``` - -我们来解释下述源码: - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -public final boolean release(int arg) { - // 上边自定义的tryRelease如果返回true,说明该锁没有被任何线程持有 - if (tryRelease(arg)) { - // 获取头结点 - Node h = head; - // 头结点不为空并且头结点的waitStatus不是初始化节点情况,解除线程挂起状态 - if (h != null && h.waitStatus != 0) - unparkSuccessor(h); - return true; - } - return false; -} -``` - -这里的判断条件为什么是 h != null && h.waitStatus != 0? - -> h == null Head 还没初始化。初始情况下,head == null,第一个节点入队,Head 会被初始化一个虚拟节点。所以说,这里如果还没来得及入队,就会出现 head == null 的情况。 -> -> h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒。 -> -> h != null && waitStatus < 0 表明后继节点可能被阻塞了,需要唤醒。 - -再看一下 unparkSuccessor 方法: - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -private void unparkSuccessor(Node node) { - // 获取头结点waitStatus - int ws = node.waitStatus; - if (ws < 0) - compareAndSetWaitStatus(node, ws, 0); - // 获取当前节点的下一个节点 - Node s = node.next; - // 如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点 - if (s == null || s.waitStatus > 0) { - s = null; - // 就从尾部节点开始找,到队首,找到队列第一个waitStatus<0的节点。 - for (Node t = tail; t != null && t != node; t = t.prev) - if (t.waitStatus <= 0) - s = t; - } - // 如果当前节点的下个节点不为空,而且状态<=0,就把当前节点unpark - if (s != null) - LockSupport.unpark(s.thread); -} -``` - -为什么要从后往前找第一个非 Cancelled 的节点呢?原因如下。 - -之前的 addWaiter 方法: - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -private Node addWaiter(Node mode) { - Node node = new Node(Thread.currentThread(), mode); - // Try the fast path of enq; backup to full enq on failure - Node pred = tail; - if (pred != null) { - node.prev = pred; - if (compareAndSetTail(pred, node)) { - pred.next = node; - return node; - } - } - enq(node); - return node; -} -``` - -我们从这里可以看到,节点入队并不是原子操作,也就是说,node.prev = pred; compareAndSetTail(pred, node) 这两个地方可以看作 Tail 入队的原子操作,但是此时 pred.next = node;还没执行,如果这个时候执行了 unparkSuccessor 方法,就没办法从前往后找了,所以需要从后往前找。还有一点原因,在产生 CANCELLED 状态节点的时候,先断开的是 Next 指针,Prev 指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的 Node。 - -综上所述,如果是从前往后找,由于极端情况下入队的非原子操作和 CANCELLED 节点产生过程中断开 Next 指针的操作,可能会导致无法遍历所有的节点。所以,唤醒对应的线程后,对应的线程就会继续往下执行。继续执行 acquireQueued 方法以后,中断如何处理? - -### 3.4 中断恢复后的执行流程 - -唤醒后,会执行 return Thread.interrupted();,这个函数返回的是当前执行线程的中断状态,并清除。 - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -private final boolean parkAndCheckInterrupt() { - LockSupport.park(this); - return Thread.interrupted(); -} -``` - -再回到 acquireQueued 代码,当 parkAndCheckInterrupt 返回 True 或者 False 的时候,interrupted 的值不同,但都会执行下次循环。如果这个时候获取锁成功,就会把当前 interrupted 返回。 - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -final boolean acquireQueued(final Node node, int arg) { - boolean failed = true; - try { - boolean interrupted = false; - for (;;) { - final Node p = node.predecessor(); - if (p == head && tryAcquire(arg)) { - setHead(node); - p.next = null; // help GC - failed = false; - return interrupted; - } - if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) - interrupted = true; - } - } finally { - if (failed) - cancelAcquire(node); - } -} -``` - -如果 acquireQueued 为 True,就会执行 selfInterrupt 方法。 - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -static void selfInterrupt() { - Thread.currentThread().interrupt(); -} -``` - -该方法其实是为了中断线程。但为什么获取了锁以后还要中断线程呢?这部分属于 Java 提供的协作式中断知识内容,感兴趣同学可以查阅一下。这里简单介绍一下: - -1. 当中断线程被唤醒时,并不知道被唤醒的原因,可能是当前线程在等待中被中断,也可能是释放了锁以后被唤醒。因此我们通过 Thread.interrupted()方法检查中断标记(该方法返回了当前线程的中断状态,并将当前线程的中断标识设置为 False),并记录下来,如果发现该线程被中断过,就再中断一次。 -2. 线程在等待资源的过程中被唤醒,唤醒后还是会不断地去尝试获取锁,直到抢到锁为止。也就是说,在整个流程中,并不响应中断,只是记录中断记录。最后抢到锁返回了,那么如果被中断过的话,就需要补充一次中断。 - -这里的处理方式主要是运用线程池中基本运作单元 Worder 中的 runWorker,通过 Thread.interrupted()进行额外的判断处理,感兴趣的同学可以看下 ThreadPoolExecutor 源码。 - -### 3.5 小结 - -我们在 1.3 小节中提出了一些问题,现在来回答一下。 - -> Q:某个线程获取锁失败的后续流程是什么呢? -> -> A:存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续。 -> -> Q:既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢? -> -> A:是 CLH 变体的 FIFO 双端队列。 -> -> Q:处于排队等候机制中的线程,什么时候可以有机会获取锁呢? -> -> A:可以详细看下 2.3.1.3 小节。 -> -> Q:如果处于排队等候机制中的线程一直无法获取锁,需要一直等待么?还是有别的策略来解决这一问题? -> -> A:线程所在节点的状态会变成取消状态,取消状态的节点会从队列中释放,具体可见 2.3.2 小节。 -> -> Q:Lock 函数通过 Acquire 方法进行加锁,但是具体是如何加锁的呢? -> -> A:AQS 的 Acquire 会调用 tryAcquire 方法,tryAcquire 由各个自定义同步器实现,通过 tryAcquire 完成加锁过程。 - -## 4 AQS 应用 - -### 4.1 ReentrantLock 的可重入应用 - -ReentrantLock 的可重入性是 AQS 很好的应用之一,在了解完上述知识点以后,我们很容易得知 ReentrantLock 实现可重入的方法。在 ReentrantLock 里面,不管是公平锁还是非公平锁,都有一段逻辑。 - -公平锁: - -```java -// java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquire - -if (c == 0) { - if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { - setExclusiveOwnerThread(current); - return true; - } -} -else if (current == getExclusiveOwnerThread()) { - int nextc = c + acquires; - if (nextc < 0) - throw new Error("Maximum lock count exceeded"); - setState(nextc); - return true; -} -``` - -非公平锁: - -```java -// java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire - -if (c == 0) { - if (compareAndSetState(0, acquires)){ - setExclusiveOwnerThread(current); - return true; - } -} -else if (current == getExclusiveOwnerThread()) { - int nextc = c + acquires; - if (nextc < 0) // overflow - throw new Error("Maximum lock count exceeded"); - setState(nextc); - return true; -} -``` - -从上面这两段都可以看到,有一个同步状态 State 来控制整体可重入的情况。State 是 Volatile 修饰的,用于保证一定的可见性和有序性。 - -```java -// java.util.concurrent.locks.AbstractQueuedSynchronizer - -private volatile int state; -``` - -接下来看 State 这个字段主要的过程: - -1. State 初始化的时候为 0,表示没有任何线程持有锁。 -2. 当有线程持有该锁时,值就会在原来的基础上+1,同一个线程多次获得锁是,就会多次+1,这里就是可重入的概念。 -3. 解锁也是对这个字段-1,一直到 0,此线程对锁释放。 - -### 4.2 JUC 中的应用场景 - -除了上边 ReentrantLock 的可重入性的应用,AQS 作为并发编程的框架,为很多其他同步工具提供了良好的解决方案。下面列出了 JUC 中的几种同步工具,大体介绍一下 AQS 的应用场景: - -| 同步工具 | 同步工具与 AQS 的关联 | -| :--------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ReentrantLock | 使用 AQS 保存锁重复持有的次数。当一个线程获取锁时,ReentrantLock 记录当前获得锁的线程标识,用于检测是否重复获取,以及错误线程试图解锁操作时异常情况的处理。 | -| Semaphore | 使用 AQS 同步状态来保存信号量的当前计数。tryRelease 会增加计数,acquireShared 会减少计数。 | -| CountDownLatch | 使用 AQS 同步状态来表示计数。计数为 0 时,所有的 Acquire 操作(CountDownLatch 的 await 方法)才可以通过。 | -| ReentrantReadWriteLock | 使用 AQS 同步状态中的 16 位保存写锁持有的次数,剩下的 16 位用于保存读锁的持有次数。 | -| ThreadPoolExecutor | Worker 利用 AQS 同步状态实现对独占线程变量的设置(tryAcquire 和 tryRelease)。 | - -### 4.3 自定义同步工具 - -了解 AQS 基本原理以后,按照上面所说的 AQS 知识点,自己实现一个同步工具。 - -```java -public class LeeLock { - - private static class Sync extends AbstractQueuedSynchronizer { - @Override - protected boolean tryAcquire (int arg) { - return compareAndSetState(0, 1); - } - - @Override - protected boolean tryRelease (int arg) { - setState(0); - return true; - } - - @Override - protected boolean isHeldExclusively () { - return getState() == 1; - } - } - - private Sync sync = new Sync(); - - public void lock () { - sync.acquire(1); - } - - public void unlock () { - sync.release(1); - } -} -``` - -通过我们自己定义的 Lock 完成一定的同步功能。 - -```java -public class LeeMain { - - static int count = 0; - static LeeLock leeLock = new LeeLock(); - - public static void main (String[] args) throws InterruptedException { - - Runnable runnable = new Runnable() { - @Override - public void run () { - try { - leeLock.lock(); - for (int i = 0; i < 10000; i++) { - count++; - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - leeLock.unlock(); - } - - } - }; - Thread thread1 = new Thread(runnable); - Thread thread2 = new Thread(runnable); - thread1.start(); - thread2.start(); - thread1.join(); - thread2.join(); - System.out.println(count); - } -} -``` - -上述代码每次运行结果都会是 20000。通过简单的几行代码就能实现同步功能,这就是 AQS 的强大之处。 - -## 5 总结 - -我们日常开发中使用并发的场景太多,但是对并发内部的基本框架原理了解的人却不多。由于篇幅原因,本文仅介绍了可重入锁 ReentrantLock 的原理和 AQS 原理,希望能够成为大家了解 AQS 和 ReentrantLock 等同步器的“敲门砖”。 - -## 参考资料 - -- Lea D. The java. util. concurrent synchronizer framework\[J]. Science of Computer Programming, 2005, 58(3): 293-309. -- 《Java 并发编程实战》 -- [不可不说的 Java“锁”事](https://tech.meituan.com/2018/11/15/java-lock.html) - - +The first step is easy to understand, but what is the subsequent processing strategy after the lock acquisition fails? There may be the following diff --git a/docs/java/concurrent/threadlocal.md b/docs/java/concurrent/threadlocal.md index 0cdaf0adfd6..da9c6e032ac 100644 --- a/docs/java/concurrent/threadlocal.md +++ b/docs/java/concurrent/threadlocal.md @@ -1,40 +1,40 @@ --- -title: ThreadLocal 详解 +title: Detailed Explanation of ThreadLocal category: Java tag: - - Java并发 + - Java Concurrency --- -> 本文来自一枝花算不算浪漫投稿, 原文地址:[https://juejin.cn/post/6844904151567040519](https://juejin.cn/post/6844904151567040519)。 +> This article is a romantic submission from a branch of flowers. The original address: [https://juejin.cn/post/6844904151567040519](https://juejin.cn/post/6844904151567040519). -### 前言 +### Introduction ![](./images/thread-local/1.png) -**全文共 10000+字,31 张图,这篇文章同样耗费了不少的时间和精力才创作完成,原创不易,请大家点点关注+在看,感谢。** +**The full text contains over 10,000 words and 31 images. This article also took a considerable amount of time and effort to complete. Original works are not easy, so please show some support by liking and following. Thank you.** -对于`ThreadLocal`,大家的第一反应可能是很简单呀,线程的变量副本,每个线程隔离。那这里有几个问题大家可以思考一下: +When it comes to `ThreadLocal`, everyone's first reaction might be that it's a simple thread-local variable copy that isolates each thread. However, here are a few questions for you to ponder: -- `ThreadLocal`的 key 是**弱引用**,那么在 `ThreadLocal.get()`的时候,发生**GC**之后,key 是否为**null**? -- `ThreadLocal`中`ThreadLocalMap`的**数据结构**? -- `ThreadLocalMap`的**Hash 算法**? -- `ThreadLocalMap`中**Hash 冲突**如何解决? -- `ThreadLocalMap`的**扩容机制**? -- `ThreadLocalMap`中**过期 key 的清理机制**?**探测式清理**和**启发式清理**流程? -- `ThreadLocalMap.set()`方法实现原理? -- `ThreadLocalMap.get()`方法实现原理? -- 项目中`ThreadLocal`使用情况?遇到的坑? -- …… +- The `key` of `ThreadLocal` is a **weak reference**. When calling `ThreadLocal.get()`, does the key become **null** after a **GC**? +- What are the **data structures** in `ThreadLocal`'s `ThreadLocalMap`? +- What is the **hash algorithm** of `ThreadLocalMap`? +- How does `ThreadLocalMap` resolve **hash collisions**? +- What is the **expansion mechanism** of `ThreadLocalMap`? +- What is the **cleanup mechanism** for expired keys in `ThreadLocalMap`? What are the processes of **probe cleanup** and **heuristic cleanup**? +- What is the implementation principle of the `ThreadLocalMap.set()` method? +- What is the implementation principle of the `ThreadLocalMap.get()` method? +- How is `ThreadLocal` used in projects? What pitfalls have been encountered? +- ... -上述的一些问题你是否都已经掌握的很清楚了呢?本文将围绕这些问题使用图文方式来剖析`ThreadLocal`的**点点滴滴**。 +Have you mastered the above questions? This article will analyze every detail of `ThreadLocal` using images and text based on these questions. -### 目录 +### Table of Contents -**注明:** 本文源码基于`JDK 1.8` +**Note:** The source code in this article is based on `JDK 1.8`. -### `ThreadLocal`代码演示 +### `ThreadLocal` Code Demonstration -我们先看下`ThreadLocal`使用示例: +Let's first look at an example of using `ThreadLocal`: ```java public class ThreadLocalTest { @@ -55,63 +55,63 @@ public class ThreadLocalTest { } public static void main(String[] args) { - ThreadLocalTest.add("一枝花算不算浪漫"); + ThreadLocalTest.add("Does a branch of flowers count as romantic?"); System.out.println(holder.get().messages); ThreadLocalTest.clear(); } } ``` -打印结果: +Output: ```java -[一枝花算不算浪漫] +[Does a branch of flowers count as romantic?] size: 0 ``` -`ThreadLocal`对象可以提供线程局部变量,每个线程`Thread`拥有一份自己的**副本变量**,多个线程互不干扰。 +The `ThreadLocal` object provides thread-local variables, where each `Thread` has its own **copy variable**, and multiple threads do not interfere with each other. -### `ThreadLocal`的数据结构 +### Data Structure of `ThreadLocal` ![](./images/thread-local/2.png) -`Thread`类有一个类型为`ThreadLocal.ThreadLocalMap`的实例变量`threadLocals`,也就是说每个线程有一个自己的`ThreadLocalMap`。 +The `Thread` class has an instance variable `threadLocals` of type `ThreadLocal.ThreadLocalMap`, which means each thread has its own `ThreadLocalMap`. -`ThreadLocalMap`有自己的独立实现,可以简单地将它的`key`视作`ThreadLocal`,`value`为代码中放入的值(实际上`key`并不是`ThreadLocal`本身,而是它的一个**弱引用**)。 +`ThreadLocalMap` has its own independent implementation, where its `key` can simply be viewed as `ThreadLocal`, and `value` is the value placed in the code (in reality, the `key` is not the `ThreadLocal` itself, but a **weak reference** to it). -每个线程在往`ThreadLocal`里放值的时候,都会往自己的`ThreadLocalMap`里存,读也是以`ThreadLocal`作为引用,在自己的`map`里找对应的`key`,从而实现了**线程隔离**。 +Whenever a thread places a value into `ThreadLocal`, it stores it in its own `ThreadLocalMap`, and reading is also done by using `ThreadLocal` as a reference to find the corresponding `key` in its own `map`, thereby achieving **thread isolation**. -`ThreadLocalMap`有点类似`HashMap`的结构,只是`HashMap`是由**数组+链表**实现的,而`ThreadLocalMap`中并没有**链表**结构。 +`ThreadLocalMap` is somewhat similar to the structure of `HashMap`, except that `HashMap` is implemented using **arrays + linked lists**, while `ThreadLocalMap` does not have a **linked list** structure. -我们还要注意`Entry`, 它的`key`是`ThreadLocal k` ,继承自`WeakReference`, 也就是我们常说的弱引用类型。 +We also need to pay attention to `Entry`, where its `key` is `ThreadLocal k`, inheriting from `WeakReference`, which is what we commonly refer to as weak reference types. -### GC 之后 key 是否为 null? +### Is the key null after GC? -回应开头的那个问题, `ThreadLocal` 的`key`是弱引用,那么在`ThreadLocal.get()`的时候,发生`GC`之后,`key`是否是`null`? +To address the question at the beginning, the `key` of `ThreadLocal` is a weak reference. So, when calling `ThreadLocal.get()`, is the `key` **null** after GC? -为了搞清楚这个问题,我们需要搞清楚`Java`的**四种引用类型**: +To clarify this, we need to understand Java's **four types of reference**: -- **强引用**:我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候 -- **软引用**:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收 -- **弱引用**:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收 -- **虚引用**:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知 +- **Strong Reference**: The objects we usually create with `new` are strong reference types. As long as the strong reference exists, the garbage collector will never reclaim the referenced object, even during memory shortage. +- **Soft Reference**: Objects referenced by `SoftReference` are considered soft references. These objects are collected when there is a risk of memory overflow. +- **Weak Reference**: Objects referenced by `WeakReference` are weak references. As soon as garbage collection occurs, if this object is only pointed to by a weak reference, it will be reclaimed. +- **Phantom Reference**: Phantom references are the weakest references, defined in Java using `PhantomReference`. The only function of a phantom reference is to receive notifications that the object is about to be reclaimed. -接着再来看下代码,我们使用反射的方式来看看`GC`后`ThreadLocal`中的数据情况:(下面代码来源自: 本地运行演示 GC 回收场景) +Next, let's look at the code. We will use reflection to check the data state in `ThreadLocal` after GC (the following code comes from: local running demonstration of GC scenarios): ```java public class ThreadLocalDemo { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException { - Thread t = new Thread(()->test("abc",false)); + Thread t = new Thread(() -> test("abc", false)); t.start(); t.join(); - System.out.println("--gc后--"); + System.out.println("--after gc--"); Thread t2 = new Thread(() -> test("def", true)); t2.start(); t2.join(); } - private static void test(String s,boolean isGC) { + private static void test(String s, boolean isGC) { try { new ThreadLocal<>().set(s); if (isGC) { @@ -133,7 +133,7 @@ public class ThreadLocalDemo { Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent"); valueField.setAccessible(true); referenceField.setAccessible(true); - System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o))); + System.out.println(String.format("Weak reference key: %s, value: %s", referenceField.get(o), valueField.get(o))); } } } catch (Exception e) { @@ -143,42 +143,42 @@ public class ThreadLocalDemo { } ``` -结果如下: +The result is as follows: ```java -弱引用key:java.lang.ThreadLocal@433619b6,值:abc -弱引用key:java.lang.ThreadLocal@418a15e3,值:java.lang.ref.SoftReference@bf97a12 ---gc后-- -弱引用key:null,值:def +Weak reference key: java.lang.ThreadLocal@433619b6, value: abc +Weak reference key: java.lang.ThreadLocal@418a15e3, value: java.lang.ref.SoftReference@bf97a12 +--after gc-- +Weak reference key: null, value: def ``` ![](./images/thread-local/3.png) -如图所示,因为这里创建的`ThreadLocal`并没有指向任何值,也就是没有任何引用: +As shown in the figure, because the `ThreadLocal` created here does not point to any value, i.e., there are no references: ```java new ThreadLocal<>().set(s); ``` -所以这里在`GC`之后,`key`就会被回收,我们看到上面`debug`中的`referent=null`, 如果**改动一下代码:** +So, after GC, the `key` will be reclaimed. We see from the above `debug` that `referent=null`. If we **modify the code** slightly: ![](./images/thread-local/4.png) -这个问题刚开始看,如果没有过多思考,**弱引用**,还有**垃圾回收**,那么肯定会觉得是`null`。 +Initially looking at this problem, if you don’t think too much about it, with **weak reference** and **garbage collection**, you may definitely feel it should be `null`. -其实是不对的,因为题目说的是在做 `ThreadLocal.get()` 操作,证明其实还是有**强引用**存在的,所以 `key` 并不为 `null`,如下图所示,`ThreadLocal`的**强引用**仍然是存在的。 +However, this is incorrect, because the scenario describes that the `ThreadLocal.get()` operation is being performed, which indicates that a **strong reference** is still present, hence the `key` is not `null`, as shown in the following figure, the **strong reference** of `ThreadLocal` is still present. ![](./images/thread-local/5.png) -如果我们的**强引用**不存在的话,那么 `key` 就会被回收,也就是会出现我们 `value` 没被回收,`key` 被回收,导致 `value` 永远存在,出现内存泄漏。 +If our **strong reference** does not exist, then the `key` will be reclaimed, which leads to `value` never being reclaimed, and the `key` being reclaimed results in memory leakage. -### `ThreadLocal.set()`方法源码详解 +### Detailed Explanation of the `ThreadLocal.set()` Method Source Code ![](./images/thread-local/6.png) -`ThreadLocal`中的`set`方法原理如上图所示,很简单,主要是判断`ThreadLocalMap`是否存在,然后使用`ThreadLocal`中的`set`方法进行数据处理。 +The principle of the `set` method in `ThreadLocal` is depicted in the diagram above. It’s quite simple: it mainly checks if `ThreadLocalMap` exists and then uses the `set` method in `ThreadLocal` to process the data. -代码如下: +The code is as follows: ```java public void set(T value) { @@ -195,19 +195,19 @@ void createMap(Thread t, T firstValue) { } ``` -主要的核心逻辑还是在`ThreadLocalMap`中的,一步步往下看,后面还有更详细的剖析。 +The key logic is still in `ThreadLocalMap`, which we will analyze in detail later. -### `ThreadLocalMap` Hash 算法 +### Hash Algorithm of `ThreadLocalMap` -既然是`Map`结构,那么`ThreadLocalMap`当然也要实现自己的`hash`算法来解决散列表数组冲突问题。 +Since it's a `Map` structure, `ThreadLocalMap` certainly needs to implement its own `hash` algorithm to address the issue of collision in the hash table array. ```java -int i = key.threadLocalHashCode & (len-1); +int i = key.threadLocalHashCode & (len - 1); ``` -`ThreadLocalMap`中`hash`算法很简单,这里`i`就是当前 key 在散列表中对应的数组下标位置。 +The `hash` algorithm in `ThreadLocalMap` is quite simple; here `i` is the index position of the current key in the hash table. -这里最关键的就是`threadLocalHashCode`值的计算,`ThreadLocal`中有一个属性为`HASH_INCREMENT = 0x61c88647` +The most crucial aspect is the calculation of the `threadLocalHashCode` value. The `ThreadLocal` contains a property `HASH_INCREMENT = 0x61c88647`. ```java public class ThreadLocal { @@ -234,107 +234,107 @@ public class ThreadLocal { } ``` -每当创建一个`ThreadLocal`对象,这个`ThreadLocal.nextHashCode` 这个值就会增长 `0x61c88647` 。 +Every time a `ThreadLocal` object is created, the value of `ThreadLocal.nextHashCode` increases by `0x61c88647`. -这个值很特殊,它是**斐波那契数** 也叫 **黄金分割数**。`hash`增量为 这个数字,带来的好处就是 `hash` **分布非常均匀**。 +This value is quite special; it is a **Fibonacci number**, also known as the **golden ratio**. The `hash` increment of this number ensures that the `hash` is **evenly distributed**. -我们自己可以尝试下: +We can try it out: ![](./images/thread-local/8.png) -可以看到产生的哈希码分布很均匀,这里不去细纠**斐波那契**具体算法,感兴趣的可以自行查阅相关资料。 +The generated hash codes are distributed very evenly. We won't go into the specifics of the **Fibonacci** algorithm here; interested readers can look up related materials themselves. -### `ThreadLocalMap` Hash 冲突 +### Hash Collisions in `ThreadLocalMap` -> **注明:** 下面所有示例图中,**绿色块**`Entry`代表**正常数据**,**灰色块**代表`Entry`的`key`值为`null`,**已被垃圾回收**。**白色块**表示`Entry`为`null`。 +> **Note:** In all example images below, **green blocks** represent **normal data**, **gray blocks** represent `Entry` with `key` value of `null`, having **been garbage collected**. **White blocks** indicate `Entry` is `null`. -虽然`ThreadLocalMap`中使用了**黄金分割数**来作为`hash`计算因子,大大减少了`Hash`冲突的概率,但是仍然会存在冲突。 +Although `ThreadLocalMap` uses the **golden ratio** as a factor for hash computation, greatly reducing the probability of hash collisions, conflicts may still occur. -`HashMap`中解决冲突的方法是在数组上构造一个**链表**结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成**红黑树**。 +`HashMap` resolves conflicts by constructing a **linked list** structure in the array. Conflicting data is appended to the list, and if the list length exceeds a certain number, it is transformed into a **red-black tree**. -而 `ThreadLocalMap` 中并没有链表结构,所以这里不能使用 `HashMap` 解决冲突的方式了。 +However, `ThreadLocalMap` does not have a linked list structure, so we cannot use the HashMap method for conflict resolution. ![](./images/thread-local/7.png) -如上图所示,如果我们插入一个`value=27`的数据,通过 `hash` 计算后应该落入槽位 4 中,而槽位 4 已经有了 `Entry` 数据。 +As shown in the diagram above, when we insert data with `value=27`, it is calculated to fall into slot 4, but slot 4 already has `Entry` data. -此时就会线性向后查找,一直找到 `Entry` 为 `null` 的槽位才会停止查找,将当前元素放入此槽位中。当然迭代过程中还有其他的情况,比如遇到了 `Entry` 不为 `null` 且 `key` 值相等的情况,还有 `Entry` 中的 `key` 值为 `null` 的情况等等都会有不同的处理,后面会一一详细讲解。 +At this point, a linear search will occur, continuing until an `Entry` with a value of `null` is found to stop searching, placing the current element in that slot. Of course, there are other scenarios during the iteration, such as encountering an entry where `key` is equal or an `Entry` where `key` is null, etc., all of which will have different treatments that will be explained later. -这里还画了一个`Entry`中的`key`为`null`的数据(**Entry=2 的灰色块数据**),因为`key`值是**弱引用**类型,所以会有这种数据存在。在`set`过程中,如果遇到了`key`过期的`Entry`数据,实际上是会进行一轮**探测式清理**操作的,具体操作方式后面会讲到。 +There is also data in `Entry` whose `key` is `null` (**the gray block data of Entry=2**). Since the `key` is a **weak reference** type, such data may exist. During the `set` process, if an expired `Entry` is encountered, a round of **probe cleanup** will actually occur, and the specific operation will be discussed later. -### `ThreadLocalMap.set()`详解 +### `ThreadLocalMap.set()` Detailed Explanation -#### `ThreadLocalMap.set()`原理图解 +#### `ThreadLocalMap.set()` Principle Diagram -看完了`ThreadLocal` **hash 算法**后,我们再来看`set`是如何实现的。 +After understanding the `hash algorithm` of `ThreadLocal`, let's look at how `set` is implemented. -往`ThreadLocalMap`中`set`数据(**新增**或者**更新**数据)分为好几种情况,针对不同的情况我们画图来说明。 +Setting data in `ThreadLocalMap` (either **adding** or **updating** data) can be divided into several scenarios, which we will explain with illustrations. -**第一种情况:** 通过`hash`计算后的槽位对应的`Entry`数据为空: +**Scenario One:** The slot corresponding to the key calculated by hash is empty: ![](./images/thread-local/9.png) -这里直接将数据放到该槽位即可。 +Here, the data can be placed directly into that slot. -**第二种情况:** 槽位数据不为空,`key`值与当前`ThreadLocal`通过`hash`计算获取的`key`值一致: +**Scenario Two:** The slot data is not empty, and the key value matches the current `ThreadLocal` obtained through `hash` calculation: ![](./images/thread-local/10.png) -这里直接更新该槽位的数据。 +Here, the data in the slot is updated directly. -**第三种情况:** 槽位数据不为空,往后遍历过程中,在找到`Entry`为`null`的槽位之前,没有遇到`key`过期的`Entry`: +**Scenario Three:** The slot data is not empty, and during the subsequent traversal, before finding an `Entry` with a value of `null`, an expired `Entry` is not encountered: ![](./images/thread-local/11.png) -遍历散列数组,线性往后查找,如果找到`Entry`为`null`的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了**key 值相等**的数据,直接更新即可。 +In this case, iterate through the hash array and perform a linear search. If an `Entry` with a value of `null` is found in the slot, place the data there or update directly upon encountering an **equal key** during the search. -**第四种情况:** 槽位数据不为空,往后遍历过程中,在找到`Entry`为`null`的槽位之前,遇到`key`过期的`Entry`,如下图,往后遍历过程中,遇到了`index=7`的槽位数据`Entry`的`key=null`: +**Scenario Four:** The slot data is not empty, and during the subsequent traversal, an expired `Entry` is encountered before finding an `Entry` with a value of `null`. For example, if an expired data `Entry` is at `index=7`: ![](./images/thread-local/12.png) -散列数组下标为 7 位置对应的`Entry`数据`key`为`null`,表明此数据`key`值已经被垃圾回收掉了,此时就会执行`replaceStaleEntry()`方法,该方法含义是**替换过期数据的逻辑**,以**index=7**位起点开始遍历,进行探测式数据清理工作。 +The slot at array index 7 has `Entry` data with a `key` of `null`, indicating that the key has already been garbage collected. At this point, the `replaceStaleEntry()` method will be executed, which is the logic for **replacing expired data**, starting the search at **index=7** for probing cleanup. -初始化探测式清理过期数据扫描的开始位置:`slotToExpunge = staleSlot = 7` +To initialize the scanning start position for probing cleanup for expired data: `slotToExpunge = staleSlot = 7` -以当前`staleSlot`开始 向前迭代查找,找其他过期的数据,然后更新过期数据起始扫描下标`slotToExpunge`。`for`循环迭代,直到碰到`Entry`为`null`结束。 +Starting from the current `staleSlot`, iterate forward to find other expired data, and update the starting scan index `slotToExpunge`. The `for` loop will iterate until it encounters an `Entry` with a value of `null`. -如果找到了过期的数据,继续向前迭代,直到遇到`Entry=null`的槽位才停止迭代,如下图所示,**slotToExpunge 被更新为 0**: +If expired data is found, continue iterating until discovering an `Entry` with a value of `null` to stop the iteration, as shown in the diagram below; **slotToExpunge gets updated to 0**: ![](./images/thread-local/13.png) -以当前节点(`index=7`)向前迭代,检测是否有过期的`Entry`数据,如果有则更新`slotToExpunge`值。碰到`null`则结束探测。以上图为例`slotToExpunge`被更新为 0。 +Iterating forward from the current node (`index=7`), check for expired `Entry` data. If it is found, update the value of `slotToExpunge`. Stop probing when encountering `null`. In this example, `slotToExpunge` was updated to 0. -上面向前迭代的操作是为了更新探测清理过期数据的起始下标`slotToExpunge`的值,这个值在后面会讲解,它是用来判断当前过期槽位`staleSlot`之前是否还有过期元素。 +The mentioned forward iteration operation's purpose is to update the value of `slotToExpunge`, which will serve later to determine whether there are any expired elements before the current stale slot. -接着开始以`staleSlot`位置(`index=7`)向后迭代,**如果找到了相同 key 值的 Entry 数据:** +Next, begin iterating backward from the `staleSlot` position (`index=7`); **if an `Entry` with the same key value is found:** ![](./images/thread-local/14.png) -从当前节点`staleSlot`向后查找`key`值相等的`Entry`元素,找到后更新`Entry`的值并交换`staleSlot`元素的位置(`staleSlot`位置为过期元素),更新`Entry`数据,然后开始进行过期`Entry`的清理工作,如下图所示: +Search for `Entry` elements with an equal key value starting from the current node `staleSlot`. If found, update the value of `Entry` and swap with the `staleSlot` element (the `staleSlot` location is the expired element); update the `Entry` data and initiate cleanup for expired `Entry`, as shown in the diagram: -![](https://oss.javaguide.cn/java-guide-blog/view.png)向后遍历过程中,如果没有找到相同 key 值的 Entry 数据: +![](https://oss.javaguide.cn/java-guide-blog/view.png) During traversal, if no `Entry` with the same key value is found: ![](./images/thread-local/15.png) -从当前节点`staleSlot`向后查找`key`值相等的`Entry`元素,直到`Entry`为`null`则停止寻找。通过上图可知,此时`table`中没有`key`值相同的`Entry`。 +Continue searching for an `Entry` with the same key value from the current `staleSlot` until reaching a slot with `Entry` set as `null` to stop searching. As seen in the above diagram, there are no `Entry` values with the same key. -创建新的`Entry`,替换`table[stableSlot]`位置: +Create a new `Entry` to replace the position of `table[stableSlot]`: ![](./images/thread-local/16.png) -替换完成后也是进行过期元素清理工作,清理工作主要是有两个方法:`expungeStaleEntry()`和`cleanSomeSlots()`,具体细节后面会讲到,请继续往后看。 +Once the replacement is complete, also run the expired elements cleanup operation, primarily two methods: `expungeStaleEntry()` and `cleanSomeSlots()`. We will cover the specifics later, so please keep reading. -#### `ThreadLocalMap.set()`源码详解 +#### Source Code for `ThreadLocalMap.set()` -上面已经用图的方式解析了`set()`实现的原理,其实已经很清晰了,我们接着再看下源码: +Having illustrated the principles of `set()` with diagrams, it's already quite clear. Let’s delve into the source code: -`java.lang.ThreadLocal`.`ThreadLocalMap.set()`: +`java.lang.ThreadLocal.ThreadLocalMap.set()`: ```java private void set(ThreadLocal key, Object value) { Entry[] tab = table; int len = tab.length; - int i = key.threadLocalHashCode & (len-1); + int i = key.threadLocalHashCode & (len - 1); for (Entry e = tab[i]; e != null; @@ -359,48 +359,30 @@ private void set(ThreadLocal key, Object value) { } ``` -这里会通过`key`来计算在散列表中的对应位置,然后以当前`key`对应的桶的位置向后查找,找到可以使用的桶。 +This method calculates the corresponding position in the hash table based on the `key`, then iteratively looks for an available bucket starting from the current key's position. ```java Entry[] tab = table; int len = tab.length; -int i = key.threadLocalHashCode & (len-1); +int i = key.threadLocalHashCode & (len - 1); ``` -什么情况下桶才是可以使用的呢? +When is a bucket usable? -1. `k = key` 说明是替换操作,可以使用 -2. 碰到一个过期的桶,执行替换逻辑,占用过期桶 -3. 查找过程中,碰到桶中`Entry=null`的情况,直接使用 +1. If `k = key`, it indicates this is a replacement operation. +1. If a bucket containing an expired `Entry` is encountered, replace it with new logic. +1. When encountering a bucket whose `Entry` is `null`. -接着就是执行`for`循环遍历,向后查找,我们先看下`nextIndex()`、`prevIndex()`方法实现: +Next in the loop: -![](./images/thread-local/17.png) +1. If the `Entry` data at the current bucket corresponding to `key` is `null`, this indicates no data conflict. Break out of the loop and set data directly into this bucket. +1. If the `Entry` data at the current bucket corresponding to `key` is not empty:\ + 2.1 If `k = key`, it signifies the current `set` operation is for replacement. Execute the replacement logic and return.\ + 2.2 If `key = null`, signifying that the current bucket’s `Entry` contains expired data, invoke `replaceStaleEntry()` (the core method) before returning. +1. If the loop concludes without finding the `key`, create a `new Entry` object in the bucket with `Entry` set to `null`. +1. After that, invoke `cleanSomeSlots()` to carry out heuristic cleanup of expired keys in the hash table. If no data was cleaned, and size exceeds the threshold (two-thirds of array length), proceed to `rehash()`. -```java -private static int nextIndex(int i, int len) { - return ((i + 1 < len) ? i + 1 : 0); -} - -private static int prevIndex(int i, int len) { - return ((i - 1 >= 0) ? i - 1 : len - 1); -} -``` - -接着看剩下`for`循环中的逻辑: - -1. 遍历当前`key`值对应的桶中`Entry`数据为空,这说明散列数组这里没有数据冲突,跳出`for`循环,直接`set`数据到对应的桶中 -2. 如果`key`值对应的桶中`Entry`数据不为空 - 2.1 如果`k = key`,说明当前`set`操作是一个替换操作,做替换逻辑,直接返回 - 2.2 如果`key = null`,说明当前桶位置的`Entry`是过期数据,执行`replaceStaleEntry()`方法(核心方法),然后返回 -3. `for`循环执行完毕,继续往下执行说明向后迭代的过程中遇到了`entry`为`null`的情况 - 3.1 在`Entry`为`null`的桶中创建一个新的`Entry`对象 - 3.2 执行`++size`操作 -4. 调用`cleanSomeSlots()`做一次启发式清理工作,清理散列数组中`Entry`的`key`过期的数据 - 4.1 如果清理工作完成后,未清理到任何数据,且`size`超过了阈值(数组长度的 2/3),进行`rehash()`操作 - 4.2 `rehash()`中会先进行一轮探测式清理,清理过期`key`,清理完成后如果**size >= threshold - threshold / 4**,就会执行真正的扩容逻辑(扩容逻辑往后看) - -接着重点看下`replaceStaleEntry()`方法,`replaceStaleEntry()`方法提供替换过期数据的功能,我们可以对应上面**第四种情况**的原理图来再回顾下,具体代码如下: +Next, closely review the `replaceStaleEntry()` method, which provides the functionality to replace expired data. You can relate it to the principle diagram in **Scenario Four** provided earlier: `java.lang.ThreadLocal.ThreadLocalMap.replaceStaleEntry()`: @@ -449,7 +431,7 @@ private void replaceStaleEntry(ThreadLocal key, Object value, } ``` -`slotToExpunge`表示开始探测式清理过期数据的开始下标,默认从当前的`staleSlot`开始。以当前的`staleSlot`开始,向前迭代查找,找到没有过期的数据,`for`循环一直碰到`Entry`为`null`才会结束。如果向前找到了过期数据,更新探测清理过期数据的开始下标为 i,即`slotToExpunge=i` +The `slotToExpunge` indicates the start index for probing cleanup of expired data, initially starting at the `staleSlot`. The search continues from `staleSlot`, iterating forward to find non-expired data to potentially update `slotToExpunge`. The `for` loop iterates until encountering an `Entry` that is `null`. ```java for (int i = prevIndex(staleSlot, len); @@ -462,13 +444,11 @@ for (int i = prevIndex(staleSlot, len); } ``` -接着开始从`staleSlot`向后查找,也是碰到`Entry`为`null`的桶结束。 -如果迭代过程中,**碰到 k == key**,这说明这里是替换逻辑,替换新数据并且交换当前`staleSlot`位置。如果`slotToExpunge == staleSlot`,这说明`replaceStaleEntry()`一开始向前查找过期数据时并未找到过期的`Entry`数据,接着向后查找过程中也未发现过期数据,修改开始探测式清理过期数据的下标为当前循环的 index,即`slotToExpunge = i`。最后调用`cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);`进行启发式过期数据清理。 +Next, the search from `staleSlot` indices forward occurs, and when encountering a key equal to the passed `key`, the element is replaced and swap positions. ```java if (k == key) { e.value = value; - tab[i] = tab[staleSlot]; tab[staleSlot] = e; @@ -480,66 +460,62 @@ if (k == key) { } ``` -`cleanSomeSlots()`和`expungeStaleEntry()`方法后面都会细讲,这两个是和清理相关的方法,一个是过期`key`相关`Entry`的启发式清理(`Heuristically scan`),另一个是过期`key`相关`Entry`的探测式清理。 - -**如果 k != key**则会接着往下走,`k == null`说明当前遍历的`Entry`是一个过期数据,`slotToExpunge == staleSlot`说明,一开始的向前查找数据并未找到过期的`Entry`。如果条件成立,则更新`slotToExpunge` 为当前位置,这个前提是前驱节点扫描时未发现过期数据。 +If `k != key`, continue searching forward in the probing process. If `k == null`, then the currently checked `Entry` is expired, indicating a potential update for `slotToExpunge`. ```java if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; ``` -往后迭代的过程中如果没有找到`k == key`的数据,且碰到`Entry`为`null`的数据,则结束当前的迭代操作。此时说明这里是一个添加的逻辑,将新的数据添加到`table[staleSlot]` 对应的`slot`中。 +When no matching `k == key` is found, but an `Entry` becomes `null`, the addition operation occurs where a new `Entry` is created for the `table[staleSlot]`. ```java tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); ``` -最后判断除了`staleSlot`以外,还发现了其他过期的`slot`数据,就要开启清理数据的逻辑: +Finally, if `slotToExpunge` differs from `staleSlot`, cleanup operations begin anew. ```java if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); ``` -### `ThreadLocalMap`过期 key 的探测式清理流程 +### Probing Cleanup Process for Expired Keys in `ThreadLocalMap` -上面我们有提及`ThreadLocalMap`的两种过期`key`数据清理方式:**探测式清理**和**启发式清理**。 +Above, we mentioned the two cleanup methods for expired keys in `ThreadLocalMap`: **probing cleanup** and **heuristic cleanup**. -我们先讲下探测式清理,也就是`expungeStaleEntry`方法,遍历散列数组,从开始位置向后探测清理过期数据,将过期数据的`Entry`设置为`null`,沿途中碰到未过期的数据则将此数据`rehash`后重新在`table`数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的`Entry=null`的桶中,使`rehash`后的`Entry`数据距离正确的桶的位置更近一些。操作逻辑如下: +We first discuss probing cleanup, specifically in the `expungeStaleEntry` method, which iterates through the hash array, probing and cleaning expired data, setting the value of the `Entry` to null. If a non-expired bucket is encountered, the values will be relocated. ![](./images/thread-local/18.png) -如上图,`set(27)` 经过 hash 计算后应该落到`index=4`的桶中,由于`index=4`桶已经有了数据,所以往后迭代最终数据放入到`index=7`的桶中,放入后一段时间后`index=5`中的`Entry`数据`key`变为了`null` +In the diagram, `set(27)` is calculated to be in `index=4`; since it already has data, it will continue iterating forward, leading to data being moved to `index=7`. After a while, the `Entry` data at `index=5` key becomes `null`. ![](./images/thread-local/19.png) -如果再有其他数据`set`到`map`中,就会触发**探测式清理**操作。 - -如上图,执行**探测式清理**后,`index=5`的数据被清理掉,继续往后迭代,到`index=7`的元素时,经过`rehash`后发现该元素正确的`index=4`,而此位置已经有了数据,往后查找离`index=4`最近的`Entry=null`的节点(刚被探测式清理掉的数据:`index=5`),找到后移动`index= 7`的数据到`index=5`中,此时桶的位置离正确的位置`index=4`更近了。 +When additional `set` operations to map occur, this will trigger **probing cleanup**. -经过一轮探测式清理后,`key`过期的数据会被清理掉,没过期的数据经过`rehash`重定位后所处的桶位置理论上更接近`i= key.hashCode & (tab.len - 1)`的位置。这种优化会提高整个散列表查询性能。 +After this operation, expired data will be cleaned up, and non-expired data will be recalled after capturing the appropriate hash key slot position more properly. -接着看下`expungeStaleEntry()`具体流程,我们还是以先原理图后源码讲解的方式来一步步梳理: +Next, let’s see the specific flow of `expungeStaleEntry()`. We adopt the principle-then-source-code approach to explain step by step. ![](./images/thread-local/20.png) -我们假设`expungeStaleEntry(3)` 来调用此方法,如上图所示,我们可以看到`ThreadLocalMap`中`table`的数据情况,接着执行清理操作: +Assume calling `expungeStaleEntry(3)` leads to the following observations, showcasing the data situation in `ThreadLocalMap`: ![](./images/thread-local/21.png) -第一步是清空当前`staleSlot`位置的数据,`index=3`位置的`Entry`变成了`null`。然后接着往后探测: +The first step clears the current `staleSlot`, changing the `index=3` data to `null`. The probing continues forward: ![](./images/thread-local/22.png) -执行完第二步后,index=4 的元素挪到 index=3 的槽位中。 +After completing Step 2, the `index=4` data is moved到 the `index=3` slot. -继续往后迭代检查,碰到正常数据,计算该数据位置是否偏移,如果被偏移,则重新计算`slot`位置,目的是让正常数据尽可能存放在正确位置或离正确位置更近的位置 +As the iteration proceeds to verify whether normal data encounters an offset at an appropriate position. ![](./images/thread-local/23.png) -在往后迭代的过程中碰到空的槽位,终止探测,这样一轮探测式清理工作就完成了,接着我们继续看看具体**实现源代码**: +The traversal continues; upon encountering an empty slot, it terminates the probing cleanup while confirming that this round of cleaning is complete. This completes a round of probing cleanup work, and now we can view the implementation in detail: ```java private int expungeStaleEntry(int staleSlot) { @@ -575,8 +551,9 @@ private int expungeStaleEntry(int staleSlot) { } ``` -这里我们还是以`staleSlot=3` 来做示例说明,首先是将`tab[staleSlot]`槽位的数据清空,然后设置`size--` -接着以`staleSlot`位置往后迭代,如果遇到`k==null`的过期数据,也是清空该槽位数据,然后`size--` +Using `staleSlot=3` as an example, the process starts by clearing the data at `tab[staleSlot]`, and then the value of `size` decreases. + +Next to confirm if the value has expired, indicating a mark for cleaning. ```java ThreadLocal k = e.get(); @@ -588,32 +565,27 @@ if (k == null) { } ``` -如果`key`没有过期,重新计算当前`key`的下标位置是不是当前槽位下标位置,如果不是,那么说明产生了`hash`冲突,此时以新计算出来正确的槽位位置往后迭代,找到最近一个可以存放`entry`的位置。 +If the key is still valid, its corresponding (`Entry`s)\` index will be calculated allowing the relevant data to be relocated in the table. ```java int h = k.threadLocalHashCode & (len - 1); if (h != i) { - tab[i] = null; - - while (tab[h] != null) - h = nextIndex(h, len); - - tab[h] = e; + ... } ``` -这里是处理正常的产生`Hash`冲突的数据,经过迭代后,有过`Hash`冲突数据的`Entry`位置会更靠近正确位置,这样的话,查询的时候 效率才会更高。 +The only way for correct data to avoid `hash` collisions will be to ensure it has been relocated at the most nearby possible point. -### `ThreadLocalMap`扩容机制 +### Expansion Mechanism of `ThreadLocalMap` -在`ThreadLocalMap.set()`方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中`Entry`的数量已经达到了列表的扩容阈值`(len*2/3)`,就开始执行`rehash()`逻辑: +At the conclusion of `ThreadLocalMap.set()`, if after running the heuristic cleanup, no data has been cleaned and the current number of `Entry`s exceeds a threshold of `(len*2/3)`, the method begins the `rehash()` logic: ```java if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); ``` -接着看下`rehash()`具体实现: +To comprehend, let's investigate how `rehash()` is specifically implemented: ```java private void rehash() { @@ -634,17 +606,17 @@ private void expungeStaleEntries() { } ``` -这里首先是会进行探测式清理工作,从`table`的起始位置往后清理,上面有分析清理的详细流程。清理完成之后,`table`中可能有一些`key`为`null`的`Entry`数据被清理掉,所以此时通过判断`size >= threshold - threshold / 4` 也就是`size >= threshold * 3/4` 来决定是否扩容。 +The first step entails performing a probing cleanup for stale entries, iterating through `table` forwards. Following the cleanup, if `size >= threshold - threshold / 4` (which is equivalent to `size >= threshold * 3/4`), this indicates a need to expand. -我们还记得上面进行`rehash()`的阈值是`size >= threshold`,所以当面试官套路我们`ThreadLocalMap`扩容机制的时候 我们一定要说清楚这两个步骤: +Be sure to make the distinction when discussing `ThreadLocalMap`'s expansion mechanism; note both steps. ![](./images/thread-local/24.png) -接着看看具体的`resize()`方法,为了方便演示,我们以`oldTab.len=8`来举例: +Next, looking at the specific implementation of `resize()` for clarity, we'll use an example with `oldTab.len=8`: ![](./images/thread-local/25.png) -扩容后的`tab`的大小为`oldLen * 2`,然后遍历老的散列表,重新计算`hash`位置,然后放到新的`tab`数组中,如果出现`hash`冲突则往后寻找最近的`entry`为`null`的槽位,遍历完成之后,`oldTab`中所有的`entry`数据都已经放入到新的`tab`中了。重新计算`tab`下次扩容的**阈值**,具体代码如下: +With the new `tab` size being `oldLen * 2`, an iteration over the former has the hash positioning recalculated, effectively moving all existing data into the newly generated `tab`. The subsequent threshold for re-expansion will be recalculated accordingly. ```java private void resize() { @@ -676,27 +648,27 @@ private void resize() { } ``` -### `ThreadLocalMap.get()`详解 +### Detailed Explanation of `ThreadLocalMap.get()` -上面已经看完了`set()`方法的源码,其中包括`set`数据、清理数据、优化数据桶的位置等操作,接着看看`get()`操作的原理。 +Now that we’ve completed examining the `set()` method source code, which consists of setting and cleaning data operations as well, the next step is to look into the principles behind the `get()` operation. -#### `ThreadLocalMap.get()`图解 +#### Diagram for `ThreadLocalMap.get()` -**第一种情况:** 通过查找`key`值计算出散列表中`slot`位置,然后该`slot`位置中的`Entry.key`和查找的`key`一致,则直接返回: +**Scenario One:** When the key value is lookup based on hash in the slot position, if the `Entry.key` matches the key, return immediately: ![](./images/thread-local/26.png) -**第二种情况:** `slot`位置中的`Entry.key`和要查找的`key`不一致: +**Scenario Two:** If the `Entry.key` differs from the intended key value: ![](./images/thread-local/27.png) -我们以`get(ThreadLocal1)`为例,通过`hash`计算后,正确的`slot`位置应该是 4,而`index=4`的槽位已经有了数据,且`key`值不等于`ThreadLocal1`,所以需要继续往后迭代查找。 +Assuming we use `get(ThreadLocal1)`, if calculation via hash indicates the correct slot is index 4 but that slot contains data where the key property doesn’t match `ThreadLocal1`, we will need to continue searching. -迭代到`index=5`的数据时,此时`Entry.key=null`,触发一次探测式数据回收操作,执行`expungeStaleEntry()`方法,执行完后,`index 5,8`的数据都会被回收,而`index 6,7`的数据都会前移。`index 6,7`前移之后,继续从 `index=5` 往后迭代,于是就在 `index=6` 找到了`key`值相等的`Entry`数据,如下图所示: +Upon reaching data in `index=5`, if `Entry.key=null`, this activates a probe cleanup, invoking `expungeStaleEntry()` method while iterating stops at encountering any remaining `Entry` as `null`. -![](./images/thread-local/28.png) +After cleaning, we can continue considering the next slot; if we maintain searching through for matching key values, we will eventually arrive back to entry with a matching `key`. -#### `ThreadLocalMap.get()`源码详解 +#### Source Code for `ThreadLocalMap.get()` `java.lang.ThreadLocal.ThreadLocalMap.getEntry()`: @@ -728,17 +700,17 @@ private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { } ``` -### `ThreadLocalMap`过期 key 的启发式清理流程 +### Heuristic Cleanup Process for Expired Keys in `ThreadLocalMap` -上面多次提及到`ThreadLocalMap`过期 key 的两种清理方式:**探测式清理(expungeStaleEntry())**、**启发式清理(cleanSomeSlots())** +We’ve previously mentioned two cleanup methods for expired keys in `ThreadLocalMap`: **probe cleanup (expungeStaleEntry())** and **heuristic cleanup (cleanSomeSlots())**. -探测式清理是以当前`Entry` 往后清理,遇到值为`null`则结束清理,属于**线性探测清理**。 +Probationary cleanup is specific about the current `Entry` being cleaned in succession until the `null` values finally stop. -而启发式清理被作者定义为:**Heuristically scan some cells looking for stale entries**. +Heuristic cleanup, however, has been defined as: **Heuristically scan some cells looking for stale entries**. ![](./images/thread-local/29.png) -具体代码如下: +The specific code is as follows: ```java private boolean cleanSomeSlots(int i, int n) { @@ -753,44 +725,44 @@ private boolean cleanSomeSlots(int i, int n) { removed = true; i = expungeStaleEntry(i); } - } while ( (n >>>= 1) != 0); + } while ((n >>>= 1) != 0); return removed; } ``` ### `InheritableThreadLocal` -我们使用`ThreadLocal`的时候,在异步场景下是无法给子线程共享父线程中创建的线程副本数据的。 +When we use `ThreadLocal`, in asynchronous scenarios, child threads cannot share the thread-local copies of data created in parent threads. -为了解决这个问题,JDK 中还有一个`InheritableThreadLocal`类,我们来看一个例子: +To address this problem, JDK provides the `InheritableThreadLocal` class. Let’s see an example: ```java public class InheritableThreadLocalDemo { public static void main(String[] args) { - ThreadLocal ThreadLocal = new ThreadLocal<>(); - ThreadLocal inheritableThreadLocal = new InheritableThreadLocal<>(); - ThreadLocal.set("父类数据:threadLocal"); - inheritableThreadLocal.set("父类数据:inheritableThreadLocal"); + ThreadLocal threadLocal = new ThreadLocal<>(); + InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal<>(); + threadLocal.set("Parent data: threadLocal"); + inheritableThreadLocal.set("Parent data: inheritableThreadLocal"); new Thread(new Runnable() { @Override public void run() { - System.out.println("子线程获取父类ThreadLocal数据:" + ThreadLocal.get()); - System.out.println("子线程获取父类inheritableThreadLocal数据:" + inheritableThreadLocal.get()); + System.out.println("Child thread gets parent ThreadLocal data: " + threadLocal.get()); + System.out.println("Child thread gets parent inheritableThreadLocal data: " + inheritableThreadLocal.get()); } }).start(); } } ``` -打印结果: +Output: ```java -子线程获取父类ThreadLocal数据:null -子线程获取父类inheritableThreadLocal数据:父类数据:inheritableThreadLocal +Child thread gets parent ThreadLocal data: null +Child thread gets parent inheritableThreadLocal data: Parent data: inheritableThreadLocal ``` -实现原理是子线程是通过在父线程中通过调用`new Thread()`方法来创建子线程,`Thread#init`方法在`Thread`的构造方法中被调用。在`init`方法中拷贝父线程数据到子线程中: +The implementation principle is that child threads are initialized through the parent thread by calling the `new Thread()` method. The `Thread#init` method within the `Thread` constructor is invoked at this point, during which parent thread data is copied over to the child thread: ```java private void init(ThreadGroup g, Runnable target, String name, @@ -808,33 +780,33 @@ private void init(ThreadGroup g, Runnable target, String name, } ``` -但`InheritableThreadLocal`仍然有缺陷,一般我们做异步化处理都是使用的线程池,而`InheritableThreadLocal`是在`new Thread`中的`init()`方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。 +However, `InheritableThreadLocal` still has drawbacks, as asynchronous processing typically utilizes thread pools; `InheritableThreadLocal` is set during the `init()` method under `new Thread`, meaning potential concerns arise regarding thread reuse. -当然,有问题出现就会有解决问题的方案,阿里巴巴开源了一个`TransmittableThreadLocal`组件就可以解决这个问题,这里就不再延伸,感兴趣的可自行查阅资料。 +Fortunately, when issues arise, solutions surface. Alibaba has open-sourced a `TransmittableThreadLocal` component, which resolves this problem. Those interested may consult additional resources. -### `ThreadLocal`项目中使用实战 +### Practical Uses of `ThreadLocal` in Projects -#### `ThreadLocal`使用场景 +#### Usage Scenarios for `ThreadLocal` -我们现在项目中日志记录用的是`ELK+Logstash`,最后在`Kibana`中进行展示和检索。 +In our project, logging is done using `ELK + Logstash`, with `Kibana` employed for display and retrieval. -现在都是分布式系统统一对外提供服务,项目间调用的关系可以通过 `traceId` 来关联,但是不同项目之间如何传递 `traceId` 呢? +Currently, services deploy distributed systems providing services externally; inter-project call relationships can be linked via `traceId`. However, how should `traceId` be transferred between different projects? -这里我们使用 `org.slf4j.MDC` 来实现此功能,内部就是通过 `ThreadLocal` 来实现的,具体实现如下: +Here we employ `org.slf4j.MDC` to achieve this, which is implemented internally via `ThreadLocal`. The specific execution is as follows: -当前端发送请求到**服务 A**时,**服务 A**会生成一个类似`UUID`的`traceId`字符串,将此字符串放入当前线程的`ThreadLocal`中,在调用**服务 B**的时候,将`traceId`写入到请求的`Header`中,**服务 B**在接收请求时会先判断请求的`Header`中是否有`traceId`,如果存在则写入自己线程的`ThreadLocal`中。 +When the frontend sends a request to **Service A**, **Service A** generates a `traceId` string akin to `UUID`, placing it into the current thread's `ThreadLocal`. In calling **Service B**, this `traceId` is inserted into the request's headers; upon **Service B** receiving the request, it first examines whether the request’s headers contain `traceId`, and if present, it writes it into its own thread’s `ThreadLocal`. ![](./images/thread-local/30.png) -图中的`requestId`即为我们各个系统链路关联的`traceId`,系统间互相调用,通过这个`requestId`即可找到对应链路,这里还有会有一些其他场景: +In the diagram, `requestId` represents our `traceId` associated with different system chains, enabling mutual calls. This also permits additional scenarios: ![](./images/thread-local/31.png) -针对于这些场景,我们都可以有相应的解决方案,如下所示 +For these scenarios, we can devise appropriate solutions, as outlined below. -#### Feign 远程调用解决方案 +#### Feign Remote Call Solution -**服务发送请求:** +**Service Sending Request:** ```java @Component @@ -851,7 +823,7 @@ public class FeignInvokeInterceptor implements RequestInterceptor { } ``` -**服务接收请求:** +**Service Receiving Request:** ```java @Slf4j @@ -869,7 +841,6 @@ public class LogInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - String requestId = request.getHeader(BaseConstant.REQUEST_ID_KEY); if (StringUtils.isBlank(requestId)) { requestId = UUID.randomUUID().toString().replace("-", ""); @@ -880,9 +851,9 @@ public class LogInterceptor extends HandlerInterceptorAdapter { } ``` -#### 线程池异步调用,requestId 传递 +#### Passing requestId in Asynchronous Thread Pool Calls -因为`MDC`是基于`ThreadLocal`去实现的,异步过程中,子线程并没有办法获取到父线程`ThreadLocal`存储的数据,所以这里可以自定义线程池执行器,修改其中的`run()`方法: +Since `MDC` is implemented based on `ThreadLocal`, during asynchronous execution, child threads cannot access data stored in the parent thread's `ThreadLocal`. Therefore, we can add a customized thread pool executor and modify the `run()` method: ```java public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { @@ -907,8 +878,6 @@ public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { } ``` -#### 使用 MQ 发送消息给第三方系统 - -在 MQ 发送的消息体中自定义属性`requestId`,接收方消费消息后,自己解析`requestId`使用即可。 +#### Using MQ to Send Messages to Third-Party Systems - +A custom attribute named `requestId` is added to the message body in MQ. Upon consumer retrieval of the message, they can decode and utilize `requestId` at will. diff --git a/docs/java/concurrent/virtual-thread.md b/docs/java/concurrent/virtual-thread.md index f7f889fb81f..d056ed1ddd8 100644 --- a/docs/java/concurrent/virtual-thread.md +++ b/docs/java/concurrent/virtual-thread.md @@ -1,51 +1,51 @@ --- -title: 虚拟线程常见问题总结 +title: Summary of Common Questions about Virtual Threads category: Java tag: - - Java并发 + - Java Concurrency --- -> 本文部分内容来自 [Lorin](https://github.com/Lorin-github) 的[PR](https://github.com/Snailclimb/JavaGuide/pull/2190)。 +> Some content in this article is derived from [Lorin](https://github.com/Lorin-github)'s [PR](https://github.com/Snailclimb/JavaGuide/pull/2190). -虚拟线程在 Java 21 正式发布,这是一项重量级的更新。 +Virtual threads were officially released in Java 21, marking a significant update. -## 什么是虚拟线程? +## What are Virtual Threads? -虚拟线程(Virtual Thread)是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),由 JVM 调度。许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。 +Virtual threads are lightweight threads (Lightweight Process, LWP) implemented by the JDK rather than the OS, scheduled by the JVM. Many virtual threads share the same operating system thread, and the number of virtual threads can far exceed the number of operating system threads. -## 虚拟线程和平台线程有什么关系? +## What is the relationship between Virtual Threads and Platform Threads? -在引入虚拟线程之前,`java.lang.Thread` 包已经支持所谓的平台线程(Platform Thread),也就是没有虚拟线程之前,我们一直使用的线程。JVM 调度程序通过平台线程(载体线程)来管理虚拟线程,一个平台线程可以在不同的时间执行不同的虚拟线程(多个虚拟线程挂载在一个平台线程上),当虚拟线程被阻塞或等待时,平台线程可以切换到执行另一个虚拟线程。 +Before the introduction of virtual threads, the `java.lang.Thread` package already supported what are known as platform threads. These are the threads we have been using prior to the existence of virtual threads. The JVM scheduler manages virtual threads through platform threads (carrier threads), where a platform thread can execute different virtual threads at different times (multiple virtual threads are mounted on a single platform thread). When a virtual thread is blocked or waiting, the platform thread can switch to execute another virtual thread. -虚拟线程、平台线程和系统内核线程的关系图如下所示(图源:[How to Use Java 19 Virtual Threads](https://medium.com/javarevisited/how-to-use-java-19-virtual-threads-c16a32bad5f7)): +The relationship diagram between virtual threads, platform threads, and system kernel threads is shown below (Image source: [How to Use Java 19 Virtual Threads](https://medium.com/javarevisited/how-to-use-java-19-virtual-threads-c16a32bad5f7)): -![虚拟线程、平台线程和系统内核线程的关系](https://oss.javaguide.cn/github/javaguide/java/new-features/virtual-threads-platform-threads-kernel-threads-relationship.png) +![Relationship between Virtual Threads, Platform Threads, and System Kernel Threads](https://oss.javaguide.cn/github/javaguide/java/new-features/virtual-threads-platform-threads-kernel-threads-relationship.png) -关于平台线程和系统内核线程的对应关系多提一点:在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个平台线程对应一个系统内核线程。Solaris 系统是一个特例,HotSpot VM 在 Solaris 上支持多对多和一对一。具体可以参考 R 大的回答: [JVM 中的线程模型是用户级的么?](https://www.zhihu.com/question/23096638/answer/29617153)。 +A bit more about the correspondence between platform threads and system kernel threads: In mainstream operating systems like Windows and Linux, Java threads adopt a one-to-one thread model, meaning one platform thread corresponds to one system kernel thread. Solaris is an exception, where the HotSpot VM supports both many-to-many and one-to-one threading. For more details, refer to R's answer: [Is the thread model in JVM user-level?](https://www.zhihu.com/question/23096638/answer/29617153). -## 虚拟线程有什么优点和缺点? +## What are the advantages and disadvantages of Virtual Threads? -### 优点 +### Advantages -- **非常轻量级**:可以在单个线程中创建成百上千个虚拟线程而不会导致过多的线程创建和上下文切换。 -- **简化异步编程**: 虚拟线程可以简化异步编程,使代码更易于理解和维护。它可以将异步代码编写得更像同步代码,避免了回调地狱(Callback Hell)。 -- **减少资源开销**: 由于虚拟线程是由 JVM 实现的,它能够更高效地利用底层资源,例如 CPU 和内存。虚拟线程的上下文切换比平台线程更轻量,因此能够更好地支持高并发场景。 +- **Very Lightweight**: You can create hundreds or thousands of virtual threads within a single thread without causing excessive thread creation and context switching. +- **Simplified Asynchronous Programming**: Virtual threads can simplify asynchronous programming, making the code easier to understand and maintain. They allow asynchronous code to be written more like synchronous code, avoiding callback hell. +- **Reduced Resource Overhead**: Since virtual threads are implemented by the JVM, they can utilize underlying resources like CPU and memory more efficiently. The context switching of virtual threads is lighter than that of platform threads, making them better suited for high-concurrency scenarios. -### 缺点 +### Disadvantages -- **不适用于计算密集型任务**: 虚拟线程适用于 I/O 密集型任务,但不适用于计算密集型任务,因为密集型计算始终需要 CPU 资源作为支持。 -- **与某些第三方库不兼容**: 虽然虚拟线程设计时考虑了与现有代码的兼容性,但某些依赖平台线程特性的第三方库可能不完全兼容虚拟线程。 +- **Not Suitable for CPU-Intensive Tasks**: Virtual threads are suitable for I/O-intensive tasks but not for CPU-intensive tasks, as intensive computation always requires CPU resources. +- **Incompatibility with Certain Third-Party Libraries**: Although virtual threads were designed with compatibility with existing code in mind, some third-party libraries that rely on platform thread characteristics may not be fully compatible with virtual threads. -## 如何创建虚拟线程? +## How to Create Virtual Threads? -官方提供了以下四种方式创建虚拟线程: +The official documentation provides the following four ways to create virtual threads: -1. 使用 `Thread.startVirtualThread()` 创建 -2. 使用 `Thread.ofVirtual()` 创建 -3. 使用 `ThreadFactory` 创建 -4. 使用 `Executors.newVirtualThreadPerTaskExecutor()`创建 +1. Create using `Thread.startVirtualThread()` +2. Create using `Thread.ofVirtual()` +3. Create using `ThreadFactory` +4. Create using `Executors.newVirtualThreadPerTaskExecutor()` -**1、使用 `Thread.startVirtualThread()` 创建** +**1. Create using `Thread.startVirtualThread()`** ```java public class VirtualThreadTest { @@ -63,16 +63,16 @@ static class CustomThread implements Runnable { } ``` -**2、使用 `Thread.ofVirtual()` 创建** +**2. Create using `Thread.ofVirtual()`** ```java public class VirtualThreadTest { public static void main(String[] args) { CustomThread customThread = new CustomThread(); - // 创建不启动 + // Create without starting Thread unStarted = Thread.ofVirtual().unstarted(customThread); unStarted.start(); - // 创建直接启动 + // Create and start directly Thread.ofVirtual().start(customThread); } } @@ -84,7 +84,7 @@ static class CustomThread implements Runnable { } ``` -**3、使用 `ThreadFactory` 创建** +**3. Create using `ThreadFactory`** ```java public class VirtualThreadTest { @@ -98,139 +98,4 @@ public class VirtualThreadTest { static class CustomThread implements Runnable { @Override - public void run() { - System.out.println("CustomThread run"); - } -} -``` - -**4、使用`Executors.newVirtualThreadPerTaskExecutor()`创建** - -```java -public class VirtualThreadTest { - public static void main(String[] args) { - CustomThread customThread = new CustomThread(); - ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); - executor.submit(customThread); - } -} -static class CustomThread implements Runnable { - @Override - public void run() { - System.out.println("CustomThread run"); - } -} -``` - -## 虚拟线程和平台线程性能对比 - -通过多线程和虚拟线程的方式处理相同的任务,对比创建的系统线程数和处理耗时。 - -**说明**:统计创建的系统线程中部分为后台线程(比如 GC 线程),两种场景下都一样,所以并不影响对比。 - -**测试代码**: - -```java -public class VirtualThreadTest { - static List list = new ArrayList<>(); - public static void main(String[] args) { - // 开启线程 统计平台线程数 - ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); - scheduledExecutorService.scheduleAtFixedRate(() -> { - ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); - ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false); - updateMaxThreadNum(threadInfo.length); - }, 10, 10, TimeUnit.MILLISECONDS); - - long start = System.currentTimeMillis(); - // 虚拟线程 - ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); - // 使用平台线程 - // ExecutorService executor = Executors.newFixedThreadPool(200); - for (int i = 0; i < 10000; i++) { - executor.submit(() -> { - try { - // 线程睡眠 0.5 s,模拟业务处理 - TimeUnit.MILLISECONDS.sleep(500); - } catch (InterruptedException ignored) { - } - }); - } - executor.close(); - System.out.println("max:" + list.get(0) + " platform thread/os thread"); - System.out.printf("totalMillis:%dms\n", System.currentTimeMillis() - start); - - - } - // 更新创建的平台最大线程数 - private static void updateMaxThreadNum(int num) { - if (list.isEmpty()) { - list.add(num); - } else { - Integer integer = list.get(0); - if (num > integer) { - list.add(0, num); - } - } - } -} -``` - -**请求数 10000 单请求耗时 1s**: - -```plain -// Virtual Thread -max:22 platform thread/os thread -totalMillis:1806ms - -// Platform Thread 线程数200 -max:209 platform thread/os thread -totalMillis:50578ms - -// Platform Thread 线程数500 -max:509 platform thread/os thread -totalMillis:20254ms - -// Platform Thread 线程数1000 -max:1009 platform thread/os thread -totalMillis:10214ms - -// Platform Thread 线程数2000 -max:2009 platform thread/os thread -totalMillis:5358ms -``` - -**请求数 10000 单请求耗时 0.5s**: - -```plain -// Virtual Thread -max:22 platform thread/os thread -totalMillis:1316ms - -// Platform Thread 线程数200 -max:209 platform thread/os thread -totalMillis:25619ms - -// Platform Thread 线程数500 -max:509 platform thread/os thread -totalMillis:10277ms - -// Platform Thread 线程数1000 -max:1009 platform thread/os thread -totalMillis:5197ms - -// Platform Thread 线程数2000 -max:2009 platform thread/os thread -totalMillis:2865ms -``` - -- 可以看到在密集 IO 的场景下,需要创建大量的平台线程异步处理才能达到虚拟线程的处理速度。 -- 因此,在密集 IO 的场景,虚拟线程可以大幅提高线程的执行效率,减少线程资源的创建以及上下文切换。 - -**注意**:有段时间 JDK 一直致力于 Reactor 响应式编程来提高 Java 性能,但响应式编程难以理解、调试、使用,最终又回到了同步编程,最终虚拟线程诞生。 - -## 虚拟线程的底层原理是什么? - -如果你想要详细了解虚拟线程实现原理,推荐一篇文章:[虚拟线程 - VirtualThread 源码透视](https://www.cnblogs.com/throwable/p/16758997.html)。 - -面试一般是不会问到这个问题的,仅供学有余力的同学进一步研究学习。 + public void run \ No newline at end of file diff --git a/docs/java/io/io-basis.md b/docs/java/io/io-basis.md index 1ea1bcd3f86..7a398bb9457 100755 --- a/docs/java/io/io-basis.md +++ b/docs/java/io/io-basis.md @@ -1,46 +1,46 @@ --- -title: Java IO 基础知识总结 +title: Summary of Java IO Basics category: Java tag: - Java IO - - Java基础 + - Java Basics --- -## IO 流简介 +## Introduction to IO Streams -IO 即 `Input/Output`,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。 +IO stands for `Input/Output`, which refers to input and output. The process of inputting data into the computer's memory is called input, while the process of outputting data to external storage (such as databases, files, or remote hosts) is called output. The data transmission process is similar to the flow of water, hence the term IO stream. In Java, IO streams are divided into input streams and output streams, and further classified into byte streams and character streams based on how data is processed. -Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。 +The more than 40 classes of Java IO streams are derived from the following four abstract class bases. -- `InputStream`/`Reader`: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 -- `OutputStream`/`Writer`: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 +- `InputStream`/`Reader`: The base class for all input streams, where the former is a byte input stream and the latter is a character input stream. +- `OutputStream`/`Writer`: The base class for all output streams, where the former is a byte output stream and the latter is a character output stream. -## 字节流 +## Byte Streams -### InputStream(字节输入流) - -`InputStream`用于从源头(通常是文件)读取数据(字节信息)到内存中,`java.io.InputStream`抽象类是所有字节输入流的父类。 +### InputStream (Byte Input Stream) -`InputStream` 常用方法: +`InputStream` is used to read data (byte information) from a source (usually a file) into memory. The abstract class `java.io.InputStream` is the parent class of all byte input streams. -- `read()`:返回输入流中下一个字节的数据。返回的值介于 0 到 255 之间。如果未读取任何字节,则代码返回 `-1` ,表示文件结束。 -- `read(byte b[ ])` : 从输入流中读取一些字节存储到数组 `b` 中。如果数组 `b` 的长度为零,则不读取。如果没有可用字节读取,返回 `-1`。如果有可用字节读取,则最多读取的字节数最多等于 `b.length` , 返回读取的字节数。这个方法等价于 `read(b, 0, b.length)`。 -- `read(byte b[], int off, int len)`:在`read(byte b[ ])` 方法的基础上增加了 `off` 参数(偏移量)和 `len` 参数(要读取的最大字节数)。 -- `skip(long n)`:忽略输入流中的 n 个字节 ,返回实际忽略的字节数。 -- `available()`:返回输入流中可以读取的字节数。 -- `close()`:关闭输入流释放相关的系统资源。 +Common methods of `InputStream`: -从 Java 9 开始,`InputStream` 新增加了多个实用的方法: +- `read()`: Returns the next byte of data from the input stream. The returned value is between 0 and 255. If no bytes have been read, it returns `-1`, indicating the end of the file. +- `read(byte b[])`: Reads some bytes from the input stream and stores them into the array `b`. If the length of array `b` is zero, no bytes are read. If there are no available bytes to read, it returns `-1`. If there are available bytes to read, it reads up to `b.length` bytes and returns the number of bytes read. This method is equivalent to `read(b, 0, b.length)`. +- `read(byte b[], int off, int len)`: This method adds the `off` parameter (offset) and `len` parameter (maximum number of bytes to read) to the `read(byte b[])` method. +- `skip(long n)`: Skips `n` bytes in the input stream and returns the actual number of bytes skipped. +- `available()`: Returns the number of bytes that can be read from the input stream. +- `close()`: Closes the input stream and releases related system resources. -- `readAllBytes()`:读取输入流中的所有字节,返回字节数组。 -- `readNBytes(byte[] b, int off, int len)`:阻塞直到读取 `len` 个字节。 -- `transferTo(OutputStream out)`:将所有字节从一个输入流传递到一个输出流。 +Starting from Java 9, `InputStream` has added several utility methods: -`FileInputStream` 是一个比较常用的字节输入流对象,可直接指定文件路径,可以直接读取单字节数据,也可以读取至字节数组中。 +- `readAllBytes()`: Reads all bytes from the input stream and returns a byte array. +- `readNBytes(byte[] b, int off, int len)`: Blocks until `len` bytes are read. +- `transferTo(OutputStream out)`: Transfers all bytes from one input stream to an output stream. -`FileInputStream` 代码示例: +`FileInputStream` is a commonly used byte input stream object that allows direct specification of the file path. It can read single-byte data or read into a byte array. + +Example code for `FileInputStream`: ```java try (InputStream fis = new FileInputStream("input.txt")) { @@ -58,11 +58,11 @@ try (InputStream fis = new FileInputStream("input.txt")) { } ``` -`input.txt` 文件内容: +Content of `input.txt`: ![](https://oss.javaguide.cn/github/javaguide/java/image-20220419155214614.png) -输出: +Output: ```plain Number of remaining bytes:11 @@ -70,481 +70,16 @@ The actual number of bytes skipped:2 The content read from file:JavaGuide ``` -不过,一般我们是不会直接单独使用 `FileInputStream` ,通常会配合 `BufferedInputStream`(字节缓冲输入流,后文会讲到)来使用。 +However, we generally do not use `FileInputStream` alone; it is usually used in conjunction with `BufferedInputStream` (byte buffered input stream, which will be discussed later). -像下面这段代码在我们的项目中就比较常见,我们通过 `readAllBytes()` 读取输入流所有字节并将其直接赋值给一个 `String` 对象。 +The following code is quite common in our projects, where we use `readAllBytes()` to read all bytes from the input stream and directly assign them to a `String` object. ```java -// 新建一个 BufferedInputStream 对象 +// Create a new BufferedInputStream object BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt")); -// 读取文件的内容并复制到 String 对象中 +// Read the content of the file and copy it to a String object String result = new String(bufferedInputStream.readAllBytes()); System.out.println(result); ``` -`DataInputStream` 用于读取指定类型数据,不能单独使用,必须结合其它流,比如 `FileInputStream` 。 - -```java -FileInputStream fileInputStream = new FileInputStream("input.txt"); -//必须将fileInputStream作为构造参数才能使用 -DataInputStream dataInputStream = new DataInputStream(fileInputStream); -//可以读取任意具体的类型数据 -dataInputStream.readBoolean(); -dataInputStream.readInt(); -dataInputStream.readUTF(); -``` - -`ObjectInputStream` 用于从输入流中读取 Java 对象(反序列化),`ObjectOutputStream` 用于将对象写入到输出流(序列化)。 - -```java -ObjectInputStream input = new ObjectInputStream(new FileInputStream("object.data")); -MyClass object = (MyClass) input.readObject(); -input.close(); -``` - -另外,用于序列化和反序列化的类必须实现 `Serializable` 接口,对象中如果有属性不想被序列化,使用 `transient` 修饰。 - -### OutputStream(字节输出流) - -`OutputStream`用于将数据(字节信息)写入到目的地(通常是文件),`java.io.OutputStream`抽象类是所有字节输出流的父类。 - -`OutputStream` 常用方法: - -- `write(int b)`:将特定字节写入输出流。 -- `write(byte b[ ])` : 将数组`b` 写入到输出流,等价于 `write(b, 0, b.length)` 。 -- `write(byte[] b, int off, int len)` : 在`write(byte b[ ])` 方法的基础上增加了 `off` 参数(偏移量)和 `len` 参数(要读取的最大字节数)。 -- `flush()`:刷新此输出流并强制写出所有缓冲的输出字节。 -- `close()`:关闭输出流释放相关的系统资源。 - -`FileOutputStream` 是最常用的字节输出流对象,可直接指定文件路径,可以直接输出单字节数据,也可以输出指定的字节数组。 - -`FileOutputStream` 代码示例: - -```java -try (FileOutputStream output = new FileOutputStream("output.txt")) { - byte[] array = "JavaGuide".getBytes(); - output.write(array); -} catch (IOException e) { - e.printStackTrace(); -} -``` - -运行结果: - -![](https://oss.javaguide.cn/github/javaguide/java/image-20220419155514392.png) - -类似于 `FileInputStream`,`FileOutputStream` 通常也会配合 `BufferedOutputStream`(字节缓冲输出流,后文会讲到)来使用。 - -```java -FileOutputStream fileOutputStream = new FileOutputStream("output.txt"); -BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream) -``` - -**`DataOutputStream`** 用于写入指定类型数据,不能单独使用,必须结合其它流,比如 `FileOutputStream` 。 - -```java -// 输出流 -FileOutputStream fileOutputStream = new FileOutputStream("out.txt"); -DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); -// 输出任意数据类型 -dataOutputStream.writeBoolean(true); -dataOutputStream.writeByte(1); -``` - -`ObjectInputStream` 用于从输入流中读取 Java 对象(`ObjectInputStream`,反序列化),`ObjectOutputStream`将对象写入到输出流(`ObjectOutputStream`,序列化)。 - -```java -ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("file.txt") -Person person = new Person("Guide哥", "JavaGuide作者"); -output.writeObject(person); -``` - -## 字符流 - -不管是文件读写还是网络发送接收,信息的最小存储单元都是字节。 **那为什么 I/O 流操作要分为字节流操作和字符流操作呢?** - -个人认为主要有两点原因: - -- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时。 -- 如果我们不知道编码类型就很容易出现乱码问题。 - -乱码问题这个很容易就可以复现,我们只需要将上面提到的 `FileInputStream` 代码示例中的 `input.txt` 文件内容改为中文即可,原代码不需要改动。 - -![](https://oss.javaguide.cn/github/javaguide/java/image-20220419154632551.png) - -输出: - -```java -Number of remaining bytes:9 -The actual number of bytes skipped:2 -The content read from file:§å®¶å¥½ -``` - -可以很明显地看到读取出来的内容已经变成了乱码。 - -因此,I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 - -字符流默认采用的是 `Unicode` 编码,我们可以通过构造方法自定义编码。 - -Unicode 本身只是一种字符集,它为每个字符分配一个唯一的数字编号,并没有规定具体的存储方式。UTF-8、UTF-16、UTF-32 都是 Unicode 的编码方式,它们使用不同的字节数来表示 Unicode 字符。例如,UTF-8 :英文占 1 字节,中文占 3 字节。 - -### Reader(字符输入流) - -`Reader`用于从源头(通常是文件)读取数据(字符信息)到内存中,`java.io.Reader`抽象类是所有字符输入流的父类。 - -`Reader` 用于读取文本, `InputStream` 用于读取原始字节。 - -`Reader` 常用方法: - -- `read()` : 从输入流读取一个字符。 -- `read(char[] cbuf)` : 从输入流中读取一些字符,并将它们存储到字符数组 `cbuf`中,等价于 `read(cbuf, 0, cbuf.length)` 。 -- `read(char[] cbuf, int off, int len)`:在`read(char[] cbuf)` 方法的基础上增加了 `off` 参数(偏移量)和 `len` 参数(要读取的最大字符数)。 -- `skip(long n)`:忽略输入流中的 n 个字符 ,返回实际忽略的字符数。 -- `close()` : 关闭输入流并释放相关的系统资源。 - -`InputStreamReader` 是字节流转换为字符流的桥梁,其子类 `FileReader` 是基于该基础上的封装,可以直接操作字符文件。 - -```java -// 字节流转换为字符流的桥梁 -public class InputStreamReader extends Reader { -} -// 用于读取字符文件 -public class FileReader extends InputStreamReader { -} -``` - -`FileReader` 代码示例: - -```java -try (FileReader fileReader = new FileReader("input.txt");) { - int content; - long skip = fileReader.skip(3); - System.out.println("The actual number of bytes skipped:" + skip); - System.out.print("The content read from file:"); - while ((content = fileReader.read()) != -1) { - System.out.print((char) content); - } -} catch (IOException e) { - e.printStackTrace(); -} -``` - -`input.txt` 文件内容: - -![](https://oss.javaguide.cn/github/javaguide/java/image-20220419154632551.png) - -输出: - -```plain -The actual number of bytes skipped:3 -The content read from file:我是Guide。 -``` - -### Writer(字符输出流) - -`Writer`用于将数据(字符信息)写入到目的地(通常是文件),`java.io.Writer`抽象类是所有字符输出流的父类。 - -`Writer` 常用方法: - -- `write(int c)` : 写入单个字符。 -- `write(char[] cbuf)`:写入字符数组 `cbuf`,等价于`write(cbuf, 0, cbuf.length)`。 -- `write(char[] cbuf, int off, int len)`:在`write(char[] cbuf)` 方法的基础上增加了 `off` 参数(偏移量)和 `len` 参数(要读取的最大字符数)。 -- `write(String str)`:写入字符串,等价于 `write(str, 0, str.length())` 。 -- `write(String str, int off, int len)`:在`write(String str)` 方法的基础上增加了 `off` 参数(偏移量)和 `len` 参数(要读取的最大字符数)。 -- `append(CharSequence csq)`:将指定的字符序列附加到指定的 `Writer` 对象并返回该 `Writer` 对象。 -- `append(char c)`:将指定的字符附加到指定的 `Writer` 对象并返回该 `Writer` 对象。 -- `flush()`:刷新此输出流并强制写出所有缓冲的输出字符。 -- `close()`:关闭输出流释放相关的系统资源。 - -`OutputStreamWriter` 是字符流转换为字节流的桥梁,其子类 `FileWriter` 是基于该基础上的封装,可以直接将字符写入到文件。 - -```java -// 字符流转换为字节流的桥梁 -public class OutputStreamWriter extends Writer { -} -// 用于写入字符到文件 -public class FileWriter extends OutputStreamWriter { -} -``` - -`FileWriter` 代码示例: - -```java -try (Writer output = new FileWriter("output.txt")) { - output.write("你好,我是Guide。"); -} catch (IOException e) { - e.printStackTrace(); -} -``` - -输出结果: - -![](https://oss.javaguide.cn/github/javaguide/java/image-20220419155802288.png) - -## 字节缓冲流 - -IO 操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。 - -字节缓冲流这里采用了装饰器模式来增强 `InputStream` 和`OutputStream`子类对象的功能。 - -举个例子,我们可以通过 `BufferedInputStream`(字节缓冲输入流)来增强 `FileInputStream` 的功能。 - -```java -// 新建一个 BufferedInputStream 对象 -BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt")); -``` - -字节流和字节缓冲流的性能差别主要体现在我们使用两者的时候都是调用 `write(int b)` 和 `read()` 这两个一次只读取一个字节的方法的时候。由于字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓存区,大幅减少 IO 次数,提高读取效率。 - -我使用 `write(int b)` 和 `read()` 方法,分别通过字节流和字节缓冲流复制一个 `524.9 mb` 的 PDF 文件耗时对比如下: - -```plain -使用缓冲流复制PDF文件总耗时:15428 毫秒 -使用普通字节流复制PDF文件总耗时:2555062 毫秒 -``` - -两者耗时差别非常大,缓冲流耗费的时间是字节流的 1/165。 - -测试代码如下: - -```java -@Test -void copy_pdf_to_another_pdf_buffer_stream() { - // 记录开始时间 - long start = System.currentTimeMillis(); - try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("深入理解计算机操作系统.pdf")); - BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("深入理解计算机操作系统-副本.pdf"))) { - int content; - while ((content = bis.read()) != -1) { - bos.write(content); - } - } catch (IOException e) { - e.printStackTrace(); - } - // 记录结束时间 - long end = System.currentTimeMillis(); - System.out.println("使用缓冲流复制PDF文件总耗时:" + (end - start) + " 毫秒"); -} - -@Test -void copy_pdf_to_another_pdf_stream() { - // 记录开始时间 - long start = System.currentTimeMillis(); - try (FileInputStream fis = new FileInputStream("深入理解计算机操作系统.pdf"); - FileOutputStream fos = new FileOutputStream("深入理解计算机操作系统-副本.pdf")) { - int content; - while ((content = fis.read()) != -1) { - fos.write(content); - } - } catch (IOException e) { - e.printStackTrace(); - } - // 记录结束时间 - long end = System.currentTimeMillis(); - System.out.println("使用普通流复制PDF文件总耗时:" + (end - start) + " 毫秒"); -} -``` - -如果是调用 `read(byte b[])` 和 `write(byte b[], int off, int len)` 这两个写入一个字节数组的方法的话,只要字节数组的大小合适,两者的性能差距其实不大,基本可以忽略。 - -这次我们使用 `read(byte b[])` 和 `write(byte b[], int off, int len)` 方法,分别通过字节流和字节缓冲流复制一个 524.9 mb 的 PDF 文件耗时对比如下: - -```plain -使用缓冲流复制PDF文件总耗时:695 毫秒 -使用普通字节流复制PDF文件总耗时:989 毫秒 -``` - -两者耗时差别不是很大,缓冲流的性能要略微好一点点。 - -测试代码如下: - -```java -@Test -void copy_pdf_to_another_pdf_with_byte_array_buffer_stream() { - // 记录开始时间 - long start = System.currentTimeMillis(); - try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("深入理解计算机操作系统.pdf")); - BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("深入理解计算机操作系统-副本.pdf"))) { - int len; - byte[] bytes = new byte[4 * 1024]; - while ((len = bis.read(bytes)) != -1) { - bos.write(bytes, 0, len); - } - } catch (IOException e) { - e.printStackTrace(); - } - // 记录结束时间 - long end = System.currentTimeMillis(); - System.out.println("使用缓冲流复制PDF文件总耗时:" + (end - start) + " 毫秒"); -} - -@Test -void copy_pdf_to_another_pdf_with_byte_array_stream() { - // 记录开始时间 - long start = System.currentTimeMillis(); - try (FileInputStream fis = new FileInputStream("深入理解计算机操作系统.pdf"); - FileOutputStream fos = new FileOutputStream("深入理解计算机操作系统-副本.pdf")) { - int len; - byte[] bytes = new byte[4 * 1024]; - while ((len = fis.read(bytes)) != -1) { - fos.write(bytes, 0, len); - } - } catch (IOException e) { - e.printStackTrace(); - } - // 记录结束时间 - long end = System.currentTimeMillis(); - System.out.println("使用普通流复制PDF文件总耗时:" + (end - start) + " 毫秒"); -} -``` - -### BufferedInputStream(字节缓冲输入流) - -`BufferedInputStream` 从源头(通常是文件)读取数据(字节信息)到内存的过程中不会一个字节一个字节的读取,而是会先将读取到的字节存放在缓存区,并从内部缓冲区中单独读取字节。这样大幅减少了 IO 次数,提高了读取效率。 - -`BufferedInputStream` 内部维护了一个缓冲区,这个缓冲区实际就是一个字节数组,通过阅读 `BufferedInputStream` 源码即可得到这个结论。 - -```java -public -class BufferedInputStream extends FilterInputStream { - // 内部缓冲区数组 - protected volatile byte buf[]; - // 缓冲区的默认大小 - private static int DEFAULT_BUFFER_SIZE = 8192; - // 使用默认的缓冲区大小 - public BufferedInputStream(InputStream in) { - this(in, DEFAULT_BUFFER_SIZE); - } - // 自定义缓冲区大小 - public BufferedInputStream(InputStream in, int size) { - super(in); - if (size <= 0) { - throw new IllegalArgumentException("Buffer size <= 0"); - } - buf = new byte[size]; - } -} -``` - -缓冲区的大小默认为 **8192** 字节,当然了,你也可以通过 `BufferedInputStream(InputStream in, int size)` 这个构造方法来指定缓冲区的大小。 - -### BufferedOutputStream(字节缓冲输出流) - -`BufferedOutputStream` 将数据(字节信息)写入到目的地(通常是文件)的过程中不会一个字节一个字节的写入,而是会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了效率 - -```java -try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.txt"))) { - byte[] array = "JavaGuide".getBytes(); - bos.write(array); -} catch (IOException e) { - e.printStackTrace(); -} -``` - -类似于 `BufferedInputStream` ,`BufferedOutputStream` 内部也维护了一个缓冲区,并且,这个缓存区的大小也是 **8192** 字节。 - -## 字符缓冲流 - -`BufferedReader` (字符缓冲输入流)和 `BufferedWriter`(字符缓冲输出流)类似于 `BufferedInputStream`(字节缓冲输入流)和`BufferedOutputStream`(字节缓冲输入流),内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息。 - -## 打印流 - -下面这段代码大家经常使用吧? - -```java -System.out.print("Hello!"); -System.out.println("Hello!"); -``` - -`System.out` 实际是用于获取一个 `PrintStream` 对象,`print`方法实际调用的是 `PrintStream` 对象的 `write` 方法。 - -`PrintStream` 属于字节打印流,与之对应的是 `PrintWriter` (字符打印流)。`PrintStream` 是 `OutputStream` 的子类,`PrintWriter` 是 `Writer` 的子类。 - -```java -public class PrintStream extends FilterOutputStream - implements Appendable, Closeable { -} -public class PrintWriter extends Writer { -} -``` - -## 随机访问流 - -这里要介绍的随机访问流指的是支持随意跳转到文件的任意位置进行读写的 `RandomAccessFile` 。 - -`RandomAccessFile` 的构造方法如下,我们可以指定 `mode`(读写模式)。 - -```java -// openAndDelete 参数默认为 false 表示打开文件并且这个文件不会被删除 -public RandomAccessFile(File file, String mode) - throws FileNotFoundException { - this(file, mode, false); -} -// 私有方法 -private RandomAccessFile(File file, String mode, boolean openAndDelete) throws FileNotFoundException{ - // 省略大部分代码 -} -``` - -读写模式主要有下面四种: - -- `r` : 只读模式。 -- `rw`: 读写模式 -- `rws`: 相对于 `rw`,`rws` 同步更新对“文件的内容”或“元数据”的修改到外部存储设备。 -- `rwd` : 相对于 `rw`,`rwd` 同步更新对“文件的内容”的修改到外部存储设备。 - -文件内容指的是文件中实际保存的数据,元数据则是用来描述文件属性比如文件的大小信息、创建和修改时间。 - -`RandomAccessFile` 中有一个文件指针用来表示下一个将要被写入或者读取的字节所处的位置。我们可以通过 `RandomAccessFile` 的 `seek(long pos)` 方法来设置文件指针的偏移量(距文件开头 `pos` 个字节处)。如果想要获取文件指针当前的位置的话,可以使用 `getFilePointer()` 方法。 - -`RandomAccessFile` 代码示例: - -```java -RandomAccessFile randomAccessFile = new RandomAccessFile(new File("input.txt"), "rw"); -System.out.println("读取之前的偏移量:" + randomAccessFile.getFilePointer() + ",当前读取到的字符" + (char) randomAccessFile.read() + ",读取之后的偏移量:" + randomAccessFile.getFilePointer()); -// 指针当前偏移量为 6 -randomAccessFile.seek(6); -System.out.println("读取之前的偏移量:" + randomAccessFile.getFilePointer() + ",当前读取到的字符" + (char) randomAccessFile.read() + ",读取之后的偏移量:" + randomAccessFile.getFilePointer()); -// 从偏移量 7 的位置开始往后写入字节数据 -randomAccessFile.write(new byte[]{'H', 'I', 'J', 'K'}); -// 指针当前偏移量为 0,回到起始位置 -randomAccessFile.seek(0); -System.out.println("读取之前的偏移量:" + randomAccessFile.getFilePointer() + ",当前读取到的字符" + (char) randomAccessFile.read() + ",读取之后的偏移量:" + randomAccessFile.getFilePointer()); -``` - -`input.txt` 文件内容: - -![](https://oss.javaguide.cn/github/javaguide/java/image-20220421162050158.png) - -输出: - -```plain -读取之前的偏移量:0,当前读取到的字符A,读取之后的偏移量:1 -读取之前的偏移量:6,当前读取到的字符G,读取之后的偏移量:7 -读取之前的偏移量:0,当前读取到的字符A,读取之后的偏移量:1 -``` - -`input.txt` 文件内容变为 `ABCDEFGHIJK` 。 - -`RandomAccessFile` 的 `write` 方法在写入对象的时候如果对应的位置已经有数据的话,会将其覆盖掉。 - -```java -RandomAccessFile randomAccessFile = new RandomAccessFile(new File("input.txt"), "rw"); -randomAccessFile.write(new byte[]{'H', 'I', 'J', 'K'}); -``` - -假设运行上面这段程序之前 `input.txt` 文件内容变为 `ABCD` ,运行之后则变为 `HIJK` 。 - -`RandomAccessFile` 比较常见的一个应用就是实现大文件的 **断点续传** 。何谓断点续传?简单来说就是上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。分片(先将文件切分成多个文件分片)上传是断点续传的基础。 - -`RandomAccessFile` 可以帮助我们合并文件分片,示例代码如下: - -![](https://oss.javaguide.cn/github/javaguide/java/io/20210609164749122.png) - -我在[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中详细介绍了大文件的上传问题。 - -![](https://oss.javaguide.cn/github/javaguide/java/image-20220428104115362.png) - -`RandomAccessFile` 的实现依赖于 `FileDescriptor` (文件描述符) 和 `FileChannel` (内存映射文件)。 - - +`DataInputStream` is used to read data of specified types and cannot be used alone; it must be combined with other streams, such as \`FileInputStream diff --git a/docs/java/io/io-design-patterns.md b/docs/java/io/io-design-patterns.md index f005a18ece4..00f03d0a790 100644 --- a/docs/java/io/io-design-patterns.md +++ b/docs/java/io/io-design-patterns.md @@ -1,26 +1,26 @@ --- -title: Java IO 设计模式总结 +title: Summary of Java IO Design Patterns category: Java tag: - Java IO - - Java基础 + - Java Basics --- -这篇文章我们简单来看看我们从 IO 中能够学习到哪些设计模式的应用。 +In this article, we will briefly explore what design patterns we can learn from IO. -## 装饰器模式 +## Decorator Pattern -**装饰器(Decorator)模式** 可以在不改变原有对象的情况下拓展其功能。 +The **Decorator Pattern** allows for extending the functionality of an object without changing its original structure. -装饰器模式通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。 +The decorator pattern uses composition to extend the functionality of the original class instead of inheritance, making it more practical in scenarios with complex inheritance relationships (such as the various class hierarchies in IO). -对于字节流来说, `FilterInputStream` (对应输入流)和`FilterOutputStream`(对应输出流)是装饰器模式的核心,分别用于增强 `InputStream` 和`OutputStream`子类对象的功能。 +For byte streams, `FilterInputStream` (for input streams) and `FilterOutputStream` (for output streams) are the core of the decorator pattern, used to enhance the functionality of `InputStream` and `OutputStream` subclasses. -我们常见的`BufferedInputStream`(字节缓冲输入流)、`DataInputStream` 等等都是`FilterInputStream` 的子类,`BufferedOutputStream`(字节缓冲输出流)、`DataOutputStream`等等都是`FilterOutputStream`的子类。 +Common subclasses of `FilterInputStream` include `BufferedInputStream` (byte buffered input stream) and `DataInputStream`, while `BufferedOutputStream` (byte buffered output stream) and `DataOutputStream` are subclasses of `FilterOutputStream`. -举个例子,我们可以通过 `BufferedInputStream`(字节缓冲输入流)来增强 `FileInputStream` 的功能。 +For example, we can enhance the functionality of `FileInputStream` using `BufferedInputStream`. -`BufferedInputStream` 构造函数如下: +The constructor of `BufferedInputStream` is as follows: ```java public BufferedInputStream(InputStream in) { @@ -36,9 +36,9 @@ public BufferedInputStream(InputStream in, int size) { } ``` -可以看出,`BufferedInputStream` 的构造函数其中的一个参数就是 `InputStream` 。 +As we can see, one of the parameters of the `BufferedInputStream` constructor is `InputStream`. -`BufferedInputStream` 代码示例: +Example code for `BufferedInputStream`: ```java try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"))) { @@ -52,15 +52,15 @@ try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("inpu } ``` -这个时候,你可以会想了:**为啥我们直接不弄一个`BufferedFileInputStream`(字符缓冲文件输入流)呢?** +At this point, you might wonder: **Why don't we just create a `BufferedFileInputStream`?** ```java BufferedFileInputStream bfis = new BufferedFileInputStream("input.txt"); ``` -如果 `InputStream`的子类比较少的话,这样做是没问题的。不过, `InputStream`的子类实在太多,继承关系也太复杂了。如果我们为每一个子类都定制一个对应的缓冲输入流,那岂不是太麻烦了。 +If there were only a few subclasses of `InputStream`, this approach would be fine. However, there are too many subclasses of `InputStream`, and the inheritance relationships are quite complex. If we were to create a corresponding buffered input stream for each subclass, it would be too cumbersome. -如果你对 IO 流比较熟悉的话,你会发现`ZipInputStream` 和`ZipOutputStream` 还可以分别增强 `BufferedInputStream` 和 `BufferedOutputStream` 的能力。 +If you are familiar with IO streams, you will notice that `ZipInputStream` and `ZipOutputStream` can further enhance the capabilities of `BufferedInputStream` and `BufferedOutputStream`, respectively. ```java BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName)); @@ -70,7 +70,7 @@ BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileNam ZipOutputStream zipOut = new ZipOutputStream(bos); ``` -`ZipInputStream` 和`ZipOutputStream` 分别继承自`InflaterInputStream` 和`DeflaterOutputStream`。 +`ZipInputStream` and `ZipOutputStream` inherit from `InflaterInputStream` and `DeflaterOutputStream`, respectively. ```java public @@ -80,242 +80,24 @@ class InflaterInputStream extends FilterInputStream { public class DeflaterOutputStream extends FilterOutputStream { } - ``` -这也是装饰器模式很重要的一个特征,那就是可以对原始类嵌套使用多个装饰器。 +This is also an important feature of the decorator pattern, which allows for the nested use of multiple decorators on the original class. -为了实现这一效果,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。上面介绍到的这些 IO 相关的装饰类和原始类共同的父类是 `InputStream` 和`OutputStream`。 +To achieve this effect, the decorator class needs to inherit from the same abstract class or implement the same interface as the original class. The IO-related decorator classes and original classes mentioned above share the common parent classes `InputStream` and `OutputStream`. -对于字符流来说,`BufferedReader` 可以用来增加 `Reader` (字符输入流)子类的功能,`BufferedWriter` 可以用来增加 `Writer` (字符输出流)子类的功能。 +For character streams, `BufferedReader` can be used to enhance the functionality of `Reader` (character input stream) subclasses, while `BufferedWriter` can enhance the functionality of `Writer` (character output stream) subclasses. ```java BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), "UTF-8")); ``` -IO 流中的装饰器模式应用的例子实在是太多了,不需要特意记忆,完全没必要哈!搞清了装饰器模式的核心之后,你在使用的时候自然就会知道哪些地方运用到了装饰器模式。 - -## 适配器模式 - -**适配器(Adapter Pattern)模式** 主要用于接口互不兼容的类的协调工作,你可以将其联想到我们日常经常使用的电源适配器。 - -适配器模式中存在被适配的对象或者类称为 **适配者(Adaptee)** ,作用于适配者的对象或者类称为**适配器(Adapter)** 。适配器分为对象适配器和类适配器。类适配器使用继承关系来实现,对象适配器使用组合关系来实现。 - -IO 流中的字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,更准确点来说是对象适配器。通过适配器,我们可以将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。 - -`InputStreamReader` 和 `OutputStreamWriter` 就是两个适配器(Adapter), 同时,它们两个也是字节流和字符流之间的桥梁。`InputStreamReader` 使用 `StreamDecoder` (流解码器)对字节进行解码,**实现字节流到字符流的转换,** `OutputStreamWriter` 使用`StreamEncoder`(流编码器)对字符进行编码,实现字符流到字节流的转换。 - -`InputStream` 和 `OutputStream` 的子类是被适配者, `InputStreamReader` 和 `OutputStreamWriter`是适配器。 - -```java -// InputStreamReader 是适配器,FileInputStream 是被适配的类 -InputStreamReader isr = new InputStreamReader(new FileInputStream(fileName), "UTF-8"); -// BufferedReader 增强 InputStreamReader 的功能(装饰器模式) -BufferedReader bufferedReader = new BufferedReader(isr); -``` - -`java.io.InputStreamReader` 部分源码: - -```java -public class InputStreamReader extends Reader { - //用于解码的对象 - private final StreamDecoder sd; - public InputStreamReader(InputStream in) { - super(in); - try { - // 获取 StreamDecoder 对象 - sd = StreamDecoder.forInputStreamReader(in, this, (String)null); - } catch (UnsupportedEncodingException e) { - throw new Error(e); - } - } - // 使用 StreamDecoder 对象做具体的读取工作 - public int read() throws IOException { - return sd.read(); - } -} -``` - -`java.io.OutputStreamWriter` 部分源码: - -```java -public class OutputStreamWriter extends Writer { - // 用于编码的对象 - private final StreamEncoder se; - public OutputStreamWriter(OutputStream out) { - super(out); - try { - // 获取 StreamEncoder 对象 - se = StreamEncoder.forOutputStreamWriter(out, this, (String)null); - } catch (UnsupportedEncodingException e) { - throw new Error(e); - } - } - // 使用 StreamEncoder 对象做具体的写入工作 - public void write(int c) throws IOException { - se.write(c); - } -} -``` - -**适配器模式和装饰器模式有什么区别呢?** - -**装饰器模式** 更侧重于动态地增强原始类的功能,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。并且,装饰器模式支持对原始类嵌套使用多个装饰器。 - -**适配器模式** 更侧重于让接口不兼容而不能交互的类可以一起工作,当我们调用适配器对应的方法时,适配器内部会调用适配者类或者和适配类相关的类的方法,这个过程透明的。就比如说 `StreamDecoder` (流解码器)和`StreamEncoder`(流编码器)就是分别基于 `InputStream` 和 `OutputStream` 来获取 `FileChannel`对象并调用对应的 `read` 方法和 `write` 方法进行字节数据的读取和写入。 - -```java -StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) { - // 省略大部分代码 - // 根据 InputStream 对象获取 FileChannel 对象 - ch = getChannel((FileInputStream)in); -} -``` - -适配器和适配者两者不需要继承相同的抽象类或者实现相同的接口。 - -另外,`FutureTask` 类使用了适配器模式,`Executors` 的内部类 `RunnableAdapter` 实现属于适配器,用于将 `Runnable` 适配成 `Callable`。 - -`FutureTask`参数包含 `Runnable` 的一个构造方法: - -```java -public FutureTask(Runnable runnable, V result) { - // 调用 Executors 类的 callable 方法 - this.callable = Executors.callable(runnable, result); - this.state = NEW; -} -``` - -`Executors`中对应的方法和适配器: +There are numerous examples of the application of the decorator pattern in IO streams, and there is no need to memorize them; it is completely unnecessary! Once you understand the core of the decorator pattern, you will naturally recognize where it is applied when using it. -```java -// 实际调用的是 Executors 的内部类 RunnableAdapter 的构造方法 -public static Callable callable(Runnable task, T result) { - if (task == null) - throw new NullPointerException(); - return new RunnableAdapter(task, result); -} -// 适配器 -static final class RunnableAdapter implements Callable { - final Runnable task; - final T result; - RunnableAdapter(Runnable task, T result) { - this.task = task; - this.result = result; - } - public T call() { - task.run(); - return result; - } -} -``` - -## 工厂模式 - -工厂模式用于创建对象,NIO 中大量用到了工厂模式,比如 `Files` 类的 `newInputStream` 方法用于创建 `InputStream` 对象(静态工厂)、 `Paths` 类的 `get` 方法创建 `Path` 对象(静态工厂)、`ZipFileSystem` 类(`sun.nio`包下的类,属于 `java.nio` 相关的一些内部实现)的 `getPath` 的方法创建 `Path` 对象(简单工厂)。 - -```java -InputStream is = Files.newInputStream(Paths.get(generatorLogoPath)) -``` - -## 观察者模式 - -NIO 中的文件目录监听服务使用到了观察者模式。 - -NIO 中的文件目录监听服务基于 `WatchService` 接口和 `Watchable` 接口。`WatchService` 属于观察者,`Watchable` 属于被观察者。 - -`Watchable` 接口定义了一个用于将对象注册到 `WatchService`(监控服务) 并绑定监听事件的方法 `register` 。 - -```java -public interface Path - extends Comparable, Iterable, Watchable{ -} - -public interface Watchable { - WatchKey register(WatchService watcher, - WatchEvent.Kind[] events, - WatchEvent.Modifier... modifiers) - throws IOException; -} -``` - -`WatchService` 用于监听文件目录的变化,同一个 `WatchService` 对象能够监听多个文件目录。 - -```java -// 创建 WatchService 对象 -WatchService watchService = FileSystems.getDefault().newWatchService(); - -// 初始化一个被监控文件夹的 Path 类: -Path path = Paths.get("workingDirectory"); -// 将这个 path 对象注册到 WatchService(监控服务) 中去 -WatchKey watchKey = path.register( -watchService, StandardWatchEventKinds...); -``` - -`Path` 类 `register` 方法的第二个参数 `events` (需要监听的事件)为可变长参数,也就是说我们可以同时监听多种事件。 - -```java -WatchKey register(WatchService watcher, - WatchEvent.Kind... events) - throws IOException; -``` - -常用的监听事件有 3 种: - -- `StandardWatchEventKinds.ENTRY_CREATE`:文件创建。 -- `StandardWatchEventKinds.ENTRY_DELETE` : 文件删除。 -- `StandardWatchEventKinds.ENTRY_MODIFY` : 文件修改。 - -`register` 方法返回 `WatchKey` 对象,通过`WatchKey` 对象可以获取事件的具体信息比如文件目录下是创建、删除还是修改了文件、创建、删除或者修改的文件的具体名称是什么。 - -```java -WatchKey key; -while ((key = watchService.take()) != null) { - for (WatchEvent event : key.pollEvents()) { - // 可以调用 WatchEvent 对象的方法做一些事情比如输出事件的具体上下文信息 - } - key.reset(); -} -``` - -`WatchService` 内部是通过一个 daemon thread(守护线程)采用定期轮询的方式来检测文件的变化,简化后的源码如下所示。 - -```java -class PollingWatchService - extends AbstractWatchService -{ - // 定义一个 daemon thread(守护线程)轮询检测文件变化 - private final ScheduledExecutorService scheduledExecutor; - - PollingWatchService() { - scheduledExecutor = Executors - .newSingleThreadScheduledExecutor(new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setDaemon(true); - return t; - }}); - } - - void enable(Set> events, long period) { - synchronized (this) { - // 更新监听事件 - this.events = events; - - // 开启定期轮询 - Runnable thunk = new Runnable() { public void run() { poll(); }}; - this.poller = scheduledExecutor - .scheduleAtFixedRate(thunk, period, period, TimeUnit.SECONDS); - } - } -} -``` +## Adapter Pattern -## 参考 +The **Adapter Pattern** is primarily used to coordinate the work of classes with incompatible interfaces, similar to the power adapters we commonly use in daily life. -- Patterns in Java APIs: -- 装饰器模式:通过剖析 Java IO 类库源码学习装饰器模式: -- sun.nio 包是什么,是 java 代码么? - RednaxelaFX +In the adapter pattern, the object or class being adapted is called the **Adaptee**, while the object or class that acts on the Adaptee is called the **Adapter**. There are two types of adapters: class adapters and object adapters. Class adapters use inheritance to implement, while object adapters use composition. - +The interfaces of character streams and byte streams in IO are different, and their ability to work together is based on the adapter pattern, more specifically, the object adapter. Through the adapter, we can adapt a byte stream diff --git a/docs/java/io/io-model.md b/docs/java/io/io-model.md index e6d48bc0439..597c2402fbc 100644 --- a/docs/java/io/io-model.md +++ b/docs/java/io/io-model.md @@ -1,132 +1,76 @@ --- -title: Java IO 模型详解 +title: Detailed Explanation of Java IO Models category: Java tag: - Java IO - - Java基础 + - Java Basics --- -IO 模型这块确实挺难理解的,需要太多计算机底层知识。写这篇文章用了挺久,就非常希望能把我所知道的讲出来吧!希望朋友们能有收获!为了写这篇文章,还翻看了一下《UNIX 网络编程》这本书,太难了,我滴乖乖!心痛~ +The IO model is indeed quite difficult to understand and requires a lot of knowledge about the underlying computer systems. It took me quite a while to write this article, and I really hope to share what I know! I hope my friends can gain something from it! To write this article, I also revisited the book "UNIX Network Programming," which was really challenging—oh my goodness! It hurts my heart~ -_个人能力有限。如果文章有任何需要补充/完善/修改的地方,欢迎在评论区指出,共同进步!_ +_My personal abilities are limited. If there are any areas in the article that need to be supplemented, improved, or modified, please feel free to point them out in the comments section for our mutual progress!_ -## 前言 +## Introduction -I/O 一直是很多小伙伴难以理解的一个知识点,这篇文章我会将我所理解的 I/O 讲给你听,希望可以对你有所帮助。 +I/O has always been a challenging concept for many friends to grasp. In this article, I will explain my understanding of I/O, hoping it can be helpful to you. ## I/O -### 何为 I/O? +### What is I/O? -I/O(**I**nput/**O**utput) 即**输入/输出** 。 +I/O (**I**nput/**O**utput) refers to **input/output**. -**我们先从计算机结构的角度来解读一下 I/O。** +**Let's first interpret I/O from the perspective of computer architecture.** -根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。 +According to the Von Neumann architecture, a computer system is divided into five main parts: the arithmetic logic unit, the control unit, memory, input devices, and output devices. -![冯诺依曼体系结构](https://oss.javaguide.cn/github/javaguide/java/io/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9pcy1jbG91ZC5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70.jpeg) +![Von Neumann Architecture](https://oss.javaguide.cn/github/javaguide/java/io/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9pcy1jbG91ZC5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70.jpeg) -输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。 +Input devices (like keyboards) and output devices (like monitors) are both considered external devices. Network cards and hard drives can serve as both input and output devices. -输入设备向计算机输入数据,输出设备接收计算机输出的数据。 +Input devices send data to the computer, while output devices receive data from the computer. -**从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。** +**From the perspective of computer architecture, I/O describes the process of communication between the computer system and external devices.** -**我们再先从应用程序的角度来解读一下 I/O。** +**Now let's interpret I/O from the perspective of application programs.** -根据大学里学到的操作系统相关的知识:为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 **用户空间(User space)** 和 **内核空间(Kernel space )** 。 +Based on what we learned in university about operating systems: to ensure the stability and security of the operating system, a process's address space is divided into **user space** and **kernel space**. -像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。 +Typically, the applications we run operate in user space, while only kernel space can perform system-level resource-related operations, such as file management, process communication, memory management, etc. This means that to perform I/O operations, we must rely on the capabilities of kernel space. -并且,用户空间的程序不能直接访问内核空间。 +Moreover, programs in user space cannot directly access kernel space. -当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。 +When an I/O operation is to be executed, due to the lack of permission to perform these operations, a system call must be initiated to request the operating system's assistance. -因此,用户进程想要执行 IO 操作的话,必须通过 **系统调用** 来间接访问内核空间 +Therefore, if a user process wants to perform an I/O operation, it must indirectly access kernel space through **system calls**. -我们在平常开发过程中接触最多的就是 **磁盘 IO(读写文件)** 和 **网络 IO(网络请求和响应)**。 +In our usual development process, we most frequently encounter **disk I/O (reading and writing files)** and **network I/O (network requests and responses)**. -**从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。** +**From the perspective of application programs, our applications initiate I/O calls (system calls) to the operating system's kernel, which is responsible for executing the specific I/O operations. In other words, our applications merely initiate the I/O operation calls, while the actual execution of I/O is completed by the operating system's kernel.** -当应用程序发起 I/O 调用后,会经历两个步骤: +After an application program initiates an I/O call, it goes through two steps: -1. 内核等待 I/O 设备准备好数据 -2. 内核将数据从内核空间拷贝到用户空间。 +1. The kernel waits for the I/O device to be ready with data. +1. The kernel copies the data from kernel space to user space. -### 有哪些常见的 IO 模型? +### What are the common I/O models? -UNIX 系统下, IO 模型一共有 5 种:**同步阻塞 I/O**、**同步非阻塞 I/O**、**I/O 多路复用**、**信号驱动 I/O** 和**异步 I/O**。 +In UNIX systems, there are five I/O models: **synchronous blocking I/O**, **synchronous non-blocking I/O**, **I/O multiplexing**, **signal-driven I/O**, and **asynchronous I/O**. -这也是我们经常提到的 5 种 IO 模型。 +These are the five I/O models we often refer to. -## Java 中 3 种常见 IO 模型 +## Three Common I/O Models in Java ### BIO (Blocking I/O) -**BIO 属于同步阻塞 IO 模型** 。 +**BIO belongs to the synchronous blocking I/O model.** -同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。 +In the synchronous blocking I/O model, when an application initiates a read call, it will block until the kernel copies the data to user space. -![图源:《深入拆解Tomcat & Jetty》](https://oss.javaguide.cn/p3-juejin/6a9e704af49b4380bb686f0c96d33b81~tplv-k3u1fbpfcp-watermark.png) +![Source: "Deep Dive into Tomcat & Jetty"](https://oss.javaguide.cn/p3-juejin/6a9e704af49b4380bb686f0c96d33b81~tplv-k3u1fbpfcp-watermark.png) -在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 +This works fine when the number of client connections is low. However, when faced with hundreds of thousands or even millions of connections, the traditional BIO model is inadequate. Therefore, we need a more efficient I/O processing model to handle higher concurrency. ### NIO (Non-blocking/New I/O) -Java 中的 NIO 于 Java 1.4 中引入,对应 `java.nio` 包,提供了 `Channel` , `Selector`,`Buffer` 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它是支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。 - -Java 中的 NIO 可以看作是 **I/O 多路复用模型**。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。 - -跟着我的思路往下看看,相信你会得到答案! - -我们先来看看 **同步非阻塞 IO 模型**。 - -![图源:《深入拆解Tomcat & Jetty》](https://oss.javaguide.cn/p3-juejin/bb174e22dbe04bb79fe3fc126aed0c61~tplv-k3u1fbpfcp-watermark.png) - -同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。 - -相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。 - -但是,这种 IO 模型同样存在问题:**应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。** - -这个时候,**I/O 多路复用模型** 就上场了。 - -![](https://oss.javaguide.cn/github/javaguide/java/io/88ff862764024c3b8567367df11df6ab~tplv-k3u1fbpfcp-watermark.png) - -IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。 - -> 目前支持 IO 多路复用的系统调用,有 select,epoll 等等。select 系统调用,目前几乎在所有的操作系统上都有支持。 -> -> - **select 调用**:内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。 -> - **epoll 调用**:linux 2.6 内核,属于 select 调用的增强版本,优化了 IO 的执行效率。 - -**IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。** - -Java 中的 NIO ,有一个非常重要的**选择器 ( Selector )** 的概念,也可以被称为 **多路复用器**。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。 - -![Buffer、Channel和Selector三者之间的关系](https://oss.javaguide.cn/github/javaguide/java/nio/channel-buffer-selector.png) - -### AIO (Asynchronous I/O) - -AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。 - -异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。 - -![](https://oss.javaguide.cn/github/javaguide/java/io/3077e72a1af049559e81d18205b56fd7~tplv-k3u1fbpfcp-watermark.png) - -目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO,不过又放弃了。这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。 - -最后,来一张图,简单总结一下 Java 中的 BIO、NIO、AIO。 - -![BIO、NIO 和 AIO 对比](https://oss.javaguide.cn/github/javaguide/java/nio/bio-aio-nio.png) - -## 参考 - -- 《深入拆解 Tomcat & Jetty》 -- 如何完成一次 IO: -- 程序员应该这样理解 IO:[https://www.jianshu.com/p/fa7bdc4f3de7](https://www.jianshu.com/p/fa7bdc4f3de7) -- 10 分钟看懂, Java NIO 底层原理: -- IO 模型知多少 | 理论篇: -- 《UNIX 网络编程 卷 1;套接字联网 API 》6.2 节 IO 模型 - - +NIO in Java was introduced in Java 1.4, corresponding to the `java.nio` package, diff --git a/docs/java/io/nio-basis.md b/docs/java/io/nio-basis.md index 4cf9723ba37..790ccd98582 100644 --- a/docs/java/io/nio-basis.md +++ b/docs/java/io/nio-basis.md @@ -1,52 +1,52 @@ --- -title: Java NIO 核心知识总结 +title: Summary of Core Knowledge of Java NIO category: Java tag: - Java IO - - Java基础 + - Java Basics --- -在学习 NIO 之前,需要先了解一下计算机 I/O 模型的基础理论知识。还不了解的话,可以参考我写的这篇文章:[Java IO 模型详解](https://javaguide.cn/java/io/io-model.html)。 +Before learning NIO, it is necessary to understand the basic theoretical knowledge of computer I/O models. If you are not familiar with it, you can refer to the article I wrote: [Detailed Explanation of Java IO Models](https://javaguide.cn/java/io/io-model.html). -## NIO 简介 +## Introduction to NIO -在传统的 Java I/O 模型(BIO)中,I/O 操作是以阻塞的方式进行的。也就是说,当一个线程执行一个 I/O 操作时,它会被阻塞直到操作完成。这种阻塞模型在处理多个并发连接时可能会导致性能瓶颈,因为需要为每个连接创建一个线程,而线程的创建和切换都是有开销的。 +In the traditional Java I/O model (BIO), I/O operations are performed in a blocking manner. This means that when a thread performs an I/O operation, it will be blocked until the operation is complete. This blocking model can lead to performance bottlenecks when handling multiple concurrent connections, as a thread needs to be created for each connection, and both thread creation and switching incur overhead. -为了解决这个问题,在 Java1.4 版本引入了一种新的 I/O 模型 — **NIO** (New IO,也称为 Non-blocking IO) 。NIO 弥补了同步阻塞 I/O 的不足,它在标准 Java 代码中提供了非阻塞、面向缓冲、基于通道的 I/O,可以使用少量的线程来处理多个连接,大大提高了 I/O 效率和并发。 +To address this issue, a new I/O model was introduced in Java 1.4 — **NIO** (New IO, also known as Non-blocking IO). NIO compensates for the shortcomings of synchronous blocking I/O by providing non-blocking, buffer-oriented, channel-based I/O in standard Java code, allowing a small number of threads to handle multiple connections, significantly improving I/O efficiency and concurrency. -下图是 BIO、NIO 和 AIO 处理客户端请求的简单对比图(关于 AIO 的介绍,可以看我写的这篇文章:[Java IO 模型详解](https://javaguide.cn/java/io/io-model.html),不是重点,了解即可)。 +The following diagram is a simple comparison of how BIO, NIO, and AIO handle client requests (for an introduction to AIO, you can refer to my article: [Detailed Explanation of Java IO Models](https://javaguide.cn/java/io/io-model.html), which is not the focus, just for understanding). -![BIO、NIO 和 AIO 对比](https://oss.javaguide.cn/github/javaguide/java/nio/bio-aio-nio.png) +![Comparison of BIO, NIO, and AIO](https://oss.javaguide.cn/github/javaguide/java/nio/bio-aio-nio.png) -⚠️需要注意:使用 NIO 并不一定意味着高性能,它的性能优势主要体现在高并发和高延迟的网络环境下。当连接数较少、并发程度较低或者网络传输速度较快时,NIO 的性能并不一定优于传统的 BIO 。 +⚠️ Note: Using NIO does not necessarily mean high performance; its performance advantages are mainly reflected in high concurrency and high-latency network environments. When the number of connections is small, concurrency is low, or network transmission speed is fast, NIO's performance may not necessarily be better than traditional BIO. -## NIO 核心组件 +## Core Components of NIO -NIO 主要包括以下三个核心组件: +NIO mainly includes the following three core components: -- **Buffer(缓冲区)**:NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。 -- **Channel(通道)**:Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。 -- **Selector(选择器)**:允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上,由 Selector 来分配线程来处理事件。 +- **Buffer**: NIO reads and writes data through buffers. During read operations, data from the Channel is filled into the Buffer, while during write operations, data from the Buffer is written into the Channel. +- **Channel**: A Channel is a bidirectional data transmission channel that can be read from and written to. NIO uses Channels to implement data input and output. A channel is an abstract concept that can represent a connection between files, sockets, or other data sources. +- **Selector**: Allows a thread to handle multiple Channels based on an event-driven I/O multiplexing model. All Channels can be registered with the Selector, which allocates threads to handle events. -三者的关系如下图所示(暂时不理解没关系,后文会详细介绍): +The relationship among the three components is shown in the diagram below (it's okay if you don't understand it for now; it will be explained in detail later): -![Buffer、Channel和Selector三者之间的关系](https://oss.javaguide.cn/github/javaguide/java/nio/channel-buffer-selector.png) +![Relationship among Buffer, Channel, and Selector](https://oss.javaguide.cn/github/javaguide/java/nio/channel-buffer-selector.png) -下面详细介绍一下这三个组件。 +Now, let's take a closer look at these three components. -### Buffer(缓冲区) +### Buffer -在传统的 BIO 中,数据的读写是面向流的, 分为字节流和字符流。 +In traditional BIO, data reading and writing are stream-oriented, divided into byte streams and character streams. -在 Java 1.4 的 NIO 库中,所有数据都是用缓冲区处理的,这是新库和之前的 BIO 的一个重要区别,有点类似于 BIO 中的缓冲流。NIO 在读取数据时,它是直接读到缓冲区中的。在写入数据时,写入到缓冲区中。 使用 NIO 在读写数据时,都是通过缓冲区进行操作。 +In the NIO library of Java 1.4, all data is processed using buffers, which is an important distinction between the new library and the previous BIO, somewhat similar to buffered streams in BIO. NIO reads data directly into the buffer. When writing data, it is written into the buffer. When using NIO for reading and writing data, operations are performed through buffers. -`Buffer` 的子类如下图所示。其中,最常用的是 `ByteBuffer`,它可以用来存储和操作字节数据。 +The subclasses of `Buffer` are shown in the diagram below. Among them, the most commonly used is `ByteBuffer`, which can be used to store and manipulate byte data. -![Buffer 的子类](https://oss.javaguide.cn/github/javaguide/java/nio/buffer-subclasses.png) +![Subclasses of Buffer](https://oss.javaguide.cn/github/javaguide/java/nio/buffer-subclasses.png) -你可以将 Buffer 理解为一个数组,`IntBuffer`、`FloatBuffer`、`CharBuffer` 等分别对应 `int[]`、`float[]`、`char[]` 等。 +You can think of a Buffer as an array, where `IntBuffer`, `FloatBuffer`, `CharBuffer`, etc., correspond to `int[]`, `float[]`, `char[]`, and so on. -为了更清晰地认识缓冲区,我们来简单看看`Buffer` 类中定义的四个成员变量: +To better understand the buffer, let's take a brief look at the four member variables defined in the `Buffer` class: ```java public abstract class Buffer { @@ -58,335 +58,7 @@ public abstract class Buffer { } ``` -这四个成员变量的具体含义如下: +The specific meanings of these four member variables are as follows: -1. 容量(`capacity`):`Buffer`可以存储的最大数据量,`Buffer`创建时设置且不可改变; -2. 界限(`limit`):`Buffer` 中可以读/写数据的边界。写模式下,`limit` 代表最多能写入的数据,一般等于 `capacity`(可以通过`limit(int newLimit)`方法设置);读模式下,`limit` 等于 Buffer 中实际写入的数据大小。 -3. 位置(`position`):下一个可以被读写的数据的位置(索引)。从写操作模式到读操作模式切换的时候(flip),`position` 都会归零,这样就可以从头开始读写了。 -4. 标记(`mark`):`Buffer`允许将位置直接定位到该标记处,这是一个可选属性; - -并且,上述变量满足如下的关系:**0 <= mark <= position <= limit <= capacity** 。 - -另外,Buffer 有读模式和写模式这两种模式,分别用于从 Buffer 中读取数据或者向 Buffer 中写入数据。Buffer 被创建之后默认是写模式,调用 `flip()` 可以切换到读模式。如果要再次切换回写模式,可以调用 `clear()` 或者 `compact()` 方法。 - -![position 、limit 和 capacity 之前的关系](https://oss.javaguide.cn/github/javaguide/java/nio/JavaNIOBuffer.png) - -![position 、limit 和 capacity 之前的关系](https://oss.javaguide.cn/github/javaguide/java/nio/NIOBufferClassAttributes.png) - -`Buffer` 对象不能通过 `new` 调用构造方法创建对象 ,只能通过静态方法实例化 `Buffer`。 - -这里以 `ByteBuffer`为例进行介绍: - -```java -// 分配堆内存 -public static ByteBuffer allocate(int capacity); -// 分配直接内存 -public static ByteBuffer allocateDirect(int capacity); -``` - -Buffer 最核心的两个方法: - -1. `get` : 读取缓冲区的数据 -2. `put` :向缓冲区写入数据 - -除上述两个方法之外,其他的重要方法: - -- `flip` :将缓冲区从写模式切换到读模式,它会将 `limit` 的值设置为当前 `position` 的值,将 `position` 的值设置为 0。 -- `clear`: 清空缓冲区,将缓冲区从读模式切换到写模式,并将 `position` 的值设置为 0,将 `limit` 的值设置为 `capacity` 的值。 -- …… - -Buffer 中数据变化的过程: - -```java -import java.nio.*; - -public class CharBufferDemo { - public static void main(String[] args) { - // 分配一个容量为8的CharBuffer - CharBuffer buffer = CharBuffer.allocate(8); - System.out.println("初始状态:"); - printState(buffer); - - // 向buffer写入3个字符 - buffer.put('a').put('b').put('c'); - System.out.println("写入3个字符后的状态:"); - printState(buffer); - - // 调用flip()方法,准备读取buffer中的数据,将 position 置 0,limit 的置 3 - buffer.flip(); - System.out.println("调用flip()方法后的状态:"); - printState(buffer); - - // 读取字符 - while (buffer.hasRemaining()) { - System.out.print(buffer.get()); - } - - // 调用clear()方法,清空缓冲区,将 position 的值置为 0,将 limit 的值置为 capacity 的值 - buffer.clear(); - System.out.println("调用clear()方法后的状态:"); - printState(buffer); - - } - - // 打印buffer的capacity、limit、position、mark的位置 - private static void printState(CharBuffer buffer) { - System.out.print("capacity: " + buffer.capacity()); - System.out.print(", limit: " + buffer.limit()); - System.out.print(", position: " + buffer.position()); - System.out.print(", mark 开始读取的字符: " + buffer.mark()); - System.out.println("\n"); - } -} -``` - -输出: - -```bash -初始状态: -capacity: 8, limit: 8, position: 0 - -写入3个字符后的状态: -capacity: 8, limit: 8, position: 3 - -准备读取buffer中的数据! - -调用flip()方法后的状态: -capacity: 8, limit: 3, position: 0 - -读取到的数据:abc - -调用clear()方法后的状态: -capacity: 8, limit: 8, position: 0 -``` - -为了帮助理解,我绘制了一张图片展示 `capacity`、`limit`和`position`每一阶段的变化。 - -![capacity、limit和position每一阶段的变化](https://oss.javaguide.cn/github/javaguide/java/nio/NIOBufferClassAttributesDataChanges.png) - -### Channel(通道) - -Channel 是一个通道,它建立了与数据源(如文件、网络套接字等)之间的连接。我们可以利用它来读取和写入数据,就像打开了一条自来水管,让数据在 Channel 中自由流动。 - -BIO 中的流是单向的,分为各种 `InputStream`(输入流)和 `OutputStream`(输出流),数据只是在一个方向上传输。通道与流的不同之处在于通道是双向的,它可以用于读、写或者同时用于读写。 - -Channel 与前面介绍的 Buffer 打交道,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。 - -![Channel 和 Buffer之间的关系](https://oss.javaguide.cn/github/javaguide/java/nio/channel-buffer.png) - -另外,因为 Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API。特别是在 UNIX 网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。 - -`Channel` 的子类如下图所示。 - -![Channel 的子类](https://oss.javaguide.cn/github/javaguide/java/nio/channel-subclasses.png) - -其中,最常用的是以下几种类型的通道: - -- `FileChannel`:文件访问通道; -- `SocketChannel`、`ServerSocketChannel`:TCP 通信通道; -- `DatagramChannel`:UDP 通信通道; - -![Channel继承关系图](https://oss.javaguide.cn/github/javaguide/java/nio/channel-inheritance-relationship.png) - -Channel 最核心的两个方法: - -1. `read` :读取数据并写入到 Buffer 中。 -2. `write` :将 Buffer 中的数据写入到 Channel 中。 - -这里我们以 `FileChannel` 为例演示一下是读取文件数据的。 - -```java -RandomAccessFile reader = new RandomAccessFile("/Users/guide/Documents/test_read.in", "r")) -FileChannel channel = reader.getChannel(); -ByteBuffer buffer = ByteBuffer.allocate(1024); -channel.read(buffer); -``` - -### Selector(选择器) - -Selector(选择器) 是 NIO 中的一个关键组件,它允许一个线程处理多个 Channel。Selector 是基于事件驱动的 I/O 多路复用模型,主要运作原理是:通过 Selector 注册通道的事件,Selector 会不断地轮询注册在其上的 Channel。当事件发生时,比如:某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来。Selector 会将相关的 Channel 加入到就绪集合中。通过 SelectionKey 可以获取就绪 Channel 的集合,然后对这些就绪的 Channel 进行相应的 I/O 操作。 - -![Selector 选择器工作示意图](https://oss.javaguide.cn/github/javaguide/java/nio/selector-channel-selectionkey.png) - -一个多路复用器 Selector 可以同时轮询多个 Channel,由于 JDK 使用了 `epoll()` 代替传统的 `select` 实现,所以它并没有最大连接句柄 `1024/2048` 的限制。这也就意味着只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。 - -Selector 可以监听以下四种事件类型: - -1. `SelectionKey.OP_ACCEPT`:表示通道接受连接的事件,这通常用于 `ServerSocketChannel`。 -2. `SelectionKey.OP_CONNECT`:表示通道完成连接的事件,这通常用于 `SocketChannel`。 -3. `SelectionKey.OP_READ`:表示通道准备好进行读取的事件,即有数据可读。 -4. `SelectionKey.OP_WRITE`:表示通道准备好进行写入的事件,即可以写入数据。 - -`Selector`是抽象类,可以通过调用此类的 `open()` 静态方法来创建 Selector 实例。Selector 可以同时监控多个 `SelectableChannel` 的 `IO` 状况,是非阻塞 `IO` 的核心。 - -一个 Selector 实例有三个 `SelectionKey` 集合: - -1. 所有的 `SelectionKey` 集合:代表了注册在该 Selector 上的 `Channel`,这个集合可以通过 `keys()` 方法返回。 -2. 被选择的 `SelectionKey` 集合:代表了所有可通过 `select()` 方法获取的、需要进行 `IO` 处理的 Channel,这个集合可以通过 `selectedKeys()` 返回。 -3. 被取消的 `SelectionKey` 集合:代表了所有被取消注册关系的 `Channel`,在下一次执行 `select()` 方法时,这些 `Channel` 对应的 `SelectionKey` 会被彻底删除,程序通常无须直接访问该集合,也没有暴露访问的方法。 - -简单演示一下如何遍历被选择的 `SelectionKey` 集合并进行处理: - -```java -Set selectedKeys = selector.selectedKeys(); -Iterator keyIterator = selectedKeys.iterator(); -while (keyIterator.hasNext()) { - SelectionKey key = keyIterator.next(); - if (key != null) { - if (key.isAcceptable()) { - // ServerSocketChannel 接收了一个新连接 - } else if (key.isConnectable()) { - // 表示一个新连接建立 - } else if (key.isReadable()) { - // Channel 有准备好的数据,可以读取 - } else if (key.isWritable()) { - // Channel 有空闲的 Buffer,可以写入数据 - } - } - keyIterator.remove(); -} -``` - -Selector 还提供了一系列和 `select()` 相关的方法: - -- `int select()`:监控所有注册的 `Channel`,当它们中间有需要处理的 `IO` 操作时,该方法返回,并将对应的 `SelectionKey` 加入被选择的 `SelectionKey` 集合中,该方法返回这些 `Channel` 的数量。 -- `int select(long timeout)`:可以设置超时时长的 `select()` 操作。 -- `int selectNow()`:执行一个立即返回的 `select()` 操作,相对于无参数的 `select()` 方法而言,该方法不会阻塞线程。 -- `Selector wakeup()`:使一个还未返回的 `select()` 方法立刻返回。 -- …… - -使用 Selector 实现网络读写的简单示例: - -```java -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; -import java.util.Iterator; -import java.util.Set; - -public class NioSelectorExample { - - public static void main(String[] args) { - try { - ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); - serverSocketChannel.configureBlocking(false); - serverSocketChannel.socket().bind(new InetSocketAddress(8080)); - - Selector selector = Selector.open(); - // 将 ServerSocketChannel 注册到 Selector 并监听 OP_ACCEPT 事件 - serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); - - while (true) { - int readyChannels = selector.select(); - - if (readyChannels == 0) { - continue; - } - - Set selectedKeys = selector.selectedKeys(); - Iterator keyIterator = selectedKeys.iterator(); - - while (keyIterator.hasNext()) { - SelectionKey key = keyIterator.next(); - - if (key.isAcceptable()) { - // 处理连接事件 - ServerSocketChannel server = (ServerSocketChannel) key.channel(); - SocketChannel client = server.accept(); - client.configureBlocking(false); - - // 将客户端通道注册到 Selector 并监听 OP_READ 事件 - client.register(selector, SelectionKey.OP_READ); - } else if (key.isReadable()) { - // 处理读事件 - SocketChannel client = (SocketChannel) key.channel(); - ByteBuffer buffer = ByteBuffer.allocate(1024); - int bytesRead = client.read(buffer); - - if (bytesRead > 0) { - buffer.flip(); - System.out.println("收到数据:" +new String(buffer.array(), 0, bytesRead)); - // 将客户端通道注册到 Selector 并监听 OP_WRITE 事件 - client.register(selector, SelectionKey.OP_WRITE); - } else if (bytesRead < 0) { - // 客户端断开连接 - client.close(); - } - } else if (key.isWritable()) { - // 处理写事件 - SocketChannel client = (SocketChannel) key.channel(); - ByteBuffer buffer = ByteBuffer.wrap("Hello, Client!".getBytes()); - client.write(buffer); - - // 将客户端通道注册到 Selector 并监听 OP_READ 事件 - client.register(selector, SelectionKey.OP_READ); - } - - keyIterator.remove(); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } -} -``` - -在示例中,我们创建了一个简单的服务器,监听 8080 端口,使用 Selector 处理连接、读取和写入事件。当接收到客户端的数据时,服务器将读取数据并将其打印到控制台,然后向客户端回复 "Hello, Client!"。 - -## NIO 零拷贝 - -零拷贝是提升 IO 操作性能的一个常用手段,像 ActiveMQ、Kafka 、RocketMQ、QMQ、Netty 等顶级开源项目都用到了零拷贝。 - -零拷贝是指计算机执行 IO 操作时,CPU 不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及 CPU 的拷贝时间。也就是说,零拷贝主要解决操作系统在处理 I/O 操作时频繁复制数据的问题。零拷贝的常见实现技术有: `mmap+write`、`sendfile`和 `sendfile + DMA gather copy` 。 - -下图展示了各种零拷贝技术的对比图: - -| | CPU 拷贝 | DMA 拷贝 | 系统调用 | 上下文切换 | -| -------------------------- | -------- | -------- | ---------- | ---------- | -| 传统方法 | 2 | 2 | read+write | 4 | -| mmap+write | 1 | 2 | mmap+write | 4 | -| sendfile | 1 | 2 | sendfile | 2 | -| sendfile + DMA gather copy | 0 | 2 | sendfile | 2 | - -可以看出,无论是传统的 I/O 方式,还是引入了零拷贝之后,2 次 DMA(Direct Memory Access) 拷贝是都少不了的。因为两次 DMA 都是依赖硬件完成的。零拷贝主要是减少了 CPU 拷贝及上下文的切换。 - -Java 对零拷贝的支持: - -- `MappedByteBuffer` 是 NIO 基于内存映射(`mmap`)这种零拷⻉⽅式的提供的⼀种实现,底层实际是调用了 Linux 内核的 `mmap` 系统调用。它可以将一个文件或者文件的一部分映射到内存中,形成一个虚拟内存文件,这样就可以直接操作内存中的数据,而不需要通过系统调用来读写文件。 -- `FileChannel` 的`transferTo()/transferFrom()`是 NIO 基于发送文件(`sendfile`)这种零拷贝方式的提供的一种实现,底层实际是调用了 Linux 内核的 `sendfile`系统调用。它可以直接将文件数据从磁盘发送到网络,而不需要经过用户空间的缓冲区。关于`FileChannel`的用法可以看看这篇文章:[Java NIO 文件通道 FileChannel 用法](https://www.cnblogs.com/robothy/p/14235598.html)。 - -代码示例: - -```java -private void loadFileIntoMemory(File xmlFile) throws IOException { - FileInputStream fis = new FileInputStream(xmlFile); - // 创建 FileChannel 对象 - FileChannel fc = fis.getChannel(); - // FileChannel.map() 将文件映射到直接内存并返回 MappedByteBuffer 对象 - MappedByteBuffer mmb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); - xmlFileBuffer = new byte[(int)fc.size()]; - mmb.get(xmlFileBuffer); - fis.close(); -} -``` - -## 总结 - -这篇文章我们主要介绍了 NIO 的核心知识点,包括 NIO 的核心组件和零拷贝。 - -如果我们需要使用 NIO 构建网络程序的话,不建议直接使用原生 NIO,编程复杂且功能性太弱,推荐使用一些成熟的基于 NIO 的网络编程框架比如 Netty。Netty 在 NIO 的基础上进行了一些优化和扩展比如支持多种协议、支持 SSL/TLS 等等。 - -## 参考 - -- Java NIO 浅析: - -- 面试官:Java NIO 了解? - -- Java NIO:Buffer、Channel 和 Selector: - - +1. Capacity (`capacity`): The maximum amount of data that the `Buffer` can store, set when the `Buffer` is created and cannot be changed. +1. Limit (`limit`): The boundary for reading/writing data in the `Buffer`. In write mode, `limit` represents the maximum amount of data that can be written, generally equal to `capacity` (can be set using the \`limit(int new diff --git a/docs/java/jvm/class-file-structure.md b/docs/java/jvm/class-file-structure.md index 31cc64e30fb..2b3b0edfd76 100644 --- a/docs/java/jvm/class-file-structure.md +++ b/docs/java/jvm/class-file-structure.md @@ -1,216 +1,76 @@ --- -title: 类文件结构详解 +title: Detailed Explanation of Class File Structure category: Java tag: - JVM --- -## 回顾一下字节码 +## A Review of Bytecode -在 Java 中,JVM 可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 +In Java, the code that the JVM can understand is called `bytecode` (i.e., files with the `.class` extension). It is not targeted at any specific processor but rather at the virtual machine. The Java language, through bytecode, addresses the low execution efficiency problem of traditional interpreted languages to some extent while retaining the portability characteristic of interpreted languages. Therefore, Java programs run efficiently, and since bytecode is not targeted at a specific machine, Java programs can run on various operating systems without recompilation. -Clojure(Lisp 语言的一种方言)、Groovy、Scala、JRuby、Kotlin 等语言都是运行在 Java 虚拟机之上。下图展示了不同的语言被不同的编译器编译成`.class`文件最终运行在 Java 虚拟机之上。`.class`文件的二进制格式可以使用 [WinHex](https://www.x-ways.net/winhex/) 查看。 +Languages such as Clojure (a dialect of Lisp), Groovy, Scala, JRuby, and Kotlin run on the Java Virtual Machine. The following diagram shows how different languages are compiled into `.class` files by different compilers and ultimately run on the Java Virtual Machine. The binary format of `.class` files can be viewed using [WinHex](https://www.x-ways.net/winhex/). -![运行在 Java 虚拟机之上的编程语言](https://oss.javaguide.cn/github/javaguide/java/basis/java-virtual-machine-program-language-os.png) +![Programming Languages Running on the Java Virtual Machine](https://oss.javaguide.cn/github/javaguide/java/basis/java-virtual-machine-program-language-os.png) -可以说`.class`文件是不同的语言在 Java 虚拟机之间的重要桥梁,同时也是支持 Java 跨平台很重要的一个原因。 +It can be said that `.class` files are an important bridge between different languages on the Java Virtual Machine and are also a significant reason supporting Java's cross-platform capabilities. -## Class 文件结构总结 +## Summary of Class File Structure -根据 Java 虚拟机规范,Class 文件通过 `ClassFile` 定义,有点类似 C 语言的结构体。 +According to the Java Virtual Machine Specification, a Class file is defined by `ClassFile`, which is somewhat similar to a structure in C. -`ClassFile` 的结构如下: +The structure of `ClassFile` is as follows: ```java ClassFile { - u4 magic; //Class 文件的标志 - u2 minor_version;//Class 的小版本号 - u2 major_version;//Class 的大版本号 - u2 constant_pool_count;//常量池的数量 - cp_info constant_pool[constant_pool_count-1];//常量池 - u2 access_flags;//Class 的访问标记 - u2 this_class;//当前类 - u2 super_class;//父类 - u2 interfaces_count;//接口数量 - u2 interfaces[interfaces_count];//一个类可以实现多个接口 - u2 fields_count;//字段数量 - field_info fields[fields_count];//一个类可以有多个字段 - u2 methods_count;//方法数量 - method_info methods[methods_count];//一个类可以有个多个方法 - u2 attributes_count;//此类的属性表中的属性数 - attribute_info attributes[attributes_count];//属性表集合 + u4 magic; // Signature of the Class file + u2 minor_version; // Minor version number of the Class + u2 major_version; // Major version number of the Class + u2 constant_pool_count; // Number of constants in the pool + cp_info constant_pool[constant_pool_count-1]; // Constant pool + u2 access_flags; // Access flags of the Class + u2 this_class; // Current class + u2 super_class; // Parent class + u2 interfaces_count; // Number of interfaces + u2 interfaces[interfaces_count]; // A class can implement multiple interfaces + u2 fields_count; // Number of fields + field_info fields[fields_count]; // A class can have multiple fields + u2 methods_count; // Number of methods + method_info methods[methods_count]; // A class can have multiple methods + u2 attributes_count; // Number of attributes in this class's attribute table + attribute_info attributes[attributes_count]; // Collection of attributes } ``` -通过分析 `ClassFile` 的内容,我们便可以知道 class 文件的组成。 +By analyzing the contents of `ClassFile`, we can understand the composition of a class file. -![ClassFile 内容分析](https://oss.javaguide.cn/java-guide-blog/16d5ec47609818fc.jpeg) +![ClassFile Content Analysis](https://oss.javaguide.cn/java-guide-blog/16d5ec47609818fc.jpeg) -下面这张图是通过 IDEA 插件 `jclasslib` 查看的,你可以更直观看到 Class 文件结构。 +The following image is viewed through the IDEA plugin `jclasslib`, allowing you to see the Class file structure more intuitively. ![](https://oss.javaguide.cn/java-guide-blog/image-20210401170711475.png) -使用 `jclasslib` 不光可以直观地查看某个类对应的字节码文件,还可以查看类的基本信息、常量池、接口、属性、函数等信息。 +Using `jclasslib`, you can not only visually inspect the bytecode file corresponding to a class but also view basic information about the class, constant pool, interfaces, attributes, functions, and more. -下面详细介绍一下 Class 文件结构涉及到的一些组件。 +Below are detailed introductions to some components involved in the Class file structure. -### 魔数(Magic Number) +### Magic Number ```java - u4 magic; //Class 文件的标志 + u4 magic; // Signature of the Class file ``` -每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是**确定这个文件是否为一个能被虚拟机接收的 Class 文件**。Java 规范规定魔数为固定值:0xCAFEBABE。如果读取的文件不是以这个魔数开头,Java 虚拟机将拒绝加载它。 +The first 4 bytes of each Class file are called the magic number, and its sole purpose is to **determine whether this file is a Class file that can be accepted by the virtual machine**. The Java specification defines the magic number as a fixed value: 0xCAFEBABE. If the read file does not start with this magic number, the Java Virtual Machine will refuse to load it. -### Class 文件版本号(Minor&Major Version) +### Class File Version Number (Minor & Major Version) ```java - u2 minor_version;//Class 的小版本号 - u2 major_version;//Class 的大版本号 + u2 minor_version; // Minor version number of the Class + u2 major_version; // Major version number of the Class ``` -紧接着魔数的四个字节存储的是 Class 文件的版本号:第 5 和第 6 个字节是**次版本号**,第 7 和第 8 个字节是**主版本号**。 +Immediately following the magic number are the version numbers of the Class file: the 5th and 6th bytes are the **minor version number**, and the 7th and 8th bytes are the **major version number**. -每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。你可以使用 `javap -v` 命令来快速查看 Class 文件的版本号信息。 +Each time a major version of Java is released (e.g., Java 8, Java 9), the major version number increases by 1. You can quickly check the version number information of a Class file using the `javap -v` command. -高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。所以,我们在实际开发的时候要确保开发的的 JDK 版本和生产环境的 JDK 版本保持一致。 - -### 常量池(Constant Pool) - -```java - u2 constant_pool_count;//常量池的数量 - cp_info constant_pool[constant_pool_count-1];//常量池 -``` - -紧接着主次版本号之后的是常量池,常量池的数量是 `constant_pool_count-1`(**常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项”**)。 - -常量池主要存放两大常量:字面量和符号引用。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量: - -- 类和接口的全限定名 -- 字段的名称和描述符 -- 方法的名称和描述符 - -常量池中每一项常量都是一个表,这 14 种表有一个共同的特点:**开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型.** - -| 类型 | 标志(tag) | 描述 | -| :------------------------------: | :---------: | :--------------------: | -| CONSTANT_utf8_info | 1 | UTF-8 编码的字符串 | -| CONSTANT_Integer_info | 3 | 整形字面量 | -| CONSTANT_Float_info | 4 | 浮点型字面量 | -| CONSTANT_Long_info | 5 | 长整型字面量 | -| CONSTANT_Double_info | 6 | 双精度浮点型字面量 | -| CONSTANT_Class_info | 7 | 类或接口的符号引用 | -| CONSTANT_String_info | 8 | 字符串类型字面量 | -| CONSTANT_FieldRef_info | 9 | 字段的符号引用 | -| CONSTANT_MethodRef_info | 10 | 类中方法的符号引用 | -| CONSTANT_InterfaceMethodRef_info | 11 | 接口中方法的符号引用 | -| CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 | -| CONSTANT_MethodType_info | 16 | 标志方法类型 | -| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | -| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | - -`.class` 文件可以通过`javap -v class类名` 指令来看一下其常量池中的信息(`javap -v class类名-> temp.txt`:将结果输出到 temp.txt 文件)。 - -### 访问标志(Access Flags) - -```java - u2 access_flags;//Class 的访问标记 -``` - -在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 `public` 或者 `abstract` 类型,如果是类的话是否声明为 `final` 等等。 - -类访问和属性修饰符: - -![类访问和属性修饰符](https://oss.javaguide.cn/github/javaguide/java/%E8%AE%BF%E9%97%AE%E6%A0%87%E5%BF%97.png) - -我们定义了一个 `Employee` 类 - -```java -package top.snailclimb.bean; -public class Employee { - ... -} -``` - -通过`javap -v class类名` 指令来看一下类的访问标志。 - -![查看类的访问标志](https://oss.javaguide.cn/github/javaguide/java/%E6%9F%A5%E7%9C%8B%E7%B1%BB%E7%9A%84%E8%AE%BF%E9%97%AE%E6%A0%87%E5%BF%97.png) - -### 当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合 - -```java - u2 this_class;//当前类 - u2 super_class;//父类 - u2 interfaces_count;//接口数量 - u2 interfaces[interfaces_count];//一个类可以实现多个接口 -``` - -Java 类的继承关系由类索引、父类索引和接口索引集合三项确定。类索引、父类索引和接口索引集合按照顺序排在访问标志之后, - -类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 `java.lang.Object` 之外,所有的 Java 类都有父类,因此除了 `java.lang.Object` 外,所有 Java 类的父类索引都不为 0。 - -接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按 `implements` (如果这个类本身是接口的话则是`extends`) 后的接口顺序从左到右排列在接口索引集合中。 - -### 字段表集合(Fields) - -```java - u2 fields_count;//字段数量 - field_info fields[fields_count];//一个类会可以有个字段 -``` - -字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。 - -**field info(字段表) 的结构:** - -![字段表的结构 ](https://oss.javaguide.cn/github/javaguide/java/%E5%AD%97%E6%AE%B5%E8%A1%A8%E7%9A%84%E7%BB%93%E6%9E%84.png) - -- **access_flags:** 字段的作用域(`public` ,`private`,`protected`修饰符),是实例变量还是类变量(`static`修饰符),可否被序列化(transient 修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写)。 -- **name_index:** 对常量池的引用,表示的字段的名称; -- **descriptor_index:** 对常量池的引用,表示字段和方法的描述符; -- **attributes_count:** 一个字段还会拥有一些额外的属性,attributes_count 存放属性的个数; -- **attributes[attributes_count]:** 存放具体属性具体内容。 - -上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型这些都是无法固定的,只能引用常量池中常量来描述。 - -**字段的 access_flag 的取值:** - -![字段的 access_flag 的取值](https://oss.javaguide.cn/JVM/image-20201031084342859.png) - -### 方法表集合(Methods) - -```java - u2 methods_count;//方法数量 - method_info methods[methods_count];//一个类可以有个多个方法 -``` - -methods_count 表示方法的数量,而 method_info 表示方法表。 - -Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。 - -**method_info(方法表的) 结构:** - -![方法表的结构](https://oss.javaguide.cn/github/javaguide/java/%E6%96%B9%E6%B3%95%E8%A1%A8%E7%9A%84%E7%BB%93%E6%9E%84.png) - -**方法表的 access_flag 取值:** - -![方法表的 access_flag 取值](https://oss.javaguide.cn/JVM/image-20201031084248965.png) - -注意:因为`volatile`修饰符和`transient`修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了`synchronized`、`native`、`abstract`等关键字修饰方法,所以也就多了这些关键字对应的标志。 - -### 属性表集合(Attributes) - -```java - u2 attributes_count;//此类的属性表中的属性数 - attribute_info attributes[attributes_count];//属性表集合 -``` - -在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。 - -## 参考 - -- 《实战 Java 虚拟机》 -- Chapter 4. The class File Format - Java Virtual Machine Specification: -- 实例分析 JAVA CLASS 的文件结构: -- 《Java 虚拟机原理图解》 1.2.2、Class 文件中的常量池详解(上): - - +Higher versions of the Java Virtual Machine can execute Class files generated by lower version compilers, but lower versions of the Java Virtual Machine cannot execute Class files generated by higher version compilers. Therefore, in actual development, we must ensure that the JDK version used for development is consistent diff --git a/docs/java/jvm/class-loading-process.md b/docs/java/jvm/class-loading-process.md index 6d6bcd2ea54..39a5bb04aa5 100644 --- a/docs/java/jvm/class-loading-process.md +++ b/docs/java/jvm/class-loading-process.md @@ -1,144 +1,144 @@ --- -title: 类加载过程详解 +title: Detailed Explanation of Class Loading Process category: Java tag: - JVM --- -## 类的生命周期 +## Lifecycle of a Class -类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。 +The entire lifecycle of a class can be simply summarized in 7 phases: Loading, Verification, Preparation, Resolution, Initialization, Using, and Unloading. Among these, Verification, Preparation, and Resolution can collectively be referred to as Linking. -这 7 个阶段的顺序如下图所示: +The order of these 7 phases is shown in the diagram below: -![一个类的完整生命周期](https://oss.javaguide.cn/github/javaguide/java/jvm/lifecycle-of-a-class.png) +![Complete Lifecycle of a Class](https://oss.javaguide.cn/github/javaguide/java/jvm/lifecycle-of-a-class.png) -## 类加载过程 +## Class Loading Process -**Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?** +**Class files need to be loaded into the virtual machine before they can be executed and used. So how does the virtual machine load these Class files?** -系统加载 Class 类型的文件主要三步:**加载->连接->初始化**。连接过程又可分为三步:**验证->准备->解析**。 +The system loads Class type files mainly in three steps: **Loading -> Linking -> Initialization**. The linking process can be further divided into three steps: **Verification -> Preparation -> Resolution**. -![类加载过程](https://oss.javaguide.cn/github/javaguide/java/jvm/class-loading-procedure.png) +![Class Loading Process](https://oss.javaguide.cn/github/javaguide/java/jvm/class-loading-procedure.png) -详见 [Java Virtual Machine Specification - 5.3. Creation and Loading](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.3 "Java Virtual Machine Specification - 5.3. Creation and Loading")。 +For more details, see [Java Virtual Machine Specification - 5.3. Creation and Loading](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.3 "Java Virtual Machine Specification - 5.3. Creation and Loading"). -### 加载 +### Loading -类加载过程的第一步,主要完成下面 3 件事情: +The first step in the class loading process mainly involves the following 3 tasks: -1. 通过全类名获取定义此类的二进制字节流。 -2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。 -3. 在内存中生成一个代表该类的 `Class` 对象,作为方法区这些数据的访问入口。 +1. Retrieve the binary byte stream that defines the class using its fully qualified name. +1. Convert the static storage structure represented by the byte stream into a runtime data structure for the method area. +1. Generate a `Class` object in memory that represents the class as an access point for the method area's data. -虚拟机规范上面这 3 点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取( `ZIP`、 `JAR`、`EAR`、`WAR`、网络、动态代理技术运行时动态生成、其他文件生成比如 `JSP`...)、怎样获取。 +The virtual machine specification is quite flexible regarding these 3 points. For example, "getting the binary byte stream that defines the class using its fully qualified name" does not specify where to acquire it from (e.g., `ZIP`, `JAR`, `EAR`, `WAR`, network, dynamically generated at runtime through dynamic proxy technology, other file generation such as `JSP`, etc.) or how to acquire it. -加载这一步主要是通过我们后面要讲到的 **类加载器** 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 **双亲委派模型** 决定(不过,我们也能打破由双亲委派模型)。 +This loading step is primarily completed by the **Class Loader**, which we will discuss later. There are many types of class loaders, and when we want to load a class, the specific class loader that does the loading is determined by the **parent delegation model** (however, we can also break this model). -> 类加载器、双亲委派模型也是非常重要的知识点,这部分内容在[类加载器详解](https://javaguide.cn/java/jvm/classloader.html "类加载器详解")这篇文章中有详细介绍到。阅读本篇文章的时候,大家知道有这么个东西就可以了。 +> Class loaders and the parent delegation model are also very important concepts. This part is elaborated in the article [Detailed Explanation of Class Loaders](https://javaguide.cn/java/jvm/classloader.html "Detailed Explanation of Class Loaders"). When reading this article, it's sufficient to know that such a concept exists. -每个 Java 类都有一个引用指向加载它的 `ClassLoader`。不过,数组类不是通过 `ClassLoader` 创建的,而是 JVM 在需要的时候自动创建的,数组类通过`getClassLoader()`方法获取 `ClassLoader` 的时候和该数组的元素类型的 `ClassLoader` 是一致的。 +Every Java class has a reference pointing to the `ClassLoader` that loaded it. However, array classes are not created through `ClassLoader`; they are automatically created by the JVM when needed. The `ClassLoader` for an array class is consistent with that of its element type. -一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 `loadClass()` 方法)。 +The loading phase for a non-array class (the action of getting the binary byte stream of the class) is the most controllable phase. At this step, we can implement and customize class loaders to control how to acquire the byte streams (by overriding the `loadClass()` method of a class loader). -加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。 +Some actions in the loading phase (such as certain bytecode file format verification actions) are interleaved with the linking phase. The loading phase may not be finished when the linking phase has already begun. -### 验证 +### Verification -**验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。** +**Verification is the first step in the linking phase, aimed at ensuring that the information contained in the byte stream of the Class file meets all constraints of the Java Virtual Machine Specification, ensuring that this information will not harm the safety of the virtual machine when executed as code.** -验证阶段这一步在整个类加载过程中耗费的资源还是相对较多的,但很有必要,可以有效防止恶意代码的执行。任何时候,程序安全都是第一位。 +The verification phase consumes relatively more resources in the entire class loading process, but it is very necessary as it effectively prevents the execution of malicious code. At any time, program safety is the top priority. -不过,验证阶段也不是必须要执行的阶段。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 `-Xverify:none` 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。但是需要注意的是 `-Xverify:none` 和 `-noverify` 在 JDK 13 中被标记为 deprecated ,在未来版本的 JDK 中可能会被移除。 +However, the verification phase is not always mandatory. If all the code running in the program (including custom code, third-party packages, externally loaded code, dynamically generated code, etc.) has been repeatedly used and verified, the `-Xverify:none` parameter can be considered to disable most class verification measures to shorten the class loading time of the virtual machine in production environments. However, it is noteworthy that `-Xverify:none` and `-noverify` have been marked as deprecated in JDK 13, and may be removed in future versions of the JDK. -验证阶段主要由四个检验阶段组成: +The verification phase primarily consists of four verification stages: -1. 文件格式验证(Class 文件格式检查) -2. 元数据验证(字节码语义检查) -3. 字节码验证(程序语义检查) -4. 符号引用验证(类的正确性检查) +1. File format verification (checks the class file format) +1. Metadata verification (checks the bytecode semantics) +1. Bytecode verification (program semantic checks) +1. Symbol reference verification (checks the correctness of the class) -![验证阶段示意图](https://oss.javaguide.cn/github/javaguide/java/jvm/class-loading-process-verification.png) +![Verification Phase Diagram](https://oss.javaguide.cn/github/javaguide/java/jvm/class-loading-process-verification.png) -文件格式验证这一阶段是基于该类的二进制字节流进行的,主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。除了这一阶段之外,其余三个验证阶段都是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。 +The file format verification phase is based on the class's binary byte stream, mainly aiming to ensure that the input byte stream can be correctly parsed and stored in the method area, and that it meets the requirements for describing Java type information. Aside from this phase, the other three verification stages are based on the storage structure of the method area and do not read or manipulate the byte stream directly. -> 方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 **类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据**。 +> The method area is a logical area of the JVM's runtime data area and is a shared memory area for all threads. When the virtual machine needs to use a class, it must read and parse the Class file to obtain related information and then store this information in the method area. The method area will store the **class information, field information, method information, constants, static variables, and code caches compiled by the just-in-time compiler** that have been loaded by the virtual machine. > -> 关于方法区的详细介绍,推荐阅读 [Java 内存区域详解](https://javaguide.cn/java/jvm/memory-area.html "Java 内存区域详解") 这篇文章。 +> For a detailed introduction to the method area, it is recommended to read [Detailed Explanation of Java Memory Areas](https://javaguide.cn/java/jvm/memory-area.html "Detailed Explanation of Java Memory Areas"). -符号引用验证发生在类加载过程中的解析阶段,具体点说是 JVM 将符号引用转化为直接引用的时候(解析阶段会介绍符号引用和直接引用)。 +Symbol reference verification occurs during the resolution phase of the class loading process, specifically when the JVM converts symbolic references into direct references (the resolution phase will introduce symbolic references and direct references). -符号引用验证的主要目的是确保解析阶段能正常执行,如果无法通过符号引用验证,JVM 会抛出异常,比如: +The main purpose of symbol reference verification is to ensure that the resolution phase can be executed normally. If symbol reference verification fails, the JVM will throw exceptions, such as: -- `java.lang.IllegalAccessError`:当类试图访问或修改它没有权限访问的字段,或调用它没有权限访问的方法时,抛出该异常。 -- `java.lang.NoSuchFieldError`:当类试图访问或修改一个指定的对象字段,而该对象不再包含该字段时,抛出该异常。 -- `java.lang.NoSuchMethodError`:当类试图访问一个指定的方法,而该方法不存在时,抛出该异常。 -- …… +- `java.lang.IllegalAccessError`: Thrown when a class attempts to access or modify a field it does not have permission to access, or calls a method it does not have permission to access. +- `java.lang.NoSuchFieldError`: Thrown when a class attempts to access or modify a specified object field, but that object no longer contains that field. +- `java.lang.NoSuchMethodError`: Thrown when a class attempts to access a specified method that does not exist. +- ... -### 准备 +### Preparation -**准备阶段是正式为类变量分配内存并设置类变量初始值的阶段**,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意: +**The preparation phase is the stage where memory is formally allocated for class variables and initializes them.** This memory is allocated in the method area. Notably, the following points should be noted regarding this phase: -1. 这时候进行内存分配的仅包括类变量( Class Variables ,即静态变量,被 `static` 关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。 -2. 从概念上讲,类变量所使用的内存都应当在 **方法区** 中进行分配。不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。相关阅读:[《深入理解 Java 虚拟机(第 3 版)》勘误#75](https://github.com/fenixsoft/jvm_book/issues/75 "《深入理解Java虚拟机(第3版)》勘误#75") -3. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如 0、0L、null、false 等),比如我们定义了`public static int value=111` ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字`public static final int value=111` ,那么准备阶段 value 的值就被赋值为 111。 +1. At this point, memory allocation only includes class variables (Class Variables, i.e., static variables marked with the `static` keyword, which are only related to the class), and does not include instance variables. Instance variables are allocated in the Java heap alongside the object during object instantiation. +1. Conceptually, the memory used by class variables should be allocated in the **method area**. It is worth noting that before JDK 7, when HotSpot used the permanent generation to implement the method area, this implementation fully complied with this logical concept. However, from JDK 7 onward, HotSpot has moved the string constant pool, static variables, etc., which were originally placed in the permanent generation, to the heap; at this point, class variables are stored in the Java heap alongside Class objects. For further reading, see the erratum #75 in the book [Understanding Java Virtual Machine (3rd Edition)](https://github.com/fenixsoft/jvm_book/issues/75 "Understanding Java Virtual Machine (3rd Edition) Erratum #75"). +1. The initial value set here is "typically" the default zero value of the data type (such as 0, 0L, null, false, etc.). For example, if we define `public static int value=111`, the initial value of the variable `value` in the preparation phase is 0 rather than 111 (it will be assigned in the initialization phase). A special case is when the `final` keyword is added to `value`, as in `public static final int value=111`; in this case, the value of `value` in the preparation phase is assigned 111. -**基本数据类型的零值**:(图片来自《深入理解 Java 虚拟机》第 3 版 7.3.3 ) +**Zero values of primitive data types**: (Image from "Understanding Java Virtual Machine" 3rd Edition 7.3.3) -![基本数据类型的零值](https://oss.javaguide.cn/github/javaguide/java/%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E7%9A%84%E9%9B%B6%E5%80%BC.png) +![Zero Values of Primitive Data Types](https://oss.javaguide.cn/github/javaguide/java/%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E7%9A%84%E9%9B%B6%E5%80%BC.png) -### 解析 +### Resolution -**解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。** 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。 +**The resolution phase is the process where the virtual machine replaces symbol references in the constant pool with direct references.** The resolution action primarily targets seven types of symbol references: class or interface, fields, class methods, interface methods, method types, method handles, and invocation qualifiers. -《深入理解 Java 虚拟机》7.3.4 节第三版对符号引用和直接引用的解释如下: +In Section 7.3.4 of "Understanding Java Virtual Machine" 3rd Edition, the explanations for symbol references and direct references are as follows: -![符号引用和直接引用](https://oss.javaguide.cn/github/javaguide/java/jvm/symbol-reference-and-direct-reference.png) +![Symbol References and Direct References](https://oss.javaguide.cn/github/javaguide/java/jvm/symbol-reference-and-direct-reference.png) -举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。 +For example, when invoking a method during program execution, the system needs to know the location of that method. The Java Virtual Machine prepares a method table for each class to store all methods within that class. When a method of a class needs to be called, as long as the offset of the method in the method table is known, that method can be called directly. The resolution operation converts the symbolic reference into the direct method table position of the target method within the class, enabling the method to be called. -综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。 +In summary, the resolution phase is the process by which the virtual machine replaces symbolic references in the constant pool with direct references, obtaining pointers or offsets of classes, fields, or methods in memory. -### 初始化 +### Initialization -**初始化阶段是执行初始化方法 ` ()`方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。** +**The initialization phase is the process of executing the initialization method `()`, and is the last step of class loading. It is at this point that the JVM truly begins executing the Java program code (bytecode) defined in the class.** -> 说明:` ()`方法是编译之后自动生成的。 +> Note: The `()` method is automatically generated after compilation. -对于` ()` 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 ` ()` 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个线程阻塞,并且这种阻塞很难被发现。 +The JVM will ensure the safety of calling the `()` method in a multithreaded environment. Since the `()` method is thread-safe with locking, initializing a class in a multithreaded environment may cause multiple threads to block, and such blocking is difficult to detect. -对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类): +The JVM strictly specifies that there are only 6 situations in which a class must be initialized (a class will only be initialized when actively used): -1. 遇到 `new`、`getstatic`、`putstatic` 或 `invokestatic` 这 4 条字节码指令时: - - `new`: 创建一个类的实例对象。 - - `getstatic`、`putstatic`: 读取或设置一个类型的静态字段(被 `final` 修饰、已在编译期把结果放入常量池的静态字段除外)。 - - `invokestatic`: 调用类的静态方法。 -2. 使用 `java.lang.reflect` 包的方法对类进行反射调用时如 `Class.forName("...")`, `newInstance()` 等等。如果类没初始化,需要触发其初始化。 -3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。 -4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 `main` 方法的那个类),虚拟机会先初始化这个类。 -5. `MethodHandle` 和 `VarHandle` 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,就必须先使用 `findStaticVarHandle` 来初始化要调用的类。 -6. **「补充,来自[issue745](https://github.com/Snailclimb/JavaGuide/issues/745 "issue745")」** 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。 +1. When encountering the bytecode instructions `new`, `getstatic`, `putstatic`, or `invokestatic`: + - `new`: Create an instance object of a class. + - `getstatic`, `putstatic`: Read or set a static field of a type (except for static fields that are marked as `final` or have their results placed into the constant pool at compile time). + - `invokestatic`: Call a static method of a class. +1. When reflecting on a class using methods from the `java.lang.reflect` package, such as `Class.forName("...")`, `newInstance()`, etc. If the class is not initialized, it needs to trigger its initialization. +1. When initializing a class, if its parent class is not initialized, it first triggers the initialization of that parent class. +1. When the JVM starts, the user defines a main class (the class containing the `main` method) to execute, and the JVM will first initialize that class. +1. `MethodHandle` and `VarHandle` can be seen as lightweight reflection mechanisms, and to use these two calls, you must first initialize the class you intend to call using `findStaticVarHandle`. +1. **"Supplementary note from [issue745](https://github.com/Snailclimb/JavaGuide/issues/745 "issue745")"**: When an interface defines a default method (marked with the `default` keyword) introduced in JDK 8, if an implementation of that interface is initialized, then that interface must be initialized before it. -## 类卸载 +## Class Unloading -> 卸载这部分内容来自 [issue#662](https://github.com/Snailclimb/JavaGuide/issues/662 "issue#662")由 **[guang19](https://github.com/guang19 "guang19")** 补充完善。 +> The content on unloading comes from [issue#662](https://github.com/Snailclimb/JavaGuide/issues/662 "issue#662") contributed by **[guang19](https://github.com/guang19 "guang19")**. -**卸载类即该类的 Class 对象被 GC。** +**Unloading a class means the Class object of that class is collected by the GC.** -卸载类需要满足 3 个要求: +To unload a class, three requirements must be met: -1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。 -2. 该类没有在其他任何地方被引用 -3. 该类的类加载器的实例已被 GC +1. All instances of that class have been collected by the GC, meaning no instance objects of that class exist in the heap. +1. The class is not referenced anywhere else. +1. The instance of the class loader for that class has been collected by the GC. -所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。 +Therefore, classes loaded by the JVM's built-in class loaders will not be unloaded within the JVM's lifecycle. However, classes loaded by our custom class loaders may be unloaded. -只要想通一点就好了,JDK 自带的 `BootstrapClassLoader`, `ExtClassLoader`, `AppClassLoader` 负责加载 JDK 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。 +The key point to understand is that the JDK's built-in `BootstrapClassLoader`, `ExtClassLoader`, and `AppClassLoader` are responsible for loading classes provided by the JDK, so their instances (class loaders) will definitely not be collected. In contrast, the instances of our custom class loaders can be collected, and thus the classes loaded by our custom loaders can be unloaded. -**参考** +**References** -- 《深入理解 Java 虚拟机》 -- 《实战 Java 虚拟机》 -- Chapter 5. Loading, Linking, and Initializing - Java Virtual Machine Specification: +- "Understanding Java Virtual Machine" +- "Practical Java Virtual Machine" +- Chapter 5. Loading, Linking, and Initializing - Java Virtual Machine Specification: diff --git a/docs/java/jvm/classloader.md b/docs/java/jvm/classloader.md index 35a8bfd0eda..6e009f6362d 100644 --- a/docs/java/jvm/classloader.md +++ b/docs/java/jvm/classloader.md @@ -1,32 +1,32 @@ --- -title: 类加载器详解(重点) +title: Detailed Explanation of Class Loaders (Key Points) category: Java tag: - JVM --- -## 回顾一下类加载过程 +## A Review of the Class Loading Process -开始介绍类加载器和双亲委派模型之前,简单回顾一下类加载过程。 +Before introducing class loaders and the parent delegation model, let's briefly review the class loading process. -- 类加载过程:**加载->连接->初始化**。 -- 连接过程又可分为三步:**验证->准备->解析**。 +- Class loading process: **Loading -> Linking -> Initialization**. +- The linking process can be further divided into three steps: **Verification -> Preparation -> Resolution**. -![类加载过程](https://oss.javaguide.cn/github/javaguide/java/jvm/class-loading-procedure.png) +![Class Loading Process](https://oss.javaguide.cn/github/javaguide/java/jvm/class-loading-procedure.png) -加载是类加载过程的第一步,主要完成下面 3 件事情: +Loading is the first step in the class loading process, mainly completing the following three tasks: -1. 通过全类名获取定义此类的二进制字节流 -2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构 -3. 在内存中生成一个代表该类的 `Class` 对象,作为方法区这些数据的访问入口 +1. Obtaining the binary byte stream that defines the class through its fully qualified name. +1. Converting the static storage structure represented by the byte stream into a runtime data structure in the method area. +1. Generating a `Class` object in memory that represents the class, serving as an access point for these data in the method area. -## 类加载器 +## Class Loaders -### 类加载器介绍 +### Introduction to Class Loaders -类加载器从 JDK 1.0 就出现了,最初只是为了满足 Java Applet(已经被淘汰) 的需要。后来,慢慢成为 Java 程序中的一个重要组成部分,赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。 +Class loaders have been around since JDK 1.0, initially created to meet the needs of Java Applets (which have since been deprecated). Over time, they have become an important part of Java programs, enabling Java classes to be dynamically loaded into the JVM and executed. -根据官方 API 文档的介绍: +According to the official API documentation: > A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system. > @@ -34,17 +34,17 @@ tag: > > Class objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader. -翻译过来大概的意思是: +In summary, it means: -> 类加载器是一个负责加载类的对象。`ClassLoader` 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。 +> A class loader is an object responsible for loading classes. `ClassLoader` is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system. > -> 每个 Java 类都有一个引用指向加载它的 `ClassLoader`。不过,数组类不是通过 `ClassLoader` 创建的,而是 JVM 在需要的时候自动创建的,数组类通过`getClassLoader()`方法获取 `ClassLoader` 的时候和该数组的元素类型的 `ClassLoader` 是一致的。 +> Every Java class has a reference pointing to the `ClassLoader` that loaded it. However, array classes are not created by `ClassLoader`, but are automatically created by the JVM when needed. The `ClassLoader` for an array class, when obtained via `getClassLoader()`, is the same as that of its element type. -从上面的介绍可以看出: +From the above introduction, we can see: -- 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。 -- 每个 Java 类都有一个引用指向加载它的 `ClassLoader`。 -- 数组类不是通过 `ClassLoader` 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。 +- A class loader is an object responsible for loading classes, implementing the loading step in the class loading process. +- Every Java class has a reference pointing to the `ClassLoader` that loaded it. +- Array classes are not created by `ClassLoader` (array classes do not have corresponding binary byte streams) but are generated directly by the JVM. ```java class Class { @@ -58,334 +58,24 @@ class Class { } ``` -简单来说,**类加载器的主要作用就是动态加载 Java 类的字节码( `.class` 文件)到 JVM 中(在内存中生成一个代表该类的 `Class` 对象)。** 字节码可以是 Java 源程序(`.java`文件)经过 `javac` 编译得来,也可以是通过工具动态生成或者通过网络下载得来。 +In simple terms, **the main function of a class loader is to dynamically load the bytecode of Java classes (`.class` files) into the JVM (creating a `Class` object in memory that represents the class).** The bytecode can be derived from Java source code (`.java` files) compiled by `javac`, or it can be dynamically generated by tools or downloaded over the network. -其实除了加载类之外,类加载器还可以加载 Java 应用所需的资源如文本、图像、配置文件、视频等等文件资源。本文只讨论其核心功能:加载类。 +In addition to loading classes, class loaders can also load resources required by Java applications, such as text, images, configuration files, videos, and other file resources. This article will only discuss its core function: loading classes. -### 类加载器加载规则 +### Class Loader Loading Rules -JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。 +When the JVM starts, it does not load all classes at once but dynamically loads them as needed. This means that most classes are loaded only when they are actually used, which is more memory-friendly. -对于已经加载的类会被放在 `ClassLoader` 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。 +Classes that have already been loaded are stored in the `ClassLoader`. During class loading, the system first checks whether the current class has already been loaded. If it has, the loaded class is returned directly; otherwise, it attempts to load it. In other words, for a class loader, a class with the same binary name will only be loaded once. ```java public abstract class ClassLoader { ... private final ClassLoader parent; - // 由这个类加载器加载的类。 + // Classes loaded by this class loader. private final Vector> classes = new Vector<>(); - // 由VM调用,用此类加载器记录每个已加载类。 + // Called by VM to record each loaded class with this class loader. void addClass(Class c) { classes.addElement(c); } - ... -} -``` - -### 类加载器总结 - -JVM 中内置了三个重要的 `ClassLoader`: - -1. **`BootstrapClassLoader`(启动类加载器)**:最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( `%JAVA_HOME%/lib`目录下的 `rt.jar`、`resources.jar`、`charsets.jar`等 jar 包和类)以及被 `-Xbootclasspath`参数指定的路径下的所有类。 -2. **`ExtensionClassLoader`(扩展类加载器)**:主要负责加载 `%JRE_HOME%/lib/ext` 目录下的 jar 包和类以及被 `java.ext.dirs` 系统变量所指定的路径下的所有类。 -3. **`AppClassLoader`(应用程序类加载器)**:面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。 - -> 🌈 拓展一下: -> -> - **`rt.jar`**:rt 代表“RunTime”,`rt.jar`是 Java 基础类库,包含 Java doc 里面看到的所有的类的类文件。也就是说,我们常用内置库 `java.xxx.*`都在里面,比如`java.util.*`、`java.io.*`、`java.nio.*`、`java.lang.*`、`java.sql.*`、`java.math.*`。 -> - Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 `java.base` 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。 - -除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。就比如说,我们可以对 Java 类的字节码( `.class` 文件)进行加密,加载时再利用自定义的类加载器对其解密。 - -![类加载器层次关系图](https://oss.javaguide.cn/github/javaguide/java/jvm/class-loader-parents-delegation-model.png) - -除了 `BootstrapClassLoader` 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 `ClassLoader`抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。 - -每个 `ClassLoader` 可以通过`getParent()`获取其父 `ClassLoader`,如果获取到 `ClassLoader` 为`null`的话,那么该类是通过 `BootstrapClassLoader` 加载的。 - -```java -public abstract class ClassLoader { - ... - // 父加载器 - private final ClassLoader parent; - @CallerSensitive - public final ClassLoader getParent() { - //... - } - ... -} -``` - -**为什么 获取到 `ClassLoader` 为`null`就是 `BootstrapClassLoader` 加载的呢?** 这是因为`BootstrapClassLoader` 由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。 - -下面我们来看一个获取 `ClassLoader` 的小案例: - -```java -public class PrintClassLoaderTree { - - public static void main(String[] args) { - - ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader(); - - StringBuilder split = new StringBuilder("|--"); - boolean needContinue = true; - while (needContinue){ - System.out.println(split.toString() + classLoader); - if(classLoader == null){ - needContinue = false; - }else{ - classLoader = classLoader.getParent(); - split.insert(0, "\t"); - } - } - } - -} ``` - -输出结果(JDK 8 ): - -```plain -|--sun.misc.Launcher$AppClassLoader@18b4aac2 - |--sun.misc.Launcher$ExtClassLoader@53bd815b - |--null -``` - -从输出结果可以看出: - -- 我们编写的 Java 类 `PrintClassLoaderTree` 的 `ClassLoader` 是`AppClassLoader`; -- `AppClassLoader`的父 `ClassLoader` 是`ExtClassLoader`; -- `ExtClassLoader`的父`ClassLoader`是`Bootstrap ClassLoader`,因此输出结果为 null。 - -### 自定义类加载器 - -我们前面也说说了,除了 `BootstrapClassLoader` 其他类加载器均由 Java 实现且全部继承自`java.lang.ClassLoader`。如果我们要自定义自己的类加载器,很明显需要继承 `ClassLoader`抽象类。 - -`ClassLoader` 类有两个关键的方法: - -- `protected Class loadClass(String name, boolean resolve)`:加载指定二进制名称的类,实现了双亲委派机制 。`name` 为类的二进制名称,`resolve` 如果为 true,在加载时调用 `resolveClass(Class c)` 方法解析该类。 -- `protected Class findClass(String name)`:根据类的二进制名称来查找类,默认实现是空方法。 - -官方 API 文档中写到: - -> Subclasses of `ClassLoader` are encouraged to override `findClass(String name)`, rather than this method. -> -> 建议 `ClassLoader`的子类重写 `findClass(String name)`方法而不是`loadClass(String name, boolean resolve)` 方法。 - -如果我们不想打破双亲委派模型,就重写 `ClassLoader` 类中的 `findClass()` 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 `loadClass()` 方法。 - -## 双亲委派模型 - -### 双亲委派模型介绍 - -类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载呢?这就需要提到双亲委派模型了。 - -根据官网介绍: - -> The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine's built-in class loader, called the "bootstrap class loader", does not itself have a parent but may serve as the parent of a ClassLoader instance. - -翻译过来大概的意思是: - -> `ClassLoader` 类使用委托模型来搜索类和资源。每个 `ClassLoader` 实例都有一个相关的父类加载器。需要查找类或资源时,`ClassLoader` 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。 -> 虚拟机中被称为 "bootstrap class loader"的内置类加载器本身没有父类加载器,但是可以作为 `ClassLoader` 实例的父类加载器。 - -从上面的介绍可以看出: - -- `ClassLoader` 类使用委托模型来搜索类和资源。 -- 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。 -- `ClassLoader` 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。 - -下图展示的各种类加载器之间的层次关系被称为类加载器的“**双亲委派模型(Parents Delegation Model)**”。 - -![类加载器层次关系图](https://oss.javaguide.cn/github/javaguide/java/jvm/class-loader-parents-delegation-model.png) - -注意 ⚠️:双亲委派模型并不是一种强制性的约束,只是 JDK 官方推荐的一种方式。如果我们因为某些特殊需求想要打破双亲委派模型,也是可以的,后文会介绍具体的方法。 - -其实这个双亲翻译的容易让别人误解,我们一般理解的双亲都是父母,这里的双亲更多地表达的是“父母这一辈”的人而已,并不是说真的有一个 `MotherClassLoader` 和一个`FatherClassLoader` 。个人觉得翻译成单亲委派模型更好一些,不过,国内既然翻译成了双亲委派模型并流传了,按照这个来也没问题,不要被误解了就好。 - -另外,类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。 - -```java -public abstract class ClassLoader { - ... - // 组合 - private final ClassLoader parent; - protected ClassLoader(ClassLoader parent) { - this(checkCreateClassLoader(), parent); - } - ... -} -``` - -在面向对象编程中,有一条非常经典的设计原则:**组合优于继承,多用组合少用继承。** - -### 双亲委派模型的执行流程 - -双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 `java.lang.ClassLoader` 的 `loadClass()` 中,相关代码如下所示。 - -```java -protected Class loadClass(String name, boolean resolve) - throws ClassNotFoundException -{ - synchronized (getClassLoadingLock(name)) { - //首先,检查该类是否已经加载过 - Class c = findLoadedClass(name); - if (c == null) { - //如果 c 为 null,则说明该类没有被加载过 - long t0 = System.nanoTime(); - try { - if (parent != null) { - //当父类的加载器不为空,则通过父类的loadClass来加载该类 - c = parent.loadClass(name, false); - } else { - //当父类的加载器为空,则调用启动类加载器来加载该类 - c = findBootstrapClassOrNull(name); - } - } catch (ClassNotFoundException e) { - //非空父类的类加载器无法找到相应的类,则抛出异常 - } - - if (c == null) { - //当父类加载器无法加载时,则调用findClass方法来加载该类 - //用户可通过覆写该方法,来自定义类加载器 - long t1 = System.nanoTime(); - c = findClass(name); - - //用于统计类加载器相关的信息 - sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); - sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); - sun.misc.PerfCounter.getFindClasses().increment(); - } - } - if (resolve) { - //对类进行link操作 - resolveClass(c); - } - return c; - } -} -``` - -每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。 - -结合上面的源码,简单总结一下双亲委派模型的执行流程: - -- 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。 -- 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 `loadClass()`方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 `BootstrapClassLoader` 中。 -- 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 `findClass()` 方法来加载类)。 -- 如果子类加载器也无法加载这个类,那么它会抛出一个 `ClassNotFoundException` 异常。 - -🌈 拓展一下: - -**JVM 判定两个 Java 类是否相同的具体规则**:JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 `Class` 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。 - -### 双亲委派模型的好处 - -双亲委派模型是 Java 类加载机制的重要组成部分,它通过委派父加载器优先加载类的方式,实现了两个关键的安全目标:避免类的重复加载和防止核心 API 被篡改。 - -JVM 区分不同类的依据是类名加上加载该类的类加载器,即使类名相同,如果由不同的类加载器加载,也会被视为不同的类。 双亲委派模型确保核心类总是由 `BootstrapClassLoader` 加载,保证了核心类的唯一性。 - -例如,当应用程序尝试加载 `java.lang.Object` 时,`AppClassLoader` 会首先将请求委派给 `ExtClassLoader`,`ExtClassLoader` 再委派给 `BootstrapClassLoader`。`BootstrapClassLoader` 会在 JRE 核心类库中找到并加载 `java.lang.Object`,从而保证应用程序使用的是 JRE 提供的标准版本。 - -有很多小伙伴就要说了:“那我绕过双亲委派模型不就可以了么?”。 - -然而,即使攻击者绕过了双亲委派模型,Java 仍然具备更底层的安全机制来保护核心类库。`ClassLoader` 的 `preDefineClass` 方法会在定义类之前进行类名校验。任何以 `"java."` 开头的类名都会触发 `SecurityException`,阻止恶意代码定义或加载伪造的核心类。 - -JDK 8 中`ClassLoader#preDefineClass` 方法源码如下: - -```java -private ProtectionDomain preDefineClass(String name, - ProtectionDomain pd) - { - // 检查类名是否合法 - if (!checkName(name)) { - throw new NoClassDefFoundError("IllegalName: " + name); - } - - // 防止在 "java.*" 包中定义类。 - // 此检查对于安全性至关重要,因为它可以防止恶意代码替换核心 Java 类。 - // JDK 9 利用平台类加载器增强了 preDefineClass 方法的安全性 - if ((name != null) && name.startsWith("java.")) { - throw new SecurityException - ("禁止的包名: " + - name.substring(0, name.lastIndexOf('.'))); - } - - // 如果未指定 ProtectionDomain,则使用默认域(defaultDomain)。 - if (pd == null) { - pd = defaultDomain; - } - - if (name != null) { - checkCerts(name, pd.getCodeSource()); - } - - return pd; - } -``` - -JDK 9 中这部分逻辑有所改变,多了平台类加载器(`getPlatformClassLoader()` 方法获取),增强了 `preDefineClass` 方法的安全性。这里就不贴源码了,感兴趣的话,可以自己去看看。 - -### 打破双亲委派模型方法 - -~~为了避免双亲委托机制,我们可以自己定义一个类加载器,然后重写 `loadClass()` 即可。~~ - -**🐛 修正(参见:[issue871](https://github.com/Snailclimb/JavaGuide/issues/871) )**:自定义加载器的话,需要继承 `ClassLoader` 。如果我们不想打破双亲委派模型,就重写 `ClassLoader` 类中的 `findClass()` 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 `loadClass()` 方法。 - -为什么是重写 `loadClass()` 方法打破双亲委派模型呢?双亲委派模型的执行流程已经解释了: - -> 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 `loadClass()`方法来加载类)。 - -重写 `loadClass()`方法之后,我们就可以改变传统双亲委派模型的执行流程。例如,子类加载器可以在委派给父类加载器之前,先自己尝试加载这个类,或者在父类加载器返回之后,再尝试从其他地方加载这个类。具体的规则由我们自己实现,根据项目需求定制化。 - -我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 `WebAppClassLoader` 来打破双亲委托机制。这也是 Tomcat 下 Web 应用之间的类实现隔离的具体原理。 - -Tomcat 的类加载器的层次结构如下: - -![Tomcat 的类加载器的层次结构](https://oss.javaguide.cn/github/javaguide/java/jvm/tomcat-class-loader-parents-delegation-model.png) - -Tomcat 这四个自定义的类加载器对应的目录如下: - -- `CommonClassLoader`对应`/common/*` -- `CatalinaClassLoader`对应`/server/*` -- `SharedClassLoader`对应 `/shared/*` -- `WebAppClassloader`对应 `/webapps//WEB-INF/*` - -从图中的委派关系中可以看出: - -- `CommonClassLoader`作为 `CatalinaClassLoader` 和 `SharedClassLoader` 的父加载器。`CommonClassLoader` 能加载的类都可以被 `CatalinaClassLoader` 和 `SharedClassLoader` 使用。因此,`CommonClassLoader` 是为了实现公共类库(可以被所有 Web 应用和 Tomcat 内部组件使用的类库)的共享和隔离。 -- `CatalinaClassLoader` 和 `SharedClassLoader` 能加载的类则与对方相互隔离。`CatalinaClassLoader` 用于加载 Tomcat 自身的类,为了隔离 Tomcat 本身的类和 Web 应用的类。`SharedClassLoader` 作为 `WebAppClassLoader` 的父加载器,专门来加载 Web 应用之间共享的类比如 Spring、Mybatis。 -- 每个 Web 应用都会创建一个单独的 `WebAppClassLoader`,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 `WebAppClassLoader`。各个 `WebAppClassLoader` 实例之间相互隔离,进而实现 Web 应用之间的类隔。 - -单纯依靠自定义类加载器没办法满足某些场景的要求,例如,有些情况下,高层的类加载器需要加载低层的加载器才能加载的类。 - -比如,SPI 中,SPI 的接口(如 `java.sql.Driver`)是由 Java 核心库提供的,由`BootstrapClassLoader` 加载。而 SPI 的实现(如`com.mysql.cj.jdbc.Driver`)是由第三方供应商提供的,它们是由应用程序类加载器或者自定义类加载器来加载的。默认情况下,一个类及其依赖类由同一个类加载器加载。所以,加载 SPI 的接口的类加载器(`BootstrapClassLoader`)也会用来加载 SPI 的实现。按照双亲委派模型,`BootstrapClassLoader` 是无法找到 SPI 的实现类的,因为它无法委托给子类加载器去尝试加载。 - -再比如,假设我们的项目中有 Spring 的 jar 包,由于其是 Web 应用之间共享的,因此会由 `SharedClassLoader` 加载(Web 服务器是 Tomcat)。我们项目中有一些用到了 Spring 的业务类,比如实现了 Spring 提供的接口、用到了 Spring 提供的注解。所以,加载 Spring 的类加载器(也就是 `SharedClassLoader`)也会用来加载这些业务类。但是业务类在 Web 应用目录下,不在 `SharedClassLoader` 的加载路径下,所以 `SharedClassLoader` 无法找到业务类,也就无法加载它们。 - -如何解决这个问题呢? 这个时候就需要用到 **线程上下文类加载器(`ThreadContextClassLoader`)** 了。 - -拿 Spring 这个例子来说,当 Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。还记得我上面说的吗?每个 Web 应用都会创建一个单独的 `WebAppClassLoader`,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 `WebAppClassLoader`。这样就可以让高层的类加载器(`SharedClassLoader`)借助子类加载器( `WebAppClassLoader`)来加载业务类,破坏了 Java 的类加载委托机制,让应用逆向使用类加载器。 - -线程上下文类加载器的原理是将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用。这个类加载器通常是由应用程序或者容器(如 Tomcat)设置的。 - -`Java.lang.Thread` 中的`getContextClassLoader()`和 `setContextClassLoader(ClassLoader cl)`分别用来获取和设置线程的上下文类加载器。如果没有通过`setContextClassLoader(ClassLoader cl)`进行设置的话,线程将继承其父线程的上下文类加载器。 - -Spring 获取线程线程上下文类加载器的代码如下: - -```java -cl = Thread.currentThread().getContextClassLoader(); -``` - -感兴趣的小伙伴可以自行深入研究一下 Tomcat 打破双亲委派模型的原理,推荐资料:[《深入拆解 Tomcat & Jetty》](http://gk.link/a/10Egr)。 - -## 推荐阅读 - -- 《深入拆解 Java 虚拟机》 -- 深入分析 Java ClassLoader 原理: -- Java 类加载器(ClassLoader): -- Class Loaders in Java: -- Class ClassLoader - Oracle 官方文档: -- 老大难的 Java ClassLoader 再不理解就老了: - - diff --git a/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md b/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md index 33fc2d8767b..b315df2aef9 100644 --- a/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md +++ b/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md @@ -1,26 +1,26 @@ --- -title: JDK监控和故障处理工具总结 +title: Summary of JDK Monitoring and Fault Handling Tools category: Java tag: - JVM --- -## JDK 命令行工具 +## JDK Command Line Tools -这些命令在 JDK 安装目录下的 bin 目录下: +These commands are located in the bin directory of the JDK installation: -- **`jps`** (JVM Process Status): 类似 UNIX 的 `ps` 命令。用于查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息; -- **`jstat`**(JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据; -- **`jinfo`** (Configuration Info for Java) : Configuration Info for Java,显示虚拟机配置信息; -- **`jmap`** (Memory Map for Java) : 生成堆转储快照; -- **`jhat`** (JVM Heap Dump Browser) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果。JDK9 移除了 jhat; -- **`jstack`** (Stack Trace for Java) : 生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。 +- **`jps`** (JVM Process Status): Similar to the UNIX `ps` command. Used to view information about all Java processes, including the main class, input parameters, and JVM parameters; +- **`jstat`** (JVM Statistics Monitoring Tool): Used to collect various runtime data from the HotSpot virtual machine; +- **`jinfo`** (Configuration Info for Java): Displays configuration information for the virtual machine; +- **`jmap`** (Memory Map for Java): Generates heap dump snapshots; +- **`jhat`** (JVM Heap Dump Browser): Used to analyze heap dump files. It sets up an HTTP/HTML server for users to view analysis results in a browser. `jhat` was removed in JDK9; +- **`jstack`** (Stack Trace for Java): Generates a snapshot of the threads in the virtual machine at the current moment, which is a collection of the method stacks being executed by each thread in the virtual machine. -### `jps`:查看所有 Java 进程 +### `jps`: View All Java Processes -`jps`(JVM Process Status) 命令类似 UNIX 的 `ps` 命令。 +The `jps` (JVM Process Status) command is similar to the UNIX `ps` command. -`jps`:显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID(Local Virtual Machine Identifier,LVMID)。`jps -q`:只输出进程的本地虚拟机唯一 ID。 +`jps`: Displays the main class name executed by the virtual machine and the local virtual machine unique ID (Local Virtual Machine Identifier, LVMID). `jps -q`: Outputs only the local virtual machine unique ID of the processes. ```powershell C:\Users\SnailClimb>jps @@ -31,7 +31,7 @@ C:\Users\SnailClimb>jps 17340 NettyServer ``` -`jps -l`:输出主类的全名,如果进程执行的是 Jar 包,输出 Jar 路径。 +`jps -l`: Outputs the full name of the main class. If the process is executing a Jar file, it outputs the Jar path. ```powershell C:\Users\SnailClimb>jps -l @@ -42,284 +42,39 @@ C:\Users\SnailClimb>jps -l 17340 firstNettyDemo.NettyServer ``` -`jps -v`:输出虚拟机进程启动时 JVM 参数。 +`jps -v`: Outputs the JVM parameters used when starting the virtual machine process. -`jps -m`:输出传递给 Java 进程 main() 函数的参数。 +`jps -m`: Outputs the parameters passed to the Java process's main() function. -### `jstat`: 监视虚拟机各种运行状态信息 +### `jstat`: Monitor Various Runtime Status Information of the Virtual Machine -jstat(JVM Statistics Monitoring Tool) 使用于监视虚拟机各种运行状态信息的命令行工具。 它可以显示本地或者远程(需要远程主机提供 RMI 支持)虚拟机进程中的类信息、内存、垃圾收集、JIT 编译等运行数据,在没有 GUI,只提供了纯文本控制台环境的服务器上,它将是运行期间定位虚拟机性能问题的首选工具。 +`jstat` (JVM Statistics Monitoring Tool) is a command-line tool used to monitor various runtime status information of the virtual machine. It can display class information, memory, garbage collection, JIT compilation, and other runtime data from local or remote (requires RMI support from the remote host) virtual machine processes. It is the preferred tool for locating virtual machine performance issues on servers without a GUI, providing only a pure text console environment. -**`jstat` 命令使用格式:** +**`jstat` command usage format:** ```powershell jstat -